Post

Whitespace Manipulation Zafiyeti: Kimlik Doğrulama Sistemlerinde Güvenlik Açığı

Whitespace Manipulation Zafiyeti: Kimlik Doğrulama Sistemlerinde Güvenlik Açığı

Bu makalede, PHP ve MySQL kullanarak basit bir login/signup sistemi oluşturacak ve sistemde bilerek bir güvenlik açığı bırakacağız. Bu açık, “Whitespace Manipulation” veya “Trailing Whitespace Vulnerability” olarak bilinen, kullanıcı adı alanında boşluk karakteri kullanımıyla ilgili bir zafiyettir ve bu sayede admin hesabına yetkisiz erişim sağlanabilmektedir.

Kodlar:
https://gitlab.com/fr0stb1rd/php-whitespace-auth-bypass-demo

🔧 Gereksinimler

  • PHP (7.0 veya üzeri)
  • MySQL veritabanı
  • Temel HTML/CSS bilgisi

🚀 Kurulum ve Ayarlamalar

Öncelikle MariaDB durumunu kontrol edelim:

1
2
3
4
5
6
7
┌──(fr0stb1rd㉿kali)-[~]
└─$ systemctl status mariadb
○ mariadb.service - MariaDB 11.8.1 database server
     Loaded: loaded (/usr/lib/systemd/system/mariadb.service; disabled; preset: disabled)
     Active: inactive (dead)
       Docs: man:mariadbd(8)
             https://mariadb.com/kb/en/library/systemd/

Servis açık değil. Önce servisi başlatalım:

1
sudo systemctl start mariadb

Şimdi durumu kontrol edelim:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
┌──(fr0stb1rd㉿kali)-[~]
└─$ systemctl status mariadb
● mariadb.service - MariaDB 11.8.1 database server
     Loaded: loaded (/usr/lib/systemd/system/mariadb.service; disabled; preset: disabled)
     Active: active (running) since Sat 2025-05-17 14:03:00 EDT; 15s ago
 Invocation: 6c338a33638f47bd944ab56366f98d08
       Docs: man:mariadbd(8)
             https://mariadb.com/kb/en/library/systemd/
    Process: 89123 ExecStartPre=/usr/bin/install -m 755 -o mysql -g root -d /var/run/mysqld (code=exited, status=0/SUCCESS)
    Process: 89125 ExecStartPre=/bin/sh -c [ ! -e /usr/bin/galera_recovery ] && VAR= ||   VAR=`/usr/bin/galera_recovery`; [ $? -eq 0 ]   >
    Process: 89199 ExecStartPost=/bin/rm -f /run/mysqld/wsrep-start-position (code=exited, status=0/SUCCESS)
    Process: 89201 ExecStartPost=/etc/mysql/debian-start (code=exited, status=0/SUCCESS)
   Main PID: 89178 (mariadbd)
     Status: "Taking your SQL requests now..."
      Tasks: 13 (limit: 61856)
     Memory: 328.5M (peak: 418.9M)
        CPU: 4.000s
     CGroup: /system.slice/mariadb.service
             └─89178 /usr/sbin/mariadbd

May 17 14:02:59 kali mariadbd[89178]: 2025-05-17 14:02:59 0 [Note] InnoDB: log sequence number 47763; transaction id 14
May 17 14:02:59 kali mariadbd[89178]: 2025-05-17 14:02:59 0 [Note] Plugin 'FEEDBACK' is disabled.
May 17 14:02:59 kali mariadbd[89178]: 2025-05-17 14:02:59 0 [Note] Plugin 'wsrep-provider' is disabled.
May 17 14:02:59 kali mariadbd[89178]: 2025-05-17 14:02:59 0 [Note] InnoDB: Loading buffer pool(s) from /var/lib/mysql/ib_buffer_pool
May 17 14:02:59 kali mariadbd[89178]: 2025-05-17 14:02:59 0 [Note] InnoDB: Buffer pool(s) load completed at 250517 14:02:59
May 17 14:03:00 kali mariadbd[89178]: 2025-05-17 14:03:00 0 [Note] Server socket created on IP: '127.0.0.1'.
May 17 14:03:00 kali mariadbd[89178]: 2025-05-17 14:03:00 0 [Note] mariadbd: Event Scheduler: Loaded 0 events
May 17 14:03:00 kali mariadbd[89178]: 2025-05-17 14:03:00 0 [Note] /usr/sbin/mariadbd: ready for connections.
May 17 14:03:00 kali mariadbd[89178]: Version: '11.8.1-MariaDB-4'  socket: '/run/mysqld/mysqld.sock'  port: 3306  Debian n/a
May 17 14:03:00 kali systemd[1]: Started mariadb.service - MariaDB 11.8.1 database server.

MariaDB’ye erişelim:

1
2
3
4
5
6
7
8
9
10
11
12
┌──(root㉿kali)-[/home/kali]
└─# mariadb
Welcome to the MariaDB monitor.  Commands end with ; or \g.
Your MariaDB connection id is 32
Server version: 11.8.1-MariaDB-4 Debian n/a

Copyright (c) 2000, 2018, Oracle, MariaDB Corporation Ab and others.

Support MariaDB developers by giving a star at https://github.com/MariaDB/server
Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

MariaDB [(none)]> 

💾 Veritabanı Oluşturma

İlk olarak MySQL veritabanımızı oluşturalım:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
CREATE DATABASE login_system;
USE login_system;

CREATE TABLE users (
    id INT AUTO_INCREMENT PRIMARY KEY,
    username VARCHAR(50) NOT NULL UNIQUE,
    password VARCHAR(255) NOT NULL,
    email VARCHAR(100) NOT NULL,
    is_admin BOOLEAN DEFAULT 0,
    registration_date DATETIME DEFAULT CURRENT_TIMESTAMP
);

