analyzers/submit/nmap-dir/nselib/sslv2.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;
