summaryrefslogtreecommitdiffstats
path: root/nselib/sslv2.lua
diff options
context:
space:
mode:
Diffstat (limited to 'nselib/sslv2.lua')
-rw-r--r--nselib/sslv2.lua366
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;