mod_auth_openidcのOIDCCacheType redisを試す

はじめに

mod_auth_openidcはキャッシュ先としては以下の中から選ぶことができます (https://github.com/zmartzone/mod_auth_openidc/wiki/Caching) 。

  • shared memory (default)

  • memcache

  • Redis

  • file storage

shared memoryだと不便なケースがあるのでRedisを利用してみます。
以下のリポジトリにて動作確認を行いました。 github.com

設定

設定方法についてはmod_auth_openidcのwikiに記載されているので、こちらを参考に進めていきます。

github.com

httpd.confの以下のディレクティブに設定をを追加します。

OIDCCacheType                 redis
OIDCRedisCacheServer          redis:6379
OIDCRedisCachePassword        foobar

redisを利用する際にはOIDCCachTyperedisにします。
OIDCRedisCacheServerにredisサーバーのホスト名とポート([:])を設定します。
もしパスワードを設定していればOIDCRedisCachePasswordにパスワードを設定します。

動作確認

データがredisに格納されているか確認してみる。
事前にredisに接続しやすいようにdocker-compose.ymlのportを編集して外部にportを公開しておくと楽です。

  redis:
    image: "redis:alpine"
    ports:
      - "6379:6379"
    command: redis-server --appendonly yes --requirepass foobar 

redis-cliで対象ホストにアクセスして中身を見てみる。パスワードを設定している場合は事前にAUTHコマンドを実行しておく。

$ redis-cli
127.0.0.1:6379> AUTH foobar(パスワード)
OK
127.0.0.1:6379> keys *
1) "n:DFivOxw8xZrleyjsLU7-4Sdoa9v4E-PRQbsfbCFLQqg"
2) "j:sulYTRjjayqQ1YZRie_5FM0raz_Zygxbp4rPLIRWUxc"
3) "n:Oj4W_istnIxmeGW1Gq8gmNGYGP9KQhCL7pzucOX5anE"
4) "p:CR-pEhlBO5nQ-wW4rBt_UlZyxN8d0ydQrvFKDkx_UVU"
5) "s:a3-NEm6xqSmiLR-KSJpn3ed97FsJDvW1YDW0SXs8zVg"

確認できました。何か入っているので問題なさそうです。

おわりに

今回はmod_auth_openidcのキャッシュ先としてRedisを使ってみました。
意外と設定も少なくできたのでよかったです。

OAuth2.0 Clientでの各種トークン管理について調べてみた (Web App)

はじめに

 これはポエムです。
 mod_auth_openidcの実装からClientでの各種トークンの管理について調べて学びを得ます。

github.com

 対象としてmod_auth_openidcを選んだ理由については、既存のアプリケーションの実装に依存せずに諸々を良い感じに処理してくれる印象があったからです。
 また、設定が豊富で様々な方法を提示されており、トークン管理について調べる題材として良いと思ったからです。  今回はConfidential Clientでの話になります。Public Clientではまたトークンの保管方法は違うはずなのでご了承ください。

動作確認

 動作環境については以下のリポジトリに用意してあります。 github.com

 ここでは上記の動作確認方法について説明をしておきます。特に動作確認する必要が無ければ読み飛ばしてください。
 まず、/ets/hosts/に以下のように追記しておきます。

127.0.0.1    localhost keycloak

 次にリポジトリをクローンして諸々を立ち上げます。

$ git clone https://github.com/kg0r0/keycloak-oidc-compose.git
$ cd keycloak-oidc-compose
$ docker-compose build
$ docker-compose up -d

 諸々立ち上がったら、http://localhost/privateにブラウザからアクセスします。
 すると、ログイン画面が表示されるのでUsernameにuser、Passwordにpasswordをそれぞれ入力します。 f:id:kent056-n:20190701093316p:plain  正しく入力できるとログインが成功し、アプリケーションにヘッダの情報が表示されます。
f:id:kent056-n:20190701093344p:plain

 今回のサンプルアプリケーションでは何が起きているか把握するためにアプリケーションでAccess Tokenなどを表示しています。
 本来Access TokenはClientが保持する秘密の値であるため、今回のようにアプリケーションで表示しないでください。

トークンの管理について

 mod_auth_openidcで各トークンがどのように扱われるのか調べます。
 最初に各トークンの扱いを調べる前にmod_auth_openidcが管理する情報を確認しておきます。
 mod_auth_openidcは以下の情報をキャッシュするようです。

  • authenticated user session state
  • nonce values from authorization requests (to prevent replay attacks)
  • validated OAuth 2.0 access tokens
  • JWK sets that have been retrieved from jwk_uri's (to validate ID tokens)
  • resolved OP metadata when using OIDCProviderMetadataUrl
  • JWT ID claims (jti) when using OP-init-SSO draft-bradley-oauth-jwt-encoded-state
  • temporary state associated with Request URI's

 また、キャッシュ先としては以下の中から選ぶことができるようです。

  • shared memory (default)
  • memcache
  • Redis
  • file storage

https://github.com/zmartzone/mod_auth_openidc/wiki/Caching

ID Token

 ID Tokenを検証して、Cookieをセットするまでの処理はmod_auth_openidcがおこなっており、基本的にアプリケーションがID Tokenを直接扱う必要はなさそうです(Hybrid Flowの時は要確認)。
 アプリケーション側から見てID Tokenが隠蔽されていた場合、認証後にユーザー情報を取得してページを表示したい場合に不便にならないかという懸念がありますが、それは問題なさそうです。
 先ほどの動作確認結果からOIDC_CLAIMというヘッダでID Tokenの情報がアプリケーションに渡ってきているのが確認できます。
 また、後述しますがOIDC_access_tokenヘッダでAccess Tokenもアプリケーションに渡されており、User Infoエンドポイントを叩くことも可能です。
 モジュール自身がUser Infoエンドポイントを叩いてextra claimsについても解決してくれるようなことも記載されています。

