ふるつき

v(*'='*)v かに

RCTF 2019 writeup

RCTF 2019 has been held. We participated in it as team zer0pts and were the 59th place with 791 points. I'm writing up about the baby crypto challenge.

f:id:Furutsuki:20190520100734p:plain

[Crypto 400pts(Solved by 31 teams)] baby crypto

nc 45.76.208.70 20000 nc 207.148.68.109 20000

First, it was very similar to the decrypto challenge of BSidesSF 2019 CTF. Luckily I knew it, so I could get the second solve.

We were given the server side script file: crypto.py.

#!/usr/bin/python3 -u
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import padding
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from hashlib import sha1
import binascii
import json
import os
import re
import sys

def pad(s):
    padder = padding.PKCS7(128).padder()
    return padder.update(s) + padder.finalize()

def unpad(s):
    unpadder = padding.PKCS7(128).unpadder()
    return unpadder.update(s) + unpadder.finalize()

key = os.urandom(16)
iv = os.urandom(16)
salt = key

iv_len = 16

print("Input username:")
username = sys.stdin.readline().strip()
if not re.match("^[a-z]{5,10}$", username):
    print("Invalid username")
    exit()

print("Input password:")
password = sys.stdin.readline().strip()
if not re.match("^[a-z]{5,10}$", password):
    print("Invalid password")
    exit()

cookie = b"admin:0;username:%s;password:%s" %(username.encode(), password.encode())

h = sha1()
h.update(salt)
h.update(cookie)
hv = h.digest()
hv_hex = h.hexdigest()
hash_len = len(hv)

cookie_padded = pad(cookie)

backend = default_backend()
cipher = Cipher(algorithms.AES(key), modes.CBC(iv), backend=backend)
encryptor = cipher.encryptor()
cookie_padded_encrypted = encryptor.update(cookie_padded) + encryptor.finalize()
print("Your cookie:")
print(binascii.hexlify(iv).decode() + binascii.hexlify(cookie_padded_encrypted).decode() + hv_hex)

def is_valid_hash(cookie, hv):
    h = sha1()
    h.update(salt)
    h.update(cookie)
    return hv == h.digest()

while True:
    try:
        print("Input your cookie:")
        data_hex = sys.stdin.readline().strip()
        data = binascii.unhexlify(data_hex)
        assert(len(data) > iv_len + hash_len)
        iv, cookie_padded_encrypted, hv = data[:iv_len], data[iv_len: -hash_len], data[-hash_len:]
        cipher = Cipher(algorithms.AES(key), modes.CBC(iv), backend=backend)
        decryptor = cipher.decryptor()
        cookie_padded = decryptor.update(cookie_padded_encrypted) + decryptor.finalize()
        try:
            cookie = unpad(cookie_padded)
        except Exception as e:
            print("Invalid padding")
            continue
        if not is_valid_hash(cookie, hv):
            print("Invalid hash")
            continue
        info = {}
        for _ in cookie.split(b";"):
            k, v = _.split(b":")
            info[k] = v
        if info[b"admin"] == b"1":
            with open("flag") as f:
                flag = f.read()
                print("Your flag: %s" %flag)
        else:
            print("Goodbye nobody")
        exit()
    except Exception as e:
        print("Invalid cookie %s" %e)

The application manages a login credential encrypted with AES-CBC PCKS#7. As we can get the decryption oracle, the padding oracle encryption attack works.

However, a salted SHA-1 hash value of the login credential was also used for checking the consistency of the credential value. It prevents us from a naive padding oracle attack.

How can we avoid the check? The length extension attack is also available since we know the length of the salt and the heading several bytes of the plaintext.

Thus, to get the flag, we just have to create a new login credential which is known_plaintext || paddings || ;admin:1, get its SHA-1 value by length extension attack, and then encrypt it using padding oracle encryption attack.

The following script does this.

from ptrlib import *
from binascii import hexlify, unhexlify

iv_len = 16
hash_len = 20
salt_len = 16

username = b"takoyaki"
password = b"hogepiyo"

sock = Socket("207.148.68.109", 20000)
# sock = Socket("localhost", 8765)
sock.recvuntil("Input username:\n")
sock.sendline(username)

