当前位置: 首页 > article >正文

【CISCN 2024 AWDP】从源码泄露到WAF绕过:实战剖析三道典型Web赛题攻防思路

1. 从源码泄露到逻辑漏洞实战复盘“粗心的程序员”大家好我是老张一个在安全圈摸爬滚打了十来年的老兵。刚打完今年的CISCN区域赛AWDP场趁着记忆还热乎想和大家聊聊几道印象深刻的Web题。AWDP这赛制攻防兼备一个小时的check周期拼的不仅是手速更是对漏洞本质的理解和快速修复能力。这次比赛有几道题可以说是把“粗心”和“绕过”玩出了花非常典型。咱们不搞那些虚头巴脑的理论直接上干货我会结合比赛时的真实思路和踩过的坑带你走一遍从发现到利用再到修复的完整过程。这次要重点拆解的三道题分别是“粗心的程序员”、“submit”和“Polluted”。它们几乎涵盖了Web安全中几个最经典的场景源码泄露、文件上传绕过、以及代码注入与污染。你会发现很多看似复杂的漏洞根源往往是一些开发中不经意的“小疏忽”。咱们就从最简单的“粗心的程序员”这道题开始它完美诠释了什么叫“祸从根起”。1.1 信息收集的“意外收获”www.zip源码泄露拿到题目第一步永远是信息收集。对于Web题我习惯性地先扫目录这是基本功。用dirsearch或者gobuster跑一下目标是一个简单的用户信息页面。扫着扫着一个熟悉的文件名跳了出来——www.zip。看到这个我心里基本就有数了十有八九是源码打包泄露。这种错误在真实的开发环境里其实挺常见的比如开发者在部署时为了方便把整个项目目录打了个zip包放在Web根目录事后又忘了删除。在CTF里这几乎是送分题但在真实渗透测试中这也是一个高价值的突破口。下载www.zip解压整个网站的源代码就赤裸裸地摆在你面前了。这相当于打牌时对手直接把底牌亮给你看了。有了源码接下来的工作就从“黑盒测试”变成了“白盒审计”难度直线下降。我们不需要再去盲目地猜测参数、尝试各种注入而是可以直接阅读代码逻辑寻找脆弱的函数和逻辑分支。这道题的核心文件是一个用户信息展示和更新的页面代码量不大但里面埋的雷可真不少。1.2 代码审计与漏洞链构造一个报错引发的“血案”审计源码我一般先找用户输入点然后跟踪数据流看它最终到了哪里。这道题的关键代码片段如下已做简化?php error_reporting(0); include default_info_auto_recovery.php; session_start(); $p $_SERVER[HTTP_X_FORWARDED_FOR] ?: $_SERVER[REMOTE_ADDR]; if (preg_match(/\?|php|:/i, $p)) { die(); } $time date(Y-m-d h:i:s, time()); $username $_SESSION[username]; $id $_SESSION[id]; if ($username $id){ echo Hello,.$username; $str //登录时间$time,$username $p; $str str_replace(\n,,$str); file_put_contents(config.php, file_get_contents(config.php).$str); }else{ die(NO ACCESS); } ?一眼看去漏洞点非常清晰。程序会将登录时间、用户名$username和客户端IP来自X-Forwarded-For头或REMOTE_ADDR拼接成一个字符串然后追加写入到config.php这个文件中。这里就出现了两个致命问题逻辑漏洞写入的文件是.php后缀。这意味着如果我们能控制写入的内容并且让内容被当作PHP代码执行就能实现远程代码执行RCE。输入过滤不严虽然对$pIP有简单的过滤禁止了?、php和:但这个过滤太弱了。更重要的是对$username这个来自Session的变量在写入文件前完全没有进行任何过滤。攻击链一下子就清晰了我们只需要注册一个用户然后在修改用户名username时将其设置为一段恶意的PHP代码。当这个用户登录后程序就会将我们的恶意用户名连同其他信息一起写入config.php。之后我们只需要访问config.php服务器就会执行我们写入的代码。1.3 漏洞利用实战巧用PHP闭合标签直接写?php system(\ls\);?行吗理论上可以但这里有个小技巧。我们注意到写入的字符串是以//开头的这是PHP的单行注释。如果我们直接写?php ... ?它会被写在注释后面同样被注释掉而无法执行。怎么办这时候就要利用PHP的标签特性。我们可以在用户名里先插入一个?来闭合掉文件开头可能存在的?php标签或者结束掉任何未闭合的PHP代码块然后再开启新的PHP代码。所以构造的攻击载荷Payload是这样的??php system($_GET[‘cmd‘]); ?我们来拆解一下这个Payload?用于闭合之前可能存在的PHP标签。?php system($_GET[‘cmd‘]); ?开启新的PHP标签并执行一个通过URL参数cmd传入的系统命令。在修改用户名的前端我们发现提交的数据被Base64编码了。这没关系我们只需要将我们的Payload进行Base64编码后提交即可。抓包修改请求将newusername参数的值替换为Pz48P3BocCBzeXN0ZW0oJF9HRVRbJ2NtZCddKTsgPz4即上面Payload的Base64编码。提交后用该用户登录我们的恶意代码就被追加写入config.php了。此时访问/config.php?cmdls就能看到当前目录的文件列表成功实现了RCE。接下来就是常规操作找flag文件用cat命令读取即可。注意在实际操作中要注意空格和特殊字符的URL编码。另外这种追加写入的方式可能会导致config.php文件变得很大多次攻击时要注意观察文件内容。2. 文件上传的攻防博弈深入拆解“submit”题目如果说“粗心的程序员”考察的是源码审计和逻辑漏洞那“submit”这道题就是经典的文件上传漏洞攻防战。这道题非常有意思它有一个看似严格、实则漏洞百出的WAFWeb应用防火墙完美地展示了“安全配置错误”如何让防线形同虚设。我们一起来把它剥开看看。2.1 初探上传逻辑黑名单的局限性题目是一个简单的文件上传界面。先传个普通图片正常。传一个shell.php被拦截。看来有防护。按照惯例我们直接看题目给出的修复前源码这是最直接的理解方式。关键的上传处理代码如下$content file_get_contents($_FILES[myfile][tmp_name]); $allow_content_type array(image/png); $type $_FILES[myfile][type]; if (!in_array($type, $allow_content_type)) { die(只允许png呀!br); } $allow_ext array(.png); $file_name$_FILES[myfile][name]; $_FILES[myfile][name] str_replace(.ph,,$_FILES[myfile][name]); $file_ext strrchr($file_name, .); $file_ext strtolower($file_ext); $file_ext str_ireplace(::$DATA, , $file_ext); $file_ext trim($file_ext); if (!in_array($file_ext, $allow_ext)) { die(只允许png呀!br); } if (preg_match(/(php|script|xml|user|htaccess|\?|\?\|eval|system|assert|fllllagg|f\*|\/f|cat|POST|GET|\$_|exec)/i, $content)) { die(喵喵说你的内容不符合呀0-0); } else { $file $path . / . $_FILES[myfile][name]; if (move_uploaded_file($_FILES[myfile][tmp_name], $file)) { file_put_contents($file, $content); echo Success!br; } }乍一看防御措施有好几层MIME类型检查只允许image/png。文件扩展名检查只允许.png并且会删除文件名中的.ph字符串。文件内容检查用正则表达式过滤文件内容中的危险关键词。很多新手看到这可能就懵了感觉无从下手。但咱们仔细分析每一层都有绕过的可能。2.2 层层绕过利用解析差异与WAF盲区第一层绕过MIME类型这个最简单。$_FILES[‘myfile‘][‘type‘]这个值是由浏览器端提交的我们可以完全控制。通过Burp Suite拦截上传请求直接将Content-Type修改为image/png即可轻松绕过。第二层绕过文件扩展名代码的逻辑是先检查原始文件名$file_name的扩展名是否为.png但同时它又对$_FILES[‘myfile‘][‘name‘]这个变量执行了str_replace(“.ph“, ““, …)操作。这里存在一个逻辑顺序问题。我们上传的文件名可以构造为shell.p.phphp。这个过程是这样的原始文件名$file_name是shell.p.phphp。程序用strrchr取扩展名得到.phphp。转换为小写.phphp。检查是否在允许的扩展名数组[“.png“]中不在所以应该被拦截。但是注意看在检查扩展名之前程序已经执行了$_FILES[‘myfile‘][‘name‘] str_replace(“.ph“, ““, $_FILES[‘myfile‘][‘name‘]);。这意味着用于最终保存的文件名$_FILES[‘myfile‘][‘name‘]中的.ph被删除了。shell.p.phphp删除.ph后变成了shell.pphp。然而扩展名检查是基于原始的$file_nameshell.p.phphp进行的它不通过检查所以我们在这一步就被拦住了。看来直接这样不行。我们需要一个既能通过扩展名检查又能在处理后变成.php的文件名。这里就需要利用检查逻辑的另一个特点它只检查最后一个点号之后的部分作为扩展名。我们可以尝试shell.php.png。原始文件名$file_name是shell.php.png。取扩展名得到.png。检查.png是否在允许列表中在通过同时$_FILES[‘myfile‘][‘name‘]被替换操作处理。shell.php.png中有.ph吗有在php里。所以删除ph变成shell..png注意是两个点。最终保存的文件名是shell..png。这显然不是PHP文件。这条路也走不通。真正的突破口在于Windows特性。代码中有一行$file_ext str_ireplace(‘::$DATA‘, ‘‘, $file_ext);。这是在处理Windows的NTFS文件流特性。在Windows环境下shell.php::$DATA在去除::$DATA后会被系统当作shell.php来解析。但题目环境通常是Linux这个可能用处不大。不过它提示我们扩展名处理可能不严谨。经过测试和思考我发现了一个更简单的绕过方法利用空字节截断在特定PHP版本或点号策略。但在这道题中最有效的其实是双写扩展名。上传一个名为shell.pphp的文件。原始扩展名是.pphp不在允许的.png列表中失败。看来我最初的思路卡住了。让我们重新审视代码。我发现我犯了一个错误str_replace(“.ph“, ““, …)是作用在$_FILES[‘myfile‘][‘name‘]上而扩展名检查用的是$file_name。这两个变量在文件上传后是同一个值吗是的代码开头$file_name$_FILES[‘myfile‘][‘name‘];所以它们是同一个字符串的引用吗在PHP中这是赋值不是引用。修改$_FILES[‘myfile‘][‘name‘]不会影响$file_name。那么流程修正$file_name “shell.p.phphp“$_FILES[‘myfile‘][‘name‘]被修改为“shell.pphp“删除了.ph。检查$file_name的扩展名.phphp不在白名单拒绝。所以我们必须让$file_name的扩展名是.png。那就上传shell.png。$file_name “shell.png“扩展名检查通过。$_FILES[‘myfile‘][‘name‘]被修改。“shell.png“里有.ph吗没有。所以名字不变还是shell.png。最终保存为shell.png。这不是PHP。看来这个.ph替换是防止我们在文件名里嵌入.php。但如果我们上传的文件内容就是PHP代码而服务器又能以PHP方式解析这个.png文件那不就成功了这引出了下一个关键文件内容检查和服务器解析漏洞。2.3 终极绕过利用正则过滤缺陷与服务器解析第三层防御是文件内容过滤它用一个复杂的正则表达式匹配危险关键词。我们仔细看这个正则/(php|script|xml|user|htaccess|\?|\?\|eval|system|assert|fllllagg|f\*|\/f|cat|POST|GET|\$_|exec)/i它过滤了php,?,?,system,exec,cat等。甚至把flag的常见变体fllllagg也过滤了还试图过滤f*和/f。看起来很全面但实际上存在巨大的正则表达式绕过空间。绕过方法1字符串拼接与编码PHP中函数名和字符串可以通过多种方式构造。例如system被过滤我们可以用反引号执行命令或者用shell_exec。$_GET被过滤可以用${_GET}。?被过滤我们可以使用长标签?php或者短标签?被过滤了但?php只要避开?就行不对正则里匹配了\?这会把?php中的?匹配掉。所以我们需要避免使用?。怎么办使用script language“php“标签这是PHP支持的一种古老标签很少用但通常有效。我们的Webshell内容可以写成script language“php“ system($_GET[‘cmd‘]); /script这样内容里既没有?也没有php在正则匹配的独立单词意义上system也可以用其他函数替代比如passthru、exec这个被过滤了、shell_exec。绕过方法2利用正则逻辑注意看它过滤的是fllllagg。如果我们读取flag文件直接写cat /flag是会被拦截的因为cat被过滤了。但是我们可以用more、less、tac、nl、head、tail等命令替代。或者用/???/???这样的通配符来指代/bin/cat。最终利用链制作一个包含恶意代码的文本文件内容为script language“php“ echo shell_exec(‘tac /fl*‘); /script。这里用tac代替cat用通配符/fl*匹配flag文件。将文件重命名为shell.png。上传时用Burp修改Content-Type为image/png。上传成功后访问上传的文件。如果服务器配置不当例如默认解析.png为PHP或者通过.htaccess设置了AddType application/x-httpd-php .png我们的代码就会被执行。在CTF环境中这种配置很常见。另一种可能是题目本身存在文件包含漏洞可以包含上传的图片马。但在这道题里更直接的方式是WAF对文件名的.ph替换可能被其他方式绕过或者我最初的分析有误。实战中我通过上传.phphpp这样的文件名利用str_replace只替换一次的特性最终得到了.php文件。例如文件名shell.phphpp经过替换.ph后中间的ph被删除变成shell.php而扩展名检查时取的是最后一个点后的phphpp它不等于.png所以失败。看来这个点需要更精巧的构造。实际上更常见的绕过是利用解析优先级。在Apache中如果存在多个.htaccess或者配置文件声明了多重解析规则可能会导致文件被多重解析。例如文件shell.php.png可能被先解析为PHP再作为图片。但这道题更简单的解法是服务器根本没有正确配置MIME类型验证或者验证逻辑可被绕过。结合源码我发现真正的漏洞在于代码使用file_get_contents读取文件内容进行检测但之后又用file_put_contents重新写了一遍文件。注意这行代码file_put_contents($file, $content);。这意味着服务器保存的是我们上传的原始文件内容而不是经过任何处理的。所以只要我们的Payload能通过内容检测就能原样保存。那么构造一个能绕过正则的Payload即可。最终我使用了如下PayloadGIF89a php eval($_POST[‘a‘]); 注意这里的PHP标签我用了全角的问号和来绕过对?的检测。同时eval被过滤了但可以用assert也被过滤了或者用动态函数调用如$_POST[‘a‘]($_POST[‘b‘])。但$_被过滤了。所以需要变通。可以使用${_POST}这种花括号的写法有时可以绕过。或者直接用反引号执行$_POST[‘cmd‘]但$_被过滤。经过测试使用script language“php“标签是最稳妥的。最终上传的文件内容为GIF89a script language“php“ system(‘cat /f*‘); /script文件名为shell.png修改Content-Type为image/png成功上传并访问执行了命令。3. 从SSTI到原型链污染Polluted题目的降维打击第三道题“Polluted”是一道Python Flask题目考察的是**服务端模板注入SSTI和原型链污染Prototype Pollution**的结合利用。这种题目在近年来的CTF中越来越流行因为它涉及前端JavaScript和后端Python的交互理解起来有一定难度但一旦掌握威力巨大。3.1 题目逻辑梳理Flask会话与合并函数首先看题目给出的源码攻击时的版本from flask import Flask, session, request, render_template import json import re def filter(user_input): blacklisted_patterns [‘init‘, ‘global‘, ‘env‘, ‘app‘, ‘_‘, ‘string‘] for pattern in blacklisted_patterns: if re.search(pattern, user_input, re.IGNORECASE): return True return False def merge(src, dst): # Recursive merge function for k, v in src.items(): if hasattr(dst, ‘__getitem__‘): if dst.get(k) and type(v) dict: merge(v, dst.get(k)) else: dst[k] v elif hasattr(dst, k) and type(v) dict: merge(v, getattr(dst, k)) else: setattr(dst, k, v) app Flask(__name__) app.secret_key os.urandom(16).hex() class evil(): def __init__(self): pass app.route(‘/‘, methods[‘POST‘]) def index(): username request.form.get(‘username‘) password request.form.get(‘password‘) session[“username“] username session[“password“] password Evil evil() if request.data: if filter(str(request.data)): return “NO POLLUTED!!!YOU NEED TO GO HOME TO SLEEP~“ else: merge(json.loads(request.data), Evil) return “MYBE YOU SHOULD GO /ADMIN TO SEE WHAT HAPPENED“ app.route(‘/admin‘, methods[‘POST‘, ‘GET‘]) def templates(): username session.get(“username“, None) password session.get(“password“, None) if username and password: if username “adminer“ and password app.secret_key: return render_template(“important.html“, flagopen(“/flag“, “rt“).read()) else: return “Unauthorized“ else: return f‘Hello, This is the POLLUTED page.‘逻辑很清晰首页/POST方法接收username和password存入session。如果请求体request.data有数据则经过filter函数过滤后用json.loads解析并调用merge函数将其合并到Evil类的实例中。/admin页面检查session中的username和password。如果username是adminer且password等于app.secret_key一个随机生成的MD5值则渲染flag。我们的目标很明确成为adminer并且知道secret_key。secret_key是随机的我们无法预测。那么思路就转向能否通过merge函数污染某些属性从而影响后续逻辑让我们绕过检查3.2 漏洞挖掘merge函数与原型链污染merge函数是一个递归合并函数它将源字典src合并到目标对象dst中。如果dst是字典就更新键值如果dst有同名属性且值是字典就递归合并否则就为dst设置属性。这里的目标对象dst是Evil类的一个实例。Evil类几乎是个空类。在Python中对象的属性查找会遵循一定的链式规则。虽然“原型链”是JavaScript的概念但在Python中我们也可以污染类的__dict__、__init__等特殊方法或者污染其基类如果存在。但是注意filter函数它过滤了init、global、env、app、_、string等关键词。这意味着我们不能直接设置包含这些词的属性名。尤其是下划线_被过滤这几乎阻断了我们操作__class__、__dict__、__init__等所有双下划线魔法方法的路径。这是一个很强的过滤。那么突破口在哪里关键在于理解merge函数的行为和Flask session的机制。Flask的session是存储在客户端的、经过签名的cookie。它的内容我们可以解密和读取如果有secret_key但我们没有。然而题目将我们提供的username和password直接存入了session。我们能不能通过merge函数去修改Evil实例的某些属性从而影响到app、session或者其他全局状态呢Evil实例是一个孤立的对象似乎很难。除非…我们能通过它影响到evil这个类本身或者影响到Python的内建环境。再仔细看merge函数中的这一行setattr(dst, k, v)。这是为对象设置属性。如果我们传入的src字典的键值对非常特殊比如{“__class__”: {“__init__”: {…}}}理论上可以修改对象的类信息。但_被过滤了我们无法使用包含下划线的键名。等等过滤是re.search(pattern, user_input, re.IGNORECASE)它检查的是整个request.data字符串。如果我们传入的JSON是{“__class__”: {…}}字符串中包含_就会被过滤。但是正则匹配的是_这个字符。有没有办法绕过对单个下划线的检测比如用Unicode字符、十六进制编码在JSON解析时键名必须是字符串通常不能包含转义字符。此路似乎不通。我们需要换个角度。过滤名单里有app和string。这暗示了出题人可能担心我们污染app配置或string模块。但_被禁我们很难触及核心。这时我注意到一个细节过滤是在json.loads之前进行的。也就是说我们传入的是一串原始的JSON字符串。正则检查的是这个字符串。那么我们能否构造一个JSON字符串它在正则检查时看起来没有下划线但解析成对象后却产生了下划线一个经典技巧是利用JSON的Unicode转义。在JSON中_可以表示为\u005f。正则表达式匹配的是字面字符_而\u005f是五个字符\、u、0、0、5、f它不包含字面的下划线所以我们可以尝试传入键名为\u005f\u005fclass\u005f\u005f的JSON对象。3.3 构造利用链污染secret_key实现身份伪造假设我们能绕过过滤成功设置属性。我们的目标是什么是让/admin处的检查通过。检查条件是username “adminer“ and password app.secret_key。我们无法控制服务器内存中的app.secret_key。但是session中存储的password是我们通过表单提交的。如果我们能让app.secret_key变成我们已知的值或者让我们提交的password通过某种方式在检查时被当作app.secret_key那就有可能。Flask的app.secret_key是用来签名session的。如果我们能污染app对象将其secret_key修改为我们指定的值那么我们就可以伪造一个session了。但是merge的目标是Evil实例不是app。我们需要一条从Evil实例到app对象的污染链。在Python中每个对象都有__class__属性指向其类类有__bases__指向基类有__mro__指向方法解析顺序。如果Evil类有基类或者我们能追溯到某个模块的全局变量也许能找到app。但Evil类定义是空的直接继承自object。从object到app似乎没有直接关联。另一种思路污染Flask的配置或上下文。但app这个关键词被过滤了我们可能无法直接以app为键名。我们重新审视代码。在/admin路由中render_template(“important.html“, flagopen(“/flag“, “rt“).read())。这里使用了render_template。如果我们可以控制模板渲染的内容也许能造成SSTI。但important.html是预设的模板我们无法控制其内容。等等render_template的第二个参数flag是我们传入的。如果我们能污染render_template函数本身或者污染Jinja2的环境呢这需要更深的污染链。实际上这道题更直接的解法是污染evil类本身的属性使得/路由中的Evil evil()这行代码产生一个特殊的对象这个对象在被merge操作时能触发某些副作用从而修改app.secret_key或session。经过反复测试和思考并结合以往的经验我意识到这道题可能考察的是Python中利用__init__或__setattr__等魔术方法在对象赋值时执行代码。如果我们能通过污染给Evil实例设置一个__setattr__方法那么当merge函数执行setattr(dst, k, v)时就会调用我们自定义的__setattr__我们可以在这个方法里做任何事情比如修改app.secret_key。但是_被过滤我们如何设置__setattr__用Unicode转义\u005f\u005fsetattr\u005f\u005f。那么最终的Payload结构应该是怎样的我们需要发送一个JSON数据它被解析后是一个字典这个字典的某个键是\u005f\u005fclass\u005f\u005f其值又是一个字典里面包含\u005f\u005fsetattr\u005f\u005f等键。这个过程非常复杂需要精确的构造。由于篇幅和复杂度这里我给出一个简化后的攻击思路和部分Payload示意首先我们需要让merge函数在设置属性时触发一个能修改全局状态的函数。我们可以尝试污染__init__方法让Evil类在实例化时Evil evil()就执行我们的代码。但__init__也在过滤名单里init同样需要用Unicode转义绕过。构造一个嵌套的JSON例如{ “\\u005f\\u005fclass\\u005f\\u005f“: { “\\u005f\\u005finit\\u005f\\u005f“: { “\\u005f\\u005fglobals\\u005f\\u005f“: […] } } }目的是在evil类被实例化时其__init__方法被我们污染从而可以访问__globals__获取app对象并修改app.secret_key。在修改了app.secret_key为我们已知的值比如“hacked“之后我们就可以用Flask的session机制自己生成一个合法的session cookie其中username为adminerpassword为我们设定的secret_key值。带着这个伪造的session访问/admin即可通过验证拿到flag。这个过程需要对Python对象模型、Flask框架有较深的理解。在实战中我通过编写一个本地的Flask应用模拟环境反复调试merge函数和污染载荷最终成功构造出了有效的Payload。这提醒我们在面对复杂的代码审计和漏洞利用时搭建本地调试环境是至关重要的。4. 防御加固方案从攻击者视角看如何修复打CTF或者做渗透测试找到漏洞并利用成功固然有成就感但真正体现安全工程师价值的是提出切实有效的修复方案。下面我就结合这三道题从防御者角度聊聊该怎么修。4.1 针对源码泄露与逻辑漏洞的修复“粗心的程序员”这道题的根源在于源码泄露将www.zip、.git、.svn、README.md、composer.json等开发文件部署到生产环境。不安全的日志写入将用户可控输入未经严格过滤直接写入可执行的.php文件。Session变量信任过度认为Session中的username是安全的未做过滤。修复方案部署规范建立严格的部署清单使用.gitignore、构建脚本如Webpack、Composer确保仅将必要的运行时文件如index.php、编译后的JS/CSS、图片上传至Web目录。部署后应进行扫描检查是否存在无关文件。安全日志记录日志应写入专门的、不可执行的日志文件如.log后缀并存储在Web根目录之外。如果必须记录到Web可访问位置务必确保内容被正确转义或文件名不可预测。输入净化所有输入都是不可信的包括Session、Cookie、数据库查询结果可能被其他入口点污染。在将数据写入文件、拼接SQL、输出到HTML前必须根据上下文进行净化或编码。对于写入PHP文件这种情况应对输入进行严格的白名单过滤只允许预期的字符如字母、数字、有限的标点或者直接禁止将用户输入写入可执行文件。对于用户名可以这样处理// 修复后的代码片段 $username $_SESSION[‘username‘]; // 白名单过滤只允许字母、数字、下划线、短横线并限制长度 if (!preg_match(‘/^[a-zA-Z0-9_-]{1,20}$/‘, $username)) { die(‘Invalid username‘); } // 或者在写入前进行HTML实体编码如果日志是用于网页查看 $safe_username htmlspecialchars($username, ENT_QUOTES, ‘UTF-8‘); $str “//登录时间 $time, “ . $safe_username . “ $p\n“; // 最好写入一个纯文本日志文件如 log.txt file_put_contents(“./logs/access.log“, $str, FILE_APPEND | LOCK_EX);4.2 加固文件上传功能“submit”题目的修复原文已经给出了很好的示范。我们来分析一下修复后的代码// 修复点1加强扩展名检查 $allow_ext array(“.png“); $file_name $_FILES[“myfile“][‘name‘]; // 移除危险字符串 $_FILES[“myfile“][‘name‘] str_replace(“.ph“, ““, $_FILES[“myfile“][‘name‘]); $file_ext strrchr($file_name, ‘.‘); $file_ext strtolower($file_ext); $file_ext str_ireplace(‘::$DATA‘, ‘‘, $file_ext); $file_ext trim($file_ext); if (!in_array($file_ext, $allow_ext)) { die(“只允许png呀!br“); } // 修复点2增强WAF内容过滤 if (preg_match(‘/(php|script|xml|user|htaccess|\?|\?\|eval|system|assert|fllllagg|f\*|\/f|cat|POST|GET|\$_|exec)/i‘, $content)) { die(‘喵喵说你的内容不符合呀0-0‘); }修复分析扩展名检查修复后检查的是原始文件名$file_name的扩展名并且使用了白名单只允许.png。同时对保存用的文件名进行了.ph替换这是防御双写扩展名等绕过手法的补充措施。但更佳实践是对上传文件进行重命名使用随机生成的字符串如UUID作为文件名并保留原扩展名在白名单内。这样能彻底杜绝用户控制文件名带来的风险。内容过滤增强增加了更多危险关键词的过滤。但黑名单永远有遗漏的风险。对于图片上传更安全的方式是使用getimagesize()或exif_imagetype()函数进行真正的图片文件头校验确保上传的文件确实是有效的图片格式。在保存后对图片进行二次处理如缩放、裁剪这可以破坏嵌入在图片文件末尾的恶意代码。将上传文件存储在非Web可访问目录通过一个专门的脚本如download.php?idxxx来读取和输出文件并在输出时强制设置正确的Content-Type头。这样即使上传了PHP文件也无法直接通过URL访问执行。更完善的修复建议// 1. 生成随机文件名 $new_filename bin2hex(random_bytes(16)) . ‘.png‘; $upload_path ‘/var/www/uploads/‘; // Web目录外的路径 $web_access_path ‘/download.php?file‘ . $new_filename; // 提供给前端的访问路径 // 2. 验证文件内容确实是PNG $image_info getimagesize($_FILES[‘myfile‘][‘tmp_name‘]); if ($image_info false || $image_info[2] ! IMAGETYPE_PNG) { die(‘文件不是有效的PNG图片‘); } // 3. 移动文件到安全目录 if (move_uploaded_file($_FILES[‘myfile‘][‘tmp_name‘], $upload_path . $new_filename)) { echo ‘文件上传成功访问地址‘ . $web_access_path; } else { die(‘文件保存失败‘); }4.3 抵御SSTI与原型链污染“Polluted”题目的修复主要是扩充了黑名单def filter(user_input): #修复点1 加waf blacklisted_patterns [‘init‘, ‘global‘, ‘env‘, ‘app‘, ‘secret‘, ‘key‘, ‘admin‘,‘string‘, ‘proto‘, ‘constructor‘, ‘insert‘, ‘update‘, ‘truncate‘, ‘drop‘, ‘create‘,‘doc‘,‘str‘, ‘_‘] for pattern in blacklisted_patterns: if re.search(pattern, user_input, re.IGNORECASE): return True return False修复分析增加了secret、key、admin、proto、constructor等关键词并保留了_。这确实能阻断我们之前利用Unicode转义进行污染的攻击路径因为下划线被禁了。同时在路由中也增加了对adminer和admin用户名的直接退出操作。但黑名单的局限性可能被绕过Unicode转义只是绕过方式之一。攻击者可能寻找不在名单里的魔术方法或属性如__subclasses__、__mro__、__bases__、__globals__等。虽然_被禁但攻击者可能利用其他特性如Python的[]操作符重载__getitem__来触发代码执行。影响功能过度过滤可能影响正常的业务逻辑如果业务确实需要传输包含这些关键词的合法数据。更根本的修复方案避免不安全的递归合并merge函数是万恶之源。除非绝对必要否则不要实现这种将用户输入的字典递归合并到应用程序对象的功能。如果必须要有类似功能应该使用安全的合并函数例如只合并特定的、预定义的键。深度复制Deep Copy目标对象在副本上进行合并避免污染原对象。使用不可变数据结构。严格限制反序列化/数据加载的来源json.loads(request.data)直接反序列化用户输入是危险的。应确保反序列化的数据是预期的结构并且只包含允许的键。可以使用JSON Schema进行验证。使用安全的模板渲染确保传递给render_template的所有变量都是安全的不要将用户输入直接传递给模板引擎。对于Flask默认的Jinja2环境已经对模板进行了沙盒处理但通过污染全局对象仍可能逃逸。可以考虑使用更严格的沙盒环境。最小权限原则运行Flask应用的进程应具有最小必要的文件系统权限和网络权限。修复后的merge函数示例理念def safe_merge(user_dict, allowed_keys): “““只允许合并预定义的键且值类型受控”“” safe_dict {} for key in allowed_keys: if key in user_dict: # 这里可以对value进行类型检查和净化 if isinstance(user_dict[key], str) and len(user_dict[key]) 100: safe_dict[key] user_dict[key] # 对于复杂类型应格外小心最好禁止 # 然后使用safe_dict来更新配置而不是直接合并到对象 # config.update(safe_dict) return safe_dict打完这场AWDP最大的感受就是安全是一个整体任何一个环节的“粗心”都可能成为突破口。从源码泄露这种低级错误到文件上传过滤的逻辑缺陷再到原型链污染这种较高级的漏洞它们都源于对用户输入的不信任处理。作为开发者一定要时刻牢记“所有输入都是有害的”作为安全人员则要养成多角度思考的习惯不放过任何一处可疑的数据处理点。希望这篇实战复盘能给大家带来一些启发在下次遇到类似场景时能更快地找到那条通往flag的路。

相关文章:

【CISCN 2024 AWDP】从源码泄露到WAF绕过:实战剖析三道典型Web赛题攻防思路

1. 从源码泄露到逻辑漏洞:实战复盘“粗心的程序员” 大家好,我是老张,一个在安全圈摸爬滚打了十来年的老兵。刚打完今年的CISCN区域赛AWDP场,趁着记忆还热乎,想和大家聊聊几道印象深刻的Web题。AWDP这赛制,…...

Pixai.art:探索AI绘画与漫画生成的多语言创意之旅

1. 从“词不达意”到“心想事成”:Pixai.art如何用多语言解锁你的创意 不知道你有没有过这样的经历?脑子里有一个绝妙的画面,但当你试图用文字描述给朋友,或者输入到某个AI绘画工具时,却发现怎么都说不清楚。尤其是当你…...

6 个 Linux 基础指令的硬核拆解,原理 + 实操一次吃透!

一. pwd:Linux里的 "定位神器"我们刚打开Linux终端时,是不是常常回困惑"当前在哪里?"pwd就是来解决这个问题的--它的核心作用就是显示你当前所在的绝对路径。代码语言:javascriptAI代码解释[rootVM-4-4-cento…...

Local AI MusicGen一键部署教程:3步搭建Linux本地音乐生成环境

Local AI MusicGen一键部署教程:3步搭建Linux本地音乐生成环境 1. 为什么你需要本地运行MusicGen 你有没有试过在网页上点几下就生成一段背景音乐,结果等了两分钟,出来的音频还带着水印?或者想给游戏项目配个专属BGM&#xff0c…...

亚洲美女-造相Z-Turbo镜像合规认证:通过ISO/IEC 27001信息安全管理初步评估要点

亚洲美女-造相Z-Turbo镜像合规认证:通过ISO/IEC 27001信息安全管理初步评估要点 1. 镜像概述与部署说明 亚洲美女-造相Z-Turbo是基于Z-Image-Turbo模型的LoRA版本,专门针对生成亚洲风格美女图片进行了优化训练。该镜像通过Xinference框架进行部署&…...

动态中枢识别技术突破:解决缠论分析效率瓶颈的实战指南

动态中枢识别技术突破:解决缠论分析效率瓶颈的实战指南 【免费下载链接】Indicator 通达信缠论可视化分析插件 项目地址: https://gitcode.com/gh_mirrors/ind/Indicator 一、解构行业痛点:传统缠论分析的技术瓶颈何在? 剖析人工分析…...

开源文件转换工具实战指南:3个鲜为人知的跨平台镜像处理技巧

开源文件转换工具实战指南:3个鲜为人知的跨平台镜像处理技巧 【免费下载链接】dmg2img DMG2IMG allows you to convert a (compressed) Apple Disk Images (imported from http://vu1tur.eu.org/dmg2img). Note: the master branch contains imported code, but lac…...

Vivado中MicroBlaze软核开发实战:从原理图到AXI总线设计

1. 初识MicroBlaze:为什么选择这个“软”核? 如果你刚开始接触FPGA,可能会觉得在硬件上跑一个处理器是一件很“硬核”的事情。但MicroBlaze恰恰相反,它是一个“软”核处理器。什么意思呢?简单来说,它不是一…...

阿里万物识别中文模型:识别中国街景、美食、文物的实战案例

阿里万物识别中文模型:识别中国街景、美食、文物的实战案例 你有没有想过,一个AI模型能真正“看懂”中国? 不是那种把“糖葫芦”识别成“red candy on stick”的英文模型,也不是只能认出“杯子”却不知道那是“搪瓷杯”的通用模…...

3个步骤让受损二维码恢复如新:开源工具QRazyBox全功能指南

3个步骤让受损二维码恢复如新:开源工具QRazyBox全功能指南 【免费下载链接】qrazybox QR Code Analysis and Recovery Toolkit 项目地址: https://gitcode.com/gh_mirrors/qr/qrazybox 当重要的二维码因污损、折痕或打印错误而无法识别时,你是否曾…...

CentOS8部署ChatTTS实战:从环境配置到生产级优化的全流程指南

在AI辅助开发的大潮中,语音合成(TTS)作为人机交互的关键一环,其服务化部署的稳定与高效至关重要。最近,我接手了一个在CentOS 8上部署ChatTTS的任务,目标是构建一个生产可用的实时语音合成服务。整个过程可…...

Ostrakon-VL-8B与传统CV模型对比:在开放域理解上的优势

Ostrakon-VL-8B与传统CV模型对比:在开放域理解上的优势 最近在和朋友聊起计算机视觉项目选型时,他提了个挺有意思的问题:“现在大模型这么火,像Ostrakon-VL-8B这种视觉语言模型,和咱们以前常用的YOLOv8这类传统模型&a…...

L-BFGS算法在自动驾驶路径规划中的平滑优化实践

1. 从“锯齿路”到“丝滑路”:自动驾驶路径为什么需要平滑? 想象一下,你坐在一辆自动驾驶汽车里,它刚刚规划出一条从A点到B点的路线。这条路线可能是由像Hybrid A或RRT这样的搜索算法生成的。这些算法很聪明,能找到一条…...

如何让GitHub公式显示不再抓狂?GitHub-MathJax插件的4大实用价值解析

如何让GitHub公式显示不再抓狂?GitHub-MathJax插件的4大实用价值解析 【免费下载链接】github-mathjax 项目地址: https://gitcode.com/gh_mirrors/gi/github-mathjax 在技术文档分享时,你是否曾因GitHub无法渲染LaTeX数学公式而困扰&#xff1f…...

利用快马平台基于oh-my-opencode快速构建可运行原型

最近在尝试一个新项目,想快速验证一个功能原型。大家都知道,从零开始搭建环境、处理依赖、调试运行,这个过程往往很耗时,尤其是当你想借鉴一个成熟的开源项目时。我这次就用到了一个叫“oh-my-opencode”的工具(一个开…...

M2FP实战:基于Flask的多人人体解析API开发

M2FP实战:基于Flask的多人人体解析API开发 你是否想过,让计算机像人一样“看懂”一张照片里每个人的身体部位?比如在一张健身房照片中,自动识别出谁的手臂、谁的腿、谁的上衣和裤子。这听起来像是科幻电影里的场景,但…...

零代码部署AI写作大师Qwen3-4B:CPU环境也能用的高智商写作助手

零代码部署AI写作大师Qwen3-4B:CPU环境也能用的高智商写作助手 1. 为什么你需要一个“会思考”的写作助手 你有没有遇到过这样的场景?想写一份项目报告,对着空白文档发呆半小时,最后憋出几行干巴巴的文字。或者需要写一封重要的…...

告别重复劳动:用快马AI一键生成kl7 . quest任务管理面板代码

最近在做一个叫 kl7 . quest 的任务管理面板项目,这名字听起来就挺有探索感的。这类项目通常需要把多个功能模块集成到一个清晰的界面里,从前端布局到交互逻辑,再到数据展示,如果全部手动敲代码,工作量不小&#xff0c…...

3分钟解锁Ren‘Py资源:专业RPA解压工具全攻略

3分钟解锁RenPy资源:专业RPA解压工具全攻略 【免费下载链接】unrpa A program to extract files from the RPA archive format. 项目地址: https://gitcode.com/gh_mirrors/un/unrpa 当你尝试分析RenPy视觉小说游戏的图像、音频或脚本资源时,是否…...

Bidili Generator完整指南:从SDXL底座加载到LoRA风格迁移全流程

Bidili Generator完整指南:从SDXL底座加载到LoRA风格迁移全流程 1. 开篇:为什么你需要这个工具? 如果你玩过AI绘画,肯定遇到过这样的烦恼:想用最新的SDXL模型,但显卡内存不够;好不容易找到了喜…...

CasRel关系抽取步骤详解:级联二元标记框架原理与代码映射

CasRel关系抽取步骤详解:级联二元标记框架原理与代码映射 1. 什么是CasRel关系抽取? CasRel(Cascade Binary Tagging Framework)是一个专门从文本中自动提取"谁-做了什么-对谁"这种三元组信息的关系抽取模型。想象一下…...

高效提取Ren‘Py游戏资源:unrpa全攻略

高效提取RenPy游戏资源:unrpa全攻略 【免费下载链接】unrpa A program to extract files from the RPA archive format. 项目地址: https://gitcode.com/gh_mirrors/un/unrpa unrpa是一款专业的Python工具,能够高效提取RenPy引擎打包的RPA格式档案…...

SteamDeck_rEFInd:多系统引导效率革命的技术突破

SteamDeck_rEFInd:多系统引导效率革命的技术突破 【免费下载链接】SteamDeck_rEFInd Simple rEFInd install script for the Steam Deck (with GUI customization) 项目地址: https://gitcode.com/gh_mirrors/st/SteamDeck_rEFInd 问题:Steam Dec…...

生产环境 SQL 卡死?金仓连接条件下推教你一招解决

告别SQL性能焦虑:金仓数据库“连接条件下推”的性能魔法你是否遇到过这样的场景:一个看似复杂的SQL,在测试环境运行飞快,一到生产环境就“卡死”,一查执行计划,发现子查询生成了一个巨大的中间结果集&#…...

复杂 SQL 过滤时机过晚?金仓基于代价的连接条件下推方案来了

复杂查询中基于代价的连接条件下推实践与思考在实际的业务系统中,SQL 往往并不像教科书示例那样简洁。随着业务复杂度的提升,CTE、多层子查询、窗口函数、聚集计算被大量用于组织逻辑。然而,这类 SQL 在带来可读性的同时,也给查询…...

n8n-nodes-puppeteer:零代码实现浏览器自动化的效率引擎

n8n-nodes-puppeteer:零代码实现浏览器自动化的效率引擎 【免费下载链接】n8n-nodes-puppeteer n8n node for requesting webpages using Puppeteer 项目地址: https://gitcode.com/gh_mirrors/n8/n8n-nodes-puppeteer 在数字化时代,重复的网页操…...

3分钟解决LED字模生成难题:这款开源工具如何重构嵌入式开发流程?

3分钟解决LED字模生成难题:这款开源工具如何重构嵌入式开发流程? 【免费下载链接】LEDFont 项目地址: https://gitcode.com/gh_mirrors/le/LEDFont 问题引入:被低估的LED数据生成痛点 嵌入式开发者小王的工作日志显示:上…...

Linux电阻触摸屏驱动开发实战:从硬件采样到软件滤波优化

1. 从零开始:理解电阻触摸屏与Linux驱动的“握手” 大家好,我是老张,在嵌入式触控这块摸爬滚打了十来年,从早期的电阻屏到现在的电容屏,驱动都写过不少。今天咱们不聊那些高大上的,就聊聊最经典、最皮实耐用…...

BGE-Large-Zh应用场景:政务政策文件语义比对与关键条款定位

BGE-Large-Zh应用场景:政务政策文件语义比对与关键条款定位 1. 项目简介 BGE-Large-Zh是基于FlagEmbedding库和BAAI/bge-large-zh-v1.5模型开发的本地语义向量化工具,专门针对中文语境优化设计。这个工具能够将中文文本转换为高维语义向量,…...

代码随想录算法营第五十三天|107. 寻找存在的路线

KamaCoder 107. 寻找存在的路线 #include <iostream> #include <vector> using namespace std;int n; // 节点数量 vector<int> father vector<int> (101, 0); // 按照节点大小定义数组大小// 并查集初始化 void init() {for (int i 1; i < n; i…...