Skip to content

Commit b1e6719

Browse files
authored
Merge pull request #1 from c1982/develop
feat: mvp
2 parents ea5b2c7 + a8f63c9 commit b1e6719

File tree

18 files changed

+15612
-9
lines changed

18 files changed

+15612
-9
lines changed

.gitignore

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,13 @@
1-
# Binaries for programs and plugins
21
*.exe
32
*.exe~
43
*.dll
54
*.so
6-
*.dylib
7-
8-
# Test binary, built with `go test -c`
95
*.test
10-
11-
# Output of the go coverage tool, specifically when used with LiteIDE
126
*.out
13-
14-
# Dependency directories (remove the comment below to include it)
15-
# vendor/
7+
vendor/
8+
go.sum
9+
scripts/collection-*
10+
scripts/fuzzy.db
11+
scripts/fuzzy.db-e
12+
pkg/fuzzyhash/db
13+
cmd/shellboy/shellboy

README.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,15 @@
11
# shellboy
22
Web Shell Scanner www.shellboy.net
3+
4+
# Yara Files Sources
5+
* https://raw.githubusercontent.com/Neo23x0/signature-base/50f14d7d1def5ee1032158af658a5c0b82fe50c9/yara/thor-webshells.yar
6+
* https://github.com/DarkenCode/yara-rules/blob/master/malware/Webshell-shell.yar
7+
8+
# Data Sources
9+
* https://github.com/tennc/webshell.git
10+
* https://github.com/ysrc/webshell-sample.git
11+
* https://github.com/xl7dev/WebShell.git
12+
* https://github.com/JohnTroony/php-webshells/tree/master/Collection
13+
* https://github.com/tanjiti/webshellSample.git
14+
* https://github.com/BlackArch/webshells
15+
* https://github.com/tutorial0/WebShell

cmd/shellboy/README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# shellboy
2+
3+
w00t!

cmd/shellboy/main.go

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
package main
2+
3+
import (
4+
"flag"
5+
"fmt"
6+
"os"
7+
"shellboy/pkg/fuzzyhash"
8+
"shellboy/pkg/walk"
9+
"strings"
10+
)
11+
12+
var (
13+
Version = "0.0.0"
14+
BuildDate = ""
15+
)
16+
17+
func main() {
18+
currentPath, _ := os.Getwd()
19+
rootPath := flag.String("directory", currentPath, "Root directory for scan.")
20+
minScore := flag.Int("score", 98, "Minimum similarity score.")
21+
minBytes := flag.Int("minbytes", 4000, "Minimum file size (byte). Default 4kb")
22+
maxBytes := flag.Int("maxbytes", 1000000, "Maximum file size (byte). Default 1Mb")
23+
excludes := flag.String("excludes", "zip,exe,md,rar,gz", "Excluded file extensions. Empty is disabled")
24+
includes := flag.String("includes", "", "Included file extensions. Default empty")
25+
help := flag.Bool("h", false, "Display this help message")
26+
verbose := flag.Bool("v", false, "Verbose mode")
27+
flag.Parse()
28+
29+
if *help {
30+
flag.PrintDefaults()
31+
return
32+
}
33+
34+
excludeExtensions := strings.Split(*excludes, ",")
35+
includeExtensions := strings.Split(*includes, ",")
36+
fhash := fuzzyhash.NewFuzzyHash(*minScore)
37+
walker := walk.NewWalker(*rootPath, *minBytes, *maxBytes, excludeExtensions, includeExtensions)
38+
39+
fmt.Printf("scanning...")
40+
err := walker.Scan(checkFileHash(fhash, *verbose))
41+
if err != nil {
42+
fmt.Printf("error: %v\r\n", err)
43+
}
44+
45+
fmt.Printf("done!\r\n")
46+
}
47+
48+
func checkFileHash(f *fuzzyhash.FuzzyHash, verbose bool) walk.ScanFunc {
49+
return func(path string, fileInfo os.FileInfo) error {
50+
score, name, err := f.FindHash(path)
51+
if err != nil {
52+
if verbose {
53+
fmt.Printf("error: %s, %s\r\n", err, path)
54+
}
55+
return nil
56+
}
57+
58+
if score >= f.GetMinScore() {
59+
fmt.Printf("score: %d, name: %s, file: %s\r\n", score, name, path)
60+
return nil
61+
}
62+
63+
return nil
64+
}
65+
}

go.mod

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
module shellboy
2+
3+
go 1.16
4+
5+
require (
6+
github.com/VirusTotal/gyp v0.5.4
7+
github.com/glaslos/ssdeep v0.3.1
8+
)

