题目解析

解题链接: http://ctf5.shiyanbar.com/10/web1/

首先进入题目界面

1562823085465

先点一下登陆系统,又跳回到这个界面,看来用户名密码不对(废话)

那看看源码

1562823163856

哦吼,看到一个php的提示,要MD5加密后的$test == '0',那这很常见啊,随便找一个MD5加密后是0e开头的就行了。

给个QNKCDZO(加密后的结果是0e830400451993494058024219903391)试一下

1562824378658

出来个这,我开始没仔细看,天真的以为flag已经出来了,把hjkleffifer当flag了😅

仔细一看,哦,原来是url后缀,好,把index.php改成出现的后缀,有了新发现

1562824822673

嗯,又是一段php代码,具体代码如下

$unserialize_str = $_POST['password'];
$data_unserialize = unserialize($unserialize_str);
if($data_unserialize['user'] == '???' && $data_unserialize['pass'] == '???'){
    print_r($flag);
}

就是说要通过password传递参数让data_unserialize数组中的user项和pass项满足条件。但是现在不知道两处???是什么,所以没法直接构造相等的值。

但是还有一个提示。“成也布尔,败也布尔”。bool类型的true可以和任意字符串弱类型相等。所以我们只要让user和pass都为bool类型的ture就行了。

构造password的值:a:2:{s:4:"user";b:1;s:4:"pass";b:1;}

a代表array,s代表string,b代表bool,数字代表个数/长度(具体请参照反序列化知识点)。

提交,拿到flag。

1562825962891


本题知识点

PHP弱类型

PHP比较相等性的运算符有两种,一种是strict,一种是non-strict。"==="和"!=="即strict比较符,只有在类型相同时才相等。"=="和"!="即non-strict比较符,会在类型转换后进行比较。

看以下的例子:

 true
    var_dump("1" == "01"); // 1 == 1 -> true
    var_dump("10" == "1e1"); // 10 == 10 -> true
    var_dump(100 == "1e2"); // 100 == 100 -> true
    var_dump(0 == '0'); // true
    var_dump(0 == 'abcdefg'); // true 
    var_dump(0 === 'abcdefg'); // false
    var_dump(1 == '1abcdef'); // true
?>

在和整数比较的时候,会把另一个参数强制转换成整数:

  • 如果参数是字符串,返回字符串中第一个不是数字的字符之前的数字串所代表的的整数值。
  • 如果字符串第一个是‘-’,则从第二个开始算起。
  • 如果参数是浮点数,则返回它取整后的值。

a是纯字符串,所以被转换成0。1abcdef被转换成1。

而e在比较的时候会被视为科学计数法,10的次方,1e2就是10的2次方。

关于布尔值的比较,见下表:

松散比较 ==

TRUE FALSE
1 TRUE FAUSE
0 FAUSE TRUE
-1 TRUE FAUSE
"1" TRUE FAUSE
"0" FAUSE TRUE
"-1" TRUE FAUSE
NULL FAUSE TRUE
array() FAUSE TURE
"php" TRUE FAUSE
"" FAUSE TRUE
TRUE TRUE FAUSE
FALSE FAUSE TRUE

从上表可以看出

当转换为bool型时,以下值被认为是FAUSE:

  • FAUSE本身
  • 整型值0
  • 空字符串
  • 字符串"0"
  • 不包括任何元素的数组(注意,一旦包含元素,就算包含的元素只是一个空数组,也是true)
  • NULL
  • 从空标记生成的SimpleXML对象

其余所有值都认为是TRUE。

具体请参见PHP类型比较表

所以当MD5加密后出现0exxxxxxxxxxxxxxxxxxxxxx之类的,如果用non-strict比较符,php就会当成0的多少多少次方(那当然结果还是0),和0比较就为true。因此可以利用这个漏洞进行验证绕过。

MD5加密后开头为0e的字符串举例:

QNKCDZO
0e830400451993494058024219903391

s878926199a
0e545993274517709034328855841020

s155964671a
0e342768416822451524974117254469

s214587387a
0e848240448830537924465865611904

s214587387a
0e848240448830537924465865611904

s878926199a
0e545993274517709034328855841020

s1091221200a
0e940624217856561557816327384675

s1885207154a
0e509367213418206700842008763514

s1502113478a
0e861580163291561247404381396064

s1885207154a
0e509367213418206700842008763514

s1836677006a
0e481036490867661113260034900752

s155964671a
0e342768416822451524974117254469

s1184209335a
0e072485820392773389523109082030

s1665632922a
0e731198061491163073197128363787

s1502113478a
0e861580163291561247404381396064

s1836677006a
0e481036490867661113260034900752

s1091221200a
0e940624217856561557816327384675

s155964671a
0e342768416822451524974117254469

s1502113478a
0e861580163291561247404381396064

s155964671a
0e342768416822451524974117254469

s1665632922a
0e731198061491163073197128363787

s155964671a
0e342768416822451524974117254469

s1091221200a
0e940624217856561557816327384675

s1836677006a
0e481036490867661113260034900752

s1885207154a
0e509367213418206700842008763514

s532378020a
0e220463095855511507588041205815

s878926199a
0e545993274517709034328855841020

s1091221200a
0e940624217856561557816327384675

s214587387a
0e848240448830537924465865611904

s1502113478a
0e861580163291561247404381396064

s1091221200a
0e940624217856561557816327384675

s1665632922a
0e731198061491163073197128363787

s1885207154a
0e509367213418206700842008763514

s1836677006a
0e481036490867661113260034900752

s1665632922a
0e731198061491163073197128363787

s878926199a
0e545993274517709034328855841020

PHP关联数组

在本题中我们看到数组的下标不是数字,而是字符串,这是为什么呢?原来这是PHP的关联数组,如果你学过Python,那么你可以类比Python中的字典。PHP关联数组是使用用户分配给数组的指定键的数组。

//php中有两种创建关联数组的方法
//第一种
age=array("Bill"=>"35","Steve"=>"37","Elon"=>"43");
//第二种age['Bill']="63";
age['Steve']="56";age['Elon']="47";

然后在脚本中可以使用指定键:

"63","Steve"=>"56","Elon"=>"47");
echo "Elon is " . $age['Elon'] . " years old.";
?>
//运行结果:
//Elon is 47 years old.

如需遍历并输出关联数组的所有值,可以使用foreach循环:

"63","Steve"=>"56","Elon"=>"47");

foreach(age asx=>x_value) {
  echo "Key=" .x . ", Value=" . $x_value;
  echo "
"; } ?> /*运行结果: Key=Bill, Value=63 Key=Steve, Value=56 Key=Elon, Value=47 */

PHP中的序列化和反序列化

“序列化”,听着好像hin高大上,但实际上当你了解它之后会发现,其实它并不可怕。实际上PHP所谓的序列化实际上就是将各种类型的数据,压缩并按照一定格式存储。说白了就是压缩。使用的函数是serialize()。

看以下实例:

pri = private;
    }
    public function get_pri()
    {
        returnthis->pri;
    }
}

obj = new test();obj->set_pri('Active');
data = serialize(obj);
echo $data;

运行实例得出的结果是:

O:4:"test":3:{s:9:"testpri";s:6:"Active";s:6:"*pro";s:5:"test1";s:3:"pub";s:5:"test2";}

看着好吓人!让我们一点一点看,慢慢你就会发现它其实很好理解。

Untitled (Draft)-3

如果你足够细心,你会发现private里明明我的属性名是pri,为什么这里是testpri,而且testpri不应该是7个字符吗,为什么这里是9个字符?protected中的名字和字符数也不一样。

这其实涉及到PHP的属性的访问权限问题,具体可以参照这篇文章:《一篇文章带你深入理解漏洞之PHP反序列化漏洞》。这里不做深入。我们只需要记住以下几点即可:

(1)Public权限

正常该是几个字符就是几个字符

(2)Protected权限

16进制打开看到属性名的格式为:

%00*%00属性名

在属性名前有一个 ***** ,而且 * 两边各有一个%00,每个%00占一个字符,所以字符数多了2。

(3)Private权限

16进制打开看到属性名格式为:

%00类名%00属性名

在属性名前面有这个类的名字,两边各有一个%00,所以字符数多了2。

这个特性必须记住,否则后期我们在构造或修改我们的攻击语句的时候就很容易出错。

如果你再细致一点,就会发现这个类不但有属性,还定义了许多的方法。可是序列化的只有属性,没有方法。这是为什么?

PHP的序列化只序列化属性,不序列化方法,这个性质引出了两个重要话题:

(1)在反序列化的时候一定要保证在当前的作用域环境下有该类存在

(2)反序列化攻击也就是依托类属性进行攻击

  • 因为序列化没有方法,所以我们只能控制类的属性,所以类属性是我们唯一的攻击入口。我们寻找合适的能被我们控制的类属性,然后利用它本身存在的方法,发动攻击。

看一个例子:

test = new L();
    }

    function __destruct()
    {
        this->test->action();
    }
}

class L
{
    function action()
    {
        echo "welcome to my PHP";
    }
}

class Evil
{
    vartest2;
    function action()
    {
        eval(this->test2);
    }
}

unserialize(_GET['test']);

这段代码就存在可以利用的漏洞。

在分析这段代码之前,我们首先看以下方法:

  1. construct():当对象创建时会自动调用(但在unserialize()时是不会自动调用的)。
  2. wakeup() :unserialize()时会自动调用。
  3. destruct():当对象被销毁时会自动调用。
  4. toString():当反序列化后的对象被输出在模板中的时候(转换成字符串的时候)自动调用。
  5. get() :当从不可访问的属性读取数据。
  6. call(): 在对象上下文中调用不可访问的方法时触发。

我们发现有很多方法会被自动调用。这就给我们了可乘之机。回到上面的例子,我们分析这段代码,从上往下看,test类在构造对象时让test变量指向一个L的对象,然后在test对象销毁时调用了L对象的action()。

首先看我们可以控制的参数,也就是可以利用的只有$test,也就是test类,那么我们接着分析,__destruct()函数会在对象销毁时自动调用,那么它就会执行action()。我们再看在action()执行过程中有没有可以利用的,发现Evil类里面也有action(),而且它调用了eval(),那这样我们就有了攻击思路。

我们可以修改test变量的值,让它指向Evil的对象,这样在test对象销毁调用destruct()时就会调用Evil对象的action(),就会执行eval(),同时我们还可以再构造test2变量的payload,这样就可以进行攻击。

那么我们就可以构造序列化结果:

test = new Evil //让它指向Evil
    }
}
class Evil
{
    var test2 = "phpinfo();"; //payload为phpinfo(); 
}data = new test;
data = serialize(data);
echo $data

得到结果:

O:4:"test":1:{s:10:"testtest";O:4:"Evil":1:{s:5:"test2";s:10:"phpinfo();";}}

将它送入$test,实现攻击。特别注意在构造的时候不要忘了两个%00

1562905573925

提交就可以执行我们的payload

1562905711077

这是一个很简单的示例,PHP反序列化漏洞的知识和利用方式还有很多,如果要深入展开实在是太多了。如果读者有兴趣可以参考这篇文章:《一篇文章带你深入理解漏洞之PHP反序列化漏洞》


"Imagination will take you everywhere."