J'ai constaté il y a quelques jours que l'application Android sur laquelle je travaille à mes heures perdues, ApkTrack, ne parvenait plus à lire l'un des sites sur lesquelles elle récupère habituellement des informations. Pour résumer, ApkTrack fait principalement du web scraping pour collecter des informations de version, et il arrive régulièrement que les sites consultés mettent en place des mesures pour empêcher les robots, même non-malveillants, d'accéder à leur contenu. Ce post décrit l'une de ces contre-mesures à laquelle j'ai été confronté ce week-end, et comment elle a pu être contournée.
Tout d'abord, le constat : j'ai remarqué que le script suivant était servi sur un site qu'on ne nommera pas en lieu et place des données attendues.
<body>
<script type="text/javascript" src="/aes.min.js"></script>
<script>
function toNumbers(d) {
var e = [];
d.replace(/(..)/g, function(d) {
e.push(parseInt(d, 16))
});
return e
}
function toHex() {
for (var d = [], d = 1 == arguments.length && arguments[0].constructor == Array ? arguments[0] : arguments, e = "", f = 0; f < d.length; f++) e += (16 > d[f] ? "0" : "") + d[f].toString(16);
return e.toLowerCase()
}
var a = toNumbers("5d026cff5942d1ab28e3757e4b2e2f87"),
b = toNumbers("845dd1e672b840c246aa8cfe9b5d3632"),
c = toNumbers("e48176221e1325e09b9a959370446f05");
var now = new Date(),
time = now.getTime();
time += 3600 * 1000 * 24;
now.setTime(time);
document.cookie = "BKS=" + toHex(slowAES.decrypt(c, 2, a, b)) + "; expires=" + now.toUTCString() + "; path=/";
location.href = "http://site.com/page/?ckattempt=1";
</script>
</body>
On comprend assez vite que le script a pour but d'effectuer un calcul (de l'AES, et lentement en plus) pour générer un cookie qui sert de sésame pour afficher les pages du site. J'observe que la valeur obtenue ressemble à un MD5, tout comme les variables a
, b
et c
du code ci-dessus qui sont d'ailleurs régénérées à chaque nouvelle tentative. Je ne parviens à casser aucun de ces hashes rapidement : il va donc falloir creuser un peu. Idéalement, j'aimerais lire le code source qui génère ces valeurs et par chance, on le retrouve assez facilement via une recherche bien sentie : c'est un module nginx intitulé testcookie.
La lecture des quelques 2000 lignes de code est rendue pénible par l'omniprésence de macros issues de nginx, mais les éléments suivants finissent par émerger :
a
etb
sont respectivement la clé et le vecteur d'initialisation utilisés par l'algorithme AES-CBC ;c
est la valeur à déchiffrer.- Cette valeur est calculée de la manière suivante :
c = AES(MD5($testcookie_session + $testcookie_secret))
, ces deux variables étant définies dans la configuration nginx. Plus précisément :- Selon la documentation,
testcookie_session
peut-être soit l'adresse IP du visiteur (ex :127.0.0.1
), soit l'adresse IP concaténée à l'user-agent du navigateur (ex :127.0.0.1Mozilla/5.0 (X11; Ubuntu; Linux x86_64; [...]
). Cette partie est connue, et on peut la générer facilement. testcookie_secret
en revanche est une valeur fixe mais inconnue. Elle est soit prédéterminée, soit aléatoire (auquel cas elle change à chaque redémarrage du serveur web).
- Selon la documentation,
A partir d'ici, deux voies sont possibles pour contourner ce script. Soit se débrouiller pour exécuter le javascript comme le ferait un navigateur légitime, soit trouver un moyen de deviner la valeur que prendra le cookie. La première option me parait trop lourde, je commence donc par étudier la seconde. Il faut en premier lieu déterminer comment la valeur testcookie_session
est générée. Pour cela, rien de plus facile : je change de navigateur et me connecte au site, puis compare les deux cookies. Ils sont identiques : cela signifie que cette variable ne contient que mon adresse IP. La seconde étape est a priori plus délicate : arriver à trouver la valeur de testcookie_secret
qui a été paramétrée sur le site cible. L'équation est la suivante :
- Je connais un cookie valide (il suffit de se connecter au site) :
64534e58cbc178830089d06de12c00ed
. - Je sais que mon adresse IP pour cette connexion était
95.130.11.147
. - Enfin, on a établi que
64534e58cbc178830089d06de12c00ed = MD5("95.130.11.147" + testcookie_secret)
.
Nous nous retrouvons donc dans un cas de bruteforce classique. Je sors l'artillerie lourde, Hashcat :
PS C:\Users\Ivan\oclHashcat-1.33> .\oclHashcat64.exe -m0 .\targets\site.txt -a7 95.130.11.147 .\dicts\wordlist.txt
oclHashcat v1.33 starting...
[...]
64534e58cbc178830089d06de12c00ed:95.130.11.147keepmesecret
L'option a7
correspond à une attaque hybride, c'est à dire que chaque mot du dictionnaire est préfixé par une chaine de caractères. Après un certain temps passé à mouliner, l'outil crache glorieusement le résultat : testcookie_secret = keepmesecret
. En réalité, j'avais découvert cette valeur avant que le bruteforce ne soit terminé pour une raison amusante : keepmesecret
est la valeur donnée en exemple dans la documentation du script, et dans le doute, je l'avais testée manuellement. Parier sur le fait que l'admin système est paresseux et va copier/coller la configuration donnée en exemple paye toujours.
Tous les ingrédients sont à présent réunis et il n'y a plus qu'à calculer un MD5 avant chaque requête afin de pouvoir accéder aux pages souhaitées.
EDIT : Suite à ce post, la taille minimale de testcookie_secret
a été portée à 32 caractères dans la dernière version du script.