CMakeLists.txt000664 000000 000000 00000004635 12207732173 013612 0ustar00rootroot000000 000000 cmake_minimum_required (VERSION 2.6) set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} "${CMAKE_SOURCE_DIR}/cmake/Modules/") project(pam-abl) set(PAM_ABL_COMMON_SRC config.c dbfun.c log.c pam_abl.c rule.c typefun.c ) set(PAM_ABL_TOOLS_SRC tools.c ) set(PAM_ABL_LIB_SRC pam_functions.c ) set(PAM_ABL_TEST_SRC test_abl.c test.c test_db.c test_rule.c test_types.c test_config.c ) find_package(BerkeleyDB REQUIRED) find_package(Pam REQUIRED) #Building pam-abl using a different version of Berkeley db. #If you have only one version of Berkeley db installed, it should normally work without any changes. #If you have multiple versions installed, you can specify the version to use by providing the params #on the commandline when calling cmake: # -DDB_LIBRARY= # -DDB_INCLUDE_DIR= # -DDB_LINK_DIR= # #for example: cmake -DDB_INCLUDE_DIR=/db-5.3.15/include/ -DDB_LINK_DIR=/db-5.3.15/lib/ -DDB_LIBRARY=db-5.3 #or it can be as easy as: cmake -DDB_LIBRARY=db-4.7 ../ if (DEFINED DB_LINK_DIR) link_directories(${DB_LINK_DIR}) endif(DEFINED DB_LINK_DIR) #if you want a debug build, please add "-DCMAKE_BUILD_TYPE=Debug" as param to the cmake call if( NOT CMAKE_BUILD_TYPE) set( CMAKE_BUILD_TYPE Release) endif( NOT CMAKE_BUILD_TYPE) include_directories(${DB_INCLUDE_DIR} ${PAM_INCLUDE_DIRS}) add_definitions(-W -Wall -Wshadow -Winit-self -Wredundant-decls -Wcast-align -Wfloat-equal -Winline -Wunreachable-code -Wmissing-declarations -Wswitch-enum -Wswitch-default -Wformat -Wmain -Wextra -Wunused -Wmissing-noreturn) set(CMAKE_SHARED_LIBRARY_PREFIX "") add_executable(pam-abl_bin ${PAM_ABL_COMMON_SRC} ${PAM_ABL_TOOLS_SRC}) set_target_properties(pam-abl_bin PROPERTIES OUTPUT_NAME pam_abl) set_target_properties(pam-abl_bin PROPERTIES COMPILE_DEFINITIONS "TOOLS") target_link_libraries(pam-abl_bin ${DB_LIBRARY} ) add_executable(pam-abl_test ${PAM_ABL_COMMON_SRC} ${PAM_ABL_TEST_SRC}) set_target_properties(pam-abl_test PROPERTIES OUTPUT_NAME pam_abl_test) set_target_properties(pam-abl_test PROPERTIES COMPILE_DEFINITIONS "TEST") target_link_libraries(pam-abl_test ${DB_LIBRARY} ) add_library(pam-abl_lib SHARED ${PAM_ABL_COMMON_SRC} ${PAM_ABL_LIB_SRC}) set_target_properties(pam-abl_lib PROPERTIES OUTPUT_NAME pam_abl) target_link_libraries(pam-abl_lib ${DB_LIBRARY} ${PAM_LIBRARY}) INSTALL(TARGETS pam-abl_bin RUNTIME DESTINATION bin ) INSTALL(TARGETS pam-abl_lib DESTINATION lib/security) Changelog.txt000664 000000 000000 00000007113 12207732173 013474 0ustar00rootroot000000 000000 pam-abl is a pam module designed to automatically block hosts which are attempting a brute force attack. Building Pam-abl doesn't come with a normal makefile, mostly because I find them to complicated for what they need to do. That's why I decided to go with cmake. If you are handy with the autotools framework, feel free to write some scripts and I will be happy to include them. Changelog: 0.6.0 Bugfix release for the 0.5.0 release. But because there are some incompatibilities between this version and 0.5.0 we increased the version number. Incompatibilities: - target is named pam_abl again - syntax for specifying the commands to run has changed (please see the manpages for more details) Fixes: - BUG #33 test.c fails to compile on hurd-i386 (thanx to Alex Mestiashvili for reporting this bug) - create a copy of the user/host/service (reported by Dan) - Rename targets to pam_abl (sorry for the name mixup in previous release) - automatically cleanup db log files - security fix, do not run commands using system(). Please see the manpages for more details. - Sourceforge bug #3564436 second authentication succeeds 0.5.0 This version is a total rewrite of the previous versions. I guess +-400 lines are unchanged (mostly dealing with parsing the config file). The total rewrite results in a lot more stability and some extra features (Please see the manpages in the doc directory for more details). - the two db tables have been merged making db locking easier - added a block reason, making troubleshooting easier - introduce limits in the db, preventing a hacker from filling up the machine - white listing support for users, computers and ip ranges - fixed the custom commands, they were a little bit buggy in previous versions - and a lot more 0.4.3.1 Fix a segfault when no user_rule is provided (Sourceforge bug #3556423) 0.4.3 Fix printf problem on 32bit machines, as reported by Petr Písař and antpu on Sourceforge bug #3472603. Fixed cursor close bug. (Sourceforge #3468220) Fixed pointer alias problem. (Sourceforge bug #3469379) Incorporate the very helpful patches from Lode (a.k.a. danta on sourceforge) which add transactions to the databases to prevent corruption with concurrent access, change the script calling behavior and clean up some resources. (Sourceforge bug #3469409) 0.4.2 Streamlined database handling to reduce the number of times the databases need to be opened. Fixed a bug with host blocking. Added a patch from sourceforge to fix long options for the command line tool. Fixed some reported typos. Fixed automake to actually use the -Wall and -Werror options. They were in the wrong macro before. Fixed compiler warnings. 0.4.1 Fixed a bad typo that caused the PAM portion to always fail people. Clarified some documentation, updated examples and fixed some typos. 0.4.0 ***THIS VERSION IS NOT COMPATIBLE WITH YOUR OLD DATABASE FILES*** If you get strange errors when running pam_abl about invalid arguments, you will need to either delete or move your old database files, and let them be recreated. Changed the command line interface to allow non-pam interaction. You can now fail, whitelist (unblock), and check hosts and users from the command line. Added the ability to run commands with parameter substitution when a host or user is checked and they change state from the last time they were checked. Fixed a bug that kept databases from being created if they didn't exist. Changed the host checking to check against the host name. This was checking the user name against the times stored for the host in the host database. 0.3.0 Added autotools support. Added man page documentation. README000664 000000 000000 00000003742 12207732173 011730 0ustar00rootroot000000 000000 Welcome to pam-abl. 1) Introduction 2) Building 3) Supported databases 1) Introduction pam_abl is a pam module designed to automatically block hosts which are attempting a brute force attack. Brute force attacks are an unsophisticated way to find authentication credentials. Basically, a computer is setup to try all kinds of user names and password combinations until one works. It may sound fairly far fetched, but it does actually work. Many system accounts have common user names. Passwords are also easily guessable in many situations. The latest version is 0.6.0 This version is a bugfix release on the total rewrite introduced in 0.5.0 Please see the Changelog for more details 2) Building As you may have noticed pam-abl doesn't come with a normal makefile, mostly because I find them to complicated for what they need to do. That's why I decided to go with cmake. CMake is a family of tools designed to build, test and package software. You can download your copy from their website (http://www.cmake.org/). Eventually the idea is to build packages for most common distributions using cmake. So what do you need to do to build this version. a. Pick yourself a build directory. It can be the directory with the sources. mkdir cmake_build b. cd to that dir cd cmake_build c. call cmake with as argument the top level project directory. This will generate a makefile. cmake ../ d. call make make The build process will make 3 targets: a. The library "pam-abl.so" b. The commandline tool "pam-abl" c. A test executable "pam-abl_test". Really handy for testing it with different db versions !!!!!!!!!! !!! Building using different version of Berkeley db. !!! See the build file "CMakeLists.txt" for more details. !!!!!!!!!! 3) Supported databases Currently pam-abl is tested with the following versions of Berkeley db: - db-5.3.15 - db-5.2.42 - db-5.1.25 - db-4.8.30 - db-4.7.25 - db-4.6.21 - db-4.5.20 - db-4.4.20 Versions <= 4.3 are currently no longer supported. cmake/000775 000000 000000 00000000000 12207732173 012122 5ustar00rootroot000000 000000 cmake/Modules/000775 000000 000000 00000000000 12207732173 013532 5ustar00rootroot000000 000000 cmake/Modules/FindBerkeleyDB.cmake000664 000000 000000 00000001031 12207732173 017300 0ustar00rootroot000000 000000 # -*- cmake -*- set(DB_FIND_QUIETLY ON) set(DB_FIND_REQUIRED ON) if (NOT DB_INCLUDE_DIR) find_path(DB_INCLUDE_DIR NAMES db.h PATHS /usr/include /usr/local/include /opt/local/include /sw/include ) endif (NOT DB_INCLUDE_DIR) if (NOT DB_LIBRARY) find_library(DB_LIBRARY NAMES db PATHS /usr/lib /usr/local/lib /opt/local/lib /sw/lib ) endif (NOT DB_LIBRARY) if (NOT DB_LIBRARY) message(FATAL_ERROR "Could not find Berkeley DB") endif (NOT DB_LIBRARY) cmake/Modules/FindPam.cmake000664 000000 000000 00000004437 12207732173 016062 0ustar00rootroot000000 000000 # - Try to find PAM # Once done this will define # # PAM_FOUND - system has PAM # PAM_INCLUDE_DIRS - the PAM include directory # PAM_LIBRARIES - Link these to use PAM # PAM_DEFINITIONS - Compiler switches required for using PAM # # Copyright (c) 2008 Andreas Schneider # # Redistribution and use is allowed according to the terms of the New # BSD license. # For details see the accompanying COPYING-CMAKE-SCRIPTS file. # if (PAM_LIBRARIES AND PAM_INCLUDE_DIRS) # in cache already set(PAM_FOUND TRUE) else (PAM_LIBRARIES AND PAM_INCLUDE_DIRS) find_path(PAM_INCLUDE_DIR NAMES security/pam_modules.h PATHS /usr/include /usr/local/include /opt/local/include /sw/include ) find_library(PAM_LIBRARY NAMES pam PATHS /usr/lib /usr/local/lib /opt/local/lib /sw/lib ) find_library(PAM_MISC_LIBRARY NAMES pam_misc PATHS /usr/lib /usr/local/lib /opt/local/lib /sw/lib ) find_library(PAMC_LIBRARY NAMES pamc PATHS /usr/lib /usr/local/lib /opt/local/lib /sw/lib ) if (PAM_LIBRARY) set(PAM_FOUND TRUE) endif (PAM_LIBRARY) if (PAM_MISC_LIBRARY) set(PAM_MISC_FOUND TRUE) endif (PAM_MISC_LIBRARY) if (PAMC_LIBRARY) set(PAMC_FOUND TRUE) endif (PAMC_LIBRARY) set(PAM_INCLUDE_DIRS ${PAM_INCLUDE_DIR} ) if (PAM_FOUND) set(PAM_LIBRARIES ${PAM_LIBRARIES} ${PAM_LIBRARY} ) endif (PAM_FOUND) if (PAM_MISC_FOUND) set(PAM_LIBRARIES ${PAM_LIBRARIES} ${PAM_MISC_LIBRARY} ) endif (PAM_MISC_FOUND) if (PAMC_FOUND) set(PAM_LIBRARIES ${PAM_LIBRARIES} ${PAMC_LIBRARY} ) endif (PAMC_FOUND) if (PAM_INCLUDE_DIRS AND PAM_LIBRARIES) set(PAM_FOUND TRUE) endif (PAM_INCLUDE_DIRS AND PAM_LIBRARIES) if (PAM_FOUND) if (NOT PAM_FIND_QUIETLY) message(STATUS "Found PAM: ${PAM_LIBRARIES}") endif (NOT PAM_FIND_QUIETLY) else (PAM_FOUND) if (PAM_FIND_REQUIRED) message(FATAL_ERROR "Could not find PAM") endif (PAM_FIND_REQUIRED) endif (PAM_FOUND) # show the PAM_INCLUDE_DIRS and PAM_LIBRARIES variables only in the advanced view mark_as_advanced(PAM_INCLUDE_DIRS PAM_LIBRARIES) endif (PAM_LIBRARIES AND PAM_INCLUDE_DIRS) conf/000775 000000 000000 00000000000 12207732173 011767 5ustar00rootroot000000 000000 conf/pam_abl.conf000664 000000 000000 00000000616 12207732173 014234 0ustar00rootroot000000 000000 db_home=/var/lib/abl host_db=/var/lib/abl/hosts.db host_purge=1d host_rule=*:30/1h user_db=/var/lib/abl/users.db user_purge=1d user_rule=*:3/1h host_clear_cmd=[logger] [clear] [host] [%h] host_block_cmd=[logger] [block] [host] [%h] user_clear_cmd=[logger] [clear] [user] [%u] user_block_cmd=[logger] [block] [user] [%u] limits=1000-1200 host_whitelist=1.1.1.1/24;2.1.1.1 user_whitelist=danta;chris config.c000664 000000 000000 00000032516 12207732173 012462 0ustar00rootroot000000 000000 /* * pam_abl - a PAM module and program for automatic blacklisting of hosts and users * * Copyright (C) 2005-2012 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "config.h" #include "rule.h" #include #include #include #include #include #include #define HOURSECS (60 * 60) /* Host purge time in seconds */ #define HOST_PURGE (HOURSECS * 24) /* User purge time in seconds */ #define USER_PURGE (HOURSECS * 24) static size_t config_match(const char *pattern, const char *target, size_t len) { return len == strlen(pattern) && memcmp(pattern, target, len) == 0; } /* Check an arg string of the form arg=something and return either * NULL if the arg doesn't match the supplied name or * a pointer to 'something' if the arg matches */ static const char *is_arg(const char *name, const char *arg) { char *eq; if (eq = strchr(arg, '='), NULL == eq) { return NULL; } if (!config_match(name, arg, (size_t)(eq - arg))) { return NULL; } eq++; /* skip '=' */ while (*eq != '\0' && isspace(*eq)) { /* skip spaces */ eq++; } return eq; } static void config_clear(abl_args *args) { /* Init the args structure */ args->db_home = NULL; args->host_db = NULL; args->host_rule = NULL; args->host_purge = HOST_PURGE; args->host_whitelist = NULL; args->host_blk_cmd = NULL; args->host_clr_cmd = NULL; args->user_db = NULL; args->user_rule = NULL; args->user_purge = USER_PURGE; args->user_whitelist = NULL; args->user_blk_cmd = NULL; args->user_clr_cmd = NULL; args->upperlimit = 0; args->lowerlimit = 0; args->strs = NULL; } abl_args *config_create() { abl_args *retValue = malloc(sizeof(abl_args)); if (retValue) config_clear(retValue); return retValue; } static int parse_arg(const char *arg, abl_args *args, log_context *logContext) { const char *v; int err; if (0 == strcmp(arg, "debug")) { logContext->debug = 1; } else if (v = is_arg("db_home", arg), NULL != v) { args->db_home = v; } else if (v = is_arg("limits", arg), NULL != v) { long upper = 0; long lower = 0; long val = 0; short error = 1; const char *ptr = v; if (parse_long(&ptr, &val) == 0) { lower = val; if (*ptr == '-') { ++ptr; if (parse_long(&ptr, &val) == 0) { upper = val; if (upper >= 0 && lower >= 0 && upper >= lower) error = 0; } } } if (error) { log_warning(logContext, "limits needs to have the following syntax: - with upper > lower."); args->upperlimit = 0; args->lowerlimit = 0; } else { args->upperlimit = upper; args->lowerlimit = lower; } } else if (v = is_arg("host_db", arg), NULL != v) { args->host_db = v; } else if (v = is_arg("host_rule", arg), NULL != v) { args->host_rule = v; } else if (v = is_arg("host_purge", arg), NULL != v) { if (err = rule_parse_time(v, &args->host_purge, HOURSECS), 0 != err) { log_error(logContext, "Illegal host_purge value: %s", v); } } else if (v = is_arg("host_blk_cmd", arg), NULL != v) { log_error(logContext, "host_blk_cmd is deprecated for security reasons, please use host_block_cmd."); } else if (v = is_arg("host_clr_cmd", arg), NULL != v) { log_error(logContext, "host_clr_cmd is deprecated for security reasons, please use host_clear_cmd."); } else if (v = is_arg("host_block_cmd", arg), NULL != v) { //we will check if it's valid when we try to run it args->host_blk_cmd = v; } else if (v = is_arg("host_clear_cmd", arg), NULL != v) { //we will check if it's valid when we try to run it args->host_clr_cmd = v; } else if (v = is_arg("host_whitelist", arg), NULL != v) { args->host_whitelist = v; } else if (v = is_arg("user_db", arg), NULL != v) { args->user_db = v; } else if (v = is_arg("user_rule", arg), NULL != v) { args->user_rule = v; } else if (v = is_arg("user_purge", arg), NULL != v) { if (err = rule_parse_time(v, &args->user_purge, HOURSECS), 0 != err) { log_error(logContext, "Illegal user_purge value: %s", v); } } else if (v = is_arg("user_blk_cmd", arg), NULL != v) { log_error(logContext, "user_blk_cmd is deprecated for security reasons, please use user_block_cmd."); } else if (v = is_arg("user_clr_cmd", arg), NULL != v) { log_error(logContext, "user_clr_cmd is deprecated for security reasons, please use user_clear_cmd."); } else if (v = is_arg("user_block_cmd", arg), NULL != v) { //we will check if it's valid when we try to run it args->user_blk_cmd = v; } else if (v = is_arg("user_clear_cmd", arg), NULL != v) { //we will check if it's valid when we try to run it args->user_clr_cmd = v; } else if (v = is_arg("user_whitelist", arg), NULL != v) { args->user_whitelist = v; } else if (v = is_arg("config", arg), NULL != v) { config_parse_file(v, args, logContext); } else { log_error(logContext, "Illegal option: %s", arg); return EINVAL; } return 0; } struct linebuf { char *buf; int len; int size; }; struct reader { FILE *f; int lc; }; static int ensure(log_context *logContext, struct linebuf *b, int minfree) { if (b->size - b->len < minfree) { char *nb; int ns; if (minfree < 128) { minfree = 128; } ns = b->len + minfree; nb = realloc(b->buf, ns); if (NULL == nb) { log_sys_error(logContext, ENOMEM, "parsing config file"); return ENOMEM; } b->size = ns; b->buf = nb; /*log_debug(args, "Line buffer grown to %d", b->size);*/ } return 0; } static int readc(struct reader *r) { int nc; for (;;) { nc = r->lc; r->lc = (nc == EOF) ? EOF : getc(r->f); /* Handle line continuation */ if (nc != '\\' || r->lc != '\n') { return nc; } /* No need for EOF check here */ r->lc = getc(r->f); } } static int read_line(log_context *logContext, struct linebuf *b, struct reader *r) { int c, err; c = readc(r); b->len = 0; while (c != '\n' && c != EOF && c != '#') { while (c != '\n' && c != EOF && isspace(c)) { c = readc(r); } while (c != '\n' && c != EOF && c != '#') { if (err = ensure(logContext, b, 1), 0 != err) { return err; } b->buf[b->len++] = c; c = readc(r); } } while (c != '\n' && c != EOF) { c = readc(r); } /* Trim trailing spaces from line */ while (b->len > 0 && isspace(b->buf[b->len-1])) { b->len--; } ensure(logContext, b, 1); b->buf[b->len++] = '\0'; return 0; } static const char *dups(abl_args *args, const char *s) { int l = strlen(s); abl_string *str = malloc(sizeof(abl_string) + l + 1); memcpy(str + 1, s, l + 1); str->link = args->strs; args->strs = str; return (const char *) (str + 1); } /* Parse the contents of a config file */ int config_parse_file(const char *name, abl_args *args, log_context *logContext) { struct linebuf b; struct reader r; int err = 0; const char *l; b.buf = NULL; b.len = 0; b.size = 0; if (r.f = fopen(name, "r"), NULL == r.f) { err = errno; goto done; } r.lc = getc(r.f); while (r.lc != EOF) { if (err = read_line(logContext, &b, &r), 0 != err) { goto done; } if (b.len > 1) { if (l = dups(args, b.buf), NULL == l) { err = ENOMEM; goto done; } log_debug(logContext, "%s: %s", name, l); if (err = parse_arg(l, args, logContext), 0 != err) { goto done; } } } done: if (0 != err) { log_sys_error(logContext, err, "reading config file"); } if (err == 0) { if (!args->db_home) { err = ENOENT; log_sys_error(logContext, err, "reading config file: No db_home dir specified"); } else { struct stat sb; if (!(stat(args->db_home, &sb) == 0 && S_ISDIR(sb.st_mode))) { err = ENOTDIR; log_sys_error(logContext, err, "parsing the value of db_home"); } } } if (NULL != r.f) { fclose(r.f); } free(b.buf); return err; } void dump_args(const abl_args *args, log_context *logContext) { abl_string *s; log_debug(logContext, "debug = %d", logContext->debug); log_debug(logContext, "db_home = %s", args->db_home); log_debug(logContext, "host_db = %s", args->host_db); log_debug(logContext, "host_rule = %s", args->host_rule); log_debug(logContext, "host_purge = %ld", args->host_purge); log_debug(logContext, "host_block_cmd = %s", args->host_blk_cmd); log_debug(logContext, "host_clear_cmd = %s", args->host_clr_cmd); log_debug(logContext, "user_db = %s", args->user_db); log_debug(logContext, "user_rule = %s", args->user_rule); log_debug(logContext, "user_purge = %ld", args->user_purge); log_debug(logContext, "user_block_cmd = %s", args->user_blk_cmd); log_debug(logContext, "user_clear_cmd = %s", args->user_clr_cmd); log_debug(logContext, "lower limit = %ld", args->lowerlimit); log_debug(logContext, "upper limit = %ld", args->upperlimit); for (s = args->strs; NULL != s; s = s->link) { log_debug(logContext, "str[%p] = %s", s, (char *) (s + 1)); } } /* Parse our argments and populate an abl_args structure accordingly. */ int config_parse_args(int argc, const char **argv, abl_args *args, log_context *logContext) { int argn; int err; config_clear(args); for (argn = 0; argn < argc; argn++) { err = parse_arg(argv[argn], args, logContext); if (err) { return err; } } if (logContext->debug) dump_args(args, logContext); return 0; } /* Destroy any storage allocated by args */ void config_free(abl_args *args) { abl_string *s, *next; for (s = args->strs; s != NULL; s = next) { next = s->link; free(s); } args->strs = NULL; free(args); } int splitCommand(char *command, char* result[], log_context *logContext) { if (command == NULL) return 0; int nofParts = 0; int partStarted = 0; unsigned long inputIndex = 0; unsigned long outputIndex = 0; int lastCharWasEscape = 0; while (command[inputIndex]) { if (lastCharWasEscape) { lastCharWasEscape = 0; } else { switch(command[inputIndex]) { case '\\': lastCharWasEscape = 1; //only increase the inputIndex, do not copy the char ++inputIndex; continue; case '[': if (partStarted) { if (logContext) log_error(logContext, "command syntax error: parsed '[' while already parsing a part in \"%s\"", command); return -1; } if (result) result[nofParts] = command+outputIndex+1; ++nofParts; partStarted = 1; break; case ']': if (!partStarted) { if (logContext) log_error(logContext, "command syntax error: parsed ']' without opening '[' in \"%s\"", command); return -1; } partStarted = 0; if (result) command[inputIndex] = '\0'; //use inputIndex here so that the copy at the end of the while is correct break; default: break; }; } if (result) command[outputIndex] = command[inputIndex]; ++outputIndex; ++inputIndex; } //syntax error, we didn't see a final ']' if (partStarted) { if (logContext) { log_error(logContext, "command syntax error: no closing ] in \"%s\"", command); } return -1; } return nofParts; } config.h000664 000000 000000 00000006174 12207732173 012470 0ustar00rootroot000000 000000 /* * pam_abl - a PAM module and program for automatic blacklisting of hosts and users * * Copyright (C) 2005-2012 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #ifndef CONFIG_H #define CONFIG_H #include "log.h" typedef struct abl_string { struct abl_string *link; } abl_string; typedef struct { /* Our args */ const char *db_home; const char *host_db; const char *host_rule; long host_purge; const char *host_whitelist; const char *host_blk_cmd; const char *host_clr_cmd; const char *user_db; const char *user_rule; long user_purge; const char *user_whitelist; const char *user_blk_cmd; const char *user_clr_cmd; unsigned int upperlimit; unsigned int lowerlimit; /* Storage */ abl_string *strs; } abl_args; abl_args *config_create(); void config_free(abl_args *args); int config_parse_args(int argc, const char **argv, abl_args *args, log_context *logContext); int config_parse_file(const char *name, abl_args *args, log_context *logContext); void dump_args(const abl_args *args, log_context *logContext); /* * Split a command based on what's between brackets, strings not in brackets are ignored * \param command: the command to split. The command will be split to all fields between '[]'. If you want a '[' or ']' in your command, precede it by a '\' If you want a '\', precede it by another '\': '\\' => '\' All other escape chars are resolved to the char itself: '\c' => 'c' * \param result: if this value is not NULL, this array will be filled with pointers to the different parts. * The pointers point to memory in the original command. * Make sure it's big enough * \param logContext: if not NULL this will be used to log syntax errors. * \return: negative if there is a syntax error, otherwise the number of parts * NOTE: command will be modified if result != NULL, \0 will be inserted and escape chars are removed if followed by [ or ] * NOTE: syntax error message can be very cryptic if result != NULL => check with result == NULL for syntax errors first * example: cmd = "lol [command] ignored [arg1] [arg2]" will result in the following result: - result[0] = "command" - result[1] = "arg1" - result[2] = "arg2" with as return value 3 */ int splitCommand(char *command, char* result[], log_context *logContext); #endif dbfun.c000664 000000 000000 00000017325 12207732173 012314 0ustar00rootroot000000 000000 /* * pam_abl - a PAM module and program for automatic blacklisting of hosts and users * * Copyright (C) 2005-2012 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "dbfun.h" #include #include #define DBPERM 0600 //do a checkpoint every 8MB of log #define CHECKPOINTSIZE (8000) //let's allocate a 'large' buffer char largeBuffer[1024*50]; int createEnvironment(log_context *context, const char *home, DbEnvironment **env) { int ret = 0; *env = NULL; DB_ENV *dbenv = NULL; if ((ret = db_env_create(&dbenv, 0)) != 0) { log_db_error(context, ret, "creating environment object"); return ret; } dbenv->set_errpfx(dbenv, "pam-abl"); if ((ret = dbenv->open(dbenv, home, DB_CREATE | DB_INIT_TXN | DB_INIT_LOCK | DB_INIT_MPOOL | DB_RECOVER | DB_REGISTER, 0)) != 0) { log_db_error(context, ret, "opening the database environment"); dbenv->close(dbenv, 0); dbenv = 0; } if (dbenv) { /* Do deadlock detection internally. */ if ((ret = dbenv->set_lk_detect(dbenv, DB_LOCK_DEFAULT)) != 0) { log_db_error(context, ret, "setting lock detection."); } #if ((DB_VERSION_MAJOR >= 5)||(DB_VERSION_MAJOR == 4 && DB_VERSION_MINOR >= 7)) ret = dbenv->log_set_config(dbenv, DB_LOG_AUTO_REMOVE, 1); #else ret = dbenv->set_flags(dbenv, DB_LOG_AUTOREMOVE, 1); #endif if (ret != 0) { log_db_error(context, ret, "setting automatic log file removal."); } if ((ret = dbenv->txn_checkpoint(dbenv, CHECKPOINTSIZE, 0, 0)) != 0) { log_db_error(context, ret, "setting the automatic checkpoint option."); } } if (dbenv) { DbEnvironment *retValue = malloc(sizeof(DbEnvironment)); memset(retValue, 0, sizeof(DbEnvironment)); retValue->m_envHandle = dbenv; retValue->m_logContext = context; retValue->m_transaction = NULL; *env = retValue; } return ret; } void destroyEnvironment(DbEnvironment *env) { if (!env) return; if (env->m_envHandle) env->m_envHandle->close(env->m_envHandle, 0); env->m_envHandle = NULL; free(env); } int startTransaction(DbEnvironment *env) { if (!env || !env->m_envHandle) return 1; //for the moment we only support one transaction at the time if (env->m_transaction) return 0; DB_TXN *tid = NULL; int err = 0; if ((err = env->m_envHandle->txn_begin(env->m_envHandle, NULL, &tid, 0)) != 0) { log_db_error(env->m_logContext, err, "starting transaction"); return err; } env->m_transaction = tid; return err; } int commitTransaction(DbEnvironment *env) { if (!env || !env->m_envHandle) return 1; //if we are not in a transaction, just ignore it if (!env->m_transaction) return 0; int err = env->m_transaction->commit(env->m_transaction, 0); env->m_transaction = NULL; return err; } int abortTransaction(DbEnvironment *env) { if (!env || !env->m_envHandle) return 1; //if we are not in a transaction, just ignore it if (!env->m_transaction) return 0; int err = env->m_transaction->abort(env->m_transaction); env->m_transaction = NULL; return err; } int openDatabase(DbEnvironment *env, const char *dbfile, const char *dbname, Database **db) { if (!env || !env->m_envHandle) return 1; *db = NULL; int err = 0; DB *dbHandle = NULL; if (err = db_create(&dbHandle, env->m_envHandle, 0), 0 != err) { log_db_error(env->m_logContext, err, "creating database object"); return err; } DB_TXN *tid = 0; if ((err = env->m_envHandle->txn_begin(env->m_envHandle, NULL, &tid, 0)) != 0) { log_db_error(env->m_logContext, err, "starting transaction"); return err; } if ((err = dbHandle->open(dbHandle, tid, dbfile, dbname, DB_BTREE, DB_CREATE, DBPERM)) != 0) { log_db_error(env->m_logContext, err, "opening or creating database"); tid->abort(tid); return err; } if ((err = tid->commit(tid, 0))) { log_db_error(env->m_logContext, err, "committing transaction"); return err; } log_debug(env->m_logContext, "%s opened", dbname); Database *retValue = malloc(sizeof(Database)); memset(retValue, 0, sizeof(Database)); retValue->m_dbHandle = dbHandle; retValue->m_environment = env; *db = retValue; return 0; } void closeDatabase(Database *db) { if (!db) return; if (db->m_dbHandle) db->m_dbHandle->close(db->m_dbHandle,0); db->m_dbHandle = NULL; free(db); } int getUserOrHostInfo(Database *db, const char *hostOrUser, AuthState **hostOrUserState) { *hostOrUserState = NULL; if (!db || !db->m_environment || !db->m_dbHandle || !hostOrUser) return 1; int err = 0; void *allocData = NULL; DBT dbtdata; memset(&dbtdata, 0, sizeof(DBT)); dbtdata.flags = DB_DBT_USERMEM; dbtdata.data = &largeBuffer[0]; dbtdata.ulen = sizeof(largeBuffer); DBT key; memset(&key, 0, sizeof(DBT)); key.data = (void*)hostOrUser; key.size = strlen(hostOrUser); DB_TXN *tid = db->m_environment->m_transaction; err = db->m_dbHandle->get(db->m_dbHandle, tid, &key, &dbtdata, DB_RMW); /*Called with DB_DBT_USERMEM? What was there wasn't enough*/ if (err == DB_BUFFER_SMALL) { allocData = malloc(dbtdata.size); if (!allocData) return 1; dbtdata.data = allocData; dbtdata.ulen = dbtdata.size; dbtdata.size = 0; /* ...and try again. */ err = db->m_dbHandle->get(db->m_dbHandle, tid, &key, &dbtdata, 0600); } if (err != 0 && err != DB_NOTFOUND) { db->m_dbHandle->err(db->m_dbHandle, err, "DB->get"); if (allocData) free(allocData); return err; } if (err == DB_NOTFOUND) { //it was not in the db, just report no error and don't fill in the state if (allocData) free(allocData); return 0; } err = createAuthState(dbtdata.data, dbtdata.size, hostOrUserState); if (allocData) free(allocData); return err; } int saveInfo(Database *db, const char *hostOrUser, AuthState *hostOrUserState) { if (!db || !db->m_environment || !db->m_dbHandle || !hostOrUser || !*hostOrUser || !hostOrUserState) return 1; DB_TXN *tid = db->m_environment->m_transaction; DBT key, data; memset(&key, 0, sizeof(DBT)); memset(&data, 0, sizeof(DBT)); key.data = (void*)hostOrUser; key.size = strlen(hostOrUser); data.data = hostOrUserState->m_data; data.size = hostOrUserState->m_usedSize; int err = db->m_dbHandle->put(db->m_dbHandle, tid, &key, &data, 0); return err; } int removeInfo(Database *db, const char *hostOrUser) { if (!db || !db->m_environment || !db->m_dbHandle || !hostOrUser || !*hostOrUser) return 1; DB_TXN *tid = db->m_environment->m_transaction; DBT key; memset(&key, 0, sizeof(key)); key.data = (void*)hostOrUser; key.size = strlen(hostOrUser); int err = db->m_dbHandle->del(db->m_dbHandle, tid, &key, 0); return err; } dbfun.h000664 000000 000000 00000010666 12207732173 012322 0ustar00rootroot000000 000000 /* * pam_abl - a PAM module and program for automatic blacklisting of hosts and users * * Copyright (C) 2005-2012 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #ifndef DBFUN_H #define DBFUN_H #include "typefun.h" #include "log.h" #include typedef struct DbEnvironment { DB_ENV *m_envHandle; DB_TXN *m_transaction; log_context *m_logContext; } DbEnvironment; typedef struct Database { DB *m_dbHandle; DbEnvironment *m_environment; } Database; /* Create and open the database environment \param context The logger context to be used for all db actions in this environment \param home The place where the Berkeley db can put itś locking files and such \param env The newly created environment will be returned through this pointer \return zero on success, otherwise non zero \note context can be a NULL ptr, do not delete context while the environment is still active */ int createEnvironment(log_context *context, const char *home, DbEnvironment **env); /* Cleanup the given environment, releasing all it's resources Do not use the env pointer after calling this */ void destroyEnvironment(DbEnvironment *env); /* Start a transaction on the given environment \return zero on success, otherwise non zero \note For the moment only on transaction can be active at the same time. \note ALL database actions need to be wrapped in a transaction for them to work. */ int startTransaction(DbEnvironment *env); /* End a transaction started on the environment applying all the changes \return zero on success, otherwise non zero \note calling this function on an environment with no transaction started sill succeed */ int commitTransaction(DbEnvironment *env); /* End a transaction started on the environment discarding all the changes \return zero on success, otherwise non zero \note calling this function on an environment with no transaction started sill succeed */ int abortTransaction(DbEnvironment *env); /* Open a database in the given environment. Locking occurs at environemnt level, so be sure to pass the right environment \param env The environment to open the db in. \param dbFile The file to use as storage for the db \param dbName The name of the db table \param db This param will be used to return the newly created db \return zero on success, otherwise non zero \note Opening a db will automatically be wrapped in a transaction */ int openDatabase(DbEnvironment *env, const char *dbfile, const char *dbname, Database **db); /* Close an open Database. Make sure that there are no transaction on this db. Do not use the db pointer after calling this function. */ void closeDatabase(Database *db); /* Get the authentication state for the given subject. Make sure that you already started a transaction before calling this method \param db The database to get the info from \param subject The subject to look for, this ptr has te be valid and not an empty string \subjectState Returns the state of the subject. If it is not fount subjectState will be a nullptr \return zero on success, non zero otherwise */ int getUserOrHostInfo(Database *db, const char *subject, AuthState **subjectState); /* Save the given AuthState for the given subject in the given database Make sure that you already started a transaction before calling this method \param db The database to store the value in \param subject The subject to use to store the value. This needs to be valid and not an empty string \param subjectState The state to save, this needs to be a valid state \return zero on success, non zero otherwise */ int saveInfo(Database *db, const char *subject, AuthState *subjectState); /* Remove a given subject out of the given database Make sure that you already started a transaction before calling this method */ int removeInfo(Database *db, const char *subject); #endif doc/000775 000000 000000 00000000000 12207732173 011607 5ustar00rootroot000000 000000 doc/generate.sh000775 000000 000000 00000000225 12207732173 013737 0ustar00rootroot000000 000000 #!/bin/bash if [[ "$1" == "clean" ]] then ls ./|grep -v 'Makefile\|txt\|generate'|xargs rm else for page in *.txt;do a2x -f manpage $page;done fi doc/pam_abl.1.txt000664 000000 000000 00000010036 12207732173 014102 0ustar00rootroot000000 000000 PAM_ABL(1) ========= :man source: GNU :man manual: User Commands :author: Chris Tasma NAME ---- pam_abl - query or purge the databases used by the pam_abl module. SYNOPSIS -------- pam_abl [OPTION] [CONFIG] DESCRIPTION ----------- Provides a non-pam interface to the infomration stored in the pam_abl module databases. CONFIG is the name of the pam_abl config file (default: /etc/security/pam_abl.conf). The config file is read to discover the names of the pam_abl databases, the rules that control purging of old data from them and commands to run when a user or host switches state. OPTIONS ------- MAINTENANCE ~~~~~~~~~~~ *-h, --help*:: See this message. *-d, --debugcommand*:: Print the block/clear commands split in arguments. *-p, --purge*:: Purge databases according to purge rules in config. *-r, --relative*:: Display times relative to now. *-v, --verbose*:: Verbose output. NON-PAM INTERACTION ~~~~~~~~~~~~~~~~~~~ *-f, --fail*:: Fail user or host. *-w, --whitelist*:: Perform whitelisting (remove from blacklist, does not provide immunity). *-c, --check*:: Check status. Returns non-zero if currently blocked Prints 'name: status' if verboseness is specified. If more than one host or user is given, checks only the first host/user pair. *-u, --update*:: Update the state of all users/hosts in the db. This will also cause the appropriate scripts to be called. *-s, --service*:: Operate in context of specified service. Defaults to 'none'. *-U, --user*:: Operate on user (wildcards are ok for whitelisting). *-H, --host*:: Operate on host (wildcards are ok for whitelisting). *-R, --reason*:: Only used when -f is provided (defaults to "AUTH"). Specifies why the authentication failed. Possible values are USER, HOST, BOTH, AUTH If you specified commands to run in your configuration, those commands will try to run if the host or user switches state (blocked <-> clear) since the last time it was checked. The command will only be able to run, however, if you supply enough information to fill in the substitutions in the command. For instance, if your host_clr_command uses the %s parameter, you will need to specify the service with -s in order for the command to actually run. EXAMPLES -------- Obtain a list of failed hosts and users: ********* $ pam_abl ********* Obtain a full list of failures listing times relative to now: ******************************************** $ pam_abl -rv $ pam_abl --relative --verbose ******************************************** Purge old data: ****************************** $ pam_abl -p $ pam_abl --purge ****************************** Unblock all example.com, somewhere.com hosts: *********************************************************************** $ pam_abl -w -H \*.example.com -H \*.somewhere.com *********************************************************************** Fail the host badguy.com and the user joe because the authentication failed: *********************************************************************** $ pam_abl -f -H badguy.com -U joe -R AUTH *********************************************************************** Check whether joe is currently allowed to use your neato service from somehost, running the necessary commands if he switches state: *********************************************************************** $ pam_abl -c -U joe -H somehost -s neato *********************************************************************** Because the user/host state is only updated when an attempt is made, you can manually force pam-abl to update the states and call the correct scripts: *********************************************************************** $ pam_abl -u *********************************************************************** AUTHORS ------- Lode Mertens Andy Armstrong Chris Tasma REPORTING BUGS -------------- Report bugs to or using the bugtracker on sourceforge. SEE ALSO -------- pam_abl.conf(5), pam_abl(8) doc/pam_abl.8.txt000664 000000 000000 00000016513 12207732173 014117 0ustar00rootroot000000 000000 PAM_ABL(8) ========= :man source: GNU :man manual: Linux-PAM Manual :author: Chris Tasma NAME ---- pam_abl - PAM Auto Blacklist Module SYNOPSIS -------- Provides auto blacklisting of hosts and users responsible for repeated failed authentication attempts. Generally configured so that blacklisted users still see normal login prompts but are guaranteed to fail to authenticate. This functionality is only available to services which call PAM as root. If pam_abl is called for uid != 0 it will silently succeed. DESCRIPTION ----------- Brute force password discovery attacks involve repeated attempts to authenticate against a service using a dictionary of common passwords. While it is desirable to enforce strong passwords for users this is not always possible and in cases where a weak password has been used brute force attacks can be effective. The pam_abl module monitors failed authentication attempts and automatically blacklists those hosts (and accounts) that are responsible for large numbers of failed attempts. Once a host is blacklisted it is guaranteed to fail authentication even if the correct credentials are provided. Blacklisting is triggered when the number of failed authentication attempts in a particular period of time exceeds a predefined limit. Hosts which stop attempting to authenticate will, after a period of time, be un-blacklisted. Commands can be specified which will be run when a host or user switches state from being blocked to clear or clear to blocked. See below or the pam_abl.conf(5) manpage for the details. If pam_abl is called for uid != 0 it will silently succeed. If this was not the case it would be possible for a malicious local user to poison the pam_abl data by, for example, discovering the names of the hosts from which root typically logs in and then constructing PAM authentication code to lock out root login attempts from those hosts. OPTIONS ------- [frame="none",cols="s,25d,50d",options="header"] |========================================================================= |'Name'|'Arguments'|'Description' |debug|None|Enable debug output to syslog. |expose_account|None|Ignored |no_warn|None| Disable warnings which are otherwise output to syslog. try_first_pass None Ignored |use_first_pass|None|Ignored |use_mapped_pass|None|Ignored |config|Path to the configuration file.| The configuration file contains additional arguments. In order for the pam_abl command line tool to work correctly most of the configuration should be placed in the config file rather than being provided by arguments. The format of the config file is described below. |limits|Minimum and maximum number of attempts to keep.| It's value should have the following syntax "-". If you do not block machines that do too many attempts, the db can easily become bloated. To prevent this we introduced this setting. As soon as there are a number of attempts for a user/host, the number of stored attempts is reduced to . A of 0 means no limits. Make sure that is larger then any rule specified. We recommend a value of "1000-1200". |db_home|Directory for db locking and logging files.| Path to a directory where Berkeley DB can place it's locking and logging files. Make sure this dir is writable. |host_db|Path to host database file.| Path to the Berkeley DB which is used to log the host responsible for failed authentication attempts. |host_purge|Purge time for the host database.| Defines how long failed hosts are retained in the host database. Defaults to 1 day. |host_rule|Rule for host blacklisting.| The rule (see below for format) which defines the conditions under which a failed hosts will be blackisted. |host_whitelist|Host that do not need to be tracked.| ;-seperated list of host that do not need to be tracked. You can specify single IP addresses here or use subnets. For example 1.1.1.1 or 1.1.1.1/24 |host_blk_cmd|Host block command | Deprecated for security reasons. Please use host_block_cmd |host_clr_cmd|Host clear command | Deprecated for security reasons. Please use host_clear_cmd |host_block_cmd|Host block command | Command that should be run when a host is checked, and is currently blocked. Within the command, the strings %u, %h and %s are substituted with username, host and service. Not all need to be used. Please see the manpage of pam_abl.conf for the correct syntax. |host_clear_cmd|Host clear command | Command that should be run when a host is checked, and is currently clear. Within the command, the strings %u, %h and %s are substituted with username, host and service. Not all need to be used. Please see the manpage of pam_abl.conf for the correct syntax. |user_db|Path to user database file.| Path to the Berkeley DB which is used to log the user responsible for failed authentication attempts. |user_purge|Purge time for the user database.| Defines how long failed users are retained in the user database. Defaults to 1 day. |user_rule|Rule for user blacklisting.| The rule (see below for format) which defines the conditions under which a failed users will be blackisted. |user_whitelist|Users that do not need to be tracked.| ;-seperated list of users whose attempts do not need to be recorded. This does not prevent the machine they are using from being blocked. |user_blk_cmd|User block command | Deprecated for security reasons. Please use user_block_cmd |user_clr_cmd|User clear command | Deprecated for security reasons. Please use clear_block_cmd |user_blk_cmd|User block command | Command that should be run when a user is checked, and is currently blocked. Within the command, the strings %u, %h and %s are substituted with username, host and service. Not all need to be used. |user_clr_cmd|User block command | Command that should be run when a user is checked, and is currently clear. Within the command, the strings %u, %h and %s are substituted with username, host and service. Not all need to be used. |========================================================================= USAGE ----- Typically pam_abl.so is added to the auth stack as a required module just before whatever modules actually peform authentication. Here's a fragment of the PAM config for a production server that is running pam_abl: auth required /lib/security/pam_env.so auth required /lib/security/pam_abl.so config=/etc/security/pam_abl.conf auth sufficient /lib/security/pam_unix.so likeauth nullok auth required /lib/security/pam_deny.so Although all of accepted arguments can be supplied here they will usually be placed in a separate config file and linked to using the config argument as in the above example. The pam_abl command line tool reads the external config file (/etc/security/pam_abl.conf in this case) to find the databases so in order for it work correctly an external config should be used. EXAMPLES -------- ------------------------------------- auth required /lib/security/pam_env.so auth required /lib/security/pam_abl.so config=/etc/security/pam_abl.conf auth sufficient /lib/security/pam_unix.so likeauth nullok auth required /lib/security/pam_deny.so ------------------------------------- SEE ALSO -------- pam_abl.conf(5), pam_abl(1) AUTHORS ------- Lode Mertens Andy Armstrong Chris Tasma doc/pam_abl.conf.5.txt000664 000000 000000 00000021337 12207732173 015040 0ustar00rootroot000000 000000 PAM_ABL.CONF(5) ============== :man source: GNU :man manual: Linux-PAM Manual :author: Chris Tasma NAME ---- pam_abl.conf - Configuration file for pam_abl PAM module. SYNOPSIS -------- Configuration file for both the pam_abl(8) PAM module, and the pam_abl(1) command line tool. DESCRIPTION ----------- Syntax ~~~~~~ ------------------------------- word ::= /[^\s\|\/\*]+/ name ::= word | '*' username ::= name servicename ::= name userservice ::= username | username '/' servicename namelist ::= userservice | userservice '|' namelist userspec ::= namelist | '!' namelist multiplier ::= 's' | 'm' | 'h' | 'd' number ::= /\d+/ period ::= number | number multiplier trigger ::= number '/' period triglist ::= trigger | trigger ',' triglist userclause ::= userspec ':' triglist rule ::= userclause | userclause /\s+/ rule ------------------------------------- Rule syntax ~~~~~~~~~~~ Each rule consists of a number of space separated 'user clauses'. A user clause specifies the user (and service) names to match and a set of triggers. A simple example would be ------- *:10/1h ------- which means 'block any user (*) if they are responsible for ten or more failed authentication attempts in the last hour'. In place of the '*' which matches any user a list of usernames can be supplied like this -------------------- root|dba|admin:10/1h -------------------- which means 'block the users root, dba and admin if they are responsible for ten or more failed authentication attempts in the last hour'. You can also specify a service name to match against like this -------------------- root/sshd|dba/*:3/1d -------------------- which means 'block the users root for service 'sshd' and dba for any service if they are responsible for three or more failed authentication attempts in the last day'. Finally you can specify multiple triggers like this ---------------- root:10/1h,20/1d ---------------- which means 'block the user root if they are responsible for ten or more failed attempts in the last hour or twenty or more failed attempts in the last day. Multiple rules can be provided separated by spaces like this ----------------------- *:10/1h root:5/1h,10/1d ----------------------- in which case all rules that match a particular user and service will be checked. The user or host will be blocked if any of the rule triggers matches. The sense of the user matching can be inverted by placing a '!' in front of the rule so that ----------- !root:20/1d ----------- is a rule which would match for all users apart from root. It is important to treat root as a special case in the user_rule otherwise excessive attempts to authenticate as root will result in the root account being locked out even for valid holders of root credentials. The config file can contain any arguments that would be supplied via PAM config. In the config file arguments are placed on separate lines. Comments may be included after a '#' and line continuation is possible by placing a back slash at the end of the line to be continued. Here is a sample /etc/security/pam_abl.conf: ---------------------------- # /etc/security/pam_abl.conf debug host_db=/var/lib/abl/hosts.db host_purge=2d host_rule=*:10/1h,30/1d user_db=/var/lib/abl/users.db user_purge=2d user_rule=!root:10/1h,30/1d --------------------------- All of the standard PAM arguments (debug, expose_account, no_warn, try_first_pass, use_first_pass, use_mapped_pass) are accepted; with the exception of debug and no_warn these are ignored. The arguments that are specific to the PAM module are as follows: *db_home*:: Specify the directory where the Berkeley db can store it's lock and log files. Make sure this dir exists and is writable. *limits*:: It's value should have the following syntax "-". If you do not block machines that do too many attempts, the db can easily become bloated. To prevent this we introduced this setting. As soon as there are a number of attempts for a user/host, the number of stored attempts for this user/host is reduced to . A of 0 means no limits. Make sure that is larger then any rule specified. We recommend a value of "1000-1200". *host_db, user_db*:: Specify the name of the databases that will be used to log failed authentication attempts. The host database is used to log the hostname responsible for a failed auth and the user database is used to log the requested username. If host_db or user_db is omitted the corresponding auto blacklisting will be disabled. *host_purge, user_purge*:: Specify the length of time for which failed attempts should be kept in the databases. For rules to work correctly this must be at least as long as the longest period specified in a corresponding rule. You may wish to retain information about failed attempts for longer than this so that the pam_abl command line tool can report information over a longer period of time. The format for this item is a number with an optional multiplier suffix, 's', 'm', 'h' or 'd' which correspond with seconds, minutes, hours and days. To specify seven days for example one would use '7d'. Note that in normal operation pam_abl will only purge the logged data for a particular host or user if it happens to be updating it, i.e. if that host or user makes another failed attempt. To purge all old entries the pam_abl command line tool should be used. *host_rule, user_rule*:: These are the rules which determine the circumstances under which accounts are auto-blacklisted. The host_rule is used to block access to hosts that are responsible for excessive authentication failures and the user_rule is used to disable accounts for which there have been excessive authentication failures. The rule syntax is described in full below. *host_clr_cmd, host_blk_cmd, user_clr_cmd, user_blk_cmd*:: Deprecated for security reasons. Please use the corresponding safer option: host_clear_cmd, host_block_cmd, user_clear_cmd, user_block_cmd *host_clear_cmd, host_block_cmd, user_clear_cmd, user_block_cmd*:: These specify commands that will run during a check when an item switches state since its last check. + host_clear_cmd and user_clear_cmd will run if the host or user is currently allowed access. host_block_cmd and user_block_cmd are run if the host or user is currently being blocked by their respective rules. + Within the commands, you can specify substitutions with %h, %u and %s, which will be replace with the host name, user name and service currently being checked. For security reasons we do not run the command using the system call. We use the more secure fork/exec solution. This means that you can't specify input and output redirections. + Note that this also means that no escaping is done, so if you call a shell here, you might introduce a security problem. + The commands should follow a special syntax (you can use the command line tool with the -d option to test the parsing of your commands) where the command and it's different arguments need to be enclosed in [] and all text not enclosed in [] is simply ignored. For example: "[/usr/bin/logger] ignored [block] [user] [%u]" will run the command "/usr/bin/logger block user ". If you want to specify a '[', ']' or '\', you need to escape them with a '\'. *host_whitelist, user_whitelist*:: ;-seperated list of hosts/users whose attempts will not be recorded. So if an attempt is made from "10.10.10.10" for user "root" and "root" is in the whitelist, only an attempt for his machine is recorded. If a user is whitelisted, this does not prevent his machine from being blocked. Hosts can be specified using their IP (1.1.1.1) or using a netmask (1.1.1.1/24) EXAMPLE ------- ---------------------------- # /etc/security/pam_abl.conf debug host_db=/var/lib/abl/hosts.db host_purge=2d host_rule=*:10/1h,30/1d host_block_cmd=[/sbin/iptables] [-I] [INPUT] [-s] [%h] [-j] [DROP] user_db=/var/lib/abl/users.db user_purge=2d user_rule=!root:10/1h,30/1d user_clear_cmd=[/usr/bin/logger] [block] [user] [%u] ---------------------------- SEE ALSO -------- pam_abl.conf(5), pam_abl(1) AUTHORS ------- Lode Mertens Andy Armstrong + Chris Tasma log.c000664 000000 000000 00000007422 12207732173 011774 0ustar00rootroot000000 000000 /* * pam_abl - a PAM module and program for automatic blacklisting of hosts and users * * Copyright (C) 2005-2012 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "log.h" #include #include #include #include #include #include #define MODULE_NAME "pam-abl" #define UNUSED(x) (void)(x) log_context *createLogContext() { log_context *retValue = malloc(sizeof(log_context)); retValue->debug = 0; return retValue; } void destroyLogContext(log_context *context) { free(context); } static void log_out(int pri, const char *format, ...) { va_list ap; va_start(ap, format); #if defined(TEST) || defined(TOOLS) UNUSED(pri); //to please the compiler when TEST or TOOLS is defined vfprintf(stderr, format, ap); fprintf(stderr, "\n"); #else openlog(MODULE_NAME, LOG_CONS | LOG_PID, LOG_AUTHPRIV); vsyslog(pri, format, ap); closelog(); #endif va_end(ap); } #if !defined(TOOLS) && !defined(TEST) void log_pam_error(log_context *context, pam_handle_t *handle, int err, const char *what) { UNUSED(context); log_out(LOG_ERR, "%s (%d) while %s", pam_strerror(handle, err), err, what); } #endif void log_sys_error(log_context *context, int err, const char *what) { UNUSED(context); log_out(LOG_ERR, "%s (%d) while %s", strerror(err), err, what); } void log_db_error(log_context *context, int err, const char *what) { UNUSED(context); log_out(LOG_ERR, "%s (%d) while %s", db_strerror(err), err, what); } void log_info(log_context *context, const char *format, ...) { UNUSED(context); va_list ap; va_start(ap, format); #if defined(TEST) || defined(TOOLS) fprintf(stderr, "INFO: "); vfprintf(stderr, format, ap); fprintf(stderr, "\n"); #else openlog(MODULE_NAME, LOG_CONS | LOG_PID, LOG_AUTHPRIV); vsyslog(LOG_INFO, format, ap); closelog(); #endif va_end(ap); } void log_error(log_context *context, const char *format, ...) { UNUSED(context); va_list ap; va_start(ap, format); #if defined(TEST) || defined(TOOLS) fprintf(stderr, "ERROR: "); vfprintf(stderr, format, ap); fprintf(stderr, "\n"); #else openlog(MODULE_NAME, LOG_CONS | LOG_PID, LOG_AUTHPRIV); vsyslog(LOG_WARNING, format, ap); closelog(); #endif va_end(ap); } void log_warning(log_context *context, const char *format, ...) { UNUSED(context); va_list ap; va_start(ap, format); #if defined(TEST) || defined(TOOLS) fprintf(stderr, "WARNING: "); vfprintf(stderr, format, ap); fprintf(stderr, "\n"); #else openlog(MODULE_NAME, LOG_CONS | LOG_PID, LOG_AUTHPRIV); vsyslog(LOG_WARNING, format, ap); closelog(); #endif va_end(ap); } void log_debug(log_context *context, const char *format, ...) { va_list ap; va_start(ap, format); if (context == NULL || context->debug) { #if defined(TEST) || defined(TOOLS) # ifndef TEST fprintf(stderr, "DEBUG: "); vfprintf(stderr, format, ap); fprintf(stderr, "\n"); # endif #else openlog(MODULE_NAME, LOG_CONS | LOG_PID, LOG_AUTHPRIV); vsyslog(LOG_DEBUG, format, ap); closelog(); #endif } va_end(ap); } log.h000664 000000 000000 00000004236 12207732173 012001 0ustar00rootroot000000 000000 /* * pam_abl - a PAM module and program for automatic blacklisting of hosts and users * * Copyright (C) 2005-2012 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #ifndef LOG_H #define LOG_H #include typedef struct log_context { short debug; } log_context; /* Create an empty log context that can be used with the functions below */ log_context *createLogContext(); /* release all the resources occupied by the given context After calling this function do not use the context ptr anymore */ void destroyLogContext(log_context *context); /* Log a system error. This will also lookup the string representation of err Make sure err is a value specified in errno.h */ void log_sys_error(log_context *context, int err, const char *what); /* Log a Berkeley db error. This will also lookup the string representation of err Make sure err is a value returned by a Berkeley db function */ void log_db_error(log_context *context, int err, const char *what); /* Log an informational message */ void log_info(log_context *context, const char *format, ...); /* Log a normal error message */ void log_error(log_context *context, const char *format, ...); /* Log a normal warning message */ void log_warning(log_context *context, const char *format, ...); /* If debugging output is requested, write the given message out */ void log_debug(log_context *context, const char *format, ...); #if !defined(TOOLS) && !defined(TEST) void log_pam_error(log_context *context, pam_handle_t *handle, int err, const char *what); #endif #endif pam_abl.c000664 000000 000000 00000051507 12207732173 012611 0ustar00rootroot000000 000000 /* * pam_abl - a PAM module and program for automatic blacklisting of hosts and users * * Copyright (C) 2005-2012 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "pam_abl.h" #include "dbfun.h" #include "rule.h" #include #include #include #include #include #include #include #define DB_NAME "state" #define COMMAND_SIZE 1024 abl_info *createAblInfo() { abl_info *retValue = malloc(sizeof(abl_info)); if (retValue) memset(retValue, 0, sizeof(abl_info)); return retValue; } void destroyAblInfo(abl_info *info) { //setting it with NULL effectively frees the memory setInfo(info, NULL, NULL, NULL); if (info) free(info); } void setInfo(abl_info *info, const char *user, const char *host, const char *service) { if (!info) return; if (info->user) free(info->user); if (info->host) free(info->host); if (info->service) free(info->service); info->user = NULL; info->host = NULL; info->service = NULL; if (user) info->user = strdup(user); if (host) info->host = strdup(host); if (service) info->service = strdup(service); } /* * Substitute the user/host/service in the given string * \param str: The string where we need to substitute in. '%' is the escape char, so if you want a '%' in your string you will need to add 2: '%%' => '%' If the char after the '%' is not in [uhs] then it is just copied to the output buffer '%r' => 'r' '%u' => info->user '%h' => info->host '%s' => info->service * \param info: The info that needs to be filled in * \param result: The buffer to write to. if this is NULL nothing will be written, only the number of bytes needed is returned * \return: The number of bytes needed to do the substitution, negative on failure. This will include the \0 char at the end */ int prepare_string(const char *str, const abl_info *info, char *result) { int host_sz = 0; int user_sz = 0; int service_sz = 0; if(info->host != NULL) host_sz = strlen(info->host); if(info->user != NULL) user_sz = strlen(info->user); if(info->service != NULL) service_sz = strlen(info->service); int inputIndex = 0; int outputIndex = 0; int percentSeen = 0; while (str[inputIndex]) { if (percentSeen) { percentSeen = 0; switch (str[inputIndex]) { case 'u': if (result && info->user) memcpy(result+outputIndex,info->user, user_sz); outputIndex += user_sz; break; case 'h': if (result && info->host) memcpy(result+outputIndex,info->host, host_sz); outputIndex += host_sz; break; case 's': if (result && info->service) memcpy(result+outputIndex,info->service, service_sz); outputIndex += service_sz; break; default: //no special char, let's just add the char to the output buffer if (result) result[outputIndex] = str[inputIndex]; ++outputIndex; break; } } else { if (str[inputIndex] == '%') { percentSeen = 1; } else { if (result) result[outputIndex] = str[inputIndex]; ++outputIndex; } } ++inputIndex; } if (result) result[outputIndex] = '\0'; ++outputIndex; return outputIndex; } int ablExec(char *const arg[]) { if (arg == NULL || arg[0] == NULL || (arg[0])[0] == '\0') return -1; pid_t pid = fork(); if (pid) { //parent int childStatus = 0; waitpid(pid, &childStatus, 0); return WEXITSTATUS(childStatus); } else if (pid == 0) { //child int result = execv(arg[0], arg); exit(result); } else { //fork failed return -1; } } int _runCommand(const char *origCommand, const abl_info *info, log_context *logContext, int (execFun)(char *const arg[])) { int err = 0; int bufSize = 0; int argNum = 0; char** result = NULL; char** substResult = NULL; char *command = NULL; if (!origCommand || ! *origCommand) return 0; command = strdup(origCommand); if (!command) return 1; //first split the command in it's pieces argNum = splitCommand(command, NULL, logContext); //no real command if (argNum == 0) goto cleanup; if (argNum < 0) { err = 1; goto cleanup; } result = malloc((argNum+1) * sizeof(char*)); substResult = malloc((argNum+1) * sizeof(char*)); memset(result, 0, (argNum+1) * sizeof(char*)); memset(substResult, 0, (argNum+1) * sizeof(char*)); argNum = splitCommand(command, result, logContext); //now iterate over all the parts of the array and substitute everything int partIndex = 0; while (result[partIndex]) { bufSize = prepare_string(result[partIndex], info, NULL); if (bufSize <= 0) { //crap, something went wrong //the error should already been logged log_warning(logContext, "failed to substitute %s.", result[partIndex]); err = 1; goto cleanup; } if (bufSize > COMMAND_SIZE) { log_warning(logContext, "command length error."); goto cleanup; } substResult[partIndex] = malloc(bufSize * sizeof(char)); if (substResult[partIndex] == NULL) { err = 1; goto cleanup; } bufSize = prepare_string(result[partIndex], info, substResult[partIndex]); ++partIndex; } //the first value in the substResult array is the command to execute //we should now execute the command err = execFun(substResult); cleanup: //result does not hold dynamically allocated strings if (result) { free(result); } if (substResult) { partIndex = 0; while (substResult[partIndex]) { free(substResult[partIndex]); ++partIndex; } free(substResult); } if (command) free(command); return err; } static int runCommand(const char *origCommand, const abl_info *info, log_context *logContext) { return _runCommand(origCommand, info, logContext, ablExec); } int runHostCommand(BlockState bState, const abl_args *args, abl_info *info, log_context *logContext) { const char *command = NULL; if (bState == BLOCKED) command = args->host_blk_cmd; else if (bState == CLEAR) command = args->host_clr_cmd; return runCommand(command, info, logContext); } int runUserCommand(BlockState bState, const abl_args *args, abl_info *info, log_context *logContext) { const char *command = NULL; if (bState == BLOCKED) command = args->user_blk_cmd; else if (bState == CLEAR) command = args->user_clr_cmd; return runCommand(command, info, logContext); } PamAblDbEnv *openPamAblDbEnvironment(abl_args *args, log_context *logContext) { if (!args || !args->db_home || !*args->db_home) return NULL; DbEnvironment *environment = NULL; Database *hostDb = NULL; Database *userDb = NULL; int err = createEnvironment(logContext, args->db_home, &environment); if (err) { log_db_error(logContext, err, "Creating database environment."); return NULL; } if (args->host_db && *args->host_db) { err = openDatabase(environment, args->host_db, DB_NAME, &hostDb); if (err) { log_db_error(logContext, err, "Creating host database."); goto open_fail; } } if (args->user_db && *args->user_db) { err = openDatabase(environment, args->user_db, DB_NAME, &userDb); if (err) { log_db_error(logContext, err, "Creating user database."); goto open_fail; } } PamAblDbEnv *retValue = malloc(sizeof(PamAblDbEnv)); if (!retValue) { log_error(logContext, "Memory allocation failed while opening the databases."); goto open_fail; } memset(retValue, 0, sizeof(PamAblDbEnv)); retValue->m_environment = environment; retValue->m_hostDb = hostDb; retValue->m_userDb = userDb; return retValue; open_fail: if (hostDb) closeDatabase(hostDb); if (userDb) closeDatabase(userDb); if (environment) destroyEnvironment(environment); return NULL; } void destroyPamAblDbEnvironment(PamAblDbEnv *env) { if (!env) return; if (env->m_hostDb) closeDatabase(env->m_hostDb); if (env->m_userDb) closeDatabase(env->m_userDb); if (env->m_environment) destroyEnvironment(env->m_environment); free(env); } static int update_status(Database *db, const char *subject, const char *service, const char *rule, time_t tm, log_context *logContext, BlockState *updatedState, int *stateChanged) { //assume the state will not change *stateChanged = 0; DbEnvironment *dbEnv = db->m_environment; AuthState *subjectState = NULL; //all database actions need to be wrapped in a transaction int err = startTransaction(dbEnv); if (err) { log_db_error(logContext, err, "starting transaction to update_status."); return err; } err = getUserOrHostInfo(db, subject, &subjectState); if (err) log_db_error(logContext, err, "retrieving information failed."); //only update if we have a subjectState (It is already in the database and no error) if (subjectState) { *updatedState = rule_test(logContext, rule, subject, service, subjectState, tm); //is the state changed if (*updatedState != getState(subjectState)) { //update the BlockState in the subjectState if (setState(subjectState, *updatedState)) { log_error(logContext, "The state could not be updated."); } else { //save the subjectState err = saveInfo(db, subject, subjectState); if (err) { log_db_error(logContext, err, "saving the changed info."); } else { *stateChanged = 1; } } } destroyAuthState(subjectState); } if (err) abortTransaction(dbEnv); else commitTransaction(dbEnv); return err; } BlockState check_attempt(const PamAblDbEnv *dbEnv, const abl_args *args, abl_info *info, log_context *logContext) { if (info) info->blockReason = AUTH_FAILED; if (!dbEnv || !args || !info) return CLEAR; time_t tm = time(NULL); const char *user = info->user; const char *host = info->host; const char *service = info->service; BlockState updatedHostState = CLEAR; BlockState updatedUserState = CLEAR; //fist check the host //do we need to update the host information? if (host && *host && dbEnv->m_hostDb && dbEnv->m_hostDb->m_environment && args->host_rule) { int hostStateChanged = 0; int err = update_status(dbEnv->m_hostDb, host, service, args->host_rule, tm, logContext, &updatedHostState, &hostStateChanged); if (!err) { if (hostStateChanged) runHostCommand(updatedHostState, args, info, logContext); } else { //if something went wrong, we can't trust the value returned, so by default, do not block updatedHostState = CLEAR; } } //now check the user if (user && *user && dbEnv->m_userDb && dbEnv->m_userDb->m_environment && args->user_rule) { int userStateChanged = 0; int err = update_status(dbEnv->m_userDb, user, service, args->user_rule, tm, logContext, &updatedUserState, &userStateChanged); if (!err) { if (userStateChanged) runUserCommand(updatedUserState, args, info, logContext); } else { //if something went wrong, do not trust the returned value //and by default do not block updatedUserState = CLEAR; } } //last but not least, update the blockreason info->blockReason = AUTH_FAILED; if (updatedHostState == BLOCKED && updatedUserState == BLOCKED) { info->blockReason = BOTH_BLOCKED; return BLOCKED; } if (updatedHostState == BLOCKED) info->blockReason = HOST_BLOCKED; if (updatedUserState == BLOCKED) info->blockReason = USER_BLOCKED; return updatedHostState == BLOCKED || updatedUserState == BLOCKED ? BLOCKED : CLEAR; } /* static int whitelistMatch(const char *subject, const char *whitelist) { if (!subject || !whitelist) return 0; size_t subjLen = strlen(subject); const char *begin = whitelist; const char *end = NULL; while ((end = strchr(begin, ';')) != NULL) { size_t len = (size_t)(end - begin); if (subjLen == len) { if (memcmp(begin, subject, subjLen) == 0) return 1; } begin = end+1; } if (subjLen == strlen(begin)) { if (memcmp(begin, subject, subjLen) == 0) return 1; } return 0; } */ static int parseNumber(const char *numberStr, size_t length, unsigned max, unsigned *number, size_t *consumedSize) { size_t x = 0; unsigned result = 0; while (x < length) { if (isdigit(numberStr[x])) { result *= 10; result += numberStr[x] - '0'; if (result > max) return 1; } else { break; } ++x; } //if no characther parsed, just tell it failed if (!x) return 1; if (number) *number = result; if (consumedSize) *consumedSize = x; return 0; } /* Currently we only support ipv4 addresses If no netmask is found, the netmask param will be -1 */ int parseIP(const char *ipStr, size_t length, int *netmask, u_int32_t *ip) { if (netmask) *netmask = -1; if (ip) *ip = 0; u_int32_t parsedIp = 0; size_t consumed = 0; //try to parse the 4 parts of the IP int i = 0; for (; i < 4; ++i) { //can we parse a number from the string size_t used = 0; unsigned parsed = 0; if (parseNumber(&ipStr[consumed], length - consumed, 255, &parsed, &used) != 0) return 1; //if we come here, we have a parsed number stored in parsed consumed += used; parsedIp <<= 8; parsedIp += parsed; //if it's not the last part, we expect there to be a '.' if (i < 3) { if (consumed >= length || ipStr[consumed] != '.') return 1; ++consumed; } } //check if there is still a netmask that we can parse if (consumed < length) { if (ipStr[consumed] != '/') return 1; ++consumed; size_t used = 0; unsigned parsed = 0; if (parseNumber(&ipStr[consumed], length - consumed, 32, &parsed, &used) != 0) return 1; consumed += used; if (netmask) *netmask = parsed; } //did we use every char? If not, it probably was something that looked like a ip, but wasn't if (consumed != length) return 1; if (ip) *ip = parsedIp; return 0; } /* Is the given host ip part of the subnet defined by ip and netmask */ int inSameSubnet(u_int32_t host, u_int32_t ip, int netmask) { //an invalid netmask never matches if (netmask < 0 || netmask > 32) return 0; //The behaviour of shifts is only defined if the value of the right //operand is less than the number of bits in the left operand. //So shifting a 32-bit value by 32 or more is undefined if (netmask == 0) return 1; host >>= (32-netmask); ip >>= (32-netmask); return host == ip; } int whitelistMatch(const char *subject, const char *whitelist, int isHost) { if (!subject || !whitelist) return 0; size_t subjLen = strlen(subject); int hostParsed = 0; u_int32_t ip = 0; if (isHost) { int netmask = 0; if (parseIP(subject, subjLen, &netmask, &ip) == 0 && netmask == -1) hostParsed = 1; } const char *begin = whitelist; const char *end = NULL; while ((end = strchr(begin, ';')) != NULL) { size_t len = (size_t)(end - begin); if (hostParsed) { int netmask = 0; u_int32_t netmaskIp = 0; if (parseIP(begin, len, &netmask, &netmaskIp) == 0) { if (ip == netmaskIp || (netmask >= 0 && inSameSubnet(ip, netmaskIp, netmask))) return 1; } } if (subjLen == len && memcmp(begin, subject, subjLen) == 0) return 1; begin = end+1; } size_t len = strlen(begin); if (hostParsed) { int netmask = 0; u_int32_t netmaskIp = 0; if (parseIP(begin, len, &netmask, &netmaskIp) == 0) { if (ip == netmaskIp || (netmask >= 0 && inSameSubnet(ip, netmaskIp, netmask))) return 1; } } if (subjLen == len && memcmp(begin, subject, subjLen) == 0) return 1; return 0; } static int recordSubject(const PamAblDbEnv *pamDb, const abl_args *args, abl_info *info, log_context *logContext, int isHost) { if (!pamDb || !args || !info) return 1; DbEnvironment *dbEnv = pamDb->m_environment; Database *db = pamDb->m_userDb; const char *subject = info->user; const char *data = info->host; const char *service = info->service; long purgeTimeout = args->user_purge; const char *whitelist = args->user_whitelist; if (isHost) { db = pamDb->m_hostDb; subject = info->host; data = info->user; purgeTimeout = args->host_purge; whitelist = args->host_whitelist; } //if the db was not opened, or nothing to record on => do nothing if (!db || !subject || !*subject) return 0; if (whitelistMatch(subject, whitelist, isHost)) return 0; if (!dbEnv || purgeTimeout <= 0) return 1; if (!data) data = ""; if (!service) service = ""; AuthState *subjectState = NULL; //all database actions need to be wrapped in a transaction int err = startTransaction(dbEnv); if (err) { log_db_error(logContext, err, "starting the transaction to record_attempt."); return err; } err = getUserOrHostInfo(db, subject, &subjectState); if (err) { log_db_error(logContext, err, "retrieving information failed."); } else if (!subjectState) { if (createEmptyState(CLEAR, &subjectState)) { log_error(logContext, "Could not create an empty entry."); } } if (subjectState) { time_t tm = time(NULL); time_t purgeTime = tm - purgeTimeout; //if it already existed in the db, we loaded it and otherwise we created an empty state. //first do a purge, this way we can make some room for our next attempt purgeAuthState(subjectState, purgeTime); if (addAttempt(subjectState, info->blockReason, tm, data, service, args->lowerlimit, args->upperlimit)) { log_error(logContext, "adding an attempt."); } else { err = saveInfo(db, subject, subjectState); if (err) log_db_error(logContext, err, "saving the changed entry with added attempt."); } destroyAuthState(subjectState); } if (err) abortTransaction(pamDb->m_environment); else commitTransaction(pamDb->m_environment); return err; } int record_attempt(const PamAblDbEnv *dbEnv, const abl_args *args, abl_info *info, log_context *logContext) { if (!dbEnv || !args || !info) return 1; int addHostResult = 0; int addUserResult = 0; if (info->host && *info->host) addHostResult = recordSubject(dbEnv, args, info, logContext, 1); if (info->user && *info->user) addUserResult = recordSubject(dbEnv, args, info, logContext, 0); return addHostResult || addUserResult; } pam_abl.h000664 000000 000000 00000010013 12207732173 012601 0ustar00rootroot000000 000000 /* * pam_abl - a PAM module and program for automatic blacklisting of hosts and users * * Copyright (C) 2005-2012 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #ifndef PAM_ABL_H #define PAM_ABL_H #include "config.h" #include "dbfun.h" typedef struct PamAblDbEnv { DbEnvironment *m_environment; Database *m_userDb; Database *m_hostDb; } PamAblDbEnv; typedef struct { BlockReason blockReason; char *user; char *host; char *service; } abl_info; abl_info *createAblInfo(); void destroyAblInfo(abl_info *info); void setInfo(abl_info *info, const char *user, const char *host, const char *service); /* Given a full configuration open the required environment and databases \param args The config with all the db params \param logContext The loggin context to use when reporting errors/warnings/... \return a valid environment on success, otherwise a nullptr \note if something goes wrong, error messages are written to the logContext */ PamAblDbEnv *openPamAblDbEnvironment(abl_args *args, log_context *logContext); /* Close a full environment. Make sure no transaction is open \note Do not use the env pointer anymore after calling this function */ void destroyPamAblDbEnvironment(PamAblDbEnv *env); /* Call the desired scripts if possible \param bState Determines what script gets called (BLOCKED or CLEAR) \param args Holds the strings with the scripts \param info The current host/user/service \param logContext The context that will be used when reporting errors/warnings/... \return zero on success, otherwise non zero */ int runHostCommand(BlockState bState, const abl_args *args, abl_info *info, log_context *logContext); int runUserCommand(BlockState bState, const abl_args *args, abl_info *info, log_context *logContext); /* Returns the current state for the given attempt. This will: - calculate the current state - update the state saved in the database - run the required scripts - change the blockReason (by default this will be AUTH_FAILED), unless it could already be determined If something goes wrong while checking, CLEAR is returned and diagnostic messages are written using the given logContext. */ BlockState check_attempt(const PamAblDbEnv *dbEnv, const abl_args *args, abl_info *info, log_context *logContext); /* Record an authentication attempt. This will: - purge the db data for the given host and user - add an entry for the given host and user with as reason info->blockReason If something went wrong, a non zero value is returned and a diagnostic message is logged using the logContext */ int record_attempt(const PamAblDbEnv *dbEnv, const abl_args *args, abl_info *info, log_context *logContext); /* Following functions are only 'exported' for testing purposes */ int prepare_string(const char *str, const abl_info *info, char *result); int parseIP(const char *ipStr, size_t length, int *netmask, u_int32_t *ip); int whitelistMatch(const char *subject, const char *whitelist, int isHost); int inSameSubnet(u_int32_t host, u_int32_t ip, int netmask); int ablExec(char *const arg[]); //the following function takes a pointer to a real exec function just for testing purpopes //that way we don't actually have to run an external command just to test this function int _runCommand(const char *origCommand, const abl_info *info, log_context *logContext, int (execFun)(char *const arg[])); #endif pam_functions.c000664 000000 000000 00000014515 12207732173 014061 0ustar00rootroot000000 000000 /* * pam_abl - a PAM module and program for automatic blacklisting of hosts and users * * Copyright (C) 2005-2012 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "pam_abl.h" #include "config.h" #include #include #include #include #define MODULE_NAME "pam-abl" typedef struct abl_context { abl_args *args; abl_info *attemptInfo; PamAblDbEnv *dbEnv; log_context *logContext; } abl_context; static void cleanup(pam_handle_t *pamh, void *data, int err) { (void)(pamh); //if we are replacing our data pointer, ignore the cleanup. //the function replacing our data should handle the cleanup if (err & PAM_DATA_REPLACE) return; if (NULL != data) { abl_context *context = data; log_debug(context->logContext, "In cleanup, err is %08x", err); if (err) { int recordResult = record_attempt(context->dbEnv, context->args, context->attemptInfo, context->logContext); log_debug(context->logContext, "record returned %d", recordResult); } if (context->dbEnv) destroyPamAblDbEnvironment(context->dbEnv); destroyAblInfo(context->attemptInfo); if (context->args) config_free(context->args); if (context->logContext) destroyLogContext(context->logContext); free(context); } } /* Authentication management functions */ PAM_EXTERN int pam_sm_authenticate(pam_handle_t *pamh, int flags, int argc, const char **argv) { (void)(flags); int err = PAM_BUF_ERR; abl_context *context = NULL; const char *pUser = NULL; const char *pService = NULL; const char *pHost = NULL; err = pam_get_data(pamh, MODULE_NAME, (const void **)(&context)); if (err != PAM_SUCCESS) { context = NULL; } if (!context) { context = malloc(sizeof(abl_context)); if (!context) { err = PAM_BUF_ERR; goto psa_fail; } memset(context, 0, sizeof(abl_context)); context->attemptInfo = createAblInfo(); context->args = config_create(); context->logContext = createLogContext(); if (!context->attemptInfo || !context->args || !context->logContext) { err = PAM_BUF_ERR; goto psa_fail; } err = config_parse_args(argc, argv, context->args, context->logContext); if (err != 0) { err = PAM_SERVICE_ERR; log_error(context->logContext, "Could not parse the config."); goto psa_fail; } /* We now keep the database open from the beginning to avoid the cost * of opening them repeatedly. */ context->dbEnv = openPamAblDbEnvironment(context->args, context->logContext); if (!context->dbEnv) { log_error(context->logContext, "The database environment could not be opened"); goto psa_fail; } err = pam_set_data(pamh, MODULE_NAME, context, cleanup); if (err != PAM_SUCCESS) { log_pam_error(context->logContext, pamh, err, "setting PAM data"); goto psa_fail; } } else { //we have a previous data pointer. We will ASSUME that it was from a previous failed attempt //a good example is sshd, when you try to login, you are given 3 attempts, so this function //can be called up to three times before the cleanup function is called. int recordResult = record_attempt(context->dbEnv, context->args, context->attemptInfo, context->logContext); log_debug(context->logContext, "record from authenticate returned %d", recordResult); } //get the user again, it can be that another module has changed the username or something else err = pam_get_item(pamh, PAM_USER, (const void **) &pUser); if (err != PAM_SUCCESS) { log_pam_error(context->logContext, pamh, err, "getting PAM_USER"); goto psa_fail; } err = pam_get_item(pamh, PAM_SERVICE, (const void **) &pService); if (err != PAM_SUCCESS) { log_pam_error(context->logContext, pamh, err, "getting PAM_SERVICE"); goto psa_fail; } err = pam_get_item(pamh, PAM_RHOST, (const void **) &pHost); if (err != PAM_SUCCESS) { log_pam_error(context->logContext, pamh, err, "getting PAM_RHOST"); goto psa_fail; } //this will also delete the old info that was already stored setInfo(context->attemptInfo, pUser, pHost, pService); BlockState bState = check_attempt(context->dbEnv, context->args, context->attemptInfo, context->logContext); if (bState == BLOCKED) { log_info(context->logContext, "Blocking access from %s to service %s, user %s", context->attemptInfo->host, context->attemptInfo->service, context->attemptInfo->user); return PAM_AUTH_ERR; } else { return PAM_SUCCESS; } psa_fail: if (context) { if (context->dbEnv) destroyPamAblDbEnvironment(context->dbEnv); destroyAblInfo(context->attemptInfo); if (context->args) config_free(context->args); if (context->logContext) destroyLogContext(context->logContext); free(context); //it can be that we already set the data pointer, let's remove it pam_set_data(pamh, MODULE_NAME, NULL, NULL); } return err; } PAM_EXTERN int pam_sm_setcred(pam_handle_t *pamh, int flags, int argc, const char **argv) { (void)(argc); (void)(argv); (void)(flags); return pam_set_data(pamh, MODULE_NAME, NULL, cleanup); } /* Init structure for static modules */ #ifdef PAM_STATIC struct pam_module _pam_abl_modstruct = { MODULE_NAME, pam_sm_authenticate, pam_sm_setcred, NULL, NULL, NULL, NULL }; #endif rule.c000664 000000 000000 00000013162 12207732173 012160 0ustar00rootroot000000 000000 /* * pam_abl - a PAM module and program for automatic blacklisting of hosts and users * * Copyright (C) 2005-2012 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "rule.h" #include #include #include int parse_long(const char **sp, long *lp) { long l = 0; if (!isdigit(**sp)) { return EINVAL; } while (isdigit(**sp)) { l = l * 10 + *(*sp)++ - '0'; } *lp = l; return 0; } /* Parse a time specification in the form * [s|m|h|d] */ static int parse_time(const char **sp, long *tp) { long t; int err; if (err = parse_long(sp, &t), 0 != err) { return err; } /* Handle the multiplier suffix */ switch (**sp) { case 'd': t *= 24; case 'h': t *= 60; case 'm': t *= 60; case 's': (*sp)++; default: ; } *tp = t; return 0; } int rule_parse_time(const char *p, long *t, long min) { int err; if (err = parse_time(&p, t), 0 != err) { *t = min; return err; } if (*p != '\0') { *t = min; return EINVAL; } if (*t < min) { *t = min; } return 0; } static size_t wordlen(const char *rp) { size_t l = 0; while (*rp != '\0' && *rp != '/' && *rp != '|' && *rp != ':' && !isspace(*rp)) { rp++; l++; } return l; } static int match(log_context *log, const char *pattern, const char *target, size_t len) { log_debug(log, "match('%s', '%s', %d)", pattern, target, len); if (!pattern) return 0; return (len == strlen(pattern)) && (memcmp(pattern, target, len) == 0); } static int matchname(log_context *log, const char *user, const char *service, const char **rp) { size_t l = wordlen(*rp); int ok; log_debug(log, "Check %s/%s against %s(%d)", user, service, *rp, l); ok = (l != 0) && ((l == 1 && **rp == '*') || match(log, user, *rp, l)); (*rp) += l; if (ok) { log_debug(log, "Name part matches, **rp = '%c'", **rp); } if (**rp == '/') { (*rp)++; l = wordlen(*rp); ok &= (l != 0) && ((l == 1 && **rp == '*') || match(log, service, *rp, l)); (*rp) += l; } log_debug(log, "%satch!", ok ? "M" : "No m"); return ok; } static int matchnames(log_context *log, const char *user, const char *service, const char **rp) { int ok = matchname(log, user, service, rp); while (**rp == '|') { (*rp)++; ok |= matchname(log, user, service, rp); } return ok; } static long howmany(log_context *log, AuthState *history, time_t now, long limit) { if (firstAttempt(history)) return -1; long i = 0; AuthAttempt attempt; while (!nextAttempt(history, &attempt)) { if (difftime(now, attempt.m_time) <= (double) limit) ++i; } log_debug(log, "howmany(%ld) = %ld", limit, i); return i; } static int matchperiod(log_context *log, AuthState *history, time_t now, const char **rp) { int err; long count, period; log_debug(log, "matchperiod(%p, %u, '%s')", history, getNofAttempts(history), *rp); if (err = parse_long(rp, &count), 0 != err) { return 0; } log_debug(log, "count is %ld, **rp='%c'", count, **rp); if (**rp != '/') { return 0; } (*rp)++; if (err = parse_time(rp, &period), 0 != err) { return 0; } log_debug(log, "period is %ld, **rp='%c'", period, **rp); log_debug(log, "Checking %ld/%ld", count, period); return howmany(log, history, now, period) >= count; } int rule_matchperiods(log_context *log, AuthState *history, time_t now, const char **rp) { if (matchperiod(log, history, now, rp)) { return 1; } while (**rp == ',') { (*rp)++; if (matchperiod(log, history, now, rp)) { return 1; } } return 0; } static int check_clause(log_context *log, const char **rp, const char *user, const char *service, AuthState *history, time_t now) { int inv = 0; if (**rp == '!') { inv = 1; (*rp)++; } if (!(inv ^ matchnames(log, user, service, rp))) { return 0; } log_debug(log, "Name matched, next char is '%c'", **rp); /* The name part matches so now check the trigger clauses */ if (**rp != ':') { return 0; } (*rp)++; return rule_matchperiods(log, history, now, rp); } BlockState rule_test(log_context *log, const char *rule, const char *user, const char *service, AuthState *history, time_t now) { if (!rule) return CLEAR; const char *rp = rule; while (*rp != '\0') { if (check_clause(log, &rp, user, service, history, now)) { return BLOCKED; } while (*rp != '\0' && !isspace(*rp)) { rp++; } while (isspace(*rp)) { rp++; } } return CLEAR; } rule.h000664 000000 000000 00000006304 12207732173 012165 0ustar00rootroot000000 000000 /* * pam_abl - a PAM module and program for automatic blacklisting of hosts and users * * Copyright (C) 2005-2012 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #ifndef RULE_H #define RULE_H #include "log.h" #include "typefun.h" /* Parses a long from the string pointed to by sp. On success sp will point to the char right after the long \param sp The string to parse a long with \param lp The found long \return zero on success, non zero otherwise */ int parse_long(const char **sp, long *lp); /* Parse a time specification in the form * [s|m|h|d] \param p The string that holds the time to parse \param t The parsed time in seconds \param min The minimum time that needs to be returned \return zero on success, non zero otherwise */ int rule_parse_time(const char *p, long *t, long min); /* Tries to match a comma seperated list of '/