# KMA CTF-23
---
###### tags: `CTF`
## 1. Vào đây!
### Phân tích
Link: http://103.163.25.143:20110/
Source:
```javascript=
const fs = require('fs')
const app = require('fastify')()
const crypto = require('crypto')
const md5 = d => crypto.createHash('md5').update(d).digest('hex')
const dbPromisePool = require('mysql2').createPool({
host: 'mysql',
user: 'root',
database: 'local_db',
password: 'local_password'
}).promise()
// app.setErrorHandler((error, req, resp) => {
// console.error(`[fastify]`, error)
// resp.status(503).send({ error: 'Vui lĂ²ng thá» lại sau.' })
// })
app.addHook('preHandler', async (req, resp) => {
resp.status(200).header('Content-Type', 'application/json')
})
app.post('/login', async req => {
if (req.body.user === 'admin') return;
const [rows] = await dbPromisePool.query(`select *, bio as flag from users where username = ? and password = ? limit 1`, [req.body.user, req.body.pass])
return rows[0]
})
app.post('/register', async req => {
const [rows] = await dbPromisePool.query(`insert users(username, password, bio) values(?, ?, ?)`, [req.body.user, md5(req.body.pass), req.body.bio])
if (rows.insertId) return String(rows.insertId)
return { error: 'Lá»—i, vui lĂ²ng thá» lại sau' }
})
app.get('/', async (req, resp) => {
resp.status(200).header('Content-Type', 'text/plain')
return fs.promises.readFile(__filename)
})
app.listen({ port: 3000, host: '0.0.0.0' }, () => console.log('Running', app.addresses()))
```
Đoạn code trên là một một ứng dụng server backend sử dụng framework Fastify và kết nối đến cơ sở dữ liệu MySQL.
- Định nghĩa một hook được thực thi trước khi xử lý yêu cầu. Nó đặt mã trạng thái HTTP thành 200 (OK) và đặt kiểu nội dung của phản hồi là `'application/json'`.
- Khi nhận được yêu cầu POST tới `/login`, đoạn code trên kiểm tra nếu req.body.user là `admin`, thì return. Ngược lại, nó thực hiện một truy vấn SQL sử dụng kết nối đến cơ sở dữ liệu `mysql2`. Truy vấn bảng `users` với điều kiện username và password tương ứng với giá trị của req.body.user và req.body.pass. Kết quả truy vấn được gán vào biến rows, và sau đó trả về phần tử đầu tiên của rows (rows[0]).
- Khi nhận được yêu cầu POST tới `/register`, truy vấn SQL chèn dữ liệu vào bảng `users`. Dữ liệu được chèn bao gồm username, password (được băm bằng hàm md5) và bio (lấy từ req.body.user, req.body.pass và req.body.bio).
Mình bắt request bằng Burp Suite, thực hiện POST đến `/register`:
- Thêm header `Content-Type: application/json`
- Thêm json `user, pass` để thực hiện đăng kí, đăng kí thành công sẽ trả về `id` của mình

Thực hiện login:

