local msrpc = require "msrpc"
local nmap = require "nmap"
local smb = require "smb"
local stdnse = require "stdnse"
local string = require "string"
local table = require "table"
local vulns = require "vulns"
description = [[
Checks for vulnerability:
* Conficker, an infection by the Conficker worm
WARNING: These checks are dangerous, and are very likely to bring down a server.
These should not be run in a production environment unless you (and, more importantly,
the business) understand the risks!
As a system administrator, performing these kinds of checks is crucial, because
a lot more damage can be done by a worm or a hacker using this vulnerability than
by a scanner. Penetration testers, on the other hand, might not want to use this
script -- crashing services is not generally a good way of sneaking through a
network.
If you set the script parameter unsafe, then scripts will run that are almost
(or totally) guaranteed to crash a vulnerable system; do NOT specify unsafe
in a production environment! And that isn't to say that non-unsafe scripts will
not crash a system, they're just less likely to.
If you set the script parameter safe, then script will run that rarely or never
crash a vulnerable system. No promises, though.
]]
---
--@usage
-- nmap --script smb-vuln-conficker.nse -p445 
-- sudo nmap -sU -sS --script smb-vuln-conficker.nse -p U:137,T:139 
--
--@output
-- Host script results:
-- | smb-check-vulns:
-- |_  Conficker: Likely CLEAN
--
-- @args unsafe If set, this script will run checks that, if the system isn't
--       patched, are basically guaranteed to crash something. Remember that
--       non-unsafe checks aren't necessarily safe either)
-- @args safe   If set, this script will only run checks that are known (or at
--       least suspected) to be safe.
-----------------------------------------------------------------------
author = "Ron Bowes"
copyright = "Ron Bowes"
license = "Same as Nmap--See http://nmap.org/book/man-legal.html"
categories = {"intrusive","exploit","dos","vuln"}
-- run after all smb-* scripts (so if it DOES crash something, it doesn't kill
-- other scans have had a chance to run)
dependencies = {
  "smb-brute", "smb-enum-sessions", "smb-security-mode",
  "smb-enum-shares", "smb-server-stats",
  "smb-enum-domains", "smb-enum-users", "smb-system-info",
  "smb-enum-groups", "smb-os-discovery", "smb-enum-processes",
  "smb-psexec",
};
hostrule = function(host)
  return smb.get_port(host) ~= nil
end
local VULNERABLE = 1
local PATCHED    = 2
local UNKNOWN    = 3
local NOTRUN     = 4
local INFECTED   = 5
local INFECTED2  = 6
local CLEAN      = 7
local NOTUP      = 8
-- Help messages for the more common errors seen by the Conficker check.
CONFICKER_ERROR_HELP = {
  ["NT_STATUS_BAD_NETWORK_NAME"] =
  [[UNKNOWN; Network name not found (required service has crashed). (Error NT_STATUS_BAD_NETWORK_NAME)]],
  -- http://seclists.org/nmap-dev/2009/q1/0918.html "non-Windows boxes (Samba on Linux/OS X, or a printer)"
  -- http://www.skullsecurity.org/blog/?p=209#comment-156
  --   "That means either it isn’t a Windows machine, or the service is
  --    either crashed or not running. That may indicate a failed (or
  --    successful) exploit attempt, or just a locked down system.
  --    NT_STATUS_OBJECT_NAME_NOT_FOUND can be returned if the browser
  --    service is disabled. There are at least two ways that can happen:
  --    1) The service itself is disabled in the services list.
  --    2) The registry key HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\Browser\Parameters\MaintainServerList
  --       is set to Off/False/No rather than Auto or yes.
  --    On these systems, if you reenable the browser service, then the
  --    test will complete."
  ["NT_STATUS_OBJECT_NAME_NOT_FOUND"] =
  [[UNKNOWN; not Windows, or Windows with disabled browser service (CLEAN); or Windows with crashed browser service (possibly INFECTED).
|  If you know the remote system is Windows, try rebooting it and scanning
|_ again. (Error NT_STATUS_OBJECT_NAME_NOT_FOUND)]],
  -- http://www.skullsecurity.org/blog/?p=209#comment-100
  --   "That likely means that the server has been locked down, so we
  --    don’t have access to the necessary pipe. Fortunately, that means
  --    that neither does Conficker — NT_STATUS_ACCESS_DENIED probably
  --    means you’re ok."
  ["NT_STATUS_ACCESS_DENIED"] =
  [[Likely CLEAN; access was denied.
|  If you have a login, try using --script-args=smbuser=xxx,smbpass=yyy
|  (replace xxx and yyy with your username and password). Also try
|_ smbdomain=zzz if you know the domain. (Error NT_STATUS_ACCESS_DENIED)]],
  -- The cause of these two is still unknown.
  -- ["NT_STATUS_NOT_SUPPORTED"] =
  -- [[]]
  -- http://thatsbroken.com/?cat=5 (doesn't seem common)
  -- ["NT_STATUS_REQUEST_NOT_ACCEPTED"] =
  -- [[]]
}
---Check if the server is infected with Conficker. This can be detected by a modified MS08-067 patch,
-- which rejects a different illegal string than the official patch rejects.
--
-- Based loosely on the Simple Conficker Scanner, found here:
-- http://iv.cs.uni-bonn.de/wg/cs/applications/containing-conficker/
--
-- If there's a licensing issue, please let me (Ron Bowes) know so I can fix it
--
--@param host The host object.
--@return (status, result) If status is false, result is an error code; otherwise, result is either
--        INFECTED for infected or CLEAN for not infected.
function check_conficker(host)
  local status, smbstate
  local bind_result, netpathcompare_result
  -- Create the SMB session
  status, smbstate = msrpc.start_smb(host, "\\\\BROWSER", true)
  if(status == false) then
    return false, smbstate
  end
  -- Bind to SRVSVC service
  status, bind_result = msrpc.bind(smbstate, msrpc.SRVSVC_UUID, msrpc.SRVSVC_VERSION, nil)
  if(status == false) then
    msrpc.stop_smb(smbstate)
    return false, bind_result
  end
  -- Try checking a valid string to find Conficker.D
  local netpathcanonicalize_result, error_result
  status, netpathcanonicalize_result, error_result = msrpc.srvsvc_netpathcanonicalize(smbstate, host.ip, "\\")
  if(status == true and netpathcanonicalize_result['can_path'] == 0x5c45005c) then
    msrpc.stop_smb(smbstate)
    return true, INFECTED2
  end
  -- Try checking an illegal string ("\..\") to find Conficker.C and earlier
  status, netpathcanonicalize_result, error_result = msrpc.srvsvc_netpathcanonicalize(smbstate, host.ip, "\\..\\")
  if(status == false) then
    if(string.find(netpathcanonicalize_result, "INVALID_NAME")) then
      msrpc.stop_smb(smbstate)
      return true, CLEAN
    elseif(string.find(netpathcanonicalize_result, "WERR_INVALID_PARAMETER") ~= nil) then
      msrpc.stop_smb(smbstate)
      return true, INFECTED
    else
      msrpc.stop_smb(smbstate)
      return false, netpathcanonicalize_result
    end
  end
  -- Stop the SMB session
  msrpc.stop_smb(smbstate)
  return true, CLEAN
end
action = function(host)
  local status, result, message
  local response = {}
  local vuln_report = vulns.Report:new(SCRIPT_NAME, host)
  local vuln_table = {
    title = 'Conficker, an infection by the Conficker worm',
    state = vulns.STATE.NOT_VULN
  }
  -- Check for Conficker
  status, result = check_conficker(host)
  if(status == false) then
    -- local msg = CONFICKER_ERROR_HELP[result] or "UNKNOWN; got error " .. result
    -- table.insert(response, get_response("Conficker", msg, nil, 1)) -- Only set verbosity for this, since it might be an error or it might be UNKNOWN
    vuln_table.state = vulns.STATE.NOT_VULN
  else
    if(result == CLEAN) then
      -- table.insert(response, get_response("Conficker", "Likely CLEAN",    nil,                        1))
      vuln_table.state = vulns.STATE.NOT_VULN
    elseif(result == INFECTED) then
      -- table.insert(response, get_response("Conficker", "Likely INFECTED", "by Conficker.C or lower",  0))
      vuln_table.state = vulns.STATE.LIKELY_VULN
    elseif(result == INFECTED2) then
      -- table.insert(response, get_response("Conficker", "Likely INFECTED", "by Conficker.D or higher", 0))
      vuln_table.state = vulns.STATE.LIKELY_VULN
    else
      -- table.insert(response, get_response("Conficker", "UNKNOWN",         result,                     0, 1))
      vuln_table.state = vulns.STATE.NOT_VULN
    end
  end
  return vuln_report:make_output(vuln_table)
end