PHPCMS漏洞重现--parse_str函数特性引发的漏洞
in 漏洞分析 with 0 comment

PHPCMS漏洞重现--parse_str函数特性引发的漏洞

in 漏洞分析 with 0 comment

0x01 准备工作

在开始这个漏洞的讲解之前,我们先介绍一下parse_str()这个函数
这个函数的作用是将字符串解析到变量中,举例子说明

<?php
 parse_str(“name=me&password=123”)
echo $name;
echo $password;
?>

这个结果就是me和123
再举个例子

<?php 
parse_str(“name=me&password=123”,$a)
print_r($a);
?>

这个输出结果是一个数组Array ( [name] => 'me' [password] => 123 )
讲完了这个函数,就进入我们的正题

0x02 漏洞分析

/phpcms/modules/member/index.phpaccount_manage_password方法

public function account_manage_password() {

  if(isset($_POST['dosubmit'])) {

   $updateinfo = array();

   if(!is_password($_POST['info']['password'])) {

    showmessage(L('password_format_incorrect'), HTTP_REFERER);

   }

   if($this->memberinfo['password'] != password($_POST['info']['password'], $this->memberinfo['encrypt'])) {

    showmessage(L('old_password_incorrect'), HTTP_REFERER);

   }

   

   //修改会员邮箱

   if($this->memberinfo['email'] != $_POST['info']['email'] && is_email($_POST['info']['email'])) {

    $email = $_POST['info']['email'];

    $updateinfo['email'] = $_POST['info']['email'];

   } else {

    $email = '';

   }

   $newpassword = password($_POST['info']['newpassword'], $this->memberinfo['encrypt']);

   $updateinfo['password'] = $newpassword;

   

   $this->db->update($updateinfo, array('userid'=>$this->memberinfo['userid']));

   if(pc_base::load_config('system', 'phpsso')) {

    //初始化phpsso

    $this->_init_phpsso();

    $res = $this->client->ps_member_edit('', $email, $_POST['info']['password'], $_POST['info']['newpassword'], $this->memberinfo['phpssouid'], $this->memberinfo['encrypt']); //漏洞关键点

    $message_error = array('-1'=>L('user_not_exist'), '-2'=>L('old_password_incorrect'), '-3'=>L('email_already_exist'), '-4'=>L('email_error'), '-5'=>L('param_error'));

    if ($res < 0) showmessage($message_error[$res]); }

我们可以看到,我们的数据都在ps_member_edit()这个方法中处理,跟踪ps_member_edit
phpcms/modules/member/classes/client.class.php

public function ps_member_edit($username, $email, $password='', $newpassword='', $uid='', $random='') {

  if($email && !$this->_is_email($email)) {

   return -4;

  }

  if ((!empty($username) && !is_string($username)) || (!empty($email) && !is_string($email)) || (!empty($password) && !is_string($password)) || (!empty($newpassword) && !is_string($newpassword))) {

   return -5;

  }

  return $this->_ps_send('edit', array('username'=>$username, 'password'=>$password, 'newpassword'=>$newpassword, 'email'=>$email, 'uid'=>$uid, 'random'=>$random));

 }

我们传递过来的数据又被发送到_ps_send中处理,我们就要继续跟踪_ps_send(),在这个页面中

private function _ps_send($action, $data = null) {

   return $this->_ps_post($this->ps_api_url."/index.php?m=phpsso&c=index&a=".$action, 500000, $this->auth_data($data));

 }

直接把我们的数据发送到/index.php?m=phpsso&c=index&a=edit我们先看一下_ps_post

private function _ps_post($url, $limit = 0, $post = '', $cookie = '', $ip = '', $timeout = 15, $block = true) {

  $return = '';

  $matches = parse_url($url);

  $host = $matches['host'];

  $path = $matches['path'] ? $matches['path'].($matches['query'] ? '?'.$matches['query'] : '') : '/';

  $port = !empty($matches['port']) ? $matches['port'] : 80;

  $siteurl = $this->_get_url();

  if($post) {

   $out = "POST $path HTTP/1.1\r\n";

   $out .= "Accept: */*\r\n";

   $out .= "Referer: ".$siteurl."\r\n";

   $out .= "Accept-Language: zh-cn\r\n";

   $out .= "Content-Type: application/x-www-form-urlencoded\r\n";

   $out .= "User-Agent: $_SERVER[HTTP_USER_AGENT]\r\n";

   $out .= "Host: $host\r\n" ;

   $out .= 'Content-Length: '.strlen($post)."\r\n" ;

   $out .= "Connection: Close\r\n" ;

   $out .= "Cache-Control: no-cache\r\n" ;

   $out .= "Cookie: $cookie\r\n\r\n" ;

   $out .= $post ;

  } else {

   $out = "GET $path HTTP/1.1\r\n";

   $out .= "Accept: */*\r\n";

   $out .= "Referer: ".$siteurl."\r\n";

   $out .= "Accept-Language: zh-cn\r\n";

   $out .= "User-Agent: $_SERVER[HTTP_USER_AGENT]\r\n";

   $out .= "Host: $host\r\n";

   $out .= "Connection: Close\r\n";

   $out .= "Cookie: $cookie\r\n\r\n";

  }

  $fp = @fsockopen(($ip ? $ip : $host), $port, $errno, $errstr, $timeout);

  if(!$fp) return '';

 

  stream_set_blocking($fp, $block);

  stream_set_timeout($fp, $timeout);

  @fwrite($fp, $out);

  $status = stream_get_meta_data($fp);

 

  if($status['timed_out']) return ''; 

  while (!feof($fp)) {

   if(($header = @fgets($fp)) && ($header == "\r\n" ||  $header == "\n"))  break;    

  }

  

  $stop = false;

  while(!feof($fp) && !$stop) {

   $data = fread($fp, ($limit == 0 || $limit > 8192 ? 8192 : $limit));

   $return .= $data;

   if($limit) {

    $limit -= strlen($data);

    $stop = $limit <= 0; } } @fclose($fp); //部分虚拟主机返回数值有误,暂不确定原因,过滤返回数据格式 $return_arr = explode("\n", $return); if(isset($return_arr[1])) { $return = trim($return_arr[1]); } unset($return_arr); return $return; }

这一部分就是模拟一个post的过程,具体的处理还在发送过去的页面中
数据发送到的页面是/phpsso_server/phpcms/modules/phpsso/index.php
追踪到edit()方法

public function edit() {

  $this->email = isset($this->data['email']) ? $this->data['email'] : '';

  $this->uid = isset($this->data['uid']) ? $this->data['uid'] : '';

  $userinfo = $this->getuserinfo(1);

  if (isset($this->data['password']) && !empty($this->data['password'])) {

   $this->password = create_password($this->data['password'], $userinfo['random']);

  }

  $this->random = !empty($this->data['random']) ? $this->data['random'] : $userinfo['random'];

  if (isset($this->data['newpassword']) && !empty($this->data['newpassword'])) {

   $this->newpassword = create_password($this->data['newpassword'], $this->random);

  }

  if ($userinfo == -1) {

   exit('-1');
  }

  if (isset($this->password) && !empty($this->password) && $userinfo['password'] != $this->password) {
   exit('-2');

  }

  if ($this->email && $userinfo['email'] != $this->email) {
   if($this->checkemail(1) == -1) exit('-3');
  } 

  $data = array();
  $data['appname'] = $this->applist[$this->appid]['name'];
  if (!empty($this->email) && $userinfo['email'] != $this->email) {

   $data['email'] = $this->email;

  }

  if (isset($this->newpassword) && $userinfo['password'] != $this->newpassword) {
   $data['password'] = $this->newpassword;

   $data['random'] = $this->random;

  }

  if (!empty($data)) {   

   //ucenter部份

   if ($this->config['ucuse']) {

    pc_base::load_config('uc_config');

    require_once PHPCMS_PATH.'api/uc_client/client.php';

    $r = uc_user_edit($userinfo['username'], '', (isset($this->data['newpassword']) && !empty($this->data['newpassword']) ? $this->data['newpassword'] : ''), $data['email'],1);

    if ($r != 1) {

     //{-1:用户不存在;-2:旧密码错误;-3:email已经存在 ;1:成功;0:未作修改}

     switch ($r) {

      case '-1':

       exit('-2');

      break;

      case '0':    

      case '-4':      

      case '-5':      

      case '-6':

      case '-7':

      case '-8':

       exit('0');

      break;

     }

    }

   }

   if (empty($data['email'])) unset($data['email']);
   /*插入消息队列*/

   $noticedata = $data;

   $noticedata['uid'] = $userinfo['uid'];

   messagequeue::add('member_edit', $noticedata);

   if($this->username) {

    $res = $this->db->update($data, array('username'=>$this->username));

   } else {

    $res = $this->db->update($data, array('uid'=>$this->uid));

   }

   exit("$res");

  } else {

   exit('0');

  }

 }

在追踪到该edit()函数之前,其中页面已经pc_base::load_app_class('phpsso', 'phpsso', 0);
调用了phpsso_server/phpcms/modules/phpsso/classes/phpsso.class.php对已加密的数据进行解密,其实edit()这个函数没有什么问题,就是对数据的存储更新,但是在phpsso.class.php这个页面中先对我们的数据进行了处理,其中该页面当中出现的漏洞即为


if(isset($_POST['data'])) {

   parse_str(sys_auth($_POST['data'], 'DECODE', $this->applist[$this->appid]['authkey']), $this->data);

    
   if(empty($this->data) || !is_array($this->data)) {

    exit('0');

   }

parse_str在php高版本当中在解析过程中会对编码进行一次解码
解析过程中会后续存在变量将会覆盖
username=me&password=22222&username=33333
最终为username=33333,password=22222
根据该特征:通过username=111111&password=22222&username=33333即可造成前面的变量覆盖
达到任意用户密码重置

Comments are closed.