sock.recvuntil("Input password:\n")
sock.sendline(password)


sock.recvuntil("Your cookie:\n")
hex_cookie = sock.recvline().decode().strip()
data = unhexlify(hex_cookie)
iv, cookie, checksum = data[:iv_len], data[iv_len:-hash_len], data[-hash_len:]
print("[+]cookie, checksum = {}, {}".format(cookie, checksum))


def oracle(x):
    sock.recvuntil("Input your cookie:\n")
    my_cookie = hexlify(x + checksum).decode()
    sock.sendline(my_cookie)
    result = sock.recvline().decode().strip()
    if "padding" in result:
        return False
    if "hash" in result:
        return True
    raise Exception(result)


def pad(x):
    l = (16 - len(x)) % 16
    return x + bytes([l] * l)


known_msg = b"admin:0;username:%s;password:%s" % (username, password)
append_msg = b";admin:1"

new_checksum, new_data = lenext(SHA1, salt_len, checksum, known_msg, append_msg)
attack_data = padding_oracle_encrypt(oracle, plain=pad(new_data), bs=16, unknown=b"A")

print("[+]new_cookie, new_checksum = {}, {}".format(repr(new_data), repr(new_checksum)))
print("[+]attack data = {}".format(hexlify(new_data)))

sock.recvuntil("Input your cookie:\n")
attack_cookie = hexlify(attack_data).decode() + new_checksum
sock.sendline(attack_cookie)

print(new_checksum)
print(sock.recvline())
print(sock.recvline())
print(sock.recvline())
print(sock.recvline())

TSG CTF 2019 Writeup

insecureとしてTSG CTFに出ていました。868点で18位でした。難しくて実力不足を感じました。泣いた。

[Forensics 92 + 178 pts (90 + 61)Solves] Obliterated File & Obliterated File Again

※ This problem has unintended solution, fixed as "Obliterated File Again". Original problem statement is below.

Working on making a problem of TSG CTF, I noticed that I have staged and committed the flag file by mistake before I knew it. I googled and found the following commands, so I'm not sure but anyway typed them. It should be ok, right?


※ この問題は非想定な解法があり,"Obliterated File Again" で修正されました.元の問題文は以下の通りです.

TSG CTFに向けて問題を作っていたんですが,いつの間にか誤ってflagのファイルをコミットしていたことに気付いた!とにかく,Google先生にお伺いして次のようなコマンドを打ちこみました.よくわからないけどこれできっと大丈夫...?


$ git filter-branch --index-filter "git rm -f --ignore-unmatch problem/flag" --prune-empty -- --all
$ git reflog expire --expire=now --all
$ git gc --aggressive --prune=now

Difficulty Estimate: easy

I realized that the previous command had a mistake. It should be right this time...?


さっきのコマンドには間違いがあったことに気づきました.これで今度こそ本当に,本当に大丈夫なはず......?


$ git filter-branch --index-filter "git rm -f --ignore-unmatch *flag" --prune-empty -- --all
$ git reflog expire --expire=now --all
$ git gc --aggressive --prune=now

Difficulty Estimate: easy - medium

要するにgitで丁寧に削除したファイルを復元してみろという問題です。

1個目は

