当前位置:WooYun >> 漏洞信息

漏洞概要 关注数(24) 关注此漏洞

缺陷编号:wooyun-2015-0131548

漏洞标题:phpcms一个函数引起的安全漏洞(任意密码重置、多个SQL注入、甚至getshell)

相关厂商:phpcms

漏洞作者: 路人甲

提交时间:2015-08-04 15:22

修复时间:2015-11-02 15:44

公开时间:2015-11-02 15:44

漏洞类型:设计缺陷/逻辑错误

危害等级:高

自评Rank:20

漏洞状态:厂商已经确认

漏洞来源: http://www.wooyun.org,如有疑问或需要帮助请联系 [email protected]

Tags标签:

4人收藏 收藏
分享漏洞:


漏洞详情

披露状态:

2015-08-04: 细节已通知厂商并且等待厂商处理中
2015-08-04: 厂商已经确认,细节仅向厂商公开
2015-08-07: 细节向第三方安全合作伙伴开放
2015-09-28: 细节向核心白帽子及相关领域专家公开
2015-10-08: 细节向普通白帽子公开
2015-10-18: 细节向实习白帽子公开
2015-11-02: 细节向公众公开

简要描述:

拖了好久。。。一直酗酒。。。。忙。。。按照剑心的要求,写上来吧。
最后说一句。。。360就一坨屎。。。一个问题重复的做死的爆。。。我就一次爆完算了。。艹

详细说明:

本文当中所有安全问题都将围绕parse_str()这个函数展开,parse_str函数在php 版本当中,在对变量进行解析过程中,有解码、以及变量后置等安全问题,这里不啰嗦,看问题。
任意密码重置:
在/phpcms/modules/member/index.php中account_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 在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

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;
}


继续上追踪到/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对已加密的数据进行解密
其中该页面当中出现的漏洞即为。

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=111111&password=22222&username=33333,最终为username=33333,password=22222.
根据该特征:
1、通过引入%2527即可带入单引号,即产生注入,甚至getshell。
2、通过username=111111&password=22222&username=33333即可造成前面的变量覆盖,如达到任意用户密码重置

漏洞证明:

一、任意用户密码重置漏洞详细说明如下:
通过$_POST['info']['password']获取的变量设置为:

1-1.png


该值在经过parse_str解析前,应该是info[email][email protected]&info[password]=test1234&info%5Bnewpassword%5D=test1234a%26username%3dtest12345
经过 if(isset($_POST['data'])) {
parse_str(sys_auth($_POST['data'], 'DECODE', $this->applist[$this->appid]['authkey']), $this->data);
由于前面提到的username会覆盖。。。
在/phpsso_server/phpcms/modules/phpsso/index.php第15行中
$this->username = isset($this->data['username']) ? $this->data['username'] : '';
将会产生一个username的变量,即后面可控,即可控制任何人的用户名
在phpsso_server/phpcms/modules/phpsso/index.php中edit方法

….//省略若干代码
$noticedata['uid'] = $userinfo['uid'];
messagequeue::add('member_edit', $noticedata);
if($this->username) {
$res = $this->db->update($data, array('username'=>$this->username));//这里的$this->username即为我们为变量覆盖的值,任意定义,漏洞触发店
} else {
$res = $this->db->update($data, array('uid'=>$this->uid));
}


这里即更改了不属于我们的用户test12345的密码了,这就是任意密码重置漏洞。
二、SQL注入漏洞详细说明如下(注入有多个,前台无需登录也有,会员中心也有):
注入主要是通过%2527经过2次解码进入数据库。。。
涉及SQL注入的方法有login \ public_checkname_ajax \ register \ account_manage_password …..等等,所以你会看到有好多好多的注入。。。哈哈,并且这些SQL注入还可以getshell,因为可以带入单引号进入写shell (前提你得有路径。不过phpcms也有报路径方法的,但是之前的爆路径的方法修复了。)
在这里只说明一处,其他的可以按照这个去发掘。。
我就说一下account_manage_password这个方法,其他的sql注入我就不一一爆了原理一样2次解码%2527带入单引号。。。
我们还是选取account_manage_password方法
我们设置newpassword (新密码)为test123456a%26username%3dtest12345%2527
如图:

1-2.png


其中username进入数据库
我们在mysql.class.php update方法将sql语句记录下来,无法直接回显,因为内部接口通信,所以你直接echo不出来的

$sql = 'UPDATE `'.$this->config['database'].'`.`'.$table.'` SET '.$field.$where;
file_put_contents("fuck4.txt",$sql);//exit();


1-3.png


UPDATE `phpcms`.`v9_sso_members` SET `appname`='phpcms v9',`email`='[email protected]',`password`='8a58c2fcafc50bccb9b3b97c1871e31e',`random`='xcnhzq' WHERE `username` = 'test12345''
这个注入点也就是二次注入…..但是这个注入点不是回显的,知道为什么吗?
因为前面已经说了,看看client.class.php中的_ps_send方法(内部通信)
所以这个注入点不回显的,所以利用方式,你就是如何把他变回显,或者直接写shell,延时注入….
后面真正的poc就不再提供,杀人原理已经给出来,杀人工具不再造。。。如果你利用的好,直接sqlmap都可以跑数据,相信自己。。。。
总之围绕这个函数的安全问题就全部总结了,包括任意用户密码重置、SQL注入漏洞、xss、getshell等等

修复方案:

重新对该函数进行修改。。。

版权声明:转载请注明来源 路人甲@乌云


漏洞回应

厂商回应:

危害等级:高

漏洞Rank:15

确认时间:2015-08-04 15:42

厂商回复:

感谢提出

最新状态:

暂无