알쓸Web잡 CH.04 - 웹해킹쩜케알 prob 39/46

By choirish | August 31, 2018

(난) 알고싶지만 (너)에겐 쓸데없는 Web 잡동사니 CH.04


안녕하세요, choirish 입니다 :)

알쓸웹잡 네 번째 시간(!)
webhacking.kr 문제를 풀며 알게된 내용에 대해 썰을 풀어보겠습니다.




오늘의 탐구 Challenge

  • Challenge 39
  • Challenge 46




Challenge 39



해당 페이지의 소스코드는 다음과 같다.

<html>
<head>
<title>Chellenge 39</title>
</head>
<body>
<!-- index.phps -->


<form method=post action=index.php>
<input type=text name=id maxlength=15 size=30>
<input type=submit>
</form>
</body>
</html>


index.phps를 제공한다고 하니 확인해보자(!)

<html>
<head>
<title>Chellenge 39</title>
</head>
<body>

<?

$pw="????";

if($_POST[id])
{
$_POST[id]=str_replace("\\","",$_POST[id]);
$_POST[id]=str_replace("'","''",$_POST[id]);
$_POST[id]=substr($_POST[id],0,15);
$q=mysql_fetch_array(mysql_query("select 'good' from zmail_member where id='$_POST[id]"));

if($q[0]=="good") @solve();

}

?>

<form method=post action=index.php>
<input type=text name=id maxlength=15 size=30>
<input type=submit>
</form>
</body>
</html>


소스 코드의 동작을 해석해보았다.


  1. id를 POST 방식으로 받는다.

  2. $_POST[id]를 필터링 처리한다.

    • \를 없애버린다.
    • '를 없애버린다. 가 아니고…
      '''로 치환한다. 즉 작은따옴표(‘) 한 개를 두 개로 바꾼다. ( 수상해 )
    • str_replace로 처리한 id앞에서 15글자 만큼만 잘라서 가져온다.
  3. $POST[id]가 where 조건절에 포함된 mysql 구문을 실행한 후, 그 결과를 array 형태로 $q에 저장한다.

    • select 'good' from zmail_member where id='$_POST[id]
    • 자세히 보면…. '$_POST[id]로.. 해당 인자를 닫는 '가 하나 실종되었다( …ㅋㅋ 아주 수상함 )
  4. sql 구문을 실행한 결과의 첫 번째 값이 good이면 성공(!)


php - str_replace( )
php - substr( )
php - mysql_fetch_array( )


3번 동작을 보면, sql 구문에 quote 1개(')가 빠져있다.

그래서 input 박스에 단순히 a만 입력해도 에러 메시지가 출력된다.




즉, sql 구문의 syntax가 잘못되어 원래부터 에러가 나는 녀석이었던 것이다(!)


그래서… '를 적어도 1개는 넣어줘야 sql 구문을 닫아줄 수 있는데…
\를 쓸 수 없고, '는 2개로 증식하여 ''가 된다.


그런데 필터링 적용 후, id의 15글자만 잘라서 가져간다(!)


즉, 무조건 2개로 늘어나는 '를 1개만 넣으려면(?)
id 15글자의 제일 마지막에 '를 1개 추가하면, 하나 더 생긴 녀석은 잘려나갈 것이다(!)




Test 1


[ a 14개 + ‘ 1개 ]

aaaaaaaaaaaaaa'

그래서 요로코롬 넣으면 에러 메시지는 안뜬다.

그런데 문제 초기 화면처럼… 아무것도 안뜬다 ^^


$_POST[id]가 들어가는 sql 구문을 다시 살펴보면,

select 'good' from zmail_member where id='$_POST[id]'

zmail_member 테이블에 해당 아이디가 존재할 경우 good 문자열을 출력한다.


aaaaaaaaaaaaaa'를 input으로 넣으면 id가 aaaaaaaaaaaaaaa로 where 조건절에 들어간다.

아무 결과가 나오지 않는다는 건…. 저런 id가 존재하지 않는 다는 거겠지.. 또오륵


그럼, a라는 id는 어떨까?




Test 2 : 성공


[ a 1개 + 공백 13개 + ‘ 1개 ]

a             '

제일 끝에는 '가 들어가야하니까 가운데 공백을 채워, id를 a로 넣어보았다.


성!공!

a 대신에 baa를 넣고 15글자를 맞춰줘도 통과된다.


zmail_member에 어떤 id가 존재하는지도 모르는데…
그 정도는 이것저것 때려보고 알아맞혀야 하나보다.. ‘ㅅ’a


MySQL을 사용할 때, 비교 구문에서 공백을 처리하는 방법에 대해 의문을 가진다면,
우아한형제들 기술블로그에서 해당 내용을 매우 친절하고 자세하게 다루고 있으니 참고 하길 바란다(!)

우아한형제들 - MySQL에서 ‘a’ = ‘a ‘가 true로 평가된다?


그런데… 문제를 해결하고 나서 한 가지 더 테스트해보고 싶은 것이 생겼다…

그래서 테스트해보았다(!)




Test 3 : Single quote in MySQL


문득, 공백 대신에 따옴표(‘)로 빈 공간을 채우면 어떨까? 라는 생각을 하게 되었다.


[ a 1개 + ‘ 1개 + ’ 13개 ]

a''''''''''''''


즉 이렇게 a와 ' 14개를 입력하면,

a 다음에 오는 첫 번째 '$_POST[id] 값을 닫아주는 따옴표가 되고
그 뒤 ' 13개는 정상적인 sql 구문에 붙어있는 쓰레기 값이 되어, 안 해석되지 않을까? 라는 가설을 세워보았다.


select 'good' from zmail_member where id='a'”“”“”“’


하지만 아니었다… 그리고 심지어 syntax error 에러가 뜬다(!) 왜??




이것저것 넣어보다가… 이번에는 aa와 ' 13개를 입력해보았다.


[ a 2개 + ‘ 1개 + ’ 12개 ]

aa'''''''''''''


그랬더니 error 메시지가 안뜬다(!)
근데 아무것도 안뜬다…!!!

…. 뭔가 $_POST[id] 값을 닫아주는 따옴표 1개를 제외하고 남은 따옴표 개수가 짝수(12개)여서
잘 열고 닫힌 건가?? ‘ㅅ’a


그런데 처음 세운 가설처럼, aa 다음에 오는 '로 sql 구문이 닫히고,
그 뒤의 ' 12개가 dummy 처럼 뒤에서 열고 닫혀서 해석이 안된거라면…

인증에 성공해야 한다!
그런데 되지 않았다는 건, 나의 가설처럼 해석되지 않았다는 것이다…


그래서 직접 MySQL 명령어를 쳐보면서 테스트해보았다(!)

먼저, id/pw 컬럼을 가진 login 테이블을 생성하고 “admin” id를 추가했다.




처음 세운 가설을 테스트해보자.




조건에 해당하는 값이 아무것도 없다….

하지만 쿼리 에러는 안 났으니까, 'admin'''''이 어떠한 값으로 해석이 되었다는 것이다.

