Alessandro Sartori

Cyber Apocalypse 2022 - Writeup


About Cyber Apocalypse 2022

After having a lot of fun with the Cyber Apocalypse 2021 CTF, I decided to take part in this year’s edition too (check the link for more info). Unfortunately though, it has been a pretty busy period for me, and out of 5 days I could only play on few evenings, so the number of challenges I managed to solve is quite low.

Index

Here are the puzzles I solved:

Blinker Fluids

This challenge was focused on a web portal that allowed to create, delete, and export invoices as PDFs.

Blinker Fluids web portal

The image above shows the textarea where invoices can be created using the Markdown format. The downloadable part of the challenge showed that the flag was located in a flag.txt file in the root folder of the challenge, which however was not accessible from outside. The only content being statically served was the /static/invoices/ path in which PDFs were generated by the md-to-pdf NodeJS module. I tried embedding some HTML in the markdown code to somehow include the flag while being generated, but with no success.

<p id='target'></p>

<script>
    x = new XMLHttpRequest();
    x.onload = function() {
        document.getElementById('target').innerHTML = this.responseText;
    };
    x.open("GET","file:///flag.txt");
    x.send();
</script>

After a while, I found the solution to be an RCE vulnerability concerning md-to-pdf versions below 5.0, which was the case. Essentially, code can be injected in the frontmatter of an input file like so:

---js
((require("child_process")).execSync("cp ../flag.txt  static/invoices/x.txt"))
---

Which lead to:

Blinker Fluids web portal

Compressor

Connecting to the indicated socket returned a terminal interface to a utility that apparently allows to move across four directories and perform a number of predefined actions such as list, read, or compress files.

Compressor interface

After playing around for a bit it became obvious that the vulnerability to discover had to do with some kind of command injection. After reading some other write-ups I realized there was a much shorter path to the solution than mine (piping user commands in the input), but here’s how I did it. Essentially, the detail in which I focused was the freedom in the <options> part of the zip command, since the flag.txt file was at the root and the cat command was prefixed with ./.

Compressor flag location Compressor tweaked command

At this point, all I did was creating .zip archive with the -0 flag to disable compression and only store the flag.txt file. Being uncompressed, I could read the file contents just by invoking cat on the archive.

Compressor final command Compressor flag.txt contents

Matrioska Brain

This problem stated that a pattern was hidden among heat measurements of some concentric Dyson Spheres. I quickly inspected the provided file, a CSV list of integers:

$ head -n 4 heat_measurements.csv
"Sphere no.","1","2","3","4","5","6","7","8","9","10","11","12","13","14","15","16","17","18","19","20","21","22","23","24","25","26","27","28","29","30","31","32","33","34","35","36","37","38","39","40","41","42","43","44","45","46","47","48","49","50","51","52","53","54","55","56","57","58","59","60","61","62","63","64","65","66","67","68","69","70","71","72","73","74","75","76","77","78","79","80","81","82","83","84","85","86","87","88","89","90","91","92","93","94","95","96","97","98","99","100","101","102","103","104","105","106","107","108","109","110","111","112","113","114","115","116","117","118","119","120","121","122","123","124","125","126","127","128","129","130","131","132","133","134","135","136","137","138","139","140","141","142","143","144","145","146","147","148","149","150","151","152","153","154","155","156","157","158","159","160","161","162"
"Sphere 1",28,88,70,21,64,25,20,26,25,24,61,24,22,22,63,60,79,23,20,83,64,22,69,67,27,64,80,72,23,80,28,73,89,86,30,60,84,23,26,62,81,23,75,66,26,74,67,24,23,29,67,70,28,26,66,77,67,71,76,76,80,72,20,20,72,81,30,87,65,21,62,77,29,26,87,73,28,24,30,61,84,70,27,26,70,78,26,79,84,86,20,63,62,68,78,68,84,87,20,80,86,28,71,60,86,22,62,27,83,63,64,22,82,60,30,22,86,62,26,79,89,20,82,64,20,26,26,65,69,28,29,65,67,60,65,84,82,88,29,82,85,25,87,83,21,28,86,64,61,86,25,69,60,26,23,29,29,27,82,24,25,78
"Sphere 2",27,73,71,22,88,72,87,27,84,61,74,27,74,89,26,80,71,21,75,71,26,26,69,88,20,28,78,27,27,83,20,27,73,29,24,65,20,87,79,24,74,25,22,89,26,75,23,63,64,65,63,22,72,86,21,88,63,61,70,78,67,27,90,87,28,64,29,30,86,26,69,26,64,63,28,60,26,89,82,28,70,22,84,66,72,89,25,61,73,60,30,64,63,63,82,70,74,21,30,60,70,29,23,67,20,24,73,22,29,83,30,23,64,21,71,84,24,75,29,26,77,28,78,30,72,63,72,67,29,90,90,27,77,73,88,80,61,88,21,88,61,30,87,26,65,78,26,79,89,28,30,90,62,60,74,24,66,82,64,82,24,86
"Sphere 3",21,28,28,30,61,64,76,25,66,83,62,23,29,24,77,90,28,89,70,85,73,22,88,63,22,64,23,79,22,83,30,81,25,62,28,82,83,80,23,89,67,20,69,21,21,74,90,24,23,86,89,74,63,27,74,80,84,84,76,70,74,61,78,30,61,89,21,78,26,27,69,61,79,21,60,61,22,25,22,82,87,22,67,25,24,72,78,23,21,20,79,64,70,63,63,61,67,90,20,82,69,30,68,26,76,22,60,28,69,23,76,21,74,66,87,27,62,72,24,61,22,23,68,67,26,28,82,88,67,89,24,66,90,86,71,72,64,69,23,22,24,27,70,70,69,22,65,80,23,75,30,65,74,77,61,22,67,88,71,86,69,28