-- Admin kullanıcısını oluşturalım (şifre: fr0stb1rd)
INSERT INTO users (username, password, email, is_admin) 
VALUES ('root', '$2y$12$J7exOjHTMTZVari0m6TzW.X5pUHBivIDiU3jukzbK0RXmcCG56Wwi', 'admin@example.com', 1);

Komutları terminale girelim:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
┌──(root㉿kali)-[/home/kali]
└─# mariadb
Welcome to the MariaDB monitor.  Commands end with ; or \g.
Your MariaDB connection id is 32
Server version: 11.8.1-MariaDB-4 Debian n/a

Copyright (c) 2000, 2018, Oracle, MariaDB Corporation Ab and others.

Support MariaDB developers by giving a star at https://github.com/MariaDB/server
Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

MariaDB [(none)]> CREATE DATABASE login_system;
Query OK, 1 row affected (0.004 sec)

MariaDB [(none)]> USE login_system;
Database changed
MariaDB [login_system]> 
MariaDB [login_system]> CREATE TABLE users (
    ->     id INT AUTO_INCREMENT PRIMARY KEY,
    ->     username VARCHAR(50) NOT NULL UNIQUE,
    ->     password VARCHAR(255) NOT NULL,
    ->     email VARCHAR(100) NOT NULL,
    ->     is_admin BOOLEAN DEFAULT 0,
    ->     registration_date DATETIME DEFAULT CURRENT_TIMESTAMP
    -> );
Query OK, 0 rows affected (0.009 sec)

MariaDB [login_system]> 
MariaDB [login_system]> -- Admin kullanıcısını oluşturalım (şifre: fr0stb1rd)
MariaDB [login_system]> INSERT INTO users (username, password, email, is_admin) 
    -> VALUES ('root', '$2y$12$J7exOjHTMTZVari0m6TzW.X5pUHBivIDiU3jukzbK0RXmcCG56Wwi', 'admin@example.com', 1);
Query OK, 1 row affected (0.001 sec)

MariaDB [login_system]> 

📁 Dosya Yapısı

Projemiz için aşağıdaki dosyaları oluşturacağız:

DosyaAçıklama
config.phpVeritabanı bağlantısı
index.phpAna sayfa
signup.phpKayıt sayfası
login.phpGiriş sayfası
profile.phpKullanıcı profil sayfası
admin.phpAdmin paneli
logout.phpÇıkış işlemi

Önce /var/www/html/ klasörüne gidip w adında bir klasör oluşturalım ve aşağıdaki dosyaları içine yazalım.

Tüm kodları tek seferde indirmek için:
php-whitespace-auth-bypass-demo-main.zip
Tüm dosyalar src klasörü içinde yer almaktadır. İsterseniz aşağıdan da kopyala yapıştır yapabilirsiniz. Tercih sizin.

📄 1. config.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?php
// Veritabanı bağlantı bilgileri
$host = "localhost";
$dbname = "login_system";
$username = "phpuser";
$password = "guclusifre";


// Veritabanı bağlantısı oluştur
try {
    $conn = new PDO("mysql:host=$host;dbname=$dbname", $username, $password);
    // Hata modunu ayarla
    $conn->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
} catch(PDOException $e) {
    echo "Bağlantı hatası: " . $e->getMessage();
    die();
}

// Oturum başlat
session_start();
?>

📄 2. index.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
<?php
require_once "config.php";
?>

<!DOCTYPE html>
<html lang="tr">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Ana Sayfa</title>
    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css">
    <style>
        body { padding-top: 50px; }
    </style>
</head>
<body>
    <div class="container">
        <div class="jumbotron">
            <h1 class="display-4">Hoş Geldiniz!</h1>
            <p class="lead">Basit PHP ve MySQL login sistemine hoş geldiniz.</p>
            <hr class="my-4">
            
            <?php if(isset($_SESSION["loggedin"]) && $_SESSION["loggedin"] === true): ?>
                <p>Merhaba, <b><?php echo htmlspecialchars($_SESSION["username"]); ?></b>. Hesabınıza giriş yaptınız.</p>
                <a href="profile.php" class="btn btn-primary">Profilim</a>
                <a href="logout.php" class="btn btn-danger">Çıkış Yap</a>
                
                <?php if(isset($_SESSION["is_admin"]) && $_SESSION["is_admin"] === true): ?>
                    <a href="admin.php" class="btn btn-warning">Admin Paneli</a>
                <?php endif; ?>
                
            <?php else: ?>
                <p>Devam etmek için lütfen giriş yapın veya kayıt olun.</p>
                <a href="login.php" class="btn btn-primary">Giriş Yap</a>
                <a href="signup.php" class="btn btn-success">Kayıt Ol</a>
            <?php endif; ?>
        </div>
    </div>
</body>
</html>

📄 3. signup.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
<?php
require_once "config.php";

