From 87159c46c410dd7a84ea35f02766e87515ec9e9c Mon Sep 17 00:00:00 2001 From: immibis Date: Mon, 13 Jan 2025 18:19:50 +0100 Subject: [PATCH] make git repository for git upload. quick code presented without warranty or support. --- Makefile | 2 + main.cpp | 249 ++++++++++++++++++++++++++++++++ proftpd_test/command | 1 + proftpd_test/proftpd.conf | 8 + proftpd_test_2/command | 1 + proftpd_test_2/proftpd.conf | 31 ++++ sshd_test/ftp-upload | 1 + sshd_test/pam-service-name-note | 1 + sshd_test/sshd_command | 2 + sshd_test/sshd_config | 19 +++ vsftpd_test/vsftpd.conf | 25 ++++ 11 files changed, 340 insertions(+) create mode 100644 Makefile create mode 100644 main.cpp create mode 100644 proftpd_test/command create mode 100644 proftpd_test/proftpd.conf create mode 100644 proftpd_test_2/command create mode 100644 proftpd_test_2/proftpd.conf create mode 120000 sshd_test/ftp-upload create mode 100644 sshd_test/pam-service-name-note create mode 100644 sshd_test/sshd_command create mode 100644 sshd_test/sshd_config create mode 100644 vsftpd_test/vsftpd.conf diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..87e48b6 --- /dev/null +++ b/Makefile @@ -0,0 +1,2 @@ +pam_firstcomefirstserve.so: main.cpp + g++ -shared -fPIC -o $@ $^ -lsqlite3 diff --git a/main.cpp b/main.cpp new file mode 100644 index 0000000..5dfc3da --- /dev/null +++ b/main.cpp @@ -0,0 +1,249 @@ +#include +#include +#include +#include +#include +#include +#include // for mkdir +#include // for chown +#include +#include +#include + +#define MYNAME "pam_firstcomefirstserve" + +struct database { + sqlite3 *sql = nullptr; + ~database() { + if(sql) { + if(SQLITE_OK != sqlite3_close(sql)) { + fprintf(stderr, MYNAME ": sqlite3_close failed: %s\n", sqlite3_errmsg(sql)); + // still continue, but with leaking memory + } + sql = nullptr; + } + } + + bool commit(); + + database() {} + database(database const&) = delete; + database& operator=(database const&) = delete; +}; + +// error handling designed so that if an error occurs, all operations are ignored and then you check once at the end. +struct statement { + sqlite3_stmt *stmt = nullptr; + sqlite3 *db; + bool ok = true; + statement(database& db, const char *command) { + this->db = db.sql; + if(SQLITE_OK != sqlite3_prepare_v2(db.sql, command, -1, &stmt, nullptr)) { + fprintf(stderr, MYNAME ": %s: %s\n", command, sqlite3_errmsg(db.sql)); + ok = false; + } + } + + // sqlite3_step returns an error, finished, or a row data. + bool step() { // returns false if statement is finished (possibly with error), or true if we paused because another row is available to read from the results + if(!stmt || !ok) + return false; + int result = sqlite3_step(stmt); + if(result == SQLITE_ROW) + return true; + if(result != SQLITE_DONE) { + fprintf(stderr, MYNAME ": %s\n", sqlite3_errmsg(db)); + ok = false; + } + return false; + } + + void bind(int paramNum, std::string const& string) { // string must outlive statement object is destroyed (that's what SQLITE_STATIC means) + if(!stmt || !ok) return; + + if(SQLITE_OK != sqlite3_bind_text(stmt, paramNum, string.data(), string.size(), SQLITE_STATIC)) { + fprintf(stderr, MYNAME ": sqlite3_bind_text: %s\n", sqlite3_errmsg(db)); + ok = false; + } + } + + // undefined results (including crash) if row data is accessed any time except after step returns true, or if type doesn't match + // note: parameters start at 1, but columns start at 0 + std::string getColumnText(int columnNum) { + const char *data = (const char *)sqlite3_column_text(stmt, columnNum); + int length = sqlite3_column_bytes(stmt, columnNum); + return std::string(data, data+length); + } + + ~statement() { + if(stmt) { + if(sqlite3_finalize(stmt) != SQLITE_OK) { + fprintf(stderr, MYNAME ": sqlite3_finalize: %s\n", sqlite3_errmsg(db)); + // leak memory, but continue anyway + } + stmt = nullptr; + } + } + + statement(statement const&) = delete; + statement& operator=(statement const&) = delete; +}; + +bool database::commit() { + statement st(*this, "commit"); + while(st.step()) {} // should complete in one step + return st.ok; +} + +int pam_sm_authenticate(pam_handle_t *pamh, int flags, int argc, const char **argv) { + struct pam_conv *conv; + if(PAM_SUCCESS != pam_get_item(pamh, PAM_CONV, (const void**)&conv)) { + fprintf(stderr, MYNAME ": auth: pam_get_item(PAM_CONV) failed (programming error in application)\n"); + return PAM_AUTHINFO_UNAVAIL; + } + + const char *username_; + if(PAM_SUCCESS != pam_get_item(pamh, PAM_USER, (const void**)&username_)) { + fprintf(stderr, MYNAME ": auth: pam_get_item(PAM_USER) failed (programming error in application)\n"); + return PAM_AUTHINFO_UNAVAIL; + } + std::string username = username_; + + // Since we create directories based on usernames, we have to enforce some basic restrictions + // so we don't create /srv/upload/../../etc/passwd or something. + // Usernames are not empty, don't start with "." and contain alphanumeric characters, '_', '-' and '.'. + for(char c : username) { + if(!isalnum(c) && c != '_' && c != '.' && c != '-') { + fprintf(stderr, MYNAME ": invalid username\n"); + return PAM_AUTH_ERR; + } + } + if(username.size() == 0 || username[0] == '.') { + fprintf(stderr, MYNAME ": invalid username\n"); + return PAM_AUTH_ERR; + } + + // argv does not include the module name: argv[0] is the first argument, and argc is 0 if there are no arguments. + if(argc != 4) { + fprintf(stderr, MYNAME ": auth: wrong number of arguments. arguments must be database path, parent of user home directories, home dir UID and home dir GID\n"); + return PAM_AUTHINFO_UNAVAIL; + } + int home_dir_uid = atoi(argv[2]), home_dir_gid = atoi(argv[3]); + if(!home_dir_uid || !home_dir_gid) { // conveniently catches non-numbers and root + fprintf(stderr, MYNAME ": auth: please check home uid/gid (args 3/4)\n"); + return PAM_AUTHINFO_UNAVAIL; + } + + + std::string password; + { + struct pam_message msg = {PAM_PROMPT_ECHO_OFF, "Password: "}; + const struct pam_message *msgs = {&msg}; + struct pam_response *responses = NULL; + if(PAM_SUCCESS != conv->conv(1, &msgs, &responses, conv->appdata_ptr)) { + fprintf(stderr, MYNAME ": auth: password prompt failed\n"); + // do not free responses in this case? "The application should not set *resp" + return PAM_AUTHINFO_UNAVAIL; + } + password = responses[0].resp; + free(responses[0].resp); + free(responses); + }; + + database db; + if(SQLITE_OK != sqlite3_open(argv[0], &db.sql)) { + fprintf(stderr, MYNAME ": auth: sqlite3_open failed\n"); + return PAM_AUTHINFO_UNAVAIL; + } + + if(SQLITE_OK != sqlite3_busy_timeout(db.sql, 10000)) { // milliseconds. Call should always succeed? + fprintf(stderr, MYNAME ": auth: sqlite3_busy_timeout: %s\n", sqlite3_errmsg(db.sql)); + } + + // Don't make any database changes until after reading the password, because SQLite may take a lock + + { + statement st(db, "begin transaction"); + while(st.step()) {} + if(!st.ok) { + fprintf(stderr, MYNAME ": auth: failed to begin database transaction\n"); + return PAM_AUTHINFO_UNAVAIL; + } + } + + { + statement st(db, "create table if not exists users (username varchar not null primary key, password varchar not null)"); + while(st.step()) {} // should finish in one step, but anyway + // no st.ok check - if an error occurred, it was already printed, and we continue anyway - + // if the error is still present, we'll get the same error when accessing the data, anyway. + } + + auto make_home_dir_and_subdirs = [&]() { + auto make_home_dir = [&](std::string const& subdir) { + std::string createpath = std::string(argv[1])+"/"+username; + if(subdir != "") createpath += "/" + subdir; + if(mkdir(createpath.c_str(), 0700) && errno != EEXIST) { + fprintf(stderr, "mkdir %s: %s\n", createpath.c_str(), strerror(errno)); + } + if(chown(createpath.c_str(), home_dir_uid, home_dir_gid)) { + fprintf(stderr, "chown %s: %s\n", createpath.c_str(), strerror(errno)); + } + }; + make_home_dir(""); + + std::ifstream in("/srv/ftp-subdirs"); + std::string subdir; + while(std::getline(in, subdir)) { + if(subdir != "") + make_home_dir(subdir); + } + }; + + { + statement st(db, "select password from users where username = ?1"); + st.bind(1, username); + if(st.step()) { + // Found the username. Does their password match? + std::string right_pw = st.getColumnText(0); + if(right_pw == password) { + make_home_dir_and_subdirs(); + return PAM_SUCCESS; + } else { + return PAM_AUTH_ERR; + } + + } else if(st.ok) { + // New user! (Query succeeded, but didn't find any row) + statement st2(db, "insert into users (username, password) values (?1, ?2)"); + st2.bind(1, username); + st2.bind(2, password); + while(st2.step()) {} + + if(st2.ok && db.commit()) { + make_home_dir_and_subdirs(); + return PAM_SUCCESS; + } + } + } + + return PAM_AUTHINFO_UNAVAIL; +} + +int pam_sm_setcred(pam_handle_t *pamh, int flags, int argc, const char **argv) { + return PAM_SUCCESS; +} + +#if 0 + +int pam_sm_acct_mgmt(pam_handle_t *pamh, int flags, int argc, const char **argv) { + +} + +int pam_sm_open_session(pam_handle_t *pamh, int flags, int argc, const char **argv) { + +} + +int pam_sm_close_session(pam_handle_t *pamh, int flags, int argc, const char **argv) { + +} +#endif diff --git a/proftpd_test/command b/proftpd_test/command new file mode 100644 index 0000000..44c1e57 --- /dev/null +++ b/proftpd_test/command @@ -0,0 +1 @@ +sudo proftpd -c $PWD/proftpd.conf -n -d10 diff --git a/proftpd_test/proftpd.conf b/proftpd_test/proftpd.conf new file mode 100644 index 0000000..2b26fb5 --- /dev/null +++ b/proftpd_test/proftpd.conf @@ -0,0 +1,8 @@ +AuthPAM on +# configures which file in /etc/pam.d is used for proftpd access check +AuthPAMConfig ftp-upload +# ONLY authenticate with pam +AuthOrder mod_auth_pam.c + +Trace auth:10 auth.pam:10 +TraceLog /dev/tty diff --git a/proftpd_test_2/command b/proftpd_test_2/command new file mode 100644 index 0000000..ba29d15 --- /dev/null +++ b/proftpd_test_2/command @@ -0,0 +1 @@ +sudo proftpd -c $PWD/proftpd.conf -n diff --git a/proftpd_test_2/proftpd.conf b/proftpd_test_2/proftpd.conf new file mode 100644 index 0000000..54b0dbb --- /dev/null +++ b/proftpd_test_2/proftpd.conf @@ -0,0 +1,31 @@ +# ONLY authenticate with pam, but sql provides the user information lookup. +AuthOrder mod_auth_pam.c* mod_sql.c + +AuthPAM on +# configures which file in /etc/pam.d is used for proftpd access check +AuthPAMConfig ftp-upload + +# /etc/pam.d looks like this: +#auth required /home/user/projects/pam_firstcomefirstserve/pam_firstcomefirstserve.so /home/user/projects/pam_firstcomefirstserve/proftpd_test_2/auth.db /srv/testupload 65534 65534 +#account required pam_permit.so +#password required pam_permit.so +#session required pam_permit.so + +# pam_firstcomefirstserve.so manages the passwords, and creates home directories with the specified uid/gid. + +# PAM doesn't look up user information, so SQL is configured for that. + +SQLAuthenticate users +SQLBackend sqlite +#It can be any sqlite database since we don't use any data. +SQLConnectInfo /home/user/projects/pam_firstcomefirstserve/proftpd_test_2/auth.db +SQLEngine auth +SQLUserInfo custom:/get-user-by-name +# Even though we return password 'hunter2', login with this password fails because the * AuthOrder means that PAM auth has to succeed. +SQLNamedQuery get-user-by-name select "'%U','hunter2',65534,65534,'/srv/testupload/%U','/bin/sh'" + +# Restrict users to their home directories. This is the only thing that stops users accessing each others' files. +DefaultRoot ~ + +#Trace auth:10 auth.pam:10 +#TraceLog /dev/tty diff --git a/sshd_test/ftp-upload b/sshd_test/ftp-upload new file mode 120000 index 0000000..cf9865a --- /dev/null +++ b/sshd_test/ftp-upload @@ -0,0 +1 @@ +/usr/sbin/sshd \ No newline at end of file diff --git a/sshd_test/pam-service-name-note b/sshd_test/pam-service-name-note new file mode 100644 index 0000000..7db2c86 --- /dev/null +++ b/sshd_test/pam-service-name-note @@ -0,0 +1 @@ +pam service name is name of sshd binary, so we provide a symlink. sshd must run as root to provide pam. diff --git a/sshd_test/sshd_command b/sshd_test/sshd_command new file mode 100644 index 0000000..4ca2249 --- /dev/null +++ b/sshd_test/sshd_command @@ -0,0 +1,2 @@ +sudo killall ftp-upload +sudo $PWD/ftp-upload -f sshd_config -e -D -d diff --git a/sshd_test/sshd_config b/sshd_test/sshd_config new file mode 100644 index 0000000..0a8c7fb --- /dev/null +++ b/sshd_test/sshd_config @@ -0,0 +1,19 @@ +Port 5022 +ListenAddress 127.0.0.1 +ListenAddress ::1 + +HostKey /home/user/projects/pam_firstcomefirstserve/sshd_test/ssh_host_ed25519_key +# with PAM doesn't kbd-interactive follow the design better? we see if password can work anyway +PasswordAuthentication yes +PermitEmptyPasswords no +KbdInteractiveAuthentication no +AuthenticationMethods password +UsePAM yes + +Subsystem sftp internal-sftp +#ForceCommand /usr/lib64/misc/sftp-server -d /%u +ChrootDirectory /srv/testupload +X11Forwarding no +AllowTcpForwarding no +AllowAgentForwarding no +ForceCommand internal-sftp -d /%u diff --git a/vsftpd_test/vsftpd.conf b/vsftpd_test/vsftpd.conf new file mode 100644 index 0000000..e378eaf --- /dev/null +++ b/vsftpd_test/vsftpd.conf @@ -0,0 +1,25 @@ +anonymous_enable=NO +local_enable=YES +write_enable=YES + +# Activate logging of uploads/downloads. +xferlog_enable=YES +xferlog_file=/dev/stderr + +# It is recommended that you define on your system a unique user which the +# ftp server can use as a totally isolated and unprivileged user. +#nopriv_user=ftpsecure + +ftpd_banner=test ftp service for pam_firstcomefirstserve + +# When "listen" directive is enabled, vsftpd runs in standalone mode and +# listens on IPv4 sockets. This directive cannot be used in conjunction +# with the listen_ipv6 directive. +listen=YES +# +# This directive enables listening on IPv6 sockets. To listen on IPv4 and IPv6 +# sockets, you must run two copies of vsftpd with two configuration files. +# Make sure, that one of the listen options is commented !! +#listen_ipv6=YES + +#pam_service_name=ftp-upload \ No newline at end of file