学习Python渗透第二天:Python编写DHCP客户端

本文介绍了DHCP的工作流程,包括DHCPDiscover、DHCPOffer、DHCPRequest和DHCPACK等步骤,并展示了如何使用Python和scapy库来模拟DHCP客户端,包括构造和发送DHCP数据包的过程。
摘要由CSDN通过智能技术生成

DHCP的工作流程:

1:DHCP客户端在一个局域网中发送一个DHCP Discover 数据包,目的是发现能给他提供IP地址的DHCP服务器端

2:接收到DHCP Discover数据包的DHCP服务端会向请求的DHCP客户端发送一个DHCP Offer数据包,其中包含可以使用的IP地址

3:如果DHCP客户端接收到多个DHCP Offer数据包,那么只会处理最先收到的。然后DHCP客户端会广播一个DHCP Request数据包,声明自己选用DHCP服务器端和IP地址。

4:DHCP服务器端在接收到DHCP客户端广播的DHCP Request后,会判断该数据包中的IP地址是否与自己的地址相同。如果相同,就会向DHCP客户端发送一个响应的DHCP ACK数据包

目前为止,我们已经知道了DHCP的基本工作流程。下面让我们用Python来实现DHCP的客户端吧。

Python模拟DHCP客户端

1,DHCP Discover数据包构造

在上面,我们知道了DHCP客户端一共发送了四个数据包,而前面我们学习了scapy可以自己构造数据包,所以我们可以使用scapy来构造这四个数据包,但是困难在于每个数据包都需要设置Ether,IP,UDP,BOOTP.DHCP这五个协议的内容,而这五个协议就是由下至上的数据链路层,网络层,传输层,应用层,应用层管BOOTP和DHCP,而BOOTP是DHCP的前身。

(1)构造DHCP Discover数据包:我们知道DHCP工作的第一步是DHCP客户端发送DHCP Discover以广播的形式发送给DHCP服务端,然而客户端并不知道服务端的位置,所以在构造数据包时我们需要在Ether和IP部分将目标地址设置为广播地址,IP的广播地址:255.255.255.255,Ether的广播地址:ff:ff:ff:ff:ff:ff

(2),我们首先设置前三个协议,ether需要设置两个参数,一个是源mac地址,一个是目的mac地址,IP也是设置两个参数,由于此时没有源IP地址,所以设置为0.0.0.0,,表示未分配IP地址,作为临时的占位,UDP设置源端口和目的端口。

from scapy.all import Ether,RandMAC,IP,UDP

random_mac=str(RandMAC())
#这里是获得了一个随机的mac地址作为源mac地址
Ether_dis=Ether(src=random_mac,dst="ff:ff:ff:ff:ff:ff")
IP_dis=IP(src="0.0.0.0",dst="255.255.255.255")
UDP_dis=UDP(sport=67,dport=68)

(3),后面两个协议DHCP和BOOTP,我们首先来查看这两个协议由哪些内容构成,就知道需要哪些必要的参数

from scapy.all import ls,DHCP,BOOTP

print("下面是DHCP的")
ls(DHCP)
print("下面是BOOTP的")
ls(BOOTP)

我们能看出多数都是BOOTP的,所以在应用层,我们多数需要靠BOOTP()来实现,而BOOTP()的必要参数有两个,一个是chaddr,表示客户端的mac地址,一个是xid,表示事务ID,这个ID用于标识数据包,这样这个数据包就会是唯一的,一般使用一个随机数来作为xid参数的值。DHCP()中要设置的是optionss的值,用于指定DHCP数据包的选项字段,可以是IP,子网,网关,消息类型等等。

import binascii
from scapy.all import RandMAC,BOOTP,DHCP
import random

random_mac=str(RandMAC())
#这里是获得了一个随机的mac地址作为源mac地址

client_id=binascii.unhexlify(random_mac.replace(":",""))
#由于BOOTP的mac地址是没有:的,所以需要利用replace进行字符转换,将:转换为空' ',然后利用binascii.unhexlify将这一串mac地址转换为二进制
random_xid=random.randint(1,99999)
#random.randint是用于生成一个在1-99999范围内的交换标识符,也就是xid,注意xid是32位无符号位,所以范围是 0 到 4294967295。
BOOTP_dis = BOOTP(chaddr=client_id,xid=random_xid)
DHCP_dis=DHCP(options=[("message-type","discover"),"end"])
#message-type是指DHCP数据包的消息类型为discover,end表示DHCP数据包的结束字段为end

