Table of Contents
Hello ctfian's!,
I played the CSAW CTF Qualification Round 2022 which is a world wide ctf event held on 9th September 2022. CSAW CTF is one of the oldest and biggest CTFs with 1216 teams with 1+ points in 2021. Designed as an entry-level, jeopardy-style CTF, this competition is for students who are trying to break into the field of security, as well as for advanced students and industry professionals who want to practice their skills.
I solved 5 challenges in crypto, rev, web and forensics categories. These challenges boosted my confidence as it is my second ctf event and i spent complete two days to solve these challenges.
Okay lets see how i solved those challenges.
Gotta Crack Them All
The content of encrypt.py
with open('key.txt','rb') as f:
key = f.read()
def encrypt(plain):
return b''.join((ord(x) ^ y).to_bytes(1,'big') for (x,y) in zip(plain,key))
plain = "Cacturne-Grass-Dark"
print(encrypt(plain))
The leaked password is Cacturne-Grass-Dark
and the encrypted passwords are available in encrypted_password.txt
.
If we observe the encrypt.py the passwords are encrypted using the XOR encryption.
First i tried to encrypt the leaked password with the given encryption algorithm.
Now i have a leaked password and its cipher kz\xc6\xb9\xd9Du\xcb\x8a\x9e\xe0\x9d\xbeo\xee\x03\xcf\xddd
. Now the idea was simple if we XORed the leaked password with the cipher we can get the encryption key then that key can be used to decrypt all the encrypted passwords.
This code can do what we are talking about.
from pwn import xor
f = open("encrypted_passwords.txt","rb")
l = "Cacturne-Grass-Dark"
c = b"kz\xc6\xb9\xd9Du\xcb\x8a\x9e\xe0\x9d\xbeo\xee\x03\xcf\xddd"
key = xor(c,l)
lines = f.readlines()
for i in lines:
i = i.strip()
tmp =''
for y,l in zip(key,i):
tmp += chr(y ^ l)
print(tmp)
The output is
Chespin-Grass
Mr. Mime-Psychic-Fa
Tornadus-Flying
Pupitar-Rock-Ground
Combusken-Fire-Figh
Guzzlord-Dark-Drago
Carnivine-Grass
Growlithe-Fire
Grubbin-Bug
Gastrodon-Water-Gro
Goomy-Dragon
Thievul-Dark
1n53cu2357234mc1ph3
Seadra-Water
As we can see the last but one row has the flag but its not a complete flag. To find the full flag we have to find the key which is longer than the current one for that we can take the any decrypted password which is longer than the leaked password, then we can repeat the above process to get the complete flag.
I am taking Gastrodon-Water-Ground-Steel
for now,
from pwn import xor
f = open("encrypted_passwords.txt","rb")
l = "Gastrodon-Water-Ground-Steel"
c = b'oz\xd6\xb9\xdeY\x7f\xc1\xc9\xf4\xc5\x9d\xb9y\xb1j\xe9\xdd`\xe0\xe2\xdf'
key = xor(c,l)
lines = f.readlines()
for i in lines:
i = i.strip()
tmp =''
for y,l in zip(key,i):
tmp += chr(y ^ l)
print(tmp)
Wooooooh! we got it...
The flag is 1n53cu2357234mc1ph32
Phi in Too Much Common
Here it is the Chall which takes 5 hours of my life. But at the end it boosted my confidence levels upto the sky.
First i connected to the challenge with the netcat.
$ nc crypto.chal.csaw.io 5000
********** TOO MUCH IN COMMON **********
Have at it!
/------------------------------\
| COMMANDS |
| |
| 1) ciphertext_info |
| 2) solve_challenge <password> |
| 3) exit |
\------------------------------/
> 1
N = 66230379529365020257079274492974065493126330971013187062230550667484536336498921190934189244362957312249586769969174687366278892047021464836842536529323843984917711253546999262427337059964746457287003004612989077687496386808318868155443955433495669933416861831960406309846051654977485404679018836950591207073
e = 3203033219058218846809870595889303749356759660802874921757919347285050689023
c = 16602634789029238804962653840385596228672793718393061759190379436477434589539439401344810124749337507914027486814367920958698864422367836842000865838169143446244202199945258912479805482529460471614475452210103481314126501905118652422131330354981855680471366776223771244856426005553912820240798175126871245542
> /------------------------------\
| COMMANDS |
| |
| 1) ciphertext_info |
| 2) solve_challenge <password> |
| 3) exit |
\------------------------------/
> 1
N = 97158515552243305887166842257349497196761944723849796333314570210192465467248058977973817520811466238434579242618470413011446370885671769783517122178886737136267378478851110781637707375501871752419723352747812903232025026662684439394479235396634760931569159089432041476376506570454407306279469567609447874133
e = 18408112354007983870269760119469358714794425138855862698209283717095203251527
c = 40597559051967819648988812970676624339815299554173974340185756834851649772498697096416499057158638069790189014561709885506503658509085168258788719816632907655273832559720389170846504255470221971178328064025401908633312901398161846240306537659732243916190526957713561623469784228736357566604931785270598213207
When i choose option 1 multiple times the N, e and c values are changing. When i observed carefully the modulus(N) was common for the two different groups of N, e and c. Which means the common modulus attack can be performed on it.
What is common modulus attack?
A Common Modulus attack can be used to recover the plaintext when the same message is encrypted to two RSA keys that use the same modulus.
c1 = m^e1 % N
c2 = m^e2 % N
Assume that we find out a
and b
such that (e1 * a) + (e2 * b) = 1
then we can decode the plain text as (c1 ^ a) + (c2 ^ b)
. If we substitute how c1
and c2
is calculated to above equation, we can get m^(e1 * a + e2 * b) = m^1 = m
I used the below script to perform common modulus attack.
import gmpy2
import codecs
from Crypto.Util.number import long_to_bytes
class RSAModuli:
def __init__(self):
self.a = 0
self.b = 0
self.m = 0
self.i = 0
def gcd(self, num1, num2):
if num1 < num2:
num1, num2 = num2, num1
while num2 != 0:
num1, num2 = num2, num1 % num2
return num1
def extended_euclidean(self, e1, e2):
self.a = gmpy2.invert(e1, e2)
self.b = (float(self.gcd(e1, e2)-(self.a*e1)))/float(e2)
def modular_inverse(self, c1, c2, N):
i = gmpy2.invert(c2, N)
mx = pow(c1, self.a,N)
my = pow(i, int(-self.b),N)
self.m= mx * my % N
def print_value(self):
print(self.m)
print("password: ",long_to_bytes(self.m))
def main():
c = RSAModuli()
N = 109912000665029852639110613517648329286456723008641339857280704469596849864902904497433699518400583047127310193951815657600773158070846253214404943365973285212833463085396438213201198699119774809946581077944790242766448113154577763539028406891886330959853165465205722422606657099078700917856140467421167666409
e1 = 8483335375054406977170922622504583186337530016838437863282901026110032873283
c1 = 36218335015829965116681857862094923911529670604844967899611205377832760775576260326404103773026455274015383498872180067299326123507394567136495776656445721750008053407884406429721981213028123626917002902133344001001211001153129276054910760656332569712713895293665052824134204563668126899459472515260162748678
e2 = 1696092050134307306926141131399489465348028792933115624119794329313181584569
c2 = 94573796694066531827336525466059061961386054753413091261062046296320713225203050227297438106686083614973232091111723401726835099681471993365822074034482977617851105101070185949797837862416460448964419708443214952302196757178552098691993586224947072414712511340978383044414218196216980045151588556976520156351
c.extended_euclidean(e1, e2)
c.modular_inverse(c1, c2, N)
c.print_value()
if __name__ == '__main__':
main()
I found the password as d0nt_reUs3_c0mm0n_m0duLus_iN_RSA
We are done with the first part of the challenge to solve the second part..
$ solve_challenge d0nt_reUs3_c0mm0n_m0duLus_iN_RSA
********** What the Phi? **********
Give me phi and I'll give you a flag
N = 66066217976516971108331646970610979560181811216809026125374761401858626331392870627229620955983691200317085064577482992998109175639414254550344337027939622032942798889975535536500814391606941216261629656776109514048858091999991043910655394546974123840192922578050723036392957820297149086606568090771652618243
e = 20911404459149607080658877037893189424319551114362027603024719781837162747157
d = 32917938316361268851470019425123434593213351924151169940566051302446183421108067642636220773665398581581750049220982066160562413581944447113654550659609920491577412625130031315991437456861571031336990406560139987319989696833394896272093289655434473735396587020547428232094106902009380465366775232334689581885
/------------------------------\
| COMMANDS |
| |
| 1) try_again |
| 2) phi <phi_value> |
| 3) exit |
\------------------------------/
Now we have to find the phi value to get the flag.
For this second part i struggled a lot and i wrote this script on my own and i dont know how it was successfully generated those two valid primes and the phi, haha.
import random
import math
d = 32917938316361268851470019425123434593213351924151169940566051302446183421108067642636220773665398581581750049220982066160562413581944447113654550659609920491577412625130031315991437456861571031336990406560139987319989696833394896272093289655434473735396587020547428232094106902009380465366775232334689581885
e = 20911404459149607080658877037893189424319551114362027603024719781837162747157
n = 66066217976516971108331646970610979560181811216809026125374761401858626331392870627229620955983691200317085064577482992998109175639414254550344337027939622032942798889975535536500814391606941216261629656776109514048858091999991043910655394546974123840192922578050723036392957820297149086606568090771652618243
k = (d*e)-1
t = k
g = random.randint(2,n-1)
for i in range(1000000):
if (t%2 == 0):
t = t//2
x = pow(g,t,n)
else:
g = random.randint(2,n-1)
y = math.gcd(x-1,n)
if(x>1 and y>1):
p = y
q = n//y
p=p-1
q=q-1
print("phi = ",p*q)
The output is
phi = 6606621797651697110833164697061097956018181121680902612537476140185862633139287062722962095598369120031708506457748299299810917563941425455034433702793960573434125683360519280660171596715437809832319695445903799227924638336403470759412317164477901296089898605107284333156354738806489492495310099184632281164
But the hardest truth is that we can do the second part in just 5 lines of code -_-
from Crypto.Util.number import *
from Crypto.PublicKey import RSA
rsa = RSA.construct((N, e, d))
p = rsa.p
q = rsa.q
phi = (p - 1) * (q - 1)
Anyways, when i submitted the phi value to the program it printed the flag. Woooooooh!
> 2 6606621797651697110833164697061097956018181121680902612537476140185862633139287062722962095598369120031708506457748299299810917563941425455034433702793960573434125683360519280660171596715437809832319695445903799227924638336403470759412317164477901296089898605107284333156354738806489492495310099184632281164
What?! How did you do that??
flag{aR3nT_U_tH3_RSA_ninJA}
Flag : flag{aR3nT_U_tH3_RSA_ninJA}
Thank you Phi in Too Much Common.
Docker Leakage
Docker Leakage is a reversing challenge, the challenge provides a dockeREleakage.zip file which consist of docker image details.
Lets do some reversing on the dockeRElekage folder.
I found some intresting data in the acbb216b17482071caca135101282177f6ffed7b8ee0bfc5323aae103c216d74.json
file.
{"architecture":"amd64","config":{"Hostname":"","Domainname":"","User":"","AttachStdin":false,"AttachStdout":false,"AttachStderr":false,"Tty":false,"OpenStdin":false,"StdinOnce":false,"Env":["PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"],"Cmd":["/bin/sh"],"Image":"sha256:3b6dbca4ef6990ef88e9d4f28fae8ca57308e779dcb94dc9c4a8fee02e1322c0","Volumes":null,"WorkingDir":"/chal","Entrypoint":null,"OnBuild":null,"Labels":null},"container":"1e4786117e284daaf67335704abef1dac17eaff92d5714a9683ce735c48004c8","container_config":{"Hostname":"1e4786117e28","Domainname":"","User":"","AttachStdin":false,"AttachStdout":false,"AttachStderr":false,"Tty":false,"OpenStdin":false,"StdinOnce":false,"Env":["PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"],"Cmd":["/bin/sh","-c","#(nop) ","CMD [\"/bin/sh\"]"],"Image":"sha256:3b6dbca4ef6990ef88e9d4f28fae8ca57308e779dcb94dc9c4a8fee02e1322c0","Volumes":null,"WorkingDir":"/chal","Entrypoint":null,"OnBuild":null,"Labels":{}},"created":"2022-09-03T07:46:14.235116602Z","docker_version":"20.10.11+dfsg1","history":[{"created":"2022-08-09T17:19:53.274069586Z","created_by":"/bin/sh -c #(nop) ADD file:2a949686d9886ac7c10582a6c29116fd29d3077d02755e87e111870d63607725 in / "},{"created":"2022-08-09T17:19:53.47374331Z","created_by":"/bin/sh -c #(nop) CMD [\"/bin/sh\"]","empty_layer":true},{"created":"2022-09-03T07:46:11.653961901Z","created_by":"/bin/sh -c #(nop) WORKDIR /chal"},{"created":"2022-09-03T07:46:11.863666686Z","created_by":"/bin/sh -c #(nop) COPY file:d65d0cfa1f5c483eff02b6016940ff4d85eb3b216f05d23a2b891cea6801be2a in p-flag.txt "},{"created":"2022-09-03T07:46:12.680399343Z","created_by":"/bin/sh -c echo \"ZmxhZ3tuM3Yzcl9sMzR2M181M241MTcxdjNfMW5mMHJtNDcxMG5fdW5wcjA=\" \u003e /dev/null","empty_layer":true},{"created":"2022-09-03T07:46:13.319972067Z","created_by":"/bin/sh -c cat p-flag.txt \u003e tmp.txt; rm -rf flag.txt p-flag.txt; mv tmp.txt flag.txt; echo \"\" \u003e\u003e flag.txt"},{"created":"2022-09-03T07:46:14.02363242Z","created_by":"/bin/sh -c echo \"Find the rest of the flag by yourself!\" \u003e\u003e flag.txt"},{"created":"2022-09-03T07:46:14.235116602Z","created_by":"/bin/sh -c #(nop) CMD [\"/bin/sh\"]","empty_layer":true}],"os":"linux","rootfs":{"type":"layers","diff_ids":["sha256:994393dc58e7931862558d06e46aa2bb17487044f670f310dffe1d24e4d1eec7","sha256:3496f3297d64782df57121721ca912bebd3d8c0bf5c5a12d76ac2f4c58d900a5","sha256:0c0811e580c073e93ce547fd176f40debc3e67b99325fa633c4ea877dbf2c543","sha256:10f39dbbae65c969d02d804d337d3836517c61706ec93b1f929a326451cbe0b2","sha256:d09e067ee442e434d4c350e403b686f5038c29da8f94eff1f25b15aa7c158c46"]}}
There is a base64 encoded data found in the above data. ZmxhZ3tuM3Yzcl9sMzR2M181M241MTcxdjNfMW5mMHJtNDcxMG5fdW5wcjA=
$ echo "ZmxhZ3tuM3Yzcl9sMzR2M181M241MTcxdjNfMW5mMHJtNDcxMG5fdW5wcjA=" | base64 -d
$ flag{n3v3r_l34v3_53n5171v3_1nf0rm4710n_unpr0
As you can see i got the first part of the flag.
Lets move much deeper to for the remaining flag. I found a layer.zip file in the 4ec42253273e93963f11241e29497f0fcef730a2864d8ea025dcdb4fc316659e folder. When i extract that zip file i found the flag.txt file inside the chall folder. The flag.txt has the second part of the flag.
Flag : flag{n3v3r_l34v3_53n5171v3_1nf0rm4710n_unpr073c73d_w17h1n_7h3_d0ck3rf1l3}
Word Wide Web
http://web.chal.csaw.io:5010/stuff have the huge number of words in it, among all of them one word has the hyperlink to the another page. When we automate the process until we get the last page which can get the flag.
#!/usr/bin/env python3
import requests
import re
s = requests.Session()
url = 'http://web.chal.csaw.io:5010'
path = ''
while True:
r = s.get(url + path)
body = r.text
pattern = r'\<a href="(\/.+)"\>'
m = re.search(pattern, body)
if m is None:
print(body)
break
path = m.group(1)
Flag : CTF{w0rdS_4R3_4mAz1nG_r1ght}
Our Spy In New Terrain(OSINT)
This is an intresting OSINT challenge from the forensics category. They provided just the twitter account name of the recruit. We have to answer the questions been asked in the remote program nc misc.chall.csaw.io 5005
.
There are total 6 questions asked.
1.When did the enemy agent join twitter?
08/2022
2.What is the spy's github username?
I used wayback machine to view the deleted tweets of the agent.
spyduhman
3.What is the full name of the file that contains communications between the enemy and the evil spy?
There is a deleted log.txt file in which communications are done.
log.txt
4.Which country is the target based in?
The log.txt file contains a link to the a audio file. I used an online morse code converter to decode the Assignment.wav
The decoded text is HTLLO EVIL AGENT YOUR NEXT TARGET IS A BANK THE BANK'S BIN NUMBER IS 452234 THE TARGETS SWIFT CODE IS YOUR PASSWORD FOR MORE INSTRUCTIONS VISIT BIT.LY SLASH ØSINTSEC GOOD LUCK
canada
5.What is the target's international Swift code?
BIN : 452234(canada bank) The bank which is having above bin number is TORONTO-DOMINION BANK
SWIFT code: TDOMCATTTOR
6.What is a crime?
The YoureSoClose.pdf is available in bit.ly/osintsec the link is found from the morse code decoder.
The YoureSoClose.pdf is password protected the password is TDOMCATTTOR. The title of the pdf is the crime.
copyright infringement
After that we got the flag.
Flag : flag{C0N6r475463N7600DW0rKN3X771M3N0PU811C53rV3r}