From 7b43d1cafb23921e428b1d4a83351861765dd5d9 Mon Sep 17 00:00:00 2001 From: patrik Date: Wed, 9 Nov 2011 18:56:16 +0000 Subject: [PATCH] o [NSE] Added the scripts bitcoin-info, bitcoin-getaddr and a supporting Bitcoin library. The script bitcoin-info retrieves information about the remote server, while the bitcoin-getaddr script retrieves a list of discovered remote Bitcoin nodes. [Patrik] --- CHANGELOG | 5 + nselib/bitcoin.lua | 495 ++++++++++++++++++++++++++++++++++++ scripts/bitcoin-getaddr.nse | 43 ++++ scripts/bitcoin-info.nse | 63 +++++ scripts/script.db | 2 + 5 files changed, 608 insertions(+) create mode 100644 nselib/bitcoin.lua create mode 100644 scripts/bitcoin-getaddr.nse create mode 100644 scripts/bitcoin-info.nse diff --git a/CHANGELOG b/CHANGELOG index 1ca261cb7..b0353b142 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,10 @@ # Nmap Changelog ($Id$); -*-text-*- +o [NSE] Added the scripts bitcoin-info, bitcoin-getaddr and a supporting + Bitcoin library. The script bitcoin-info retrieves information about the + remote server, while the bitcoin-getaddr script retrieves a list of + discovered remote Bitcoin nodes. [Patrik] + o [NSE] Modified the following vulnerability scripts to use the new vulnerability library. - ftp-libopie.nse diff --git a/nselib/bitcoin.lua b/nselib/bitcoin.lua new file mode 100644 index 000000000..fe2317998 --- /dev/null +++ b/nselib/bitcoin.lua @@ -0,0 +1,495 @@ +--- +-- This library implements a minimal subset of the BitCoin protocol +-- It currently supports the version handshake and processing Addr responses. +-- +-- The library contains the following classes: +-- +-- * NetworkAddress - Contains functionality for encoding and decoding the +-- BitCoin network address structure. +-- +-- * Request - Classs containing BitCoin client requests +-- o Version - The client version exchange packet +-- +-- * Response - Class containing BitCoin server responses +-- o Version - The server version exchange packet +-- o VerAck - The server version ACK packet +-- o Addr - The server address packet +-- o Inv - The server inventory packet +-- +-- * BCSocket - A buffering socket class +-- +-- * Helper - The primary interface to scripts +-- + +-- +-- Version 0.1 +-- +-- Created 11/09/2011 - v0.1 - created by Patrik Karlsson +-- + +module(... or "bitcoin", package.seeall) + +require 'ipOps' +stdnse.silent_require('openssl') + + +-- A class that supports the BitCoin network address structure +NetworkAddress = { + + NODE_NETWORK = 1, + + -- Creates a new instance of the NetworkAddress class + -- @param host table as received by the action method + -- @param port table as received by the action method + -- @return o instance of NetworkAddress + new = function(self, host, port) + local o = { + host = "table" == type(host) and host.ip or host, + port = "table" == type(port) and port.number or port, + service = NetworkAddress.NODE_NETWORK, + } + setmetatable(o, self) + self.__index = self + return o + end, + + -- Creates a new instance of NetworkAddress based on the data string + -- @param data string of bytes + -- @return na instance of NetworkAddress + fromString = function(data) + assert(26 == #data, "Expected 26 bytes of data") + + local na = NetworkAddress:new() + local _ + _, na.service, na.ipv6_prefix, na.host, na.port = bin.unpack("S", data) + na.host = ipOps.fromdword(na.host) + return na + end, + + -- Converts the NetworkAddress instance to string + -- @return data string containing the NetworkAddress instance + __tostring = function(self) + local ipv6_prefix = "00 00 00 00 00 00 00 00 00 00 FF FF" + local ip = ipOps.todword(self.host) + return bin.pack("IS", self.service, ipv6_prefix, ip, self.port ) + end +} + +-- The request class container +Request = { + + -- The version request + Version = { + + -- Creates a new instance of the Version request + -- @param host table as received by the action method + -- @param port table as received by the action method + -- @param lhost string containing the source IP + -- @param lport number containing the source port + -- @return o instance of Version + new = function(self, host, port, lhost, lport) + local o = { + host = host, + port = port, + lhost= lhost, + lport= lport, + } + setmetatable(o, self) + self.__index = self + return o + end, + + -- Converts the Version request to a string + -- @return data as string + __tostring = function(self) + local magic = 0xD9B4BEF9 + local cmd = "version\0\0\0\0\0" + local len = 85 + -- ver: 0.4.0 + local ver = 0x9c40 + + -- NODE_NETWORK = 1 + local services = 1 + local timestamp = os.time() + local ra = NetworkAddress:new(self.host, self.port) + local sa = NetworkAddress:new(self.lhost, self.lport) + local nodeid = openssl.rand_bytes(8) + local subver = "\0" + local lastblock = 0 + + return bin.pack(" 31402 ) then + local timestamp, data + pos, timestamp, data = bin.unpack("timeout - the socket timeout in ms + -- @return instance of BCSocket + new = function(self, host, port, options) + local o = { + host = host, + port = port, + timeout = "table" == type(options) and options.timeout or 10000 + } + setmetatable(o, self) + self.__index = self + o.Socket = nmap.new_socket() + o.Buffer = nil + return o + end, + + --- Establishes a connection. + -- + -- @return Status (true or false). + -- @return Error code (if status is false). + connect = function( self ) + self.Socket:set_timeout( self.timeout ) + return self.Socket:connect( self.host, self.port ) + end, + + --- Closes an open connection. + -- + -- @return Status (true or false). + -- @return Error code (if status is false). + close = function( self ) + return self.Socket:close() + end, + + --- Opposed to the socket:receive_bytes function, that returns + -- at least x bytes, this function returns the amount of bytes requested. + -- + -- @param count of bytes to read + -- @return true on success, false on failure + -- @return data containing bytes read from the socket + -- err containing error message if status is false + recv = function( self, count ) + local status, data + + self.Buffer = self.Buffer or "" + + if ( #self.Buffer < count ) then + status, data = self.Socket:receive_bytes( count - #self.Buffer ) + if ( not(status) or #data < count - #self.Buffer ) then + return false, data + end + self.Buffer = self.Buffer .. data + end + + data = self.Buffer:sub( 1, count ) + self.Buffer = self.Buffer:sub( count + 1) + + return true, data + end, + + --- Sends data over the socket + -- + -- @return Status (true or false). + -- @return Error code (if status is false). + send = function( self, data ) + return self.Socket:send( data ) + end, +} + +-- The Helper class used as a primary interface to scripts +Helper = { + + -- Creates a new Helper instance + -- @param host table as received by the action method + -- @param port table as received by the action method + -- @param options table containing additional options + -- timeout - the socket timeout in ms + -- @return instance of Helper + new = function(self, host, port, options) + local o = { + host = host, + port = port, + options = options + } + setmetatable(o, self) + self.__index = self + return o + end, + + -- Connects to the BitCoin Server + -- @return status true on success false on failure + -- @return err string containing the error message in case status is false + connect = function(self) + self.socket = BCSocket:new(self.host, self.port, self.options) + local status, err = self.socket:connect() + + if ( not(status) ) then + return false, err + end + status, self.lhost, self.lport = self.socket.Socket:get_info() + return status, (status and nil or self.lhost) + end, + + -- Performs a version handshake with the server + -- @return status, true on success false on failure + -- @return version instance if status is true + -- err string containing an error message if status is false + exchVersion = function(self) + if ( not(self.socket) ) then + return false + end + + local req = Request.Version:new( + self.host, self.port, self.lhost, self.lport + ) + + local status, err = self.socket:send(tostring(req)) + if ( not(status) ) then + return false, "Failed to send \"Version\" request to server" + end + + local version + status, version = Response.recvPacket(self.socket) + + if ( not(status) or not(version) or version.cmd ~= "version\0\0\0\0\0" ) then + return false, "Failed to read \"Version\" response from server" + end + + if ( version.ver_raw > 29000 ) then + local status, verack = Response.recvPacket(self.socket) + end + + self.version = version.ver_raw + return status, version + end, + + getNodes = function(self) + local req = Request.GetAddr:new( + self.host, self.port, self.lhost, self.lport + ) + + local status, err = self.socket:send(tostring(req)) + if ( not(status) ) then + return false, "Failed to send \"Version\" request to server" + end + + return Response.recvPacket(self.socket, self.version) + end, + + -- Reads a message from the server + -- @return status true on success, false on failure + -- @return response instance of response packet if status is true + -- err string containing the error message if status is false + readMessage = function(self) + assert(self.version, "Version handshake has not been performed") + return Response.recvPacket(self.socket, self.version) + end, + + -- Closes the connection to the server + -- @return status true on success false on failure + -- @return err code, if status is false + close = function(self) + return self.socket:close() + end +} diff --git a/scripts/bitcoin-getaddr.nse b/scripts/bitcoin-getaddr.nse new file mode 100644 index 000000000..e4f17ceb7 --- /dev/null +++ b/scripts/bitcoin-getaddr.nse @@ -0,0 +1,43 @@ +description = [[ +Queries a BitCoin server for a list of known BitCoin nodes +]] + +author = "Patrik Karlsson" +license = "Same as Nmap--See http://nmap.org/book/man-legal.html" +categories = {"discovery", "safe"} + +require 'shortport' +require 'bitcoin' +require 'tab' + +portrule = shortport.port_or_service(8333, "bitcoin", "tcp" ) + +action = function(host, port) + + local bcoin = bitcoin.Helper:new(host, port, { timeout = 10000 }) + local status = bcoin:connect() + + if ( not(status) ) then + return "\n ERROR: Failed to connect to server" + end + + local status, ver = bcoin:exchVersion() + if ( not(status) ) then + return "\n ERROR: Failed to extract version information" + end + + local status, nodes = bcoin:getNodes() + if ( not(status) ) then + return "\n ERROR: Failed to extract version information" + end + bcoin:close() + + local response = tab.new(2) + tab.addrow(response, "ip", "timestamp") + + for _, node in ipairs(nodes.addresses) do + tab.addrow(response, ("%s:%d"):format(node.address.host, node.address.port), os.date("%x %X", node.ts)) + end + + return stdnse.format_output(true, tab.dump(response) ) +end \ No newline at end of file diff --git a/scripts/bitcoin-info.nse b/scripts/bitcoin-info.nse new file mode 100644 index 000000000..64c09dc28 --- /dev/null +++ b/scripts/bitcoin-info.nse @@ -0,0 +1,63 @@ +description = [[ +Extracts version and node information from a BitCoin server +]] + +--- +-- @usage +-- nmap -p 8333 --script bitcoin-info +-- +-- @output +-- PORT STATE SERVICE +-- 8333/tcp open unknown +-- | bitcoin-info: +-- | Timestamp: Wed Nov 9 19:47:23 2011 +-- | Network: main +-- | Version: 0.4.0 +-- | Node Id: DD5DFCBAAD0F882D +-- |_ Lastblock: 152589 +-- + +author = "Patrik Karlsson" +license = "Same as Nmap--See http://nmap.org/book/man-legal.html" +categories = {"discovery", "safe"} + +-- +-- Version 0.1 +-- +-- Created 11/09/2011 - v0.1 - created by Patrik Karlsson +-- + +require 'shortport' +require 'bitcoin' + +portrule = shortport.port_or_service(8333, "bitcoin", "tcp" ) + +action = function(host, port) + + local NETWORK = { + [3652501241] = "main", + [3669344250] = "testnet" + } + + local bcoin = bitcoin.Helper:new(host, port, { timeout = 10000 }) + local status = bcoin:connect() + + if ( not(status) ) then + return "\n ERROR: Failed to connect to server" + end + + local status, ver = bcoin:exchVersion() + if ( not(status) ) then + return "\n ERROR: Failed to extract version information" + end + bcoin:close() + + local result = {} + table.insert(result, ("Timestamp: %s"):format(os.date("%c", ver.timestamp))) + table.insert(result, ("Network: %s"):format(NETWORK[ver.magic])) + table.insert(result, ("Version: %s"):format(ver.ver)) + table.insert(result, ("Node Id: %s"):format(ver.nodeid)) + table.insert(result, ("Lastblock: %s"):format(ver.lastblock)) + + return stdnse.format_output(true, result) +end \ No newline at end of file diff --git a/scripts/script.db b/scripts/script.db index fa78a9d7e..40aa5526f 100644 --- a/scripts/script.db +++ b/scripts/script.db @@ -11,6 +11,8 @@ Entry { filename = "auth-spoof.nse", categories = { "malware", "safe", } } Entry { filename = "backorifice-brute.nse", categories = { "brute", "intrusive", } } Entry { filename = "backorifice-info.nse", categories = { "default", "discovery", "safe", } } Entry { filename = "banner.nse", categories = { "discovery", "safe", } } +Entry { filename = "bitcoin-getaddr.nse", categories = { "discovery", "safe", } } +Entry { filename = "bitcoin-info.nse", categories = { "discovery", "safe", } } Entry { filename = "bitcoinrpc-info.nse", categories = { "default", "discovery", "safe", } } Entry { filename = "bittorrent-discovery.nse", categories = { "discovery", "safe", } } Entry { filename = "broadcast-avahi-dos.nse", categories = { "broadcast", "dos", "intrusive", "vuln", } }