Hello and welcome to the launch of the Owl Cyber Defense System Evaluation, Exploitation, and Research (SEER) Laboratory miniblog!
This is the very first in a line of forthcoming posts. We hope you find it educational and fun; we had a blast writing it!
**Please stay tuned as we will be releasing future blog posts in the future!**
## The Starting Line
Perusing the Common Vulnerabilities and Exposures (CVE) database (as one does), we stumbled across this gem: [CVE-2023-21093]:
|“In extractRelativePath of FileUtils.java, there is a possible way to access files in a directory belonging to other applications due to a path traversal error. […]”|
…hmm, the CVE record creation date was 03 November 2022.
The CVE was addressed in the April 2023 Android Security Bulletin. The bulletin yielded our starting point:
|CVE||References||Type||Severity||Updated [Android Open Source Project] Versions|
|CVE-2023-21093||A-228450832||EoP||High||11, 12, 12L, 13|
As this was patched months ago, we decided to make a public proof of concept (POC) for learning purposes.
Tell your family and friends – **keep your stuff up to date!**
## The Race
Following the Android bug Identifier, A-228450832, we noticed an added line in the diff for the MediaProvider module that invoked the private method getCanonicalPath in the function extractRelativePath:
The (newly) added definition of getCanonicalPath followed:
|A canonical pathname is both absolute and unique. The precise definition of canonical form is system-dependent. This method first converts this pathname to absolute form if necessary, as if by invoking the getAbsolutePath() method, and then maps it to its unique form in a system-dependent way. This typically involves removing redundant names such as “.” and “..” from the pathname, resolving symbolic links (on UNIX platforms)|
…So, if the method previously did *not* get the canonical path, then it permitted relative directories – i.e., .. and . – and symbolic links.
This was a source that we controlled, what was the sink?
|LiveOverflow did a phenomenal video on sources and sinks!|
The definition of extractRelativePath from the previous commit follows.
The method extractRelativePath accepted a string, data above, and generated a Matcher via the matcher method of the pattern PATTERN_RELATIVE_PATH – where the latter accepted the string as input. So the matcher method of the pattern PATTERN_RELATIVE_PATH was our sink.
|Note: PATTERN_RELATIVE_PATH did not change between commits.|
We needed to request access to a resource that:
- started with /storage/,
- either immediately continued with emulated/[0-9]/, where the inclusive group [0-9] occurred at least once and ended with ‘/’,
- or immediately continued with a non-/ and ended with a /.
Simply stated, we needed the resource to start with /storage/, contain literally any non-/ character, and end with a /.
Hmm, so perhaps something as simple as a request to access something like /storage/../data/data/?
Well, maybe… Utilizing the wonderful site pythex, we observed that our posited resource would match:
…but the method extractRelativePath did not utilize the entirety of its data string argument. Assuming a successful match on the find method of the generated Matcher, the method extractRelativePath located the index of the final / in the data string argument via the lastIndexOf method. If the data string argument did not contain a /, the index (lastSlash above) was set to -1. If the data string argument did contain a /, lastSlash became the index of the final / in the data string argument. If either lastSlash was -1 or if / was not the terminating character of the matcher, as specified by the condition lastSlash < matcher.end(), extractRelativePath returned /. Otherwise, extractRelativePath returned the substring of the data string argument from the first character after the match to lastSlash + 1.
|See here for the definition of the end method of a Matcher.|
Nested conditionals are so much fun to explain in prose! /s
Alright, so it seemed we could get extractRelativePath to return a path that arbitrarily traversed *as long as* it started with /storage/ and ended with a /.
We happened to have access to an unpatched Android 11 device, so we fired up Android Studio and created a ‘Basic Views Activity’ Android Application Package (APK) targeting API 30: Android 11.0.
Spoiler alert: Since Android 11 changed to enforcing scoped storage, we eventually shifted to targeting API 29: Android 10 and requesting legacy external storage.
In our build.gradle(:app), we included the following:
|Note the required annotation because Google Play requires that APKs target API 31+, as of this post.|
In our AndroidManifest.xml, via this, we included the following:
|We almost forgot to manually grant the READ_EXTERNAL_STORAGE permission to the APK in ‘Settings > Apps & Notifications > cve202321093poc > Permissions > Files and media.’ Since we installed the APK via Android Studio, this was a required step.|
We created a non-empty file /storage/emulated/0/seer.txt via an adb shell:
Note the permissions on /storage/emulated/0/seer.txt. It was octal 600 for u0_a126, but who was that?
An AID in the 10000-19999 range is an application user. A quick check on a nearby rooted phone revealed it was the Application Identifier (AID) for the Media module.
Back to work, though – we added the following to onClick in our Android Studio project:
When accessing on a vulnerable version of Android, MediaProvider seemed to throw an IOException:
…as noted here.
When accessing on a patched version of Android, MediaProvider seemed to throw either an IllegalStateException or a SecurityException:
…as noted here.
|For the unfamiliar, Java can catch multiple exceptions with a single catch.|
Interestingly, both paths were canonicalized… We must have not hit the desired code path – i.e., we did not trigger extractRelativePath. hmm…
## The Finish Line
We seemed to have hit a brick wall. So, back to the beginning. We re-read the Java File API:
|[…] resolving symbolic links|
We had tried a lot of alternatives with relative paths. So we shifted to symbolic links. We also wanted to guarantee we genuinely had file read access, so we modified our onClick to the following:
|Due credit to Anupam Chugh at DigitalOcean for the file-reading boilerplate.|
If we saw the file contents in logcat under the tag SEER inputText, we were golden. We ran the APK and…
Boo! SELinux in Android blocked us!
Unsurprisingly, the target device was rooted. So we disabled it:
We tried, again, and…
Who brought cake?!
Let us review what we accomplished.
Android sandboxes all applications using Linux User Identifiers (UIDs), commonly known as AIDs on Android. Let us review permissions on the two relevant files:
The POC APK symlink, /storage/emulated/0/Android/data/com.seer.cve202321093java/files/blah, was a mode 777 symbolic link with the POC APK as owner and app-private data directories on external storage as group.
The target file, /storage/emulated/0/seer.txt, had octal 600 permissions for the Media module AID.
Note how the AID of the APK (10142) did not share any groups with the media provider (10126). This is the key – we accessed a file owned by another AID in a directory owned by another AID.
So what did we do? We created a POC that exploited** CVE-2021-21093.
* …as long as you can publish an expired targetSdk to the Android Play Store
* …andddddd the target has SELinux in permissive mode
Is this groundbreaking? No.
But this article demonstrated the CVE POC process we employ:
- Find an attractive CVE
- Research, research, research!
- Play with the code until it does what you need
- Document all the things!
If you made it this far and find yourself or your company in need of deep technical, offensive requirements, reach out to us!
Thank you for reading! Remember to check back in as we plan to publish more posts!
/Owl Cyber Defense SEER Lab
P.S. Random fun fact: while developing this POC, we discovered the hidden API content://media/. Even more fun, we learned we were not the first!