Introduction

During a pentest I encountered password hashes from Oracle Apex. In this article we'll reverse Oracle Apex to describe how these hashes work and how they can be cracked.

We'll take an example hash from a 2013 tutorial from Oracle. In the tutorial we see a password hash.

  p_email_address=> 'martin.gubar@oracle.com',
  p_web_password => '8DF155F03639AD1FE5951213FE239CF9',
  p_web_password_format => 'HEX_ENCODED_DIGEST_V2',

This password hashing format was previously described here, but the post links to an attachement that is gone. Also, Oracle now supports different hashing methods, so let's find out what's going on under the hood.

Reversing Oracle Apex

We can download Apex 21.2 from here, which is the latest version at the time of writing. In the zip file you'll find that most of Oracle Apex are just SQL files to extend the Oracle database API, but in the core folder you'll find PLB files, which are PL SQL binaries.

We can decompile the PLB binaries with a script found here, which I ported to Python3 here. If you look at the script you can see that PLB files are just zlib-compressed files with some character mapping. I used the script to decompile all the PLB files in bulk. In them are more SQL scripts.

We're looking for the hashing format 'HEX_ENCODED_DIGEST_V2', which we can find in the file wwv_flow_fnd_user_int.plb. It shows that the 'password version' is '3;1;', which is important for later.

ELSIF P_WEB_PASSWORD_FORMAT = \'HEX_ENCODED_DIGEST_V2\' THEN
    L_HASHED_PASSWORD        := HEXTORAW(P_WEB_PASSWORD);
    L_PASSWORD_VERSION       := \'3;1;\';

The actual hashing function can be found in wwv_flow_crypto.plb. It parses the password version as [version]:[hashfunction]:[iterations].

FUNCTION HASH_PASSWORD (
    P_PASSWORD          IN VARCHAR2,
    P_VERSION           IN VARCHAR2,
    P_SECURITY_GROUP_ID IN NUMBER,
    P_USER_NAME         IN VARCHAR2,
    P_USER_ID           IN NUMBER )
    RETURN RAW
IS
    C_VERSION_NO    CONSTANT NUMBER          := REGEXP_SUBSTR (
                                                    SRCSTR        => P_VERSION,
                                                    PATTERN       => \'(\\d);(\\d);(\\d*)\',
                                                    SUBEXPRESSION => 1 );
   PRAGMA INLINE(NUMBER_TO_HASH_FUNCTION,\'YES\');
    C_FUNCTION      CONSTANT T_HASH_FUNCTION := NUMBER_TO_HASH_FUNCTION (
                                                    REGEXP_SUBSTR (
                                                        SRCSTR        => P_VERSION,
                                                        PATTERN       => \'(\\d);(\\d);(\\d*)\',
                                                        SUBEXPRESSION => 2 ));
    C_ITERATIONS    CONSTANT NUMBER          := REGEXP_SUBSTR (
                                                    SRCSTR        => P_VERSION,
                                                    PATTERN       => \'(\\d);(\\d);(\\d*)\',
                                                    SUBEXPRESSION => 3 );

The hash functions can be found in the enum below, where 1 maps to MD5. For some reason these enums start counting at 1 instead of 0.

C_HASH_FUNCTIONS CONSTANT WWV_FLOW_T_VARCHAR2 := WWV_FLOW_T_VARCHAR2 (
                                                     C_HASH_MD5,
                                                     C_HASH_SH1,
                                                     C_HASH_SH256,
                                                     C_HASH_SH384,
                                                     C_HASH_SH512 );

Later we find how the hash is build up when the hashing function is MD5.

IF C_FUNCTION = C_HASH_MD5 THEN
    L_SECRET := P_PASSWORD||NVL(P_SECURITY_GROUP_ID, 0)||
                WWV_FLOW_SECURITY.UPPER_SEC_OK(P_USER_NAME);

Cracking the hash

The above code shows that the hashing setup is MD5(password + securityGroupID + upper(username)). To crack this hash, we can set the security group and the username as salt values, and use hashcat's mode 10 (=md5(pwd.salt)) to crack it. The security group ID from the Oracle tutorial is 982802811840052. An example hash file based on the link from the Oracle tutorial would be:

8DF155F03639AD1FE5951213FE239CF9:982802811840052ADMIN2
F78CF371B6F636E27632F66691C2AE66:982802811840052DM
63079E96920A8B0D3B444521148887BF:982802811840052OLAPTRAIN

We can now run hashcat in mode 10 to crack them.

./hashcat -m 10 -a 0 hash ~/Documents/Wordlists/10_million_password_list_top_100000.txt
[...]
f78cf371b6f636e27632f66691c2ae66:982802811840052DM:oracle
63079e96920a8b0d3b444521148887bf:982802811840052OLAPTRAIN:oracle
[...]

Other supported hashes

Other hash functions are supported as can be seen in the rest of the SQL code of the hash function below. For example, version 5 of their hashing mechanism uses PBKDF with securityGroupID + username + userID as a salt. They still support all older hash types for backwards compatibility.

    CASE C_VERSION_NO
    WHEN 5 THEN
        RETURN PBKDF2 (
                   P_FUNCTION     => C_FUNCTION,
                   P_PASSWORD     => SYS.UTL_RAW.CAST_TO_RAW(P_PASSWORD),
                   P_SALT         => SYS.UTL_RAW.CAST_TO_RAW(NVL(P_SECURITY_GROUP_ID, 0)||P_USER_NAME||P_USER_ID),
                   P_ITERATIONS   => C_ITERATIONS,
                   P_RESULT_BYTES => NULL );
    WHEN 3 THEN
        IF C_FUNCTION = C_HASH_MD5 THEN
            L_SECRET := P_PASSWORD||NVL(P_SECURITY_GROUP_ID, 0)||
                        WWV_FLOW_SECURITY.UPPER_SEC_OK(P_USER_NAME); 
        ELSE
            L_SECRET := P_PASSWORD||NVL(P_SECURITY_GROUP_ID, 0)||P_USER_NAME||P_USER_ID;
        END IF;
        RETURN HASH_RAW (
                P_SRC      => L_SECRET,
                P_FUNCTION => C_FUNCTION );
    WHEN 1 THEN
        RETURN SYS.UTL_RAW.CAST_TO_VARCHAR2 (
                   HASH_RAW (
                       P_SRC      => P_PASSWORD,
                       P_FUNCTION => WWV_FLOW_CRYPTO.C_HASH_MD5 ));
    END CASE;