Application Express: Betriebssystem-Kommandos ausführen

Ab und zu ist wäre hilfreich, wenn man aus einer Application Express-Anwendung Betriebssystem-Kommandos auf dem Server aufrufen könnte. Wie das geht, erfahren Sie in diesem Tipp. Bitte beachten Sie: Es geht um Kommandos auf dem Server, nicht auf dem Client-PC, mit dem der Anwender arbeitet.

Wenn Betriebssystem-Kommandos aus Application Express heraus aufgerufen werden sollen, bedeutet dies, dass die aus der Datenbank, aus PL/SQL heraus aufgerufen werden müssen. Dazu stehen grundsätzlich drei Varianten zur Verfügung.

  • Sie können eine DLL (Windows) bzw. ein Shared Object (Unix/Linux) erstellen, welche das Kommando ausführt und diese als PL/SQL-Prozedur verfügbar machen (CREATE LIBRARY). Diese Variante ist recht schlank, zur Umsetzung benötigen Sie jedoch in aller Regel einen C-Compiler und C-Kenntnisse.
  • Ab Oracle10g ist es möglich, mit dem PL/SQL-Paket DBMS_SCHEDULER und dem Job-Typ EXECUTABLE Betriebssystem-Kommandos auszuführen. Allerdings laufen diese Jobs asynchron und Sie müssen sich selbst darum kümmern, die Konsolen-Ausgabe in eine Datei umzuleiten und diese zu lesen. Dem Paket DBMS_SCHEDULER wurde übrigens bereits ein eigener Community-Tipp gewidmet.
  • Mit Hilfe der Java-Engine, die ohnehin in der Datenbank enthalten ist, lässt sich die Aufgabe ebenfalls lösen. Das Kommando wird dann synchron ausgeführt (was bei einfachen Aufrufen ja auch sinnvoll ist) und Sie können sich die Konsolen-Ausgabe bequem zurückgeben lassen. Diese Variante wird im folgenden detailliert betrachtet.

Bitte beachten Sie, dass dieser Tipp nicht in Oracle XE läuft. Oracle XE enthält die hier genutzte Java-Engine nicht.

1. Schritt: Java Stored Procedure und "PL/SQL-Wrapper" erzeugen

Die eigentliche Arbeit wird durch eine Java-Klasse erledigt. Der Java-Code ist ebenfalls dafür verantwortlich, die Konsolen-Ausgabe auszulesen und an den Aufrufer zurückzugeben. Anschließend wird ein PL/SQL-Wrapper erzeugt, so dass der Java-Code wie eine PL/SQL-Prozedur oder -Funktion aufgerufen werden kann.

Übrigens: Auf diesem Wege können Sie auch anderen Java-Code in Application Express ausführen und nutzen. Die Anbindung an Application Express erfolgt mit Hilfe des PL/SQL-Wrappers - ein Aufrufer bekommt von der Tatsache, dass hier Java im Spiel ist, nichts mehr mit.

create or replace java source named "ExternalCall" as
import java.io.*;
import oracle.jdbc.*;
import oracle.sql.*;
import java.sql.*;

public class ExternalCall {
  /*
   * Die statische Methode "call_short" nimmt das Kommando als Parameter
   * entgegen, führt es aus und gibt die ersten 32 Kb der Ausgabe
   * an den Aufrufer zurück. 
   */
  public static String call_short(String sOsCall) throws Exception {
    Process p = null;
    InputStreamReader isr = null;

    char[] caBuffer = new char[32000];
    int iCharsRead = 0;

    p = Runtime.getRuntime().exec(sOsCall);
    p.waitFor();
    if (p.exitValue() == 0) {
      isr = new InputStreamReader(p.getInputStream());
    } else {
      isr = new InputStreamReader(p.getErrorStream());
    }

    iCharsRead = isr.read(caBuffer, 0, 32000);
    if (iCharsRead != -1) {
      return new String(caBuffer, 0, iCharsRead);
    } else {
      return "";
    }
  }

