diff --git a/src/OVAL/oval_recordField.c b/src/OVAL/oval_recordField.c index dd765097db..07c2b1daa4 100644 --- a/src/OVAL/oval_recordField.c +++ b/src/OVAL/oval_recordField.c @@ -422,7 +422,8 @@ xmlNode *oval_record_field_to_dom(struct oval_record_field *rf, bool parent_mask root_node = xmlDocGetRootElement(doc); name = oval_record_field_get_name(rf); rf_mask = oval_record_field_get_mask(rf); - if (!xmlStrcmp(root_node->name, BAD_CAST OVAL_ROOT_ELM_RESULTS) + if ((!xmlStrcmp(root_node->name, BAD_CAST OVAL_ROOT_ELM_RESULTS) || + !xmlStrcmp(root_node->name, BAD_CAST OVAL_ROOT_ELM_SYSCHARS)) && (rf_mask || parent_mask)) { value = NULL; masked = true; diff --git a/src/OVAL/oval_sysEnt.c b/src/OVAL/oval_sysEnt.c index f266a13226..c1a8443838 100644 --- a/src/OVAL/oval_sysEnt.c +++ b/src/OVAL/oval_sysEnt.c @@ -284,8 +284,9 @@ void oval_sysent_to_dom(struct oval_sysent *sysent, xmlDoc * doc, xmlNode * pare char *content = oval_sysent_get_value(sysent); bool mask = oval_sysent_get_mask(sysent); - /* omit the value in oval_results if mask=true */ - if (mask && !xmlStrcmp(root_node->name, BAD_CAST OVAL_ROOT_ELM_RESULTS)) { + /* omit the value in oval_results and oval_system_characteristics if mask=true */ + if (mask && (!xmlStrcmp(root_node->name, BAD_CAST OVAL_ROOT_ELM_RESULTS) || + !xmlStrcmp(root_node->name, BAD_CAST OVAL_ROOT_ELM_SYSCHARS))) { sysent_tag = xmlNewTextChild(parent, ent_ns, BAD_CAST tagname, BAD_CAST ""); } else { xmlChar *encoded_content = xmlEncodeEntitiesReentrant(doc, BAD_CAST content); diff --git a/src/OVAL/probes/unix/shadow_probe.c b/src/OVAL/probes/unix/shadow_probe.c index dd5ca66299..319908e6c8 100644 --- a/src/OVAL/probes/unix/shadow_probe.c +++ b/src/OVAL/probes/unix/shadow_probe.c @@ -182,10 +182,62 @@ static void report_finding(struct result_info *res, probe_ctx *ctx) SEXP_free_r(&se_flg_mem); } +static char *strip_hash(const char *raw) +{ + const char *p; + size_t prefix_len; + size_t keep_len; + char *buf; + + if (raw == NULL) + return strdup("*"); + + if (*raw == '\0' || + strcmp(raw, "!") == 0 || strcmp(raw, "!!") == 0 || + strcmp(raw, "!*") == 0 || strcmp(raw, "*") == 0 || + strcmp(raw, "*LK*") == 0 || strcmp(raw, "x") == 0) + return strdup(raw); + + p = raw; + prefix_len = 0; + while (*p == '!') + p++, prefix_len++; + + if (*p == '$') { + /* glibc/libxcrypt hash ($id$salt$hash), keep lock prefix + method id ($id$) */ + const char *id_end = strchr(p + 1, '$'); + if (id_end != NULL) { + keep_len = (size_t)(id_end + 1 - raw); + buf = malloc(keep_len + 1); + memcpy(buf, raw, keep_len); + buf[keep_len] = '\0'; + return buf; + } + } else if (*p == '_') { + /* bsdicrypt (BSDI extended DES), keep lock prefix + '_' marker */ + keep_len = prefix_len + 1; + buf = malloc(keep_len + 1); + memcpy(buf, raw, keep_len); + buf[keep_len] = '\0'; + return buf; + } + + if (prefix_len > 0) { + /* locked account with non-crypt hash (e.g. descrypt), keep lock prefix only */ + buf = malloc(prefix_len + 1); + memcpy(buf, raw, prefix_len); + buf[prefix_len] = '\0'; + return buf; + } + + return strdup("*"); +} + static void _process_struct_shadow(struct spwd *sp, SEXP_t *un_ent, probe_ctx *ctx) { SEXP_t *un; struct result_info r; + char *stripped_hash; dI("Have user: %s", sp->sp_namp); un = SEXP_string_newf("%s", sp->sp_namp); @@ -194,8 +246,10 @@ static void _process_struct_shadow(struct spwd *sp, SEXP_t *un_ent, probe_ctx *c return; } + stripped_hash = strip_hash(sp->sp_pwdp); + r.username = sp->sp_namp; - r.password = sp->sp_pwdp; + r.password = stripped_hash; r.chg_lst = sp->sp_lstchg; r.chg_allow = sp->sp_min; r.chg_req = sp->sp_max; @@ -205,6 +259,7 @@ static void _process_struct_shadow(struct spwd *sp, SEXP_t *un_ent, probe_ctx *c r.flag = sp->sp_flag; report_finding(&r, ctx); + free(stripped_hash); SEXP_free(un); } diff --git a/tests/probes/shadow/CMakeLists.txt b/tests/probes/shadow/CMakeLists.txt index 1531708041..664a75db6f 100644 --- a/tests/probes/shadow/CMakeLists.txt +++ b/tests/probes/shadow/CMakeLists.txt @@ -2,4 +2,5 @@ if(ENABLE_PROBES_UNIX) add_oscap_test("test_probes_shadow.sh" LABELS unix) add_oscap_test("test_probes_shadow_offline.sh" LABELS unix) add_oscap_test("test_probes_shadow_offline_unsupported.sh" LABELS unix macos) + add_oscap_test("test_probes_shadow_stripped.sh" LABELS unix) endif() diff --git a/tests/probes/shadow/test_probes_shadow.xml.sh b/tests/probes/shadow/test_probes_shadow.xml.sh index 290fbe71cb..7bb6630c4d 100644 --- a/tests/probes/shadow/test_probes_shadow.xml.sh +++ b/tests/probes/shadow/test_probes_shadow.xml.sh @@ -9,7 +9,29 @@ function getField { echo $LINE | awk -F':' '{print $1}' ;; 'password' ) - echo $LINE | awk -F':' '{print $2}' + local pwd=$(echo $LINE | awk -F':' '{print $2}') + case "$pwd" in + ''|'!'|'!!'|'!*'|'*'|'*LK*'|'x') + echo "$pwd" ;; + *) + local lock="" + local rest="$pwd" + while [[ "${rest:0:1}" = "!" ]]; do + lock="${lock}!" + rest="${rest:1}" + done + if [[ "${rest:0:1}" = '$' ]]; then + local id_end=$(echo "$rest" | cut -d '$' -f1-2) + echo "${lock}${id_end}\$" + elif [[ "${rest:0:1}" = '_' ]]; then + echo "${lock}_" + elif [[ -n "$lock" ]]; then + echo "$lock" + else + echo "*" + fi + ;; + esac ;; 'chg_lst' ) local CHGLST=`echo $LINE | awk -F':' '{print $3}'` diff --git a/tests/probes/shadow/test_probes_shadow_offline.xml b/tests/probes/shadow/test_probes_shadow_offline.xml index 64385bae4b..11ffe99644 100644 --- a/tests/probes/shadow/test_probes_shadow_offline.xml +++ b/tests/probes/shadow/test_probes_shadow_offline.xml @@ -41,7 +41,7 @@ root - !locked + ! -1 0 99999 diff --git a/tests/probes/shadow/test_probes_shadow_stripped.sh b/tests/probes/shadow/test_probes_shadow_stripped.sh new file mode 100755 index 0000000000..5b655d845e --- /dev/null +++ b/tests/probes/shadow/test_probes_shadow_stripped.sh @@ -0,0 +1,58 @@ +#!/usr/bin/env bash + +. $builddir/tests/test_common.sh + +set -e -o pipefail + +function test_probes_shadow_stripped { + + probecheck "shadow" || return 255 + + local ret_val=0 + local df="${srcdir}/test_probes_shadow_stripped.xml" + local rf="$(mktemp results.XXXXXXX.xml)" + + [[ -f $rf ]] && rm -f $rf + + tmpdir=$(make_temp_dir /tmp "test_probes_shadow_stripped") + mkdir -p "${tmpdir}/etc" + cat > "${tmpdir}/etc/shadow" << 'SHADOW' +sha512user:$6$saltsalt$longhashvaluethatneedstoberedacted:19000:0:99999:7::: +lockedhash:!!$6$anothersalt$anotherlonghashvalue:19000:0:99999:7::: +lockednohash:!:19000:0:99999:7::: +disabled:*:19000:0:99999:7::: +neverset:!!:19000:0:99999:7::: +bsdiuser:_bsdihashvalueredact1:19000:0:99999:7::: +bsdilocked:!_bsdihashvalueredact2:19000:0:99999:7::: +sunmd5user:$md5,rounds=4294963199$saltvalueredact$$hashvalueredacted:19000:0:99999:7::: +descryptuser:aZ4ZloVToj1nA:19000:0:99999:7::: +descryptlocked:!aZ4ZloVToj1nA:19000:0:99999:7::: +SHADOW + + export OSCAP_PROBE_ROOT="${tmpdir}" + + $OSCAP oval eval --results $rf $df + + unset OSCAP_PROBE_ROOT + rm -rf "${tmpdir}" + + if [[ -f $rf ]]; then + verify_results "def" $df $rf 10 && verify_results "tst" $df $rf 10 + ret_val=$? + else + ret_val=1 + fi + + if grep -q 'longhashvaluethatneedstoberedacted\|anotherlonghashvalue\|saltsalt\|anothersalt\|bsdihashvalueredact1\|bsdihashvalueredact2\|saltvalueredact\|hashvalueredacted\|aZ4ZloVToj1nA' $rf; then + ret_val=1 + fi + + rm -f $rf + return $ret_val +} + +test_init + +test_run "test_probes_shadow_stripped" test_probes_shadow_stripped + +test_exit diff --git a/tests/probes/shadow/test_probes_shadow_stripped.xml b/tests/probes/shadow/test_probes_shadow_stripped.xml new file mode 100644 index 0000000000..94c9055351 --- /dev/null +++ b/tests/probes/shadow/test_probes_shadow_stripped.xml @@ -0,0 +1,259 @@ + + + + + shadow-stripped-test + 1.0 + 5.8 + 2026-09-23T00:00:00-00:00 + + + + + + SHA-512 hash is stripped + Password hash should be stripped to method prefix only + + + + + + + + + Locked account with hash is stripped + Locked account should keep lock prefix and method id + + + + + + + + + Locked account marker preserved + Simple lock marker should be kept as-is + + + + + + + + + Disabled account marker preserved + Disabled account marker should be kept as-is + + + + + + + + + Never-set password marker preserved + Double-bang marker should be kept as-is + + + + + + + + + BSDi extended DES hash is stripped + Non-$-delimited bsdicrypt hash should be reduced to its '_' marker + + + + + + + + + Locked BSDi extended DES hash is stripped + Locked account should keep lock prefix and '_' marker + + + + + + + + + SunMD5 hash with rounds parameter is stripped + Long id component (with embedded rounds) should be preserved in full, salt/hash dropped + + + + + + + + + Traditional DES hash is stripped + Hash with no delimiter and no lock marker has no id to preserve, reduced to '*' + + + + + + + + + Locked traditional DES hash is stripped + Locked account with non-crypt hash should keep lock prefix only + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + sha512user + + + + lockedhash + + + + lockednohash + + + + disabled + + + + neverset + + + + bsdiuser + + + + bsdilocked + + + + sunmd5user + + + + descryptuser + + + + descryptlocked + + + + + + sha512user + $6$ + + + + lockedhash + !!$6$ + + + + lockednohash + ! + + + + disabled + * + + + + neverset + !! + + + + bsdiuser + _ + + + + bsdilocked + !_ + + + + sunmd5user + $md5,rounds=4294963199$ + + + + descryptuser + * + + + + descryptlocked + ! + + + +