SoftwareSecurity2013/Group 3/Manual Scanning

Uit Werkplaats
< SoftwareSecurity2013‎ | Group 3
Versie door Tim Cooijmans (overleg | bijdragen) op 21 jun 2013 om 09:42 (V7.6 Verify that all random numbers, random file names, random GUIDs, and random strings are generated using the cryptographic module’s approved random number generator when these random values are intended to be unguessable by an attacker.)
(wijz) ← Oudere versie | Huidige versie (wijz) | Nieuwere versie → (wijz)
Ga naar: navigatie, zoeken

Inhoud

V2.1 Verify that all pages and resources require authentication except those specifically intended to be public.

To check this requirement we first need to determine which page and resources are intended to be public. Guests may use login and registration forms. They may also view forums, topics and posts, but only if the board is not configured to be viewed only by logged in users. Posting can only be done by users who have specifically been given permission to do so.

Access to the list of users and their profiles is configurable, as is access to the search form, so these are only intended to be public if it is specifically configured this way.

include files

In the include directory there are php-files that are supposed to be included, and therefore are not intended to be public. FluxBB makes sure these files are not executed. Some of these files only include classes and/or functions that need to be called from the file that includes them, so on their own they are not executed. If the file does contain code that could be executed on it's own execution is prevented with the following code at the top of these files:

// Make sure no one attempts to run this script "directly"
if (!defined('PUN'))
    exit;

The constant PUN must be defined in the file that includes this file, so if it is not included, the file is not executed.

There is also a include/template directory containing templates. These templates on their own are not specifically intended to be public. However, there is nothing that prevents access to these files. Webmasters can limit access to these files themselves, for example by using .htaccess files if they use Apache, but this is not included in FluxBB itself. FAILED

admin pages

All admin pages check whether the user is an administrator:

if ($pun_user['g_id'] != PUN_ADMIN)
    message($lang_common['No permission'], false, '403 Forbidden');

or

if (!$pun_user['is_admmod'])
    message($lang_common['No permission'], false, '403 Forbidden');

If someone unauthorized tries to visit an admin page the message function is called. At the end of this function footer.php is included. On line 163 of footer.php we find the following:

exit($tpl_main);

This makes sure the code after the calling of the message function on the admin page is not executed. The admin pages are therefore not accesable by unauthorized users. PASSED

installation and updating

The install.php file is used to install FluxBB. If installation is completed, it will not run:

if (file_exists(PUN_ROOT.'config.php'))
{
    // Check to see whether FluxBB is already installed
    include PUN_ROOT.'config.php';

    // [...]

    // If PUN is defined, config.php is probably valid and thus the software is installed
    if (defined('PUN'))
        exit($lang_install['Already installed']);
}

As long is installation is not completed, however it can be run by anyone. FAILED

The update_db.php is used to update the database after an update. This file does not contain any authorization checks, so this file can also be executed by anyone. FAILED

Accessing the board

FluxBB can be configured to be viewable only by logged in users. In that case the board is not specifically intended to be public, otherwise it is. All pages related to viewing the board therefore have the following at the top:

if ($pun_user['g_read_board'] == '0')
    message($lang_common['No view'], false, '403 Forbidden');

Some pages can also be specifically set to be viewed only by logged in users. This is enforced in almost the same way (for search.php, other pages are similar):

if ($pun_user['g_search'] == '0')
    message($lang_search['No search permission'], false, '403 Forbidden');

We've seen before that the message function makes sure that the rest of the file is not processed. PASSED

Interacting with the board

Posting to the board can only be done by users who have permission to post:

// Do we have permission to post?
if ((($tid && (($cur_posting['post_replies'] == '' && $pun_user['g_post_replies'] == '0') || $cur_posting['post_replies'] == '0')) ||
    ($fid && (($cur_posting['post_topics'] == '' && $pun_user['g_post_topics'] == '0') || $cur_posting['post_topics'] == '0')) ||
    (isset($cur_posting['closed']) && $cur_posting['closed'] == '1')) &&
    !$is_admmod)
    message($lang_common['No permission'], false, '403 Forbidden');

Editing and deleting can also be done only by those who have permission. Furthermore, users can only edit or delete their own posts, unless they are moderator:

