Skip to content

Commit

Permalink
Merge pull request #342 from lunasec-io/bump-log4shell-cli-version
Browse files Browse the repository at this point in the history
bump cli version to 1.4.0

Former-commit-id: 712a040
Former-commit-id: dd5cb3f97e01e268e37209fcfda4a2b7077ff9c1
  • Loading branch information
breadchris authored Dec 17, 2021
2 parents 20282f2 + 88ae4ed commit 92b0c5a
Show file tree
Hide file tree
Showing 19 changed files with 1,088 additions and 230 deletions.
2 changes: 2 additions & 0 deletions docs/blog/2021-12-12-log4j-zero-day-mitigation-guide.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,8 @@ log4shell scan your-project-dir/
```shell title="Example Windows Command"
log4shell.exe scan your-project-dir/
```
Because the tool contains exploit strings needed for the `livepatch` command, it might be falsely recognized as malware by some
virus scanners on Windows. Please add an exception for it.

**Example Output**
```shell
Expand Down
15 changes: 15 additions & 0 deletions tools/log4shell/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,13 @@ You can disable these by passing `--ignore-warnings`.
$ log4shell scan --ignore-warnings <dir1> <dir2> ...
```

It can be common to run into symlink'ed jar files, and by default they are resolved. To not have this happen
use the `--no-follow-symlinks` flag.

```shell
$ log4shell scan --no-follow-symlinks <dir1> <dir2> ...
```

You may exclude subdirectories while searching by using `--exclude`. This can be used multiple times in the command to
exclude multiple subdirectories.

Expand Down Expand Up @@ -81,3 +88,11 @@ make build && ./log4shell
## Releases

