Vulnerabilities > CVE-2017-2641 - SQL Injection vulnerability in Moodle

047910
CVSS 7.5 - HIGH
Attack vector
NETWORK
Attack complexity
LOW
Privileges required
NONE
Confidentiality impact
PARTIAL
Integrity impact
PARTIAL
Availability impact
PARTIAL
network
low complexity
moodle
CWE-89
nessus
exploit available

Summary

In Moodle 2.x and 3.x, SQL injection can occur via user preferences.

Common Attack Pattern Enumeration and Classification (CAPEC)

  • Command Line Execution through SQL Injection
    An attacker uses standard SQL injection methods to inject data into the command line for execution. This could be done directly through misuse of directives such as MSSQL_xp_cmdshell or indirectly through injection of data into the database that would be interpreted as shell commands. Sometime later, an unscrupulous backend application (or could be part of the functionality of the same application) fetches the injected data stored in the database and uses this data as command line arguments without performing proper validation. The malicious data escapes that data plane by spawning new commands to be executed on the host.
  • Object Relational Mapping Injection
    An attacker leverages a weakness present in the database access layer code generated with an Object Relational Mapping (ORM) tool or a weakness in the way that a developer used a persistence framework to inject his or her own SQL commands to be executed against the underlying database. The attack here is similar to plain SQL injection, except that the application does not use JDBC to directly talk to the database, but instead it uses a data access layer generated by an ORM tool or framework (e.g. Hibernate). While most of the time code generated by an ORM tool contains safe access methods that are immune to SQL injection, sometimes either due to some weakness in the generated code or due to the fact that the developer failed to use the generated access methods properly, SQL injection is still possible.
  • SQL Injection through SOAP Parameter Tampering
    An attacker modifies the parameters of the SOAP message that is sent from the service consumer to the service provider to initiate a SQL injection attack. On the service provider side, the SOAP message is parsed and parameters are not properly validated before being used to access a database in a way that does not use parameter binding, thus enabling the attacker to control the structure of the executed SQL query. This pattern describes a SQL injection attack with the delivery mechanism being a SOAP message.
  • Expanding Control over the Operating System from the Database
    An attacker is able to leverage access gained to the database to read / write data to the file system, compromise the operating system, create a tunnel for accessing the host machine, and use this access to potentially attack other machines on the same network as the database machine. Traditionally SQL injections attacks are viewed as a way to gain unauthorized read access to the data stored in the database, modify the data in the database, delete the data, etc. However, almost every data base management system (DBMS) system includes facilities that if compromised allow an attacker complete access to the file system, operating system, and full access to the host running the database. The attacker can then use this privileged access to launch subsequent attacks. These facilities include dropping into a command shell, creating user defined functions that can call system level libraries present on the host machine, stored procedures, etc.
  • SQL Injection
    This attack exploits target software that constructs SQL statements based on user input. An attacker crafts input strings so that when the target software constructs SQL statements based on the input, the resulting SQL statement performs actions other than those the application intended. SQL Injection results from failure of the application to appropriately validate input. When specially crafted user-controlled input consisting of SQL syntax is used without proper validation as part of SQL queries, it is possible to glean information from the database in ways not envisaged during application design. Depending upon the database and the design of the application, it may also be possible to leverage injection to have the database execute system-related commands of the attackers' choice. SQL Injection enables an attacker to talk directly to the database, thus bypassing the application completely. Successful injection can cause information disclosure as well as ability to add or modify data in the database. In order to successfully inject SQL and retrieve information from a database, an attacker:

Exploit-Db

descriptionMoodle 2.x/3.x - SQL Injection. CVE-2017-2641. Webapps exploit for PHP platform
fileexploits/php/webapps/41828.php
idEDB-ID:41828
last seen2017-04-06
modified2017-04-06
platformphp
port
published2017-04-06
reporterExploit-DB
sourcehttps://www.exploit-db.com/download/41828/
titleMoodle 2.x/3.x - SQL Injection
typewebapps

