My name is Kyle Benack, you might know me as b3nack on Twitter, and I'm super stoked to be presenting my Android application exploitation talk here at DEF CON Red Team Village. Try saying that three times fast. I also want to thank Joseph and Omar for inviting me. I am a full-time security researcher, and in my spare time, I really enjoy testing Android applications. This talk isn't targeting any specific skill level, and I hope all skill levels find the information in this talk useful. Also, I'll be focusing on the application level vulnerabilities in the Android ecosystem. So, let's get the party started. Right off the bat, I just wanted to show all the exciting things we have in store today. Here's my xMyMap for Android attack vectors. We're going to be covering a little bit of each of these main subjects. Everything from exported components, webviews, deeplinks, third-party SDKs, third-party plugins, APIs, insecure cryptography, and insecure data and file storage. I'll be focusing more on deeplinks just because I feel like this is an area that isn't researched as much, and the possibilities for deeplinks are honestly endless because they can be chained with all kinds of other different vulnerabilities. They behave the same way as exported activities in a way where they take users directly to an activity, and they're honestly just a ton of fun, and I'm excited to share my research with you. Also, this xMyMap will be available after the talk. Alright. Everybody has a methodology, so I wanted to go over my Android methodology a little bit before we get to the fun stuff. First is the reconnaissance phase. During this phase, I'm going to try to detect which framework is in use, whether it's native, Flutter, or React Native. If there's any exported activities, if there's any custom deeplink schemas or regular deeplink schemas, if there's any third-party plugin integrations or SDK integrations, as well as API keys. So, this is pretty easily done with the AndroidManifest.xml because the AndroidManifest.xml is basically a blueprint for the entire application. And I'll show you what key phrases that I look for, key entries, in AndroidManifest.xml as far as figuring out what framework is in use. Next is the preparation phase. So, using Objection, you can bypass most native and React Native applications' SSL. Unfortunately, it doesn't work for Flutter because Flutter uses a different SSL library called BornSSL. I will also show you what I use for split APKs because every once in a while, you'll run into a split APK. Next is attack vectors. This is what I'm going to spend most of my time on. I have an example of WebViewXSS for native and Flutter, as well as some deeplink examples. There's all kinds of awesome deeplink shenanigans to be had in combinations with other vulnerabilities. Next is Unicode collisions. I haven't found any write-ups yet that show that Android apps are vulnerable to Unicode collisions, but they definitely are because Java methods and Kotlin methods both have Unicode mapping collisions. And I will show examples of that. Lastly, validation bypass via FlutterRoute. And that's just an interesting implementation error that can lead to random results. Now let's talk about the ecosystems. It's important to understand the ecosystem of the different frameworks just to kind of figure out where you're going to go next as far as exploiting the application or trying to find other bugs that are worth a bounty. Native apps, the code base will be mostly Java or Kotlin with shared object files if the NDK is in use. They're easier to decompile because there's a ton of tools out there that decompiles the code into readable format. It's easier to read and statically analyze after it's decompiled. And it imports AndroidX libraries. AndroidX libraries are the newer libraries for native applications currently. Flutter uses Dart. It's created with cross-platform compatibility in mind. So when you install Flutter, you're installing this huge tool set where you code it once and you can deploy it basically to any platform. Production binaries are not easy to reverse engineer because it's compiled to a .so file. The debug APKs create a bin file which is easy to find the source code. It's as easy as a grep command and then you get the Dart source back. The APKs tend to be bulkier unless you optimize the deployment to specific architectures. For React Native, these applications are mostly written in JavaScript then rendered with native activities. And a lot of the plugins are under the com directory. Moving on to detecting native Flutter and React applications. For native, usually there's a lot of AndroidManifest entries and most of the activities are easy to find in the com directory. I do this a lot with JADX where I'll find a path in the manifest and just trace it to the decompiled source code. For Flutter, Flutter has flutter.embedding entry in the AndroidManifest. There's also a folder called io.flutter in Flutter applications. There will be shared object files in the lib directory. In the release, there's libflutter.so and libapp.so. If it's a debug Flutter application, you'll find in the Flutter assets directory kernelblob.bin. And this is the file where you can grep for the Dart source code. But it's not very likely you'll find one of these in production. For React Native, the entry com.facebook.react in AndroidManifest.xml is a key indicator that React Native is in use, as well as a dev settings activity. In the assets folder, you'll find an index.android.bundle file most of the time. And this is all the JavaScript code that hooks into the native activities. For the NDK and JNI, for the apps that use these, there's a shared object created in the lib directory. You will find JNI calls in those shared object files, and then that will for sure tell you that the NDK is in use. And you can decompile these shared object files with Ghidra or Dari2 and stuff like that. Using Objection for SSL pinning bypasses. So I use this script to automate the process of pulling the APK off of my device to my computer, renaming it, and opening JADX on the fly for a quick static source code analysis. Then, after I have the APK in that same directory, I'll use Objection to patch the APK. And what Objection does is it creates another APK with an agent loaded. And it tries to inject that agent into a launcher activity. So when you launch that patched APK, it'll pause execution. And when you use Objection Connect, you'll connect to that gadget. And from there, you can bypass the SSL pinning. If it's a split APK, then you can use patch-apk.py to patch the split APK and then save it as whatever name you want to. And that Python script actually runs on top of Objection. So if you have Objection installed, you're good to go. And this works on React Native and native apps. Demo time! So, what I have here is an app that I own. And I'm going to show you how to do this for the most part on the app I own, just so I don't make anybody angry. So, what I'll do is... So, what I have is that script is aliased to auto-apk. And then all I have to do is put in the name of the app. And then from here, it'll do the rest of the work for me. Pulls it, puts it in the directory I'm in. And then all I have to do is use my other aliased command to patch it. So, this will install the gadget version I specified. While it's doing that, I can uninstall this version of the app. Because I have it automated to the point where it'll install the patched version on my device. And you can customize this Objection patch apk command however you like. Because sometimes the process will work without skip resources and skip native libs. And sometimes there's entries in Android Manifest that throw off the process and cause errors and stuff. So, it all depends on the application. Looks like it's done loading the agent... I mean the free-to-gadget. It re-signs the apk and puts it back in apk format and installs the apk on the device. So, since this apk doesn't have an SSL component to it, it won't freeze on startup. But production apps will stop execution because it will target the launcher activity. Bypassing Flutter SSL pinning. So, this is an entirely different beast in itself. So, Flutter uses Dart programming. I should say the Dart programming language which has it's own keystore. And the SSL libraries are completely different. It uses boring SSL to handle all of the SSL related functions. There's two main ways to bypass SSL pinning locally. And that's to find the SSL verification function in libflutter.so and change the response to true with a free-to-script. If the plugin is used, then it's way easier to hook a plugin that is using SSL pinning than it is to decompile libflutter.so. Find the offset to this exact method. Trust me, I was trying to find the offset for a specific architecture and it wasn't super easy. And I was having problems with compatibility with VMs. So, I'm going to show the second plugin bypass method. And later on, maybe in a YouTube video, I'll show the first one. And I want to say thank you to Joe and Beckers for creating the free-to-script and finding the Java method being used for SSL pinning in this particular plugin. So, running this command with the free-to-script waits for the method to execute. And once it executes, it changes the response in route to true. And I will show you exactly what I mean by that. So, I have this exercise programmed into Android that has the SSL pinning plugin. And so, if we use... So, if we use this command... This is actually... This app... Loading the script... This will restart the app, I believe. Because it injects the thread. So, once the app is restarted, you'll see... It doesn't matter what I put in for these, but just for demo purposes, it intercepts the pinning request every single time. And just to show you what happens without this... I'll exit out real quick. And I'll go back to the activity. So, without running the script, it actually does what it's supposed to do. So, if I put that in there, whatever fingerprint... It'll fail with the java exception. And there's that. Technical details of the Freedia script. So, this is, from my understanding, the Freedia API calls in use are java.use and java.perform. And java.use wraps a library in an object that can be used to modify calls to that library during runtime and return what is specified in the function. Java.perform ensures the call is attached to the current running thread for the function to disable pinning. And I believe that's why it restarts the application when you load the script. Third-party SDK and API exploitation. I can't emphasize enough the importance of reviewing third-party documentation to discover potential bugs. So, demo apps make great POCs, and almost all third-party services have demo apps. There's been numerous times where I'll be looking into a third-party integration and there's a demo app that shows how to quickly access the API. But it doesn't check if it's an official app, if it's the official production app that's accessing it. So, it's basically just like a doorway into the production app. And it's even better if you can disclose sensitive data via some API call after you've SSL pinned the application. Mobile endpoints sometimes have separate APIs, like completely different ones that aren't designed the same exact way. And there could be different server headers, there could be calls without CSRF tokens. There could be calls with just one authentication header for all the calls. And sometimes you can even find the token in the Android app and use it to potentially elevate privileges and disclose more data. So there could be a default one that's just set to the user's account, but then you can find another token that you can use in strings.xml or something like that. And lastly, I found this in an engagement where S3 bucket keys were in strings.xml. And I could upload and read the data in that bucket. I could see what files there were, so there's stack traces, all kinds of different files in that S3 bucket. And all I needed to do was add the keys in the bucket name to AWS Clive via the command line. So there's all kinds of stuff that you can find as far as API exploitation and third-party SDK. And there are some third-party integrations that you can actually overwrite certain parameters and perform link poisoning, which leads to spear texting, I like to call it. Because I've literally been able to change the links in text messages to go wherever I wanted and not where, but it was under that company's name. External file storage. So external file storage is essentially public on that device and any app can access external file storage. Also, it's important to note that when you delete an app and that app saves data to external file storage, it doesn't disappear off that phone. So a third-party app could scrape data from external file storage, literally these directories, and upload them to wherever they want, hypothetically. But I would like to note that external storage behavior changes in API 29+, because they scope it per user and per app. And shared storage has to be specified in order for files and data to be truly public. So it's a lot more stricter in the newer APIs. And these environment.getExternalStorage calls can easily be automated into a recon script. Native WebView XSS. So WebView XSS can be chained with deep links. So this can turn a quote-unquote self-XSS into a reflected XSS. The identifiers for vulnerable WebViews are similar between native and Flutter apps. By default, native and Flutter apps aren't susceptible to XSS unless JavaScript is enabled. So, for example, I have examples here for Java, Kotlin, and Flutter. They all look pretty similar, and as you can see, JavaScript is enabled, and the other main part is that user input isn't escaped. Flutter WebView XSS via plugin. The official Flutter WebView plugin doesn't support alerting functions, but the development one does. And that's the interesting part, is that the plugin ecosystem kind of introduces vulnerabilities in a similar way that a WordPress environment does. And the way the values are handled is slightly different because the Dart programming language is used. So, firstly, the form value is transferred to a WebView with a navigator push material page route. Secondly, the WebView where the value is going has with JavaScript true set. And lastly, the user input value isn't escaped, and it's executed with eval JavaScript, which is built into the development plugin. And now it's demo time. So, I have an exercise for this, too. Let's just say this application doesn't filter or escape what the user puts in as a username. And I almost put that backwards. So, input whatever we want for the password. After the page loads, let's go to the profile. And bada-bing, bada-boom. We have stored XSS. So, you can see here, if you see eval JavaScript in a Flutter app, as well as JavaScript set to true, it's definitely vulnerable if the development plugin is in use. Native deep links. Deep links are specified in the Android manifest, and they can have a custom or web schema. Also, you can tell which activities have deep links by the intent filters. In the example below, there's a scheme of flag 13 and a host of RCE assigned to RCE activity. What this means is that a deep link will send a user directly to RCE activity as long as those conditions are met. And I'll show you exactly what I mean by that. So, if I use this adb shell example test command on Android, it'll send me directly to that activity. And that's the exact definition of a deep link, is it sends a user directly to a certain part of an Android application. Flutter deep links. So, Flutter deep links are implemented via the plugin unilinks, and they are handled pretty much the same way as native applications, where the plugin intercepts the deep link and then routes the user to wherever the deep link router for the application wants. However it's implemented, it'll send the user to that specific area. The Android manifest also has specific parts of that application that's assigned to the deep link. And the main thing is that they just look a lot different. The implementations just look a lot different because of the dark programming language and the async calls. Native deep link to reflected XSS. This is where the fun starts. So, we're going to use a deep link and a vulnerable web view to basically create reflected XSS in a mobile application. So, there's a couple preconditions that we need to have. It's a custom schema, so that way we know it's going to the mobile app. And the web view needs to have JavaScript enabled. And the example below shows that JavaScript is enabled for the web view. And as long as the intent doesn't equal null, it's going to load the data from the query parameter totally secure and load it into that web view. And it's demo time. So, the way you make deep link POCs is with HTML files. So, I'll just show you that it's just the same exact thing as a hyperlink. So, deep links are hyperlinks for mobile apps, basically, that take users directly to a specific part of an application. And then, I'm going to push it to my device with adb push the deep link file name. And for simplicity, just sdcard download. So, now if I go to downloads, and open the file, you can use any browser you want. It'll load the HTML file, and I'm just going to zoom in, click on this, it'll load in the web view, and it'll execute the JavaScript. And this is the vulnerable web view I used. This will also be a part of one of my other examples. Native deep link to LFI. Depending on how the deep link is implemented, it's definitely possible to include files on the device that aren't meant to be included. I have actually found a deep link LFI and was awarded for it, so that inspired me to create a scenario where this is actually possible. It's more likely that production apps will use a file provider or a PDF reader to do this, but still, for POC purposes, this is pretty fun. As you can see from the code example below, the intent is sending a parameter called totally secure LFI, which is what we want to intercept and what we want to take advantage of to read files from the assets directory. So, it's going to read whatever file we put as that parameter, totally secure LFI, and log it to the text view. It's demo time. So I have a totally separate POC for the LFI that I will show everybody real fast. As you can see, it's the same deal as a hyperlink, except for it has the custom schema of XSS, the host of whatever, and the parameter of totally secure LFI, which equals the file we want to load into the web view. We'll push this file once again to our device, and to SD card download. Now we'll open the deep link. Actually, let's close this and go back to the files, open it in the browser, and it will load the contents in the web view. More fun with deep links. So I was curious if I could get remote code execution or command execution in general with a custom Go binary. Turns out, I was able to get code execution and remote code execution via deep link with this specific scenario. So, first I establish the process, and I get the run time of the custom Go binary, which is in the files directory of the application. The intent param is the Go binary name, and the binary param is the command that the binary has programmed as an option. Next, I make sure that I define a buffered reader to read the output for the command. And also a string builder, so I can put the command output into the string builder, and then the buffer reader for each line will use the string builder to append the output to it. Well, until the process finishes with processWaitFor, then once the process finishes, the string builder, also the log variable, will draw or print all the strings to the text view, which is defined as TV. First, I implemented this in Java, and then I figured out how to do it in Kotlin. The entire process was a lot of fun, and I'm really excited to share it. Without further ado, it's demo time. So, first, I thought it would be really cool just to see what the output should be. So, let me exit out of this. You can connect to a VM and phones via ADB once USB debugging is enabled, and establish a shell. I'll show you how to do this. Using ADB shell, we can navigate to this application's files directory, where the binaries are. And then, the binaries that execute the commands are narnia.x86-64, because that's the architecture for this VM. So, if I want to execute this binary, as long as I have permissions to execute it, I can just do that, and it says a parameter is needed. I remember programming a help parameter, so the commands available are test1, rick, test2, potato, and test3. So, let's try test1. It's treasure. Okay, cool. Test2. It's underscore. Test3. So, treasure planet. And, just for fun, let's see what rick is. So, rick is definitely the rip roll YouTube video. Because, you know, you have to have some fun. While programming CTF exercises. So, now that we know what the expected output is, let's try to execute commands via deep links. Clear the terminal, and I will show you my POC. HTML file. So, as you can see, this particular activity has the schema flag13, a host of RCE, the parameters binary, and param test1. So, the parameter called param is actually the binary command. And, so, how to be able to make a deep link POC, I will show you exactly where to look in the application itself. So, if I open Android Manifest for Android, right here. So, the RCE activity is the one that we are looking for. So, this is how, if you were looking to make deep link POCs, you would look for activities that have this intent filter. And, then you can find out what the scheme is, and what the host is. Now that I know what the scheme is and the host is, it's very valuable to see also what the activity has, because that could also disclose the params involved. And it could disclose what params you can test out. So, if you do shift-shift in Android Studio, you can go right to the activity. So, that's what I'm going to do. So, the params that were used for this were binary and param. So, sometimes you can find specific parameters that the activity is looking for and use those to your advantage and find bugs. Now, back to the demo. So, we just have a normal blank RCE activity here. We need to push this POC to the device itself. And we'll push it to sdcarddownload, just for simplicity. Now, what we want to find is the RCE POC.html. You can open it with whatever browser you want. Zoom in. And with Android Studio VMs, it's CTRL and then dragging the mouse. So, let's try the test1 command variable. And that's awesome, but it output exactly the same as the terminal. Test2, underscore, test3, plant. And this should solve the flag for the exercise. And also redirect the user. So, needless to say, looking for intent params in the source code can disclose ways you can navigate to other parts of the navigation. Or ways you can find bugs in sensitive data. So, as an example, when the application is getting parameters, that's prime for you should append those to your deeplink POCs. Just like this. Now for Unicode collisions. This is kind of uncharted territory as far as Android applications go. So, Java and Kotlin both have Unicode mapping collisions. It just depends on how the application is using Unicode. This is prevalent in email. So, password reset functions, for example. It could exist in a database that converts Unicode to uppercase or lowercase characters. This script right here just shows a simple way that I was able to confirm that Unicode collisions did exist in Java. As you can see here, first I just tried to string to lowercase and uppercase the Unicode B character. And if it equaled double S as to uppercase, it printed true. And then, if the next one, I tried the email from the writeup that I was reading, which is included in my resources, to uppercase, equaled what the POC was in that writeup, it was true, which it did. Needless to say, Unicode collisions do exist in Android applications that use Java or Kotlin. It just depends on the implementation. I also included a Unicode collision example in Kotlin just because I don't think there's very many examples out there currently. And this is actually one of the examples in Android as well. So, this code is taking the post string value and converting it to all uppercase characters. Databases sometimes do uppercase or two lowercase values to keep data consistent. So, this is a very realistic test case that is worth testing. Or, I guess, worth putting in your workflow for every application. And it's important to note that Unicode collisions can affect mobile-to-web interactions. Especially if the mobile app is connected to a database that I haven't tried out a Unicode collision yet with Firebase. But definitely with certain types of databases, Unicode collisions could affect them. And there was one instance where I saved my username as a Unicode character and I wasn't even able to run the Android app after that. And the only way I could get access back to that specific account was to change my username from a Unicode character back to a regular character. So, that's also important to remember that it could happen for production apps. Authentication bypasses via insecure Flutter material page route. So, navigator.push material page routes behave similarly to deep links except, if not implemented correctly, could bypass application logic and validations. The example below is an example of a correct implementation. You want to put these navigator.push routes after all of the form inputs. If you don't, I will show you what exactly happens. It's kind of surprising. So, this is an example of an insecure material page route. And implementations that are like this can possibly have unexpected behavior and could leak sensitive data. For example, if this was like a login form connected to a database and, for whatever reason, only the username was needed to link to the data for the user and it didn't check the password and pass validations because the navigator.push wasn't after the form inputs, then that could be all bad. And it's demo time. So, I have an example in here of an insecure navigator page route, which could potentially be an off-bypass. So, you'll notice if I push signup, it says please enter your password and username. If I just put password, it says please enter a username. If I just put a username, it takes me... It automatically brings me to the next page and doesn't really bother checking to make sure that my password was correct. So, it's just another weird scenario, or I guess I should say odd scenario, where an implementation of a material page route located in the wrong spot could bypass validation. And this is all because of the material page route being after the username value that was entered. So, I'll show you what exactly I mean. The material page route for this scenario is only validating that the username is not empty. And if it's not empty, then the page route... these navigator push, basically, they're just like, alright, cool. Everything before this being executed is good, so I'm going to send you to the next page, which is the home page. And the validation ends down here. So, ideally, the navigator page route should be after all of the validation takes place. Easily bypassable and broken encryption. So, plugins for Cordova and Flutter are more susceptible to this. Any CBC cipher with a static initialization vector and hardcoded key. Hardcoded XORs or XOR bitwise operators can be found in assembly, so it's important to note that you potentially could find keys that are hardcoded in shared object files. It's going to be a little harder to identify, though, depending on the implementation, but decompiling those shared objects with Ghidra or Redari 2 will definitely show you any strings. So, these two code examples are examples I've found in a Cordova app. Or, not a Cordova app, React Native. And I believe that they use a Cordova plugin. And, so, in my opinion, there's really no point to use a plugin that encrypts data and decrypts it at runtime if you have a hardcoded encryption key. Because once you find the hardcoded encryption key, it's basically... it's just all the protection from encrypting your data is gone. So, when I was testing this application and I found this, I was able to decrypt the data pretty easily. Luckily for them, there was nothing sensitive, which also made no sense to me. I wasn't sure why they would encrypt data that wasn't sensitive. But, yeah, if you find a plugin that uses a hardcoded key and doesn't change it upon every use and a hardcoded initialization vector, there's really no point. Because somebody can decompile the app and then use that key to decrypt all the data. And I'll show you the script I used to do that on the next slide. So, like I said before, static code analysis may reveal the encryption keys. Then you can leverage them to decrypt potentially sensitive data with a Python script just to make it easier. Because it's easier than making a proof-of-concept app. Because in this situation, yeah, for encryption, making a proof-of-concept app would take a lot longer. Because you really just need to decrypt the data that's encrypted. You can also use a language of your choice to decrypt this, but this example is in Python and it was pretty easy to write up. Reversing an XOR example. There's only two steps in the process of reversing an XOR encryption. One, the XOR key. Two, applying the XOR key again to get the original data. So, once you find an XOR key, or if you think you have an XOR key, just take the encrypted data and try applying the script to it. And you'll know pretty quickly if it was XOR encryption being used. It's also good to note that to keep a lookout for any byte arrays, because byte arrays can be converted back to plain text pretty easily as well. And I have an example of that in this script. Because in one of the exercises for Injured Android, there's a byte array that is supposed to be a clue to how to solve the actual flag. So, I'll show you what I mean by that. So, this is kind of a bonus demo time. But, in this scenario, there's a byte array, which can easily be converted back to plain text. I'll show you how to do that right now. So, if we run the Python script that's in the slides, it gives back the encrypted string, the decrypted, or the byte array, which is here. So, when you apply this XOR to when, it turns into this. And when you apply the XOR to this, it turns into when. I'll go over some further technical details of how I implemented this activity. After installing the Native Development Kit, you can create shared object files or native libraries with C++ and C. And these can be used for a number of reasons. I mean, you could actually probably just program the entire Android application with JNI and the Native Development Kit. It all depends on what your language or preference is. Anywho, every JNI call for the shared object file will look something like that. It'll have these attributes declared. One interesting fact is that there's still a bug with this specific function, newStringUTF, where if it returns a Unicode character above a certain number, it will crash the entire app. Now I'll show you how the shared object file is initialized and used. So in the assembly activity, which is programmed in Kotlin, this is where I have a companion object that initializes the native library by using System.LoadLibrary. In order to use the function, I had to declare this function as external and it has the same name as what's in the native lib. This is important to note because while you're looking through assembly, you can track down the actual call faster if you know the method or the function being used. Now that we're looking through this, you can see that XORString is the result of this method. And that's one way we know that method was intended just to return a specific value or specific data. And then XORString is being converted to UTF-8. The bytes are being stored to a byte array so we can print it to the text view with bytes.content to string. And that's why you see the byte array on that specific exercise, or CTF exercise. Thank you everyone for listening to my talk. I am super grateful to have been given the opportunity to present here at DefCon Red Team Village. I highly recommend you check out the OWASP Mobile Security Testing Guide if you want to get into mobile security. And I will be sticking around for questions and answers in the Discord channel. Also feel free to message me on Discord or Twitter whenever, and I will try to answer any questions you may have. Thanks again, until next time.