(4),以上五个协议我们都书写完了,最后就可以将所以协议放入一个数据包,组成完整的DHCP数据包

Discover=Ether_dis/IP_dis/UDP_dis/BOOTP_dis/DHCP_dis

(5),以上准备工作完成之后,考虑到我们的客户端并没有自己的IP地址,所以我们需要用前面学到的sendp()来发送数据包,但是发送的时候要选择合适的网卡,或者默认使用你的第一张网卡,如果你想选择网卡,windows系统中输入ipconfig,查看网卡信息,linux系统输入ifconfig查看网卡信息,我这里选择以太网作为我的网卡

sendp(Discover,iface='以太网')

这里为什么要用sendp()而不用send()呢,是因为DHCP是原始数据包,原始数据包包括以太网帧,ARP包,DHCP包,这些包是发往网络层的,所以叫原始数据包,而ICMP 包、TCP 包、UDP 包这些是经过协议处理过的包,这些是发往协议栈的,这些包使用send()来进行发送。

以下是全部代码:

import binascii
from scapy.all import RandMAC,BOOTP,DHCP,Ether,IP,UDP,sendp
import random

random_mac=str(RandMAC())


Ether_dis=Ether(src=random_mac,dst="ff:ff:ff:ff:ff:ff")
IP_dis=IP(src="0.0.0.0",dst="255.255.255.255")
UDP_dis=UDP(sport=67,dport=68)
client_id=binascii.unhexlify(random_mac.replace(":",""))

random_xid=random.randint(1,9999)

BOOTP_dis = BOOTP(chaddr=client_id,xid=random_xid)
DHCP_dis=DHCP(options=[("message-type","discover"),"end"])

Discover=Ether_dis/IP_dis/UDP_dis/BOOTP_dis/DHCP_dis
sendp(Discover,iface='以太网')

2,DHCP Offer数据包的捕获与解析

我们在wireshark中抓到了我们刚刚发出去的包,但是某些个人电脑原因,没有收到路由器返回的包,所以说这里只能展示一个包,正常来说是有两个

我们发送了DHCP数据包之后,就要捕获来自服务端的数据包,才能知道是否能给我们分配IP地址,所以我们可以用前面学过的sniff函数来进行过滤服务端返回的数据包,我们设置的目的端口dport是68,所以过滤语句这样写;

sniff(filter="src port 68")
#因为是由服务端发来,它作为源地址,所以这里是src而不是dst

捕获数据包后我们使用ls()函数查看数据包内容结构:

