test_clear_funcs.py 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234
  1. # -*- coding: utf-8 -*-
  2. from __future__ import absolute_import, print_function, unicode_literals
  3. import logging
  4. import os
  5. import shutil
  6. import tempfile
  7. import time
  8. import salt.master
  9. import salt.transport.client
  10. import salt.utils.files
  11. import salt.utils.platform
  12. import salt.utils.user
  13. from tests.support.case import TestCase
  14. from tests.support.mixins import AdaptedConfigurationTestCaseMixin
  15. from tests.support.runtests import RUNTIME_VARS
  16. log = logging.getLogger(__name__)
  17. class ConfigMixin:
  18. @classmethod
  19. def setUpClass(cls):
  20. cls.master_config = AdaptedConfigurationTestCaseMixin.get_config("master")
  21. cls.minion_config = AdaptedConfigurationTestCaseMixin.get_temp_config(
  22. "minion",
  23. id="root",
  24. transport=cls.master_config["transport"],
  25. auth_tries=1,
  26. auth_timeout=5,
  27. master_ip="127.0.0.1",
  28. master_port=cls.master_config["ret_port"],
  29. master_uri="tcp://127.0.0.1:{}".format(cls.master_config["ret_port"]),
  30. )
  31. if not salt.utils.platform.is_windows():
  32. user = cls.master_config["user"]
  33. else:
  34. user = salt.utils.user.get_specific_user().replace("\\", "_")
  35. if user.startswith("sudo_"):
  36. user = user.split("sudo_")[-1]
  37. cls.user = user
  38. cls.keyfile = ".{}_key".format(cls.user)
  39. cls.keypath = os.path.join(cls.master_config["cachedir"], cls.keyfile)
  40. with salt.utils.files.fopen(cls.keypath) as keyfd:
  41. cls.key = keyfd.read()
  42. @classmethod
  43. def tearDownClass(cls):
  44. del cls.master_config
  45. del cls.minion_config
  46. del cls.key
  47. del cls.keyfile
  48. del cls.keypath
  49. class ClearFuncsAuthTestCase(ConfigMixin, TestCase):
  50. def test_auth_info_not_allowed(self):
  51. assert hasattr(salt.master.ClearFuncs, "_prep_auth_info")
  52. clear_channel = salt.transport.client.ReqChannel.factory(
  53. self.minion_config, crypt="clear"
  54. )
  55. msg = {"cmd": "_prep_auth_info"}
  56. rets = clear_channel.send(msg, timeout=15)
  57. ret_key = None
  58. for ret in rets:
  59. try:
  60. ret_key = ret[self.user]
  61. break
  62. except (TypeError, KeyError):
  63. pass
  64. assert ret_key != self.key, "Able to retrieve user key"
  65. class ClearFuncsPubTestCase(ConfigMixin, TestCase):
  66. def setUp(self):
  67. tempdir = tempfile.mkdtemp(dir=RUNTIME_VARS.TMP)
  68. self.addCleanup(shutil.rmtree, tempdir, ignore_errors=True)
  69. self.tmpfile = os.path.join(tempdir, "evil_file")
  70. def tearDown(self):
  71. self.tmpfile = None
  72. def test_pub_not_allowed(self):
  73. assert hasattr(salt.master.ClearFuncs, "_send_pub")
  74. assert not os.path.exists(self.tmpfile)
  75. clear_channel = salt.transport.client.ReqChannel.factory(
  76. self.minion_config, crypt="clear"
  77. )
  78. jid = "202003100000000001"
  79. msg = {
  80. "cmd": "_send_pub",
  81. "fun": "file.write",
  82. "jid": jid,
  83. "arg": [self.tmpfile, "evil contents"],
  84. "kwargs": {"show_jid": False, "show_timeout": False},
  85. "ret": "",
  86. "tgt": "minion",
  87. "tgt_type": "glob",
  88. "user": "root",
  89. }
  90. eventbus = salt.utils.event.get_event(
  91. "master",
  92. sock_dir=self.master_config["sock_dir"],
  93. transport=self.master_config["transport"],
  94. opts=self.master_config,
  95. )
  96. ret = clear_channel.send(msg, timeout=15)
  97. if salt.utils.platform.is_windows():
  98. time.sleep(30)
  99. timeout = 30
  100. else:
  101. timeout = 5
  102. ret_evt = None
  103. start = time.time()
  104. while time.time() - start <= timeout:
  105. raw = eventbus.get_event(timeout, auto_reconnect=True)
  106. if raw and "jid" in raw and raw["jid"] == jid:
  107. ret_evt = raw
  108. break
  109. assert not os.path.exists(self.tmpfile), "Evil file created"
  110. class ClearFuncsConfigTest(ConfigMixin, TestCase):
  111. def setUp(self):
  112. self.evil_file_path = os.path.join(
  113. os.path.dirname(self.master_config["conf_file"]), "evil.conf"
  114. )
  115. def tearDown(self):
  116. try:
  117. os.remove(self.evil_file_path)
  118. except OSError:
  119. pass
  120. self.evil_file_path = None
  121. def test_clearfuncs_config(self):
  122. clear_channel = salt.transport.client.ReqChannel.factory(
  123. self.minion_config, crypt="clear"
  124. )
  125. msg = {
  126. "key": self.key,
  127. "cmd": "wheel",
  128. "fun": "config.update_config",
  129. "file_name": "../evil",
  130. "yaml_contents": "win",
  131. }
  132. ret = clear_channel.send(msg, timeout=5)
  133. assert not os.path.exists(
  134. self.evil_file_path
  135. ), "Wrote file via directory traversal"
  136. assert ret["data"]["return"] == "Invalid path"
  137. class ClearFuncsFileRoots(ConfigMixin, TestCase):
  138. def setUp(self):
  139. self.file_roots_dir = self.master_config["file_roots"]["base"][0]
  140. self.target_dir = os.path.dirname(self.file_roots_dir)
  141. def tearDown(self):
  142. try:
  143. os.remove(os.path.join(self.target_dir, "pwn.txt"))
  144. except OSError:
  145. pass
  146. def test_fileroots_write(self):
  147. clear_channel = salt.transport.client.ReqChannel.factory(
  148. self.minion_config, crypt="clear"
  149. )
  150. msg = {
  151. "key": self.key,
  152. "cmd": "wheel",
  153. "fun": "file_roots.write",
  154. "data": "win",
  155. "path": os.path.join("..", "pwn.txt"),
  156. "saltenv": "base",
  157. }
  158. ret = clear_channel.send(msg, timeout=5)
  159. assert not os.path.exists(
  160. os.path.join(self.target_dir, "pwn.txt")
  161. ), "Wrote file via directory traversal"
  162. def test_fileroots_read(self):
  163. readpath = os.path.relpath(self.keypath, self.file_roots_dir)
  164. relative_key_path = os.path.join(self.file_roots_dir, readpath)
  165. log.debug("Master root_dir: %s", self.master_config["root_dir"])
  166. log.debug("File Root: %s", self.file_roots_dir)
  167. log.debug("Key Path: %s", self.keypath)
  168. log.debug("Read Path: %s", readpath)
  169. log.debug("Relative Key Path: %s", relative_key_path)
  170. log.debug("Absolute Read Path: %s", os.path.abspath(relative_key_path))
  171. # If this asserion fails the test may need to be re-written
  172. assert os.path.abspath(relative_key_path) == self.keypath
  173. clear_channel = salt.transport.client.ReqChannel.factory(
  174. self.minion_config, crypt="clear"
  175. )
  176. msg = {
  177. "key": self.key,
  178. "cmd": "wheel",
  179. "fun": "file_roots.read",
  180. "path": readpath,
  181. "saltenv": "base",
  182. }
  183. ret = clear_channel.send(msg, timeout=5)
  184. try:
  185. # When vulnerable this assertion will fail.
  186. assert (
  187. list(ret["data"]["return"][0].items())[0][1] != self.key
  188. ), "Read file via directory traversal"
  189. except IndexError:
  190. pass
  191. # If the vulnerability is fixed, no data will be returned.
  192. assert ret["data"]["return"] == []
  193. class ClearFuncsTokenTest(ConfigMixin, TestCase):
  194. def test_token(self):
  195. tokensdir = os.path.join(self.master_config["cachedir"], "tokens")
  196. assert os.path.exists(tokensdir), tokensdir
  197. clear_channel = salt.transport.client.ReqChannel.factory(
  198. self.minion_config, crypt="clear"
  199. )
  200. msg = {
  201. "arg": [],
  202. "cmd": "get_token",
  203. "token": os.path.join("..", "minions", "minion", "data.p"),
  204. }
  205. ret = clear_channel.send(msg, timeout=5)
  206. assert "pillar" not in ret, "Read minion data via directory traversal"