Nessus

  • NASL familyFedora Local Security Checks
    NASL idFEDORA_2017-D5DBC23747.NASL
    description3.2.2 Note that Tenable Network Security has extracted the preceding description block directly from the Fedora update system website. Tenable has attempted to automatically clean and format it as much as possible without introducing additional issues.
    last seen2020-06-05
    modified2017-07-17
    plugin id101726
    published2017-07-17
    reporterThis script is Copyright (C) 2017-2020 and is owned by Tenable, Inc. or an Affiliate thereof.
    sourcehttps://www.tenable.com/plugins/nessus/101726
    titleFedora 26 : moodle (2017-d5dbc23747)
    code
    #%NASL_MIN_LEVEL 80502
    #
    # (C) Tenable Network Security, Inc.
    #
    # The descriptive text and package checks in this plugin were  
    # extracted from Fedora Security Advisory FEDORA-2017-d5dbc23747.
    #
    
    include("compat.inc");
    
    if (description)
    {
      script_id(101726);
      script_version("3.5");
      script_set_attribute(attribute:"plugin_modification_date", value:"2020/06/04");
    
      script_cve_id("CVE-2017-2641");
      script_xref(name:"FEDORA", value:"2017-d5dbc23747");
    
      script_name(english:"Fedora 26 : moodle (2017-d5dbc23747)");
      script_summary(english:"Checks rpm output for the updated package.");
    
      script_set_attribute(
        attribute:"synopsis", 
        value:"The remote Fedora host is missing a security update."
      );
      script_set_attribute(
        attribute:"description", 
        value:
    "3.2.2
    
    Note that Tenable Network Security has extracted the preceding
    description block directly from the Fedora update system website.
    Tenable has attempted to automatically clean and format it as much as
    possible without introducing additional issues."
      );
      script_set_attribute(
        attribute:"see_also",
        value:"https://bodhi.fedoraproject.org/updates/FEDORA-2017-d5dbc23747"
      );
      script_set_attribute(
        attribute:"solution", 
        value:"Update the affected moodle package."
      );
      script_set_cvss_base_vector("CVSS2#AV:N/AC:L/Au:N/C:P/I:P/A:P");
      script_set_cvss_temporal_vector("CVSS2#E:POC/RL:OF/RC:C");
      script_set_cvss3_base_vector("CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H");
      script_set_cvss3_temporal_vector("CVSS:3.0/E:P/RL:O/RC:C");
      script_set_attribute(attribute:"exploitability_ease", value:"Exploits are available");
      script_set_attribute(attribute:"exploit_available", value:"true");
    
      script_set_attribute(attribute:"plugin_type", value:"local");
      script_set_attribute(attribute:"cpe", value:"p-cpe:/a:fedoraproject:fedora:moodle");
      script_set_attribute(attribute:"cpe", value:"cpe:/o:fedoraproject:fedora:26");
    
      script_set_attribute(attribute:"vuln_publication_date", value:"2017/03/26");
      script_set_attribute(attribute:"patch_publication_date", value:"2017/04/01");
      script_set_attribute(attribute:"plugin_publication_date", value:"2017/07/17");
      script_set_attribute(attribute:"generated_plugin", value:"current");
      script_end_attributes();
    
      script_category(ACT_GATHER_INFO);
      script_copyright(english:"This script is Copyright (C) 2017-2020 and is owned by Tenable, Inc. or an Affiliate thereof.");
      script_family(english:"Fedora Local Security Checks");
    
      script_dependencies("ssh_get_info.nasl");
      script_require_keys("Host/local_checks_enabled", "Host/RedHat/release", "Host/RedHat/rpm-list");
    
      exit(0);
    }
    
    
    include("audit.inc");
    include("global_settings.inc");
    include("rpm.inc");
    
    
    if (!get_kb_item("Host/local_checks_enabled")) audit(AUDIT_LOCAL_CHECKS_NOT_ENABLED);
    release = get_kb_item("Host/RedHat/release");
    if (isnull(release) || "Fedora" >!< release) audit(AUDIT_OS_NOT, "Fedora");
    os_ver = pregmatch(pattern: "Fedora.*release ([0-9]+)", string:release);
    if (isnull(os_ver)) audit(AUDIT_UNKNOWN_APP_VER, "Fedora");
    os_ver = os_ver[1];
    if (! preg(pattern:"^26([^0-9]|$)", string:os_ver)) audit(AUDIT_OS_NOT, "Fedora 26", "Fedora " + os_ver);
    
    if (!get_kb_item("Host/RedHat/rpm-list")) audit(AUDIT_PACKAGE_LIST_MISSING);
    
    
    cpu = get_kb_item("Host/cpu");
    if (isnull(cpu)) audit(AUDIT_UNKNOWN_ARCH);
    if ("x86_64" >!< cpu && cpu !~ "^i[3-6]86$") audit(AUDIT_LOCAL_CHECKS_NOT_IMPLEMENTED, "Fedora", cpu);
    
    
    flag = 0;
    if (rpm_check(release:"FC26", reference:"moodle-3.2.2-1.fc26")) flag++;
    
    
    if (flag)
    {
      security_report_v4(
        port       : 0,
        severity   : SECURITY_HOLE,
        extra      : rpm_report_get()
      );
      exit(0);
    }
    else
    {
      tested = pkg_tests_get();
      if (tested) audit(AUDIT_PACKAGE_NOT_AFFECTED, tested);
      else audit(AUDIT_PACKAGE_NOT_INSTALLED, "moodle");
    }
    
  • NASL familyFedora Local Security Checks
    NASL idFEDORA_2017-0196511D58.NASL
    descriptionUpdate for multiple CVEs Note that Tenable Network Security has extracted the preceding description block directly from the Fedora update system website. Tenable has attempted to automatically clean and format it as much as possible without introducing additional issues.
    last seen2020-06-05
    modified2017-04-03
    plugin id99141
    published2017-04-03
    reporterThis script is Copyright (C) 2017-2020 and is owned by Tenable, Inc. or an Affiliate thereof.
    sourcehttps://www.tenable.com/plugins/nessus/99141
    titleFedora 25 : moodle (2017-0196511d58)
    code
    #%NASL_MIN_LEVEL 80502
    #
    # (C) Tenable Network Security, Inc.
    #
    # The descriptive text and package checks in this plugin were  
    # extracted from Fedora Security Advisory FEDORA-2017-0196511d58.
    #
    
    include("compat.inc");
    
    if (description)
    {
      script_id(99141);
      script_version("3.5");
      script_set_attribute(attribute:"plugin_modification_date", value:"2020/06/04");
    
      script_cve_id("CVE-2017-2641");
      script_xref(name:"FEDORA", value:"2017-0196511d58");
    
      script_name(english:"Fedora 25 : moodle (2017-0196511d58)");
      script_summary(english:"Checks rpm output for the updated package.");
    
      script_set_attribute(
        attribute:"synopsis", 
        value:"The remote Fedora host is missing a security update."
      );
      script_set_attribute(
        attribute:"description", 
        value:
    "Update for multiple CVEs
    
    Note that Tenable Network Security has extracted the preceding
    description block directly from the Fedora update system website.
    Tenable has attempted to automatically clean and format it as much as
    possible without introducing additional issues."
      );
      script_set_attribute(
        attribute:"see_also",
        value:"https://bodhi.fedoraproject.org/updates/FEDORA-2017-0196511d58"
      );
      script_set_attribute(
        attribute:"solution", 
        value:"Update the affected moodle package."
      );
      script_set_cvss_base_vector("CVSS2#AV:N/AC:L/Au:N/C:P/I:P/A:P");
      script_set_cvss_temporal_vector("CVSS2#E:POC/RL:OF/RC:C");
      script_set_cvss3_base_vector("CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H");
      script_set_cvss3_temporal_vector("CVSS:3.0/E:P/RL:O/RC:C");
      script_set_attribute(attribute:"exploitability_ease", value:"Exploits are available");
      script_set_attribute(attribute:"exploit_available", value:"true");
    
      script_set_attribute(attribute:"plugin_type", value:"local");
      script_set_attribute(attribute:"cpe", value:"p-cpe:/a:fedoraproject:fedora:moodle");
      script_set_attribute(attribute:"cpe", value:"cpe:/o:fedoraproject:fedora:25");
    
      script_set_attribute(attribute:"vuln_publication_date", value:"2017/03/26");
      script_set_attribute(attribute:"patch_publication_date", value:"2017/03/31");
      script_set_attribute(attribute:"plugin_publication_date", value:"2017/04/03");
      script_set_attribute(attribute:"generated_plugin", value:"current");
      script_end_attributes();
    
      script_category(ACT_GATHER_INFO);
      script_copyright(english:"This script is Copyright (C) 2017-2020 and is owned by Tenable, Inc. or an Affiliate thereof.");
      script_family(english:"Fedora Local Security Checks");
    
      script_dependencies("ssh_get_info.nasl");
      script_require_keys("Host/local_checks_enabled", "Host/RedHat/release", "Host/RedHat/rpm-list");
    
      exit(0);
    }
    
    
    include("audit.inc");
    include("global_settings.inc");
    include("rpm.inc");
    
    
    if (!get_kb_item("Host/local_checks_enabled")) audit(AUDIT_LOCAL_CHECKS_NOT_ENABLED);
    release = get_kb_item("Host/RedHat/release");
    if (isnull(release) || "Fedora" >!< release) audit(AUDIT_OS_NOT, "Fedora");
    os_ver = pregmatch(pattern: "Fedora.*release ([0-9]+)", string:release);
    if (isnull(os_ver)) audit(AUDIT_UNKNOWN_APP_VER, "Fedora");
    os_ver = os_ver[1];
    if (! preg(pattern:"^25([^0-9]|$)", string:os_ver)) audit(AUDIT_OS_NOT, "Fedora 25", "Fedora " + os_ver);
    
    if (!get_kb_item("Host/RedHat/rpm-list")) audit(AUDIT_PACKAGE_LIST_MISSING);
    
    
    cpu = get_kb_item("Host/cpu");
    if (isnull(cpu)) audit(AUDIT_UNKNOWN_ARCH);
    if ("x86_64" >!< cpu && cpu !~ "^i[3-6]86$") audit(AUDIT_LOCAL_CHECKS_NOT_IMPLEMENTED, "Fedora", cpu);
    
    
    flag = 0;
    if (rpm_check(release:"FC25", reference:"moodle-3.1.5-1.fc25")) flag++;
    
    
    if (flag)
    {
      security_report_v4(
        port       : 0,
        severity   : SECURITY_HOLE,
        extra      : rpm_report_get()
      );
      exit(0);
    }
    else
    {
      tested = pkg_tests_get();
      if (tested) audit(AUDIT_PACKAGE_NOT_AFFECTED, tested);
      else audit(AUDIT_PACKAGE_NOT_INSTALLED, "moodle");
    }
    
  • NASL familyFedora Local Security Checks
    NASL idFEDORA_2017-0FCAF52F1A.NASL
    descriptionUpdate for multiple CVEs Note that Tenable Network Security has extracted the preceding description block directly from the Fedora update system website. Tenable has attempted to automatically clean and format it as much as possible without introducing additional issues.
    last seen2020-06-05
    modified2017-04-03
    plugin id99142
    published2017-04-03
    reporterThis script is Copyright (C) 2017-2020 and is owned by Tenable, Inc. or an Affiliate thereof.
    sourcehttps://www.tenable.com/plugins/nessus/99142
    titleFedora 24 : moodle (2017-0fcaf52f1a)
    code
    #%NASL_MIN_LEVEL 80502
    #
    # (C) Tenable Network Security, Inc.
    #
    # The descriptive text and package checks in this plugin were  
    # extracted from Fedora Security Advisory FEDORA-2017-0fcaf52f1a.
    #
    
    include("compat.inc");
    
    if (description)
    {
      script_id(99142);
      script_version("3.5");
      script_set_attribute(attribute:"plugin_modification_date", value:"2020/06/04");
    
      script_cve_id("CVE-2017-2641");
      script_xref(name:"FEDORA", value:"2017-0fcaf52f1a");
    
      script_name(english:"Fedora 24 : moodle (2017-0fcaf52f1a)");
      script_summary(english:"Checks rpm output for the updated package.");
    
      script_set_attribute(
        attribute:"synopsis", 
        value:"The remote Fedora host is missing a security update."
      );
      script_set_attribute(
        attribute:"description", 
        value:
    "Update for multiple CVEs
    
    Note that Tenable Network Security has extracted the preceding
    description block directly from the Fedora update system website.
    Tenable has attempted to automatically clean and format it as much as
    possible without introducing additional issues."
      );
      script_set_attribute(
        attribute:"see_also",
        value:"https://bodhi.fedoraproject.org/updates/FEDORA-2017-0fcaf52f1a"
      );
      script_set_attribute(
        attribute:"solution", 
        value:"Update the affected moodle package."
      );
      script_set_cvss_base_vector("CVSS2#AV:N/AC:L/Au:N/C:P/I:P/A:P");
      script_set_cvss_temporal_vector("CVSS2#E:POC/RL:OF/RC:C");
      script_set_cvss3_base_vector("CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H");
      script_set_cvss3_temporal_vector("CVSS:3.0/E:P/RL:O/RC:C");
      script_set_attribute(attribute:"exploitability_ease", value:"Exploits are available");
      script_set_attribute(attribute:"exploit_available", value:"true");
    
      script_set_attribute(attribute:"plugin_type", value:"local");
      script_set_attribute(attribute:"cpe", value:"p-cpe:/a:fedoraproject:fedora:moodle");
      script_set_attribute(attribute:"cpe", value:"cpe:/o:fedoraproject:fedora:24");
    
      script_set_attribute(attribute:"vuln_publication_date", value:"2017/03/26");
      script_set_attribute(attribute:"patch_publication_date", value:"2017/03/31");
      script_set_attribute(attribute:"plugin_publication_date", value:"2017/04/03");
      script_set_attribute(attribute:"generated_plugin", value:"current");
      script_end_attributes();
    
      script_category(ACT_GATHER_INFO);
      script_copyright(english:"This script is Copyright (C) 2017-2020 and is owned by Tenable, Inc. or an Affiliate thereof.");
      script_family(english:"Fedora Local Security Checks");
    
      script_dependencies("ssh_get_info.nasl");
      script_require_keys("Host/local_checks_enabled", "Host/RedHat/release", "Host/RedHat/rpm-list");
    
      exit(0);
    }
    
    
    include("audit.inc");
    include("global_settings.inc");
    include("rpm.inc");
    
    
    if (!get_kb_item("Host/local_checks_enabled")) audit(AUDIT_LOCAL_CHECKS_NOT_ENABLED);
    release = get_kb_item("Host/RedHat/release");
    if (isnull(release) || "Fedora" >!< release) audit(AUDIT_OS_NOT, "Fedora");
    os_ver = pregmatch(pattern: "Fedora.*release ([0-9]+)", string:release);
    if (isnull(os_ver)) audit(AUDIT_UNKNOWN_APP_VER, "Fedora");
    os_ver = os_ver[1];
    if (! preg(pattern:"^24([^0-9]|$)", string:os_ver)) audit(AUDIT_OS_NOT, "Fedora 24", "Fedora " + os_ver);
    
    if (!get_kb_item("Host/RedHat/rpm-list")) audit(AUDIT_PACKAGE_LIST_MISSING);
    
    
    cpu = get_kb_item("Host/cpu");
    if (isnull(cpu)) audit(AUDIT_UNKNOWN_ARCH);
    if ("x86_64" >!< cpu && cpu !~ "^i[3-6]86$") audit(AUDIT_LOCAL_CHECKS_NOT_IMPLEMENTED, "Fedora", cpu);
    
    
    flag = 0;
    if (rpm_check(release:"FC24", reference:"moodle-3.1.5-1.fc24")) flag++;
    
    
    if (flag)
    {
      security_report_v4(
        port       : 0,
        severity   : SECURITY_HOLE,
        extra      : rpm_report_get()
      );
      exit(0);
    }
    else
    {
      tested = pkg_tests_get();
      if (tested) audit(AUDIT_PACKAGE_NOT_AFFECTED, tested);
      else audit(AUDIT_PACKAGE_NOT_INSTALLED, "moodle");
    }
    