// Do we have permission to edit this post?
if (($pun_user['g_delete_posts'] == '0' ||
    ($pun_user['g_delete_topics'] == '0' && $is_topic_post) ||
    $cur_post['poster_id'] != $pun_user['id'] ||
    $cur_post['closed'] == '1') &&
    !$is_admmod)
    message($lang_common['No permission'], false, '403 Forbidden');

and

// Do we have permission to edit this post?
if (($pun_user['g_edit_posts'] == '0' ||
    $cur_post['poster_id'] != $pun_user['id'] ||
    $cur_post['closed'] == '1') &&
    !$is_admmod)
    message($lang_common['No permission'], false, '403 Forbidden');

Editing the profile of a user can also only be done by the user himself or by a moderator. We find checks like this in profile.php:

// Make sure we are allowed to change this user's password
if ($pun_user['id'] != $id)
{
    if (!$pun_user['is_admmod']) // A regular user trying to change another user's password?
        message($lang_common['No permission'], false, '403 Forbidden');
    else if ($pun_user['g_moderator'] == '1') // A moderator trying to change a user's password?
    {
        $result = $db->query('SELECT u.group_id, g.g_moderator FROM '.$db->prefix.'users AS u INNER JOIN '.$db->prefix.'groups AS g ON (g.g_id=u.group_id) WHERE u.id='.$id) or error('Unable to fetch user info', __FILE__, __LINE__, $db->error());
        if (!$db->num_rows($result))
            message($lang_common['Bad request'], false, '404 Not Found');

        list($group_id, $is_moderator) = $db->fetch_row($result);

        if ($pun_user['g_mod_edit_users'] == '0' || $pun_user['g_mod_change_passwords'] == '0' || $group_id == PUN_ADMIN || $is_moderator == '1')
            message($lang_common['No permission'], false, '403 Forbidden');
    }
}

So all interactions with the board are properly authorized. PASSED

V2.2 Verify that all password fields do not echo the user's password when it is entered, and that password fields (or the forms that contain them) have autocomplete disabled.

In admin_options.php we find:

<input type="password" name="form[smtp_pass1]" size="25" maxlength="50" value="<?php echo $smtp_pass ?>" />
<input type="password" name="form[smtp_pass2]" size="25" maxlength="50" value="<?php echo $smtp_pass ?>" />

So the password is echoed after it has been entered and the page is visited again. It will not be shown to the user, but it is clearly readable in the HTML. FAILED

If, after submitting, the form is shown again, which can happen if it was not correctly filled in, the password is echoed to the user:

<input type="password" name="req_password1" value="<?php if (isset($_POST['req_password1']))
    echo pun_htmlspecialchars($_POST['req_password1']); ?>" size="16" />

FAILED

The remainging password fields look like this:

<input type="password" name="req_password" size="25" tabindex="2" />

So they do not echo the user's password.

In the snippets above it can be seen that none of the password fields have autocomplete disabled. Also none of the formes have it disabled. FAILED

V2.3 Verify that if a maximum number of authentication attempts is exceeded, the account is locked for a period of time long enough to deter brute force attacks.

In login.php the variable $authorized is initialized to false (line 32) and then set to true if a user enters a correct username and password (lines 43, 53 and 60). There is not something like an else clause for these cases, so the account locking should happen if the $authorization variable is not set to true.

This variable is checked once, on line 63:

if (!$authorized)
        message($lang_login['Wrong user/pass'].' <a href="login.php?action=forget">'.$lang_login['Forgotten pass'].'</a>');

All this does is show a message that the username and password combination is wrong, with a link to retreive it. No locking is taking place. FAILED

V2.4 Verify that all authentication controls are enforced on the server side.

No authentication controls are enforced on the client side. There are a few things that are done with javascript:

  • going back one page in the page history:
    <a href="javascript:history.go(-1)">
  • going to a different url:
    <script type="text/javascript">window.location="admin_maintenance.php'.$query_str.'"</script>
  • Checking that all required fields are entered, without doing further authentication:
