설명
이 취약점은 비밀번호를 저장하거나 검증할 때 MD5 같은 오래되거나 취약한 해시 알고리즘을 사용하는 경우 발생합니다. MD5는 계산이 매우 빨라 유출된 해시를 대상으로 많은 비밀번호 후보를 대입할 수 있습니다. 솔트가 없으면 미리 계산한 테이블도 재사용할 수 있습니다. 실제 복구 가능성은 비밀번호 강도와 공격 자원에 따라 달라지며, 일반적인 충돌 공격이 특정 사용자의 비밀번호 복구를 뜻하지는 않습니다.
잠재적 영향
- 비밀번호 크래킹(비밀번호 탈취): 빠른 MD5 연산 속도를 이용하여 공격자가 대규모 무차별 대입 공격을 수행해 해시된 비밀번호를 실제 비밀번호로 복구할 수 있습니다.
- 계정 탈취 및 권한 오남용: 탈취된 비밀번호를 이용해 사용자 계정에 로그인하고, 개인 정보 조회, 결제, 관리자 기능 수행 등 계정을 악용할 수 있습니다.
- 재사용 비밀번호로 인한 2차 피해: 사용자가 다른 서비스에서도 같은 비밀번호를 사용하는 경우, 한 번 노출된 MD5 해시로 여러 시스템이 연쇄적으로 침해될 수 있습니다.
- 규정 위반 및 법적 리스크: 안전하지 않은 비밀번호 저장 방식은 개인정보보호법, 컴플라이언스(예: GDPR 유사 규제) 요구사항을 위반해 과징금·제재 대상이 될 수 있습니다.
해결 방법
- 비밀번호 전용 해시 함수 사용: bcrypt, scrypt, Argon2 같은 비밀번호 해시 알고리즘을 사용하세요. Go에서는
golang.org/x/crypto/bcrypt패키지를 사용할 수 있습니다. - 느린 해시(연산 비용) 설정: bcrypt의 cost 값을 충분히 크게 설정해 무차별 대입 공격을 어렵게 하세요(예: 10 이상, 성능 테스트 후 결정).
- 솔트 관리:
bcrypt.GenerateFromPassword는 무작위 솔트를 생성해 결과에 포함합니다. Go의argon2.IDKey는 호출자가 솔트를 전달해야 하므로, 알고리즘별로 검증된 비밀번호 저장 형식을 사용하세요. - 기존 MD5 해시 마이그레이션: 로그인 시 MD5 해시를 검증한 뒤, 성공하면 즉시 bcrypt 등 안전한 해시로 재해시하여 저장하는 점진적 마이그레이션을 적용하세요.
- 직접 구현 금지: 직접 해시·솔트·PBKDF 함수를 구현하지 말고, 검증된 라이브러리(bcrypt, Argon2 등)를 그대로 사용하세요.
예시
변경 전
go
package main
import (
"crypto/md5"
"encoding/hex"
)
type User struct {
PasswordHash string
}
// 취약한 예: MD5로 비밀번호 해시
func (u *User) SetPassword(plain string) {
h := md5.New()
h.Write([]byte(plain))
sum := h.Sum(nil)
// 비밀번호를 MD5 해시로 저장 (매우 위험)
u.PasswordHash = hex.EncodeToString(sum)
}
func (u *User) CheckPassword(plain string) bool {
h := md5.New()
h.Write([]byte(plain))
sum := h.Sum(nil)
return u.PasswordHash == hex.EncodeToString(sum)
}
변경 후
go
package main
import (
"fmt"
"golang.org/x/crypto/bcrypt"
)
type User struct {
PasswordHash string
}
// 안전한 예: bcrypt로 비밀번호 해시
func (u *User) SetPassword(plain string) error {
// cost 값은 10 이상으로 설정하고, 성능 테스트를 통해 조정
hash, err := bcrypt.GenerateFromPassword([]byte(plain), bcrypt.DefaultCost)
if err != nil {
return err
}
u.PasswordHash = string(hash)
return nil
}
func (u *User) CheckPassword(plain string) bool {
err := bcrypt.CompareHashAndPassword([]byte(u.PasswordHash), []byte(plain))
return err == nil
}
func main() {
u := &User{}
if err := u.SetPassword("MySecurePassword!123"); err != nil {
panic(err)
}
fmt.Println("login ok?", u.CheckPassword("MySecurePassword!123")) // true
fmt.Println("login ok?", u.CheckPassword("wrong")) // false
}
설명:
- 변경 전: 위 비준수 코드에서는 사용자 비밀번호를
crypto/md5로 해시해 그대로 저장합니다. MD5는 매우 빠르게 계산되기 때문에 공격자가 GPU/ASIC 등을 사용해 초당 엄청난 양의 후보 비밀번호를 시도할 수 있어, 해시만 유출돼도 비밀번호를 쉽게 크래킹할 수 있습니다. 또한 솔트(salt) 사용도 없기 때문에, 같은 비밀번호는 항상 같은 해시를 만들어 레인보우 테이블 공격에 특히 취약합니다. - 변경 후: 준수 코드에서는 MD5 대신 비밀번호 전용 해시 알고리즘인
bcrypt를 사용합니다.bcrypt.GenerateFromPassword는 내부적으로 무작위 솔트를 포함해 느린 연산을 수행하므로 무차별 대입 공격의 비용을 크게 높입니다.bcrypt.CompareHashAndPassword를 통해, 저장된 해시와 입력 비밀번호를 안전하게 비교할 수 있습니다. 직접 솔트·반복 횟수를 구현하지 않고 검증된 라이브러리를 그대로 사용함으로써 빠른 MD5 해시를 이용한 추측 공격의 비용을 높입니다.