goLoL initial commit

This commit is contained in:
Aaron Kidwell
2026-05-21 14:04:06 -04:00
parent 433681a5a7
commit 96cf6f02c4
8 changed files with 1152 additions and 160 deletions
+119 -38
View File
@@ -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 (AZ)
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
BIN
View File
Binary file not shown.
+1 -14
View File
@@ -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
-34
View File
@@ -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=
+74
View File
@@ -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
}
+15
View File
@@ -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
}
+89
View File
@@ -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
}
+854 -74
View File
@@ -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
}