The access_token that mod_auth_openidc receives from the OP will be used by the module itself against the user_info endpoint of the OP (if configured) to resolve extra claims about the user. Besides that an application may be interested in the access_token to use it against other OAuth 2.0 protected APIs, typically when additional scopes have been requested in addition to the OpenID Connect scopes (using OIDCScope).

 検証時に認証および認可もする場合、以下の2つの方法が用意されています。

  • Use the functions that mod_auth_openidc provides to authorize users based on the claims that have been provided for that user by the OpenID Connect provider.

  • Use another Apache module that performs the authorization based on the user identity provided by mod_auth_openidc.

https://github.com/zmartzone/mod_auth_openidc/wiki/Authorization

 認証後はCookieがセットされることが記載されています。mod_auth_openidcではID Tokenの管理というよりもユーザーのセッション管理をおこなっているようです。

The "session" cookie is created after the user returns from the OpenID Connect provider with a successful authentication response (note that the state cookie is deleted at the same time).

https://github.com/zmartzone/mod_auth_openidc/wiki/Cookies#session-cookie

 実際にCookieがセットされていることが確認できます。 f:id:kent056-n:20190701110638p:plain

Access Token

 先程確認したキャッシュの項目にvalidated OAuth 2.0 access tokensがあるので、Access Tokenの管理もmod_auth_openidcがやってくれてそうです。  mod_auth_openidcではAccess TokenはOIDC_access_tokenというヘッダでアプリケーションに渡されるようです。

For that purpose mod_auth_openidc passes the access_token that it receives from the OP to applications in a header named OIDC_access_token.
https://github.com/zmartzone/mod_auth_openidc/wiki/Access-Tokens-and-Refresh-Tokens

 リバースプロキシからアプリケーションへのリクエストに毎回Access Tokenがヘッダにセットされてくるので、要件にもよりますがアプリケーションでAccess Tokenを管理する必要はあまりなさそうです。

Refresh Token

 Refresh Tokenはmod_auth_openidcがユーザーセッションに紐づけて管理してくれるようです。

If a refresh_token is returned by the OP, the module stores the refresh_token in the user session.

 また、Refresh TokenによるAccess Tokenの更新もmod_auth_openidcが担ってくれるようです。

When called on this hook mod_auth_openidc will refresh the access_token using the stored refresh_token as described in the OpenID Connect specification in section 12. Using Refresh Tokens.

https://github.com/zmartzone/mod_auth_openidc/wiki/Access-Tokens-and-Refresh-Tokens

まとめ

今回はリバースプロキシの構成でトークンの管理について見てみました。
どういった認証モジュールを利用するかにもよりますが、今回利用したmod_auth_openidcだとアプリケーション側でトークンを管理する手間はかなり少なそうです。
他の認証モジュールを利用する場合だと、一部担っていない機能などあると思うのでそこは自前で管理する必要がありそうです。
所感としては、アプリケーション側からトークンの検証や管理を隠蔽しつつ、必要な情報はちゃんとアプリケーションに渡しているんだなあという感じです。

【WebAuthn】認証器としてTouch IDが利用されたかどうか判定してみる

はじめに

WebAuthnで認証器としてMacBookのTouch IDが利用されたかどうかRPで判定したい気がしていた。
するとGWに手が滑ってMacBook Air (Retina, 13-inch, 2018)を買ってしまい、手持ちのMacBookが二台になって検証できるようになった。
せっかくなので検証して、その結果をメモしておく。
今回は以下のデモサイトを用いて検証する。 demo.yubico.com

認証器のモデルの判定

認証器のモデルの特定はaaguid (Authenticator Attestation GUID) でおこなう。
上記で紹介したデモサイトでYubicoの製品を使って登録をすると、以下のように製品のモデルが表示される。

  • Security Key f:id:kent056-n:20190620213222p:plain

  • YubiKey 4 Series f:id:kent056-n:20190620213300p:plain

おそらくこの判定もaaguidによりおこなっているのではないかと思う。 (YubiKey 4 Seriesはfido-u2f attestationなのにaaguidから正確に判定できているのか謎)

fido-u2f attestationはaaguidが0x00になるので注意が必要だが、Touch IDはpacked attestation (self attestation) なので問題なさそう。

Touch IDの判定

さっそくデモサイトでTouch IDを利用してみる。
以下は、登録後の画面。先ほどのYubicoの製品を利用した時みたいに製品の情報は表示されない。 f:id:kent056-n:20190620211701p:plain

次に、MacBook Air (Retina, 13-inch, 2018)とMacBook Pro (13-inch, 2016, Four Thunderbolt 3 Ports)それぞれで登録を行い、aaguidを比較してみる。

