Tenable CTF 2021 Writeup

この大会は2021/2/18 21:00(JST)~2021/2/22 21:00(JST)に開催されました。
今回もチームで参戦。結果は3925点で1762チーム中53位でした。
自分で解けた問題をWriteupとして書いておきます。

Intro 1 (_Introduction 25)

問題にフラグが書いてあった。

flag{some_clever_text}

Intro 2 (_Introduction 25)

Doscordに入ると、#generalチャネルのトピックにフラグが書いてあった。

flag{you_f0und_m3}

Esoteric (Misc 25)

Brainf*ck言語。https://sange.fi/esoteric/brainfuck/impl/interp/i.htmlで実行する。

flag{wtf_is_brainfuck}

Quit messing with my flags (Misc 25)

161EBD7D45089B3446EE4E0D86DBCF92をmd5の逆変換として検索する。

https://md5.gromweb.com/?md5=161ebd7d45089b3446ee4e0d86dbcf92
⇒P@ssw0rd
flag{P@ssw0rd}

One Byte at a Time (Misc 50)

$ nc challenges.ctfd.io 30468

Show me how much of the flag you know and I'll help you with the rest.'

[flag]>f

You seem to know the first 1 characters of the flag!
XORing the next flag character with a random octet taken from some unknown IPv4 address I have...

0x1b


$ nc challenges.ctfd.io 30468

Show me how much of the flag you know and I'll help you with the rest.'

[flag]>fl

You seem to know the first 2 characters of the flag!
XORing the next flag character with a random octet taken from some unknown IPv4 address I have...

0x16

>>> 0x1b ^ ord('l')
119
>>> 0x16 ^ ord('a')
119

$ nc challenges.ctfd.io 30468

Show me how much of the flag you know and I'll help you with the rest.'

[flag]> 

You seem to know the first 0 characters of the flag!
XORing the next flag character with a random octet taken from some unknown IPv4 address I have...

0x76

>>> 0x76 ^ ord('f')
16

$ nc challenges.ctfd.io 30468

Show me how much of the flag you know and I'll help you with the rest.'

[flag]>

You seem to know the first 0 characters of the flag!
XORing the next flag character with a random octet taken from some unknown IPv4 address I have...

0x64

>>> 0x64 ^ ord('f')
2

$ nc challenges.ctfd.io 30468

Show me how much of the flag you know and I'll help you with the rest.'

[flag]>    

You seem to know the first 0 characters of the flag!
XORing the next flag character with a random octet taken from some unknown IPv4 address I have...

0x11

>>> 0x11 ^ ord('f')
119

とりあえずXOR鍵は、16, 2, 119のどれか。プログラムで119の1点張りで復号する。

import socket

def recvuntil(s, tail):
    data = ''
    while True:
        if tail in data:
            return data
        data += s.recv(1)

key = 119

flag = ''
c = ''
while True:
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    s.connect(('challenges.ctfd.io', 30468))

    data = recvuntil(s, '>')
    print data + flag + c
    s.sendall(flag + c + '\n')

    data = recvuntil(s, '\n').rstrip()
    print data
    data = recvuntil(s, '\n').rstrip()
    print data
    if data.startswith('You seem'):
        flag += c
        if 'character' not in data:
            break
        data = recvuntil(s, '\n').rstrip()
        print data
        data = recvuntil(s, '\n').rstrip()
        print data
        data = recvuntil(s, '\n').rstrip()
        print data
        code = eval(data)
        c = chr(code ^ key)
    else:
        data = recvuntil(s, '\n').rstrip()
        print data
        c = ''

print flag

実行結果は以下の通り。

        :
Show me how much of the flag you know and I'll help you with the rest.'