// Form gönderildiğinde
if($_SERVER["REQUEST_METHOD"] == "POST"){
    
    // Değişkenleri tanımla ve boş değerleri kontrol et
    $username = $_POST["username"];
    $password = trim($_POST["password"]);
    $confirm_password = trim($_POST["confirm_password"]);
    $email = trim($_POST["email"]);
    
    $username_err = $password_err = $confirm_password_err = $email_err = "";
    
    // Kullanıcı adı kontrolü
    if(empty($username)){
        $username_err = "Lütfen bir kullanıcı adı girin.";
    } else {
        // İŞTE GÜVENLİK AÇIĞI: Burada kullanıcı adında boşluk kontrolü yapmıyoruz!
        // Normal şartlarda aşağıdaki gibi bir kontrol yapılmalıdır:
        // if(strpos($username, ' ') !== false) {
        //     $username_err = "Kullanıcı adı boşluk içeremez.";
        // }
        
        // Kullanıcı adı zaten var mı kontrol et
        $sql = "SELECT id FROM users WHERE username = :username";
        
        if($stmt = $conn->prepare($sql)){
            $stmt->bindParam(":username", $username, PDO::PARAM_STR);
            
            if($stmt->execute()){
                if($stmt->rowCount() == 1){
                    $username_err = "Bu kullanıcı adı zaten alınmış.";
                }
            } else {
                echo "Hata oluştu. Lütfen daha sonra tekrar deneyin.";
            }
            
            unset($stmt);
        }
    }
    
    // Şifre kontrolü
    if(empty($password)){
        $password_err = "Lütfen bir şifre girin.";     
    } elseif(strlen($password) < 6){
        $password_err = "Şifre en az 6 karakter olmalıdır.";
    }
    
    // Şifre onay kontrolü
    if(empty($confirm_password)){
        $confirm_password_err = "Lütfen şifreyi onaylayın.";     
    } else{
        if(empty($password_err) && ($password != $confirm_password)){
            $confirm_password_err = "Şifreler eşleşmiyor.";
        }
    }
    
    // E-posta kontrolü
    if(empty($email)){
        $email_err = "Lütfen bir e-posta adresi girin.";
    } elseif(!filter_var($email, FILTER_VALIDATE_EMAIL)){
        $email_err = "Geçersiz e-posta formatı.";
    }
    
    // Hata yoksa veritabanına ekle
    if(empty($username_err) && empty($password_err) && empty($confirm_password_err) && empty($email_err)){
        
        $sql = "INSERT INTO users (username, password, email) VALUES (:username, :password, :email)";
         
        if($stmt = $conn->prepare($sql)){
            // Parametreleri bağla
            $stmt->bindParam(":username", $username, PDO::PARAM_STR);
            $stmt->bindParam(":email", $email, PDO::PARAM_STR);
            
            // Şifreyi hash'le
            $hashed_password = password_hash($password, PASSWORD_DEFAULT);
            $stmt->bindParam(":password", $hashed_password, PDO::PARAM_STR);
            
            // Çalıştır
            if($stmt->execute()){
                // Giriş sayfasına yönlendir
                header("location: login.php");
            } else{
                echo "Bir şeyler yanlış gitti. Lütfen daha sonra tekrar deneyin.";
            }

            unset($stmt);
        }
    }
    
    unset($conn);
}
?>
 
<!DOCTYPE html>
<html lang="tr">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Kayıt Ol</title>
    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css">
    <style>
        body { padding-top: 50px; }
        .wrapper { width: 360px; margin: 0 auto; }
    </style>
</head>
<body>
    <div class="wrapper">
        <h2>Kayıt Ol</h2>
        <p>Hesap oluşturmak için formu doldurun.</p>
        <form action="<?php echo htmlspecialchars($_SERVER["PHP_SELF"]); ?>" method="post">
            <div class="form-group">
                <label>Kullanıcı Adı</label>
                <input type="text" name="username" class="form-control <?php echo (!empty($username_err)) ? 'is-invalid' : ''; ?>" value="<?php echo $username; ?>">
                <span class="invalid-feedback"><?php echo $username_err; ?></span>
            </div>    
            <div class="form-group">
                <label>E-posta</label>
                <input type="email" name="email" class="form-control <?php echo (!empty($email_err)) ? 'is-invalid' : ''; ?>" value="<?php echo $email; ?>">
                <span class="invalid-feedback"><?php echo $email_err; ?></span>
            </div>
            <div class="form-group">
                <label>Şifre</label>
                <input type="password" name="password" class="form-control <?php echo (!empty($password_err)) ? 'is-invalid' : ''; ?>" value="<?php echo $password; ?>">
                <span class="invalid-feedback"><?php echo $password_err; ?></span>
            </div>
            <div class="form-group">
                <label>Şifre Onayı</label>
                <input type="password" name="confirm_password" class="form-control <?php echo (!empty($confirm_password_err)) ? 'is-invalid' : ''; ?>" value="<?php echo $confirm_password; ?>">
                <span class="invalid-feedback"><?php echo $confirm_password_err; ?></span>
            </div>
            <div class="form-group">
                <input type="submit" class="btn btn-primary" value="Kayıt Ol">
                <input type="reset" class="btn btn-secondary ml-2" value="Sıfırla">
            </div>
            <p>Zaten bir hesabınız var mı? <a href="login.php">Giriş yapın</a>.</p>
        </form>
    </div>    
</body>
</html>

📄 4. login.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
<?php
require_once "config.php";

// Kullanıcı zaten giriş yapmışsa ana sayfaya yönlendir
if(isset($_SESSION["loggedin"]) && $_SESSION["loggedin"] === true){
    header("location: index.php");
    exit;
}