{
  "success": true,
  "attestationObject": {
    "attStmt": {
      "alg": -7,
      "sig": "MEUCIFzidjErMHcC09YUGplSH05BYu++nNgJ3qWxwopRbneGAiEA5Ouclw1Pflm1vuKnDeMGPRqKiUj3PjUpIIxkRtHd0mk="
    },
    "authData": {
      "credentialData": {
        "aaguid": "rc4AAjW8xgpkiwsl8fBVAw==",
        "credentialId": "AD3ZP3yPHJOtnEhNBbgftElwebHu2+mkJXjQqa5y4knrC13X7Xie/PWNugSr/UXWiKvJ6ZeIXhxmYZMCSMmpLQ/kInELz5/BRVkSZsYUE7Wq+3s5Y6tnpGjUJwCrkvJaYzA=",
        "publicKey": {
          "1": 2,
          "3": -7,
          "-1": 1,
          "-2": "epkLJQNrfh9m9p4nwGo7sK1GMgglkQazvqNIyL1flBA=",
          "-3": "f+rTwucl+o1/rYjLpMrSlx1cDBIiGIQBwa2UjFrA62g="
        }
      },
      "flags": {
        "AT": true,
        "ED": false,
        "UP": true,
        "UV": true,
        "value": 69
      },
      "rpIdHash": "xGzvgq0bVGR3WR0Aiwh1nsPm0uy085R0v+ppaZJdA7c=",
      "signatureCounter": 1561032466
    },
    "fmt": "packed"
  },
  "clientData": {
    "challenge": "sAYJw8ryLy_jyDqX9jADGwj_4-x54aavrc41kfMZ1q0",
    "origin": "https://demo.yubico.com",
    "type": "webauthn.create"
  },
  "device": {
    "name": "Unknown device",
    "type": "unknown"
  }
}
{
  "success": true,
  "attestationObject": {
    "attStmt": {
      "alg": -7,
      "sig": "MEQCICdX4bExE0u8FPdx6rgTXdqFGt9xD+VLpf+7r/NNQ2YCAiBsv9N+jlIsiS/6sOAX+geEUL5H7ThTrroR+NN9cI3itw=="
    },
    "authData": {
      "credentialData": {
        "aaguid": "rc4AAjW8xgpkiwsl8fBVAw==",
        "credentialId": "AEYJafU1xWSslkoR25s3tRzd5cJGqPttejySu76wdORtcM4PjKwJtWKaYjDUw851MfnHgSjXRlV+M8AUTrJrOyvVy8N3/SAZMKfn0tZr5jsKOiUr6oIWq8Px/WPReEybwLU=",
        "publicKey": {
          "1": 2,
          "3": -7,
          "-1": 1,
          "-2": "ldU1EdHtJywMgfm8cYydb4c/oH59o9Mnr2PDBl77vqE=",
          "-3": "PJohvgh2OZbT/c6z+qrv8Jiyu94PIV0RxxgnXuJJdXw="
        }
      },
      "flags": {
        "AT": true,
        "ED": false,
        "UP": true,
        "UV": true,
        "value": 69
      },
      "rpIdHash": "xGzvgq0bVGR3WR0Aiwh1nsPm0uy085R0v+ppaZJdA7c=",
      "signatureCounter": 1561015243
    },
    "fmt": "packed"
  },
  "clientData": {
    "challenge": "HNjfDLl8ASZpI8s-VB7SE7nqew1ddQo89DnLVyb0klc",
    "origin": "https://demo.yubico.com",
    "type": "webauthn.create"
  },
  "device": {
    "name": "Unknown device",
    "type": "unknown"
  }
}

MacBook Air (Retina, 13-inch, 2018)とMacBook Pro (13-inch, 2016, Four Thunderbolt 3 Ports)でaaguidは同じだった。
おそらく、ChromeからMacBookのTouch IDを認証器として呼び出した場合のaaguidはrc4AAjW8xgpkiwsl8fBVAw==になるようだ。

おわりに

以下まとめです。

  • 確証はないが認証器としてTouch IDが利用されたかどうかは判定できそう

  • MacBook AirMacBook Proでaaguidに変化はない

  • Chrome以外のブラウザから認証器としてTouch IDを呼び出した場合にどうなるかは不明

おまけ

別の人がMac Book Proを持っていたので念には念をいれて一応確認してみた。

{
  "success": true,
  "attestationObject": {
    "attStmt": {
      "alg": -7,
      "sig": "MEUCIQDw3lx6tfEJWDH2+VDCrZE2giDbI2z8Skx4YNtzFGA72QIgR6ucKwCfWEShXQAHPqPwXNv5cNsjBegsGh9YO3C2FUI="
    },
    "authData": {
      "credentialData": {
        "aaguid": "rc4AAjW8xgpkiwsl8fBVAw==",
        "credentialId": "AB1ZRA8GxEx5Q37k5csN9glkPrmZXx/pG/zRWaD5Ccz5okPBScxmCUN+mjjo9Lu0SKQXvTTDAQHgnu+weefXzRtbqWcc5E9GfjuPMcQPJLrgN282ffuwgBcHwdgd9qzvxXw=",
        "publicKey": {
          "1": 2,
          "3": -7,
          "-1": 1,
          "-2": "/enNu+rmfc1g/OO6yq5u+ygYcN0N2ew/TiCpxh85bOk=",
          "-3": "hIyjyekzludhdfhWjhuPWsm/wxwSYALsxqNlyGpy/cw="
        }
      },
      "flags": {
        "AT": true,
        "ED": false,
        "UP": true,
        "UV": true,
        "value": 69
      },
      "rpIdHash": "xGzvgq0bVGR3WR0Aiwh1nsPm0uy085R0v+ppaZJdA7c=",
      "signatureCounter": 1561015290
    },
    "fmt": "packed"
  },
  "clientData": {
    "challenge": "_-iFeOfhI4ZNFgrx3uWQSdWLMvHJf_SmA38Xu_ybzOU",
    "origin": "https://demo.yubico.com",
    "type": "webauthn.create"
  },
  "device": {
    "name": "Unknown device",
    "type": "unknown"
  }
}

やっぱりrc4AAjW8xgpkiwsl8fBVAw==だった。

SECCON Beginners CTF 2019 writeup

はじめに

yharimaで参加し、11th / 666 placeでした。(自分は実質何もやってない)
今回はbinary強い人とweb強い人がチームにいたのでcryptoに専念してみた。(2問しか解けなかったので死にたい...) f:id:kent056-n:20190526175444p:plain

他のメンバーのブログはこちら。 yuta1024.hateblo.jp

lai.hateblo.jp

So Tired

問題文。

最強の暗号を作りました。 暗号よくわからないけどきっと大丈夫!

File: so_tired.tar.gz

Fileを展開するとBase64エンコードされた文字列(めっちゃ長い)が書かれたテキストファイルが出てくる。
とりあえずデコードしてファイルとして保存してみる。

$ cat encrypted.txt | base64 -D > outdata

fileコマンドを実行してみると、zlib形式であることがわかる。

$ file outdata
outdata: zlib compressed data

zlibを展開すると、またBase64エンコードされた文字列(めっちゃ長い)が書かれたテキストファイルが出てくる。
以降、zlib -> Base64エンコード文字列 -> zlib -> ... の繰り返しになり、クッソめんどくさいので問題文とも一致する。
以下、solverです。適当に"ctf4b"が含まれている箇所でbreakするようにしました。

import zlib
import base64

