● 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 / web / websocket.go
← Back to directory
265 lines · 7.6 KB
Raw
1package web
2
3import (
4 "bytes"
5 "context"
6 "crypto/sha256"
7 "encoding/hex"
8 "encoding/json"
9 "fmt"
10 "io"
11 "net/http"
12 "time"
13
14 "github.com/shehzade/contraband/pkg/client/evasion"
15 "github.com/shehzade/contraband/pkg/client/exfil"
16 "github.com/shehzade/contraband/pkg/common"
17 "nhooyr.io/websocket"
18)
19
20type WebSocketExfiltrator struct{}
21
22func NewWebSocketExfiltrator() *WebSocketExfiltrator {
23 return &WebSocketExfiltrator{}
24}
25
26func init() {
27 wsExfil := NewWebSocketExfiltrator()
28 exfil.RegisterExfiltrator(common.ProtoWebSocket, wsExfil)
29 exfil.RegisterExfiltrator("websocket", wsExfil)
30 exfil.RegisterExfiltrator("ws", wsExfil)
31}
32
33func (e *WebSocketExfiltrator) Protocol() string {
34 return common.ProtoWebSocket
35}
36
37func (e *WebSocketExfiltrator) Exfiltrate(ctx context.Context, reader io.Reader, opts exfil.ExfilOptions) (*common.ProbeResult, error) {
38 if err := opts.Validate(); err != nil {
39 return nil, err
40 }
41
42 start := time.Now().UTC()
43 result, err := common.NewProbeResult("exfil", e.Protocol(), opts.TargetHost, opts.TargetPort)
44 if err != nil {
45 return nil, err
46 }
47 result.Timestamps.StartTCP = start
48 result.TLSProfile = opts.TLSFingerprint
49 result.PayloadType = opts.PayloadType
50 result.CarrierType = opts.CarrierType
51
52 if opts.TotalBytes <= 0 {
53 if br, ok := reader.(*bytes.Reader); ok {
54 opts.TotalBytes = int64(br.Len())
55 } else if bb, ok := reader.(*bytes.Buffer); ok {
56 opts.TotalBytes = int64(bb.Len())
57 }
58 }
59
60 if opts.ExpectedHash == "" {
61 if br, ok := reader.(*bytes.Reader); ok {
62 cur, _ := br.Seek(0, io.SeekCurrent)
63 h := sha256.New()
64 _, _ = io.Copy(h, br)
65 opts.ExpectedHash = hex.EncodeToString(h.Sum(nil))
66 _, _ = br.Seek(cur, io.SeekStart)
67 }
68 }
69 result.PayloadHash = opts.ExpectedHash
70
71 if opts.SessionToken.IsZero() {
72 tok, tokErr := common.NewSessionToken()
73 if tokErr != nil {
74 return nil, tokErr
75 }
76 opts.SessionToken = tok
77 }
78
79 chunker, err := evasion.NewStreamChunker(reader, opts.SessionToken, opts.ChunkSize, opts.TotalBytes)
80 if err != nil {
81 return nil, fmt.Errorf("failed to initialize chunk streamer: %w", err)
82 }
83 defer chunker.Close()
84
85 totalChunks := chunker.TotalChunks()
86 result.TargetVolume = opts.TotalBytes
87
88 isTLS := opts.UseTLS || (opts.TargetPort != 80 && opts.InsecureTLS) || opts.TargetPort == 443 || opts.TargetPort == 8443 || opts.TargetPort == 9443 || opts.TargetPort == 10443
89 scheme := "ws"
90 if isTLS {
91 scheme = "wss"
92 }
93 wsURL := fmt.Sprintf("%s://%s:%d/ws", scheme, opts.TargetHost, opts.TargetPort)
94
95 dialOpts := &websocket.DialOptions{
96 HTTPHeader: make(http.Header),
97 }
98 dialOpts.HTTPHeader.Set(common.HeaderSessionID, opts.SessionToken.String())
99 dialOpts.HTTPHeader.Set(common.HeaderPayloadHash, opts.ExpectedHash)
100 for k, v := range opts.Headers {
101 dialOpts.HTTPHeader.Set(k, v)
102 }
103
104 if isTLS {
105 tlsCfg := exfil.TLSConfig{
106 ServerName: opts.TargetHost,
107 InsecureSkipVerify: opts.InsecureTLS,
108 Fingerprint: opts.TLSFingerprint,
109 ALPN: []string{"http/1.1"},
110 }
111 tr := exfil.NewHTTPTransport(tlsCfg)
112 if opts.ProxyDialer != nil {
113 tr.DialContext = opts.ProxyDialer.DialContext
114 }
115 dialOpts.HTTPClient = &http.Client{Transport: tr, Timeout: opts.Timeout}
116 } else if opts.ProxyDialer != nil {
117 tr := &http.Transport{
118 DialContext: opts.ProxyDialer.DialContext,
119 }
120 dialOpts.HTTPClient = &http.Client{Transport: tr, Timeout: opts.Timeout}
121 }
122
123 conn, _, err := websocket.Dial(ctx, wsURL, dialOpts)
124 if err != nil {
125 result.Verdict = common.ClassifyError(err)
126 result.ErrorMessage = fmt.Sprintf("websocket dial failed: %v", err)
127 result.Timestamps.TeardownDone = time.Now().UTC()
128 e.calculateDurations(&result)
129 return &result, err
130 }
131 conn.SetReadLimit(16 * 1024 * 1024)
132 defer conn.Close(websocket.StatusInternalError, "deferred exit")
133
134 result.Timestamps.ConnectedTCP = time.Now().UTC()
135 if isTLS {
136 result.Timestamps.HandshakeTLS = time.Now().UTC()
137 }
138 result.Timestamps.FirstByteSent = time.Now().UTC()
139
140 var lastMsgType websocket.MessageType
141 var lastAckData []byte
142 var headEvidence []byte
143
144 for {
145 chunkFrame, _, err := chunker.Next()
146 if err == io.EOF {
147 break
148 }
149 if err != nil {
150 result.Verdict = common.ClassifyError(err)
151 result.ErrorMessage = err.Error()
152 result.Timestamps.TeardownDone = time.Now().UTC()
153 e.calculateDurations(&result)
154 return &result, err
155 }
156
157 if len(headEvidence) < 768 && len(chunkFrame) > common.ChunkHeaderSize {
158 p := chunkFrame[common.ChunkHeaderSize:]
159 needed := 768 - len(headEvidence)
160 if needed > len(p) {
161 needed = len(p)
162 }
163 headEvidence = append(headEvidence, p[:needed]...)
164 }
165
166 if opts.Shaper != nil {
167 if err := opts.Shaper.WaitN(ctx, len(chunkFrame)); err != nil {
168 result.Verdict = common.ClassifyError(err)
169 result.ErrorMessage = err.Error()
170 result.Timestamps.TeardownDone = time.Now().UTC()
171 e.calculateDurations(&result)
172 return &result, err
173 }
174 }
175
176 if opts.Cadence != nil {
177 delay := opts.Cadence.NextDelay()
178 if delay > 0 {
179 select {
180 case <-time.After(delay):
181 case <-ctx.Done():
182 result.Verdict = common.ClassifyError(ctx.Err())
183 result.ErrorMessage = ctx.Err().Error()
184 result.Timestamps.TeardownDone = time.Now().UTC()
185 e.calculateDurations(&result)
186 return &result, ctx.Err()
187 }
188 }
189 }
190
191 if err := conn.Write(ctx, websocket.MessageBinary, chunkFrame); err != nil {
192 result.Verdict = common.ClassifyError(err)
193 result.ErrorMessage = fmt.Sprintf("websocket write failed: %v", err)
194 result.Timestamps.TeardownDone = time.Now().UTC()
195 e.calculateDurations(&result)
196 return &result, err
197 }
198 result.BytesSent += int64(len(chunkFrame))
199
200 // Read per-chunk ACK
201 msgType, ackData, err := conn.Read(ctx)
202 if err != nil {
203 result.Verdict = common.ClassifyError(err)
204 result.ErrorMessage = fmt.Sprintf("failed to read websocket ack: %v", err)
205 result.Timestamps.TeardownDone = time.Now().UTC()
206 e.calculateDurations(&result)
207 return &result, err
208 }
209 result.BytesRecv += int64(len(ackData))
210
211 var chunkAck common.VerificationACK
212 if err := json.Unmarshal(ackData, &chunkAck); err == nil && chunkAck.Status == "ERROR" {
213 if chunkAck.Verdict != "" {
214 result.Verdict = chunkAck.Verdict
215 } else {
216 result.Verdict = common.VerdictBlockedL7
217 }
218 result.ErrorMessage = chunkAck.ErrorMessage
219 result.Timestamps.TeardownDone = time.Now().UTC()
220 e.calculateDurations(&result)
221 return &result, fmt.Errorf("%s", chunkAck.ErrorMessage)
222 }
223
224 lastMsgType = msgType
225 lastAckData = ackData
226 }
227 _ = lastMsgType
228
229 result.Timestamps.AckReceived = time.Now().UTC()
230 result.Timestamps.TeardownDone = time.Now().UTC()
231
232 _ = conn.Close(websocket.StatusNormalClosure, "exfil complete")
233
234 // Parse and strictly verify cryptographic verification ACK
235 var ack common.VerificationACK
236 if err := json.Unmarshal(lastAckData, &ack); err != nil {
237 result.Verdict = common.VerdictBlockedL7
238 result.ErrorMessage = fmt.Sprintf("failed to parse websocket verification ACK: %v", err)
239 e.calculateDurations(&result)
240 return &result, fmt.Errorf("invalid verification ACK: %w", err)
241 }
242
243 verdict, verifyErr := ack.Verify(opts.ExpectedHash, int(totalChunks))
244 result.Verdict = verdict
245 if verifyErr != nil {
246 result.ErrorMessage = verifyErr.Error()
247 e.calculateDurations(&result)
248 return &result, verifyErr
249 }
250
251 result.RawEvidence = common.FormatBoundedRawEvidence(headEvidence)
252 e.calculateDurations(&result)
253 return &result, nil
254}
255
256func (e *WebSocketExfiltrator) calculateDurations(r *common.ProbeResult) {
257 t := &r.Timestamps
258 if t.TeardownDone.IsZero() {
259 t.TeardownDone = time.Now().UTC()
260 }
261 t.ComputeDurations()
262 if t.TransferDuration > 0 {
263 r.ThroughputBps = float64(r.BytesSent*8) / (float64(t.TransferDuration) / float64(time.Second)) / 1000.0
264 }
265}