// Form gönderildiğinde
if($_SERVER["REQUEST_METHOD"] == "POST"){
    
    // Değişkenleri tanımla ve boş değerleri kontrol et
    $username = $_POST["username"];
    $password = trim($_POST["password"]);
    
    $username_err = $password_err = $login_err = "";
    
    // Kullanıcı adı kontrolü
    if(empty($username)){
        $username_err = "Lütfen kullanıcı adınızı girin.";
    }
    
    // Şifre kontrolü
    if(empty($password)){
        $password_err = "Lütfen şifrenizi girin.";
    }
    
    // Giriş bilgilerini doğrula
    if(empty($username_err) && empty($password_err)){
        $sql = "SELECT id, username, password, is_admin FROM users WHERE username = :username";
        
        if($stmt = $conn->prepare($sql)){
            $stmt->bindParam(":username", $username, PDO::PARAM_STR);
            
            if($stmt->execute()){
                // Kullanıcı var mı kontrol et
                if($stmt->rowCount() == 1){
                    if($row = $stmt->fetch()){
                        $id = $row["id"];
                        $username = $row["username"];
                        $hashed_password = $row["password"];
                        $is_admin = $row["is_admin"];
                        
                        if(password_verify($password, $hashed_password)){
                            // Şifre doğruysa oturum başlat
                            session_start();
                            
                            // Oturum değişkenlerini ayarla
                            $_SESSION["loggedin"] = true;
                            $_SESSION["id"] = $id;
                            $_SESSION["username"] = trim($username);
                            $_SESSION["is_admin"] = $is_admin;
                            
                            // Ana sayfaya yönlendir
                            header("location: index.php");
                        } else{
                            // Şifre yanlışsa hata mesajı
                            $login_err = "Geçersiz kullanıcı adı veya şifre.";
                        }
                    }
                } else{
                    // Kullanıcı yoksa hata mesajı
                    $login_err = "Geçersiz kullanıcı adı veya şifre.";
                }
            } else{
                echo "Hata oluştu. Lütfen daha sonra tekrar deneyin.";
            }
            
            unset($stmt);
        }
    }
    
    unset($conn);
}
?>
 
<!DOCTYPE html>
<html lang="tr">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Giriş</title>
    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css">
    <style>
        body { padding-top: 50px; }
        .wrapper { width: 360px; margin: 0 auto; }
    </style>
</head>
<body>
    <div class="wrapper">
        <h2>Giriş</h2>
        <p>Giriş yapmak için bilgilerinizi girin.</p>

        <?php 
        if(!empty($login_err)){
            echo '<div class="alert alert-danger">' . $login_err . '</div>';
        }        
        ?>

        <form action="<?php echo htmlspecialchars($_SERVER["PHP_SELF"]); ?>" method="post">
            <div class="form-group">
                <label>Kullanıcı Adı</label>
                <input type="text" name="username" class="form-control <?php echo (!empty($username_err)) ? 'is-invalid' : ''; ?>" value="<?php echo $username; ?>">
                <span class="invalid-feedback"><?php echo $username_err; ?></span>
            </div>    
            <div class="form-group">
                <label>Şifre</label>
                <input type="password" name="password" class="form-control <?php echo (!empty($password_err)) ? 'is-invalid' : ''; ?>">
                <span class="invalid-feedback"><?php echo $password_err; ?></span>
            </div>
            <div class="form-group">
                <input type="submit" class="btn btn-primary" value="Giriş Yap">
            </div>
            <p>Hesabınız yok mu? <a href="signup.php">Şimdi kayıt olun</a>.</p>
        </form>
    </div>
</body>
</html>

📄 5. profile.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
<?php
require_once "config.php";

// Kullanıcı giriş yapmamışsa login sayfasına yönlendir
if(!isset($_SESSION["loggedin"]) || $_SESSION["loggedin"] !== true){
    header("location: login.php");
    exit;
}

// Kullanıcı bilgilerini al
$sql = "SELECT email, registration_date FROM users WHERE username = :username";

if($stmt = $conn->prepare($sql)){
    $stmt->bindParam(":username", $_SESSION["username"], PDO::PARAM_INT);
    
    if($stmt->execute()){
        if($row = $stmt->fetch()){
            $email = $row["email"];
            $registration_date = $row["registration_date"];
        }
    }
    
    unset($stmt);
}
?>
 
<!DOCTYPE html>
<html lang="tr">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Profil</title>
    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css">
    <style>
        body { padding-top: 50px; }
        .wrapper { width: 600px; margin: 0 auto; }
    </style>
</head>
<body>
    <div class="wrapper">
        <div class="container">
            <div class="row">
                <div class="col-md-12">
                    <div class="card">
                        <div class="card-header">
                            <h2>Profil Sayfası</h2>
                        </div>
                        <div class="card-body">
                            <h5 class="card-title">Merhaba, <b><?php echo htmlspecialchars($_SESSION["username"]); ?></b></h5>
                            <p class="card-text">Hesap detaylarınız aşağıdadır:</p>
                            
                            <div class="form-group">
                                <label>Kullanıcı Adı:</label>
                                <p class="form-control-static"><?php echo htmlspecialchars($_SESSION["username"]); ?></p>
                            </div>
                            <div class="form-group">
                                <label>E-posta:</label>
                                <p class="form-control-static"><?php echo htmlspecialchars($email); ?></p>
                            </div>
                            <div class="form-group">
                                <label>Kayıt Tarihi:</label>
                                <p class="form-control-static"><?php echo htmlspecialchars($registration_date); ?></p>
                            </div>
                            <div class="form-group">
                                <label>Hesap Türü:</label>
                                <p class="form-control-static"><?php echo ($_SESSION["username"] == 'root') ? "Yönetici" : "Normal Kullanıcı"; ?></p>
                            </div>

                            <div class="form-group">
                                <label>Bayrak:</label>
                                <p class="form-control-static"><?php echo ($_SESSION["username"] == 'root') ? "fr0stb1rd.gitlab.io" : "🇹🇷"; ?></p>
                            </div>
                            
                            <a href="index.php" class="btn btn-primary">Ana Sayfa</a>
                            <a href="logout.php" class="btn btn-danger">Çıkış Yap</a>
                        </div>
                    </div>
                </div>
            </div>
        </div>
    </div>
</body>
</html>

📄 6. admin.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
<?php
require_once "config.php";

// Kullanıcı giriş yapmamışsa veya admin değilse ana sayfaya yönlendir
if(!isset($_SESSION["loggedin"]) || $_SESSION["loggedin"] !== true || !isset($_SESSION["is_admin"]) || $_SESSION["username"] !== 'root'){
    header("location: index.php");
    exit;
}

// Tüm kullanıcıları getir
$sql = "SELECT id, username, email, is_admin, registration_date FROM users";
$users = [];

if($stmt = $conn->prepare($sql)){
    if($stmt->execute()){
        $users = $stmt->fetchAll(PDO::FETCH_ASSOC);
    }
    
    unset($stmt);
}
?>
 
<!DOCTYPE html>
<html lang="tr">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Admin Paneli</title>
    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css">
    <style>
        body { padding-top: 50px; }
        .wrapper { width: 800px; margin: 0 auto; }
    </style>
</head>
<body>
    <div class="wrapper">
        <div class="container">
            <div class="row">
                <div class="col-md-12">
                    <div class="card">
                        <div class="card-header">
                            <h2>Admin Paneli</h2>
                        </div>
                        <div class="card-body">
                            <h5 class="card-title">Merhaba, <b><?php echo htmlspecialchars($_SESSION["username"]); ?></b></h5>
                            <p class="card-text">Admin paneline hoş geldiniz.</p>
                            
                            <!-- Gizli flag (güvenlik açığı sonucu erişilecek) -->
                            <div class="alert alert-success">
                                <strong>Gizli Flag:</strong> FLAG{b0sluk_karakteri_guvenlik_acigi}
                            </div>
                            
                            <h4>Kullanıcı Listesi</h4>
                            <table class="table table-bordered">
                                <thead>
                                    <tr>
                                        <th>ID</th>
                                        <th>Kullanıcı Adı</th>
                                        <th>E-posta</th>
                                        <th>Admin</th>
                                        <th>Kayıt Tarihi</th>
                                    </tr>
                                </thead>
                                <tbody>
                                    <?php foreach($users as $user): ?>
                                    <tr>
                                        <td><?php echo htmlspecialchars($user["id"]); ?></td>
                                        <td><?php echo htmlspecialchars($user["username"]); ?></td>
                                        <td><?php echo htmlspecialchars($user["email"]); ?></td>
                                        <td><?php echo ($user["is_admin"]) ? "Evet" : "Hayır"; ?></td>
                                        <td><?php echo htmlspecialchars($user["registration_date"]); ?></td>
                                    </tr>
                                    <?php endforeach; ?>
                                </tbody>
                            </table>
                            
                            <a href="index.php" class="btn btn-primary">Ana Sayfa</a>
                            <a href="logout.php" class="btn btn-danger">Çıkış Yap</a>
                        </div>
                    </div>
                </div>
            </div>
        </div>
    </div>
</body>
</html>

📄 7. logout.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<?php
// Oturumu başlat
session_start();
 
// Tüm oturum değişkenlerini temizle
$_SESSION = array();
 
// Oturum çerezini yok et
if (ini_get("session.use_cookies")) {
    $params = session_get_cookie_params();
    setcookie(session_name(), '', time() - 42000,
        $params["path"], $params["domain"],
        $params["secure"], $params["httponly"]
    );
}
 
// Oturumu sonlandır
session_destroy();
 
// Giriş sayfasına yönlendir
header("location: login.php");
exit;
?>

🧪 Sömürüyü Test Edelim

Apache durumuna bakalım:

1
2
3
4
5
6
7
┌──(root㉿kali)-[/home/kali]
└─# sudo systemctl status apache2

○ apache2.service - The Apache HTTP Server
     Loaded: loaded (/usr/lib/systemd/system/apache2.service; disabled; preset: disabled)
     Active: inactive (dead)
       Docs: https://httpd.apache.org/docs/2.4/

Servisi başlatalım:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
┌──(root㉿kali)-[/home/kali]
└─# sudo systemctl status apache2

● apache2.service - The Apache HTTP Server
     Loaded: loaded (/usr/lib/systemd/system/apache2.service; disabled; preset: disabled)
     Active: active (running) since Sat 2025-05-17 14:21:23 EDT; 20s ago
 Invocation: c048f8902d9f4a679b76d39aff68213e
       Docs: https://httpd.apache.org/docs/2.4/
    Process: 98766 ExecStart=/usr/sbin/apachectl start (code=exited, status=0/SUCCESS)
   Main PID: 98782 (apache2)
      Tasks: 6 (limit: 9372)
     Memory: 21.1M (peak: 21.6M)
        CPU: 97ms
     CGroup: /system.slice/apache2.service
             ├─98782 /usr/sbin/apache2 -k start
             ├─98785 /usr/sbin/apache2 -k start
             ├─98786 /usr/sbin/apache2 -k start
             ├─98787 /usr/sbin/apache2 -k start
             ├─98788 /usr/sbin/apache2 -k start
             └─98789 /usr/sbin/apache2 -k start

May 17 14:21:22 kali systemd[1]: Starting apache2.service - The Apache HTTP Server...
May 17 14:21:23 kali apachectl[98781]: AH00558: apache2: Could not reliably determine the server's fully qualified domain name, using 127>
May 17 14:21:23 kali systemd[1]: Started apache2.service - The Apache HTTP Server.
lines 1-21/21 (END)

Not: Eğer aşağıdaki bağlantı hatasını alırsak:

1
Bağlantı hatası: SQLSTATE[HY000] [1698] Access denied for user 'root'@'localhost'

Yeni bir veritabanı kullanıcısı oluşturmamız gerekecektir:

1
2
3
4
5
```sql
CREATE USER 'phpuser'@'localhost' IDENTIFIED BY 'guclusifre';
GRANT ALL PRIVILEGES ON login_system.* TO 'phpuser'@'localhost';
FLUSH PRIVILEGES;
```