<script type="text/javascript">
/* <![CDATA[ */
function process_form(the_form)
{
    var required_fields = {
<?php
    // Output a JavaScript object with localised field names
    $tpl_temp = count($required_fields);
    foreach ($required_fields as $elem_orig => $elem_trans)
    {
        echo "\t\t\"".$elem_orig.'": "'.addslashes(str_replace(' ', ' ', $elem_trans));
        if (--$tpl_temp) echo "\",\n";
        else echo "\"\n\t};\n";
    }
?>
    if (document.all || document.getElementById)
    {
        for (var i = 0; i < the_form.length; ++i)
        {
            var elem = the_form.elements[i];
            if (elem.name && required_fields[elem.name] && !elem.value && elem.type && (/^(?:text(?:area)?|password|file)$/i.test(elem.type)))
            {
                alert('"' + required_fields[elem.name] + '" <?php echo $lang_common['required field'] ?>');
                elem.focus();
                return false;
            }
        }
    }
    return true;
}
/* ]]> */
</script>
  • Selecting and unselecting checkboxes:
function select_checkboxes(curFormId, link, new_string)
{
    // [...]
}

function unselect_checkboxes(curFormId, link, new_string)
{
    // [...]
}

PASSED

V2.5 Verify that all authentication controls (including libraries that call external authentication services) have a centralized implementation.

We identified two authentication controls: the file login.php and the function authenticate_user(./include/functions.php:157).

In both cases the user username and password will be checked for authentication. The main difference is that authenticate_user is used by the file extern.php to provide HTTP Authentication. Nevertheless, the authentication mechanism could be the same. FAILED

V2.6 Verify that all authentication controls fail securely.

In login.php, the variable $authorized is initially set to false (line 32). If no password or a wrong password (with/without salt, sha1/md5 or other kind of password) is given the variable $authorized maintains is value. Only when a correct password is given, the user becomes authorized (lines 43,53 or 60). Before any other authentication details (e.g user status)the variable $authorized is checked and if false the user is redirected to "Forgot password" page. This implies that in case of unexpected failure the user cannot be authorized unless he submits the right password.

In authenticate_user (./include/function:157), the default state of the user is guest (minimum privileges). If the submitted username and password match the user will maintain the default values but $guest is set to false On the other hand, if no password or wrong password are given the user will be reset to the original values (guest). After calling this function and before requesting any information from the web application the variable $guest is checked. Therefore, if any unexpected error occurs the user would be always a guest user unless he was able to provide the correct password and username before the error.

PASSED

V2.7 Verify that the strength of any authentication credentials are sufficient to withstand attacks that are typical of the threats in the deployed environment.

For authentication credentials unsalted sha1 passwords are used. This passwords must have at least 4 characters. Nowadays, passwords hashes without salt, smaller than 8 characters and build with sha1 are no longer secure. FAILED

V2.8 Verify that all account management functions are at least as resistant to attack as the primary authentication mechanism.

As account management functions we considered the 'lost/forgot password' functionality. The 'lost/forgot password' functionality is implemented in login.php (lines 112-230).

In order to obtain a new password a valid and existing email must be given. Furthermore, the code checks whether that email address is being target of an email flood. If that is the case the password recovery stops.

If a valid and existing email is given that is not being flood, two new random passwords with length 8 are created. One of the passwords is the new user's password and the second one is used to update the user's password in the database (one time key) during activation. The user's password is sent in the email body while the other one is sent in the activation link, also inside the email body. In order to activate the new password, they check whether the key in the link matches the one in the database and then user's password is updated in the database.

As such, we conclude that 'lost/forgot password' functionality is as secure as the preliminary authentication mechanism. PASSED

V2.9. Verify that users can safely change their credentials using a mechanism that is at least as resistant to attack as the primary authentication mechanism.

The options for changing credentials, i.e., username, email address and password , are implemented in profile.php. Let us go through these in the order in which they are found in the source code.

Password updates are implemented in line 38-155 of profile.php. Essentially there seem to be three use cases for the change_pass action:

  • The activation of the password using an activation key.
  • The update of a password by a user using some form.
  • The update of a password by an administrator or moderator with sufficient rights using some form..

In the first case, the code checks for the validity of the activation key and user id; if this checks out the password is updated. Essentially the security of this procedure is the same as the security of the lost/forgot password feature mentioned in V2.8 as thus we can say this part should be fine.

In the second and third cases, the code checks for the rights to change the password, i.e., the password can be changed by the user itself; the administrator or a moderator with permission g_mod_edit_users set to non-zero. This procedure is as secure as the initial login procedures, so this should be fine as well.

It then goes on to process the password change form. The form consists of the old password, and two separate entries of the new password. If these inputs are valid, i.e., the old password matches the password found in the database and both new password entries are the same, the password is changed. If an administrator or moderator attempts this, the old password need not be valid (since this should be unknown even to the administrator). As the security of this procedure reduces to the security of primary authentication mechanism, this is fine.

Summarizing, the password change procedure's security depends on primary authentication mechanisms and thus we have a PARTIAL PASS.

The email change procedure is implemented in lines 158-310 of profile.php. Most of the arguments given for password updates also apply for email updates. We will consider the main differences.

First, the code always checks for a proper logged-in user, the user itself of an administrator or moderator given sufficient rights. Given such a user, the update email form is processed (seemingly properly). During processing, the email address is validated and checked against duplicates and a blacklist of banned email addresses. If the email address survives this process, an activation mail is sent to the new email address. The activation key is generated using the same procedure as is used during primary authentication. The activation is subsequently done in the same way as with the password.

Given that the email address update process is as either as secure as the password update procedure or dependent on the security of the primary authenication procedure, we consider email updating the second PARTIAL PASS.

Concerning username updates, this is implemented in lines 720-741 and lines 939-986 of profile.php. Note that only administrators or moderators can change usernames, this is guaranteed in lines 720-741. In lines 936-972 the database is updated with the full set of changes needed to promote a username change through the database. This code is only executed when the username_changed field is set which happens in the aforementioned code in lines 720-741. This piece of code is a secure as the authenication mechanism, making this another PARTIAL PASS.

Since all credential update functions are as secure as the primary authentication mechanisms, we deem this requirement sufficiently guaranteed.

PASSED

V2.10. Verify that re-authentication is required before any application- specific sensitive operations are permitted.

We can be fairly short about this, re-authentication is, as a cursory review of profile.php shows, required for none of the credential update procedures. These procedures, like password updates, we deem sufficiently sensitive to warrant re-authentication.

FAILED

V2.11 Verify that after an administratively-configurable period of time, authentication credentials expire.

There is no password expiration. Cookies expire by nature but the passwords itself are never expired. There is no database field in the users table to accommodate this functionality. FAILED

V2.12 Verify that all authentication decisions are logged.

When logging in on line 21 of login.php there is no logging in place. The same holds for logging out. The time of the last visit however is logged but when a user logs in twice the first successful attempt is lost. The authentication decision if not logged. FAILED.

V2.13 Verify that account passwords are salted using a salt that is unique to that account (e.g., internal user ID, account creation) and hashed before storing.

The passwords are hashed using the pun_hash function defined on line 1040 of functions.php:

//
// Compute a hash of $str
//
function pun_hash($str)
{
	return sha1($str);
}

This function performs a normal SHA1 hash on the password string without salting. When registering a user this function is called to compute the hashed password on line 156 of register.php. If we look at line 38 of login.php there appears to have been some kind of salting. However currently salts are removed for a strange reason. FAILED

V2.14 Verify that all authentication credentials for accessing services external to the application are encrypted and stored in a protected location (not in source code).

The authentication credentials for the SMTP mail server are stored in plaintext in the database. This can be seen in on line 1575 in install.php:

// Insert config data
$pun_config = array(
    ...
    'o_smtp_host'  => NULL,
    'o_smtp_user'  => NULL,
    'o_smtp_pass'  => NULL,
    'o_smtp_ssl'   => 0,
    ...
)

Although this is not in code I don't think that this is a protected location.

What makes matters even worse if that the programmers implemented a kind of caching to kind the config stored in the database. If we look at the common.php file which is run for every request the following lines are executed:

// Load cached config
if (file_exists(FORUM_CACHE_DIR.'cache_config.php'))
	include FORUM_CACHE_DIR.'cache_config.php';

if (!defined('PUN_CONFIG_LOADED'))
{
	if (!defined('FORUM_CACHE_FUNCTIONS_LOADED'))
		require PUN_ROOT.'include/cache.php';

	generate_config_cache();
	require FORUM_CACHE_DIR.'cache_config.php';
}

As we can see a cache_config.php file is required to continue. If it is not available one is created first using the generate_config_cache() function defined in cache.php:

function generate_config_cache()
{
	global $db;

	// Get the forum config from the DB
	$result = $db->query('SELECT * FROM '.$db->prefix.'config', true) or error('Unable to fetch forum config', __FILE__, __LINE__, $db->error());

	$output = array();
	while ($cur_config_item = $db->fetch_row($result))
		$output[$cur_config_item[0]] = $cur_config_item[1];

	// Output config as PHP code
	$fh = @fopen(FORUM_CACHE_DIR.'cache_config.php', 'wb');
	if (!$fh)
		error('Unable to write configuration cache file to cache directory. Please make sure PHP has write access to the directory \''.pun_htmlspecialchars(FORUM_CACHE_DIR).'\'', __FILE__, __LINE__);

	fwrite($fh, '<?php'."\n\n".'define(\'PUN_CONFIG_LOADED\', 1);'."\n\n".'$pun_config = '.var_export($output, true).';'."\n\n".'?>');

	fclose($fh);

	if (function_exists('apc_delete_file'))
		@apc_delete_file(FORUM_CACHE_DIR.'cache_config.php');
}

This stores all config settings in plain text in the cache dir of the software.

The database credentials are saved in plaintext. On line 111 of install.php a config.php file is created containing database connection information including username and password. This file contains the connection information in plain text:

//
// Generate output to be used for config.php
//
function generate_config_file()
{
	global $db_type, $db_host, $db_name, $db_username, $db_password, $db_prefix, $cookie_name, $cookie_seed;

	return '<?php'."\n\n".'$db_type = \''.$db_type."';\n".'$db_host = \''.$db_host."';\n".'$db_name = \''.addslashes($db_name)."';\n".'$db_username = \''.addslashes($db_username)."';\n".'$db_password = \''.addslashes($db_password)."';\n".'$db_prefix = \''.addslashes($db_prefix)."';\n".'$p_connect = false;'."\n\n".'$cookie_name = '."'".$cookie_name."';\n".'$cookie_domain = '."'';\n".'$cookie_path = '."'/';\n".'$cookie_secure = 0;'."\n".'$cookie_seed = \''.random_key(16, false, true)."';\n\ndefine('PUN', 1);\n";
}

FAILED.

V7.1 Verify that all cryptographic functions used to protect secrets from the application user are implemented server side.

In the list of V2.4 we can see that no cryptography is used on the client side. PASSED

V7.2 Verify that all cryptographic modules fail securely.

As cryptographic modules we identified the functions pun_hash (./include/functions.php:1040) and forum_hmac (./include/functions.php:293).

The function pun_hash returns directly the output of sha1[1]. According to php manual[2], Sha1 function is implemented according to RFC3174. We assume that this sha1 implementation fails securely.

As for the function forum_hmac, if the function hash_hmac exists in the system it's used to create a Sha1 hash. Otherwise, an implementation of hash_hmac with SHA1 is used to create a hash.

In neither of the functions there is error handling. Furthermore, none of the functions used in these cryptographic modules seems to have error handling according to php manual (str_pad, str_repeat, pack, ...).

Therefore, we conclude they fail securely. PASSED

V7.3. Verify that access to any master secret(s) is protected from unauthorized access (A master secret is an application credential stored as plaintext on disk that is used to protect access to security configuration information).

Note that this is not about external credentials as defined by V2.14. That was about credentials to a SMTP server, which is seperate from the application under review.

However, the database password is part of the application and is indeed written to a config file (see line 106 in install.php) in plaintext. Whether this gives access to "security configuration information" is a matter of some debate. However, we shall err on the side of caution and will deem this requirement as not satisfied.

FAILED

V7.4 Verify that password hashes are salted when they are created.

See V2.13: FAILED.

V7.5 Verify that cryptographic module failures are logged.

A HMAC function is used to ensure the integrity of cookies. That is defined in functions.php on line 293. By default a built in version called hash_hmac is used. If this function is not available the same function is defined. However no there is no error handling and no logging in place. However maybe there are no failures possible?

For the SHA1 function that is used throughout the software no error logging is defined. PASSED

V7.6 Verify that all random numbers, random file names, random GUIDs, and random strings are generated using the cryptographic module’s approved random number generator when these random values are intended to be unguessable by an attacker.

If available the openssl_random_pseudo_bytes function defined in php is used. This function uses the the random_pseudo_bytes function of the OpenSSL library and is considered reasonably ok. However if this function is not available several fallbacks are used. The first one ins MCrypt_Create_IV, the randomness of this function however there is a bug known that causes this function to return the same result several times. See: http://blog.php-security.org/archives/80-Watching-the-PHP-CVS.html. This could be a problem on certain configurations. The next fallback is just reading /dev/urandom/ and adding entropy from stats and memory usage and hashing that. If /dev/urandom/ is not available the time to do several hashes is used as random value. I doubt if this a good random generator. Furthermore no warning is generated to warn a administrator if a weaker random generator is used. FAILED