Tuesday, January 24, 2023

Good bye loginhooks, Hello launchdaemons...

Recently our team began testing macOS Ventura (13.x) in our production testing environment. One of the first issues we noticed was that macOS no longer was going to run our loginhooks, either log in or log out...we knew it was coming.

Shell scripts have the ability to trap the exit of a script and perform additional things. We can exploit that to make a Login and Logout script with the same file. 

https://community.jamf.com/t5/jamf-pro/how-can-i-use-launchd-to-run-logout-script/td-p/42209

So, let's launch the scripts using /Library/LaunchAgents/org.example.loginhook.plist pointing to the scripts? Sure, that'll work...but no root access for anything we wanted to do "special" for the user. Examples of such might be mounting a shadow disk image to /Applications/, setting display or system sleep times, rebooting on specific conditions, etc...

Ok, how about we add a /Library/LaunchDaemon/org.example.login.plist that launches a signed app, which launches a script to run scripts with admin rights. (Am I the baddie?) We also add a PathState to watch for a /tmp/org.example.login file. When that's created, it runs the script. In this case, we wrap the script in an app.

Platypus.app provides a quick method to wrap a script and codesign it for deployment:

sudo codesign -f -s "Developer ID" -v orgLoginHook.app

Once the app is created and signed, it's placed in /Library/Example/Hools/orgLoginHook.app. This works, but the user can't then stop the script at logout...it just goes on and on. I can trigger the LaunchDaemon by writing a file in /tmp/ (PathState), but it won't stop if I delete the files. In fact, the PathState just gets triggered again when it's deleted. Luckily, it's still running...

How does a standard user stop a LaunchDaemon? 

sudo launchctl stop org.example.login

It's possible to allow the standard user to use sudo in the context. (This can't be the best way?) Add the following line to a new file named /etc/sudoers.d/org:

ALL ALL=NOPASSWD: /bin/launchctl stop org.example.login

Users can now stop the script running and trigger the logout events. Logout Hooks are restored.

Comment and let me know what I'm doing wrong or how you also like to abuse your operating system. 

Thanks, rusty.


::Files::

/Library/LaunchDaemons/org.example.login.plist

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>Label</key>
    <string>org.example.login</string>
    <key>PathState</key>
    <dict>
<key>/tmp/org.example.login</key>
<true/>
    </dict>
    <key>RunAtLoad</key>
    <false/>
    <key>ProgramArguments</key>
    <array>
 <string>/Library/Example/Hooks/orgLoginHook.app/Contents/MacOS/orgLoginHook</string>
    </array>
</dict>
</plist>

/Library/LaunchAgents/org.example.loginhook.plist

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>org.example.loginhook</string>
<key>ProgramArguments</key>
<array>
<string>/Library/Example/Hooks/orgLogin.sh</string>
</array>
<key>RunAtLoad</key>
<true/>
</dict>
</plist>

orgLoginHook
#! /bin/zsh
#3.0
# runs everything in /Library/Example/Hooks beginning with LI (for LogIn), LO (for Logout)
#--------------------------------------------------------------------------------------------------
#-- Log - Echo messages with date and timestamp
#--------------------------------------------------------------------------------------------------
Log () {
logText=$1
# indent lines except for program entry and exit
if [[ "${logText}" == "-->"* ]];then
echo 
logText="${logText}`basename $0`: launched..."
else
if [[ "${logText}" == "<--"* ]];then
logText="${logText}`basename $0`: ...terminated" 
else
logText="   ${logText}"
fi
fi
date=$(/bin/date)
echo "${date/E[DS]T /} ${logText}"
}
loginmain () {
Log "-->"
if [[ -d ${HOOKSDIR} ]]; then
for hook in ${HOOKSDIR}/LI*; do
if [[ -s ${hook} && -x ${hook} ]]; then
Log "Executing ${hook} for $loggedInUserID..."
# run the item
${hook} "$loggedInUser"
if [[ $? -ne 0 ]]; then
Log "$0 ${hook} failed!"
fi
fi
done
fi
Log "<--" >> /Library/Example/org/login.log
}

logoutmain () {
Log "-->"
if [[ -d ${HOOKSDIR} ]]; then
for hook in ${HOOKSDIR}/LO*; do
if [[ -s ${hook} && -x ${hook} ]]; then
Log "$0 Executing ${hook} for $loggedInUser... "
# run the item
${hook} "$loggedInUser"
if [[ $? -ne 0 ]]; then
Log "$0 ${hook} failed!"
fi
fi
done
fi
    rm /tmp/org.example.login
Log "<--"
}

export PATH=/bin:/usr/bin:/usr/local/bin:/sbin:/usr/sbin:/usr/local/sbin

HOOKSDIR="/Library/Example/Hooks"
loggedInUser=$( scutil <<< "show State:/Users/ConsoleUser" | awk '/Name :/ && ! /loginwindow/ { print $3 }' )
onLogout() {
    echo 'Logging out' >> /Library/Example/org/login.log
    logoutmain $loggedInUser 2>&1 >> /Library/Example/org/login.log
    exit
}
loginmain $loggedInUser 2>&1 >> /Library/Example/org/login.log

trap 'onLogout' SIGINT SIGHUP SIGTERM
while true; do
    sleep 21600 &
    wait $!
done


orgLogin.sh
#! /bin/zsh
#3.0

#--------------------------------------------------------------------------------------------------
#-- Log - Echo messages with date and timestamp
#--------------------------------------------------------------------------------------------------
Log ()
{
logText=$1
# indent lines except for program entry and exit
if [[ "${logText}" == "-->"* ]];then
logText="${logText}`basename $0`: launched..."
else
if [[ "${logText}" == "<--"* ]];then
logText="${logText}`basename $0`: ...terminated" 
else
logText="   ${logText}"
fi
fi
date=$(/bin/date)
echo "${date/E[DS]T /} ${logText}"
}

loginmain () {
Log "-->"
    touch /tmp/org.example.login
Log "<--"
}

logoutmain () {
Log "-->"
    rm /tmp/org.example.login
    sudo launchctl stop org.example.login
Log "<--"
}

onLogout() {
    echo 'Logging out' >> /Library/Example/org/login.log
    logoutmain $loggedInUser 2>&1 >> /Library/Example/org/login.log
    exit
}

export PATH=/bin:/usr/bin:/usr/local/bin:/sbin:/usr/sbin:/usr/local/sbin
loggedInUser=$( scutil <<< "show State:/Users/ConsoleUser" | awk '/Name :/ && ! /loginwindow/ { print $3 }' )

loginmain $loggedInUser 2>&1 >> /Library/Example/org/login.log

trap 'onLogout' SIGINT SIGHUP SIGTERM
while true; do
    sleep 21600 &
    wait $!
done

No comments: