Skip to content

Commit 4490d1b

Browse files
authored
feat(linpeas): expand doas security checks (#676)
1 parent 9b77fde commit 4490d1b

3 files changed

Lines changed: 767 additions & 31 deletions

File tree

Lines changed: 227 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,56 +1,252 @@
11
# Title: Users Information - Doas
22
# ID: UG_Doas
33
# Author: Carlos Polop
4-
# Last Update: 22-08-2023
5-
# Description: Check doas configuration and permissions for privilege escalation
4+
# Last Update: 11-08-2026
5+
# Description: Check doas/OpenDoas configuration, effective rules, binary permissions, and known vulnerable versions.
6+
# Detects unrestricted and nopass root rules, GTFOBins-capable commands (including the HTB Soccer dstat path), dangerous environment preservation, writable configuration paths, and applicable CVEs.
67
# License: GNU GPL
7-
# Version: 1.0
8+
# Version: 1.1
89
# Mitre: T1548.003
9-
# Functions Used: echo_not_found, print_2title, print_info
10-
# Global Variables: $DEBUG, $nosh_usrs, $sh_usrs, $USER
10+
# Functions Used: doas_check_command, doas_command_is_dangerous, doas_config_syntax_valid, doas_extract_upstream_version, doas_get_package_details, doas_read_rules, doas_rule_applies_to_current_user, doas_rule_command, doas_rule_has_dangerous_environment, doas_rule_has_option, doas_rule_targets_root, doas_version_ge, doas_version_le, doas_version_lt, echo_not_found, print_2title, print_3title, print_info
11+
# Global Variables: $doas_package_full_version, $doas_package_homepage, $doas_package_implementation, $doas_package_manager, $doas_package_name
1112
# Initial Functions:
12-
# Generated Global Variables: $doas_dir_name, $doas_bin, $conf_file
13+
# Generated Global Variables: $conf_file, $doas_active_rules, $doas_bin, $doas_bin_mode, $doas_bin_owner, $doas_bin_trusted, $doas_conf_candidates, $doas_conf_dir, $doas_conf_dir_mode, $doas_conf_found, $doas_conf_mode, $doas_conf_owner, $doas_conf_trusted, $doas_current_gids, $doas_current_groups, $doas_current_uid, $doas_current_user, $doas_package_label, $doas_rule_applies, $doas_rule_cmd_value, $doas_rule_dangerous, $doas_rule_env, $doas_rule_line, $doas_rule_nopass, $doas_rule_number, $doas_rule_root, $doas_rule_unrestricted, $doas_seen_configs, $doas_seen_test_commands, $doas_strings_conf, $doas_test_cmd, $doas_test_commands, $doas_check_output, $doas_tiocsti, $doas_upstream_version
1314
# Fat linpeas: 0
1415
# Small linpeas: 1
1516

1617

17-
if [ -f "/etc/doas.conf" ] || [ -f "/usr/local/etc/doas.conf" ] || [ "$DEBUG" ]; then
18-
print_2title "Doas Configuration" "T1548.003"
18+
doas_bin="$(command -v doas 2>/dev/null)"
19+
doas_current_user="$(id -un 2>/dev/null)"
20+
doas_current_uid="$(id -u 2>/dev/null)"
21+
doas_current_groups="$(id -Gn 2>/dev/null)"
22+
doas_current_gids="$(id -G 2>/dev/null)"
23+
doas_bin_trusted="no"
24+
25+
doas_conf_candidates="/etc/doas.conf
26+
/usr/local/etc/doas.conf
27+
/opt/local/etc/doas.conf
28+
/usr/pkg/etc/doas.conf"
29+
30+
if [ -n "$doas_bin" ]; then
31+
doas_conf_candidates="$doas_conf_candidates
32+
$(dirname "$doas_bin")/doas.conf
33+
$(dirname "$doas_bin")/../etc/doas.conf
34+
$(dirname "$doas_bin")/etc/doas.conf"
35+
if command -v strings >/dev/null 2>&1; then
36+
doas_strings_conf="$(strings "$doas_bin" 2>/dev/null | grep -E '^/[^[:space:]]*/doas\.conf$' | head -n 10)"
37+
[ -n "$doas_strings_conf" ] && doas_conf_candidates="$doas_conf_candidates
38+
$doas_strings_conf"
39+
fi
40+
fi
41+
42+
doas_conf_found="no"
43+
for conf_file in /etc/doas.conf /usr/local/etc/doas.conf /opt/local/etc/doas.conf /usr/pkg/etc/doas.conf; do
44+
[ -e "$conf_file" ] && doas_conf_found="yes"
45+
done
46+
47+
if [ -n "$doas_bin" ] || [ "$doas_conf_found" = "yes" ]; then
48+
print_2title "Doas/OpenDoas configuration and vulnerabilities" "T1548.003"
1949
print_info "https://book.hacktricks.wiki/en/linux-hardening/privilege-escalation/index.html#doas"
2050

21-
# Find doas binary and its config locations
22-
doas_bin=$(command -v doas 2>/dev/null)
2351
if [ -n "$doas_bin" ]; then
24-
doas_dir_name=$(dirname "$doas_bin")
52+
print_3title "Doas binary and version" "T1548.003"
53+
# -L makes permission checks describe the executable target, not a package-manager symlink.
54+
doas_bin_owner="$(ls -ldLn "$doas_bin" 2>/dev/null | awk '{print $3}')"
55+
doas_bin_mode="$(ls -ldL "$doas_bin" 2>/dev/null | awk '{print $1}')"
2556
echo "Doas binary found at: $doas_bin" | sed -${E} "s,.*,${SED_LIGHT_CYAN},g"
26-
27-
# Check doas binary permissions
57+
ls -ld "$doas_bin" 2>/dev/null
58+
59+
doas_bin_trusted="yes"
60+
if [ "$doas_bin_owner" != "0" ]; then
61+
echo "WARNING: doas is not owned by root (owner UID: ${doas_bin_owner:-unknown})" | sed -${E} "s,.*,${SED_RED},g"
62+
doas_bin_trusted="no"
63+
fi
2864
if [ -u "$doas_bin" ]; then
29-
echo "Doas binary has SUID bit set!" | sed -${E} "s,.*,${SED_RED},g"
65+
echo "Doas has the expected SUID bit set (normal for a privilege-delegation binary)" | sed -${E} "s,.*,${SED_GREEN},g"
66+
else
67+
echo "Doas does not have its usual SUID bit; verify how privileges are granted" | sed -${E} "s,.*,${SED_RED_YELLOW},g"
68+
doas_bin_trusted="no"
3069
fi
31-
ls -l "$doas_bin" 2>/dev/null | sed -${E} "s,.*,${SED_RED_YELLOW},g"
70+
if { [ "$doas_current_uid" != "0" ] && [ -w "$doas_bin" ]; } || printf "%s" "$doas_bin_mode" | cut -c6,9 | grep -q w; then
71+
echo "CRITICAL: doas is writable by a non-root context or by group/other" | sed -${E} "s,.*,${SED_RED},g"
72+
doas_bin_trusted="no"
73+
fi
74+
75+
doas_get_package_details "$doas_bin"
76+
doas_upstream_version="$(doas_extract_upstream_version "$doas_package_full_version")"
77+
if [ -n "$doas_package_full_version" ]; then
78+
doas_package_label="$doas_package_name $doas_package_full_version (${doas_package_manager:-unknown manager}, implementation: $doas_package_implementation)"
79+
echo "Package: $doas_package_label"
80+
[ -n "$doas_package_homepage" ] && echo "Homepage: $doas_package_homepage"
81+
82+
if { [ "$doas_package_implementation" = "opendoas" ] || [ "$doas_package_implementation" = "unknown" ]; } && \
83+
[ -n "$doas_upstream_version" ] && doas_version_ge "$doas_upstream_version" "6.6" && doas_version_lt "$doas_upstream_version" "6.8.1"; then
84+
echo "Potentially vulnerable to CVE-2019-25016: OpenDoas 6.6 through 6.8 may inherit an attacker-controlled PATH for unrestricted rules (verify distro backports)" | sed -${E} "s,.*,${SED_RED_YELLOW},g"
85+
fi
86+
87+
if [ "$doas_package_implementation" = "slicer69" ] && [ -n "$doas_upstream_version" ] && doas_version_lt "$doas_upstream_version" "6.2"; then
88+
echo "Potentially vulnerable to CVE-2019-15900 and CVE-2019-15901: slicer69/doas before 6.2 can mishandle identities/groups on non-OpenBSD platforms" | sed -${E} "s,.*,${SED_RED_YELLOW},g"
89+
elif [ "$doas_package_implementation" = "unknown" ] && [ -n "$doas_upstream_version" ] && doas_version_lt "$doas_upstream_version" "6.2"; then
90+
echo "Old doas version detected; if this is the slicer69 portable implementation, review CVE-2019-15900 and CVE-2019-15901" | sed -${E} "s,.*,${SED_RED_YELLOW},g"
91+
fi
92+
93+
if [ "$(uname -s 2>/dev/null)" = "Linux" ] && \
94+
{ [ "$doas_package_implementation" = "opendoas" ] || [ "$doas_package_implementation" = "unknown" ]; } && \
95+
[ -n "$doas_upstream_version" ] && doas_version_le "$doas_upstream_version" "6.8.2"; then
96+
if [ -r /proc/sys/dev/tty/legacy_tiocsti ]; then
97+
doas_tiocsti="$(cat /proc/sys/dev/tty/legacy_tiocsti 2>/dev/null)"
98+
if [ "$doas_tiocsti" = "1" ]; then
99+
echo "Potentially vulnerable to CVE-2023-28339: OpenDoas <=6.8.2 shares the terminal and legacy TIOCSTI is enabled" | sed -${E} "s,.*,${SED_RED_YELLOW},g"
100+
else
101+
echo "CVE-2023-28339 TIOCSTI path appears mitigated (dev.tty.legacy_tiocsti=$doas_tiocsti)" | sed -${E} "s,.*,${SED_GREEN},g"
102+
fi
103+
else
104+
echo "OpenDoas <=6.8.2 detected; review CVE-2023-28339 because the kernel TIOCSTI mitigation state could not be read" | sed -${E} "s,.*,${SED_RED_YELLOW},g"
105+
fi
106+
fi
107+
else
108+
echo "Could not determine the installed doas package/version; check vendor advisories manually" | sed -${E} "s,.*,${SED_RED_YELLOW},g"
109+
fi
110+
else
111+
echo_not_found "doas"
32112
fi
33113

34-
# Check all possible doas.conf locations
35-
echo -e "\nChecking doas.conf files:" | sed -${E} "s,.*,${SED_LIGHT_CYAN},g"
36-
for conf_file in "/etc/doas.conf" "$doas_dir_name/doas.conf" "$doas_dir_name/../etc/doas.conf" "$doas_dir_name/etc/doas.conf" "/usr/local/etc/doas.conf"; do
37-
if [ -f "$conf_file" ]; then
38-
echo "Found: $conf_file" | sed -${E} "s,.*,${SED_RED_YELLOW},g"
39-
if [ -w "$conf_file" ]; then
40-
echo "WARNING: $conf_file is writable!" | sed -${E} "s,.*,${SED_RED},g"
114+
echo ""
115+
print_3title "Doas configuration rules" "T1548.003"
116+
doas_conf_found="no"
117+
doas_seen_configs="|"
118+
while IFS= read -r conf_file; do
119+
[ -n "$conf_file" ] || continue
120+
case "$doas_seen_configs" in *"|$conf_file|"*) continue ;; esac
121+
doas_seen_configs="$doas_seen_configs$conf_file|"
122+
[ -e "$conf_file" ] || continue
123+
doas_conf_found="yes"
124+
125+
# Follow the final symlink for ownership/mode checks, but still report the indirection below.
126+
doas_conf_owner="$(ls -ldLn "$conf_file" 2>/dev/null | awk '{print $3}')"
127+
doas_conf_mode="$(ls -ldL "$conf_file" 2>/dev/null | awk '{print $1}')"
128+
doas_conf_dir="$(dirname "$conf_file")"
129+
doas_conf_dir_mode="$(ls -ld "$doas_conf_dir" 2>/dev/null | awk '{print $1}')"
130+
echo "Found: $conf_file ($doas_conf_mode owner UID ${doas_conf_owner:-unknown})" | sed -${E} "s,.*,${SED_LIGHT_CYAN},g"
131+
132+
doas_conf_trusted="yes"
133+
if [ -L "$conf_file" ]; then
134+
echo "WARNING: $conf_file is a symbolic link; verify its target and ownership" | sed -${E} "s,.*,${SED_RED_YELLOW},g"
135+
fi
136+
if [ "$doas_conf_owner" != "0" ]; then
137+
echo "CRITICAL: $conf_file is not owned by root" | sed -${E} "s,.*,${SED_RED},g"
138+
doas_conf_trusted="no"
139+
fi
140+
if { [ "$doas_current_uid" != "0" ] && [ -w "$conf_file" ]; } || printf "%s" "$doas_conf_mode" | cut -c6,9 | grep -q w; then
141+
echo "CRITICAL: $conf_file is writable by the current user, group, or other users" | sed -${E} "s,.*,${SED_RED},g"
142+
doas_conf_trusted="no"
143+
fi
144+
if { [ "$doas_current_uid" != "0" ] && [ -w "$doas_conf_dir" ]; } || printf "%s" "$doas_conf_dir_mode" | cut -c6,9 | grep -q w; then
145+
echo "CRITICAL: configuration directory $doas_conf_dir is writable; doas.conf may be replaceable" | sed -${E} "s,.*,${SED_RED},g"
146+
doas_conf_trusted="no"
147+
fi
148+
149+
if [ -r "$conf_file" ]; then
150+
doas_active_rules="$(doas_read_rules "$conf_file")"
151+
if [ -z "$doas_active_rules" ]; then
152+
echo "No active permit/deny rules found in $conf_file"
153+
else
154+
while IFS=" " read -r doas_rule_number doas_rule_line; do
155+
[ -n "$doas_rule_line" ] || continue
156+
doas_rule_applies="no"
157+
doas_rule_root="no"
158+
doas_rule_nopass="no"
159+
doas_rule_unrestricted="no"
160+
doas_rule_dangerous="no"
161+
doas_rule_env="no"
162+
doas_rule_cmd_value="$(doas_rule_command "$doas_rule_line")"
163+
doas_rule_applies_to_current_user "$doas_rule_line" && doas_rule_applies="yes"
164+
doas_rule_targets_root "$doas_rule_line" && doas_rule_root="yes"
165+
doas_rule_has_option "$doas_rule_line" nopass && doas_rule_nopass="yes"
166+
[ -z "$doas_rule_cmd_value" ] && doas_rule_unrestricted="yes"
167+
[ -n "$doas_rule_cmd_value" ] && doas_command_is_dangerous "$doas_rule_cmd_value" && doas_rule_dangerous="yes"
168+
doas_rule_has_dangerous_environment "$doas_rule_line" && doas_rule_env="yes"
169+
170+
if printf "%s" "$doas_rule_line" | grep -q '^deny'; then
171+
echo " $conf_file:$doas_rule_number $doas_rule_line"
172+
continue
173+
fi
174+
175+
if [ "$doas_rule_applies" = "yes" ] && [ "$doas_rule_root" = "yes" ]; then
176+
echo " $conf_file:$doas_rule_number $doas_rule_line" | sed -${E} "s,.*,${SED_RED_YELLOW},g"
177+
if [ "$doas_rule_unrestricted" = "yes" ] && [ "$doas_rule_nopass" = "yes" ]; then
178+
echo "POTENTIAL: a matching permit rule allows arbitrary root commands without a password; a later rule may override it" | sed -${E} "s,.*,${SED_RED},g"
179+
elif [ "$doas_rule_unrestricted" = "yes" ]; then
180+
echo "POTENTIAL: a matching permit rule allows arbitrary root commands after authentication; a later rule may override it" | sed -${E} "s,.*,${SED_RED_YELLOW},g"
181+
elif [ "$doas_rule_dangerous" = "yes" ] && [ "$doas_rule_nopass" = "yes" ]; then
182+
echo "POTENTIAL: a matching permit rule allows GTFOBins-capable command $doas_rule_cmd_value as root without a password; a later rule may override it" | sed -${E} "s,.*,${SED_RED},g"
183+
if [ "${doas_rule_cmd_value##*/}" = "dstat" ]; then
184+
echo "This is the HTB Soccer privilege-escalation pattern: a user-controlled dstat plugin can execute as root" | sed -${E} "s,.*,${SED_RED},g"
185+
fi
186+
elif [ "$doas_rule_dangerous" = "yes" ]; then
187+
echo "POTENTIAL: a matching permit rule allows GTFOBins-capable command $doas_rule_cmd_value as root after authentication; a later rule may override it" | sed -${E} "s,.*,${SED_RED_YELLOW},g"
188+
elif [ "$doas_rule_nopass" = "yes" ]; then
189+
echo "POTENTIAL: a matching permit rule allows $doas_rule_cmd_value as root without a password; inspect command-specific escapes and later rules" | sed -${E} "s,.*,${SED_RED_YELLOW},g"
190+
fi
191+
if [ "$doas_rule_env" = "yes" ]; then
192+
echo "Dangerous environment preservation is enabled for an applicable root rule (keepenv or sensitive setenv variable)" | sed -${E} "s,.*,${SED_RED_YELLOW},g"
193+
fi
194+
elif [ "$doas_rule_root" = "yes" ] && { [ "$doas_rule_nopass" = "yes" ] || [ "$doas_rule_unrestricted" = "yes" ] || [ "$doas_rule_dangerous" = "yes" ]; }; then
195+
echo " $conf_file:$doas_rule_number $doas_rule_line" | sed -${E} "s,.*,${SED_LIGHT_CYAN},g"
196+
else
197+
echo " $conf_file:$doas_rule_number $doas_rule_line"
198+
fi
199+
done <<EOF
200+
$doas_active_rules
201+
EOF
41202
fi
42-
cat "$conf_file" 2>/dev/null | sed -${E} "s,$sh_usrs,${SED_RED},g" | sed "s,root,${SED_RED},g" | sed "s,nopass,${SED_RED},g" | sed -${E} "s,$nosh_usrs,${SED_BLUE},g" | sed "s,$USER,${SED_RED_YELLOW},g"
203+
else
204+
echo "Cannot read $conf_file directly; attempting safe effective-rule checks with doas -C"
205+
doas_active_rules=""
43206
fi
44-
done
45207

