Write-Up 9 Août 2022

HTB RouterSpace - Décompiler et analyser un APK

Image de présentation pour

Pour ce premier write-up d’une machine HackTheBox, je vous propose de revenir sur une machine facile : RouterSpace. Ce n’est pas un write-up complet mais une présentation d’une technique d’analyse statique de l’application Android.

Décompiler l’APK avec JADX

Dans sa vidéo de write-up, IppSec présente les étapes pour obtenir un APK et ensuite l’analyser. Comme il le dit au début de la vidéo, il passe une bonne partie de la vidéo à installer une VM Android pour tester dynamiquement l’APK. Cette étape est souvent fastidieuse mais Genymotion permet souvent de la simplifier. Ici, je vous propose de faire une analyse statique, c’est à dire en regardant uniquement le code de l’application.

Pour commencer nous allons utiliser JADX, un décompilateur de DEX (code source d’un APK) en Java :

Décompilation de l’APK avec JADX
Décompilation de l’APK avec JADX

On voit que l’application utilise le framework de Facebook ReactNative. Nous sommes donc à la recherche de code Javascript et non pas de code Java/Kotlin.

ReactNative utilise du code Javascript
ReactNative utilise du code Javascript

Ce dernier se trouve dans le fichier /Resources/assets/index.android.bundle. Avant cette machine, je ne connaissais pas le nom de ce fichier mais je savais que je devais trouver du code Javascript dans les resources de l’APK, j’ai donc regarder les différentes ressources afin de trouver un fichier qui semble contenir du code dans ce langage.

Code Javascript utilisé par ReactNative
Code Javascript utilisé par ReactNative

Analyser du code Javascript minimifié

Le code Javascript ne correspond pas au code qui a été écrit ; il est minimifié. Ceci veut dire que les différents noms (variables, fonctions, …) ne sont pas ceux utilisés par les développeurs mais réduit à quelques lettres pour réduire la taille du code.

Si on cherche une référence à hacktheBox, htb ou encore routerspace dans le code on ne trouve rien. Cela peut surprendre, une code minimifié ne change pas la valeur des chaines de caractères mais dans notre cas il y a eu en complément une fonction d’obfuscation. Néanmoins, si on cherche d’autres termes, on tombe rapidement sur la ligne 546 qui contient la chaine de caractères suivantes : 'http://rou'. Elle semble intéressante !

Recherche du mot “http” dans le bundle
Recherche du mot “http” dans le bundle

Nous pouvons ré-indenter le code afin de la rendre plus lisible avec un outils comme https://beautifier.io/. On ne veut pas forcément comprendre 100% de ce que fait le code mais simplement récupérer les strings décodées. Je vous met à disposition le code dé-minimifié ici afin d’avoir les mêmes lignes que moi.

On peut voir qu’entre les lignes de 92 à 94 contiennent des mots clefs d’une requête HTTP :

Mots clefs d’une requête HTTP
Mots clefs d’une requête HTTP

Les valeurs pour les en-têtes User-Agent et Content-Type sont encodées avec une table de référence dans l’objet _0x1f5205. Ici s’arrête la partie pure-statique, nous allons maintenant utilisé directement le code pour récupérer les valeurs.

Si on remonte plus haut, on note que _0x1f5205 a besoin de _0x32f76d :

Erreur lors de la création de l’objet _0x1f5205
Erreur lors de la création de l’objet _0x1f5205

La variable _0x32f76d correspond à une autre variable. Si on continue à remonter les dépendences et renommages, on obtient le chemin suivant :

Dépendance entre les objets Javascript
Dépendance entre les objets Javascript

Le block suivant est exécuté lors du chargement du script, nous allons aussi l’ajouter :

(function(_0x506418, _0x1f605c) {
    ...
    while (!![]) {
        ...
    }
}(_0x31d2, -0x90a19 + 0x9b062 * -0x1 + -0xb688 * -0x26));

On copie alors uniquement les éléments qui nous intéressent et on obtient un script plus simple. Par exemple, le mien est le suivant :

