sábado, 29 de septiembre de 2012

cafein


Este es un manual de como preparar la charla de Test Driven Secure Development que he presentado en in.cafelug.org.ar el 2012/09/29, que es una refactorización profunda de lo presentado en agiles2011, en Agile Open Seguridad 2010 y en una o dos techtalks en Teracode en 2010 o 2011.
La versión original usaba unas funciones de testing que había elaborado para la ocasión y para cambiar el estado de la demo usaba unos ingeniosos scripts, ahora reemplazado por branches de git.

¿Por qué no subo el código que he generado? Por  estricta adherencia al proverbio chino:

Regala un pescado a una persona hambrienta y le darás alimento para un día, enséñale a pescar y le salvarás la vida.

Cualquier duda, me pregunta, yo contesto. Quizas, quizas, muestre esto en Agile Open Seguridad del 24 de noviembre http://www.agiles.org/agile-open-tour/agile-open-buenos-aires-2012---seguridad

Requerimientos de software


Instalar mysql, php, pdo y apache o equivalentes y sus dependencias.

En el navegador data tamper para mostrar bien la manipulación de los post.
  
wireshark para mostrar el tráfico entre el servidor web y la base de datos.
   
wget o curl para efectuar llamadas al servidor web desde bash

grep para evaluar las respuestas

shunit2 para evaluar los resultados esperados contra los obtenidos

git para poder mostrar paso a paso todo el proceso sin escribir ni una linea en el momento de la charla.

La idea

La idea es ir procesando requerimientos o bugs. En caso de ser un bug, está bueno mostrarlo, para eso tenemos wireshark, wget y el navegador. De un modo u otro hacer un test que falle, implementar la solución hasta que el test pase, quizas ajustar el test, quizas agregar otro test. Y seguir asi...

La idea de usar versionamiento la saqué de un curso de Ruby dado por Nicolás Gaivironsky. Give credit where credit is due.

La elección de bash y php pelado como [ausencia de] framework es debido a que al usar frameworks hay mucha magia que mete ruido y no se puede ver bien que ocurre si no se conoce el framework.

Ciertos requerimientos o bugs pueden romper test existentes, hay que arremangarse y arreglarlos o tirarlos.

Aunque la doctrina dice "primero test, luego implementación", la realidad dicta que "primero algo parecido a un test, un poco de implementación, ajuste del test...", como que hay un micro loop.


Una práctica que me resultó muy efectiva es esta secuencia:


git branch 01_xxx
git checkout 01_xxx
crear test
codear
ajustar test y código hasta que funcionen bien
git add test.sh
git commit 
git branch 02_xxx
git checkout 02_xxx

git add codigo
git commit



De este modo, cada branch se comitea cuando ya está el código estable.

Git lola dice:

* a70524e (HEAD, 28_code_session_fixation) code session fixation
* d4542a1 code session fixation rompe tests
* 2b1365a test session fixation
| * 90458ef (27_test_session_fixation) test session fixation
|/ 
* 0d47ab6 (26_session_complete) Refactorizacion completa
* 83bb6c6 (25_test_refactor) Refactorizacion completa con error de xss
* bf042c9 refactorizacion de tests tras incorporacion de session
* 34ae950 (24_code_login_redirect) code login redirect
* 255077f (23_test_login_redirect) test login redirect
* 9b0501f (22_code_login_post) code login post
* fb036a3 (21_test_login_post) test login post
* 9cbe347 (20_code_login) code auth get
* 6b93405 (19_test_login) test auth get
* e4bd6c6 (18_code_session) codigo session
* 531c868 (17_test_session) test basico session
* 33289da (16_code_xss_reflected) code xss reflected
* d568fbe (15_test_xss_reflected) test xss reflected
* 8756029 (14_code_xss_stored) code no xss stored
* e7d961d (13_test_xss_stored) test xss stored
* 5352ff4 (12_code_no_sql_injection) code no sql injection
* 1a29453 (11_test_sql_injection) test sql injection
* 6ec4d4a (10_code_refactor_no_sniffing) code refactor no sniffing
* 913a49d (09_code_persistencia) codigo persistencia
* da5f08f test persistencia
| * 1750417 (08_test_persistencia) test persistencia
|/ 
* 8a1a3fd (07_code_overwrite) codigo auth overwrite
* f0ef483 (06_test_overwrite) test auth overwrite
* d01e7c1 (05_test_refactor) test auth refactor
* 470fecd (04_code_auth) codigo auth
* a7fee5d test auth
| * 65c181b (03_test_auth) test auth
|/ 
| * 765808d (02_code_post) rollback
| * 10ed237 test para auth
|/ 
* 2e63946 codigo post
* ec198a6 (01_test_post) test post
* 9766ef2 (master) inicio


