From 285bded8c193226d9d26792950148f00c723412b Mon Sep 17 00:00:00 2001 From: hupe1980 Date: Sun, 19 Dec 2021 15:19:47 +0100 Subject: [PATCH] Add CVE-2021-45105 support --- Makefile | 2 +- README.md | 12 ++++- cmd/local.go | 23 +++++---- cmd/remote_cidr.go | 2 +- cmd/remote_url.go | 2 +- internal/local.go | 126 ++++++++++++++++++++++++++++++++++----------- 6 files changed, 122 insertions(+), 45 deletions(-) diff --git a/Makefile b/Makefile index 0129cb9..ed414ed 100644 --- a/Makefile +++ b/Makefile @@ -25,7 +25,7 @@ test: .PHONY: run run: - @go run *.go remote url -h + @go run *.go local -h .PHONY: run-local run-local: diff --git a/README.md b/README.md index 86a51dd..7b686fd 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,8 @@ CVE-2021-44228 is a remote code execution (RCE) vulnerability in Apache Log4j 2. - Domain Name Service (DNS) :warning: There is a patch bypass on Log4J v2.15.0: [CVE-2021-45046](https://nvd.nist.gov/vuln/detail/CVE-2021-45046) + +:warning: Log4J v2.16 High Severity Vulnerability discovered: [CVE-2021-45105](https://nvd.nist.gov/vuln/detail/CVE-2021-45105) ## Installing You can install the pre-compiled binary in several different ways @@ -70,9 +72,10 @@ Usage: scan4log4shell local [paths] [flags] Flags: - --check-cve-2021-45046 check for CVE-2021-45046 -e, --exclude stringArray path to exclude -h, --help help for local + --ignore-cve-2021-45046 ignore CVE-2021-45046 + --ignore-cve-2021-45105 ignore CVE-2021-45105 --ignore-ext stringArray ignore .jar | .zip | .war | .ear | .aar --ignore-v1 ignore log4j 1.x versions --max-threads int max number of concurrent threads (default 5) @@ -97,12 +100,17 @@ scanner_1 | [i] Inspecting /walk/apache-log4j-2.14.0-bin/log4j-api-2.14.0-javad scanner_1 | [i] Inspecting /walk/apache-log4j-2.14.0-bin/log4j-api-2.14.0-sources.jar... scanner_1 | [i] Inspecting /walk/apache-log4j-2.14.0-bin/log4j-api-2.14.0.jar... scanner_1 | [i] Inspecting /walk/apache-log4j-2.14.0-bin/log4j-core-2.14.0.jar... -scanner_1 | [!] Hit: possibly vulnerable file identified: /walk/apache-log4j-2.14.0-bin/log4j-core-2.14.0.jar +scanner_1 | [!] Hit: possibly CVE-2021-45046 vulnerable file identified: /walk/apache-log4j-2.14.0-bin/log4j-core-2.14.0.jar +scanner_1 | [!] Hit: possibly CVE-2021-45105 vulnerable file identified: /walk/apache-log4j-2.14.0-bin/log4j-core-2.14.0.jar +scanner_1 | [!] Hit: possibly CVE-2021-44228 vulnerable file identified: /walk/apache-log4j-2.14.0-bin/log4j-core-2.14.0.jar scanner_1 | [i] Inspecting /walk/apache-log4j-2.15.0-bin/log4j-api-2.15.0.jar... scanner_1 | [i] Inspecting /walk/apache-log4j-2.15.0-bin/log4j-core-2.15.0.jar... +scanner_1 | [!] Hit: possibly CVE-2021-45046 vulnerable file identified: /walk/apache-log4j-2.15.0-bin/log4j-core-2.15.0.jar +scanner_1 | [!] Hit: possibly CVE-2021-45105 vulnerable file identified: /walk/apache-log4j-2.15.0-bin/log4j-core-2.15.0.jar scanner_1 | [i] Inspecting /walk/apache-log4j-2.15.0-bin/log4j-spring-boot-2.15.0.jar... scanner_1 | [i] Inspecting /walk/apache-log4j-2.16.0-bin/log4j-api-2.16.0.jar... scanner_1 | [i] Inspecting /walk/apache-log4j-2.16.0-bin/log4j-core-2.16.0.jar... +scanner_1 | [!] Hit: possibly CVE-2021-45105 vulnerable file identified: /walk/apache-log4j-2.16.0-bin/log4j-core-2.16.0.jar scanner_1 | [i] Inspecting /walk/jakarta-log4j-1.2.8/dist/lib/log4j-1.2.8.jar... scanner_1 | [!] Hit: log4j V1 identified: /walk/jakarta-log4j-1.2.8/dist/lib/log4j-1.2.8.jar scanner_1 | [i] Completed scanning diff --git a/cmd/local.go b/cmd/local.go index d63733e..2ca01a3 100644 --- a/cmd/local.go +++ b/cmd/local.go @@ -13,11 +13,12 @@ import ( ) type localOptions struct { - excludes []string - ignoreExts []string - ignoreV1 bool - maxThreads int - checkCVE2021_45046 bool + excludes []string + ignoreExts []string + ignoreV1 bool + ignoreCVE2021_45046 bool + ignoreCVE2021_45105 bool + maxThreads int } func newLocalCmd(noColor *bool, output *string, verbose *bool) *cobra.Command { @@ -51,12 +52,13 @@ func newLocalCmd(noColor *bool, output *string, verbose *bool) *cobra.Command { var wg sync.WaitGroup sem := semaphore.NewWeighted(int64(opts.maxThreads)) - printInfo("Log4Shell CVE-2021-44228 Local Vulnerability Scan") + printInfo("Log4Shell Local Vulnerability Scan") scanner := internal.NewLocalScanner(&internal.LocalOptions{ - Excludes: opts.excludes, - IgnoreExts: opts.ignoreExts, - CheckCVE2021_45046: opts.checkCVE2021_45046, + Excludes: opts.excludes, + IgnoreExts: opts.ignoreExts, + IgnoreCVE2021_45046: opts.ignoreCVE2021_45046, + IgnoreCVE2021_45105: opts.ignoreCVE2021_45105, }) go func() { @@ -106,7 +108,8 @@ func newLocalCmd(noColor *bool, output *string, verbose *bool) *cobra.Command { } cmd.Flags().BoolVarP(&opts.ignoreV1, "ignore-v1", "", false, "ignore log4j 1.x versions") - cmd.Flags().BoolVarP(&opts.checkCVE2021_45046, "check-cve-2021-45046", "", false, "check for CVE-2021-45046") + cmd.Flags().BoolVarP(&opts.ignoreCVE2021_45046, "ignore-cve-2021-45046", "", false, "ignore CVE-2021-45046") + cmd.Flags().BoolVarP(&opts.ignoreCVE2021_45105, "ignore-cve-2021-45105", "", false, "ignore CVE-2021-45105") cmd.Flags().StringArrayVarP(&opts.ignoreExts, "ignore-ext", "", []string{}, "ignore .jar | .zip | .war | .ear | .aar") cmd.Flags().StringArrayVarP(&opts.excludes, "exclude", "e", []string{}, "path to exclude") cmd.Flags().IntVarP(&opts.maxThreads, "max-threads", "", 5, "max number of concurrent threads") diff --git a/cmd/remote_cidr.go b/cmd/remote_cidr.go index a19f8a1..924df6d 100644 --- a/cmd/remote_cidr.go +++ b/cmd/remote_cidr.go @@ -51,7 +51,7 @@ func newRemoteCIDRCmd(noColor *bool, output *string, verbose *bool) *cobra.Comma cidr := args[0] - printInfo("Log4Shell CVE-2021-44228 Remote Vulnerability Scan") + printInfo("Log4Shell Remote Vulnerability Scan") ctx, cancel := context.WithCancel(context.Background()) defer cancel() diff --git a/cmd/remote_url.go b/cmd/remote_url.go index 79f4bab..1d9cd0f 100644 --- a/cmd/remote_url.go +++ b/cmd/remote_url.go @@ -48,7 +48,7 @@ func newRemoteURLCmd(noColor *bool, output *string, verbose *bool) *cobra.Comman color.NoColor = true } - printInfo("Log4Shell CVE-2021-44228 Remote Vulnerability Scan") + printInfo("Log4Shell Remote Vulnerability Scan") ctx, cancel := context.WithCancel(context.Background()) defer cancel() diff --git a/internal/local.go b/internal/local.go index eefcf08..a80f857 100644 --- a/internal/local.go +++ b/internal/local.go @@ -12,10 +12,11 @@ import ( ) type LocalOptions struct { - Excludes []string - IgnoreExts []string - IgnoreV1 bool - CheckCVE2021_45046 bool + Excludes []string + IgnoreExts []string + IgnoreV1 bool + IgnoreCVE2021_45046 bool + IgnoreCVE2021_45105 bool } type LocalScanner struct { opts *LocalOptions @@ -88,12 +89,63 @@ func (ls *LocalScanner) ArchieveWalk(root string, fn func(path string, ra io.Rea } type localScanState struct { - isLog4J1 bool - log4jIndicator int - hasJndiLookup bool - hasJndiManager bool - hasCVE2021_44228VulnJndiManager bool - hasCVE2021_45046VulnJndiManager bool + isLog4J1 bool + log4j2Confidence int + isLog4JGE2_10 bool + isLog4J2_12_2 bool + isLog4J2_15 bool + isLog4J2_16 bool + isLog4J2_17 bool + hasJndiLookup bool + hasJndiManager bool +} + +func (ls *localScanState) IsLog4J1() bool { + return ls.isLog4J1 +} + +func (ls *localScanState) IsLog4j2_12() bool { + return ls.isLog4JGE2_10 && ls.isLog4J2_12_2 +} + +func (ls *localScanState) IsLog4J2() bool { + return ls.log4j2Confidence >= 5 +} + +func (ls *localScanState) Version() string { + if ls.IsLog4J1() { + return "log4j V1.x" + } + + if ls.IsLog4J2() && !ls.isLog4JGE2_10 { + return "log4j >= V2.0-beta9 and < V2.10.0" + } + + return "" +} + +func (ls *localScanState) HasCVE2021_44228() bool { + if !ls.IsLog4J2() { + return false + } + + if ls.IsLog4j2_12() || ls.isLog4J2_15 || ls.isLog4J2_16 || ls.isLog4J2_17 { + return false + } + + return ls.hasJndiLookup +} + +func (ls *localScanState) HasCVE2021_45046() bool { + if ls.IsLog4J1() || ls.IsLog4j2_12() || ls.isLog4J2_16 || ls.isLog4J2_17 { + return false + } + + return ls.IsLog4J2() +} + +func (ls *localScanState) HasCVE2021_45105() bool { + return ls.IsLog4J2() && !ls.isLog4J2_17 } func (ls *LocalScanner) InspectJar(path string, ra io.ReaderAt, sz int64, opts *LocalOptions) { @@ -105,23 +157,40 @@ func (ls *LocalScanner) InspectJar(path string, ra io.ReaderAt, sz int64, opts * state := localScanState{} -LOOP: + // Fingerprints were taken from https://github.com/mergebase/log4j-detector for _, file := range zr.File { switch strings.ToLower(filepath.Ext(file.Name)) { case ".class": if hasSuffix(file.Name, "log4j/DailyRollingFileAppender.class") { state.isLog4J1 = true - break LOOP + break + } + + if hasSuffix(file.Name, "core/appender/nosql/NoSqlAppender.class") { + state.isLog4JGE2_10 = true + continue } if hasSuffix(file.Name, "core/lookup/JndiLookup.class") { state.hasJndiLookup = true + + buf, err := readArchiveMember(file) + if err != nil { + ls.errsChan <- fmt.Errorf("cannot read JAR file member: %s (%s): %v", path, file.Name, err) + return + } + + if bytes.Contains(buf, []byte("JNDI must be enabled by setting log4j2.enableJndiLookup=true")) { // v2.17.0 + state.isLog4J2_17 = true + } else if !bytes.Contains(buf, []byte("Error looking up JNDI resource [{}].")) { + state.isLog4J2_12_2 = true + } + continue } if hasSuffix(file.Name, "core/net/JndiManager.class") { state.hasJndiManager = true - state.hasCVE2021_44228VulnJndiManager = true buf, err := readArchiveMember(file) if err != nil { @@ -130,37 +199,36 @@ LOOP: } if bytes.Contains(buf, []byte("log4j2.enableJndi")) { // v2.16.0 - state.hasCVE2021_44228VulnJndiManager = false - state.hasCVE2021_45046VulnJndiManager = false + state.isLog4J2_16 = true } else if bytes.Contains(buf, []byte("Invalid JNDI URI - {}")) { // v2.15.0 - state.hasCVE2021_44228VulnJndiManager = false - state.hasCVE2021_45046VulnJndiManager = true + state.isLog4J2_15 = true } + continue } if hasSuffix(file.Name, "core/LogEvent.class") { - state.log4jIndicator++ + state.log4j2Confidence++ continue } if hasSuffix(file.Name, "core/Appender.class") { - state.log4jIndicator++ + state.log4j2Confidence++ continue } if hasSuffix(file.Name, "core/Filter.class") { - state.log4jIndicator++ + state.log4j2Confidence++ continue } if hasSuffix(file.Name, "core/Layout.class") { - state.log4jIndicator++ + state.log4j2Confidence++ continue } if hasSuffix(file.Name, "core/LoggerContext.class") { - state.log4jIndicator++ + state.log4j2Confidence++ continue } case ".jar", ".war", ".ear", ".zip", ".aar": @@ -174,22 +242,20 @@ LOOP: } } - if !opts.IgnoreV1 && state.isLog4J1 { + if !opts.IgnoreV1 && state.IsLog4J1() { ls.hitsChan <- fmt.Sprintf("log4j V1 identified: %s", absFilepath(path)) return } - if state.hasJndiLookup && state.hasCVE2021_44228VulnJndiManager { - ls.hitsChan <- fmt.Sprintf("possibly CVE-2021-44228 vulnerable file identified: %s", absFilepath(path)) - return + if !opts.IgnoreCVE2021_45046 && state.HasCVE2021_45046() { + ls.hitsChan <- fmt.Sprintf("possibly CVE-2021-45046 vulnerable file identified: %s", absFilepath(path)) } - if ls.opts.CheckCVE2021_45046 && state.hasCVE2021_45046VulnJndiManager { - ls.hitsChan <- fmt.Sprintf("possibly CVE-2021-45046 vulnerable file identified: %s", absFilepath(path)) - return + if !opts.IgnoreCVE2021_45105 && state.HasCVE2021_45105() { + ls.hitsChan <- fmt.Sprintf("possibly CVE-2021-45105 vulnerable file identified: %s", absFilepath(path)) } - if state.log4jIndicator > 4 && state.hasJndiLookup && !state.hasJndiManager { + if state.HasCVE2021_44228() { ls.hitsChan <- fmt.Sprintf("possibly CVE-2021-44228 vulnerable file identified: %s", absFilepath(path)) return }