---
# System prepended metadata

title: KMACTF 2023 WRITEUP
tags: [CTF, Writeup]

---

# KMACTF 2023 WRITEUP
###### tags: `CTF` `Writeup`
---
## 1. Vào Đây! (WEB)
- Link Challenge: http://103.163.25.143:20110/
### Đề Bài
Trang web còn cho ta source code:
```nodejs!=
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()))
```
### Phân Tích
- Thứ làm em chú ý đến đầu tiền là dòng code:
```nodejs!=
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' }
})
```
- Đầu tiên em đã nghĩ rằng chỉ cần truyền vào body request là được, nhưng sau khi thử không được và tìm hiểu về Fastify thì em nhận ra em cần truyền vào theo dạng json, với các giá trị key khi đăng kí sẽ là "user","pass","bio". Sau khi đăng kí thành công, password của em khi được insert vào database sẽ được MD5 Hash. Từ đoạn code này ta thấy chức năng của đăng kí cơ bản chỉ báo hiệu đăng kí thành công thì sẽ đưa ra số thứ tự đăng kí, hay "insertID"
- Tiếp theo là đoạn code về chức năng đăng nhập:
```nodejs!=
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]
})
```
- Chức năng đăng nhập này đầu tiên sẽ check tên username có phải 'admin' không, nếu có thì sẽ không trả về cái gì cả, còn nếu không thì nó sẽ tìm hàng chứa username và password(đã MD5 Hash) đó, thay cột "bio" thành "flag" và lấy 1 hàng, rồi show ra cho ta hàng đầu tiên mà nó tìm được
- Tất cả được trả về dưới dạng content-type: json 
- Khi bắt tay vào xử lý bài này, em đã đăng kí và đăng nhập vào 1 tài khoản, và kết quả cho ra:
![](https://hackmd.io/_uploads/S1-Ru30w2.png)
- Thì em nhận thấy cột flag được thêm vào, từ đó em đã nghĩ về việc: Làm thế nào để xem được bio của admin, hay làm thế nào để đăng nhập thành công với username là admin
### Phương Hướng Giải Quyết
- Vì dòng code kia là 3 dấu bằng, nên việc để tên tài khoản là admin là điều không thể, tuy nhiên mysql lại không phân biệt chữ hoa hay chữ thường, nên em đã nghĩ đến việc nhập vào "Admin" thay vì "admin" để bypass được dòng đó
- Tuy nhiên còn mật khẩu thì sao? Mật khẩu trong database đã là md5 hash, với 32 ký tự thì việc bruteforce là không thể, nên em đã nghĩ đến SQL Injection, em đã thử tìm trên mạng các cách để bypass login nodejs, và em đã tìm thấy cái này![](https://hackmd.io/_uploads/S1tginRP2.png)
- Mọi thứ đã đầy đủ, em chỉ việc tiến hành đăng nhập với username là "Admin" và mật khẩu là "{"password": 1}", em đã solve được bài này:
![](https://hackmd.io/_uploads/SkdLsn0v3.png)
- Flag của challenge: `KMACTF{SQLInjection_That_La_Don_Gian}`

## 2. Time Chaos (FOR)
- Link file: https://ctf.actvn.edu.vn/files/303502292cf04f9b05c68b134cfc3da9/Time_chaos.pcap?token=eyJ1c2VyX2lkIjozMiwidGVhbV9pZCI6bnVsbCwiZmlsZV9pZCI6M30.ZJMmPA.nmePZKHnmiaWpY7DmARlMP4yk1Y
### Đề Bài
- Với challenge này, họ cho em 1 file là **Time_chaos.pcap**, em không biết nó là gì nên em đã đưa nó vào Kali Linux để xem:
![](https://hackmd.io/_uploads/Syyz33RDn.png)
### Phương Hướng Giải Quyết
- Kết quả ra được một file chứa thời gian và thông tin của cuộc trò chuyện được gửi đi, tuy nhiên thời gian đang rất lộn xộn, và đồng thời em cũng để ký tự cuối của một số chỗ có chữ K, C, T, F và dấu {}, nên em đã nghĩ đến việc sắp xếp chúng theo thứ tự thời gian để ghép thành flag
![](https://hackmd.io/_uploads/Bkf4xa0P3.png)
- Sau một thời gian cực nhọc làm tay, em đã có được flag:`KMACTF{C0d3_cun9_du0c_t0Ol_Cun9_DuOc_nHun9_h1_v0n9_b4n_kh0n9_l@m_m0t_c4cH_tHu_C0nG}`
*Em xin lỗi vì đã làm tay ạ* 😢

## 3. Flag Holder (WEB)
- Link Challenge: http://103.163.25.143:20105/
### Đề Bài
- Challenge khởi đầu bằng việc đưa cho ta trang web trắng như này và 2 input là template và variable
![](https://hackmd.io/_uploads/Hkg3TLJdn.png)
- Đầu tiên là em ctrl U trang web và thấy dòng này xuất hiện: ![](https://hackmd.io/_uploads/SkqLCI1O3.png)
- Quá hay, em ngay lập tức tải source về để tham khảo:
```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)
```
### Phân Tích
- Đầu tiên, em để ý việc challenge blacklist `{{` cùng với hàng loạt từ như `os, eval, class, builtins` và sự hiện diện của 1 input là template đã làm em nghĩ rằng đây là 1 bài SSTI, và em đã thực sự mất rất nhiều thời gian nhưng đã thất bại trong việc thực hiện SSTI đối với challenge này
- Em đã bị lừa, Bài này thực chất không phải là SSTI, hãy đọc source kĩ hơn nào:
- Đầu tiên, ta cần phải đọc kĩ hơn về hàm **waf** này, nó blacklist các từ có thể được sử dụng để SSTI, và rồi chức năng check của nó là xem bản viết thường của data trong khoảng MAX_LENGTH (20 ký tự -> lí do chính khiến việc SSTI là điều không thể đối với em) có từ nào trong danh sách blacklist không, nếu có sẽ trả về False, còn không thì sẽ trả về True
- Tiếp theo là 2 input được nhập vào, template và variable, khi ta ấn vào submit thì sẽ kích hoạt đến trang /render và trang /render sẽ check xem có input nào để trống không, sau đó sẽ check xem độ dài của template và variable có vượt quá 20 ký tự không, cuối cùng là filter 2 input này bằng hàm waf đã được định nghĩa trước đó để xem có dính từ blacklist nào không, nếu không, dữ liệu sẽ được thay phần {FLAG} thành biến FLAG(chính là FLAG cần tìm) và thay phần {variable} bằng giá trị của input variable vừa nhập vào, ví dụ như: 
![](https://hackmd.io/_uploads/HkuAzDkdh.png)
    + Nhập vào variable là "sunny" thì phần {variable} sẽ bị thay thế bằng "sunny"
![](https://hackmd.io/_uploads/ry5fXPk_n.png)
- Vậy chúng ta phải đi theo hướng nào? Ta không thể nào cứ cho {FLAG} vào phần template được, làm như vậy thì sẽ bị blacklist ngay. Có một vấn đề ta chưa nghĩ đến, là tại sao hàm waf lại chỉ check kí tự viết thường của 20 ký tự của đầu vào???? Liệu ta có thể nhét vào template một ký tự nào mà lower có độ dài dài hơn so với không lower không???? Nếu làm được vậy thì ta hoàn toàn có thể giải quyết được challenge này.
### Phương Hướng Giải Quyết
- Bắt đầu với việc tìm ký tự mà độ dài viết thường dài hơn độ dài viết hoa, em đã viết script bằng ngôn ngữ python:
```python!=
for i in range(0, 10000):
    if len(chr(i)) != len(chr(i).lower()):
            print(chr(i))
```
- Em sẽ tìm kiếm trong khoảng 0 đến 10000, xem kí tự của nó khi lower có độ dài khác so với độ dài bình thường thì in ra:![](https://hackmd.io/_uploads/ry4qEwy_h.png)
- Kết quả đã có, em có ký tự `İ` mà khi lower độ dài của nó là 2, so với độ dài bình thường của nó là 1
![](https://hackmd.io/_uploads/BkduSPJu3.png)
- Vậy là giờ em chỉ cần nhét 10 ký tự này vào phần template kèm với {FLAG} là có thể solve được challenge này, vì bản chất hàm waf sẽ chỉ check 20 kí tự được lower nên sau 20 ký tự sẽ không thể check được phần {FLAG} của em:
![](https://hackmd.io/_uploads/rk7frwk_h.png)
- Và như vậy, em đã có được flag:
![](https://hackmd.io/_uploads/ryv7rD1_2.png)
- Flag của challenge: `KMACTF{WAF_1s_s0_5tR0nG_BuT_pYth0n_1s_s0_H4rd}`

## 4. Ninja Shop (WEB)
- Link challenge: http://103.163.25.143:20108/
### Đề Bài
- Link file zip source code: https://ctf.actvn.edu.vn/files/b425364bfdf713b35fed2b6b6218a35d/public.zip?token=eyJ1c2VyX2lkIjozMiwidGVhbV9pZCI6bnVsbCwiZmlsZV9pZCI6NH0.ZJK5qQ.RwEPk6iE8-dZHF0E71kWfb1vB30
- Challenge này thì có file source code với đầy đủ các trang chức năng, file docker và file sql cho database
![](https://hackmd.io/_uploads/BJtQdC1u3.png)
### Phân Tích
- Đầu tiên em cần phải hiểu được trang web hoạt động như thế nào, cũng như challenge trước, ta thấy có sự xuất hiện của hàm **waf** trong file config.php
```php!=
function waf($input) {
    // Prevent sqli -.-
    $blacklist = join("|", ["sleep", "benchmark", "order", "limit", "exp", "extract", "xml", "floor", "rand"
                        , "count", "or" ,"and", ">", "<", "\|", "&","\(", "\)", "\\\\" ,"1337", "0x539"]);
    if (preg_match("/${blacklist}/si", $input)) die("<strong>Stop! No cheat =))) </strong>");
    return TRUE;
}
```
- Hàm này sẽ filter những ký hiệu dùng để SQL Injection, đồng thời kiểm tra theo regex rằng bất kể mình có xuống dòng cho input hay viết hoa thì vẫn sẽ bị filter, làm việc SQL Injection trở nên khó khăn
- Sau đó ta đến với trang register.php:
```php!=
foreach (array("username", "password", "fullname") as $value) {
    if (empty($_POST[$value])) die("<aside>Missing parameter!</aside>");
    waf($_POST[$value]);
}
if (strlen($_POST["username"]) > 26) die("<aside>Username too long<aside>");
if (strlen($_POST["fullname"]) > 10) die("<aside>Fullname too long<aside>");

// check account is exists
if ( $connection->query(sprintf('SELECT * FROM users WHERE username="%s" and password="%s"'
, $_POST["username"], md5($_POST["password"])))->fetch_assoc()["uid"] )
    die("<strong>User is exists</strong>");

$result = $connection->query(sprintf('INSERT INTO users(username, password, fullname) 
VALUES ("%s", "%s", "%s")', $_POST["username"], md5($_POST["password"]), $_POST["fullname"]));

if ($result) {
    $uid = $connection->query(sprintf('SELECT uid FROM users WHERE username="%s" and password="%s" 
    limit 0,1', $_POST["username"], md5($_POST["password"])))->fetch_assoc()["uid"];
    if ($uid) {
        if ($connection->query(sprintf('INSERT INTO coins(coin, uid) VALUES (100, %d)', (int)$uid)))
            die("<strong>User created successfully</strong>");
    }
} else die("<strong>Something went wrong! Try again!</strong>");
```
- Ta cần đăng kí với đầy đủ 3 biến truyền vào: username, password và fullname, các input sẽ được filter trước rồi kiểm tra đội dài, với username độ dài chỉ được lớn nhất là 25 còn với fullname là 9, sau đó tài khoản sẽ được tạo với mật khẩu đợc md5 hash trong database, đồng thời tại bảng coins số coin ứng với user đó thông qua uid sẽ được set là 100
- Hàm login cũng có chức năng check như hàm register:
```php!=
foreach (array("username", "password") as $value) {
    if (empty($_POST[$value])) die("<aside>Missing parameter!</aside>");
    waf($_POST[$value]);
}

if (strlen($_POST["username"]) > 26) die("<aside>Username too long<aside>");
$result = $connection->query(sprintf('SELECT * FROM users WHERE username="%s" AND password="%s" 
                                    limit 0,1', $_POST["username"], md5($_POST["password"])))->fetch_assoc();
```
- Trang login cũng sẽ filter đầu vào bằng hàm waf, sau đó check độ dài của username, nếu độ dài hợp lệ(nhỏ hơn 26 ký tự) tiếp tục tìm trong database, nếu đăng nhập thành công sẽ chuyển hướng ta đến với trang index.html, không thì sẽ thông báo rằng Tài khoản hoặc mật khẩu sai.
- Đây sẽ là giao diện chính khi ta vào được index.php
![](https://hackmd.io/_uploads/r1-t2CJun.png)
```php!=
if ( isset($_GET["buy"]) ) {
    if (!in_array($_GET["buy"], $ninjas)) die("<aside>Product not available yet</aside>");
    $coin = $connection->query(sprintf("SELECT coin FROM coins WHERE uid=%d limit 0,1", (int)$_SESSION["uid"]))->fetch_assoc()["coin"];
    if ($_GET["buy"] === "flag") {          
        if ( (int)$coin === 1337 /*(int)$coin > 1337 */  ) {        // must be 1337 :v
            $connection->query(sprintf("UPDATE coins SET coin=%d-1337 WHERE uid=%d", (int)$coin, (int)$_SESSION["uid"]));
            die("<strong>Nice brooo!! Are you a millionaire??? Here your flag: $FLAG</strong>");
        }
        else die("<strong>Try harder!!! =))) </strong>");
    }
    else {
        if ((int)$coin > 1) {
            $result = $connection->query(sprintf("UPDATE coins SET coin=%d-1 WHERE uid=%d", (int)$coin, (int)$_SESSION["uid"]));
            if ($result) die(sprintf("<strong>Buy successfully!!! Your coin: %d", (int)$coin - 1));
        }
        else die("<strong>Coin do not enough!! =(( </strong>");
    }
}
```
- Trong giao diện ta sẽ có 3 ninja, mỗi lần mua ta sẽ mất 1 coin, còn đặc biệt ở chỗ nếu ta buy flag mà số coin của ta không phải là 1337 thì sẽ hiện ra dòng chữ "Try harder!!! =)))", còn nếu không thì số coin của ta sẽ trừ đi 1337 và flag sẽ hiện ra
- Cuối cùng là 1 trang quan trọng, đó là trang profile.php:
```php!=
$fullname = $connection->query(sprintf("SELECT fullname FROM users WHERE username='%s' limit 0,1", $_SESSION["username"]));
if (gettype($fullname) !== "boolean") echo "<h2>Hello: ". $fullname->fetch_assoc()["fullname"] ."</h2>";
else echo "<h2>Hello: Anonymous </h2>";
$coin = $connection->query(sprintf("SELECT * FROM coins WHERE uid=%d limit 0,1", (int)$_SESSION["uid"]))->fetch_assoc()["coin"];
echo <<<EOF
    <h2>Your coin: $coin</h2>
EOF;

// Ran out of money?? No need to worry, you can reset carefree!! But only limited from 1-99 coins =)))
if ( isset($_GET["new_balance"]) and waf($_GET["new_balance"]) ) {
    if (strlen($_GET["new_balance"]) > 2) die("<strong>Only allow from 1 to 99</strong>");
    else {
        $result = $connection->query(sprintf("UPDATE coins SET coin=%s WHERE uid=%d", $_GET["new_balance"], (int) $_SESSION['uid']));
        if ($result) die("<strong>Your coin has been updated</strong>");
        else die("<strong>0ops!!! Coin update has failed</strong>");
    }
} 
```
- Đây sẽ là trang web hiển thị số coin mà ta đang có, đồng thời cho phép ta update số coin thông qua 1 biến trên URL là "new_balance", tuy nhiên, biến này được nhập vào dưới dạng chuỗi và độ dài của nó không quá 2 ký tự, cũng có nghĩa rằng bạn chỉ nhập được 99 chứ không thể nào nhập 1337 để bypass được ở index.php
- Sau một hồi đọc source thì có một vài câu hỏi em chưa trả lời được. Tại sao mình có thể nhập vào coin dưới dạng string mà coin trả ra để trừ là int, nếu nhập vào string thì làm thế nào để em có thể nhập vào được số 1337 trong khi new_balance không được quá 2 ký tự??
- Vấn đề chính là khi mình tạo 2 bảng trong file SQL, có một thứ khiến em chú ý
```sql!=
USE ninjashop;
CREATE TABLE users(uid int AUTO_INCREMENT PRIMARY KEY, username VARCHAR(32), password VARCHAR(32), fullname VARCHAR(255));
CREATE TABLE coins(id int AUTO_INCREMENT PRIMARY KEY, coin INT, uid INT);
```
- Bảng coins được tạo với khóa chính là id sẽ tự động tăng, số coin là dạng số nguyên, và số uid là để liên kết với cả user tương ứng.
- Khoan, ta có id tự động tăng là khóa chính, vậy nếu ta gán giá trị của biến new_balance bằng id, thì có nghĩa số coin sẽ bằng giá trị của id!! Phương hướng giải quyết đã có, ta tiến hành làm thôi
### Phương Hướng Giải Quyết 
- Đầu tiên em cũng sẽ đăng ký và đăng nhập 1 tài khoản mới:
![](https://hackmd.io/_uploads/HJU2lygd3.png)
![](https://hackmd.io/_uploads/HJgyTxkedh.png)
- Em sẽ chuyển sang trang profile.php, và đồng thời chỉnh trên URL là **?new_balance=id**
![](https://hackmd.io/_uploads/H1aJbylu2.png)
- Số coin của em đã lên 9516, nghĩa là em là tài khoản thứ 9516 được lập, giờ em chỉ cần sử dụng chức năng mua các ninja để trừ số tiền của mình xuống 1337 coin là có thể buy flag rồi
- Vì việc bấm tay sẽ rất mệt mỏi nên em sẽ sử dụng chức năng Intruder của BurpSuite:
- Em đưa tab mua Naruto vào Intruder, và không chọn payload nào cả
![](https://hackmd.io/_uploads/HJAqGyed3.png)
- Set payload type là null rồi để gửi 8178 payload để số tiền đưa về là 1337
![](https://hackmd.io/_uploads/SywszyxOn.png)
- Hoàn thành việc Intruder, F5 lại trang web và số tiền của em đã về 1337
![](https://hackmd.io/_uploads/HJFIXkg_3.png)
- Giờ chỉ cần quay về index.php và buy flag thôi
![](https://hackmd.io/_uploads/Hk-SNylu3.png)
- Flag của challenge: `KMACTF{Bruhhhhhh!!Lmaoooooo!!m4ke_SQL1_gr34t_4g4in!!!!!}`