Dojo #35 - Chatroom

Introduction

The 35th Dojo Challenge, Chatroom, invited participants to exploit a CWE-73: External Control of File Name or Path vulnerability and read a file containing the challenge flag.

YesWeHack asked to produce a qualified report explaining the logic allowing exploitation, as set out by the challenge.

Here’s my write-up for this challenge, which unfortunately didn’t make the top 3 :(.

Setup Code

const child_process = require('child_process')
const process = require('process')
const path = require('path')
const ejs = require('ejs')
const fs = require('fs')

process.chdir("/tmp")

// Flag 
fs.writeFileSync('flag.txt', flag)

// Design
fs.writeFileSync('index.ejs', `
<html>
<body>
    <div class="wrapper">
        <div class="base contacts">
        <div class="header">
            <input type="text" placeholder="Search...">
            <img class="icon" src="https://api.iconify.design/ic:baseline-search.svg?color=%23fff">
        </div>
        <ul>
            <li>
                <img class="profile" src="https://static.vecteezy.com/ti/gratis-foton/p1/22717360-sot-kanin-med-morot-vaska-tecknad-serie-ikon-illustration-djur-utbildning-ikon-begrepp-isolerat-generat-ai-gratis-fotona.jpg">
                <div class="user">
                    <p>Root</p>
                    <div class="status">
                        <div class="dot-active"></div>
                        <p style="font-size: 14px;"> Online</p>
                    </div>
                </div>
            </li>
            <li>
                <img class="profile" src="https://static.vecteezy.com/ti/gratis-foton/p1/22716493-sot-bi-flygande-tecknad-serie-ikon-illustration-djur-natur-ikon-begrepp-isolerat-generat-ai-gratis-fotona.jpg">
                <div class="user">
                    <p>Hackerman</p>
                    <div class="status">
                        <div class="dot"></div>
                        <p style="font-size: 14px;"> Offline</p>
                    </div>
                </div>
            </li>
            <li>
                <img class="profile" src="https://static.vecteezy.com/ti/gratis-foton/p1/22711661-hacker-rorelse-en-barbar-dator-tecknad-serie-ikon-illustration-teknologi-ikon-begrepp-isolerat-platt-tecknad-serie-stil-generat-ai-gratis-fotona.jpg">
                <div class="user">
                    <p>Pwner</p>
                    <div class="status">
                        <div class="dot"></div>
                        <p style="font-size: 14px;"> Offline</p>
                    </div>
                </div>
            </li>
            <li>
                <img class="profile" src="https://static.vecteezy.com/ti/gratis-foton/p1/30493982-sot-tecknad-serie-robot-med-horlurar-och-gul-blommor-vektor-illustration-ai-genererad-gratis-fotona.jpg">
                <div class="user">
                    <p>Robo7</p>
                    <div class="status">
                        <div class="dot"></div>
                        <p style="font-size: 14px;"> Offline</p>
                    </div>
                </div>
            </li>
        </ul>
        <img class="settings" src="https://api.iconify.design/solar:settings-bold.svg?color=%23999da5">
        </div>
        <div class="base chat">
            <div class="header">
                <img class="profile" src="https://pbs.twimg.com/profile_images/1576633593203392513/7lbM_Fd0_400x400.jpg">
                <p class="user" style="font-size: 22px;">Brumens</p>
                <div class="dot-active"></div>
                <img class="icon" src="https://api.iconify.design/majesticons:microphone.svg?color=%23fff">
                <img class="icon" src="https://api.iconify.design/majesticons:video-camera.svg?color=%23fff">
                <img class="props" src="https://api.iconify.design/mdi:dots-vertical.svg?color=%23999da5">
            </div>
            <div class="msg">
                <div class="dm1">
                    Hello there, so you trying to exploit this code injection? &#128526;
                </div>
                <div class="dm2">
                    Yes, leave me alone... &#129402;
                </div>
            </div>
            <div class="footer">
                <img src="https://api.iconify.design/material-symbols:add-circle.svg?color=%23999da5">
                    <% if ( message != null ) { %>
                        <input type="text" placeholder="<%= message %>">
                    <% } else { %>
                        <input type="text" placeholder="Message...">
                    <% } %>
                <button>Send</button>
            </div>
        </div>
    </div>
<a class="ref" href="https://www.vecteezy.com/free-vector">Images by Vecteezy</a>
<!-- Only design below (ignore) -->
<style>
@import url('https://fonts.googleapis.com/css2?family=Anta&family=Bungee+Shade&family=Clicker+Script&family=Indie+Flower&family=Inter+Tight:ital,wght@0,100..900;1,100..900&family=League+Spartan:wght@100..900&family=Madimi+One&family=Nabla&family=Sunflower:wght@300&display=swap');

:root {
    --color-shadow: rgb(0,0,0,0.33);
    --color-btn: #7d8fc5;
    --color-bg: #36393e;
    --color-primary: #424549;
    --color-border: #707377;
    --color-transparent: rgba(255, 255, 255, 0.2);
    --color-txt: #999da5;
    --color-offline: rgb(217, 42, 42);
    --color-online: #5de423;
}

body {
    margin: 0px;
    padding: 0px;
    background-color: var(--color-bg);
    color: var(--color-txt);
    font-family: "Madimi One", sans-serif;
    font-weight: 600;
    font-style: normal;
    object-fit: cover;
    width: 100%;
    height: 100%;
}

input {
    background-color: var(--color-primary);
    color: var(--color-txt);
    border: 1px solid var(--color-border);
    margin: 4px;
    border-radius: 13px;
    padding-left: 10px;
    font-size: 16px;
}

button {
    cursor: pointer;
    text-decoration: none;
    background-color: var(--color-btn);
    color: #fff;
    border: 0;
    border-radius: 13px;
    width: 100px;
    height: 50px;
    font-size: 16px;
    font-weight: 900;
    margin: 4px;
    transition: 0.3s;
}

.icon {
    cursor: pointer;
    transition: 0.3s;
    width: 24px;
    height: 24px;
}

.icon:hover {
    transform: translate(0, 3px);
}

button:hover {
    transform: translate(0, 3px);
}


li {
    margin-top: 10px;
    display: flex;
    list-style: none;
}

.wrapper {
    position: absolute;
    width: 100%;
    height: 100%;
    display: flex;
}

.profile {
    margin: 8px;    
    width: 50px;
    height: 50px;
    border-radius: 50%;
    border: 2px solid var(--color-border);
}

.contacts {
    border: 2px solid var(--color-border);
    box-shadow: 0 5px 20px var(--color-shadow);
    padding: 10px;
    flex: 30%;
    margin: 20px;
    border-radius: 22px;
    background-color: var(--color-primary);
    backdrop-filter: blur(3px);
    height: 100%;
}

.contacts input {
    width: 100%;
    height: 32px;
}

.contacts .header {
    display: flex;
    align-items: center;
}

.contacts .header .icon {
    background-color: var(--color-btn);
    border-radius: 50%;
    padding: 8px;
}

.contacts ul {
    margin: 0;
    padding: 0px;
}

.contacts ul li {
    border: 2px solid var(--color-border);
    border-radius: 13px;
    background-color: var(--color-primary);
}

.user .status {
    display: flex;
    align-items: center;
}

.dot {
    margin-right: 4px;
    width: 12px;
    height: 12px;
    background-color: var(--color-offline);
    border-radius: 50%;
}

.dot-active {
    margin-right: 4px;
    width: 12px;
    height: 12px;
    background-color: var(--color-online);
    border-radius: 50%;
}

.contacts .settings {
    border-radius: 50%;
    position: absolute;
    padding: 8px;
    width: 32px;
    height: 32px;
    bottom: 10;
    left: 10;
}

.chat {
    border: 2px solid var(--color-border);
    box-shadow: 0 5px 20px var(--color-shadow);
    padding: 10px;
    flex: 70%;
    margin: 20px;
    margin-left: 0px;
    border-radius: 22px;
    background-color: var(--color-primary);
    backdrop-filter: blur(3px);
    height: 100%;
}
.chat .header {
    border-radius: 19px 19px 0 0;
    padding-bottom: 10px;
    display: flex;
    align-items: center;
    gap: 20px;
    width: 100%;
}  

.user {
    padding: 6px;
}

.user p {
    font-weight: 900;
    font-size: 16px;
    margin: 0;
}

.user .status {
    text-align: center;
    border-radius: 12px;
    height: 20px;
    width: 60px;
    padding: 4px;
    padding-left: 8px;
    padding-right: 8px;
    background-color: var(--color-bg); 
    margin: 0;
}

.chat .profile {
    width: 80px;
    height: 80px;
}

.chat .icon {
    background-color: var(--color-btn);
    padding: 8px;
    border-radius: 50%;
}

.chat .props {
    padding: 6px;
    position: absolute;
    width: 32px;
    height: 32px;
    right: 50px;
}

.chat .msg {
    border-radius: 22px;
    padding-bottom: 10px;
    border: 2px solid var(--color-border);
    background-color: var(--color-bg);
    height: 50%;
}

/*I'm lazy*/
.chat .dm1 {
    border-radius: 22px 22px 22px 0;
    background-color: var(--color-btn);
    margin-left: 8px;
    margin-top: 20px;
    color: #fff;
    font-size: 20px;
    text-align: left;
    padding: 20px;
    width: 220px;
    height: 80px;
}
.chat .dm2 {
    position: absolute;
    border-radius: 22px 22px 0 22px;
    background-color: var(--color-btn);
    margin-right: 20px;
    margin-top: 3%;
    color: #fff;
    font-size: 20px;
    text-align: left;
    right: 0;
    padding: 20px;
    width: 220px;
    height: 30px;
}
.chat .footer {
    display: flex;
    justify-content: center;
    align-items: center;
    border-radius: 0 0 19px 19px;
    height: 100px;
}
.chat .footer  {
    height: 70px;
}
.chat .footer img {
    border-radius: 50%;
    padding: 8px;
    width: 38px;
    height: 38px;
}
.chat .footer input{
    width: 320px;
    height: 50px;
}

.ref {
    position: absolute;
    margin: 12px;
    color: #fff;
    bottom: 0;
    right: 0;
    font-weight: 100;
    font-size: 12px;
}
</style>
</body>
</html>
`)

return {fs, ejs, path, process, child_process}
class Message {
    constructor(to, msg) {
        this.to = to;
        this.msg = msg;
        this.file = null
    }
    send() {
        console.log(`Message sent to: ${this.to}`)
    }
    makeDraft() {
        this.file = path.basename(`${Date.now()}_${this.to}`)
        fs.writeFileSync(this.file, this.msg)
    }
    getDraft() {
        return fs.readFileSync(this.file)
    }
}

const userData = decodeURIComponent("")

var data = {"to":"", "msg":""}
if ( userData != "" ) {
    try {
        data = JSON.parse(userData)
    } catch(err) {
        console.error("Error : Message could not be sent!")
    }
}

var message = new Message(data["to"], data["msg"])
message.makeDraft()

console.log( ejs.render(fs.readFileSync('index.ejs', 'utf8'), {message: message.msg}) )

Description

When a message is sent, it is saved in the same folder as the index.ejs file. The path.basename function is used to attempt to secure the retrieval of the username in the to POST parameter.

As the rights to the index.ejs file were not hardened, it is possible to overwrite the contents of this file, which could allow an attacker to execute commands on the system.

Exploitation

The web application allows us to send messages via a JSON input that must contain two fields:

  • to, which contains the user’s name.
  • msg, which contains the message we wish to send.

Code Analysis - Find the vulnerability

When we look at the code, we see that the path.basename function is used when saving a draft.

class Message {
    constructor(to, msg) {
        this.to = to;
        this.msg = msg;
        this.file = null
    }
    [snip]
    makeDraft() {
        this.file = path.basename(`${Date.now()}_${this.to}`)
        fs.writeFileSync(this.file, this.msg)
    }
    [snip]
}

const userData = decodeURIComponent("")

var data = {"to":"", "msg":""}
if ( userData != "" ) {
    try {
        data = JSON.parse(userData)
    } catch(err) {
        console.error("Error : Message could not be sent!")
    }
}

var message = new Message(data["to"], data["msg"])
[snip]

Our value is not cleaned up, and by playing around with the function, we can see that if our value contains / then the function returns everything after it.

This will allow us to write to any arbitrary file in our current directory.

Code Analysis - Exploit the vulnerability

Now that we’ve got a first entry point, we need to know which file we’re going to be able to overwrite to read the flag.

At the end of the script, the index.ejs file is rendered, making it look like easy prey.

console.log( ejs.render(fs.readFileSync('index.ejs', 'utf8'), {message: message.msg}) )

By sending the value {"to":"x/index.ejs","msg":"x"} we can see that the page content has become x, which confirms the vulnerability.

All we need to do now is inspect the ejs documentation to understand how to inject code. Source: https://ejs.co/#docs

PoC

Reading the documentation, we learn that:

  • <%- allows you to include a file and display it in the template.
  • <%= allows you to display the return value in the template.

So we can use <%- to have an easy arbitrary file read.

File inclusion

Or we can use <%= to retrieve the return of a JavaScript code and display it in the page.

JavaScript code execution for SHELL code execution

Retrieving the flag: FLAG{W1th_Cr34t1vity_C0m3s_RCE!!}

RISK

File storage and rights must be taken into account when designing the application architecture. By exploiting a vulnerability enabling a file used for the application to be overwritten, an attacker would be able to read or overwrite the contents of important files, thus affecting the integrity and confidentiality of data present on the server.

He might also be able to execute commands, thus having an impact beyond his scope. This can lead to privacy breaches, data integrity violations, or even complete compromise of the system.

REMEDIATION

To protect against this vulnerability, apply the following recommendations:

  • Message storage: We recommend storing messages in a database and using an ORM to retrieve the various data.
  • Security audit: It is advisable to set up regular security audits with external service providers who can take a fresh look at the application and thus detect this type of vulnerability.
  • CI: During CI, it is recommended to set up unit tests to avoid vulnerability regressions.
  • Awareness and training: Educate developers and security teams on best practices for secure coding.

REFERENCES