Skip to content

Instantly share code, notes, and snippets.

@victomteng1997
Last active July 21, 2021 05:28
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save victomteng1997/bfa1e0e07dd22f7e0b13256eda79626f to your computer and use it in GitHub Desktop.
Save victomteng1997/bfa1e0e07dd22f7e0b13256eda79626f to your computer and use it in GitHub Desktop.
RPCMS CVE Submission

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) bypass

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) xss

## 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,'修改失败');
}
This file has been truncated, but you can view the full file.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment