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;
}