Completed
Pull Request — master (#769)
by
unknown
06:17
created

scan.detectWpTheme   A

Complexity

Conditions 5

Size

Total Lines 14
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 5
eloc 9
dl 0
loc 14
rs 9.3333
c 0
b 0
f 0
nop 1
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
	"bufio"
22
	"encoding/json"
23
	"fmt"
24
	"net"
25
	"regexp"
26
	"strings"
27
	"time"
28
29
	"github.com/future-architect/vuls/config"
30
	"github.com/future-architect/vuls/models"
31
	"github.com/k0kubun/pp"
32
	"github.com/sirupsen/logrus"
33
)
34
35
type base struct {
36
	ServerInfo config.ServerInfo
37
	Distro     config.Distro
38
	Platform   models.Platform
39
	osPackages
40
41
	log  *logrus.Entry
42
	errs []error
43
}
44
45
func (l *base) scanWp() (err error) {
46
	if len(l.ServerInfo.WpPath) == 0 && len(l.ServerInfo.WpToken) == 0 {
47
		return nil
48
	}
49
50
	var unsecures []models.VulnInfo
51
	if unsecures, err = detectWp(l.ServerInfo); err != nil {
52
		l.log.Errorf("Failed to scan wordpress: %s", err)
53
		return err
54
	}
55
	for _, i := range unsecures {
56
		l.VulnInfos[i.CveID] = i
57
	}
58
59
	return err
60
}
61
62
//CveInfos hoge
63
type CveInfos struct {
64
	ReleaseDate     string    `json:"release_date"`
65
	ChangelogURL    string    `json:"changelog_url"`
66
	Status          string    `json:"status"`
67
	Vulnerabilities []CveInfo `json:"vulnerabilities"`
68
}
69
70
//CveInfo hoge
71
type CveInfo struct {
72
	ID            int        `json:"id"`
73
	Title         string     `json:"title"`
74
	CreatedAt     string     `json:"created_at"`
75
	UpdatedAt     string     `json:"updated_at"`
76
	PublishedDate string     `json:"Published_date"`
77
	VulnType      string     `json:"Vuln_type"`
78
	References    References `json:"references"`
79
	FixedIn       string     `json:"fixed_in"`
80
}
81
82
//References hoge
83
type References struct {
84
	URL []string `json:"url"`
85
	Cve []string `json:"cve"`
86
}
87
88
func detectWp(c config.ServerInfo) (rs []models.VulnInfo, err error) {
89
90
	var coreVuln []models.VulnInfo
91
	if coreVuln, err = detectWpCore(c); err != nil {
92
		return
93
	}
94
	for _, i := range coreVuln {
95
		rs = append(rs, i)
96
	}
97
98
	var themeVuln []models.VulnInfo
99
	if themeVuln, err = detectWpTheme(c); err != nil {
100
		return
101
	}
102
	for _, i := range themeVuln {
103
		rs = append(rs, i)
104
	}
105
106
	var pluginVuln []models.VulnInfo
107
	if pluginVuln, err = detectWpPlugin(c); err != nil {
108
		return
109
	}
110
	for _, i := range pluginVuln {
111
		rs = append(rs, i)
112
	}
113
	return
114
}
115
116
func detectWpCore(c config.ServerInfo) (rs []models.VulnInfo, err error) {
117
	cmd := fmt.Sprintf("wp core version --path=%s", c.WpPath)
118
119
	var coreVersion string
120
	if r := exec(c, cmd, noSudo); r.isSuccess() {
121
		tmp := strings.Split(r.Stdout, ".")
122
		coreVersion = strings.Join(tmp, "")
123
		coreVersion = strings.TrimRight(coreVersion, "\r\n")
124
		if len(coreVersion) == 0 {
125
			return
126
		}
127
	}
128
	cmd = fmt.Sprintf("curl -H 'Authorization: Token token=%s' https://wpvulndb.com/api/v3/wordpresses/%s", c.WpToken, coreVersion)
129
	if r := exec(c, cmd, noSudo); r.isSuccess() {
130
		data := map[string]CveInfos{}
131
		if err = json.Unmarshal([]byte(r.Stdout), &data); err != nil {
132
			return
133
		}
134
		for _, i := range data {
135
			for _, e := range i.Vulnerabilities {
136
				var cve string
137
				for _, k := range e.References.Cve {
138
					cve = "CVE-" + k
139
				}
140
141
				rs = append(rs, models.VulnInfo{
142
					CveID: cve,
143
				})
144
			}
145
		}
146
	}
147
	return
148
}
149
150
//WpStatus is hogehoge...
151
type WpStatus struct {
152
	Name    string `json:"-"`
153
	Status  string `json:"-"`
154
	Update  string `json:"-"`
155
	Version string `json:"-"`
156
}
157
158
func detectWpTheme(c config.ServerInfo) (rs []models.VulnInfo, err error) {
159
	cmd := fmt.Sprintf("wp theme list --path=%s", c.WpPath)
160
161
	var themes []WpStatus
162
	if r := exec(c, cmd, noSudo); r.isSuccess() {
163
		themes = parseStatus(r.Stdout)
164
	}
165
166
	for _, i := range themes {
167
		cmd := fmt.Sprintf("curl -H 'Authorization: Token token=%s' https://wpvulndb.com/api/v3/themes/%s", c.WpToken, i.Name)
168
		if r := exec(c, cmd, noSudo); r.isSuccess() {
169
		}
170
	}
171
	return
172
}
173
174
func detectWpPlugin(c config.ServerInfo) (rs []models.VulnInfo, err error) {
175
	cmd := fmt.Sprintf("wp plugin list --path=%s", c.WpPath)
176
177
	var plugins []WpStatus
178
	if r := exec(c, cmd, noSudo); r.isSuccess() {
179
		plugins = parseStatus(r.Stdout)
180
	}
181
182
	for _, i := range plugins {
183
		cmd := fmt.Sprintf("curl -H 'Authorization: Token token=%s' https://wpvulndb.com/api/v3/plugins/%s", c.WpToken, i.Name)
184
		pp.Println(cmd)
185
		if r := exec(c, cmd, noSudo); r.isSuccess() {
186
			pp.Println(r.Stdout)
187
		}
188
	}
189
	return
190
}
191
192
func parseStatus(r string) (themes []WpStatus) {
193
	tmp := strings.Split(r, "\r\n")
194
	tmp = unset(tmp, 0)
195
	tmp = unset(tmp, 0)
196
	tmp = unset(tmp, 0)
197
	tmp = unset(tmp, len(tmp)-1)
198
	tmp = unset(tmp, len(tmp)-1)
199
	for _, k := range tmp {
200
		theme := strings.Split(k, "|")
201
		themes = append(themes, WpStatus{
202
			Name:    strings.TrimSpace(theme[1]),
203
			Status:  strings.TrimSpace(theme[2]),
204
			Update:  strings.TrimSpace(theme[3]),
205
			Version: strings.TrimSpace(theme[4]),
206
		})
207
	}
208
	return
209
}
210
211
func unset(s []string, i int) []string {
212
	if i >= len(s) {
213
		return s
214
	}
215
	return append(s[:i], s[i+1:]...)
216
}
217
218
func (l *base) exec(cmd string, sudo bool) execResult {
219
	return exec(l.ServerInfo, cmd, sudo, l.log)
220
}
221
222
func (l *base) setServerInfo(c config.ServerInfo) {
223
	l.ServerInfo = c
224
}
225
226
func (l *base) getServerInfo() config.ServerInfo {
227
	return l.ServerInfo
228
}
229
230
func (l *base) setDistro(fam, rel string) {
231
	d := config.Distro{
232
		Family:  fam,
233
		Release: rel,
234
	}
235
	l.Distro = d
236
237
	s := l.getServerInfo()
238
	s.Distro = d
239
	l.setServerInfo(s)
240
}
241
242
func (l *base) getDistro() config.Distro {
243
	return l.Distro
244
}
245
246
func (l *base) setPlatform(p models.Platform) {
247
	l.Platform = p
248
}
249
250
func (l *base) getPlatform() models.Platform {
251
	return l.Platform
252
}
253
254
func (l *base) runningKernel() (release, version string, err error) {
255
	r := l.exec("uname -r", noSudo)
256
	if !r.isSuccess() {
257
		return "", "", fmt.Errorf("Failed to SSH: %s", r)
258
	}
259
	release = strings.TrimSpace(r.Stdout)
260
261
	switch l.Distro.Family {
262
	case config.Debian:
263
		r := l.exec("uname -a", noSudo)
264
		if !r.isSuccess() {
265
			return "", "", fmt.Errorf("Failed to SSH: %s", r)
266
		}
267
		ss := strings.Fields(r.Stdout)
268
		if 6 < len(ss) {
269
			version = ss[6]
270
		}
271
	}
272
	return
273
}
274
275
func (l *base) allContainers() (containers []config.Container, err error) {
276
	switch l.ServerInfo.ContainerType {
277
	case "", "docker":
278
		stdout, err := l.dockerPs("-a --format '{{.ID}} {{.Names}} {{.Image}}'")
279
		if err != nil {
280
			return containers, err
281
		}
282
		return l.parseDockerPs(stdout)
283
	case "lxd":
284
		stdout, err := l.lxdPs("-c n")
285
		if err != nil {
286
			return containers, err
287
		}
288
		return l.parseLxdPs(stdout)
289
	case "lxc":
290
		stdout, err := l.lxcPs("-1")
291
		if err != nil {
292
			return containers, err
293
		}
294
		return l.parseLxcPs(stdout)
295
	default:
296
		return containers, fmt.Errorf(
297
			"Not supported yet: %s", l.ServerInfo.ContainerType)
298
	}
299
}
300
301
func (l *base) runningContainers() (containers []config.Container, err error) {
302
	switch l.ServerInfo.ContainerType {
303
	case "", "docker":
304
		stdout, err := l.dockerPs("--format '{{.ID}} {{.Names}} {{.Image}}'")
305
		if err != nil {
306
			return containers, err
307
		}
308
		return l.parseDockerPs(stdout)
309
	case "lxd":
310
		stdout, err := l.lxdPs("volatile.last_state.power=RUNNING -c n")
311
		if err != nil {
312
			return containers, err
313
		}
314
		return l.parseLxdPs(stdout)
315
	case "lxc":
316
		stdout, err := l.lxcPs("-1 --running")
317
		if err != nil {
318
			return containers, err
319
		}
320
		return l.parseLxcPs(stdout)
321
	default:
322
		return containers, fmt.Errorf(
323
			"Not supported yet: %s", l.ServerInfo.ContainerType)
324
	}
325
}
326
327
func (l *base) exitedContainers() (containers []config.Container, err error) {
328
	switch l.ServerInfo.ContainerType {
329
	case "", "docker":
330
		stdout, err := l.dockerPs("--filter 'status=exited' --format '{{.ID}} {{.Names}} {{.Image}}'")
331
		if err != nil {
332
			return containers, err
333
		}
334
		return l.parseDockerPs(stdout)
335
	case "lxd":
336
		stdout, err := l.lxdPs("volatile.last_state.power=STOPPED -c n")
337
		if err != nil {
338
			return containers, err
339
		}
340
		return l.parseLxdPs(stdout)
341
	case "lxc":
342
		stdout, err := l.lxcPs("-1 --stopped")
343
		if err != nil {
344
			return containers, err
345
		}
346
		return l.parseLxcPs(stdout)
347
	default:
348
		return containers, fmt.Errorf(
349
			"Not supported yet: %s", l.ServerInfo.ContainerType)
350
	}
351
}
352
353
func (l *base) dockerPs(option string) (string, error) {
354
	cmd := fmt.Sprintf("docker ps %s", option)
355
	r := l.exec(cmd, noSudo)
356
	if !r.isSuccess() {
357
		return "", fmt.Errorf("Failed to SSH: %s", r)
358
	}
359
	return r.Stdout, nil
360
}
361
362
func (l *base) lxdPs(option string) (string, error) {
363
	cmd := fmt.Sprintf("lxc list %s", option)
364
	r := l.exec(cmd, noSudo)
365
	if !r.isSuccess() {
366
		return "", fmt.Errorf("failed to SSH: %s", r)
367
	}
368
	return r.Stdout, nil
369
}
370
371
func (l *base) lxcPs(option string) (string, error) {
372
	cmd := fmt.Sprintf("lxc-ls %s 2>/dev/null", option)
373
	r := l.exec(cmd, sudo)
374
	if !r.isSuccess() {
375
		return "", fmt.Errorf("failed to SSH: %s", r)
376
	}
377
	return r.Stdout, nil
378
}
379
380
func (l *base) parseDockerPs(stdout string) (containers []config.Container, err error) {
381
	lines := strings.Split(stdout, "\n")
382
	for _, line := range lines {
383
		fields := strings.Fields(line)
384
		if len(fields) == 0 {
385
			break
386
		}
387
		if len(fields) != 3 {
388
			return containers, fmt.Errorf("Unknown format: %s", line)
389
		}
390
		containers = append(containers, config.Container{
391
			ContainerID: fields[0],
392
			Name:        fields[1],
393
			Image:       fields[2],
394
		})
395
	}
396
	return
397
}
398
399
func (l *base) parseLxdPs(stdout string) (containers []config.Container, err error) {
400
	lines := strings.Split(stdout, "\n")
401
	for i, line := range lines[3:] {
402
		if i%2 == 1 {
403
			continue
404
		}
405
		fields := strings.Fields(strings.Replace(line, "|", " ", -1))
406
		if len(fields) == 0 {
407
			break
408
		}
409
		if len(fields) != 1 {
410
			return containers, fmt.Errorf("Unknown format: %s", line)
411
		}
412
		containers = append(containers, config.Container{
413
			ContainerID: fields[0],
414
			Name:        fields[0],
415
		})
416
	}
417
	return
418
}
419
420
func (l *base) parseLxcPs(stdout string) (containers []config.Container, err error) {
421
	lines := strings.Split(stdout, "\n")
422
	for _, line := range lines {
423
		fields := strings.Fields(line)
424
		if len(fields) == 0 {
425
			break
426
		}
427
		containers = append(containers, config.Container{
428
			ContainerID: fields[0],
429
			Name:        fields[0],
430
		})
431
	}
432
	return
433
}
434
435
// ip executes ip command and returns IP addresses
436
func (l *base) ip() ([]string, []string, error) {
437
	// e.g.
438
	// 2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP qlen 1000\    link/ether 52:54:00:2a:86:4c brd ff:ff:ff:ff:ff:ff
439
	// 2: eth0    inet 10.0.2.15/24 brd 10.0.2.255 scope global eth0
440
	// 2: eth0    inet6 fe80::5054:ff:fe2a:864c/64 scope link \       valid_lft forever preferred_lft forever
441
	r := l.exec("/sbin/ip -o addr", noSudo)
442
	if !r.isSuccess() {
443
		return nil, nil, fmt.Errorf("Failed to detect IP address: %v", r)
444
	}
445
	ipv4Addrs, ipv6Addrs := l.parseIP(r.Stdout)
446
	return ipv4Addrs, ipv6Addrs, nil
447
}
448
449
// parseIP parses the results of ip command
450
func (l *base) parseIP(stdout string) (ipv4Addrs []string, ipv6Addrs []string) {
451
	lines := strings.Split(stdout, "\n")
452
	for _, line := range lines {
453
		fields := strings.Fields(line)
454
		if len(fields) < 4 {
455
			continue
456
		}
457
		ip, _, err := net.ParseCIDR(fields[3])
458
		if err != nil {
459
			continue
460
		}
461
		if !ip.IsGlobalUnicast() {
462
			continue
463
		}
464
		if ipv4 := ip.To4(); ipv4 != nil {
465
			ipv4Addrs = append(ipv4Addrs, ipv4.String())
466
		} else {
467
			ipv6Addrs = append(ipv6Addrs, ip.String())
468
		}
469
	}
470
	return
471
}
472
473
func (l *base) detectPlatform() {
474
	if l.getServerInfo().Mode.IsOffline() {
475
		l.setPlatform(models.Platform{Name: "unknown"})
476
		return
477
	}
478
	ok, instanceID, err := l.detectRunningOnAws()
479
	if err != nil {
480
		l.setPlatform(models.Platform{Name: "other"})
481
		return
482
	}
483
	if ok {
484
		l.setPlatform(models.Platform{
485
			Name:       "aws",
486
			InstanceID: instanceID,
487
		})
488
		return
489
	}
490
491
	//TODO Azure, GCP...
492
	l.setPlatform(models.Platform{Name: "other"})
493
	return
494
}
495
496
func (l *base) detectRunningOnAws() (ok bool, instanceID string, err error) {
497
	if r := l.exec("type curl", noSudo); r.isSuccess() {
498
		cmd := "curl --max-time 1 --noproxy 169.254.169.254 http://169.254.169.254/latest/meta-data/instance-id"
499
		r := l.exec(cmd, noSudo)
500
		if r.isSuccess() {
501
			id := strings.TrimSpace(r.Stdout)
502
			if !l.isAwsInstanceID(id) {
503
				return false, "", nil
504
			}
505
			return true, id, nil
506
		}
507
508
		switch r.ExitStatus {
509
		case 28, 7:
510
			// Not running on AWS
511
			//  7   Failed to connect to host.
512
			// 28  operation timeout.
513
			return false, "", nil
514
		}
515
	}
516
517
	if r := l.exec("type wget", noSudo); r.isSuccess() {
518
		cmd := "wget --tries=3 --timeout=1 --no-proxy -q -O - http://169.254.169.254/latest/meta-data/instance-id"
519
		r := l.exec(cmd, noSudo)
520
		if r.isSuccess() {
521
			id := strings.TrimSpace(r.Stdout)
522
			if !l.isAwsInstanceID(id) {
523
				return false, "", nil
524
			}
525
			return true, id, nil
526
		}
527
528
		switch r.ExitStatus {
529
		case 4, 8:
530
			// Not running on AWS
531
			// 4   Network failure
532
			// 8   Server issued an error response.
533
			return false, "", nil
534
		}
535
	}
536
	return false, "", fmt.Errorf(
537
		"Failed to curl or wget to AWS instance metadata on %s. container: %s",
538
		l.ServerInfo.ServerName, l.ServerInfo.Container.Name)
539
}
540
541
// http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/resource-ids.html
542
var awsInstanceIDPattern = regexp.MustCompile(`^i-[0-9a-f]+$`)
543
544
func (l *base) isAwsInstanceID(str string) bool {
545
	return awsInstanceIDPattern.MatchString(str)
546
}
547
548
func (l *base) convertToModel() models.ScanResult {
549
	ctype := l.ServerInfo.ContainerType
550
	if l.ServerInfo.Container.ContainerID != "" && ctype == "" {
551
		ctype = "docker"
552
	}
553
	container := models.Container{
554
		ContainerID: l.ServerInfo.Container.ContainerID,
555
		Name:        l.ServerInfo.Container.Name,
556
		Image:       l.ServerInfo.Container.Image,
557
		Type:        ctype,
558
	}
559
560
	errs := []string{}
561
	for _, e := range l.errs {
562
		errs = append(errs, fmt.Sprintf("%s", e))
563
	}
564
565
	return models.ScanResult{
566
		JSONVersion:   models.JSONVersion,
567
		ServerName:    l.ServerInfo.ServerName,
568
		ScannedAt:     time.Now(),
569
		ScanMode:      l.ServerInfo.Mode.String(),
570
		Family:        l.Distro.Family,
571
		Release:       l.Distro.Release,
572
		Container:     container,
573
		Platform:      l.Platform,
574
		IPv4Addrs:     l.ServerInfo.IPv4Addrs,
575
		IPv6Addrs:     l.ServerInfo.IPv6Addrs,
576
		ScannedCves:   l.VulnInfos,
577
		RunningKernel: l.Kernel,
578
		Packages:      l.Packages,
579
		SrcPackages:   l.SrcPackages,
580
		Optional:      l.ServerInfo.Optional,
581
		Errors:        errs,
582
	}
583
}
584
585
func (l *base) setErrs(errs []error) {
586
	l.errs = errs
587
}
588
589
func (l *base) getErrs() []error {
590
	return l.errs
591
}
592
593
const (
594
	systemd  = "systemd"
595
	upstart  = "upstart"
596
	sysVinit = "init"
597
)
598
599
// https://unix.stackexchange.com/questions/196166/how-to-find-out-if-a-system-uses-sysv-upstart-or-systemd-initsystem
600
func (l *base) detectInitSystem() (string, error) {
601
	var f func(string) (string, error)
602
	f = func(cmd string) (string, error) {
603
		r := l.exec(cmd, sudo)
604
		if !r.isSuccess() {
605
			return "", fmt.Errorf("Failed to stat %s: %s", cmd, r)
606
		}
607
		scanner := bufio.NewScanner(strings.NewReader(r.Stdout))
608
		scanner.Scan()
609
		line := strings.TrimSpace(scanner.Text())
610
		if strings.Contains(line, "systemd") {
611
			return systemd, nil
612
		} else if strings.Contains(line, "upstart") {
613
			return upstart, nil
614
		} else if strings.Contains(line, "File: ‘/proc/1/exe’ -> ‘/sbin/init’") ||
615
			strings.Contains(line, "File: `/proc/1/exe' -> `/sbin/init'") {
616
			return f("stat /sbin/init")
617
		} else if line == "File: ‘/sbin/init’" ||
618
			line == "File: `/sbin/init'" {
619
			r := l.exec("/sbin/init --version", noSudo)
620
			if r.isSuccess() {
621
				if strings.Contains(r.Stdout, "upstart") {
622
					return upstart, nil
623
				}
624
			}
625
			return sysVinit, nil
626
		}
627
		return "", fmt.Errorf("Failed to detect a init system: %s", line)
628
	}
629
	return f("stat /proc/1/exe")
630
}
631
632
func (l *base) detectServiceName(pid string) (string, error) {
633
	cmd := fmt.Sprintf("systemctl status --quiet --no-pager %s", pid)
634
	r := l.exec(cmd, noSudo)
635
	if !r.isSuccess() {
636
		return "", fmt.Errorf("Failed to stat %s: %s", cmd, r)
637
	}
638
	return l.parseSystemctlStatus(r.Stdout), nil
639
}
640
641
func (l *base) parseSystemctlStatus(stdout string) string {
642
	scanner := bufio.NewScanner(strings.NewReader(stdout))
643
	scanner.Scan()
644
	line := scanner.Text()
645
	ss := strings.Fields(line)
646
	if len(ss) < 2 || strings.HasPrefix(line, "Failed to get unit for PID") {
647
		return ""
648
	}
649
	return ss[1]
650
}
651