Safe Penetration Testing Labs · intermediate · ~13 min
Identify a crypt(3) hash's scheme from its prefix and flag weak, fast-to-crack schemes.
/etc/shadow stores password hashes in the crypt(3) format, where a leading $id$ tag names the algorithm — $1$ for md5crypt, $5$ for SHA-256, $6$ for SHA-512, $2a$/$2b$/$2y$ for bcrypt, $7$ for scrypt-based schemes and $y$ for yescrypt. Older entries have no tag at all: a bare 13-character string is traditional DES crypt. Triage means classifying each entry by its tag and flagging the schemes that are no longer acceptable, so an administrator knows which accounts need a password reset.
Hash strength decides how long a stolen /etc/shadow protects its users. Traditional DES crypt truncates passwords to eight characters and falls in seconds on modern hardware; md5crypt is not far behind. Knowing which accounts still use them turns a vague "we should modernise" into a concrete list. This is also a good exercise in prefix parsing where the absence of a marker is itself a meaningful case.
The $id$salt$hash shape. A leading $, an algorithm id, another $, then salt and hash. Matching the tag is an exact prefix comparison — $1$ and $5$ differ by one character and by many orders of magnitude in strength.
bcrypt has variants. $2a$, $2b$ and $2y$ are all bcrypt with historical differences; a classifier must accept all of them rather than matching only one.
No tag means DES. A 13-character string drawn from [./0-9A-Za-z] is traditional DES crypt. Here the absence of a $ prefix is the identifying feature, which is why the shape check matters.
Special non-hash values. * and ! (or a leading !) mean the account cannot authenticate by password — locked, not weak. Reporting those as weak hashes is a false positive that erodes trust in the tool.
Empty field is the real emergency. An empty password field means no password is required at all — far more serious than a weak algorithm, and easy to miss if you only classify tags.
Weakness is a policy decision. DES and md5crypt are indefensible today; SHA-256/512 crypt are acceptable with a high round count; bcrypt, scrypt and yescrypt are preferred. Encode the policy explicitly so it can be reviewed and updated.
#include <string.h>
/* returns: 0 unknown/other, 1 DES, 2 md5crypt, 5 sha256crypt,
6 sha512crypt, 7 scrypt-family, 8 bcrypt */
static int cc(char c) { /* the DES crypt alphabet */
return (c >= '.' && c <= '9') || (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z');
}
int shadow_hash_scheme(const char *h) {
if (strncmp(h, "$1$", 3) == 0) return 2;
if (strncmp(h, "$5$", 3) == 0) return 5;
if (strncmp(h, "$6$", 3) == 0) return 6;
if (strncmp(h, "$7$", 3) == 0) return 7;
if (strncmp(h, "$2a$", 4) == 0 || strncmp(h, "$2b$", 4) == 0
|| strncmp(h, "$2y$", 4) == 0) return 8; /* all bcrypt variants */
/* no tag: a 13-char crypt-alphabet string is traditional DES */
size_t n = strlen(h);
if (n == 13) {
for (size_t i = 0; i < n; i++) if (!cc(h[i])) return 0;
return 1;
}
return 0;
}
int is_weak_shadow_hash(const char *h) {
int s = shadow_hash_scheme(h);
return (s == 1 || s == 2); /* DES and md5crypt - policy, stated explicitly */
}
Key points:
Every hash in /etc/shadow carries a scheme tag: $1$ md5crypt, $5$/$6$ SHA-crypt, $y$ yescrypt, $2a/$2b/$2y$ bcrypt, or a bare 13-character traditional DES hash. The scheme dictates how expensive the hash is to crack.
The run below classifies a set of hashes and labels the weak ones (DES and md5crypt crack fast; bcrypt and yescrypt are slow by design).
#include <stdio.h>
#include <string.h>
static int cc(char c){return (c>='.'&&c<='9')||(c>='A'&&c<='Z')||(c>='a'&&c<='z');}
static int scheme(const char*h){if(strncmp(h,"$1$",3)==0)return 2;if(strncmp(h,"$5$",3)==0)return 5;if(strncmp(h,"$6$",3)==0)return 6;if(strncmp(h,"$y$",3)==0)return 7;if(strncmp(h,"$2a$",4)==0||strncmp(h,"$2b$",4)==0||strncmp(h,"$2y$",4)==0)return 8;size_t n=strlen(h);if(n==13){for(size_t i=0;i<13;i++)if(!cc(h[i]))return 0;return 1;}return 0;}
static int weak(const char*h){int s=scheme(h);return (s==0||s==1||s==2)?1:0;}
int main(void){
const char *hashes[] = { "$6$salt$0123456789abcdef", "$1$abcd$xxxxxxxxxxxxxxxxxxxxx0", "kR8s0Xq1pLmA1", "$2b$12$abcdefghijklmnopqrstuv" };
const char *names[] = { "sha512crypt", "md5crypt", "DES", "bcrypt" };
for(int i=0;i<4;i++)
printf("%-12s scheme=%d %s\n", names[i], scheme(hashes[i]), weak(hashes[i])?"WEAK":"strong");
return 0;
}
| Step | Line | What happens |
|---|---|---|
| 1 | strncmp(h, "$1$", 3) |
Exact prefix match for md5crypt. $1$ and $5$ differ by a single character but are worlds apart in strength. |
| 2 | bcrypt variants | $2a$, $2b$ and $2y$ are all bcrypt; matching only one would misclassify the others as unknown. |
| 3 | strlen(h) == 13 |
With no $ tag, length is the first DES signal. |
| 4 | cc(h[i]) over all 13 |
Confirms every character is in the DES crypt alphabet — length alone is far too weak a test. |
| 5 | fall through to 0 | Anything unrecognised is reported as unknown rather than guessed at. |
| 6 | is_weak_shadow_hash |
Encodes the policy — DES and md5crypt are weak — in one reviewable expression. |
Confusing the $5$ and $6$ tags; treating an unrecognized scheme as strong instead of conservatively weak.
Compiler errors and warnings:
-Wchar-subscripts if cc takes a plain char and you later use <ctype.h>; keep the comparisons explicit as above.-Wsign-compare between size_t n and an int index.Runtime symptoms:
$2a$ and the sample uses $2b$ or $2y$.*, !) reported as weak. Handle those as a separate "no password login" category before classifying.$5$ and $6$ confused. Off-by-one in the prefix comparison length; each tag is exactly 3 characters here.strncmp is safe on short inputs, but any manual indexing must check the length first.Technique: build a fixture with one entry per scheme plus a locked account, an empty field and a malformed string. Every branch then has a test, including the ones that are easy to forget.
strncmp is bounded, manual indexing is not. Prefix checks with strncmp are safe on short strings; if you index h[3] directly you must first confirm the string is long enough.strlen is appropriate here because a shadow field is genuine NUL-terminated text — unlike the binary blobs elsewhere in this track, where an explicit length is required.const char * throughout; an audit never modifies the file it examines.Concrete uses: Compliance scanners and host-audit tools inventory which hashing schemes are in use so administrators can force resets on accounts still holding DES or md5crypt hashes. Migration tooling uses the same classification to decide which entries need rehashing on next login. Incident responders check it when assessing how exposed credentials are after a /etc/shadow disclosure.
Professional best practices:
Beginner:
Intermediate:
1. (Beginner) Classify a scheme. Implement int shadow_hash_scheme(const char *h) for the tagged schemes. Example: $6$salt$... -> 6; $2b$12$... -> 8. Concepts: exact prefix matching.
2. (Beginner) Detect DES. Add the untagged case requiring exactly 13 characters from the crypt alphabet. Example: a 13-char alphanumeric string -> 1; a 13-char string containing $ -> 0. Concepts: shape validation.
3. (Intermediate) Weakness policy. Implement int is_weak_shadow_hash(const char *h) flagging DES and md5crypt, and write the policy rationale as a comment. Concepts: encoding policy explicitly.
4. (Intermediate) Full triage. Extend it to report four categories — weak, acceptable, locked (*/!), and empty (critical) — over a multi-line fixture. Concepts: complete case coverage.
crypt(3) hashes announce their algorithm with a $id$ tag — $1$ md5crypt, $5$/$6$ SHA-crypt, $2a$/$2b$/$2y$ bcrypt, $7$/$y$ scrypt-family — while an untagged 13-character string from the crypt alphabet is traditional DES, so the absence of a marker is itself the identifying feature. Classification is exact prefix matching, with the DES case requiring both the length and the alphabet. Handle the special values separately: * and ! mean the account cannot log in with a password (not weak), and an empty field means no password is required at all, which is the most serious finding. Keep the weakness policy in one reviewable place, and report schemes and accounts rather than the hash values.