巧用python和libnmapd,提取Nmap扫描结果
每当我进行内网渗透面对大量主机和服务时,我总是习惯使用自动化的方式从nmap扫描结果中提取信息。这样有利于自动化检测不同类型的服务,例如对web服务进行路径爆破,测试SSL/TLS服务使用的密钥或协议,以及其他有针对性的测试。
我在渗透测试中也会经常使用到IPthon或*nixshell,而这些又都能够通过Python来访问,无论是直接在脚本中使用、在REPL环境下使用,还是将代码写入到磁盘上然后通过shell命令访问都是非常好用的。
为了完成这些,libnmap库会提供很好的帮助。本文将会讲述一系列如何使用一行代码解析nmap扫描结果,其中会在Python环境中使用到libnmap里的NmapParser库,这个库可以很容易的帮助我们解析nmap的扫描结果。
我希望本文不仅仅是提供给你可以直接复制粘贴的代码,还可以了解到IPython也是渗透测试时一个非常好用的数据处理工具。
配置
解析nmap扫描结果的第一步是你要进行一次nmap扫描。我不打算在这里关注过多的细节部分,但是你想要直接使用本文的代码,你需要将扫描结构保存到一个xml文件中(-oX或者-oA)并且在开放端口上执行了服务侦测(-sV)和运行相关脚本(-sC)。
本文的命令假设你在一个PythonREPL环境如IPython并且安装libnmap模块(可以使用easy_install或pip安装)的环境下执行。
开始前,你需要设置下相应的环境,首先导入NmapParser模块并读入你的xml扫描结果文件(实例中名为”up_hosts_all_ports_fullscan.xml”位于当前工作目录下)
fromlibnmap.parserimportNmapParser nmap_report=NmapParser.parse_fromfile('up_hosts_all_ports_fullscan.xml')
本文的余下部分会包含一系列使用一行代码提取各种各样有用的信息。全部的示例都假设nmap扫描结果保存在一个如上所示的文件中。下面的会给出一些基本的示例代码,如果你想在IPython中直接运行它们,请先运行上面的代码,这样它会直接在控制台输出方便你的查看。我通常会先做好这一步,这样我就可以确保输出的数据跟预期的一样。
然后,你可以选择一个变量名并使用“=”将数据赋值给这个变量,这样你就可以在随后的代码中直接调用,或者将其写入到磁盘上以便shell命令使用。如果有些东西你想使用多次,可以粘贴一些代码段到Python脚本中,或者想加入一些更加复杂的逻辑但这样可能会使REPL环境难以处理,我会在最后一节中讲述如何快速的执行这些操作。
端口信息
开放指定端口号的主机
显示所有开放指定端口号的主机。生成一个包含主机地址(string)的列表。下面以443端口为例,你可以修改成你自己需要的值。
[a.addressforainnmap_report.hostsif(a.get_open_ports())and443in[b[0]forbina.get_open_ports()]]
开放端口数量
显示一系列主机开放端口的数量。生成一个包含端口数量(int)的列表,并进行排序。
sorted(set([b[0]forainnmap_report.hostsforbina.get_open_ports()]),key=int)
主机开放端口对应的服务,按端口号进行分组
显示所有主机开放的端口号,按端口号进行分组和排序。生成一个包含多个列表的列表(即列表的每个元素也为列表),其中每个成员列表第一个元素为端口号(int),第二个元素为一个包含开放对应端口主机IP地址(string)的列表。
[[a,[b.addressforbinnmap_report.hostsforcinb.get_open_ports()ifa==c[0]]]forainsorted(set([b[0]forainnmap_report.hostsforbina.get_open_ports()]),key=int)] SSL/TLS和HTTP/HTTPS
使用SSL的主机和端口
显示所有使用SSL的主机和端口。这是通过查找是否有服务使用了“SSL”通道或者相关脚本检测的结果中包含pem证书。生成一个包含一系列列表的列表,每个成员列表中包含主机地址(string)和端口号(int)。
[[a.address,b.port]forainnmap_report.hostsforbina.servicesifb.tunnel=='ssl'or"'pem'"instr(b.scripts_results)]
下面的内容包含上述相同的信息,但不在是一个包含列表的列表,而是使用join函数创建了一个包含“主机:端口号”(string)的列表。
[':'.join([a.address,str(b.port)])forainnmap_report.hostsforbina.servicesifb.tunnel=='ssl'or"'pem'"instr(b.scripts_results)]
包含web服务的主机和端口
显示所有的web服务及其对对应的端口号和协议(http或https)。这会生成一个包含多个列表的列表,其中每个成员列表包含协议(string)、地址(string)和端口号(int)。但这里会有些问题,nmap在报告使用https的网站时,有些时候会显示服务是“https”,而有时则会显示为使用“ssl”通道的“http”,所以我调整了下数据格式以便统一输出。
[[(b.service+b.tunnel).replace('sl',''),a.address,b.port]forainnmap_report.hostsforbina.servicesifb.open()andb.service.startswith('http')]
这里还是相同的信息,只不过是在原先包含协议、主机和端口号的列表中增加了url(string)。
[(b.service+b.tunnel).replace('sl','')+'://'+a.address+':'+str(b.port)+'/'forainnmap_report.hostsforbina.servicesifb.open()andb.service.startswith('http')]
其他服务信息
未知服务
显示所有nmap无法识别的服务。生成一个包含多个列表的列表,其中每个成员列表包含地址(string)、端口号(int)和nmap扫描的端口指纹(string)。生成这些信息,主要是为了方便后续人工审查那些特定的服务,而不会参与到任何自动化的过程中。
[[a.address,b.port,b.servicefp]forainnmap_report.hostsforbina.servicesif(b.service=='unknown'orb.servicefp)andb.portin[c[0]forcina.get_open_ports()]]
nmap识别出的软件
显示nmap扫描中识别出的所有软件。生成按产品字母排序的列表。
sorted(set([b.bannerforainnmap_report.hostsforbina.servicesif'product'inb.banner]))
软件对应的主机和端口号,按产品分组
显示扫描出软件对应的主机和端口,按产品分组。生成一个包含多个列表的列表,其中每个成员列表的第一个元素为软件的名称(string),随后是另一个列表包含地址(string)和端口号(int)。
[[a,[[b.address,c.port]forbinnmap_report.hostsforcinb.servicesifc.banner==a]]forainsorted(set([b.bannerforainnmap_report.hostsforbina.servicesif'product'inb.banner]))]
同上相同的信息,只是输出略有不同。同样还是生成一个包含多个列表的列表,成员列表的第一个元素还是软件的名称(string),但第二个是一个包含“主机:端口号”的列表。
[[a,[':'.join([b.address,str(c.port)])forbinnmap_report.hostsforcinb.servicesifc.banner==a]]forainsorted(set([b.bannerforainnmap_report.hostsforbina.servicesif'product'inb.banner]))]
搜索指定关键词相关的主机和端口
显示所有与给定关键词相关联的主机和端口,从nmap扫描结果的原始文本中查找包含产品名称、服务名称等等。下面以“Oracle”为例。生成一个包含多个列表的列表,其中每个成员列表包含主机地址(string)和端口号(int)。
[[a.address,b.port]forainnmap_report.hostsforbina.servicesifb.open()and'Oracle'instr(b.get_dict())+str(b.scripts_results)]
同上一样的方法,只是将存储的信息修改后一律使用小写进行搜索(下面示例为小写的“oracle”),输出格式还是跟上面一样。
[[a.address,b.port]forainnmap_report.hostsforbina.servicesifb.open()and'oracle'in(str(b.get_dict())+str(b.scripts_results)).lower()]
其他的事情
相同的证书名称
显示找到的SSL证书和使用nmap脚本解析后得到证书名称相同的部分。这样在当你从一个IP地址开始扫描且反向DNS失效的时候,可以帮助确定系统的主机名。生成一个包含多个列表的列表,其中每个成员列表包含IP地址(string)和提取出的主机名(string)。
[[a.address,c['elements']['subject']['commonName']]forainnmap_report.hostsforbina.servicesforcinb.scripts_resultsifc.has_key('elements')andc['elements'].has_key('subject')]
处理以上结果的方法
正向前面所说,上述的例子,当你直接粘贴进IPythonREPL时只是将输出打印在屏幕上。这的确不错,因为这样你可以随时查看到自己感兴趣的信息,但你可能还会想做更多的事情。之所以去生成上述信息,一大好处就在于你可以根据结果轻松执行一些自动化的操作。
如果你已经很熟悉Python,应当可以很容易完成这些工作,那么你可以跳过这一节。但如果你不熟悉,那么本节会讲述一些很基本的知识,告诉你如何使用上述的代码段。
保存到磁盘
如果你想将上述代码段的输出结果保存到磁盘上的文本文件中,你需要将输出的列表转换为适当的字符串格式(具体取决于你的需求),然后在将这个字符串写入文件。在Python中,你可以使用join函数来整合这些列表并将其写入文件,这里只是一个示例。
我们想要从生成的列表中提取出支持SSL的主机和端口,并将它们保存到一个新的文件中,这样可以在bash中使用循环来完成并使用命令行工具来进行测试。
我通常会在IPython中使用一行代码来完成这些,虽然一行代码会比较方便,但这里为了方便阅读和理解,我会将代码拆分出来说。
让我们来解析之前生成了一个包含“主机:端口”的列表,请注意我们使用了str函数将端口号从整数类型装换为了字符类型,这样使得它也能够使用join函数与其他字符串拼接在一起。
[':'.join([a.address,str(b.port)])forainnmap_report.hostsforbina.servicesifb.tunnel=='ssl'or"'pem'"instr(b.scripts_results)]
让我们来给上面这段代码的结果分配名为“ssl_services”变量,以方便后续的调用。
ssl_services=[':'.join([a.address,str(b.port)])forainnmap_report.hostsforbina.servicesifb.tunnel=='ssl'or"'pem'"instr(b.scripts_results)]
现在,让我们来使用join函数将列表的每一个元素拼接起来并使用(‘\n')进行换行,然后给它分配一个名为“ssl_services_text”的变量。
ssl_services_text='\n'.join(ssl_services)
随后,我们就可以在当前工作目录下创建一个名为“ssl_services_file.txt”的新文建,并将“ssl_services_text”变量的内容写入其中。
open('ssl_services_file.txt','w').write(ssl_services_text)
就这么简单,后续你可以根据自己的需要来使用文件内容了。
使用其他Python代码
也许你还会想用其他的Python代码来完成上述工作?同样很简单,下面就是另一个示例,这里我们遍历每一个nmap识别出的web服务及其网页的请求结果。
下面会生成一个包含URLs的列表,我们分配一个名为“urls”的变量给它。
urls=[(b.service+b.tunnel).replace('sl','')+'://'+a.address+':'+str(b.port)+'/'forainnmap_report.hostsforbina.servicesifb.open()andb.service.startswith('http')]
下一步,我们先进行一些准备工作,导入requests模块,然后设置一个简单的getAndSave函数进行web请求并将返回结果保存到磁盘上,文件名按url自动生成。你可能会注意到下面代码中,在get请求中使用了“verify=False”选项,这会在发送请求时忽略证书验证的错误,这个选项经常在测试内部机器时使用,因为内部机器基本不会有可信的证书颁发机构颁发的SSL证书。
importrequests defgetAndSave(url): r=requests.get(url,verify=False) open('_'.join(url.split('/')[2:]).replace(':',''),'wb').write(r.text.encode('utf8'))
现在,让我们增加一些代码来遍历每一个url,请求每个站点的robots.txt文件,并将其保存到本地以供后续使用。
forainurls: getAndSave(a+'robots.txt')
这样就会将每一个站点的robots.txt文件爬取到当前工作目录下。这只是一个很简单的例子。
总结
希望你在阅读完本文后,可以自己灵活的使用Python解析nmap扫描结果。