ssh: custom shell definition & client version spoof

This commit is contained in:
Pavel 2023-11-20 22:23:58 +03:00
parent fc7195a274
commit a3a56bb795
6 changed files with 79 additions and 15 deletions

View File

@ -38,7 +38,11 @@ servers:
# Disables PTY request for this server. # Disables PTY request for this server.
nopty: true nopty: true
# Requests interactive shell for SSH sessions. Should be `true` for the `commands`. # Requests interactive shell for SSH sessions. Should be `true` for the `commands`.
# You can also pass a string with shell binary, for example, "/bin/sh".
# Note: commands will be executed using provided shell binary.
shell: false shell: false
# Spoof client version with provided (value below is taken directly from OpenSSH). This value must be compliant with RFC-4253.
client_version: "SSH-2.0-OpenSSH_9.5"
# Authentication data. # Authentication data.
auth: auth:
# Authentication type. Supported types: key, password, passwordless # Authentication type. Supported types: key, password, passwordless
@ -84,7 +88,7 @@ servers:
forward_port: 80 forward_port: 80
fake_remote_host: true fake_remote_host: true
nopty: false nopty: false
shell: true shell: "/usr/bin/bash"
mode: single mode: single
keepalive: keepalive:
interval: 1 interval: 1

View File

@ -29,6 +29,7 @@ type SSH struct {
auth []ssh.AuthMethod auth []ssh.AuthMethod
hostKeys []ssh.PublicKey hostKeys []ssh.PublicKey
conns map[string]conn conns map[string]conn
clientVersion string
rw sync.RWMutex rw sync.RWMutex
wg sync.WaitGroup wg sync.WaitGroup
domainRegExp *regexp.Regexp domainRegExp *regexp.Regexp
@ -58,6 +59,7 @@ func New(ctx context.Context, name string, params config.DriverParams) (base.Dri
drv.domainRegExp = matcher drv.domainRegExp = matcher
drv.populateFromSSHConfig() drv.populateFromSSHConfig()
drv.auth = drv.authenticators() drv.auth = drv.authenticators()
drv.clientVersion = drv.buildClientVersion()
return drv, nil return drv, nil
} }
@ -69,7 +71,8 @@ func (d *SSH) forward(val sshtun.Forward, domainMatcher func(string)) conn {
Forward: val, Forward: val,
HostKeys: d.hostKeys, HostKeys: d.hostKeys,
NoPTY: d.params.NoPTY, NoPTY: d.params.NoPTY,
Shell: d.params.Shell, Shell: sshtun.BoolOrStr(d.params.Shell),
ClientVersion: d.clientVersion,
FakeRemoteHost: d.params.FakeRemoteHost, FakeRemoteHost: d.params.FakeRemoteHost,
KeepAliveInterval: uint(d.params.KeepAlive.Interval), KeepAliveInterval: uint(d.params.KeepAlive.Interval),
KeepAliveMax: uint(d.params.KeepAlive.MaxAttempts), KeepAliveMax: uint(d.params.KeepAlive.MaxAttempts),
@ -88,6 +91,23 @@ func (d *SSH) forward(val sshtun.Forward, domainMatcher func(string)) conn {
return conn{ctx: ctx, cancel: cancel, tun: tun} return conn{ctx: ctx, cancel: cancel, tun: tun}
} }
func (d *SSH) buildClientVersion() string {
ver := strings.TrimSpace(d.params.ClientVersion)
if ver == "" {
return ""
}
if !strings.HasPrefix(ver, "SSH-2.0-") {
d.Log().Warn(
"client_version must have 'SSH-2.0-' prefix (see RFC-4253), this will be fixed automatically")
ver = "SSH-2.0-" + ver
}
if !isValidClientVersion(ver) {
d.Log().Warnf("invalid client_version value, using default...")
return ""
}
return ver
}
func (d *SSH) buildHostKeys() error { func (d *SSH) buildHostKeys() error {
if d.params.HostKeys == "" { if d.params.HostKeys == "" {
return nil return nil

View File

@ -18,7 +18,8 @@ type Params struct {
Mode types.DomainMode `mapstructure:"mode" validate:"required,oneof=single multi"` Mode types.DomainMode `mapstructure:"mode" validate:"required,oneof=single multi"`
FakeRemoteHost bool `mapstructure:"fake_remote_host"` FakeRemoteHost bool `mapstructure:"fake_remote_host"`
NoPTY bool `mapstructure:"nopty"` NoPTY bool `mapstructure:"nopty"`
Shell bool `mapstructure:"shell"` Shell string `mapstructure:"shell"`
ClientVersion string `mapstructure:"client_version"`
Commands types.Commands `mapstructure:"commands"` Commands types.Commands `mapstructure:"commands"`
} }

View File

@ -29,12 +29,27 @@ type TunnelConfig struct {
Forward Forward Forward Forward
HostKeys []ssh.PublicKey HostKeys []ssh.PublicKey
NoPTY bool NoPTY bool
Shell bool Shell BoolOrStr
ClientVersion string
FakeRemoteHost bool FakeRemoteHost bool
KeepAliveInterval uint KeepAliveInterval uint
KeepAliveMax uint KeepAliveMax uint
} }
type BoolOrStr string
func (b BoolOrStr) IsBool() bool {
return b == "true" || b == "false"
}
func (b BoolOrStr) Falsy() bool {
return b == "" || b == "false"
}
func (b BoolOrStr) String() string {
return string(b)
}
func New(address, user string, auth []ssh.AuthMethod, sc TunnelConfig, log *zap.SugaredLogger) *Tunnel { func New(address, user string, auth []ssh.AuthMethod, sc TunnelConfig, log *zap.SugaredLogger) *Tunnel {
return &Tunnel{ return &Tunnel{
address: AddrToEndpoint(address), address: AddrToEndpoint(address),
@ -88,6 +103,7 @@ func (t *Tunnel) connect(ctx context.Context, bannerCb ssh.BannerCallback, sessi
Auth: t.authMethods, Auth: t.authMethods,
HostKeyCallback: t.buildHostKeyCallback(), HostKeyCallback: t.buildHostKeyCallback(),
BannerCallback: bannerCb, BannerCallback: bannerCb,
ClientVersion: t.tunConfig.ClientVersion,
} }
var sshClient *ssh.Client var sshClient *ssh.Client
@ -125,9 +141,15 @@ func (t *Tunnel) connect(ctx context.Context, bannerCb ssh.BannerCallback, sessi
t.log.Warnf("PTY allocation failed: %s", err.Error()) t.log.Warnf("PTY allocation failed: %s", err.Error())
} }
} }
if t.tunConfig.Shell { if !t.tunConfig.Shell.Falsy() {
if t.tunConfig.Shell.IsBool() {
if err := sess.Shell(); err != nil { if err := sess.Shell(); err != nil {
t.log.Warnf("failed to start shell: %s", err.Error()) t.log.Warnf("failed to start empty shell: %s", err.Error())
}
} else {
if err := sess.Start(t.tunConfig.Shell.String()); err != nil {
t.log.Warnf("failed to start shell '%s': %s", t.tunConfig.Shell, err.Error())
}
} }
wg.Add(1) wg.Add(1)
go func() { go func() {

View File

@ -0,0 +1,10 @@
package ssh
import "regexp"
var clientVersionVerifier = regexp.MustCompile(`^[a-zA-Z0-9\.\-\_]+\x{20}?[a-zA-Z0-9\.\-\_]+?$`)
// isValidClientVersion returns true if provided SSH client version string is compliant with RFC-4253.
func isValidClientVersion(ver string) bool {
return clientVersionVerifier.MatchString(ver)
}

View File

@ -10,7 +10,14 @@ type ValidationAvailable interface {
} }
func UnmarshalParams(params config.DriverParams, target ValidationAvailable) error { func UnmarshalParams(params config.DriverParams, target ValidationAvailable) error {
if err := mapstructure.Decode(params, target); err != nil { dec, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{
Result: target,
WeaklyTypedInput: true,
})
if err != nil {
return err
}
if err := dec.Decode(params); err != nil {
return err return err
} }
if val, canValidate := target.(ValidationAvailable); canValidate { if val, canValidate := target.(ValidationAvailable); canValidate {