  /*
   * Die statische Methode "call_long" nimmt das Kommando als Parameter
   * entgegen, führt es aus und gibt die Ausgabe komplett an den Aufrufer 
   * zurück. Daher ist der Rückgabetyp hier "CLOB"
   */
  public static CLOB call_long(String sOsCall) throws Exception {
    Process p = null;
    InputStreamReader isr = null;
    Writer clobWriter = null;

    Connection con = DriverManager.getConnection("jdbc:default:connection:");
    CLOB tempClob = CLOB.createTemporary(con, true, CLOB.DURATION_CALL);

    p = Runtime.getRuntime().exec(sOsCall);
    p.waitFor();
    if (p.exitValue() == 0) {
      isr = new InputStreamReader(p.getInputStream());
    } else {
      isr = new InputStreamReader(p.getErrorStream());
    }
    clobWriter = tempClob.getCharacterOutputStream();

    char[] caBuffer = new char[tempClob.getChunkSize()];
    int iCharsRead = 0;

    while ((iCharsRead = isr.read(caBuffer, 0, caBuffer.length)) != -1) {
      clobWriter.write(caBuffer, 0, iCharsRead);
    }
    clobWriter.close();
    return tempClob;
  }
}
/

alter java source "ExternalCall" compile
/

create or replace package external_call is
  function call_short(p_command in varchar2) return varchar2;
  function call_long(p_command in varchar2) return clob;
end external_call;
/
create or replace package body external_call is
  function call_short (p_command in varchar2) return varchar2
  is language java name 'ExternalCall.call_short(java.lang.String) return java.lang.String';

  function call_long (p_command in varchar2) return clob
  is language java name 'ExternalCall.call_long(java.lang.String) return oracle.sql.CLOB';
end external_call;
/

Navigieren Sie nun also zum SQL Workshop, dort zu den Skripts und laden Sie den obigen Code als neues SQL-Skript hoch oder übertragen Sie ihn mit Copy & Paste in den Skript-Editor.

SQL-Skript im SQL Workshop

Abbildung 1: Skript im SQL Workshop

Starten Sie das Skript anschließend - Sie sollten die in Abbildung 2 dargestellten Meldungen erhalten.

Ergebnisse des SQL-Skripts

Abbildung 2: Die Ergebnisse des SQL-Skripts

2. Schritt: Rechtevergabe

Nun ist das PL/SQL-Paket erzeugt und kann grundsätzlich ausgeführt werden. Da das Ausführen von Betriebssystem-Kommandos jedoch ein sicherheitsrelevantes Thema ist, fehlen Ihnen zunächst noch die nötigen Privilegien.

Lassen Sie bei der Nutzung dieser Möglichkeit bitte größte Vorsicht walten. Die Betriebssystem-Kommandos werden mit den gleichen Rechten ausgeführt, welche die Oracle-Prozesse haben.

Mit besonderer Sorgfalt müssen Sie vorgehen, wenn Benutzereingaben in die Betriebssystem-Kommandos integriert werden - hier sind dem SQL Injection-Prinzip ähnliche Angriffe (jedoch mit weit schwerwiegenderen Konsequenzen möglich). Am besten gehen Sie hier so restriktiv wie möglich vor: Geben Sie dem Endanwender nur die Auswahl zwischen vorgefertigten Kommandos und binden Sie Benutzereingaben nur unter größter Sorgfalt ein - Hilfreich dürfte auch der Community-Tipp zum Thema "SQL Injection" sein.

Vor dem Hintergrund dieser Sicherheitsrisiken ist es verständlich, dass Sie sich vom DBA zusätzliche Privilegien geben lassen müssen. Das nun folgende Skript (welches der DBA als SYS oder SYSTEM) laufen lassen muss, gibt dem Application Express Parsing Schema das Recht, jedes Betriebssystem-Kommando auszuführen - dies kann und sollte natürlich eingeschränkt werden, indem anstelle des <<ALL FILES>> nur bestimmte Aufrufe zugelassen werden.