op   : ByteEnumField      =2          (1)
htype   : ByteField       =1          (1)
hlen   : ByteField       =6          (6)
hops   : ByteField       =0          (0)
xid   : IntField       =340091113          (0)
secs   : ShortField       =0         (0)
flags  :FlagsFiled(16bits)     =<Flag 0 ()>    ( <Flag 0 ()>) 
ciaddr  :IPField  ='0.0.0.0'  ('0.0.0.0')
yiaddr  :IPField  ='192.168.1.105'  ('0.0.0.0')
siaddr  :IPField  ='0.0.0.0'   ('0.0.0.0')
giaddr  :IPField  ='0.0.0.0'   ('0.0.0.0')
chaddr   :Field   =b'b7:95:a7:de:96:b'  (b'')
sname   : Field =b'\×00\×00\x00\x00\x00\×00\×00\×00\×00\×00\x00\x80\x0e\x00\x00\×00\x00\×00\x00\x20\x00\
file  :Field =b'\×00\×00\x00\x00\x00\×00\×00\×00\×00\×00\x00\x80\x0e\x00\x00\×00\x00\×00\x00\x20\x00\
options  : StrField  =b'c\x82Sc'  (b'')
--
options  :  DHCPOptionsField   =[('message-type', 2), ('server_id', '192.168.1.1'), ('lease_time', 7200),('name_server')



--以上的是BOOTP的内容,--下面的是DHCP的内容,我们想要获取这里面的内容可以通过下标索引,我们把接收这个数据包的变量命名为pkt,所以以下是一些内容的具体索引表示:

1,本次DHCP的xid:pkt[BOOTP].xid,我们可以看出xid是在上面的BOOTP中的,在哪个部分就[]哪个部分,然后需要那个部分的哪个值就在后面.就可以了

2,客户端的mac地址chaddr:pkt[BOOTP].chaddr或者pkt[Ether].dst

3,服务端IP地址:我们发现在最下面的options后面的值中有server_id,这就是返回的服务器的IP,而--下面的属于DHCP,所以服务器端IP地址是pkt[DHCP].options[1][1],因为是多元组,所以ip的下标为[1][1],同理可得,server_id 的下标为[1][0],2的下标为[0][1],message-type的下标为[0][0]。

4,客户端向服务端请求的IP地址:pkt[BOOTP].yiaddr

3,DHCP  Request数据包的构造

当我们获得服务端的Offer,知道哪些IP地址可以选用后,我们就要构造request数据包来声明自己选用的客户端和IP地址,这个数据包仍然是用广播的方式来发送。

1,Ether部分设置两个参数src和dst

Ether_request=Ether(src=pkt[Ether]dst,dst="ff:ff:ff:ff:ff:ff")
#pkt[Ether]dst是上面我们捕获到的服务端Offer包里面提供的客户端mac地址

2,IP部分设置两个参数src和dst,由于此时服务端还没有分配IP地址,所以还是0.0.0.0

IP_request=IP(src="0.0.0.0",dst="255.255.255.255")

3,UDP部分设置两个参数sport和dport

UDP_request=UDP(sport=67,dport=68)

4,BOOTP需要指明xid和本机的mac地址

BOOTP_request=BOOTP(chaddr=pkt[Ether].dst,xid=pkt[BOOTP].xid)
#pkt[BOOTP].xid这个也是从服务端传回的Offer中提取出的xid号

5,DHCP中需要指定DHCP服务器端的IP地址和申请的IP地址

DHCP_request=DHCP(options=[("message-type",'request'),("server_id",pkt[DHCP].options[1][1]),("requested_addr",pkt[BOOTP].yiaddr),"end"])
#message-type,request表示消息类型为请求,还有其他消息类型,比如发现(Discover)、提供(Offer)、请求(Request)和确认(Acknowledge)
#server_id,pkt[DHCP].options[1][1]表示DHCP服务器的地址,从先抓取到的DHCP Offer中获取
#requested_addr",pkt[BOOTP].yiaddr,表示请求的IP地址,也是从刚刚的Offer中获取
#end表示结束标识符

6,完整的请求数据包如下:

request=Ether_request/IP_request/UDP_request/BOOTP_request/DHCP_request

7,发送请求包

sendp(request,iface="WLAN")
#这里的WLAN是我的无线网络接口的名字的代替,自己写的时候如果你用的是以太网,就要用以太网的名字
#如果是wifi,就要用wifi的名字,怎样查看名字,在windows中输入ipconfig /all然后找到你电脑
#正在连接的那个项目,找到描述,描述的内容就是你的接口名字。

完整代码如下:

from scapy.all import Ether,IP,UDP,BOOTP,DHCP,sendp

Ether_request=Ether(src=pkt[Ether]dst,dst="ff:ff:ff:ff:ff:ff")
IP_request=IP(src="0.0.0.0",dst="255.255.255.255")
UDP_request=UDP(sport=68,dport=67)
BOOTP_request=BOOTP(chaddr=pkt[Ether].dst,xid=pkt[BOOTP].xid)
DHCP_request=DHCP(options=[("message-type",'request'),("server_id",pkt[DHCP].options[1][1]),("requested_addr",pkt[BOOTP].yiaddr),"end"])
request=Ether_request/IP_request/UDP_request/BOOTP_request/DHCP_request
sendp(request,iface="WLAN")
print(pkt[BOOTP].yiaddr + "正在分配")
if pkt[DHCP].options[0][1]==5:
    print(pkt[BOOTP].yiaddr+"已经分配")
sniff(filter="src port 67",iface='WLAN',prn=detect_dhcp,count=10)

好了,今天的学习分享就到这里了,有什么不足的请大家指正,谢谢大家的观看。

说明: 1, 暂未实现重传机制, 所以若抓包无响应, 请尝试停止后重发. 2, dhcp状态显示采用1s定时器刷新, 所以状态显示可能存在延时的情况; 3, xcap通过pcap导入报文会有部分字段自动变化, 且导入的报文DHCP数据部分无法正常解析, 建议通过新建的方式解决; 4, 添加报文格式举例: 1,2 说明: 1表示报文组1, 选中报文组后, 在状态栏会显示报文组的索引, 2表示第三个报文, 即索引为3的报文. 版本记录: V1.0.1(基础版本) 1, 支持连接xcap并读取报文功能; 2, 支持刷新按钮自动更新报文功能; 3, 支持选择网卡功能; 4, 支持通过pcap文件打开报文功能(已废弃); 5, 支持指定服务器交互; 6, 支持dhcp交互状态显示; 7, 支持输入框通过正则表达式限制输入字符; 8, 支持选择特定报文操作; V1.0.2 1, 将状态修改为自动显示, 即动态识别报文类型并显示结果; 2, 解决解析option字段, 若字段中存在多个value时存在丢失的问题; 3, 增加鼠标点击状态显示气泡信息; 4, 增加隔行显示不同颜色; V1.0.3 1, 修改dhcp的状态机, 之前的版本是收到报文则发送request, 之后收到报文则认为收到ack. 现修改为只有收到offer报文才发送request报文 , 收到ack报文才结束. 2, 增加dhcpv6功能; 3, 优化代码; V1.0.4 1, 修改request报文由于校验和和报文长度未初始化导致构造错误的问题 V1.0.5 1, 增加服务器地址的气泡提示; 2, 增加自动填充的气泡提示; 3, 添加的报文默认为选中状态; 4, 选择网卡下拉框中将虚拟网卡排放靠后; 5, 关闭程序时自动保存设置; V1.0.6 1, 优化代码, 将字段设置使用统一的函数处理; 2, 状态气泡显示格式化; 3, 双击表格表头实现全选和反选; 4, 增加renew(50%), rebind(87.5%)和release的自动发送功能; 5, 增加手动释放按钮和实现; 6, 增加部分打印信息用于调试; 暂未实现报文重传机制, 计划下一个版本实现 V1.0.7 1, 实现discover/solicit报文自动重传机制 2, renew, rebind以及release修改为手动发送 3, 解决报文发送错乱问题 4, 增加decline报文的发送 5, 解决设备无故发送discover报文问题 问题解决: 1, 停止后再次发送数据会出现数据错乱 分析: 停止客户端的时候, 删除过滤器是通过callback函数删除的, 这里应该是通过filter来进行删除. self.widget.sniff.del_filter(self.callback)修改为 self.widget.sniff.del_filter(self.filter) 2, 设备无故发送discover问题 分析: 由于发送discover报文使用的定时器, 定时器是通过判断当前的direction来确定是否重传的, 而当定时器老化时, 可能正好收到报文导 致direction被修改, 所以导致错误的发送discover报文的问题. 将接收逻辑修改为重传时判断当前状态是否为discover报文, 若是则重传, 否则不重传. V1.0.8 1, 增加inform实现 V1.0.9 1, 增加报文五元组的源mac地址和xid的气泡显示; 2, 增加步长和报文限制功能; 问题解决: 1, 修改ipv6报文添加失败的问题. 由于ipv4报文为xid, ipv6报文为trid, 需要区分处理. V1.0.10 1, 在发送dhcpv6报文之前, 先发送na报文触发服务器学习nd消息. V1.0.11 1, 增加发送solicit/request前, 自动响应ns报文. 自动响应ns报文的目标地址为solicit/request报文源mac地址生成的ipv6地址 2, 实现dhcpv6的renew续约功能. 3, 解决ipv6地址转换格式化不正确, 导致无法响应ns报文问题. 4, 增加日志输出到dhcp.log文件. V1.0.12 1, 增加dhcpv6的续约功能, 通过renew和rebind实现续约, 增加release、decline报文的实现; V1.0.13 1, 解决服务器无法设置ipv6地址的问题. 之前的输入框只允许输入数字和., 修改为运行输入数字.:和a-f 2, 解决多个客户端时, 若选中其中的部分客户端发送时报错. 由于客户端采用的是列表中包含元组的形式, 即[(row, [client1, client2])], 这样实际客户端无法直接通过row索引到clients, 导致列表读 取时溢出. 譬如有1、2、3三行数据, 这里只选中了第三行, 限制为1, 那么如果点击发送, 则clients = [(row, [client1]], 此时clients[2] 就会溢出. 所以这里讲clients修改为字典, 即通过row来索引客户端client = {3: [client1]} 3, 将数据发送放到线程中, 规避模拟大量客户端时界面假死的问题. 4, 当客户端限制小于等于50, 则气泡显示trid和ip地址信息. 当大于50, 则气泡显示获取ip地址的数量. V1.0.14 1, 解决监听报文使用的网卡不正确问题. V1.0.15 1, 解决dhcpv6的响应报文的IANA中包含Status code选项导致程序无法解析的问题. 兼容性处理, 即option为IAAddress时按照IAAddress解析, 当option为status code时按照Status code解析 V1.0.16 1, 解决DHCPv6的client_id的duid处理, 支持任意格式的duid.
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值