CREATE OR REPLACE FUNCTION pentest_password_verify ( fp_username VARCHAR2 , fp_password VARCHAR2 , fp_old_password VARCHAR2 ) RETURN boolean ------------------------------------------------------------------------------- -- Author: Simon Fletcher, Pentest Limited -- Contributors: Andy and Howard of the Oracle Ethical Hacking Team, -- Tim Fenby of Pentest -- Program: Password verification function -- This function is designed for use with the PROFILE resource -- parameter PASSWORD_VERIFY_FUNCTION. It is intended as an -- alternative to the Oracle supplied VERIFY_FUNCTION (created -- by the UTLPWDMG.SQL script). -- Release: 03 April 2008 -- $Revision: 22 $ -- $Date: 2008-04-03 10:51:55 +0100 (Thu, 03 Apr 2008) $ ------------------------------------------------------------------------------- -- Copyright 2007, Simon Fletcher, email: simon.fletcher@pentest.co.uk -- -- -- -- This program is free software: you can redistribute it and/or modify -- -- it under the terms of the GNU General Public License as published by -- -- the Free Software Foundation, either version 3 of the License, or -- -- (at your option) any later version. -- -- -- -- This program is distributed in the hope that it will be useful, -- -- but WITHOUT ANY WARRANTY; without even the implied warranty of -- -- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -- -- GNU General Public License for more details. -- -- -- -- For a copy of the GNU General Public License see -- -- . -- ------------------------------------------------------------------------------- -- Note: Compatibile with Oracle10g and onwards -- -- Usage: -- Step 1: Amend the constants below to reflect your password policy -- Step 2: Create the function in your SYS schema -- Step 3: Specify the function name in your database profile -- e.g. -- CREATE PROFILE secure LIMIT -- FAILED_LOGIN_ATTEMPTS 5 -- PASSWORD_LIFE_TIME 30 -- PASSWORD_GRACE_TIME 7 -- PASSWORD_REUSE_TIME UNLIMITED -- PASSWORD_REUSE_MAX UNLIMITED -- PASSWORD_LOCK_TIME 1/24 -- PASSWORD_VERIFY_FUNCTION pentest_password_verify; -- Step 4: Assign the profile to your users -- e.g. -- ALTER USER system PROFILE secure; ------------------------------------------------------------------------------- IS -- Types TYPE char_idx_tabtype IS TABLE OF BINARY_INTEGER INDEX BY VARCHAR2(1); TYPE password_tabtype IS TABLE OF VARCHAR2(30); -- Exceptions contains_forbidden_word EXCEPTION; contains_rev_username EXCEPTION; contains_username EXCEPTION; not_diverse EXCEPTION; not_enough_change EXCEPTION; similar_to_old_password EXCEPTION; similar_to_forbidden_word EXCEPTION; similar_to_username EXCEPTION; too_few_alphas EXCEPTION; too_few_lowers EXCEPTION; too_few_numerics EXCEPTION; too_few_punct EXCEPTION; too_few_spec_punct EXCEPTION; too_few_std_punct EXCEPTION; too_few_uppers EXCEPTION; too_many_dup_characters EXCEPTION; too_short EXCEPTION; -- Enforce password to not contain the username c_chk_eq_username CONSTANT BOOLEAN := TRUE; -- Enforce password to not contain the username in reverse c_chk_eq_rev_username CONSTANT BOOLEAN := TRUE; -- Enforce to not be similar to the username c_chk_like_username CONSTANT BOOLEAN := TRUE; -- Enforce to not be similar to the previous password c_chk_like_old_password CONSTANT BOOLEAN := FALSE; -- Password minimum length c_min_length CONSTANT BINARY_INTEGER := 10; -- Enforce password to not contain a forbidden word c_chk_eq_forbidden CONSTANT BOOLEAN := TRUE; -- Enforce password to not be like a forbidden word c_chk_like_forbidden CONSTANT BOOLEAN := FALSE; -- Add dbname to forbidden word list c_add_dbname_to_forbidden CONSTANT BOOLEAN := TRUE; -- Add host name to forbidden word list c_add_hostname_to_forbidden CONSTANT BOOLEAN := TRUE; -- Add month to forbidden word list c_add_month_to_forbidden CONSTANT BOOLEAN := TRUE; -- Minimum characters from particular charactersets -- NOTE: c_min_lower and c_min_upper only checked with case sensitive passwords c_min_alpha CONSTANT BINARY_INTEGER := 1; c_min_lower CONSTANT BINARY_INTEGER := 1; c_min_upper CONSTANT BINARY_INTEGER := 1; c_min_numeric CONSTANT BINARY_INTEGER := 1; c_min_punct CONSTANT BINARY_INTEGER := 1; c_min_standard_punct CONSTANT BINARY_INTEGER := 0; c_min_special_punct CONSTANT BINARY_INTEGER := 0; -- Maximum number of times a single character can be used within the password -- NOTE: This check is case insensitive c_max_char_duplication CONSTANT BINARY_INTEGER := 6; -- Minimum number of different characters used -- NOTE: This check is case insensitive c_min_char_diversity CONSTANT BINARY_INTEGER := 5; -- Minimum number of differing characters from previous password -- NOTE: This check is case insensitive c_min_diff_from_prev CONSTANT BINARY_INTEGER := 4; -- Forbidden passwords -- NOTE: Customise this list for your organisation v_password_tab password_tabtype := password_tabtype ( 'WELCOME' , 'PASSWORD' , 'ORACLE' , 'DATABASE' , 'LETMEIN' , 'FORGOTTEN' , 'MANAGER' , 'CHANGE_ON_INSTALL' , 'ABCDEF' , 'QWERTY' , '123456' , 'PENTEST' ); -- Working variables v_alpha_count BINARY_INTEGER := 0; v_case_sensitive BOOLEAN; v_char_idx_tab char_idx_tabtype; v_charidx01 VARCHAR2(1); v_differ_count BINARY_INTEGER := 0; v_idx01 BINARY_INTEGER := 0; v_lower_count BINARY_INTEGER := 0; v_numeric_count BINARY_INTEGER := 0; v_punct_count BINARY_INTEGER := 0; v_reverse VARCHAR2(32767); v_specpunct_count BINARY_INTEGER := 0; v_stdpunct_count BINARY_INTEGER := 0; v_upper_count BINARY_INTEGER := 0; -- Function to retrieve the v_sec_case_sensitive_logon initialisation parameter -- Applies to releases Oracle11g onwards FUNCTION f_sec_case_sensitive_logon RETURN boolean IS v_sec_case_sensitive_logon sys.v_$parameter.value%TYPE; BEGIN -- Query initialisation parameters for sec_case_sensitive_logon SELECT value INTO v_sec_case_sensitive_logon FROM sys.v_$parameter WHERE name = 'sec_case_sensitive_logon'; IF v_sec_case_sensitive_logon = 'TRUE' THEN RETURN TRUE; ELSE RETURN FALSE; END IF; EXCEPTION WHEN NO_DATA_FOUND THEN RETURN FALSE; WHEN OTHERS THEN raise_application_error ( -20000 , 'f_sec_case_sensitive_logon: '||SQLERRM , TRUE); END f_sec_case_sensitive_logon; -- Function to retrieve the dbname from v_$database FUNCTION f_dbname RETURN VARCHAR2 IS v_dbname sys.v_$database.name%TYPE; BEGIN -- Query v_$database for database name SELECT name INTO v_dbname FROM sys.v_$database; RETURN v_dbname; EXCEPTION WHEN OTHERS THEN raise_application_error ( -20000 , 'f_dbname: '||SQLERRM , TRUE); END f_dbname; -- Function to retrieve the hostname from v_$instance FUNCTION f_hostname RETURN VARCHAR2 IS v_hostname sys.v_$instance.host_name%TYPE; BEGIN -- Query v_$instance for host name SELECT host_name INTO v_hostname FROM sys.v_$instance; RETURN v_hostname; EXCEPTION WHEN OTHERS THEN raise_application_error ( -20000 , 'f_hostname: '||SQLERRM , TRUE); END f_hostname; BEGIN -- Set case sensitivity based on initialisation parameter v_case_sensitive := f_sec_case_sensitive_logon; -- Enforce to not contain the username IF c_chk_eq_username AND instr(nls_lower(fp_password), nls_lower(fp_username)) > 0 THEN RAISE contains_username; END IF; -- Enforce to not contain the reverse of the username IF c_chk_eq_rev_username THEN v_reverse := NULL; <> FOR v_idx01 IN REVERSE 1..length(fp_username) LOOP v_reverse := v_reverse||substr(fp_username, v_idx01, 1); END LOOP reverse_loop; IF instr(nls_lower(fp_password), nls_lower(v_reverse)) > 0 THEN RAISE contains_rev_username; END IF; END IF; -- Enforce to not be similar to the username IF c_chk_like_username AND soundex( fp_password ) = soundex( fp_username ) THEN RAISE similar_to_username; END IF; -- Check the length of the password IF length(fp_password) < c_min_length THEN RAISE too_short; END IF; -- Enforce to not be similar to old password IF c_chk_like_old_password AND fp_old_password IS NOT NULL AND soundex( fp_password ) = soundex( fp_old_password ) THEN RAISE similar_to_old_password; END IF; -- Enforce forbidden words rules IF c_chk_eq_forbidden OR c_chk_like_forbidden THEN -- Add database name to list of forbidden words IF c_add_dbname_to_forbidden THEN v_password_tab.EXTEND; v_password_tab(v_password_tab.LAST) := f_dbname; END IF; -- Add host name to list of forbidden words IF c_add_hostname_to_forbidden THEN v_password_tab.EXTEND; v_password_tab(v_password_tab.LAST) := f_hostname; END IF; -- Add current month to list of forbidden words IF c_add_month_to_forbidden THEN v_password_tab.EXTEND; v_password_tab(v_password_tab.LAST) := rtrim( to_char( sysdate, 'MONTH' ) ); END IF; <> FOR v_idx in v_password_tab.FIRST .. v_password_tab.LAST LOOP -- Enforce password to not contain a forbidden word IF c_chk_eq_forbidden AND instr(nls_lower(fp_password), nls_lower(v_password_tab(v_idx))) > 0 THEN RAISE contains_forbidden_word; END IF; -- Enforce password to not be like a forbidden word IF c_chk_like_forbidden AND soundex(fp_password) = soundex(v_password_tab(v_idx)) THEN RAISE similar_to_forbidden_word; END IF; END LOOP weakpw_loop; END IF; <> FOR v_idx01 IN 1..length(fp_password) LOOP -- Compare passwords for differences IF fp_old_password IS NOT NULL THEN IF nls_lower(substr(fp_password,v_idx01,1)) != nls_lower(substr(fp_old_password,v_idx01,1)) OR substr(fp_old_password,v_idx01,1) IS NULL THEN v_differ_count := v_differ_count + 1; END IF; END IF; -- Build table of counts per character (case insensitive) IF v_char_idx_tab.EXISTS(nls_upper(substr(fp_password,v_idx01,1))) THEN v_char_idx_tab(nls_upper(substr(fp_password,v_idx01,1))) := v_char_idx_tab(nls_upper(substr(fp_password,v_idx01,1))) + 1; ELSE v_char_idx_tab(nls_upper(substr(fp_password,v_idx01,1))) := 1; END IF; CASE -- Count numeric characters WHEN regexp_like( substr(fp_password,v_idx01,1), '^[[:digit:]]$' ) THEN v_numeric_count := v_numeric_count + 1; -- Count alphabetic characters WHEN regexp_like( substr(fp_password,v_idx01,1), '^[[:alpha:]]$' ) THEN v_alpha_count := v_alpha_count + 1; CASE -- Count uppercase characters WHEN regexp_like( substr(fp_password,v_idx01,1), '^[[:upper:]]$' ) THEN v_upper_count := v_upper_count + 1; -- Count lowercase characters WHEN regexp_like( substr(fp_password,v_idx01,1), '^[[:lower:]]$' ) THEN v_lower_count := v_lower_count + 1; END CASE; -- Count standard oracle punctuation characters WHEN regexp_like( substr(fp_password,v_idx01,1), '^[_,#,$]$' ) THEN v_stdpunct_count := v_stdpunct_count + 1; -- Count other characters ELSE v_specpunct_count := v_specpunct_count + 1; END CASE; END LOOP character_loop; -- Enforce minimum number of different characters from previous password IF fp_old_password IS NOT NULL THEN IF length(fp_old_password) > length(fp_password) THEN v_differ_count := v_differ_count + (length(fp_old_password) - length(fp_password)); END IF; IF v_differ_count < c_min_diff_from_prev THEN RAISE not_enough_change; END IF; END IF; -- Enforce minimum number of alphabetic characters IF v_alpha_count < c_min_alpha THEN RAISE too_few_alphas; END IF; IF v_case_sensitive THEN -- Enforce minimum number of uppercase characters IF v_upper_count < c_min_upper THEN RAISE too_few_uppers; END IF; -- Enforce minimum number of lowercase characters IF v_lower_count < c_min_lower THEN RAISE too_few_lowers; END IF; END IF; -- Enforce minimum number of numeric characters IF v_numeric_count < c_min_numeric THEN RAISE too_few_numerics; END IF; -- Enforce minimum number of punctuation characters v_punct_count := v_stdpunct_count + v_specpunct_count; IF v_punct_count < c_min_punct THEN RAISE too_few_punct; END IF; -- Enforce minimum number of standard punctuation characters IF v_stdpunct_count < c_min_standard_punct THEN RAISE too_few_std_punct; END IF; -- Enforce minimum number of special punctuation characters IF v_specpunct_count < c_min_special_punct THEN RAISE too_few_spec_punct; END IF; -- Enforce minimum number of different characters within password IF v_char_idx_tab.COUNT < c_min_char_diversity THEN RAISE not_diverse; END IF; -- Enforce maximum occurrences of the same character within password IF c_max_char_duplication > 1 THEN v_charidx01 := v_char_idx_tab.FIRST; <> WHILE v_charidx01 IS NOT NULL LOOP IF v_char_idx_tab(v_charidx01) > c_max_char_duplication THEN RAISE too_many_dup_characters; END IF; v_charidx01 := v_char_idx_tab.NEXT(v_charidx01) ; END LOOP duplication_loop; END IF; -- Password passes verification RETURN(TRUE); EXCEPTION WHEN contains_username THEN raise_application_error ( -20001 , 'Password contains the username.' , FALSE ); WHEN contains_rev_username THEN raise_application_error ( -20002 , 'Password contains the username in reverse.' , FALSE ); WHEN similar_to_username THEN raise_application_error ( -20003 , 'Password too similar to username.' , FALSE ); WHEN too_short THEN raise_application_error ( -20004 , 'Password length less than '||c_min_length||'.' , FALSE ); WHEN similar_to_old_password THEN raise_application_error ( -20005 , 'Password too similar to old password.' , FALSE ); WHEN contains_forbidden_word THEN raise_application_error ( -20006 , 'Password contains a forbidden word.' , FALSE ); WHEN similar_to_forbidden_word THEN raise_application_error ( -20007 , 'Password is similar to a forbidden word.' , FALSE ); WHEN not_enough_change THEN raise_application_error ( -20008 , 'Password fails to differ from previous by at least '||c_min_diff_from_prev||' characters.' , FALSE ); WHEN too_few_alphas THEN raise_application_error ( -20009 , 'Password contains less than '||c_min_alpha||' alphabetic characters.' , FALSE ); WHEN too_few_uppers THEN raise_application_error ( -20010 , 'Password contains less than '||c_min_upper||' uppercase characters.' , FALSE ); WHEN too_few_lowers THEN raise_application_error ( -20011 , 'Password contains less than '||c_min_lower||' lowercase characters.' , FALSE ); WHEN too_few_numerics THEN raise_application_error ( -20012 , 'Password contains less than '||c_min_numeric||' numeric characters.' , FALSE ); WHEN too_few_punct THEN raise_application_error ( -20013 , 'Password contains less than '||c_min_punct||' punctuation characters.' , FALSE ); WHEN too_few_std_punct THEN raise_application_error ( -20014 , 'Password contains less than '||c_min_standard_punct||' of the following characters ''_'', ''#'', ''$''.' , FALSE ); WHEN too_few_spec_punct THEN raise_application_error ( -20015 , 'Password contains less than '||c_min_special_punct||' special characters i.e. not alphanumeric or ''_'', ''#'', ''$''.' , FALSE ); WHEN not_diverse THEN raise_application_error ( -20016 , 'Password contains less than '||c_min_char_diversity||' distinct characters.' , FALSE ); WHEN too_many_dup_characters THEN raise_application_error ( -20017 , 'Password contains more than '||c_max_char_duplication||' occurrences of the same character.' , FALSE ); WHEN OTHERS THEN raise_application_error ( -20000 , 'pentest_password_verify: Unexpected error: '||SQLERRM , TRUE); END; /