Seebug

bulletinFamilyexploit
descriptionThe vulnerability (CVE-2017-2641) allows an attacker to execute PHP code at the vulnerable Moodle server. This vulnerability actually consists of many small vulnerabilities, as described further in the blog post. Moodle is a very popular learning management system, deployed in many universities around the world, including top institutes such as MIT, Stanford, the University of Cambridge, and Oxfords’ University. These statistics, along with the fact Moodle stores a lot of sensitive information, such as grades, tests, and students private data, makes it a critical target, and the main reason I audited it. A user is required to exploit the vulnerability. It does not matter which capabilities it has (i.e. student, teacher) as long as it is not a guest. This vulnerability works on almost all Moodle versions deployed today, as seen in the Vulnerable Versions section. I recommend all Moodle administrators to apply the [security patch](https://git.moodle.org/gw?p=moodle.git&a=search&h=HEAD&st=commit&s=MDL-58010). #### Vulnerable Versions “3.2 to 3.2.1, 3.1 to 3.1.4, 3.0 to 3.0.8, 2.7.0 to 2.7.18 and other unsupported versions.” #### Technical Description **Part 1 – The Problems of Having Too Much Code** Moodle is an extremely large system. It contains thousands of files, hundreds of different components and approximately two million lines of PHP code. As such, it is obvious different developers wrote different parts of the code, even if those parts interact with each other. In the following white paper, I will demonstrate how having too much code, too many developers, and lacking documentation can lead to critical logical vulnerabilities. Keep in mind that logical vulnerabilities can and will occur in almost all systems featuring a large code base. Security issues in large code bases is of course not Moodle specific. A clear case of “same feature, different code” can be observed at the built-in Ajax mechanism of the system. Moodle is featuring a dynamic Ajax system, which allows different components to use the system’s built-in Ajax interface. The way Moodle does that is by using “External Functions”. Each component that wishes to use the built-in Ajax mechanism is registering its own “External Function”, specifying the component being called, the function name and the privileges required to use it. Later, when components wish to use the Ajax interface, they can simply call the _“service.php”_ file, supplying the name of the external function they have registered earlier. That way Moodle is allowing component developers to use its built-in Ajax interface, saving them the trouble of writing a new one by themselves. The problem starts when Moodle’s core developers has started using this interface too. Not so long ago, if a component needed to change the user’s preferences through an Ajax request, it called the _“setuserpref.php”_ file, specifying the name and value of the property it wanted to change. This can be viewed in the following code: ``` // Check access. if (!confirm_sesskey()) { print_error('invalidsesskey'); } // Get the name of the preference to update, and check it is allowed. $name = required_param('pref', PARAM_RAW); if (!isset($USER->ajax_updatable_user_prefs[$name])) { print_error('notallowedtoupdateprefremotely'); } // Get and the value. $value = required_param('value', $USER->ajax_updatable_user_prefs[$name]); // Update if (!set_user_preference($name, $value)) { print_error('errorsettinguserpref'); } echo 'OK'; ``` In the middle of the code, at the highlighted line, we can see that Moodle is trying to make sure the preference that needs to be changed is defined in the “_ajax_updatable_user_prefs”_ array, which defines which preferences can be changed via Ajax. This makes a lot of sense as Moodle does not want malicious attackers, such as us, to change anything that could prove to be critical. Although most of the user preferences can be changed via other measures, even if not through the Ajax interface, Moodle’s developers tried to think ahead and prevented any future abuse of this mechanism. That was true, until the external function “_update_user_preferences”_ has been added. This function was added to replace the old “_update_users”_ function, which could “potentially [be] used to update any user attribute”, which is obviously a very bad thing; and, as the old function was only used to update user’s preferences anyway, there was obviously no need to allow it to change anything else. The main difference between the old function and the new one, is that the old function **could not be accessed through the Ajax interface**, while the new one could, as the supposedly dangerous feature – the ability to change every user attribute, has been removed. On top of that, they implemented a proper privilege check, so even if an attacker could exploit something using user preferences, it will be able to exploit it only on its own user. But that doesn’t really matter if these preferences are later used inside an “eval” or an “exec” call, right? It doesn’t matter which user is exploiting the dangerous function as long as it’s being exploited. But let’s not get ahead of ourselves and have a look at the code of this function: ``` public static function update_user_preferences($userid, …, $preferences) { ... // If we are trying to edit our own user preferences if ($userid == $USER->id) { // Requires the capability to edit our own profile, which we have require_capability('moodle/user:editownmessageprofile', $systemcontext); } else { // Otherwise, we are tring to edit someone else's preferences /* Require admin capabilities... */ } // Set the user's preferences. foreach ($preferences as $preference) { set_user_preference($preference['type'], $preference['value'], $userid); } ... } ``` By looking at the code, we can see that we can only edit our own preferences because of the privileges check. But still, something is missing. Although the code makes sure we are only editing our own user preferences, it doesn’t check **which** preference we are changing, contrary to the other Ajax page responsible for changing user preferences – _“setuserpref.php”_. A classic example of how different developers, at different times, with different needs in mind write different code for the exact same functionality. This time, they assumed user preferences could not be exploited in any malicious way. They assumed it’s unexploitable. **Part 2 – Exploiting the Unexploitable** There’s a reason user preferences are considered unexploitable. They literally have almost no impact in terms of how to system operates – they are not used in DB queries, they are not defining any components, and the only thing they somewhat impact is GUI part of the system, and even then, only in a miner way. So, what _can_ we do? Well, let’s have a look at how the GUI parts of the system function. Moodle is using a Blocks mechanism to allow components to display relevant data to the user. These blocks can be added and removed by the user. One of these blocks – “course_overview”, is used to display the user a list of its enrolled courses. In order to store the order of the courses he enrolled into, The Course Overview block mechanism is using a specific user preference called “_course_overview_course_sortorder”_. This preference store a list of all courses IDs the user has enrolled into, ordered by the time he enrolled into them. This list separates the course IDs using a comma, so the following line of code is used to split the list: ``` return explode(',', $value); // Split the IDs using a comma ``` But what happens if that preference is empty? In that case, the block mechanism is trying to retrieve the legacy preference, “_course_overview_course_order”_, which again contains a list of all courses IDs, but in a rather different way. This time, in order to retrieve to IDs the block mechanism actually executes: ``` $order = unserialize($value); // Unserialize the course IDs ``` An unserialize call. What a surprise. Another classic example of how legacy code and backwards compatibility can compromise your entire system if you still use it, and how different developers from different times can implement the exact same feature using completely different ways. For us, this means we can now exploit an Object Injection attack. Unfortunately, because of the Moodle is filtering user input, there are some limitations about what we can injection: 1. We cannot inject Null bytes. At all. This means we cannot set any protected or private object properties, because when PHP is serializing them it adds null bytes to their serialized declaration. 2. Although there are a lot of classes in the code base, most of them are unreachable. They are either not included when our payload is unserialized, or cannot be reached using an autoload function. These limitations lead to a pretty difficult Object Injection. Yet, I did write RCE in the title, didn’t I? **Part 3 – Taking the Fun Out of Object Injections** Because of the specified limitations, we can only use public properties of already included classes. We can’t also use any code that relies on any protected or private properties as well, as they will be initialized as their default value or, most of the time, just a NULL. This really narrows our attack surface. Almost all classes use protected properties in some way, and most of them doesn’t feature any public properties at all. The first step is to understand exactly which magic PHP methods we can use. We can obviously call “___wakeup()”_, which is called when the object is unserialized, and _“__destruct()”_, which is called when the object is destroyed, but can we call another very popular method – “___toString()”_? Well, if we’ll look at the code that’s being executed right after our payload is unserialized we will see that our unserialized payload is treated as an array: ``` function block_course_overview_update_myorder($sortorder) { // $sortorder is our unserialized payload. $value = implode(',', $sortorder); ... set_user_preference('course_overview_course_sortorder', $value); } ``` This function tries to join our unserialized array members into one big happy string. But, if one of our members was actually, say, an Object, its _“__toString()”_ method would have been called. So we can not only execute one object’s _“__toString()”_ method, we could execute how many of them as we’d like. But what could we possibly do with a _“__toString()”?_ Well, let’s have a look at how the “_attribute_format_” abstract class implemented its own _“__toString()”_ method: ``` /** * Convert this to an element and then to a string * @return string */ public function __toString() { return $this->determine_format()->html(); } ``` Looks simple, right? Let’s look at one code flow we can access by calling the _“determine_format()”_ method of the class _“feedback”_, which inherits from our _“attribute_format”_ class: ``` /** * Create a text_attribute for this ui element. * * @return text_attribute */ public function determine_format() { return new text_attribute( $this->get_name(), $this->get_value(), $this->get_label(), $this->is_disabled() ); } /** * Determine if this input should be disabled based on the other settings. * * @return boolean Should this input be disabled when the page loads. */ public function is_disabled() { ... if ($this->grade->grade_item->is_overridable_item() …) { $overridden = 1; } … } ``` See that _“is_overridable_item()”_ call? That’s another method call, but this time using one of the object’s properties as an object. Now we are getting somewhere. Because we control the property, we control the object being called, which really expands our attack surface. One of the classes implementing the _“is_overridable_item()”_ method is the _“grade_item”_ class Following a series of method calls, we eventually arrive to the _“update”_ method: ``` /** * Is the grade item overridable */ public function is_overridable_item() { ... return ... ($this->is_external_item() or $this->is_calculated() ...); } /** * Checks if grade calculated. Returns this object's calculation. */ public function is_calculated() { ... if (!$this->calculation_normalized and strpos($this->calculation, '[[') !== false) { $this->set_calculation($this->calculation); } ... } /** * Sets this item's calculation (creates it) if not yet set, or * updates it if already set (in the DB). If no calculation is given, * the calculation is removed. */ public function set_calculation($formula) { ... $this->calculation_normalized = true; return $this->update(); } /** * Updates this object in the Database, based on its object variables. ID must be set. */ public function update($source=null) { ... // Get the data to update (IDs, column names, values) $data = $this->get_record_data(); // Update the record in the database $DB->update_record($this->table, $data); ... } ``` As can be seen, the _“update”_ method is responsible for updating the database with the data stored in the object. It’s using the property “_table”_ as the table name to update and the data is derived straight from the object own properties. So, basically, that’s a win. Using our object injection, we could update any row we’d like in the entire database. We could update administrator accounts, passwords, the site configuration, and basically whatever we want. That being said, there are a few limitations of how we can update: 1. The update SQL statement always ends with a WHERE condition, checking for an ID we specify it. 2. We cannot exploit SQL injections in our data. The values we are setting are escaped and the fields we are updating are checked against the actual fields of the table, so we can’t use any field that isn’t already there. But why do we even care about SQL Injections? We can already update whatever we want. Right? **Wrong.** We can update everything we’d like as long as we know **the ID of the row** we want to update. So, if for example we are trying to update the administrator’s password, we either need to guess its user ID, or just brute-force every account in the database. And as we are 1337 h4x0rs, we are trying to minimize the impact on the server as much as possible. **Part 4 – 1337 H4x0rs** So, first, we don’t want to start changing passwords for every user in the system. In fact, we don’t want to change anyone’s password, as this will probably be pretty suspicious. In order to bypass that we could add our user as another administrator in the system by changing the “_site_admins”_ configuration value, stored in the _“config”_ table. But how can we guess the ID of that specific configuration in the table? Well, we can’t. But we can try to change the WHERE SQL statement somehow. To do that, we will need to use an SQL Injection after all. As we can’t exploit any of the data fields, we will have to inject our SQL in the table name itself, which is not being escaped anywhere. But that raises another problem – before the UPDATE statement is executed, Moodle is querying the database for the column names and types of our specified table. This means we will have to make the same SQL Injection work both on the SELECT statement, which should return the correct data for the table, and the UPDATE statement, which should update the _“site_admins”_ configuration value. Let’s have a look at both statements: ``` SELECT column_name, data_type, character_maximum_length, numeric_precision, numeric_scale, is_nullable, column_type, column_default, column_key, extra FROM information_schema.columns WHERE table_name = '[TABLE_NAME]' ORDER BY ordinal_position ``` ``` UPDATE [TABLE_NAME] SET [DATA] WHERE WHERE id='[ID]' ``` It is clear the only injection point we have in both tables is the table name. One way to exploit this SQL Injection is by starting a multiline comment (/*) after the table we wish to update, and close it in one of our data parameters. That way our data could contain our altered WHERE statement, filtering the data by the configuration name instead of its ID. So, our payload thus far looks like this: ``` SELECT … WHERE table_name = 'config' /*' ORDER BY ordinal_position ``` ``` UPDATE config' /* SET field='*/ SET value=our_user_id WHERE name=site_admins-- WHERE id='[ID]' ``` But how can we insert a comment in the table name and still make both the SELECT statement and UPDATE statement to work? Clearly, the UPDATE statement will not work because of the added apostrophe after the table name, and the select statement will not work because we can’t open a multiline comment without closing it in SQL. What happens if we will insert the multiline comment into our table name without closing the string, and then just continue the WHERE condition using another OR statement? I mean, something like this: ``` SELECT … WHERE table_name = 'config /*' or table_name LIKE '%config' ORDER BY ordinal_position ``` ``` UPDATE config /*' or table_name LIKE '%config SET field='*/ SET value=our_user_id WHERE name=site_admins-- WHERE id='[ID]' ``` While the SELECT statement is pretty much self-explanatory, the UPDATE statement probably needs a bit more explanation. Allow me to display it the way MySQL will parse it: ``` UPDATE config /*' or table_name LIKE '%config SET field='*/ SET value=our_user_id WHERE name=site_admins -- WHERE id='[ID]' ``` As you can now clearly see, we’ve commented out everything between the table name and the first data value we control. That way, we can use SQL statements only on the UPDATE statement, without effecting the SELECT statement. We then just set the configuration value to whatever we want, preferably our user ID, and then just add our improved WHERE statement, filtering the table based on the configuration name. Finally, we add a single-line comment in order to remove the built-in WHERE statement, and that’s it. We successfully exploiting the same SQL Injection on two different queries. So, all we have to do now is wait for the configuration cache to refresh, which should happen every day or so, or just force refresh it ourselves by removing the configuration value of “allversionshash”, which stores a SHA-1 hash of all the core files in the system. Changing the value of this configuration will make Moodle think it went through a firmware update and just refresh the entire cache for us. After gaining full administrator privileges executing code is as simple as uploading a new plugin or template to the server. So, after exploiting some false assumptions, an Object Injection, a double SQL Injection and a permissive Administrator dashboard, we finally won. **We executed code on the server.**
idSSV:92807
last seen2017-11-19
modified2017-03-21
published2017-03-21
reporterRoot
titleMoodle Remote Code Execution Vulnerability (CVE-2017-2641)