Chamilo LMS... It's raining 0days, hallelujah, it's raining 0days Security researchers Mathieu Farrell and Sean Matthews identified multiple previously unknown vulnerabilities in the latest version of the open source Chamilo Learning Management System, including an unauthenticated SQL injection tracked as CVE-2026-61600 that chains to full pre-authentication remote code execution. The SQL injection was found through manual code review in main/inc/ajax/model.ajax.php and main/work/pending.php, where the course_id, work_parent_ids and groupOp values were concatenated directly into SQL WHERE clauses, and the researchers reported the flaws to the vendor, which fixed them and obtained CVE identifiers. Exploiting the SQL injection without authentication requires only a valid cidReq course code such as cidReq=TESTCOURSE, which can be derived from course titles listed on the home page or /main/auth/courses.php?action=display_courses. Authors Mathieu Farrell ./author/mathieu-farrell.html , Sean Matthews ./author/sean-matthews.html Category Vulnerability ./category/vulnerability.html Tags 2026 ./tag/2026.html , pentest ./tag/pentest.html , Chamilo ./tag/chamilo.html , PHP ./tag/php.html , vulnerability ./tag/vulnerability.html Chamilo https://chamilo.org is an open source Learning Management System LMS widely deployed in schools and enterprises around the world. In this blogpost we explain how we were able to identify multiple vulnerabilities including a full unauthenticated Remote Code Execution chain in the latest version. Introduction It is commonly observed that projects carried out outside of working hours begin with the same optimistic thought: "I will just take a quick look". Driven by skepticism toward automated security tools and AI-assisted code review, or, by a desire to manually validate whether they would have caught certain vulnerabilities. Sometimes, these audits confirm that traditional approaches remain effective, and sometimes, gaps are revealed that show that we do indeed require the usage of AI. This was the case with Chamilo https://github.com/chamilo/chamilo-lms , a widely deployed open source Learning Management System LMS that we observe being used by schools and enterprises around the world. What initially began as a fun security review quickly evolved into a much deeper exploration of the application's attack surface. As more components were audited, vulnerabilities were continuously surfaced, each revealing new attack primitives, and, in some cases, entirely different classes of security issues. By the end of the research, previously unknown vulnerabilities a.k.a. 0days had been identified, reported to the vendor, fixed, and the following CVE identifiers were assigned. Throughout this post, we will be diving into some vulnerabilities. More importantly, we will demonstrate how seemingly independent issues can be chained together to achieve a full Pre-Auth Remote Code Execution RCE , illustrating how different vulnerabilities can collectively lead to complete system compromise. SQL Injection without authentication CVE-2026-61600 This vulnerability was identified without the help of an LLM. The first vulnerability in the exploit chain is an unauthenticated SQL injection. This vulnerability was identified through manual review demonstrating that traditional code audit remains effective. The vulnerability was spotted in main/inc/ajax/model.ajax.php and main/work/pending.php. The following values were observed as being concatenated directly into SQL WHERE clauses. - course id - work parent ids - groupOp field from the JSON parameter filters The action get work student which is reachable without authentication, handles the JSON parameter filters . The value $filters- groupOp was concatenated directly into $whereCondition , and the resulting clause was passed to getWorkListStudent . 💡 Only information necessary to run the exploit: A valid cidReq e.g., cidReq=TESTCOURSE is needed to exploit this bug without authentication. Courses can be listed through the home page or /main/auth/courses.php?action=display courses. The course's code cidReq can be derived from the title of the course. Request HTTP : GET /main/inc/ajax/model.ajax.php?a=get work student& search=true&filters={%22groupOp%22:%22%20OR%20IF 1= SELECT+user id+FROM+user+WHERE+username%3d'admin' ,SLEEP 5 ,0 %20OR%20%22,%22rules%22: {%22field%22:1,%22op%22:%22eq%22,%22data%22:2},{%22field%22:%222%22,%22op%22:%22eq%22,%22data%22:%221%22} }&cidReq=TESTCOURSE HTTP/1.1 Host: 127.0.0.1 Response HTTP : HTTP/1.1 200 OK ... Content-Length: 32 Content-Type: application/json;charset=utf-8 {"page":0,"total":0,"records":0} Figure 1 - First example of unauthenticated SQLi exploited using a Time-Based attack part 1/2 . Figure 2 - First example of unauthenticated SQLi exploited using a Time-Based attack part 2/2 . Figure 3 - Second example of unauthenticated SQLi exploited using a Time-Based attack part 1/2 . Figure 4 - Second example of unauthenticated SQLi exploited using a Time-Based attack part 2/2 . Figure 5 - Third example of unauthenticated SQLi exploited using a Time-Based attack. After a password reset request is performed, it is possible to use the SQLi to retrieve the reset token stored in database. The request below resets the admin user's password. Request HTTP : POST /main/auth/lostPassword.php HTTP/1.1 Host: 127.0.0.1 Content-Type: application/x-www-form-urlencoded Content-Length: 38 Referer: http://127.0.0.1/main/auth/lostPassword.php user=admin&submit=& qf lost password= Response HTTP : HTTP/1.1 302 Found ... Location: http://127.0.0.1/ Content-Length: 0 Content-Type: text/html; charset=UTF-8 Once the reset token has been retrieved via SQL Injection, it can be used to reset the admin user's password with the following request. Request HTTP : POST /main/auth/reset.php?token=0ec87515cc65d60c36cf5c5b81d284f2 HTTP/1.1 Host: 127.0.0.1 Content-Type: application/x-www-form-urlencoded Content-Length: 98 pass1=admin1234567 &pass2=admin1234567 &submit=& qf reset=&token=0ec87515cc65d60c36cf5c5b81d284f2 Response HTTP : HTTP/1.1 302 Found ... Location: http://127.0.0.1/ Content-Length: 0 Content-Type: text/html; charset=UTF-8 In the full exploitation chain, the SQL injection vulnerability is also used to back up the database records associated with the administrator account before performing the email change and password reset. After gaining access to the admin account via email hijacking, the attacker can restore the original database records, reverting the email and hashed password. This approach minimizes traces of the attack and allows the administrator to continue using their original credentials, reducing the likelihood of detection while the attacker maintains backdoor access to the system. These findings identified through manual code audit along with the following research article Blind SQL Injection Attacks Optimization http://blog.quarkslab.com/resources/2026-09-01 chamilo-lms/r0.pdf , were provided to an LLM to develop a working Proof-Of-Concept. Email update to password reset without authentication This vulnerability was identified with the help of an LLM. The update users action in main/inc/ajax/user manager.ajax.php is designed to accept a JSON array of users and forward the submitted values to UserManager::update user without authentication enforcement. Because the request is fully controlled by the attacker for both the user id parameter and user fields such as email , an unauthenticated attacker can send a direct request to the AJAX endpoint and modify the email address of any user, including privileged accounts like admin . This vulnerability becomes critical when combined with the password reset feature because, by changing a user's email address, an attacker's controlled email address can be used to hijack an account by requesting a password reset and receiving the reset token. Figure 6 - Compromise flow of a user's email. The request enters through main/inc/ajax/user manager.ajax.php. File: main/inc/ajax/user manager.ajax.php php require once DIR .'/../global.inc.php'; $request = HttpRequest::createFromGlobals ; $isRequestByAjax = $request- isXmlHttpRequest ; $action = $ REQUEST 'a' ; switch $action { ... The code dispatches directly on the value of $ REQUEST 'a' . php ... case 'update users': $usersData = json decode $ POST 'users' , true ; $updatedCount = 0; foreach $usersData as $userData { if empty $userData 'user id' { continue; } $userId = int $userData 'user id' ; $currentUserData = api get user info $userId ; if $currentUserData { continue; } ... 1. Parses the JSON from $ POST 'users' . 2. Extracts user id . 3. Casts it to integer. 4. Loads the current data for user 1 for administrator . 5. Continues only if that user exists. Then the code builds $updatedData by mixing submitted values with the current database values using a fallback pattern. php $updatedData = 'firstname' = $userData 'firstname' ?? $currentUserData 'firstname' , 'lastname' = $userData 'lastname' ?? $currentUserData 'lastname' , 'email' = $userData 'email' ?? $currentUserData 'email' , 'phone' = $userData 'phone' ?? $currentUserData 'phone' , 'official code' = $userData 'official code' ?? $currentUserData 'official code' , 'status' = isset $userData 'status' ? int $userData 'status' : $currentUserData 'status' , 'active' = isset $userData 'active' ? int $userData 'active' : $currentUserData 'active' , ; If the payload only contains {"user id":1,"email":"attacker@evil.com"} , then, only the email is changed to the attacker's address. All other fields are preserved from the original user record, making the modification appear as a legitimate account update. This means the email field can be selectively modified without any authentication check, allowing complete account hijacking when paired with the password reset feature. The AJAX action then forwards the values into UserManager::update user . UserManager::update user $userId, $updatedData 'firstname' , $updatedData 'lastname' , $currentUserData 'username' , $updatedData 'password' ?? null, $currentUserData 'auth source' , $updatedData 'email' , $updatedData 'status' , $updatedData 'official code' , $updatedData 'phone' , $currentUserData 'picture uri' , null, $updatedData 'active' , null, null, null, $currentUserData 'language' ; For user 1 , this means the current username and other values are reused, while the submitted email is passed through as the new target value. Inside main/inc/lib/usermanager.lib.php, update user loads the Doctrine user entity and applies the new field values. php $userManager = self::getManager ; / @var User $user / $user = self::getRepository - find $user id ; if empty $user { return false; } Then: php $user - setLastname $lastname - setFirstname $firstname - setUsername $username - setStatus $status - setAuthSource $auth source - setLanguage $language - setEmail $email - setOfficialCode $official code - setPhone $phone - setAddress $address - setPictureUri $picture uri - setExpirationDate $expiration date - setActive $active - setEnabled $active - setHrDeptId $hr dept id ; The key line is: php - setEmail $email Finally, the entity is persisted. php $userManager- updateUser $user, true ; Unserialize to Arbitrary File Write to RCE as admin CVE-2026-70647 This vulnerability was identified without the help of an LLM. Chamilo's course backup import feature, unsafely handle attacker-controlled serialized data from course info.dat. During backup creation, CourseArchiver::createBackup writes a base64-encoded serialized Course object to this file, which stores course files using the paths found within the serialized resource objects. During import, CourseArchiver::readCourse extracts a user-supplied ZIP archive, reads course info.dat, base64-decodes it, and deserializes it through UnserializeApi::unserialize 'course', ... . This process is intended to restore the course structure and all associated documents to their original locations within the application directory. The deserialization allowlist in UnserializeApi is designed to include only legitimate classes needed for course restoration, specifically Course and Document . An attacker does not need a PHP Object Injection POP chain since properties on valid serialized objects can be directly manipulated. A Document resource's path property can be modified from a legitimate value like document/payload.txt to an arbitrary path such as coiffeur.php or any location within the web root. When the manipulated backup is imported, Chamilo trusts the modified path values and uses them during file restoration, writing files to malicious locations. This Arbitrary File Write can be leveraged to place a PHP webshell within the application directory, leading to Remote Code Execution. Figure 7 - Exploiting the course import feature. Final exploit Boolean-Based Blind SQL Injection CVE-2026-61600 The proof of concept starts by exploiting the SQL injections via a Boolean-Based Blind attack. For each byte we want to recover, eight separate requests are sent testing each bit position until the entire byte value is reconstructed. Slowly, the entire admin user row from the database is dumped. Extracting the password reset token Among the data leaked through the SQLi vulnerability is the confirmation token field. A second extraction pass is performed targeting just this column, recovering its exact value. By stealing it through SQL injection, the email verification step can be bypassed entirely and a valid reset credential is gained without needing access to the admin's mailbox. Email hijacking only if needed The update users AJAX action allows an unauthenticated attacker to modify user attributes of any existing account within the system. By exploiting this vulnerability, arbitrary fields such as email addresses or phone numbers can be selectively updated for any user record, including privileged administrative accounts. This allows complete account takeover without any authentication requirement or notification to the original account holder, making it a powerful component of our chain. Resetting the admin's password Once the confirmation token has been stolen, a request is made to Chamilo's password reset endpoint and a new password is defined. Chamilo validates the token that is provided which matches the database , sees it as legitimate, and changes the admin account password to our chosen value. From that moment on, we can log into the admin account with this new password. The original admin still has no idea their account has been compromised because their email was never involved. Logging in and creating a malicious course Using the new password, we can log into Chamilo as the legitimate administrator. Now a dummy course can be created with a random name and a malicious PHP file likely a webshell is immediately uploaded into the course's document folder with an extension accepted by Chamilo . This file appears harmless as it is just sitting in the course like any normal document. Then, the course backup feature is triggered, which creates a ZIP archive containing all the course contents, including this malicious file, along with serialized metadata about the course structure stored in a file called course info.dat . Modifying backup metadata CVE-2026-70647 Before uploading the backup back to the server, it is modified locally. The course info.dat file inside the backup is base64-encoded PHP serialized data that describes all documents and their paths. This ZIP is extracted, the data is decoded and deserialized, the reference to the uploaded malicious file is found, and it is changed to point to a different filename coiffeur.php , which places it directly in the web accessible directory where webshells can execute. The data is then re-serialized, re-encoded to base64, repackaged into the ZIP, and prepared for uploading to the server. Importing the malicious backup and achieving RCE CVE-2026-70647 Legitimate course import functionality is used to upload the modified backup file. When Chamilo processes the import, the course info.dat metadata is deserialized using PHP's unserialize function. Because the metadata has been tampered with to rename the malicious PHP file to coiffeur.php and place it in the course root, the deserialization process writes the file to its final location in a web accessible directory. Once the import is completed, the webshell can be accessed at /app/courses/{course code}/coiffeur.php and arbitrary PHP code can be executed on the server, achieving complete Remote Code Execution with the privileges of the web server. POC File: exploit.py http://blog.quarkslab.com/resources/2026-09-01 chamilo-lms/exploit/exploit.py bash /usr/bin/env python3 /\ .-----. /\ \\/ \ \\ |/\| 0 |/\| \\\;-----; /\\ \/ . \/ \\ | ,- |coiffeur| -, | \.-.-./ \\ /.- -.\ \\ \ | ' ' | / | | \ / Title: Chamilo-LMS 1.11.36 0days full chain exploit. Author: Mathieu Farrell aka @Coiffeur0x90 Date: 2026-04-01 Summary: Exploit chains a pre-auth SQLi & ATO if needed then an unserialize to Arbitrary File Write to write a Webshell RCE . Details: Dump the admin row, try the reset-token path, fall back to account takeover when needed, the create, rewrite, and re-import a course backup to drop "coiffeur.php". ... The exploit uses the file payload.txt http://blog.quarkslab.com/resources/2026-09-01 chamilo-lms/exploit/payload.txt , which is ultimately renamed to coiffeur.php and works as a web shell. Conclusion AI is an extremely powerful tool for offensive security research, but in my experience, humans still outperform AI when it comes to creative ideas. Rather than replacing the researcher, AI should be seen as a force multiplier that can accelerate analysis, explore hypotheses, process large amounts of information, and help researchers investigate areas that might otherwise take much more time. The real game changer comes from combining human creativity, intuition, and experience with the scale and power of AI. Humans provide the ideas and direction, while AI helps turn those ideas into deeper and faster research. This combination has the potential to fundamentally change the way vulnerabilities are discovered and push offensive security research far beyond what either humans or AI could achieve alone. Appendix Other vulnerabilities identified Arbitrary File Delete in plugin/cleandeletedfiles/src/ajax.php as admin CVE-2026-61578 This vulnerability was identified without the help of an LLM. File: plugin/cleandeletedfiles/src/ajax.php php