367 lines
9.7 KiB
Lua

---
-- A library providing functions for doing SSLv2 communications
--
--
-- @author Bertrand Bonnefoy-Claudet
-- @author Daniel Miller
local stdnse = require "stdnse"
local table = require "table"
local tableaux = require "tableaux"
local nmap = require "nmap"
local sslcert = require "sslcert"
local string = require "string"
local rand = require "rand"
_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_ERRORS = {
[1] = "SSL_PE_NO_CIPHER",
[2] = "SSL_PE_NO_CERTIFICATE",
[3] = "SSL_PE_BAD_CERTIFICATE",
[4] = "SSL_PE_UNSUPPORTED_CERTIFICATE_TYPE",
}
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
-- 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
len, i = string.unpack(">I2", buffer, i)
local msb = (len & 0x8000) == 0x8000
local header_length, record_length, padding_length, is_escape
if msb then
header_length = 2
record_length = len & 0x7fff
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 = len & 0x3fff
is_escape = not not (len & 0x4000)
padding_length, i = string.unpack("B", 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
h.message_type, i = string.unpack("B", buffer, i)
if h.message_type == SSL_MESSAGE_TYPES.SERVER_HELLO then
local SID_hit, certificate_type, ssl_version, certificate_len, ciphers_len, connection_id_len, j = string.unpack(">BBI2I2I2I2", buffer, i)
local certificate, j = string.unpack("c" .. certificate_len, buffer, j)
local ciphers_end = j + ciphers_len
local ciphers = {}
while j < ciphers_end do
local cipher
cipher, j = string.unpack("c3", buffer, j)
local cipher_name = SSL_CIPHERS[cipher] and SSL_CIPHERS[cipher].str or ("0x" .. stdnse.tohex(cipher))
ciphers[#ciphers+1] = cipher_name
end
local connection_id, j = string.unpack("c" .. connection_id_len, buffer, j)
h.body = {
cert_type = certificate_type,
cert = certificate,
ciphers = ciphers,
connection_id = connection_id,
}
i = j
elseif h.message_type == SSL_MESSAGE_TYPES.ERROR and h.record_length == 3 then
local err, j = string.unpack(">I2", buffer, i)
h.body = {
error = SSL_ERRORS[err] or err
}
i = j
else
-- TODO: Other message types?
h.message_type = "encrypted"
local data, j = string.unpack("c"..h.record_length, buffer, i)
h.body = {
data = data
}
i = j
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 string.pack(">I2B", length, pad_length) .. payload
else
return string.pack(">I2", 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 = rand.random_string(16)
local ssl_v2_hello = string.pack(">BI2I2I2I2",
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
function client_master_secret(cipher_name, clear_key, encrypted_key, key_arg)
local key_arg = key_arg or ""
local ck = SSL_CIPHER_CODES[cipher_name] or cipher_name
assert(#ck == 3, "Unknown cipher in client_master_secret")
return ssl_record( string.pack(">Bc3I2I2I2",
SSL_MESSAGE_TYPES.CLIENT_MASTER_KEY,
ck,
#clear_key,
#encrypted_key,
#key_arg)
.. clear_key
.. encrypted_key
.. key_arg, 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
function test_sslv2 (host, port)
local timeout = stdnse.get_timeout(host, 10000, 5000)
-- Create socket.
local status, socket, err
local starttls = sslcert.getPrepareTLSWithoutReconnect(port)
if starttls then
status, socket = starttls(host, port)
if not status then
stdnse.debug(1, "Can't connect using STARTTLS: %s", socket)
return nil
end
else
socket = nmap.new_socket()
socket:set_timeout(timeout)
status, err = socket:connect(host, port)
if not status then
stdnse.debug(1, "Can't connect: %s", err)
return nil
end
end
socket:set_timeout(timeout)
local ssl_v2_hello = client_hello(tableaux.keys(SSL_CIPHER_CODES))
socket:send(ssl_v2_hello)
local status, record = record_buffer(socket)
socket:close();
if not status then
return nil
end
local _, message = record_read(record)
-- some sanity checks:
-- is it SSLv2?
if not message or not message.body then
return
end
-- is response a server hello?
if (message.message_type ~= SSL_MESSAGE_TYPES.SERVER_HELLO) then
return
end
---- is certificate in X.509 format?
--if (message.body.cert_type ~= 1) then
-- return
--end
return message.body.ciphers
end
return _ENV;