Three simple CVEs for a good VoIP phone

In this first post, I explain three (simple) malformed input vulnerability that I have found on Grandstream GXP1625 VoIP phone (firmware release version 1.0.4.128 of 03/08/2018 and maybe older ones).

Grandstream GXP1625 “…is a reliable Basic IP phone for the user who requires standard features for a light to medium call volume”1. I have several units of this model (which work very well 🙂 ) in addition to PBXs and DECT phones of the same vendor.

I started the analysis trying to decompress the firmware image available on the company website2. After performs a short research, I have understood that the firmware uses a custom file format and its sections are encrypted with a block cypher (apparently AES). I will explain the firmware file format in a next post.

The phone exposes a WEB server and an SSH daemon listening, respectively, on their “canonical” ports.

The login page

Connecting to the SSH server with user “admin” (with the same password used for web login) we obtain a prompt, which permits to configure phone parameters and run some diagnostic routines but does not permit the access to a complete system shell.

The limited prompt reachable from SSH

CVE-2018-17565

However, one command of this prompt is not well implemented:
the command “ping6”. Ping6 accepts an IPv6 address as input and performs a simple ping to it.

A well formatted input for the ping6 command…

The reply of the command remember me something.. and in facts, a Google search reveals that ping6 command included in busybox replies with the same message”is alive!”3 as the command implemented in the prompt. I suspect that developers have used a system() call in order to “recycle” the busybox ping6 system command to implement the ping. And hopefully, for me, the input is not well checked..

…and a not well formatted one.

OK let’s try to use this vulnerability (probably based on system()) in order to obtain a system shell. From the system() man pages:
“The system() library function uses fork(2) to create a child process that executes the shell command specified in command using execl(3)”4.. so if I use a concatenation of shell commands, system() will execute all of them:

Concatenation of commands executed by the SSH prompt

Bingo! This malformed input permits me to obtain a complete root system shell bypassing the SSH server restrictions induced by the limited prompt.
Now, I want to check directly my hypothesis (system() + not checks on user input). I have found that the ssh daemon (dropbear) calls gs_config, a binary located at /sbin, which executes ping6 and the ash shell.

Since scp was not available on the GXP1625 I used a netcat redirection in order to copy gs_config on my local machine.
I have decompiled gs_config using IDA Pro finding this block:

The developers check only if the first part of the command string is “ping6 ” and call system() using the entire command string as parameter permitting me to use shell concatenation in order to execute arbitrary commands.

Two curiosities: the gs_config is not the default shell of admin, which does not even appear as a valid user of the system…

No sign of user “admin” nor gs_config… but a lot of /bin/sh for internal users…

The developers have modified dropbear in order to call (as root) gs_config as login shell if the logged user is “admin”.
But… is it possible to login as root? No! because dropbear is modified in order to prohibit root login with password… but with keys? Ehm…

I didn’t add it, it is burned in the firmware… and it’s not a good practice…
The haserl script interpreter

The command ps also shows that mini_httpd5 is listening on port 80/TCP and uses the path /app/war as default directory for the files to be served.

The content of /app/war and /app/war/cgi-bin

In particular, the cgi-bin subdir contains scripts which are executed by the web server as root. All these scripts are structured in this way:

#!/usr/bin/haserl
<? 
bash commands
?>

Haserl is a small (no longer maintained) “… cgi wrapper that allows “PHP” style cgi programming, but uses a UNIX bash-like shell or Lua as the programming language … it parses POST and GET requests, placing form-elements as name=value pairs into the environment for the CGI script to use… and it opens a shell, and translates all output text into printable statements”6.

It also contains some security features:

  • it converts all passed parameters var=value into shell variables FORM_VAR=value in order to prevent the overwriting of system variables of the env of the script (e.g. foo.cgi?PATH=”/tmp” produces FORM_PATH=/tmp and not PATH=/tmp)
  • “…The code to populate the environment variables is outside the scope of the sub-shell. It parses on the characters ? and &, so it is harder for a client to do “injection” attacks. As an example, foo.cgi?a=test;cat /etc/passwd could result in a variable being assigned the value test and then the results of running cat /etc/passwd being sent to the client. Haserl will assign the variable the complete value: test;cat /etc/passwd. It is safe to use this “dangerous” variable in shell scripts by enclosing it in quotes; although validation should be done on all input fields …”7

This last statement is relative to some special characters (as “; ? &”) which permit to compose non safe expressions. However, URL encoding permits to write other insecure expressions using “+” as replacement of <space>. For example if you call:

foo.cgi?id=1+-o+-8432

In this case haserl displays a non secure behaviour because it creates only one variable (FORM_id=”1 -o -8432″) containing spaces which will be passed to the script.
It is important to remember that bash expands variables inside test expressions like if [ $VAR EXPRESSION ] (also if variables are double-quoted or inside braces) and, if the variable’s value is a valid logical expression the shell evaluates it. I use this fact in the next two security breaches.

nvram command and the admin password

As shown in the explanation of CVE-2018-17565 I need the admin password in order to access to the limited SSH prompt and escape to a root shell. It would be very interesting to obtain an access to the SSH prompt without knowing the admin password.
The majority of the CGI scripts present into /app/war/cgi-bin require that the user is already authenticated or knows the admin credentials in order to be executed. However, there are some utility scripts which do not require authentication: two of these scripts are the targets of the next attacks.