Şimdi kullanıcılara göz atalım:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
┌──(root㉿kali)-[/home/kali/Desktop/w]
└─# mariadb
Welcome to the MariaDB monitor.  Commands end with ; or \g.
Your MariaDB connection id is 196
Server version: 11.8.1-MariaDB-4 Debian n/a

Copyright (c) 2000, 2018, Oracle, MariaDB Corporation Ab and others.

Support MariaDB developers by giving a star at https://github.com/MariaDB/server
Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

MariaDB [(none)]> USE login_system;
Reading table information for completion of table and column names
You can turn off this feature to get a quicker startup with -A

Database changed
MariaDB [login_system]> show tables;
+------------------------+
| Tables_in_login_system |
+------------------------+
| users                  |
+------------------------+
1 row in set (0.001 sec)

MariaDB [login_system]> select * from users;
+----+----------+--------------------------------------------------------------+-------------------+----------+---------------------+
| id | username | password                                                     | email             | is_admin | registration_date   |
+----+----------+--------------------------------------------------------------+-------------------+----------+---------------------+
|  1 | root     | $2y$12$J7exOjHTMTZVari0m6TzW.X5pUHBivIDiU3jukzbK0RXmcCG56Wwi | admin@example.com |        1 | 2025-05-17 16:45:12 |
+----+----------+--------------------------------------------------------------+-------------------+----------+---------------------+
1 row in set (0.001 sec)

MariaDB [login_system]> 

Login sayfasına gidip girişi kontrol edelim:

(Kullanıcı adı “root”, parola fr0stb1rd):

root_profile

Burada sorun yok. Normal bir kullanıcı oluşturalım ve deneyelim:

(Kullanıcı adı fr0stb1rd, parola fr0stb1rd olsun.):

fr0stb1rd_signup

Şimdi profile gidip profili kontrol edelim:

fr0stb1rd_profile

Her şey normal çalışıyor gibi görünüyor. Veritabanını kontrol edelim:

1
2
3
4
5
6
7
8
MariaDB [login_system]> select * from users;
+----+-----------+--------------------------------------------------------------+-------------------+----------+---------------------+
| id | username  | password                                                     | email             | is_admin | registration_date   |
+----+-----------+--------------------------------------------------------------+-------------------+----------+---------------------+
|  1 | root      | $2y$12$J7exOjHTMTZVari0m6TzW.X5pUHBivIDiU3jukzbK0RXmcCG56Wwi | admin@example.com |        1 | 2025-05-17 16:45:12 |
|  2 | fr0stb1rd | $2y$12$8vpH6tkafbmGp8ogr5sez.MqT6USYmCeWjLshil0peyXvIT6O2v9O | deneme@deneme.com |        0 | 2025-05-17 17:12:39 |
+----+-----------+--------------------------------------------------------------+-------------------+----------+---------------------+
2 rows in set (0.001 sec)

Bayrağın olduğu admin.php sayfasına gitmeyi deneyelim: http://localhost/w/admin.php

Gider gitmez bizi index.php’ye yönlendiriyor. Yani doğru çalışıyor.

🔑 Sahte root kullanıcısı

Şimdi zafiyeti test edelim. “ root” (baştaki boşluk karakteriyle) ve parola 123456 şeklinde kayıt olalım:

root_whitespace_signup

Şimdi giriş yapıp, root profiline gidelim:

root_whitespace_profile

Görüldüğü üzere, “ root” kullanıcısı “root” profiline erişebiliyor. bayrağı da fr0stb1rd.gitlab.io olarak yakalamış olduk.

Şimdi http://localhost/w/admin.php adresine gitmeyi deneyelim:

root_whitespace_admin

Görüldüğü üzere Gizli Flag: FLAG{b0sluk_karakteri_guvenlik_acigi} bayrağını yakalamış olduk. Hem de “root” kullanıcısı olmamamıza rağmen.

Veritabanını tekrar kontrol edelim:

1
2
3
4
5
6
7
8
9
10
11
MariaDB [login_system]> select * from users;
+----+-----------+--------------------------------------------------------------+-------------------+----------+---------------------+
| id | username  | password                                                     | email             | is_admin | registration_date   |
+----+-----------+--------------------------------------------------------------+-------------------+----------+---------------------+
|  1 | root      | $2y$12$J7exOjHTMTZVari0m6TzW.X5pUHBivIDiU3jukzbK0RXmcCG56Wwi | admin@example.com |        1 | 2025-05-17 16:45:12 |
|  2 | fr0stb1rd | $2y$12$8vpH6tkafbmGp8ogr5sez.MqT6USYmCeWjLshil0peyXvIT6O2v9O | deneme@deneme.com |        0 | 2025-05-17 17:12:39 |
|  3 |  root     | $2y$12$BPeMwSxy4h1lzqA8XrY50.hjIGuKBnyalm37YLn1vIg4SxdL4vsXW | fake@root.com     |        0 | 2025-05-17 17:19:11 |
+----+-----------+--------------------------------------------------------------+-------------------+----------+---------------------+
3 rows in set (0.001 sec)

MariaDB [login_system]> 

🔍 Güvenlik Açığı Açıklaması: Whitespace Manipulation

Bu projede bilerek bırakılan güvenlik açığı, “Whitespace Manipulation” veya “Trailing Whitespace Vulnerability” olarak bilinen bir zafiyet türüdür. Bu açık, kullanıcı adında boşluk karakterine (özellikle sonda veya başta) izin verilmesiyle ortaya çıkar.