Tragen Sie, bevor das Skript gestartet wird, in Zeile 2 anstelle des [APEX Parsing Schema] noch das tatsächliche Parsing Schema Ihrer Anwendung ein - Sie finden es in den Applikationsattributen oder im SQL Workshop.

declare
  v_grantee constant varchar2(30) := '[APEX Parsing Schema]';
begin
  dbms_java.grant_permission(
    grantee =>           v_grantee,
    permission_type =>   'SYS:java.io.FilePermission',
    permission_name =>   '<<ALL FILES>>',
    permission_action => 'execute'
  );
/*
 * Der folgende Call erlaubt nur das Absetzen des "ls"-Kommandos ...
 *

  dbms_java.grant_permission( 
    grantee =>           v_grantee, 
    permission_type =>   'SYS:java.io.FilePermission', 
    permission_name =>   '/bin/ls',
    permission_action => 'execute' 
  );
*/
  dbms_java.grant_permission(
    grantee =>           v_grantee,
    permission_type =>   'SYS:java.lang.RuntimePermission',
    permission_name =>   'readFileDescriptor',
    permission_action => null
  );
  dbms_java.grant_permission(
    grantee =>           v_grantee,
    permission_type =>   'SYS:java.lang.RuntimePermission',
    permission_name =>   'writeFileDescriptor',
    permission_action => null
  );
end;
/

3. Schritt: Das neue PL/SQL-Paket nutzen ...

Nun ist alles fertig - Sie können das PL/SQL-Paket nun testen. Erzeugen Sie eine Anwendung, eine Seite und erstellen Sie auf dieser Seite einen Bericht mit folgendem SQL-Kommando:

select EXTERNAL_CALL.CALL_SHORT('/bin/ls -la /') as DIR_LIST from dual

... oder, wenn Application Express auf einem Windows-Server läuft ...

select EXTERNAL_CALL.CALL_SHORT('C:\windows\system32\cmd.exe /C dir \') as DIR_LIST from dual

... oder, wenn Application Express auf einem Windows-Server läuft ...

Das erste Ergebnis sieht dann wie in Abbildung 3 aus.

Directory-Listing in Application Express: Zwischenergebnis

Abbildung 3: Directory-Listing in Application Express: Zwischenergebnis

Nun werden wir dies noch ein wenig formatieren. Navigieren Sie zu den Berichtsattributen und dort zur Spalte DIRLIST. Tragen Sie dann einen

Das erste Ergebnis sieht dann wie in Abbildung 3 aus.

Directory-Listing in Application Express: Zwischenergebnis

Abbildung 3: Directory-Listing in Application Express: Zwischenergebnis

Nun werden wir dies noch ein wenig formatieren. Navigieren Sie zu den Berichtsattributen und dort zur Spalte DIRLIST. Tragen Sie dann einen HTML-Ausdruck wie in Abbildung 4 dargestellt, ein.

Formatierung des Directory-Listings in den Berichtsattributen

Abbildung 4: Formatierung des Directory-Listings in den Berichtsattributen

Starten Sie die Seite anschließend neu - Das Ergebnis sollte wie folgt aussehen:

Directory-Listing in Application Express

Abbildung 5: Directory-Listing in Application Express

Auf diese Weise können Sie externe Applikationen auf dem Application Express-Server nun bequem einbinden. Zum Schluß allerdings nochmals die Sicherheitswarnung:

Seien Sie extrem vorsichtig beim Einbinden von Benutzereingaben in solche Aufrufe - Schafft es ein böswilliger Angreifer, "seine" Kommandos hier auszuführen, kann er den Datenbankserver komplett unter seine Kontrolle bringen. Sehen Sie am besten jede Benutzereingabe als potenziellen Angriff an, prüfen Sie sie mit entsprechender Vorsicht und Sorgfalt und geben Sie nur die Kommandos frei, die Sie tatsächlich benötigen!

Zurück zur Community-Seite