도대체 뭘까…?


도저히 생각이 안나서 웹잘알 우엉님께 여쭈어보았더니… 답을 내려주셨다…(!)

MySQL 에서는 따옴표 2개(“)를, 문자로서의 따옴표(‘) 1개로 해석한다는 것이다(!!!)

즉, sql 구문에서 특수한 문자인 '를 문자 그대로 쓰기 위해서는 \'이나 ''로 입력해야 하는 것이다.


평소에 웹이 아니더라도, 특수한 문자를 문자그대로 쓰기 위해서 \를 앞에 붙여 쓰는 경우가 많고
\를 문자 그대로 쓰려면 \\로 써야하는 것도 알고 있었는데…

왜 이걸 생각못했을까 싶지만, 원래 알면 보이고 모르면 안 보이는 법이다 ㅎ


무튼 이 사실을 알게 되었으니, MySQL 명령어로 직접 다시 테스트해보자.


먼저, \를 이용해서 bbb''라는 아이디를 등록하고
''를 이용하여 해당 아이디에 대한 pw를 뽑아냈다.


mysql> insert into login values('bbb\'\'','ddd');  
mysql> select pw from login where id = 'bbb''''';  




성!공!적!


이번에는 바꾸어서, ''를 이용해서 아이디를 등록하고
\를 이용하여 해당 아이디에 대한 pw를 뽑아냈다.


mysql> insert into login values('ccc''''''','eee');  
mysql> select pw from login where id = 'ccc\'\'\'';  




MySQL에서 '를 연달아 쓰면 어떻게 되는지,
'를 문자 그대로 입력하려면 어떻게 해야하는지 완벽히 알게 되었다 ^^ (뿌듯)


Sigle quote(‘) 탐구를 마무리하며, MySQL 레퍼런스 매뉴얼에서 해당 내용을 찾아보았다.

MySQL 5.6 Reference Manual - String Literals




해당 페이지에서는, MySql에서 single quote(‘)를 문자 그대로 입력하는 방법 뿐만 아니라

double quote(“)를 어떻게 쓰는지, double quote(”)와 single quote(‘)를 함께 사용할 경우 어떻게 해석되는지에 대하여 자세히 설명되어있으니 꼭 읽어보길 바란다(!)




Challenge 46



여기에서도 index.phps를 제공한다고 하니 살펴보자(!)

<html>
<head>
<title>Challenge 46</title>
</head>
<body>
<form method=get action=index.php>
level : <input name=lv value=1><input type=submit>
</form>
<?
if(time()<1256900400) exit();

?>
<!-- index.phps -->
<?

$_GET[lv]=str_replace(" ","",$_GET[lv]);
$_GET[lv]=str_replace("/","",$_GET[lv]);
$_GET[lv]=str_replace("*","",$_GET[lv]);
$_GET[lv]=str_replace("%","",$_GET[lv]);

if(eregi("union",$_GET[lv])) exit();
if(eregi("select",$_GET[lv])) exit();
if(eregi("from",$_GET[lv])) exit();
if(eregi("challenge",$_GET[lv])) exit();
if(eregi("0x",$_GET[lv])) exit();
if(eregi("limit",$_GET[lv])) exit();
if(eregi("cash",$_GET[lv])) exit();

$q=@mysql_fetch_array(mysql_query("select id,cash from members where lv=$_GET[lv]"));

if($q && $_GET[lv])
{
echo("$q[0] information<br><br>money : $q[1]");

if($q[0]=="admin") @solve();

}
?>

</body>
</html>


소스 코드의 동작을 해석해보았다.


  1. lv를 GET 방식으로 받는다.

  2. $_GET[lv]공백, /, *, %가 있으면 없애버린다.

  3. $_GET[lv]에 다음 문자열이 포함될 경우 exit() 한다.

    • union / select / from / challenge / 0x / limit / cash
  4. $_GET[lv]가 where 조건절에 포함된 mysql 구문을 실행한 후, 그 결과를 array 형태로 $q에 저장한다.

    • select id,cash from members where lv=$_GET[lv]
    • members 테이블에서, lv=$_GET[lv] 인 조건을 만족하는 id, cash 값을 가져온다.
  5. $q와 $_GET[lv] 값이 존재할 경우 첫 번째 id/cash 정보를 담은 내용이 출력된다.

  6. 이 때, $q[0]( = 첫 번째 id ) 값이 admin이면 성공(!)


level 칸에 1을 넣어 제출하면 다음과 같이 찌봉이의 정보가 출력된다.




찌봉이는 만원을 갖고 있다…

level 칸에 1을 제외하고, 음수부터 아주 큰 수까지 다 넣어봤지만
아무 정보도 출력되지 않는다…


즉 1 말고 다른 레벨을 가진 id는 없는 것 같고
level이 1인 녀석들(?) 중에서는 zzibong이가 제일 처음으로 출력되는 것을 알 수 있다.
(sql 구문을 만족하는 id/cash 중 첫 번째 꺼 하나만 출력하기 때문)


그렇다면… admin이 제일 처음으로 출력되도록 하려면 어떻게 조건을 줘야할까?




Test 1


일단 사용자 입력에서, 공백이 필터링되어있으므로
공백 대신에 %0a를 쓰면 된다.


%가 필터링되어 있긴하지만, %0a는 어차피 php에게 줄바꿈문자로 넘어가기 때문에 필터링에 걸리지 않아서 괜찮다. 즉 %25만 안쓰면 괜찮다는 것(!)


or은 써도 되니까 where 조건절에 or 1을 추가해서 입력해보았다.

그럼… 찌봉이만 또 출력된다 ^^




하… 조건절을 where lv = 1 or 1 로 설정함으로써, 모든 조건을 무효화 시켰는데…도(!)
찌봉이가 나온다는 건, lv과 상관없이 찌봉이가 테이블의 가장 위에 있거나 우선 출력된다는 것이다.

그렇다면… 조건을 다르게 설정해보자(!)




Test 2


lv의 조건으로는 admin을 얻어낼 수 없으니…
무조건 admin을 불러낼 수 있도록 or id = 'admin' 조건을 걸어보자.

(or 뒤에 오는 조건이 성립될 때의 결과를 얻고 싶으면, 앞에 오는 lv 조건을 1이 아닌 없는 레벨로 줘야함)

아무것도 안 나온다….

왜냐면… webhacking.kr 서버에는 magic_quotes_gpc 옵션이 설정되어 있어서
', ", \, NUL 앞에 자동적으로 \가 붙어 ESCAPE 되기 때문이다(!)


php - configuration info
php - magic quotes


그렇다면… 따옴표(‘)를 우회할 수 있는 방법을 찾아보자(!)




Test 3 : 성공


'를 쓸 수 없으니, 'admin'을 표현해줄 다른 녀석이 필요하다.


이 때 사용할 수 있는 건 MySQL의 CHAR( ) 함수 이다.

MySQL 5.6 Reference Manual - string functions




CHAR 함수는, 함수에 인자로 주어진 각 integer(정수)를 문자로 변환하여 연결된 문자열로 만들어낸다.


admin의 각 문자를 십진수로 변환하면 (97,100,109,105,110) 이므로,
'admin' 대신에 Char(97,100,109,105,110)을 사용해보자(!)


admin의 정보가 출력되면서 성!공!




이렇게 따옴표(‘) 대신에 쓸 수 있는 CHAR() 함수를 새롭게 알게 되었습니다^0^


그런데… 해당 문제에서 0x를 필터링하는 것을 보니…
한 가지 더 시도해 보고싶은 것이 생겼다(!)




Test 4 : 성공


구글에 “mysql 0x hex“라고 검색하면, mysql에서 0x로 string을 표현하는 방법에 대한 매뉴얼을 찾을 수 있다.


MySQL 5.5 Reference Manual - Hexadecimal Literals




aa를 X'6161'0x6161로 모두 표현할 수 있는데,
해당 문제에서는 '를 쓸 수 없으니 0x를 이용하여 문자열을 표현할 수 있다.


그!런!데! 지금 우리는 0x를 쓸 수 없다(!)

그렇다면… 0x도 있으니 0b도 쓸 수 있지 않을까??


그렇다(!) 매뉴얼의 바로 다음 페이지로 가면 Bit-Value Literals에 대한 설명이 있다.

MySQL 5.5 Reference Manual - Bit-Value Literals


즉 a가 2진수로 0b1100001이니까, aa를 0b0110000101100001로 나타낼 수 있다(!)


※ 잠시 알고 넘어가기

  • 문자 1개를 2진수로 표현할 때는 0b1100001과 같이 0b 뒤에 홀수 개의 숫자가 있어도 자동으로 앞에 0을 붙여 처리해준다.

  • 하지만 여러 개의 문자를 2진수로 표현할 때는 0b 뒤에 8자리를 맞추어 0으로 패딩을 한 후 연결해줘야 올바른 문자열로 변환할 수 있다.


그렇다면, admin도 2진수로 변환해서 넣어보자(!!!)

나는 python을 이용해서 admin의 각 문자를 2진수로 변환하였다.

>>> a = "admin"
>>> [bin(ord(a[i])) for i in range(5)]
['0b1100001', '0b1100100', '0b1101101', '0b1101001', '0b1101110']


각 문자의 2진수가 8자리가 되도록 0을 추가하여 모두 이어붙이면 다음과 같다.

0b0110000101100100011011010110100101101110


그럼 이제, 'admin' 대신에 위에서 변환한 2진수를 넣어보자(!)

성!공!




오오 뭔가 의도되지 않은 방법으로 해결한 거 같아서 재미있었다 ^^




CH.04 끄읕(!)


다음 시간에도 이어서 SQL Injection에 관한 문제를 더 풀어볼 예정입니다.

한 가지 Tip을 드리자면,
SQL Injection 문제를 풀거나, 해당 개념을 공부할 때에는
옆에다가 MySQL을 뙇 켜놓고 명령어를 이것 저것 직접 쳐보면서 테스트해보시길 추천합니다(!)

그래야 더 잘 이해하고 오래 기억할 수 있습니다(!!!)


실습 환경을 구축하기 어려운 상황이라면,
온라인에서 sql 명령을 실행할 수 있는 사이트도 있으니
꼭 직접 테스트해보면서 공부하시길 바랍니다(!)


그럼, DB 입문자가 참고할 만한 링크를 투척하고 떠납니다(!)


mysqltutorial.org - MySQL Cheat Sheet
zentut.com - SQL CHEAT SHEET
MySQL 5.6 Reference Manual - Data Types
sqlfiddle.com - sql online test


TO BE CONTINUED… SEE YOU IN CH.05 (!)


comments powered by Disqus