I. Background
I happened across a CMS and casually tested it for CSRF. The attack failed, so I inspected the CMS's CSRF defenses.
II. Process
A simple administration page

First, attempt a CSRF attack that adds an administrator.

Captured request:

The plaintext password is not worth dwelling on because its impact here is limited. More importantly, the data is transmitted as JSON. I previously wrote about constructing CSRF requests in this format at /talksafe/116.html, so I will not repeat the details. The resulting PoC is:
<html>
<head>
<title>This i a CSRF test!</title>
</head>
<form action="http://192.168.159.134/pagekit/index.php/api/user/1" method="post" enctype="text/plain" >
<input name='{"user":{"id":1,"username":"admin","email":"panda@cnpanda.net","url":"","registered":"2019-04-26T01:31:19+00:00","status":1,"name":"admin","login":"2019-04-26T01:54:08+00:00","permissions":null,"roles":[2,3],"data":{}},"password":"admin888"}'type='hidden'>
<input type=submit>
</form>
</html>
Submit the request. The result is:
Reviewing the captured request again revealed an X-XSRF-TOKEN header, presumably used to prevent CSRF. A CSRF header alone does not guarantee safety: if its token can be forged or reused, the header provides no meaningful protection. I then found the defense protecting administrator creation:
public function onRequest($event, $request)
{
$this->provider->setToken($request->get('_csrf', $request->headers->get('X-XSRF-TOKEN')));
$attributes = $request->attributes->get('_request', []);
if (isset($attributes['csrf']) && !$this->provider->validate()) {
throw new CsrfException('Invalid CSRF token.');
}
}
The request is passed into the function, which compares its CSRF token with the token generated by the system. If they do not match, it returns 'Invalid CSRF token.' Next, let us see how the token is generated:
public function generate()
{
return sha1($this->getSessionId().$this->getSessionToken());
}
The token is composed of two values produced by getSessionId() and getSessionToken(). Those functions are shown below:
protected function getSessionId()
{
if (!session_id()) {
session_start();
}
return session_id();
}
protected function getSessionToken()
{
if (!isset($_SESSION[$this->name])) {
$_SESSION[$this->name] = sha1(uniqid(rand(), true));
}
return $_SESSION[$this->name];
}
The first function checks whether a session exists and returns one if necessary. The second uses uniqid to generate a unique ID from the current time in microseconds, hashes it with SHA-1, combines it with the session, and hashes the result again. At this point, adding an administrator or changing an administrator password through CSRF is essentially infeasible.
III. Result
Vulnerability research does not mean selecting a CMS and assuming it will contain the vulnerability you want. Modern developers have more security awareness. Auditors must check not only whether a defense exists, but exactly how that defense is implemented.