local nmap      = require "nmap"
local stdnse    = require "stdnse"
local shortport = require "shortport"
local tn3270    = require "tn3270"
local brute     = require "brute"
local creds     = require "creds"
local unpwdb    = require "unpwdb"
local io        = require "io"
local table     = require "table"
local string   = require "string"
local stringaux = require "stringaux"


description = [[
CICS transaction ID enumerator for IBM mainframes.
This script is based on mainframe_brute by Dominic White
(https://github.com/sensepost/mainframe_brute). However, this script
doesn't rely on any third party libraries or tools and instead uses
the NSE TN3270 library which emulates a TN3270 screen in lua.

CICS only allows for 4 byte transaction IDs, that is the only specific rule
found for CICS transaction IDs.
]]

---
-- @args idlist Path to list of transaction IDs.
--  Defaults to the list of CICS transactions from IBM.
-- @args cics-enum.commands Commands in a semi-colon separated list needed
--  to access CICS. Defaults to <code>CICS</code>.
-- @args cics-enum.path Folder used to store valid transaction id 'screenshots'
--  Defaults to <code>None</code> and doesn't store anything.
-- @args cics-enum.user Username to use for authenticated enumeration
-- @args cics-enum.pass Password to use for authenticated enumeration
--
-- @usage
-- nmap --script=cics-enum -p 23 <targets>
--
-- nmap --script=cics-enum --script-args=idlist=default_cics.txt,
-- cics-enum.command="exit;logon applid(cics42)",
-- cics-enum.path="/home/dade/screenshots/",cics-enum.noSSL=true -p 23 <targets>
--
-- @output
-- PORT   STATE SERVICE
-- 23/tcp open  tn3270
-- | cics-enum:
-- |   Accounts:
-- |     CBAM: Valid - CICS Transaction ID
-- |     CETR: Valid - CICS Transaction ID
-- |     CEST: Valid - CICS Transaction ID
-- |     CMSG: Valid - CICS Transaction ID
-- |     CEDA: Valid - CICS Transaction ID
-- |     CEDF: Potentially Valid - CICS Transaction ID
-- |     DSNC: Valid - CICS Transaction ID
-- |_  Statistics: Performed 31 guesses in 114 seconds, average tps: 0
--
-- @changelog
-- 2015-07-04 - v0.1 - created by Soldier of Fortran
-- 2015-11-14 - v0.2 - rewrote iterator
-- 2017-01-22 - v0.3 - added authenticated CICS ID enumeration
-- 2019-02-01 - v0.4 - Removed TN3270E support (breaks location)
--
-- @author Philip Young
-- @copyright Same as Nmap--See https://nmap.org/book/man-legal.html
--

author = "Philip Young aka Soldier of Fortran"
license = "Same as Nmap--See https://nmap.org/book/man-legal.html"
categories = {"intrusive", "brute"}
portrule = shortport.port_or_service({23,992}, "tn3270")

--- Saves the Screen generated by the CICS command to disk
--
-- @param filename string containing the name and full path to the file
-- @param data contains the data
-- @return status true on success, false on failure
-- @return err string containing error message if status is false
local function save_screens( filename, data )
  local f = io.open( filename, "w")
  if not f then return false, ("Failed to open file (%s)"):format(filename) end
  if not(f:write(data)) then return false, ("Failed to write file (%s)"):format(filename) end
  f:close()
  return true
end

Driver = {
  new = function(self, host, port, options)
    local o = {}
    setmetatable(o, self)
    self.__index = self
    o.host = host
    o.port = port
    o.options = options
    o.tn3270 = tn3270.Telnet:new()
    o.tn3270:disable_tn3270e()
    return o
  end,
  connect = function( self )
    local status, err = self.tn3270:initiate(self.host,self.port)
    self.tn3270:get_screen_debug(2)
    if not status then
      stdnse.debug("Could not initiate TN3270: %s", err )
      return false
    end
    return true
  end,
  disconnect = function( self )
    self.tn3270:disconnect()
    self.tn3270 = nil
    return true
  end,
  login = function (self, user, pass) -- pass is actually the CICS transaction we want to try
    local commands = self.options['key1']
    local path = self.options['key2']
    local cics_user = self.options['user']
    local cics_pass = self.options['pass']
    local timeout = 300
    local max_blank = 1
    local loop = 1
    local err, status
    stdnse.debug(2,"Getting to CICS")
    local run = stringaux.strsplit(";%s*", commands)
    for i = 1, #run do
      stdnse.debug(1,"Issuing Command (#%s of %s): %s", i, #run ,run[i])
      self.tn3270:send_cursor(run[i])
      self.tn3270:get_all_data()
      self.tn3270:get_screen_debug(2)
    end
    while self.tn3270:isClear() and max_blank < 7 do
      stdnse.debug(2, "Screen is not clear for %s. Reading all data with a timeout of %s. Count %s",pass, timeout, max_blank)
      self.tn3270:get_all_data(timeout)
      timeout = timeout + 100
      max_blank = max_blank + 1
    end

    while not self.tn3270:isClear() and loop < 10 do
      -- by this point we're at *some* CICS transaction
      -- so we send F3 to exit it
      stdnse.debug(2,"Sending: F3")
      self.tn3270:send_pf(3) -- send F3
      self.tn3270:get_all_data()
      self.tn3270:get_screen_debug(2)
      -- now we want to clear the screen
      self.tn3270:send_clear()
      self.tn3270:get_all_data()
      stdnse.debug(2,"Current CLEARed Screen. Loop: %s", loop )
      self.tn3270:get_screen_debug(2)
      loop = loop + 1
    end

    if loop == 10 then
      -- something is wrong but we can still try transactions. Print error to debug.
      stdnse.debug('Error. Failed to get to a blank screen under CICS (sending F3 followed by CLEAR). Try lowering maxthreads to fix.')
    end
    -- If username/password provided try to authenticate first
    if not (cics_user == nil and cics_pass == nil) then -- We're doing authenticated CICS testing now baby!
      stdnse.debug(2,'Logging in with %s / %s for auth testing', cics_user, cics_pass)
      self.tn3270:send_cursor('CESN')
      self.tn3270:get_all_data()
      self.tn3270:get_screen_debug(2)
      local fields = self.tn3270:writeable() -- Get the writeable field areas
      local user_loc = {fields[1][1],cics_user}   -- This is the 'UserID:' field
      local pass_loc = {fields[3][1],cics_pass}   -- This is the 'Password:' field ([2] is a group ID)
      stdnse.debug(2,'Trying CICS: %s : %s', user, pass)
      self.tn3270:send_locations({user_loc,pass_loc})
      self.tn3270:get_all_data()
      stdnse.debug(2,"Screen Received for User ID: %s / %s", user, pass)
      self.tn3270:get_screen_debug(2)
      local count = 1
      while not self.tn3270:find('DFHCE3549') and count < 6 do -- some systems show a message for a bit before we get to CICS again
          self.tn3270:get_all_data(1000) -- loop for 6 seconds
          count = count + 1
      end
    end
    self.tn3270:get_screen_debug(2)
    self.tn3270:send_clear()
    self.tn3270:get_all_data()
    self.tn3270:get_screen_debug(2)
    stdnse.verbose("Trying Transaction ID: %s", pass)
    self.tn3270:send_cursor(pass)
    self.tn3270:get_all_data()

    max_blank = 1
    while self.tn3270:isClear() and max_blank < 7 do
      stdnse.debug(2, "Screen is not clear for %s. Reading all data with a timeout of %s. Count %s",pass, timeout, max_blank)
      self.tn3270:get_all_data(timeout)
      timeout = timeout + 100
      max_blank = max_blank + 1
    end

    stdnse.debug(2,"Screen Received for Transaction ID: %s", pass)
    self.tn3270:get_screen_debug(2)
    if self.tn3270:find('not recognized') or self.tn3270:find('DFHAC2002') then -- known invalid command
      stdnse.debug("Invalid CICS Transaction ID: %s", string.upper(pass))
      return false,  brute.Error:new( "Incorrect CICS Transaction ID" )
    elseif self.tn3270:isClear() then
      stdnse.debug(2,"Empty Screen when we expect an error.")
      -- this can mean that the transaction ID was valid
      -- but it didn't send a screen update so you should check by hand.
      -- We're not dumping this screen to disk because it's blank.
      return true, creds.Account:new("CICS ID [blank screen]", string.upper(pass), creds.State.VALID)
    elseif self.tn3270:find('Unauthorized') or self.tn3270:find('DFHAC2002') then
      -- this is a VALID cics transaction but you must be authenticated to used it
      -- This will be the same screen for each so we dont bother saving it either
      stdnse.verbose("Valid CICS Transaction ID [requires auth]: %s", string.upper(pass))
      return true, creds.Account:new("CICS ID [requires auth]", string.upper(pass), creds.State.VALID)
    elseif self.tn3270:find('DFHAC2008') or self.tn3270:find('DFHAC2206') or self.tn3270:find('DFHAC2028') or
           self.tn3270:find('DFHRT4415') or self.tn3270:find('DFHRT4480') or self.tn3270:find('TSS7254E') then
      -- these are technically valid CICS transactions
      -- but they are of little/no value. If verbosity is turned way up we'll return these/save a screenshot
      -- otherwise there's no point
      -- DFHAC2008 -- TranID has been Disabled
      -- DFHAC2206 -- Abend
      -- DFHRT4415 -- Cannot access through terminal
      -- DFHRT4480 -- No Longer Supported
      -- DFHAC2028 -- cannot be used
      -- TSS7254E  -- Access not available through this facility
      stdnse.verbose("Valid CICS Transaction ID [Abbend or ID Disabled]: %s", string.upper(pass))
      if nmap.verbosity() > 3 then
        if path ~= nil then
          stdnse.verbose(2,"Writting screen to: %s", path..string.upper(pass)..".txt")
          status, err = save_screens(path..string.upper(pass)..".txt",self.tn3270:get_screen())
          if not status then
            stdnse.verbose(2,"Failed writting screen to: %s", path..string.upper(pass)..".txt")
          end
        end
        return true, creds.Account:new("CICS ID [Abbend]", string.upper(pass), creds.State.VALID)
      else
        return false, brute.Error:new( "Correct Transaction ID - Access Denied" )
      end
    elseif not (cics_user == nil and cics_pass == nil) and
           (self.tn3270:find('TSS7251E') or self.tn3270:find('DFHAC2033')) then
      -- We've logged on but we don't have access to this transaction
      -- TSS7251E  : Access Denied to PROGRAM <X>
      -- DFHAC2033 : You are not authorized to use transaction <X>
      stdnse.verbose("Valid CICS Transaction ID [Access Denied]: %s", string.upper(pass))
      if nmap.verbosity() > 3 then
        return true, creds.Account:new("CICS ID [Access Denied]", string.upper(pass), creds.State.VALID)
      else
        return false, brute.Error:new( "Correct Transaction ID - Access Denied" )
      end
    else
      stdnse.verbose("Valid CICS Transaction ID: %s", string.upper(pass))
      if path ~= nil then
        stdnse.verbose(2,"Writting screen to: %s", path..string.upper(pass)..".txt")
        status, err = save_screens(path..string.upper(pass)..".txt",self.tn3270:get_screen())
        if not status then
          stdnse.verbose(2,"Failed writting screen to: %s", path..string.upper(pass)..".txt")
        end
      end
      return true, creds.Account:new("CICS ID", string.upper(pass), creds.State.VALID)
    end
    return false, brute.Error:new("Something went wrong, we didn't get a proper response")
  end
}

--- Tests the target to see if we can even get to CICS
--
-- @param host host NSE object
-- @param port port NSE object
-- @param user CICS userID
-- @param pass CICS userID password
-- @param commands optional script-args of commands to use to get to CICS
-- @return status true on success, false on failure

local function cics_test( host, port, commands, user, pass )
  stdnse.debug("Checking for CICS")
  local tn = tn3270.Telnet:new()
  tn:disable_tn3270e()
  local status, err = tn:initiate(host,port)
  local msg = 'Unable to get to CICS'
  local cics = false -- initially we're not at CICS
  if not status then
    stdnse.debug("Could not initiate TN3270: %s", err )
    return cics
  end
  tn:get_screen_debug(2) -- prints TN3270 screen to debug
  stdnse.debug("Getting to CICS")
  local run = stringaux.strsplit(";%s*", commands)
  for i = 1, #run do
    stdnse.debug(1,"Issuing Command (#%s of %s): %s", i, #run ,run[i])
    tn:send_cursor(run[i])
    tn:get_all_data()
    tn:get_screen_debug(2)
  end
  tn:get_all_data()
  tn:get_screen_debug(2) -- for debug purposes
  -- we should technically be at CICS. So we send:
  --   * F3 to exit the CICS program
  --   * CLEAR (a tn3270 command) to clear the screen.
  --     (you need to clear before sending a transaction ID)
  --   * a known default CICS transaction ID with predictable outcome
  --     (CESF with 'Sign-off is complete.' as the result)
  -- to confirm that we were in CICS. If so we return true
  -- otherwise we return false
  local count = 1
  while not tn:isClear() and count < 6 do
    -- some systems will just kick you off others are slow in responding
    -- this loop continues to try getting out of CICS 6 times. If it can't
    -- then we probably weren't in CICS to begin with.
    if tn:find("Signon") then
      stdnse.debug(2,"Found 'Signon' sending PF3")
      tn:send_pf(3)
      tn:get_all_data()
    end
    tn:get_all_data()
    stdnse.debug(2,"Clearing the Screen")
    tn:send_clear()
    tn:get_all_data()
    tn:get_screen_debug(2)
    count = count + 1
  end
  if count == 6 then
    return cics
  end
  stdnse.debug(2,"Sending CESF (CICS Default Sign-off)")
  tn:send_cursor('CESF')
  tn:get_all_data()
  if tn:isClear() then
    tn:get_all_data(1000)
  end
  tn:get_screen_debug(2)

  if tn:find('off is complete.') then
      cics = true
  end

  if not (user == nil and pass == nil) then -- We're doing authenticated CICS testing now baby!
    stdnse.verbose(2,'Logging in with %s / %s for auth testing', user, pass)
    tn:send_clear()
    tn:get_all_data()
    tn:get_screen_debug(2)
    tn:send_cursor('CESN')
    tn:get_all_data()
    tn:get_screen_debug(2)
    local fields = tn:writeable() -- Get the writeable field areas
    local user_loc = {fields[1][1],user}   -- This is the 'UserID:' field
    local pass_loc = {fields[3][1],pass}   -- This is the 'Password:' field ([2] is a group ID)
    stdnse.verbose('Trying CICS: %s : %s', user, pass)
    tn:send_locations({user_loc,pass_loc})
    tn:get_all_data()
    stdnse.debug(2,"Screen Received for User ID: %s / %s", user, pass)
    tn:get_screen_debug(2)
    count = 1
    while not tn:find('DFHCE3549') and count < 6 do
	      tn:get_all_data(1000) -- loop for 6 seconds
        tn:get_screen_debug(2)
        count = count + 1
    end
    if not tn:find('DFHCE3549') then
        cics = false
        msg = 'Unable to access CICS with User: '..user..' / Pass: '..pass
    else
        tn:send_cursor('CESF')
        tn:get_all_data()
    end
  end

  tn:disconnect()
  return cics,msg
end

-- Filter iterator for unpwdb
-- CICS is limited to 4 characters.
local valid_cics = function(x)
  return (string.len(x) <= 4)
end

function iter(t)
  local i, val
  return function()
    i, val = next(t, i)
    return val
  end
end

action = function(host, port)
  local cics_id_file = stdnse.get_script_args("idlist")
  local path = stdnse.get_script_args(SCRIPT_NAME .. '.path') -- Folder for screenshots
  local commands = stdnse.get_script_args(SCRIPT_NAME .. '.commands') or 'cics'-- VTAM commands/macros to get to CICS
  local username = stdnse.get_script_args(SCRIPT_NAME .. '.user') or nil
  local password = stdnse.get_script_args(SCRIPT_NAME .. '.pass') or nil
  local cics_ids = {"CADP", "CATA", "CATD", "CATR", "CBAM", "CCIN", "CCRL", "CDBC", "CDBD",
    "CDBF", "CDBI", "CDBM", "CDBN", "CDBO", "CDBQ", "CDBT", "CDFS", "CDST",
    "CDTS", "CEBR", "CEBT", "CECI", "CECS", "CEDA", "CEDB", "CEDC", "CEDF",
    "CEDX", "CEGN", "CEHP", "CEHS", "CEKL", "CEMN", "CEMT", "CEOT", "CEPD",
    "CEPF", "CEPH", "CEPM", "CEPQ", "CEPS", "CEPT", "CESC", "CESD", "CESF",
    "CESL", "CESN", "CEST", "CETR", "CEX2", "CFCL", "CFCR", "CFOR", "CFQR",
    "CFQS", "CFTL", "CFTS", "CGRP", "CHLP", "CIDP", "CIEP", "CIND", "CIS1",
    "CIS4", "CISB", "CISC", "CISD", "CISE", "CISM", "CISP", "CISQ", "CISR",
    "CISS", "CIST", "CISU", "CISX", "CITS", "CJLR", "CJSA", "CJSL", "CJSR",
    "CJTR", "CKAM", "CKBC", "CKBM", "CKBP", "CKBR", "CKCN", "CKDL", "CKDP",
    "CKQC", "CKRS", "CKRT", "CKSD", "CKSQ", "CKTI", "CLDM", "CLQ2", "CLR1",
    "CLR2", "CLS1", "CLS2", "CLS3", "CLS4", "CMAC", "CMPX", "CMSG", "CMTS",
    "COVR", "CPCT", "CPIA", "CPIH", "CPIL", "CPIQ", "CPIR", "CPIS", "CPLT",
    "CPMI", "CPSS", "CQPI", "CQPO", "CQRY", "CRLR", "CRMD", "CRMF", "CRPA",
    "CRPC", "CRPM", "CRSQ", "CRSR", "CRST", "CRSY", "CRTE", "CRTP", "CRTX",
    "CSAC", "CSCY", "CSFE", "CSFR", "CSFU", "CSGM", "CSHA", "CSHQ", "CSHR",
    "CSKP", "CSMI", "CSM1", "CSM2", "CSM3", "CSM5", "CSNC", "CSNE", "CSOL",
    "CSPG", "CSPK", "CSPP", "CSPQ", "CSPS", "CSQC", "CSRK", "CSRS", "CSSF",
    "CSSY", "CSTE", "CSTP", "CSXM", "CSZI", "CTIN", "CTSD", "CVMI", "CWBA",
    "CWBG", "CWTO", "CWWU", "CWXN", "CWXU", "CW2A", "CXCU", "CXRE", "CXRT",
    "DSNC"} -- Default CICS from https://www-01.ibm.com/support/knowledgecenter/SSGMCP_5.2.0/com.ibm.cics.ts.systemprogramming.doc/topics/dfha726.html

  cics_id_file = ( (cics_id_file and nmap.fetchfile(cics_id_file)) or cics_id_file )

  if cics_id_file then
    for l in io.lines(cics_id_file) do
      if not l:match("#!comment:") then
        table.insert(cics_ids, l)
      end
    end
  end
  local cicstst,msg = cics_test(host, port, commands, username, password)
  if cicstst then
    local title = 'CICS Transaction IDs'
    if not(username == nil and password == nil) then title = 'CICS Transaction IDs for User: '.. username end
    local options = { key1 = commands, key2 = path, user = username, pass = password }
    stdnse.debug("Starting CICS Transaction ID Enumeration")
    if path ~= nil then stdnse.verbose(2,"Saving Screenshots to: %s", path) end
    local engine = brute.Engine:new(Driver, host, port, options)
    engine.options.script_name = SCRIPT_NAME
    engine:setPasswordIterator(unpwdb.filter_iterator(iter(cics_ids), valid_cics))
    engine.options.passonly = true
    engine.options:setTitle(title)
    local status, result = engine:start()
    return result
  else
    return msg
  end
end
