diff --git a/artifacts/definitions/Server/Utils/CreateCollector.yaml b/artifacts/definitions/Server/Utils/CreateCollector.yaml index bff11bd9eef..5730abae27b 100644 --- a/artifacts/definitions/Server/Utils/CreateCollector.yaml +++ b/artifacts/definitions/Server/Utils/CreateCollector.yaml @@ -66,6 +66,7 @@ parameters: - ZIP - GCS - S3 + - SFTP - name: target_args description: Type Dependent args @@ -143,6 +144,19 @@ parameters: name=name, credentials=TargetArgs.GCSKey) + - name: SFTPCollection + type: hidden + default : | + LET upload_file(filename, name, accessor) = upload_sftp( + file=filename, + accessor=accessor, + name=name, + user=TargetArgs.user, + path=TargetArgs.path, + privatekey=TargetArgs.privatekey, + endpoint=TargetArgs.endpoint, + hostkey = TargetArgs.hostkey) + - name: CloudCollection type: hidden default: | @@ -294,7 +308,8 @@ sources: a = { SELECT StandardCollection AS Value FROM scope() WHERE target = "ZIP" }, b = { SELECT S3Collection + CloudCollection AS Value FROM scope() WHERE target = "S3" }, c = { SELECT GCSCollection + CloudCollection AS Value FROM scope() WHERE target = "GCS" }, - d = { SELECT "" AS Value FROM scope() WHERE log(message="Unknown collection type " + target) } + d = { SELECT SFTPCollection + CloudCollection AS Value FROM scope() WHERE target = "SFTP" }, + e = { SELECT "" AS Value FROM scope() WHERE log(message="Unknown collection type " + target) } ) LET definitions <= SELECT * FROM chain( diff --git a/go.mod b/go.mod index cbd7c59ad8b..2b2d53bca64 100644 --- a/go.mod +++ b/go.mod @@ -74,6 +74,7 @@ require ( github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect github.com/olekukonko/tablewriter v0.0.4 github.com/pkg/errors v0.9.1 + github.com/pkg/sftp v1.12.0 // indirect github.com/pkg/term v0.0.0-20190109203006-aa71e9d9e942 // indirect github.com/pquerna/cachecontrol v0.0.0-20200921180117-858c6e7e6b7e // indirect github.com/processout/grpc-go-pool v1.2.1 diff --git a/go.sum b/go.sum index d83220c3c4e..e963615d9f9 100644 --- a/go.sum +++ b/go.sum @@ -303,6 +303,8 @@ github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+o github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.3 h1:CE8S1cTafDpPvMhIxNJKvHsGVBgn1xWYf1NbHQhywc8= github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8= +github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.0 h1:s5hAObm+yFO5uHYt5dYjxi2rXrsnmRpJx4OYvIWUaQs= @@ -369,6 +371,8 @@ github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/sftp v1.12.0 h1:/f3b24xrDhkhddlaobPe2JgBqfdt+gC/NYl0QY9IOuI= +github.com/pkg/sftp v1.12.0/go.mod h1:fUqqXB5vEgVCZ131L+9say31RAri6aF6KDViawhxKK8= github.com/pkg/term v0.0.0-20190109203006-aa71e9d9e942 h1:A7GG7zcGjl3jqAqGPmcNjd/D9hzL95SuoOQAaFNdLU0= github.com/pkg/term v0.0.0-20190109203006-aa71e9d9e942/go.mod h1:eCbImbZ95eXtAUIbLAuAVnBnwf83mjf6QIVH8SHYwqQ= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= diff --git a/gui/velociraptor/src/components/flows/offline-collector.js b/gui/velociraptor/src/components/flows/offline-collector.js index 68e41a5fd44..28e24dad1cd 100644 --- a/gui/velociraptor/src/components/flows/offline-collector.js +++ b/gui/velociraptor/src/components/flows/offline-collector.js @@ -83,6 +83,7 @@ class OfflineCollectorParameters extends React.Component { + @@ -185,7 +186,100 @@ class OfflineCollectorParameters extends React.Component { /> + + + Skip Cert Verification + + { + this.props.parameters.target_args.noverifycert = e.target.value; + this.props.setParameters(this.props.parameters); + }} + > + + + + + + + + + + } + + { this.props.parameters.target === "SFTP" && <> + + Upload Path + + { + this.props.parameters.target_args.path = e.target.value; + this.props.setParameters(this.props.parameters); + }} + /> + + + + + Private Key + + { + this.props.parameters.target_args.privatekey = e.target.value; + this.props.setParameters(this.props.parameters); + }} + /> + + + + + User + + { + this.props.parameters.target_args.user = e.target.value; + this.props.setParameters(this.props.parameters); + }} + /> + + + + + Endpoint + + { + this.props.parameters.target_args.endpoint = e.target.value; + this.props.setParameters(this.props.parameters); + }} + /> + + + + + Host Key + + { + this.props.parameters.target_args.hostkey = e.target.value; + this.props.setParameters(this.props.parameters); + }} + /> + + + } diff --git a/vql/tools/s3_upload.go b/vql/tools/s3_upload.go index 921b96db0bc..41b89450f5b 100644 --- a/vql/tools/s3_upload.go +++ b/vql/tools/s3_upload.go @@ -82,7 +82,12 @@ func (self *S3UploadFunction) Call(ctx context.Context, upload_response, err := upload_S3( sub_ctx, scope, file, arg.Bucket, - arg.Name, arg.CredentialsKey, arg.CredentialsSecret, arg.Region, arg.Endpoint, arg.NoVerifyCert) + arg.Name, + arg.CredentialsKey, + arg.CredentialsSecret, + arg.Region, + arg.Endpoint, + arg.NoVerifyCert) if err != nil { scope.Log("upload_S3: %v", err) // Relay the error in the UploadResponse @@ -97,7 +102,11 @@ func (self *S3UploadFunction) Call(ctx context.Context, func upload_S3(ctx context.Context, scope *vfilter.Scope, reader glob.ReadSeekCloser, bucket, name string, - credentialsKey string, credentialsSecret string, region string, endpoint string, NoVerifyCert bool) ( + credentialsKey string, + credentialsSecret string, + region string, + endpoint string, + NoVerifyCert bool) ( *api.UploadResponse, error) { scope.Log("upload_S3: Uploading %v to %v", name, bucket) diff --git a/vql/tools/sftp_upload.go b/vql/tools/sftp_upload.go new file mode 100644 index 00000000000..333aa2f1bfd --- /dev/null +++ b/vql/tools/sftp_upload.go @@ -0,0 +1,223 @@ +//+build extras + +package tools + +import ( + "encoding/base64" + "errors" + "fmt" + "io" + "net" + "path/filepath" + + "github.com/Velocidex/ordereddict" + "github.com/pkg/sftp" + "golang.org/x/crypto/ssh" + "golang.org/x/net/context" + "www.velocidex.com/golang/velociraptor/file_store/api" + "www.velocidex.com/golang/velociraptor/glob" + vql_subsystem "www.velocidex.com/golang/velociraptor/vql" + "www.velocidex.com/golang/vfilter" +) + +type SFTPUploadArgs struct { + File string `vfilter:"required,field=file,doc=The file to upload"` + Name string `vfilter:"optional,field=name,doc=The name of the file that should be stored on the server"` + User string `vfilter:"required,field=user,doc=The username to connect to the endpoint with"` + Path string `vfilter:"required,field=path,doc=Path on server to upload file to"` + Accessor string `vfilter:"optional,field=accessor,doc=The accessor to use"` + PrivateKey string `vfilter:"required,field=privatekey,doc=The private key to use"` + Endpoint string `vfilter:"required,field=endpoint,doc=The Endpoint to use"` + HostKey string `vfilter:"optional,field=hostkey,doc=Host key to verify. Blank to disable"` +} + +type SFTPUploadFunction struct{} + +func (self *SFTPUploadFunction) Call(ctx context.Context, + scope *vfilter.Scope, + args *ordereddict.Dict) vfilter.Any { + + arg := &SFTPUploadArgs{} + err := vfilter.ExtractArgs(scope, args, arg) + if err != nil { + scope.Log("upload_sftp: %s", err.Error()) + return vfilter.Null{} + } + + err = vql_subsystem.CheckFilesystemAccess(scope, arg.Accessor) + if err != nil { + scope.Log("upload_SFTP: %s", err) + return vfilter.Null{} + } + + accessor, err := glob.GetAccessor(arg.Accessor, scope) + if err != nil { + scope.Log("upload_SFTP: %v", err) + return vfilter.Null{} + } + + file, err := accessor.Open(arg.File) + if err != nil { + scope.Log("upload_SFTP: Unable to open %s: %s", + arg.File, err.Error()) + return &vfilter.Null{} + } + defer file.Close() + + if arg.Name == "" { + arg.Name = arg.File + } + + stat, err := file.Stat() + if err != nil { + scope.Log("upload_SFTP: Unable to stat %s: %v", + arg.File, err) + } else if !stat.IsDir() { + // Abort uploading when the scope is destroyed. + sub_ctx, cancel := context.WithCancel(ctx) + scope.AddDestructor(func() { + cancel() + }) + + upload_response, err := upload_SFTP( + sub_ctx, scope, file, + arg.User, + arg.Path, + arg.Name, + arg.PrivateKey, + arg.Endpoint, + arg.HostKey) + if err != nil { + scope.Log("upload_SFTP: %v", err) + // Relay the error in the UploadResponse + return upload_response + } + return upload_response + } + + return vfilter.Null{} +} + +func keyString(k ssh.PublicKey) string { + return k.Type() + " " + base64.StdEncoding.EncodeToString(k.Marshal()) +} + +func hostkeycallback(trustedkey string) ssh.HostKeyCallback { + return func(_ string, _ net.Addr, k ssh.PublicKey) error { + ks := keyString(k) + if trustedkey != ks { + return fmt.Errorf("SSH-key verification: expected %s but got %s", trustedkey, ks) + } + return nil + } +} + +func getSFTPClient(scope *vfilter.Scope, user string, privateKey string, endpoint string, hostKey string) (*sftp.Client, error) { + cacheKey := fmt.Sprintf("%s %s", user, endpoint) + client := vql_subsystem.CacheGet(scope, cacheKey) + if client == nil { + signer, err := ssh.ParsePrivateKey([]byte(privateKey)) + if err != nil { + vql_subsystem.CacheSet(scope, cacheKey, err) + return nil, err + } + var clientConfig *ssh.ClientConfig + if hostKey == "" { + clientConfig = &ssh.ClientConfig{ + User: user, + Auth: []ssh.AuthMethod{ + ssh.PublicKeys(signer), + }, + HostKeyCallback: ssh.InsecureIgnoreHostKey(), + } + } else { + clientConfig = &ssh.ClientConfig{ + User: user, + Auth: []ssh.AuthMethod{ + ssh.PublicKeys(signer), + }, + HostKeyCallback: hostkeycallback(hostKey), + } + } + + conn, err := ssh.Dial("tcp", endpoint, clientConfig) + if err != nil { + vql_subsystem.CacheSet(scope, cacheKey, err) + return nil, err + } + client, err := sftp.NewClient(conn) + if err != nil { + vql_subsystem.CacheSet(scope, cacheKey, err) + return nil, err + } + vql_subsystem.AddGlobalDestructor(scope, func() { + conn.Close() + client.Close() + }) + vql_subsystem.CacheSet(scope, cacheKey, client) + return client, nil + } + switch t := client.(type) { + case error: + return nil, t + case *sftp.Client: + return t, nil + default: + return nil, errors.New("Error") + } +} + +func upload_SFTP(ctx context.Context, scope *vfilter.Scope, + reader io.Reader, + user, path, name string, + privateKey string, endpoint string, hostKey string) ( + *api.UploadResponse, error) { + + scope.Log("upload_SFTP: Uploading %v to %v", name, endpoint) + client, err := getSFTPClient(scope, user, privateKey, endpoint, hostKey) + if err != nil { + return &api.UploadResponse{ + Error: err.Error(), + }, err + } + + fpath := filepath.Join(path, name) + file, err := client.Create(fpath) + if err != nil { + return &api.UploadResponse{ + Error: err.Error(), + }, err + } + defer file.Close() + if _, err := file.ReadFrom(reader); err != nil { + return &api.UploadResponse{ + Error: err.Error(), + }, err + } + + check, err := client.Lstat(fpath) + if err != nil { + return &api.UploadResponse{ + Error: err.Error(), + }, err + } + + response := &api.UploadResponse{ + Path: fpath, + Size: uint64(check.Size()), + } + return response, nil +} + +func (self SFTPUploadFunction) Info( + scope *vfilter.Scope, type_map *vfilter.TypeMap) *vfilter.FunctionInfo { + return &vfilter.FunctionInfo{ + Name: "upload_sftp", + Doc: "Upload files to SFTP.", + ArgType: type_map.AddType(scope, &SFTPUploadArgs{}), + } +} + +func init() { + vql_subsystem.RegisterFunction(&SFTPUploadFunction{}) +}