Protejer JSON REST API

Como proteger o teu site WordPress contra ataques via REST API

Um dos maiores ataques recentes contra sites desenvolvidos em WordPress teve origem numa vulnerabilidade existente na REST API. O ‘bug’ chegou ao WordPress pela introdução no core dos endpoints da REST API, na versão 4.7 e continuou pela 4.7.1. A falha de segurança permitia a um atacante alterar o conteúdo de qualquer artigo.

O pior de tudo, acentuando a gravidade do problema, é que o ataque em si é tão simples como fazer download de um script, apontar a um domínio e, se a versão do WordPress coincidir, usar a REST API para trocar o conteúdo de qualquer artigo.

Acreditem. Eu tentei e fui bem sucedido.

Como resolver o problema e ficar mais seguro

  • Actualizar o WordPress!
  • Bloquear acesso externo à REST API
  • Garantir actualizações automáticas

No final do artigo, explicarei o que é a REST API e como é que funcionam estes ataques. Até lá, vou mostrar como podem proteger a REST API.

Bloquear acesso externo à REST API

Há duas maneiras de o fazer: via servidor ou por plugins. A melhor solução é a nível do servidor. Mas se não tem acesso de administração do servidor, usem os plugins.

Servidor

Aconselho a fazer a nível do servidor, por duas razões muito simples. Menos recursos usados e mais cedo. Tipicamente, se a segurança for feita por um plugin, o pedido passar pelo servidor, PHP, WordPress e finalmente analisado pelo plugin.

Por isso, vamos cortar o pedido quanto antes. Se ainda não tem fail2ban, existe este tutorial para VPS pela Linode. Os requisitos são duas ferramentas essenciais em qualquer servidor:

Proteger

Para proteger, vou usar uma regra simples no nginx:

location ~ /wp-json/wp/v2/ {
  allow <ip>;
  deny all;
  try_files $uri $uri/ /index.php?$args;
}

com “allow www.xxx.yyy.zzz;” podemos, por exemplo, ligar a um proxy, e esse proxy comunicar com o nosso site.

se não usam nenhum plugin que usa wp-json, podem mudar a location para location ~ /wp-json. Alguns plugins, como o jetpack, já usam wp-json.

 

Por omissão, o servidor responde com 403, mas vamos responder 404 para confundir mais um pouco. Existe uma vantagem neste pensamento, chamada Obscurity as a Layer. Há uma apresentação fantástica na DEFCON 2013  sobre como se pode enganar os bots retornando códigos aleatórios ou simplesmente errados.

Para retornar 404 Not found, em vez de 403 Forbidden, esta linha deve ser também incluída no bloco server{}.

error_page 403 =404 /404.html;

Devemos fazer o mesmo para xmlrpc.php, com “location ~ xmlrpc\.php

Bloquear

Para bloquear o pedido temos que ter algum tipo de sinal. Vamos usar fail2ban e iptables para esta tarefa. A ideia é analisar os logs do vosso nginx, procurar o IP, comparar com os últimos dois dias e, se encontrar pelo menos dois pedidos, banir por um ano.

> /etc/fail2ban/jail.local

[wp-json]
enabled =true
port = http,https
filter = wp-json
action = iptables-multiport[name=WordPressJSON, port="http,https"]
          wp-json[name=WordPressJSON]
