Hitcon 2018 Web 题解

Oh My Raddit

题目一开始直接告诉我们 flag 的格式:

Flag is hitcon{ENCRYPTION_KEY}

再结合题目所给的 提示assert ENCRYPTION_KEY.islower(),我们可以明白本题的考点:猜测本题使用的加密算法,并破解其使用的密钥。

继续来看题目,可以看到题目中的所有链接均不是直接指向网站自身的 URL,而是向服务器进行请求:

<a href="?s=8c762b8f22036dbbdda56facf732ffa71c3a372e4530241246449a55e25888cf98164f49a25f54a84ea0640e3adaf107cc67c8f2e688e8adf18895d89bfae58e33ae2e67609b509afb0e52f2f8b2145e">50 million Facebook accounts owned</a>

但当我们访问 http://13.115.255.46/?s=8c762b8f22036dbbdda56facf732ffa71c3a372e4530241246449a55e25888cf98164f49a25f54a84ea0640e3adaf107cc67c8f2e688e8adf18895d89bfae58e33ae2e67609b509afb0e52f2f8b2145e 时,浏览器却访问了 https://newsroom.fb.com/news/2018/09/security-update/ 所以在这里必然存在着服务器上的一个处理操作。

通过查看 burp 拦截的 Response,可以看到服务器实际上返回的是一个 303 See Other,再由浏览器跳转到指定的 URL。

HTTP/1.1 303 See Other
Date: Mon, 22 Oct 2018 07:24:06 GMT
Server: localhost
Content-Type: text/html
Location: https://newsroom.fb.com/news/2018/09/security-update/
Connection: close
Content-Length: 0

那么我们可以这样假设,8c762b8f22036dbbdda56facf732ffa71c3a372e4530241246449a55e25888cf98164f49a25f54a84ea0640e3adaf107cc67c8f2e688e8adf18895d89bfae58e33ae2e67609b509afb0e52f2f8b2145e 实际上是对应网页的数据在某种加密过后得到的十六进制字符串,我们可以通过二者之间的明密文关系来推测使用的加密算法和密钥。

继续来看,我们可以发现修改请求参数中的部分字节(前 48 个字节之后)不影响我们得到的结果,比如访问 http://13.115.255.46/?s=a2be4d31c8cbf83fc7be364caf7dae82b50a6fb6362320e888208fded4e2d881716d81db5701860df8c8c72896dc5bc30000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000074a3bfe5 ,同样能获得正确的结果。

Date →Mon, 22 Oct 2018 07:33:53 GMT
Server →localhost
Content-Type →text/html
Location →https://medium.com/@Wflki/sql-injection-oracle-and-full-width-characters-13bb86fc034a
Keep-Alive →timeout=5, max=100
Connection →Keep-Alive
Transfer-Encoding →chunked

所以可以猜测,明文的前半部分应该是每个网页所对应的 id,服务器会根据 id 返回对应的网址,所以该部分的修改会影响结果的显示,而后半部分的结果不影响结果的显示,则可能对应网页的 title,因为我们注意到 title 长度不同的网页对应的密文长度也不同,而且二者成正相关。

我们继续观察 title 和密文的关系,可以观察到存在着相同字符子串的 title 中的密文中也存在部分相同的子串,如

<a href="?s=4b596c43212b27b7c948390491293dd24f6f5f3b635ddb984c1c23f162d392ccf900061d8b6338771d8feb029243ed633882b1034e8789849136472bd93ffe2dfd8017786de53c1785a67bbbcecad1c78b096aa66c3ff957aaa3bb913d35c75f">Bypassing Web Cache Poisoning Countermeasures</a>

<a href="?s=b0b7a350f4a4f27848b204d056b25fb0f785e6357390b3bc73bbbbffc6bf5071b47143690fe718f21d8feb029243ed633882b1034e878984233b2d964a4138bbfe4bcb8834342001d2446e0f6d464355833f3b6c39beee1bfd5d3bce98966870">Bypassing WAFs and cracking XOR with Hackvertor</a>

title 存在着相同的子字符串 Bypassing W,而密文则存在着相同的子字符串 1d8feb029243ed633882b1034e878984,结合上文的猜测,可以看出 assing W3882b1034e878984 对应。

可以注意到密文块很明显的以 8 bytes 为一组,且各组间相互独立,后续块的加密不受前面块的影响

所以结合以上的分析,可以有以下两个推测:

  1. 本题使用的 DES 加密,且使用的是 ECB 模式
  2. 密钥长度为 8,且均为小写字母

所以我们能得到对应明密文对 3882b1034e878984:617373696e672b57 (题目使用了 + 代替空格,被坑了很久。。另外感谢评论区的师傅帮我纠正了这里的一个错误 orz)

为了加深理解,我们可以看上面这张对比图,可以看到,第一条的左右两个字符串,共同的子字符串为 Bypassing W,加密后的文本存在两个 8 字节的共同子串;第三条左右两个字符串共同字串为 Bypassing,对应处的共同子字符串仅为 1 个,所以很明显 W 是对应块的最后一个字母,然后可以从 W 开始倒着读 8 个字符,所以得到 assing W

看了大佬们的解答,发现他们用的是另一个明密文对:3ca92540eb2d0a42:0808080808080808,即 DES padding 的字符和对应的密文。

然后我们可以写脚本进行爆破,由于脚本写的太慢就不往上贴了-_-^ 10min 单线程完全爆不动

