Add the scripts

citrix-brute-xml
  citrix-enum-apps
  citrix-enum-apps-xml
  citrix-enum-servers
  citrix-enum-servers-xml
and the citrixxml modules, all by Patrik Karlsson.
This commit is contained in:
david 2009-12-14 07:30:38 +00:00
parent 304762b07d
commit f2ae05968b
8 changed files with 1240 additions and 0 deletions

View file

@ -0,0 +1,160 @@
description = [[ Attempts to guess valid credentials for the Citrix PN Web Agent XML Service.
The XML service authenticates against the local Windows server or the Active Directory.
CAUTION: This script makes no attempt of preventing account lockout.
If the password list contains more passwords than the lockout-threshold
accounts WILL be locked.
]]
---
-- @usage
-- nmap --script=citrix-brute-xml --script-args=userdb=<userdb>,passdb=<passdb>,ntdomain=<domain> -p 80,443,8080 <host>
--
-- @output
-- PORT STATE SERVICE REASON
-- 8080/tcp open http-proxy syn-ack
-- | citrix-brute-xml:
-- | Joe:password => Must change password at next logon
-- | Luke:summer => Login was successful
-- |_ Jane:secret => Account is disabled
--
---
-- Version 0.2
-- Created 11/30/2009 - v0.1 - created by Patrik Karlsson <patrik@cqure.net>
-- Revised 12/02/2009 - v0.2 - Use stdnse.format_ouput for output
author = "Patrik Karlsson"
license = "Same as Nmap--See http://nmap.org/book/man-legal.html"
categories = {"intrusive", "auth"}
require 'unpwdb'
require 'shortport'
require 'citrixxml'
portrule = shortport.portnumber({8080,80,443}, "tcp")
--- Verifies if the credentials (username, password and domain) are valid
--
-- @param host string, the ip against which to perform
-- @param port number, the port number of the XML service
-- @param username string, the username to authenticate as
-- @param password string, the password to authenticate with
-- @param domain string, the Windows domain to authenticate against
--
-- @return success, message
--
function verify_password( host, port, username, password, domain )
local response = citrixxml.request_validate_credentials(host, port, {Credentials={Domain=domain, Password=password, UserName=username}})
local cred_status = citrixxml.parse_validate_credentials_response(response)
local account = {}
account.username = username
account.password = password
account.domain = domain
if cred_status.ErrorId then
if cred_status.ErrorId == "must-change-credentials" then
account.valid = true
account.message = "Must change password at next logon"
elseif cred_status.ErrorId == "account-disabled" then
account.valid = true
account.message = "Account is disabled"
elseif cred_status.ErrorId == "account-locked-out" then
account.valid = false
account.message = "Account Locked Out"
elseif cred_status.ErrorId == "failed-credentials" then
account.valid = false
account.message = "Incorrect Password"
elseif cred_status.ErrorId == "unspecified" then
account.valid = false
account.message = "Unspecified"
else
print("UNKNOWN response: " .. response)
account.valid = false
account.message = "failed"
end
else
account.message = "Login was successful"
account.valid = true
end
return account
end
--- Formats the result from the table of valid accounts
--
-- @param accounts table containing accounts (tables)
-- @return string containing the result
function create_result_from_table(accounts)
local result = ""
for _, account in ipairs(accounts) do
result = result .. " " .. account.username .. ":" .. account.password .. " => " .. account.message .. "\n"
end
return " \n" .. result
end
action = function(host, port)
local status, nextUser, nextPass
local username, password
local args = nmap.registry.args
local ntdomain = args.ntdomain
local valid_accounts = {}
if not ntdomain then
return "FAILED: No domain specified (use ntdomain argument)"
end
status, nextUser = unpwdb.usernames()
if not status then
return
end
status, nextPass = unpwdb.passwords()
if not status then
return
end
username = nextUser()
-- iterate over userlist
while username do
password = nextPass()
-- iterate over passwordlist
while password do
local result = "Trying " .. username .. "/" .. password .. " "
local account = verify_password(host.ip, port.number, username, password, ntdomain)
if account.valid then
table.insert(valid_accounts, account)
if account.valid then
stdnse.print_debug(1, "Trying %s/%s => Login Correct, Info: %s", username, password, account.message)
else
stdnse.print_debug(1, "Trying %s/%s => Login Correct", username, password)
end
else
stdnse.print_debug(1, "Trying %s/%s => Login Failed, Reason: %s", username, password, account.message)
end
password = nextPass()
end
nextPass("reset")
username = nextUser()
end
return create_result_from_table(valid_accounts)
end

View file

@ -0,0 +1,150 @@
description = [[
Extracts a list of applications, acls and settings from Citrix XML service
The script returns the shorter, comma separated output per default.
Running nmap with the verbose flag (-v) triggers the detailed output.
]]
---
-- @usage
-- nmap --script=citrix-enum-apps-xml -p 80,443,8080 <host>
--
-- @output
-- PORT STATE SERVICE
-- 8080/tcp open http-proxy
-- | citrix-enum-apps-xml:
-- | Application: Notepad
-- | Disabled: false
-- | Desktop: false
-- | On Desktop: false
-- | Encryption: basic
-- | In start menu: false
-- | Publisher: labb1farm
-- | SSL: false
-- | Remote Access: false
-- | Users: Anonymous
-- | Application: iexplorer
-- | Disabled: false
-- | Desktop: false
-- | On Desktop: false
-- | Encryption: basic
-- | In start menu: false
-- | Publisher: labb1farm
-- | SSL: false
-- | Remote Access: false
-- | Users: Anonymous
-- | Application: registry editor
-- | Disabled: false
-- | Desktop: false
-- | On Desktop: false
-- | Encryption: basic
-- | In start menu: false
-- | Publisher: labb1farm
-- | SSL: false
-- | Remote Access: false
-- | Users: WIN-B4RL0SUCJ29\Joe
-- |_ Groups: WIN-B4RL0SUCJ29\HR, *CITRIX_BUILTIN*\*CITRIX_ADMINISTRATORS*
--
--
-- PORT STATE SERVICE
-- 8080/tcp open http-proxy
-- | citrix-enum-apps-xml:
-- | Application: Notepad; Users: Anonymous
-- | Application: iexplorer; Users: Anonymous
-- |_ Application: registry editor; Users: WIN-B4RL0SUCJ29\Joe; Groups: WIN-B4RL0SUCJ29\HR, *CITRIX_BUILTIN*\*CITRIX_ADMINISTRATORS*
--
---
-- Version 0.2
-- Created 11/26/2009 - v0.1 - created by Patrik Karlsson <patrik@cqure.net>
-- Revised 12/02/2009 - v0.2 - Use stdnse.format_ouput for output
author = "Patrik Karlsson"
license = "Same as Nmap--See http://nmap.org/book/man-legal.html"
categories = {"discovery", "safe"}
require "comm"
require 'shortport'
require 'citrixxml'
portrule = shortport.portnumber({8080,80,443}, "tcp")
--- Creates a table which is suitable for use with stdnse.format_output
--
-- @param appdata table with results from parse_appdata_response
-- @param mode string short or long, see usage above
-- @return table suitable for stdnse.format_output
function format_output(appdata, mode)
local result = {}
local setting_titles = { {appisdisabled="Disabled"}, {appisdesktop="Desktop"}, {AppOnDesktop="On Desktop"},
{Encryption="Encryption"}, {AppInStartmenu="In start menu"},
{PublisherName="Publisher"}, {SSLEnabled="SSL"}, {RemoteAccessEnabled="Remote Access"} }
if mode == "short" then
for app_name, AppData in ipairs(appdata) do
local line = "Application: " .. AppData.FName
if AppData.AccessList then
if AppData.AccessList.User then
line = line .. "; Users: " .. stdnse.strjoin(", ", AppData.AccessList.User)
end
if AppData.AccessList.Group then
line = line .. "; Groups: " .. stdnse.strjoin(", ", AppData.AccessList.Group)
end
table.insert(result, line)
end
end
else
for app_name, AppData in ipairs(appdata) do
local result_part = {}
result_part.name = "Application: " .. AppData.FName
local settings = AppData.Settings
for _, setting_pairs in ipairs(setting_titles) do
for setting_key, setting_title in pairs(setting_pairs) do
local setting_value = settings[setting_key] and settings[setting_key] or ""
table.insert(result_part, setting_title .. ": " .. setting_value )
end
end
if AppData.AccessList then
if AppData.AccessList.User then
table.insert(result_part, "Users: " .. stdnse.strjoin(", ", AppData.AccessList.User) )
end
if AppData.AccessList.Group then
table.insert(result_part, "Groups: " .. stdnse.strjoin(", ", AppData.AccessList.Group) )
end
table.insert(result, result_part)
end
end
end
return result
end
action = function(host,port)
local response = citrixxml.request_appdata(host.ip, port.number, {ServerAddress="",attr={addresstype="dot"},DesiredDetails={"all","access-list"} })
local appdata = citrixxml.parse_appdata_response(response)
local response = format_output(appdata, (nmap.verbosity() > 1 and "long" or "short"))
return stdnse.format_output(true, response)
end

View file

