️🏴 BSides London 2023 CTF
BSides London 2023 CTF
Prompted by a message on the Hack The Box Meetup:UK Discord, the below documents my attempt at the CTF from the BSides London 2023 conference, shamelessly purloined from the archive at phyushin/bsidesLDN2023
.
…and yes, I resisted the urge to peek at the write-up. Honest.
1985
AM@.\;(t"!;cP\G=>4Pj=>2U/<^o)=<,56-=&qXf;cPhRAl1]S
85 you say?
>>> import base64
>>> print(
... base64.b64decode(
... base64.a85decode(
... """AM@.\;(t"!;cP\G=>4Pj=>2U/<^o)=<,56-=&qXf;cPhRAl1]S"""
... ).decode()
... ).decode()
... )
{ENCODING_IS_NOT_ENCRYPTION}
Bsides_guide
See the APK here.
Using http://www.javadecompilers.com/apk and viewing the decompiled source code, sources/com/bsides/london/ctf/MainActivity$$ExternalSyntheticLambda1.java
contains:
public final String getFlag() {
RequestQueue queue = Volley.newRequestQueue(this);
Intrinsics.checkNotNullExpressionValue(queue, "newRequestQueue(this)");
String url = getString(R.string.pbin);
Intrinsics.checkNotNullExpressionValue(url, "getString(R.string.pbin)");
Ref.ObjectRef retVal = new Ref.ObjectRef();
retVal.element = "";
queue.add(new StringRequest(0, url + getString(R.string.slug), new MainActivity$$ExternalSyntheticLambda0(retVal), new MainActivity$$ExternalSyntheticLambda1(retVal)));
return (String) retVal.element;
}
Those referenced R.string.pbin
and R.string.slug
values are:
public static int pbin = 2131820698;
public static int slug = 2131820707;
That said, the file ./resources/res/values/strings.xml
also contains these:
<string name="pbin">https://www.pastebin.com/raw/</string>
<string name="slug">DTGDa6Pi</string>
…so combining those:
❯ curl https://pastebin.com/raw/DTGDa6Pi
{PASTEBIN_LFG_BSIDES_LDN}
Devoops
See the provided ZIP file, which contains a Git repository.
❯ git log
commit 257aace5ae595f6f7cb378de0752fa6ca0997ec5 (HEAD -> master)
Author: phyushin <phyushin@biosin>
Date: Sun Dec 3 11:42:25 2023 +0000
LGTM
commit 3852fe8a088da8d32ab7deb0815f9ccfd629c30e
Author: phyushin <phyushin@biosin>
Date: Sun Dec 3 11:20:58 2023 +0000
removed mistakenly uploaded secrets.py
commit 1b85f5e860571eab612b5640c09cd91954e7a55d
Author: phyushin <phyushin@biosin>
Date: Sun Dec 3 11:18:51 2023 +0000
initial commit
After a git checkout
that suspicious 3852fe8…
commit:
❯ cat secrets.py
secrets = {
'ssid': 'SPONSOR_FLAG',
'pw': 'bsideslondon',
'flag':'{GIT_IGNORE_PLS}'
}
Dont_tell_my_friends
See the provided WAV file.
Playing the file, it sounds like a series of dial tones. Definite echoes of the Dialed Up CTF here. And, as per that CTF, ribt/dtmf-decoder
is useful here:
❯ python3 dtmf.py ~/Downloads/sample.wav
77767777
The multi-tap cipher decoder here suggest that this is RMS
but more likely:
SMS
Morse
Not Everything is as it seems, with CTFs morseo
we've received the following data:
`111 100 100 000 001101 01 10 100 001101 0 0001 0 10 000`
Translating that into a more standard morse-code format and using CyberChef:
--- -.. -.. ... ..--.- .- -. -.. ..--.- . ...- . -. ...
…gives:
ODDS_AND_EVENS
Not Det
See the provided ZIP file:
❯ unzip -l notdet.zip
Archive: notdet.zip
Length Date Time Name
--------- ---------- ----- ----
0 2023-11-23 01:45 net6.0/
410 2023-11-23 01:45 net6.0/NotDet.deps.json
6144 2023-11-23 02:00 net6.0/NotDet.dll
147968 2023-11-23 02:00 net6.0/NotDet.exe
10944 2023-11-23 02:00 net6.0/NotDet.pdb
147 2023-11-23 01:45 net6.0/NotDet.runtimeconfig.json
--------- -------
165613 6 files
Leaning on ilspycmd
via dotnet
in Docker to do this.
After decompiling, there’s this snippet:
namespace NotDet
{
internal class SuperSecretClass
{
private string key = "ZTBsTVgxTlFXVjlYU1ZSSVgwMVpYMHhKVkZSTVJWOUZXVVY5";
private string nothingHere()
{
return key;
}
…which, as it turns out, is the double-base64-encode flag:
root@6843fafef480:/mnt# echo ZTBsTVgxTlFXVjlYU1ZSSVgwMVpYMHhKVkZSTVJWOUZXVVY5 | base64 -d | base64 -d
{IL_SPY_WITH_MY_LITTLE_EYE}
monthly_backwhat
See the provided pcap file.
Using Wireshark, it’s possible recreate three objects sent via HTTP:
backup-2023-10-09.zip
backup-2023-11-09.zip
flag.txt
The latter two appear to be corrupt but backup-2023-10-09.zip
is a valid ZIP file with one entry: flag.txt
…and it’s password-protected.
import itertools
import string
import zipfile
z = zipfile.ZipFile("backup-2023-10-09.zip")
def bruteforce():
count = itertools.count()
for length in count:
for guess in itertools.product(
string.ascii_lowercase,
repeat=length,
):
guess = "".join(guess)
try:
z.extract("flag.txt")
return guess
except RuntimeError:
...
print(bruteforce())
It takes a little while but the password is—unsurprisingly, with the benefit of hindsight—bsides
.
❯ unzip -c backup-2023-10-09.zip flag.txt
Archive: backup-2023-10-09.zip
[backup-2023-10-09.zip] flag.txt password:
extracting: flag.txt
{B4S1C_TCP_DUMP_CH4LL3NG3}
no-kia
See the provided JPEG file.
It appears there’s a hidden ZIP file:
┌─[root@psy-xps-13]─[/tmp]
└──╼ #steghide extract --stegofile 3210.jpg
Enter passphrase:
wrote extracted data to "info.zip".
┌─[root@psy-xps-13]─[/tmp]
└──╼ #unzip -l info.zip
Archive: info.zip
Length Date Time Name
--------- ---------- ----- ----
0 2023-11-22 20:58 info/
498 2023-11-18 22:54 info/chatlog.txt
75896 2023-11-18 22:56 info/important-docs.zip
--------- -------
76394 3 files
important-docs.zip
is password-protected; chatlog.txt
contains some…thing:
Neo:
3 444 3 0 999 666 88 0 44 33 2 777 0 9 44 2 8 0 8 44 33 0 66 33 9 0 7777 999 7777 2 3 6 444 66 0 7777 2 444 3 0 8 44 33 0 7 2 7777 7777 9 666 777 3 0 333 666 777 0 8 44 33 0 3 666 222 88 6 33 66 8 0 9999 444 7 0 9 2 7777 0
---
Trinity:
999 33 2 44 0 444 8 7777 0 333 666 555 555 666 9 8 44 33 9 44 444 8 33 777 2 22 22 444 8 0 2 555 555 0 555 666 9 33 777 0 222 2 7777 33 0 66 666 0 7777 7 2 222 33 7777
�[34m~@~T
Neo:
4 777 33 2 8 0 8 44 2 8 7777 0 9 666 777 55 33 3 0 8 44 2 66 55 7777 0
As hinted by the name/image, this is multi-tap encoding again, i.e. the buttons that would have been pressed when texting on a mobile phone.
Again, thanks to https://www.dcode.fr/multitap-abc-cipher:
Neo:
DID YOU HEAR WHAT THE NEW SYSADMIN SAID THE PASSWORD FOR THE DOCUMENT ZIP WAS
Trinity:
YEAH ITS FOLLOWTHEWHITERABBIT ALL LOWER CASE NO SPACES
Neo:
GREAT THATS WORKED THANKS
More files:
┌─[✗]─[root@psy-xps-13]─[/tmp/info]
└──╼ #unzip important-docs.zip
Archive: important-docs.zip
[important-docs.zip] important_docs/email-id2091234.html password:
inflating: important_docs/email-id2091234.html
inflating: important_docs/TPS-report.doc
inflating: important_docs/2021-sales-figs.csv
creating: important_docs/grandad/
inflating: important_docs/grandad/grandads-passwords.txt
extracting: important_docs/grandad/grandads-file.zip
inflating: important_docs/2020-sales-figs.csv
grandads-passwords.txt
does indeed contain passwords:
┌─[✗]─[root@psy-xps-13]─[/tmp/info/important_docs]
└──╼ #!ca
cat grandad/grandads-passwords.txt
Password : Account
reapply8-monotype-deniable : Facebook
amid3-unease-wow : Hotmail
playgroup-quantum9-zombie : Amazon
armband9-virus-plot : Netflix
paragraph-varying-handwoven1 : zipfile
…and that last one is indeed for the ZIP file:
┌─[root@psy-xps-13]─[/tmp/info/important_docs]
└──╼ #unzip grandad/grandads-file.zip
Archive: grandad/grandads-file.zip
creating: grandads-files/
[grandad/grandads-file.zip] grandads-files/Wales.jpg password:
inflating: grandads-files/Wales.jpg
inflating: grandads-files/Scotland.png
extracting: grandads-files/flag.txt
inflating: grandads-files/US.jpg
┌─[root@psy-xps-13]─[/tmp/info/important_docs]
└──╼ #cat grandads-files/flag.txt
{BSIDES-GLOBAL-FLAGS}
pag3r
See the provided JPEG file.
The image shows braille:
⠓⠁⠉⠅⠞⠓⠑⠏⠇⠁⠝⠑⠞
This, wonderfully enough, is hacktheplanet
; again, a hidden ZIP file:
┌─[root@psy-xps-13]─[/tmp]
└──╼ #steghide extract --stegofile /mnt/pager.jpg --passphrase hacktheplanet
wrote extracted data to "flag.zip".
That is again password-protected but with the same password:
┌─[root@psy-xps-13]─[/tmp]
└──╼ #unzip flag.zip
Archive: flag.zip
[flag.zip] flag password:
inflating: flag
┌─[root@psy-xps-13]─[/tmp]
└──╼ #cat flag
ZRFF JVGU GUR ORFG! QVR YVXR GUR ERFG!
{OFVQRF-UNPX-GUR-CYNARG}
Thankfully that’s just ROT13:
❯ echo "ZRFF JVGU GUR ORFG! QVR YVXR GUR ERFG!
∙ {OFVQRF-UNPX-GUR-CYNARG}" | tr 'A-Za-z' 'N-ZA-Mn-za-m'
MESS WITH THE BEST! DIE LIKE THE REST!
{BSIDES-HACK-THE-PLANET}
xordinary
See the provided main
binary.
The binary uses a key to XOR a base64-encoded ciphertext to the flag (both of which are visible in a straight strings
call):
- the key is
bsides
. - the base64-encoded ciphertext is
GS0nLDoqUl8sLScsGA==
.
Reading the binary via Ghidra, the output should be this:
"".join(
chr(c ^ ord(key[i & len(key)]))
for i, c in enumerate(base64.b64decode("GS0nLDoqUl8sLScsGA=="))
)
…but the &
bitwise operator can produce a value exceeding the key length. Constraining that slightly to (i & len(key)) % len(key)
produces: {ONE_O0=NONE}
which is close to a reasonable value but clearly not quite right.
It turns out that, if that condition is true, the raw base64-decoded value is used instead of an XOR’d one:
data = base64.b64decode("GS0nLDoqUl8sLScsGA==")
out = ""
for i, c in enumerate(data):
if i & len(key) == len(key):
out += chr(c)
else:
out += chr(c ^ ord(key[i & len(key)]))
print(out)
…which yields:
{ONE_OR_NONE}
zipity_doo_dah
See the provided ZIP file.
The ZIP has only one entry:
❯ unzip -l flag.zip
Archive: flag.zip
Length Date Time Name
--------- ---------- ----- ----
8324 2023-11-11 21:49 flag.zip
--------- -------
8324 1 file
Presuming this is a nested series of ZIPs:
import io
import zipfile
z = zipfile.ZipFile("flag.zip")
while True:
if "flag.txt" in z.namelist():
print(z.read("flag.txt").decode())
break
z = zipfile.ZipFile(io.BytesIO(z.read("flag.zip")))
…which, after a few dozen iterations, prints:
{ZIPS_ALL_THE_WAY_DOWN}