❌

Visualização normal

Antes de ontemZero Day Initiative - Blog
  • ✇Zero Day Initiative - Blog
  • CVE-2026-35273: SSRF to RCE in Oracle PeopleSoft Bobby Gould
    On September 25, 2026, Mandiant released a report regarding continued mass exploitation of CVE-2026-35273 by the ShinyHunters group. TrendAI’s Zero Day Initiative first reported this CVE to Oracle on June 10, 2026, following an investigation I conducted with Minh Giang and Lucas Miller of TrendAI Research. Although Oracle issued a patch under a single CVE, we reported three distinct vulnerabilities across multiple PeopleSoft components. We are coordinating with Oracle to assign separate CVEs for
     

CVE-2026-35273: SSRF to RCE in Oracle PeopleSoft

27 de Outubro de 2026, 13:39

On September 25, 2026, Mandiant released a report regarding continued mass exploitation of CVE-2026-35273 by the ShinyHunters group. TrendAI’s Zero Day Initiative first reported this CVE to Oracle on June 10, 2026, following an investigation I conducted with Minh Giang and Lucas Miller of TrendAI Research. Although Oracle issued a patch under a single CVE, we reported three distinct vulnerabilities across multiple PeopleSoft components. We are coordinating with Oracle to assign separate CVEs for each issue.

In this blog post, we will walk through each of these vulnerabilities, starting with a Server-Side Request Forgery (SSRF) vulnerability. We will also review the validateClient method that Oracle introduced to allow PeopleSoft system administrators to restrict access to certain functionality. Spoiler alert: the SSRF allows attackers to make requests that satisfy this validation and access endpoints vulnerable to remote code execution (RCE).

Overview

There are two main components involved here, both served as individual Oracle WebLogic applications.

The first, the PeopleSoft Environment Management Hub (PSEMHUB), is part of Oracle’s Environment Management Framework. This component implements the validateClient method, ensuring only trusted clients can reach PSEMHUB functionality. Because this is an internal management interface, it makes sense that Oracle would restrict access to its web endpoints. However, this also means it’s a prime target for attackers looking to pivot from their existing, low-privileged access.

The second, the PeopleSoft Integration Gateway (PSIGW), contains functionality for exchanging messages between the PeopleSoft server and remote systems. This is accomplished using a set of Listening and Target Connectors.

One such connector is the HttpListeningConnector, which translates data from inbound HTTP requests into the format supported by the specified Target Connector before routing the request to the desired URL. In this chain, attackers specify the HttpTargetConnector.

The diagram below shows how an attacker like ShinyHunters can combine these vulnerabilities to compromise PeopleSoft systems. Notice that the attacker cannot simply hit the PSEMHUB functionality directly:

Bug 1: PSIGW Server-Side Request Forgery (ZDI-CAN-31816)

Now let’s take a look at how the PSIGW SSRF provides attackers with a foothold in the PeopleSoft application. An example malicious request to PSIGW shows just how powerful this is for an attacker. We control the URL to hit AND the full payload to send!

At [1], the attacker sends a POST request to /PSIGW/HttpListeningConnector

At [2], the HttpTargetConnector handler class is specified

At [3], the target is PeopleSoft on the loopback interface at http://127.0.0.1:8000, satisfying the validateClient logic

At [4], the full POST request payload is specified in the Data field, to be forwarded to the target URL at [3]

The HttpTargetConnector handler appears below. Note there is no validation to ensure this traffic is legitimate and bound for a valid target. I've done my best to pull out the important parts of this 800+-line function. If you want a fun side quest, go decompile this for yourself and check it out. I hope you like spaghetti!

At [1], retrieve the target URL from the attacker's request

At [2], create an httpObj using the URL from [1]

At [3], retrieve the HTTP method (POST) and apply it to the httpObj from [2]

At [4], send the content data seen in the Data tag of the network request. This content is sent as-is using the HTTP parameters configured in the httpObj

To fully understand why this SSRF is valuable, here is the validateClient method I mentioned earlier:

At [1] and [2], the validateClient method pulls the hostname and IP of the requesting client

At [3], a file path is built to the /envmetadata/config/allowedhosts.properties file, which contains the list of hosts allowed to access functionality protected by validateClient

At [4], the server checks if the allowedList set contains any entries. The first time this path is hit, the set is empty, so execution continues into the client list refresh block