pkg/fuzzyhash/hash.go

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
package fuzzyhash
2+
3+
import (
4+
_ "embed"
5+
"sort"
6+
"strings"
7+
8+
"github.com/glaslos/ssdeep"
9+
)
10+
11+
const (
12+
DefaultScore = -1
13+
DefaultName = "unknown"
14+
)
15+
16+
//go:embed db/fuzzy.db
17+
var webShellStore string
18+
19+
//FuzzyHash main struct for fuzzyhash package
20+
type FuzzyHash struct {
21+
minscore int
22+
webshellHashes []string
23+
webshellCount int
24+
}
25+
26+
//NewFuzzyHash create fuzzy hash object for ssdeep hash check
27+
func NewFuzzyHash(minscore int) *FuzzyHash {
28+
f := &FuzzyHash{minscore: minscore}
29+
_ = f.LoadHashes(webShellStore)
30+
return f
31+
}
32+
33+
//SetMinScore set minimum webshell score
34+
func (f *FuzzyHash) SetMinScore(score int) {
35+
if score > 100 {
36+
score = 100
37+
}
38+
f.minscore = score
39+
}
40+
41+
//GetMinScore return the minimum score
42+
func (f *FuzzyHash) GetMinScore() int {
43+
return f.minscore
44+
}
45+
46+
func (f *FuzzyHash) LoadHashes(store string) int {
47+
f.webshellHashes = strings.Split(store, "\n")
48+
sort.Strings(f.webshellHashes)
49+
return len(f.webshellHashes)
50+
}
51+
52+
func (f *FuzzyHash) fileHash(file string) (hash string, err error) {
53+
h1, err := ssdeep.FuzzyFilename(file)
54+
if err != nil {
55+
return "", err
56+
}
57+
58+
return h1, err
59+
}
60+
61+
//FindHash search hash from webshell store
62+
func (f *FuzzyHash) FindHash(file string) (score int, name string, err error) {
63+
var (
64+
lo int
65+
hi int
66+
mid int
67+
midValue string
68+
)
69+
70+
filehash, err := f.fileHash(file)
71+
if err != nil {
72+
return DefaultScore, DefaultName, err
73+
}
74+
75+
hi = len(f.webshellHashes) - 1
76+
for lo <= hi {
77+
mid = lo + (hi-lo)/2
78+
midValue = f.webshellHashes[mid]
79+
if s, _ := ssdeep.Distance(filehash, midValue); s >= f.minscore {
80+
name = f.getNameFromHash(midValue)
81+
return s, name, nil
82+
} else if midValue > filehash {
83+
hi = mid - 1
84+
} else {
85+
lo = mid + 1
86+
}
87+
}
88+
89+
return score, DefaultName, err
90+
}
91+
92+
func (f *FuzzyHash) getNameFromHash(hash string) string {
93+
blocks := strings.Split(hash, ",")
94+
if len(blocks) == 2 {
95+
return strings.Trim(blocks[1], `"`)
96+
}
97+
return DefaultName
98+
}

pkg/fuzzyhash/hash_test.go

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
package fuzzyhash_test
2+
3+
import (
4+
"shellboy/pkg/fuzzyhash"
5+
"testing"
6+
)
7+
8+
var testHashes = `48:Lgyb7ZNBBPggOZNTnhSnPvs+rDsJZ4N1are4mH27BQ:UyzBRluMhnWZ2xs7K,"UpServlet.java"
9+
1536:TqNXIlidRzaWRNe2dzaWRNedp3n9cumC6bkWsEkpcCj:TsIJlMkWsjj,"jspspy有屏幕.txt"
10+
3072:l7VXb7dijqGv1r8/7QyrgudxUtbEcM/WTMt4K5ole73WGv:PXtevkxj4K5qbGv,"AspxSpy2014Final.aspx"`
11+
12+
func TestLoadFromStore(t *testing.T) {
13+
mockObj := fuzzyhash.FuzzyHash{}
14+
count := mockObj.LoadHashes(testHashes)
15+
if count != 3 {
16+
t.Errorf("invalid hash count. expect: 3, got: %d", count)
17+
}
18+
}
19+
20+
func TestFindHash(t *testing.T) {
21+
mockObj := fuzzyhash.FuzzyHash{}
22+
mockObj.LoadHashes(testHashes)
23+
mockObj.SetMinScore(98)
24+
25+
score, name, err := mockObj.FindHash("./testdata/AspxSpy2014Final.aspx")
26+
if err != nil {
27+
t.Errorf("cannot find hash: %v", err)
28+
}
29+
30+
if score != 100 {
31+
t.Errorf("invalid hash score. expect: 100, got: %d", score)
32+
}
33+
34+
if name != "AspxSpy2014Final.aspx" {
35+
t.Errorf("invalid file name: expect: AspxSpy2014Final.aspx, got: %s", name)
36+
}
37+
}
38+
39+
func BenchmarkFindHash(b *testing.B) {
40+
mockObj := fuzzyhash.FuzzyHash{}
41+
mockObj.LoadHashes(testHashes)
42+
mockObj.SetMinScore(100)
43+
for n := 0; n < b.N; n++ {
44+
_, _, _ = mockObj.FindHash("./testdata/AspxSpy2014Final.aspx")
45+
}
46+
}

0 commit comments

Comments
 (0)