analyzers/submit/nmap-dir/nselib/rdp.lua
---
-- A minimal RDP (Remote Desktop Protocol) library. Currently has functionality to determine encryption
-- and cipher support.
--
--
-- @author Patrik Karlsson <patrik@cqure.net>
-- @copyright Same as Nmap--See https://nmap.org/book/man-legal.html
--

local nmap = require("nmap")
local stdnse = require("stdnse")
local string = require "string"
local asn1 = require "asn1"
_ENV = stdnse.module("rdp", stdnse.seeall)

-- Server Core Data  2.2.1.4.2
PROTO_VERSION = {
  [0x00080001] = " RDP 4.0 server",
  [0x00080004] = " RDP 5.x, 6.x, 7.x, or 8.x server",
  [0x00080005] = " RDP 10.0 server",
  [0x00080006] = " RDP 10.1 server",
  [0x00080007] = " RDP 10.2 server",
  [0x00080008] = " RDP 10.3 server",
  [0x00080009] = " RDP 10.4 server",
  [0x0008000A] = " RDP 10.5 server",
  [0x0008000B] = " RDP 10.6 server",
  [0x0008000C] = " RDP 10.7 server",
}

-- T.125 Result enumerated type
CONNECT_RESPONSE_RESULT = {
  [ 0] = "rt-successful",
  [ 1] = "rt-domain-merging",
  [ 2] = "rt-domain-not-hierarchical",
  [ 3] = "rt-no-such-channel",
  [ 4] = "rt-no-such-domain",
  [ 5] = "rt-no-such-user",
  [ 6] = "rt-not-admitted",
  [ 7] = "rt-other-user-id",
  [ 8] = "rt-parameters-unacceptable",
  [ 9] = "rt-token-not-available",
  [10] = "rt-token-not-possessed",
  [11] = "rt-too-many-channels",
  [12] = "rt-too-many-tokens",
  [13] = "rt-too-many-users",
  [14] = "rt-unspecified-failure",
  [15] = "rt-user-rejected",
}

-- requestedProtocols - flag - RDP_NEG_REQ - MS-RDPBCGR 2.2.1.1.1
PROTOCOL_RDP = 0         -- Standard RDP Security
PROTOCOL_SSL = 1         -- TLS 1.0, 1.1, 1.2
PROTOCOL_HYBRID = 2      -- CredSSP (NLA). TLS flag should be set as well
PROTOCOL_RDSTLS = 4      -- RDSTLS
PROTOCOL_HYBRID_EX = 8   -- CredSSP (NLA) with Early User Auth PDU

