diff --git a/nselib/sslv2.lua b/nselib/sslv2.lua new file mode 100644 index 000000000..d956ac5d7 --- /dev/null +++ b/nselib/sslv2.lua @@ -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 ciphers 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; diff --git a/scripts/sslv2.nse b/scripts/sslv2.nse index fae2afea9..9eb860cb3 100644 --- a/scripts/sslv2.nse +++ b/scripts/sslv2.nse @@ -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