1 Punkte von GN⁺ 4 시간 전 | 1 Kommentare | Auf WhatsApp teilen
  • In minimalen Container-Images fehlen curl oder wget oft, daher ist ein Umweg nützlich, um die Erreichbarkeit interner Services ohne Paketinstallation zu prüfen
  • Die Bash-Umleitung /dev/tcp/host/port kann einen TCP-Socket öffnen, sodass sich ein HTTP/1.1-Request-String direkt schreiben und die Antwort auslesen lässt
  • /dev/tcp ist kein Dateisystempfad, sondern eine interne Bash-Funktion, daher funktionieren ls /dev/tcp oder übliche Dateizugriffe aus anderen Shells nicht
  • Diese Methode ist eine einfache Debugging-Technik, die keine Redirects, chunked Responses, Komprimierung, Retries oder TLS verarbeitet; ohne Connection: close kann cat blockieren
  • Für alltägliche HTTP-Aufgaben ist curl die richtige Wahl, aber in kleinen Containern, in denen sich zusätzliche Tools schwer hinzufügen lassen, reicht sie für eine schnelle Konnektivitätsprüfung aus

HTTP-Request mit einem Bash-Dateideskriptor aufbauen

  • In einem internen Docker-Netzwerk musste geprüft werden, ob der /health-Endpoint eines anderen Services erreichbar ist, aber im Image waren weder curl noch wget vorhanden
  • Bash kann TCP-Sockets an Dateideskriptoren binden, sodass sich ein HTTP-Request wie folgt direkt formulieren und senden lässt
exec 3<>/dev/tcp/service/8642
printf 'GET /health HTTP/1.1\r\nHost: service\r\nConnection: close\r\n\r\n' >&3
cat <&3
  • service muss ein Hostname sein, der vom Ausführungsort aus aufgelöst und erreicht werden kann
    • Es kann sich um einen im Docker-Netzwerk konfigurierten Container- oder Service-Namen handeln
    • Auch ein auflösbarer DNS-Name kann verwendet werden
    • Host und Port müssen an die jeweilige Umgebung angepasst werden
  • Die Ausgabe der Antwort enthält Statuszeile, Header, Leerzeile und Body zusammen
  • Um Header hinzuzufügen, muss vor der abschließenden Leerzeile des Requests einfach eine weitere mit \r\n endende Zeile eingefügt werden
exec 3<>/dev/tcp/service/8642
printf 'GET /v1/models HTTP/1.1\r\nHost: service\r\nAuthorization: Bearer %s\r\nConnection: close\r\n\r\n' "$API_KEY" >&3
cat <&3

Warum /dev/tcp keine echte Datei ist

  • /dev/tcp ist keine echte Gerätedatei, sondern eine von Bash verarbeitete Umleitung
    • Da auf dem Datenträger kein entsprechender Pfad existiert, schlägt ls /dev/tcp fehl
    • Auch wenn man in einer anderen Shell cat /dev/tcp/... ausführt, entsteht ein Fehler
  • Laut dem Bash manual versucht Bash bei /dev/tcp/host/port einen TCP-Socket zu öffnen, wenn host ein gültiger Hostname oder eine Internetadresse ist und port eine Ganzzahl als Portnummer oder ein Service-Name ist
  • Bash führt die DNS-Auflösung und connect(2) aus, und exec 3<> verbindet den Socket mit dem Dateideskriptor 3, sodass Lesen und Schreiben möglich werden

Kein Ersatz für einen HTTP-Client, sondern ein temporäres Prüfwerkzeug

  • Dieser Ansatz ist kein echter HTTP-Client und verarbeitet daher keine Redirects, chunked Responses, Komprimierung, Retries, TLS usw.
  • Der Header Connection: close ist wichtig
    • Ohne ihn kann der Server die Verbindung entsprechend dem Standardverhalten von HTTP/1.1 offen halten
    • In diesem Fall kann cat <&3 auf EOF warten und nicht enden
  • Eine Hülle wie timeout 6 bash -c '...' kann zusätzlich absichern, falls die Verbindung nicht geschlossen wird
  • /dev/tcp öffnet einen rohen Socket und gilt daher nur für unverschlüsseltes HTTP; für https wird openssl s_client benötigt
  • Es handelt sich nicht um eine POSIX-Funktion, sondern um eine Bash-Funktion; in dash als /bin/sh unter Debian oder in zsh funktioniert das nicht, daher muss bash direkt aufgerufen werden
  • Es ist eine Compile-Time-Option, die beim Bash-Build mit --enable-net-redirections aktiviert wird
  • Insgesamt ist das also weniger ein allgemeines Werkzeug als Ersatz für curl, sondern eher geeignet, um in kleinen Containern ohne zusätzliche Installationsmöglichkeit schnell die Konnektivität zu prüfen

