️🏴 BSides London 2023 CTF

Better late than never.

2024-01-29

#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):

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}