Created
September 15, 2026 19:15
-
-
Save danieljs777/42e1c101c033b21ff73b24fcbe7eb0a5 to your computer and use it in GitHub Desktop.
PFX Password Brute-Force / Sanitization Tester A Bash script to recover a PKCS#12 (.pfx) certificate password when a password manager or certificate-issuing app has silently stripped special characters (e.g. ] & | ! } $) during copy/transfer. Given the original password, it generates and tests sanitized variations against the PFX using openssl p…
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env bash | |
| # | |
| # bruteforce-pfx-password.sh | |
| # Tests possibly-sanitized variations of a password against a PFX file. | |
| # | |
| # Focus: special characters that certificate-issuing apps tend to strip or | |
| # alter, especially the shell metacharacters ] & | ! } $ | |
| # | |
| # Usage: ./bruteforce-pfx-password.sh certificate.pfx | |
| # | |
| set -u | |
| # --------------------------------------------------------------------------- | |
| # Configuration | |
| # --------------------------------------------------------------------------- | |
| PFX="${1:?Usage: $0 file.pfx}" | |
| [[ -f "$PFX" ]] || { echo "File not found: $PFX"; exit 1; } | |
| # Special characters suspected of being stripped by the app. | |
| # Adjust this list if you know which ones were in the password. | |
| SPECIALS=(']' '&' '|' '!' '}' '$' '{' '[' '#' '%' '^' '*' '(' ')' '@' '~' '`' '\' '"' "'" ' ') | |
| # Limit of combinations for removal of MULTIPLE characters (avoids explosion). | |
| # Given N characters present in the password, we test removals of all | |
| # subsets up to this size. | |
| MAX_REMOVALS=4 | |
| # --------------------------------------------------------------------------- | |
| # Read the password (no echo on screen) | |
| # --------------------------------------------------------------------------- | |
| read -rsp "Original password (from the vault): " ORIGINAL | |
| echo | |
| echo "Original password length: ${#ORIGINAL} bytes" | |
| echo | |
| # --------------------------------------------------------------------------- | |
| # Function: test a password against the PFX | |
| # Returns 0 if the password opens the file. | |
| # Tries with and without -legacy (OpenSSL 3.x needs -legacy for RC2/3DES). | |
| # --------------------------------------------------------------------------- | |
| test_password() { | |
| local password="$1" | |
| openssl pkcs12 -in "$PFX" -nokeys -passin "pass:$password" -legacy \ | |
| >/dev/null 2>&1 && return 0 | |
| openssl pkcs12 -in "$PFX" -nokeys -passin "pass:$password" \ | |
| >/dev/null 2>&1 && return 0 | |
| return 1 | |
| } | |
| # --------------------------------------------------------------------------- | |
| # Find out which special characters actually appear in the password | |
| # (only makes sense to generate removals for those) | |
| # --------------------------------------------------------------------------- | |
| PRESENT=() | |
| for c in "${SPECIALS[@]}"; do | |
| if [[ "$ORIGINAL" == *"$c"* ]]; then | |
| PRESENT+=("$c") | |
| fi | |
| done | |
| echo "Special characters found in the password: ${#PRESENT[@]}" | |
| if [[ ${#PRESENT[@]} -gt 0 ]]; then | |
| printf ' ' | |
| for c in "${PRESENT[@]}"; do printf '%s ' "$c"; done | |
| echo | |
| fi | |
| echo | |
| # --------------------------------------------------------------------------- | |
| # Remove a set of characters from the password (all occurrences) | |
| # --------------------------------------------------------------------------- | |
| remove_chars() { | |
| local s="$1"; shift | |
| local c | |
| for c in "$@"; do | |
| s="${s//"$c"/}" | |
| done | |
| printf '%s' "$s" | |
| } | |
| # --------------------------------------------------------------------------- | |
| # Generate all subsets of PRESENT up to size MAX_REMOVALS | |
| # and produce the resulting password from removing each subset. | |
| # Also includes common "structural" variations. | |
| # --------------------------------------------------------------------------- | |
| declare -A SEEN | |
| CANDIDATES_FILE="$(mktemp)" | |
| trap 'rm -f "$CANDIDATES_FILE"' EXIT | |
| emit() { | |
| local candidate="$1" | |
| if [[ -z "${SEEN[$candidate]+x}" ]]; then | |
| SEEN[$candidate]=1 | |
| printf '%s\n' "$candidate" >> "$CANDIDATES_FILE" | |
| fi | |
| } | |
| # 1) The original password, first of all | |
| emit "$ORIGINAL" | |
| # 2) Frequent structural variations | |
| emit "${ORIGINAL//[^[:alnum:]]/}" # letters and numbers only | |
| emit "${ORIGINAL// /}" # no spaces | |
| emit "${ORIGINAL%%\$*}" # everything before the first $ | |
| emit "$(printf '%s' "$ORIGINAL" | tr -cd '[:print:]')" # remove non-printable | |
| emit "$(printf '%s' "$ORIGINAL" | tr -cd '[:alnum:]!@#%^&*()_+=-')" # "safe" set | |
| # 3) Removal of subsets of the present characters (combinations) | |
| N=${#PRESENT[@]} | |
| if [[ $N -gt 0 ]]; then | |
| # Iterate over bit masks from 1 to 2^N - 1 | |
| total=$(( (1 << N) - 1 )) | |
| for (( mask=1; mask<=total; mask++ )); do | |
| # Count set bits; skip if it exceeds MAX_REMOVALS | |
| bits=0 | |
| for (( b=0; b<N; b++ )); do | |
| (( (mask >> b) & 1 )) && (( bits++ )) | |
| done | |
| (( bits > MAX_REMOVALS )) && continue | |
| subset=() | |
| for (( b=0; b<N; b++ )); do | |
| (( (mask >> b) & 1 )) && subset+=("${PRESENT[$b]}") | |
| done | |
| candidate="$(remove_chars "$ORIGINAL" "${subset[@]}")" | |
| emit "$candidate" | |
| done | |
| fi | |
| TOTAL_CANDIDATES=$(wc -l < "$CANDIDATES_FILE") | |
| echo "Total candidates generated: $TOTAL_CANDIDATES" | |
| echo "Testing..." | |
| echo | |
| # --------------------------------------------------------------------------- | |
| # Test loop | |
| # --------------------------------------------------------------------------- | |
| FOUND="" | |
| i=0 | |
| while IFS= read -r candidate; do | |
| (( i++ )) | |
| printf '\r[%d/%d] testing...' "$i" "$TOTAL_CANDIDATES" | |
| if test_password "$candidate"; then | |
| FOUND="$candidate" | |
| break | |
| fi | |
| done < "$CANDIDATES_FILE" | |
| echo | |
| echo | |
| # --------------------------------------------------------------------------- | |
| # Result | |
| # --------------------------------------------------------------------------- | |
| if [[ -n "$FOUND" ]]; then | |
| echo "✅ Valid password found!" | |
| echo | |
| echo " Password : »${FOUND}«" | |
| echo " Bytes : ${#FOUND}" | |
| echo | |
| echo " Password hex (for verification):" | |
| printf ' ' | |
| printf '%s' "$FOUND" | xxd -p | tr -d '\n' | |
| echo | |
| echo | |
| echo " Difference from the original:" | |
| diff <(printf '%s' "$ORIGINAL" | xxd) <(printf '%s' "$FOUND" | xxd) \ | |
| | sed 's/^/ /' || true | |
| else | |
| echo "❌ No variation worked (tested $TOTAL_CANDIDATES)." | |
| echo | |
| echo " Suggested next steps:" | |
| echo " - Increase MAX_REMOVALS (careful: grows exponentially)." | |
| echo " - Check Unicode normalization (NFC/NFD) if the password has accents." | |
| echo " - Confirm the PFX opens in OpenSSL 3.x with -legacy." | |
| echo " - If nothing works, revoke and reissue the certificate." | |
| fi |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment