Skip to content

Instantly share code, notes, and snippets.

@Chaosvex
Created December 5, 2016 22:14
Show Gist options
  • Save Chaosvex/b62d8e7c24e60435b3834b5ec6b610a4 to your computer and use it in GitHub Desktop.
Save Chaosvex/b62d8e7c24e60435b3834b5ec6b610a4 to your computer and use it in GitHub Desktop.

Brief:

Vulnerability in authentication state machines allows for an attacker to bypass account security, allowing for server authentication without providing a password.

Affected emulators:

All versions of: Ascent, TrinityCore 3.x, MaNGOS, CMaNGOS, and all known forks.

Background:

World of Warcraft uses the SRP6 cryptographic algorithm to authenticate with servers without requiring the user's password to be sent over the Internet (in plaintext or as a hash).

Although the specific details of SRP6 are outside of the scope of this write-up, the integrity of this process requires that the server holds 'secret' values that the client cannot guess. Only by deriving its own SRP6 values from the user's password can it prove itself to the server.

The exploit takes advantage of lax checks in the server's state machine to send authentication packets out of order, allowing the attacker to manipulate the server's SRP6 variables to known values, thus undermining the integrity of the process.

A typical authentication session is as follows:

  1. Client sends a packet known as CMD_LOGON_CHALLENGE, containing information such as the username and game version.
  2. Server loads precomputed SRP6 values known as the salt and verifier from the database for the provided username and sends its own CMD_LOGON_CHALLENGE packet to the client containing 'seed' values to be used by the client in the SRP6 algorithm. These seed values are used to prevent replay attacks, where an attacker could otherwise capture packets to spoof a login.
  3. Client uses these seed values as well as the password to generate a 'proof'. This proof is sent to the server in a packet known as CMD_LOGON_PROOF.
  4. The server checks the client's proof against its own calculated proof, to ensure a match. If there is no match, the authentication attempt is rejected. If the proofs match, the server will send its own proof to the client in a CMD_LOGON_PROOF packet.
  5. The client verifies the server's proof. If it matches, the login is complete.

Exploit walkthrough:

This example discusses CMaNGOS. Other emulators, such as Ascent (which randomises the salt once per connection), may require slight variations but the general concept is the same.

When a player connects to the CMaNGOS authentication server, realmd, it instantiates an object known as AuthSocket. This object acts as a container for the client's login state and data such as the client socket, username and the SRP6 variables.

In AuthSocket.h, we can see the the variables used for the SRP6 calculations:

BigNumber N, s, g, v;
BigNumber b, B;

Only two of these values are initialised explicitly in the object's constructor, N (safe prime) and g (generator value). Both of these values are known to the client and aren't required to be kept secret.

/// Constructor - set the N and g values for SRP6
AuthSocket::AuthSocket(boost::asio::io_service &service, std::function<void (Socket *)> closeHandler)
    : Socket(service, closeHandler), _authed(false), _build(0), _accountSecurityLevel(SEC_PLAYER)
{
    N.SetHexStr("894B645E89E1535BBDAD5B8B290650530801B18EBFBF5E8FAB3C82872A3E9BB7");
    g.SetDword(7);
}

The rest of the BigNumber SRP6 values are initialised to zero.

Rather than starting the authentication session with a CMD_LOGON_PROOF, the attacker will initiate a reconnection attempt by sending CMD_RECONNECT_PROOF. Typically, a reconnection request is only made after a successful login (e.g. returning to the realm list after logging out of the game world). The server will read CMD_RECONNECT_PROOF and set the username variable to the value provided by the attacker.

_login = (const char*)ch->I;
_safelogin = _login;
LoginDatabase.escape_string(_safelogin);

Since the server does not correctly enforce the order of packets, the attacker is now free to send a CMD_LOGON_PROOF packet, rather than the expected CMD_RECONNECT_PROOF.

Since the CMD_LOGON_CHALLENGE packet has been skipped, the secret SRP6 values are still initialised to zero, rather than having been set by values loaded from the database.

 result = LoginDatabase.PQuery("SELECT sha_pass_hash,id,locked,last_ip,gmlevel,v,s FROM account WHERE username = '%s'", _safelogin.c_str());

With all secret SRP6 values known to the attacker, they can now perform the same SRP6 calculation as the server to derive the correct proof and send it to the server.

The server will accept the attacker's proof (lp.M1) and write a new session key to the database, using the username provided in the earlier CMD_RECONNECT_CHALLENGE.

///- Check if SRP6 results match (password is correct), else send an error
if (!memcmp(M.AsByteArray(), lp.M1, 20))
{
    ...
    
    LoginDatabase.PExecute("UPDATE account SET sessionkey = '%s', last_ip = '%s', last_login = NOW(), locale = '%u', failed_logins = 0 WHERE username = '%s'", K_hex, m_address.c_str(), GetLocaleByName(_localizationName), _safelogin.c_str());
    
    ...
    
    ///- Set _authed to true!
    _authed = true;
}

With the session key (sometimes referred to as the 'shared secret') written to the database, the account has now been compromised. At this point, the attacker may optionally send a CMD_LOGON_CHALLENGE packet.

Full debug output from a compromised authentication:

Accepting connection from '127.0.0.1'
[Auth] got data for cmd 2 recv length 174
Entering _HandleReconnectChallenge
[ReconnectChallenge] got header, body is 0x2b bytes
[ReconnectChallenge] got full packet, 0x2b bytes
[ReconnectChallenge] name(13): 'ADMINISTRATOR'
[Auth] got data for cmd 1 recv length 127
Entering _HandleLogonProof
S time:
A: '2B55691CF5A5F296A8914396CD2F4043C58947C102ED0E35636FFD63D45C9285'
v: '0
u: '476245DB850E0135C92911AF9FBA87B3971E61E8'
N: '894B645E89E1535BBDAD5B8B290650530801B18EBFBF5E8FAB3C82872A3E9BB7'
b: '0'
S: '01'
K: '74FFD4666B50A044D30D9016AC5E23A1BAF095CD754BBEC4245C9DBCAC03D451907C5CF26829
88E1'
Login: 'ADMINISTRATOR'
M: 'D39126BEE734A019B34BB81EE4552A638F41B961'
Client M1:
61b9418f632a55e41eb84bb319a034e7be2691d3
User 'ADMINISTRATOR' successfully authenticated
[Auth] got data for cmd 0 recv length 52
Entering _HandleLogonChallenge
[AuthChallenge] got header, body is 0x2b bytes
[AuthChallenge] got full packet, 0x2b bytes
[AuthChallenge] name(13): 'ADMINISTRATOR'
[AuthChallenge] Account 'ADMINISTRATOR' is not locked to ip
database authentication values: v='75DED16F3D4B1D284004CDE931DAB03D9860ADC48DB6F
889BB5F7A73CBCC9E94' s='87ECEDA24887386D85B966763E18533F392AF68C1DA242853B8167F3
EF6D035D'
[AuthChallenge] account ADMINISTRATOR is using 'enUS' locale (0)
[Auth] got data for cmd 16 recv length 5
Entering _HandleRealmList
Updating Realm List...

Patching:

The most robust fix to this exploit is to correctly enforce the protocol order. For example, if the client begins a session with CMD_RECONNECT_CHALLENGE, the server should not accept any subsequent packet that isn't a CMD_RECONNECT_PROOF.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment