From 79d5343f198c04c4b29c454c5d60e9328bb2e20b Mon Sep 17 00:00:00 2001 From: Arden97 Date: Thu, 24 Sep 2026 11:25:45 +0200 Subject: [PATCH 1/3] fix shadow password hash leakage --- src/OVAL/oval_recordField.c | 3 +- src/OVAL/oval_sysEnt.c | 5 ++-- src/OVAL/probes/unix/shadow_probe.c | 43 ++++++++++++++++++++++++++++- 3 files changed, 47 insertions(+), 4 deletions(-) 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..b2c1148aa0 100644 --- a/src/OVAL/probes/unix/shadow_probe.c +++ b/src/OVAL/probes/unix/shadow_probe.c @@ -182,10 +182,51 @@ static void report_finding(struct result_info *res, probe_ctx *ctx) SEXP_free_r(&se_flg_mem); } +static const char *strip_hash(const char *raw, char *buf, size_t buf_len) +{ + const char *p; + size_t prefix_len, prefix_with_id_len; + + if (raw == NULL || *raw == '\0' || + strcmp(raw, "!") == 0 || strcmp(raw, "!!") == 0 || + strcmp(raw, "*") == 0 || strcmp(raw, "*LK*") == 0 || + strcmp(raw, "x") == 0) + return raw; + + p = raw; + prefix_len = 0; + + /* crypt(3) hash ($id$salt$hash), keep lock prefix + method id ($id$) */ + while (*p == '!') + p++, prefix_len++; + + if (*p == '$') { + const char *id_end = strchr(p + 1, '$'); + if (id_end != NULL) { + prefix_with_id_len = (size_t)(id_end + 1 - raw); + if (prefix_with_id_len < buf_len) { + memcpy(buf, raw, prefix_with_id_len); + buf[prefix_with_id_len] = '\0'; + return buf; + } + } + } + + /* locked account with non-crypt hash, keep lock prefix only */ + if (prefix_len > 0 && prefix_len < buf_len) { + memcpy(buf, raw, prefix_len); + buf[prefix_len] = '\0'; + return buf; + } + + return "*"; +} + static void _process_struct_shadow(struct spwd *sp, SEXP_t *un_ent, probe_ctx *ctx) { SEXP_t *un; struct result_info r; + char stripped[8]; dI("Have user: %s", sp->sp_namp); un = SEXP_string_newf("%s", sp->sp_namp); @@ -195,7 +236,7 @@ static void _process_struct_shadow(struct spwd *sp, SEXP_t *un_ent, probe_ctx *c } r.username = sp->sp_namp; - r.password = sp->sp_pwdp; + r.password = strip_hash(sp->sp_pwdp, stripped, sizeof(stripped)); r.chg_lst = sp->sp_lstchg; r.chg_allow = sp->sp_min; r.chg_req = sp->sp_max; From 6e8c2c8b306c1bb97605f803717df8ea92778e87 Mon Sep 17 00:00:00 2001 From: Arden97 Date: Thu, 24 Sep 2026 11:26:22 +0200 Subject: [PATCH 2/3] test shadow leak --- tests/probes/shadow/CMakeLists.txt | 1 + .../shadow/test_probes_shadow_stripped.sh | 53 +++++++ .../shadow/test_probes_shadow_stripped.xml | 139 ++++++++++++++++++ 3 files changed, 193 insertions(+) create mode 100755 tests/probes/shadow/test_probes_shadow_stripped.sh create mode 100644 tests/probes/shadow/test_probes_shadow_stripped.xml 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_stripped.sh b/tests/probes/shadow/test_probes_shadow_stripped.sh new file mode 100755 index 0000000000..8ca4e28039 --- /dev/null +++ b/tests/probes/shadow/test_probes_shadow_stripped.sh @@ -0,0 +1,53 @@ +#!/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::: +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 5 && verify_results "tst" $DF $RF 5 + ret_val=$? + else + ret_val=1 + fi + + if grep -q 'longhashvaluethatneedstoberedacted\|anotherlonghashvalue\|saltsalt\|anothersalt' $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..9dc31b965f --- /dev/null +++ b/tests/probes/shadow/test_probes_shadow_stripped.xml @@ -0,0 +1,139 @@ + + + + + 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 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + sha512user + + + + lockedhash + + + + lockednohash + + + + disabled + + + + neverset + + + + + + sha512user + $6$ + + + + lockedhash + !!$6$ + + + + lockednohash + ! + + + + disabled + * + + + + neverset + !! + + + + From 8fcaef443b82dc9f01011703337c150044f749be Mon Sep 17 00:00:00 2001 From: Arden97 Date: Fri, 25 Sep 2026 15:34:44 +0200 Subject: [PATCH 3/3] update shadow probe test scripts --- src/OVAL/probes/unix/shadow_probe.c | 4 ++-- tests/probes/shadow/test_probes_shadow.xml.sh | 22 ++++++++++++++++++- .../shadow/test_probes_shadow_offline.xml | 2 +- 3 files changed, 24 insertions(+), 4 deletions(-) diff --git a/src/OVAL/probes/unix/shadow_probe.c b/src/OVAL/probes/unix/shadow_probe.c index b2c1148aa0..f0abe4eeb3 100644 --- a/src/OVAL/probes/unix/shadow_probe.c +++ b/src/OVAL/probes/unix/shadow_probe.c @@ -189,8 +189,8 @@ static const char *strip_hash(const char *raw, char *buf, size_t buf_len) if (raw == NULL || *raw == '\0' || strcmp(raw, "!") == 0 || strcmp(raw, "!!") == 0 || - strcmp(raw, "*") == 0 || strcmp(raw, "*LK*") == 0 || - strcmp(raw, "x") == 0) + strcmp(raw, "!*") == 0 || strcmp(raw, "*") == 0 || + strcmp(raw, "*LK*") == 0 || strcmp(raw, "x") == 0) return raw; p = raw; diff --git a/tests/probes/shadow/test_probes_shadow.xml.sh b/tests/probes/shadow/test_probes_shadow.xml.sh index 290fbe71cb..030ea21dce 100644 --- a/tests/probes/shadow/test_probes_shadow.xml.sh +++ b/tests/probes/shadow/test_probes_shadow.xml.sh @@ -9,7 +9,27 @@ 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 [ -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