其实我研究这个东西很久了,在今年年初就做了个残品,无奈一直都缺乏时间好好做完,同时残品的方案也有一定困难,所以我抛弃了那个方案,另外想了思路;
最近和同事一起讨论,再参考了商业软件的一些做法,总算是确定了一个思路。
我去年写过一篇blog:
Javascript的输入输出,以及二次渲染问题
就已经在探讨这个问题了,当时的产出是有了一个防御方案。
今年7月底的blackhat
2010上,shreeraj shah 做了一个“ ” ,并发布了两个工具用于检测javascript里的安全问题,包括DOM
XSS。
我看了他的成果后,发现这个思路和我是一致的,甚至我还走在了他的前面,但当时遇到不可逾越的障碍。
直到前不久,我结合了商业化扫描器的思路,以及和同事讨论后,得出了一个较为靠谱的方案。
理论基础:
DOM based XSS 的产生原因,我们只需要关注两个方面
A) 脏数据的输入
location
document.referrer
window.name
ajax response
jsonp
form下的inputs框
B) 脏数据的输出
document.write(ln)
innerHTML =
outterHTML =
写 window.location 操作
写 javascript: (伪协议后内容的自定义)
eval、setTimeout 、setInterval 等直接执行
关于DOM XSS 的输入点,
"DOM XSS之父"
总结了一个表:
基于这个理论基础,我们要实现自动化检测,就很好做了。
一开始我的想法是,先找到
document.write 等会造成危害的地方,然后回溯变量与函数调用过程,看用户是否能够控制输入。这也是一般挖漏洞的思路。shreeraj shah 也是这个思路。
这个思路最大的问题,就是在回溯变量与函数调用过程中,会极其复杂,而且大部分时间会发现做了无用功。
我基于firefox 的
greasemonkey 写了一个脚本,其中核心代码部分:
- // Dangerous Methods
- var dm = {dw:"document.write", ih:"innerHTML"};
- // get All Scripts
- var ws, arr, arrline;
- ws = document.getElementsByTagName("script");
- arr = [];
- /*
- arr[i] = {
- content = raw_content;
- fn = filename;
- lc = [line1,line2,....];
- };
- */
- arrline = [];
- // 变量列表
- var vlist = new Array();
- // 获取所有scripts内容,包括ajax
- for (i=0; i<ws.length; i++){
- var m = {};
- if (ws[i].textContent){
- m.fn = window.location.href;
- m.content = js_beautify(ws[i].textContent);
- arr.push(m);
- } else if (ws[i].src){
- GM_xmlhttpRequest({
- method: 'GET',
- url: ws[i].src,
- headers: {
- 'User-agent': 'Mozilla/4.0 (compatible) Greasemonkey',
- //'Accept': 'application/atom+xml,application/xml,text/xml',
- },
- onload: function(responseData) {
- m.fn = this.url;
- m.content = js_beautify(responseData.responseText);
- arr.push(m);
- }
- });
- }
- }
- // 移除文本中的静态内容 'zzz', "xxx"
- function removeStatic(str, c){
- var s, result, start, end, i;
- if (str.charAt(0) == c){ //第一个引号
- start = 0
- } else {
- for (i=1;i<str.length;i++){
- if (str.charAt(i) == c && str.charAt(i-1) !== '\\'){
- start = i;
- break;
- }
- }
- }
-
- for (i=start+1;i<str.length;i++){ // 第二个引号
- if ( str.charAt(i) == c && str.charAt(i-1) !== '\\'){
- end = i;
- break;
- }
- }
- s = str.substring(0, start) + str.substring(end+1, str.length);
- result = s;
-
- return result;
- }
-
- // 发现并处理 innerHTML 的函数
- function checkInnerHTML(rawScript, n){
- var tl = rawScript.lc[n], r = rawScript.content, fn = rawScript.fn;
- var index, tmp, c;
-
- if (tl.charAt(tl.length-1)!=';'){ // 补上个 ;
- tl = tl+';';
- }
-
- var tlr = tl.replace(new RegExp(" ","g") , ""); // 去除空格
- if (tlr.indexOf('innerHTML=')>0){ // check 注意有多个 innerHTML=的情况;
- index = tlr.indexOf('innerHTML=');
- tmp = tlr.substring(index+10);
-
- // 去除 '', "" 中的内容
- c = '\'';
- while (tmp.indexOf(c)>=0){
- tmp = removeStatic(tmp, c);
- }
-
- c = '"';
- while (tmp.indexOf(c)>=0){
- tmp = removeStatic(tmp, c);
- }
- tmp = tmp.split(';')[0]; // 获取innerHTML的语句 innerHTML=....;
- if (tmp !== ''){
- //vlist.push(tmp);
-
- // 如果全是符号或数字,则也不是变量,变量必然是包含了字母的
- var containAlpha = false;
- for (var i=0;i<tmp.length;i++){
- if ((tmp.charCodeAt(i)>=65 && tmp.charCodeAt(i)<=90) || (tmp.charCodeAt(i)>=97 && tmp.charCodeAt(i)<=122) ){ // a-z,A-Z
- containAlpha = true;
- break;
- }
- }
- if (containAlpha == false){
- return -1;
- }
- // 找到函数名,与回溯函数过程
- var f, st='';
- for (var x=n; x>=0; x--){
- if (rawScript.lc[x].indexOf('function')>=0 || x==0){
- f = rawScript.lc[x];
- for (var y=x;y<=n;y++){
- st = st+rawScript.lc[y]+'\r\n';
- }
- break;
- }
- }
-
- GM_log('\r\n'
- +'Script: '+fn+' line '+n+':\r\n'
- +'raw: '+tl+'\r\n'
- +'after: '+tmp+'\r\n'
- +'function: '+f+'\r\n'
- +'stack: \r\n'+st+'\r\n'
- );
- return 1;
- }
- } else {
- return -1;
- }
- }
- // 发现并处理 document.write 的函数
- function checkDocumentWrite(rawScript, n){
- var tl = rawScript.lc[n], r = rawScript.content, fn = rawScript.fn;
- var tmp, index, c;
-
- var tlr = tl.replace(new RegExp(" ","g") , ""); // 去除空格
- index = tlr.indexOf('document.write(');
- if (index>=0){
- tmp = tlr.substring(index+15);
- }
-
- // 去除 '', "" 中的内容
- c = '\'';
- while (tmp.indexOf(c)>=0){
- tmp = removeStatic(tmp, c);
- }
- c = '"';
- while (tmp.indexOf(c)>=0){
- tmp = removeStatic(tmp, c);
- }
-
- tmp = tmp.split(')')[0];
-
- if (tmp !== ''){
- // 找到函数名,与回溯函数过程
- var f, st='';
-
- for (var x=n; x>=0; x--){
- if (rawScript.lc[x].indexOf('function')>=0 || x==0){
- f = rawScript.lc[x];
- for (var y=x;y<=n;y++){
- st = st+rawScript.lc[y]+'\r\n';
- }
- break;
- }
- }
- GM_log('\r\n'
- +'Script: '+fn+' line '+n+':\r\n'
- +'raw: '+tl+'\r\n'
- +'after: '+tmp+'\r\n'
- +'function: '+f+'\r\n'
- +'stack: \r\n'+st+'\r\n'
- );
- }
- return 1;
- }
- var timer = 0;
- var found = false;
- // 主函数
- var tag = setInterval(function(){
- if (arr.length == ws.length || timer == 2){
- clearInterval(tag);
-
- for (i=0;i<arr.length;i++){
- arr[i].lc = arr[i].content.split('\n');
-
- for (j=0;j<arr[i].lc.length;j++){
- if (arr[i].lc[j].indexOf("innerHTML")>0 ){
- if (checkInnerHTML(arr[i], j) == 1){
- found = true;
- }
- }
- if (arr[i].lc[j].indexOf("document.write")>=0){
- if (checkDocumentWrite(arr[i], j) == 1){
- found = true;
- }
- }
-
- }
- }
-
- if (found == false){
- GM_log("Not Found!");
- }
- }
- else {
- GM_log('Read: '+arr.length+' Scripts:'+ws.length+' '+'Waiting....');
- }
- timer++;
- },2000);
使用效果:
(注意这里其实已经是一个误报了,因为 j 明显是个 int)
后来与同事讨论后,发现从输入端检测变量是否有被污染,从程序实现上会更简单。
我们看IBM Watchfire公布的一张图,他们准备这么做,不过此时尚未发布任何东西。
url 这个变量是从污染源
document.URL 来的,经过变量传递,陆续污染了
变量 tempArr 和 TARGET
最终变量 TARGET 被输出函数
document.write 输出,造成了XSS。
所以这种检测思路就是从输入入手,观察变量传递的过程,最终检查是否有在危险函数输出,中途是否有经过安全函数,比如 htmlencode 之类。
http://blog.watchfire.com/wfblog/2010/11/scanning-for-client-side-javascript-vulnerabilities.html
Acunetix 的思路也类似
http://www.acunetix.com/blog/web-security-zone/articles/dom-xss/
这种是白盒自动化检测的思路,注意它一定是要工作在javascript层,也就是说客户端需要有一个javascript引擎,解析执行了javascript后,去分析,否则会漏掉比较多的内容。比如:
document.write("");
有一段脚本是远程加载的,不解析执行的话,可能就漏掉了。
我之前还想到了另外一种FUZZ的思路,对于单个页面的准确率会非常高,但是资源消耗也会特别大。
1. 混淆所有输入点
2. 页面渲染后,看是否会执行混淆后的脚本,比如 alert(123)
3. 遍历出页面中所有的javascript事件,然后一一触发之,看是否会执行
因为有的dom xss是隐藏在函数里的,只有一些事件能够触发这些函数,比如:
abc
这个FUZZ的思路,应该也是可行的,可以与白盒方法结合起来使用。
阅读(3951) | 评论(0) | 转发(0) |