Protegiendo nuestro sistema de VoIP con Kamailio

 

Cuando se gestiona una centralita local, lo normal es no tener ningún tipo de puerto abierto al exterior, dado que las conexiones las realizamos siempre nosotros hacia nuestro proveedor de VoIP. A no ser que realicemos una validación por IP, con lo que deberemos permitir el acceso a nuestro puerto SIP única y exclusivamente a la IP que nos facilite nuestro(s) operador(es).

En el caso de que administremos un sistema más complejo, o simplemente, ofrezcamos servicios de VoIP a terceros, seguramente tendremos otros elementos involucrados en nuestro sistema, como servidores proxy.

En mi último post – guau! hace más de 1 año ya! – escribí sobre Kamailio y, sin despreciar a su ‘hermano’ OpenSIPS, voy a poner algunas configuraciones bastante útiles para protegernos tanto de escaneos como de ataques (fuerza bruta, DoS, …). Para ello vamos a utilizar programación en LUA, que nos permitirá dejar un poco más limpio nuestro fichero de configuración de Kamailio.

 

Evitando los molestos escáner SIP

Normalmente todos los escáner SIP utilizan un User-Agent propio por defecto. De hecho, parece mentira que el escáner más conocido, SIPVicious, esté programado en Python y la gente siga usándolo ‘sin alterar’, es decir, sin cambiar ese agente de usuario tan conocido … hablo de ‘friendly-scanner’. Por tanto, nos sobra con poner un pequeño filtro en nuestro Kamailio que busque los UA más conocidos … algo así:

route[REQINIT] {
..........

if($ua == "friendly-scanner" || $ua == "sundayddr" || $ua == "sip-scan" || $ua == "iWar" || $ua == "sipsak") {
   xlog("L_ALERT", "Attack attempt! IP:$si:$sp - R:$ruri - F:$fu - T:$tu - UA:$ua - $rm\n");
   drop();
}

..........
}

 

Lo que estamos haciendo es un drop del paquete cuando detectemos un User_agent que concuerde con un escáner SIP conocido.

 

Módulo pike + htable de Kamailio

Los módulos pikehtable de Kamailio son más que conocidos y nos permiten bloquear intentos de registro erróneos. Se puede ver toda la información en la documentación de Kamailio:

http://kamailio.org/docs/modules/stable/modules/pike.html

http://kamailio.org/docs/modules/stable/modules/htable.html

Por ejemplo, podemos definir cómo queremos que se comporte en la definición de parámetros, donde size es el número de intentos y autoexpire indica el tiempo tras el cual se libera el bloqueo:

modparam("htable", "htable", "wrongpass=>size=8;autoexpire=900")
modparam("htable", "htable", "ipban=>size=8;autoexpire=300")

 

En la ruta de autenticación comprobaremos el número de intentos erróneos y, si es superior al permitido, lo bloquearemos:

route[AUTH] {
..........

        if (is_method("REGISTER|INVITE") || from_uri==myself)
        {
                # User reached auth_count top
                if($sht(wrongpass=>$au::auth_count)==$sel(cfg_get.auth.max{s.int}))
                {
                        $var(exp) = $Ts - $sel(cfg_get.auth.bantime{s.int});

                        if ($sht(wrongpass=>$au::last_auth) > $var(exp))
                        {
                                # no more auth checks
                                xdbg("username $au $rm blocked from $proto:$si:$sp\n");
                                # we also can send a fake 200 OK reply
                                # sl_send_reply("200","OK");
                                exit;
                        } else {
                                # bantime seg reached... clean count
                                $sht(wrongpass=>$au::auth_count) = 0;
                        }
                }

                # authenticate requests
                if (!radius_www_authorize("mysip.server.com")) {
                        switch ($rc) {
                        case -7:
                           send_reply("500", "Server Internal Error");
                           exit;
                        case -2:
                                if ($sht(wrongpass=>$au::auth_count) == $null) {
                                        $sht(wrongpass=>$au::auth_count) = 0;
                                }

                                $sht(wrongpass=>$au::auth_count) = $sht(wrongpass=>$au::auth_count) + 1;

                                if ($sht(wrongpass=>$au::auth_count) == $sel(cfg_get.auth.max{s.int})) {
                                        xlog("L_ALERT","Auth failed $sel(cfg_get.auth.max) times - user:$au from $proto:$si:$sp\n");
                                }

                                $sht(wrongpass=>$au::last_auth) = $Ts;
                                break;
                        case -1:
                           send_reply("400", "Bad Request");
                           exit;
                        default:
                        };
                        if (defined($avp(digest_challenge)) &&
                                ($avp(digest_challenge) != "")) {
                                append_to_reply("$avp(digest_challenge)");
                        };
                        send_reply("401", "Unauthorized");
                        exit;
                }
                # clean
                if ($sht(wrongpass=>$au::auth_count) != $null) {
                        $sht(wrongpass=>$au::auth_count) = 0;
                }
        }

..........
}

 