At [5], that allowedhosts.properties file is read and iterated over

At [6] and [7], each allowed host is added to the allowedList set, including the default localhost entry alongside the server's resolved hostname and IP address

At [8] and [9], if the requesting host is found in the allowedList set, validateClient returns true

We can see the localhost entry in the default allowedhosts.properties that ships with PeopleSoft:

While this solution does restrict access, an attacker on any trusted host can still reach PSEMHUB. Because Oracle allows localhost by default, the PSIGW SSRF provides exactly that access.

Bug 2: PSEMHUB External Control of File Path (ZDI-CAN-31818)

Now that the attacker can make requests to the PSEMHUB endpoints using the PSIGW SSRF, let's see how they can turn that into RCE.

The first path to code execution uses an External Control of File Path vulnerability in the ExecuteProcessActivityCommand handler, which is designed to allow remote clients to write log entries to the PeopleSoft server’s filesystem.

However, callers of the command control both the file contents AND the absolute file path on disk to which that content is written. Here’s an example of the request made to PSEMHUB. Note that, in practice, each parameter value is a form-encoded serialized Java object. For clarity, I’ve decoded and formatted it here:

At [1], we see the request is being made to PSEMHUB’s central routing hub

At [2], note that the request is made to the loopback address 127.0.0.1. Since this request originates from localhost, the validateClient check is satisfied

At [3], the web shell contents are specified in the m_line parameter

At [4], the absolute path where the contents will be written is specified in the m_targetFile parameter

Looking at the handler for this request, we can see that no validation is performed to ensure that m_targetFile points to a legitimate log directory:

By writing this shell to the PSEMHUB WAR path, the attacker achieves immediate remote code execution in the context of the PeopleSoft service account.

Here's how those bugs look when chained together:

Bug 3: PSEMHUB Deserialization of Untrusted Data (ZDI-CAN-31817)

Using any file write primitive (like the ExecuteProcessActivityCommand seen above), an attacker can write a malicious XML file inside the [docroot]/envmetadata/data/environment/ directory. When the PSEMHUB HubServlet class initializes next (after a system or service restart, for example), it triggers a code flow that ultimately deserializes this XML file. Because this vector does not require writing a JSP web shell, it is less likely to be noticed by standard detection methods.

Specifically, on initialization, the PSEMHUB HubServlet class calls startServerPeer in the com.peoplesoft.pt.environmentmanagement.hub.Server class:

At [1], HubMBeanPersistance.getInstance() is called, which triggers the private constructor for that class

At [2], env_dir_path is set to <context_root>/envmetadata/data/environment/, which contains the malicious XML file placed by the attacker

At [3], all files from this folder are loaded into a list variable fl

At [4] and [5], the absolute path for each file in that directory is built

At [6], XMLDecoder is instantiated for each file

At [7], the content of the malicious XML placed by the attacker is deserialized

Java's XMLDecoder instantiates and configures the objects described in the XML during parsing, so the attacker's payload executes before the cast to Environment throws an exception.

Conclusion

The combination of the powerful PSIGW Server-Side Request Forgery primitive alongside the decision to allow access to PSEMHUB via the loopback address by default renders the validateClient protection ineffective. Attackers can combine these vulnerabilities to compromise affected instances of PeopleSoft.

The impact of SSRFs can sometimes be overlooked, especially if they have no standalone information disclosure impact. Under the right circumstances, however, the humble SSRF can open the door for complete compromise.

You can find me on Twitter at @bobbygould5, and follow the team on Twitter, Mastodon, LinkedIn, or Bluesky for the latest in exploit techniques and security patches.

  • ✇Zero Day Initiative - Blog
  • Node.js Trust Falls: Dangerous Module Resolution on Windows Bobby Gould and Michael DePlante
    In September of 2024, ZDI received a vulnerability submission from an anonymous researcher affecting npm CLI that revealed a fundamental design issue in Node.js. This blog details how it continues to expose applications to local privilege escalation (LPE) attacks on Windows systems, including the Discord desktop app (CVE-2026-0776 0-Day), which remains unpatched and vulnerable. The issue is straightforward: when Node.js resolves modules, the runtime searches
     

Node.js Trust Falls: Dangerous Module Resolution on Windows