[flag]>flag{f0ll0w_th3_whit3_r@bb1t

You seem to know the first 28 characters of the flag!
XORing the next flag character with a random octet taken from some unknown IPv4 address I have...

0x0a

Show me how much of the flag you know and I'll help you with the rest.'

[flag]>flag{f0ll0w_th3_whit3_r@bb1t}

You seem to know it all. Can't help you anymore
flag{f0ll0w_th3_whit3_r@bb1t}
flag{f0ll0w_th3_whit3_r@bb1t}

Forwards from Grandma (Misc 100)

メールのSubjectにこう書いてある。

FWD: FWD: RE: FWD:  FWD: RE: FWD: FWD:  FWD: RE:  RE: RE: FWD: { FWD:
 FWD:  FWD: FWD: RE: RE: FWD: RE:  RE: RE:  FWD: FWD:  FWD: FWD: FWD:  FWD:
 FWD: FWD:  FWD: FWD: RE: RE: FWD: RE:  FWD: RE:  RE: RE: RE:  FWD: RE: FWD:
 FWD: } THIS IS HILARIOUS AND SO TRUE

FWD: を "."、RE: を"-"にすると、" "区切りでモールス信号になっているので、デコードする。

..-. .-.. .- --. {.. ..--.- -- .. ... ... ..--.- .- --- .-..}
flag{i_miss_aol}

Parsey Mcparser (Code 50)

指定したグループ名のユーザ名をリストで返すようスクリプトを組む。以下のスクリプトで通った。

def ParseNamesByGroup(blob, group_name):
    uname = []
    arr = blob.split('+++,')[1:]
    for a in arr:
        data = a.split(',', 1)[1]
        info = data[1:-1].split(',[')
        for i in info:
            info = eval('{' + i.rstrip(']') + '}')
            group = info['Group']
            if group == group_name:
                uname.append(info['user_name'])
    return uname
   
data = raw_input()
group_name = data.split('|')[0]
blob = data.split('|')[1]
result_names_list = ParseNamesByGroup(blob, group_name)
print result_names_list

Random Encryption (Code 100)

コードの中に記載されているフラグで通った。

flag{n0t_that_r4ndom}

Random Encryption Fixed (Code 100)

seedを使った乱数なので、XOR鍵を割り出せる。XOR鍵を割り出したら、その鍵で復号する。

#!/usr/bin/python3
import random

rand_list = [[249, 182, 79], [136, 198, 95], [159, 167, 6], [223, 136, 101], [66, 27, 77], [213, 234, 239], [25, 36, 53], [89, 113, 149], [65, 127, 119], [50, 63, 147], [204, 189, 228], [228, 229, 4], [64, 12, 191], [65, 176, 96], [185, 52, 207], [37, 24, 110], [62, 213, 244], [141, 59, 81], [166, 50, 189], [228, 5, 16], [59, 42, 251], [180, 239, 144], [13, 209, 132]]
res = [184, 161, 235, 97, 140, 111, 84, 182, 162, 135, 76, 10, 69, 246, 195, 152, 133, 88, 229, 104, 111, 22, 39]
seeds = [9925, 8861, 5738, 1649, 2696, 6926, 1839, 7825, 6434, 9699, 227, 7379, 9024, 817, 4022, 7129, 1096, 4149, 6147, 2966, 1027, 4350, 4272]

flag = ''
for i in range(len(res)):
    random.seed(seeds[i])
    rands = []
    for j in range(4):
        rands.append(random.randint(0, 255))
    key = rands[i % 4]
    del rands[i % 4]
    assert rands == rand_list[i]
    flag += chr(res[i] ^ key)

print(flag)
flag{Oppsie_LULZ_fixed}

Find Largest Triangle (Code 125)

三角形の面積は以下の式で求められる。

S = |(B - A) × (C - A)| /  2
※"×"は外積

これを使って、全組合せで面積の最大値を返すようスクリプトを組む。以下のスクリプトで通った。

import math
import itertools

def ext_prod(a, b):
    v_aa = a + a[0:2]
    v_bb = b + b[0:2]
    v_o =[]
    for i in range(len(a)):
        v_o.append(v_aa[i+1] * v_bb[i+2] - v_aa[i+2] * v_bb[i+1])
    return v_o

def calc_area(p1, p2, p3):
    pt1 = [e2 - e1 for e1, e2 in zip(p1, p2)]
    pt2 = [e3 - e1 for e1, e3 in zip(p1, p3)]
    prod = ext_prod(pt1, pt2)
    size = math.sqrt(sum([pow(x, 2) for x in prod]))
    area = size / 2
    return area

def FindLargestTriangleArea(points):
    max_area = 0
    for p in itertools.combinations(points, 3):
        area = calc_area(p[0], p[1], p[2])
        if max_area < area:
            max_area = area
    return max_area

# Reading space delimited points from stdin
# and building list of 3D points
points_data = raw_input()
points = []
for point in points_data.split(' '):
  point_xyz = point.split(',')
  points.append([int(point_xyz[0]), int(point_xyz[1]), int(point_xyz[2])])

# Compute Largest Triangle and Print Area rounded to nearest whole number
area = FindLargestTriangleArea(points)
print int(round(area))

We Need an Emulator (Code 150)

XOR, MOV, REVERSEのオペランドレジスタもTRX, DRXしかないので、全パターンをプログラムで作成する。

def xor(a, b):
    if len(a) > len(b):
        s1 = a
        s2 = b + '\x00' * (len(a) - len(b))
    else:
        s1 = a + '\x00' * (len(b) - len(a))
        s2 = b
    return ''.join(chr(ord(x) ^ ord(y)) for x, y in zip(s1, s2))

with open('Crypto.asm', 'r') as f:
    lines = [line.rstrip() for line in f.readlines()]

trx = 'GED\x03hG\x15&Ka =;\x0c\x1a31o*5M'
drx = ''

for line in lines:
    op = line.split(' ')[0]
    if op == 'XOR':
        arg1 = line.split(' ')[1]
        arg2 = line.split(' ')[2]
        if arg2 == 'TRX':
            arg2 = trx
        else:
            arg2 = drx
        if arg1 == 'TRX':
            trx = xor(trx, arg2)
        else:
            drx = xor(drx, arg2)
    elif op == 'MOV':
        arg1 = line.split(' ')[1]
        arg2 = line.split(' ')[2]
        if arg2 == 'TRX':
            arg2 = trx
        elif arg2 == 'DRX':
            arg2 = drx
        else:
            arg2 = eval(arg2)
        if arg1 == 'TRX':
            trx = arg2
        else:
            drx = arg2
    else:
        arg1 = line.split(' ')[1]
        if arg1 == 'TRX':
            trx = trx[::-1]
        else:
            drx = drx[::-1]

print trx
flag{N1ce_Emul8tor!1}

The only tool you'll ever need (Reverse Engineering 25)

$ strings a.out | grep flag
flag{str1ngs_FTW}
Enter the password to get the flag:
flag{str1ngs_FTW}

Stay Away Creepy Crawlers (Web App 25)

http://167.71.246.232/robots.txtにアクセスすると、フラグが書いてあった。

User-agent: *
Disallow: /admin/
# flag{mr_roboto}
flag{mr_roboto}

Source of All Evil (Web App 25)

HTMLソースを見ると、フラグが書いてあった。

flag{best_implants_ever}

Can't find it (Web App 25)

適当なパス(例:http://167.71.246.232/a)にアクセスすると、エラーページが表示され、フラグが書いてある。

flag{404_oh_no}

Show me what you got (Web App 25)

http://167.71.246.232/imagesにアクセスすると、indexが見えている。
http://167.71.246.232/images/aljdi3sd.txtにアクセスするとフラグが書いてあった。

flag{disable_directory_indexes}

Certificate of Authenticity (Web App 25)

httpsでアクセスし、証明書を見ると、フラグが書いてあった。
f:id:satou-y:20210227093330p:plain

flag{selfsignedcert}

Headers for you inspiration (Web App 25)

レスポンスヘッダにフラグがあった。

flag{headersftw}

Spring MVC 1 (Web App 25)

src/main/java/com/tenable/ctf/mvc/MainController.javaを見る。

	@GetMapping("/main")
        public ModelAndView getMain() {
		ModelAndView modelAndView = new ModelAndView("flag");
                modelAndView.addObject("flag", flags.getFlag("spring_mvc_1"));	// get main
                return modelAndView;
        }

GETメソッドで/mainにアクセスすればよい。http://challenges.ctfd.io:30542/mainにアクセスすると、フラグが表示された。

flag{flag1_517d74}

Spring MVC 2 (Web App 25)

src/main/java/com/tenable/ctf/mvc/MainController.javaを見る。

	@PostMapping("/main")
        public String postMain(@RequestParam(name="magicWord", required=false, defaultValue="") String magicWord, Model model) {
		if (magicWord.equals("please"))
			model.addAttribute("flag", flags.getFlag("spring_mvc_3"));	// post main param 
		else
                	model.addAttribute("flag", flags.getFlag("spring_mvc_2"));	// post main
                return "flag";
        }

POSTメソッドでmagicWordに"please"以外の値をセットして/mainにアクセスすればよい。

$ curl http://challenges.ctfd.io:30542/main -d 'magicWord=a'
<!DOCTYPE HTML>
<html>
<head>
    <title>Tenable CTF: Spring MVC</title>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
</head>
<body>
	<p >flag{flag2_de3981}</p>
</body>
</html>
flag{flag2_de3981}

Spring MVC 3 (Web App 25)

src/main/java/com/tenable/ctf/mvc/MainController.javaを見る。

	@PostMapping("/main")
        public String postMain(@RequestParam(name="magicWord", required=false, defaultValue="") String magicWord, Model model) {
		if (magicWord.equals("please"))
			model.addAttribute("flag", flags.getFlag("spring_mvc_3"));	// post main param 
		else
                	model.addAttribute("flag", flags.getFlag("spring_mvc_2"));	// post main
                return "flag";
        }

POSTメソッドでmagicWordに"please"をセットして/mainにアクセスすればよい。

$ curl http://challenges.ctfd.io:30542/main -d 'magicWord=please'
<!DOCTYPE HTML>
<html>
<head>
    <title>Tenable CTF: Spring MVC</title>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
</head>
<body>
	<p >flag{flag3_0d431e}</p>
</body>
</html>
flag{flag3_0d431e}

Spring MVC 4 (Web App 25)

src/main/java/com/tenable/ctf/mvc/MainController.javaを見る。

	@PostMapping(path = "/main", consumes = "application/json")
	public String postMainJson(Model model) {
                model.addAttribute("flag", flags.getFlag("spring_mvc_4"));	// post main flag json
                return "flag";
        }

POSTメソッドでapplication/jsonのコンテンツタイプを指定して/mainにアクセスすればよい。

$ curl http://challenges.ctfd.io:30542/main -H "Content-Type: application/json" -X POST
<!DOCTYPE HTML>
<html>
<head>
    <title>Tenable CTF: Spring MVC</title>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
</head>
<body>
	<p >flag{flag4_695954}</p>
</body>
</html>
flag{flag4_695954}

Spring MVC 5 (Web App 25)

src/main/java/com/tenable/ctf/mvc/MainController.javaを見る。

	@RequestMapping(path = "/main", method = RequestMethod.OPTIONS)
        public String optionsMain(Model model) {
                model.addAttribute("flag", flags.getFlag("spring_mvc_5"));	// options main
                return "flag";
        }

OPTIONSメソッドで/mainにアクセスすればよい。

$ curl http://challenges.ctfd.io:30542/main -X OPTIONS
<!DOCTYPE HTML>
<html>
<head>
    <title>Tenable CTF: Spring MVC</title>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
</head>
<body>
	<p >flag{flag5_70102b}</p>
</body>
</html>
flag{flag5_70102b}

Spring MVC 6 (Web App 25)

src/main/java/com/tenable/ctf/mvc/MainController.javaを見る。

	@RequestMapping(path = "/main", method = RequestMethod.GET, headers = "Magic-Word=please")
        public String headersMain(Model model) {
                model.addAttribute("flag", flags.getFlag("spring_mvc_6"));	// headers main
                return "flag";
        }

GETメソッドでHTTPヘッダに"Magic-Word: please"をセットして/mainにアクセスすればよい。

$ curl http://challenges.ctfd.io:30542/main -H 'Magic-Word: please'
<!DOCTYPE HTML>
<html>
<head>
    <title>Tenable CTF: Spring MVC</title>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
</head>
<body>
	<p >flag{flag6_ca1ddf}</p>
</body>
</html>
flag{flag6_ca1ddf}

Ripper Doc (Web App 50)

Cookieのauthenticatedの値をfalseからtrueに変更して、http://167.71.246.232/certified_rippers.phpにアクセスすると、フラグが表示された。

flag{messing_with_cookies}

Protected Directory (Web App 50)

http://167.71.246.232/.htpasswdにアクセスする。

admin:$apr1$1U8G15kK$tr9xPqBn68moYoH4atbg20

パスワードクラックする。

$ john .htpasswd           
Warning: detected hash type "md5crypt", but the string is also recognized as "md5crypt-long"
Use the "--format=md5crypt-long" option to force loading these as that type instead
Using default input encoding: UTF-8
Loaded 1 password hash (md5crypt, crypt(3) $1$ (and variants) [MD5 256/256 AVX2 8x3])
Proceeding with single, rules:Single
Press 'q' or Ctrl-C to abort, almost any other key for status
Warning: Only 2 candidates buffered for the current salt, minimum 24 needed for performance.
Warning: Only 20 candidates buffered for the current salt, minimum 24 needed for performance.
Warning: Only 23 candidates buffered for the current salt, minimum 24 needed for performance.
Warning: Only 21 candidates buffered for the current salt, minimum 24 needed for performance.
Warning: Only 13 candidates buffered for the current salt, minimum 24 needed for performance.
Almost done: Processing the remaining buffered candidate passwords, if any.
Warning: Only 15 candidates buffered for the current salt, minimum 24 needed for performance.
Proceeding with wordlist:/usr/share/john/password.lst, rules:Wordlist
Warning: Only 4 candidates left, minimum 24 needed for performance.
Proceeding with incremental:ASCII
alesh16          (admin)
1g 0:00:00:11 DONE 3/3 (2021-02-19 11:29) 0.08532g/s 88568p/s 88568c/s 88568C/s alesio2..alemeis
Use the "--show" option to display all of the cracked passwords reliably
Session completed

http://167.71.246.232/adminにアクセスし、以下の認証情報で認証すると、フラグが表示された。

admin / alesh16
flag{cracked_the_password}

Hackerman (Stego 25)

$ strings silly_hacker.svg | grep flag
       style="font-size:1.25px;fill:#808080">flag{m1cr0dot}</tspan></text>
flag{m1cr0dot}

Secret Images (Stego 125)

各RGBのXORを取ってみると、フラグが現れた。

from PIL import Image

o_img = Image.open('crypted1.png').convert('RGB')
e_img = Image.open('crypted2.png').convert('RGB')

w, h = o_img.size

output_img = Image.new('RGB', (w, h), (255, 255, 255))

for y in range(h):
    for x in range(w):
        r1, g1, b1 = o_img.getpixel((x, y))
        r2, g2, b2 = e_img.getpixel((x, y))
        output_img.putpixel((x, y), (r1 ^ r2, g1 ^ g2, b1 ^ b2))

output_img.save('flag.png')

f:id:satou-y:20210227095320p:plain

flag{otp_reuse_fail}

A3S Turtles (Stego 250)

$ fcrackzip -u -D -p dict/rockyou.txt turtles128.zip 


PASSWORD FOUND!!!!: pw == 0
$ unzip -P 0 turtles128.zip
Archive:  turtles128.zip
  inflating: turtles127.zip
$ fcrackzip -u -D -p dict/rockyou.txt turtles127.zip 


PASSWORD FOUND!!!!: pw == 0
$ unzip -P 0 turtles127.zip
Archive:  turtles127.zip
  inflating: turtles126.zip
$ fcrackzip -u -D -p dict/rockyou.txt turtles126.zip 


PASSWORD FOUND!!!!: pw == 1

パスワードは1桁の数字に決まっていそう。スクリプトを組んで試した結果はパスワードは0か1。turtle1.zipを解凍すると、key.pngが展開された。そこにはこう書いてある。

ed 57 0e 22 d4 58 e2 57 34 fc 08 d8 49 96 1d a9

問題のタイトルから考えると、AES暗号が関係しているのかもしれない。0, 1の128ビットのデータを暗号データとして、上記の鍵でAES-ECBの復号をしてみる。

import zipfile
from Crypto.Cipher import AES

def unzip_with_pwd(filename, path='.', pwd=''):
    with zipfile.ZipFile(filename, 'r') as zf:
        try:
            zf.extractall(path=path, pwd=pwd)
            return True
        except RuntimeError:
            return False

def unpad(s):
    return s[:-ord(s[-1])]

b_ct = ''
for i in range(128, 0, -1):
    fname = 'turtles%d.zip' % i
    for j in [0, 1]:
        pwd = str(j)
        res = unzip_with_pwd(fname, pwd=pwd)
        if res:
            b_ct += str(j)
            break

ct = ''.join([chr(int(b_ct[i:i+8], 2)) for i in range(0, len(b_ct), 8)])
key = 'ed570e22d458e25734fc08d849961da9'.decode('hex')

cipher = AES.new(key, AES.MODE_ECB)
flag = unpad(cipher.decrypt(ct))
print flag
flag{steg0_a3s}

Classic Crypto (Crypto 50)

Vigenere暗号。https://www.guballa.de/vigenere-solverで復号する。

There are many theories about us. That we’re anarchists, kids, crazy film-buffs that saw one too many superhero movies. The truth is, we are all these things. Anonymous is a symbol, like the flag a country flies. The flag is the symbol of the country. Our masks are our national identity. We are not Anonymous – we represent the ideals of Anonymous. Truth, freedom and the removal of censorship. Like any symbol, we affix it wherever we go, as you have seen from street protests.

We have no leaders, civilians or soldiers. We are all one. We run operations because that is what the group decides to do. We choose targets because that is what the people who represent the ideals of Anonymous want to fight for. The world is in trouble. We see it every day – war, poverty, murder. Every day we are bombarded with news and images, as we sit at home safe in the knowledge that we are powerless, that “better” minds are dealing with the situation.

But what if you could be the change you want to see? I’m twenty five years old. I went to school and college. I fought for my country then got a job and paid my taxes. If you met me on the street I wouldn’t even register on your radar. I am just another person in a sea of faces.

But in cyberspace we are different. We helped free the people of Egypt. We helped fight against Israel as it attempted genocide. We exposed more than fifty thousand paedophiles around the world. We fought the drug cartels. We have taken to the streets to fight for the rights you are letting slip through your fingers.

We are Anonymous.

The flag is "flag{classicvigenere}"

In today’s world we are seen as terrorists or at best dangerous anarchists. We’re called “cowards” and “posers” for hiding behind masks, but who is the real poser? We take away the face and leave only the message. Behind the mask we could be anyone, which is why we are judged by what we say and do, not who we are or what we have.

We exist without nationality, skin colour or religious bias.

復号した文中にフラグがあった。

flag{classicvigenere}

Easy Peasy (Crypto 50)

base64デコード、hexデコード、rot13で復号すると、フラグになった。

enc = 'NzMgNzkgNmUgNzQgN2IgNzAgNjIgNjEgNzQgNjUgNmUgNjcgNjYgNWYgNmMgNjIgNjggNWYgNzQgNjIgNjcgNWYgN2EgNzIgN2Q='
dec = enc.decode('base64')
print dec

dec = dec.replace(' ', '').decode('hex')
print dec

flag = dec.decode('rot13')
print flag

実行結果は以下の通り。

73 79 6e 74 7b 70 62 61 74 65 6e 67 66 5f 6c 62 68 5f 74 62 67 5f 7a 72 7d
synt{pbatengf_lbh_tbg_zr}
flag{congrats_you_got_me}
flag{congrats_you_got_me}

Netrunner Encryption (Crypto 200)

Source Codeをクリックすると、以下のようになっている。

<html>
<body>
  <h1>Netrunner Encryption Tool</h1>
  <a href="netrun.txt">Source Code</a>
  <form method=post action="crypto.php">
  <input type=text name="text_to_encrypt">
  <input type="submit" name="do_encrypt" value="Encrypt">
  </form>

<?php

function pad_data($data)
{
  $flag = "flag{wouldnt_y0u_lik3_to_know}"; 

  $pad_len = (16 - (strlen($data.$flag) % 16));
  return $data . $flag . str_repeat(chr($pad_len), $pad_len);
}

if(isset($_POST["do_encrypt"]))
{
  $cipher = "aes-128-ecb";
  $iv  = hex2bin('00000000000000000000000000000000');
  $key = hex2bin('74657374696E676B6579313233343536');
  echo "</br><br><h2>Encrypted Data:</h2>";
  $ciphertext = openssl_encrypt(pad_data($_POST['text_to_encrypt']), $cipher, $key, 0, $iv); 

  echo "<br/>";
  echo "<b>$ciphertext</b>";
}
?>
</body>
</html>

<入力文字列><フラグ>をAES-ECBで暗号化している。ソースコード中のflagやiv, keyは正しくないので、フラグを1文字ずつはみ出させながら、暗号を比較し平文を求める。

a
7ii97QpMPBNhknGD3igg4aKhzWdroplDGd0QSc/8pG5I/EjJ6OXYKXOVQPIDkUpB

aa
lCODIH4f3+rCvpYde8/oaAW0B7t/tMwBOtJadxcOFcZI/EjJ6OXYKXOVQPIDkUpB

aaa
HsoEuhN3CpdMSxbOv0MRcYHQ4lxtL157fCDm+Bq0zX5I/EjJ6OXYKXOVQPIDkUpB

aaaa
CdvUd/SwH2a6O4lgnmq5BeVLvLtBezfPf0Il0LGIZ+NI/EjJ6OXYKXOVQPIDkUpB

aaaaa
guFCH9dTJ1fBIZLA5iLGXZX8HlrVjaA+bkMjwPdrXXFI/EjJ6OXYKXOVQPIDkUpB

aaaaaa
zKMyyy5eAwCWk/Ui/PIlnmE0aprqGzDI8Ap/JyX8VY9I/EjJ6OXYKXOVQPIDkUpBSPxIyejl2ClzlUDyA5FKQQ==
0123456789abcdef
aaaaaaFFFFFFFFFF
FFFFFFFFFFFFFFFF
PPPPPPPPPPPPPPPP

0123456789abcdef
aaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaF
FFFFFFFFFFFFFFFF
FFFFFFFFFPPPPPPP
import requests
import re

url = 'http://167.71.246.232:8080/crypto.php'

flag = ''
for i in range(26):
    for code in range(32, 127):
        print '[+] try flag:', flag + chr(code)
        if i < 16:
            text = 'a' * (15 - i) + flag + chr(code) + 'a' * (63 - i)
        else:
            text = flag[-15:] + chr(code) + 'a' * (63 - i)
        payload = {'text_to_encrypt': text, 'do_encrypt': 'Encrypt'}
        r = requests.post(url, data=payload)
        body = r.text

        pattern = '<b>(.+)</b>'
        m = re.search(pattern, body)
        enc = m.group(1).decode('base64')
        ct0 = enc[16*0:16*1]
        ct4 = enc[16*4:16*5]
        if ct0 == ct4:
            flag += chr(code)
            break

print '[*] flag:', flag

実行結果は以下の通り。

        :
[+] try flag: flag{b4d_bl0cks_for_g0nksv
[+] try flag: flag{b4d_bl0cks_for_g0nksw
[+] try flag: flag{b4d_bl0cks_for_g0nksx
[+] try flag: flag{b4d_bl0cks_for_g0nksy
[+] try flag: flag{b4d_bl0cks_for_g0nksz
[+] try flag: flag{b4d_bl0cks_for_g0nks{
[+] try flag: flag{b4d_bl0cks_for_g0nks|
[+] try flag: flag{b4d_bl0cks_for_g0nks}
[*] flag: flag{b4d_bl0cks_for_g0nks}
flag{b4d_bl0cks_for_g0nks}

ECDSA Implementation Review (Crypto 225)

ECDSAのrが同じ場合の脆弱性の問題。kを計算できるので、それからsecretを計算できる。あとはそれを元にAESの復号をすればフラグになる。

#!/usr/bin/python3
from Crypto.Cipher import AES
from Crypto.Util.number import *
import binascii

def unpad(s):
    return s[:-s[-1]]

r = 50394691958404671760038142322836584427075094292966481588111912351250929073849
s1 = 26685296872928422980209331126861228951100823826633336689685109679472227918891
s2 = 40762052781056121604891649645502377037837029273276315084687606790921202237960

hash1 = 777971358777664237997807487843929900983351335441289679035928005996851307115
hash2 = 91840683637030200077344423945857298017410109326488651848157059631440788354195
order = 115792089210356248762697446949407573529996955224135760342422259061068512044369

k = int(((hash1 - hash2) % order) * inverse(((s1 - s2) % order), order))
secret = int((((((s1 * k) % order) - hash1) % order) * inverse(r, order)) % order)
print('[+] secret =', secret)

ct = b'f3ccfd5877ec7eb886d5f9372e97224c43f4412ca8eaeb567f9b20dd5e0aabd5'
ctxt = binascii.unhexlify(ct)

aes_key = secret.to_bytes(64, byteorder='little')[0:16]
IV = b'\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0'
cipher = AES.new(aes_key, AES.MODE_CBC, IV)
flag = unpad(cipher.decrypt(ctxt))
print('[*] flag =', flag)

実行結果は以下の通り。

[+] secret = 26924620604793025490002124762205825722410676804960639851404176074662508843402
[*] flag = b'flag{cRypt0_c4r3fully}'
flag{cRypt0_c4r3fully}