Issues (121)

scan/executil.go (2 issues)

Severity
1
/* Vuls - Vulnerability Scanner
2
Copyright (C) 2016  Future Corporation , Japan.
3
4
This program is free software: you can redistribute it and/or modify
5
it under the terms of the GNU General Public License as published by
6
the Free Software Foundation, either version 3 of the License, or
7
(at your option) any later version.
8
9
This program is distributed in the hope that it will be useful,
10
but WITHOUT ANY WARRANTY; without even the implied warranty of
11
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
GNU General Public License for more details.
13
14
You should have received a copy of the GNU General Public License
15
along with this program.  If not, see <http://www.gnu.org/licenses/>.
16
*/
17
18
package scan
19
20
import (
21
	"bytes"
22
	"crypto/x509"
23
	"encoding/pem"
24
	"fmt"
25
	"io/ioutil"
26
	"net"
27
	"os"
28
	ex "os/exec"
29
	"path/filepath"
30
	"strings"
31
	"syscall"
32
	"time"
33
34
	"golang.org/x/crypto/ssh"
35
	"golang.org/x/crypto/ssh/agent"
36
	"golang.org/x/xerrors"
37
38
	"github.com/cenkalti/backoff"
39
	conf "github.com/future-architect/vuls/config"
40
	"github.com/future-architect/vuls/util"
41
	homedir "github.com/mitchellh/go-homedir"
42
	"github.com/sirupsen/logrus"
43
)
44
45
type execResult struct {
46
	Servername string
47
	Container  conf.Container
48
	Host       string
49
	Port       string
50
	Cmd        string
51
	Stdout     string
52
	Stderr     string
53
	ExitStatus int
54
	Error      error
55
}
56
57
func (s execResult) String() string {
58
	sname := ""
59
	if s.Container.ContainerID == "" {
60
		sname = s.Servername
61
	} else {
62
		sname = s.Container.Name + "@" + s.Servername
63
	}
64
65
	return fmt.Sprintf(
66
		"execResult: servername: %s\n  cmd: %s\n  exitstatus: %d\n  stdout: %s\n  stderr: %s\n  err: %s",
67
		sname, s.Cmd, s.ExitStatus, s.Stdout, s.Stderr, s.Error)
68
}
69
70
func (s execResult) isSuccess(expectedStatusCodes ...int) bool {
71
	if len(expectedStatusCodes) == 0 {
72
		return s.ExitStatus == 0
73
	}
74
	for _, code := range expectedStatusCodes {
75
		if code == s.ExitStatus {
76
			return true
77
		}
78
	}
79
	if s.Error != nil {
80
		return false
81
	}
82
	return false
83
}
84
85
// sudo is Const value for sudo mode
86
const sudo = true
87
88
// noSudo is Const value for normal user mode
89
const noSudo = false
90
91
//  Issue commands to the target servers in parallel via SSH or local execution.  If execution fails, the server will be excluded from the target server list(servers) and added to the error server list(errServers).
92
func parallelExec(fn func(osTypeInterface) error, timeoutSec ...int) {
93
	resChan := make(chan osTypeInterface, len(servers))
94
	defer close(resChan)
95
96
	for _, s := range servers {
97
		go func(s osTypeInterface) {
98
			defer func() {
99
				if p := recover(); p != nil {
100
					util.Log.Debugf("Panic: %s on %s",
101
						p, s.getServerInfo().GetServerName())
102
				}
103
			}()
104
			if err := fn(s); err != nil {
105
				s.setErrs([]error{err})
106
				resChan <- s
107
			} else {
108
				resChan <- s
109
			}
110
		}(s)
111
	}
112
113
	var timeout int
114
	if len(timeoutSec) == 0 {
115
		timeout = 10 * 60
116
	} else {
117
		timeout = timeoutSec[0]
118
	}
119
120
	var successes []osTypeInterface
121
	isTimedout := false
122
	for i := 0; i < len(servers); i++ {
123
		select {
124
		case s := <-resChan:
125
			if len(s.getErrs()) == 0 {
126
				successes = append(successes, s)
127
			} else {
128
				util.Log.Errorf("Error on %s, err: %+v",
129
					s.getServerInfo().GetServerName(), s.getErrs())
130
				errServers = append(errServers, s)
131
			}
132
		case <-time.After(time.Duration(timeout) * time.Second):
133
			isTimedout = true
134
		}
135
	}
136
137
	if isTimedout {
138
		// set timed out error and append to errServers
139
		for _, s := range servers {
140
			name := s.getServerInfo().GetServerName()
141
			found := false
142
			for _, ss := range successes {
143
				if name == ss.getServerInfo().GetServerName() {
144
					found = true
145
					break
146
				}
147
			}
148
			if !found {
149
				err := xerrors.Errorf("Timed out: %s",
150
					s.getServerInfo().GetServerName())
151
				util.Log.Errorf("%+v", err)
152
				s.setErrs([]error{err})
153
				errServers = append(errServers, s)
154
			}
155
		}
156
	}
157
	servers = successes
158
	return
159
}
160
161
func exec(c conf.ServerInfo, cmd string, sudo bool, log ...*logrus.Entry) (result execResult) {
162
	logger := getSSHLogger(log...)
163
	logger.Debugf("Executing... %s", strings.Replace(cmd, "\n", "", -1))
164
165
	if isLocalExec(c.Port, c.Host) {
166
		result = localExec(c, cmd, sudo)
167
	} else if conf.Conf.SSHNative {
168
		result = sshExecNative(c, cmd, sudo)
169
	} else {
170
		result = sshExecExternal(c, cmd, sudo)
171
	}
172
173
	logger.Debug(result)
174
	return
175
}
176
177
func isLocalExec(port, host string) bool {
178
	return port == "local" && (host == "127.0.0.1" || host == "localhost")
179
}
180
181
func localExec(c conf.ServerInfo, cmdstr string, sudo bool) (result execResult) {
182
	cmdstr = decorateCmd(c, cmdstr, sudo)
183
	var cmd *ex.Cmd
184
	switch c.Distro.Family {
185
	// case conf.FreeBSD, conf.Alpine, conf.Debian:
186
	// cmd = ex.Command("/bin/sh", "-c", cmdstr)
187
	default:
188
		cmd = ex.Command("/bin/sh", "-c", cmdstr)
189
	}
190
	var stdoutBuf, stderrBuf bytes.Buffer
191
	cmd.Stdout = &stdoutBuf
192
	cmd.Stderr = &stderrBuf
193
194
	if err := cmd.Run(); err != nil {
195
		result.Error = err
196
		if exitError, ok := err.(*ex.ExitError); ok {
197
			waitStatus := exitError.Sys().(syscall.WaitStatus)
198
			result.ExitStatus = waitStatus.ExitStatus()
199
		} else {
200
			result.ExitStatus = 999
201
		}
202
	} else {
203
		result.ExitStatus = 0
204
	}
205
206
	result.Stdout = stdoutBuf.String()
207
	result.Stderr = stderrBuf.String()
208
	result.Cmd = strings.Replace(cmdstr, "\n", "", -1)
209
	return
210
}
211
212
func sshExecNative(c conf.ServerInfo, cmd string, sudo bool) (result execResult) {
213
	result.Servername = c.ServerName
214
	result.Container = c.Container
215
	result.Host = c.Host
216
	result.Port = c.Port
217
218
	var client *ssh.Client
219
	var err error
220
	if client, err = sshConnect(c); err != nil {
221
		result.Error = err
222
		result.ExitStatus = 999
223
		return
224
	}
225
	defer client.Close()
226
227
	var session *ssh.Session
228
	if session, err = client.NewSession(); err != nil {
229
		result.Error = xerrors.Errorf(
0 ignored issues
show
unrecognized printf verb 'w'
Loading history...
230
			"Failed to create a new session. servername: %s, err: %w",
231
			c.ServerName, err)
232
		result.ExitStatus = 999
233
		return
234
	}
235
	defer session.Close()
236
237
	// http://blog.ralch.com/tutorial/golang-ssh-connection/
238
	modes := ssh.TerminalModes{
239
		ssh.ECHO:          0,     // disable echoing
240
		ssh.TTY_OP_ISPEED: 14400, // input speed = 14.4kbaud
241
		ssh.TTY_OP_OSPEED: 14400, // output speed = 14.4kbaud
242
	}
243
	if err = session.RequestPty("xterm", 400, 1000, modes); err != nil {
244
		result.Error = xerrors.Errorf(
0 ignored issues
show
unrecognized printf verb 'w'
Loading history...
245
			"Failed to request for pseudo terminal. servername: %s, err: %w",
246
			c.ServerName, err)
247
		result.ExitStatus = 999
248
		return
249
	}
250
251
	var stdoutBuf, stderrBuf bytes.Buffer
252
	session.Stdout = &stdoutBuf
253
	session.Stderr = &stderrBuf
254
255
	cmd = decorateCmd(c, cmd, sudo)
256
	if err := session.Run(cmd); err != nil {
257
		if exitErr, ok := err.(*ssh.ExitError); ok {
258
			result.ExitStatus = exitErr.ExitStatus()
259
		} else {
260
			result.ExitStatus = 999
261
		}
262
	} else {
263
		result.ExitStatus = 0
264
	}
265
266
	result.Stdout = stdoutBuf.String()
267
	result.Stderr = stderrBuf.String()
268
	result.Cmd = strings.Replace(cmd, "\n", "", -1)
269
	return
270
}
271
272
func sshExecExternal(c conf.ServerInfo, cmd string, sudo bool) (result execResult) {
273
	sshBinaryPath, err := ex.LookPath("ssh")
274
	if err != nil {
275
		return sshExecNative(c, cmd, sudo)
276
	}
277
278
	defaultSSHArgs := []string{"-tt"}
279
280
	if !conf.Conf.SSHConfig {
281
		home, err := homedir.Dir()
282
		if err != nil {
283
			msg := fmt.Sprintf("Failed to get HOME directory: %s", err)
284
			result.Stderr = msg
285
			result.ExitStatus = 997
286
			return
287
		}
288
		controlPath := filepath.Join(home, ".vuls", `controlmaster-%r-`+c.ServerName+`.%p`)
289
290
		defaultSSHArgs = append(defaultSSHArgs,
291
			"-o", "StrictHostKeyChecking=yes",
292
			"-o", "LogLevel=quiet",
293
			"-o", "ConnectionAttempts=3",
294
			"-o", "ConnectTimeout=10",
295
			"-o", "ControlMaster=auto",
296
			"-o", fmt.Sprintf("ControlPath=%s", controlPath),
297
			"-o", "Controlpersist=10m",
298
		)
299
	}
300
301
	if conf.Conf.Vvv {
302
		defaultSSHArgs = append(defaultSSHArgs, "-vvv")
303
	}
304
305
	args := append(defaultSSHArgs, fmt.Sprintf("%s@%s", c.User, c.Host))
306
	args = append(args, "-p", c.Port)
307
	if 0 < len(c.KeyPath) {
308
		args = append(args, "-i", c.KeyPath)
309
		args = append(args, "-o", "PasswordAuthentication=no")
310
	}
311
312
	cmd = decorateCmd(c, cmd, sudo)
313
	cmd = fmt.Sprintf("stty cols 1000; %s", cmd)
314
315
	args = append(args, cmd)
316
	execCmd := ex.Command(sshBinaryPath, args...)
317
318
	var stdoutBuf, stderrBuf bytes.Buffer
319
	execCmd.Stdout = &stdoutBuf
320
	execCmd.Stderr = &stderrBuf
321
	if err := execCmd.Run(); err != nil {
322
		if e, ok := err.(*ex.ExitError); ok {
323
			if s, ok := e.Sys().(syscall.WaitStatus); ok {
324
				result.ExitStatus = s.ExitStatus()
325
			} else {
326
				result.ExitStatus = 998
327
			}
328
		} else {
329
			result.ExitStatus = 999
330
		}
331
	} else {
332
		result.ExitStatus = 0
333
	}
334
335
	result.Stdout = stdoutBuf.String()
336
	result.Stderr = stderrBuf.String()
337
	result.Servername = c.ServerName
338
	result.Container = c.Container
339
	result.Host = c.Host
340
	result.Port = c.Port
341
	result.Cmd = fmt.Sprintf("%s %s", sshBinaryPath, strings.Join(args, " "))
342
	return
343
}
344
345
func getSSHLogger(log ...*logrus.Entry) *logrus.Entry {
346
	if len(log) == 0 {
347
		return util.NewCustomLogger(conf.ServerInfo{})
348
	}
349
	return log[0]
350
}
351
352
func dockerShell(family string) string {
353
	switch family {
354
	// case conf.Alpine, conf.Debian:
355
	// return "/bin/sh"
356
	default:
357
		// return "/bin/bash"
358
		return "/bin/sh"
359
	}
360
}
361
362
func decorateCmd(c conf.ServerInfo, cmd string, sudo bool) string {
363
	if sudo && c.User != "root" && !c.IsContainer() {
364
		cmd = fmt.Sprintf("sudo -S %s", cmd)
365
	}
366
367
	// If you are using pipe and you want to detect preprocessing errors, remove comment out
368
	//  switch c.Distro.Family {
369
	//  case "FreeBSD", "ubuntu", "debian", "raspbian":
370
	//  default:
371
	//      // set pipefail option. Bash only
372
	//      // http://unix.stackexchange.com/questions/14270/get-exit-status-of-process-thats-piped-to-another
373
	//      cmd = fmt.Sprintf("set -o pipefail; %s", cmd)
374
	//  }
375
376
	if c.IsContainer() {
377
		switch c.ContainerType {
378
		case "", "docker":
379
			cmd = fmt.Sprintf(`docker exec --user 0 %s %s -c '%s'`,
380
				c.Container.ContainerID, dockerShell(c.Distro.Family), cmd)
381
		case "lxd":
382
			// If the user belong to the "lxd" group, root privilege is not required.
383
			cmd = fmt.Sprintf(`lxc exec %s -- %s -c '%s'`,
384
				c.Container.Name, dockerShell(c.Distro.Family), cmd)
385
		case "lxc":
386
			cmd = fmt.Sprintf(`lxc-attach -n %s 2>/dev/null -- %s -c '%s'`,
387
				c.Container.Name, dockerShell(c.Distro.Family), cmd)
388
			// LXC required root privilege
389
			if c.User != "root" {
390
				cmd = fmt.Sprintf("sudo -S %s", cmd)
391
			}
392
		}
393
	}
394
	//  cmd = fmt.Sprintf("set -x; %s", cmd)
395
	return cmd
396
}
397
398
func getAgentAuth() (auth ssh.AuthMethod, ok bool) {
399
	if sock := os.Getenv("SSH_AUTH_SOCK"); 0 < len(sock) {
400
		if agconn, err := net.Dial("unix", sock); err == nil {
401
			ag := agent.NewClient(agconn)
402
			auth = ssh.PublicKeysCallback(ag.Signers)
403
			ok = true
404
		}
405
	}
406
	return
407
}
408
409
func tryAgentConnect(c conf.ServerInfo) *ssh.Client {
410
	if auth, ok := getAgentAuth(); ok {
411
		config := &ssh.ClientConfig{
412
			User:            c.User,
413
			Auth:            []ssh.AuthMethod{auth},
414
			HostKeyCallback: ssh.InsecureIgnoreHostKey(),
415
		}
416
		client, _ := ssh.Dial("tcp", c.Host+":"+c.Port, config)
417
		return client
418
	}
419
	return nil
420
}
421
422
func sshConnect(c conf.ServerInfo) (client *ssh.Client, err error) {
423
	if client = tryAgentConnect(c); client != nil {
424
		return client, nil
425
	}
426
427
	var auths = []ssh.AuthMethod{}
428
	if auths, err = addKeyAuth(auths, c.KeyPath, c.KeyPassword); err != nil {
429
		return nil, err
430
	}
431
432
	// http://blog.ralch.com/tutorial/golang-ssh-connection/
433
	config := &ssh.ClientConfig{
434
		User:            c.User,
435
		Auth:            auths,
436
		HostKeyCallback: ssh.InsecureIgnoreHostKey(),
437
	}
438
439
	notifyFunc := func(e error, t time.Duration) {
440
		logger := getSSHLogger()
441
		logger.Debugf("Failed to Dial to %s, err: %s, Retrying in %s...",
442
			c.ServerName, e, t)
443
	}
444
	err = backoff.RetryNotify(func() error {
445
		if client, err = ssh.Dial("tcp", c.Host+":"+c.Port, config); err != nil {
446
			return err
447
		}
448
		return nil
449
	}, backoff.NewExponentialBackOff(), notifyFunc)
450
451
	return
452
}
453
454
// https://github.com/rapidloop/rtop/blob/ba5b35e964135d50e0babedf0bd69b2fcb5dbcb4/src/sshhelper.go#L100
455
func addKeyAuth(auths []ssh.AuthMethod, keypath string, keypassword string) ([]ssh.AuthMethod, error) {
456
	if len(keypath) == 0 {
457
		return auths, nil
458
	}
459
460
	// read the file
461
	pemBytes, err := ioutil.ReadFile(keypath)
462
	if err != nil {
463
		return auths, err
464
	}
465
466
	// get first pem block
467
	block, _ := pem.Decode(pemBytes)
468
	if block == nil {
469
		return auths, xerrors.Errorf("no key found in %s", keypath)
470
	}
471
472
	// handle plain and encrypted keyfiles
473
	if x509.IsEncryptedPEMBlock(block) {
474
		block.Bytes, err = x509.DecryptPEMBlock(block, []byte(keypassword))
475
		if err != nil {
476
			return auths, err
477
		}
478
		key, err := parsePemBlock(block)
479
		if err != nil {
480
			return auths, err
481
		}
482
		signer, err := ssh.NewSignerFromKey(key)
483
		if err != nil {
484
			return auths, err
485
		}
486
		return append(auths, ssh.PublicKeys(signer)), nil
487
	}
488
489
	signer, err := ssh.ParsePrivateKey(pemBytes)
490
	if err != nil {
491
		return auths, err
492
	}
493
	return append(auths, ssh.PublicKeys(signer)), nil
494
}
495
496
// ref golang.org/x/crypto/ssh/keys.go#ParseRawPrivateKey.
497
func parsePemBlock(block *pem.Block) (interface{}, error) {
498
	switch block.Type {
499
	case "RSA PRIVATE KEY":
500
		return x509.ParsePKCS1PrivateKey(block.Bytes)
501
	case "EC PRIVATE KEY":
502
		return x509.ParseECPrivateKey(block.Bytes)
503
	case "DSA PRIVATE KEY":
504
		return ssh.ParseDSAPrivateKey(block.Bytes)
505
	default:
506
		return nil, xerrors.Errorf("Unsupported key type %q", block.Type)
507
	}
508
}
509