RPCMS CVE Submission
Official Website: https://www.rpcms.cn/, https://github.com/ralap-z/RPCMS/
Version v1.8: https://github.com/ralap-z/rpcms/tree/74c49c4df0e0ac6b187def23bb2dadef40990dce (updated on 12th July 2021, latest version)
Audited on 20th July, 2021, credit to Zhang Zhiyi.
The source code can also be downloaded in this gist (rpcms_1.8.zip) folder
For better understanding of the problems, in-line comments are added in the key parts of source code after //
sign.
Vulnerability 1: Admin User Registration
When API is enabled, the registered user is normal user by default. However, user can manually set the role to be admin
to register as admin user.
//system/api/Member.class.php
public function register(){
$data=array();
$data['username']=!empty(input('post.username')) ? strip_tags(input('post.username')) : '';
$data['password']=!empty(input('post.password')) ? strip_tags(input('post.password')) : '';
$data['nickname']=!empty(input('post.nickname')) ? strip_tags(input('post.nickname')) : $data['username'];
$data['role']=!empty(input('post.role')) ? strip_tags(input('post.role')) : 'member';// normal user access by default
$data['email']=input('post.email');
$data['phone']=input('post.phone');
$data['status']=0;
$data['isCheck']=1;
...
$data['password']=psw($data['password']);
$res=Db::name('user')->insert($data);
Cache::update('user');
Hook::doHook('api_member_register',array($res));
$this->response($res,200,'注册成功!');
}
Trigger the vulnerability (0002.jpg)
Vulnerability 2: Stored XSS1
In the following code, $title
is displayed directly without sanitizing, resulting in stored XSS. Specifically, it can be triggered at |0|
and |1|
.
//system/index/Author.class.php
public function index(){
if(!isset($this->params[1]) || empty($this->params[1])){
redirect($this->App->baseUrl);
}
$data=explode('_',$this->params[1]);
$page=isset($data[1]) ? intval($data[1]) : 1;
$user=Cache::read('user');
if(is_numeric($data[0])){
$userId=intval($data[0]);
}else{
$user2=array_column($user,NULL,'nickname');
$userId=isset($user2[$data[0]]) ? $user2[$data[0]]['id'] : '';
}
if(empty($userId) || !isset($user[$userId])){
rpMsg('当前作者不存在!');
}
$LogsMod=new LogsMod();
$logData=$LogsMod->page($page)->order($this->getLogOrder(array('a.isTop'=>'desc')))->author($userId)->select();
$logData['count']=$user[$userId]['logNum'];
$title=$user[$userId]['nickname'];// nickname from the user
$pageHtml=pageInationHome($logData['count'],$logData['limit'],$logData['page'],'author',$userId);
$this->setKeywords();
$this->setDescription('关于作者'.$title.'的一些文章整理归档'); //|0|
$this->assign('title',$title.'-'.$this->webConfig['webName']); //|1|
$this->assign('listId',$userId);
$this->assign('listType','author');
$this->assign('logList',$logData['list']);
$this->assign('pageHtml',$pageHtml);
return $this->display('/list');
}
It can be seen that variable nickname
can be used by the attacker. By tracing the usage of nickname
before loading into database:
function update user password: updatePsw
public function updatePsw(){
$nickname=input('post.nickname');
$password=input('post.password');
$password2=input('post.password2');
if(empty($nickname)){
return json(array('code'=>-1,'msg'=>'昵称不可为空'));
}
if(!empty($password) && $password != $password2){
return json(array('code'=>-1,'msg'=>'两次密码输入不一致'));
}
$updata=array('nickname'=>$nickname);
if(!empty($password)){
$updata['password']=psw($password);
}
if($res=Db::name('user')->where('id='.$this->user['id'])->update($updata)){
Cache::update('user');
return json(array('code'=>200,'msg'=>'修改成功','data'=>!empty($password) ? 1 : 0));
}
return json(array('code'=>-1,'msg'=>'修改失败,请稍后重试'));
}
It can be seen that nickname
variable is not properly sanitized, taking the direct post input. A stored XSS can be found here.
Trigger the vulnerability: (0001.jpg)
## Vulnerability 3: Stored XSS
Similar to vulnerability 2, after enabling API, users can update to variable nickname
directly through API without strip tags. Once nickname
is updated, a stored xss can be triggered when user view articles.
//system/api/Member.class.php
public function post(){
$this->chechAuth(true);
$nickname=input('post.nickname'); //no filtering
$password=input('post.password');
$password2=input('post.password2');
if(empty($nickname)){
$this->response('',401,'昵称不可为空');
}
if(!empty($password) && $password != $password2){
$this->response('',401,'两次密码输入不一致');
}
$updata=array('nickname'=>$nickname);
if(!empty($password)){
$updata['password']=psw($password);
}
if($res=Db::name('user')->where('id='.self::$user['id'])->update($updata)){
Cache::update('user');
Hook::doHook('api_member_post',array(self::$user));
$this->response('',200,'修改成功');
}
$this->response('',401,'修改失败');
}