summaryrefslogtreecommitdiffstats
path: root/nselib/proxy.lua
blob: 87a15ee3118ccc72e878c5c2f42a69baf9f51e20 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
---
-- 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: %s", 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: %s", 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: %s", 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;