Finding a CSRF Vulnerability in a CMS

SummaryI. Background. I happened across a CMS and casually tested it for CSRF. The attack failed, so I inspected its CSRF defense. II. Process. Start from a simple administration page and attempt a CSRF attack that adds an administrator. The request transmits a plaintext password and uses JSON. I previously described how to construct a CSRF request in this format at /talksafe/116…

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

1.png

First, attempt a CSRF attack that adds an administrator.

2.png

Captured request:

3.png

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:

TEXT
<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:

TEXT
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:

TEXT
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:

TEXT
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.