Creando listas negras

A parte de estos módulos que nos ofrece Kamailio, podemos programar todo lo que se nos ocurra, gracias al enorme potencial de Kamailio. Por ejemplo, podemos crear listas negras y blancas de usuarios o de IPs para permitir o bloquear a determinadas direcciones IP o usuarios.

Por ejemplo, sirva como base la siguiente tabla de MySQL para crear listas negras de direcciones IP:

CREATE TABLE IF NOT EXISTS `black_list` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`ip` varchar(32) COLLATE utf8_spanish_ci NOT NULL,
`detail` varchar(255) COLLATE utf8_spanish_ci NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8 COLLATE=utf8_spanish_ci AUTO_INCREMENT=42 ;

Podemos utilizar los campos:

– ip: para bloquear una dirección IP
– detail: para añadir alguna observación

Desde Kamailio comprobaremos si está en la lista negra, en cuyo caso bloquearemos la petición:

route[REQINIT] {
..........

        if(src_ip!=myself)
        {
                # check if the IP is blacklisted
                lua_run("check_blacklist", "$si");
                if ($avp(s:wb_status) == "black") {
                       xlog("L_ALERT","Received $rm from a blacklisted $avp(s:wb_type)! $fU (IP:$si) - Sending Drop ...\n");
                       $sht(ipban=>$si) = 1;
                       drop();
                }
         }

..........
}

Y usando LUA programaremos la función check_blacklist

-- check blacklist
function check_blacklist(ip)
        local con, cur, row
        con = connect_ro('openser')
        cur = assert (con:execute("SELECT id FROM black_list WHERE ip='" .. ip .. "'))
        row = cur:fetch({}, "a")
        cur:close()
        con:close()
        if row then
                sr.pv.sets('$avp(wb_status)', 'black')
        else
                sr.pv.sets('$avp(wb_status)', '')
        end

        return
end

 

Al igual que bloqueamos un paquete que provenga de una determinada dirección IP, sería muy sencillo bloquear ciertos usuarios, agentes de usuario, etc.

 

Filtrado por países

Del mismo modo que bloqueamos direcciones IP, podemos realizar una comprobación del país que realiza las peticiones. Por ejemplo:

CREATE TABLE IF NOT EXISTS `black_list_country` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`country` varchar(5) COLLATE utf8_spanish_ci NOT NULL,
`detail` varchar(255) COLLATE utf8_spanish_ci NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8 COLLATE=utf8_spanish_ci AUTO_INCREMENT=42 ;

Podemos utilizar los campos:

– country: código de país a bloquear
– detail: para añadir alguna observación

Por otro lado:

CREATE TABLE IF NOT EXISTS `ipcountry` (
  `ipstart` varchar(20) NOT NULL,
  `ipend` varchar(20) NOT NULL,
  `countrycode` varchar(5) NOT NULL,
  `ipfrom` bigint(11) NOT NULL,
  `ipto` bigint(11) NOT NULL,
  KEY `ipstart` (`ipstart`,`ipend`)
) ENGINE=MyISAM DEFAULT CHARSET=latin1;

Donde:

– ipstart e ipend: rangos de IPs (ejemplo: 2.20.179.0 – 2.20.179.255)
– countrycode: código del país (ejemplo: ES)
– ipfrom e ipto: IPs en formato decimal (ejemplo: 34910976 – 34911231)

Podemos descargar la base de datos de IPs de aquí: http://db-ip.com/db/download/country

Y el motivo por el que meto en la base de datos las IPs en dos formatos es por temas de optimización, para no tener que calcularlas en cada consulta. Para ello, tras importar la base de datos deberemos calcular las direcciones IP en su formato decimal:

mysql> update `ipcountry` set ipfrom = INET_ATON(ipstart);
mysql> update `ipcountry` set ipto = INET_ATON(ipend);

Y desde Kamailio, para filtrar los países bloqueados, será de forma similar a lo visto antes. Comprobamos si el país está bloqueado y hacemos un drop a la vez que bloqueamos la IP:

route[REQINIT] {
..........

      lua_run("get_countrycode", "$si");
      lua_run("check_country_blacklist", "$avp(s:countryCode)");
      if ($avp(s:wb_status) == "black") {
              xlog("L_ALERT","Received $rm from a blacklisted country ($avp(s:countryCode))! $fU (IP:$si) - Sending Drop ...\n");
             $sht(ipban=>$si) = 1;
             drop();
       }

..........
}

 

En el LUA tendremos:

-- get country code from IP address
function get_countrycode(ipaddr)
        local con, cur, row
        con = connect_ro('openser')
        cur = assert (con:execute("SELECT countrycode FROM ipcountry WHERE INET_ATON('" .. ipaddr .. "') BETWEEN ipfrom AND ipto LIMIT 1"))
        row = cur:fetch({}, "a")
        cur:close()
        con:close()
        if row then
                sr.pv.sets('$avp(countryCode)', row.countrycode)
        else
                sr.pv.sets('$avp(countryCode)', '')
        end

        return
end

-- check countries blacklisted
function check_country_blacklist(country)
        local con, cur, row
        con = connect_ro('openser')
        cur = assert (con:execute("SELECT id FROM black_list_country WHERE country='" .. country .. "'"))
        row = cur:fetch({}, "a")
        cur:close()
        con:close()
        if row then
                sr.pv.sets('$avp(wb_status)', 'black')
        else
                sr.pv.sets('$avp(wb_status)', '')
        end

        return
end

 

Si lo ejecutamos bloqueando IPs españolas obtendremos algo así:

Jul 30 20:44:51 sip /usr/sbin/kamailio[1672]: ALERT: <script>: Received REGISTER from a blacklisted country (ES)! user1 (IP:62.xx.xx.xx) - Sending Drop ...
Jul 30 20:44:52 sip /usr/sbin/kamailio[1672]: ALERT: <script>: Received REGISTER from a blacklisted country (ES)! user2 (IP:62.xx.xx.xx) - Sending Drop ...
Jul 30 20:44:53 sip /usr/sbin/kamailio[1674]: ALERT: <script>: Received REGISTER from a blacklisted country (ES)! user3 (IP:83.xx.xx.xx) - Sending Drop ...
Jul 30 20:44:53 sip /usr/sbin/kamailio[1672]: ALERT: <script>: Received REGISTER from a blacklisted country (ES)! user4 (IP:83.xx.xx.xx) - Sending Drop ...
Jul 30 20:44:53 sip /usr/sbin/kamailio[1693]: ALERT: <script>: Received REGISTER from a blacklisted country (ES)! user5 (IP:62.xx.xx.xx) - Sending Drop ...

 

De lectura obligatoria:
http://www.kamailio.org/wiki/tutorials/security/kamailio-security