前言

  在上一篇文章中,我介紹了 Json Web Token 的相關概念。在這篇文章中,我主要介紹JWT的相關攻擊,並引用了一些CTF題目。

  查看上一篇文章:

  

  深入瞭解Json Web Token之概念篇:http://www.freebuf.com/articles/web/180874.html。

  

  本來想用python DRF 的 JWT做,後來各種失敗。最終嘗試了用Php,發現非常便利。

  PHP 版本 PHP 7.2.4-1+b2 (cli),也就是kali linux自帶的php,至於composer的安裝方法,以及各個庫的使用方法在此不展開,需要的話可以自己查閱官方文檔

  php jwt庫的評測

  在jwt.io上有些php jwt的庫,在此說一下使用下來的感覺。只取了評分前三的庫:

  firebase/php-jwt Star 3786

  

  支持PHP5/7;

  操作非常簡單,但是不具備很多功能,不是很推薦。

  

  lcobucci/jwt Star Star 2729

  

  支持PHP5/7;

  不具備JWE的方法,操作簡單;

  不具備多重JWS,JWE方法以及其對應序列化方法。

  

  spomky-labs/jose Star 351

  

  僅支持 PHP 7;

  功能齊全,具有多重JWE,JWS,以及其對應序列化方法。以上兩個都不具備。

  

  JWT 的攻擊手段包括以下內容:

  參考網站:https://auth0.com/blog/critical-vulnerabilities-in-json-web-token-libraries//。

  1. 敏感信息泄露

  當服務端的祕鑰泄密的時候,JWT的僞造就變得非常簡單容易。對此,服務端應該妥善保管好私鑰,以免被他人竊取。

  2. 將加密方式改爲’none’

  下文實戰中的 Juice Shop JWT issue 1 便是這個問題。之前談及過nonsecure JWT的問題。

  簽名算法確保惡意用戶在傳輸過程中不會修改JWT。但是標題中的alg字段可以更改爲none。一些JWT庫支持無算法,即沒有簽名算法。當alg爲none時,後端將不執行簽名驗證。將alg更改爲none後,從JWT中刪除簽名數據(僅標題+’.'+ payload +’.')並將其提交給服務器。

  解決對策:

  

  不允許出現 none 的方法;

  將開啓 alg : none 作爲一種額外的配置選項。

  

  HS256使用密鑰來簽名和驗證每個消息。而RS256使用私鑰對消息進行簽名並使用公鑰進行認證。

  如果將算法從RS256更改爲HS256,則後端代碼使用公鑰作爲密鑰,然後使用HS256算法驗證簽名。由於攻擊者有時可以獲取公鑰,因此攻擊者可以將標頭中的算法修改爲HS256,然後使用RSA公鑰對數據進行簽名。

  此時,後端代碼就會使用RSA公鑰+HS256算法進行簽名驗證,從而讓驗證通過。

  解決對策:

  

  不允許 HS256等對稱加密 算法讀取祕鑰。jwtpy就是限制了這種方法。當讀取到 類似於 “— xxx key —” 的參數的時候應拋出錯誤;

  將祕鑰與驗證算法相互匹配。

  

  如果HS256密鑰強度較弱,則可以直接強制使用,通過爆破 HS256的祕鑰可以完成該操作。難度比較低。解決對策很簡單,使用複雜的祕鑰即可。

  5. 錯誤的堆疊加密+簽名驗證假設

  錯誤的堆疊加密

  這種攻擊發生在單個的或者嵌套的JWE中,我們想象一個JWE如下所示:

  JWT RAW header : ... payload: "admin" : false "uid" : 123 "umail" : [email protected] ... JWE Main protected / unprotected recipients: en_key : key1 en_key : key2 cipher : xxx

  在攻擊者不修改祕鑰的情況下,對於ciphertext進行修改。往往會導致解密的失敗。但是,即使是失敗,很多JWT的解密也是會有輸出的,在沒有附加認證數據(ADD)的情況下更是如此。攻擊者對於ciphertext的內容進行修改,可能會讓其他的數據無法解密,但是隻要最後輸出的payload中,有“admin”:true。 其目的就已經達到了。

  解決對策:

  

  對於JWE而言,應當解密所有數據,而非從解密的結果中提取單個需要的數據。另外,利用附加認證數據ADD,也是非常好的選擇。

  

  簽名假設驗證

  這種攻擊發生嵌套的JWS中。我們想象一個嵌套的JWS,其包括了兩層的部分,其結構如下:

  JWT Main JWT Sub1 payload Signature2 Signature

  現在,攻擊者通過一定的方式,能夠讓外層的驗證通過的時候,此時,系統還應該檢查內層的簽名數據,如果不檢查,攻擊者就可以隨意篡改payload的數據,來達到越權的目的。

  解決對策:

  

  因此對於嵌套JWS而言,應當驗證所有層面的簽名是否正確,而非驗證最外層的簽名是否正確就足夠。

  

  橢圓曲線加密是一種非常安全的方式,甚至從某種程度上而言,比RSA更加安全。關於橢圓曲線的算法,在此不展開。

  在橢圓曲線加密中,公鑰是橢圓曲線上的一個點,而私鑰只是一個位於特殊但非常大的範圍內的數字。 如果未驗證對這些操作的輸入,那攻擊者就可以進行設計,從而恢復私鑰。

  而這種攻擊已在過去中得到證實。這類攻擊被稱爲無效曲線攻擊。這種攻擊比較複雜,也設計到很多的數學知識。詳細可以參考文檔:critical-vulnerability-uncovered-in-json-encryption。

  解決對策:

  

  檢查傳遞給任何公共函數的所有輸入是否有效是解決這類攻擊的關鍵點。驗證內容包括公鑰是所選曲線的有效橢圓曲線點,以及私鑰位於有效值範圍內。

  

  在這種攻擊中,攻擊者需要至少獲得兩種不同的JWT,然後攻擊者可以將令牌中的一個或者兩個用在其他的地方。

  在JWT中,替換共嘰有兩種方式,我們稱他們爲相同接收方攻擊(跨越式JWT)和不同接收方攻擊。

  不同接收方攻擊

  我們可以設想一個業務邏輯如下:

  

  Auth 機構,有着自己的私鑰,並且給 App1 和 App2 發放了兩個公鑰,用於驗證簽名;

  Attacker 利用自己的祕鑰登錄了 App1。

  

  此時 Auth 機構給 Attacker 下發了一個 附帶簽名的JWT,其payload內容爲:

  { 'uname':'Attacker' 'role' :'admin' }

  此時,如果 Attacker 知道 App1 和 App2 的公鑰是同一個Auth 簽發的話,他可以利用這個JWT去登錄 App2,從而獲取Admin權限。

  解決方法:

  

  在jwt中帶上 aud 聲明,比如 aud : App1 這樣。來限定該jwt只能用於App1。

  

  相同接收方攻擊/跨越式JWT same recipient/Cross JWT

  

  在同一站點下,有兩個應用程序,wordpress和phpmyadmin,他們都利用了相同的祕鑰對和算法來驗證JWT簽名;

  站點管理員知道 Different Recipient 的問題,所以給 wordpress 的應用增加了 aud 驗證,但是 phpmyadmin 的用戶人數較少,沒有增加 aud 的驗證;

  Attacker 利用自己的祕鑰登錄了 wordpress。

  

  此時 站點 給 Attacker 下發了一個 附帶簽名的JWT,其payload內容爲:

  { 'uname':'Attacker' 'role' :'writer' 'aud' :'shaobaobaoer.cn/wordpress' 'iss' :'shaobaobaoer.cn' }

  這個JWT看似非常安全,但這僅僅是對於 wordpress 的應用程序而言,。從而Attacker 可以以 writer 的身份登錄 phpmyadmin。

  解決方案:

  爲所有子應用程序增加 aud 的驗證

  8. 其他假想的攻擊方式

  JWT + SQL 注入

  參考鏈接:https://github.com/greunion/ctf-write-ups/tree/master/2018-nullcon/web/400-web6。

  當解密JWT的祕鑰很多的時候,往往需要通過kid來確定使用哪個祕鑰,而keyid參數通過b64加密來保存,可以被篡改。當keyid要通過數據庫API拿取得時候,往往就會聯想到sql 注入。

  我們可以想象一下的攻擊情況:

  $keyID = $token-> getKeyID(); $keyContent = sqlAPI -> fromKeyidGetKeyContent($keyID) ### class sqlAPI(): function fromKeyidGetKeyContent($keyID){ $result= Query("select key_content from keyTable where key_id = '$keyID'"); return $result['key_content'] } ### if($token-> verify($JWA,$keyContetn)){ echo $flag; }

  在下列比較簡易的代碼中,通過讓數據庫返回值爲我們自定義的key_content。就可以達到破解JWT的目的。

  通過注入:

  ' union select 'easy' limit 1,1--+

  即可讓祕鑰改成easy。

  0×03 實戰練習 實戰練習1 敏感信息泄露

  我搜到了一個demo,在線演示地址爲:http://demo.sjoerdlangkemper.nl/jwtdemo/hs256.php。

  GitHub地址爲:https://github.com/Sjord/jwtdemo/。

  使用firebase/jwt寫的php-jwt的demo。可以用來完成後面的題目。

  爲了達到修改jwt的目的,我會把data字段改爲:

  "data":{ "hacker":"shaobaobaoer" }

  該題目地址爲:http://demo.sjoerdlangkemper.nl/jwtdemo/rs256。

  在知道github項目的情況下,我們變相知道了公鑰私鑰的地址,直接訪問:

  http://demo.sjoerdlangkemper.nl/jwtdemo/private.pem http://demo.sjoerdlangkemper.nl/jwtdemo/public.pem

  可以拿到公鑰私鑰的內容,之後,利用我們自己的jwt庫進行加密即可。

  關鍵代碼如下所示:

  $keychain = new Keychain(); $sign = new Sha256(); $token = "eyJ0eXAiO..."; $token = (new Parser())->parse((string) $token); $hacktoken = (new Builder()) ->setIssuer($token->getClaim('iss')) ->setIssuedAt($token->getClaim('iat')) ->setExpiration($token->getClaim('exp')) ->set("data",["hack"=>"shaobaobaoer"]) ->sign($sign,$keychain->getPrivateKey('file://key_box/private.pem')) ->getToken(); echo $hacktoken.PHP_EOL; var_dump($hacktoken->verify($sign,$keychain->getPublicKey('file://key_box/public.pem')));

  可以看到,我們已經更改成功。

   實戰練習2 Juice Shop JWT issue 1

  juice shop 是一個OWASP 的 vulnerable WEB 項目,後端語言爲node.js。

  當初我做的時候連jwt是什麼都不知道,兩道jwt的題目就此跳過了,現在已經掌握了這些概念,就可以拿出來回味一下。

  關於juice shop 的大部分題解可以看OWASP juice shop 實戰報告。

  題目描述

  Forge an essentially unsigned JWT token that impersonates the (non-existing) user [email protected].

  實戰過程

  首先,先用萬能登錄,獲取到jwt 如下所示:

  eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdGF0dXMiOiJzdWNjZXNzIiwiZGF0YSI6eyJpZCI6MSwiZW1haWwiOiJhZG1pbkBqdWljZS1zaC5vcCIsInBhc3N3b3JkIjoiMDE5MjAyM2E3YmJkNzMyNTA1MTZmMDY5ZGYxOGI1MDAiLCJjcmVhdGVkQXQiOiIyMDE4LTA4LTEyIDA3OjUzOjM4LjA2NCArMDA6MDAiLCJ1cGRhdGVkQXQiOiIyMDE4LTA4LTEyIDA3OjUzOjM4LjA2NCArMDA6MDAifSwiaWF0IjoxNTM0MDYwNTM5LCJleHAiOjE1MzQwNzg1Mzl9.Jivk7Pil6wukFkShzCCaHNq7qmxegvcyD83FkbglT0uYYP0azTW2rM-FH4R8WYneTu1A5gQmUjB6VdFJh8APz5Qej_AA4RP3Q6nH-9qbytxQ5cebiEuuhRSridDxbXxuS0-oquQ0PkRtpenJ75mLJFzVROeaBWgKFNNcFIrV9hs

  放到 jwt.io中去解密。可以看到數據的架構如下所示:

  

  之前我們做過實驗,當alg選擇爲 none 的時候,是不用對JWT進行簽名的,這樣的jwt也被稱爲 不安全的jwt。

  這道題目的思路就是修改 alg。

  當後端不限定alg的時候,這種方法就可以被利用。當然jwt.io是不會讓你把alg改成none的。你需要自己手動改:

  

  對頭部稍微操作一下,得到的新token如下:

  eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJzdGF0dXMiOiJzdWNjZXNzIiwiZGF0YSI6eyJpZCI6MSwiZW1haWwiOiJqd3RuM2RAanVpY2Utc2gub3AiLCJwYXNzd29yZCI6IjAxOTIwMjNhN2JiZDczMjUwNTE2ZjA2OWRmMThiNTAwIiwiY3JlYXRlZEF0IjoiMjAxOC0wOC0xMiAwNzo1MzozOC4wNjQgKzAwOjAwIiwidXBkYXRlZEF0IjoiMjAxOC0wOC0xMiAwNzo1MzozOC4wNjQgKzAwOjAwIn0sImlhdCI6MTUzNDA2MDUzOSwiZXhwIjoxNTM0MDc4NTM5fQ

  將新的jwt發送,可以解決這個題目:

  

  後面還有個jwt issue 2 ,我分解不了公鑰,按照官方文檔的做法也無從下手。做出來的可以交流一二。

  實戰練習3 加密方式更改

  那個網站的後端代碼是不能夠演示加密方式修改的攻擊方法的。那篇博客有些問題。這邊就給出個樣例。

  後端的僞代碼應該如下所示:

  # sometimes called "decode" verify(string token, string verificationKey){ # returns payload if valid token, else throws an error } string token = $input string verificationKey = file_get_content('rsa_pub.key')

  後端代碼應該會判斷jwt的加密方式,其實這種方法是比較侷限的。首先對於一個優秀的JWT的庫而言,RS256和SH256的認證不會放在一起。另外,HMAC應當禁止公鑰作爲secret。

  例如:在pyjwt中,這種方法是被禁止的。會拋出錯誤。

  jwt.exceptions.InvalidKeyError: The specified key is an asymmetric key or x509 certificate and should not be used as an HMAC secret.

  大概寫了個小腳本,利用了公鑰來簽名,如下所示:

  $secret = file_get_contents("./key_box/public.pem"); //var_dump($secret); $sign = new Sha256(); $token = "eyJ0eXAiO..."; $token = (new Parser())->parse((string) $token); $hacktoken = (new Builder()) ->setIssuer($token->getClaim('iss')) ->setIssuedAt($token->getClaim('iat')) ->setExpiration($token->getClaim('exp')) ->set("data",["hack"=>"shaobaobaoer"]) ->sign($sign,$secret) ->getToken(); echo $hacktoken.PHP_EOL; var_dump($hacktoken->verify($sign,$secret)); 實戰練習4 HMAC祕鑰爆破

  參考鏈接:https://delcoding.github.io/2018/03/jwt-bypass/。

  在這道題目中,訪問web,可以返回一個jwt的字符串:

  eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhZG1pbiI6ImZhbHNlIn0.oe4qhTxvJB8nNAsFWJc7_m3UylVZzO3FwhkYuESAyUM

  將它解密,可以發現,算法是HS256,admin爲flase。

  { "alg": "HS256", "typ": "JWT" } { "admin": "false" }

  我們的目標很簡單,只需要將admin轉成true就可以了。而此刻能夠做的只有爆破祕鑰了。你當然可以寫一個小腳本來爆破祕鑰,這裏推薦一個工具c-jwt cracker。

  通過小工具,我們能迅速跑出祕鑰來,我的VPS大概用了2m跑出了祕鑰 54l7y。

  

  提交JWT即可得到flag。

  參考:

  JWT Hand Book:https://auth0.com/resources/ebooks/jwt-handbook

  

  *本文作者:NinthDevilHunster,轉載請註明來自FreeBuf.COM

相關文章