i = 1
while True:
    data = open("outdata" + str(i - 1), "r")
    s = zlib.decompress(data.read())
    data.close()
    if ("ctf4b" in s) :
        print s
        break

    if ("ctf4b" in base64.b64decode(s)) :
        print base64.b64decode(s)
        break

    with open("outdata" + str(i), mode='w') as f:
        f.write(base64.b64decode(s))
    i += 1

上記のsolverを実行すると500個くらいファイルがブチまけられて最悪な気持ちになるけどフラグがでてきます。

ctf4b{very_l0ng_l0ng_BASE64_3nc0ding}

Party

以下、問題文。

Let's 暗号パーティ

File: party.tar.gz

tarを展開すると、encrypt.pyとencryptedというファイルが出てくる。それぞれのファイルの中身は以下の通り。
encrypt.py

from flag import FLAG
from Crypto.Util.number import bytes_to_long, getRandomInteger, getPrime


def f(x, coeff):
    y = 0
    for i in range(len(coeff)):
        y += coeff[i] * pow(x, i) # i = 0, 1, 2
    return y


N = 512
M = 3
secret = bytes_to_long(FLAG)
assert(secret < 2**N)

coeff = [secret] + [getRandomInteger(N) for i in range(M-1)]
party = [getRandomInteger(N) for i in range(M)]

val = map(lambda x: f(x, coeff), party)
output = list(zip(party, val))
print(output)

encrypted

[(5100090496682565208825623434336918311864447624450952089752237720911276820495717484390023008022927770468262348522176083674815520433075299744011857887705787, 222638290427721156440609599834544835128160823091076225790070665084076715023297095195684276322931921148857141465170916344422315100980924624012693522150607074944043048564215929798729234427365374901697953272928546220688006218875942373216634654077464666167179276898397564097622636986101121187280281132230947805911792158826522348799847505076755936308255744454313483999276893076685632006604872057110505842966189961880510223366337320981324768295629831215770023881406933), (3084167692493508694370768656017593556897608397019882419874114526720613431299295063010916541874875224502547262257703456540809557381959085686435851695644473, 81417930808196073362113286771400172654343924897160732604367319504584434535742174505598230276807701733034198071146409460616109362911964089058325415946974601249986915787912876210507003930105868259455525880086344632637548921395439909280293255987594999511137797363950241518786018566983048842381134109258365351677883243296407495683472736151029476826049882308535335861496696382332499282956993259186298172080816198388461095039401628146034873832017491510944472269823075), (6308915880693983347537927034524726131444757600419531883747894372607630008404089949147423643207810234587371577335307857430456574490695233644960831655305379, 340685435384242111115333109687836854530859658515630412783515558593040637299676541210584027783029893125205091269452871160681117842281189602329407745329377925190556698633612278160369887385384944667644544397208574141409261779557109115742154052888418348808295172970976981851274238712282570481976858098814974211286989340942877781878912310809143844879640698027153722820609760752132963102408740130995110184113587954553302086618746425020532522148193032252721003579780125)]

encryptedの中身はpartyとvalであることがわかる。また、FLAG(secret)は coeff[0]に入っていることがわかる。つまり、partyとvalからcoeffを導出できれば良さそうということに気づく。
そして、よくみると方程式を立てることができ、導出可能であることがわかる。以下、solver。

from sympy import *

party0 = 5100090496682565208825623434336918311864447624450952089752237720911276820495717484390023008022927770468262348522176083674815520433075299744011857887705787
val0 = 222638290427721156440609599834544835128160823091076225790070665084076715023297095195684276322931921148857141465170916344422315100980924624012693522150607074944043048564215929798729234427365374901697953272928546220688006218875942373216634654077464666167179276898397564097622636986101121187280281132230947805911792158826522348799847505076755936308255744454313483999276893076685632006604872057110505842966189961880510223366337320981324768295629831215770023881406933
party1 = 3084167692493508694370768656017593556897608397019882419874114526720613431299295063010916541874875224502547262257703456540809557381959085686435851695644473
val1 = 81417930808196073362113286771400172654343924897160732604367319504584434535742174505598230276807701733034198071146409460616109362911964089058325415946974601249986915787912876210507003930105868259455525880086344632637548921395439909280293255987594999511137797363950241518786018566983048842381134109258365351677883243296407495683472736151029476826049882308535335861496696382332499282956993259186298172080816198388461095039401628146034873832017491510944472269823075
party2 = 6308915880693983347537927034524726131444757600419531883747894372607630008404089949147423643207810234587371577335307857430456574490695233644960831655305379
val2 = 340685435384242111115333109687836854530859658515630412783515558593040637299676541210584027783029893125205091269452871160681117842281189602329407745329377925190556698633612278160369887385384944667644544397208574141409261779557109115742154052888418348808295172970976981851274238712282570481976858098814974211286989340942877781878912310809143844879640698027153722820609760752132963102408740130995110184113587954553302086618746425020532522148193032252721003579780125
# coeff[0] = x, coeff[1] = y, coeff[2] = z
x, y, z = symbols('x y z')
ans = solve([x + (y * party0) + (z * pow(party0, 2)) - val0, x + (y * party1) + (z * pow(party1, 2)) - val1, x + (y * party2) + (z * pow(party2, 2)) - val2])
print(ans)

出力はこんな感じ。

{x: 175721217420600153444809007773872697631803507409137493048703574941320093728, y: 6759741750199108721817212574266152064959437506612887142001761070682826541920627672362291016337903640265385249474489124882116454124173716091800442011015857, z: 8559415203809303629563171044315478022492879973152936590413420646926860552595649298493153041683835412421908115002277197166850496088216040975415228249635834}

x = coeff[0] = secret なので、xを文字列にして終了。

ctf4b{just_d0ing_sh4mir}

おわりに

Go RSAとBit Flip解けなかった...死にてぇ...
ちょっと捻られると解けなくなるので良くないな...

Auth0の設定してみた際のメモ

はじめに

Auth0を利用してみた際の手順を忘れないようにメモしておきます。

登録

以下のリンク先の無償トライアルから登録して使ってみる。 auth0.com

