有很多方法来爆破登录表单,你只需要 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
响应中包含了一个 ''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