任何一个靠谱的网络攻击都是起步于侦察的。
攻击者必须在挑选并确定利用目标中的漏洞之前找到目标在哪里有漏洞。
编写一个扫描目标主机开放的TCP端口的侦察小脚本。
为了与TCP端口进行交互,我们要先建立TCP套接字。
Python提供了BSD套接字的接口。
BSD套接字提供了一个应用编程接口,使程序员能编写在主机之间进行网络通信的应用程序。
通过一系列套接字API函数,我们可以创建、绑定监听、连接,或在TCP/IP套接字上发送数据。
大多数能访问互联网的应用使用的都是TCP协议。
例如,在目标组织中,Web服务器可能位于TCP80端口,电子邮件服务器在TCP25端口,FTP服务器在TCP21端口。
要连接目标组织中的任一服务器,攻击者必须知道与服务器相关联的IP地址和TCP端口。
所有成功的网络攻击一般都是从端口扫描来开序幕的。
有一种类型的端口扫描会向一系列常用的端口发送TCP SYN数据包,并等待TCP ACK响应——这能让我们确定这个端口是开放的。
与此相反,TCP连接扫描是使用完整的三次握手来确定服务器或端口是否可用的。
TCP全连接扫描
开始编写自己用的TCP全连接扫描来识别主机的TCP端口扫描器。
要导入Python的BSD套接字API实现。
套接字API会为我们提供一些在实现TCP端口扫描程序时有用的函数。
要深入的了解请查看文档.
为了更好的了解TCP端口扫描器的工作原理,我们将脚本分成五个独立的步骤,分别为它们编写Python代码。
首先输入一个主机名和用逗号分割的端口列表,并予以扫描。
接下来将主机转换成IPv4互联网地址。对列表中的每个端口,我们都会链接目标地址和该端口。
最后,为了确定在该端口上运行什么服务,我们将发送垃圾数据并读取由具体应用发回的Banner。
在第一步中,从用户那里获得主机名和端口。
为了做到这一点,我们在程序中使用optparse库解析命令行参数。
调用optparse.OptionParser([usage message])会生成一个参数解析器(option parser)类的实例。
接着,在parser.add_option中指定这个脚本具体要解析哪个命令行参数。
e.g. 一个快速解析要扫描的目标主机名和端口的方法
import optparse
parser = optparse.OptionParser('usage %prog -H <target host> -p <target port>')
parser.add_option('-H', dest='tgtHost', type='string', help='specify target host')
parser.add_option('-p', dest='tgtPort', type='string', help='specify target port')
(options, args) = parser.parse_args()
tgtHost = options.tgtHost
tgtPort = options.tgtPort
if tgtHost == None or tgtPort == None:
print(parser.usage)
exit(0)
else:
print(tgtHost)
print(tgtPort)
接下来我么要生成俩个个函数: connScan和portScan.
portScan函数以参数的形式接受主机名和目标端口列表。
它首先会尝试用gethostbyname()函数确定主机名对应的IP地址。
接下来,它会使用connScan函数输出主机名字(或IP地址),并使用connScan()函数尝试逐个连接我们要连接的每个端口。
connScan函数接受两个参数:tgtHost和tgtPort,它会去尝试建立与目标主机端口的连接。
如果成功,connScan将打印出一个端口开放的消息。如果不成功,它会打印出端口关闭的消息。
from socket import *
def connScan(tgtHost, tgtPort):
try:
connSkt = socket(AF_INET, SOCK_STREAM)
connSkt.connect((tgtHost, tgtPort))
print('[+] %d/tcp open' % tgtPort)
connSkt.close()
except Exception:
print('[-] %d/tcp closed' % tgtPort)
def portScan(tgtHost, tgtPorts):
try:
tgtIP = gethostbyname(tgtHost)
except:
print("[-] Cannot resolve '%s': Unknown host" % tgtHost)
return
try:
tgtName = gethostbyaddr(tgtHost)
print("\n[+] Scan Results for: " + tgtName[0])
except:
print("\n[-] Scan Results for: " + tgtIP)
setdefaulttimeout(1)
for tgtPost in tgtPorts:
print("Scannint port " + tgtPost)
connScan(tgtHost, tgtPost)
抓取应用的Banner
为了抓取目标主机上应用的Banner,必须现在connScan函数中插入一些新增的代码。
找到开放端口后,我们向它发送一个数据传并等待响应。
根据收集到的响应,就能推断出目标主机和端口上运行的应用。
from socket import *
def connScan(tgtHost, tgtPort):
try:
connSkt = socket(AF_INET, SOCK_STREAM)
connSkt.connect((tgtHost, tgtPort))
connSkt.send('Hello Python\r\n')
results = connSkt.recv(1024)
print('[+] {}/tcp open'.format(tgtPort))
print('[+] ' + str(results))
connSkt.close()
except:
print('[-] {}/tcp closed'.format(tgtPort))
def portScan(tgtHost, tgtPorts):
try:
tgtIP = gethostbyname(tgtHost)
except:
print("[-] Cannot resolve '{}': Unknown host".format(tgtHost))
return
try:
tgtName = gethostbyaddr(tgtIP)
print("\n[+] Scan Results for: " + tgtName[0])
except:
print('\n[+] Scan Results for: ' + tgtIP)
setdefaulttimeout(1)
for tgtPort in tgtPorts:
print('Scanning port ' + tgtPort)
connScan(tgtHost, tgtPort)
def main():
import optparse
parser = optparse.OptionParser('usage %prog -H <target host> -p <target port>')
parser.add_option('-H', dest='tgtHost', type='string', help='specify target host')
parser.add_option('-p', dest='tgtPort', type='string', help='specify target port')
(options, args) = parser.parse_args()
tgtHost = options.tgtHost
tgtPorts = (options.tgtPort).split(' ')
if tgtPorts[0] == None or tgtHost == None:
print('[-] You must specify a target host and port[s].')
exit(0)
portScan(tgtHost, tgtPorts)
if __name__ == '__main__':
main()
<<<<<<< HEAD
=======
>>>>>>> origin/master
线程扫描
根据套接字中timeout变量的值,每扫描一个套接字都会花费几秒钟。
这看上去微不足道,但如果我们要扫描多个主机端口,时间总量就会成倍增加。
理想情况下,我们希望能同时扫描多个套接字,而不是一个一个地进行扫描。
这时,我们必须引入Python线程,线程是一种能提供这类同时执行多项任务的方法。
具体到我们这个扫描器,我们要修改的是portScann()函数中迭代循环里的代码。
from threading import *
for tgtPort in tgtPorts:
t = Thread(target=connScan, args=(tgtHost, int(tgtPort)))
t.start()
这让我们的速度有了显著的提升,但这又有一个缺点。connScan()函数会在屏幕上打印一个输出。
如果多个线程同时打印输出,就可能会出现乱码和失序。
为了让一个函数获得完整的屏幕控制前,我们需要使用一个信号量(semaphore)。
一个简单的信号量就能阻止其他线程的运行。
注意,在打印输出前,我们用screenLock。acquire()执行一个加锁操作。
如果信号量还没被锁上,线程就有权继续运行,并输出到屏幕上。
如果信号量已经被锁定,我们只能等待,直到持有信号量的线程释放信号量。
通过利用信号量,我们现在能够确保在任何给定时间上只有一个线程可以打印屏幕。
在异常处理代码中,位于finally关键字的是在终止阻塞(其他线程)之前需要执行的代码。
from threading import *
from socket import *
screenLock = Semaphore(value=1)
def connScan(tgtHost, tgtPOrt):
try:
connSkt = socket(AF_INET, SOCK_STREAM)
connSkt.connect((tgtHost, tgtPOrt))
connSkt.send('something')
results = connSkt.recv(1024)
screenLock.acquire()
print('[+] {}/tcp open'.format(tgtPOrt))
print('[-] ' + str(results))
except:
screenLock.acquire()
print('[-] {}/tcp closed'.format(tgtPOrt))
finally:
screenLock.release()
<<<<<<< HEAD
connSkt.close()
=======
connSkt.close()
>>>>>>> origin/master
把其他所有的函数放入同一个脚本中,并添加一些参数解析代码,就有了最终的端口扫描脚本。
#!/usr/bin/python3
# -*- coding: utf-8 -*-
from socket import *
from threading import *
screenLock = Semaphore(value=1)
def connScan(tgtHost, tgtPort):
try:
connSkt = socket(AF_INET, SOCK_STREAM)
connSkt.connect((tgtHost, tgtPort))
connSkt.send('some information\r\n')
results = connSkt.recv(1024)
screenLock.acquire()
print('[+] {}/tcp open'.format(tgtPort))
print('[+] ' + str(results))
except:
screenLock.acquire()
print('[-] {}/tcp closed'.format(tgtPort))
finally:
screenLock.release()
connSkt.close()
def portScan(tgtHost, tgtPorts):
try:
tgtIP = gethostbyname(tgtHost)
except:
print("[-] Cannot resolve '{}': Unknown host".format(tgtHost))
return
try:
tgtName = gethostbyaddr(tgtIP)
print("\n[+] Scan Results for: " + tgtName[0])
except:
print('\n[+] Scan Results for: ' + tgtIP)
setdefaulttimeout(1)
for tgtPort in tgtPorts:
t = Thread(target=connScan, args=(tgtHost, tgtPort))
t.start()
def main():
import optparse
parser = optparse.OptionParser('usage %prog -H <target host> -p <target port>')
parser.add_option('-H', dest='tgtHost', type='string', help='specify target host')
parser.add_option('-p', dest='tgtPort', type='string', help='specify target port[s] separated by comma')
(options, args) = parser.parse_args()
tgtHost = options.tgtHost
tgtPorts = str(options.tgtPort).split(', ')
if tgtPorts[0] == None or tgtHost == None:
print(parser.usage)
exit(0)
portScan(tgtHost, tgtPorts)
if __name__ == '__main__':
main()
使用NMAP端口扫描代码
前面的例子是快速编写能进行TCP链接扫描的一个脚本。
这可能还不能够用,因为我们可能还要进行其他类型的扫描,例如,由Nmap工具包提供的ACK、RST、FIN或SYN-ACK扫描等。
实际的工业标准——Namp端口扫描工具包提供了大量功能。
这也引出一个问题,为什么不直接使用Nmap?这就是Python真正美妙的地方。
虽然Fyodor Vaskovich编写的Nmap中也能使用C和Lua编写的脚本,但是Nmap还能被非常好的很合到Python中。
Nmap可以生成基于XML的输出。这让我们能在Python脚本中使用Nmap的全部功能。在编写代码之前,你必须安装Python-Nmap。
安装好Python-Nmap之后,我们就可以将Nmap导入到现有的脚本中。
创建一个PortScanner()类对象,这使我们能用这个对象完成扫描操作。
PortScanner类有一个scan()函数,它可将目标和端口的列表作为参数输入,并对它们进行基本的Nmap扫描。
另外还可以把目标主机的地址/端口放入数组中备查,并打印出端口状态。
def nmapScan(tgtHost, tgtPort):
import nmap
nmScan = nmap.PortScanner()
nmScan.scan(tgtHost, tgtPort)
state = nmScan[tgtHost]['tcp'][int(tgtPort)]['state']
print('[+] ' + tgtHost + ' tcp/' + tgtPort + " " + state)
def main():
import optparse
parser = optparse.OptionParser('usage %prog -H <target host> -p <target port>')
parser.add_option('-H', dest='tgtHost', type='string', help='specify target host')
parser.add_option('-p', dest='tgtPort', type='string', help='specify target port[s] separated by comma')
(options, args) = parser.parse_args()
tgtHost = options.tgtHost
tgtPorts = str(options.tgtPort).split(', ')
if tgtPorts[0] == None or tgtHost == None:
print(parser.usage)
exit(0)
for tgtPort in tgtPorts:
nmapScan(tgtHost, tgtPort)
if __name__ == '__main__':
main()
more
- TCP SYN SCAN —— 也称为半开放扫描,这种类型的扫描发送一个SYN包,启动一个TCP会话,并等待响应的数据包。
如果收到的是一个reset包,表明端口是关闭的,而如果收到的是一个SYN/ACK包,则表示响应的端口是开放的。 - TCP NULL SCAN —— NULL扫描把TCP头中的所有标志位都设为NULL。如果受到的是一个RST包,则表示响应的端口是关闭的。
- TCP FIN SCAN —— TCP FIN 扫描发送一个表示拆除一个活动的TCP连接的FIN包,让对方关闭连接。
如果收到了一个RST包,则表示相应的端口是关闭的。 - TCP XMAS SCAN —— TCP XMAS扫描发送PSH、FIN、URG和TCP标志为被设为1的数据包。
如果受到了一个RST包,则表示响应的端口是关闭的。