mirror of
https://github.com/aaron-kidwell/goLoL
synced 2026-06-21 13:46:44 +00:00
goLoL initial commit
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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=
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user