local bin = require "bin"
local bit = require "bit"
local brute = require "brute"
local creds = require "creds"
local nmap = require "nmap"
local shortport = require "shortport"
local stdnse = require "stdnse"
local string = require "string"
local table = require "table"

description = [[
Performs brute force password auditing against the BackOrifice service. The
<code>backorifice-brute.ports</code> script argument is mandatory (it specifies ports to run
the script against).
]]

---
-- @usage
-- nmap -sU --script backorifice-brute <host> --script-args backorifice-brute.ports=<ports>
--
-- @arg backorifice-brute.ports (mandatory) List of UDP ports to run the script against separated with "," ex. "U:31337,25252,151-222", "U:1024-1512"
--
-- This script uses the brute library to perform password guessing. A
-- successful password guess is stored in the nmap registry, under the
-- <code>nmap.registry.credentials.backorifice</code> table for other BackOrifice
-- scripts to use.
--
-- @output
-- PORT       STATE  SERVICE
-- 31337/udp  open   BackOrifice
-- | backorifice-brute:
-- |   Accounts:
-- |     michael => Valid credentials
-- |   Statistics
-- |_    Perfomed 60023 guesses in 467 seconds, average tps: 138
--

-- Summary
-- -------
--   x The Driver class contains the driver implementation used by the brute
--     library
--   x The backorifice class contains the backorifice client implementation
--

author = "Gorjan Petrovski"
license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
categories = {"intrusive", "brute"}


-- This portrule succeeds only when the open|filtered port is in the port range
-- which is specified by the ports script argument
portrule = function(host, port)
  if not stdnse.get_script_args(SCRIPT_NAME .. ".ports") then
    stdnse.debug3("Skipping '%s' %s, 'ports' argument is missing.",SCRIPT_NAME, SCRIPT_TYPE)
    return false
  end

  local ports = stdnse.get_script_args(SCRIPT_NAME .. ".ports")

  --print out a debug message if port 31337/udp is open
  if port.number==31337 and port.protocol == "udp" and not(ports) then
    stdnse.debug1("Port 31337/udp is open. Possibility of version detection and password bruteforcing using the backorifice-brute script")
    return false
  end

  return port.protocol == "udp" and stdnse.in_port_range(port, ports:gsub(",",",") ) and
    not(shortport.port_is_excluded(port.number,port.protocol))
