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.
		
		
		
		
		
			
		
			
				
					
					
						
							319 lines
						
					
					
						
							12 KiB
						
					
					
				
			
		
		
	
	
							319 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",
 | |
| ]
 | |
| aliases = [
 | |
|     "challenge_24"
 | |
| ]
 | |
| +++
 | |
| 
 | |
| -   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:
 | |
| 
 | |
| {{< collapsible-block badge="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": ""
 | |
| }
 | |
| ```
 | |
| 
 | |
| {{< /collapsible-block >}}
 | |
| 
 | |
| 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:
 | |
| 
 | |
| {{< collapsible-block badge="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))
 | |
| ```
 | |
| 
 | |
| {{< /collapsible-block >}}
 | |
| 
 | |
| 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}`.
 | |
| 
 |