1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278
|
local coroutine = require "coroutine"
local creds = require "creds"
local io = require "io"
local nmap = require "nmap"
local packet = require "packet"
local shortport = require "shortport"
local snmp = require "snmp"
local stdnse = require "stdnse"
local unpwdb = require "unpwdb"
description = [[
Attempts to find an SNMP community string by brute force guessing.
This script opens a sending socket and a sniffing pcap socket in parallel
threads. The sending socket sends the SNMP probes with the community strings,
while the pcap socket sniffs the network for an answer to the probes. If
valid community strings are found, they are added to the creds database and
reported in the output.
The script takes the <code>snmp-brute.communitiesdb</code> argument that
allows the user to define the file that contains the community strings to
be used. If not defined, the default wordlist used to bruteforce the SNMP
community strings is <code>nselib/data/snmpcommunities.lst</code>. In case
this wordlist does not exist, the script falls back to
<code>nselib/data/passwords.lst</code>
No output is reported if no valid account is found.
]]
-- 2008-07-03 Philip Pickering, basic version
-- 2011-07-17 Gorjan Petrovski, Patrik Karlsson, optimization and creds
-- accounts, rejected use of the brute library because of
-- implementation using unconnected sockets.
-- 2011-12-29 Patrik Karlsson - Added lport to sniff_snmp_responses to fix
-- bug preventing multiple scripts from working
-- properly.
-- 2015-05-31 Gioacchino Mazzurco - Add IPv6 support by making the script IP
-- version agnostic
---
-- @usage
-- nmap -sU --script snmp-brute <target> [--script-args snmp-brute.communitiesdb=<wordlist> ]
--
-- @args snmp-brute.communitiesdb The filename of a list of community strings to try.
--
-- @output
-- PORT STATE SERVICE
-- 161/udp open snmp
-- | snmp-brute:
-- | dragon - Valid credentials
-- |_ jordan - Valid credentials
author = {"Philip Pickering", "Gorjan Petrovski", "Patrik Karlsson", "Gioacchino Mazzurco"}
license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
categories = {"intrusive", "brute"}
portrule = shortport.port_or_service(161, "snmp", "udp", {"open", "open|filtered"})
local communitiestable = {}
local filltable = function(filename, table)
if #table ~= 0 then
return true
end
local file = io.open(filename, "r")
if not file then
return false
end
for l in file:lines() do
-- Comments takes up a whole line
if not l:match("#!comment:") then
table[#table + 1] = l
end
end
file:close()
return true
end
local closure = function(table)
local i = 1
return function(cmd)
if cmd == "reset" then
i = 1
return
end
local elem = table[i]
if elem then i = i + 1 end
return elem
end
end
local communities_raw = function(path)
if not path then
return false, "Cannot find communities list"
end
if not filltable(path, communitiestable) then
return false, "Error parsing communities list"
end
return true, closure(communitiestable)
end
local communities = function()
local communities_file = stdnse.get_script_args('snmp-brute.communitiesdb') or
nmap.fetchfile("nselib/data/snmpcommunities.lst")
if communities_file then
stdnse.debug1("Using the %s as the communities file", communities_file)
local status, iterator = communities_raw(communities_file)
if not status then
return false, iterator
end
local time_limit = unpwdb.timelimit()
local count_limit = 0
if stdnse.get_script_args("unpwdb.passlimit") then
count_limit = tonumber(stdnse.get_script_args("unpwdb.passlimit"))
end
return true, unpwdb.limited_iterator(iterator, time_limit, count_limit, "communities")
else
stdnse.debug1("Cannot read the communities file, using the nmap username/password database instead")
return unpwdb.passwords()
end
end
local send_snmp_queries = function(socket, result, nextcommunity)
local condvar = nmap.condvar(result)
local request = snmp.buildGetRequest({}, "1.3.6.1.2.1.1.3.0")
local payload, status, response, err
local community = nextcommunity()
while community do
if result.status == false then
--in case the sniff_snmp_responses thread was shut down
condvar("signal")
return
end
payload = snmp.encode(snmp.buildPacket(request, nil, community))
status, err = socket:send(payload)
if not status then
result.status = false
result.msg = "Could not send SNMP probe"
condvar "signal"
return
end
community = nextcommunity()
end
result.sent = true
condvar("signal")
end
local sniff_snmp_responses = function(host, port, lport, result)
local condvar = nmap.condvar(result)
local pcap = nmap.new_socket()
pcap:set_timeout(host.times.timeout * 1000 * 3)
pcap:pcap_open(host.interface, 300, false,
("src host %s and udp and src port %d and dst port %d"):format(host.ip, port.number, lport))
local communities = creds.Credentials:new(SCRIPT_NAME, host, port)
-- last_run indicated whether there will be only one more receive
local last_run = false
-- receive even when status=false until all the probes are sent
while true do
if coroutine.status(result.main_thread) == "dead" then
-- Oops, main thread quit. Time to bail.
return
end
local status, plen, l2, l3, _ = pcap:pcap_receive()
if status then
local p = packet.Packet:new(l3,#l3)
if not p:udp_parse() then
--shouldn't happen
result.status = false
result.msg = "Wrong type of packet received"
condvar "signal"
return
end
local response = p:raw(p.udp_offset + 8, #p.buf)
local res = snmp.decode(response)
if type(res) == "table" then
communities:add(nil, res[2], creds.State.VALID)
else
result.status = false
result.msg = "Wrong type of SNMP response received"
condvar "signal"
return
end
else
if last_run or not result.status then
condvar "signal"
return
else
if result.sent then
last_run = true
end
end
end
end
pcap:close()
condvar "signal"
return
end
local function fail (err) return stdnse.format_output(false, err) end
action = function(host, port)
local status, nextcommunity = communities()
if not status then
return fail("Failed to read the communities database")
end
local result = {}
local threads = {}
local condvar = nmap.condvar(result)
result.sent = false --whether the probes are sent
result.msg = "" -- Error/Status msg
result.status = true -- Status (is everything ok)
result.main_thread = coroutine.running() -- to check if the main thread is dead.
local socket = nmap.new_socket("udp")
status = socket:connect(host, port)
if ( not(status) ) then
return fail("Failed to connect to server")
end
local status, _, lport = socket:get_info()
if( not(status) ) then
return fail("Failed to retrieve local port")
end
local recv_co = stdnse.new_thread(sniff_snmp_responses, host, port, lport, result)
local send_co = stdnse.new_thread(send_snmp_queries, socket, result, nextcommunity)
local recv_dead, send_dead
while true do
condvar "wait"
recv_dead = (coroutine.status(recv_co) == "dead")
send_dead = (coroutine.status(send_co) == "dead")
if send_dead then result.sent = true end
if recv_dead then break end
end
socket:close()
if result.status then
return creds.Credentials:new(SCRIPT_NAME, host, port):getTable()
else
stdnse.debug1("An error occurred: "..result.msg)
end
end
|