/
githubmirror
/
salt
Обзор
Документация
Войти
/
githubmirror
/
salt
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
master
tests/pytests/integration/netapi/test_ssh_client.py
473 строки
14 KB
Daniel A. Wozniak
Improve salt-ssh relenv/thin parity and fix various regressions
05 апр 2026, 01:26
05 апр 2026, 01:26
a164b4e
Код
Авторство
О чём код?
import logging import pytest import salt.netapi from salt.exceptions import EauthAuthenticationError, SaltInvocationError from tests.support.helpers import ( SaveRequestsPostHandler, Webserver, system_python_version, ) from tests.support.mock import patch pytestmark = [ pytest.mark.slow_test, pytest.mark.requires_sshd_server, pytest.mark.skipif( 'grains["osfinger"].startswith(("Fedora Linux-40", "Ubuntu-24.04", "Arch Linux"))', reason="System ships with a version of python that is too recent for salt-ssh tests", # Actually, the problem is that the tornado we ship is not prepared for Python 3.12, # and it imports `ssl` and checks if the `match_hostname` function is defined, which # has been deprecated since Python 3.7, so, the logic goes into trying to import # backports.ssl-match-hostname which is not installed on the system. ), pytest.mark.skipif( system_python_version() < (3, 10), reason="System python too old for these tests", ), pytest.mark.skipif( """grains["osfinger"].startswith("VMware Photon OS-5") and __import__("subprocess").run(["rpm", "-q", "openssh-server"], capture_output=True, text=True).stdout.strip() in ["openssh-server-9.3p2-18.ph5.x86_64", "openssh-server-9.3p2-18.ph5.aarch64"]""", reason="Photon OS OpenSSH 9.3p2-18 has a bug that breaks salt-ssh", # This version causes "vdollar_percent_expand: NULL replacement for token n" errors # when using the User config option that salt-ssh depends on. ), pytest.mark.timeout_unless_on_windows(120), ] log = logging.getLogger(__name__) @pytest.fixture def client_config(client_config, known_hosts_file): client_config["known_hosts_file"] = str(known_hosts_file) client_config["netapi_enable_clients"] = ["ssh"] return client_config @pytest.fixture def client(client_config, salt_minion): return salt.netapi.NetapiClient(client_config) @pytest.fixture def rosters_dir(salt_ssh_roster_file): return str(salt_ssh_roster_file.parent) @pytest.fixture def ssh_priv_key(sshd_server): return str(sshd_server.config_dir / "client_key") @pytest.fixture(scope="module") def webserver(): with Webserver(handler=SaveRequestsPostHandler) as server: yield server @pytest.fixture(scope="module") def webserver_root(webserver): return webserver.web_root @pytest.fixture(scope="module") def webserver_handler(webserver): return webserver.handler @pytest.fixture(scope="module") def salt_auth_account_1(salt_auth_account_1_factory): with salt_auth_account_1_factory as account: yield account def test_ssh(client, auth_creds, salt_ssh_roster_file, rosters_dir, ssh_priv_key): low = { "client": "ssh", "tgt": "localhost", "fun": "test.ping", "roster_file": str(salt_ssh_roster_file), "rosters": [rosters_dir], "ssh_priv": ssh_priv_key, **auth_creds, } ret = client.run(low) assert "localhost" in ret assert "return" in ret["localhost"] assert ret["localhost"]["return"] is True assert ret["localhost"]["id"] == "localhost" assert ret["localhost"]["fun"] == "test.ping" def test_ssh_unauthenticated(client): low = {"client": "ssh", "tgt": "localhost", "fun": "test.ping"} with pytest.raises(EauthAuthenticationError): client.run(low) def test_ssh_unauthenticated_raw_shell_curl(client, webserver_root, webserver_handler): fun = f"-o ProxyCommand curl {webserver_root}" low = {"client": "ssh", "tgt": "localhost", "fun": fun, "raw_shell": True} with pytest.raises(EauthAuthenticationError): client.run(low) assert webserver_handler.received_requests == [] def test_ssh_unauthenticated_raw_shell_touch(client, tmp_path): badfile = tmp_path / "badfile.txt" fun = f"-o ProxyCommand touch {badfile}" low = {"client": "ssh", "tgt": "localhost", "fun": fun, "raw_shell": True} with pytest.raises(EauthAuthenticationError): client.run(low) assert badfile.exists() is False def test_ssh_authenticated_raw_shell_disabled(client, tmp_path): badfile = tmp_path / "badfile.txt" fun = f"-o ProxyCommand touch {badfile}" low = {"client": "ssh", "tgt": "localhost", "fun": fun, "raw_shell": True} with patch.dict(client.opts, {"netapi_allow_raw_shell": False}): with pytest.raises(EauthAuthenticationError): client.run(low) assert badfile.exists() is False def test_ssh_disabled(client, auth_creds): low = {"client": "ssh", "tgt": "localhost", "fun": "test.ping", **auth_creds} ret = None with patch.dict(client.opts, {"netapi_enable_clients": []}): with pytest.raises(SaltInvocationError): ret = client.run(low) assert ret is None @pytest.mark.timeout_unless_on_windows(360) def test_shell_inject_ssh_priv( client, salt_ssh_roster_file, rosters_dir, tmp_path, salt_auto_account ): """ Verify CVE-2020-16846 for ssh_priv variable """ # ZDI-CAN-11143 path = tmp_path / "test-11143" low = { "roster": "cache", "client": "ssh", "tgt": "127.0.0.1", "ssh_priv": f"aaa|id>{path} #", "fun": "test.ping", "eauth": "auto", "username": salt_auto_account.username, "password": salt_auto_account.password, "roster_file": str(salt_ssh_roster_file), "rosters": "/", "ignore_host_keys": True, } ret = client.run(low) assert path.exists() is False assert ret assert not ret["127.0.0.1"]["stdout"] assert ret["127.0.0.1"]["stderr"] def test_shell_inject_tgt(client, salt_ssh_roster_file, tmp_path, salt_auto_account): """ Verify CVE-2020-16846 for tgt variable """ # ZDI-CAN-11167 path = tmp_path / "test-11167" low = { "roster": "cache", "client": "ssh", "tgt": f"root|id>{path} #@127.0.0.1", "roster_file": str(salt_ssh_roster_file), "rosters": "/", "fun": "test.ping", "eauth": "auto", "username": salt_auto_account.username, "password": salt_auto_account.password, "ignore_host_keys": True, } ret = client.run(low) assert path.exists() is False assert not ret["127.0.0.1"]["stdout"] assert ret["127.0.0.1"]["stderr"] def test_shell_inject_ssh_options( client, salt_ssh_roster_file, tmp_path, salt_auto_account ): """ Verify CVE-2020-16846 for ssh_options """ # ZDI-CAN-11169 path = tmp_path / "test-11169" low = { "roster": "cache", "client": "ssh", "tgt": "127.0.0.1", "renderer": "jinja|yaml", "fun": "test.ping", "eauth": "auto", "username": salt_auto_account.username, "password": salt_auto_account.password, "roster_file": str(salt_ssh_roster_file), "rosters": "/", "ssh_options": [f"|id>{path} #", "lol"], } ret = client.run(low) assert path.exists() is False assert not ret["127.0.0.1"]["stdout"] assert ret["127.0.0.1"]["stderr"] def test_shell_inject_ssh_port( client, salt_ssh_roster_file, tmp_path, salt_auto_account ): """ Verify CVE-2020-16846 for ssh_port variable """ # ZDI-CAN-11172 path = tmp_path / "test-11172" low = { "roster": "cache", "client": "ssh", "tgt": "127.0.0.1", "renderer": "jinja|yaml", "fun": "test.ping", "eauth": "auto", "username": salt_auto_account.username, "password": salt_auto_account.password, "roster_file": str(salt_ssh_roster_file), "rosters": "/", "ssh_port": f"hhhhh|id>{path} #", "ignore_host_keys": True, } ret = client.run(low) assert path.exists() is False assert not ret["127.0.0.1"]["stdout"] assert ret["127.0.0.1"]["stderr"] def test_shell_inject_remote_port_forwards( client, salt_ssh_roster_file, tmp_path, salt_auto_account ): """ Verify CVE-2020-16846 for remote_port_forwards variable """ # ZDI-CAN-11173 path = tmp_path / "test-1173" low = { "roster": "cache", "client": "ssh", "tgt": "127.0.0.1", "renderer": "jinja|yaml", "fun": "test.ping", "roster_file": str(salt_ssh_roster_file), "rosters": "/", "ssh_remote_port_forwards": f"hhhhh|id>{path} #, lol", "eauth": "auto", "username": salt_auto_account.username, "password": salt_auto_account.password, "ignore_host_keys": True, } ret = client.run(low) assert path.exists() is False assert not ret["127.0.0.1"]["stdout"] assert ret["127.0.0.1"]["stderr"] def test_extra_mods(client, ssh_priv_key, rosters_dir, tmp_path, salt_auth_account_1): """ validate input from extra_mods """ path = tmp_path / "test_extra_mods" low = { "client": "ssh", "tgt": "localhost", "fun": "test.ping", "roster_file": "roster", "rosters": [rosters_dir], "ssh_priv": ssh_priv_key, "eauth": "pam", "username": salt_auth_account_1.username, "password": salt_auth_account_1.password, "regen_thin": True, "thin_extra_mods": f"';touch {path};'", } ret = client.run(low) assert path.exists() is False assert "localhost" in ret assert "return" in ret["localhost"] assert ret["localhost"]["return"] is True def test_ssh_auth_bypass(client, salt_ssh_roster_file): """ CVE-2020-25592 - Bogus eauth raises exception. """ low = { "roster": "cache", "client": "ssh", "tgt": "127.0.0.1", "renderer": "jinja|yaml", "fun": "test.ping", "roster_file": str(salt_ssh_roster_file), "rosters": "/", "eauth": "xx", } with pytest.raises(EauthAuthenticationError): client.run(low) def test_ssh_auth_valid(client, ssh_priv_key, rosters_dir, salt_auth_account_1): """ CVE-2020-25592 - Valid eauth works as expected. """ low = { "client": "ssh", "tgt": "localhost", "fun": "test.ping", "roster_file": "roster", "rosters": [rosters_dir], "ssh_priv": ssh_priv_key, "eauth": "pam", "username": salt_auth_account_1.username, "password": salt_auth_account_1.password, } ret = client.run(low) assert "localhost" in ret assert "return" in ret["localhost"] assert ret["localhost"]["return"] is True def test_ssh_auth_invalid(client, rosters_dir, ssh_priv_key, salt_auth_account_1): """ CVE-2020-25592 - Wrong password raises exception. """ low = { "client": "ssh", "tgt": "localhost", "fun": "test.ping", "roster_file": "roster", "rosters": [rosters_dir], "ssh_priv": ssh_priv_key, "eauth": "pam", "username": salt_auth_account_1.username, "password": "notvalidpassword", } with pytest.raises(EauthAuthenticationError): client.run(low) def test_ssh_auth_invalid_acl(client, rosters_dir, ssh_priv_key, salt_auth_account_1): """ CVE-2020-25592 - Eauth ACL enforced. """ low = { "client": "ssh", "tgt": "localhost", "fun": "at.at", "args": ["12:05am", "echo foo"], "roster_file": "roster", "rosters": [rosters_dir], "ssh_priv": ssh_priv_key, "eauth": "pam", "username": salt_auth_account_1.username, "password": "notvalidpassword", } with pytest.raises(EauthAuthenticationError): client.run(low) def test_ssh_auth_token(client, rosters_dir, ssh_priv_key, salt_auth_account_1): """ CVE-2020-25592 - Eauth tokens work as expected. """ low = { "eauth": "pam", "username": salt_auth_account_1.username, "password": salt_auth_account_1.password, } ret = client.loadauth.mk_token(low) assert "token" in ret assert ret["token"] low = { "client": "ssh", "tgt": "localhost", "fun": "test.ping", "roster_file": "roster", "rosters": [rosters_dir], "ssh_priv": ssh_priv_key, "token": ret["token"], } ret = client.run(low) assert "localhost" in ret assert "return" in ret["localhost"] assert ret["localhost"]["return"] is True def test_ssh_cve_2021_3197_a( client, rosters_dir, ssh_priv_key, salt_auth_account_1, tmp_path ): exploited_path = tmp_path / "exploited" assert exploited_path.exists() is False low = { "eauth": "auto", "username": salt_auth_account_1.username, "password": salt_auth_account_1.password, "client": "ssh", "tgt": "localhost", "fun": "test.ping", "ssh_port": f'22 -o ProxyCommand="touch {exploited_path}"', "ssh_priv": ssh_priv_key, "roster_file": "roster", "rosters": [rosters_dir], } ret = client.run(low) assert exploited_path.exists() is False assert "localhost" in ret assert ret["localhost"]["return"] is True def test_ssh_cve_2021_3197_b( client, rosters_dir, ssh_priv_key, salt_auth_account_1, tmp_path ): exploited_path = tmp_path / "exploited" assert exploited_path.exists() is False low = { "eauth": "auto", "username": salt_auth_account_1.username, "password": salt_auth_account_1.password, "client": "ssh", "tgt": "localhost", "fun": "test.ping", "ssh_port": 22, "ssh_options": [f'ProxyCommand="touch {exploited_path}"'], "ssh_priv": ssh_priv_key, "roster_file": "roster", "rosters": [rosters_dir], } ret = client.run(low) assert exploited_path.exists() is False assert "localhost" in ret assert "return" in ret["localhost"] assert ret["localhost"]["return"] is True