[https://stackoverflow.com/a/6018043:title]

を見てこれで探してみると、削除漏れらしきコミットが見えるので、その一つ前のコミットにcheckoutして中身を覗きました。zlib compressedなデータの読み方をPHP以外に知らないので、gzuncompressed を使いました。

$ git log --diff-filter=D --summary
commit 266f4148e4cf37bdbfb57da379ea49b2f106e6b2 (HEAD -> master)
Author: tsgctf <info@tsg.ne.jp>
Date:   Fri May 3 04:33:22 2019 +0900

   delete .travis.yml

delete mode 100644 .travis.yml

commit 28d2b74b0c40583a87cf275f9f0cdfd55042884d
Author: tsgctf <info@tsg.ne.jp>
Date:   Thu May 2 05:45:41 2019 +0900

   add problem statement

delete mode 100644 flag
php -a
Interactive mode enabled

php > $a = file_get_contents("flag");
php > echo gzuncompress($a);
TSGCTF{$_git_update-ref_-d_refs/original/refs/heads/master}

2問目はコマンドが少し変わって、1問目と同じ解き方ができなくなっています。最早 .git以下のオブジェクトを全部見て回るしか無いと思ってそういう手段を探すと git show - Git - how to list ALL objects in the database - Stack Overflow を見つけたのでやりました。

user@box:~/TSGCTF/obliterated_file_again/easy_web$ {
>     git rev-list --objects --all
>     git rev-list --objects -g --no-walk --all
>     git rev-list --objects --no-walk \
>         $(git fsck --unreachable |
>           grep '^unreachable commit' |
>           cut -d' ' -f3)
> } | sort | uniq | grep flag
Checking object directories: 100% (256/256), done.
Checking objects: 100% (101/101), done.
c1e375244c834c08d537d564e2763a7b92d5f9a8 problem/flag
user@box:~/TSGCTF/obliterated_file_again/easy_web$ git cat-file -p c1e375244c834c08d537d564e2763a7b92d5f9a8 > flag
user@box:~/TSGCTF/obliterated_file_again/easy_web$ php -a
Interactive mode enabled

php > echo gzuncompress(file_get_contents("flag"));
TSGCTF{$_git_update-ref_-d_refs/original/refs/heads/master_S0rry_f0r_m4king_4_m1st4k3_0n_th1s_pr0bl3m}

[Crypto 497pts (10 Solves)] OPQRX

Can you decrypt RSA? I'll give a hint value, XOR.

ここにRSAの暗号文がありますが、XORをあげるので、代わりに平文をください。


Difficulty Estimate: Easy

単純なRSAですが、 N, C, E に加えて X = P xor Q としたXも与えられています。次のようなスクリプトで上位bitから候補を絞っていきました。少し待つとP, Qが得られます。p, qは順不同なのでそれをやると探索範囲が半分になりますね。今気が付きました。

exec(open("flag.enc").read())

K = 1 << 4095

kouhos = [[0, 0]]
count = 4095
while K > 0:
    next_kouho = set()
    for kouho in kouhos:
        P, Q = kouho
        p = P | K
        q = Q | K
        if X & K:
            if p * Q <= N:
                next_kouho.add((p, Q))
            if P * q <= N:
                next_kouho.add((P, q))
        else:
            if p * q <= N:
                next_kouho.add((p, q))
            else:
                next_kouho.add((P, Q))
    assert len(next_kouho) > 0

    kouhos = []
    for n in next_kouho:
        P, Q = n
        p = P | (K - 1)
        q = Q | (K - 1)
        if p * q >= N and P * Q <= N:
            kouhos.append(n)

    print(count, len(kouhos))
    count -= 1
    K >>= 1

print(kouhos)

あとはやるだけです。

exec(open("primes").read())
exec(open("flag.enc").read())

from Crypto.Util.number import *

print(len(primes))
for prime in primes:
    p, q = prime
    phi = (p - 1) * (q - 1)
    d = inverse(E, phi)
    m = pow(C, d, N)
    print(long_to_bytes(m))

フラグは TSGCTF{Absolutely, X should be 'S' in 'OPQRX'.} でした。

ångstromCTF 2019 writeup

I played ångstromCTF 2019 as a member of zer0pts. We gained 3730pts totally and got 8th place. I learned so many things from all the challenges. Thanks to all the admins!

f:id:Furutsuki:20190425111527p:plain

[Rev 130pts] Icthyo

Long before stegosaurus roamed the earth, another species prowled the sea; here is an artist's rendition.

We were given two files: icthyo and out.png. ichtyo, which was a 64-bit ELF file, hid some texts into the image file. I decompiled it by ghidra and ovserbed the steganography process, then I found the interesting part of the code in encode function (I edited variable names to make it easier to understand the process) .

// (snipped)
printf("message (less than 256 bytes): ");
fgets((char *)msg,0x100,stdin);
y = 0;
while (y < 0x100) {
    line = *(long *)(rows + (long)y * 8);
    // (snipped)
    i = 0;
    while (i < 8) {
        rgb = (byte *)(line + (long)(i * 0x60));
        c = *(char *)((long)msg + (long)y);
        if ((rgb[2] & 1) != 0) {
            rgb[2] = rgb[2] ^ 1;
        }
        rgb[2] = rgb[2] | (byte)((int)c >> ((byte)i & 0x1f)) & 1 ^ (rgb[1] ^ *rgb) & 1;
        i = i + 1;
    }
    y = y + 1;
}

In this process it sets the LSB to each bit of the character. So it could extract the hidden message by the following code. The hidden message was the flag: actf{lurking_in_the_depths_of_random_bits}.

from PIL import Image

img = Image.open("out.png")
w, h = img.size

lines = []

for y in range(h):
    line = []
    for x in range(w):
        line.append(img.getpixel((x, y)))
    lines.append(line)

buf = ""
for l in lines:
    c = ""
    for x in range(8):
        r, g, b = l[x * 32]
        c += str((b & 1) ^ ((r ^ g) & 1))
    x = int(c[::-1], 2)
    if x == 0:
        break
    buf += chr(x)
print(buf)

Note that the decompiled code processes the image byte by byte, whereas my code treated it for each pixels.

[Crypto 100pts] Paint

This amazing new paint protocol lets artists share secret paintings with each other! Good thing U.S. Patent 4200770 is expired.

The values palette, base, my mix, your mix, and painting were given while secret and shared mix were hidden. They have the following relation.

 my mix = base^{secret} \mod palette

 shared mix = your mix^{secret} \mod palette

 painting = image \oplus shared mix

We need to get the shared mix to recover the image from the painting. Also, to get the shared mix We need to get the secret.

Then, how can I get the secret? Now, we know my mix, base and palette. So if the discrete log problem secret \equiv \log{(base)}{my mix} \mod palette could be solved, we will get the secret

Surprizingly, sage was able to do it.

my_mix = 68702...93113
base = 13489...86329
pallete = 32317...30656

secret = discrete_log(my_mix, Mod(base, pallete))
print(secret)
from Crypto.Util.number import *

secret = 62992...62361
pallete = 32317...30656
your_mix = 14317...48217
painting = 17665...43620

shared_mix = pow(your_mix, secret, pallete)
print(long_to_bytes(shared_mix ^ painting))

The flag was actf{powers_of_two_are_not_two_powerful}

[Crypto 120pts]Secret Sheep Society

The sheep are up to no good. They have a web portal for their secret society, which we have the source for. It seems fairly easy to join the organization, but climbing up its ranks is a different story.

When we logged in to the web portal, the token was issued. Token consisted of an IV and a json encrypted with CBC mode AES. The json had two columns: admin and handle, and our mission is to make admin column true.

As the json was encrypted with CBC mode AES and the IV was given, we could tamper the first block of the plaintext by editing the iv.

import json
import base64


x = 'jirU9ZpEy+x0J9dOK1AOiU1rRsB+4cY9Hj8b19oQ/iYaK4zM0UdegzxK4dv7aYtv'
bs = bytearray(base64.b64decode(x))

offset = len('{"admin": ')
bs[offset] = bs[offset] ^ ord('f') ^ ord(' ')
bs[offset+1] = bs[offset+1] ^ ord('a') ^ ord('t')
bs[offset+2] = bs[offset+2] ^ ord('l') ^ ord('r')
bs[offset+3] = bs[offset+3] ^ ord('s') ^ ord('u')
# bs[offset] = bs[offset] ^ ord('e') ^ ord('e')

print(base64.b64encode(bs))

[Crypto 130pts]WALL-E

My friend and I have been encrypting our messages using RSA, but someone keeps intercepting and decrypting them! Maybe you can figure out what's happening?

from Crypto.Util.number import getPrime, bytes_to_long, inverse, long_to_bytes
# from secret import flag

flag = 'actf{' + '~'*(86-6) + '}'

assert(len(flag) < 87) # leave space for padding since padding is secure

p = getPrime(1024)
q = getPrime(1024)
n = p*q
e = 3
d = inverse(e,(p-1)*(q-1))
m = bytes_to_long(flag.center(255,"\x00")) # pad on both sides for extra security
c = pow(m,e,n)
print("n = {}".format(n))
print("e = {}".format(e))
print("c = {}".format(c))

Since e=3 and the flag was padded with "\x00", I tried the Coppersmith's attack.

from Crypto.Util.number import *
import string
n = 16930...08409
e = 3
c = 11517...26989

l = 86
pad_len = (255 - l) // 2
low_pad = pow(256, pad_len)

l2 = l - 5
high_pad = bytes_to_long(b'actf{' + b'\x00' * (l2 + pad_len))

PR.<x> = PolynomialRing(Zmod(n))
f = (high_pad + x*low_pad)^e - c
f = f.monic()
xs = f.small_roots(X=2^(l2*8), beta=1)
for x in xs:
  print(long_to_bytes(x))

The flag was actf{bad_padding_makes_u_very_sadding_even_if_u_add_words_just_for_the_sake_of_adding}.

ASIS CTF Quals 2019 Writeup

I played ASIS CTF Quals 2019 as a member of insecure. We got 224 pts and reached 88th place. Thanks to all the admins for the great competition.

I wanted to solve more difficult challenges.

[Coding 67pts(78 Solves)] Flag collision

Warm-up your fingers to capture next flags!

nc 37.139.9.232 19199

Just do it.

from ptrlib import *
import binascii
import string
import random
import hashlib
import re

def randomString(length, table=string.ascii_lowercase):
    return ''.join(random.choice(table) for i in range(length))

sock = Socket("37.139.9.232", 19199)

sock.recvuntil("Submit a printable string X, such that ")
method = sock.recvuntil("(X)").decode()[:-3]
pos = int(sock.recvuntil("] = ").decode()[1:-5])
target = sock.recvline().decode().strip()
print(method, pos, target)

i = 0
while True:
    s = str(i)
    h = hashlib.new(method)
    h.update(s.encode())
    if h.hexdigest()[pos:] == target:
        print(s)
        sock.sendline(s)
        break
    i += 1


while True:
    while True:
        line = sock.recvline().decode().strip()
        print(line)
        l = re.findall("len = ([0-9]+)", line)
        if l:
            l = int(l[0])
            break
    print("len = {}".format(l))
    table = {}
    while True:
        s = randomString(l-len("ASIS{}"))
        s = "ASIS{{{}}}".format(s)
        v = binascii.crc32(s.encode())
        if v in table:
            print("({}, {})".format(table[v], s))
            sock.sendline("{}, {}".format(table[v], s))
            # sock.sendline("ASIS{{{}}}".format(s))
            break
        table[v] = s

for i in range(10):
    print(sock.recvline())

[Crypto 44pts(135 Solves)] A delicious soup

Once you've decided on what ingredients to use, making a delicious soup is very simple!

As I looked over the given script ( simple_and_delicious.py), I found it shuffled the flag for a random number of times.

If we can get the two parameters, how many times the shuffle is applied and the resulf of random.shuffle(range(7)), then the flag can be recovered. And, we can find the both parameters by bruteforce.

This solve.py lists up all the possible flag patterns. Since the flag starts with ASIS{..., I could filter the output. The flag was ASIS{1n54n3ly_Simpl3_And_d3lic1Ous_5n4ckS_eVEn_l4zY_Pe0pL3_Can_Mak3}.

CPCTF2019 writeup

CPCTF 2019に参加していました。最終的に8100点で3位でした。嬉しいです。全ての問題のwriteupを書くのは大変なので、面白かった問題や回答数の少ない問題についてのwriteupをします。

f:id:Furutsuki:20190417214536p:plain

こうしてみると僕の解いた問題は回答数の多い、誰でも解ける問題ばかりで挑戦をしてこなかったことがよくわかります。深く反省

[Crypto 300]worm_eaten

フラグ 虫に 食べられて 穴が 空いた

$ ruby encrypt.rb > output.txt

http://files.problem.cpctf.space/worm_eaten/encrypt.rb
http://files.problem.cpctf.space/worm_eaten/output.txt

500の方は解けませんでした。Coppersmith's methodを使うんだろうという推測はたちましたが、知識も実装の経験もなく、短時間のCTFでこれに挑戦するリスクはとれませんでした。

300点の方は一般的なRSAですが、平文の殆どがみえています。というわけで3文字分をブルートフォースしてeとnを用いて暗号化、cと同じ暗号文が生成されるものを探します。

from Crypto.Util.number import bytes_to_long
from binascii import unhexlify, hexlify
from ptrlib import *


n = 143683020644322299318564543310032550731362229816998143115532955396082143235946288551633953435584612777128870426064762700722474983419557070478193865039092489162972540442193198467681495674219822666567094248226655965731005254314292588896865658797569039580229607586511231175110747375426393447205505697507722501469
e = 3


c = 0x4bbd08e630c706c6d9e5757d3392f4aef603ed7e34f649666b8796afe772bcee84ca6882fec351dac1003861fc3a8f306a232d381f36714f41119f8df37fab0e07e02965898913bc349ca4aa4a275498ab85444664480d3ff72b707844d57a1b40658529d375ada1df10a1f867527f2686ea27392af83cdbf8f4bd9ebc46604a

for password in brute_force_attack(3):
    x = brute_force_pattern(password)
    m = "FLAG_300{{{}0513a1db877145db49b38f80f8fe7a6c6c9912b67d6a9a6d2c8dada7e15e}}".format(x)
    m2 = int(hexlify(m.encode()), 16)

    if pow(m2, e, n) == c:
        print(m)
        exit()

[Crypto 300]SHAII_we_collide?

HEY can you find SHA-256 collision?

http://shaii_we_collide.problem.cpctf.space/

問題文に示されたページへいくと、次のようなソースコードが示されています。

from wsgiref.simple_server import make_server
import cgi
import hashlib

def app(env, start_response):
    method = env['REQUEST_METHOD']
    path = env['PATH_INFO']
    if method == 'GET' and path == '/':
        # index
        headers = [('Content-Type', 'text/html; charset=utf-8')]
        start_response('200 OK', headers)
        with open('./index.html') as f:
            return [bytes(f.read(), encoding='utf-8')]
    elif method == 'POST' and path == '/collide':
        # collide query
        headers = [('Content-Type', 'text/plain; charset=utf-8')]
        start_response('200 OK', headers)
        # post data
        form = cgi.FieldStorage(fp=env["wsgi.input"], environ=env, keep_blank_values=True)
        x = form["x"].value
        y = form["y"].value

        # !!! collision check !!!
        if x == y: return [bytes('not collide!', encoding='utf-8')]
        ## get sha256
        shax = hashlib.sha256(x.encode()).hexdigest()
        shay = hashlib.sha256(y.encode()).hexdigest()
        ## assert shax and shay is 32byte
        shax = int(shax,16) & ((1<<32)-1)
        shay = int(shay,16) & ((1<<32)-1)
        ## check
        if shax != shay: return [bytes('not collide!', encoding='utf-8')]

        # congrats!
        flag = 'Flag is Here: ***CENSORED***'
        return [bytes(flag, encoding='utf-8')]

    # doesn't match
    headers = [('Content-Type', 'text/plain; charset=utf-8')]
    start_response('400 Bad Request', headers)
    return [bytes('Error', encoding='utf-8')]

PORT = 8383
httpd = make_server('', PORT, app)
httpd.serve_forever()

どうやらsha256をとって衝突する異なる2つの文字列を入力すれば良いようですが、それぞれ int(shax,16) & ((1<<32)-1) のように切り詰められていて簡単に衝突させることが出来ます。

書くだけです。すぐに衝突する組み合わせ 01@[, 09xtが見つかります。

from ptrlib import *
import hashlib

table = {}

for password in brute_force_attack(4):
    x = brute_force_pattern(password)
    shax = hashlib.sha256(x.encode()).hexdigest()
    shax = int(shax,16) & ((1<<32)-1)
    if shax in table:
        print(table[shax])
        print(x)
        exit()
    else:
        table[shax] = x

[Binary 300]fast_fibonacci

fib(1)=fib(2)=1
binary

注:そのまま実行するとSegmentation fault (core dumped)と表示されるか、何も表示されず実行が終了もしない、のどちらかになると思いますが、それが正常です。

問題名を見ると、フィボナッチ数列の第n項の計算が遅いのでその後のフラグ出力パートが実行できていないことがわかります。オーバーフローするように注意しながら早いフィボナッチを書いて fib(0x53e691) の結果が f0bbeb3d になることを突き止め、 gdbでステップ実行しながら fib関数の呼び出しを飛ばして、 eaxf0bbeb3dを代入した状態で decode関数を呼び出すとフラグが手に入ります。(b main r set $pc=0x565557b8 set $eax=0xf0bbeb3d)

[Crypto 300] [input]

stage7だけ解法を書いておきます。

def f(s):
    m = 0
    for c in s:
        m += (ord(c) - ord('a')) + 1

    return "".join([chr((ord(c) - ord('a') + m) % 26 + ord('a')) for c in s])

from ptrlib import *
import string

for password in brute_force_attack(5, table_len=26):
    x = brute_force_pattern(password, table=string.ascii_lowercase)

    if f(x) == "input":
        print(x)
        exit()

PlaidCTF 2019 writeup

I played PlaidCTF 2019 as a member of insecure. Our team got 261pts and reached the 116th place. I don't feel it's a good result. All the challenges I tried were very difficult but also a lot of fun.

[Misc 10pts(368 solves)] docker

docker pull whowouldeverguessthis/public

I ran docker pull and docker run --it public bash as the description says. Then I found the file flag in /root but the content was I'm sorry, but your princess is in another castle. I wondered that I'd get the solution if I could see how this container image was created. So, I ran docker image history and found that the flag was overwritten during the container creation.

$ docker image history whowouldeverguessthis/public --no-trunc
IMAGE                                                                     CREATED             CREATED BY                                                                                          SIZE                COMMENT
sha256:969996089570ead17d586e6b940c8cb0375aba7bd329076cbe2a2fc18653b8d9   6 hours ago         /bin/sh -c echo "I'm sorry, but your princess is in another castle" > /flag                         50B                 
<missing>                                                                 6 hours ago         /bin/sh -c echo "PCTF{well_it_isnt_many_points_what_did_you_expect}" > /flag                        51B                 
<missing>                                                                 2 months ago        /bin/sh -c #(nop)  CMD ["bash"]                                                                     0B                  
<missing>                                                                 2 months ago        /bin/sh -c #(nop) ADD file:34b9952e66cb98287bc41fab82739375fe6c43f38ed3b893e98a99035b494770 in /    68.9MB             

[Misc 100pts (306 solves)]can you guess me

Here's the source to a guessing game: here

You can access the server at

As I read the distributed python script, I found that I had to construct a python code with 10 or less than 10 types of characters which must be evaluated as the same value of secret_value_for_password in order to get the flag. Unfortunately print(flag) was invalid because it had 11 types of characters. I tried some other ways to dump the variables and found print(vars()). It has just 10 types of characters and it'll dump all the variables.

$ nc canyouguessme.pwni.ng 12349


  ____         __   __           ____                     __  __       
 / ___|__ _ _ _\ \ / /__  _   _ / ___|_   _  ___  ___ ___|  \/  | ___  
| |   / _` | '_ \ V / _ \| | | | |  _| | | |/ _ \/ __/ __| |\/| |/ _ \ 
| |__| (_| | | | | | (_) | |_| | |_| | |_| |  __/\__ \__ \ |  | |  __/ 
 \____\__,_|_| |_|_|\___/ \__,_|\____|\__,_|\___||___/___/_|  |_|\___| 
                                                                       


Input value: print(vars())
{'__name__': '__main__', '__doc__': None, '__package__': None, '__loader__': <_frozen_importlib_external.SourceFileLoader object at 0x7fe9664399e8>, '__spec__': None, '__annotations__': {}, '__builtins__': <module 'builtins' (built-in)>, '__file__': '/home/guessme/can-you-guess-me.py', '__cached__': None, 'exit': <built-in function exit>, 'secret_value_for_password': 'not even a number; this is a damn string; and it has all 26 characters of the alphabet; abcdefghijklmnopqrstuvwxyz; lol', 'flag': 'PCTF{hmm_so_you_were_Able_2_g0lf_it_down?_Here_have_a_flag}', 'exec': <function exec at 0x7fe966382158>, 'val': 0, 'inp': 'print(vars())', 'count_digits': 10}
Nope. Better luck next time.

Well done.

[Misc 150pts(97 solves)]A Whaley Good Joke

You'll have a whale of a time with this one! I couldn't decide what I wanted the flag to be so I alternated adding and removing stuff in waves until I got something that looked good. Can you dive right in and tell me what was so punny?

We were given a tar.gz file which had various sha256-string name jsons/directories, manifest.json, and repositories. After doing a search, I found they were created by docker save. So, I tried docker load but it failed because some layer names were filled with ???. When I checked the contents archived in layer.tar for each layer directories, I found /root/flag.sh as shown below.

#!/bin/bash

for i in {1..32}
do
    test -f $i
    if [[ $? -ne 0 ]]
    then
        echo "Missing file $i - no flag for you!"
        exit
    fi
done

echo pctf{1_b3t$(cat 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32)}

In order to get the flag, we have to concatenate the files from 1 to 32. Other layer.tar files contained some files such as /root/1, /root/20 and so on. However, they were duplicated since there were some processes which overwrote some files. So, I decided to do a search for the candidates of the flag by brute force.

Thus I decided to search the candidates of the flag by brute force.

for d in `ls`; do
  if [[ -d "$d" ]]; then
    tar xf "$d/layer.tar" -C "$d"
  fi
done
import glob
import re

paths = [p for p in glob.glob("**", recursive=True) if re.search(r'root/[0-9]+$', p) and not p.startswith('workspace')]

table = {}
for p in paths:
    n = int(re.findall(r'root/([0-9]+)$', p)[0])
    with open(p) as f:
        x = f.read()

    if n not in table:
        table[n] = set()
    table[n].add(x)


def f(i, e, s):
    if i > e:
        print(s)
        return

    for x in table[i]:
        f(i+1, e, s+x)

Of course it'd be hard to do a search for 32 characters so I did a search word by word.

Eventually I got the most plausible flag: pctf{1_b3t_4_couldnt_c0nt4in3r_ur_l4ught3r} but it didn't work. So, I posted the previous script to our team Slack. Then my teammate id:ptr-yudai submitted the flag pctf{1_b3t_u_couldnt_c0nt4in3r_ur_l4ught3r} and it was correct.

jedi-vimでpipenvから補完する

  • ファイルを開いたときにそれがpipenv環境下のファイルであればpipenvによって作られた仮想環境から補完候補を出してほしい
  • jedi-vimのロード後でも雑に環境を選びたい

をうまく実現しようとしたら、jedi-vim本体に変更を加える羽目になりました。

GitHub - theoldmoon0602/jedi-vim at theoldmoon0602

g:jedi#virtualenv_pathという変数をg:jedi#force_py_versionの代わりに使えて、let g:jedi#virtualenv_path = ~/.local/share/virtualenvs/someenv というようにpipenv --venvで返ってくるパスを代入しておくと、こちらのパスから補完してくれます。私は以下のように書いて運用しています。

" vim-plug を使っているのでこう
Plug 'theoldmoon0602/jedi-vim', {'do': 'pip install --user -U jedi'}


" ファイルのディレクトリまたは親ディレクトリのいずれかにPipfileがあればpipenv管理下としてパスをセットする
function! s:addPipenvPath()
  let pipenv_dir = expand('%:p:h')
  while pipenv_dir != '/'
    if filereadable(l:pipenv_dir . '/Pipfile')
      let venv_path = trim(system(printf("sh -c 'cd %s; pipenv --venv'", pipenv_dir)))
      let g:jedi#virtualenv_path = venv_path
      return
    endif

    let pipenv_dir = fnamemodify(pipenv_dir, ':h')
  endwhile
endfunction

augroup vimrc-python
  autocmd!
  autocmd FileType python :call s:addPipenvPath()
augroup END

別のpipenv環境下にある二つのファイルを開いたときに2つ目のファイルの環境だけを使うことになってしまうのは問題だなぁと思っていますが、小さい問題だし無視しようかなと思っています。

追記

思い切ってjedi-vim本体にPull Reqという形でコードを投げてみたら、すでにこういうことをやろうとしている(しかももっとちゃんとやろうとしている)動きはあって、 https://github.com/davidhalter/jedi-vim/pull/836 がそれに当たるみたいです。こちらが取り込まれればこの記事も不要になりますね