wargame.kr의 8번째 문제인 md5 password다.
md5는 해시 함수의 일종으로 결함이 발견되는 등 여러 문제로 인해 사용하지 않는 추세라고 알고 있다. 물론 많이 사용되던 해시 함수인 만큼 여러 프레임워크에서 내장 함수로 구현되어 있는데 php의 경우 md5() 함수로 구현되어 있다. 그런데 이를 문제의 힌트에서 알려주는 이유는 무엇일까? 일단 Start 버튼을 눌러 문제를 확인해보자.
문제는 단순한 입력폼 하나만 주어진다. 아무 값이나 넣어서 login을 시도해보면 "wrong..."이라는 문자열이 출력될 뿐 별다른 변화가 없어 자세한 정보를 얻을 수 없다. 일단 get_source를 눌러서 소스를 한번 확인해보자.
<?php
if (isset($_GET['view-source'])) {
show_source(__FILE__);
exit();
}
if(isset($_POST['ps'])){
sleep(1);
mysql_connect("localhost","md5_password","md5_password_pz");
mysql_select_db("md5_password");
mysql_query("set names utf8");
/*
create table admin_password(
password char(64) unique
);
*/
include "../lib.php"; // include for auth_code function.
$key=auth_code("md5 password");
$ps = mysql_real_escape_string($_POST['ps']);
$row=@mysql_fetch_array(mysql_query("select * from admin_password where password='".md5($ps,true)."'"));
if(isset($row[0])){
echo "hello admin!"."<br />";
echo "Password : ".$key;
}else{
echo "wrong..";
}
}
?>
버튼을 눌러서 비밀번호를 전송하면 md5_password라는 데이터베이스에 연결하는 것을 볼 수 있다. 테이블의 정보도 알려주고 있는데 문자열 64자리 컬럼 하나밖에 없는 단순한 테이블이다. 사용자 입력값인 $_POST['ps'] 변수에 대해서 mysql_real_escape_string() 함수를 호출하고 있기 때문에 이 함수 자체를 우회하는것 보다는 다른 쪽에서, 특히 문제에서도 힌트로 주어졌던 바로 아랫줄의 md5() 함수를 공략하는 쪽이 이 문제의 해결방법일 것 같다.
그런데 md5() 함수에서 두번째 매개변수가 true로 설정되어 있는데 이는 어떤 기능일까? 이는 md5() 함수로 해싱한 값을 문자열이 아닌 바이너리 형식으로 반환하도록 설정한다. 아래 예시를 보면 차이를 알 수 있을 것이다.
$string = "suzumiya haruhi";
echo md5($string)."\n"; // fc6dffd7e513e790758db5651e9e7462
echo md5($string, true); // �m����u��e�tb
md5() 함수를 이용한 해시값이 보통 "fc6dffd7...7462"같은 일반 문자열로 반환되었다면 두번째 매개변수로 true를 넘겨줬을때는 문자열이 아닌 \xFC, \x6D, \xFF, \xD7, ..., \x74, \x62같은 바이너리 값의 연속으로 반환되는 것이다. 이 중에서 해당하는 ASCII 문자가 존재하지 않는 값들은 위의 예시처럼 ? 같은 문자로 표현되는 것이다. 그런데 이런 raw binary를 활용해서 뭘 할 수 있을까? 결정적인 단서는 php에서 이를 문자열로 치환할 수 있다는 것이다.
$raw_binary = "\x68\x61\x72\x75\x68\x69";
$string = "Suzumiya ".$raw_binary." no Yuuutsu";
echo $string; // Suzumiya haruhi no Yuuutsu
위의 예시를 보자. "haruhi"란 단어는 코드 어디에도 보이지 않는데 이를 이어붙인 문자열을 출력시키면 16진수값이 아스키 코드로 변환되어 출력되는 것을 볼 수 있다. 문제 코드에서도 "... where password='".md5($ps,true)."'"));"처럼 md5() 함수의 결과값을 쿼리에 직접 이어붙이고 있기 때문에 만약 md5() 함수의 바이너리 결과값이 우연히 어떤 문자열을 띄게 된다면, 위의 쿼리를 예로 들자면 "' or 1=1#"같은 문자열의 16진수 값으로 해싱된다면 따옴표를 이스케이프하는 mysql_real_escape_string() 함수를 지나서도 사용자가 입력한 값으로 쿼리에 문자열을 이어붙일 수 있을 것이다.
근데 "' or 1=1#"같은 특정 문자열의 바이너리 값으로 해싱되는 원본 값은 어떻게 찾을 수 있을까? 이 경우는 방법이 없다. 무작정 반복문을 통해서 바닥부터 모든 경우의 수를 비교해야 할 것이다. 그나마 시도할 수 있는 방법은 저 '특정 문자열'의 길이를 최대한 줄여서 "'||true#"처럼 시도하는 것이다. 어차피 주석 문자가 있기 때문에 해당 문자열 뒤에 무슨 값이 오든 상관이 없으며 작은 따옴표(')로 맨 앞에 감싸고 있기 때문에 해당 문자열 앞에 무슨 값이 오든 상관이 없다. 그래서 이 문자열의 바이너리로 해싱되는 원본 문자열 값을 계산해보자.
import hashlib
target = "277c7c7472756523"
for index in range(10000000):
print(index)
result = hashlib.md5(str(index).encode())
if target in result.hexdigest():
print("FOUND HASH.")
print(index)
input("continue? ...")
print("END");
사실 직접 계산해보려고 위와 같은 파이썬 코드를 짜서 돌려봤지만 너무 느려서 40만개에서 그만뒀다. C++로 하려고 해봤더니 비주얼 스튜디오 라이브러리 환경 설정부터 막혀서 역시 그만뒀다. 사실 확률 문제도 있어서 얼마나 오래 걸릴지 모르기 때문에 구글에 검색을 좀 해봤다.
검색 결과 이를 이용하여 비슷한 CTF 문제를 푸는 Write-Up이 있다. 아마 쿼리문까지 비슷한 걸 보면 wargame.kr에서 이 문제를 차용한 듯 한데, 사실 이 글이 아니었으면 아마 이번 문제를 풀지도 못했을 것이다.
어쨌든 링크에서 설명하는 글을 읽어보면 129581926211651571912466741651878684928라는 긴 값을 해싱했을 때 "?T0D??o#??'or'8.N=?"라는 문자열을 얻을 수 있었다고 한다. 이를 위의 쿼리와 조합해보면 "select * from admin_password where password='?T0D??o#??'or'8.N=?'"이 된다. 쿼리 뒷부분에 or 절에 뭔가 이상한 비교문이 온다고 생각할 수 있지만 이는 쿼리 맨 마지막의 따옴표로 인해 그냥 하나의 문자열로 묶여버린다. 이런 리터럴은 mysql에서 자동으로 형변환이 되기 때문에 '8.N=?'는 그냥 '8', 정수 8이 되버린다. 그래서 0을 초과하기 때문에 참으로 판단되며 결과적으로 항상 참인 조건에서 테이블의 password값을 받아오는 쿼리가 완성된다. 이후 테이블이 텅 비어있지 않는이상 password 값을 받아올 것이기 때문에 isset() 함수를 통과해서 플래그를 출력하게 된다.
이번 문제는 정말 많이 헤맸던 문제로 뭣도 모르고 mysql_real_escape_string() 함수를 우회하는 건줄 알고 멀티바이트, 싱글바이트 문자같은걸 찾아보면서 삽질을 몇시간동안 했던것 같다. 결국에는 위의 Write-Up을 읽고 md5() 함수의 raw binary를 어떻게 사용하는지 이해하고 친절하게도 해시값까지 써놓은 덕분에 이를 입력해서 플래그를 얻을 수 있었다. 완전히 혼자 푼 게 아니기 때문에 좀 찜찜하지만 아마 혼자 끙끙댔으면 지금까지도 못풀고 시간만 날렸을지도 모른다. 특히 저 해시값을 구하려고 파이썬으로 반복문을 돌리고 있었을 생각을 하면... 끔찍하다.
재밌는 것은 mysql에서 문자열 리터럴을 자동으로 형변환한다는 사실을 알게 된 것이다. 처음에는 저 쿼리가 왜 동작하는지 이해가 가지 않았는데 스택오버플로우 게시글을 보니 이런 리터럴은 자동으로 자료형이 변환된다는 것을 알 수 있었다.
예를 들어 "select 'CORRECT' where '8helloworld' > 7"같은 쿼리가 있다면 '8helloworld'는 맨 첫글자가 8이기 때문에 그냥 숫자 8로 변환되어 8>7은 참이므로 'CORRECT' 문자열이 출력된다. 반대로 'helloworld8'은 그냥 문자열이기 때문에 '0'으로 변환된다. 그래서 'CORRECT' 문자열이 출력되지 않는다.
'챌린지 > Wargame.kr' 카테고리의 다른 글
bughela - md5_compare (0) | 2020.12.22 |
---|---|
bughela - DB is really GOOD (0) | 2020.12.22 |
bughela - strcmp (0) | 2020.12.18 |
bughela - fly me to the moon (0) | 2020.12.16 |
bughela - WTF_CODE (0) | 2020.12.14 |