Veritabanında admin kullanıcısı “root” olarak tanımlanmıştır. Ancak signup.php dosyasında kullanıcı adında boşluk kontrolü yapılmadığı için, bir saldırgan “ root” (başında bir boşluk olan) şeklinde kayıt olabilir. Sistem, bu iki kullanıcı adını farklı olarak algılar, çünkü karakter dizileri tam olarak eşleşmemektedir.

Bu açık, özellikle kullanıcı adı karşılaştırması yapılan sistemlerde ciddi güvenlik sorunlarına yol açabilir. Saldırganlar, yetkisiz erişim sağlamak için bu tür açıkları kullanabilirler.

Normal şartlarda kullanıcı adında boşluk karakteri olmaması için kontrol yapılmalıdır:

1
2
3
if(strpos($username, ' ') !== false) {
    $username_err = "Kullanıcı adı boşluk içeremez.";
}

Ancak bu kontrolü bilerek yapmadık. Bu sayede, bir saldırgan “ root” (başında boşluk olan) kullanıcı adıyla kayıt olabilir ve sonrasında bu hesapla giriş yaparak admin paneline erişebilir.

🛡️ Güvenlik Önlemleri ve Açığın Kapatılması

Gerçek bir projede bu tür güvenlik açıklarını önlemek için aşağıdaki önlemleri almalısınız:

1️⃣ Kullanıcı Adında Boşluk ve Özel Karakterlere İzin Vermeyin

Kullanıcı adı doğrulama işleminde boşluk ve özel karakterleri engelleyin:

1
2
3
4
5
6
7
8
// Kullanıcı adı kontrolü
if(empty($username)) {
    $username_err = "Lütfen bir kullanıcı adı girin.";
} elseif(strpos($username, ' ') !== false) {
    $username_err = "Kullanıcı adı boşluk içeremez.";
} elseif(!preg_match('/^[a-zA-Z0-9_]+$/', $username)) {
    $username_err = "Kullanıcı adı sadece harf, rakam ve alt çizgi içerebilir.";
}

2️⃣ Kullanıcı Girişlerini Her Zaman Temizleyin

Kullanıcıdan gelen tüm verileri işlemeden önce temizleyin:

1
2
$username = trim($_POST["username"]); // Başta ve sondaki boşlukları temizler
$username = htmlspecialchars($username); // HTML özel karakterleri dönüştürür

3️⃣ Kullanıcı Adlarını Karşılaştırırken trim() Fonksiyonu Kullanın

Veritabanı sorgularında ve karşılaştırmalarda trim() fonksiyonu kullanın:

1
$sql = "SELECT id FROM users WHERE TRIM(username) = TRIM(:username)";

Veya daha güvenli bir yaklaşım olarak, kullanıcı adını veritabanına kaydetmeden önce trim() uygulayın:

1
2
$username = trim($username);
$sql = "INSERT INTO users (username, password, email) VALUES (:username, :password, :email)";

4️⃣ Şifreleri Güçlü Bir Algoritma ile Hash’leyin

Şifreleri her zaman güçlü bir algoritma ile hash’leyin ve salt ekleyin:

1
$hashed_password = password_hash($password, PASSWORD_DEFAULT);

5️⃣ Ek Güvenlik Önlemleri

  • Admin hesaplarına erişimi IP kısıtlaması gibi ek güvenlik önlemleriyle koruyun
  • İki faktörlü kimlik doğrulama (2FA) uygulayın
  • Başarısız giriş denemelerini sınırlayın ve kaydedin
  • Güvenli oturum yönetimi uygulayın
  • CSRF ve XSS saldırılarına karşı koruma ekleyin

🛡️ Düzeltilmiş Kodlar

Sistemimizde bulunan güvenlik açıklarını kapatmak için her dosyada yapılması gereken düzeltmeleri aşağıda açıklıyoruz:

🛡️ 1. config.php - Güvenlik İyileştirmeleri

  • Veritabanı Bağlantı Güvenliği: Veritabanı bağlantı bilgilerini .env dosyasında saklayıp, bu dosyayı .gitignore ile versiyon kontrolünden hariç tutmak
  • Oturum Güvenliği: Güvenli oturum ayarları eklemek
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<?php
// .env dosyasından yükle
require_once "vendor/autoload.php";
$dotenv = Dotenv\Dotenv::createImmutable(__DIR__);
$dotenv->load();

// Veritabanı bağlantı bilgileri
$host = $_ENV['DB_HOST'];
$dbname = $_ENV['DB_NAME'];
$username = $_ENV['DB_USER'];
$password = $_ENV['DB_PASS'];

// Veritabanı bağlantısı oluştur
try {
    $conn = new PDO("mysql:host=$host;dbname=$dbname", $username, $password);
    // Hata modunu ayarla
    $conn->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
} catch(PDOException $e) {
    error_log("Bağlantı hatası: " . $e->getMessage());
    die("Veritabanı bağlantısı kurulamadı.");
}

// Güvenli oturum ayarları
ini_set('session.cookie_httponly', 1);
ini_set('session.use_only_cookies', 1);
ini_set('session.cookie_secure', 1);
session_start();
?>

🛡️ 2. signup.php - Güvenlik İyileştirmeleri

  • Whitespace Manipulation Zafiyeti: Kullanıcı adında boşluk karakterlerini engellemek
  • Veri Doğrulama: Güçlü veri doğrulama kontrolleri eklemek
  • Güçlü Şifre Politikası: Şifre karmaşıklığını kontrol etmek
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// Kullanıcı adı kontrolü
if(empty($username)){
    $username_err = "Lütfen bir kullanıcı adı girin.";
} elseif(strpos($username, ' ') !== false) {
    // Boşluk kontrolü eklendi
    $username_err = "Kullanıcı adı boşluk içeremez.";
} elseif(!preg_match('/^[a-zA-Z0-9_]+$/', $username)) {
    // Sadece alfanumerik karakterler ve alt çizgi
    $username_err = "Kullanıcı adı sadece harf, rakam ve alt çizgi içerebilir.";
} else {
    // Kullanıcı adı zaten var mı kontrol et
    $sql = "SELECT id FROM users WHERE username = :username";
    
    if($stmt = $conn->prepare($sql)){
        $stmt->bindParam(":username", $username, PDO::PARAM_STR);
        
        if($stmt->execute()){
            if($stmt->rowCount() == 1){
                $username_err = "Bu kullanıcı adı zaten alınmış.";
            }
        } else {
            echo "Hata oluştu. Lütfen daha sonra tekrar deneyin.";
        }
        
        unset($stmt);
    }
}

