作者:0x7F@知道創宇404實驗室
時間: 2021年09月28日

0x00 前言

前段時間看到一款局域網掃描的 App「Fing」,相比于 Nmap 的端口服務掃描,他可以掃描獲取目標主機的設備名稱和用戶名,在內網資產梳理時這些信息能夠提供一定的幫助。

本文從 Fing 的功能入手,學習和介紹了目前常用的局域網服務發現協議,并根據這些協議,嘗試編寫 Python 掃描腳本。

0x01 Fing

Fing App 設備掃描功能演示:

[1.Fing設備掃描]

使用 Wireshark 抓包和測試發現,Fing 同樣也通過常規的掃描技術進行主機發現,然后通過嗅探通信在 5353 端口的 MDNS 報文,從中提取設備信息。(關于 Fing 的掃描原理這里就不展開了)

mDNS 是常見的局域網主機/服務發現協議,這類協議常用于局域網設備之間進行自動發現,從而上層應用或服務可以實現零配置使用;由于這些協議功能的需要,所以協議中都包含了大量的描述信息,可以用于內網主機的掃描。

除了 mDNS 以外,局域網服務發現協議還有很多,比如:

  1. DNS-SD(DNS Service Discovery):基于DNS協議的服務發現
  2. SSDP(Simple Service Discovery Protocol):簡答服務發現協議
  3. NBNS(NetBIOS name service):NetBIOS名稱服務(已過時)
  4. etc

0x02 DNS-SD

DNS-SD(DNS Service Discovery)是一種基于 DNS 協議的服務發現協議,設備之間可以通過該協議自動發現服務;DNS-SD 兼容 mDNS 協議,同樣使用 UDP 5353 端口,在 Wireshark 中統一標注為 MDNS

使用 DNS-SD 協議的設備會周期性的在組播地址 224.0.0.251 廣播自己感興趣的服務名稱,若有設備開啟指定服務就會發送服務的詳細信息給源設備;除此之外,這個報文還包含了源設備的信息,Fing 就是通過這個報文獲取到的設備信息。通過 Wireshark 抓包如下:

[2.嗅探周期性的DNS-SD/mDNS報文]

