315 lines
11 KiB
Lua
315 lines
11 KiB
Lua
|
---
|
||
|
-- Functions for proxy testing.
|
||
|
--
|
||
|
-- @args proxy.url Url that will be requested to the proxy
|
||
|
-- @args proxy.pattern Pattern that will be searched inside the request results
|
||
|
--
|
||
|
-- @author Joao Correa <joao@livewire.com.br>
|
||
|
-- @copyright Same as Nmap--See https://nmap.org/book/man-legal.html
|
||
|
|
||
|
local dns = require "dns"
|
||
|
local ipOps = require "ipOps"
|
||
|
local nmap = require "nmap"
|
||
|
local stdnse = require "stdnse"
|
||
|
local string = require "string"
|
||
|
local stringaux = require "stringaux"
|
||
|
_ENV = stdnse.module("proxy", stdnse.seeall)
|
||
|
|
||
|
-- Start of local functions
|
||
|
|
||
|
--- check function, checks for all valid returned status
|
||
|
--- If any of the HTTP status below is found, the proxy is potentially open
|
||
|
--- The script tries to split header from body before checking for status
|
||
|
--@param result connection result
|
||
|
--@return true if any of the status is found, otherwise false
|
||
|
local function check_code(result)
|
||
|
if result then
|
||
|
local header
|
||
|
if result:match( "\r?\n\r?\n" ) then
|
||
|
result = result:match( "^(.-)\r?\n\r?\n(.*)$" )
|
||
|
end
|
||
|
if result:lower():match("^http/%d%.%d%s*200") then return true end
|
||
|
if result:lower():match("^http/%d%.%d%s*30[12]") then return true end
|
||
|
end
|
||
|
return false
|
||
|
end
|
||
|
|
||
|
--- check pattern, searches a pattern inside a response with multiple lines
|
||
|
--@param result Connection result
|
||
|
--@param pattern The pattern to be searched
|
||
|
--@return true if pattern is found, otherwise false
|
||
|
local function check_pattern(result, pattern)
|
||
|
local lines = stringaux.strsplit("\n", result)
|
||
|
for i, line in ipairs(lines) do
|
||
|
if line:lower():match(pattern:lower()) then return true end
|
||
|
end
|
||
|
return false
|
||
|
end
|
||
|
|
||
|
--- check, decides what kind of check should be done on the response,
|
||
|
--- depending if a specific pattern is being used
|
||
|
--@param result Connection result
|
||
|
--@param pattern The pattern that should be checked (must be false, in case of
|
||
|
--code check)
|
||
|
--@return true, if the performed check returns true, otherwise false
|
||
|
local function check(result, pattern)
|
||
|
local s_pattern = false
|
||
|
local s_code = check_code(result)
|
||
|
if s_code and pattern then
|
||
|
s_pattern = check_pattern(result, pattern)
|
||
|
end
|
||
|
return s_code, s_pattern
|
||
|
end
|
||
|
|
||
|
--- Performs a request to the web server and calls check to check if
|
||
|
-- the response is a valid result
|
||
|
--
|
||
|
--@param socket The socket to send the request through
|
||
|
--@param req The request to be sent
|
||
|
--@param pattern The pattern to check for valid result
|
||
|
--@return check_status True or false. If pattern was used, depends on pattern check result. If not, depends on code check result.
|
||
|
--@return result The result of the request
|
||
|
--@return code_status True or false. If pattern was used, returns the result of code checking for the same result. If pattern was not used, is nil.
|
||
|
local function test(socket, req, pattern)
|
||
|
local status, result = socket:send(req)
|
||
|
if not status then
|
||
|
socket:close()
|
||
|
return false, result
|
||
|
end
|
||
|
status, result = socket:receive()
|
||
|
if not status then
|
||
|
socket:close()
|
||
|
return false, result
|
||
|
end
|
||
|
socket:close()
|
||
|
local s_code, s_pattern = check(result, pattern)
|
||
|
if result and pattern then return s_pattern, result, s_code end
|
||
|
if result then return s_code, result, nil end
|
||
|
return false, nil, nil
|
||
|
end
|
||
|
|
||
|
--- Builds the GET request and calls test
|
||
|
-- @param host The host table
|
||
|
-- @param port The port table
|
||
|
-- @param proxyType The proxy type to be tested, might be 'socks4', 'socks5' or 'http'
|
||
|
-- @param test_url The url to send the request
|
||
|
-- @param hostname The hostname of the server to send the request
|
||
|
-- @param pattern The pattern to check for valid result
|
||
|
-- @return the result of the function test (status and the request result)
|
||
|
function test_get(host, port, proxyType, test_url, hostname, pattern)
|
||
|
local status, socket = connectProxy(host, port, proxyType, hostname)
|
||
|
if not status then
|
||
|
return false, socket
|
||
|
end
|
||
|
local req = "GET " .. test_url .. " HTTP/1.0\r\nHost: " .. hostname .. "\r\n\r\n"
|
||
|
stdnse.debug1("GET Request: " .. req)
|
||
|
return test(socket, req, pattern)
|
||
|
end
|
||
|
|
||
|
--- Builds the HEAD request and calls test
|
||
|
-- @param host The host table
|
||
|
-- @param port The port table
|
||
|
-- @param proxyType The proxy type to be tested, might be 'socks4', 'socks5' or 'http'
|
||
|
-- @param test_url The url te send the request
|
||
|
-- @param hostname The hostname of the server to send the request
|
||
|
-- @param pattern The pattern to check for valid result
|
||
|
-- @return the result of the function test (status and the request result)
|
||
|
function test_head(host, port, proxyType, test_url, hostname, pattern)
|
||
|
local status, socket = connectProxy(host, port, proxyType, hostname)
|
||
|
if not status then
|
||
|
return false, socket
|
||
|
end
|
||
|
local req = "HEAD " .. test_url .. " HTTP/1.0\r\nHost: " .. hostname .. "\r\n\r\n"
|
||
|
stdnse.debug1("HEAD Request: " .. req)
|
||
|
return test(socket, req, pattern)
|
||
|
end
|
||
|
|
||
|
--- Builds the CONNECT request and calls test
|
||
|
-- @param host The host table
|
||
|
-- @param port The port table
|
||
|
-- @param proxyType The proxy type to be tested, might be 'socks4', 'socks5' or 'http'
|
||
|
-- @param hostname The hostname of the server to send the request
|
||
|
-- @return the result of the function test (status and the request result)
|
||
|
function test_connect(host, port, proxyType, hostname)
|
||
|
local status, socket = connectProxy(host, port, proxyType, hostname)
|
||
|
if not status then
|
||
|
return false, socket
|
||
|
end
|
||
|
local req = "CONNECT " .. hostname .. ":80 HTTP/1.0\r\n\r\n"
|
||
|
stdnse.debug1("CONNECT Request: " .. req)
|
||
|
return test(socket, req, false)
|
||
|
end
|
||
|
|
||
|
--- Checks if any parameter was used in old or new syntax
|
||
|
-- and return the parameters
|
||
|
-- @return url the proxy.url parameter
|
||
|
-- @return pattern the proxy.pattern parameter
|
||
|
function return_args()
|
||
|
local url = false
|
||
|
local pattern = false
|
||
|
if nmap.registry.args['proxy.url']
|
||
|
then url = nmap.registry.args['proxy.url']
|
||
|
elseif nmap.registry.args.proxy and nmap.registry.args.proxy.url
|
||
|
then url = nmap.registry.args.proxy.url
|
||
|
end
|
||
|
if nmap.registry.args['proxy.pattern']
|
||
|
then pattern = nmap.registry.args['proxy.pattern']
|
||
|
elseif nmap.registry.args.proxy and nmap.registry.args.proxy.url
|
||
|
then pattern = nmap.registry.args.proxy.pattern
|
||
|
end
|
||
|
return url, pattern
|
||
|
end
|
||
|
|
||
|
--- Creates a socket, performs proxy handshake if necessary
|
||
|
--- and returns it
|
||
|
-- @param host The host table
|
||
|
-- @param port The port table
|
||
|
-- @param proxyType A string with the proxy type. Might be "http","socks4" or "socks5"
|
||
|
-- @param hostname The proxy destination hostname
|
||
|
-- @return status True if handshake succeeded, false otherwise
|
||
|
-- @return socket A socket with the handshake already done, or an error if
|
||
|
function connectProxy(host, port, proxyType, hostname)
|
||
|
local socket = nmap.new_socket()
|
||
|
socket:set_timeout(10000)
|
||
|
local status, err = socket:connect(host, port)
|
||
|
if not status then
|
||
|
socket:close()
|
||
|
return false, err
|
||
|
end
|
||
|
if proxyType == "http" then return true, socket end
|
||
|
if proxyType == "socks4" then return socksHandshake(socket, 4, hostname) end
|
||
|
if proxyType == "socks5" then return socksHandshake(socket, 5, hostname) end
|
||
|
socket:close()
|
||
|
return false, "Invalid proxyType"
|
||
|
end
|
||
|
|
||
|
--- Performs a socks handshake on a socket and returns it
|
||
|
-- @param socket The socket where the handshake will be performed
|
||
|
-- @param version The socks version (might be 4 or 5)
|
||
|
-- @param hostname The proxy destination hostname
|
||
|
-- @return status True if handshake succeeded, false otherwise
|
||
|
-- @return socket A socket with the handshake already done, or an error if
|
||
|
-- status is false
|
||
|
function socksHandshake(socket, version, hostname)
|
||
|
local status, ip = dns.query(hostname)
|
||
|
if not status then
|
||
|
return false, "Unable to resolve hostname"
|
||
|
end
|
||
|
if version == 4 then
|
||
|
local payload = '\x04\x01\x00\x50' .. ipOps.ip_to_str(ip) .. '\x6e\x6d\x61\x70\x00'
|
||
|
local status, response = socket:send(payload)
|
||
|
if not status then
|
||
|
socket:close()
|
||
|
return false, response
|
||
|
end
|
||
|
status, response = socket:receive()
|
||
|
if not status then
|
||
|
socket:close()
|
||
|
return false, response
|
||
|
end
|
||
|
if #response < 2 then
|
||
|
socket:close()
|
||
|
return false, "Invalid or unknown SOCKS response"
|
||
|
end
|
||
|
local request_status = string.byte(response, 2)
|
||
|
local err = string.format("Unknown response (0x%02x)", request_status)
|
||
|
if(request_status == 0x5a) then
|
||
|
stdnse.debug1('Socks4: Received "Request Granted" from proxy server')
|
||
|
return true, socket
|
||
|
end
|
||
|
if(request_status == 0x5b) then
|
||
|
err = "Request rejected or failed"
|
||
|
elseif (request_status == 0x5c) then
|
||
|
err = "request failed because client is not running identd"
|
||
|
elseif (request_status == 0x5d) then
|
||
|
err = "request failed because client program and identd report different user-ids"
|
||
|
end
|
||
|
stdnse.debug1('Socks4: Received "%s" from proxy server', err)
|
||
|
return false, err
|
||
|
end
|
||
|
if version == 5 then
|
||
|
local payload = '\x05\x01\x00'
|
||
|
local status, err = socket:send(payload)
|
||
|
if not status then
|
||
|
socket:close()
|
||
|
return false, err
|
||
|
end
|
||
|
local auth
|
||
|
status, auth = socket:receive()
|
||
|
local r2 = string.byte(auth,2)
|
||
|
|
||
|
-- If Auth is required, proxy is closed, skip next test
|
||
|
if(r2 ~= 0x00) then
|
||
|
err = "Authentication Required"
|
||
|
else
|
||
|
-- If no Auth is required, try to establish connection
|
||
|
stdnse.debug1("Socks5: No authentication required")
|
||
|
-- Socks5 second payload: Version, Command, Null, Address type, Ip-Address, Port number
|
||
|
payload = '\x05\x01\x00\x01' .. ipOps.ip_to_str(ip) .. '\x00\x50'
|
||
|
status, err = socket:send(payload)
|
||
|
if not status then
|
||
|
socket:close()
|
||
|
return false, err
|
||
|
end
|
||
|
local z
|
||
|
status, z = socket:receive()
|
||
|
if not status then
|
||
|
socket:close()
|
||
|
return false, z
|
||
|
end
|
||
|
local request_status = string.byte(z, 2)
|
||
|
err = string.format("Unknown response (0x%02x)", request_status)
|
||
|
if (request_status == 0x00) then
|
||
|
stdnse.debug1('Socks5: Received "Request Granted" from proxy server')
|
||
|
return true, socket
|
||
|
elseif(request_status == 0x01) then
|
||
|
err = "General Failure"
|
||
|
elseif (request_status == 0x02) then
|
||
|
err = "Connection not allowed by ruleset"
|
||
|
elseif (request_status == 0x03) then
|
||
|
err = "Network unreachable"
|
||
|
elseif (request_status == 0x04) then
|
||
|
err = "Host unreachable"
|
||
|
elseif (request_status == 0x05) then
|
||
|
err = "Connection refused by destination host"
|
||
|
elseif (request_status == 0x06) then
|
||
|
err = "TTL Expired"
|
||
|
elseif (request_status == 0x07) then
|
||
|
err = "command not supported / protocol error"
|
||
|
elseif (request_status == 0x08) then
|
||
|
err = "Address type not supported"
|
||
|
end
|
||
|
end
|
||
|
stdnse.debug1('Socks5: Received "%s" from proxy server', err)
|
||
|
return false, err
|
||
|
end
|
||
|
return false, "Invalid SOCKS version"
|
||
|
end
|
||
|
|
||
|
--- Checks if two different responses are equal,
|
||
|
-- if true, the proxy server might be redirecting the requests
|
||
|
-- to a default page
|
||
|
--
|
||
|
-- Functions splits body from head before comparing, to avoid session
|
||
|
-- variables, cookies...
|
||
|
--
|
||
|
-- @param resp1 A string with the response for the first request
|
||
|
-- @param resp2 A string with the response for the second request
|
||
|
-- @return bool true if both responses are equal, otherwise false
|
||
|
function redirectCheck(resp1, resp2)
|
||
|
local body1, body2, _
|
||
|
if resp1:match( "\r?\n\r?\n" ) then
|
||
|
local body1
|
||
|
_, body1 = resp1:match( "^(.-)\r?\n\r?\n(.*)$" )
|
||
|
if resp2:match( "\r?\n\r?\n" ) then
|
||
|
_, body2 = resp2:match( "^(.-)\r?\n\r?\n(.*)$" )
|
||
|
if body1 == body2 then
|
||
|
return true
|
||
|
end
|
||
|
end
|
||
|
end
|
||
|
return false
|
||
|
end
|
||
|
|
||
|
return _ENV;
|