logpath = /var/log/nginx/*access.log /var/log/nginx/*access.log.1
maxretry = 2
findtime = 172800
bantime = 31539000

Nota: iptables-multiport já vem incluído.

 

Esta acção é responsável por criar a lista (chain) “f2b-WordPressJSON”, adicionar um IP à lista de IPs banidos.

De seguida vamos criar uma nova acção para gravar os IP e escrever nos logs. Para manter a lista sem duplicados, quando esta acção é iniciada, limpamos e ordenamos os IP.

> /etc/fail2ban/action.d/wp-json.conf

[Definition]
actionstart = touch /var/log/fail2ban/wordpress-json.log
printf %%b "<init>\n" >> /var/log/fail2ban/wordpress-json.log
cat /etc/fail2ban/persistent.bans | awk '/^fail2ban-<name>/ {print $2}' | while read IP; do iptables -I fail2ban-<name> 1 -s $IP -j <blocktype>; done
actionstop = echo "" > /var/log/fail2ban/wordpress-json.log
actioncheck =
actionban = printf %%b "+<ip> : <failures>\n" >> /var/log/fail2ban/wordpress-json.log
actionunban = printf %%b "-<ip> : <failures>\n" >> /var/log/fail2ban/wordpress-json.log

[Init]
init =

Este filtro procura nos logs por linhas com o IP e “wp-json”. Podemos detalhar mais a failregex, caso haja muitas falsos-positivos. A failregex depende da estrutura dos logs.

> /etc/fail2ban/filter.d/wp-json.conf

[INCLUDES]
before = common.conf
[Definition]
_daemon = wordpress
failregex = ^<HOST>.*wp-json.*$
ignoreregex =

Nota: common.conf já vem com fail2ban

Plugins

Se não poderes usar o servidor ou preferires usar plugins também tens opções. Vamos a elas.

Bloquear REST API

Disable json API

Este plugin bloqueia pedidos à REST API e é extra-light.

Cerber Security

Este plugin não foi analisado mas promete bloquear REST API ( e xmlrpc ).

Wordfence

Não analisado, mas promete bloquear apenas este endpoint, e não toda a REST API.

Sem plugins, código retirado de stackoverflow.com

O uso da REST API é legitimo, se não tiver buracos de segurança.  Para bloquear wp-json/wp/v2/users/users/, o seguinte código foi testado:

add_filter('rest_endpoints', function($endpoints) {
    if ( isset( $endpoints['/wp/v2/users'] ) ) {
        unset( $endpoints['/wp/v2/users'] );
    }
});

Desabilitar totalmente a REST API.

Podemos pensar que não faz falta, mas a realidade é que o WordPress já depende dela e cada vez mais vai substituir outras API já existentes. Por isso, não recomendo que a desabilitem. 

Para garantir actualizações automáticas

Apesar de os actualizações automáticas, que foram introduzidas na versão 3.7, estarem activas por omissão, pode acontecer que um site não se actualize. As actualizações são automáticas se forem entre versões menores, ou seja, 4.6 para 4.6.1, ou 4.6.1 para 4.6.2.

Se sair uma nova versão base, 4.7 por exemplo, é necessária a actualização manual.

Para actualizar as traduções

add_filter( 'auto_update_translation', '__return_true' );

Para actualizar os plugins

add_filter( 'auto_update_plugin', '__return_true' );

Para actualizar os temas

add_filter( 'auto_update_theme', '__return_true' );

Para actualizar o core, incluído as versões maiores e menores:

define('WP_AUTO_UPDATE_CORE', true);

De qualquer modo, é importante confirmar sempre que o site está actualizado.

REST API

O que é a REST API? Na verdade o título completo é JSON REST API. Vamos por partes:

JSON

Os sites mostram a informação em HTML no browser, mas na REST API, a informação é agrupada em JSON.

JSON é JavaScript Object Notation. É portanto apenas um formato diferente, mas mais organizado virado para máquinas.

 

REST

Significa Representational State Transfer, e implica uma estrutura organizacional dos dados e permite acções básicas CRUD: Create, Read, Update e Delete.

Por exemplo, podemos ler a lista de todos os utilizadores com GET /wp-json/wp/v2/users.

Ou criar um novo post com POST /wp-json/wp/v2/posts.

Ou simplesmente ter uma visão completa de toda os caminhos ( routes ) e acções ( GET, POST, DELETE, etc ) com o seguinte endpoint: GET /wp-json/wp/v2/

Da mesma maneira que organizamos os artigos por categorias e subpáginas, a informação da REST API é bem estruturada. Já sabemos onde podemos ver os utilizadores, por isso, para encontrar informação sobre o utilizador com id 33, basta adicionar 33 ao endpoint:  GET /wp-json/wp/v2/users/33.

 

API

Significa Application Programming Interface. Representa a ponte entre a estrutura REST e o código PHP do WordPress. Cada acção tem uma função equivalente no código do WordPress.

 

Tudo junto, significa que existe um mapeamento estruturado do backoffice em algumas funcionalidades importantes. Para aprofundar estas noções, consultar a documentação.

Ataque

O ataque em si é incrivelmente simples. Com base do que mostrei acima, podemos entender o que acontece:

Se fizermos uma acção GET no caminho /wp-json/wp/v2/posts/123, a API devolve-nos o conteúdo do artigo 123.

Se fizermos POST /wp-json/wp/v2/posts/123, API diz-nos que não temos permissão para alterar o artigo 123 e com razão.

Mas e se fizermos POST /wp-json/wp/v2/posts/123aaaa, fará diferença?

Obviamente o WordPress não vai encontrar um artigo com o ID 123aaaa, porque todos os IDs são apenas numéricos. Mas internamente, o WordPress vai tentar converter 123aaaa para número 123 e tentar comparar se o ID existe e se tem permissões.

Para perceber onde está a falha temos que perceber que para o WordPress, ambas as seguintes condições tem que ocorrer:

1- ID 123 existe? sim;
2- tem permissões para alterar o artigo 123? não

e, portanto, pode alterar? não.

Mas se o id não existe, não é necessário verificar se o utilizador tem permissões para alterar um artigo que não existe. Neste caso, o WordPress não valida a segunda condição e não dá erro.

Ou seja, para o WordPress, pode continuar a executar o resto do código porque não houve erro!

função update_item_permissions_check()

Isso é suficiente para dar permissão ao pedido e deixar alterar o conteúdo do artigo. Na verdade, é um pouco mais complexo, como podem ver na análise feita pela Sucuri.

É por isso que até agora os ataques se tem deixado ficar por apenas alterar o conteúdo dos artigos, sem ainda conseguirem aceder a outras partes do site. Mas com a combinação de alguns plugins é possível!

 

Mantém-te seguro.

Deixar uma resposta