### Khai thác
Flag nằm trong `bio` của admin nhưng login với user=admin sẽ bị return ngay. Website dùng database mysql2, mặc dù đã sử dụng placeholder để tránh bị injection nhưng không filter đầu vào nên vẫn có thể bị injection.
Dạo trên google search một hồi mình tìm được trang blog [này](https://www.stackhawk.com/blog/node-js-sql-injection-guide-examples-and-prevention/).

Cơ bản là mình có thể truyền Object thay vì chuỗi vào biến `pass` để password luôn đúng. Test thử trên Burp và nó hoạt động:

Vì truy vấn chỉ `limit 1` lấy 1 hàng nên mình nghĩ là admin là user có `id=1`. Thử làm cho user luôn đúng cem:

**Flag: `KMACTF{SQLInjection_That_La_Don_Gian}`**
## 2. Time chaos
File có đuôi `.pcap`, search thử thì PCAP là Data Files - Packet Capture Data, một chương trình sử dụng để phân tích mạng, chứa dữ liệu gói mạng, được sử dụng để "packet sniffing" và phân tích các dữ liệu mạng.
Có thể mở file bằng Wireshark để đọc data.

Time chaos nên mình sort by Time và nhận thấy các ký tự cuối chính là flag.

Nhập tay một chút là ra flag nma :<
`KMACTF{C0d3_cun9_du0c_t0Ol_Cun9_DuOc_nHun9_h1_v0n9_b4n_kh0n9_l@m_m0t_c4cH_tHu_C0nG}`
## 3. Flag Holder
### Phân tích
Link: http://103.163.25.143:20105/

F12:

Thực hiện fetch đến `/source` để lấy source:
```python=
from flask import Flask, request, render_template_string, render_template, make_response
import os
app = Flask(__name__)
FLAG = os.getenv("FLAG")
MAX_LENGTH = 20
def waf(string):
blacklist = ["{{", "_", "'", "\"", "[", "]", "|", "eval", "os", "system", "env", "import", "builtins", "class", "flag", "mro", "base", "config", "query", "request", "attr", "set", "glob", "py"]
for word in blacklist:
if word in string.lower()[:MAX_LENGTH]:
return False
return True
@app.route('/')
def hello():
return render_template("index.html")
@app.route("/render", methods = ["GET"])
def render():
template = request.args.get("template")
variable = request.args.get("variable")
if len(template) == 0 or len(variable) == 0:
return "Missing parameter required"
if len(template) > MAX_LENGTH or len(variable) > MAX_LENGTH:
return "Input too long"
if not waf(template) or not waf(variable):
return "Try harder broooo =)))"
data = template.replace("{FLAG}", FLAG).replace("{variable}", variable)
return render_template_string(data)
@app.route("/source", methods = ["GET", "POST"])
def source():
response = make_response(open("./app.py", "r").read(), 200)
response.mimetype = "text/plain"
return response
if __name__ == "__main__":
app.run(host="0.0.0.0", port=5000)
```
Đoạn code sử dụng Flask để xây dựng website:
- `render_template_string` và `render_template` để hiển thị các template nên website có thể dính lỗi SSTI. Tuy nhiên hàm `waf()` giới hạn một loạt các kí tự đầu vào để chống SSTI.
- `/render` nhận hai tham số `template` và `variable` thông qua query string của yêu cầu GET. Tiến hành filter kiểm tra các điều kiện như độ dài và `blacklist` trước khi render template.
- `data` được `render_template_string` sau khi thay thế hai chuỗi `"{FLAG}"` bằng biến `FLAG` và `"{variable}"` bằng biến `variable` trong biến **`template`**
### Khai thác
Ban đầu mình thử tìm cách bypass qua blacklist, khai thác SSTI để thực thi câu lệnh `os.getenv("FLAG")`. Tuy nhiên blacklist khá nhiều cộng với việc giới hạn kí tự đầu vào nên cách này khá no hope.
Để ý một chút thì trong hàm `waf()` có đoạn code:
```python
if word in string.lower()[:MAX_LENGTH]
```
Đoạn code này kiểm tra `MAX_LENGTH` kí tự đầu vào sau khi đã `lower()` xem có chuỗi con nào nằm trong blacklist hay không. Hàm `lower()` khá đáng nghi nên mình đã nảy ra ý tưởng, liệu có kí tự nào mà độ dài của nó so với nó sau khi `lower()` hay không.
Code:
```python=
for i in range(0, 1000):
if len(chr(i)) != len(chr(i).lower()):
print(chr(i))
```

Kí tự đó đây nha `İ`.
Kiểm tra thử thì thấy:

Như vậy là sau khi `lower()` thì kí tự `İ` có độ dài là 2.
Nếu mình truyền `İİİİİİİİİİİİİİ{FLAG}` thì khi chạy vào `string.lower()[:MAX_LENGTH]`, nó sẽ chỉ check 10 kí tự `İ` đầu tiên và bỏ qua phần còn lại => Bypass `waf()` thành công.

## 4. Discord