Find the compiled tool for your OS [here](https://github.com/lunasec-io/lunasec/releases/).


## How to manually release to github
```shell
git tag -a v<VERSION>-log4shell -m "<RELEASE NAME>"
git push origin v<VERSION>-log4shell
GITHUB_TOKEN=<GITHUB_PERSONAL_ACCESS_TOKEN> goreleaser release --rm-dist
```
107 changes: 59 additions & 48 deletions tools/log4shell/analyze/analyze.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,34 +22,23 @@ import (
"github.com/rs/zerolog/log"
"io"
"path"
"regexp"
"strings"
)

func isVersionALog4ShellVersion(semverVersion string) bool {
version, _ := semver.Make(semverVersion)
var alphaRegex = regexp.MustCompile("([a-z]+)")

vulnerableRange, _ := semver.ParseRange(">=2.0.0-beta9 <=2.14.1")
if vulnerableRange(version) {
return true
}
return false
}

func isVersionACVE202145046Version(semverVersion string) bool {
version, _ := semver.Make(semverVersion)

vulnerableRange, _ := semver.ParseRange("=2.15.0")
if vulnerableRange(version) {
return true
func versionIsInRange(fileName string, semverVersion string, semverRange semver.Range) bool {
version, err := semver.Make(semverVersion)
if err != nil {
log.Warn().
Str("fileName", fileName).
Str("semverVersion", semverVersion).
Msg("Unable to parse semver version")
return false
}
return false
}

func isVersionACVE201917571Version(semverVersion string) bool {
version, _ := semver.Make(semverVersion)

vulnerableRange, _ := semver.ParseRange(">=1.2.0 <=1.2.17")
if vulnerableRange(version) {
if semverRange(version) {
return true
}
return false
Expand All @@ -68,39 +57,61 @@ func adjustMissingPatchVersion(semverVersion string) string {
return semverVersion
}

func fileNameToSemver(fileNameNoExt string) string {
fileNameParts := strings.Split(fileNameNoExt, "-")

var tag, semverVersion string
for i := len(fileNameParts) - 1; i >= 0; i-- {
fileNamePart := fileNameParts[i]
if (
strings.HasPrefix(fileNamePart, "1") ||
strings.HasPrefix(fileNamePart, "2")) &&
strings.Contains(fileNamePart, ".") {

tagPart := alphaRegex.FindString(fileNamePart)
if tagPart != "" {
fileNamePart = strings.Replace(fileNamePart, tagPart, "", 1)
if tag == "" {
tag = tagPart
} else {
tag = tagPart + "-" + tag
}
}

fileNamePart = adjustMissingPatchVersion(fileNamePart)

if tag == "" {
semverVersion = fileNamePart
break
}
semverVersion = fileNamePart + "-" + tag
break
}
if tag == "" {
tag = fileNamePart
continue
}
tag = fileNamePart + "-" + tag
}
return semverVersion
}

func ProcessArchiveFile(reader io.Reader, filePath, fileName string) (finding *types.Finding) {
_, file := path.Split(filePath)
version := strings.TrimSuffix(file, path.Ext(file))
fileNameNoExt := strings.TrimSuffix(file, path.Ext(file))

// small adjustments to the version so that it can be parsed as semver
semverVersion := strings.Replace(version, "log4j-core-", "", -1)
semverVersion = strings.Replace(semverVersion, "logging-log4j-", "", -1)
semverVersion = strings.Replace(semverVersion, "jakarta-log4j-", "", -1)
semverVersion = strings.Replace(semverVersion, "log4j-", "", -1)

semverVersion = adjustMissingPatchVersion(semverVersion)
semverVersion := fileNameToSemver(fileNameNoExt)

versionCve := ""

if isVersionALog4ShellVersion(semverVersion) {
if !strings.Contains(fileName, "JndiLookup.class") {
return
}
versionCve = constants.Log4ShellCve
}

if isVersionACVE202145046Version(semverVersion) {
if !strings.Contains(fileName, "JndiManager$JndiManagerFactory.class") {
return
}
versionCve = constants.CtxCve
}

if isVersionACVE201917571Version(semverVersion) {
if !strings.Contains(fileName, "SocketNode.class") {
return
for _, fileVersionCheck := range constants.FileVersionChecks {
if versionIsInRange(fileNameNoExt, semverVersion, fileVersionCheck.SemverRange) {
if !strings.Contains(fileName, fileVersionCheck.LibraryFile) {
return
}
versionCve = fileVersionCheck.Cve
}
versionCve = constants.Log4j1RceCve
}

if versionCve == "" {
Expand All @@ -126,7 +137,7 @@ func ProcessArchiveFile(reader io.Reader, filePath, fileName string) (finding *t
if versionCve == "" {
log.Debug().
Str("hash", fileHash).
Str("version", version).
Str("version", semverVersion).
Msg("Skipping version as it is not vulnerable to any known CVE")
return nil
}
Expand Down
6 changes: 3 additions & 3 deletions tools/log4shell/commands/analyze.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,14 @@ import (
"github.com/urfave/cli/v2"
)

func AnalyzeCommand(c *cli.Context) error {
enableGlobalFlags(c)
func AnalyzeCommand(c *cli.Context, globalBoolFlags map[string]bool) error {
enableGlobalFlags(c, globalBoolFlags)

searchDirs := c.Args().Slice()

processArchiveFile := analyze.ProcessArchiveFile

scanner := scan.NewLog4jDirectoryScanner([]string{}, false, processArchiveFile)
scanner := scan.NewLog4jDirectoryScanner([]string{}, false, false, processArchiveFile)

scannerFindings := scanner.Scan(searchDirs)

Expand Down
36 changes: 30 additions & 6 deletions tools/log4shell/commands/flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,11 @@ import (
"os"
)

func enableGlobalFlags(c *cli.Context) {
verbose := c.Bool("verbose")
ignoreWarnings := c.Bool("ignore-warnings")
debug := c.Bool("debug")
func enableGlobalFlags(c *cli.Context, globalBoolFlags map[string]bool) {
verbose := globalBoolFlags["verbose"]
debug := globalBoolFlags["debug"]
jsonFlag := globalBoolFlags["json"]
ignoreWarnings := globalBoolFlags["ignore-warnings"]

if verbose || debug {
zerolog.SetGlobalLevel(zerolog.DebugLevel)
Expand All @@ -41,13 +42,36 @@ func enableGlobalFlags(c *cli.Context) {
log.Logger = log.With().Caller().Logger()
}

jsonFlag := c.Bool("json")
if !jsonFlag {
// pretty print output to the console if we are not interested in parsable output
consoleOutput := zerolog.ConsoleWriter{Out: os.Stderr}
consoleOutput := zerolog.ConsoleWriter{Out: os.Stdout}
consoleOutput.FormatFieldName = func(i interface{}) string {
return fmt.Sprintf("\n\t%s: ", util.Colorize(constants.ColorBlue, i))
}

consoleOutput.FormatLevel = func(i interface{}) string {
if i == nil {
return util.Colorize(constants.ColorBold,"Scan Result:")
}

level := i.(string)

var formattedLevel string
switch level {
case "warn":
formattedLevel = util.Colorize(constants.ColorYellow, level)
case "error":
formattedLevel = util.Colorize(constants.ColorRed, level)
case "info":
formattedLevel = util.Colorize(constants.ColorBlue, level)
case "debug":
formattedLevel = util.Colorize(constants.ColorGreen, level)
default:
formattedLevel = util.Colorize(constants.ColorWhite, level)
}
return fmt.Sprintf("| %s |", formattedLevel)
}

log.Logger = log.Output(consoleOutput)

}
Expand Down
4 changes: 2 additions & 2 deletions tools/log4shell/commands/livepatch.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@ import (
"github.com/urfave/cli/v2"
)

func LivePatchCommand(c *cli.Context, hotpatchFiles embed.FS) error {
enableGlobalFlags(c)
func LivePatchCommand(c *cli.Context, globalBoolFlags map[string]bool, hotpatchFiles embed.FS) error {
enableGlobalFlags(c, globalBoolFlags)

payloadUrl := c.String("payload-url")
ldapHost := c.String("ldap-host")
Expand Down
14 changes: 10 additions & 4 deletions tools/log4shell/commands/scan.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,11 @@ import (
"github.com/urfave/cli/v2"
)

func loadHashLookup(log4jLibraryHashes []byte, versionHashes string, onlyScanArchives bool) (hashLookup types.VulnerableHashLookup, err error) {
func loadHashLookup(
log4jLibraryHashes []byte,
versionHashes string,
onlyScanArchives bool,
) (hashLookup types.VulnerableHashLookup, err error) {
if versionHashes != "" {
hashLookup, err = scan.LoadVersionHashesFromFile(versionHashes)
if err != nil {
Expand All @@ -47,8 +51,8 @@ func loadHashLookup(log4jLibraryHashes []byte, versionHashes string, onlyScanArc
return
}

func ScanCommand(c *cli.Context, log4jLibraryHashes []byte) (err error) {
enableGlobalFlags(c)
func ScanCommand(c *cli.Context, globalBoolFlags map[string]bool, log4jLibraryHashes []byte) (err error) {
enableGlobalFlags(c, globalBoolFlags)

searchDirs := c.Args().Slice()
log.Debug().
Expand All @@ -59,6 +63,7 @@ func ScanCommand(c *cli.Context, log4jLibraryHashes []byte) (err error) {
onlyScanArchives := c.Bool("archives")
excludeDirs := c.StringSlice("exclude")
versionHashes := c.String("version-hashes")
noFollowSymlinks := c.Bool("no-follow-symlinks")

hashLookup, err := loadHashLookup(log4jLibraryHashes, versionHashes, onlyScanArchives)
if err != nil {
Expand All @@ -67,7 +72,8 @@ func ScanCommand(c *cli.Context, log4jLibraryHashes []byte) (err error) {

processArchiveFile := scan.IdentifyPotentiallyVulnerableFiles(scanLog4j1, hashLookup)

scanner := scan.NewLog4jDirectoryScanner(excludeDirs, onlyScanArchives, processArchiveFile)
scanner := scan.NewLog4jDirectoryScanner(
excludeDirs, onlyScanArchives, noFollowSymlinks, processArchiveFile)

scannerFindings := scanner.Scan(searchDirs)

Expand Down
2 changes: 1 addition & 1 deletion tools/log4shell/constants/version.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,4 @@
//
package constants

const Version = "1.3.1"
const Version = "1.4.0"
Loading

0 comments on commit 92b0c5a

Please sign in to comment.