// Şifre karmaşıklık kontrolü
if(empty($password)){
    $password_err = "Lütfen bir şifre girin.";
} elseif(strlen($password) < 8){
    $password_err = "Şifre en az 8 karakter olmalıdır.";
} elseif(!preg_match('/[A-Z]/', $password) || !preg_match('/[a-z]/', $password) || !preg_match('/[0-9]/', $password)) {
    $password_err = "Şifre en az bir büyük harf, bir küçük harf ve bir rakam içermelidir.";
}

🛡️ 3. login.php - Güvenlik İyileştirmeleri

  • Whitespace Manipulation Zafiyeti: Giriş sırasında da kullanıcı adını temizlemek
  • Brute Force Koruması: Başarısız giriş denemelerini sınırlamak
  • Güvenli Oturum Yönetimi: Oturum bilgilerini güvenli şekilde saklamak
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// Kullanıcı adı ve şifre kontrolü
if(empty(trim($_POST["username"]))){
    $username_err = "Lütfen kullanıcı adını girin.";
} else{
    $username = trim($_POST["username"]);
    // Boşluk kontrolü ekle
    if(strpos($username, ' ') !== false) {
        $username_err = "Geçersiz kullanıcı adı formatı.";
    }
}

// Brute force koruması
if(!isset($_SESSION["login_attempts"])) {
    $_SESSION["login_attempts"] = 0;
    $_SESSION["last_attempt_time"] = time();
}

if($_SESSION["login_attempts"] >= 5) {
    $time_diff = time() - $_SESSION["last_attempt_time"];
    if($time_diff < 300) { // 5 dakika bekleme süresi
        $login_err = "Fazla sayıda başarısız giriş denemesi. Lütfen 5 dakika sonra tekrar deneyin.";
        // Formu gösterme
        exit();
    } else {
        // Süre dolmuşsa sıfırla
        $_SESSION["login_attempts"] = 0;
    }
}

🛡️ 4. admin.php - Güvenlik İyileştirmeleri

  • Yetkilendirme Kontrolü: Kullanıcı adı yerine rol tabanlı yetkilendirme kullanmak
  • IP Kısıtlaması: Admin paneline erişimi belirli IP adreslerine sınırlamak
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php
require_once "config.php";

// Güvenli IP kontrolü
$allowed_ips = ['127.0.0.1', '::1']; // İzin verilen IP'ler
if(!in_array($_SERVER['REMOTE_ADDR'], $allowed_ips)) {
    header("location: index.php");
    exit;
}

// Kullanıcı giriş yapmamışsa veya admin değilse ana sayfaya yönlendir
if(!isset($_SESSION["loggedin"]) || $_SESSION["loggedin"] !== true || !isset($_SESSION["is_admin"]) || $_SESSION["is_admin"] !== true){
    header("location: index.php");
    exit;
}
?>

🛡️ 5. Genel Güvenlik Önlemleri

ÖnlemAçıklama
CSRF KorumasıTüm formlarda CSRF token kullanmak
XSS KorumasıKullanıcı girdilerini görüntülemeden önce filtrelemek
Güvenli Şifre SaklamaBcrypt ile güçlü şifre hashleme
İki Faktörlü Kimlik Doğrulama (2FA)Özellikle admin hesapları için 2FA eklemek
Güvenli HTTP BaşlıklarıGüvenli HTTP başlıkları eklemek
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// CSRF Token oluşturma
function generateCSRFToken() {
    if (!isset($_SESSION['csrf_token'])) {
        $_SESSION['csrf_token'] = bin2hex(random_bytes(32));
    }
    return $_SESSION['csrf_token'];
}

// XSS Koruması
function escapeHTML($str) {
    return htmlspecialchars($str, ENT_QUOTES, 'UTF-8');
}

// Güvenli HTTP Başlıkları
header("Content-Security-Policy: default-src 'self'");
header("X-Content-Type-Options: nosniff");
header("X-Frame-Options: DENY");
header("X-XSS-Protection: 1; mode=block");

🛡️ Sonuç

Bu makalede “Whitespace Manipulation” zafiyetini inceledik. Bu zafiyet, kullanıcı adında boşluk karakterlerine izin verildiğinde, saldırganların admin hesaplarına erişim sağlayabilmesine olanak tanır.

Öğrendiklerimiz:

✅ Kullanıcı girdilerini her zaman temizleyin ve doğrulayın
✅ Boşluk karakterlerini ve özel karakterleri kullanıcı adlarında engelleyin
✅ Kimlik doğrulama sistemlerinde rol tabanlı yetkilendirme kullanın
✅ Güvenliği katmanlı olarak uygulayarak tek bir hatanın tüm sistemi tehlikeye atmasını engelleyin

Güvenlik, geliştirme sürecinin başından itibaren düşünülmesi gereken bir konudur. Basit görünen hatalar bile büyük güvenlik açıklarına yol açabilir.

Başka bir yazıda görüşmek üzere, esen kalın.

This post is licensed under CC BY 4.0 by the author.