%08d | %s | %d\n", fileIndex, filePath, fileRisk)
136 | } else {
137 | res = fmt.Sprintf("[+] %08d %-80s Risk:%d\n", fileIndex, filePath, fileRisk)
138 | }
139 | if *outputPtr != "" {
140 | outputFile, err := os.OpenFile(*outputPtr, os.O_WRONLY|os.O_APPEND, 0600)
141 | if err != nil {
142 | log.Fatal(err)
143 | }
144 | outputFile.WriteString(res)
145 | outputFile.Close()
146 | } else {
147 | for i := 0; i < 128; i++ {
148 | fmt.Printf("\b")
149 | }
150 | fmt.Print(res)
151 | }
152 | }
153 |
154 | func work(files []string) {
155 | for _, v := range files {
156 | info, err := os.Stat(v)
157 | if err != nil {
158 | log.Fatal(err)
159 | }
160 | if info.IsDir() {
161 | log.Printf("Testing %s...", v)
162 | truePath, err := filepath.EvalSymlinks(v)
163 | if err != nil {
164 | log.Fatal(err)
165 | }
166 | multiTest(truePath)
167 | } else {
168 | singleTest(v)
169 | }
170 | }
171 | }
172 |
173 | func main() {
174 | version := "1.0.0"
175 | flag.Usage = func() {
176 | fmt.Fprintf(flag.CommandLine.Output(), "Chaitin CloudWalker Webshell Detector\n[version %s]\n\nusage: %s [options] name ...\n\n", version, os.Args[0])
177 | flag.PrintDefaults()
178 | }
179 | outputPtr = flag.String("output", "", "Export result to output file")
180 | htmlPtr = flag.Bool("html", false, "Show result as HTML")
181 | flag.Parse()
182 | if len(flag.Args()) == 0 {
183 | flag.Usage()
184 | }
185 | files := flag.Args()
186 | php.Start()
187 | detector, err = WebshellDetector.NewDefaultDetector(php.Stdin, php.Stdout)
188 | if err != nil {
189 | log.Println("Detector kernel cannot load.")
190 | }
191 | if *outputPtr != "" {
192 | outputFile, err := os.OpenFile(*outputPtr, os.O_WRONLY|os.O_TRUNC|os.O_CREATE, 0600)
193 | if err != nil {
194 | log.Fatal(err)
195 | }
196 | if *htmlPtr {
197 | nowStr := time.Now().Format("2006-01-02 15:04:05")
198 | outputFile.WriteString(`
199 |
200 |
201 |
202 | Report - Chaitin CloudWalker
203 |
204 |
259 |
260 |
263 |
264 | Report
265 | Time: ` + nowStr + `
266 |
267 |
268 |
269 |
270 | Tested
271 | | Risk:1
272 | | Risk:2
273 | | Risk:3
274 | | Risk:4
275 | | Risk:5
276 | |
277 |
278 |
279 | Waiting
280 | | Waiting
281 | | Waiting
282 | | Waiting
283 | | Waiting
284 | | Waiting
285 | |
286 |
287 |
288 |
289 |
290 |
291 | Index
292 | | File
293 | | Risk
294 | |
295 | `)
296 | outputFile.Close()
297 | }
298 | work(files)
299 | outputFile, err = os.OpenFile(*outputPtr, os.O_WRONLY|os.O_APPEND, 0600)
300 | if err != nil {
301 | log.Fatal(err)
302 | }
303 | if *htmlPtr {
304 | outputFile.WriteString(fmt.Sprintf(` `, countFile, countRisk1, countRisk2, countRisk3, countRisk4, countRisk5))
312 | outputFile.Close()
313 | }
314 | } else {
315 | fmt.Println(" _____ _ ___ __ _ _")
316 | fmt.Println(" / ____| | | \\ \\ / / | | |")
317 | fmt.Println("| | | | ___ _ _ __| |\\ \\ /\\ / /_ _| | | _____ _ __ __ ___ ")
318 | fmt.Println("| | | |/ _ \\| | | |/ _` | \\ \\/ \\/ / _` | | |/ / _ \\ '__| /_ | / _ \\ ")
319 | fmt.Println("| |____| | (_) | |_| | (_| | \\ /\\ / (_| | | < __/ | | | ||_||")
320 | fmt.Println(" \\_____|_|\\___/ \\__,_|\\__,_| \\/ \\/ \\__,_|_|_|\\_\\___|_| |_(_)___/ \n")
321 | log.Println("Detector started.")
322 |
323 | work(files)
324 |
325 | for i := 0; i < 256; i++ {
326 | fmt.Printf("\b")
327 | }
328 | fmt.Print("\n\n")
329 | log.Printf("Risk (level1): %d files", countRisk1)
330 | log.Printf("Risk (level2): %d files", countRisk2)
331 | log.Printf("Risk (level3): %d files", countRisk3)
332 | log.Printf("Risk (level4): %d files", countRisk4)
333 | log.Printf("Risk (level5): %d files", countRisk5)
334 | log.Printf("Tested: %d files", countFile)
335 |
336 | fmt.Print("\n\n")
337 | log.Printf("Detector done (%v).\n", time.Since(t0))
338 | }
339 | }
340 |
--------------------------------------------------------------------------------
/tool/webshell-detector/bin/static/config/statState.json:
--------------------------------------------------------------------------------
1 | {
2 | "MinStat": {
3 | "LVC": 0.1,
4 | "SR": 10,
5 | "SPL": 0.001
6 | },
7 | "MaxStat": {
8 | "LM": 2048,
9 | "WM": 1024
10 | }
11 | }
--------------------------------------------------------------------------------
/tool/webshell-detector/bin/static/model-latest/Words.model:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chaitin/cloudwalker/d0a30200a3c498df03db1e527548a2c90baa1bd2/tool/webshell-detector/bin/static/model-latest/Words.model
--------------------------------------------------------------------------------
/tool/webshell-detector/php/.gitignore:
--------------------------------------------------------------------------------
1 | # macOS Finder metadata
2 | .DS_Store
3 |
4 | # build
5 | /bin/
6 | /include/
7 | /lib/
8 | /payload-phar.c
9 | /payload.phar
10 | /php-*
11 | /php/
12 |
--------------------------------------------------------------------------------
/tool/webshell-detector/php/Makefile:
--------------------------------------------------------------------------------
1 | PHP_RELEASE := 7.2.10
2 | PHP_MIRROR := http://php.net
3 |
4 | PHP_MAJOR := $(firstword $(subst ., ,$(PHP_RELEASE)))
5 | PHP_SRC := php-$(PHP_RELEASE).tar.bz2
6 | PHP_DIR := php-$(PHP_RELEASE)
7 | PHP := bin/php
8 | PHP_INC := include/php
9 | PHP_LIB := lib/libphp$(PHP_MAJOR).a
10 |
11 | # these flags are not exported and will not affect PHP build
12 | CFLAGS := -g -Wall -Wextra -Werror -Iinclude/php -I$(PHP_INC)/Zend -I$(PHP_INC)/TSRM -I$(PHP_INC)/main
13 | LDFLAGS := -lm
14 |
15 | all: $(PHP_LIB) payload-phar.c
16 |
17 | .PHONY: all clean distclean
18 |
19 | clean:
20 | $(RM) -r $(PHP_DIR) bin include lib payload.phar payload-phar.c
21 |
22 | distclean: clean
23 | $(RM) $(PHP_SRC)
24 |
25 | $(PHP_SRC):
26 | wget --no-verbose $(PHP_MIRROR)/distributions/$(PHP_SRC)
27 |
28 | $(PHP): $(PHP_SRC)
29 | tar --extract --file $(PHP_SRC)
30 | cd $(PHP_DIR) && for PATCHFILE in ../patches/*.patch; do echo "applying $${PATCHFILE}" && patch -p1 <"$${PATCHFILE}"; done
31 | cd $(PHP_DIR) && ./buildconf --force && CFLAGS=-O2 ./configure --prefix='$(realpath .)' --enable-embed=static --disable-phpdbg --disable-cgi --disable-all --enable-ast --enable-ctype --enable-filter --enable-json --enable-tokenizer --enable-phar --without-pear --enable-debug
32 | $(MAKE) -C $(PHP_DIR) install-cli install-headers install-sapi
33 |
34 | # dummy rule
35 | $(PHP_LIB): $(PHP)
36 |
37 | # FIXME: incomplete dependency
38 | payload.phar: $(PHP)
39 | $(PHP) --define phar.readonly=0 --run '$$phar = new Phar("payload.phar"); $$phar->buildFromDirectory(dirname(__FILE__) . "/payload"); $$phar->setStub($$phar->createDefaultStub("index.php"));'
40 |
41 | payload-phar.c: payload.phar
42 | xxd -i $< >$@
43 |
--------------------------------------------------------------------------------
/tool/webshell-detector/php/patches/0001-eliminate-libresolv.patch:
--------------------------------------------------------------------------------
1 | From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
2 | From: =?UTF-8?q?=E5=BC=A0=E9=85=89=E5=A4=AB?=
3 | Date: Wed, 27 Jun 2018 16:15:11 +0800
4 | Subject: [PATCH] eliminate libresolv
5 |
6 | ---
7 | ext/standard/config.m4 | 13 ++++++-------
8 | 1 file changed, 6 insertions(+), 7 deletions(-)
9 |
10 | diff --git a/ext/standard/config.m4 b/ext/standard/config.m4
11 | index b800aaa1..9833898a 100644
12 | --- a/ext/standard/config.m4
13 | +++ b/ext/standard/config.m4
14 | @@ -339,17 +339,16 @@ dnl
15 | dnl Detect library functions needed by php dns_xxx functions
16 | dnl ext/standard/php_dns.h will collect these in a single define: HAVE_FULL_DNS_FUNCS
17 | dnl
18 | -PHP_CHECK_FUNC(res_nsearch, resolv, bind, socket)
19 | -PHP_CHECK_FUNC(res_ndestroy, resolv, bind, socket)
20 | -PHP_CHECK_FUNC(dns_search, resolv, bind, socket)
21 | -PHP_CHECK_FUNC(dn_expand, resolv, bind, socket)
22 | -PHP_CHECK_FUNC(dn_skipname, resolv, bind, socket)
23 | +#PHP_CHECK_FUNC(res_nsearch, resolv, bind, socket)
24 | +#PHP_CHECK_FUNC(res_ndestroy, resolv, bind, socket)
25 | +#PHP_CHECK_FUNC(dns_search, resolv, bind, socket)
26 | +#PHP_CHECK_FUNC(dn_expand, resolv, bind, socket)
27 | +#PHP_CHECK_FUNC(dn_skipname, resolv, bind, socket)
28 |
29 | dnl
30 | dnl These are old deprecated functions
31 | dnl
32 | -
33 | -PHP_CHECK_FUNC(res_search, resolv, bind, socket)
34 | +#PHP_CHECK_FUNC(res_search, resolv, bind, socket)
35 |
36 | dnl
37 | dnl Check for strptime()
38 |
--------------------------------------------------------------------------------
/tool/webshell-detector/php/patches/0002-eliminate-libnsl.patch:
--------------------------------------------------------------------------------
1 | From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
2 | From: =?UTF-8?q?=E5=BC=A0=E9=85=89=E5=A4=AB?=
3 | Date: Sat, 29 Sep 2018 22:31:10 +0800
4 | Subject: [PATCH] eliminate libnsl
5 |
6 | ---
7 | TSRM/tsrm.m4 | 4 ++--
8 | configure.ac | 12 +++++-------
9 | ext/standard/dns.c | 6 +++---
10 | ext/standard/filestat.c | 4 ++--
11 | main/fopen_wrappers.c | 2 +-
12 | main/main.c | 2 +-
13 | main/network.c | 2 +-
14 | 7 files changed, 15 insertions(+), 17 deletions(-)
15 |
16 | diff --git a/TSRM/tsrm.m4 b/TSRM/tsrm.m4
17 | index 98aa2b8c..7d3f7852 100644
18 | --- a/TSRM/tsrm.m4
19 | +++ b/TSRM/tsrm.m4
20 | @@ -1,4 +1,4 @@
21 | -m4_include([TSRM/m4/gethostbyname.m4])
22 | +#m4_include([TSRM/m4/gethostbyname.m4])
23 |
24 | dnl TSRM_CHECK_GCC_ARG(ARG, ACTION-IF-FOUND, ACTION-IF-NOT_FOUND)
25 | AC_DEFUN([TSRM_CHECK_GCC_ARG],[
26 | @@ -33,7 +33,7 @@ AC_CHECK_HEADERS(stdarg.h)
27 |
28 | AC_CHECK_FUNCS(sigprocmask)
29 |
30 | -AX_FUNC_WHICH_GETHOSTBYNAME_R()
31 | +#AX_FUNC_WHICH_GETHOSTBYNAME_R()
32 |
33 | ])
34 |
35 | diff --git a/configure.ac b/configure.ac
36 | index 974c7b08..ff48a4df 100644
37 | --- a/configure.ac
38 | +++ b/configure.ac
39 | @@ -418,9 +418,9 @@ dnl Also, uClibc will bark at linking with glibc's libnsl.
40 | PHP_CHECK_FUNC(socket, socket)
41 | PHP_CHECK_FUNC(socketpair, socket)
42 | PHP_CHECK_FUNC(htonl, socket)
43 | -PHP_CHECK_FUNC(gethostname, nsl)
44 | -PHP_CHECK_FUNC(gethostbyaddr, nsl)
45 | -PHP_CHECK_FUNC(yp_get_default_domain, nsl)
46 | +#PHP_CHECK_FUNC(gethostname, nsl)
47 | +#PHP_CHECK_FUNC(gethostbyaddr, nsl)
48 | +#PHP_CHECK_FUNC(yp_get_default_domain, nsl)
49 |
50 | PHP_CHECK_FUNC(dlopen, dl)
51 | if test "$ac_cv_func_dlopen" = "yes"; then
52 | @@ -610,10 +610,6 @@ gai_strerror \
53 | gcvt \
54 | getloadavg \
55 | getlogin \
56 | -getprotobyname \
57 | -getprotobynumber \
58 | -getservbyname \
59 | -getservbyport \
60 | gethostname \
61 | getrusage \
62 | gettimeofday \
63 | @@ -684,6 +680,7 @@ nanosleep \
64 | dnl Some systems (like OpenSolaris) do not have nanosleep in libc
65 | PHP_CHECK_FUNC_LIB(nanosleep, rt)
66 |
67 | +if false; then
68 | dnl Check for getaddrinfo, should be a better way, but...
69 | dnl Also check for working getaddrinfo
70 | AC_CACHE_CHECK([for getaddrinfo], ac_cv_func_getaddrinfo,
71 | @@ -730,6 +727,7 @@ ac_cv_func_getaddrinfo=no)])
72 | if test "$ac_cv_func_getaddrinfo" = yes; then
73 | AC_DEFINE(HAVE_GETADDRINFO,1,[Define if you have the getaddrinfo function])
74 | fi
75 | +fi
76 |
77 | dnl Check for the __sync_fetch_and_add builtin
78 | AC_CACHE_CHECK([for __sync_fetch_and_add], ac_cv_func_sync_fetch_and_add,
79 | diff --git a/ext/standard/dns.c b/ext/standard/dns.c
80 | index fb21d6dd..13af9727 100644
81 | --- a/ext/standard/dns.c
82 | +++ b/ext/standard/dns.c
83 | @@ -184,9 +184,9 @@ static zend_string *php_gethostbyaddr(char *ip)
84 |
85 | #if HAVE_IPV6 && HAVE_INET_PTON
86 | if (inet_pton(AF_INET6, ip, &addr6)) {
87 | - hp = gethostbyaddr((char *) &addr6, sizeof(addr6), AF_INET6);
88 | + hp = NULL;
89 | } else if (inet_pton(AF_INET, ip, &addr)) {
90 | - hp = gethostbyaddr((char *) &addr, sizeof(addr), AF_INET);
91 | + hp = NULL;
92 | } else {
93 | return NULL;
94 | }
95 | @@ -197,7 +197,7 @@ static zend_string *php_gethostbyaddr(char *ip)
96 | return NULL;
97 | }
98 |
99 | - hp = gethostbyaddr((char *) &addr, sizeof(addr), AF_INET);
100 | + hp = NULL;
101 | #endif
102 |
103 | if (!hp || hp->h_name == NULL || hp->h_name[0] == '\0') {
104 | diff --git a/ext/standard/filestat.c b/ext/standard/filestat.c
105 | index e730b5ae..a4839585 100644
106 | --- a/ext/standard/filestat.c
107 | +++ b/ext/standard/filestat.c
108 | @@ -315,7 +315,7 @@ PHPAPI int php_get_gid_by_name(const char *name, gid_t *gid)
109 | efree(grbuf);
110 | *gid = gr.gr_gid;
111 | #else
112 | - struct group *gr = getgrnam(name);
113 | + struct group *gr = NULL;
114 |
115 | if (!gr) {
116 | return FAILURE;
117 | @@ -451,7 +451,7 @@ PHPAPI uid_t php_get_uid_by_name(const char *name, uid_t *uid)
118 | efree(pwbuf);
119 | *uid = pw.pw_uid;
120 | #else
121 | - struct passwd *pw = getpwnam(name);
122 | + struct passwd *pw = NULL;
123 |
124 | if (!pw) {
125 | return FAILURE;
126 | diff --git a/main/fopen_wrappers.c b/main/fopen_wrappers.c
127 | index 520edfad..03feb87c 100644
128 | --- a/main/fopen_wrappers.c
129 | +++ b/main/fopen_wrappers.c
130 | @@ -389,7 +389,7 @@ PHPAPI int php_fopen_primary_script(zend_file_handle *file_handle)
131 | return FAILURE;
132 | }
133 | #else
134 | - pw = getpwnam(user);
135 | + pw = NULL;
136 | #endif
137 | if (pw && pw->pw_dir) {
138 | spprintf(&filename, 0, "%s%c%s%c%s", pw->pw_dir, PHP_DIR_SEPARATOR, PG(user_dir), PHP_DIR_SEPARATOR, s + 1); /* Safe */
139 | diff --git a/main/main.c b/main/main.c
140 | index ee92bcc7..4049f4d1 100644
141 | --- a/main/main.c
142 | +++ b/main/main.c
143 | @@ -1329,7 +1329,7 @@ PHPAPI char *php_get_current_user(void)
144 | }
145 | pwd = &_pw;
146 | #else
147 | - if ((pwd=getpwuid(pstat->st_uid))==NULL) {
148 | + if (1) {
149 | return "";
150 | }
151 | #endif
152 | diff --git a/main/network.c b/main/network.c
153 | index 30e22003..002861fb 100644
154 | --- a/main/network.c
155 | +++ b/main/network.c
156 | @@ -1340,7 +1340,7 @@ struct hostent * gethostname_re (const char *host,struct hostent *hostbuf,char *
157 |
158 | PHPAPI struct hostent* php_network_gethostbyname(char *name) {
159 | #if !defined(HAVE_GETHOSTBYNAME_R)
160 | - return gethostbyname(name);
161 | + return NULL;
162 | #else
163 | if (FG(tmp_host_buf)) {
164 | free(FG(tmp_host_buf));
165 |
--------------------------------------------------------------------------------
/tool/webshell-detector/php/patches/0003-eliminate-libdl.patch:
--------------------------------------------------------------------------------
1 | From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
2 | From: =?UTF-8?q?=E5=BC=A0=E9=85=89=E5=A4=AB?=
3 | Date: Wed, 27 Jun 2018 17:08:57 +0800
4 | Subject: [PATCH] eliminate libdl
5 |
6 | ---
7 | Zend/Zend.m4 | 2 ++
8 | Zend/zend_portability.h | 2 +-
9 | configure.ac | 2 ++
10 | 3 files changed, 5 insertions(+), 1 deletion(-)
11 |
12 | diff --git a/Zend/Zend.m4 b/Zend/Zend.m4
13 | index 7a01c0b0..8a4a5a11 100644
14 | --- a/Zend/Zend.m4
15 | +++ b/Zend/Zend.m4
16 | @@ -66,8 +66,10 @@ AC_TYPE_SIZE_T
17 | AC_TYPE_SIGNAL
18 |
19 | AC_DEFUN([LIBZEND_LIBDL_CHECKS],[
20 | +if false; then
21 | AC_CHECK_LIB(dl, dlopen, [LIBS="-ldl $LIBS"])
22 | AC_CHECK_FUNC(dlopen,[AC_DEFINE(HAVE_LIBDL, 1,[ ])])
23 | +fi
24 | ])
25 |
26 | AC_DEFUN([LIBZEND_DLSYM_CHECK],[
27 | diff --git a/Zend/zend_portability.h b/Zend/zend_portability.h
28 | index 79632f65..6e581e7c 100644
29 | --- a/Zend/zend_portability.h
30 | +++ b/Zend/zend_portability.h
31 | @@ -159,7 +159,7 @@
32 | # define DL_ERROR dlerror
33 | # define DL_HANDLE void *
34 | # define ZEND_EXTENSIONS_SUPPORT 1
35 | -#elif defined(ZEND_WIN32)
36 | +#elif 0
37 | # define DL_LOAD(libname) LoadLibrary(libname)
38 | # define DL_FETCH_SYMBOL GetProcAddress
39 | # define DL_UNLOAD FreeLibrary
40 | diff --git a/configure.ac b/configure.ac
41 | index ff48a4df..8df71f77 100644
42 | --- a/configure.ac
43 | +++ b/configure.ac
44 | @@ -422,10 +422,12 @@ PHP_CHECK_FUNC(htonl, socket)
45 | #PHP_CHECK_FUNC(gethostbyaddr, nsl)
46 | #PHP_CHECK_FUNC(yp_get_default_domain, nsl)
47 |
48 | +if false; then
49 | PHP_CHECK_FUNC(dlopen, dl)
50 | if test "$ac_cv_func_dlopen" = "yes"; then
51 | AC_DEFINE(HAVE_LIBDL, 1, [ ])
52 | fi
53 | +fi
54 | AC_CHECK_LIB(m, sin)
55 |
56 | dnl Check for inet_aton
57 |
--------------------------------------------------------------------------------
/tool/webshell-detector/php/patches/0004-eliminate-librt.patch:
--------------------------------------------------------------------------------
1 | From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
2 | From: =?UTF-8?q?=E5=BC=A0=E9=85=89=E5=A4=AB?=
3 | Date: Fri, 29 Jun 2018 16:23:08 +0800
4 | Subject: [PATCH] eliminate librt
5 |
6 | ---
7 | configure.ac | 2 +-
8 | 1 file changed, 1 insertion(+), 1 deletion(-)
9 |
10 | diff --git a/configure.ac b/configure.ac
11 | index 8df71f77..2fd8fc9f 100644
12 | --- a/configure.ac
13 | +++ b/configure.ac
14 | @@ -680,7 +680,7 @@ nanosleep \
15 | )
16 |
17 | dnl Some systems (like OpenSolaris) do not have nanosleep in libc
18 | -PHP_CHECK_FUNC_LIB(nanosleep, rt)
19 | +PHP_CHECK_FUNC(nanosleep)
20 |
21 | if false; then
22 | dnl Check for getaddrinfo, should be a better way, but...
23 |
--------------------------------------------------------------------------------
/tool/webshell-detector/php/patches/0005-expose-phar_open_from_fp-as-PHP_PHAR_API.patch:
--------------------------------------------------------------------------------
1 | From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
2 | From: =?UTF-8?q?=E5=BC=A0=E9=85=89=E5=A4=AB?=
3 | Date: Tue, 25 Sep 2018 15:06:40 +0800
4 | Subject: [PATCH] expose phar_open_from_fp as PHP_PHAR_API
5 |
6 | ---
7 | ext/phar/phar.c | 2 +-
8 | ext/phar/phar_internal.h | 2 --
9 | ext/phar/php_phar.h | 4 +++-
10 | 3 files changed, 4 insertions(+), 4 deletions(-)
11 |
12 | diff --git a/ext/phar/phar.c b/ext/phar/phar.c
13 | index c1c57120..5633164c 100644
14 | --- a/ext/phar/phar.c
15 | +++ b/ext/phar/phar.c
16 | @@ -1553,7 +1553,7 @@ static inline char *phar_strnstr(const char *buf, int buf_len, const char *searc
17 | * that the manifest is proper, then pass it to phar_parse_pharfile(). SUCCESS
18 | * or FAILURE is returned and pphar is set to a pointer to the phar's manifest
19 | */
20 | -static int phar_open_from_fp(php_stream* fp, char *fname, int fname_len, char *alias, int alias_len, int options, phar_archive_data** pphar, int is_data, char **error) /* {{{ */
21 | +PHP_PHAR_API int phar_open_from_fp(php_stream* fp, char *fname, int fname_len, char *alias, int alias_len, int options, phar_archive_data** pphar, int is_data, char **error) /* {{{ */
22 | {
23 | const char token[] = "__HALT_COMPILER();";
24 | const char zip_magic[] = "PK\x03\x04";
25 | diff --git a/ext/phar/phar_internal.h b/ext/phar/phar_internal.h
26 | index c23e6f51..8c82b5c0 100644
27 | --- a/ext/phar/phar_internal.h
28 | +++ b/ext/phar/phar_internal.h
29 | @@ -131,7 +131,6 @@
30 | #define PHAR_MUNG_SCRIPT_FILENAME (1<<3)
31 |
32 | typedef struct _phar_entry_fp phar_entry_fp;
33 | -typedef struct _phar_archive_data phar_archive_data;
34 |
35 | ZEND_BEGIN_MODULE_GLOBALS(phar)
36 | /* a list of phar_archive_data objects that reference a cached phar, so
37 | @@ -575,7 +574,6 @@ int phar_open_or_create_zip(char *fname, int fname_len, char *alias, int alias_l
38 | int phar_zip_flush(phar_archive_data *archive, char *user_stub, zend_long len, int defaultstub, char **error);
39 |
40 | #ifdef PHAR_MAIN
41 | -static int phar_open_from_fp(php_stream* fp, char *fname, int fname_len, char *alias, int alias_len, int options, phar_archive_data** pphar, int is_data, char **error);
42 | extern php_stream_wrapper php_stream_phar_wrapper;
43 | #else
44 | extern HashTable cached_phars;
45 | diff --git a/ext/phar/php_phar.h b/ext/phar/php_phar.h
46 | index efaacc57..9013f88d 100644
47 | --- a/ext/phar/php_phar.h
48 | +++ b/ext/phar/php_phar.h
49 | @@ -34,8 +34,10 @@ extern zend_module_entry phar_module_entry;
50 | #define PHP_PHAR_API PHPAPI
51 | #endif
52 |
53 | -PHP_PHAR_API int phar_resolve_alias(char *alias, int alias_len, char **filename, int *filename_len);
54 | +typedef struct _phar_archive_data phar_archive_data;
55 |
56 | +PHP_PHAR_API int phar_resolve_alias(char *alias, int alias_len, char **filename, int *filename_len);
57 | +PHP_PHAR_API int phar_open_from_fp(php_stream* fp, char *fname, int fname_len, char *alias, int alias_len, int options, phar_archive_data** pphar, int is_data, char **error);
58 | #endif /* PHP_PHAR_H */
59 |
60 |
61 |
--------------------------------------------------------------------------------
/tool/webshell-detector/php/patches/0007-use-alias-in-opened_path-for-phar-stream-wrapper.patch:
--------------------------------------------------------------------------------
1 | From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
2 | From: =?UTF-8?q?=E5=BC=A0=E9=85=89=E5=A4=AB?=
3 | Date: Tue, 25 Sep 2018 17:44:59 +0800
4 | Subject: [PATCH] use alias in opened_path for phar:// stream wrapper
5 |
6 | ---
7 | ext/phar/stream.c | 4 ++--
8 | 1 file changed, 2 insertions(+), 2 deletions(-)
9 |
10 | diff --git a/ext/phar/stream.c b/ext/phar/stream.c
11 | index 0936681a..feb48e95 100644
12 | --- a/ext/phar/stream.c
13 | +++ b/ext/phar/stream.c
14 | @@ -233,7 +233,7 @@ static php_stream * phar_wrapper_open_url(php_stream_wrapper *wrapper, const cha
15 | }
16 | }
17 | if (opened_path) {
18 | - *opened_path = strpprintf(MAXPATHLEN, "phar://%s/%s", idata->phar->fname, idata->internal_file->filename);
19 | + *opened_path = strpprintf(MAXPATHLEN, "phar://%s/%s", idata->phar->alias, idata->internal_file->filename);
20 | }
21 | return fpf;
22 | } else {
23 | @@ -334,7 +334,7 @@ idata_error:
24 | }
25 | }
26 | if (opened_path) {
27 | - *opened_path = strpprintf(MAXPATHLEN, "phar://%s/%s", idata->phar->fname, idata->internal_file->filename);
28 | + *opened_path = strpprintf(MAXPATHLEN, "phar://%s/%s", idata->phar->alias, idata->internal_file->filename);
29 | }
30 | efree(internal_file);
31 | phar_stub:
32 |
--------------------------------------------------------------------------------
/tool/webshell-detector/php/patches/0008-expose-an-API-that-increment-refcount-of-phar_archiv.patch:
--------------------------------------------------------------------------------
1 | From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
2 | From: =?UTF-8?q?=E5=BC=A0=E9=85=89=E5=A4=AB?=
3 | Date: Tue, 25 Sep 2018 18:53:23 +0800
4 | Subject: [PATCH] expose an API that increment refcount of phar_archive_data
5 |
6 | ---
7 | ext/phar/phar.c | 6 ++++++
8 | ext/phar/php_phar.h | 1 +
9 | 2 files changed, 7 insertions(+)
10 |
11 | diff --git a/ext/phar/phar.c b/ext/phar/phar.c
12 | index 5633164c..00b1b0a7 100644
13 | --- a/ext/phar/phar.c
14 | +++ b/ext/phar/phar.c
15 | @@ -264,6 +264,12 @@ void phar_destroy_phar_data(phar_archive_data *phar) /* {{{ */
16 | }
17 | /* }}}*/
18 |
19 | +PHP_PHAR_API void phar_archive_addref(phar_archive_data *phar)
20 | +{
21 | + if (phar->is_persistent) { return; }
22 | + ++phar->refcount;
23 | +}
24 | +
25 | /**
26 | * Delete refcount and destruct if needed. On destruct return 1 else 0.
27 | */
28 | diff --git a/ext/phar/php_phar.h b/ext/phar/php_phar.h
29 | index 9013f88d..e44d44c9 100644
30 | --- a/ext/phar/php_phar.h
31 | +++ b/ext/phar/php_phar.h
32 | @@ -38,6 +38,7 @@ typedef struct _phar_archive_data phar_archive_data;
33 |
34 | PHP_PHAR_API int phar_resolve_alias(char *alias, int alias_len, char **filename, int *filename_len);
35 | PHP_PHAR_API int phar_open_from_fp(php_stream* fp, char *fname, int fname_len, char *alias, int alias_len, int options, phar_archive_data** pphar, int is_data, char **error);
36 | +PHP_PHAR_API void phar_archive_addref(phar_archive_data *phar);
37 | #endif /* PHP_PHAR_H */
38 |
39 |
40 |
--------------------------------------------------------------------------------
/tool/webshell-detector/php/payload/index.php:
--------------------------------------------------------------------------------
1 | getMessage();
55 |
56 | return json_encode(
57 | array(
58 | "status" => "failed",
59 | "reason" => $err_msg
60 | ),
61 | JSON_PARTIAL_OUTPUT_ON_ERROR
62 | );
63 | }
64 |
65 | $json = json_encode(
66 | array(
67 | "status" => "successed",
68 | "ast" => $ast
69 | ),
70 | JSON_PARTIAL_OUTPUT_ON_ERROR,
71 | 4096
72 | );
73 |
74 | if($json === false) {
75 | return json_encode(
76 | array(
77 | "status" => "failed",
78 | "reason" => "json encode error: ".json_last_error_msg()
79 | )
80 | );
81 | }
82 |
83 | return $json;
84 |
85 | }
86 |
87 | /**
88 | * php ast server : socket communication
89 | */
90 | class PhpAstServer {
91 |
92 | /**
93 | * get header data
94 | * @return length of data
95 | */
96 | function getHeader() : int {
97 | fscanf(STDIN, "%d%*c", $length);
98 | if (!$length) {
99 | throw new Exception('message header must be number');
100 | }
101 | return $length;
102 | }
103 |
104 | /**
105 | * get body data
106 | * @param body_length:int could given by getHeader()
107 | */
108 | function getBody(int $body_length) : string {
109 | $receive_length = 0;
110 | $receive_buffer = "";
111 | while($receive_length < $body_length) {
112 | $receive_buffer .= fread(STDIN, $body_length - $receive_length);
113 | $receive_length = strlen($receive_buffer);
114 | }
115 | return $receive_buffer;
116 | }
117 |
118 | /**
119 | * write data to client
120 | */
121 | public function write(string $msg) {
122 | $msg = $msg."\n";
123 | $len_str = strval(strlen($msg))."\n";
124 | fwrite(STDOUT, $len_str);
125 | fwrite(STDOUT, $msg);
126 | }
127 |
128 | /**
129 | * loop to deal with request
130 | */
131 | public function loop() {
132 | while (!feof(STDIN)) {
133 | try {
134 | $total_length = $this->getHeader();
135 | $src = $this->getBody($total_length);
136 | $talk_back = parseToJson($src);
137 | $this->write($talk_back);
138 | }
139 | catch(Exception $exception) {
140 | $this->write(json_encode(
141 | array(
142 | "status" => "failed",
143 | "reason" => $exception->getMessage()
144 | )
145 | ));
146 | }
147 | };
148 | }
149 | }
150 |
151 | function main() {
152 | $server = new PhpAstServer();
153 | $server->loop();
154 | }
155 |
156 | main();
157 |
158 | ?>
159 |
--------------------------------------------------------------------------------
/tool/webshell-detector/php/php.c:
--------------------------------------------------------------------------------
1 | #include
2 | #include
3 |
4 | #ifdef _WIN32
5 | #include
6 | #include
7 | #include
8 | #else
9 | #include
10 | #endif
11 |
12 | #include "sapi/embed/php_embed.h"
13 | #include "Zend/zend_exceptions.h"
14 | #include "ext/phar/php_phar.h"
15 |
16 | extern unsigned char payload_phar[];
17 | extern unsigned int payload_phar_len;
18 |
19 | static const size_t memory_limit = 1024 * 1024 * 1024; // 1 GB
20 | static const size_t stack_limit = 64 * 1024 * 1024; // 64 MB
21 |
22 | static int eval(const char *str, size_t len, zval *retval) {
23 | zval script;
24 | ZVAL_STRINGL(&script, str, len);
25 |
26 | zend_op_array *op_array;
27 | op_array = zend_compile_string(&script, "script");
28 | zval_dtor(&script);
29 | if (!op_array) {
30 | return 1;
31 | }
32 |
33 | int failed = 0;
34 | zend_first_try {
35 | zend_execute(op_array, retval);
36 | if (EG(exception)) {
37 | failed = 1;
38 | }
39 | } zend_catch {
40 | failed = 1;
41 | } zend_end_try();
42 |
43 | destroy_op_array(op_array);
44 | efree(op_array);
45 |
46 | return failed;
47 | }
48 |
49 | static char *get_error_message(void) {
50 | if (EG(exception)) {
51 | zval exception_object, rv, *message;
52 | zend_class_entry *exception_ce = NULL;
53 |
54 | ZVAL_OBJ(&exception_object, EG(exception));
55 | if (instanceof_function(Z_OBJCE(exception_object), zend_ce_exception)) {
56 | exception_ce = zend_ce_exception;
57 | } else if (instanceof_function(Z_OBJCE(exception_object), zend_ce_error)) {
58 | exception_ce = zend_ce_error;
59 | }
60 |
61 | if (exception_ce) {
62 | message = zend_read_property_ex(exception_ce, &exception_object, ZSTR_KNOWN(ZEND_STR_MESSAGE), 1, &rv);
63 | if (Z_TYPE(*message) == IS_STRING) {
64 | return Z_STRVAL(*message);
65 | }
66 | }
67 | }
68 | return "unknown error";
69 | }
70 |
71 | static int init_php_stdio(int fd_in, int fd_out) {
72 | int fd_err = 2;
73 |
74 | php_stream *s_in, *s_out, *s_err;
75 | s_in = php_stream_fopen_from_fd(fd_in, "rb", NULL);
76 | s_out = php_stream_fopen_from_fd(fd_out, "wb", NULL);
77 | s_err = php_stream_fopen_from_fd(fd_err, "wb", NULL);
78 |
79 | if (!s_in || !s_out || !s_err) {
80 | if (s_in) { php_stream_close(s_in); }
81 | if (s_out) { php_stream_close(s_out); }
82 | if (s_err) { php_stream_close(s_err); }
83 | return 1;
84 | }
85 |
86 | s_in->flags |= PHP_STREAM_FLAG_NO_BUFFER;
87 | s_out->flags |= PHP_STREAM_FLAG_NO_BUFFER;
88 | s_err->flags |= PHP_STREAM_FLAG_NO_BUFFER;
89 |
90 | // php only checks S_ISFIFO, which is not enough (e.g. tty)
91 | s_in->flags |= PHP_STREAM_FLAG_NO_SEEK;
92 | s_out->flags |= PHP_STREAM_FLAG_NO_SEEK;
93 | s_err->flags |= PHP_STREAM_FLAG_NO_SEEK;
94 |
95 | zend_constant ic, oc, ec;
96 | php_stream_to_zval(s_in, &ic.value);
97 | php_stream_to_zval(s_out, &oc.value);
98 | php_stream_to_zval(s_err, &ec.value);
99 |
100 | ic.flags = CONST_CS;
101 | ic.name = zend_string_init("STDIN", sizeof("STDIN")-1, 1);
102 | ic.module_number = 0;
103 | zend_register_constant(&ic);
104 |
105 | oc.flags = CONST_CS;
106 | oc.name = zend_string_init("STDOUT", sizeof("STDOUT")-1, 1);
107 | oc.module_number = 0;
108 | zend_register_constant(&oc);
109 |
110 | ec.flags = CONST_CS;
111 | ec.name = zend_string_init("STDERR", sizeof("STDERR")-1, 1);
112 | ec.module_number = 0;
113 | zend_register_constant(&ec);
114 |
115 | return 0;
116 | }
117 |
118 | static int load_phar(void) {
119 | php_stream *fp = php_stream_memory_open(TEMP_STREAM_READONLY, (char *)payload_phar, payload_phar_len);
120 |
121 | static const char fname[] = "payload.phar";
122 | static const char alias[] = "payload";
123 | phar_archive_data *pphar;
124 | if (phar_open_from_fp(fp, (char*)fname, sizeof fname - 1, (char*)alias, sizeof alias - 1, 0, &pphar, 0, NULL) != SUCCESS) {
125 | return 1;
126 | }
127 | phar_archive_addref(pphar);
128 |
129 | return 0;
130 | }
131 |
132 | int init(intptr_t fd_in, intptr_t fd_out) {
133 | php_embed_module.php_ini_ignore = 1;
134 | if (php_embed_init(0, NULL) != SUCCESS) { return 1; }
135 |
136 | EG(error_handling) = EH_THROW;
137 |
138 | PG(memory_limit) = memory_limit;
139 | if (zend_set_memory_limit(memory_limit) != SUCCESS) { return 1; }
140 |
141 | #ifdef _WIN32
142 | // duplicate Windows file handle and convert to msvcrt file descriptor
143 | HANDLE hProcess = GetCurrentProcess();
144 |
145 | if (!DuplicateHandle(hProcess, fd_in, hProcess, &fd_in, 0, FALSE, DUPLICATE_SAME_ACCESS)) {
146 | return 1;
147 | }
148 | fd_in = _open_osfhandle(fd_in, _O_RDONLY | _O_NOINHERIT | _O_BINARY);
149 | if (fd_in == -1) {
150 | CloseHandle(fd_in);
151 | return 1;
152 | }
153 |
154 | if (!DuplicateHandle(hProcess, fd_out, hProcess, &fd_out, 0, FALSE, DUPLICATE_SAME_ACCESS)) {
155 | _close(fd_in);
156 | return 1;
157 | }
158 | fd_out = _open_osfhandle(fd_out, _O_WRONLY | _O_NOINHERIT | _O_BINARY);
159 | if (fd_out == -1) {
160 | CloseHandle(fd_out);
161 | _close(fd_in);
162 | return 1;
163 | }
164 | #else
165 | fd_in = dup(fd_in);
166 | if (fd_in == -1) {
167 | return 1;
168 | }
169 |
170 | fd_out = dup(fd_out);
171 | if (fd_out == -1) {
172 | close(fd_in);
173 | return 1;
174 | }
175 | #endif
176 |
177 | if (init_php_stdio(fd_in, fd_out)) {
178 | return 1;
179 | }
180 |
181 | if (load_phar()) {
182 | return 1;
183 | }
184 |
185 | return 0;
186 | }
187 |
188 | static void* _execute(void* arg) {
189 | static const char entry_php[] = "require 'phar://payload/';";
190 | if (eval(entry_php, sizeof entry_php - 1, NULL)) {
191 | fputs(get_error_message(), stderr);
192 | }
193 | return NULL;
194 | }
195 |
196 | int execute(void) {
197 | #ifdef WIN32
198 | _execute();
199 | #else
200 | pthread_t thread;
201 | pthread_attr_t attr;
202 |
203 | if (pthread_attr_init(&attr) ||
204 | pthread_attr_setstacksize(&attr, stack_limit) ||
205 | pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED) ||
206 | pthread_create(&thread, &attr, _execute, NULL)
207 | ) {
208 | return 1;
209 | }
210 | #endif
211 | return 0;
212 | }
213 |
--------------------------------------------------------------------------------
/tool/webshell-detector/php/php.go:
--------------------------------------------------------------------------------
1 | package php
2 |
3 | /*
4 | #cgo CFLAGS: -Wall -Wextra -Werror -Wno-unused-parameter -I${SRCDIR}/include/php -I${SRCDIR}/include/php/Zend -I${SRCDIR}/include/php/TSRM -I${SRCDIR}/include/php/main
5 | #cgo LDFLAGS: ${SRCDIR}/lib/libphp7.a -lm
6 |
7 | #include
8 |
9 | int init(intptr_t fd_in, intptr_t fd_out);
10 | int execute(void);
11 | */
12 | import "C"
13 |
14 | import (
15 | "errors"
16 | "os"
17 | )
18 |
19 | var Stdin, Stdout *os.File
20 |
21 | func init() {
22 | var err error
23 | var stdin, stdout *os.File
24 | stdin, Stdin, err = os.Pipe()
25 | if err != nil { panic(err) }
26 | Stdout, stdout, err = os.Pipe()
27 | if err != nil { panic(err) }
28 |
29 | if ret := C.init(C.intptr_t(stdin.Fd()), C.intptr_t(stdout.Fd())); ret != 0 {
30 | panic("cannot initialize PHP runtime")
31 | }
32 | }
33 |
34 | func Start() error {
35 | if (C.execute() != 0) {
36 | return errors.New("cannot start PHP runtime")
37 | }
38 | return nil
39 | }
40 |
--------------------------------------------------------------------------------
/tool/webshell-detector/src/Ast.go:
--------------------------------------------------------------------------------
1 | package WebshellDetector
2 |
3 | import (
4 | "encoding/json"
5 | "errors"
6 | "os"
7 | "sort"
8 | "strings"
9 | )
10 |
11 | /*
12 | WebshellDetector - Refactor version 1
13 | Date 0814
14 | Author Twice
15 | Intro operations for PHP AST
16 | */
17 |
18 | type ast struct {
19 | root interface{}
20 | }
21 |
22 | func newAst(data []byte) (*ast, error) {
23 | var astData interface{}
24 | if err := json.Unmarshal(data, &astData); err != nil {
25 | return nil, err
26 | }
27 |
28 | if astMap, ok := astData.(map[string]interface{}); ok {
29 | if astNode, ok := astMap["ast"]; ok {
30 | return &ast{transformAstNode(astNode)}, nil
31 | } else if reason, ok := astMap["reason"]; ok {
32 | if reasonStr, ok := reason.(string); ok {
33 | return nil, errors.New(reasonStr)
34 | }
35 | }
36 | }
37 |
38 | return &ast{nil}, nil
39 | }
40 |
41 | func transformAstNode(ast interface{}) (result interface{}) {
42 | switch value := ast.(type) {
43 | case float64:
44 | result = value
45 | case string:
46 | result = value
47 | case nil:
48 | result = value
49 | case []interface{}:
50 | resArray := make([]interface{}, 0)
51 | for _, v := range value {
52 | resArray = append(resArray, transformAstNode(v))
53 | }
54 | result = resArray
55 | case map[string]interface{}:
56 | if _, ok := value["kind"]; ok {
57 | result = astNode{
58 | int(value["kind"].(float64)),
59 | int(value["flags"].(float64)),
60 | int(value["lineno"].(float64)),
61 | transformAstNode(value["children"])}
62 | } else {
63 | resMap := make(map[string]interface{})
64 | for k, v := range value {
65 | resMap[k] = transformAstNode(v)
66 | }
67 | result = resMap
68 | }
69 | }
70 |
71 | return
72 | }
73 |
74 | func newAstFromGenerator(src []byte, stdin *os.File, stdout *os.File) (*ast, error) {
75 | server := newPhpAstGenerator(stdin, stdout)
76 | data, err := server.GetData(src)
77 | if err != nil {
78 | return nil, err
79 | }
80 | return newAst(data)
81 | }
82 |
83 | type opQueueNode struct {
84 | Key string
85 | Value interface{}
86 | Layer int
87 | Father *astNode
88 | }
89 |
90 | func getOpSerial(ast interface{}) (result [][]int) {
91 |
92 | var queue []opQueueNode
93 |
94 | nowSerial := make([]int, 0)
95 |
96 | queue = append(queue, opQueueNode{"root", ast, 0, nil})
97 |
98 | for len(queue) != 0 {
99 | node := queue[0]
100 | switch value := node.Value.(type) {
101 | case astNode:
102 | nowSerial = append(nowSerial, value.Kind)
103 | //fmt.Printf("kind:%4v(%4v)\t", value.Kind, node.Layer)
104 | queue = append(queue, opQueueNode{"children", value.Children, node.Layer, &value})
105 | case string:
106 | case float64:
107 | case nil:
108 | if node.Key == "separator" {
109 | if len(nowSerial) != 0 {
110 | //fmt.Printf("\n")
111 | finishSerial := make([]int, 1)
112 | finishSerial[0] = node.Father.Kind
113 | finishSerial = append(finishSerial, nowSerial...)
114 |
115 | result = append(result, finishSerial)
116 | nowSerial = make([]int, 0)
117 | }
118 | }
119 | case []interface{}:
120 | for i, v := range value {
121 | queue = append(queue, opQueueNode{string(i), v, node.Layer + 1, node.Father})
122 | }
123 | if node.Key == "children" {
124 | queue = append(queue, opQueueNode{"separator", nil, node.Layer + 1, node.Father})
125 | }
126 | case map[string]interface{}:
127 | keys := make([]string, 0)
128 | for key, _ := range value {
129 | keys = append(keys, key)
130 | }
131 | sort.Strings(keys)
132 |
133 | for _, k := range keys {
134 | queue = append(queue, opQueueNode{k, value[k], node.Layer + 1, node.Father})
135 | }
136 | if node.Key == "children" {
137 | queue = append(queue, opQueueNode{"separator", nil, node.Layer + 1, node.Father})
138 | }
139 | }
140 | queue = queue[1:]
141 | }
142 |
143 | return
144 | }
145 |
146 | func cleanOpSerial(data [][]int, maxLen int) [][]int {
147 |
148 | blockCompare := func(data []int, i int, length int) bool {
149 | if i+2*length > len(data) {
150 | return false
151 | }
152 | for k := 0; k < length; k++ {
153 | if data[i+k] != data[i+length+k] {
154 | return false
155 | }
156 | }
157 | return true
158 | }
159 |
160 | var result [][]int
161 |
162 | for _, v := range data {
163 | tmp := v
164 | for i := maxLen; i >= 1; i-- {
165 | for j := 0; j < len(tmp); {
166 | if blockCompare(tmp, j, i) {
167 | tmp = append(tmp[:j], tmp[j+i:]...)
168 | } else {
169 | j++
170 | }
171 | }
172 | }
173 | result = append(result, tmp)
174 | }
175 |
176 | return result
177 | }
178 |
179 | func cleanOpSerialRepeatedly(serials *[][]int, cleanTimes, cleanLength int) {
180 | for i := 1; i <= cleanTimes; i++ {
181 | *serials = cleanOpSerial(*serials, cleanLength)
182 | }
183 | }
184 |
185 | type arrayHashState struct {
186 | Table map[string]int
187 | DeTable map[int]string
188 | }
189 |
190 | func newArrayHashState() arrayHashState {
191 | return arrayHashState{make(map[string]int), make(map[int]string)}
192 | }
193 |
194 | func serialToString(serial []int) string {
195 | builder := strings.Builder{}
196 | for _, value := range serial {
197 | builder.WriteByte(byte(value / 100))
198 | builder.WriteByte(byte(value % 100))
199 | }
200 |
201 | return builder.String()
202 | }
203 |
204 | func stringToSerial(str string) (result []int) {
205 | for i := 0; i < len(str); i += 2 {
206 | result = append(result, int([]byte(str)[i])*100+int([]byte(str)[i+1]))
207 | }
208 |
209 | return
210 | }
211 |
212 | func (state *arrayHashState) Hash(span []int) int {
213 | if value, ok := state.Table[serialToString(span)]; ok {
214 | return value
215 | } else {
216 | return -1
217 | }
218 | }
219 |
220 | func (state *arrayHashState) Find(hash int) []int {
221 | return stringToSerial(state.DeTable[hash])
222 | }
223 |
224 | func (state *arrayHashState) Load(bytes []byte) error {
225 | return json.Unmarshal(bytes, state)
226 | }
227 |
228 | func (ast ast) GetOpSerial(state *arrayHashState) opSerial {
229 | serials := getOpSerial(ast.root)
230 | cleanOpSerialRepeatedly(&serials, 10, 5)
231 |
232 | vector := make([]float64, len(state.Table))
233 | for _, s := range serials {
234 | hash := state.Hash(s)
235 | if hash != -1 {
236 | vector[hash]++
237 | }
238 | }
239 |
240 | return opSerial{vector}
241 | }
242 |
243 | type iterateAstState struct {
244 | Key string
245 | KindPredict func(int) bool
246 | FlagsPredict func(int) bool
247 | ChildrenPredict func(interface{}) bool
248 | }
249 |
250 | func predictTrue(int) bool {
251 | return true
252 | }
253 |
254 | func predictTrueInterface(interface{}) bool {
255 | return true
256 | }
257 |
258 | func (state iterateAstState) Iterate(ast interface{}) (count int) {
259 | count = 0
260 |
261 | switch value := ast.(type) {
262 | case astNode:
263 | if state.KindPredict(value.Kind) && state.FlagsPredict(value.Flag) && state.ChildrenPredict(value.Children) {
264 | count += 1
265 | }
266 | count += state.Iterate(value.Children)
267 | case float64:
268 | case string:
269 | case []interface{}:
270 | for i, v := range value {
271 | state.Key = string(i)
272 | count += state.Iterate(v)
273 | }
274 | case map[string]interface{}:
275 | for k, v := range value {
276 | state.Key = k
277 | count += state.Iterate(v)
278 | }
279 | }
280 |
281 | return
282 | }
283 |
284 | func checkNameAndKind(ast interface{}, nameChecker func(string) bool, kindChecker func(int) bool) int {
285 | state := iterateAstState{
286 | "",
287 | kindChecker,
288 | predictTrue,
289 | func(ast interface{}) bool {
290 | if nameMap, ok := ast.(map[string]interface{}); ok {
291 | if name, ok := nameMap["name"]; ok {
292 | if nameStr, ok := name.(string); ok {
293 | return nameChecker(nameStr)
294 | }
295 | }
296 | }
297 | return false
298 | }}
299 |
300 | return state.Iterate(ast)
301 | }
302 |
303 | func (ast ast) GetWordsAndCallable() (words, bool) {
304 | result := false
305 | vector := make([]string, 0)
306 | checkNameAndKind(ast.root, func(s string) bool {
307 | vector = append(vector, s)
308 | return true
309 | }, func(k int) bool {
310 | if k == 269 || k == 265 || k == 515 || k == 768 || k == 769 {
311 | result = true
312 | }
313 | return true
314 | })
315 |
316 | return words{vector}, result
317 | }
318 |
--------------------------------------------------------------------------------
/tool/webshell-detector/src/AstNode.go:
--------------------------------------------------------------------------------
1 | package WebshellDetector
2 |
3 | /*
4 | WebshellDetector - Refactor version 1
5 | Date 0814
6 | Author Twice
7 | Intro A node structure in PHP AST
8 | */
9 |
10 | type astNode struct {
11 | Kind int
12 | Flag int
13 | LineNo int
14 | Children interface{}
15 | }
16 |
--------------------------------------------------------------------------------
/tool/webshell-detector/src/Ast_test.go:
--------------------------------------------------------------------------------
1 | package WebshellDetector
2 |
3 | import (
4 | "testing"
5 | )
6 |
7 | func Test_Ast_New(t *testing.T) {
8 | ast, err := newAstFromGenerator([]byte(``), stdin, stdout)
9 | if err != nil {
10 | t.Error(err.Error())
11 | t.Log(ast)
12 | }
13 | }
14 |
15 | func Test_Ast_GetOpSerial(t *testing.T) {
16 | ast, err := newAstFromGenerator([]byte(``), stdin, stdout)
17 | if err != nil {
18 | t.Error(err.Error())
19 | }
20 |
21 | det, err := NewDefaultDetector(stdin, stdout)
22 | if err != nil {
23 | t.Error(err.Error())
24 | }
25 |
26 | vec1 := ast.GetOpSerial(&det.hashState).data
27 |
28 | vec2 := make([]float64, len(vec1))
29 | vec2[0] = 3
30 | vec2[2] = 1
31 | vec2[6] = 1
32 | vec2[20] = 2
33 | vec2[26] = 1
34 | vec2[45] = 1
35 | vec2[195] = 1
36 |
37 | for i := 0; i < len(vec1); i++ {
38 | if vec1[i] != vec2[i] {
39 | t.Error("vec1 != vec2")
40 | }
41 | }
42 | }
43 |
44 | func Test_Ast_GetWords(t *testing.T) {
45 |
46 | ast, err := newAstFromGenerator([]byte(``), stdin, stdout)
47 | if err != nil {
48 | return
49 | }
50 | res, _ := ast.GetWordsAndCallable()
51 | vec := res.data
52 |
53 | t.Log(len(vec))
54 |
55 | for i := 0; i < len(vec); i++ {
56 | if vec[i] != "f" && vec[i] != "g" {
57 | t.Error("vec1 != vec2")
58 | }
59 | }
60 |
61 | }
62 |
63 | func Test_Ast_IsCallable(t *testing.T) {
64 | ast1, err := newAstFromGenerator([]byte(``), stdin, stdout)
65 | if err != nil {
66 | return
67 | }
68 | _, res1 := ast1.GetWordsAndCallable()
69 | if res1 != true {
70 | t.Error("Missing Callable")
71 | }
72 |
73 | ast2, err := newAstFromGenerator([]byte(``), stdin, stdout)
74 | if err != nil {
75 | return
76 | }
77 | _, res2 := ast2.GetWordsAndCallable()
78 | if res2 != false {
79 | t.Error("Misjudge Callable")
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/tool/webshell-detector/src/BinData_test.go:
--------------------------------------------------------------------------------
1 | package WebshellDetector
2 |
3 | import (
4 | "testing"
5 | )
6 |
7 | func Test_BinData_opSerialStream(t *testing.T) {
8 | _, err := Asset("static/model-latest/OpSerial.model")
9 | if err != nil {
10 | t.Log(err)
11 | t.Error("FAILED - Test_BinData - opSerialStream")
12 | }
13 | }
14 | func Test_BinData_processorStream(t *testing.T) {
15 | _, err := Asset("static/model-latest/Processor.model")
16 | if err != nil {
17 | t.Log(err)
18 | t.Error("FAILED - Test_BinData - processorStream")
19 | }
20 | }
21 | func Test_BinData_wordStream(t *testing.T) {
22 | _, err := Asset("static/model-latest/Words.model")
23 | if err != nil {
24 | t.Log(err)
25 | t.Error("FAILED - Test_BinData - wordStream")
26 | }
27 | }
28 | func Test_BinData_hashStateBytes(t *testing.T) {
29 | _, err := Asset("static/model-latest/hashState.json")
30 | if err != nil {
31 | t.Log(err)
32 | t.Error("FAILED - Test_BinData - hashStateBytes")
33 | }
34 | }
35 | func Test_BinData_statStateBytes(t *testing.T) {
36 | _, err := Asset("static/config/statState.json")
37 | if err != nil {
38 | t.Log(err)
39 | t.Error("FAILED - Test_BinData - statStateBytes")
40 | }
41 | }
42 | func Test_BinData_regMatcherStream(t *testing.T) {
43 | _, err := Asset("static/config/rules.conf")
44 | if err != nil {
45 | t.Log(err)
46 | t.Error("FAILED - Test_BinData - regMatcherStream")
47 | }
48 | }
49 | func Test_BinData_sampleMatcherStream(t *testing.T) {
50 | _, err := Asset("static/model-latest/SampleHash.txt")
51 | if err != nil {
52 | t.Log(err)
53 | t.Error("FAILED - Test_BinData - sampleMatcherStream")
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/tool/webshell-detector/src/Detector.go:
--------------------------------------------------------------------------------
1 | package WebshellDetector
2 |
3 | import (
4 | "log"
5 |
6 | "github.com/CyrusF/go-bayesian"
7 | "github.com/CyrusF/libsvm-go"
8 |
9 | "bytes"
10 | "os"
11 | "regexp"
12 |
13 | "github.com/glaslos/ssdeep"
14 | )
15 |
16 | /*
17 | WebshellDetector - Refactor version 1
18 | Date 0814
19 | Author Cyrus, Twice
20 | Intro Read model from file
21 | Give an interface for predict
22 | */
23 |
24 | type DetectorConfig struct {
25 | modelPath string
26 | configPath string
27 | enableProcessor bool
28 | enableRegMatcher bool
29 | enableSampleMather bool
30 | enableStatCheck bool
31 | }
32 |
33 | func NewDetectorConfig() (*DetectorConfig, error) {
34 | return &DetectorConfig{"", "", true, true, true, true}, nil
35 | }
36 |
37 | type Detector struct {
38 | opSerialModel *libSvm.Model
39 | processModel *libSvm.Model
40 | wordsModel *bayesian.Classifier
41 | regMatcher *regMatcher
42 | sampleMatcher *sampleMatcher
43 | hashState arrayHashState
44 | statState arrayStatState
45 | stdin *os.File
46 | stdout *os.File
47 | config *DetectorConfig
48 | }
49 |
50 | func NewDetectorWithConfig(config *DetectorConfig, opSerialStream []byte, processorStream []byte, wordStream []byte, hashStateBytes []byte, statStateBytes []byte, regMatcherStream []byte, sampleMatcherStream []byte, stdin *os.File, stdout *os.File) (*Detector, error) {
51 | var self Detector
52 | var err error
53 |
54 | self.config = config
55 | opSerialStreamReader := bytes.NewReader(opSerialStream)
56 | self.opSerialModel = libSvm.NewModelFromFileStream(opSerialStreamReader)
57 | processorStreamReader := bytes.NewReader(processorStream)
58 | self.processModel = libSvm.NewModelFromFileStream(processorStreamReader)
59 |
60 | wordStreamReader := bytes.NewReader(wordStream)
61 | tmpWordsModel, _ := bayesian.NewClassifierFromFileStream(wordStreamReader)
62 | self.wordsModel = &tmpWordsModel // make all struct element to be pointer
63 |
64 | self.hashState = newArrayHashState()
65 | self.hashState.Load(hashStateBytes)
66 |
67 | self.statState = newArrayStatState()
68 | err = self.statState.Load(statStateBytes)
69 | if err != nil {
70 | return nil, err
71 | }
72 |
73 | regMatcherStreamReader := bytes.NewReader(regMatcherStream)
74 | self.regMatcher, err = newRegMatcher(regMatcherStreamReader)
75 | if err != nil {
76 | return nil, err
77 | }
78 |
79 | sampleMatcherStreamReader := bytes.NewReader(sampleMatcherStream)
80 | self.sampleMatcher, err = newSampleMatcher(sampleMatcherStreamReader)
81 | if err != nil {
82 | return nil, err
83 | }
84 |
85 | self.stdin = stdin
86 | self.stdout = stdout
87 | return &self, nil
88 | }
89 |
90 | func NewDetector(opSerialStream []byte, processorStream []byte, wordStream []byte, hashStateBytes []byte, statStateBytes []byte, regMatcherStream []byte, sampleMatcherStream []byte, stdin *os.File, stdout *os.File) (*Detector, error) {
91 | if config, err := NewDetectorConfig(); err == nil {
92 | return NewDetectorWithConfig(config, opSerialStream, processorStream, wordStream, hashStateBytes, statStateBytes, regMatcherStream, sampleMatcherStream, stdin, stdout)
93 | } else {
94 | return nil, err
95 | }
96 | }
97 |
98 | func NewDefaultDetector(stdin *os.File, stdout *os.File) (*Detector, error) {
99 | opSerialStream, err := Asset("static/model-latest/OpSerial.model")
100 | if err != nil {
101 | log.Fatal(err)
102 | }
103 | processorStream, err := Asset("static/model-latest/Processor.model")
104 | if err != nil {
105 | log.Fatal(err)
106 | }
107 | wordStream, err := Asset("static/model-latest/Words.model")
108 | if err != nil {
109 | log.Fatal(err)
110 | }
111 | hashStateBytes, err := Asset("static/model-latest/hashState.json")
112 | if err != nil {
113 | log.Fatal(err)
114 | }
115 | statStateBytes, err := Asset("static/config/statState.json")
116 | if err != nil {
117 | log.Fatal(err)
118 | }
119 | regMatcherStream, err := Asset("static/config/rules.conf")
120 | if err != nil {
121 | log.Fatal(err)
122 | }
123 | sampleMatcherStream, err := Asset("static/model-latest/SampleHash.txt")
124 | if err != nil {
125 | log.Fatal(err)
126 | }
127 |
128 | return NewDetector(opSerialStream, processorStream, wordStream, hashStateBytes, statStateBytes, regMatcherStream, sampleMatcherStream, stdin, stdout)
129 | }
130 |
131 | func (self Detector) Predict(src []byte) (int, error) {
132 |
133 | isPhpCode := func(src []byte) bool {
134 | return bytes.Contains(src, []byte("")) || regexp.MustCompile(` |