除了通過嗅探周期性的報文獲取信息,我們還發現 DNS-SD 協議提供了一種主動查詢服務的功能(https://datatracker.ietf.org/doc/html/rfc6763#section-9),通過向目標主機發送查詢名為 _services._dns-sd._udp.local,類型為 PTR 記錄的 DNS 查詢報文,目標主機將返回自身開放的服務名稱。(這里我們只討論在內網環境下 DNS-SD 使用的場景,DOMAIN=.local)

使用 Python scapy 包的功能展示該請求和響應報文格式(未顯示字段為默認值):

[3.dnssd.local查詢(scapy)]

隨后再以目標的服務名稱為查詢名,發送 PTR 記錄 DNS 查詢報文,查詢服務的詳細信息,請求和響應報文格式如下:

[4.service詳情查詢(scapy)]

響應報文的附加字段里包含了服務的詳細信息,從中我們可以提取到服務的協議、端口、以及設備信息:

[5.從dnssd響應報文中提取信息]

根據以上交互流程,我們編寫 dnssd 的掃描腳本如下:

#/usr/bin/python3
#!coding=utf-8

import socket
import sys
from scapy.all import raw, DNS, DNSQR

def get_service_info(sock, target, resp):
    service = (resp.an.rdata).decode()

    # query each service detail informations
    req = DNS(id=0x0001, rd=1, qd=DNSQR(qtype="PTR", qname=service))
    #req.show()
    sock.sendto(raw(req), target)
    data, _ = sock.recvfrom(1024)
    resp = DNS(data)
    #resp.show()

    # parse additional records
    repeat = {}
    for i in range(0, resp.arcount):
        rrname = (resp.ar[i].rrname).decode()
        rdata  = resp.ar[i].rdata

        if rrname in repeat:
            continue
        repeat[rrname] = rdata

        if hasattr(resp.ar[i], "port"):
            rrname += (" " + str(resp.ar[i].port))

        if rrname.find("._device-info._tcp.local.") > 0:
            print(" "*4, rrname, rdata)
        else:
            print(" "*4, rrname)
# end get_service_info()

def dnssd_scan(target):
    sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    sock.settimeout(2)

    # query all service name
    req = DNS(id=0x0001, rd=1, qd=DNSQR(qtype="PTR", qname="_services._dns-sd._udp.local"))
    #req.show()
    try:
        sock.sendto(raw(req), target)
        data, _ = sock.recvfrom(1024)
    except KeyboardInterrupt:
        exit(0)
    except:
        print("[%s] OFFLINE" % target[0])
        return
    resp = DNS(data)
    #resp.show()

    print("[%s] ONLINE" % target[0])
    for i in range(0, resp.ancount):
        get_service_info(sock, target, resp)
# end dnssd_scan()

if __name__ == "__main__":
    if not (len([sys.argv]) > 0 and sys.argv[1].endswith(".0")):
        print("usage: python3 dnssd.py 192.168.1.0")
        exit(0)

    print("dnssd scan start")
    network = sys.argv[1].rstrip("0")

    # scan local network
    for i in range(1, 256):
        target = (network + str(i), 5353)
        dnssd_scan(target)

    print("dnssd scan end")
# end main()

運行效果如下:

[6.dnssd.py運行結果]

后來發現 Nmap 也提供了 dns-service-discovery.nse 的掃描腳本(https://github.com/nmap/nmap/blob/master/scripts/dns-service-discovery.nse)

0x03 SSDP

不過目前 DNS-SD 使用范圍還是比較小的,最成熟 Zeroconf 實現是蘋果家的 Bonjour,底層使用 DNS-SD 協議,用上面的腳本掃出來的大部分都是蘋果的產品。

相比之下,SSDP(Simple Service Discovery Protocol)就使用得非常廣泛了,他是 UPnP(Universal Plug and Play) 的核心實現;在 SSDP 協議中,請求和響應報文會附加一些主機信息(RFC文檔未強制規定),我們同樣可以利用這一點來掃描內網主機。

SSDP 的標準查詢服務的報文格式如下,其中 USER-AGENT 字段未強制規定:

M-SEARCH * HTTP/1.1
S: uuid:ijklmnop-7dec-11d0-a765-00a0c91e6bf6
Host: 239.255.255.250:reservedSSDPport
Man: "ssdp:discover"
ST: ge:fridge
MX: 3
USER-AGENT: Google Chrome/93.0.4577.82 Mac OS X

SSDP 客戶端會周期性的發送該查詢報文,以尋找自己感興趣的服務,我們可以通過嗅探提取 USER-AGENT 字段,獲得主機的操作系統信息。

除此之外,如果設置 SSDP 請求報文中 ST: ssdp:all 字段,并將報文發向組網地址 239.255.255.250:1900,SSDP 服務端收到報文后,會將自身服務響應給源地址,響應報文格式如下,其中 Server 字段未強制規定:

HTTP/1.1 200 OK
S: uuid:ijklmnop-7dec-11d0-a765-00a0c91e6bf6
Ext:
Cache-Control: no-cache="Ext", max-age = 5000
ST: ge:fridge
USN: uuid:abcdefgh-7dec-11d0-a765-00a0c91e6bf6
AL: <blender:ixl><http://foo/bar>
Server:Microsoft-Windows/6.3 UPnP/1.0 UPnP-Device-Host/1.0

通過解析響應報文提取 Server 字段,也可以獲得主機的操作系統信息。

根據以上交互流程,我們編寫 ssdp 的掃描腳本如下:

#/usr/bin/python3
#!coding=utf-8

import socket
import struct

address = ("239.255.255.250", 1900)
result = {}

def get_serv_ua(resp):
    lines = resp.split("\r\n")
    for i in lines:
        array = i.split(":")
        if array[0].upper() == "SERVER" or array[0].upper() == "USER-AGENT":
            return array[1]
    # end-for
# end get_serv_ua()


def ssdp_scan():
    print("[scan mode]")
    req = b'M-SEARCH * HTTP/1.1\r\nHost: 239.255.255.250:1900\r\nST:ssdp:all\r\nMan: "ssdp:discover"\r\nMX:1\r\n\r\n'

    sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    sock.settimeout(5)

    # send "ssdp:all" query request
    sock.sendto(req, address)

    # receive and print
    while True:
        try:
            resp, raddr = sock.recvfrom(1024)
        except:
            break
        if raddr[0] not in result:
            data = get_serv_ua(resp.decode())
            result[raddr[0]] = data
            print(raddr[0], data)
    # end-while
# ssdp_scan()

def ssdp_sniffer():
    print("[sniffer mode] (stop by Ctrl-C)")

    sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    sock.bind(address)

    # join the multicast group
    maddr = struct.pack("4sl", socket.inet_aton("239.255.255.250"), socket.INADDR_ANY)
    sock.setsockopt(socket.IPPROTO_IP, socket.IP_ADD_MEMBERSHIP, maddr)

    # receive and print
    while True:
        try:
            resp, raddr = sock.recvfrom(1024)
        except:
            break
        if raddr[0] not in result:
            data = get_serv_ua(resp.decode())
            result[raddr[0]] = data
            print(raddr[0], data)
# ssdp_sniffer()

if __name__ == "__main__":
    print("ssdp scan start")

    ssdp_scan()
    ssdp_sniffer()

    print("ssdp scan end")
# end main()

運行效果如下:

[7.ssdp.py運行結果]

0x04 總結

除了文中提到的 DNS-SD 和 SSDP 協議,還有很多其他的協議可以幫助我們對內網主機進行梳理,讀者可以自行擴展學習。


References:
https://www.ietf.org/rfc/rfc6762.txt
https://www.ietf.org/rfc/rfc6763.txt
https://datatracker.ietf.org/doc/html/draft-cai-ssdp-v1-03
https://apps.apple.com/cn/app/fing-%E7%BD%91%E7%BB%9C%E6%89%AB%E6%8F%8F%E4%BB%AA/id430921107
https://zyun.#/blog/?p=42
https://github.com/nmap/nmap/blob/master/scripts/dns-service-discovery.nse


Paper 本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.bjnorthway.com/1727/