Module: Msf::Exploit::Remote::HTTP::Spip

Includes:
Msf::Exploit::Remote::HttpClient
Defined in:
lib/msf/core/exploit/remote/http/spip.rb

Instance Attribute Summary

Attributes included from Msf::Exploit::Remote::HttpClient

#client, #cookie_jar

Instance Method Summary collapse

Methods included from Msf::Exploit::Remote::HttpClient

#basic_auth, #cleanup, #configure_http_login_scanner, #connect, #connect_ws, #deregister_http_client_options, #disconnect, #download, #full_uri, #handler, #http_fingerprint, #lookup_http_fingerprints, #normalize_uri, #path_from_uri, #peer, #proxies, #reconfig_redirect_opts!, #request_opts_from_url, #request_url, #rhost, #rport, #send_request_cgi, #send_request_cgi!, #send_request_raw, #service_details, #setup, #ssl, #ssl_version, #sslkeylogfile, #strip_tags, #target_uri, #validate_fingerprint, #vhost

Methods included from CertificateTrace

#certificate_csr_trace, #certificate_peer_cert_trace, #certificate_trace, #certificate_trace_enabled?

Methods included from Kerberos::ServiceAuthenticator::Options

#kerberos_auth_options, #kerberos_clock_skew_seconds

Methods included from Kerberos::Ticket::Storage

#kerberos_storage_options, #kerberos_ticket_storage, store_ccache

Methods included from Auxiliary::LoginScanner

#configure_login_scanner

Methods included from Auxiliary::Report

#active_db?, #create_cracked_credential, #create_credential, #create_credential_and_login, #create_credential_login, #db, #db_warning_given?, #get_client, #get_host, #inside_workspace_boundary?, #invalidate_login, #mytask, #myworkspace, #myworkspace_id, #report_auth_info, #report_client, #report_exploit, #report_host, #report_loot, #report_note, #report_service, #report_vuln, #report_web_form, #report_web_page, #report_web_site, #report_web_vuln, #store_cred, #store_local, #store_loot

Methods included from Metasploit::Framework::Require

optionally, optionally_active_record_railtie, optionally_include_metasploit_credential_creation, #optionally_include_metasploit_credential_creation, optionally_require_metasploit_db_gem_engines

Instance Method Details

#initialize(info = {}) ⇒ Object



7
8
9
10
11
12
13
# File 'lib/msf/core/exploit/remote/http/spip.rb', line 7

def initialize(info = {})
  super

  register_options([
    OptString.new('TARGETURI', [true, 'Path to Spip install', '/'])
  ])
end

#parse_paquet_xml_version(plugin_name) ⇒ Rex::Version?

Attempt to read the plugin version from its paquet.xml file. Plugins can be installed under plugins/ or plugins/auto/.

Parameters:

  • plugin_name (String) —

    Name of the plugin directory

Returns:

  • (Rex::Version, nil) —

    Version from the paquet.xml prefix attribute, or nil



130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
# File 'lib/msf/core/exploit/remote/http/spip.rb', line 130

def parse_paquet_xml_version(plugin_name)
  %W[
    plugins/#{plugin_name}/paquet.xml
    plugins/auto/#{plugin_name}/paquet.xml
  ].each do |path|
    res = send_request_cgi('method' => 'GET', 'uri' => normalize_uri(target_uri.path, path))
    next unless res&.code == 200

    if res.body =~ /prefix="#{plugin_name}"/ && res.body =~ /version="(\d+(?:\.\d+)+)"/
      return Rex::Version.new(::Regexp.last_match(1))
    end
  end

  nil
end

#parse_plugin_version(body, plugin_name) ⇒ Rex::Version?

Parse the plugin version from config.txt or composed-by

Parameters:

  • body (String) —

    The body content to parse

  • plugin_name (String) —

    Name of the plugin to find the version for

Returns:

  • (Rex::Version, nil) —

    Version of the plugin as Rex::Version, or nil if not found



151
152
153
154
155
156
157
158
# File 'lib/msf/core/exploit/remote/http/spip.rb', line 151

def parse_plugin_version(body, plugin_name)
  body.each_line do |line|
    if line =~ /#{plugin_name}\((\d+(\.\d+)+)\)/
      return Rex::Version.new(::Regexp.last_match(1))
    end
  end
  nil
end

#spip_extract_cvt_fields(html, action_name) ⇒ Hash?

Extract the HMAC-signed CVT hidden fields for a given form action.

