BSidesSF 2019 CTF|decrypto

#BSidesSF2019CTF

https://ctftime.org/task/7764

(decrypto's signature cookie is important.. the text at the top is wrong. Sorry!) Hack the mainframe! Location -

https://decrypto-6213399b.challenges.bsidessf.net

Web +Crypto。幸いにも問題情報がgithubで公開されてた。丁寧。dockerを建ててアクセスしてみると次のように言われる。

We've logged into the mainframe, but we have a crappy UID! Can you set your UID to 0 instead?

It might interest you to know that the signature cookie is SHA256(8-byte secret || data). But I've already said too much!

Note: the "rack.session" cookie is not part of the challenge!

それから、ページにアクセスすると文字が流れる様になっているんだけど、そちらはこんな感じのJS実装で残念ながら特に見るべきところはない。

  var data = []
  data.push("Welcome to the mainframe!");
  data.push("It looks like you want to access the flag!");
  data.push("Please present user object");
  data.push("...scanning");
  data.push("...scanning");
  data.push("Scanning user object...");

  
    data.push("...your UID value is set to 40");
  
    data.push("...your NAME value is set to baseuser");
  
    data.push("...your SKILLS value is set to n/a");
  

  
    data.push("ERROR: ACCESS DENIED");
    data.push("UID MUST BE '0'");
  

  var current = data.shift();
  function t() {
    return Math.random() * 40 + 40;
  }

  function append() {
    if(current.length == 0) {
      document.getElementById('content').innerHTML += "\n";
      if(data.length == 0) {
        return;
      }

      if(Math.random() < 0.5) {
        current = "...";
      } else {
        current = data.shift();
      }
    }

    c = current[0];
    current = current.substr(1);

    document.getElementById('content').innerHTML += c;
    setTimeout(append, t());
  }

  setTimeout(append, t());

cookieを見てみると、 rack.session, signature, userという3つが定義されている。この内 rack.session は関係ないらしいので考えるのはsignatureとuserのみ。signatureはsha256(salt || data) になっているらしい。実際のデータは↓

uid = 40, username = baseuser, skills = n/a のとき

signature=d84d04a318ae83b1eb82225df54a4c21fd74b6300e47a485c8f26a1b587b4a65

user=32a1cab33801beb72d9bc8a6ecd0ec1aa60a26910003c8803cb3f619fd0e428e6b5c68723502ca8777a689e69c7835c4ce7d48166e8afd8dfb6a809b0260e6d7

uid = 48, username = baseuser, skills = n/aのとき

signature=ce4b1ccb5ee28ad81d38bc89c8526f46c2d3361ab8c908cc233dd529c0e76a5b

user=9b851e2e7cdaf8578b24227cb04a9f688861a6e5cec167fab9d5b1bc3dad4cc035ceb0a279883d3802298cfc8f8291d795cd98a2ce6b017461a08b83e692d402

試しにcookieの値をいろいろいじってみる

  • userを空にしてみた

    • FATAL ERROR: iv must be 16 bytes

  • signatureを空にしてみた

    • FATAL ERROR: Bad signature!

  • signatureを適当なsha256にしてみた

    • FATAL ERROR: Bad signature!

  • userの値をreverseしてみた

    • FATAL ERROR: bad decrypt

  • userの末端1バイトを削った

  • FATAL ERROR: wrong final block length

userは何かしらの暗号化されたデータ、signatureはそのhashみたいな認識で良いんだろうか。IVの情報から1block 16byteのブロック暗号ということはわかる。多分AESだろう。もしCBCモードを使っているならば、IVを適当に変更しても最初のブロック以外はうまく復号されるはず。と言うかnブロック目に適当なxorを入れることでn+1ブロック目の復号結果をいじれたんじゃないかな。

というわけで、二つのuser cookieからIV部分、暗号部分をそれぞれ切り出して混ぜたら FATAL ERROR: Bad signature! と言われた。復号は出来てるっぽいがsignatureがあわないと。

CBCモードでやってるっぽいのでPadding Oracle Attackで暗号文を復元できそう。

こんなかんじで出来た。今回のケースだと最終ブロックは全て0x10だったっぽいんだけど、なぜか最初の1バイトを求められなかったので飛ばした。

import requests
from pwn import *
from binascii import *
import re

URL='http://localhost:3000/'
user=unhexlify(b'9b851e2e7cdaf8578b24227cb04a9f688861a6e5cec167fab9d5b1bc3dad4cc035ceb0a279883d3802298cfc8f8291d795cd98a2ce6b017461a08b83e692d402')[:-16]
signature='ce4b1ccb5ee28ad81d38bc89c8526f46c2d3361ab8c908cc233dd529c0e76a5b'
rack_session='BAh7CEkiD3Nlc3Npb25faWQGOgZFVEkiRWViMjM4MDA2YThjZjNiZDFmNGVk%0AODcyYTMyODgyYmEzZTI2NzM1ZTYxYzQ4NTFmOTBjZjliZDA3MjM2ZDlhODYG%0AOwBGSSILc2VjcmV0BjsARiINkBoXKVoJ9tRJIghrZXkGOwBGIiURwdBj0FCz%0AgSC4dqUax5Zijd8DNdmJCQm%2FGl9CulHjpg%3D%3D%0A--5af2792e0662bbeb14adce303225a06d275f96fb'

m = ''
last_byte = b''
l = 0
while len(user) > 16:
    for b in range(256):
        dummy_block = bytearray([0]*(15-l) + [b])
        for b2 in last_byte:
            dummy_block += bytearray([b2 ^ (l+1) ^ user[-32:][len(dummy_block)]])

        dummy_user = user[:-32] + dummy_block + user[-16:]
        r = requests.get(URL, cookies={'user': hexlify(dummy_user).decode(), 'signature': signature, 'rack.session': rack_session})

        if 'Bad signature' in r.text:
            last_byte = bytearray([b ^ user[-32:][15-l] ^ (l+1)]) + last_byte
            l += 1
            print(l, last_byte)
            break
    if l == 16:
        m = last_byte.decode() + m
        last_byte = b''
        l = 0
        user = user[:-16]
        print(m)

そして復号の結果、↓が得られた

UID 48

NAME baseuser

SKILLS n/a

ところで暗号文にUID 0を追加する方法だけど、C_Nを適当に改ざんしてC_N+1をUID 0に書き換えれば良さそう。IVの書き換えで大丈夫だろうか。 C1 = E(M1 + IV)として D(C1) + IV2 = M1 + IV + IV2からIV2を計算できる、はず。

と思ったけどsaltつきのハッシュ関数があるので改竄は出来なくて、Length Extension Attackをするしかない。暗号文を少し伸ばしてCn+1, Cn+2の二つのブロックを追加する。Cn+2がpayloadで、まあこれはC1と同じで、Cn+1はMn+2をうまくxorする為に使う。D(Cn+2) = M1 + IV なので Cn+2 = IV + M1 + pad("\nUID 0\n") とかにすると D(Cn+1) + Cn+1 = pad("UID 0")になって良さそう。

すると復号された文は次のようになるはず

UID 48

NAME baseuser

SKILLS n\a

\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10

なんかCn+1を復号したゴミ

UID 0

えー、ゴミの部分がわからないのでLength Extension Attackできなくないですか、ということで調べたらPadding Oracle Encryption Attackができそう。手順は Padding Oracle Attackのところに書いた

要するに

UID 48

NAME baseuser

SKILLS n\a

UID 0

と復号される暗号文をPadding Oracle Encryption Attackで作って、この平文に合うようにLength Extension Attackをやる。ソルバをがーっと書き直した。

何故かBad decryptoと怒られるので泣いてる。たぶんEncryption Attackがなにか良くないんだけど、何が良くないのかはわからない。他のところではうごいてるんだよな。

import requests
from ptrlib import *
from binascii import *
import re


URL = "http://localhost:3000/"
r = requests.get(URL, allow_redirects=False)

user = unhexlify(r.cookies["user"].encode())
signature = r.cookies["signature"]
rack_session = r.cookies["rack.session"]

print(signature)


def oracle(user):
    r = requests.get(
        URL,
        cookies={
            "user": hexlify(user).decode(),
            "signature": signature,
            "rack.session": rack_session,
        },
    )
    if "bad decrypt" in r.text:
        return False
    if "Bad signature" in r.text:
        return True
    raise Exception(r.text)


def pad(bs, l):
    l2 = len(bs) % l
    return bs + bytes([l2]) * (l - l2)


def unpad(bs):
    l = bs[-1]
    return bs[:-l]


m = padding_oracle(oracle, user[16:], 16, unknown=b"A", iv=user[:16])
m = unpad(m)
print(m)

new_hash, data = lenext(SHA256, 8, signature, m, b"\nUID 0")
new_user = padding_oracle_encrypt(oracle, plain=pad(data, 16), bs=16, unknown=b"A")
print(repr(data))
print(new_user)
print(repr(new_hash))

r = requests.get(
    URL,
    cookies={
        "user": hexlify(new_user).decode(),
        "signature": signature,
        "rack.session": rack_session,
    },
)
print(r.text)