end

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

  --- Initializes the backorifice object
  --
  initialize = function(self)
    --create socket
    self.socket = nmap.new_socket("udp")
    self.socket:set_timeout(self.host.times.timeout * 1000)
    return true
  end,

  --- Attempts to send an encrypted PING packet to BackOrifice service
  --
  -- @param password string containing password for encryption
  -- @param initial_seed number containing initial encryption seed
  -- @return status, true on success, false on failure
  -- @return err string containing error message on failure
  try_password = function(self, password, initial_seed)
    --initialize BackOrifice PING packet:   |MAGICSTRING|size|packetID|TYPE_PING|arg1|arg_separat|arg2|CRC/disregarded|
    local PING_PACKET = bin.pack("A<IICACAC", "*!*QWTY?",  19,       0,     0x01,  "",       0x00,  "",           0x00)
    local seed, status, response, encrypted_ping

    if not(initial_seed) then
      seed = self:gen_initial_seed(password)
    else
      seed = initial_seed
    end

    encrypted_ping = self:BOcrypt(PING_PACKET,seed)

    status, response = self.socket:sendto(self.host, self.port, encrypted_ping)
    if not(status) then
      return false, response
    end
    status, response = self.socket:receive()

    -- The first 8 bytes of both response and sent data are
    -- magicstring = "*!*QWTY?", without the quotes, and since
    -- both are encrypted with the same initial seed, this is
    -- how we verify we are talking to a BackOrifice service.
    -- The statement is optimized so as not to decrypt unless
    -- comparison of encrypted magicstrings succeeds
    if status and response:sub(1,8) == encrypted_ping:sub(1,8)
        and self:BOcrypt(response,seed):match("!PONG!(1%.20)!.*!") then
      local BOversion, BOhostname = self:BOcrypt(response,seed):match("!PONG!(1%.20)!(.*)!")
      self:insert_version_info(BOversion,BOhostname,nil,password)
      return true
    else
      if not(status) then
        return false, response
      else
        return false,"Response not recognized."
      end
    end
  end,

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

  --- Generates the initial encryption seed from a password
  --
  -- @param password string containing password
  -- @return seed number containing initial seed
  gen_initial_seed = function(self, password)
    if password == nil then
      return 31337
    else
      local y = #password
      local z = 0

      for x = 1,y do
        local pchar = string.byte(password,x)
        z = z + pchar
      end

      for x=1,y do
        local pchar = string.byte(password,x)
        if (x-1)%2 == 1 then
          z = z - (pchar * (y-(x-1)+1))
        else
          z = z + (pchar * (y-(x-1)+1))
        end
        z = z % 0x7fffffff
      end
      z = (z*y) % 0x7fffffff
      return z
    end
  end,

  --- Generates next encryption seed from given seed
  --
  -- @param seed number containing current seed
  -- @return seed number containing next seed
  gen_next_seed = function(self, seed)
    seed = seed*214013 + 2531011
    seed = bit.band(seed,0xffffff)
    return seed
  end,

  --- Encrypts/decrypts data using BackOrifice algorithm
  --
  -- @param data binary string containing data to be encrypted/decrypted
  -- @param initial_seed number containing initial encryption seed
  -- @return data binary string containing encrypted/decrypted data
  BOcrypt = function(self, data, initial_seed )
    if data==nil then return end

    local output =""
    local seed = initial_seed
    local data_byte
    local crypto_byte

    for i = 1, #data  do
      data_byte = string.byte(data,i)

      --calculate next seed
      seed = self:gen_next_seed(seed)
      --calculate encryption key based on seed
      local key = bit.band(bit.arshift(seed,16), 0xff)

      crypto_byte = bit.bxor(data_byte,key)
      output = bin.pack("AC",output,crypto_byte)
      --ARGSIZE limitation from BackOrifice server
      if i == 256 then break end
    end
    return output
  end,

  insert_version_info = function(self,BOversion,BOhostname,initial_seed,password)
    if not self.port.version then self.port.version={} end
    if not self.port.version.name then
      self.port.version.name ="BackOrifice"
      self.port.version.name_confidence = 10
    end
    if not self.port.version.product then self.port.version.product ="BackOrifice trojan" end
    if not self.port.version.version then self.port.version.version = BOversion end
    if not self.port.version.extrainfo then
      if not password then
        if not initial_seed then
          self.port.version.extrainfo = "no password"
        else
          self.port.version.extrainfo = "initial encryption seed="..initial_seed
        end
      else
        self.port.version.extrainfo = "password="..password
      end
    end
    self.port.version.hostname = BOhostname
    if not self.port.version.ostype then self.port.version.ostype = "Windows" end
    nmap.set_port_version(self.host, self.port)
    nmap.set_port_state(self.host,self.port,"open")
  end
}

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

  connect=function(self)
    --only initialize since BackOrifice service knows no connect()
    self.bo = backorifice:new(self.host,self.port)
    self.bo:initialize()
    return true
  end,

  disconnect = function( self )
    self.bo:close()
  end,

  --- Attempts to send encrypted PING packet to BackOrifice service
  --
  -- @param username string containing username which is disregarded
  -- @param password string containing login password
  -- @return brute.Error object on failure
  --         creds.Account object on success
  login = function( self, username, password )
    local status, msg = self.bo:try_password(password,nil)
    if status then
      if not(nmap.registry['credentials']) then
        nmap.registry['credentials']={}
      end
      if ( not( nmap.registry.credentials['backorifice'] ) ) then
        nmap.registry.credentials['backorifice'] = {}
      end
      table.insert( nmap.registry.credentials.backorifice, { password = password } )
      return true, creds.Account:new("", password, creds.State.VALID)
    else
      -- The only indication that the password is incorrect is a timeout
      local err = brute.Error:new( "Incorrect password" )
      err:setRetry(false)
      return false, err
    end
  end,

}

action = function( host, port )

  local status, result
  local engine = brute.Engine:new(Driver,host,port)

  engine.options.firstonly = true
  engine.options.passonly = true
  engine.options.script_name = SCRIPT_NAME

  status, result = engine:start()

  return result
end