Espero ansioso la ocasión para que alguien me enseñe como transformarlo para que quede en una sola linea, como la que tira git branch

  01_test_post
  02_code_post
  03_test_auth
  04_code_auth
  05_test_refactor
  06_test_overwrite
  07_code_overwrite
  08_test_persistencia
  09_code_persistencia
  10_code_refactor_no_sniffing
  11_test_sql_injection
  12_code_no_sql_injection
  13_test_xss_stored
  14_code_xss_stored
  15_test_xss_reflected
  16_code_xss_reflected
  17_test_session
  18_code_session
  19_test_login
  20_code_login
  21_test_login_post
  22_code_login_post
  23_test_login_redirect
  24_code_login_redirect
  25_test_refactor
  26_session_complete
  27_test_session_fixation
  28_code_session_fixation


Todo esto se reduce a la siguiente lista de requerimientos o bugs:

Requerimiento: Crear una página que permita postear comentarios

  Fácil, un formulario

Requerimiento: Agregar autenticación

  Fácil, agregar user/pass con contraseñas hardcodeadas

Bug: Se pueden pisar las variables y evitar la autenticación

La implementación era:

foreach($_POST as $key=>$value) {
  $user[$key]=$value;
}

if ($user['user']=='ana' && $user['pass'] == 'ana123') {
   $user['valid']=true;
}


El ataque es --post-data="text=post sin permiso&valid=1"

La solución es... sólo tomar los parámetros que estamos esperando.

Requerimiento: Persistir los comentarios en una base de datos

Fácil, consultar en la base en lugar de hardcoded

Bug: Se ha detectado un error de diseño que permite ver las credenciales de todos los usuarios circulando entre el servidor web y la base de datos.

El código era

$result = mysql_query('select * from user') or die('no query');

while ($entry = mysql_fetch_assoc($result)) {
  if ($entry['name'] == $user['user']
      && $entry['pass'] == $user['pass']
  ) {
    $user['valid'] = true;
    break;
  }
}


esta es la captura de wireshark entre el web server y la base de datos, ademas no escala si tenés un millón de usuarios.

Lo que me gusta de este error es que lo he visto en la vida real una vez.

Bug: Se ha detectado sql injection que permite evitar la autenticación


Reemplazamos sql pelado por Prepared Statements

Bug: Se ha detectado xss

Al menos hay que poner htmlentities() al mostrar los posts.

Requerimiento: Agregar session y una página independiente para login


Esto fue un pijazo, no hay otra palabra. Tuve que tirar todos los tests e ir recuperandolos de a poquito, va desde 17_test_session hasta   26_session_complete


Bug: Se ha detectado session fixation, que permite a un atacante "colgarse" de una sesión ajena.


Hay que agregar sesión

        if (loginOk()) {
            session_regenerate_id();
            $_SESSION['valid'] = true;
             header('Location: chat.php');
            exit();
        }

Ejemplo de un test


Para testear he usado sshunit2, wget y grep, veamos un ejemplo detallado, de los últimos:
testXSSstored(){el nombre "test" le permite a sshunit2 hallarlo
    setupDBversion1ponemos la base en un estado conocido
    rm cookies.txt -fpor las dudas tiramos las cookies que puedan haber
    wget -q -O - --post-data="user=ana&pass=ana123" http://127.0.0.1/login.php --keep-session-cookies \ --save-cookies cookies.txt > /dev/nullnos autenticamos salvando las cookies estoy usando header("Location: chat.php") en login. con max-redirect=0 se impide la redirección, tiramos el output a stdout con -O - y de ahi a /dev/null, le pedimos con -q que no diga nada de como le fué
    wget -q -O - --post-data="text=<script>alert(12*12)</script>" http://127.0.0.1/chat.php --load-cookies cookies.txt > /dev/nullle decimos que tome las cookies que habiamos recibido antes y hacemos el ataque
    rm cookies.txt -f
    wget -q -O - --post-data="user=ana&pass=ana123" http://127.0.0.1/login.php \ --keep-session-cookies | grep -q -F "<script>alert(12*12)</script>"volvemos a autenticarnos, aceptamos la redirección y vemos si el script está intacto
    RESULT=$? guardamos el resultado de la operación anterior
    assertEquals 1 $RESULT
}
que debe ser 1 si no hubo match, que es lo esperado en este caso y 0 si hubo, será uno u otro dependiendo de lo que estemos testeando

La implementación esta es para dejar la base de datos en un estado conocido antes de cada test que necesite datos

setupDBversion1() {
  echo "drop database chat;
  create database chat;
  use chat;
  create table messages(message varchar(64));
  create table user (name varchar(16), pass varchar(32));
  insert into user values (\"ana\",\"ana123\");
  insert into user values (\"root\",\"root_pass\");
  insert into user values (\"master\",\"master_pass\");" \
  | mysql -u admin chat
}

. shunit2 ;# esto invoca la ejecución de todos los tests


Este es un ejemplo de ejecución donde eliminé el htmlentities() y entró el xss. Es importante romper el código una vez que el test pasa para estar seguros que el test lo detecta.




Detalles de configuración


apache

Crear un htdocs accesible tanto por apache como por el usuario de ejecucion modificar configuración apache para que apunte a la carpeta de trabajo crear repositorio de git (git init)



mysql


create user 'admin'@'localhost';

grant all on chat.* to 'admin'@'localhost';

Es una decisión controvertida quedarse sin password, pero la opción que mejor conozco es usar expect, que no aporta a la demo.

No hay comentarios:

Publicar un comentario