字节跳动CTF线下赛Web题解&复盘

字节跳动CTF线下赛Web题解&复盘

PythonWeb

做题小谈

之前线下赛没运维过pythonweb,踩了很多坑,小记一下。

flask在更改代码后要重启才能生效,但是如果app.DEBUG=True则不需要重启flask,这个配置多见于config.py,比赛的时候也可以全局搜索。

目录结构如下
image_1dnm2lc67dmv18gg4fn1d95fq5m.png-112.2kB

在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!"

image_1dnm8r3b65901pom16tg12t1c031g.png-34.8kB

模版注入

这个点我没挖到,但是后来听别的师傅说存在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

image_1dnp66nft9tm243119nifpt49.png-91.2kB

流量记录

这次还是吃了没流量的亏,测试了一下别人的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下生成日志
image_1dnn61c77p61mvu1v011ini1qtt7e.png-533.4kB

Opensns

基于TP3.2.2开发,比赛复现出两个漏洞。其中一个是内置后门就不说了,还有一个是渲染模版时的任意文件读取漏洞

漏洞复现

任意文件读取

keywords[_filename]=/flag

漏洞点在ThinkPHP/Library/Think/Storage/Driver/File.class.php的File类驱动中,同时File类继承自Storage类:class File extends Storage

image_1dnmqjgof1rfhksv1ae71engq2320.png-396.7kB

在判断了$vars是否存在后,进行了一次变量覆盖,再调用load方法进行了文件包含。追踪一下哪里调用了load方法,发现都是在解析模版的时候调用的,这里选择文件ThinkPHP/ThinkPHP/Library/Think/Template.class.php的fetch()方法

image_1dnmrrf2k138168g9gm12sfoip2t.png-245.5kB

发现可控参数$templateVar,会被当作实参传入load(),继续寻找调用fetch()方法的位置。因为TP代码中有很多实例化的方法,并不都像storage::这样的调用方式,只能全局搜索fetch看哪里调用。最终发现在ThinkPHP/Library/Behavior/ParseTemplateBehavior.class.php中实例化了Template类并且调用fetch()方法。

image_1dnms90lf1fbckc91cnr18281g1c3a.png-505.2kB

刚才说到可控参数$templateVar的原型在这里为$_data,那么$_data是否可控?继续追调用链,在这里追的时候就卡壳了,因为涉及到tp的一个知识:Thinkphp下利用钩子使用行为扩展

简单来说就在xx文件中,先宏定义了监听器对应的类名再加载进程序(这里的类指的是类似于ParseTemplateBehavior.class.php这种的行为拓展类)
image_1dnmt4sfa1c6ja0pgb4v00mft4q.png-50.2kB

而后,程序中的Hook机制通过触发不同类型的监听器,实现对应行为拓展类的实例化且会调用对应行为拓展类的run方法。一个简单的触发机制就是Hook::Listen(tags,prarm)

那么在这里我们希望它触发view_parse,从而实例化ParseTemplateBehavior。就全局搜索Hook::listen('view_parse'。在TP的视图类里找到了该监听器的hook
image_1dnmtdrpd6t6101m1omechn1n0857.png-507.6kB

接下来就好说了,只要$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()。
image_1dnn1fspe10st1i4rbe6nt1p6g5k.png-208.2kB

总结来说,该点漏洞就是加载模版的时候,把本地文件作为模版变量赋值,再渲染到页面。这个在平常的代码审计中也是一个不错的思路,膜出题人。

douchat

同样使用TP3.2.2开发的

漏洞浮现

代码注入

跟opensns类似的漏洞,也是模版的渲染,只不过这次存在content参数,即生成的缓存有效时,加载缓存造成代码注入。由于开启了ob_start(),因此在include的时候代码注入
image_1dnn52qdims3v3217dm1s6ph8f6h.png-535.2kB

文件上传

漏洞文件:/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/下并回显出来

not found!