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 :
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.
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.
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 !
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 :
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
:
La variable _0x32f76d
correspond à une autre variable. Si on continue à remonter
les dépendences et renommages, on obtient le chemin suivant :
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 :
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 à :
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!