Broken access control in GoAnywhere Admin portal

Table of content

  1. TL;DR
  2. Spot the bug
  3. First exploit attempt: accessing Dashboard and identifying some obstacles
  4. Second exploit attempt: Found some endpoint to exploit, create a high-privilege admin account from a low-privilege admin account
  5. Final exploit attempt: Preauth bypass access control, create a new high-privilege admin account
  6. Bonus
  7. Timeline (GMT +7)

0. TL;DR

With the authentication bugs, I can perform several actions like: creating a high-privileged admin account from a low-privileged admin account, accessing features that an admin account doesn't have access to, etc. But the most critical one (which also happens to be the easiest) is creating an admin account pre-authenticated:

  • Class SecurityFilter control access by comparing relative URIs with exact path → use /a/../ to bypass
  • SecurityFilter will block access to the first-time setup page if the relative URI path equals /wizard/InitialAccountSetup.xhtml and admin is set up → access using /a/../wizard/InitialAccountSetup.xhtml (violate the first condition)
  • Register a new admin account & profit

1. Spot the bug

After some time getting acquainted with the flow of the program, I realized that most of the authentication checks are in com.linoma.dpa.security.SecurityFilter.doFilter

This method has few if-conditions to determine if you can immediately pass this filter. I then proceed to dig into those conditions to see if there is anything interesting

One part of the code caught my attention

Stack trace
com.linoma.ga.core.iam.URLAuthorizationCache#getAction

this.map value is loaded from gamft-7.4.0.jar/com/linoma/dpa/iam/config/security-config.xml

security-config.xml

→ the access control check here is just a blacklist URI check

Therefore, I can easily bypass the whole filter with path traversal /a/../

2. First exploit attempt: accessing the Dashboard and identifying some obstacles

There are a few challenges when exploiting the server.

GoAnywhere uses the JSF framework, meaning the content of the page will be served in a “state”. Basically, I need to do a GET request first to initialize the state, and then I can send POSTs to edit that state

GET request will create a new viewstate
POST request with viewstate parameter

However, when access /goanywhere/a/../Dashboard.xhtml (or most of the pages), I receive a 500 error. This is because when accessing a page normally, JSF will render all components, including those that require user preferences. But since we’re not logged in yet, .getUserPreferences() will return null → Null Pointer Exception [1]

E.g. component com.linoma.ga.ui.admin.dashboard.DashboardForm#DashboardForm

After reading and analyzing the code a bit more carefully, I realized that I could use the header Faces-Request: partial/ajax to not render all components (which only much later that I know this is a JSF framework header) and only get the state ID

Without the ajax header → Internal Server Error
With ajax header

Voila, I now have the state of LoginSettings page

There are also a few other problems:

  • Some of the buttons can only be rendered if you have the authorization
Examples in ListUsers.xhtml
  • Some of the web functions require the page to be fully rendered with a POST request to get a new viewstate (no can do, because rendering the page fully will cause the 500 Internal error mentioned above [1])
Some functions require a non-ajax POST to go into next step with a new viewstate
  • When saving some system settings, the server requires a user session
Example com.linoma.ga.ui.admin.globalsettings.security.GlobalSecuritySettingsForm
  • When creating a new form to update/add new stuff, we (most of the time) need to initialize the form from a button on the previous page. If we access the form page directly, some forms will be empty because the form object needs to be inflated with some data when initializing

Example:

The form class corresponding with this page: com.linoma.ga.ui.admin.users.EditUserForm

Method to insert data into fields (in other words, "method to initialize the form"): com.linoma.ga.ui.admin.users.EditUserForm#setUserBE. The method is called in 3 different places:

Let’s see com.linoma.ga.ui.admin.users.ListUsersForm#editUser

editUser action is used in ListUsers.xhtml file

which is this edit button

Given the problems I faced, obviously, the impact is very limited with most of the endpoints

3. Second exploit attempt: Found some endpoint to exploit, create a high-privilege admin account from a low-privilege admin account

After a period of time, I finally found some ways for my exploit to actually have impacts on the server, such as:

  • Modify none-system settings (/godrive/GoDriveGlobalSettings.xhtml /security/loginmethods/LoginSettings.xhtml, etc.)
  • Use some JSF param to read settings/data from the server

We cannot fully render the page (because of the preferences stuff mentioned above), so how about partially rendering it?

It turns out I can use the param javax.faces.partial.render=id (on both POST and GET requests) to render only the required component. Therefore, other unnecessary components (such as component needs session.preferences) will not be rendered → no 500 Internal Server Error

Example: read web user settings in /security/WebUserSecuritySettings.xhtml
  • Exploit some form used to create stuff (because such form may not need to be initialized)
Some forms have methods to initialize inside the constructor → can access directly with URL
  • Use a low-privilege admin account to completely avoid any problems regarding the session (mentioned at [1]) 🤡

Impact: I can create a new full-role admin account from a zero-role admin account (need an admin account because, without one, the session thing will cause a Null Pointer Exception [1])

Stack trace

a post-auth privilege escalation bug, leads to the entire server compromised

4. Final exploit attempt: Preauth bypass access control, create a new high-privilege admin account

Still not satisfied with a post-auth bug, I tried harder to find other bugs to chain with to get a pre-auth RCE / Bypass access

After a long time, I realized that there’s an endpoint I completely forgot: /wizard/InitialAccountSetup.xhtml

This path only appears once, after you input the key to activate the server and start setting up the server

With this endpoint, I can easily add a new full-role admin account because the code that checks whether the server is already set up or not also lies in SecurityFilter

Compare the path with relative URI → can bypass the check with /a/../wizard/InitialAccountSetup.xhtml

Pre auth access bypass leads to the creation of high-privilege admin

5. Bonus

com.linoma.ga.ui.wc.application.WebClientSecurityFilter use startWith() to control access → also can be bypassed using ../ (credit to @q5ca)

6. Timeline

  • Sep 14 2023: Found the Post-auth privilege escalation bug
  • Oct 05 2023: Report to info@helpsystems.com
  • Oct 07 2023: Report to goanywhere.support@fortra.com
  • Oct 09 and Oct 10 2023: goanywhere.support@fortra.com reply and ask for CVE number. I explain that this is a new vulnerability and its impacts
  • Oct 12 2023: Found the Pre-auth access bypass bug
  • Oct 13 2023: Since this is a pre-auth bug, I quickly remind them about my report
  • Oct 25 2023: GoAnywhere (wrongly) closes my report case
  • Dec 04 2023: GoAnywhere published a security advisory about the bug

(Image from https://twitter.com/malcolmx0x/status/1732126084584620515)