bundle agent CVE_2023_26560 #@ brief Remediate CVE-2023-26560 #@ description On an enterprise hub, ensure that root does not have superuser role and rotate CFE_ROBOT credentials if necessary #@ #@ **Manually integrating into policy** #@ #@ To integrate into policy manually add the policy file to inputs, and ensure the #@ bundle `CVE_2023_26560` is added to the /bundlesequence/. This example #@ illustrates augments settings (`def.json` in the root of your policy set) for #@ manual integration and execution. #@ #@ ```json #@ { #@ "inputs": [ "./services/CVE_2023_26560.cf" ], #@ "vars": { #@ "control_common_bundlesequence_end": [ "CVE_2023_26560" ] #@ } #@ } #@ ```` #@ #@ The policy has been designed to limit resource utilization, and is safe to #@ leave active if desired (though not required). When remediation occurs, the #@ file `CVE_2023_26560_remediated` will be present in state. A date stamp is added #@ to the file each time remediation occurs, though only a single remediation is #@ expected. #@ #@ **Single execution instructions** #@ #@ If you do not wish to integrate this policy into your larger policy set, you #@ can instead execute it manually as a standalone policy file. The following #@ examples show executing the standalone policy. #@ #@ - `chmod 700 ./CVE_2023_26560.cf && ./CVE_2023_26560.cf --bundlesequence CVE_2023_26560` #@ - `cf-agent -Kf ./CVE_2023_26560.cf --bundlesequence CVE_2023_26560` { meta: (policy_server|am_policy_hub).enterprise_edition:: "tags" -> { "ENT-9827", "CVE-2023-26560" } slist => { "autorun" }; "version" string => "1"; methods: "CVE_2023_26560_roles" classes => CVE_2023_26560:results("bundle", "CVE_2023_26560_roles"); CVE_2023_26560_roles_repaired:: "CVE_2023_26560_robot_creds"; } bundle agent root_no_rolsuper_dbname_postgres { vars: "psql_cmd_prefix" string => concat( "/var/cfengine/bin/psql ", "--username cfpostgres ", "--dbname postgres ", "--tuples-only ", "--command "); # A query to get the status of Superuser role for root "psql_query_root_rolsuper" string => `"SELECT rolsuper from pg_roles WHERE rolname = 'root'"`; "r" string => execresult( "$(psql_cmd_prefix) $(psql_query_root_rolsuper)", noshell); classes: "_root_has_super_role_on_dbname_postgres" # Yes, it's space t we are checking against expression => strcmp( " t", "$(r)" ); commands: _root_has_super_role_on_dbname_postgres:: '$(psql_cmd_prefix) "ALTER user root NOSUPERUSER"'; } bundle agent all_privileges_on_TABLES_and_SEQUENCES_in_SCHEMA_public_to_root( dbname ) { vars: "psql_cmd_prefix" string => concat( "/var/cfengine/bin/psql ", "--username cfpostgres ", "--dbname $(dbname) ", "--tuples-only ", "--command "); # A query to get the tables in the public schema "query_public_schema_tables" string => `"SELECT DISTINCT(table_name) FROM information_schema.table_privileges where table_schema = 'public' AND table_catalog = '$(dbname)'"`; # The list of tables in the public schema (beware, each element has a leading space) "public_schema_tables" slist => string_split( execresult( "$(psql_cmd_prefix) $(query_public_schema_tables)", noshell), "\n", inf); # A query to get the privileges granted to root on each table "query_public_schema_table_priv[$(public_schema_tables)]" string => concat( `"SELECT count(*) `, `FROM information_schema.table_privileges `, `WHERE grantee = 'root' AND table_name = '$(with)' AND `, `privilege_type IN ('TRIGGER', 'REFERENCES', 'TRUNCATE', 'DELETE', 'SELECT', 'UPDATE', 'INSERT')"` ), with => string_trim( "$(public_schema_tables)"); # Deal with the leading space of each element # The privileges for each table, we expect to find 7 privileges for each table "public_schema_table_priv[$(public_schema_tables)]" string => string_trim(execresult( "$(psql_cmd_prefix) $(query_public_schema_table_priv[$(public_schema_tables)])", noshell ) ); # Deal with public schema sequences # A query to get the sequences in public schema "query_public_schema_sequences" string => `"SELECT sequence_name FROM information_schema.sequences where sequence_schema = 'public' AND sequence_catalog = '$(dbname)'"`; # The list of sequences in public schema (beware, each element has a leading space) "public_schema_sequences" slist => string_split( execresult( "$(psql_cmd_prefix) $(query_public_schema_sequences)", noshell), "\n", inf); # A query to get the privileges for each sequence "query_sequence_priv[$(public_schema_sequences)]" string => concat( `"SELECT relacl `, `FROM pg_class `, `WHERE relkind = 'S' `, `AND relacl is not null `, `AND relnamespace IN ( `, `SELECT oid `, `FROM pg_namespace `, `WHERE nspname NOT LIKE 'pg_%' `, `AND nspname != 'information_schema'`, `)`, `AND relname = '$(with)'" `), with => string_trim( "$(public_schema_sequences)" ); # The privileges for each sequence, we expect to find root=rwU on each table "public_schema_sequence_priv[$(public_schema_sequences)]" string => string_trim(execresult( "$(psql_cmd_prefix) $(query_sequence_priv[$(public_schema_sequences)])", noshell ) ); classes: # If any table does not have 7 privileges granted for root "_public_schema_tables_not_granting_root_all_priv" expression => not( strcmp( "7", "$(public_schema_table_priv[$(public_schema_tables)])" ) ); # If any sequence does not contain root=rwU "_public_schema_sequences_not_granting_root_all_priv" expression => not( regcmp( ".*root=rwU.*", "$(public_schema_sequence_priv[$(public_schema_sequences)])" ) ); commands: _public_schema_tables_not_granting_root_all_priv:: `$(psql_cmd_prefix) "GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO root"` -> { "CVE-2023-26560" }; _public_schema_sequences_not_granting_root_all_priv:: `$(psql_cmd_prefix) "GRANT ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA public TO root"` -> { "CVE-2023-26560" }; } bundle agent CVE_2023_26560_roles { vars: "dbnames" slist => { "cfdb", "cfsettings" }; methods: "Ensure root is granted all privileges on all Tables and Sequences in $(dbnames)" -> { "CVE-2023-26560" } usebundle => all_privileges_on_TABLES_and_SEQUENCES_in_SCHEMA_public_to_root( "$(dbnames)" ); "Ensure root is not ranted superuser role on postgres" -> { "CVE-2023-26560" } usebundle => root_no_rolsuper_dbname_postgres; } bundle agent CVE_2023_26560_robot_creds { classes: (policy_server|am_policy_hub).enterprise_edition:: "HAVE_REMEDIATION_FLAG_$(this.bundle)" expression => fileexists( "$(sys.statedir)/$(this.bundle)_remediated" ); "WANT_ROBOT_CREDENTIAL_ROTATION" expression => "FIXME", comment => "If we remediate the superuser role for root, we also want to rotate credentials"; "REMEDIATION_COMPLETE_$(this.bundle)" and => { "HAVE_REMEDIATION_FLAG_$(this.bundle)" }, comment => "When remediation is complete, remediation scripts are cleaned up, and no correction actions are considered"; vars: "!REMEDIATION_COMPLETE_$(this.bundle)":: "script_path" string => "/var/cfengine/bin/remediate_$(this.bundle).sh"; # For convenience the remediation script is embedded within the policy # file and the script itself is created as necessary. A script is used to # remediate to ensure that credentials never end up inside cfengine # variables that may be reported back to the hub. # The script is dumb, it just rotates credentials. The agent assumes based # on a return code of 0 that the script was successful in all operations, # and. "script_content" string => '#!/bin/bash ( set +x pwgen() { dd if=/dev/urandom bs=1024 count=1 2>/dev/null | tr -dc \'a-zA-Z0-9\' | fold -w $1 | head -n 1 } pwhash() { echo -n "$1" | "/var/cfengine/bin/openssl" dgst -sha256 | awk \'{print $2}\' } CFE_ROBOT_PW="$(sed \'/^cf_robot_password=/!d;s/.*=//\' "/var/cfengine/httpd/secrets.ini")" test -n "$CFE_ROBOT_PW" || { echo "ERROR reading cf_robot_password from secrets.ini"; exit 1; } CFE_ROBOT_PW_SALT=`pwgen 10` CFE_ROBOT_PW_HASH=`pwhash "$CFE_ROBOT_PW_SALT$CFE_ROBOT_PW"` # note that here we `echo "..." | psql` instead of `psql -c "..."` to avoid # leaking secrets in `ps -ef` output. echo "UPDATE users SET password = \'SHA=$CFE_ROBOT_PW_HASH\', salt = \'$CFE_ROBOT_PW_SALT\' WHERE username = \'CFE_ROBOT\'" | "/var/cfengine/bin/psql" cfsettings ) '; files: "/var/cfengine/bin/remediate_$(this.bundle).sh" create => "true", edit_defaults => CVE_2023_26560:empty, edit_line => CVE_2023_26560:insert_lines( $(script_content) ), perms => CVE_2023_26560:mog( 700, root, root ); "$(this.bundle)_CREDS_repaired":: "$(sys.statedir)/$(this.bundle)_remediated" create => "true", edit_line => CVE_2023_26560:insert_lines( $(sys.date) ), comment => "We drop a timestamp into the file each time this gets remediated, though we only expect one such entry."; "REMEDIATION_COMPLETE_$(this.bundle)":: "/var/cfengine/bin/remediate_$(this.bundle).sh" delete => CVE_2023_26560:tidy; commands: "!REMEDIATION_COMPLETE_$(this.bundle)":: "$(script_path)" contain => CVE_2023_26560:in_shell, classes => CVE_2023_26560:results( "bundle", "$(this.bundle)_CREDS" ); reports: "$(this.bundle)_PERMS_repaired":: "$(this.bundle) permissions tightened"; "$(this.bundle)_CREDS_repaired":: "$(this.bundle) credentials rotated"; } @if minimum_version(3.12) bundle agent __main__ { methods: "CVE_2023_26560"; } @endif body file control # We inline bodies used from the standard library so that the remediation policy # can be executed as a stand alone policy or integrated with a larger policy # set without modification. { namespace => "CVE_2023_26560"; } body contain in_shell # @brief run command in shell # # **Example:** # # ```cf3 # commands: # "/bin/pwd | /bin/cat" # contain => in_shell; # ``` { useshell => "true"; # canonical "useshell" but this is backwards-compatible } body classes results(scope, class_prefix) # @brief Define classes prefixed with `class_prefix` and suffixed with # appropriate outcomes: _kept, _repaired, _not_kept, _error, _failed, # _denied, _timeout, _reached # # @param scope The scope in which the class should be defined (`bundle` or `namespace`) # @param class_prefix The prefix for the classes defined # # This body can be applied to any promise and sets global # (`namespace`) or local (`bundle`) classes based on its outcome. For # instance, with `class_prefix` set to `abc`: # # * if the promise is to change a file's owner to `nick` and the file # was already owned by `nick`, the classes `abc_reached` and # `abc_kept` will be set. # # * if the promise is to change a file's owner to `nick` and the file # was owned by `adam` and the change succeeded, the classes # `abc_reached` and `abc_repaired` will be set. # # This body is a simpler, more consistent version of the body # `scoped_classes_generic`, which see. The key difference is that # fewer classes are defined, and only for outcomes that we can know. # For example this body does not define "OK/not OK" outcome classes, # since a promise can be both kept and failed at the same time. # # It's important to understand that promises may do multiple things, # so a promise is not simply "OK" or "not OK." The best way to # understand what will happen when your specific promises get this # body is to test it in all the possible combinations. # # **Suffix Notes:** # # * `_reached` indicates the promise was tried. Any outcome will result # in a class with this suffix being defined. # # * `_kept` indicates some aspect of the promise was kept # # * `_repaired` indicates some aspect of the promise was repaired # # * `_not_kept` indicates some aspect of the promise was not kept. # error, failed, denied and timeout outcomes will result in a class # with this suffix being defined # # * `_error` indicates the promise repair encountered an error # # * `_failed` indicates the promise failed # # * `_denied` indicates the promise repair was denied # # * `_timeout` indicates the promise timed out # # **Example:** # # ```cf3 # bundle agent example # { # commands: # "/bin/true" # classes => results("bundle", "my_class_prefix"); # # reports: # my_class_prefix_kept:: # "My promise was kept"; # # my_class_prefix_repaired:: # "My promise was repaired"; # } # ``` # # **See also:** `scope`, `scoped_classes_generic`, `classes_generic` { scope => "$(scope)"; promise_kept => { "$(class_prefix)_reached", "$(class_prefix)_kept" }; promise_repaired => { "$(class_prefix)_reached", "$(class_prefix)_repaired" }; repair_failed => { "$(class_prefix)_reached", "$(class_prefix)_error", "$(class_prefix)_not_kept", "$(class_prefix)_failed" }; repair_denied => { "$(class_prefix)_reached", "$(class_prefix)_error", "$(class_prefix)_not_kept", "$(class_prefix)_denied" }; repair_timeout => { "$(class_prefix)_reached", "$(class_prefix)_error", "$(class_prefix)_not_kept", "$(class_prefix)_timeout" }; } body delete tidy # @brief Delete the file and remove empty directories # and links to directories { dirlinks => "delete"; rmdirs => "true"; } bundle edit_line insert_lines(lines) # @brief Alias for `lines_present` { insert_lines: "$(lines)" comment => "Append lines if they don't exist"; } body perms mog(mode,user,group) # @brief Set the file's mode, owner and group # @param mode The new mode # @param user The username of the new owner # @param group The group name { owners => { "$(user)" }; groups => { "$(group)" }; mode => "$(mode)"; rxdirs => "false"; } body edit_defaults empty # @brief Empty the file before editing # # No backup is made { empty_file_before_editing => "true"; edit_backup => "false"; #max_file_size => "300000"; }