diff --git a/README.md b/README.md index f8c6d0d..6ca5505 100644 --- a/README.md +++ b/README.md @@ -1,66 +1,149 @@ -# offensive-go +# goLoL -Windows host reconnaissance tool written in Go. Collects basic system information, lists running processes (with parent PID), and flags running executables that lack an embedded Authenticode signature. +**goLoL** is a Windows host scanner that finds [LOLBAS](https://lolbas-project.github.io/) binaries present on the current machine and lists techniques you can run at your current privilege level — with MITRE ATT&CK mappings and example commands. -Uses in-process `WinVerifyTrust` via `wintrust.dll` — no PowerShell, no child processes, no external verifier binary. +**Author:** Aaron Kidwell + +``` + ____ ____ _ + / ___| ___ | _ \ ___ | | +| | _ / _ \| | | / _ \| | +| |_| | (_) | |_| | (_) | |___ + \____|\___/|____/ \___/|_____| + L o L +``` ## Features -- **System information** — OS, architecture, CPU count, memory, hostname -- **Process enumeration** — PID, PPID, process name, full command line -- **Unsigned binary detection** — scans unique executable paths and reports processes whose on-disk image has no embedded signature (`TRUST_E_NOSIGNATURE`) +- **Live LOLBAS catalog** — pulls the latest entries from [lolbas-project.github.io](https://lolbas-project.github.io/api/lolbas.json) +- **On-disk detection** — resolves documented paths to local `%WINDIR%`, `%ProgramFiles%`, `%USERPROFILE%`, and WindowsApps locations +- **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 +- **Plain output mode** — ASCII-only output for telnet, reverse shells, and other unstable terminals +- **No child processes** — privilege checks use the process token and `net localgroup`; scanning uses standard Go file APIs + +## Privilege tiers + +| Your context | What you see | +|---|---| +| Standard user | User-tier techniques | +| Member of local **Administrators** | User-tier + admin-tier techniques | +| **NT AUTHORITY\\SYSTEM** | User-tier + admin-tier + SYSTEM-tier techniques | + +Admin-tier commands may still require an elevated shell even if your account is in the Administrators group. SYSTEM-tier entries are hidden unless the process token is SYSTEM (`S-1-5-18`). ## Requirements -- **Go** 1.21+ (project uses Go 1.26.2) -- **Windows** for signature verification (other sections may run elsewhere with limited value) +- **Windows** (primary target; non-Windows builds stub out privilege checks) +- **Go 1.21+** (project uses Go 1.26.2) +- **Network access** to fetch the LOLBAS JSON catalog on first run ## Install ```bash -git clone https://github.com/aaron-kidwell/offensive-go.git -cd offensive-go +git clone https://github.com/aaron-kidwell/dev.git +cd dev go mod download ``` ## Usage +Run from the module root (required for `internal/` packages): + ```bash go run . ``` -Or build a binary: +Build a binary: ```bash -go build -o offensive-go.exe . -.\offensive-go.exe +go build -o golol.exe . +.\golol.exe +``` + +### Flags + +| Flag | Description | +|---|---| +| `-h`, `-help` | Show help | +| `-plain` | ASCII-only output — no colors, Unicode, or cursor control | +| `-sort` | Sort results: `binary` (default), `privilege`, or `attack` | + +Sort aliases: `b`, `priv` / `p`, `mitre` / `a`. + +### Examples + +```bash +# Default — grouped by binary name (A–Z) +go run . + +# Admin techniques first, then user techniques +go run . -sort privilege + +# Sorted by MITRE ATT&CK ID +go run . -sort attack + +# Reverse shell / telnet friendly output +go run . -plain + +# Combine flags +go run . -plain -sort attack ``` ### Example output +**Interactive mode** (colored terminal): + ``` -=== System Information === -Operating System: windows -Architecture: amd64 -CPU Cores: 16 -... +Role: administrator +Sort: binary +Binaries: 147 +Techniques: 299 -=== Running Processes === -PID 1234 PPID 5678: example.exe - "C:\path\to\example.exe" ... + [1] Esentutl.exe + Path C:\Windows\System32\esentutl.exe + Description Binary for working with Microsoft JET database + -- technique 1 + Privileges Admin + ATT&CK T1003.003: NTDS + Use case Copy/extract a locked file such as the AD Database + Command esentutl.exe /y /vss c:\windows\ntds\ntds.dit /d {PATH_ABSOLUTE:.dit} +``` -=== Unsigned Binaries (in-process WinVerifyTrust) === -PID 25272: main.exe - C:\Users\...\AppData\Local\Temp\go-build...\main.exe +**Plain mode** (`-plain`): + +``` +[*] Checking process token... +[*] Fetching LOLBAS catalog... +[+] Found 147 binaries, 299 techniques + +============================================================== +Role: administrator +Sort: binary +Binaries: 147 +Techniques: 299 +============================================================== + + [1] Esentutl.exe + Path: C:\Windows\System32\esentutl.exe + ... ``` ## How it works -| Component | Package / API | -|-----------|----------------| -| Process & memory stats | [gopsutil](https://github.com/shirou/gopsutil) | -| Signature check | `internal/signverify` → `WinVerifyTrust` (`wintrust.dll`) | +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. +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. -Unsigned detection checks **embedded** Authenticode signatures only. Catalog-signed system binaries under `\Windows\` are skipped to reduce false positives. Many legitimate tools (Go binaries, dev tools) appear unsigned — triage results in context. +| Component | Location | +|---|---| +| LOLBAS catalog | `https://lolbas-project.github.io/api/lolbas.json` | +| Privilege detection | `internal/privileges` | +| MITRE technique names | `internal/mitre` | +| Path resolution & output | `main.go` | ## Project layout @@ -68,22 +151,20 @@ Unsigned detection checks **embedded** Authenticode signatures only. Catalog-sig . ├── main.go ├── internal/ -│ └── signverify/ -│ ├── signverify_windows.go # WinVerifyTrust implementation -│ └── signverify_stub.go # non-Windows stub +│ ├── mitre/ +│ │ └── names.go # MITRE ATT&CK ID → label map +│ └── privileges/ +│ ├── privileges_windows.go # Token / Administrators group checks +│ └── privileges_stub.go # Non-Windows stub ├── go.mod └── go.sum ``` -## Tests - -```bash -go test ./... -``` - ## Disclaimer -For **authorized** security testing, lab use, and education only. Only run against systems you own or have explicit permission to assess. The authors are not responsible for misuse. +For **authorized** security testing, lab use, and education only. Only run against systems you own or have explicit permission to assess. LOLBAS entries describe techniques that may be abused by attackers — use responsibly. The author is not responsible for misuse. + +Technique data and binary metadata are sourced from the [LOLBAS Project](https://github.com/LOLBAS-Project/LOLBAS). goLoL is not affiliated with or endorsed by the LOLBAS Project. ## License diff --git a/dev.exe b/dev.exe new file mode 100644 index 0000000..3292b13 Binary files /dev/null and b/dev.exe differ diff --git a/go.mod b/go.mod index 99283be..aa78f9e 100644 --- a/go.mod +++ b/go.mod @@ -2,17 +2,4 @@ module dev go 1.26.2 -require ( - github.com/shirou/gopsutil/v3 v3.24.5 - golang.org/x/sys v0.44.0 -) - -require ( - github.com/go-ole/go-ole v1.2.6 // indirect - github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect - github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect - github.com/shoenig/go-m1cpu v0.1.6 // indirect - github.com/tklauser/go-sysconf v0.3.12 // indirect - github.com/tklauser/numcpus v0.6.1 // indirect - github.com/yusufpapurcu/wmi v1.2.4 // indirect -) +require golang.org/x/sys v0.44.0 diff --git a/go.sum b/go.sum index e5fc4e6..0e11bfe 100644 --- a/go.sum +++ b/go.sum @@ -1,36 +1,2 @@ -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= -github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= -github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= -github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4= -github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw= -github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= -github.com/shirou/gopsutil/v3 v3.24.5 h1:i0t8kL+kQTvpAYToeuiVk3TgDeKOFioZO3Ztz/iZ9pI= -github.com/shirou/gopsutil/v3 v3.24.5/go.mod h1:bsoOS1aStSs9ErQ1WWfxllSeS1K5D+U30r2NfcubMVk= -github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM= -github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ= -github.com/shoenig/test v0.6.4 h1:kVTaSd7WLz5WZ2IaoM0RSzRsUD+m8wRR+5qvntpn4LU= -github.com/shoenig/test v0.6.4/go.mod h1:byHiCGXqrVaflBLAMq/srcZIHynQPQgeyvkvXnjqq0k= -github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= -github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU= -github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI= -github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk= -github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY= -github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= -github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= -golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.44.0 h1:ildZl3J4uzeKP07r2F++Op7E9B29JRUy+a27EibtBTQ= golang.org/x/sys v0.44.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= -gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/mitre/names.go b/internal/mitre/names.go new file mode 100644 index 0000000..29733a1 --- /dev/null +++ b/internal/mitre/names.go @@ -0,0 +1,74 @@ +package mitre + +import "strings" + +// techniqueNames maps MITRE ATT&CK IDs to sub-technique labels used in output. +var techniqueNames = map[string]string{ + "T1003": "OS Credential Dumping", + "T1003.001": "LSASS Memory", + "T1003.002": "Security Account Manager", + "T1003.003": "NTDS", + "T1027.013": "Encrypted/Encoded File", + "T1036": "Masquerading", + "T1036.005": "Match Legitimate Name or Location", + "T1040": "Network Sniffing", + "T1047": "Windows Management Instrumentation", + "T1048": "Exfiltration Over Alternative Protocol", + "T1048.003": "Exfiltration Over Unencrypted Non-C2 Protocol", + "T1053.002": "At (Windows)", + "T1053.005": "Scheduled Task", + "T1055": "Process Injection", + "T1059": "Command and Scripting Interpreter", + "T1059.003": "Windows Command Shell", + "T1070": "Indicator Removal", + "T1078": "Valid Accounts", + "T1105": "Ingress Tool Transfer", + "T1113": "Screen Capture", + "T1127": "Trusted Developer Utilities Proxy Execution", + "T1127.001": "MSBuild", + "T1127.002": "ClickOnce", + "T1140": "Deobfuscate/Decode Files or Information", + "T1187": "Forced Authentication", + "T1202": "Indirect Command Execution", + "T1216": "System Script Proxy Execution", + "T1216.001": "PubPrn", + "T1216.002": "SyncAppvPublishingServer", + "T1218": "System Binary Proxy Execution", + "T1218.001": "Compiled HTML File", + "T1218.002": "Control Panel", + "T1218.003": "CMSTP", + "T1218.004": "InstallUtil", + "T1218.005": "Mshta", + "T1218.007": "Msiexec", + "T1218.008": "Odbcconf", + "T1218.009": "Regsvcs/Regasm", + "T1218.010": "Regsvr32", + "T1218.011": "Rundll32", + "T1218.012": "Verclsid", + "T1218.013": "Mavinject", + "T1218.014": "MMC", + "T1218.015": "Electron Applications", + "T1220": "XSL Script Processing", + "T1485": "Data Destruction", + "T1543.003": "Windows Service", + "T1546.007": "Netsh Helper DLL", + "T1547": "Boot or Logon Autostart Execution", + "T1548.002": "Bypass User Account Control", + "T1552.001": "Credentials In Files", + "T1562": "Impair Defenses", + "T1562.001": "Disable or Modify Tools", + "T1564": "Hide Artifacts", + "T1564.004": "NTFS File Attributes", + "T1567": "Exfiltration Over Web Service", +} + +func TechniqueLabel(id string) string { + id = strings.TrimSpace(id) + if id == "" { + return "Unknown" + } + if name, ok := techniqueNames[id]; ok { + return id + ": " + name + } + return id +} diff --git a/internal/privileges/privileges_stub.go b/internal/privileges/privileges_stub.go new file mode 100644 index 0000000..9990aa7 --- /dev/null +++ b/internal/privileges/privileges_stub.go @@ -0,0 +1,15 @@ +//go:build !windows + +package privileges + +func IsLocalAdministrator() (bool, error) { + return false, nil +} + +func IsLocalSystem() (bool, error) { + return false, nil +} + +func IsElevated() (bool, error) { + return false, nil +} diff --git a/internal/privileges/privileges_windows.go b/internal/privileges/privileges_windows.go new file mode 100644 index 0000000..20c0dfd --- /dev/null +++ b/internal/privileges/privileges_windows.go @@ -0,0 +1,89 @@ +//go:build windows + +package privileges + +import ( + "os" + "os/exec" + "strings" + "unsafe" + + "golang.org/x/sys/windows" +) + +func IsLocalAdministrator() (bool, error) { + username := os.Getenv("USERNAME") + if username == "" { + return false, nil + } + + out, err := exec.Command("net", "localgroup", "administrators").Output() + if err != nil { + return false, err + } + + inMembers := false + for _, line := range strings.Split(string(out), "\n") { + line = strings.TrimSpace(line) + if line == "" { + continue + } + if strings.EqualFold(line, "Members") { + inMembers = true + continue + } + if !inMembers { + continue + } + if strings.HasPrefix(line, "The command completed") { + break + } + if strings.EqualFold(line, username) { + return true, nil + } + } + + return false, nil +} + +func IsLocalSystem() (bool, error) { + var token windows.Token + if err := windows.OpenProcessToken(windows.CurrentProcess(), windows.TOKEN_QUERY, &token); err != nil { + return false, err + } + defer token.Close() + + user, err := token.GetTokenUser() + if err != nil { + return false, err + } + + systemSID, err := windows.StringToSid("S-1-5-18") + if err != nil { + return false, err + } + + return windows.EqualSid(user.User.Sid, systemSID), nil +} + +func IsElevated() (bool, error) { + var token windows.Token + if err := windows.OpenProcessToken(windows.CurrentProcess(), windows.TOKEN_QUERY, &token); err != nil { + return false, err + } + defer token.Close() + + var elevation uint32 + returnSize := uint32(0) + err := windows.GetTokenInformation( + token, + windows.TokenElevation, + (*byte)(unsafe.Pointer(&elevation)), + uint32(unsafe.Sizeof(elevation)), + &returnSize, + ) + if err != nil { + return false, err + } + return elevation != 0, nil +} diff --git a/main.go b/main.go index 02dd37e..8c69a6c 100644 --- a/main.go +++ b/main.go @@ -1,104 +1,884 @@ package main import ( - "dev/internal/signverify" + "encoding/json" + "flag" "fmt" + "net/http" "os" + "path/filepath" "runtime" + "sort" + "strings" + "syscall" + "unsafe" - memory "github.com/shirou/gopsutil/v3/mem" - process "github.com/shirou/gopsutil/v3/process" + "dev/internal/mitre" + "dev/internal/privileges" ) -func main() { - printSystemInfo() - printRunningProcesses() - printUnsignedProcesses() +type lolbasCommand struct { + Command string `json:"Command"` + Description string `json:"Description"` + Usecase string `json:"Usecase"` + Category string `json:"Category"` + Privileges string `json:"Privileges"` + MitreID string `json:"MitreID"` } -func printRunningProcesses() { - fmt.Println("=== Running Processes ===") - - processes, err := process.Processes() - if err != nil { - fmt.Println(err) - return - } - - for _, p := range processes { - name, err := p.Name() - if err != nil { - continue - } - cmdline, err := p.Cmdline() - if err != nil { - continue - } - - ppid, err := p.Ppid() - if err != nil { - continue - } - - fmt.Printf("PID %d PPID %d: %s - %s\n\n", p.Pid, ppid, name, cmdline) - } +type lolbasEntry struct { + Name string `json:"Name"` + Desc string `json:"Description"` + Paths []struct { + Path string `json:"Path"` + } `json:"Full_Path"` + Commands []lolbasCommand `json:"Commands"` } -func printUnsignedProcesses() { - fmt.Println("=== Unsigned Binaries (in-process WinVerifyTrust) ===") +func resolveLocalPath(documented string) string { + p := filepath.FromSlash(documented) + lower := strings.ToLower(p) - if runtime.GOOS != "windows" { - fmt.Println("signature check requires Windows") - return + windir := os.Getenv("WINDIR") + if windir == "" { + windir = os.Getenv("SystemRoot") } - if err := signverify.MustLoad(); err != nil { - fmt.Println("signverify:", err) - return + userProfile := os.Getenv("USERPROFILE") + programFiles := os.Getenv("ProgramFiles") + programFilesX86 := os.Getenv("ProgramFiles(x86)") + + switch { + case programFilesX86 != "" && strings.HasPrefix(lower, `c:\program files (x86)`): + p = filepath.Join(programFilesX86, p[len(`c:\program files (x86)`):]) + case programFiles != "" && strings.HasPrefix(lower, `c:\program files`): + p = filepath.Join(programFiles, p[len(`c:\program files`):]) + case windir != "" && strings.HasPrefix(lower, `c:\windows`): + p = filepath.Join(windir, p[len(`c:\windows`):]) + case userProfile != "" && strings.HasPrefix(lower, `c:\users\`): + parts := strings.SplitN(p, `\`, 4) + if len(parts) == 4 { + p = filepath.Join(userProfile, parts[3]) + } } - processes, err := process.Processes() - if err != nil { - fmt.Println(err) - return + return filepath.Clean(p) +} + +func findOnDisk(documented string) []string { + resolved := resolveLocalPath(documented) + if _, err := os.Stat(resolved); err == nil { + return []string{resolved} } + if strings.Contains(strings.ToLower(documented), `\windowsapps\`) { + base := filepath.Base(resolved) + if programFiles := os.Getenv("ProgramFiles"); programFiles != "" { + matches, err := filepath.Glob(filepath.Join(programFiles, "WindowsApps", "*", base)) + if err == nil && len(matches) > 0 { + return matches + } + } + } + + return nil +} + +func entryLocalPaths(e lolbasEntry) []string { + var paths []string seen := make(map[string]struct{}) - found := 0 - - for _, p := range processes { - exe, err := p.Exe() - if err != nil || exe == "" { - continue + for _, p := range e.Paths { + for _, local := range findOnDisk(p.Path) { + if _, ok := seen[local]; ok { + continue + } + seen[local] = struct{}{} + paths = append(paths, local) } - if _, ok := seen[exe]; ok { - continue - } - seen[exe] = struct{}{} + } + return paths +} - unsigned, err := signverify.IsUnsigned(exe) - if err != nil || !unsigned { - continue - } +func requiresSystem(privileges string) bool { + p := strings.ToLower(strings.TrimSpace(privileges)) + return p == "system" +} - name, _ := p.Name() - fmt.Printf("PID %d: %s\n %s\n\n", p.Pid, name, exe) - found++ +func requiresAdministrator(privileges string) bool { + if requiresSystem(privileges) { + return false } - if found == 0 { - fmt.Println("none found (or no accessible exe paths)") + p := strings.ToLower(strings.TrimSpace(privileges)) + switch p { + case "", "any", "low privileges", "user": + return false + } + + adminMarkers := []string{ + "admin", + "administrator", + "dns admin", + "backup operators", + "sebackup", + } + for _, marker := range adminMarkers { + if strings.Contains(p, marker) { + return true + } + } + return false +} + +func commandVisible(privileges string, isSystem, isAdmin bool) bool { + if requiresSystem(privileges) { + return isSystem + } + if isSystem || isAdmin { + return true + } + return !requiresAdministrator(privileges) +} + +func primaryLocalPath(paths []string) string { + if len(paths) == 0 { + return "" + } + if len(paths) == 1 { + return paths[0] + } + + prefs := []string{`\system32\`, `\framework64\`, `\syswow64\`, `\framework\`} + for _, pref := range prefs { + for _, p := range paths { + if strings.Contains(strings.ToLower(p), pref) { + return p + } + } + } + return paths[0] +} + +func runnableCommands(e lolbasEntry, isSystem, isAdmin bool) []lolbasCommand { + var allowed []lolbasCommand + for _, cmd := range e.Commands { + if commandVisible(cmd.Privileges, isSystem, isAdmin) { + allowed = append(allowed, cmd) + } + } + return allowed +} + +const ( + colorReset = "\033[0m" + colorBold = "\033[1m" + colorDim = "\033[2m" + colorCyan = "\033[96m" + colorOrange = "\033[38;5;208m" + colorGreen = "\033[92m" + colorMagenta = "\033[95m" +) + +var plainMode bool + +type sortMode string + +const ( + sortBinary sortMode = "binary" + sortPrivilege sortMode = "privilege" + sortAttack sortMode = "attack" +) + +func parseSortMode(raw string) (sortMode, error) { + switch strings.ToLower(strings.TrimSpace(raw)) { + case "binary", "b": + return sortBinary, nil + case "privilege", "priv", "p": + return sortPrivilege, nil + case "attack", "mitre", "a": + return sortAttack, nil + default: + return "", fmt.Errorf("unknown sort %q (use binary, privilege, or attack)", raw) } } -func printSystemInfo() { - fmt.Println("\n=== System Information ===") - fmt.Println("Operating System:", runtime.GOOS) - fmt.Println("Architecture:", runtime.GOARCH) - fmt.Println("CPU Cores:", runtime.NumCPU()) - mem, _ := memory.VirtualMemory() - fmt.Printf("Total Memory: %.2f GB\n", float64(mem.Total)/(1024*1024*1024)) - fmt.Printf("Free Memory: %.2f GB\n", float64(mem.Free)/(1024*1024*1024)) - hostname, _ := os.Hostname() - fmt.Println("Hostname:", hostname) +const bannerArt = ` + █████ █████ + ░░███ ░░███ + ███████ ██████ ░███ ██████ ░███ + ███░░███ ███░░███ ░███ ███░░███ ░███ +░███ ░███░███ ░███ ░███ ░███ ░███ ░███ +░███ ░███░███ ░███ ░███ █░███ ░███ ░███ █ +░░███████░░██████ ███████████░░██████ ███████████ + ░░░░░███ ░░░░░░ ░░░░░░░░░░░ ░░░░░░ ░░░░░░░░░░░ + ███ ░███ +░░██████ + ░░░░░░ +` + +const plainBannerArt = ` + _____ ____ _ ____ _ +/ __// _ \/ \ / _ \/ \ +| | _| / \|| | | / \|| | +| |_//| \_/|| |_/\| \_/|| |_/\ +\____\\____/\____/\____/\____/ +` + +func printBanner() { + if plainMode { + fmt.Print(plainBannerArt) + fmt.Println("Author: Aaron Kidwell") + fmt.Println(strings.Repeat("-", 48)) + fmt.Println() + return + } + fmt.Printf("\n%s%s%s", colorCyan, bannerArt, colorReset) + fmt.Printf("%sAuthor: Aaron Kidwell%s\n\n", colorGreen, colorReset) +} + +type loadingBox struct { + message string + drawn bool +} + +func newLoadingBox(message string) *loadingBox { + return &loadingBox{message: message} +} + +func (l *loadingBox) start() { + if plainMode { + fmt.Printf("[*] %s\n", l.message) + return + } + l.draw(false) +} + +func loadingLine(done bool, message string) string { + prefix := "..." + if done { + prefix = "✓" + } + plain := fmt.Sprintf(" %s %s", prefix, message) + if len(plain) > 40 { + plain = plain[:37] + "..." + } + return plain + strings.Repeat(" ", 40-len(plain)) +} + +func (l *loadingBox) draw(done bool) { + inner := loadingLine(done, l.message) + if done { + inner = colorGreen + inner + colorReset + } + if l.drawn { + fmt.Print("\033[3A") + } + l.drawn = true + fmt.Printf("\033[2K\r %s╭──────────────────────────────────────────╮%s\n", colorCyan, colorReset) + fmt.Printf("\033[2K\r %s│%s%s%s│%s\n", colorCyan, colorReset, inner, colorCyan, colorReset) + fmt.Printf("\033[2K\r %s╰──────────────────────────────────────────╯%s\n", colorCyan, colorReset) +} + +func (l *loadingBox) setMessage(message string) { + l.message = message + if plainMode { + fmt.Printf("[*] %s\n", message) + return + } + l.draw(false) +} + +func (l *loadingBox) finish(message string) { + if plainMode { + fmt.Printf("[+] %s\n\n", message) + return + } + l.message = message + l.draw(true) fmt.Println() } + +func printHelp() { + if !plainMode { + enableVirtualTerminal() + } + printBanner() + if plainMode { + fmt.Print(`Lists LOLBAS binaries present on this machine that match your privilege level, +with ATT&CK techniques and example commands from lolbas-project.github.io. + +Privilege tiers: user, administrator (local Administrators group), and +SYSTEM (NT AUTHORITY\SYSTEM token). SYSTEM-tier techniques are shown only +when running as SYSTEM. + +Usage: + go run . [flags] + +Flags: + -h, -help Show this help + -plain ASCII-only output for telnet/reverse shells + -sort string Sort results (default "binary") + binary Group by binary name (A-Z) + privilege Admin tier first, then user tier + attack Sort by ATT&CK ID (Txxxx) + +Examples: + go run . + go run . -plain + go run . -sort privilege + go run . -sort attack + go run . -h + +`) + return + } + fmt.Printf(`%sLists LOLBAS binaries present on this machine that match your privilege level, +with ATT&CK techniques and example commands from lolbas-project.github.io. + +Privilege tiers: user, administrator (local Administrators group), and +SYSTEM (NT AUTHORITY\\SYSTEM token). SYSTEM-tier techniques are shown only +when running as SYSTEM. + +%sUsage:%s + go run . [flags] + +%sFlags:%s + -h, -help Show this help + -plain ASCII-only output for telnet/reverse shells + -sort string Sort results (default "binary") + binary Group by binary name (A-Z) + privilege Admin tier first, then user tier + attack Sort by ATT&CK ID (Txxxx) + +%sExamples:%s + go run . + go run . -plain + go run . -sort privilege + go run . -sort attack + go run . -h + +`, colorDim, colorBold, colorReset, colorBold, colorReset, colorBold, colorReset) +} + +func privilegeDisplay(priv string) string { + label := strings.TrimSpace(priv) + if label == "" { + label = "User" + } + if plainMode { + return label + } + if requiresSystem(label) { + return colorMagenta + label + colorReset + } + if requiresAdministrator(label) { + return colorGreen + label + colorReset + } + return colorOrange + label + colorReset +} + +type commandEntry struct { + privilegeRaw string + privilege string + attackID string + attack string + usecase string + command string + isSystemTier bool + isAdminTier bool +} + +type listItem struct { + name string + description string + path string + commands []commandEntry +} + +type flatRow struct { + binary string + description string + path string + command commandEntry +} + +func flattenItems(items []listItem) []flatRow { + var rows []flatRow + for _, item := range items { + for _, cmd := range item.commands { + rows = append(rows, flatRow{ + binary: item.name, + description: item.description, + path: item.path, + command: cmd, + }) + } + } + return rows +} + +func sortFlatRows(rows []flatRow, mode sortMode) { + sort.Slice(rows, func(i, j int) bool { + a, b := rows[i], rows[j] + switch mode { + case sortPrivilege: + if a.command.isSystemTier != b.command.isSystemTier { + return a.command.isSystemTier + } + if a.command.isAdminTier != b.command.isAdminTier { + return a.command.isAdminTier + } + if a.command.privilegeRaw != b.command.privilegeRaw { + return a.command.privilegeRaw < b.command.privilegeRaw + } + if a.command.attackID != b.command.attackID { + return a.command.attackID < b.command.attackID + } + return strings.ToLower(a.binary) < strings.ToLower(b.binary) + case sortAttack: + if a.command.attackID != b.command.attackID { + return a.command.attackID < b.command.attackID + } + if a.command.isAdminTier != b.command.isAdminTier { + return a.command.isAdminTier + } + return strings.ToLower(a.binary) < strings.ToLower(b.binary) + default: + bi := strings.ToLower(a.binary) + bj := strings.ToLower(b.binary) + if bi != bj { + return bi < bj + } + if a.command.isAdminTier != b.command.isAdminTier { + return a.command.isAdminTier + } + return a.command.attackID < b.command.attackID + } + }) +} + +func sortListItems(items []listItem) { + sort.Slice(items, func(i, j int) bool { + return strings.ToLower(items[i].name) < strings.ToLower(items[j].name) + }) + for i := range items { + sort.Slice(items[i].commands, func(a, b int) bool { + ca, cb := items[i].commands[a], items[i].commands[b] + if ca.isSystemTier != cb.isSystemTier { + return ca.isSystemTier + } + if ca.isAdminTier != cb.isAdminTier { + return ca.isAdminTier + } + return ca.attackID < cb.attackID + }) + } +} + +func roleLabel(isSystem, isAdmin bool) string { + if isSystem { + if plainMode { + return "NT AUTHORITY\\SYSTEM" + } + return colorMagenta + "NT AUTHORITY\\SYSTEM" + colorReset + } + if isAdmin { + if plainMode { + return "administrator" + } + return colorGreen + "administrator" + colorReset + } + if plainMode { + return "standard user" + } + return colorOrange + "standard user" + colorReset +} + +func printHeader(isSystem, isAdmin bool, mode sortMode, binaries, techniques int) { + role := roleLabel(isSystem, isAdmin) + if plainMode { + fmt.Println(strings.Repeat("=", 62)) + fmt.Printf("Role: %s\n", role) + fmt.Printf("Sort: %s\n", mode) + fmt.Printf("Binaries: %d\n", binaries) + fmt.Printf("Techniques: %d\n", techniques) + fmt.Println(strings.Repeat("=", 62)) + fmt.Println() + return + } + fmt.Printf(" %sRole:%s %s\n", colorDim, colorReset, role) + fmt.Printf(" %sSort:%s %s\n", colorDim, colorReset, mode) + fmt.Printf(" %sBinaries:%s %d\n", colorDim, colorReset, binaries) + fmt.Printf(" %sTechniques:%s %d\n\n", colorDim, colorReset, techniques) +} + +func printSection(title string) { + if plainMode { + fmt.Printf("\n== %s ==\n\n", title) + return + } + fmt.Printf("\n %s%s\n", title, colorReset) + fmt.Printf(" %s%s%s\n\n", colorDim, strings.Repeat("─", 62), colorReset) +} + +func flatRowTitle(mode sortMode, row flatRow) string { + switch mode { + case sortAttack: + id := row.command.attackID + if id == "" { + if plainMode { + id = "-" + } else { + id = "—" + } + } + if plainMode { + return fmt.Sprintf("%s - %s", id, row.binary) + } + return fmt.Sprintf("%s · %s", id, row.binary) + case sortPrivilege: + tier := "User tier" + switch { + case row.command.isSystemTier: + tier = "SYSTEM tier" + case row.command.isAdminTier: + tier = "Admin tier" + } + if plainMode { + return fmt.Sprintf("%s - %s", tier, row.binary) + } + return fmt.Sprintf("%s · %s", tier, row.binary) + default: + return row.binary + } +} + +func printField(label, value string) { + if plainMode { + fmt.Printf(" %-14s %s\n", label+":", value) + return + } + fmt.Printf(" %s%-14s%s %s\n", colorDim, label, colorReset, value) +} + +func printDivider() { + if plainMode { + fmt.Printf(" %s\n", strings.Repeat("-", 62)) + return + } + fmt.Printf(" %s%s%s\n", colorDim, strings.Repeat("─", 62), colorReset) +} + +func printFlatRows(rows []flatRow, mode sortMode) { + var prevSection string + for i, row := range rows { + section := flatSectionKey(mode, row) + if section != prevSection { + if prevSection != "" { + fmt.Println() + } + printSection(flatSectionLabel(mode, row)) + prevSection = section + } else if i > 0 { + fmt.Println() + } + + if plainMode { + fmt.Printf(" [%d] %s\n", i+1, flatRowTitle(mode, row)) + } else { + fmt.Printf(" %s╭─%s %s[%d]%s %s%s%s\n", colorCyan, colorReset, colorDim, i+1, colorReset, colorBold, flatRowTitle(mode, row), colorReset) + } + printField("Path", row.path) + printField("Description", row.description) + printDivider() + printField("Privileges", row.command.privilege) + printField("ATT&CK", row.command.attack) + printField("Use case", row.command.usecase) + printField("Command", row.command.command) + if plainMode { + fmt.Printf(" %s\n", strings.Repeat("-", 62)) + } else { + fmt.Printf(" %s╰%s\n", colorCyan, strings.Repeat("─", 63)) + } + } +} + +func flatSectionKey(mode sortMode, row flatRow) string { + switch mode { + case sortAttack: + return row.command.attackID + case sortPrivilege: + switch { + case row.command.isSystemTier: + return "system" + case row.command.isAdminTier: + return "admin" + default: + return "user" + } + default: + return "" + } +} + +func flatSectionLabel(mode sortMode, row flatRow) string { + switch mode { + case sortAttack: + label := row.command.attack + if label == "" { + label = row.command.attackID + } + if label == "" { + label = "Unknown technique" + } + if plainMode { + return label + } + return colorBold + label + colorReset + case sortPrivilege: + switch { + case row.command.isSystemTier: + if plainMode { + return "SYSTEM tier" + } + return colorBold + colorMagenta + "SYSTEM tier" + colorReset + case row.command.isAdminTier: + if plainMode { + return "Administrator tier" + } + return colorBold + colorGreen + "Administrator tier" + colorReset + default: + if plainMode { + return "User tier" + } + return colorBold + colorOrange + "User tier" + colorReset + } + default: + return "" + } +} + +func printGroupedItems(items []listItem) { + for i, item := range items { + if i > 0 { + fmt.Println() + } + if plainMode { + fmt.Printf(" [%d] %s\n", i+1, item.name) + } else { + fmt.Printf(" %s╭─%s %s[%d]%s %s%s%s\n", colorCyan, colorReset, colorDim, i+1, colorReset, colorBold, item.name, colorReset) + } + printField("Path", item.path) + printField("Description", item.description) + + for j, cmd := range item.commands { + if plainMode { + fmt.Printf(" -- technique %d\n", j+1) + } else { + fmt.Printf(" %s├─ technique %d%s\n", colorCyan, j+1, colorReset) + } + printField("Privileges", cmd.privilege) + printField("ATT&CK", cmd.attack) + printField("Use case", cmd.usecase) + printField("Command", cmd.command) + } + if plainMode { + fmt.Printf(" %s\n", strings.Repeat("-", 62)) + } else { + fmt.Printf(" %s╰%s\n", colorCyan, strings.Repeat("─", 63)) + } + } +} + +func printResults(items []listItem, isSystem, isAdmin bool, mode sortMode) { + if !plainMode { + enableVirtualTerminal() + } + + techniques := 0 + for _, item := range items { + techniques += len(item.commands) + } + printHeader(isSystem, isAdmin, mode, len(items), techniques) + + if mode == sortBinary { + printGroupedItems(items) + return + } + + rows := flattenItems(items) + sortFlatRows(rows, mode) + printFlatRows(rows, mode) +} + +func enableVirtualTerminal() { + if runtime.GOOS != "windows" { + return + } + + kernel32 := syscall.NewLazyDLL("kernel32.dll") + getConsoleMode := kernel32.NewProc("GetConsoleMode") + setConsoleMode := kernel32.NewProc("SetConsoleMode") + + handle := syscall.Handle(os.Stdout.Fd()) + var mode uint32 + r, _, _ := getConsoleMode.Call(uintptr(handle), uintptr(unsafe.Pointer(&mode))) + if r == 0 { + return + } + + const enableVirtualTerminalProcessing = 0x0004 + _, _, _ = setConsoleMode.Call(uintptr(handle), uintptr(mode|enableVirtualTerminalProcessing)) +} + +func main() { + help := flag.Bool("h", false, "show help") + helpLong := flag.Bool("help", false, "show help") + plainFlag := flag.Bool("plain", false, "ASCII-only output for telnet/reverse shells") + sortFlag := flag.String("sort", "binary", "sort by: binary, privilege, attack") + flag.Parse() + + plainMode = *plainFlag + + if *help || *helpLong { + printHelp() + return + } + + sortMode, err := parseSortMode(*sortFlag) + if err != nil { + fmt.Fprintln(os.Stderr, err) + printHelp() + os.Exit(2) + } + + if !plainMode { + enableVirtualTerminal() + } + printBanner() + + loader := newLoadingBox("Checking privileges...") + loader.start() + + loader.setMessage("Checking process token...") + isSystem, err := privileges.IsLocalSystem() + if err != nil { + loader.finish("Failed") + fmt.Println("Failed to check process token:", err) + return + } + + loader.setMessage("Checking local group membership...") + isAdmin, err := privileges.IsLocalAdministrator() + if err != nil { + loader.finish("Failed") + fmt.Println("Failed to check local group membership:", err) + return + } + + loader.setMessage("Fetching LOLBAS catalog...") + resp, err := http.Get("https://lolbas-project.github.io/api/lolbas.json") + if err != nil { + loader.finish("Failed") + fmt.Println("Failed to fetch LOLBAS list:", err) + return + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + loader.finish("Failed") + fmt.Println("Unexpected status:", resp.Status) + return + } + + loader.setMessage("Parsing LOLBAS catalog...") + var entries []lolbasEntry + if err := json.NewDecoder(resp.Body).Decode(&entries); err != nil { + loader.finish("Failed") + fmt.Println("Failed to parse JSON:", err) + return + } + + loader.setMessage("Scanning local binaries...") + seenPaths := make(map[string]struct{}) + var items []listItem + for i, e := range entries { + if i > 0 && i%40 == 0 { + loader.setMessage(fmt.Sprintf("Scanning local binaries... (%d/%d)", i, len(entries))) + } + + paths := entryLocalPaths(e) + path := primaryLocalPath(paths) + if path == "" { + continue + } + + pathKey := strings.ToLower(path) + if _, ok := seenPaths[pathKey]; ok { + continue + } + + commands := runnableCommands(e, isSystem, isAdmin) + if len(commands) == 0 { + continue + } + + sort.Slice(commands, func(i, j int) bool { + sysI := requiresSystem(commands[i].Privileges) + sysJ := requiresSystem(commands[j].Privileges) + if sysI != sysJ { + return sysI + } + adminI := requiresAdministrator(commands[i].Privileges) + adminJ := requiresAdministrator(commands[j].Privileges) + if adminI != adminJ { + return adminI + } + return commands[i].MitreID < commands[j].MitreID + }) + + seenPaths[pathKey] = struct{}{} + + commandEntries := make([]commandEntry, 0, len(commands)) + for _, cmd := range commands { + privRaw := strings.TrimSpace(cmd.Privileges) + if privRaw == "" { + privRaw = "User" + } + commandEntries = append(commandEntries, commandEntry{ + privilegeRaw: privRaw, + privilege: privilegeDisplay(cmd.Privileges), + attackID: strings.TrimSpace(cmd.MitreID), + attack: mitre.TechniqueLabel(cmd.MitreID), + usecase: cmd.Usecase, + command: cmd.Command, + isSystemTier: requiresSystem(cmd.Privileges), + isAdminTier: requiresAdministrator(cmd.Privileges), + }) + } + + items = append(items, listItem{ + name: e.Name, + description: e.Desc, + path: path, + commands: commandEntries, + }) + } + + if len(items) == 0 { + loader.finish("No runnable binaries found") + fmt.Println("No runnable LOLBAS binaries found on this host.") + return + } + + loader.finish(fmt.Sprintf("Found %d binaries, %d techniques", len(items), countTechniques(items))) + + sortListItems(items) + printResults(items, isSystem, isAdmin, sortMode) +} + +func countTechniques(items []listItem) int { + n := 0 + for _, item := range items { + n += len(item.commands) + } + return n +}