字节跳动CTF线下赛Web题解&复盘
字节跳动CTF线下赛Web题解&复盘
PythonWeb
做题小谈
之前线下赛没运维过pythonweb,踩了很多坑,小记一下。
flask在更改代码后要重启才能生效,但是如果app.DEBUG=True
则不需要重启flask,这个配置多见于config.py
,比赛的时候也可以全局搜索。
目录结构如下
在pyweb的awd中,一定要先ps -ef
看一下系统的进程,看看原始的服务是怎么启动的,我们就可以照着它的命令去重启flask,而不至于把服务启崩。这次我就是没有看进程,自己相当然的用命令flask run
,结果被down了好几轮。
最后看了下手册,这里pyweb使用gunicorn来启动的:
gunicorn -b 0.0.0.0:5000 manage:app
漏洞审计
任意文件读取
@main.route('/file')
def file():
file = request.args.get('file',base64.b64encode('/tmp/Blog_mini/app/static/images/background.jpg'))
f = open(base64.b64decode(file),'rb')
res = f.read()
return jsonify({"res":res})
request.args.get()获取file参数,如果参数不存在则为base64.b64encode('/tmp/Blog_mini/app/static/images/background.jpg)
命令执行
后台有一点路由/backup
,可以调用popen(),那就可以先注册用户,再去rce。同时这个点应该也可以读取任意文件
@main.route('/backup',methods = ['GET'])
@login_required
def backup():
if request.args.get('name'):
shell = 'tar -zcf ./'+ request.args.get('name') +'.tar.gz ./'
res = os.popen(shell).read()
ress = {"res":res}
return jsonify(ress)
else:
return "param is name! please backup!"
模版注入
这个点我没挖到,但是后来听别的师傅说存在404的ssti
@main.app_errorhandler(404)
def page_not_found(e):
for x in request.path:
if x in '._%':
return render_template('404.html'), 404
template = '''
{%% block body %%}
<div class="center-content error">
<h1>Oops! That page doesn't exist in this Blog.</h1>
<h3>%s</h3>
</div>
{%% endblock %%}
''' % (request.url)
return render_template_string(template), 404
过滤了._%
,下面的ssti是经典的漏洞案例。利用|
过滤器和动态传参就可以bypass了,跟今年护网杯的题目思路差不多,这里盗用一下@Smile师傅的payload
流量记录
这次还是吃了没流量的亏,测试了一下别人的py流量脚本,这里贴出来
@main.before_request
def awdlog():
import time
f = open('/tmp/pylog.txt','a+')
f.writelines(time.strftime('%Y-%m-%d %H:%M:%S\n', time.localtime(time.time())))
f.writelines("{method} {url} \n".format(method=request.method,url=request.url))
s = ''
for d,v in dict(request.headers).items():
s += "%s: %s\n"%(d,v)
f.writelines(s+'\n')
s = ''
for d,v in dict(request.form).items():
s += "%s=%s&"%(d,v)
f.writelines(s.strip("&"))
f.writelines('\n\n')
f.close()
main指的是应用名,每次根据实际情况更改,最终在/tmp下生成日志
Opensns
基于TP3.2.2开发,比赛复现出两个漏洞。其中一个是内置后门就不说了,还有一个是渲染模版时的任意文件读取漏洞
漏洞复现
任意文件读取
keywords[_filename]=/flag
漏洞点在ThinkPHP/Library/Think/Storage/Driver/File.class.php
的File类驱动中,同时File类继承自Storage类:class File extends Storage
在判断了$vars是否存在后,进行了一次变量覆盖,再调用load方法进行了文件包含。追踪一下哪里调用了load方法,发现都是在解析模版的时候调用的,这里选择文件ThinkPHP/ThinkPHP/Library/Think/Template.class.php
的fetch()方法
发现可控参数$templateVar
,会被当作实参传入load(),继续寻找调用fetch()方法的位置。因为TP代码中有很多实例化的方法,并不都像storage::
这样的调用方式,只能全局搜索fetch看哪里调用。最终发现在ThinkPHP/Library/Behavior/ParseTemplateBehavior.class.php
中实例化了Template类并且调用fetch()方法。
刚才说到可控参数$templateVar
的原型在这里为$_data
,那么$_data
是否可控?继续追调用链,在这里追的时候就卡壳了,因为涉及到tp的一个知识:Thinkphp下利用钩子使用行为扩展
简单来说就在xx文件中,先宏定义了监听器对应的类名再加载进程序(这里的类指的是类似于ParseTemplateBehavior.class.php这种的行为拓展类)
而后,程序中的Hook机制通过触发不同类型的监听器,实现对应行为拓展类的实例化且会调用对应行为拓展类的run方法。一个简单的触发机制就是Hook::Listen(tags,prarm)
。
那么在这里我们希望它触发view_parse
,从而实例化ParseTemplateBehavior
。就全局搜索Hook::listen('view_parse'
。在TP的视图类里找到了该监听器的hook
接下来就好说了,只要$parama
可控,并且找到一处能够调用fetch函数的地方,整条利用链就完整了。
其中$param的取值经过一系列的操作
public function assign($name, $value = '')
{
if (is_array($name)) {
$this->tVar = array_merge($this->tVar, $name);
} else {
$this->tVar[$name] = $value;
}
}
$params = array('var' => $this->tVar, 'file' => $templateFile, 'content' => $content, 'prefix' => $prefix);
file、content、prefix为定值,只有$this->tvar
可操作且被assign函数赋值。这里存在以前tp3的模版rce漏洞的挖掘链,通过assign这个模版赋值函数,赋值变量$this->tvar
。
同时这里还涉及Tp的一个小操作:在tp3.2中,对模版的加载&渲染依靠ThinkPHP/ThinkPHP/Library/Think/View.class.php
。先通过View类方法assgin()
对模版赋值,再调用display()
加载模板和页面输出。在display函数的内部同时实现了fetch()
函数解析并获取模板内容,也解决了上面调用fetch函数的困扰。
所以只需要找一个Controller,接受post/get传入参数,并且能够传入assign()
去模版赋值,之后再经过tp的display()
函数渲染模版,这里出题人在控制器里造了一个方法search,接受keywords参数
public function search()
{
$keywords=I('post.keywords','','text');
$modules = D('Common/Module')->getAll();
foreach ($modules as $m) {
if ($m['is_setup'] == 1 && $m['entry'] != '') {
if (file_exists(APP_PATH . $m['name'] . '/Widget/SearchWidget.class.php')) {
$mod[] = $m['name'];
}
}
}
$show_search = get_kanban_config('SEARCH', 'enable', $mod, 'Home');
$this->assign($keywords);
$this->assign('showBlocks', $show_search);
$this->display();
}
构造如下poc本地包含文件,由于开启了ob_start()。
总结来说,该点漏洞就是加载模版的时候,把本地文件作为模版变量赋值,再渲染到页面。这个在平常的代码审计中也是一个不错的思路,膜出题人。
douchat
同样使用TP3.2.2开发的
漏洞浮现
代码注入
跟opensns类似的漏洞,也是模版的渲染,只不过这次存在content参数,即生成的缓存有效时,加载缓存造成代码注入。由于开启了ob_start(),因此在include的时候代码注入
文件上传
漏洞文件:/Public/Plugins/webuploader/server/preview.php
$src = file_get_contents('php://input');
if (preg_match("#^data:image/(\w+);base64,(.*)$#", $src, $matches)) {
$previewUrl = sprintf(
"%s://%s%s",
isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] != 'off' ? 'https' : 'http',
$_SERVER['HTTP_HOST'],
$_SERVER['REQUEST_URI']
);
$previewUrl = str_replace("preview.php", "", $previewUrl);
$base64 = $matches[2];
$type = $matches[1];
if ($type === 'jpeg') {
$type = 'jpg';
}
$filename = md5($base64).".$type";
$filePath = $DIR.DIRECTORY_SEPARATOR.$filename;
if (file_exists($filePath)) {
die('{"jsonrpc" : "2.0", "result" : "'.$previewUrl.'preview/'.$filename.'", "id" : "id"}');
} else {
$data = base64_decode($base64);
file_put_contents($filePath, $data);
die('{"jsonrpc" : "2.0", "result" : "'.$previewUrl.'preview/'.$filename.'", "id" : "id"}');
}
} else {
die('{"jsonrpc" : "2.0", "error" : {"code": 100, "message": "un recoginized source"}}');
}
$src
可控,通过伪协议控制生成的文件名,然后写入到preview/下并回显出来