I tried the first and simplest thing that came to my mind: plotting them as a matrix heatmap. Luckily, that was all it took to discover the flag:

import matplotlib.pyplot as plt


f = open('heat_measurements.csv').readlines()[1:]
mat = [list(map(int, line.strip().split(',')[1:])) for line in f]
plt.matshow(mat)
plt.show()

# HTB{1MM3NS3_3N3RGY_1MM3NS3_H34T}

Matrioska Brain flag

WIDE

The challenge folder contained two files: a main executable called wide and a database file called db.ex. Running the binary outputs:

$ ./wide db.ex
[*] Welcome user: kr4eq4L2$12xb, to the Widely Inflated Dimension Editor [*]
[*]    Serving your pocket dimension storage needs since 14,012.5 B      [*]
[*]                       Displaying Dimensions....                      [*]
[*]       Name       |              Code                |   Encrypted    [*]
[X] Primus           | people breathe variety practice  |                [*]
[X] Cheagaz          | scene control river importance   |                [*]
[X] Byenoovia        | fighting cast it parallel        |                [*]
[X] Cloteprea        | facing motor unusual heavy       |                [*]
[X] Maraqa           | stomach motion sale valuable     |                [*]
[X] Aidor            | feathers stream sides gate       |                [*]
[X] Flaggle Alpha    | admin secret power hidden        |       *        [*]
Which dimension would you like to examine?

Responding to the prompt with the index of a dimension provides a short description, except for the last one (supposedly containing the flag) which required a passphrase:

Which dimension would you like to examine? 1
The Ice Dimension
Which dimension would you like to examine? 4
The Water Dimension
Which dimension would you like to examine? 6
[X] That entry is encrypted - please enter your WIDE decryption key:

Checking for ASCII strings in the binary or the database file is of no help, as one simply gived no useful information and the other is in fact encrypted. However, decompiling the binary yields an interesting piece of code:

// [...]
    printf("[X] That entry is encrypted - please enter your WIDE decryption key:");
    fgets(local_c8, 0x10, stdin);
    mbstowcs(local_1c8, local_c8, 0x10);
    iVar1 = wcscmp(local_1c8, L"sup3rs3cr3tw1d3");

    if (iVar == 1) {
        // [...]
    }
// [...]

Essentially, the string was not visible because it is encoded as a Wide Character String and therefore has characters of size greater than 8 bits. Entering the passphrase yields the encrypted flag:

Which dimension would you like to examine? 6
[X] That entry is encrypted - please enter your WIDE decryption key: sup3rs3cr3tw1d3
HTB{str1ngs_4r3nt_4lw4ys_4sc11}

Rebuilding

