A vueltas con la detección de proxys

Hace unos meses se escribió un primer artículo en securyartwork sobre la detección de proxys con NSE de nmap. Se llamaba «Detectando proxies anónimos”.

En esta segunda entrega vamos a aportar algunos matices prácticos sobre la ejecución de este sistema de descubrimiento de proxys. Maticemos algunos puntos ya mencionados.

Como ya leímos en aquella entrada, los NSE necesitan una declaración que haga una asignación del script a unos puertos concretos, es decir, el script SÓLO aplica a los puertos establecidos por código (en el script NSE) y no a los puertos detectados desde nmap. Esto es importante cuando se trata de proxys fuera de los servicios habituales. La línea era:

portrule = shortport.port_or_service({8123,3128,8000,8080},{'polipo','squid-http','http-proxy'})

Así que si queréis añadir otros puertos no dudéis en modificar esta línea.

Otro punto a tener en cuenta (como ya se decía en aquella magnifica entrada), es el hecho de que es posible la detección desde un modo «custom«. No solamente se tiene más control sino que en algunos casos incluso es más rápido. La definición por defecto del script aborda hasta tres posibilidades en caso de que la anterior falle. Esto es importante en caso, por ejemplo, de que el proxy tenga capado Google. Las «probes» se realizan contra las siguientes urls siguiendo este orden concreto:

