수정 이력(4건, 최종 수정 2026년 09월 03일)
이 글에 적용한 변경 사항의 기록입니다. 보관해 둔 수정 전 버전은 DOI가 부여된 고정 URL에서 읽을 수 있습니다.
- permalink·저자 표기·지식 맵 래퍼·깨진 내부 링크 등 CI가 지적한 표시용 수정을 반영했습니다. 본문의 기술적인 주장은 바꾸지 않았습니다.
- 글 맨 앞에 「이 글의 지식 맵」 절을 추가했습니다. 본문에서 다루는 개념과 그 관계를 요약·그림·상세 페이지 링크로 정리한 것입니다. 본문의 주장은 바꾸지 않았습니다.
- 외부 리뷰(1283건)에 맞춰 본문을 업데이트했습니다. 개별 변경 내용은 아래 이력을 참조하십시오.
- 서두와 본문에, 허가된 환경에서만 조사하라는 주의를 추가했습니다. 식별한 형식을 검증하는 절차(`hashlib`로 대조, `openssl passwd`로 재계산)를 보강하고, 수록한 예가 실제로 재현되는지를 로컬에서 확인했습니다. hashcat 모드 번호 표, Base64 문자 수 읽는 법, 용어 미니 사전도 추가했습니다. 아울러 「NTLM계」로 적었던 부분을 「NT 해시(MD4 기반)」로 고치고, 둘을 같은 것으로 다루는 오류를 설명하는 절을 새로 넣었습니다.
- 최초 공개
이 글을 인용하기(DOI: 10.5281/zenodo.21635248)
이 글은 Zenodo에 보관되어 있습니다. 항상 최신 버전으로 연결되는 DOI와 지금 보고 있는 버전에 고정된 DOI를 아래에 함께 제시합니다.
Go Komura (2026). 「해시 문자열에서 방식을 가려내는 실무 절차」. 합동회사 코무라소프트. https://doi.org/10.5281/zenodo.21635248 https://comcomponent.com/ko/blog/hash-format-identification/
- DOI(최신 버전)
- 10.5281/zenodo.21635248
- DOI(이 버전)
- 10.5281/zenodo.21635249
로그나 DB에 남아 있는 5f4dcc3b5aa765d61d8327deb882cf99나 $2b$12$... 같은 문자열을 보고, 「이것은 무슨 해시인가」를 가려내고 싶은 상황은 꽤 있습니다. 기존 시스템 이전, 인증 방식 조사, 로그 분석, 타사 시스템 연동에서는 여기서 막히는 일이 드물지 않습니다.
다만 여기서 위험한 것은 길이만 보고 바로 단정하는 것입니다.
64자의 16진 문자열을 보고 「SHA-256입니다」라고 단언하는 것은 이릅니다. SHA3-256, SHA-512/256, BLAKE2s-256, BLAKE3의 기본 32-byte 출력도 같은 길이가 될 수 있습니다. 반대로 $2b$나 $argon2id$처럼 prefix와 파라미터까지 포함한 저장 형식은 문자열만으로도 꽤 높은 정확도로 식별할 수 있습니다.
이 글에서는 hash라는 말을 넓게 써서, MD5 / SHA-2 / SHA-3 같은 메시지 digest뿐 아니라 bcrypt / scrypt / Argon2 / PBKDF2 같은 비밀번호 저장용 문자열 표현도 포함합니다.
내용은 2026년 4월 시점에 공개된 RFC, NIST, Linux crypt(5), Apache, Django, Spring Security 등의 공식 자료를 바탕으로 정리했습니다.
대상 독자는 기존 시스템 이전, 인증 방식 조사, 로그 분석을 담당하며, 손에 있는 「해시처럼 보이는 문자열」이 무엇인지 가려내고 싶은 분입니다. 전제는 16진 표기와 Base64가 무엇인지를 아는 정도면 충분합니다.
이 글의 절차는 직접 관리하는 환경, 또는 조사를 명시적으로 허가받은 환경의 문자열에만 사용하십시오. 다른 사람의 계정 비밀번호 해시를 꺼내거나, 권한이 없는 시스템의 해시를 분석하는 행위는 목적이 조사여도 정당화되지 않습니다. 후반의 검증 명령도 테스트용 계정이나 직접 만든 샘플로 시험하는 것을 전제로 합니다.
목차
- 먼저 결론
- 한눈에 보는 식별표
- 실무 식별 절차
- 흔한 오식별
- 100% 확정하고 싶을 때의 확인 순서
- 정리
- 이 주제와 이어지는 서비스
- 참고 자료
이 글의 지식 맵
이 글은 로그나 DB에 남는 해시 문자열의 방식을 가려내는 절차를 다루며, bcrypt나 Argon2처럼 맨 앞에 방식 이름을 두는 MCF나 PHC string format 계열의 저장 형식은 문자열만으로 거의 특정할 수 있는 반면, 접두사 없는 16진이나 Base64만으로 된 문자열은 길이와 문자 종류로 후보를 좁히는 데 그친다고 정리합니다. sha256crypt·sha512crypt·md5crypt는 모두 crypt(3) 함수의 출력 형식으로 SHA-256·SHA-512·MD5를 반복 적용하며, openssl passwd에 같은 salt를 주어 재계산하면 형식을 검증할 수 있습니다. 또한 64자리 16진은 SHA-256뿐 아니라 여러 알고리즘이 후보가 되므로 길이만으로 단정해서는 안 되며, NT 해시는 MD5가 아니라 MD4를 바탕으로 계산된다는 점에서 NTLM과도 구별해야 한다고 합니다. 후보 방식이 파악된 뒤에는 hashcat의 모드 번호로 대응시켜 조사를 진행합니다.
flowchart LR
accTitle: 해시 형식 식별의 지식 맵
accDescr: 해시 문자열을 판별하는 단서로 접두사 기반 저장 형식인 MCF·PHC string format과 문자 종류·길이만으로 하는 추정이 어떻게 관련되는지, crypt(3) 계열의 md5crypt·sha256crypt·sha512crypt, bcrypt, Argon2 같은 구체적 형식과 NT 해시와 NTLM의 차이, hashcat 및 openssl passwd로 검증하는 수단까지를 보여주는 그림
password_hash_string["비밀번호 해시 문자열"]
length_based_misidentification["길이만으로 인한 오판"]
crypt_3_function["crypt(3)"]
modular_crypt_format["MCF(Modular Crypt Format)"]
phc_string_format["PHC string format"]
argon2["Argon2"]
bcrypt["bcrypt"]
sha256crypt["sha256crypt"]
sha512crypt["sha512crypt"]
md5crypt["md5crypt"]
sha_256["SHA-256"]
sha_512["SHA-512"]
md5["MD5"]
hex_encoding["16진 표현(hex encoding)"]
nt_hash["NT 해시"]
md4["MD4"]
ntlm["NTLM"]
openssl_passwd["openssl passwd"]
hashcat["hashcat"]
pbkdf2["PBKDF2"]
scrypt["scrypt"]
ldap_scheme_prefix["LDAP 스킴 접두사({SHA} 등)"]
base64_encoding["Base64(RFC 4648)"]
sha_1["SHA-1"]
crypt_3_function -->|"이용한다"| modular_crypt_format
phc_string_format -->|"의 후속"| modular_crypt_format
argon2 -->|"이용한다"| phc_string_format
bcrypt -->|"이용한다"| modular_crypt_format
sha256crypt -->|"이용한다"| crypt_3_function
sha512crypt -->|"이용한다"| crypt_3_function
md5crypt -->|"이용한다"| crypt_3_function
sha256crypt -->|"이용한다"| sha_256
sha512crypt -->|"이용한다"| sha_512
md5crypt -->|"이용한다"| md5
hex_encoding -.->|"원인이 될 수 있다"| length_based_misidentification
modular_crypt_format -->|"방지한다"| length_based_misidentification
nt_hash -->|"이용한다"| md4
ntlm -->|"이용한다"| nt_hash
sha512crypt -->|"에서 확인할 수 있다"| openssl_passwd
sha256crypt -->|"에서 확인할 수 있다"| openssl_passwd
md5crypt -->|"에서 확인할 수 있다"| openssl_passwd
md5 -->|"에서 구성할 수 있다"| hashcat
sha_256 -->|"에서 구성할 수 있다"| hashcat
sha512crypt -->|"에서 구성할 수 있다"| hashcat
bcrypt -->|"에서 구성할 수 있다"| hashcat
nt_hash -->|"에서 구성할 수 있다"| hashcat
pbkdf2 -->|"에서 구성할 수 있다"| hashcat
scrypt -->|"에서 구성할 수 있다"| hashcat
ldap_scheme_prefix -.->|"이용한다"| base64_encoding
ldap_scheme_prefix -.->|"이용한다"| sha_1
ldap_scheme_prefix -->|"방지한다"| length_based_misidentification
그림의 실선은 항상 성립하는 관계, 점선은 조건이 붙는 관계입니다(성립 조건은 상세 페이지의 관계별 설명에 적혀 있습니다). 관계 전체 목록(총 27건, 근거와 확신도 포함)과 주요 개념의 정의는 지식 맵 상세 페이지에 정리되어 있습니다(일본어). 데이터: JSON-LD / Turtle
1. 먼저 결론
먼저 결론만 짧게 정리합니다.
-
prefix나 구분자가 있는 저장 형식은 문자열만으로도 꽤 식별하기 쉽습니다. 예:
$argon2id$...,$2b$...,$5$...,$6$...,{SHA}...,pbkdf2_sha256$... -
평범한 16진 문자열이나 Base64만이면 대개 「후보를 좁히는」 데까지입니다. 예:
32 hex = MD5일 수도 있지만, MD4나 NT 해시에서 왔을 수도 있다 -
문자 종류는 길이만큼 중요합니다.
+/=가 있으면 RFC 4648의 Base64로 보이고,.가 들어 있고$로 구분되면crypt(3)계통으로 보이는 식의 구분이 됩니다. -
100% 확정하려면 맥락이 필요합니다.
/etc/shadow인지,.htpasswd인지, Django의auth_user인지, Spring Security인지에 따라 이야기가 달라집니다.
요점은, 「문자열만으로 식별할 수 있는 방식」과 「문자열만으로는 후보군까지밖에 모르는 방식」은 별개라는 것입니다. 여기를 나눠 생각하면 조사의 진행 방식이 달라집니다.
2. 한눈에 보는 식별표
표를 읽기 전에, 이 글에서 「형식 이름」으로 나오는 말을 4가지만 정리합니다. 비슷한 맥락에서 쓰이지만 가리키는 것이 다릅니다.
| 용어 | 풀어 쓴 이름 | 무엇을 가리키는가 |
|---|---|---|
crypt(3) |
- | Unix 비밀번호 해시 함수 자체. C 라이브러리 함수 이름이며, man 3장(라이브러리 함수)에 실려 있어 이렇게 씁니다 |
crypt(5) |
- | 그 함수가 읽고 쓰는 문자열 형식을 설명한 man 페이지. man 5장(파일 형식)이며, $6$salt$hash 같은 표기는 여기에 나옵니다 |
| MCF | Modular Crypt Format | 「앞에 $id$를 두어 방식을 나타낸다」는 표기법의 통칭. 단일 명세가 있는 것은 아니고, crypt(3) 계 구현이 늘어나는 과정에서 관습으로 굳은 이름입니다 |
| PHC string format | Password Hashing Competition string format | MCF를 다시 정리한 명세. $argon2id$v=19$m=65536,t=3,p=4$salt$hash처럼 버전과 파라미터 표기까지 정해 두었습니다 |
거칠게 말하면, crypt(3)은 함수, crypt(5)는 그 출력 형식의 명세, MCF는 그 형식의 통칭, PHC string format은 그것을 명문화해 다시 적은 것입니다. 이후 표에서 「PHC string format」「crypt 계」라고 쓸 때는 이 구분으로 읽으십시오.
2.1 prefix나 형식 마커로 거의 식별할 수 있는 것
표의 「식별 강도」는 다음 의미입니다.
- 강: 문자열만으로 거의 식별할 수 있다
- 중: 후보는 꽤 좁혀지지만, 구현 차이에 주의가 필요하다
- 약: 길이나 겉모습만으로는 단정할 수 없다
| 겉모습의 특징 | 먼저 의심할 방식 | 식별 강도 | 보충 | 예 |
|---|---|---|---|---|
$argon2id$... |
Argon2id | 강 | PHC string format. v=, m=, t=, p=가 이어지는 경우가 많음 |
$argon2id$v=19$m=65536,t=3,p=4$MDEyMzQ1Njc4OWFiY2RlZg$uKZLaN6muIyoyIYr5waqw3y+zaDbe9aLSPj6Ln/rbz4 |
$argon2i$... |
Argon2i | 강 | 위와 같음 | $argon2i$v=19$m=65536,t=3,p=4$MDEyMzQ1Njc4OWFiY2RlZg$Kx1koF/7n8EytGJYTS5krh+ag+FlG5ksM4xOsjOSDvo |
$argon2d$... |
Argon2d | 강 | 위와 같음 | $argon2d$v=19$m=65536,t=3,p=4$MDEyMzQ1Njc4OWFiY2RlZg$HLIGA+T1bwK8akx3LGOco+Df+PvxX6cIXhycO7O7t6c |
$2a$... / $2b$... / $2y$... |
bcrypt | 강 | 2자리 cost + crypt 계 alphabet | $2b$12$9YQ2u/e5Y/ArOnG.gJKxK.0makLATcYLP1q.Nsabzrw7XErYCfoYO |
$1$... |
md5crypt | 강 | Unix 계 MD5 비밀번호 저장 형식 | $1$vA7mQ9xZ$Erz32JUFnZ9991KdU5.N3. |
$5$... |
sha256crypt | 강 | plain SHA-256이 아님 | $5$rounds=5000$N3v8Kx2Lq9Rt$uOTla5GAHaRH2aHlUSjkrZUBCuFiahQZ36O/seB39r3 |
$6$... |
sha512crypt | 강 | plain SHA-512가 아님 | $6$rounds=5000$N3v8Kx2Lq9Rt$6LUcSUAELX3aC/.60pTB.TFLTQi1mOGRCwKqNCqtRSaXjorxj01HJ9oNni97Kci1uDt7a/Kn4t3OS20Dw/.vi1 |
$7$... |
scrypt (crypt 계) | 강 | Linux crypt(5) 계 구현에서 볼 수 있음 |
$7$CU..../....k2XAnEHBqQ1Ct2aMXFKNa/$y3Q0e/UlCHacIGWQshgvvz6UIbP.BCja.5BfVWP2Ml8 |
$y$... |
yescrypt | 강 | 비교적 새로운 Linux 계에서 볼 수 있음 | $y$j9T$k2XAnEHBqQ1Ct2aMXFKNa/$OVYXzjlkiQpWT/F1CUE0JrvV4phLY8FB.ofDttnrSQ7 |
$apr1$... |
Apache APR1-MD5 | 강 | .htpasswd에서 자주 봄 |
$apr1$vA7mQ9xZ$ZE64.ohiyK11sPZmtnJZQ. |
{SHA}... |
SHA-1 digest의 Base64 표현 | 강 | Apache / LDAP 계에서 자주 봄 | {SHA}VBPuJHI7uixaa6LQGWx4s+5GKNE= |
{SSHA}... |
salted SHA-1 | 강 | LDAP 계 | {SSHA}/OczD0GNNkOAUPbYhA3L9fjmcyBCbHVlTWVzYTQyIQ== |
{MD5}... / {SMD5}... |
MD5 / salted MD5 | 강 | LDAP 계 | {MD5}X03MO1qnZdYdgyfeuILPmQ=={SMD5}fOn1rOv4ZH0OrO/KT9H0fEJsdWVNZXNhNDIh |
pbkdf2_sha256$... |
PBKDF2-HMAC-SHA256 | 중~강 | Django 등, 구현이 형식 이름을 앞에 붙임 | pbkdf2_sha256$600000$N3v8Kx2Lq9Rt$CLxGB+zTiV1IdOt2y4m9JpaAONzHuRTOd96xKQwRQAs |
{bcrypt}$2b$... |
bcrypt | 강 | Spring Security의 {id} 래퍼 포함 |
{bcrypt}$2b$12$9YQ2u/e5Y/ArOnG.gJKxK.0makLATcYLP1q.Nsabzrw7XErYCfoYO |
{pbkdf2}... / {scrypt}... |
구현 레이블이 붙은 방식 | 중~강 | Spring Security 등, 알고리즘 자체보다 래퍼 형식을 가려낸다 | {pbkdf2}sha256$600000$Qmx1ZU1lc2E0MiE$4eNuai1qNkgs1kXz3+tBUMzAexVsSUz9SrQKEhbk0Cw{scrypt}ln=14,r=8,p=1$Qmx1ZU1lc2E0MiE$xAgBRhXbMtHB1UHUR0br5bI+1XdXWKbwauiFv5VRQBY |
이 표의 요점은, 앞의 몇 글자에 의미가 있는 형식은 강하다는 것입니다.
특히 $...$로 구분된 것은 Unix crypt(3) / MCF / PHC 계일 가능성이 높고, 길이보다 먼저 prefix를 보는 편이 빠릅니다.
2.2 평범한 16진 / Base64의 길이로 후보를 좁히는 표
이쪽은 prefix 없는 「그냥 digest 문자열」을 볼 때의 표입니다.
:나 -, 공백이 섞인 표현은 먼저 구분자를 제거하고 길이를 셉니다.
| raw 바이트 길이 | 16진 문자 수 | Base64 문자 수 (= 있음 / 없음) |
주 후보 | 예 |
|---|---|---|---|---|
| 4 | 8 | 8 / 6 | CRC32 등의 checksum | cbf43926 |
| 16 | 32 | 24 / 22 | MD5, MD4, NT 해시(MD4 기반) | 5f4dcc3b5aa765d61d8327deb882cf99 |
| 20 | 40 | 28 / 27 | SHA-1, RIPEMD-160 | da39a3ee5e6b4b0d3255bfef95601890afd80709 |
| 28 | 56 | 40 / 38 | SHA-224, SHA-512/224, SHA3-224 | d14a028c2a3a2bc9476102bb288234c415a2b01f828ea62ac5b3e42f |
| 32 | 64 | 44 / 43 | SHA-256, SHA-512/256, SHA3-256, BLAKE2s-256, BLAKE3의 기본 32-byte 출력 | e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 |
| 48 | 96 | 64 / 64 | SHA-384, SHA3-384, BLAKE2b-384 | 38b060a751ac96384cd9327eb1b1e36a21fdb71114be07434c0cc7bf63f6e1da274edebfe76f65fbd51ad2f14898b95b |
| 64 | 128 | 88 / 86 | SHA-512, SHA3-512, BLAKE2b-512, Whirlpool | cf83e1357eefb8bdf1542850d66d8007d620e4050b5715dc83f4a921d36ce9ce47d0d13c5d85f2b0ff8318d2877eec2f63b931bd47417a81a538327af927da3e |
「Base64 문자 수」열을 읽는 법: 이 열은 = 패딩을 포함한 경우 / 뺀 경우 두 가지를 나란히 적었습니다. RFC 4648의 Base64는 4자 단위로 맞추므로, raw 바이트 길이가 3의 배수가 아닐 때만 끝에 =가 1~2개 붙습니다. JWT나 URL 삽입에서는 이 =를 떨어뜨리는 구현이 많아서, 같은 digest라도 43자와 44자 둘 다 있을 수 있습니다. 반대로 48바이트처럼 3의 배수이면 패딩이 생기지 않으므로 64 / 64처럼 같은 숫자가 나란히 옵니다. 길이로 후보를 좁힐 때는 반드시 양쪽 숫자와 대조하십시오.
여기서 중요한 것은, 길이가 맞아도 방식이 하나로 정해지지 않는다는 점입니다.
특히 32 / 64 / 128자 16진은 후보가 많아, 이것만으로 단정하면 빗나가기 쉽습니다.
2.3 헷갈리기 쉬운 대표 예
| 문자열의 모습 | 흔한 바로 단정 | 실제 보는 법 | 예 |
|---|---|---|---|
5f4dcc3b5aa765d61d8327deb882cf99 |
MD5로 확정 | MD5처럼 보이지만, MD4나 NT 해시, 앱 고유의 MD5 이용도 있을 수 있음 | 8846f7eaee8fb117ad06bdd830b7586c |
2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824 같은 64 hex |
SHA-256으로 확정 | SHA-256 후보이기는 하나, SHA3-256 / SHA-512/256 / BLAKE2s-256 / BLAKE3도 있을 수 있음 | e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 |
$6$rounds=5000$salt$hash |
SHA-512의 hex 표현 | 그게 아니라 sha512crypt라는 password hash 문자열 | $6$rounds=5000$N3v8Kx2Lq9Rt$6LUcSUAELX3aC/.60pTB.TFLTQi1mOGRCwKqNCqtRSaXjorxj01HJ9oNni97Kci1uDt7a/Kn4t3OS20Dw/.vi1 |
{SHA}VBPuJHI7uixaa6LQGWx4s+5GKNE= |
무언가의 「SHA」 | Apache / LDAP 계에서는 Base64로 만든 SHA-1 digest를 가리키는 경우가 많음 | {SHA}VBPuJHI7uixaa6LQGWx4s+5GKNE= |
{bcrypt}$2b$12$... |
{bcrypt}라는 고유 방식 |
Spring Security 래퍼가 붙은 bcrypt | {bcrypt}$2b$12$9YQ2u/e5Y/ArOnG.gJKxK.0makLATcYLP1q.Nsabzrw7XErYCfoYO |
3. 실무 식별 절차
여기서부터는 실제로 어떻게 볼지를 순서대로 정리합니다. 추천 순서는 prefix → 구분자 → 문자 종류 → 길이 → 맥락입니다.
3.1 먼저 앞부분 기호를 본다
처음 1자에서 10자 정도로 꽤 좁힐 수 있습니다.
-
$argon2id$/$argon2i$/$argon2d$Argon2의 PHC string format을 강하게 의심합니다. 구성 요소는 2.1의예열을 보면 따라가기 쉽습니다. -
$2a$/$2b$/$2y$bcrypt를 강하게 의심합니다. -
$1$/$5$/$6$/$7$/$y$Unixcrypt(3)계 password hash를 의심합니다. -
{SHA}/{SSHA}/{MD5}/{SMD5}LDAP / Apache 계 표현을 의심합니다. -
{bcrypt}/{pbkdf2}/{scrypt}Spring Security처럼 구현 레이블이 붙은 저장 형식을 의심합니다.
여기서의 요령은, 「알고리즘 자체」뿐 아니라 「저장 형식」도 보는 것입니다.
예를 들어 $6$는 「SHA-512의 digest」가 아니라 「SHA-512를 쓴 password hash 문자열」입니다. 여기를 혼동하면 이후 조사가 어긋납니다.
3.2 구분자 개수를 본다
다음으로 $, :, {}, ,, = 같은 구분자를 봅니다.
-
$가 여러 개 있다 파라미터, salt, hash를 함께 갖는 형식을 의심합니다. Argon2, bcrypt, sha256crypt, sha512crypt 등이 전형입니다. -
{name}으로 시작한다 LDAP / Spring Security처럼 방식 이름을 명시한 래퍼를 의심합니다. -
algo:salt:hash나algo$iterations$salt$hash같은 형태 프레임워크나 앱 고유 형식을 의심합니다. Django의pbkdf2_sha256$iterations$salt$hash가 그 전형입니다.
구분자가 많은 문자열일수록 방식을 식별하기 쉽습니다. 반대로 그냥 16진이나 Base64가 한 덩어리로만 있으면 꽤 모호해집니다.
3.3 문자 종류를 본다
문자 종류는 길이만큼 중요합니다.
16진 표현
[0-9a-fA-F]만으로 되어 있으면 우선 16진 표현을 의심합니다.
이 경우 문자 수 ÷ 2 = raw 바이트 길이입니다.
- 32 hex → 16 bytes
- 40 hex → 20 bytes
- 64 hex → 32 bytes
- 128 hex → 64 bytes
RFC 4648의 Base64 / Base64url
+ / =가 있으면 우선 일반 Base64를 의심합니다.
- _가 있으면 Base64url을 의심합니다.
padding의 =가 생략되기도 하므로 43 / 44, 86 / 88처럼 「둘 다 가능한 길이」를 갖습니다.
crypt 계 radix64
.와 /가 나오고, 게다가 $...$로 구분되면, 일반 Base64보다 crypt 계 alphabet을 의심하는 편이 자연스럽습니다.
bcrypt, sha256crypt, sha512crypt, md5crypt, yescrypt, scrypt 등은 이 계통의 문자 집합을 씁니다.
여기는 눈에 띄지 않지만 꽤 효과가 있습니다.
.가 들어 있으니 깨진 Base64다고 보면 bcrypt나 crypt(3) 계를 놓치기 쉽습니다.
3.4 길이를 센다
문자 종류를 본 다음이 길이입니다. 생각은 단순합니다.
- 16진이면
raw 바이트 길이 = 문자 수 / 2 - Base64이면
문자 수 ≒ 4 × ceil(raw 바이트 길이 / 3)다만 padding=가 생략되면 0~2자 짧아집니다
이 단계에서 후보를 좁힙니다. 다만 64 hex를 보고 SHA-256으로 확정 같은 비약은 하지 않는 편이 안전합니다.
3.5 맥락으로 확정한다
마지막에 효력이 있는 것은 맥락입니다. 여기서 100%에 가까워집니다.
-
/etc/shadow에 있다$y$,$6$,$5$,$1$등 Linux password hash 형식을 의심한다 -
.htpasswd에 있다$apr1$,{SHA}, bcrypt 등 Apache 계를 의심한다 -
Django 설정이나
auth_user.password에 있다pbkdf2_sha256$...나argon2$...같은 Django 형식을 의심한다 -
Spring Security 인증 테이블에 있다
{bcrypt}...나{pbkdf2}...처럼{id}가 붙은 형식을 의심한다 -
SMB / AD 연동 주변의 32 hex NT 해시(MD4 기반) 가능성을 강하게 본다
실무에서는 문자열 자체만 보는 것보다, 저장원의 제품·프레임워크·설정 파일 이름을 보는 편이 빠른 경우가 적지 않습니다.
4. 흔한 오식별
4.1 64 hex = SHA-256으로 못 박는다
꽤 흔합니다. 물론 SHA-256은 유력 후보이지만, 같은 32-byte 출력을 갖는 방식은 여러 가지입니다. SHA3-256, SHA-512/256, BLAKE2s-256, BLAKE3의 기본 출력 등도 같은 길이입니다.
길이는 후보군을 만드는 재료이지, 확정 재료가 아닙니다.
4.2 $6$를 plain SHA-512로 오해한다
$6$...는 sha512crypt의 prefix입니다.
이것은 「SHA-512의 hex digest」가 아니라, salt나 rounds를 포함한 password hash 문자열입니다.
마찬가지로,
$5$는 sha256crypt$1$는 md5crypt
입니다. prefix가 붙어 있는 시점에서 이미 「그냥 digest」가 아닙니다.
4.3 {SHA}를 「SHA-256이나 SHA-512 중 하나」로 생각한다
Apache나 LDAP 맥락에서 {SHA}는 막연한 「SHA 계」가 아닙니다.
많은 경우 Base64로 만든 SHA-1 digest를 가리킵니다. {SSHA}는 salted SHA-1입니다.
{SHA} 겉모습만으로 「무언가의 SHA」로 모호하게 다루면 검증 코드나 이전 처리를 틀립니다.
4.4 비밀번호 해시와 콘텐츠 해시를 같은 것으로 다룬다
같은 「해시 문자열」이어도 목적이 다릅니다.
- 파일 무결성 확인용 digest
- API 서명용 digest
- 비밀번호 저장용 hash / KDF 문자열
이 셋은 겉모습이 비슷해도 다루는 방법이 다릅니다. 특히 password hash는 salt, rounds, memory cost, parallelism 등을 문자열에 포함하는 경우가 많아, 「생 digest를 비교한다」는 발상으로는 알아내지 못합니다.
4.5 XOF나 가변 길이 digest를 잊는다
SHAKE128 / SHAKE256은 XOF이므로 출력 길이를 자유롭게 고를 수 있습니다. BLAKE2도 digest length를 바꿀 수 있고, BLAKE3도 extendable output을 갖습니다.
즉 「이 길이이므로 이 방식」이라는 추정은, 고정 길이의 고전 digest를 너무 전제하면 빗나갑니다.
4.6 「NTLM」과 「NT 해시」를 같은 것으로 다룬다
용어 이야기이지만, Windows / AD 주변 조사에서는 꽤 효과가 있습니다.
- NT 해시: 비밀번호를 UTF-16LE로 인코딩한 뒤 MD4에 넣은 16바이트 값입니다.
8846f7eaee8fb117ad06bdd830b7586c처럼 32자 16진으로 나타납니다. 명세에서도NTOWFv1(Passwd, User, UserDom) = MD4(UNICODE(Passwd))로 정의되어 있습니다. 이 정의는 참고 자료 13에 든 MS-NLMP의 NTLM v1 Authentication 항에 그대로 나옵니다. - NTLM: 그 값을 키로 써서 챌린지 / 응답을 하는 인증 프로토콜 이름입니다. 문자열의 형식 이름이 아닙니다.
현장에서는 「NTLM 해시」라는 말도 통하지만, 식별표에 내릴 때는 NT 해시(MD4 기반)라고 적는 편이 정확합니다. 여기를 나눠 두면, 「이 32 hex는 MD5인가 NT 해시인가」라는 형식 식별과, 「이 통신은 NTLM인가 Kerberos인가」라는 프로토콜 이야기가 섞이지 않습니다.
참고로 NT 해시에는 salt가 없습니다. 같은 비밀번호면 항상 같은 32 hex가 되므로, 「salt가 없는 32 hex가 AD에서 온 테이블에 늘어서 있다」는 것 자체가 단서가 됩니다.
5. 100% 확정하고 싶을 때의 확인 순서
이전이나 인증 연동에서는 최종적으로 확정이 필요합니다. 그때는 다음 순서로 보면 사고가 나기 어렵습니다.
5.1 저장원을 특정한다
먼저 문자열이 어디서 왔는지를 확정합니다.
- Linux의 shadow인가
- Apache / Nginx의 basic auth인가
- LDAP인가
- Django / Spring Security인가
- 자체 앱의 DB인가
문자열 단독보다 저장원 사양이 더 강한 경우가 많습니다.
5.2 공식 문서에서 「저장 형식」을 조사한다
다음으로 알고리즘 이름이 아니라 저장 형식을 조사합니다.
Django password formatSpring Security password storage formatcrypt(5) sha512crypt formatApache htpasswd password formats
처럼 format / storage / encoding을 키워드로 쓰면 찾기 쉽습니다.
5.3 알려진 평문이 있으면 후보 방식으로 실제로 대조한다
테스트용 계정이나 알려진 평문이 있으면, 후보 방식으로 실제로 계산해 비교하는 편이 빠릅니다. 이때 password hash에서는 salt나 rounds를 문자열에서 꺼내 재계산해야 합니다.
절차는 어느 방식이든 같은 3단계입니다.
- 문자열에서 salt와 파라미터를 꺼낸다
- 알려진 평문에 대해 같은 salt와 파라미터로 재계산한다
- 나온 문자열이 원래 문자열과 완전히 일치하는지 본다
prefix 없는 digest를 확인한다
우선 「그냥 digest」부터입니다. Python 3 표준 라이브러리만으로 충분합니다.
# Python 3.8 이후 / 표준 라이브러리만
import base64
import hashlib
target = "5f4dcc3b5aa765d61d8327deb882cf99" # 식별하고 싶은 문자열
plain = b"password" # 알려진 평문
for name in ("md5", "sha1", "sha256", "sha512", "sha3_256", "blake2s"):
digest = hashlib.new(name, plain).digest()
if digest.hex() == target.lower():
print("hex 일치:", name)
if base64.b64encode(digest).decode() == target:
print("base64 일치:", name)
이 예라면 hex 일치: md5가 나옵니다. 2.2 표에서 후보로 든 방식을 그대로 for 안에 나열하는 것이 기본형입니다.
NT 해시를 시험하고 싶어지면 hashlib.new("md4", "password".encode("utf-16-le"))처럼 쓰게 되지만, OpenSSL 3 계에 링크된 Python에서는 legacy provider가 기본으로 꺼져 있어 unsupported hash type md4가 될 수 있습니다. 여기는 먼저 로컬 환경에서 확인해 두십시오.
crypt 계 비밀번호 해시를 확인한다
$1$ / $5$ / $6$ / $apr1$는 openssl passwd에 salt를 넘기면 재계산할 수 있습니다. 2.1 표에 실은 예도 이 방법으로 재현할 수 있습니다.
# OpenSSL 3.x
openssl passwd -6 -salt N3v8Kx2Lq9Rt password
# $6$N3v8Kx2Lq9Rt$6LUcSUAELX3aC/.60pTB.TFLTQi1mOGRCwKqNCqtRSaXjorxj01HJ9oNni97Kci1uDt7a/Kn4t3OS20Dw/.vi1
openssl passwd -5 -salt N3v8Kx2Lq9Rt password # sha256crypt
openssl passwd -1 -salt vA7mQ9xZ password # md5crypt
openssl passwd -apr1 -salt vA7mQ9xZ password # Apache APR1-MD5
출력이 식별 대상과 일치하면, 그 시점에서 방식과 평문 둘 다 확정됩니다.
rounds=가 붙은 문자열은 그 값도 함께 넘겨야 합니다. $6$rounds=5000$...는 기본값이므로 명시하든 안 하든 같은 digest가 되지만, rounds=100000처럼 기본과 다른 값이면 반드시 그 값으로 계산하십시오.
Debian / Ubuntu 계라면 whois 패키지에 들어 있는 mkpasswd로도 같은 일을 할 수 있습니다(mkpasswd -m sha512crypt -S N3v8Kx2Lq9Rt password 형태입니다).
bcrypt나 Argon2를 확인한다
bcrypt나 Argon2는 salt가 독자 alphabet으로 인코딩되어 있어, 손으로 잘라 내기보다 라이브러리의 verify에 문자열을 통째로 넘기는 편이 확실합니다.
# pip install "passlib[bcrypt]" argon2-cffi
from passlib.hash import argon2, bcrypt
samples = [
(bcrypt, "$2b$12$9YQ2u/e5Y/ArOnG.gJKxK.0makLATcYLP1q.Nsabzrw7XErYCfoYO"),
(argon2, "$argon2id$v=19$m=65536,t=3,p=4$MDEyMzQ1Njc4OWFiY2RlZg$uKZLaN6muIyoyIYr5waqw3y+zaDbe9aLSPj6Ln/rbz4"),
]
for handler, stored in samples:
# identify 는 그 형식인지, verify 는 평문과 일치하는지를 반환합니다
print(handler.name, handler.identify(stored), handler.verify("password", stored))
verify는 cost나 rounds, salt를 문자열 쪽에서 읽어 재계산해 주므로, 파라미터 추출을 직접 쓰지 않아도 됩니다. 위의 bcrypt 예는 평문이 password이므로 True가 반환됩니다.
참고로 Python 표준 crypt 모듈은 deprecated되었고, Python 3.13에서 삭제되었습니다. 3.13 이후 crypt 계를 다루려면 openssl passwd나 passlib 쪽으로 옮기십시오.
5.4 후보 방식을 hashcat 번호나 john 이름으로 바꾼다
평문을 모르고 도구 쪽에서 다루고 싶을 때는, 방식 이름이 아니라 도구가 쓰는 식별자로 옮겨야 합니다. 여기서 자주 막히므로 대표적인 것을 표로 둡니다.
| 겉모습 | 방식 | hashcat 모드(-m) |
|---|---|---|
| 32 hex | MD5 | 0 |
| 32 hex(AD 유래) | NT 해시 | 1000 |
| 40 hex | SHA-1 | 100 |
| 64 hex | SHA-256 | 1400 |
| 128 hex | SHA-512 | 1700 |
$1$... |
md5crypt | 500 |
$apr1$... |
Apache APR1-MD5 | 1600 |
$2a$ / $2b$ / $2y$ |
bcrypt | 3200 |
$5$... |
sha256crypt | 7400 |
$6$... |
sha512crypt | 1800 |
pbkdf2_sha256$... |
Django PBKDF2-HMAC-SHA256 | 10000 |
| scrypt 계 저장 형식 | scrypt | 8900 |
모드 번호는 버전이 오를 때마다 늘어나므로, Argon2나 yescrypt처럼 비교적 새로운 방식은 로컬 hashcat 도움말에 나오는 목록으로 확인하십시오.
John the Ripper 쪽은 숫자가 아니라 이름으로 지정합니다. 기본 빌드(1.8.0)에서 유효한 것은 descrypt, bsdicrypt, md5crypt, bcrypt, LM, AFS, tripcode, dummy, crypt이고, 이외의 다수는 jumbo 판에서 추가됩니다. 쓰고 싶은 방식 이름이 기본 빌드에 없으면 jumbo 판 문서에서 확인하십시오.
반복하지만, 여기에 든 도구는 직접 관리하는 환경, 또는 조사 허가를 받은 환경에 대해서만 사용하십시오.
5.5 구현 코드나 설정을 확인한다
조사 대상이 자사 시스템이라면, 최종적으로는 코드나 설정을 보는 것이 가장 확실합니다.
- 사용 라이브러리
- 프레임워크 설정
- 생성 시 옵션
- 출력 인코딩 방식(hex / Base64 / Base64url / crypt alphabet)
여기를 보면 대개 결론이 납니다.
5.6 앞날을 위해 방식 레이블을 붙여 저장한다
이제부터 설계하는 쪽이라면, 방식을 문자열에 심는 형식을 고르면 이후 이전이 꽤 편해집니다.
- Argon2의 PHC string format
- Spring Security의
{id}encodedPassword - Django의
algo$iterations$salt$hash - Unix
crypt(3)계 prefix가 붙은 형식
이렇게 해 두면 나중에 본 사람이 덜 헤맵니다. 반대로 「그냥 64 hex」만 DB에 두는 설계는 미래의 자신에게 친절하지 않습니다.
6. 정리
해시 값의 문자열 표현에서 방식을 가려낼 때는 다음 순서로 보면 정리하기 쉽습니다.
- prefix가 있는가
- 구분자가 무엇인가
- 문자 집합이 무엇인가
- 길이가 몇 바이트에 해당하는가
- 저장원의 맥락이 무엇인가
가장 중요한 것은 다음 두 가지입니다.
- prefix가 있는 저장 형식은 꽤 식별하기 쉽다
- 평범한 16진 / Base64는 후보군까지밖에 모르는 경우가 많다
따라서 실무 판단은 이렇게 됩니다.
$argon2id$...,$2b$...,$6$...,{SHA}...,pbkdf2_sha256$...이면 문자열만으로 꽤 앞으로 나아갈 수 있다32 / 40 / 64 / 128자 16진뿐이면 단정이 아니라 「후보를 좁힌다」고 생각한다- 정말로 확정이 필요하면 저장원의 제품·설정·구현까지 본다
이 순서로 보면 조사가 꽤 빨라집니다. 반대로 길이만 보고 바로 단정하면, 조용히 멀리 돌아갑니다.
7. 이 주제와 이어지는 서비스
기술 상담·설계 리뷰
기존 DB에 남은 비밀번호 해시의 방식 식별, 인증 기반 이전, Windows / Web 혼재 시스템의 로그 조사에서는 문자열 겉모습뿐 아니라 저장원 구현이나 이전 방침까지 정리해야 합니다. 방식 식별부터 이전 설계까지 한꺼번에 보면 사고를 줄이기 쉽습니다.
장애 조사·원인 분석
「이 문자열이 무엇인지 몰라 검증이 진행되지 않는다」는 조사는 드물지 않습니다. 로그, 설정 파일, DB 스키마, 앱 구현의 어디에서 방식이 정해지는지를 나누면 원인 파악이 꽤 빨라집니다.
8. 참고 자료
- RFC 1321 - The MD5 Message-Digest Algorithm
- NIST FIPS 180-4 - Secure Hash Standard (SHA-1, SHA-2, SHA-512/224, SHA-512/256)
- NIST FIPS 202 - SHA-3 Standard: Permutation-Based Hash and Extendable-Output Functions
- PHC string format specification
- Argon2 reference implementation
- RFC 7693 - The BLAKE2 Cryptographic Hash and Message Authentication Code (MAC)
- BLAKE3 C README - default output length and extendable output
- crypt(5) - prefixes and hashed passphrase formats
- Apache HTTP Server 2.4 - Password Formats
- slappasswd(8) - RFC 2307 schemes such as {SHA} and {SSHA}
- Django documentation - example of
pbkdf2_sha256$... - Spring Security -
DelegatingPasswordEncoderstorage format{id}encodedPassword - MS-NLMP: NTLM v1 Authentication -
NTOWFv1(Passwd, User, UserDom) = MD4(UNICODE(Passwd)) - hashcat wiki - example hashes(모드 번호 목록)
- John the Ripper - command line options(
--format=NAME으로 쓰는 이름) - passlib -
PasswordHashAPI(identify와verify) - openssl-passwd(1) -
-1/-apr1/-5/-6과-salt
관련 기사
같은 태그를 공유하는 최신 기사입니다. 더 가까운 주제로 지식을 넓힐 수 있습니다.
Windows 가상화의 심층(제2회) ── 커널에서도 보이지 않는 메모리: VBS·HVCI·Credential Guard의 구조
대응 하드웨어에 클린 설치하면 기본으로 켜지는 VBS는, 하이퍼바이저와 SLAT로 커널보다 강한 격리를 만듭니다. VTL, 보안 커널, HVCI, Credential Guard의 구조를 설명합니다.
Named Pipe 실무 ── Windows 프로세스 간 통신의 정석을 설계부터 보안까지
Windows의 프로세스 간 통신의 정석인 Named Pipe를 실무 관점에서 해설합니다. 바이트/메시지 모드 선택, 여러 클라이언트를 처리하는 서버 설계, ACL과 impersonation 보안, .NET NamedPipeStream까지 1차 ...
Windows 서비스 계정 선정 ── LocalSystem·가상 계정·gMSA를 구분해서 쓰기
Windows 서비스를 아직 LocalSystem으로 돌리고 있지는 않습니까. LocalService·NetworkService·가상 계정·도메인 사용자·gMSA의 권한과 네트워크상 신원을 판단표로 비교하고, 최소 권한으로 운영하는 고르는 법을 ...
Windows 보안 감사 정책과 이벤트 로그 조사 실무 ── 4625를 읽는 정보시스템 담당자가 되기
「로그온 실패 로그를 조사해 달라」는 요청에 답하기 위한 실무 가이드입니다. 기본 감사 정책과 고급 감사 정책의 관계, 최소한 활성화해야 할 하위 범주, 이벤트 ID 4624/4625/4688을 읽는 법, Security 로그의 용량 설계, Ge...
Windows LAPS 실무 가이드 ── 전 PC 공통 로컬 관리자 비밀번호를 그만두기
전 PC 공통 로컬 관리자 비밀번호는 한 대의 침해가 전대로 번지는 Pass-the-Hash 공격의 온상입니다. OS 표준 기능이 된 Windows LAPS의 자동 로테이션과 AD/Entra ID 저장 설정, 운영상의 함정을 설명합니다.
관련 토픽
이 기사와 가까운 토픽 페이지입니다. 기사를 출발점 삼아 관련 서비스와 다른 기사로 이어집니다.
Windows 기술 토픽
Windows 개발, 장애 조사, 기존 자산 활용에 관한 KomuraSoft LLC 기사를 모은 토픽 허브입니다.
이 주제와 연결되는 서비스
이 기사는 다음 서비스 페이지로 이어집니다. 가까운 입구부터 확인해 주세요.
기술 상담 & 설계 리뷰
기존 시스템의 로그, DB, 인증 방식, 저장 형식을 나눠 해시 방식을 식별하고, 이전이나 조사 판단을 정리하는 용도에 맞기 때문입니다.
자주 묻는 질문
이 기사 주제에 대해 상담 시 자주 나오는 질문을 모았습니다.
- 해시 문자열에서 방식을 가려내려면 어떤 순서로 보면 되나요?
- prefix → 구분자 → 문자 종류 → 길이 → 맥락 순으로 보면 정리하기 쉽습니다. 앞의 몇 글자에 의미가 있는 형식은 식별하기 쉽고, $argon2id$이면 Argon2, $2b$이면 bcrypt, $5$는 sha256crypt, $6$는 sha512crypt를 강하게 의심할 수 있습니다. 반대로 prefix가 없는 평범한 16진이나 Base64는 후보를 좁히는 데까지이므로, 최종적으로는 저장원의 제품이나 프레임워크, 설정을 확인해 확정합니다.
- 64자의 16진 문자열은 SHA-256이라고 단정해도 되나요?
- 단정은 위험합니다. SHA-256은 유력 후보이지만, 같은 32바이트 출력을 갖는 방식으로는 SHA3-256, SHA-512/256, BLAKE2s-256, BLAKE3의 기본 출력 등이 있어 길이만으로는 구별할 수 없습니다. 길이는 후보군을 만드는 재료이지 확정 재료가 아닙니다. 확정이 필요하면 저장원 사양, 공식 문서의 저장 형식, 알려진 평문으로의 대조, 구현 코드나 설정 확인까지 진행해야 합니다.
- $6$로 시작하는 문자열은 SHA-512 해시인가요?
- 아닙니다. $6$는 sha512crypt라는 Unix 계 비밀번호 저장 형식의 prefix이며, SHA-512의 hex digest 자체가 아니라 salt나 rounds를 포함한 비밀번호 해시 문자열입니다. 마찬가지로 $5$는 sha256crypt, $1$는 md5crypt입니다. prefix가 붙어 있는 시점에서 「그냥 digest」가 아니므로, 여기를 혼동하면 이전이나 검증 코드 구현이 어긋납니다.
- 문자 종류에서는 무엇을 알 수 있나요?
- 문자 종류는 길이만큼 중요한 단서입니다. 0-9a-fA-F 범위의 문자만이면 16진 표현이고, 문자 수의 절반이 raw 바이트 길이가 됩니다. + / =가 있으면 RFC 4648의 Base64, -나 _가 있으면 Base64url을 의심합니다. 마침표와 슬래시가 나오고 $로 구분되면, 일반 Base64가 아니라 bcrypt나 sha512crypt 등 crypt 계 alphabet을 의심하는 편이 자연스럽습니다. 마침표가 있다고 깨진 Base64로 보면 crypt 계 형식을 놓칩니다.