在内网中,好的信息搜集能力能够帮助开发者更快地拿到权限及达成目标。 内网里数据多种多样,需要根据需求寻找任何能对下一步渗透行动有所帮助的信 息。信息搜集能力是渗透过程中不可或缺的重要一步。下面将介绍各类搜集信息 的方法。
4.2.1 基于ICMP的主机发现
ICMP(Internet Control Message Protocol ,Internet报文协议)是TCP/IP的一种 子协议,位于OSI 7层网络模型中的网络层,其目的是用于在IP主机、路由器之间 传递控制消息。
1.ICMP工作流程
ICMP中提供了多种报文,这些报文又可分成两大类:“差错通知”和“信息查 询”。
(1)差错通知
当IP数据包在对方计算机处理过程中出现未知的发送错误时,ICMP会向发送 者传送错误事实以及错误原因等,如图4-1所示。
图4-1 差错通知
(2)信息查询
信息查询由一个请求和一个应答构成的。只需要向目标发送一个请求数据
包,如果收到了来自目标的回应,就可以判断目标是活跃主机,否则可以判断目 标是非活跃主机,如图4-2所示。
图4-2 信息查询
2.ICMP主机探测过程
Ping命令是ICMP中较为常见的一种应用,经常使用这个命令来测试本地与目 标之间的连通性,发送一个ICMP请求消息给目标主机,若源主机收到目标主机的 应答响应消息,则表示目标可达,主机存在。例如,我们所在的主机IP地址为
192.168.124.134 ,而通信的目标IP地址为192.168.124.5 。如果要判断192.168.124.5 是否为活跃主机,只需要向其发送一个ICMP请求,如果192.168.124.5这台主机处 于活跃状态,那么它在收到这个请求之后,就会给出一个回应,如下所示:
现在来编写一个利用ICMP实现探测活跃主机的代码程序。程序有很多种可实 现方式,此处我们借助Scapy库来完成。Scapy是Python中一个第三方库,在Scapy 库内部已经实现了大量的网络协议,例如TCP 、UDP 、IP 、ARP等,使用Scapy可 以灵活地编写各种网络工具。
首先我们对Scapy进行安装,命令如下:
安装过程如下所示: |
接下来,我们可以利用Scapy库函数中的ICMP实现探测主机存活,详细过程 如下所示。 1)导入程序代码所应用到的模块:scapy 、random 、optparse ,其中scapy用 于发送ping请求和接收目标主机的应答数据,random用于产生随机字段,optparse 用于生成命行参数形式。示例如下: |
#!/usr/bin/python #coding:utf-8 from scapy .all import * from random import randin t from optparse import OptionParser |
2)对用户输入的参数进行接收和批量处理,并将处理后的IP地址传入Scan函 数。 |
def main() :
parser = OptionParser("Usage:%prog -i <target host> ") # 输出帮助信息
parser .add_option( '-i ',type= 'string ',dest= 'IP ',help= 'specify target host ') # 获取IP地址参数
options,args = parser .parse_args()
print("Scan report for " + options .IP + "\n")
# 判断是单台主机还是多台主机
# IP中存在-,说明是要扫描多台主机
if '- ' in options .IP:
# 代码举例:192 .168 .1 .1-120
# 通过“ -”进行分隔,把192 .168 .1 .1和120分开
# 把192.168.1.1通过“,”进行分隔,取最后一个数作为range函数的start,然后把120+
1作为range函数的stop
# 这样循环遍历出需要扫描的IP地址
for i in range(in t(options .IP .split( '- ')[0] .split( ' . ')[3]), in t
(options .IP .split( '- ')[1]) + 1) :
Scan(
options .IP .split( ' . ')[0] + ' . ' + options .IP .split( ' . ')[1] + ' . ' + options .IP .split( ' . ')[2] + ' . ' + str(i))
time .sleep(0 .2)
else:
Scan(options .IP)
print("\nScan finished!. . .\n")
if __name__ == "__main__" :
try:
main()
except Keyboard Interrupt :
print("interrupted by user, killing all threads . . .")
3)Scan函数通过调用ICMP ,将构造好的请求包发送到目的地址,并根据目 的地址的应答数据判断目标主机是否存活。存活的IP地址会打印 出“xx.xx.xx.xx →Host is up” ,对于不存活的主机打印出“xx.xx.xx.xx →Host is down”: |
icmp_id = randin t(1, 65535) icmp_seq = randin t(1, 65535) packet=IP(dst=ip,ttl=64,id=ip_id)/ICMP(id=icmp_id,seq=icmp_seq)/b ' rootkit ' result = sr1(packet, timeout=1, verbose=False) if result : for rcv in result : scan_ip = rcv[IP] .src print(scan_ip + '---> ' 'Host is up ') else: print(ip + '---> ' 'host is down ') |
运行效果如下所示。 |
此处,我们也可以在程序中导入Nmap库函数,实现探测主机存活工具的编 写。这里使用Nmap函数的-sn与-PE参数,-PE表示使用ICMP ,-sn表示只测试该主 机的状态,具体步骤如下所示。 1)导入程序代码所应用到的模块:nmap 、optparse 。nmap模块用于产生 ICMP的请求数据包,optparse用于生成命令行参数。 |
#!/usr/bin/python3
# -*- coding: utf-8 -*
import nmap
import optparse
2)利用optparse模块生成命令行参数化形式,对用户输入的参数进行接收和 批量处理,最后将处理后的IP地址传入NmapScan函数。 |
if __name__ == '__main__ ' : parser = optparse .OptionParser( 'usage: python %prog -i ip \n\n ' 'Example: python %prog -i 192 .168 .1 .1 [192.168.1.1-100]\n') # 添加目标IP参数-i parser.add_option('-i','--ip',dest='targetIP',default='192.168.1.1 ',type='string', help= 'target ip address ') options,args = parser .parse_args() # 判断是单台主机还是多台主机 # IP中存在“ -”,说明是要扫描多台主机 if '- ' in options .target IP: # 代码举例:192 .168 .1 .1-120 # 通过 '- '进行分割,把192 .168 .1 .1和120进行分离 # 把192.168.1.1通过“,”进行分隔,取最后一个数作为range函数的start,然后把 120+1作为range函数的stop # 这样循环遍历出需要扫描的IP地址 for i in range(in t(options .target IP .split( '- ')[0] .split( ' . ')[2]),in t (options .target IP .split( '- ')[1])+1) : NmapScan(options .target IP .split( ' . ')[0] + ' . ' + options .target IP . split( ' . ')[1] + ' . ' + options .target IP .split( ' . ')[2] + ' . ' + str(i)) else: NmapScan(options .target IP) |
3)NmapScan函数通过调用nm.scan() 函数,传入-sn-PE参数,发起ping扫 描,并打印出扫描后的结果。 |
def NmapScan(target IP) : # 实例化PortScanner对象 nm = nmap .PortScanner() try: # hosts为目标IP地址,argusments为Nmap的扫描参数 # -sn:使用ping进行扫描 # -PE:使用ICMP的 echo请求包(-PP:使用timestamp请求包 -PM:netmask请求包) result = nm .scan(hosts=target IP, arguments= '-sn -PE ') # 对结果进行切片,提取主机状态信息 state = result[ 'scan '][target IP][ 'status '][ 'state '] print("[{}] is [{}]" .format(target IP, state)) except Exception as e: pass |
运行效果如下所示。
基于ICMP的探测主机存活是一种很常见的方法,无论是以太网还是互联网都
可以使用这种方法。但是该方法也存在一定的缺陷,就是当网络设备,例如路由 器、防火墙等对ICMP采取了屏蔽策略时,就会导致扫描结果不准确。
4.2.2 基于TCP 、UDP的主机发现
基于TCP 、UDP的主机发现属于四层主机发现,是一个位于传输层的协议。 可以用来探测远程主机存活、端口开放、服务类型以及系统类型等信息,相比于 三层主机发现更为可靠,用途更广。
TCP是一种面向连接的、可靠的传输通信协议,位于IP层之上,应用层之下 的中间层。每一次建立连接都基于三次握手通信,终止一个连接也需要经过四次 握手,建立完连接之后,才可以传输数据。当主动方发出SYN连接请求后,等待 对方回答TCP的三次握手SYN+ACK ,并最终对对方的SYN执行ACK确认。这种 建立连接的方法可以防止产生错误的连接,所以TCP是一个可靠的传输协议。
因此,我们可以利用TCP三次握手原理进行主机存活的探测。当向目标主机 直接发送ACK数据包时,如果目标主机存活,就会返回一个RST数据包以终止这 个不正常的TCP连接。也可以发送正常的SYN数据包,如果目标主机返回
SYN/ACK或者RST数据包,也可以证明目标主机为存活状态。其工作原理主要依 据目标主机响应数据包中flags字段,如果flags字段有值,则表示主机存活,该字 段通常包括SYN 、FIN 、ACK 、PSH 、RST 、URG六种类型。SYN表示建立连
接,FIN表示关闭连接,ACK表示应答,PSH表示包含DATA数据传输,RST表示 连接重置,URG表示紧急指针。
现在来编写一个利用TCP实现的活跃主机扫描程序,这个程序有很多种方式 可以实现,首先借助Scapy库来完成。在安装好Scapy的终端输入Scapy运行程序。 设定远程IP地址为39.xx.xx.238 ,flag标志为A表示给目标主机发送ACK应答数据 包,通过sr1() 函数将构造好的数据包发出。相关代码如下所示:
>>> ip=IP()
>>> tcp=TCP()
>>> r=(ip/tcp)
>>> r[IP] .dst="39 .xx .xx .238"
>>> r[TCP] .flags="A"
>>> a=sr1(r)
>>> a .display()
通过a.display() 函数查看目标主机的返回数据包信息,此时可以发现flags 标志位为R ,表示远程主机给源主机发送了一个REST 。由此可以验证远程目标主 机为存活状态。响应结果如下所示。
1)导入程序代码所应用到的模块:time 、optparse 、random和scapy 。time模 块主要用于产生延迟时间,optparse用于生成命行参数,random模块用于生成随机 的端口,scapy用于以TCP发送请求以及接收应答数据,例如:
import time from optparse import OptionParser from random import randin t from scapy .all import * |
2)利用optparse模块生成命令行参数化形式,对用户输入的参数进行接收和 批量处理,最后将处理后的IP地址传入Scan() 函数。 |
def main() :
usage = "Usage: %prog -i <ip address>" # 输出帮助信息
parse = OptionParser(usage=usage)
parse .add_option("-i", '--ip ', type="string", dest="target IP", help=
"specify the IP address") # 获取网段地址
options, args = parse .parse_args() #实例化用户输入的参数
if '- ' in options .target IP:
# 代码举例:192 .168 .1 .1-120
# 通过“ -”进行分隔,把192 .168 .1 .1和120进行分离
# 把192.168.1.1通过“,”进行分隔,取最后一个数作为range函数的start,然后把 120+1作为range函数的stop
# 这样循环遍历出需要扫描的IP地址
for i in range(in t(options .target IP .split( '- ')[0] .split( ' . ')[3]), in t (options .target IP .split( '- ')[1]) + 1) :
Scan(options .target IP .split( ' . ')[0] + ' . ' + options .target IP .split ( ' . ')[1] + ' . ' + options .target IP .split( ' . ')[2] + ' . ' + str(i))
else:
Scan(options .target IP)
if __name__ == '__main__ ' :
main()
3)Scan() 函数,通过调用TCP将构造好的请求包发送到目的地址,并根据 目的地址的响应数据包中flags字段值判断主机是否存活。若flags字段为R ,其整 型数值为4时表示接收到了目标主机的REST , 目标主机为存活状态,打印 出“xx.xx.xx.xx Host is up” ,否则为不存活主机,打印出“xx.xx.xx.xx Host is down”。 |
def Scan(ip) : try: dport = random .randin t(1, 65535) #随机目的端口 packet = IP(dst=ip)/TCP(flags="A",dport=dport) #构造标志位为ACK的数据包 response = sr1(packet,timeout=1 .0, verbose=0) if response: if in t(response[TCP] .flags) == 4: #判断响应包中是否存在RST标志位 time .sleep(0 .5) print(ip + ' ' + "is up") else: print(ip + ' ' + "is down") else: print(ip + ' ' + "is down") except : pass |
运行效果如下所示。
同时,可以打开Wireshark软件进行流量监听,根据抓到的数据流量可以分 析,源主机向目标主机发送ACK请求,当主机存活时, 目标主机会发送一个
REST的应答数据包,效果如图4-3所示。
图4-3 向目标主机发送ACK请求时的监听效果
UDP(User Datagram Protocol ,用户数据报协议)是一种利用IP提供面向无 连接的网络通信服务。UDP会把应用程序发来的数据,在收到的一刻立即原样发 送到网络上。即使在网络传输过程中出现丢包、顺序错乱等情况时,UDP也不会 负责重新发送以及纠错。当向目标发送一个UDP数据包之后, 目标是不会发回任 何UDP数据包的。不过,如果目标主机处于活跃状态,但是目标端口是关闭状态 时,会返回一个ICMP数据包,这个数据包的含义为unreachable 。如果目标主机不 处于活跃状态,这时是收不到任何响应数据的。利用UDP原理可以实现探测存活 主机。
现在来编写一个利用UDP实现的活跃主机的扫描程序,首先借助Scapy库来完 成。在安装好Scapy的终端输入Scapy运行程序。设定远程IP地址为39.xx.xx.238,
端口dport可为任意值,此处将dport设为7345 ,通过sr1() 函数将构造好的数据包 发出。相关代码如下所示:
>>> udp=UDP()
>>> r = (ip/udp)
>>> r[IP] .dst="192 .168 .19 .141"
>>> r[UDP] .dport=734
>>> a=sr1(r)
如果目标主机处于存活状态,则会接收到目标主机的应答信息,可通过
a.display() 函数查看数据包信息。可以查看到返回的信息中存在ICMP的应答信 息,“code=port-unreachable”表示目标端口不可达。由此可以验证远程目标主机为 存活状态。若目标主机不为存活状态,则不会收到目标主机的响应数据包。响应 结果如下所示。
根据以上UDP发现存活主机的原理,我们可以编写相应的Python工具进行实 现,具体过程如下所示:
1)导入程序代码所应用到的模块:time 、optparse 、random和scapy 。time模 块主要用于产生延迟时间,optparse模块用于生成命令行参数,random模块用于生 成随机的端口,scapy模块用于以UDP发送请求以及接收应答数据。
#!/usr/bin/python import time from optparse import OptionParser from random import randin t from scapy .all import * |
2)利用optparse模块生成命令行参数化形式,对用户输入的参数进行接收和 批量处理,最后将处理后的IP地址传入Scan() 函数。 |
def main() :
usage = "Usage: %prog -i <ip address>" # 输出帮助信息
parse = OptionParser(usage=usage)
parse .add_option("-i", '--ip ', type="string", dest="target IP", help=
"specify the IP address") # 获取网段地址
options, args = parse .parse_args() #实例化用户输入的参数
if '- ' in options .target IP:
# 代码举例:192 .168 .1 .1-120
# 通过“ -”进行分隔,把192 .168 .1 .1和120进行分离
# 把192.168.1.1通过“,”进行分隔,取最后一个数作为range函数的start,然后把 120+1作为range函数的stop
# 这样循环遍历出需要扫描的IP地址
for i in range(in t(options .target IP .split( '- ')[0] .split( ' . ')[3]), in t (options .target IP .split( '- ')[1]) + 1) :
Scan(options .target IP .split( ' . ')[0] + ' . ' + options .target IP .split ( ' . ')[1] + ' . ' + options .target IP .split( ' . ')[2] + ' . ' + str(i))
else:
Scan(options .target IP)
if __name__ == '__main__ ' :
main()
3)Scan() 函数,通过调用UDP将构造好的请求包发送到目的地址,并根据 是否接收到目标主机的响应数据包判断主机的存活状态。若接收到响应数据包, proto字段整型数据为1时,则代表目标主机为存活状态,打印出“xx.xx.xx.xx Host is up” ,否则为不存活主机,打印出“xx.xx.xx.xx Host is down”。 |
def Scan(ip) : try: dport = random .randin t(1, 65535) packet = IP(dst=ip)/UDP(dport=dport) response = sr1(packet,timeout=1 .0, verbose=0) response: if in t(response[IP] .proto) == 1: time .sleep(0 .5) print(ip + ' ' + "is up") else: print(ip + ' ' + "is down") else: print(ip + ' ' + "is down") except : pass |
运行效果如下所示。
同时,可以打开Wireshark软件进行流量监听,根据抓到的数据流量可以分 析,源主机向目标主机发送UDP数据包,当主机存活时, 目标主机会发送一
个“Destination unreachableb(port unreachable )” 的应答数据包,效果如图4-4所 示。
图4-4 向目标主机发送UDP数据包时的监听效果
对于TCP 、UDP主机发现,同样可以借助Nmap库来实现。这里需要用到
Nmap的-sT和-PU两个参数。详细的代码过程这里不再赘述,读者可在4.2.1节的基 础上进行修改,所需修改代码部分如下所示:
result = nm .scan(hosts=target IP, arguments= '-sT ') |
TCP主机发现的测试命令及效果如下: |
result = nm .scan(hosts=target IP, arguments= '-PU ') |
UDP主机发现的测试效果如下:
4.2.3 基于ARP的主机发现
ARP协议(地址解析协议)属于数据链路层的协议,主要负责根据网络层地 址(IP)来获取数据链路层地址(MAC)。
以太网协议规定,同一局域网中的一台主机要和另一台主机进行直接通信, 必须知道目标主机的MAC地址。而在TCP/IP中,网络层只关注目标主机的IP地 址。这就导致在以太网中使用IP协议时,数据链路层的以太网协议接收到的网络 层IP协议提供的数据中,只包含目的主机的IP地址。于是需要ARP协议来完成IP 地址到MAC地址的转换。假设我们当前的以太网结构如图4-5所示。
图4-5 以太网结构
在上述以太网结构中,假设PC1想与PC3通信,步骤如下。
1)PC1知道PC3的IP地址为192.168.1.3,然后PC1会检查自己的APR缓存表中 该IP是否有对应的MAC地址。
2)如果有,则进行通信。如果没有,PC1就会使用以太网广播包来给网络上 的每一台主机发送ARP请求,询问192.168.1.3的MAC地址。ARP请求中同时也包 含了PC1的IP地址和MAC地址。以太网内的所有主机都会接收到ARP请求,并检 查是否与自己的IP地址匹配。如果不匹配,则丢弃该ARP请求。
3)PC3确定ARP请求中的IP地址与自己的IP地址匹配,则将ARP请求中PC1 的IP地址和MAC地址添加到本地ARP缓存中。
4)PC3将自己的MAC地址发送给PC1。
5)PC1收到PC3的ARP响应时,将PC3的IP地址和MAC地址都更新到本地 ARP缓存表中。
本地ARP缓存表是有生存周期的,生存周期结束后,将再次重复上面的过 程。
当目标主机与我们处于同一以太网的时候,利用ARP进行主机发现是一个最 好的选择。因为这种扫描方式快且精准。现在我们借助Scapy来编写ARP主机发现 脚本,通过脚本对以太网内的每个主机都进行ARP请求。若主机存活,则会响应 我们的ARP请求,否则不会响应。因为ARP涉及网络层和数据链路层,所以需要 使用Scapy中的Ether和ARP 。Scapy中的ARP参数如下所示:
Scapy中的Ether参数如下所示:
这里介绍一下脚本中所使用的参数。Ether中src表示源MAC地址,dst表示目 的MAC地址。ARP中op代表消息类型,1为ARP请求,2为ARP响应,hwsrc和psrc 表示源MAC地址和源IP地址,pdst表示目的IP地址。接下来我们编写ARP主机发 现脚本。
1)写入脚本信息,导入相关模块:
U
#!/usr/bin/python3
# -*- coding: utf-8 -*-
import
import
import
from scapy .all import *
2)编写本机IP地址和MAC地址获取函数,通过正则表达式来进行获取:
# 取IP地址和MAC地址函数 def HostAddress(iface) : # os .popen执行后返回执行结果 ipData = os .popen( 'ifconfig '+iface) # 对ipData进行类型转换,再用正则进行匹配 dataLine = ipData .readlines() # re .search利用正则匹配返回第一个成功匹配的结果,存在结果则为true # 取MAC地址 if re .search( '\w\w:\w\w:\w\w:\w\w:\w\w:\w\w ',str(dataLine)) : # 取出匹配的结果 MAC = re .search( '\w\w:\w\w:\w\w:\w\w:\w\w:\w\w ',str(dataLine)) .group(0) # 取IP地址 if re .search(r '((2[0-4]\d|25[0-5]|[01]?\d\d?)\ .){3}(2[0-4]\d|25[0-5]|[01]? \d\d?) ',str(dataLine )) : IP = re .search(r '((2[0-4]\d|25[0-5]|[01]?\d\d?)\ .){3}(2[0-4]\d|25[0-5]| [01]?\d\d?) ',str(dataLine)) .group(0) # 将IP和MAC通过元组的形式返回 addressInfo = (IP,MAC) return addressInfo |
3)编写ARP探测函数,根据本机的IP地址和MAC地址信息, 自动生成目标 进行探测并把结果写入文件: |
# ARP扫描函数
def ArpScan(iface= 'eth0 ') :
# 通过HostAddres返回的元组取出MAC地址
mac = HostAddress(iface)[1]
# 取出本机IP地址
ip = HostAddress(iface)[0]
# 对本机IP地址进行分隔并作为依据元素,用于生成需要扫描的IP地址
ipSplit = ip .split( ' . ')
# 需要扫描的IP地址列表
ipList = []
# 根据本机IP生成IP扫描范围
for i in range(1,255) :
ipItem = ipSplit[0] + ' . ' + ipSplit[1] + ' . ' + ipSplit[2] + ' . ' + str(i) ipList.append(ipItem)
'''
发送ARP包
因为要用到OSI的二层和三层,所以要写成Ether/ARP。
因为最底层用到了二层,所以要用srp()发包
'''
result=srp(Ether(src=mac,dst= 'FF:FF:FF:FF:FF:FF ')/ARP(op=1,hwsrc=mac,hwdst= '00:00:00:00:00:00 ',pds t=ipList),iface=iface,timeout=2,verbose=False)
# 读取result中的应答包和应答包内容
resultAns = result[0] .res
# 存活主机列表
liveHost = []
# number 为接收到应答包的总数
number = len(resultAns)
print("=====================")
print("ARP 探测结果")
print("本机IP地址 :" + ip)
print("本机MAC地址 :" + mac)
print("=====================")
for x in range(number) :
IP = resultAns[x][1][1] .fields[ 'psrc ']
MAC = resultAns[x][1][1] .fields[ 'hwsrc ']
liveHost.append([IP,MAC])
print("IP:" + IP + "\n\n" + "MAC:" + MAC )
print("=====================")
# 把存活主机IP写入文件
resultFile = open("result","w")
for i in range(len(liveHost)) :
resultFile .write(liveHost[i][0] + "\n")
resultFile .close()
4)编写main 函数,利用optparse模块生成命令行参数化形式: |
if __name__ == '__main__ ' : parser = optparse .OptionParser( 'usage: python %prog -i interfaces \n\n ' 'Example: python %prog -i e th0\n ') # 添加网卡参数 -i parser .add_option( '-i ', '--iface ',dest= 'iface ',default= 'eth0 ',type= 'string ', help= 'interfaces name ') (options, args) = parser .parse_args() ArpScan(options .iface) |
这样我们的ARP主机发现脚本功能就完成了。脚本测试结果下所示:
查看结果文件如下所示:
提示:普通用户运行时需要进行sudo ,否则会出现Operation not permitted提 醒!
下面介绍通过Nmap库来实现ARP主机发现,这里需要用到Nmap的-PR参数。 详细的过程此处不再赘述,读者可在4.2.1节的基础上进行修改,所需修改的代码 部分如下所示:
U
result = nm .scan(hosts=target IP, arguments= '-PR ')
ARP主机发现,测试效果如下所示:
4.2.4 端口探测
端口是设备与外界通信交流的接口。如果把服务器看作一栋房子,那么端口 就是可以进出这栋房子的门。真正的房子只有一个或几个门,但是服务器可以至 多有65 536个门。不同的端口(门)可以指向不同的服务(房间)。
例如,我们经常浏览网页时涉及的WWW服务用的是80号端口,上传或下载 文件时的FTP服务用的是21号端口,远程桌面用的是3389号端口。
所以入侵者想要获取到房子(服务器)的控制权,势必要先从一个门进入一 个房间,再通过这个房间控制整个房子。那么服务器开了几个端口,端口后面的 服务是什么,这些都是十分重要的信息,可以为入侵者制定详细的入侵计划提供 依据。因此在信息搜集阶段,端口开放情况的扫描就显得尤为重要。
下面将通过Python的Socket模块来编写一个简便的多线程端口扫描工具。
1)导入脚本信息以及相关的模块:
#!/usr/bin/python3 # -*- coding:utf-8 -*- | |
import import import import import | sys socket optparse threading queue |
2)编写一个端口扫描类,继承threading.Thread 。这个类需要传递3个参数, 分别是目标IP 、端口队列、超时时间。通过这个类创建多个子线程来加快扫描进 度: |
# 端口扫描类,继承threading .Thread
class PortScaner(threading .Thread) :
# 需要传入端口队列、 目标IP,探测超时时间
def __init__(self, portqueue, ip, timeout=3) :
threading .Thread.__init__(self)
self._portqueue = portqueue
self._ip = ip
self._timeout = timeout
def run(self) :
while True:
# 判断端口队列是否为空
if self._portqueue .empty() :
# 端口队列为空,说明已经扫描完毕,跳出循环
break
# 从端口队列中取出端口,超时时间为1s
port = self._portqueue .get(timeout=0 .5)
try:
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s .settimeout(self._timeout)
result_code = s .connect_ex((self._ip, port))
# sys .stdout.write("[%d]Scan\n" % port)
# 若端口开放,则会返回0
if result_code == 0:
sys .stdout.write("[%d] OPEN\n" % port)
except Exception as e:
print(e)
finally:
s .close()
3)编写一个函数,根据用户的参数来指定目标IP 、端口队列的生成以及子线 程的生成,同时能支持单个端口的扫描和范围端口的扫描: |
def StartScan(targetip, port, threadNum) : # 端口列表 port List = [] portNumb = port # 判断是单个端口还是范围端口 if '- ' in port : for i in range(in t(port.split( '- ')[0]), in t(port.split( '- ')[1])+1) : port List.append(i) else: port List.append(in t(port)) # 目标IP地址 ip = targetip # 线程列表 threads = [] # 线程数量 threadNumber = threadNum # 端口队列 portQueue = queue .Queue() # 生成端口,加入端口队列 for port in port List : portQueue .put(port) for t in range(threadNumber) : threads .append(PortScaner(portQueue, ip, timeout=3)) # 启动线程 for thread in threads: thread.start() # 阻塞线程 for thread in threads: thread.join() |
4)编写主函数来制定参数的规则: |
if __name__ == '__main__ ' : parser = optparse .OptionParser( 'Example: python %prog -i 127 .0 .0 .1 -p 80 \n python %prog -i 127 .0 .0 .1 -p 1-100\n ') # 目标IP参数-i parser .add_option( '-i ', '--ip ', dest= 'target IP ',default= '127 .0 .0 .1 ', type= 'string ',help= 'target IP ') # 添加端口参数-p parser .add_option( '-p ', '--port ', dest= 'port ', default= '80 ', type= 'string ', help= 'scann port ') # 线程数量参数-t parser .add_option( '-t ', '--thread ', dest= 'threadNum ', default=100, type= 'in t ', help= 'scann thread number ') (options, args) = parser .parse_args() StartScan(options .target IP, options .port, options .threadNum) |
这里打开了一个CentOS7的服务器作为目标,IP地址为192.168.61.62 ,服务器
开放了22 、80 、3306号端口,然后利用编写好的程序脚本对服务器进行端口扫 描,扫描结果如下所示:
再对服务器进行范围端口的扫描,如下所示:
对于开放端口探测,同样也可以借助Nmap库来实现,这里需要用到Nmap的- p参数。详细的代码此处不再赘述,读者可在4.2.1节的基础上进行修改。所需修 改的代码部分如下所示:
U
result = nm .scan(hosts=target IP, arguments= '-p '+str(targetPort))
测试效果如下:
4.2.5 服务识别
在渗透测试的过程中,服务识别是一个很重要的环节。如果能识别出目标主 机的服务、版本等信息,对于渗透测试将有重要帮助。对于入侵者来说,发现这 些运行在目标上的服务,就可以利用这些软件上的漏洞入侵目标;对于网络安全 的维护者来说,也可以提前发现系统的漏洞,从而预防这些入侵行为。
很多扫描工具都采用了一种十分简单的方式,就是根据端口判断服务类型,
因为通常常见的服务都会运行在固定的端口上(见表4-1~表4-7),例如,FTP服 务总会运行在21号端口上,HTTP服务运行在80号端口上。但是利用该方式进行
服务识别存在明显的缺陷,很多人会将服务运行在其他端口上,例如,将本来运 行在23号端口上的Telnet运行在22号端口上,这样就会误以为这是一个SSH服务, 进而增加不必要的工作量。由于很多软件在连接之后都会提供一个表明自身信息 的banner ,在这里我们可以根据获取的banner信息对运行的服务类型进行判断,进 而可以确定开放端口对应的服务类型及版本号。
表4-1 文件共享服务端口
表4-2 远程连接服务端口
表4-3 Web应用服务端口
表4-4 数据库服务端口
表4-5 邮件服务端口
表4-6 网络常见协议端口
表4-7 特殊服务端口
因此,可以向目标开放的端口发送探针数据包,根据目标主机返回的banner 信息与存储总结的banner信息进行比对,进而确定运行的服务类型。著名的Nmap 扫描工具就是采用了这种方法,它包含一个十分强大的banner数据库,而且这个 库仍在不断完善中。接下来按照上面介绍的思路来编写对目标服务进行扫描的程 序。
1)导入程序代码所应用到的模块:time 、optparse 、socket和re 。time模块主 要用于产生延迟时间,optparse模块用于生成命行参数,socket模块用于产生TCP 请求,re模块为正则表达式模块,与指纹信息进行有效匹配,进而确定服务类
型。SIGNS为指纹库,用于对目标
from optparse import OptionParser
import
import
import
SIGNS = (
# 协议 | 版本 | 关键字
b 'FTP |FTP |^220 .*FTP ',
b 'MySQL |MySQL |mysql_native_password ',
b 'oracle-https |^220- ora ',
b 'Telnet|Telnet|Telnet ',
b 'Telnet|Telnet|^\r\n%connection closed by remote host!\x00$ ',
b 'VNC |VNC |^RFB ',
b 'IMAP |IMAP |^\* OK .*?IMAP ',
b 'POP |POP |^\+OK .*? ',
b 'SMTP |SMTP |^220 .*?SMTP ',
b 'Kangle |Kangle |HTTP .*kangle ',
b 'SMTP |SMTP |^554 SMTP ',
b 'SSH |SSH |^SSH- ',
b 'HTTPS |HTTPS |Location: https ',
b 'HTTP |HTTP |HTTP/1 .1 ',
b 'HTTP |HTTP |HTTP/1 .0 ',
)
def main() :
parser = OptionParser("Usage:%prog -i <target host> ") # 输出帮助信息
parser .add_option( '-i ',type= 'string ',dest= 'IP ',help= 'specify target host ') # 获取IP地址参数
parser .add_option( '-p ', type= 'string ', dest= 'PORT ', help= 'specify target
host ') # 获取IP地址参数
options,args = parser .parse_args()
ip = options .IP
port = options .PORT
print("Scan report for "+ip+"\n")
for line in port.split( ', ') :
request(ip,line)
time .sleep(0 .2)
print("\nScan finished!. . .\n")
if __name__ == "__main__" :
try:
main()
except Keyboard Interrupt :
print("interrupted by user, killing all threads . . .")
3)在request() 函数中,首先调用sock.connect() 函数探测目标主机端口 是否开放,如果端口开放,则利用sock.sendall() 函数将PROBE探针发送给目标 端口。sock.recv() 函数用于接收返回的指纹信息,并将指纹信息及端口发送到 regex() 函数。 |
def request(ip,port) : response = ' ' PROBE = 'GET / HTTP/1 .0\r\n\r\n ' sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.settimeout(10) result = sock.connect_ex((ip, in t(port))) if result == 0: try: sock.sendall(PROBE .encode()) response = sock.recv(256) if response: regex(response, port) except(ConnectionResetError,socket.timeout) : pass else: pass sock.close() |
4)利用re.search() 函数将返回的banner信息与SIGNS包含的指纹信息进行 正则匹配,并将匹配到的结果输出。如果没有在SIGNS中找到相匹配的信息,则 输出Unrecognized。 |
def regex(response, port) :
text = ""
if re .search(b '<title>502 Bad Gateway ', response) :
proto = {"Service failed to access !!"}
for pattern in SIGNS:
pattern = pattern .split(b ' | ')
if re .search(pattern[-1], response, re .IGNORECASE) :
proto = "["+port+"]" + " open " + pattern[1] .decode()
break
else:
proto = "["+port+"]" + " open " + "Unrecognized"
print(proto)
|
端口服务版本的识别实现起来也是比较困难的, 目前市面上能提供相关服务 的软件也非常多,而且每个软件也会出现多个版本。下面借助Nmap库来实现对主 机端口服务的探测,这里还需要用到Nmap的-sV参数。详细的代码此处就不再赘 述,读者可在4.2.1节的基础上进行修改,所需修改的代码部分如下所示: |
(2) print("[{} :{}] : [{} :{}]" .format(targetPort, port_infor[ 'state '] , port_ infor[ 'name '], port_infor[ 'product ']) |
测试效果如下:
4.2.6 系统识别
识别出目标主机操作系统的类型和版本,可以大量减少不必要的测试成本, 缩小测试范围,更精确地针对目标进行渗透测试。
但是判断目标的操作系统并非一件简单的事情。因为现在的操作系统类型繁 多,仅Windows和Linux就有包含了许多衍生系统,同时,现今的防火墙、路由 器、智能设备等都有其自带的操作系统,所以需要精确判断目标操作系统的类型 并非易事。 目前主要通过“指纹识别” 的方式来对目标的操作系统来进行猜测。检 测的方法一般分为两种:主动式探测和被动式探测。
(1)主动式探测:向目标主机发送一段特定的数据包,根据目标主机对数 据包做出的回应进行分析,判断目标主机中可能的操作系统类型。与被动式探测 相比,主动式获取的结果更加精确,但也容易触发目标安全系统的警报。
(2)被动式探测:通过工具嗅探、记录、分析数据包流。根据数据包信息 来分析目标主机的操作系统。与主动式探测相比,被动式探测的结果虽然不如主 动式探测精确,但是不容易被目标主机安全系统察觉。
主机识别的技术原理:Windows操作系统与Linux操作系统的TCP/IP实现方式 并不相同,导致两种系统对特定格式的数据包会有不同的响应结果,包括响应数 据包的内容、响应时间等,形成了操作系统的指纹。通常情况下,可在对目标主 机进行ping操作后,依据其返回的TTL值对系统类型进行判断,Windows系统的 TTL起始值为128 ,Linux系统的TTL起始值为64 ,且每经过一跳路由,TTL值减 1。
Windows的TTL返回值如下:
Linux的TTL返回值如下:
根据按照目标主机返回的响应数据包中的TTL值来判断操作系统类型的原 理,可编写Python程序实现自动化,详细过程如下所示。
1)导入程序代码所应用的模块:optparse 、os和re 。optparse用于生成命行参 数;os用于执行系统命令;re为正则表达式模块,用于匹配返回的TTL值。
#!/usr/bin/python3 .7 #!coding:utf-8 from optparse import OptionParser import os import re |
2)利用optparse模块生成命令行参数化形式,对用户输入的参数进行接收和 批量处理,最后将处理后的IP地址传入ttl_scan() 函数。 |
def main() : parser = OptionParser("Usage:%prog -i <target host> ") # 输出帮助信息 parser .add_option( '-i ',type= 'string ',dest= 'IP ',help= 'specify target host ') # 获取IP地址参数 options,args = parser .parse_args() ip = options .IP ttl_scan(ip) if __name__ == "__main__" : main() |
3)调用os.popen() 函数执行ping命令,并将返回的结果通过正则表达式识 别,提取出TTL值。当TTL值小于等于64时,操作系统为Linux类型,输 出“xx.xx.xx.xx is Linux/UNIX” ,否则输出“xx.xx.xx.xx is Windows”。 |
def ttl_scan(ip) :
ttlstrmatch = re .compile(r 'ttl=\d+ ')
ttlnummatch = re .compile(r '\d+ ')
result = os .popen("ping -c 1 "+ip)
res = result.read()
for line in res .splitlines() :
result = ttlstrmatch.findall(line)
if result :
ttl = ttlnummatch.findall(result[0])
if in t(ttl[0]) <= 64: # 判断目标主机响应包中TTL值是否小于等于64
print("%s is Linux/UNIX"%ip) # TTL≤64时为Linux/UNIX系统 else:
print("%s is Windows"%ip) # 反之为Windows系统
else:
pass
运行结果如下: |
当然,这里也可以借助Nmap库来实现操作系统类型识别的功能,通过Nmap 的-O参数对目标主机操作进行系统识别,代码如下所示: |
result = nm .scan(hosts=target IP, arguments= '-O ') |
借助Nmap库我们可以很轻松地完成一个主动式系统探测工具,而且其判断的 结果在实际运用中也非常具有参考价值。
运行结果如下:
4.2.7 敏感目录探测
资源发现属于信息搜集的一部分,善于发现隐藏的信息,如隐藏目录、隐藏 文件等,可提高渗透测试的全面细致性。本节将用Python实现敏感目录发现。在 渗透测试过程中,资源发现是极其重要的一环。具备好的资源发现能力能够令整 个工作事半功倍。
在渗透测试过程中进行目录扫描是很有必要的,例如,当发现开发过程中未 关闭或忘记关闭的页面,可能就会发现许多可以利用的信息。下面我们编写一个 基于字典的目录扫描脚本。
1)要进行网页目录扫描,需要进行网页访问,所以先导入requests模块备 用,然后等待用户输入url和字典:
import requests headers = { "User-Agent" : "Mozilla/5 .0 (Windows NT 6 .1; WOW64; rv:6 .0) Gecko/20100101 Firefox/6 .0" } url = input("url : ") txt = input( 'php .txt ') |
2)当用户没有输入字典时,默认打开根目录的php.txt ,后将字典中的内容 放进队列中: |
url_list = [] if txt == "" : txt = "php .txt" try: with open(txt, 'r ') as f : for a in f : a = a .replace( '\n ', '') url_list.append(a) f.close() except : print("error!") |
3)将队列中的内容拼接到url中组成需要验证的地址,通过返回值判断是否 存在此目录:
|
for li in url_list : conn = "http://" + url +"/"+ li try: response = requests .get(conn,headers = headers) print("%s %s" % (conn, response)) except e: print( '%s-------------%s ' %(conn, e .code)) |
至此,一个简单的目录扫描脚本就完成了。运行效果如下所示: