본문 바로가기

챌린지/Wargame.kr

bughela - type confusion

wargame.kr의 12번째 문제인 type confusion이다.

간단한 비교 챌린지라고 한다. 이 문제의 이름이 힌트라도 하는데 일단 Start 버튼을 눌러 문제를 확인해보자.

간단한 입력폼 하나만 나와있다. HTML 코드를 보면 간단한 자바스크립트로 lock을 걸어서 여러번 시도하지 못하게 하고 HTML 태그 대신 POST 요청을 보내고 있다. 약간 운영체제의 레이스 컨디션을 방지하려는 건지 뭔지 잘 모르겠지만 어쨌든 입력폼에 넣은 값으로 요청을 보내서 그 결과에 플래그가 포함되어 있다면 이를 출력하는 것 같은데 이쪽 부분에서는 더이상 할 수 있는게 없으므로 view-source를 눌러서 소스를 확인해보자.

<?php
 if (isset($_GET['view-source'])) {
     show_source(__FILE__);
    exit();
 }
 if (isset($_POST['json'])) {
     usleep(500000);
     require("../lib.php"); // include for auth_code function.
    $json = json_decode($_POST['json']);
    $key = gen_key();
    if ($json->key == $key) {
        $ret = ["code" => true, "flag" => auth_code("type confusion")];
    } else {
        $ret = ["code" => false];
    }
    die(json_encode($ret));
 }

 function gen_key(){
     $key = uniqid("welcome to wargame.kr!_", true);
    $key = sha1($key);
     return $key;
 }
?>

입력폼에서 POST로 받은 데이터(json)를 gen_key() 함수를 이용하여 생성된 랜덤값과 비교하고 있다. php의 uniqid() 함수는 현재 시간(microseconds)에 기반하여 첫번째 매개변수를 접두사로 한 랜덤한 id를 반환한다. 거기다 두번째 매개변수인 more_entropy를 true로 설정하였기 때문에 반환되는 랜덤값은 수십자리나 되며 이는 예측하거나 브루트포스로 맞추기 힘들다.

위의 사진처럼 매번 다른값을 반환하고 있는 것을 볼 수 있는데 어쨌든 이 문자열과 비교하여 동일하다고 판단되어야 플래그를 응답으로 얻을 수 있다. 그렇다면 이를 어떻게 우회할 수 있을까? 그 힌트는 역시 문제의 이름인 type confusion에 있었다.

 

PHP: PHP type comparison tables - Manual

 

PHP: PHP type comparison tables - Manual

Some function to write out your own comparisson table in tsv format. Can be easily modified to add more testcases and/or binary functions. It will test all comparables against each other with all functions. '==',        'ne' => '!=',        'gt' =>

www.php.net

이전에 다른 문제에서도 언급했던 것 같지만 PHP에서 '==' 연산자는 느슨한 비교를 수행한다. 그렇기 때문에 서로 자료형이 다르거나 값이 달라도 대충 공통 타입으로 변환한 뒤 비교하기 때문에 일반적으로 생각하기에는 다른 값이라도 종종 같은 값으로 판단되는 경우가 있다. 예를 들면 아래의 비교 코드는 모두 참이 된다.

function compare($val1, $val2){
    if($val1 == $val2){
        echo "[".$val1."] and [".$val2."] are equal!\n";
    }
}

compare(true, 1); // [1] and [1] are equal!
compare(true, "1"); // [1] and [1] are equal!
compare(true, "Hello World"); // [1] and [Hello World] are equal!
compare(false, array()); // [] and [Array] are equal!
compare(false, null); // [] and [] are equal!
compare(false, ""); // [] and [] are equal!

그래서 지금 gen_key()로 생성되는 문자열과 비교했을 때 true가 되는 값은 뭐가 있을까? 위의 링크에 있는 테이블을 찾아보면 다음과 같은 값이 있는 것을 볼 수 있다.

같은 자료형인 문자열 "php"와 비교했을 때 true로 판정되는 값은 true, 0, "php"가 있다. 이 중 문자열인 "php"는 당연히 동일하니 제외하고 남은건 true와 0인데 이 중 아무거나 골라서 POST로 전달하면 백엔드에서 gen_key()로 생성된 문자열과 이 값을 비교하여 true로 판정, if문을 통과하여 플래그가 응답 메시지에 포함되어 반환될 것이다. 그런데 이를 어떻게 넘겨줘야 할까? php로 GET이나 POST로 전달되는 값은 대부분 자동적으로 문자열로 변환되기 때문에 문제의 입력폼에 0이나 true를 입력해도 "0", "true"로 전달되어 문자열로 비교된다. 하지만 이번 문제에서는 아까 봤듯이 자바스크립트에서 그 요청과 응답을 대신 받아주고 있는데 이 코드를 좀 더 살펴보자.

var lock = false;
function submit_check(f){
	if (lock) {
		alert("waiting..");
		return false;
	}
	lock = true;
	var key = f.key.value;
	if (key == "") {
		alert("please fill the input box.");
		lock = false;
		return false;
	}

	submit(key);

	return false;
}

function submit(key){
	$.ajax({
		type : "POST",
		async : false,
		url : "./index.php",
		data : {json:JSON.stringify({key: key})},
		dataType : 'json'
	}).done(function(result){
		if (result['code'] == true) {
			document.write("Congratulations! flag is " + result['flag']);
		} else {
			alert("nope...");
		}
		lock = false;
	});
}

우리가 입력폼에 어떤 값을 입력하고 버튼을 누르면 submit_check() 함수가 호출된다. 아까 말한 lock 변수를 이용해 한 요청이 끝나기 전까지 반복적인 요청을 방지하는데 아마 php의 usleep(500000) 코드와 연계하여 생각해보면 브루트포스를 방지하려고 이같이 구현한 것 같다. 어차피 지금 풀이는 브루트포스가 아니기 때문에 차치하고 submit() 함수를 살펴보자.

 

submit() 함수에서는 jquery와 ajax를 이용해 "./index.php"에 POST로 비동기 요청을 보내고 있다. "./index.php"는 즉 지금 이 문제 사이트기 때문에 살펴볼 건 없고 data와 dataType 파라미터를 좀 더 살펴보자. 현재 dataType은 json이며 이는 php 코드에서 json_decode()로 디코딩하고 있기 때문에 POST 요청 시 json으로 인코딩해서 보내야 한다. 그래서 data 파라미터에서 "{json:JSON.stringify({key: key})}"처럼 인코딩해서 보내고 있는데 key-value 형식인 json 특성상 최상위 키인 'json'의 값으로 'key'와 key 변수의 쌍이 들어있는 형태다. key란 단어가 여러번 나와 헷갈릴 수 있지만 왼쪽은 key-value 형식의 key를 뜻하며 오른쪽은 submit() 함수의 매개변수로 전달된 key 변수, 즉 우리가 입력한 값이다. JSON 함수 stringify()는 매개변수로 전달된 json 데이터를 json 문자열로 변환한다.

 

결과적으로 우리가 입력한 값을 key-value 쌍으로 만들어서 문자열로 만들고 이를 'json' 키의 값으로 전달해 주기 때문에 위의 요청대로라면 백엔드단에 다음과 같이 전달된다.

{
    'json': {
        'key': 'INPUT_FROM_FORM'
    }
}

이는 백엔드단에서 $_POST['json']처럼 참조하고 결국 우리가 입력한 key 값은 문자열로 변환되어 문자열끼리 비교된다. 그렇다면 우리가 입력한 key 값을 문자열이 아닌 true나 정수 그 자체로 전달하려면 어떻게 하면 될까? 가장 좋은 방법은 자바스크립트 코드를 수정해서 stringify() 메소드 호출에 true를 하드코딩해주는 것이다.

위처럼 코드를 수정한다면 우리가 어떤 값을 입력하던 submit() 함수에서는 사용되지 않고 POST 요청으로 true가 전달된다. 이는 Boolean 형식이기 때문에 php에서 비교할 때 문자열과 Boolean과 비교하게 되며 위의 표를 참고해볼 때 이는 true가 된다. 그래서 if문에서 비교문은 성공하며 플래그가 생성되어 응답으로 반환되어 submit() 함수가 이를 받아서 출력해주는 것이다.

 

어쨌든 true값을 전달해주면 풀리는 문제기 때문에 자바스크립트를 수정한다는건 동일하지만 다른 방법을 생각해볼 수도 있다. HTML 요소 중에서 true, false같은 bool값을 가질만한 요소가 뭐가 있을까? 제일 먼저 생각나는건 체크박스다.

자바스크립트 코드에서 "f.key.value"가 아니라 "f.key.checked"로 바꿔준 후 HTML 태그 수정을 통해 input 태그를 text 형식이 아닌 checkbox 형식으로 바꿔준다. 그리고 체크박스를 체크하면 결과적으로 자바스크립트에서 checked 속성을 참조, true 값을 읽어오기 때문에 json 요청을 보낼 때도 true값을 보내게 된다. 그래서 위처럼 동일하게 플래그를 얻을 수 있는 것을 볼 수 있다.

 

 

이번 문제에서는 POST 요청을 HTML 폼으로 보내지 않고 자바스크립트로 보내기 때문에 이를 응용하여 해결할 수 있는 문제였다. 역시 프론트엔드단에서 무언가를 검증하거나 동작하려고 하는 것은 항상 분석당하고 악용될 수 있는 우려를 가지고 있는 것 같다. 클라이언트 입력은 항상 신뢰하지 않고 검증하라는 원칙이 생각나는 문제였다.

'챌린지 > Wargame.kr' 카테고리의 다른 글

bughela - php? c?  (0) 2020.12.29
bughela - img recovery  (0) 2020.12.28
bughela - tmitter  (0) 2020.12.26
bughela - md5_compare  (0) 2020.12.22
bughela - DB is really GOOD  (0) 2020.12.22