title: DDCTF2019 HOMEBREW EVENT LOOP.md
date: 2022-09-27 11:42:22
tags:
[DDCTF 2019]homebrew event loop
代码审计
from flask import Flask, session, request, Response
import urllib
app = Flask(__name__)
app.secret_key = '*********************' # censored
url_prefix = '/d5afe1f66147e857'
def FLAG():
return '*********************' # 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(
'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_0123456789:;#')
resp = None
while len(request.event_queue) > 0:
\# `event` is something like "action:ACTION;ARGS0#ARGS1#ARGS2......"
event = request.event_queue[0]
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:
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.<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'])
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:
\# show_flag_function has been disabled, no worries
trigger_event('func:show_flag;' + FLAG())
trigger_event('action:view;index')
if __name__ == '__main__':
app.run(debug=False, host='0.0.0.0')
审计代码把 我一直不擅长python代码审计 这个题其实页折磨了好久
但是有一句话 当你感觉到困难的时候 就是我提升的开始
所以都是在折磨中成长的
废话不多说 审计代码
是个pythonflask框架
对于python的flask框架 在路由下 执行得到函数是 第一个定义的函数
所以我们就开始从第一个函数 entry_point来开始
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()
这里的 urllib.unquote函数就是对参数进行url解码
里面的request.query_string函数就是接受url穿过来的东西
如果我们url传过来的东西为空 或者 我们穿过来的东西没有以action:开头的时候 后者长度大于100 就强制querystring赋值为 action:index;False#False
if 'num_items' not in session:
session['num_items'] = 0
session['points'] = 3
session['log'] = []
这些东西应该是如果session中没有num_items 的话 就初始化一些东西
request.prev_session = dict(session)
应该就是放如session里面吧 (不过这个应该没什么用)
然后执行
trigger_event(querystring)
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)
这里append是在里面列表里面加入我们刚刚传入的值
应该是个log日志把
然后后来就是把我们传入的东西页加入到request.event_queue
继续回到entry_point():函数
执行 execute_event_loop()函数
def execute_event_loop():
valid_event_chars = set(
'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_0123456789:;#')
resp = None
while len(request.event_queue) > 0:
# `event` is something like "action:ACTION;ARGS0#ARGS1#ARGS2......"
event = request.event_queue[0]
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
看看这个是什么东西 定义了2个变量
valid_event_chars
resp = None
event = request.event_queue[0]
把我们第一个传入的东西赋值为event
request.event_queue = request.event_queue[1:]
然后 从request.event_queue 去除掉第一个
if not event.startswith(('action:', 'func:')):
continue
如果我们传入的第一个东西没有以action:或者func:
开头的话 就退出这次循环
一个个检查我们刚才传入的字符不在valid_event_chars中的话 就退出
is_action = event[0] == 'a'
这个等会会说到
action = get_mid_str(event, ':', ';')
继续跟进get_mid_str函数
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
如果 第三个参数为空的话就返回 prefix后面的所有字符
如果第三个参数不为空的话 就返回 第二个参数到第三个参数之间的字符
args = get_mid_str(event, action+';').split('#')
这里就是通过#把返回值分割赋值为args
event_handler = eval(
action + ('_handler' if is_action else '_function'))
说一下这个 如果is_action为空的话就返回_handler 为假的话就返回 _function
eval函数可以把字符串当成python代码来执行
返回resp
所有我们就想要直接执行FLAG()函数 但是发现是不行的 因为这里FLAG()函数没有参数 传入参数的话就报错
所有继续审计代码
看这个函数 他是有接受参数的
def get_flag_handler(args):
if session['num_items'] >= 5:
# show_flag_function has been disabled, no worries
trigger_event('func:show_flag;' + FLAG())
trigger_event('action:view;index')
如果这里num_items >=5的话 执行trigger_event函数就会被放入log中
所有看看如何得到num_items的
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'])
看这个函数
这里就是有漏洞的 他这里会直接加上参数里面的东西
所有就有思路了 我们用eval函数执行buy_handler函数
这里构造payload
action:trigger_event%23;action:buy;5%23action:get_flag;
[红明谷CTF 2021]write_shell
给出源码
<?php
error_reporting(0);
highlight_file(__FILE__);
function check($input){
if(preg_match("/'| |_|php|;|~|\\^|\\+|eval|{|}/i",$input)){
// if(preg_match("/'| |_|=|php/",$input)){
die('hacker!!!');
}else{
return $input;
}
}
function waf($input){
if(is_array($input)){
foreach($input as $key=>$output){
$input[$key] = waf($output);
}
}else{
$input = check($input);
}
}
$dir = 'sandbox/' . md5($_SERVER['REMOTE_ADDR']) . '/';
if(!file_exists($dir)){
mkdir($dir);
}
switch($_GET["action"] ?? "") {
case 'pwd':
echo $dir;
break;
case 'upload':
$data = $_GET["data"] ?? "";
waf($data);
file_put_contents("$dir" . "index.php", $data);
}
?>
考点 段标签
<?= ?>
其中=等价为echo
可以利用段标签绕过php
PHP 支持一个执行运算符:反引号(``)。注意这不是单引号!PHP 将尝试将反引号中的内容作为 shell 命令来执行,并将其输出信息返回(即,可以赋给一个变量而不是简单地丢弃到标准输出)。使用反引号运算符"`"的效果与函数shell_exec() 相同。
可以过滤了空格 可以用%09绕过
这里说一下我的理解 用%09的时候 url会对其进行解码传到浏览器就会变成0x09 十六进制绕过空格
标签:DDCTF2019,items,resp,request,event,session,action,HOMEBREW,EVENT From: https://www.cnblogs.com/kkkkl/p/16748373.html