前言
das偶遇b神出题,java&.net&python强如怪物,拼尽全力也无法战胜😭(也就是又爆零了)
应该是近期最后一次打ctf了,剩下的事交给复现了
官方wp:https://www.yuque.com/yuqueyonghu30d1fk/gd2y5h/ksfsfw8yf1u2xhhq?singleDoc#
Web
OtenkiImp (Unsolved)
python aiohttp框架
又是百分百晴天女
f12发现/hint
from aiohttp import web
import time
import json
import base64
import pickle
import time
import aiomysql
from settings import config, messages
async def mysql_init(app):
mysql_conf = app['config']['mysql']
while True:
try:
mysql_pool = await aiomysql.create_pool(host=mysql_conf['host'],
port=mysql_conf['port'],
user=mysql_conf['user'],
password=mysql_conf['password'],
db=mysql_conf['db'])
break
except:
time.sleep(5)
app.on_shutdown.append(mysql_close)
app['mysql_pool'] = mysql_pool
return app
async def mysql_close(app):
app['mysql_pool'].close()
await app['mysql_pool'].wait_closed()
async def index(request):
with open("./static/index.html", "r", encoding="utf-8") as f:
html = f.read()
return web.Response(text=html, content_type="text/html")
async def waf(request):
return web.Response(text=messages[0], status=403)
def check(string):
black_list = [b'R', b'i', b'o', b'b', b'V', b'__setstate__']
white_list = [b'__main__', b'builtins', b'contact', b'time', b'dict', b'reason']
try:
s = base64.b64decode(string)
except:
return False
for i in white_list:
s = s.replace(i, b'')
for i in black_list:
if i in s:
return False
return True
async def getWishes(request):
wishes = []
id = request.query.get("id")
try:
pool = request.app['mysql_pool']
async with pool.acquire() as conn:
async with conn.cursor() as cur:
try:
id = str(int(id))
sql = 'select id,wish from wishes where id={id}'.format(
id=id)
except:
sql = 'select id,wish from wishes'
await cur.execute(sql)
datas = await cur.fetchall()
except:
return web.Response(text=messages[1])
for (id, wish) in datas:
if check(wish):
wishes.append(pickle.loads(base64.b64decode(wish)))
return web.Response(text=json.dumps(wishes), content_type="application/json")
async def addWishes(request):
data = {}
if request.query.get("contact") and request.query.get("place") and request.query.get("reason") and request.query.get("date") and request.query.get("id"):
data["contact"] = request.query.get("contact")
data["place"] = request.query.get("place")
data["reason"] = request.query.get("reason")
data["date"] = request.query.get("date")
data["timestamp"] = int(time.time()*1000)
id = request.query.get("id")
wish = base64.b64encode(pickle.dumps(data))
else:
return web.Response(text=messages[3])
try:
pool = request.app['mysql_pool']
async with pool.acquire() as conn:
async with conn.cursor() as cur:
sql = 'insert into wishes(`id`, `wish`) values ({id}, "{wish}")'.format(
id=id, wish=wish.decode())
await cur.execute(sql)
return web.Response(text=messages[2])
except:
return web.Response(text=messages[1])
async def rmWishes(request):
try:
pool = request.app['mysql_pool']
async with pool.acquire() as conn:
async with conn.cursor() as cur:
sql = 'delete from wishes'
await cur.execute(sql)
return web.Response(text=messages[2])
except:
return web.Response(text=messages[1])
async def hint(request):
with open(__file__, 'r') as f:
source = f.read()
return web.Response(text=source)
if __name__ == '__main__':
app = web.Application()
app['config'] = config
app.router.add_static('/static', path='./static')
app.add_routes([web.route('*', '/', index),
web.route('*', '/waf', waf),
web.route('*', '/addWishes', addWishes),
web.get('/getWishes', getWishes),
web.post('/rmWishes', rmWishes),
web.get('/hint', hint)])
app = mysql_init(app)
web.run_app(app, port=5000)
注意到aiohttp/3.8.4
尝试CVE-2024-23334失败,因为这里是app.router.add_static('/static', path='./static')
,没开follow_symlinks
审一下代码
最终利用点应该是pickle反序列化,即getWishes
方法里的wishes.append(pickle.loads(base64.b64decode(wish)))
async def getWishes(request):
wishes = []
id = request.query.get("id")
try:
pool = request.app['mysql_pool']
async with pool.acquire() as conn:
async with conn.cursor() as cur:
try:
id = str(int(id))
sql = 'select id,wish from wishes where id={id}'.format(
id=id)
except:
sql = 'select id,wish from wishes'
await cur.execute(sql)
datas = await cur.fetchall()
except:
return web.Response(text=messages[1])
for (id, wish) in datas:
if check(wish):
wishes.append(pickle.loads(base64.b64decode(wish)))
return web.Response(text=json.dumps(wishes), content_type="application/json")
接下来往前看条件,首先是check
方法
def check(string):
black_list = [b'R', b'i', b'o', b'b', b'V', b'__setstate__']
white_list = [b'__main__', b'builtins', b'contact', b'time', b'dict', b'reason']
try:
s = base64.b64decode(string)
except:
return False
for i in white_list:
s = s.replace(i, b'')
for i in black_list:
if i in s:
return False
return True
限制了黑名单和白名单,白名单是替换为空,很明显可以用双写绕过
然后是前面的数据库查询,传入参数查询数据库,把查询结果中的wish
取出来,那么我们首先得想办法往数据库里插入pickle语句
那么就是addWishes
方法:
async def addWishes(request):
data = {}
if request.query.get("contact") and request.query.get("place") and request.query.get("reason") and request.query.get("date") and request.query.get("id"):
data["contact"] = request.query.get("contact")
data["place"] = request.query.get("place")
data["reason"] = request.query.get("reason")
data["date"] = request.query.get("date")
data["timestamp"] = int(time.time()*1000)
id = request.query.get("id")
wish = base64.b64encode(pickle.dumps(data))
else:
return web.Response(text=messages[3])
try:
pool = request.app['mysql_pool']
async with pool.acquire() as conn:
async with conn.cursor() as cur:
sql = 'insert into wishes(`id`, `wish`) values ({id}, "{wish}")'.format(
id=id, wish=wish.decode())
await cur.execute(sql)
return web.Response(text=messages[2])
except:
return web.Response(text=messages[1])
传入的data会以pickle字符串传入到数据库中
但是这里有一个问题
app.add_routes([web.route('*', '/', index),
web.route('*', '/waf', waf),
web.route('*', '/addWishes', addWishes),
web.get('/getWishes', getWishes),
web.post('/rmWishes', rmWishes),
web.get('/hint', hint)])
我们访问/addWishes的时候会触发waf
猜测得从aiohttp自身的漏洞出发思考绕过方式
稍微搜一下发现 aiohttp(yarl) 会对 url 部分字符自动 urldecode:https://blog.csdn.net/qq_31720329/article/details/82024036
本地测试也发现会自动urldecode
但是进不去/addWishes路由。。可以发现它进入路由是在urldecode之前,然后path的值为urldecode之后
RceHouse (Unsolved)
import subprocess
import clickhouse_connect
from flask import *
import os
app = Flask(__name__)
client = clickhouse_connect.get_client(host='127.0.0.1', port=8123, username='default', password='')
@app.route("/status",methods=['POST'])
def status():
if request.method=="POST":
remote_addr = request.remote_addr
print(remote_addr)
if remote_addr=='127.0.0.1':
command = ["clickhouse-client", "--query", request.args.get('param') ]
result = subprocess.check_output(command, stderr=subprocess.STDOUT, text=True,shell=False)
return result
else:
result=os.popen(f"clickhouse-client --query=\"select 'try harder'\"").read()
return result
else:
result = os.popen(f"clickhouse-client --query=\"select 'try better'\"").read()
return result
@app.route("/sql", methods=["POST"])
def sql():
try:
#此处是clickhouse的查询语法,不存在注入问题
sql = 'SELECT * FROM ctf.users WHERE id = ' + request.form.get("id")
res=client.command(sql)
client.close()
return res
except Exception as e:
return e;
@app.route("/upload",methods=['POST'])
def upload():
if 'file' not in request.files:
return redirect(request.url)
file = request.files['file']
if file.filename == '':
return redirect(request.url)
filename = "Boogipop"
file_path ="/tmp/"+filename
file.save(file_path)
return file_path
if __name__=="__main__":
app.run("0.0.0.0",5000,debug=False)
审一下代码,总共就三个路由
/status路由:
@app.route("/status",methods=['POST'])
def status():
if request.method=="POST":
remote_addr = request.remote_addr
print(remote_addr)
if remote_addr=='127.0.0.1':
command = ["clickhouse-client", "--query", request.args.get('param') ]
result = subprocess.check_output(command, stderr=subprocess.STDOUT, text=True,shell=False)
return result
else:
result=os.popen(f"clickhouse-client --query=\"select 'try harder'\"").read()
return result
else:
result = os.popen(f"clickhouse-client --query=\"select 'try better'\"").read()
return result
一眼打ssrf然后命令执行
/sql路由:
@app.route("/sql", methods=["POST"])
def sql():
try:
#此处是clickhouse的查询语法,不存在注入问题
sql = 'SELECT * FROM ctf.users WHERE id = ' + request.form.get("id")
res=client.command(sql)
client.close()
return res
except Exception as e:
return e;
执行sql查询
/upload路由:
@app.route("/upload",methods=['POST'])
def upload():
if 'file' not in request.files:
return redirect(request.url)
file = request.files['file']
if file.filename == '':
return redirect(request.url)
filename = "Boogipop"
file_path ="/tmp/"+filename
file.save(file_path)
return file_path
一个任意文件上传的功能,上传的文件路径为/tmp/Boogipop
ImpossibleUnser (Unsolved)
先审代码
package com.ctf;
import com.sun.net.httpserver.HttpServer;
import java.net.InetSocketAddress;
import java.util.concurrent.Executor;
public class IndexController {
public IndexController() {
}
public static void main(String[] args) throws Exception {
HttpServer server = HttpServer.create(new InetSocketAddress(8000), 0);
server.createContext("/ctf", new SPELHandler());
server.createContext("/index", new IndexHandler());
server.createContext("/unser", new UnserHandler());
server.setExecutor((Executor)null);
server.start();
}
}
/ctf
路由:
package com.ctf;
import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpHandler;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.URLDecoder;
import java.util.HashMap;
import java.util.Map;
import org.springframework.expression.Expression;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser;
public class SPELHandler implements HttpHandler {
public SPELHandler() {
}
public void handle(HttpExchange httpExchange) throws IOException {
InputStream requestBody = httpExchange.getRequestBody();
String body = this.readInputStream(requestBody);
if (!body.equals("")) {
Map<String, String> PostData = this.parseFormData(body);
String payload = (String)PostData.get("payload");
ExpressionParser parser = new SpelExpressionParser();
payload = URLDecoder.decode(payload);
MySecurityWaf mySecurityWaf = new MySecurityWaf();
if (mySecurityWaf.securitycheck(payload)) {
Expression exp = parser.parseExpression(payload);
Object value = exp.getValue();
System.out.println(value);
String response = "Welcome to My Challenge";
httpExchange.sendResponseHeaders(200, (long)response.length());
OutputStream os = httpExchange.getResponseBody();
os.write(response.getBytes());
os.close();
}
}
String response = "Give me some payload Plz inject me";
httpExchange.sendResponseHeaders(200, (long)response.length());
OutputStream os = httpExchange.getResponseBody();
os.write(response.getBytes());
os.close();
}
private Map<String, String> parseFormData(String body) {
Map<String, String> params = new HashMap();
String[] kvPairs = body.split("&");
String[] var4 = kvPairs;
int var5 = kvPairs.length;
for(int var6 = 0; var6 < var5; ++var6) {
String kv = var4[var6];
String[] splits = kv.split("=");
params.put(splits[0], splits[1]);
}
return params;
}
private String readInputStream(InputStream is) throws IOException {
StringBuilder sb = new StringBuilder();
InputStreamReader sr = new InputStreamReader(is);
char[] buf = new char[1024];
int len;
while((len = sr.read(buf)) > 0) {
sb.append(buf, 0, len);
}
return sb.toString();
}
}
用来打SPEL的