Parameters:

  • html (Nokogiri::HTML::Document) —

    Parsed HTML document

  • action_name (String) —

    Value of the formulaire_action hidden input

Returns:

  • (Hash, nil) —

    Hash with 'args' and 'sign' keys, or nil if not found



20
21
22
23
24
25
26
27
28
29
30
31
# File 'lib/msf/core/exploit/remote/http/spip.rb', line 20

def spip_extract_cvt_fields(html, action_name)
  container = html.css("input[name='formulaire_action']").find { |n| n['value'] == action_name }&.parent
  return unless container

  args = container.at_css("input[name='formulaire_action_args']")
  return unless args

  sign = container.at_css("input[name='formulaire_action_sign']")
  return unless sign

  { 'args' => args['value'].to_s, 'sign' => sign['value'].to_s }
end

#spip_login(username, password) ⇒ String

Authenticate against the SPIP login form. Sets @spip_cookie on success. Requires USERNAME and PASSWORD datastore options.

Parameters:

  • username (String) —

    SPIP username

  • password (String) —

    SPIP password

Returns:

  • (String) —

    Session cookie string

Raises:

  • (Rex::RuntimeError) —

    on authentication failure



40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
# File 'lib/msf/core/exploit/remote/http/spip.rb', line 40

def (username, password)
  res = send_request_cgi(
    'method' => 'GET',
    'uri' => normalize_uri(target_uri.path, 'spip.php'),
    'vars_get' => { 'page' => 'login' }
  )
  raise Rex::RuntimeError, 'Login page did not respond' unless res

  cvt = spip_extract_cvt_fields(res.get_html_document, 'login')
  raise Rex::RuntimeError, 'Login form not found' unless cvt

  cookie = res.get_cookies
  res = send_request_cgi(
    'method' => 'POST',
    'uri' => normalize_uri(target_uri.path, 'spip.php'),
    'cookie' => cookie,
    'vars_post' => {
      'page' => 'login',
      'formulaire_action' => 'login',
      'formulaire_action_args' => cvt['args'],
      'formulaire_action_sign' => cvt['sign'],
      'var_login' => username,
      'password' => password
    }
  )
  raise Rex::RuntimeError, 'No response to login' unless res

  @spip_cookie = res.get_cookies
  raise Rex::RuntimeError, 'Authentication failed: no spip_session cookie' unless @spip_cookie&.include?('spip_session')

  @spip_cookie
end

#spip_plugin_version(plugin_name) ⇒ Rex::Version?

Determine Spip plugin version by name

Parameters:

  • plugin_name (String) —

    Name of the plugin to search for

Returns:

  • (Rex::Version, nil) —

    Version of the plugin as Rex::Version, or nil if not found



107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
# File 'lib/msf/core/exploit/remote/http/spip.rb', line 107

def spip_plugin_version(plugin_name)
  res = send_request_cgi('method' => 'GET', 'uri' => normalize_uri(target_uri.path, 'spip.php'))
  return unless res

  composed_by = res.headers['Composed-By']
  # Case 1: Check if 'Composed-By' header is present and not empty
  version = composed_by&.present? ? parse_plugin_version(composed_by, plugin_name) : nil
  return version if version

  # Case 2: Extract URL from 'Composed-By' header and send a request to fetch the config.txt file
  config_url = composed_by =~ %r{(https?://[^\s]+/local/config\.txt)}i ? ::Regexp.last_match(1) : normalize_uri(target_uri.path, 'local', 'config.txt')
  config_res = send_request_cgi('method' => 'GET', 'uri' => config_url)
  return parse_plugin_version(config_res.body, plugin_name) if config_res&.code == 200

  # Case 3: Try fetching paquet.xml directly from common plugin paths
  parse_paquet_xml_version(plugin_name)
end

#spip_version ⇒ Rex::Version

Determine Spip version

Returns:



76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
# File 'lib/msf/core/exploit/remote/http/spip.rb', line 76

def spip_version
  res = send_request_cgi(
    'method' => 'GET',
    'uri' => normalize_uri(target_uri.path, 'spip.php')
  )

  return unless res

  version = nil

  potential_sources = [
    res.get_html_document.at('head/meta[@name="generator"]/@content')&.text,
    res.headers['Composed-By']
  ]

  potential_sources.each do |text|
    next unless text

    if text =~ /SPIP\s(\d+(\.\d+)+)/
      version = ::Regexp.last_match(1)
      break
    end
  end

  return version ? Rex::Version.new(version) : nil
end