Spring Bootでエラーページをカスタマイズする
「Spring Bootで簡単なWebアプリケーションを書いてみる」では、Spring Bootで簡単なWebアプリケーションを書いた。 ここでは、デフォルトのエラーページをカスタマイズしてみる。
環境
Windows 10 Pro、Java SE 8、Spring Framework 4.3.7.RELEASE(Spring Boot 1.5.2.RELEASE)
>systeminfo OS 名: Microsoft Windows 10 Pro OS バージョン: 10.0.14393 N/A ビルド 14393 OS ビルドの種類: Multiprocessor Free システムの種類: x64-based PC プロセッサ: 1 プロセッサインストール済みです。 [01]: Intel64 Family 6 Model 69 Stepping 1 GenuineIntel ~1596 Mhz >java -version java version "1.8.0_121" Java(TM) SE Runtime Environment (build 1.8.0_121-b13) Java HotSpot(TM) 64-Bit Server VM (build 25.121-b13, mixed mode)
Custom error pageを作る
Spring Bootのデフォルトでは、コントローラで例外が発生するとHTTPの500エラーと合わせてWhitelabel Error Pageと呼ばれるページが表示される。 このページには例外の内容が表示されており、ユーザに不親切であると同時にセキュリティ上の問題を誘発するおそれがある。
Spring Bootでは、errorという名前のビューを作成することで独自のエラーページを表示させることができる。
- 27.1.9 Error Handling - Spring Boot Reference Guide
- spring-boot/error.html at v1.5.2.RELEASE · spring-projects/spring-boot
テンプレートエンジンとしてThemeleafを使用している場合は、次のパスにファイルを作成すればよい。
- src/main/resources/templates/error.html
<!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="utf-8" /> <title>Error</title> </head> <body> <h1 th:text="${status} + ' ' + ${error}">500 Internal Server Error</h1> <p><a th:href="@{/}">Back to index</a></p> </body> </html>
特定のHTTPステータスに対応するerror pageを作る
/error.htmlは任意のステータスについてのエラーページとなるが、404など特定のステータスに応じたエラーページを作成したい場合もある。
そのような場合は、/error/XXX.html
を作成すればよい。
- src/main/resources/templates/error/404.html
<!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="utf-8" /> <title>Error</title> </head> <body> <h1 th:text="${status} + ' ' + ${error}">404 Not Found</h1> <p>The requested URL was not found on this server 😇</p> <p><a th:href="@{/}">Back to index</a></p> </body> </html>
アプリケーションを起動し、ブラウザから存在しないパス/four-zero-four
にアクセスした際のスクリーンショットを次に示す。
関連リンク
Spring BootでMySQLを使う
「TomcatとApache HTTP ServerでSpring Bootアプリケーションをデプロイしてみる」では、作成したSpring BootアプリケーションをTomcat上にデプロイした。 しかし、現在の状態ではデータベースとしてH2を利用する状態になっている。 ここでは、アプリケーションを書き換え、データベースとしてMySQLを利用するようにしてみる。
環境
Ubuntu 16.04.2 LTS 64bit版
$ uname -a Linux vm-ubuntu64 4.4.0-72-generic #93-Ubuntu SMP Fri Mar 31 14:07:41 UTC 2017 x86_64 x86_64 x86_64 GNU/Linux $ lsb_release -a No LSB modules are available. Distributor ID: Ubuntu Description: Ubuntu 16.04.2 LTS Release: 16.04 Codename: xenial
サーバにMySQLをインストールする
まず、接続先となるMySQLサーバを用意する。 ここでは、TomcatをインストールしたサーバにMySQLをインストールすることにする。
aptを使い、MySQLをインストールするには次のようにする。 このとき、ダイアログに従いrootパスワードを適当に設定する。
$ sudo apt install mysql-server
MySQLに接続し、接続先となるデータベースと接続に利用するユーザを作成する。
$ mysql -u root -p Enter password: [type root password] mysql> CREATE DATABASE demo CHARACTER SET utf8mb4 COLLATE utf8mb4_bin; Query OK, 1 row affected (0.00 sec) mysql> GRANT ALL PRIVILEGES ON demo.* TO demo@localhost IDENTIFIED BY 'youmustchangethis'; Query OK, 0 rows affected, 1 warning (0.00 sec) mysql> FLUSH PRIVILEGES; Query OK, 0 rows affected (0.00 sec) mysql> \q Bye
データベースの設定を行う
次に、アプリケーションを書き換え、MySQLデータベースを利用するように設定する。
MySQLを利用する場合、依存パッケージにMySQL Connector-Jを追加する必要がある。 プロジェクトにMySQL Connector-Jを追加するには次のようにする。
- 「demo [boot]」を右クリックして、「Maven」→「Add Dependency」を選択
- ダイアログの各フィールドに以下の文字列を入力してOK
Spring Bootでは、設定ファイルとしてpropertiesファイル(またはYAMLファイル)を使用する。 まず、application.propertiesに本番環境用のプロファイルを使う設定を記述する。
- src/main/resources/application.properties
spring.profiles.active=production
次に、本番環境用のプロファイルに接続先データベースの設定を記述する。
- src/main/resources/application-production.properties
spring.jpa.hibernate.ddl-auto=create spring.datasource.url=jdbc:mysql://localhost:3306/demo spring.datasource.username=demo spring.datasource.password=youmustchangethis
spring.jpa.hibernate.ddl-auto=create
を指定することで、アプリケーション起動時に必要なテーブルが作成される。
テーブルが作成された後は、update
に変更することでコードに合わせてテーブルが修正されるようになる。
Tomcatにデプロイする
「TomcatとApache HTTP ServerでSpring Bootアプリケーションをデプロイしてみる」と同様の手順でWARファイルをエクスポートし、ROOT##002.war
にリネームする。
続けて、Tomcat ManagerからROOT##002.war
をデプロイする。
アプリケーションに接続し、適当なデータを追加した後、MySQLデータベースを確認すると次のようになる。
$ mysql -u root -p Enter password: [type root password] mysql> use demo; Database changed mysql> show tables; +------------------+ | Tables_in_demo | +------------------+ | message | | user | | user_authorities | +------------------+ 3 rows in set (0.00 sec) mysql> select * from message; +----+---------------------+-------+-------------+-------+ | id | created_at | name | remote_addr | text | +----+---------------------+-------+-------------+-------+ | 1 | 2017-04-26 10:21:24 | name1 | 127.0.0.1 | text1 | | 2 | 2017-04-26 10:21:31 | name2 | 127.0.0.1 | text2 | | 3 | 2017-04-26 10:21:40 | name3 | 127.0.0.1 | text3 | +----+---------------------+-------+-------------+-------+ 3 rows in set (0.00 sec) mysql> \q Bye
上の結果から、Entityに対応するテーブルが作成されていること、追加したデータが挿入されていることが確認できる。
本番環境でMySQL、開発環境でH2を使う
本番環境ではMySQLを使うが、開発時はH2を使いたい場合がある。
このような場合は、開発時spring.profiles.active
の行を#
でコメントアウトしておき、WARファイルのエクスポート前にコメントアウトを外すようにすればよい。
関連リンク
TomcatとApache HTTP ServerでSpring Bootアプリケーションをデプロイしてみる
「Spring Securityでユーザ認証を実装してみる」では、Spring Securityで簡単なユーザ認証を実装した。 ここでは、UbuntuサーバにTomcatをインストールし、作成したアプリケーションをデプロイしてみる。 また、Apache HTTP Serverをインストールし、アプリケーションをHTTPSで配信してみる。
環境
Ubuntu 16.04.2 LTS 64bit版
$ uname -a Linux vm-ubuntu64 4.4.0-72-generic #93-Ubuntu SMP Fri Mar 31 14:07:41 UTC 2017 x86_64 x86_64 x86_64 GNU/Linux $ lsb_release -a No LSB modules are available. Distributor ID: Ubuntu Description: Ubuntu 16.04.2 LTS Release: 16.04 Codename: xenial
Tomcatのインストール
まず、Tomcat本体とTomcat Managerをインストールする。
$ sudo apt install tomcat8 tomcat8-admin
インストールが終わったら、Tomcat Managerへのアクセスを有効にするため、ユーザを作成する。 ここで、ユーザ名・パスワードは推測不可能なものに変えておくこと。
$ sudo vi /etc/tomcat8/tomcat-users.xml (snip) <role rolename="manager-gui"/> <role rolename="admin-gui"/> <user username="USERNAME" password="PASSWORD" roles="manager-gui,admin-gui"/> </tomcat-users>
Tomcatサーバを再起動し、設定を反映させる。
$ sudo systemctl restart tomcat8
ブラウザから http://localhost:8080/ を開き、Tomcatのデフォルトページが表示されることを確認する。 また、http://localhost:8080/manager/html に設定したユーザ名・パスワードでアクセスし、Tomcat Managerが表示されることを確認する。
WARファイルをデプロイしてみる
Tomcatがインストールできたので、Tomcat ManagerからWARファイルをデプロイしてみる。
まず、「Spring Bootで簡単なWebアプリケーションを書いてみる」で説明した手順に従い、アプリケーションのWARファイルを作成する。
Tomcatでは、WARファイルのファイル名は パス名 + "##" + バージョン番号 + ".war"
という命名規約に従って扱われる。
なお、配置するパスが /
の場合はパス名を ROOT
とする。
また、バージョン番号の比較は単純な文字列比較で行われるため、ゼロパディングすることが推奨されている。
配置するパスを /
、バージョン番号を 001
とし、作成したWARファイルを ROOT##001.war
にリネームする。
これを、Tomcat Managerからファイルアップロードし、デプロイする。
アップロードが完了した後、http://localhost:8080/ にアクセスすると、デプロイしたアプリケーションが表示される。
アプリケーションを更新する際には、パス名をそのままに、バージョン番号の数字を上げたWARファイルを新たにデプロイする。 これにより、古いバージョンに接続しているセッションはそのままに、新規セッションが新バージョンに接続される(Parallel Deployment)。 ただし、同時に複数バージョンのアプリケーションを動作させることになるため、メモリ使用量に注意が必要である。
Apache HTTP Serverと連携してHTTPS対応してみる
Tomcatは簡易HTTPサーバとしても機能するが、パフォーマンスの観点からApache HTTP Server(以下Apache)と連携させて使うことが多い。 そこで、Apacheをインストールし、HTTPSでアプリケーションを配信してみる。 なお、HTTPの場合もSSLに関連する部分を除いて同様に行えばよい。
まず、TomcatのAJPコネクタを有効にする。 具体的には、下記の箇所のコメントを外す。
$ sudo vi /etc/tomcat8/server.xml <!-- Define an AJP 1.3 Connector on port 8009 --> <Connector port="8009" protocol="AJP/1.3" redirectPort="8443" />
Tomcatを再起動し、設定を反映させる。
$ sudo service tomcat8 restart
次に、Apacheをインストールし、proxy_ajpモジュールとsslモジュールを有効にする。
$ sudo apt install apache2 $ sudo a2enmod proxy_ajp ssl
default-sslをベースに、サイト設定ファイルを作成する。 具体的には、DocumentRootをコメントアウトし、ProxyPassを追記する。
$ sudo cp /etc/apache2/sites-available/{default-ssl,tomcat-ssl}.conf $ sudo vi /etc/apache2/sites-available/tomcat-ssl.conf #DocumentRoot /var/www/html ProxyPass / ajp://localhost:8009/
作成したサイト設定を有効にし、Apacheを再起動する。
$ sudo a2ensite tomcat-ssl $ sudo service apache2 restart
ブラウザから https://localhost/ にアクセスすると、HTTPSでアプリケーションにアクセスできることが確認できる。
さらに、curlコマンドでレスポンスヘッダを確認してみると次のようになる。
$ curl -v --insecure https://localhost/login (snip) > GET /login HTTP/1.1 > Host: localhost > User-Agent: curl/7.47.0 > Accept: */* > < HTTP/1.1 200 OK < Date: Mon, 24 Apr 2017 07:56:29 GMT < Server: Apache/2.4.18 (Ubuntu) < X-Content-Type-Options: nosniff < X-XSS-Protection: 1; mode=block < Cache-Control: no-cache, no-store, max-age=0, must-revalidate < Pragma: no-cache < Expires: 0 < Strict-Transport-Security: max-age=31536000 ; includeSubDomains < X-Frame-Options: DENY < Set-Cookie: JSESSIONID=E49A71C1D34D08184F23B1BF59483BB1; Path=/; Secure; HttpOnly < Content-Type: text/html;charset=UTF-8 < Content-Language: en-US < Vary: Accept-Encoding < Transfer-Encoding: chunked < (snip)
上の結果から、Spring SecurityによりStrict-Transport-SecurityヘッダおよびCookieのSecure属性が付与されていることが確認できる。
あとは、通常のApacheと同様に、SSL証明書やログの設定を行えばよい。
実行可能jarとnginxによるデプロイ
Spring Bootでは従来のWARファイルによるデプロイの他に、組み込みtomcatを用いた実行可能jarによるデプロイも可能である。 最近ではTwelve-Factor Appの観点から、実行可能jarとして起動したアプリケーションをnginxで振り分ける方法が好まれることもある。 詳しくは、下記のページを参照。
関連リンク
Spring Bootでテストを書いてみる
「Spring Securityでユーザ認証を実装してみる」では、Spring Securityで簡単なユーザ認証を実装した。 ここでは作成したアプリケーションについて、認証処理の挙動に関するテストコードを記述してみる。
環境
Windows 10 Pro、Java SE 8、Spring Framework 4.3.7.RELEASE(Spring Boot 1.5.2.RELEASE)
>systeminfo OS 名: Microsoft Windows 10 Pro OS バージョン: 10.0.14393 N/A ビルド 14393 OS ビルドの種類: Multiprocessor Free システムの種類: x64-based PC プロセッサ: 1 プロセッサインストール済みです。 [01]: Intel64 Family 6 Model 69 Stepping 1 GenuineIntel ~1596 Mhz >java -version java version "1.8.0_121" Java(TM) SE Runtime Environment (build 1.8.0_121-b13) Java HotSpot(TM) 64-Bit Server VM (build 25.121-b13, mixed mode)
spring-securty-testを依存ライブラリに追加する
Spring Bootの場合、テスト用ライブラリが標準で含まれている。 しかし、Spring Securityを使う場合は、CSRFトークン付きのPOSTや認証状態のセットを行うためにspring-security-testを依存ライブラリに追加する必要がある。
MavenのCentral Repositoryを検索すると、spring-security-testの最新版が4.2.2.RELEASEであることがわかる。
- g:“org.springframework.security” AND a:“spring-security-test” - The Central Repository Search Engine
プロジェクトにspring-security-test 4.2.2.RELEASEを追加するには次のようにする。
- 「demo [boot]」を右クリックして、「Maven」→「Add Dependency」を選択
- ダイアログの各フィールドに以下の文字列を入力してOK
- GroupId: org.springframework.security
- ArtifactId: spring-security-test
- Version: 4.2.2.RELEASE
簡単なテストコードを書いてみる
Spring Bootでは、JUnitを用いてテストを記述する。
また、テストコードはsrc/test/java
以下で管理する。
実際にテストコードを書いてみると次のようになる。
package com.example; import static org.hamcrest.CoreMatchers.containsString; import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.anonymous; import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf; import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user; import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.redirectedUrl; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.view; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.junit4.SpringRunner; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.setup.MockMvcBuilders; import org.springframework.transaction.annotation.Transactional; import org.springframework.web.context.WebApplicationContext; @RunWith(SpringRunner.class) @SpringBootTest @AutoConfigureMockMvc @Transactional public class AuthControllerTests { @Autowired private WebApplicationContext context; private MockMvc mvc; @Autowired private UserService userService; @Before public void setup() { mvc = MockMvcBuilders .webAppContextSetup(context) .apply(springSecurity()) .build(); userService.registerUser("testuser", "iamatestuser", "testuser@localhost"); } @Test public void shouldAnonymousBeRedirected() throws Exception { this.mvc.perform(get("/messages").with(anonymous())) .andExpect(status().isFound()) .andExpect(redirectedUrl("http://localhost/login")); } @Test public void shouldNewUserBeAbleToSignupAndLogin() throws Exception { this.mvc.perform(post("/signup").with(csrf()) .param("username", "testuser2") .param("password", "iamatestuser") .param("mailAddress", "testuser2@localhost")) .andExpect(status().isFound()) .andExpect(redirectedUrl("/messages")); this.mvc.perform(post("/login").with(csrf()) .param("username", "testuser2") .param("password", "iamatestuser")) .andExpect(status().isFound()) .andExpect(redirectedUrl("/")); this.mvc.perform(get("/messages").with(user("testuser2"))) .andExpect(status().isOk()) .andExpect(view().name("messages")); } @Test public void shouldSignupWithDuplicateUsernameFail() throws Exception { this.mvc.perform(post("/signup").with(csrf()) .param("username", "testuser") .param("password", "duplicateusername") .param("mailAddress", "foobar@localhost")) .andExpect(status().isOk()) .andExpect(content().string(containsString("already used"))); } @Test public void shouldSignupWithDuplicateMailAddressFail() throws Exception { this.mvc.perform(post("/signup").with(csrf()) .param("username", "testuser2") .param("password", "duplicatemailaddress") .param("mailAddress", "testuser@localhost")) .andExpect(status().isOk()) .andExpect(content().string(containsString("already used"))); } @Test public void shouldLoginWithNonExistUserFail() throws Exception { this.mvc.perform(post("/login").with(csrf()) .param("username", "nonexistuser") .param("password", "iamatestuser")) .andExpect(status().isFound()) .andExpect(redirectedUrl("/login-error")); } @Test public void shouldLoginWithWrongPasswordFail() throws Exception { this.mvc.perform(post("/login").with(csrf()) .param("username", "testuser") .param("password", "wrongpassword")) .andExpect(status().isFound()) .andExpect(redirectedUrl("/login-error")); } @Test public void shouldLoginWithJPQLInjectionFail() throws Exception { this.mvc.perform(post("/login").with(csrf()) .param("username", "testuser' OR ''='") .param("password", "jpqlinjection")) .andExpect(status().isFound()) .andExpect(redirectedUrl("/login-error")); } }
上のコードでは、次のような内容をテストしている。
- shouldAnonymousBeRedirected(): 非ログインユーザがログイン画面にリダイレクトされること
- shouldNewUserBeAbleToSignupAndLogin(): 新規ユーザがアカウント登録・ログインできること
- shouldSignupWithDuplicateUsernameFail(): すでに存在するユーザ名でのアカウント登録が失敗すること
- shouldSignupWithDuplicateMailAddressFail(): すでに存在するメールアドレスでのアカウント登録が失敗すること
- shouldLoginWithNonExistUserFail(): 存在しないユーザ名でのログインが失敗すること
- shouldLoginWithWrongPasswordFail(): 誤ったパスワードでのログインが失敗すること
- shouldLoginWithJPQLInjectionFail(): JPQL injection攻撃による不正ログインが失敗すること
MockMvcを用いることで、エンドポイントへのリクエストとそれに対するレスポンスをテストすることができる。 @Beforeはすべてのテストメソッド(@Test)の前に実行される。 また、@Afterが存在する場合はすべてのテストメソッドの後に実行される。
また、クラスに@Transactional
をつけることで、@Before、@Test、@Afterのサイクルが終わるたびにDBへの変更がrollbackされる。
テスト中でDBへの変更が加えられるような場合、指定しておくとよい。
Annotating a test method with @Transactional causes the test to be run within a transaction that will, by default, be automatically rolled back after completion of the test. If a test class is annotated with @Transactional, each test method within that class hierarchy will be run within a transaction.
テストを実行してみる
テストを実行するには次のようにする。
- 「demo [boot]」を右クリックして「Run As」→「4 JUnit Test」を選択
実行すると、右下のペインにJUnitのテスト結果が表示される。
関連リンク
PlaidCTF 2017 供養(Writeup)
PlaidCTF 2017に参加。236ptで182位。
sanity check (Misc 1)
The flag is PCTF{poop}
logarithms are hard (Misc 10)
What is e1.000000001?
Please enter in decimal with 7 places. (For example, if the answer was 2.71828183… the flag would be 2.7182818 )
普通に計算すると2.7182818になるが、通らない。
$ gp -q ? exp(1.000000001) 2.7182818311773270651784736213955195583
過去の問題にならい、対数に関するバグについて調べてみると、関数電卓TI-35Xのバグについて書かれているページが見つかった。
関数電卓TI-30X/TI-36X SOLARで計算される値を答えたところ、通った。
2.7191928
zipper (Misc 50)
壊れているzipファイルが与えられる。
$ unzip zipper_50d3dc76dcdfa047178f5a1c19a52118.zip Archive: zipper_50d3dc76dcdfa047178f5a1c19a52118.zip warning: filename too long--truncating. : bad extra field length (central)
データがdeflateで圧縮されているものと仮定し、1バイトずつずらしながら展開を試みるとフラグが得られた。
import zlib with open('zipper_50d3dc76dcdfa047178f5a1c19a52118.zip') as f: data = f.read() for i in xrange(len(data)): print hex(i) chunk = data[i:] try: print zlib.decompress(chunk, -15) except zlib.error as e: pass
$ python test.py 0x0 0x1 f 0x2 (snip) 0x42 Huzzah, you have captured the flag: PCTF{f0rens1cs_yay} (snip) 0xea 0xeb
Multicast (Crypto 175)
RSA。 generate.sageを読むと、mを512bitの整数として、ai, bi, ci, niを5組出力しており、これがdata.txtとして与えられている。
nbits = 1024 e = 5 flag = open("flag.txt").read().strip() assert len(flag) <= 64 m = Integer(int(flag.encode('hex'),16)) out = open("data.txt","w") for i in range(e): (snip) mi = ai*m + bi ci = pow(mi, e, ni) (snip)
一般化されたHåstad’s broadcast attackで解ける。
a1 = 69083553146183344839772978505267712546348345951915552104934666999364893606414967631473531356169090286677897220956536147203986323670778671853966574104550916190831974039291113434428713226539656233005536401237618731903013444912424490954403050283261443145833828774338405850464149745370010921929208582803372719662 b1 = 1847353606880715823527099792264841437499256859046112182901817731898583109812226034453486290034759426343181112477498401843027934563915568068662901070357638240406212287869791959600724393823543974271939728800168160170264361943043888105413027223078955278440961637038127459036967868261370295558665462738851664674 c1 = 13924397671629169794872532654354896735104301636897981275021597449713744312702850679542667853889775700566291452624971794825605462609659305736119167763171202028684488729179106128850062049316631850373987498751231054904292395688010798166991604433834113228237987911867991301264314792383410544657232986272138720776 n1 = 107176667955056054888106289112863406099193890480114548290390156586316295797860714447523894766296345238663487884525751440074160698573876676529510120482633305843604033670838833224316621039974842663423971964367617383524243164757298543267528648455662196669719411550337416706436014652957797379123891565563380621721 a2 = 27025595670607123748133955387986761928986723842407963667833744851457582823949409825451502999363014637018266381381800334740959357732693327871649715849440610655814448606276225985636706847320729571129651265791968308846145775455134446011658371731137714821594128627843120134392299295278193619998726010683820300906 b2 = 76626521446699164152465241536023395842467420530412705255543098462161399927889232797875074266701010280661667851769988806969262190694662659916308198862288454347865777843483804317714263754964326435968683758707252482706186266157923587034286069745922345373717550980870551177895535541280767243258161167226140529179 c2 = 74820001900443652318294067181707795319993357148314983190736617977547527951708569170206478241294503335669164830735894900549009883421968340647253048091278584463455833751347773269310148496440204410893225151858930449694234125324321395015257722229646579662704510552084383728048226410232141635889320648460673299335 n2 = 116940935524323795130742526994657925027084667081524402264947741484054748546876541534619667289853985612350852220069194599338546449074832255593771931123087798582432365325897386813589970457955530817464044545954375253380129491859158211405510009895941963611340400391118219478974834168088677930751757315159110921841 a3 = 3004029671513060545517047598385797934912754972599429487078366757715733246340760740484289209221138121568405251845847578856114711659946228365274190780159966913357682874178318970562635144717761245951754408952773122354583612422645258507146795630561366955268226048296864853067903656400207186949463324893914485565 b3 = 39916349286626463801197107509708900778499859478703695888378041406302001988773262878930035955207487249221227735861868182215434820807844055889132141226547303138247159556705455695270756436706739886462479337109843197568024375533105139465029460850504284879314129902197486373473299170494478698741715992027042335920 c3 = 14108999386222915459940913638433135402751523880380098398237968303803785854582535395117824943688282642164021530817702254201291805032640778641839809162886268856556652703619578965302291774447936003759868066323907388148146727752981125365333046242305065445858381847752308536916145502431706840471314490470498925933 n3 = 100668102943542293762890526110731145470895119442621421158649868631915746146377966542031367735374167457406299434355768725246104804285632863373024153609404762902208230939718098090168262799174612923680636870557478441319366190063047687786785211877198536555745316733683533528849562416675658775836498023052914901749 a4 = 42305211872257990504112591982462712826623117527580306258621876854892728665082535381823460722212076449810881809439537932537619270097066694079814712136330261508201872405339882571623217945205024525421229411985020496914748229102523658456972139082089291624782533401385935487411288676903751198610145921301518231107 b4 = 37927948227203538741746385301195518552457676697015260677758470292294745311488915679784285945487731017142600921756814455642818173556814139042013977633776585763163271296255014647248684364947573160457459801483696377003575286309739820627536302900476424058673287304085337544422656511271366685116274814463373028939 c4 = 29808378278310626950656040722768610557513777159495566134112105939237344988328390169388824486265334935284692186547128505302437631500267072936205473588904993250984366335171085215776781494184026623681600861765081754836053424849422843633084928107942855476866942904060229897408614881375916806167293356422910088562 n4 = 80450031402201203587730534009338660865222026164984422981250298135548464160260574889923232542563882168618899594606271070764435488631855210441010916420059803623249294290301484880522559794167514055495411717729597100446344174893389277795663285345593590647340104666332031316803275436638414115193469755829893511487 a5 = 35798442949372217448131245718267683892608947365147471016957781854054771928382329233065506241624702265207177594187978141208820788381204095301540789196845409818326829309725267401115274448449544207730582441399296587733377747658506089439891602945020732454221665678849354836056443444544603686977883511757122333808 b5 = 15148604454321045616017299717211628700774278430049633748723698755621714384646643282913626328387905917563070000934175316190944226369346680779250458706206743862204417104832969982264440206554850551723416393459529398494316425018200651517022177685036277113534264161058597282135279496222169055121895473052233762246 c5 = 90847039947327313643774540191254303763104502856236195148227169238729840332433440088244863054796973078431673232305152421572163200464861625035107176753552370732936207870756803675490237581973525804663863823181351604257906567409397759543713937699041851977124940235865938476195092603195522703292454934101412022811 n5 = 93862849424043561789940054837392625966883465717813492561917969675964529083848723311514364070936115132154848096497003118110036719543385624344289211314373383330520240583297483284951457880437389550791045654411987778690675595035123064122359417085803319134114455113012761800931917960807358624095094896528184569953 e = 5 n = n1*n2*n3*n4*n5 t1 = crt([1,0,0,0,0], [n1,n2,n3,n4,n5]) t2 = crt([0,1,0,0,0], [n1,n2,n3,n4,n5]) t3 = crt([0,0,1,0,0], [n1,n2,n3,n4,n5]) t4 = crt([0,0,0,1,0], [n1,n2,n3,n4,n5]) t5 = crt([0,0,0,0,1], [n1,n2,n3,n4,n5]) PR.<x> = PolynomialRing(Zmod(n)) g1 = (a1*x + b1)^e - c1 g2 = (a2*x + b2)^e - c2 g3 = (a3*x + b3)^e - c3 g4 = (a4*x + b4)^e - c4 g5 = (a5*x + b5)^e - c5 g = 1*t1*g1 + 2*t2*g2 + 3*t3*g3 + 4*t4*g4 + 5*t5*g5 g = g.monic() kbits = 64*8 x = g.small_roots(X=2^kbits, beta=1)[0] print hex(long(x))[2:-1].decode('hex')
sage@vm-ubuntu64:~$ sage test.sage PCTF{L1ne4r_P4dd1ng_w0nt_s4ve_Y0u_fr0m_H4s7ad!}
所感
他に解きたかった問題は以下。
- no_mo_flo (Reversing 125)
- Echo (Web 200)
- Pykemon (Web 151)
- bigpicture (Pwnable 200)
Spring Securityでユーザ認証を実装してみる
「Spring Bootで簡単なWebアプリケーションを書いてみる」では、Spring Bootで簡単なWebアプリケーションを書いた。 ここでは作成したアプリケーションをベースに、Spring Securityを使ってユーザ認証を実装してみる。
環境
Windows 10 Pro、Java SE 8、Spring Framework 4.3.7.RELEASE(Spring Boot 1.5.2.RELEASE)
>systeminfo OS 名: Microsoft Windows 10 Pro OS バージョン: 10.0.14393 N/A ビルド 14393 OS ビルドの種類: Multiprocessor Free システムの種類: x64-based PC プロセッサ: 1 プロセッサインストール済みです。 [01]: Intel64 Family 6 Model 69 Stepping 1 GenuineIntel ~1596 Mhz >java -version java version "1.8.0_121" Java(TM) SE Runtime Environment (build 1.8.0_121-b13) Java HotSpot(TM) 64-Bit Server VM (build 25.121-b13, mixed mode)
Spring Securityを依存ライブラリに追加する
Spring Securityは、認証・認可を中心に、各種HTTPセキュリティヘッダやCSRF対策を含めたセキュリティ機能を提供するライブラリである。
Hello Spring Security with Bootを参考に、以下の三つのライブラリをプロジェクトの依存ライブラリに追加する。
- spring-security-web
- groupId: org.springframework.security
- artifactId: spring-security-web
- version: 4.2.2.RELEASE
- spring-security-config
- groupId: org.springframework.security
- artifactId: spring-security-config
- version: 4.2.2.RELEASE
- thymeleaf-extras-springsecurity4
- groupId: org.thymeleaf.extras
- artifactId: thymeleaf-extras-springsecurity4
- version: 2.1.2.RELEASE
依存ライブラリの追加は次のように行う。
- 「demo [boot]」を右クリックして、「Maven」→「Add Dependency」を選択
- ダイアログの各フィールドを入力してOK
Spring Securityを有効にする
まず、Spring Securityの設定を行うWebSecurityConfigクラスを作成する。 ここでは、パッケージ名をcom.example.configに分けることにする。
package com.example.config; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; @EnableWebSecurity public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http .authorizeRequests() .antMatchers("/", "/login", "/login-error").permitAll() .antMatchers("/**").hasRole("USER") .and() .formLogin() .loginPage("/login").failureUrl("/login-error"); } @Autowired public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception { auth .inMemoryAuthentication() .withUser("user").password("password").roles("USER"); } }
次に、認証関係のエンドポイントを実装するControllerを作る。
ついでに、/
にアクセスした際は認証後の適当なページにリダクレクトするようにする。
package com.example; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; @Controller public class AuthController { @RequestMapping("/") public String index() { return "redirect:/messages"; } @GetMapping("/login") public String login() { return "login"; } @PostMapping("/login") public String loginPost() { return "redirect:/login-error"; } @GetMapping("/login-error") public String loginError(Model model) { model.addAttribute("loginError", true); return "login"; } }
続けて、ログインページのテンプレートファイルを作る。
- src/main/resources/templates/login.html
<!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="utf-8" /> <title>Login page</title> </head> <body> <h1>Login page</h1> <p>Example user: user / password</p> <form th:action="@{/login}" method="post"> <p th:if="${loginError}"><em>Username or password is wrong.</em></p> <p><label for="username">Username</label>: <input type="text" id="username" name="username" autofocus="autofocus" /></p> <p><label for="password">Password</label>: <input type="password" id="password" name="password" /></p> <p><input type="submit" value="Log in" /></p> </form> <p><a th:href="@{/signup}">Sign up</a></p> <p><a th:href="@{/}">Back to index</a></p> </body> </html>
ここでは、あとで実装する新規登録ページへのリンクも記述している。
最後に、ログイン後のページで認証情報を表示するように変更する。
- src/main/resources/templates/messages.html
<!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org" xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity4"> <head> <meta charset="UTF-8" /> <title>Hello, Spring Boot!</title> </head> <body> <h1>Hello, Spring Boot!</h1> <div th:fragment="logout" sec:authorize="isAuthenticated()"> <p>Login as: <span sec:authentication="name"></span> <span sec:authentication="principal.authorities"></span></p> <form action="#" th:action="@{/logout}" method="post"> <input type="submit" value="Logout" /> </form> </div> (snip)
ユーザ認証が有効になっていることを確認してみる
アプリケーションを起動し、ブラウザで http://localhost:8080/messages にアクセスすると作成したログインページにリダイレクトされる。 ここで、ハードコードした認証情報を入力してログインした後のスクリーンショットを次に示す。
ログインユーザのユーザ名とロールが表示されていることが確認できる。
curlコマンドを使ってアクセスしてみると次のようになる。
$ curl -vL http://localhost:8080/messages (snip) > GET /messages HTTP/1.1 > Host: localhost:8080 > User-Agent: curl/7.52.1 > Accept: */* > (snip) < HTTP/1.1 302 < X-Content-Type-Options: nosniff < X-XSS-Protection: 1; mode=block < Cache-Control: no-cache, no-store, max-age=0, must-revalidate < Pragma: no-cache < Expires: 0 < X-Frame-Options: DENY < Set-Cookie: JSESSIONID=EF42809C6B5C0F4776B475AE64DFB530;path=/;HttpOnly < Location: http://localhost:8080/login < Content-Length: 0 < Date: Wed, 12 Apr 2017 05:02:50 GMT < (snip) > GET /login HTTP/1.1 > Host: localhost:8080 > User-Agent: curl/7.52.1 > Accept: */* > (snip) < HTTP/1.1 200 < X-Content-Type-Options: nosniff < X-XSS-Protection: 1; mode=block < Cache-Control: no-cache, no-store, max-age=0, must-revalidate < Pragma: no-cache < Expires: 0 < X-Frame-Options: DENY < Set-Cookie: JSESSIONID=5199D5F5515BA8D428C2E2376ACEF95B;path=/;HttpOnly < Content-Type: text/html;charset=UTF-8 < Content-Language: ja-JP < Transfer-Encoding: chunked < Date: Wed, 12 Apr 2017 05:02:50 GMT < <!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <title>Login page</title> </head> <body> <h1>Login page</h1> <p>Example user: user / password</p> <form method="post" action="/login"> <p><label for="username">Username</label>: <input type="text" id="username" name="username" autofocus="autofocus" /></p> <p><label for="password">Password</label>: <input type="password" id="password" name="password" /></p> <p><input type="submit" value="Log in" /> <a href="/recovery/">Forgot password?</a></p> <input type="hidden" name="_csrf" value="8d46d6f8-8ae5-4d89-805e-7cbb1cac8185" /></form> <p><a href="/signup">Sign up</a></p> <p><a href="/">Back to index</a></p> </body> </html> (snip)
レスポンスから、以下のセキュリティ機能が有効になっていることがわかる。
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
、Pragma: no-cache
、Expires: 0
- クライアントがキャッシュしないようにする。認証後のコンテンツがクライアント側にキャッシュとして残ることを防ぐ。
X-Frame-Options: DENY
- iframe内では表示されないようにする。クリックジャッキングを防ぐ。
Set-Cookie: JSESSIONID=7BFDAA8C20E3C9FCDC7569B879DBB406;path=/;HttpOnly
- CookieをJavaScriptから取得できないようにする。XSSによるセッションハイジャックを防ぐ。
<input type="hidden" name="_csrf" value="7e18728d-d26f-462f-9410-6c1b0d2671a9" />
この他にも、Session Fixationを防ぐためのセッションID更新や、HTTPSの場合にはHTTP Strict Transport Security(HSTS)ヘッダの付与が行われる。 個々の詳細については、リファレンスを参照。
DBにユーザ管理テーブルを作る
ハードコードした認証情報でログインできることが確認できたので、DBにユーザ管理テーブルを作り、アカウント作成ができるようにしてみる。 ここでは、Userロールに加えてAdminロールも作り、管理者権限を持ったユーザを作れるようにする。
まず、WebSecurityConfigクラスを変更し、ユーザ認証にUserServiceクラスを使うようにする。 また、パスワードはBCryptでハッシュ化して扱うようにする。
package com.example.config; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; import com.example.User; import com.example.UserService; @EnableWebSecurity public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private UserService userService; @Override protected void configure(HttpSecurity http) throws Exception { http .authorizeRequests() .antMatchers("/", "/login", "/login-error").permitAll() .antMatchers("/**").hasRole("USER") .and() .formLogin() .loginPage("/login").failureUrl("/login-error"); } @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth .userDetailsService(userService) .passwordEncoder(passwordEncoder()); userService.registerAdmin("admin", "youmustchangethis", "admin@localhost"); } @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } }
passwordEncoderメソッドには@Bean
アノテーションをつけ、他のクラスでDependency Injectionできるようにしておく。
次に、UserDetailsインタフェースを実装したEntityクラスを作る。
package com.example; import java.util.ArrayList; import java.util.Collection; import java.util.Date; import java.util.EnumSet; import java.util.List; import java.util.Set; import javax.persistence.Column; import javax.persistence.ElementCollection; import javax.persistence.Entity; import javax.persistence.EnumType; import javax.persistence.Enumerated; import javax.persistence.FetchType; import javax.persistence.GeneratedValue; import javax.persistence.GenerationType; import javax.persistence.Id; import javax.persistence.PrePersist; import javax.persistence.Temporal; import javax.persistence.TemporalType; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.userdetails.UserDetails; @Entity public class User implements UserDetails { private static final long serialVersionUID = 1L; public enum Authority {ROLE_USER, ROLE_ADMIN} @Id @GeneratedValue(strategy = GenerationType.AUTO) private Long id; @Column(nullable = false, unique = true) private String username; @Column(nullable = false) private String password; @Column(nullable = false, unique = true) private String mailAddress; @Column(nullable = false) private boolean mailAddressVerified; @Column(nullable = false) private boolean enabled; @Temporal(TemporalType.TIMESTAMP) private Date createdAt; @ElementCollection(fetch = FetchType.EAGER) @Enumerated(EnumType.STRING) @Column(nullable = false) private Set<Authority> authorities; // JPA requirement protected User() {} public User(String username, String password, String mailAddress) { this.username = username; this.password = password; this.mailAddress = mailAddress; this.mailAddressVerified = false; this.enabled = true; this.authorities = EnumSet.of(Authority.ROLE_USER); } @PrePersist public void prePersist() { this.createdAt = new Date(); } public boolean isAdmin() { return this.authorities.contains(Authority.ROLE_ADMIN); } public void setAdmin(boolean isAdmin) { if (isAdmin) { this.authorities.add(Authority.ROLE_ADMIN); } else { this.authorities.remove(Authority.ROLE_ADMIN); } } @Override public Collection<? extends GrantedAuthority> getAuthorities() { List<GrantedAuthority> authorities = new ArrayList<>(); for (Authority authority : this.authorities) { authorities.add(new SimpleGrantedAuthority(authority.toString())); } return authorities; } @Override public boolean isAccountNonExpired() { return true; } @Override public boolean isAccountNonLocked() { return true; } @Override public boolean isCredentialsNonExpired() { return true; } @Override public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } @Override public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } @Override public boolean isEnabled() { return this.enabled; } public void setEnabled(boolean enabled) { this.enabled = enabled; } public Long getId() { return id; } public String getMailAddress() { return mailAddress; } public void setMailAddress(String mailAddress) { this.mailAddress = mailAddress; } public boolean isMailAddressVerified() { return mailAddressVerified; } public void setMailAddressVerified(boolean mailAddressVerified) { this.mailAddressVerified = mailAddressVerified; } public Date getCreatedAt() { return createdAt; } }
ここでは、UserDetailsインタフェースで定義されているisAccountNonExpired()
、isAccountNonLocked()
、isCredentialsNonExpired()
は使用せず、常にtrueを返すようにしている。
また、isAdmin/setAdminメソッドを実装し、管理者権限の確認と変更を行えるようにしている。
ロールを表すAuthorityはenumで定義し、authoritiesメンバをAuthorityのSetとして定義する。
このようなCollectionを持つメンバについては、@ElementCollection(fetch = FetchType.EAGER)
をつけることでDB上のマッピングを行うことができる。
enumの場合はさらに@Enumerated(EnumType.STRING)
をつけ、Setインタフェースの実装としてEnumSetクラスを利用する。
続けて、DB操作に対応するRepositoryインタフェースを定義する。
package com.example; import org.springframework.data.repository.CrudRepository; import org.springframework.stereotype.Repository; @Repository public interface UserRepository extends CrudRepository<User, Long> { public User findByUsername(String username); public User findByMailAddress(String mailAddress); }
最後に、UserDetailsServiceインタフェースを実装したServiceクラスを作る。
package com.example; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @Service public class UserService implements UserDetailsService { @Autowired private UserRepository repository; @Autowired private PasswordEncoder passwordEncoder; @Override public User loadUserByUsername(String username) throws UsernameNotFoundException { if (username == null || "".equals(username)) { throw new UsernameNotFoundException("Username is empty"); } User user = repository.findByUsername(username); if (user == null) { throw new UsernameNotFoundException("User not found: " + username); } return user; } @Transactional public void registerAdmin(String username, String password, String mailAddress) { User user = new User(username, passwordEncoder.encode(password), mailAddress); user.setAdmin(true); repository.save(user); } @Transactional public void registerUser(String username, String password, String mailAddress) { User user = new User(username, passwordEncoder.encode(password), mailAddress); repository.save(user); } }
loadUserByUsernameメソッドは、ユーザ名を引数に取り対応するUserDetailsかUsernameNotFoundException例外を返す。 ここでは、ユーザ名が空かどうかをチェックした後、そのユーザ名でDBを検索している。
また、registerAdmin/registerUserメソッドでは、DBに保存する前にパスワードをBCryptでハッシュ化する。
passwordEncoderメンバは、@AutoWired
によるDependency Injectionにより、実行時にWebSecurityConfigクラスのメソッドが呼ばれて代入される。
ユーザ登録ページを作る
まず、WebSecurityConfigクラスのpermitAll()
に/signup
を追加し、非ロクインユーザがアクセスできるようにする。
package com.example.config; (snip) @EnableWebSecurity public class WebSecurityConfig extends WebSecurityConfigurerAdapter { (snip) @Override protected void configure(HttpSecurity http) throws Exception { http .authorizeRequests() .antMatchers("/", "/signup", "/login", "/login-error").permitAll() .antMatchers("/**").hasRole("USER") .and() .formLogin() .loginPage("/login").failureUrl("/login-error"); } (snip) }
次に、Controllerにユーザ登録ページのエンドポイントを作る。
package com.example; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.validation.Valid; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.dao.DataIntegrityViolationException; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.validation.BindingResult; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; @Controller public class AuthController { @Autowired UserService userService; (snip) @GetMapping("/signup") public String signup(Model model) { model.addAttribute("signupForm", new SignupForm()); return "signup"; } @PostMapping("/signup") public String signupPost(Model model, @Valid SignupForm signupForm, BindingResult bindingResult, HttpServletRequest request) { if (bindingResult.hasErrors()) { return "signup"; } try { userService.registerUser(signupForm.getUsername(), signupForm.getPassword(), signupForm.getMailAddress()); } catch (DataIntegrityViolationException e) { model.addAttribute("signupError", true); return "signup"; } try { request.login(signupForm.getUsername(), signupForm.getPassword()); } catch (ServletException e) { e.printStackTrace(); } return "redirect:/messages"; } }
package com.example; import javax.validation.constraints.Pattern; import javax.validation.constraints.Size; import org.hibernate.validator.constraints.Email; public class SignupForm { @Pattern(regexp="^\\w{3,32}$", message="size must be between 3 and 32, each character must be alphanumeric or underscore (A-Za-z0-9_)") private String username; @Size(min=8, max=255) private String password; @Email @Size(min=3, max=255) private String mailAddress; public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } public String getMailAddress() { return mailAddress; } public void setMailAddress(String mailAddress) { this.mailAddress = mailAddress; } }
フォームを送信すると、新しいユーザをDBに登録した後、ログインを行い適当なページにリダイレクトする。 このとき、DBのUNIQUE制約違反(DataIntegrityViolationException例外)が発生する場合があることに注意する。
ここでは、ユーザ名のバリデーションに@Pattern
を用い、正規表現で文字種を制限している。
@Pattern
のデフォルトのエラーメッセージはユーザに不親切なため、messageパラメータで適切なエラーメッセージを指定しておくとよい。
また、メールアドレスのバリデーションにはHibernate Validatorの@Email
が使える。
最後に、ユーザ登録ページのテンプレートファイルを作成する。
- src/main/resources/templates/signup.html
<!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="utf-8" /> <title>Sign up page</title> </head> <body> <h1>Sign up page</h1> <form action="#" th:action="@{/signup}" th:object="${signupForm}" method="post"> <p th:if="${signupError}"><em>Username or mail address is already used.</em></p> <p><label for="username">Username</label>: <input type="text" id="username" name="username" autofocus="autofocus" /> <em th:if="${#fields.hasErrors('username')}" th:errors="*{username}">Username Error</em></p> <p><label for="password">Password</label>: <input type="password" id="password" name="password" /> <em th:if="${#fields.hasErrors('password')}" th:errors="*{password}">Password Error</em></p> <p><label for="mailAddress">Mail address</label>: <input type="email" id="mailAddress" name="mailAddress" /> <em th:if="${#fields.hasErrors('mailAddress')}" th:errors="*{mailAddress}">Mail Address Error</em></p> <p><input type="submit" value="Sign up" /></p> </form> <p><a th:href="@{/login}">Login</a></p> <p><a th:href="@{/}">Back to index</a></p> </body> </html>
/signup
にアクセスしてユーザ登録すると、作成したユーザでログインできていることが確認できる。
また、既存のユーザ名で新規登録しようとすると正しくエラーが表示されること、ハードコードした管理者アカウントでログインするとロールにROLE_ADMIN
がついていることが確認できる。
DB認証以外の認証方式
Spring Securityは、ここで利用したインメモリ認証、DB認証の他に、LDAP、CAS、JAAS(Java Authentication and Authorization Service)による認証方式をサポートしている。 また、別プロジェクトとしてOAuth、SAML、Kerberosを利用するライブラリも開発されている。
注意事項
この記事はライブラリの使い方の説明を主目的としているため、認証機能の実装は簡略化されている。 現実には、メール送信によるメールアドレスの到達性確認、連続ログイン失敗時におけるアカウントロックやディレイ、パスワードリセットやその承認など、要件に応じてさまざまな実装が必要となる。
また、Spring Securityはセキュリティ機能の実装を支援するライブラリであるが、これを利用したアプリケーションそのものがセキュアであるかは別の問題である。 権限によるアクセス制御に不備があれば、直接アクセスすることで権限を持たないユーザが権限を要する情報を閲覧したり、操作を実行できたりしてしまう。 あるいは、セッションを用いた画面遷移のチェックに不備があれば、直接アクセスすることで途中の入力とそのチェックが飛ばされてしまう。 テンプレートエンジンの不適切な使用によるXSS、JavaScript中の不適切な実装によるDOM-based XSSもある。 したがってライブラリの有無によらず、設計・実装を適宜確認することと、異常系や考えうる攻撃に対して安全であるかのテストが必要である。
StrutsにおけるOGNLのように、SpringにもSpring Expression Language(SpEL)と呼ばれる式言語がある。
SpELはクラスの参照やプロパティへのアクセス、メソッド呼び出しが可能であり、外部からの入力を含む式を実行するとEL Injection攻撃が成立してしまう。
SpEL式が実行される箇所としてはコード中のSpelExpressionParser#parseExpression()
の他に、JSPの<spring:message code="" />
や<spring:eval expression="" />
が知られている。
Spring Framework本体や依存ライブラリ、JREの脆弱性についても注意が必要である。 Springを含むPivotal製ソフトウェアの脆弱性情報は次のページで公開されており、ここ一年においてもSpring関係の脆弱性がいくつか報告されている。
多くの場合ライブラリのパッチアップデートが根本的な対策となるため、インターネットに公開するなどリスクが高い場合はパッチアップデートを適用できる状態にしておくのが望ましい。
関連リンク
- Spring BootとSpring Securityでユーザ認証(インメモリ&独自のユーザテーブル) - くらげになりたい。
- java - Spring authentication with bcrypt - Stack Overflow
- Effective Java Item 32: Use EnumSet instead of bit fields – Dhruba Bandopadhyay
- java - How to persist an EnumSet (using two database tables)? - Stack Overflow
- hibernate - Difference between @OneToMany and @ElementCollection? - Stack Overflow
- Spring+Hibernate連携時の主なExceptionまとめ - yomamaのdev日記 (from jemomo)
- Hibernate 3の例外とSpringの例外の対応関係 - ただのSEの雑記帳
- 勉強メモ/Spring Framework関連での脆弱性報告の傾向 - Qiita
BCTF 2017 供養(Writeup)
BCTF 2017に参加。767ptで62位。
Checkin (Misc 69)
スコアサーバに表示されているトークンを送る。
$ nc 202.112.51.247 6666 Connection UUID:[redacted] Token:[redacted] bctf{N0_PWN_N0_FUN}
monkey (Pwn 327)
Spidermonkeyのjsshellが動いている。 helpを見るとos.systemがあり、特に制限なく使うことができた。
$ nc 202.112.51.248 2333 js> help() (snip) os - interface object os.getenv os.getpid os.system os.spawn os.kill os.waitpid os.file os.path (snip) js> os.system("ls") os.system("ls") bin boot dev etc home initrd.img initrd.img.old lib lib32 lib64 log lost+found media mnt opt proc root run sbin snap srv sys tmp usr var vmlinuz vmlinuz.old js> os.system("ls /home") os.system("ls /home") js js> os.system("ls /home/js") os.system("ls /home/js") bin dev flag js lib lib32 lib64 js> os.system("cat /home/js/flag") os.system("cat /home/js/flag") bctf{319c1b47f786c7b99a757da74fd38408}
Hulk (Crypto 869)
ブロック長128 bitのCBCモード暗号。 2回暗号化でき、1回目の平文にはフラグが連結される。 また、2回目のIVは1回目の暗号文の最後のブロックになっている。
$ nc 202.112.51.217 9999 Give me the first hex vaule to encrypt: 0x41414141 plaintext: 0x41414141|flag ciphertext: 0x78d67c1f9b25f8a1320bf84c1e037f2cdeeb26184517dc48585c03e815f18c760df6094ae64c620f55be51716eb04740 Give me the second hex vaule to encrypt: 0x41414141 plaintext: 0x41414141 ciphertext: 0xc99784c01a45051c43f42e1f8b981bf6 $ python Python 2.7.12 (default, Nov 19 2016, 06:48:10) [GCC 5.4.0 20160609] on linux2 Type "help", "copyright", "credits" or "license" for more information. >>> len('c99784c01a45051c43f42e1f8b981bf6')*4 128
1回目に与える文字数を変えてみると、10文字のとき最後のブロックが増える。 このとき、最後のブロックがpaddingのみであると仮定し、2回目の平文を調整すると実際に暗号化後のブロックが一致する。
from minipwn import * s = socket.create_connection(('202.112.51.217', 9999)) print recvuntil(s, '0x') plain1 = '00' * 10 sendline(s, plain1) m = expect(s, r'ciphertext: 0x(\w+)\n') cipher1 = m.group(1) cipher1_blocks = re.findall(r'(\w{32})', cipher1) print cipher1_blocks print recvuntil(s, '0x') test = '\x10'*16 plain2 = xor(test, cipher1_blocks[-2].decode('hex')) plain2 = xor(plain2, cipher1_blocks[-1].decode('hex')) plain2 = plain2.encode('hex') sendline(s, plain2) m = expect(s, r'ciphertext: 0x(\w+)\n') cipher2 = m.group(1) cipher2_blocks = re.findall(r'(\w{32})', cipher2) print cipher2_blocks interact(s)
$ python test.py Give me the first hex vaule to encrypt: 0x ['ff621ece7133119b0ce0be64b4be3313', '8c920847fa1f92f136d12c3c239adaee', 'e5488568e852bd3cced52303c75b3eef', '036e930a2d9c9f176e661cad603b41de'] Give me the second hex vaule to encrypt: 0x ['036e930a2d9c9f176e661cad603b41de', '516bc85ad71223c1281a18737270b4f9'] *** Connection closed by remote host ***
よって、1回目の入力に追加されるフラグは 3*16 - 10 = 38
文字であることがわかる。
あとは、padding oracle attackの要領で、後ろから1文字ずつ特定していくことができる。
from minipwn import * def attack(n, test): s = socket.create_connection(('202.112.51.217', 9999)) recvuntil(s, '0x') plain1 = '00' * (10+n) sendline(s, plain1) m = expect(s, r'ciphertext: 0x(\w+)\n') cipher1 = m.group(1) cipher1_blocks = re.findall(r'(\w{32})', cipher1) recvuntil(s, '0x') test += chr(16-len(test)%16)*(16-len(test)%16) k = len(test)/16 plain2 = xor(test, cipher1_blocks[-k-1].decode('hex')) plain2 = xor(plain2, cipher1_blocks[-1].decode('hex')) plain2 = plain2.encode('hex') sendline(s, plain2) m = expect(s, r'ciphertext: 0x(\w+)\n') cipher2 = m.group(1) cipher2_blocks = re.findall(r'(\w{32})', cipher2) s.close() return cipher1_blocks[-k] == cipher2_blocks[0] chars = '{}0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!"#$%&\'()*+,-./:;<=>?@[\\]^_`|~ ' s = '' while len(s) < 38: for c in chars: s2 = c + s if attack(len(s2), s2): s = s2 print s break
$ python test.py } 5} 05} 905} (snip) ctf{3c1fffb76f147d420f984ac651505905} bctf{3c1fffb76f147d420f984ac651505905}
所感
他に解きたかった問題は以下。
- Baby Sqli (Web 161)
- babyuse (Pwn 273)
- foolme (Misc 384)