我与代码审计[一]
- 背景
好久没写文章了,来写一篇水文( PS: 为了对得起观众,文章中会送肉鸡和送福利 – 需要自己动手)。 2016 年对自己最大的愿望就是养成看书的习惯,改掉以前只买书不看书的“坏习惯”。 2016 年看的第一本书是 Seay 法师的代码审计的书。还记得当程序员时的梦想 : “要把开发和安全结合起来,写出安全的代码!”,虽然现今早已不走在开发的路上,但这个梦还是值得去追寻。
- 起因
上周我司 XX 群突然被一张聊天截图炸开了锅,从该图上的信息可以看到从某代码托管平台发现了我司 WAF 产品相关源码以及同事的内网账号信息。这一爆炸性新闻直接把总裁办的领导吸引到了群里,于是各路大牛们纷纷上各大代码托管平台搜索着我司的信息。
像这样的因为企业员工使用互联网公开的代码托管平台不当,导致企业敏感信息泄露的案例多不甚数,从 WooYun 上搜索了一下关键字: Github 得到 591 条记录,截图如下:
可见对于企业来说,自己员工的安全意识有必要提升加强!
- 获取源码
获取源代码的最好途径就是从公开的代码托管平台去寻找或者从目标的 svn|Git 泄露获得源码。从 Github 上获取源码简单的用法就是打开 Github 网站,选中左边区 “Code”, 表示我们只搜索代码,下面的 “ Language ” 表示偏好搜索的语言类型,然后输入关键字进行搜索。下面是示意截图:
一般思路是从乌云等漏洞平台上找目标企业已经泄露的内部职员账号,手机号,内部邮箱,内部系统名称,或服务器信息等等,然后作为关键字放到代码托管平台进行搜索信息。 从搜出来的信息中,进一步去打开分辨提取有用的信息,这里贴一篇前辈写的检索语法参考文章: Github 上寻找敏感信息技巧分享 -getshell1993
这里是一个例子:捡到一只规模还不小的游戏公司的运维狗的 github,
Github 地址: https://github.com/jokimina/script ( 已告知对方删除该 Github 项目 )
截图如下:
(目测这是一个运维汪的 Github ,不同的人拿到这个有不同的用处,我拿着呢就是学习学习脚本,里面的更多信息没有深度挖掘。提醒下看到这篇文章的无论程序猿、运维汪、还是工程狮们请一定要注意保护自己企业的隐私,如果因为自己的安全意识不强导致企业遭受巨大损失就亏大了!)
我在 github 找到了这个地址:
通过查看源代码初步分析,找到并确认了正在线上运行的网站地址:
于是开始了下面的故事。
- 审计源码
1.做好审计前的准备
在拿到目标的源码后,我马上打开熟悉的 Eclipse 工具、 import 导入项目、根据是否乱码调整 IDE 环境编码,一般审计的时候很难取得与线上的网站一样的运行环境,所以更多的时候是不能代码在本地模拟线上环境正常跑起来的,所以一些报错缺少包的问题就没管了。
2.预编译模型防 SQL 注入正确用法
大概扫了一下项目的构成,这里没有用上 Java 的重量级框架,就是一个简单的 Java Web 项目。 WebRoot 目录下面除了 WEB-INF/*
和 META-INF
外都是可以用浏览器直接访问的文件。关于 Java Web 基础可以见园长的文章: JavaWeb 应用 [1]-JavaEE 基础
对于 Java Web 项目,可以看每一个 jsp 页面里面的 Servlet 地址。一般来说请求的 Servlet 地址,在服务器端都有个名字差不多的完成请求和响应的 JavaBean 类。这个准确的对应关系我们不能靠猜,可以去 WEB-INF/web.xml
文件里去看映射关系。
比如下面:
举例如下面的这段代码:
<servlet-mapping>
<servlet-name>dealQuery</servlet-name>
<url-pattern>/dealQuery.do</url-pattern>
</servlet-mapping>
<servlet>
<display-name>jsonSevlet</display-name>
<servlet-name>dealQuery</servlet-name>
<servlet-class>myServlet.dealQuery</servlet-class>
</servlet>
从上面的代码我们可以知道,当浏览器请求 /dealQuery.do 这个地址时,由服务器端 myServlet 包下 dealQuery 这个 java 类来完成客户端的请求和响应。这个项目的所有处理的 Servlet 类都放在 myServlet 文件夹下面,
知道这个流程后,我开始检查 myServlet 文件下每一个处理的 Servlet 类,有如下的 java 文件:
第一个 dealCoorrect.java 文件内容如下:
public void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
request.setCharacterEncoding("utf-8");
response.setCharacterEncoding("utf-8");
PrintWriter out = response.getWriter();
JSONObject ms = new JSONObject();
HttpSession ss = request.getSession();
usInformation usInf = (usInformation)ss.getAttribute("usInf");
String CSId,CSName,CSAddr,CSDate,CSMode,CSFast,CSlow,Operator,ParkFee,CSPub,CSState,CSPhone,CSNotes;
System.out.println("进入dealCorrect");
CSId=new String( request.getParameter("CSId"));
CSName=new String( request.getParameter("CSName"));
CSAddr=new String( request.getParameter("CSAddr"));
CSDate=new String( request.getParameter("CSDate"));
CSMode=new String( request.getParameter("CSMode"));
CSFast=new String( request.getParameter("CSFast"));
CSlow=new String( request.getParameter("CSlow"));
Operator=new String( request.getParameter("Operator"));
ParkFee=new String( request.getParameter("ParkFee"));
CSPub=new String( request.getParameter("CSPub"));
CSState=new String( request.getParameter("CSState"));
CSPhone=new String( request.getParameter("CSPhone"));
CSNotes=new String( request.getParameter("CSNotes"));
String usId = usInf.getUsId();
这里定义了不同的字符串 CSId , CSName , CSAddr , CSDate , CSMode , CSFast , CSlow , Operator , ParkFee , CSPub , CSState , CSPhone , CSNotes ; 用来表示从请求中获取到的不同参数的值。
往下:
看到了熟悉的 insert 操作的 sql 语句。一条正常的 SQL 语句在放入数据库引擎中执行前,是会先经过各种词法错误、语义逻辑等识别和分析。通常的 SQL 注入也就是通过构造参数利用 SQL 语句在解析时解析引擎对参数中特殊符号的解析产生了偏差,使得一条唯一性的 SQL 语句变成了多重语义的执行语句,从而达到攻击者想要的目的。
虽然这里从请求中获取的变量值没有经过任何的处理直接带入了 SQL 语句,但是这里使用了参数绑定的预编译模型 , 也就是使用?占位符的帮助下这条 Insert 的语句执行模型已经编译确定了。按照预编译好的模型使用 setter 方法将参数值依次代替占位符?并进行执行。在 Java 中 SQL 注入对于像使用 PreparedStatement 预编译模式是无效的 , 这是因为 PreparedStatement 不允许在插入值时改变查询的逻辑结构。
所以得出结论
结论:这个Servlet处理是不存在SQL注入漏洞的!
3.预编译模型之错误使用导致 SQL 注入漏洞
上面的代码中,开发人员虽然没有过滤 SQL 注入的安全意识,但是使用的方法却有效的把 SQL 注入漏洞拒之门外了。怀着忐忑的心继续往下一个处理 Servlet 类开始看。
这个类同样的还是定义了很多字符串变量接收浏览器传递来的参数的值,未获取到值的变量赋予默认值。
从上面代码中可以看到创建了一个数据库实例对象,定义了 condition 字符串,这是一个 select 查询语句,用 StringBuffer 来拼接 sql 语句。先追踪 dataBase 类可以看到:
看到了SqlServer数据库连接信息,用的 sa 用户,第一次见到数据库名是汉字的 !!! telnet 试了下 1433 端口不能连接
于是继续看代码
拼接完 SQL 查询语句后,用拼接后的 SQL 语句创建了一个预编译对象并执行。这里犯了一个很多程序员都会犯的错 —— 错误的使用了 prepareStatement 对象。可能大多数程序员知道使用预编译对象可以防 SQL 注入,但是很多人都理解为创建一个预编译对象,然后用它的 executeQuery 来执行 SQL 语句就能防止 SQL 注入了。这样的想法是错误的,拼接后的语句再放入预编译对象是徒劳的,因为在预编译之前拼接的 SQL 语句执行逻辑已经被破坏,原 SQL 语句的本意已经被改变了。
所以得出下面结论:
结论:错误使用PreparedStatement预编译对象导致这个查询功能存在SQL注入漏洞!
4.退出系统功能
当用户请求退出系统时,浏览器会使用 GET 请求地址 /dealExit.do ,处理退出的功能由上面的代码实现。这里从 Http 请求的 Session 会话中获取参数 usInf, 如果获取到就使用 removeAttribute 从 session 中移除获取到的 usInf 。这里销毁的 usInf 是从当前会话中获取到的,所以这里的退出功能不存在越权漏洞。
所以得出如下结论:
结论:不存在漏洞
5.用户是否已注册判断存在 SQL 注入
在注册页面输入任意用户名 admin123, 看一下下发出去的请求: http://www.ezchong.com/dealPhoneMessage?act=checkIsReg&usId=admin123
通过查看 web.xml 映射关系找到这个逻辑处理代码在: dealPhoneMessage.java 类里:
打开文件找到 checkIsReg 函数:
可以看到传入的 usId 参数被直接拼接进了 checkCondition 字符串定义的 select 语句,然后把这条 sql 语句作为参数传入数据库帮助类的 query() 函数,这个函数代码如下:
这里的 query() 函数接收的参数是: sql 字符串 ,和 可变长参数 pras 。不得不说这个函数写的真的漂亮,这里使用了 String… 的特性 , 使得作者可以写更少的代码,满足更多的场景做更多的事。这个函数可以接收下面的方式的调用:
下面是用 prepareStatement 对象预编译这条 SQL 语句后,用一个 for 循环将参数绑定,这里的代码正好体现了预编译对象参数绑定思想。
但是这里判断用户是否注册时,传入的是一个已经封装好的 SQL 语句,参数没有经过预编译再绑定,所以这里很明显存在 SQL 注入漏洞。
1、手工验证:
分别请求:
http://www.ezchong.com/dealPhoneMessage?act=checkIsReg&usId=admin123%27or%271%27=%272
和
http://www.ezchong.com/dealPhoneMessage?act=checkIsReg&usId=admin123%27or%271%27=%271
第二个截图传值:admin’or’1’=’1 这是一个典型的SQL注入绕过参数。
1、SQLMAP验证:
此时已经无需多说,漏洞就摆在这里了!
6.用户注册逻辑绕过实现任意用户注册
首先看到用户注册处理逻辑的代码截图如下:
依我我审计水平在上面的 doPost 函数实现代码上,能看到存在 4 个漏洞。
- 预编译用法的错误使用导致的 SQL 注入漏洞
- 任意用户注册漏洞
- 潜在的 XSS 漏洞
- 跨站请求伪造 CSRF 漏洞
这里的 SQL 注入漏洞依然还是先拼接 SQL 语句导致的,和上面发现的 2 处注入点是一样的。
这里的任意用户注册漏洞很好理解,从这里的实现逻辑分析,从客户端的 POST 请求中获取各种用户信息的值,然后先执行有 SQL 注入漏洞的查询语句,如果未查询到该用户名的记录,就直接把获取到的信息作为新用户添加到数据库记录里。也就是说,我们直接带上用户注册页面上必填的注册信息直接 POST 请求这个 Servlet 地址,服务器会直接给我们新加一个我们随意控制的用户,完全不用输入验证码短信!
这里的潜在的 XSS 漏洞,是因为这里作为用户信息的输入点和保存到数据库的 SQL 插入点,这里没有对我们的输入值进行 XSS 过滤处理,那么我们可以构造 XSS 攻击脚本进行 XSS 攻击。。
CSRF 请求伪造漏洞倒是有些牵强,如果这里是管理员调用来添加新用户的 Servlet 就可以说攻击者可以利用 CSRF 让管理悄悄加个用户。但是,那样的话这里还不如叫越权添加用户呢,这里很明显连基本的权限判断都没有做。
一些证明
构造POST请求添加两个新用户: fuckme 和 FuckGod
Post 参数: username=fuckme&email=111&tel=222&password=123456
Post 参数: username=FuckGod&email=111111111@baidu.com&tel=188888888&password=123456
这里的 XSS 漏洞没有找到用户信息的输出点,不过这里的用户名是一个输出点。但是要注意的是这里的 XSS payload 里面要避免单引号,双引号出现,因为在插入之前有个存在 SQL 注入的查询,单双引号可能破坏上面的查询导致不会执行到下面的添加用户的流程,我构造的 payload 简单如:
POST 参数:username=FuckaGod<script>alert(1);</script>&email=111111111@baidu.com&tel=188888888&password=123456
7.验证码短信绕过实现任意手机号注册
这个漏洞的发现更简单,先黑盒分析:
我们观察一下发送验证码的过程,先输入手机号,然后输入图形验证码,然后点击发送验证短信:
http://www.ezchong.com/dealPhoneMessage?act=sendSMS&phone=15555555555
可以看到截图的右边,发送验证码请求的响应里面是一个 JSON 对象,里面有 message 和 isSuccess , message 是一个 6 位数的字符串,相信你的直觉这就是发送到注册手机上的验证码。为了知其所以然,我们定位到这段逻辑处理的服务端代码:
服务器端通过 Java 的 Random 类以 899999 作为种子数生成一个随机数然后加 100000 所得到随机数验证码,关于 Random 产生随机数的类我所知道的是这个类产生的随机数是不安全的因为在同一环境用同样的种子是可以预测每一次将要产生的随机数的:
贴一张做程序猿时测试的贴图:
结果: r1 产生的随机数
3
0
3
0
++++++++++++++++++++++
3 r2产生的随机数
0
3
0
换成:
System.out.println(r1.nextDouble(5))System.out.println(r2.nextDouble(5))
结果:
0.7304302967434272
0.2578027905957804
0.059201965811244595
0.24411725056425315
++++++++++++++++++++++
0.7304302967434272
0.2578027905957804
0.059201965811244595
0.24411725056425315
不过目前我还没有遇到因为这个 Random 随机数可被预测,失去随机性的案例(也可能是自己认知上存在问题,希望大牛指点 ! )
这里的问题在于代码 829 行:
data.put(“message”,
result);
这里不应该把发给用户的验证码添加到 JSON 对象返回给客户端,现在攻击者完全不用看手机短信就知道用户收到的验证码是多少了,从而就可以伪造任意手机号进行注册。
写到这里实在不想写了,基本都是这些类型的漏洞太多了最后贴一张图片!
提示:在某一个端口有 xxx
- 最后总结
- 代码托管平台是个很好的获得各种源代码的地方。
- 作为企业或公司的员工,一定要注意保护自己企业的隐私。
- 菜鸟学审计,不知哪门飞,望老鸟们带路。