root@animal:/root # grep -A 2 " test_url " /usr/local/share/nmap/scripts/http-open-proxy.nse
-- @param test_url The url te send the request
-- @param pattern The pattern to check for valid result
-- @return status (if any request was succeded
--
    test_url = "http://" .. test_url
    stdnse.print_debug("URL missing scheme. URL concatenated to http://")
  end
--
  local test_url = "http://www.google.com"
  local hostname = "www.google.com"
  local pattern  = "^server: gws"
--
  test_url = "http://www.wikipedia.org"
  hostname = "www.wikipedia.org"
  pattern  = "wikimedia"
--
  test_url = "http://www.computerhistory.org"
  hostname = "www.computerhistory.org"
  pattern  = "museum"

Nota: ni preguntéis que hace ahí computerhistory.org pero está. O_O

Atención al pattern por si el proxy cambia cosas y el script se hace la «picha-un-lío». Aunque esto de alguna forma ya lo detecta por código posteriormente (ver código) y menos mal.

Otra marcianada que hay que conocer en la práctica es que el timeout no viene definido por nmap. Es decir, estos parámetros de nmap:

--max-rtt-timeout  --initial-rtt-timeout  y -host-timeout  

No afectan a los scripts NSE que depende de la librería proxy. Que son dos (por lo menos en esta FreeBSD):

grep -lc 'require "proxy"' /usr/local/share/nmap/scripts/*
/usr/local/share/nmap/scripts/http-open-proxy.nse
/usr/local/share/nmap/scripts/socks-open-proxy.nse

Por tanto, si nuestros proxys, los que andamos buscando, tienen una latencia por encima de los 10 segundos (visto en código mas adelante) el script fallará. Dejando únicamente la información del puerto abierto:

Starting Nmap 6.25 ( http://nmap.org ) at 2013-05-13 11:57 CEST
Nmap scan report for 194.0.229.54
Host is up (0.098s latency).
PORT     STATE SERVICE
9050/tcp open  tor-socks
|_socks-open-proxy: ERROR: Script execution failed (use -d to debug)

Nota: con el -d veríamos el error TIMEOUT comentado.

Ahora bien, la sorpresa es que no hay llamada alguna desde ninguno de los dos scripts NSE al control de timeout y nos tenemos que ir a buscarlo, si necesitamos ampliarlo, allá donde la cueva de Ali babá. Es la línea:

socket:set_timeout(10000)

Que lo encontramos aquí, por lo menos en mi FreeBSD:

/usr/local/share/nmap/nselib/proxy.lua

Es decir fuera de la ubicación de los scripts NSE. ¡Ojito!

Dicho esto… llega la mayor y nos preguntamos: ¿Ajustando los parámetros correctamente detectamos proxys? Of course. Y ¿Sabemos cuáles son anónimos? ¡Pues va a ser que no! El script NSE no dice eso. Solo que el proxy está abierto y que es lo que soporta (GET, HEAD o CONNECT).

Para trabajar esta idea de comprobar si el proxy es «anonimizador» me he inventado una chorradita. Perdonad la osadía puesto que no trabajo ni en seguridad ni soy programador. Resulta que soy de sistemas. Mil perdones.

Lo primero que he hecho es crearme por el universo lo que yo llamo el antiscript de php al que llamo, en un alarde de originalidad, ip.php. El contenido es algo tan chorra o más que lo que sigue:

<?php
//Find the IP of the visitor
     if (getenv("HTTP_CLIENT_IP") && strcasecmp(getenv("HTTP_CLIENT_IP"), "unknown"))
     {
        $rip = getenv("HTTP_CLIENT_IP");
     }
     else if (getenv("HTTP_X_FORWARDED_FOR") && strcasecmp(getenv("HTTP_X_FORWARDED_FOR"), "unknown"))
     {
        $rip = getenv("HTTP_X_FORWARDED_FOR");
     }
     else if (getenv("REMOTE_ADDR") && strcasecmp(getenv("REMOTE_ADDR"), "unknown"))
     {
        $rip = getenv("REMOTE_ADDR");
     }
     else if (isset($_SERVER['REMOTE_ADDR']) && $_SERVER['REMOTE_ADDR'] && 
              strcasecmp($_SERVER['REMOTE_ADDR'], "unknown"))
     {
        $rip = $_SERVER['REMOTE_ADDR'];
     }
     else
     {
        $rip = "unknown";
     }
 
//Display the IP of the Visitor
echo "Your IP is $rip";
?>

Nota: De programar ni idea, pero a snippets no me gana nadie XDDDD

Probamos que funciona claro. Ahora cogemos de base nuestro querido http-open-proxy.nse y lo copiamos en http-open-proxy-anon.nse. Y le meto los siguientes chorra-cambios (vuelvo a pedir disculpas a los que programan):

--- /usr/local/share/nmap/scripts/http-open-proxy.nse   2013-04-17 12:12:30.958089928 +0200
+++ /usr/local/share/nmap/scripts/http-open-proxy-anon.nse      2013-05-11 11:13:23.456681724 +0200
@@ -38,52 +38,11 @@
 -- 
 -- @usage
 -- nmap --script http-open-proxy.nse \
---      --script-args proxy.url=,proxy.pattern=
 
 author = "Arturo 'Buanzo' Busleiman"
 license = "Same as Nmap--See http://nmap.org/book/man-legal.html"
 categories = {"default", "discovery", "external", "safe"}
 
---- Performs the custom test, with user's arguments 
--- @param host The host table
--- @param port The port table
--- @param test_url The url te send the request
--- @param pattern The pattern to check for valid result
--- @return status (if any request was succeded
--- @return response String with supported methods
-function custom_test(host, port, test_url, pattern)
-  local lstatus = false
-  local response = ""
-  -- if pattern is not used, result for test is code check result.
-  -- otherwise it is pattern check result.
-
-  -- strip hostname
-  if not string.match(test_url, "^http://.*") then 
-    test_url = "http://" .. test_url
-    stdnse.print_debug("URL missing scheme. URL concatenated to http://")
-  end
-  local url_table = url.parse(test_url)
-  local hostname = url_table.host
-
-  local get_status = proxy.test_get(host, port, "http", test_url, hostname, pattern)
-  local head_status = proxy.test_head(host, port, "http", test_url, hostname, pattern)
-  local conn_status = proxy.test_connect(host, port, "http", hostname)
-  if get_status then
-    lstatus = true
-    response = response .. " GET"
-  end
-  if head_status then
-    lstatus = true
-    response = response .. " HEAD"
-  end
-  if conn_status then
-    lstatus = true
-    response = response .. " CONNECTION"
-  end
-  if lstatus then response = "Methods supported: " .. response end
-  return lstatus, response
-end
-
 --- Performs the default test
 -- First: Default google request and checks for Server: gws
 -- Seconde: Request to wikipedia.org and checks for wikimedia pattern
@@ -98,23 +57,25 @@
 -- @param port The port table
 -- @return status (if any request was succeded
 -- @return response String with supported methods
-function default_test(host, port)
+
+function anon_test(host, port)
   local fstatus = false
   local cstatus = false
   local response = ""
-  local get_status, head_status, conn_status
+  local get_status, head_status, conn_status, anon_status
   local get_r1, get_r2, get_r3
   local get_cstatus, head_cstatus
 
-  -- Start test n1 -> google.com
+  -- Start test n1
   -- making requests
-  local test_url = "http://www.google.com"
-  local hostname = "www.google.com"
-  local pattern  = "^server: gws"
+  local test_url = "http://www.xxxx.net/ip.php"
+  local hostname = "www.xxxx.net"
+  local pattern  = "Your IP is"
   get_status, get_r1, get_cstatus = proxy.test_get(host, port, "http", test_url, hostname, pattern)
   local _
   head_status, _, head_cstatus = proxy.test_head(host, port, "http", test_url, hostname, pattern)
   conn_status = proxy.test_connect(host, port, "http", hostname)
+  anon_status = string.match (get_r1, "Your IP is (%d+.%d+.%d+.%d+)")
 
   -- checking results
   -- conn_status use a different flag (cstatus)
@@ -127,7 +88,8 @@
   if get_status then fstatus = true; response = response .. " GET" end
   if head_status then fstatus = true; response = response .. " HEAD" end
   if conn_status then cstatus = true; response = response .. " CONNECTION" end
-
+  if anon_status then fstatus = true; response = response .. " IP:" .. anon_status end
+ 
   -- if proxy is open, return it!
   if fstatus then return fstatus, "Methods supported: " .. response end
 
@@ -136,44 +98,7 @@
   -- if we do not receive any valid status code,
   -- there is no reason to keep testing... the proxy is probably not open
   if not (get_cstatus or head_cstatus or conn_status) then return false, nil end
-  stdnse.print_debug("Test 1 - Google Web Server\nReceived valid status codes, but pattern does not match")
-
-  test_url = "http://www.wikipedia.org"
-  hostname = "www.wikipedia.org"
-  pattern  = "wikimedia"
-  get_status, get_r2, get_cstatus = proxy.test_get(host, port, "http", test_url, hostname, pattern)
-  head_status, _, head_cstatus = proxy.test_head(host, port, "http", test_url, hostname, pattern)
-  conn_status = proxy.test_connect(host, port, "http", hostname)
-
-  if get_status then fstatus = true; response = response .. " GET" end
-  if head_status then fstatus = true; response = response .. " HEAD" end
-  if conn_status then 
-    if not cstatus then response = response .. " CONNECTION" end
-    cstatus = true
-  end
-
-  if fstatus then return fstatus, "Methods supported: "  .. response end
-
-  -- same valid code checking as above
-  if not (get_cstatus or head_cstatus or conn_status) then return false, nil end
-  stdnse.print_debug("Test 2 - Wikipedia.org\nReceived valid status codes, but pattern does not match")
-
-  test_url = "http://www.computerhistory.org"
-  hostname = "www.computerhistory.org"
-  pattern  = "museum"
-  get_status, get_r3, get_cstatus = proxy.test_get(host, port, "http", test_url, hostname, pattern)
-  conn_status = proxy.test_connect(host, port, "http", hostname)
-
-  if get_status then fstatus = true; response = response .. " GET" end
-  if conn_status then
-    if not cstatus then response = response .. " CONNECTION" end
-    cstatus = true
-  end
- 
-  if fstatus then return fstatus, "Methods supported:" .. response end
-  if not get_cstatus then
-    stdnse.print_debug("Test 3 - Computer History\nReceived valid status codes, but pattern does not match")
-  end
+  stdnse.print_debug("Received valid but something goes bad")
 
   -- Check if GET is being redirected
   if proxy.redirectCheck(get_r1, get_r2) and proxy.redirectCheck(get_r2, get_r3) then
@@ -192,18 +117,12 @@
 action = function(host, port)
   local supported_methods = "\nMethods successfully tested: "
   local fstatus = false
-  local def_test = true
   local test_url, pattern
 
   test_url, pattern = proxy.return_args() 
  
-  if(test_url) then def_test = false end
   if(pattern) then pattern = ".*" .. pattern .. ".*" end
-
-  if def_test 
-    then fstatus, supported_methods = default_test(host, port)
-    else fstatus, supported_methods = custom_test(host, port, test_url, pattern);
-  end
+  fstatus, supported_methods = anon_test(host, port);
 
   -- If any of the tests were OK, then the proxy is potentially open
   if fstatus then

Nota: ¡Vaya diff ha salido para cuatro líneas que he cambiado tú!

Siendo xxxx.net donde hemos alojado el antiscript.php. Lo que hace es mostrarnos, con la ayuda del php, cual es la IP que llega después de pasar por el proxy. Y ahora, nos lanzamos con el discovery a muerte. Engancho pastebin-lista-de-proxys-de-turno y ¡le damos gas!

 
Nmap scan report for cmts-188-9.cepat.net.id (202.43.188.9)
Host is up (0.39s latency).
PORT     STATE    SERVICE
3128/tcp closed   squid-http
8080/tcp filtered http-proxy
8081/tcp closed   blackice-icecap

Nmap scan report for 91.98.155.120.pol.ir (91.98.155.120)
Host is up (0.65s latency).
PORT     STATE  SERVICE
3128/tcp open   squid-http
|_http-open-proxy-anon: ERROR: Script execution failed (use -d to debug)
8080/tcp closed http-proxy
8081/tcp closed blackice-icecap

Nmap scan report for 217.218.43.130
Host is up (0.39s latency).
PORT     STATE    SERVICE
3128/tcp open     squid-http
| http-open-proxy-anon: Potentially OPEN proxy.
|_Methods supported:  GET CONNECTION IP:11.222.33.444
8080/tcp closed   http-proxy
8081/tcp filtered blackice-icecap

Nmap scan report for 219.137.229.214
Host is up (0.32s latency).
PORT     STATE    SERVICE
3128/tcp open     squid-http
| http-open-proxy-anon: Potentially OPEN proxy.
|_Methods supported:  GET CONNECTION IP:11.222.33.444
8080/tcp filtered http-proxy
8081/tcp closed   blackice-icecap

Aquí tenemos de todo. Vemos los TIMEOUTs que hablábamos al principio (failed) y vemos GET CONNECTION IP:11.222.33.444 (obviamente la IP esta cambiada). De esta forma, y conociendo nuestra IP sabemos si el proxy la cambia (anónimo) o la introduce en alguna de las cabeceras (no anónimo pero si proxy abierto).

El diff mostrado se puede descargar desde aquí, aunque si no se hace una modificación en la url no funcionara.

A partir de este PoC, vuestra imaginación es sabia e infinita compañeros.

Comments

  1. Interesante post!

  2. Buena entrada, buena idea y buen matiz sobre la otra entrada. El título debería haber sido «Detección de proxies abiertos (sin autenticación)», sobretodo para el caso de los métodos GET y HEAD que es cuando depende de la configuración del proxy y las cabeceras HTTP que pueda introducir. Por otro lado, los que detecta el script original con soporte para el método CONNECT directamente se podrían considerar anónimos, ¿no? y hacer usos malintencionados como un escaneo de puertos tcp o lanzar ataques sobre cualquier puerto TCP, incluido el 80.

    Lo dicho muy buena matización :-)

    Un saludo.

Trackbacks

  1. […] Hace unos meses se escribió un primer artículo en securyartwork sobre la detección de proxys con NSE de nmap. Se llamaba “Detectando proxies anónimos”.  […]