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
|
---
-- Functions for the SSH-1 protocol. This module also contains functions for
-- formatting key fingerprints.
--
-- @author Sven Klemm <sven@c3d2.de>
-- @copyright Same as Nmap--See https://nmap.org/book/man-legal.html
local io = require "io"
local math = require "math"
local nmap = require "nmap"
local os = require "os"
local stdnse = require "stdnse"
local string = require "string"
local stringaux = require "stringaux"
local table = require "table"
local base64 = require "base64"
local openssl = stdnse.silent_require "openssl"
_ENV = stdnse.module("ssh1", stdnse.seeall)
--- Retrieve the size of the packet that is being received
-- and checks if it is fully received
--
-- This function is very similar to the function generated
-- with match.numbytes(num) function, except that this one
-- will check for the number of bytes on-the-fly, based on
-- the written on the SSH packet.
--
-- @param buffer The receive buffer
-- @return packet_length, packet_length or nil
-- the return is similar to the lua function string:find()
check_packet_length = function( buffer )
if #buffer < 4 then return nil end
local payload_length = string.unpack( ">I4", buffer )
local padding = 8 - payload_length % 8
assert(payload_length)
local total = 4+payload_length+padding;
if total > #buffer then return nil end
return total, total;
end
--- Receives a complete SSH packet, even if fragmented
--
-- This function is an abstraction layer to deal with
-- checking the packet size to know if there is any more
-- data to receive.
--
-- @param socket The socket used to receive the data
-- @return status True or false
-- @return packet The packet received
receive_ssh_packet = function( socket )
local status, packet = socket:receive_buf(check_packet_length, true)
return status, packet
end
local function unpack_with_padding(len_bytes, data, offset)
local length, offset = string.unpack( ">I".. len_bytes, data, offset )
return string.unpack( ">c" .. math.ceil( length / 8 ), data, offset )
end
--- Fetch an SSH-1 host key.
-- @param host Nmap host table.
-- @param port Nmap port table.
-- @return A table with the following fields: <code>exp</code>,
-- <code>mod</code>, <code>bits</code>, <code>key_type</code>,
-- <code>fp_input</code>, <code>full_key</code>, <code>algorithm</code>, and
-- <code>fingerprint</code>.
fetch_host_key = function(host, port)
local socket = nmap.new_socket()
local status, _
status = socket:connect(host, port)
if not status then return end
-- fetch banner
status = socket:receive_lines(1)
if not status then socket:close(); return end
-- send our banner
status = socket:send("SSH-1.5-Nmap-SSH1-Hostkey\r\n")
if not status then socket:close(); return end
local data, packet_length, padding, offset
status,data = receive_ssh_packet( socket )
socket:close()
if not status then return end
packet_length, offset = string.unpack( ">I4", data )
padding = 8 - packet_length % 8
offset = offset + padding
if padding + packet_length + 4 == #data then
-- seems to be a proper SSH1 packet
local msg_code,host_key_bits,exp,mod,length,fp_input
msg_code, offset = string.unpack( ">B", data, offset )
if msg_code == 2 then -- 2 => SSH_SMSG_PUBLIC_KEY
-- ignore cookie and server key bits
offset = offset + 8 + 4
-- skip server key exponent and modulus
_, offset = unpack_with_padding(2, data, offset)
_, offset = unpack_with_padding(2, data, offset)
host_key_bits, offset = string.unpack( ">I4", data, offset )
exp, offset = unpack_with_padding(2, data, offset)
exp = openssl.bignum_bin2bn( exp )
mod, offset = unpack_with_padding(2, data, offset)
mod = openssl.bignum_bin2bn( mod )
fp_input = mod:tobin()..exp:tobin()
return {exp=exp,mod=mod,bits=host_key_bits,key_type='rsa1',fp_input=fp_input,
full_key=('%d %s %s'):format(host_key_bits, exp:todec(), mod:todec()),
key=('%s %s'):format(exp:todec(), mod:todec()), algorithm="RSA1",
fingerprint=openssl.md5(fp_input), fp_sha256=openssl.digest("sha256",fp_input)}
end
end
end
--- Format a key fingerprint in hexadecimal.
-- @param fingerprint Key fingerprint.
-- @param algorithm Key algorithm.
-- @param bits Key size in bits.
fingerprint_hex = function( fingerprint, algorithm, bits )
fingerprint = stdnse.tohex(fingerprint,{separator=":",group=2})
return ("%d %s (%s)"):format( bits, fingerprint, algorithm )
end
--- Format a key fingerprint in base64.
-- @param fingerprint Key fingerprint.
-- @param hash The hashing algorithm used
-- @param algorithm Key algorithm.
-- @param bits Key size in bits.
fingerprint_base64 = function( fingerprint, hash, algorithm, bits )
fingerprint = base64.enc(fingerprint)
return ("%d %s:%s (%s)"):format( bits, hash, fingerprint:match("[^=]+"), algorithm )
end
--- Format a key fingerprint in Bubble Babble.
-- @param fingerprint Key fingerprint.
-- @param algorithm Key algorithm.
-- @param bits Key size in bits.
fingerprint_bubblebabble = function( fingerprint, algorithm, bits )
local vowels = {'a','e','i','o','u','y'}
local consonants = {'b','c','d','f','g','h','k','l','m','n','p','r','s','t','v','z','x'}
local s = "x"
local seed = 1
for i=1,#fingerprint+2,2 do
local in1,in2,idx1,idx2,idx3,idx4,idx5
if i < #fingerprint or #fingerprint / 2 % 2 ~= 0 then
in1 = fingerprint:byte(i)
idx1 = (((in1 >> 6) & 3) + seed) % 6 + 1
idx2 = ((in1 >> 2) & 15) + 1
idx3 = ((in1 & 3) + math.floor(seed/6)) % 6 + 1
s = s .. vowels[idx1] .. consonants[idx2] .. vowels[idx3]
if i < #fingerprint then
in2 = fingerprint:byte(i+1)
idx4 = ((in2 >> 4) & 15) + 1
idx5 = (in2 & 15) + 1
s = s .. consonants[idx4] .. '-' .. consonants[idx5]
seed = (seed * 5 + in1 * 7 + in2) % 36
end
else
idx1 = seed % 6 + 1
idx2 = 16 + 1
idx3 = math.floor(seed/6) + 1
s = s .. vowels[idx1] .. consonants[idx2] .. vowels[idx3]
end
end
s = s .. 'x'
return ("%d %s (%s)"):format( bits, s, algorithm )
end
--- Format a key fingerprint into a visual ASCII art representation.
--
-- Ported from http://www.openbsd.org/cgi-bin/cvsweb/~checkout~/src/usr.bin/ssh/key.c.
-- @param fingerprint Key fingerprint.
-- @param algorithm Key algorithm.
-- @param bits Key size in bits.
fingerprint_visual = function( fingerprint, algorithm, bits )
local i,j,field,characters,input,fieldsize_x,fieldsize_y,s
fieldsize_x, fieldsize_y = 17, 9
characters = {' ','.','o','+','=','*','B','O','X','@','%','&','#','/','^','S','E'}
-- initialize drawing area
field = {}
for i=1,fieldsize_x do
field[i]={}
for j=1,fieldsize_y do field[i][j]=1 end
end
-- we start in the center and mark it
local x, y = math.ceil(fieldsize_x/2), math.ceil(fieldsize_y/2)
field[x][y] = #characters - 1;
-- iterate over fingerprint
for i=1,#fingerprint do
input = fingerprint:byte(i)
-- each byte conveys four 2-bit move commands
for j=1,4 do
if (input & 1) == 1 then x = x + 1 else x = x - 1 end
if (input & 2) == 2 then y = y + 1 else y = y - 1 end
x = math.max(x,1); x = math.min(x,fieldsize_x)
y = math.max(y,1); y = math.min(y,fieldsize_y)
if field[x][y] < #characters - 2 then
field[x][y] = field[x][y] + 1
end
input = input >> 2
end
end
-- mark end point
field[x][y] = #characters;
-- build output
s = ('\n+--[%4s %4d]----+\n'):format( algorithm, bits )
for i=1,fieldsize_y do
s = s .. '|'
for j=1,fieldsize_x do s = s .. characters[ field[j][i] ] end
s = s .. '|\n'
end
s = s .. '+-----------------+\n'
return s
end
-- A lazy parsing function for known_hosts_file.
-- The script checks for the known_hosts file in this order:
--
-- (1) If known_hosts is specified in a script arg, use that. If turned
-- off (false), then don't do any known_hosts checking.
-- (2) Look at ~/.ssh/config to see if user known_hosts is in an
-- alternate location*. Look for "UserKnownHostsFile". If
-- UserKnownHostsFile is specified, open that known_hosts.
-- (3) Otherwise, open ~/.ssh/known_hosts.
parse_known_hosts_file = function(path)
local common_paths = {}
local f, knownhostspath
if path and io.open(path) then
knownhostspath = path
end
if not knownhostspath then
for l in io.lines(os.getenv("HOME") .. "/.ssh/config") do
if l and string.find(l, "UserKnownHostsFile") then
knownhostspath = string.match(l, "UserKnownHostsFile%s(.*)")
if string.sub(knownhostspath,1,1)=="~" then
knownhostspath = os.getenv("HOME") .. string.sub(knownhostspath, 2)
end
end
end
end
if not knownhostspath then
knownhostspath = os.getenv("HOME") .."/.ssh/known_hosts"
end
if not knownhostspath then
return
end
local known_host_entries = {}
local lnumber = 0
for l in io.lines(knownhostspath) do
lnumber = lnumber + 1
if l and string.sub(l, 1, 1) ~= "#" then
local parts = stringaux.strsplit(" ", l)
table.insert(known_host_entries, {entry=parts, linenumber=lnumber})
end
end
return known_host_entries
end
return _ENV;
|