mirror of
https://github.com/nmap/nmap.git
synced 2026-01-20 05:09:02 +00:00
Refactor some code from sslv2.nse into sslv2.lua
This commit is contained in:
293
nselib/sslv2.lua
Normal file
293
nselib/sslv2.lua
Normal file
@@ -0,0 +1,293 @@
|
||||
---
|
||||
-- A library providing functions for doing SSLv2 communications
|
||||
--
|
||||
--
|
||||
-- @author Bertrand Bonnefoy-Claudet
|
||||
-- @author Daniel Miller
|
||||
|
||||
local stdnse = require "stdnse"
|
||||
local bin = require "bin"
|
||||
local bit = require "bit"
|
||||
local table = require "table"
|
||||
_ENV = stdnse.module("sslv2", stdnse.seeall)
|
||||
|
||||
SSL_MESSAGE_TYPES = {
|
||||
ERROR = 0,
|
||||
CLIENT_HELLO = 1,
|
||||
CLIENT_MASTER_KEY = 2,
|
||||
CLIENT_FINISHED = 3,
|
||||
SERVER_HELLO = 4,
|
||||
SERVER_VERIFY = 5,
|
||||
SERVER_FINISHED = 6,
|
||||
REQUEST_CERTIFICATE = 7,
|
||||
CLIENT_CERTIFICATE = 8,
|
||||
}
|
||||
|
||||
SSL_CERT_TYPES = {
|
||||
X509_CERTIFICATE = 1,
|
||||
}
|
||||
|
||||
-- (cut down) table of codes with their corresponding ciphers.
|
||||
-- inspired by Wireshark's 'epan/dissectors/packet-ssl-utils.h'
|
||||
|
||||
--- SSLv2 ciphers, keyed by cipher code as a string of 3 bytes.
|
||||
--
|
||||
-- @class table
|
||||
-- @name SSL_CIPHERS
|
||||
-- @field str The cipher name as a string
|
||||
-- @field key_length The length of the cipher's key
|
||||
-- @field encrypted_key_length How much of the key is encrypted in the handshake (effective key strength)
|
||||
SSL_CIPHERS = {
|
||||
["\x01\x00\x80"] = {
|
||||
str = "SSL2_RC4_128_WITH_MD5",
|
||||
key_length = 16,
|
||||
encrypted_key_length = 16,
|
||||
},
|
||||
["\x02\x00\x80"] = {
|
||||
str = "SSL2_RC4_128_EXPORT40_WITH_MD5",
|
||||
key_length = 16,
|
||||
encrypted_key_length = 5,
|
||||
},
|
||||
["\x03\x00\x80"] = {
|
||||
str = "SSL2_RC2_128_CBC_WITH_MD5",
|
||||
key_length = 16,
|
||||
encrypted_key_length = 16,
|
||||
},
|
||||
["\x04\x00\x80"] = {
|
||||
str = "SSL2_RC2_128_CBC_EXPORT40_WITH_MD5",
|
||||
key_length = 16,
|
||||
encrypted_key_length = 5,
|
||||
},
|
||||
["\x05\x00\x80"] = {
|
||||
str = "SSL2_IDEA_128_CBC_WITH_MD5",
|
||||
key_length = 16,
|
||||
encrypted_key_length = 16,
|
||||
},
|
||||
["\x06\x00\x40"] = {
|
||||
str = "SSL2_DES_64_CBC_WITH_MD5",
|
||||
key_length = 8,
|
||||
encrypted_key_length = 8,
|
||||
},
|
||||
["\x07\x00\xc0"] = {
|
||||
str = "SSL2_DES_192_EDE3_CBC_WITH_MD5",
|
||||
key_length = 24,
|
||||
encrypted_key_length = 24,
|
||||
},
|
||||
["\x00\x00\x00"] = {
|
||||
str = "SSL2_NULL_WITH_MD5",
|
||||
key_length = 0,
|
||||
encrypted_key_length = 0,
|
||||
},
|
||||
["\x08\x00\x80"] = {
|
||||
str = "SSL2_RC4_64_WITH_MD5",
|
||||
key_length = 16,
|
||||
encrypted_key_length = 8,
|
||||
},
|
||||
}
|
||||
|
||||
--- Another table of ciphers
|
||||
--
|
||||
-- Unlike SSL_CIPHERS, this one is keyed by cipher name and the values are the
|
||||
-- cipher code as a 3-byte string.
|
||||
-- @class table
|
||||
-- @name SSL_CIPHER_CODES
|
||||
SSL_CIPHER_CODES = {}
|
||||
for k, v in pairs(SSL_CIPHERS) do
|
||||
SSL_CIPHER_CODES[v.str] = k
|
||||
end
|
||||
|
||||
local SSL_MAX_RECORD_LENGTH_2_BYTE_HEADER = 32767
|
||||
local SSL_MAX_RECORD_LENGTH_3_BYTE_HEADER = 16383
|
||||
|
||||
local function parse_record_header_1_2(header_1_2)
|
||||
local _, b0, b1 = bin.unpack(">CC", header_1_2)
|
||||
local msb = bit.band(b0, 0x80) == 0x80
|
||||
local header_length
|
||||
local record_length
|
||||
if msb then
|
||||
header_length = 2
|
||||
record_length = bit.bor(bit.lshift(bit.band(b0, 0x7f), 8), b1)
|
||||
else
|
||||
header_length = 3
|
||||
record_length = bit.bor(bit.lshift(bit.band(b0, 0x3f), 8), b1)
|
||||
end
|
||||
return header_length, record_length
|
||||
end
|
||||
|
||||
-- 2 bytes of length minimum
|
||||
local SSL_MIN_HEADER = 2
|
||||
|
||||
local function read_header(buffer, i)
|
||||
i = i or 1
|
||||
-- Ensure we have enough data for the header.
|
||||
if #buffer - i + 1 < SSL_MIN_HEADER then
|
||||
return i, nil
|
||||
end
|
||||
|
||||
local len
|
||||
i, len = bin.unpack(">S", buffer, i)
|
||||
local msb = bit.band(len, 0x8000) == 0x8000
|
||||
local header_length, record_length, padding_length, is_escape
|
||||
if msb then
|
||||
header_length = 2
|
||||
record_length = bit.band(len, 0x7fff)
|
||||
is_escape = false
|
||||
padding_length = 0
|
||||
else
|
||||
header_length = 3
|
||||
if #buffer - i + 1 < 1 then
|
||||
-- don't have enough for the message_type. Back up.
|
||||
return i - SSL_MIN_HEADER, nil
|
||||
end
|
||||
record_length = bit.band(len, 0x3fff)
|
||||
is_escape = not not bit.band(len, 0x4000)
|
||||
i, padding_length = bin.unpack("C", buffer, i)
|
||||
end
|
||||
|
||||
return i, {
|
||||
record_length = record_length,
|
||||
is_escape = is_escape,
|
||||
padding_length = padding_length,
|
||||
}
|
||||
end
|
||||
|
||||
---
|
||||
-- Read a SSLv2 record
|
||||
-- @param buffer The read buffer
|
||||
-- @param i The position in the buffer to start reading
|
||||
-- @return The current position in the buffer
|
||||
-- @return The record that was read, as a table
|
||||
function record_read(buffer, i)
|
||||
local i, h = read_header(buffer, i)
|
||||
|
||||
if #buffer - i + 1 < h.record_length or not h then
|
||||
return i, nil
|
||||
end
|
||||
|
||||
i, h.message_type = bin.unpack("C", buffer, i)
|
||||
|
||||
if h.message_type == SSL_MESSAGE_TYPES.SERVER_HELLO then
|
||||
local i, SID_hit, certificate_type, ssl_version, certificate_len, ciphers_len, connection_id_len = bin.unpack(">CCSSSS", buffer, i)
|
||||
local i, certificate = bin.unpack("A" .. certificate_len, buffer, i)
|
||||
local ciphers_end = i + ciphers_len
|
||||
local ciphers = {}
|
||||
while i < ciphers_end do
|
||||
local cipher
|
||||
i, cipher = bin.unpack("A3", buffer, i)
|
||||
local cipher_name = SSL_CIPHERS[cipher] and SSL_CIPHERS[cipher].str or ("0x" .. stdnse.tohex(cipher))
|
||||
ciphers[#ciphers+1] = cipher_name
|
||||
end
|
||||
local i, connection_id = bin.unpack("A" .. connection_id_len, buffer, i)
|
||||
|
||||
h.body = {
|
||||
cert_type = certificate_type,
|
||||
cert = certificate,
|
||||
ciphers = ciphers,
|
||||
connection_id = connection_id,
|
||||
}
|
||||
else
|
||||
stdnse.debug1("Unknown message type: %s", h.message_type)
|
||||
return i, nil
|
||||
end
|
||||
return i, h
|
||||
end
|
||||
|
||||
--- Wrap a payload in an SSLv2 record header
|
||||
--
|
||||
--@param payload The padded payload to send
|
||||
--@param pad_length The length of the padding. If the payload is not padded, set to 0
|
||||
--@return An SSLv2 record containing the payload
|
||||
function ssl_record (payload, pad_length)
|
||||
local length = #payload
|
||||
assert(
|
||||
length < (pad_length == 0 and SSL_MAX_RECORD_LENGTH_2_BYTE_HEADER or SSL_MAX_RECORD_LENGTH_3_BYTE_HEADER),
|
||||
"SSL record too long")
|
||||
assert(pad_length < 256, "SSL record padding too long")
|
||||
if pad_length > 0 then
|
||||
return bin.pack(">SCA", length, pad_length, payload)
|
||||
else
|
||||
return bin.pack(">SA", bit.bor(length, 0x8000), payload)
|
||||
end
|
||||
end
|
||||
|
||||
---
|
||||
-- Build a client_hello message
|
||||
--
|
||||
-- The <code>ciphers</code> parameter can contain cipher names or raw 3-byte
|
||||
-- cipher codes.
|
||||
-- @param ciphers Table of cipher names
|
||||
-- @return The client_hello record as a string
|
||||
function client_hello (ciphers)
|
||||
local cipher_codes = {}
|
||||
|
||||
for _, c in ipairs(ciphers) do
|
||||
local ck = SSL_CIPHER_CODES[c] or c
|
||||
assert(#ck == 3, "Unknown cipher")
|
||||
cipher_codes[#cipher_codes+1] = ck
|
||||
end
|
||||
|
||||
local challenge = stdnse.generate_random_string(16)
|
||||
|
||||
local ssl_v2_hello = bin.pack(">CSSSSAA",
|
||||
1, -- MSG-CLIENT-HELLO
|
||||
2, -- version: SSL 2.0
|
||||
#cipher_codes * 3, -- cipher spec length
|
||||
0, -- session ID length
|
||||
#challenge, -- challenge length
|
||||
table.concat(cipher_codes),
|
||||
challenge
|
||||
)
|
||||
|
||||
return ssl_record(ssl_v2_hello, 0)
|
||||
end
|
||||
|
||||
local function read_atleast(s, n)
|
||||
local buf = {}
|
||||
local count = 0
|
||||
while count < n do
|
||||
local status, data = s:receive_bytes(n - count)
|
||||
if not status then
|
||||
return status, data, table.concat(buf)
|
||||
end
|
||||
buf[#buf+1] = data
|
||||
count = count + #data
|
||||
end
|
||||
return true, table.concat(buf)
|
||||
end
|
||||
|
||||
--- Get an entire record into a buffer
|
||||
--
|
||||
-- Caller is responsible for closing the socket if necessary.
|
||||
-- @param sock The socket to read additional data from
|
||||
-- @param buffer The string buffer holding any previously-read data
|
||||
-- (default: "")
|
||||
-- @param i The position in the buffer where the record should start
|
||||
-- (default: 1)
|
||||
-- @return status Socket status
|
||||
-- @return Buffer containing at least 1 record if status is true
|
||||
-- @return Error text if there was an error
|
||||
function record_buffer(sock, buffer, i)
|
||||
buffer = buffer or ""
|
||||
i = i or 1
|
||||
if #buffer - i + 1 < SSL_MIN_HEADER then
|
||||
local status, resp, rem = read_atleast(sock, SSL_MIN_HEADER - (#buffer - i + 1))
|
||||
if not status then
|
||||
return false, buffer .. rem, resp
|
||||
end
|
||||
buffer = buffer .. resp
|
||||
end
|
||||
local i, h = read_header(buffer, i)
|
||||
if not h then
|
||||
return false, buffer, "Couldn't read a SSLv2 header"
|
||||
end
|
||||
if (#buffer - i + 1) < h.record_length then
|
||||
local status, resp = read_atleast(sock, h.record_length - (#buffer - i + 1))
|
||||
if not status then
|
||||
return false, buffer, resp
|
||||
end
|
||||
buffer = buffer .. resp
|
||||
end
|
||||
return true, buffer
|
||||
end
|
||||
|
||||
return _ENV;
|
||||
@@ -7,6 +7,7 @@ local bin = require "bin"
|
||||
local bit = require "bit"
|
||||
local stdnse = require "stdnse"
|
||||
local sslcert = require "sslcert"
|
||||
local sslv2 = require "sslv2"
|
||||
|
||||
description = [[
|
||||
Determines whether the server supports obsolete and less secure SSLv2, and discovers which ciphers it
|
||||
@@ -49,91 +50,6 @@ portrule = function(host, port)
|
||||
return shortport.ssl(host, port) or sslcert.getPrepareTLSWithoutReconnect(port)
|
||||
end
|
||||
|
||||
local ssl_ciphers = {
|
||||
-- (cut down) table of codes with their corresponding ciphers.
|
||||
-- inspired by Wireshark's 'epan/dissectors/packet-ssl-utils.h'
|
||||
["\x00\x00\x00"] = "SSL2_NULL_WITH_MD5",
|
||||
["\x01\x00\x80"] = "SSL2_RC4_128_WITH_MD5",
|
||||
["\x02\x00\x80"] = "SSL2_RC4_128_EXPORT40_WITH_MD5",
|
||||
["\x03\x00\x80"] = "SSL2_RC2_128_CBC_WITH_MD5",
|
||||
["\x04\x00\x80"] = "SSL2_RC2_128_CBC_EXPORT40_WITH_MD5",
|
||||
["\x05\x00\x80"] = "SSL2_IDEA_128_CBC_WITH_MD5",
|
||||
["\x06\x00\x40"] = "SSL2_DES_64_CBC_WITH_MD5",
|
||||
["\x07\x00\xc0"] = "SSL2_DES_192_EDE3_CBC_WITH_MD5",
|
||||
["\x08\x00\x80"] = "SSL2_RC4_64_WITH_MD5",
|
||||
}
|
||||
|
||||
--Invert a one-to-one mapping
|
||||
local function invert(t)
|
||||
local out = {}
|
||||
for k, v in pairs(t) do
|
||||
out[v] = k
|
||||
end
|
||||
return out
|
||||
end
|
||||
|
||||
local cipher_codes = invert(ssl_ciphers)
|
||||
|
||||
local ciphers = function(cipher_list)
|
||||
|
||||
-- returns names of ciphers supported by the server
|
||||
|
||||
local seen = {}
|
||||
local available_ciphers = {}
|
||||
|
||||
for idx = 1, #cipher_list, 3 do
|
||||
local _, cipher = bin.unpack("A3", cipher_list, idx)
|
||||
local cipher_name = ssl_ciphers[cipher] or ("0x" .. stdnse.tohex(cipher))
|
||||
|
||||
-- Check for duplicate ciphers
|
||||
if not seen[cipher] then
|
||||
table.insert(available_ciphers, cipher_name)
|
||||
seen[cipher] = true
|
||||
end
|
||||
end
|
||||
|
||||
return available_ciphers
|
||||
end
|
||||
|
||||
local function parse_record_header_1_2(header_1_2)
|
||||
local _, b0, b1 = bin.unpack(">CC", header_1_2)
|
||||
local msb = bit.band(b0, 0x80) == 0x80
|
||||
local header_length
|
||||
local record_length
|
||||
if msb then
|
||||
header_length = 2
|
||||
record_length = bit.bor(bit.lshift(bit.band(b0, 0x7f), 8), b1)
|
||||
else
|
||||
header_length = 3
|
||||
record_length = bit.bor(bit.lshift(bit.band(b0, 0x3f), 8), b1)
|
||||
end
|
||||
return header_length, record_length
|
||||
end
|
||||
|
||||
local function read_ssl_record(sock)
|
||||
local status, header_1_2 = sock:receive_buf(match.numbytes(2), true)
|
||||
if not status then
|
||||
return status
|
||||
end
|
||||
|
||||
local header_length, record_length = parse_record_header_1_2(header_1_2)
|
||||
local padding_length
|
||||
if header_length == 2 then
|
||||
padding_length = 0
|
||||
else
|
||||
local status, header_3 = sock:receive_buf(match.numbytes(1), true)
|
||||
if not status then
|
||||
return status
|
||||
end
|
||||
local _
|
||||
_, padding_length = bin.unpack(">C", header_3)
|
||||
end
|
||||
|
||||
local status, payload = sock:receive_buf(match.numbytes(record_length), true)
|
||||
|
||||
return status, payload, padding_length
|
||||
end
|
||||
|
||||
action = function(host, port)
|
||||
local timeout = stdnse.get_timeout(host, 10000, 5000)
|
||||
|
||||
@@ -158,67 +74,35 @@ action = function(host, port)
|
||||
|
||||
socket:set_timeout(timeout)
|
||||
|
||||
-- build client hello packet (contents inspired by
|
||||
-- http://mail.nessus.org/pipermail/plugins-writers/2004-October/msg00041.html )
|
||||
local cipher_list = (
|
||||
cipher_codes.SSL2_DES_192_EDE3_CBC_WITH_MD5 ..
|
||||
cipher_codes.SSL2_IDEA_128_CBC_WITH_MD5 ..
|
||||
cipher_codes.SSL2_RC2_128_CBC_WITH_MD5 ..
|
||||
cipher_codes.SSL2_RC4_128_WITH_MD5 ..
|
||||
cipher_codes.SSL2_RC4_64_WITH_MD5 ..
|
||||
cipher_codes.SSL2_DES_64_CBC_WITH_MD5 ..
|
||||
cipher_codes.SSL2_RC2_128_CBC_EXPORT40_WITH_MD5 ..
|
||||
cipher_codes.SSL2_RC4_128_EXPORT40_WITH_MD5 ..
|
||||
cipher_codes.SSL2_NULL_WITH_MD5
|
||||
)
|
||||
-- Random
|
||||
local challenge = "\xe4\xbd\x00\x00\xa4\x41\xb6\x74\x71\x2b\x27\x95\x44\xc0\x3d\xc0"
|
||||
local ssl_v2_hello = bin.pack(">CSSSSAA",
|
||||
1, -- MSG-CLIENT-HELLO
|
||||
2, -- version: SSL 2.0
|
||||
#cipher_list, -- cipher spec length
|
||||
0, -- session ID length
|
||||
#challenge, -- challenge length
|
||||
cipher_list,
|
||||
challenge
|
||||
)
|
||||
-- Prepend length plus MSB
|
||||
ssl_v2_hello = bin.pack(">SA", #ssl_v2_hello + 0x8000, ssl_v2_hello)
|
||||
local ssl_v2_hello = sslv2.client_hello(stdnse.keys(sslv2.SSL_CIPHER_CODES))
|
||||
|
||||
socket:send(ssl_v2_hello)
|
||||
|
||||
local status, server_hello = read_ssl_record(socket)
|
||||
local status, record = sslv2.record_buffer(socket)
|
||||
socket:close();
|
||||
if not status then
|
||||
return nil
|
||||
end
|
||||
|
||||
-- split up server hello into components
|
||||
local idx, message_type, SID_hit, certificate_type, ssl_version, certificate_len, ciphers_len, connection_ID_len = bin.unpack(">CCCSSSS", server_hello)
|
||||
local _, message = sslv2.record_read(record)
|
||||
|
||||
-- some sanity checks:
|
||||
-- is it SSLv2?
|
||||
if (ssl_version ~= 2) then
|
||||
if not message or not message.body then
|
||||
return
|
||||
end
|
||||
-- is response a server hello?
|
||||
if (message_type ~= 4) then
|
||||
if (message.message_type ~= sslv2.SSL_MESSAGE_TYPES.SERVER_HELLO) then
|
||||
return
|
||||
end
|
||||
-- is certificate in X.509 format?
|
||||
if (certificate_type ~= 1) then
|
||||
return
|
||||
end
|
||||
|
||||
local idx, certificate = bin.unpack("A" .. certificate_len, server_hello, idx)
|
||||
local idx, cipher_list = bin.unpack("A" .. ciphers_len, server_hello, idx)
|
||||
local idx, connection_ID = bin.unpack("A" .. connection_ID_len, server_hello, idx)
|
||||
|
||||
-- get a list of ciphers offered
|
||||
local available_ciphers = ciphers_len > 0 and ciphers(cipher_list) or "none"
|
||||
---- is certificate in X.509 format?
|
||||
--if (message.body.cert_type ~= 1) then
|
||||
-- return
|
||||
--end
|
||||
|
||||
return {
|
||||
"SSLv2 supported",
|
||||
ciphers = available_ciphers
|
||||
ciphers = #message.body.ciphers > 0 and message.body.ciphers or "none"
|
||||
}
|
||||
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user