function _0x3f1b(_0x32ad91, _0x55c7ed) {
    var _0x561355 = _0x31d2();
    return _0x3f1b = function(_0x4ec447, _0x28fd76) {
        _0x4ec447 = _0x4ec447 - (0x3d * -0x9e + 0x1 * -0xaad + 0x30ee * 0x1);
        var _0x41f779 = _0x561355[_0x4ec447];
        return _0x41f779;
    }, _0x3f1b(_0x32ad91, _0x55c7ed);
}
var _0x47a8f2 = _0x3f1b;
(function(_0x506418, _0x1f605c) {
    var _0x30a08d = _0x3f1b,
        _0x55f539 = _0x506418();
    while (!![]) {
        try {
            var _0xf72415 = -parseInt(_0x30a08d(0xd0)) / (0x135d * 0x1 + 0x56d + -0x4f5 * 0x5) + parseInt(_0x30a08d(0xe4)) / (-0x1 * -0x1db7 + -0x1 * 0x1ab7 + -0x2fe * 0x1) * (-parseInt(_0x30a08d(0xae)) / (0x1ab * -0xd + 0x1b35 + -0x583)) + -parseInt(_0x30a08d(0xef)) / (-0x209f * -0x1 + 0x23b * 0x1 + -0xd * 0x2ae) * (parseInt(_0x30a08d(0xbb)) / (-0x96a + 0x74a * 0x1 + 0x1 * 0x225)) + parseInt(_0x30a08d(0xe5)) / (-0xcfb + 0x3 * -0x29 + -0x35f * -0x4) * (parseInt(_0x30a08d(0xa0)) / (0x8c6 + -0xf7 * 0x1d + 0x133c)) + -parseInt(_0x30a08d(0xd9)) / (0x19ba + -0x1521 + 0x491 * -0x1) + -parseInt(_0x30a08d(0xe6)) / (0x68f * 0x2 + 0x1d96 + 0x1 * -0x2aab) + -parseInt(_0x30a08d(0xfa)) / (-0x1364 + 0x21c7 * -0x1 + 0x101 * 0x35) * (-parseInt(_0x30a08d(0xf6)) / (0x15 * -0xc1 + 0x1f * -0xe8 + -0x218 * -0x15));
            if (_0xf72415 === _0x1f605c) break;
            else _0x55f539['push'](_0x55f539['shift']());
        } catch (_0x91a71d) {
            _0x55f539['push'](_0x55f539['shift']());
        }
    }
}(_0x31d2, -0x90a19 + 0x9b062 * -0x1 + -0xb688 * -0x26));

function _0x31d2() {
    var _0x379495 = ['EwCVL', 'ugPGw', 'Router\x20is\x20', '-Bold', 'data', '30158095HXLvSs', 'post', 'eAgent', 'http://rou', '10BrHGoD', 'gray', '80%', 'applicatio', 'white', 'ck\x20your\x20in', 'ternet\x20con', 'tb/api/v4/', 'Please\x20pro', 'Image', 'XvhFJ', '2111347AIyazK', 'v/check/de', 'vide\x20an\x20IP', 'working\x20fi', 'DKyDg', 'YnNsf', 'tzoEq', 'EKNxl', 'the\x20server', 'log', 'ne!.', 'NunitoSans', 'OgZoU', 'TouchableO', '32457sfggQZ', 'nection.', '[\x20RESPOND\x20', 'center', 'createElem', '__esModule', 'per', 'mGNnc', 'then', 'catch', 'contain', 'uAiCt', 'bottom', '42740dmWhFN', 'Text', 'ButtonWrap', 'OLDvc', 'Sorry\x20!', 'terspace.h', 'n/json', 'StyleSheet', '/router/de', 'darkgray', 'JHvFI', 'transparen', 'UWIVj', 'Please\x20che', 'SZqEq', 'default', 'HrHYj', 'Hey\x20!', 'monitoring', 'StatusBar', 'error', '1013605BwxVJG', '[\x20DEBUG\x20]\x20', 'defineProp', 'gUnlE', 'Unable\x20to\x20', '25%', 'pacity', 'ButtonText', 'gKQYs', '1006000MsdmAT', 'handleSubm', 'PpdRl', 'shxxV', 'ent', 'View', 'erty', 'show', 'Formik', 'Check\x20Stat', '0.0.0.0', '128BJBUSC', '6BAxhAU', '4584186MTHGwP', 'connet\x20to\x20', 'vESlr', 'GHjuW', '\x20Address.', 'container', 'create', 'RouterSpac', 'viceAccess', '72dIvHGU', 'info'];
    _0x31d2 = function() {
        return _0x379495;
    };
    return _0x31d2();
}

