Oh! JUN

[WebGoat] Authentication Bypasses 본문

웹 해킹/Broken Authentication

[WebGoat] Authentication Bypasses

Kwon Oh! JUN 2023. 12. 1. 23:57


 

1. 사례

paytal에서 발생했던 인증 우회 예시입니다.

클라이언트가 SMS를 통해서 코드를 받을 수 없었기 때문에 보안 질문 대체 방법을 사용해서 획득할려고 했습니다.


 

2. 시나리오

 

패스워드 재설정을 하기 위해 "좋아하는 선생님"과 "살아온 거리 이름" 두 가지 질문에 대답을 해야합니다.


 

3. 공격방법

'선생님(SecQuestion0)'과 '거리(secQuestion1)' 질문에 대답한 답변은 3자인 입장에서 저희는 모르니까 당연히 틀릴 수 밖에 없습니다.


 

secQuestion0와 secQuestion1 파라미터를 secQuestion2와 secQuestion3로 변경하니까 성공적으로 인증 우회한것을 확인할 수 있습니다.


4. 소스코드 분석

//AccountVerificationHelper.java

    static {
        userSecQuestions.put("secQuestion0", "Dr. Watson");
        userSecQuestions.put("secQuestion1", "Baker Street");
    }

'userSecQuestion'는 Java에서 사용하는 HashMap 형태의 데이터 구조로 key와 value값으로 저장되어있습니다.

'secQuestion0'에는 'Dr. Watson'과 'secQuestion1'에는 'Baker Street'가 저장되어있습니다.

 

그래서 'teacher'와 'street에 위에 내용을 입력하면

정상적으로 인증할 수 있겠으나 우리의 목적은 취약점을 찾는것이니까 계속 시도해보도록 하겠습니다.


 

//AccountVerificationHelper.java

	public boolean didUserLikelylCheat(HashMap<String, String> submittedAnswers) {
        boolean likely = false;

        if (submittedAnswers.size() == secQuestionStore.get(verifyUserId).size()) {
            likely = true;
        }

        if ((submittedAnswers.containsKey("secQuestion0") && submittedAnswers.get("secQuestion0").equals(secQuestionStore.get(verifyUserId).get("secQuestion0")))
                && (submittedAnswers.containsKey("secQuestion1") && submittedAnswers.get("secQuestion1").equals(secQuestionStore.get(verifyUserId).get("secQuestion1")))) {
            likely = true;
        } else {
            likely = false;
        }

        return likely;

    }
submittedAnswers.containsKey("secQuestion0")

'submittedAnswers'라는 HashMap에 'secQuestion0'이 존재하는지 확인합니다.

 

submittedAnswers.get("secQuestion0").equals(secQuestionStore.get(verifyUserId).get("secQuestion0")

'submittedAnswers'에서 'secQuestion0'의 값과

'secQuestionStore에 저장되어 있는 'verifyUserId'의 'secQuestion0'의 값과

일치하는지 확인합니다.

('secQuestion0'이 Dr. Watson인가? / 'secQuestion1'이 Baker Street'인가?)

 

* 'secQuestion1'에 대한 내용도 위와 동일합니다.

 

그래서 위 두개의 조건이 True이면 &&연산자에 의해 true를 return 해줍니다.


// VerifyAccount.java

		if (verificationHelper.didUserLikelylCheat((HashMap) submittedAnswers)) {
            return failed(this)
                    .feedback("verify-account.cheated")
                    .output("Yes, you guessed correctly, but see the feedback message")
                    .build();
        }

didUserLikelyCheat 함수에서 true를 반환해주고 해당 조건문을 실행하게 됩니다. 

 

didUserLikelyCheat 함수에서 true를 반환해준 결과값

 


    public boolean verifyAccount(Integer userId, HashMap<String, String> submittedQuestions) {
        //short circuit if no questions are submitted
        if (submittedQuestions.entrySet().size() != secQuestionStore.get(verifyUserId).size()) {
            return false;
        }

        if (submittedQuestions.containsKey("secQuestion0") && !submittedQuestions.get("secQuestion0").equals(secQuestionStore.get(verifyUserId).get("secQuestion0"))) {
            return false;
        }

        if (submittedQuestions.containsKey("secQuestion1") && !submittedQuestions.get("secQuestion1").equals(secQuestionStore.get(verifyUserId).get("secQuestion1"))) {
            return false;
        }

        // else
        return true;

    }
submittedQuestions.containsKey("secQuestion0")

'submittedQuestions'라는 HashMap에 'secQuestion0'이 존재하는지 확인합니다.

!submittedQuestions.get("secQuestion0").equals(secQuestionStore.get(verifyUserId).get("secQuestion0")

'submittedQuestions'에서 'secQuestion0'의 값과

'secQuestionStore에 저장되어 있는 'verifyUserId'의 'secQuestion0'의 값과

일치하는지 확인합니다.


 

그런데!!

 

submittedQuestions.containsKey("secQuestion0")

'secQuestion0'과 'secQuestion1'에 대한 key 검사를 실행하지 않고 임의의 다른 key로 변경시키니까

해당 코드가 False가 되어버리면 && 연산자로 인해

뒤에 여부와 상관없이 해당 조건은 false가 되면서 조건문을 실행하지 않고 true를 return 시켜줍니다.

 

        if (verificationHelper.verifyAccount(Integer.valueOf(userId), (HashMap) submittedAnswers)) {
            userSessionData.setValue("account-verified-id", userId);
            return success(this)
                    .feedback("verify-account.success")
                    .build();
        } else {
            return failed(this)
                    .feedback("verify-account.failed")
                    .build();
        }

 

결국 return success를 실행시키면서 해당 인증을 우회하게 됩니다.

 

return success 결과