今回はSign UpせずにGithubのアカウントと連携させてみる。 f:id:kent056-n:20190506193735p:plain

GitHubのアイコンをクリックすると、Auth0がGithubアカウントのPersonal user data (read-only)を要求してくるので認可する。 f:id:kent056-n:20190506194039p:plain

Welcome to Auth0というページでTENANT DOMAINとREGIONが選択できるが今回は両方ともデフォルトのまま進める。
Tenantは1つのサービスを表し、複数のApplicationが紐づくものらしい。 f:id:kent056-n:20190506194449p:plain

最後にACCOUNT TYPEをPersonal、ROLEをDeveloper、MAIN CHALLENGEをOtherにしてCREATE ACCOUNTを押下する。 f:id:kent056-n:20190506194733p:plain

これで登録は終わりのようです。

Application

登録後は[Applications] > [CREATE APPLICATION]の順に押下してApplicationの設定を行います。 f:id:kent056-n:20190506204207p:plain

NameとApplicationのTypeを選択してCREATEを押下すると、Applicationの詳細情報ページに遷移します。 f:id:kent056-n:20190506204516p:plain

SettingsタブからName、Domain、Client ID、Client Secretなど開発時に必要な情報が閲覧できます。

おわりに

ここまでは意外と簡単にできました。
Auth0にはたくさんの機能があるようなので利用してみたいです。

INS'hAck 2019 writeup

はじめに

あんまりやってないです。最終417位だったぽい。 f:id:kent056-n:20190506021947p:plain

gflag - MIC 50

以下の内容のファイルが渡される。 https://gist.github.com/kg0r0/0799da01e22f1210c345026344af3585

適当に検索してみると、どうやらG-Codeというコードらしいことが判明。

reprap.org

さらに調べてみると、GCode viewerを発見。

ncviewer.com

先ほどのコードをペーストしてみると、FLAGが現れた。

f:id:kent056-n:20190505223625p:plain

Telegram - MIC 50

Telegram channelのリンクを踏むだけ f:id:kent056-n:20190505223812p:plain

Yet Another RSA Challenge - Part 1 - CRY 50

解けなかったし、なぜ解けないのか最後までわからなかった。
以下問題文。

You can find a harder version of this challenge in the Programming category.⏎

以下の2つのファイルが渡される。

import subprocess
p = subprocess.check_output('openssl prime -generate -bits 2048 -hex')
q = subprocess.check_output('openssl prime -generate -bits 2048 -hex')
flag = int('INSA{REDACTED}'.encode('hex'), 16)

N = int(p,16) * int(q,16)
print N
print '0x'+p.replace('9F','FC')
print pow(flag,65537,N)
719579745653303119025873098043848913976880838286635817351790189702008424828505522253331968992725441130409959387942238566082746772468987336980704680915524591881919460709921709513741059003955050088052599067720107149755856317364317707629467090624585752920523062378696431510814381603360130752588995217840721808871896469275562085215852034302374902524921137398710508865248881286824902780186249148613287250056380811479959269915786545911048030947364841177976623684660771594747297272818410589981294227084173316280447729440036251406684111603371364957690353449585185893322538541593242187738587675489180722498945337715511212885934126635221601469699184812336984707723198731876940991485904637481371763302337637617744175461566445514603405016576604569057507997291470369704260553992902776099599438704680775883984720946337235834374667842758010444010254965664863296455406931885650448386682827401907759661117637294838753325610213809162253020362015045242003388829769019579522792182295457962911430276020610658073659629786668639126004851910536565721128484604554703970965744790413684836096724064390486888113608024265771815004188203124405817878645103282802994701531113849607969243815078720289912255827700390198089699808626116357304202660642601149742427766381
0xDCC5A0BD3A1FC0BEB0DA1C2E8CF6B474481B7C12849B76E03C4C946724DB577D2825D6AA193DB559BC9DBABE1DDE8B5E7805E48749EF002F622F7CDBD7853B200E2A027E87E331AFCFD066ED9900F1E5F5E5196A451A6F9E329EB889D773F08E5FBF45AACB818FD186DD74626180294DCC31805A88D1B71DE5BFEF3ED01F12678D906A833A78EDCE9BDAF22BBE45C0BFB7A82AFE42C1C3B8581C83BF43DFE31BFD81527E507686956458905CC9A660604552A060109DC81D01F229A264AB67C6D7168721AB36DE769CEAFB97F238050193EC942078DDF5329A387F46253A4411A9C8BB71F9AEB11AC9623E41C14FCD2739D76E69283E57DDB11FC531B4611EE3
596380963583874022971492302071822444225514552231574984926542429117396590795270181084030717066220888052607057994262255729890598322976783889090993129161030148064314476199052180347747135088933481343974996843632511300255010825580875930722684714290535684951679115573751200980708359500292172387447570080875531002842462002727646367063816531958020271149645805755077133231395881833164790825731218786554806777097126212126561056170733032553159740167058242065879953688453169613384659653035659118823444582576657499974059388261153064772228570460351169216103620379299362366574826080703907036316546232196313193923841110510170689800892941998845140534954264505413254429240789223724066502818922164419890197058252325607667959185100118251170368909192832882776642565026481260424714348087206462283972676596101498123547647078981435969530082351104111747783346230914935599764345176602456069568419879060577771404946743580809330315332836749661503035076868102720709045692483171306425207758972682717326821412843569770615848397477633761506670219845039890098105484693890695897858251238713238301401843678654564558196040100908796513657968507381392735855990706254646471937809011610992016368630851454275478216664521360246605400986428230407975530880206404171034278692756

こんなのreplaceしてpを求めて、Nからqを導出して復号すれば終わりだと思って以下のようなコードを書いた。

from Crypto.Util.number import inverse
from sympy import isprime
import binascii

out = '0xDCC5A0BD3A1FC0BEB0DA1C2E8CF6B474481B7C12849B76E03C4C946724DB577D2825D6AA193DB559BC9DBABE1DDE8B5E7805E48749EF002F622F7CDBD7853B200E2A027E87E331AFCFD066ED9900F1E5F5E5196A451A6F9E329EB889D773F08E5FBF45AACB818FD186DD74626180294DCC31805A88D1B71DE5BFEF3ED01F12678D906A833A78EDCE9BDAF22BBE45C0BFB7A82AFE42C1C3B8581C83BF43DFE31BFD81527E507686956458905CC9A660604552A060109DC81D01F229A264AB67C6D7168721AB36DE769CEAFB97F238050193EC942078DDF5329A387F46253A4411A9C8BB71F9AEB11AC9623E41C14FCD2739D76E69283E57DDB11FC531B4611EE3'
N = 719579745653303119025873098043848913976880838286635817351790189702008424828505522253331968992725441130409959387942238566082746772468987336980704680915524591881919460709921709513741059003955050088052599067720107149755856317364317707629467090624585752920523062378696431510814381603360130752588995217840721808871896469275562085215852034302374902524921137398710508865248881286824902780186249148613287250056380811479959269915786545911048030947364841177976623684660771594747297272818410589981294227084173316280447729440036251406684111603371364957690353449585185893322538541593242187738587675489180722498945337715511212885934126635221601469699184812336984707723198731876940991485904637481371763302337637617744175461566445514603405016576604569057507997291470369704260553992902776099599438704680775883984720946337235834374667842758010444010254965664863296455406931885650448386682827401907759661117637294838753325610213809162253020362015045242003388829769019579522792182295457962911430276020610658073659629786668639126004851910536565721128484604554703970965744790413684836096724064390486888113608024265771815004188203124405817878645103282802994701531113849607969243815078720289912255827700390198089699808626116357304202660642601149742427766381
p = '0xDCC5A0BD3A1FC0BEB0DA1C2E8CF6B474481B7C12849B76E03C4C946724DB577D2825D6AA193DB559BC9DBABE1DDE8B5E7805E48749EF002F622F7CDBD7853B200E2A027E87E331AFCFD066ED9900F1E5F5E5196A451A6F9E329EB889D773F08E5FBF45AACB818FD186DD74626180294DCC31805A88D1B71DE5BFEF3ED01F12678D906A833A78EDCE9BDAF22BBE45C0BFB7A82AFE42C1C3B8581C83BF43DFE31BFD81527E507686956458905CC9A660604552A060109DC81D01F229A264AB67C6D7168721AB36DE769CEAFB97F238050193EC942078DDF5329A387F46253A4411A9C8BB71F9AEB11AC9623E41C14FCD2739D76E69283E57DDB11FC531B4611EE3'.replace('FC', '9F')[2:]
assert '0x'+p.replace('9F','FC') == out, "fail"
p = int(p, 16)
assert isprime(p), "p is not prime"
q = N / p
e = 65537
assert (p * q) == N, "unexpected value"
phi = (p - 1) * (q - 1)
d = inverse(e, phi)
print(binascii.unhexlify(hex(pow(c, d, N))[2:]).decode())⏎

なぜか"p is not prime"になって詰んだ。
writeup読んでみたけど情報量少なすぎて何もわからない。 追記 : 理解した。"FC"を全て"9F"に戻すと、もともとFCだった箇所も9Fになってしまうじゃん!

おわりに

今の気持ちです。

f:id:kent056-n:20190506024959g:plain

参考

【 FIDO Conformance Toolsメモ 】Server-ServerAuthenticatorAttestationResponse-Resp-5 Test server processing "packed" FULL attestation

はじめに

Server-ServerAuthenticatorAttestationResponse-Resp-5 Test server processing "packed" FULL attestationというテスト項目について書きます。
こちらのテスト項目はPacked Attestationの中でもFULL attestationというattestation statementのx5cというフィールドに証明書が格納されているパターンのテストになります。 Packed Attestationの検証については以下の記事に詳しく記載されています。 medium.com

P-1~2

  • P-1 Send a valid ServerAuthenticatorAttestationResponse with FULL "packed" attestation, and check that server succeeds
    FULL Attestationの検証については上記で紹介したブログに詳細に記載されていますのでそちらをご覧ください。(気が向いたら書きます)
    FULL AttestationではAuthenticatorAttestationReponseが証明書を含んだ状態で送られてきます。簡単にやることを説明すると、「証明書の各フィールドの検証」、「証明書から取り出した公開鍵で署名の検証」の2点になります。
...
        const leafCert = base64ToPem(attestationStruct.attStmt.x5c[0].toString('base64'));
        const certInfo = getCertificateInfo(leafCert);

        if (certInfo.subject.OU !== 'Authenticator Attestation')
            throw new Error('Batch certificate OU MUST be set strictly to "Authenticator Attestation"!');

        if (!certInfo.subject.CN)
            throw new Error('Batch certificate CN MUST no be empty!');

        if (!certInfo.subject.O)
            throw new Error('Batch certificate CN MUST no be empty!');

        if (!certInfo.subject.C || certInfo.subject.C.length !== 2)
            throw new Error('Batch certificate C MUST be set to two character ISO 3166 code!');

        if (certInfo.basicConstraintsCA)
            throw new Error('Batch certificate basic constraints CA MUST be false!');

        if (certInfo.version !== 3)
            throw new Error('Batch certificate version MUST be 3(ASN1 2)!');

        response.verified = crypto.createVerify('sha256')
            .update(signatureBaseBuffer)
            .verify(leafCert, signatureBuffer);
...
  • P-2 Send a valid ServerAuthenticatorAttestationResponse with FULL "packed" attestation that contains chain that links to the root certificate in the metadata in it's response, and check that server succeeds
    こちら詳しくわかっていないのですが、送信されてきたリクエストのx5cに複数証明書が入っていたので各証明書の検証を行い、結果を返します。 中間証明書のIssuerとmetadataのRoot証明書のIssuerとSubjectが合うことも加えて検証して結果を返せば良いような気がします。

F-1~14

  • F-1 Send ServerAuthenticatorAttestationResponse with FULL "packed" attestation, with fmt set to an unknown attestation format, and check that server returns an error
    fmtに関するテストのようです。
    以下のように意図していないfmtが送信されていた場合にエラーを返せば良さそうです。
if (attestationStruct.fmt === 'packed')
  • F-2 Send ServerAuthenticatorAttestationResponse with FULL "packed" attestation, and with attStmt.sig contains a signature that can not be verified, and check that server returns an error
    attStmtのsigに関するテストのようです。
    以下のようにsignatureの検証に失敗した場合にエラーを返します。
        if (!signatureIsValid)
            throw new Error('Failed to verify the signature!')
  • F-3 Send ServerAuthenticatorAttestationResponse with FULL "packed" attestation, with attStmt missing "x5c" field, and check that server returns an error
    attStmtのx5cに関するテストのようです。
    以下のようにfmtがpackedでx5cに何も入っていなければエラーを返します。
  if (attestationStruct.attStmt.x5c) 
  • F-4 Send ServerAuthenticatorAttestationResponse with FULL "packed" attestation, with attStmt.x5c is not of type ARRAY, and check that server returns an error
    attStmtのx5cに関するテストのようです。
    x5cは以下のような配列のはずなので、配列でなければエラーを返します。
{
    "fmt": "packed",
    "authData": "9569088f1ecee3232954035dbd10d7cae391305a2751b559bb8fd7cbb229bdd44100000000f8a011f38c0a4d15800617111f9edc7d0040c2c2ebc8e0315cc539e2cd9f09258fcd68d71ce04a3e996837e5077ed8f8feba48bba059c4d5e9a66f367fb63a897f4510e28a792e7e3764c02decb32e725f00a50102032620012158200180f54d2a5fd76d03495c5e9fc922e7a905d5b993e045cef8fd099d293998cc225820774e5319051a8f63a965cc371f1ddfa85b410775a14d8017651861686e91ca97",
    "attStmt": {
        "alg": -7,
        "sig": "30450221008cce8a10ba05af34a39267a1838927f12d11599641c02fc550a7b0216755141902201f78875d2cfb0f97515e497f01e7164035c40a18da0eee96f44d93e7b8975e6c",
        "x5c": [
            "308202af30820197a0030201020204485b3db6300d06092a864886f70d01010b05003021311f301d06035504030c1659756269636f204649444f2050726576696577204341301e170d3138303431323130353731305a170d3138313233313130353731305a306f310b300906035504061302534531123010060355040a0c0959756269636f20414231223020060355040b0c1941757468656e74696361746f72204174746573746174696f6e3128302606035504030c1f59756269636f205532462045452053657269616c20313231333933393132363059301306072a8648ce3d020106082a8648ce3d03010703420004fb2cdd304328c5724a50cce6f60bad7d27a91b59e1e66f297b89c9d43dc2b2c77889b4f0ff9d0228cb946dfce01b19589b67804aac977f28189ccdb32574ca28a36c306a302206092b0601040182c40a020415312e332e362e312e342e312e34313438322e312e363013060b2b0601040182e51c0201010404030204303021060b2b0601040182e51c01010404120410f8a011f38c0a4d15800617111f9edc7d300c0603551d130101ff04023000300d06092a864886f70d01010b0500038201010032f3e4bd58d7422baf499986081f0da93bc6aa1c7211f92853ebf3eb73da693b06de31338e5d02ecf676e95c42bea58f25d3373f77bb2a9d7cb23e118c41d49a4c9ad8f3e2a4ec01777a74a8c41243c31ece208f2d0f6ebc619be184a172f6a9accbf8736d5be298b36bece71e778d0a69aaf994b8636de8faf62fd3ce7f044c322cf7263e3499e6a5b2b02abbad5bd9ece5b0714d73bb9461499c942a5f1dccaf65033b3939d447d9fcc47b0b16d8e901fcec3f8c1bc0c6ac0b5d74c7bb03056917e9981a19b9095ca1f4ab9f027c280f8af9ed1d293cf6cc2f046d9ad662b4a96eb1cacaac5e053e8391477c1f8b6001de653abff2aabb559886917ead3b36"
        ]
    }
}
  • F-5 Send ServerAuthenticatorAttestationResponse with FULL "packed" attestation, with attStmt.x5c is an empty ARRAY, and check that server returns an error
    attStmtのx5cに関するテストのようです。
    x5cの配列の中身が空の場合にエラーを返します。

  • F-6 Send ServerAuthenticatorAttestationResponse with FULL "packed" attestation, with attStmt.x5c contains a leaf certificate that is expired, and check that server returns an error
    attStmtのx5cに関するテストのようです。
    x5cの中に入っている証明書が期限切れであればエラーを返えすと良さそうです。現在の時間と証明書内のNot Afterを比較します。
    この時、証明書の中のNot BeforeとNot AfterはGeneralizedTimeという形式で入っているようなので、Unix TimeStampを比較できるように変換する必要があります。 en.wikipedia.org

今回はldap2dateというパッケージを利用してGeneralizedTimeの変換を行いました。

const ldap2date = require('ldap2date');
const jsrsasign = require('jsrsasign');
...

   const subjectCert = new jsrsasign.X509();
    subjectCert.readCertPEM(certificate);
    const notbefore = ldap2date.parse('20' + subjectCert.getNotBefore()).getTime();
    const notafter = ldap2date.parse('20' + subjectCert.getNotAfter()).getTime();
    const now = new Date().getTime();

    if (notafter < now)
        throw new Error('Leaf certificate is expired!')
  • F-7 Send ServerAuthenticatorAttestationResponse with FULL "packed" attestation, with attStmt.x5c contains a leaf certificate that is not yet started, and check that server returns an error
    attStmtのx5cに関するテストのようです。
    F-6とほぼ同様です。今回はNot Beforeと比較します。
