C++ / SSH / SUDO / ROOT / BASH / ЗАДРОЧКА
Тут у нас будет тестовая программа для создания скрипта /test.sh на удаленном хосте от имени root при подключении по ssh. Работать будем с libssh. Суть программы в том, чтобы разобраться в методах аутентификации. Есть ведь еще такой прикол, что в некоторых Linux-дистрибутивах нельзя напрямую подключиться от имени root и поэтому мы идем кривым путем, чтобы рассмотреть получше как жить дальше в таком случае.
nano root_test.cpp
// root_test.cpp // Подключается к удалённому хосту по SSH (libssh), // кладёт /tmp/test.sh через SFTP и переносит его в /test.sh от root с sudo. #include <libssh/libssh.h> #include <libssh/sftp.h> #include <sys/stat.h> #include <fcntl.h> #include <cstring> #include <iostream> #include <string> // Вспомогалки static int run_cmd(ssh_session sess, const std::string& cmd) { ssh_channel ch = ssh_channel_new(sess); if (!ch) return SSH_ERROR; if (ssh_channel_open_session(ch) != SSH_OK) { ssh_channel_free(ch); return SSH_ERROR; } if (ssh_channel_request_exec(ch, cmd.c_str()) != SSH_OK) { ssh_channel_close(ch); ssh_channel_free(ch); return SSH_ERROR; } // выведем stdout/stderr, чтобы было видно, что происходит char buf[4096]; int n; while ((n = ssh_channel_read(ch, buf, sizeof(buf), 0)) > 0) std::cout.write(buf, n); while ((n = ssh_channel_read(ch, buf, sizeof(buf), 1)) > 0) std::cerr.write(buf, n); ssh_channel_send_eof(ch); ssh_channel_close(ch); int rc = ssh_channel_get_exit_status(ch); ssh_channel_free(ch); return rc; } static bool sftp_put_string(ssh_session sess, const std::string& remote_path, const std::string& content, mode_t mode) { sftp_session sftp = sftp_new(sess); if (!sftp) return false; if (sftp_init(sftp) != SSH_OK) { sftp_free(sftp); return false; } sftp_file f = sftp_open(sftp, remote_path.c_str(), O_WRONLY | O_CREAT | O_TRUNC, mode); if (!f) { sftp_free(sftp); return false; } const char* data = content.data(); size_t left = content.size(); while (left > 0) { int w = sftp_write(f, data, left); if (w < 0) { sftp_close(f); sftp_free(sftp); return false; } data += w; left -= w; } sftp_close(f); sftp_free(sftp); return true; } static std::string sh_escape_single(const std::string& s) { std::string out; out.reserve(s.size()+8); out.push_back('\''); for (char c : s) { if (c == '\'') out += "'\\''"; else out.push_back(c); } out.push_back('\''); return out; } int main(int argc, char** argv) { // Параметры можно пробросить аргументами: // make_root_test 192.168.88.202 madmentat 'USER_PASS' 'SUDO_PASS' if (argc < 5) { std::cerr << "usage: " << argv[0] << " <host> <user> <ssh_password> <sudo_password|->\n"; return 2; } std::string host = argv[1]; std::string user = argv[2]; std::string ssh_pass = argv[3]; std::string sudo_pass = argv[4]; // "-" если sudo пароля нет (NOPASSWD) // 1) SSH connect ssh_session sess = ssh_new(); if (!sess) { std::cerr << "ssh_new failed\n"; return 1; } ssh_options_set(sess, SSH_OPTIONS_HOST, host.c_str()); ssh_options_set(sess, SSH_OPTIONS_USER, user.c_str()); int port = 22; ssh_options_set(sess, SSH_OPTIONS_PORT, &port); if (ssh_connect(sess) != SSH_OK) { std::cerr << "ssh_connect: " << ssh_get_error(sess) << "\n"; ssh_free(sess); return 1; } if (ssh_userauth_password(sess, nullptr, ssh_pass.c_str()) != SSH_AUTH_SUCCESS) { std::cerr << "auth failed: " << ssh_get_error(sess) << "\n"; ssh_disconnect(sess); ssh_free(sess); return 1; } std::cout << "✓ SSH connected as " << user << "@" << host << "\n"; // 2) SFTP: пишем /tmp/test.sh const std::string remote_tmp = "/tmp/test.sh"; const std::string payload = R"(#!/usr/bin/env bash echo "Hello from /test.sh" id date )"; if (!sftp_put_string(sess, remote_tmp, payload, 0644)) { std::cerr << "SFTP upload failed\n"; ssh_disconnect(sess); ssh_free(sess); return 1; } std::cout << "✓ uploaded " << remote_tmp << "\n"; // 3) sudo install → /test.sh (root:root, 0755), cleanup std::string sudo_prefix; if (sudo_pass == "-" || sudo_pass.empty()) { sudo_prefix = "sudo -n"; } else { // без перевода строки и без prompt sudo_prefix = "printf %s " + sh_escape_single(sudo_pass) + " | sudo -S -p ''"; } std::string cmd = "sh -lc " + sh_escape_single( sudo_prefix + " install -m 0755 /tmp/test.sh /test.sh; " + sudo_prefix + " chown root:root /test.sh; " + sudo_prefix + " rm -f /tmp/test.sh; " + "ls -l /test.sh" ); int rc = run_cmd(sess, cmd); if (rc != 0) { std::cerr << "remote sudo/install failed, rc=" << rc << "\n"; ssh_disconnect(sess); ssh_free(sess); return 1; } std::cout << "✓ /test.sh installed as root\n"; // 4) (необязательно) — выполнить /test.sh, чтобы увидеть вывод std::string run = "sh -lc " + sh_escape_single(sudo_prefix + " /test.sh"); run_cmd(sess, run); ssh_disconnect(sess); ssh_free(sess); return 0; }
g++ -std=c++17 -O2 make_root_test.cpp -lssh -o make_root_test
Запуск командой
./root_test 192.168.88.202 madmentat 'XXX' 'XXX'
Далее немного более навороченная версия с рабочим скриптом для переключалки конфига Nginx
// make_root_test.cpp // Uploads nginx switcher script to 202 via SFTP and installs it as /test.sh via sudo. // Build: g++ -std=c++17 -O2 make_root_test.cpp -lssh -o make_root_test #include <libssh/libssh.h> #include <libssh/sftp.h> #include <fcntl.h> #include <unistd.h> #include <cstdio> #include <cstdlib> #include <cstring> #include <string> #include <iostream> #include <sstream> // ---------- helpers ---------- static inline std::string sh_escape_single(const std::string& s) { // Escape single quotes for single-quoted shell strings: ' -> '\'' std::string out; out.reserve(s.size() + 8); for (char c: s) { if (c == '\'') out += "'\\''"; else out += c; } return out; } static int ssh_exec(ssh_session ses, const std::string& sh_cmd, bool print_out=true) { ssh_channel ch = ssh_channel_new(ses); if (!ch) return -1; if (ssh_channel_open_session(ch) != SSH_OK) { ssh_channel_free(ch); return -1; } // run as: sh -lc '<cmd>' std::string wrapped = "sh -lc '" + sh_escape_single(sh_cmd) + "'"; if (ssh_channel_request_exec(ch, wrapped.c_str()) != SSH_OK) { ssh_channel_close(ch); ssh_channel_free(ch); return -1; } if (print_out) { char buf[4096]; int n; while ((n = ssh_channel_read(ch, buf, sizeof(buf), 0)) > 0) { std::cout.write(buf, n); } while ((n = ssh_channel_read(ch, buf, sizeof(buf), 1)) > 0) { std::cerr.write(buf, n); } } else { char buf[1024]; while (ssh_channel_read(ch, buf, sizeof(buf), 0) > 0) {} while (ssh_channel_read(ch, buf, sizeof(buf), 1) > 0) {} } int status = ssh_channel_get_exit_status(ch); ssh_channel_send_eof(ch); ssh_channel_close(ch); ssh_channel_free(ch); return status; } static int sftp_write_all(sftp_session sftp, const std::string& remote, const std::string& data, mode_t mode) { sftp_file f = sftp_open(sftp, remote.c_str(), O_WRONLY | O_CREAT | O_TRUNC, mode); if (!f) return -1; const char* p = data.data(); size_t left = data.size(); while (left > 0) { int chunk = (left > 32768) ? 32768 : (int)left; int w = sftp_write(f, p, chunk); if (w < 0) { sftp_close(f); return -2; } left -= (size_t)w; p += w; } int rc = sftp_close(f); return (rc == SSH_OK) ? 0 : -3; } // ---------- main ---------- int main(int argc, char** argv) { if (argc != 5) { std::cerr << "usage:\n " << argv[0] << " <host> <user> <ssh_pass> <sudo_pass>\n"; return 2; } std::string host = argv[1]; std::string user = argv[2]; std::string ssh_pass = argv[3]; std::string sudo_pass = argv[4]; // 1) SSH connect ssh_session ses = ssh_new(); if (!ses) { std::cerr << "ssh_new failed\n"; return 1; } ssh_options_set(ses, SSH_OPTIONS_HOST, host.c_str()); ssh_options_set(ses, SSH_OPTIONS_USER, user.c_str()); if (ssh_connect(ses) != SSH_OK) { std::cerr << "ssh_connect: " << ssh_get_error(ses) << "\n"; ssh_free(ses); return 1; } if (ssh_userauth_password(ses, nullptr, ssh_pass.c_str()) != SSH_AUTH_SUCCESS) { std::cerr << "auth failed: " << ssh_get_error(ses) << "\n"; ssh_disconnect(ses); ssh_free(ses); return 1; } std::cout << "OK SSH " << user << "@" << host << "\n"; // 2) Build script content safely (no raw strings) std::ostringstream oss; oss << "#!/usr/bin/env bash\n"; oss << "set -Eeuo pipefail\n"; oss << "SERVER_NAME='madmentat.ru'\n"; oss << "# local backend (on 202)\n"; oss << "LOCAL_HOST=127.0.0.1\n"; oss << "LOCAL_PORT=8081\n"; oss << "# remote backend (main host 198)\n"; oss << "REMOTE_HOST='192.168.88.198'\n"; oss << "REMOTE_PORT=80\n"; oss << "CONF=/etc/nginx/conf.d/${SERVER_NAME}.backend.conf\n"; oss << "usage(){ echo \"usage: $0 local|remote|status\" >&2; exit 2; }\n"; oss << "write_local() {\n"; oss << " {\n"; oss << " echo \"# autogenerated setup_${SERVER_NAME}_nginx.sh\"\n"; oss << " echo \"set \\$mad_backend http://${LOCAL_HOST}:${LOCAL_PORT};\"\n"; oss << " } > \"$CONF\"\n"; oss << "}\n"; oss << "write_remote() {\n"; oss << " {\n"; oss << " echo \"# autogenerated setup_${SERVER_NAME}_nginx.sh\"\n"; oss << " echo \"set \\$mad_backend http://${REMOTE_HOST}:${REMOTE_PORT};\"\n"; oss << " } > \"$CONF\"\n"; oss << "}\n"; oss << "case \"${1:-}\" in\n"; oss << " local) write_local ;;\n"; oss << " remote) write_remote ;;\n"; oss << " status)\n"; oss << " echo \"----- $CONF -----\"\n"; oss << " [ -f \"$CONF\" ] && sed -n '1,200p' \"$CONF\" || echo \"(no file)\"\n"; oss << " exit 0\n"; oss << " ;;\n"; oss << " *) usage ;;\n"; oss << "esac\n"; oss << "if nginx -t; then\n"; oss << " systemctl reload nginx || nginx -s reload || true\n"; oss << " echo \"switched to: $1\"\n"; oss << "else\n"; oss << " echo \"nginx -t failed; not reloading\" >&2\n"; oss << " exit 1\n"; oss << "fi\n"; const std::string script = oss.str(); // 3) SFTP upload to /tmp/_test.sh sftp_session sftp = sftp_new(ses); if (!sftp) { std::cerr << "sftp_new failed\n"; ssh_disconnect(ses); ssh_free(ses); return 1; } if (sftp_init(sftp) != SSH_OK) { std::cerr << "sftp_init failed: " << ssh_get_error(ses) << "\n"; sftp_free(sftp); ssh_disconnect(ses); ssh_free(ses); return 1; } const std::string remote_tmp = "/tmp/_test.sh"; if (sftp_write_all(sftp, remote_tmp, script, 0644) != 0) { std::cerr << "upload failed\n"; sftp_free(sftp); ssh_disconnect(ses); ssh_free(ses); return 1; } sftp_free(sftp); std::cout << "uploaded " << remote_tmp << "\n"; // 4) Install to /test.sh via sudo (ASCII-only, each sudo has its own password feed) std::ostringstream cmd; cmd << "printf %s '" << sh_escape_single(sudo_pass) << "' | sudo -S -p '' install -m 0755 /tmp/_test.sh /test.sh && " << "printf %s '" << sh_escape_single(sudo_pass) << "' | sudo -S -p '' chown root:root /test.sh && " << "printf %s '" << sh_escape_single(sudo_pass) << "' | sudo -S -p '' chmod 0755 /test.sh && " << "printf %s '" << sh_escape_single(sudo_pass) << "' | sudo -S -p '' ls -l /test.sh"; int rc = ssh_exec(ses, cmd.str(), true); if (rc != 0) { std::cerr << "remote install failed, rc=" << rc << "\n"; ssh_disconnect(ses); ssh_free(ses); return 1; } ssh_disconnect(ses); ssh_free(ses); return 0; }