In September of 2024, ZDI received a vulnerability submission from an anonymous researcher affecting npm CLI that revealed a fundamental design issue in Node.js. This blog details how it continues to expose applications to local privilege escalation (LPE) attacks on Windows systems, including the Discord desktop app (CVE-2026-0776 0-Day), which remains unpatched and vulnerable.

The issue is straightforward: when Node.js resolves modules, the runtime searches for packages in C:\node_modules as part of its default behavior. Since low-privileged Windows users can create this directory and plant malicious modules there, any Node.js application with missing or optional dependencies becomes vulnerable to privilege escalation.

This issue is not new. Concerned discussions about Node.js's module search path behavior date back to 2013 and 2014.

Node.js has explicitly stated that they consider this behavior intentional:

"Node.js trusts the file system."

They do not treat CWE-427 (Uncontrolled Search Path Element) as a vulnerability, pushing responsibility onto application developers.

Figure 1: The vendor’s security policy stance on CWE-427 as a non-issue

As the case studies below demonstrate, this stance has dangerous consequences. Developers are largely unaware of this attack surface, and the result is a proliferation of exploitable applications. We will show examples in npm CLI and Discord, but there are likely many more applications that are impacted by this.

Root Cause

The root cause lies in the way Node.js performs module resolution. This is documented here. Although UNIX paths are used in the documentation provided by Node.js, the same logic is applied on Windows.

When a Node.js application calls require(‘bar’), the runtime searches for the module in the following order:

  1. C:\Users\Administrator\projects\node_modules\bar.js
  2. C:\Users\Administrator\node_modules\bar.js
  3. C:\Users\node_modules\bar.js
  4. C:\node_modules\bar.js <-- The problem

If the legitimate package is missing, whether due to optional dependencies, development packages removed in production, or installation failures, the resolution search will eventually reach the root of the drive. Any user can create C:\node_modules and place a malicious package there. Once the low-privileged user has populated C:\node_modules\bar.js, Node.js will load and execute it in the context of the current user. In the following case studies, we will provide evidence of how, despite properly following NPM’s guidelines, third-party dependencies end up triggering this vulnerability anytime you launch the application.

Case Studies: Real-World Manifestations

The Optional Dependency Pattern: npm supports optional dependencies to be specified in the project’s package.json file. The recommended pattern for checking for these dependencies is as follows:

Figure 2: npm Docs showing optionalDependencies example code

This pattern silently catches errors when optional packages are missing, allowing execution to continue. So what’s the problem? On Windows, Node.js will search all the way up to C:\node_modules where an attacker may have planted a malicious replacement. This search behavior mirrors UNIX conventions where /node_modules at the filesystem root is typically only writable by root. Windows systems by default allow any user to create C:\node_modules. Once require is called, Node.js will traverse the search path and execute any matching module it finds.

Important things to note:

  1. This pattern can be found in third party libraries deep in a dependency tree, as we will see in the following examples.
  2. There is no runtime indication to either the developers or the end users that such a vulnerability exists without looking at the filesystem logs with Procmon.
  3. The optional dependency pattern itself would not be dangerous if Node.js did not search for packages in C:\node_modules.

Let’s take a deeper look at both cases and see why this is so dangerous.

Case 1: npm CLI (ZDI-26-043 / ZDI-CAN-25430 / CVE-2026-0775).

Prior to version 11.2.0, npm CLI used a library called “promise-inflight”, which contained an optional dependency on a package called “bluebird”.

Figure 3: npm CLI repo snippet showing require call for missing bluebird package dependency

