Instantly share code, notes, and snippets.
Created
May 16, 2016 18:36
-
Star
(2)
2
You must be signed in to star a gist -
Fork
(2)
2
You must be signed in to fork a gist
-
Save littleairmada/b04319742c29efe44d5662d842c20e1c to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
description = [[ | |
A simple banner grabber which connects to an open TCP port and prints out anything sent by the listening service within five seconds. | |
If no banner is received, a HTTP GET request is sent and the response recorded. Banners which contain telnet sequences will trigger | |
telnet option negotiation, with the intent to get far enough into the handshake that we can receive the real banner. If data is | |
received, more data will be read for up to fifteen seconds. | |
]] | |
--- | |
-- @output | |
-- 21/tcp open ftp | |
-- |_ banner-plus: 220 FTP version 1.0\x0D\x0A | |
author = "hdm" | |
license = "Same as Nmap--See http://nmap.org/book/man-legal.html" | |
categories = {"discovery", "safe"} | |
local nmap = require "nmap" | |
local comm = require "comm" | |
local stdnse = require "stdnse" | |
local strbuf = require "strbuf" | |
local nsedebug = require "nsedebug" | |
--- | |
-- Script is executed for any TCP port. | |
portrule = function( host, port ) | |
return port.protocol == "tcp" | |
end | |
--- | |
-- Grabs a banner and outputs it nicely formatted. | |
action = function( host, port ) | |
local out = grab_banner(host, port) | |
return output( out ) | |
end | |
--- | |
-- Go through telnet's option palaver so we can get to the login prompt. | |
-- We just deny every options the server asks us about. | |
-- Stolen entirely from telnet-brute.nse with tweaks | |
local negotiate_options = function(result, soc) | |
local index, x, opttype, opt, retbuf, data | |
count = 0 | |
index = 0 | |
retbuf = strbuf.new() | |
while count < 20 do | |
-- 255 is IAC (Interpret As Command) | |
index, x = string.find(result, '\255', index) | |
if not index then | |
break | |
end | |
opttype = string.byte(result, index+1) | |
opt = string.byte(result, index+2) | |
-- don't want it! won't do it! | |
if opttype == 251 or opttype == 252 then | |
opttype = 254 | |
elseif opttype == 253 or opttype == 254 then | |
opttype = 252 | |
end | |
retbuf = retbuf .. string.char(255) | |
retbuf = retbuf .. string.char(opttype) | |
retbuf = retbuf .. string.char(opt) | |
index = index + 1 | |
count = count + 1 | |
end | |
local data = strbuf.dump(retbuf) | |
if data:len() > 0 then | |
soc:send(data) | |
end | |
end | |
--- | |
-- Returns a number of milliseconds for use as a socket timeout value (defaults to 5 seconds). | |
-- | |
-- @return Number of milliseconds. | |
function get_timeout() | |
return 5000 | |
end | |
--- | |
-- Connects to the target on the given port and returns any data issued by a listening service. | |
-- @param host Host Table. | |
-- @param port Port Table. | |
-- @return Socket descriptor and initial banner | |
function grab_banner(host, port) | |
local st, buff, banner | |
local pnum = port.number | |
local probe = "GET / HTTP/1.1\r\nHost: www\r\nAccept: */*\r\nUser-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64; rv:12.0) Gecko/20100101 Firefox/12.0\r\n\r\n" | |
local proto = "tcp" | |
local socket = nmap.new_socket() | |
socket:set_timeout(get_timeout()) | |
banner = "" | |
if pnum == 443 then | |
proto = "ssl" | |
end | |
st = socket:connect(host, port, proto) | |
if not st then | |
if proto == "ssl" then | |
-- Fall back to non-SSL if our guess was wrong | |
proto = "tcp" | |
else | |
-- Could not connect to the TCP port as a plain socket | |
socket:close() | |
return nil | |
end | |
st = socket:connect(host, port, proto) | |
-- Give up if the second try fails | |
if not st then | |
socket:close() | |
return nil | |
end | |
end | |
local probe_sent = 0 | |
if pnum == 80 or pnum == 443 or pnum == 8080 then | |
socket:send("GET / HTTP/1.0\r\n\r\n") | |
probe_sent = 1 | |
end | |
st, buff = socket:receive() | |
if st then | |
banner = banner .. buff | |
end | |
-- Send a probe if no banner was recieved | |
if not st then | |
socket:send(probe) | |
probe_sent = 1 | |
st, buff = socket:receive_bytes(1) | |
if st then | |
banner = banner .. buff | |
end | |
end | |
-- Flip SSL states and try again | |
if not st then | |
socket:close() | |
if proto == "ssl" then | |
proto = "tcp" | |
else | |
proto = "ssl" | |
end | |
st = socket:connect(host, port, proto) | |
if not st then | |
socket:close() | |
return nil | |
end | |
st, buff = socket:receive() | |
if st then | |
banner = buff | |
else | |
socket:send(probe) | |
probe_sent = true | |
st,buff = socket:receive() | |
if st then | |
banner = banner .. buff | |
end | |
end | |
end | |
if not st then | |
socket:close() | |
return nil | |
end | |
negotiate_options(banner, socket) | |
-- Echo the original banner back to avoid ugly logs in SSH | |
if string.find(banner, '^SSH-') then | |
socket:send(banner) | |
end | |
-- This matches on both FTP and SMTP | |
if string.find(banner, '^220 ') or string.find(banner, '^220-' ) then | |
if string.find(banner, 'FTP') or pnum == 21 then | |
socket:send("USER ftp\r\n") | |
socket:send("PASS ftp@example.org\r\n") | |
else | |
socket:send("EHLO mail\r\n") | |
end | |
stdnse.sleep(1) | |
socket:send("HELP\r\n") | |
stdnse.sleep(1) | |
socket:send("QUIT\r\n") | |
end | |
socket:set_timeout(1000) | |
local cnt = 0 | |
for cnt=1,15 do | |
st,more = socket:receive_bytes(8192) | |
if not st then | |
break | |
end | |
negotiate_options(more, socket) | |
banner = banner .. more | |
end | |
return banner | |
end | |
--- | |
-- Formats the banner for printing to the port script result. | |
-- | |
-- Non-printable characters are hex encoded and the banner is | |
-- then truncated to fit into the number of lines of output desired. | |
-- @param out String banner issued by a listening service. | |
-- @return String formatted for output. | |
-- Ripped from banner.nse with line wrap disabled (corrupts output) | |
function output( out ) | |
if type(out) ~= "string" or out == "" then return nil end | |
local filename = SCRIPT_NAME | |
local line_len = 75 -- The character width of command/shell prompt window. | |
local fline_offset = 5 -- number of chars excluding script id not available to the script on the first line | |
-- number of chars available on the first line of output | |
-- we'll skip the first line of output if the filename is looong | |
local fline_len | |
if filename:len() < (line_len-fline_offset) then | |
fline_len = line_len -1 -filename:len() -fline_offset | |
else | |
fline_len = 0 | |
end | |
-- number of chars allowed on subsequent lines | |
local sline_len = line_len -1 -(fline_offset-2) | |
-- replace non-printable ascii chars - no need to do the whole string | |
out = replace_nonprint(out, (out:len() * 3) + 1) -- 1 extra char so we can truncate below. | |
-- break into lines - this will look awful if line_len is more than the actual space available on a line... | |
local ptr = fline_len | |
local t = {} | |
t[#t+1] = out | |
return table.concat(t,"\n") | |
end | |
--- | |
-- Replaces characters with ASCII values outside of the range of standard printable | |
-- characters (decimal 32 to 126 inclusive) with hex encoded equivalents. | |
-- | |
-- The second parameter dictates the number of characters to return, however, if the | |
-- last character before the number is reached is one that needs replacing then up to | |
-- three characters more than this number may be returned. | |
-- If the second parameter is nil, no limit is applied to the number of characters | |
-- that may be returned. | |
-- @param s String on which to perform substitutions. | |
-- @param len Number of characters to return. | |
-- @return String. | |
-- Pulled from banner.nse and mangled to escape \r\t\n separately | |
function replace_nonprint( s, len ) | |
local t = {} | |
local count = 0 | |
for c in s:gmatch(".") do | |
if c:byte() == 9 then | |
t[#t+1] = ("\\%s"):format("t") | |
count = count+3 | |
elseif c:byte() == 10 then | |
t[#t+1] = ("\\%s"):format("n") | |
count = count+3 | |
elseif c:byte() == 13 then | |
t[#t+1] = ("\\%s"):format("r") | |
count = count+3 | |
elseif c:byte() < 32 or c:byte() > 126 then | |
t[#t+1] = ("\\x%s"):format( ("0%s"):format( ( (stdnse.tohex( c:byte() )):upper() ) ):sub(-2,-1) ) -- capiche | |
count = count+4 | |
else | |
t[#t+1] = c | |
count = count+1 | |
end | |
if type(len) == "number" and count >= len then break end | |
end | |
return table.concat(t) | |
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment