Java程式碼審計之不安全的Java程式碼

2022-09-15 12:00:11

Java程式碼審計之不安全的Java程式碼

​ 在打靶場的同時,需要想一下如果你是開發人員你會怎樣去防禦這種漏洞,而作為攻擊方你又怎麼去繞過開發人員的防禦。

環境搭建

https://github.com/j3ers3/Hello-Java-Sec

SQL隱碼攻擊

​ SQLI(SQL Injection), SQL隱碼攻擊是因為程式未能正確對使用者的輸入進行檢查,將使用者的輸入以拼接的方式帶入SQL語句,導致了SQL隱碼攻擊的產生。攻擊者可通過SQL隱碼攻擊直接獲取資料庫資訊,造成資訊洩漏。

SQL隱碼攻擊之JDBC注入

​ JDBC有兩個方法執行SQL語句,分別是PrepareStatement和Statement。

漏洞程式碼:

// 採用原始的Statement拼接語句,導致漏洞產生

public String jdbcVul(String id) {
    StringBuilder result = new StringBuilder();
    try {
        Class.forName("com.mysql.cj.jdbc.Driver");
        Connection conn = DriverManager.getConnection(db_url, db_user, db_pass);

        Statement stmt = conn.createStatement();
        // 拼接語句產生SQL隱碼攻擊
        String sql = "select * from users where id = '" + id + "'";
        ResultSet rs = stmt.executeQuery(sql);

        while (rs.next()) {
            String res_name = rs.getString("user");
            String res_pass = rs.getString("pass");
            String info = String.format("查詢結果 %s: %s", res_name, res_pass);
            result.append(info);
        }                  

漏洞程式碼二:

// PrepareStatement會對SQL語句進行預編譯,但有時開發者為了便利,直接採取拼接的方式構造SQL,此時進行預編譯也無用。

Connection conn = DriverManager.getConnection(db_url, db_user, db_pass);
String sql = "select * from users where id = " + id;
PreparedStatement st = conn.prepareStatement(sql);
System.out.println("[*] 執行SQL語句:" + st);
ResultSet rs = st.executeQuery();     

SQL隱碼攻擊之MyBatis

​ MyBatis框架底層已經實現了對SQL隱碼攻擊的防禦,但存在使用不當的情況下,仍然存在SQL隱碼攻擊的風險。

漏洞程式碼:

// 此漏洞出現頻率較高並且很嚴重!
// 為何產生:由於使用 #{} 會將物件轉成字串,形成 order by "user" desc 造成錯誤,因此很多研發會採用${}來解決,從而造成SQL隱碼攻擊
// 點選執行可通過報錯語句獲取資料庫user

@RequestMapping("/mybatis/vul/order")
public List<User> orderBy(String field, String sort) {
    return userMapper.orderBy(field, sort);
}

// mapper.xml語句
<select id="orderBy" resultType="com.best.hello.entity.User">
    select * from users order by ${field} ${sort}
</select>

漏洞程式碼二:

// 模糊搜尋時,直接使用'%#{q}%' 會報錯,部分研發圖方便直接改成'%${q}%'從而造成注入

@Select("select * from users where user like '%${q}%'")
List<User> search(String q);

// 安全程式碼,採用concat
@Select("select * from users where user like concat('%',#{q},'%')")
List<User> search(String q); 

檔案上傳

​ 檔案上傳漏洞,是指使用者上傳了一個可執行的指令碼檔案(如jsp\php\asp),並通過此指令碼檔案獲得了執行伺服器端命令的能力。常見場景是web伺服器允許使用者上傳圖片或者普通文字檔案儲存,這種漏洞屬於低成本高殺傷力

漏洞程式碼:

// 允許上傳任意檔案導致的安全風險

public String singleFileUpload(@RequestParam("file") MultipartFile file, RedirectAttributes redirectAttributes) {
    try {
        byte[] bytes = file.getBytes();
        Path dir = Paths.get(UPLOADED_FOLDER);
        Path path = Paths.get(UPLOADED_FOLDER + file.getOriginalFilename());

        Files.write(path, bytes);
        redirectAttributes.addFlashAttribute("message","上傳檔案成功:" + path + "");
    } catch (Exception e) {
         return e.toString();
    }
    return "redirect:upload_status";
}

目錄遍歷

​ 目錄遍歷, 應用系統在處理下載檔案時未對檔案進行過濾,系統後臺程式程式中如果不能正確地過濾使用者端提交的../和./之類的目錄跳轉符,攻擊者可以通過輸入../進行目錄跳轉,從而下載、刪除任意檔案。

// 檔案路徑沒做限制,通過../遞迴下載任意檔案
// PoC:/Traversal/download?filename=../../../../../../../etc/passwd

@GetMapping("/download")
public String download(String filename, HttpServletRequest request, HttpServletResponse response) {
    String filePath = System.getProperty("user.dir") + "/logs/" + filename;
    try {
        File file = new File(filePath);
        InputStream is = new BufferedInputStream(new FileInputStream(file));
        byte[] buffer = new byte[is.available()];
        fis.read(buffer);
        fis.close();
        response.reset();
        response.addHeader("Content-Disposition", "attachment;filename=" + filename);
        response.addHeader("Content-Length", "" + file.length());
        OutputStream toClient = new BufferedOutputStream(response.getOutputStream());
        response.setContentType("application/octet-stream");
        toClient.write(buffer);
        toClient.flush();
        toClient.close();
        return "下載檔案成功:" + filePath;

XSS

​ XSS(Cross Site Scripting) 跨站指令碼攻擊,攻擊者插入惡意Script程式碼,當用戶瀏覽該頁之時,嵌入其中Web裡面的Script程式碼會被執行,從而達到惡意攻擊使用者的目的。

漏洞程式碼:

// 簡單的反射型XSS,沒對輸出做處理。當攻擊者輸入惡意js語句時可觸發

@GetMapping("/reflect")
public static String input(String content) {
    return content;
}

SSRF

​ SSRF(Server-Side Request Forgery) 伺服器端請求偽造,是一種由攻擊者構造形成由伺服器端發起請求的一個安全漏洞。

漏洞程式碼:

// url引數沒做限制,可呼叫URLConnection發起任意請求,比如請求內網,或使用file等協定讀取檔案

public static String URLConnection(String url) {
    try {
        URL u = new URL(url);
        URLConnection conn = u.openConnection();
        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();
    }
}

漏洞程式碼二:

// SSRF修復經常碰到的問題,雖然過濾了內網地址,但通過短連結跳轉的方式可以繞過

public String URLConnectionSafe(String url) {
    if (!Security.is_http(url)){
        return "不允許非http/https協定!!!";
    }else if (Security.isIntranet(url)) {
        return "不允許存取內網!!!";
    }else{
        return Http.URLConnection(url);
    }
}

遠端程式碼執行

​ RCE (Remote Code Execution), 遠端程式碼執行漏洞,這裡包含兩種型別漏洞:

  • 命令注入(Command Injection),在某種開發需求中,需要引入對系統本地命令的支援來完成特定功能,當未對輸入做過濾時,則會產生命令注入
  • 程式碼注入(Code Injection),在正常的java程式中注入一段java程式碼並執行,即使用者輸入的資料當作java程式碼進行執行。

漏洞程式碼:

// 功能是利用ProcessBuilder執行ls命令檢視檔案,但攻擊者通過拼接; & |等連線符來執行自己的命令。

@RequestMapping("/ProcessBuilder")
public static String cmd(String filepath) {
    String[] cmdList = {"sh", "-c", "ls -l " + filepath};
    StringBuilder sb = new StringBuilder();

    ProcessBuilder pb = new ProcessBuilder(cmdList);
    pb.redirectErrorStream(true);
    ...

漏洞程式碼二:

// getRuntime()常用於執行本地命令,使用頻率較高。

@RequestMapping("/runtime")
public static String cmd2(String cmd) {
    StringBuilder sb = new StringBuilder();
    try {
        Process proc = Runtime.getRuntime().exec(cmd);
        InputStream fis = proc.getInputStream();
        InputStreamReader isr = new InputStreamReader(fis);
        BufferedReader br = new BufferedReader(isr);
     ...

漏洞程式碼三:

// 通過載入遠端js檔案來執行程式碼,如果載入了惡意js則會造成任意命令執行
// 遠端惡意js: var a = mainOutput(); function mainOutput() { var x=java.lang.Runtime.getRuntime().exec("open -a Calculator");}
// ⚠️ 在Java 8之後移除了ScriptEngineManager的eval

public void jsEngine(String url) throws Exception {
    ScriptEngine engine = new ScriptEngineManager().getEngineByName("JavaScript");
    Bindings bindings = engine.getBindings(ScriptContext.ENGINE_SCOPE);
    String payload = String.format("load('%s')", url);
    engine.eval(payload, bindings);
}

漏洞程式碼四:

// 不安全的使用Groovy呼叫命令

import groovy.lang.GroovyShell;
@GetMapping("/groovy")
public void groovy(String cmd) {
    GroovyShell shell = new GroovyShell();
    shell.evaluate(cmd);
}              

漏洞程式碼五:

// ProcessImpl是更為底層的實現,Runtime和ProcessBuilder執行命令實際上也是呼叫了ProcessImpl這個類

Class clazz = Class.forName("java.lang.ProcessImpl");
Method method = clazz.getDeclaredMethod("start", String[].class, Map.class, String.class, ProcessBuilder.Redirect[].class, boolean.class);
method.setAccessible(true);
method.invoke(null, new String[]{cmd}, null, null, null, false);

不安全的反序列化

​ 反序列化漏洞,當輸入的反序列化的資料可被使用者控制,那麼攻擊者即可通過構造惡意輸入,讓反序列化產生非預期的物件,在此過程中執行構造的任意程式碼(多見於第三方元件產生的漏洞)

漏洞程式碼:

// readObject,讀取輸入流,並轉換物件。ObjectInputStream.readObject() 方法的作用正是從一個源輸入流中讀取位元組序列,再把它們反序列化為一個物件。
// payload:java -jar ysoserial-0.0.6-SNAPSHOT-BETA-all.jar CommonsCollections5 "open -a Calculator" | base64

public String cc(String base64) {
    try {
        BASE64Decoder decoder = new BASE64Decoder();

        base64 = base64.replace(" ", "+");
        byte[] bytes = decoder.decodeBuffer(base64);

        ByteArrayInputStream stream = new ByteArrayInputStream(bytes);

        // 反序列化流,將序列化的原始資料恢復為物件
        ObjectInputStream in = new ObjectInputStream(stream);
        in.readObject();
        in.close();
        return "反序列化漏洞";
    } catch (Exception e) {
        return e.toString();
    }
}

漏洞程式碼二:

// 遠端伺服器支援使用者可以輸入yaml格式的內容並且進行資料解析,沒有做沙箱,黑名單之類的防控

public void yaml(String content) {
    Yaml y = new Yaml();
    y.load(content);
}

漏洞程式碼三:

// Java RMI Registry 反序列化漏洞,受jdk版本影響,< = jdk8u111

public String rmi() {
    try {
        Registry registry = LocateRegistry.createRegistry(9999);
    } catch (Exception e) {
        e.printStackTrace();
    }
    return "開啟RMI監聽,埠:9999";
}

失效的身份認證

​ 失效的身份認證,通過錯誤使用應用程式的身份認證和對談管理功能,攻擊者能夠破譯密碼、金鑰或對談令牌,或者利用其它開發缺陷來暫時性或永久性冒充其他使用者的身份。

表示式注入

​ SpEL(Spring Expression Language)表示式注入, 是一種功能強大的表示式語言、用於在執行時查詢和操作物件圖,由於未對引數做過濾可造成任意命令執行。

漏洞程式碼:

// PoC: T(java.lang.Runtime).getRuntime().exec(%22open%20-a%20Calculator%22)

@GetMapping("/vul")
public String spelVul(String ex) {
    ExpressionParser parser = new SpelExpressionParser();
    String result = parser.parseExpression(ex).getValue().toString();
    System.out.println(result);
    return result;
}

XML外部實體注

​ XXE (XML External Entity Injection), XML外部實體注入,當開發人員設定其XML解析功能允許外部實體參照時,攻擊者可利用這一可引發安全問題的設定方式,實施任意檔案讀取、內網埠探測、命令執行、拒絕服務等攻擊。

漏洞程式碼:

@RequestMapping(value = "/XMLReader")
public String XMLReader(@RequestBody String content) {
    try {
        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();
    }
}

漏洞程式碼二:

// SAXReader
SAXReader sax = new SAXReader();
// 修復:禁用外部實體
// sax.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
sax.read(new InputSource(new StringReader(content)));
                    

漏洞程式碼三:

// SAXBuilder
@RequestMapping(value = "/SAXBuilder")
public String SAXBuilder(@RequestBody String content) {
    try {
        SAXBuilder saxbuilder = new SAXBuilder();
        // 修復: saxbuilder.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
        saxbuilder.build(new InputSource(new StringReader(content)));
        return "SAXBuilder XXE";
    } catch (Exception e) {
        return e.toString();
    }
}             

越權存取

​ 失效的存取控制(Broken Access Control),應用在檢查授權時存在紕漏,使得攻擊者在獲得低許可權使用者賬戶後,利用一些方式繞過許可權檢查,存取或者操作其他使用者或者更高許可權。越權漏洞的成因主要是因為開發人員在對資料進行增、刪、改、查詢時對使用者端請求的資料過分相信而遺漏了許可權的判定,一旦許可權驗證不充分,就易致越權漏洞。

漏洞程式碼:

// 未做許可權控制,通過遍歷name引數可查詢任意使用者資訊
@GetMapping("/vul/info")
public List<User> vul(String name) {
    return userMapper.queryByUser(name);
}                   

介面未授權存取

​ 介面未授權存取(Unauthorized Access),在不進行請求授權的情況下,能夠直接對相應的業務邏輯功能進行存取、操作等。通常是由於認證頁面存在缺陷或者無認證、安全設定不當等導致的。

漏洞程式碼:

// 對部分介面未做鑑權攔截,導致可未授權存取
@Override
public void addInterceptors(InterceptorRegistry registry) {
    registry.addInterceptor(new LoginHandlerInterceptor())
    .addPathPatterns("/**")
    .excludePathPatterns("/Unauth/**", "/css/**", "/js/**", "/img/**");
}

SSTI模板注入

​ SSTI(Server Side Template Injection) 伺服器模板注入, 伺服器端接收了使用者的輸入,將其作為 Web 應用模板內容的一部分,在進行目標編譯渲染的過程中,執行了使用者插入的惡意內容。

漏洞程式碼:

 /**
   * 將請求的url作為檢視名稱,呼叫模板引擎去解析
   * 在這種情況下,我們只要可以控制請求的controller的引數,一樣可以造成RCE漏洞
   * payload: __${T(java.lang.Runtime).getRuntime().exec("open -a Calculator")}__::.x
   */

 @GetMapping("/doc/{document}")
 public void getDocument(@PathVariable String document) {
     System.out.println(document);
 }

漏洞元件

元件中存在的漏洞

XStream反序列化

​ XStream是一個簡單的基於Java庫,Java物件序列化到XML,歷史上存在多個反序列化漏洞。

漏洞程式碼:

public String vul(@RequestBody String content) {
    XStream xs = new XStream();
    xs.fromXML(content);
    return "XStream Vul";
}

Fastjson反序列化

​ fastjson是阿里巴巴的開源JSON解析庫,它可以解析JSON格式的字串,支援將Java Bean序列化為JSON字串,也可以從JSON字串反序列化到JavaBean,歷史上存在多個反序列化漏洞。

// 使用了低版本,存在漏洞
// poc: {"@type":"java.net.Inet4Address","val":"8d5tv8.dnslog.cn"}

<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>fastjson</artifactId>
    <version>1.2.24</version>
</dependency>

Jackson反序列化

​ Jackson是一套開源的java序列化與反序列化工具框架,可將java物件序列化為xml和json格式的字串並提供對應的反序列化過程。由於其解析效率較高,Jackson目前是Spring MVC中內建使用的解析方式。

漏洞程式碼:

public void vul() {
    try {
        String payload = "[\"com.nqadmin.rowset.JdbcRowSetImpl\",{\"dataSourceName\":\"ldap://127.0.0.1:1389/Exploit\",\"autoCommit\":\"true\"}]";

        ObjectMapper mapper = new ObjectMapper();
        mapper.enableDefaultTyping();
        Object o = mapper.readValue(payload, Object.class);
        mapper.writeValueAsString(o);
    } catch (Exception e) {
        e.printStackTrace();
    }
}

Log4j2反序列化

​ Apache Log4j2是一款優秀的Java紀錄檔框架。此次漏洞是由 Log4j2 提供的lookup功能造成的,該功能允許開發者通過一些協定去讀取相應環境中的設定。但在處理資料時,並未對輸入(如${jndi)進行嚴格的判斷,從而造成JNDI注入

漏洞程式碼:

// log4j-core < 2.15.0-rc1

public String vul(String content) {
    logger.error(content);
    return "Log4j2 RCE";
}

shiro反序列化

其他漏洞

其他的一些漏洞

開放重定向

​ 開放重定向漏洞,是指後臺伺服器在告知瀏覽器跳轉時,未對使用者端傳入的重定向地址進行合法性校驗,導致使用者瀏覽器跳轉到釣魚頁面的一種漏洞
出現場景:使用者登入、統一身份認證等需要跳轉的地方

漏洞程式碼:

// 滿足引數url可控,且未做限制

public String vul(String url) {
    return "redirect:" + url;
}

Actuator未授權存取

​ Actuator, 是Spring Boot提供的服務監控和管理中介軟體,預設設定會出現介面未授權存取,部分介面會洩露網站流量資訊和記憶體資訊等,使用Jolokia庫特性甚至可以遠端執行任意程式碼,獲取伺服器許可權。

漏洞程式碼:

# 不安全的設定:Actuator設定全部暴露
management.endpoints.web.exposure.include=*

IP地址偽造

​ X-Forwarded-For地址偽造,很多Web應用需要獲取使用者的IP,通過IP偽造可以繞過一些安全限制。

漏洞程式碼:

public static String vul(HttpServletRequest request) {
    String ip2 = request.getHeader("X-Forwarded-For");
    if(!Objects.equals(ip2, "127.0.0.1")) {
        return  "禁止存取,只允許本地IP!";
    } else {
        return "success,你的IP:" + ip2;
    }
}

Swagger未授權存取

​ Swagger未開啟頁面存取限制,Swagger未開啟嚴格的Authorize認證。

CORS

​ CORS(Cross-origin resource sharing),即跨域資源共用,用於繞過SOP(同源策略)來實現跨域資源存取的一種技術。 CORS漏洞則是利用CORS技術竊取使用者敏感資料,CORS漏洞的成因是伺服器端設定的規則不當所導致的,伺服器端沒有設定Access-Control-Allow-Origin等欄位

漏洞程式碼:

public String corsVul(HttpServletRequest request, HttpServletResponse response) {
        String origin = request.getHeader("origin");
        response.setHeader("Access-Control-Allow-Origin", origin);
        response.setHeader("Access-Control-Allow-Credentials", "true");
        response.setHeader("Access-Control-Allow-Methods", "GET,POST,PUT,DELETE,OPTIONS");
        return "cors vul";
}

JNDI注入

​ Java命名和目錄介面(JNDI)是一種Java API,類似於一個索引中心,它允許使用者端通過name發現和查詢資料和物件。JNDI注入就是當上文程式碼中jndiName這個變數可控時,引發的漏洞,它將導致遠端class檔案載入,從而導致遠端程式碼執行。

漏洞程式碼:

// lookup是通過名字檢索執行的物件,當lookup()方法的引數可控時,攻擊者便能提供一個惡意的url地址來載入惡意類。
Context ctx = new InitialContext();
ctx.lookup(url);

DoS漏洞

​ DoS是Denial of Service的簡稱,即拒絕服務,造成DoS的攻擊行為被稱為DoS攻擊,其目的是使計算機或網路無法提供正常的服務。

漏洞程式碼:

// Pattern.matches造成的ReDoS
// PoC: vul?contnet=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaab

public String vul(String content) {

    boolean match = Pattern.matches("(a|aa)+", content);
    return String.format("正則匹配:%s,正規表示式拒絕服務攻擊", match);
}

驗證碼複用

​ 驗證碼反覆利用,可以直接進行暴力破解。(這是一類常見的安全問題)
一般來說,驗證碼是與Session繫結的,Session生成時,也伴隨著驗證碼的生成和繫結,在存取頁面時,介面的請求和驗證碼的生成通常是非同步進行的,這使得兩個功能變得相對獨立。也就意味著我們如果僅請求介面,而不觸發驗證碼的生成,那麼驗證碼就不會變化。 並且在考慮安全時,開發人員的關注點往往在 驗證碼校驗 是否通過,通過則進入業務流程,不通過則重新填寫,而忽視了這個使用者是否按照既定的業務流程在走(介面存取與驗證碼生成是否同時進行),驗證碼是否被多次使用了。

漏洞程式碼:

 // 未清除session中的驗證碼,導致可複用

 if (!CaptchaUtil.ver(captcha, request)) {
     model.addAttribute("msg", "驗證碼不正確");
     return "login";
 }

饅頭出品

部落格:https://www.cnblogs.com/mantou0/