diff --git a/README.md b/README.md index eb1170b5..3b96d3e5 100644 --- a/README.md +++ b/README.md @@ -427,12 +427,18 @@ You can customize your configuration using this template. #] #containers = ["${running}"] ``` + You can overwrite the default value specified in default section. - Vuls supports multiple SSH authentication methods. + + Vuls supports two types of SSH. One is native go implementation. The other is external SSH command. For details, see [-ssh-external option](https://github.com/future-architect/vuls#-ssh-external-option) + + Multiple SSH authentication methods are supported. - SSH agent - SSH public key authentication (with password, empty password) - Password authentication + + ---- # Usage: Prepare @@ -484,6 +490,7 @@ scan: [-cve-dictionary-url=http://127.0.0.1:1323] [-cvss-over=7] [-ignore-unscored-cves] + [-ssh-external] [-report-json] [-report-mail] [-report-s3] @@ -538,6 +545,8 @@ scan: Send report via Slack -report-text Write report to text files ($PWD/results/current) + -ssh-external + Use external ssh command. Default: Use the Go native implementation -use-unattended-upgrades [Deprecated] For Ubuntu. Scan by unattended-upgrades or not (use apt-get upgrade --dry-run by default) -use-yum-plugin-security @@ -545,6 +554,16 @@ scan: ``` +## -ssh-external option + +Vuls supports different types of SSH. + +By Defaut, using a native Go implementation from crypto/ssh. +This is useful in situations where you may not have access to traditional UNIX tools. + +To use external ssh command, specify this option. +This is useful If you want to use ProxyCommand or chiper algorithm of SSH that is not supported by native go implementation. + ## -ask-key-password option | SSH key password | -ask-key-password | | @@ -559,6 +578,7 @@ scan: | NOPASSWORD | - | defined as NOPASSWORD in /etc/sudoers on target servers | | with password | required | . | + ## -report-json , -report-text option At the end of the scan, scan results will be available in the $PWD/result/current/ directory. diff --git a/commands/scan.go b/commands/scan.go index 17f0b7ac..bea1ff3e 100644 --- a/commands/scan.go +++ b/commands/scan.go @@ -67,6 +67,8 @@ type ScanCmd struct { awsProfile string awsS3Bucket string awsRegion string + + sshExternal bool } // Name return subcommand name @@ -86,6 +88,7 @@ func (*ScanCmd) Usage() string { [-cve-dictionary-url=http://127.0.0.1:1323] [-cvss-over=7] [-ignore-unscored-cves] + [-ssh-external] [-report-json] [-report-mail] [-report-s3] @@ -141,6 +144,12 @@ func (p *ScanCmd) SetFlags(f *flag.FlagSet) { false, "Don't report the unscored CVEs") + f.BoolVar( + &p.sshExternal, + "ssh-external", + false, + "Use external ssh command. Default: Use the Go native implementation") + f.StringVar( &p.httpProxy, "http-proxy", @@ -292,6 +301,7 @@ func (p *ScanCmd) Execute(_ context.Context, f *flag.FlagSet, _ ...interface{}) c.Conf.CveDictionaryURL = p.cveDictionaryURL c.Conf.CvssScoreOver = p.cvssScoreOver c.Conf.IgnoreUnscoredCves = p.ignoreUnscoredCves + c.Conf.SSHExternal = p.sshExternal c.Conf.HTTPProxy = p.httpProxy c.Conf.UseYumPluginSecurity = p.useYumPluginSecurity c.Conf.UseUnattendedUpgrades = p.useUnattendedUpgrades diff --git a/config/config.go b/config/config.go index 7004a75b..057696a7 100644 --- a/config/config.go +++ b/config/config.go @@ -44,6 +44,8 @@ type Config struct { CvssScoreOver float64 IgnoreUnscoredCves bool + SSHExternal bool + HTTPProxy string `valid:"url"` DBPath string CveDBPath string diff --git a/scan/freebsd.go b/scan/freebsd.go index 9c29ce2c..8795215f 100644 --- a/scan/freebsd.go +++ b/scan/freebsd.go @@ -35,6 +35,7 @@ func detectFreebsd(c config.ServerInfo) (itsMe bool, bsd osTypeInterface) { } } } + Log.Debugf("Not FreeBSD. Host: %s:%s", c.Host, c.Port) return false, bsd } diff --git a/scan/serverapi.go b/scan/serverapi.go index 70830e81..d1433759 100644 --- a/scan/serverapi.go +++ b/scan/serverapi.go @@ -108,19 +108,19 @@ func (s CvePacksList) Less(i, j int) bool { func detectOS(c config.ServerInfo) (osType osTypeInterface) { var itsMe bool - itsMe, osType = detectDebian(c) - if itsMe { + if itsMe, osType = detectDebian(c); itsMe { + Log.Debugf("Debian like Linux. Host: %s:%s", c.Host, c.Port) return } - itsMe, osType = detectRedhat(c) - if itsMe { + if itsMe, osType = detectRedhat(c); itsMe { + Log.Debugf("Redhat like Linux. Host: %s:%s", c.Host, c.Port) return } - itsMe, osType = detectFreebsd(c) - if itsMe { + if itsMe, osType = detectFreebsd(c); itsMe { + Log.Debugf("FreeBSD. Host: %s:%s", c.Host, c.Port) return } - + osType.setServerInfo(c) osType.setErrs([]error{fmt.Errorf("Unknown OS Type")}) return } @@ -177,18 +177,21 @@ func detectServerOSes() (oses []osTypeInterface, err error) { }(s) } - timeout := time.After(300 * time.Second) + timeout := time.After(60 * time.Second) for i := 0; i < len(config.Conf.Servers); i++ { select { case res := <-osTypeChan: - if 0 < len(res.getErrs()) { - continue - } - Log.Infof("(%d/%d) Detected %s: %s", - i+1, len(config.Conf.Servers), - res.getServerInfo().ServerName, - res.getDistributionInfo()) oses = append(oses, res) + if 0 < len(res.getErrs()) { + Log.Infof("(%d/%d) Failed %s", + i+1, len(config.Conf.Servers), + res.getServerInfo().ServerName) + } else { + Log.Infof("(%d/%d) Detected %s: %s", + i+1, len(config.Conf.Servers), + res.getServerInfo().ServerName, + res.getDistributionInfo()) + } case <-timeout: msg := "Timeout occurred while detecting" Log.Error(msg) @@ -208,19 +211,16 @@ func detectServerOSes() (oses []osTypeInterface, err error) { } } - errorOccurred := false + errs := []error{} for _, osi := range oses { - if errs := osi.getErrs(); 0 < len(errs) { - errorOccurred = true - Log.Errorf("Some errors occurred on %s", - osi.getServerInfo().ServerName) - for _, err := range errs { - Log.Error(err) - } + if 0 < len(osi.getErrs()) { + errs = append(errs, fmt.Errorf( + "Error occurred on %s. errs: %s", + osi.getServerInfo().ServerName, osi.getErrs())) } } - if errorOccurred { - return oses, fmt.Errorf("Some errors occurred") + if 0 < len(errs) { + return oses, fmt.Errorf("%s", errs) } return } @@ -234,10 +234,11 @@ func detectContainerOSes() (oses []osTypeInterface, err error) { }(s) } - timeout := time.After(300 * time.Second) + timeout := time.After(60 * time.Second) for i := 0; i < len(config.Conf.Servers); i++ { select { case res := <-osTypesChan: + oses = append(oses, res...) for _, osi := range res { if 0 < len(osi.getErrs()) { continue @@ -247,7 +248,6 @@ func detectContainerOSes() (oses []osTypeInterface, err error) { sinfo.Container.ContainerID, sinfo.Container.Name, sinfo.ServerName, osi.getDistributionInfo()) } - oses = append(oses, res...) case <-timeout: msg := "Timeout occurred while detecting" Log.Error(msg) @@ -267,19 +267,16 @@ func detectContainerOSes() (oses []osTypeInterface, err error) { } } - errorOccurred := false + errs := []error{} for _, osi := range oses { - if errs := osi.getErrs(); 0 < len(errs) { - errorOccurred = true - Log.Errorf("Some errors occurred on %s", - osi.getServerInfo().ServerName) - for _, err := range errs { - Log.Error(err) - } + if 0 < len(osi.getErrs()) { + errs = append(errs, fmt.Errorf( + "Error occurred on %s. errs: %s", + osi.getServerInfo().ServerName, osi.getErrs())) } } - if errorOccurred { - return oses, fmt.Errorf("Some errors occurred") + if 0 < len(errs) { + return oses, fmt.Errorf("%s", errs) } return } diff --git a/scan/sshutil.go b/scan/sshutil.go index 9a927580..88ea7bab 100644 --- a/scan/sshutil.go +++ b/scan/sshutil.go @@ -25,7 +25,10 @@ import ( "io/ioutil" "net" "os" + "os/exec" + "runtime" "strings" + "syscall" "time" "golang.org/x/crypto/ssh" @@ -105,53 +108,21 @@ func parallelSSHExec(fn func(osTypeInterface) error, timeoutSec ...int) (errs [] } func sshExec(c conf.ServerInfo, cmd string, sudo bool, log ...*logrus.Entry) (result sshResult) { - // Setup Logger - var logger *logrus.Entry - if len(log) == 0 { - level := logrus.InfoLevel - if conf.Conf.Debug == true { - level = logrus.DebugLevel - } - l := &logrus.Logger{ - Out: os.Stderr, - Formatter: new(logrus.TextFormatter), - Hooks: make(logrus.LevelHooks), - Level: level, - } - logger = logrus.NewEntry(l) - } else { - logger = log[0] - } - c.SudoOpt.ExecBySudo = true - var err error - if sudo && c.User != "root" && !c.IsContainer() { - switch { - case c.SudoOpt.ExecBySudo: - cmd = fmt.Sprintf("echo %s | sudo -S %s", c.Password, cmd) - case c.SudoOpt.ExecBySudoSh: - cmd = fmt.Sprintf("echo %s | sudo sh -c '%s'", c.Password, cmd) - default: - logger.Panicf("sudoOpt is invalid. SudoOpt: %v", c.SudoOpt) - } + if runtime.GOOS == "windows" || !conf.Conf.SSHExternal { + return sshExecNative(c, cmd, sudo, log...) } + return sshExecExternal(c, cmd, sudo, log...) +} - if c.Family != "FreeBSD" { - // set pipefail option. Bash only - // http://unix.stackexchange.com/questions/14270/get-exit-status-of-process-thats-piped-to-another - cmd = fmt.Sprintf("set -o pipefail; %s", cmd) - } - - if c.IsContainer() { - switch c.Container.Type { - case "", "docker": - cmd = fmt.Sprintf(`docker exec %s /bin/bash -c "%s"`, c.Container.ContainerID, cmd) - } - } +func sshExecNative(c conf.ServerInfo, cmd string, sudo bool, log ...*logrus.Entry) (result sshResult) { + logger := getSSHLogger(log...) + cmd = decolateCmd(c, cmd, sudo) logger.Debugf("Command: %s", strings.Replace(maskPassword(cmd, c.Password), "\n", "", -1)) var client *ssh.Client + var err error client, err = sshConnect(c) defer client.Close() @@ -200,12 +171,125 @@ func sshExec(c conf.ServerInfo, cmd string, sudo bool, log ...*logrus.Entry) (re result.Port = c.Port logger.Debugf( - "SSH executed. cmd: %s, status: %#v\nstdout: \n%s\nstderr: \n%s", - maskPassword(cmd, c.Password), err, result.Stdout, result.Stderr) + "SSH executed. cmd: %s, err: %#v, status: %d\nstdout: \n%s\nstderr: \n%s", + maskPassword(cmd, c.Password), err, result.ExitStatus, result.Stdout, result.Stderr) return } +func sshExecExternal(c conf.ServerInfo, cmd string, sudo bool, log ...*logrus.Entry) (result sshResult) { + logger := getSSHLogger(log...) + + sshBinaryPath, err := exec.LookPath("ssh") + if err != nil { + logger.Debug("Failed to find SSH binary, using native Go implementation") + return sshExecNative(c, cmd, sudo, log...) + } + + defaultSSHArgs := []string{ + "-o", "StrictHostKeyChecking=no", + "-o", "UserKnownHostsFile=/dev/null", + "-o", "LogLevel=quiet", + "-o", "ConnectionAttempts=3", + "-o", "ConnectTimeout=10", + "-o", "ControlMaster=no", + "-o", "ControlPath=none", + + // TODO ssh session multiplexing + // "-o", "ControlMaster=auto", + // "-o", `ControlPath=~/.ssh/controlmaster-%r-%h.%p`, + // "-o", "Controlpersist=30m", + } + args := append(defaultSSHArgs, fmt.Sprintf("%s@%s", c.User, c.Host)) + args = append(args, "-p", c.Port) + + // if conf.Conf.Debug { + // args = append(args, "-v") + // } + + if 0 < len(c.KeyPath) { + args = append(args, "-i", c.KeyPath) + args = append(args, "-o", "PasswordAuthentication=no") + } + + cmd = decolateCmd(c, cmd, sudo) + args = append(args, cmd) + execCmd := exec.Command(sshBinaryPath, args...) + + var stdoutBuf, stderrBuf bytes.Buffer + execCmd.Stdout = &stdoutBuf + execCmd.Stderr = &stderrBuf + if err := execCmd.Run(); err != nil { + if e, ok := err.(*exec.ExitError); ok { + if s, ok := e.Sys().(syscall.WaitStatus); ok { + result.ExitStatus = s.ExitStatus() + } else { + result.ExitStatus = 998 + } + } else { + result.ExitStatus = 999 + } + } else { + result.ExitStatus = 0 + } + + result.Stdout = stdoutBuf.String() + result.Stderr = stderrBuf.String() + result.Host = c.Host + result.Port = c.Port + + logger.Debugf( + "SSH executed. cmd: %s %s, err: %#v, status: %d\nstdout: \n%s\nstderr: \n%s", + sshBinaryPath, + maskPassword(strings.Join(args, " "), c.Password), + err, result.ExitStatus, result.Stdout, result.Stderr) + + return +} + +func getSSHLogger(log ...*logrus.Entry) *logrus.Entry { + if len(log) == 0 { + level := logrus.InfoLevel + if conf.Conf.Debug == true { + level = logrus.DebugLevel + } + l := &logrus.Logger{ + Out: os.Stderr, + Formatter: new(logrus.TextFormatter), + Hooks: make(logrus.LevelHooks), + Level: level, + } + return logrus.NewEntry(l) + } + return log[0] +} + +func decolateCmd(c conf.ServerInfo, cmd string, sudo bool) string { + c.SudoOpt.ExecBySudo = true + if sudo && c.User != "root" && !c.IsContainer() { + switch { + case c.SudoOpt.ExecBySudo: + cmd = fmt.Sprintf("echo %s | sudo -S %s", c.Password, cmd) + case c.SudoOpt.ExecBySudoSh: + cmd = fmt.Sprintf("echo %s | sudo sh -c '%s'", c.Password, cmd) + } + } + + if c.Family != "FreeBSD" { + // set pipefail option. Bash only + // http://unix.stackexchange.com/questions/14270/get-exit-status-of-process-thats-piped-to-another + cmd = fmt.Sprintf("set -o pipefail; %s", cmd) + } + + if c.IsContainer() { + switch c.Container.Type { + case "", "docker": + cmd = fmt.Sprintf(`docker exec %s /bin/bash -c "%s"`, c.Container.ContainerID, cmd) + } + } + return cmd +} + func getAgentAuth() (auth ssh.AuthMethod, ok bool) { if sock := os.Getenv("SSH_AUTH_SOCK"); len(sock) > 0 { if agconn, err := net.Dial("unix", sock); err == nil { @@ -317,7 +401,7 @@ func parsePemBlock(block *pem.Block) (interface{}, error) { case "DSA PRIVATE KEY": return ssh.ParseDSAPrivateKey(block.Bytes) default: - return nil, fmt.Errorf("rtop: unsupported key type %q", block.Type) + return nil, fmt.Errorf("Unsupported key type %q", block.Type) } }