You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
316 lines
12 KiB
316 lines
12 KiB
+++
|
|
author = "Maik de Kruif"
|
|
title = "Final Battle"
|
|
subtitle = "Challenge 24 - AdventOfCTF"
|
|
date = 2021-09-22T12:12:12+01:00
|
|
description = "A writeup for challenge 24 of AdventOfCTF."
|
|
cover = "img/writeups/adventofctf/2020/b915cb528c4b3d6fc4644f73ba8b829d.png"
|
|
tags = [
|
|
"AdventOfCTF",
|
|
"challenge",
|
|
"ctf",
|
|
"hacking",
|
|
"writeup",
|
|
"web",
|
|
"python",
|
|
"serialization",
|
|
"blockchain",
|
|
]
|
|
categories = [
|
|
"ctf",
|
|
"writeups",
|
|
"hacking",
|
|
]
|
|
+++
|
|
|
|
- Points: 2400
|
|
|
|
## Description
|
|
|
|
The final battle! The elves want revenge for their lost game! They have enhanced the tic-tac-toe game with blockchain technology. Cyber Security on the Blockchain will revolutionize everything, but most importantly ensure they will win this time. No cheating Santa!
|
|
|
|
Visit <https://24.adventofctf.com> to start the challenge.
|
|
|
|
## Recon
|
|
|
|
When opening the page, we're greeted with what looks like the same screen as we had on [challenge 20]({{< ref "challenge_20.md" >}}). The only noticeable difference is the addition of `blockchain? True` in the footer.
|
|
|
|
If we take a look at the source, we also find a comment with the following python code:
|
|
|
|
```py
|
|
# <!-- Development notes: Do not let santa see!
|
|
|
|
def hash_string(string):
|
|
return hashlib.md5(string.encode('utf-8')).hexdigest()
|
|
|
|
def hash_row(row):
|
|
conv = lambda i : i or ' '
|
|
res = [conv(i) for i in row]
|
|
return hash_string(' '.join(res))
|
|
|
|
def hash_board(board):
|
|
acc = ""
|
|
for row in board:
|
|
acc += hash_row(row)
|
|
return acc
|
|
|
|
def verify_chain(game):
|
|
board=game["board"]
|
|
chain = game["chain"]
|
|
|
|
if len(chain) > 0:
|
|
if board != chain[-1]["board"]:
|
|
return False
|
|
|
|
for i in range(len(chain)):
|
|
block=chain[i]
|
|
h = hash_board(block["board"])
|
|
h = hash_string(h + block["prev"])
|
|
if h != block["hash"]:
|
|
return False
|
|
return True
|
|
|
|
# -->
|
|
```
|
|
|
|
Just like in challenge 20, we alse find a game cookie:
|
|
|
|
```text
|
|
game=gAN9cQAoWAUAAABib2FyZHEBXXECKF1xAyhYAQAAAE9xBGgETmVdcQUoaARYAQAAAFhxBmgGZV1xByhoBGgGaAZlZVgEAAAAdHVybnEIaAZYCAAAAGZpbmlzaGVkcQmJWAYAAAB3aW5uZXJxClgAAAAAcQtYBAAAAHNhbmVxDIhYCgAAAGJsb2NrY2hhaW5xDYhYBQAAAGNoYWlucQ5dcQ8ofXEQKGgBXXERKF1xEihOTk5lXXETKE5OTmVdcRQoTk5oBmVlWAQAAABwcmV2cRVYIAAAAGNlZjIxNWM1YmU4Y2Y2M2ZjZjNkNDNlY2YyNTEwYjMzcRZYBAAAAGhhc2hxF1ggAAAAZTdkYzhlMWY3YTY3ODhiYzBjYjY4NDE1MzhiMjE2ZThxGHV9cRkoaAFdcRooXXEbKGgETk5lXXEcKE5OTmVdcR0oTk5oBmVlaBVoGGgXWCAAAABmYzkzMjM2YjVlZWE1ZjFkNTVlMmI1YjMwOGQ2NzM5MHEedX1xHyhoAV1xIChdcSEoaAROTmVdcSIoTmgGTmVdcSMoTk5oBmVlaBVoHmgXWCAAAABhOGRjMGQzZGEyOTBkMWU4OTRlYWFmZmNiOTgzOThjOXEkdX1xJShoAV1xJihdcScoaARoBE5lXXEoKE5oBk5lXXEpKE5OaAZlZWgVaCRoF1ggAAAAZTc0ZjViMjJmNTIxM2JhNGMyNDQ5NzU5Y2U5MWMyYWFxKnV9cSsoaAFdcSwoXXEtKGgEaAROZV1xLihOaAZoBmVdcS8oTk5oBmVlaBVoKmgXWCAAAABlZjI1NTE0ZGZmYmY4MjQ3Y2ZmNjA2M2JlOTBmMmQ1NHEwdX1xMShoAV1xMihdcTMoaARoBE5lXXE0KGgEaAZoBmVdcTUoTk5oBmVlaBVoMGgXWCAAAABlM2E0YzAzN2JkZjE1NGIzNDRlZDliZDE2NDNhNjI5ZHE2dX1xNyhoAV1xOChdcTkoaARoBE5lXXE6KGgEaAZoBmVdcTsoTmgGaAZlZWgVaDZoF1ggAAAAYzI0MGZhMTYxNzM3Yzk2N2VjZTVmZDk0NjcyYWIwZjhxPHV9cT0oaAFdcT4oXXE/KGgEaAROZV1xQChoBGgGaAZlXXFBKGgEaAZoBmVlaBVoPGgXWCAAAAAyNGJhNzE1ZGMwNTY4M2NlNDViOWUxOTFlZDE4OGI5Y3FCdWV1Lg==
|
|
```
|
|
|
|
## Finding the vulnerability
|
|
|
|
Using the same method as in challenge 20, we can take a look at the board:
|
|
|
|
```py
|
|
In [1]: import pickle
|
|
|
|
In [2]: import base64
|
|
|
|
In [3]: pickle.loads(base64.b64decode("gAN9cQAoWAUAAABib2FyZHEBXXECKF1xAyhYAQAAAE9xBGgETmVdcQUoaARYAQAAAFhxBmgGZV1xByhoBGgGaAZlZVgEAAAAdHVybnEIaAZYCAAAAGZpbmlzaGVkcQmJWAYAAAB3aW5uZXJxClgAAAAAcQtYBAAAAHNhbmVxDIhYCgAAAGJsb2NrY2hhaW5xDYhYBQAAAGNoYWlucQ5dcQ8ofXEQKGgBXXERKF1xEihOTk5lXXETKE5OTmVdcRQoTk5oBmVlWAQAAABwcmV2cRVYIAAAAGNlZjIxNWM1YmU4Y2Y2M2ZjZjNkNDNlY2YyNTEwYjMzcRZYBAAAAGhhc2hxF1ggAAAAZTdkYzhlMWY3YTY3ODhiYzBjYjY4NDE1MzhiMjE2ZThxGHV9cRkoaAFdcRooXXEbKGgETk5lXXEcKE5OTmVdcR0oTk5oBmVlaBVoGGgXWCAAAABmYzkzMjM2YjVlZWE1ZjFkNTVlMmI1YjMwOGQ2NzM5MHEedX1xHyhoAV1xIChdcSEoaAROTmVdcSIoTmgGTmVdcSMoTk5oBmVlaBVoHmgXWCAAAABhOGRjMGQzZGEyOTBkMWU4OTRlYWFmZmNiOTgzOThjOXEkdX1xJShoAV1xJihdcScoaARoBE5lXXEoKE5oBk5lXXEpKE5OaAZlZWgVaCRoF1ggAAAAZTc0ZjViMjJmNTIxM2JhNGMyNDQ5NzU5Y2U5MWMyYWFxKnV9cSsoaAFdcSwoXXEtKGgEaAROZV1xLihOaAZoBmVdcS8oTk5oBmVlaBVoKmgXWCAAAABlZjI1NTE0ZGZmYmY4MjQ3Y2ZmNjA2M2JlOTBmMmQ1NHEwdX1xMShoAV1xMihdcTMoaARoBE5lXXE0KGgEaAZoBmVdcTUoTk5oBmVlaBVoMGgXWCAAAABlM2E0YzAzN2JkZjE1NGIzNDRlZDliZDE2NDNhNjI5ZHE2dX1xNyhoAV1xOChdcTkoaARoBE5lXXE6KGgEaAZoBmVdcTsoTmgGaAZlZWgVaDZoF1ggAAAAYzI0MGZhMTYxNzM3Yzk2N2VjZTVmZDk0NjcyYWIwZjhxPHV9cT0oaAFdcT4oXXE/KGgEaAROZV1xQChoBGgGaAZlXXFBKGgEaAZoBmVlaBVoPGgXWCAAAAAyNGJhNzE1ZGMwNTY4M2NlNDViOWUxOTFlZDE4OGI5Y3FCdWV1Lg=="))
|
|
|
|
```
|
|
|
|
This time our board is quite a bit larger:
|
|
|
|
{{< code language="py" title="Board" >}}
|
|
|
|
```py
|
|
{
|
|
"blockchain": True,
|
|
"board": [
|
|
["O", "O", None],
|
|
["O", "X", "X"],
|
|
[None, "X", "X"]
|
|
],
|
|
"chain": [
|
|
{
|
|
"board": [
|
|
[None, None, None],
|
|
[None, None, None],
|
|
[None, None, "X"]
|
|
],
|
|
"hash": "e7dc8e1f7a6788bc0cb6841538b216e8",
|
|
"prev": "cef215c5be8cf63fcf3d43ecf2510b33"
|
|
},
|
|
{
|
|
"board": [
|
|
["O", None, None],
|
|
[None, None, None],
|
|
[None, None, "X"]
|
|
],
|
|
"hash": "fc93236b5eea5f1d55e2b5b308d67390",
|
|
"prev": "e7dc8e1f7a6788bc0cb6841538b216e8"
|
|
},
|
|
{
|
|
"board": [
|
|
["O", None, None],
|
|
[None, "X", None],
|
|
[None, None, "X"]
|
|
],
|
|
"hash": "a8dc0d3da290d1e894eaaffcb98398c9",
|
|
"prev": "fc93236b5eea5f1d55e2b5b308d67390"
|
|
},
|
|
{
|
|
"board": [
|
|
["O", "O", None],
|
|
[None, "X", None],
|
|
[None, None, "X"]
|
|
],
|
|
"hash": "e74f5b22f5213ba4c2449759ce91c2aa",
|
|
"prev": "a8dc0d3da290d1e894eaaffcb98398c9"
|
|
},
|
|
{
|
|
"board": [
|
|
["O", "O", None],
|
|
[None, "X", "X"],
|
|
[None, None, "X"]
|
|
],
|
|
"hash": "ef25514dffbf8247cff6063be90f2d54",
|
|
"prev": "e74f5b22f5213ba4c2449759ce91c2aa"
|
|
},
|
|
{
|
|
"board": [
|
|
["O", "O", None],
|
|
["O", "X", "X"],
|
|
[None, None, "X"]
|
|
],
|
|
"hash": "e3a4c037bdf154b344ed9bd1643a629d",
|
|
"prev": "ef25514dffbf8247cff6063be90f2d54"
|
|
},
|
|
{
|
|
"board": [
|
|
["O", "O", None],
|
|
["O", "X", "X"],
|
|
[None, "X", "X"]
|
|
],
|
|
"hash": "c240fa161737c967ece5fd94672ab0f8",
|
|
"prev": "e3a4c037bdf154b344ed9bd1643a629d"
|
|
}
|
|
],
|
|
"finished": False,
|
|
"sane": True,
|
|
"turn": "O",
|
|
"winner": ""
|
|
}
|
|
```
|
|
|
|
{{< /code >}}
|
|
|
|
We can see that a `chain` value has been added. From the title of this challenge, we can say that this is the blockchain that we likely have to bypass.
|
|
|
|
## Exploit
|
|
|
|
Cracking this shouldn't be too hard. Let's start by copying our script from challenge 20 and resetting the chain:
|
|
|
|
```py
|
|
import base64
|
|
import pickle
|
|
|
|
board_b64 = "gAN9cQAoWAUAAABib2FyZHEBXXECKF1xAyhYAQAAAE9xBGgETmVdcQUoaARYAQAAAFhxBmgGZV1xByhOaAZoBmVlWAQAAAB0dXJucQhoBFgIAAAAZmluaXNoZWRxCYlYBgAAAHdpbm5lcnEKWAAAAABxC1gEAAAAc2FuZXEMiFgKAAAAYmxvY2tjaGFpbnENiFgFAAAAY2hhaW5xDl1xDyh9cRAoaAFdcREoXXESKE5OTmVdcRMoTk5OZV1xFChOTmgGZWVYBAAAAHByZXZxFVggAAAAY2VmMjE1YzViZThjZjYzZmNmM2Q0M2VjZjI1MTBiMzNxFlgEAAAAaGFzaHEXWCAAAABlN2RjOGUxZjdhNjc4OGJjMGNiNjg0MTUzOGIyMTZlOHEYdX1xGShoAV1xGihdcRsoaAROTmVdcRwoTk5OZV1xHShOTmgGZWVoFWgYaBdYIAAAAGZjOTMyMzZiNWVlYTVmMWQ1NWUyYjViMzA4ZDY3MzkwcR51fXEfKGgBXXEgKF1xIShoBE5OZV1xIihOaAZOZV1xIyhOTmgGZWVoFWgeaBdYIAAAAGE4ZGMwZDNkYTI5MGQxZTg5NGVhYWZmY2I5ODM5OGM5cSR1fXElKGgBXXEmKF1xJyhoBGgETmVdcSgoTmgGTmVdcSkoTk5oBmVlaBVoJGgXWCAAAABlNzRmNWIyMmY1MjEzYmE0YzI0NDk3NTljZTkxYzJhYXEqdX1xKyhoAV1xLChdcS0oaARoBE5lXXEuKE5oBmgGZV1xLyhOTmgGZWVoFWgqaBdYIAAAAGVmMjU1MTRkZmZiZjgyNDdjZmY2MDYzYmU5MGYyZDU0cTB1fXExKGgBXXEyKF1xMyhoBGgETmVdcTQoaARoBmgGZV1xNShOTmgGZWVoFWgwaBdYIAAAAGUzYTRjMDM3YmRmMTU0YjM0NGVkOWJkMTY0M2E2MjlkcTZ1fXE3KGgBXXE4KF1xOShoBGgETmVdcTooaARoBmgGZV1xOyhOaAZoBmVlaBVoNmgXWCAAAABjMjQwZmExNjE3MzdjOTY3ZWNlNWZkOTQ2NzJhYjBmOHE8dWV1Lg=="
|
|
|
|
game = pickle.loads(base64.b64decode(board_b64))
|
|
|
|
game["board"] = [['X', 'X', 'X'], ['O', None, 'O'], [None, None, None]]
|
|
game["chain"] = []
|
|
game["finished"] = True
|
|
game["winner"] = 'X'
|
|
game["turn"] = 'O'
|
|
|
|
print(base64.b64encode(pickle.dumps(data)))
|
|
```
|
|
|
|
Sadly, just submitting the result of this script doesn't work. We have to fill the chain.
|
|
|
|
To find out how the chain works, let's take another look at the `verify_chain()` function:
|
|
|
|
```py
|
|
def verify_chain(game):
|
|
board=game["board"]
|
|
chain = game["chain"]
|
|
|
|
if len(chain) > 0:
|
|
if board != chain[-1]["board"]:
|
|
return False
|
|
|
|
for i in range(len(chain)):
|
|
block=chain[i]
|
|
h = hash_board(block["board"])
|
|
h = hash_string(h + block["prev"])
|
|
if h != block["hash"]:
|
|
return False
|
|
return True
|
|
```
|
|
|
|
We can see that, to verify the game, the function loops through all the blocks in the chain. It then calculates a hash based on the board, and the previous hash and compares that to the actual hash in the block (the user provided one).
|
|
|
|
To crack this, we can simply reverse this algorithm. To do this, I grabbed the script from the HTML source, and added a crack method like so:
|
|
|
|
{{< code language="py" title="solve.py" >}}
|
|
|
|
```python
|
|
import hashlib
|
|
import pickle
|
|
import base64
|
|
|
|
|
|
def load_base64(base64_string):
|
|
return pickle.loads(base64.b64decode(base64_string))
|
|
|
|
|
|
def export_base64(game):
|
|
return base64.b64encode(pickle.dumps(game)).decode()
|
|
|
|
|
|
def hash_string(string):
|
|
return hashlib.md5(string.encode('utf-8')).hexdigest()
|
|
|
|
|
|
def hash_row(row):
|
|
def conv(i): return i or ' '
|
|
res = [conv(i) for i in row]
|
|
return hash_string(' '.join(res))
|
|
|
|
|
|
def hash_board(board):
|
|
acc = ""
|
|
for row in board:
|
|
acc += hash_row(row)
|
|
return acc
|
|
|
|
|
|
def verify_chain(game):
|
|
board = game["board"]
|
|
chain = game["chain"]
|
|
|
|
if len(chain) > 0:
|
|
if board != chain[-1]["board"]:
|
|
return False
|
|
|
|
for block in chain:
|
|
h = hash_board(block["board"])
|
|
h = hash_string(h + block["prev"])
|
|
if h != block["hash"]:
|
|
return False
|
|
return True
|
|
|
|
|
|
def crack(game):
|
|
game["board"] = [['X', 'X', 'X'], ['O', None, 'O'], [None, None, None]]
|
|
game["chain"] = []
|
|
game["finished"] = True
|
|
game["winner"] = 'X'
|
|
game["turn"] = 'O'
|
|
|
|
board = game["board"]
|
|
|
|
h = hash_board(board)
|
|
h = hash_string(h)
|
|
|
|
game["chain"].append({
|
|
"board": board,
|
|
"hash": h,
|
|
"prev": ""
|
|
})
|
|
|
|
|
|
if __name__ == "__main__":
|
|
game = load_base64("gAN9cQAoWAUAAABib2FyZHEBXXECKF1xAyhYAQAAAE9xBGgETmVdcQUoaARYAQAAAFhxBmgGZV1xByhOaAZoBmVlWAQAAAB0dXJucQhoBFgIAAAAZmluaXNoZWRxCYlYBgAAAHdpbm5lcnEKWAAAAABxC1gEAAAAc2FuZXEMiFgKAAAAYmxvY2tjaGFpbnENiFgFAAAAY2hhaW5xDl1xDyh9cRAoaAFdcREoXXESKE5OTmVdcRMoTk5OZV1xFChOTmgGZWVYBAAAAHByZXZxFVggAAAAY2VmMjE1YzViZThjZjYzZmNmM2Q0M2VjZjI1MTBiMzNxFlgEAAAAaGFzaHEXWCAAAABlN2RjOGUxZjdhNjc4OGJjMGNiNjg0MTUzOGIyMTZlOHEYdX1xGShoAV1xGihdcRsoaAROTmVdcRwoTk5OZV1xHShOTmgGZWVoFWgYaBdYIAAAAGZjOTMyMzZiNWVlYTVmMWQ1NWUyYjViMzA4ZDY3MzkwcR51fXEfKGgBXXEgKF1xIShoBE5OZV1xIihOaAZOZV1xIyhOTmgGZWVoFWgeaBdYIAAAAGE4ZGMwZDNkYTI5MGQxZTg5NGVhYWZmY2I5ODM5OGM5cSR1fXElKGgBXXEmKF1xJyhoBGgETmVdcSgoTmgGTmVdcSkoTk5oBmVlaBVoJGgXWCAAAABlNzRmNWIyMmY1MjEzYmE0YzI0NDk3NTljZTkxYzJhYXEqdX1xKyhoAV1xLChdcS0oaARoBE5lXXEuKE5oBmgGZV1xLyhOTmgGZWVoFWgqaBdYIAAAAGVmMjU1MTRkZmZiZjgyNDdjZmY2MDYzYmU5MGYyZDU0cTB1fXExKGgBXXEyKF1xMyhoBGgETmVdcTQoaARoBmgGZV1xNShOTmgGZWVoFWgwaBdYIAAAAGUzYTRjMDM3YmRmMTU0YjM0NGVkOWJkMTY0M2E2MjlkcTZ1fXE3KGgBXXE4KF1xOShoBGgETmVdcTooaARoBmgGZV1xOyhOaAZoBmVlaBVoNmgXWCAAAABjMjQwZmExNjE3MzdjOTY3ZWNlNWZkOTQ2NzJhYjBmOHE8dWV1Lg==")
|
|
crack(game)
|
|
print(export_base64(game))
|
|
```
|
|
|
|
{{< /code >}}
|
|
|
|
After running this script, we get the following result:
|
|
|
|
```text
|
|
gASVsgAAAAAAAAB9lCiMBWJvYXJklF2UKF2UKIwBWJRoBGgEZV2UKIwBT5ROaAZlXZQoTk5OZWWMBHR1cm6UaAaMCGZpbmlzaGVklIiMBndpbm5lcpRoBIwEc2FuZZSIjApibG9ja2NoYWlulIiMBWNoYWlulF2UfZQojAVib2FyZJRoAowEaGFzaJSMIDZkZTM2NDM0OTAwZTI4YTdlYWYwNDhhYjBhY2JlNjA0lIwEcHJldpSMAJR1YXUu
|
|
```
|
|
|
|
When we set this and refresh the page, we get the following message: `"Game goes to: X NOVI{blockchain_cyb3r_security} Thank you for playing Advent of CTF! I hope you have a great christmas!"`.
|
|
|
|
## Solution
|
|
|
|
We got the flag! It is `NOVI{blockchain_cyb3r_security}`.
|
|
|