Disassembled Java Bytecode
For this "exercise" I dove straight into the disassembled Java bytecode using apktool
. Apologies for the long code block, but take a read through and see if you can grok what is going on in the app's MainActivity
:
.line 23
invoke-virtual {p0}, Lcom/hacker101/level13/MainActivity;->getIntent()Landroid/content/Intent;
move-result-object v0
.line 24
invoke-virtual {v0}, Landroid/content/Intent;->getData()Landroid/net/Uri;
move-result-object v0
const-string v1, "http://34.74.105.127/5534a1d80f/appRoot"
const-string v2, ""
if-eqz v0, :cond_0
.line 28
invoke-virtual {v0}, Landroid/net/Uri;->toString()Ljava/lang/String;
move-result-object v0
const/16 v2, 0x1c
invoke-virtual {v0, v2}, Ljava/lang/String;->substring(I)Ljava/lang/String;
move-result-object v2
.line 29
new-instance v0, Ljava/lang/StringBuilder;
invoke-direct {v0}, Ljava/lang/StringBuilder;-><init>()V
invoke-virtual {v0, v1}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;
invoke-virtual {v0, v2}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;
invoke-virtual {v0}, Ljava/lang/StringBuilder;->toString()Ljava/lang/String;
move-result-object v1
:cond_0
const-string v0, "?"
.line 31
invoke-virtual {v1, v0}, Ljava/lang/String;->contains(Ljava/lang/CharSequence;)Z
move-result v0
if-nez v0, :cond_1
.line 32
new-instance v0, Ljava/lang/StringBuilder;
invoke-direct {v0}, Ljava/lang/StringBuilder;-><init>()V
invoke-virtual {v0, v1}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;
const-string v1, "?"
invoke-virtual {v0, v1}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;
invoke-virtual {v0}, Ljava/lang/StringBuilder;->toString()Ljava/lang/String;
move-result-object v1
:cond_1
:try_start_0
const-string v0, "SHA-256"
.line 34
invoke-static {v0}, Ljava/security/MessageDigest;->getInstance(Ljava/lang/String;)Ljava/security/MessageDigest;
move-result-object v0
const-string v3, "s00p3rs3cr3tk3y"
.line 35
sget-object v4, Ljava/nio/charset/StandardCharsets;->UTF_8:Ljava/nio/charset/Charset;
invoke-virtual {v3, v4}, Ljava/lang/String;->getBytes(Ljava/nio/charset/Charset;)[B
move-result-object v3
invoke-virtual {v0, v3}, Ljava/security/MessageDigest;->update([B)V
.line 36
sget-object v3, Ljava/nio/charset/StandardCharsets;->UTF_8:Ljava/nio/charset/Charset;
invoke-virtual {v2, v3}, Ljava/lang/String;->getBytes(Ljava/nio/charset/Charset;)[B
move-result-object v2
invoke-virtual {v0, v2}, Ljava/security/MessageDigest;->update([B)V
.line 37
invoke-virtual {v0}, Ljava/security/MessageDigest;->digest()[B
move-result-object v0
const-string v2, "%064x"
const/4 v3, 0x1
.line 38
new-array v4, v3, [Ljava/lang/Object;
const/4 v5, 0x0
new-instance v6, Ljava/math/BigInteger;
invoke-direct {v6, v3, v0}, Ljava/math/BigInteger;-><init>(I[B)V
aput-object v6, v4, v5
invoke-static {v2, v4}, Ljava/lang/String;->format(Ljava/lang/String;[Ljava/lang/Object;)Ljava/lang/String;
move-result-object v0
.line 39
new-instance v2, Ljava/lang/StringBuilder;
invoke-direct {v2}, Ljava/lang/StringBuilder;-><init>()V
invoke-virtual {v2, v1}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;
const-string v1, "&hash="
invoke-virtual {v2, v1}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;
invoke-virtual {v2, v0}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;
invoke-virtual {v2}, Ljava/lang/StringBuilder;->toString()Ljava/lang/String;
move-result-object v0
.line 40
invoke-virtual {p1, v0}, Landroid/webkit/WebView;->loadUrl(Ljava/lang/String;)V
:try_end_0
.catch Ljava/security/NoSuchAlgorithmException; {:try_start_0 .. :try_end_0} :catch_0
But First...
So the app is going to open http://34.74.105.127/919a907fa3/appRoot
in a WebView
(like an embedded browser). What happens if we just open the URL in our desktop browser? Ah... interesting.
<h1>Welcome to Level13</h1><a href="appRoot/flagBearer">Flag</a>
flag-bearer Noun (plural flag-bearers)
- One who carries a flag, especially at a ceremony.
- One who openly promotes an idea or value and becomes symbolic for it.
So what does the app's code do?
The tl;dr of that code is that it
- Takes the URI specified as the data value of the Intent which launched the app
- Removes the first 28 characters (which, by the way, is the length of
"http://level13.hacker101.com/"
) by callingsubstring(int beginIndex)
) - Appends the rest of the URI from the
Intent
to"http://34.74.105.127/919a907fa3/appRoot"
. - Calculates a message digest by using the
MessageDigest
class initialized to use the SHA-2561 algorithm. - The hash consists of calling update twice with two pieces of data.
update("s00p3rs3cr3tk3y"`) update("appRoot/flagBearer/") // the portion of the URL that follows `"http://level13.hacker101.com/"` from the Intent
- Ultimately that is appended as a query parameter
"?hash=<sha256 message digest>"
. - And finally tells the
WebView
to open the URL that's been constructed.That's why you can't just go to that flag bearer url directly because it will reject your request for a missing or incorrect hash parameter.
Why is http://level13.hacker101.com
relevant? Because if you look at the AndroidManifest.xml
you will see that you can launch the app (via an Intent
, hence the play on words in the challenge title) and supply your own url. This is the opening we need to cause the app to query the flagBearer
path we witnessed when browsing the base URL.
<intent-filter>
<action android:name="android.intent.action.VIEW"/>
<category android:name="android.intent.category.DEFAULT"/>
<category android:name="android.intent.category.BROWSABLE"/>
<data android:host="level13.hacker101.com" android:scheme="http"/>
</intent-filter>
The Exploit
$ adb shell am start -a android.intent.action.VIEW -d "http://level13.hacker101.com/flagBearer" com.hacker101.level13
Yields: http://34.74.105.127/919a907fa3/appRoot/flagBearer?&hash=8743a18df6861ced0b7d472b34278dc29abba81b3fa4cf836013426d6256bd5e
and that gets the flag!
All flags captured.