From 0d47952611198ef6b1163f366dc03922d20b1475 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Wed, 17 Apr 2024 09:42:04 +0200 Subject: Adding upstream version 7.94+git20230807.3be01efb1+dfsg. Signed-off-by: Daniel Baumann --- nselib/snmp.lua | 598 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 598 insertions(+) create mode 100644 nselib/snmp.lua (limited to 'nselib/snmp.lua') diff --git a/nselib/snmp.lua b/nselib/snmp.lua new file mode 100644 index 0000000..b732160 --- /dev/null +++ b/nselib/snmp.lua @@ -0,0 +1,598 @@ +--- +-- SNMP library. +-- +-- @args snmp.version The SNMP protocol version. Use "v1" or 0 for SNMPv1 (default) and "v2c" or 1 for SNMPv2c. +-- +-- @author Patrik Karlsson +-- @author Gioacchino Mazzurco +-- @copyright Same as Nmap--See https://nmap.org/book/man-legal.html + +-- 2015-06-11 Gioacchino Mazzurco - Use creds library to handle SNMP community + +local asn1 = require "asn1" +local creds = require "creds" +local math = require "math" +local nmap = require "nmap" +local stdnse = require "stdnse" +local string = require "string" +local table = require "table" +_ENV = stdnse.module("snmp", stdnse.seeall) + + +-- SNMP ASN.1 Encoders +local tagEncoder = {} + +-- Override the boolean encoder +tagEncoder['boolean'] = function(self, val) + return '\x05\x00' +end + +-- Complex tag encoders +tagEncoder['table'] = function(self, val) + if val._snmp == '\x06' then -- OID + local oidStr = string.char(val[1]*40 + val[2]) + for i = 3, #val do + oidStr = oidStr .. self.encode_oid_component(val[i]) + end + return val._snmp .. self.encodeLength(#oidStr) .. oidStr + + elseif (val._snmp == '\x40') then -- ipAddress + return string.pack('Bs1', 0x40, string.pack('BBBB', table.unpack(val))) + + -- counter or gauge or timeticks or opaque + elseif (val._snmp == '\x41' or val._snmp == '\x42' or val._snmp == '\x43' or val._snmp == '\x44') then + local val = self:encodeInt(val[1]) + return val._snmp .. self.encodeLength(#val) .. val + end + + local encVal = "" + for _, v in ipairs(val) do + encVal = encVal .. self:encode(v) -- todo: buffer? + end + + local tableType = val._snmp or "\x30" + return tableType .. self.encodeLength(#encVal) .. encVal +end + +--- +-- Encodes a given value according to ASN.1 basic encoding rules for SNMP +-- packet creation. +-- @param val Value to be encoded. +-- @return Encoded value. +function encode(val) + local vtype = type(val) + local encoder = asn1.ASN1Encoder:new() + encoder:registerTagEncoders( tagEncoder ) + + + local encVal = encoder:encode(val) + + if encVal then + return encVal + end + + return '' +end + +-- SNMP ASN.1 Decoders +local tagDecoder = {} + +-- Application specific tags +-- +-- IP Address + +-- Response-PDU +-- TOOD: Figure out how to remove these dependencies +tagDecoder["\xa2"] = function( self, encStr, elen, pos ) + local seq = {} + + seq, pos = self:decodeSeq(encStr, elen, pos) + seq._snmp = "\xa2" + return seq, pos +end + +tagDecoder["\x40"] = function( self, encStr, elen, pos ) + local ip = {} + -- TODO: possibly convert to ipOps.str_to_ip() if octets are not used separately elsewhere. + ip[1], ip[2], ip[3], ip[4], pos = string.unpack("BBBB", encStr, pos) + ip._snmp = '\x40' + return ip, pos +end + +--- +-- Decodes an SNMP packet or a part of it according to ASN.1 basic encoding +-- rules. +-- @param encStr Encoded string. +-- @param pos Current position in the string. +-- @return The decoded value(s). +-- @return The position after decoding +function decode(encStr, pos) + local decoder = asn1.ASN1Decoder:new() + + if ( #tagDecoder == 0 ) then + decoder:registerBaseDecoders() + -- Application specific tags + -- tagDecoder["40"] = decoder.decoder["06"] -- IP Address; same as OID + tagDecoder["\x41"] = decoder.decoder["\x02"] -- Counter; same as Integer + tagDecoder["\x42"] = decoder.decoder["\x02"] -- Gauge + tagDecoder["\x43"] = decoder.decoder["\x02"] -- TimeTicks + tagDecoder["\x44"] = decoder.decoder["\x04"] -- Opaque; same as Octet String + tagDecoder["\x45"] = decoder.decoder["\x06"] -- NsapAddress + tagDecoder["\x46"] = decoder.decoder["\x02"] -- Counter64 + tagDecoder["\x47"] = decoder.decoder["\x02"] -- UInteger32 + + -- Context specific tags + tagDecoder["\xa0"] = decoder.decoder["\x30"] -- GetRequest-PDU + tagDecoder["\xa1"] = decoder.decoder["\x30"] -- GetNextRequest-PDU + --tagDecoder["\xa2"] = decoder.decoder["\x30"] -- Response-PDU + tagDecoder["\xa3"] = decoder.decoder["\x30"] -- SetRequest-PDU + tagDecoder["\xa4"] = decoder.decoder["\x30"] -- Trap-PDU + tagDecoder["\xa5"] = decoder.decoder["\x30"] -- GetBulkRequest-PDU + tagDecoder["\xa6"] = decoder.decoder["\x30"] -- InformRequest-PDU (not implemented here yet) + tagDecoder["\xa7"] = decoder.decoder["\x30"] -- SNMPv2-Trap-PDU (not implemented here yet) + tagDecoder["\xa8"] = decoder.decoder["\x30"] -- Report-PDU (not implemented here yet) + end + + + decoder:registerTagDecoders( tagDecoder ) + + return decoder:decode( encStr, pos ) +end + +local version_to_num = {v1=0, v2c=1} +local num_to_version = {[0]="v1", [1]="v2c"} + +--- Returns the numerical value of a given SNMP protocol version +-- +-- Numerical input is simply passed through, assuming it is valid. +-- String input is translated to its corresponding numerical value. +-- @param version of the SNMP protocol. See script argument snmp.version for valid codes +-- @param default numerical version of the SNMP protocol if the version parameter is nil or its value is invalid. +-- @return 0 or 1, depending on which protocol version was specified. +local function getVersion (version, default) + if version then + version = version_to_num[version] or tonumber(version) + if num_to_version[version] then + return version + end + stdnse.debug1("Unrecognized SNMP version; proceeding with SNMP%s", num_to_version[default]) + end + return default +end + +-- the library functions will use this version of SNMP by default +local default_version = getVersion(stdnse.get_script_args("snmp.version"), 0) + +--- +-- Create an SNMP packet. +-- @param PDU SNMP Protocol Data Unit to be encapsulated in the packet. +-- @param version SNMP version; defaults to script argument snmp.version +-- @param commStr community string. +function buildPacket(PDU, version, commStr) + local packet = {} + packet[1] = getVersion(version, default_version) + packet[2] = commStr + packet[3] = PDU + return packet +end + +--- SNMP options table +-- @class table +-- @name snmp.options +-- @field reqId Request ID. +-- @field err Error. +-- @field errIdx Error index. + +--- +-- Create an SNMP Get Request PDU. +-- @param options SNMP options table +-- @see snmp.options +-- @param ... Object identifiers to be queried. +-- @return Table representing PDU. +function buildGetRequest(options, ...) + if not options then options = {} end + + if not options.reqId then options.reqId = math.fmod(nmap.clock_ms(), 65000) end + if not options.err then options.err = 0 end + if not options.errIdx then options.errIdx = 0 end + + local req = {} + req._snmp = '\xa0' + req[1] = options.reqId + req[2] = options.err + req[3] = options.errIdx + + local payload = {} + for i=1, select('#', ...) do + payload[i] = {} + payload[i][1] = select(i, ...) + if type(payload[i][1]) == "string" then + payload[i][1] = str2oid(payload[i][1]) + end + payload[i][2] = false + end + req[4] = payload + return req +end + + +--- +-- Create an SNMP Get Next Request PDU. +-- @param options SNMP options table +-- @see snmp.options +-- @param ... Object identifiers to be queried. +-- @return Table representing PDU. +function buildGetNextRequest(options, ...) + options = options or {} + options.reqId = options.reqId or math.fmod(nmap.clock_ms(), 65000) + options.err = options.err or 0 + options.errIdx = options.errIdx or 0 + + local req = {} + req._snmp = '\xa1' + req[1] = options.reqId + req[2] = options.err + req[3] = options.errIdx + + local payload = {} + for i=1, select('#', ...) do + payload[i] = {} + payload[i][1] = select(i, ...) + if type(payload[i][1]) == "string" then + payload[i][1] = str2oid(payload[i][1]) + end + payload[i][2] = false + end + req[4] = payload + return req +end + +--- +-- Create an SNMP Set Request PDU. +-- +-- Takes one OID/value pair or an already prepared table. +-- @param options SNMP options table +-- @see snmp.options +-- @param oid Object identifiers of object to be set. +-- @param value To which value object should be set. If given a table, use the +-- table instead of OID/value pair. +-- @return Table representing PDU. +function buildSetRequest(options, oid, value) + if not options then options = {} end + + if not options.reqId then options.reqId = math.fmod(nmap.clock_ms(), 65000) end + if not options.err then options.err = 0 end + if not options.errIdx then options.errIdx = 0 end + + local req = {} + req._snmp = '\xa3' + req[1] = options.reqId + req[2] = options.err + req[3] = options.errIdx + + if (type(value) == "table") then + req[4] = value + else + local payload = {} + if (type(oid) == "string") then + payload[1] = str2oid(oid) + else + payload[1] = oid + end + payload[2] = value + req[4] = {} + req[4][1] = payload + end + return req +end + +--- +-- Create an SNMP Trap PDU. +-- @return Table representing PDU +function buildTrap(enterpriseOid, agentIp, genTrap, specTrap, timeStamp) + local req = {} + req._snmp = '\xa4' + if (type(enterpriseOid) == "string") then + req[1] = str2oid(enterpriseOid) + else + req[1] = enterpriseOid + end + req[2] = {} + req[2]._snmp = '\x40' + for n in string.gmatch(agentIp, "%d+") do + table.insert(req[2], tonumber(n)) + end + req[3] = genTrap + req[4] = specTrap + + req[5] = {} + req[5]._snmp = '\x43' + req[5][1] = timeStamp + + req[6] = {} + + return req +end + +--- +-- Create an SNMP Get Response PDU. +-- +-- Takes one OID/value pair or an already prepared table. +-- @param options SNMP options table +-- @see snmp.options +-- @param oid Object identifiers of object to be sent back. +-- @param value If given a table, use the table instead of OID/value pair. +-- @return Table representing PDU. +function buildGetResponse(options, oid, value) + if not options then options = {} end + + -- if really a response, should use reqId of request! + if not options.reqId then options.reqId = math.fmod(nmap.clock_ms(), 65000) end + if not options.err then options.err = 0 end + if not options.errIdx then options.errIdx = 0 end + + local resp = {} + resp._snmp = '\xa2' + resp[1] = options.reqId + resp[2] = options.err + resp[3] = options.errIdx + + if (type(value) == "table") then + resp[4] = value + else + + local payload = {} + if (type(oid) == "string") then + payload[1] = str2oid(oid) + else + payload[1] = oid + end + payload[2] = value + resp[4] = {} + resp[4][1] = payload + end + return resp +end + +--- +-- Transforms a string into an object identifier table. +-- @param oidStr Object identifier as string, for example +-- "1.3.6.1.2.1.1.1.0". +-- @return Table representing OID. +function str2oid(oidStr) + local oid = {} + for n in string.gmatch(oidStr, "%d+") do + table.insert(oid, tonumber(n)) + end + oid._snmp = '\x06' + return oid +end + +--- +-- Transforms a table representing an object identifier to a string. +-- @param oid Object identifier table. +-- @return OID string. +function oid2str(oid) + if (type(oid) ~= "table") then return 'invalid oid' end + return table.concat(oid, '.') +end + +--- +-- Transforms a table representing an IP to a string. +-- @param ip IP table. +-- @return IP string. +function ip2str(ip) + if (type(ip) ~= "table") then return 'invalid ip' end + return table.concat(ip, '.') +end + + +--- +-- Transforms a string into an IP table. +-- @param ipStr IP as string. +-- @return Table representing IP. +function str2ip(ipStr) + local ip = {} + for n in string.gmatch(ipStr, "%d+") do + table.insert(ip, tonumber(n)) + end + ip._snmp = '\x40' + return ip +end + + +--- +-- Fetches values from a SNMP response. +-- @param resp SNMP Response (will be decoded if necessary). +-- @return Table with all decoded responses and their OIDs. +function fetchResponseValues(resp) + if (type(resp) == "string") then + resp = decode(resp) + end + + if (type(resp) ~= "table") then + return {} + end + + local varBind + if (resp._snmp and resp._snmp == '\xa2') then + varBind = resp[4] + elseif (resp[3] and resp[3]._snmp and resp[3]._snmp == '\xa2') then + varBind = resp[3][4] + end + + if (varBind and type(varBind) == "table") then + local result = {} + for k, v in ipairs(varBind) do + local val = v[2] + if (type(v[2]) == "table") then + if (v[2]._snmp == '\x40') then + val = v[2][1] .. '.' .. v[2][2] .. '.' .. v[2][3] .. '.' .. v[2][4] + elseif (v[2]._snmp == '\x41') then + val = v[2][1] + elseif (v[2]._snmp == '\x42') then + val = v[2][1] + elseif (v[2]._snmp == '\x43') then + val = v[2][1] + elseif (v[2]._snmp == '\x44') then + val = v[2][1] + end + end + table.insert(result, {val, oid2str(v[1]), v[1]}) + end + return result + end + return {} +end + + +--- SNMP Helper class +-- +-- Handles socket communication, parsing, and setting of community strings +Helper = { + + --- Creates a new Helper instance + -- + -- @param host string containing the host name or ip + -- @param port table containing the port details to connect to + -- @param community string containing SNMP community + -- @param options A table with appropriate options: + -- * timeout - the timeout in milliseconds (Default: 5000) + -- * version - the SNMP version; defaults to script argument snmp.version. + -- @return o a new instance of Helper + new = function( self, host, port, community, options ) + local o = {} + setmetatable(o, self) + self.__index = self + o.host = host + o.port = port + + o.community = community or "public" + if community == nil then + local creds_store = creds.Credentials:new(creds.ALL_DATA, host, port) + for _,cs in ipairs({creds.State.PARAM, creds.State.VALID}) do + local account = creds_store:getCredentials(cs)() + if account then + if account.pass and account.pass ~= "" and account.pass ~= "" then + o.community = account.pass + break + elseif account.user then + o.community = account.user + break + end + end + end + end + + o.options = options or { + timeout = 5000, + version = default_version + } + + return o + end, + + --- Connect to the server + -- For UDP ports, this doesn't send any packets, but it creates the + -- socket and locks in the timeout. + -- @return status true on success, false on failure + connect = function( self ) + self.socket = nmap.new_socket() + self.socket:set_timeout(self.options.timeout) + local status, err = self.socket:connect(self.host, self.port) + if ( not(status) ) then return false, err end + + return true + end, + + --- Communications helper + -- Sends an SNMP message and receives a response. + -- @param message the result of one of the build*Request functions + -- @return status False if there was an error, true otherwise. + -- @return response The raw response read from the socket. + request = function (self, message) + local payload = encode( buildPacket( + message, + self.options.version, + self.community + ) ) + + local status, err = self.socket:send(payload) + if not status then + stdnse.debug2("snmp.Helper.request: Send to %s failed: %s", self.host.ip, err) + return false, err + end + + return self.socket:receive_bytes(1) + end, + + --- Sends an SNMP Get Next request + -- @param options SNMP options table + -- @see snmp.options + -- @param ... Object identifiers to be queried. + -- @return status False if error, true otherwise + -- @return Table with all decoded responses and their OIDs. + getnext = function (self, options, ...) + local status, response = self:request(buildGetNextRequest(options or {}, ...)) + if not status then + return status, response + end + return status, fetchResponseValues(response) + end, + + --- Sends an SNMP Get request + -- @param options SNMP options table + -- @see snmp.options + -- @param ... Object identifiers to be queried. + -- @return status False if error, true otherwise + -- @return Table with all decoded responses and their OIDs. + get = function (self, options, ...) + local status, response = self:request(buildGetRequest(options or {}, ...)) + if not status then + return status, response + end + return status, fetchResponseValues(response) + end, + + --- Sends an SNMP Set request + -- @param options SNMP options table + -- @see snmp.options + -- @param oid Object identifiers of object to be set. + -- @param value To which value object should be set. If given a table, + -- use the table instead of OID/value pair. + -- @return status False if error, true otherwise + -- @return Table with all decoded responses and their OIDs. + set = function (self, options, oid, setparam) + local status, response = self:request(buildSetRequest(options or {}, oid, setparam)) + if not status then + return status, response + end + return status, fetchResponseValues(response) + end, + + --- Walks the MIB Tree + -- + -- @param base_oid string containing the base object ID to walk + -- @return status true on success, false on failure + -- @return table containing oid and value + walk = function (self, base_oid) + + local snmp_table = { baseoid = base_oid } + local oid = base_oid + local options = {} + + local status, snmpdata = self:getnext(options, oid) + while ( snmpdata and snmpdata[1] and snmpdata[1][1] and snmpdata[1][2] ) do + oid = snmpdata[1][2] + if not oid:match(base_oid) or base_oid == oid then break end + + table.insert(snmp_table, { oid = oid, value = snmpdata[1][1] }) + local _ -- NSE don't want you to use global even if it is _ + _, snmpdata = self:getnext(options, oid) + end + + return status, snmp_table + end +} + +return _ENV; -- cgit v1.2.3