STATEMENT
声明
由于传播、利用此文所提供的信息而造成的任何直接或者间接的后果及损失,均由使用者本人负责,雷神众测及文章作者不为此承担任何责任。
雷神众测拥有对此文章的修改和解释权。如欲转载或传播此文章,必须保证此文章的完整性,包括版权声明等全部内容。未经雷神众测允许,不得任意修改或者增减此文章内容,不得以任何方式将其用于商业目的。
西安安全运营中心
NO.1 前言
CobaltStrike TeamServer 在认证的时候,是没有类似验证码的功能的,这就为蓝队爆破 TeamServer 密码提供了可能。
再加上攻防演练当中出现的「红队钓鱼事件」,团队中不乏有新鲜血液,稍有不慎就会让用户家目录下的 .aggressor.prop 文件被他人窃取,令团队成果付之东流。
那么如何规避这种情况呢?我们有如下几个方案考虑:
方案一: 直接反编译 CobaltStrike JAR 包,修改逻辑之后重打包。
该方案的好处是可控性强,可以改的面目全非,甚至可以把配置文件中的密码保存改成加密的形式。
缺点也显而易见:
· 首先CS反编译之后重打包挺让人抓狂的,不好弄。
· 其次必须要用专有的 cobaltstrike 去连接,后续如果拿到新的版本,工作量是比较大的
· 团队成员机器如果被控,攻击者完全可以偷走专有的工具和配置文件进行连接
所以该方案 Pass
方案二:网络层动手脚
TeamServer 监听在 127.0.0.1 接口上,通过 SSH 等其它方式建立 Socks5隧道或者 VPN,团队成员在接入 TeamServer 时,先进入到专网当中,再连接。
好处是成本低,无任何额外工作量,TeamServer 监听的端口也不会被 fofa zoomeye 这些网络空间搜索引擎扫描到。
缺点是团队成员机器被控后,攻击者完全可以该成员机器作为跳板,通过专网连接。
所以该方案也 Pass
进一步分析
将认证的过程分离开来,一部分保存在团队成员电脑上,另一部分放在团队成员的个人手机上,这就是本文要推荐的方案了,也是两步验证的核心思路了。
我们先来看一下 CS TeamServer 中关于认证过程的逻辑:
package server;
public class ManageUser implements Runnable {
protected TeamSocket client;
protected boolean authenticated = false;
protected String nickname = "";
protected Resources resources;
protected ManageUser.BroadcastWriter writer = null;
protected Map calls = null;
protected Thread mine = null;
public void process(Request request) throws Exception {
String reqArg0;
if (!this.authenticated && "aggressor.authenticate".equals(request.getCall()) && request.size() == 3) {
reqArg0 = request.arg(0) + ""; // 用户昵称
String password = request.arg(1) + ""; // 密码
String clientver = request.arg(2) + ""; // 客户端版本
if (!Aggressor.VERSION.equals(clientver)) { // 版本比对
this.client.writeObject(request.reply("Your client software does not match this servernClient: " + clientver + "nServer: " + Aggressor.VERSION));
} else if (ServerUtils.getServerPassword(this.resources, reqArg0).equals(password)) { // 密码比对
if (this.resources.isRegistered(reqArg0)) { // 用户是否已经登录了
this.client.writeObject(request.reply("User is already connected."));
} else { // 登录成功
this.client.writeObject(request.reply("SUCCESS"));
this.authenticated = true;
this.nickname = reqArg0;
Thread.currentThread().setName("Manage: " + this.nickname);
this.writer = new ManageUser.BroadcastWriter();
(new Thread(this.writer, "Writer for: " + this.nickname)).start();
}
} else {
this.client.writeObject(request.reply("Logon failure"));
}
} else if (!this.authenticated) {
this.client.close();
}
// ... 省略其它代码
}
}
ServerUtils.getServerPassword 的内容也很简单,就是直接获取密码而已
package server;
public class ServerUtils {
public static String getServerPassword(Resources resource, String key) {
return (String)resource.get("password");
}
}
可以看到,整个流程当中,密码比对了一次,那是不是意味着我们修改了 getServerPassword 这块就完事了?
刚开始我也是这么想的,后来发现事情并没有这么简单,TeamServer 的 password 不止一处使用到了
我们先来看一下 TeamServer 初始化的代码,先从 main 函数看,main 函数从命令行中接到了参数之后,初始化 teamserver 然后调用了 teamserver 的 go 方法
package server;
public class TeamServer {
public void go() {
try {
new ProfileEdits(this.c2profile);
this.c2profile.addParameter(".watermark", this.auth.getWatermark());
this.c2profile.addParameter(".self", CommonUtils.readAndSumFi1e(TeamServer.class.getProtectionDomain().getCodeSource().getLocation().getPath()));
this.resources = new Resources(this.calls);
this.resources.put("c2profile", this.c2profile);
this.resources.put("localip", this.host);
this.resources.put("password", this.pass); // 向 resources 这个 Map 中设置 password
(new TestCall()).register(this.calls);
WebCalls webcalls = new WebCalls(this.resources);
webcalls.register(this.calls);
// ... 省略中间其它初始化的代码
SecureServerSocket secserversocket = new SecureServerSocket(this.port); // 初始化 ssl socket 对指定的端口监听
while(true) {
secserversocket.acceptAndAuthenticate(this.pass, new PostAuthentication() { // 注意这里,直接传入 password 进行认证
public void clientAuthenticated(Socket socket) {
try {
socket.setSoTimeout(0);
TeamSocket teamsocket = new TeamSocket(socket);
(new Thread(new ManageUser(teamsocket, TeamServer.this.resources, TeamServer.this.calls), "Manage: unauth'd user")).start();
} catch (Exception ex) {
MudgeSanity.logException("Start client thread", ex, false);
}
}
});
}
} catch (Exception e) {
MudgeSanity.logException("team server startup", e, false);
}
}
public static void main(String[] args) {
int port = CommonUtils.toNumber(System.getProperty("cobaltstrike.server_port", "50050"), 50050);
if (!AssertUtils.TestPort(port)) {
System.exit(0);
}
Requirements.checkConsole();
Authorization authorization = new Authorization();
License.checkLicenseConsole(authorization);
MudgeSanity.systemDetail("scheme", QuickSecurity.getCryptoScheme() + "");
if (args.length != 0 && (args.length != 1 || !"-h".equals(args[0]) && !"--help".equals(args[0]))) {
if (args.length != 2 && args.length != 3 && args.length != 4) {
CommonUtils.print_error("Missing arguments to start team servernt./teamserver <host> <password> [/path/to/c2.profile] [YYYY-MM-DD]");
} else if (!CommonUtils.isIP(args[0])) {
CommonUtils.print_error("The team server <host> must be an IP address. " + host_help);
} else if ("127.0.0.1".equals(args[0])) {
CommonUtils.print_error("Don't use 127.0.0.1 for the team server <host>. " + host_help);
} else if ("0.0.0.0".equals(args[0])) {
CommonUtils.print_error("Don't use 0.0.0.0 for the team server <host>. " + host_help);
} else if (args.length == 2) { // 没有指定 teamserver profile 的情况
MudgeSanity.systemDetail("c2Profile", "default");
TeamServer teamserver = new TeamServer(args[0], port, args[1], Loader.LoadDefaultProfile(), authorization);
teamserver.go();
} else if (args.length == 3 || args.length == 4) { // 指定了 profile 的情况
// ... 省略中间其它初始化的代码
TeamServer teamserver = new TeamServer(args[0], port, args[1], profile, authorization); // args[1] 是 teamserver 的 password
teamserver.go(); // 调用了上面 go 方法
}
} else {
CommonUtils.print_info("./teamserver <host> <password> [/path/to/c2.profile] [YYYY-MM-DD]nnt<host> is the (default) IP address of this Cobalt Strike team servernt<password> is the shared password to connect to this servernt[/path/to/c2.profile] is your Malleable C2 profilent[YYYY-MM-DD] is a kill date for Beacon payloads run from this servern");
}
}
}
再跟进 SecureServerSocket 类里面,看一下 acceptAndAuthenticate 的实现
package ssl;
public class SecureServerSocket {
protected ServerSocket server;
protected boolean authenticate(Socket socket, String password, String addr) throws IOException {
DataInputStream in = new DataInputStream(socket.getInputStream());
DataOutputStream out = new DataOutputStream(socket.getOutputStream());
int var6 = in.readInt();
if (var6 != 48879) {
CommonUtils.print_error("rejected client from " + addr + ": invalid auth protocol (old client?)");
return false;
} else {
int var7 = in.readUnsignedByte();
if (var7 <= 0) {
CommonUtils.print_error("rejected client from " + addr + ": bad password length");
return false;
} else {
StringBuffer sb = new StringBuffer();
for(int i = 0; i < var7; ++i) {
sb.append((char)in.readUnsignedByte());
}
for(int i = var7; i < 256; ++i) {
in.readUnsignedByte();
}
synchronized(this.getClass()) {
CommonUtils.sleep((long)CommonUtils.rand(1000));
}
if (sb.toString().equals(password)) { // 读 password
out.writeInt(51966);
return true;
} else {
out.writeInt(0);
CommonUtils.print_error("rejected client from " + addr + ": invalid password");
return false;
}
}
}
}
public Socket acceptAndAuthenticate(final String password, final PostAuthentication postauth) {
String addr = "unknown";
try {
final Socket socket = this.server.accept();
addr = socket.getInetAddress().getHostAddress();
(new Thread(new Runnable() {
public void run() {
String var1x = "unknown";
try {
var1x = socket.getInetAddress().getHostAddress();
if (SecureServerSocket.this.authenticate(socket, password, var1x)) { // 调用上面的 authenticate
postauth.clientAuthenticated(socket);
return;
}
} catch (Exception var4x) {
MudgeSanity.logException("could not authenticate client from " + var1x, var4x, false);
}
// ...
}
}, "accept client from " + addr + " (auth phase)")).start();
} catch (Exception e) {
MudgeSanity.logException("could not accept client from " + addr, e, false);
}
return null;
}
// ...
}
password 这个东西,除了我们在CobaltStrike Client 上点了 Connect 按钮之后调用 aggressor.authenticate 时会用到之外,还会在 Client 和 TeamServer 通信过程中也会使用,除此之外还有很多处会用到(我太懒了没去找了),所以我们如果把 password 字段作为 2FA 参与的字段的话,要改动的地方太多了。那么,不如换个思路,把 2FA TOTP Code 放在 nickname处,那么就只需要修改 server.ManageUser 这个类的 process 方法中关于 aggressor.authenticate 处理部分的逻辑就好了。
NO.2 动手实现
如何操作呢,直接反编译 cs 源码重打包的方式固然可以,麻烦是麻烦了一点,但是我们不用,因为这种方式不够通用。Java 中提供了 javaagent 这么个东西, 熟悉 Burp 的同学应该会有印象,BurpLoader 就是用 javaagent 这种方式实现的破解:
java -Dfile.encoding=utf-8 -javaagent:burp-loader.jar -noverify -jar burpsuite_pro_v2020.11.jar
我们完全可以借助 javaagent 和 javassist 对 cobaltstrike.jar 在加载前动态修改。
-javaagent 参数是可以加很多个的,也就是说,我们可以实现一堆的服务端的「插件」,想用哪个开哪个,还不用担心破坏原 jar 包。
核心代码如下:
try {
if (className == null) {
return classfileBuffer;
} else if (className.equals("server/ManageUser")) { // 只修改 ManageUser 类
CtClass cls = classPool.makeClass(new ByteArrayInputStream(classfileBuffer));
CtMethod ctmethod = cls.getDeclaredMethod("process",
new CtClass[]{
classPool.get("common.Request")
});
String func = "{"
+ "if (!$0.authenticated && "aggressor.authenticate".equals($1.getCall()) && $1.size() == 3) {"
+ " java.lang.String mnickname = $1.arg(0)+"";"
+ " java.lang.String mpassword = $1.arg(1)+"";"
+ " java.lang.String mver = $1.arg(2)+"";"
+ " if(mnickname.length() < 6){ $0.client.writeObject($1.reply("Dynamic Code Error."));return; };" // 用户名如果低于 6 位就直接 return
+ " java.lang.String lastcode = de.taimos.totp.TOTP.getOTP(""+totpSecretKey+"");" // 生成 TOTP 6位数字
+ " if(!mnickname.substring(mnickname.length()-6, mnickname.length()).equals(lastcode)) {" // 比对动态口令,如果口令没对上,就 return
+ " $0.client.writeObject($1.reply("Dynamic Code Error."));return;"
+ " }"
+ "}"
+ "}";
ctmethod.insertBefore(func); // 把上面的代码插入到 process 函数最前面,如果口令正确,就继续走 cs 常规的流程
return cls.toBytecode();
}
} catch (Exception ex) {
ex.printStackTrace();
System.out.printf("[CSTOTPAgent] PreMain transform Error: %sn", ex);
}
为了方便使用,我们再加一点小细节,比如说每次动态生成 totp secretkey 的函数,secretkey 生成二维码的功能啥的,就比较方便了。
最终打包成 CSTOTP.jar 用于使用
NO.3 使用
1、把生成好的 CSTOTP.jar 放到服务端和 cobaltstrike.jar 相同目录
没错,不需要给团队成员分发,只用放在 server 端就行了
2、生成 TOTP SecretKey
java -cp CSTOTP.jar com.maxwell.Main
不要奇怪包名为啥叫麦斯威尔,我自己随便写的
3、掏出你的手机,下载 Google Authenticator (有些叫 谷歌身份验证器),或者随便找一个支持 2FA 的 APP 就行。然后扫描上面的二维码,或者是手动把 SecretKey 添加进去就行。
这玩意儿请务必保存好,这再被人偷了就只能活该了。攻防演练前生成好,让团队成员扫描这个二维码即可
4、生成 SecretKey 之后,根据最后一行提示,把最后一行的内容添加到 teamserver 这个文件里
红线上的内容之前是没有的,之后我们保存
5、你之前是怎么运行 teamserver 的,现在还是怎么运行
比如我启动的 TeamServer 密码是 qax
NO.4 测试效果
客户端无需任何额外的文件,你该怎么用就怎么用
下图是以 agscript 方式登录
可以看到即使输入正确的密码,也是无法登录的,只有输入正确的密码,并且在 nickname后面加上有效的 6 位动态口令,才可以成功
换 UI 方式测试
不带 TOTP Code
nickname 后面带上 TOTP Code 之后:
完美~
RECRUITMENT
招聘启事
END
长按识别二维码关注我们
本文始发于微信公众号(雷神众测):为 CobaltStrike TeamServer 加上谷歌二次验证
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论