When Node.js is installed on the system, npm is included by default without the bluebird package. This vulnerability was introduced when bluebird was removed through a well-intentioned pull request (https://github.com/npm/cli/pull/1438/changes), demonstrating how easy it is for developers to unknowingly create this attack surface.

We can see Node’s package resolution logic at work in the screenshot below:

Figure 4: Procmon log showing the package resolution behavior of Node.js via CVE-2026-0775

First, the application looks for the bluebird.js package in the Node.js installation directory. Node.js sequentially searches back to the system root until it finds the package. If an attacker has placed C:\node_modules\bluebird.js, the require call will find, read, and execute the malicious payload in the context of any user running npm on the system.

This vulnerability is especially dangerous because it is triggered when many npm * cli commands are used. Common development commands such as npm install, npm –l, and npm prune will all execute the malicious bluebird.jspackage.

Case 2: Discord (ZDI-26-040/ ZDI-CAN-27057 / CVE-2026-0776/ UNPATCHED)

On April 22, 2025, ZDI received a report for a similar vulnerability in Discord reported by T. Doğa Gelişli. Discord uses the ws WebSocket library, which contains an optional dependency on utf-8-validate for compatibility with older Node.js versions:

Figure 5: websockets library repo snippet showing require call for missing utf-8-validate package dependency

Discord does not ship with the utf-8-validate package. As a result, the following Procmon logs show the same behavior as Case 1. Anytime Discord is launched, the attacker controlled C:\node_modules\utf-8-validate.js is executed.

Figure 6: Procmon log showing the package resolution behavior of Node.js via CVE-2026-0776

The ws library does support disabling this check via the WS_NO_UTF_8_VALIDATE environment variable, but this requires the consuming application (Discord) to set it explicitly. Here’s a quick video demonstrating the bug by popping the calc app when opening Discord:

Discord automatically opens on login by default, so in practice code execution happens immediately without any user interaction. Strangely, the Discord Security team made it clear to us in their responses that they do not consider local attack vectors as valid security issues.

The Bigger Picture

The cases above represent only a few of the applications affected by this pattern. During our investigation we found many other independent reports.  These issues in Mongo DB Compass and Mongo DB Shell are just two other examples.

Every Windows application built on Node.js with missing or optional dependencies is potentially vulnerable. This includes desktop applications that utilize Electron as well as popular web frameworks such as Next.js and React.

Each vendor has clearly stated that they will not treat these issues as vulnerabilities:

NPM’s response to our report:

“exploits that require local access to a machine are considered ineligible for npm CLI

Discord’s response to our report:

“We do not consider physical/local attacks as valid security issues”

Node.js, in the “Examples of non-vulnerabilities” section of their Security Policy:

“Node.js trusts the file system in the environment accessible to it. Therefore, it is not a vulnerability if it accesses/loads files from any path that is accessible to it.”

Conclusion

The vulnerability pattern described in this blog stems from a deliberate design decision by Node.js maintainers. While Node.js's position that “applications should trust their filesystem” may hold true on properly administered UNIX systems, it creates a systemic vulnerability on Windows where low-privileged users can write to C:\node_modules. Without a fix from Node.js, the burden silently falls on application developers.

Making matters worse, the vulnerable code may not live in the application code itself. The optional dependencies that trigger this behavior could come from third-party libraries buried in the dependency tree as we saw with both Discord and npm CLI.

We encourage security researchers to further review this issue and investigate other applications for this dangerous behavior. You can find us online at @bobbygould5 and @izobashi, and follow the team on Twitter, Mastodon, LinkedIn, or Bluesky for the latest in exploit techniques and security patches.

 

DISCLOSURE TIMELINES

 

NPM CLI:

2024-11-13 – ZDI submitted the report to the vendor

2024-11-13 – The vendor acknowledged the receipt of the report

2024-11-13 – The vendor communicated that the reported behavior was by design and they do not consider local attacks as valid security issues

2025-08-05 – ZDI encouraged the vendor to re-assess the issue

2025-12-18 – ZDI notified the vendor of the intention to publish the case as a 0-day advisory

 

DISCORD:

2025-07-08 – ZDI notified vendor

2025-09-11 – ZDI followed up with vendor

2025-09-15 – Vendor stated they do not consider local attacks as valid security issues

2025-12-01 – ZDI explained why we believe the issue is still valid

2025-12-10 – Vendor replied that the vulnerability is still out of scope 

2025-12-11 – ZDI informed vendor of intent to publish 0-day 

  

REFERENCES

https://nodejs.org/api/modules.html#loading-from-node_modules-folders

https://docs.npmjs.com/cli/v10/configuring-npm/package-json#optionaldependencies

https://groups.google.com/g/nodejs/c/5BGr5dliUIk/m/abJEH3sPymcJ?pli=1

https://github.com/nodejs/node-v0.x-archive/issues/8830

https://bounty.github.com/ineligible.html#vulnerability_in_upstream_dependencies:~:text=eligible%20for%20rewards.-,Local%20access,-Vulnerabilities%20which%20require

https://github.com/nodejs/node/security/policy#examples-of-non-vulnerabilities

  • ✇Zero Day Initiative - Blog
  • CVE-2025-20281: Cisco ISE API Unauthenticated Remote Code Execution Vulnerability Bobby Gould
    On January 25th, 2025, the Trend Zero Day Initiative (ZDI) received a report from Kentaro Kawane of GMO Cybersecurity by Ierae regarding a deserialization of untrusted data vulnerability in Cisco Identity Services Engine (ISE). This pre-authentication vulnerability existed in the enableStrongSwanTunnel method of the DescriptionRegistrationListener class. While analyzing this vulnerability, I noticed that the same function was also vulnerable to command injection as root. Cisco patched this initi
     

CVE-2025-20281: Cisco ISE API Unauthenticated Remote Code Execution Vulnerability

25 de Julho de 2025, 13:30

On January 25th, 2025, the Trend Zero Day Initiative (ZDI) received a report from Kentaro Kawane of GMO Cybersecurity by Ierae regarding a deserialization of untrusted data vulnerability in Cisco Identity Services Engine (ISE). This pre-authentication vulnerability existed in the enableStrongSwanTunnel method of the DescriptionRegistrationListener class. While analyzing this vulnerability, I noticed that the same function was also vulnerable to command injection as root. Cisco patched this initially as CVE-2025-20281(ZDI-25-609), but also released CVE-2025-20337 (ZDI-25-607) to fully address the vulnerability. You’ll see why below.

Exploitation wasn't as straight forward as I'd originally hoped but was ultimately a lot more fun than a normal, run-of-the-mill command injection. In this blog, I'll share how I was able to achieve a root shell on affected installations of Cisco ISE, including an escape from a Docker container where the command injection was actually being executed!

More on all that later, let's take a look at the initial vulnerability.

Getting Command Injection

Below is the enableStrongSwanTunnelmethod, which leads to two vulnerabilities – one being the original report ZDI received, while the other is the subject of this blog. Notably, Cisco disagreed and initially assigned the same CVE to both submissions. Was that the right call? I'll leave that judgement to the reader.

At [1] and [2] above, the originally reported unsafe deserialization is seen.

At [3], though, we see that if an attacker provides a valid serialized Java string array, that value is used to invoke a shell script.

Not only that, but the invokeStrongSwanShellScript function contained another interesting goodie:

The sudo command at [4] was even more exciting. Not only did this look very promising as a command injection vulnerability, but that code would be executed as root.

The logger.debug call at [5] looked helpful, so after reconfiguring log4j to output debug logs, I started by sending some test inputs. A quick aside:

Each of the log outputs below came from making individual POST requests to /deployment-rpc/enableStrongSwanTunnel. The body of these requests were string representations of serialized Java String[] array objects, where the first element of the array contained the command injection.

The following array, as an example, shows in the logs as configureStrongSwan.sh enable x; touch /flag when serialized and sent to that endpoint.

      String[] arr = {"x; touch /flag", ""};

For brevity, in the rest of this blog I have included only the resulting commands executed with bash via exec(). In practice, the vector for triggering each was the vulnerable enableStrongSwanTunnel endpoint.

Now, back to those test inputs:

The log looks great; clearly we can control the value of the second concat at [4] above.

Unfortunately, as the subsequent ls call shows, exploitation wasn't quite so straightforward. Because the exec command calls a bash script on disk and passes the attacker-controlled input as a parameter, we cannot simply break the exec call and execute our own code.

Instead, we need to look at configureStrongSwan.sh to see how that parameter is used.

At [1], we call the enableStrongSwan function when using the enable operation specified at [3] above. Here OPERATION is enable and IKE_ID is our attacker-controlled payload.

At [2], we see the only usage of the passed IKE_ID in the function, as a parameter to another function verifyIpsecConnectionStatus At [3] and [4], we use our attacker-controlled payload to build a swanctl command. The argument $1 at [3] corresponds to the IKE_ID parameter.

Finally, at [5], note that we run this command in the docker container: strongswan-container. This was another piece of the puzzle in place. Perhaps the code is executing in that container? Let's take a look:

Still no dice, but a step closer!

A key thing to notice above is that the vulnerable parameter IKE_ID comes from the script argument stored in the variable $2. Consider the following example from the initial test inputs above:

      configureStrongSwan.sh enable x; touch /flag

Here, arg $2 is simply x;. Not as threatening as we hoped. Additional args $3 and $4 exist containing touch and /flag.txt, but those are never used by the script.

Even when we use quotes or backticks, which prevent bash from splitting the payload, nothing happens on disk. Why is that, though? The value of arg $2 in that example tells the story:

      configureStrongSwan.sh enable "x; touch /flag"

Interestingly, the arg value $2 used as the IKE_ID in configureStrongSwan.sh is actually "x;. This is odd. Normally when sending an argument to a bash script we can expect quoted arguments to be kept together. However, it turns out that Java is handling this command differently than bash would by itself.

Tokenization in Java exec() Calls

To properly craft our final exploit, we must first understand a little bit more about how Java's exec method works. Cisco ISE uses Java 8, so let's take a look at the Open JDK code for that version:

At [5], we see that when a string command is passed to exec(), Java tokenizes it using the StringTokenizer class.

More can be read about the StringTokenizer class in Java 8 here. For the purpose of this blog, though, I'll jump right to the point:

StringTokenizer, by design, does not respect quotes or backticks. Rather, its behavior is to simply split string by the provided token (default value is \t\n\r\f, which is used the exec method). This explains why the IKE_ID value from our test inputs was simply "x; when trying to use quotation marks. By the time bash received our input, it had already been tokenized and thus wasn't parsed as one single argument.

Luckily for us, Bash comes built in with a special variable called the Internal Field Separator (${IFS}). More can be read about the purpose and function of this variable here.

For the purposes of this exploit, the usage is simple: an attacker can simply replace all spaces with ${IFS} and bash will interpret everything as expected. The following examples are effectively the same from the perspective of bash:

With the ${IFS} version, however, Java will see the entire payload as a single token, maintaining the integrity of the command.

Additionally, exec is actually working for us now, and we don't even need to include quotes. Java will tokenize our input and ensure it is passed to bash as a single argument.

We can now run the payload using the ${IFS} variable and, viola!

Finally, code execution! This wasn't quite what I was looking for yet, though. The code is executed in the context of the Docker strongswan-container, but what I really wanted was a full root shell on the ISE server host.

Escaping the Container

We can see from the host machine that strongswan-container is running as privileged:

This is quite the gift from Cisco. Because it is running as privileged, we can leverage the "User-Mode Helpers" technique described in this talk [PDF] by Brandon Edwards and Nick Freeman at Black Hat USA 2019.

I've summarized the details related to this specific container setup. More generic information can be found here. The final payload looked like this:

At [1], a Linux cgroup is mounted.

At [2], we specify a shell script simulate.sh to be executed when the cgroup is emptied.

At [3], simulate.sh is created and configured. Here, I've chosen to echo an SSH public key to the /root/.ssh/authorized_keys file on the ISE server.

At [4], empty the cgroup. This causes simulate.sh to be executed on the host ISE server as root.

Putting It All Together

The above technique is great, but we still can't use spaces. Instead of applying ${IFS} everywhere, I chose to base64-encode the container escape script. Then, the payload sent to the vulnerable endpoint could include a base64 -d call to decode the script and pipe it to bash:

echo${IFS}[base64-encoded-escape-script]${IFS}|${IFS}base64${IFS}-d${IFS}|${IFS}bash

Altogether, the final malicious request looked like this:

And, finally, a root shell!

Conclusion

This bug was a lot of fun to exploit and highlights several good-to-know techniques that can come in handy even for a relatively straightforward bug class like command injection. To summarize, an attacker can leverage a perfect storm of mistakes in Cisco Identity Services Engine to achieve full system takeover as root.

By utilizing the ${IFS} variable in bash, we could get around Java's exec method restraining us from using the space character. Additionally, although the initial command injection was executed inside of a Docker container, we were able to break out of the sandbox and execute code on the host because Cisco configured the container in privileged mode.

Finally, for those following along at home: those two bugs Cisco deemed to be duplicates of each other? They were fixed by two different code changes. The definition of "duplicate" is, again, left to the reader.

Keep an eye out for future blogs where I will go into more detail on vulnerabilities I have found in this area. Until then, you can follow me on Twitter at @bobbygould5 and follow the team on Twitter, Mastodon, LinkedIn, or Bluesky for the latest in exploit techniques and security patches.

❌
❌