rebuilding is a binary that, when called, checks if the provided argument matches a supposed password. Decompiling the file reveals this main function:

    // [...]
    local_14 = 0;
    sVar1 = strlen(*(char **)(param_2 + 8));
    if (sVar1 == 0x20) {
        for (local_10 = 0; local_10 < 0x20; local_10 = local_10 + 1) {
            printf("\rCalculating");
            for (local_c = 0; local_c < 6; local_c = local_c + 1) {
                if (local_c == local_10 % 6) {
                    __c = 0x2e;
                } else {
                    __c = 0x20;
                }
                putchar(__c);
            }
            fflush(stdout);
            local_14 = local_14 +
                       (uint)((byte)(encrypted[local_10] ^ key[local_10 % 6]) ==
                             *(byte *)((long)local_10 + *(long *)(param_2 + 8)));
            usleep(200000);
        }
        puts("");
        if (local_14 == 0x20) {
            puts("The password is correct");
            uVar2 = 0;
        } else {
            puts("The password is incorrect");
            uVar2 = 0xffffffff;
        }
    } else {
        puts("Password length is incorrect");
        uVar2 = 0xffffffff;
    }
    return uVar2;

The first noticeable thing is that the password needs to be 32 characters long. Searching for the key and encrypted array declarations reveal their contents: the string “humans” for the former and a series of bytes for the latter.

00301041 68 75 6d        ds         "humans"
61 6e 73 00

; ...

00301020 29              undefined129h                     [0]
00301021 38              undefined138h                     [1]
00301022 2b              undefined12Bh                     [2]
00301023 1e              undefined11Eh                     [3]
00301024 06              undefined106h                     [4]
00301025 42              undefined142h                     [5]
00301026 05              undefined105h                     [6]
00301027 5d              undefined15Dh                     [7]
00301028 07              undefined107h                     [8]
00301029 02              undefined102h                     [9]
0030102a 31              undefined131h                     [10]
0030102b 42              undefined142h                     [11]
0030102c 0f              undefined10Fh                     [12]
0030102d 33              undefined133h                     [13]
0030102e 0a              undefined10Ah                     [14]
0030102f 55              undefined155h                     [15]
00301030 00              undefined100h                     [16]
00301031 00              undefined100h                     [17]
00301032 15              undefined115h                     [18]
00301033 1e              undefined11Eh                     [19]
00301034 1c              undefined11Ch                     [20]
00301035 06              undefined106h                     [21]
00301036 1a              undefined11Ah                     [22]
00301037 43              undefined143h                     [23]
00301038 13              undefined113h                     [24]
00301039 59              undefined159h                     [25]
0030103a 36              undefined136h                     [26]
0030103b 54              undefined154h                     [27]
0030103c 00              undefined100h                     [28]
0030103d 42              undefined142h                     [29]
0030103e 15              undefined115h                     [30]
0030103f 11              undefined111h                     [31]
00301040 00              undefined100h                     [32]

At this point it was only a matter of reversing the XOR operation between the two strings to recover the original password… Unfortunately though, the output was composed by a lot of non-printable characters and the password was not accepted anyway.

import binascii


asm = """
    00301020 29              undefined129h                     [0]
    ; ...           
    00301040 00              undefined100h                     [32]
""".strip().split('\n')

key = "humans".encode('utf-8')
sol = []
encrypted = ''.join(l.split()[1] for l in asm)
encrypted = binascii.unhexlify(encrypted)

for i in range(len(encrypted)):
  sol.append(encrypted[i]^key[i%6])

print(''.join(chr(x) for x in sol))

At this point I tried digging a bit deeper in the code and found out that the key array was being overwritten in one of the initialization functions that set up the environment with the string “aliens” one character at a time (so that the strings command would not detect it). Changing the key value in the script yielded the correct flag:

$ ./rebuilding HTB{h1d1ng_1n_c0nstruct0r5_1n1t}
Preparing secret keys
Calculating . . .
The password is correct

Snake Code

This challenge provides a .pyc file, which is compiled Python 2.7 code and therefore not human readable. Launching it opens a window with an ASCII version of the Snake game, where eating the target sometimes reveals a letter, supposedly the characters of the final flag. Since playing it all the way through and remembering the letters was not feasible, I used Decompyle++ to reveal its original source:

# Source Generated with Decompyle++
# File: chall.pyc (Python 2.7)

import marshal
import types
import time
ll = types.FunctionType(marshal.loads('YwEAAAABAAAABQAAAEMAAABzNAAAAHQAAGoBAHQCAGoDAHQEAGQBAIMBAGoFAHwAAGoGAGQCAIMB\nAIMBAIMBAHQHAIMAAIMCAFMoAwAAAE50BAAAAHpsaWJ0BgAAAGJhc2U2NCgIAAAAdAUAAAB0eXBl\nc3QMAAAARnVuY3Rpb25UeXBldAcAAABtYXJzaGFsdAUAAABsb2Fkc3QKAAAAX19pbXBvcnRfX3QK\nAAAAZGVjb21wcmVzc3QGAAAAZGVjb2RldAcAAABnbG9iYWxzKAEAAAB0AQAAAHMoAAAAACgAAAAA\ncwcAAAA8c3RkaW4+dAoAAABsb2FkTGFtYmRhAQAAAHQAAAAA\n'.decode('base64')), globals())
i0 = ll('eJxLZoACJiB2BuJiLiBRwsCQwsjQzMgQrAES9ythA5JFiXkp+bkajCB5kKL4+Mzcgvyikvh4DZAB\nCKKYHUjYFJekZObZlXCA2DmJuUkpiXaMEKMZGAC+nBJh\n')
i1 = ll('eJxLZoACJiB2BuJiLiBRwsCQwsjQzMgQrAES9ythA5LJpUXFqcUajCB5kKL4+Mzcgvyikvh4DZAB\nCKKYHUjYFJekZObZlXCA2DmJuUkpiXaMEKMZGADEORJ1\n')
f0 = ll('eJxLZmRgYABhJiB2BuJiXiBRw8CQxcCQwsjQzMgQrAGS8ssEEgwaIJUl7CAiMzc1v7QEIsAMJMoz\n8zTASkBEMUiJTXFJSmaeXQkHiJ2TmJuUkmgHVg5SAQBjWRD5\n')
f1 = ll('eJxLZmRgYIBhZyAu5gISNQwMWQwMzQwMwRogcT8wWcIKJNJTS5IzIFxmIFGemacBpBjARDE7kLAp\nLknJzLMr4QCxcxJzk1IS7cDKQSoAvuUPJw==\n')
f2 = ll('eJx1kL1uwkAQhOfOBsxPQZUmL+DOEnWUBghEQQbFIESVglUkY5ECX+lHoMz7Jrt7HCgSOWlGO/rm\n1tbtIwBBY1b9zdYYkEFlcRqiAQoWxaginDJhjcUBijNQy+O24jxgfzsHdTxOFB8DtoqPoK7HPcXn\ngCPFZ1BfcUGsdMA/lpc/fEqeUBq21Mp0L0rv/3grX/f5aELlbryVYzbXZnub7j42K5dcxslym7vu\nJby/zubrK1pMX9apPLOTraReqe9T3SlWd9ieakfl17OTb36OpFE/CDQDE5vHv7K/FKBNmA==\n')
f3 = ll('eJxVj00KAjEMhV+b8Q9040IZT9C9WxHEvRvBC1iFUhhk2sUIIwgexLWn1KQzI9qSl/DlhaZHDSDj\nII4tR3ix1IBVyK1GXitImt/0l1JDSSih1rAZfIZyI4x9BRIkeKA8SLeF1Dl9clIHG+c9OakdZ35O\nT/o+yiciZI2Hgvpt702Pt925Nx/HFZwSGbIYqaL87FS5aKSIgi5JbZR/F1WTrkZmk4QByypE64p1\nap6X4g8LaaoZ3zFGfzFVE/UBTuovhA==\n')
f4 = ll('eJw1zDsKgEAMBNCJilb2drZ7AEuxsbfxBOIHFFkWNqWdF3eyYJEXkgxZcwB/jazYkkdwUeAVCAcV\nW3F4MjTt7ISZyWVUS7KEsPtN7cW9e2ddLeKTIXk7gkSsSB91O/2g9uToLBELO0otH2W6Ez8=\n')
f5 = ll('eJxdjr0OwjAMhM9J+as6M7HTF0AsiKV7F54ACJUKVaiSjOnEi2MbISQGf4rtu3OuMwBSBVfDFQdG\nBhzwMAgNMsER1s58+wJ3Hlm4Ai/z33YGE+A1IrNljnBBtiLYT1ZSf2sr6lMt19u+ZPYQkGDJqA0j\nycfap7+lBT/C2bveJ/UkEQ7KqByTGMbPKNQSpojiPMTEzqNKup2aKlnShramopJW5g2ipyUM\n')
f6 = ll('eJxdjTEOglAQRB98iMbEKxhLbkBjaLSwsrHWBEUJCRKULTT5VFzc3W9nMS+zk93ZqwNS1UK1VQ17\nRQ0CVcQUsTvljO4vWjEmSIRP8A4PXn3MlHKOea4DlxyzWMsOjXUHK/bpVXb1TWy855kF2gN9SPo2\nDD9+At8Zdm4YZorNIFXTFTI335aPS1UWtie28QV3xx4p\n')
f7 = ll('eJxtjz8LwjAQxV/S1mrRxcnZKat/qyAuOrv0E4ilIJRS2hsUCg7OfmcvubZTIe/97nKPcHkEADpd\nWPWPjYCGj0Kj0fjIfHwVqiWIbzxbJ6SHEleQ1yf8ocQHFLSJqgKN+nTYVUUEGndNCiRG8UY3M7F7\nabb7TrAS7AVrQSw4CDaCreBo7CfJPvdy/nZeummZuyY3bHBWh2ynmtJncXaRLLaJem6HaqGiVlMV\n6Zn+Azn/L1k=\n')
f8 = ll('eJwljr0KAkEMhCf3o2hrIb7BlWIhFiKC1jYWViKHe+qKnHob0GKt7sVNcsV8ZDeTSc45gJ5oINqI\nwkkQgTvQAvRdgwmO0BK2xxl+uTUTxBwugUtxT8EZIiHKZ4o21dZE7FLRe4yD+nMLixlchvG+0KU7\nPxR6EVjhSVDoKazt86MqG6uasr5WrI3SucCNbJPEp685keIy576aqktThVs3r0kf48s8r4c9Ogaj\nL3SnIej8MrDz9aqLXJhPzwMNaURT4R/aUC0X\n')
a1 = ll('eJxLZmRgYIBhZyAuZgESKYwMwRpMQIZfCUhcWwNIMGiAmGB+DoQPIorZgYRNcUlKZp5dCQeInZOY\nm5SSaAdWDFIBALI0C1U=\n')
a2 = ll('eJxLZmRgYIBhZyAuZgESKYwMwRpMQIZfCUhcWQNIMGiAmGB+DoQPIorZgYRNcUlKZp5dCQeInZOY\nm5SSaAdWDFIBALBMC00=\n')
a3 = ll('eJw10EtLw0AUBeAzTWLqo74bML8gSyFdiotm40rEZF+kRyVtCGKmqzar/nHvHBDmfty5c+fBrB2A\niUVuUVkMG4MOnIARGIMJeAKm4BQ8Bc9UsfwcvABn/5VL8Aq81tINeAveKb/Hd47R4WDDTp5j7hEm\nR4fsoS4yu+7Vh1e8yEYu5V7WciffZCl/5UpW8l162cuF3Mq1fJSUY5uYhTZFRvfZF+EvfOCnU89X\ngdATGFLjafBs+2e1fJShY4jDomvcH1q4K9U=\n')
a4 = ll('eJxLZmRgYIBhZyAuZgESKYwMwRpMQIZfCUhcRQNIMGiAmGB+DoQPIorZgYRNcUlKZp5dCQeInZOY\nm5SSaAdWDFIBALCJC04=\n')
a5 = ll('eJxNzTELwjAQBeCXS4r6TzKJP6DUgruLq0s1S7BKIRkqJP/dd3Hp8D4ex3H3NAA6xjEXJo2kAHeH\nalAF1aI6FINg8BIsZxTZdM5lM2/95i2PXCNBPBCvzeubLOR4yvp2bX6bS3P+LyppR/qUQ/wMea99\nnt6PMA26l/SKxQ/XGxky\n')
a6 = ll('eJwlzLsKwkAQheF/L0afw2qr4AOENOnT2NpEgyDGENgtFHbfPTNrcT6G4cw8DHCQeMkgiWchw81T\nDMVSHMWTDdnytGTHu+Ea9G4MAkHPkxXaS9L1t/qrbtXlX1TiUehiml9rn046L9PnPk+99qJ+cewN\nxxM9\n')
a7 = ll('eJwlzLEKwjAQxvF/rhF9jk6Zig8gXdy7uLq0FqFYRUiGFpJ39y4O34/j+O4eDjhovOaqia2S4e4p\njiKUhuLJjiw8hex5Cbdgd0NQCHaeROnOydZbda9+q+u/aMSjcolpXj59Otm8ju9pHnvrRfvS8AMM\nqhM6\n')
a8 = ll('eJxLZmRgYABhJiB2BuJiPiBRw8CQwsgglsLEkM3EEKzBDBTyy2QFkplAzKABJkCaSkBEjgZcsJgd\nSNgUl6Rk5tmVcIDYOYm5SSmJdmDFIBUAVDAM/Q==\n')
a9 = ll('eJxLZmRgYIBhZyAuZgESKYwMwRpMQIZfCUhcQQNIMGiAmGB+DoQPIorZgYRNcUlKZp5dCQeInZOY\nm5SSaAdWDFIBAK+VC0o=\n')
m0 = ll('eJw1jTELwjAUhC9Jq/0VzhldBAfr4u7i6mYpFFSKCXRJp/7x3rsi5L5Avnsvrx0AS8PcmNQSGSg8\nDsWjBJQKS42nxwzMQSog09b/gsrs9AGP6LjhHr3tMfSn7TpH+yebfYtJHGXH7eknTpGAkPbEJeVu\n+F5V/Bw1Wpl0B7cCYGsZOw==\n')
m1 = ll('eJw1zUEKAjEMBdCfdMQreIRuRwU3Mhv3bjzCDAOCitCAm7rqxU1+cZGX0v408wbAvy5e5eQYUAUm\nqAnNHdASvsJLhSVUBpryoPG6Km5ZfPaah/hBnXXf29jbsbdDjl0W2Tdd6IN+6JwdkLJ1zsWW+2vi\n/HOMRIklkJ38AF2QGOk=\n')
m2 = ll('eJxNjj8LAjEMxV96fz+Fk0NHdT5c3F1cD5c7BEHlsAWXdrov7kuKICS/0LyXpFMP4JcnZrgSEUgM\nQXJIDVKLtcHokAWZKvsVUm0eGjr1rC3GCplBW/03Xpy2hM5bj4sXnjh7p4cUz30pO6+fiKouxtn6\ny8MehcH4MU7GtydgCB0xhDjfX8ey8mAzrYqyka18AW5IIKw=\n')