Packet = {

  TPKT = {

    new = function(self, data)
      local o = { data = tostring(data), version = 3 }
      setmetatable(o, self)
      self.__index = self
      return o
    end,

    __tostring = function(self)
      return string.pack(">BBI2",
        self.version,
        self.reserved or 0,
        (self.data and #self.data + 4 or 4))
      ..self.data
    end,

    parse = function(data)
      local tpkt = Packet.TPKT:new()
      local pos

      tpkt.version, tpkt.reserved, tpkt.length, pos = string.unpack(">BBI2", data)
      tpkt.data = data:sub(pos)
      return tpkt
    end
  },

  ITUT = {

    new = function(self, code, data)
      local o = { data = tostring(data), code = code }
      setmetatable(o, self)
      self.__index = self
      return o
    end,

    parse = function(data)
      local itut = Packet.ITUT:new()
      local pos

      itut.length, itut.code, pos = string.unpack("BB", data)

      if ( itut.code == 0xF0 ) then
        -- X.224 - Data TPDU (DT)
        itut.eot, pos = string.unpack("B", data, pos)
      elseif ( itut.code == 0xD0 ) then
        -- X.224 - Connection Confirm (CC)
        itut.dstref, itut.srcref, itut.class, pos = string.unpack(">I2I2B", data, pos)
      end

      itut.data = data:sub(pos)
      return itut
    end,

    __tostring = function(self)
      local len, eot
      if self.code == 0xF0 then
        eot = "\x80"
        len = 2
      else
        eot = ""
        len = #self.data + 1
      end
      local data = string.pack("BB",
        len,
        self.code or 0)
      .. eot
      .. self.data

      return data
    end,

  },

  ConfCreateResponse = {


    new = function(self)
      local o =  {}
      setmetatable(o, self)
      self.__index = self
      return o
    end,

    parse = function(data)

      local tag_decoder = {}

      tag_decoder["\x0A"] = function( self, encStr, elen, pos )
        return self.decodeInt(encStr, elen, pos)
      end

      local ccr = Packet.ConfCreateResponse:new()

      local decoder = asn1.ASN1Decoder:new()
      decoder:registerTagDecoders( tag_decoder )

      local _, pos = decoder.decodeLength(data, 3)
      local response_result, userdata
      response_result, pos = decoder:decode(data, pos)
      ccr.result = CONNECT_RESPONSE_RESULT[response_result]

      ccr.calledConnectId, pos = decoder:decode(data, pos)

      -- T.125 DomainParameters SEQUENCE
      -- Not interested in its values now, just need to correctly parse
      -- the block so we can arrive at userData
      _, pos =  decoder:decode(data, pos)

      -- T.125 userData OCTO string
      userdata, _ =  decoder:decode(data, pos)

      if userdata == nil then
        return ccr
      end

      -- Hackery to avoid writing ASN.1 PER decoding. Skip over fixed length
      -- T.124 ConnectData header. Decode the length since it can be multiple
      -- bytes. Drops us where we need to be.
       _, pos = asn1.ASN1Decoder.decodeLength(userdata, 22 )
      local block_type, block_len
      while userdata:len() > pos do
        block_type, block_len  = string.unpack("<I2I2", userdata, pos)
        if block_type == 0x0c01 then
          -- 2.2.1.42 Server Core Data - TS_UD_SC_CORE
          local proto_ver = string.unpack("<I4",userdata, pos + 4)
          ccr.proto_version = ("RDP Protocol Version: %s"):format(PROTO_VERSION[proto_ver] or "Unknown")
        elseif block_type == 0x0c02 then
          -- 2.2.1.4.3 Server Security Data - TS_UD_SC_SEC1
          ccr.enc_level = string.unpack("B", userdata, pos + 8)
          ccr.enc_cipher= string.unpack("B", userdata, pos + 4)
        end
        pos = pos + block_len
      end
      return ccr
    end,
  },

}

Request = {

  ConnectionRequest = {

    new = function(self, proto)
      local o = { proto = proto }
      setmetatable(o, self)
      self.__index = self
      return o
    end,

    __tostring = function(self)
      local cookie = "mstshash=nmap"

      local data = string.pack(">I2I2B",
        0x0000, -- dst reference
        0x0000, -- src reference
        0x00) -- class and options
        .. ("Cookie: %s\r\n"):format(cookie)

      if ( self.proto ) then
        data = data .. string.pack("<BBI2I4",
          0x01, -- TYPE_RDP_NEG_REQ
          0x00, -- flags
          0x0008, -- length
          self.proto -- protocol
        )
      end
      return tostring(Packet.TPKT:new(Packet.ITUT:new(0xE0, data)))
    end
  },

  MCSConnectInitial = {

    new = function(self, cipher, server_proto)
      local o = { cipher = cipher, server_proto = server_proto }
      setmetatable(o, self)
      self.__index = self
      return o
    end,

    __tostring = function(self)

      local data = stdnse.fromhex(
      "7f 65" .. -- BER: Application-Defined Type = APPLICATION 101,
      "82 01 94" .. -- BER: Type Length = 404 bytes
      "04 01 01" .. -- Connect-Initial::callingDomainSelector
      "04 01 01" .. -- Connect-Initial::calledDomainSelector
      "01 01 ff" .. -- Connect-Initial::upwardFlag = TRUE
      "30 19" .. -- Connect-Initial::targetParameters (25 bytes)
      "02 01 22" .. -- DomainParameters::maxChannelIds = 34
      "02 01 02" .. -- DomainParameters::maxUserIds = 2
      "02 01 00" .. -- DomainParameters::maxTokenIds = 0
      "02 01 01" .. -- DomainParameters::numPriorities = 1
      "02 01 00" .. -- DomainParameters::minThroughput = 0
      "02 01 01" .. -- DomainParameters::maxHeight = 1
      "02 02 ff ff" .. -- DomainParameters::maxMCSPDUsize = 65535
      "02 01 02" .. -- DomainParameters::protocolVersion = 2
      "30 19" .. -- Connect-Initial::minimumParameters (25 bytes)
      "02 01 01" .. -- DomainParameters::maxChannelIds = 1
      "02 01 01" .. -- DomainParameters::maxUserIds = 1
      "02 01 01" .. -- DomainParameters::maxTokenIds = 1
      "02 01 01" .. -- DomainParameters::numPriorities = 1
      "02 01 00" .. -- DomainParameters::minThroughput = 0
      "02 01 01" .. -- DomainParameters::maxHeight = 1
      "02 02 04 20" .. -- DomainParameters::maxMCSPDUsize = 1056
      "02 01 02" .. -- DomainParameters::protocolVersion = 2
      "30 1c" .. -- Connect-Initial::maximumParameters (28 bytes)
      "02 02 ff ff" .. -- DomainParameters::maxChannelIds = 65535
      "02 02 fc 17" .. -- DomainParameters::maxUserIds = 64535
      "02 02 ff ff" .. -- DomainParameters::maxTokenIds = 65535
      "02 01 01" .. -- DomainParameters::numPriorities = 1
      "02 01 00" .. -- DomainParameters::minThroughput = 0
      "02 01 01" .. -- DomainParameters::maxHeight = 1
      "02 02 ff ff" .. -- DomainParameters::maxMCSPDUsize = 65535
      "02 01 02" .. -- DomainParameters::protocolVersion = 2
      "04 82 01 33" .. -- Connect-Initial::userData (307 bytes)
      "00 05" .. -- object length = 5 bytes
      "00 14 7c 00 01" .. -- object
      "81 2a" .. -- ConnectData::connectPDU length = 42 bytes
      "00 08 00 10 00 01 c0 00 44 75 63 61 81 1c" .. -- PER encoded (ALIGNED variant of BASIC-PER) GCC Conference Create Request PDU
      "01 c0 d8 00" .. -- TS_UD_HEADER::type = CS_CORE (0xc001), length = 216 bytes
      "04 00 08 00" .. -- TS_UD_CS_CORE::version = 0x0008004
      "00 05" .. -- TS_UD_CS_CORE::desktopWidth = 1280
      "20 03" .. -- TS_UD_CS_CORE::desktopHeight = 1024
      "01 ca" .. -- TS_UD_CS_CORE::colorDepth = RNS_UD_COLOR_8BPP (0xca01)
      "03 aa" .. -- TS_UD_CS_CORE::SASSequence
      "09 04 00 00" .. -- TS_UD_CS_CORE::keyboardLayout = 0x409 = 1033 = English (US)
      "28 0a 00 00" .. -- TS_UD_CS_CORE::clientBuild = 2600
      "45 00 4d 00 50 00 2d 00 4c 00 41 00 50 00 2d 00 " ..
      "30 00 30 00 31 00 34 00 00 00 00 00 00 00 00 00 " .. -- TS_UD_CS_CORE::clientName = EMP-LAP-0014
      "04 00 00 00" .. -- TS_UD_CS_CORE::keyboardType
      "00 00 00 00" .. -- TS_UD_CS_CORE::keyboardSubtype
      "0c 00 00 00" .. -- TS_UD_CS_CORE::keyboardFunctionKey
      "00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 " ..
      "00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 " ..
      "00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 " ..
      "00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 " .. -- TS_UD_CS_CORE::imeFileName = ""
      "01 ca" .. -- TS_UD_CS_CORE::postBeta2ColorDepth = RNS_UD_COLOR_8BPP (0xca01)
      "01 00" .. -- TS_UD_CS_CORE::clientProductId
      "00 00 00 00" .. -- TS_UD_CS_CORE::serialNumber
      "18 00" .. -- TS_UD_CS_CORE::highColorDepth = 24 bpp
      "07 00" .. -- TS_UD_CS_CORE::supportedColorDepths =  24 bpp
      "01 00" .. -- TS_UD_CS_CORE::earlyCapabilityFlags
      "00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 " ..
      "00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 " ..
      "00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 " ..
      "00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 " ..  -- TS_UD_CS_CORE::clientDigProductId
      "00" .. -- TS_UD_CS_CORE::connectionType = 0 (not used as RNS_UD_CS_VALID_CONNECTION_TYPE not set)
      "00")   -- TS_UD_CS_CORE::pad1octet
      -- TS_UD_CS_CORE::serverSelectedProtocol
      .. string.pack("<I4", self.server_proto or 0) .. stdnse.fromhex(
      "04 c0 0c 00" .. -- TS_UD_HEADER::type = CS_CLUSTER (0xc004), length = 12 bytes
      "09 00 00 00" .. -- TS_UD_CS_CLUSTER::Flags = 0x0d
      "00 00 00 00" .. -- TS_UD_CS_CLUSTER::RedirectedSessionID
      "02 c0 0c 00") -- TS_UD_HEADER::type = CS_SECURITY (0xc002), length = 12 bytes
      -- "1b 00 00 00" .. -- TS_UD_CS_SEC::encryptionMethods
      .. string.pack("<I4", self.cipher or 0) .. stdnse.fromhex(
      "00 00 00 00" .. -- TS_UD_CS_SEC::extEncryptionMethods
      "03 c0 2c 00" .. -- TS_UD_HEADER::type = CS_NET (0xc003), length = 44 bytes
      "03 00 00 00" .. -- TS_UD_CS_NET::channelCount = 3
      "72 64 70 64 72 00 00 00" .. -- CHANNEL_DEF::name = "rdpdr"
      "00 00 80 80" .. -- CHANNEL_DEF::options = 0x80800000
      "63 6c 69 70 72 64 72 00" .. -- CHANNEL_DEF::name = "cliprdr"
      "00 00 a0 c0" .. -- CHANNEL_DEF::options = 0xc0a00000
      "72 64 70 73 6e 64 00 00" .. -- CHANNEL_DEF::name = "rdpsnd"
      "00 00 00 c0" -- CHANNEL_DEF::options = 0xc0000000
      )
      return tostring(Packet.TPKT:new(Packet.ITUT:new(0xF0, data)))
    end

  },

}

Response = {

  ConnectionConfirm = {

    new = function(self)
      local o = { }
      setmetatable(o, self)
      self.__index = self
      return o
    end,

    parse = function(data)
      local cc = Response.ConnectionConfirm:new()

      cc.tpkt = Packet.TPKT.parse(data)
      cc.itut = Packet.ITUT.parse(cc.tpkt.data)
      return cc
    end,

  },

  MCSConnectResponse = {
    new = function(self)
      local o = { }
      setmetatable(o, self)
      self.__index = self
      return o
    end,

    parse = function(data)
      local cr = Response.MCSConnectResponse:new()

      cr.tpkt = Packet.TPKT.parse(data)
      cr.itut = Packet.ITUT.parse(cr.tpkt.data)
      if ( cr.itut.code == 0xF0 ) then
        -- X.224 - Data TPDU (DT)
        cr.ccr  = Packet.ConfCreateResponse.parse(cr.itut.data)
      end
      return cr
    end
  }

}

Comm = {

  -- Creates a new Comm instance
  -- @param host table
  -- @param port table
  -- @return o instance of Comm
  new = function(self, host, port)
    local o = { host = host, port = port }
    setmetatable(o, self)
    self.__index = self
    return o
  end,

  -- Connect to the server
  -- @return status true on success, false on failure
  -- @return err string containing error message, if status is false
  connect = function(self)
    self.socket = nmap.new_socket()
    self.socket:set_timeout(5000)
    if ( not(self.socket:connect(self.host, self.port)) ) then
      return false, "Failed connecting to server"
    end
    return true
  end,

  -- Close the connection to the server
  -- @return status true on success, false on failure
  close = function(self)
    return self.socket:close()
  end,

  -- Sends a message to the server
  -- @param pkt an instance of Request.*
  -- @return status true on success, false on failure
  -- @return err string containing error message, if status is false
  send = function(self, pkt)
    return self.socket:send(tostring(pkt))
  end,

  -- Receives a message from the server
  -- @return status true on success, false on failure
  -- @return err string containing error message, if status is false
  recv = function(self)
    return self.socket:receive()
  end,

  -- Sends a message to the server and receives the response
  -- @param pkt an instance of Request.*
  -- @return status true on success, false on failure
  -- @return err string containing error message, if status is false
  --         pkt instance of Response.* on success
  exch = function(self, pkt)
    local status, err = self:send(pkt)
    if ( not(status) ) then
      return false, err
    end

    local _, data = self:recv()
    if ( #data< 5 ) then
      return false, "Packet too short"
    end

    local itut_code = string.byte(data, 6)
    if ( itut_code == 0xD0 ) then
      stdnse.debug2("RDP: Received ConnectionConfirm response")
      return true, Response.ConnectionConfirm.parse(data)
    elseif ( itut_code == 0xF0 ) then
      return true, Response.MCSConnectResponse.parse(data)
    elseif itut_code ~= nil then
        stdnse.debug1(("comm:exch - Unknown itut_code: %s"):format(itut_code))
    end
    return false, "Received unhandled packet"
  end,
}

return _ENV;