@ -0,0 +1,157 @@
description = [[
Extract published applications from the ICA Browser service
]]
---
-- @usage sudo ./nmap -sU --script=citrix-enum-apps -p 1604 <host>
--
-- @output
-- PORT STATE SERVICE
-- 1604/udp open unknown
-- 1604/udp open unknown
-- | citrix-enum-apps:
-- | Notepad
-- | iexplorer
-- |_ registry editor
--
-- Version 0.2
-- Created 11/24/2009 - v0.1 - created by Patrik Karlsson <patrik@cqure.net>
-- Revised 11/25/2009 - v0.2 - fixed multiple packet response bug
author = "Patrik Karlsson <patrik@cqure.net>"
license = "Same as Nmap--See http://nmap.org/book/man-legal.html"
categories = {"discovery","intrusive"}
require "comm"
require "shortport"
require "stdnse"
require "bin"
portrule = shortport.portnumber(1604, "udp")
-- process the response from the server
-- @param response string, complete server response
-- @return string row delimited with \n containing all published applications
function process_pa_response(response)
local pos, packet_len = bin.unpack("SS", response)
local app_name
local pa_list = {}
if packet_len < 40 then
return
end
-- the list of published applications starts at offset 40
offset = 41
while offset < packet_len do
pos, app_name = bin.unpack("z", response:sub(offset))
offset = offset + pos - 1
table.insert(pa_list, app_name)
end
return pa_list
end
action = function(host, port)
local packet, counter
local query = {}
local pa_list = {}
--
-- Packets were intercepted from the Citrix Program Neighborhood client
-- They are used to query a server for it's list of servers
--
-- We're really not interested in the responses to the first two packets
-- The third response contains the list of published applications
-- I couldn't find any documentation on this protocol so I'm providing
-- some brief information for the bits and bytes this script uses.
--
-- Spec. of response to query[2] that contains a list of published apps
--
-- offset size content
-- -------------------------
-- 0 16-bit Length
-- 12 32-bit Server IP (not used here)
-- 30 8-bit Last packet (1), More packets(0)
-- 40 - null-separated list of applications
--
query[0] = string.char(
0x1e, 0x00, -- Length: 30
0x01, 0x30, 0x02, 0xfd, 0xa8, 0xe3, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00
)
query[1] = string.char(
0x20, 0x00, -- Length: 32
0x01, 0x36, 0x02, 0xfd, 0xa8, 0xe3, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00
)
query[2] = string.char(
0x2a, 0x00, -- Length: 42
0x01, 0x32, 0x02, 0xfd, 0xa8, 0xe3, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x21, 0x00, 0x02, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
)
counter = 0
local socket = nmap.new_socket()
socket:set_timeout(5000)
try = nmap.new_try(function() socket:close() end)
try( socket:connect(host.ip, port.number, port.protocol) )
-- send the two first packets and never look back
repeat
try( socket:send(query[counter]) )
packet = try(socket:receive())
counter = counter + 1
until (counter>#query)
-- process the first response
pa_list = process_pa_response( packet )
--
-- the byte at offset 31 in the response has a really magic function
-- if it is set to zero (0) we have more response packets to process
-- if it is set to one (1) we have arrived at the last packet of our journey
--
while packet:sub(31,31) ~= string.char(0x01) do
packet = try( socket:receive() )
local tmp_table = process_pa_response( packet )
for _,v in pairs(tmp_table) do
table.insert(pa_list, v)
end
end
-- set port to open
if #pa_list>0 then
nmap.set_port_state(host, port, "open")
end
socket:close()
return stdnse.format_output(true, pa_list)
end

View file

@ -0,0 +1,45 @@
description = [[ Extracts the name of the server farm and member severs from Citrix XML service
]]
---
-- @usage
-- nmap --script=citrix-enum-servers-xml -p 80,443,8080 <host>
--
-- @output
-- PORT STATE SERVICE REASON
-- 8080/tcp open http-proxy syn-ack
-- | citrix-enum-servers-xml:
-- | CITRIX-SRV01
-- |_ CITRIX-SRV01
--
---
-- Version 0.2
-- Created 11/26/2009 - v0.1 - created by Patrik Karlsson <patrik@cqure.net>
-- Revised 12/02/2009 - v0.2 - Use stdnse.format_ouput for output
author = "Patrik Karlsson"
license = "Same as Nmap--See http://nmap.org/book/man-legal.html"
categories = {"discovery", "safe"}
require "comm"
require 'shortport'
require 'citrixxml'
portrule = shortport.portnumber({8080,80,443}, "tcp")
action = function(host, port)
local xmldata = citrixxml.request_server_data(host.ip, port.number)
local servers = citrixxml.parse_server_data_response(xmldata)
local response = {}
for _, srv in ipairs(servers) do
table.insert(response, srv)
end
return stdnse.format_output(true, response)
end

View file

@ -0,0 +1,141 @@
description = [[
Extract a list of Citrix servers from the ICA Browser service
]]
---
-- @usage sudo ./nmap -sU --script=citrix-enum-servers -p 1604
--
-- @output
-- PORT STATE SERVICE
-- 1604/udp open unknown
-- | citrix-enum-servers:
-- | CITRIXSRV01
-- |_ CITRIXSRV02
--
-- Version 0.2
-- Created 11/26/2009 - v0.1 - created by Patrik Karlsson <patrik@cqure.net>
-- Revised 11/26/2009 - v0.2 - minor packet documentation
author = "Patrik Karlsson <patrik@cqure.net>"
license = "Same as Nmap--See http://nmap.org/book/man-legal.html"
categories = {"discovery", "safe"}
require "comm"
require "shortport"
portrule = shortport.portnumber(1604, "udp")
--
-- process the response from the server
-- @param response string, complete server response
-- @return string row delimited with \n containing all published applications
--
function process_server_response(response)
local pos, packet_len = bin.unpack("SS", response)
local server_name
local server_list = {}
if packet_len < 40 then
return
end
-- the list of published applications starts at offset 40
offset = 41
while offset < packet_len do
pos, server_name = bin.unpack("z", response:sub(offset))
offset = offset + pos - 1
table.insert(server_list, server_name)
end
return server_list
end
action = function(host, port)
local packet, counter, socket
local query = {}
local server_list = {}
--
-- Packets were intercepted from the Citrix Program Neighborhood client
-- They are used to query a server for it's list of published applications
--
-- We're really not interested in the responses to the first two packets
-- The third response contains the list of published applications
-- I couldn't find any documentation on this protocol so I'm providing
-- some brief information for the bits and bytes this script uses.
--
-- Spec. of response to query[2] that contains a list of published apps
--
-- offset size content
-- -------------------------
-- 0 16-bit Length
-- 12 32-bit Server IP (not used here)
-- 30 8-bit Last packet (1), More packets(0)
-- 40 - null-separated list of applications
--
query[0] = string.char(
0x1e, 0x00, -- Length: 30
0x01, 0x30, 0x02, 0xfd, 0xa8, 0xe3, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00
)
query[1] = string.char(
0x2a, 0x00, -- Length: 42
0x01, 0x32, 0x02, 0xfd, 0xa8, 0xe3, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x01, 0x00, 0x02, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
)
counter = 0
socket = nmap.new_socket()
socket:set_timeout(5000)
try = nmap.new_try(function() socket:close() end)
try(socket:connect(host.ip, port.number, port.protocol))
-- send the two first packets and never look back
repeat
try(socket:send(query[counter]))
packet = try(socket:receive())
counter = counter + 1
until (counter>#query)
-- process the first response
server_list = process_server_response( packet )
--
-- the byte at offset 31 in the response has a really magic function
-- if it is set to zero (0) we have more response packets to process
-- if it is set to one (1) we have arrived at the last packet of our journey
--
while packet:sub(31,31) ~= string.char(0x01) do
packet = try( socket:receive() )
local tmp_table = process_server_response( packet )
for _, v in ipairs(tmp_table) do
table.insert(server_list, v)
end
end
if #server_list>0 then
nmap.set_port_state(host, port, "open")
end
socket:close()
return stdnse.format_output(true, server_list)
end

View file

@ -2,6 +2,11 @@ Entry { filename = "asn-query.nse", categories = { "discovery", "external", "saf
Entry { filename = "auth-owners.nse", categories = { "default", "safe", } }
Entry { filename = "auth-spoof.nse", categories = { "malware", "safe", } }
Entry { filename = "banner.nse", categories = { "discovery", "safe", } }
Entry { filename = "citrix-brute-xml.nse", categories = { "auth", "intrusive", } }
Entry { filename = "citrix-enum-apps-xml.nse", categories = { "discovery", "safe", } }
Entry { filename = "citrix-enum-apps.nse", categories = { "discovery", "intrusive", } }
Entry { filename = "citrix-enum-servers-xml.nse", categories = { "discovery", "safe", } }
Entry { filename = "citrix-enum-servers.nse", categories = { "discovery", "safe", } }
Entry { filename = "daytime.nse", categories = { "discovery", "safe", } }
Entry { filename = "db2-info.nse", categories = { "discovery", "safe", "version", } }
Entry { filename = "dhcp-discover.nse", categories = { "default", "discovery", "intrusive", } }