diff options
Diffstat (limited to 'nselib/sslv2.lua')
-rw-r--r-- | nselib/sslv2.lua | 366 |
1 files changed, 366 insertions, 0 deletions
diff --git a/nselib/sslv2.lua b/nselib/sslv2.lua new file mode 100644 index 0000000..31190bc --- /dev/null +++ b/nselib/sslv2.lua @@ -0,0 +1,366 @@ +--- +-- 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; |