def snake(w):
    r = i0()
    c = i1()
    f0(w)
    d = (0, 1)
    p = [
        (5, 5)]
    pl = 1
    s = 0
    l = None
    while None:
        (p, d, pl, l, s, w, c, r) = m2(p, d, pl, l, s, w, c, r)

i1().wrapper(snake)

Essentially, the first instruction after the imports loads into ll a function that dynamically marshals code into the executable by deserializing the provided base-64 strings. After a bit of analysis I found out that r and c are the random and curses modules respectively, f0(w) instantiates a Curses window, d and p are the direction and the covered cells of the snake, and m2 is basically the “step” function of the game which reads the arrow keys and updates the game grid.

To solve the challenge, I modified the script to automatically move the snake all across the map to progressively eat every target and reveal the entire flag.

def snake(w):
    r = random
    c = curses
    f0(w) # Curses window
    direction = (1, 0) # (x, y), positive = right, down
    cells = [(1, 1)] # snake, head = last
    pl = 1 # snake len
    s = 0 # "Ticks"
    l = None # Target

    W, H = 8, 8

    while 1:        
        d_x, d_y = direction
        p_x, p_y = cells[-1]

        if (d_x == 1 and p_x == W) or (d_x == -1 and p_x == 1):
            direction = (0, 1)
        elif d_x == 0:
            if p_x == 1:
                direction = (1, 0)
            elif p_x == W:
                direction = (-1, 0)

                cells = [cells[-1]]
                (cells, direction, pl, l, s, w, c, r) = m2(cells, direction, pl, l, s, w, c, r)
                sys.stdout.flush()

I launched the game and piped it to tee so that I could both observe the status and save the output to a file to later be processed:

python2 chall-2.py | tee out.txt

Filtering the relevant chars from the output gives the flag:

HTB{SuP3r_S3CRt_Sn4k3c0d3}

Categories: computer-science Tags: writeup ctf cyber-apocalypse hackathon solutions capture-the-flag

Copyright 2018-2023, Alessandro Sartori