var _0x32f76d = _0x47a8f2,
    _0x1f5205 = {
        'gUnlE': _0x32f76d(0xf0),
        'uAiCt': _0x32f76d(0xcc),
        'PpdRl': _0x32f76d(0xf3) + _0x32f76d(0xa3) + _0x32f76d(0xaa),
        'JHvFI': _0x32f76d(0xd1) + _0x32f76d(0xf3) + _0x32f76d(0xa3) + _0x32f76d(0xaa),
        'vESlr': function(_0x4f848e, _0xcfdf5c) {
            return _0x4f848e + _0xcfdf5c;
        },
        'SZqEq': _0x32f76d(0xb0) + ']\x20',
        'EKNxl': _0x32f76d(0xcf),
        'DKyDg': _0x32f76d(0xd4) + _0x32f76d(0xe7) + _0x32f76d(0xa8) + '\x20!',
        'XvhFJ': _0x32f76d(0xc8) + _0x32f76d(0xff) + _0x32f76d(0x9b) + _0x32f76d(0xaf),
        'shxxV': _0x32f76d(0xd1) + _0x32f76d(0xc8) + _0x32f76d(0xff) + _0x32f76d(0x9b) + _0x32f76d(0xaf),
        'OgZoU': function(_0x38d154, _0x48711d) {
            return _0x38d154 == _0x48711d;
        },
        'mGNnc': _0x32f76d(0xbf),
        'HrHYj': _0x32f76d(0x9d) + _0x32f76d(0xa2) + _0x32f76d(0xea),
        'tzoEq': _0x32f76d(0xd1) + _0x32f76d(0x9d) + _0x32f76d(0xa2) + _0x32f76d(0xea),
        'EwCVL': _0x32f76d(0xf9) + _0x32f76d(0xc0) + _0x32f76d(0x9c) + _0x32f76d(0xcd) + _0x32f76d(0xc3) + _0x32f76d(0xa1) + _0x32f76d(0xee),
        'ugPGw': _0x32f76d(0xed) + _0x32f76d(0xf8),
        'UWIVj': _0x32f76d(0xfd) + _0x32f76d(0xc1),
        'OLDvc': _0x32f76d(0xc6) + 't',
        'gKQYs': _0x32f76d(0xe2) + 'us',
        'YnNsf': _0x32f76d(0xba),
        'GHjuW': _0x32f76d(0xe3)
    };
var _0x36a162 = _0x32f76d;

Je ne vais pas chercher à comprendre le principe des différentes fonctions. Mon objectif ici est de récupérer les valeurs en clair des différentes chaines de caractères.

Tester avec une console Javascript

Nous avons pas forcément NodeJS d’installé sur notre poste mais nous avons à tous les coups un navigateur. Personnellement j’utilise Firefox, j’ouvre donc la console de debug pour exécuter directement du code Javascript ! Maintenant, si on tente de récupérer les valeurs des chaines de caractères suivantes on obtient :

Console Firefox pour exécuter le script
Console Firefox pour exécuter le script

Cela semble cohérent, on remonte un peu dans le code qui ressemble de plus en plus à un appel qui ressemble à la fonction d’appel HTTP fetch :

o[_0x36a162(0xca)][_0x36a162(0xf7)](_0x1f5205[_0x36a162(0xf1)], _0x39f1a7, {
    'headers': {
        'User-Agent': 'RouterSpaceAgent',  // _0x1f5205[_0x36a162(0xf2)],
        'Content-Type': 'application/json' // _0x1f5205[_0x36a162(0xc7)]
    }
})

L’URL finale sera retournée par _0x1f5205[_0x36a162(0xf1), ce qui correspond à :

URL décodée
URL décodée

De la même manière, le code décodé devient :

o["default"]["post"]("http://routerspace.htb/api/v4/monitoring/router/dev/check/deviceAccess", _0x39f1a7, {
    'headers': {
        'User-Agent': 'RouterSpaceAgent',
        'Content-Type': 'application/json'
    }
})

Au final, ce n’est pas un appel à fetch mais la libraierie Axios qui est utilisé ici. Le deuxième paramètre, _0x39f1a7, contient un objet JSON qui semble avoir une clef ip et une valeur passée par l’utilisateur :

'onSubmit': function(_0x2c3ea5) { // appel de la fonction avec une entrée utilisateur
    ...
    ... (', _0x2c3ea5['ip']) ?   // test sur la présence de la clef `ip`
    ... _0x39f1a7 = _0x2c3ea5,    // positionnement de l'objet dans une nouvelle variable

Il ne reste plus qu’à reconstruire la requête HTTP complète pour faire un test :

import requests

r = requests.post(
    'http://routerspace.htb//api/v4/monitoring/router/dev/check/deviceAccess',
    headers={'User-Agent': 'RouterSpaceAgent'},
    json={'ip': '10.10.10.10'}
)

Je ne vais pas entrer dans le détail de la suite de la machine, elle est très bien présentée dans d’autres write-up:

  • Injection de commande dans la valeur du champ ip (pensez à toujours encoder en base64 votre payload)
  • Passage en root via la vulnérabilité sur sudo : Baron Samedit (CVE-2021-3156)

C’est une bonne occasion pour tester ses compétences en reverse de code APK et surtout Javascript sur une machine easy de HackTheBox. C’est une méthode afin d’arriver à ses fins, il en existe surement d’autres!

Retrouvez cet article en vidéo

Merci d'avoir lu cet article !

Si vous avez des commentaires ou des retours sur cet article, n'hésitez pas à me contacter. Venez aussi me dire bonjour sur Twitter ou sur Linkedin.