导语:有很多方法来爆破登录表单,你只需要google一下就可以在搜索的结果中看到一些通常的做法。在你使用Burp的情况下,这些搜索出来的爆破方式将足以满足大多数的形式爆破。
有很多方法来爆破登录表单,你只需要google一下就可以在搜索的结果中看到一些通常的做法。在你使用Burp的情况下,这些搜索出来的爆破方式将足以满足大多数的形式爆破。但有时候,爆破不会那么简单,你需要编写自己的工具。这可能是因为一些各种原因所致,但通常它归结为通过HTTP(S)的自定义协议或输入的数据的是一些自定义的加密算法。在这篇文章中,我们将介绍两种编写这些工具的方法:
1.编写你自己的python脚本 2. 一个Greasemonkey脚本
既然要编写这两种工具,那么首先你需要了解和分析非默认形式的登录表单,我们先来做一下分析。如果你想跟着我一起操作,你需要安装以下工具:
1. Python 2. Burp免费版 3. 安装了Greasemonkey插件的Firefox 4. FoxyProxy 5. FireFox开发工具(F12)
请注意,即使我们使用了一些商业可用的软件作为示例,但这并不是软件本身的一个漏洞。大多数登录表单都可以被爆破,只是某些登录表单的形式可能比其他形式慢一些;)像往常一样,你也可以跳过本博文的一些介绍,直接下载python脚本和Greasemonkey脚本。请注意,你可能需要根据你自己的情况对这些工具进行调整。
遇到的问题
有时你会碰到一些非常有趣的登录表单,就像 Milestone XProtect软件一样。我没有执行XProtect软件的任何配置,似乎它使用了Windows登录凭据进行身份验证。
我们的目标是你可以使用用户名和密码登录的软件的“Web客户端”。如果你正确配置了foxyproxy ,那么你的请求应该在打开XProtect登录表单之前进行拦截。请注意,如果你在localhost上运行XProtect,则可能需要配置Firefox,以避免绕过本地主机127.0.0.1的代理设置。
所以当你尝试登录时,你应该可以看到以下两个请求:
首次登录的请求
第二次登录的请求
现在,如果你尝试base64解码用户名和密码的值,最终会出现乱码,第一个请求中的第一个初始的base64字符串也是如此。另外,看起来似乎每个登录都需要发起两个请求。看来我们已经找到了问题所在并且需要进行进一步的分析。
开始分析
首先让我们看看每个请求和响应消息的数据。
这是第一个请求,我们可以发现不同的东西,可能有助于我们更好地了解我们输入的用户名和密码的值是由于什么样的转换所发生了篡改。以下XML参数引起了我的注意:
1. PublicKey 2. EncryptionPadding
这是第一个请求的响应,就像第一个请求一样,以下XML参数也引起了我的注意:
1. ConnectionId 2. PublicKey
响应中也包含其他一些有趣的信息,甚至有些信息可以被看作信息泄露,但为了我们本文所述的目的,我们将忽略这些敏感的信息。
上图的内容是实际包含我们可能加密的用户名和密码的请求。我唯一看到的XML参数是:
l ConnectionId
响应中包含了一个'<Result>'XML标签,可以让我们知道登录是否成功。因为我只截取了整个数据的下面的一部分,所以你在上图中看不到。
根据已经得知的信息,我们得出以下结论:
1. 客户端向服务器发送公钥
2. 服务器向客户端发送公钥
3. 魔术发生,凭证被加密
1. 显然加密模式使用了'ISO10126'进行填充
上面提到的还有什么吗?当然有! 不过这似乎是一本教科书Diffie Hellman密钥交换。填充模式表示加密最有可能是一个块密码,因为如果你去谷歌一下的话,你会发现维基百科的这篇文章。如果我们进行一些积极的探索,我们可以推断出更多的信息。如果我们输入一个'a'作为密码,然后对该值进行解码,则它将变为16个字节长。为了清楚起见,我添加了空格:
输入:a
Base64:qlqsMXD7uS / Kl15iyIIlxA ==
解码后的字节:aa 5a ac 31 70 fb b9 2f ca 97 5e 62 c8 82 25 c4
如果我们输入少于16个'a'字符,它仍然是16个字节长度。
输入:aaaa aaaa aaaa aaa
Base64:PIV8H1Rg3KuVi + GyhYsPsg ==
解码后的字节:3c 85 7c 1f 54 60 dc ab 95 8b e1 b2 85 8b 0f b2
但是,如果我们输入16个'a'或更多字符,则会变为32个字节长度。
输入:aaaa aaaa aaaa aaaa a
Base64:G3vCXn54ZV6gHq9hxV + S0E1Fs619AccEnvq2WMRKPMQ =
解码字节:1b 7b c2 5e 7e 78 65 5e a0 1e af 61 c5 5f 92 d0 49 45 b3 ad 7d 01 c7 04 9e fa b6 58 c4 4a 3c c4
这表明它最有可能是一个16位字节的块密码。所以当你将其转换为位(8 * 16)时,会产生128bit的块,即使我们没有进一步的证据来证明,但是这可能会让你想到AES加密算法。
确切的块密码算法以及确认它确实是一个Diffie Hellman密钥交换的方式,我们将在我们尝试编写我们的爆破脚本时弄清楚。
编写我们的python脚本
现在我们至少有了一些我们必须需要实现的想法,让我们开始工作吧。在这种情况下,这不意味着单纯的进行编码开发,而是意味着我们需要进一步深入加密的内部工作原理,然后再做一些编码开发工作。
由于所有的加密都在浏览器中发生,所以了解一下相关的javascript是一个很好的开始。当你查看/js/文件夹中的文件时,你可以很快看到'main.js'很可能包含了所有的逻辑,因为这个js文件很大。你要做的第一件事是格式化javascript代码,使用开发人员工具中的内置工具,或者使用自定义的插件,比如你最喜爱的编辑器,如Atom或Sublime。
在格式化JS代码之后,有几种不同的策略来定位我们感兴趣的代码,我最喜欢的代码之一是搜索任何之前识别到的加密字符串,如“填充”,“ISO10126”或者搜索默认的加密字符串,如“encrypt ','decrypt','aes','diffie hellman','random'。所有这些搜索字词都会在我们正在查看的main.js文件中查找到密码。让我们看看我们该如何理解这一点,而不是需要去充分了解所有的代码。
兼容性检查
当我在处理跨编程语言的加密实现时,我学到的一件事是:要记住,实现可能不太一样,你应该为一些长的调试会话做好准备。为了避免这种情况,我总是试图找到一个易于实现,但是又非常重要的加密技术,并且仅实现该部分来验证加密是否兼容。虽然这不是百分之百需要做的测试工作,但它确实能给你一些洞察力。
对于这种兼容性测试,我首先选择输入用户名和密码的加密,前提是假设它可能是用AES加密的并且易于实现。假定的逻辑如下:
1.调试javascript并查找AES操作 2.提取加密密钥 3.创建python解密代码
启动开发人员工具,并在以下位置打个断点:
1. loginSubmit:function(a){ 2. Connection.login(a) 3. aP.Username = at.dh.encodeString(aP.Username); 4. aP.Password = at.dh.encodeString(aP.Password)
如果你想知道为什么在这些地方打断点,那么当你查看页面的HTML源码时,你会看到表单和提交按钮的“onsubmit”和“onclick”事件设置为了“loginSubmit”。如果你随后转到“main.js”文件并搜索该字符串,那么你就可以在同一个地方找到它。使用一些很好的老式阅读方式(从该行往下读)并应用一些试错的点,你可以遵循代码执行流程,并发现上述有趣的函数调用。在调试期间,我注意到,Chrome开发人员工具似乎比Firefox更好用,如:实际触发的断点。
当你单步调试(有时需要选择into)这些函数时,你应该能够看到输入被加密的代码点。所以如果我们进入'encodeString'函数,我们就可以看到加密字符串的源代码:
this.encodeString = function(r) { var o = this.getSharedKey().substring(0, 96); var n = CryptoJS.enc.Hex.parse(o.substring(32, 96)); var m = CryptoJS.enc.Hex.parse(o.substring(0, 32)); var q = { iv: m }; if (Settings.DefaultEncryptionPadding && CryptoJS.pad[Settings.DefaultEncryptionPadding]) { q.padding = CryptoJS.pad[Settings.DefaultEncryptionPadding] } return CryptoJS.AES.encrypt(r, n, q).ciphertext.toString(CryptoJS.enc.Base64) }
如果你阅读了上面的代码,那么你可以得出以下结论:
1. o可能是diffie hellman密钥交换的结果 2. q&m是IV 3. n是实际的加密密钥 4. r是要加密的字符串
所以这意味着如果我们从调试器中获取到加密值,并且使用'o'变量的值,我们应该就能够解密它。你可能会喜欢这样的操作,但是你缺少操作方法!是的,你是正确的,但我们的目标是不完全理解所有的代码,所以让我们直接来操作吧;)要运行下面的代码片段,你只需要执行“pip install pycrypto”和“pip install Padding”。
#decrypt values if key is known encdata = base64.urlsafe_b64decode('9OTg1OvudO7jOYOrnkttMA==') aesrawkey = '6b0df8a5406348aab2aa0883c3b3f4e55b45e00ad6959f7468e25e88c3eb166a3ee8934ceda08e4116b7afc05eae4d6c' aeskey = aesrawkey[32:96].decode('hex') aesiv = aesrawkey[0:32].decode('hex') cipher = AES.new(aeskey, AES.MODE_CBC, aesiv ) print removePadding(cipher.decrypt(encdata),16,'Random')
如果你运行了上述代码,应该会打印出'sdf',这是我输入到用户名字段中的用户名。现在从加密的结果来看,这意味着加密似乎是兼容的,不需要任何特殊的努力。
diffie hellman实现
我们所需要的核心是diffie hellman密钥交换(DHke)的实现。原因是实际的加密密钥是从这里派生的,所以没有它,我们注定要失败。在上一段中,我们已经发现了一个DHke函数:getSharedKey(),如果你进入了该函数,然后向上滚动到在所有的DHke代码的中间,即:
创建私钥
var g = randBigInt(160, 0);
创建公钥
this.createPublicKey = function() { var n = b(e(bigInt2str(powMod(d, g, f), 16))); n.push(0); var m = Base64.encodeArray(n); return m }
创建共享密钥
this.getSharedKey = function() { var m = b(e(bigInt2str(powMod(str2bigInt(l, 16, 1), g, f), 16))); return CryptoJS.enc.Base64.parse(Base64.encodeArray(m)).toString() }
如果你了解Diffie Hellman算法,你可以将它的算法代码移植到python脚本中,在脚本中,你可以阅读完整的代码实现。以下是python中的三个函数:
def genprivkey(): return getrandbits(160) def genpubkey(g,prkey,prime): pubkey = pow(g, prkey, prime) packedpubkey = pack_bigint(pubkey) return base64.b64encode(bytes(packedpubkey)) def gensharedkey(rpubkey, privkey, prime): decrkey = base64.b64decode(rpubkey) rkey = unpack_bigint(decrkey) sharedkey = pow(rkey,privkey,prime) return sharedkey
我遇到的最大的陷阱是你需要使用大数的方式,如果你想对它们进行编码,将它们拼接成不同的字节等等,你就必须使用big int。你必须打包/解包它们!以下是从stackoverflow复制来的代码段:
def pack_bigint(i): #https://stackoverflow.com/a/14764681 b = bytearray() while i: b.append(i & 0xFF) i >>= 8 return b def unpack_bigint(b): #https://stackoverflow.com/a/14764681 b = bytearray(b) # in case you're passing in a bytes/str return sum((1 << (bi*8)) * bb for (bi, bb) in enumerate(b))
随着diffie hellman密钥交换核心代码的实现,脚本的其余部分就不言自明了。它创建了XML请求,进行必要的解析以读取/写入值并发送/接收请求。脚本有点暴力…但是为了演示这个过程,请忽略吧。:)
Greasemonkey:最后的手段
如果你之前的分析失败了或是一些奇怪的加密互操作导致错误发生了,那么在这种情况下,有一个普遍使用的可选方式,并不要求我们完全了解到底发生了什么。当然,它也有一些缺点,如执行速度比较慢。
我选择的通用解决方案是创建一个Greasemonkey脚本,因为这样可以控制窗体,就像你输入凭据一样,手动提交它们。Greasemonkey脚本最重要的限制是,据我所知,你不能(轻松地)读取本地文件以用作你的暴力尝试的输入。这将导致脚本中需要嵌入用于爆破尝试的用户名和密码。这个脚本可以很容易地适应于其他爆破登录表单。
上面的屏幕截图显示了Greasemonkey脚本的执行结果,你可以在其中看到添加的按钮来启动爆破,以及在开发人员工具控制台中滚动显示当前的尝试。我们需要为脚本实现的逻辑非常简单:
1. 在网站上显示一个按钮,点击后开始爆破 2.循环使用用户名和密码组合 3. 尝试登录 4. 检测登录尝试是否成功
上述逻辑的实现并不困难,只需要了解一些浏览器和JavaScript的知识(事件驱动的编程)。幸运的是我们可以在stackoverflow中搜索代码,我们不需要将它变成世界上最华丽的脚本(从而可以忽略事件驱动方法的一部分)。我将从脚本中解释一些有趣的亮点:
尝试登录,休眠,再次尝试
这是我创建的比较挫的代码的一部分,因为我并没有想到JavaScript没有sleep函数。阅读完代码之后,我尝试使用sleep的替代方法,因为它会挂起你的浏览器。所以我最后将整块代码包裹在一个函数并且每隔5秒使用 setInterval函数调用这个函数。这当然意味着我必须保持某种形式的状态,因此我设置的全局变量都变得疯狂(更确切地说,我用while循环和全局变量替换了循环,这样我可以在每次尝试后退出while循环,但是恢复循环后,函数会再次被调用。
我对实际提交的表单进行了一些搜索和阅读,因为我在大多数示例中遇到的默认方法并不起作用。通常你可以收集表单元素或提交按钮元素,并调用click()或submit()函数。我没有完全弄清楚为什么没有起作用,但是对于这种具体的形式,它并没有奏效。以下代码是起作用的,应该也是普遍的做法:
//https://stackoverflow.com/a/6337307 var evt = document.createEvent ("HTMLEvents"); evt.initEvent ("click", true, true); document.getElementById('loginWindow_submit').dispatchEvent(evt);
检测登录是否成功
//https://stackoverflow.com/a/2497223 var oldTitle = document.title; window.setInterval(function() { if (document.title !== oldTitle) { console.log("[!!!!!!!!] YAY! "+currUsr+" "+currPwd); if (currUsr != ""){ foundcreds = 1; } } oldTitle = document.title; }, 100); //check every 100ms
上述代码使用setInterval函数来检查每100毫秒页面标题的更改。你也可以通过订阅事件来解决这个问题,但这似乎更容易理解并且更通用一些,因为大多数网站在成功登录时都会更改标题。
脚本的其余部分比较简单,尽管我还没有测试,但应该是可重用的。这些类型的脚本的开发并不需要很长时间,在我看来,当密码加密操作变得很复杂或者是自定义加密算法或者你没有足够的时间来处理它们时,这是一个很好的可选方案。
结论
感谢你能花时间来阅读这篇博文!我希望你学习了一种额外的爆破某种特别的数据加密登录表单的方法。像往常一样,工具的实现比工具本身更加有趣:)