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 formularioRequerimiento: Agregar autenticación
Fácil, agregar user/pass con contraseñas hardcodeadasBug: 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 hardcodedBug: 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 |
setupDBversion1 | ponemos la base en un estado conocido |
rm cookies.txt -f | por 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/null | nos 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/null | le 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.