@app.route('/create_game', methods=['POST']) # 这里是上半部分
def create_game():
if 'username' not in session:
return redirect(url_for('index'))
# generate unique code
while True:
code = ''.join(random.choices(string.ascii_uppercase + string.digits, k=6))
if code not in games:
break
# prepare game with selected language word list
# determine language (default to first available)
language = request.form.get('language', None)
if not language or '.' in language:
language = LANGUAGES[0] if LANGUAGES else None
# load words for this language
word_list = []
if language:
wl_path = os.path.join(WORDS_DIR, f"{language}.txt")
try:
with open(wl_path) as wf:
word_list = [line.strip() for line in wf if line.strip()]
except IOError as e:
print(e)
word_list = []
接着看 /create_game 的后半部分,我们发现后半部分把 word_list 这个内容通过 words = random.sample(word_list, 25) 赋值给 words,这也就解释了为什么后续结果显示有 25 个 flag 结果。这里解释一下 random.sample(列表, 数量),前者是列表,后者是数量,意思是选取列表中一定数量的内容赋值给 words。
之后将 words 存入 game 这个字典里面,把字典 game 那段的操作理解为 game[board]=words,赋完之后进行:
python
games[code] = game
return redirect(url_for('game_view', code=code))
这一段的意思是把 game 这个字典全局接入 game[code],然后重定向到 game_view 访问内容。
python
if not word_list: # 这是 /create_game 的后半部分
word_list = []
# pick 25 random words
words = random.sample(word_list, 25) if len(word_list) >= 25 else random.sample(word_list * 25, 25)
start_team = random.choice(['red', 'blue'])
counts = {
'red': 9 if start_team == 'red' else 8,
'blue': 9 if start_team == 'blue' else 8
}
# assign colors by index to support duplicate words
indices = list(range(25))
random.shuffle(indices)
colors_list = [None] * 25
# one assassin
assassin_idx = indices.pop()
colors_list[assassin_idx] = 'assassin'
# team words
for team in ['red', 'blue']:
for _ in range(counts[team]):
idx = indices.pop()
colors_list[idx] = team
# the rest are neutral
for idx in indices:
colors_list[idx] = 'neutral'
# determine hard mode (double win points)
hard_mode = bool(request.form.get('hard_mode'))
# initialize game state
game = {
'players': [session['username']],
'board': words,
'colors': colors_list,
'revealed': [False] * 25,
'start_team': start_team,
'team_color': start_team,
'clue_giver': None,
'clue': None,
'guesses_remaining': 0,
'score': 0,
'hard_mode': hard_mode,
'bots': []
}
games[code] = game
return redirect(url_for('game_view', code=code))
3. 对于 /join_game 的分析
这段代码主要是关于其他用户如何通过邀请码 code 加入 game,其中 game = games.get(code),将局部的 code 接入全局,再结合前面 games[code] = game,现在全局 game 和 game[code] 实际上是互通的,当后续其他用户通过邀请码加入就可以获取全局内容,这个通过以下代码也可以体现:
python
if session['username'] in game['players']:
return redirect(url_for('game_view', code=code))
python
@app.route('/join_game', methods=['POST'])
def join_game():
if 'username' not in session:
return redirect(url_for('index'))
code = request.form.get('code', '').strip().upper()
game = games.get(code)
if not game or len(game['players']) >= 2:
flash('Invalid or full game code')
return redirect(url_for('lobby'))
if session['username'] in game['players']:
return redirect(url_for('game_view', code=code))
game['players'].append(session['username'])
# assign the joiner as clue giver
game['clue_giver'] = session['username']
return redirect(url_for('game_view', code=code))
看到这里你可能还是有点懵,别担心继续往下看,下面我将介绍如何利用,并在后续梳理整个触发的链路。
b. 具体利用思路
根据前面的代码审计,我们可以得知 join 这个函数,如果出现 / 会抛弃前面的路径,那我们能否将其中的参数 language 改为 /flag(这里路径穿越 ../../../../,但是由于前面好像有验证,如果出现 . 就会默认选取第一种语言)。我们用 bp 修改 language 为 /flag,之后抛弃掉前面的路径,最后 wl_path 打开的内容也是 /flag 路径下的,并把该内容传给 word_list,然后通过 random.sample(word_list, 25)(这也就是为啥后续会出现 25 个 flag)传给 words,words 再把值存入字典 game,但是这里只通过一个用户无法访问到 flag 的内容。
我们还需要利用一个新用户,用它的邀请码加入 game,因为全局 game 和局部 game[code] 互通,相当于当另外的用户使用 code 就可以读取到所有全局内容,然后重定向到 game_view 得到 flag 的内容。