const ldap2date = require('ldap2date');
const jsrsasign = require('jsrsasign');
...

   const subjectCert = new jsrsasign.X509();
    subjectCert.readCertPEM(certificate);
    const notbefore = ldap2date.parse('20' + subjectCert.getNotBefore()).getTime();
    const notafter = ldap2date.parse('20' + subjectCert.getNotAfter()).getTime();
    const now = new Date().getTime();

    if (now < notbefore)
        throw new Error('Leaf certificate is not yet started!')
  • F-8 Send ServerAuthenticatorAttestationResponse with FULL "packed" attestation, with attStmt.x5c contains a leaf certificate algorithm does not equal to the one that is specified in MetadataStatement, and check that server returns an error
    attStmtのx5cに関するテストのようです。
    このテストに関しては記憶にない。アルゴリズムの検証としては、attestation statementのalgのみな気がするのでこれかもしれない。
    もしくは以下のように利用しているライブラリの中でエラーを返しているのかもしれない。要確認。
        const algorithm = subjectCert.getSignatureAlgorithmField();
...
        const Signature = new jsrsasign.crypto.Signature({ alg: algorithm });
  • F-9 Send ServerAuthenticatorAttestationResponse with FULL "packed" attestation, with attStmt.x5c contains certificate chain, that can not be verified, and check that server returns an error
    attStmtのx5cに関するテストのようです。
    こちらのテストは以下のようにx5cに証明書が複数格納されていた場合にも検証結果を元にエラーを返していれば良さそうです。
const validateCertificatePath = (certificates) => {
...
    for (let i = 0; i < certificates.length - 1; i++) {
        const subjectPem = certificates[i];
        const subjectCert = new jsrsasign.X509();
        subjectCert.readCertPEM(subjectPem);

        const issuerPem = certificates[i + 1];
        const issuerCert = new jsrsasign.X509();
        issuerCert.readCertPEM(issuerPem);
... 
        const subjectCertStruct = jsrsasign.ASN1HEX.getTLVbyList(subjectCert.hex, 0, [0]);
        const algorithm = subjectCert.getSignatureAlgorithmField();
        const signatureHex = subjectCert.getSignatureValueHex();

        const Signature = new jsrsasign.crypto.Signature({ alg: algorithm });
        Signature.init(issuerPem);
        Signature.updateHex(subjectCertStruct);

        if (!Signature.verify(signatureHex))
            throw new Error('Failed to validate certificate path!')

    }

    return true
}
  • F-10 Send ServerAuthenticatorAttestationResponse with FULL "packed" attestation, with attStmt.x5c containing full chain, and check that server returns an error
    attStmtのx5cに関するテストのようです。
    Root CAの証明書が含まれている場合にエラーを返さないといけないらしい。 github.com

Root CAの証明書はIssuerとSubjectが同じになっているはずなので以下のような検証を行った。注意点としては、オレオレ証明書もIssuerとSubjectが同じになるはずなので、あくまでx5cのlengthが2以上の場合のみこちらの処理を適用します。

const verifyRootCert = (certificate) => {
    const rootPem = certificate;
    const rootCert = new jsrsasign.X509();
    rootCert.readCertPEM(rootPem);
    if (rootCert.getIssuerString() == rootCert.getSubjectString()) {
        return true;
    }
    return false;
}
  • F-11 Send ServerAuthenticatorAttestationResponse with FULL "packed" attestation, with attStmt.x5c containing full chain, that is not correctly ordered, and check that server returns an error
    attStmtのx5cに関するテストのようです。
    証明書チェーンの検証が正しくできていれば良さそうです。
    具体的には、IssuerとSubjectが正しく合うかどうか検証します。
    for (let i = 0; i < certificates.length - 1; i++) {
        const subjectPem = certificates[i];
        const subjectCert = new jsrsasign.X509();
        subjectCert.readCertPEM(subjectPem);

        const issuerPem = certificates[i + 1];
        const issuerCert = new jsrsasign.X509();
        issuerCert.readCertPEM(issuerPem);
...
        if (subjectCert.getIssuerString() !== issuerCert.getSubjectString())
            throw new Error(`Failed to validate certificate path! Issuers dont match!`)

証明書チェーンの検証項目は以下のエントリにわかりやすく記載されていました。

qiita.com

  • F-12 Send ServerAuthenticatorAttestationResponse with FULL "packed" attestation, with attStmt.x5c contains expired intermediate certificate, and check that server returns an error
    attStmtのx5cに関するテストのようです。
    こちらのテストは中間証明書の期限が切れていないかどうか検証すれば良さそうです。検証方法をF-6と同様です。
const validateCertificatePath = (certificates) => {
...
    for (let i = 0; i < certificates.length - 1; i++) {
        const subjectPem = certificates[i];
        const subjectCert = new jsrsasign.X509();
        subjectCert.readCertPEM(subjectPem);

        const issuerPem = certificates[i + 1];
        const issuerCert = new jsrsasign.X509();
        issuerCert.readCertPEM(issuerPem);
        const notbefore = ldap2date.parse('20' + issuerCert.getNotBefore()).getTime();
        const notafter = ldap2date.parse('20' + issuerCert.getNotAfter()).getTime();
        const now = new Date().getTime();
        if (now < notbefore)
            throw new Error('Leaf certificate is not yet started!')

        if (notafter < now)
            throw new Error('Leaf certificate is expired!')
...

    }

    return true
}
  • F-13 Send ServerAuthenticatorAttestationResponse with FULL "packed" attestation, with signature that can not be verified by the public key extracted from leaf certificate, and check that server returns an error
    signatureの検証に関するテストのようです。
    証明書から取得した公開鍵で検証を行っていればこちらのテストはパスできそうです。
        const leafCert = base64ToPem(attestationStruct.attStmt.x5c[0].toString('base64'));
...
        response.verified = crypto.createVerify('sha256')
            .update(signatureBaseBuffer)
            .verify(leafCert, signatureBuffer);
  • F-14 Send ServerAuthenticatorAttestationResponse with FULL "packed" attestation, with signature that is generated using new credential private key, and not attestation batch private key, and check that server returns an error
    signatureの検証に関するテストのようです。
    検証に利用する公開鍵を正しく保存し、検証に利用していればこちらのテストはパスできそうです。

参考