Nmap腳本文件分析(AMQP協議為例)


Nmap腳本文件分析(AMQP協議為例)

一、介紹

  上兩篇文章 Nmap腳本引擎原理   編寫自己的Nmap(NSE)腳本,分析了Nmap腳本引擎的執行過程,以及腳本文件的編寫,這篇文章將以解析AMQP Server為例,介紹Nmap自帶庫的使用,以及上兩篇文章中介紹不足的地方。

  轉載請注明出處:http://www.cnblogs.com/liun1994/

 

二、AMQP協議

  AMQP協議的全稱為: Advanced Message Queuing Protocol,提供統一消息服務的應用層標准高級消息隊列協議;是應用層協議的一個開放標准,為面向消息的中間件設計,基於此協議的客戶端與消息中間件可傳遞消息,並不
受客戶端/中間件不同產品,不同開發語言等條件的限制。下圖顯示為AMQP的原理圖,典型的生產消費消息模型:

 原理圖1

 

原理圖2

  各組件的作用如下:
   
 1)Broker:接收和分發消息的應用,比如:RabbitMQ Server就是Message Broker。

    2)Connection: publisher/consumer和broker之間的TCP連接。斷開連接的操作只會在client端進行,Broker不會斷開連接,除非出現網絡故障或broker服務出現問題。

    3)Exchange:message到達broker的第一站,根據分發規則,匹配查詢表中的routing key,分發消息到queue中去。常用的類型有:direct (point-to-point), topic (publish-subscribe) and fanout (multicast)。

    4)Queue: 消息最終被送到這里等待consumer取走。一個message可以被同時拷貝到多個queue中。

  在RabbitMQ實現中服務器允許的端口:參考(http://www.rabbitmq.com/networking.html

z

 

  在Nmap探測時,使用的正是AMQP 0-9-1 Client,因此我們探測時探測端口5672。

  遵循什么格式與AMQP Server交互?參考(http://www.rabbitmq.com/resources/specs/amqp-xml-doc0-9-1.pdf

 

 

  由上兩幅圖可以看出,AMQP客戶端與服務器交互首先發送協議頭:“AMQP0091”到服務器,服務器如果拒絕連接,則返回一個無效的協議頭,Nmap的處理方式是根據返回的協議版本再次請求服務。

 

三、腳本文件分析

   amqp-info.nse文件:

-- amqp庫提供檢索AMQP服務信息的基本函數,目前支持AMQP0-9和AMQP0-8協議 -- 這個庫包含一個AMQP類,這個類包含於AMQP通信及核心函數。
local amqp = require "amqp" 

-- nmap模塊是與nmap內部函數交互和數據結構化的API,API提供目標主機的詳細信息 -- 例如端口狀態和版本探測結果;同時API也提供與Nsock交互的接口;文件中共48個函數。
local nmap = require "nmap"

-- 構建簡要端口規則的函數,端口規則被多數scripts所使用,因此 -- 這個模塊提供最基本的測試,函數返回true or false。
local shortport = require "shortport"

-- 標准Nmap腳本引擎函數,這個模塊包含各種實用的函數,由於太小不能以模塊的形式給出。 -- 該模塊下有個module函數,作用跟Lua 5.1的module函數類似; -- 例如_ENV = stdnse.module("socks", stdnse.seeall)的作用就是定義一個以文件名socks -- 為變量的模塊,這樣方便我們修改,便於統一,印象筆記中有Lua moudle詳解。
local stdnse = require "stdnse"

-- 從AMQP服務器上收集信息
description = [[ Gathers information (a list of all server properties) from an AMQP (advanced message queuing protocol) server. ]]

-- 用於控制腳本的選擇 nmap --script default,safe;只運行safe和discovery類別的腳本
categories = {"default", "discovery", "safe", "version"} -- 端口規則,當這個函數返回true的時候,執行action函數.
portrule = shortport.version_port_or_service(5672, "amqp", "tcp", "open") action = function(host, port) -- 調用amqp模塊新建一個cli表,表里面包括好多屬性,host/port/amqpsocket等。
  local cli = amqp.AMQP:new( host, port ) -- 通過connect方法連接服務器,通過namp模塊的connect()方法與底層取得聯系
  local status, data = cli:connect() -- 如果status為nil或false,則返回下面的數據輸出,data為error字符串,在nmap模塊中可查到。
  if not status then return "Unable to open connection: " .. data end
  
  -- 如果連接成功,則進一步握手處理,數據解析也是在amqp.lua這個模塊中解析
  status, data = cli:handshake() -- 如果status為nil或false則返回錯誤信息data
  if not status then return data end

  -- 斷開連接
 cli:disconnect() -- 能夠進行到這一步,說明沒有錯誤出現,確定下來是AMQP協議了。
  port.version.name = "amqp" port.version.product = cli:getServerProduct() port.version.extrainfo = cli:getProtocolVersion() port.version.version = cli:getServerVersion() -- 設置host,port表為新的狀態。
 nmap.set_port_version(host, port) -- server_properties表存儲了握手的所有信息
  return stdnse.format_output(status, cli:getServerProperties()) -- 綜上所述,如果我們想修改交互過程的包,以及解析返回的數據;
  -- 關注amqp.lua模塊的handshake()函數即可。
end

    函數預覽圖:

 

  內嵌庫amqp.lua文件:

-- 該模塊提供基本的檢索AMQP服務器信息的函數,目前支持 AMQP 0-9和AMQP 0-8協議格式
-- 該模塊包含一個類,AMQP類,該類定義了AMQP交互過程所需要的屬性。
local bin = require "bin"
local match = require "match"
local nmap = require "nmap"
local stdnse = require "stdnse"
local table = require "table"

-- 統一化定義模塊,跟lua原始方式定義是一樣的,只不過這樣定義可以統一。
_ENV = stdnse.module("amqp", stdnse.seeall);


AMQP = {

  -- protocol versions sent by the server
  versions = {
    [0x0800] = "0-8",
    [0x0009] = "0-9"
  },

  -- version strings the client supports
  client_version_strings = {
    ["0-8"] = "\x01\x01\x08\x00",
    ["0-9"] = "\x00\x00\x09\x00",
    ["0-9-1"] = "\x00\x00\x09\x01"
  },

  -- setmetatable,self,__index等關鍵字,是Lua模擬類的操作;返回一個o對象。
  new = function(self, host, port)
    local o = {}
    setmetatable(o, self)
    self.__index = self
    o.host = host
    o.port = port
    o.amqpsocket = nmap.new_socket()
    -- nmap通過registry共享全局數據,如果參數里面有["0-8"]["0-9"]["0-9-1"]則設置為相應的字符串
    -- 如果沒有則設置為["0-9-1"]對應的字符串
    o.cli_version = self.client_version_strings[nmap.registry.args['amqp.version']] or self.client_version_strings["0-9-1"]
    o.protover = nil
    o.server_version = nil
    o.server_product = nil
    o.serer_properties = nil
    return o
  end,

  --- Connects the AMQP socket
  connect = function(self)
    local data, status, msg

    status, msg = self.amqpsocket:connect(self.host, self.port, "tcp")
    return status, msg
  end,

  --- Disconnects the AMQP socket
  disconnect = function(self)
    self.amqpsocket:close()
  end,

  --- Decodes a table value in the server properties field.
  --
  -- @param tbl the decoded table
  -- @param tsize number, the table size in bytes
  -- @return status, true on success, false on failure
  -- @return error string containing error message if status is false
  -- @return decoded value
  decodeTable = function(self, tbl, tsize)
    local status, err, tmp, read, value
    read = 0

    while read < tsize do
      local key, value

      status, tmp = self.amqpsocket:receive_buf(match.numbytes(1), true)
      if ( not(status) ) then
        return status, "ERROR: AMQP:handshake connection closed unexpectedly while reading key length", nil
      end
      read = read + 1

      tmp = select( 2, bin.unpack("C", tmp) )
      status, key = self.amqpsocket:receive_buf(match.numbytes(tmp), true)
      if ( not(status) ) then
        return status, "ERROR: AMQP:handshake connection closed unexpectedly while reading key", nil
      end
      read = read + tmp

      status, tmp = self.amqpsocket:receive_buf(match.numbytes(1), true)
      if ( not(status) ) then
        return status, "ERROR: AMQP:handshake connection closed unexpectedly while reading value type for " .. key, nil
      end
      read = read + 1

      if ( tmp == 'F' ) then -- table type
        status, tmp = self.amqpsocket:receive_buf(match.numbytes(4), true)
        if ( not(status) ) then
          return status, "ERROR: AMQP:handshake connection closed unexpectedly while reading table size", nil
        end

        read = read + 4
        value = {}
        tmp = select( 2, bin.unpack(">I", tmp) )
        status, err, value = self:decodeTable(value, tmp)
        read = read + tmp
        table.insert(tbl, key .. ": ")
        table.insert(tbl, value)
      elseif ( tmp == 'S' ) then -- string type
        status, err, value, read = self:decodeString(key, read)
        if ( key == "product" ) then
          self.server_product = value
        elseif ( key == "version" ) then
          self.server_version = value
        end
        table.insert(tbl, key .. ": " .. value)
      elseif ( tmp == 't' ) then -- boolean type
        status, err, value, read = self:decodeBoolean(key, read)
        table.insert(tbl, key .. ": " .. value)
      end

      if ( not(status) ) then
        return status, err, nil
      end

    end

    return true, nil, tbl
  end,

  --- Decodes a string value in the server properties field.
  --
  -- @param key string, the key being read
  -- @param read number, number of bytes already read
  -- @return status, true on success, false on failure
  -- @return error string containing error message if status is false
  -- @return decoded value
  -- @return number of bytes read after decoding this value
  decodeString = function(self, key, read)
    local value, status, tmp
    status, tmp = self.amqpsocket:receive_buf(match.numbytes(4), true)
    if ( not(status) ) then
      return status, "ERROR: AMQP:handshake connection closed unexpectedly while reading value size for " .. key, nil, 0
    end

    read = read + 4
    tmp = select( 2, bin.unpack(">I", tmp) )
    status, value = self.amqpsocket:receive_buf(match.numbytes(tmp), true)

    if ( not(status) ) then
      return status, "ERROR: AMQP:handshake connection closed unexpectedly while reading value for " .. key, nil, 0
    end
    read = read + tmp

    return true, nil, value, read
  end,

  --- Decodes a boolean value in the server properties field.
  --
  -- @param key string, the key being read
  -- @param read number, number of bytes already read
  -- @return status, true on success, false on failure
  -- @return error string containing error message if status is false
  -- @return decoded value
  -- @return number of bytes read after decoding this value
  decodeBoolean = function(self, key, read)
    local status, value
    status, value = self.amqpsocket:receive_buf(match.numbytes(1), true)
    if ( not(status) ) then
      return status, "ERROR: AMQP:handshake connection closed unexpectedly while reading value for " .. key, nil, 0
    end

    value = select( 2, bin.unpack("C", value) )
    read = read + 1

    return true, nil, value == 0x01 and "YES" or "NO", read
  end,

  --- Performs the AMQP handshake and determines
  -- * The AMQP protocol version
  -- * The server properties/capabilities
  --
  -- @return status, true on success, false on failure
  -- @return error string containing error message if status is false
  handshake = function(self)
    local _, status, err, version, tmp, value, properties
    
    -- 遵循AMQP協議格式,向AMQP服務器發送連接信息
    status = self.amqpsocket:send( "AMQP" .. self.cli_version )
    if ( not(status) ) then
      return false, "ERROR: AMQP:handshake failed while sending client version"
    end

    status, tmp = self.amqpsocket:receive_buf(match.numbytes(11), true)
    if ( not(status) ) then
      return status, "ERROR: AMQP:handshake connection closed unexpectedly while reading frame header"
    end

    -- check if the server rejected our proposed version
    if ( #tmp ~= 11 ) then --如果tmp不等於11,如果連接成功字節會在11以上,11是固定頭。
      -- 如果服務拒絕連接,那么返回8字節的數據。select函數是lua自帶函數,2代表選擇bin.unpack返回值的第二個參數
      -- bin.unpack,二進制解包; 目前推薦使用Lua5.3的string.unpack功能。
      -- 可參考http://www.lua.org/manual/5.3/manual.html#6.4.2
      -- http://www.lua.org/manual/5.3/manual.html#pdf-string.unpack
      -- > 表示大端存儲,I表示讀取四字節的無符號整數。
      if ( #tmp == 8 and select( 2, bin.unpack(">I", tmp) ) == 0x414D5150 ) then
        local vi, vii, v1, v2, v3, v4, found
        _, vi = bin.unpack(">I", tmp, 5)
        found = false

        -- check if we support the server's version
        for _, v in pairs( self.client_version_strings ) do
          _, vii = bin.unpack(">I", v)
          if ( vii == vi ) then
            version = v
            found = true
            break
          end
        end

        -- try again with new version string
        if ( found and version ~= self.cli_version ) then
          self.cli_version = version
          self:disconnect()
          status, err = self:connect()

          if ( not(status) ) then
            return status, err
          end

          return self:handshake()
        end

        -- version unsupported, _代表最后一個結束字符list[list.n]。
        -- >表示大端存儲,C表示讀取單字節的無符號整數。
        _, v1, v2, v3, v4 = bin.unpack(">CCCC", tmp, 5)
        return false, ("ERROR: AMQP:handshake unsupported version (%d.%d.%d.%d)"):format( v1, v2, v3, v4 )
      else -- 返回的不是AMQP...八個字節,證明不是AMQP協議
        return false, ("ERROR: AMQP:handshake server might not be AMQP, received: %s"):format( tmp )
      end
    end
    
    -- 上述過程結束之后,說明連接成功,收到11個以上的字節,開始解析協議。
    -- parse frame header
    local frametype, chnumber, framesize, method
    _, frametype, chnumber, framesize, method = bin.unpack(">CSII", tmp)
    stdnse.debug1("frametype: %d, chnumber: %d, framesize: %d, method: %d", frametype, chnumber, framesize, method)

    if (frametype ~= 1) then
      return false, ("ERROR: AQMP:handshake expected header (1) frame, but was %d"):format(frametype)
    end

    if (method ~= 0x000A000A) then
      return false, ("ERROR: AQMP:handshake expected connection.start (0x000A000A) method, but was %x"):format(method)
    end

    -- 解析11字節之后的字節,12,13字節分別是Major Version和 Minor Version
    -- parse protocol version
    status, tmp = self.amqpsocket:receive_buf(match.numbytes(2), true)
    if ( not(status) ) then
      return status, "ERROR: AMQP:handshake connection closed unexpectedly while reading version"
    end
    version = select( 2, bin.unpack(">S", tmp) )
    self.protover = AMQP.versions[version]

    if ( not(self.protover) ) then
      return false, ("ERROR: AMQP:handshake unsupported version (%x)"):format(version)
    end

    -- 解析服務屬性,4字節的內容代表數據長度。
    -- parse server properties
    status, tmp = self.amqpsocket:receive_buf(match.numbytes(4), true)
    if ( not(status) ) then
      return status, "ERROR: AMQP:handshake connection closed unexpectedly while reading server properties size"
    end

    local tablesize = select( 2, bin.unpack(">I", tmp) )
    properties = {}
    -- 封裝到decodeTable解析,在decodeTable里面會繼續從receive_buf接收數據
    status, err, properties = self:decodeTable(properties, tablesize)

    if ( not(status) ) then
      return status, err
    end

    -- 解析mechanisms
    status, err, value, tmp = self:decodeString("mechanisms", 0)
    if ( not(status) ) then
      return status, err
    end
    table.insert(properties, "mechanisms: " .. value)

    -- 解析locales
    status, err, value, tmp = self:decodeString("locales", 0)
    if ( not(status) ) then
      return status, err
    end
    table.insert(properties, "locales: " .. value)

    self.server_properties = properties
    
    -- 解析內容設置到AMQP o這個對象里面,返回true
    return true
  end,

  --- Returns the protocol version reported by the server
  --
  -- @return string containing the version number
  getProtocolVersion = function( self )
    return self.protover
  end,

  --- Returns the product version reported by the server
  --
  -- @return string containing the version number
  getServerVersion = function( self )
    return self.server_version
  end,

  --- Returns the product name reported by the server
  --
  -- @return string containing the product name
  getServerProduct = function( self )
    return self.server_product
  end,

  --- Returns the properties reported by the server
  --
  -- @return table containing server properties
  getServerProperties = function( self )
    return self.server_properties
  end,
}

return _ENV;
View Code

 

四、總結

   1)目前500多種腳本文件大致的執行流程類似,因為在腳本中可以拿到socket連接,可以與服務進行通信,拿到banner信息進行解析。

   2)更高級的用法需要了解每一個NSE的語句,通過這個例子,其他NSE腳本也不難理解。

   3)Nmap整體架構指的學習,有時間分析分析源碼有助於對其他工具的理解。

五、參考文獻

  http://www.cnblogs.com/frankyou/p/5283539.html   博客:RabbitMQ與AMQP協議詳解

  http://www.rabbitmq.com/documentation.html    RabbitMQ Documentation

  http://www.rabbitmq.com/protocol.html          AMQP 0-9-1 協議文檔

  

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM