Older, proprietary Unix systems tend to include the "logins" command, which is a handy little command for searching your passwd and shadow files for different sorts of information. In particular, "logins -p" (find accounts with null passwords) and "logins -d" (find accounts with duplicate UIDs) are useful when auditing a large user database. Unfortunately, the "logins" command doesn't exist in Linux. But of course you can emulate some of its functionality using other command-line primitives.
Finding accounts with null passwords is just a simple awk expression:
# awk -F: '($2 == "") {print $1}' /etc/shadow
We use "-F:" to tell awk to split on the colon delimiters in the file and then look for entries where the password hash in the second field is empty. When we get a match, we print the username in the first field.
Finding duplicate UIDs is a little more complicated:
# cut -f3 -d: /etc/passwd | sort -n | uniq -c | awk '!/ 1 / {print $2}'
Here we're using "cut" to pull the UIDs out of the third field of /etc/passwd, then passing them into "sort -n" to put them in numeric order. "uniq -c" counts the number of occurrences of each UID, creating one line of output for each UID with the count in the first column. Our awk expression looks for lines where this count is not 1 and prints the UID from each matching line.
Another useful password auditing command is to look for all accounts with UID 0. Normally there should only be a single UID 0 account in your password file ("root"), but sometimes you'll see attackers hiding UID 0 accounts in the middle of the password file. The following awk snippet will display the usernames of all UID 0 accounts:
# awk -F: '($3 == 0) {print $1}' /etc/passwd
More generally, you can use the "sort" command to sort the entire password file numerically by UID:
# sort -t: -k3 -n /etc/passwd
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/bin/sh
bin:x:2:2:bin:/bin:/bin/sh
...
"-t" is used to specify the column delimiter, "-k" specifies which column(s) to sort on, and "-n" means do a numeric (as opposed to alphabetic) sort. The advantage to viewing the password file this way is that all the UID 0 accounts bubble right up to the top of the output, plus it's easier to spot accounts with duplicate UIDs this way.
Ed responds:
OK, sports fans... Hal really threw down the gauntlet here. Brace yourselves, because this is gonna get ugly. You've been warned.
Unlike Linux with its /etc/passwd and /etc/shadow files, Windows doesn't make it easy to access user account password hashes. Thus, it's much harder for us to determine if a password is blank... but we do have some options.
For starters, we could blatantly violate our ground rules here and use third-party tools. One option that pops into mind is fgdump, my favorite tool for dumping Windows hashes. We could run:
C:\> fgdump -c & find "NO PASSWORD" 127.0.0.1.pwdump & del 127.0.0.1.pwdump
This command invokes fgdump, which runs against localhost by default, with the -c option to turn off the dumping of cached credentials, making it give us only the local SAM database. Unfortunately, fgdump doesn't have the option of displaying the hashes on standard output, but instead stores its results in a file called [IPaddr].pwdump. So, we then run the find command to look for output that contains the words "NO PASSWORD" in this file, and then delete the file.
Now, keep in mind that if a given user has a password that is 15 or more characters in length, that user will authenticate using only the NT Hash, and Windows will set the LANMAN to a value of a hash of padding, that old AAD3B4... stuff. In its output for such accounts, fgdump will display "NO PASSWORD" for the LANMAN hash of such accounts, even though they do have an NT hash with an NT password. Thus, to avoid false positives with accounts that have passwords greater than 14 characters, we should tweak our command to:
C:\> fgdump -c & find "NO PASSWORD*********************:NO PASSWORD" 127.0.0.1.pwdump
& del 127.0.0.1.pwdump
Easy.
Yeah, it's easy, if you throw away our treasured rule of using only built-in tools.
But, there's another way, which violates a completely different ground rule we've got around here. Instead of using a third-party tool, we could rely on built-in functionality via a Visual Basic Script. There's a great script from the awesome Scripting Guys, available here, which attempts to change each user's password from blank to blank. If it is successful, you've got a user with a blank password. Nice and easy!
But, this one also throws another precious ground rule under the bus. That is, we aren't supposed to be using scripts, but instead we rely on single (albeit at times complex) commands.
For a third option, why don't we try to mimic the operation of this VB script at the cmd.exe command line? We could change a user password to blank by running:
C:\> net user [user] ""
This command tells the system to change the password of [user] to blank. If the password policy allows such passwords, it will succeed. Ummm... that's no good for us, because it succeeds regardless of the current user's password. So, this command violates a third coveted rule around here: Commands have to actually work.
Oooookay then. Is there a fourth option? Turns out there is, but it gets pretty ugly. The basis of this command is to rely on "net use" to make an SMB connection locally, thusly:
C:\> net use \\[hostname] "" /u:[user]
If the guest account is disabled, and you otherwise have a default security policy, the system displays the following text if [user] has a blank password:
System error 1327 has occurred.
Logon failure: user account restriction.
Possible reasons are blank passwords not allowed, logon hour restrictions,
or a policy restriction has been enforced.
Note that first possible reason -- blank passwords.
Also, note that this same message comes up if there are policies defined that restrict the account from logging on during certain times of day or other policy restrictions. But, still, in most environments, this is an indication that the password is blank. Not perfect, but good enough for most cases.
Building on this, here ya go, a "single" command that checks to see if local accounts have blank passwords, without using any third-party tools or scripts:
C:\> FOR /F "tokens=2 skip=1" %i in ('wmic useraccount list brief') do @echo.
& echo Checking %i & net use \\[hostname] "" /u:%i 2>&1 | find /i "blank" >nul
&& echo %i MAY HAVE A BLANK PASSWORD & net use * /del /y > nul
Wow! There's a mess, huh? Here's what I'm doing...
I'm setting up a FOR /F loop to iterate on the output of the command 'wmic useraccount list brief', which will show all of the locally defined accounts on the box. I'm parsing the output of that command by skipping the first line (which is column headers) and setting the value of my iterator variable to the second item in each line (the first is the Account Type, the second is the SYSTEM\username).
I'm then echoing a blank line to our output (echo.) to make things prettier followed by displaying a message that I'm checking a given account (echo Checking %i). Then, I try to make an SMB connection to our local hostname (you really should put in your own system's hostname... using \\127.0.0.1 isn't reliable on every version of Windows). The attempted SMB connection has a password of blank ("") and a user name of our current iterator variable (/u:%i).
Now, if the account has a blank password, I'll get an error message that says: "Logon failure: user account restrictions. Possible reasons are blank passwords...". Remember that if you have any of those other restrictions defined on the box, our little one-liner will give you false positives.
Then, I take our error message and dump it into a replicated copy of our standard output (2>&1) so that I can scrape through what was Standard Error with the find command to look for the word "blank". I dump the output of that to nul so we don't see it's ugliness. Then, if the find command is successful (&&), I print out a message saying that %i MAY HAVE A BLANK PASSWORD. Note the weasel word "MAY". That's because there may be other account restrictions applied to the account or system.
Finally, I drop any residual SMB connections we've made (net use * /del /y), dumping its output to nul.
Whew! I tried several other methods for doing this at the command line, but they got even uglier, believe it or not.
Also, note that the above command depends on the Guest account being disabled. If you have that account enabled, it'll show that no accounts have blank passwords, as you'll never get the requisite error message. But, for most production environments, you really should disable that Guest account, you know. You can do that at the command line with:
C:\> net user guest active:no
Be careful, though, to make sure that you don't have any apps that actually rely on the Guest account being enabled.
Now, let's see what else Hal has in store for us in his initial challenge...
Ahhh... userID numbers, better known as SIDs in Windows. Well, Windows assigns those at account creation, attempting to make sure that they are all unique. Therefore, we should never have the same value for two different accounts at the same time... right? Just to make sure, we can dump them using:
C:\> wmic useraccount get sid, name
Unfortunately, the output shows name first followed by sid. That's a crummy aspect of wmic... it shows you attributes in its output alphabetically by attribute name. "Name" comes before "Sid" alphabetically, so we get name, sid even though we asked for sid, name. We can reverse them using a FOR /F loop to parse, and then sort them, using the following command:
C:\> (for /F "tokens=1,2 skip=1" %i in ('"wmic useraccount get sid, name"')
do @echo %j %i) | sort
So, here, I'm running the wmic command inside a FOR /F loop. I've embedded the command in single-quote followed by double-quote at the beginning, and double-quote followed by single-quote at the end. The reason for this is two fold... Normally, we require just the single quotes at the beginning and end to run a command inside the parens of a FOR /F loop. But, if the command has a comma or quote in it, we must either use a ^ before the comma or quote, or put the whole thing inside of single-quote double-quotes as I have here. I used the latter because of the parens around the entire FOR /F loop, which I used so I could pipe it through the sort command. I've found that the ^, or ^" in FOR /F loops have problems when you put parens around the entire FOR /F loop, so I've taken to using the ' " and " ' as they work regardless of the ( ) around the FOR /F loop. It's a little trick I figured out on a flight to Defcon years ago.
So, where was I? Oh yeah... we've now got a list of SIDs and usernames, sorted. Our sort is alphabetic, not numeric, which kinda stinks. Still, you could eyeball the resulting list and see if any of them are identical. Sadly, there is no built-in "uniq" command in Windows. Man, Hal and Paul have it easy, don't they?
If you really want a uniq, you could download a "uniq" command for Windows. Or, you could simulate one. Are you ready for a sick little trick to detect whether a file has all unique lines using built-in tools in Windows?
For this stunt, we'll rely on the built-in Windows fc command, which compares two files (fc stands for "file compare". We can use it as follows:
C:\> (for /F "tokens=1,2 skip=1" %i in ('"wmic useraccount get sid, name"')
do @echo %j %i) | sort > accounts.txt & sort /r accounts.txt > accountsr.txt
& fc accounts.txt accountsr.txt & del accounts.txt & del accountsr.txt
The idea here is to use the sort /r command to create a list of accounts in reverse order, and then compare it to the original list of accounts. If there are no duplicate SIDs, your output will simply show the list of accounts forward, followed by the list of accounts backward. If there are one or more duplicate SIDs, you will see a blank line in the middle of your output as fc tries to show you the differences. Let me illustrate with an example.
Here is the output when we have all unique SIDs:
Comparing files accounts.txt and ACCOUNTSR.TXT
***** accounts.txt
S-1-5-21-2574636452-2948509063-3462863534-1002 SUPPORT_388945a0
S-1-5-21-2574636452-2948509063-3462863534-1003 ASPNET
S-1-5-21-2574636452-2948509063-3462863534-1004 HelpAssistant
S-1-5-21-2574636452-2948509063-3462863534-1005 skodo
S-1-5-21-2574636452-2948509063-3462863534-1006 nonadmin
S-1-5-21-2574636452-2948509063-3462863534-1064 __vmware_user__
S-1-5-21-2574636452-2948509063-3462863534-1072 frank
S-1-5-21-2574636452-2948509063-3462863534-1073 dog
S-1-5-21-2574636452-2948509063-3462863534-1074 fred
S-1-5-21-2574636452-2948509063-3462863534-500 Administrator
S-1-5-21-2574636452-2948509063-3462863534-501 Guest
***** ACCOUNTSR.TXT
S-1-5-21-2574636452-2948509063-3462863534-501 Guest
S-1-5-21-2574636452-2948509063-3462863534-500 Administrator
S-1-5-21-2574636452-2948509063-3462863534-1074 fred
S-1-5-21-2574636452-2948509063-3462863534-1073 dog
S-1-5-21-2574636452-2948509063-3462863534-1072 frank
S-1-5-21-2574636452-2948509063-3462863534-1064 __vmware_user__
S-1-5-21-2574636452-2948509063-3462863534-1006 nonadmin
S-1-5-21-2574636452-2948509063-3462863534-1005 skodo
S-1-5-21-2574636452-2948509063-3462863534-1004 HelpAssistant
S-1-5-21-2574636452-2948509063-3462863534-1003 ASPNET
S-1-5-21-2574636452-2948509063-3462863534-1002 SUPPORT_388945a0
*****
And, here is the output when we have a dupe:
Comparing files accounts.txt and ACCOUNTSR.TXT
***** accounts.txt
S-1-5-21-2574636452-2948509063-3462863534-1002 SUPPORT_388945a0
S-1-5-21-2574636452-2948509063-3462863534-1003 ASPNET
S-1-5-21-2574636452-2948509063-3462863534-1004 HelpAssistant
S-1-5-21-2574636452-2948509063-3462863534-1005 skodo
S-1-5-21-2574636452-2948509063-3462863534-1006 nonadmin
S-1-5-21-2574636452-2948509063-3462863534-1064 __vmware_user__
S-1-5-21-2574636452-2948509063-3462863534-1072 frank
***** ACCOUNTSR.TXT
S-1-5-21-2574636452-2948509063-3462863534-501 Guest
S-1-5-21-2574636452-2948509063-3462863534-500 Administrator
S-1-5-21-2574636452-2948509063-3462863534-1074 fred
S-1-5-21-2574636452-2948509063-3462863534-1072 frank
*****
***** accounts.txt
S-1-5-21-2574636452-2948509063-3462863534-1072 dog
S-1-5-21-2574636452-2948509063-3462863534-1074 fred
S-1-5-21-2574636452-2948509063-3462863534-500 Administrator
S-1-5-21-2574636452-2948509063-3462863534-501 Guest
***** ACCOUNTSR.TXT
S-1-5-21-2574636452-2948509063-3462863534-1072 dog
S-1-5-21-2574636452-2948509063-3462863534-1064 __vmware_user__
S-1-5-21-2574636452-2948509063-3462863534-1006 nonadmin
S-1-5-21-2574636452-2948509063-3462863534-1005 skodo
S-1-5-21-2574636452-2948509063-3462863534-1004 HelpAssistant
S-1-5-21-2574636452-2948509063-3462863534-1003 ASPNET
S-1-5-21-2574636452-2948509063-3462863534-1002 SUPPORT_388945a0
See, the frank and dog account have the same SID, as indicated on either side of that blank line in the middle there. If there are any dupe SIDs, you'll see that tell-tale blank line in the middle of the output. Sure, it's a kluge, but it is a quick and dirty way of determining whether there is a duplicate in a stream of information.
And, we end on a much simpler note. Hal wants to find accounts with superuser privileges (UID 0 on Linux). We can simply look for accounts in the administrators group using:
C:\> net localgroup administrators
So, there you have it. I warned you that it would get ugly, and I am to deliver on my promises. In the end, we were able to achieve nearly all of what Hal did in Linux, making certain assumptions.