This post is about a pet peeve of mine — local applications which expose various (not only) HTTP services by binding to the loopback interface without considering the “standard” attack vectors which web applications should be protected against.

Basic case

Let’s say you have code approximating the following.

@route("/run/<cmd>")
def cmd(cmd):
    return os.system(cmd)

run(host="127.0.0.1")

For an outside-facing application, this surely looks ridiculous. However, we are binding to the loopback interface, so nothing can possibly go wrong right?

This is an all too common of an assumption.

If the user of the application is using a browser on the same machine (very reasonable expectation in most cases), any website they visit can execute a request to 127.0.0.0:8080/run/do_evil! Cross origin policy is going to prevent the requesting website from reading the response contents, but as the damage has been already done when executing the request, that’s not really relevant.

<img src="http://127.0.0.1:8080/run/do_evil"/>
Local application User (Browser) Malicious website Go to www.evil.com Malicious JavaScript HTTP Requests opt [ Attacker controlled ]

JavaScript execution isn’t even always necessary for these attacks, as long as we are happy with being limited to sending “normal” HTML-induced GETs and <form> POSTs.

WebSockets

In a similar way, unsecured websockets can provide an even easier avenue for exploitation.

By default, any website can connect to any websocket and perform bidirectional communication. This “cross site websocket hijacking” is something to be aware of as it can leak information even if the attacker can’t cause damage on some another HTTP endpoint.

Password protection

Methods like basic auth or standard session cookie based authentication aren’t sufficient for the same reason they aren’t sufficient for “normal” web services. Attacker-executed request will inherit the users credentials even if the request later fails because of the same-origin policy. Explicit protection against Cross Site Request Forgery is necessary.

Cross protocol scripting

Anyway, so all these browsers and HTTPs and JavaScripts and what have you clearly suck.

Let’s just write a simple telnet interface for our application like it’s the nineties. Then none of this modern rubbish can do us any harm right?

Unfortunately, this assumption is also wrong.

Let’s see what happens if we send an HTTP request to the telnet interface of (an unpatched version of) OpenOCD (a popular open source tool for microcontroller debugging).

Open On-Chip Debugger
> POST / HTTP/1.1
invalid command name "POST"
> Host: 127.0.0.1:4444
invalid command name "Host:"
> Connection: keep-alive
invalid command name "Connection:"
> Content-Length: 12
invalid command name "Content-Length:"
> Pragma: no-cache
invalid command name "Pragma:"
> Cache-Control: no-cache
invalid command name "Cache-Control:"
> Origin: https://m.atx.name
invalid command name "Origin:"
> User-Agent: Mozilla/5.0 (X11; Linux x86_64) ...
invalid command name "User-Agent:"
> Content-Type: text/plain;charset=UTF-8
invalid command name "Content-Type:"
> Accept: */*
invalid command name "Accept:"
> Accept-Encoding: gzip, deflate, br
invalid command name "Accept-Encoding:"
> Accept-Language: en-US,en;q=0.9,cs;q=0.8,la;q=0.7,fr;q=0.6
invalid command name "Accept-Language:"
> 

That’s right — nothing really happens. Line based protocols in general tend to be very fault-tolerant, especially if they are meant to be used manually.

This opens up an attack vector — we are forced by the constraint of running in a browser to send an HTTP requests with a bunch of headers we can’t really affect in any meaningful way. Body of the request is completely under our control though.

As all of the “invalid commands” get dropped anyway, all we need is to include exec do_evil\n (OpenOCD for “run this in a shell”) in our POST request body and we are done.

var x = new XMLHttpRequest();
x.open("POST", "http://127.0.0.1:4444", true);
x.send("exec xcalc\r\n");

This is a very insidious type of attack as all the potential cross-protocol paths might not be obvious. For example, the above described exploit can be slightly modified to use Gopher instead of HTTP. The modified version works with current OpenOCD and elinks.

Modern browsers provide some mitigation by refusing to connect to a list of what are considered “unsafe” ports. Applications can attempt to identify cross-protocol requests (for instance by detecting the Host: string) and terminate the connection before damage occurs.

DNS Rebinding

This is an awesome trick originally invented for bypassing same-origin policy on firewalled servers by bouncing off of a browser behind said firewall. It also works nicely for our localhost-bound services.

Local application User (Browser) Attacker Where is evil.com? 111.222.111.222 opt [ DNS ] GET http://evil.com evil.js, ... opt [ HTTP ] loop [ Wait until DNS cache clears ] Where is evil.com? 127.0.0.1 opt [ DNS ] GET http://evil.com/ secret.html POST secret.html opt [ HTTP ]

The malicious JavaScript has no problem with the same-origin policy. For all intents and purposes the code comes from the same origin as the response.

When poking around with this attack method, the public rbndr server comes in handy.

Some DNS recursors prevent this attack by refusing to resolve public TLDs to private IP ranges (dnsmasq --stop-dns-rebind).

Checking the Host: request header server-side and refusing to serve unexpected domains goes a long way in preventing this.

Final observation

TCP was never meant as a mechanism for communication between processes on the same machine. Caution should be exercised when implementing localhost-only applications.