后来看了 orange 大佬的解答才知道可以使用 hashcat 爆破,速度快多了(哭泣

.\hashcat64.exe -m 14000 42aa7c80bae5f78f:6e6a656374696f6e -a 3 '?l?l?l?l?l?l?l?l' --show
42aa7c80bae5f78f:6e6a656374696f6e:ldgonaro

直接提交发现 flag 错误,这是因为由于 DES 只使用了 64 bits 中的 56 bits 做校验,所以实际上每个字符存在着另一个等效的字符,由等效字符替换后的密钥依旧是有效的。所以我们可以爆破所有可能的 key 并提交(当然太粗暴了)。另一个思路是根据题目的提示 P.S. If you fail in submitting the flag and want to argue with author, read the source first! 去获得题目的源代码。

我们观察到题目中实际上应该存在着三种链接,如

  1. 06e77f2958b65ffd3ca92540eb2d0a42,解密后的明文是 m=p&l=100
  2. 59154ed9ef5129d081160c5f9882f57dcfd76f05f6ac8f1a38114a30fb1839a27fea88c412d9e1149dedcb1c01c0a6662a36d91fd8751e52ba939a65efbe150f9504247abb9fe6be24d3d4dcfda82306,解密后的明文是 u=f90b0983-23fc-42ae-a333-019b6593da75&m=r&t=An+Innovative+Phishing+Style
  3. 2e7e305f2da018a2cf8208fa1fefc238522c932a276554e5f8085ba33f9600b301c3c95652a912b0342653ddcdc4703e5975bd2ff6cc8a133ca92540eb2d0a42,解密后的明文是 m=d&f=uploads%2F70c97cc1-079f-4d01-8798-f36925ec1fd7.pdf

很明显第三种链接能让我们下载对应的文件,因此我们构造 m=d&f=app.py 对应的密文 e2272b36277c708bc21066647bc214b8,成功获得源代码:

# coding: UTF-8
import os
import web
import urllib
import urlparse
from Crypto.Cipher import DES

web.config.debug = False
ENCRPYTION_KEY = 'megnnaro'


urls = (
    '/', 'index'
)
app = web.application(urls, globals())
db = web.database(dbn='sqlite', db='db.db')


def encrypt(s):
    length = DES.block_size - (len(s) % DES.block_size)
    s = s + chr(length)*length

    cipher = DES.new(ENCRPYTION_KEY, DES.MODE_ECB)
    return cipher.encrypt(s).encode('hex')

def decrypt(s):
    try:
        data = s.decode('hex')
        cipher = DES.new(ENCRPYTION_KEY, DES.MODE_ECB)

        data = cipher.decrypt(data)
        data = data[:-ord(data[-1])]
        return dict(urlparse.parse_qsl(data))
    except Exception as e:
        print e.message
        return {}

def get_posts(limit=None):
    records = []
    for i in db.select('posts', limit=limit, order='ups desc'):
        tmp = {
            'm': 'r', 
            't': i.title.encode('utf-8', 'ignore'), 
            'u': i.id, 
        } 
        tmp['param'] = encrypt(urllib.urlencode(tmp))
        tmp['ups'] = i.ups
        if i.file:
            tmp['file'] = encrypt(urllib.urlencode({'m': 'd', 'f': i.file}))
        else:
            tmp['file'] = ''

        records.append( tmp )
    return records

def get_urls():
    urls = []
    for i in [10, 100, 1000]:
        data = {
            'm': 'p', 
            'l': i
        }
        urls.append( encrypt(urllib.urlencode(data)) )
    return urls

class index:
    def GET(self):
        s = web.input().get('s')
        if not s:
            return web.template.frender('templates/index.html')(get_posts(), get_urls())
        else:
            s = decrypt(s)
            method = s.get('m', '')
            if method and method not in list('rdp'):
                return 'param error'
            if method == 'r':
                uid = s.get('u')
                record = db.select('posts', where='id=$id', vars={'id': uid}).first()
                if record:
                    raise web.seeother(record.url)
                else:
                    return 'not found'
            elif method == 'd':
                file = s.get('f')
                if not os.path.exists(file):
                    return 'not found'
                name = os.path.basename(file)
                web.header('Content-Disposition', 'attachment; filename=%s' % name)
                web.header('Content-Type', 'application/pdf')
                with open(file, 'rb') as fp:
                    data = fp.read()
                return data
            elif method == 'p':
                limit = s.get('l')
                return web.template.frender('templates/index.html')(get_posts(limit), get_urls())
            else:
                return web.template.frender('templates/index.html')(get_posts(), get_urls())


if __name__ == "__main__":
    app.run()

所以 flag 为 hitcon{megnnaro}

Oh My Raddit v2

首先看第二题的提示 Give me SHELL!!!,很明显考点是 getshell,所以第一步是代码审计。

可以看到使用的是 web.py 框架,然后根据获得的 requirements.txt 可以得到版本为 0.38。

然后再看代码,题目只处理 GET 请求,然后根据 ?s= 后跟的参数的不同,有三种不同的处理方式:

  1. r: 根据网页的 id 获得 url 并构造 303 响应
  2. d:根据文件名读取文件
  3. p:根据参数获得渲染首页

第一步想到的思路是直接构造 {'m':'d','f':'/flag'} 来阅读 flag 文件,发现被禁止了(毕竟本题考点是 getshell),那么下一步还是需要思考如何 getshell。

可以搜到相应的文章:Remote Code Execution in Web.py framework(ps:其实比赛的时候根本没搜到,看了 orange 的说明才找到的,还是太菜了

点击收藏 | 0 关注 | 1
登录 后跟帖