ssh: custom shell definition & client version spoof
This commit is contained in:
parent
fc7195a274
commit
a3a56bb795
@ -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
|
||||||
|
@ -25,13 +25,14 @@ var ErrAlreadyInUse = errors.New("domain is already in use")
|
|||||||
|
|
||||||
type SSH struct {
|
type SSH struct {
|
||||||
base.Base
|
base.Base
|
||||||
params Params
|
params Params
|
||||||
auth []ssh.AuthMethod
|
auth []ssh.AuthMethod
|
||||||
hostKeys []ssh.PublicKey
|
hostKeys []ssh.PublicKey
|
||||||
conns map[string]conn
|
conns map[string]conn
|
||||||
rw sync.RWMutex
|
clientVersion string
|
||||||
wg sync.WaitGroup
|
rw sync.RWMutex
|
||||||
domainRegExp *regexp.Regexp
|
wg sync.WaitGroup
|
||||||
|
domainRegExp *regexp.Regexp
|
||||||
}
|
}
|
||||||
|
|
||||||
type conn struct {
|
type conn struct {
|
||||||
@ -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
|
||||||
|
@ -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"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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 err := sess.Shell(); err != nil {
|
if t.tunConfig.Shell.IsBool() {
|
||||||
t.log.Warnf("failed to start shell: %s", err.Error())
|
if err := sess.Shell(); err != nil {
|
||||||
|
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() {
|
||||||
|
10
internal/server/driver/ssh/validate.go
Normal file
10
internal/server/driver/ssh/validate.go
Normal 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)
|
||||||
|
}
|
@ -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 {
|
||||||
|
Loading…
Reference in New Issue
Block a user