취약하거나 위험한 암호 알고리즘 사용 (MD5 비밀번호 해시)

Use of a Broken or Risky Cryptographic Algorithm

설명

이 취약점은 비밀번호를 저장하거나 검증할 때 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 해시를 이용한 추측 공격의 비용을 높입니다.

참조