Skip to content

Commit a87abc6

Browse files
authored
Merge pull request #29 from /issues/28
simulator: add ssh-transfer and ssh-exfil modules
2 parents 07c9f16 + 4970239 commit a87abc6

File tree

16 files changed

+1595
-16
lines changed

16 files changed

+1595
-16
lines changed

cmd/run/run.go

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,9 @@ type Module struct {
140140
Timeout time.Duration
141141
// FailMsg string
142142
SuccessMsg string
143+
// False by default. If true, don't wait until Timeout between simulation
144+
// runs of this module.
145+
Fast bool
143146
}
144147

145148
func (m *Module) FormatHost(host string) string {
@@ -149,7 +152,10 @@ func (m *Module) FormatHost(host string) string {
149152
host = h
150153
}
151154
}
152-
155+
// Check if the simulator module implements the HostMsgFormatter interface.
156+
if hostMsgFormatter, ok := m.Module.(simulator.HostMsgFormatter); ok {
157+
return hostMsgFormatter.HostMsg(host)
158+
}
153159
f := m.HostMsg
154160
if f == "" {
155161
switch m.Pipeline {
@@ -194,7 +200,7 @@ var allModules = []Module{
194200
// Pipeline: PipelineDNS,
195201
// NumOfHosts: 1,
196202
// HeaderMsg: "",
197-
// HostMsg: "Resolving %s via ns1.sandbox.alphasoc.xyz",
203+
// HostMsg: "Resolving %s via dns.sandbox-services.alphasoc.xyz",
198204
// Timeout: 1 * time.Second,
199205
// // FailMsg: "Test failed (queries to arbitrary DNS servers are blocked)",
200206
// SuccessMsg: "Success! DNS hijacking is possible in this environment",
@@ -277,6 +283,24 @@ var allModules = []Module{
277283
HeaderMsg: "Resolving random imposter domains",
278284
Timeout: 1 * time.Second,
279285
},
286+
Module{
287+
Module: simulator.NewSSHTransfer(),
288+
Name: "ssh-transfer",
289+
Pipeline: PipelineIP,
290+
NumOfHosts: 1,
291+
HeaderMsg: "Preparing to send randomly generated data to a standard SSH port",
292+
Timeout: 5 * time.Minute,
293+
Fast: true,
294+
},
295+
Module{
296+
Module: simulator.NewSSHExfil(),
297+
Name: "ssh-exfil",
298+
Pipeline: PipelineIP,
299+
NumOfHosts: 1,
300+
HeaderMsg: "Preparing to send randomly generated data to a non-standard SSH port",
301+
Timeout: 5 * time.Minute,
302+
Fast: true,
303+
},
280304
}
281305

282306
type Simulation struct {
@@ -344,8 +368,9 @@ func run(sims []*Simulation, extIP net.IP, size int) error {
344368
okHosts++
345369
}
346370

347-
// wait until context expires (unless fast mode or very last iteration)
348-
if !fast && ((simN < len(sims)-1) || (hostN < len(hosts)-1)) {
371+
// Wait until context expires, unless fast global mode,
372+
// fast module (default false) or very last iteration.
373+
if !(fast || sim.Fast) && ((simN < len(sims)-1) || (hostN < len(hosts)-1)) {
349374
<-ctx.Done()
350375
}
351376

go.mod

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,9 @@ module github.com/alphasoc/flightsim
33
go 1.13
44

55
require (
6-
github.com/cretz/bine v0.1.1-0.20191105223159-1c71414a61dc
7-
github.com/pkg/errors v0.8.1-0.20171018195549-f15c970de5b7
8-
github.com/stretchr/testify v1.4.0 // indirect
9-
golang.org/x/crypto v0.0.0-20191122220453-ac88ee75c92c // indirect
10-
golang.org/x/net v0.0.0-20191126235420-ef20fe5d7933
11-
golang.org/x/sys v0.0.0-20191128015809-6d18c012aee9 // indirect
6+
github.com/cretz/bine v0.2.0
7+
github.com/inhies/go-bytesize v0.0.0-20201103132853-d0aed0d254f8 // indirect
8+
github.com/pkg/errors v0.8.1
9+
golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e // indirect
10+
golang.org/x/net v0.0.0-20210525063256-abc453219eb5
1211
)

go.sum

Lines changed: 489 additions & 0 deletions
Large diffs are not rendered by default.

main.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,10 @@ Available commands:
2424
version Prints the version number
2525
2626
Cheatsheet:
27-
flightsim run Run all the modules
28-
flightsim run c2 Simulate C2 traffic
29-
flightsim run c2:trickbot Simulate C2 traffic for the TrickBot family
27+
flightsim run Run all the modules
28+
flightsim run c2 Simulate C2 traffic
29+
flightsim run c2:trickbot Simulate C2 traffic for the TrickBot family
30+
flightsim run ssh-transfer:1MB Simulate a 1MB SSH/SFTP file transfer
3031
`
3132

3233
func main() {

simulator/hijack.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ func (*Hijack) Simulate(ctx context.Context, extIP net.IP, host string) error {
2323
r := &net.Resolver{
2424
PreferGo: true,
2525
Dial: func(ctx context.Context, network, address string) (net.Conn, error) {
26-
return d.DialContext(ctx, "udp", "ns1.sandbox.alphasoc.xyz:53")
26+
return d.DialContext(ctx, "udp", "dns.sandbox-services.alphasoc.xyz:53")
2727
},
2828
}
2929

simulator/simulator.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,12 @@ import (
99
"github.com/alphasoc/flightsim/utils"
1010
)
1111

12+
// HostMsgFormatter allows a simulator to implement a custom HostMsg method to be called in
13+
// place of parsing the Module.HostMsg field.
14+
type HostMsgFormatter interface {
15+
HostMsg(host string) string
16+
}
17+
1218
type Simulator interface {
1319
Init(bind net.IP) error
1420
Simulate(ctx context.Context, host string) error

simulator/ssh-exfil.go

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
package simulator
2+
3+
import (
4+
"fmt"
5+
"math/rand"
6+
7+
simssh "github.com/alphasoc/flightsim/simulator/ssh"
8+
bytesize "github.com/inhies/go-bytesize"
9+
)
10+
11+
// SSHExfil defines this simulation. It's little more than a wrapper around SSHTransfer.
12+
type SSHExfil struct {
13+
SSHTransfer
14+
}
15+
16+
// NewSSHExfil creates a new SSH Exfiltration simulator.
17+
func NewSSHExfil() *SSHExfil {
18+
return &SSHExfil{}
19+
}
20+
21+
// defualtTargetHosts returns a default string slice of targets in the {HOST:IP} form.
22+
// Random selection of a non-standard SSH port is performed.
23+
func (s *SSHExfil) defaultTargetHosts() []string {
24+
// Ports to be used for ssh exfil detectability.
25+
ports := []string{"443", "465", "993", "995"}
26+
pos := rand.Intn(len(ports))
27+
return []string{fmt.Sprintf("ssh.sandbox-services.alphasoc.xyz:%v", ports[pos])}
28+
}
29+
30+
// defaultSendSize returns a 100 bytesize.MB default.
31+
func (s *SSHExfil) defaultSendSize() bytesize.ByteSize {
32+
return 100 * bytesize.MB
33+
}
34+
35+
// Hosts sets the simulation send size, and extracts the destination hosts. A slice of
36+
// strings representing the destination hosts (IP:port) is returned along with an error.
37+
func (s *SSHExfil) Hosts(scope string, size int) ([]string, error) {
38+
dstHosts, sendSize, err := simssh.ParseScope(scope, s.defaultTargetHosts(), s.defaultSendSize())
39+
if err != nil {
40+
return dstHosts, err
41+
}
42+
s.sendSize = sendSize
43+
return dstHosts, nil
44+
}

simulator/ssh-transfer.go

Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
package simulator
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"net"
7+
"strings"
8+
9+
simssh "github.com/alphasoc/flightsim/simulator/ssh"
10+
"github.com/alphasoc/flightsim/utils"
11+
"golang.org/x/crypto/ssh"
12+
13+
bytesize "github.com/inhies/go-bytesize"
14+
)
15+
16+
// SSHTransfer defines this simulation.
17+
type SSHTransfer struct {
18+
src net.IP // Connect from this IP.
19+
sendSize bytesize.ByteSize
20+
}
21+
22+
// Client connection results struct.
23+
type clientConnRes struct {
24+
c *simssh.Client
25+
err error
26+
}
27+
28+
// NewSSHTransfer creates a new SSH/SFTP simulator.
29+
func NewSSHTransfer() *SSHTransfer {
30+
return &SSHTransfer{}
31+
}
32+
33+
// defaultSendSize returns a 100 bytesize.MB default.
34+
func (s *SSHTransfer) defaultSendSize() bytesize.ByteSize {
35+
return 100 * bytesize.MB
36+
}
37+
38+
// defualtTargetHosts returns a default string slice of targets in the {HOST:IP} form.
39+
func (s *SSHTransfer) defaultTargetHosts() []string {
40+
return []string{"ssh.sandbox-services.alphasoc.xyz:22"}
41+
}
42+
43+
// HostMsg implements the HostMsgFormatter interface, returning a custom host message
44+
// string to be output by the run command.
45+
func (s *SSHTransfer) HostMsg(host string) string {
46+
return fmt.Sprintf(
47+
"Simulating an SSH/SFTP file transfer of %v (%v) to %v",
48+
s.sendSize.Format("%.0f", "B", false),
49+
s.sendSize.Format("%.2f", "", false),
50+
host)
51+
}
52+
53+
// Init sets the source IP for this simulation.
54+
func (s *SSHTransfer) Init(src net.IP) error {
55+
s.src = src
56+
return nil
57+
}
58+
59+
// newClient initializes and returns SSH/SFTP Client along with an error.
60+
func newClient(
61+
ctx context.Context,
62+
clientName string,
63+
src net.IP,
64+
dst string,
65+
signer ssh.Signer) (*simssh.Client, error) {
66+
// Create a Client that's ready to use for SSH/SFTP transfers.
67+
c, err := simssh.NewClient(ctx, clientName, src, dst, signer)
68+
if err != nil {
69+
// No need to invoke client Teardown(), as underlying connections would have been
70+
// closed by NewClient().
71+
return nil, err
72+
}
73+
// Init/Version.
74+
initResp, err := c.SendInit()
75+
if err != nil {
76+
c.Teardown()
77+
return c, err
78+
}
79+
// TODO: Do we really care about version mismatches? From the sftp spec, a 3 can be
80+
// followed by some form of version negotiaion.
81+
if initResp.Version != simssh.ClientVer {
82+
c.Teardown()
83+
return c, fmt.Errorf("server version mismatch, expecting %v, received %v",
84+
simssh.ClientVer, initResp.Version)
85+
}
86+
return c, nil
87+
}
88+
89+
type simulationContext struct {
90+
Ctx context.Context
91+
Dst string
92+
ClientName string
93+
Handle string
94+
SendSize bytesize.ByteSize
95+
Signer ssh.Signer
96+
Ch chan<- simssh.WriteResponse
97+
}
98+
99+
// simulate performs the actual client connect and write on behalf of Simulate().
100+
func (s *SSHTransfer) simulate(simCtx *simulationContext) {
101+
c, err := newClient(simCtx.Ctx, simCtx.ClientName, s.src, simCtx.Dst, simCtx.Signer)
102+
if err != nil {
103+
// Piggy back client connect errors on WriteResponse chan.
104+
res := simssh.WriteResponse{}
105+
res.ClientName = simCtx.ClientName
106+
res.Err = err
107+
simCtx.Ch <- res
108+
return
109+
}
110+
simCtx.Ch <- c.WriteRandom(simCtx.Handle, simCtx.SendSize)
111+
c.Teardown()
112+
}
113+
114+
// Simulate an ssh/sftp file transfer.
115+
func (s *SSHTransfer) Simulate(ctx context.Context, dst string) error {
116+
// Auth.
117+
signer, err := simssh.NewSignerFromKey()
118+
if err != nil {
119+
return err
120+
}
121+
// Compute number of clients and a send size for each, such that we don't exceed
122+
// maxClients.
123+
const maxClients = 2
124+
const minSendSize = 1 * bytesize.MB
125+
if s.sendSize <= 0 {
126+
return fmt.Errorf("invalid send size: %v", s.sendSize)
127+
}
128+
senderSizes := utils.ComputeSenderSizes(maxClients, s.sendSize, minSendSize)
129+
numClients := len(senderSizes)
130+
// Create a WriteResponse channel, used by clients to return connection errors and
131+
// write responses.
132+
writeCh := make(chan simssh.WriteResponse, numClients)
133+
for i := 0; i < numClients; i++ {
134+
go s.simulate(&simulationContext{
135+
Ctx: ctx,
136+
Dst: dst,
137+
ClientName: fmt.Sprintf("alphasoc-%v", i),
138+
Handle: fmt.Sprintf("flightsim-ssh-transfer-%v", i),
139+
SendSize: senderSizes[i],
140+
Signer: signer,
141+
Ch: writeCh})
142+
143+
}
144+
var errsEncountered []string
145+
var totalBytesSent bytesize.ByteSize
146+
for i := 0; i < numClients; i++ {
147+
res := <-writeCh
148+
// Append all client connect and write errors, but continue.
149+
if res.Err != nil {
150+
errsEncountered = append(errsEncountered, fmt.Sprintf("client %v: %v", res.ClientName, res.Err.Error()))
151+
}
152+
totalBytesSent += bytesize.ByteSize(res.BytesSent)
153+
}
154+
if len(errsEncountered) != 0 {
155+
// Don't append ':" to leading '%v' as composed err already has trailing ':'.
156+
return fmt.Errorf(
157+
"[%v (%v) successfully transferred] Errors encountered:\n\t%v",
158+
totalBytesSent.Format("%.0f", "B", false),
159+
totalBytesSent.Format("%.2f", "", false),
160+
strings.Join(errsEncountered, "\n\t"),
161+
)
162+
}
163+
// Success.
164+
return nil
165+
}
166+
167+
// Cleanup is a no-op.
168+
func (s *SSHTransfer) Cleanup() {}
169+
170+
// Hosts sets the simulation send size, and extracts the destination hosts. A slice of
171+
// strings representing the destination hosts (IP:port) is returned along with an error.
172+
func (s *SSHTransfer) Hosts(scope string, size int) ([]string, error) {
173+
dstHosts, sendSize, err := simssh.ParseScope(scope, s.defaultTargetHosts(), s.defaultSendSize())
174+
if err != nil {
175+
return dstHosts, err
176+
}
177+
s.sendSize = sendSize
178+
return dstHosts, nil
179+
}

simulator/ssh/auth.go

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
package ssh
2+
3+
import (
4+
"fmt"
5+
6+
"golang.org/x/crypto/ssh"
7+
)
8+
9+
// The private key here is solely used to authenticate with the server-side application
10+
// accpeting the SSH/SFTP data transfer. This is being done on purpose.
11+
const privKey = `-----BEGIN OPENSSH PRIVATE KEY-----
12+
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
13+
QyNTUxOQAAACA6El6xrgg3fzl6dbygysFLXmVN3ysbLnbnkD8jpgOAxQAAAJiL5q0ti+at
14+
LQAAAAtzc2gtZWQyNTUxOQAAACA6El6xrgg3fzl6dbygysFLXmVN3ysbLnbnkD8jpgOAxQ
15+
AAAED/EajTMrrGDzvT2VVeQhF/pf+mE9zINK7Kv3tHRynbpDoSXrGuCDd/OXp1vKDKwUte
16+
ZU3fKxsudueQPyOmA4DFAAAAEGthcm9sQGVhc3kubG9jYWwBAgMEBQ==
17+
-----END OPENSSH PRIVATE KEY-----`
18+
19+
// gSigner is the ssh signer used to authenticate. It's global to this package to avoid
20+
// computing it multiple times during a simulation
21+
var gSigner ssh.Signer = nil
22+
23+
// NewSignerFromKey returns the global signer, if set, otherwise it generates an ssh signer
24+
// from a static private key, sets the global signer varialbe, and returns the signer. An
25+
// error code is also returned.
26+
func NewSignerFromKey() (ssh.Signer, error) {
27+
if gSigner != nil {
28+
return gSigner, nil
29+
}
30+
key := []byte(privKey)
31+
parsedKey, err := ssh.ParseRawPrivateKey(key)
32+
if err != nil {
33+
return nil, fmt.Errorf("unable to parse private key: %w", err)
34+
}
35+
signer, err := ssh.NewSignerFromKey(parsedKey)
36+
if err != nil {
37+
return nil, fmt.Errorf("unable to generate signer from private key: %w", err)
38+
}
39+
gSigner = signer
40+
return gSigner, nil
41+
}

0 commit comments

Comments
 (0)