test_client.py 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579
  1. import copy
  2. import logging
  3. import os
  4. import time
  5. import pytest
  6. import salt.config
  7. import salt.netapi
  8. import salt.utils.files
  9. import salt.utils.platform
  10. import salt.utils.pycrypto
  11. from salt.exceptions import EauthAuthenticationError
  12. from tests.support.case import ModuleCase, SSHCase
  13. from tests.support.helpers import (
  14. SKIP_IF_NOT_RUNNING_PYTEST,
  15. SaveRequestsPostHandler,
  16. Webserver,
  17. slowTest,
  18. )
  19. from tests.support.mixins import AdaptedConfigurationTestCaseMixin
  20. from tests.support.mock import patch
  21. from tests.support.runtests import RUNTIME_VARS
  22. from tests.support.unit import TestCase, skipIf
  23. log = logging.getLogger(__name__)
  24. @pytest.mark.usefixtures("salt_master", "salt_sub_minion")
  25. class NetapiClientTest(TestCase):
  26. eauth_creds = {
  27. "username": "saltdev_auto",
  28. "password": "saltdev",
  29. "eauth": "auto",
  30. }
  31. def setUp(self):
  32. """
  33. Set up a NetapiClient instance
  34. """
  35. opts = AdaptedConfigurationTestCaseMixin.get_config("client_config").copy()
  36. self.netapi = salt.netapi.NetapiClient(opts)
  37. def tearDown(self):
  38. del self.netapi
  39. @slowTest
  40. def test_local(self):
  41. low = {"client": "local", "tgt": "*", "fun": "test.ping", "timeout": 300}
  42. low.update(self.eauth_creds)
  43. ret = self.netapi.run(low)
  44. # If --proxy is set, it will cause an extra minion_id to be in the
  45. # response. Since there's not a great way to know if the test
  46. # runner's proxy minion is running, and we're not testing proxy
  47. # minions here anyway, just remove it from the response.
  48. ret.pop("proxytest", None)
  49. self.assertEqual(ret, {"minion": True, "sub_minion": True})
  50. @slowTest
  51. def test_local_batch(self):
  52. low = {"client": "local_batch", "tgt": "*", "fun": "test.ping", "timeout": 300}
  53. low.update(self.eauth_creds)
  54. ret = self.netapi.run(low)
  55. rets = []
  56. for _ret in ret:
  57. rets.append(_ret)
  58. self.assertIn({"sub_minion": True}, rets)
  59. self.assertIn({"minion": True}, rets)
  60. def test_local_async(self):
  61. low = {"client": "local_async", "tgt": "*", "fun": "test.ping"}
  62. low.update(self.eauth_creds)
  63. ret = self.netapi.run(low)
  64. # Remove all the volatile values before doing the compare.
  65. self.assertIn("jid", ret)
  66. ret.pop("jid", None)
  67. ret["minions"] = sorted(ret["minions"])
  68. try:
  69. # If --proxy is set, it will cause an extra minion_id to be in the
  70. # response. Since there's not a great way to know if the test
  71. # runner's proxy minion is running, and we're not testing proxy
  72. # minions here anyway, just remove it from the response.
  73. ret["minions"].remove("proxytest")
  74. except ValueError:
  75. pass
  76. self.assertEqual(ret, {"minions": sorted(["minion", "sub_minion"])})
  77. def test_local_unauthenticated(self):
  78. low = {"client": "local", "tgt": "*", "fun": "test.ping"}
  79. with self.assertRaises(EauthAuthenticationError) as excinfo:
  80. ret = self.netapi.run(low)
  81. @slowTest
  82. def test_wheel(self):
  83. low = {"client": "wheel", "fun": "key.list_all"}
  84. low.update(self.eauth_creds)
  85. ret = self.netapi.run(low)
  86. # Remove all the volatile values before doing the compare.
  87. self.assertIn("tag", ret)
  88. ret.pop("tag")
  89. data = ret.get("data", {})
  90. self.assertIn("jid", data)
  91. data.pop("jid", None)
  92. self.assertIn("tag", data)
  93. data.pop("tag", None)
  94. ret.pop("_stamp", None)
  95. data.pop("_stamp", None)
  96. self.maxDiff = None
  97. self.assertTrue(
  98. {"master.pem", "master.pub"}.issubset(set(ret["data"]["return"]["local"]))
  99. )
  100. @slowTest
  101. def test_wheel_async(self):
  102. # Give this test a little breathing room
  103. time.sleep(3)
  104. low = {"client": "wheel_async", "fun": "key.list_all"}
  105. low.update(self.eauth_creds)
  106. ret = self.netapi.run(low)
  107. self.assertIn("jid", ret)
  108. self.assertIn("tag", ret)
  109. def test_wheel_unauthenticated(self):
  110. low = {"client": "wheel", "tgt": "*", "fun": "test.ping"}
  111. with self.assertRaises(EauthAuthenticationError) as excinfo:
  112. ret = self.netapi.run(low)
  113. @skipIf(True, "This is not testing anything. Skipping for now.")
  114. def test_runner(self):
  115. # TODO: fix race condition in init of event-- right now the event class
  116. # will finish init even if the underlying zmq socket hasn't connected yet
  117. # this is problematic for the runnerclient's master_call method if the
  118. # runner is quick
  119. # low = {'client': 'runner', 'fun': 'cache.grains'}
  120. low = {"client": "runner", "fun": "test.sleep", "arg": [2]}
  121. low.update(self.eauth_creds)
  122. ret = self.netapi.run(low)
  123. @skipIf(True, "This is not testing anything. Skipping for now.")
  124. def test_runner_async(self):
  125. low = {"client": "runner", "fun": "cache.grains"}
  126. low.update(self.eauth_creds)
  127. ret = self.netapi.run(low)
  128. def test_runner_unauthenticated(self):
  129. low = {"client": "runner", "tgt": "*", "fun": "test.ping"}
  130. with self.assertRaises(EauthAuthenticationError) as excinfo:
  131. ret = self.netapi.run(low)
  132. @SKIP_IF_NOT_RUNNING_PYTEST
  133. @pytest.mark.requires_sshd_server
  134. class NetapiSSHClientTest(SSHCase):
  135. eauth_creds = {
  136. "username": "saltdev_auto",
  137. "password": "saltdev",
  138. "eauth": "auto",
  139. }
  140. def setUp(self):
  141. """
  142. Set up a NetapiClient instance
  143. """
  144. opts = AdaptedConfigurationTestCaseMixin.get_config("client_config").copy()
  145. self.netapi = salt.netapi.NetapiClient(opts)
  146. self.priv_file = os.path.join(RUNTIME_VARS.TMP_SSH_CONF_DIR, "client_key")
  147. self.rosters = os.path.join(RUNTIME_VARS.TMP_CONF_DIR)
  148. self.roster_file = os.path.join(self.rosters, "roster")
  149. def tearDown(self):
  150. del self.netapi
  151. @classmethod
  152. def setUpClass(cls):
  153. cls.post_webserver = Webserver(handler=SaveRequestsPostHandler)
  154. cls.post_webserver.start()
  155. cls.post_web_root = cls.post_webserver.web_root
  156. cls.post_web_handler = cls.post_webserver.handler
  157. @classmethod
  158. def tearDownClass(cls):
  159. cls.post_webserver.stop()
  160. del cls.post_webserver
  161. @slowTest
  162. def test_ssh(self):
  163. low = {
  164. "client": "ssh",
  165. "tgt": "localhost",
  166. "fun": "test.ping",
  167. "ignore_host_keys": True,
  168. "roster_file": self.roster_file,
  169. "rosters": [self.rosters],
  170. "ssh_priv": self.priv_file,
  171. }
  172. low.update(self.eauth_creds)
  173. ret = self.netapi.run(low)
  174. self.assertIn("localhost", ret)
  175. self.assertIn("return", ret["localhost"])
  176. self.assertEqual(ret["localhost"]["return"], True)
  177. self.assertEqual(ret["localhost"]["id"], "localhost")
  178. self.assertEqual(ret["localhost"]["fun"], "test.ping")
  179. @slowTest
  180. def test_ssh_unauthenticated(self):
  181. low = {"client": "ssh", "tgt": "localhost", "fun": "test.ping"}
  182. with self.assertRaises(EauthAuthenticationError) as excinfo:
  183. ret = self.netapi.run(low)
  184. @slowTest
  185. def test_ssh_unauthenticated_raw_shell_curl(self):
  186. fun = "-o ProxyCommand curl {}".format(self.post_web_root)
  187. low = {"client": "ssh", "tgt": "localhost", "fun": fun, "raw_shell": True}
  188. ret = None
  189. with self.assertRaises(EauthAuthenticationError) as excinfo:
  190. ret = self.netapi.run(low)
  191. self.assertEqual(self.post_web_handler.received_requests, [])
  192. self.assertEqual(ret, None)
  193. @slowTest
  194. def test_ssh_unauthenticated_raw_shell_touch(self):
  195. badfile = os.path.join(RUNTIME_VARS.TMP, "badfile.txt")
  196. fun = "-o ProxyCommand touch {}".format(badfile)
  197. low = {"client": "ssh", "tgt": "localhost", "fun": fun, "raw_shell": True}
  198. ret = None
  199. with self.assertRaises(EauthAuthenticationError) as excinfo:
  200. ret = self.netapi.run(low)
  201. self.assertEqual(ret, None)
  202. self.assertFalse(os.path.exists("badfile.txt"))
  203. @slowTest
  204. def test_ssh_authenticated_raw_shell_disabled(self):
  205. badfile = os.path.join(RUNTIME_VARS.TMP, "badfile.txt")
  206. fun = "-o ProxyCommand touch {}".format(badfile)
  207. low = {"client": "ssh", "tgt": "localhost", "fun": fun, "raw_shell": True}
  208. low.update(self.eauth_creds)
  209. ret = None
  210. with patch.dict(self.netapi.opts, {"netapi_allow_raw_shell": False}):
  211. with self.assertRaises(EauthAuthenticationError) as excinfo:
  212. ret = self.netapi.run(low)
  213. self.assertEqual(ret, None)
  214. self.assertFalse(os.path.exists("badfile.txt"))
  215. @staticmethod
  216. def cleanup_file(path):
  217. try:
  218. os.remove(path)
  219. except OSError:
  220. pass
  221. @staticmethod
  222. def cleanup_dir(path):
  223. try:
  224. salt.utils.files.rm_rf(path)
  225. except OSError:
  226. pass
  227. @slowTest
  228. def test_shell_inject_ssh_priv(self):
  229. """
  230. Verify CVE-2020-16846 for ssh_priv variable
  231. """
  232. # ZDI-CAN-11143
  233. path = "/tmp/test-11143"
  234. self.addCleanup(self.cleanup_file, path)
  235. self.addCleanup(self.cleanup_file, "aaa")
  236. self.addCleanup(self.cleanup_file, "aaa.pub")
  237. self.addCleanup(self.cleanup_dir, "aaa|id>")
  238. tgt = "www.zerodayinitiative.com"
  239. low = {
  240. "roster": "cache",
  241. "client": "ssh",
  242. "tgt": tgt,
  243. "ssh_priv": "aaa|id>{} #".format(path),
  244. "fun": "test.ping",
  245. "eauth": "auto",
  246. "username": "saltdev_auto",
  247. "password": "saltdev",
  248. "roster_file": self.roster_file,
  249. "rosters": self.rosters,
  250. }
  251. ret = self.netapi.run(low)
  252. self.assertFalse(ret[tgt]["stdout"])
  253. self.assertTrue(ret[tgt]["stderr"])
  254. self.assertFalse(os.path.exists(path))
  255. @slowTest
  256. def test_shell_inject_tgt(self):
  257. """
  258. Verify CVE-2020-16846 for tgt variable
  259. """
  260. # ZDI-CAN-11167
  261. path = "/tmp/test-11167"
  262. self.addCleanup(self.cleanup_file, path)
  263. low = {
  264. "roster": "cache",
  265. "client": "ssh",
  266. "tgt": "root|id>{} #@127.0.0.1".format(path),
  267. "roster_file": self.roster_file,
  268. "rosters": "/",
  269. "fun": "test.ping",
  270. "eauth": "auto",
  271. "username": "saltdev_auto",
  272. "password": "saltdev",
  273. "ignore_host_keys": True,
  274. }
  275. ret = self.netapi.run(low)
  276. self.assertFalse(ret["127.0.0.1"]["stdout"])
  277. self.assertTrue(ret["127.0.0.1"]["stderr"])
  278. self.assertFalse(os.path.exists(path))
  279. @slowTest
  280. def test_shell_inject_ssh_options(self):
  281. """
  282. Verify CVE-2020-16846 for ssh_options
  283. """
  284. # ZDI-CAN-11169
  285. path = "/tmp/test-11169"
  286. self.addCleanup(self.cleanup_file, path)
  287. low = {
  288. "roster": "cache",
  289. "client": "ssh",
  290. "tgt": "127.0.0.1",
  291. "renderer": "jinja|yaml",
  292. "fun": "test.ping",
  293. "eauth": "auto",
  294. "username": "saltdev_auto",
  295. "password": "saltdev",
  296. "roster_file": self.roster_file,
  297. "rosters": "/",
  298. "ssh_options": ["|id>{} #".format(path), "lol"],
  299. }
  300. ret = self.netapi.run(low)
  301. self.assertFalse(ret["127.0.0.1"]["stdout"])
  302. self.assertTrue(ret["127.0.0.1"]["stderr"])
  303. self.assertFalse(os.path.exists(path))
  304. @slowTest
  305. def test_shell_inject_ssh_port(self):
  306. """
  307. Verify CVE-2020-16846 for ssh_port variable
  308. """
  309. # ZDI-CAN-11172
  310. path = "/tmp/test-11172"
  311. self.addCleanup(self.cleanup_file, path)
  312. low = {
  313. "roster": "cache",
  314. "client": "ssh",
  315. "tgt": "127.0.0.1",
  316. "renderer": "jinja|yaml",
  317. "fun": "test.ping",
  318. "eauth": "auto",
  319. "username": "saltdev_auto",
  320. "password": "saltdev",
  321. "roster_file": self.roster_file,
  322. "rosters": "/",
  323. "ssh_port": "hhhhh|id>{} #".format(path),
  324. "ignore_host_keys": True,
  325. }
  326. ret = self.netapi.run(low)
  327. self.assertFalse(ret["127.0.0.1"]["stdout"])
  328. self.assertTrue(ret["127.0.0.1"]["stderr"])
  329. self.assertFalse(os.path.exists(path))
  330. @slowTest
  331. def test_shell_inject_remote_port_forwards(self):
  332. """
  333. Verify CVE-2020-16846 for remote_port_forwards variable
  334. """
  335. # ZDI-CAN-11173
  336. path = "/tmp/test-1173"
  337. self.addCleanup(self.cleanup_file, path)
  338. low = {
  339. "roster": "cache",
  340. "client": "ssh",
  341. "tgt": "127.0.0.1",
  342. "renderer": "jinja|yaml",
  343. "fun": "test.ping",
  344. "roster_file": self.roster_file,
  345. "rosters": "/",
  346. "ssh_remote_port_forwards": "hhhhh|id>{} #, lol".format(path),
  347. "eauth": "auto",
  348. "username": "saltdev_auto",
  349. "password": "saltdev",
  350. "ignore_host_keys": True,
  351. }
  352. ret = self.netapi.run(low)
  353. self.assertFalse(ret["127.0.0.1"]["stdout"])
  354. self.assertTrue(ret["127.0.0.1"]["stderr"])
  355. self.assertFalse(os.path.exists(path))
  356. @pytest.mark.requires_sshd_server
  357. class NetapiSSHClientAuthTest(SSHCase):
  358. USERA = "saltdev-auth"
  359. USERA_PWD = "saltdev"
  360. def setUp(self):
  361. """
  362. Set up a NetapiClient instance
  363. """
  364. opts = salt.config.client_config(
  365. os.path.join(RUNTIME_VARS.TMP_CONF_DIR, "master")
  366. )
  367. naopts = copy.deepcopy(opts)
  368. naopts["ignore_host_keys"] = True
  369. self.netapi = salt.netapi.NetapiClient(naopts)
  370. self.priv_file = os.path.join(RUNTIME_VARS.TMP_SSH_CONF_DIR, "client_key")
  371. self.rosters = os.path.join(RUNTIME_VARS.TMP_CONF_DIR)
  372. self.roster_file = os.path.join(self.rosters, "roster")
  373. # Initialize salt-ssh
  374. self.run_function("test.ping")
  375. self.mod_case = ModuleCase()
  376. try:
  377. add_user = self.mod_case.run_function(
  378. "user.add", [self.USERA], createhome=False
  379. )
  380. self.assertTrue(add_user)
  381. if salt.utils.platform.is_darwin():
  382. hashed_password = self.USERA_PWD
  383. else:
  384. hashed_password = salt.utils.pycrypto.gen_hash(password=self.USERA_PWD)
  385. add_pwd = self.mod_case.run_function(
  386. "shadow.set_password", [self.USERA, hashed_password],
  387. )
  388. self.assertTrue(add_pwd)
  389. except AssertionError:
  390. self.mod_case.run_function("user.delete", [self.USERA], remove=True)
  391. self.skipTest("Could not add user or password, skipping test")
  392. def tearDown(self):
  393. del self.netapi
  394. self.mod_case.run_function("user.delete", [self.USERA], remove=True)
  395. @classmethod
  396. def setUpClass(cls):
  397. cls.post_webserver = Webserver(handler=SaveRequestsPostHandler)
  398. cls.post_webserver.start()
  399. cls.post_web_root = cls.post_webserver.web_root
  400. cls.post_web_handler = cls.post_webserver.handler
  401. @classmethod
  402. def tearDownClass(cls):
  403. cls.post_webserver.stop()
  404. del cls.post_webserver
  405. @slowTest
  406. def test_ssh_auth_bypass(self):
  407. """
  408. CVE-2020-25592 - Bogus eauth raises exception.
  409. """
  410. low = {
  411. "roster": "cache",
  412. "client": "ssh",
  413. "tgt": "127.0.0.1",
  414. "renderer": "jinja|yaml",
  415. "fun": "test.ping",
  416. "roster_file": self.roster_file,
  417. "rosters": "/",
  418. "eauth": "xx",
  419. "ignore_host_keys": True,
  420. }
  421. with self.assertRaises(salt.exceptions.EauthAuthenticationError):
  422. ret = self.netapi.run(low)
  423. @slowTest
  424. def test_ssh_auth_valid(self):
  425. """
  426. CVE-2020-25592 - Valid eauth works as expected.
  427. """
  428. low = {
  429. "client": "ssh",
  430. "tgt": "localhost",
  431. "fun": "test.ping",
  432. "roster_file": "roster",
  433. "rosters": [self.rosters],
  434. "ssh_priv": self.priv_file,
  435. "eauth": "pam",
  436. "username": self.USERA,
  437. "password": self.USERA_PWD,
  438. }
  439. ret = self.netapi.run(low)
  440. assert "localhost" in ret
  441. assert ret["localhost"]["return"] is True
  442. @slowTest
  443. def test_ssh_auth_invalid(self):
  444. """
  445. CVE-2020-25592 - Wrong password raises exception.
  446. """
  447. low = {
  448. "client": "ssh",
  449. "tgt": "localhost",
  450. "fun": "test.ping",
  451. "roster_file": "roster",
  452. "rosters": [self.rosters],
  453. "ssh_priv": self.priv_file,
  454. "eauth": "pam",
  455. "username": self.USERA,
  456. "password": "notvalidpassword",
  457. }
  458. with self.assertRaises(salt.exceptions.EauthAuthenticationError):
  459. ret = self.netapi.run(low)
  460. @slowTest
  461. def test_ssh_auth_invalid_acl(self):
  462. """
  463. CVE-2020-25592 - Eauth ACL enforced.
  464. """
  465. low = {
  466. "client": "ssh",
  467. "tgt": "localhost",
  468. "fun": "at.at",
  469. "args": ["12:05am", "echo foo"],
  470. "roster_file": "roster",
  471. "rosters": [self.rosters],
  472. "ssh_priv": self.priv_file,
  473. "eauth": "pam",
  474. "username": self.USERA,
  475. "password": "notvalidpassword",
  476. }
  477. with self.assertRaises(salt.exceptions.EauthAuthenticationError):
  478. ret = self.netapi.run(low)
  479. @slowTest
  480. def test_ssh_auth_token(self):
  481. """
  482. CVE-2020-25592 - Eauth tokens work as expected.
  483. """
  484. low = {
  485. "eauth": "pam",
  486. "username": self.USERA,
  487. "password": self.USERA_PWD,
  488. }
  489. ret = self.netapi.loadauth.mk_token(low)
  490. assert "token" in ret and ret["token"]
  491. low = {
  492. "client": "ssh",
  493. "tgt": "localhost",
  494. "fun": "test.ping",
  495. "roster_file": "roster",
  496. "rosters": [self.rosters],
  497. "ssh_priv": self.priv_file,
  498. "token": ret["token"],
  499. }
  500. ret = self.netapi.run(low)
  501. assert "localhost" in ret
  502. assert ret["localhost"]["return"] is True