mirror of
https://github.com/aaron-kidwell/goLoL
synced 2026-06-21 13:46:44 +00:00
added ability to scan local drivers against the LOLDrivers catalog
This commit is contained in:
@@ -28,6 +28,7 @@
|
||||
- **Privilege-aware filtering** — shows only techniques runnable at your current tier
|
||||
- **MITRE ATT&CK labels** — technique IDs mapped to readable names (e.g. `T1003.003: NTDS`)
|
||||
- **Flexible sorting** — group by binary, privilege tier, or ATT&CK technique
|
||||
- **Driver mode** — hashes local `.sys` files and matches against the live [LOLDrivers](https://www.loldrivers.io/) JSON catalog
|
||||
- **Plain output mode** — ASCII-only output for telnet, reverse shells, and other unstable terminals
|
||||
- **Lightweight scanning** — filesystem checks via Go APIs; admin-group detection uses `net localgroup` (one child process on Windows)
|
||||
|
||||
@@ -87,6 +88,7 @@ go build -ldflags="-s -w" -trimpath -o golol.exe .
|
||||
| Flag | Description |
|
||||
|---|---|
|
||||
| `-h`, `-help` | Show help |
|
||||
| `-driver` | Scan local drivers and list known vulnerable/malicious matches from LOLDrivers |
|
||||
| `-plain` | ASCII-only output — no colors, Unicode, or cursor control |
|
||||
| `-s`, `-search` | Show one binary by name (`certutil` or `certutil.exe`); reports if not on disk |
|
||||
| `-sort` | Sort results: `binary` (default), `privilege`, or `attack` |
|
||||
@@ -99,6 +101,9 @@ Sort aliases: `b`, `priv` / `p`, `mitre` / `a`. Invalid values print an error an
|
||||
# Default — grouped by binary name (A–Z)
|
||||
go run .
|
||||
|
||||
# Driver mode (scan local .sys files against LOLDrivers hashes)
|
||||
go run . -driver
|
||||
|
||||
# Look up a single binary
|
||||
go run . -s certutil
|
||||
.\golol.exe -s certutil.exe
|
||||
@@ -142,14 +147,16 @@ Techniques: 299
|
||||
## How it works
|
||||
|
||||
1. Detects the current process privilege context (standard user, local admin group member, or SYSTEM).
|
||||
2. Downloads and parses the LOLBAS JSON catalog.
|
||||
3. For each entry, remaps documented paths to the local filesystem and checks whether the binary exists.
|
||||
2. In default mode, downloads and parses the LOLBAS JSON catalog.
|
||||
3. For each LOLBAS entry, remaps documented paths to the local filesystem and checks whether the binary exists.
|
||||
4. Filters commands by privilege tier and deduplicates by resolved on-disk path.
|
||||
5. Prints results with paths, ATT&CK technique, use case, and example command.
|
||||
5. In `-driver` mode, downloads the LOLDrivers JSON catalog, hashes local `.sys` files, and reports hash matches.
|
||||
6. Prints results with paths, ATT&CK technique, use case, and example command (or driver match metadata in `-driver` mode).
|
||||
|
||||
| Component | Location |
|
||||
|---|---|
|
||||
| LOLBAS catalog | `https://lolbas-project.github.io/api/lolbas.json` |
|
||||
| LOLDrivers catalog | `https://www.loldrivers.io/api/drivers.json` |
|
||||
| Privilege detection | `internal/privileges` |
|
||||
| MITRE technique names | `internal/mitre` |
|
||||
| Path resolution & output | `main.go` |
|
||||
|
||||
@@ -1,11 +1,17 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/csv"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"sort"
|
||||
@@ -35,6 +41,53 @@ type lolbasEntry struct {
|
||||
Commands []lolbasCommand `json:"Commands"`
|
||||
}
|
||||
|
||||
type lolDriverSample struct {
|
||||
Filename string `json:"Filename"`
|
||||
Description string `json:"Description"`
|
||||
SHA256 string `json:"SHA256"`
|
||||
SHA1 string `json:"SHA1"`
|
||||
MD5 string `json:"MD5"`
|
||||
}
|
||||
|
||||
type lolDriverCommand struct {
|
||||
Command string `json:"Command"`
|
||||
Description string `json:"Description"`
|
||||
Usecase string `json:"Usecase"`
|
||||
Privileges string `json:"Privileges"`
|
||||
OperatingSystem string `json:"OperatingSystem"`
|
||||
}
|
||||
|
||||
type lolDriverEntry struct {
|
||||
ID string `json:"Id"`
|
||||
Category string `json:"Category"`
|
||||
Verified string `json:"Verified"`
|
||||
Commands lolDriverCommand `json:"Commands"`
|
||||
Resources []string `json:"Resources"`
|
||||
KnownVulnerableSamples []lolDriverSample `json:"KnownVulnerableSamples"`
|
||||
}
|
||||
|
||||
type localDriver struct {
|
||||
Path string
|
||||
Filename string
|
||||
SHA256 string
|
||||
}
|
||||
|
||||
type driverMatch struct {
|
||||
Path string
|
||||
Filename string
|
||||
SHA256 string
|
||||
Category string
|
||||
Verified string
|
||||
ID string
|
||||
Why string
|
||||
ExampleCommand string
|
||||
Usecase string
|
||||
Privileges string
|
||||
OS string
|
||||
Resource string
|
||||
Loaded string
|
||||
}
|
||||
|
||||
func resolveLocalPath(documented string) string {
|
||||
p := filepath.FromSlash(documented)
|
||||
lower := strings.ToLower(p)
|
||||
@@ -350,6 +403,7 @@ Usage:
|
||||
|
||||
Flags:
|
||||
-h, -help Show this help
|
||||
-driver Scan local drivers against the LOLDrivers catalog
|
||||
-plain ASCII-only output for telnet/reverse shells
|
||||
-s, -search string Search for one binary (e.g. certutil or certutil.exe)
|
||||
-sort string Sort results (default "binary")
|
||||
@@ -359,13 +413,14 @@ Flags:
|
||||
|
||||
Examples:
|
||||
%s
|
||||
%s -driver
|
||||
%s -s certutil
|
||||
%s -plain
|
||||
%s -sort privilege
|
||||
%s -sort attack
|
||||
%s -h
|
||||
|
||||
`, exe, exe, exe, exe, exe, exe, exe))
|
||||
`, exe, exe, exe, exe, exe, exe, exe, exe))
|
||||
return
|
||||
}
|
||||
fmt.Printf(`%sLists LOLBAS binaries present on this machine that match your privilege level,
|
||||
@@ -380,6 +435,7 @@ when running as SYSTEM.
|
||||
|
||||
%sFlags:%s
|
||||
-h, -help Show this help
|
||||
-driver Scan local drivers against the LOLDrivers catalog
|
||||
-plain ASCII-only output for telnet/reverse shells
|
||||
-s, -search string Search for one binary (e.g. certutil or certutil.exe)
|
||||
-sort string Sort results (default "binary")
|
||||
@@ -389,13 +445,329 @@ when running as SYSTEM.
|
||||
|
||||
%sExamples:%s
|
||||
%s
|
||||
%s -driver
|
||||
%s -s certutil
|
||||
%s -plain
|
||||
%s -sort privilege
|
||||
%s -sort attack
|
||||
%s -h
|
||||
|
||||
`, colorDim, colorBold, colorReset, exe, colorBold, colorReset, colorBold, colorReset, exe, exe, exe, exe, exe, exe)
|
||||
`, colorDim, colorBold, colorReset, exe, colorBold, colorReset, colorBold, colorReset, exe, exe, exe, exe, exe, exe, exe)
|
||||
}
|
||||
|
||||
func fetchLOLDriversCatalog() ([]lolDriverEntry, error) {
|
||||
resp, err := http.Get("https://www.loldrivers.io/api/drivers.json")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("unexpected status: %s", resp.Status)
|
||||
}
|
||||
|
||||
var entries []lolDriverEntry
|
||||
if err := json.NewDecoder(resp.Body).Decode(&entries); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return entries, nil
|
||||
}
|
||||
|
||||
func driverSearchRoots() []string {
|
||||
windir := os.Getenv("WINDIR")
|
||||
if windir == "" {
|
||||
windir = os.Getenv("SystemRoot")
|
||||
}
|
||||
if windir == "" {
|
||||
windir = `C:\Windows`
|
||||
}
|
||||
return []string{
|
||||
filepath.Join(windir, "System32", "drivers"),
|
||||
filepath.Join(windir, "System32", "DriverStore", "FileRepository"),
|
||||
}
|
||||
}
|
||||
|
||||
func hashFileSHA256(path string) (string, error) {
|
||||
f, err := os.Open(path)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
h := sha256.New()
|
||||
if _, err := io.Copy(h, f); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return fmt.Sprintf("%x", h.Sum(nil)), nil
|
||||
}
|
||||
|
||||
func collectLocalDrivers() ([]localDriver, error) {
|
||||
var out []localDriver
|
||||
seenPaths := make(map[string]struct{})
|
||||
|
||||
for _, root := range driverSearchRoots() {
|
||||
if _, err := os.Stat(root); err != nil {
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
continue
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err := filepath.WalkDir(root, func(path string, d fs.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
if d.IsDir() {
|
||||
return nil
|
||||
}
|
||||
if !strings.EqualFold(filepath.Ext(d.Name()), ".sys") {
|
||||
return nil
|
||||
}
|
||||
|
||||
key := strings.ToLower(path)
|
||||
if _, ok := seenPaths[key]; ok {
|
||||
return nil
|
||||
}
|
||||
seenPaths[key] = struct{}{}
|
||||
|
||||
sum, err := hashFileSHA256(path)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
out = append(out, localDriver{
|
||||
Path: path,
|
||||
Filename: d.Name(),
|
||||
SHA256: strings.ToLower(sum),
|
||||
})
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func findVulnerableDrivers(catalog []lolDriverEntry, local []localDriver) []driverMatch {
|
||||
bySHA256 := make(map[string]lolDriverEntry)
|
||||
for _, entry := range catalog {
|
||||
for _, sample := range entry.KnownVulnerableSamples {
|
||||
sum := strings.ToLower(strings.TrimSpace(sample.SHA256))
|
||||
if sum == "" {
|
||||
continue
|
||||
}
|
||||
if _, exists := bySHA256[sum]; !exists {
|
||||
bySHA256[sum] = entry
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var matches []driverMatch
|
||||
for _, d := range local {
|
||||
entry, ok := bySHA256[d.SHA256]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
why := strings.TrimSpace(entry.Commands.Description)
|
||||
if why == "" {
|
||||
why = firstSampleDescription(entry.KnownVulnerableSamples)
|
||||
}
|
||||
matches = append(matches, driverMatch{
|
||||
Path: d.Path,
|
||||
Filename: d.Filename,
|
||||
SHA256: d.SHA256,
|
||||
Category: entry.Category,
|
||||
Verified: entry.Verified,
|
||||
ID: entry.ID,
|
||||
Why: why,
|
||||
ExampleCommand: strings.TrimSpace(entry.Commands.Command),
|
||||
Usecase: strings.TrimSpace(entry.Commands.Usecase),
|
||||
Privileges: strings.TrimSpace(entry.Commands.Privileges),
|
||||
OS: strings.TrimSpace(entry.Commands.OperatingSystem),
|
||||
Resource: firstResource(entry.Resources),
|
||||
})
|
||||
}
|
||||
|
||||
sort.Slice(matches, func(i, j int) bool {
|
||||
ai := strings.ToLower(matches[i].Filename)
|
||||
aj := strings.ToLower(matches[j].Filename)
|
||||
if ai != aj {
|
||||
return ai < aj
|
||||
}
|
||||
return matches[i].Path < matches[j].Path
|
||||
})
|
||||
return matches
|
||||
}
|
||||
|
||||
func printDriverResults(matches []driverMatch, scanned int) {
|
||||
if plainMode {
|
||||
fmt.Println(strings.Repeat("=", 62))
|
||||
fmt.Printf("Scanned drivers: %d\n", scanned)
|
||||
fmt.Printf("Matches: %d\n", len(matches))
|
||||
fmt.Println(strings.Repeat("=", 62))
|
||||
fmt.Println()
|
||||
} else {
|
||||
fmt.Printf(" %sScanned drivers:%s %d\n", colorDim, colorReset, scanned)
|
||||
fmt.Printf(" %sMatches:%s %d\n\n", colorDim, colorReset, len(matches))
|
||||
}
|
||||
fmt.Println(" Note: Example command comes from LOLDrivers. If the driver is already loaded,")
|
||||
fmt.Println(" skip service creation/start and interact with the existing device interface.")
|
||||
fmt.Println()
|
||||
|
||||
for i, m := range matches {
|
||||
if i > 0 {
|
||||
fmt.Println()
|
||||
}
|
||||
if plainMode {
|
||||
fmt.Printf(" [%d] %s\n", i+1, m.Filename)
|
||||
} else {
|
||||
fmt.Printf(" %s╭─%s %s[%d]%s %s%s%s\n", colorCyan, colorReset, colorDim, i+1, colorReset, colorBold, m.Filename, colorReset)
|
||||
}
|
||||
printField("Path", m.Path)
|
||||
printField("SHA256", m.SHA256)
|
||||
printField("Category", m.Category)
|
||||
printField("Verified", m.Verified)
|
||||
printField("Loaded", m.Loaded)
|
||||
printField("ID", m.ID)
|
||||
if m.Why != "" {
|
||||
printField("Why", m.Why)
|
||||
}
|
||||
if m.Usecase != "" {
|
||||
printField("Use case", m.Usecase)
|
||||
}
|
||||
if m.Privileges != "" {
|
||||
printField("Privileges", m.Privileges)
|
||||
}
|
||||
if m.OS != "" {
|
||||
printField("OS", m.OS)
|
||||
}
|
||||
if m.ExampleCommand != "" {
|
||||
printField("Example", m.ExampleCommand)
|
||||
}
|
||||
if m.Resource != "" {
|
||||
printField("Source", m.Resource)
|
||||
}
|
||||
if plainMode {
|
||||
fmt.Printf(" %s\n", strings.Repeat("-", 62))
|
||||
} else {
|
||||
fmt.Printf(" %s╰%s\n", colorCyan, strings.Repeat("─", 63))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func firstResource(resources []string) string {
|
||||
for _, r := range resources {
|
||||
r = strings.TrimSpace(r)
|
||||
if r != "" {
|
||||
return r
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func firstSampleDescription(samples []lolDriverSample) string {
|
||||
for _, s := range samples {
|
||||
desc := strings.TrimSpace(s.Description)
|
||||
if desc != "" {
|
||||
return desc
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func normalizeDriverName(name string) string {
|
||||
n := strings.ToLower(strings.TrimSpace(name))
|
||||
n = strings.TrimSuffix(n, ".sys")
|
||||
return n
|
||||
}
|
||||
|
||||
func loadedDriverNames() map[string]struct{} {
|
||||
loaded := make(map[string]struct{})
|
||||
|
||||
// service names of loaded kernel/file system drivers
|
||||
out, err := exec.Command("sc.exe", "query", "type=", "driver", "state=", "all").Output()
|
||||
if err == nil {
|
||||
for _, line := range strings.Split(string(out), "\n") {
|
||||
line = strings.TrimSpace(line)
|
||||
const prefix = "SERVICE_NAME:"
|
||||
if strings.HasPrefix(line, prefix) {
|
||||
name := normalizeDriverName(strings.TrimSpace(strings.TrimPrefix(line, prefix)))
|
||||
if name != "" {
|
||||
loaded[name] = struct{}{}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// module names currently loaded by the OS
|
||||
out, err = exec.Command("driverquery", "/fo", "csv", "/nh").Output()
|
||||
if err == nil {
|
||||
r := csv.NewReader(strings.NewReader(string(out)))
|
||||
for {
|
||||
rec, err := r.Read()
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
if len(rec) < 1 {
|
||||
continue
|
||||
}
|
||||
name := normalizeDriverName(rec[0])
|
||||
if name != "" {
|
||||
loaded[name] = struct{}{}
|
||||
}
|
||||
}
|
||||
}
|
||||
return loaded
|
||||
}
|
||||
|
||||
func annotateLoadedState(matches []driverMatch) {
|
||||
if runtime.GOOS != "windows" {
|
||||
for i := range matches {
|
||||
matches[i].Loaded = "unknown"
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
loaded := loadedDriverNames()
|
||||
for i := range matches {
|
||||
name := normalizeDriverName(matches[i].Filename)
|
||||
if _, ok := loaded[name]; ok {
|
||||
matches[i].Loaded = "yes"
|
||||
} else {
|
||||
matches[i].Loaded = "no"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func runDriverMode(loader *loadingBox) error {
|
||||
if runtime.GOOS != "windows" {
|
||||
return fmt.Errorf("driver scan is currently supported on Windows hosts only")
|
||||
}
|
||||
|
||||
loader.setMessage("Fetching LOLDrivers catalog...")
|
||||
catalog, err := fetchLOLDriversCatalog()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to fetch LOLDrivers catalog: %w", err)
|
||||
}
|
||||
|
||||
loader.setMessage("Hashing local drivers...")
|
||||
local, err := collectLocalDrivers()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to scan local drivers: %w", err)
|
||||
}
|
||||
|
||||
loader.setMessage("Matching hashes...")
|
||||
matches := findVulnerableDrivers(catalog, local)
|
||||
annotateLoadedState(matches)
|
||||
if len(matches) == 0 {
|
||||
loader.finish("No vulnerable drivers found")
|
||||
fmt.Printf("No known vulnerable LOLDrivers found on disk. Scanned %d local drivers.\n", len(local))
|
||||
return nil
|
||||
}
|
||||
|
||||
loader.finish(fmt.Sprintf("Found %d vulnerable drivers", len(matches)))
|
||||
printDriverResults(matches, len(local))
|
||||
return nil
|
||||
}
|
||||
|
||||
func privilegeDisplay(priv string) string {
|
||||
@@ -772,6 +1144,7 @@ func enableVirtualTerminal() {
|
||||
func main() {
|
||||
help := flag.Bool("h", false, "show help")
|
||||
helpLong := flag.Bool("help", false, "show help")
|
||||
driverFlag := flag.Bool("driver", false, "scan local drivers against the LOLDrivers catalog")
|
||||
plainFlag := flag.Bool("plain", false, "ASCII-only output for telnet/reverse shells")
|
||||
var searchQuery string
|
||||
flag.StringVar(&searchQuery, "s", "", "search for a specific binary by name")
|
||||
@@ -793,6 +1166,16 @@ func main() {
|
||||
printHelp()
|
||||
os.Exit(2)
|
||||
}
|
||||
if *driverFlag {
|
||||
if searchQuery != "" {
|
||||
fmt.Fprintln(os.Stderr, "-driver cannot be combined with -s/-search")
|
||||
os.Exit(2)
|
||||
}
|
||||
if sortMode != sortBinary {
|
||||
fmt.Fprintln(os.Stderr, "-driver does not support -sort (only default binary mode)")
|
||||
os.Exit(2)
|
||||
}
|
||||
}
|
||||
|
||||
if !plainMode {
|
||||
enableVirtualTerminal()
|
||||
@@ -802,6 +1185,14 @@ func main() {
|
||||
loader := newLoadingBox("Checking privileges...")
|
||||
loader.start()
|
||||
|
||||
if *driverFlag {
|
||||
if err := runDriverMode(loader); err != nil {
|
||||
loader.finish("Failed")
|
||||
fmt.Println(err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
loader.setMessage("Checking process token...")
|
||||
isSystem, err := privileges.IsLocalSystem()
|
||||
if err != nil {
|
||||
|
||||
Reference in New Issue
Block a user