Nmap擴展開發(四)

Nmap擴展開發(四)

來自專欄一葉知安11 人贊了文章

由於傳播、利用此文所提供的信息而造成的任何直接或者間接的後果及損失,均由使用者本人負責,一葉知安以及文章作者不為此承擔任何責任。

一葉知安擁有對此文章的修改和解釋權。如欲轉載或傳播此文章,必須保證此文章的完整性,包括版權聲明等全部內容。未經一葉知安允許,不得任意修改或者增減此文章內容,不得以任何方式將其用於商業目的。

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 OKx0D| | 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()endhostrule=function(host) return falseendportrule=function(host,port) if(port.number == 80) then return true end return falseendaction = 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 endendpostrule=function()end

代碼解讀:

看完代碼讀者可能會有疑問,hostrule函數為什麼返回false?

在hostrule中返回false是因為如果是true會自動調用action,此時port的值是nil,所以會拋出一些錯誤。

緊接著就是將埠號為80的host和port交給action函數執行,調用can_use_head函數,判斷status是否為true,是true則支持HEAD方法請求。最後生成一個output_table,用來將響應內容填入這個表,以便于格式化顯示。

如果只想取得目標主機響應的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 endend……

執行結果如下:

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()endhostrule=function(host) return falseendportrule=function(host,port) if(port.number == 80) then return true end return falseendaction = 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 endendpostrule=function()end

執行結果如下:

  • 返回值:

該函數返回一個響應表,具體可參考前面的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()endhostrule=function(host) return falseendportrule=function(host,port) if(port.number == 80) then return true end return falseendaction = 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 endendpostrule=function()end

執行結果:

  • 返回值:

該函數返回一個響應表,具體可參考前面的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腳本語言寫一個簡單登錄判斷的頁面:

<?phpif(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 endend……

執行結果:

  • 返回值:

該函數返回一個響應表,具體可參考前面的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環境:

編寫的腳本如下:

local stdnse = require "stdnse"local http = require "http"prerule=function()endhostrule=function(host) return falseendportrule=function(host,port) local ports = {80,8080,8090,8899} for i in pairs(ports)do if(port.number == ports[i])then return true end endendaction = 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 falseendpostrule=function()end

在action函數中,首先生成一個隨機的文件名,然後發送PUT請求,判斷響應碼是否是201。注意,發送PUT請求的時候,文件擴展名後門必須帶」/」,是為了繞過tomcat的檢測。

執行結果:

使用瀏覽器訪問:

發現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 falseend……

腳本執行結果與上一節中的內容相同,只是多了一次GET請求,為了讓讀者真正理解這個腳本的執行過程,下面對比一下wireshark流量:

  • 優化前:

首先腳本直接向80埠發送了一個PUT請求,然後伺服器響應405,漏洞利用失敗。

緊接著腳本又請求了8080埠,伺服器響應201,證明漏洞利用成功。

  • 優化後:

通過向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_requestsend……

執行結果:

因為調用了兩次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_linesend……

這段代碼中添加了兩個請求隊列元素,分別是:

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

將隊列交給http.pipeline_go函數後,返回一個響應列表,all_response[1]對應index.jsp的響應表,all_response[2]對應/docs/changelog.html的響應表。因此可以使用迭代,將每個請求的某個欄位放入一個輸出表裡,本次示例是取得了兩次請求隊status-line。

執行結果:

0x09 表單操作

關於表單操作,在爬蟲、漏洞掃描、爆破時用的較多,一般要先取回目標主機的響應body,然後字元串匹配獲得表單結構。但是這些操作在Nmap中已經提供了一些方法,接下來就讓我們一起學習http包中的表單操作函數吧。

http. grab_forms 用於在響應內容中查找表單,並返回一個form_list表,參數如下:

  • body : 響應表中的body

返回值:

  • form_list : 表單列表

獲取頁面中的表單列表

我使用apache和php搭建了一個簡單的登錄界面,嘗試通過nmap的擴展腳本來獲取表單列表。

查看一下HTML源碼:

可以發現頁面中這個表單是提交到index.php的,並且提供了兩個輸入值,分別是username和password,這種情況下我們完全可以採用lua的字元串匹配模式獲得input的name,但是如果頁面上有很多表單,這就會增加我們處理數據的壓力。

我們來試試http.grab_forms函數:

local http = require "http"prerule=function()endhostrule=function(host) return falseendportrule=function(host,port) if(port.number == 80)then return true endendaction = function(host,port) local response = http.get(host,port,"/index.php") local login_forms = http.grab_forms(response.body) return login_formsendpostrule=function()end

因為http.grab_forms函數接收的是響應表的body,所以需要調用http.get函數將響應表取到,再把響應表的body傳遞進去。

執行結果:

目前是獲得了一個登錄表單,注意:返回的是一個表單列表,而不是只能獲得一個表單,在登錄頁面中新添加一個表單後:

再執行腳本觀察一下:

如果還想進一步獲得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_tableend

本段代碼首先獲取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><?phpif(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請求等


推薦閱讀:

如何評價2018強網杯線下賽?
早期的中國黑客喜歡讀什麼書?那個時代的「教科書」或者"聖經"是什麼?
我國軍網的防護性是什麼級別的?
電腦課上如何黑進老師的電腦。控制全班,甚至老師的電腦?
如何看待阮一峰的博客被人攻擊?

TAG:信息安全 | 黑客Hacker | Web安全測試 |