Before continuing is necessary that I explain a little fact about how the phone loads and saves configuration parameters. Like many others embedded Linux devices, GXP1625 loads and saves configuration parameters inside a non-volatile memory. On the software side the userland command nvram (included into busybox) permits to access to the non-volatile memory and store/retrieve variables as simple tuples VAR=value.

nvram command and its options

In order to find the variable which contains the admin password I use nvram show which dumps all the tuples saved into the non-volatile memory and look for the password.

The variable 2 contains the admin password. nvram permits to use more than one command at the same time (get and show here).

The admin password is contained inside the numerical variable “2”. Furthermore, nvram permits to use more than one command at the same time (get and show in the figure) and print empty line if the variable (or the variables) required with a get command does not exist. These facts are very important for the last CVE that I will explain in this post.

CVE-2018-17564

I focused the attention on /app/war/cgi-bin/delete_CA, an haserl script which does not require authentication and permits to delete, from the phone, a CA certificate previously uploaded.

/app/war/cgi-bin/delete_CA

The interesting parts here are represented by the check test -e $file_path, the assignment pvalue=echo $((_id + 8433)) and the command nvram unset $pvalue followed by nvram commit.

I look for a numerical value for _id variable which permits me to go beyond the test condition (which looks at the existence of a cert file which contains _id into its name) and set pvalue to 2 which corresponds to the name of the variable containing the admin password. So, if I delete its content, thanks to nvram, I can obtain the access to the SSH shell with user: admin and empty password.

It’s time to use the previously shown anomalous behaviour of haserl in the case of URL encoded string containing spaces. Having a phone with IP address 172.16.1.36 and sending to it the request:

http://172.16.1.36/cgi-bin/delete_CA?id=1+-o+-8432

I’m able to delete the admin password.
Detailed explanation:
haserl assigns the entire parameter string to _id, but when _id is used in test statement it transform the logical condition in:

test -e /var/user/CA/gxp_1 -o -8432

which results always true (-o is equal to OR) because ash (and bash) evaluates to True every character after OR (in this case -8432).
Furthermore, bash math operators ignore strings which are not convertible to integer so pvalue=echo $(("1 -o -8432" - + 8433)) assigns to pvalue variable the value 1 – 8432 + 8433 = 2
Finally, the nvram unset $pvalue command deletes the content of the variable pvalue (the admin password) while nvram commit saves the change to the non-volatile memory.

However, if I try to login to the SSH prompt I receive a “wrong password error”. This happens because dropbear refuses to login users with empty passwords, but the web interface permits, in the case of missing password, to use default credentials (admin:admin) in order to login:
curl -X POST -d "username=admin&password=admin" 'http://172.16.1.36/cgi-bin/dologin'
which returns the session cookie 460535838e153739760 and using:
curl -X POST -d "sid=460535838e1537397605&oldadmin=&P2=mynewpass" 'http://172.16.1.36/cgi-bin/api-change_password

I reset the admin password at “mynewpass” which I use to login into SSH.
I use this vulnerability to delete the variable “2” but, generally, it permits to delete any numerical variable you want from the configuration.

CVE-2018-17563

CVE-2018-17564 permits to reset the admin password but this approach to the privilege escalation is not optimal since the legitimate user can detect the intrusion because he is not able to login anymore using the original password. A better strategy consist into directly obtain the admin password stored in the memory. In order to reach this target I exploit the script get_line_status which permits to obtain, without credentials, the status of the VoIP lines configured on the phone.

/app/war/cgi-bin/api-get_line_status (click to enlarge)

The strategy here is the same as in delete_CA: use an input string able to bypass the if clause and pass arguments to nvram. However, in this case I use also the possibility offered by nvram to use multiple commands to interact with the non-volatile memory. The following request:

api-get_line_status?line=show+=+show+-o+6

permits to dump all the values of the configuration without authentication nor session cookie. The bugs in this script are in the following lines:

  • if [ ${_req_line} -ge 1 -a ${_req_line} -le ${num_line} ] ; then This condition requires more attention than the previous one because the expression is longer and contains various sub-expressions. After the substitution of the variable, with the malicious input string, the logical expression results equal to if [show = show -o 6 -ge 1 -a show = show -o 6 -le 1 ]; then which is equivalent (in pseudo-code) to: if (show == show OR 6 >= 1 AND show == show OR 6 <= 1) which is always True.
  • acct="`nvram get cs_${_req_line}_acct`" which after the substitution becomes nvram get cs_show = show -o 6_act tries to print the variables “cs_show”, “=”, “-o” and “6_acct” (resulting in 4 empty lines ) and assigns to $acct all the variables present in the non-volatile memory including the admin password.
  • finally the return_with_jsonwrap_ar function returns the content of $RESULT (which contains also $acct) to the client which has performed the request.
Last exploit in action.

The above picture shows the exploit in action and the admin password exfiltrated from the phone configuration.

Conclusions

I chose to perform a responsible disclosure so I have informed Grandstream development team about these three bugs. After some days we started a fruitful and constant dialogue which has permitted them to release a new firmware version (1.0.4.132) which completely and permanently closes these three vulnerability. I’m continuing to look for other vulnerability in this and other Grandstream products but for now, that’s all folks!