├── assets ├── SSHapendoes-logo.png └── Abusing PAM and NSS for Malice and Benefit.pdf ├── Makefile ├── nss_canary.c ├── LICENSE ├── pam_canary.c └── README.md /assets/SSHapendoes-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VirusFriendly/SSHapendoes/HEAD/assets/SSHapendoes-logo.png -------------------------------------------------------------------------------- /assets/Abusing PAM and NSS for Malice and Benefit.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VirusFriendly/SSHapendoes/HEAD/assets/Abusing PAM and NSS for Malice and Benefit.pdf -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | arch = $(shell uname -m) 2 | 3 | all: 4 | gcc -fPIC -shared -o libnss_canary.so.2 -Wl,-soname,libnss_canary.so.2 nss_canary.c 5 | gcc -fPIC -DPIC -shared -rdynamic -o pam_canary.so pam_canary.c 6 | install: $(arch) 7 | install -m 0644 libnss_canary.so.2 /lib 8 | /sbin/ldconfig -n /lib /usr/lib 9 | arm: 10 | install -m 0644 pam_canary.so /lib/arm-linux-gnuabihf/security 11 | x86_64: 12 | install -m 0644 pam_canary.so /lib/x86_64-linux-gnu/security 13 | clean: 14 | /bin/rf -rf libnss_canary.so.2 15 | -------------------------------------------------------------------------------- /nss_canary.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | enum nss_status _getpwnam(const char *, struct passwd *, char *, size_t, int *); 10 | 11 | char username[LOGIN_NAME_MAX]; 12 | char *gecos = "CANARY"; 13 | char *dir = "/home/SSHapendoes"; 14 | char *shell = "/bin/false"; 15 | char *spasswd = "*"; 16 | 17 | enum nss_status _nss_canary_setpwent(void) { 18 | return NSS_STATUS_SUCCESS; 19 | } 20 | 21 | enum nss_status _nss_canary_endpwent(void) { 22 | return NSS_STATUS_SUCCESS; 23 | } 24 | 25 | enum nss_status _nss_canary_getpwent_r(struct passwd *result_buf, char *buffer, size_t buflen, int *errnop) { 26 | return NSS_STATUS_NOTFOUND; 27 | } 28 | 29 | enum nss_status _nss_canary_getpwnam(const char *name, struct passwd *result, char *buffer, size_t buflen, int *errnop) { 30 | return _getpwnam(name, result, buffer, buflen, errnop); 31 | } 32 | 33 | enum nss_status _nss_canary_getpwnam_r(const char *name, struct passwd *result, char *buffer, size_t buflen, int *errnop) { 34 | return _getpwnam(name, result, buffer, buflen, errnop); 35 | } 36 | 37 | enum nss_status _getpwnam(const char *name, struct passwd *result, char *buffer, size_t buflen, int *errnop) { 38 | if(name == NULL) { 39 | *errnop = EINVAL; 40 | return NSS_STATUS_UNAVAIL; 41 | } 42 | memcpy(username, name, (size_t) LOGIN_NAME_MAX); 43 | 44 | result->pw_name=username; 45 | result->pw_uid=32767; 46 | result->pw_gid=32767; 47 | result->pw_gecos=gecos; 48 | result->pw_dir=dir; 49 | result->pw_passwd=spasswd; 50 | result->pw_shell=shell; 51 | snprintf(buffer, buflen, "%s", name); 52 | 53 | return NSS_STATUS_SUCCESS; 54 | } 55 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015, Eric Gragsone 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | 1. Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 2. Redistributions in binary form must reproduce the above copyright notice, 10 | this list of conditions and the following disclaimer in the documentation 11 | and/or other materials provided with the distribution. 12 | 3. If we meet some day, and you think this stuff is worth it, you can buy the 13 | author a beer in return. If meeting in person is impractical either due to 14 | distance or the author's demise, then you may drink to the author's honor 15 | provided that you submit photographic evidence of the event to the current 16 | maintainer. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 19 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 20 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 22 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 23 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 24 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 25 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 27 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /pam_canary.c: -------------------------------------------------------------------------------- 1 | /* Define which PAM interfaces we provide */ 2 | #define PAM_SM_ACCOUNT 3 | #define PAM_SM_AUTH 4 | #define PAM_SM_PASSWORD 5 | #define PAM_SM_SESSION 6 | 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | 17 | /* PAM entry point for session creation */ 18 | int pam_sm_open_session(pam_handle_t *pamh, int flags, int argc, const char **argv) { 19 | return(PAM_SUCCESS); 20 | } 21 | 22 | /* PAM entry point for session cleanup */ 23 | int pam_sm_close_session(pam_handle_t *pamh, int flags, int argc, const char **argv) { 24 | return(PAM_SUCCESS); 25 | } 26 | 27 | /* PAM entry point for accounting */ 28 | int pam_sm_acct_mgmt(pam_handle_t *pamh, int flags, int argc, const char **argv) { 29 | return(PAM_SUCCESS); 30 | } 31 | 32 | /* PAM entry point for authentication verification */ 33 | int pam_sm_authenticate(pam_handle_t *pamh, int flags, int argc, const char **argv) { 34 | char *user, *passwd, *host; 35 | struct passwd *pwent=NULL; 36 | struct spwd *spent=NULL; 37 | static char *pwprompt="Password:"; 38 | static char *incorrect="#010#012#015#177INCORRECT"; 39 | static char *salt="$6$Zc5QRoCj$"; 40 | struct pam_conv *conv; 41 | struct pam_message msg; 42 | const struct pam_message *msgp; 43 | struct pam_response *resp; 44 | int pam_err, retry; 45 | 46 | pam_get_user(pamh, (const char **) &user, NULL); 47 | pwent=getpwnam(user); 48 | openlog(NULL, LOG_PID, LOG_AUTH); 49 | 50 | if(strcmp(pwent->pw_gecos, "CANARY") != 0) { 51 | spent=getspnam(user); 52 | 53 | if((spent == NULL) || (strcmp(spent->sp_pwdp, "*") != 0)) { 54 | return(PAM_SUCCESS); 55 | } 56 | } 57 | 58 | pam_err = pam_get_item(pamh, PAM_CONV, (const void **)&conv); 59 | 60 | if(pam_err != PAM_SUCCESS) { 61 | 62 | return (PAM_SYSTEM_ERR); 63 | } 64 | 65 | pam_err = pam_get_item(pamh, PAM_RHOST, (const void **)&host); 66 | 67 | if(pam_err != PAM_SUCCESS) { 68 | return (PAM_SYSTEM_ERR); 69 | } 70 | 71 | msg.msg_style=PAM_PROMPT_ECHO_OFF; 72 | msg.msg = pwprompt; 73 | msgp=&msg; 74 | pam_err=pam_get_authtok(pamh, PAM_AUTHTOK, (const char **)&passwd, NULL); 75 | 76 | if(pam_err != PAM_SUCCESS) { 77 | return(PAM_AUTH_ERR); 78 | } 79 | 80 | crypt(incorrect, salt); 81 | 82 | syslog(LOG_AUTH|LOG_ERR, "SSHapendoes Triggered user=%s passwd=%s rhost=%s", user, passwd, host); 83 | closelog(); 84 | 85 | return(PAM_AUTH_ERR); 86 | } 87 | 88 | /* 89 | PAM entry point for setting user credentials (that is, to actually 90 | establish the authenticated user's credentials to the service provider) 91 | */ 92 | int pam_sm_setcred(pam_handle_t *pamh, int flags, int argc, const char **argv) { 93 | return(PAM_SUCCESS); 94 | } 95 | 96 | /* PAM entry point for authentication token (password) changes */ 97 | int pam_sm_chauthtok(pam_handle_t *pamh, int flags, int argc, const char **argv) { 98 | return(PAM_SUCCESS); 99 | } 100 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![SSHapendoes' Logo](https://github.com/VirusFriendly/SSHapendoes/blob/master/assets/SSHapendoes-logo.png) 2 | ### Capture passwords of login attempts for non-existent and disabled accounts. 3 | 4 | SSHapendoes turns any Linux host with an SSH port into a medium-interaction honeypot, by spoofing the existence of Canary Accounts in a similar manner to AD Honey Accounts. 5 | 6 | Anyone with an SSH port open to the Internet is constantly being attacked by threat actors attempting to brute-force root and other accounts. Metadata such as a list of targeted users can be used as behavioral signatures to profile threat actors as they attack from different hosts. Password lists is another key piece of metadata, but usually attempted passwords are not logged. This tool exposes the passwords attempted by attackers without exposing passwords of legitimite user accounts. 7 | 8 | ## Background 9 | 10 | While studying attacker behavior using [SSH-Ranking](https://github.com/pronto/SSH-Ranking), I noticed that different attackers attack different lists of users, and that these userlists could be used identitify attackers even if they use different IP's. I became curious of what could be learned by studying the passwords they used. Additionally, I have a side project of writing John the Ripper [word mangling rules](https://github.com/maetrics/john-scripts), for which harvesting actual attacker passwords is useful. 11 | 12 | My first step to gathering passwords was to build a custom PAM module. (Editorial/Rant: I find that PAM is one of those things that most admins know what it stands for, and what it does, but often don't understand the inner workerings and probably wouldn't be able to identify a malicious PAM module. I highly recommend creating your own custom PAM module to be more enlightened with the power they contain. In the future I'll likely release various malicious PAM modules to demonstrate this. For use in CTFs and pen-tests of course.) 13 | 14 | However, even with a custom PAM module, OpenSSH don't send passwords to PAM for accounts that don't exist. Instead it sends #010#012#015#177INCORRECT. My guess is this is to prevent attackers learning which accounts are valid by using timing attacks. To work around this issue, I created a custom NSS module that spoofs all accounts. 15 | 16 | The result: 17 | 18 | ``` 19 | Mar 31 08:24:02 (none) sshd[1382]: SSHapendoes Triggered user=root passwd=tslinux rhost=115.230.127.61 20 | Mar 31 08:24:02 (none) sshd[1382]: SSHapendoes Triggered user=root passwd=kodiak rhost=115.230.127.61 21 | Mar 31 08:24:09 (none) sshd[1386]: SSHapendoes Triggered user=root passwd=PASSW0RD rhost=115.230.127.61 22 | Mar 31 08:24:10 (none) sshd[1386]: SSHapendoes Triggered user=root passwd=qwerty15 rhost=115.230.127.61 23 | Mar 31 08:24:10 (none) sshd[1386]: SSHapendoes Triggered user=root passwd=k4hvdq9tj9 rhost=115.230.127.61 24 | Mar 31 08:24:21 (none) sshd[1388]: SSHapendoes Triggered user=root passwd=itadmin rhost=115.230.127.61 25 | Mar 31 08:24:21 (none) sshd[1388]: SSHapendoes Triggered user=root passwd=picasso rhost=115.230.127.61 26 | Mar 31 08:24:21 (none) sshd[1388]: SSHapendoes Triggered user=root passwd=needhouse rhost=115.230.127.61 27 | Mar 31 08:24:30 (none) sshd[1390]: SSHapendoes Triggered user=root passwd=cracker88 rhost=115.230.127.61 28 | ``` 29 | 30 | ## How it works 31 | 32 | After getpwnam() checks the legitimate passwd databases for users, it will check nss_canary which will spoof any user request it recieves. These user requests will have "CANARY" in the gecos field. Legitimate users will not be affected by this, as NSSwitch will be configured to use CANARY as last resort. 33 | 34 | For example: 35 | 36 | ``` 37 | virusfriendly@(none):~$ getent passwd nonexistantuser 38 | nonexistantuser:*:32767:32767:CANARY:/home/SSHapendoes:/bin/false 39 | ``` 40 | 41 | This "CANARY" gecos field will indicate to the SSHapendoes PAM module that it's a spoofed account, and log the attempt along with the password used. 42 | 43 | The SSHapendoes PAM module will also log attempts for accounts with no password hash (as determined by NSSwitch). 44 | 45 | For example, these accounts have no password hashes: 46 | 47 | ``` 48 | root:*:16112:0:99999:7::: 49 | daemon:*:16112:0:99999:7::: 50 | bin:*:16112:0:99999:7::: 51 | sys:*:16112:0:99999:7::: 52 | sync:*:16112:0:99999:7::: 53 | games:*:16112:0:99999:7::: 54 | ``` 55 | 56 | If the account doesn't have "CANARY" in the gecos field, and it has a hash in the shadow file (as determined by NSSwitch), the SSHapendoes PAM module will return a success without logging anything, allowing PAM to continue through its normal list of modules. Thus allowing normal user logins via passwords or ssh-keys, and without logging the passwords of legitimate users. 57 | 58 | ## Warning 59 | **In case it's not obvious, mucking around with authenication internals can accidentailly disable your ability to log into the system, or worse allow attackers to log in. I don't recommend installing this on a production server, or in a secure environment.** 60 | 61 | ## OS Support 62 | SSHapendoes is known to work on the following Operating Systems/Distros 63 | 64 | Linux 65 | 66 | * Raspbian 10 67 | * Ubuntu 20 68 | 69 | ### How YOU can help 70 | If you successfully install this project, create an issue letting me know what distro and version you installed it on and any installation notes. 71 | 72 | If you're the clever type and can help with making this project more portable, feel free send a pull request. 73 | 74 | # Installation 75 | ## Ubuntu 20 76 | ### Setting up the Build Environment 77 | `apt install build-essential libpam0g-dev` 78 | 79 | ### Compiling 80 | `make` 81 | 82 | ### Installing 83 | `sudo make install` 84 | 85 | ### Configuration 86 | #### NS Switch 87 | Edit /etc/nsswitch and append the passwd line with canary as in the following example. 88 | 89 | ``` 90 | passwd: files systemd canary 91 | ``` 92 | 93 | **It's very important that canary is listed last, otherwise once PAM is configured all users will be blocked.** 94 | 95 | Additionally, any install packages that check for a user's existence before creating daemon accounts (like TOR does), will fail because of this NSSwitch Configuration. You will need to temporarily disable and later renable this configuration for such installs. 96 | 97 | You can test the configuration with the following command 98 | 99 | `getent passwd nonexistantuser` 100 | 101 | which should display something like the following 102 | 103 | `nonexistantuser:*:32767:32767:CANARY:/home/SSHapendoes:/bin/false` 104 | 105 | #### SSHD PAM 106 | Edit /etc/pam.d/sshd and insert the following line before any other `auth` configure lines. Often this means to insert prior to the `@include common-auth` line. 107 | 108 | `auth requisite pam_canary.so` 109 | 110 | With NS Switch and the SSHD PAM configurations updated, SSHapendoes can be tested by SSHing into the localhost with a unknown user like so: 111 | 112 | `ssh nonexistantuser@localhost` 113 | 114 | Supply fake passwords until SSHD kicks you out, then check the auth.log as follows: 115 | 116 | `grep SSHapendoes auth.log` 117 | 118 | which should show something like the following: 119 | 120 | ``` 121 | auth.log:Aug 27 17:06:43 ubuntu20server sshd[1986]: SSHapendoes Triggered user=nonexistantuser passwd=changeme rhost=127.0.0.1 122 | auth.log:Aug 27 17:06:47 ubuntu20server sshd[1986]: SSHapendoes Triggered user=nonexistantuser passwd=badpasswd rhost=127.0.0.1 123 | auth.log:Aug 27 17:06:50 ubuntu20server sshd[1986]: SSHapendoes Triggered user=nonexistantuser passwd=letmein rhost=127.0.0.1 124 | ``` 125 | 126 | An example modified config file using the default sshd pam configuration on Ubuntu 20 127 | 128 | ``` 129 | # PAM configuration for the Secure Shell service 130 | 131 | auth requisite pam_canary.so 132 | 133 | # Standard Un*x authentication. 134 | @include common-auth 135 | 136 | # Disallow non-root logins when /etc/nologin exists. 137 | account required pam_nologin.so 138 | 139 | # Uncomment and edit /etc/security/access.conf if you need to set complex 140 | # access limits that are hard to express in sshd_config. 141 | # account required pam_access.so 142 | 143 | # Standard Un*x authorization. 144 | @include common-account 145 | 146 | # SELinux needs to be the first session rule. This ensures that any 147 | # lingering context has been cleared. Without this it is possible that a 148 | # module could execute code in the wrong domain. 149 | session [success=ok ignore=ignore module_unknown=ignore default=bad] pam_selinux.so close 150 | 151 | # Set the loginuid process attribute. 152 | session required pam_loginuid.so 153 | 154 | # Create a new session keyring. 155 | session optional pam_keyinit.so force revoke 156 | 157 | # Standard Un*x session setup and teardown. 158 | @include common-session 159 | 160 | # Print the message of the day upon successful login. 161 | # This includes a dynamically generated part from /run/motd.dynamic 162 | # and a static (admin-editable) part from /etc/motd. 163 | session optional pam_motd.so motd=/run/motd.dynamic 164 | session optional pam_motd.so noupdate 165 | 166 | # Print the status of the user's mailbox upon successful login. 167 | session optional pam_mail.so standard noenv # [1] 168 | 169 | # Set up user limits from /etc/security/limits.conf. 170 | session required pam_limits.so 171 | 172 | # Read environment variables from /etc/environment and 173 | # /etc/security/pam_env.conf. 174 | session required pam_env.so # [1] 175 | # In Debian 4.0 (etch), locale-related environment variables were moved to 176 | # /etc/default/locale, so read that as well. 177 | session required pam_env.so user_readenv=1 envfile=/etc/default/locale 178 | 179 | # SELinux needs to intervene at login time to ensure that the process starts 180 | # in the proper default security context. Only sessions which are intended 181 | # to run in the user's context should be run after this. 182 | session [success=ok ignore=ignore module_unknown=ignore default=bad] pam_selinux.so open 183 | 184 | # Standard Un*x password updating. 185 | @include common-password 186 | ``` 187 | 188 | #### SSHD (Optional) 189 | 190 | Edit /etc/ssh/sshd_config 191 | 192 | If you don't want SSHapendoes/SSHD to resolve the attacker's IP. Set `UseDNS no`. 193 | 194 | **Red flag warnings all over the place here. Be sure you know what you're doing before proceeding further. Don't be mad at me if you kill your box...or worse** 195 | 196 | Most attackers target the root account. To collect the passwords for these attacks, you need to first disable the root password. This is the default configuation on Ubuntu systems, but it is best to double check. 197 | 198 | Then enable Root Login in the SSHD Config as follows. 199 | 200 | `PermitRootLogin yes` 201 | 202 | # Future Plans 203 | 204 | * SSHapendoes Docker Images 205 | * Including other services, like Samba 206 | * Adding SSHapendoes support in [SSH-Rankings](https://github.com/pronto/SSH-Ranking) 207 | 208 | If you think this project could use additional features, please submit a feature request. 209 | --------------------------------------------------------------------------------