analyzers/submit/nmap-dir/nselib/anyconnect.lua
---
-- This library implements HTTP requests used by the Cisco AnyConnect VPN Client
--
-- @author Patrik Karlsson <patrik@cqure.net>
--
-- @args anyconnect.group AnyConnect tunnel group (default: VPN)
-- @args anyconnect.mac MAC address of connecting client (default: random MAC)
-- @args anyconnect.version Version of connecting client (default: 3.1.05160)
-- @args anyconnect.ua User Agent of connecting client (default: AnyConnect Darwin_i386 3.1.05160)

local http = require('http')
local stdnse = require('stdnse')
local url = require('url')
local table = require('table')
local rand = require "rand"

local args_group= stdnse.get_script_args('anyconnect.group') or "VPN"
local args_mac= stdnse.get_script_args('anyconnect.mac')
local args_ver = stdnse.get_script_args('anyconnect.version') or "3.1.05160"
local args_ua = stdnse.get_script_args('anyconnect.ua') or ("AnyConnect Darwin_i386 %s"):format(args_ver)

_ENV = stdnse.module("anyconnect", stdnse.seeall)

Cisco = {

  Util = {

    generate_mac = function()
      return stdnse.format_mac(rand.random_string(6))
    end,

  },

  AnyConnect = {

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

    -- generate a random hex-string of length 'length'
    --
    generate_random = function(length)
      return rand.random_string(length * 2, '0123456789ABCDEF')
    end,

    connect = function(self)
      args_mac = args_mac or Cisco.Util.generate_mac()
      local headers = {
        ['User-Agent'] = args_ua,
        ['Accept'] = '*/*',
        ['Accept-Encoding'] = 'identity',
        ['X-Transcend-Version'] = 1,
        ['X-Aggregate-Auth'] = 1,
        ['X-AnyConnect-Platform'] = 'mac-intel'
      }

      local data = ([[<?xml version="1.0" encoding="UTF-8"?>
<config-auth client="vpn" type="init" aggregate-auth-version="2">
<version who="vpn">%s</version>
<device-id device-type="MacBookAir4,1" platform-version="10.9.2" unique-id="%s">mac-intel</device-id>
<mac-address-list>
<mac-address>%s</mac-address></mac-address-list>
<group-select>%s</group-select>
<group-access>https://%s:%s</group-access>
</config-auth>]]):format(args_ver, self.generate_random(64), args_mac, args_group, self.host.ip, self.port.number)

      local options = { header=headers , no_cache=true, redirect_ok = function(host,port)
          local c = 5
          return function(url)
            if ( c==0 ) then return false end
            c = c - 1
            return true
          end
        end
      }

      local path = '/'
      local response = http.head(self.host, self.port, path, options)
      -- account for redirects
      if response.status ~= 200 then
        return false, "Failed to connect to SSL VPN server"
      elseif response.location then
        local u = url.parse(response.location[#response.location])
        if u.host then
          self.host = u.host
        end
        if u.path then
          path = u.path
        end
      end

      response = http.post(self.host, self.port, path, options, nil, data)

      if response.status ~= 200 or response.body == nil then
        return false, "Not a Cisco ASA or unsupported version"
      end

      local xmltags = {
        'version',
        'tunnel-group',
        'group-alias',
        'config-hash',
        'host-scan-ticket',
        'host-scan-token',
        'host-scan-base-uri',
        'host-scan-wait-uri',
        'banner'
      }

      self.conn_attr = {}
      for _, tag in ipairs(xmltags) do
        local body = response.body:gsub('\r?\n', '')
        local filter = ("<%s.->(.*)</%s>"):format(tag:gsub('-', '%%-'), tag:gsub('-', '%%-'))
        local m = body:match(filter)
        if m then
          self.conn_attr[tag] = m
        end
      end

      if not self.conn_attr['version'] then
        return false, "Not a Cisco ASA or unsupported version"
      end

      -- in case we were redirected
      self.conn_attr['host'] = stdnse.get_hostname(self.host)
      return true
    end,

    ---
    -- Returns the version of the remote SSL VPN concentrator
    -- @return table containing major, minor and rev numeric values
    get_version = function(self)
      local ver = {}
      ver['major'], ver['minor'], ver['rev'] = self.conn_attr['version']:match('^(%d-)%.(%d-)%((.*)%)$')
      return ver
    end

  }
}

return _ENV