1 Kommentare

 
GN⁺ 4 시간 전
Hacker-News-Kommentare
  • Als ich Ende der 90er als Kind entdeckte, dass man sich mit telnet auf Port 80, 25 und 110 verbinden und direkt mit Servern sprechen konnte, war ich völlig verblüfft.
    Man konnte einfache Anfragen wie GET / HTTP/1.1 von Hand eintippen, auf Port 25 mit HELO, mail-from und mail-to E-Mails verschicken und per POP3 die Mailbox-Liste sowie einzelne Nachrichten abrufen.
    Diese Erfahrung war der Anfang der Erkenntnis, dass es „keine Magie“ gibt: Alle Teile eines Computers sind von Menschen gemacht, und mit genug Mühe kann man sie bis zu einem gewissen Grad verstehen.
    In Zukunft wird man das meiste wohl Agenten überlassen, aber für Menschen, die ohne Filter von Modellen und Sicherheitsmechanismen lernen wollen, wie Dinge wirklich funktionieren, dürften in vielen Systemen noch interessante Schlupflöcher bleiben.

    • In der Zeit vor DKIM/SPF und bei lockerer SMTP-Server-Authentifizierung konnte man E-Mails mit Adressen wie jacques.chirac@elysee.fr verschicken und vor Freunden wie ein Hacker wirken.
    • Damals gab es nicht nur weder DKIM noch SPF, sondern die meisten SMTP-Server waren Open Relays, die E-Mails von jedem an jeden annahmen.
    • Letztlich sind es alles nur Textdateien.
      Es war ein Aufbau aus vielen Arten, strukturierte Textdateien zu erzeugen, zu versenden und zu lesen, überlagert von jeder Menge Akronymen.
      Als mir eines Tages klar wurde, dass sogar Datenbanken Textdateien sind, musste ich mich erst einmal kurz hinsetzen.
    • Im letzten Jahrhundert habe ich im Unternehmen persönliche E-Mails gelesen und verschickt, indem ich mich jeweils per telnet mit POP3 und SMTP verbunden habe.
    • Mit HTTP/2 geht das nicht mehr so, aber zum Glück sprechen fast alle Server noch HTTP/1.
      TLS funktioniert mit telnet ebenfalls nicht, und viele Server liefern für HTTP-Anfragen nur noch Redirects zurück.
      Mit openssl s_client statt telnet kann man zwar Text durch TLS tunneln, aber das fühlt sich ein wenig wie ein Trick an.
      Schade ist auch, dass viele moderne Protokolle binäre Kodierung bevorzugen und sich ohne Spezialwerkzeuge nicht mehr so leicht auf Leitungsebene anfassen lassen.
      Trotzdem wird es wohl auch in Zukunft Menschen geben, die sich damit beschäftigen; alte Techniken wie Feuer mit Stöcken zu machen oder Lehmziegel zu brennen sind unterhaltsam und manchmal tatsächlich nützlich.
      Eigentlich macht gerade AI solche Experimente sogar leichter, weil man statt RFCs zu durchforsten einfach ein LLM fragen und sich zum Beispiel die meisten üblichen IMAP-Befehle erklären lassen kann.
  • In zsh gibt es zusätzlich zu Bashs /dev/tcp die Module zsh/net/tcp und zsh/zftp.
    https://zsh.sourceforge.io/Doc/Release/TCP-Function-System.h...
    https://zsh.sourceforge.io/Doc/Release/Zsh-Modules.html#The-...
    https://zsh.sourceforge.io/Doc/Release/Zftp-Function-System....

  • In Plan 9 gab es mit /net ein echtes synthetisches Dateisystem, mit dem man solche Dinge und noch mehr aus jedem Programm heraus tun konnte.
    Man konnte sogar das /net einer anderen Maschine per 9P-Protokoll mounten und wie ein spontanes VPN verwenden; mit 9front lässt sich das unter Linux ausprobieren.
    Auch in Go-Bibliotheken sieht man Spuren des Plan-9-artigen /net, vermutlich ein Vermächtnis von Rob Pike.

  • Mit example.com funktioniert es gut.
    Wenn man mit exec 3<>/dev/tcp/example.com/80 öffnet, dann printf 'GET / HTTP/1.1\r\nHost: example.com\r\nConnection: close\r\n\r\n' >&3 sendet und danach cat <&3 ausführt, bekommt man HTTP/1.1 200 OK.
    Heutzutage gibt es so wenige Domains, die HTTPS nicht erzwingen, dass man für solche Tests am Ende fast immer bei example.com landet.

    • Captive Portals in öffentlichem WLAN sind ebenfalls ein Fall, in dem example.com nützlich ist.
      Wenn man im Browser http://example.com aufruft, wird man wieder auf die Captive-Portal-Seite umgeleitet und kann den Internetzugang erneut freischalten.
    • Es funktioniert auch, wenn man in printf echte Zeilenumbrüche verwendet.
      \r sollte korrektheitshalber dabei sein, aber es geht auch ohne.
  • Man kann den Witz machen, dass alle für Gespräche mit dem Computer eines Freundes bash -i >& /dev/tcp/IP/PORT 0>&1 verwenden.

  • Es ist nicht so, dass Bash HTTP „spricht“, sondern dass es TCP-Sockets öffnen kann
    Was hier passiert, ist, dass man HTTP selbst direkt spricht; für Tests oder Debugging ist das in Ordnung, und es macht Spaß, es einmal von Hand auszuprobieren, aber in einer echten unbeaufsichtigten Umgebung schießt man sich mit so einem Fake-HTTP-Client schnell ins Knie
    Dieser Spielzeug-Code kann kaputtgehen, weil er HTTP nicht sauber parsen kann
    Natürlich kann man in Bash auch einen vollständigen HTTP/1.1-Client schreiben, und sogar einen HTTP-Server in reinem Bash bauen: https://github.com/bahamas10/bash-web-server
    Die weniger verrückte Option ist normalerweise nc, und meist ist das auch die klügere Wahl

    • Die Formulierung „vollständiger HTTP-Server in reinem Bash“ ist streng genommen falsch
      Bash kann nicht auf TCP/UDP-Sockets lauschen, um eingehende Verbindungen anzunehmen
      Das Projekt bash-web-server baut einen Socket-Listener in C und lädt ihn zur Laufzeit als „eingebautes“ Modul dynamisch, um diese Funktion bereitzustellen
      [0] https://github.com/bahamas10/bash-web-server/tree/main/loada...
    • Der Hinweis ist richtig, und die Formulierung im Beitrag war überzogen, daher werde ich sie präziser aktualisieren
      nc oder ähnliche Werkzeuge aus der netcat-Familie wären die bessere Wahl, aber in dem damals verwendeten Image gab es solche Tools nicht
    • So verrückt ist das nun auch wieder nicht
      Ich tippe HTTP-Requests schon seit vor HTTP/1.1 und dem obligatorischen Host-Header von Hand ein
      Für ernsthafte Zwecke wäre das verrückt, und einen Webserver in Bash zu implementieren genauso, aber für schnelle Tests ist es ziemlich gut geeignet
    • Jemand hat sogar einen Minecraft-Server in reinem Bash gebaut
      https://sdomi.pl/weblog/15-witchcraft-minecraft-server-in-ba...
    • Es gibt auch ein Rails-ähnliches Framework für Bash: https://github.com/jneen/balls
  • Ich habe das gelernt, als ich gesehen habe, wie das Team von Bauhinia es beim Lösen einer CTF-Aufgabe verwendet hat
    Es war eine mehrstufige CTF, bei der man zunächst mit einer ROP-Kette eine system-Shell bekommt, aber praktisch in einer gefängnisartigen Umgebung sitzt, in der sich außer Bash fast nichts ausführen lässt
    Verfügbar waren nur Dinge wie read und cat, also wurde cat /dev/tcp verwendet, anschließend auf ein virtuelles Terminal umgeleitet, und dessen Inhalt wurde gelesen, um eine interne System-URL zu erhalten und die Flag zu finden

  • Ich bin auf diese Methode gestoßen, als ich in einem internen Docker-Netzwerk die Verbindung zwischen Containern geprüft habe und im Image weder curl noch wget vorhanden waren
    Überraschend war, dass Bash /dev/tcp hat und man mit ein wenig Shell-Magie etwas bauen kann, das wie ein HTTP-Request aussieht
    Zum Beispiel mit exec 3<>/dev/tcp/service/8642 öffnen, dann printf 'GET /health HTTP/1.1\r\nHost: service\r\nConnection: close\r\n\r\n' >&3 senden und anschließend cat <&3 ausführen
    Dabei ist service der Hostname des Zielsystems, und 8642 der Port, auf dem man per HTTP sprechen will

    • Das ist zwar cool, aber ich frage mich, ob es irgendeinen Nachteil hat, nicht einfach ein Image mit curl-Unterstützung zu verwenden
      Mir fällt keiner ein, und ich würde es selbst in Produktions-Images fast als Pflicht ansehen
  • In älteren Debian-Versionen und davon abgeleiteten Distributionen funktionierte das früher nicht, weil TCP-Zugriff über virtuelle Dateien standardmäßig deaktiviert war
    Soweit ich weiß, änderte sich diese Haltung 2009 und die Funktion wurde aktiviert; Diskussionen und Links dazu gibt es in Bug #146464
    <https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=146464#37>
    Es gibt auch viele andere Möglichkeiten, direkt aus Shell-Tools heraus auf Netzwerkfunktionen zuzugreifen, darunter curl, wget, Perls Befehle HEAD und GET, netcat/nc, socat, telnet und weitere

  • Ich erinnere mich noch daran, wie ich als Teenager anderen über echo unheimliche Nachrichten an ihr /dev/ptty geschickt und sie damit erschreckt habe
    Die von mir gesendeten Nachrichten tauchten wie durch Zauberhand in ihrem offenen Terminal auf
    Ich weiß bis heute nicht, warum man im Computerraum pro Client unterschiedliche Accounts benutzt hat und sie nicht gesperrt hat; vielleicht war das einfach eine Beschränkung von VAX damals