46-
# Check if doas is working
47-
if [ -n "$doas_bin" ]; then
48-
echo -e "\nTesting doas:" | sed -${E} "s,.*,${SED_LIGHT_CYAN},g"
49-
if $doas_bin -l 2>/dev/null; then
50-
echo "doas -l command works!" | sed -${E} "s,.*,${SED_RED_YELLOW},g"
208+
if [ -n "$doas_bin" ] && [ "$doas_bin_trusted" = "yes" ] && [ "$doas_conf_trusted" = "yes" ]; then
209+
if doas_config_syntax_valid "$doas_bin" "$conf_file"; then
210+
doas_test_commands="/bin/sh
211+
/bin/bash
212+
/usr/bin/env
213+
/usr/bin/dstat"
214+
while IFS=" " read -r doas_rule_number doas_rule_line; do
215+
doas_rule_cmd_value="$(doas_rule_command "$doas_rule_line")"
216+
[ -n "$doas_rule_cmd_value" ] && doas_test_commands="$doas_test_commands
217+
$doas_rule_cmd_value"
218+
done <<EOF
219+
$doas_active_rules
220+
EOF
221+
doas_seen_test_commands="|"
222+
while IFS= read -r doas_test_cmd; do
223+
[ -n "$doas_test_cmd" ] || continue
224+
case "$doas_seen_test_commands" in *"|$doas_test_cmd|"*) continue ;; esac
225+
doas_seen_test_commands="$doas_seen_test_commands$doas_test_cmd|"
226+
doas_check_output="$(doas_check_command "$doas_bin" "$conf_file" "$doas_test_cmd")"
227+
case "$doas_check_output" in
228+
"permit nopass"*)
229+
echo "EFFECTIVE RULE: current user may run $doas_test_cmd as root without a password ($doas_check_output)" | sed -${E} "s,.*,${SED_RED},g"
230+
;;
231+
permit*)
232+
echo "Effective rule: current user may run $doas_test_cmd as root after authentication ($doas_check_output)" | sed -${E} "s,.*,${SED_RED_YELLOW},g"
233+
;;
234+
esac
235+
done <<EOF
236+
$doas_test_commands
237+
EOF
238+
else
239+
echo "doas rejected or could not validate $conf_file with -C; inspect its syntax and security properties" | sed -${E} "s,.*,${SED_RED_YELLOW},g"
240+
fi
51241
fi
242+
done <<EOF
243+
$doas_conf_candidates
244+
EOF
245+
246+
if [ "$doas_conf_found" = "no" ]; then
247+
echo_not_found "doas.conf"
52248
fi
53249
else
54-
echo_not_found "doas.conf"
250+
echo_not_found "doas"
55251
fi
56-
echo ""
252+
echo ""

0 commit comments

Comments
 (0)